diff --git a/src/console.ts b/src/console.ts index 528146a75d..71d6304d77 100644 --- a/src/console.ts +++ b/src/console.ts @@ -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; } diff --git a/src/dom.ts b/src/dom.ts index 411b63bb6a..3aa057743c 100644 --- a/src/dom.ts +++ b/src/dom.ts @@ -121,6 +121,12 @@ export class ElementHandle extends js.JSHandle { 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 | null { diff --git a/src/injected/injectedScript.ts b/src/injected/injectedScript.ts index 38f449abb7..6ba3f5aa40 100644 --- a/src/injected/injectedScript.ts +++ b/src/injected/injectedScript.ts @@ -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}`; + return oneLine(`<${element.nodeName.toLowerCase()}${attrText}>${text}`); } } 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([ ['auxclick', 'mouse'], diff --git a/src/javascript.ts b/src/javascript.ts index 6faa030064..01da04c7c6 100644 --- a/src/javascript.ts +++ b/src/javascript.ts @@ -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 { + utilityScript(): Promise> { 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 { 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(pageFunction: types.FuncOn, arg: Arg): Promise; @@ -121,14 +126,8 @@ export class JSHandle { 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; } } diff --git a/src/selectors.ts b/src/selectors.ts index bcd8433e05..c7cce63f78 100644 --- a/src/selectors.ts +++ b/src/selectors.ts @@ -24,7 +24,6 @@ import { ParsedSelector, parseSelector } from './common/selectorParser'; export class Selectors { readonly _builtinEngines: Set; readonly _engines: Map; - _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) { diff --git a/src/webkit/wkExecutionContext.ts b/src/webkit/wkExecutionContext.ts index 1fa038abb3..10c6b018c4 100644 --- a/src/webkit/wkExecutionContext.ts +++ b/src/webkit/wkExecutionContext.ts @@ -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 { + private async _returnObjectByValue(utilityScript: js.JSHandle, objectId: Protocol.Runtime.RemoteObjectId): Promise { // 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!, diff --git a/test/assets/dom.html b/test/assets/dom.html index 775683cc5d..957b74e0cf 100644 --- a/test/assets/dom.html +++ b/test/assets/dom.html @@ -1,2 +1,2 @@
Text, -more text
+more text diff --git a/test/elementhandle.spec.js b/test/elementhandle.spec.js index 4cbc278182..2598a3e3ab 100644 --- a/test/elementhandle.spec.js +++ b/test/elementhandle.spec.js @@ -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@
'); + expect(String(inner)).toBe('JSHandle@
Text,↵more text
'); + expect(String(text)).toBe('JSHandle@#text=Text,↵more text'); + expect(String(check)).toBe('JSHandle@'); + }); it('getAttribute should work', async({page, server}) => { await page.goto(`${server.PREFIX}/dom.html`); const handle = await page.$('#outer'); diff --git a/test/jshandle.spec.js b/test/jshandle.spec.js index 4c0e3a2aff..521438d4ad 100644 --- a/test/jshandle.spec.js +++ b/test/jshandle.spec.js @@ -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'); diff --git a/test/queryselector.spec.js b/test/queryselector.spec.js index 4bce9060ec..f73dedddf2 100644 --- a/test/queryselector.spec.js +++ b/test/queryselector.spec.js @@ -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('
'); - 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');