mirror of
https://github.com/microsoft/playwright.git
synced 2024-12-11 12:33:45 +03:00
chore: show snapshots for sync assertions (#21775)
This commit is contained in:
parent
32d33cb8d5
commit
22e11a12ab
@ -104,7 +104,7 @@ export class SnapshotRenderer {
|
||||
const prefix = snapshot.doctype ? `<!DOCTYPE ${snapshot.doctype}>` : '';
|
||||
html = prefix + [
|
||||
'<style>*,*::before,*::after { visibility: hidden }</style>',
|
||||
`<style>*[__playwright_target__="${this._callId}"] { background-color: #6fa8dc7f; }</style>`,
|
||||
`<style>*[__playwright_target__="${this._callId}"] { outline: 2px solid #006ab1 !important; background-color: #6fa8dc7f !important; }</style>`,
|
||||
`<script>${snapshotScript()}</script>`
|
||||
].join('') + html;
|
||||
|
||||
|
@ -22,7 +22,8 @@ import type { ContextEntry, PageEntry } from '../entries';
|
||||
import type { SerializedError, StackFrame } from '@protocol/channels';
|
||||
|
||||
const contextSymbol = Symbol('context');
|
||||
const nextSymbol = Symbol('next');
|
||||
const nextInContextSymbol = Symbol('next');
|
||||
const prevInListSymbol = Symbol('prev');
|
||||
const eventsSymbol = Symbol('events');
|
||||
const resourcesSymbol = Symbol('resources');
|
||||
|
||||
@ -78,7 +79,7 @@ function indexModel(context: ContextEntry) {
|
||||
for (let i = 0; i < context.actions.length; ++i) {
|
||||
const action = context.actions[i] as any;
|
||||
action[contextSymbol] = context;
|
||||
action[nextSymbol] = context.actions[i + 1];
|
||||
action[nextInContextSymbol] = context.actions[i + 1];
|
||||
}
|
||||
for (const event of context.events)
|
||||
(event as any)[contextSymbol] = context;
|
||||
@ -114,15 +115,22 @@ function dedupeActions(actions: ActionTraceEvent[]) {
|
||||
result.push(expectAction);
|
||||
}
|
||||
|
||||
return result.sort((a1, a2) => a1.startTime - a2.startTime);
|
||||
result.sort((a1, a2) => a1.startTime - a2.startTime);
|
||||
for (let i = 1; i < result.length; ++i)
|
||||
(result[i] as any)[prevInListSymbol] = result[i - 1];
|
||||
return result;
|
||||
}
|
||||
|
||||
export function context(action: ActionTraceEvent): ContextEntry {
|
||||
return (action as any)[contextSymbol];
|
||||
}
|
||||
|
||||
function next(action: ActionTraceEvent): ActionTraceEvent {
|
||||
return (action as any)[nextSymbol];
|
||||
function nextInContext(action: ActionTraceEvent): ActionTraceEvent {
|
||||
return (action as any)[nextInContextSymbol];
|
||||
}
|
||||
|
||||
export function prevInList(action: ActionTraceEvent): ActionTraceEvent {
|
||||
return (action as any)[prevInListSymbol];
|
||||
}
|
||||
|
||||
export function stats(action: ActionTraceEvent): { errors: number, warnings: number } {
|
||||
@ -149,7 +157,7 @@ export function eventsForAction(action: ActionTraceEvent): EventTraceEvent[] {
|
||||
if (result)
|
||||
return result;
|
||||
|
||||
const nextAction = next(action);
|
||||
const nextAction = nextInContext(action);
|
||||
result = context(action).events.filter(event => {
|
||||
return event.time >= action.startTime && (!nextAction || event.time < nextAction.startTime);
|
||||
});
|
||||
@ -162,7 +170,7 @@ export function resourcesForAction(action: ActionTraceEvent): ResourceSnapshot[]
|
||||
if (result)
|
||||
return result;
|
||||
|
||||
const nextAction = next(action);
|
||||
const nextAction = nextInContext(action);
|
||||
result = context(action).resources.filter(resource => {
|
||||
return typeof resource._monotonicTime === 'number' && resource._monotonicTime > action.startTime && (!nextAction || resource._monotonicTime < nextAction.startTime);
|
||||
});
|
||||
|
@ -18,7 +18,7 @@ import './snapshotTab.css';
|
||||
import * as React from 'react';
|
||||
import { useMeasure } from './helpers';
|
||||
import type { ActionTraceEvent } from '@trace/trace';
|
||||
import { context } from './modelUtil';
|
||||
import { context, prevInList } from './modelUtil';
|
||||
import { CodeMirrorWrapper } from '@web/components/codeMirrorWrapper';
|
||||
import { Toolbar } from '@web/components/toolbar';
|
||||
import { ToolbarButton } from '@web/components/toolbarButton';
|
||||
@ -36,49 +36,46 @@ export const SnapshotTab: React.FunctionComponent<{
|
||||
testIdAttributeName: string,
|
||||
}> = ({ action, sdkLanguage, testIdAttributeName }) => {
|
||||
const [measure, ref] = useMeasure<HTMLDivElement>();
|
||||
const [snapshotIndex, setSnapshotIndex] = React.useState(0);
|
||||
const [snapshotTab, setSnapshotTab] = React.useState<'action'|'before'|'after'>('action');
|
||||
const [isInspecting, setIsInspecting] = React.useState(false);
|
||||
const [highlightedLocator, setHighlightedLocator] = React.useState<string>('');
|
||||
const [pickerVisible, setPickerVisible] = React.useState(false);
|
||||
|
||||
const { snapshots, snapshotInfoUrl, snapshotUrl, pointX, pointY, popoutUrl } = React.useMemo(() => {
|
||||
const actionSnapshot = action?.inputSnapshot || action?.afterSnapshot;
|
||||
const snapshots = [
|
||||
actionSnapshot ? { title: 'action', snapshotName: actionSnapshot } : undefined,
|
||||
action?.beforeSnapshot ? { title: 'before', snapshotName: action?.beforeSnapshot } : undefined,
|
||||
action?.afterSnapshot ? { title: 'after', snapshotName: action.afterSnapshot } : undefined,
|
||||
].filter(Boolean) as { title: string, snapshotName: string }[];
|
||||
const { snapshots } = React.useMemo(() => {
|
||||
if (!action)
|
||||
return { snapshots: {} };
|
||||
|
||||
let snapshotUrl = 'data:text/html,<body style="background: #ddd"></body>';
|
||||
let popoutUrl: string | undefined;
|
||||
let snapshotInfoUrl: string | undefined;
|
||||
let pointX: number | undefined;
|
||||
let pointY: number | undefined;
|
||||
if (action) {
|
||||
const snapshot = snapshots[snapshotIndex];
|
||||
if (snapshot && snapshot.snapshotName) {
|
||||
const params = new URLSearchParams();
|
||||
params.set('trace', context(action).traceUrl);
|
||||
params.set('name', snapshot.snapshotName);
|
||||
snapshotUrl = new URL(`snapshot/${action.pageId}?${params.toString()}`, window.location.href).toString();
|
||||
snapshotInfoUrl = new URL(`snapshotInfo/${action.pageId}?${params.toString()}`, window.location.href).toString();
|
||||
if (snapshot.title === 'action') {
|
||||
pointX = action.point?.x;
|
||||
pointY = action.point?.y;
|
||||
}
|
||||
const popoutParams = new URLSearchParams();
|
||||
popoutParams.set('r', snapshotUrl);
|
||||
popoutParams.set('trace', context(action).traceUrl);
|
||||
popoutUrl = new URL(`popout.html?${popoutParams.toString()}`, window.location.href).toString();
|
||||
}
|
||||
// if the action has no beforeSnapshot, use the last available afterSnapshot.
|
||||
let beforeSnapshot = action.beforeSnapshot ? { action, snapshotName: action.beforeSnapshot } : undefined;
|
||||
let a = action;
|
||||
while (!beforeSnapshot && a) {
|
||||
a = prevInList(a);
|
||||
beforeSnapshot = a?.afterSnapshot ? { action: a, snapshotName: a?.afterSnapshot } : undefined;
|
||||
}
|
||||
return { snapshots, snapshotInfoUrl, snapshotUrl, pointX, pointY, popoutUrl };
|
||||
}, [action, snapshotIndex]);
|
||||
const afterSnapshot = action.afterSnapshot ? { action, snapshotName: action.afterSnapshot } : beforeSnapshot;
|
||||
const actionSnapshot = action.inputSnapshot ? { action, snapshotName: action.inputSnapshot } : afterSnapshot;
|
||||
return { snapshots: { action: actionSnapshot, before: beforeSnapshot, after: afterSnapshot } };
|
||||
}, [action]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (snapshots.length >= 1 && snapshotIndex >= snapshots.length)
|
||||
setSnapshotIndex(snapshots.length - 1);
|
||||
}, [snapshotIndex, snapshots]);
|
||||
const { snapshotInfoUrl, snapshotUrl, pointX, pointY, popoutUrl } = React.useMemo(() => {
|
||||
const snapshot = snapshots[snapshotTab];
|
||||
if (!snapshot)
|
||||
return { snapshotUrl: kBlankSnapshotUrl };
|
||||
|
||||
const params = new URLSearchParams();
|
||||
params.set('trace', context(snapshot.action).traceUrl);
|
||||
params.set('name', snapshot.snapshotName);
|
||||
const snapshotUrl = new URL(`snapshot/${snapshot.action.pageId}?${params.toString()}`, window.location.href).toString();
|
||||
const snapshotInfoUrl = new URL(`snapshotInfo/${snapshot.action.pageId}?${params.toString()}`, window.location.href).toString();
|
||||
|
||||
const pointX = snapshotTab === 'action' ? snapshot.action.point?.x : undefined;
|
||||
const pointY = snapshotTab === 'action' ? snapshot.action.point?.y : undefined;
|
||||
const popoutParams = new URLSearchParams();
|
||||
popoutParams.set('r', snapshotUrl);
|
||||
popoutParams.set('trace', context(snapshot.action).traceUrl);
|
||||
const popoutUrl = new URL(`popout.html?${popoutParams.toString()}`, window.location.href).toString();
|
||||
return { snapshots, snapshotInfoUrl, snapshotUrl, pointX, pointY, popoutUrl };
|
||||
}, [snapshots, snapshotTab]);
|
||||
|
||||
const iframeRef = React.useRef<HTMLIFrameElement>(null);
|
||||
const [snapshotInfo, setSnapshotInfo] = React.useState({ viewport: kDefaultViewport, url: '' });
|
||||
@ -141,12 +138,12 @@ export const SnapshotTab: React.FunctionComponent<{
|
||||
setIsInspecting(!pickerVisible);
|
||||
}}>Pick locator</ToolbarButton>
|
||||
<div style={{ width: 5 }}></div>
|
||||
{snapshots.map((snapshot, index) => {
|
||||
{['action', 'before', 'after'].map(tab => {
|
||||
return <TabbedPaneTab
|
||||
id={snapshot.title}
|
||||
title={renderTitle(snapshot.title)}
|
||||
selected={snapshotIndex === index}
|
||||
onSelect={() => setSnapshotIndex(index)}
|
||||
id={tab}
|
||||
title={renderTitle(tab)}
|
||||
selected={snapshotTab === tab}
|
||||
onSelect={() => setSnapshotTab(tab as 'action' | 'before' | 'after')}
|
||||
></TabbedPaneTab>;
|
||||
})}
|
||||
<div style={{ flex: 'auto' }}></div>
|
||||
@ -168,7 +165,7 @@ export const SnapshotTab: React.FunctionComponent<{
|
||||
}}></ToolbarButton>
|
||||
</Toolbar>}
|
||||
<div ref={ref} className='snapshot-wrapper'>
|
||||
{ snapshots.length ? <div className='snapshot-container' style={{
|
||||
<div className='snapshot-container' style={{
|
||||
width: snapshotContainerSize.width + 'px',
|
||||
height: snapshotContainerSize.height + 'px',
|
||||
transform: `translate(${translate.x}px, ${translate.y}px) scale(${scale})`,
|
||||
@ -189,8 +186,7 @@ export const SnapshotTab: React.FunctionComponent<{
|
||||
</div>
|
||||
</div>
|
||||
<iframe ref={iframeRef} id='snapshot' name='snapshot'></iframe>
|
||||
</div> : <div className='no-snapshot'>Action does not have snapshots</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>;
|
||||
};
|
||||
@ -215,8 +211,13 @@ export const InspectModeController: React.FunctionComponent<{
|
||||
}> = ({ iframe, isInspecting, sdkLanguage, testIdAttributeName, highlightedLocator, setHighlightedLocator }) => {
|
||||
React.useEffect(() => {
|
||||
const win = iframe?.contentWindow as any;
|
||||
if (!win || !isInspecting && !highlightedLocator && !win._recorder)
|
||||
try {
|
||||
if (!win || !isInspecting && !highlightedLocator && !win._recorder)
|
||||
return;
|
||||
} catch {
|
||||
// Potential cross-origin exception.
|
||||
return;
|
||||
}
|
||||
let recorder: Recorder | undefined = win._recorder;
|
||||
if (!recorder) {
|
||||
const injectedScript = new InjectedScript(win, false, sdkLanguage, testIdAttributeName, 1, 'chromium', []);
|
||||
@ -240,3 +241,4 @@ export const InspectModeController: React.FunctionComponent<{
|
||||
};
|
||||
|
||||
const kDefaultViewport = { width: 1280, height: 720 };
|
||||
const kBlankSnapshotUrl = 'data:text/html,<body style="background: #ddd"></body>';
|
||||
|
95
tests/playwright-test/ui-mode-trace.spec.ts
Normal file
95
tests/playwright-test/ui-mode-trace.spec.ts
Normal file
@ -0,0 +1,95 @@
|
||||
/**
|
||||
* Copyright (c) Microsoft Corporation.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { test, expect } from './ui-mode-fixtures';
|
||||
test.describe.configure({ mode: 'parallel' });
|
||||
|
||||
test('should merge trace events', async ({ runUITest, server }) => {
|
||||
const page = await runUITest({
|
||||
'a.test.ts': `
|
||||
import { test, expect } from '@playwright/test';
|
||||
test('trace test', async ({ page }) => {
|
||||
await page.setContent('<button>Submit</button>');
|
||||
expect(1).toBe(1);
|
||||
await page.getByRole('button').click();
|
||||
expect(2).toBe(2);
|
||||
});
|
||||
`,
|
||||
});
|
||||
|
||||
await page.getByText('trace test').dblclick();
|
||||
|
||||
const listItem = page.getByTestId('action-list').getByRole('listitem');
|
||||
await expect(
|
||||
listItem,
|
||||
'action list'
|
||||
).toHaveText([
|
||||
/browserContext\.newPage[\d.]+m?s/,
|
||||
/page\.setContent[\d.]+m?s/,
|
||||
/expect\.toBe[\d.]+m?s/,
|
||||
/locator\.clickgetByRole\('button'\)[\d.]+m?s/,
|
||||
/expect\.toBe[\d.]+m?s/,
|
||||
]);
|
||||
});
|
||||
|
||||
test('should locate sync assertions in source', async ({ runUITest, server }) => {
|
||||
const page = await runUITest({
|
||||
'a.test.ts': `
|
||||
import { test, expect } from '@playwright/test';
|
||||
test('trace test', async ({}) => {
|
||||
expect(1).toBe(1);
|
||||
});
|
||||
`,
|
||||
});
|
||||
|
||||
await page.getByText('trace test').dblclick();
|
||||
|
||||
await expect(
|
||||
page.locator('.CodeMirror .source-line-running'),
|
||||
'check source tab',
|
||||
).toHaveText('4 expect(1).toBe(1);');
|
||||
});
|
||||
|
||||
test('should show snapshots for sync assertions', async ({ runUITest, server }) => {
|
||||
const page = await runUITest({
|
||||
'a.test.ts': `
|
||||
import { test, expect } from '@playwright/test';
|
||||
test('trace test', async ({ page }) => {
|
||||
await page.setContent('<button>Submit</button>');
|
||||
await page.getByRole('button').click();
|
||||
expect(1).toBe(1);
|
||||
});
|
||||
`,
|
||||
});
|
||||
|
||||
await page.getByText('trace test').dblclick();
|
||||
|
||||
const listItem = page.getByTestId('action-list').getByRole('listitem');
|
||||
await expect(
|
||||
listItem,
|
||||
'action list'
|
||||
).toHaveText([
|
||||
/browserContext\.newPage[\d.]+m?s/,
|
||||
/page\.setContent[\d.]+m?s/,
|
||||
/locator\.clickgetByRole\('button'\)[\d.]+m?s/,
|
||||
/expect\.toBe[\d.]+m?s/,
|
||||
]);
|
||||
|
||||
await expect(
|
||||
page.frameLocator('id=snapshot').locator('button'),
|
||||
'verify snapshot'
|
||||
).toHaveText('Submit');
|
||||
});
|
Loading…
Reference in New Issue
Block a user