cuprite 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,123 @@
1
+ module Capybara::Cuprite
2
+ class Browser
3
+ module Input
4
+ def click(node, keys = [], offset = {})
5
+ x, y, modifiers = prepare_before_click(__method__, node, keys, offset)
6
+ command("Input.dispatchMouseEvent", type: "mousePressed", modifiers: modifiers, button: "left", x: x, y: y, clickCount: 1)
7
+ @wait = 0.05 # Potential wait because if network event is triggered then we have to wait until it's over.
8
+ command("Input.dispatchMouseEvent", type: "mouseReleased", modifiers: modifiers, button: "left", x: x, y: y, clickCount: 1)
9
+ end
10
+
11
+ def right_click(node, keys = [], offset = {})
12
+ x, y, modifiers = prepare_before_click(__method__, node, keys, offset)
13
+ command("Input.dispatchMouseEvent", type: "mousePressed", modifiers: modifiers, button: "right", x: x, y: y, clickCount: 1)
14
+ command("Input.dispatchMouseEvent", type: "mouseReleased", modifiers: modifiers, button: "right", x: x, y: y, clickCount: 1)
15
+ end
16
+
17
+ def double_click(node, keys = [], offset = {})
18
+ x, y, modifiers = prepare_before_click(__method__, node, keys, offset)
19
+ command("Input.dispatchMouseEvent", type: "mousePressed", modifiers: modifiers, button: "left", x: x, y: y, clickCount: 2)
20
+ command("Input.dispatchMouseEvent", type: "mouseReleased", modifiers: modifiers, button: "left", x: x, y: y, clickCount: 2)
21
+ end
22
+
23
+ def click_coordinates(x, y)
24
+ command("Input.dispatchMouseEvent", type: "mousePressed", button: "left", x: x, y: y, clickCount: 1)
25
+ @wait = 0.05 # Potential wait because if network event is triggered then we have to wait until it's over.
26
+ command("Input.dispatchMouseEvent", type: "mouseReleased", button: "left", x: x, y: y, clickCount: 1)
27
+ end
28
+
29
+ def hover(node)
30
+ evaluate_on(node: node, expr: "_cuprite.scrollIntoViewport(this)")
31
+ x, y = calculate_quads(node)
32
+ command("Input.dispatchMouseEvent", type: "mouseMoved", x: x, y: y)
33
+ end
34
+
35
+ def set(node, value)
36
+ object_id = command("DOM.resolveNode", nodeId: node["nodeId"]).dig("object", "objectId")
37
+ evaluate("_cuprite.set(arguments[0], arguments[1])", { "objectId" => object_id }, value)
38
+ end
39
+
40
+ def drag(node, other)
41
+ raise NotImplementedError
42
+ end
43
+
44
+ def drag_by(node, x, y)
45
+ raise NotImplementedError
46
+ end
47
+
48
+ def select(node, value)
49
+ evaluate_on(node: node, expr: "_cuprite.select(this, #{value})")
50
+ end
51
+
52
+ def trigger(node, event)
53
+ options = event.to_s == "click" ? { wait: 0.1 } : {}
54
+ evaluate_on(node: node, expr: %(_cuprite.trigger(this, "#{event}")), **options)
55
+ end
56
+
57
+ def scroll_to(left, top)
58
+ raise NotImplementedError
59
+ end
60
+
61
+ def send_keys(node, keys)
62
+ # value.each_char do |char|
63
+ # # Check puppeteer Input.js and USKeyboardLayout.js
64
+ # # also send_keys and modifiers from capybara API and unify all that.
65
+ # if /\n/.match?(char)
66
+ # command("Input.insertText", text: char)
67
+ # # command("Input.dispatchKeyEvent", type: "keyDown", code: "Enter", key: "Enter", text: "\r")
68
+ # # command("Input.dispatchKeyEvent", type: "keyUp", code: "Enter", key: "Enter")
69
+ # else
70
+ # command("Input.dispatchKeyEvent", type: "keyDown", text: char)
71
+ # command("Input.dispatchKeyEvent", type: "keyUp", text: char)
72
+ # end
73
+ # end
74
+ # command "send_keys", node, normalize_keys(keys)
75
+ raise NotImplementedError
76
+ end
77
+
78
+ private
79
+
80
+ def prepare_before_click(name, node, keys, offset)
81
+ evaluate_on(node: node, expr: "_cuprite.scrollIntoViewport(this)")
82
+ x, y = calculate_quads(node, offset[:x], offset[:y])
83
+ evaluate_on(node: node, expr: "_cuprite.mouseEventTest(this, '#{name}', #{x}, #{y})")
84
+
85
+ click_modifiers = { alt: 1, ctrl: 2, control: 2, meta: 4, command: 4, shift: 8 }
86
+ modifiers = keys.map { |k| click_modifiers[k.to_sym] }.compact.reduce(0, :|)
87
+
88
+ command("Input.dispatchMouseEvent", type: "mouseMoved", x: x, y: y)
89
+
90
+ [x, y, modifiers]
91
+ end
92
+
93
+ def calculate_quads(node, offset_x = nil, offset_y = nil)
94
+ quads = get_content_quads(node)
95
+ offset_x, offset_y = offset_x.to_i, offset_y.to_i
96
+
97
+ if offset_x > 0 || offset_y > 0
98
+ point = quads.first
99
+ [point[:x] + offset_x, point[:y] + offset_y]
100
+ else
101
+ x, y = quads.inject([0, 0]) do |memo, point|
102
+ [memo[0] + point[:x],
103
+ memo[1] + point[:y]]
104
+ end
105
+ [x / 4, y / 4]
106
+ end
107
+ end
108
+
109
+ def get_content_quads(node)
110
+ result = command("DOM.getContentQuads", nodeId: node["nodeId"])
111
+ raise "Node is either not visible or not an HTMLElement" if result["quads"].size == 0
112
+
113
+ # FIXME: Case when a few quads returned
114
+ result["quads"].map do |quad|
115
+ [{x: quad[0], y: quad[1]},
116
+ {x: quad[2], y: quad[3]},
117
+ {x: quad[4], y: quad[5]},
118
+ {x: quad[6], y: quad[7]}]
119
+ end.first
120
+ end
121
+ end
122
+ end
123
+ end
@@ -0,0 +1,407 @@
1
+ class InvalidSelector extends Error {}
2
+ class TimedOutPromise extends Error {}
3
+ class MouseEventFailed extends Error {}
4
+
5
+ const EVENTS = {
6
+ FOCUS: ["blur", "focus", "focusin", "focusout"],
7
+ MOUSE: ["click", "dblclick", "mousedown", "mouseenter", "mouseleave",
8
+ "mousemove", "mouseover", "mouseout", "mouseup", "contextmenu"],
9
+ FORM: ["submit"]
10
+ }
11
+
12
+ class Cuprite {
13
+ find(method, selector, within = document) {
14
+ try {
15
+ let results = [];
16
+
17
+ if (method == "xpath") {
18
+ let xpath = document.evaluate(selector, within, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);
19
+ for (let i = 0; i < xpath.snapshotLength; i++) {
20
+ results.push(xpath.snapshotItem(i));
21
+ }
22
+ } else {
23
+ results = within.querySelectorAll(selector);
24
+ }
25
+
26
+ return results;
27
+ } catch (error) {
28
+ // DOMException.INVALID_EXPRESSION_ERR is undefined, using pure code
29
+ if (error.code == DOMException.SYNTAX_ERR || error.code == 51) {
30
+ throw new InvalidSelector;
31
+ } else {
32
+ throw error;
33
+ }
34
+ }
35
+ }
36
+
37
+ parents(node) {
38
+ let nodes = [];
39
+ let parent = node.parentNode;
40
+ while (parent != document) {
41
+ nodes.push(parent);
42
+ parent = parent.parentNode;
43
+ }
44
+ return nodes;
45
+ }
46
+
47
+ visibleText(node) {
48
+ if (this.isVisible(node)) {
49
+ if (node.nodeName == "TEXTAREA") {
50
+ return node.textContent;
51
+ } else {
52
+ if (node instanceof SVGElement) {
53
+ return node.textContent;
54
+ } else {
55
+ return node.innerText;
56
+ }
57
+ }
58
+ }
59
+ }
60
+
61
+ isVisible(node) {
62
+ let mapName, style;
63
+ // if node is area, check visibility of relevant image
64
+ if (node.tagName === "AREA") {
65
+ mapName = document.evaluate("./ancestor::map/@name", node, null, XPathResult.STRING_TYPE, null).stringValue;
66
+ node = document.querySelector(`img[usemap="#${mapName}"]`);
67
+ if (node == null) {
68
+ return false;
69
+ }
70
+ }
71
+
72
+ while (node) {
73
+ style = window.getComputedStyle(node);
74
+ if (style.display === "none" || style.visibility === "hidden" || parseFloat(style.opacity) === 0) {
75
+ return false;
76
+ }
77
+ node = node.parentElement;
78
+ }
79
+
80
+ return true;
81
+ }
82
+
83
+
84
+ isDisabled(node) {
85
+ let xpath = "parent::optgroup[@disabled] | \
86
+ ancestor::select[@disabled] | \
87
+ parent::fieldset[@disabled] | \
88
+ ancestor::*[not(self::legend) or preceding-sibling::legend][parent::fieldset[@disabled]]";
89
+
90
+ return node.disabled || document.evaluate(xpath, node, null, XPathResult.BOOLEAN_TYPE, null).booleanValue;
91
+ }
92
+
93
+ path(node) {
94
+ let nodes = [node];
95
+ let parent = node.parentNode;
96
+ while (parent !== document) {
97
+ nodes.unshift(parent);
98
+ parent = parent.parentNode;
99
+ }
100
+
101
+ let selectors = nodes.map(node => {
102
+ let prevSiblings = [];
103
+ let xpath = document.evaluate(`./preceding-sibling::${node.tagName}`, node, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);
104
+
105
+ for (let i = 0; i < xpath.snapshotLength; i++) {
106
+ prevSiblings.push(xpath.snapshotItem(i));
107
+ }
108
+
109
+ return `${node.tagName}[${(prevSiblings.length + 1)}]`;
110
+ });
111
+
112
+ return `//${selectors.join("/")}`;
113
+ }
114
+
115
+ set(node, value) {
116
+ if (node.readOnly) return;
117
+
118
+ if (node.maxLength >= 0) {
119
+ value = value.substr(0, node.maxLength);
120
+ }
121
+
122
+ this.trigger(node, "focus");
123
+ node.value = "";
124
+
125
+ if (node.type == "number") {
126
+ node.value = value
127
+ } else {
128
+ for (let i = 0; i < value.length; i++) {
129
+ let char = value[i];
130
+ let keyCode = this.characterToKeyCode(char);
131
+ this.keyupdowned(node, "keydown", keyCode);
132
+ node.value += char;
133
+
134
+ this.keypressed(node, false, false, false, false, char.charCodeAt(0), char.charCodeAt(0));
135
+ this.keyupdowned(node, "keyup", keyCode);
136
+ }
137
+ }
138
+
139
+ this.changed(node);
140
+ this.input(node);
141
+ this.trigger(node, "blur");
142
+ }
143
+
144
+ input(node) {
145
+ let event = document.createEvent("HTMLEvents");
146
+ event.initEvent("input", true, false);
147
+ node.dispatchEvent(event);
148
+ }
149
+
150
+ keyupdowned(node, eventName, keyCode) {
151
+ let event = document.createEvent("UIEvents");
152
+ event.initEvent(eventName, true, true);
153
+ event.keyCode = keyCode;
154
+ event.charCode = 0;
155
+ node.dispatchEvent(event);
156
+ }
157
+
158
+ keypressed(node, altKey, ctrlKey, shiftKey, metaKey, keyCode, charCode) {
159
+ event = document.createEvent("UIEvents");
160
+ event.initEvent("keypress", true, true);
161
+ event.window = window;
162
+ event.altKey = altKey;
163
+ event.ctrlKey = ctrlKey;
164
+ event.shiftKey = shiftKey;
165
+ event.metaKey = metaKey;
166
+ event.keyCode = keyCode;
167
+ event.charCode = charCode;
168
+ node.dispatchEvent(event);
169
+ }
170
+
171
+ characterToKeyCode(char) {
172
+ const specialKeys = {
173
+ 96: 192, // `
174
+ 45: 189, // -
175
+ 61: 187, // =
176
+ 91: 219, // [
177
+ 93: 221, // ]
178
+ 92: 220, // \
179
+ 59: 186, // ;
180
+ 39: 222, // '
181
+ 44: 188, // ,
182
+ 46: 190, // .
183
+ 47: 191, // /
184
+ 127: 46, // delete
185
+ 126: 192, // ~
186
+ 33: 49, // !
187
+ 64: 50, // @
188
+ 35: 51, // #
189
+ 36: 52, // $
190
+ 37: 53, // %
191
+ 94: 54, // ^
192
+ 38: 55, // &
193
+ 42: 56, // *
194
+ 40: 57, // (
195
+ 41: 48, // )
196
+ 95: 189, // _
197
+ 43: 187, // +
198
+ 123: 219, // {
199
+ 125: 221, // }
200
+ 124: 220, // |
201
+ 58: 186, // :
202
+ 34: 222, // "
203
+ 60: 188, // <
204
+ 62: 190, // >
205
+ 63: 191, // ?
206
+ }
207
+
208
+ let code = char.toUpperCase().charCodeAt(0);
209
+ return specialKeys[code] || code;
210
+ }
211
+
212
+ scrollIntoViewport(node) {
213
+ let areaImage = this._getAreaImage(node);
214
+
215
+ if (areaImage) {
216
+ return this.scrollIntoViewport(areaImage);
217
+ } else {
218
+ node.scrollIntoViewIfNeeded();
219
+
220
+ if (!this._isInViewport(node)) {
221
+ node.scrollIntoView({block: "center", inline: "center", behavior: "instant"});
222
+ return this._isInViewport(node);
223
+ }
224
+
225
+ return true;
226
+ }
227
+ }
228
+
229
+ mouseEventTest(node, name, x, y) {
230
+ let frameOffset = this._frameOffset();
231
+ x -= frameOffset.left;
232
+ y -= frameOffset.top;
233
+
234
+ let element = document.elementFromPoint(x, y);
235
+
236
+ let el = element;
237
+ while (el) {
238
+ if (el == node) {
239
+ return true;
240
+ } else {
241
+ el = el.parentNode;
242
+ }
243
+ }
244
+
245
+ let selector = element && this._getSelector(element) || "none";
246
+ throw new MouseEventFailed([name, selector, x, y].join(", "));
247
+ }
248
+
249
+ _getAreaImage(node) {
250
+ if ("area" == node.tagName.toLowerCase()) {
251
+ let map = node.parentNode;
252
+ if (map.tagName.toLowerCase() != "map") {
253
+ throw new Error("the area is not within a map");
254
+ }
255
+
256
+ let mapName = map.getAttribute("name");
257
+ if (typeof mapName === "undefined" || mapName === null) {
258
+ throw new Error("area's parent map must have a name");
259
+ }
260
+
261
+ mapName = `#${mapName.toLowerCase()}`;
262
+ let imageNode = this.find("css", `img[usemap='${mapName}']`)[0];
263
+ if (typeof imageNode === "undefined" || imageNode === null) {
264
+ throw new Error("no image matches the map");
265
+ }
266
+
267
+ return imageNode;
268
+ }
269
+ }
270
+
271
+ _frameOffset() {
272
+ let win = window;
273
+ let offset = { top: 0, left: 0 };
274
+
275
+ while (win.frameElement) {
276
+ let rect = win.frameElement.getClientRects()[0];
277
+ let style = win.getComputedStyle(win.frameElement);
278
+ win = win.parent;
279
+
280
+ offset.top += rect.top + parseInt(style.getPropertyValue("padding-top"), 10)
281
+ offset.left += rect.left + parseInt(style.getPropertyValue("padding-left"), 10)
282
+ }
283
+
284
+ return offset;
285
+ }
286
+
287
+ _getSelector(el) {
288
+ let selector = (el.tagName != 'HTML') ? this._getSelector(el.parentNode) + " " : "";
289
+ selector += el.tagName.toLowerCase();
290
+ if (el.id) { selector += `#${el.id}` };
291
+ el.classList.forEach(c => selector += `.${c}`);
292
+ return selector;
293
+ }
294
+
295
+ _isInViewport(node) {
296
+ let rect = node.getBoundingClientRect();
297
+ return rect.top >= 0 &&
298
+ rect.left >= 0 &&
299
+ rect.bottom <= window.innerHeight &&
300
+ rect.right <= window.innerWidth;
301
+ }
302
+
303
+ select(node, value) {
304
+ if (this.isDisabled(node)) {
305
+ return false;
306
+ } else if (value == false && !node.parentNode.multiple) {
307
+ return false;
308
+ } else {
309
+ this.trigger(node.parentNode, "focus");
310
+
311
+ node.selected = value;
312
+ this.changed(node);
313
+
314
+ this.trigger(node.parentNode, "blur");
315
+ return true;
316
+ }
317
+ }
318
+
319
+ changed(node) {
320
+ let element;
321
+ let event = document.createEvent("HTMLEvents");
322
+ event.initEvent("change", true, false);
323
+
324
+ // In the case of an OPTION tag, the change event should come
325
+ // from the parent SELECT
326
+ if (node.nodeName == "OPTION") {
327
+ element = node.parentNode
328
+ if (element.nodeName == "OPTGROUP") {
329
+ element = element.parentNode
330
+ }
331
+ element
332
+ } else {
333
+ element = node
334
+ }
335
+
336
+ element.dispatchEvent(event)
337
+ }
338
+
339
+ trigger(node, name, options = {}) {
340
+ let event;
341
+
342
+ if (EVENTS.MOUSE.indexOf(name) != -1) {
343
+ event = document.createEvent("MouseEvent");
344
+ event.initMouseEvent(
345
+ name, true, true, window, 0,
346
+ options["screenX"] || 0, options["screenY"] || 0,
347
+ options["clientX"] || 0, options["clientY"] || 0,
348
+ options["ctrlKey"] || false,
349
+ options["altKey"] || false,
350
+ options["shiftKey"] || false,
351
+ options["metaKey"] || false,
352
+ options["button"] || 0, null
353
+ )
354
+ } else if (EVENTS.FOCUS.indexOf(name) != -1) {
355
+ event = this.obtainEvent(name);
356
+ } else if (EVENTS.FORM.indexOf(name) != -1) {
357
+ event = this.obtainEvent(name);
358
+ } else {
359
+ throw "Unknown event";
360
+ }
361
+
362
+ node.dispatchEvent(event);
363
+ }
364
+
365
+ obtainEvent(name) {
366
+ let event = document.createEvent("HTMLEvents");
367
+ event.initEvent(name, true, true);
368
+ return event;
369
+ }
370
+
371
+ getAttributes(node) {
372
+ let attrs = {};
373
+ for (let i = 0, len = node.attributes.length; i < len; i++) {
374
+ let attr = node.attributes[i];
375
+ attrs[attr.name] = attr.value.replace("\n", "\\n");
376
+ }
377
+
378
+ return JSON.stringify(attrs);
379
+ }
380
+
381
+ getAttribute(node, name) {
382
+ if (name == "checked" || name == "selected") {
383
+ return node[name];
384
+ } else {
385
+ return node.getAttribute(name);
386
+ }
387
+ }
388
+
389
+ value(node) {
390
+ if (node.tagName == "SELECT" && node.multiple) {
391
+ let result = []
392
+
393
+ for (let i = 0, len = node.children.length; i < len; i++) {
394
+ let option = node.children[i];
395
+ if (option.selected) {
396
+ result.push(option.value);
397
+ }
398
+ }
399
+
400
+ return result;
401
+ } else {
402
+ return node.value;
403
+ }
404
+ }
405
+ }
406
+
407
+ window._cuprite = new Cuprite;