mirror of
https://github.com/microsoft/playwright.git
synced 2025-06-26 21:40:17 +00:00
chore(ct): allow using component as a property (#27272)
This commit is contained in:
parent
4e62468aee
commit
aed86c98a8
@ -42,19 +42,21 @@ export default declare((api: BabelAPI) => {
|
|||||||
if (!t.isStringLiteral(importNode.source))
|
if (!t.isStringLiteral(importNode.source))
|
||||||
return;
|
return;
|
||||||
|
|
||||||
let remove = false;
|
let components = 0;
|
||||||
for (const specifier of importNode.specifiers) {
|
for (const specifier of importNode.specifiers) {
|
||||||
if (!componentNames.has(specifier.local.name))
|
const specifierName = specifier.local.name;
|
||||||
|
const componentName = componentNames.has(specifierName) ? specifierName : [...componentNames].find(c => c.startsWith(specifierName + '.'));
|
||||||
|
if (!componentName)
|
||||||
continue;
|
continue;
|
||||||
if (t.isImportNamespaceSpecifier(specifier))
|
if (t.isImportNamespaceSpecifier(specifier))
|
||||||
continue;
|
continue;
|
||||||
const { fullName } = componentInfo(specifier, importNode.source.value, this.filename!);
|
const { fullName } = componentInfo(specifier, importNode.source.value, this.filename!, componentName);
|
||||||
fullNames.set(specifier.local.name, fullName);
|
fullNames.set(componentName, fullName);
|
||||||
remove = true;
|
++components;
|
||||||
}
|
}
|
||||||
|
|
||||||
// If one of the imports was a component, consider them all component imports.
|
// All the imports were components => delete.
|
||||||
if (remove) {
|
if (components && components === importNode.specifiers.length) {
|
||||||
p.skip();
|
p.skip();
|
||||||
p.remove();
|
p.remove();
|
||||||
}
|
}
|
||||||
@ -70,8 +72,14 @@ export default declare((api: BabelAPI) => {
|
|||||||
JSXElement(path) {
|
JSXElement(path) {
|
||||||
const jsxElement = path.node;
|
const jsxElement = path.node;
|
||||||
const jsxName = jsxElement.openingElement.name;
|
const jsxName = jsxElement.openingElement.name;
|
||||||
if (!t.isJSXIdentifier(jsxName))
|
let nameOrExpression: string = '';
|
||||||
|
if (t.isJSXIdentifier(jsxName))
|
||||||
|
nameOrExpression = jsxName.name;
|
||||||
|
else if (t.isJSXMemberExpression(jsxName) && t.isJSXIdentifier(jsxName.object) && t.isJSXIdentifier(jsxName.property))
|
||||||
|
nameOrExpression = jsxName.object.name + '.' + jsxName.property.name;
|
||||||
|
if (!nameOrExpression)
|
||||||
return;
|
return;
|
||||||
|
const componentName = fullNames.get(nameOrExpression) || nameOrExpression;
|
||||||
|
|
||||||
const props: (T.ObjectProperty | T.SpreadElement)[] = [];
|
const props: (T.ObjectProperty | T.SpreadElement)[] = [];
|
||||||
|
|
||||||
@ -113,7 +121,6 @@ export default declare((api: BabelAPI) => {
|
|||||||
children.push(t.spreadElement(child.expression));
|
children.push(t.spreadElement(child.expression));
|
||||||
}
|
}
|
||||||
|
|
||||||
const componentName = fullNames.get(jsxName.name) || jsxName.name;
|
|
||||||
path.replaceWith(t.objectExpression([
|
path.replaceWith(t.objectExpression([
|
||||||
t.objectProperty(t.identifier('kind'), t.stringLiteral('jsx')),
|
t.objectProperty(t.identifier('kind'), t.stringLiteral('jsx')),
|
||||||
t.objectProperty(t.identifier('type'), t.stringLiteral(componentName)),
|
t.objectProperty(t.identifier('type'), t.stringLiteral(componentName)),
|
||||||
@ -147,8 +154,12 @@ export function collectComponentUsages(node: T.Node) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Treat JSX-everything as component usages.
|
// Treat JSX-everything as component usages.
|
||||||
if (t.isJSXElement(p.node) && t.isJSXIdentifier(p.node.openingElement.name))
|
if (t.isJSXElement(p.node)) {
|
||||||
|
if (t.isJSXIdentifier(p.node.openingElement.name))
|
||||||
names.add(p.node.openingElement.name.name);
|
names.add(p.node.openingElement.name.name);
|
||||||
|
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 + '.' + p.node.openingElement.name.property.name);
|
||||||
|
}
|
||||||
|
|
||||||
// Treat mount(identifier, ...) as component usage if it is in the importedLocalNames list.
|
// 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') {
|
if (t.isAwaitExpression(p.node) && t.isCallExpression(p.node.argument) && t.isIdentifier(p.node.argument.callee) && p.node.argument.callee.name === 'mount') {
|
||||||
@ -170,10 +181,11 @@ export type ComponentInfo = {
|
|||||||
importPath: string;
|
importPath: string;
|
||||||
isModuleOrAlias: boolean;
|
isModuleOrAlias: boolean;
|
||||||
importedName?: string;
|
importedName?: string;
|
||||||
|
importedNameProperty?: string;
|
||||||
deps: 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, componentName: string): ComponentInfo {
|
||||||
const isModuleOrAlias = !importSource.startsWith('.');
|
const isModuleOrAlias = !importSource.startsWith('.');
|
||||||
const unresolvedImportPath = path.resolve(path.dirname(filename), importSource);
|
const unresolvedImportPath = path.resolve(path.dirname(filename), importSource);
|
||||||
// Support following notations for Button.tsx:
|
// Support following notations for Button.tsx:
|
||||||
@ -183,10 +195,19 @@ export function componentInfo(specifier: T.ImportSpecifier | T.ImportDefaultSpec
|
|||||||
const prefix = importPath.replace(/[^\w_\d]/g, '_');
|
const prefix = importPath.replace(/[^\w_\d]/g, '_');
|
||||||
const pathInfo = { importPath, isModuleOrAlias };
|
const pathInfo = { importPath, isModuleOrAlias };
|
||||||
|
|
||||||
|
const specifierName = specifier.local.name;
|
||||||
|
let fullNameSuffix = '';
|
||||||
|
let importedNameProperty = '';
|
||||||
|
if (componentName !== specifierName) {
|
||||||
|
const suffix = componentName.substring(specifierName.length + 1);
|
||||||
|
fullNameSuffix = '_' + suffix;
|
||||||
|
importedNameProperty = '.' + suffix;
|
||||||
|
}
|
||||||
|
|
||||||
if (t.isImportDefaultSpecifier(specifier))
|
if (t.isImportDefaultSpecifier(specifier))
|
||||||
return { fullName: prefix, deps: [], ...pathInfo };
|
return { fullName: prefix + fullNameSuffix, importedNameProperty, deps: [], ...pathInfo };
|
||||||
|
|
||||||
if (t.isIdentifier(specifier.imported))
|
if (t.isIdentifier(specifier.imported))
|
||||||
return { fullName: prefix + '_' + specifier.imported.name, importedName: specifier.imported.name, deps: [], ...pathInfo };
|
return { fullName: prefix + '_' + specifier.imported.name + fullNameSuffix, importedName: specifier.imported.name, importedNameProperty, deps: [], ...pathInfo };
|
||||||
return { fullName: prefix + '_' + specifier.imported.value, importedName: specifier.imported.value, deps: [], ...pathInfo };
|
return { fullName: prefix + '_' + specifier.imported.value + fullNameSuffix, importedName: specifier.imported.value, importedNameProperty, deps: [], ...pathInfo };
|
||||||
}
|
}
|
||||||
|
@ -300,6 +300,7 @@ async function parseTestFile(testFile: string): Promise<ComponentInfo[]> {
|
|||||||
const text = await fs.promises.readFile(testFile, 'utf-8');
|
const text = await fs.promises.readFile(testFile, 'utf-8');
|
||||||
const ast = parse(text, { errorRecovery: true, plugins: ['typescript', 'jsx'], sourceType: 'module' });
|
const ast = parse(text, { errorRecovery: true, plugins: ['typescript', 'jsx'], sourceType: 'module' });
|
||||||
const componentUsages = collectComponentUsages(ast);
|
const componentUsages = collectComponentUsages(ast);
|
||||||
|
const componentNames = componentUsages.names;
|
||||||
const result: ComponentInfo[] = [];
|
const result: ComponentInfo[] = [];
|
||||||
|
|
||||||
traverse(ast, {
|
traverse(ast, {
|
||||||
@ -310,11 +311,13 @@ async function parseTestFile(testFile: string): Promise<ComponentInfo[]> {
|
|||||||
return;
|
return;
|
||||||
|
|
||||||
for (const specifier of importNode.specifiers) {
|
for (const specifier of importNode.specifiers) {
|
||||||
if (!componentUsages.names.has(specifier.local.name))
|
const specifierName = specifier.local.name;
|
||||||
|
const componentName = componentNames.has(specifierName) ? specifierName : [...componentNames].find(c => c.startsWith(specifierName + '.'));
|
||||||
|
if (!componentName)
|
||||||
continue;
|
continue;
|
||||||
if (t.isImportNamespaceSpecifier(specifier))
|
if (t.isImportNamespaceSpecifier(specifier))
|
||||||
continue;
|
continue;
|
||||||
result.push(componentInfo(specifier, importNode.source.value, testFile));
|
result.push(componentInfo(specifier, importNode.source.value, testFile, componentName));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -366,9 +369,9 @@ function vitePlugin(registerSource: string, templateDir: string, buildInfo: Buil
|
|||||||
for (const [alias, value] of componentRegistry) {
|
for (const [alias, value] of componentRegistry) {
|
||||||
const importPath = value.isModuleOrAlias ? value.importPath : './' + path.relative(folder, value.importPath).replace(/\\/g, '/');
|
const importPath = value.isModuleOrAlias ? value.importPath : './' + path.relative(folder, value.importPath).replace(/\\/g, '/');
|
||||||
if (value.importedName)
|
if (value.importedName)
|
||||||
lines.push(`const ${alias} = () => import('${importPath}').then((mod) => mod.${value.importedName});`);
|
lines.push(`const ${alias} = () => import('${importPath}').then((mod) => mod.${value.importedName + (value.importedNameProperty || '')});`);
|
||||||
else
|
else
|
||||||
lines.push(`const ${alias} = () => import('${importPath}').then((mod) => mod.default);`);
|
lines.push(`const ${alias} = () => import('${importPath}').then((mod) => mod.default${value.importedNameProperty || ''});`);
|
||||||
}
|
}
|
||||||
|
|
||||||
lines.push(`pwRegister({ ${[...componentRegistry.keys()].join(',\n ')} });`);
|
lines.push(`pwRegister({ ${[...componentRegistry.keys()].join(',\n ')} });`);
|
||||||
|
@ -137,6 +137,7 @@ test('should extract component list', async ({ runInlineTest }, testInfo) => {
|
|||||||
expect(metainfo.components).toEqual([{
|
expect(metainfo.components).toEqual([{
|
||||||
fullName: expect.stringContaining('playwright_test_src_button_tsx_Button'),
|
fullName: expect.stringContaining('playwright_test_src_button_tsx_Button'),
|
||||||
importedName: 'Button',
|
importedName: 'Button',
|
||||||
|
importedNameProperty: '',
|
||||||
importPath: expect.stringContaining('button.tsx'),
|
importPath: expect.stringContaining('button.tsx'),
|
||||||
isModuleOrAlias: false,
|
isModuleOrAlias: false,
|
||||||
deps: [
|
deps: [
|
||||||
@ -146,6 +147,7 @@ test('should extract component list', async ({ runInlineTest }, testInfo) => {
|
|||||||
}, {
|
}, {
|
||||||
fullName: expect.stringContaining('playwright_test_src_clashingNames1_tsx_ClashingName'),
|
fullName: expect.stringContaining('playwright_test_src_clashingNames1_tsx_ClashingName'),
|
||||||
importedName: 'ClashingName',
|
importedName: 'ClashingName',
|
||||||
|
importedNameProperty: '',
|
||||||
importPath: expect.stringContaining('clashingNames1.tsx'),
|
importPath: expect.stringContaining('clashingNames1.tsx'),
|
||||||
isModuleOrAlias: false,
|
isModuleOrAlias: false,
|
||||||
deps: [
|
deps: [
|
||||||
@ -155,6 +157,7 @@ test('should extract component list', async ({ runInlineTest }, testInfo) => {
|
|||||||
}, {
|
}, {
|
||||||
fullName: expect.stringContaining('playwright_test_src_clashingNames2_tsx_ClashingName'),
|
fullName: expect.stringContaining('playwright_test_src_clashingNames2_tsx_ClashingName'),
|
||||||
importedName: 'ClashingName',
|
importedName: 'ClashingName',
|
||||||
|
importedNameProperty: '',
|
||||||
importPath: expect.stringContaining('clashingNames2.tsx'),
|
importPath: expect.stringContaining('clashingNames2.tsx'),
|
||||||
isModuleOrAlias: false,
|
isModuleOrAlias: false,
|
||||||
deps: [
|
deps: [
|
||||||
@ -164,6 +167,7 @@ test('should extract component list', async ({ runInlineTest }, testInfo) => {
|
|||||||
}, {
|
}, {
|
||||||
fullName: expect.stringContaining('playwright_test_src_components_tsx_Component1'),
|
fullName: expect.stringContaining('playwright_test_src_components_tsx_Component1'),
|
||||||
importedName: 'Component1',
|
importedName: 'Component1',
|
||||||
|
importedNameProperty: '',
|
||||||
importPath: expect.stringContaining('components.tsx'),
|
importPath: expect.stringContaining('components.tsx'),
|
||||||
isModuleOrAlias: false,
|
isModuleOrAlias: false,
|
||||||
deps: [
|
deps: [
|
||||||
@ -173,6 +177,7 @@ test('should extract component list', async ({ runInlineTest }, testInfo) => {
|
|||||||
}, {
|
}, {
|
||||||
fullName: expect.stringContaining('playwright_test_src_components_tsx_Component2'),
|
fullName: expect.stringContaining('playwright_test_src_components_tsx_Component2'),
|
||||||
importedName: 'Component2',
|
importedName: 'Component2',
|
||||||
|
importedNameProperty: '',
|
||||||
importPath: expect.stringContaining('components.tsx'),
|
importPath: expect.stringContaining('components.tsx'),
|
||||||
isModuleOrAlias: false,
|
isModuleOrAlias: false,
|
||||||
deps: [
|
deps: [
|
||||||
@ -182,6 +187,7 @@ test('should extract component list', async ({ runInlineTest }, testInfo) => {
|
|||||||
}, {
|
}, {
|
||||||
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'),
|
||||||
|
importedNameProperty: '',
|
||||||
isModuleOrAlias: false,
|
isModuleOrAlias: false,
|
||||||
deps: [
|
deps: [
|
||||||
expect.stringContaining('defaultExport.tsx'),
|
expect.stringContaining('defaultExport.tsx'),
|
||||||
@ -493,6 +499,7 @@ test('should retain deps when test changes', async ({ runInlineTest }, testInfo)
|
|||||||
expect(metainfo.components).toEqual([{
|
expect(metainfo.components).toEqual([{
|
||||||
fullName: expect.stringContaining('playwright_test_src_button_tsx_Button'),
|
fullName: expect.stringContaining('playwright_test_src_button_tsx_Button'),
|
||||||
importedName: 'Button',
|
importedName: 'Button',
|
||||||
|
importedNameProperty: '',
|
||||||
importPath: expect.stringContaining('button.tsx'),
|
importPath: expect.stringContaining('button.tsx'),
|
||||||
isModuleOrAlias: false,
|
isModuleOrAlias: false,
|
||||||
deps: [
|
deps: [
|
||||||
|
@ -337,3 +337,38 @@ test('should bundle public folder', async ({ runInlineTest }) => {
|
|||||||
expect(result.exitCode).toBe(0);
|
expect(result.exitCode).toBe(0);
|
||||||
expect(result.passed).toBe(1);
|
expect(result.passed).toBe(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('should work with property expressions in JSX', async ({ runInlineTest }) => {
|
||||||
|
const result = await runInlineTest({
|
||||||
|
'playwright.config.ts': playwrightConfig,
|
||||||
|
'playwright/index.html': `<script type="module" src="./index.ts"></script>`,
|
||||||
|
'playwright/index.ts': `
|
||||||
|
`,
|
||||||
|
'src/button1.tsx': `
|
||||||
|
const Button = () => <button>Button 1</button>;
|
||||||
|
export const components1 = { Button };
|
||||||
|
`,
|
||||||
|
'src/button2.tsx': `
|
||||||
|
const Button = () => <button>Button 2</button>;
|
||||||
|
export default { Button };
|
||||||
|
`,
|
||||||
|
'src/button.test.tsx': `
|
||||||
|
import { test, expect } from '@playwright/experimental-ct-react';
|
||||||
|
import { components1 } from './button1';
|
||||||
|
import components2 from './button2';
|
||||||
|
|
||||||
|
test('pass 1', async ({ mount }) => {
|
||||||
|
const component = await mount(<components1.Button />);
|
||||||
|
await expect(component).toHaveText('Button 1');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('pass 2', async ({ mount }) => {
|
||||||
|
const component = await mount(<components2.Button />);
|
||||||
|
await expect(component).toHaveText('Button 2');
|
||||||
|
});
|
||||||
|
`,
|
||||||
|
}, { workers: 1 });
|
||||||
|
|
||||||
|
expect(result.exitCode).toBe(0);
|
||||||
|
expect(result.passed).toBe(2);
|
||||||
|
});
|
||||||
|
Loading…
x
Reference in New Issue
Block a user