1 |
|
2 |
|
3 |
|
4 |
|
5 |
|
6 |
|
7 |
|
8 |
|
9 |
|
10 |
|
11 |
|
12 |
|
13 |
|
14 |
|
15 |
|
16 | const path = require('path');
|
17 | const {JSHandle} = require('./ExecutionContext');
|
18 | const {helper, assert, debugError} = require('./helper');
|
19 |
|
20 | class ElementHandle extends JSHandle {
|
21 | |
22 |
|
23 |
|
24 |
|
25 |
|
26 |
|
27 |
|
28 | constructor(context, client, remoteObject, page, frameManager) {
|
29 | super(context, client, remoteObject);
|
30 | this._client = client;
|
31 | this._remoteObject = remoteObject;
|
32 | this._page = page;
|
33 | this._frameManager = frameManager;
|
34 | this._disposed = false;
|
35 | }
|
36 |
|
37 | |
38 |
|
39 |
|
40 |
|
41 | asElement() {
|
42 | return this;
|
43 | }
|
44 |
|
45 | |
46 |
|
47 |
|
48 | async contentFrame() {
|
49 | const nodeInfo = await this._client.send('DOM.describeNode', {
|
50 | objectId: this._remoteObject.objectId
|
51 | });
|
52 | if (typeof nodeInfo.node.frameId !== 'string')
|
53 | return null;
|
54 | return this._frameManager.frame(nodeInfo.node.frameId);
|
55 | }
|
56 |
|
57 | async _scrollIntoViewIfNeeded() {
|
58 | const error = await this.executionContext().evaluate(async(element, pageJavascriptEnabled) => {
|
59 | if (!element.isConnected)
|
60 | return 'Node is detached from document';
|
61 | if (element.nodeType !== Node.ELEMENT_NODE)
|
62 | return 'Node is not of type HTMLElement';
|
63 |
|
64 | if (!pageJavascriptEnabled) {
|
65 | element.scrollIntoView({block: 'center', inline: 'center', behavior: 'instant'});
|
66 | return false;
|
67 | }
|
68 | const visibleRatio = await new Promise(resolve => {
|
69 | const observer = new IntersectionObserver(entries => {
|
70 | resolve(entries[0].intersectionRatio);
|
71 | observer.disconnect();
|
72 | });
|
73 | observer.observe(element);
|
74 | });
|
75 | if (visibleRatio !== 1.0)
|
76 | element.scrollIntoView({block: 'center', inline: 'center', behavior: 'instant'});
|
77 | return false;
|
78 | }, this, this._page._javascriptEnabled);
|
79 | if (error)
|
80 | throw new Error(error);
|
81 | }
|
82 |
|
83 | |
84 |
|
85 |
|
86 | async _clickablePoint() {
|
87 | const result = await this._client.send('DOM.getContentQuads', {
|
88 | objectId: this._remoteObject.objectId
|
89 | }).catch(debugError);
|
90 | if (!result || !result.quads.length)
|
91 | throw new Error('Node is either not visible or not an HTMLElement');
|
92 |
|
93 | const quads = result.quads.map(quad => this._fromProtocolQuad(quad)).filter(quad => computeQuadArea(quad) > 1);
|
94 | if (!quads.length)
|
95 | throw new Error('Node is either not visible or not an HTMLElement');
|
96 |
|
97 | const quad = quads[0];
|
98 | let x = 0;
|
99 | let y = 0;
|
100 | for (const point of quad) {
|
101 | x += point.x;
|
102 | y += point.y;
|
103 | }
|
104 | return {
|
105 | x: x / 4,
|
106 | y: y / 4
|
107 | };
|
108 | }
|
109 |
|
110 | |
111 |
|
112 |
|
113 | _getBoxModel() {
|
114 | return this._client.send('DOM.getBoxModel', {
|
115 | objectId: this._remoteObject.objectId
|
116 | }).catch(error => debugError(error));
|
117 | }
|
118 |
|
119 | |
120 |
|
121 |
|
122 |
|
123 | _fromProtocolQuad(quad) {
|
124 | return [
|
125 | {x: quad[0], y: quad[1]},
|
126 | {x: quad[2], y: quad[3]},
|
127 | {x: quad[4], y: quad[5]},
|
128 | {x: quad[6], y: quad[7]}
|
129 | ];
|
130 | }
|
131 |
|
132 | async hover() {
|
133 | await this._scrollIntoViewIfNeeded();
|
134 | const {x, y} = await this._clickablePoint();
|
135 | await this._page.mouse.move(x, y);
|
136 | }
|
137 |
|
138 | |
139 |
|
140 |
|
141 | async click(options = {}) {
|
142 | await this._scrollIntoViewIfNeeded();
|
143 | const {x, y} = await this._clickablePoint();
|
144 | await this._page.mouse.click(x, y, options);
|
145 | }
|
146 |
|
147 | |
148 |
|
149 |
|
150 |
|
151 | async uploadFile(...filePaths) {
|
152 | const files = filePaths.map(filePath => path.resolve(filePath));
|
153 | const objectId = this._remoteObject.objectId;
|
154 | return this._client.send('DOM.setFileInputFiles', { objectId, files });
|
155 | }
|
156 |
|
157 | async tap() {
|
158 | await this._scrollIntoViewIfNeeded();
|
159 | const {x, y} = await this._clickablePoint();
|
160 | await this._page.touchscreen.tap(x, y);
|
161 | }
|
162 |
|
163 | async focus() {
|
164 | await this.executionContext().evaluate(element => element.focus(), this);
|
165 | }
|
166 |
|
167 | |
168 |
|
169 |
|
170 |
|
171 | async type(text, options) {
|
172 | await this.focus();
|
173 | await this._page.keyboard.type(text, options);
|
174 | }
|
175 |
|
176 | |
177 |
|
178 |
|
179 |
|
180 | async press(key, options) {
|
181 | await this.focus();
|
182 | await this._page.keyboard.press(key, options);
|
183 | }
|
184 |
|
185 | |
186 |
|
187 |
|
188 | async boundingBox() {
|
189 | const result = await this._getBoxModel();
|
190 |
|
191 | if (!result)
|
192 | return null;
|
193 |
|
194 | const quad = result.model.border;
|
195 | const x = Math.min(quad[0], quad[2], quad[4], quad[6]);
|
196 | const y = Math.min(quad[1], quad[3], quad[5], quad[7]);
|
197 | const width = Math.max(quad[0], quad[2], quad[4], quad[6]) - x;
|
198 | const height = Math.max(quad[1], quad[3], quad[5], quad[7]) - y;
|
199 |
|
200 | return {x, y, width, height};
|
201 | }
|
202 |
|
203 | |
204 |
|
205 |
|
206 | async boxModel() {
|
207 | const result = await this._getBoxModel();
|
208 |
|
209 | if (!result)
|
210 | return null;
|
211 |
|
212 | const {content, padding, border, margin, width, height} = result.model;
|
213 | return {
|
214 | content: this._fromProtocolQuad(content),
|
215 | padding: this._fromProtocolQuad(padding),
|
216 | border: this._fromProtocolQuad(border),
|
217 | margin: this._fromProtocolQuad(margin),
|
218 | width,
|
219 | height
|
220 | };
|
221 | }
|
222 |
|
223 | |
224 |
|
225 |
|
226 |
|
227 |
|
228 | async screenshot(options = {}) {
|
229 | let needsViewportReset = false;
|
230 |
|
231 | let boundingBox = await this.boundingBox();
|
232 | assert(boundingBox, 'Node is either not visible or not an HTMLElement');
|
233 |
|
234 | const viewport = this._page.viewport();
|
235 |
|
236 | if (boundingBox.width > viewport.width || boundingBox.height > viewport.height) {
|
237 | const newViewport = {
|
238 | width: Math.max(viewport.width, Math.ceil(boundingBox.width)),
|
239 | height: Math.max(viewport.height, Math.ceil(boundingBox.height)),
|
240 | };
|
241 | await this._page.setViewport(Object.assign({}, viewport, newViewport));
|
242 |
|
243 | needsViewportReset = true;
|
244 | }
|
245 |
|
246 | await this._scrollIntoViewIfNeeded();
|
247 |
|
248 | boundingBox = await this.boundingBox();
|
249 | assert(boundingBox, 'Node is either not visible or not an HTMLElement');
|
250 |
|
251 | const { layoutViewport: { pageX, pageY } } = await this._client.send('Page.getLayoutMetrics');
|
252 |
|
253 | const clip = Object.assign({}, boundingBox);
|
254 | clip.x += pageX;
|
255 | clip.y += pageY;
|
256 |
|
257 | const imageData = await this._page.screenshot(Object.assign({}, {
|
258 | clip
|
259 | }, options));
|
260 |
|
261 | if (needsViewportReset)
|
262 | await this._page.setViewport(viewport);
|
263 |
|
264 | return imageData;
|
265 | }
|
266 |
|
267 | |
268 |
|
269 |
|
270 |
|
271 | async $(selector) {
|
272 | const handle = await this.executionContext().evaluateHandle(
|
273 | (element, selector) => element.querySelector(selector),
|
274 | this, selector
|
275 | );
|
276 | const element = handle.asElement();
|
277 | if (element)
|
278 | return element;
|
279 | await handle.dispose();
|
280 | return null;
|
281 | }
|
282 |
|
283 | |
284 |
|
285 |
|
286 |
|
287 | async $$(selector) {
|
288 | const arrayHandle = await this.executionContext().evaluateHandle(
|
289 | (element, selector) => element.querySelectorAll(selector),
|
290 | this, selector
|
291 | );
|
292 | const properties = await arrayHandle.getProperties();
|
293 | await arrayHandle.dispose();
|
294 | const result = [];
|
295 | for (const property of properties.values()) {
|
296 | const elementHandle = property.asElement();
|
297 | if (elementHandle)
|
298 | result.push(elementHandle);
|
299 | }
|
300 | return result;
|
301 | }
|
302 |
|
303 | |
304 |
|
305 |
|
306 |
|
307 |
|
308 |
|
309 | async $eval(selector, pageFunction, ...args) {
|
310 | const elementHandle = await this.$(selector);
|
311 | if (!elementHandle)
|
312 | throw new Error(`Error: failed to find element matching selector "${selector}"`);
|
313 | const result = await this.executionContext().evaluate(pageFunction, elementHandle, ...args);
|
314 | await elementHandle.dispose();
|
315 | return result;
|
316 | }
|
317 |
|
318 | |
319 |
|
320 |
|
321 |
|
322 |
|
323 |
|
324 | async $$eval(selector, pageFunction, ...args) {
|
325 | const arrayHandle = await this.executionContext().evaluateHandle(
|
326 | (element, selector) => Array.from(element.querySelectorAll(selector)),
|
327 | this, selector
|
328 | );
|
329 |
|
330 | const result = await this.executionContext().evaluate(pageFunction, arrayHandle, ...args);
|
331 | await arrayHandle.dispose();
|
332 | return result;
|
333 | }
|
334 |
|
335 | |
336 |
|
337 |
|
338 |
|
339 | async $x(expression) {
|
340 | const arrayHandle = await this.executionContext().evaluateHandle(
|
341 | (element, expression) => {
|
342 | const document = element.ownerDocument || element;
|
343 | const iterator = document.evaluate(expression, element, null, XPathResult.ORDERED_NODE_ITERATOR_TYPE);
|
344 | const array = [];
|
345 | let item;
|
346 | while ((item = iterator.iterateNext()))
|
347 | array.push(item);
|
348 | return array;
|
349 | },
|
350 | this, expression
|
351 | );
|
352 | const properties = await arrayHandle.getProperties();
|
353 | await arrayHandle.dispose();
|
354 | const result = [];
|
355 | for (const property of properties.values()) {
|
356 | const elementHandle = property.asElement();
|
357 | if (elementHandle)
|
358 | result.push(elementHandle);
|
359 | }
|
360 | return result;
|
361 | }
|
362 |
|
363 | |
364 |
|
365 |
|
366 | isIntersectingViewport() {
|
367 | return this.executionContext().evaluate(async element => {
|
368 | const visibleRatio = await new Promise(resolve => {
|
369 | const observer = new IntersectionObserver(entries => {
|
370 | resolve(entries[0].intersectionRatio);
|
371 | observer.disconnect();
|
372 | });
|
373 | observer.observe(element);
|
374 | });
|
375 | return visibleRatio > 0;
|
376 | }, this);
|
377 | }
|
378 | }
|
379 |
|
380 | function computeQuadArea(quad) {
|
381 |
|
382 |
|
383 | let area = 0;
|
384 | for (let i = 0; i < quad.length; ++i) {
|
385 | const p1 = quad[i];
|
386 | const p2 = quad[(i + 1) % quad.length];
|
387 | area += (p1.x * p2.y - p2.x * p1.y) / 2;
|
388 | }
|
389 | return area;
|
390 | }
|
391 |
|
392 | module.exports = {ElementHandle};
|
393 | helper.tracePublicAPI(ElementHandle);
|