mirror of
https://github.com/microsoft/playwright.git
synced 2024-09-20 00:41:16 +03:00
chore: parse tsx tests (#10917)
This commit is contained in:
parent
04e82ce71c
commit
f579f9c806
6
package-lock.json
generated
6
package-lock.json
generated
@ -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",
|
||||
|
@ -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",
|
||||
|
@ -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);
|
@ -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]);
|
@ -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 },
|
||||
});
|
@ -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>;
|
||||
};
|
||||
|
@ -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/);
|
@ -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 },
|
||||
});
|
@ -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>;
|
||||
|
@ -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>;
|
||||
};
|
||||
|
73
packages/html-reporter/src/testCaseView.spec.tsx
Normal file
73
packages/html-reporter/src/testCaseView.spec.tsx
Normal 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();
|
||||
});
|
@ -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),
|
||||
|
@ -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(' › ')}
|
||||
|
@ -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>;
|
||||
};
|
||||
|
||||
|
@ -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;
|
||||
|
@ -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');
|
||||
});
|
||||
},
|
||||
|
@ -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']
|
||||
|
@ -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",
|
||||
|
@ -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);
|
||||
|
@ -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),
|
||||
};
|
||||
|
@ -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));
|
||||
|
@ -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
|
||||
});`;
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user