fix(trace viewer): clear old highlighted elements upon change (#32917)

When the list of highlighted elements changes over time, we should
update the elements marked as `__playwright_target__` in the snapshot.

A good example is an `expect(locator).toHaveText([...])` where the list
of elements changes from 4 items to 3 after clicking a "Delete" button.
This commit is contained in:
Dmitry Gozman 2024-10-02 23:48:26 -07:00 committed by GitHub
parent 616425a0fb
commit 3c5967d4f5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 40 additions and 5 deletions

View file

@ -65,6 +65,7 @@ export class InjectedScript {
readonly isUnderTest: boolean; readonly isUnderTest: boolean;
private _sdkLanguage: Language; private _sdkLanguage: Language;
private _testIdAttributeNameForStrictErrorAndConsoleCodegen: string = 'data-testid'; private _testIdAttributeNameForStrictErrorAndConsoleCodegen: string = 'data-testid';
private _markedElements?: { callId: string, elements: Set<Element> };
// eslint-disable-next-line no-restricted-globals // eslint-disable-next-line no-restricted-globals
readonly window: Window & typeof globalThis; readonly window: Window & typeof globalThis;
readonly document: Document; readonly document: Document;
@ -1081,14 +1082,33 @@ export class InjectedScript {
} }
markTargetElements(markedElements: Set<Element>, callId: string) { markTargetElements(markedElements: Set<Element>, callId: string) {
const customEvent = new CustomEvent('__playwright_target__', { if (this._markedElements?.callId !== callId)
this._markedElements = undefined;
const previous = this._markedElements?.elements || new Set();
const unmarkEvent = new CustomEvent('__playwright_unmark_target__', {
bubbles: true, bubbles: true,
cancelable: true, cancelable: true,
detail: callId, detail: callId,
composed: true, composed: true,
}); });
for (const element of markedElements) for (const element of previous) {
element.dispatchEvent(customEvent); if (!markedElements.has(element))
element.dispatchEvent(unmarkEvent);
}
const markEvent = new CustomEvent('__playwright_mark_target__', {
bubbles: true,
cancelable: true,
detail: callId,
composed: true,
});
for (const element of markedElements) {
if (!previous.has(element))
element.dispatchEvent(markEvent);
}
this._markedElements = { callId, elements: markedElements };
} }
private _setupGlobalListenersRemovalDetection() { private _setupGlobalListenersRemovalDetection() {

View file

@ -139,12 +139,19 @@ export function frameSnapshotStreamer(snapshotStreamer: string, removeNoScript:
} }
private _refreshListeners() { private _refreshListeners() {
(document as any).addEventListener('__playwright_target__', (event: CustomEvent) => { (document as any).addEventListener('__playwright_mark_target__', (event: CustomEvent) => {
if (!event.detail) if (!event.detail)
return; return;
const callId = event.detail as string; const callId = event.detail as string;
(event.composedPath()[0] as any).__playwright_target__ = callId; (event.composedPath()[0] as any).__playwright_target__ = callId;
}); });
(document as any).addEventListener('__playwright_unmark_target__', (event: CustomEvent) => {
if (!event.detail)
return;
const callId = event.detail as string;
if ((event.composedPath()[0] as any).__playwright_target__ === callId)
delete (event.composedPath()[0] as any).__playwright_target__;
});
} }
private _interceptNativeMethod(obj: any, method: string, cb: (thisObj: any, result: any) => void) { private _interceptNativeMethod(obj: any, method: string, cb: (thisObj: any, result: any) => void) {

View file

@ -761,7 +761,7 @@ test('should highlight target elements', async ({ page, runAndTrace, browserName
await page.setContent(` await page.setContent(`
<div>t1</div> <div>t1</div>
<div>t2</div> <div>t2</div>
<div>t3</div> <div id=div3>t3</div>
<div>t4</div> <div>t4</div>
<div>t5</div> <div>t5</div>
<div>t6</div> <div>t6</div>
@ -778,6 +778,11 @@ test('should highlight target elements', async ({ page, runAndTrace, browserName
await page.mouse.move(123, 234); await page.mouse.move(123, 234);
await page.getByText(/^t\d$/).click().catch(() => {}); await page.getByText(/^t\d$/).click().catch(() => {});
await expect(page.getByText(/t3|t4/)).toBeVisible().catch(() => {}); await expect(page.getByText(/t3|t4/)).toBeVisible().catch(() => {});
const expectPromise = expect(page.getByText(/t3|t4/)).toHaveText(['t4']);
await page.waitForTimeout(1000);
await page.evaluate(() => document.querySelector('#div3').textContent = 'changed');
await expectPromise;
}); });
async function highlightedDivs(frameLocator: FrameLocator) { async function highlightedDivs(frameLocator: FrameLocator) {
@ -825,6 +830,9 @@ test('should highlight target elements', async ({ page, runAndTrace, browserName
const frameExpectStrictViolation = await traceViewer.snapshotFrame('expect.toBeVisible'); const frameExpectStrictViolation = await traceViewer.snapshotFrame('expect.toBeVisible');
await expect.poll(() => highlightedDivs(frameExpectStrictViolation)).toEqual(['t3', 't4']); await expect.poll(() => highlightedDivs(frameExpectStrictViolation)).toEqual(['t3', 't4']);
const frameUpdatedListOfTargets = await traceViewer.snapshotFrame('expect.toHaveText', 2);
await expect.poll(() => highlightedDivs(frameUpdatedListOfTargets)).toEqual(['t4']);
}); });
test('should highlight target element in shadow dom', async ({ page, server, runAndTrace }) => { test('should highlight target element in shadow dom', async ({ page, server, runAndTrace }) => {