feat(tracing): clip canvas contents from screenshots (#33119)
This commit is contained in:
parent
b194d6a1e9
commit
ef84051c91
|
|
@ -85,7 +85,7 @@ export class InMemorySnapshotter implements SnapshotterDelegate, HarTracerDelega
|
||||||
|
|
||||||
onFrameSnapshot(snapshot: FrameSnapshot): void {
|
onFrameSnapshot(snapshot: FrameSnapshot): void {
|
||||||
++this._snapshotCount;
|
++this._snapshotCount;
|
||||||
const renderer = this._storage.addFrameSnapshot(snapshot);
|
const renderer = this._storage.addFrameSnapshot(snapshot, []);
|
||||||
this._snapshotReadyPromises.get(snapshot.snapshotName || '')?.resolve(renderer);
|
this._snapshotReadyPromises.get(snapshot.snapshotName || '')?.resolve(renderer);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -129,6 +129,13 @@ async function doFetch(event: FetchEvent): Promise<Response> {
|
||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (relativePath.startsWith('/closest-screenshot/')) {
|
||||||
|
const { snapshotServer } = loadedTraces.get(traceUrl!) || {};
|
||||||
|
if (!snapshotServer)
|
||||||
|
return new Response(null, { status: 404 });
|
||||||
|
return snapshotServer.serveClosestScreenshot(relativePath, url.searchParams);
|
||||||
|
}
|
||||||
|
|
||||||
if (relativePath.startsWith('/sha1/')) {
|
if (relativePath.startsWith('/sha1/')) {
|
||||||
// Sha1 for sources is based on the file path, can't load it of a random model.
|
// Sha1 for sources is based on the file path, can't load it of a random model.
|
||||||
const sha1 = relativePath.slice('/sha1/'.length);
|
const sha1 = relativePath.slice('/sha1/'.length);
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,16 @@
|
||||||
|
|
||||||
import { escapeHTMLAttribute, escapeHTML } from '@isomorphic/stringUtils';
|
import { escapeHTMLAttribute, escapeHTML } from '@isomorphic/stringUtils';
|
||||||
import type { FrameSnapshot, NodeNameAttributesChildNodesSnapshot, NodeSnapshot, RenderedFrameSnapshot, ResourceSnapshot, SubtreeReferenceSnapshot } from '@trace/snapshot';
|
import type { FrameSnapshot, NodeNameAttributesChildNodesSnapshot, NodeSnapshot, RenderedFrameSnapshot, ResourceSnapshot, SubtreeReferenceSnapshot } from '@trace/snapshot';
|
||||||
|
import type { PageEntry } from '../types/entries';
|
||||||
|
|
||||||
|
function findClosest<T>(items: T[], metric: (v: T) => number, target: number) {
|
||||||
|
return items.find((item, index) => {
|
||||||
|
if (index === items.length - 1)
|
||||||
|
return true;
|
||||||
|
const next = items[index + 1];
|
||||||
|
return Math.abs(metric(item) - target) < Math.abs(metric(next) - target);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function isNodeNameAttributesChildNodesSnapshot(n: NodeSnapshot): n is NodeNameAttributesChildNodesSnapshot {
|
function isNodeNameAttributesChildNodesSnapshot(n: NodeSnapshot): n is NodeNameAttributesChildNodesSnapshot {
|
||||||
return Array.isArray(n) && typeof n[0] === 'string';
|
return Array.isArray(n) && typeof n[0] === 'string';
|
||||||
|
|
@ -60,13 +70,15 @@ export class SnapshotRenderer {
|
||||||
private _resources: ResourceSnapshot[];
|
private _resources: ResourceSnapshot[];
|
||||||
private _snapshot: FrameSnapshot;
|
private _snapshot: FrameSnapshot;
|
||||||
private _callId: string;
|
private _callId: string;
|
||||||
|
private _screencastFrames: PageEntry['screencastFrames'];
|
||||||
|
|
||||||
constructor(resources: ResourceSnapshot[], snapshots: FrameSnapshot[], index: number) {
|
constructor(resources: ResourceSnapshot[], snapshots: FrameSnapshot[], screencastFrames: PageEntry['screencastFrames'], index: number) {
|
||||||
this._resources = resources;
|
this._resources = resources;
|
||||||
this._snapshots = snapshots;
|
this._snapshots = snapshots;
|
||||||
this._index = index;
|
this._index = index;
|
||||||
this._snapshot = snapshots[index];
|
this._snapshot = snapshots[index];
|
||||||
this._callId = snapshots[index].callId;
|
this._callId = snapshots[index].callId;
|
||||||
|
this._screencastFrames = screencastFrames;
|
||||||
this.snapshotName = snapshots[index].snapshotName;
|
this.snapshotName = snapshots[index].snapshotName;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -78,6 +90,14 @@ export class SnapshotRenderer {
|
||||||
return this._snapshots[this._index].viewport;
|
return this._snapshots[this._index].viewport;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
closestScreenshot(): string | undefined {
|
||||||
|
const { wallTime, timestamp } = this.snapshot();
|
||||||
|
const closestFrame = (wallTime && this._screencastFrames[0]?.frameSwapWallTime)
|
||||||
|
? findClosest(this._screencastFrames, frame => frame.frameSwapWallTime!, wallTime)
|
||||||
|
: findClosest(this._screencastFrames, frame => frame.timestamp, timestamp);
|
||||||
|
return closestFrame?.sha1;
|
||||||
|
}
|
||||||
|
|
||||||
render(): RenderedFrameSnapshot {
|
render(): RenderedFrameSnapshot {
|
||||||
const result: string[] = [];
|
const result: string[] = [];
|
||||||
const visit = (n: NodeSnapshot, snapshotIndex: number, parentTag: string | undefined, parentAttrs: [string, string][] | undefined) => {
|
const visit = (n: NodeSnapshot, snapshotIndex: number, parentTag: string | undefined, parentAttrs: [string, string][] | undefined) => {
|
||||||
|
|
@ -244,6 +264,8 @@ function snapshotNodes(snapshot: FrameSnapshot): NodeSnapshot[] {
|
||||||
|
|
||||||
function snapshotScript(...targetIds: (string | undefined)[]) {
|
function snapshotScript(...targetIds: (string | undefined)[]) {
|
||||||
function applyPlaywrightAttributes(unwrapPopoutUrl: (url: string) => string, ...targetIds: (string | undefined)[]) {
|
function applyPlaywrightAttributes(unwrapPopoutUrl: (url: string) => string, ...targetIds: (string | undefined)[]) {
|
||||||
|
const isUnderTest = new URLSearchParams(location.search).has('isUnderTest');
|
||||||
|
|
||||||
const kPointerWarningTitle = 'Recorded click position in absolute coordinates did not' +
|
const kPointerWarningTitle = 'Recorded click position in absolute coordinates did not' +
|
||||||
' match the center of the clicked element. This is likely due to a difference between' +
|
' match the center of the clicked element. This is likely due to a difference between' +
|
||||||
' the test runner and the trace viewer operating systems.';
|
' the test runner and the trace viewer operating systems.';
|
||||||
|
|
@ -251,6 +273,7 @@ function snapshotScript(...targetIds: (string | undefined)[]) {
|
||||||
const scrollTops: Element[] = [];
|
const scrollTops: Element[] = [];
|
||||||
const scrollLefts: Element[] = [];
|
const scrollLefts: Element[] = [];
|
||||||
const targetElements: Element[] = [];
|
const targetElements: Element[] = [];
|
||||||
|
const canvasElements: HTMLCanvasElement[] = [];
|
||||||
|
|
||||||
const visit = (root: Document | ShadowRoot) => {
|
const visit = (root: Document | ShadowRoot) => {
|
||||||
// Collect all scrolled elements for later use.
|
// Collect all scrolled elements for later use.
|
||||||
|
|
@ -326,6 +349,8 @@ function snapshotScript(...targetIds: (string | undefined)[]) {
|
||||||
}
|
}
|
||||||
(root as any).adoptedStyleSheets = adoptedSheets;
|
(root as any).adoptedStyleSheets = adoptedSheets;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
canvasElements.push(...root.querySelectorAll('canvas'));
|
||||||
};
|
};
|
||||||
|
|
||||||
const onLoad = () => {
|
const onLoad = () => {
|
||||||
|
|
@ -342,12 +367,12 @@ function snapshotScript(...targetIds: (string | undefined)[]) {
|
||||||
document.styleSheets[0].disabled = true;
|
document.styleSheets[0].disabled = true;
|
||||||
|
|
||||||
const search = new URL(window.location.href).searchParams;
|
const search = new URL(window.location.href).searchParams;
|
||||||
|
const isTopFrame = window.location.pathname.match(/\/page@[a-z0-9]+$/);
|
||||||
|
|
||||||
if (search.get('pointX') && search.get('pointY')) {
|
if (search.get('pointX') && search.get('pointY')) {
|
||||||
const pointX = +search.get('pointX')!;
|
const pointX = +search.get('pointX')!;
|
||||||
const pointY = +search.get('pointY')!;
|
const pointY = +search.get('pointY')!;
|
||||||
const hasInputTarget = search.has('hasInputTarget');
|
const hasInputTarget = search.has('hasInputTarget');
|
||||||
const isTopFrame = window.location.pathname.match(/\/page@[a-z0-9]+$/);
|
|
||||||
const hasTargetElements = targetElements.length > 0;
|
const hasTargetElements = targetElements.length > 0;
|
||||||
const roots = document.documentElement ? [document.documentElement] : [];
|
const roots = document.documentElement ? [document.documentElement] : [];
|
||||||
for (const target of (hasTargetElements ? targetElements : roots)) {
|
for (const target of (hasTargetElements ? targetElements : roots)) {
|
||||||
|
|
@ -393,6 +418,76 @@ function snapshotScript(...targetIds: (string | undefined)[]) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (canvasElements.length > 0) {
|
||||||
|
function drawCheckerboard(context: CanvasRenderingContext2D, canvas: HTMLCanvasElement) {
|
||||||
|
function createCheckerboardPattern() {
|
||||||
|
const pattern = document.createElement('canvas');
|
||||||
|
pattern.width = pattern.width / Math.floor(pattern.width / 24);
|
||||||
|
pattern.height = pattern.height / Math.floor(pattern.height / 24);
|
||||||
|
const context = pattern.getContext('2d')!;
|
||||||
|
context.fillStyle = 'lightgray';
|
||||||
|
context.fillRect(0, 0, pattern.width, pattern.height);
|
||||||
|
context.fillStyle = 'white';
|
||||||
|
context.fillRect(0, 0, pattern.width / 2, pattern.height / 2);
|
||||||
|
context.fillRect(pattern.width / 2, pattern.height / 2, pattern.width, pattern.height);
|
||||||
|
return context.createPattern(pattern, 'repeat')!;
|
||||||
|
}
|
||||||
|
|
||||||
|
context.fillStyle = createCheckerboardPattern();
|
||||||
|
context.fillRect(0, 0, canvas.width, canvas.height);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if (!isTopFrame) {
|
||||||
|
for (const canvas of canvasElements) {
|
||||||
|
const context = canvas.getContext('2d')!;
|
||||||
|
drawCheckerboard(context, canvas);
|
||||||
|
canvas.title = `Playwright displays canvas contents on a best-effort basis. It doesn't support canvas elements inside an iframe yet. If this impacts your workflow, please open an issue so we can prioritize.`;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const img = new Image();
|
||||||
|
img.onload = () => {
|
||||||
|
for (const canvas of canvasElements) {
|
||||||
|
const context = canvas.getContext('2d')!;
|
||||||
|
|
||||||
|
const boundingRect = canvas.getBoundingClientRect();
|
||||||
|
const xStart = boundingRect.left / window.innerWidth;
|
||||||
|
const yStart = boundingRect.top / window.innerHeight;
|
||||||
|
const xEnd = boundingRect.right / window.innerWidth;
|
||||||
|
const yEnd = boundingRect.bottom / window.innerHeight;
|
||||||
|
|
||||||
|
const partiallyUncaptured = xEnd > 1 || yEnd > 1;
|
||||||
|
const fullyUncaptured = xStart > 1 || yStart > 1;
|
||||||
|
if (fullyUncaptured) {
|
||||||
|
canvas.title = `Playwright couldn't capture canvas contents because it's located outside the viewport.`;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
drawCheckerboard(context, canvas);
|
||||||
|
|
||||||
|
context.drawImage(img, xStart * img.width, yStart * img.height, (xEnd - xStart) * img.width, (yEnd - yStart) * img.height, 0, 0, canvas.width, canvas.height);
|
||||||
|
if (isUnderTest)
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.log(`canvas drawn:`, JSON.stringify([xStart, yStart, xEnd, yEnd].map(v => Math.floor(v * 100))));
|
||||||
|
|
||||||
|
if (partiallyUncaptured)
|
||||||
|
canvas.title = `Playwright couldn't capture full canvas contents because it's located partially outside the viewport.`;
|
||||||
|
else
|
||||||
|
canvas.title = `Canvas contents are displayed on a best-effort basis based on viewport screenshots taken during test execution.`;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
img.onerror = () => {
|
||||||
|
for (const canvas of canvasElements) {
|
||||||
|
const context = canvas.getContext('2d')!;
|
||||||
|
drawCheckerboard(context, canvas);
|
||||||
|
canvas.title = `Playwright couldn't show canvas contents because the screenshot failed to load.`;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
img.src = location.href.replace('/snapshot', '/closest-screenshot');
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const onDOMContentLoaded = () => visit(document);
|
const onDOMContentLoaded = () => visit(document);
|
||||||
|
|
|
||||||
|
|
@ -35,11 +35,20 @@ export class SnapshotServer {
|
||||||
const snapshot = this._snapshot(pathname.substring('/snapshot'.length), searchParams);
|
const snapshot = this._snapshot(pathname.substring('/snapshot'.length), searchParams);
|
||||||
if (!snapshot)
|
if (!snapshot)
|
||||||
return new Response(null, { status: 404 });
|
return new Response(null, { status: 404 });
|
||||||
|
|
||||||
const renderedSnapshot = snapshot.render();
|
const renderedSnapshot = snapshot.render();
|
||||||
this._snapshotIds.set(snapshotUrl, snapshot);
|
this._snapshotIds.set(snapshotUrl, snapshot);
|
||||||
return new Response(renderedSnapshot.html, { status: 200, headers: { 'Content-Type': 'text/html; charset=utf-8' } });
|
return new Response(renderedSnapshot.html, { status: 200, headers: { 'Content-Type': 'text/html; charset=utf-8' } });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async serveClosestScreenshot(pathname: string, searchParams: URLSearchParams): Promise<Response> {
|
||||||
|
const snapshot = this._snapshot(pathname.substring('/closest-screenshot'.length), searchParams);
|
||||||
|
const sha1 = snapshot?.closestScreenshot();
|
||||||
|
if (!sha1)
|
||||||
|
return new Response(null, { status: 404 });
|
||||||
|
return new Response(await this._resourceLoader(sha1));
|
||||||
|
}
|
||||||
|
|
||||||
serveSnapshotInfo(pathname: string, searchParams: URLSearchParams): Response {
|
serveSnapshotInfo(pathname: string, searchParams: URLSearchParams): Response {
|
||||||
const snapshot = this._snapshot(pathname.substring('/snapshotInfo'.length), searchParams);
|
const snapshot = this._snapshot(pathname.substring('/snapshotInfo'.length), searchParams);
|
||||||
return this._respondWithJson(snapshot ? {
|
return this._respondWithJson(snapshot ? {
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@
|
||||||
|
|
||||||
import type { FrameSnapshot, ResourceSnapshot } from '@trace/snapshot';
|
import type { FrameSnapshot, ResourceSnapshot } from '@trace/snapshot';
|
||||||
import { rewriteURLForCustomProtocol, SnapshotRenderer } from './snapshotRenderer';
|
import { rewriteURLForCustomProtocol, SnapshotRenderer } from './snapshotRenderer';
|
||||||
|
import type { PageEntry } from '../types/entries';
|
||||||
|
|
||||||
export class SnapshotStorage {
|
export class SnapshotStorage {
|
||||||
private _resources: ResourceSnapshot[] = [];
|
private _resources: ResourceSnapshot[] = [];
|
||||||
|
|
@ -29,7 +30,7 @@ export class SnapshotStorage {
|
||||||
this._resources.push(resource);
|
this._resources.push(resource);
|
||||||
}
|
}
|
||||||
|
|
||||||
addFrameSnapshot(snapshot: FrameSnapshot) {
|
addFrameSnapshot(snapshot: FrameSnapshot, screencastFrames: PageEntry['screencastFrames']) {
|
||||||
for (const override of snapshot.resourceOverrides)
|
for (const override of snapshot.resourceOverrides)
|
||||||
override.url = rewriteURLForCustomProtocol(override.url);
|
override.url = rewriteURLForCustomProtocol(override.url);
|
||||||
let frameSnapshots = this._frameSnapshots.get(snapshot.frameId);
|
let frameSnapshots = this._frameSnapshots.get(snapshot.frameId);
|
||||||
|
|
@ -43,7 +44,7 @@ export class SnapshotStorage {
|
||||||
this._frameSnapshots.set(snapshot.pageId, frameSnapshots);
|
this._frameSnapshots.set(snapshot.pageId, frameSnapshots);
|
||||||
}
|
}
|
||||||
frameSnapshots.raw.push(snapshot);
|
frameSnapshots.raw.push(snapshot);
|
||||||
const renderer = new SnapshotRenderer(this._resources, frameSnapshots.raw, frameSnapshots.raw.length - 1);
|
const renderer = new SnapshotRenderer(this._resources, frameSnapshots.raw, screencastFrames, frameSnapshots.raw.length - 1);
|
||||||
frameSnapshots.renderers.push(renderer);
|
frameSnapshots.renderers.push(renderer);
|
||||||
return renderer;
|
return renderer;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -159,7 +159,7 @@ export class TraceModernizer {
|
||||||
contextEntry.resources.push(event.snapshot);
|
contextEntry.resources.push(event.snapshot);
|
||||||
break;
|
break;
|
||||||
case 'frame-snapshot':
|
case 'frame-snapshot':
|
||||||
this._snapshotStorage.addFrameSnapshot(event.snapshot);
|
this._snapshotStorage.addFrameSnapshot(event.snapshot, this._pageEntry(event.snapshot.pageId).screencastFrames);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
// Make sure there is a page entry for each page, even without screencast frames,
|
// Make sure there is a page entry for each page, even without screencast frames,
|
||||||
|
|
|
||||||
|
|
@ -369,10 +369,14 @@ export function collectSnapshots(action: ActionTraceEvent | undefined): Snapshot
|
||||||
return { action: actionSnapshot, before: beforeSnapshot, after: afterSnapshot };
|
return { action: actionSnapshot, before: beforeSnapshot, after: afterSnapshot };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const isUnderTest = new URLSearchParams(window.location.search).has('isUnderTest');
|
||||||
|
|
||||||
export function extendSnapshot(snapshot: Snapshot): SnapshotUrls {
|
export function extendSnapshot(snapshot: Snapshot): SnapshotUrls {
|
||||||
const params = new URLSearchParams();
|
const params = new URLSearchParams();
|
||||||
params.set('trace', context(snapshot.action).traceUrl);
|
params.set('trace', context(snapshot.action).traceUrl);
|
||||||
params.set('name', snapshot.snapshotName);
|
params.set('name', snapshot.snapshotName);
|
||||||
|
if (isUnderTest)
|
||||||
|
params.set('isUnderTest', 'true');
|
||||||
if (snapshot.point) {
|
if (snapshot.point) {
|
||||||
params.set('pointX', String(snapshot.point.x));
|
params.set('pointX', String(snapshot.point.x));
|
||||||
params.set('pointY', String(snapshot.point.y));
|
params.set('pointY', String(snapshot.point.y));
|
||||||
|
|
|
||||||
|
|
@ -7,4 +7,7 @@
|
||||||
ctx.fillRect(25, 25, 100, 100);
|
ctx.fillRect(25, 25, 100, 100);
|
||||||
ctx.clearRect(45, 45, 60, 60);
|
ctx.clearRect(45, 45, 60, 60);
|
||||||
ctx.strokeRect(50, 50, 50, 50);
|
ctx.strokeRect(50, 50, 50, 50);
|
||||||
|
|
||||||
|
if (location.hash.includes('canvas-on-edge'))
|
||||||
|
canvas.style.marginTop = '90vh';
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@ import path from 'path';
|
||||||
import { pathToFileURL } from 'url';
|
import { pathToFileURL } from 'url';
|
||||||
import { expect, playwrightTest } from '../config/browserTest';
|
import { expect, playwrightTest } from '../config/browserTest';
|
||||||
import type { FrameLocator } from '@playwright/test';
|
import type { FrameLocator } from '@playwright/test';
|
||||||
|
import { rafraf } from 'tests/page/pageTest';
|
||||||
|
|
||||||
const test = playwrightTest.extend<TraceViewerFixtures>(traceViewerFixtures);
|
const test = playwrightTest.extend<TraceViewerFixtures>(traceViewerFixtures);
|
||||||
|
|
||||||
|
|
@ -1439,6 +1440,32 @@ test.skip('should allow showing screenshots instead of snapshots', async ({ runA
|
||||||
await expect(screenshot).toBeVisible();
|
await expect(screenshot).toBeVisible();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('canvas clipping', async ({ runAndTrace, page, server }) => {
|
||||||
|
const traceViewer = await runAndTrace(async () => {
|
||||||
|
await page.goto(server.PREFIX + '/screenshots/canvas.html#canvas-on-edge');
|
||||||
|
await rafraf(page, 5);
|
||||||
|
});
|
||||||
|
|
||||||
|
const msg = await traceViewer.page.waitForEvent('console', { predicate: msg => msg.text().startsWith('canvas drawn:') });
|
||||||
|
expect(msg.text()).toEqual('canvas drawn: [0,91,12,111]');
|
||||||
|
|
||||||
|
const snapshot = await traceViewer.snapshotFrame('page.goto');
|
||||||
|
await expect(snapshot.locator('canvas')).toHaveAttribute('title', `Playwright couldn't capture full canvas contents because it's located partially outside the viewport.`);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('canvas clipping in iframe', async ({ runAndTrace, page, server }) => {
|
||||||
|
const traceViewer = await runAndTrace(async () => {
|
||||||
|
await page.setContent(`
|
||||||
|
<iframe src="${server.PREFIX}/screenshots/canvas.html#canvas-on-edge"></iframe>
|
||||||
|
`);
|
||||||
|
await rafraf(page, 5);
|
||||||
|
});
|
||||||
|
|
||||||
|
const snapshot = await traceViewer.snapshotFrame('page.evaluate');
|
||||||
|
const canvas = snapshot.locator('iframe').contentFrame().locator('canvas');
|
||||||
|
await expect(canvas).toHaveAttribute('title', `Playwright displays canvas contents on a best-effort basis. It doesn't support canvas elements inside an iframe yet. If this impacts your workflow, please open an issue so we can prioritize.`);
|
||||||
|
});
|
||||||
|
|
||||||
test.skip('should handle case where neither snapshots nor screenshots exist', async ({ runAndTrace, page, server }) => {
|
test.skip('should handle case where neither snapshots nor screenshots exist', async ({ runAndTrace, page, server }) => {
|
||||||
const traceViewer = await runAndTrace(async () => {
|
const traceViewer = await runAndTrace(async () => {
|
||||||
await page.goto(server.PREFIX + '/one-style.html');
|
await page.goto(server.PREFIX + '/one-style.html');
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue