feat(debug): expose playwright object in console (#2365)
- playwright.$ and playwright.$$ to query elements; - playwright.inspect to reveal an element; - playwright.clear to remove highlight.
This commit is contained in:
parent
0753c2d54b
commit
ece4789165
|
|
@ -26,6 +26,7 @@ import { Download } from './download';
|
||||||
import { BrowserBase } from './browser';
|
import { BrowserBase } from './browser';
|
||||||
import { Log, InnerLogger, Logger, RootLogger } from './logger';
|
import { Log, InnerLogger, Logger, RootLogger } from './logger';
|
||||||
import { FunctionWithSource } from './frames';
|
import { FunctionWithSource } from './frames';
|
||||||
|
import * as debugSupport from './debug/debugSupport';
|
||||||
|
|
||||||
export type PersistentContextOptions = {
|
export type PersistentContextOptions = {
|
||||||
viewport?: types.Size | null,
|
viewport?: types.Size | null,
|
||||||
|
|
@ -95,6 +96,10 @@ export abstract class BrowserContextBase extends ExtendedEventEmitter implements
|
||||||
this._closePromise = new Promise(fulfill => this._closePromiseFulfill = fulfill);
|
this._closePromise = new Promise(fulfill => this._closePromiseFulfill = fulfill);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async _initialize() {
|
||||||
|
await debugSupport.installConsoleHelpers(this);
|
||||||
|
}
|
||||||
|
|
||||||
protected _abortPromiseForEvent(event: string) {
|
protected _abortPromiseForEvent(event: string) {
|
||||||
return event === Events.BrowserContext.Close ? super._abortPromiseForEvent(event) : this._closePromise;
|
return event === Events.BrowserContext.Close ? super._abortPromiseForEvent(event) : this._closePromise;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -291,7 +291,7 @@ export class CRBrowserContext extends BrowserContextBase {
|
||||||
|
|
||||||
async _initialize() {
|
async _initialize() {
|
||||||
assert(!Array.from(this._browser._crPages.values()).some(page => page._browserContext === this));
|
assert(!Array.from(this._browser._crPages.values()).some(page => page._browserContext === this));
|
||||||
const promises: Promise<any>[] = [];
|
const promises: Promise<any>[] = [ super._initialize() ];
|
||||||
if (this._browser._options.downloadsPath) {
|
if (this._browser._options.downloadsPath) {
|
||||||
promises.push(this._browser._session.send('Browser.setDownloadBehavior', {
|
promises.push(this._browser._session.send('Browser.setDownloadBehavior', {
|
||||||
behavior: this._options.acceptDownloads ? 'allowAndName' : 'deny',
|
behavior: this._options.acceptDownloads ? 'allowAndName' : 'deny',
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,13 @@
|
||||||
|
|
||||||
import * as sourceMap from './sourceMap';
|
import * as sourceMap from './sourceMap';
|
||||||
import { getFromENV } from '../helper';
|
import { getFromENV } from '../helper';
|
||||||
|
import { BrowserContextBase } from '../browserContext';
|
||||||
|
import { Frame } from '../frames';
|
||||||
|
import { Events } from '../events';
|
||||||
|
import { Page } from '../page';
|
||||||
|
import { parseSelector } from '../selectors';
|
||||||
|
import * as types from '../types';
|
||||||
|
import InjectedScript from '../injected/injectedScript';
|
||||||
|
|
||||||
let debugMode: boolean | undefined;
|
let debugMode: boolean | undefined;
|
||||||
export function isDebugMode(): boolean {
|
export function isDebugMode(): boolean {
|
||||||
|
|
@ -45,3 +52,96 @@ export async function generateSourceMapUrl(functionText: string, generatedText:
|
||||||
const sourceMapUrl = await sourceMap.generateSourceMapUrl(functionText, generatedText);
|
const sourceMapUrl = await sourceMap.generateSourceMapUrl(functionText, generatedText);
|
||||||
return sourceMapUrl || generateSourceUrl();
|
return sourceMapUrl || generateSourceUrl();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function installConsoleHelpers(context: BrowserContextBase) {
|
||||||
|
if (!isDebugMode())
|
||||||
|
return;
|
||||||
|
const installInFrame = async (frame: Frame) => {
|
||||||
|
try {
|
||||||
|
const mainContext = await frame._mainContext();
|
||||||
|
const injectedScript = await mainContext.injectedScript();
|
||||||
|
await injectedScript.evaluate(installPlaywrightObjectOnWindow, parseSelector.toString());
|
||||||
|
} catch (e) {
|
||||||
|
}
|
||||||
|
};
|
||||||
|
context.on(Events.BrowserContext.Page, (page: Page) => {
|
||||||
|
installInFrame(page.mainFrame());
|
||||||
|
page.on(Events.Page.FrameNavigated, installInFrame);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function installPlaywrightObjectOnWindow(injectedScript: InjectedScript, parseSelectorFunctionString: string) {
|
||||||
|
const parseSelector: (selector: string) => types.ParsedSelector =
|
||||||
|
new Function('...args', 'return (' + parseSelectorFunctionString + ')(...args)') as any;
|
||||||
|
|
||||||
|
const highlightContainer = document.createElement('div');
|
||||||
|
highlightContainer.style.cssText = 'position: absolute; left: 0; top: 0; pointer-events: none; overflow: visible; z-index: 10000;';
|
||||||
|
|
||||||
|
function checkSelector(parsed: types.ParsedSelector) {
|
||||||
|
for (const {name} of parsed.parts) {
|
||||||
|
if (!injectedScript.engines.has(name))
|
||||||
|
throw new Error(`Unknown engine "${name}"`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function highlightElements(elements: Element[] = [], target?: Element) {
|
||||||
|
const scrollLeft = document.scrollingElement ? document.scrollingElement.scrollLeft : 0;
|
||||||
|
const scrollTop = document.scrollingElement ? document.scrollingElement.scrollTop : 0;
|
||||||
|
highlightContainer.textContent = '';
|
||||||
|
for (const element of elements) {
|
||||||
|
const rect = element.getBoundingClientRect();
|
||||||
|
const highlight = document.createElement('div');
|
||||||
|
highlight.style.position = 'absolute';
|
||||||
|
highlight.style.left = (rect.left + scrollLeft) + 'px';
|
||||||
|
highlight.style.top = (rect.top + scrollTop) + 'px';
|
||||||
|
highlight.style.height = rect.height + 'px';
|
||||||
|
highlight.style.width = rect.width + 'px';
|
||||||
|
highlight.style.pointerEvents = 'none';
|
||||||
|
if (element === target) {
|
||||||
|
highlight.style.background = 'hsla(30, 97%, 37%, 0.3)';
|
||||||
|
highlight.style.border = '3px solid hsla(30, 97%, 37%, 0.6)';
|
||||||
|
} else {
|
||||||
|
highlight.style.background = 'hsla(120, 100%, 37%, 0.3)';
|
||||||
|
highlight.style.border = '3px solid hsla(120, 100%, 37%, 0.8)';
|
||||||
|
}
|
||||||
|
highlight.style.borderRadius = '3px';
|
||||||
|
highlightContainer.appendChild(highlight);
|
||||||
|
}
|
||||||
|
document.body.appendChild(highlightContainer);
|
||||||
|
}
|
||||||
|
|
||||||
|
function $(selector: string): (Element | undefined) {
|
||||||
|
if (typeof selector !== 'string')
|
||||||
|
throw new Error(`Usage: playwright.query('Playwright >> selector').`);
|
||||||
|
const parsed = parseSelector(selector);
|
||||||
|
checkSelector(parsed);
|
||||||
|
const elements = injectedScript.querySelectorAll(parsed, document);
|
||||||
|
highlightElements(elements, elements[0]);
|
||||||
|
return elements[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
function $$(selector: string): Element[] {
|
||||||
|
if (typeof selector !== 'string')
|
||||||
|
throw new Error(`Usage: playwright.$$('Playwright >> selector').`);
|
||||||
|
const parsed = parseSelector(selector);
|
||||||
|
checkSelector(parsed);
|
||||||
|
const elements = injectedScript.querySelectorAll(parsed, document);
|
||||||
|
highlightElements(elements);
|
||||||
|
return elements;
|
||||||
|
}
|
||||||
|
|
||||||
|
function inspect(selector: string) {
|
||||||
|
if (typeof (window as any).inspect !== 'function')
|
||||||
|
return;
|
||||||
|
if (typeof selector !== 'string')
|
||||||
|
throw new Error(`Usage: playwright.inspect('Playwright >> selector').`);
|
||||||
|
highlightElements();
|
||||||
|
(window as any).inspect($(selector));
|
||||||
|
}
|
||||||
|
|
||||||
|
function clear() {
|
||||||
|
highlightContainer.remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
(window as any).playwright = { $, $$, inspect, clear };
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -157,7 +157,7 @@ export class FFBrowserContext extends BrowserContextBase {
|
||||||
async _initialize() {
|
async _initialize() {
|
||||||
assert(!this._ffPages().length);
|
assert(!this._ffPages().length);
|
||||||
const browserContextId = this._browserContextId || undefined;
|
const browserContextId = this._browserContextId || undefined;
|
||||||
const promises: Promise<any>[] = [];
|
const promises: Promise<any>[] = [ super._initialize() ];
|
||||||
if (this._browser._options.downloadsPath) {
|
if (this._browser._options.downloadsPath) {
|
||||||
promises.push(this._browser._connection.send('Browser.setDownloadOptions', {
|
promises.push(this._browser._connection.send('Browser.setDownloadOptions', {
|
||||||
browserContextId,
|
browserContextId,
|
||||||
|
|
|
||||||
|
|
@ -378,7 +378,7 @@ export default class InjectedScript {
|
||||||
if (!element || !element.isConnected)
|
if (!element || !element.isConnected)
|
||||||
return { status: 'notconnected' };
|
return { status: 'notconnected' };
|
||||||
element = element.closest('button, [role=button]') || element;
|
element = element.closest('button, [role=button]') || element;
|
||||||
let hitElement = this._deepElementFromPoint(document, point.x, point.y);
|
let hitElement = this.deepElementFromPoint(document, point.x, point.y);
|
||||||
while (hitElement && hitElement !== element)
|
while (hitElement && hitElement !== element)
|
||||||
hitElement = this._parentElementOrShadowHost(hitElement);
|
hitElement = this._parentElementOrShadowHost(hitElement);
|
||||||
return { status: 'success', value: hitElement === element };
|
return { status: 'success', value: hitElement === element };
|
||||||
|
|
@ -408,7 +408,7 @@ export default class InjectedScript {
|
||||||
return (element.parentNode as ShadowRoot).host;
|
return (element.parentNode as ShadowRoot).host;
|
||||||
}
|
}
|
||||||
|
|
||||||
private _deepElementFromPoint(document: Document, x: number, y: number): Element | undefined {
|
deepElementFromPoint(document: Document, x: number, y: number): Element | undefined {
|
||||||
let container: Document | ShadowRoot | null = document;
|
let container: Document | ShadowRoot | null = document;
|
||||||
let element: Element | undefined;
|
let element: Element | undefined;
|
||||||
while (container) {
|
while (container) {
|
||||||
|
|
|
||||||
119
src/selectors.ts
119
src/selectors.ts
|
|
@ -160,66 +160,73 @@ export class Selectors {
|
||||||
|
|
||||||
private _parseSelector(selector: string): types.ParsedSelector {
|
private _parseSelector(selector: string): types.ParsedSelector {
|
||||||
assert(helper.isString(selector), `selector must be a string`);
|
assert(helper.isString(selector), `selector must be a string`);
|
||||||
let index = 0;
|
const parsed = parseSelector(selector);
|
||||||
let quote: string | undefined;
|
for (const {name} of parsed.parts) {
|
||||||
let start = 0;
|
|
||||||
const result: types.ParsedSelector = { parts: [] };
|
|
||||||
const append = () => {
|
|
||||||
const part = selector.substring(start, index).trim();
|
|
||||||
const eqIndex = part.indexOf('=');
|
|
||||||
let name: string;
|
|
||||||
let body: string;
|
|
||||||
if (eqIndex !== -1 && part.substring(0, eqIndex).trim().match(/^[a-zA-Z_0-9-+:*]+$/)) {
|
|
||||||
name = part.substring(0, eqIndex).trim();
|
|
||||||
body = part.substring(eqIndex + 1);
|
|
||||||
} else if (part.length > 1 && part[0] === '"' && part[part.length - 1] === '"') {
|
|
||||||
name = 'text';
|
|
||||||
body = part;
|
|
||||||
} else if (part.length > 1 && part[0] === "'" && part[part.length - 1] === "'") {
|
|
||||||
name = 'text';
|
|
||||||
body = part;
|
|
||||||
} else if (/^\(*\/\//.test(part)) {
|
|
||||||
// If selector starts with '//' or '//' prefixed with multiple opening
|
|
||||||
// parenthesis, consider xpath. @see https://github.com/microsoft/playwright/issues/817
|
|
||||||
name = 'xpath';
|
|
||||||
body = part;
|
|
||||||
} else {
|
|
||||||
name = 'css';
|
|
||||||
body = part;
|
|
||||||
}
|
|
||||||
name = name.toLowerCase();
|
|
||||||
let capture = false;
|
|
||||||
if (name[0] === '*') {
|
|
||||||
capture = true;
|
|
||||||
name = name.substring(1);
|
|
||||||
}
|
|
||||||
if (!this._builtinEngines.has(name) && !this._engines.has(name))
|
if (!this._builtinEngines.has(name) && !this._engines.has(name))
|
||||||
throw new Error(`Unknown engine "${name}" while parsing selector ${selector}`);
|
throw new Error(`Unknown engine "${name}" while parsing selector ${selector}`);
|
||||||
result.parts.push({ name, body });
|
|
||||||
if (capture) {
|
|
||||||
if (result.capture !== undefined)
|
|
||||||
throw new Error(`Only one of the selectors can capture using * modifier`);
|
|
||||||
result.capture = result.parts.length - 1;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
while (index < selector.length) {
|
|
||||||
const c = selector[index];
|
|
||||||
if (c === '\\' && index + 1 < selector.length) {
|
|
||||||
index += 2;
|
|
||||||
} else if (c === quote) {
|
|
||||||
quote = undefined;
|
|
||||||
index++;
|
|
||||||
} else if (!quote && c === '>' && selector[index + 1] === '>') {
|
|
||||||
append();
|
|
||||||
index += 2;
|
|
||||||
start = index;
|
|
||||||
} else {
|
|
||||||
index++;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
append();
|
return parsed;
|
||||||
return result;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const selectors = new Selectors();
|
export const selectors = new Selectors();
|
||||||
|
|
||||||
|
export function parseSelector(selector: string): types.ParsedSelector {
|
||||||
|
let index = 0;
|
||||||
|
let quote: string | undefined;
|
||||||
|
let start = 0;
|
||||||
|
const result: types.ParsedSelector = { parts: [] };
|
||||||
|
const append = () => {
|
||||||
|
const part = selector.substring(start, index).trim();
|
||||||
|
const eqIndex = part.indexOf('=');
|
||||||
|
let name: string;
|
||||||
|
let body: string;
|
||||||
|
if (eqIndex !== -1 && part.substring(0, eqIndex).trim().match(/^[a-zA-Z_0-9-+:*]+$/)) {
|
||||||
|
name = part.substring(0, eqIndex).trim();
|
||||||
|
body = part.substring(eqIndex + 1);
|
||||||
|
} else if (part.length > 1 && part[0] === '"' && part[part.length - 1] === '"') {
|
||||||
|
name = 'text';
|
||||||
|
body = part;
|
||||||
|
} else if (part.length > 1 && part[0] === "'" && part[part.length - 1] === "'") {
|
||||||
|
name = 'text';
|
||||||
|
body = part;
|
||||||
|
} else if (/^\(*\/\//.test(part)) {
|
||||||
|
// If selector starts with '//' or '//' prefixed with multiple opening
|
||||||
|
// parenthesis, consider xpath. @see https://github.com/microsoft/playwright/issues/817
|
||||||
|
name = 'xpath';
|
||||||
|
body = part;
|
||||||
|
} else {
|
||||||
|
name = 'css';
|
||||||
|
body = part;
|
||||||
|
}
|
||||||
|
name = name.toLowerCase();
|
||||||
|
let capture = false;
|
||||||
|
if (name[0] === '*') {
|
||||||
|
capture = true;
|
||||||
|
name = name.substring(1);
|
||||||
|
}
|
||||||
|
result.parts.push({ name, body });
|
||||||
|
if (capture) {
|
||||||
|
if (result.capture !== undefined)
|
||||||
|
throw new Error(`Only one of the selectors can capture using * modifier`);
|
||||||
|
result.capture = result.parts.length - 1;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
while (index < selector.length) {
|
||||||
|
const c = selector[index];
|
||||||
|
if (c === '\\' && index + 1 < selector.length) {
|
||||||
|
index += 2;
|
||||||
|
} else if (c === quote) {
|
||||||
|
quote = undefined;
|
||||||
|
index++;
|
||||||
|
} else if (!quote && c === '>' && selector[index + 1] === '>') {
|
||||||
|
append();
|
||||||
|
index += 2;
|
||||||
|
start = index;
|
||||||
|
} else {
|
||||||
|
index++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
append();
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -213,7 +213,7 @@ export class WKBrowserContext extends BrowserContextBase {
|
||||||
async _initialize() {
|
async _initialize() {
|
||||||
assert(!this._wkPages().length);
|
assert(!this._wkPages().length);
|
||||||
const browserContextId = this._browserContextId;
|
const browserContextId = this._browserContextId;
|
||||||
const promises: Promise<any>[] = [];
|
const promises: Promise<any>[] = [ super._initialize() ];
|
||||||
if (this._browser._options.downloadsPath) {
|
if (this._browser._options.downloadsPath) {
|
||||||
promises.push(this._browser._browserSession.send('Playwright.setDownloadBehavior', {
|
promises.push(this._browser._browserSession.send('Playwright.setDownloadBehavior', {
|
||||||
behavior: this._options.acceptDownloads ? 'allow' : 'deny',
|
behavior: this._options.acceptDownloads ? 'allow' : 'deny',
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue