Skip to content

Commit 8f0f32b

Browse files
authored
chore: move debug-related code to src/debug (#2309)
1 parent 4e86d39 commit 8f0f32b

File tree

12 files changed

+227
-181
lines changed

12 files changed

+227
-181
lines changed

src/chromium/crBrowser.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ import { Events } from './events';
3030
import { Protocol } from './protocol';
3131
import { CRExecutionContext } from './crExecutionContext';
3232
import { logError } from '../logger';
33-
import { CRDevTools } from './crDevTools';
33+
import { CRDevTools } from '../debug/crDevTools';
3434

3535
export class CRBrowser extends BrowserBase {
3636
readonly _connection: CRConnection;

src/chromium/crCoverage.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,8 @@
1818
import { CRSession } from './crConnection';
1919
import { assert, helper, RegisteredListener } from '../helper';
2020
import { Protocol } from './protocol';
21-
import * as js from '../javascript';
2221
import * as types from '../types';
22+
import * as debugSupport from '../debug/debugSupport';
2323
import { logError, InnerLogger } from '../logger';
2424

2525
type JSRange = {
@@ -125,7 +125,7 @@ class JSCoverage {
125125

126126
async _onScriptParsed(event: Protocol.Debugger.scriptParsedPayload) {
127127
// Ignore playwright-injected scripts
128-
if (js.isPlaywrightSourceUrl(event.url))
128+
if (debugSupport.isPlaywrightSourceUrl(event.url))
129129
return;
130130
this._scriptIds.add(event.scriptId);
131131
// Ignore other anonymous scripts unless the reportAnonymousScripts option is true.

src/chromium/crExecutionContext.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import { helper } from '../helper';
2020
import { valueFromRemoteObject, getExceptionMessage, releaseObject } from './crProtocolHelper';
2121
import { Protocol } from './protocol';
2222
import * as js from '../javascript';
23+
import * as debugSupport from '../debug/debugSupport';
2324

2425
export class CRExecutionContext implements js.ExecutionContextDelegate {
2526
_client: CRSession;
@@ -32,7 +33,7 @@ export class CRExecutionContext implements js.ExecutionContextDelegate {
3233

3334
async rawEvaluate(expression: string): Promise<js.RemoteObject> {
3435
const { exceptionDetails, result: remoteObject } = await this._client.send('Runtime.evaluate', {
35-
expression: js.ensureSourceUrl(expression),
36+
expression: debugSupport.ensureSourceUrl(expression),
3637
contextId: this._contextId,
3738
}).catch(rewriteError);
3839
if (exceptionDetails)
@@ -44,7 +45,7 @@ export class CRExecutionContext implements js.ExecutionContextDelegate {
4445
if (helper.isString(pageFunction)) {
4546
return this._callOnUtilityScript(context,
4647
`evaluate`, [
47-
{ value: js.ensureSourceUrl(pageFunction) },
48+
{ value: debugSupport.ensureSourceUrl(pageFunction) },
4849
], returnByValue, () => { });
4950
}
5051

@@ -85,7 +86,7 @@ export class CRExecutionContext implements js.ExecutionContextDelegate {
8586
try {
8687
const utilityScript = await context.utilityScript();
8788
const { exceptionDetails, result: remoteObject } = await this._client.send('Runtime.callFunctionOn', {
88-
functionDeclaration: `function (...args) { return this.${method}(...args) }${js.generateSourceUrl()}`,
89+
functionDeclaration: `function (...args) { return this.${method}(...args) }` + debugSupport.generateSourceUrl(),
8990
objectId: utilityScript._remoteObject.objectId,
9091
arguments: [
9192
{ value: returnByValue },
@@ -128,7 +129,7 @@ export class CRExecutionContext implements js.ExecutionContextDelegate {
128129
const remoteObject = toRemoteObject(handle);
129130
if (remoteObject.objectId) {
130131
const response = await this._client.send('Runtime.callFunctionOn', {
131-
functionDeclaration: 'function() { return this; }',
132+
functionDeclaration: 'function() { return this; }' + debugSupport.generateSourceUrl(),
132133
objectId: remoteObject.objectId,
133134
returnByValue: true,
134135
awaitPromise: true,

src/chromium/crPage.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@
1616
*/
1717

1818
import * as dom from '../dom';
19-
import * as js from '../javascript';
2019
import * as frames from '../frames';
2120
import { helper, RegisteredListener, assert } from '../helper';
2221
import * as network from '../network';
@@ -38,6 +37,7 @@ import * as types from '../types';
3837
import { ConsoleMessage } from '../console';
3938
import { NotConnectedError } from '../errors';
4039
import { logError } from '../logger';
40+
import * as debugSupport from '../debug/debugSupport';
4141

4242

4343
const UTILITY_WORLD_NAME = '__playwright_utility_world__';
@@ -432,7 +432,7 @@ class FrameSession {
432432
lifecycleEventsEnabled = this._client.send('Page.setLifecycleEventsEnabled', { enabled: true }),
433433
this._client.send('Runtime.enable', {}),
434434
this._client.send('Page.addScriptToEvaluateOnNewDocument', {
435-
source: js.generateSourceUrl(),
435+
source: debugSupport.generateSourceUrl(),
436436
worldName: UTILITY_WORLD_NAME,
437437
}),
438438
this._networkManager.initialize(),

src/chromium/crDevTools.ts renamed to src/debug/crDevTools.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,11 @@
1616

1717
import * as fs from 'fs';
1818
import * as util from 'util';
19-
import { CRSession } from './crConnection';
19+
import { CRSession } from '../chromium/crConnection';
2020

2121
const kBindingName = '__pw_devtools__';
2222

23-
// This method intercepts preferences-related DevTools embedder methods
23+
// This class intercepts preferences-related DevTools embedder methods
2424
// and stores preferences as a json file in the browser installation directory.
2525
export class CRDevTools {
2626
private _preferencesPath: string;

src/debug/debugSupport.ts

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
/**
2+
* Copyright (c) Microsoft Corporation.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import * as sourceMap from './sourceMap';
18+
import { getFromENV } from '../helper';
19+
20+
let debugMode: boolean | undefined;
21+
export function isDebugMode(): boolean {
22+
if (debugMode === undefined)
23+
debugMode = !!getFromENV('PLAYWRIGHT_DEBUG_UI');
24+
return debugMode;
25+
}
26+
27+
let sourceUrlCounter = 0;
28+
const playwrightSourceUrlPrefix = '__playwright_evaluation_script__';
29+
const sourceUrlRegex = /^[\040\t]*\/\/[@#] sourceURL=\s*(\S*?)\s*$/m;
30+
export function generateSourceUrl(): string {
31+
return `\n//# sourceURL=${playwrightSourceUrlPrefix}${sourceUrlCounter++}\n`;
32+
}
33+
34+
export function isPlaywrightSourceUrl(s: string): boolean {
35+
return s.startsWith(playwrightSourceUrlPrefix);
36+
}
37+
38+
export function ensureSourceUrl(expression: string): string {
39+
return sourceUrlRegex.test(expression) ? expression : expression + generateSourceUrl();
40+
}
41+
42+
export async function generateSourceMapUrl(functionText: string, generatedText: string): Promise<string> {
43+
if (!isDebugMode())
44+
return generateSourceUrl();
45+
const sourceMapUrl = await sourceMap.generateSourceMapUrl(functionText, generatedText);
46+
return sourceMapUrl || generateSourceUrl();
47+
}

src/debug/sourceMap.ts

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
/**
2+
* Copyright (c) Microsoft Corporation.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import * as fs from 'fs';
18+
import * as util from 'util';
19+
import * as path from 'path';
20+
21+
// NOTE: update this to point to playwright/lib when moving this file.
22+
const PLAYWRIGHT_LIB_PATH = path.normalize(path.join(__dirname, '..'));
23+
24+
type Position = {
25+
line: number;
26+
column: number;
27+
};
28+
29+
export async function generateSourceMapUrl(functionText: string, generatedText: string): Promise<string | undefined> {
30+
const filePath = getCallerFilePath();
31+
if (!filePath)
32+
return;
33+
try {
34+
const generatedIndex = generatedText.indexOf(functionText);
35+
if (generatedIndex === -1)
36+
return;
37+
const compiledPosition = findPosition(generatedText, generatedIndex);
38+
const source = await util.promisify(fs.readFile)(filePath, 'utf8');
39+
const sourceIndex = source.indexOf(functionText);
40+
if (sourceIndex === -1)
41+
return;
42+
const sourcePosition = findPosition(source, sourceIndex);
43+
const delta = findPosition(functionText, functionText.length);
44+
const sourceMap = generateSourceMap(filePath, sourcePosition, compiledPosition, delta);
45+
return `\n//# sourceMappingURL=data:application/json;base64,${Buffer.from(sourceMap).toString('base64')}\n`;
46+
} catch (e) {
47+
}
48+
}
49+
50+
const VLQ_BASE_SHIFT = 5;
51+
const VLQ_BASE = 1 << VLQ_BASE_SHIFT;
52+
const VLQ_BASE_MASK = VLQ_BASE - 1;
53+
const VLQ_CONTINUATION_BIT = VLQ_BASE;
54+
const BASE64_DIGITS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
55+
56+
function base64VLQ(value: number): string {
57+
if (value < 0)
58+
value = ((-value) << 1) | 1;
59+
else
60+
value <<= 1;
61+
let result = '';
62+
do {
63+
let digit = value & VLQ_BASE_MASK;
64+
value >>>= VLQ_BASE_SHIFT;
65+
if (value > 0)
66+
digit |= VLQ_CONTINUATION_BIT;
67+
result += BASE64_DIGITS[digit];
68+
} while (value > 0);
69+
return result;
70+
}
71+
72+
function generateSourceMap(filePath: string, sourcePosition: Position, compiledPosition: Position, delta: Position): any {
73+
const mappings = [];
74+
let lastCompiled = { line: 0, column: 0 };
75+
let lastSource = { line: 0, column: 0 };
76+
for (let line = 0; line < delta.line; line++) {
77+
// We need at least a mapping per line. This will yield an execution line at the start of each line.
78+
// Note: for more granular mapping, we can do word-by-word.
79+
const source = advancePosition(sourcePosition, { line, column: 0 });
80+
const compiled = advancePosition(compiledPosition, { line, column: 0 });
81+
while (lastCompiled.line < compiled.line) {
82+
mappings.push(';');
83+
lastCompiled.line++;
84+
lastCompiled.column = 0;
85+
}
86+
mappings.push(base64VLQ(compiled.column - lastCompiled.column));
87+
mappings.push(base64VLQ(0)); // Source index.
88+
mappings.push(base64VLQ(source.line - lastSource.line));
89+
mappings.push(base64VLQ(source.column - lastSource.column));
90+
lastCompiled = compiled;
91+
lastSource = source;
92+
}
93+
return JSON.stringify({
94+
version: 3,
95+
sources: ['file://' + filePath],
96+
names: [],
97+
mappings: mappings.join(''),
98+
});
99+
}
100+
101+
function findPosition(source: string, offset: number): Position {
102+
const result: Position = { line: 0, column: 0 };
103+
let index = 0;
104+
while (true) {
105+
const newline = source.indexOf('\n', index);
106+
if (newline === -1 || newline >= offset)
107+
break;
108+
result.line++;
109+
index = newline + 1;
110+
}
111+
result.column = offset - index;
112+
return result;
113+
}
114+
115+
function advancePosition(position: Position, delta: Position) {
116+
return {
117+
line: position.line + delta.line,
118+
column: delta.column + (delta.line ? 0 : position.column),
119+
};
120+
}
121+
122+
function getCallerFilePath(): string | null {
123+
const error = new Error();
124+
const stackFrames = (error.stack || '').split('\n').slice(1);
125+
// Find first stackframe that doesn't point to PLAYWRIGHT_LIB_PATH.
126+
for (let frame of stackFrames) {
127+
frame = frame.trim();
128+
if (!frame.startsWith('at '))
129+
return null;
130+
if (frame.endsWith(')')) {
131+
const from = frame.indexOf('(');
132+
frame = frame.substring(from + 1, frame.length - 1);
133+
} else {
134+
frame = frame.substring('at '.length);
135+
}
136+
const match = frame.match(/^(?:async )?(.*):(\d+):(\d+)$/);
137+
if (!match)
138+
return null;
139+
const filePath = match[1];
140+
if (filePath.startsWith(PLAYWRIGHT_LIB_PATH))
141+
continue;
142+
return filePath;
143+
}
144+
return null;
145+
}

src/firefox/ffExecutionContext.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import { helper } from '../helper';
1919
import * as js from '../javascript';
2020
import { FFSession } from './ffConnection';
2121
import { Protocol } from './protocol';
22+
import * as debugSupport from '../debug/debugSupport';
2223

2324
export class FFExecutionContext implements js.ExecutionContextDelegate {
2425
_session: FFSession;
@@ -31,7 +32,7 @@ export class FFExecutionContext implements js.ExecutionContextDelegate {
3132

3233
async rawEvaluate(expression: string): Promise<js.RemoteObject> {
3334
const payload = await this._session.send('Runtime.evaluate', {
34-
expression: js.ensureSourceUrl(expression),
35+
expression: debugSupport.ensureSourceUrl(expression),
3536
returnByValue: false,
3637
executionContextId: this._executionContextId,
3738
}).catch(rewriteError);
@@ -43,7 +44,7 @@ export class FFExecutionContext implements js.ExecutionContextDelegate {
4344
if (helper.isString(pageFunction)) {
4445
return this._callOnUtilityScript(context,
4546
`evaluate`, [
46-
{ value: pageFunction },
47+
{ value: debugSupport.ensureSourceUrl(pageFunction) },
4748
], returnByValue, () => {});
4849
}
4950
if (typeof pageFunction !== 'function')
@@ -75,7 +76,7 @@ export class FFExecutionContext implements js.ExecutionContextDelegate {
7576
try {
7677
const utilityScript = await context.utilityScript();
7778
const payload = await this._session.send('Runtime.callFunction', {
78-
functionDeclaration: `(utilityScript, ...args) => utilityScript.${method}(...args)`,
79+
functionDeclaration: `(utilityScript, ...args) => utilityScript.${method}(...args)` + debugSupport.generateSourceUrl(),
7980
args: [
8081
{ objectId: utilityScript._remoteObject.objectId, value: undefined },
8182
{ value: returnByValue },
@@ -123,7 +124,7 @@ export class FFExecutionContext implements js.ExecutionContextDelegate {
123124
const simpleValue = await this._session.send('Runtime.callFunction', {
124125
executionContextId: this._executionContextId,
125126
returnByValue: true,
126-
functionDeclaration: ((e: any) => e).toString() + js.generateSourceUrl(),
127+
functionDeclaration: ((e: any) => e).toString() + debugSupport.generateSourceUrl(),
127128
args: [this._toCallArgument(payload)],
128129
});
129130
return deserializeValue(simpleValue.result!);

src/helper.ts

Lines changed: 0 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,6 @@ import { TimeoutError } from './errors';
2323
import * as types from './types';
2424
import { ChildProcess, execSync } from 'child_process';
2525

26-
// NOTE: update this to point to playwright/lib when moving this file.
27-
const PLAYWRIGHT_LIB_PATH = __dirname;
28-
2926
export type RegisteredListener = {
3027
emitter: EventEmitter;
3128
eventName: (string | symbol);
@@ -397,38 +394,6 @@ export function logPolitely(toBeLogged: string) {
397394
console.log(toBeLogged); // eslint-disable-line no-console
398395
}
399396

400-
export function getCallerFilePath(ignorePrefix = PLAYWRIGHT_LIB_PATH): string | null {
401-
const error = new Error();
402-
const stackFrames = (error.stack || '').split('\n').slice(1);
403-
// Find first stackframe that doesn't point to ignorePrefix.
404-
for (let frame of stackFrames) {
405-
frame = frame.trim();
406-
if (!frame.startsWith('at '))
407-
return null;
408-
if (frame.endsWith(')')) {
409-
const from = frame.indexOf('(');
410-
frame = frame.substring(from + 1, frame.length - 1);
411-
} else {
412-
frame = frame.substring('at '.length);
413-
}
414-
const match = frame.match(/^(?:async )?(.*):(\d+):(\d+)$/);
415-
if (!match)
416-
return null;
417-
const filePath = match[1];
418-
if (filePath.startsWith(ignorePrefix))
419-
continue;
420-
return filePath;
421-
}
422-
return null;
423-
}
424-
425-
let debugMode: boolean | undefined;
426-
export function isDebugMode(): boolean {
427-
if (debugMode === undefined)
428-
debugMode = !!getFromENV('PLAYWRIGHT_DEBUG_UI');
429-
return debugMode;
430-
}
431-
432397
const escapeGlobChars = new Set(['/', '$', '^', '+', '.', '(', ')', '=', '!', '|']);
433398

434399
export const helper = Helper;

0 commit comments

Comments
 (0)