mirror of
https://github.com/microsoft/playwright.git
synced 2025-06-26 21:40:17 +00:00
fix(ct): resolve paths before detecting JS (#29308)
This commit is contained in:
parent
d56a50245b
commit
eb8cbbbf62
@ -17,8 +17,7 @@
|
|||||||
import path from 'path';
|
import path from 'path';
|
||||||
import type { T, BabelAPI, PluginObj } from 'playwright/src/transform/babelBundle';
|
import type { T, BabelAPI, PluginObj } from 'playwright/src/transform/babelBundle';
|
||||||
import { types, declare, traverse } from 'playwright/lib/transform/babelBundle';
|
import { types, declare, traverse } from 'playwright/lib/transform/babelBundle';
|
||||||
import { resolveImportSpecifierExtension } from 'playwright/lib/util';
|
import { resolveHook, setTransformData } from 'playwright/lib/transform/transform';
|
||||||
import { setTransformData } from 'playwright/lib/transform/transform';
|
|
||||||
const t: typeof T = types;
|
const t: typeof T = types;
|
||||||
|
|
||||||
let jsxComponentNames: Set<string>;
|
let jsxComponentNames: Set<string>;
|
||||||
@ -72,15 +71,15 @@ export default declare((api: BabelAPI) => {
|
|||||||
const importNode = p.node;
|
const importNode = p.node;
|
||||||
if (!t.isStringLiteral(importNode.source))
|
if (!t.isStringLiteral(importNode.source))
|
||||||
return;
|
return;
|
||||||
|
const importPath = resolveImportSource(importNode.source.value, this.filename!);
|
||||||
const ext = path.extname(importNode.source.value);
|
const ext = path.extname(importPath);
|
||||||
|
|
||||||
// Convert all non-JS imports into refs.
|
// Convert all non-JS imports into refs.
|
||||||
if (!allJsExtensions.has(ext)) {
|
if (!allJsExtensions.has(ext)) {
|
||||||
for (const specifier of importNode.specifiers) {
|
for (const specifier of importNode.specifiers) {
|
||||||
if (t.isImportNamespaceSpecifier(specifier))
|
if (t.isImportNamespaceSpecifier(specifier))
|
||||||
continue;
|
continue;
|
||||||
const { localName, info } = importInfo(importNode, specifier, this.filename!);
|
const { localName, info } = importInfo(importPath, specifier, this.filename!);
|
||||||
importInfos.set(localName, info);
|
importInfos.set(localName, info);
|
||||||
}
|
}
|
||||||
p.skip();
|
p.skip();
|
||||||
@ -93,7 +92,7 @@ export default declare((api: BabelAPI) => {
|
|||||||
for (const specifier of importNode.specifiers) {
|
for (const specifier of importNode.specifiers) {
|
||||||
if (t.isImportNamespaceSpecifier(specifier))
|
if (t.isImportNamespaceSpecifier(specifier))
|
||||||
continue;
|
continue;
|
||||||
const { localName, info } = importInfo(importNode, specifier, this.filename!);
|
const { localName, info } = importInfo(importPath, specifier, this.filename!);
|
||||||
if (jsxComponentNames.has(localName)) {
|
if (jsxComponentNames.has(localName)) {
|
||||||
importInfos.set(localName, info);
|
importInfos.set(localName, info);
|
||||||
++importCount;
|
++importCount;
|
||||||
@ -144,25 +143,22 @@ function collectJsxComponentUsages(node: T.Node): Set<string> {
|
|||||||
|
|
||||||
export type ImportInfo = {
|
export type ImportInfo = {
|
||||||
id: string;
|
id: string;
|
||||||
isModuleOrAlias: boolean;
|
|
||||||
importPath: string;
|
importPath: string;
|
||||||
remoteName: string | undefined;
|
remoteName: string | undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function importInfo(importNode: T.ImportDeclaration, specifier: T.ImportSpecifier | T.ImportDefaultSpecifier, filename: string): { localName: string, info: ImportInfo } {
|
function resolveImportSource(importSource: string, filename: string): string {
|
||||||
const importSource = importNode.source.value;
|
|
||||||
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:
|
const importPath = resolveHook(filename, importSource) || unresolvedImportPath;
|
||||||
// - import { Button } from './Button.js' - via resolveImportSpecifierExtension
|
return importPath;
|
||||||
// - import { Button } from './Button' - via require.resolve
|
}
|
||||||
const importPath = isModuleOrAlias ? importSource : resolveImportSpecifierExtension(unresolvedImportPath) || require.resolve(unresolvedImportPath);
|
|
||||||
|
function importInfo(importPath: string, specifier: T.ImportSpecifier | T.ImportDefaultSpecifier, filename: string): { localName: string, info: ImportInfo } {
|
||||||
const idPrefix = importPath.replace(/[^\w_\d]/g, '_');
|
const idPrefix = importPath.replace(/[^\w_\d]/g, '_');
|
||||||
|
|
||||||
const result: ImportInfo = {
|
const result: ImportInfo = {
|
||||||
id: idPrefix,
|
id: idPrefix,
|
||||||
importPath,
|
importPath,
|
||||||
isModuleOrAlias,
|
|
||||||
remoteName: undefined,
|
remoteName: undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -143,7 +143,7 @@ export async function populateComponentsFromTests(componentRegistry: ComponentRe
|
|||||||
for (const importInfo of importList)
|
for (const importInfo of importList)
|
||||||
componentRegistry.set(importInfo.id, importInfo);
|
componentRegistry.set(importInfo.id, importInfo);
|
||||||
if (componentsByImportingFile)
|
if (componentsByImportingFile)
|
||||||
componentsByImportingFile.set(file, importList.filter(i => !i.isModuleOrAlias).map(i => i.importPath));
|
componentsByImportingFile.set(file, importList.map(i => i.importPath));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -179,7 +179,7 @@ export function transformIndexFile(id: string, content: string, templateDir: str
|
|||||||
lines.push(registerSource);
|
lines.push(registerSource);
|
||||||
|
|
||||||
for (const value of importInfos.values()) {
|
for (const value of importInfos.values()) {
|
||||||
const importPath = value.isModuleOrAlias ? value.importPath : './' + path.relative(folder, value.importPath).replace(/\\/g, '/');
|
const importPath = './' + path.relative(folder, value.importPath).replace(/\\/g, '/');
|
||||||
lines.push(`const ${value.id} = () => import('${importPath}').then((mod) => mod.${value.remoteName || 'default'});`);
|
lines.push(`const ${value.id} = () => import('${importPath}').then((mod) => mod.${value.remoteName || 'default'});`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -143,31 +143,25 @@ test('should extract component list', async ({ runInlineTest }, testInfo) => {
|
|||||||
id: expect.stringContaining('playwright_test_src_button_tsx_Button'),
|
id: expect.stringContaining('playwright_test_src_button_tsx_Button'),
|
||||||
remoteName: 'Button',
|
remoteName: 'Button',
|
||||||
importPath: expect.stringContaining('button.tsx'),
|
importPath: expect.stringContaining('button.tsx'),
|
||||||
isModuleOrAlias: false,
|
|
||||||
}, {
|
}, {
|
||||||
id: expect.stringContaining('playwright_test_src_clashingNames1_tsx_ClashingName'),
|
id: expect.stringContaining('playwright_test_src_clashingNames1_tsx_ClashingName'),
|
||||||
remoteName: 'ClashingName',
|
remoteName: 'ClashingName',
|
||||||
importPath: expect.stringContaining('clashingNames1.tsx'),
|
importPath: expect.stringContaining('clashingNames1.tsx'),
|
||||||
isModuleOrAlias: false,
|
|
||||||
}, {
|
}, {
|
||||||
id: expect.stringContaining('playwright_test_src_clashingNames2_tsx_ClashingName'),
|
id: expect.stringContaining('playwright_test_src_clashingNames2_tsx_ClashingName'),
|
||||||
remoteName: 'ClashingName',
|
remoteName: 'ClashingName',
|
||||||
importPath: expect.stringContaining('clashingNames2.tsx'),
|
importPath: expect.stringContaining('clashingNames2.tsx'),
|
||||||
isModuleOrAlias: false,
|
|
||||||
}, {
|
}, {
|
||||||
id: expect.stringContaining('playwright_test_src_components_tsx_Component1'),
|
id: expect.stringContaining('playwright_test_src_components_tsx_Component1'),
|
||||||
remoteName: 'Component1',
|
remoteName: 'Component1',
|
||||||
importPath: expect.stringContaining('components.tsx'),
|
importPath: expect.stringContaining('components.tsx'),
|
||||||
isModuleOrAlias: false,
|
|
||||||
}, {
|
}, {
|
||||||
id: expect.stringContaining('playwright_test_src_components_tsx_Component2'),
|
id: expect.stringContaining('playwright_test_src_components_tsx_Component2'),
|
||||||
remoteName: 'Component2',
|
remoteName: 'Component2',
|
||||||
importPath: expect.stringContaining('components.tsx'),
|
importPath: expect.stringContaining('components.tsx'),
|
||||||
isModuleOrAlias: false,
|
|
||||||
}, {
|
}, {
|
||||||
id: expect.stringContaining('playwright_test_src_defaultExport_tsx'),
|
id: expect.stringContaining('playwright_test_src_defaultExport_tsx'),
|
||||||
importPath: expect.stringContaining('defaultExport.tsx'),
|
importPath: expect.stringContaining('defaultExport.tsx'),
|
||||||
isModuleOrAlias: false,
|
|
||||||
}]);
|
}]);
|
||||||
|
|
||||||
for (const [, value] of Object.entries(metainfo.deps))
|
for (const [, value] of Object.entries(metainfo.deps))
|
||||||
@ -464,7 +458,6 @@ test('should retain deps when test changes', async ({ runInlineTest }, testInfo)
|
|||||||
id: expect.stringContaining('playwright_test_src_button_tsx_Button'),
|
id: expect.stringContaining('playwright_test_src_button_tsx_Button'),
|
||||||
remoteName: 'Button',
|
remoteName: 'Button',
|
||||||
importPath: expect.stringContaining('button.tsx'),
|
importPath: expect.stringContaining('button.tsx'),
|
||||||
isModuleOrAlias: false,
|
|
||||||
}]);
|
}]);
|
||||||
|
|
||||||
for (const [, value] of Object.entries(metainfo.deps))
|
for (const [, value] of Object.entries(metainfo.deps))
|
||||||
@ -557,6 +550,31 @@ test('should pass imported images from test to component', async ({ runInlineTes
|
|||||||
expect(result.passed).toBe(1);
|
expect(result.passed).toBe(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('should import from file shortcuts (no .ts ext)', 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/tab.types.ts': `
|
||||||
|
export enum ALLOWED_TABS {
|
||||||
|
DRAW = "DRAW",
|
||||||
|
TYPE = "TYPE",
|
||||||
|
IMAGE = "IMAGE",
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
'src/image.test.tsx': `
|
||||||
|
import { test, expect } from '@playwright/experimental-ct-react';
|
||||||
|
import { ALLOWED_TABS } from './tab.types';
|
||||||
|
test('pass', async ({ mount }) => {
|
||||||
|
expect(ALLOWED_TABS.DRAW).toBe('DRAW');
|
||||||
|
});
|
||||||
|
`,
|
||||||
|
}, { workers: 1 });
|
||||||
|
|
||||||
|
expect(result.exitCode).toBe(0);
|
||||||
|
expect(result.passed).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
test('should pass dates, regex, urls and bigints', async ({ runInlineTest }) => {
|
test('should pass dates, regex, urls and bigints', async ({ runInlineTest }) => {
|
||||||
const result = await runInlineTest({
|
const result = await runInlineTest({
|
||||||
'playwright.config.ts': playwrightConfig,
|
'playwright.config.ts': playwrightConfig,
|
||||||
|
59
tests/playwright-test/playwright.ct-resolve.spec.ts
Normal file
59
tests/playwright-test/playwright.ct-resolve.spec.ts
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
/**
|
||||||
|
* 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 } from './playwright-test-fixtures';
|
||||||
|
|
||||||
|
test.describe.configure({ mode: 'parallel' });
|
||||||
|
|
||||||
|
const playwrightConfig = `
|
||||||
|
import { defineConfig } from '@playwright/experimental-ct-react';
|
||||||
|
import path from 'path';
|
||||||
|
export default defineConfig({
|
||||||
|
use: {
|
||||||
|
ctPort: ${3200 + (+process.env.TEST_PARALLEL_INDEX)},
|
||||||
|
},
|
||||||
|
projects: [{name: 'foo'}],
|
||||||
|
});
|
||||||
|
`;
|
||||||
|
|
||||||
|
test('should resolve component names using tsconfig', async ({ runInlineTest }, testInfo) => {
|
||||||
|
const result = await runInlineTest({
|
||||||
|
'playwright.config.ts': playwrightConfig,
|
||||||
|
'playwright/index.html': `<script type="module" src="./index.js"></script>`,
|
||||||
|
'playwright/index.js': ``,
|
||||||
|
'tsconfig.json': `{
|
||||||
|
"compilerOptions": {
|
||||||
|
"baseUrl": ".",
|
||||||
|
"paths": {
|
||||||
|
"@test/*": ["./src/*"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}`,
|
||||||
|
'src/button.tsx': `
|
||||||
|
export const Button = () => <button>Button</button>;
|
||||||
|
`,
|
||||||
|
'tests/button.spec.tsx': `
|
||||||
|
import { test, expect } from '@playwright/experimental-ct-react';
|
||||||
|
import { Button } from '@test/button';
|
||||||
|
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);
|
||||||
|
});
|
Loading…
x
Reference in New Issue
Block a user