mirror of
https://github.com/microsoft/playwright.git
synced 2025-01-05 19:04:43 +03: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))
|
||||
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 };
|
||||
}
|
||||
|
@ -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 ')} });`);
|
||||
|
@ -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: [
|
||||
|
@ -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);
|
||||
});
|
||||
|
Loading…
Reference in New Issue
Block a user