chore: parse tsx tests (#10917)

This commit is contained in:
Pavel Feldman 2021-12-14 19:25:07 -08:00 committed by GitHub
parent 04e82ce71c
commit f579f9c806
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 220 additions and 165 deletions

6
package-lock.json generated
View File

@ -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",

View File

@ -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",

View File

@ -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);

View File

@ -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(<AutoChip header='title'>
Chip body
</AutoChip>);
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(<AutoChip header={title}>
Chip body
</AutoChip>);
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(<Chip header='Title'
setExpanded={(expanded: boolean) => expandedValues.push(expanded)}>
</Chip>);
await component.locator('text=Title').click();
expect(expandedValues).toEqual([true]);

View File

@ -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 header={title || 'Title'} expanded={expanded} setExpanded={setExpanded}>
Chip body
</Chip>;
};
registerComponent('ChipComponent', ChipComponent, {
viewport: { width: 500, height: 500 },
});
const ChipComponentWithFunctions: React.FC<{
setExpanded: (expanded: boolean) => void,
}> = ({ setExpanded }) => {
return <Chip header='Title' expanded={false} setExpanded={setExpanded}>
Chip body
</Chip>;
};
registerComponent('ChipComponentWithFunctions', ChipComponentWithFunctions, {
viewport: { width: 500, height: 500 },
});

View File

@ -39,3 +39,20 @@ export const Chip: React.FunctionComponent<{
{(!setExpanded || expanded) && <div className={'chip-body' + (noInsets ? ' chip-body-no-insets' : '')}>{children}</div>}
</div>;
};
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 <Chip
header={header}
expanded={expanded}
setExpanded={setExpanded}
noInsets={noInsets}
>
{children}
</Chip>;
};

View File

@ -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(<HeaderView stats={{
total: 100,
expected: 42,
unexpected: 31,
@ -28,8 +30,7 @@ test('should render counters', async ({ renderComponent }) => {
skipped: 10,
ok: false,
duration: 100000
};
const component = await renderComponent('HeaderView', { stats });
}} filterText='' setFilterText={() => {}}></HeaderView>);
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(<HeaderView
stats={{
total: 100,
expected: 42,
unexpected: 31,
flaky: 17,
skipped: 10,
ok: false,
duration: 100000
}}
filterText=''
setFilterText={(filterText: string) => filters.push(filterText)}>
</HeaderView>);
await component.locator('a', { hasText: 'All' }).click();
await component.locator('a', { hasText: 'Passed' }).click();
await expect(page).toHaveURL(/#\?q=s:passed/);

View File

@ -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 },
});

View File

@ -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 <Link href={`#?q=p:${value}`}>
<span className={'label label-color-' + (report.projectNames.indexOf(projectName) % 6)}>
<span className={'label label-color-' + (projectNames.indexOf(projectName) % 6)}>
{projectName}
</span>
</Link>;

View File

@ -80,5 +80,5 @@ const TestCaseViewLoader: React.FC<{
}
})();
}, [test, report, testId]);
return <TestCaseView report={report.json()} test={test}></TestCaseView>;
return <TestCaseView projectNames={report.json().projectNames} test={test}></TestCaseView>;
};

View File

@ -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(<TestCaseView projectNames={['chromium', 'webkit']} test={testCase}></TestCaseView>);
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();
});

View File

