diff --git a/packages/playwright-ct-core/src/tsxTransform.ts b/packages/playwright-ct-core/src/tsxTransform.ts index 6ef051a6b4..a0f35bc4a6 100644 --- a/packages/playwright-ct-core/src/tsxTransform.ts +++ b/packages/playwright-ct-core/src/tsxTransform.ts @@ -17,8 +17,7 @@ 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'; +import { resolveHook, setTransformData } from 'playwright/lib/transform/transform'; const t: typeof T = types; let jsxComponentNames: Set; @@ -72,15 +71,15 @@ export default declare((api: BabelAPI) => { const importNode = p.node; if (!t.isStringLiteral(importNode.source)) return; - - const ext = path.extname(importNode.source.value); + const importPath = resolveImportSource(importNode.source.value, this.filename!); + const ext = path.extname(importPath); // 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!); + const { localName, info } = importInfo(importPath, specifier, this.filename!); importInfos.set(localName, info); } p.skip(); @@ -93,7 +92,7 @@ export default declare((api: BabelAPI) => { for (const specifier of importNode.specifiers) { if (t.isImportNamespaceSpecifier(specifier)) continue; - const { localName, info } = importInfo(importNode, specifier, this.filename!); + const { localName, info } = importInfo(importPath, specifier, this.filename!); if (jsxComponentNames.has(localName)) { importInfos.set(localName, info); ++importCount; @@ -144,25 +143,22 @@ function collectJsxComponentUsages(node: T.Node): Set { export type ImportInfo = { id: string; - isModuleOrAlias: boolean; importPath: string; remoteName: string | undefined; }; -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('.'); +function resolveImportSource(importSource: string, filename: string): string { const unresolvedImportPath = path.resolve(path.dirname(filename), importSource); - // Support following notations for Button.tsx: - // - import { Button } from './Button.js' - via resolveImportSpecifierExtension - // - import { Button } from './Button' - via require.resolve - const importPath = isModuleOrAlias ? importSource : resolveImportSpecifierExtension(unresolvedImportPath) || require.resolve(unresolvedImportPath); + const importPath = resolveHook(filename, importSource) || unresolvedImportPath; + return importPath; +} + +function importInfo(importPath: string, specifier: T.ImportSpecifier | T.ImportDefaultSpecifier, filename: string): { localName: string, info: ImportInfo } { const idPrefix = importPath.replace(/[^\w_\d]/g, '_'); const result: ImportInfo = { id: idPrefix, importPath, - isModuleOrAlias, remoteName: undefined, }; diff --git a/packages/playwright-ct-core/src/viteUtils.ts b/packages/playwright-ct-core/src/viteUtils.ts index 249537b4a7..752f75ef32 100644 --- a/packages/playwright-ct-core/src/viteUtils.ts +++ b/packages/playwright-ct-core/src/viteUtils.ts @@ -143,7 +143,7 @@ export async function populateComponentsFromTests(componentRegistry: ComponentRe for (const importInfo of importList) componentRegistry.set(importInfo.id, importInfo); 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); 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'});`); } diff --git a/tests/playwright-test/playwright.ct-build.spec.ts b/tests/playwright-test/playwright.ct-build.spec.ts index 6304b44a12..efc75878bd 100644 --- a/tests/playwright-test/playwright.ct-build.spec.ts +++ b/tests/playwright-test/playwright.ct-build.spec.ts @@ -143,31 +143,25 @@ test('should extract component list', async ({ runInlineTest }, testInfo) => { id: expect.stringContaining('playwright_test_src_button_tsx_Button'), remoteName: 'Button', importPath: expect.stringContaining('button.tsx'), - isModuleOrAlias: false, }, { id: expect.stringContaining('playwright_test_src_clashingNames1_tsx_ClashingName'), remoteName: 'ClashingName', importPath: expect.stringContaining('clashingNames1.tsx'), - isModuleOrAlias: false, }, { id: expect.stringContaining('playwright_test_src_clashingNames2_tsx_ClashingName'), remoteName: 'ClashingName', importPath: expect.stringContaining('clashingNames2.tsx'), - isModuleOrAlias: false, }, { id: expect.stringContaining('playwright_test_src_components_tsx_Component1'), remoteName: 'Component1', importPath: expect.stringContaining('components.tsx'), - isModuleOrAlias: false, }, { id: expect.stringContaining('playwright_test_src_components_tsx_Component2'), remoteName: 'Component2', importPath: expect.stringContaining('components.tsx'), - isModuleOrAlias: false, }, { id: expect.stringContaining('playwright_test_src_defaultExport_tsx'), importPath: expect.stringContaining('defaultExport.tsx'), - isModuleOrAlias: false, }]); 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'), remoteName: 'Button', importPath: expect.stringContaining('button.tsx'), - isModuleOrAlias: false, }]); 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); }); +test('should import from file shortcuts (no .ts ext)', async ({ runInlineTest }, testInfo) => { + const result = await runInlineTest({ + 'playwright.config.ts': playwrightConfig, + 'playwright/index.html': ``, + '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 }) => { const result = await runInlineTest({ 'playwright.config.ts': playwrightConfig, diff --git a/tests/playwright-test/playwright.ct-resolve.spec.ts b/tests/playwright-test/playwright.ct-resolve.spec.ts new file mode 100644 index 0000000000..bebcd93fc1 --- /dev/null +++ b/tests/playwright-test/playwright.ct-resolve.spec.ts @@ -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': ``, + 'playwright/index.js': ``, + 'tsconfig.json': `{ + "compilerOptions": { + "baseUrl": ".", + "paths": { + "@test/*": ["./src/*"], + }, + }, + }`, + 'src/button.tsx': ` + export const 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(); + await expect(component).toHaveText('Button'); + }); + `, + }, { workers: 1 }); + expect(result.exitCode).toBe(0); + expect(result.passed).toBe(1); +});