chore: move debug-related code to src/debug (#2309)
This commit is contained in:
parent
4e86d39881
commit
8f0f32b5e6
|
|
@ -30,7 +30,7 @@ import { Events } from './events';
|
||||||
import { Protocol } from './protocol';
|
import { Protocol } from './protocol';
|
||||||
import { CRExecutionContext } from './crExecutionContext';
|
import { CRExecutionContext } from './crExecutionContext';
|
||||||
import { logError } from '../logger';
|
import { logError } from '../logger';
|
||||||
import { CRDevTools } from './crDevTools';
|
import { CRDevTools } from '../debug/crDevTools';
|
||||||
|
|
||||||
export class CRBrowser extends BrowserBase {
|
export class CRBrowser extends BrowserBase {
|
||||||
readonly _connection: CRConnection;
|
readonly _connection: CRConnection;
|
||||||
|
|
|
||||||
|
|
@ -18,8 +18,8 @@
|
||||||
import { CRSession } from './crConnection';
|
import { CRSession } from './crConnection';
|
||||||
import { assert, helper, RegisteredListener } from '../helper';
|
import { assert, helper, RegisteredListener } from '../helper';
|
||||||
import { Protocol } from './protocol';
|
import { Protocol } from './protocol';
|
||||||
import * as js from '../javascript';
|
|
||||||
import * as types from '../types';
|
import * as types from '../types';
|
||||||
|
import * as debugSupport from '../debug/debugSupport';
|
||||||
import { logError, InnerLogger } from '../logger';
|
import { logError, InnerLogger } from '../logger';
|
||||||
|
|
||||||
type JSRange = {
|
type JSRange = {
|
||||||
|
|
@ -125,7 +125,7 @@ class JSCoverage {
|
||||||
|
|
||||||
async _onScriptParsed(event: Protocol.Debugger.scriptParsedPayload) {
|
async _onScriptParsed(event: Protocol.Debugger.scriptParsedPayload) {
|
||||||
// Ignore playwright-injected scripts
|
// Ignore playwright-injected scripts
|
||||||
if (js.isPlaywrightSourceUrl(event.url))
|
if (debugSupport.isPlaywrightSourceUrl(event.url))
|
||||||
return;
|
return;
|
||||||
this._scriptIds.add(event.scriptId);
|
this._scriptIds.add(event.scriptId);
|
||||||
// Ignore other anonymous scripts unless the reportAnonymousScripts option is true.
|
// Ignore other anonymous scripts unless the reportAnonymousScripts option is true.
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,7 @@ import { helper } from '../helper';
|
||||||
import { valueFromRemoteObject, getExceptionMessage, releaseObject } from './crProtocolHelper';
|
import { valueFromRemoteObject, getExceptionMessage, releaseObject } from './crProtocolHelper';
|
||||||
import { Protocol } from './protocol';
|
import { Protocol } from './protocol';
|
||||||
import * as js from '../javascript';
|
import * as js from '../javascript';
|
||||||
|
import * as debugSupport from '../debug/debugSupport';
|
||||||
|
|
||||||
export class CRExecutionContext implements js.ExecutionContextDelegate {
|
export class CRExecutionContext implements js.ExecutionContextDelegate {
|
||||||
_client: CRSession;
|
_client: CRSession;
|
||||||
|
|
@ -32,7 +33,7 @@ export class CRExecutionContext implements js.ExecutionContextDelegate {
|
||||||
|
|
||||||
async rawEvaluate(expression: string): Promise<js.RemoteObject> {
|
async rawEvaluate(expression: string): Promise<js.RemoteObject> {
|
||||||
const { exceptionDetails, result: remoteObject } = await this._client.send('Runtime.evaluate', {
|
const { exceptionDetails, result: remoteObject } = await this._client.send('Runtime.evaluate', {
|
||||||
expression: js.ensureSourceUrl(expression),
|
expression: debugSupport.ensureSourceUrl(expression),
|
||||||
contextId: this._contextId,
|
contextId: this._contextId,
|
||||||
}).catch(rewriteError);
|
}).catch(rewriteError);
|
||||||
if (exceptionDetails)
|
if (exceptionDetails)
|
||||||
|
|
@ -44,7 +45,7 @@ export class CRExecutionContext implements js.ExecutionContextDelegate {
|
||||||
if (helper.isString(pageFunction)) {
|
if (helper.isString(pageFunction)) {
|
||||||
return this._callOnUtilityScript(context,
|
return this._callOnUtilityScript(context,
|
||||||
`evaluate`, [
|
`evaluate`, [
|
||||||
{ value: js.ensureSourceUrl(pageFunction) },
|
{ value: debugSupport.ensureSourceUrl(pageFunction) },
|
||||||
], returnByValue, () => { });
|
], returnByValue, () => { });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -85,7 +86,7 @@ export class CRExecutionContext implements js.ExecutionContextDelegate {
|
||||||
try {
|
try {
|
||||||
const utilityScript = await context.utilityScript();
|
const utilityScript = await context.utilityScript();
|
||||||
const { exceptionDetails, result: remoteObject } = await this._client.send('Runtime.callFunctionOn', {
|
const { exceptionDetails, result: remoteObject } = await this._client.send('Runtime.callFunctionOn', {
|
||||||
functionDeclaration: `function (...args) { return this.${method}(...args) }${js.generateSourceUrl()}`,
|
functionDeclaration: `function (...args) { return this.${method}(...args) }` + debugSupport.generateSourceUrl(),
|
||||||
objectId: utilityScript._remoteObject.objectId,
|
objectId: utilityScript._remoteObject.objectId,
|
||||||
arguments: [
|
arguments: [
|
||||||
{ value: returnByValue },
|
{ value: returnByValue },
|
||||||
|
|
@ -128,7 +129,7 @@ export class CRExecutionContext implements js.ExecutionContextDelegate {
|
||||||
const remoteObject = toRemoteObject(handle);
|
const remoteObject = toRemoteObject(handle);
|
||||||
if (remoteObject.objectId) {
|
if (remoteObject.objectId) {
|
||||||
const response = await this._client.send('Runtime.callFunctionOn', {
|
const response = await this._client.send('Runtime.callFunctionOn', {
|
||||||
functionDeclaration: 'function() { return this; }',
|
functionDeclaration: 'function() { return this; }' + debugSupport.generateSourceUrl(),
|
||||||
objectId: remoteObject.objectId,
|
objectId: remoteObject.objectId,
|
||||||
returnByValue: true,
|
returnByValue: true,
|
||||||
awaitPromise: true,
|
awaitPromise: true,
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,6 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import * as dom from '../dom';
|
import * as dom from '../dom';
|
||||||
import * as js from '../javascript';
|
|
||||||
import * as frames from '../frames';
|
import * as frames from '../frames';
|
||||||
import { helper, RegisteredListener, assert } from '../helper';
|
import { helper, RegisteredListener, assert } from '../helper';
|
||||||
import * as network from '../network';
|
import * as network from '../network';
|
||||||
|
|
@ -38,6 +37,7 @@ import * as types from '../types';
|
||||||
import { ConsoleMessage } from '../console';
|
import { ConsoleMessage } from '../console';
|
||||||
import { NotConnectedError } from '../errors';
|
import { NotConnectedError } from '../errors';
|
||||||
import { logError } from '../logger';
|
import { logError } from '../logger';
|
||||||
|
import * as debugSupport from '../debug/debugSupport';
|
||||||
|
|
||||||
|
|
||||||
const UTILITY_WORLD_NAME = '__playwright_utility_world__';
|
const UTILITY_WORLD_NAME = '__playwright_utility_world__';
|
||||||
|
|
@ -432,7 +432,7 @@ class FrameSession {
|
||||||
lifecycleEventsEnabled = this._client.send('Page.setLifecycleEventsEnabled', { enabled: true }),
|
lifecycleEventsEnabled = this._client.send('Page.setLifecycleEventsEnabled', { enabled: true }),
|
||||||
this._client.send('Runtime.enable', {}),
|
this._client.send('Runtime.enable', {}),
|
||||||
this._client.send('Page.addScriptToEvaluateOnNewDocument', {
|
this._client.send('Page.addScriptToEvaluateOnNewDocument', {
|
||||||
source: js.generateSourceUrl(),
|
source: debugSupport.generateSourceUrl(),
|
||||||
worldName: UTILITY_WORLD_NAME,
|
worldName: UTILITY_WORLD_NAME,
|
||||||
}),
|
}),
|
||||||
this._networkManager.initialize(),
|
this._networkManager.initialize(),
|
||||||
|
|
|
||||||
|
|
@ -16,11 +16,11 @@
|
||||||
|
|
||||||
import * as fs from 'fs';
|
import * as fs from 'fs';
|
||||||
import * as util from 'util';
|
import * as util from 'util';
|
||||||
import { CRSession } from './crConnection';
|
import { CRSession } from '../chromium/crConnection';
|
||||||
|
|
||||||
const kBindingName = '__pw_devtools__';
|
const kBindingName = '__pw_devtools__';
|
||||||
|
|
||||||
// This method intercepts preferences-related DevTools embedder methods
|
// This class intercepts preferences-related DevTools embedder methods
|
||||||
// and stores preferences as a json file in the browser installation directory.
|
// and stores preferences as a json file in the browser installation directory.
|
||||||
export class CRDevTools {
|
export class CRDevTools {
|
||||||
private _preferencesPath: string;
|
private _preferencesPath: string;
|
||||||
47
src/debug/debugSupport.ts
Normal file
47
src/debug/debugSupport.ts
Normal file
|
|
@ -0,0 +1,47 @@
|
||||||
|
/**
|
||||||
|
* 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 * as sourceMap from './sourceMap';
|
||||||
|
import { getFromENV } from '../helper';
|
||||||
|
|
||||||
|
let debugMode: boolean | undefined;
|
||||||
|
export function isDebugMode(): boolean {
|
||||||
|
if (debugMode === undefined)
|
||||||
|
debugMode = !!getFromENV('PLAYWRIGHT_DEBUG_UI');
|
||||||
|
return debugMode;
|
||||||
|
}
|
||||||
|
|
||||||
|
let sourceUrlCounter = 0;
|
||||||
|
const playwrightSourceUrlPrefix = '__playwright_evaluation_script__';
|
||||||
|
const sourceUrlRegex = /^[\040\t]*\/\/[@#] sourceURL=\s*(\S*?)\s*$/m;
|
||||||
|
export function generateSourceUrl(): string {
|
||||||
|
return `\n//# sourceURL=${playwrightSourceUrlPrefix}${sourceUrlCounter++}\n`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isPlaywrightSourceUrl(s: string): boolean {
|
||||||
|
return s.startsWith(playwrightSourceUrlPrefix);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ensureSourceUrl(expression: string): string {
|
||||||
|
return sourceUrlRegex.test(expression) ? expression : expression + generateSourceUrl();
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function generateSourceMapUrl(functionText: string, generatedText: string): Promise<string> {
|
||||||
|
if (!isDebugMode())
|
||||||
|
return generateSourceUrl();
|
||||||
|
const sourceMapUrl = await sourceMap.generateSourceMapUrl(functionText, generatedText);
|
||||||
|
return sourceMapUrl || generateSourceUrl();
|
||||||
|
}
|
||||||
145
src/debug/sourceMap.ts
Normal file
145
src/debug/sourceMap.ts
Normal file
|
|
@ -0,0 +1,145 @@
|
||||||
|
/**
|
||||||
|
* 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 * as fs from 'fs';
|
||||||
|
import * as util from 'util';
|
||||||
|
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, '..'));
|
||||||
|
|
||||||
|
type Position = {
|
||||||
|
line: number;
|
||||||
|
column: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function generateSourceMapUrl(functionText: string, generatedText: string): Promise<string | undefined> {
|
||||||
|
const filePath = getCallerFilePath();
|
||||||
|
if (!filePath)
|
||||||
|
return;
|
||||||
|
try {
|
||||||
|
const generatedIndex = generatedText.indexOf(functionText);
|
||||||
|
if (generatedIndex === -1)
|
||||||
|
return;
|
||||||
|
const compiledPosition = findPosition(generatedText, generatedIndex);
|
||||||
|
const source = await util.promisify(fs.readFile)(filePath, 'utf8');
|
||||||
|
const sourceIndex = source.indexOf(functionText);
|
||||||
|
if (sourceIndex === -1)
|
||||||
|
return;
|
||||||
|
const sourcePosition = findPosition(source, sourceIndex);
|
||||||
|
const delta = findPosition(functionText, functionText.length);
|
||||||
|
const sourceMap = generateSourceMap(filePath, sourcePosition, compiledPosition, delta);
|
||||||
|
return `\n//# sourceMappingURL=data:application/json;base64,${Buffer.from(sourceMap).toString('base64')}\n`;
|
||||||
|
} catch (e) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const VLQ_BASE_SHIFT = 5;
|
||||||
|
const VLQ_BASE = 1 << VLQ_BASE_SHIFT;
|
||||||
|
const VLQ_BASE_MASK = VLQ_BASE - 1;
|
||||||
|
const VLQ_CONTINUATION_BIT = VLQ_BASE;
|
||||||
|
const BASE64_DIGITS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
|
||||||
|
|
||||||
|
function base64VLQ(value: number): string {
|
||||||
|
if (value < 0)
|
||||||
|
value = ((-value) << 1) | 1;
|
||||||
|
else
|
||||||
|
value <<= 1;
|
||||||
|
let result = '';
|
||||||
|
do {
|
||||||
|
let digit = value & VLQ_BASE_MASK;
|
||||||
|
value >>>= VLQ_BASE_SHIFT;
|
||||||
|
if (value > 0)
|
||||||
|
digit |= VLQ_CONTINUATION_BIT;
|
||||||
|
result += BASE64_DIGITS[digit];
|
||||||
|
} while (value > 0);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateSourceMap(filePath: string, sourcePosition: Position, compiledPosition: Position, delta: Position): any {
|
||||||
|
const mappings = [];
|
||||||
|
let lastCompiled = { line: 0, column: 0 };
|
||||||
|
let lastSource = { line: 0, column: 0 };
|
||||||
|
for (let line = 0; line < delta.line; line++) {
|
||||||
|
// We need at least a mapping per line. This will yield an execution line at the start of each line.
|
||||||
|
// Note: for more granular mapping, we can do word-by-word.
|
||||||
|
const source = advancePosition(sourcePosition, { line, column: 0 });
|
||||||
|
const compiled = advancePosition(compiledPosition, { line, column: 0 });
|
||||||
|
while (lastCompiled.line < compiled.line) {
|
||||||
|
mappings.push(';');
|
||||||
|
lastCompiled.line++;
|
||||||
|
lastCompiled.column = 0;
|
||||||
|
}
|
||||||
|
mappings.push(base64VLQ(compiled.column - lastCompiled.column));
|
||||||
|
mappings.push(base64VLQ(0)); // Source index.
|
||||||
|
mappings.push(base64VLQ(source.line - lastSource.line));
|
||||||
|
mappings.push(base64VLQ(source.column - lastSource.column));
|
||||||
|
lastCompiled = compiled;
|
||||||
|
lastSource = source;
|
||||||
|
}
|
||||||
|
return JSON.stringify({
|
||||||
|
version: 3,
|
||||||
|
sources: ['file://' + filePath],
|
||||||
|
names: [],
|
||||||
|
mappings: mappings.join(''),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function findPosition(source: string, offset: number): Position {
|
||||||
|
const result: Position = { line: 0, column: 0 };
|
||||||
|
let index = 0;
|
||||||
|
while (true) {
|
||||||
|
const newline = source.indexOf('\n', index);
|
||||||
|
if (newline === -1 || newline >= offset)
|
||||||
|
break;
|
||||||
|
result.line++;
|
||||||
|
index = newline + 1;
|
||||||
|
}
|
||||||
|
result.column = offset - index;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
function advancePosition(position: Position, delta: Position) {
|
||||||
|
return {
|
||||||
|
line: position.line + delta.line,
|
||||||
|
column: delta.column + (delta.line ? 0 : position.column),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function getCallerFilePath(): string | null {
|
||||||
|
const error = new Error();
|
||||||
|
const stackFrames = (error.stack || '').split('\n').slice(1);
|
||||||
|
// Find first stackframe that doesn't point to PLAYWRIGHT_LIB_PATH.
|
||||||
|
for (let frame of stackFrames) {
|
||||||
|
frame = frame.trim();
|
||||||
|
if (!frame.startsWith('at '))
|
||||||
|
return null;
|
||||||
|
if (frame.endsWith(')')) {
|
||||||
|
const from = frame.indexOf('(');
|
||||||
|
frame = frame.substring(from + 1, frame.length - 1);
|
||||||
|
} else {
|
||||||
|
frame = frame.substring('at '.length);
|
||||||
|
}
|
||||||
|
const match = frame.match(/^(?:async )?(.*):(\d+):(\d+)$/);
|
||||||
|
if (!match)
|
||||||
|
return null;
|
||||||
|
const filePath = match[1];
|
||||||
|
if (filePath.startsWith(PLAYWRIGHT_LIB_PATH))
|
||||||
|
continue;
|
||||||
|
return filePath;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
@ -19,6 +19,7 @@ import { helper } from '../helper';
|
||||||
import * as js from '../javascript';
|
import * as js from '../javascript';
|
||||||
import { FFSession } from './ffConnection';
|
import { FFSession } from './ffConnection';
|
||||||
import { Protocol } from './protocol';
|
import { Protocol } from './protocol';
|
||||||
|
import * as debugSupport from '../debug/debugSupport';
|
||||||
|
|
||||||
export class FFExecutionContext implements js.ExecutionContextDelegate {
|
export class FFExecutionContext implements js.ExecutionContextDelegate {
|
||||||
_session: FFSession;
|
_session: FFSession;
|
||||||
|
|
@ -31,7 +32,7 @@ export class FFExecutionContext implements js.ExecutionContextDelegate {
|
||||||
|
|
||||||
async rawEvaluate(expression: string): Promise<js.RemoteObject> {
|
async rawEvaluate(expression: string): Promise<js.RemoteObject> {
|
||||||
const payload = await this._session.send('Runtime.evaluate', {
|
const payload = await this._session.send('Runtime.evaluate', {
|
||||||
expression: js.ensureSourceUrl(expression),
|
expression: debugSupport.ensureSourceUrl(expression),
|
||||||
returnByValue: false,
|
returnByValue: false,
|
||||||
executionContextId: this._executionContextId,
|
executionContextId: this._executionContextId,
|
||||||
}).catch(rewriteError);
|
}).catch(rewriteError);
|
||||||
|
|
@ -43,7 +44,7 @@ export class FFExecutionContext implements js.ExecutionContextDelegate {
|
||||||
if (helper.isString(pageFunction)) {
|
if (helper.isString(pageFunction)) {
|
||||||
return this._callOnUtilityScript(context,
|
return this._callOnUtilityScript(context,
|
||||||
`evaluate`, [
|
`evaluate`, [
|
||||||
{ value: pageFunction },
|
{ value: debugSupport.ensureSourceUrl(pageFunction) },
|
||||||
], returnByValue, () => {});
|
], returnByValue, () => {});
|
||||||
}
|
}
|
||||||
if (typeof pageFunction !== 'function')
|
if (typeof pageFunction !== 'function')
|
||||||
|
|
@ -75,7 +76,7 @@ export class FFExecutionContext implements js.ExecutionContextDelegate {
|
||||||
try {
|
try {
|
||||||
const utilityScript = await context.utilityScript();
|
const utilityScript = await context.utilityScript();
|
||||||
const payload = await this._session.send('Runtime.callFunction', {
|
const payload = await this._session.send('Runtime.callFunction', {
|
||||||
functionDeclaration: `(utilityScript, ...args) => utilityScript.${method}(...args)`,
|
functionDeclaration: `(utilityScript, ...args) => utilityScript.${method}(...args)` + debugSupport.generateSourceUrl(),
|
||||||
args: [
|
args: [
|
||||||
{ objectId: utilityScript._remoteObject.objectId, value: undefined },
|
{ objectId: utilityScript._remoteObject.objectId, value: undefined },
|
||||||
{ value: returnByValue },
|
{ value: returnByValue },
|
||||||
|
|
@ -123,7 +124,7 @@ export class FFExecutionContext implements js.ExecutionContextDelegate {
|
||||||
const simpleValue = await this._session.send('Runtime.callFunction', {
|
const simpleValue = await this._session.send('Runtime.callFunction', {
|
||||||
executionContextId: this._executionContextId,
|
executionContextId: this._executionContextId,
|
||||||
returnByValue: true,
|
returnByValue: true,
|
||||||
functionDeclaration: ((e: any) => e).toString() + js.generateSourceUrl(),
|
functionDeclaration: ((e: any) => e).toString() + debugSupport.generateSourceUrl(),
|
||||||
args: [this._toCallArgument(payload)],
|
args: [this._toCallArgument(payload)],
|
||||||
});
|
});
|
||||||
return deserializeValue(simpleValue.result!);
|
return deserializeValue(simpleValue.result!);
|
||||||
|
|
|
||||||
|
|
@ -23,9 +23,6 @@ import { TimeoutError } from './errors';
|
||||||
import * as types from './types';
|
import * as types from './types';
|
||||||
import { ChildProcess, execSync } from 'child_process';
|
import { ChildProcess, execSync } from 'child_process';
|
||||||
|
|
||||||
// NOTE: update this to point to playwright/lib when moving this file.
|
|
||||||
const PLAYWRIGHT_LIB_PATH = __dirname;
|
|
||||||
|
|
||||||
export type RegisteredListener = {
|
export type RegisteredListener = {
|
||||||
emitter: EventEmitter;
|
emitter: EventEmitter;
|
||||||
eventName: (string | symbol);
|
eventName: (string | symbol);
|
||||||
|
|
@ -397,38 +394,6 @@ export function logPolitely(toBeLogged: string) {
|
||||||
console.log(toBeLogged); // eslint-disable-line no-console
|
console.log(toBeLogged); // eslint-disable-line no-console
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getCallerFilePath(ignorePrefix = PLAYWRIGHT_LIB_PATH): string | null {
|
|
||||||
const error = new Error();
|
|
||||||
const stackFrames = (error.stack || '').split('\n').slice(1);
|
|
||||||
// Find first stackframe that doesn't point to ignorePrefix.
|
|
||||||
for (let frame of stackFrames) {
|
|
||||||
frame = frame.trim();
|
|
||||||
if (!frame.startsWith('at '))
|
|
||||||
return null;
|
|
||||||
if (frame.endsWith(')')) {
|
|
||||||
const from = frame.indexOf('(');
|
|
||||||
frame = frame.substring(from + 1, frame.length - 1);
|
|
||||||
} else {
|
|
||||||
frame = frame.substring('at '.length);
|
|
||||||
}
|
|
||||||
const match = frame.match(/^(?:async )?(.*):(\d+):(\d+)$/);
|
|
||||||
if (!match)
|
|
||||||
return null;
|
|
||||||
const filePath = match[1];
|
|
||||||
if (filePath.startsWith(ignorePrefix))
|
|
||||||
continue;
|
|
||||||
return filePath;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
let debugMode: boolean | undefined;
|
|
||||||
export function isDebugMode(): boolean {
|
|
||||||
if (debugMode === undefined)
|
|
||||||
debugMode = !!getFromENV('PLAYWRIGHT_DEBUG_UI');
|
|
||||||
return debugMode;
|
|
||||||
}
|
|
||||||
|
|
||||||
const escapeGlobChars = new Set(['/', '$', '^', '+', '.', '(', ')', '=', '!', '|']);
|
const escapeGlobChars = new Set(['/', '$', '^', '+', '.', '(', ')', '=', '!', '|']);
|
||||||
|
|
||||||
export const helper = Helper;
|
export const helper = Helper;
|
||||||
|
|
|
||||||
|
|
@ -16,12 +16,10 @@
|
||||||
|
|
||||||
import * as types from './types';
|
import * as types from './types';
|
||||||
import * as dom from './dom';
|
import * as dom from './dom';
|
||||||
import * as fs from 'fs';
|
import { helper } from './helper';
|
||||||
import * as util from 'util';
|
|
||||||
import * as js from './javascript';
|
|
||||||
import * as utilityScriptSource from './generated/utilityScriptSource';
|
import * as utilityScriptSource from './generated/utilityScriptSource';
|
||||||
import { helper, getCallerFilePath, isDebugMode } from './helper';
|
|
||||||
import { InnerLogger } from './logger';
|
import { InnerLogger } from './logger';
|
||||||
|
import * as debugSupport from './debug/debugSupport';
|
||||||
|
|
||||||
export interface ExecutionContextDelegate {
|
export interface ExecutionContextDelegate {
|
||||||
evaluate(context: ExecutionContext, returnByValue: boolean, pageFunction: string | Function, ...args: any[]): Promise<any>;
|
evaluate(context: ExecutionContext, returnByValue: boolean, pageFunction: string | Function, ...args: any[]): Promise<any>;
|
||||||
|
|
@ -35,7 +33,7 @@ export interface ExecutionContextDelegate {
|
||||||
export class ExecutionContext {
|
export class ExecutionContext {
|
||||||
readonly _delegate: ExecutionContextDelegate;
|
readonly _delegate: ExecutionContextDelegate;
|
||||||
readonly _logger: InnerLogger;
|
readonly _logger: InnerLogger;
|
||||||
private _utilityScriptPromise: Promise<js.JSHandle> | undefined;
|
private _utilityScriptPromise: Promise<JSHandle> | undefined;
|
||||||
|
|
||||||
constructor(delegate: ExecutionContextDelegate, logger: InnerLogger) {
|
constructor(delegate: ExecutionContextDelegate, logger: InnerLogger) {
|
||||||
this._delegate = delegate;
|
this._delegate = delegate;
|
||||||
|
|
@ -62,7 +60,7 @@ export class ExecutionContext {
|
||||||
return this.doEvaluateInternal(false /* returnByValue */, true /* waitForNavigations */, pageFunction, ...args);
|
return this.doEvaluateInternal(false /* returnByValue */, true /* waitForNavigations */, pageFunction, ...args);
|
||||||
}
|
}
|
||||||
|
|
||||||
utilityScript(): Promise<js.JSHandle> {
|
utilityScript(): Promise<JSHandle> {
|
||||||
if (!this._utilityScriptPromise) {
|
if (!this._utilityScriptPromise) {
|
||||||
const source = `new (${utilityScriptSource.source})()`;
|
const source = `new (${utilityScriptSource.source})()`;
|
||||||
this._utilityScriptPromise = this._delegate.rawEvaluate(source).then(object => this.createHandle(object));
|
this._utilityScriptPromise = this._delegate.rawEvaluate(source).then(object => this.createHandle(object));
|
||||||
|
|
@ -237,119 +235,6 @@ export async function prepareFunctionCall<T>(
|
||||||
toDispose.map(handlePromise => handlePromise.then(handle => handle.dispose()));
|
toDispose.map(handlePromise => handlePromise.then(handle => handle.dispose()));
|
||||||
};
|
};
|
||||||
|
|
||||||
const sourceMapUrl = await generateSourceMapUrl(originalText);
|
functionText += await debugSupport.generateSourceMapUrl(originalText, functionText);
|
||||||
functionText += sourceMapUrl;
|
|
||||||
return { functionText, values: [ args.length, ...args, guids.length, ...guids ], handles: resultHandles, dispose };
|
return { functionText, values: [ args.length, ...args, guids.length, ...guids ], handles: resultHandles, dispose };
|
||||||
}
|
}
|
||||||
|
|
||||||
let sourceUrlCounter = 0;
|
|
||||||
const playwrightSourceUrlPrefix = '__playwright_evaluation_script__';
|
|
||||||
const sourceUrlRegex = /^[\040\t]*\/\/[@#] sourceURL=\s*(\S*?)\s*$/m;
|
|
||||||
export function generateSourceUrl(): string {
|
|
||||||
return `\n//# sourceURL=${playwrightSourceUrlPrefix}${sourceUrlCounter++}\n`;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function isPlaywrightSourceUrl(s: string): boolean {
|
|
||||||
return s.startsWith(playwrightSourceUrlPrefix);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function ensureSourceUrl(expression: string): string {
|
|
||||||
return sourceUrlRegex.test(expression) ? expression : expression + generateSourceUrl();
|
|
||||||
}
|
|
||||||
|
|
||||||
type Position = {
|
|
||||||
line: number;
|
|
||||||
column: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
async function generateSourceMapUrl(functionText: string): Promise<string> {
|
|
||||||
if (!isDebugMode())
|
|
||||||
return generateSourceUrl();
|
|
||||||
const filePath = getCallerFilePath();
|
|
||||||
if (!filePath)
|
|
||||||
return generateSourceUrl();
|
|
||||||
try {
|
|
||||||
const source = await util.promisify(fs.readFile)(filePath, 'utf8');
|
|
||||||
const index = source.indexOf(functionText);
|
|
||||||
if (index === -1)
|
|
||||||
return generateSourceUrl();
|
|
||||||
const sourcePosition = findPosition(source, index);
|
|
||||||
const delta = findPosition(functionText, functionText.length);
|
|
||||||
const sourceMap = generateSourceMap(filePath, sourcePosition, { line: 0, column: 0 }, delta);
|
|
||||||
return `\n//# sourceMappingURL=data:application/json;base64,${Buffer.from(sourceMap).toString('base64')}\n`;
|
|
||||||
} catch (e) {
|
|
||||||
return generateSourceUrl();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const VLQ_BASE_SHIFT = 5;
|
|
||||||
const VLQ_BASE = 1 << VLQ_BASE_SHIFT;
|
|
||||||
const VLQ_BASE_MASK = VLQ_BASE - 1;
|
|
||||||
const VLQ_CONTINUATION_BIT = VLQ_BASE;
|
|
||||||
const BASE64_DIGITS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
|
|
||||||
|
|
||||||
function base64VLQ(value: number): string {
|
|
||||||
if (value < 0)
|
|
||||||
value = ((-value) << 1) | 1;
|
|
||||||
else
|
|
||||||
value <<= 1;
|
|
||||||
let result = '';
|
|
||||||
do {
|
|
||||||
let digit = value & VLQ_BASE_MASK;
|
|
||||||
value >>>= VLQ_BASE_SHIFT;
|
|
||||||
if (value > 0)
|
|
||||||
digit |= VLQ_CONTINUATION_BIT;
|
|
||||||
result += BASE64_DIGITS[digit];
|
|
||||||
} while (value > 0);
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
function generateSourceMap(filePath: string, sourcePosition: Position, compiledPosition: Position, delta: Position): any {
|
|
||||||
const mappings = [];
|
|
||||||
let lastCompiled = { line: 0, column: 0 };
|
|
||||||
let lastSource = { line: 0, column: 0 };
|
|
||||||
for (let line = 0; line < delta.line; line++) {
|
|
||||||
// We need at least a mapping per line. This will yield an execution line at the start of each line.
|
|
||||||
// Note: for more granular mapping, we can do word-by-word.
|
|
||||||
const source = advancePosition(sourcePosition, { line, column: 0 });
|
|
||||||
const compiled = advancePosition(compiledPosition, { line, column: 0 });
|
|
||||||
while (lastCompiled.line < compiled.line) {
|
|
||||||
mappings.push(';');
|
|
||||||
lastCompiled.line++;
|
|
||||||
lastCompiled.column = 0;
|
|
||||||
}
|
|
||||||
mappings.push(base64VLQ(compiled.column - lastCompiled.column));
|
|
||||||
mappings.push(base64VLQ(0)); // Source index.
|
|
||||||
mappings.push(base64VLQ(source.line - lastSource.line));
|
|
||||||
mappings.push(base64VLQ(source.column - lastSource.column));
|
|
||||||
lastCompiled = compiled;
|
|
||||||
lastSource = source;
|
|
||||||
}
|
|
||||||
return JSON.stringify({
|
|
||||||
version: 3,
|
|
||||||
sources: ['file://' + filePath],
|
|
||||||
names: [],
|
|
||||||
mappings: mappings.join(''),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function findPosition(source: string, offset: number): Position {
|
|
||||||
const result: Position = { line: 0, column: 0 };
|
|
||||||
let index = 0;
|
|
||||||
while (true) {
|
|
||||||
const newline = source.indexOf('\n', index);
|
|
||||||
if (newline === -1 || newline >= offset)
|
|
||||||
break;
|
|
||||||
result.line++;
|
|
||||||
index = newline + 1;
|
|
||||||
}
|
|
||||||
result.column = offset - index;
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
function advancePosition(position: Position, delta: Position) {
|
|
||||||
return {
|
|
||||||
line: position.line + delta.line,
|
|
||||||
column: delta.column + (delta.line ? 0 : position.column),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
import { helper, assert, isDebugMode } from '../helper';
|
import { helper, assert } from '../helper';
|
||||||
import { CRBrowser } from '../chromium/crBrowser';
|
import { CRBrowser } from '../chromium/crBrowser';
|
||||||
import * as ws from 'ws';
|
import * as ws from 'ws';
|
||||||
import { Env } from './processLauncher';
|
import { Env } from './processLauncher';
|
||||||
|
|
@ -26,7 +26,8 @@ import { WebSocketWrapper } from './browserServer';
|
||||||
import { ConnectionTransport, ProtocolRequest } from '../transport';
|
import { ConnectionTransport, ProtocolRequest } from '../transport';
|
||||||
import { InnerLogger, logError } from '../logger';
|
import { InnerLogger, logError } from '../logger';
|
||||||
import { BrowserDescriptor } from '../install/browserPaths';
|
import { BrowserDescriptor } from '../install/browserPaths';
|
||||||
import { CRDevTools } from '../chromium/crDevTools';
|
import { CRDevTools } from '../debug/crDevTools';
|
||||||
|
import * as debugSupport from '../debug/debugSupport';
|
||||||
import { BrowserOptions } from '../browser';
|
import { BrowserOptions } from '../browser';
|
||||||
|
|
||||||
export class Chromium extends BrowserTypeBase {
|
export class Chromium extends BrowserTypeBase {
|
||||||
|
|
@ -34,7 +35,7 @@ export class Chromium extends BrowserTypeBase {
|
||||||
|
|
||||||
constructor(packagePath: string, browser: BrowserDescriptor) {
|
constructor(packagePath: string, browser: BrowserDescriptor) {
|
||||||
super(packagePath, browser, null /* use pipe not websocket */);
|
super(packagePath, browser, null /* use pipe not websocket */);
|
||||||
if (isDebugMode())
|
if (debugSupport.isDebugMode())
|
||||||
this._devtools = this._createDevTools();
|
this._devtools = this._createDevTools();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,7 @@ import { helper } from '../helper';
|
||||||
import { valueFromRemoteObject, releaseObject } from './wkProtocolHelper';
|
import { valueFromRemoteObject, releaseObject } from './wkProtocolHelper';
|
||||||
import { Protocol } from './protocol';
|
import { Protocol } from './protocol';
|
||||||
import * as js from '../javascript';
|
import * as js from '../javascript';
|
||||||
|
import * as debugSupport from '../debug/debugSupport';
|
||||||
|
|
||||||
type MaybeCallArgument = Protocol.Runtime.CallArgument | { unserializable: any };
|
type MaybeCallArgument = Protocol.Runtime.CallArgument | { unserializable: any };
|
||||||
|
|
||||||
|
|
@ -44,7 +45,7 @@ export class WKExecutionContext implements js.ExecutionContextDelegate {
|
||||||
async rawEvaluate(expression: string): Promise<js.RemoteObject> {
|
async rawEvaluate(expression: string): Promise<js.RemoteObject> {
|
||||||
const contextId = this._contextId;
|
const contextId = this._contextId;
|
||||||
const response = await this._session.send('Runtime.evaluate', {
|
const response = await this._session.send('Runtime.evaluate', {
|
||||||
expression: js.ensureSourceUrl(expression),
|
expression: debugSupport.ensureSourceUrl(expression),
|
||||||
contextId,
|
contextId,
|
||||||
returnByValue: false
|
returnByValue: false
|
||||||
});
|
});
|
||||||
|
|
@ -82,11 +83,11 @@ export class WKExecutionContext implements js.ExecutionContextDelegate {
|
||||||
private async _evaluateRemoteObject(context: js.ExecutionContext, pageFunction: Function | string, args: any[], returnByValue: boolean): Promise<Protocol.Runtime.callFunctionOnReturnValue> {
|
private async _evaluateRemoteObject(context: js.ExecutionContext, pageFunction: Function | string, args: any[], returnByValue: boolean): Promise<Protocol.Runtime.callFunctionOnReturnValue> {
|
||||||
if (helper.isString(pageFunction)) {
|
if (helper.isString(pageFunction)) {
|
||||||
const utilityScript = await context.utilityScript();
|
const utilityScript = await context.utilityScript();
|
||||||
const functionDeclaration = `function (returnByValue, pageFunction) { return this.evaluate(returnByValue, pageFunction); }${js.generateSourceUrl()}`;
|
const functionDeclaration = `function (returnByValue, pageFunction) { return this.evaluate(returnByValue, pageFunction); }` + debugSupport.generateSourceUrl();
|
||||||
return await this._session.send('Runtime.callFunctionOn', {
|
return await this._session.send('Runtime.callFunctionOn', {
|
||||||
functionDeclaration,
|
functionDeclaration,
|
||||||
objectId: utilityScript._remoteObject.objectId!,
|
objectId: utilityScript._remoteObject.objectId!,
|
||||||
arguments: [ { value: returnByValue }, { value: pageFunction } ],
|
arguments: [ { value: returnByValue }, { value: debugSupport.ensureSourceUrl(pageFunction) } ],
|
||||||
returnByValue: false, // We need to return real Promise if that is a promise.
|
returnByValue: false, // We need to return real Promise if that is a promise.
|
||||||
emulateUserGesture: true
|
emulateUserGesture: true
|
||||||
});
|
});
|
||||||
|
|
@ -113,7 +114,7 @@ export class WKExecutionContext implements js.ExecutionContextDelegate {
|
||||||
const utilityScript = await context.utilityScript();
|
const utilityScript = await context.utilityScript();
|
||||||
const callParams = this._serializeFunctionAndArguments(functionText, values, handles, returnByValue);
|
const callParams = this._serializeFunctionAndArguments(functionText, values, handles, returnByValue);
|
||||||
return await this._session.send('Runtime.callFunctionOn', {
|
return await this._session.send('Runtime.callFunctionOn', {
|
||||||
functionDeclaration: callParams.functionText,
|
functionDeclaration: callParams.functionText + debugSupport.generateSourceUrl(),
|
||||||
objectId: utilityScript._remoteObject.objectId!,
|
objectId: utilityScript._remoteObject.objectId!,
|
||||||
arguments: callParams.callArguments,
|
arguments: callParams.callArguments,
|
||||||
returnByValue: false, // We need to return real Promise if that is a promise.
|
returnByValue: false, // We need to return real Promise if that is a promise.
|
||||||
|
|
@ -126,7 +127,7 @@ export class WKExecutionContext implements js.ExecutionContextDelegate {
|
||||||
|
|
||||||
private _serializeFunctionAndArguments(originalText: string, values: any[], handles: MaybeCallArgument[], returnByValue: boolean): { functionText: string, callArguments: Protocol.Runtime.CallArgument[]} {
|
private _serializeFunctionAndArguments(originalText: string, values: any[], handles: MaybeCallArgument[], returnByValue: boolean): { functionText: string, callArguments: Protocol.Runtime.CallArgument[]} {
|
||||||
const callArguments: Protocol.Runtime.CallArgument[] = values.map(value => ({ value }));
|
const callArguments: Protocol.Runtime.CallArgument[] = values.map(value => ({ value }));
|
||||||
let functionText = `function (returnByValue, functionText, ...args) { return this.callFunction(returnByValue, functionText, ...args); }${js.generateSourceUrl()}`;
|
let functionText = `function (returnByValue, functionText, ...args) { return this.callFunction(returnByValue, functionText, ...args); }`;
|
||||||
if (handles.some(handle => 'unserializable' in handle)) {
|
if (handles.some(handle => 'unserializable' in handle)) {
|
||||||
const paramStrings = [];
|
const paramStrings = [];
|
||||||
for (let i = 0; i < callArguments.length; i++)
|
for (let i = 0; i < callArguments.length; i++)
|
||||||
|
|
@ -139,7 +140,7 @@ export class WKExecutionContext implements js.ExecutionContextDelegate {
|
||||||
callArguments.push(handle);
|
callArguments.push(handle);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
functionText = `function (returnByValue, functionText, ...a) { return this.callFunction(returnByValue, functionText, ${paramStrings.join(',')}); }${js.generateSourceUrl()}`;
|
functionText = `function (returnByValue, functionText, ...a) { return this.callFunction(returnByValue, functionText, ${paramStrings.join(',')}); }`;
|
||||||
} else {
|
} else {
|
||||||
callArguments.push(...(handles as Protocol.Runtime.CallArgument[]));
|
callArguments.push(...(handles as Protocol.Runtime.CallArgument[]));
|
||||||
}
|
}
|
||||||
|
|
@ -167,7 +168,7 @@ export class WKExecutionContext implements js.ExecutionContextDelegate {
|
||||||
try {
|
try {
|
||||||
const serializeResponse = await this._session.send('Runtime.callFunctionOn', {
|
const serializeResponse = await this._session.send('Runtime.callFunctionOn', {
|
||||||
// Serialize object using standard JSON implementation to correctly pass 'undefined'.
|
// Serialize object using standard JSON implementation to correctly pass 'undefined'.
|
||||||
functionDeclaration: 'function(){return this}\n' + js.generateSourceUrl(),
|
functionDeclaration: 'function(){return this}\n' + debugSupport.generateSourceUrl(),
|
||||||
objectId: objectId,
|
objectId: objectId,
|
||||||
returnByValue: true
|
returnByValue: true
|
||||||
});
|
});
|
||||||
|
|
@ -206,7 +207,7 @@ export class WKExecutionContext implements js.ExecutionContextDelegate {
|
||||||
const remoteObject = handle._remoteObject;
|
const remoteObject = handle._remoteObject;
|
||||||
if (remoteObject.objectId) {
|
if (remoteObject.objectId) {
|
||||||
const response = await this._session.send('Runtime.callFunctionOn', {
|
const response = await this._session.send('Runtime.callFunctionOn', {
|
||||||
functionDeclaration: 'function() { return this; }',
|
functionDeclaration: 'function() { return this; }' + debugSupport.generateSourceUrl(),
|
||||||
objectId: remoteObject.objectId,
|
objectId: remoteObject.objectId,
|
||||||
returnByValue: true
|
returnByValue: true
|
||||||
});
|
});
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue