mirror of
https://github.com/microsoft/playwright.git
synced 2024-10-27 13:50:25 +03:00
feat(debug): generate preview for ElementHandle (#2549)
This is a best-effort debugging feature - we try to generate preview asynchronously, so it might not be available immediately.
This commit is contained in:
parent
defeeb9cee
commit
c4e8720eb5
@ -42,7 +42,7 @@ export class ConsoleMessage {
|
||||
|
||||
text(): string {
|
||||
if (this._text === undefined)
|
||||
this._text = this._args.map(arg => arg._handleToString(false /* includeType */)).join(' ');
|
||||
this._text = this._args.map(arg => arg._value).join(' ');
|
||||
return this._text;
|
||||
}
|
||||
|
||||
|
@ -121,6 +121,12 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
|
||||
this._objectId = objectId;
|
||||
this._context = context;
|
||||
this._page = context.frame._page;
|
||||
this._initializePreview().catch(e => {});
|
||||
}
|
||||
|
||||
async _initializePreview() {
|
||||
const utility = await this._context.injectedScript();
|
||||
this._preview = await utility.evaluate((injected, e) => 'JSHandle@' + injected.previewNode(e), this);
|
||||
}
|
||||
|
||||
asElement(): ElementHandle<T> | null {
|
||||
|
@ -487,18 +487,29 @@ export default class InjectedScript {
|
||||
return element;
|
||||
}
|
||||
|
||||
previewElement(element: Element): string {
|
||||
previewNode(node: Node): string {
|
||||
if (node.nodeType === Node.TEXT_NODE)
|
||||
return oneLine(`#text=${node.nodeValue || ''}`);
|
||||
if (node.nodeType !== Node.ELEMENT_NODE)
|
||||
return oneLine(`<${node.nodeName.toLowerCase()} />`);
|
||||
const element = node as Element;
|
||||
|
||||
const attrs = [];
|
||||
for (let i = 0; i < element.attributes.length; i++) {
|
||||
if (element.attributes[i].name !== 'style')
|
||||
attrs.push(` ${element.attributes[i].name}="${element.attributes[i].value}"`);
|
||||
const { name, value } = element.attributes[i];
|
||||
if (name === 'style')
|
||||
continue;
|
||||
if (!value && booleanAttributes.has(name))
|
||||
attrs.push(` ${name}`);
|
||||
else
|
||||
attrs.push(` ${name}="${value}"`);
|
||||
}
|
||||
attrs.sort((a, b) => a.length - b.length);
|
||||
let attrText = attrs.join('');
|
||||
if (attrText.length > 50)
|
||||
attrText = attrText.substring(0, 49) + '\u2026';
|
||||
if (autoClosingTags.has(element.nodeName))
|
||||
return `<${element.nodeName.toLowerCase()}${attrText}/>`;
|
||||
return oneLine(`<${element.nodeName.toLowerCase()}${attrText}/>`);
|
||||
|
||||
const children = element.childNodes;
|
||||
let onlyText = false;
|
||||
@ -507,14 +518,19 @@ export default class InjectedScript {
|
||||
for (let i = 0; i < children.length; i++)
|
||||
onlyText = onlyText && children[i].nodeType === Node.TEXT_NODE;
|
||||
}
|
||||
let text = onlyText ? (element.textContent || '') : '';
|
||||
let text = onlyText ? (element.textContent || '') : (children.length ? '\u2026' : '');
|
||||
if (text.length > 50)
|
||||
text = text.substring(0, 49) + '\u2026';
|
||||
return `<${element.nodeName.toLowerCase()}${attrText}>${text}</${element.nodeName.toLowerCase()}>`;
|
||||
return oneLine(`<${element.nodeName.toLowerCase()}${attrText}>${text}</${element.nodeName.toLowerCase()}>`);
|
||||
}
|
||||
}
|
||||
|
||||
const autoClosingTags = new Set(['AREA', 'BASE', 'BR', 'COL', 'COMMAND', 'EMBED', 'HR', 'IMG', 'INPUT', 'KEYGEN', 'LINK', 'MENUITEM', 'META', 'PARAM', 'SOURCE', 'TRACK', 'WBR']);
|
||||
const booleanAttributes = new Set(['checked', 'selected', 'disabled', 'readonly', 'multiple']);
|
||||
|
||||
function oneLine(s: string): string {
|
||||
return s.replace(/\n/g, '↵').replace(/\t/g, '⇆');
|
||||
}
|
||||
|
||||
const eventType = new Map<string, 'mouse'|'keyboard'|'touch'|'pointer'|'focus'|'drag'>([
|
||||
['auxclick', 'mouse'],
|
||||
|
@ -20,6 +20,7 @@ import * as utilityScriptSource from './generated/utilityScriptSource';
|
||||
import * as sourceMap from './utils/sourceMap';
|
||||
import { serializeAsCallArgument } from './common/utilityScriptSerializers';
|
||||
import { helper } from './helper';
|
||||
import UtilityScript from './injected/utilityScript';
|
||||
|
||||
type ObjectId = string;
|
||||
export type RemoteObject = {
|
||||
@ -47,7 +48,7 @@ export class ExecutionContext {
|
||||
return null;
|
||||
}
|
||||
|
||||
utilityScript(): Promise<JSHandle> {
|
||||
utilityScript(): Promise<JSHandle<UtilityScript>> {
|
||||
if (!this._utilityScriptPromise) {
|
||||
const source = `new (${utilityScriptSource.source})()`;
|
||||
this._utilityScriptPromise = this._delegate.rawEvaluate(source).then(objectId => new JSHandle(this, 'object', objectId));
|
||||
@ -66,12 +67,16 @@ export class JSHandle<T = any> {
|
||||
readonly _objectId: ObjectId | undefined;
|
||||
readonly _value: any;
|
||||
private _objectType: string;
|
||||
protected _preview: string;
|
||||
|
||||
constructor(context: ExecutionContext, type: string, objectId?: ObjectId, value?: any) {
|
||||
this._context = context;
|
||||
this._objectId = objectId;
|
||||
this._value = value;
|
||||
this._objectType = type;
|
||||
if (this._objectId)
|
||||
this._value = 'JSHandle@' + this._objectType;
|
||||
this._preview = 'JSHandle@' + String(this._objectId ? this._objectType : this._value);
|
||||
}
|
||||
|
||||
async evaluate<R, Arg>(pageFunction: types.FuncOn<T, Arg, R>, arg: Arg): Promise<R>;
|
||||
@ -121,14 +126,8 @@ export class JSHandle<T = any> {
|
||||
await this._context._delegate.releaseHandle(this);
|
||||
}
|
||||
|
||||
_handleToString(includeType: boolean): string {
|
||||
if (this._objectId)
|
||||
return 'JSHandle@' + this._objectType;
|
||||
return (includeType ? 'JSHandle:' : '') + this._value;
|
||||
}
|
||||
|
||||
toString(): string {
|
||||
return this._handleToString(true);
|
||||
return this._preview;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -24,7 +24,6 @@ import { ParsedSelector, parseSelector } from './common/selectorParser';
|
||||
export class Selectors {
|
||||
readonly _builtinEngines: Set<string>;
|
||||
readonly _engines: Map<string, { source: string, contentScript: boolean }>;
|
||||
_generation = 0;
|
||||
|
||||
constructor() {
|
||||
// Note: keep in sync with SelectorEvaluator class.
|
||||
@ -51,7 +50,6 @@ export class Selectors {
|
||||
if (this._engines.has(name))
|
||||
throw new Error(`"${name}" selector engine has been already registered`);
|
||||
this._engines.set(name, { source, contentScript });
|
||||
++this._generation;
|
||||
}
|
||||
|
||||
private _needsMainContext(parsed: ParsedSelector): boolean {
|
||||
@ -128,7 +126,7 @@ export class Selectors {
|
||||
if (!element)
|
||||
progress.log(` selector did not resolve to any element`);
|
||||
else
|
||||
progress.log(` selector resolved to ${visible ? 'visible' : 'hidden'} ${injected.previewElement(element)}`);
|
||||
progress.log(` selector resolved to ${visible ? 'visible' : 'hidden'} ${injected.previewNode(element)}`);
|
||||
}
|
||||
|
||||
switch (state) {
|
||||
|
@ -81,17 +81,16 @@ export class WKExecutionContext implements js.ExecutionContextDelegate {
|
||||
if (!returnByValue)
|
||||
return utilityScript._context.createHandle(response.result);
|
||||
if (response.result.objectId)
|
||||
return await this._returnObjectByValue(utilityScript._context, response.result.objectId);
|
||||
return await this._returnObjectByValue(utilityScript, response.result.objectId);
|
||||
return parseEvaluationResultValue(response.result.value);
|
||||
} catch (error) {
|
||||
throw rewriteError(error);
|
||||
}
|
||||
}
|
||||
|
||||
private async _returnObjectByValue(context: js.ExecutionContext, objectId: Protocol.Runtime.RemoteObjectId): Promise<any> {
|
||||
private async _returnObjectByValue(utilityScript: js.JSHandle<any>, objectId: Protocol.Runtime.RemoteObjectId): Promise<any> {
|
||||
// This is different from handleJSONValue in that it does not throw.
|
||||
try {
|
||||
const utilityScript = await context.utilityScript();
|
||||
const serializeResponse = await this._session.send('Runtime.callFunctionOn', {
|
||||
functionDeclaration: 'object => object' + sourceMap.generateSourceUrl(),
|
||||
objectId: utilityScript._objectId!,
|
||||
|
@ -1,2 +1,2 @@
|
||||
<div id="outer" name="value"><div id="inner">Text,
|
||||
more text</div></div>
|
||||
more text</div></div><input id="check" type=checkbox checked foo="bar"">
|
||||
|
@ -376,6 +376,18 @@ describe('ElementHandle.selectText', function() {
|
||||
|
||||
|
||||
describe('ElementHandle convenience API', function() {
|
||||
it('should have a nice preview', async({page, server}) => {
|
||||
await page.goto(`${server.PREFIX}/dom.html`);
|
||||
const outer = await page.$('#outer');
|
||||
const inner = await page.$('#inner');
|
||||
const check = await page.$('#check');
|
||||
const text = await inner.evaluateHandle(e => e.firstChild);
|
||||
await page.evaluate(() => 1); // Give them a chance to calculate the preview.
|
||||
expect(String(outer)).toBe('JSHandle@<div id="outer" name="value">…</div>');
|
||||
expect(String(inner)).toBe('JSHandle@<div id="inner">Text,↵more text</div>');
|
||||
expect(String(text)).toBe('JSHandle@#text=Text,↵more text');
|
||||
expect(String(check)).toBe('JSHandle@<input checked id="check" foo="bar"" type="checkbox"/>');
|
||||
});
|
||||
it('getAttribute should work', async({page, server}) => {
|
||||
await page.goto(`${server.PREFIX}/dom.html`);
|
||||
const handle = await page.$('#outer');
|
||||
|
@ -236,9 +236,9 @@ describe('JSHandle.asElement', function() {
|
||||
describe('JSHandle.toString', function() {
|
||||
it('should work for primitives', async({page, server}) => {
|
||||
const numberHandle = await page.evaluateHandle(() => 2);
|
||||
expect(numberHandle.toString()).toBe('JSHandle:2');
|
||||
expect(numberHandle.toString()).toBe('JSHandle@2');
|
||||
const stringHandle = await page.evaluateHandle(() => 'a');
|
||||
expect(stringHandle.toString()).toBe('JSHandle:a');
|
||||
expect(stringHandle.toString()).toBe('JSHandle@a');
|
||||
});
|
||||
it('should work for complicated objects', async({page, server}) => {
|
||||
const aHandle = await page.evaluateHandle(() => window);
|
||||
@ -252,15 +252,15 @@ describe('JSHandle.toString', function() {
|
||||
});
|
||||
it('should work with different subtypes', async({page, server}) => {
|
||||
expect((await page.evaluateHandle('(function(){})')).toString()).toBe('JSHandle@function');
|
||||
expect((await page.evaluateHandle('12')).toString()).toBe('JSHandle:12');
|
||||
expect((await page.evaluateHandle('true')).toString()).toBe('JSHandle:true');
|
||||
expect((await page.evaluateHandle('undefined')).toString()).toBe('JSHandle:undefined');
|
||||
expect((await page.evaluateHandle('"foo"')).toString()).toBe('JSHandle:foo');
|
||||
expect((await page.evaluateHandle('12')).toString()).toBe('JSHandle@12');
|
||||
expect((await page.evaluateHandle('true')).toString()).toBe('JSHandle@true');
|
||||
expect((await page.evaluateHandle('undefined')).toString()).toBe('JSHandle@undefined');
|
||||
expect((await page.evaluateHandle('"foo"')).toString()).toBe('JSHandle@foo');
|
||||
expect((await page.evaluateHandle('Symbol()')).toString()).toBe('JSHandle@symbol');
|
||||
expect((await page.evaluateHandle('new Map()')).toString()).toBe('JSHandle@map');
|
||||
expect((await page.evaluateHandle('new Set()')).toString()).toBe('JSHandle@set');
|
||||
expect((await page.evaluateHandle('[]')).toString()).toBe('JSHandle@array');
|
||||
expect((await page.evaluateHandle('null')).toString()).toBe('JSHandle:null');
|
||||
expect((await page.evaluateHandle('null')).toString()).toBe('JSHandle@null');
|
||||
expect((await page.evaluateHandle('/foo/')).toString()).toBe('JSHandle@regexp');
|
||||
expect((await page.evaluateHandle('document.body')).toString()).toBe('JSHandle@node');
|
||||
expect((await page.evaluateHandle('new Date()')).toString()).toBe('JSHandle@date');
|
||||
|
@ -789,10 +789,7 @@ describe('selectors.register', () => {
|
||||
// Can be chained to css.
|
||||
expect(await page.$eval('main=ignored >> css=section', e => e.nodeName)).toBe('SECTION');
|
||||
});
|
||||
it('should update', async ({page}) => {
|
||||
await page.setContent('<div><dummy id=d1></dummy></div><span><dummy id=d2></dummy></span>');
|
||||
expect(await page.$eval('div', e => e.nodeName)).toBe('DIV');
|
||||
|
||||
it('should handle errors', async ({page}) => {
|
||||
let error = await page.$('neverregister=ignored').catch(e => e);
|
||||
expect(error.message).toBe('Unknown engine "neverregister" while parsing selector neverregister=ignored');
|
||||
|
||||
@ -812,8 +809,6 @@ describe('selectors.register', () => {
|
||||
expect(error.message).toBe('Selector engine name may only contain [a-zA-Z0-9_] characters');
|
||||
|
||||
await registerEngine('dummy', createDummySelector);
|
||||
expect(await page.$eval('dummy=ignored', e => e.id)).toBe('d1');
|
||||
expect(await page.$eval('css=span >> dummy=ignored', e => e.id)).toBe('d2');
|
||||
|
||||
error = await playwright.selectors.register('dummy', createDummySelector).catch(e => e);
|
||||
expect(error.message).toBe('"dummy" selector engine has been already registered');
|
||||
|
Loading…
Reference in New Issue
Block a user