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 memoryCache = new Map<string, MemoryCache>();
|
||||
// Dependencies resolved by the loader.
|
||||
const fileDependencies = new Map<string, Set<string>>();
|
||||
// Dependencies resolved by the external bundler.
|
||||
const externalDependencies = new Map<string, Set<string>>();
|
||||
|
||||
Error.stackTraceLimit = 200;
|
||||
|
||||
|
|
@ -93,6 +96,7 @@ export function serializeCompilationCache(): any {
|
|||
sourceMaps: [...sourceMaps.entries()],
|
||||
memoryCache: [...memoryCache.entries()],
|
||||
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]);
|
||||
for (const entry of payload.fileDependencies)
|
||||
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 {
|
||||
|
|
@ -146,6 +152,11 @@ export function currentFileDepsCollector(): Set<string> | undefined {
|
|||
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() {
|
||||
return fileDependencies;
|
||||
}
|
||||
|
|
@ -156,6 +167,10 @@ export function collectAffectedTestFiles(dependency: string, testFileCollector:
|
|||
if (deps.has(dependency))
|
||||
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
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@
|
|||
import fs from 'fs';
|
||||
import type { Suite } from '../../types/testReporter';
|
||||
import path from 'path';
|
||||
import type { InlineConfig, Plugin } from 'vite';
|
||||
import type { InlineConfig, Plugin, ResolveFn, ResolvedConfig } from 'vite';
|
||||
import type { TestRunnerPlugin } from '.';
|
||||
import { parse, traverse, types as t } from '../common/babelBundle';
|
||||
import { stoppable } from '../utilsBundle';
|
||||
|
|
@ -28,6 +28,8 @@ import { assert, calculateSha1 } from 'playwright-core/lib/utils';
|
|||
import type { AddressInfo } from 'net';
|
||||
import { getPlaywrightVersion } from 'playwright-core/lib/utils';
|
||||
import type { PlaywrightTestConfig as BasePlaywrightTestConfig } from '@playwright/test';
|
||||
import type { PluginContext } from 'rollup';
|
||||
import { setExternalDependencies } from '../common/compilationCache';
|
||||
|
||||
let stoppableServer: any;
|
||||
const playwrightVersion = getPlaywrightVersion();
|
||||
|
|
@ -155,6 +157,9 @@ export function createPlugin(
|
|||
if (hasNewTests || hasNewComponents || sourcesDirty)
|
||||
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);
|
||||
stoppableServer = stoppable(previewServer.httpServer, 0);
|
||||
const isAddressInfo = (x: any): x is AddressInfo => x?.address;
|
||||
|
|
@ -185,6 +190,7 @@ type BuildInfo = {
|
|||
[key: string]: {
|
||||
timestamp: number;
|
||||
components: string[];
|
||||
deps: string[];
|
||||
}
|
||||
};
|
||||
};
|
||||
|
|
@ -218,7 +224,7 @@ async function checkNewTests(suite: Suite, buildInfo: BuildInfo, componentRegist
|
|||
const components = await parseTestFile(testFile);
|
||||
for (const component of components)
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
|
@ -272,10 +278,15 @@ async function parseTestFile(testFile: string): Promise<ComponentInfo[]> {
|
|||
|
||||
function vitePlugin(registerSource: string, relativeTemplateDir: string, buildInfo: BuildInfo, componentRegistry: ComponentRegistry): Plugin {
|
||||
buildInfo.sources = {};
|
||||
let moduleResolver: ResolveFn;
|
||||
return {
|
||||
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 file = queryIndex !== -1 ? id.substring(0, queryIndex) : id;
|
||||
if (!buildInfo.sources[file]) {
|
||||
|
|
@ -318,9 +329,43 @@ function vitePlugin(registerSource: string, relativeTemplateDir: string, buildIn
|
|||
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 {
|
||||
for (const component of components) {
|
||||
const extname = path.extname(component.importPath);
|
||||
|
|
|
|||
|
|
@ -177,7 +177,11 @@ test('should extract component list', async ({ runInlineTest }, testInfo) => {
|
|||
components: [
|
||||
expect.stringContaining('clashingNames1_tsx_ClashingName'),
|
||||
expect.stringContaining('clashingNames2_tsx_ClashingName'),
|
||||
]
|
||||
],
|
||||
deps: [
|
||||
expect.stringContaining('clashingNames1.tsx'),
|
||||
expect.stringContaining('clashingNames2.tsx'),
|
||||
],
|
||||
});
|
||||
}
|
||||
if (file.endsWith('default-import.spec.tsx')) {
|
||||
|
|
@ -185,6 +189,9 @@ test('should extract component list', async ({ runInlineTest }, testInfo) => {
|
|||
timestamp: expect.any(Number),
|
||||
components: [
|
||||
expect.stringContaining('defaultExport_tsx'),
|
||||
],
|
||||
deps: [
|
||||
expect.stringContaining('defaultExport.tsx'),
|
||||
]
|
||||
});
|
||||
}
|
||||
|
|
@ -194,6 +201,9 @@ test('should extract component list', async ({ runInlineTest }, testInfo) => {
|
|||
components: [
|
||||
expect.stringContaining('components_tsx_Component1'),
|
||||
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),
|
||||
components: [
|
||||
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('Waiting for file changes.');
|
||||
testProcess.clearOutput();
|
||||
writeFiles({
|
||||
await writeFiles({
|
||||
'c.test.ts': `
|
||||
pwt.test('passes', () => {});
|
||||
`,
|
||||
|
|
@ -337,7 +337,7 @@ test('should run on changed deps', async ({ runWatchTest, writeFiles }) => {
|
|||
await testProcess.waitForOutput('old helper');
|
||||
await testProcess.waitForOutput('Waiting for file changes.');
|
||||
testProcess.clearOutput();
|
||||
writeFiles({
|
||||
await writeFiles({
|
||||
'helper.ts': `
|
||||
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('Waiting for file changes.');
|
||||
testProcess.clearOutput();
|
||||
writeFiles({
|
||||
await writeFiles({
|
||||
'c.test.ts': `
|
||||
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.');
|
||||
|
||||
testProcess.clearOutput();
|
||||
writeFiles({
|
||||
await writeFiles({
|
||||
'helper.ts': `
|
||||
console.log('helper');
|
||||
`,
|
||||
|
|
@ -423,7 +423,7 @@ test('should only watch selected projects', async ({ runWatchTest, writeFiles })
|
|||
await testProcess.waitForOutput('Waiting for file changes.');
|
||||
|
||||
testProcess.clearOutput();
|
||||
writeFiles({
|
||||
await writeFiles({
|
||||
'a.test.ts': `
|
||||
pwt.test('passes', () => {});
|
||||
`,
|
||||
|
|
@ -450,7 +450,7 @@ test('should watch filtered files', async ({ runWatchTest, writeFiles }) => {
|
|||
await testProcess.waitForOutput('Waiting for file changes.');
|
||||
|
||||
testProcess.clearOutput();
|
||||
writeFiles({
|
||||
await writeFiles({
|
||||
'b.test.ts': `
|
||||
pwt.test('passes', () => {});
|
||||
`,
|
||||
|
|
@ -475,7 +475,7 @@ test('should not watch unfiltered files', async ({ runWatchTest, writeFiles }) =
|
|||
await testProcess.waitForOutput('Waiting for file changes.');
|
||||
|
||||
testProcess.clearOutput();
|
||||
writeFiles({
|
||||
await writeFiles({
|
||||
'a.test.ts': `
|
||||
pwt.test('passes', () => {});
|
||||
`,
|
||||
|
|
@ -487,3 +487,96 @@ test('should not watch unfiltered files', async ({ runWatchTest, writeFiles }) =
|
|||
expect(testProcess.output).not.toContain('b.test');
|
||||
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