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 { Log, InnerLogger, Logger, RootLogger } from './logger';
|
||||
import { FunctionWithSource } from './frames';
|
||||
import * as debugSupport from './debug/debugSupport';
|
||||
|
||||
export type PersistentContextOptions = {
|
||||
viewport?: types.Size | null,
|
||||
|
|
@ -95,6 +96,10 @@ export abstract class BrowserContextBase extends ExtendedEventEmitter implements
|
|||
this._closePromise = new Promise(fulfill => this._closePromiseFulfill = fulfill);
|
||||
}
|
||||
|
||||
async _initialize() {
|
||||
await debugSupport.installConsoleHelpers(this);
|
||||
}
|
||||
|
||||
protected _abortPromiseForEvent(event: string) {
|
||||
return event === Events.BrowserContext.Close ? super._abortPromiseForEvent(event) : this._closePromise;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -291,7 +291,7 @@ export class CRBrowserContext extends BrowserContextBase {
|
|||
|
||||
async _initialize() {
|
||||
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) {
|
||||
promises.push(this._browser._session.send('Browser.setDownloadBehavior', {
|
||||
behavior: this._options.acceptDownloads ? 'allowAndName' : 'deny',
|
||||
|
|
|
|||
|
|
@ -16,6 +16,13 @@
|
|||
|
||||
import * as sourceMap from './sourceMap';
|
||||
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;
|
||||
export function isDebugMode(): boolean {
|
||||
|
|
@ -45,3 +52,96 @@ export async function generateSourceMapUrl(functionText: string, generatedText:
|
|||
const sourceMapUrl = await sourceMap.generateSourceMapUrl(functionText, generatedText);
|
||||
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() {
|
||||
assert(!this._ffPages().length);
|
||||
const browserContextId = this._browserContextId || undefined;
|
||||
const promises: Promise<any>[] = [];
|
||||
const promises: Promise<any>[] = [ super._initialize() ];
|
||||
if (this._browser._options.downloadsPath) {
|
||||
promises.push(this._browser._connection.send('Browser.setDownloadOptions', {
|
||||
browserContextId,
|
||||
|
|
|
|||
|
|
@ -378,7 +378,7 @@ export default class InjectedScript {
|
|||
if (!element || !element.isConnected)
|
||||
return { status: 'notconnected' };
|
||||
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)
|
||||
hitElement = this._parentElementOrShadowHost(hitElement);
|
||||
return { status: 'success', value: hitElement === element };
|
||||
|
|
@ -408,7 +408,7 @@ export default class InjectedScript {
|
|||
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 element: Element | undefined;
|
||||
while (container) {
|
||||
|
|
|
|||
119
src/selectors.ts
119
src/selectors.ts
|
|
@ -160,66 +160,73 @@ export class Selectors {
|
|||
|
||||
private _parseSelector(selector: string): types.ParsedSelector {
|
||||
assert(helper.isString(selector), `selector must be a string`);
|
||||
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);
|
||||
}
|
||||
const parsed = parseSelector(selector);
|
||||
for (const {name} of parsed.parts) {
|
||||
if (!this._builtinEngines.has(name) && !this._engines.has(name))
|
||||
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 result;
|
||||
return parsed;
|
||||
}
|
||||
}
|
||||
|
||||
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() {
|
||||
assert(!this._wkPages().length);
|
||||
const browserContextId = this._browserContextId;
|
||||
const promises: Promise<any>[] = [];
|
||||
const promises: Promise<any>[] = [ super._initialize() ];
|
||||
if (this._browser._options.downloadsPath) {
|
||||
promises.push(this._browser._browserSession.send('Playwright.setDownloadBehavior', {
|
||||
behavior: this._options.acceptDownloads ? 'allow' : 'deny',
|
||||
|
|
|
|||
Loading…
Reference in a new issue