chore(ct): allow using component as a property (#27272)

This commit is contained in:
Pavel Feldman 2023-09-25 17:00:52 -07:00 committed by GitHub
parent 4e62468aee
commit aed86c98a8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 85 additions and 19 deletions

View File

@ -42,19 +42,21 @@ export default declare((api: BabelAPI) => {
if (!t.isStringLiteral(importNode.source))
return;
let remove = false;
let components = 0;
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;
if (t.isImportNamespaceSpecifier(specifier))
continue;
const { fullName } = componentInfo(specifier, importNode.source.value, this.filename!);
fullNames.set(specifier.local.name, fullName);
remove = true;
const { fullName } = componentInfo(specifier, importNode.source.value, this.filename!, componentName);
fullNames.set(componentName, fullName);
++components;
}
// If one of the imports was a component, consider them all component imports.
if (remove) {
// All the imports were components => delete.
if (components && components === importNode.specifiers.length) {
p.skip();
p.remove();
}
@ -70,8 +72,14 @@ export default declare((api: BabelAPI) => {
JSXElement(path) {
const jsxElement = path.node;
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;
const componentName = fullNames.get(nameOrExpression) || nameOrExpression;
const props: (T.ObjectProperty | T.SpreadElement)[] = [];
@ -113,7 +121,6 @@ export default declare((api: BabelAPI) => {
children.push(t.spreadElement(child.expression));
}
const componentName = fullNames.get(jsxName.name) || jsxName.name;
path.replaceWith(t.objectExpression([
t.objectProperty(t.identifier('kind'), t.stringLiteral('jsx')),
t.objectProperty(t.identifier('type'), t.stringLiteral(componentName)),
@ -147,8 +154,12 @@ export function collectComponentUsages(node: T.Node) {
}
// Treat JSX-everything as component usages.
if (t.isJSXElement(p.node) && t.isJSXIdentifier(p.node.openingElement.name))
names.add(p.node.openingElement.name.name);
if (t.isJSXElement(p.node)) {
if (t.isJSXIdentifier(p.node.openingElement.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.
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;
isModuleOrAlias: boolean;
importedName?: string;
importedNameProperty?: 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 unresolvedImportPath = path.resolve(path.dirname(filename), importSource);
// 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 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))
return { fullName: prefix, deps: [], ...pathInfo };
return { fullName: prefix + fullNameSuffix, importedNameProperty, deps: [], ...pathInfo };
if (t.isIdentifier(specifier.imported))
return { fullName: prefix + '_' + specifier.imported.name, importedName: specifier.imported.name, deps: [], ...pathInfo };
return { fullName: prefix + '_' + specifier.imported.value, importedName: specifier.imported.value, deps: [], ...pathInfo };
return { fullName: prefix + '_' + specifier.imported.name + fullNameSuffix, importedName: specifier.imported.name, importedNameProperty, deps: [], ...pathInfo };
return { fullName: prefix + '_' + specifier.imported.value + fullNameSuffix, importedName: specifier.imported.value, importedNameProperty, deps: [], ...pathInfo };
}

View File

@ -300,6 +300,7 @@ async function parseTestFile(testFile: string): Promise<ComponentInfo[]> {
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: ComponentInfo[] = [];
traverse(ast, {
@ -310,11 +311,13 @@ async function parseTestFile(testFile: string): Promise<ComponentInfo[]> {
return;
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;
if (t.isImportNamespaceSpecifier(specifier))
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) {
const importPath = value.isModuleOrAlias ? value.importPath : './' + path.relative(folder, value.importPath).replace(/\\/g, '/');
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
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 ')} });`);

View File

@ -137,6 +137,7 @@ test('should extract component list', async ({ runInlineTest }, testInfo) => {
expect(metainfo.components).toEqual([{
fullName: expect.stringContaining('playwright_test_src_button_tsx_Button'),
importedName: 'Button',
importedNameProperty: '',
importPath: expect.stringContaining('button.tsx'),
isModuleOrAlias: false,
deps: [
@ -146,6 +147,7 @@ test('should extract component list', async ({ runInlineTest }, testInfo) => {
}, {
fullName: expect.stringContaining('playwright_test_src_clashingNames1_tsx_ClashingName'),
importedName: 'ClashingName',
importedNameProperty: '',
importPath: expect.stringContaining('clashingNames1.tsx'),
isModuleOrAlias: false,
deps: [
@ -155,6 +157,7 @@ test('should extract component list', async ({ runInlineTest }, testInfo) => {
}, {
fullName: expect.stringContaining('playwright_test_src_clashingNames2_tsx_ClashingName'),
importedName: 'ClashingName',
importedNameProperty: '',
importPath: expect.stringContaining('clashingNames2.tsx'),
isModuleOrAlias: false,
deps: [
@ -164,6 +167,7 @@ test('should extract component list', async ({ runInlineTest }, testInfo) => {
}, {
fullName: expect.stringContaining('playwright_test_src_components_tsx_Component1'),
importedName: 'Component1',
importedNameProperty: '',
importPath: expect.stringContaining('components.tsx'),
isModuleOrAlias: false,
deps: [
@ -173,6 +177,7 @@ test('should extract component list', async ({ runInlineTest }, testInfo) => {
}, {
fullName: expect.stringContaining('playwright_test_src_components_tsx_Component2'),
importedName: 'Component2',
importedNameProperty: '',
importPath: expect.stringContaining('components.tsx'),
isModuleOrAlias: false,
deps: [
@ -182,6 +187,7 @@ test('should extract component list', async ({ runInlineTest }, testInfo) => {
}, {
fullName: expect.stringContaining('playwright_test_src_defaultExport_tsx'),
importPath: expect.stringContaining('defaultExport.tsx'),
importedNameProperty: '',
isModuleOrAlias: false,
deps: [
expect.stringContaining('defaultExport.tsx'),
@ -493,6 +499,7 @@ test('should retain deps when test changes', async ({ runInlineTest }, testInfo)
expect(metainfo.components).toEqual([{
fullName: expect.stringContaining('playwright_test_src_button_tsx_Button'),
importedName: 'Button',
importedNameProperty: '',
importPath: expect.stringContaining('button.tsx'),
isModuleOrAlias: false,
deps: [

View File

@ -337,3 +337,38 @@ test('should bundle public folder', async ({ runInlineTest }) => {
expect(result.exitCode).toBe(0);
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);
});