diff --git a/packages/playwright-test/src/plugins/vitePlugin.ts b/packages/playwright-test/src/plugins/vitePlugin.ts index 884419b612..61c60d606c 100644 --- a/packages/playwright-test/src/plugins/vitePlugin.ts +++ b/packages/playwright-test/src/plugins/vitePlugin.ts @@ -24,9 +24,10 @@ import type { ComponentInfo } from '../tsxTransform'; import { collectComponentUsages, componentInfo } from '../tsxTransform'; import type { FullConfig } from '../types'; import { assert } from 'playwright-core/lib/utils'; +import type { AddressInfo } from 'net'; let previewServer: PreviewServer; -const VERSION = 2; +const VERSION = 3; type CtConfig = { ctPort?: number; @@ -49,7 +50,6 @@ export function createPlugin( const port = use.ctPort || 3100; const viteConfig: InlineConfig = use.ctViteConfig || {}; const relativeTemplateDir = use.ctTemplateDir || 'playwright'; - process.env.PLAYWRIGHT_VITE_COMPONENTS_BASE_URL = `http://localhost:${port}/${relativeTemplateDir}/index.html`; const rootDir = viteConfig.root || configDir; const templateDir = path.join(rootDir, relativeTemplateDir); @@ -113,6 +113,10 @@ export function createPlugin( if (hasNewTests || hasNewComponents || sourcesDirty) await fs.promises.writeFile(buildInfoFile, JSON.stringify(buildInfo, undefined, 2)); previewServer = await preview(viteConfig); + const isAddressInfo = (x: any): x is AddressInfo => x?.address; + const address = previewServer.httpServer.address(); + if (isAddressInfo(address)) + process.env.PLAYWRIGHT_VITE_COMPONENTS_BASE_URL = `http://localhost:${address.port}/${relativeTemplateDir}/index.html`; }, teardown: async () => { diff --git a/packages/playwright-test/src/tsxTransform.ts b/packages/playwright-test/src/tsxTransform.ts index 391f645101..77a48f1353 100644 --- a/packages/playwright-test/src/tsxTransform.ts +++ b/packages/playwright-test/src/tsxTransform.ts @@ -154,7 +154,7 @@ export type ComponentInfo = { export function componentInfo(specifier: T.ImportSpecifier | T.ImportDefaultSpecifier, importSource: string, filename: string): ComponentInfo { const isModuleOrAlias = !importSource.startsWith('.'); - const importPath = isModuleOrAlias ? importSource : path.resolve(path.dirname(filename), importSource); + const importPath = isModuleOrAlias ? importSource : require.resolve(path.resolve(path.dirname(filename), importSource)); const prefix = importPath.replace(/[^\w_\d]/g, '_'); const pathInfo = { importPath, isModuleOrAlias }; diff --git a/tests/playwright-test/playwright.ct-build.spec.ts b/tests/playwright-test/playwright.ct-build.spec.ts new file mode 100644 index 0000000000..7c78201722 --- /dev/null +++ b/tests/playwright-test/playwright.ct-build.spec.ts @@ -0,0 +1,270 @@ +/** + * 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, stripAnsi } from './playwright-test-fixtures'; +import fs from 'fs'; +import path from 'path'; + +test.describe.configure({ mode: 'parallel' }); + +test('should work with the empty component list', async ({ runInlineTest }, testInfo) => { + const result = await runInlineTest({ + 'playwright/index.html': ``, + 'playwright/index.js': ``, + + 'a.test.ts': ` + //@no-header + import { test, expect } from '@playwright/experimental-ct-react'; + test('pass', async ({ mount }) => {}); + `, + }, { workers: 1 }); + expect(result.exitCode).toBe(0); + expect(result.passed).toBe(1); + const output = stripAnsi(result.output); + expect(output).toContain('transforming...'); + expect(output).toContain(path.join('playwright', '.cache', 'playwright', 'index.html')); + + const metainfo = JSON.parse(fs.readFileSync(testInfo.outputPath('playwright/.cache/metainfo.json'), 'utf-8')); + expect(metainfo.version).toEqual(expect.any(Number)); + expect(Object.entries(metainfo.tests)).toHaveLength(1); + expect(Object.entries(metainfo.sources)).toHaveLength(9); +}); + +test('should extract component list', async ({ runInlineTest }, testInfo) => { + const result = await runInlineTest({ + 'playwright/index.html': ``, + 'playwright/index.ts': ``, + + 'src/button.tsx': ` + export const Button = () => ; + `, + + 'src/components.tsx': ` + export const Component1 = () =>
Component 1
; + export const Component2 = () =>
Component 2
; + `, + + 'src/defaultExport.tsx': ` + export default () =>
Default export
; + `, + + 'src/clashingNames1.tsx': ` + export const ClashingName = () =>
Clashing name 1
; + `, + + 'src/clashingNames2.tsx': ` + export const ClashingName = () =>
Clashing name 2
; + `, + + 'src/one-import.spec.tsx': ` + //@no-header + import { test, expect } from '@playwright/experimental-ct-react'; + import { Button } from './button'; + test('pass', async ({ mount }) => { + const component = await mount(); + await expect(component).toHaveText('Button'); + }); + `, + + 'src/named-imports.spec.tsx': ` + //@no-header + import { test, expect } from '@playwright/experimental-ct-react'; + import { Component1, Component2 } from './components'; + + test('pass 1', async ({ mount }) => { + const component = await mount(); + await expect(component).toHaveText('Component 1'); + }); + + test('pass 2', async ({ mount }) => { + const component = await mount(); + await expect(component).toHaveText('Component 2'); + }); + `, + + 'src/default-import.spec.tsx': ` + //@no-header + import { test, expect } from '@playwright/experimental-ct-react'; + import DefaultComponent from './defaultExport'; + + test('named', async ({ mount }) => { + const component = await mount(); + await expect(component).toHaveText('Default export'); + }); + `, + + 'src/clashing-imports.spec.tsx': ` + //@no-header + import { test, expect } from '@playwright/experimental-ct-react'; + + import DefaultComponent from './defaultExport.tsx'; + import { ClashingName as CN1 } from './clashingNames1'; + import { ClashingName as CN2 } from './clashingNames2'; + + test('named', async ({ mount }) => { + const component = await mount(); + await expect(component).toHaveText('Clashing name 1'); + }); + + test('pass 2', async ({ mount }) => { + const component = await mount(); + await expect(component).toHaveText('Clashing name 2'); + }); + `, + }, { workers: 1 }); + expect(result.exitCode).toBe(0); + + const metainfo = JSON.parse(fs.readFileSync(testInfo.outputPath('playwright/.cache/metainfo.json'), 'utf-8')); + metainfo.components.sort((a, b) => { + return (a.importPath + '/' + a.importedName).localeCompare(b.importPath + '/' + b.importedName); + }); + + expect(metainfo.components).toEqual([{ + fullName: expect.stringContaining('playwright_test_src_button_tsx_Button'), + importedName: 'Button', + importPath: expect.stringContaining('button.tsx'), + isModuleOrAlias: false + }, { + fullName: expect.stringContaining('playwright_test_src_clashingNames1_tsx_ClashingName'), + importedName: 'ClashingName', + importPath: expect.stringContaining('clashingNames1.tsx'), + isModuleOrAlias: false + }, { + fullName: expect.stringContaining('playwright_test_src_clashingNames2_tsx_ClashingName'), + importedName: 'ClashingName', + importPath: expect.stringContaining('clashingNames2.tsx'), + isModuleOrAlias: false + }, { + fullName: expect.stringContaining('playwright_test_src_components_tsx_Component1'), + importedName: 'Component1', + importPath: expect.stringContaining('components.tsx'), + isModuleOrAlias: false + }, { + fullName: expect.stringContaining('playwright_test_src_components_tsx_Component2'), + importedName: 'Component2', + importPath: expect.stringContaining('components.tsx'), + isModuleOrAlias: false + }, { + fullName: expect.stringContaining('playwright_test_src_defaultExport_tsx'), + importPath: expect.stringContaining('defaultExport.tsx'), + isModuleOrAlias: false + }]); + + for (const [file, test] of Object.entries(metainfo.tests)) { + if (file.endsWith('clashing-imports.spec.tsx')) { + expect(test).toEqual({ + timestamp: expect.any(Number), + components: [ + expect.stringContaining('clashingNames1_tsx_ClashingName'), + expect.stringContaining('clashingNames2_tsx_ClashingName'), + ] + }); + } + if (file.endsWith('default-import.spec.tsx')) { + expect(test).toEqual({ + timestamp: expect.any(Number), + components: [ + expect.stringContaining('defaultExport_tsx'), + ] + }); + } + if (file.endsWith('named-imports.spec.tsx')) { + expect(test).toEqual({ + timestamp: expect.any(Number), + components: [ + expect.stringContaining('components_tsx_Component1'), + expect.stringContaining('components_tsx_Component2'), + ] + }); + } + if (file.endsWith('one-import.spec.tsx')) { + expect(test).toEqual({ + timestamp: expect.any(Number), + components: [ + expect.stringContaining('button_tsx_Button'), + ] + }); + } + } +}); + +test('should cache build', async ({ runInlineTest }, testInfo) => { + await test.step('original test', async () => { + const result = await runInlineTest({ + 'playwright/index.html': ``, + 'playwright/index.ts': ``, + + 'src/button.tsx': ` + export const Button = () => ; + `, + + 'src/button.test.tsx': ` + //@no-header + import { test, expect } from '@playwright/experimental-ct-react'; + import { Button } from './button.tsx'; + + 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); + const output = stripAnsi(result.output); + expect(output, 'should rebuild bundle').toContain('modules transformed'); + }); + + await test.step('re-run same test', async () => { + const result = await runInlineTest({}, { workers: 1 }); + expect(result.exitCode).toBe(0); + expect(result.passed).toBe(1); + const output = stripAnsi(result.output); + expect(output, 'should not rebuild bundle').not.toContain('modules transformed'); + }); + + await test.step('modify test', async () => { + const result = await runInlineTest({ + 'src/button.test.tsx': ` + //@no-header + import { test, expect } from '@playwright/experimental-ct-react'; + import { Button } from './button.tsx'; + + test('pass updated', async ({ mount }) => { + const component = await mount(); + await expect(component).toHaveText('Button 2', { timeout: 200 }); + }); + `, + }, { workers: 1 }); + expect(result.exitCode).toBe(1); + expect(result.passed).toBe(0); + const output = stripAnsi(result.output); + expect(output, 'should not rebuild bundle').not.toContain('modules transformed'); + }); + + await test.step('modify source', async () => { + const result = await runInlineTest({ + 'src/button.tsx': ` + export const Button = () => ; + `, + }, { workers: 1 }); + expect(result.exitCode).toBe(0); + expect(result.passed).toBe(1); + const output = stripAnsi(result.output); + expect(output, 'should rebuild bundle').toContain('modules transformed'); + }); +}); diff --git a/tests/playwright-test/playwright.ct-react.spec.ts b/tests/playwright-test/playwright.ct-react.spec.ts new file mode 100644 index 0000000000..6523e3a293 --- /dev/null +++ b/tests/playwright-test/playwright.ct-react.spec.ts @@ -0,0 +1,91 @@ +/** + * 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('should work with TSX', async ({ runInlineTest }, testInfo) => { + const result = await runInlineTest({ + 'playwright/index.html': ``, + 'playwright/index.ts': ``, + 'src/button.tsx': ` + export const Button = () => ; + `, + + 'src/button.test.tsx': ` + //@no-header + import { test, expect } from '@playwright/experimental-ct-react'; + import { Button } from './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); +}); + +test.fixme('should work with JSX', async ({ runInlineTest }, testInfo) => { + const result = await runInlineTest({ + 'playwright/index.html': ``, + 'playwright/index.js': ``, + + 'src/button.jsx': ` + export const Button = () => ; + `, + + 'src/button.test.jsx': ` + //@no-header + import { test, expect } from '@playwright/experimental-ct-react'; + import { Button } from './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); +}); + +test.fixme('should work with JS in JSX', async ({ runInlineTest }, testInfo) => { + const result = await runInlineTest({ + 'playwright/index.html': ``, + 'playwright/index.js': ``, + + 'src/button.js': ` + export const Button = () => ; + `, + + 'src/button.test.jsx': ` + //@no-header + import { test, expect } from '@playwright/experimental-ct-react'; + import { Button } from './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); +});