mirror of
https://github.com/microsoft/playwright.git
synced 2024-10-26 21:33:38 +03:00
chore: do not add plugins to config twice (#26670)
This commit is contained in:
parent
39a6b23309
commit
e7bd1864a8
@ -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 };
|
||||
}
|
||||
|
@ -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];
|
||||
}
|
||||
},
|
||||
};
|
||||
|
@ -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)));
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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');
|
||||
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
}
|
||||
]
|
||||
]);
|
||||
});
|
||||
|
209
tests/playwright-test/ui-mode-test-ct.spec.ts
Normal file
209
tests/playwright-test/ui-mode-test-ct.spec.ts
Normal 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 <=
|
||||
`);
|
||||
});
|
Loading…
Reference in New Issue
Block a user