feat(JSHandles): introduce JSHandles (#943)

This patch:
- introduces ExecutionContext class that incapsulates javascript
  execution context. An examples of execution contexts are workers and
  frames
- introduces JSHandle that holds a references to the javascript
  object in ExecutionContext
- inherits ElementHandle from JSHandle

Fixes #382.
This commit is contained in:
Andrey Lushnikov
2017-10-06 15:35:02 -07:00
committed by GitHub
parent 59bcc2ee56
commit 0d0f9b7984
8 changed files with 654 additions and 112 deletions

View File

@@ -265,9 +265,10 @@ describe('Page', function() {
const result = await page.evaluate((a, b) => Object.is(a, undefined) && Object.is(b, 'foo'), undefined, 'foo');
expect(result).toBe(true);
}));
it('should not fail for window object', SX(async function() {
const result = await page.evaluate(() => window);
expect(result).toBe('Window');
it('should fail for window object', SX(async function() {
let error = null;
await page.evaluate(() => window).catch(e => error = e);
expect(error.message).toContain('Converting circular structure to JSON');
}));
it('should accept a string', SX(async function() {
const result = await page.evaluate('1 + 2');
@@ -294,7 +295,7 @@ describe('Page', function() {
await element.dispose();
let error = null;
await page.evaluate(e => e.textContent, element).catch(e => error = e);
expect(error.message).toContain('ElementHandle is disposed');
expect(error.message).toContain('JSHandle is disposed');
}));
it('should throw if elementHandles are from other frames', SX(async function() {
const FrameUtils = require('./frame-utils');
@@ -303,7 +304,117 @@ describe('Page', function() {
let error = null;
await page.evaluate(body => body.innerHTML, bodyHandle).catch(e => error = e);
expect(error).toBeTruthy();
expect(error.message).toContain('ElementHandles passed as arguments should belong');
expect(error.message).toContain('JSHandles can be evaluated only in the context they were created');
}));
it('should accept object handle as an argument', SX(async function() {
const navigatorHandle = await page.evaluateHandle(() => navigator);
const text = await page.evaluate(e => e.userAgent, navigatorHandle);
expect(text).toContain('Mozilla');
}));
it('should accept object handle to primitive types', SX(async function() {
const aHandle = await page.evaluateHandle(() => 5);
const isFive = await page.evaluate(e => Object.is(e, 5), aHandle);
expect(isFive).toBeTruthy();
}));
});
describe('Page.evaluateHandle', function() {
it('should work', SX(async function() {
const windowHandle = await page.evaluateHandle(() => window);
expect(windowHandle).toBeTruthy();
}));
});
describe('JSHandle.getProperty', function() {
it('should work', SX(async function() {
const aHandle = await page.evaluateHandle(() => ({
one: 1,
two: 2,
three: 3
}));
const twoHandle = await aHandle.getProperty('two');
expect(await twoHandle.jsonValue()).toEqual(2);
}));
});
describe('JSHandle.jsonValue', function() {
it('should work', SX(async function() {
const aHandle = await page.evaluateHandle(() => ({foo: 'bar'}));
const json = await aHandle.jsonValue();
expect(json).toEqual({foo: 'bar'});
}));
it('should work with dates', SX(async function() {
const dateHandle = await page.evaluateHandle(() => new Date('2017-09-26T00:00:00.000Z'));
const json = await dateHandle.jsonValue();
expect(json).toBe('2017-09-26T00:00:00.000Z');
}));
it('should throw for circular objects', SX(async function() {
const windowHandle = await page.evaluateHandle('window');
let error = null;
await windowHandle.jsonValue().catch(e => error = e);
expect(error.message).toContain('Converting circular structure to JSON');
}));
});
describe('JSHandle.getProperties', function() {
it('should work', SX(async function() {
const aHandle = await page.evaluateHandle(() => ({
foo: 'bar'
}));
const properties = await aHandle.getProperties();
const foo = properties.get('foo');
expect(foo).toBeTruthy();
expect(await foo.jsonValue()).toBe('bar');
}));
it('should return even non-own properties', SX(async function() {
const aHandle = await page.evaluateHandle(() => {
class A {
constructor() {
this.a = '1';
}
}
class B extends A {
constructor() {
super();
this.b = '2';
}
}
return new B();
});
const properties = await aHandle.getProperties();
expect(await properties.get('a').jsonValue()).toBe('1');
expect(await properties.get('b').jsonValue()).toBe('2');
}));
});
describe('JSHandle.asElement', function() {
it('should work', SX(async function() {
const aHandle = await page.evaluateHandle(() => document.body);
const element = aHandle.asElement();
expect(element).toBeTruthy();
}));
it('should return null for non-elements', SX(async function() {
const aHandle = await page.evaluateHandle(() => 2);
const element = aHandle.asElement();
expect(element).toBeFalsy();
}));
it('should return ElementHandle for TextNodes', SX(async function() {
await page.setContent('<div>ee!</div>');
const aHandle = await page.evaluateHandle(() => document.querySelector('div').firstChild);
const element = aHandle.asElement();
expect(element).toBeTruthy();
expect(await page.evaluate(e => e.nodeType === HTMLElement.TEXT_NODE, element));
}));
});
describe('JSHandle.toString', function() {
it('should work for primitives', SX(async function() {
const aHandle = await page.evaluateHandle(() => 2);
expect(aHandle.toString()).toBe('2');
}));
it('should work for complicated objects', SX(async function() {
const aHandle = await page.evaluateHandle(() => window);
expect(aHandle.toString()).toBe('JSHandle@object');
}));
});
@@ -322,6 +433,30 @@ describe('Page', function() {
}));
});
describe('Frame.context', function() {
const FrameUtils = require('./frame-utils');
it('should work', SX(async function() {
await page.goto(EMPTY_PAGE);
await FrameUtils.attachFrame(page, 'frame1', EMPTY_PAGE);
expect(page.frames().length).toBe(2);
const [frame1, frame2] = page.frames();
expect(frame1.executionContext()).toBeTruthy();
expect(frame2.executionContext()).toBeTruthy();
expect(frame1.executionContext() !== frame2.executionContext()).toBeTruthy();
await Promise.all([
frame1.executionContext().evaluate(() => window.a = 1),
frame2.executionContext().evaluate(() => window.a = 2)
]);
const [a1, a2] = await Promise.all([
frame1.executionContext().evaluate(() => window.a),
frame2.executionContext().evaluate(() => window.a)
]);
expect(a1).toBe(1);
expect(a2).toBe(2);
}));
});
describe('Frame.evaluate', function() {
const FrameUtils = require('./frame-utils');
it('should have different execution contexts', SX(async function() {
@@ -1241,9 +1376,24 @@ describe('Page', function() {
it('should work', SX(async function() {
await page.goto(PREFIX + '/input/button.html');
const button = await page.$('button');
await button.click('button');
await button.click();
expect(await page.evaluate(() => result)).toBe('Clicked');
}));
it('should work for TextNodes', SX(async function() {
await page.goto(PREFIX + '/input/button.html');
const buttonTextNode = await page.evaluateHandle(() => document.querySelector('button').firstChild);
let error = null;
await buttonTextNode.click().catch(err => error = err);
expect(error.message).toBe('Node is not of type HTMLElement');
}));
it('should throw for detached nodes', SX(async function() {
await page.goto(PREFIX + '/input/button.html');
const button = await page.$('button');
await page.evaluate(button => button.remove(), button);
let error = null;
await button.click().catch(err => error = err);
expect(error.message).toBe('Node is detached from document');
}));
});
describe('ElementHandle.hover', function() {