mirror of
https://github.com/microsoft/playwright.git
synced 2025-06-26 21:40:17 +00:00
fix(evaluate): reject all context operations when frame detaches (#9987)
This commit is contained in:
parent
61881f3835
commit
c373986ca0
@ -192,7 +192,7 @@ export class CRBrowser extends Browser {
|
|||||||
const serviceWorker = this._serviceWorkers.get(targetId);
|
const serviceWorker = this._serviceWorkers.get(targetId);
|
||||||
if (serviceWorker) {
|
if (serviceWorker) {
|
||||||
this._serviceWorkers.delete(targetId);
|
this._serviceWorkers.delete(targetId);
|
||||||
serviceWorker.emit(Worker.Events.Close);
|
serviceWorker.didClose();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -471,7 +471,7 @@ export class CRBrowserContext extends BrowserContext {
|
|||||||
// asynchronously and we get detached from them later.
|
// asynchronously and we get detached from them later.
|
||||||
// To avoid the wrong order of notifications, we manually fire
|
// To avoid the wrong order of notifications, we manually fire
|
||||||
// "close" event here and forget about the serivce worker.
|
// "close" event here and forget about the serivce worker.
|
||||||
serviceWorker.emit(Worker.Events.Close);
|
serviceWorker.didClose();
|
||||||
this._browser._serviceWorkers.delete(targetId);
|
this._browser._serviceWorkers.delete(targetId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -663,6 +663,7 @@ class FrameSession {
|
|||||||
else if (contextPayload.name === UTILITY_WORLD_NAME)
|
else if (contextPayload.name === UTILITY_WORLD_NAME)
|
||||||
worldName = 'utility';
|
worldName = 'utility';
|
||||||
const context = new dom.FrameExecutionContext(delegate, frame, worldName);
|
const context = new dom.FrameExecutionContext(delegate, frame, worldName);
|
||||||
|
(context as any)[contextDelegateSymbol] = delegate;
|
||||||
if (worldName)
|
if (worldName)
|
||||||
frame._contextCreated(worldName, context);
|
frame._contextCreated(worldName, context);
|
||||||
this._contextIdToContext.set(contextPayload.id, context);
|
this._contextIdToContext.set(contextPayload.id, context);
|
||||||
@ -1135,7 +1136,7 @@ class FrameSession {
|
|||||||
async _adoptBackendNodeId(backendNodeId: Protocol.DOM.BackendNodeId, to: dom.FrameExecutionContext): Promise<dom.ElementHandle> {
|
async _adoptBackendNodeId(backendNodeId: Protocol.DOM.BackendNodeId, to: dom.FrameExecutionContext): Promise<dom.ElementHandle> {
|
||||||
const result = await this._client._sendMayFail('DOM.resolveNode', {
|
const result = await this._client._sendMayFail('DOM.resolveNode', {
|
||||||
backendNodeId,
|
backendNodeId,
|
||||||
executionContextId: (to._delegate as CRExecutionContext)._contextId,
|
executionContextId: ((to as any)[contextDelegateSymbol] as CRExecutionContext)._contextId,
|
||||||
});
|
});
|
||||||
if (!result || result.object.subtype === 'null')
|
if (!result || result.object.subtype === 'null')
|
||||||
throw new Error(dom.kUnableToAdoptErrorMessage);
|
throw new Error(dom.kUnableToAdoptErrorMessage);
|
||||||
@ -1167,3 +1168,5 @@ async function emulateTimezone(session: CRSession, timezoneId: string) {
|
|||||||
throw exception;
|
throw exception;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const contextDelegateSymbol = Symbol('delegate');
|
||||||
|
|||||||
@ -95,7 +95,7 @@ export class FrameExecutionContext extends js.ExecutionContext {
|
|||||||
);
|
);
|
||||||
})();
|
})();
|
||||||
`;
|
`;
|
||||||
this._injectedScriptPromise = this._delegate.rawEvaluateHandle(source).then(objectId => new js.JSHandle(this, 'object', undefined, objectId));
|
this._injectedScriptPromise = this.rawEvaluateHandle(source).then(objectId => new js.JSHandle(this, 'object', undefined, objectId));
|
||||||
}
|
}
|
||||||
return this._injectedScriptPromise;
|
return this._injectedScriptPromise;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -163,6 +163,7 @@ export class FFPage implements PageDelegate {
|
|||||||
else if (!auxData.name)
|
else if (!auxData.name)
|
||||||
worldName = 'main';
|
worldName = 'main';
|
||||||
const context = new dom.FrameExecutionContext(delegate, frame, worldName);
|
const context = new dom.FrameExecutionContext(delegate, frame, worldName);
|
||||||
|
(context as any)[contextDelegateSymbol] = delegate;
|
||||||
if (worldName)
|
if (worldName)
|
||||||
frame._contextCreated(worldName, context);
|
frame._contextCreated(worldName, context);
|
||||||
this._contextIdToContext.set(executionContextId, context);
|
this._contextIdToContext.set(executionContextId, context);
|
||||||
@ -536,7 +537,7 @@ export class FFPage implements PageDelegate {
|
|||||||
const result = await this._session.send('Page.adoptNode', {
|
const result = await this._session.send('Page.adoptNode', {
|
||||||
frameId: handle._context.frame._id,
|
frameId: handle._context.frame._id,
|
||||||
objectId: handle._objectId,
|
objectId: handle._objectId,
|
||||||
executionContextId: (to._delegate as FFExecutionContext)._executionContextId
|
executionContextId: ((to as any)[contextDelegateSymbol] as FFExecutionContext)._executionContextId
|
||||||
});
|
});
|
||||||
if (!result.remoteObject)
|
if (!result.remoteObject)
|
||||||
throw new Error(dom.kUnableToAdoptErrorMessage);
|
throw new Error(dom.kUnableToAdoptErrorMessage);
|
||||||
@ -570,3 +571,5 @@ export class FFPage implements PageDelegate {
|
|||||||
function webSocketId(frameId: string, wsid: string): string {
|
function webSocketId(frameId: string, wsid: string): string {
|
||||||
return `${frameId}---${wsid}`;
|
return `${frameId}---${wsid}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const contextDelegateSymbol = Symbol('delegate');
|
||||||
|
|||||||
@ -35,8 +35,7 @@ import type { ElementStateWithoutStable, FrameExpectParams, InjectedScriptPoll,
|
|||||||
import { isSessionClosedError } from './common/protocolError';
|
import { isSessionClosedError } from './common/protocolError';
|
||||||
|
|
||||||
type ContextData = {
|
type ContextData = {
|
||||||
contextPromise: Promise<dom.FrameExecutionContext>;
|
contextPromise: ManualPromise<dom.FrameExecutionContext | Error>;
|
||||||
contextResolveCallback: (c: dom.FrameExecutionContext) => void;
|
|
||||||
context: dom.FrameExecutionContext | null;
|
context: dom.FrameExecutionContext | null;
|
||||||
rerunnableTasks: Set<RerunnableTask<any>>;
|
rerunnableTasks: Set<RerunnableTask<any>>;
|
||||||
};
|
};
|
||||||
@ -445,8 +444,8 @@ export class Frame extends SdkObject {
|
|||||||
|
|
||||||
this._detachedPromise = new Promise<void>(x => this._detachedCallback = x);
|
this._detachedPromise = new Promise<void>(x => this._detachedCallback = x);
|
||||||
|
|
||||||
this._contextData.set('main', { contextPromise: new Promise(() => {}), contextResolveCallback: () => {}, context: null, rerunnableTasks: new Set() });
|
this._contextData.set('main', { contextPromise: new ManualPromise(), context: null, rerunnableTasks: new Set() });
|
||||||
this._contextData.set('utility', { contextPromise: new Promise(() => {}), contextResolveCallback: () => {}, context: null, rerunnableTasks: new Set() });
|
this._contextData.set('utility', { contextPromise: new ManualPromise(), context: null, rerunnableTasks: new Set() });
|
||||||
this._setContext('main', null);
|
this._setContext('main', null);
|
||||||
this._setContext('utility', null);
|
this._setContext('utility', null);
|
||||||
|
|
||||||
@ -662,9 +661,11 @@ export class Frame extends SdkObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_context(world: types.World): Promise<dom.FrameExecutionContext> {
|
_context(world: types.World): Promise<dom.FrameExecutionContext> {
|
||||||
if (this._detached)
|
return this._contextData.get(world)!.contextPromise.then(contextOrError => {
|
||||||
throw new Error(`Execution Context is not available in detached frame "${this.url()}" (are you trying to evaluate?)`);
|
if (contextOrError instanceof js.ExecutionContext)
|
||||||
return this._contextData.get(world)!.contextPromise;
|
return contextOrError;
|
||||||
|
throw contextOrError;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
_mainContext(): Promise<dom.FrameExecutionContext> {
|
_mainContext(): Promise<dom.FrameExecutionContext> {
|
||||||
@ -1250,9 +1251,13 @@ export class Frame extends SdkObject {
|
|||||||
this._stopNetworkIdleTimer();
|
this._stopNetworkIdleTimer();
|
||||||
this._detached = true;
|
this._detached = true;
|
||||||
this._detachedCallback();
|
this._detachedCallback();
|
||||||
|
const error = new Error('Frame was detached');
|
||||||
for (const data of this._contextData.values()) {
|
for (const data of this._contextData.values()) {
|
||||||
|
if (data.context)
|
||||||
|
data.context.contextDestroyed(error);
|
||||||
|
data.contextPromise.resolve(error);
|
||||||
for (const rerunnableTask of data.rerunnableTasks)
|
for (const rerunnableTask of data.rerunnableTasks)
|
||||||
rerunnableTask.terminate(new Error('waitForFunction failed: frame got detached.'));
|
rerunnableTask.terminate(error);
|
||||||
}
|
}
|
||||||
if (this._parentFrame)
|
if (this._parentFrame)
|
||||||
this._parentFrame._childFrames.delete(this);
|
this._parentFrame._childFrames.delete(this);
|
||||||
@ -1339,13 +1344,11 @@ export class Frame extends SdkObject {
|
|||||||
const data = this._contextData.get(world)!;
|
const data = this._contextData.get(world)!;
|
||||||
data.context = context;
|
data.context = context;
|
||||||
if (context) {
|
if (context) {
|
||||||
data.contextResolveCallback.call(null, context);
|
data.contextPromise.resolve(context);
|
||||||
for (const rerunnableTask of data.rerunnableTasks)
|
for (const rerunnableTask of data.rerunnableTasks)
|
||||||
rerunnableTask.rerun(context);
|
rerunnableTask.rerun(context);
|
||||||
} else {
|
} else {
|
||||||
data.contextPromise = new Promise(fulfill => {
|
data.contextPromise = new ManualPromise();
|
||||||
data.contextResolveCallback = fulfill;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1354,12 +1357,19 @@ export class Frame extends SdkObject {
|
|||||||
// In case of multiple sessions to the same target, there's a race between
|
// In case of multiple sessions to the same target, there's a race between
|
||||||
// connections so we might end up creating multiple isolated worlds.
|
// connections so we might end up creating multiple isolated worlds.
|
||||||
// We can use either.
|
// We can use either.
|
||||||
if (data.context)
|
if (data.context) {
|
||||||
|
data.context.contextDestroyed(new Error('Execution context was destroyed, most likely because of a navigation'));
|
||||||
this._setContext(world, null);
|
this._setContext(world, null);
|
||||||
|
}
|
||||||
this._setContext(world, context);
|
this._setContext(world, context);
|
||||||
}
|
}
|
||||||
|
|
||||||
_contextDestroyed(context: dom.FrameExecutionContext) {
|
_contextDestroyed(context: dom.FrameExecutionContext) {
|
||||||
|
// Sometimes we get this after detach, in which case we should not reset
|
||||||
|
// our already destroyed contexts to something that will never resolve.
|
||||||
|
if (this._detached)
|
||||||
|
return;
|
||||||
|
context.contextDestroyed(new Error('Execution context was destroyed, most likely because of a navigation'));
|
||||||
for (const [world, data] of this._contextData) {
|
for (const [world, data] of this._contextData) {
|
||||||
if (data.context === context)
|
if (data.context === context)
|
||||||
this._setContext(world, null);
|
this._setContext(world, null);
|
||||||
|
|||||||
@ -19,6 +19,7 @@ import * as utilityScriptSource from '../generated/utilityScriptSource';
|
|||||||
import { serializeAsCallArgument } from './common/utilityScriptSerializers';
|
import { serializeAsCallArgument } from './common/utilityScriptSerializers';
|
||||||
import type UtilityScript from './injected/utilityScript';
|
import type UtilityScript from './injected/utilityScript';
|
||||||
import { SdkObject } from './instrumentation';
|
import { SdkObject } from './instrumentation';
|
||||||
|
import { ManualPromise } from '../utils/async';
|
||||||
|
|
||||||
export type ObjectId = string;
|
export type ObjectId = string;
|
||||||
export type RemoteObject = {
|
export type RemoteObject = {
|
||||||
@ -53,14 +54,54 @@ export interface ExecutionContextDelegate {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class ExecutionContext extends SdkObject {
|
export class ExecutionContext extends SdkObject {
|
||||||
readonly _delegate: ExecutionContextDelegate;
|
private _delegate: ExecutionContextDelegate;
|
||||||
private _utilityScriptPromise: Promise<JSHandle> | undefined;
|
private _utilityScriptPromise: Promise<JSHandle> | undefined;
|
||||||
|
private _destroyedPromise = new ManualPromise<Error>();
|
||||||
|
|
||||||
constructor(parent: SdkObject, delegate: ExecutionContextDelegate) {
|
constructor(parent: SdkObject, delegate: ExecutionContextDelegate) {
|
||||||
super(parent, 'execution-context');
|
super(parent, 'execution-context');
|
||||||
this._delegate = delegate;
|
this._delegate = delegate;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
contextDestroyed(error: Error) {
|
||||||
|
this._destroyedPromise.resolve(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
private _raceAgainstContextDestroyed<T>(promise: Promise<T>): Promise<T> {
|
||||||
|
return Promise.race([
|
||||||
|
this._destroyedPromise.then(e => { throw e; }),
|
||||||
|
promise,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
rawEvaluateJSON(expression: string): Promise<any> {
|
||||||
|
return this._raceAgainstContextDestroyed(this._delegate.rawEvaluateJSON(expression));
|
||||||
|
}
|
||||||
|
|
||||||
|
rawEvaluateHandle(expression: string): Promise<ObjectId> {
|
||||||
|
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> {
|
||||||
|
return this._raceAgainstContextDestroyed(this._delegate.evaluateWithArguments(expression, returnByValue, utilityScript, values, objectIds));
|
||||||
|
}
|
||||||
|
|
||||||
|
getProperties(context: ExecutionContext, objectId: ObjectId): Promise<Map<string, JSHandle>> {
|
||||||
|
return this._raceAgainstContextDestroyed(this._delegate.getProperties(context, objectId));
|
||||||
|
}
|
||||||
|
|
||||||
|
createHandle(remoteObject: RemoteObject): JSHandle {
|
||||||
|
return this._delegate.createHandle(this, remoteObject);
|
||||||
|
}
|
||||||
|
|
||||||
|
releaseHandle(objectId: ObjectId): Promise<void> {
|
||||||
|
return this._delegate.releaseHandle(objectId);
|
||||||
|
}
|
||||||
|
|
||||||
async waitForSignalsCreatedBy<T>(action: () => Promise<T>): Promise<T> {
|
async waitForSignalsCreatedBy<T>(action: () => Promise<T>): Promise<T> {
|
||||||
return action();
|
return action();
|
||||||
}
|
}
|
||||||
@ -76,19 +117,11 @@ export class ExecutionContext extends SdkObject {
|
|||||||
${utilityScriptSource.source}
|
${utilityScriptSource.source}
|
||||||
return new pwExport();
|
return new pwExport();
|
||||||
})();`;
|
})();`;
|
||||||
this._utilityScriptPromise = this._delegate.rawEvaluateHandle(source).then(objectId => new JSHandle(this, 'object', undefined, objectId));
|
this._utilityScriptPromise = this._raceAgainstContextDestroyed(this._delegate.rawEvaluateHandle(source).then(objectId => new JSHandle(this, 'object', undefined, objectId)));
|
||||||
}
|
}
|
||||||
return this._utilityScriptPromise;
|
return this._utilityScriptPromise;
|
||||||
}
|
}
|
||||||
|
|
||||||
createHandle(remoteObject: RemoteObject): JSHandle {
|
|
||||||
return this._delegate.createHandle(this, remoteObject);
|
|
||||||
}
|
|
||||||
|
|
||||||
async rawEvaluateJSON(expression: string): Promise<any> {
|
|
||||||
return await this._delegate.rawEvaluateJSON(expression);
|
|
||||||
}
|
|
||||||
|
|
||||||
async doSlowMo() {
|
async doSlowMo() {
|
||||||
// overridden in FrameExecutionContext
|
// overridden in FrameExecutionContext
|
||||||
}
|
}
|
||||||
@ -113,7 +146,7 @@ export class JSHandle<T = any> extends SdkObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
callFunctionNoReply(func: Function, arg: any) {
|
callFunctionNoReply(func: Function, arg: any) {
|
||||||
this._context._delegate.rawCallFunctionNoReply(func, this, arg);
|
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> {
|
||||||
@ -145,7 +178,7 @@ export class JSHandle<T = any> extends SdkObject {
|
|||||||
async getProperties(): Promise<Map<string, JSHandle>> {
|
async getProperties(): Promise<Map<string, JSHandle>> {
|
||||||
if (!this._objectId)
|
if (!this._objectId)
|
||||||
return new Map();
|
return new Map();
|
||||||
return this._context._delegate.getProperties(this._context, this._objectId);
|
return this._context.getProperties(this._context, this._objectId);
|
||||||
}
|
}
|
||||||
|
|
||||||
rawValue() {
|
rawValue() {
|
||||||
@ -157,7 +190,7 @@ export class JSHandle<T = any> extends SdkObject {
|
|||||||
return this._value;
|
return this._value;
|
||||||
const utilityScript = await this._context.utilityScript();
|
const utilityScript = await this._context.utilityScript();
|
||||||
const script = `(utilityScript, ...args) => utilityScript.jsonValue(...args)`;
|
const script = `(utilityScript, ...args) => utilityScript.jsonValue(...args)`;
|
||||||
return this._context._delegate.evaluateWithArguments(script, true, utilityScript, [true], [this._objectId]);
|
return this._context.evaluateWithArguments(script, true, utilityScript, [true], [this._objectId]);
|
||||||
}
|
}
|
||||||
|
|
||||||
asElement(): dom.ElementHandle | null {
|
asElement(): dom.ElementHandle | null {
|
||||||
@ -169,7 +202,7 @@ export class JSHandle<T = any> extends SdkObject {
|
|||||||
return;
|
return;
|
||||||
this._disposed = true;
|
this._disposed = true;
|
||||||
if (this._objectId)
|
if (this._objectId)
|
||||||
this._context._delegate.releaseHandle(this._objectId).catch(e => {});
|
this._context.releaseHandle(this._objectId).catch(e => {});
|
||||||
}
|
}
|
||||||
|
|
||||||
override toString(): string {
|
override toString(): string {
|
||||||
@ -232,7 +265,7 @@ export async function evaluateExpression(context: ExecutionContext, returnByValu
|
|||||||
|
|
||||||
const script = `(utilityScript, ...args) => utilityScript.evaluate(...args)`;
|
const script = `(utilityScript, ...args) => utilityScript.evaluate(...args)`;
|
||||||
try {
|
try {
|
||||||
return await context._delegate.evaluateWithArguments(script, returnByValue, utilityScript, utilityScriptValues, utilityScriptObjectIds);
|
return await context.evaluateWithArguments(script, returnByValue, utilityScript, utilityScriptValues, utilityScriptObjectIds);
|
||||||
} finally {
|
} finally {
|
||||||
toDispose.map(handlePromise => handlePromise.then(handle => handle.dispose()));
|
toDispose.map(handlePromise => handlePromise.then(handle => handle.dispose()));
|
||||||
}
|
}
|
||||||
|
|||||||
@ -464,13 +464,13 @@ export class Page extends SdkObject {
|
|||||||
const worker = this._workers.get(workerId);
|
const worker = this._workers.get(workerId);
|
||||||
if (!worker)
|
if (!worker)
|
||||||
return;
|
return;
|
||||||
worker.emit(Worker.Events.Close, worker);
|
worker.didClose();
|
||||||
this._workers.delete(workerId);
|
this._workers.delete(workerId);
|
||||||
}
|
}
|
||||||
|
|
||||||
_clearWorkers() {
|
_clearWorkers() {
|
||||||
for (const [workerId, worker] of this._workers) {
|
for (const [workerId, worker] of this._workers) {
|
||||||
worker.emit(Worker.Events.Close, worker);
|
worker.didClose();
|
||||||
this._workers.delete(workerId);
|
this._workers.delete(workerId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -547,6 +547,12 @@ export class Worker extends SdkObject {
|
|||||||
return this._url;
|
return this._url;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
didClose() {
|
||||||
|
if (this._existingExecutionContext)
|
||||||
|
this._existingExecutionContext.contextDestroyed(new Error('Worker was closed'));
|
||||||
|
this.emit(Worker.Events.Close, this);
|
||||||
|
}
|
||||||
|
|
||||||
async evaluateExpression(expression: string, isFunction: boolean | undefined, arg: any): Promise<any> {
|
async evaluateExpression(expression: string, isFunction: boolean | undefined, arg: any): Promise<any> {
|
||||||
return js.evaluateExpression(await this._executionContextPromise, true /* returnByValue */, expression, isFunction, arg);
|
return js.evaluateExpression(await this._executionContextPromise, true /* returnByValue */, expression, isFunction, arg);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -24,19 +24,10 @@ import { isSessionClosedError } from '../common/protocolError';
|
|||||||
export class WKExecutionContext implements js.ExecutionContextDelegate {
|
export class WKExecutionContext implements js.ExecutionContextDelegate {
|
||||||
private readonly _session: WKSession;
|
private readonly _session: WKSession;
|
||||||
readonly _contextId: number | undefined;
|
readonly _contextId: number | undefined;
|
||||||
private _contextDestroyedCallback: () => void = () => {};
|
|
||||||
private readonly _executionContextDestroyedPromise: Promise<unknown>;
|
|
||||||
|
|
||||||
constructor(session: WKSession, contextId: number | undefined) {
|
constructor(session: WKSession, contextId: number | undefined) {
|
||||||
this._session = session;
|
this._session = session;
|
||||||
this._contextId = contextId;
|
this._contextId = contextId;
|
||||||
this._executionContextDestroyedPromise = new Promise<void>((resolve, reject) => {
|
|
||||||
this._contextDestroyedCallback = resolve;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
_dispose() {
|
|
||||||
this._contextDestroyedCallback();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async rawEvaluateJSON(expression: string): Promise<any> {
|
async rawEvaluateJSON(expression: string): Promise<any> {
|
||||||
@ -81,9 +72,7 @@ export class WKExecutionContext implements js.ExecutionContextDelegate {
|
|||||||
|
|
||||||
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 Promise.race([
|
const response = await this._session.send('Runtime.callFunctionOn', {
|
||||||
this._executionContextDestroyedPromise.then(() => { throw new Error(contextDestroyedError); }),
|
|
||||||
this._session.send('Runtime.callFunctionOn', {
|
|
||||||
functionDeclaration: expression,
|
functionDeclaration: expression,
|
||||||
objectId: utilityScript._objectId!,
|
objectId: utilityScript._objectId!,
|
||||||
arguments: [
|
arguments: [
|
||||||
@ -94,8 +83,7 @@ export class WKExecutionContext implements js.ExecutionContextDelegate {
|
|||||||
returnByValue,
|
returnByValue,
|
||||||
emulateUserGesture: true,
|
emulateUserGesture: true,
|
||||||
awaitPromise: true
|
awaitPromise: true
|
||||||
})
|
});
|
||||||
]);
|
|
||||||
if (response.wasThrown)
|
if (response.wasThrown)
|
||||||
throw new js.JavaScriptErrorInEvaluate(response.result.description);
|
throw new js.JavaScriptErrorInEvaluate(response.result.description);
|
||||||
if (returnByValue)
|
if (returnByValue)
|
||||||
@ -130,8 +118,6 @@ export class WKExecutionContext implements js.ExecutionContextDelegate {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const contextDestroyedError = 'Execution context was destroyed.';
|
|
||||||
|
|
||||||
function potentiallyUnserializableValue(remoteObject: Protocol.Runtime.RemoteObject): any {
|
function potentiallyUnserializableValue(remoteObject: Protocol.Runtime.RemoteObject): any {
|
||||||
const value = remoteObject.value;
|
const value = remoteObject.value;
|
||||||
const isUnserializable = remoteObject.type === 'number' && ['NaN', '-Infinity', 'Infinity', '-0'].includes(remoteObject.description!);
|
const isUnserializable = remoteObject.type === 'number' && ['NaN', '-Infinity', 'Infinity', '-0'].includes(remoteObject.description!);
|
||||||
|
|||||||
@ -452,7 +452,6 @@ export class WKPage implements PageDelegate {
|
|||||||
private _removeContextsForFrame(frame: frames.Frame, notifyFrame: boolean) {
|
private _removeContextsForFrame(frame: frames.Frame, notifyFrame: boolean) {
|
||||||
for (const [contextId, context] of this._contextIdToContext) {
|
for (const [contextId, context] of this._contextIdToContext) {
|
||||||
if (context.frame === frame) {
|
if (context.frame === frame) {
|
||||||
(context._delegate as WKExecutionContext)._dispose();
|
|
||||||
this._contextIdToContext.delete(contextId);
|
this._contextIdToContext.delete(contextId);
|
||||||
if (notifyFrame)
|
if (notifyFrame)
|
||||||
frame._contextDestroyed(context);
|
frame._contextDestroyed(context);
|
||||||
@ -473,6 +472,7 @@ export class WKPage implements PageDelegate {
|
|||||||
else if (contextPayload.type === 'user' && contextPayload.name === UTILITY_WORLD_NAME)
|
else if (contextPayload.type === 'user' && contextPayload.name === UTILITY_WORLD_NAME)
|
||||||
worldName = 'utility';
|
worldName = 'utility';
|
||||||
const context = new dom.FrameExecutionContext(delegate, frame, worldName);
|
const context = new dom.FrameExecutionContext(delegate, frame, worldName);
|
||||||
|
(context as any)[contextDelegateSymbol] = delegate;
|
||||||
if (worldName)
|
if (worldName)
|
||||||
frame._contextCreated(worldName, context);
|
frame._contextCreated(worldName, context);
|
||||||
if (contextPayload.type === 'normal' && frame === this._page.mainFrame())
|
if (contextPayload.type === 'normal' && frame === this._page.mainFrame())
|
||||||
@ -902,7 +902,7 @@ export class WKPage implements PageDelegate {
|
|||||||
async adoptElementHandle<T extends Node>(handle: dom.ElementHandle<T>, to: dom.FrameExecutionContext): Promise<dom.ElementHandle<T>> {
|
async adoptElementHandle<T extends Node>(handle: dom.ElementHandle<T>, to: dom.FrameExecutionContext): Promise<dom.ElementHandle<T>> {
|
||||||
const result = await this._session.sendMayFail('DOM.resolveNode', {
|
const result = await this._session.sendMayFail('DOM.resolveNode', {
|
||||||
objectId: handle._objectId,
|
objectId: handle._objectId,
|
||||||
executionContextId: (to._delegate as WKExecutionContext)._contextId
|
executionContextId: ((to as any)[contextDelegateSymbol] as WKExecutionContext)._contextId
|
||||||
});
|
});
|
||||||
if (!result || result.object.subtype === 'null')
|
if (!result || result.object.subtype === 'null')
|
||||||
throw new Error(dom.kUnableToAdoptErrorMessage);
|
throw new Error(dom.kUnableToAdoptErrorMessage);
|
||||||
@ -1139,3 +1139,5 @@ function isLoadedSecurely(url: string, timing: network.ResourceTiming) {
|
|||||||
return true;
|
return true;
|
||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const contextDelegateSymbol = Symbol('delegate');
|
||||||
|
|||||||
@ -113,7 +113,7 @@ it('should throw for detached frames', async ({ page, server }) => {
|
|||||||
await detachFrame(page, 'frame1');
|
await detachFrame(page, 'frame1');
|
||||||
let error = null;
|
let error = null;
|
||||||
await frame1.evaluate(() => 7 * 8).catch(e => error = e);
|
await frame1.evaluate(() => 7 * 8).catch(e => error = e);
|
||||||
expect(error.message).toContain('Execution Context is not available in detached frame');
|
expect(error.message).toContain('frame.evaluate: Frame was detached');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should be isolated between frames', async ({ page, server }) => {
|
it('should be isolated between frames', async ({ page, server }) => {
|
||||||
|
|||||||
@ -15,6 +15,7 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { attachFrame, detachFrame } from '../config/utils';
|
||||||
import { test as it, expect } from './pageTest';
|
import { test as it, expect } from './pageTest';
|
||||||
|
|
||||||
it('should work', async ({ page }) => {
|
it('should work', async ({ page }) => {
|
||||||
@ -570,3 +571,13 @@ it('should not use toJSON in jsonValue', async ({ page }) => {
|
|||||||
it('should not expose the injected script export', async ({ page }) => {
|
it('should not expose the injected script export', async ({ page }) => {
|
||||||
expect(await page.evaluate('typeof pwExport === "undefined"')).toBe(true);
|
expect(await page.evaluate('typeof pwExport === "undefined"')).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should throw when frame is detached', async ({ page, server }) => {
|
||||||
|
await attachFrame(page, 'frame1', server.EMPTY_PAGE);
|
||||||
|
const frame = page.frames()[1];
|
||||||
|
const promise = frame.evaluate(() => new Promise<void>(() => {})).catch(e => e);
|
||||||
|
await detachFrame(page, 'frame1');
|
||||||
|
const error = await promise;
|
||||||
|
expect(error).toBeTruthy();
|
||||||
|
expect(error.message).toMatch(/frame.evaluate: (Frame was detached|Execution context was destroyed)/);
|
||||||
|
});
|
||||||
|
|||||||
@ -15,6 +15,7 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { attachFrame, detachFrame } from '../config/utils';
|
||||||
import { test as it, expect } from './pageTest';
|
import { test as it, expect } from './pageTest';
|
||||||
|
|
||||||
it('should timeout', async ({ page }) => {
|
it('should timeout', async ({ page }) => {
|
||||||
@ -264,3 +265,13 @@ it('should not be called after finishing unsuccessfully', async ({ page, server
|
|||||||
|
|
||||||
expect(messages.join('|')).toBe('waitForFunction1|waitForFunction2|waitForFunction3');
|
expect(messages.join('|')).toBe('waitForFunction1|waitForFunction2|waitForFunction3');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should throw when frame is detached', async ({ page, server }) => {
|
||||||
|
await attachFrame(page, 'frame1', server.EMPTY_PAGE);
|
||||||
|
const frame = page.frames()[1];
|
||||||
|
const promise = frame.waitForFunction(() => false).catch(e => e);
|
||||||
|
await detachFrame(page, 'frame1');
|
||||||
|
const error = await promise;
|
||||||
|
expect(error).toBeTruthy();
|
||||||
|
expect(error.message).toMatch(/frame.waitForFunction: (Frame was detached|Execution context was destroyed)/);
|
||||||
|
});
|
||||||
|
|||||||
@ -81,7 +81,7 @@ it('elementHandle.waitForSelector should throw on navigation', async ({ page, se
|
|||||||
await page.evaluate(() => 1);
|
await page.evaluate(() => 1);
|
||||||
await page.goto(server.EMPTY_PAGE);
|
await page.goto(server.EMPTY_PAGE);
|
||||||
const error = await promise;
|
const error = await promise;
|
||||||
expect(error.message).toContain('Execution context was destroyed, most likely because of a navigation');
|
expect(error.message).toContain('Execution context was destroyed');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should work with removed MutationObserver', async ({ page, server }) => {
|
it('should work with removed MutationObserver', async ({ page, server }) => {
|
||||||
@ -242,5 +242,5 @@ it('should throw when frame is detached', async ({ page, server }) => {
|
|||||||
await detachFrame(page, 'frame1');
|
await detachFrame(page, 'frame1');
|
||||||
await waitPromise;
|
await waitPromise;
|
||||||
expect(waitError).toBeTruthy();
|
expect(waitError).toBeTruthy();
|
||||||
expect(waitError.message).toContain('waitForFunction failed: frame got detached.');
|
expect(waitError.message).toContain('frame.waitForSelector: Frame was detached');
|
||||||
});
|
});
|
||||||
|
|||||||
@ -236,7 +236,7 @@ it('should throw when frame is detached xpath', async ({ page, server }) => {
|
|||||||
await detachFrame(page, 'frame1');
|
await detachFrame(page, 'frame1');
|
||||||
await waitPromise;
|
await waitPromise;
|
||||||
expect(waitError).toBeTruthy();
|
expect(waitError).toBeTruthy();
|
||||||
expect(waitError.message).toContain('waitForFunction failed: frame got detached.');
|
expect(waitError.message).toContain('frame.waitForSelector: Frame was detached');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return the element handle xpath', async ({ page, server }) => {
|
it('should return the element handle xpath', async ({ page, server }) => {
|
||||||
|
|||||||
@ -41,7 +41,7 @@ it('should emit created and destroyed events', async function({ page }) {
|
|||||||
await page.evaluate(workerObj => workerObj.terminate(), workerObj);
|
await page.evaluate(workerObj => workerObj.terminate(), workerObj);
|
||||||
expect(await workerDestroyedPromise).toBe(worker);
|
expect(await workerDestroyedPromise).toBe(worker);
|
||||||
const error = await workerThisObj.getProperty('self').catch(error => error);
|
const error = await workerThisObj.getProperty('self').catch(error => error);
|
||||||
expect(error.message).toContain('Target closed');
|
expect(error.message).toMatch(/jSHandle.getProperty: (Worker was closed|Target closed)/);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should report console logs', async function({ page }) {
|
it('should report console logs', async function({ page }) {
|
||||||
|
|||||||
@ -174,7 +174,7 @@ test.beforeAll(async function recordTrace({ browser, browserName, browserType, s
|
|||||||
|
|
||||||
test('should show empty trace viewer', async ({ showTraceViewer }, testInfo) => {
|
test('should show empty trace viewer', async ({ showTraceViewer }, testInfo) => {
|
||||||
const traceViewer = await showTraceViewer(testInfo.outputPath());
|
const traceViewer = await showTraceViewer(testInfo.outputPath());
|
||||||
expect(await traceViewer.page.title()).toBe('Playwright Trace Viewer');
|
await expect(traceViewer.page).toHaveTitle('Playwright Trace Viewer');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should open simple trace viewer', async ({ showTraceViewer }) => {
|
test('should open simple trace viewer', async ({ showTraceViewer }) => {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user