feat(trace): highlight targets for accessors and expects (#9527)

This commit is contained in:
Dmitry Gozman 2021-10-15 15:07:15 -07:00 committed by GitHub
parent cd7dfc8448
commit 4ce765c3ae
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 72 additions and 5 deletions

View File

@ -28,6 +28,7 @@ export type CallMetadata = {
apiName?: string;
stack?: StackFrame[];
log: string[];
afterSnapshot?: string;
snapshots: { title: string, snapshotName: string }[];
error?: SerializedError;
result?: any;

View File

@ -1281,9 +1281,10 @@ export class Frame extends SdkObject {
return controller.run(async progress => {
progress.log(`waiting for selector "${selector}"`);
const rerunnableTask = new RerunnableTask(data, progress, injectedScript => {
return injectedScript.evaluateHandle((injected, { info, taskData, callbackText, querySelectorAll, logScale, omitAttached }) => {
return injectedScript.evaluateHandle((injected, { info, taskData, callbackText, querySelectorAll, logScale, omitAttached, snapshotName }) => {
const callback = injected.eval(callbackText) as DomTaskBody<T, R, Element | undefined>;
const poller = logScale ? injected.pollLogScale.bind(injected) : injected.pollRaf.bind(injected);
let markedElements = new Set<Element>();
return poller((progress, continuePolling) => {
let element: Element | undefined;
let elements: Element[] = [];
@ -1293,16 +1294,30 @@ export class Frame extends SdkObject {
progress.logRepeating(` selector resolved to ${elements.length} element${elements.length === 1 ? '' : 's'}`);
} else {
element = injected.querySelector(info.parsed, document, info.strict);
elements = [];
elements = element ? [element] : [];
if (element)
progress.logRepeating(` selector resolved to ${injected.previewNode(element)}`);
}
if (!element && !omitAttached)
return continuePolling;
if (snapshotName) {
const previouslyMarkedElements = markedElements;
markedElements = new Set(elements);
for (const e of previouslyMarkedElements) {
if (!markedElements.has(e))
e.removeAttribute('__playwright_target__');
}
for (const e of markedElements) {
if (!previouslyMarkedElements.has(e))
e.setAttribute('__playwright_target__', snapshotName);
}
}
return callback(progress, element, taskData as T, elements, continuePolling);
});
}, { info, taskData, callbackText, querySelectorAll: options.querySelectorAll, logScale: options.logScale, omitAttached: options.omitAttached });
}, { info, taskData, callbackText, querySelectorAll: options.querySelectorAll, logScale: options.logScale, omitAttached: options.omitAttached, snapshotName: progress.metadata.afterSnapshot });
}, true);
if (this._detached)

View File

@ -19,7 +19,7 @@ import { createGuid } from '../utils/utils';
import type { Browser } from './browser';
import type { BrowserContext } from './browserContext';
import type { BrowserType } from './browserType';
import { ElementHandle } from './dom';
import type { ElementHandle } from './dom';
import type { Frame } from './frames';
import type { Page } from './page';

View File

@ -234,10 +234,17 @@ export class Tracing implements InstrumentationListener, SnapshotterDelegate, Ha
return;
const snapshotName = `${name}@${metadata.id}`;
metadata.snapshots.push({ title: name, snapshotName });
// We have |element| for input actions (page.click and handle.click)
// and |sdkObject| element for accessors like handle.textContent.
if (!element && sdkObject instanceof ElementHandle)
element = sdkObject;
await this._snapshotter.captureSnapshot(sdkObject.attribution.page, snapshotName, element).catch(() => {});
}
async onBeforeCall(sdkObject: SdkObject, metadata: CallMetadata) {
// Set afterSnapshot name for all the actions that operate selectors.
// Elements resolved from selectors will be marked on the snapshot.
metadata.afterSnapshot = `after@${metadata.id}`;
const beforeSnapshot = this._captureSnapshot('before', sdkObject, metadata);
this._pendingCalls.set(metadata.id, { sdkObject, metadata, beforeSnapshot });
await beforeSnapshot;

View File

@ -83,7 +83,11 @@ class TraceViewerPage {
}
async snapshotFrame(actionName: string, ordinal: number = 0, hasSubframe: boolean = false): Promise<Frame> {
await this.selectAction(actionName, ordinal);
const existing = this.page.mainFrame().childFrames()[0];
await Promise.all([
existing ? existing.waitForNavigation() as any : Promise.resolve(),
this.selectAction(actionName, ordinal),
]);
while (this.page.frames().length < (hasSubframe ? 3 : 2))
await this.page.waitForEvent('frameattached');
return this.page.mainFrame().childFrames()[0];
@ -497,3 +501,43 @@ test('should handle src=blob', async ({ page, server, runAndTrace, browserName }
const size = await img.evaluate(e => (e as HTMLImageElement).naturalWidth);
expect(size).toBe(10);
});
test('should highlight target elements', async ({ page, runAndTrace, browserName }) => {
test.skip(browserName === 'firefox');
const traceViewer = await runAndTrace(async () => {
await page.setContent(`
<div>hello</div>
<div>world</div>
`);
await page.click('text=hello');
await page.innerText('text=hello');
const handle = await page.$('text=hello');
await handle.click();
await handle.innerText();
await page.locator('text=hello').innerText();
await expect(page.locator('text=hello')).toHaveText(/hello/i);
await expect(page.locator('div')).toHaveText(['a', 'b'], { timeout: 1000 }).catch(() => {});
});
const framePageClick = await traceViewer.snapshotFrame('page.click');
await expect(framePageClick.locator('[__playwright_target__]')).toHaveText(['hello']);
const framePageInnerText = await traceViewer.snapshotFrame('page.innerText');
await expect(framePageInnerText.locator('[__playwright_target__]')).toHaveText(['hello']);
const frameHandleClick = await traceViewer.snapshotFrame('elementHandle.click');
await expect(frameHandleClick.locator('[__playwright_target__]')).toHaveText(['hello']);
const frameHandleInnerText = await traceViewer.snapshotFrame('elementHandle.innerText');
await expect(frameHandleInnerText.locator('[__playwright_target__]')).toHaveText(['hello']);
const frameLocatorInnerText = await traceViewer.snapshotFrame('locator.innerText');
await expect(frameLocatorInnerText.locator('[__playwright_target__]')).toHaveText(['hello']);
const frameExpect1 = await traceViewer.snapshotFrame('expect.toHaveText', 0);
await expect(frameExpect1.locator('[__playwright_target__]')).toHaveText(['hello']);
const frameExpect2 = await traceViewer.snapshotFrame('expect.toHaveText', 1);
await expect(frameExpect2.locator('[__playwright_target__]')).toHaveText(['hello', 'world']);
});