chore: make watch + ct happy (#20804)

This commit is contained in:
Pavel Feldman 2023-02-10 08:33:25 -08:00 committed by GitHub
parent e03f0ea309
commit 1ba768bf60
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 177 additions and 11 deletions

View file

@ -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

View file

@ -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);

View file

@ -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'),
]
});
}

View file

@ -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.');
});