Repository navigation
Tests: Migrate internal unit-test linting to Vitest #83089
Description
Follow-up to #80855 and #82975.
Overall goal
Use Vitest lint rules for Gutenberg-owned unit and integration tests, preserve useful existing checks, and adopt additional rules only where they catch real mistakes without excessive noise.
All four internal steps are complete on trunk as of October 1, 2026, through #83502, #83572, #83620 and #83783. The internal configuration uses the public Vitest recommended baseline with explicit internal rules and narrow exceptions. The optional-rule audit adopted JavaScript mock-import checks and deferred the other candidates for the reasons recorded below. Public-default changes remain optional and deferred; the public @wordpress/eslint-plugin test-unit configuration still exports Vitest's recommended rules. This work does not block completion of #80855.
Scope
Cover all discovered Node, jsdom and Browser unit tests, their shared helpers, inline rule directives, and the relevant lint configuration. Preserve jest-dom and Testing Library rules, Browser query allowances, test discovery and environment policy.
Keep the Jest plugin wherever legacy E2E or public consumer configurations still need it. Removing the unit-test override does not by itself justify deleting the dependency. Public defaults change only in the optional separate PR below.
Tasks
Each numbered step is intended to be one independently passing PR. Steps 2 and 3 follow step 1 and may proceed independently. Step 4 follows the internal baseline work. If a genuine test or production defect needs a substantial fix, split it out rather than expanding a lint PR or suppressing the failure.
1. Switch the internal lint baseline
- Replace Jest rules on Vitest suites and shared unit-test helpers with the public Vitest baseline plus explicit internal overrides.
Refresh the diagnostic inventory on current trunk using the full repository configuration and locked plugin version. Compare effective rules before and after the switch. Preserve existing enforcement; carry forward justified disabled rules with reasons and links to steps 2 and 3. Configure assertion helpers and type-only test exceptions narrowly. Update applicable inline directives and obsolete comments. Add focused configuration checks for representative Node, jsdom, Browser and shared-helper files, and verify that legacy E2E rules remain intact. Update workspace dependencies and the retained-Jest inventory only when actual consumers change.
Done when the full repository lint and convention checks pass, unit tests use Vitest rules, and every remaining internal override has an explicit reason. Do not disable expect-expect globally or increase blanket suppression counts to make the switch pass.
Pull requests:
- Tests: Migrate internal linting to Vitest and remove unused Jest dependencies #83502 merged into trunk on September 25, 2026, as
45b233da4f463221ec8d457104d0241861694221. - E2E Tests: Remove unused Jest peer dependency #83505 removed the obsolete
@wordpress/e2e-testsJest peer and its retained-Jest inventory entry. It was merged into Tests: Migrate internal linting to Vitest and remove unused Jest dependencies #83502, which contains both changes.
The trunk implementation meets step 1. It includes effective-config and fixture checks for Node, jsdom, Browser, shared helpers and legacy E2E. The merged PR's full JavaScript lint, conventions, routing, typecheck, build and unit-test checks passed. No bulk suppression counts increased.
Implementation details that refine the original plan:
- At the step 1 merge,
expect-expectremained a warning andno-conditional-expectremained disabled for suites. Step 2 has since enabled both at error severity. Assertion helpers are recognized only in their owning files; type-only checks and the intentionally assertion-free console failure fixture have local directives. - At the step 1 merge, the parser's shared helper retained stricter rules while
valid-expect-in-promise,valid-describe-callbackandvalid-titleremained disabled for other suites. Step 3 has since enabled these rules across suites and shared helpers and removed the parser-specific configuration. The console setup'sno-done-callbackexception is now a local directive beside the awaitedaroundEachcallback; see step 3 for the remaining plugin limitations. - The public baseline intentionally raises
no-commented-out-testsfrom warning to error and addsno-import-node-test,no-unneeded-async-expect-function,prefer-called-exactly-once-withandrequire-local-test-context-for-concurrent-snapshots. These had no violations in the refreshed inventory. - Vitest has no direct replacements for all former Jest checks. The convention validator now rejects suite exports, and ESLint core rules preserve Jasmine-global restrictions.
- The internal lint workspace's unused direct Jest-plugin dependency and the unused E2E Jest peer were removed. Jest support for actual legacy consumers remains. The E2E peer removal is a public metadata cleanup; public lint defaults did not change.
The refreshed full-config inventory is recorded in #83502. At its pinned trunk baseline, the unmodified Vitest configuration reported 108 expect-expect, 86 no-conditional-expect, 13 valid-title, six valid-expect-in-promise, five valid-describe-callback and four no-disabled-tests diagnostics across 1,189 files. These include valid helper and type-only patterns and are not the final merged lint output.
2. Tighten assertion coverage
- Resolve actionable
expect-expectandno-conditional-expectfindings and enable the checks wherever they can reliably enforce the intended behavior.
Distinguish runtime assertions, custom assertion helpers and compile-time TypeScript checks. Fix tests whose assertions can be skipped without failing. Preserve meaningful type-only coverage and avoid adding dummy assertions just to satisfy lint. Keep valid helper patterns supported through narrow configuration. Document any justified exceptions that remain.
Done when affected suites pass, the selected checks are active, and tests still exercise the same behavior. This PR should not include broad matcher-style rewrites.
PR #83572 merged into trunk on September 28, 2026, as cfba3fee987c669ffecbdd1c48d53d660e30e051. The trunk configuration enables both checks at error severity for internal unit tests and shared helpers. Effective-configuration checks cover representative Node, jsdom, Browser, shared-helper and Tabs files, including missing and conditional assertions. Custom assertion helpers remain file-scoped, type-only tests retain local exceptions, and justified conditional cases in Tabs and fixture notices have scoped directives. Tests with skippable assertions were repaired, including a resolver invalidation test that now verifies the resolver runs again. The merged PR's JavaScript, Browser and unit-test CI checks passed.
3. Resolve promise, callback and title compatibility
- Reassess the previously disabled
valid-expect-in-promise,valid-describe-callbackandvalid-titleequivalents, fix genuine problems, and enable each rule where supported patterns remain valid.
Cover promises awaited through Promise.all, returned assertion helpers, generated test titles and shared callback factories. Reassess the file-specific no-done-callback exception for aroundEach in the console setup. Test both valid and invalid examples before changing enforcement. Evaluate require-awaited-expect-poll here as an optional correctness rule: the earlier audit flagged Menu helpers that return expect.element promises and are awaited by callers. Do not treat those as unawaited assertions. Where the plugin cannot distinguish a valid pattern, record a narrow exception or upstream reproduction rather than rewriting correct behavior to appease the rule.
Done when each candidate has an explicit enable/defer decision, enabled rules have focused configuration coverage, and affected suites pass.
PR #83620 merged into trunk on September 29, 2026, as 9679b5dc9fa587c0e56422e833895c2cacc649ae. It enables valid-expect-in-promise, valid-describe-callback, valid-title and the optional require-awaited-expect-poll rule at error severity for internal unit tests and shared helpers. Generated titles remain supported through valid-title's allowArguments: true option. Parser and console suites now use inline registration callbacks, and Menu assertion helpers use explicit returns that preserve the promises awaited by their callers.
Two plugin limitations retain local directives, with no file-wide configuration exceptions for these cases:
- The lock tests have six
valid-expect-in-promisedisables because the rule stops following assertion promises at an intervening assignment before the awaitedPromise.all. The tests still await those promises. - The console setup retains one
no-done-callbackdisable because the deprecated rule mistakes the awaitedaroundEachrunTestcallback for a done callback.
The configuration validator covers effective rules for Node, jsdom, Browser and shared-helper files, valid and invalid callbacks/titles, awaited and floating assertions, returned Browser assertions, and the intervening-assignment limitation. The merged PR's static-analysis checks, including JavaScript lint, typecheck, routing, conventions and build, and unit-test checks, including all Node 24/26 shards and Chromium, passed.
4. Adopt selected optional internal rules
- Audit the remaining non-recommended rules against the resulting baseline, then enable only a small, justified set with its required cleanup.
PR #83783 merged into trunk on October 1, 2026, as 0aa166091e687ec64dbb95f534baf6ee22932ea6. It enables vitest/prefer-import-in-mock at error severity for JavaScript unit tests and shared helpers. The cleanup converted 72 mock paths in 41 files to import-based paths, allowing the existing import/no-unresolved rule to catch unresolved modules. Three mocks of unexported package internals now use relative paths to the same source modules. Factories and hoisting behavior are preserved.
TypeScript retains the stronger typescript-vi-mock convention in vitest-policy-rules.mjs, which also checks nonliteral arguments and namespace aliases that the plugin misses. TypeScript vi.doMock() remains outside that existing convention; expanding it is a separate decision. require-awaited-expect-poll, enabled by #83620, and the step 3 local exceptions remain unchanged. No new suppressions or dependencies were added.
The configuration validator covers string and import-based mocks, unresolved paths, aliased imports, and the unchanged TypeScript and legacy E2E scope. The merged PR's static-analysis checks, including lint, typecheck, routing, conventions and build, and unit-test checks, including all Node 24/26 shards and Chromium, passed.
Optional-rule decisions
The September 29 audit used trunk 9679b5dc9fa587c0e56422e833895c2cacc649ae, ESLint 10.10.0 and locked @vitest/eslint-plugin 1.6.27. It tried 60 inactive rules with default options through the full repository configuration across 1,241 discovered tests and matching helper and infrastructure files, preserving inline directives. unbound-method was assessed separately because the configuration does not provide TypeScript parser services. Diagnostic counts were audit signals, not confirmed defects. The audit file was removed before merge; these decisions are recorded here.
| Rule or group | Decision |
|---|---|
prefer-import-in-mock |
Adopt for JavaScript. Keep the stronger TypeScript convention and defer any expansion to TypeScript vi.doMock(). |
consistent-test-filename, prefer-importing-vitest-globals |
Defer. Routing and convention validators already own discovery, explicit imports, aliases and shadowed bindings. The parser helper legitimately receives test APIs through a runner argument. Equivalent replacement coverage was not established. |
no-conditional-tests, no-conditional-in-test |
Defer. Findings include valid fixed-variant registration, conditional setup and fixtures, plus a false positive on RegExp.test(). Step 2 already enforces unconditional assertions. |
no-duplicate-hooks, require-hook |
Defer. Findings include independent global-mock helper registrations and module-level setup, without an established lifecycle defect. |
no-test-return-statement |
Defer. It rejects returned promises that Vitest waits for, including valid assertion patterns preserved in step 3. |
prefer-vi-mocked, require-mock-type-parameters |
Defer. Existing casts can narrow generic store signatures; blanket mock type arguments reject useful inference. |
require-to-throw-message |
Defer. Some rejection-contract tests intentionally do not fix the error message. Assess stronger assertions per test. |
unbound-method |
Defer. It requires type-aware linting and an audit of overlap with the base TypeScript rule. |
no-hooks, no-importing-vitest-globals |
Reject. They conflict with repository hook and explicit-import conventions. |
no-restricted-matchers, no-restricted-vi-methods |
Leave unconfigured. No additional forbidden matcher or API was identified. |
| Naming, formatting, ordering and matcher preferences | Defer. consistent-each-for, consistent-test-it, consistent-vitest-vi, hoisted-apis-on-top, padding-around-* and remaining prefer-* rules mainly rewrite accepted style without a demonstrated benefit. |
max-expects, max-nested-describe, no-large-snapshots, prefer-expect-assertions, require-test-timeout, require-top-level-describe |
Defer. Their defaults impose limits, redundant assertion counts or extra structure without a demonstrated coverage gain. |
warn-todo |
Leave disabled. The trial found no diagnostics. Existing disabled-test warnings remain unchanged. |
Revisit deferred rules when recurring problems provide evidence for adopting them. See the review discussion.
Optional separate PR: public lint defaults
- Promote only broadly useful rules demonstrated by the internal work to
packages/eslint-plugin/configs/test-unit.js.
PR #83995
Deferred. No public-default promotion is included in #83620 or #83783. The public configuration still exports Vitest's recommended baseline. This optional work does not block completion of the internal migration.
Keep repository-specific conventions internal. Verify flat configuration and the supported legacy configuration path with ESLint 9/10 consumer checks, preserve the documented legacy Jest setup, add package changelog/documentation updates, and assess the release boundary for newly failing lint rules. This step can be deferred without blocking the internal migration.
Verification and completion
Each implementation PR must pass relevant lint, test routing/conventions and focused affected suites. Run typecheck and build when changed code or types require them; dependency/export changes also need the repository's dependency checks. Preserve assertions, test isolation and public compatibility. Prune obsolete suppressions rather than adding blanket exemptions.
The internal completion criteria are met: the switch is complete, optional-rule decisions are documented, and accepted checks passed verification. The step 3 upstream limitations remain covered by local directives. Public-default promotion and the other deferred candidates are optional follow-ups, with no further internal implementation required by this issue.
Starting evidence, to refresh during step 1
The September 17 standalone syntax-only audit at 19ad6fcbe3be3402538918d3a1547a9afeccca93 checked 1,170 discovered files with @vitest/eslint-plugin 1.6.27 and ESLint 10.10.0, without existing inline suppressions. Recommended rules produced 210 diagnostics: 108 expect-expect, 77 no-conditional-expect, 13 valid-title, six valid-expect-in-promise, four no-disabled-tests, and two valid-describe-callback. These are workload signals, not 210 confirmed bugs or the output of the full repository configuration.
The optional-rule trial reported 64 prefer-import-in-mock, 737 prefer-called-once, 652 prefer-to-be, and 3,043 require-mock-type-parameters diagnostics. Counts alone do not justify adopting a rule. Keep intentional skipped tests traceable; enabling a warning rule does not require fixing unrelated product bugs within this issue.
Activity
youknowriad commentedon Oct 6, 2026
My AI agent is telling me that everything is done on this issue. Do you confirm?
ciampo commentedon Oct 7, 2026
Yes, we can close. Thank you checking in!