@ -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 <div className='test-case-column vbox'>
{test && <div className='test-case-path'>{test.path.join(' ')}</div>}
{test && <div className='test-case-title'>{test?.title}</div>}
{test && <div className='test-case-location'>{test.location.file}:{test.location.line}</div>}
{test && !!test.projectName && <ProjectLink report={report} projectName={test.projectName}></ProjectLink>}
{test && !!test.annotations.length && <Chip header='Annotations'>
{test && !!test.projectName && <ProjectLink projectNames={projectNames} projectName={test.projectName}></ProjectLink>}
{test && !!test.annotations.length && <AutoChip header='Annotations'>
{test.annotations.map(a => <div className='test-case-annotation'>
<span style={{ fontWeight: 'bold' }}>{a.type}</span>
{a.description && <span>: {a.description}</span>}
</div>)}
</Chip>}
</AutoChip>}
{test && <TabbedPane tabs={
test.results.map((result, index) => ({
id: String(index),

View File

@ -42,7 +42,7 @@ export const TestFileView: React.FC<{
<div key={`test-${test.testId}`} className={'test-file-test test-file-test-outcome-' + test.outcome}>
<span style={{ float: 'right' }}>{msToString(test.duration)}</span>
{report.projectNames.length > 1 && !!test.projectName &&
<span style={{ float: 'right' }}><ProjectLink report={report} projectName={test.projectName}></ProjectLink></span>}
<span style={{ float: 'right' }}><ProjectLink projectNames={report.projectNames} projectName={test.projectName}></ProjectLink></span>}
{statusIcon(test.outcome)}
<Link href={`#?testId=${test.testId}`} title={[...test.path, test.title].join(' ')}>
{[...test.path, test.title].join(' ')}

View File

@ -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 <div className='test-result'>
{result.error && <Chip header='Errors'>
{result.error && <AutoChip header='Errors'>
<ErrorMessage key='test-result-error-message' error={result.error}></ErrorMessage>
</Chip>}
{!!result.steps.length && <Chip header='Test Steps'>
</AutoChip>}
{!!result.steps.length && <AutoChip header='Test Steps'>
{result.steps.map((step, i) => <StepTreeItem key={`step-${i}`} step={step} depth={0}></StepTreeItem>)}
</Chip>}
</AutoChip>}
{expected && actual && <Chip header='Image mismatch'>
{expected && actual && <AutoChip header='Image mismatch'>
<ImageDiff actual={actual} expected={expected} diff={diff}></ImageDiff>
<AttachmentLink key={`expected`} attachment={expected}></AttachmentLink>
<AttachmentLink key={`actual`} attachment={actual}></AttachmentLink>
{diff && <AttachmentLink key={`diff`} attachment={diff}></AttachmentLink>}
</Chip>}
</AutoChip>}
{!!screenshots.length && <Chip header='Screenshots'>
{!!screenshots.length && <AutoChip header='Screenshots'>
{screenshots.map((a, i) => {
return <div key={`screenshot-${i}`}>
<img src={a.path} />
<AttachmentLink attachment={a}></AttachmentLink>
</div>;
})}
</Chip>}
</AutoChip>}
{!!traces.length && <Chip header='Traces'>
{!!traces.length && <AutoChip header='Traces'>
{traces.map((a, i) => <div key={`trace-${i}`}>
<a href={`trace/index.html?trace=${new URL(a.path!, window.location.href)}`}>
<img src={traceImage} style={{ width: 192, height: 117, marginLeft: 20 }} />
</a>
<AttachmentLink attachment={a}></AttachmentLink>
</div>)}
</Chip>}
</AutoChip>}
{!!videos.length && <Chip header='Videos'>
{!!videos.length && <AutoChip header='Videos'>
{videos.map((a, i) => <div key={`video-${i}`}>
<video controls>
<source src={a.path} type={a.contentType}/>
</video>
<AttachmentLink attachment={a}></AttachmentLink>
</div>)}
</Chip>}
</AutoChip>}
{!!otherAttachments.length && <Chip header='Attachments'>
{!!otherAttachments.length && <AutoChip header='Attachments'>
{otherAttachments.map((a, i) => <AttachmentLink key={`attachment-link-${i}`} attachment={a}></AttachmentLink>)}
</Chip>}
</AutoChip>}
</div>;
};

View File

@ -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;

View File

@ -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<Locator>;
render: (component: { type: string, props: Object }) => Promise<Locator>;
webpack: string;
};
export const test = baseTest.extend<TestFixtures>({
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: `<html>
@ -49,27 +48,23 @@ export const test = baseTest.extend<TestFixtures>({
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');
});
},

View File

@ -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']

View File

@ -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",

View File

@ -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);

View File

@ -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),
};

View File

@ -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));

View File

@ -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<string, string> = 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<A extends any[], R>(func: (location: Location, ...args: A) => R): (...args: A) => R {
@ -151,3 +159,20 @@ export function wrapFunctionWithLocation<A extends any[], R>(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
});`;
}