Skip to content

max-expects, no-standalone-expect, and require-hook: false positives with it.extend when callback omits custom fixture properties #891

@gtbuchanan

Description

@gtbuchanan

Description

vitest/max-expects, vitest/no-standalone-expect, and vitest/require-hook produce false positives when using it.extend() and the test callback destructures only { expect } without any custom fixture properties. The rules fail to recognize the it(...) call as a test boundary, so:

  • max-expects: assertion counts accumulate across tests instead of resetting
  • no-standalone-expect: expects are reported as "outside a test block"
  • require-hook: it(...) calls are reported as setup code that "should be done within a hook"

When the callback does destructure a custom fixture property (e.g., { value, expect }), all three rules work correctly.

Reproduction

import { it as base, describe } from 'vitest';

const it = base.extend<{ value: string }>({
  value: async ({}, use) => {
    await use('hello');
  },
});

// BUG: destructures { expect } only — no custom fixture properties
describe('without fixture property', () => {
  it('test A — 6 expects', ({ expect }) => {
    expect(1).toBe(1);
    expect(1).toBe(1);
    expect(1).toBe(1);
    expect(1).toBe(1);
    expect(1).toBe(1);
    expect(1).toBe(1); // max-expects fires here (correct)
  });

  it('test B — count should reset but does not', ({ expect }) => {
    expect(1).toBe(1); // max-expects: "Too many assertion calls (7)"
    // no-standalone-expect: "Expect must be called inside a test block"
  });
  // require-hook: both it() calls are reported as
  // "This should be done within a hook"
});

// OK: destructures { value, expect } — includes custom fixture property
describe('with fixture property', () => {
  it('test C — 6 expects', ({ value, expect }) => {
    expect(value).toBe('hello');
    expect(value).toBe('hello');
    expect(value).toBe('hello');
    expect(value).toBe('hello');
    expect(value).toBe('hello');
    expect(value).toBe('hello'); // max-expects fires here (correct)
  });

  it('test D — count resets correctly', ({ value, expect }) => {
    expect(value).toBe('hello'); // no violation (correct)
  });
  // no require-hook violations (correct)
});

Expected

Both describes should behave the same — the presence or absence of custom fixture properties in the destructuring pattern shouldn't affect whether it(...) is recognized as a test boundary.

Actual

"without fixture property" describe:

  • test A and test B: require-hook reports "This should be done within a hook"
  • test B: max-expects reports "Too many assertion calls (7)" — count accumulated from test A
  • test B: no-standalone-expect reports "Expect must be called inside a test block"

"with fixture property" describe:

  • No require-hook violations
  • test D: no max-expects or no-standalone-expect violations — count correctly reset

Root cause

The rules appear to use the destructured parameters to determine whether a callback is a test function. When the callback only destructures { expect } (a built-in vitest property), the rules don't recognize the it(...) call as a test boundary. When a custom fixture property like { value, expect } is present, the rules correctly identify the test boundary.

Environment

  • @vitest/eslint-plugin: 1.6.14
  • eslint: 10.x
  • vitest: 4.x

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions