chore: do not add plugins to config twice (#26670)

This commit is contained in:
Pavel Feldman 2023-08-24 16:19:57 -07:00 committed by GitHub
parent 39a6b23309
commit e7bd1864a8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 415 additions and 90 deletions

View File

@ -166,10 +166,11 @@ export function collectComponentUsages(node: T.Node) {
}
export type ComponentInfo = {
fullName: string,
importPath: string,
isModuleOrAlias: boolean,
importedName?: string
fullName: string;
importPath: string;
isModuleOrAlias: boolean;
importedName?: string;
deps: string[];
};
export function componentInfo(specifier: T.ImportSpecifier | T.ImportDefaultSpecifier, importSource: string, filename: string): ComponentInfo {
@ -183,9 +184,9 @@ export function componentInfo(specifier: T.ImportSpecifier | T.ImportDefaultSpec
const pathInfo = { importPath, isModuleOrAlias };
if (t.isImportDefaultSpecifier(specifier))
return { fullName: prefix, ...pathInfo };
return { fullName: prefix, deps: [], ...pathInfo };
if (t.isIdentifier(specifier.imported))
return { fullName: prefix + '_' + specifier.imported.name, importedName: specifier.imported.name, ...pathInfo };
return { fullName: prefix + '_' + specifier.imported.value, importedName: specifier.imported.value, ...pathInfo };
return { fullName: prefix + '_' + specifier.imported.name, importedName: specifier.imported.name, deps: [], ...pathInfo };
return { fullName: prefix + '_' + specifier.imported.value, importedName: specifier.imported.value, deps: [], ...pathInfo };
}

View File

@ -17,11 +17,12 @@
import type { Suite } from '@playwright/test/reporter';
import type { PlaywrightTestConfig as BasePlaywrightTestConfig, FullConfig } from '@playwright/test';
import type { InlineConfig, Plugin, ResolveFn, ResolvedConfig } from 'vite';
import type { InlineConfig, Plugin, ResolveFn, ResolvedConfig, UserConfig } from 'vite';
import type { TestRunnerPlugin } from '../../playwright-test/src/plugins';
import type { ComponentInfo } from './tsxTransform';
import type { AddressInfo } from 'net';
import type { PluginContext } from 'rollup';
import { debug } from 'playwright-core/lib/utilsBundle';
import fs from 'fs';
import path from 'path';
@ -31,6 +32,9 @@ import { assert, calculateSha1 } from 'playwright-core/lib/utils';
import { getPlaywrightVersion } from 'playwright-core/lib/utils';
import { setExternalDependencies } from '@playwright/test/lib/transform/compilationCache';
import { collectComponentUsages, componentInfo } from './tsxTransform';
import { version as viteVersion, build, preview, mergeConfig } from 'vite';
const log = debug('pw:vite');
let stoppableServer: any;
const playwrightVersion = getPlaywrightVersion();
@ -59,23 +63,51 @@ export function createPlugin(
},
begin: async (suite: Suite) => {
// We are going to have 3 config files:
// - the defaults that user config overrides (baseConfig)
// - the user config (userConfig)
// - frameworks overrides (frameworkOverrides);
const use = config.projects[0].use as CtConfig;
const port = use.ctPort || 3100;
const viteConfig = typeof use.ctViteConfig === 'function' ? await use.ctViteConfig() : (use.ctViteConfig || {});
const templateDirConfig = use.ctTemplateDir || 'playwright';
const relativeTemplateDir = use.ctTemplateDir || 'playwright';
const rootDir = viteConfig.root || configDir;
const templateDir = path.resolve(rootDir, templateDirConfig);
const outDir = viteConfig?.build?.outDir || (use.ctCacheDir ? path.resolve(rootDir, use.ctCacheDir) : path.resolve(templateDir, '.cache'));
// FIXME: use build plugin to determine html location to resolve this.
// TemplateDir must be relative, otherwise we can't move the final index.html into its target location post-build.
// This regressed in https://github.com/microsoft/playwright/pull/26526
const templateDir = path.join(configDir, relativeTemplateDir);
// Compose base config from the playwright config only.
const baseConfig = {
root: configDir,
configFile: false,
define: {
__VUE_PROD_DEVTOOLS__: true,
},
css: {
devSourcemap: true,
},
build: {
outDir: use.ctCacheDir ? path.resolve(configDir, use.ctCacheDir) : path.resolve(templateDir, '.cache')
},
preview: {
port
},
// Vite preview server will otherwise always return the index.html with 200.
appType: 'custom',
};
// Apply user config on top of the base config. This could have changed root and build.outDir.
const userConfig = typeof use.ctViteConfig === 'function' ? await use.ctViteConfig() : (use.ctViteConfig || {});
const baseAndUserConfig = mergeConfig(baseConfig, userConfig);
const buildInfoFile = path.join(baseAndUserConfig.build.outDir, 'metainfo.json');
const buildInfoFile = path.join(outDir, 'metainfo.json');
let buildExists = false;
let buildInfo: BuildInfo;
const registerSource = await fs.promises.readFile(registerSourceFile, 'utf-8');
const registerSourceHash = calculateSha1(registerSource);
const { version: viteVersion } = await import('vite');
try {
buildInfo = JSON.parse(await fs.promises.readFile(buildInfoFile, 'utf-8')) as BuildInfo;
assert(buildInfo.version === playwrightVersion);
@ -92,54 +124,52 @@ export function createPlugin(
sources: {},
};
}
log('build exists:', buildExists);
const componentRegistry: ComponentRegistry = new Map();
// 1. Re-parse changed tests and collect required components.
const hasNewTests = await checkNewTests(suite, buildInfo, componentRegistry);
log('has new tests:', hasNewTests);
// 2. Check if the set of required components has changed.
const hasNewComponents = await checkNewComponents(buildInfo, componentRegistry);
log('has new components:', hasNewComponents);
// 3. Check component sources.
const sourcesDirty = !buildExists || hasNewComponents || await checkSources(buildInfo);
log('sourcesDirty:', sourcesDirty);
// 4. Update component info.
buildInfo.components = [...componentRegistry.values()];
viteConfig.root = rootDir;
viteConfig.preview = { port, ...viteConfig.preview };
// Vite preview server will otherwise always return the index.html with 200.
viteConfig.appType = viteConfig.appType || 'custom';
const frameworkOverrides: UserConfig = { plugins: [] };
// React heuristic. If we see a component in a file with .js extension,
// consider it a potential JSX-in-JS scenario and enable JSX loader for all
// .js files.
if (hasJSComponents(buildInfo.components)) {
viteConfig.esbuild = {
log('jsx-in-js detected');
frameworkOverrides.esbuild = {
loader: 'jsx',
include: /.*\.jsx?$/,
exclude: [],
};
viteConfig.optimizeDeps = {
frameworkOverrides.optimizeDeps = {
esbuildOptions: {
loader: { '.js': 'jsx' },
}
};
}
const { build, preview } = await import('vite');
// Build config unconditionally, either build or build & preview will use it.
viteConfig.plugins ??= [];
if (frameworkPluginFactory && !viteConfig.plugins.length)
viteConfig.plugins = [await frameworkPluginFactory()];
// We assume that any non-empty plugin list includes `vite-react` or similar.
if (frameworkPluginFactory && !baseAndUserConfig.plugins?.length)
frameworkOverrides.plugins = [await frameworkPluginFactory()];
// But only add out own plugin when we actually build / transform.
if (sourcesDirty)
viteConfig.plugins.push(vitePlugin(registerSource, templateDir, buildInfo, componentRegistry));
viteConfig.configFile = viteConfig.configFile || false;
viteConfig.define = viteConfig.define || {};
viteConfig.define.__VUE_PROD_DEVTOOLS__ = true;
viteConfig.css = viteConfig.css || {};
viteConfig.css.devSourcemap = true;
viteConfig.build = {
...viteConfig.build,
outDir,
frameworkOverrides.plugins!.push(vitePlugin(registerSource, templateDir, buildInfo, componentRegistry));
frameworkOverrides.build = {
target: 'esnext',
minify: false,
rollupOptions: {
@ -151,24 +181,34 @@ export function createPlugin(
sourcemap: true,
};
const finalConfig = mergeConfig(baseAndUserConfig, frameworkOverrides);
if (sourcesDirty) {
await build(viteConfig);
const relativeTemplateDir = path.relative(rootDir, templateDir);
await fs.promises.rename(path.resolve(outDir, relativeTemplateDir, 'index.html'), `${outDir}/index.html`);
log('build');
await build(finalConfig);
await fs.promises.rename(`${finalConfig.build.outDir}/${relativeTemplateDir}/index.html`, `${finalConfig.build.outDir}/index.html`);
}
if (hasNewTests || hasNewComponents || sourcesDirty)
if (hasNewTests || hasNewComponents || sourcesDirty) {
log('write manifest');
await fs.promises.writeFile(buildInfoFile, JSON.stringify(buildInfo, undefined, 2));
}
for (const [filename, testInfo] of Object.entries(buildInfo.tests))
setExternalDependencies(filename, testInfo.deps);
for (const [filename, testInfo] of Object.entries(buildInfo.tests)) {
const deps = new Set<string>();
for (const componentName of testInfo.components) {
const component = componentRegistry.get(componentName);
component?.deps.forEach(d => deps.add(d));
}
setExternalDependencies(filename, [...deps]);
}
const previewServer = await preview(viteConfig);
const previewServer = await preview(finalConfig);
stoppableServer = stoppable(previewServer.httpServer, 0);
const isAddressInfo = (x: any): x is AddressInfo => x?.address;
const address = previewServer.httpServer.address();
if (isAddressInfo(address)) {
const protocol = viteConfig.preview.https ? 'https:' : 'http:';
const protocol = finalConfig.preview.https ? 'https:' : 'http:';
process.env.PLAYWRIGHT_TEST_BASE_URL = `${protocol}//localhost:${address.port}`;
}
},
@ -194,7 +234,6 @@ type BuildInfo = {
[key: string]: {
timestamp: number;
components: string[];
deps: string[];
}
};
};
@ -205,9 +244,12 @@ async function checkSources(buildInfo: BuildInfo): Promise<boolean> {
for (const [source, sourceInfo] of Object.entries(buildInfo.sources)) {
try {
const timestamp = (await fs.promises.stat(source)).mtimeMs;
if (sourceInfo.timestamp !== timestamp)
if (sourceInfo.timestamp !== timestamp) {
log('source has changed:', source);
return true;
}
} catch (e) {
log('check source failed:', e);
return true;
}
}
@ -226,9 +268,10 @@ async function checkNewTests(suite: Suite, buildInfo: BuildInfo, componentRegist
const timestamp = (await fs.promises.stat(testFile)).mtimeMs;
if (buildInfo.tests[testFile]?.timestamp !== timestamp) {
const components = await parseTestFile(testFile);
log('changed test:', testFile);
for (const component of components)
componentRegistry.set(component.fullName, component);
buildInfo.tests[testFile] = { timestamp, components: components.map(c => c.fullName), deps: [] };
buildInfo.tests[testFile] = { timestamp, components: components.map(c => c.fullName) };
hasNewTests = true;
}
}
@ -336,23 +379,13 @@ function vitePlugin(registerSource: string, templateDir: string, buildInfo: Buil
},
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];
component.deps = [...deps];
}
},
};

View File

@ -53,7 +53,7 @@ function isComponent(component) {
*/
async function __pwResolveComponent(component) {
if (!isComponent(component))
return
return;
let componentFactory = __pwLoaderRegistry.get(component.type);
if (!componentFactory) {
@ -69,11 +69,11 @@ async function __pwResolveComponent(component) {
if (!componentFactory && component.type[0].toUpperCase() === component.type[0])
throw new Error(`Unregistered component: ${component.type}. Following components are registered: ${[...__pwRegistry.keys()]}`);
if(componentFactory)
__pwRegistry.set(component.type, await componentFactory())
if (componentFactory)
__pwRegistry.set(component.type, await componentFactory());
if ('children' in component)
await Promise.all(component.children.map(child => __pwResolveComponent(child)))
await Promise.all(component.children.map(child => __pwResolveComponent(child)));
}
/**

View File

@ -52,7 +52,7 @@ function isComponent(component) {
*/
async function __pwResolveComponent(component) {
if (!isComponent(component))
return
return;
let componentFactory = __pwLoaderRegistry.get(component.type);
if (!componentFactory) {
@ -68,11 +68,11 @@ async function __pwResolveComponent(component) {
if (!componentFactory && component.type[0].toUpperCase() === component.type[0])
throw new Error(`Unregistered component: ${component.type}. Following components are registered: ${[...__pwRegistry.keys()]}`);
if(componentFactory)
__pwRegistry.set(component.type, await componentFactory())
if (componentFactory)
__pwRegistry.set(component.type, await componentFactory());
if ('children' in component)
await Promise.all(component.children.map(child => __pwResolveComponent(child)))
await Promise.all(component.children.map(child => __pwResolveComponent(child)));
}
/**
@ -83,7 +83,7 @@ function __pwRender(component) {
return component;
const componentFunc = __pwRegistry.get(component.type);
if (component.kind !== 'jsx')
throw new Error('Object mount notation is not supported');

View File

@ -177,7 +177,12 @@ export function collectAffectedTestFiles(dependency: string, testFileCollector:
}
export function dependenciesForTestFile(filename: string): Set<string> {
return fileDependencies.get(filename) || new Set();
const result = new Set<string>();
for (const dep of fileDependencies.get(filename) || [])
result.add(dep);
for (const dep of externalDependencies.get(filename) || [])
result.add(dep);
return result;
}
// These two are only used in the dev mode, they are specifically excluding

View File

@ -138,31 +138,55 @@ test('should extract component list', async ({ runInlineTest }, testInfo) => {
fullName: expect.stringContaining('playwright_test_src_button_tsx_Button'),
importedName: 'Button',
importPath: expect.stringContaining('button.tsx'),
isModuleOrAlias: false
isModuleOrAlias: false,
deps: [
expect.stringContaining('button.tsx'),
expect.stringContaining('jsx-runtime.js'),
]
}, {
fullName: expect.stringContaining('playwright_test_src_clashingNames1_tsx_ClashingName'),
importedName: 'ClashingName',
importPath: expect.stringContaining('clashingNames1.tsx'),
isModuleOrAlias: false
isModuleOrAlias: false,
deps: [
expect.stringContaining('clashingNames1.tsx'),
expect.stringContaining('jsx-runtime.js'),
]
}, {
fullName: expect.stringContaining('playwright_test_src_clashingNames2_tsx_ClashingName'),
importedName: 'ClashingName',
importPath: expect.stringContaining('clashingNames2.tsx'),
isModuleOrAlias: false
isModuleOrAlias: false,
deps: [
expect.stringContaining('clashingNames2.tsx'),
expect.stringContaining('jsx-runtime.js'),
]
}, {
fullName: expect.stringContaining('playwright_test_src_components_tsx_Component1'),
importedName: 'Component1',
importPath: expect.stringContaining('components.tsx'),
isModuleOrAlias: false
isModuleOrAlias: false,
deps: [
expect.stringContaining('components.tsx'),
expect.stringContaining('jsx-runtime.js'),
]
}, {
fullName: expect.stringContaining('playwright_test_src_components_tsx_Component2'),
importedName: 'Component2',
importPath: expect.stringContaining('components.tsx'),
isModuleOrAlias: false
isModuleOrAlias: false,
deps: [
expect.stringContaining('components.tsx'),
expect.stringContaining('jsx-runtime.js'),
]
}, {
fullName: expect.stringContaining('playwright_test_src_defaultExport_tsx'),
importPath: expect.stringContaining('defaultExport.tsx'),
isModuleOrAlias: false
isModuleOrAlias: false,
deps: [
expect.stringContaining('defaultExport.tsx'),
expect.stringContaining('jsx-runtime.js'),
]
}]);
for (const [file, test] of Object.entries(metainfo.tests)) {
@ -173,11 +197,6 @@ test('should extract component list', async ({ runInlineTest }, testInfo) => {
expect.stringContaining('clashingNames1_tsx_ClashingName'),
expect.stringContaining('clashingNames2_tsx_ClashingName'),
],
deps: [
expect.stringContaining('clashingNames1.tsx'),
expect.stringContaining('jsx-runtime.js'),
expect.stringContaining('clashingNames2.tsx'),
],
});
}
if (file.endsWith('default-import.spec.tsx')) {
@ -186,10 +205,6 @@ test('should extract component list', async ({ runInlineTest }, testInfo) => {
components: [
expect.stringContaining('defaultExport_tsx'),
],
deps: [
expect.stringContaining('defaultExport.tsx'),
expect.stringContaining('jsx-runtime.js'),
]
});
}
if (file.endsWith('named-imports.spec.tsx')) {
@ -199,10 +214,6 @@ test('should extract component list', async ({ runInlineTest }, testInfo) => {
expect.stringContaining('components_tsx_Component1'),
expect.stringContaining('components_tsx_Component2'),
],
deps: [
expect.stringContaining('components.tsx'),
expect.stringContaining('jsx-runtime.js'),
]
});
}
if (file.endsWith('one-import.spec.tsx')) {
@ -211,10 +222,6 @@ test('should extract component list', async ({ runInlineTest }, testInfo) => {
components: [
expect.stringContaining('button_tsx_Button'),
],
deps: [
expect.stringContaining('button.tsx'),
expect.stringContaining('jsx-runtime.js'),
]
});
}
}
@ -436,3 +443,73 @@ test('list compilation cache should not clash with the run one', async ({ runInl
expect(runResult.exitCode).toBe(0);
expect(runResult.passed).toBe(1);
});
test('should retain deps when test changes', async ({ runInlineTest }, testInfo) => {
test.slow();
await test.step('original test', async () => {
const result = await runInlineTest({
'playwright.config.ts': playwrightConfig,
'playwright/index.html': `<script type="module" src="./index.ts"></script>`,
'playwright/index.ts': ``,
'src/button.tsx': `
export const Button = () => <button>Button</button>;
`,
'src/button.test.tsx': `
import { test, expect } from '@playwright/experimental-ct-react';
import { Button } from './button.tsx';
test('pass', async ({ mount }) => {
const component = await mount(<Button></Button>);
await expect(component).toHaveText('Button');
});
`,
}, { workers: 1 });
expect(result.exitCode).toBe(0);
expect(result.passed).toBe(1);
const output = result.output;
expect(output).toContain('modules transformed');
});
await test.step('modify test and run it again', async () => {
const result = await runInlineTest({
'src/button.test.tsx': `
import { test, expect } from '@playwright/experimental-ct-react';
import { Button } from './button.tsx';
test('pass', async ({ mount }) => {
const component1 = await mount(<Button></Button>);
await expect(component1).toHaveText('Button');
});
`,
}, { workers: 1 });
expect(result.exitCode).toBe(0);
expect(result.passed).toBe(1);
const output = result.output;
expect(output).not.toContain('modules transformed');
});
const metainfo = JSON.parse(fs.readFileSync(testInfo.outputPath('playwright/.cache/metainfo.json'), 'utf-8'));
expect(metainfo.components).toEqual([{
fullName: expect.stringContaining('playwright_test_src_button_tsx_Button'),
importedName: 'Button',
importPath: expect.stringContaining('button.tsx'),
isModuleOrAlias: false,
deps: [
expect.stringContaining('button.tsx'),
expect.stringContaining('jsx-runtime.js'),
]
}]);
expect(Object.entries(metainfo.tests)).toEqual([
[
expect.stringContaining('button.test.tsx'),
{
components: [
expect.stringContaining('src_button_tsx_Button'),
],
timestamp: expect.any(Number)
}
]
]);
});

View File

@ -0,0 +1,209 @@
/**
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { test, expect, retries, dumpTestTree } from './ui-mode-fixtures';
test.describe.configure({ mode: 'parallel', retries });
const basicTestTree = {
'playwright.config.ts': `
import { defineConfig } from '@playwright/experimental-ct-react';
export default defineConfig({});
`,
'playwright/index.html': `<script type="module" src="./index.ts"></script>`,
'playwright/index.ts': ``,
'src/button.tsx': `
export const Button = () => <button>Button</button>;
`,
'src/button.test.tsx': `
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: 1 });
});
`,
};
test('should run component tests', async ({ runUITest }) => {
const { page } = await runUITest(basicTestTree);
await expect.poll(dumpTestTree(page)).toBe(`
button.test.tsx
pass
`);
await page.getByTitle('Run all').click();
await expect.poll(dumpTestTree(page)).toBe(`
button.test.tsx
pass
`);
});
test('should run component tests after editing test', async ({ runUITest, writeFiles }) => {
const { page } = await runUITest(basicTestTree);
await expect.poll(dumpTestTree(page)).toBe(`
button.test.tsx
pass
`);
await page.getByTitle('Run all').click();
await expect.poll(dumpTestTree(page)).toBe(`
button.test.tsx
pass
`);
await writeFiles({
'src/button.test.tsx': `
import { test, expect } from '@playwright/experimental-ct-react';
import { Button } from './button';
test('fail', async ({ mount }) => {
const component = await mount(<Button></Button>);
await expect(component).toHaveText('Button2', { timeout: 1 });
});
`
});
await expect.poll(dumpTestTree(page)).toBe(`
button.test.tsx
fail
`);
await page.getByTitle('Run all').click();
await expect.poll(dumpTestTree(page)).toBe(`
button.test.tsx
fail <=
`);
});
test('should run component tests after editing component', async ({ runUITest, writeFiles }) => {
const { page } = await runUITest(basicTestTree);
await expect.poll(dumpTestTree(page)).toBe(`
button.test.tsx
pass
`);
await page.getByTitle('Run all').click();
await expect.poll(dumpTestTree(page)).toBe(`
button.test.tsx
pass
`);
await writeFiles({
'src/button.tsx': `
export const Button = () => <button>Button2</button>;
`
});
await page.getByTitle('Run all').click();
await expect.poll(dumpTestTree(page)).toBe(`
button.test.tsx
pass <=
`);
});
test('should run component tests after editing test and component', async ({ runUITest, writeFiles }) => {
const { page } = await runUITest(basicTestTree);
await expect.poll(dumpTestTree(page)).toBe(`
button.test.tsx
pass
`);
await page.getByTitle('Run all').click();
await expect.poll(dumpTestTree(page)).toBe(`
button.test.tsx
pass
`);
await writeFiles({
'src/button.test.tsx': `
import { test, expect } from '@playwright/experimental-ct-react';
import { Button } from './button';
test('pass 2', async ({ mount }) => {
const component = await mount(<Button></Button>);
await expect(component).toHaveText('Button2', { timeout: 1 });
});
`,
'src/button.tsx': `
export const Button = () => <button>Button2</button>;
`
});
await expect.poll(dumpTestTree(page)).toBe(`
button.test.tsx
pass 2
`);
await page.getByTitle('Run all').click();
await expect.poll(dumpTestTree(page)).toBe(`
button.test.tsx
pass 2
`);
});
test('should watch test', async ({ runUITest, writeFiles }) => {
const { page } = await runUITest(basicTestTree);
await expect.poll(dumpTestTree(page)).toBe(`
button.test.tsx
pass
`);
await page.getByTitle('Watch all').click();
await page.getByTitle('Run all').click();
await expect.poll(dumpTestTree(page)).toBe(`
button.test.tsx
pass
`);
await writeFiles({
'src/button.test.tsx': `
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('Button2', { timeout: 1 });
});
`
});
await expect.poll(dumpTestTree(page)).toBe(`
button.test.tsx
pass <=
`);
});
test('should watch component', async ({ runUITest, writeFiles }) => {
const { page } = await runUITest(basicTestTree);
await expect.poll(dumpTestTree(page)).toBe(`
button.test.tsx
pass
`);
await page.getByTitle('Watch all').click();
await page.getByTitle('Run all').click();
await expect.poll(dumpTestTree(page)).toBe(`
button.test.tsx
pass
`);
await writeFiles({
'src/button.tsx': `
export const Button = () => <button>Button2</button>;
`
});
await expect.poll(dumpTestTree(page)).toBe(`
button.test.tsx
pass <=
`);
});