diff --git a/package-lock.json b/package-lock.json index 6923203ac2..6bf8ae384f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -590,7 +590,6 @@ "version": "7.14.5", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.14.5.tgz", "integrity": "sha512-ohuFIsOMXJnbOMRfX7/w7LocdR6R7whhuRD4ax8IipLcLPlZGJKkBxgHp++U4N/vKyU16/YDQr2f5seajD3jIw==", - "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.14.5" }, @@ -731,7 +730,6 @@ "version": "7.14.5", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.14.5.tgz", "integrity": "sha512-7RylxNeDnxc1OleDm0F5Q/BSL+whYRbOAR+bwgCxIr0L32v7UFh/pz1DLMZideAUxKT6eMoS2zQH6fyODLEi8Q==", - "dev": true, "dependencies": { "@babel/helper-annotate-as-pure": "^7.14.5", "@babel/helper-module-imports": "^7.14.5", @@ -9151,6 +9149,7 @@ "@babel/plugin-syntax-object-rest-spread": "^7.8.3", "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", "@babel/plugin-transform-modules-commonjs": "^7.14.5", + "@babel/plugin-transform-react-jsx": "^7.14.5", "@babel/preset-typescript": "^7.14.5", "babel-plugin-module-resolver": "^4.1.0", "colors": "^1.4.0", @@ -9574,7 +9573,6 @@ "version": "7.14.5", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.14.5.tgz", "integrity": "sha512-ohuFIsOMXJnbOMRfX7/w7LocdR6R7whhuRD4ax8IipLcLPlZGJKkBxgHp++U4N/vKyU16/YDQr2f5seajD3jIw==", - "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.14.5" } @@ -9667,7 +9665,6 @@ "version": "7.14.5", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.14.5.tgz", "integrity": "sha512-7RylxNeDnxc1OleDm0F5Q/BSL+whYRbOAR+bwgCxIr0L32v7UFh/pz1DLMZideAUxKT6eMoS2zQH6fyODLEi8Q==", - "dev": true, "requires": { "@babel/helper-annotate-as-pure": "^7.14.5", "@babel/helper-module-imports": "^7.14.5", @@ -9966,6 +9963,7 @@ "@babel/plugin-syntax-object-rest-spread": "^7.8.3", "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", "@babel/plugin-transform-modules-commonjs": "^7.14.5", + "@babel/plugin-transform-react-jsx": "^7.14.5", "@babel/preset-typescript": "^7.14.5", "babel-plugin-module-resolver": "^4.1.0", "colors": "^1.4.0", diff --git a/package.json b/package.json index 0867035423..7b44b7c630 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,7 @@ "wtest": "playwright test --config=tests/config/default.config.ts --project=webkit", "atest": "playwright test --config=tests/config/android.config.ts", "etest": "playwright test --config=tests/config/electron.config.ts", - "htest": "playwright test --config=packages/html-reporter", + "htest": "cross-env PW_COMPONENT_TESTING=1 playwright test --config=packages/html-reporter", "ttest": "node ./tests/playwright-test/stable-test-runner/node_modules/@playwright/test/cli test --config=tests/playwright-test/playwright-test.config.ts", "vtest": "cross-env PLAYWRIGHT_DOCKER=1 node ./tests/playwright-test/stable-test-runner/node_modules/@playwright/test/cli test --config=tests/playwright-test/playwright-test.config.ts", "test": "playwright test --config=tests/config/default.config.ts", diff --git a/packages/html-reporter/playwright.stories.tsx b/packages/html-reporter/playwright.components.tsx similarity index 63% rename from packages/html-reporter/playwright.stories.tsx rename to packages/html-reporter/playwright.components.tsx index 1e12dbcb6d..6d120d5ed4 100644 --- a/packages/html-reporter/playwright.stories.tsx +++ b/packages/html-reporter/playwright.components.tsx @@ -14,6 +14,13 @@ * limitations under the License. */ +import { AutoChip, Chip } from './src/chip'; +import { HeaderView } from './src/headerView'; +import { TestCaseView } from './src/testCaseView'; import './src/theme.css'; -import './src/chip.story.tsx'; -import './src/headerView.story.tsx'; +import { registerComponent } from './test/component'; + +registerComponent('HeaderView', HeaderView); +registerComponent('Chip', Chip); +registerComponent('TestCaseView', TestCaseView); +registerComponent('AutoChip', AutoChip); diff --git a/packages/html-reporter/src/chip.spec.ts b/packages/html-reporter/src/chip.spec.tsx similarity index 73% rename from packages/html-reporter/src/chip.spec.ts rename to packages/html-reporter/src/chip.spec.tsx index 5c276543fa..7b75d45cde 100644 --- a/packages/html-reporter/src/chip.spec.ts +++ b/packages/html-reporter/src/chip.spec.tsx @@ -14,12 +14,17 @@ * limitations under the License. */ +import React from 'react'; import { test, expect } from '../test/componentTest'; +import { Chip, AutoChip } from './chip'; test.use({ webpack: require.resolve('../webpack.config.js') }); +test.use({ viewport: { width: 500, height: 500 } }); -test('chip expand collapse', async ({ renderComponent }) => { - const component = await renderComponent('ChipComponent'); +test('chip expand collapse', async ({ render }) => { + const component = await render( + Chip body + ); await expect(component.locator('text=Chip body')).toBeVisible(); // expect(await component.screenshot()).toMatchSnapshot('expanded.png'); await component.locator('text=Title').click(); @@ -30,18 +35,20 @@ test('chip expand collapse', async ({ renderComponent }) => { // expect(await component.screenshot()).toMatchSnapshot('expanded.png'); }); -test('chip render long title', async ({ renderComponent }) => { +test('chip render long title', async ({ render }) => { const title = 'Extremely long title. '.repeat(10); - const component = await renderComponent('ChipComponent', { title }); + const component = await render( + Chip body + ); await expect(component).toContainText('Extremely long title.'); await expect(component.locator('text=Extremely long title.')).toHaveAttribute('title', title); }); -test('chip setExpanded is called', async ({ renderComponent }) => { +test('chip setExpanded is called', async ({ render }) => { const expandedValues: boolean[] = []; - const component = await renderComponent('ChipComponentWithFunctions', { - setExpanded: (expanded: boolean) => expandedValues.push(expanded) - }); + const component = await render( expandedValues.push(expanded)}> + ); await component.locator('text=Title').click(); expect(expandedValues).toEqual([true]); diff --git a/packages/html-reporter/src/chip.story.tsx b/packages/html-reporter/src/chip.story.tsx deleted file mode 100644 index b8fef79d65..0000000000 --- a/packages/html-reporter/src/chip.story.tsx +++ /dev/null @@ -1,42 +0,0 @@ -/** - * 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 React from 'react'; -import { Chip } from './chip'; -import { registerComponent } from '../test/component'; - -const ChipComponent: React.FC<{ - title?: string -}> = ({ title }) => { - const [expanded, setExpanded] = React.useState(true); - return - Chip body - ; -}; -registerComponent('ChipComponent', ChipComponent, { - viewport: { width: 500, height: 500 }, -}); - -const ChipComponentWithFunctions: React.FC<{ - setExpanded: (expanded: boolean) => void, -}> = ({ setExpanded }) => { - return - Chip body - ; -}; -registerComponent('ChipComponentWithFunctions', ChipComponentWithFunctions, { - viewport: { width: 500, height: 500 }, -}); diff --git a/packages/html-reporter/src/chip.tsx b/packages/html-reporter/src/chip.tsx index a6c1edffce..71f82a5632 100644 --- a/packages/html-reporter/src/chip.tsx +++ b/packages/html-reporter/src/chip.tsx @@ -39,3 +39,20 @@ export const Chip: React.FunctionComponent<{ {(!setExpanded || expanded) &&
{children}
} ; }; + +export const AutoChip: React.FC<{ + header: JSX.Element | string, + initialExpanded?: boolean, + noInsets?: boolean, + children?: any, +}> = ({ header, initialExpanded, noInsets, children }) => { + const [expanded, setExpanded] = React.useState(initialExpanded || initialExpanded === undefined); + return + {children} + ; +}; diff --git a/packages/html-reporter/src/headerView.spec.ts b/packages/html-reporter/src/headerView.spec.tsx similarity index 74% rename from packages/html-reporter/src/headerView.spec.ts rename to packages/html-reporter/src/headerView.spec.tsx index 1096461d93..39d7f276d2 100644 --- a/packages/html-reporter/src/headerView.spec.ts +++ b/packages/html-reporter/src/headerView.spec.tsx @@ -14,13 +14,15 @@ * limitations under the License. */ -import type { Stats } from '@playwright/test/src/reporters/html'; +import React from 'react'; import { test, expect } from '../test/componentTest'; +import { HeaderView } from './headerView'; test.use({ webpack: require.resolve('../webpack.config.js') }); +test.use({ viewport: { width: 720, height: 200 } }); -test('should render counters', async ({ renderComponent }) => { - const stats: Stats = { +test('should render counters', async ({ render }) => { + const component = await render( { skipped: 10, ok: false, duration: 100000 - }; - const component = await renderComponent('HeaderView', { stats }); + }} filterText='' setFilterText={() => {}}>); await expect(component.locator('a', { hasText: 'All' }).locator('.counter')).toHaveText('100'); await expect(component.locator('a', { hasText: 'Passed' }).locator('.counter')).toHaveText('42'); await expect(component.locator('a', { hasText: 'Failed' }).locator('.counter')).toHaveText('31'); @@ -37,21 +38,21 @@ test('should render counters', async ({ renderComponent }) => { await expect(component.locator('a', { hasText: 'Skipped' }).locator('.counter')).toHaveText('10'); }); -test('should toggle filters', async ({ page, renderComponent }) => { - const stats: Stats = { - total: 100, - expected: 42, - unexpected: 31, - flaky: 17, - skipped: 10, - ok: false, - duration: 100000 - }; +test('should toggle filters', async ({ page, render: render }) => { const filters: string[] = []; - const component = await renderComponent('HeaderView', { - stats, - setFilterText: (filterText: string) => filters.push(filterText) - }); + const component = await render( filters.push(filterText)}> + ); await component.locator('a', { hasText: 'All' }).click(); await component.locator('a', { hasText: 'Passed' }).click(); await expect(page).toHaveURL(/#\?q=s:passed/); diff --git a/packages/html-reporter/src/headerView.story.tsx b/packages/html-reporter/src/headerView.story.tsx deleted file mode 100644 index 6706ac6c68..0000000000 --- a/packages/html-reporter/src/headerView.story.tsx +++ /dev/null @@ -1,22 +0,0 @@ -/** - * 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 { registerComponent } from '../test/component'; -import { HeaderView } from './headerView'; - -registerComponent('HeaderView', HeaderView, { - viewport: { width: 720, height: 500 }, -}); diff --git a/packages/html-reporter/src/links.tsx b/packages/html-reporter/src/links.tsx index 5efcb67208..7e2c220589 100644 --- a/packages/html-reporter/src/links.tsx +++ b/packages/html-reporter/src/links.tsx @@ -14,7 +14,7 @@ limitations under the License. */ -import type { HTMLReport, TestAttachment } from '@playwright/test/src/reporters/html'; +import type { TestAttachment } from '@playwright/test/src/reporters/html'; import * as React from 'react'; import * as icons from './icons'; import { TreeItem } from './treeItem'; @@ -53,13 +53,13 @@ export const Link: React.FunctionComponent<{ }; export const ProjectLink: React.FunctionComponent<{ - report: HTMLReport, + projectNames: string[], projectName: string, -}> = ({ report, projectName }) => { +}> = ({ projectNames, projectName }) => { const encoded = encodeURIComponent(projectName); const value = projectName === encoded ? projectName : `"${encoded.replace(/%22/g, '%5C%22')}"`; return - + {projectName} ; diff --git a/packages/html-reporter/src/reportView.tsx b/packages/html-reporter/src/reportView.tsx index b0adffab69..6cdb20d432 100644 --- a/packages/html-reporter/src/reportView.tsx +++ b/packages/html-reporter/src/reportView.tsx @@ -80,5 +80,5 @@ const TestCaseViewLoader: React.FC<{ } })(); }, [test, report, testId]); - return ; + return ; }; diff --git a/packages/html-reporter/src/testCaseView.spec.tsx b/packages/html-reporter/src/testCaseView.spec.tsx new file mode 100644 index 0000000000..39ce0c57a3 --- /dev/null +++ b/packages/html-reporter/src/testCaseView.spec.tsx @@ -0,0 +1,73 @@ +/** + * Copyright (c) Microsoft Corporation. All rights reserved. + * + * 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 React from 'react'; +import { test, expect } from '../test/componentTest'; +import { TestCaseView } from './testCaseView'; +import type { TestCase, TestResult } from '../../playwright-test/src/reporters/html'; + +test.use({ webpack: require.resolve('../webpack.config.js') }); +test.use({ viewport: { width: 800, height: 600 } }); + +const result: TestResult = { + retry: 0, + startTime: new Date(0).toUTCString(), + duration: 100, + steps: [{ + title: 'Outer step', + startTime: new Date(100).toUTCString(), + duration: 10, + location: { file: 'test.spec.ts', line: 62, column: 0 }, + steps: [{ + title: 'Inner step', + startTime: new Date(200).toUTCString(), + duration: 10, + location: { file: 'test.spec.ts', line: 82, column: 0 }, + steps: [], + }], + }], + attachments: [], + status: 'passed', +}; + +const testCase: TestCase = { + testId: 'testid', + title: 'My test', + path: [], + projectName: 'chromium', + location: { file: 'test.spec.ts', line: 42, column: 0 }, + annotations: [ + { type: 'annotation', description: 'Annotation text' }, + { type: 'annotation', description: 'Another annotation text' }, + ], + outcome: 'expected', + duration: 10, + ok: true, + results: [result] +}; + +test('should render counters', async ({ render }) => { + const component = await render(); + await expect(component.locator('text=Annotation text').first()).toBeVisible(); + await component.locator('text=Annotations').click(); + await expect(component.locator('text=Annotation text')).not.toBeVisible(); + await expect(component.locator('text=Outer step')).toBeVisible(); + await expect(component.locator('text=Inner step')).not.toBeVisible(); + await component.locator('text=Outer step').click(); + await expect(component.locator('text=Inner step')).toBeVisible(); + await expect(component.locator('text=test.spec.ts:42')).toBeVisible(); + await expect(component.locator('text=My test')).toBeVisible(); +}); diff --git a/packages/html-reporter/src/testCaseView.tsx b/packages/html-reporter/src/testCaseView.tsx index 5d4205c97e..1ae2224d0c 100644 --- a/packages/html-reporter/src/testCaseView.tsx +++ b/packages/html-reporter/src/testCaseView.tsx @@ -14,10 +14,10 @@ limitations under the License. */ -import type { HTMLReport, TestCase } from '@playwright/test/src/reporters/html'; +import type { TestCase } from '@playwright/test/src/reporters/html'; import * as React from 'react'; import { TabbedPane } from './tabbedPane'; -import { Chip } from './chip'; +import { AutoChip } from './chip'; import './common.css'; import { ProjectLink } from './links'; import { statusIcon } from './statusIcon'; @@ -25,22 +25,22 @@ import './testCaseView.css'; import { TestResultView } from './testResultView'; export const TestCaseView: React.FC<{ - report: HTMLReport, + projectNames: string[], test: TestCase | undefined, -}> = ({ report, test }) => { +}> = ({ projectNames, test }) => { const [selectedResultIndex, setSelectedResultIndex] = React.useState(0); return
{test &&
{test.path.join(' › ')}
} {test &&
{test?.title}
} {test &&
{test.location.file}:{test.location.line}
} - {test && !!test.projectName && } - {test && !!test.annotations.length && + {test && !!test.projectName && } + {test && !!test.annotations.length && {test.annotations.map(a =>
{a.type} {a.description && : {a.description}}
)} -
} + } {test && ({ id: String(index), diff --git a/packages/html-reporter/src/testFileView.tsx b/packages/html-reporter/src/testFileView.tsx index cf55ca1450..b969a52deb 100644 --- a/packages/html-reporter/src/testFileView.tsx +++ b/packages/html-reporter/src/testFileView.tsx @@ -42,7 +42,7 @@ export const TestFileView: React.FC<{
{msToString(test.duration)} {report.projectNames.length > 1 && !!test.projectName && - } + } {statusIcon(test.outcome)} {[...test.path, test.title].join(' › ')} diff --git a/packages/html-reporter/src/testResultView.tsx b/packages/html-reporter/src/testResultView.tsx index f859cb2db6..dbf804d74e 100644 --- a/packages/html-reporter/src/testResultView.tsx +++ b/packages/html-reporter/src/testResultView.tsx @@ -20,7 +20,7 @@ import * as React from 'react'; import { TreeItem } from './treeItem'; import { TabbedPane } from './tabbedPane'; import { msToString } from './uiUtils'; -import { Chip } from './chip'; +import { AutoChip } from './chip'; import { traceImage } from './images'; import { AttachmentLink } from './links'; import { statusIcon } from './statusIcon'; @@ -51,50 +51,50 @@ export const TestResultView: React.FC<{ const actual = attachmentsMap.get('actual'); const diff = attachmentsMap.get('diff'); return
- {result.error && + {result.error && - } - {!!result.steps.length && + } + {!!result.steps.length && {result.steps.map((step, i) => )} - } + } - {expected && actual && + {expected && actual && {diff && } - } + } - {!!screenshots.length && + {!!screenshots.length && {screenshots.map((a, i) => { return
; })} -
} + } - {!!traces.length && + {!!traces.length && {traces.map((a, i) => )} - } + } - {!!videos.length && + {!!videos.length && {videos.map((a, i) =>
)} -
} + } - {!!otherAttachments.length && + {!!otherAttachments.length && {otherAttachments.map((a, i) => )} - } + }
; }; diff --git a/packages/html-reporter/test/component.js b/packages/html-reporter/test/component.js index faf4f65445..bd1b916b91 100644 --- a/packages/html-reporter/test/component.js +++ b/packages/html-reporter/test/component.js @@ -57,21 +57,15 @@ const Component = ({ style, children }) => { const registry = new Map(); -export const registerComponent = (name, component, options) => { - registry.set(name, { component, options }); +export const registerComponent = (name, component) => { + registry.set(name, component); }; function render(name, params) { - const entry = registry.get(name); + const component = registry.get(name); ReactDOM.render( - React.createElement(Component, null, React.createElement(entry.component, params || null)), + React.createElement(Component, null, React.createElement(component, params || null)), document.getElementById('root')); } -function options(name) { - const entry = registry.get(name); - return entry.options; -} - window.__playwright_render = render; -window.__playwright_options = options; diff --git a/packages/html-reporter/test/componentTest.ts b/packages/html-reporter/test/componentTest.ts index 7834ff2af1..bac8e2de4a 100644 --- a/packages/html-reporter/test/componentTest.ts +++ b/packages/html-reporter/test/componentTest.ts @@ -20,22 +20,21 @@ import { test as baseTest, Locator } from '@playwright/test'; declare global { interface Window { __playwright_render: (component: string, props: any) => void; - __playwright_options: (component: string) => { viewport: { width: number, height: number } }; } } type TestFixtures = { - renderComponent: (component: string, params?: any) => Promise; + render: (component: { type: string, props: Object }) => Promise; webpack: string; }; export const test = baseTest.extend({ webpack: '', - renderComponent: async ({ page, webpack }, use, testInfo) => { + render: async ({ page, webpack }, use) => { const webpackConfig = require(webpack); const outputPath = webpackConfig.output.path; const filename = webpackConfig.output.filename.replace('[name]', 'playwright'); - await use(async (component: string, optionalParams?: Object) => { + await use(async (component: { type: string, props: Object }) => { await page.route('http://component/index.html', route => { route.fulfill({ body: ` @@ -49,27 +48,23 @@ export const test = baseTest.extend({ await page.goto('http://component/index.html'); await page.addScriptTag({ path: path.resolve(__dirname, outputPath, filename) }); - const options = await page.evaluate((component: string) => { - return window.__playwright_options(component); - }, component); - await page.setViewportSize(options.viewport); - const params = { ...optionalParams }; - for (const [key, value] of Object.entries(params)) { + const props = { ...component.props }; + for (const [key, value] of Object.entries(props)) { if (typeof value === 'function') { const functionName = '__pw_func_' + key; await page.exposeFunction(functionName, value); - (params as any)[key] = functionName; + (props as any)[key] = functionName; } } await page.evaluate(v => { - const params = v.params; - for (const [key, value] of Object.entries(params)) { + const props = v.props; + for (const [key, value] of Object.entries(props)) { if (typeof value === 'string' && (value as string).startsWith('__pw_func_')) - (params as any)[key] = window[value]; + (props as any)[key] = (window as any)[value]; } - window.__playwright_render(v.component, params); - }, { component, params }); + window.__playwright_render(v.type, props); + }, { type: component.type, props }); return page.locator('#pw-root'); }); }, diff --git a/packages/html-reporter/webpack.config.js b/packages/html-reporter/webpack.config.js index f22a09a72c..e8e9cfab92 100644 --- a/packages/html-reporter/webpack.config.js +++ b/packages/html-reporter/webpack.config.js @@ -25,7 +25,7 @@ module.exports = { entry: { zip: require.resolve('@zip.js/zip.js/dist/zip-no-worker-inflate.min.js'), app: path.join(__dirname, 'src', 'index.tsx'), - playwright: path.join(__dirname, 'playwright.stories.tsx'), + playwright: path.join(__dirname, 'playwright.components.tsx'), }, resolve: { extensions: ['.ts', '.js', '.tsx', '.jsx'] diff --git a/packages/playwright-test/package.json b/packages/playwright-test/package.json index 6c62971d7f..30528659e2 100644 --- a/packages/playwright-test/package.json +++ b/packages/playwright-test/package.json @@ -43,6 +43,7 @@ "@babel/plugin-syntax-object-rest-spread": "^7.8.3", "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", "@babel/plugin-transform-modules-commonjs": "^7.14.5", + "@babel/plugin-transform-react-jsx": "^7.14.5", "@babel/preset-typescript": "^7.14.5", "babel-plugin-module-resolver": "^4.1.0", "colors": "^1.4.0", diff --git a/packages/playwright-test/src/experimentalLoader.ts b/packages/playwright-test/src/experimentalLoader.ts index 4b996ac429..ca16535a65 100644 --- a/packages/playwright-test/src/experimentalLoader.ts +++ b/packages/playwright-test/src/experimentalLoader.ts @@ -26,15 +26,15 @@ async function resolve(specifier: string, context: { parentURL: string }, defaul return defaultResolve(specifier, context, defaultResolve); let url = new URL(specifier, context.parentURL).toString(); url = url.substring('file://'.length); - if (fs.existsSync(url + '.ts')) - return defaultResolve(specifier + '.ts', context, defaultResolve); - if (fs.existsSync(url + '.js')) - return defaultResolve(specifier + '.js', context, defaultResolve); + for (const extension of ['.ts', '.js', '.tsx', '.jsx']) { + if (fs.existsSync(url + extension)) + return defaultResolve(specifier + extension, context, defaultResolve); + } return defaultResolve(specifier, context, defaultResolve); } async function load(url: string, context: any, defaultLoad: any) { - if (url.endsWith('.ts')) { + if (url.endsWith('.ts') || url.endsWith('.tsx')) { const filename = url.substring('file://'.length); const cwd = path.dirname(filename); let tsconfig = tsConfigCache.get(cwd); diff --git a/packages/playwright-test/src/loader.ts b/packages/playwright-test/src/loader.ts index b85c48763b..f344196e56 100644 --- a/packages/playwright-test/src/loader.ts +++ b/packages/playwright-test/src/loader.ts @@ -185,7 +185,7 @@ export class Loader { testDir, snapshotDir, testIgnore: takeFirst(this._configOverrides.testIgnore, projectConfig.testIgnore, this._config.testIgnore, []), - testMatch: takeFirst(this._configOverrides.testMatch, projectConfig.testMatch, this._config.testMatch, '**/?(*.)@(spec|test).@(ts|js|mjs)'), + testMatch: takeFirst(this._configOverrides.testMatch, projectConfig.testMatch, this._config.testMatch, '**/?(*.)@(spec|test).*'), timeout: takeFirst(this._configOverrides.timeout, projectConfig.timeout, this._config.timeout, 10000), use: mergeObjects(mergeObjects(this._config.use, projectConfig.use), this._configOverrides.use), }; diff --git a/packages/playwright-test/src/runner.ts b/packages/playwright-test/src/runner.ts index 6fdfb52223..d121c87f65 100644 --- a/packages/playwright-test/src/runner.ts +++ b/packages/playwright-test/src/runner.ts @@ -161,7 +161,8 @@ export class Runner { const allFiles = await collectFiles(project.config.testDir); const testMatch = createFileMatcher(project.config.testMatch); const testIgnore = createFileMatcher(project.config.testIgnore); - const testFileExtension = (file: string) => ['.js', '.ts', '.mjs'].includes(path.extname(file)); + const extensions = ['.js', '.ts', '.mjs', ...(process.env.PW_COMPONENT_TESTING ? ['.tsx', '.jsx'] : [])]; + const testFileExtension = (file: string) => extensions.includes(path.extname(file)); const testFiles = allFiles.filter(file => !testIgnore(file) && testMatch(file) && testFileFilter(file) && testFileExtension(file)); files.set(project, testFiles); testFiles.forEach(file => allTestFiles.add(file)); diff --git a/packages/playwright-test/src/transform.ts b/packages/playwright-test/src/transform.ts index aa20f6e629..29694fb430 100644 --- a/packages/playwright-test/src/transform.ts +++ b/packages/playwright-test/src/transform.ts @@ -24,7 +24,7 @@ import * as url from 'url'; import type { Location } from './types'; import { TsConfigLoaderResult } from './third_party/tsconfig-loader'; -const version = 4; +const version = 5; const cacheDir = process.env.PWTEST_CACHE_DIR || path.join(os.tmpdir(), 'playwright-transform-cache'); const sourceMaps: Map = new Map(); @@ -54,6 +54,8 @@ function calculateCachePath(content: string, filePath: string): string { } export function transformHook(code: string, filename: string, tsconfig: TsConfigLoaderResult, isModule = false): string { + if (isComponentImport(filename)) + return componentStub(); const cachePath = calculateCachePath(code, filename); const codePath = cachePath + '.js'; const sourceMapPath = cachePath + '.map'; @@ -65,7 +67,7 @@ export function transformHook(code: string, filename: string, tsconfig: TsConfig process.env.BROWSERSLIST_IGNORE_OLD_DATA = 'true'; const babel: typeof import('@babel/core') = require('@babel/core'); - const alias: { [key: string]: string | ((s: string[]) => string) } = {}; + const extensions = ['', '.js', '.ts', '.mjs', ...(process.env.PW_COMPONENT_TESTING ? ['.tsx', '.jsx'] : [])]; const alias: { [key: string]: string | ((s: string[]) => string) } = {}; for (const [key, values] of Object.entries(tsconfig.paths || {})) { const regexKey = '^' + key.replace('*', '.*'); alias[regexKey] = ([name]) => { @@ -73,8 +75,10 @@ export function transformHook(code: string, filename: string, tsconfig: TsConfig const relative = (key.endsWith('/*') ? value.substring(0, value.length - 1) + name.substring(key.length - 1) : value) .replace(/\//g, path.sep); const result = path.resolve(tsconfig.baseUrl || '', relative); - if (fs.existsSync(result) || fs.existsSync(result + '.js') || fs.existsSync(result + '.ts')) - return result; + for (const extension of extensions) { + if (fs.existsSync(result + extension)) + return result; + } } return name; }; @@ -96,6 +100,10 @@ export function transformHook(code: string, filename: string, tsconfig: TsConfig alias }], ]; + + if (process.env.PW_COMPONENT_TESTING) + plugins.unshift([require.resolve('@babel/plugin-transform-react-jsx')]); + if (!isModule) { plugins.push([require.resolve('@babel/plugin-transform-modules-commonjs')]); plugins.push([require.resolve('@babel/plugin-proposal-dynamic-import')]); @@ -125,7 +133,7 @@ export function transformHook(code: string, filename: string, tsconfig: TsConfig } export function installTransform(tsconfig: TsConfigLoaderResult): () => void { - return pirates.addHook((code: string, filename: string) => transformHook(code, filename, tsconfig), { exts: ['.ts'] }); + return pirates.addHook((code: string, filename: string) => transformHook(code, filename, tsconfig), { exts: ['.ts', '.tsx'] }); } export function wrapFunctionWithLocation(func: (location: Location, ...args: A) => R): (...args: A) => R { @@ -151,3 +159,20 @@ export function wrapFunctionWithLocation(func: (location: Lo return func(location, ...args); }; } + +// Experimental components support for internal testing. +function isComponentImport(filename: string): boolean { + if (!process.env.PW_COMPONENT_TESTING) + return false; + if (filename.endsWith('.tsx') && !filename.endsWith('spec.tsx') && !filename.endsWith('test.tsx')) + return true; + if (filename.endsWith('.jsx') && !filename.endsWith('spec.jsx') && !filename.endsWith('test.jsx')) + return true; + return false; +} + +function componentStub(): string { + return `module.exports = new Proxy({}, { + get: (obj, prop) => prop + });`; +}