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, 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(element => {
|
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 | element.scrollIntoViewIfNeeded();
|
64 | return false;
|
65 | }, this);
|
66 | if (error)
|
67 | throw new Error(error);
|
68 | }
|
69 |
|
70 | |
71 |
|
72 |
|
73 | async _visibleCenter() {
|
74 | await this._scrollIntoViewIfNeeded();
|
75 | const box = await this._assertBoundingBox();
|
76 | return {
|
77 | x: box.x + box.width / 2,
|
78 | y: box.y + box.height / 2
|
79 | };
|
80 | }
|
81 |
|
82 | |
83 |
|
84 |
|
85 | _getBoxModel() {
|
86 | return this._client.send('DOM.getBoxModel', {
|
87 | objectId: this._remoteObject.objectId
|
88 | }).catch(error => debugError(error));
|
89 | }
|
90 |
|
91 | |
92 |
|
93 |
|
94 |
|
95 | _fromProtocolQuad(quad) {
|
96 | return [
|
97 | {x: quad[0], y: quad[1]},
|
98 | {x: quad[2], y: quad[3]},
|
99 | {x: quad[4], y: quad[5]},
|
100 | {x: quad[6], y: quad[7]}
|
101 | ];
|
102 | }
|
103 |
|
104 | async hover() {
|
105 | const {x, y} = await this._visibleCenter();
|
106 | await this._page.mouse.move(x, y);
|
107 | }
|
108 |
|
109 | |
110 |
|
111 |
|
112 | async click(options = {}) {
|
113 | const {x, y} = await this._visibleCenter();
|
114 | await this._page.mouse.click(x, y, options);
|
115 | }
|
116 |
|
117 | |
118 |
|
119 |
|
120 |
|
121 | async uploadFile(...filePaths) {
|
122 | const files = filePaths.map(filePath => path.resolve(filePath));
|
123 | const objectId = this._remoteObject.objectId;
|
124 | return this._client.send('DOM.setFileInputFiles', { objectId, files });
|
125 | }
|
126 |
|
127 | async tap() {
|
128 | const {x, y} = await this._visibleCenter();
|
129 | await this._page.touchscreen.tap(x, y);
|
130 | }
|
131 |
|
132 | async focus() {
|
133 | await this.executionContext().evaluate(element => element.focus(), this);
|
134 | }
|
135 |
|
136 | |
137 |
|
138 |
|
139 |
|
140 | async type(text, options) {
|
141 | await this.focus();
|
142 | await this._page.keyboard.type(text, options);
|
143 | }
|
144 |
|
145 | |
146 |
|
147 |
|
148 |
|
149 | async press(key, options) {
|
150 | await this.focus();
|
151 | await this._page.keyboard.press(key, options);
|
152 | }
|
153 |
|
154 | |
155 |
|
156 |
|
157 | async boundingBox() {
|
158 | const result = await this._getBoxModel();
|
159 |
|
160 | if (!result)
|
161 | return null;
|
162 |
|
163 | const quad = result.model.border;
|
164 | const x = Math.min(quad[0], quad[2], quad[4], quad[6]);
|
165 | const y = Math.min(quad[1], quad[3], quad[5], quad[7]);
|
166 | const width = Math.max(quad[0], quad[2], quad[4], quad[6]) - x;
|
167 | const height = Math.max(quad[1], quad[3], quad[5], quad[7]) - y;
|
168 |
|
169 | return {x, y, width, height};
|
170 | }
|
171 |
|
172 | |
173 |
|
174 |
|
175 | async boxModel() {
|
176 | const result = await this._getBoxModel();
|
177 |
|
178 | if (!result)
|
179 | return null;
|
180 |
|
181 | const {content, padding, border, margin, width, height} = result.model;
|
182 | return {
|
183 | content: this._fromProtocolQuad(content),
|
184 | padding: this._fromProtocolQuad(padding),
|
185 | border: this._fromProtocolQuad(border),
|
186 | margin: this._fromProtocolQuad(margin),
|
187 | width,
|
188 | height
|
189 | };
|
190 | }
|
191 |
|
192 | |
193 |
|
194 |
|
195 | async _assertBoundingBox() {
|
196 | const boundingBox = await this.boundingBox();
|
197 | if (boundingBox)
|
198 | return boundingBox;
|
199 |
|
200 | throw new Error('Node is either not visible or not an HTMLElement');
|
201 | }
|
202 |
|
203 | |
204 |
|
205 |
|
206 |
|
207 |
|
208 | async screenshot(options = {}) {
|
209 | let needsViewportReset = false;
|
210 |
|
211 | let boundingBox = await this._assertBoundingBox();
|
212 |
|
213 | const viewport = this._page.viewport();
|
214 |
|
215 | if (boundingBox.width > viewport.width || boundingBox.height > viewport.height) {
|
216 | const newViewport = {
|
217 | width: Math.max(viewport.width, Math.ceil(boundingBox.width)),
|
218 | height: Math.max(viewport.height, Math.ceil(boundingBox.height)),
|
219 | };
|
220 | await this._page.setViewport(Object.assign({}, viewport, newViewport));
|
221 |
|
222 | needsViewportReset = true;
|
223 | }
|
224 |
|
225 | await this.executionContext().evaluate(function(element) {
|
226 | element.scrollIntoView({block: 'center', inline: 'center', behavior: 'instant'});
|
227 | }, this);
|
228 |
|
229 | boundingBox = await this._assertBoundingBox();
|
230 |
|
231 | const { layoutViewport: { pageX, pageY } } = await this._client.send('Page.getLayoutMetrics');
|
232 |
|
233 | const clip = Object.assign({}, boundingBox);
|
234 | clip.x += pageX;
|
235 | clip.y += pageY;
|
236 |
|
237 | const imageData = await this._page.screenshot(Object.assign({}, {
|
238 | clip
|
239 | }, options));
|
240 |
|
241 | if (needsViewportReset)
|
242 | await this._page.setViewport(viewport);
|
243 |
|
244 | return imageData;
|
245 | }
|
246 |
|
247 | |
248 |
|
249 |
|
250 |
|
251 | async $(selector) {
|
252 | const handle = await this.executionContext().evaluateHandle(
|
253 | (element, selector) => element.querySelector(selector),
|
254 | this, selector
|
255 | );
|
256 | const element = handle.asElement();
|
257 | if (element)
|
258 | return element;
|
259 | await handle.dispose();
|
260 | return null;
|
261 | }
|
262 |
|
263 | |
264 |
|
265 |
|
266 |
|
267 | async $$(selector) {
|
268 | const arrayHandle = await this.executionContext().evaluateHandle(
|
269 | (element, selector) => element.querySelectorAll(selector),
|
270 | this, selector
|
271 | );
|
272 | const properties = await arrayHandle.getProperties();
|
273 | await arrayHandle.dispose();
|
274 | const result = [];
|
275 | for (const property of properties.values()) {
|
276 | const elementHandle = property.asElement();
|
277 | if (elementHandle)
|
278 | result.push(elementHandle);
|
279 | }
|
280 | return result;
|
281 | }
|
282 |
|
283 | |
284 |
|
285 |
|
286 |
|
287 | async $x(expression) {
|
288 | const arrayHandle = await this.executionContext().evaluateHandle(
|
289 | (element, expression) => {
|
290 | const document = element.ownerDocument || element;
|
291 | const iterator = document.evaluate(expression, element, null, XPathResult.ORDERED_NODE_ITERATOR_TYPE);
|
292 | const array = [];
|
293 | let item;
|
294 | while ((item = iterator.iterateNext()))
|
295 | array.push(item);
|
296 | return array;
|
297 | },
|
298 | this, expression
|
299 | );
|
300 | const properties = await arrayHandle.getProperties();
|
301 | await arrayHandle.dispose();
|
302 | const result = [];
|
303 | for (const property of properties.values()) {
|
304 | const elementHandle = property.asElement();
|
305 | if (elementHandle)
|
306 | result.push(elementHandle);
|
307 | }
|
308 | return result;
|
309 | }
|
310 | }
|
311 |
|
312 | module.exports = ElementHandle;
|
313 | helper.tracePublicAPI(ElementHandle);
|