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
+ });`;
+}