feat(trace-viewer): show remote object previews in console (#8024)
This commit is contained in:
parent
6549bc4d8d
commit
2e63c59157
|
|
@ -95,7 +95,7 @@ export class CRExecutionContext implements js.ExecutionContextDelegate {
|
|||
}
|
||||
|
||||
createHandle(context: js.ExecutionContext, remoteObject: Protocol.Runtime.RemoteObject): js.JSHandle {
|
||||
return new js.JSHandle(context, remoteObject.subtype || remoteObject.type, remoteObject.objectId, potentiallyUnserializableValue(remoteObject));
|
||||
return new js.JSHandle(context, remoteObject.subtype || remoteObject.type, renderPreview(remoteObject), remoteObject.objectId, potentiallyUnserializableValue(remoteObject));
|
||||
}
|
||||
|
||||
async releaseHandle(objectId: js.ObjectId): Promise<void> {
|
||||
|
|
@ -121,3 +121,26 @@ function potentiallyUnserializableValue(remoteObject: Protocol.Runtime.RemoteObj
|
|||
const unserializableValue = remoteObject.unserializableValue;
|
||||
return unserializableValue ? js.parseUnserializableValue(unserializableValue) : value;
|
||||
}
|
||||
|
||||
function renderPreview(object: Protocol.Runtime.RemoteObject): string | undefined {
|
||||
if (object.type === 'undefined')
|
||||
return 'undefined';
|
||||
if ('value' in object)
|
||||
return String(object.value);
|
||||
if (object.unserializableValue)
|
||||
return String(object.unserializableValue);
|
||||
|
||||
if (object.description === 'Object' && object.preview) {
|
||||
const tokens = [];
|
||||
for (const { name, value } of object.preview.properties)
|
||||
tokens.push(`${name}: ${value}`);
|
||||
return `{${tokens.join(', ')}}`;
|
||||
}
|
||||
if (object.subtype === 'array' && object.preview) {
|
||||
const result = [];
|
||||
for (const { name, value } of object.preview.properties)
|
||||
result[+name] = value;
|
||||
return '[' + String(result) + ']';
|
||||
}
|
||||
return object.description;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ export class ConsoleMessage extends SdkObject {
|
|||
|
||||
text(): string {
|
||||
if (this._text === undefined)
|
||||
this._text = this._args.map(arg => arg._value).join(' ');
|
||||
this._text = this._args.map(arg => arg.preview()).join(' ');
|
||||
return this._text;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -101,7 +101,7 @@ export class FrameExecutionContext extends js.ExecutionContext {
|
|||
);
|
||||
})();
|
||||
`;
|
||||
this._injectedScriptPromise = this._delegate.rawEvaluateHandle(source).then(objectId => new js.JSHandle(this, 'object', objectId));
|
||||
this._injectedScriptPromise = this._delegate.rawEvaluateHandle(source).then(objectId => new js.JSHandle(this, 'object', undefined, objectId));
|
||||
}
|
||||
return this._injectedScriptPromise;
|
||||
}
|
||||
|
|
@ -117,7 +117,7 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
|
|||
declare readonly _objectId: string;
|
||||
|
||||
constructor(context: FrameExecutionContext, objectId: string) {
|
||||
super(context, 'node', objectId);
|
||||
super(context, 'node', undefined, objectId);
|
||||
this._page = context.frame._page;
|
||||
this._initializePreview().catch(e => {});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -88,7 +88,7 @@ export class FFExecutionContext implements js.ExecutionContextDelegate {
|
|||
}
|
||||
|
||||
createHandle(context: js.ExecutionContext, remoteObject: Protocol.Runtime.RemoteObject): js.JSHandle {
|
||||
return new js.JSHandle(context, remoteObject.subtype || remoteObject.type || '', remoteObject.objectId, potentiallyUnserializableValue(remoteObject));
|
||||
return new js.JSHandle(context, remoteObject.subtype || remoteObject.type || '', renderPreview(remoteObject), remoteObject.objectId, potentiallyUnserializableValue(remoteObject));
|
||||
}
|
||||
|
||||
async releaseHandle(objectId: js.ObjectId): Promise<void> {
|
||||
|
|
@ -123,3 +123,22 @@ function potentiallyUnserializableValue(remoteObject: Protocol.Runtime.RemoteObj
|
|||
const unserializableValue = remoteObject.unserializableValue;
|
||||
return unserializableValue ? js.parseUnserializableValue(unserializableValue) : value;
|
||||
}
|
||||
|
||||
function renderPreview(object: Protocol.Runtime.RemoteObject): string | undefined {
|
||||
if (object.type === 'undefined')
|
||||
return 'undefined';
|
||||
if (object.unserializableValue)
|
||||
return String(object.unserializableValue);
|
||||
if (object.type === 'symbol')
|
||||
return 'Symbol()';
|
||||
if (object.subtype === 'regexp')
|
||||
return 'RegExp';
|
||||
if (object.subtype === 'weakmap')
|
||||
return 'WeakMap';
|
||||
if (object.subtype === 'weakset')
|
||||
return 'WeakSet';
|
||||
if (object.subtype)
|
||||
return object.subtype[0].toUpperCase() + object.subtype.slice(1);
|
||||
if ('value' in object)
|
||||
return String(object.value);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -76,7 +76,7 @@ export class ExecutionContext extends SdkObject {
|
|||
${utilityScriptSource.source}
|
||||
return new pwExport();
|
||||
})();`;
|
||||
this._utilityScriptPromise = this._delegate.rawEvaluateHandle(source).then(objectId => new JSHandle(this, 'object', objectId));
|
||||
this._utilityScriptPromise = this._delegate.rawEvaluateHandle(source).then(objectId => new JSHandle(this, 'object', undefined, objectId));
|
||||
}
|
||||
return this._utilityScriptPromise;
|
||||
}
|
||||
|
|
@ -90,7 +90,7 @@ export class ExecutionContext extends SdkObject {
|
|||
}
|
||||
|
||||
async doSlowMo() {
|
||||
// overrided in FrameExecutionContext
|
||||
// overridden in FrameExecutionContext
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -103,15 +103,13 @@ export class JSHandle<T = any> extends SdkObject {
|
|||
protected _preview: string;
|
||||
private _previewCallback: ((preview: string) => void) | undefined;
|
||||
|
||||
constructor(context: ExecutionContext, type: string, objectId?: ObjectId, value?: any) {
|
||||
constructor(context: ExecutionContext, type: string, preview: string | undefined, objectId?: ObjectId, value?: any) {
|
||||
super(context, 'handle');
|
||||
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);
|
||||
this._preview = this._objectId ? preview || `JSHandle@${this._objectType}` : String(value);
|
||||
}
|
||||
|
||||
callFunctionNoReply(func: Function, arg: any) {
|
||||
|
|
@ -182,6 +180,10 @@ export class JSHandle<T = any> extends SdkObject {
|
|||
this._previewCallback = callback;
|
||||
}
|
||||
|
||||
preview(): string {
|
||||
return this._preview;
|
||||
}
|
||||
|
||||
_setPreview(preview: string) {
|
||||
this._preview = preview;
|
||||
if (this._previewCallback)
|
||||
|
|
|
|||
|
|
@ -121,7 +121,7 @@ export class WKExecutionContext implements js.ExecutionContextDelegate {
|
|||
|
||||
createHandle(context: js.ExecutionContext, remoteObject: Protocol.Runtime.RemoteObject): js.JSHandle {
|
||||
const isPromise = remoteObject.className === 'Promise';
|
||||
return new js.JSHandle(context, isPromise ? 'promise' : remoteObject.subtype || remoteObject.type, remoteObject.objectId, potentiallyUnserializableValue(remoteObject));
|
||||
return new js.JSHandle(context, isPromise ? 'promise' : remoteObject.subtype || remoteObject.type, renderPreview(remoteObject), remoteObject.objectId, potentiallyUnserializableValue(remoteObject));
|
||||
}
|
||||
|
||||
async releaseHandle(objectId: js.ObjectId): Promise<void> {
|
||||
|
|
@ -147,3 +147,24 @@ function rewriteError(error: Error): Error {
|
|||
return new Error('Execution context was destroyed, most likely because of a navigation.');
|
||||
return error;
|
||||
}
|
||||
|
||||
function renderPreview(object: Protocol.Runtime.RemoteObject): string | undefined {
|
||||
if (object.type === 'undefined')
|
||||
return 'undefined';
|
||||
if ('value' in object)
|
||||
return String(object.value);
|
||||
|
||||
if (object.description === 'Object' && object.preview) {
|
||||
const tokens = [];
|
||||
for (const { name, value } of object.preview.properties!)
|
||||
tokens.push(`${name}: ${value}`);
|
||||
return `{${tokens.join(', ')}}`;
|
||||
}
|
||||
if (object.subtype === 'array' && object.preview) {
|
||||
const result = [];
|
||||
for (const { name, value } of object.preview.properties!)
|
||||
result[+name] = value;
|
||||
return '[' + String(result) + ']';
|
||||
}
|
||||
return object.description;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,41 +19,47 @@ import { test as it, expect } from './pageTest';
|
|||
|
||||
it('should work for primitives', async ({page}) => {
|
||||
const numberHandle = await page.evaluateHandle(() => 2);
|
||||
expect(numberHandle.toString()).toBe('JSHandle@2');
|
||||
expect(numberHandle.toString()).toBe('2');
|
||||
const stringHandle = await page.evaluateHandle(() => 'a');
|
||||
expect(stringHandle.toString()).toBe('JSHandle@a');
|
||||
expect(stringHandle.toString()).toBe('a');
|
||||
});
|
||||
|
||||
it('should work for complicated objects', async ({page}) => {
|
||||
it('should work for complicated objects', async ({ page, browserName }) => {
|
||||
const aHandle = await page.evaluateHandle(() => window);
|
||||
expect(aHandle.toString()).toBe('JSHandle@object');
|
||||
if (browserName !== 'firefox')
|
||||
expect(aHandle.toString()).toBe('Window');
|
||||
else
|
||||
expect(aHandle.toString()).toBe('JSHandle@object');
|
||||
});
|
||||
|
||||
it('should work for promises', async ({page}) => {
|
||||
it('should work for promises', async ({ page }) => {
|
||||
// wrap the promise in an object, otherwise we will await.
|
||||
const wrapperHandle = await page.evaluateHandle(() => ({b: Promise.resolve(123)}));
|
||||
const bHandle = await wrapperHandle.getProperty('b');
|
||||
expect(bHandle.toString()).toBe('JSHandle@promise');
|
||||
expect(bHandle.toString()).toBe('Promise');
|
||||
});
|
||||
|
||||
it('should work with different subtypes', async ({page, browserName}) => {
|
||||
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('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('/foo/')).toString()).toBe('JSHandle@regexp');
|
||||
it('should work with different subtypes', async ({ page, browserName }) => {
|
||||
expect((await page.evaluateHandle('(function(){})')).toString()).toContain('function');
|
||||
expect((await page.evaluateHandle('12')).toString()).toBe('12');
|
||||
expect((await page.evaluateHandle('true')).toString()).toBe('true');
|
||||
expect((await page.evaluateHandle('undefined')).toString()).toBe('undefined');
|
||||
expect((await page.evaluateHandle('"foo"')).toString()).toBe('foo');
|
||||
expect((await page.evaluateHandle('Symbol()')).toString()).toBe('Symbol()');
|
||||
expect((await page.evaluateHandle('new Map()')).toString()).toContain('Map');
|
||||
expect((await page.evaluateHandle('new Set()')).toString()).toContain('Set');
|
||||
expect((await page.evaluateHandle('[]')).toString()).toContain('Array');
|
||||
expect((await page.evaluateHandle('null')).toString()).toBe('null');
|
||||
expect((await page.evaluateHandle('document.body')).toString()).toBe('JSHandle@node');
|
||||
expect((await page.evaluateHandle('new Date()')).toString()).toBe('JSHandle@date');
|
||||
expect((await page.evaluateHandle('new WeakMap()')).toString()).toBe('JSHandle@weakmap');
|
||||
expect((await page.evaluateHandle('new WeakSet()')).toString()).toBe('JSHandle@weakset');
|
||||
expect((await page.evaluateHandle('new Error()')).toString()).toBe('JSHandle@error');
|
||||
// TODO(yurys): change subtype from array to typedarray in WebKit.
|
||||
expect((await page.evaluateHandle('new Int32Array()')).toString()).toBe(browserName === 'webkit' ? 'JSHandle@array' : 'JSHandle@typedarray');
|
||||
expect((await page.evaluateHandle('new Proxy({}, {})')).toString()).toBe('JSHandle@proxy');
|
||||
expect((await page.evaluateHandle('new WeakMap()')).toString()).toBe('WeakMap');
|
||||
expect((await page.evaluateHandle('new WeakSet()')).toString()).toBe('WeakSet');
|
||||
expect((await page.evaluateHandle('new Error()')).toString()).toContain('Error');
|
||||
expect((await page.evaluateHandle('new Proxy({}, {})')).toString()).toBe('Proxy');
|
||||
});
|
||||
|
||||
it('should work with previewable subtypes', async ({ page, browserName }) => {
|
||||
it.skip(browserName === 'firefox');
|
||||
expect((await page.evaluateHandle('/foo/')).toString()).toBe('/foo/');
|
||||
expect((await page.evaluateHandle('new Date(0)')).toString()).toContain('GMT');
|
||||
expect((await page.evaluateHandle('new Int32Array()')).toString()).toContain('Int32Array');
|
||||
});
|
||||
|
|
|
|||
|
|
@ -88,50 +88,6 @@ it('textContent should work', async ({ page, server }) => {
|
|||
expect(await page.textContent('#inner')).toBe('Text,\nmore text');
|
||||
});
|
||||
|
||||
it('textContent should be atomic', async ({ playwright, page }) => {
|
||||
const createDummySelector = () => ({
|
||||
query(root, selector) {
|
||||
const result = root.querySelector(selector);
|
||||
if (result)
|
||||
Promise.resolve().then(() => result.textContent = 'modified');
|
||||
return result;
|
||||
},
|
||||
queryAll(root: HTMLElement, selector: string) {
|
||||
const result = Array.from(root.querySelectorAll(selector));
|
||||
for (const e of result)
|
||||
Promise.resolve().then(() => e.textContent = 'modified');
|
||||
return result;
|
||||
}
|
||||
});
|
||||
await playwright.selectors.register('textContentFromLocators', createDummySelector);
|
||||
await page.setContent(`<div>Hello</div>`);
|
||||
const tc = await page.textContent('textContentFromLocators=div');
|
||||
expect(tc).toBe('Hello');
|
||||
expect(await page.evaluate(() => document.querySelector('div').textContent)).toBe('modified');
|
||||
});
|
||||
|
||||
it('innerText should be atomic', async ({ playwright, page }) => {
|
||||
const createDummySelector = () => ({
|
||||
query(root: HTMLElement, selector: string) {
|
||||
const result = root.querySelector(selector);
|
||||
if (result)
|
||||
Promise.resolve().then(() => result.textContent = 'modified');
|
||||
return result;
|
||||
},
|
||||
queryAll(root: HTMLElement, selector: string) {
|
||||
const result = Array.from(root.querySelectorAll(selector));
|
||||
for (const e of result)
|
||||
Promise.resolve().then(() => e.textContent = 'modified');
|
||||
return result;
|
||||
}
|
||||
});
|
||||
await playwright.selectors.register('innerTextFromLocators', createDummySelector);
|
||||
await page.setContent(`<div>Hello</div>`);
|
||||
const tc = await page.innerText('innerTextFromLocators=div');
|
||||
expect(tc).toBe('Hello');
|
||||
expect(await page.evaluate(() => document.querySelector('div').innerText)).toBe('modified');
|
||||
});
|
||||
|
||||
it('isVisible and isHidden should work', async ({ page }) => {
|
||||
await page.setContent(`<div>Hi</div><span></span>`);
|
||||
|
||||
|
|
|
|||
|
|
@ -18,14 +18,17 @@
|
|||
import { test as it, expect } from './pageTest';
|
||||
import util from 'util';
|
||||
|
||||
it('should work', async ({page}) => {
|
||||
it('should work', async ({page, browserName}) => {
|
||||
let message = null;
|
||||
page.once('console', m => message = m);
|
||||
await Promise.all([
|
||||
page.evaluate(() => console.log('hello', 5, {foo: 'bar'})),
|
||||
page.waitForEvent('console')
|
||||
]);
|
||||
expect(message.text()).toEqual('hello 5 JSHandle@object');
|
||||
if (browserName !== 'firefox')
|
||||
expect(message.text()).toEqual('hello 5 {foo: bar}');
|
||||
else
|
||||
expect(message.text()).toEqual('hello 5 JSHandle@object');
|
||||
expect(message.type()).toEqual('log');
|
||||
expect(await message.args()[0].jsonValue()).toEqual('hello');
|
||||
expect(await message.args()[1].jsonValue()).toEqual(5);
|
||||
|
|
@ -72,18 +75,21 @@ it('should work for different console API calls', async ({page}) => {
|
|||
'calling console.dir',
|
||||
'calling console.warn',
|
||||
'calling console.error',
|
||||
'JSHandle@promise',
|
||||
'Promise',
|
||||
]);
|
||||
});
|
||||
|
||||
it('should not fail for window object', async ({page}) => {
|
||||
it('should not fail for window object', async ({ page, browserName }) => {
|
||||
let message = null;
|
||||
page.once('console', msg => message = msg);
|
||||
await Promise.all([
|
||||
page.evaluate(() => console.error(window)),
|
||||
page.waitForEvent('console')
|
||||
]);
|
||||
expect(message.text()).toBe('JSHandle@object');
|
||||
if (browserName !== 'firefox')
|
||||
expect(message.text()).toEqual('Window');
|
||||
else
|
||||
expect(message.text()).toEqual('JSHandle@object');
|
||||
});
|
||||
|
||||
it('should trigger correct Log', async ({page, server}) => {
|
||||
|
|
@ -135,3 +141,30 @@ it('should not throw when there are console messages in detached iframes', async
|
|||
// 4. Connect to the popup and make sure it doesn't throw.
|
||||
expect(await popup.evaluate('1 + 1')).toBe(2);
|
||||
});
|
||||
|
||||
it('should use object previews for arrays and objects', async ({page, browserName}) => {
|
||||
let text: string;
|
||||
page.on('console', message => {
|
||||
text = message.text();
|
||||
});
|
||||
await page.evaluate(() => console.log([1, 2, 3], {a: 1}, window));
|
||||
|
||||
if (browserName !== 'firefox')
|
||||
expect(text).toEqual('[1,2,3] {a: 1} Window');
|
||||
else
|
||||
expect(text).toEqual('Array JSHandle@object JSHandle@object');
|
||||
});
|
||||
|
||||
it('should use object previews for errors', async ({page, browserName}) => {
|
||||
let text: string;
|
||||
page.on('console', message => {
|
||||
text = message.text();
|
||||
});
|
||||
await page.evaluate(() => console.log(new Error('Exception')));
|
||||
if (browserName === 'chromium')
|
||||
expect(text).toContain('.evaluate');
|
||||
if (browserName === 'webkit')
|
||||
expect(text).toEqual('Error: Exception');
|
||||
if (browserName === 'firefox')
|
||||
expect(text).toEqual('Error');
|
||||
});
|
||||
|
|
|
|||
|
|
@ -67,11 +67,14 @@ it('should not report console logs from workers twice', async function({page}) {
|
|||
expect(page.url()).not.toContain('blob');
|
||||
});
|
||||
|
||||
it('should have JSHandles for console logs', async function({page}) {
|
||||
it('should have JSHandles for console logs', async function({ page, browserName }) {
|
||||
const logPromise = new Promise<ConsoleMessage>(x => page.on('console', x));
|
||||
await page.evaluate(() => new Worker(URL.createObjectURL(new Blob(['console.log(1,2,3,this)'], {type: 'application/javascript'}))));
|
||||
const log = await logPromise;
|
||||
expect(log.text()).toBe('1 2 3 JSHandle@object');
|
||||
if (browserName !== 'firefox')
|
||||
expect(log.text()).toBe('1 2 3 DedicatedWorkerGlobalScope');
|
||||
else
|
||||
expect(log.text()).toBe('1 2 3 JSHandle@object');
|
||||
expect(log.args().length).toBe(4);
|
||||
expect(await (await log.args()[3].getProperty('origin')).jsonValue()).toBe('null');
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in a new issue