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/
|
||||
.eslintcache
|
||||
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
|
||||
* since: v1.10
|
||||
- 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
|
||||
```
|
||||
|
||||
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
|
||||
|
||||
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.
|
||||
*/
|
||||
|
||||
.test-error-message {
|
||||
.test-error-view {
|
||||
white-space: pre;
|
||||
font-family: monospace;
|
||||
overflow: auto;
|
||||
flex: none;
|
||||
padding: 0;
|
||||
|
|
@ -26,3 +25,7 @@
|
|||
line-height: initial;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.test-error-text {
|
||||
font-family: monospace;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,21 +17,39 @@
|
|||
import ansi2html from 'ansi-to-html';
|
||||
import * as React from 'react';
|
||||
import './testErrorView.css';
|
||||
import type { ImageDiff } from '@web/shared/imageDiffView';
|
||||
import { ImageDiffView } from '@web/shared/imageDiffView';
|
||||
|
||||
export const TestErrorView: React.FC<{
|
||||
error: string;
|
||||
}> = ({ error }) => {
|
||||
const html = React.useMemo(() => {
|
||||
const config: any = {
|
||||
bg: 'var(--color-canvas-subtle)',
|
||||
fg: 'var(--color-fg-default)',
|
||||
};
|
||||
config.colors = ansiColors;
|
||||
return new ansi2html(config).toHtml(escapeHTML(error));
|
||||
}, [error]);
|
||||
return <div className='test-error-message' dangerouslySetInnerHTML={{ __html: html || '' }}></div>;
|
||||
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 = {
|
||||
bg: 'var(--color-canvas-subtle)',
|
||||
fg: 'var(--color-fg-default)',
|
||||
};
|
||||
config.colors = ansiColors;
|
||||
return new ansi2html(config).toHtml(escapeHTML(text || ''));
|
||||
}
|
||||
|
||||
const ansiColors = {
|
||||
0: '#000',
|
||||
1: '#C00',
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ import { AttachmentLink, generateTraceUrl } from './links';
|
|||
import { statusIcon } from './statusIcon';
|
||||
import type { ImageDiff } from '@web/shared/imageDiffView';
|
||||
import { ImageDiffView } from '@web/shared/imageDiffView';
|
||||
import { TestErrorView } from './testErrorView';
|
||||
import { TestErrorView, TestScreenshotErrorView } from './testErrorView';
|
||||
import './testResultView.css';
|
||||
|
||||
function groupImageDiffs(screenshots: Set<TestAttachment>): ImageDiff[] {
|
||||
|
|
@ -67,7 +67,7 @@ export const TestResultView: React.FC<{
|
|||
anchor: 'video' | 'diff' | '',
|
||||
}> = ({ 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 screenshots = new Set(attachments.filter(a => a.contentType.startsWith('image/')));
|
||||
const videos = attachments.filter(a => a.name === 'video');
|
||||
|
|
@ -76,7 +76,8 @@ export const TestResultView: React.FC<{
|
|||
const otherAttachments = new Set<TestAttachment>(attachments);
|
||||
[...screenshots, ...videos, ...traces, ...htmls].forEach(a => otherAttachments.delete(a));
|
||||
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]);
|
||||
|
||||
const videoRef = React.useRef<HTMLDivElement>(null);
|
||||
|
|
@ -94,15 +95,19 @@ export const TestResultView: React.FC<{
|
|||
}, [scrolled, anchor, setScrolled, videoRef]);
|
||||
|
||||
return <div className='test-result'>
|
||||
{!!result.errors.length && <AutoChip header='Errors'>
|
||||
{result.errors.map((error, index) => <TestErrorView key={'test-result-error-message-' + index} error={error}></TestErrorView>)}
|
||||
{!!errors.length && <AutoChip header='Errors'>
|
||||
{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>}
|
||||
{!!result.steps.length && <AutoChip header='Test Steps'>
|
||||
{result.steps.map((step, i) => <StepTreeItem key={`step-${i}`} step={step} depth={0}></StepTreeItem>)}
|
||||
</AutoChip>}
|
||||
|
||||
{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>
|
||||
</AutoChip>
|
||||
)}
|
||||
|
|
@ -145,6 +150,29 @@ export const TestResultView: React.FC<{
|
|||
</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<{
|
||||
step: TestStep;
|
||||
depth: number,
|
||||
|
|
|
|||
|
|
@ -9,9 +9,9 @@
|
|||
},
|
||||
{
|
||||
"name": "chromium-tip-of-tree",
|
||||
"revision": "1267",
|
||||
"revision": "1268",
|
||||
"installByDefault": false,
|
||||
"browserVersion": "131.0.6764.0"
|
||||
"browserVersion": "131.0.6768.0"
|
||||
},
|
||||
{
|
||||
"name": "firefox",
|
||||
|
|
@ -27,7 +27,7 @@
|
|||
},
|
||||
{
|
||||
"name": "webkit",
|
||||
"revision": "2084",
|
||||
"revision": "2090",
|
||||
"installByDefault": true,
|
||||
"revisionOverrides": {
|
||||
"mac10.14": "1446",
|
||||
|
|
|
|||
|
|
@ -43,7 +43,7 @@ import type { TimeoutOptions } from '../common/types';
|
|||
import { isInvalidSelectorError } from '../utils/isomorphic/selectorParser';
|
||||
import { parseEvaluationResultValue, source } from './isomorphic/utilityScriptSerializers';
|
||||
import type { SerializedValue } from './isomorphic/utilityScriptSerializers';
|
||||
import { TargetClosedError } from './errors';
|
||||
import { TargetClosedError, TimeoutError } from './errors';
|
||||
import { asLocator } from '../utils';
|
||||
import { helper } from './helper';
|
||||
|
||||
|
|
@ -662,7 +662,7 @@ export class Page extends SdkObject {
|
|||
return {};
|
||||
}
|
||||
|
||||
if (areEqualScreenshots(actual, options.expected, previous)) {
|
||||
if (areEqualScreenshots(actual, options.expected, undefined)) {
|
||||
progress.log(`screenshot matched expectation`);
|
||||
return {};
|
||||
}
|
||||
|
|
@ -672,10 +672,13 @@ export class Page extends SdkObject {
|
|||
// A: We want user to receive a friendly diff between actual and expected/previous.
|
||||
if (js.isJavaScriptErrorInEvaluate(e) || isInvalidSelectorError(e))
|
||||
throw e;
|
||||
let errorMessage = e.message;
|
||||
if (e instanceof TimeoutError && intermediateResult?.previous)
|
||||
errorMessage = `Failed to take two consecutive stable screenshots. ${e.message}`;
|
||||
return {
|
||||
log: e.message ? [...metadata.log, e.message] : metadata.log,
|
||||
...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.
|
||||
*/
|
||||
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.
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -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: 'InputTypeMonthEnabled', 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);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -45,6 +45,7 @@ export class FullConfigInternal {
|
|||
readonly webServers: NonNullable<FullConfig['webServer']>[];
|
||||
readonly plugins: TestRunnerPluginRegistration[];
|
||||
readonly projects: FullProjectInternal[] = [];
|
||||
readonly singleTSConfigPath?: string;
|
||||
cliArgs: string[] = [];
|
||||
cliGrep: string | undefined;
|
||||
cliGrepInvert: string | undefined;
|
||||
|
|
@ -69,6 +70,7 @@ export class FullConfigInternal {
|
|||
this.configCLIOverrides = configCLIOverrides;
|
||||
const privateConfiguration = (userConfig as any)['@playwright/test'];
|
||||
this.plugins = (privateConfiguration?.plugins || []).map((p: any) => ({ factory: p }));
|
||||
this.singleTSConfigPath = pathResolve(configDir, userConfig.tsconfig);
|
||||
|
||||
this.config = {
|
||||
configFile: resolvedConfigFile,
|
||||
|
|
|
|||
|
|
@ -118,6 +118,8 @@ export async function loadConfig(location: ConfigLocation, overrides?: ConfigCLI
|
|||
const babelPlugins = (userConfig as any)['@playwright/test']?.babelPlugins || [];
|
||||
const external = userConfig.build?.external || [];
|
||||
setTransformConfig({ babelPlugins, external });
|
||||
if (!overrides?.tsconfig)
|
||||
setSingleTSConfig(fullConfig?.singleTSConfigPath);
|
||||
|
||||
// 4. Send transform options to ESM loader.
|
||||
await configureESMLoaderTransformConfig();
|
||||
|
|
|
|||
|
|
@ -77,5 +77,6 @@ export async function configureESMLoader() {
|
|||
export async function configureESMLoaderTransformConfig() {
|
||||
if (!loaderChannel)
|
||||
return;
|
||||
await loaderChannel.send('setSingleTSConfig', { tsconfig: singleTSConfig() });
|
||||
await loaderChannel.send('setTransformConfig', { config: transformConfig() });
|
||||
}
|
||||
|
|
|
|||
|
|
@ -423,7 +423,7 @@ export async function toHaveScreenshot(
|
|||
// - regular matcher (i.e. not a `.not`)
|
||||
// - perhaps an 'all' flag to update non-matching screenshots
|
||||
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)
|
||||
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.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) {
|
||||
|
|
|
|||
|
|
@ -302,10 +302,11 @@ export class TestServerDispatcher implements TestServerInterface {
|
|||
preserveOutputDir: true,
|
||||
reporter: params.reporters ? params.reporters.map(r => [r]) : undefined,
|
||||
use: {
|
||||
...(this._configCLIOverrides.use || {}),
|
||||
trace: params.trace === 'on' ? { mode: 'on', sources: false, _live: true } : (params.trace === 'off' ? 'off' : undefined),
|
||||
video: params.video === 'on' ? 'on' : (params.video === 'off' ? 'off' : undefined),
|
||||
headless: params.headed ? false : undefined,
|
||||
...this._configCLIOverrides.use,
|
||||
...(params.trace === 'on' ? { trace: { mode: 'on', sources: false, _live: true } } : {}),
|
||||
...(params.trace === 'off' ? { trace: 'off' } : {}),
|
||||
...(params.video === 'on' || params.video === 'off' ? { video: params.video } : {}),
|
||||
...(params.headed !== undefined ? { headless: !params.headed } : {}),
|
||||
_optionContextReuseMode: params.reuseContext ? 'when-possible' : 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;
|
||||
|
||||
/**
|
||||
* 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'`.
|
||||
* - `'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<{
|
||||
diff: ImageDiff,
|
||||
noTargetBlank?: boolean,
|
||||
}> = ({ diff, noTargetBlank }) => {
|
||||
hideDetails?: boolean,
|
||||
}> = ({ diff, noTargetBlank, hideDetails }) => {
|
||||
const [mode, setMode] = React.useState<'diff' | 'actual' | 'expected' | 'slider' | 'sxs'>(diff.diff ? 'diff' : 'actual');
|
||||
const [showSxsDiff, setShowSxsDiff] = React.useState<boolean>(false);
|
||||
|
||||
const [expectedImage, setExpectedImage] = React.useState<HTMLImageElement | null>(null);
|
||||
const [expectedImageTitle, setExpectedImageTitle] = React.useState<string>('Expected');
|
||||
const [actualImage, setActualImage] = React.useState<HTMLImageElement | null>(null);
|
||||
const [diffImage, setDiffImage] = React.useState<HTMLImageElement | null>(null);
|
||||
const [measure, ref] = useMeasure<HTMLDivElement>();
|
||||
|
|
@ -73,6 +75,7 @@ export const ImageDiffView: React.FC<{
|
|||
React.useEffect(() => {
|
||||
(async () => {
|
||||
setExpectedImage(await loadImage(diff.expected?.attachment.path));
|
||||
setExpectedImageTitle(diff.expected?.title || 'Expected');
|
||||
setActualImage(await loadImage(diff.actual?.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' }}>
|
||||
{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 === '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 === 'slider' ? 600 : 'initial' }} onClick={() => setMode('slider')}>Slider</div>
|
||||
</div>
|
||||
<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 === 'actual' && <ImageWithSize image={actualImage} alt='Actual' canvasWidth={fitWidth} canvasHeight={fitHeight} scale={scale}/>}
|
||||
{diff.diff && mode === 'expected' && <ImageWithSize image={expectedImage} alt='Expected' canvasWidth={fitWidth} canvasHeight={fitHeight} scale={scale}/>}
|
||||
{diff.diff && mode === 'slider' && <ImageDiffSlider expectedImage={expectedImage} actualImage={actualImage} 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' hideSize={hideDetails} 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} hideSize={hideDetails} canvasWidth={fitWidth} canvasHeight={fitHeight} scale={scale} expectedTitle={expectedImageTitle} />}
|
||||
{diff.diff && mode === 'sxs' && <div style={{ display: 'flex' }}>
|
||||
<ImageWithSize image={expectedImage} title='Expected' 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={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)} hideSize={hideDetails} canvasWidth={sxsScale * imageWidth} canvasHeight={sxsScale * imageHeight} scale={sxsScale} />
|
||||
</div>}
|
||||
{!diff.diff && mode === 'actual' && <ImageWithSize image={actualImage} title='Actual' canvasWidth={fitWidth} canvasHeight={fitHeight} scale={scale}/>}
|
||||
{!diff.diff && mode === 'expected' && <ImageWithSize image={expectedImage} title='Expected' 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={expectedImageTitle} hideSize={hideDetails} canvasWidth={fitWidth} canvasHeight={fitHeight} scale={scale}/>}
|
||||
{!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} />
|
||||
</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><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>
|
||||
</div>}
|
||||
</>}
|
||||
</div>;
|
||||
};
|
||||
|
|
@ -133,7 +136,9 @@ export const ImageDiffSlider: React.FC<{
|
|||
canvasWidth: number,
|
||||
canvasHeight: number,
|
||||
scale: number,
|
||||
}> = ({ expectedImage, actualImage, canvasWidth, canvasHeight, scale }) => {
|
||||
expectedTitle: string,
|
||||
hideSize?: boolean,
|
||||
}> = ({ expectedImage, actualImage, canvasWidth, canvasHeight, scale, expectedTitle, hideSize }) => {
|
||||
const absoluteStyle: React.CSSProperties = {
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
|
|
@ -144,7 +149,7 @@ export const ImageDiffSlider: React.FC<{
|
|||
const sameSize = expectedImage.naturalWidth === actualImage.naturalWidth && expectedImage.naturalHeight === actualImage.naturalHeight;
|
||||
|
||||
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>}
|
||||
<span>{expectedImage.naturalWidth}</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 style={{ flex: 'none', margin: '0 5px' }}>x</span>}
|
||||
{!sameSize && <span>{actualImage.naturalHeight}</span>}
|
||||
</div>
|
||||
</div>}
|
||||
<div style={{ position: 'relative', width: canvasWidth, height: canvasHeight, margin: 15, ...checkerboardStyle }}>
|
||||
<ResizeView
|
||||
orientation={'horizontal'}
|
||||
|
|
@ -161,7 +166,7 @@ export const ImageDiffSlider: React.FC<{
|
|||
setOffsets={offsets => setSlider(offsets[0])}
|
||||
resizerColor={'#57606a80'}
|
||||
resizerWidth={6}></ResizeView>
|
||||
<img alt='Expected' style={{
|
||||
<img alt={expectedTitle} style={{
|
||||
width: expectedImage.naturalWidth * scale,
|
||||
height: expectedImage.naturalHeight * scale,
|
||||
}} draggable='false' src={expectedImage.src} />
|
||||
|
|
@ -179,18 +184,19 @@ const ImageWithSize: React.FunctionComponent<{
|
|||
image: HTMLImageElement,
|
||||
title?: string,
|
||||
alt?: string,
|
||||
hideSize?: boolean,
|
||||
canvasWidth: number,
|
||||
canvasHeight: number,
|
||||
scale: number,
|
||||
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' }}>
|
||||
<div style={{ margin: 5 }}>
|
||||
{!hideSize && <div style={{ margin: 5 }}>
|
||||
{title && <span style={{ flex: 'none', margin: '0 5px' }}>{title}</span>}
|
||||
<span>{image.naturalWidth}</span>
|
||||
<span style={{ flex: 'none', margin: '0 5px' }}>x</span>
|
||||
<span>{image.naturalHeight}</span>
|
||||
</div>
|
||||
</div>}
|
||||
<div style={{ display: 'flex', flex: 'none', width: canvasWidth, height: canvasHeight, margin: 15, ...checkerboardStyle }}>
|
||||
<img
|
||||
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 context = await browser.newContext({ ...iPhone });
|
||||
const page = await context.newPage();
|
||||
|
|
@ -62,7 +62,7 @@ it.describe('mobile viewport', () => {
|
|||
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 page = await context.newPage();
|
||||
await page.goto(server.EMPTY_PAGE);
|
||||
|
|
@ -154,7 +154,7 @@ it.describe('mobile viewport', () => {
|
|||
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
|
||||
const context = await browser.newContext({ viewport: { width: 360, height: 640 }, isMobile: true });
|
||||
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/23573' },
|
||||
]
|
||||
}, async ({ playwright, browser, server, browserName, isLinux, headless }) => {
|
||||
it.fixme(browserName === 'webkit' && isLinux && headless, 'Fails on WPE apparently due to accelerated compositing + fixed layout');
|
||||
}, async ({ playwright, browser, server }) => {
|
||||
const iPhone = playwright.devices['iPhone 12'];
|
||||
const context = await browser.newContext({ ...iPhone });
|
||||
const page = await context.newPage();
|
||||
|
|
@ -204,7 +203,7 @@ it.describe('mobile viewport', () => {
|
|||
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' });
|
||||
const context = await browser.newContext({
|
||||
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' });
|
||||
});
|
||||
|
||||
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.
|
||||
server.setRoute('/empty.html', (req, res) => {
|
||||
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=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',
|
||||
'Actual',
|
||||
'Expected',
|
||||
|
|
@ -187,36 +187,40 @@ for (const useIntermediateMergeReport of [false] as const) {
|
|||
'Slider',
|
||||
]);
|
||||
|
||||
const imageDiff = page.getByTestId('test-result-image-mismatch');
|
||||
await test.step('Diff', async () => {
|
||||
await expect(imageDiff.locator('img')).toHaveAttribute('alt', 'Diff');
|
||||
});
|
||||
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 expect(imageDiff.locator('img')).toHaveAttribute('alt', 'Diff');
|
||||
});
|
||||
|
||||
await test.step('Actual', async () => {
|
||||
await imageDiff.getByText('Actual', { exact: true }).click();
|
||||
await expect(imageDiff.locator('img')).toHaveAttribute('alt', 'Actual');
|
||||
});
|
||||
await test.step('Actual', async () => {
|
||||
await imageDiff.getByText('Actual', { exact: true }).click();
|
||||
await expect(imageDiff.locator('img')).toHaveAttribute('alt', 'Actual');
|
||||
});
|
||||
|
||||
await test.step('Expected', async () => {
|
||||
await imageDiff.getByText('Expected', { exact: true }).click();
|
||||
await expect(imageDiff.locator('img')).toHaveAttribute('alt', 'Expected');
|
||||
});
|
||||
await test.step('Expected', async () => {
|
||||
await imageDiff.getByText('Expected', { exact: true }).click();
|
||||
await expect(imageDiff.locator('img')).toHaveAttribute('alt', 'Expected');
|
||||
});
|
||||
|
||||
await test.step('Side by side', async () => {
|
||||
await imageDiff.getByText('Side by side').click();
|
||||
await expect(imageDiff.locator('img')).toHaveCount(2);
|
||||
await expect(imageDiff.locator('img').first()).toHaveAttribute('alt', 'Expected');
|
||||
await expect(imageDiff.locator('img').last()).toHaveAttribute('alt', 'Actual');
|
||||
await imageDiff.locator('img').last().click();
|
||||
await expect(imageDiff.locator('img').last()).toHaveAttribute('alt', 'Diff');
|
||||
});
|
||||
await test.step('Side by side', async () => {
|
||||
await imageDiff.getByText('Side by side').click();
|
||||
await expect(imageDiff.locator('img')).toHaveCount(2);
|
||||
await expect(imageDiff.locator('img').first()).toHaveAttribute('alt', 'Expected');
|
||||
await expect(imageDiff.locator('img').last()).toHaveAttribute('alt', 'Actual');
|
||||
await imageDiff.locator('img').last().click();
|
||||
await expect(imageDiff.locator('img').last()).toHaveAttribute('alt', 'Diff');
|
||||
});
|
||||
|
||||
await test.step('Slider', async () => {
|
||||
await imageDiff.getByText('Slider', { exact: true }).click();
|
||||
await expect(imageDiff.locator('img')).toHaveCount(2);
|
||||
await expect(imageDiff.locator('img').first()).toHaveAttribute('alt', 'Expected');
|
||||
await expect(imageDiff.locator('img').last()).toHaveAttribute('alt', 'Actual');
|
||||
});
|
||||
await test.step('Slider', async () => {
|
||||
await imageDiff.getByText('Slider', { exact: true }).click();
|
||||
await expect(imageDiff.locator('img')).toHaveCount(2);
|
||||
await expect(imageDiff.locator('img').first()).toHaveAttribute('alt', 'Expected');
|
||||
await expect(imageDiff.locator('img').last()).toHaveAttribute('alt', 'Actual');
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
test('should include multiple image diffs', async ({ runInlineTest, page, showReport }) => {
|
||||
|
|
@ -285,8 +289,14 @@ for (const useIntermediateMergeReport of [false] as const) {
|
|||
|
||||
await showReport();
|
||||
await page.click('text=fails');
|
||||
await expect(page.locator('data-testid=test-result-image-mismatch')).toHaveCount(3);
|
||||
await expect(page.locator('text=Image mismatch:')).toHaveText([
|
||||
await expect(page.getByTestId('test-screenshot-error-view').getByTestId('error-suffix')).toContainText([
|
||||
`> 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-1.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([
|
||||
'Diff',
|
||||
'Actual',
|
||||
'Expected',
|
||||
'Previous',
|
||||
'Side by side',
|
||||
'Slider',
|
||||
]);
|
||||
|
|
@ -460,7 +470,7 @@ for (const useIntermediateMergeReport of [false] as const) {
|
|||
|
||||
await showReport();
|
||||
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 }) => {
|
||||
|
|
|
|||
|
|
@ -675,6 +675,65 @@ test('should respect --tsconfig option', async ({ runInlineTest }) => {
|
|||
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('should resolve index.js without path mapping in CJS', async ({ runInlineTest, runTSC }) => {
|
||||
const files = {
|
||||
|
|
|
|||
Loading…
Reference in a new issue