mirror of
https://github.com/microsoft/playwright.git
synced 2025-06-26 21:40:17 +00: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 = {
|
export type ComponentInfo = {
|
||||||
fullName: string,
|
fullName: string;
|
||||||
importPath: string,
|
importPath: string;
|
||||||
isModuleOrAlias: boolean,
|
isModuleOrAlias: boolean;
|
||||||
importedName?: string
|
importedName?: string;
|
||||||
|
deps: string[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export function componentInfo(specifier: T.ImportSpecifier | T.ImportDefaultSpecifier, importSource: string, filename: string): ComponentInfo {
|
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 };
|
const pathInfo = { importPath, isModuleOrAlias };
|
||||||
|
|
||||||
if (t.isImportDefaultSpecifier(specifier))
|
if (t.isImportDefaultSpecifier(specifier))
|
||||||
return { fullName: prefix, ...pathInfo };
|
return { fullName: prefix, deps: [], ...pathInfo };
|
||||||
|
|
||||||
if (t.isIdentifier(specifier.imported))
|
if (t.isIdentifier(specifier.imported))
|
||||||
return { fullName: prefix + '_' + specifier.imported.name, importedName: specifier.imported.name, ...pathInfo };
|
return { fullName: prefix + '_' + specifier.imported.name, importedName: specifier.imported.name, deps: [], ...pathInfo };
|
||||||
return { fullName: prefix + '_' + specifier.imported.value, importedName: specifier.imported.value, ...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 { Suite } from '@playwright/test/reporter';
|
||||||
import type { PlaywrightTestConfig as BasePlaywrightTestConfig, FullConfig } from '@playwright/test';
|
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 { TestRunnerPlugin } from '../../playwright-test/src/plugins';
|
||||||
import type { ComponentInfo } from './tsxTransform';
|
import type { ComponentInfo } from './tsxTransform';
|
||||||
import type { AddressInfo } from 'net';
|
import type { AddressInfo } from 'net';
|
||||||
import type { PluginContext } from 'rollup';
|
import type { PluginContext } from 'rollup';
|
||||||
|
import { debug } from 'playwright-core/lib/utilsBundle';
|
||||||
|
|
||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
@ -31,6 +32,9 @@ import { assert, calculateSha1 } from 'playwright-core/lib/utils';
|
|||||||
import { getPlaywrightVersion } from 'playwright-core/lib/utils';
|
import { getPlaywrightVersion } from 'playwright-core/lib/utils';
|
||||||
import { setExternalDependencies } from '@playwright/test/lib/transform/compilationCache';
|
import { setExternalDependencies } from '@playwright/test/lib/transform/compilationCache';
|
||||||
import { collectComponentUsages, componentInfo } from './tsxTransform';
|
import { collectComponentUsages, componentInfo } from './tsxTransform';
|
||||||
|
import { version as viteVersion, build, preview, mergeConfig } from 'vite';
|
||||||
|
|
||||||
|
const log = debug('pw:vite');
|
||||||
|
|
||||||
let stoppableServer: any;
|
let stoppableServer: any;
|
||||||
const playwrightVersion = getPlaywrightVersion();
|
const playwrightVersion = getPlaywrightVersion();
|
||||||
@ -59,23 +63,51 @@ export function createPlugin(
|
|||||||
},
|
},
|
||||||
|
|
||||||
begin: async (suite: Suite) => {
|
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 use = config.projects[0].use as CtConfig;
|
||||||
const port = use.ctPort || 3100;
|
const port = use.ctPort || 3100;
|
||||||
const viteConfig = typeof use.ctViteConfig === 'function' ? await use.ctViteConfig() : (use.ctViteConfig || {});
|
const relativeTemplateDir = use.ctTemplateDir || 'playwright';
|
||||||
const templateDirConfig = use.ctTemplateDir || 'playwright';
|
|
||||||
|
|
||||||
const rootDir = viteConfig.root || configDir;
|
// FIXME: use build plugin to determine html location to resolve this.
|
||||||
const templateDir = path.resolve(rootDir, templateDirConfig);
|
// TemplateDir must be relative, otherwise we can't move the final index.html into its target location post-build.
|
||||||
const outDir = viteConfig?.build?.outDir || (use.ctCacheDir ? path.resolve(rootDir, use.ctCacheDir) : path.resolve(templateDir, '.cache'));
|
// 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 buildExists = false;
|
||||||
let buildInfo: BuildInfo;
|
let buildInfo: BuildInfo;
|
||||||
|
|
||||||
const registerSource = await fs.promises.readFile(registerSourceFile, 'utf-8');
|
const registerSource = await fs.promises.readFile(registerSourceFile, 'utf-8');
|
||||||
const registerSourceHash = calculateSha1(registerSource);
|
const registerSourceHash = calculateSha1(registerSource);
|
||||||
|
|
||||||
const { version: viteVersion } = await import('vite');
|
|
||||||
try {
|
try {
|
||||||
buildInfo = JSON.parse(await fs.promises.readFile(buildInfoFile, 'utf-8')) as BuildInfo;
|
buildInfo = JSON.parse(await fs.promises.readFile(buildInfoFile, 'utf-8')) as BuildInfo;
|
||||||
assert(buildInfo.version === playwrightVersion);
|
assert(buildInfo.version === playwrightVersion);
|
||||||
@ -92,54 +124,52 @@ export function createPlugin(
|
|||||||
sources: {},
|
sources: {},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
log('build exists:', buildExists);
|
||||||
|
|
||||||
const componentRegistry: ComponentRegistry = new Map();
|
const componentRegistry: ComponentRegistry = new Map();
|
||||||
// 1. Re-parse changed tests and collect required components.
|
// 1. Re-parse changed tests and collect required components.
|
||||||
const hasNewTests = await checkNewTests(suite, buildInfo, componentRegistry);
|
const hasNewTests = await checkNewTests(suite, buildInfo, componentRegistry);
|
||||||
|
log('has new tests:', hasNewTests);
|
||||||
|
|
||||||
// 2. Check if the set of required components has changed.
|
// 2. Check if the set of required components has changed.
|
||||||
const hasNewComponents = await checkNewComponents(buildInfo, componentRegistry);
|
const hasNewComponents = await checkNewComponents(buildInfo, componentRegistry);
|
||||||
|
log('has new components:', hasNewComponents);
|
||||||
|
|
||||||
// 3. Check component sources.
|
// 3. Check component sources.
|
||||||
const sourcesDirty = !buildExists || hasNewComponents || await checkSources(buildInfo);
|
const sourcesDirty = !buildExists || hasNewComponents || await checkSources(buildInfo);
|
||||||
|
log('sourcesDirty:', sourcesDirty);
|
||||||
|
|
||||||
// 4. Update component info.
|
// 4. Update component info.
|
||||||
buildInfo.components = [...componentRegistry.values()];
|
buildInfo.components = [...componentRegistry.values()];
|
||||||
|
|
||||||
viteConfig.root = rootDir;
|
const frameworkOverrides: UserConfig = { plugins: [] };
|
||||||
viteConfig.preview = { port, ...viteConfig.preview };
|
|
||||||
// Vite preview server will otherwise always return the index.html with 200.
|
|
||||||
viteConfig.appType = viteConfig.appType || 'custom';
|
|
||||||
|
|
||||||
// React heuristic. If we see a component in a file with .js extension,
|
// 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
|
// consider it a potential JSX-in-JS scenario and enable JSX loader for all
|
||||||
// .js files.
|
// .js files.
|
||||||
if (hasJSComponents(buildInfo.components)) {
|
if (hasJSComponents(buildInfo.components)) {
|
||||||
viteConfig.esbuild = {
|
log('jsx-in-js detected');
|
||||||
|
frameworkOverrides.esbuild = {
|
||||||
loader: 'jsx',
|
loader: 'jsx',
|
||||||
include: /.*\.jsx?$/,
|
include: /.*\.jsx?$/,
|
||||||
exclude: [],
|
exclude: [],
|
||||||
};
|
};
|
||||||
viteConfig.optimizeDeps = {
|
frameworkOverrides.optimizeDeps = {
|
||||||
esbuildOptions: {
|
esbuildOptions: {
|
||||||
loader: { '.js': 'jsx' },
|
loader: { '.js': 'jsx' },
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
const { build, preview } = await import('vite');
|
|
||||||
// Build config unconditionally, either build or build & preview will use it.
|
// We assume that any non-empty plugin list includes `vite-react` or similar.
|
||||||
viteConfig.plugins ??= [];
|
if (frameworkPluginFactory && !baseAndUserConfig.plugins?.length)
|
||||||
if (frameworkPluginFactory && !viteConfig.plugins.length)
|
frameworkOverrides.plugins = [await frameworkPluginFactory()];
|
||||||
viteConfig.plugins = [await frameworkPluginFactory()];
|
|
||||||
|
|
||||||
// But only add out own plugin when we actually build / transform.
|
// But only add out own plugin when we actually build / transform.
|
||||||
if (sourcesDirty)
|
if (sourcesDirty)
|
||||||
viteConfig.plugins.push(vitePlugin(registerSource, templateDir, buildInfo, componentRegistry));
|
frameworkOverrides.plugins!.push(vitePlugin(registerSource, templateDir, buildInfo, componentRegistry));
|
||||||
viteConfig.configFile = viteConfig.configFile || false;
|
|
||||||
viteConfig.define = viteConfig.define || {};
|
frameworkOverrides.build = {
|
||||||
viteConfig.define.__VUE_PROD_DEVTOOLS__ = true;
|
|
||||||
viteConfig.css = viteConfig.css || {};
|
|
||||||
viteConfig.css.devSourcemap = true;
|
|
||||||
viteConfig.build = {
|
|
||||||
...viteConfig.build,
|
|
||||||
outDir,
|
|
||||||
target: 'esnext',
|
target: 'esnext',
|
||||||
minify: false,
|
minify: false,
|
||||||
rollupOptions: {
|
rollupOptions: {
|
||||||
@ -151,24 +181,34 @@ export function createPlugin(
|
|||||||
sourcemap: true,
|
sourcemap: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const finalConfig = mergeConfig(baseAndUserConfig, frameworkOverrides);
|
||||||
|
|
||||||
if (sourcesDirty) {
|
if (sourcesDirty) {
|
||||||
await build(viteConfig);
|
log('build');
|
||||||
const relativeTemplateDir = path.relative(rootDir, templateDir);
|
await build(finalConfig);
|
||||||
await fs.promises.rename(path.resolve(outDir, relativeTemplateDir, 'index.html'), `${outDir}/index.html`);
|
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));
|
await fs.promises.writeFile(buildInfoFile, JSON.stringify(buildInfo, undefined, 2));
|
||||||
|
}
|
||||||
|
|
||||||
for (const [filename, testInfo] of Object.entries(buildInfo.tests))
|
for (const [filename, testInfo] of Object.entries(buildInfo.tests)) {
|
||||||
setExternalDependencies(filename, testInfo.deps);
|
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);
|
stoppableServer = stoppable(previewServer.httpServer, 0);
|
||||||
const isAddressInfo = (x: any): x is AddressInfo => x?.address;
|
const isAddressInfo = (x: any): x is AddressInfo => x?.address;
|
||||||
const address = previewServer.httpServer.address();
|
const address = previewServer.httpServer.address();
|
||||||
if (isAddressInfo(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}`;
|
process.env.PLAYWRIGHT_TEST_BASE_URL = `${protocol}//localhost:${address.port}`;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -194,7 +234,6 @@ type BuildInfo = {
|
|||||||
[key: string]: {
|
[key: string]: {
|
||||||
timestamp: number;
|
timestamp: number;
|
||||||
components: string[];
|
components: string[];
|
||||||
deps: string[];
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@ -205,9 +244,12 @@ async function checkSources(buildInfo: BuildInfo): Promise<boolean> {
|
|||||||
for (const [source, sourceInfo] of Object.entries(buildInfo.sources)) {
|
for (const [source, sourceInfo] of Object.entries(buildInfo.sources)) {
|
||||||
try {
|
try {
|
||||||
const timestamp = (await fs.promises.stat(source)).mtimeMs;
|
const timestamp = (await fs.promises.stat(source)).mtimeMs;
|
||||||
if (sourceInfo.timestamp !== timestamp)
|
if (sourceInfo.timestamp !== timestamp) {
|
||||||
|
log('source has changed:', source);
|
||||||
return true;
|
return true;
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
log('check source failed:', e);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -226,9 +268,10 @@ async function checkNewTests(suite: Suite, buildInfo: BuildInfo, componentRegist
|
|||||||
const timestamp = (await fs.promises.stat(testFile)).mtimeMs;
|
const timestamp = (await fs.promises.stat(testFile)).mtimeMs;
|
||||||
if (buildInfo.tests[testFile]?.timestamp !== timestamp) {
|
if (buildInfo.tests[testFile]?.timestamp !== timestamp) {
|
||||||
const components = await parseTestFile(testFile);
|
const components = await parseTestFile(testFile);
|
||||||
|
log('changed test:', testFile);
|
||||||
for (const component of components)
|
for (const component of components)
|
||||||
componentRegistry.set(component.fullName, component);
|
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;
|
hasNewTests = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -336,23 +379,13 @@ function vitePlugin(registerSource: string, templateDir: string, buildInfo: Buil
|
|||||||
},
|
},
|
||||||
|
|
||||||
async writeBundle(this: PluginContext) {
|
async writeBundle(this: PluginContext) {
|
||||||
const componentDeps = new Map<string, Set<string>>();
|
|
||||||
for (const component of componentRegistry.values()) {
|
for (const component of componentRegistry.values()) {
|
||||||
const id = (await moduleResolver(component.importPath));
|
const id = (await moduleResolver(component.importPath));
|
||||||
if (!id)
|
if (!id)
|
||||||
continue;
|
continue;
|
||||||
const deps = new Set<string>();
|
const deps = new Set<string>();
|
||||||
collectViteModuleDependencies(this, id, deps);
|
collectViteModuleDependencies(this, id, deps);
|
||||||
componentDeps.set(component.fullName, deps);
|
component.deps = [...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];
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
@ -53,7 +53,7 @@ function isComponent(component) {
|
|||||||
*/
|
*/
|
||||||
async function __pwResolveComponent(component) {
|
async function __pwResolveComponent(component) {
|
||||||
if (!isComponent(component))
|
if (!isComponent(component))
|
||||||
return
|
return;
|
||||||
|
|
||||||
let componentFactory = __pwLoaderRegistry.get(component.type);
|
let componentFactory = __pwLoaderRegistry.get(component.type);
|
||||||
if (!componentFactory) {
|
if (!componentFactory) {
|
||||||
@ -69,11 +69,11 @@ async function __pwResolveComponent(component) {
|
|||||||
if (!componentFactory && component.type[0].toUpperCase() === component.type[0])
|
if (!componentFactory && component.type[0].toUpperCase() === component.type[0])
|
||||||
throw new Error(`Unregistered component: ${component.type}. Following components are registered: ${[...__pwRegistry.keys()]}`);
|
throw new Error(`Unregistered component: ${component.type}. Following components are registered: ${[...__pwRegistry.keys()]}`);
|
||||||
|
|
||||||
if(componentFactory)
|
if (componentFactory)
|
||||||
__pwRegistry.set(component.type, await componentFactory())
|
__pwRegistry.set(component.type, await componentFactory());
|
||||||
|
|
||||||
if ('children' in component)
|
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) {
|
async function __pwResolveComponent(component) {
|
||||||
if (!isComponent(component))
|
if (!isComponent(component))
|
||||||
return
|
return;
|
||||||
|
|
||||||
let componentFactory = __pwLoaderRegistry.get(component.type);
|
let componentFactory = __pwLoaderRegistry.get(component.type);
|
||||||
if (!componentFactory) {
|
if (!componentFactory) {
|
||||||
@ -68,11 +68,11 @@ async function __pwResolveComponent(component) {
|
|||||||
if (!componentFactory && component.type[0].toUpperCase() === component.type[0])
|
if (!componentFactory && component.type[0].toUpperCase() === component.type[0])
|
||||||
throw new Error(`Unregistered component: ${component.type}. Following components are registered: ${[...__pwRegistry.keys()]}`);
|
throw new Error(`Unregistered component: ${component.type}. Following components are registered: ${[...__pwRegistry.keys()]}`);
|
||||||
|
|
||||||
if(componentFactory)
|
if (componentFactory)
|
||||||
__pwRegistry.set(component.type, await componentFactory())
|
__pwRegistry.set(component.type, await componentFactory());
|
||||||
|
|
||||||
if ('children' in component)
|
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;
|
return component;
|
||||||
|
|
||||||
const componentFunc = __pwRegistry.get(component.type);
|
const componentFunc = __pwRegistry.get(component.type);
|
||||||
|
|
||||||
if (component.kind !== 'jsx')
|
if (component.kind !== 'jsx')
|
||||||
throw new Error('Object mount notation is not supported');
|
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> {
|
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
|
// 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'),
|
fullName: expect.stringContaining('playwright_test_src_button_tsx_Button'),
|
||||||
importedName: 'Button',
|
importedName: 'Button',
|
||||||
importPath: expect.stringContaining('button.tsx'),
|
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'),
|
fullName: expect.stringContaining('playwright_test_src_clashingNames1_tsx_ClashingName'),
|
||||||
importedName: 'ClashingName',
|
importedName: 'ClashingName',
|
||||||
importPath: expect.stringContaining('clashingNames1.tsx'),
|
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'),
|
fullName: expect.stringContaining('playwright_test_src_clashingNames2_tsx_ClashingName'),
|
||||||
importedName: 'ClashingName',
|
importedName: 'ClashingName',
|
||||||
importPath: expect.stringContaining('clashingNames2.tsx'),
|
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'),
|
fullName: expect.stringContaining('playwright_test_src_components_tsx_Component1'),
|
||||||
importedName: 'Component1',
|
importedName: 'Component1',
|
||||||
importPath: expect.stringContaining('components.tsx'),
|
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'),
|
fullName: expect.stringContaining('playwright_test_src_components_tsx_Component2'),
|
||||||
importedName: 'Component2',
|
importedName: 'Component2',
|
||||||
importPath: expect.stringContaining('components.tsx'),
|
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'),
|
fullName: expect.stringContaining('playwright_test_src_defaultExport_tsx'),
|
||||||
importPath: expect.stringContaining('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)) {
|
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('clashingNames1_tsx_ClashingName'),
|
||||||
expect.stringContaining('clashingNames2_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')) {
|
if (file.endsWith('default-import.spec.tsx')) {
|
||||||
@ -186,10 +205,6 @@ test('should extract component list', async ({ runInlineTest }, testInfo) => {
|
|||||||
components: [
|
components: [
|
||||||
expect.stringContaining('defaultExport_tsx'),
|
expect.stringContaining('defaultExport_tsx'),
|
||||||
],
|
],
|
||||||
deps: [
|
|
||||||
expect.stringContaining('defaultExport.tsx'),
|
|
||||||
expect.stringContaining('jsx-runtime.js'),
|
|
||||||
]
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (file.endsWith('named-imports.spec.tsx')) {
|
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_Component1'),
|
||||||
expect.stringContaining('components_tsx_Component2'),
|
expect.stringContaining('components_tsx_Component2'),
|
||||||
],
|
],
|
||||||
deps: [
|
|
||||||
expect.stringContaining('components.tsx'),
|
|
||||||
expect.stringContaining('jsx-runtime.js'),
|
|
||||||
]
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (file.endsWith('one-import.spec.tsx')) {
|
if (file.endsWith('one-import.spec.tsx')) {
|
||||||
@ -211,10 +222,6 @@ test('should extract component list', async ({ runInlineTest }, testInfo) => {
|
|||||||
components: [
|
components: [
|
||||||
expect.stringContaining('button_tsx_Button'),
|
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.exitCode).toBe(0);
|
||||||
expect(runResult.passed).toBe(1);
|
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…
x
Reference in New Issue
Block a user