Merge branch 'main' into test-filter
This commit is contained in:
commit
a457d169eb
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -35,3 +35,4 @@ test-results
|
||||||
.cache/
|
.cache/
|
||||||
.eslintcache
|
.eslintcache
|
||||||
playwright.env
|
playwright.env
|
||||||
|
firefox
|
||||||
|
|
|
||||||
|
|
@ -587,6 +587,22 @@ export default defineConfig({
|
||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## property: TestConfig.tsconfig
|
||||||
|
* since: v1.49
|
||||||
|
- type: ?<[string]>
|
||||||
|
|
||||||
|
Path to a single `tsconfig` applicable to all imported files. By default, `tsconfig` for each imported file is looked up separately. Note that `tsconfig` property has no effect while the configuration file or any of its dependencies are loaded. Ignored when `--tsconfig` command line option is specified.
|
||||||
|
|
||||||
|
**Usage**
|
||||||
|
|
||||||
|
```js title="playwright.config.ts"
|
||||||
|
import { defineConfig } from '@playwright/test';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
tsconfig: './tsconfig.test.json',
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
## property: TestConfig.updateSnapshots
|
## property: TestConfig.updateSnapshots
|
||||||
* since: v1.10
|
* since: v1.10
|
||||||
- type: ?<[UpdateSnapshots]<"all"|"none"|"missing">>
|
- type: ?<[UpdateSnapshots]<"all"|"none"|"missing">>
|
||||||
|
|
|
||||||
|
|
@ -90,6 +90,16 @@ Alternatively, you can specify a single tsconfig file to use in the command line
|
||||||
npx playwright test --tsconfig=tsconfig.test.json
|
npx playwright test --tsconfig=tsconfig.test.json
|
||||||
```
|
```
|
||||||
|
|
||||||
|
You can specify a single tsconfig file in the config file, that will be used for loading test files, reporters, etc. However, it will not be used while loading the playwright config itself or any files imported from it.
|
||||||
|
|
||||||
|
```js title="playwright.config.ts"
|
||||||
|
import { defineConfig } from '@playwright/test';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
tsconfig: './tsconfig.test.json',
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
## Manually compile tests with TypeScript
|
## Manually compile tests with TypeScript
|
||||||
|
|
||||||
Sometimes, Playwright Test will not be able to transform your TypeScript code correctly, for example when you are using experimental or very recent features of TypeScript, usually configured in `tsconfig.json`.
|
Sometimes, Playwright Test will not be able to transform your TypeScript code correctly, for example when you are using experimental or very recent features of TypeScript, usually configured in `tsconfig.json`.
|
||||||
|
|
|
||||||
|
|
@ -14,9 +14,8 @@
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
.test-error-message {
|
.test-error-view {
|
||||||
white-space: pre;
|
white-space: pre;
|
||||||
font-family: monospace;
|
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
flex: none;
|
flex: none;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
|
@ -26,3 +25,7 @@
|
||||||
line-height: initial;
|
line-height: initial;
|
||||||
margin-bottom: 6px;
|
margin-bottom: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.test-error-text {
|
||||||
|
font-family: monospace;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,20 +17,38 @@
|
||||||
import ansi2html from 'ansi-to-html';
|
import ansi2html from 'ansi-to-html';
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import './testErrorView.css';
|
import './testErrorView.css';
|
||||||
|
import type { ImageDiff } from '@web/shared/imageDiffView';
|
||||||
|
import { ImageDiffView } from '@web/shared/imageDiffView';
|
||||||
|
|
||||||
export const TestErrorView: React.FC<{
|
export const TestErrorView: React.FC<{
|
||||||
error: string;
|
error: string;
|
||||||
}> = ({ error }) => {
|
}> = ({ error }) => {
|
||||||
const html = React.useMemo(() => {
|
const html = React.useMemo(() => ansiErrorToHtml(error), [error]);
|
||||||
|
return <div className='test-error-view test-error-text' dangerouslySetInnerHTML={{ __html: html || '' }}></div>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const TestScreenshotErrorView: React.FC<{
|
||||||
|
errorPrefix?: string,
|
||||||
|
diff: ImageDiff,
|
||||||
|
errorSuffix?: string,
|
||||||
|
}> = ({ errorPrefix, diff, errorSuffix }) => {
|
||||||
|
const prefixHtml = React.useMemo(() => ansiErrorToHtml(errorPrefix), [errorPrefix]);
|
||||||
|
const suffixHtml = React.useMemo(() => ansiErrorToHtml(errorSuffix), [errorSuffix]);
|
||||||
|
return <div data-testid='test-screenshot-error-view' className='test-error-view'>
|
||||||
|
<div dangerouslySetInnerHTML={{ __html: prefixHtml || '' }} className='test-error-text' style={{ marginBottom: 20 }}></div>
|
||||||
|
<ImageDiffView key='image-diff' diff={diff} hideDetails={true}></ImageDiffView>
|
||||||
|
<div data-testid='error-suffix' dangerouslySetInnerHTML={{ __html: suffixHtml || '' }} className='test-error-text'></div>
|
||||||
|
</div>;
|
||||||
|
};
|
||||||
|
|
||||||
|
function ansiErrorToHtml(text?: string): string {
|
||||||
const config: any = {
|
const config: any = {
|
||||||
bg: 'var(--color-canvas-subtle)',
|
bg: 'var(--color-canvas-subtle)',
|
||||||
fg: 'var(--color-fg-default)',
|
fg: 'var(--color-fg-default)',
|
||||||
};
|
};
|
||||||
config.colors = ansiColors;
|
config.colors = ansiColors;
|
||||||
return new ansi2html(config).toHtml(escapeHTML(error));
|
return new ansi2html(config).toHtml(escapeHTML(text || ''));
|
||||||
}, [error]);
|
}
|
||||||
return <div className='test-error-message' dangerouslySetInnerHTML={{ __html: html || '' }}></div>;
|
|
||||||
};
|
|
||||||
|
|
||||||
const ansiColors = {
|
const ansiColors = {
|
||||||
0: '#000',
|
0: '#000',
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,7 @@ import { AttachmentLink, generateTraceUrl } from './links';
|
||||||
import { statusIcon } from './statusIcon';
|
import { statusIcon } from './statusIcon';
|
||||||
import type { ImageDiff } from '@web/shared/imageDiffView';
|
import type { ImageDiff } from '@web/shared/imageDiffView';
|
||||||
import { ImageDiffView } from '@web/shared/imageDiffView';
|
import { ImageDiffView } from '@web/shared/imageDiffView';
|
||||||
import { TestErrorView } from './testErrorView';
|
import { TestErrorView, TestScreenshotErrorView } from './testErrorView';
|
||||||
import './testResultView.css';
|
import './testResultView.css';
|
||||||
|
|
||||||
function groupImageDiffs(screenshots: Set<TestAttachment>): ImageDiff[] {
|
function groupImageDiffs(screenshots: Set<TestAttachment>): ImageDiff[] {
|
||||||
|
|
@ -67,7 +67,7 @@ export const TestResultView: React.FC<{
|
||||||
anchor: 'video' | 'diff' | '',
|
anchor: 'video' | 'diff' | '',
|
||||||
}> = ({ result, anchor }) => {
|
}> = ({ result, anchor }) => {
|
||||||
|
|
||||||
const { screenshots, videos, traces, otherAttachments, diffs, htmls } = React.useMemo(() => {
|
const { screenshots, videos, traces, otherAttachments, diffs, errors, htmls } = React.useMemo(() => {
|
||||||
const attachments = result?.attachments || [];
|
const attachments = result?.attachments || [];
|
||||||
const screenshots = new Set(attachments.filter(a => a.contentType.startsWith('image/')));
|
const screenshots = new Set(attachments.filter(a => a.contentType.startsWith('image/')));
|
||||||
const videos = attachments.filter(a => a.name === 'video');
|
const videos = attachments.filter(a => a.name === 'video');
|
||||||
|
|
@ -76,7 +76,8 @@ export const TestResultView: React.FC<{
|
||||||
const otherAttachments = new Set<TestAttachment>(attachments);
|
const otherAttachments = new Set<TestAttachment>(attachments);
|
||||||
[...screenshots, ...videos, ...traces, ...htmls].forEach(a => otherAttachments.delete(a));
|
[...screenshots, ...videos, ...traces, ...htmls].forEach(a => otherAttachments.delete(a));
|
||||||
const diffs = groupImageDiffs(screenshots);
|
const diffs = groupImageDiffs(screenshots);
|
||||||
return { screenshots: [...screenshots], videos, traces, otherAttachments, diffs, htmls };
|
const errors = classifyErrors(result.errors, diffs);
|
||||||
|
return { screenshots: [...screenshots], videos, traces, otherAttachments, diffs, errors, htmls };
|
||||||
}, [result]);
|
}, [result]);
|
||||||
|
|
||||||
const videoRef = React.useRef<HTMLDivElement>(null);
|
const videoRef = React.useRef<HTMLDivElement>(null);
|
||||||
|
|
@ -94,15 +95,19 @@ export const TestResultView: React.FC<{
|
||||||
}, [scrolled, anchor, setScrolled, videoRef]);
|
}, [scrolled, anchor, setScrolled, videoRef]);
|
||||||
|
|
||||||
return <div className='test-result'>
|
return <div className='test-result'>
|
||||||
{!!result.errors.length && <AutoChip header='Errors'>
|
{!!errors.length && <AutoChip header='Errors'>
|
||||||
{result.errors.map((error, index) => <TestErrorView key={'test-result-error-message-' + index} error={error}></TestErrorView>)}
|
{errors.map((error, index) => {
|
||||||
|
if (error.type === 'screenshot')
|
||||||
|
return <TestScreenshotErrorView key={'test-result-error-message-' + index} errorPrefix={error.errorPrefix} diff={error.diff!} errorSuffix={error.errorSuffix}></TestScreenshotErrorView>;
|
||||||
|
return <TestErrorView key={'test-result-error-message-' + index} error={error.error!}></TestErrorView>;
|
||||||
|
})}
|
||||||
</AutoChip>}
|
</AutoChip>}
|
||||||
{!!result.steps.length && <AutoChip header='Test Steps'>
|
{!!result.steps.length && <AutoChip header='Test Steps'>
|
||||||
{result.steps.map((step, i) => <StepTreeItem key={`step-${i}`} step={step} depth={0}></StepTreeItem>)}
|
{result.steps.map((step, i) => <StepTreeItem key={`step-${i}`} step={step} depth={0}></StepTreeItem>)}
|
||||||
</AutoChip>}
|
</AutoChip>}
|
||||||
|
|
||||||
{diffs.map((diff, index) =>
|
{diffs.map((diff, index) =>
|
||||||
<AutoChip key={`diff-${index}`} header={`Image mismatch: ${diff.name}`} targetRef={imageDiffRef}>
|
<AutoChip key={`diff-${index}`} dataTestId='test-results-image-diff' header={`Image mismatch: ${diff.name}`} targetRef={imageDiffRef}>
|
||||||
<ImageDiffView key='image-diff' diff={diff}></ImageDiffView>
|
<ImageDiffView key='image-diff' diff={diff}></ImageDiffView>
|
||||||
</AutoChip>
|
</AutoChip>
|
||||||
)}
|
)}
|
||||||
|
|
@ -145,6 +150,29 @@ export const TestResultView: React.FC<{
|
||||||
</div>;
|
</div>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function classifyErrors(testErrors: string[], diffs: ImageDiff[]) {
|
||||||
|
return testErrors.map(error => {
|
||||||
|
if (error.includes('Screenshot comparison failed:')) {
|
||||||
|
const matchingDiff = diffs.find(diff => {
|
||||||
|
const attachmentName = diff.actual?.attachment.name;
|
||||||
|
return attachmentName && error.includes(attachmentName);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (matchingDiff) {
|
||||||
|
const lines = error.split('\n');
|
||||||
|
const index = lines.findIndex(line => /Expected:|Previous:|Received:/.test(line));
|
||||||
|
const errorPrefix = index !== -1 ? lines.slice(0, index).join('\n') : lines[0];
|
||||||
|
|
||||||
|
const diffIndex = lines.findIndex(line => / +Diff:/.test(line));
|
||||||
|
const errorSuffix = diffIndex !== -1 ? lines.slice(diffIndex + 2).join('\n') : lines.slice(1).join('\n');
|
||||||
|
|
||||||
|
return { type: 'screenshot', diff: matchingDiff, errorPrefix, errorSuffix };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { type: 'regular', error };
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const StepTreeItem: React.FC<{
|
const StepTreeItem: React.FC<{
|
||||||
step: TestStep;
|
step: TestStep;
|
||||||
depth: number,
|
depth: number,
|
||||||
|
|
|
||||||
|
|
@ -9,9 +9,9 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "chromium-tip-of-tree",
|
"name": "chromium-tip-of-tree",
|
||||||
"revision": "1267",
|
"revision": "1268",
|
||||||
"installByDefault": false,
|
"installByDefault": false,
|
||||||
"browserVersion": "131.0.6764.0"
|
"browserVersion": "131.0.6768.0"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "firefox",
|
"name": "firefox",
|
||||||
|
|
@ -27,7 +27,7 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "webkit",
|
"name": "webkit",
|
||||||
"revision": "2084",
|
"revision": "2090",
|
||||||
"installByDefault": true,
|
"installByDefault": true,
|
||||||
"revisionOverrides": {
|
"revisionOverrides": {
|
||||||
"mac10.14": "1446",
|
"mac10.14": "1446",
|
||||||
|
|
|
||||||
|
|
@ -43,7 +43,7 @@ import type { TimeoutOptions } from '../common/types';
|
||||||
import { isInvalidSelectorError } from '../utils/isomorphic/selectorParser';
|
import { isInvalidSelectorError } from '../utils/isomorphic/selectorParser';
|
||||||
import { parseEvaluationResultValue, source } from './isomorphic/utilityScriptSerializers';
|
import { parseEvaluationResultValue, source } from './isomorphic/utilityScriptSerializers';
|
||||||
import type { SerializedValue } from './isomorphic/utilityScriptSerializers';
|
import type { SerializedValue } from './isomorphic/utilityScriptSerializers';
|
||||||
import { TargetClosedError } from './errors';
|
import { TargetClosedError, TimeoutError } from './errors';
|
||||||
import { asLocator } from '../utils';
|
import { asLocator } from '../utils';
|
||||||
import { helper } from './helper';
|
import { helper } from './helper';
|
||||||
|
|
||||||
|
|
@ -662,7 +662,7 @@ export class Page extends SdkObject {
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (areEqualScreenshots(actual, options.expected, previous)) {
|
if (areEqualScreenshots(actual, options.expected, undefined)) {
|
||||||
progress.log(`screenshot matched expectation`);
|
progress.log(`screenshot matched expectation`);
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
@ -672,10 +672,13 @@ export class Page extends SdkObject {
|
||||||
// A: We want user to receive a friendly diff between actual and expected/previous.
|
// A: We want user to receive a friendly diff between actual and expected/previous.
|
||||||
if (js.isJavaScriptErrorInEvaluate(e) || isInvalidSelectorError(e))
|
if (js.isJavaScriptErrorInEvaluate(e) || isInvalidSelectorError(e))
|
||||||
throw e;
|
throw e;
|
||||||
|
let errorMessage = e.message;
|
||||||
|
if (e instanceof TimeoutError && intermediateResult?.previous)
|
||||||
|
errorMessage = `Failed to take two consecutive stable screenshots. ${e.message}`;
|
||||||
return {
|
return {
|
||||||
log: e.message ? [...metadata.log, e.message] : metadata.log,
|
log: e.message ? [...metadata.log, e.message] : metadata.log,
|
||||||
...intermediateResult,
|
...intermediateResult,
|
||||||
errorMessage: e.message,
|
errorMessage,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6510,7 +6510,7 @@ the top of the viewport and Y increases as it proceeds towards the bottom of the
|
||||||
/**
|
/**
|
||||||
* List of settings able to be overridden by WebInspector.
|
* List of settings able to be overridden by WebInspector.
|
||||||
*/
|
*/
|
||||||
export type Setting = "PrivateClickMeasurementDebugModeEnabled"|"AuthorAndUserStylesEnabled"|"ICECandidateFilteringEnabled"|"ITPDebugModeEnabled"|"ImagesEnabled"|"MediaCaptureRequiresSecureConnection"|"MockCaptureDevicesEnabled"|"NeedsSiteSpecificQuirks"|"ScriptEnabled"|"ShowDebugBorders"|"ShowRepaintCounter"|"WebSecurityEnabled"|"DeviceOrientationEventEnabled"|"SpeechRecognitionEnabled"|"PointerLockEnabled"|"NotificationsEnabled"|"FullScreenEnabled"|"InputTypeMonthEnabled"|"InputTypeWeekEnabled";
|
export type Setting = "PrivateClickMeasurementDebugModeEnabled"|"AuthorAndUserStylesEnabled"|"ICECandidateFilteringEnabled"|"ITPDebugModeEnabled"|"ImagesEnabled"|"MediaCaptureRequiresSecureConnection"|"MockCaptureDevicesEnabled"|"NeedsSiteSpecificQuirks"|"ScriptEnabled"|"ShowDebugBorders"|"ShowRepaintCounter"|"WebSecurityEnabled"|"DeviceOrientationEventEnabled"|"SpeechRecognitionEnabled"|"PointerLockEnabled"|"NotificationsEnabled"|"FullScreenEnabled"|"InputTypeMonthEnabled"|"InputTypeWeekEnabled"|"FixedBackgroundsPaintRelativeToDocument";
|
||||||
/**
|
/**
|
||||||
* A user preference that can be overriden by Web Inspector, like an accessibility preference.
|
* A user preference that can be overriden by Web Inspector, like an accessibility preference.
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -231,6 +231,7 @@ export class WKPage implements PageDelegate {
|
||||||
promises.push(session.send('Page.overrideSetting', { setting: 'PointerLockEnabled', value: !contextOptions.isMobile }));
|
promises.push(session.send('Page.overrideSetting', { setting: 'PointerLockEnabled', value: !contextOptions.isMobile }));
|
||||||
promises.push(session.send('Page.overrideSetting', { setting: 'InputTypeMonthEnabled', value: contextOptions.isMobile }));
|
promises.push(session.send('Page.overrideSetting', { setting: 'InputTypeMonthEnabled', value: contextOptions.isMobile }));
|
||||||
promises.push(session.send('Page.overrideSetting', { setting: 'InputTypeWeekEnabled', value: contextOptions.isMobile }));
|
promises.push(session.send('Page.overrideSetting', { setting: 'InputTypeWeekEnabled', value: contextOptions.isMobile }));
|
||||||
|
promises.push(session.send('Page.overrideSetting', { setting: 'FixedBackgroundsPaintRelativeToDocument', value: contextOptions.isMobile }));
|
||||||
await Promise.all(promises);
|
await Promise.all(promises);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -45,6 +45,7 @@ export class FullConfigInternal {
|
||||||
readonly webServers: NonNullable<FullConfig['webServer']>[];
|
readonly webServers: NonNullable<FullConfig['webServer']>[];
|
||||||
readonly plugins: TestRunnerPluginRegistration[];
|
readonly plugins: TestRunnerPluginRegistration[];
|
||||||
readonly projects: FullProjectInternal[] = [];
|
readonly projects: FullProjectInternal[] = [];
|
||||||
|
readonly singleTSConfigPath?: string;
|
||||||
cliArgs: string[] = [];
|
cliArgs: string[] = [];
|
||||||
cliGrep: string | undefined;
|
cliGrep: string | undefined;
|
||||||
cliGrepInvert: string | undefined;
|
cliGrepInvert: string | undefined;
|
||||||
|
|
@ -69,6 +70,7 @@ export class FullConfigInternal {
|
||||||
this.configCLIOverrides = configCLIOverrides;
|
this.configCLIOverrides = configCLIOverrides;
|
||||||
const privateConfiguration = (userConfig as any)['@playwright/test'];
|
const privateConfiguration = (userConfig as any)['@playwright/test'];
|
||||||
this.plugins = (privateConfiguration?.plugins || []).map((p: any) => ({ factory: p }));
|
this.plugins = (privateConfiguration?.plugins || []).map((p: any) => ({ factory: p }));
|
||||||
|
this.singleTSConfigPath = pathResolve(configDir, userConfig.tsconfig);
|
||||||
|
|
||||||
this.config = {
|
this.config = {
|
||||||
configFile: resolvedConfigFile,
|
configFile: resolvedConfigFile,
|
||||||
|
|
|
||||||
|
|
@ -118,6 +118,8 @@ export async function loadConfig(location: ConfigLocation, overrides?: ConfigCLI
|
||||||
const babelPlugins = (userConfig as any)['@playwright/test']?.babelPlugins || [];
|
const babelPlugins = (userConfig as any)['@playwright/test']?.babelPlugins || [];
|
||||||
const external = userConfig.build?.external || [];
|
const external = userConfig.build?.external || [];
|
||||||
setTransformConfig({ babelPlugins, external });
|
setTransformConfig({ babelPlugins, external });
|
||||||
|
if (!overrides?.tsconfig)
|
||||||
|
setSingleTSConfig(fullConfig?.singleTSConfigPath);
|
||||||
|
|
||||||
// 4. Send transform options to ESM loader.
|
// 4. Send transform options to ESM loader.
|
||||||
await configureESMLoaderTransformConfig();
|
await configureESMLoaderTransformConfig();
|
||||||
|
|
|
||||||
|
|
@ -77,5 +77,6 @@ export async function configureESMLoader() {
|
||||||
export async function configureESMLoaderTransformConfig() {
|
export async function configureESMLoaderTransformConfig() {
|
||||||
if (!loaderChannel)
|
if (!loaderChannel)
|
||||||
return;
|
return;
|
||||||
|
await loaderChannel.send('setSingleTSConfig', { tsconfig: singleTSConfig() });
|
||||||
await loaderChannel.send('setTransformConfig', { config: transformConfig() });
|
await loaderChannel.send('setTransformConfig', { config: transformConfig() });
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -423,7 +423,7 @@ export async function toHaveScreenshot(
|
||||||
// - regular matcher (i.e. not a `.not`)
|
// - regular matcher (i.e. not a `.not`)
|
||||||
// - perhaps an 'all' flag to update non-matching screenshots
|
// - perhaps an 'all' flag to update non-matching screenshots
|
||||||
expectScreenshotOptions.expected = await fs.promises.readFile(helper.expectedPath);
|
expectScreenshotOptions.expected = await fs.promises.readFile(helper.expectedPath);
|
||||||
const { actual, diff, errorMessage, log } = await page._expectScreenshot(expectScreenshotOptions);
|
const { actual, previous, diff, errorMessage, log } = await page._expectScreenshot(expectScreenshotOptions);
|
||||||
|
|
||||||
if (!errorMessage)
|
if (!errorMessage)
|
||||||
return helper.handleMatching();
|
return helper.handleMatching();
|
||||||
|
|
@ -436,7 +436,7 @@ export async function toHaveScreenshot(
|
||||||
return helper.createMatcherResult(helper.expectedPath + ' running with --update-snapshots, writing actual.', true);
|
return helper.createMatcherResult(helper.expectedPath + ' running with --update-snapshots, writing actual.', true);
|
||||||
}
|
}
|
||||||
|
|
||||||
return helper.handleDifferent(actual, expectScreenshotOptions.expected, undefined, diff, errorMessage, log);
|
return helper.handleDifferent(actual, expectScreenshotOptions.expected, previous, diff, errorMessage, log);
|
||||||
}
|
}
|
||||||
|
|
||||||
function writeFileSync(aPath: string, content: Buffer | string) {
|
function writeFileSync(aPath: string, content: Buffer | string) {
|
||||||
|
|
|
||||||
|
|
@ -302,10 +302,11 @@ export class TestServerDispatcher implements TestServerInterface {
|
||||||
preserveOutputDir: true,
|
preserveOutputDir: true,
|
||||||
reporter: params.reporters ? params.reporters.map(r => [r]) : undefined,
|
reporter: params.reporters ? params.reporters.map(r => [r]) : undefined,
|
||||||
use: {
|
use: {
|
||||||
...(this._configCLIOverrides.use || {}),
|
...this._configCLIOverrides.use,
|
||||||
trace: params.trace === 'on' ? { mode: 'on', sources: false, _live: true } : (params.trace === 'off' ? 'off' : undefined),
|
...(params.trace === 'on' ? { trace: { mode: 'on', sources: false, _live: true } } : {}),
|
||||||
video: params.video === 'on' ? 'on' : (params.video === 'off' ? 'off' : undefined),
|
...(params.trace === 'off' ? { trace: 'off' } : {}),
|
||||||
headless: params.headed ? false : undefined,
|
...(params.video === 'on' || params.video === 'off' ? { video: params.video } : {}),
|
||||||
|
...(params.headed !== undefined ? { headless: !params.headed } : {}),
|
||||||
_optionContextReuseMode: params.reuseContext ? 'when-possible' : undefined,
|
_optionContextReuseMode: params.reuseContext ? 'when-possible' : undefined,
|
||||||
_optionConnectOptions: params.connectWsEndpoint ? { wsEndpoint: params.connectWsEndpoint } : undefined,
|
_optionConnectOptions: params.connectWsEndpoint ? { wsEndpoint: params.connectWsEndpoint } : undefined,
|
||||||
},
|
},
|
||||||
|
|
|
||||||
19
packages/playwright/types/test.d.ts
vendored
19
packages/playwright/types/test.d.ts
vendored
|
|
@ -1684,6 +1684,25 @@ interface TestConfig<TestArgs = {}, WorkerArgs = {}> {
|
||||||
*/
|
*/
|
||||||
timeout?: number;
|
timeout?: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Path to a single `tsconfig` applicable to all imported files. By default, `tsconfig` for each imported file is
|
||||||
|
* looked up separately. Note that `tsconfig` property has no effect while the configuration file or any of its
|
||||||
|
* dependencies are loaded. Ignored when `--tsconfig` command line option is specified.
|
||||||
|
*
|
||||||
|
* **Usage**
|
||||||
|
*
|
||||||
|
* ```js
|
||||||
|
* // playwright.config.ts
|
||||||
|
* import { defineConfig } from '@playwright/test';
|
||||||
|
*
|
||||||
|
* export default defineConfig({
|
||||||
|
* tsconfig: './tsconfig.test.json',
|
||||||
|
* });
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
tsconfig?: string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Whether to update expected snapshots with the actual results produced by the test run. Defaults to `'missing'`.
|
* Whether to update expected snapshots with the actual results produced by the test run. Defaults to `'missing'`.
|
||||||
* - `'all'` - All tests that are executed will update snapshots that did not match. Matching snapshots will not be
|
* - `'all'` - All tests that are executed will update snapshots that did not match. Matching snapshots will not be
|
||||||
|
|
|
||||||
|
|
@ -61,11 +61,13 @@ const checkerboardStyle: React.CSSProperties = {
|
||||||
export const ImageDiffView: React.FC<{
|
export const ImageDiffView: React.FC<{
|
||||||
diff: ImageDiff,
|
diff: ImageDiff,
|
||||||
noTargetBlank?: boolean,
|
noTargetBlank?: boolean,
|
||||||
}> = ({ diff, noTargetBlank }) => {
|
hideDetails?: boolean,
|
||||||
|
}> = ({ diff, noTargetBlank, hideDetails }) => {
|
||||||
const [mode, setMode] = React.useState<'diff' | 'actual' | 'expected' | 'slider' | 'sxs'>(diff.diff ? 'diff' : 'actual');
|
const [mode, setMode] = React.useState<'diff' | 'actual' | 'expected' | 'slider' | 'sxs'>(diff.diff ? 'diff' : 'actual');
|
||||||
const [showSxsDiff, setShowSxsDiff] = React.useState<boolean>(false);
|
const [showSxsDiff, setShowSxsDiff] = React.useState<boolean>(false);
|
||||||
|
|
||||||
const [expectedImage, setExpectedImage] = React.useState<HTMLImageElement | null>(null);
|
const [expectedImage, setExpectedImage] = React.useState<HTMLImageElement | null>(null);
|
||||||
|
const [expectedImageTitle, setExpectedImageTitle] = React.useState<string>('Expected');
|
||||||
const [actualImage, setActualImage] = React.useState<HTMLImageElement | null>(null);
|
const [actualImage, setActualImage] = React.useState<HTMLImageElement | null>(null);
|
||||||
const [diffImage, setDiffImage] = React.useState<HTMLImageElement | null>(null);
|
const [diffImage, setDiffImage] = React.useState<HTMLImageElement | null>(null);
|
||||||
const [measure, ref] = useMeasure<HTMLDivElement>();
|
const [measure, ref] = useMeasure<HTMLDivElement>();
|
||||||
|
|
@ -73,6 +75,7 @@ export const ImageDiffView: React.FC<{
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
(async () => {
|
(async () => {
|
||||||
setExpectedImage(await loadImage(diff.expected?.attachment.path));
|
setExpectedImage(await loadImage(diff.expected?.attachment.path));
|
||||||
|
setExpectedImageTitle(diff.expected?.title || 'Expected');
|
||||||
setActualImage(await loadImage(diff.actual?.attachment.path));
|
setActualImage(await loadImage(diff.actual?.attachment.path));
|
||||||
setDiffImage(await loadImage(diff.diff?.attachment.path));
|
setDiffImage(await loadImage(diff.diff?.attachment.path));
|
||||||
})();
|
})();
|
||||||
|
|
@ -98,31 +101,31 @@ export const ImageDiffView: React.FC<{
|
||||||
<div data-testid='test-result-image-mismatch-tabs' style={{ display: 'flex', margin: '10px 0 20px' }}>
|
<div data-testid='test-result-image-mismatch-tabs' style={{ display: 'flex', margin: '10px 0 20px' }}>
|
||||||
{diff.diff && <div style={{ ...modeStyle, fontWeight: mode === 'diff' ? 600 : 'initial' }} onClick={() => setMode('diff')}>Diff</div>}
|
{diff.diff && <div style={{ ...modeStyle, fontWeight: mode === 'diff' ? 600 : 'initial' }} onClick={() => setMode('diff')}>Diff</div>}
|
||||||
<div style={{ ...modeStyle, fontWeight: mode === 'actual' ? 600 : 'initial' }} onClick={() => setMode('actual')}>Actual</div>
|
<div style={{ ...modeStyle, fontWeight: mode === 'actual' ? 600 : 'initial' }} onClick={() => setMode('actual')}>Actual</div>
|
||||||
<div style={{ ...modeStyle, fontWeight: mode === 'expected' ? 600 : 'initial' }} onClick={() => setMode('expected')}>Expected</div>
|
<div style={{ ...modeStyle, fontWeight: mode === 'expected' ? 600 : 'initial' }} onClick={() => setMode('expected')}>{expectedImageTitle}</div>
|
||||||
<div style={{ ...modeStyle, fontWeight: mode === 'sxs' ? 600 : 'initial' }} onClick={() => setMode('sxs')}>Side by side</div>
|
<div style={{ ...modeStyle, fontWeight: mode === 'sxs' ? 600 : 'initial' }} onClick={() => setMode('sxs')}>Side by side</div>
|
||||||
<div style={{ ...modeStyle, fontWeight: mode === 'slider' ? 600 : 'initial' }} onClick={() => setMode('slider')}>Slider</div>
|
<div style={{ ...modeStyle, fontWeight: mode === 'slider' ? 600 : 'initial' }} onClick={() => setMode('slider')}>Slider</div>
|
||||||
</div>
|
</div>
|
||||||
<div style={{ display: 'flex', justifyContent: 'center', flex: 'auto', minHeight: fitHeight + 60 }}>
|
<div style={{ display: 'flex', justifyContent: 'center', flex: 'auto', minHeight: fitHeight + 60 }}>
|
||||||
{diff.diff && mode === 'diff' && <ImageWithSize image={diffImage} alt='Diff' canvasWidth={fitWidth} canvasHeight={fitHeight} scale={scale}/>}
|
{diff.diff && mode === 'diff' && <ImageWithSize image={diffImage} alt='Diff' hideSize={hideDetails} canvasWidth={fitWidth} canvasHeight={fitHeight} scale={scale}/>}
|
||||||
{diff.diff && mode === 'actual' && <ImageWithSize image={actualImage} alt='Actual' canvasWidth={fitWidth} canvasHeight={fitHeight} scale={scale}/>}
|
{diff.diff && mode === 'actual' && <ImageWithSize image={actualImage} alt='Actual' hideSize={hideDetails} canvasWidth={fitWidth} canvasHeight={fitHeight} scale={scale}/>}
|
||||||
{diff.diff && mode === 'expected' && <ImageWithSize image={expectedImage} alt='Expected' canvasWidth={fitWidth} canvasHeight={fitHeight} scale={scale}/>}
|
{diff.diff && mode === 'expected' && <ImageWithSize image={expectedImage} alt={expectedImageTitle} hideSize={hideDetails} canvasWidth={fitWidth} canvasHeight={fitHeight} scale={scale}/>}
|
||||||
{diff.diff && mode === 'slider' && <ImageDiffSlider expectedImage={expectedImage} actualImage={actualImage} canvasWidth={fitWidth} canvasHeight={fitHeight} scale={scale} />}
|
{diff.diff && mode === 'slider' && <ImageDiffSlider expectedImage={expectedImage} actualImage={actualImage} hideSize={hideDetails} canvasWidth={fitWidth} canvasHeight={fitHeight} scale={scale} expectedTitle={expectedImageTitle} />}
|
||||||
{diff.diff && mode === 'sxs' && <div style={{ display: 'flex' }}>
|
{diff.diff && mode === 'sxs' && <div style={{ display: 'flex' }}>
|
||||||
<ImageWithSize image={expectedImage} title='Expected' canvasWidth={sxsScale * imageWidth} canvasHeight={sxsScale * imageHeight} scale={sxsScale} />
|
<ImageWithSize image={expectedImage} title={expectedImageTitle} hideSize={hideDetails} canvasWidth={sxsScale * imageWidth} canvasHeight={sxsScale * imageHeight} scale={sxsScale} />
|
||||||
<ImageWithSize image={showSxsDiff ? diffImage : actualImage} title={showSxsDiff ? 'Diff' : 'Actual'} onClick={() => setShowSxsDiff(!showSxsDiff)} canvasWidth={sxsScale * imageWidth} canvasHeight={sxsScale * imageHeight} scale={sxsScale} />
|
<ImageWithSize image={showSxsDiff ? diffImage : actualImage} title={showSxsDiff ? 'Diff' : 'Actual'} onClick={() => setShowSxsDiff(!showSxsDiff)} hideSize={hideDetails} canvasWidth={sxsScale * imageWidth} canvasHeight={sxsScale * imageHeight} scale={sxsScale} />
|
||||||
</div>}
|
</div>}
|
||||||
{!diff.diff && mode === 'actual' && <ImageWithSize image={actualImage} title='Actual' canvasWidth={fitWidth} canvasHeight={fitHeight} scale={scale}/>}
|
{!diff.diff && mode === 'actual' && <ImageWithSize image={actualImage} title='Actual' hideSize={hideDetails} canvasWidth={fitWidth} canvasHeight={fitHeight} scale={scale}/>}
|
||||||
{!diff.diff && mode === 'expected' && <ImageWithSize image={expectedImage} title='Expected' canvasWidth={fitWidth} canvasHeight={fitHeight} scale={scale}/>}
|
{!diff.diff && mode === 'expected' && <ImageWithSize image={expectedImage} title={expectedImageTitle} hideSize={hideDetails} canvasWidth={fitWidth} canvasHeight={fitHeight} scale={scale}/>}
|
||||||
{!diff.diff && mode === 'sxs' && <div style={{ display: 'flex' }}>
|
{!diff.diff && mode === 'sxs' && <div style={{ display: 'flex' }}>
|
||||||
<ImageWithSize image={expectedImage} title='Expected' canvasWidth={sxsScale * imageWidth} canvasHeight={sxsScale * imageHeight} scale={sxsScale} />
|
<ImageWithSize image={expectedImage} title={expectedImageTitle} canvasWidth={sxsScale * imageWidth} canvasHeight={sxsScale * imageHeight} scale={sxsScale} />
|
||||||
<ImageWithSize image={actualImage} title='Actual' canvasWidth={sxsScale * imageWidth} canvasHeight={sxsScale * imageHeight} scale={sxsScale} />
|
<ImageWithSize image={actualImage} title='Actual' canvasWidth={sxsScale * imageWidth} canvasHeight={sxsScale * imageHeight} scale={sxsScale} />
|
||||||
</div>}
|
</div>}
|
||||||
</div>
|
</div>
|
||||||
<div style={{ alignSelf: 'start', lineHeight: '18px', marginLeft: '15px' }}>
|
{!hideDetails && <div style={{ alignSelf: 'start', lineHeight: '18px', marginLeft: '15px' }}>
|
||||||
<div>{diff.diff && <a target='_blank' href={diff.diff.attachment.path} rel='noreferrer'>{diff.diff.attachment.name}</a>}</div>
|
<div>{diff.diff && <a target='_blank' href={diff.diff.attachment.path} rel='noreferrer'>{diff.diff.attachment.name}</a>}</div>
|
||||||
<div><a target={noTargetBlank ? '' : '_blank'} href={diff.actual!.attachment.path} rel='noreferrer'>{diff.actual!.attachment.name}</a></div>
|
<div><a target={noTargetBlank ? '' : '_blank'} href={diff.actual!.attachment.path} rel='noreferrer'>{diff.actual!.attachment.name}</a></div>
|
||||||
<div><a target={noTargetBlank ? '' : '_blank'} href={diff.expected!.attachment.path} rel='noreferrer'>{diff.expected!.attachment.name}</a></div>
|
<div><a target={noTargetBlank ? '' : '_blank'} href={diff.expected!.attachment.path} rel='noreferrer'>{diff.expected!.attachment.name}</a></div>
|
||||||
</div>
|
</div>}
|
||||||
</>}
|
</>}
|
||||||
</div>;
|
</div>;
|
||||||
};
|
};
|
||||||
|
|
@ -133,7 +136,9 @@ export const ImageDiffSlider: React.FC<{
|
||||||
canvasWidth: number,
|
canvasWidth: number,
|
||||||
canvasHeight: number,
|
canvasHeight: number,
|
||||||
scale: number,
|
scale: number,
|
||||||
}> = ({ expectedImage, actualImage, canvasWidth, canvasHeight, scale }) => {
|
expectedTitle: string,
|
||||||
|
hideSize?: boolean,
|
||||||
|
}> = ({ expectedImage, actualImage, canvasWidth, canvasHeight, scale, expectedTitle, hideSize }) => {
|
||||||
const absoluteStyle: React.CSSProperties = {
|
const absoluteStyle: React.CSSProperties = {
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
top: 0,
|
top: 0,
|
||||||
|
|
@ -144,7 +149,7 @@ export const ImageDiffSlider: React.FC<{
|
||||||
const sameSize = expectedImage.naturalWidth === actualImage.naturalWidth && expectedImage.naturalHeight === actualImage.naturalHeight;
|
const sameSize = expectedImage.naturalWidth === actualImage.naturalWidth && expectedImage.naturalHeight === actualImage.naturalHeight;
|
||||||
|
|
||||||
return <div style={{ flex: 'none', display: 'flex', alignItems: 'center', flexDirection: 'column', userSelect: 'none' }}>
|
return <div style={{ flex: 'none', display: 'flex', alignItems: 'center', flexDirection: 'column', userSelect: 'none' }}>
|
||||||
<div style={{ margin: 5 }}>
|
{!hideSize && <div style={{ margin: 5 }}>
|
||||||
{!sameSize && <span style={{ flex: 'none', margin: '0 5px' }}>Expected </span>}
|
{!sameSize && <span style={{ flex: 'none', margin: '0 5px' }}>Expected </span>}
|
||||||
<span>{expectedImage.naturalWidth}</span>
|
<span>{expectedImage.naturalWidth}</span>
|
||||||
<span style={{ flex: 'none', margin: '0 5px' }}>x</span>
|
<span style={{ flex: 'none', margin: '0 5px' }}>x</span>
|
||||||
|
|
@ -153,7 +158,7 @@ export const ImageDiffSlider: React.FC<{
|
||||||
{!sameSize && <span>{actualImage.naturalWidth}</span>}
|
{!sameSize && <span>{actualImage.naturalWidth}</span>}
|
||||||
{!sameSize && <span style={{ flex: 'none', margin: '0 5px' }}>x</span>}
|
{!sameSize && <span style={{ flex: 'none', margin: '0 5px' }}>x</span>}
|
||||||
{!sameSize && <span>{actualImage.naturalHeight}</span>}
|
{!sameSize && <span>{actualImage.naturalHeight}</span>}
|
||||||
</div>
|
</div>}
|
||||||
<div style={{ position: 'relative', width: canvasWidth, height: canvasHeight, margin: 15, ...checkerboardStyle }}>
|
<div style={{ position: 'relative', width: canvasWidth, height: canvasHeight, margin: 15, ...checkerboardStyle }}>
|
||||||
<ResizeView
|
<ResizeView
|
||||||
orientation={'horizontal'}
|
orientation={'horizontal'}
|
||||||
|
|
@ -161,7 +166,7 @@ export const ImageDiffSlider: React.FC<{
|
||||||
setOffsets={offsets => setSlider(offsets[0])}
|
setOffsets={offsets => setSlider(offsets[0])}
|
||||||
resizerColor={'#57606a80'}
|
resizerColor={'#57606a80'}
|
||||||
resizerWidth={6}></ResizeView>
|
resizerWidth={6}></ResizeView>
|
||||||
<img alt='Expected' style={{
|
<img alt={expectedTitle} style={{
|
||||||
width: expectedImage.naturalWidth * scale,
|
width: expectedImage.naturalWidth * scale,
|
||||||
height: expectedImage.naturalHeight * scale,
|
height: expectedImage.naturalHeight * scale,
|
||||||
}} draggable='false' src={expectedImage.src} />
|
}} draggable='false' src={expectedImage.src} />
|
||||||
|
|
@ -179,18 +184,19 @@ const ImageWithSize: React.FunctionComponent<{
|
||||||
image: HTMLImageElement,
|
image: HTMLImageElement,
|
||||||
title?: string,
|
title?: string,
|
||||||
alt?: string,
|
alt?: string,
|
||||||
|
hideSize?: boolean,
|
||||||
canvasWidth: number,
|
canvasWidth: number,
|
||||||
canvasHeight: number,
|
canvasHeight: number,
|
||||||
scale: number,
|
scale: number,
|
||||||
onClick?: () => void;
|
onClick?: () => void;
|
||||||
}> = ({ image, title, alt, canvasWidth, canvasHeight, scale, onClick }) => {
|
}> = ({ image, title, alt, hideSize, canvasWidth, canvasHeight, scale, onClick }) => {
|
||||||
return <div style={{ flex: 'none', display: 'flex', alignItems: 'center', flexDirection: 'column' }}>
|
return <div style={{ flex: 'none', display: 'flex', alignItems: 'center', flexDirection: 'column' }}>
|
||||||
<div style={{ margin: 5 }}>
|
{!hideSize && <div style={{ margin: 5 }}>
|
||||||
{title && <span style={{ flex: 'none', margin: '0 5px' }}>{title}</span>}
|
{title && <span style={{ flex: 'none', margin: '0 5px' }}>{title}</span>}
|
||||||
<span>{image.naturalWidth}</span>
|
<span>{image.naturalWidth}</span>
|
||||||
<span style={{ flex: 'none', margin: '0 5px' }}>x</span>
|
<span style={{ flex: 'none', margin: '0 5px' }}>x</span>
|
||||||
<span>{image.naturalHeight}</span>
|
<span>{image.naturalHeight}</span>
|
||||||
</div>
|
</div>}
|
||||||
<div style={{ display: 'flex', flex: 'none', width: canvasWidth, height: canvasHeight, margin: 15, ...checkerboardStyle }}>
|
<div style={{ display: 'flex', flex: 'none', width: canvasWidth, height: canvasHeight, margin: 15, ...checkerboardStyle }}>
|
||||||
<img
|
<img
|
||||||
width={image.naturalWidth * scale}
|
width={image.naturalWidth * scale}
|
||||||
|
|
|
||||||
|
|
@ -54,7 +54,7 @@ it.describe('mobile viewport', () => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should be detectable', async ({ playwright, browser, server, browserName, platform }) => {
|
it('should be detectable', async ({ playwright, browser }) => {
|
||||||
const iPhone = playwright.devices['iPhone 6'];
|
const iPhone = playwright.devices['iPhone 6'];
|
||||||
const context = await browser.newContext({ ...iPhone });
|
const context = await browser.newContext({ ...iPhone });
|
||||||
const page = await context.newPage();
|
const page = await context.newPage();
|
||||||
|
|
@ -62,7 +62,7 @@ it.describe('mobile viewport', () => {
|
||||||
await context.close();
|
await context.close();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should detect touch when applying viewport with touches', async ({ browser, server, browserName, platform }) => {
|
it('should detect touch when applying viewport with touches', async ({ browser, server }) => {
|
||||||
const context = await browser.newContext({ viewport: { width: 800, height: 600 }, hasTouch: true });
|
const context = await browser.newContext({ viewport: { width: 800, height: 600 }, hasTouch: true });
|
||||||
const page = await context.newPage();
|
const page = await context.newPage();
|
||||||
await page.goto(server.EMPTY_PAGE);
|
await page.goto(server.EMPTY_PAGE);
|
||||||
|
|
@ -154,7 +154,7 @@ it.describe('mobile viewport', () => {
|
||||||
await desktopPage.close();
|
await desktopPage.close();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('mouse should work with mobile viewports and cross process navigations', async ({ browser, server, browserName }) => {
|
it('mouse should work with mobile viewports and cross process navigations', async ({ browser, server }) => {
|
||||||
// @see https://crbug.com/929806
|
// @see https://crbug.com/929806
|
||||||
const context = await browser.newContext({ viewport: { width: 360, height: 640 }, isMobile: true });
|
const context = await browser.newContext({ viewport: { width: 360, height: 640 }, isMobile: true });
|
||||||
const page = await context.newPage();
|
const page = await context.newPage();
|
||||||
|
|
@ -193,8 +193,7 @@ it.describe('mobile viewport', () => {
|
||||||
{ type: 'issue', description: 'https://github.com/microsoft/playwright/issues/31551' },
|
{ type: 'issue', description: 'https://github.com/microsoft/playwright/issues/31551' },
|
||||||
{ type: 'issue', description: 'https://github.com/microsoft/playwright/issues/23573' },
|
{ type: 'issue', description: 'https://github.com/microsoft/playwright/issues/23573' },
|
||||||
]
|
]
|
||||||
}, async ({ playwright, browser, server, browserName, isLinux, headless }) => {
|
}, async ({ playwright, browser, server }) => {
|
||||||
it.fixme(browserName === 'webkit' && isLinux && headless, 'Fails on WPE apparently due to accelerated compositing + fixed layout');
|
|
||||||
const iPhone = playwright.devices['iPhone 12'];
|
const iPhone = playwright.devices['iPhone 12'];
|
||||||
const context = await browser.newContext({ ...iPhone });
|
const context = await browser.newContext({ ...iPhone });
|
||||||
const page = await context.newPage();
|
const page = await context.newPage();
|
||||||
|
|
@ -204,7 +203,7 @@ it.describe('mobile viewport', () => {
|
||||||
await context.close();
|
await context.close();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('view scale should reset after navigation', async ({ browser, browserName }) => {
|
it('view scale should reset after navigation', async ({ browser }) => {
|
||||||
it.info().annotations.push({ type: 'issue', description: 'https://github.com/microsoft/playwright/issues/26876' });
|
it.info().annotations.push({ type: 'issue', description: 'https://github.com/microsoft/playwright/issues/26876' });
|
||||||
const context = await browser.newContext({
|
const context = await browser.newContext({
|
||||||
viewport: { width: 390, height: 664 },
|
viewport: { width: 390, height: 664 },
|
||||||
|
|
|
||||||
|
|
@ -258,7 +258,8 @@ it('should work with subframes return 204 with domcontentloaded', async ({ page,
|
||||||
await page.goto(server.PREFIX + '/frames/one-frame.html', { waitUntil: 'domcontentloaded' });
|
await page.goto(server.PREFIX + '/frames/one-frame.html', { waitUntil: 'domcontentloaded' });
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should fail when server returns 204', async ({ page, server, browserName }) => {
|
it('should fail when server returns 204', async ({ page, server, browserName, isLinux }) => {
|
||||||
|
it.fixme(browserName === 'webkit' && isLinux, 'Regressed in https://github.com/microsoft/playwright-browsers/pull/1297');
|
||||||
// WebKit just loads an empty page.
|
// WebKit just loads an empty page.
|
||||||
server.setRoute('/empty.html', (req, res) => {
|
server.setRoute('/empty.html', (req, res) => {
|
||||||
res.statusCode = 204;
|
res.statusCode = 204;
|
||||||
|
|
|
||||||
|
|
@ -179,7 +179,7 @@ for (const useIntermediateMergeReport of [false] as const) {
|
||||||
await expect(page.locator('text=Image mismatch')).toBeVisible();
|
await expect(page.locator('text=Image mismatch')).toBeVisible();
|
||||||
await expect(page.locator('text=Snapshot mismatch')).toHaveCount(0);
|
await expect(page.locator('text=Snapshot mismatch')).toHaveCount(0);
|
||||||
|
|
||||||
await expect(page.getByTestId('test-result-image-mismatch-tabs').locator('div')).toHaveText([
|
await expect(page.getByTestId('test-screenshot-error-view').getByTestId('test-result-image-mismatch-tabs').locator('div')).toHaveText([
|
||||||
'Diff',
|
'Diff',
|
||||||
'Actual',
|
'Actual',
|
||||||
'Expected',
|
'Expected',
|
||||||
|
|
@ -187,7 +187,9 @@ for (const useIntermediateMergeReport of [false] as const) {
|
||||||
'Slider',
|
'Slider',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const imageDiff = page.getByTestId('test-result-image-mismatch');
|
for (const testId of ['test-results-image-diff', 'test-screenshot-error-view']) {
|
||||||
|
await test.step(testId, async () => {
|
||||||
|
const imageDiff = page.getByTestId(testId).getByTestId('test-result-image-mismatch');
|
||||||
await test.step('Diff', async () => {
|
await test.step('Diff', async () => {
|
||||||
await expect(imageDiff.locator('img')).toHaveAttribute('alt', 'Diff');
|
await expect(imageDiff.locator('img')).toHaveAttribute('alt', 'Diff');
|
||||||
});
|
});
|
||||||
|
|
@ -218,6 +220,8 @@ for (const useIntermediateMergeReport of [false] as const) {
|
||||||
await expect(imageDiff.locator('img').last()).toHaveAttribute('alt', 'Actual');
|
await expect(imageDiff.locator('img').last()).toHaveAttribute('alt', 'Actual');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
test('should include multiple image diffs', async ({ runInlineTest, page, showReport }) => {
|
test('should include multiple image diffs', async ({ runInlineTest, page, showReport }) => {
|
||||||
const IMG_WIDTH = 200;
|
const IMG_WIDTH = 200;
|
||||||
|
|
@ -285,8 +289,14 @@ for (const useIntermediateMergeReport of [false] as const) {
|
||||||
|
|
||||||
await showReport();
|
await showReport();
|
||||||
await page.click('text=fails');
|
await page.click('text=fails');
|
||||||
await expect(page.locator('data-testid=test-result-image-mismatch')).toHaveCount(3);
|
await expect(page.getByTestId('test-screenshot-error-view').getByTestId('error-suffix')).toContainText([
|
||||||
await expect(page.locator('text=Image mismatch:')).toHaveText([
|
`> 6 | await expect.soft(screenshot).toMatchSnapshot('expected.png');`,
|
||||||
|
`> 7 | await expect.soft(screenshot).toMatchSnapshot('expected.png');`,
|
||||||
|
`> 8 | await expect.soft(screenshot).toMatchSnapshot('expected.png');`,
|
||||||
|
]);
|
||||||
|
const imageDiffs = page.getByTestId('test-results-image-diff');
|
||||||
|
await expect(imageDiffs.getByTestId('test-result-image-mismatch')).toHaveCount(3);
|
||||||
|
await expect(imageDiffs.getByText('Image mismatch:')).toHaveText([
|
||||||
'Image mismatch: expected.png',
|
'Image mismatch: expected.png',
|
||||||
'Image mismatch: expected-1.png',
|
'Image mismatch: expected-1.png',
|
||||||
'Image mismatch: expected-2.png',
|
'Image mismatch: expected-2.png',
|
||||||
|
|
@ -323,7 +333,7 @@ for (const useIntermediateMergeReport of [false] as const) {
|
||||||
await expect(page.getByTestId('test-result-image-mismatch-tabs').locator('div')).toHaveText([
|
await expect(page.getByTestId('test-result-image-mismatch-tabs').locator('div')).toHaveText([
|
||||||
'Diff',
|
'Diff',
|
||||||
'Actual',
|
'Actual',
|
||||||
'Expected',
|
'Previous',
|
||||||
'Side by side',
|
'Side by side',
|
||||||
'Slider',
|
'Slider',
|
||||||
]);
|
]);
|
||||||
|
|
@ -460,7 +470,7 @@ for (const useIntermediateMergeReport of [false] as const) {
|
||||||
|
|
||||||
await showReport();
|
await showReport();
|
||||||
await page.click('text=fails');
|
await page.click('text=fails');
|
||||||
await expect(page.locator('.test-error-message span:has-text("received")').nth(1)).toHaveCSS('color', 'rgb(204, 0, 0)');
|
await expect(page.locator('.test-error-view span:has-text("received")').nth(1)).toHaveCSS('color', 'rgb(204, 0, 0)');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should show trace source', async ({ runInlineTest, page, showReport }) => {
|
test('should show trace source', async ({ runInlineTest, page, showReport }) => {
|
||||||
|
|
|
||||||
|
|
@ -675,6 +675,65 @@ test('should respect --tsconfig option', async ({ runInlineTest }) => {
|
||||||
expect(result.output).not.toContain(`Could not`);
|
expect(result.output).not.toContain(`Could not`);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('should respect config.tsconfig option', async ({ runInlineTest }) => {
|
||||||
|
const result = await runInlineTest({
|
||||||
|
'playwright.config.ts': `
|
||||||
|
export { configFoo } from '~/foo';
|
||||||
|
export default {
|
||||||
|
testDir: './tests',
|
||||||
|
tsconfig: './tsconfig.tests.json',
|
||||||
|
};
|
||||||
|
`,
|
||||||
|
'tsconfig.json': `{
|
||||||
|
"compilerOptions": {
|
||||||
|
"baseUrl": ".",
|
||||||
|
"paths": {
|
||||||
|
"~/*": ["./mapped-from-config/*"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}`,
|
||||||
|
'mapped-from-config/foo.ts': `
|
||||||
|
export const configFoo = 17;
|
||||||
|
`,
|
||||||
|
'tsconfig.tests.json': `{
|
||||||
|
"compilerOptions": {
|
||||||
|
"baseUrl": ".",
|
||||||
|
"paths": {
|
||||||
|
"~/*": ["./mapped-from-tests/*"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}`,
|
||||||
|
'mapped-from-tests/foo.ts': `
|
||||||
|
export const testFoo = 42;
|
||||||
|
`,
|
||||||
|
'tests/tsconfig.json': `{
|
||||||
|
"compilerOptions": {
|
||||||
|
"baseUrl": ".",
|
||||||
|
"paths": {
|
||||||
|
"~/*": ["../should-be-ignored/*"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}`,
|
||||||
|
'tests/a.test.ts': `
|
||||||
|
import { testFoo } from '~/foo';
|
||||||
|
import { configFoo } from '../playwright.config';
|
||||||
|
import { test, expect } from '@playwright/test';
|
||||||
|
test('test', ({}) => {
|
||||||
|
expect(testFoo).toBe(42);
|
||||||
|
expect(configFoo).toBe(17);
|
||||||
|
});
|
||||||
|
`,
|
||||||
|
'should-be-ignored/foo.ts': `
|
||||||
|
export const testFoo = 43;
|
||||||
|
export const configFoo = 18;
|
||||||
|
`,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.passed).toBe(1);
|
||||||
|
expect(result.exitCode).toBe(0);
|
||||||
|
expect(result.output).not.toContain(`Could not`);
|
||||||
|
});
|
||||||
|
|
||||||
test.describe('directory imports', () => {
|
test.describe('directory imports', () => {
|
||||||
test('should resolve index.js without path mapping in CJS', async ({ runInlineTest, runTSC }) => {
|
test('should resolve index.js without path mapping in CJS', async ({ runInlineTest, runTSC }) => {
|
||||||
const files = {
|
const files = {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue