mirror of
https://github.com/microsoft/playwright.git
synced 2024-12-14 13:45:36 +03:00
chore(logs): rework logs for simplicity (#2592)
This commit is contained in:
parent
4b2efd6e3e
commit
c220fc7f46
@ -22,7 +22,7 @@ export { Dialog } from './dialog';
|
||||
export { Download } from './download';
|
||||
export { ElementHandle } from './dom';
|
||||
export { FileChooser } from './fileChooser';
|
||||
export { Logger } from './logger';
|
||||
export { Logger } from './types';
|
||||
export { TimeoutError } from './errors';
|
||||
export { Frame } from './frames';
|
||||
export { Keyboard, Mouse } from './input';
|
||||
|
@ -20,11 +20,11 @@ import { EventEmitter } from 'events';
|
||||
import { Download } from './download';
|
||||
import type { BrowserServer } from './server/browserServer';
|
||||
import { Events } from './events';
|
||||
import { InnerLogger } from './logger';
|
||||
import { Loggers } from './logger';
|
||||
import { ProxySettings } from './types';
|
||||
|
||||
export type BrowserOptions = {
|
||||
logger: InnerLogger,
|
||||
loggers: Loggers,
|
||||
downloadsPath?: string,
|
||||
headful?: boolean,
|
||||
persistent?: PersistentContextOptions, // Undefined means no persistent context.
|
||||
|
@ -25,7 +25,7 @@ import * as types from './types';
|
||||
import { Events } from './events';
|
||||
import { Download } from './download';
|
||||
import { BrowserBase } from './browser';
|
||||
import { InnerLogger, Logger } from './logger';
|
||||
import { Loggers, Logger } from './logger';
|
||||
import { EventEmitter } from 'events';
|
||||
import { ProgressController } from './progress';
|
||||
import { DebugController } from './debug/debugController';
|
||||
@ -52,7 +52,7 @@ type CommonContextOptions = {
|
||||
|
||||
export type PersistentContextOptions = CommonContextOptions;
|
||||
export type BrowserContextOptions = CommonContextOptions & {
|
||||
logger?: Logger,
|
||||
logger?: types.Logger,
|
||||
};
|
||||
|
||||
export interface BrowserContext {
|
||||
@ -89,14 +89,15 @@ export abstract class BrowserContextBase extends EventEmitter implements Browser
|
||||
readonly _permissions = new Map<string, string[]>();
|
||||
readonly _downloads = new Set<Download>();
|
||||
readonly _browserBase: BrowserBase;
|
||||
readonly _logger: InnerLogger;
|
||||
readonly _apiLogger: Logger;
|
||||
private _debugController: DebugController | undefined;
|
||||
|
||||
constructor(browserBase: BrowserBase, options: BrowserContextOptions) {
|
||||
super();
|
||||
this._browserBase = browserBase;
|
||||
this._options = options;
|
||||
this._logger = options.logger ? new InnerLogger(options.logger) : browserBase._options.logger;
|
||||
const loggers = options.logger ? new Loggers(options.logger) : browserBase._options.loggers;
|
||||
this._apiLogger = loggers.api;
|
||||
this._closePromise = new Promise(fulfill => this._closePromiseFulfill = fulfill);
|
||||
}
|
||||
|
||||
@ -115,7 +116,7 @@ export abstract class BrowserContextBase extends EventEmitter implements Browser
|
||||
|
||||
async waitForEvent(event: string, optionsOrPredicate: types.WaitForEventOptions = {}): Promise<any> {
|
||||
const options = typeof optionsOrPredicate === 'function' ? { predicate: optionsOrPredicate } : optionsOrPredicate;
|
||||
const progressController = new ProgressController(this._logger, this._timeoutSettings.timeout(options));
|
||||
const progressController = new ProgressController(this._apiLogger, this._timeoutSettings.timeout(options), 'browserContext.waitForEvent');
|
||||
if (event !== Events.BrowserContext.Close)
|
||||
this._closePromise.then(error => progressController.abort(error));
|
||||
return progressController.run(progress => helper.waitForEvent(progress, this, event, options.predicate));
|
||||
|
@ -46,7 +46,7 @@ export class CRBrowser extends BrowserBase {
|
||||
private _tracingClient: CRSession | undefined;
|
||||
|
||||
static async connect(transport: ConnectionTransport, options: BrowserOptions, devtools?: CRDevTools): Promise<CRBrowser> {
|
||||
const connection = new CRConnection(SlowMoTransport.wrap(transport, options.slowMo), options.logger);
|
||||
const connection = new CRConnection(SlowMoTransport.wrap(transport, options.slowMo), options.loggers);
|
||||
const browser = new CRBrowser(connection, options);
|
||||
browser._devtools = devtools;
|
||||
const session = connection.rootSession;
|
||||
|
@ -16,10 +16,10 @@
|
||||
*/
|
||||
|
||||
import { assert } from '../helper';
|
||||
import { ConnectionTransport, ProtocolRequest, ProtocolResponse, protocolLog } from '../transport';
|
||||
import { ConnectionTransport, ProtocolRequest, ProtocolResponse } from '../transport';
|
||||
import { Protocol } from './protocol';
|
||||
import { EventEmitter } from 'events';
|
||||
import { InnerLogger, errorLog } from '../logger';
|
||||
import { Loggers, Logger } from '../logger';
|
||||
import { rewriteErrorMessage } from '../utils/stackTrace';
|
||||
|
||||
export const ConnectionEvents = {
|
||||
@ -36,12 +36,12 @@ export class CRConnection extends EventEmitter {
|
||||
private readonly _sessions = new Map<string, CRSession>();
|
||||
readonly rootSession: CRSession;
|
||||
_closed = false;
|
||||
readonly _logger: InnerLogger;
|
||||
readonly _logger: Logger;
|
||||
|
||||
constructor(transport: ConnectionTransport, logger: InnerLogger) {
|
||||
constructor(transport: ConnectionTransport, loggers: Loggers) {
|
||||
super();
|
||||
this._transport = transport;
|
||||
this._logger = logger;
|
||||
this._logger = loggers.protocol;
|
||||
this._transport.onmessage = this._onMessage.bind(this);
|
||||
this._transport.onclose = this._onClose.bind(this);
|
||||
this.rootSession = new CRSession(this, '', 'browser', '');
|
||||
@ -62,15 +62,15 @@ export class CRConnection extends EventEmitter {
|
||||
const message: ProtocolRequest = { id, method, params };
|
||||
if (sessionId)
|
||||
message.sessionId = sessionId;
|
||||
if (this._logger.isLogEnabled(protocolLog))
|
||||
this._logger.log(protocolLog, 'SEND ► ' + rewriteInjectedScriptEvaluationLog(message));
|
||||
if (this._logger.isEnabled())
|
||||
this._logger.info('SEND ► ' + rewriteInjectedScriptEvaluationLog(message));
|
||||
this._transport.send(message);
|
||||
return id;
|
||||
}
|
||||
|
||||
async _onMessage(message: ProtocolResponse) {
|
||||
if (this._logger.isLogEnabled(protocolLog))
|
||||
this._logger.log(protocolLog, '◀ RECV ' + JSON.stringify(message));
|
||||
if (this._logger.isEnabled())
|
||||
this._logger.info('◀ RECV ' + JSON.stringify(message));
|
||||
if (message.id === kBrowserCloseMessageId)
|
||||
return;
|
||||
if (message.method === 'Target.attachedToTarget') {
|
||||
@ -166,9 +166,9 @@ export class CRSession extends EventEmitter {
|
||||
}
|
||||
|
||||
_sendMayFail<T extends keyof Protocol.CommandParameters>(method: T, params?: Protocol.CommandParameters[T]): Promise<Protocol.CommandReturnValues[T] | void> {
|
||||
return this.send(method, params).catch(error => {
|
||||
return this.send(method, params).catch((error: Error) => {
|
||||
if (this._connection)
|
||||
this._connection._logger.log(errorLog, error, []);
|
||||
this._connection._logger.error(error);
|
||||
});
|
||||
}
|
||||
|
||||
|
94
src/dom.ts
94
src/dom.ts
@ -28,8 +28,7 @@ import * as js from './javascript';
|
||||
import { Page } from './page';
|
||||
import { selectors } from './selectors';
|
||||
import * as types from './types';
|
||||
import { apiLog } from './logger';
|
||||
import { Progress, runAbortableTask } from './progress';
|
||||
import { Progress, ProgressController } from './progress';
|
||||
import DebugScript from './debug/injected/debugScript';
|
||||
|
||||
export type PointerActionOptions = {
|
||||
@ -120,6 +119,11 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
|
||||
this._initializePreview().catch(e => {});
|
||||
}
|
||||
|
||||
private _runAbortableTask<T>(task: (progress: Progress) => Promise<T>, timeout: number, apiName: string): Promise<T> {
|
||||
const controller = new ProgressController(this._page._logger, timeout, `elementHandle.${apiName}`);
|
||||
return controller.run(task);
|
||||
}
|
||||
|
||||
async _initializePreview() {
|
||||
const utility = await this._context.injectedScript();
|
||||
this._preview = await utility.evaluate((injected, e) => 'JSHandle@' + injected.previewNode(e), this);
|
||||
@ -268,25 +272,25 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
|
||||
async _retryPointerAction(progress: Progress, action: (point: types.Point) => Promise<void>, options: PointerActionOptions & types.PointerActionWaitOptions & types.NavigatingActionWaitOptions): Promise<'notconnected' | 'done'> {
|
||||
let first = true;
|
||||
while (progress.isRunning()) {
|
||||
progress.log(apiLog, `${first ? 'attempting' : 'retrying'} ${progress.apiName} action`);
|
||||
progress.logger.info(`${first ? 'attempting' : 'retrying'} ${progress.apiName} action`);
|
||||
const result = await this._performPointerAction(progress, action, options);
|
||||
first = false;
|
||||
if (result === 'notvisible') {
|
||||
if (options.force)
|
||||
throw new Error('Element is not visible');
|
||||
progress.log(apiLog, ' element is not visible');
|
||||
progress.logger.info(' element is not visible');
|
||||
continue;
|
||||
}
|
||||
if (result === 'notinviewport') {
|
||||
if (options.force)
|
||||
throw new Error('Element is outside of the viewport');
|
||||
progress.log(apiLog, ' element is outside of the viewport');
|
||||
progress.logger.info(' element is outside of the viewport');
|
||||
continue;
|
||||
}
|
||||
if (result === 'nothittarget') {
|
||||
if (options.force)
|
||||
throw new Error('Element does not receive pointer events');
|
||||
progress.log(apiLog, ' element does not receive pointer events');
|
||||
progress.logger.info(' element does not receive pointer events');
|
||||
continue;
|
||||
}
|
||||
if (result === 'notconnected')
|
||||
@ -306,14 +310,14 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
|
||||
if ((options as any).__testHookAfterStable)
|
||||
await (options as any).__testHookAfterStable();
|
||||
|
||||
progress.log(apiLog, ' scrolling into view if needed');
|
||||
progress.logger.info(' scrolling into view if needed');
|
||||
progress.throwIfAborted(); // Avoid action that has side-effects.
|
||||
const scrolled = await this._scrollRectIntoViewIfNeeded(position ? { x: position.x, y: position.y, width: 0, height: 0 } : undefined);
|
||||
if (scrolled === 'notvisible')
|
||||
return 'notvisible';
|
||||
if (scrolled === 'notconnected')
|
||||
return 'notconnected';
|
||||
progress.log(apiLog, ' done scrolling');
|
||||
progress.logger.info(' done scrolling');
|
||||
|
||||
const maybePoint = position ? await this._offsetPoint(position) : await this._clickablePoint();
|
||||
if (maybePoint === 'notvisible')
|
||||
@ -325,13 +329,13 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
|
||||
if (!force) {
|
||||
if ((options as any).__testHookBeforeHitTarget)
|
||||
await (options as any).__testHookBeforeHitTarget();
|
||||
progress.log(apiLog, ` checking that element receives pointer events at (${point.x},${point.y})`);
|
||||
progress.logger.info(` checking that element receives pointer events at (${point.x},${point.y})`);
|
||||
const hitTargetResult = await this._checkHitTargetAt(point);
|
||||
if (hitTargetResult === 'notconnected')
|
||||
return 'notconnected';
|
||||
if (hitTargetResult === 'nothittarget')
|
||||
return 'nothittarget';
|
||||
progress.log(apiLog, ` element does receive pointer events`);
|
||||
progress.logger.info(` element does receive pointer events`);
|
||||
}
|
||||
|
||||
await this._page._frameManager.waitForSignalsCreatedBy(progress, options.noWaitAfter, async () => {
|
||||
@ -341,24 +345,24 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
|
||||
let restoreModifiers: input.Modifier[] | undefined;
|
||||
if (options && options.modifiers)
|
||||
restoreModifiers = await this._page.keyboard._ensureModifiers(options.modifiers);
|
||||
progress.log(apiLog, ` performing ${progress.apiName} action`);
|
||||
progress.logger.info(` performing ${progress.apiName} action`);
|
||||
await action(point);
|
||||
progress.log(apiLog, ` ${progress.apiName} action done`);
|
||||
progress.log(apiLog, ' waiting for scheduled navigations to finish');
|
||||
progress.logger.info(` ${progress.apiName} action done`);
|
||||
progress.logger.info(' waiting for scheduled navigations to finish');
|
||||
if ((options as any).__testHookAfterPointerAction)
|
||||
await (options as any).__testHookAfterPointerAction();
|
||||
if (restoreModifiers)
|
||||
await this._page.keyboard._ensureModifiers(restoreModifiers);
|
||||
}, 'input');
|
||||
progress.log(apiLog, ' navigations have finished');
|
||||
progress.logger.info(' navigations have finished');
|
||||
|
||||
return 'done';
|
||||
}
|
||||
|
||||
hover(options: PointerActionOptions & types.PointerActionWaitOptions = {}): Promise<void> {
|
||||
return runAbortableTask(async progress => {
|
||||
return this._runAbortableTask(async progress => {
|
||||
throwIfNotConnected(await this._hover(progress, options));
|
||||
}, this._page._logger, this._page._timeoutSettings.timeout(options));
|
||||
}, this._page._timeoutSettings.timeout(options), 'hover');
|
||||
}
|
||||
|
||||
_hover(progress: Progress, options: PointerActionOptions & types.PointerActionWaitOptions): Promise<'notconnected' | 'done'> {
|
||||
@ -366,9 +370,9 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
|
||||
}
|
||||
|
||||
click(options: ClickOptions & types.PointerActionWaitOptions & types.NavigatingActionWaitOptions = {}): Promise<void> {
|
||||
return runAbortableTask(async progress => {
|
||||
return this._runAbortableTask(async progress => {
|
||||
throwIfNotConnected(await this._click(progress, options));
|
||||
}, this._page._logger, this._page._timeoutSettings.timeout(options));
|
||||
}, this._page._timeoutSettings.timeout(options), 'click');
|
||||
}
|
||||
|
||||
_click(progress: Progress, options: ClickOptions & types.PointerActionWaitOptions & types.NavigatingActionWaitOptions): Promise<'notconnected' | 'done'> {
|
||||
@ -376,9 +380,9 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
|
||||
}
|
||||
|
||||
dblclick(options: MultiClickOptions & types.PointerActionWaitOptions & types.NavigatingActionWaitOptions = {}): Promise<void> {
|
||||
return runAbortableTask(async progress => {
|
||||
return this._runAbortableTask(async progress => {
|
||||
throwIfNotConnected(await this._dblclick(progress, options));
|
||||
}, this._page._logger, this._page._timeoutSettings.timeout(options));
|
||||
}, this._page._timeoutSettings.timeout(options), 'dblclick');
|
||||
}
|
||||
|
||||
_dblclick(progress: Progress, options: MultiClickOptions & types.PointerActionWaitOptions & types.NavigatingActionWaitOptions): Promise<'notconnected' | 'done'> {
|
||||
@ -386,9 +390,9 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
|
||||
}
|
||||
|
||||
async selectOption(values: string | ElementHandle | types.SelectOption | string[] | ElementHandle[] | types.SelectOption[] | null, options: types.NavigatingActionWaitOptions = {}): Promise<string[]> {
|
||||
return runAbortableTask(async progress => {
|
||||
return this._runAbortableTask(async progress => {
|
||||
return throwIfNotConnected(await this._selectOption(progress, values, options));
|
||||
}, this._page._logger, this._page._timeoutSettings.timeout(options));
|
||||
}, this._page._timeoutSettings.timeout(options), 'selectOption');
|
||||
}
|
||||
|
||||
async _selectOption(progress: Progress, values: string | ElementHandle | types.SelectOption | string[] | ElementHandle[] | types.SelectOption[] | null, options: types.NavigatingActionWaitOptions): Promise<string[] | 'notconnected'> {
|
||||
@ -419,16 +423,16 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
|
||||
}
|
||||
|
||||
async fill(value: string, options: types.NavigatingActionWaitOptions = {}): Promise<void> {
|
||||
return runAbortableTask(async progress => {
|
||||
return this._runAbortableTask(async progress => {
|
||||
throwIfNotConnected(await this._fill(progress, value, options));
|
||||
}, this._page._logger, this._page._timeoutSettings.timeout(options));
|
||||
}, this._page._timeoutSettings.timeout(options), 'fill');
|
||||
}
|
||||
|
||||
async _fill(progress: Progress, value: string, options: types.NavigatingActionWaitOptions): Promise<'notconnected' | 'done'> {
|
||||
progress.log(apiLog, `elementHandle.fill("${value}")`);
|
||||
progress.logger.info(`elementHandle.fill("${value}")`);
|
||||
assert(helper.isString(value), 'Value must be string. Found value "' + value + '" of type "' + (typeof value) + '"');
|
||||
return this._page._frameManager.waitForSignalsCreatedBy(progress, options.noWaitAfter, async () => {
|
||||
progress.log(apiLog, ' waiting for element to be visible, enabled and editable');
|
||||
progress.logger.info(' waiting for element to be visible, enabled and editable');
|
||||
const poll = await this._evaluateHandleInUtility(([injected, node, value]) => {
|
||||
return injected.waitForEnabledAndFill(node, value);
|
||||
}, value);
|
||||
@ -437,7 +441,7 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
|
||||
progress.throwIfAborted(); // Avoid action that has side-effects.
|
||||
if (filled === 'notconnected')
|
||||
return 'notconnected';
|
||||
progress.log(apiLog, ' element is visible, enabled and editable');
|
||||
progress.logger.info(' element is visible, enabled and editable');
|
||||
if (filled === 'needsinput') {
|
||||
if (value)
|
||||
await this._page.keyboard.insertText(value);
|
||||
@ -449,17 +453,17 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
|
||||
}
|
||||
|
||||
async selectText(): Promise<void> {
|
||||
return runAbortableTask(async progress => {
|
||||
return this._runAbortableTask(async progress => {
|
||||
progress.throwIfAborted(); // Avoid action that has side-effects.
|
||||
const selected = throwIfError(await this._evaluateInUtility(([injected, node]) => injected.selectText(node), {}));
|
||||
throwIfNotConnected(selected);
|
||||
}, this._page._logger, 0);
|
||||
}, 0, 'selectText');
|
||||
}
|
||||
|
||||
async setInputFiles(files: string | types.FilePayload | string[] | types.FilePayload[], options: types.NavigatingActionWaitOptions = {}) {
|
||||
return runAbortableTask(async progress => {
|
||||
return this._runAbortableTask(async progress => {
|
||||
throwIfNotConnected(await this._setInputFiles(progress, files, options));
|
||||
}, this._page._logger, this._page._timeoutSettings.timeout(options));
|
||||
}, this._page._timeoutSettings.timeout(options), 'setInputFiles');
|
||||
}
|
||||
|
||||
async _setInputFiles(progress: Progress, files: string | types.FilePayload | string[] | types.FilePayload[], options: types.NavigatingActionWaitOptions): Promise<'notconnected' | 'done'> {
|
||||
@ -500,9 +504,9 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
|
||||
}
|
||||
|
||||
async focus(): Promise<void> {
|
||||
return runAbortableTask(async progress => {
|
||||
return this._runAbortableTask(async progress => {
|
||||
throwIfNotConnected(await this._focus(progress));
|
||||
}, this._page._logger, 0);
|
||||
}, 0, 'focus');
|
||||
}
|
||||
|
||||
async _focus(progress: Progress): Promise<'notconnected' | 'done'> {
|
||||
@ -511,13 +515,13 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
|
||||
}
|
||||
|
||||
async type(text: string, options: { delay?: number } & types.NavigatingActionWaitOptions = {}): Promise<void> {
|
||||
return runAbortableTask(async progress => {
|
||||
return this._runAbortableTask(async progress => {
|
||||
throwIfNotConnected(await this._type(progress, text, options));
|
||||
}, this._page._logger, this._page._timeoutSettings.timeout(options));
|
||||
}, this._page._timeoutSettings.timeout(options), 'type');
|
||||
}
|
||||
|
||||
async _type(progress: Progress, text: string, options: { delay?: number } & types.NavigatingActionWaitOptions): Promise<'notconnected' | 'done'> {
|
||||
progress.log(apiLog, `elementHandle.type("${text}")`);
|
||||
progress.logger.info(`elementHandle.type("${text}")`);
|
||||
return this._page._frameManager.waitForSignalsCreatedBy(progress, options.noWaitAfter, async () => {
|
||||
const focused = await this._focus(progress);
|
||||
if (focused === 'notconnected')
|
||||
@ -529,13 +533,13 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
|
||||
}
|
||||
|
||||
async press(key: string, options: { delay?: number } & types.NavigatingActionWaitOptions = {}): Promise<void> {
|
||||
return runAbortableTask(async progress => {
|
||||
return this._runAbortableTask(async progress => {
|
||||
throwIfNotConnected(await this._press(progress, key, options));
|
||||
}, this._page._logger, this._page._timeoutSettings.timeout(options));
|
||||
}, this._page._timeoutSettings.timeout(options), 'press');
|
||||
}
|
||||
|
||||
async _press(progress: Progress, key: string, options: { delay?: number } & types.NavigatingActionWaitOptions): Promise<'notconnected' | 'done'> {
|
||||
progress.log(apiLog, `elementHandle.press("${key}")`);
|
||||
progress.logger.info(`elementHandle.press("${key}")`);
|
||||
return this._page._frameManager.waitForSignalsCreatedBy(progress, options.noWaitAfter, async () => {
|
||||
const focused = await this._focus(progress);
|
||||
if (focused === 'notconnected')
|
||||
@ -547,11 +551,11 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
|
||||
}
|
||||
|
||||
async check(options: types.PointerActionWaitOptions & types.NavigatingActionWaitOptions = {}) {
|
||||
return runAbortableTask(progress => this._setChecked(progress, true, options), this._page._logger, this._page._timeoutSettings.timeout(options));
|
||||
return this._runAbortableTask(progress => this._setChecked(progress, true, options), this._page._timeoutSettings.timeout(options), 'check');
|
||||
}
|
||||
|
||||
async uncheck(options: types.PointerActionWaitOptions & types.NavigatingActionWaitOptions = {}) {
|
||||
return runAbortableTask(progress => this._setChecked(progress, false, options), this._page._logger, this._page._timeoutSettings.timeout(options));
|
||||
return this._runAbortableTask(progress => this._setChecked(progress, false, options), this._page._timeoutSettings.timeout(options), 'uncheck');
|
||||
}
|
||||
|
||||
async _setChecked(progress: Progress, state: boolean, options: types.PointerActionWaitOptions & types.NavigatingActionWaitOptions) {
|
||||
@ -599,14 +603,14 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
|
||||
}
|
||||
|
||||
async _waitForDisplayedAtStablePositionAndEnabled(progress: Progress): Promise<'notconnected' | 'done'> {
|
||||
progress.log(apiLog, ' waiting for element to be visible, enabled and not moving');
|
||||
progress.logger.info(' waiting for element to be visible, enabled and not moving');
|
||||
const rafCount = this._page._delegate.rafCountForStablePosition();
|
||||
const poll = this._evaluateHandleInUtility(([injected, node, rafCount]) => {
|
||||
return injected.waitForDisplayedAtStablePositionAndEnabled(node, rafCount);
|
||||
}, rafCount);
|
||||
const pollHandler = new InjectedScriptPollHandler(progress, await poll);
|
||||
const result = throwIfError(await pollHandler.finish());
|
||||
progress.log(apiLog, ' element is visible, enabled and does not move');
|
||||
progress.logger.info(' element is visible, enabled and does not move');
|
||||
return result;
|
||||
}
|
||||
|
||||
@ -651,7 +655,7 @@ export class InjectedScriptPollHandler<T> {
|
||||
return;
|
||||
logs.evaluate(logs => logs.current).catch(e => [] as string[]).then(messages => {
|
||||
for (const message of messages)
|
||||
this._progress.log(apiLog, message);
|
||||
this._progress.logger.info(message);
|
||||
});
|
||||
this._streamLogs(logs.evaluateHandle(logs => logs.next));
|
||||
});
|
||||
@ -683,7 +687,7 @@ export class InjectedScriptPollHandler<T> {
|
||||
// Retrieve all the logs before continuing.
|
||||
const messages = await this._poll.evaluate(poll => poll.takeLastLogs()).catch(e => [] as string[]);
|
||||
for (const message of messages)
|
||||
this._progress.log(apiLog, message);
|
||||
this._progress.logger.info(message);
|
||||
}
|
||||
|
||||
async cancel() {
|
||||
|
@ -35,7 +35,7 @@ export class FFBrowser extends BrowserBase {
|
||||
private _eventListeners: RegisteredListener[];
|
||||
|
||||
static async connect(transport: ConnectionTransport, options: BrowserOptions): Promise<FFBrowser> {
|
||||
const connection = new FFConnection(SlowMoTransport.wrap(transport, options.slowMo), options.logger);
|
||||
const connection = new FFConnection(SlowMoTransport.wrap(transport, options.slowMo), options.loggers);
|
||||
const browser = new FFBrowser(connection, options);
|
||||
const promises: Promise<any>[] = [
|
||||
connection.send('Browser.enable', { attachToDefaultContext: !!options.persistent }),
|
||||
|
@ -17,9 +17,9 @@
|
||||
|
||||
import { EventEmitter } from 'events';
|
||||
import { assert } from '../helper';
|
||||
import { ConnectionTransport, ProtocolRequest, ProtocolResponse, protocolLog } from '../transport';
|
||||
import { ConnectionTransport, ProtocolRequest, ProtocolResponse } from '../transport';
|
||||
import { Protocol } from './protocol';
|
||||
import { InnerLogger, errorLog } from '../logger';
|
||||
import { Loggers, Logger } from '../logger';
|
||||
import { rewriteErrorMessage } from '../utils/stackTrace';
|
||||
|
||||
export const ConnectionEvents = {
|
||||
@ -34,7 +34,7 @@ export class FFConnection extends EventEmitter {
|
||||
private _lastId: number;
|
||||
private _callbacks: Map<number, {resolve: Function, reject: Function, error: Error, method: string}>;
|
||||
private _transport: ConnectionTransport;
|
||||
readonly _logger: InnerLogger;
|
||||
readonly _logger: Logger;
|
||||
readonly _sessions: Map<string, FFSession>;
|
||||
_closed: boolean;
|
||||
|
||||
@ -44,10 +44,10 @@ export class FFConnection extends EventEmitter {
|
||||
removeListener: <T extends keyof Protocol.Events | symbol>(event: T, listener: (payload: T extends symbol ? any : Protocol.Events[T extends keyof Protocol.Events ? T : never]) => void) => this;
|
||||
once: <T extends keyof Protocol.Events | symbol>(event: T, listener: (payload: T extends symbol ? any : Protocol.Events[T extends keyof Protocol.Events ? T : never]) => void) => this;
|
||||
|
||||
constructor(transport: ConnectionTransport, logger: InnerLogger) {
|
||||
constructor(transport: ConnectionTransport, loggers: Loggers) {
|
||||
super();
|
||||
this._transport = transport;
|
||||
this._logger = logger;
|
||||
this._logger = loggers.protocol;
|
||||
this._lastId = 0;
|
||||
this._callbacks = new Map();
|
||||
|
||||
@ -79,14 +79,14 @@ export class FFConnection extends EventEmitter {
|
||||
}
|
||||
|
||||
_rawSend(message: ProtocolRequest) {
|
||||
if (this._logger.isLogEnabled(protocolLog))
|
||||
this._logger.log(protocolLog, 'SEND ► ' + rewriteInjectedScriptEvaluationLog(message));
|
||||
if (this._logger.isEnabled())
|
||||
this._logger.info('SEND ► ' + rewriteInjectedScriptEvaluationLog(message));
|
||||
this._transport.send(message);
|
||||
}
|
||||
|
||||
async _onMessage(message: ProtocolResponse) {
|
||||
if (this._logger.isLogEnabled(protocolLog))
|
||||
this._logger.log(protocolLog, '◀ RECV ' + JSON.stringify(message));
|
||||
if (this._logger.isEnabled())
|
||||
this._logger.info('◀ RECV ' + JSON.stringify(message));
|
||||
if (message.id === kBrowserCloseMessageId)
|
||||
return;
|
||||
if (message.sessionId) {
|
||||
@ -187,7 +187,7 @@ export class FFSession extends EventEmitter {
|
||||
|
||||
sendMayFail<T extends keyof Protocol.CommandParameters>(method: T, params?: Protocol.CommandParameters[T]): Promise<Protocol.CommandReturnValues[T] | void> {
|
||||
return this.send(method, params).catch(error => {
|
||||
this._connection._logger.log(errorLog, error, []);
|
||||
this._connection._logger.error(error);
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -26,10 +26,8 @@ import * as network from './network';
|
||||
import { Page } from './page';
|
||||
import { selectors } from './selectors';
|
||||
import * as types from './types';
|
||||
import { waitForTimeoutWasUsed } from './hints';
|
||||
import { BrowserContext } from './browserContext';
|
||||
import { Progress, ProgressController, runAbortableTask } from './progress';
|
||||
import { apiLog } from './logger';
|
||||
import { Progress, ProgressController } from './progress';
|
||||
|
||||
type ContextType = 'main' | 'utility';
|
||||
type ContextData = {
|
||||
@ -340,11 +338,21 @@ export class Frame {
|
||||
this._parentFrame._childFrames.add(this);
|
||||
}
|
||||
|
||||
private _runAbortableTask<T>(task: (progress: Progress) => Promise<T>, timeout: number, apiName: string): Promise<T> {
|
||||
const controller = new ProgressController(this._page._logger, timeout, this._apiName(apiName));
|
||||
return controller.run(task);
|
||||
}
|
||||
|
||||
private _apiName(method: string) {
|
||||
const subject = this._page._callingPageAPI ? 'page' : 'frame';
|
||||
return `${subject}.${method}`;
|
||||
}
|
||||
|
||||
async goto(url: string, options: GotoOptions = {}): Promise<network.Response | null> {
|
||||
const progressController = new ProgressController(this._page._logger, this._page._timeoutSettings.navigationTimeout(options));
|
||||
const progressController = new ProgressController(this._page._logger, this._page._timeoutSettings.navigationTimeout(options), this._apiName('goto'));
|
||||
abortProgressOnFrameDetach(progressController, this);
|
||||
return progressController.run(async progress => {
|
||||
progress.log(apiLog, `navigating to "${url}", waiting until "${options.waitUntil || 'load'}"`);
|
||||
progress.logger.info(`navigating to "${url}", waiting until "${options.waitUntil || 'load'}"`);
|
||||
const headers = (this._page._state.extraHTTPHeaders || {});
|
||||
let referer = headers['referer'] || headers['Referer'];
|
||||
if (options.referer !== undefined) {
|
||||
@ -376,11 +384,11 @@ export class Frame {
|
||||
}
|
||||
|
||||
async waitForNavigation(options: types.WaitForNavigationOptions = {}): Promise<network.Response | null> {
|
||||
const progressController = new ProgressController(this._page._logger, this._page._timeoutSettings.navigationTimeout(options));
|
||||
const progressController = new ProgressController(this._page._logger, this._page._timeoutSettings.navigationTimeout(options), this._apiName('waitForNavigation'));
|
||||
abortProgressOnFrameDetach(progressController, this);
|
||||
return progressController.run(async progress => {
|
||||
const toUrl = typeof options.url === 'string' ? ` to "${options.url}"` : '';
|
||||
progress.log(apiLog, `waiting for navigation${toUrl} until "${options.waitUntil || 'load'}"`);
|
||||
progress.logger.info(`waiting for navigation${toUrl} until "${options.waitUntil || 'load'}"`);
|
||||
const frameTask = new FrameTask(this, progress);
|
||||
let documentId: string | undefined;
|
||||
await Promise.race([
|
||||
@ -395,7 +403,7 @@ export class Frame {
|
||||
}
|
||||
|
||||
async waitForLoadState(state: types.LifecycleEvent = 'load', options: types.TimeoutOptions = {}): Promise<void> {
|
||||
const progressController = new ProgressController(this._page._logger, this._page._timeoutSettings.navigationTimeout(options));
|
||||
const progressController = new ProgressController(this._page._logger, this._page._timeoutSettings.navigationTimeout(options), this._apiName('waitForLoadState'));
|
||||
abortProgressOnFrameDetach(progressController, this);
|
||||
return progressController.run(progress => this._waitForLoadState(progress, state));
|
||||
}
|
||||
@ -453,8 +461,8 @@ export class Frame {
|
||||
if (!['attached', 'detached', 'visible', 'hidden'].includes(state))
|
||||
throw new Error(`Unsupported state option "${state}"`);
|
||||
const { world, task } = selectors._waitForSelectorTask(selector, state);
|
||||
return runAbortableTask(async progress => {
|
||||
progress.log(apiLog, `waiting for selector "${selector}"${state === 'attached' ? '' : ' to be ' + state}`);
|
||||
return this._runAbortableTask(async progress => {
|
||||
progress.logger.info(`waiting for selector "${selector}"${state === 'attached' ? '' : ' to be ' + state}`);
|
||||
const result = await this._scheduleRerunnableTask(progress, world, task);
|
||||
if (!result.asElement()) {
|
||||
result.dispose();
|
||||
@ -468,16 +476,16 @@ export class Frame {
|
||||
return adopted;
|
||||
}
|
||||
return handle;
|
||||
}, this._page._logger, this._page._timeoutSettings.timeout(options));
|
||||
}, this._page._timeoutSettings.timeout(options), 'waitForSelector');
|
||||
}
|
||||
|
||||
async dispatchEvent(selector: string, type: string, eventInit?: Object, options: types.TimeoutOptions = {}): Promise<void> {
|
||||
const task = selectors._dispatchEventTask(selector, type, eventInit || {});
|
||||
return runAbortableTask(async progress => {
|
||||
progress.log(apiLog, `Dispatching "${type}" event on selector "${selector}"...`);
|
||||
return this._runAbortableTask(async progress => {
|
||||
progress.logger.info(`Dispatching "${type}" event on selector "${selector}"...`);
|
||||
const result = await this._scheduleRerunnableTask(progress, 'main', task);
|
||||
result.dispose();
|
||||
}, this._page._logger, this._page._timeoutSettings.timeout(options));
|
||||
}, this._page._timeoutSettings.timeout(options), 'dispatchEvent');
|
||||
}
|
||||
|
||||
async $eval<R, Arg>(selector: string, pageFunction: types.FuncOn<Element, Arg, R>, arg: Arg): Promise<R>;
|
||||
@ -519,11 +527,11 @@ export class Frame {
|
||||
}
|
||||
|
||||
async setContent(html: string, options: types.NavigateOptions = {}): Promise<void> {
|
||||
const progressController = new ProgressController(this._page._logger, this._page._timeoutSettings.navigationTimeout(options));
|
||||
const progressController = new ProgressController(this._page._logger, this._page._timeoutSettings.navigationTimeout(options), this._apiName('setContent'));
|
||||
abortProgressOnFrameDetach(progressController, this);
|
||||
return progressController.run(async progress => {
|
||||
const waitUntil = options.waitUntil === undefined ? 'load' : options.waitUntil;
|
||||
progress.log(apiLog, `setting frame content, waiting until "${waitUntil}"`);
|
||||
progress.logger.info(`setting frame content, waiting until "${waitUntil}"`);
|
||||
const tag = `--playwright--set--content--${this._id}--${++this._setContentCounter}--`;
|
||||
const context = await this._utilityContext();
|
||||
const lifecyclePromise = new Promise((resolve, reject) => {
|
||||
@ -706,10 +714,11 @@ export class Frame {
|
||||
|
||||
private async _retryWithSelectorIfNotConnected<R>(
|
||||
selector: string, options: types.TimeoutOptions,
|
||||
action: (progress: Progress, handle: dom.ElementHandle<Element>) => Promise<R | 'notconnected'>): Promise<R> {
|
||||
return runAbortableTask(async progress => {
|
||||
action: (progress: Progress, handle: dom.ElementHandle<Element>) => Promise<R | 'notconnected'>,
|
||||
apiName: string): Promise<R> {
|
||||
return this._runAbortableTask(async progress => {
|
||||
while (progress.isRunning()) {
|
||||
progress.log(apiLog, `waiting for selector "${selector}"`);
|
||||
progress.logger.info(`waiting for selector "${selector}"`);
|
||||
const { world, task } = selectors._waitForSelectorTask(selector, 'attached');
|
||||
const handle = await this._scheduleRerunnableTask(progress, world, task);
|
||||
const element = handle.asElement() as dom.ElementHandle<Element>;
|
||||
@ -717,77 +726,76 @@ export class Frame {
|
||||
const result = await action(progress, element);
|
||||
element.dispose();
|
||||
if (result === 'notconnected') {
|
||||
progress.log(apiLog, 'element was detached from the DOM, retrying');
|
||||
progress.logger.info('element was detached from the DOM, retrying');
|
||||
continue;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
return undefined as any;
|
||||
}, this._page._logger, this._page._timeoutSettings.timeout(options));
|
||||
}, this._page._timeoutSettings.timeout(options), apiName);
|
||||
}
|
||||
|
||||
async click(selector: string, options: dom.ClickOptions & types.PointerActionWaitOptions & types.NavigatingActionWaitOptions = {}) {
|
||||
await this._retryWithSelectorIfNotConnected(selector, options, (progress, handle) => handle._click(progress, options));
|
||||
await this._retryWithSelectorIfNotConnected(selector, options, (progress, handle) => handle._click(progress, options), 'click');
|
||||
}
|
||||
|
||||
async dblclick(selector: string, options: dom.MultiClickOptions & types.PointerActionWaitOptions & types.NavigatingActionWaitOptions = {}) {
|
||||
await this._retryWithSelectorIfNotConnected(selector, options, (progress, handle) => handle._dblclick(progress, options));
|
||||
await this._retryWithSelectorIfNotConnected(selector, options, (progress, handle) => handle._dblclick(progress, options), 'dblclick');
|
||||
}
|
||||
|
||||
async fill(selector: string, value: string, options: types.NavigatingActionWaitOptions = {}) {
|
||||
await this._retryWithSelectorIfNotConnected(selector, options, (progress, handle) => handle._fill(progress, value, options));
|
||||
await this._retryWithSelectorIfNotConnected(selector, options, (progress, handle) => handle._fill(progress, value, options), 'fill');
|
||||
}
|
||||
|
||||
async focus(selector: string, options: types.TimeoutOptions = {}) {
|
||||
await this._retryWithSelectorIfNotConnected(selector, options, (progress, handle) => handle._focus(progress));
|
||||
await this._retryWithSelectorIfNotConnected(selector, options, (progress, handle) => handle._focus(progress), 'focus');
|
||||
}
|
||||
|
||||
async textContent(selector: string, options: types.TimeoutOptions = {}): Promise<null|string> {
|
||||
return await this._retryWithSelectorIfNotConnected(selector, options, (progress, handle) => handle.textContent());
|
||||
return await this._retryWithSelectorIfNotConnected(selector, options, (progress, handle) => handle.textContent(), 'textContent');
|
||||
}
|
||||
|
||||
async innerText(selector: string, options: types.TimeoutOptions = {}): Promise<string> {
|
||||
return await this._retryWithSelectorIfNotConnected(selector, options, (progress, handle) => handle.innerText());
|
||||
return await this._retryWithSelectorIfNotConnected(selector, options, (progress, handle) => handle.innerText(), 'innerText');
|
||||
}
|
||||
|
||||
async innerHTML(selector: string, options: types.TimeoutOptions = {}): Promise<string> {
|
||||
return await this._retryWithSelectorIfNotConnected(selector, options, (progress, handle) => handle.innerHTML());
|
||||
return await this._retryWithSelectorIfNotConnected(selector, options, (progress, handle) => handle.innerHTML(), 'innerHTML');
|
||||
}
|
||||
|
||||
async getAttribute(selector: string, name: string, options: types.TimeoutOptions = {}): Promise<string | null> {
|
||||
return await this._retryWithSelectorIfNotConnected(selector, options, (progress, handle) => handle.getAttribute(name));
|
||||
return await this._retryWithSelectorIfNotConnected(selector, options, (progress, handle) => handle.getAttribute(name), 'getAttribute');
|
||||
}
|
||||
|
||||
async hover(selector: string, options: dom.PointerActionOptions & types.PointerActionWaitOptions = {}) {
|
||||
await this._retryWithSelectorIfNotConnected(selector, options, (progress, handle) => handle._hover(progress, options));
|
||||
await this._retryWithSelectorIfNotConnected(selector, options, (progress, handle) => handle._hover(progress, options), 'hover');
|
||||
}
|
||||
|
||||
async selectOption(selector: string, values: string | dom.ElementHandle | types.SelectOption | string[] | dom.ElementHandle[] | types.SelectOption[] | null, options: types.NavigatingActionWaitOptions = {}): Promise<string[]> {
|
||||
return this._retryWithSelectorIfNotConnected(selector, options, (progress, handle) => handle._selectOption(progress, values, options));
|
||||
return this._retryWithSelectorIfNotConnected(selector, options, (progress, handle) => handle._selectOption(progress, values, options), 'selectOption');
|
||||
}
|
||||
|
||||
async setInputFiles(selector: string, files: string | types.FilePayload | string[] | types.FilePayload[], options: types.NavigatingActionWaitOptions = {}): Promise<void> {
|
||||
await this._retryWithSelectorIfNotConnected(selector, options, (progress, handle) => handle._setInputFiles(progress, files, options));
|
||||
await this._retryWithSelectorIfNotConnected(selector, options, (progress, handle) => handle._setInputFiles(progress, files, options), 'setInputFiles');
|
||||
}
|
||||
|
||||
async type(selector: string, text: string, options: { delay?: number } & types.NavigatingActionWaitOptions = {}) {
|
||||
await this._retryWithSelectorIfNotConnected(selector, options, (progress, handle) => handle._type(progress, text, options));
|
||||
await this._retryWithSelectorIfNotConnected(selector, options, (progress, handle) => handle._type(progress, text, options), 'type');
|
||||
}
|
||||
|
||||
async press(selector: string, key: string, options: { delay?: number } & types.NavigatingActionWaitOptions = {}) {
|
||||
await this._retryWithSelectorIfNotConnected(selector, options, (progress, handle) => handle._press(progress, key, options));
|
||||
await this._retryWithSelectorIfNotConnected(selector, options, (progress, handle) => handle._press(progress, key, options), 'press');
|
||||
}
|
||||
|
||||
async check(selector: string, options: types.PointerActionWaitOptions & types.NavigatingActionWaitOptions = {}) {
|
||||
await this._retryWithSelectorIfNotConnected(selector, options, (progress, handle) => handle._setChecked(progress, true, options));
|
||||
await this._retryWithSelectorIfNotConnected(selector, options, (progress, handle) => handle._setChecked(progress, true, options), 'check');
|
||||
}
|
||||
|
||||
async uncheck(selector: string, options: types.PointerActionWaitOptions & types.NavigatingActionWaitOptions = {}) {
|
||||
await this._retryWithSelectorIfNotConnected(selector, options, (progress, handle) => handle._setChecked(progress, false, options));
|
||||
await this._retryWithSelectorIfNotConnected(selector, options, (progress, handle) => handle._setChecked(progress, false, options), 'uncheck');
|
||||
}
|
||||
|
||||
async waitForTimeout(timeout: number) {
|
||||
waitForTimeoutWasUsed(this._page);
|
||||
await new Promise(fulfill => setTimeout(fulfill, timeout));
|
||||
}
|
||||
|
||||
@ -809,9 +817,9 @@ export class Frame {
|
||||
return injectedScript.poll(polling, () => innerPredicate(arg));
|
||||
}, { injectedScript, predicateBody, polling, arg });
|
||||
};
|
||||
return runAbortableTask(
|
||||
return this._runAbortableTask(
|
||||
progress => this._scheduleRerunnableTask(progress, 'main', task),
|
||||
this._page._logger, this._page._timeoutSettings.timeout(options));
|
||||
this._page._timeoutSettings.timeout(options), 'waitForFunction');
|
||||
}
|
||||
|
||||
async title(): Promise<string> {
|
||||
@ -999,14 +1007,14 @@ class FrameTask {
|
||||
|
||||
onSameDocument() {
|
||||
if (this._progress)
|
||||
this._progress.log(apiLog, `navigated to "${this._frame._url}"`);
|
||||
this._progress.logger.info(`navigated to "${this._frame._url}"`);
|
||||
if (this._onSameDocument && helper.urlMatches(this._frame.url(), this._onSameDocument.url))
|
||||
this._onSameDocument.resolve();
|
||||
}
|
||||
|
||||
onNewDocument(documentId: string, error?: Error) {
|
||||
if (this._progress && !error)
|
||||
this._progress.log(apiLog, `navigated to "${this._frame._url}"`);
|
||||
this._progress.logger.info(`navigated to "${this._frame._url}"`);
|
||||
if (this._onSpecificDocument) {
|
||||
if (documentId === this._onSpecificDocument.expectedDocumentId) {
|
||||
if (error)
|
||||
@ -1027,7 +1035,7 @@ class FrameTask {
|
||||
|
||||
onLifecycle(frame: Frame, lifecycleEvent: types.LifecycleEvent) {
|
||||
if (this._progress && frame === this._frame && frame._url !== 'about:blank')
|
||||
this._progress.log(apiLog, `"${lifecycleEvent}" event fired`);
|
||||
this._progress.logger.info(`"${lifecycleEvent}" event fired`);
|
||||
if (this._onLifecycle && this._checkLifecycleRecursively(this._frame, this._onLifecycle.waitUntil))
|
||||
this._onLifecycle.resolve();
|
||||
}
|
||||
|
33
src/hints.ts
33
src/hints.ts
@ -1,33 +0,0 @@
|
||||
/**
|
||||
* 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 { Page } from './page';
|
||||
import { Log } from './logger';
|
||||
|
||||
const hintsLog: Log = {
|
||||
name: 'hint',
|
||||
severity: 'warning'
|
||||
};
|
||||
|
||||
let waitForTimeoutWasUsedReported = false;
|
||||
export function waitForTimeoutWasUsed(page: Page) {
|
||||
if (waitForTimeoutWasUsedReported)
|
||||
return;
|
||||
waitForTimeoutWasUsedReported = true;
|
||||
page._logger.log(hintsLog, `WARNING: page.waitForTimeout(timeout) should only be used for debugging.
|
||||
Tests using the timer in production are going to be flaky.
|
||||
Use signals such as network events, selectors becoming visible, etc. instead.`);
|
||||
}
|
142
src/logger.ts
142
src/logger.ts
@ -15,49 +15,116 @@
|
||||
*/
|
||||
|
||||
import * as debug from 'debug';
|
||||
import { helper } from './helper';
|
||||
import { Logger as LoggerSink, LoggerSeverity } from './types';
|
||||
|
||||
export type LoggerSeverity = 'verbose' | 'info' | 'warning' | 'error';
|
||||
|
||||
export type Log = {
|
||||
name: string;
|
||||
severity?: LoggerSeverity;
|
||||
color?: string | undefined;
|
||||
};
|
||||
|
||||
export interface Logger {
|
||||
isEnabled(name: string, severity: LoggerSeverity): boolean;
|
||||
log(name: string, severity: LoggerSeverity, message: string | Error, args: any[], hints: { color?: string }): void;
|
||||
export function logError(logger: Logger): (error: Error) => void {
|
||||
return error => logger.error(error);
|
||||
}
|
||||
|
||||
export const errorLog: Log = { name: 'generic', severity: 'error' };
|
||||
export const apiLog: Log = { name: 'api', color: 'cyan' };
|
||||
export class Logger {
|
||||
private _loggerSink: LoggerSink;
|
||||
private _name: string;
|
||||
private _hints: { color?: string; };
|
||||
private _scopeName: string | undefined;
|
||||
private _recording: string[] | undefined;
|
||||
|
||||
export function logError(logger: InnerLogger): (error: Error) => void {
|
||||
return error => logger.log(errorLog, error, []);
|
||||
constructor(loggerSink: LoggerSink, name: string, hints: { color?: string }, scopeName?: string, record?: boolean) {
|
||||
this._loggerSink = loggerSink;
|
||||
this._name = name;
|
||||
this._hints = hints;
|
||||
this._scopeName = scopeName;
|
||||
if (record)
|
||||
this._recording = [];
|
||||
}
|
||||
|
||||
isEnabled(severity?: LoggerSeverity): boolean {
|
||||
return this._loggerSink.isEnabled(this._name, severity || 'info');
|
||||
}
|
||||
|
||||
verbose(message: string, ...args: any[]) {
|
||||
return this._innerLog('verbose', message, args);
|
||||
}
|
||||
|
||||
info(message: string, ...args: any[]) {
|
||||
return this._innerLog('info', message, args);
|
||||
}
|
||||
|
||||
warn(message: string, ...args: any[]) {
|
||||
return this._innerLog('warning', message, args);
|
||||
}
|
||||
|
||||
error(message: string | Error, ...args: any[]) {
|
||||
return this._innerLog('error', message, args);
|
||||
}
|
||||
|
||||
createScope(scopeName: string, record?: boolean): Logger {
|
||||
this._loggerSink.log(this._name, 'info', `=> ${scopeName} started`, [], this._hints);
|
||||
return new Logger(this._loggerSink, this._name, this._hints, scopeName, record);
|
||||
}
|
||||
|
||||
endScope(status: string) {
|
||||
this._loggerSink.log(this._name, 'info', `<= ${this._scopeName} ${status}`, [], this._hints);
|
||||
}
|
||||
|
||||
private _innerLog(severity: LoggerSeverity, message: string | Error, ...args: any[]) {
|
||||
if (this._recording)
|
||||
this._recording.push(`[${this._name}] ${message}`);
|
||||
this._loggerSink.log(this._name, severity, message, args, this._hints);
|
||||
}
|
||||
|
||||
recording(): string[] {
|
||||
return this._recording ? this._recording.slice() : [];
|
||||
}
|
||||
}
|
||||
|
||||
export class InnerLogger {
|
||||
private _userSink: Logger | undefined;
|
||||
private _debugSink: DebugLogger;
|
||||
export class Loggers {
|
||||
readonly api: Logger;
|
||||
readonly browser: Logger;
|
||||
readonly protocol: Logger;
|
||||
|
||||
constructor(userSink: Logger | undefined) {
|
||||
this._userSink = userSink;
|
||||
this._debugSink = new DebugLogger();
|
||||
constructor(userSink: LoggerSink | undefined) {
|
||||
const loggerSink = new MultiplexingLoggerSink();
|
||||
if (userSink)
|
||||
loggerSink.add('user', userSink);
|
||||
if (helper.isDebugMode())
|
||||
loggerSink.add('pwdebug', new PwDebugLoggerSink());
|
||||
loggerSink.add('debug', new DebugLoggerSink());
|
||||
|
||||
this.api = new Logger(loggerSink, 'api', { color: 'cyan' });
|
||||
this.browser = new Logger(loggerSink, 'browser', {});
|
||||
this.protocol = new Logger(loggerSink, 'protocol', { color: 'green' });
|
||||
}
|
||||
}
|
||||
|
||||
class MultiplexingLoggerSink implements LoggerSink {
|
||||
private _loggers = new Map<string, LoggerSink>();
|
||||
|
||||
add(id: string, logger: LoggerSink) {
|
||||
this._loggers.set(id, logger);
|
||||
}
|
||||
|
||||
isLogEnabled(log: Log): boolean {
|
||||
const severity = log.severity || 'info';
|
||||
if (this._userSink && this._userSink.isEnabled(log.name, severity))
|
||||
return true;
|
||||
return this._debugSink.isEnabled(log.name, severity);
|
||||
get(id: string): LoggerSink | undefined {
|
||||
return this._loggers.get(id);
|
||||
}
|
||||
|
||||
log(log: Log, message: string | Error, ...args: any[]) {
|
||||
const severity = log.severity || 'info';
|
||||
const hints = log.color ? { color: log.color } : {};
|
||||
if (this._userSink && this._userSink.isEnabled(log.name, severity))
|
||||
this._userSink.log(log.name, severity, message, args, hints);
|
||||
this._debugSink.log(log.name, severity, message, args, hints);
|
||||
remove(id: string) {
|
||||
this._loggers.delete(id);
|
||||
}
|
||||
|
||||
isEnabled(name: string, severity: LoggerSeverity): boolean {
|
||||
for (const logger of this._loggers.values()) {
|
||||
if (logger.isEnabled(name, severity))
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
log(name: string, severity: LoggerSeverity, message: string | Error, args: any[], hints: { color?: string }) {
|
||||
for (const logger of this._loggers.values()) {
|
||||
if (logger.isEnabled(name, severity))
|
||||
logger.log(name, severity, message, args, hints);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -71,7 +138,7 @@ const colorMap = new Map<string, number>([
|
||||
['reset', 0],
|
||||
]);
|
||||
|
||||
class DebugLogger {
|
||||
class DebugLoggerSink {
|
||||
private _debuggers = new Map<string, debug.IDebugger>();
|
||||
|
||||
isEnabled(name: string, severity: LoggerSeverity): boolean {
|
||||
@ -96,3 +163,12 @@ class DebugLogger {
|
||||
cachedDebugger(message, ...args);
|
||||
}
|
||||
}
|
||||
|
||||
class PwDebugLoggerSink {
|
||||
isEnabled(name: string, severity: LoggerSeverity): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
log(name: string, severity: LoggerSeverity, message: string | Error, args: any[], hints: { color?: string }) {
|
||||
}
|
||||
}
|
||||
|
91
src/page.ts
91
src/page.ts
@ -30,8 +30,8 @@ import { ConsoleMessage, ConsoleMessageLocation } from './console';
|
||||
import * as accessibility from './accessibility';
|
||||
import { EventEmitter } from 'events';
|
||||
import { FileChooser } from './fileChooser';
|
||||
import { logError, InnerLogger } from './logger';
|
||||
import { ProgressController } from './progress';
|
||||
import { logError, Logger } from './logger';
|
||||
import { ProgressController, Progress } from './progress';
|
||||
|
||||
export interface PageDelegate {
|
||||
readonly rawMouse: input.RawMouse;
|
||||
@ -100,7 +100,7 @@ export class Page extends EventEmitter {
|
||||
readonly mouse: input.Mouse;
|
||||
readonly _timeoutSettings: TimeoutSettings;
|
||||
readonly _delegate: PageDelegate;
|
||||
readonly _logger: InnerLogger;
|
||||
readonly _logger: Logger;
|
||||
readonly _state: PageState;
|
||||
readonly _pageBindings = new Map<string, PageBinding>();
|
||||
readonly _evaluateOnNewDocumentSources: string[] = [];
|
||||
@ -112,11 +112,12 @@ export class Page extends EventEmitter {
|
||||
readonly coverage: any;
|
||||
_routes: { url: types.URLMatch, handler: network.RouteHandler }[] = [];
|
||||
_ownedContext: BrowserContext | undefined;
|
||||
_callingPageAPI = false;
|
||||
|
||||
constructor(delegate: PageDelegate, browserContext: BrowserContextBase) {
|
||||
super();
|
||||
this._delegate = delegate;
|
||||
this._logger = browserContext._logger;
|
||||
this._logger = browserContext._apiLogger;
|
||||
this._closedCallback = () => {};
|
||||
this._closedPromise = new Promise(f => this._closedCallback = f);
|
||||
this._disconnectedCallback = () => {};
|
||||
@ -139,6 +140,11 @@ export class Page extends EventEmitter {
|
||||
this.coverage = delegate.coverage ? delegate.coverage() : null;
|
||||
}
|
||||
|
||||
private _runAbortableTask<T>(task: (progress: Progress) => Promise<T>, timeout: number, apiName: string): Promise<T> {
|
||||
const controller = new ProgressController(this._logger, timeout, `page.${apiName}`);
|
||||
return controller.run(task);
|
||||
}
|
||||
|
||||
_didClose() {
|
||||
assert(!this._closed, 'Page closed twice');
|
||||
this._closed = true;
|
||||
@ -202,48 +208,48 @@ export class Page extends EventEmitter {
|
||||
}
|
||||
|
||||
async $(selector: string): Promise<dom.ElementHandle<Element> | null> {
|
||||
return this.mainFrame().$(selector);
|
||||
return this._attributeToPage(() => this.mainFrame().$(selector));
|
||||
}
|
||||
|
||||
async waitForSelector(selector: string, options?: types.WaitForElementOptions): Promise<dom.ElementHandle<Element> | null> {
|
||||
return this.mainFrame().waitForSelector(selector, options);
|
||||
return this._attributeToPage(() => this.mainFrame().waitForSelector(selector, options));
|
||||
}
|
||||
|
||||
async dispatchEvent(selector: string, type: string, eventInit?: Object, options?: types.TimeoutOptions): Promise<void> {
|
||||
return this.mainFrame().dispatchEvent(selector, type, eventInit, options);
|
||||
return this._attributeToPage(() => this.mainFrame().dispatchEvent(selector, type, eventInit, options));
|
||||
}
|
||||
|
||||
async evaluateHandle<R, Arg>(pageFunction: types.Func1<Arg, R>, arg: Arg): Promise<types.SmartHandle<R>>;
|
||||
async evaluateHandle<R>(pageFunction: types.Func1<void, R>, arg?: any): Promise<types.SmartHandle<R>>;
|
||||
async evaluateHandle<R, Arg>(pageFunction: types.Func1<Arg, R>, arg: Arg): Promise<types.SmartHandle<R>> {
|
||||
assertMaxArguments(arguments.length, 2);
|
||||
return this.mainFrame().evaluateHandle(pageFunction, arg);
|
||||
return this._attributeToPage(() => this.mainFrame().evaluateHandle(pageFunction, arg));
|
||||
}
|
||||
|
||||
async $eval<R, Arg>(selector: string, pageFunction: types.FuncOn<Element, Arg, R>, arg: Arg): Promise<R>;
|
||||
async $eval<R>(selector: string, pageFunction: types.FuncOn<Element, void, R>, arg?: any): Promise<R>;
|
||||
async $eval<R, Arg>(selector: string, pageFunction: types.FuncOn<Element, Arg, R>, arg: Arg): Promise<R> {
|
||||
assertMaxArguments(arguments.length, 3);
|
||||
return this.mainFrame().$eval(selector, pageFunction, arg);
|
||||
return this._attributeToPage(() => this.mainFrame().$eval(selector, pageFunction, arg));
|
||||
}
|
||||
|
||||
async $$eval<R, Arg>(selector: string, pageFunction: types.FuncOn<Element[], Arg, R>, arg: Arg): Promise<R>;
|
||||
async $$eval<R>(selector: string, pageFunction: types.FuncOn<Element[], void, R>, arg?: any): Promise<R>;
|
||||
async $$eval<R, Arg>(selector: string, pageFunction: types.FuncOn<Element[], Arg, R>, arg: Arg): Promise<R> {
|
||||
assertMaxArguments(arguments.length, 3);
|
||||
return this.mainFrame().$$eval(selector, pageFunction, arg);
|
||||
return this._attributeToPage(() => this.mainFrame().$$eval(selector, pageFunction, arg));
|
||||
}
|
||||
|
||||
async $$(selector: string): Promise<dom.ElementHandle<Element>[]> {
|
||||
return this.mainFrame().$$(selector);
|
||||
return this._attributeToPage(() => this.mainFrame().$$(selector));
|
||||
}
|
||||
|
||||
async addScriptTag(options: { url?: string; path?: string; content?: string; type?: string; }): Promise<dom.ElementHandle> {
|
||||
return this.mainFrame().addScriptTag(options);
|
||||
return this._attributeToPage(() => this.mainFrame().addScriptTag(options));
|
||||
}
|
||||
|
||||
async addStyleTag(options: { url?: string; path?: string; content?: string; }): Promise<dom.ElementHandle> {
|
||||
return this.mainFrame().addStyleTag(options);
|
||||
return this._attributeToPage(() => this.mainFrame().addStyleTag(options));
|
||||
}
|
||||
|
||||
async exposeFunction(name: string, playwrightFunction: Function) {
|
||||
@ -279,19 +285,19 @@ export class Page extends EventEmitter {
|
||||
}
|
||||
|
||||
url(): string {
|
||||
return this.mainFrame().url();
|
||||
return this._attributeToPage(() => this.mainFrame().url());
|
||||
}
|
||||
|
||||
async content(): Promise<string> {
|
||||
return this.mainFrame().content();
|
||||
return this._attributeToPage(() => this.mainFrame().content());
|
||||
}
|
||||
|
||||
async setContent(html: string, options?: types.NavigateOptions): Promise<void> {
|
||||
return this.mainFrame().setContent(html, options);
|
||||
return this._attributeToPage(() => this.mainFrame().setContent(html, options));
|
||||
}
|
||||
|
||||
async goto(url: string, options?: frames.GotoOptions): Promise<network.Response | null> {
|
||||
return this.mainFrame().goto(url, options);
|
||||
return this._attributeToPage(() => this.mainFrame().goto(url, options));
|
||||
}
|
||||
|
||||
async reload(options?: types.NavigateOptions): Promise<network.Response | null> {
|
||||
@ -301,11 +307,11 @@ export class Page extends EventEmitter {
|
||||
}
|
||||
|
||||
async waitForLoadState(state?: types.LifecycleEvent, options?: types.TimeoutOptions): Promise<void> {
|
||||
return this.mainFrame().waitForLoadState(state, options);
|
||||
return this._attributeToPage(() => this.mainFrame().waitForLoadState(state, options));
|
||||
}
|
||||
|
||||
async waitForNavigation(options?: types.WaitForNavigationOptions): Promise<network.Response | null> {
|
||||
return this.mainFrame().waitForNavigation(options);
|
||||
return this._attributeToPage(() => this.mainFrame().waitForNavigation(options));
|
||||
}
|
||||
|
||||
async waitForRequest(urlOrPredicate: string | RegExp | ((r: network.Request) => boolean), options: types.TimeoutOptions = {}): Promise<network.Request> {
|
||||
@ -328,7 +334,7 @@ export class Page extends EventEmitter {
|
||||
|
||||
async waitForEvent(event: string, optionsOrPredicate: types.WaitForEventOptions = {}): Promise<any> {
|
||||
const options = typeof optionsOrPredicate === 'function' ? { predicate: optionsOrPredicate } : optionsOrPredicate;
|
||||
const progressController = new ProgressController(this._logger, this._timeoutSettings.timeout(options));
|
||||
const progressController = new ProgressController(this._logger, this._timeoutSettings.timeout(options), 'page.waitForEvent');
|
||||
this._disconnectedPromise.then(error => progressController.abort(error));
|
||||
return progressController.run(progress => helper.waitForEvent(progress, this, event, options.predicate));
|
||||
}
|
||||
@ -376,7 +382,7 @@ export class Page extends EventEmitter {
|
||||
async evaluate<R>(pageFunction: types.Func1<void, R>, arg?: any): Promise<R>;
|
||||
async evaluate<R, Arg>(pageFunction: types.Func1<Arg, R>, arg: Arg): Promise<R> {
|
||||
assertMaxArguments(arguments.length, 2);
|
||||
return this.mainFrame().evaluate(pageFunction, arg);
|
||||
return this._attributeToPage(() => this.mainFrame().evaluate(pageFunction, arg));
|
||||
}
|
||||
|
||||
async addInitScript(script: Function | string | { path?: string, content?: string }, arg?: any) {
|
||||
@ -424,7 +430,7 @@ export class Page extends EventEmitter {
|
||||
}
|
||||
|
||||
async title(): Promise<string> {
|
||||
return this.mainFrame().title();
|
||||
return this._attributeToPage(() => this.mainFrame().title());
|
||||
}
|
||||
|
||||
async close(options: { runBeforeUnload: (boolean | undefined); } = {runBeforeUnload: undefined}) {
|
||||
@ -443,64 +449,73 @@ export class Page extends EventEmitter {
|
||||
return this._closed;
|
||||
}
|
||||
|
||||
private _attributeToPage<T>(func: () => T): T {
|
||||
try {
|
||||
this._callingPageAPI = true;
|
||||
return func();
|
||||
} finally {
|
||||
this._callingPageAPI = false;
|
||||
}
|
||||
}
|
||||
|
||||
async click(selector: string, options?: dom.ClickOptions & types.PointerActionWaitOptions & types.NavigatingActionWaitOptions) {
|
||||
return this.mainFrame().click(selector, options);
|
||||
return this._attributeToPage(() => this.mainFrame().click(selector, options));
|
||||
}
|
||||
|
||||
async dblclick(selector: string, options?: dom.MultiClickOptions & types.PointerActionWaitOptions & types.NavigatingActionWaitOptions) {
|
||||
return this.mainFrame().dblclick(selector, options);
|
||||
return this._attributeToPage(() => this.mainFrame().dblclick(selector, options));
|
||||
}
|
||||
|
||||
async fill(selector: string, value: string, options?: types.NavigatingActionWaitOptions) {
|
||||
return this.mainFrame().fill(selector, value, options);
|
||||
return this._attributeToPage(() => this.mainFrame().fill(selector, value, options));
|
||||
}
|
||||
|
||||
async focus(selector: string, options?: types.TimeoutOptions) {
|
||||
return this.mainFrame().focus(selector, options);
|
||||
return this._attributeToPage(() => this.mainFrame().focus(selector, options));
|
||||
}
|
||||
|
||||
async textContent(selector: string, options?: types.TimeoutOptions): Promise<null|string> {
|
||||
return this.mainFrame().textContent(selector, options);
|
||||
return this._attributeToPage(() => this.mainFrame().textContent(selector, options));
|
||||
}
|
||||
|
||||
async innerText(selector: string, options?: types.TimeoutOptions): Promise<string> {
|
||||
return this.mainFrame().innerText(selector, options);
|
||||
return this._attributeToPage(() => this.mainFrame().innerText(selector, options));
|
||||
}
|
||||
|
||||
async innerHTML(selector: string, options?: types.TimeoutOptions): Promise<string> {
|
||||
return this.mainFrame().innerHTML(selector, options);
|
||||
return this._attributeToPage(() => this.mainFrame().innerHTML(selector, options));
|
||||
}
|
||||
|
||||
async getAttribute(selector: string, name: string, options?: types.TimeoutOptions): Promise<string | null> {
|
||||
return this.mainFrame().getAttribute(selector, name, options);
|
||||
return this._attributeToPage(() => this.mainFrame().getAttribute(selector, name, options));
|
||||
}
|
||||
|
||||
async hover(selector: string, options?: dom.PointerActionOptions & types.PointerActionWaitOptions) {
|
||||
return this.mainFrame().hover(selector, options);
|
||||
return this._attributeToPage(() => this.mainFrame().hover(selector, options));
|
||||
}
|
||||
|
||||
async selectOption(selector: string, values: string | dom.ElementHandle | types.SelectOption | string[] | dom.ElementHandle[] | types.SelectOption[] | null, options?: types.NavigatingActionWaitOptions): Promise<string[]> {
|
||||
return this.mainFrame().selectOption(selector, values, options);
|
||||
return this._attributeToPage(() => this.mainFrame().selectOption(selector, values, options));
|
||||
}
|
||||
|
||||
async setInputFiles(selector: string, files: string | types.FilePayload | string[] | types.FilePayload[], options?: types.NavigatingActionWaitOptions): Promise<void> {
|
||||
return this.mainFrame().setInputFiles(selector, files, options);
|
||||
return this._attributeToPage(() => this.mainFrame().setInputFiles(selector, files, options));
|
||||
}
|
||||
|
||||
async type(selector: string, text: string, options?: { delay?: number } & types.NavigatingActionWaitOptions) {
|
||||
return this.mainFrame().type(selector, text, options);
|
||||
return this._attributeToPage(() => this.mainFrame().type(selector, text, options));
|
||||
}
|
||||
|
||||
async press(selector: string, key: string, options?: { delay?: number } & types.NavigatingActionWaitOptions) {
|
||||
return this.mainFrame().press(selector, key, options);
|
||||
return this._attributeToPage(() => this.mainFrame().press(selector, key, options));
|
||||
}
|
||||
|
||||
async check(selector: string, options?: types.PointerActionWaitOptions & types.NavigatingActionWaitOptions) {
|
||||
return this.mainFrame().check(selector, options);
|
||||
return this._attributeToPage(() => this.mainFrame().check(selector, options));
|
||||
}
|
||||
|
||||
async uncheck(selector: string, options?: types.PointerActionWaitOptions & types.NavigatingActionWaitOptions) {
|
||||
return this.mainFrame().uncheck(selector, options);
|
||||
return this._attributeToPage(() => this.mainFrame().uncheck(selector, options));
|
||||
}
|
||||
|
||||
async waitForTimeout(timeout: number) {
|
||||
@ -510,7 +525,7 @@ export class Page extends EventEmitter {
|
||||
async waitForFunction<R, Arg>(pageFunction: types.Func1<Arg, R>, arg: Arg, options?: types.WaitForFunctionOptions): Promise<types.SmartHandle<R>>;
|
||||
async waitForFunction<R>(pageFunction: types.Func1<void, R>, arg?: any, options?: types.WaitForFunctionOptions): Promise<types.SmartHandle<R>>;
|
||||
async waitForFunction<R, Arg>(pageFunction: types.Func1<Arg, R>, arg: Arg, options?: types.WaitForFunctionOptions): Promise<types.SmartHandle<R>> {
|
||||
return this.mainFrame().waitForFunction(pageFunction, arg, options);
|
||||
return this._attributeToPage(() => this.mainFrame().waitForFunction(pageFunction, arg, options));
|
||||
}
|
||||
|
||||
workers(): Worker[] {
|
||||
|
@ -14,18 +14,18 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { InnerLogger, Log, apiLog } from './logger';
|
||||
import { Logger } from './logger';
|
||||
import { TimeoutError } from './errors';
|
||||
import { assert } from './helper';
|
||||
import { getCurrentApiCall, rewriteErrorMessage } from './utils/stackTrace';
|
||||
import { rewriteErrorMessage } from './utils/stackTrace';
|
||||
|
||||
export interface Progress {
|
||||
readonly apiName: string;
|
||||
readonly aborted: Promise<void>;
|
||||
readonly logger: Logger;
|
||||
timeUntilDeadline(): number;
|
||||
isRunning(): boolean;
|
||||
cleanupWhenAborted(cleanup: () => any): void;
|
||||
log(log: Log, message: string | Error): void;
|
||||
throwIfAborted(): void;
|
||||
}
|
||||
|
||||
@ -35,7 +35,7 @@ export function isRunningTask(): boolean {
|
||||
return !!runningTaskCount;
|
||||
}
|
||||
|
||||
export async function runAbortableTask<T>(task: (progress: Progress) => Promise<T>, logger: InnerLogger, timeout: number, apiName?: string): Promise<T> {
|
||||
export async function runAbortableTask<T>(task: (progress: Progress) => Promise<T>, logger: Logger, timeout: number, apiName: string): Promise<T> {
|
||||
const controller = new ProgressController(logger, timeout, apiName);
|
||||
return controller.run(task);
|
||||
}
|
||||
@ -54,15 +54,14 @@ export class ProgressController {
|
||||
// Cleanups to be run only in the case of abort.
|
||||
private _cleanups: (() => any)[] = [];
|
||||
|
||||
private _logger: InnerLogger;
|
||||
private _logRecording: string[] = [];
|
||||
private _logger: Logger;
|
||||
private _state: 'before' | 'running' | 'aborted' | 'finished' = 'before';
|
||||
private _apiName: string;
|
||||
private _deadline: number;
|
||||
private _timeout: number;
|
||||
|
||||
constructor(logger: InnerLogger, timeout: number, apiName?: string) {
|
||||
this._apiName = apiName || getCurrentApiCall();
|
||||
constructor(logger: Logger, timeout: number, apiName: string) {
|
||||
this._apiName = apiName;
|
||||
this._logger = logger;
|
||||
|
||||
this._timeout = timeout;
|
||||
@ -78,9 +77,12 @@ export class ProgressController {
|
||||
this._state = 'running';
|
||||
++runningTaskCount;
|
||||
|
||||
const loggerScope = this._logger.createScope(this._apiName, true);
|
||||
|
||||
const progress: Progress = {
|
||||
apiName: this._apiName,
|
||||
aborted: this._abortedPromise,
|
||||
logger: loggerScope,
|
||||
timeUntilDeadline: () => this._deadline ? this._deadline - monotonicTime() : 2147483647, // 2^31-1 safe setTimeout in Node.
|
||||
isRunning: () => this._state === 'running',
|
||||
cleanupWhenAborted: (cleanup: () => any) => {
|
||||
@ -89,20 +91,11 @@ export class ProgressController {
|
||||
else
|
||||
runCleanup(cleanup);
|
||||
},
|
||||
log: (log: Log, message: string | Error) => {
|
||||
if (this._state === 'running') {
|
||||
this._logRecording.push(`[${log.name}] ${message.toString()}`);
|
||||
this._logger.log(log, ' ' + message);
|
||||
} else {
|
||||
this._logger.log(log, message);
|
||||
}
|
||||
},
|
||||
throwIfAborted: () => {
|
||||
if (this._state === 'aborted')
|
||||
throw new AbortedError();
|
||||
},
|
||||
};
|
||||
this._logger.log(apiLog, `=> ${this._apiName} started`);
|
||||
|
||||
const timeoutError = new TimeoutError(`Timeout ${this._timeout}ms exceeded during ${this._apiName}.`);
|
||||
const timer = setTimeout(() => this._forceAbort(timeoutError), progress.timeUntilDeadline());
|
||||
@ -111,18 +104,17 @@ export class ProgressController {
|
||||
const result = await Promise.race([promise, this._forceAbortPromise]);
|
||||
clearTimeout(timer);
|
||||
this._state = 'finished';
|
||||
this._logger.log(apiLog, `<= ${this._apiName} succeeded`);
|
||||
loggerScope.endScope('succeeded');
|
||||
return result;
|
||||
} catch (e) {
|
||||
this._aborted();
|
||||
rewriteErrorMessage(e, e.message + formatLogRecording(this._logRecording, this._apiName) + kLoggingNote);
|
||||
rewriteErrorMessage(e, e.message + formatLogRecording(loggerScope.recording(), this._apiName) + kLoggingNote);
|
||||
clearTimeout(timer);
|
||||
this._state = 'aborted';
|
||||
this._logger.log(apiLog, `<= ${this._apiName} failed`);
|
||||
loggerScope.endScope(`failed`);
|
||||
await Promise.all(this._cleanups.splice(0).map(cleanup => runCleanup(cleanup)));
|
||||
throw e;
|
||||
} finally {
|
||||
this._logRecording = [];
|
||||
--runningTaskCount;
|
||||
}
|
||||
}
|
||||
|
@ -21,7 +21,7 @@ import * as util from 'util';
|
||||
import { BrowserContext, PersistentContextOptions, verifyProxySettings, validateBrowserContextOptions } from '../browserContext';
|
||||
import { BrowserServer } from './browserServer';
|
||||
import * as browserPaths from '../install/browserPaths';
|
||||
import { Logger, InnerLogger } from '../logger';
|
||||
import { Loggers, Logger } from '../logger';
|
||||
import { ConnectionTransport, WebSocketTransport } from '../transport';
|
||||
import { BrowserBase, BrowserOptions, Browser } from '../browser';
|
||||
import { assert, helper } from '../helper';
|
||||
@ -29,7 +29,7 @@ import { launchProcess, Env, waitForLine } from './processLauncher';
|
||||
import { Events } from '../events';
|
||||
import { PipeTransport } from './pipeTransport';
|
||||
import { Progress, runAbortableTask } from '../progress';
|
||||
import { ProxySettings } from '../types';
|
||||
import * as types from '../types';
|
||||
import { TimeoutSettings } from '../timeoutSettings';
|
||||
import { WebSocketServer } from './webSocketServer';
|
||||
|
||||
@ -45,18 +45,18 @@ export type LaunchOptionsBase = {
|
||||
handleSIGTERM?: boolean,
|
||||
handleSIGHUP?: boolean,
|
||||
timeout?: number,
|
||||
logger?: Logger,
|
||||
logger?: types.Logger,
|
||||
env?: Env,
|
||||
headless?: boolean,
|
||||
devtools?: boolean,
|
||||
proxy?: ProxySettings,
|
||||
proxy?: types.ProxySettings,
|
||||
downloadsPath?: string,
|
||||
};
|
||||
|
||||
type ConnectOptions = {
|
||||
wsEndpoint: string,
|
||||
slowMo?: number,
|
||||
logger?: Logger,
|
||||
logger?: types.Logger,
|
||||
timeout?: number,
|
||||
};
|
||||
export type LaunchOptions = LaunchOptionsBase & { slowMo?: number };
|
||||
@ -105,8 +105,8 @@ export abstract class BrowserTypeBase implements BrowserType {
|
||||
assert(!(options as any).userDataDir, 'userDataDir option is not supported in `browserType.launch`. Use `browserType.launchPersistentContext` instead');
|
||||
assert(!(options as any).port, 'Cannot specify a port without launching as a server.');
|
||||
options = validateLaunchOptions(options);
|
||||
const logger = new InnerLogger(options.logger);
|
||||
const browser = await runAbortableTask(progress => this._innerLaunch(progress, options, logger, undefined), logger, TimeoutSettings.timeout(options));
|
||||
const loggers = new Loggers(options.logger);
|
||||
const browser = await runAbortableTask(progress => this._innerLaunch(progress, options, loggers, undefined), loggers.browser, TimeoutSettings.timeout(options), `browserType.launch`);
|
||||
return browser;
|
||||
}
|
||||
|
||||
@ -114,12 +114,12 @@ export abstract class BrowserTypeBase implements BrowserType {
|
||||
assert(!(options as any).port, 'Cannot specify a port without launching as a server.');
|
||||
options = validateLaunchOptions(options);
|
||||
const persistent = validateBrowserContextOptions(options);
|
||||
const logger = new InnerLogger(options.logger);
|
||||
const browser = await runAbortableTask(progress => this._innerLaunch(progress, options, logger, persistent, userDataDir), logger, TimeoutSettings.timeout(options));
|
||||
const loggers = new Loggers(options.logger);
|
||||
const browser = await runAbortableTask(progress => this._innerLaunch(progress, options, loggers, persistent, userDataDir), loggers.browser, TimeoutSettings.timeout(options), 'browserType.launchPersistentContext');
|
||||
return browser._defaultContext!;
|
||||
}
|
||||
|
||||
async _innerLaunch(progress: Progress, options: LaunchOptions, logger: InnerLogger, persistent: PersistentContextOptions | undefined, userDataDir?: string): Promise<BrowserBase> {
|
||||
async _innerLaunch(progress: Progress, options: LaunchOptions, logger: Loggers, persistent: PersistentContextOptions | undefined, userDataDir?: string): Promise<BrowserBase> {
|
||||
options.proxy = options.proxy ? verifyProxySettings(options.proxy) : undefined;
|
||||
const { browserServer, downloadsPath, transport } = await this._launchServer(progress, options, !!persistent, logger, userDataDir);
|
||||
if ((options as any).__testHookBeforeCreateBrowser)
|
||||
@ -128,7 +128,7 @@ export abstract class BrowserTypeBase implements BrowserType {
|
||||
slowMo: options.slowMo,
|
||||
persistent,
|
||||
headful: !options.headless,
|
||||
logger,
|
||||
loggers: logger,
|
||||
downloadsPath,
|
||||
ownedServer: browserServer,
|
||||
proxy: options.proxy,
|
||||
@ -145,28 +145,28 @@ export abstract class BrowserTypeBase implements BrowserType {
|
||||
async launchServer(options: LaunchServerOptions = {}): Promise<BrowserServer> {
|
||||
assert(!(options as any).userDataDir, 'userDataDir option is not supported in `browserType.launchServer`. Use `browserType.launchPersistentContext` instead');
|
||||
options = validateLaunchOptions(options);
|
||||
const logger = new InnerLogger(options.logger);
|
||||
const loggers = new Loggers(options.logger);
|
||||
const { port = 0 } = options;
|
||||
return runAbortableTask(async progress => {
|
||||
const { browserServer, transport } = await this._launchServer(progress, options, false, logger);
|
||||
browserServer._webSocketServer = this._startWebSocketServer(transport, logger, port);
|
||||
const { browserServer, transport } = await this._launchServer(progress, options, false, loggers);
|
||||
browserServer._webSocketServer = this._startWebSocketServer(transport, loggers.browser, port);
|
||||
return browserServer;
|
||||
}, logger, TimeoutSettings.timeout(options));
|
||||
}, loggers.browser, TimeoutSettings.timeout(options), 'browserType.launchServer');
|
||||
}
|
||||
|
||||
async connect(options: ConnectOptions): Promise<Browser> {
|
||||
const logger = new InnerLogger(options.logger);
|
||||
const loggers = new Loggers(options.logger);
|
||||
return runAbortableTask(async progress => {
|
||||
const transport = await WebSocketTransport.connect(progress, options.wsEndpoint);
|
||||
progress.cleanupWhenAborted(() => transport.closeAndWait());
|
||||
if ((options as any).__testHookBeforeCreateBrowser)
|
||||
await (options as any).__testHookBeforeCreateBrowser();
|
||||
const browser = await this._connectToTransport(transport, { slowMo: options.slowMo, logger });
|
||||
const browser = await this._connectToTransport(transport, { slowMo: options.slowMo, loggers });
|
||||
return browser;
|
||||
}, logger, TimeoutSettings.timeout(options));
|
||||
}, loggers.browser, TimeoutSettings.timeout(options), 'browserType.connect');
|
||||
}
|
||||
|
||||
private async _launchServer(progress: Progress, options: LaunchServerOptions, isPersistent: boolean, logger: InnerLogger, userDataDir?: string): Promise<{ browserServer: BrowserServer, downloadsPath: string, transport: ConnectionTransport }> {
|
||||
private async _launchServer(progress: Progress, options: LaunchServerOptions, isPersistent: boolean, loggers: Loggers, userDataDir?: string): Promise<{ browserServer: BrowserServer, downloadsPath: string, transport: ConnectionTransport }> {
|
||||
const {
|
||||
ignoreDefaultArgs = false,
|
||||
args = [],
|
||||
@ -240,14 +240,14 @@ export abstract class BrowserTypeBase implements BrowserType {
|
||||
transport = await WebSocketTransport.connect(progress, innerEndpoint);
|
||||
} else {
|
||||
const stdio = launchedProcess.stdio as unknown as [NodeJS.ReadableStream, NodeJS.WritableStream, NodeJS.WritableStream, NodeJS.WritableStream, NodeJS.ReadableStream];
|
||||
transport = new PipeTransport(stdio[3], stdio[4], logger);
|
||||
transport = new PipeTransport(stdio[3], stdio[4], loggers.browser);
|
||||
}
|
||||
return { browserServer, downloadsPath, transport };
|
||||
}
|
||||
|
||||
abstract _defaultArgs(options: LaunchOptionsBase, isPersistent: boolean, userDataDir: string): string[];
|
||||
abstract _connectToTransport(transport: ConnectionTransport, options: BrowserOptions): Promise<BrowserBase>;
|
||||
abstract _startWebSocketServer(transport: ConnectionTransport, logger: InnerLogger, port: number): WebSocketServer;
|
||||
abstract _startWebSocketServer(transport: ConnectionTransport, logger: Logger, port: number): WebSocketServer;
|
||||
abstract _amendEnvironment(env: Env, userDataDir: string, executable: string, browserArguments: string[]): Env;
|
||||
abstract _attemptToGracefullyCloseBrowser(transport: ConnectionTransport): void;
|
||||
}
|
||||
|
@ -23,7 +23,7 @@ import { Env } from './processLauncher';
|
||||
import { kBrowserCloseMessageId } from '../chromium/crConnection';
|
||||
import { LaunchOptionsBase, BrowserTypeBase } from './browserType';
|
||||
import { ConnectionTransport, ProtocolRequest, ProtocolResponse } from '../transport';
|
||||
import { InnerLogger } from '../logger';
|
||||
import { Logger } from '../logger';
|
||||
import { BrowserDescriptor } from '../install/browserPaths';
|
||||
import { CRDevTools } from '../chromium/crDevTools';
|
||||
import { BrowserOptions } from '../browser';
|
||||
@ -72,7 +72,7 @@ export class Chromium extends BrowserTypeBase {
|
||||
transport.send(message);
|
||||
}
|
||||
|
||||
_startWebSocketServer(transport: ConnectionTransport, logger: InnerLogger, port: number): WebSocketServer {
|
||||
_startWebSocketServer(transport: ConnectionTransport, logger: Logger, port: number): WebSocketServer {
|
||||
return startWebSocketServer(transport, logger, port);
|
||||
}
|
||||
|
||||
@ -130,7 +130,7 @@ type SessionData = {
|
||||
parent?: string,
|
||||
};
|
||||
|
||||
function startWebSocketServer(transport: ConnectionTransport, logger: InnerLogger, port: number): WebSocketServer {
|
||||
function startWebSocketServer(transport: ConnectionTransport, logger: Logger, port: number): WebSocketServer {
|
||||
const awaitingBrowserTarget = new Map<number, ws>();
|
||||
const sessionToData = new Map<string, SessionData>();
|
||||
const socketToBrowserSession = new Map<ws, { sessionId?: string, queue?: ProtocolRequest[] }>();
|
||||
|
@ -20,7 +20,7 @@ import { CRConnection, CRSession } from '../chromium/crConnection';
|
||||
import { CRExecutionContext } from '../chromium/crExecutionContext';
|
||||
import { Events } from '../events';
|
||||
import * as js from '../javascript';
|
||||
import { InnerLogger, Logger } from '../logger';
|
||||
import { Loggers, Logger } from '../logger';
|
||||
import { Page } from '../page';
|
||||
import { TimeoutSettings } from '../timeoutSettings';
|
||||
import { WebSocketTransport } from '../transport';
|
||||
@ -41,7 +41,7 @@ type ElectronLaunchOptions = {
|
||||
handleSIGTERM?: boolean,
|
||||
handleSIGHUP?: boolean,
|
||||
timeout?: number,
|
||||
logger?: Logger,
|
||||
logger?: types.Logger,
|
||||
};
|
||||
|
||||
export const ElectronEvents = {
|
||||
@ -57,7 +57,7 @@ interface ElectronPage extends Page {
|
||||
}
|
||||
|
||||
export class ElectronApplication extends EventEmitter {
|
||||
private _logger: InnerLogger;
|
||||
private _apiLogger: Logger;
|
||||
private _browserContext: CRBrowserContext;
|
||||
private _nodeConnection: CRConnection;
|
||||
private _nodeSession: CRSession;
|
||||
@ -67,9 +67,9 @@ export class ElectronApplication extends EventEmitter {
|
||||
private _lastWindowId = 0;
|
||||
readonly _timeoutSettings = new TimeoutSettings();
|
||||
|
||||
constructor(logger: InnerLogger, browser: CRBrowser, nodeConnection: CRConnection) {
|
||||
constructor(logger: Loggers, browser: CRBrowser, nodeConnection: CRConnection) {
|
||||
super();
|
||||
this._logger = logger;
|
||||
this._apiLogger = logger.api;
|
||||
this._browserContext = browser._defaultContext as CRBrowserContext;
|
||||
this._browserContext.on(Events.BrowserContext.Close, () => this.emit(ElectronEvents.ElectronApplication.Close));
|
||||
this._browserContext.on(Events.BrowserContext.Page, event => this._onPage(event));
|
||||
@ -131,7 +131,7 @@ export class ElectronApplication extends EventEmitter {
|
||||
|
||||
async waitForEvent(event: string, optionsOrPredicate: types.WaitForEventOptions = {}): Promise<any> {
|
||||
const options = typeof optionsOrPredicate === 'function' ? { predicate: optionsOrPredicate } : optionsOrPredicate;
|
||||
const progressController = new ProgressController(this._logger, this._timeoutSettings.timeout(options));
|
||||
const progressController = new ProgressController(this._apiLogger, this._timeoutSettings.timeout(options), 'electron.waitForEvent');
|
||||
if (event !== ElectronEvents.ElectronApplication.Close)
|
||||
this._browserContext._closePromise.then(error => progressController.abort(error));
|
||||
return progressController.run(progress => helper.waitForEvent(progress, this, event, options.predicate));
|
||||
@ -172,7 +172,7 @@ export class Electron {
|
||||
handleSIGTERM = true,
|
||||
handleSIGHUP = true,
|
||||
} = options;
|
||||
const logger = new InnerLogger(options.logger);
|
||||
const loggers = new Loggers(options.logger);
|
||||
return runAbortableTask(async progress => {
|
||||
let app: ElectronApplication | undefined = undefined;
|
||||
const electronArguments = ['--inspect=0', '--remote-debugging-port=0', '--require', path.join(__dirname, 'electronLoader.js'), ...args];
|
||||
@ -196,15 +196,15 @@ export class Electron {
|
||||
|
||||
const nodeMatch = await waitForLine(progress, launchedProcess, launchedProcess.stderr, /^Debugger listening on (ws:\/\/.*)$/);
|
||||
const nodeTransport = await WebSocketTransport.connect(progress, nodeMatch[1]);
|
||||
const nodeConnection = new CRConnection(nodeTransport, logger);
|
||||
const nodeConnection = new CRConnection(nodeTransport, loggers);
|
||||
|
||||
const chromeMatch = await waitForLine(progress, launchedProcess, launchedProcess.stderr, /^DevTools listening on (ws:\/\/.*)$/);
|
||||
const chromeTransport = await WebSocketTransport.connect(progress, chromeMatch[1]);
|
||||
const browserServer = new BrowserServer(launchedProcess, gracefullyClose, kill);
|
||||
const browser = await CRBrowser.connect(chromeTransport, { headful: true, logger, persistent: { viewport: null }, ownedServer: browserServer });
|
||||
app = new ElectronApplication(logger, browser, nodeConnection);
|
||||
const browser = await CRBrowser.connect(chromeTransport, { headful: true, loggers, persistent: { viewport: null }, ownedServer: browserServer });
|
||||
app = new ElectronApplication(loggers, browser, nodeConnection);
|
||||
await app._init();
|
||||
return app;
|
||||
}, logger, TimeoutSettings.timeout(options));
|
||||
}, loggers.browser, TimeoutSettings.timeout(options), 'electron.launch');
|
||||
}
|
||||
}
|
||||
|
@ -24,7 +24,7 @@ import { kBrowserCloseMessageId } from '../firefox/ffConnection';
|
||||
import { LaunchOptionsBase, BrowserTypeBase, FirefoxUserPrefsOptions } from './browserType';
|
||||
import { Env } from './processLauncher';
|
||||
import { ConnectionTransport, ProtocolResponse, ProtocolRequest } from '../transport';
|
||||
import { InnerLogger } from '../logger';
|
||||
import { Logger } from '../logger';
|
||||
import { BrowserOptions } from '../browser';
|
||||
import { BrowserDescriptor } from '../install/browserPaths';
|
||||
import { WebSocketServer } from './webSocketServer';
|
||||
@ -52,7 +52,7 @@ export class Firefox extends BrowserTypeBase {
|
||||
transport.send(message);
|
||||
}
|
||||
|
||||
_startWebSocketServer(transport: ConnectionTransport, logger: InnerLogger, port: number): WebSocketServer {
|
||||
_startWebSocketServer(transport: ConnectionTransport, logger: Logger, port: number): WebSocketServer {
|
||||
return startWebSocketServer(transport, logger, port);
|
||||
}
|
||||
|
||||
@ -110,7 +110,7 @@ type SessionData = {
|
||||
socket: ws,
|
||||
};
|
||||
|
||||
function startWebSocketServer(transport: ConnectionTransport, logger: InnerLogger, port: number): WebSocketServer {
|
||||
function startWebSocketServer(transport: ConnectionTransport, logger: Logger, port: number): WebSocketServer {
|
||||
const pendingBrowserContextCreations = new Set<number>();
|
||||
const pendingBrowserContextDeletions = new Map<number, string>();
|
||||
const browserContextIds = new Map<string, ws>();
|
||||
|
@ -17,7 +17,7 @@
|
||||
|
||||
import { helper, RegisteredListener } from '../helper';
|
||||
import { ConnectionTransport, ProtocolRequest, ProtocolResponse } from '../transport';
|
||||
import { logError, InnerLogger } from '../logger';
|
||||
import { logError, Logger } from '../logger';
|
||||
|
||||
export class PipeTransport implements ConnectionTransport {
|
||||
private _pipeWrite: NodeJS.WritableStream;
|
||||
@ -29,7 +29,7 @@ export class PipeTransport implements ConnectionTransport {
|
||||
onmessage?: (message: ProtocolResponse) => void;
|
||||
onclose?: () => void;
|
||||
|
||||
constructor(pipeWrite: NodeJS.WritableStream, pipeRead: NodeJS.ReadableStream, logger: InnerLogger) {
|
||||
constructor(pipeWrite: NodeJS.WritableStream, pipeRead: NodeJS.ReadableStream, logger: Logger) {
|
||||
this._pipeWrite = pipeWrite;
|
||||
this._eventListeners = [
|
||||
helper.addEventListener(pipeRead, 'data', buffer => this._dispatch(buffer)),
|
||||
|
@ -16,26 +16,12 @@
|
||||
*/
|
||||
|
||||
import * as childProcess from 'child_process';
|
||||
import { Log } from '../logger';
|
||||
import * as readline from 'readline';
|
||||
import * as removeFolder from 'rimraf';
|
||||
import * as stream from 'stream';
|
||||
import { helper } from '../helper';
|
||||
import { Progress } from '../progress';
|
||||
|
||||
export const browserLog: Log = {
|
||||
name: 'browser',
|
||||
};
|
||||
|
||||
const browserStdOutLog: Log = {
|
||||
name: 'browser:out',
|
||||
};
|
||||
|
||||
const browserStdErrLog: Log = {
|
||||
name: 'browser:err',
|
||||
severity: 'warning'
|
||||
};
|
||||
|
||||
export type Env = {[key: string]: string | number | boolean | undefined};
|
||||
|
||||
export type LaunchProcessOptions = {
|
||||
@ -68,7 +54,7 @@ export async function launchProcess(options: LaunchProcessOptions): Promise<Laun
|
||||
|
||||
const progress = options.progress;
|
||||
const stdio: ('ignore' | 'pipe')[] = options.pipe ? ['ignore', 'pipe', 'pipe', 'pipe', 'pipe'] : ['ignore', 'pipe', 'pipe'];
|
||||
progress.log(browserLog, `<launching> ${options.executablePath} ${options.args.join(' ')}`);
|
||||
progress.logger.info(`<launching> ${options.executablePath} ${options.args.join(' ')}`);
|
||||
const spawnedProcess = childProcess.spawn(
|
||||
options.executablePath,
|
||||
options.args,
|
||||
@ -90,16 +76,16 @@ export async function launchProcess(options: LaunchProcessOptions): Promise<Laun
|
||||
});
|
||||
return cleanup().then(() => failedPromise).then(e => Promise.reject(e));
|
||||
}
|
||||
progress.log(browserLog, `<launched> pid=${spawnedProcess.pid}`);
|
||||
progress.logger.info(`<launched> pid=${spawnedProcess.pid}`);
|
||||
|
||||
const stdout = readline.createInterface({ input: spawnedProcess.stdout });
|
||||
stdout.on('line', (data: string) => {
|
||||
progress.log(browserStdOutLog, data);
|
||||
progress.logger.info(data);
|
||||
});
|
||||
|
||||
const stderr = readline.createInterface({ input: spawnedProcess.stderr });
|
||||
stderr.on('line', (data: string) => {
|
||||
progress.log(browserStdErrLog, data);
|
||||
progress.logger.warn(data);
|
||||
});
|
||||
|
||||
let processClosed = false;
|
||||
@ -108,7 +94,7 @@ export async function launchProcess(options: LaunchProcessOptions): Promise<Laun
|
||||
let fulfillCleanup = () => {};
|
||||
const waitForCleanup = new Promise<void>(f => fulfillCleanup = f);
|
||||
spawnedProcess.once('exit', (exitCode, signal) => {
|
||||
progress.log(browserLog, `<process did exit: exitCode=${exitCode}, signal=${signal}>`);
|
||||
progress.logger.info(`<process did exit: exitCode=${exitCode}, signal=${signal}>`);
|
||||
processClosed = true;
|
||||
helper.removeEventListeners(listeners);
|
||||
options.onExit(exitCode, signal);
|
||||
@ -135,21 +121,21 @@ export async function launchProcess(options: LaunchProcessOptions): Promise<Laun
|
||||
// reentrancy to this function, for example user sends SIGINT second time.
|
||||
// In this case, let's forcefully kill the process.
|
||||
if (gracefullyClosing) {
|
||||
progress.log(browserLog, `<forecefully close>`);
|
||||
progress.logger.info(`<forecefully close>`);
|
||||
killProcess();
|
||||
await waitForClose; // Ensure the process is dead and we called options.onkill.
|
||||
return;
|
||||
}
|
||||
gracefullyClosing = true;
|
||||
progress.log(browserLog, `<gracefully close start>`);
|
||||
progress.logger.info(`<gracefully close start>`);
|
||||
await options.attemptToGracefullyClose().catch(() => killProcess());
|
||||
await waitForCleanup; // Ensure the process is dead and we have cleaned up.
|
||||
progress.log(browserLog, `<gracefully close end>`);
|
||||
progress.logger.info(`<gracefully close end>`);
|
||||
}
|
||||
|
||||
// This method has to be sync to be used as 'exit' event handler.
|
||||
function killProcess() {
|
||||
progress.log(browserLog, `<kill>`);
|
||||
progress.logger.info(`<kill>`);
|
||||
helper.removeEventListeners(listeners);
|
||||
if (spawnedProcess.pid && !spawnedProcess.killed && !processClosed) {
|
||||
// Force kill the browser.
|
||||
|
@ -17,7 +17,7 @@
|
||||
import { IncomingMessage } from 'http';
|
||||
import * as ws from 'ws';
|
||||
import { helper } from '../helper';
|
||||
import { InnerLogger, logError } from '../logger';
|
||||
import { logError, Logger } from '../logger';
|
||||
import { ConnectionTransport, ProtocolRequest, ProtocolResponse } from '../transport';
|
||||
|
||||
export interface WebSocketServerDelegate {
|
||||
@ -30,7 +30,7 @@ export interface WebSocketServerDelegate {
|
||||
|
||||
export class WebSocketServer {
|
||||
private _transport: ConnectionTransport;
|
||||
private _logger: InnerLogger;
|
||||
private _logger: Logger;
|
||||
private _server: ws.Server;
|
||||
private _guid: string;
|
||||
readonly wsEndpoint: string;
|
||||
@ -40,7 +40,7 @@ export class WebSocketServer {
|
||||
private _sockets = new Set<ws>();
|
||||
private _pendingRequests = new Map<number, { message: ProtocolRequest, source: ws | null }>();
|
||||
|
||||
constructor(transport: ConnectionTransport, logger: InnerLogger, port: number, delegate: WebSocketServerDelegate) {
|
||||
constructor(transport: ConnectionTransport, logger: Logger, port: number, delegate: WebSocketServerDelegate) {
|
||||
this._guid = helper.guid();
|
||||
this._transport = transport;
|
||||
this._logger = logger;
|
||||
|
@ -22,7 +22,7 @@ import { kBrowserCloseMessageId } from '../webkit/wkConnection';
|
||||
import { LaunchOptionsBase, BrowserTypeBase } from './browserType';
|
||||
import { ConnectionTransport, ProtocolResponse, ProtocolRequest } from '../transport';
|
||||
import * as ws from 'ws';
|
||||
import { InnerLogger } from '../logger';
|
||||
import { Logger } from '../logger';
|
||||
import { BrowserOptions } from '../browser';
|
||||
import { BrowserDescriptor } from '../install/browserPaths';
|
||||
import { WebSocketServer } from './webSocketServer';
|
||||
@ -45,7 +45,7 @@ export class WebKit extends BrowserTypeBase {
|
||||
transport.send({method: 'Playwright.close', params: {}, id: kBrowserCloseMessageId});
|
||||
}
|
||||
|
||||
_startWebSocketServer(transport: ConnectionTransport, logger: InnerLogger, port: number): WebSocketServer {
|
||||
_startWebSocketServer(transport: ConnectionTransport, logger: Logger, port: number): WebSocketServer {
|
||||
return startWebSocketServer(transport, logger, port);
|
||||
}
|
||||
|
||||
@ -87,7 +87,7 @@ export class WebKit extends BrowserTypeBase {
|
||||
}
|
||||
}
|
||||
|
||||
function startWebSocketServer(transport: ConnectionTransport, logger: InnerLogger, port: number): WebSocketServer {
|
||||
function startWebSocketServer(transport: ConnectionTransport, logger: Logger, port: number): WebSocketServer {
|
||||
const pendingBrowserContextCreations = new Set<number>();
|
||||
const pendingBrowserContextDeletions = new Map<number, string>();
|
||||
const browserContextIds = new Map<string, ws>();
|
||||
|
@ -17,9 +17,7 @@
|
||||
|
||||
import * as WebSocket from 'ws';
|
||||
import { helper } from './helper';
|
||||
import { Log } from './logger';
|
||||
import { Progress } from './progress';
|
||||
import { browserLog } from './server/processLauncher';
|
||||
|
||||
export type ProtocolRequest = {
|
||||
id: number;
|
||||
@ -128,7 +126,7 @@ export class WebSocketTransport implements ConnectionTransport {
|
||||
onclose?: () => void;
|
||||
|
||||
static async connect(progress: Progress, url: string): Promise<WebSocketTransport> {
|
||||
progress.log(browserLog, `<ws connecting> ${url}`);
|
||||
progress.logger.info(`<ws connecting> ${url}`);
|
||||
const transport = new WebSocketTransport(progress, url);
|
||||
let success = false;
|
||||
progress.aborted.then(() => {
|
||||
@ -137,11 +135,11 @@ export class WebSocketTransport implements ConnectionTransport {
|
||||
});
|
||||
await new Promise<WebSocketTransport>((fulfill, reject) => {
|
||||
transport._ws.addEventListener('open', async () => {
|
||||
progress.log(browserLog, `<ws connected> ${url}`);
|
||||
progress.logger.info(`<ws connected> ${url}`);
|
||||
fulfill(transport);
|
||||
});
|
||||
transport._ws.addEventListener('error', event => {
|
||||
progress.log(browserLog, `<ws connect error> ${url} ${event.message}`);
|
||||
progress.logger.info(`<ws connect error> ${url} ${event.message}`);
|
||||
reject(new Error('WebSocket error: ' + event.message));
|
||||
transport._ws.close();
|
||||
});
|
||||
@ -171,7 +169,7 @@ export class WebSocketTransport implements ConnectionTransport {
|
||||
});
|
||||
|
||||
this._ws.addEventListener('close', event => {
|
||||
this._progress && this._progress.log(browserLog, `<ws disconnected> ${url}`);
|
||||
this._progress && this._progress.logger.info(`<ws disconnected> ${url}`);
|
||||
if (this.onclose)
|
||||
this.onclose.call(null);
|
||||
});
|
||||
@ -184,7 +182,7 @@ export class WebSocketTransport implements ConnectionTransport {
|
||||
}
|
||||
|
||||
close() {
|
||||
this._progress && this._progress.log(browserLog, `<ws disconnecting> ${this._ws.url}`);
|
||||
this._progress && this._progress.logger.info(`<ws disconnecting> ${this._ws.url}`);
|
||||
this._ws.close();
|
||||
}
|
||||
|
||||
@ -229,9 +227,3 @@ export class InterceptingTransport implements ConnectionTransport {
|
||||
this._delegate.close();
|
||||
}
|
||||
}
|
||||
|
||||
export const protocolLog: Log = {
|
||||
name: 'protocol',
|
||||
severity: 'verbose',
|
||||
color: 'green'
|
||||
};
|
||||
|
@ -177,4 +177,11 @@ export type ProxySettings = {
|
||||
password?: string
|
||||
};
|
||||
|
||||
export type LoggerSeverity = 'verbose' | 'info' | 'warning' | 'error';
|
||||
|
||||
export interface Logger {
|
||||
isEnabled(name: string, severity: LoggerSeverity): boolean;
|
||||
log(name: string, severity: LoggerSeverity, message: string | Error, args: any[], hints: { color?: string }): void;
|
||||
}
|
||||
|
||||
export type WaitForEventOptions = Function | { predicate?: Function, timeout?: number };
|
||||
|
@ -18,7 +18,6 @@ import * as path from 'path';
|
||||
|
||||
// NOTE: update this to point to playwright/lib when moving this file.
|
||||
const PLAYWRIGHT_LIB_PATH = path.normalize(path.join(__dirname, '..'));
|
||||
const APICOVERAGE = path.normalize(path.join(__dirname, '..', '..', 'test', 'apicoverage'));
|
||||
|
||||
type ParsedStackFrame = { filePath: string, functionName: string };
|
||||
|
||||
@ -61,26 +60,6 @@ export function getCallerFilePath(ignorePrefix = PLAYWRIGHT_LIB_PATH): string |
|
||||
return null;
|
||||
}
|
||||
|
||||
export function getCurrentApiCall(prefix = PLAYWRIGHT_LIB_PATH): string {
|
||||
const error = new Error();
|
||||
const stackFrames = (error.stack || '').split('\n').slice(1);
|
||||
// Find last stackframe that points to prefix - that should be the api call.
|
||||
let apiName: string = '';
|
||||
for (const frame of stackFrames) {
|
||||
const parsed = parseStackFrame(frame);
|
||||
if (!parsed || (!parsed.filePath.startsWith(prefix) && !parsed.filePath.startsWith(APICOVERAGE) && parsed.filePath !== __filename))
|
||||
break;
|
||||
apiName = parsed.functionName;
|
||||
}
|
||||
const parts = apiName.split('.');
|
||||
if (parts.length && parts[0].length) {
|
||||
parts[0] = parts[0][0].toLowerCase() + parts[0].substring(1);
|
||||
if (parts[0] === 'webKit')
|
||||
parts[0] = 'webkit';
|
||||
}
|
||||
return parts.join('.');
|
||||
}
|
||||
|
||||
export function rewriteErrorMessage(e: Error, newMessage: string): Error {
|
||||
if (e.stack) {
|
||||
const index = e.stack.indexOf(e.message);
|
||||
|
@ -51,7 +51,7 @@ export class WKBrowser extends BrowserBase {
|
||||
|
||||
constructor(transport: ConnectionTransport, options: BrowserOptions) {
|
||||
super(options);
|
||||
this._connection = new WKConnection(transport, options.logger, this._onDisconnect.bind(this));
|
||||
this._connection = new WKConnection(transport, options.loggers, this._onDisconnect.bind(this));
|
||||
this._browserSession = this._connection.browserSession;
|
||||
this._eventListeners = [
|
||||
helper.addEventListener(this._browserSession, 'Playwright.pageProxyCreated', this._onPageProxyCreated.bind(this)),
|
||||
|
@ -17,9 +17,9 @@
|
||||
|
||||
import { EventEmitter } from 'events';
|
||||
import { assert } from '../helper';
|
||||
import { ConnectionTransport, ProtocolRequest, ProtocolResponse, protocolLog } from '../transport';
|
||||
import { ConnectionTransport, ProtocolRequest, ProtocolResponse } from '../transport';
|
||||
import { Protocol } from './protocol';
|
||||
import { InnerLogger, errorLog } from '../logger';
|
||||
import { Loggers, Logger } from '../logger';
|
||||
import { rewriteErrorMessage } from '../utils/stackTrace';
|
||||
|
||||
// WKPlaywright uses this special id to issue Browser.close command which we
|
||||
@ -37,11 +37,11 @@ export class WKConnection {
|
||||
private _lastId = 0;
|
||||
private _closed = false;
|
||||
readonly browserSession: WKSession;
|
||||
readonly _logger: InnerLogger;
|
||||
readonly _logger: Logger;
|
||||
|
||||
constructor(transport: ConnectionTransport, logger: InnerLogger, onDisconnect: () => void) {
|
||||
constructor(transport: ConnectionTransport, loggers: Loggers, onDisconnect: () => void) {
|
||||
this._transport = transport;
|
||||
this._logger = logger;
|
||||
this._logger = loggers.protocol;
|
||||
this._transport.onmessage = this._dispatchMessage.bind(this);
|
||||
this._transport.onclose = this._onClose.bind(this);
|
||||
this._onDisconnect = onDisconnect;
|
||||
@ -55,14 +55,14 @@ export class WKConnection {
|
||||
}
|
||||
|
||||
rawSend(message: ProtocolRequest) {
|
||||
if (this._logger.isLogEnabled(protocolLog))
|
||||
this._logger.log(protocolLog, 'SEND ► ' + rewriteInjectedScriptEvaluationLog(message));
|
||||
if (this._logger.isEnabled())
|
||||
this._logger.info('SEND ► ' + rewriteInjectedScriptEvaluationLog(message));
|
||||
this._transport.send(message);
|
||||
}
|
||||
|
||||
private _dispatchMessage(message: ProtocolResponse) {
|
||||
if (this._logger.isLogEnabled(protocolLog))
|
||||
this._logger.log(protocolLog, '◀ RECV ' + JSON.stringify(message));
|
||||
if (this._logger.isEnabled())
|
||||
this._logger.info('◀ RECV ' + JSON.stringify(message));
|
||||
if (message.id === kBrowserCloseMessageId)
|
||||
return;
|
||||
if (message.pageProxyId) {
|
||||
@ -139,7 +139,7 @@ export class WKSession extends EventEmitter {
|
||||
|
||||
sendMayFail<T extends keyof Protocol.CommandParameters>(method: T, params?: Protocol.CommandParameters[T]): Promise<Protocol.CommandReturnValues[T] | void> {
|
||||
return this.send(method, params).catch(error => {
|
||||
this.connection._logger.log(errorLog, error, []);
|
||||
this.connection._logger.error(error);
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -363,7 +363,7 @@ describe('launchPersistentContext()', function() {
|
||||
const userDataDir = await makeUserDataDir();
|
||||
const options = { ...defaultBrowserOptions, timeout: 5000, __testHookBeforeCreateBrowser: () => new Promise(f => setTimeout(f, 6000)) };
|
||||
const error = await browserType.launchPersistentContext(userDataDir, options).catch(e => e);
|
||||
expect(error.message).toContain(`Timeout 5000ms exceeded during ${browserType.name()}.launchPersistentContext.`);
|
||||
expect(error.message).toContain(`Timeout 5000ms exceeded during browserType.launchPersistentContext.`);
|
||||
await removeUserDataDir(userDataDir);
|
||||
});
|
||||
it('should handle exception', async({browserType, defaultBrowserOptions}) => {
|
||||
|
@ -187,10 +187,4 @@ describe('StackTrace', () => {
|
||||
});
|
||||
expect(filePath).toBe(__filename);
|
||||
});
|
||||
it('api call', async state => {
|
||||
const stackTrace = require(path.join(state.playwrightPath, 'lib', 'utils', 'stackTrace'));
|
||||
const callme = require('./fixtures/callback');
|
||||
const apiCall = callme(stackTrace.getCurrentApiCall.bind(stackTrace, path.join(__dirname, 'fixtures') + path.sep));
|
||||
expect(apiCall).toBe('callme');
|
||||
});
|
||||
});
|
||||
|
@ -53,7 +53,7 @@ describe('Playwright', function() {
|
||||
it('should handle timeout', async({browserType, defaultBrowserOptions}) => {
|
||||
const options = { ...defaultBrowserOptions, timeout: 5000, __testHookBeforeCreateBrowser: () => new Promise(f => setTimeout(f, 6000)) };
|
||||
const error = await browserType.launch(options).catch(e => e);
|
||||
expect(error.message).toContain(`Timeout 5000ms exceeded during ${browserType.name()}.launch.`);
|
||||
expect(error.message).toContain(`Timeout 5000ms exceeded during browserType.launch.`);
|
||||
expect(error.message).toContain(`[browser] <launching>`);
|
||||
expect(error.message).toContain(`[browser] <launched> pid=`);
|
||||
});
|
||||
|
Loading…
Reference in New Issue
Block a user