feat(trace): highlight strict mode violation elements in the snapshot
This commit is contained in:
parent
011034050b
commit
ad3d9dd345
|
|
@ -77,10 +77,6 @@ export class BidiExecutionContext implements js.ExecutionContextDelegate {
|
||||||
throw new js.JavaScriptErrorInEvaluate('Unexpected response type: ' + JSON.stringify(response));
|
throw new js.JavaScriptErrorInEvaluate('Unexpected response type: ' + JSON.stringify(response));
|
||||||
}
|
}
|
||||||
|
|
||||||
rawCallFunctionNoReply(func: Function, ...args: any[]) {
|
|
||||||
throw new Error('Method not implemented.');
|
|
||||||
}
|
|
||||||
|
|
||||||
async evaluateWithArguments(functionDeclaration: string, returnByValue: boolean, utilityScript: js.JSHandle<any>, values: any[], objectIds: string[]): Promise<any> {
|
async evaluateWithArguments(functionDeclaration: string, returnByValue: boolean, utilityScript: js.JSHandle<any>, values: any[], objectIds: string[]): Promise<any> {
|
||||||
const response = await this._session.send('script.callFunction', {
|
const response = await this._session.send('script.callFunction', {
|
||||||
functionDeclaration,
|
functionDeclaration,
|
||||||
|
|
|
||||||
|
|
@ -53,16 +53,6 @@ export class CRExecutionContext implements js.ExecutionContextDelegate {
|
||||||
return remoteObject.objectId!;
|
return remoteObject.objectId!;
|
||||||
}
|
}
|
||||||
|
|
||||||
rawCallFunctionNoReply(func: Function, ...args: any[]) {
|
|
||||||
this._client.send('Runtime.callFunctionOn', {
|
|
||||||
functionDeclaration: func.toString(),
|
|
||||||
arguments: args.map(a => a instanceof js.JSHandle ? { objectId: a._objectId } : { value: a }),
|
|
||||||
returnByValue: true,
|
|
||||||
executionContextId: this._contextId,
|
|
||||||
userGesture: true
|
|
||||||
}).catch(() => {});
|
|
||||||
}
|
|
||||||
|
|
||||||
async evaluateWithArguments(expression: string, returnByValue: boolean, utilityScript: js.JSHandle<any>, values: any[], objectIds: string[]): Promise<any> {
|
async evaluateWithArguments(expression: string, returnByValue: boolean, utilityScript: js.JSHandle<any>, values: any[], objectIds: string[]): Promise<any> {
|
||||||
const { exceptionDetails, result: remoteObject } = await this._client.send('Runtime.callFunctionOn', {
|
const { exceptionDetails, result: remoteObject } = await this._client.send('Runtime.callFunctionOn', {
|
||||||
functionDeclaration: expression,
|
functionDeclaration: expression,
|
||||||
|
|
|
||||||
|
|
@ -421,7 +421,7 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
|
||||||
return maybePoint;
|
return maybePoint;
|
||||||
const point = roundPoint(maybePoint);
|
const point = roundPoint(maybePoint);
|
||||||
progress.metadata.point = point;
|
progress.metadata.point = point;
|
||||||
await progress.beforeInputAction(this);
|
await this.instrumentation.onBeforeInputAction(this, progress.metadata);
|
||||||
|
|
||||||
let hitTargetInterceptionHandle: js.JSHandle<HitTargetInterceptionResult> | undefined;
|
let hitTargetInterceptionHandle: js.JSHandle<HitTargetInterceptionResult> | undefined;
|
||||||
if (force) {
|
if (force) {
|
||||||
|
|
@ -490,9 +490,19 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
|
||||||
return 'done';
|
return 'done';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async _markAsTargetElement(metadata: CallMetadata) {
|
||||||
|
if (!metadata.id)
|
||||||
|
return;
|
||||||
|
await this.evaluateInUtility(([injected, node, callId]) => {
|
||||||
|
if (node.nodeType === 1 /* Node.ELEMENT_NODE */)
|
||||||
|
injected.markTargetElements(new Set([node as Node as Element]), callId);
|
||||||
|
}, metadata.id);
|
||||||
|
}
|
||||||
|
|
||||||
async hover(metadata: CallMetadata, options: types.PointerActionOptions & types.PointerActionWaitOptions): Promise<void> {
|
async hover(metadata: CallMetadata, options: types.PointerActionOptions & types.PointerActionWaitOptions): Promise<void> {
|
||||||
const controller = new ProgressController(metadata, this);
|
const controller = new ProgressController(metadata, this);
|
||||||
return controller.run(async progress => {
|
return controller.run(async progress => {
|
||||||
|
await this._markAsTargetElement(metadata);
|
||||||
const result = await this._hover(progress, options);
|
const result = await this._hover(progress, options);
|
||||||
return assertDone(throwRetargetableDOMError(result));
|
return assertDone(throwRetargetableDOMError(result));
|
||||||
}, this._page._timeoutSettings.timeout(options));
|
}, this._page._timeoutSettings.timeout(options));
|
||||||
|
|
@ -505,6 +515,7 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
|
||||||
async click(metadata: CallMetadata, options: { noWaitAfter?: boolean } & types.MouseClickOptions & types.PointerActionWaitOptions = {}): Promise<void> {
|
async click(metadata: CallMetadata, options: { noWaitAfter?: boolean } & types.MouseClickOptions & types.PointerActionWaitOptions = {}): Promise<void> {
|
||||||
const controller = new ProgressController(metadata, this);
|
const controller = new ProgressController(metadata, this);
|
||||||
return controller.run(async progress => {
|
return controller.run(async progress => {
|
||||||
|
await this._markAsTargetElement(metadata);
|
||||||
const result = await this._click(progress, { ...options, waitAfter: !options.noWaitAfter });
|
const result = await this._click(progress, { ...options, waitAfter: !options.noWaitAfter });
|
||||||
return assertDone(throwRetargetableDOMError(result));
|
return assertDone(throwRetargetableDOMError(result));
|
||||||
}, this._page._timeoutSettings.timeout(options));
|
}, this._page._timeoutSettings.timeout(options));
|
||||||
|
|
@ -517,6 +528,7 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
|
||||||
async dblclick(metadata: CallMetadata, options: types.MouseMultiClickOptions & types.PointerActionWaitOptions): Promise<void> {
|
async dblclick(metadata: CallMetadata, options: types.MouseMultiClickOptions & types.PointerActionWaitOptions): Promise<void> {
|
||||||
const controller = new ProgressController(metadata, this);
|
const controller = new ProgressController(metadata, this);
|
||||||
return controller.run(async progress => {
|
return controller.run(async progress => {
|
||||||
|
await this._markAsTargetElement(metadata);
|
||||||
const result = await this._dblclick(progress, options);
|
const result = await this._dblclick(progress, options);
|
||||||
return assertDone(throwRetargetableDOMError(result));
|
return assertDone(throwRetargetableDOMError(result));
|
||||||
}, this._page._timeoutSettings.timeout(options));
|
}, this._page._timeoutSettings.timeout(options));
|
||||||
|
|
@ -529,6 +541,7 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
|
||||||
async tap(metadata: CallMetadata, options: types.PointerActionWaitOptions = {}): Promise<void> {
|
async tap(metadata: CallMetadata, options: types.PointerActionWaitOptions = {}): Promise<void> {
|
||||||
const controller = new ProgressController(metadata, this);
|
const controller = new ProgressController(metadata, this);
|
||||||
return controller.run(async progress => {
|
return controller.run(async progress => {
|
||||||
|
await this._markAsTargetElement(metadata);
|
||||||
const result = await this._tap(progress, options);
|
const result = await this._tap(progress, options);
|
||||||
return assertDone(throwRetargetableDOMError(result));
|
return assertDone(throwRetargetableDOMError(result));
|
||||||
}, this._page._timeoutSettings.timeout(options));
|
}, this._page._timeoutSettings.timeout(options));
|
||||||
|
|
@ -541,6 +554,7 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
|
||||||
async selectOption(metadata: CallMetadata, elements: ElementHandle[], values: types.SelectOption[], options: types.CommonActionOptions): Promise<string[]> {
|
async selectOption(metadata: CallMetadata, elements: ElementHandle[], values: types.SelectOption[], options: types.CommonActionOptions): Promise<string[]> {
|
||||||
const controller = new ProgressController(metadata, this);
|
const controller = new ProgressController(metadata, this);
|
||||||
return controller.run(async progress => {
|
return controller.run(async progress => {
|
||||||
|
await this._markAsTargetElement(metadata);
|
||||||
const result = await this._selectOption(progress, elements, values, options);
|
const result = await this._selectOption(progress, elements, values, options);
|
||||||
return throwRetargetableDOMError(result);
|
return throwRetargetableDOMError(result);
|
||||||
}, this._page._timeoutSettings.timeout(options));
|
}, this._page._timeoutSettings.timeout(options));
|
||||||
|
|
@ -549,7 +563,7 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
|
||||||
async _selectOption(progress: Progress, elements: ElementHandle[], values: types.SelectOption[], options: types.CommonActionOptions): Promise<string[] | 'error:notconnected'> {
|
async _selectOption(progress: Progress, elements: ElementHandle[], values: types.SelectOption[], options: types.CommonActionOptions): Promise<string[] | 'error:notconnected'> {
|
||||||
let resultingOptions: string[] = [];
|
let resultingOptions: string[] = [];
|
||||||
await this._retryAction(progress, 'select option', async () => {
|
await this._retryAction(progress, 'select option', async () => {
|
||||||
await progress.beforeInputAction(this);
|
await this.instrumentation.onBeforeInputAction(this, progress.metadata);
|
||||||
if (!options.force)
|
if (!options.force)
|
||||||
progress.log(` waiting for element to be visible and enabled`);
|
progress.log(` waiting for element to be visible and enabled`);
|
||||||
const optionsToSelect = [...elements, ...values];
|
const optionsToSelect = [...elements, ...values];
|
||||||
|
|
@ -574,6 +588,7 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
|
||||||
async fill(metadata: CallMetadata, value: string, options: types.CommonActionOptions = {}): Promise<void> {
|
async fill(metadata: CallMetadata, value: string, options: types.CommonActionOptions = {}): Promise<void> {
|
||||||
const controller = new ProgressController(metadata, this);
|
const controller = new ProgressController(metadata, this);
|
||||||
return controller.run(async progress => {
|
return controller.run(async progress => {
|
||||||
|
await this._markAsTargetElement(metadata);
|
||||||
const result = await this._fill(progress, value, options);
|
const result = await this._fill(progress, value, options);
|
||||||
assertDone(throwRetargetableDOMError(result));
|
assertDone(throwRetargetableDOMError(result));
|
||||||
}, this._page._timeoutSettings.timeout(options));
|
}, this._page._timeoutSettings.timeout(options));
|
||||||
|
|
@ -582,7 +597,7 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
|
||||||
async _fill(progress: Progress, value: string, options: types.CommonActionOptions): Promise<'error:notconnected' | 'done'> {
|
async _fill(progress: Progress, value: string, options: types.CommonActionOptions): Promise<'error:notconnected' | 'done'> {
|
||||||
progress.log(` fill("${value}")`);
|
progress.log(` fill("${value}")`);
|
||||||
return await this._retryAction(progress, 'fill', async () => {
|
return await this._retryAction(progress, 'fill', async () => {
|
||||||
await progress.beforeInputAction(this);
|
await this.instrumentation.onBeforeInputAction(this, progress.metadata);
|
||||||
if (!options.force)
|
if (!options.force)
|
||||||
progress.log(' waiting for element to be visible, enabled and editable');
|
progress.log(' waiting for element to be visible, enabled and editable');
|
||||||
const result = await this.evaluateInUtility(async ([injected, node, { value, force }]) => {
|
const result = await this.evaluateInUtility(async ([injected, node, { value, force }]) => {
|
||||||
|
|
@ -629,6 +644,7 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
|
||||||
const inputFileItems = await prepareFilesForUpload(this._frame, params);
|
const inputFileItems = await prepareFilesForUpload(this._frame, params);
|
||||||
const controller = new ProgressController(metadata, this);
|
const controller = new ProgressController(metadata, this);
|
||||||
return controller.run(async progress => {
|
return controller.run(async progress => {
|
||||||
|
await this._markAsTargetElement(metadata);
|
||||||
const result = await this._setInputFiles(progress, inputFileItems);
|
const result = await this._setInputFiles(progress, inputFileItems);
|
||||||
return assertDone(throwRetargetableDOMError(result));
|
return assertDone(throwRetargetableDOMError(result));
|
||||||
}, this._page._timeoutSettings.timeout(params));
|
}, this._page._timeoutSettings.timeout(params));
|
||||||
|
|
@ -655,7 +671,7 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
|
||||||
if (result === 'error:notconnected' || !result.asElement())
|
if (result === 'error:notconnected' || !result.asElement())
|
||||||
return 'error:notconnected';
|
return 'error:notconnected';
|
||||||
const retargeted = result.asElement() as ElementHandle<HTMLInputElement>;
|
const retargeted = result.asElement() as ElementHandle<HTMLInputElement>;
|
||||||
await progress.beforeInputAction(this);
|
await this.instrumentation.onBeforeInputAction(this, progress.metadata);
|
||||||
progress.throwIfAborted(); // Avoid action that has side-effects.
|
progress.throwIfAborted(); // Avoid action that has side-effects.
|
||||||
if (localPaths || localDirectory) {
|
if (localPaths || localDirectory) {
|
||||||
const localPathsOrDirectory = localDirectory ? [localDirectory] : localPaths!;
|
const localPathsOrDirectory = localDirectory ? [localDirectory] : localPaths!;
|
||||||
|
|
@ -677,6 +693,7 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
|
||||||
async focus(metadata: CallMetadata): Promise<void> {
|
async focus(metadata: CallMetadata): Promise<void> {
|
||||||
const controller = new ProgressController(metadata, this);
|
const controller = new ProgressController(metadata, this);
|
||||||
await controller.run(async progress => {
|
await controller.run(async progress => {
|
||||||
|
await this._markAsTargetElement(metadata);
|
||||||
const result = await this._focus(progress);
|
const result = await this._focus(progress);
|
||||||
return assertDone(throwRetargetableDOMError(result));
|
return assertDone(throwRetargetableDOMError(result));
|
||||||
}, 0);
|
}, 0);
|
||||||
|
|
@ -695,6 +712,7 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
|
||||||
async type(metadata: CallMetadata, text: string, options: { delay?: number } & types.TimeoutOptions & types.StrictOptions): Promise<void> {
|
async type(metadata: CallMetadata, text: string, options: { delay?: number } & types.TimeoutOptions & types.StrictOptions): Promise<void> {
|
||||||
const controller = new ProgressController(metadata, this);
|
const controller = new ProgressController(metadata, this);
|
||||||
return controller.run(async progress => {
|
return controller.run(async progress => {
|
||||||
|
await this._markAsTargetElement(metadata);
|
||||||
const result = await this._type(progress, text, options);
|
const result = await this._type(progress, text, options);
|
||||||
return assertDone(throwRetargetableDOMError(result));
|
return assertDone(throwRetargetableDOMError(result));
|
||||||
}, this._page._timeoutSettings.timeout(options));
|
}, this._page._timeoutSettings.timeout(options));
|
||||||
|
|
@ -702,7 +720,7 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
|
||||||
|
|
||||||
async _type(progress: Progress, text: string, options: { delay?: number } & types.TimeoutOptions & types.StrictOptions): Promise<'error:notconnected' | 'done'> {
|
async _type(progress: Progress, text: string, options: { delay?: number } & types.TimeoutOptions & types.StrictOptions): Promise<'error:notconnected' | 'done'> {
|
||||||
progress.log(`elementHandle.type("${text}")`);
|
progress.log(`elementHandle.type("${text}")`);
|
||||||
await progress.beforeInputAction(this);
|
await this.instrumentation.onBeforeInputAction(this, progress.metadata);
|
||||||
const result = await this._focus(progress, true /* resetSelectionIfNotFocused */);
|
const result = await this._focus(progress, true /* resetSelectionIfNotFocused */);
|
||||||
if (result !== 'done')
|
if (result !== 'done')
|
||||||
return result;
|
return result;
|
||||||
|
|
@ -714,6 +732,7 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
|
||||||
async press(metadata: CallMetadata, key: string, options: { delay?: number, noWaitAfter?: boolean } & types.TimeoutOptions & types.StrictOptions): Promise<void> {
|
async press(metadata: CallMetadata, key: string, options: { delay?: number, noWaitAfter?: boolean } & types.TimeoutOptions & types.StrictOptions): Promise<void> {
|
||||||
const controller = new ProgressController(metadata, this);
|
const controller = new ProgressController(metadata, this);
|
||||||
return controller.run(async progress => {
|
return controller.run(async progress => {
|
||||||
|
await this._markAsTargetElement(metadata);
|
||||||
const result = await this._press(progress, key, options);
|
const result = await this._press(progress, key, options);
|
||||||
return assertDone(throwRetargetableDOMError(result));
|
return assertDone(throwRetargetableDOMError(result));
|
||||||
}, this._page._timeoutSettings.timeout(options));
|
}, this._page._timeoutSettings.timeout(options));
|
||||||
|
|
@ -721,7 +740,7 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
|
||||||
|
|
||||||
async _press(progress: Progress, key: string, options: { delay?: number, noWaitAfter?: boolean } & types.TimeoutOptions & types.StrictOptions): Promise<'error:notconnected' | 'done'> {
|
async _press(progress: Progress, key: string, options: { delay?: number, noWaitAfter?: boolean } & types.TimeoutOptions & types.StrictOptions): Promise<'error:notconnected' | 'done'> {
|
||||||
progress.log(`elementHandle.press("${key}")`);
|
progress.log(`elementHandle.press("${key}")`);
|
||||||
await progress.beforeInputAction(this);
|
await this.instrumentation.onBeforeInputAction(this, progress.metadata);
|
||||||
return this._page._frameManager.waitForSignalsCreatedBy(progress, !options.noWaitAfter, async () => {
|
return this._page._frameManager.waitForSignalsCreatedBy(progress, !options.noWaitAfter, async () => {
|
||||||
const result = await this._focus(progress, true /* resetSelectionIfNotFocused */);
|
const result = await this._focus(progress, true /* resetSelectionIfNotFocused */);
|
||||||
if (result !== 'done')
|
if (result !== 'done')
|
||||||
|
|
@ -753,6 +772,7 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
|
||||||
const result = await this.evaluateInUtility(([injected, node]) => injected.elementState(node, 'checked'), {});
|
const result = await this.evaluateInUtility(([injected, node]) => injected.elementState(node, 'checked'), {});
|
||||||
return throwRetargetableDOMError(result);
|
return throwRetargetableDOMError(result);
|
||||||
};
|
};
|
||||||
|
await this._markAsTargetElement(progress.metadata);
|
||||||
if (await isChecked() === state)
|
if (await isChecked() === state)
|
||||||
return 'done';
|
return 'done';
|
||||||
const result = await this._click(progress, { ...options, waitAfter: 'disabled' });
|
const result = await this._click(progress, { ...options, waitAfter: 'disabled' });
|
||||||
|
|
|
||||||
|
|
@ -51,15 +51,6 @@ export class FFExecutionContext implements js.ExecutionContextDelegate {
|
||||||
return payload.result!.objectId!;
|
return payload.result!.objectId!;
|
||||||
}
|
}
|
||||||
|
|
||||||
rawCallFunctionNoReply(func: Function, ...args: any[]) {
|
|
||||||
this._session.send('Runtime.callFunction', {
|
|
||||||
functionDeclaration: func.toString(),
|
|
||||||
args: args.map(a => a instanceof js.JSHandle ? { objectId: a._objectId } : { value: a }) as any,
|
|
||||||
returnByValue: true,
|
|
||||||
executionContextId: this._executionContextId
|
|
||||||
}).catch(() => {});
|
|
||||||
}
|
|
||||||
|
|
||||||
async evaluateWithArguments(expression: string, returnByValue: boolean, utilityScript: js.JSHandle<any>, values: any[], objectIds: string[]): Promise<any> {
|
async evaluateWithArguments(expression: string, returnByValue: boolean, utilityScript: js.JSHandle<any>, values: any[], objectIds: string[]): Promise<any> {
|
||||||
const payload = await this._session.send('Runtime.callFunction', {
|
const payload = await this._session.send('Runtime.callFunction', {
|
||||||
functionDeclaration: expression,
|
functionDeclaration: expression,
|
||||||
|
|
|
||||||
|
|
@ -1124,8 +1124,10 @@ export class Frame extends SdkObject {
|
||||||
progress.throwIfAborted();
|
progress.throwIfAborted();
|
||||||
if (!resolved)
|
if (!resolved)
|
||||||
return continuePolling;
|
return continuePolling;
|
||||||
const result = await resolved.injected.evaluateHandle((injected, { info }) => {
|
const result = await resolved.injected.evaluateHandle((injected, { info, callId }) => {
|
||||||
const elements = injected.querySelectorAll(info.parsed, document);
|
const elements = injected.querySelectorAll(info.parsed, document);
|
||||||
|
if (callId)
|
||||||
|
injected.markTargetElements(new Set(elements), callId);
|
||||||
const element = elements[0] as Element | undefined;
|
const element = elements[0] as Element | undefined;
|
||||||
let log = '';
|
let log = '';
|
||||||
if (elements.length > 1) {
|
if (elements.length > 1) {
|
||||||
|
|
@ -1136,7 +1138,7 @@ export class Frame extends SdkObject {
|
||||||
log = ` locator resolved to ${injected.previewNode(element)}`;
|
log = ` locator resolved to ${injected.previewNode(element)}`;
|
||||||
}
|
}
|
||||||
return { log, success: !!element, element };
|
return { log, success: !!element, element };
|
||||||
}, { info: resolved.info });
|
}, { info: resolved.info, callId: progress.metadata.id });
|
||||||
const { log, success } = await result.evaluate(r => ({ log: r.log, success: r.success }));
|
const { log, success } = await result.evaluate(r => ({ log: r.log, success: r.success }));
|
||||||
if (log)
|
if (log)
|
||||||
progress.log(log);
|
progress.log(log);
|
||||||
|
|
@ -1478,6 +1480,8 @@ export class Frame extends SdkObject {
|
||||||
|
|
||||||
const { log, matches, received, missingReceived } = await injected.evaluate(async (injected, { info, options, callId }) => {
|
const { log, matches, received, missingReceived } = await injected.evaluate(async (injected, { info, options, callId }) => {
|
||||||
const elements = info ? injected.querySelectorAll(info.parsed, document) : [];
|
const elements = info ? injected.querySelectorAll(info.parsed, document) : [];
|
||||||
|
if (callId)
|
||||||
|
injected.markTargetElements(new Set(elements), callId);
|
||||||
const isArray = options.expression === 'to.have.count' || options.expression.endsWith('.array');
|
const isArray = options.expression === 'to.have.count' || options.expression.endsWith('.array');
|
||||||
let log = '';
|
let log = '';
|
||||||
if (isArray)
|
if (isArray)
|
||||||
|
|
@ -1486,8 +1490,6 @@ export class Frame extends SdkObject {
|
||||||
throw injected.strictModeViolationError(info!.parsed, elements);
|
throw injected.strictModeViolationError(info!.parsed, elements);
|
||||||
else if (elements.length)
|
else if (elements.length)
|
||||||
log = ` locator resolved to ${injected.previewNode(elements[0])}`;
|
log = ` locator resolved to ${injected.previewNode(elements[0])}`;
|
||||||
if (callId)
|
|
||||||
injected.markTargetElements(new Set(elements), callId);
|
|
||||||
return { log, ...await injected.expect(elements[0], options, elements) };
|
return { log, ...await injected.expect(elements[0], options, elements) };
|
||||||
}, { info, options, callId: progress.metadata.id });
|
}, { info, options, callId: progress.metadata.id });
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,6 @@ import type { APIRequestContext } from './fetch';
|
||||||
import type { Browser } from './browser';
|
import type { Browser } from './browser';
|
||||||
import type { BrowserContext } from './browserContext';
|
import type { BrowserContext } from './browserContext';
|
||||||
import type { BrowserType } from './browserType';
|
import type { BrowserType } from './browserType';
|
||||||
import type { ElementHandle } from './dom';
|
|
||||||
import type { Frame } from './frames';
|
import type { Frame } from './frames';
|
||||||
import type { Page } from './page';
|
import type { Page } from './page';
|
||||||
import type { Playwright } from './playwright';
|
import type { Playwright } from './playwright';
|
||||||
|
|
@ -57,7 +56,7 @@ export interface Instrumentation {
|
||||||
addListener(listener: InstrumentationListener, context: BrowserContext | APIRequestContext | null): void;
|
addListener(listener: InstrumentationListener, context: BrowserContext | APIRequestContext | null): void;
|
||||||
removeListener(listener: InstrumentationListener): void;
|
removeListener(listener: InstrumentationListener): void;
|
||||||
onBeforeCall(sdkObject: SdkObject, metadata: CallMetadata): Promise<void>;
|
onBeforeCall(sdkObject: SdkObject, metadata: CallMetadata): Promise<void>;
|
||||||
onBeforeInputAction(sdkObject: SdkObject, metadata: CallMetadata, element: ElementHandle): Promise<void>;
|
onBeforeInputAction(sdkObject: SdkObject, metadata: CallMetadata): Promise<void>;
|
||||||
onCallLog(sdkObject: SdkObject, metadata: CallMetadata, logName: string, message: string): void;
|
onCallLog(sdkObject: SdkObject, metadata: CallMetadata, logName: string, message: string): void;
|
||||||
onAfterCall(sdkObject: SdkObject, metadata: CallMetadata): Promise<void>;
|
onAfterCall(sdkObject: SdkObject, metadata: CallMetadata): Promise<void>;
|
||||||
onPageOpen(page: Page): void;
|
onPageOpen(page: Page): void;
|
||||||
|
|
@ -70,7 +69,7 @@ export interface Instrumentation {
|
||||||
|
|
||||||
export interface InstrumentationListener {
|
export interface InstrumentationListener {
|
||||||
onBeforeCall?(sdkObject: SdkObject, metadata: CallMetadata): Promise<void>;
|
onBeforeCall?(sdkObject: SdkObject, metadata: CallMetadata): Promise<void>;
|
||||||
onBeforeInputAction?(sdkObject: SdkObject, metadata: CallMetadata, element: ElementHandle): Promise<void>;
|
onBeforeInputAction?(sdkObject: SdkObject, metadata: CallMetadata): Promise<void>;
|
||||||
onCallLog?(sdkObject: SdkObject, metadata: CallMetadata, logName: string, message: string): void;
|
onCallLog?(sdkObject: SdkObject, metadata: CallMetadata, logName: string, message: string): void;
|
||||||
onAfterCall?(sdkObject: SdkObject, metadata: CallMetadata): Promise<void>;
|
onAfterCall?(sdkObject: SdkObject, metadata: CallMetadata): Promise<void>;
|
||||||
onPageOpen?(page: Page): void;
|
onPageOpen?(page: Page): void;
|
||||||
|
|
|
||||||
|
|
@ -53,7 +53,6 @@ export type SmartHandle<T> = T extends Node ? dom.ElementHandle<T> : JSHandle<T>
|
||||||
export interface ExecutionContextDelegate {
|
export interface ExecutionContextDelegate {
|
||||||
rawEvaluateJSON(expression: string): Promise<any>;
|
rawEvaluateJSON(expression: string): Promise<any>;
|
||||||
rawEvaluateHandle(expression: string): Promise<ObjectId>;
|
rawEvaluateHandle(expression: string): Promise<ObjectId>;
|
||||||
rawCallFunctionNoReply(func: Function, ...args: any[]): void;
|
|
||||||
evaluateWithArguments(expression: string, returnByValue: boolean, utilityScript: JSHandle<any>, values: any[], objectIds: ObjectId[]): Promise<any>;
|
evaluateWithArguments(expression: string, returnByValue: boolean, utilityScript: JSHandle<any>, values: any[], objectIds: ObjectId[]): Promise<any>;
|
||||||
getProperties(context: ExecutionContext, objectId: ObjectId): Promise<Map<string, JSHandle>>;
|
getProperties(context: ExecutionContext, objectId: ObjectId): Promise<Map<string, JSHandle>>;
|
||||||
createHandle(context: ExecutionContext, remoteObject: RemoteObject): JSHandle;
|
createHandle(context: ExecutionContext, remoteObject: RemoteObject): JSHandle;
|
||||||
|
|
@ -88,10 +87,6 @@ export class ExecutionContext extends SdkObject {
|
||||||
return this._raceAgainstContextDestroyed(this._delegate.rawEvaluateHandle(expression));
|
return this._raceAgainstContextDestroyed(this._delegate.rawEvaluateHandle(expression));
|
||||||
}
|
}
|
||||||
|
|
||||||
rawCallFunctionNoReply(func: Function, ...args: any[]): void {
|
|
||||||
this._delegate.rawCallFunctionNoReply(func, ...args);
|
|
||||||
}
|
|
||||||
|
|
||||||
evaluateWithArguments(expression: string, returnByValue: boolean, utilityScript: JSHandle<any>, values: any[], objectIds: ObjectId[]): Promise<any> {
|
evaluateWithArguments(expression: string, returnByValue: boolean, utilityScript: JSHandle<any>, values: any[], objectIds: ObjectId[]): Promise<any> {
|
||||||
return this._raceAgainstContextDestroyed(this._delegate.evaluateWithArguments(expression, returnByValue, utilityScript, values, objectIds));
|
return this._raceAgainstContextDestroyed(this._delegate.evaluateWithArguments(expression, returnByValue, utilityScript, values, objectIds));
|
||||||
}
|
}
|
||||||
|
|
@ -151,10 +146,6 @@ export class JSHandle<T = any> extends SdkObject {
|
||||||
(globalThis as any).leakedJSHandles.set(this, new Error('Leaked JSHandle'));
|
(globalThis as any).leakedJSHandles.set(this, new Error('Leaked JSHandle'));
|
||||||
}
|
}
|
||||||
|
|
||||||
callFunctionNoReply(func: Function, arg: any) {
|
|
||||||
this._context.rawCallFunctionNoReply(func, this, arg);
|
|
||||||
}
|
|
||||||
|
|
||||||
async evaluate<R, Arg>(pageFunction: FuncOn<T, Arg, R>, arg?: Arg): Promise<R> {
|
async evaluate<R, Arg>(pageFunction: FuncOn<T, Arg, R>, arg?: Arg): Promise<R> {
|
||||||
return evaluate(this._context, true /* returnByValue */, pageFunction, this, arg);
|
return evaluate(this._context, true /* returnByValue */, pageFunction, this, arg);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,6 @@ import { TimeoutError } from './errors';
|
||||||
import { assert, monotonicTime } from '../utils';
|
import { assert, monotonicTime } from '../utils';
|
||||||
import type { LogName } from '../utils/debugLogger';
|
import type { LogName } from '../utils/debugLogger';
|
||||||
import type { CallMetadata, Instrumentation, SdkObject } from './instrumentation';
|
import type { CallMetadata, Instrumentation, SdkObject } from './instrumentation';
|
||||||
import type { ElementHandle } from './dom';
|
|
||||||
import { ManualPromise } from '../utils/manualPromise';
|
import { ManualPromise } from '../utils/manualPromise';
|
||||||
|
|
||||||
export interface Progress {
|
export interface Progress {
|
||||||
|
|
@ -27,7 +26,6 @@ export interface Progress {
|
||||||
isRunning(): boolean;
|
isRunning(): boolean;
|
||||||
cleanupWhenAborted(cleanup: () => any): void;
|
cleanupWhenAborted(cleanup: () => any): void;
|
||||||
throwIfAborted(): void;
|
throwIfAborted(): void;
|
||||||
beforeInputAction(element: ElementHandle): Promise<void>;
|
|
||||||
metadata: CallMetadata;
|
metadata: CallMetadata;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -89,9 +87,6 @@ export class ProgressController {
|
||||||
if (this._state === 'aborted')
|
if (this._state === 'aborted')
|
||||||
throw new AbortedError();
|
throw new AbortedError();
|
||||||
},
|
},
|
||||||
beforeInputAction: async (element: ElementHandle) => {
|
|
||||||
await this.instrumentation.onBeforeInputAction(this.sdkObject, this.metadata, element);
|
|
||||||
},
|
|
||||||
metadata: this.metadata
|
metadata: this.metadata
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,6 @@ import type { SnapshotData } from './snapshotterInjected';
|
||||||
import { frameSnapshotStreamer } from './snapshotterInjected';
|
import { frameSnapshotStreamer } from './snapshotterInjected';
|
||||||
import { calculateSha1, createGuid, monotonicTime } from '../../../utils';
|
import { calculateSha1, createGuid, monotonicTime } from '../../../utils';
|
||||||
import type { FrameSnapshot } from '@trace/snapshot';
|
import type { FrameSnapshot } from '@trace/snapshot';
|
||||||
import type { ElementHandle } from '../../dom';
|
|
||||||
import { mime } from '../../../utilsBundle';
|
import { mime } from '../../../utilsBundle';
|
||||||
|
|
||||||
export type SnapshotterBlob = {
|
export type SnapshotterBlob = {
|
||||||
|
|
@ -105,21 +104,10 @@ export class Snapshotter {
|
||||||
eventsHelper.removeEventListeners(this._eventListeners);
|
eventsHelper.removeEventListeners(this._eventListeners);
|
||||||
}
|
}
|
||||||
|
|
||||||
async captureSnapshot(page: Page, callId: string, snapshotName: string, element?: ElementHandle): Promise<void> {
|
async captureSnapshot(page: Page, callId: string, snapshotName: string): Promise<void> {
|
||||||
// Prepare expression synchronously.
|
// Prepare expression synchronously.
|
||||||
const expression = `window["${this._snapshotStreamer}"].captureSnapshot(${JSON.stringify(snapshotName)})`;
|
const expression = `window["${this._snapshotStreamer}"].captureSnapshot(${JSON.stringify(snapshotName)})`;
|
||||||
|
|
||||||
// In a best-effort manner, without waiting for it, mark target element.
|
|
||||||
element?.callFunctionNoReply((element: Element, callId: string) => {
|
|
||||||
const customEvent = new CustomEvent('__playwright_target__', {
|
|
||||||
bubbles: true,
|
|
||||||
cancelable: true,
|
|
||||||
detail: callId,
|
|
||||||
composed: true,
|
|
||||||
});
|
|
||||||
element.dispatchEvent(customEvent);
|
|
||||||
}, callId);
|
|
||||||
|
|
||||||
// In each frame, in a non-stalling manner, capture the snapshots.
|
// In each frame, in a non-stalling manner, capture the snapshots.
|
||||||
const snapshots = page.frames().map(async frame => {
|
const snapshots = page.frames().map(async frame => {
|
||||||
const data = await frame.nonStallingRawEvaluateInExistingMainContext(expression).catch(e => debugLogger.log('error', e)) as SnapshotData;
|
const data = await frame.nonStallingRawEvaluateInExistingMainContext(expression).catch(e => debugLogger.log('error', e)) as SnapshotData;
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,6 @@ import { commandsWithTracingSnapshots } from '../../../protocol/debug';
|
||||||
import { assert, createGuid, monotonicTime, SerializedFS, removeFolders, eventsHelper, type RegisteredListener } from '../../../utils';
|
import { assert, createGuid, monotonicTime, SerializedFS, removeFolders, eventsHelper, type RegisteredListener } from '../../../utils';
|
||||||
import { Artifact } from '../../artifact';
|
import { Artifact } from '../../artifact';
|
||||||
import { BrowserContext } from '../../browserContext';
|
import { BrowserContext } from '../../browserContext';
|
||||||
import type { ElementHandle } from '../../dom';
|
|
||||||
import type { APIRequestContext } from '../../fetch';
|
import type { APIRequestContext } from '../../fetch';
|
||||||
import type { CallMetadata, InstrumentationListener } from '../../instrumentation';
|
import type { CallMetadata, InstrumentationListener } from '../../instrumentation';
|
||||||
import { SdkObject } from '../../instrumentation';
|
import { SdkObject } from '../../instrumentation';
|
||||||
|
|
@ -341,7 +340,7 @@ export class Tracing extends SdkObject implements InstrumentationListener, Snaps
|
||||||
return { artifact };
|
return { artifact };
|
||||||
}
|
}
|
||||||
|
|
||||||
async _captureSnapshot(snapshotName: string, sdkObject: SdkObject, metadata: CallMetadata, element?: ElementHandle): Promise<void> {
|
async _captureSnapshot(snapshotName: string, sdkObject: SdkObject, metadata: CallMetadata): Promise<void> {
|
||||||
if (!this._snapshotter)
|
if (!this._snapshotter)
|
||||||
return;
|
return;
|
||||||
if (!sdkObject.attribution.page)
|
if (!sdkObject.attribution.page)
|
||||||
|
|
@ -350,7 +349,7 @@ export class Tracing extends SdkObject implements InstrumentationListener, Snaps
|
||||||
return;
|
return;
|
||||||
if (!shouldCaptureSnapshot(metadata))
|
if (!shouldCaptureSnapshot(metadata))
|
||||||
return;
|
return;
|
||||||
await this._snapshotter.captureSnapshot(sdkObject.attribution.page, metadata.id, snapshotName, element).catch(() => {});
|
await this._snapshotter.captureSnapshot(sdkObject.attribution.page, metadata.id, snapshotName).catch(() => {});
|
||||||
}
|
}
|
||||||
|
|
||||||
onBeforeCall(sdkObject: SdkObject, metadata: CallMetadata) {
|
onBeforeCall(sdkObject: SdkObject, metadata: CallMetadata) {
|
||||||
|
|
@ -365,7 +364,7 @@ export class Tracing extends SdkObject implements InstrumentationListener, Snaps
|
||||||
return this._captureSnapshot(event.beforeSnapshot, sdkObject, metadata);
|
return this._captureSnapshot(event.beforeSnapshot, sdkObject, metadata);
|
||||||
}
|
}
|
||||||
|
|
||||||
onBeforeInputAction(sdkObject: SdkObject, metadata: CallMetadata, element: ElementHandle) {
|
onBeforeInputAction(sdkObject: SdkObject, metadata: CallMetadata) {
|
||||||
if (!this._state?.callIds.has(metadata.id))
|
if (!this._state?.callIds.has(metadata.id))
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
// IMPORTANT: no awaits before this._appendTraceEvent in this method.
|
// IMPORTANT: no awaits before this._appendTraceEvent in this method.
|
||||||
|
|
@ -375,7 +374,7 @@ export class Tracing extends SdkObject implements InstrumentationListener, Snaps
|
||||||
sdkObject.attribution.page?.temporarilyDisableTracingScreencastThrottling();
|
sdkObject.attribution.page?.temporarilyDisableTracingScreencastThrottling();
|
||||||
event.inputSnapshot = `input@${metadata.id}`;
|
event.inputSnapshot = `input@${metadata.id}`;
|
||||||
this._appendTraceEvent(event);
|
this._appendTraceEvent(event);
|
||||||
return this._captureSnapshot(event.inputSnapshot, sdkObject, metadata, element);
|
return this._captureSnapshot(event.inputSnapshot, sdkObject, metadata);
|
||||||
}
|
}
|
||||||
|
|
||||||
onCallLog(sdkObject: SdkObject, metadata: CallMetadata, logName: string, message: string) {
|
onCallLog(sdkObject: SdkObject, metadata: CallMetadata, logName: string, message: string) {
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,6 @@ import type { SnapshotRenderer } from '../../../../../trace-viewer/src/sw/snapsh
|
||||||
import { SnapshotStorage } from '../../../../../trace-viewer/src/sw/snapshotStorage';
|
import { SnapshotStorage } from '../../../../../trace-viewer/src/sw/snapshotStorage';
|
||||||
import type { SnapshotterBlob, SnapshotterDelegate } from '../recorder/snapshotter';
|
import type { SnapshotterBlob, SnapshotterDelegate } from '../recorder/snapshotter';
|
||||||
import { Snapshotter } from '../recorder/snapshotter';
|
import { Snapshotter } from '../recorder/snapshotter';
|
||||||
import type { ElementHandle } from '../../dom';
|
|
||||||
import type { HarTracerDelegate } from '../../har/harTracer';
|
import type { HarTracerDelegate } from '../../har/harTracer';
|
||||||
import { HarTracer } from '../../har/harTracer';
|
import { HarTracer } from '../../har/harTracer';
|
||||||
import type * as har from '@trace/har';
|
import type * as har from '@trace/har';
|
||||||
|
|
@ -59,11 +58,11 @@ export class InMemorySnapshotter implements SnapshotterDelegate, HarTracerDelega
|
||||||
this._harTracer.stop();
|
this._harTracer.stop();
|
||||||
}
|
}
|
||||||
|
|
||||||
async captureSnapshot(page: Page, callId: string, snapshotName: string, element?: ElementHandle): Promise<SnapshotRenderer> {
|
async captureSnapshot(page: Page, callId: string, snapshotName: string): Promise<SnapshotRenderer> {
|
||||||
if (this._snapshotReadyPromises.has(snapshotName))
|
if (this._snapshotReadyPromises.has(snapshotName))
|
||||||
throw new Error('Duplicate snapshot name: ' + snapshotName);
|
throw new Error('Duplicate snapshot name: ' + snapshotName);
|
||||||
|
|
||||||
this._snapshotter.captureSnapshot(page, callId, snapshotName, element).catch(() => {});
|
this._snapshotter.captureSnapshot(page, callId, snapshotName).catch(() => {});
|
||||||
const promise = new ManualPromise<SnapshotRenderer>();
|
const promise = new ManualPromise<SnapshotRenderer>();
|
||||||
this._snapshotReadyPromises.set(snapshotName, promise);
|
this._snapshotReadyPromises.set(snapshotName, promise);
|
||||||
return promise;
|
return promise;
|
||||||
|
|
|
||||||
|
|
@ -60,16 +60,6 @@ export class WKExecutionContext implements js.ExecutionContextDelegate {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
rawCallFunctionNoReply(func: Function, ...args: any[]) {
|
|
||||||
this._session.send('Runtime.callFunctionOn', {
|
|
||||||
functionDeclaration: func.toString(),
|
|
||||||
objectId: args.find(a => a instanceof js.JSHandle)!._objectId!,
|
|
||||||
arguments: args.map(a => a instanceof js.JSHandle ? { objectId: a._objectId } : { value: a }),
|
|
||||||
returnByValue: true,
|
|
||||||
emulateUserGesture: true
|
|
||||||
}).catch(() => {});
|
|
||||||
}
|
|
||||||
|
|
||||||
async evaluateWithArguments(expression: string, returnByValue: boolean, utilityScript: js.JSHandle<any>, values: any[], objectIds: string[]): Promise<any> {
|
async evaluateWithArguments(expression: string, returnByValue: boolean, utilityScript: js.JSHandle<any>, values: any[], objectIds: string[]): Promise<any> {
|
||||||
try {
|
try {
|
||||||
const response = await this._session.send('Runtime.callFunctionOn', {
|
const response = await this._session.send('Runtime.callFunctionOn', {
|
||||||
|
|
|
||||||
|
|
@ -215,20 +215,6 @@ it.describe('snapshots', () => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should capture snapshot target', async ({ page, toImpl, snapshotter }) => {
|
|
||||||
await page.setContent('<button>Hello</button><button>World</button>');
|
|
||||||
{
|
|
||||||
const handle = await page.$('text=Hello');
|
|
||||||
const snapshot = await snapshotter.captureSnapshot(toImpl(page), 'call@1', 'snapshot@call@1', toImpl(handle));
|
|
||||||
expect(distillSnapshot(snapshot, false /* distillTarget */)).toBe('<BUTTON __playwright_target__=\"call@1\">Hello</BUTTON><BUTTON>World</BUTTON>');
|
|
||||||
}
|
|
||||||
{
|
|
||||||
const handle = await page.$('text=World');
|
|
||||||
const snapshot = await snapshotter.captureSnapshot(toImpl(page), 'call@2', 'snapshot@call@2', toImpl(handle));
|
|
||||||
expect(distillSnapshot(snapshot, false /* distillTarget */)).toBe('<BUTTON __playwright_target__=\"call@1\">Hello</BUTTON><BUTTON __playwright_target__=\"call@2\">World</BUTTON>');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should collect on attribute change', async ({ page, toImpl, snapshotter }) => {
|
it('should collect on attribute change', async ({ page, toImpl, snapshotter }) => {
|
||||||
await page.setContent('<button>Hello</button>');
|
await page.setContent('<button>Hello</button>');
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -776,6 +776,8 @@ test('should highlight target elements', async ({ page, runAndTrace, browserName
|
||||||
await expect(page.locator('text=t6')).toHaveText(/t6/i);
|
await expect(page.locator('text=t6')).toHaveText(/t6/i);
|
||||||
await expect(page.locator('text=multi')).toHaveText(['a', 'b'], { timeout: 1000 }).catch(() => {});
|
await expect(page.locator('text=multi')).toHaveText(['a', 'b'], { timeout: 1000 }).catch(() => {});
|
||||||
await page.mouse.move(123, 234);
|
await page.mouse.move(123, 234);
|
||||||
|
await page.getByText(/^t\d$/).click().catch(() => {});
|
||||||
|
await expect(page.getByText(/t3|t4/)).toBeVisible().catch(() => {});
|
||||||
});
|
});
|
||||||
|
|
||||||
async function highlightedDivs(frameLocator: FrameLocator) {
|
async function highlightedDivs(frameLocator: FrameLocator) {
|
||||||
|
|
@ -817,6 +819,12 @@ test('should highlight target elements', async ({ page, runAndTrace, browserName
|
||||||
|
|
||||||
const frameMouseMove = await traceViewer.snapshotFrame('mouse.move');
|
const frameMouseMove = await traceViewer.snapshotFrame('mouse.move');
|
||||||
await expect(frameMouseMove.locator('x-pw-pointer')).toBeVisible();
|
await expect(frameMouseMove.locator('x-pw-pointer')).toBeVisible();
|
||||||
|
|
||||||
|
const frameClickStrictViolation = await traceViewer.snapshotFrame('locator.click');
|
||||||
|
await expect.poll(() => highlightedDivs(frameClickStrictViolation)).toEqual(['t1', 't2', 't3', 't4', 't5', 't6']);
|
||||||
|
|
||||||
|
const frameExpectStrictViolation = await traceViewer.snapshotFrame('expect.toBeVisible');
|
||||||
|
await expect.poll(() => highlightedDivs(frameExpectStrictViolation)).toEqual(['t3', '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 }) => {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue