diff --git a/.gitignore b/.gitignore index b742a72568..0ed3cdec56 100644 --- a/.gitignore +++ b/.gitignore @@ -15,6 +15,7 @@ dist .lighthouse **/.visual/dist **/.visual/.tmp-* +**/.visual/port-* *.diff.png coverage vendor/ diff --git a/projects/core/vitest.visual.ts b/projects/core/vitest.visual.ts index 64282ea72d..25214cd1f9 100644 --- a/projects/core/vitest.visual.ts +++ b/projects/core/vitest.visual.ts @@ -4,6 +4,9 @@ import { libraryVisualTestConfig } from '@internals/vite/configs/visual.js'; export default mergeConfig(libraryVisualTestConfig, { root: import.meta.dirname, test: { + fileParallelism: true, + maxWorkers: 2, + maxConcurrency: 1, include: ['src/**/*.test.visual.ts', '!src/internal/styles/*.test.visual.ts'], outputFile: { junit: './coverage/visual/junit.xml' diff --git a/projects/internals/vite/src/configs/visual.js b/projects/internals/vite/src/configs/visual.js index 805421eaf0..5d96f141e5 100644 --- a/projects/internals/vite/src/configs/visual.js +++ b/projects/internals/vite/src/configs/visual.js @@ -15,6 +15,7 @@ export const libraryVisualTestConfig = { } }, test: { + runner: '@internals/vite/runners/visual-test.js', retry: 2, isolate: false, fileParallelism: false, diff --git a/projects/internals/vite/src/runners/playwright.js b/projects/internals/vite/src/runners/playwright.js index 80bbab6955..1c1abdbb25 100644 --- a/projects/internals/vite/src/runners/playwright.js +++ b/projects/internals/vite/src/runners/playwright.js @@ -98,13 +98,7 @@ export class VitePlaywrightRunner { async open() { if (!this.#server) { - if (!fs.existsSync(this.#root)) { - fs.mkdirSync(this.#root); - } - - if (!fs.existsSync(this.#dist)) { - fs.mkdirSync(this.#dist); - } + fs.mkdirSync(this.#dist, { recursive: true }); console.log('playwright-runner: launching browser'); // Chromium reserves the debugging port itself, avoiding a free-port race. @@ -138,10 +132,10 @@ export class VitePlaywrightRunner { } } - async close() { + async close({ collectReports: collect = true } = {}) { const failures = []; try { - if (fs.existsSync(this.#dist)) { + if (collect && fs.existsSync(this.#dist)) { const report = collectReports(this.#root); fs.writeFileSync(`${this.#dist}/report.json`, JSON.stringify(report, null, 2)); } diff --git a/projects/internals/vite/src/runners/visual-test.js b/projects/internals/vite/src/runners/visual-test.js new file mode 100644 index 0000000000..3547903d6e --- /dev/null +++ b/projects/internals/vite/src/runners/visual-test.js @@ -0,0 +1,21 @@ +import { TestRunner } from 'vitest'; +import { visualRunner } from './visual.js'; + +/** Keeps one browser per worker and closes it after the worker's files finish. */ +export default class VisualTestRunner extends TestRunner { + constructor(config) { + super(config); + this.onCleanupWorkerContext?.(() => visualRunner.close()); + } + + async onAfterRunFiles(files) { + try { + // VM pools do not expose worker cleanup and cannot retain the browser between contexts. + if (!this.onCleanupWorkerContext) { + await visualRunner.close(); + } + } finally { + await super.onAfterRunFiles(files); + } + } +} diff --git a/projects/internals/vite/src/runners/visual.js b/projects/internals/vite/src/runners/visual.js index c9b0f7255b..7b0c3432eb 100644 --- a/projects/internals/vite/src/runners/visual.js +++ b/projects/internals/vite/src/runners/visual.js @@ -19,7 +19,11 @@ export class VisualRunner { constructor({ chromiumArgs = CHROMIUM_ARGS } = {}) { this.#chromiumArgs = chromiumArgs; - this.#runner = new VitePlaywrightRunner({ runnerID: RUNNER_ID, chromiumArgs }); + this.#runner = new VitePlaywrightRunner({ + runnerID: RUNNER_ID, + instanceID: process.env.VITEST_POOL_ID ?? '1', + chromiumArgs + }); } async open() { @@ -27,7 +31,8 @@ export class VisualRunner { } async close() { - await this.#runner.close(); + // Visual results belong to Vitest; workers must not write a shared runner report. + await this.#runner.close({ collectReports: false }); } async render(name, content, options = { network: false }) { diff --git a/projects/internals/vite/src/setup/visual.js b/projects/internals/vite/src/setup/visual.js index f0313e616e..ffa5429765 100644 --- a/projects/internals/vite/src/setup/visual.js +++ b/projects/internals/vite/src/setup/visual.js @@ -2,11 +2,6 @@ import './lit.js'; import { beforeAll } from 'vitest'; import { visualRunner } from '../runners/visual.js'; -let runnerInstance = null; - beforeAll(async () => { - if (!runnerInstance) { - runnerInstance = visualRunner; - await runnerInstance.open(); - } + await visualRunner.open(); }); diff --git a/projects/media/vitest.visual.ts b/projects/media/vitest.visual.ts index 58fed0d07f..c443ae0bfc 100644 --- a/projects/media/vitest.visual.ts +++ b/projects/media/vitest.visual.ts @@ -4,6 +4,9 @@ import { libraryVisualTestConfig } from '@internals/vite/configs/visual.js'; export default mergeConfig(libraryVisualTestConfig, { root: import.meta.dirname, test: { + fileParallelism: true, + maxWorkers: 2, + maxConcurrency: 1, include: ['src/**/*.test.visual.ts'], outputFile: { junit: './coverage/visual/junit.xml' diff --git a/projects/monaco/src/fixtures.test.visual.ts b/projects/monaco/src/fixtures.test.visual.ts new file mode 100644 index 0000000000..500fb3c9ad --- /dev/null +++ b/projects/monaco/src/fixtures.test.visual.ts @@ -0,0 +1,42 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { template as inputTemplate, readyCheck as inputReadyCheck } from './input/input.test.visual.js'; +import { template as editorTemplate, readyCheck as editorReadyCheck } from './editor/editor.test.visual.js'; +import { + template as diffEditorTemplate, + readyCheck as diffEditorReadyCheck +} from './diff-editor/diff-editor.test.visual.js'; +import { + template as diffInputTemplate, + readyCheck as diffInputReadyCheck +} from './diff-input/diff-input.test.visual.js'; +import { template as problemsTemplate, readyCheck as problemsReadyCheck } from './problems/problems.test.visual.js'; + +export async function readyCheck(evaluate: (fn: () => boolean) => Promise) { + await inputReadyCheck(evaluate); + await editorReadyCheck(evaluate); + await diffEditorReadyCheck(evaluate); + await diffInputReadyCheck(evaluate); + await problemsReadyCheck(evaluate); +} + +export function template(theme: '' | 'dark' = '') { + return /* html */ ` + +
+ ${inputTemplate()} + ${editorTemplate()} + ${diffEditorTemplate()} + ${diffInputTemplate()} + ${problemsTemplate()} +
`; +} diff --git a/projects/monaco/src/index.dark.test.visual.ts b/projects/monaco/src/index.dark.test.visual.ts new file mode 100644 index 0000000000..f3faaa96f5 --- /dev/null +++ b/projects/monaco/src/index.dark.test.visual.ts @@ -0,0 +1,13 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { expect, test, describe } from 'vitest'; +import { visualRunner } from '@internals/vite'; +import { template, readyCheck } from './fixtures.test.visual.js'; + +describe('monaco input visual', () => { + test('input should match visual baseline dark theme', async () => { + const report = await visualRunner.render('monaco.dark', template('dark'), { network: true, waitFor: readyCheck }); + expect(report.maxDiffPercentage).toBeLessThanOrEqual(5); + }); +}); diff --git a/projects/monaco/src/index.test.visual.ts b/projects/monaco/src/index.test.visual.ts index 42778e80c4..d4dec4043c 100644 --- a/projects/monaco/src/index.test.visual.ts +++ b/projects/monaco/src/index.test.visual.ts @@ -3,54 +3,11 @@ import { expect, test, describe } from 'vitest'; import { visualRunner } from '@internals/vite'; -import { template as inputTemplate, readyCheck as inputReadyCheck } from './input/input.test.visual.js'; -import { template as editorTemplate, readyCheck as editorReadyCheck } from './editor/editor.test.visual.js'; -import { - template as diffEditorTemplate, - readyCheck as diffEditorReadyCheck -} from './diff-editor/diff-editor.test.visual.js'; -import { - template as diffInputTemplate, - readyCheck as diffInputReadyCheck -} from './diff-input/diff-input.test.visual.js'; -import { template as problemsTemplate, readyCheck as problemsReadyCheck } from './problems/problems.test.visual.js'; +import { template, readyCheck } from './fixtures.test.visual.js'; describe('monaco input visual', () => { test('input should match visual baseline', async () => { const report = await visualRunner.render('monaco', template(''), { network: true, waitFor: readyCheck }); expect(report.maxDiffPercentage).toBeLessThanOrEqual(5); }); - - test('input should match visual baseline dark theme', async () => { - const report = await visualRunner.render('monaco.dark', template('dark'), { network: true, waitFor: readyCheck }); - expect(report.maxDiffPercentage).toBeLessThanOrEqual(5); - }); }); - -async function readyCheck(evaluate: (fn: () => boolean) => Promise) { - await inputReadyCheck(evaluate); - await editorReadyCheck(evaluate); - await diffEditorReadyCheck(evaluate); - await diffInputReadyCheck(evaluate); - await problemsReadyCheck(evaluate); -} - -function template(theme: '' | 'dark' = '') { - return /* html */ ` - -
- ${inputTemplate()} - ${editorTemplate()} - ${diffEditorTemplate()} - ${diffInputTemplate()} - ${problemsTemplate()} -
`; -} diff --git a/projects/monaco/vitest.visual.ts b/projects/monaco/vitest.visual.ts index c17cef23ca..96e27d38f1 100644 --- a/projects/monaco/vitest.visual.ts +++ b/projects/monaco/vitest.visual.ts @@ -4,7 +4,10 @@ import { libraryVisualTestConfig } from '@internals/vite/configs/visual.js'; export default mergeConfig(libraryVisualTestConfig, { root: import.meta.dirname, test: { - include: ['src/index.test.visual.ts'], + fileParallelism: true, + maxWorkers: 2, + maxConcurrency: 1, + include: ['src/index*.test.visual.ts'], outputFile: { junit: './coverage/visual/junit.xml' } diff --git a/projects/viewport/vitest.visual.ts b/projects/viewport/vitest.visual.ts index 58fed0d07f..c443ae0bfc 100644 --- a/projects/viewport/vitest.visual.ts +++ b/projects/viewport/vitest.visual.ts @@ -4,6 +4,9 @@ import { libraryVisualTestConfig } from '@internals/vite/configs/visual.js'; export default mergeConfig(libraryVisualTestConfig, { root: import.meta.dirname, test: { + fileParallelism: true, + maxWorkers: 2, + maxConcurrency: 1, include: ['src/**/*.test.visual.ts'], outputFile: { junit: './coverage/visual/junit.xml'