chore(evaluate): implement non-stalling evaluate (#6354)

This commit is contained in:
Pavel Feldman 2021-04-29 09:28:19 -07:00 committed by GitHub
parent 06a92684f3
commit 434f474ce8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 252 additions and 142 deletions

View File

@ -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 {

View File

@ -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,

View File

@ -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;
}

View File

@ -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,

View File

@ -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;
}

View File

@ -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() {

View File

@ -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) {

View File

@ -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) {

View File

@ -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;
}
}

View File

@ -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 {

View File

@ -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) {

View File

@ -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,

View File

@ -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);

View 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();
});
});