mirror of
https://github.com/microsoft/playwright.git
synced 2025-06-26 21:40:17 +00:00
chore(evaluate): implement non-stalling evaluate (#6354)
This commit is contained in:
parent
06a92684f3
commit
434f474ce8
@ -217,7 +217,7 @@ export class DispatcherConnection {
|
||||
}
|
||||
|
||||
const sdkObject = dispatcher._object instanceof SdkObject ? dispatcher._object : undefined;
|
||||
let callMetadata: CallMetadata = {
|
||||
const callMetadata: CallMetadata = {
|
||||
id: `call@${id}`,
|
||||
...validMetadata,
|
||||
objectId: sdkObject?.guid,
|
||||
@ -232,59 +232,52 @@ export class DispatcherConnection {
|
||||
snapshots: []
|
||||
};
|
||||
|
||||
try {
|
||||
if (sdkObject) {
|
||||
// Process logs for waitForNavigation/waitForLoadState
|
||||
if (params?.info?.waitId) {
|
||||
const info = params.info;
|
||||
switch (info.phase) {
|
||||
case 'before':
|
||||
callMetadata.apiName = info.apiName;
|
||||
this._waitOperations.set(info.waitId, callMetadata);
|
||||
break;
|
||||
case 'log':
|
||||
const originalMetadata = this._waitOperations.get(info.waitId)!;
|
||||
originalMetadata.log.push(info.message);
|
||||
sdkObject.instrumentation.onCallLog('api', info.message, sdkObject, originalMetadata);
|
||||
// Fall through.
|
||||
case 'after':
|
||||
return;
|
||||
}
|
||||
if (sdkObject && params?.info?.waitId) {
|
||||
// Process logs for waitForNavigation/waitForLoadState
|
||||
const info = params.info;
|
||||
switch (info.phase) {
|
||||
case 'before': {
|
||||
callMetadata.apiName = info.apiName;
|
||||
this._waitOperations.set(info.waitId, callMetadata);
|
||||
await sdkObject.instrumentation.onBeforeCall(sdkObject, callMetadata);
|
||||
return;
|
||||
} case 'log': {
|
||||
const originalMetadata = this._waitOperations.get(info.waitId)!;
|
||||
originalMetadata.log.push(info.message);
|
||||
sdkObject.instrumentation.onCallLog('api', info.message, sdkObject, originalMetadata);
|
||||
return;
|
||||
} case 'after': {
|
||||
const originalMetadata = this._waitOperations.get(info.waitId)!;
|
||||
originalMetadata.endTime = monotonicTime();
|
||||
originalMetadata.error = info.error;
|
||||
this._waitOperations.delete(info.waitId);
|
||||
await sdkObject.instrumentation.onAfterCall(sdkObject, originalMetadata);
|
||||
return;
|
||||
}
|
||||
await sdkObject.instrumentation.onBeforeCall(sdkObject, callMetadata);
|
||||
}
|
||||
const result = await (dispatcher as any)[method](validParams, callMetadata);
|
||||
this.onmessage({ id, result: this._replaceDispatchersWithGuids(result) });
|
||||
}
|
||||
|
||||
|
||||
let result: any;
|
||||
let error: any;
|
||||
await sdkObject?.instrumentation.onBeforeCall(sdkObject, callMetadata);
|
||||
try {
|
||||
result = await (dispatcher as any)[method](validParams, callMetadata);
|
||||
} catch (e) {
|
||||
// Dispatching error
|
||||
callMetadata.error = e.message;
|
||||
if (callMetadata.log.length)
|
||||
rewriteErrorMessage(e, e.message + formatLogRecording(callMetadata.log) + kLoggingNote);
|
||||
this.onmessage({ id, error: serializeError(e) });
|
||||
error = serializeError(e);
|
||||
} finally {
|
||||
callMetadata.endTime = monotonicTime();
|
||||
if (sdkObject) {
|
||||
// Process logs for waitForNavigation/waitForLoadState
|
||||
if (params?.info?.waitId) {
|
||||
const info = params.info;
|
||||
switch (info.phase) {
|
||||
case 'before':
|
||||
callMetadata.endTime = 0;
|
||||
// Fall through.
|
||||
case 'log':
|
||||
return;
|
||||
case 'after':
|
||||
const originalMetadata = this._waitOperations.get(info.waitId)!;
|
||||
originalMetadata.endTime = callMetadata.endTime;
|
||||
originalMetadata.error = info.error;
|
||||
this._waitOperations.delete(info.waitId);
|
||||
callMetadata = originalMetadata;
|
||||
break;
|
||||
}
|
||||
}
|
||||
await sdkObject.instrumentation.onAfterCall(sdkObject, callMetadata);
|
||||
}
|
||||
await sdkObject?.instrumentation.onAfterCall(sdkObject, callMetadata);
|
||||
}
|
||||
|
||||
if (error)
|
||||
this.onmessage({ id, error });
|
||||
else
|
||||
this.onmessage({ id, result: this._replaceDispatchersWithGuids(result) });
|
||||
}
|
||||
|
||||
private _replaceDispatchersWithGuids(payload: any): any {
|
||||
|
||||
@ -31,7 +31,18 @@ export class CRExecutionContext implements js.ExecutionContextDelegate {
|
||||
this._contextId = contextPayload.id;
|
||||
}
|
||||
|
||||
async rawEvaluate(expression: string): Promise<string> {
|
||||
async rawEvaluateJSON(expression: string): Promise<any> {
|
||||
const { exceptionDetails, result: remoteObject } = await this._client.send('Runtime.evaluate', {
|
||||
expression,
|
||||
contextId: this._contextId,
|
||||
returnByValue: true,
|
||||
}).catch(rewriteError);
|
||||
if (exceptionDetails)
|
||||
throw new Error('Evaluation failed: ' + getExceptionMessage(exceptionDetails));
|
||||
return remoteObject.value;
|
||||
}
|
||||
|
||||
async rawEvaluateHandle(expression: string): Promise<js.ObjectId> {
|
||||
const { exceptionDetails, result: remoteObject } = await this._client.send('Runtime.evaluate', {
|
||||
expression,
|
||||
contextId: this._contextId,
|
||||
|
||||
@ -98,7 +98,7 @@ export class FrameExecutionContext extends js.ExecutionContext {
|
||||
);
|
||||
})();
|
||||
`;
|
||||
this._injectedScriptPromise = this._delegate.rawEvaluate(source).then(objectId => new js.JSHandle(this, 'object', objectId));
|
||||
this._injectedScriptPromise = this._delegate.rawEvaluateHandle(source).then(objectId => new js.JSHandle(this, 'object', objectId));
|
||||
}
|
||||
return this._injectedScriptPromise;
|
||||
}
|
||||
|
||||
@ -30,7 +30,17 @@ export class FFExecutionContext implements js.ExecutionContextDelegate {
|
||||
this._executionContextId = executionContextId;
|
||||
}
|
||||
|
||||
async rawEvaluate(expression: string): Promise<string> {
|
||||
async rawEvaluateJSON(expression: string): Promise<any> {
|
||||
const payload = await this._session.send('Runtime.evaluate', {
|
||||
expression,
|
||||
returnByValue: true,
|
||||
executionContextId: this._executionContextId,
|
||||
}).catch(rewriteError);
|
||||
checkException(payload.exceptionDetails);
|
||||
return payload.result!.value;
|
||||
}
|
||||
|
||||
async rawEvaluateHandle(expression: string): Promise<js.ObjectId> {
|
||||
const payload = await this._session.send('Runtime.evaluate', {
|
||||
expression,
|
||||
returnByValue: false,
|
||||
|
||||
@ -158,11 +158,11 @@ export class FrameManager {
|
||||
return;
|
||||
for (const barrier of this._signalBarriers)
|
||||
barrier.addFrameNavigation(frame);
|
||||
if (frame._pendingDocument && frame._pendingDocument.documentId === documentId) {
|
||||
if (frame.pendingDocument() && frame.pendingDocument()!.documentId === documentId) {
|
||||
// Do not override request with undefined.
|
||||
return;
|
||||
}
|
||||
frame._pendingDocument = { documentId, request: undefined };
|
||||
frame.setPendingDocument({ documentId, request: undefined });
|
||||
}
|
||||
|
||||
frameCommittedNewDocumentNavigation(frameId: string, url: string, name: string, documentId: string, initial: boolean) {
|
||||
@ -173,24 +173,25 @@ export class FrameManager {
|
||||
frame._name = name;
|
||||
|
||||
let keepPending: DocumentInfo | undefined;
|
||||
if (frame._pendingDocument) {
|
||||
if (frame._pendingDocument.documentId === undefined) {
|
||||
const pendingDocument = frame.pendingDocument();
|
||||
if (pendingDocument) {
|
||||
if (pendingDocument.documentId === undefined) {
|
||||
// Pending with unknown documentId - assume it is the one being committed.
|
||||
frame._pendingDocument.documentId = documentId;
|
||||
pendingDocument.documentId = documentId;
|
||||
}
|
||||
if (frame._pendingDocument.documentId === documentId) {
|
||||
if (pendingDocument.documentId === documentId) {
|
||||
// Committing a pending document.
|
||||
frame._currentDocument = frame._pendingDocument;
|
||||
frame._currentDocument = pendingDocument;
|
||||
} else {
|
||||
// Sometimes, we already have a new pending when the old one commits.
|
||||
// An example would be Chromium error page followed by a new navigation request,
|
||||
// where the error page commit arrives after Network.requestWillBeSent for the
|
||||
// new navigation.
|
||||
// We commit, but keep the pending request since it's not done yet.
|
||||
keepPending = frame._pendingDocument;
|
||||
keepPending = pendingDocument;
|
||||
frame._currentDocument = { documentId, request: undefined };
|
||||
}
|
||||
frame._pendingDocument = undefined;
|
||||
frame.setPendingDocument(undefined);
|
||||
} else {
|
||||
// No pending - just commit a new document.
|
||||
frame._currentDocument = { documentId, request: undefined };
|
||||
@ -205,7 +206,7 @@ export class FrameManager {
|
||||
this._page.frameNavigatedToNewDocument(frame);
|
||||
}
|
||||
// Restore pending if any - see comments above about keepPending.
|
||||
frame._pendingDocument = keepPending;
|
||||
frame.setPendingDocument(keepPending);
|
||||
}
|
||||
|
||||
frameCommittedSameDocumentNavigation(frameId: string, url: string) {
|
||||
@ -220,17 +221,17 @@ export class FrameManager {
|
||||
|
||||
frameAbortedNavigation(frameId: string, errorText: string, documentId?: string) {
|
||||
const frame = this._frames.get(frameId);
|
||||
if (!frame || !frame._pendingDocument)
|
||||
if (!frame || !frame.pendingDocument())
|
||||
return;
|
||||
if (documentId !== undefined && frame._pendingDocument.documentId !== documentId)
|
||||
if (documentId !== undefined && frame.pendingDocument()!.documentId !== documentId)
|
||||
return;
|
||||
const navigationEvent: NavigationEvent = {
|
||||
url: frame._url,
|
||||
name: frame._name,
|
||||
newDocument: frame._pendingDocument,
|
||||
newDocument: frame.pendingDocument(),
|
||||
error: new Error(errorText),
|
||||
};
|
||||
frame._pendingDocument = undefined;
|
||||
frame.setPendingDocument(undefined);
|
||||
frame.emit(Frame.Events.Navigation, navigationEvent);
|
||||
}
|
||||
|
||||
@ -255,7 +256,7 @@ export class FrameManager {
|
||||
const frame = request.frame();
|
||||
this._inflightRequestStarted(request);
|
||||
if (request._documentId)
|
||||
frame._pendingDocument = { documentId: request._documentId, request };
|
||||
frame.setPendingDocument({ documentId: request._documentId, request });
|
||||
if (request._isFavicon) {
|
||||
const route = request._route();
|
||||
if (route)
|
||||
@ -281,11 +282,11 @@ export class FrameManager {
|
||||
requestFailed(request: network.Request, canceled: boolean) {
|
||||
const frame = request.frame();
|
||||
this._inflightRequestFinished(request);
|
||||
if (frame._pendingDocument && frame._pendingDocument.request === request) {
|
||||
if (frame.pendingDocument() && frame.pendingDocument()!.request === request) {
|
||||
let errorText = request.failure()!.errorText;
|
||||
if (canceled)
|
||||
errorText += '; maybe frame was detached?';
|
||||
this.frameAbortedNavigation(frame._id, errorText, frame._pendingDocument.documentId);
|
||||
this.frameAbortedNavigation(frame._id, errorText, frame.pendingDocument()!.documentId);
|
||||
}
|
||||
if (!request._isFavicon)
|
||||
this._page.emit(Page.Events.RequestFailed, request);
|
||||
@ -399,7 +400,7 @@ export class Frame extends SdkObject {
|
||||
private _firedLifecycleEvents = new Set<types.LifecycleEvent>();
|
||||
_subtreeLifecycleEvents = new Set<types.LifecycleEvent>();
|
||||
_currentDocument: DocumentInfo;
|
||||
_pendingDocument?: DocumentInfo;
|
||||
private _pendingDocument: DocumentInfo | undefined;
|
||||
readonly _page: Page;
|
||||
private _parentFrame: Frame | null;
|
||||
_url = '';
|
||||
@ -412,6 +413,7 @@ export class Frame extends SdkObject {
|
||||
private _setContentCounter = 0;
|
||||
readonly _detachedPromise: Promise<void>;
|
||||
private _detachedCallback = () => {};
|
||||
private _nonStallingEvaluations = new Set<(error: Error) => void>();
|
||||
|
||||
constructor(page: Page, id: string, parentFrame: Frame | null) {
|
||||
super(page, 'frame');
|
||||
@ -451,6 +453,44 @@ export class Frame extends SdkObject {
|
||||
this._startNetworkIdleTimer();
|
||||
}
|
||||
|
||||
setPendingDocument(documentInfo: DocumentInfo | undefined) {
|
||||
this._pendingDocument = documentInfo;
|
||||
if (documentInfo)
|
||||
this._invalidateNonStallingEvaluations();
|
||||
}
|
||||
|
||||
pendingDocument(): DocumentInfo | undefined {
|
||||
return this._pendingDocument;
|
||||
}
|
||||
|
||||
private async _invalidateNonStallingEvaluations() {
|
||||
if (!this._nonStallingEvaluations)
|
||||
return;
|
||||
const error = new Error('Navigation interrupted the evaluation');
|
||||
for (const callback of this._nonStallingEvaluations)
|
||||
callback(error);
|
||||
}
|
||||
|
||||
async nonStallingRawEvaluateInExistingMainContext(expression: string): Promise<any> {
|
||||
if (this._pendingDocument)
|
||||
throw new Error('Frame is currently attempting a navigation');
|
||||
const context = this._existingMainContext();
|
||||
if (!context)
|
||||
throw new Error('Frame does not yet have a main execution context');
|
||||
|
||||
let callback = () => {};
|
||||
const frameInvalidated = new Promise<void>((f, r) => callback = r);
|
||||
this._nonStallingEvaluations.add(callback);
|
||||
try {
|
||||
return await Promise.race([
|
||||
context.rawEvaluateJSON(expression),
|
||||
frameInvalidated
|
||||
]);
|
||||
} finally {
|
||||
this._nonStallingEvaluations.delete(callback);
|
||||
}
|
||||
}
|
||||
|
||||
private _recalculateLifecycle() {
|
||||
const events = new Set<types.LifecycleEvent>(this._firedLifecycleEvents);
|
||||
for (const child of this._childFrames) {
|
||||
@ -584,7 +624,7 @@ export class Frame extends SdkObject {
|
||||
return this._context('main');
|
||||
}
|
||||
|
||||
_existingMainContext(): dom.FrameExecutionContext | null {
|
||||
private _existingMainContext(): dom.FrameExecutionContext | null {
|
||||
return this._contextData.get('main')?.context || null;
|
||||
}
|
||||
|
||||
|
||||
@ -43,7 +43,8 @@ export type FuncOn<On, Arg2, R> = string | ((on: On, arg2: Unboxed<Arg2>) => R |
|
||||
export type SmartHandle<T> = T extends Node ? dom.ElementHandle<T> : JSHandle<T>;
|
||||
|
||||
export interface ExecutionContextDelegate {
|
||||
rawEvaluate(expression: string): Promise<ObjectId>;
|
||||
rawEvaluateJSON(expression: string): Promise<any>;
|
||||
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>;
|
||||
getProperties(context: ExecutionContext, objectId: ObjectId): Promise<Map<string, JSHandle>>;
|
||||
@ -75,7 +76,7 @@ export class ExecutionContext extends SdkObject {
|
||||
${utilityScriptSource.source}
|
||||
return new pwExport();
|
||||
})();`;
|
||||
this._utilityScriptPromise = this._delegate.rawEvaluate(source).then(objectId => new JSHandle(this, 'object', objectId));
|
||||
this._utilityScriptPromise = this._delegate.rawEvaluateHandle(source).then(objectId => new JSHandle(this, 'object', objectId));
|
||||
}
|
||||
return this._utilityScriptPromise;
|
||||
}
|
||||
@ -84,9 +85,8 @@ export class ExecutionContext extends SdkObject {
|
||||
return this._delegate.createHandle(this, remoteObject);
|
||||
}
|
||||
|
||||
async rawEvaluate(expression: string): Promise<void> {
|
||||
// Make sure to never return a value.
|
||||
await this._delegate.rawEvaluate(expression + '; 0');
|
||||
async rawEvaluateJSON(expression: string): Promise<any> {
|
||||
return await this._delegate.rawEvaluateJSON(expression);
|
||||
}
|
||||
|
||||
async doSlowMo() {
|
||||
|
||||
@ -51,7 +51,7 @@ export class InMemorySnapshotter extends BaseSnapshotStorage implements Snapshot
|
||||
if (this._frameSnapshots.has(snapshotName))
|
||||
throw new Error('Duplicate snapshot name: ' + snapshotName);
|
||||
|
||||
this._snapshotter.captureSnapshot(page, snapshotName, element);
|
||||
this._snapshotter.captureSnapshot(page, snapshotName, element).catch(() => {});
|
||||
return new Promise<SnapshotRenderer>(fulfill => {
|
||||
const listener = helper.addEventListener(this, 'snapshot', (renderer: SnapshotRenderer) => {
|
||||
if (renderer.snapshotName === snapshotName) {
|
||||
|
||||
@ -20,7 +20,7 @@ import * as network from '../network';
|
||||
import { helper, RegisteredListener } from '../helper';
|
||||
import { debugLogger } from '../../utils/debugLogger';
|
||||
import { Frame } from '../frames';
|
||||
import { SnapshotData, frameSnapshotStreamer } from './snapshotterInjected';
|
||||
import { frameSnapshotStreamer, SnapshotData } from './snapshotterInjected';
|
||||
import { calculateSha1, createGuid, monotonicTime } from '../../utils/utils';
|
||||
import { FrameSnapshot, ResourceSnapshot } from './snapshotTypes';
|
||||
import { ElementHandle } from '../dom';
|
||||
@ -41,7 +41,6 @@ export class Snapshotter {
|
||||
private _delegate: SnapshotterDelegate;
|
||||
private _eventListeners: RegisteredListener[] = [];
|
||||
private _snapshotStreamer: string;
|
||||
private _snapshotBinding: string;
|
||||
private _initialized = false;
|
||||
private _started = false;
|
||||
private _fetchedResponses = new Map<network.Response, string>();
|
||||
@ -51,7 +50,6 @@ export class Snapshotter {
|
||||
this._delegate = delegate;
|
||||
const guid = createGuid();
|
||||
this._snapshotStreamer = '__playwright_snapshot_streamer_' + guid;
|
||||
this._snapshotBinding = '__playwright_snapshot_binding_' + guid;
|
||||
}
|
||||
|
||||
async start() {
|
||||
@ -60,7 +58,7 @@ export class Snapshotter {
|
||||
this._initialized = true;
|
||||
await this._initialize();
|
||||
}
|
||||
this._runInAllFrames(`window["${this._snapshotStreamer}"].reset()`);
|
||||
await this._runInAllFrames(`window["${this._snapshotStreamer}"].reset()`);
|
||||
|
||||
// Replay resources loaded in all pages.
|
||||
for (const page of this._context.pages()) {
|
||||
@ -80,11 +78,44 @@ export class Snapshotter {
|
||||
helper.addEventListener(this._context, BrowserContext.Events.Page, this._onPage.bind(this)),
|
||||
];
|
||||
|
||||
await this._context.exposeBinding(this._snapshotBinding, false, (source, data: SnapshotData) => {
|
||||
const initScript = `(${frameSnapshotStreamer})("${this._snapshotStreamer}")`;
|
||||
await this._context._doAddInitScript(initScript);
|
||||
await this._runInAllFrames(initScript);
|
||||
}
|
||||
|
||||
private async _runInAllFrames(expression: string) {
|
||||
const frames = [];
|
||||
for (const page of this._context.pages())
|
||||
frames.push(...page.frames());
|
||||
await Promise.all(frames.map(frame => {
|
||||
return frame.nonStallingRawEvaluateInExistingMainContext(expression).catch(debugExceptionHandler);
|
||||
}));
|
||||
}
|
||||
|
||||
dispose() {
|
||||
helper.removeEventListeners(this._eventListeners);
|
||||
}
|
||||
|
||||
async captureSnapshot(page: Page, snapshotName: string, element?: ElementHandle): Promise<void> {
|
||||
// Prepare expression synchronously.
|
||||
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, snapshotName: string) => {
|
||||
element.setAttribute('__playwright_target__', snapshotName);
|
||||
}, snapshotName);
|
||||
|
||||
// In each frame, in a non-stalling manner, capture the snapshots.
|
||||
const snapshots = page.frames().map(async frame => {
|
||||
const data = await frame.nonStallingRawEvaluateInExistingMainContext(expression).catch(debugExceptionHandler) as SnapshotData;
|
||||
// Something went wrong -> bail out, our snapshots are best-efforty.
|
||||
if (!data)
|
||||
return;
|
||||
|
||||
const snapshot: FrameSnapshot = {
|
||||
snapshotName: data.snapshotName,
|
||||
pageId: source.page.guid,
|
||||
frameId: source.frame.guid,
|
||||
snapshotName,
|
||||
pageId: page.guid,
|
||||
frameId: frame.guid,
|
||||
frameUrl: data.url,
|
||||
doctype: data.doctype,
|
||||
html: data.html,
|
||||
@ -93,7 +124,7 @@ export class Snapshotter {
|
||||
pageTimestamp: data.timestamp,
|
||||
collectionTime: data.collectionTime,
|
||||
resourceOverrides: [],
|
||||
isMainFrame: source.page.mainFrame() === source.frame
|
||||
isMainFrame: page.mainFrame() === frame
|
||||
};
|
||||
for (const { url, content } of data.resourceOverrides) {
|
||||
if (typeof content === 'string') {
|
||||
@ -107,35 +138,7 @@ export class Snapshotter {
|
||||
}
|
||||
this._delegate.onFrameSnapshot(snapshot);
|
||||
});
|
||||
const initScript = `(${frameSnapshotStreamer})("${this._snapshotStreamer}", "${this._snapshotBinding}")`;
|
||||
await this._context._doAddInitScript(initScript);
|
||||
this._runInAllFrames(initScript);
|
||||
}
|
||||
|
||||
private _runInAllFrames(expression: string) {
|
||||
const frames = [];
|
||||
for (const page of this._context.pages())
|
||||
frames.push(...page.frames());
|
||||
frames.map(frame => {
|
||||
frame._existingMainContext()?.rawEvaluate(expression).catch(debugExceptionHandler);
|
||||
});
|
||||
}
|
||||
|
||||
dispose() {
|
||||
helper.removeEventListeners(this._eventListeners);
|
||||
}
|
||||
|
||||
captureSnapshot(page: Page, snapshotName: string, element?: ElementHandle) {
|
||||
// This needs to be sync, as in not awaiting for anything before we issue the command.
|
||||
const expression = `window["${this._snapshotStreamer}"].captureSnapshot(${JSON.stringify(snapshotName)})`;
|
||||
element?.callFunctionNoReply((element: Element, snapshotName: string) => {
|
||||
element.setAttribute('__playwright_target__', snapshotName);
|
||||
}, snapshotName);
|
||||
const snapshotFrame = (frame: Frame) => {
|
||||
const context = frame._existingMainContext();
|
||||
context?.rawEvaluate(expression).catch(debugExceptionHandler);
|
||||
};
|
||||
page.frames().map(frame => snapshotFrame(frame));
|
||||
await Promise.all(snapshots);
|
||||
}
|
||||
|
||||
private _onPage(page: Page) {
|
||||
|
||||
@ -26,12 +26,11 @@ export type SnapshotData = {
|
||||
}[],
|
||||
viewport: { width: number, height: number },
|
||||
url: string,
|
||||
snapshotName?: string,
|
||||
timestamp: number,
|
||||
collectionTime: number,
|
||||
};
|
||||
|
||||
export function frameSnapshotStreamer(snapshotStreamer: string, snapshotBinding: string) {
|
||||
export function frameSnapshotStreamer(snapshotStreamer: string) {
|
||||
// Communication with Playwright.
|
||||
if ((window as any)[snapshotStreamer])
|
||||
return;
|
||||
@ -178,15 +177,6 @@ export function frameSnapshotStreamer(snapshotStreamer: string, snapshotBinding:
|
||||
visitNode(document.documentElement);
|
||||
}
|
||||
|
||||
captureSnapshot(snapshotName: string) {
|
||||
try {
|
||||
const snapshot = this._captureSnapshot(snapshotName);
|
||||
if (snapshot)
|
||||
(window as any)[snapshotBinding](snapshot);
|
||||
} catch (e) {
|
||||
}
|
||||
}
|
||||
|
||||
private _sanitizeUrl(url: string): string {
|
||||
if (url.startsWith('javascript:'))
|
||||
return '';
|
||||
@ -234,7 +224,7 @@ export function frameSnapshotStreamer(snapshotStreamer: string, snapshotBinding:
|
||||
}
|
||||
}
|
||||
|
||||
private _captureSnapshot(snapshotName?: string): SnapshotData | undefined {
|
||||
captureSnapshot(): SnapshotData | undefined {
|
||||
const timestamp = performance.now();
|
||||
const snapshotNumber = ++this._lastSnapshotNumber;
|
||||
let nodeCounter = 0;
|
||||
@ -408,10 +398,8 @@ export function frameSnapshotStreamer(snapshotStreamer: string, snapshotBinding:
|
||||
};
|
||||
|
||||
let html: NodeSnapshot;
|
||||
let htmlEquals = false;
|
||||
if (document.documentElement) {
|
||||
const { equals, n } = visitNode(document.documentElement)!;
|
||||
htmlEquals = equals;
|
||||
const { n } = visitNode(document.documentElement)!;
|
||||
html = n;
|
||||
} else {
|
||||
html = ['html'];
|
||||
@ -426,12 +414,10 @@ export function frameSnapshotStreamer(snapshotStreamer: string, snapshotBinding:
|
||||
height: Math.max(document.body ? document.body.offsetHeight : 0, document.documentElement ? document.documentElement.offsetHeight : 0),
|
||||
},
|
||||
url: location.href,
|
||||
snapshotName,
|
||||
timestamp,
|
||||
collectionTime: 0,
|
||||
};
|
||||
|
||||
let allOverridesAreRefs = true;
|
||||
for (const sheet of this._staleStyleSheets) {
|
||||
if (sheet.href === null)
|
||||
continue;
|
||||
@ -440,16 +426,12 @@ export function frameSnapshotStreamer(snapshotStreamer: string, snapshotBinding:
|
||||
// Unable to capture stylesheet contents.
|
||||
continue;
|
||||
}
|
||||
if (typeof content !== 'number')
|
||||
allOverridesAreRefs = false;
|
||||
const base = this._getSheetBase(sheet);
|
||||
const url = removeHash(this._resolveUrl(base, sheet.href!));
|
||||
result.resourceOverrides.push({ url, content });
|
||||
}
|
||||
|
||||
result.collectionTime = performance.now() - result.timestamp;
|
||||
if (!snapshotName && htmlEquals && allOverridesAreRefs)
|
||||
return undefined;
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
@ -51,8 +51,8 @@ export class TraceSnapshotter extends EventEmitter implements SnapshotterDelegat
|
||||
await this._writeArtifactChain;
|
||||
}
|
||||
|
||||
captureSnapshot(page: Page, snapshotName: string, element?: ElementHandle) {
|
||||
this._snapshotter.captureSnapshot(page, snapshotName, element);
|
||||
async captureSnapshot(page: Page, snapshotName: string, element?: ElementHandle) {
|
||||
await this._snapshotter.captureSnapshot(page, snapshotName, element).catch(() => {});
|
||||
}
|
||||
|
||||
onBlob(blob: SnapshotterBlob): void {
|
||||
|
||||
@ -100,7 +100,7 @@ export class Tracing implements InstrumentationListener {
|
||||
this._context.instrumentation.removeListener(this);
|
||||
helper.removeEventListeners(this._eventListeners);
|
||||
for (const { sdkObject, metadata } of this._pendingCalls.values())
|
||||
this.onAfterCall(sdkObject, metadata);
|
||||
await this.onAfterCall(sdkObject, metadata);
|
||||
for (const page of this._context.pages())
|
||||
page.setScreencastEnabled(false);
|
||||
|
||||
@ -130,38 +130,38 @@ export class Tracing implements InstrumentationListener {
|
||||
return artifact;
|
||||
}
|
||||
|
||||
_captureSnapshot(name: 'before' | 'after' | 'action' | 'event', sdkObject: SdkObject, metadata: CallMetadata, element?: ElementHandle) {
|
||||
async _captureSnapshot(name: 'before' | 'after' | 'action' | 'event', sdkObject: SdkObject, metadata: CallMetadata, element?: ElementHandle) {
|
||||
if (!sdkObject.attribution.page)
|
||||
return;
|
||||
if (!this._snapshotter)
|
||||
return;
|
||||
const snapshotName = `${name}@${metadata.id}`;
|
||||
metadata.snapshots.push({ title: name, snapshotName });
|
||||
this._snapshotter!.captureSnapshot(sdkObject.attribution.page, snapshotName, element);
|
||||
await this._snapshotter!.captureSnapshot(sdkObject.attribution.page, snapshotName, element);
|
||||
}
|
||||
|
||||
async onBeforeCall(sdkObject: SdkObject, metadata: CallMetadata) {
|
||||
this._captureSnapshot('before', sdkObject, metadata);
|
||||
await this._captureSnapshot('before', sdkObject, metadata);
|
||||
this._pendingCalls.set(metadata.id, { sdkObject, metadata });
|
||||
}
|
||||
|
||||
async onBeforeInputAction(sdkObject: SdkObject, metadata: CallMetadata, element: ElementHandle) {
|
||||
this._captureSnapshot('action', sdkObject, metadata, element);
|
||||
await this._captureSnapshot('action', sdkObject, metadata, element);
|
||||
}
|
||||
|
||||
async onAfterCall(sdkObject: SdkObject, metadata: CallMetadata) {
|
||||
if (!this._pendingCalls.has(metadata.id))
|
||||
return;
|
||||
this._captureSnapshot('after', sdkObject, metadata);
|
||||
this._pendingCalls.delete(metadata.id);
|
||||
if (!sdkObject.attribution.page)
|
||||
return;
|
||||
await this._captureSnapshot('after', sdkObject, metadata);
|
||||
const event: trace.ActionTraceEvent = {
|
||||
timestamp: metadata.startTime,
|
||||
type: 'action',
|
||||
metadata,
|
||||
};
|
||||
this._appendTraceEvent(event);
|
||||
this._pendingCalls.delete(metadata.id);
|
||||
}
|
||||
|
||||
onEvent(sdkObject: SdkObject, metadata: CallMetadata) {
|
||||
|
||||
@ -29,7 +29,7 @@ export class WKExecutionContext implements js.ExecutionContextDelegate {
|
||||
constructor(session: WKSession, contextId: number | undefined) {
|
||||
this._session = session;
|
||||
this._contextId = contextId;
|
||||
this._executionContextDestroyedPromise = new Promise((resolve, reject) => {
|
||||
this._executionContextDestroyedPromise = new Promise<void>((resolve, reject) => {
|
||||
this._contextDestroyedCallback = resolve;
|
||||
});
|
||||
}
|
||||
@ -38,7 +38,22 @@ export class WKExecutionContext implements js.ExecutionContextDelegate {
|
||||
this._contextDestroyedCallback();
|
||||
}
|
||||
|
||||
async rawEvaluate(expression: string): Promise<string> {
|
||||
async rawEvaluateJSON(expression: string): Promise<any> {
|
||||
try {
|
||||
const response = await this._session.send('Runtime.evaluate', {
|
||||
expression,
|
||||
contextId: this._contextId,
|
||||
returnByValue: true
|
||||
});
|
||||
if (response.wasThrown)
|
||||
throw new Error('Evaluation failed: ' + response.result.description);
|
||||
return response.result.value;
|
||||
} catch (error) {
|
||||
throw rewriteError(error);
|
||||
}
|
||||
}
|
||||
|
||||
async rawEvaluateHandle(expression: string): Promise<js.ObjectId> {
|
||||
try {
|
||||
const response = await this._session.send('Runtime.evaluate', {
|
||||
expression,
|
||||
|
||||
@ -44,7 +44,10 @@ const PW_LIB_DIRS = [
|
||||
].map(packageName => path.sep + path.join(packageName, 'lib'));
|
||||
|
||||
export function captureStackTrace(): { stack: string, frames: StackFrame[] } {
|
||||
const stackTraceLimit = Error.stackTraceLimit;
|
||||
Error.stackTraceLimit = 30;
|
||||
const stack = new Error().stack!;
|
||||
Error.stackTraceLimit = stackTraceLimit;
|
||||
const frames: StackFrame[] = [];
|
||||
for (const line of stack.split('\n')) {
|
||||
const frame = stackUtils.parseLine(line);
|
||||
|
||||
53
tests/page-evaluate-no-stall.spec.ts
Normal file
53
tests/page-evaluate-no-stall.spec.ts
Normal file
@ -0,0 +1,53 @@
|
||||
/**
|
||||
* 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 './config/pageTest';
|
||||
|
||||
test.describe('non-stalling evaluate', () => {
|
||||
test.beforeEach(async ({mode}) => {
|
||||
test.skip(mode !== 'default');
|
||||
});
|
||||
|
||||
test('should work', async ({page, server, toImpl}) => {
|
||||
await page.goto(server.EMPTY_PAGE);
|
||||
const result = await toImpl(page.mainFrame()).nonStallingRawEvaluateInExistingMainContext('2+2');
|
||||
expect(result).toBe(4);
|
||||
});
|
||||
|
||||
test('should throw while pending navigation', async ({page, server, toImpl}) => {
|
||||
await page.goto(server.EMPTY_PAGE);
|
||||
await page.evaluate(() => document.body.textContent = 'HELLO WORLD');
|
||||
let error;
|
||||
await page.route('**/empty.html', async (route, request) => {
|
||||
error = await toImpl(page.mainFrame()).nonStallingRawEvaluateInExistingMainContext('2+2').catch(e => e);
|
||||
route.abort();
|
||||
});
|
||||
await page.goto(server.EMPTY_PAGE).catch(() => {});
|
||||
expect(error.message).toContain('Frame is currently attempting a navigation');
|
||||
});
|
||||
|
||||
test('should throw when no main execution context', async ({page, toImpl}) => {
|
||||
let errorPromise;
|
||||
page.on('frameattached', frame => {
|
||||
errorPromise = toImpl(frame).nonStallingRawEvaluateInExistingMainContext('2+2').catch(e => e);
|
||||
});
|
||||
await page.setContent('<iframe></iframe>');
|
||||
const error = await errorPromise;
|
||||
// Testing this as a race.
|
||||
const success = error.message === 'Frame does not yet have a main execution context' || 'Frame is currently attempting a navigation';
|
||||
expect(success).toBeTruthy();
|
||||
});
|
||||
});
|
||||
Loading…
x
Reference in New Issue
Block a user