chore: elementHandle getters implemented through Frame (#23557)

This is a step towards not using handles for locator operations.
This commit is contained in:
Dmitry Gozman 2023-06-09 07:18:13 -07:00 committed by GitHub
parent c242dfac4c
commit 734705e9b3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 116 additions and 149 deletions

View file

@ -333,7 +333,7 @@ export class CRPage implements PageDelegate {
injected.setInputFiles(node, files), files);
}
async setInputFilePaths(handle: dom.ElementHandle<HTMLInputElement>, files: string[]): Promise<void> {
async setInputFilePaths(progress: Progress, handle: dom.ElementHandle<HTMLInputElement>, files: string[]): Promise<void> {
const frame = await handle.ownerFrame();
if (!frame)
throw new Error('Cannot set input files to detached input element');

View file

@ -66,54 +66,54 @@ export class ElementHandleDispatcher extends JSHandleDispatcher implements chann
}
async getAttribute(params: channels.ElementHandleGetAttributeParams, metadata: CallMetadata): Promise<channels.ElementHandleGetAttributeResult> {
const value = await this._elementHandle.getAttribute(params.name);
const value = await this._elementHandle.getAttribute(metadata, params.name);
return { value: value === null ? undefined : value };
}
async inputValue(params: channels.ElementHandleInputValueParams, metadata: CallMetadata): Promise<channels.ElementHandleInputValueResult> {
const value = await this._elementHandle.inputValue();
const value = await this._elementHandle.inputValue(metadata);
return { value };
}
async textContent(params: channels.ElementHandleTextContentParams, metadata: CallMetadata): Promise<channels.ElementHandleTextContentResult> {
const value = await this._elementHandle.textContent();
const value = await this._elementHandle.textContent(metadata);
return { value: value === null ? undefined : value };
}
async innerText(params: channels.ElementHandleInnerTextParams, metadata: CallMetadata): Promise<channels.ElementHandleInnerTextResult> {
return { value: await this._elementHandle.innerText() };
return { value: await this._elementHandle.innerText(metadata) };
}
async innerHTML(params: channels.ElementHandleInnerHTMLParams, metadata: CallMetadata): Promise<channels.ElementHandleInnerHTMLResult> {
return { value: await this._elementHandle.innerHTML() };
return { value: await this._elementHandle.innerHTML(metadata) };
}
async isChecked(params: channels.ElementHandleIsCheckedParams, metadata: CallMetadata): Promise<channels.ElementHandleIsCheckedResult> {
return { value: await this._elementHandle.isChecked() };
return { value: await this._elementHandle.isChecked(metadata) };
}
async isDisabled(params: channels.ElementHandleIsDisabledParams, metadata: CallMetadata): Promise<channels.ElementHandleIsDisabledResult> {
return { value: await this._elementHandle.isDisabled() };
return { value: await this._elementHandle.isDisabled(metadata) };
}
async isEditable(params: channels.ElementHandleIsEditableParams, metadata: CallMetadata): Promise<channels.ElementHandleIsEditableResult> {
return { value: await this._elementHandle.isEditable() };
return { value: await this._elementHandle.isEditable(metadata) };
}
async isEnabled(params: channels.ElementHandleIsEnabledParams, metadata: CallMetadata): Promise<channels.ElementHandleIsEnabledResult> {
return { value: await this._elementHandle.isEnabled() };
return { value: await this._elementHandle.isEnabled(metadata) };
}
async isHidden(params: channels.ElementHandleIsHiddenParams, metadata: CallMetadata): Promise<channels.ElementHandleIsHiddenResult> {
return { value: await this._elementHandle.isHidden() };
return { value: await this._elementHandle.isHidden(metadata) };
}
async isVisible(params: channels.ElementHandleIsVisibleParams, metadata: CallMetadata): Promise<channels.ElementHandleIsVisibleResult> {
return { value: await this._elementHandle.isVisible() };
return { value: await this._elementHandle.isVisible(metadata) };
}
async dispatchEvent(params: channels.ElementHandleDispatchEventParams, metadata: CallMetadata): Promise<void> {
await this._elementHandle.dispatchEvent(params.type, parseArgument(params.eventInit));
await this._elementHandle.dispatchEvent(metadata, params.type, parseArgument(params.eventInit));
}
async scrollIntoViewIfNeeded(params: channels.ElementHandleScrollIntoViewIfNeededParams, metadata: CallMetadata): Promise<void> {

View file

@ -192,55 +192,28 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
return this._page._delegate.getContentFrame(this);
}
async getAttribute(name: string): Promise<string | null> {
return throwRetargetableDOMError(await this.evaluateInUtility(([injected, node, name]) => {
if (node.nodeType !== Node.ELEMENT_NODE)
throw injected.createStacklessError('Node is not an element');
const element = node as unknown as Element;
return { value: element.getAttribute(name) };
}, name)).value;
async getAttribute(metadata: CallMetadata, name: string): Promise<string | null> {
return this._frame.getAttribute(metadata, ':scope', name, {}, this);
}
async inputValue(): Promise<string> {
return throwRetargetableDOMError(await this.evaluateInUtility(([injected, node]) => {
const element = injected.retarget(node, 'follow-label');
if (!element || (element.nodeName !== 'INPUT' && element.nodeName !== 'TEXTAREA' && element.nodeName !== 'SELECT'))
throw injected.createStacklessError('Node is not an <input>, <textarea> or <select> element');
return { value: (element as HTMLInputElement | HTMLTextAreaElement).value };
}, undefined)).value;
async inputValue(metadata: CallMetadata): Promise<string> {
return this._frame.inputValue(metadata, ':scope', {}, this);
}
async textContent(): Promise<string | null> {
return throwRetargetableDOMError(await this.evaluateInUtility(([injected, node]) => {
return { value: node.textContent };
}, undefined)).value;
async textContent(metadata: CallMetadata): Promise<string | null> {
return this._frame.textContent(metadata, ':scope', {}, this);
}
async innerText(): Promise<string> {
return throwRetargetableDOMError(await this.evaluateInUtility(([injected, node]) => {
if (node.nodeType !== Node.ELEMENT_NODE)
throw injected.createStacklessError('Node is not an element');
if ((node as unknown as Element).namespaceURI !== 'http://www.w3.org/1999/xhtml')
throw injected.createStacklessError('Node is not an HTMLElement');
const element = node as unknown as HTMLElement;
return { value: element.innerText };
}, undefined)).value;
async innerText(metadata: CallMetadata): Promise<string> {
return this._frame.innerText(metadata, ':scope', {}, this);
}
async innerHTML(): Promise<string> {
return throwRetargetableDOMError(await this.evaluateInUtility(([injected, node]) => {
if (node.nodeType !== Node.ELEMENT_NODE)
throw injected.createStacklessError('Node is not an element');
const element = node as unknown as Element;
return { value: element.innerHTML };
}, undefined)).value;
async innerHTML(metadata: CallMetadata): Promise<string> {
return this._frame.innerHTML(metadata, ':scope', {}, this);
}
async dispatchEvent(type: string, eventInit: Object = {}) {
const main = await this._frame._mainContext();
await this._page._frameManager.waitForSignalsCreatedBy(null, false /* noWaitFor */, async () => {
return main.evaluate(([injected, node, { type, eventInit }]) => injected.dispatchEvent(node, type, eventInit), [await main.injectedScript(), this, { type, eventInit }] as const);
});
async dispatchEvent(metadata: CallMetadata, type: string, eventInit: Object = {}) {
return this._frame.dispatchEvent(metadata, ':scope', type, eventInit, {}, this);
}
async _scrollRectIntoViewIfNeeded(rect?: types.Rect): Promise<'error:notvisible' | 'error:notconnected' | 'done'> {
@ -634,7 +607,7 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
await this._page._frameManager.waitForSignalsCreatedBy(progress, options.noWaitAfter, async () => {
progress.throwIfAborted(); // Avoid action that has side-effects.
if (localPaths)
await this._page._delegate.setInputFilePaths(retargeted, localPaths);
await this._page._delegate.setInputFilePaths(progress, retargeted, localPaths);
else
await this._page._delegate.setInputFiles(retargeted, filePayloads!);
});
@ -754,51 +727,35 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
}
async evalOnSelector(selector: string, strict: boolean, expression: string, isFunction: boolean | undefined, arg: any): Promise<any> {
const handle = await this._frame.selectors.query(selector, { strict }, this);
if (!handle)
throw new Error(`Error: failed to find element matching selector "${selector}"`);
const result = await handle.evaluateExpression(expression, { isFunction }, arg);
handle.dispose();
return result;
return this._frame.evalOnSelector(selector, strict, expression, isFunction, arg, this);
}
async evalOnSelectorAll(selector: string, expression: string, isFunction: boolean | undefined, arg: any): Promise<any> {
const arrayHandle = await this._frame.selectors.queryArrayInMainWorld(selector, this);
const result = await arrayHandle.evaluateExpression(expression, { isFunction }, arg);
arrayHandle.dispose();
return result;
return this._frame.evalOnSelectorAll(selector, expression, isFunction, arg, this);
}
async isVisible(): Promise<boolean> {
const result = await this.evaluateInUtility(([injected, node]) => injected.elementState(node, 'visible'), {});
if (result === 'error:notconnected')
return false;
return result;
async isVisible(metadata: CallMetadata): Promise<boolean> {
return this._frame.isVisible(metadata, ':scope', {}, this);
}
async isHidden(): Promise<boolean> {
const result = await this.evaluateInUtility(([injected, node]) => injected.elementState(node, 'hidden'), {});
return throwRetargetableDOMError(result);
async isHidden(metadata: CallMetadata): Promise<boolean> {
return this._frame.isHidden(metadata, ':scope', {}, this);
}
async isEnabled(): Promise<boolean> {
const result = await this.evaluateInUtility(([injected, node]) => injected.elementState(node, 'enabled'), {});
return throwRetargetableDOMError(result);
async isEnabled(metadata: CallMetadata): Promise<boolean> {
return this._frame.isEnabled(metadata, ':scope', {}, this);
}
async isDisabled(): Promise<boolean> {
const result = await this.evaluateInUtility(([injected, node]) => injected.elementState(node, 'disabled'), {});
return throwRetargetableDOMError(result);
async isDisabled(metadata: CallMetadata): Promise<boolean> {
return this._frame.isDisabled(metadata, ':scope', {}, this);
}
async isEditable(): Promise<boolean> {
const result = await this.evaluateInUtility(([injected, node]) => injected.elementState(node, 'editable'), {});
return throwRetargetableDOMError(result);
async isEditable(metadata: CallMetadata): Promise<boolean> {
return this._frame.isEditable(metadata, ':scope', {}, this);
}
async isChecked(): Promise<boolean> {
const result = await this.evaluateInUtility(([injected, node]) => injected.elementState(node, 'checked'), {});
return throwRetargetableDOMError(result);
async isChecked(metadata: CallMetadata): Promise<boolean> {
return this._frame.isChecked(metadata, ':scope', {}, this);
}
async waitForElementState(metadata: CallMetadata, state: 'visible' | 'hidden' | 'stable' | 'enabled' | 'disabled' | 'editable', options: types.TimeoutOptions = {}): Promise<void> {

View file

@ -545,15 +545,15 @@ export class FFPage implements PageDelegate {
injected.setInputFiles(node, files), files);
}
async setInputFilePaths(handle: dom.ElementHandle<HTMLInputElement>, files: string[]): Promise<void> {
async setInputFilePaths(progress: Progress, handle: dom.ElementHandle<HTMLInputElement>, files: string[]): Promise<void> {
await Promise.all([
this._session.send('Page.setFileInputFiles', {
frameId: handle._context.frame._id,
objectId: handle._objectId,
files
}),
handle.dispatchEvent('input'),
handle.dispatchEvent('change')
handle.dispatchEvent(progress.metadata, 'input'),
handle.dispatchEvent(progress.metadata, 'change')
]);
}

View file

@ -829,14 +829,14 @@ export class Frame extends SdkObject {
}, this._page._timeoutSettings.timeout(options));
}
async dispatchEvent(metadata: CallMetadata, selector: string, type: string, eventInit: Object = {}, options: types.QueryOnSelectorOptions = {}): Promise<void> {
async dispatchEvent(metadata: CallMetadata, selector: string, type: string, eventInit: Object = {}, options: types.QueryOnSelectorOptions = {}, scope?: dom.ElementHandle): Promise<void> {
await this._callOnElementOnceMatches(metadata, selector, (injectedScript, element, data) => {
injectedScript.dispatchEvent(element, data.type, data.eventInit);
}, { type, eventInit }, { mainWorld: true, ...options });
}, { type, eventInit }, { mainWorld: true, ...options }, scope);
}
async evalOnSelector(selector: string, strict: boolean, expression: string, isFunction: boolean | undefined, arg: any): Promise<any> {
const handle = await this.selectors.query(selector, { strict });
async evalOnSelector(selector: string, strict: boolean, expression: string, isFunction: boolean | undefined, arg: any, scope?: dom.ElementHandle): Promise<any> {
const handle = await this.selectors.query(selector, { strict }, scope);
if (!handle)
throw new Error(`Error: failed to find element matching selector "${selector}"`);
const result = await handle.evaluateExpression(expression, { isFunction }, arg);
@ -844,8 +844,8 @@ export class Frame extends SdkObject {
return result;
}
async evalOnSelectorAll(selector: string, expression: string, isFunction: boolean | undefined, arg: any): Promise<any> {
const arrayHandle = await this.selectors.queryArrayInMainWorld(selector);
async evalOnSelectorAll(selector: string, expression: string, isFunction: boolean | undefined, arg: any, scope?: dom.ElementHandle): Promise<any> {
const arrayHandle = await this.selectors.queryArrayInMainWorld(selector, scope);
const result = await arrayHandle.evaluateExpression(expression, { isFunction }, arg);
arrayHandle.dispose();
return result;
@ -1215,33 +1215,33 @@ export class Frame extends SdkObject {
}, this._page._timeoutSettings.timeout(options));
}
async textContent(metadata: CallMetadata, selector: string, options: types.QueryOnSelectorOptions = {}): Promise<string | null> {
return this._callOnElementOnceMatches(metadata, selector, (injected, element) => element.textContent, undefined, options);
async textContent(metadata: CallMetadata, selector: string, options: types.QueryOnSelectorOptions = {}, scope?: dom.ElementHandle): Promise<string | null> {
return this._callOnElementOnceMatches(metadata, selector, (injected, element) => element.textContent, undefined, options, scope);
}
async innerText(metadata: CallMetadata, selector: string, options: types.QueryOnSelectorOptions = {}): Promise<string> {
async innerText(metadata: CallMetadata, selector: string, options: types.QueryOnSelectorOptions = {}, scope?: dom.ElementHandle): Promise<string> {
return this._callOnElementOnceMatches(metadata, selector, (injectedScript, element) => {
if (element.namespaceURI !== 'http://www.w3.org/1999/xhtml')
throw injectedScript.createStacklessError('Node is not an HTMLElement');
return (element as HTMLElement).innerText;
}, undefined, options);
}, undefined, options, scope);
}
async innerHTML(metadata: CallMetadata, selector: string, options: types.QueryOnSelectorOptions = {}): Promise<string> {
return this._callOnElementOnceMatches(metadata, selector, (injected, element) => element.innerHTML, undefined, options);
async innerHTML(metadata: CallMetadata, selector: string, options: types.QueryOnSelectorOptions = {}, scope?: dom.ElementHandle): Promise<string> {
return this._callOnElementOnceMatches(metadata, selector, (injected, element) => element.innerHTML, undefined, options, scope);
}
async getAttribute(metadata: CallMetadata, selector: string, name: string, options: types.QueryOnSelectorOptions = {}): Promise<string | null> {
return this._callOnElementOnceMatches(metadata, selector, (injected, element, data) => element.getAttribute(data.name), { name }, options);
async getAttribute(metadata: CallMetadata, selector: string, name: string, options: types.QueryOnSelectorOptions = {}, scope?: dom.ElementHandle): Promise<string | null> {
return this._callOnElementOnceMatches(metadata, selector, (injected, element, data) => element.getAttribute(data.name), { name }, options, scope);
}
async inputValue(metadata: CallMetadata, selector: string, options: types.TimeoutOptions & types.StrictOptions = {}): Promise<string> {
async inputValue(metadata: CallMetadata, selector: string, options: types.TimeoutOptions & types.StrictOptions = {}, scope?: dom.ElementHandle): Promise<string> {
return this._callOnElementOnceMatches(metadata, selector, (injectedScript, node) => {
const element = injectedScript.retarget(node, 'follow-label');
if (!element || (element.nodeName !== 'INPUT' && element.nodeName !== 'TEXTAREA' && element.nodeName !== 'SELECT'))
throw injectedScript.createStacklessError('Node is not an <input>, <textarea> or <select> element');
return (element as any).value;
}, undefined, options);
}, undefined, options, scope);
}
async highlight(selector: string) {
@ -1263,25 +1263,25 @@ export class Frame extends SdkObject {
});
}
private async _elementState(metadata: CallMetadata, selector: string, state: ElementStateWithoutStable, options: types.QueryOnSelectorOptions = {}): Promise<boolean> {
private async _elementState(metadata: CallMetadata, selector: string, state: ElementStateWithoutStable, options: types.QueryOnSelectorOptions = {}, scope?: dom.ElementHandle): Promise<boolean> {
const result = await this._callOnElementOnceMatches(metadata, selector, (injected, element, data) => {
return injected.elementState(element, data.state);
}, { state }, options);
}, { state }, options, scope);
return dom.throwRetargetableDOMError(result);
}
async isVisible(metadata: CallMetadata, selector: string, options: types.StrictOptions = {}): Promise<boolean> {
async isVisible(metadata: CallMetadata, selector: string, options: types.StrictOptions = {}, scope?: dom.ElementHandle): Promise<boolean> {
const controller = new ProgressController(metadata, this);
return controller.run(async progress => {
progress.log(` checking visibility of ${this._asLocator(selector)}`);
const resolved = await this.selectors.resolveInjectedForSelector(selector, options);
const resolved = await this.selectors.resolveInjectedForSelector(selector, options, scope);
if (!resolved)
return false;
return await resolved.injected.evaluate((injected, { info }) => {
const element = injected.querySelector(info.parsed, document, info.strict);
return await resolved.injected.evaluate((injected, { info, root }) => {
const element = injected.querySelector(info.parsed, root || document, info.strict);
const state = element ? injected.elementState(element, 'visible') : false;
return state === 'error:notconnected' ? false : state;
}, { info: resolved.info });
}, { info: resolved.info, root: resolved.frame === this ? scope : undefined });
}, this._page._timeoutSettings.timeout({})).catch(e => {
if (js.isJavaScriptErrorInEvaluate(e) || isInvalidSelectorError(e) || isSessionClosedError(e))
throw e;
@ -1289,24 +1289,24 @@ export class Frame extends SdkObject {
});
}
async isHidden(metadata: CallMetadata, selector: string, options: types.StrictOptions = {}): Promise<boolean> {
return !(await this.isVisible(metadata, selector, options));
async isHidden(metadata: CallMetadata, selector: string, options: types.StrictOptions = {}, scope?: dom.ElementHandle): Promise<boolean> {
return !(await this.isVisible(metadata, selector, options, scope));
}
async isDisabled(metadata: CallMetadata, selector: string, options: types.QueryOnSelectorOptions = {}): Promise<boolean> {
return this._elementState(metadata, selector, 'disabled', options);
async isDisabled(metadata: CallMetadata, selector: string, options: types.QueryOnSelectorOptions = {}, scope?: dom.ElementHandle): Promise<boolean> {
return this._elementState(metadata, selector, 'disabled', options, scope);
}
async isEnabled(metadata: CallMetadata, selector: string, options: types.QueryOnSelectorOptions = {}): Promise<boolean> {
return this._elementState(metadata, selector, 'enabled', options);
async isEnabled(metadata: CallMetadata, selector: string, options: types.QueryOnSelectorOptions = {}, scope?: dom.ElementHandle): Promise<boolean> {
return this._elementState(metadata, selector, 'enabled', options, scope);
}
async isEditable(metadata: CallMetadata, selector: string, options: types.QueryOnSelectorOptions = {}): Promise<boolean> {
return this._elementState(metadata, selector, 'editable', options);
async isEditable(metadata: CallMetadata, selector: string, options: types.QueryOnSelectorOptions = {}, scope?: dom.ElementHandle): Promise<boolean> {
return this._elementState(metadata, selector, 'editable', options, scope);
}
async isChecked(metadata: CallMetadata, selector: string, options: types.QueryOnSelectorOptions = {}): Promise<boolean> {
return this._elementState(metadata, selector, 'checked', options);
async isChecked(metadata: CallMetadata, selector: string, options: types.QueryOnSelectorOptions = {}, scope?: dom.ElementHandle): Promise<boolean> {
return this._elementState(metadata, selector, 'checked', options, scope);
}
async hover(metadata: CallMetadata, selector: string, options: types.PointerActionOptions & types.PointerActionWaitOptions & types.NavigatingActionWaitOptions = {}) {
@ -1549,26 +1549,26 @@ export class Frame extends SdkObject {
this._parentFrame = null;
}
private async _callOnElementOnceMatches<T, R>(metadata: CallMetadata, selector: string, body: ElementCallback<T, R>, taskData: T, options: types.TimeoutOptions & types.StrictOptions & { mainWorld?: boolean } = {}): Promise<R> {
private async _callOnElementOnceMatches<T, R>(metadata: CallMetadata, selector: string, body: ElementCallback<T, R>, taskData: T, options: types.TimeoutOptions & types.StrictOptions & { mainWorld?: boolean } = {}, scope?: dom.ElementHandle): Promise<R> {
const callbackText = body.toString();
const controller = new ProgressController(metadata, this);
return controller.run(async progress => {
progress.log(`waiting for ${this._asLocator(selector)}`);
return this.retryWithProgressAndTimeouts(progress, [0, 20, 50, 100, 100, 500], async continuePolling => {
const resolved = await this.selectors.resolveInjectedForSelector(selector, options);
const promise = this.retryWithProgressAndTimeouts(progress, [0, 20, 50, 100, 100, 500], async continuePolling => {
const resolved = await this.selectors.resolveInjectedForSelector(selector, options, scope);
progress.throwIfAborted();
if (!resolved)
return continuePolling;
const { log, success, value } = await resolved.injected.evaluate((injected, { info, callbackText, taskData, callId }) => {
const { log, success, value } = await resolved.injected.evaluate((injected, { info, callbackText, taskData, callId, root }) => {
const callback = injected.eval(callbackText) as ElementCallback<T, R>;
const element = injected.querySelector(info.parsed, document, info.strict);
const element = injected.querySelector(info.parsed, root || document, info.strict);
if (!element)
return { success: false };
const log = ` locator resolved to ${injected.previewNode(element)}`;
if (callId)
injected.markTargetElements(new Set([element]), callId);
return { log, success: true, value: callback(injected, element, taskData as T) };
}, { info: resolved.info, callbackText, taskData, callId: progress.metadata.id });
}, { info: resolved.info, callbackText, taskData, callId: progress.metadata.id, root: resolved.frame === this ? scope : undefined });
if (log)
progress.log(log);
@ -1576,6 +1576,7 @@ export class Frame extends SdkObject {
return continuePolling;
return value!;
});
return scope ? scope._context._raceAgainstContextDestroyed(promise) : promise;
}, this._page._timeoutSettings.timeout(options));
}

View file

@ -78,7 +78,7 @@ export interface PageDelegate {
getOwnerFrame(handle: dom.ElementHandle): Promise<string | null>; // Returns frameId.
getContentQuads(handle: dom.ElementHandle): Promise<types.Quad[] | null>;
setInputFiles(handle: dom.ElementHandle<HTMLInputElement>, files: types.FilePayload[]): Promise<void>;
setInputFilePaths(handle: dom.ElementHandle<HTMLInputElement>, files: string[]): Promise<void>;
setInputFilePaths(progress: Progress, handle: dom.ElementHandle<HTMLInputElement>, files: string[]): Promise<void>;
getBoundingBox(handle: dom.ElementHandle): Promise<types.Rect | null>;
getFrameElement(frame: frames.Frame): Promise<dom.ElementHandle>;
scrollRectIntoViewIfNeeded(handle: dom.ElementHandle, rect?: types.Rect): Promise<'error:notvisible' | 'error:notconnected' | 'done'>;

View file

@ -28,7 +28,7 @@ import { assert, createGuid, monotonicTime } from '../../../utils';
import { mkdirIfNeeded, removeFolders } from '../../../utils/fileUtils';
import { Artifact } from '../../artifact';
import { BrowserContext } from '../../browserContext';
import { ElementHandle } from '../../dom';
import type { ElementHandle } from '../../dom';
import type { APIRequestContext } from '../../fetch';
import type { CallMetadata, InstrumentationListener } from '../../instrumentation';
import { SdkObject } from '../../instrumentation';
@ -342,10 +342,6 @@ export class Tracing extends SdkObject implements InstrumentationListener, Snaps
return;
if (!shouldCaptureSnapshot(metadata))
return;
// 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, metadata.id, snapshotName, element).catch(() => {});
}

View file

@ -969,7 +969,7 @@ export class WKPage implements PageDelegate {
await this._session.send('DOM.setInputFiles', { objectId, files: protocolFiles });
}
async setInputFilePaths(handle: dom.ElementHandle<HTMLInputElement>, paths: string[]): Promise<void> {
async setInputFilePaths(progress: Progress, handle: dom.ElementHandle<HTMLInputElement>, paths: string[]): Promise<void> {
const pageProxyId = this._pageProxySession.sessionId;
const objectId = handle._objectId;
await Promise.all([

View file

@ -19,6 +19,7 @@ import { traceViewerFixtures } from '../config/traceViewerFixtures';
import fs from 'fs';
import path from 'path';
import { expect, playwrightTest } from '../config/browserTest';
import type { FrameLocator } from '@playwright/test';
const test = playwrightTest.extend<TraceViewerFixtures>(traceViewerFixtures);
@ -558,39 +559,51 @@ test('should register custom elements', async ({ page, server, runAndTrace }) =>
test('should highlight target elements', async ({ page, runAndTrace, browserName }) => {
const traceViewer = await runAndTrace(async () => {
await page.setContent(`
<div>hello</div>
<div>world</div>
<div>t1</div>
<div>t2</div>
<div>t3</div>
<div>t4</div>
<div>t5</div>
<div>t6</div>
<div>multi</div>
<div>multi</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(() => {});
await page.click('text=t1');
await page.innerText('text=t2');
await (await page.$('text=t3')).click();
await (await page.$('text=t4')).innerText();
await page.locator('text=t5').innerText();
await expect(page.locator('text=t6')).toHaveText(/t6/i);
await expect(page.locator('text=multi')).toHaveText(['a', 'b'], { timeout: 1000 }).catch(() => {});
});
async function highlightedDivs(frameLocator: FrameLocator) {
return frameLocator.locator('div').evaluateAll(divs => {
// See snapshotRenderer.ts for the exact color.
return divs.filter(div => getComputedStyle(div).backgroundColor === 'rgba(111, 168, 220, 0.498)').map(div => div.textContent);
});
}
const framePageClick = await traceViewer.snapshotFrame('page.click');
await expect(framePageClick.locator('[__playwright_target__]')).toHaveText(['hello']);
await expect.poll(() => highlightedDivs(framePageClick)).toEqual(['t1']);
const framePageInnerText = await traceViewer.snapshotFrame('page.innerText');
await expect(framePageInnerText.locator('[__playwright_target__]')).toHaveText(['hello']);
await expect.poll(() => highlightedDivs(framePageInnerText)).toEqual(['t2']);
const frameHandleClick = await traceViewer.snapshotFrame('elementHandle.click');
await expect(frameHandleClick.locator('[__playwright_target__]')).toHaveText(['hello']);
await expect.poll(() => highlightedDivs(frameHandleClick)).toEqual(['t3']);
const frameHandleInnerText = await traceViewer.snapshotFrame('elementHandle.innerText');
await expect(frameHandleInnerText.locator('[__playwright_target__]')).toHaveText(['hello']);
await expect.poll(() => highlightedDivs(frameHandleInnerText)).toEqual(['t4']);
const frameLocatorInnerText = await traceViewer.snapshotFrame('locator.innerText');
await expect(frameLocatorInnerText.locator('[__playwright_target__]')).toHaveText(['hello']);
await expect.poll(() => highlightedDivs(frameLocatorInnerText)).toEqual(['t5']);
const frameExpect1 = await traceViewer.snapshotFrame('expect.toHaveText', 0);
await expect(frameExpect1.locator('[__playwright_target__]')).toHaveText(['hello']);
await expect.poll(() => highlightedDivs(frameExpect1)).toEqual(['t6']);
const frameExpect2 = await traceViewer.snapshotFrame('expect.toHaveText', 1);
await expect(frameExpect2.locator('[__playwright_target__]')).toHaveText(['hello', 'world']);
await expect.poll(() => highlightedDivs(frameExpect2)).toEqual(['multi', 'multi']);
});
test('should show action source', async ({ showTraceViewer }) => {