chore(ct): remove suite dependency by connecting dependency graphs at read time, not write time (#31794)

Broken out of https://github.com/microsoft/playwright/pull/31727 as per
@dgozman's
[request](https://github.com/microsoft/playwright/pull/31727#discussion_r1685793229).

The PR goal is to remove the `suite` argument from the Component
testing's Vite Plugin. `suite` is used to enrich Vite's dependency graph
with information about dependencies between test suites and helper
files. It essentially merges the Vite graph with the
`compilationCache.ts > fileDependencies` graph, and then writes the
result back into `compilationCache.ts > externalDependencies`.

By refactoring this to make the connection on the reading end in
`collectAffectedTestFiles`, we can drop the `suite` parameter.

We didn't yet have a test that depended on the dependency graph being
connected correctly between `fileDependencies` and
`externalDepedencies`, so I've [extended an existing
test](53a539938b)
to capture that.
This commit is contained in:
Simon Knott 2024-07-23 10:19:58 +02:00 committed by GitHub
parent cf1a313a0c
commit 1408a45595
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 41 additions and 34 deletions

View file

@ -18,7 +18,6 @@
import { affectedTestFiles, cacheDir } from 'playwright/lib/transform/compilationCache';
import { buildBundle } from './vitePlugin';
import { resolveDirs } from './viteUtils';
import type { Suite } from 'playwright/types/testReporter';
import { runDevServer } from './devServer';
import type { FullConfigInternal } from 'playwright/lib/common/config';
import { removeFolderAndLogToConsole } from 'playwright/lib/runner/testServer';
@ -30,8 +29,8 @@ export async function clearCacheCommand(config: FullConfigInternal) {
await removeFolderAndLogToConsole(cacheDir);
}
export async function findRelatedTestFilesCommand(files: string[], config: FullConfigInternal, suite: Suite) {
await buildBundle(config.config, config.configDir, suite);
export async function findRelatedTestFilesCommand(files: string[], config: FullConfigInternal) {
await buildBundle(config.config, config.configDir);
return { testFiles: affectedTestFiles(files) };
}

View file

@ -20,7 +20,7 @@ import type { AddressInfo } from 'net';
import path from 'path';
import { assert, calculateSha1, getPlaywrightVersion, isURLAvailable } from 'playwright-core/lib/utils';
import { debug } from 'playwright-core/lib/utilsBundle';
import { internalDependenciesForTestFile, setExternalDependencies } from 'playwright/lib/transform/compilationCache';
import { setExternalDependencies } from 'playwright/lib/transform/compilationCache';
import { stoppable } from 'playwright/lib/utilsBundle';
import type { FullConfig, Suite } from 'playwright/types/testReporter';
import type { PluginContext } from 'rollup';
@ -49,7 +49,7 @@ export function createPlugin(): TestRunnerPlugin {
},
begin: async (suite: Suite) => {
const result = await buildBundle(config, configDir, suite);
const result = await buildBundle(config, configDir);
if (!result)
return;
@ -87,7 +87,7 @@ type BuildInfo = {
}
};
export async function buildBundle(config: FullConfig, configDir: string, suite: Suite): Promise<{ buildInfo: BuildInfo, viteConfig: Record<string, any> } | null> {
export async function buildBundle(config: FullConfig, configDir: string): Promise<{ buildInfo: BuildInfo, viteConfig: Record<string, any> } | null> {
const { registerSourceFile, frameworkPluginFactory } = frameworkConfig(config);
{
// Detect a running dev server and use it if available.
@ -169,23 +169,13 @@ export async function buildBundle(config: FullConfig, configDir: string, suite:
{
// Update dependencies based on the vite build.
for (const projectSuite of suite.suites) {
for (const fileSuite of projectSuite.suites) {
// For every test file...
const testFile = fileSuite.location!.file;
const deps = new Set<string>();
// Collect its JS dependencies (helpers).
for (const file of [testFile, ...(internalDependenciesForTestFile(testFile) || [])]) {
// For each helper, get all the imported components.
for (const componentFile of componentsByImportingFile.get(file) || []) {
// For each component, get all the dependencies.
for (const d of buildInfo.deps[componentFile] || [])
deps.add(d);
}
}
// Now we have test file => all components along with dependencies.
setExternalDependencies(testFile, [...deps]);
for (const [importingFile, components] of componentsByImportingFile) {
const deps = new Set<string>();
for (const component of components) {
for (const d of buildInfo.deps[component])
deps.add(d);
}
setExternalDependencies(importingFile, [...deps]);
}
}

View file

@ -144,7 +144,7 @@ export class Runner {
const resolvedFiles = (files as string[]).map(file => path.resolve(process.cwd(), file));
const override = (this._config.config as any)['@playwright/test']?.['cli']?.['find-related-test-files'];
if (override)
return await override(resolvedFiles, this._config, result.suite);
return await override(resolvedFiles, this._config);
return { testFiles: affectedTestFiles(resolvedFiles) };
}
}

View file

@ -211,16 +211,27 @@ export function fileDependenciesForTest() {
return fileDependencies;
}
export function collectAffectedTestFiles(dependency: string, testFileCollector: Set<string>) {
if (fileDependencies.has(dependency))
testFileCollector.add(dependency);
export function collectAffectedTestFiles(changedFile: string, testFileCollector: Set<string>) {
const isTestFile = (file: string) => fileDependencies.has(file);
if (isTestFile(changedFile))
testFileCollector.add(changedFile);
for (const [testFile, deps] of fileDependencies) {
if (deps.has(dependency))
if (deps.has(changedFile))
testFileCollector.add(testFile);
}
for (const [testFile, deps] of externalDependencies) {
if (deps.has(dependency))
testFileCollector.add(testFile);
for (const [importingFile, depsOfImportingFile] of externalDependencies) {
if (depsOfImportingFile.has(changedFile)) {
if (isTestFile(importingFile))
testFileCollector.add(importingFile);
for (const [testFile, depsOfTestFile] of fileDependencies) {
if (depsOfTestFile.has(importingFile))
testFileCollector.add(testFile);
}
}
}
}
@ -237,8 +248,11 @@ export function internalDependenciesForTestFile(filename: string): Set<string> |
export function dependenciesForTestFile(filename: string): Set<string> {
const result = new Set<string>();
for (const dep of fileDependencies.get(filename) || [])
result.add(dep);
for (const testDependency of fileDependencies.get(filename) || []) {
result.add(testDependency);
for (const externalDependency of externalDependencies.get(testDependency) || [])
result.add(externalDependency);
}
for (const dep of externalDependencies.get(filename) || [])
result.add(dep);
return result;

View file

@ -690,11 +690,15 @@ test('should run CT on indirect deps change', async ({ runWatchTest, writeFiles
import './button.css';
export const Button = () => <button>Button</button>;
`,
'src/helper.tsx': `
import { Button } from "./button";
export const buttonInstance = <Button></Button>
`,
'src/button.spec.tsx': `
import { test, expect } from '@playwright/experimental-ct-react';
import { Button } from './button';
import { buttonInstance } from './helper';
test('pass', async ({ mount }) => {
const component = await mount(<Button></Button>);
const component = await mount(buttonInstance);
await expect(component).toHaveText('Button', { timeout: 1000 });
});
`,