chore: make watch + ct happy (#20804)
This commit is contained in:
parent
e03f0ea309
commit
1ba768bf60
|
|
@ -32,7 +32,10 @@ const cacheDir = process.env.PWTEST_CACHE_DIR || path.join(os.tmpdir(), 'playwri
|
||||||
|
|
||||||
const sourceMaps: Map<string, string> = new Map();
|
const sourceMaps: Map<string, string> = new Map();
|
||||||
const memoryCache = new Map<string, MemoryCache>();
|
const memoryCache = new Map<string, MemoryCache>();
|
||||||
|
// Dependencies resolved by the loader.
|
||||||
const fileDependencies = new Map<string, Set<string>>();
|
const fileDependencies = new Map<string, Set<string>>();
|
||||||
|
// Dependencies resolved by the external bundler.
|
||||||
|
const externalDependencies = new Map<string, Set<string>>();
|
||||||
|
|
||||||
Error.stackTraceLimit = 200;
|
Error.stackTraceLimit = 200;
|
||||||
|
|
||||||
|
|
@ -93,6 +96,7 @@ export function serializeCompilationCache(): any {
|
||||||
sourceMaps: [...sourceMaps.entries()],
|
sourceMaps: [...sourceMaps.entries()],
|
||||||
memoryCache: [...memoryCache.entries()],
|
memoryCache: [...memoryCache.entries()],
|
||||||
fileDependencies: [...fileDependencies.entries()].map(([filename, deps]) => ([filename, [...deps]])),
|
fileDependencies: [...fileDependencies.entries()].map(([filename, deps]) => ([filename, [...deps]])),
|
||||||
|
externalDependencies: [...externalDependencies.entries()].map(([filename, deps]) => ([filename, [...deps]])),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -108,6 +112,8 @@ export function addToCompilationCache(payload: any) {
|
||||||
memoryCache.set(entry[0], entry[1]);
|
memoryCache.set(entry[0], entry[1]);
|
||||||
for (const entry of payload.fileDependencies)
|
for (const entry of payload.fileDependencies)
|
||||||
fileDependencies.set(entry[0], new Set(entry[1]));
|
fileDependencies.set(entry[0], new Set(entry[1]));
|
||||||
|
for (const entry of payload.externalDependencies)
|
||||||
|
externalDependencies.set(entry[0], new Set(entry[1]));
|
||||||
}
|
}
|
||||||
|
|
||||||
function calculateCachePath(content: string, filePath: string, isModule: boolean): string {
|
function calculateCachePath(content: string, filePath: string, isModule: boolean): string {
|
||||||
|
|
@ -146,6 +152,11 @@ export function currentFileDepsCollector(): Set<string> | undefined {
|
||||||
return depsCollector;
|
return depsCollector;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function setExternalDependencies(filename: string, deps: string[]) {
|
||||||
|
const depsSet = new Set(deps.filter(dep => !belongsToNodeModules(dep) && dep !== filename));
|
||||||
|
externalDependencies.set(filename, depsSet);
|
||||||
|
}
|
||||||
|
|
||||||
export function fileDependenciesForTest() {
|
export function fileDependenciesForTest() {
|
||||||
return fileDependencies;
|
return fileDependencies;
|
||||||
}
|
}
|
||||||
|
|
@ -156,6 +167,10 @@ export function collectAffectedTestFiles(dependency: string, testFileCollector:
|
||||||
if (deps.has(dependency))
|
if (deps.has(dependency))
|
||||||
testFileCollector.add(testFile);
|
testFileCollector.add(testFile);
|
||||||
}
|
}
|
||||||
|
for (const [testFile, deps] of externalDependencies) {
|
||||||
|
if (deps.has(dependency))
|
||||||
|
testFileCollector.add(testFile);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// These two are only used in the dev mode, they are specifically excluding
|
// These two are only used in the dev mode, they are specifically excluding
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@
|
||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
import type { Suite } from '../../types/testReporter';
|
import type { Suite } from '../../types/testReporter';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import type { InlineConfig, Plugin } from 'vite';
|
import type { InlineConfig, Plugin, ResolveFn, ResolvedConfig } from 'vite';
|
||||||
import type { TestRunnerPlugin } from '.';
|
import type { TestRunnerPlugin } from '.';
|
||||||
import { parse, traverse, types as t } from '../common/babelBundle';
|
import { parse, traverse, types as t } from '../common/babelBundle';
|
||||||
import { stoppable } from '../utilsBundle';
|
import { stoppable } from '../utilsBundle';
|
||||||
|
|
@ -28,6 +28,8 @@ import { assert, calculateSha1 } from 'playwright-core/lib/utils';
|
||||||
import type { AddressInfo } from 'net';
|
import type { AddressInfo } from 'net';
|
||||||
import { getPlaywrightVersion } from 'playwright-core/lib/utils';
|
import { getPlaywrightVersion } from 'playwright-core/lib/utils';
|
||||||
import type { PlaywrightTestConfig as BasePlaywrightTestConfig } from '@playwright/test';
|
import type { PlaywrightTestConfig as BasePlaywrightTestConfig } from '@playwright/test';
|
||||||
|
import type { PluginContext } from 'rollup';
|
||||||
|
import { setExternalDependencies } from '../common/compilationCache';
|
||||||
|
|
||||||
let stoppableServer: any;
|
let stoppableServer: any;
|
||||||
const playwrightVersion = getPlaywrightVersion();
|
const playwrightVersion = getPlaywrightVersion();
|
||||||
|
|
@ -155,6 +157,9 @@ export function createPlugin(
|
||||||
if (hasNewTests || hasNewComponents || sourcesDirty)
|
if (hasNewTests || hasNewComponents || sourcesDirty)
|
||||||
await fs.promises.writeFile(buildInfoFile, JSON.stringify(buildInfo, undefined, 2));
|
await fs.promises.writeFile(buildInfoFile, JSON.stringify(buildInfo, undefined, 2));
|
||||||
|
|
||||||
|
for (const [filename, testInfo] of Object.entries(buildInfo.tests))
|
||||||
|
setExternalDependencies(filename, testInfo.deps);
|
||||||
|
|
||||||
const previewServer = await preview(viteConfig);
|
const previewServer = await preview(viteConfig);
|
||||||
stoppableServer = stoppable(previewServer.httpServer, 0);
|
stoppableServer = stoppable(previewServer.httpServer, 0);
|
||||||
const isAddressInfo = (x: any): x is AddressInfo => x?.address;
|
const isAddressInfo = (x: any): x is AddressInfo => x?.address;
|
||||||
|
|
@ -185,6 +190,7 @@ type BuildInfo = {
|
||||||
[key: string]: {
|
[key: string]: {
|
||||||
timestamp: number;
|
timestamp: number;
|
||||||
components: string[];
|
components: string[];
|
||||||
|
deps: string[];
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
@ -218,7 +224,7 @@ async function checkNewTests(suite: Suite, buildInfo: BuildInfo, componentRegist
|
||||||
const components = await parseTestFile(testFile);
|
const components = await parseTestFile(testFile);
|
||||||
for (const component of components)
|
for (const component of components)
|
||||||
componentRegistry.set(component.fullName, component);
|
componentRegistry.set(component.fullName, component);
|
||||||
buildInfo.tests[testFile] = { timestamp, components: components.map(c => c.fullName) };
|
buildInfo.tests[testFile] = { timestamp, components: components.map(c => c.fullName), deps: [] };
|
||||||
hasNewTests = true;
|
hasNewTests = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -272,10 +278,15 @@ async function parseTestFile(testFile: string): Promise<ComponentInfo[]> {
|
||||||
|
|
||||||
function vitePlugin(registerSource: string, relativeTemplateDir: string, buildInfo: BuildInfo, componentRegistry: ComponentRegistry): Plugin {
|
function vitePlugin(registerSource: string, relativeTemplateDir: string, buildInfo: BuildInfo, componentRegistry: ComponentRegistry): Plugin {
|
||||||
buildInfo.sources = {};
|
buildInfo.sources = {};
|
||||||
|
let moduleResolver: ResolveFn;
|
||||||
return {
|
return {
|
||||||
name: 'playwright:component-index',
|
name: 'playwright:component-index',
|
||||||
|
|
||||||
transform: async (content, id) => {
|
configResolved(config: ResolvedConfig) {
|
||||||
|
moduleResolver = config.createResolver();
|
||||||
|
},
|
||||||
|
|
||||||
|
async transform(this: PluginContext, content, id) {
|
||||||
const queryIndex = id.indexOf('?');
|
const queryIndex = id.indexOf('?');
|
||||||
const file = queryIndex !== -1 ? id.substring(0, queryIndex) : id;
|
const file = queryIndex !== -1 ? id.substring(0, queryIndex) : id;
|
||||||
if (!buildInfo.sources[file]) {
|
if (!buildInfo.sources[file]) {
|
||||||
|
|
@ -318,9 +329,43 @@ function vitePlugin(registerSource: string, relativeTemplateDir: string, buildIn
|
||||||
map: { mappings: '' }
|
map: { mappings: '' }
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
async writeBundle(this: PluginContext) {
|
||||||
|
const componentDeps = new Map<string, Set<string>>();
|
||||||
|
for (const component of componentRegistry.values()) {
|
||||||
|
const id = (await moduleResolver(component.importPath));
|
||||||
|
if (!id)
|
||||||
|
continue;
|
||||||
|
const deps = new Set<string>();
|
||||||
|
collectViteModuleDependencies(this, id, deps);
|
||||||
|
componentDeps.set(component.fullName, deps);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const testInfo of Object.values(buildInfo.tests)) {
|
||||||
|
const deps = new Set<string>();
|
||||||
|
for (const fullName of testInfo.components) {
|
||||||
|
for (const dep of componentDeps.get(fullName) || [])
|
||||||
|
deps.add(dep);
|
||||||
|
}
|
||||||
|
testInfo.deps = [...deps];
|
||||||
|
}
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function collectViteModuleDependencies(context: PluginContext, id: string, deps: Set<string>) {
|
||||||
|
if (!path.isAbsolute(id))
|
||||||
|
return;
|
||||||
|
if (deps.has(id))
|
||||||
|
return;
|
||||||
|
deps.add(id);
|
||||||
|
const module = context.getModuleInfo(id);
|
||||||
|
for (const importedId of module?.importedIds || [])
|
||||||
|
collectViteModuleDependencies(context, importedId, deps);
|
||||||
|
for (const importedId of module?.dynamicallyImportedIds || [])
|
||||||
|
collectViteModuleDependencies(context, importedId, deps);
|
||||||
|
}
|
||||||
|
|
||||||
function hasJSComponents(components: ComponentInfo[]): boolean {
|
function hasJSComponents(components: ComponentInfo[]): boolean {
|
||||||
for (const component of components) {
|
for (const component of components) {
|
||||||
const extname = path.extname(component.importPath);
|
const extname = path.extname(component.importPath);
|
||||||
|
|
|
||||||
|
|
@ -177,7 +177,11 @@ test('should extract component list', async ({ runInlineTest }, testInfo) => {
|
||||||
components: [
|
components: [
|
||||||
expect.stringContaining('clashingNames1_tsx_ClashingName'),
|
expect.stringContaining('clashingNames1_tsx_ClashingName'),
|
||||||
expect.stringContaining('clashingNames2_tsx_ClashingName'),
|
expect.stringContaining('clashingNames2_tsx_ClashingName'),
|
||||||
]
|
],
|
||||||
|
deps: [
|
||||||
|
expect.stringContaining('clashingNames1.tsx'),
|
||||||
|
expect.stringContaining('clashingNames2.tsx'),
|
||||||
|
],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (file.endsWith('default-import.spec.tsx')) {
|
if (file.endsWith('default-import.spec.tsx')) {
|
||||||
|
|
@ -185,6 +189,9 @@ test('should extract component list', async ({ runInlineTest }, testInfo) => {
|
||||||
timestamp: expect.any(Number),
|
timestamp: expect.any(Number),
|
||||||
components: [
|
components: [
|
||||||
expect.stringContaining('defaultExport_tsx'),
|
expect.stringContaining('defaultExport_tsx'),
|
||||||
|
],
|
||||||
|
deps: [
|
||||||
|
expect.stringContaining('defaultExport.tsx'),
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -194,6 +201,9 @@ test('should extract component list', async ({ runInlineTest }, testInfo) => {
|
||||||
components: [
|
components: [
|
||||||
expect.stringContaining('components_tsx_Component1'),
|
expect.stringContaining('components_tsx_Component1'),
|
||||||
expect.stringContaining('components_tsx_Component2'),
|
expect.stringContaining('components_tsx_Component2'),
|
||||||
|
],
|
||||||
|
deps: [
|
||||||
|
expect.stringContaining('components.tsx'),
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -202,6 +212,9 @@ test('should extract component list', async ({ runInlineTest }, testInfo) => {
|
||||||
timestamp: expect.any(Number),
|
timestamp: expect.any(Number),
|
||||||
components: [
|
components: [
|
||||||
expect.stringContaining('button_tsx_Button'),
|
expect.stringContaining('button_tsx_Button'),
|
||||||
|
],
|
||||||
|
deps: [
|
||||||
|
expect.stringContaining('button.tsx'),
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -308,7 +308,7 @@ test('should run on changed files', async ({ runWatchTest, writeFiles }) => {
|
||||||
await testProcess.waitForOutput('Error: expect(received).toBe(expected)');
|
await testProcess.waitForOutput('Error: expect(received).toBe(expected)');
|
||||||
await testProcess.waitForOutput('Waiting for file changes.');
|
await testProcess.waitForOutput('Waiting for file changes.');
|
||||||
testProcess.clearOutput();
|
testProcess.clearOutput();
|
||||||
writeFiles({
|
await writeFiles({
|
||||||
'c.test.ts': `
|
'c.test.ts': `
|
||||||
pwt.test('passes', () => {});
|
pwt.test('passes', () => {});
|
||||||
`,
|
`,
|
||||||
|
|
@ -337,7 +337,7 @@ test('should run on changed deps', async ({ runWatchTest, writeFiles }) => {
|
||||||
await testProcess.waitForOutput('old helper');
|
await testProcess.waitForOutput('old helper');
|
||||||
await testProcess.waitForOutput('Waiting for file changes.');
|
await testProcess.waitForOutput('Waiting for file changes.');
|
||||||
testProcess.clearOutput();
|
testProcess.clearOutput();
|
||||||
writeFiles({
|
await writeFiles({
|
||||||
'helper.ts': `
|
'helper.ts': `
|
||||||
console.log('new helper');
|
console.log('new helper');
|
||||||
`,
|
`,
|
||||||
|
|
@ -366,7 +366,7 @@ test('should re-run changed files on R', async ({ runWatchTest, writeFiles }) =>
|
||||||
await testProcess.waitForOutput('Error: expect(received).toBe(expected)');
|
await testProcess.waitForOutput('Error: expect(received).toBe(expected)');
|
||||||
await testProcess.waitForOutput('Waiting for file changes.');
|
await testProcess.waitForOutput('Waiting for file changes.');
|
||||||
testProcess.clearOutput();
|
testProcess.clearOutput();
|
||||||
writeFiles({
|
await writeFiles({
|
||||||
'c.test.ts': `
|
'c.test.ts': `
|
||||||
pwt.test('passes', () => {});
|
pwt.test('passes', () => {});
|
||||||
`,
|
`,
|
||||||
|
|
@ -397,7 +397,7 @@ test('should not trigger on changes to non-tests', async ({ runWatchTest, writeF
|
||||||
await testProcess.waitForOutput('Waiting for file changes.');
|
await testProcess.waitForOutput('Waiting for file changes.');
|
||||||
|
|
||||||
testProcess.clearOutput();
|
testProcess.clearOutput();
|
||||||
writeFiles({
|
await writeFiles({
|
||||||
'helper.ts': `
|
'helper.ts': `
|
||||||
console.log('helper');
|
console.log('helper');
|
||||||
`,
|
`,
|
||||||
|
|
@ -423,7 +423,7 @@ test('should only watch selected projects', async ({ runWatchTest, writeFiles })
|
||||||
await testProcess.waitForOutput('Waiting for file changes.');
|
await testProcess.waitForOutput('Waiting for file changes.');
|
||||||
|
|
||||||
testProcess.clearOutput();
|
testProcess.clearOutput();
|
||||||
writeFiles({
|
await writeFiles({
|
||||||
'a.test.ts': `
|
'a.test.ts': `
|
||||||
pwt.test('passes', () => {});
|
pwt.test('passes', () => {});
|
||||||
`,
|
`,
|
||||||
|
|
@ -450,7 +450,7 @@ test('should watch filtered files', async ({ runWatchTest, writeFiles }) => {
|
||||||
await testProcess.waitForOutput('Waiting for file changes.');
|
await testProcess.waitForOutput('Waiting for file changes.');
|
||||||
|
|
||||||
testProcess.clearOutput();
|
testProcess.clearOutput();
|
||||||
writeFiles({
|
await writeFiles({
|
||||||
'b.test.ts': `
|
'b.test.ts': `
|
||||||
pwt.test('passes', () => {});
|
pwt.test('passes', () => {});
|
||||||
`,
|
`,
|
||||||
|
|
@ -475,7 +475,7 @@ test('should not watch unfiltered files', async ({ runWatchTest, writeFiles }) =
|
||||||
await testProcess.waitForOutput('Waiting for file changes.');
|
await testProcess.waitForOutput('Waiting for file changes.');
|
||||||
|
|
||||||
testProcess.clearOutput();
|
testProcess.clearOutput();
|
||||||
writeFiles({
|
await writeFiles({
|
||||||
'a.test.ts': `
|
'a.test.ts': `
|
||||||
pwt.test('passes', () => {});
|
pwt.test('passes', () => {});
|
||||||
`,
|
`,
|
||||||
|
|
@ -487,3 +487,96 @@ test('should not watch unfiltered files', async ({ runWatchTest, writeFiles }) =
|
||||||
expect(testProcess.output).not.toContain('b.test');
|
expect(testProcess.output).not.toContain('b.test');
|
||||||
await testProcess.waitForOutput('Waiting for file changes.');
|
await testProcess.waitForOutput('Waiting for file changes.');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('should run CT on changed deps', async ({ runWatchTest, writeFiles }) => {
|
||||||
|
const testProcess = await runWatchTest({
|
||||||
|
'playwright.config.ts': `
|
||||||
|
import { defineConfig } from '@playwright/experimental-ct-react';
|
||||||
|
export default defineConfig({ projects: [{name: 'default'}] });
|
||||||
|
`,
|
||||||
|
'playwright/index.html': `<script type="module" src="./index.ts"></script>`,
|
||||||
|
'playwright/index.ts': ``,
|
||||||
|
'src/button.tsx': `
|
||||||
|
export const Button = () => <button>Button</button>;
|
||||||
|
`,
|
||||||
|
'src/button.spec.tsx': `
|
||||||
|
//@no-header
|
||||||
|
import { test, expect } from '@playwright/experimental-ct-react';
|
||||||
|
import { Button } from './button';
|
||||||
|
test('pass', async ({ mount }) => {
|
||||||
|
const component = await mount(<Button></Button>);
|
||||||
|
await expect(component).toHaveText('Button', { timeout: 1000 });
|
||||||
|
});
|
||||||
|
`,
|
||||||
|
'src/link.spec.tsx': `
|
||||||
|
//@no-header
|
||||||
|
import { test, expect } from '@playwright/experimental-ct-react';
|
||||||
|
test('pass', async ({ mount }) => {
|
||||||
|
const component = await mount(<a>hello</a>);
|
||||||
|
await expect(component).toHaveText('hello');
|
||||||
|
});
|
||||||
|
`,
|
||||||
|
}, {});
|
||||||
|
await testProcess.waitForOutput('button.spec.tsx:5:11 › pass');
|
||||||
|
await testProcess.waitForOutput('link.spec.tsx:4:11 › pass');
|
||||||
|
await testProcess.waitForOutput('Waiting for file changes.');
|
||||||
|
testProcess.clearOutput();
|
||||||
|
await writeFiles({
|
||||||
|
'src/button.tsx': `
|
||||||
|
export const Button = () => <button>Button 2</button>;
|
||||||
|
`,
|
||||||
|
});
|
||||||
|
|
||||||
|
await testProcess.waitForOutput('src/button.spec.tsx:5:11 › pass');
|
||||||
|
expect(testProcess.output).not.toContain('src/link.spec.tsx');
|
||||||
|
await testProcess.waitForOutput('Error: expect(received).toHaveText(expected)');
|
||||||
|
await testProcess.waitForOutput('Waiting for file changes.');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should run CT on indirect deps change', async ({ runWatchTest, writeFiles }) => {
|
||||||
|
const testProcess = await runWatchTest({
|
||||||
|
'playwright.config.ts': `
|
||||||
|
import { defineConfig } from '@playwright/experimental-ct-react';
|
||||||
|
export default defineConfig({ projects: [{name: 'default'}] });
|
||||||
|
`,
|
||||||
|
'playwright/index.html': `<script type="module" src="./index.ts"></script>`,
|
||||||
|
'playwright/index.ts': ``,
|
||||||
|
'src/button.css': `
|
||||||
|
button { color: red; }
|
||||||
|
`,
|
||||||
|
'src/button.tsx': `
|
||||||
|
import './button.css';
|
||||||
|
export const Button = () => <button>Button</button>;
|
||||||
|
`,
|
||||||
|
'src/button.spec.tsx': `
|
||||||
|
//@no-header
|
||||||
|
import { test, expect } from '@playwright/experimental-ct-react';
|
||||||
|
import { Button } from './button';
|
||||||
|
test('pass', async ({ mount }) => {
|
||||||
|
const component = await mount(<Button></Button>);
|
||||||
|
await expect(component).toHaveText('Button', { timeout: 1000 });
|
||||||
|
});
|
||||||
|
`,
|
||||||
|
'src/link.spec.tsx': `
|
||||||
|
//@no-header
|
||||||
|
import { test, expect } from '@playwright/experimental-ct-react';
|
||||||
|
test('pass', async ({ mount }) => {
|
||||||
|
const component = await mount(<a>hello</a>);
|
||||||
|
await expect(component).toHaveText('hello');
|
||||||
|
});
|
||||||
|
`,
|
||||||
|
}, {});
|
||||||
|
await testProcess.waitForOutput('button.spec.tsx:5:11 › pass');
|
||||||
|
await testProcess.waitForOutput('link.spec.tsx:4:11 › pass');
|
||||||
|
await testProcess.waitForOutput('Waiting for file changes.');
|
||||||
|
testProcess.clearOutput();
|
||||||
|
await writeFiles({
|
||||||
|
'src/button.css': `
|
||||||
|
button { color: blue; }
|
||||||
|
`,
|
||||||
|
});
|
||||||
|
|
||||||
|
await testProcess.waitForOutput('src/button.spec.tsx:5:11 › pass');
|
||||||
|
expect(testProcess.output).not.toContain('src/link.spec.tsx');
|
||||||
|
await testProcess.waitForOutput('Waiting for file changes.');
|
||||||
|
});
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue