From 26040410e987f09c40c051c24c91f8ab1875991a Mon Sep 17 00:00:00 2001 From: Marco Ciampini Date: Tue, 1 Sep 2026 18:11:41 +0200 Subject: [PATCH 1/4] Test: Enforce JavaScript test runner ownership --- .github/workflows/static-checks.yml | 13 +- .github/workflows/unit-test.yml | 22 - docs/contributors/code/testing-overview.md | 2 +- package.json | 2 +- test/unit/VITEST_MIGRATION.md | 33 +- test/unit/config/setup-globals.vitest.js | 212 ++++++ test/unit/jest.config.js | 16 +- test/unit/scripts/discover-test-files.mjs | 29 +- test/unit/scripts/resolve-type-roots.mjs | 25 + .../scripts/test-infrastructure-policy.mjs | 96 +++ .../scripts/test/discover-test-files.test.js | 39 +- .../test/test-infrastructure-policy.test.js | 364 +++++++++++ test/unit/scripts/validate-test-routing.mjs | 143 ++-- .../scripts/validate-vitest-conventions.mjs | 50 +- test/unit/test-migration.json | 610 +++++++++++++++++- .../gutenberg-vitest-test-env/index.d.ts | 6 +- tools/eslint/config.mjs | 27 +- 17 files changed, 1490 insertions(+), 199 deletions(-) create mode 100644 test/unit/scripts/resolve-type-roots.mjs create mode 100644 test/unit/scripts/test-infrastructure-policy.mjs create mode 100644 test/unit/scripts/test/test-infrastructure-policy.test.js diff --git a/.github/workflows/static-checks.yml b/.github/workflows/static-checks.yml index fe38378bef41a7..904c1a978c17d5 100644 --- a/.github/workflows/static-checks.yml +++ b/.github/workflows/static-checks.yml @@ -48,14 +48,13 @@ jobs: os: 'macos-15' steps: - # The BASE branch commit must be present for the check-root-dependencies script to read the package.json and - # compare with the HEAD branch. Fetching a depth of 2 ensures that's present. - # All other jobs only require a depth of 1. + # The parent commit must be present for dependency and test-routing + # checks that compare the current change with its baseline. - name: Checkout repository uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: show-progress: ${{ runner.debug == '1' && 'true' || 'false' }} - fetch-depth: ${{ matrix.annotations && github.event_name == 'pull_request' && 2 || 1 }} + fetch-depth: 2 persist-credentials: false - name: Validate labeler config @@ -119,6 +118,12 @@ jobs: - name: Type checking run: npm run typecheck + - name: Validate JavaScript test routing + if: ${{ matrix.node == '20' && matrix.os == 'ubuntu-24.04' }} + env: + ROUTING_BASE: ${{ github.event_name == 'pull_request' && github.event.pull_request.base.sha || 'HEAD^1' }} + run: npm run test:unit:routing -- --base="$ROUTING_BASE" + - name: Build packages run: npm run build diff --git a/.github/workflows/unit-test.yml b/.github/workflows/unit-test.yml index d14cca211bd675..2457347ff80075 100644 --- a/.github/workflows/unit-test.yml +++ b/.github/workflows/unit-test.yml @@ -24,28 +24,6 @@ concurrency: permissions: {} jobs: - unit-js-routing: - name: JavaScript test routing - runs-on: 'ubuntu-24.04' - permissions: - contents: read - if: ${{ github.repository == 'WordPress/gutenberg' || github.event_name == 'pull_request' }} - - steps: - - name: Checkout repository - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - show-progress: ${{ runner.debug == '1' && 'true' || 'false' }} - persist-credentials: false - - - name: Setup Node.js and install dependencies - uses: ./.github/setup-node - with: - node-version: '20' - - - name: Validate JavaScript test routing - run: npm run test:unit:routing - unit-js: name: JavaScript (Node.js ${{ matrix.node }}) ${{ matrix.shard }} runs-on: 'ubuntu-24.04' diff --git a/docs/contributors/code/testing-overview.md b/docs/contributors/code/testing-overview.md index e5bef3250a8b91..ba0516147867d7 100644 --- a/docs/contributors/code/testing-overview.md +++ b/docs/contributors/code/testing-overview.md @@ -39,7 +39,7 @@ During the Jest-to-Vitest migration, run both `npm run test:unit` and `npm run t Keep your tests in a `test` folder in your working directory. The test file should have the same name as the test subject file. -Use `*.jsdom.test.*` for tests that only require a virtual DOM. Use `*.browser.test.*` for tests that require Vitest Browser Mode. Leave Node-compatible test names without an environment suffix. Node tests run in Vitest automatically. The filename selects the test environment in both Jest and Vitest. During the remaining migration, the manifest selects the runner only for JSDOM and Browser Mode tests. Do not use per-file environment overrides. +Use `*.jsdom.test.*` for tests that only require a virtual DOM. Use `*.browser.test.*` for tests that require Vitest Browser Mode. Leave Node-compatible test names without an environment suffix. Every new test runs in Vitest automatically. The filename selects its environment. During the remaining migration, the manifest lists only legacy JSDOM tests that still run in Jest. Do not use per-file environment overrides. ``` +-- test diff --git a/package.json b/package.json index 54262d04368c52..01563ad1542c6e 100644 --- a/package.json +++ b/package.json @@ -181,7 +181,7 @@ "test:unit:vitest": "npm run --workspace @wordpress/unit-tests test:unit:vitest --", "test:unit:vitest:update": "npm run --workspace @wordpress/unit-tests test:unit:vitest:update --", "test:unit:vitest:watch": "npm run --workspace @wordpress/unit-tests test:unit:vitest:watch --", - "test:unit:routing": "npm run --workspace @wordpress/unit-tests test:unit:routing", + "test:unit:routing": "npm run --workspace @wordpress/unit-tests test:unit:routing --", "test:unit:date": "bash ./bin/unit-test-date.sh", "test:unit:debug": "npm run --workspace @wordpress/unit-tests test:unit:debug --", "test:unit:profile": "npm run --workspace @wordpress/unit-tests test:unit:profile --", diff --git a/test/unit/VITEST_MIGRATION.md b/test/unit/VITEST_MIGRATION.md index 400f23db6b8343..c47ac02e4eda5f 100644 --- a/test/unit/VITEST_MIGRATION.md +++ b/test/unit/VITEST_MIGRATION.md @@ -1,20 +1,22 @@ # Vitest migration routing -The migration manifest in `test-migration.json` keeps every JavaScript unit and -integration test assigned to exactly one runner while the repository moves from -Jest to Vitest. +Every new JavaScript unit and integration test runs in Vitest. The migration +manifest in `test-migration.json` contains the remaining legacy Jest tests and +Jest-only infrastructure while the repository finishes moving them to Vitest. The routing validator derives the current test inventory from both runners and the repository's static test-file patterns. It does not depend on a fixed test count, so unrelated test additions and removals do not require migration metadata updates. -When changing test ownership: +When writing or migrating a test: -- Node tests run in Vitest automatically. They do not need migration metadata. -- Add migrated JSDOM and Browser Mode tests to `vitest.files`, or use - `vitest.directories` when an entire directory can move as one independently - revertible unit. +- New Node, JSDOM, and Browser Mode tests run in Vitest automatically. They do + not need migration metadata. +- When migrating a legacy Jest test, remove its exact path from `jest.files`. +- Do not add directories, glob patterns, or new files to `jest.files`. The + routing validator compares it with the pull request base and accepts only + removals. - Name tests `*.jsdom.test.*` to use JSDOM or `*.browser.test.*` to use Browser Mode. - Use Node for pure JavaScript, data, stores, schemas, build tooling, and @@ -30,9 +32,6 @@ When changing test ownership: - Before running Browser Mode tests locally for the first time, install Chromium with `npm exec --workspace @wordpress/unit-tests -- playwright install chromium`. -- New Node tests run in Vitest without migration metadata. A new - `*.jsdom.test.*` runs in Jest until it is assigned to Vitest, and a new - `*.browser.test.*` must be assigned to Vitest so it can run in Browser Mode. - Do not use per-file Jest or Vitest environment overrides. The filename is the single source of truth. - Run `npm test` for the complete lint and unit-test suite. For focused @@ -40,7 +39,11 @@ When changing test ownership: `npm run test:unit:conventions`, `npm run test:unit`, and `npm run test:unit:vitest` together. -The routing validator fails when a test is missing, owned by both runners, or -not assigned to the expected Vitest migration entry. It also rejects per-file -environment overrides, invalid manifest entries, and static/executable -discovery mismatches. +The required `All` CI check runs the routing validator. It fails when a test is +missing, owned by both runners, or does not match the exact legacy Jest +allowlist. It also rejects per-file environment overrides, invalid manifest +entries, and static/executable discovery mismatches. The convention validator +also rejects Vitest isolation opt-outs and global Vitest APIs. + +`wpVitest` remains an explicit opt-in for jsdom suites that need hoist-safe +helpers inside `vi.hoisted()`. diff --git a/test/unit/config/setup-globals.vitest.js b/test/unit/config/setup-globals.vitest.js index a97eb2cf701c95..2576a44ca53e97 100644 --- a/test/unit/config/setup-globals.vitest.js +++ b/test/unit/config/setup-globals.vitest.js @@ -1,3 +1,215 @@ +import ResizeObserverPolyfill from 'resize-observer-polyfill'; +import { afterAll, beforeAll, vi } from 'vitest'; + +function mockCSSSupports() { + const originalCSS = globalThis.CSS; + const originalSupports = globalThis.CSS?.supports; + + beforeAll( () => { + if ( ! globalThis.CSS ) { + Reflect.set( globalThis, 'CSS', {} ); + } + Reflect.set( + globalThis.CSS, + 'supports', + vi.fn( () => false ) + ); + } ); + + afterAll( () => { + if ( originalSupports ) { + Reflect.set( globalThis.CSS, 'supports', originalSupports ); + } else if ( originalCSS ) { + Reflect.deleteProperty( globalThis.CSS, 'supports' ); + } else { + Reflect.deleteProperty( globalThis, 'CSS' ); + } + } ); +} + +function mockMatchMedia() { + if ( typeof window === 'undefined' ) { + return; + } + + const originalMatchMedia = window.matchMedia; + const mockedMatchMedia = vi.fn( ( query ) => ( { + matches: /prefers-reduced-motion/.test( query ), + media: query, + onchange: null, + addListener: vi.fn(), + addEventListener: vi.fn(), + dispatchEvent: vi.fn(), + removeListener: vi.fn(), + removeEventListener: vi.fn(), + } ) ); + + Reflect.set( window, 'matchMedia', mockedMatchMedia ); + + afterAll( () => { + if ( originalMatchMedia ) { + Reflect.set( window, 'matchMedia', originalMatchMedia ); + } else { + Reflect.deleteProperty( window, 'matchMedia' ); + } + } ); +} + +function mockResizeObserver() { + const originalResizeObserver = globalThis.ResizeObserver; + + beforeAll( () => { + Reflect.set( globalThis, 'ResizeObserver', ResizeObserverPolyfill ); + } ); + + afterAll( () => { + if ( originalResizeObserver ) { + Reflect.set( globalThis, 'ResizeObserver', originalResizeObserver ); + } else { + Reflect.deleteProperty( globalThis, 'ResizeObserver' ); + } + } ); +} + +function mockPointerEvent() { + const originalPointerEvent = globalThis.PointerEvent; + + beforeAll( () => { + class PointerEvent extends globalThis.MouseEvent { + constructor( type, init = {} ) { + super( type, init ); + this.pointerId = init.pointerId ?? 0; + this.pointerType = init.pointerType ?? ''; + this.isPrimary = init.isPrimary ?? false; + } + } + + Reflect.set( globalThis, 'PointerEvent', PointerEvent ); + } ); + + afterAll( () => { + if ( originalPointerEvent ) { + Reflect.set( globalThis, 'PointerEvent', originalPointerEvent ); + } else { + Reflect.deleteProperty( globalThis, 'PointerEvent' ); + } + } ); +} + +function mockScrollIntoView() { + const originalScrollIntoView = globalThis.Element.prototype.scrollIntoView; + + beforeAll( () => { + Reflect.set( globalThis.Element.prototype, 'scrollIntoView', vi.fn() ); + } ); + + afterAll( () => { + if ( originalScrollIntoView ) { + Reflect.set( + globalThis.Element.prototype, + 'scrollIntoView', + originalScrollIntoView + ); + } else { + Reflect.deleteProperty( + globalThis.Element.prototype, + 'scrollIntoView' + ); + } + } ); +} + +class FakeDOMRectList extends Array { + /** + * @param {number} index Index of the rectangle to return. + * @return {DOMRect | null} The rectangle at the requested index. + */ + item( index ) { + return this[ index ] ?? null; + } +} + +/** + * @param {Element} element Element to inspect. + * @return {boolean} Whether the element has a layout box. + */ +function hasAssociatedLayoutBox( element ) { + if ( ! element.isConnected ) { + return false; + } + + let current = element; + while ( current ) { + if ( + current instanceof globalThis.HTMLElement && + ( current.hidden || current.style.display === 'none' ) + ) { + return false; + } + + if ( + current === element && + current instanceof globalThis.HTMLElement && + current.style.display === 'contents' + ) { + return false; + } + + current = current.parentElement; + } + + return true; +} + +function mockVisibleElements() { + const originalGetClientRects = globalThis.Element.prototype.getClientRects; + + beforeAll( () => { + Reflect.set( + globalThis.Element.prototype, + 'getClientRects', + function () { + const rects = []; + + if ( hasAssociatedLayoutBox( this ) ) { + rects.push( { + bottom: 1, + height: 1, + left: 0, + right: 1, + top: 0, + width: 1, + x: 0, + y: 0, + } ); + } + + return new FakeDOMRectList( ...rects ); + } + ); + } ); + + afterAll( () => { + Reflect.set( + globalThis.Element.prototype, + 'getClientRects', + originalGetClientRects + ); + } ); +} + +// Keep these opt-in mocks on `globalThis` so `vi.hoisted()` can call them +// synchronously before static imports run. +globalThis.wpVitest = { + mockCSSSupports, + mockMatchMedia, + mockPointerEvent, + mockResizeObserver, + mockScrollIntoView, + mockVisibleElements, + timers: vi, +}; + if ( typeof globalThis.window !== 'undefined' ) { globalThis.window.tinyMCEPreInit = { baseURL: 'about:blank', diff --git a/test/unit/jest.config.js b/test/unit/jest.config.js index 1542f04d312d86..e6300ba02be0f4 100644 --- a/test/unit/jest.config.js +++ b/test/unit/jest.config.js @@ -7,17 +7,6 @@ const testMigration = require( './test-migration.json' ); */ const ROOT_DIR = path.resolve( __dirname, '../..' ); -const escapeRegExp = ( value ) => - value.replace( /[.*+?^${}()|[\]\\]/g, '\\$&' ); -const vitestTestPathIgnorePatterns = [ - ...testMigration.vitest.files.map( - ( testPath ) => `/${ escapeRegExp( testPath ) }$` - ), - ...testMigration.vitest.directories.map( - ( directoryPath ) => `/${ escapeRegExp( directoryPath ) }/` - ), -]; - // Ensure Babel config resolution works from the repo root, // even when Jest runs from the workspace directory. process.chdir( ROOT_DIR ); @@ -114,7 +103,6 @@ const commonProjectConfig = { '/.*/build-module/', '/.*/build-types/', '/.+\\.d\\.ts$', - ...vitestTestPathIgnorePatterns, ], resolver: '/test/unit/scripts/resolver.js', transform: { @@ -144,7 +132,9 @@ module.exports = { testEnvironmentOptions: { url: 'http://localhost/', }, - testMatch: [ '**/*.jsdom.test.[jt]s?(x)' ], + testMatch: testMigration.jest.files.map( + ( testPath ) => `/${ testPath }` + ), }, ], watchPlugins: [ diff --git a/test/unit/scripts/discover-test-files.mjs b/test/unit/scripts/discover-test-files.mjs index e7630d56abf712..424ff55cf0ef5c 100644 --- a/test/unit/scripts/discover-test-files.mjs +++ b/test/unit/scripts/discover-test-files.mjs @@ -45,24 +45,19 @@ export function discoverTestFiles( rootDir ) { } export function getVitestTests( discoveredTests, manifest ) { - const nodeTests = discoveredTests.filter( - ( testPath ) => getTestEnvironmentName( testPath ) === 'node' - ); - const directoryTests = discoveredTests.filter( ( testPath ) => - manifest.vitest.directories.some( - ( directoryPath ) => - testPath === directoryPath || - testPath.startsWith( `${ directoryPath }/` ) - ) - ); + const jestTests = new Set( manifest.jest.files ); - return [ - ...new Set( [ - ...nodeTests, - ...manifest.vitest.files, - ...directoryTests, - ] ), - ].sort(); + return discoveredTests + .filter( ( testPath ) => ! jestTests.has( testPath ) ) + .sort(); +} + +export function findAddedLegacyJestTests( currentTests, baselineTests ) { + const baselineTestSet = new Set( baselineTests ); + + return currentTests + .filter( ( testPath ) => ! baselineTestSet.has( testPath ) ) + .sort(); } export function getTestEnvironmentName( testPath ) { diff --git a/test/unit/scripts/resolve-type-roots.mjs b/test/unit/scripts/resolve-type-roots.mjs new file mode 100644 index 00000000000000..dba9ebd9fd2b40 --- /dev/null +++ b/test/unit/scripts/resolve-type-roots.mjs @@ -0,0 +1,25 @@ +import path from 'node:path'; + +/** + * Resolve the directories holding the given `@types` packages. + * + * @param {string[]} typeNames Package names without the `@types/` scope. + * @param {Function} resolvePackage Resolver scoped to the owning workspace. + * @return {string[]} Absolute `@types` directories, without duplicates. + */ +export function resolveTypeRoots( typeNames, resolvePackage ) { + const typeRoots = new Set(); + + for ( const typeName of typeNames ) { + try { + const packageJsonPath = resolvePackage( + `@types/${ typeName }/package.json` + ); + typeRoots.add( path.dirname( path.dirname( packageJsonPath ) ) ); + } catch { + // Declared under `typings` rather than by an `@types` package. + } + } + + return [ ...typeRoots ]; +} diff --git a/test/unit/scripts/test-infrastructure-policy.mjs b/test/unit/scripts/test-infrastructure-policy.mjs new file mode 100644 index 00000000000000..30c200d8a00d3e --- /dev/null +++ b/test/unit/scripts/test-infrastructure-policy.mjs @@ -0,0 +1,96 @@ +import { readFileSync } from 'node:fs'; +import path from 'node:path'; +import { globSync } from 'glob'; + +const SOURCE_IGNORES = [ '**/node_modules/**', 'vendor/**' ]; +const ISOLATION_OPT_OUT_PATTERN = + /(?:^|[^\w-])--no-isolate(?=$|[^\w-])|(?:^|[^\w-])--isolate(?:=|\s+)false(?=$|[^\w-])/; +const ROOT_ROUTING_COMMAND = + 'npm run --workspace @wordpress/unit-tests test:unit:routing --'; +const WORKSPACE_ROUTING_COMMAND = 'node scripts/validate-test-routing.mjs'; + +export function validateRoutingScripts( rootPackageJson, unitTestPackageJson ) { + const violations = []; + if ( + rootPackageJson.scripts?.[ 'test:unit:routing' ] !== + ROOT_ROUTING_COMMAND + ) { + violations.push( + `package.json: scripts.test:unit:routing must be exactly \`${ ROOT_ROUTING_COMMAND }\`` + ); + } + if ( + unitTestPackageJson.scripts?.[ 'test:unit:routing' ] !== + WORKSPACE_ROUTING_COMMAND + ) { + violations.push( + `test/unit/package.json: scripts.test:unit:routing must be exactly \`${ WORKSPACE_ROUTING_COMMAND }\`` + ); + } + + return violations; +} + +function findPackageScriptIsolationOptOuts( rootDir ) { + const violations = []; + const packageFiles = globSync( '**/package.json', { + cwd: rootDir, + ignore: SOURCE_IGNORES, + nodir: true, + } ).sort(); + + for ( const packageFile of packageFiles ) { + const packageJson = JSON.parse( + readFileSync( path.join( rootDir, packageFile ), 'utf8' ) + ); + + for ( const [ scriptName, command ] of Object.entries( + packageJson.scripts ?? {} + ) ) { + if ( ISOLATION_OPT_OUT_PATTERN.test( command ) ) { + violations.push( + `${ packageFile }:scripts.${ scriptName } disables Vitest module isolation: ${ command }` + ); + } + } + } + + return violations; +} + +function findWorkflowIsolationOptOuts( rootDir ) { + const violations = []; + const workflowFiles = globSync( + '.github/{actions,workflows}/**/*.{yml,yaml}', + { + cwd: rootDir, + nodir: true, + } + ).sort(); + + for ( const workflowFile of workflowFiles ) { + for ( const [ index, line ] of readFileSync( + path.join( rootDir, workflowFile ), + 'utf8' + ) + .split( /\r?\n/ ) + .entries() ) { + if ( ISOLATION_OPT_OUT_PATTERN.test( line ) ) { + violations.push( + `${ workflowFile }:${ + index + 1 + } disables Vitest module isolation: ${ line.trim() }` + ); + } + } + } + + return violations; +} + +export function findVitestIsolationOptOuts( rootDir ) { + return [ + ...findPackageScriptIsolationOptOuts( rootDir ), + ...findWorkflowIsolationOptOuts( rootDir ), + ].sort(); +} diff --git a/test/unit/scripts/test/discover-test-files.test.js b/test/unit/scripts/test/discover-test-files.test.js index a699a8c6ec85c2..012e1b1ffebe2f 100644 --- a/test/unit/scripts/test/discover-test-files.test.js +++ b/test/unit/scripts/test/discover-test-files.test.js @@ -1,5 +1,6 @@ import { describe, expect, it } from 'vitest'; import { + findAddedLegacyJestTests, getTestEnvironmentName, getVitestTestsByProject, } from '../discover-test-files.mjs'; @@ -24,18 +25,46 @@ describe( 'getTestEnvironmentName', () => { } ); describe( 'getVitestTestsByProject', () => { - it( 'routes new Node tests to Vitest without migration metadata', () => { + it( 'routes every new test to Vitest without migration metadata', () => { expect( getVitestTestsByProject( - [ 'example.test.ts', 'example.jsdom.test.tsx' ], - { vitest: { directories: [], files: [] } } + [ + 'example.test.ts', + 'example.jsdom.test.tsx', + 'example.browser.test.js', + ], + { jest: { files: [] } } ) ).toEqual( { - browser: [], - jsdom: [], + browser: [ 'example.browser.test.js' ], + jsdom: [ 'example.jsdom.test.tsx' ], node: [ 'example.test.ts' ], } ); } ); + + it( 'keeps only exact allowlisted JSDOM tests in Jest', () => { + expect( + getVitestTestsByProject( + [ 'legacy.jsdom.test.tsx', 'new.jsdom.test.tsx' ], + { jest: { files: [ 'legacy.jsdom.test.tsx' ] } } + ) + ).toEqual( { + browser: [], + jsdom: [ 'new.jsdom.test.tsx' ], + node: [], + } ); + } ); +} ); + +describe( 'findAddedLegacyJestTests', () => { + it( 'allows the legacy Jest list to shrink but reports additions', () => { + expect( + findAddedLegacyJestTests( + [ 'legacy-a.jsdom.test.js', 'new.jsdom.test.js' ], + [ 'legacy-a.jsdom.test.js', 'legacy-b.jsdom.test.js' ] + ) + ).toEqual( [ 'new.jsdom.test.js' ] ); + } ); } ); describe( 'sourceHasTestEnvironmentOverride', () => { diff --git a/test/unit/scripts/test/test-infrastructure-policy.test.js b/test/unit/scripts/test/test-infrastructure-policy.test.js new file mode 100644 index 00000000000000..77fd99f1dc01bd --- /dev/null +++ b/test/unit/scripts/test/test-infrastructure-policy.test.js @@ -0,0 +1,364 @@ +import { spawnSync } from 'node:child_process'; +import { + copyFileSync, + mkdirSync, + mkdtempSync, + realpathSync, + rmSync, + symlinkSync, + writeFileSync, +} from 'node:fs'; +import { createRequire } from 'node:module'; +import { tmpdir } from 'node:os'; +import path from 'node:path'; +import { pathToFileURL } from 'node:url'; +import { afterEach, describe, expect, it } from 'vitest'; +import { resolvePackageBin } from '../resolve-package-bin.mjs'; +import { resolveTypeRoots } from '../resolve-type-roots.mjs'; +import { + findVitestIsolationOptOuts, + validateRoutingScripts, +} from '../test-infrastructure-policy.mjs'; + +const temporaryDirectories = []; + +function createTemporaryDirectory() { + const directory = mkdtempSync( + path.join( tmpdir(), 'gutenberg-test-infrastructure-' ) + ); + temporaryDirectories.push( directory ); + return directory; +} + +function writeJson( filename, value ) { + mkdirSync( path.dirname( filename ), { recursive: true } ); + writeFileSync( filename, `${ JSON.stringify( value, null, 2 ) }\n` ); +} + +function writeModulePackage( directory, packageJson, files ) { + writeJson( path.join( directory, 'package.json' ), { + type: 'module', + ...packageJson, + } ); + for ( const [ filename, source ] of Object.entries( files ) ) { + writeFileSync( path.join( directory, filename ), source ); + } +} + +afterEach( () => { + for ( const directory of temporaryDirectories.splice( 0 ) ) { + rmSync( directory, { force: true, recursive: true } ); + } +} ); + +describe( 'test infrastructure policy', () => { + it( 'requires exact root and workspace routing scripts', () => { + expect( + validateRoutingScripts( + { + scripts: { 'test:unit:routing': 'echo --' }, + }, + { + scripts: { + 'test:unit:routing': + 'node scripts/validate-test-routing.mjs', + }, + } + ) + ).toEqual( [ + 'package.json: scripts.test:unit:routing must be exactly `npm run --workspace @wordpress/unit-tests test:unit:routing --`', + ] ); + expect( + validateRoutingScripts( + { + scripts: { + 'test:unit:routing': + 'npm run --workspace @wordpress/unit-tests test:unit:routing --', + }, + }, + { + scripts: { 'test:unit:routing': 'echo' }, + } + ) + ).toEqual( [ + 'test/unit/package.json: scripts.test:unit:routing must be exactly `node scripts/validate-test-routing.mjs`', + ] ); + expect( + validateRoutingScripts( + { + scripts: { + 'test:unit:routing': + 'npm run --workspace @wordpress/unit-tests test:unit:routing --', + }, + }, + { + scripts: { + 'test:unit:routing': + 'node scripts/validate-test-routing.mjs', + }, + } + ) + ).toEqual( [] ); + } ); +} ); + +describe( 'runner isolation policy', () => { + it( 'reports command surfaces that disable module isolation', () => { + const rootDir = createTemporaryDirectory(); + writeJson( path.join( rootDir, 'package.json' ), { + scripts: { test: 'vitest --no-isolate' }, + } ); + mkdirSync( path.join( rootDir, '.github/workflows' ), { + recursive: true, + } ); + mkdirSync( path.join( rootDir, '.github/actions/run-tests' ), { + recursive: true, + } ); + writeFileSync( + path.join( rootDir, '.github/workflows/test.yml' ), + 'run: "vitest --no-isolate"\n' + ); + writeFileSync( + path.join( rootDir, '.github/actions/run-tests/action.yml' ), + "run: 'vitest --isolate false'\n" + ); + + expect( findVitestIsolationOptOuts( rootDir ) ).toEqual( [ + ".github/actions/run-tests/action.yml:1 disables Vitest module isolation: run: 'vitest --isolate false'", + '.github/workflows/test.yml:1 disables Vitest module isolation: run: "vitest --no-isolate"', + 'package.json:scripts.test disables Vitest module isolation: vitest --no-isolate', + ] ); + } ); +} ); + +describe( 'non-hoisted Vitest infrastructure', () => { + it( 'resolves type roots from the workspace that owns them', () => { + const rootDir = createTemporaryDirectory(); + const workspaceDir = path.join( rootDir, 'test/unit' ); + const typeRoot = path.join( workspaceDir, 'node_modules/@types' ); + writeJson( path.join( workspaceDir, 'package.json' ), {} ); + writeJson( path.join( typeRoot, 'fixture/package.json' ), { + name: '@types/fixture', + version: '1.0.0', + } ); + const workspaceRequire = createRequire( + path.join( workspaceDir, 'package.json' ) + ); + + expect( + resolveTypeRoots( [ 'fixture', 'missing' ], ( specifier ) => + workspaceRequire.resolve( specifier ) + ) + ).toEqual( [ realpathSync( typeRoot ) ] ); + } ); + + it( 'loads the real setup without the jest-dom Vitest entry point', () => { + const rootDir = createTemporaryDirectory(); + const workspaceDir = path.join( rootDir, 'test/unit' ); + const storeDir = path.join( rootDir, 'store' ); + const configDir = path.join( workspaceDir, 'config' ); + const sourceConfigDir = path.resolve( + import.meta.dirname, + '../../config' + ); + mkdirSync( path.join( configDir, 'matchers' ), { recursive: true } ); + for ( const filename of [ + 'testing-library.vitest.js', + 'matchers/to-match-diff-snapshot.vitest.js', + 'matchers/to-be-positioned-popover.vitest.js', + ] ) { + copyFileSync( + path.join( sourceConfigDir, filename ), + path.join( configDir, filename ) + ); + if ( filename.includes( '/matchers/' ) ) { + copyFileSync( + path.join( sourceConfigDir, filename ), + path.join( configDir, filename.slice( 0, -3 ) ) + ); + } + } + + const vitestStore = path.join( storeDir, 'vitest' ); + writeModulePackage( + vitestStore, + { name: 'vitest', exports: './index.js' }, + { + 'index.js': ` +export const expect = Object.assign( () => ( {} ), { extend() {}, addSnapshotSerializer() {} } ); +export const Snapshots = { toMatchSnapshot() {} }; +export const vi = {}; +export const afterAll = () => {}; +export const afterEach = () => {}; +export const beforeAll = () => {}; +`, + } + ); + const jestDomStore = path.join( storeDir, 'jest-dom' ); + writeModulePackage( + jestDomStore, + { + name: '@testing-library/jest-dom', + exports: { + './matchers': './matchers.js', + './vitest': './vitest.js', + }, + }, + { + 'matchers.js': 'export function toBeInTheDocument() {}\n', + 'vitest.js': + "import { expect } from 'vitest';\nexpect.extend( {} );\n", + } + ); + const reactStore = path.join( storeDir, 'testing-library-react' ); + writeModulePackage( + reactStore, + { name: '@testing-library/react', exports: './index.js' }, + { 'index.js': 'export function cleanup() {}\n' } + ); + + const nodeModules = path.join( workspaceDir, 'node_modules' ); + mkdirSync( path.join( nodeModules, '@testing-library' ), { + recursive: true, + } ); + symlinkSync( vitestStore, path.join( nodeModules, 'vitest' ), 'dir' ); + symlinkSync( + jestDomStore, + path.join( nodeModules, '@testing-library/jest-dom' ), + 'dir' + ); + symlinkSync( + reactStore, + path.join( nodeModules, '@testing-library/react' ), + 'dir' + ); + writeJson( path.join( workspaceDir, 'package.json' ), { + type: 'module', + } ); + writeFileSync( + path.join( workspaceDir, 'load-setup.mjs' ), + "import './config/testing-library.vitest.js';\n" + ); + writeFileSync( + path.join( workspaceDir, 'load-jest-dom-vitest.mjs' ), + "import '@testing-library/jest-dom/vitest';\n" + ); + const loaderPath = path.join( workspaceDir, 'extension-loader.mjs' ); + writeFileSync( + loaderPath, + `export async function resolve( specifier, context, nextResolve ) { + try { + return await nextResolve( specifier, context ); + } catch ( error ) { + if ( specifier.startsWith( '.' ) && specifier.endsWith( '.vitest' ) ) { + return nextResolve( \`${ '${specifier}' }.js\`, context ); + } + throw error; + } +} +` + ); + + const legacyResult = spawnSync( + process.execPath, + [ path.join( workspaceDir, 'load-jest-dom-vitest.mjs' ) ], + { encoding: 'utf8' } + ); + const setupResult = spawnSync( + process.execPath, + [ + '--no-warnings', + `--experimental-loader=${ pathToFileURL( loaderPath ).href }`, + path.join( workspaceDir, 'load-setup.mjs' ), + ], + { encoding: 'utf8' } + ); + + expect( legacyResult.status ).not.toBe( 0 ); + expect( `${ legacyResult.stdout }${ legacyResult.stderr }` ).toContain( + "Cannot find package 'vitest'" + ); + expect( `${ setupResult.stdout }${ setupResult.stderr }` ).toBe( '' ); + expect( setupResult.status ).toBe( 0 ); + } ); + + it( 'type-checks Gutenberg matcher augmentation with workspace-local Vitest', () => { + const rootDir = createTemporaryDirectory(); + const workspaceDir = path.join( rootDir, 'test/unit' ); + const vitestDir = path.join( workspaceDir, 'node_modules/vitest' ); + const jestDomDir = path.join( + workspaceDir, + 'node_modules/@testing-library/jest-dom' + ); + const typingsDir = path.join( + workspaceDir, + 'typings/gutenberg-vitest-test-env' + ); + mkdirSync( vitestDir, { recursive: true } ); + mkdirSync( jestDomDir, { recursive: true } ); + mkdirSync( typingsDir, { recursive: true } ); + writeJson( path.join( workspaceDir, 'package.json' ), { + type: 'module', + } ); + writeJson( path.join( vitestDir, 'package.json' ), { + name: 'vitest', + version: '4.0.0', + types: 'index.d.ts', + } ); + writeFileSync( + path.join( vitestDir, 'index.d.ts' ), + 'export interface Matchers { toBe(value: unknown): void; }\nexport declare const vi: unknown;\nexport declare function expect(value: T): Matchers;\n' + ); + writeJson( path.join( jestDomDir, 'package.json' ), { + name: '@testing-library/jest-dom', + version: '1.0.0', + exports: { + './matchers': { + types: './matchers.d.ts', + }, + }, + } ); + writeFileSync( + path.join( jestDomDir, 'matchers.d.ts' ), + 'export interface TestingLibraryMatchers { toBeVisible(): R; }\n' + ); + copyFileSync( + path.resolve( + import.meta.dirname, + '../../typings/gutenberg-vitest-test-env/index.d.ts' + ), + path.join( typingsDir, 'index.d.ts' ) + ); + writeFileSync( + path.join( workspaceDir, 'matcher.test.ts' ), + "import { expect } from 'vitest';\nexpect( {} ).toHaveWarned();\nexpect( document.body ).toBeVisible();\n" + ); + writeJson( path.join( workspaceDir, 'tsconfig.json' ), { + compilerOptions: { + lib: [ 'ES2022', 'DOM' ], + module: 'NodeNext', + moduleResolution: 'NodeNext', + noEmit: true, + strict: true, + typeRoots: [ path.join( workspaceDir, 'typings' ) ], + types: [ 'gutenberg-vitest-test-env' ], + }, + files: [ path.join( workspaceDir, 'matcher.test.ts' ) ], + } ); + + const result = spawnSync( + process.execPath, + [ + resolvePackageBin( 'typescript', 'tsc6' ), + '--project', + path.join( workspaceDir, 'tsconfig.json' ), + '--pretty', + 'false', + ], + { encoding: 'utf8' } + ); + + expect( `${ result.stdout }${ result.stderr }` ).toBe( '' ); + expect( result.status ).toBe( 0 ); + } ); +} ); diff --git a/test/unit/scripts/validate-test-routing.mjs b/test/unit/scripts/validate-test-routing.mjs index 826f750436965b..715cbe9a5f9f32 100644 --- a/test/unit/scripts/validate-test-routing.mjs +++ b/test/unit/scripts/validate-test-routing.mjs @@ -6,6 +6,7 @@ import path from 'node:path'; import { fileURLToPath } from 'node:url'; import { discoverTestFiles, + findAddedLegacyJestTests, findOverlappingVitestProjectTests, getTestEnvironmentName, getVitestTests, @@ -87,13 +88,6 @@ function assertUniquePaths( label, testPaths ) { } } -function isWithinDirectory( testPath, directoryPath ) { - return ( - testPath === directoryPath || - testPath.startsWith( `${ directoryPath }/` ) - ); -} - function isValidManifestPath( testPath, expectedType ) { const resolvedPath = path.resolve( ROOT_DIR, testPath ); const relativePath = path.relative( ROOT_DIR, resolvedPath ); @@ -109,6 +103,60 @@ function isValidManifestPath( testPath, expectedType ) { ); } +function getCommandArgument( name ) { + const inlinePrefix = `--${ name }=`; + const inlineArgument = process.argv.find( ( argument ) => + argument.startsWith( inlinePrefix ) + ); + if ( inlineArgument ) { + return inlineArgument.slice( inlinePrefix.length ); + } + + const argumentIndex = process.argv.indexOf( `--${ name }` ); + return argumentIndex === -1 ? null : process.argv[ argumentIndex + 1 ]; +} + +function runGit( args ) { + return spawnSync( 'git', args, { + cwd: ROOT_DIR, + encoding: 'utf8', + } ); +} + +function getBaselineRef() { + const explicitBase = getCommandArgument( 'base' ); + if ( explicitBase ) { + return explicitBase; + } + assert.notEqual( + process.env.GITHUB_ACTIONS, + 'true', + 'CI must pass an explicit --base= to validate that the legacy Jest allowlist did not grow.' + ); + + const mergeBase = runGit( [ 'merge-base', 'HEAD', 'origin/trunk' ] ); + return mergeBase.status === 0 ? mergeBase.stdout.trim() : 'HEAD'; +} + +function readBaselineMigrationManifest() { + const baselineRef = getBaselineRef(); + const result = runGit( [ + 'show', + `${ baselineRef }:test/unit/test-migration.json`, + ] ); + + if ( result.status !== 0 ) { + assert.notEqual( + process.env.GITHUB_ACTIONS, + 'true', + `CI must fetch the baseline commit ${ baselineRef } to validate that the legacy Jest allowlist did not grow.` + ); + return null; + } + + return JSON.parse( result.stdout ); +} + const staticInventory = discoverTestFiles( ROOT_DIR ); const testsWithEnvironmentOverrides = staticInventory.filter( ( testPath ) => sourceHasTestEnvironmentOverride( @@ -128,13 +176,8 @@ const expectedVitestTestsByProject = getVitestTestsByProject( manifest ); const expectedVitestTests = getVitestTests( staticInventory, manifest ); -const expectedVitestTestSet = new Set( expectedVitestTests ); const expectedJestTestsByProject = { - jsdom: staticInventory.filter( - ( testPath ) => - ! expectedVitestTestSet.has( testPath ) && - getTestEnvironmentName( testPath ) === 'jsdom' - ), + jsdom: [ ...manifest.jest.files ].sort(), }; const jestConfig = require( path.join( ROOT_DIR, JEST_CONFIG ) ); const jestProjectNames = jestConfig.projects.map( ( project ) => @@ -213,61 +256,35 @@ const vitestTests = new Set( ] ) ); -const migratedTestFiles = manifest.vitest.files; -const migratedDirectories = manifest.vitest.directories; - -assertUniquePaths( 'vitest.files', migratedTestFiles ); -assertUniquePaths( 'vitest.directories', migratedDirectories ); +const legacyJestTestFiles = manifest.jest.files; +const baselineMigrationManifest = readBaselineMigrationManifest(); -const overlappingManifestEntries = [ - ...migratedTestFiles.filter( ( testPath ) => - migratedDirectories.some( ( directoryPath ) => - isWithinDirectory( testPath, directoryPath ) - ) - ), - ...migratedDirectories.filter( ( directoryPath, index ) => - migratedDirectories.some( - ( otherDirectoryPath, otherIndex ) => - otherIndex !== index && - isWithinDirectory( directoryPath, otherDirectoryPath ) - ) - ), -]; -assert.deepEqual( - overlappingManifestEntries, - [], - `Vitest migration manifest entries must be disjoint:\n${ overlappingManifestEntries.join( - '\n' - ) }` -); +assertUniquePaths( 'jest.files', legacyJestTestFiles ); -const invalidMigratedEntries = [ - ...migratedTestFiles.filter( - ( testPath ) => ! isValidManifestPath( testPath, 'isFile' ) - ), - ...migratedDirectories.filter( - ( directoryPath ) => - ! isValidManifestPath( directoryPath, 'isDirectory' ) - ), -]; -assert.deepEqual( - invalidMigratedEntries, - [], - `Migrated files or directories must exist inside the repository and match their declared type:\n${ invalidMigratedEntries.join( - '\n' - ) }` -); +if ( baselineMigrationManifest?.jest?.files ) { + const addedLegacyJestTests = findAddedLegacyJestTests( + legacyJestTestFiles, + baselineMigrationManifest.jest.files + ); + assert.deepEqual( + addedLegacyJestTests, + [], + `jest.files is shrink-only. New tests must run in Vitest:\n${ addedLegacyJestTests.join( + '\n' + ) }` + ); +} -const emptyMigratedDirectories = migratedDirectories.filter( - ( directoryPath ) => - ! staticInventory.some( ( testPath ) => - isWithinDirectory( testPath, directoryPath ) - ) +const invalidLegacyJestEntries = legacyJestTestFiles.filter( + ( testPath ) => + ! isValidManifestPath( testPath, 'isFile' ) || + ! staticInventory.includes( testPath ) || + getTestEnvironmentName( testPath ) !== 'jsdom' ); assert.deepEqual( - emptyMigratedDirectories, + invalidLegacyJestEntries, [], - `Migrated directories must contain at least one test:\n${ emptyMigratedDirectories.join( + `Legacy Jest entries must be discovered JSDOM test files inside the repository:\n${ invalidLegacyJestEntries.join( '\n' ) }` ); @@ -286,7 +303,7 @@ assert.deepEqual( assert.deepEqual( [ ...vitestTests ].sort(), expectedVitestTests, - `Vitest discovery does not match the migration manifest.` + `Vitest discovery does not match the legacy Jest allowlist.` ); const runnerInventory = [ diff --git a/test/unit/scripts/validate-vitest-conventions.mjs b/test/unit/scripts/validate-vitest-conventions.mjs index 9a7431ce8c9587..2c78539002c001 100644 --- a/test/unit/scripts/validate-vitest-conventions.mjs +++ b/test/unit/scripts/validate-vitest-conventions.mjs @@ -17,6 +17,11 @@ import { VITEST_PROJECT_NAMES, } from './discover-test-files.mjs'; import { resolvePackageBin } from './resolve-package-bin.mjs'; +import { resolveTypeRoots } from './resolve-type-roots.mjs'; +import { + findVitestIsolationOptOuts, + validateRoutingScripts, +} from './test-infrastructure-policy.mjs'; import { validateVitestPolicy, validateVitestPolicyExceptions, @@ -26,6 +31,7 @@ const ROOT_DIR = path.resolve( path.dirname( fileURLToPath( import.meta.url ) ), '../../..' ); +const require = createRequire( import.meta.url ); const migration = JSON.parse( readFileSync( path.join( ROOT_DIR, 'test/unit/test-migration.json' ), @@ -126,6 +132,17 @@ violations.push( } ) ); +const rootPackageJson = JSON.parse( + readFileSync( path.join( ROOT_DIR, 'package.json' ), 'utf8' ) +); +const unitTestPackageJson = JSON.parse( + readFileSync( path.join( ROOT_DIR, 'test/unit/package.json' ), 'utf8' ) +); +violations.push( + ...findVitestIsolationOptOuts( ROOT_DIR ), + ...validateRoutingScripts( rootPackageJson, unitTestPackageJson ) +); + const vitestVersions = new Map(); for ( const file of vitestTests ) { const packagePath = findWorkspacePackage( file ); @@ -174,34 +191,6 @@ const commonTypes = [ 'style-imports', ]; -const require = createRequire( import.meta.url ); - -/** - * Resolve the directories holding the given `@types` packages. - * - * Non-hoisting installs keep them in the workspace that declares the - * dependency, so the repository root is not a reliable type root. - * - * @param {string[]} typeNames Type package names, without the `@types/` scope. - * @return {string[]} Absolute `@types` directories, without duplicates. - */ -function resolveTypeRoots( typeNames ) { - const typeRoots = new Set(); - - for ( const typeName of typeNames ) { - try { - const packageJsonPath = require.resolve( - `@types/${ typeName }/package.json` - ); - typeRoots.add( path.dirname( path.dirname( packageJsonPath ) ) ); - } catch { - // Declared under `typings` rather than by a `@types` package. - } - } - - return [ ...typeRoots ]; -} - function importsNodeBuiltin( file ) { const source = sourcesByFile.get( file ) ?? @@ -291,7 +280,10 @@ for ( const projectName of VITEST_PROJECT_NAMES ) { typeRoots: [ path.join( ROOT_DIR, 'typings' ), path.join( ROOT_DIR, 'test/unit/typings' ), - ...resolveTypeRoots( [ ...commonTypes, 'node' ] ), + ...resolveTypeRoots( + [ ...commonTypes, 'node' ], + ( specifier ) => require.resolve( specifier ) + ), ], types: projectName === 'jsdom' diff --git a/test/unit/test-migration.json b/test/unit/test-migration.json index 1ad2aeaf765366..e02bfb3a9a1279 100644 --- a/test/unit/test-migration.json +++ b/test/unit/test-migration.json @@ -1,11 +1,607 @@ { - "vitest": { + "jest": { "files": [ - "packages/html-entities/src/test/entities.jsdom.test.ts", - "test/unit/config/emotion-compiler.vitest.jsdom.test.jsx", - "test/unit/config/matchers/test/to-match-diff-snapshot.vitest.jsdom.test.js", - "test/unit/config/setup.vitest.jsdom.test.jsx" - ], - "directories": [] + "packages/a11y/src/script/test/add-container.jsdom.test.ts", + "packages/a11y/src/shared/test/clear.jsdom.test.ts", + "packages/a11y/src/test/index.jsdom.test.ts", + "packages/admin-ui/src/admin-theme-colors/test/index.jsdom.test.ts", + "packages/admin-ui/src/breadcrumbs/test/index.jsdom.test.tsx", + "packages/admin-ui/src/navigation/test/index.jsdom.test.tsx", + "packages/admin-ui/src/page/test/index.jsdom.test.tsx", + "packages/api-fetch/src/middlewares/test/preloading.jsdom.test.ts", + "packages/api-fetch/src/test/index.jsdom.test.js", + "packages/blob/src/test/index.jsdom.test.ts", + "packages/block-directory/src/components/downloadable-block-icon/test/index.jsdom.test.jsx", + "packages/block-directory/src/components/downloadable-block-list-item/test/index.jsdom.test.jsx", + "packages/block-directory/src/components/downloadable-blocks-list/test/index.jsdom.test.jsx", + "packages/block-directory/src/store/test/actions.jsdom.test.js", + "packages/block-directory/src/store/test/load-assets.jsdom.test.js", + "packages/block-directory/src/store/test/selectors.jsdom.test.js", + "packages/block-editor/src/components/alignment-control/test/index.jsdom.test.jsx", + "packages/block-editor/src/components/background-image-control/test/index.jsdom.test.js", + "packages/block-editor/src/components/block-alignment-control/test/index.jsdom.test.jsx", + "packages/block-editor/src/components/block-bindings/test/use-block-bindings-utils.jsdom.test.js", + "packages/block-editor/src/components/block-breadcrumb/test/index.jsdom.test.jsx", + "packages/block-editor/src/components/block-compare/test/block-view.jsdom.test.jsx", + "packages/block-editor/src/components/block-controls/test/index.jsdom.test.jsx", + "packages/block-editor/src/components/block-edit/test/edit.jsdom.test.jsx", + "packages/block-editor/src/components/block-icon/test/index.jsdom.test.jsx", + "packages/block-editor/src/components/block-preview/test/index.jsdom.test.jsx", + "packages/block-editor/src/components/block-selection-clearer/test/index.jsdom.test.jsx", + "packages/block-editor/src/components/block-settings-menu/test/block-mode-toggle.jsdom.test.jsx", + "packages/block-editor/src/components/block-switcher/test/index.jsdom.test.jsx", + "packages/block-editor/src/components/block-title/test/index.jsdom.test.jsx", + "packages/block-editor/src/components/block-toolbar/test/block-toolbar-icon.jsdom.test.jsx", + "packages/block-editor/src/components/block-vertical-alignment-control/test/index.jsdom.test.jsx", + "packages/block-editor/src/components/block-visibility/test/use-block-visibility.jsdom.test.js", + "packages/block-editor/src/components/border-radius-control/test/index.jsdom.test.jsx", + "packages/block-editor/src/components/child-layout-control/test/index.jsdom.test.jsx", + "packages/block-editor/src/components/color-palette/test/control.jsdom.test.jsx", + "packages/block-editor/src/components/colors-gradients/test/control.jsdom.test.jsx", + "packages/block-editor/src/components/colors/test/with-colors.jsdom.test.jsx", + "packages/block-editor/src/components/contrast-checker/test/index.jsdom.test.jsx", + "packages/block-editor/src/components/default-block-appender/test/index.jsdom.test.jsx", + "packages/block-editor/src/components/dimensions-tool/test/index.jsdom.test.jsx", + "packages/block-editor/src/components/duotone-control/test/index.jsdom.test.jsx", + "packages/block-editor/src/components/global-styles/inheritance/test/index.jsdom.test.jsx", + "packages/block-editor/src/components/global-styles/test/background-panel.jsdom.test.jsx", + "packages/block-editor/src/components/global-styles/test/border-panel.jsdom.test.jsx", + "packages/block-editor/src/components/global-styles/test/color-panel.jsdom.test.jsx", + "packages/block-editor/src/components/global-styles/test/dimensions-panel.jsdom.test.jsx", + "packages/block-editor/src/components/global-styles/test/filters-panel.jsdom.test.jsx", + "packages/block-editor/src/components/global-styles/test/inherited-value-context-core.jsdom.test.js", + "packages/block-editor/src/components/global-styles/test/inherited-value-context.jsdom.test.jsx", + "packages/block-editor/src/components/global-styles/test/state-control-badges.jsdom.test.jsx", + "packages/block-editor/src/components/global-styles/test/typography-panel-core.jsdom.test.jsx", + "packages/block-editor/src/components/global-styles/test/typography-panel.jsdom.test.jsx", + "packages/block-editor/src/components/image-size-control/test/index.jsdom.test.jsx", + "packages/block-editor/src/components/inner-blocks/test/index.jsdom.test.jsx", + "packages/block-editor/src/components/inner-content/test/index.jsdom.test.jsx", + "packages/block-editor/src/components/inserter/media-tab/test/hooks.jsdom.test.js", + "packages/block-editor/src/components/inserter/media-tab/test/media-panel.jsdom.test.jsx", + "packages/block-editor/src/components/line-height-control/test/index.jsdom.test.jsx", + "packages/block-editor/src/components/link-control/test/index.jsdom.test.jsx", + "packages/block-editor/src/components/link-control/test/url-normalization.jsdom.test.jsx", + "packages/block-editor/src/components/link-picker/test/index.jsdom.test.jsx", + "packages/block-editor/src/components/media-placeholder/test/index.jsdom.test.jsx", + "packages/block-editor/src/components/media-replace-flow/test/index.jsdom.test.jsx", + "packages/block-editor/src/components/panel-color-settings/test/index.jsdom.test.jsx", + "packages/block-editor/src/components/preset-input-control/test/index.jsdom.test.jsx", + "packages/block-editor/src/components/provider/test/experimental-provider.jsdom.test.jsx", + "packages/block-editor/src/components/provider/test/use-block-sync.jsdom.test.jsx", + "packages/block-editor/src/components/recursion-provider/test/index.jsdom.test.jsx", + "packages/block-editor/src/components/responsive-block-control/test/index.jsdom.test.jsx", + "packages/block-editor/src/components/spacing-sizes-control/test/index.jsdom.test.jsx", + "packages/block-editor/src/components/url-input/test/button.jsdom.test.jsx", + "packages/block-editor/src/components/url-input/test/index.jsdom.test.jsx", + "packages/block-editor/src/components/use-block-display-information/test/index.jsdom.test.jsx", + "packages/block-editor/src/components/warning/test/index.jsdom.test.jsx", + "packages/block-editor/src/components/writing-flow/test/index.jsdom.test.js", + "packages/block-editor/src/elements/test/index.jsdom.test.js", + "packages/block-editor/src/hooks/test/align.jsdom.test.jsx", + "packages/block-editor/src/hooks/test/anchor.jsdom.test.js", + "packages/block-editor/src/hooks/test/auto-inspector-controls.jsdom.test.js", + "packages/block-editor/src/hooks/test/background.jsdom.test.jsx", + "packages/block-editor/src/hooks/test/block-style-state.jsdom.test.js", + "packages/block-editor/src/hooks/test/cross-origin-isolation.jsdom.test.js", + "packages/block-editor/src/hooks/test/custom-class-name.jsdom.test.jsx", + "packages/block-editor/src/hooks/test/dimensions.jsdom.test.js", + "packages/block-editor/src/hooks/test/duotone.jsdom.test.js", + "packages/block-editor/src/hooks/test/font-size.jsdom.test.js", + "packages/block-editor/src/hooks/test/inherited-value-wiring.jsdom.test.jsx", + "packages/block-editor/src/hooks/test/layout-child.jsdom.test.js", + "packages/block-editor/src/hooks/test/layout.jsdom.test.js", + "packages/block-editor/src/hooks/test/style.jsdom.test.jsx", + "packages/block-editor/src/hooks/test/text-align.jsdom.test.jsx", + "packages/block-editor/src/hooks/test/use-typography-props.jsdom.test.js", + "packages/block-editor/src/hooks/test/utils.jsdom.test.js", + "packages/block-editor/src/layouts/test/constrained.jsdom.test.jsx", + "packages/block-editor/src/layouts/test/flex.jsdom.test.jsx", + "packages/block-editor/src/layouts/test/flow.jsdom.test.js", + "packages/block-editor/src/layouts/test/grid.jsdom.test.jsx", + "packages/block-editor/src/store/test/actions.jsdom.test.js", + "packages/block-editor/src/store/test/registry-selectors.jsdom.test.js", + "packages/block-editor/src/store/test/selectors.jsdom.test.jsx", + "packages/block-editor/src/utils/test/dom.jsdom.test.jsx", + "packages/block-library/src/button/test/get-experimental-label.jsdom.test.js", + "packages/block-library/src/columns/test/transforms.jsdom.test.js", + "packages/block-library/src/cover/test/edit.jsdom.test.js", + "packages/block-library/src/cover/test/transforms.jsdom.test.js", + "packages/block-library/src/embed/test/index.jsdom.test.js", + "packages/block-library/src/gallery/test/edit.jsdom.test.js", + "packages/block-library/src/gallery/test/transforms.jsdom.test.js", + "packages/block-library/src/gallery/test/use-get-new-images.jsdom.test.jsx", + "packages/block-library/src/group/test/placeholder.jsdom.test.jsx", + "packages/block-library/src/html/test/index.jsdom.test.js", + "packages/block-library/src/html/test/utils.jsdom.test.js", + "packages/block-library/src/image/test/transforms.jsdom.test.js", + "packages/block-library/src/image/test/use-open-image-media-editor-modal.jsdom.test.js", + "packages/block-library/src/latest-posts/test/deprecated.jsdom.test.js", + "packages/block-library/src/navigation-link/shared/test/controls.jsdom.test.jsx", + "packages/block-library/src/navigation-link/shared/test/use-enable-link-status-validation.jsdom.test.js", + "packages/block-library/src/navigation-link/shared/test/use-entity-binding.jsdom.test.js", + "packages/block-library/src/navigation-link/shared/test/use-handle-link-change.jsdom.test.js", + "packages/block-library/src/navigation-link/shared/test/use-link-preview.jsdom.test.js", + "packages/block-library/src/navigation-link/test/hooks.jsdom.test.js", + "packages/block-library/src/navigation/edit/test/layout-custom-properties.jsdom.test.js", + "packages/block-library/src/navigation/edit/test/navigation-menu-selector.jsdom.test.jsx", + "packages/block-library/src/navigation/edit/test/overlay-template-part-selector.jsdom.test.jsx", + "packages/block-library/src/navigation/edit/test/responsive-wrapper.jsdom.test.jsx", + "packages/block-library/src/navigation/edit/test/use-create-overlay.jsdom.test.js", + "packages/block-library/src/navigation/test/menu-items-to-blocks.jsdom.test.js", + "packages/block-library/src/navigation/test/use-navigation-menu.jsdom.test.js", + "packages/block-library/src/navigation/test/view.jsdom.test.js", + "packages/block-library/src/page-list/test/convert-to-navigation-links.jsdom.test.js", + "packages/block-library/src/playlist-track/test/edit.jsdom.test.jsx", + "packages/block-library/src/playlist/test/edit-component.jsdom.test.jsx", + "packages/block-library/src/playlist/test/edit.jsdom.test.js", + "packages/block-library/src/playlist/test/transforms.jsdom.test.js", + "packages/block-library/src/post-date/test/edit.jsdom.test.js", + "packages/block-library/src/query/test/utils.jsdom.test.js", + "packages/block-library/src/quote/test/migrate.jsdom.test.js", + "packages/block-library/src/separator/test/edit.jsdom.test.jsx", + "packages/block-library/src/utils/test/style-state.jsdom.test.js", + "packages/block-library/src/utils/test/waveform-player-dependency.jsdom.test.js", + "packages/block-library/src/utils/test/waveform-player.jsdom.test.jsx", + "packages/block-library/src/utils/test/waveform-utils.jsdom.test.js", + "packages/blocks/src/api/parser/test/apply-block-deprecated-versions.jsdom.test.jsx", + "packages/blocks/src/api/parser/test/fix-custom-classname.jsdom.test.jsx", + "packages/blocks/src/api/parser/test/get-block-attributes.jsdom.test.js", + "packages/blocks/src/api/parser/test/index.jsdom.test.jsx", + "packages/blocks/src/api/raw-handling/test/blockquote-normaliser.jsdom.test.js", + "packages/blocks/src/api/raw-handling/test/comment-remover.jsdom.test.js", + "packages/blocks/src/api/raw-handling/test/figure-content-reducer.jsdom.test.js", + "packages/blocks/src/api/raw-handling/test/html-formatting-remover.jsdom.test.js", + "packages/blocks/src/api/raw-handling/test/image-corrector.jsdom.test.js", + "packages/blocks/src/api/raw-handling/test/is-inline-content.jsdom.test.js", + "packages/blocks/src/api/raw-handling/test/list-reducer.jsdom.test.js", + "packages/blocks/src/api/raw-handling/test/ms-list-converter.jsdom.test.js", + "packages/blocks/src/api/raw-handling/test/normalise-blocks.jsdom.test.js", + "packages/blocks/src/api/raw-handling/test/paste-handler.jsdom.test.js", + "packages/blocks/src/api/raw-handling/test/phrasing-content-reducer.jsdom.test.js", + "packages/blocks/src/api/raw-handling/test/special-comment-converter.jsdom.test.js", + "packages/blocks/src/api/raw-handling/test/utils.jsdom.test.js", + "packages/blocks/src/api/test/children.jsdom.test.js", + "packages/blocks/src/api/test/matchers.jsdom.test.js", + "packages/blocks/src/api/test/node.jsdom.test.js", + "packages/blocks/src/api/test/utils.jsdom.test.js", + "packages/blocks/src/api/test/validation.jsdom.test.js", + "packages/blocks/src/store/test/selectors.jsdom.test.jsx", + "packages/components/src/alignment-matrix-control/test/index.jsdom.test.tsx", + "packages/components/src/autocomplete/test/index.jsdom.test.tsx", + "packages/components/src/badge/test/index.jsdom.test.tsx", + "packages/components/src/base-control/test/index.jsdom.test.tsx", + "packages/components/src/border-box-control/test/index.jsdom.test.tsx", + "packages/components/src/border-control/test/index.jsdom.test.tsx", + "packages/components/src/box-control/test/index.jsdom.test.tsx", + "packages/components/src/button/test/index.jsdom.test.tsx", + "packages/components/src/card/test/index.jsdom.test.tsx", + "packages/components/src/checkbox-control/test/index.jsdom.test.tsx", + "packages/components/src/circular-option-picker/test/index.jsdom.test.tsx", + "packages/components/src/color-indicator/test/index.jsdom.test.tsx", + "packages/components/src/color-palette/test/index.jsdom.test.tsx", + "packages/components/src/color-palette/test/utils.jsdom.test.ts", + "packages/components/src/color-picker/test/index.jsdom.test.tsx", + "packages/components/src/combobox-control/test/index.jsdom.test.tsx", + "packages/components/src/composite/legacy/test/index.jsdom.test.tsx", + "packages/components/src/composite/test/index.jsdom.test.tsx", + "packages/components/src/confirm-dialog/test/index.jsdom.test.tsx", + "packages/components/src/content-editable-control/test/index.jsdom.test.tsx", + "packages/components/src/context/test/context-system-provider.jsdom.test.tsx", + "packages/components/src/custom-gradient-picker/gradient-bar/test/utils.jsdom.test.ts", + "packages/components/src/custom-gradient-picker/test/index.jsdom.test.tsx", + "packages/components/src/custom-select-control-v2/test/index.jsdom.test.tsx", + "packages/components/src/custom-select-control/test/index.jsdom.test.tsx", + "packages/components/src/date-time/date-picker/test/index.jsdom.test.tsx", + "packages/components/src/date-time/date-picker/test/use-lilius.jsdom.test.ts", + "packages/components/src/date-time/date-time/test/index.jsdom.test.tsx", + "packages/components/src/date-time/time-picker/test/index.jsdom.test.tsx", + "packages/components/src/date-time/time-picker/time-input/test/index.jsdom.test.tsx", + "packages/components/src/disabled/test/index.jsdom.test.tsx", + "packages/components/src/divider/test/index.jsdom.test.tsx", + "packages/components/src/dropdown-menu/test/index.jsdom.test.tsx", + "packages/components/src/dropdown/test/index.jsdom.test.tsx", + "packages/components/src/duotone-picker/test/custom-duotone-bar.jsdom.test.tsx", + "packages/components/src/duotone-picker/test/index.jsdom.test.tsx", + "packages/components/src/elevation/test/index.jsdom.test.tsx", + "packages/components/src/external-link/test/index.jsdom.test.tsx", + "packages/components/src/flex/test/index.jsdom.test.tsx", + "packages/components/src/focal-point-picker/test/index.jsdom.test.tsx", + "packages/components/src/focal-point-picker/test/media.jsdom.test.tsx", + "packages/components/src/font-size-picker/test/font-size-picker-select.jsdom.test.tsx", + "packages/components/src/font-size-picker/test/font-size-picker-toggle-group.jsdom.test.tsx", + "packages/components/src/font-size-picker/test/index.jsdom.test.tsx", + "packages/components/src/form-file-upload/test/index.jsdom.test.tsx", + "packages/components/src/form-toggle/test/index.jsdom.test.tsx", + "packages/components/src/form-token-field/test/index.jsdom.test.tsx", + "packages/components/src/gradient-picker/test/index.jsdom.test.tsx", + "packages/components/src/grid/test/grid.jsdom.test.tsx", + "packages/components/src/guide/test/index.jsdom.test.tsx", + "packages/components/src/h-stack/test/index.jsdom.test.tsx", + "packages/components/src/heading/test/index.jsdom.test.tsx", + "packages/components/src/higher-order/with-fallback-styles/test/index.jsdom.test.tsx", + "packages/components/src/higher-order/with-filters/test/index.jsdom.test.tsx", + "packages/components/src/higher-order/with-focus-outside/test/index.jsdom.test.tsx", + "packages/components/src/higher-order/with-focus-return/test/index.jsdom.test.tsx", + "packages/components/src/higher-order/with-notices/test/index.jsdom.test.tsx", + "packages/components/src/higher-order/with-spoken-messages/test/index.jsdom.test.tsx", + "packages/components/src/icon/test/index.jsdom.test.tsx", + "packages/components/src/input-control/test/index.jsdom.test.tsx", + "packages/components/src/isolated-event-container/test/index.jsdom.test.tsx", + "packages/components/src/item-group/test/index.jsdom.test.tsx", + "packages/components/src/keyboard-shortcuts/test/index.jsdom.test.tsx", + "packages/components/src/menu-group/test/index.jsdom.test.tsx", + "packages/components/src/menu-item/test/index.jsdom.test.tsx", + "packages/components/src/menu/test/index.jsdom.test.tsx", + "packages/components/src/modal/test/aria-helper.jsdom.test.ts", + "packages/components/src/modal/test/index.jsdom.test.tsx", + "packages/components/src/navigable-container/test/navigable-menu.jsdom.test.tsx", + "packages/components/src/navigable-container/test/tababble-container.jsdom.test.tsx", + "packages/components/src/navigator/test/index.jsdom.test.tsx", + "packages/components/src/notice/test/index.jsdom.test.tsx", + "packages/components/src/notice/test/list.jsdom.test.tsx", + "packages/components/src/number-control/test/index.jsdom.test.tsx", + "packages/components/src/palette-edit/test/index.jsdom.test.tsx", + "packages/components/src/panel/test/body.jsdom.test.tsx", + "packages/components/src/panel/test/header.jsdom.test.tsx", + "packages/components/src/panel/test/index.jsdom.test.tsx", + "packages/components/src/panel/test/row.jsdom.test.tsx", + "packages/components/src/placeholder/test/index.jsdom.test.tsx", + "packages/components/src/popover/test/index.jsdom.test.tsx", + "packages/components/src/progress-bar/test/index.jsdom.test.tsx", + "packages/components/src/radio-control/test/index.jsdom.test.tsx", + "packages/components/src/range-control/test/index.jsdom.test.tsx", + "packages/components/src/resizable-box/resize-tooltip/test/index.jsdom.test.tsx", + "packages/components/src/resizable-box/resize-tooltip/test/label.jsdom.test.tsx", + "packages/components/src/sandbox/test/index.jsdom.test.tsx", + "packages/components/src/scroll-lock/test/index.jsdom.test.tsx", + "packages/components/src/scrollable/test/index.jsdom.test.tsx", + "packages/components/src/search-control/test/index.jsdom.test.tsx", + "packages/components/src/select-control/test/select-control.jsdom.test.tsx", + "packages/components/src/shortcut/test/index.jsdom.test.tsx", + "packages/components/src/slot-fill/test/index.jsdom.test.tsx", + "packages/components/src/slot-fill/test/slot.jsdom.test.tsx", + "packages/components/src/snackbar/test/index.jsdom.test.tsx", + "packages/components/src/snackbar/test/list.jsdom.test.tsx", + "packages/components/src/spacer/test/index.jsdom.test.tsx", + "packages/components/src/style-provider/test/index.jsdom.test.tsx", + "packages/components/src/surface/test/index.jsdom.test.tsx", + "packages/components/src/tab-panel/test/index.jsdom.test.tsx", + "packages/components/src/tabs/test/index.jsdom.test.tsx", + "packages/components/src/text-control/test/text-control.jsdom.test.tsx", + "packages/components/src/text-highlight/test/index.jsdom.test.tsx", + "packages/components/src/text/test/index.jsdom.test.tsx", + "packages/components/src/toggle-control/test/index.jsdom.test.tsx", + "packages/components/src/toggle-group-control/test/index.jsdom.test.tsx", + "packages/components/src/toolbar/test/index.jsdom.test.tsx", + "packages/components/src/toolbar/test/toolbar-group.jsdom.test.tsx", + "packages/components/src/tools-panel/test/index.jsdom.test.tsx", + "packages/components/src/tooltip/test/index.jsdom.test.tsx", + "packages/components/src/tree-grid/test/cell.jsdom.test.tsx", + "packages/components/src/tree-grid/test/index.jsdom.test.tsx", + "packages/components/src/tree-grid/test/roving-tab-index-item.jsdom.test.tsx", + "packages/components/src/tree-grid/test/roving-tab-index.jsdom.test.tsx", + "packages/components/src/tree-grid/test/row.jsdom.test.tsx", + "packages/components/src/truncate/test/index.jsdom.test.tsx", + "packages/components/src/unit-control/test/index.jsdom.test.tsx", + "packages/components/src/utils/hooks/test/use-controlled-state.jsdom.test.tsx", + "packages/components/src/utils/hooks/test/use-controlled-value.jsdom.test.tsx", + "packages/components/src/utils/hooks/test/use-cx.jsdom.test.tsx", + "packages/components/src/utils/test/polymorphic-element.jsdom.test.tsx", + "packages/components/src/v-stack/test/index.jsdom.test.tsx", + "packages/components/src/validated-form-controls/test/content-editable-control.jsdom.test.tsx", + "packages/components/src/validated-form-controls/test/custom-select-control.jsdom.test.tsx", + "packages/components/src/validated-form-controls/test/input-control.jsdom.test.tsx", + "packages/components/src/validated-form-controls/test/range-control.jsdom.test.tsx", + "packages/components/src/validated-form-controls/test/textarea-control.jsdom.test.tsx", + "packages/components/src/view/test/index.jsdom.test.tsx", + "packages/compose/src/higher-order/pure/test/index.jsdom.test.jsx", + "packages/compose/src/higher-order/with-global-events/test/index.jsdom.test.jsx", + "packages/compose/src/higher-order/with-global-events/test/listener.jsdom.test.js", + "packages/compose/src/higher-order/with-instance-id/test/index.jsdom.test.jsx", + "packages/compose/src/higher-order/with-state/test/index.jsdom.test.jsx", + "packages/compose/src/hooks/use-copy-on-click/test/index.jsdom.test.tsx", + "packages/compose/src/hooks/use-copy-to-clipboard/test/index.jsdom.test.tsx", + "packages/compose/src/hooks/use-dialog/test/index.jsdom.test.tsx", + "packages/compose/src/hooks/use-disabled/test/index.jsdom.test.jsx", + "packages/compose/src/hooks/use-drop-zone/test/index.jsdom.test.jsx", + "packages/compose/src/hooks/use-focus-outside/test/index.jsdom.test.jsx", + "packages/compose/src/hooks/use-instance-id/test/index.jsdom.test.jsx", + "packages/compose/src/hooks/use-media-query/test/index.jsdom.test.jsx", + "packages/compose/src/hooks/use-merge-refs/test/index.jsdom.test.jsx", + "packages/compose/src/hooks/use-observable-value/test/index.jsdom.test.jsx", + "packages/compose/src/hooks/use-state-with-history/test/index.jsdom.test.jsx", + "packages/compose/src/hooks/use-viewport-match/test/index.jsdom.test.jsx", + "packages/compose/src/utils/subscribe-delegated-listener/test/index.jsdom.test.js", + "packages/core-data/src/awareness/test/awareness-state.jsdom.test.ts", + "packages/core-data/src/awareness/test/base-awareness.jsdom.test.ts", + "packages/core-data/src/awareness/test/block-lookup.jsdom.test.ts", + "packages/core-data/src/awareness/test/post-editor-awareness.jsdom.test.ts", + "packages/core-data/src/awareness/test/typed-awareness.jsdom.test.ts", + "packages/core-data/src/awareness/test/utils.jsdom.test.ts", + "packages/core-data/src/components/entities-saved-states/test/use-is-dirty.jsdom.test.js", + "packages/core-data/src/footnotes/test/index.jsdom.test.js", + "packages/core-data/src/hooks/test/use-entity-record.jsdom.test.js", + "packages/core-data/src/hooks/test/use-entity-records.jsdom.test.js", + "packages/core-data/src/hooks/test/use-post-editor-awareness-state.jsdom.test.ts", + "packages/core-data/src/hooks/test/use-query-select.jsdom.test.js", + "packages/core-data/src/hooks/test/use-resource-permissions.jsdom.test.js", + "packages/core-data/src/queried-data/test/get-query-parts.jsdom.test.js", + "packages/core-data/src/queried-data/test/reducer.jsdom.test.js", + "packages/core-data/src/queried-data/test/selectors.jsdom.test.js", + "packages/core-data/src/test/actions.jsdom.test.js", + "packages/core-data/src/test/deprecated-entity-logging.jsdom.test.js", + "packages/core-data/src/test/entities.jsdom.test.js", + "packages/core-data/src/test/entity-provider.jsdom.test.jsx", + "packages/core-data/src/test/keyless-entities.jsdom.test.ts", + "packages/core-data/src/test/private-actions.jsdom.test.js", + "packages/core-data/src/test/private-selectors.jsdom.test.js", + "packages/core-data/src/test/reducer.jsdom.test.js", + "packages/core-data/src/test/resolvers.jsdom.test.js", + "packages/core-data/src/test/rtc-rich-text-offset-space.jsdom.test.jsx", + "packages/core-data/src/test/selectors.jsdom.test.js", + "packages/core-data/src/test/store.jsdom.test.js", + "packages/core-data/src/test/sync.jsdom.test.js", + "packages/core-data/src/utils/test/block-selection-history.jsdom.test.ts", + "packages/core-data/src/utils/test/crdt-blocks.jsdom.test.ts", + "packages/core-data/src/utils/test/crdt-user-selections.jsdom.test.ts", + "packages/core-data/src/utils/test/crdt-utils.jsdom.test.ts", + "packages/core-data/src/utils/test/crdt.jsdom.test.ts", + "packages/core-data/src/utils/test/rtc-rich-text-cursor-scope.jsdom.test.js", + "packages/core-data/src/utils/test/rtc-rich-text-offset-space.jsdom.test.js", + "packages/customize-widgets/src/components/error-boundary/test/error-boundary.jsdom.test.jsx", + "packages/data/src/components/use-dispatch/test/use-dispatch.jsdom.test.jsx", + "packages/data/src/components/use-select/test/index.jsdom.test.jsx", + "packages/data/src/components/use-select/test/suspense.jsdom.test.jsx", + "packages/data/src/components/with-dispatch/test/index.jsdom.test.jsx", + "packages/data/src/components/with-select/test/index.jsdom.test.jsx", + "packages/dataviews/src/components/dataform-controls/test/date.jsdom.test.tsx", + "packages/dataviews/src/components/dataform-controls/test/datetime.jsdom.test.tsx", + "packages/dataviews/src/components/validated-form-controls/test/checkbox-control.jsdom.test.tsx", + "packages/dataviews/src/components/validated-form-controls/test/combobox-control.jsdom.test.tsx", + "packages/dataviews/src/components/validated-form-controls/test/form-token-field.jsdom.test.tsx", + "packages/dataviews/src/components/validated-form-controls/test/number-control.jsdom.test.tsx", + "packages/dataviews/src/components/validated-form-controls/test/radio-control.jsdom.test.tsx", + "packages/dataviews/src/components/validated-form-controls/test/select-control.jsdom.test.tsx", + "packages/dataviews/src/components/validated-form-controls/test/toggle-control.jsdom.test.tsx", + "packages/dataviews/src/components/validated-form-controls/test/toggle-group-control.jsdom.test.tsx", + "packages/dataviews/src/dataform/test/dataform.jsdom.test.tsx", + "packages/dataviews/src/dataviews-picker/test/dataviews-picker.jsdom.test.tsx", + "packages/dataviews/src/dataviews/test/dataviews.jsdom.test.tsx", + "packages/dataviews/src/hooks/test/use-data.jsdom.test.ts", + "packages/dataviews/src/hooks/test/use-form-validity.jsdom.test.ts", + "packages/dom-ready/src/test/index.jsdom.test.ts", + "packages/dom/src/dom/test/safe-html.jsdom.test.js", + "packages/dom/src/dom/test/strip-html.jsdom.test.js", + "packages/dom/src/test/dom.jsdom.test.js", + "packages/dom/src/test/focusable.jsdom.test.js", + "packages/dom/src/test/tabbable.jsdom.test.js", + "packages/edit-post/src/components/browser-url/test/index.jsdom.test.jsx", + "packages/edit-post/src/components/editor-initialization/test/listener-hooks.jsdom.test.jsx", + "packages/edit-post/src/components/meta-boxes/test/use-meta-box-initialization.jsdom.test.jsx", + "packages/edit-post/src/components/preferences-modal/test/enable-custom-fields.jsdom.test.jsx", + "packages/edit-post/src/components/preferences-modal/test/meta-boxes-section.jsdom.test.jsx", + "packages/edit-post/src/store/test/actions.jsdom.test.js", + "packages/edit-post/src/store/test/selectors.jsdom.test.js", + "packages/edit-widgets/src/components/error-boundary/test/error-boundary.jsdom.test.jsx", + "packages/edit-widgets/src/store/test/selectors.jsdom.test.js", + "packages/editor/src/bindings/test/pattern-overrides.jsdom.test.js", + "packages/editor/src/bindings/test/post-data.jsdom.test.js", + "packages/editor/src/bindings/test/post-meta.jsdom.test.js", + "packages/editor/src/bindings/test/term-data.jsdom.test.js", + "packages/editor/src/components/autocompleters/test/user.jsdom.test.jsx", + "packages/editor/src/components/autosave-monitor/test/index.jsdom.test.jsx", + "packages/editor/src/components/collab-sidebar/rich-text-control/test/index.jsdom.test.tsx", + "packages/editor/src/components/collab-sidebar/test/board-store.jsdom.test.js", + "packages/editor/src/components/collab-sidebar/test/note-highlight-styles.jsdom.test.js", + "packages/editor/src/components/collab-sidebar/test/utils.jsdom.test.js", + "packages/editor/src/components/collaborators-overlay/test/cursor-dom-utils.jsdom.test.ts", + "packages/editor/src/components/collaborators-overlay/test/resolve-start-position.jsdom.test.ts", + "packages/editor/src/components/collaborators-presence/avatar-group/test/index.jsdom.test.tsx", + "packages/editor/src/components/collaborators-presence/avatar/test/index.jsdom.test.tsx", + "packages/editor/src/components/collaborators-presence/test/use-collaborator-notifications.jsdom.test.ts", + "packages/editor/src/components/more-menu/test/more-menu-group.jsdom.test.tsx", + "packages/editor/src/components/more-menu/test/more-menu-item.jsdom.test.tsx", + "packages/editor/src/components/page-attributes/test/order.jsdom.test.jsx", + "packages/editor/src/components/plugin-post-publish-panel/test/index.jsdom.test.jsx", + "packages/editor/src/components/plugin-pre-publish-panel/test/index.jsdom.test.jsx", + "packages/editor/src/components/post-author/test/check.jsdom.test.jsx", + "packages/editor/src/components/post-excerpt/test/plugin.jsdom.test.jsx", + "packages/editor/src/components/post-last-revision/test/check.jsdom.test.jsx", + "packages/editor/src/components/post-pending-status/test/check.jsdom.test.jsx", + "packages/editor/src/components/post-preview-button/test/index.jsdom.test.jsx", + "packages/editor/src/components/post-publish-button/test/index.jsdom.test.jsx", + "packages/editor/src/components/post-publish-button/test/label.jsdom.test.js", + "packages/editor/src/components/post-publish-button/test/post-publish-button-or-toggle.jsdom.test.jsx", + "packages/editor/src/components/post-publish-panel/test/index.jsdom.test.jsx", + "packages/editor/src/components/post-revisions-preview/test/block-diff.jsdom.test.js", + "packages/editor/src/components/post-revisions-preview/test/revisions-code-diff.jsdom.test.jsx", + "packages/editor/src/components/post-revisions-preview/test/revisions-slider.jsdom.test.jsx", + "packages/editor/src/components/post-revisions-timeline/test/index.jsdom.test.jsx", + "packages/editor/src/components/post-saved-state/test/index.jsdom.test.jsx", + "packages/editor/src/components/post-schedule/test/check.jsdom.test.jsx", + "packages/editor/src/components/post-schedule/test/label.jsdom.test.js", + "packages/editor/src/components/post-sticky/test/index.jsdom.test.jsx", + "packages/editor/src/components/post-taxonomies/test/hierarchical-term-selector.jsdom.test.js", + "packages/editor/src/components/post-taxonomies/test/index.jsdom.test.jsx", + "packages/editor/src/components/post-type-support-check/test/index.jsdom.test.jsx", + "packages/editor/src/components/post-view-link/test/index.jsdom.test.jsx", + "packages/editor/src/components/post-visibility/test/check.jsdom.test.jsx", + "packages/editor/src/components/preferences-modal/test/index.jsdom.test.jsx", + "packages/editor/src/components/provider/test/use-autosave-notice.jsdom.test.js", + "packages/editor/src/components/provider/test/use-network-reconnect.jsdom.test.js", + "packages/editor/src/components/theme-support-check/test/index.jsdom.test.jsx", + "packages/editor/src/components/upload-progress-snackbar/test/index.jsdom.test.jsx", + "packages/editor/src/components/visual-editor/test/get-canvas-height.jsdom.test.js", + "packages/editor/src/hooks/push-changes-to-global-styles/test/index.jsdom.test.js", + "packages/editor/src/hooks/test/default-autocompleters.jsdom.test.js", + "packages/editor/src/store/test/actions.jsdom.test.js", + "packages/editor/src/store/test/private-selectors.jsdom.test.js", + "packages/editor/src/store/test/reducer.jsdom.test.js", + "packages/editor/src/store/test/selectors.jsdom.test.jsx", + "packages/editor/src/store/utils/attach-media-in-post/test/index.jsdom.test.js", + "packages/editor/src/store/utils/attach-media-in-post/test/invalidate-attachment-resolutions.jsdom.test.js", + "packages/editor/src/utils/media-finalize/test/index.jsdom.test.js", + "packages/editor/src/utils/media-upload/test/index.jsdom.test.js", + "packages/element/src/test/create-interpolate-element.jsdom.test.tsx", + "packages/element/src/test/raw-html.jsdom.test.jsx", + "packages/fields/src/components/create-template-part-modal/test/utils.jsdom.test.js", + "packages/format-library/src/link/test/css-classes-setting.jsdom.test.jsx", + "packages/global-styles-ui/src/test/screen-block-list.jsdom.test.js", + "packages/global-styles-ui/src/test/screen-header.jsdom.test.jsx", + "packages/global-styles-ui/src/test/use-presets.jsdom.test.js", + "packages/global-styles-ui/src/variations/test/variations-panel.jsdom.test.js", + "packages/grid/src/dashboard-grid/test/item-interactions.jsdom.test.tsx", + "packages/grid/src/dashboard-grid/test/item-limits.jsdom.test.tsx", + "packages/grid/src/dashboard-grid/test/keyboard-activation.jsdom.test.tsx", + "packages/grid/src/dashboard-lanes/test/item-interactions.jsdom.test.tsx", + "packages/grid/src/dashboard-lanes/test/item-limits.jsdom.test.tsx", + "packages/grid/src/dashboard-lanes/test/keyboard-activation.jsdom.test.tsx", + "packages/grid/src/dashboard-lanes/test/use-lane-placement.jsdom.test.tsx", + "packages/grid/src/shared/test/use-span-bounds.jsdom.test.ts", + "packages/hooks/src/test/index.jsdom.test.js", + "packages/icons/src/icon/test/index.jsdom.test.tsx", + "packages/interactivity-router/src/assets/test/script-modules.jsdom.test.ts", + "packages/interactivity-router/src/assets/test/styles.jsdom.test.ts", + "packages/interactivity/src/proxies/test/context-proxy.jsdom.test.ts", + "packages/interactivity/src/proxies/test/deep-merge.jsdom.test.ts", + "packages/interactivity/src/proxies/test/state-proxy.jsdom.test.ts", + "packages/interactivity/src/proxies/test/store-proxy.jsdom.test.ts", + "packages/interactivity/src/test/types.jsdom.test.ts", + "packages/interactivity/src/test/utils.jsdom.test.ts", + "packages/interactivity/src/test/vdom.jsdom.test.ts", + "packages/interface/src/components/fullscreen-mode/test/index.jsdom.test.jsx", + "packages/keycodes/src/test/index.jsdom.test.ts", + "packages/media-editor/src/components/media-editor-crop-panel/test/index.jsdom.test.tsx", + "packages/media-editor/src/components/media-editor-image-controls/test/index.jsdom.test.tsx", + "packages/media-editor/src/components/media-editor-modal/test/index.jsdom.test.tsx", + "packages/media-editor/src/components/media-editor/test/use-crop-options.jsdom.test.tsx", + "packages/media-editor/src/components/rotation-ruler/test/index.jsdom.test.tsx", + "packages/media-editor/src/image-editor/core/export/test/canvas-renderer.jsdom.test.ts", + "packages/media-editor/src/image-editor/react/components/stencils/test/rectangle-stencil.jsdom.test.tsx", + "packages/media-editor/src/image-editor/react/components/test/cropper.jsdom.test.tsx", + "packages/media-editor/src/image-editor/react/hooks/test/use-aria-announcer.jsdom.test.ts", + "packages/media-editor/src/image-editor/react/hooks/test/use-cropper-reducer.jsdom.test.ts", + "packages/media-editor/src/image-editor/react/hooks/test/use-interaction.jsdom.test.ts", + "packages/media-editor/src/image-editor/react/hooks/test/use-transform-style.jsdom.test.ts", + "packages/media-editor/src/state/test/use-media-editor-state.jsdom.test.ts", + "packages/media-fields/src/attached_to/test/view.jsdom.test.tsx", + "packages/media-fields/src/filename/test/view.jsdom.test.tsx", + "packages/media-fields/src/media_thumbnail/test/get-best-image-url.jsdom.test.ts", + "packages/media-utils/src/components/media-upload-modal/test/index.jsdom.test.tsx", + "packages/media-utils/src/components/media-upload-modal/test/use-upload-status.jsdom.test.ts", + "packages/media-utils/src/components/media-upload/test/index.jsdom.test.js", + "packages/media-utils/src/utils/test/invalidate-attachment-resolutions.jsdom.test.ts", + "packages/media-utils/src/utils/test/sideload-media.jsdom.test.ts", + "packages/media-utils/src/utils/test/upload-media.jsdom.test.ts", + "packages/media-utils/src/utils/test/validate-file-size.jsdom.test.ts", + "packages/media-utils/src/utils/test/validate-mime-type-for-user.jsdom.test.ts", + "packages/media-utils/src/utils/test/validate-mime-type.jsdom.test.ts", + "packages/notices/src/components/inline-notices/test/index.jsdom.test.tsx", + "packages/nux/src/components/dot-tip/test/index.jsdom.test.jsx", + "packages/plugins/src/components/test/plugin-area.jsdom.test.jsx", + "packages/preferences-persistence/src/create/test/index.jsdom.test.js", + "packages/rich-text/src/test/create.jsdom.test.js", + "packages/rich-text/src/test/register-format-type.jsdom.test.js", + "packages/rich-text/src/test/to-dom.jsdom.test.js", + "packages/rich-text/src/test/to-html-string.jsdom.test.js", + "packages/rich-text/src/test/unregister-format-type.jsdom.test.js", + "packages/rich-text/src/test/update-formats.jsdom.test.js", + "packages/style-runtime/test/index.jsdom.test.ts", + "packages/sync/src/providers/http-polling/test/polling-manager.jsdom.test.ts", + "packages/sync/src/providers/test/index.jsdom.test.ts", + "packages/sync/src/test/manager.jsdom.test.ts", + "packages/theme/src/test/semantic-color-contrast.jsdom.test.ts", + "packages/theme/src/test/theme-provider.jsdom.test.tsx", + "packages/theme/src/test/use-theme-provider-styles.jsdom.test.tsx", + "packages/ui/src/alert-dialog/test/index.jsdom.test.tsx", + "packages/ui/src/breadcrumb/test/index.jsdom.test.tsx", + "packages/ui/src/button/test/button.jsdom.test.tsx", + "packages/ui/src/button/test/icon.jsdom.test.tsx", + "packages/ui/src/calendar/test/calendar.jsdom.test.tsx", + "packages/ui/src/calendar/test/localization.jsdom.test.tsx", + "packages/ui/src/calendar/test/range-calendar.jsdom.test.tsx", + "packages/ui/src/calendar/test/render-prop.jsdom.test.tsx", + "packages/ui/src/card/test/index.jsdom.test.tsx", + "packages/ui/src/collapsible-card/test/index.jsdom.test.tsx", + "packages/ui/src/collapsible/test/index.jsdom.test.tsx", + "packages/ui/src/dialog/test/index.jsdom.test.tsx", + "packages/ui/src/drawer/test/index.jsdom.test.tsx", + "packages/ui/src/empty-state/test/actions.jsdom.test.tsx", + "packages/ui/src/empty-state/test/description.jsdom.test.tsx", + "packages/ui/src/empty-state/test/icon.jsdom.test.tsx", + "packages/ui/src/empty-state/test/root.jsdom.test.tsx", + "packages/ui/src/empty-state/test/title.jsdom.test.tsx", + "packages/ui/src/empty-state/test/visual.jsdom.test.tsx", + "packages/ui/src/form/input-control/test/index.jsdom.test.tsx", + "packages/ui/src/form/primitives/autocomplete/test/index.jsdom.test.tsx", + "packages/ui/src/form/primitives/checkbox/test/index.jsdom.test.tsx", + "packages/ui/src/form/primitives/combobox/test/index.jsdom.test.tsx", + "packages/ui/src/form/primitives/control-with-error/test/control-with-error.jsdom.test.tsx", + "packages/ui/src/form/primitives/field/test/index.jsdom.test.tsx", + "packages/ui/src/form/primitives/fieldset/test/index.jsdom.test.tsx", + "packages/ui/src/form/primitives/input-layout/test/index.jsdom.test.tsx", + "packages/ui/src/form/primitives/input/test/index.jsdom.test.tsx", + "packages/ui/src/form/primitives/searchable-chip-select/test/index.jsdom.test.tsx", + "packages/ui/src/form/primitives/select/test/index.jsdom.test.tsx", + "packages/ui/src/form/primitives/textarea/test/index.jsdom.test.tsx", + "packages/ui/src/form/primitives/validity-indicator/test/validity-indicator.jsdom.test.tsx", + "packages/ui/src/form/searchable-chip-select-control/test/index.jsdom.test.tsx", + "packages/ui/src/form/select-control/test/index.jsdom.test.tsx", + "packages/ui/src/form/textarea-control/test/index.jsdom.test.tsx", + "packages/ui/src/form/with-validation/validated-input-control/test/index.jsdom.test.tsx", + "packages/ui/src/form/with-validation/validated-textarea-control/test/index.jsdom.test.tsx", + "packages/ui/src/icon-button/test/index.jsdom.test.tsx", + "packages/ui/src/icon/test/icon.jsdom.test.tsx", + "packages/ui/src/link-button/test/link-button.jsdom.test.tsx", + "packages/ui/src/link/test/index.jsdom.test.tsx", + "packages/ui/src/menu/test/index.jsdom.test.tsx", + "packages/ui/src/notice/test/index.jsdom.test.tsx", + "packages/ui/src/popover/test/index.jsdom.test.tsx", + "packages/ui/src/spinner/test/spinner.jsdom.test.tsx", + "packages/ui/src/stack/test/stack.jsdom.test.tsx", + "packages/ui/src/tabs/test/index.jsdom.test.tsx", + "packages/ui/src/tabs/test/overflow.jsdom.test.tsx", + "packages/ui/src/text/test/index.jsdom.test.tsx", + "packages/ui/src/tooltip/test/index.jsdom.test.tsx", + "packages/ui/src/utils/test/keyboard-shortcut.jsdom.test.tsx", + "packages/ui/src/utils/test/use-deprioritized-initial-focus.jsdom.test.tsx", + "packages/ui/src/utils/test/use-enable-wp-compat-overlay-slot.jsdom.test.tsx", + "packages/ui/src/utils/test/wp-compat-overlay-slot.jsdom.test.ts", + "packages/ui/src/visually-hidden/test/visually-hidden.jsdom.test.tsx", + "packages/upload-media/src/store/test/vips.jsdom.test.ts", + "packages/upload-media/src/test/feature-detection.jsdom.test.ts", + "packages/upload-media/src/test/heic-support.jsdom.test.ts", + "packages/upload-media/src/test/image-file.jsdom.test.ts", + "packages/upload-media/src/test/validate-file-size.jsdom.test.ts", + "packages/upload-media/src/test/validate-mime-type-for-user.jsdom.test.ts", + "packages/upload-media/src/test/validate-mime-type.jsdom.test.ts", + "packages/url/src/test/index.jsdom.test.js", + "packages/viewport/src/test/if-viewport-matches.jsdom.test.jsx", + "packages/viewport/src/test/with-viewport-match.jsdom.test.jsx", + "packages/views/src/test/use-view.jsdom.test.tsx", + "packages/widget-dashboard/src/test/actions.jsdom.test.tsx", + "packages/widget-dashboard/src/test/commands.jsdom.test.tsx", + "packages/widget-dashboard/src/test/dashboard-context.jsdom.test.tsx", + "packages/widget-dashboard/src/test/host-links.jsdom.test.tsx", + "packages/widget-dashboard/src/test/policy.jsdom.test.tsx", + "packages/widget-dashboard/src/test/staging.jsdom.test.tsx", + "packages/widget-dashboard/src/test/use-inline-fit.jsdom.test.tsx", + "packages/widget-dashboard/src/test/widget-attributes-transitions.jsdom.test.tsx", + "packages/widget-dashboard/src/test/widget-attributes.jsdom.test.tsx", + "packages/widget-dashboard/src/test/widget-dashboard.jsdom.test.tsx", + "packages/widget-dashboard/src/test/widget-inserter.jsdom.test.tsx", + "packages/widget-dashboard/src/test/widget-settings.jsdom.test.tsx", + "packages/widget-primitives/src/hooks/test/use-widget-types.jsdom.test.tsx", + "packages/widget-primitives/src/widget-host/test/widget-host.jsdom.test.tsx", + "routes/connectors-home/test/ai-plugin-callout.jsdom.test.tsx", + "routes/dashboard/hooks/use-dashboard-layout/test/use-dashboard-layout.jsdom.test.ts", + "routes/dashboard/widget-host/test/dashboard-widget-host-provider.jsdom.test.tsx", + "test/integration/blocks-raw-handling.jsdom.test.js", + "test/integration/full-content/full-content.jsdom.test.js", + "test/integration/is-valid-block.jsdom.test.js", + "test/integration/non-matched-tags-handling.jsdom.test.js", + "test/integration/shortcode-converter.jsdom.test.js" + ] } } diff --git a/test/unit/typings/gutenberg-vitest-test-env/index.d.ts b/test/unit/typings/gutenberg-vitest-test-env/index.d.ts index 7022c29bc4904a..e5231f24f184cf 100644 --- a/test/unit/typings/gutenberg-vitest-test-env/index.d.ts +++ b/test/unit/typings/gutenberg-vitest-test-env/index.d.ts @@ -1,4 +1,5 @@ import 'vitest'; +import type { TestingLibraryMatchers } from '@testing-library/jest-dom/matchers'; interface GutenbergVitestMatchers { toBePositionedPopover: () => void; @@ -29,6 +30,7 @@ declare global { } declare module 'vitest' { - // eslint-disable-next-line @typescript-eslint/no-unused-vars - interface Matchers< T = any > extends GutenbergVitestMatchers {} + interface Matchers< T = any > + extends GutenbergVitestMatchers, + TestingLibraryMatchers< T, void > {} } diff --git a/tools/eslint/config.mjs b/tools/eslint/config.mjs index cba3b4e9edd586..857103ba1ed06e 100644 --- a/tools/eslint/config.mjs +++ b/tools/eslint/config.mjs @@ -12,7 +12,7 @@ import tseslint from 'typescript-eslint'; import wpBuildConfig from '../../packages/wp-build/eslint-overrides.cjs'; import { discoverTestFiles, - getTestEnvironmentName, + getVitestTestsByProject, } from '../../test/unit/scripts/discover-test-files.mjs'; const require = createRequire( import.meta.url ); const rootDir = resolve( import.meta.dirname, '../..' ); @@ -21,25 +21,12 @@ const testMigration = require( join( rootDir, 'test/unit/test-migration.json' ) ); -const vitestTestPatterns = [ - ...discoverTestFiles( rootDir ).filter( - ( testPath ) => getTestEnvironmentName( testPath ) === 'node' - ), - ...testMigration.vitest.files, - ...testMigration.vitest.directories.flatMap( ( directory ) => [ - `${ directory }/**/__tests__/**/*.[jt]s?(x)`, - `${ directory }/**/test/*.[jt]s?(x)`, - `${ directory }/**/?(*.)test.[jt]s?(x)`, - ] ), -]; -const vitestJsdomTestPatterns = [ - ...testMigration.vitest.files.filter( ( file ) => - /\.jsdom\.test\.[cm]?[jt]sx?$/.test( file ) - ), - ...testMigration.vitest.directories.map( - ( directory ) => `${ directory }/**/*.jsdom.test.[cm]?[jt]s?(x)` - ), -]; +const vitestTestsByProject = getVitestTestsByProject( + discoverTestFiles( rootDir ), + testMigration +); +const vitestTestPatterns = Object.values( vitestTestsByProject ).flat(); +const vitestJsdomTestPatterns = vitestTestsByProject.jsdom; // Prefer the installed React version for linting, but fall back to the detected version. let reactVersion = 'detect'; try { From 4990435775da683578e83942a15f9e52876f2965 Mon Sep 17 00:00:00 2001 From: Marco Ciampini Date: Tue, 1 Sep 2026 22:11:55 +0200 Subject: [PATCH 2/4] Test: Cover Vitest isolation opt-outs --- test/unit/VITEST_MIGRATION.md | 4 +- .../scripts/test-infrastructure-policy.mjs | 78 ++++++++++++++++--- .../test/test-infrastructure-policy.test.js | 12 ++- 3 files changed, 78 insertions(+), 16 deletions(-) diff --git a/test/unit/VITEST_MIGRATION.md b/test/unit/VITEST_MIGRATION.md index c47ac02e4eda5f..a4abb49016bc57 100644 --- a/test/unit/VITEST_MIGRATION.md +++ b/test/unit/VITEST_MIGRATION.md @@ -1,8 +1,8 @@ # Vitest migration routing Every new JavaScript unit and integration test runs in Vitest. The migration -manifest in `test-migration.json` contains the remaining legacy Jest tests and -Jest-only infrastructure while the repository finishes moving them to Vitest. +manifest in `test-migration.json` contains the exact list of remaining legacy +Jest tests while the repository finishes moving them to Vitest. The routing validator derives the current test inventory from both runners and the repository's static test-file patterns. It does not depend on a fixed test diff --git a/test/unit/scripts/test-infrastructure-policy.mjs b/test/unit/scripts/test-infrastructure-policy.mjs index 30c200d8a00d3e..fb42e497047cee 100644 --- a/test/unit/scripts/test-infrastructure-policy.mjs +++ b/test/unit/scripts/test-infrastructure-policy.mjs @@ -4,11 +4,70 @@ import { globSync } from 'glob'; const SOURCE_IGNORES = [ '**/node_modules/**', 'vendor/**' ]; const ISOLATION_OPT_OUT_PATTERN = - /(?:^|[^\w-])--no-isolate(?=$|[^\w-])|(?:^|[^\w-])--isolate(?:=|\s+)false(?=$|[^\w-])/; + /(?:^|[^\w-])--no-isolate(?=$|[^\w-])|(?:^|[^\w-])--(?:browser\.)?isolate(?:=|\s+)false(?=$|[^\w-])/; const ROOT_ROUTING_COMMAND = 'npm run --workspace @wordpress/unit-tests test:unit:routing --'; const WORKSPACE_ROUTING_COMMAND = 'node scripts/validate-test-routing.mjs'; +function normalizeShellCommand( command ) { + return command + .replace( /\\\r?\n\s*/g, ' ' ) + .replace( /\s+/g, ' ' ) + .trim(); +} + +function disablesVitestIsolation( command ) { + return ISOLATION_OPT_OUT_PATTERN.test( normalizeShellCommand( command ) ); +} + +function getWorkflowRunBlocks( source ) { + const lines = source.split( /\r?\n/ ); + const blocks = []; + + for ( let index = 0; index < lines.length; index++ ) { + const match = lines[ index ].match( /^(\s*)(?:-\s*)?run:\s*(.*)$/ ); + if ( ! match ) { + continue; + } + + const indentation = match[ 1 ].length; + const inlineCommand = match[ 2 ].trim(); + if ( ! /^[|>][+-]?(?:\s+#.*)?$/.test( inlineCommand ) ) { + const isQuoted = + ( inlineCommand.startsWith( '"' ) && + inlineCommand.endsWith( '"' ) ) || + ( inlineCommand.startsWith( "'" ) && + inlineCommand.endsWith( "'" ) ); + blocks.push( { + command: isQuoted + ? inlineCommand.slice( 1, -1 ) + : inlineCommand, + line: index + 1, + } ); + continue; + } + + const commandLines = []; + for ( + let commandIndex = index + 1; + commandIndex < lines.length; + commandIndex++ + ) { + const commandLine = lines[ commandIndex ]; + if ( + commandLine.trim() && + commandLine.match( /^\s*/ )[ 0 ].length <= indentation + ) { + break; + } + commandLines.push( commandLine ); + } + blocks.push( { command: commandLines.join( '\n' ), line: index + 1 } ); + } + + return blocks; +} + export function validateRoutingScripts( rootPackageJson, unitTestPackageJson ) { const violations = []; if ( @@ -47,7 +106,7 @@ function findPackageScriptIsolationOptOuts( rootDir ) { for ( const [ scriptName, command ] of Object.entries( packageJson.scripts ?? {} ) ) { - if ( ISOLATION_OPT_OUT_PATTERN.test( command ) ) { + if ( disablesVitestIsolation( command ) ) { violations.push( `${ packageFile }:scripts.${ scriptName } disables Vitest module isolation: ${ command }` ); @@ -69,17 +128,16 @@ function findWorkflowIsolationOptOuts( rootDir ) { ).sort(); for ( const workflowFile of workflowFiles ) { - for ( const [ index, line ] of readFileSync( + const source = readFileSync( path.join( rootDir, workflowFile ), 'utf8' - ) - .split( /\r?\n/ ) - .entries() ) { - if ( ISOLATION_OPT_OUT_PATTERN.test( line ) ) { + ); + for ( const { command, line } of getWorkflowRunBlocks( source ) ) { + if ( disablesVitestIsolation( command ) ) { violations.push( - `${ workflowFile }:${ - index + 1 - } disables Vitest module isolation: ${ line.trim() }` + `${ workflowFile }:${ line } disables Vitest module isolation: ${ normalizeShellCommand( + command + ) }` ); } } diff --git a/test/unit/scripts/test/test-infrastructure-policy.test.js b/test/unit/scripts/test/test-infrastructure-policy.test.js index 77fd99f1dc01bd..2c2176349f38d1 100644 --- a/test/unit/scripts/test/test-infrastructure-policy.test.js +++ b/test/unit/scripts/test/test-infrastructure-policy.test.js @@ -106,7 +106,10 @@ describe( 'runner isolation policy', () => { it( 'reports command surfaces that disable module isolation', () => { const rootDir = createTemporaryDirectory(); writeJson( path.join( rootDir, 'package.json' ), { - scripts: { test: 'vitest --no-isolate' }, + scripts: { + test: 'vitest --no-isolate', + 'test:browser': 'vitest --browser.isolate=false', + }, } ); mkdirSync( path.join( rootDir, '.github/workflows' ), { recursive: true, @@ -120,13 +123,14 @@ describe( 'runner isolation policy', () => { ); writeFileSync( path.join( rootDir, '.github/actions/run-tests/action.yml' ), - "run: 'vitest --isolate false'\n" + 'run: |-\n vitest --isolate \\\n false\n' ); expect( findVitestIsolationOptOuts( rootDir ) ).toEqual( [ - ".github/actions/run-tests/action.yml:1 disables Vitest module isolation: run: 'vitest --isolate false'", - '.github/workflows/test.yml:1 disables Vitest module isolation: run: "vitest --no-isolate"', + '.github/actions/run-tests/action.yml:1 disables Vitest module isolation: vitest --isolate false', + '.github/workflows/test.yml:1 disables Vitest module isolation: vitest --no-isolate', 'package.json:scripts.test disables Vitest module isolation: vitest --no-isolate', + 'package.json:scripts.test:browser disables Vitest module isolation: vitest --browser.isolate=false', ] ); } ); } ); From 2af52635b12fe5f70ca801626cd132909d106ffd Mon Sep 17 00:00:00 2001 From: Marco Ciampini Date: Wed, 2 Sep 2026 11:09:56 +0200 Subject: [PATCH 3/4] Test: Address infrastructure review feedback --- .github/workflows/static-checks.yml | 4 +--- test/unit/VITEST_MIGRATION.md | 3 +++ .../scripts/test/test-infrastructure-policy.test.js | 12 +++--------- .../typings/gutenberg-vitest-test-env/index.d.ts | 3 ++- 4 files changed, 9 insertions(+), 13 deletions(-) diff --git a/.github/workflows/static-checks.yml b/.github/workflows/static-checks.yml index 904c1a978c17d5..6117ca1e649b69 100644 --- a/.github/workflows/static-checks.yml +++ b/.github/workflows/static-checks.yml @@ -120,9 +120,7 @@ jobs: - name: Validate JavaScript test routing if: ${{ matrix.node == '20' && matrix.os == 'ubuntu-24.04' }} - env: - ROUTING_BASE: ${{ github.event_name == 'pull_request' && github.event.pull_request.base.sha || 'HEAD^1' }} - run: npm run test:unit:routing -- --base="$ROUTING_BASE" + run: npm run test:unit:routing -- --base="$(git rev-parse HEAD^1)" - name: Build packages run: npm run build diff --git a/test/unit/VITEST_MIGRATION.md b/test/unit/VITEST_MIGRATION.md index a4abb49016bc57..fc0b64481f92e3 100644 --- a/test/unit/VITEST_MIGRATION.md +++ b/test/unit/VITEST_MIGRATION.md @@ -14,6 +14,9 @@ When writing or migrating a test: - New Node, JSDOM, and Browser Mode tests run in Vitest automatically. They do not need migration metadata. - When migrating a legacy Jest test, remove its exact path from `jest.files`. +- When renaming or moving a directory that contains legacy Jest tests, migrate + those tests to Vitest in the same pull request. The validator treats their + new paths as additions to the Jest allowlist. - Do not add directories, glob patterns, or new files to `jest.files`. The routing validator compares it with the pull request base and accepts only removals. diff --git a/test/unit/scripts/test/test-infrastructure-policy.test.js b/test/unit/scripts/test/test-infrastructure-policy.test.js index 2c2176349f38d1..293dd02d8f9de0 100644 --- a/test/unit/scripts/test/test-infrastructure-policy.test.js +++ b/test/unit/scripts/test/test-infrastructure-policy.test.js @@ -175,12 +175,6 @@ describe( 'non-hoisted Vitest infrastructure', () => { path.join( sourceConfigDir, filename ), path.join( configDir, filename ) ); - if ( filename.includes( '/matchers/' ) ) { - copyFileSync( - path.join( sourceConfigDir, filename ), - path.join( configDir, filename.slice( 0, -3 ) ) - ); - } } const vitestStore = path.join( storeDir, 'vitest' ); @@ -311,7 +305,7 @@ export const beforeAll = () => {}; } ); writeFileSync( path.join( vitestDir, 'index.d.ts' ), - 'export interface Matchers { toBe(value: unknown): void; }\nexport declare const vi: unknown;\nexport declare function expect(value: T): Matchers;\n' + 'export interface AsymmetricMatcher { asymmetricMatch(value: unknown): boolean; }\nexport interface Matchers { toBe(value: unknown): void; }\nexport declare const vi: unknown;\nexport declare function expect(value: T): Matchers;\nexport declare namespace expect { function stringContaining(value: string): AsymmetricMatcher; }\n' ); writeJson( path.join( jestDomDir, 'package.json' ), { name: '@testing-library/jest-dom', @@ -324,7 +318,7 @@ export const beforeAll = () => {}; } ); writeFileSync( path.join( jestDomDir, 'matchers.d.ts' ), - 'export interface TestingLibraryMatchers { toBeVisible(): R; }\n' + 'export interface TestingLibraryMatchers { toBeVisible(): R; toHaveAccessibleName(expected?: E | RegExp | string): R; }\n' ); copyFileSync( path.resolve( @@ -335,7 +329,7 @@ export const beforeAll = () => {}; ); writeFileSync( path.join( workspaceDir, 'matcher.test.ts' ), - "import { expect } from 'vitest';\nexpect( {} ).toHaveWarned();\nexpect( document.body ).toBeVisible();\n" + "import { expect } from 'vitest';\nexpect( {} ).toHaveWarned();\nexpect( document.body ).toBeVisible();\nexpect( document.body ).toHaveAccessibleName( expect.stringContaining( 'body' ) );\n" ); writeJson( path.join( workspaceDir, 'tsconfig.json' ), { compilerOptions: { diff --git a/test/unit/typings/gutenberg-vitest-test-env/index.d.ts b/test/unit/typings/gutenberg-vitest-test-env/index.d.ts index e5231f24f184cf..7f45ffe3c10ec2 100644 --- a/test/unit/typings/gutenberg-vitest-test-env/index.d.ts +++ b/test/unit/typings/gutenberg-vitest-test-env/index.d.ts @@ -30,7 +30,8 @@ declare global { } declare module 'vitest' { + // eslint-disable-next-line @typescript-eslint/no-unused-vars -- Keep Vitest's canonical generic matcher interface. interface Matchers< T = any > extends GutenbergVitestMatchers, - TestingLibraryMatchers< T, void > {} + TestingLibraryMatchers< any, void > {} } From aa0145eddf8b101b46026afd8d47357be84aea35 Mon Sep 17 00:00:00 2001 From: Marco Ciampini Date: Wed, 2 Sep 2026 11:35:09 +0200 Subject: [PATCH 4/4] Test: Close runner policy escape hatches --- .../scripts/test-infrastructure-policy.mjs | 162 +++++++++++++++++- .../test/test-infrastructure-policy.test.js | 64 ++++++- test/unit/scripts/validate-test-routing.mjs | 138 +++++++++++---- 3 files changed, 333 insertions(+), 31 deletions(-) diff --git a/test/unit/scripts/test-infrastructure-policy.mjs b/test/unit/scripts/test-infrastructure-policy.mjs index fb42e497047cee..b2906ce03c0d09 100644 --- a/test/unit/scripts/test-infrastructure-policy.mjs +++ b/test/unit/scripts/test-infrastructure-policy.mjs @@ -1,8 +1,18 @@ import { readFileSync } from 'node:fs'; import path from 'node:path'; import { globSync } from 'glob'; +import typescript from 'typescript'; const SOURCE_IGNORES = [ '**/node_modules/**', 'vendor/**' ]; +const JEST_CONFIG_FILE_PATTERN = /(?:^|\/)[^/]*jest[^/]*\.config\.[^/]+$/; +const JEST_COMMAND_PATTERN = + /(?:^|[^\w-])(?:jest|test-unit-js)(?=$|[^\w-])|(?:^|\s)npm\s+run(?:\s+--workspace\s+\S+)?\s+test:unit(?::(?:debug|profile|update|watch))?(?=$|\s)/; +const JEST_DEPENDENCY_SECTIONS = [ + 'dependencies', + 'devDependencies', + 'optionalDependencies', + 'peerDependencies', +]; const ISOLATION_OPT_OUT_PATTERN = /(?:^|[^\w-])--no-isolate(?=$|[^\w-])|(?:^|[^\w-])--(?:browser\.)?isolate(?:=|\s+)false(?=$|[^\w-])/; const ROOT_ROUTING_COMMAND = @@ -16,6 +26,28 @@ function normalizeShellCommand( command ) { .trim(); } +function isJestOnlyDependency( dependency ) { + return ( + dependency !== '@testing-library/jest-dom' && + ( dependency.startsWith( '@jest/' ) || + /(?:^|[\/_-])jest(?:$|[\/_-])/.test( dependency ) ) + ); +} + +function getNpmAliasTarget( specifier ) { + if ( typeof specifier !== 'string' ) { + return null; + } + + return ( + specifier.match( /^npm:(@[^/]+\/[^@]+|[^@]+)(?:@|$)/ )?.[ 1 ] ?? null + ); +} + +function usesJestCommand( command ) { + return JEST_COMMAND_PATTERN.test( normalizeShellCommand( command ) ); +} + function disablesVitestIsolation( command ) { return ISOLATION_OPT_OUT_PATTERN.test( normalizeShellCommand( command ) ); } @@ -25,7 +57,9 @@ function getWorkflowRunBlocks( source ) { const blocks = []; for ( let index = 0; index < lines.length; index++ ) { - const match = lines[ index ].match( /^(\s*)(?:-\s*)?run:\s*(.*)$/ ); + const match = lines[ index ].match( + /^(\s*)(?:-\s*)?(?:run|["']run["']):\s*(.*)$/ + ); if ( ! match ) { continue; } @@ -68,6 +102,63 @@ function getWorkflowRunBlocks( source ) { return blocks; } +export function collectJestInfrastructureEntries( files, readSource ) { + const entries = new Set(); + + for ( const file of files ) { + const source = readSource( file ); + if ( source === null ) { + continue; + } + + if ( JEST_CONFIG_FILE_PATTERN.test( file ) ) { + entries.add( `config:${ file }` ); + } + + if ( file.endsWith( '/package.json' ) || file === 'package.json' ) { + const packageJson = JSON.parse( source ); + if ( packageJson.jest !== undefined ) { + entries.add( `config:${ file }:jest` ); + } + for ( const section of JEST_DEPENDENCY_SECTIONS ) { + for ( const [ dependency, specifier ] of Object.entries( + packageJson[ section ] ?? {} + ) ) { + const aliasTarget = getNpmAliasTarget( specifier ); + if ( + isJestOnlyDependency( dependency ) || + ( aliasTarget && isJestOnlyDependency( aliasTarget ) ) + ) { + entries.add( + `dependency:${ file }:${ section }.${ dependency }` + ); + } + } + } + for ( const [ scriptName, command ] of Object.entries( + packageJson.scripts ?? {} + ) ) { + if ( usesJestCommand( command ) ) { + entries.add( + `command:${ file }:scripts.${ scriptName }=${ command }` + ); + } + } + } + + if ( /^\.github\/(?:actions|workflows)\/.*\.ya?ml$/.test( file ) ) { + for ( const { command } of getWorkflowRunBlocks( source ) ) { + const normalizedCommand = normalizeShellCommand( command ); + if ( usesJestCommand( normalizedCommand ) ) { + entries.add( `command:${ file }=${ normalizedCommand }` ); + } + } + } + } + + return [ ...entries ].sort(); +} + export function validateRoutingScripts( rootPackageJson, unitTestPackageJson ) { const violations = []; if ( @@ -146,8 +237,77 @@ function findWorkflowIsolationOptOuts( rootDir ) { return violations; } +function getPropertyName( property ) { + if ( ! property.name ) { + return null; + } + + if ( + typescript.isIdentifier( property.name ) || + typescript.isStringLiteral( property.name ) + ) { + return property.name.text; + } + if ( + typescript.isComputedPropertyName( property.name ) && + typescript.isStringLiteral( property.name.expression ) + ) { + return property.name.expression.text; + } + + return null; +} + +function findConfigIsolationOptOuts( rootDir ) { + const violations = []; + const configFiles = globSync( + '**/{vite,vitest}.config.{js,jsx,cjs,mjs,ts,tsx,cts,mts}', + { + cwd: rootDir, + ignore: SOURCE_IGNORES, + nodir: true, + } + ).sort(); + + for ( const configFile of configFiles ) { + const source = readFileSync( path.join( rootDir, configFile ), 'utf8' ); + const sourceFile = typescript.createSourceFile( + configFile, + source, + typescript.ScriptTarget.Latest, + true + ); + + function visit( node ) { + if ( + node.parent && + typescript.isObjectLiteralExpression( node.parent ) && + getPropertyName( node ) === 'isolate' && + ( ! typescript.isPropertyAssignment( node ) || + node.initializer.kind !== + typescript.SyntaxKind.TrueKeyword ) + ) { + const { line } = sourceFile.getLineAndCharacterOfPosition( + node.getStart( sourceFile ) + ); + violations.push( + `${ configFile }:${ + line + 1 + } must set isolate to the literal value true` + ); + } + typescript.forEachChild( node, visit ); + } + + visit( sourceFile ); + } + + return violations; +} + export function findVitestIsolationOptOuts( rootDir ) { return [ + ...findConfigIsolationOptOuts( rootDir ), ...findPackageScriptIsolationOptOuts( rootDir ), ...findWorkflowIsolationOptOuts( rootDir ), ].sort(); diff --git a/test/unit/scripts/test/test-infrastructure-policy.test.js b/test/unit/scripts/test/test-infrastructure-policy.test.js index 293dd02d8f9de0..d1b9c375e2b607 100644 --- a/test/unit/scripts/test/test-infrastructure-policy.test.js +++ b/test/unit/scripts/test/test-infrastructure-policy.test.js @@ -16,6 +16,7 @@ import { afterEach, describe, expect, it } from 'vitest'; import { resolvePackageBin } from '../resolve-package-bin.mjs'; import { resolveTypeRoots } from '../resolve-type-roots.mjs'; import { + collectJestInfrastructureEntries, findVitestIsolationOptOuts, validateRoutingScripts, } from '../test-infrastructure-policy.mjs'; @@ -100,6 +101,51 @@ describe( 'test infrastructure policy', () => { ) ).toEqual( [] ); } ); + + it( 'inventories Jest-only configuration, dependencies, and commands', () => { + const files = [ + '.github/workflows/test.yml', + 'packages/example/jest.config.js', + 'packages/example/package.json', + ]; + const sources = { + '.github/workflows/test.yml': + '"run": npm run test:unit:debug -- --runInBand\n', + 'packages/example/jest.config.js': 'module.exports = {};\n', + 'packages/example/package.json': JSON.stringify( { + jest: {}, + devDependencies: { + '@jest/globals': '^30.0.0', + '@testing-library/jest-dom': '^6.9.1', + '@types/jest': '^30.0.0', + 'legacy-test': 'npm:@types/jest@^30.0.0', + 'test-runner': 'npm:jest@^30.0.0', + }, + scripts: { + test: 'wp-scripts test-unit-js --config jest.config.js', + vitest: 'npm run test:unit:vitest', + watch: 'npm run test:unit:watch', + }, + } ), + }; + + expect( + collectJestInfrastructureEntries( + files, + ( file ) => sources[ file ] ?? null + ) + ).toEqual( [ + 'command:.github/workflows/test.yml=npm run test:unit:debug -- --runInBand', + 'command:packages/example/package.json:scripts.test=wp-scripts test-unit-js --config jest.config.js', + 'command:packages/example/package.json:scripts.watch=npm run test:unit:watch', + 'config:packages/example/jest.config.js', + 'config:packages/example/package.json:jest', + 'dependency:packages/example/package.json:devDependencies.@jest/globals', + 'dependency:packages/example/package.json:devDependencies.@types/jest', + 'dependency:packages/example/package.json:devDependencies.legacy-test', + 'dependency:packages/example/package.json:devDependencies.test-runner', + ] ); + } ); } ); describe( 'runner isolation policy', () => { @@ -117,9 +163,23 @@ describe( 'runner isolation policy', () => { mkdirSync( path.join( rootDir, '.github/actions/run-tests' ), { recursive: true, } ); + writeFileSync( + path.join( rootDir, 'vitest.config.mjs' ), + `const isolate = true; +export default { + test: { + isolate, + projects: [ + { test: { browser: { [ 'isolate' ]: false } } }, + { test: { isolate: true } }, + ], + }, +}; +` + ); writeFileSync( path.join( rootDir, '.github/workflows/test.yml' ), - 'run: "vitest --no-isolate"\n' + '\'run\': "vitest --no-isolate"\n' ); writeFileSync( path.join( rootDir, '.github/actions/run-tests/action.yml' ), @@ -131,6 +191,8 @@ describe( 'runner isolation policy', () => { '.github/workflows/test.yml:1 disables Vitest module isolation: vitest --no-isolate', 'package.json:scripts.test disables Vitest module isolation: vitest --no-isolate', 'package.json:scripts.test:browser disables Vitest module isolation: vitest --browser.isolate=false', + 'vitest.config.mjs:4 must set isolate to the literal value true', + 'vitest.config.mjs:6 must set isolate to the literal value true', ] ); } ); } ); diff --git a/test/unit/scripts/validate-test-routing.mjs b/test/unit/scripts/validate-test-routing.mjs index 715cbe9a5f9f32..42e26e8c6fb20d 100644 --- a/test/unit/scripts/validate-test-routing.mjs +++ b/test/unit/scripts/validate-test-routing.mjs @@ -14,6 +14,7 @@ import { VITEST_PROJECT_NAMES, } from './discover-test-files.mjs'; import { resolvePackageBin } from './resolve-package-bin.mjs'; +import { collectJestInfrastructureEntries } from './test-infrastructure-policy.mjs'; import { sourceHasTestEnvironmentOverride } from './test-environment-overrides.mjs'; const ROOT_DIR = path.resolve( @@ -38,6 +39,19 @@ function normalizeTestPath( testPath ) { } function listTests( packageName, args ) { + const lines = runTestList( packageName, args ); + + return new Set( + lines + .map( ( testPath ) => testPath.replace( /^\[[^\]]+\]\s+/, '' ) ) + .filter( ( testPath ) => + existsSync( path.resolve( ROOT_DIR, testPath ) ) + ) + .map( normalizeTestPath ) + ); +} + +function runTestList( packageName, args ) { const result = spawnSync( process.execPath, [ resolvePackageBin( packageName ), ...args ], @@ -59,17 +73,42 @@ function listTests( packageName, args ) { process.exit( result.status ?? 1 ); } - return new Set( - result.stdout - .trim() - .split( /\r?\n/ ) - .filter( Boolean ) - .map( ( testPath ) => testPath.replace( /^\[[^\]]+\]\s+/, '' ) ) - .filter( ( testPath ) => - existsSync( path.resolve( ROOT_DIR, testPath ) ) - ) - .map( normalizeTestPath ) + return result.stdout.trim().split( /\r?\n/ ).filter( Boolean ); +} + +function listVitestTestsByProject() { + const testsByProject = Object.fromEntries( + VITEST_PROJECT_NAMES.map( ( projectName ) => [ + projectName, + new Set(), + ] ) ); + const lines = runTestList( 'vitest', [ + 'list', + '--config', + VITEST_CONFIG, + '--filesOnly', + '--passWithNoTests', + ] ); + + for ( const line of lines ) { + const match = line.match( /^\[([^\]]+)\]\s+(.+)$/ ); + assert.ok( match, `Unexpected Vitest list output: ${ line }` ); + const [ , projectName, testPath ] = match; + assert.ok( + testsByProject[ projectName ], + `Unexpected Vitest project \`${ projectName }\`. Expected only: ${ VITEST_PROJECT_NAMES.join( + ', ' + ) }` + ); + assert.ok( + existsSync( path.resolve( ROOT_DIR, testPath ) ), + `Vitest ${ projectName } listed a missing test: ${ testPath }` + ); + testsByProject[ projectName ].add( normalizeTestPath( testPath ) ); + } + + return testsByProject; } function assertUniquePaths( label, testPaths ) { @@ -138,8 +177,7 @@ function getBaselineRef() { return mergeBase.status === 0 ? mergeBase.stdout.trim() : 'HEAD'; } -function readBaselineMigrationManifest() { - const baselineRef = getBaselineRef(); +function readBaselineMigrationManifest( baselineRef ) { const result = runGit( [ 'show', `${ baselineRef }:test/unit/test-migration.json`, @@ -157,6 +195,31 @@ function readBaselineMigrationManifest() { return JSON.parse( result.stdout ); } +function getChangedFiles( baselineRef ) { + const result = runGit( [ + 'diff', + '--name-only', + '--diff-filter=ACMR', + baselineRef, + '--', + ] ); + assert.equal( + result.status, + 0, + 'Could not compare Jest infrastructure with ' + + baselineRef + + ':\n' + + result.stderr + ); + + return result.stdout.trim().split( /\r?\n/ ).filter( Boolean ); +} + +function readBaselineFile( baselineRef, file ) { + const result = runGit( [ 'show', `${ baselineRef }:${ file }` ] ); + return result.status === 0 ? result.stdout : null; +} + const staticInventory = discoverTestFiles( ROOT_DIR ); const testsWithEnvironmentOverrides = staticInventory.filter( ( testPath ) => sourceHasTestEnvironmentOverride( @@ -216,22 +279,14 @@ const jestTests = new Set( ...projectTests, ] ) ); -const vitestTestsByProject = Object.fromEntries( - VITEST_PROJECT_NAMES.map( ( projectName ) => [ - projectName, - existsSync( path.join( ROOT_DIR, VITEST_CONFIG ) ) - ? listTests( 'vitest', [ - 'list', - '--config', - VITEST_CONFIG, - '--project', - projectName, - '--filesOnly', - '--passWithNoTests', - ] ) - : new Set(), - ] ) -); +const vitestTestsByProject = existsSync( path.join( ROOT_DIR, VITEST_CONFIG ) ) + ? listVitestTestsByProject() + : Object.fromEntries( + VITEST_PROJECT_NAMES.map( ( projectName ) => [ + projectName, + new Set(), + ] ) + ); const overlappingVitestProjectTests = findOverlappingVitestProjectTests( vitestTestsByProject ); assert.deepEqual( @@ -257,7 +312,8 @@ const vitestTests = new Set( ); const legacyJestTestFiles = manifest.jest.files; -const baselineMigrationManifest = readBaselineMigrationManifest(); +const baselineRef = getBaselineRef(); +const baselineMigrationManifest = readBaselineMigrationManifest( baselineRef ); assertUniquePaths( 'jest.files', legacyJestTestFiles ); @@ -275,6 +331,30 @@ if ( baselineMigrationManifest?.jest?.files ) { ); } +const changedFiles = getChangedFiles( baselineRef ); +const currentJestInfrastructure = collectJestInfrastructureEntries( + changedFiles, + ( file ) => { + const filename = path.join( ROOT_DIR, file ); + return existsSync( filename ) ? readFileSync( filename, 'utf8' ) : null; + } +); +const baselineJestInfrastructure = new Set( + collectJestInfrastructureEntries( changedFiles, ( file ) => + readBaselineFile( baselineRef, file ) + ) +); +const addedJestInfrastructure = currentJestInfrastructure.filter( + ( entry ) => ! baselineJestInfrastructure.has( entry ) +); +assert.deepEqual( + addedJestInfrastructure, + [], + `New Jest-only configuration, dependencies, and commands are not allowed:\n${ addedJestInfrastructure.join( + '\n' + ) }` +); + const invalidLegacyJestEntries = legacyJestTestFiles.filter( ( testPath ) => ! isValidManifestPath( testPath, 'isFile' ) ||