mirror of
https://github.com/microsoft/playwright.git
synced 2025-01-05 19:04:43 +03:00
fix(ct): move import list into the compilation cache data (#28986)
This commit is contained in:
parent
e6d51cf7bd
commit
1db18711a2
@ -18,10 +18,11 @@ import path from 'path';
|
||||
import type { T, BabelAPI, PluginObj } from 'playwright/src/transform/babelBundle';
|
||||
import { types, declare, traverse } from 'playwright/lib/transform/babelBundle';
|
||||
import { resolveImportSpecifierExtension } from 'playwright/lib/util';
|
||||
import { setTransformData } from 'playwright/lib/transform/transform';
|
||||
const t: typeof T = types;
|
||||
|
||||
let componentNames: Set<string>;
|
||||
let componentImports: Map<string, ImportInfo>;
|
||||
let jsxComponentNames: Set<string>;
|
||||
let importInfos: Map<string, ImportInfo>;
|
||||
|
||||
export default declare((api: BabelAPI) => {
|
||||
api.assertVersion(7);
|
||||
@ -31,9 +32,8 @@ export default declare((api: BabelAPI) => {
|
||||
visitor: {
|
||||
Program: {
|
||||
enter(path) {
|
||||
const result = collectComponentUsages(path.node);
|
||||
componentNames = result.names;
|
||||
componentImports = new Map();
|
||||
jsxComponentNames = collectJsxComponentUsages(path.node);
|
||||
importInfos = new Map();
|
||||
},
|
||||
exit(path) {
|
||||
let firstDeclaration: any;
|
||||
@ -47,13 +47,14 @@ export default declare((api: BabelAPI) => {
|
||||
const insertionPath = lastImportDeclaration || firstDeclaration;
|
||||
if (!insertionPath)
|
||||
return;
|
||||
for (const componentImport of [...componentImports.values()].reverse()) {
|
||||
|
||||
for (const [localName, componentImport] of [...importInfos.entries()].reverse()) {
|
||||
insertionPath.insertAfter(
|
||||
t.variableDeclaration(
|
||||
'const',
|
||||
[
|
||||
t.variableDeclarator(
|
||||
t.identifier(componentImport.localName),
|
||||
t.identifier(localName),
|
||||
t.objectExpression([
|
||||
t.objectProperty(t.identifier('__pw_type'), t.stringLiteral('importRef')),
|
||||
t.objectProperty(t.identifier('id'), t.stringLiteral(componentImport.id)),
|
||||
@ -63,6 +64,7 @@ export default declare((api: BabelAPI) => {
|
||||
)
|
||||
);
|
||||
}
|
||||
setTransformData('playwright-ct-core', [...importInfos.values()]);
|
||||
}
|
||||
},
|
||||
|
||||
@ -71,19 +73,35 @@ export default declare((api: BabelAPI) => {
|
||||
if (!t.isStringLiteral(importNode.source))
|
||||
return;
|
||||
|
||||
let components = 0;
|
||||
const ext = path.extname(importNode.source.value);
|
||||
|
||||
// Convert all non-JS imports into refs.
|
||||
if (!allJsExtensions.has(ext)) {
|
||||
for (const specifier of importNode.specifiers) {
|
||||
if (t.isImportNamespaceSpecifier(specifier))
|
||||
continue;
|
||||
const { localName, info } = importInfo(importNode, specifier, this.filename!);
|
||||
importInfos.set(localName, info);
|
||||
}
|
||||
p.skip();
|
||||
p.remove();
|
||||
return;
|
||||
}
|
||||
|
||||
// Convert JS imports that are used as components in JSX expressions into refs.
|
||||
let importCount = 0;
|
||||
for (const specifier of importNode.specifiers) {
|
||||
if (t.isImportNamespaceSpecifier(specifier))
|
||||
continue;
|
||||
const info = importInfo(importNode, specifier, this.filename!);
|
||||
if (!componentNames.has(info.localName))
|
||||
continue;
|
||||
componentImports.set(info.localName, info);
|
||||
++components;
|
||||
const { localName, info } = importInfo(importNode, specifier, this.filename!);
|
||||
if (jsxComponentNames.has(localName)) {
|
||||
importInfos.set(localName, info);
|
||||
++importCount;
|
||||
}
|
||||
}
|
||||
|
||||
// All the imports were components => delete.
|
||||
if (components && components === importNode.specifiers.length) {
|
||||
// All the imports were from JSX => delete.
|
||||
if (importCount && importCount === importNode.specifiers.length) {
|
||||
p.skip();
|
||||
p.remove();
|
||||
}
|
||||
@ -92,7 +110,7 @@ export default declare((api: BabelAPI) => {
|
||||
MemberExpression(path) {
|
||||
if (!t.isIdentifier(path.node.object))
|
||||
return;
|
||||
if (!componentImports.has(path.node.object.name))
|
||||
if (!importInfos.has(path.node.object.name))
|
||||
return;
|
||||
if (!t.isIdentifier(path.node.property))
|
||||
return;
|
||||
@ -108,25 +126,10 @@ export default declare((api: BabelAPI) => {
|
||||
return result;
|
||||
});
|
||||
|
||||
export function collectComponentUsages(node: T.Node) {
|
||||
const importedLocalNames = new Set<string>();
|
||||
function collectJsxComponentUsages(node: T.Node): Set<string> {
|
||||
const names = new Set<string>();
|
||||
traverse(node, {
|
||||
enter: p => {
|
||||
|
||||
// First look at all the imports.
|
||||
if (t.isImportDeclaration(p.node)) {
|
||||
const importNode = p.node;
|
||||
if (!t.isStringLiteral(importNode.source))
|
||||
return;
|
||||
|
||||
for (const specifier of importNode.specifiers) {
|
||||
if (t.isImportNamespaceSpecifier(specifier))
|
||||
continue;
|
||||
importedLocalNames.add(specifier.local.name);
|
||||
}
|
||||
}
|
||||
|
||||
// Treat JSX-everything as component usages.
|
||||
if (t.isJSXElement(p.node)) {
|
||||
if (t.isJSXIdentifier(p.node.openingElement.name))
|
||||
@ -134,30 +137,19 @@ export function collectComponentUsages(node: T.Node) {
|
||||
if (t.isJSXMemberExpression(p.node.openingElement.name) && t.isJSXIdentifier(p.node.openingElement.name.object) && t.isJSXIdentifier(p.node.openingElement.name.property))
|
||||
names.add(p.node.openingElement.name.object.name);
|
||||
}
|
||||
|
||||
// Treat mount(identifier, ...) as component usage if it is in the importedLocalNames list.
|
||||
if (t.isAwaitExpression(p.node) && t.isCallExpression(p.node.argument) && t.isIdentifier(p.node.argument.callee) && p.node.argument.callee.name === 'mount') {
|
||||
const callExpression = p.node.argument;
|
||||
const arg = callExpression.arguments[0];
|
||||
if (!t.isIdentifier(arg) || !importedLocalNames.has(arg.name))
|
||||
return;
|
||||
|
||||
names.add(arg.name);
|
||||
}
|
||||
}
|
||||
});
|
||||
return { names };
|
||||
return names;
|
||||
}
|
||||
|
||||
export type ImportInfo = {
|
||||
id: string;
|
||||
isModuleOrAlias: boolean;
|
||||
importPath: string;
|
||||
localName: string;
|
||||
remoteName: string | undefined;
|
||||
};
|
||||
|
||||
export function importInfo(importNode: T.ImportDeclaration, specifier: T.ImportSpecifier | T.ImportDefaultSpecifier, filename: string): ImportInfo {
|
||||
export function importInfo(importNode: T.ImportDeclaration, specifier: T.ImportSpecifier | T.ImportDefaultSpecifier, filename: string): { localName: string, info: ImportInfo } {
|
||||
const importSource = importNode.source.value;
|
||||
const isModuleOrAlias = !importSource.startsWith('.');
|
||||
const unresolvedImportPath = path.resolve(path.dirname(filename), importSource);
|
||||
@ -171,7 +163,6 @@ export function importInfo(importNode: T.ImportDeclaration, specifier: T.ImportS
|
||||
id: idPrefix,
|
||||
importPath,
|
||||
isModuleOrAlias,
|
||||
localName: specifier.local.name,
|
||||
remoteName: undefined,
|
||||
};
|
||||
|
||||
@ -184,5 +175,7 @@ export function importInfo(importNode: T.ImportDeclaration, specifier: T.ImportS
|
||||
|
||||
if (result.remoteName)
|
||||
result.id += '_' + result.remoteName;
|
||||
return result;
|
||||
return { localName: specifier.local.name, info: result };
|
||||
}
|
||||
|
||||
const allJsExtensions = new Set(['.js', '.jsx', '.cjs', '.mjs', '.ts', '.tsx', '.cts', '.mts', '']);
|
||||
|
@ -25,12 +25,10 @@ import { debug } from 'playwright-core/lib/utilsBundle';
|
||||
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { parse, traverse, types as t } from 'playwright/lib/transform/babelBundle';
|
||||
import { stoppable } from 'playwright/lib/utilsBundle';
|
||||
import { assert, calculateSha1 } from 'playwright-core/lib/utils';
|
||||
import { getPlaywrightVersion } from 'playwright-core/lib/utils';
|
||||
import { setExternalDependencies } from 'playwright/lib/transform/compilationCache';
|
||||
import { collectComponentUsages, importInfo } from './tsxTransform';
|
||||
import { getUserData, internalDependenciesForTestFile, setExternalDependencies } from 'playwright/lib/transform/compilationCache';
|
||||
import { version as viteVersion, build, preview, mergeConfig } from 'vite';
|
||||
import { source as injectedSource } from './generated/indexSource';
|
||||
import type { ImportInfo } from './tsxTransform';
|
||||
@ -40,14 +38,6 @@ const log = debug('pw:vite');
|
||||
let stoppableServer: any;
|
||||
const playwrightVersion = getPlaywrightVersion();
|
||||
|
||||
type ComponentInfo = {
|
||||
id: string;
|
||||
importPath: string;
|
||||
isModuleOrAlias: boolean;
|
||||
remoteName: string | undefined;
|
||||
deps: string[];
|
||||
};
|
||||
|
||||
type CtConfig = BasePlaywrightTestConfig['use'] & {
|
||||
ctPort?: number;
|
||||
ctTemplateDir?: string;
|
||||
@ -131,16 +121,16 @@ export function createPlugin(
|
||||
viteVersion,
|
||||
registerSourceHash,
|
||||
components: [],
|
||||
tests: {},
|
||||
sources: {},
|
||||
deps: {},
|
||||
};
|
||||
}
|
||||
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);
|
||||
const componentsByImportingFile = new Map<string, string[]>();
|
||||
// 1. Populate component registry based on tests' component imports.
|
||||
await populateComponentsFromTests(componentRegistry, componentsByImportingFile);
|
||||
|
||||
// 2. Check if the set of required components has changed.
|
||||
const hasNewComponents = await checkNewComponents(buildInfo, componentRegistry);
|
||||
@ -177,8 +167,9 @@ export function createPlugin(
|
||||
frameworkOverrides.plugins = [await frameworkPluginFactory()];
|
||||
|
||||
// But only add out own plugin when we actually build / transform.
|
||||
const depsCollector = new Map<string, string[]>();
|
||||
if (sourcesDirty)
|
||||
frameworkOverrides.plugins!.push(vitePlugin(registerSource, templateDir, buildInfo, componentRegistry));
|
||||
frameworkOverrides.plugins!.push(vitePlugin(registerSource, templateDir, buildInfo, componentRegistry, depsCollector));
|
||||
|
||||
frameworkOverrides.build = {
|
||||
target: 'esnext',
|
||||
@ -198,20 +189,31 @@ export function createPlugin(
|
||||
log('build');
|
||||
await build(finalConfig);
|
||||
await fs.promises.rename(`${finalConfig.build.outDir}/${relativeTemplateDir}/index.html`, `${finalConfig.build.outDir}/index.html`);
|
||||
buildInfo.deps = Object.fromEntries(depsCollector.entries());
|
||||
}
|
||||
|
||||
if (hasNewTests || hasNewComponents || sourcesDirty) {
|
||||
if (hasNewComponents || sourcesDirty) {
|
||||
log('write manifest');
|
||||
await fs.promises.writeFile(buildInfoFile, JSON.stringify(buildInfo, undefined, 2));
|
||||
}
|
||||
|
||||
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));
|
||||
for (const projectSuite of suite.suites) {
|
||||
for (const fileSuite of projectSuite.suites) {
|
||||
// For every test file...
|
||||
const testFile = fileSuite.location!.file;
|
||||
const deps = new Set<string>();
|
||||
// Collect its JS dependencies (helpers).
|
||||
for (const file of [testFile, ...(internalDependenciesForTestFile(testFile) || [])]) {
|
||||
// For each helper, get all the imported components.
|
||||
for (const componentFile of componentsByImportingFile.get(file) || []) {
|
||||
// For each component, get all the dependencies.
|
||||
for (const d of depsCollector.get(componentFile) || [])
|
||||
deps.add(d);
|
||||
}
|
||||
}
|
||||
// Now we have test file => all components along with dependencies.
|
||||
setExternalDependencies(testFile, [...deps]);
|
||||
}
|
||||
setExternalDependencies(filename, [...deps]);
|
||||
}
|
||||
|
||||
const previewServer = await preview(finalConfig);
|
||||
@ -240,16 +242,13 @@ type BuildInfo = {
|
||||
timestamp: number;
|
||||
}
|
||||
};
|
||||
components: ComponentInfo[];
|
||||
tests: {
|
||||
[key: string]: {
|
||||
timestamp: number;
|
||||
components: string[];
|
||||
}
|
||||
};
|
||||
components: ImportInfo[];
|
||||
deps: {
|
||||
[key: string]: string[];
|
||||
}
|
||||
};
|
||||
|
||||
type ComponentRegistry = Map<string, ComponentInfo>;
|
||||
type ComponentRegistry = Map<string, ImportInfo>;
|
||||
|
||||
async function checkSources(buildInfo: BuildInfo): Promise<boolean> {
|
||||
for (const [source, sourceInfo] of Object.entries(buildInfo.sources)) {
|
||||
@ -267,35 +266,13 @@ async function checkSources(buildInfo: BuildInfo): Promise<boolean> {
|
||||
return false;
|
||||
}
|
||||
|
||||
async function checkNewTests(suite: Suite, buildInfo: BuildInfo, componentRegistry: ComponentRegistry): Promise<boolean> {
|
||||
const testFiles = new Set<string>();
|
||||
for (const project of suite.suites) {
|
||||
for (const file of project.suites)
|
||||
testFiles.add(file.location!.file);
|
||||
async function populateComponentsFromTests(componentRegistry: ComponentRegistry, componentsByImportingFile: Map<string, string[]>) {
|
||||
const importInfos: Map<string, ImportInfo[]> = await getUserData('playwright-ct-core');
|
||||
for (const [file, importList] of importInfos) {
|
||||
for (const importInfo of importList)
|
||||
componentRegistry.set(importInfo.id, importInfo);
|
||||
componentsByImportingFile.set(file, importList.filter(i => !i.isModuleOrAlias).map(i => i.importPath));
|
||||
}
|
||||
|
||||
let hasNewTests = false;
|
||||
for (const testFile of testFiles) {
|
||||
const timestamp = (await fs.promises.stat(testFile)).mtimeMs;
|
||||
if (buildInfo.tests[testFile]?.timestamp !== timestamp) {
|
||||
const componentImports = await parseTestFile(testFile);
|
||||
log('changed test:', testFile);
|
||||
for (const componentImport of componentImports) {
|
||||
const ci: ComponentInfo = {
|
||||
id: componentImport.id,
|
||||
isModuleOrAlias: componentImport.isModuleOrAlias,
|
||||
importPath: componentImport.importPath,
|
||||
remoteName: componentImport.remoteName,
|
||||
deps: [],
|
||||
};
|
||||
componentRegistry.set(componentImport.id, { ...ci, deps: [] });
|
||||
}
|
||||
buildInfo.tests[testFile] = { timestamp, components: componentImports.map(c => c.id) };
|
||||
hasNewTests = true;
|
||||
}
|
||||
}
|
||||
|
||||
return hasNewTests;
|
||||
}
|
||||
|
||||
async function checkNewComponents(buildInfo: BuildInfo, componentRegistry: ComponentRegistry): Promise<boolean> {
|
||||
@ -315,36 +292,7 @@ async function checkNewComponents(buildInfo: BuildInfo, componentRegistry: Compo
|
||||
return hasNewComponents;
|
||||
}
|
||||
|
||||
async function parseTestFile(testFile: string): Promise<ImportInfo[]> {
|
||||
const text = await fs.promises.readFile(testFile, 'utf-8');
|
||||
const ast = parse(text, { errorRecovery: true, plugins: ['typescript', 'jsx'], sourceType: 'module' });
|
||||
const componentUsages = collectComponentUsages(ast);
|
||||
const componentNames = componentUsages.names;
|
||||
const result: ImportInfo[] = [];
|
||||
|
||||
traverse(ast, {
|
||||
enter: p => {
|
||||
if (t.isImportDeclaration(p.node)) {
|
||||
const importNode = p.node;
|
||||
if (!t.isStringLiteral(importNode.source))
|
||||
return;
|
||||
|
||||
for (const specifier of importNode.specifiers) {
|
||||
if (t.isImportNamespaceSpecifier(specifier))
|
||||
continue;
|
||||
const info = importInfo(importNode, specifier, testFile);
|
||||
if (!componentNames.has(info.localName))
|
||||
continue;
|
||||
result.push(info);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function vitePlugin(registerSource: string, templateDir: string, buildInfo: BuildInfo, componentRegistry: ComponentRegistry): Plugin {
|
||||
function vitePlugin(registerSource: string, templateDir: string, buildInfo: BuildInfo, importInfos: Map<string, ImportInfo>, depsCollector: Map<string, string[]>): Plugin {
|
||||
buildInfo.sources = {};
|
||||
let moduleResolver: ResolveFn;
|
||||
return {
|
||||
@ -384,12 +332,12 @@ function vitePlugin(registerSource: string, templateDir: string, buildInfo: Buil
|
||||
const lines = [content, ''];
|
||||
lines.push(registerSource);
|
||||
|
||||
for (const [alias, value] of componentRegistry) {
|
||||
for (const value of importInfos.values()) {
|
||||
const importPath = value.isModuleOrAlias ? value.importPath : './' + path.relative(folder, value.importPath).replace(/\\/g, '/');
|
||||
lines.push(`const ${alias} = () => import('${importPath}').then((mod) => mod.${value.remoteName || 'default'});`);
|
||||
lines.push(`const ${value.id} = () => import('${importPath}').then((mod) => mod.${value.remoteName || 'default'});`);
|
||||
}
|
||||
|
||||
lines.push(`__pwRegistry.initialize({ ${[...componentRegistry.keys()].join(',\n ')} });`);
|
||||
lines.push(`__pwRegistry.initialize({ ${[...importInfos.keys()].join(',\n ')} });`);
|
||||
return {
|
||||
code: lines.join('\n'),
|
||||
map: { mappings: '' }
|
||||
@ -397,13 +345,13 @@ function vitePlugin(registerSource: string, templateDir: string, buildInfo: Buil
|
||||
},
|
||||
|
||||
async writeBundle(this: PluginContext) {
|
||||
for (const component of componentRegistry.values()) {
|
||||
const id = (await moduleResolver(component.importPath));
|
||||
for (const importInfo of importInfos.values()) {
|
||||
const deps = new Set<string>();
|
||||
const id = await moduleResolver(importInfo.importPath);
|
||||
if (!id)
|
||||
continue;
|
||||
const deps = new Set<string>();
|
||||
collectViteModuleDependencies(this, id, deps);
|
||||
component.deps = [...deps];
|
||||
depsCollector.set(importInfo.importPath, [...deps]);
|
||||
}
|
||||
},
|
||||
};
|
||||
@ -423,7 +371,7 @@ function collectViteModuleDependencies(context: PluginContext, id: string, deps:
|
||||
collectViteModuleDependencies(context, importedId, deps);
|
||||
}
|
||||
|
||||
function hasJSComponents(components: ComponentInfo[]): boolean {
|
||||
function hasJSComponents(components: ImportInfo[]): boolean {
|
||||
for (const component of components) {
|
||||
const extname = path.extname(component.importPath);
|
||||
if (extname === '.js' || !extname && fs.existsSync(component.importPath + '.js'))
|
||||
|
@ -24,6 +24,7 @@
|
||||
"./lib/transform/babelBundle": "./lib/transform/babelBundle.js",
|
||||
"./lib/transform/compilationCache": "./lib/transform/compilationCache.js",
|
||||
"./lib/transform/esmLoader": "./lib/transform/esmLoader.js",
|
||||
"./lib/transform/transform": "./lib/transform/transform.js",
|
||||
"./lib/internalsForTest": "./lib/internalsForTest.js",
|
||||
"./lib/plugins": "./lib/plugins/index.js",
|
||||
"./jsx-runtime": {
|
||||
|
@ -23,9 +23,17 @@ import { isWorkerProcess } from '../common/globals';
|
||||
export type MemoryCache = {
|
||||
codePath: string;
|
||||
sourceMapPath: string;
|
||||
dataPath: string;
|
||||
moduleUrl?: string;
|
||||
};
|
||||
|
||||
type SerializedCompilationCache = {
|
||||
sourceMaps: [string, string][],
|
||||
memoryCache: [string, MemoryCache][],
|
||||
fileDependencies: [string, string[]][],
|
||||
externalDependencies: [string, string[]][],
|
||||
};
|
||||
|
||||
// Assumptions for the compilation cache:
|
||||
// - Files in the temp directory we work with can disappear at any moment, either some of them or all together.
|
||||
// - Multiple workers can be trying to read from the compilation cache at the same time.
|
||||
@ -84,12 +92,12 @@ export function installSourceMapSupportIfNeeded() {
|
||||
});
|
||||
}
|
||||
|
||||
function _innerAddToCompilationCache(filename: string, options: { codePath: string, sourceMapPath: string, moduleUrl?: string }) {
|
||||
sourceMaps.set(options.moduleUrl || filename, options.sourceMapPath);
|
||||
memoryCache.set(filename, options);
|
||||
function _innerAddToCompilationCache(filename: string, entry: MemoryCache) {
|
||||
sourceMaps.set(entry.moduleUrl || filename, entry.sourceMapPath);
|
||||
memoryCache.set(filename, entry);
|
||||
}
|
||||
|
||||
export function getFromCompilationCache(filename: string, hash: string, moduleUrl?: string): { cachedCode?: string, addToCache?: (code: string, map?: any) => void } {
|
||||
export function getFromCompilationCache(filename: string, hash: string, moduleUrl?: string): { cachedCode?: string, addToCache?: (code: string, map: any | undefined | null, data: Map<string, any>) => void } {
|
||||
// First check the memory cache by filename, this cache will always work in the worker,
|
||||
// because we just compiled this file in the loader.
|
||||
const cache = memoryCache.get(filename);
|
||||
@ -105,27 +113,30 @@ export function getFromCompilationCache(filename: string, hash: string, moduleUr
|
||||
const cachePath = calculateCachePath(filename, hash);
|
||||
const codePath = cachePath + '.js';
|
||||
const sourceMapPath = cachePath + '.map';
|
||||
const dataPath = cachePath + '.data';
|
||||
try {
|
||||
const cachedCode = fs.readFileSync(codePath, 'utf8');
|
||||
_innerAddToCompilationCache(filename, { codePath, sourceMapPath, moduleUrl });
|
||||
_innerAddToCompilationCache(filename, { codePath, sourceMapPath, dataPath, moduleUrl });
|
||||
return { cachedCode };
|
||||
} catch {
|
||||
}
|
||||
|
||||
return {
|
||||
addToCache: (code: string, map: any) => {
|
||||
addToCache: (code: string, map: any | undefined | null, data: Map<string, any>) => {
|
||||
if (isWorkerProcess())
|
||||
return;
|
||||
fs.mkdirSync(path.dirname(cachePath), { recursive: true });
|
||||
if (map)
|
||||
fs.writeFileSync(sourceMapPath, JSON.stringify(map), 'utf8');
|
||||
if (data.size)
|
||||
fs.writeFileSync(dataPath, JSON.stringify(Object.fromEntries(data.entries()), undefined, 2), 'utf8');
|
||||
fs.writeFileSync(codePath, code, 'utf8');
|
||||
_innerAddToCompilationCache(filename, { codePath, sourceMapPath, moduleUrl });
|
||||
_innerAddToCompilationCache(filename, { codePath, sourceMapPath, dataPath, moduleUrl });
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function serializeCompilationCache(): any {
|
||||
export function serializeCompilationCache(): SerializedCompilationCache {
|
||||
return {
|
||||
sourceMaps: [...sourceMaps.entries()],
|
||||
memoryCache: [...memoryCache.entries()],
|
||||
@ -200,6 +211,10 @@ export function collectAffectedTestFiles(dependency: string, testFileCollector:
|
||||
}
|
||||
}
|
||||
|
||||
export function internalDependenciesForTestFile(filename: string): Set<string> | undefined{
|
||||
return fileDependencies.get(filename);
|
||||
}
|
||||
|
||||
export function dependenciesForTestFile(filename: string): Set<string> {
|
||||
const result = new Set<string>();
|
||||
for (const dep of fileDependencies.get(filename) || [])
|
||||
@ -224,3 +239,17 @@ export function belongsToNodeModules(file: string) {
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
export async function getUserData(pluginName: string): Promise<Map<string, any>> {
|
||||
const result = new Map<string, any>();
|
||||
for (const [fileName, cache] of memoryCache) {
|
||||
if (!cache.dataPath)
|
||||
continue;
|
||||
if (!fs.existsSync(cache.dataPath))
|
||||
continue;
|
||||
const data = JSON.parse(await fs.promises.readFile(cache.dataPath, 'utf8'));
|
||||
if (data[pluginName])
|
||||
result.set(fileName, data[pluginName]);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
@ -16,8 +16,8 @@
|
||||
|
||||
import crypto from 'crypto';
|
||||
import path from 'path';
|
||||
import { sourceMapSupport, pirates } from '../utilsBundle';
|
||||
import url from 'url';
|
||||
import { sourceMapSupport, pirates } from '../utilsBundle';
|
||||
import type { Location } from '../../types/testReporter';
|
||||
import type { TsConfigLoaderResult } from '../third_party/tsconfig-loader';
|
||||
import { tsConfigLoader } from '../third_party/tsconfig-loader';
|
||||
@ -159,6 +159,12 @@ export function shouldTransform(filename: string): boolean {
|
||||
return !belongsToNodeModules(filename);
|
||||
}
|
||||
|
||||
let transformData: Map<string, any>;
|
||||
|
||||
export function setTransformData(pluginName: string, value: any) {
|
||||
transformData.set(pluginName, value);
|
||||
}
|
||||
|
||||
export function transformHook(originalCode: string, filename: string, moduleUrl?: string): string {
|
||||
const isTypeScript = filename.endsWith('.ts') || filename.endsWith('.tsx') || filename.endsWith('.mts') || filename.endsWith('.cts');
|
||||
const hasPreprocessor =
|
||||
@ -177,9 +183,10 @@ export function transformHook(originalCode: string, filename: string, moduleUrl?
|
||||
process.env.BROWSERSLIST_IGNORE_OLD_DATA = 'true';
|
||||
|
||||
const { babelTransform }: { babelTransform: BabelTransformFunction } = require('./babelBundle');
|
||||
transformData = new Map<string, any>();
|
||||
const { code, map } = babelTransform(originalCode, filename, isTypeScript, !!moduleUrl, pluginsPrologue, pluginsEpilogue);
|
||||
if (code)
|
||||
addToCache!(code, map);
|
||||
addToCache!(code, map, transformData);
|
||||
return code || '';
|
||||
}
|
||||
|
||||
|
@ -76,3 +76,4 @@ test('drag resize', async ({ page, mount }) => {
|
||||
expect.soft(mainBox).toEqual({ x: 0, y: 0, width: 500, height: 100 });
|
||||
expect.soft(sidebarBox).toEqual({ x: 0, y: 101, width: 500, height: 399 });
|
||||
});
|
||||
|
||||
|
@ -76,6 +76,8 @@ export async function writeFiles(testInfo: TestInfo, files: Files, initial: bool
|
||||
|
||||
await Promise.all(Object.keys(files).map(async name => {
|
||||
const fullName = path.join(baseDir, name);
|
||||
if (files[name] === undefined)
|
||||
return;
|
||||
await fs.promises.mkdir(path.dirname(fullName), { recursive: true });
|
||||
await fs.promises.writeFile(fullName, files[name]);
|
||||
}));
|
||||
|
@ -44,7 +44,7 @@ test('should work with the empty component list', async ({ runInlineTest }, test
|
||||
const metainfo = JSON.parse(fs.readFileSync(testInfo.outputPath('playwright/.cache/metainfo.json'), 'utf-8'));
|
||||
expect(metainfo.version).toEqual(require('playwright-core/package.json').version);
|
||||
expect(metainfo.viteVersion).toEqual(require('vite/package.json').version);
|
||||
expect(Object.entries(metainfo.tests)).toHaveLength(1);
|
||||
expect(Object.entries(metainfo.deps)).toHaveLength(0);
|
||||
expect(Object.entries(metainfo.sources)).toHaveLength(9);
|
||||
});
|
||||
|
||||
@ -139,92 +139,57 @@ test('should extract component list', async ({ runInlineTest }, testInfo) => {
|
||||
remoteName: 'Button',
|
||||
importPath: expect.stringContaining('button.tsx'),
|
||||
isModuleOrAlias: false,
|
||||
deps: [
|
||||
expect.stringContaining('button.tsx'),
|
||||
expect.stringContaining('jsx-runtime.js'),
|
||||
]
|
||||
}, {
|
||||
id: expect.stringContaining('playwright_test_src_clashingNames1_tsx_ClashingName'),
|
||||
remoteName: 'ClashingName',
|
||||
importPath: expect.stringContaining('clashingNames1.tsx'),
|
||||
isModuleOrAlias: false,
|
||||
deps: [
|
||||
expect.stringContaining('clashingNames1.tsx'),
|
||||
expect.stringContaining('jsx-runtime.js'),
|
||||
]
|
||||
}, {
|
||||
id: expect.stringContaining('playwright_test_src_clashingNames2_tsx_ClashingName'),
|
||||
remoteName: 'ClashingName',
|
||||
importPath: expect.stringContaining('clashingNames2.tsx'),
|
||||
isModuleOrAlias: false,
|
||||
deps: [
|
||||
expect.stringContaining('clashingNames2.tsx'),
|
||||
expect.stringContaining('jsx-runtime.js'),
|
||||
]
|
||||
}, {
|
||||
id: expect.stringContaining('playwright_test_src_components_tsx_Component1'),
|
||||
remoteName: 'Component1',
|
||||
importPath: expect.stringContaining('components.tsx'),
|
||||
isModuleOrAlias: false,
|
||||
deps: [
|
||||
expect.stringContaining('components.tsx'),
|
||||
expect.stringContaining('jsx-runtime.js'),
|
||||
]
|
||||
}, {
|
||||
id: expect.stringContaining('playwright_test_src_components_tsx_Component2'),
|
||||
remoteName: 'Component2',
|
||||
importPath: expect.stringContaining('components.tsx'),
|
||||
isModuleOrAlias: false,
|
||||
deps: [
|
||||
expect.stringContaining('components.tsx'),
|
||||
expect.stringContaining('jsx-runtime.js'),
|
||||
]
|
||||
}, {
|
||||
id: expect.stringContaining('playwright_test_src_defaultExport_tsx'),
|
||||
importPath: expect.stringContaining('defaultExport.tsx'),
|
||||
isModuleOrAlias: false,
|
||||
deps: [
|
||||
expect.stringContaining('defaultExport.tsx'),
|
||||
expect.stringContaining('jsx-runtime.js'),
|
||||
]
|
||||
}]);
|
||||
|
||||
for (const [file, test] of Object.entries(metainfo.tests)) {
|
||||
if (file.endsWith('clashing-imports.spec.tsx')) {
|
||||
expect(test).toEqual({
|
||||
timestamp: expect.any(Number),
|
||||
components: [
|
||||
expect.stringContaining('clashingNames1_tsx_ClashingName'),
|
||||
expect.stringContaining('clashingNames2_tsx_ClashingName'),
|
||||
],
|
||||
});
|
||||
}
|
||||
if (file.endsWith('default-import.spec.tsx')) {
|
||||
expect(test).toEqual({
|
||||
timestamp: expect.any(Number),
|
||||
components: [
|
||||
expect.stringContaining('defaultExport_tsx'),
|
||||
],
|
||||
});
|
||||
}
|
||||
if (file.endsWith('named-imports.spec.tsx')) {
|
||||
expect(test).toEqual({
|
||||
timestamp: expect.any(Number),
|
||||
components: [
|
||||
expect.stringContaining('components_tsx_Component1'),
|
||||
expect.stringContaining('components_tsx_Component2'),
|
||||
],
|
||||
});
|
||||
}
|
||||
if (file.endsWith('one-import.spec.tsx')) {
|
||||
expect(test).toEqual({
|
||||
timestamp: expect.any(Number),
|
||||
components: [
|
||||
expect.stringContaining('button_tsx_Button'),
|
||||
],
|
||||
});
|
||||
}
|
||||
}
|
||||
for (const [, value] of Object.entries(metainfo.deps))
|
||||
(value as string[]).sort();
|
||||
|
||||
expect(Object.entries(metainfo.deps)).toEqual([
|
||||
[expect.stringContaining('clashingNames1.tsx'), [
|
||||
expect.stringContaining('jsx-runtime.js'),
|
||||
expect.stringContaining('clashingNames1.tsx'),
|
||||
]],
|
||||
[expect.stringContaining('clashingNames2.tsx'), [
|
||||
expect.stringContaining('jsx-runtime.js'),
|
||||
expect.stringContaining('clashingNames2.tsx'),
|
||||
]],
|
||||
[expect.stringContaining('defaultExport.tsx'), [
|
||||
expect.stringContaining('jsx-runtime.js'),
|
||||
expect.stringContaining('defaultExport.tsx'),
|
||||
]],
|
||||
[expect.stringContaining('components.tsx'), [
|
||||
expect.stringContaining('jsx-runtime.js'),
|
||||
expect.stringContaining('components.tsx'),
|
||||
]],
|
||||
[expect.stringContaining('button.tsx'), [
|
||||
expect.stringContaining('jsx-runtime.js'),
|
||||
expect.stringContaining('button.tsx'),
|
||||
]],
|
||||
]);
|
||||
});
|
||||
|
||||
test('should cache build', async ({ runInlineTest }, testInfo) => {
|
||||
@ -495,21 +460,74 @@ test('should retain deps when test changes', async ({ runInlineTest }, testInfo)
|
||||
remoteName: 'Button',
|
||||
importPath: expect.stringContaining('button.tsx'),
|
||||
isModuleOrAlias: false,
|
||||
deps: [
|
||||
expect.stringContaining('button.tsx'),
|
||||
expect.stringContaining('jsx-runtime.js'),
|
||||
]
|
||||
}]);
|
||||
|
||||
expect(Object.entries(metainfo.tests)).toEqual([
|
||||
for (const [, value] of Object.entries(metainfo.deps))
|
||||
(value as string[]).sort();
|
||||
|
||||
expect(Object.entries(metainfo.deps)).toEqual([
|
||||
[
|
||||
expect.stringContaining('button.test.tsx'),
|
||||
{
|
||||
components: [
|
||||
expect.stringContaining('src_button_tsx_Button'),
|
||||
],
|
||||
timestamp: expect.any(Number)
|
||||
}
|
||||
expect.stringContaining('button.tsx'),
|
||||
[
|
||||
expect.stringContaining('jsx-runtime.js'),
|
||||
expect.stringContaining('button.tsx'),
|
||||
],
|
||||
]
|
||||
]);
|
||||
});
|
||||
|
||||
test('should render component via re-export', async ({ runInlineTest }, testInfo) => {
|
||||
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/buttonHelper.ts': `
|
||||
import { Button } from './button.tsx';
|
||||
export { Button };
|
||||
`,
|
||||
'src/button.test.tsx': `
|
||||
import { test, expect } from '@playwright/experimental-ct-react';
|
||||
import { Button } from './buttonHelper';
|
||||
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);
|
||||
});
|
||||
|
||||
test('should render component exported via fixture', async ({ runInlineTest }, testInfo) => {
|
||||
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/buttonFixture.tsx': `
|
||||
import { Button } from './button';
|
||||
import { test as baseTest } from '@playwright/experimental-ct-react';
|
||||
export { expect } from '@playwright/experimental-ct-react';
|
||||
export const test = baseTest.extend({
|
||||
button: async ({ mount }, use) => {
|
||||
await use(await mount(<Button></Button>));
|
||||
}
|
||||
});
|
||||
`,
|
||||
'src/button.test.tsx': `
|
||||
import { test, expect } from './buttonFixture';
|
||||
test('pass', async ({ button }) => {
|
||||
await expect(button).toHaveText('Button');
|
||||
});
|
||||
`,
|
||||
}, { workers: 1 });
|
||||
|
||||
expect(result.exitCode).toBe(0);
|
||||
expect(result.passed).toBe(1);
|
||||
});
|
||||
|
@ -207,3 +207,81 @@ test('should watch component', async ({ runUITest, writeFiles }) => {
|
||||
❌ pass <=
|
||||
`);
|
||||
});
|
||||
|
||||
test('should watch component via util', async ({ runUITest, writeFiles }) => {
|
||||
const { page } = await runUITest({
|
||||
...basicTestTree,
|
||||
'src/button.tsx': undefined,
|
||||
'src/button.ts': `
|
||||
import { Button } from './buttonComponent';
|
||||
export { Button };
|
||||
`,
|
||||
'src/buttonComponent.tsx': `
|
||||
export const Button = () => <button>Button</button>;
|
||||
`,
|
||||
});
|
||||
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/buttonComponent.tsx': `
|
||||
export const Button = () => <button>Button2</button>;
|
||||
`
|
||||
});
|
||||
|
||||
await expect.poll(dumpTestTree(page)).toBe(`
|
||||
▼ ❌ button.test.tsx
|
||||
❌ pass <=
|
||||
`);
|
||||
});
|
||||
|
||||
test('should watch component when editing util', async ({ runUITest, writeFiles }) => {
|
||||
const { page } = await runUITest({
|
||||
...basicTestTree,
|
||||
'src/button.tsx': undefined,
|
||||
'src/button.ts': `
|
||||
import { Button } from './buttonComponent';
|
||||
export { Button };
|
||||
`,
|
||||
'src/buttonComponent.tsx': `
|
||||
export const Button = () => <button>Button</button>;
|
||||
`,
|
||||
'src/buttonComponent2.tsx': `
|
||||
export const Button = () => <button>Button2</button>;
|
||||
`,
|
||||
});
|
||||
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.ts': `
|
||||
import { Button } from './buttonComponent2';
|
||||
export { Button };
|
||||
`,
|
||||
});
|
||||
|
||||
await expect.poll(dumpTestTree(page)).toBe(`
|
||||
▼ ❌ button.test.tsx
|
||||
❌ pass <=
|
||||
`);
|
||||
});
|
||||
|
Loading…
Reference in New Issue
Block a user