forked from CrowdStrike/faltest
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathindex.js
404 lines (345 loc) · 10.9 KB
/
index.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
'use strict';
const { CustomVError } = require('verror-extra');
const { hidePassword, replacementText } = require('@faltest/utils').password;
const browserFunctionsToPassThrough = [
'reloadSession',
'status',
'url',
'getUrl',
'getTitle',
'$',
'$$',
'getWindowSize',
'setWindowSize',
'waitUntil',
'execute',
'executeAsync',
'keys',
'refresh',
'back',
];
const elementFunctionsToPassThrough = [
'getValue',
'setValue',
'click',
'moveTo',
'isEnabled',
'waitForEnabled',
'scrollIntoView',
'getAttribute',
'isDisplayed',
'selectByVisibleText',
];
/**
* This is an interface to the WebDriver browser object.
* The goal is to DRY up the common commands, and provide
* helpers for gaps in the WebDriver API.
*/
class Browser {
constructor(browser) {
this._browser = browser;
}
curry(func, ...args) {
return this[func].bind(this, ...args);
}
async throttleOn(nearlyOff) {
let latency;
let throughput;
if (nearlyOff) {
latency = 1;
throughput = 1;
} else {
latency = 1e3;
throughput = 25 * 1024;
}
await this._browser.setNetworkConditions({ latency, throughput });
}
async throttleOff() {
await this._browser.deleteNetworkConditions();
}
/**
* When you open a new tab in WebDriver, the actual browser
* switches tabs as expected, but the WebDriver browser object
* stays the same. You are expected to update the object yourself
* with the provided API. This sucks, firstly because WebDriver
* should be able to update their internal representation of the
* tabs automatically, secondly because there is no way to query
* the current tab, it's all up to you to guess which tab is the
* newly opened one and set it accordingly.
*/
async syncOpenedTab() {
await switchTabByIndexChanged.call(this, 1);
}
/**
* You again have to guess at which tab is now the current in the
* real browser.
*/
async closeTab() {
await switchTabByIndexChanged.call(this, -1, async () => {
await this._browser.closeWindow();
});
}
/**
* The only API available in WebDriver to kill a browser
* is `closeWindow`. This is insufficient because if there
* are multiple tabs open, it will only close a single tab.
* This kills tabs until there are no more, which eventually
* kills the browser.
*/
async close() {
while (true) {
try {
await this.closeTab();
} catch (err) {
if (err.message === 'There are no more tabs left.') {
break;
}
throw err;
}
}
}
}
async function switchTabByIndexChanged(i, runAfterTabsQueryButBeforeSwitch) {
let tabs = await this._browser.getWindowHandles();
let lastTab = await this._browser.getWindowHandle();
if (runAfterTabsQueryButBeforeSwitch) {
await runAfterTabsQueryButBeforeSwitch();
}
let nextTab = tabs[tabs.indexOf(lastTab) + i];
if (!nextTab) {
throw new Error('There are no more tabs left.');
}
await this._browser.switchToWindow(nextTab);
}
function find(func) {
return async function find(selectorOrElementOrElementsOrFunction) {
let elementOrElements;
switch (typeof selectorOrElementOrElementsOrFunction) {
case 'function':
elementOrElements = await find(selectorOrElementOrElementsOrFunction.call(this));
break;
case 'string':
elementOrElements = await this[func](selectorOrElementOrElementsOrFunction);
break;
default:
elementOrElements = selectorOrElementOrElementsOrFunction;
}
return elementOrElements;
};
}
function findChild(methodNameForErrorMessageOverride, browserMethodName, elementMethodName) {
return resolveElement(async function findChild(element, selector) {
let elementOrElements;
switch (typeof selector) {
case 'function':
elementOrElements = await this[browserMethodName](selector.bind(this, element));
break;
case 'string':
elementOrElements = await element[elementMethodName](selector);
break;
default:
elementOrElements = selector;
}
return elementOrElements;
}, methodNameForErrorMessageOverride);
}
class ElementError extends CustomVError {
static findLastError(err) {
return CustomVError.findLastErrorByType(err, ElementError);
}
constructor({
methodName,
element,
args,
message,
err,
}) {
if (!methodName) {
throw new Error('unexpected falsy methodName');
}
// convert potential `arguments` array-like to array
args = [...args];
if (element) {
let _element = element;
if (element.selector) {
_element = element.selector;
if (element.index !== undefined) {
_element += `[${element.index}]`;
}
}
args = [_element, ...args];
}
args = args.reduce((args, arg, i) => {
let _arg;
switch (typeof arg) {
case 'string':
if (methodName === setPassword.name && i === 1) {
_arg = replacementText;
} else {
_arg = arg;
}
break;
case 'function':
_arg = '<func>';
break;
case 'object':
_arg = '<obj>';
break;
}
if (_arg) {
args = args.concat(_arg);
}
return args;
}, []);
let _message = `${methodName}(${args.join()})`;
if (message) {
_message += `: ${message}`;
}
super({
name: methodName,
cause: err,
}, _message);
this.element = element;
}
}
function resolve(func) {
return function(callback, methodNameForErrorMessageOverride) {
return async function resolve(selectorOrElementOrElementsOrFunction, ...args) {
// I don't know if this step is necessary.
selectorOrElementOrElementsOrFunction = await selectorOrElementOrElementsOrFunction;
let elementOrElements;
try {
elementOrElements = await this[func](selectorOrElementOrElementsOrFunction);
let result = await callback.call(this, elementOrElements, ...args);
return result;
} catch (err) {
throw new ElementError({
methodName: methodNameForErrorMessageOverride || callback.name,
element: elementOrElements || selectorOrElementOrElementsOrFunction,
args,
err,
});
}
};
};
}
const resolveElement = resolve('_findElement');
const resolveElements = resolve('_findElements');
/**
* class fields are not allowed until node 12
* https://node.green/#ESNEXT-candidate--stage-3--instance-class-fields-public-instance-class-fields
*/
Browser.prototype._findElement = find('$');
Browser.prototype._findElements = find('$$');
// This handles arrays (`browser.$$`) too.
Browser.prototype.isExisting = resolveElement(async function isExisting(elementOrElements, ...args) {
if (Array.isArray(elementOrElements)) {
return elementOrElements.length > 0;
}
return await elementOrElements.isExisting(...args);
});
// This version handles both `browser.$` and `browser.$$`.
// The WebDriverIO version only handles `browser.$`.
function waitForExist(methodNameForErrorMessageOverride) {
return function(reverse = false) {
function isExisting(elements) {
return elements && !elements.length === reverse;
}
// We are avoiding using `resolveElements` because we want to
// throw an error with a different signature.
return async function waitForExist(selectorOrElementOrElementsOrFunction, ...args) {
let [timeout] = args;
let elementOrElements;
try {
elementOrElements = await this._findElements(selectorOrElementOrElementsOrFunction);
if (elementOrElements && !Array.isArray(elementOrElements)) {
let element = elementOrElements;
await element.waitForExist(timeout, reverse);
return;
}
let elements = elementOrElements;
// an optimisation since we already have the first query results handy
if (isExisting(elements)) {
return;
}
let innerError;
await this._browser.waitUntil(async () => {
try {
let elements = await this._findElements(selectorOrElementOrElementsOrFunction);
return isExisting(elements);
} catch (err) {
innerError = err;
return true;
}
}, timeout);
if (innerError) {
throw innerError;
}
} catch (err) {
throw new ElementError({
methodName: methodNameForErrorMessageOverride,
element: !Array.isArray(elementOrElements) ? elementOrElements : selectorOrElementOrElementsOrFunction,
args,
err,
});
}
};
};
}
Browser.prototype.waitForInsert = waitForExist('waitForInsert')();
Browser.prototype.waitForDestroy = waitForExist('waitForDestroy')(true);
Browser.prototype.getText = resolveElement(async function getText(element) {
// `element.getText` won't work because WebDriver calculates CSS,
// meaning any `text-transform: uppercase` are applied.
let text = await element.getProperty('textContent');
return text.trim();
});
for (let name of browserFunctionsToPassThrough) {
Browser.prototype[name] = async function() {
return await this._browser[name](...arguments);
};
}
for (let name of elementFunctionsToPassThrough) {
Browser.prototype[name] = resolveElement(async (element, ...args) => {
return await element[name](...args);
}, name);
}
Browser.prototype.findByText = resolveElements(async function findByText(elements, text) {
for (let element of elements) {
if (await this.getText(element) === text) {
return element;
}
}
throw new Error(`Find by text "${text}" yielded no results.`);
});
Browser.prototype.waitForText = resolveElement(async function waitForText(element, text, invert) {
await this.waitUntil(async () => {
let currentText = await this.getText(element);
return invert ? currentText !== text : currentText === text;
});
});
Browser.prototype.elementSendKeys = resolveElement(async function elementSendKeys(element, ...args) {
await this._browser.elementSendKeys(element.elementId, ...args);
});
Browser.prototype.waitForDisabled = resolveElement(async function waitForDisabled(element) {
await element.waitForEnabled(undefined, true);
});
Browser.prototype.waitForVisible = resolveElement(async function waitForVisible(element) {
await element.waitForDisplayed();
});
Browser.prototype.waitForHidden = resolveElement(async function waitForHidden(element) {
await element.waitForDisplayed(undefined, true);
});
async function setPassword(element, ...args) {
await hidePassword(async () => {
await element.setValue(...args);
});
}
Browser.prototype.setPassword = resolveElement(setPassword);
Browser.prototype.findChild = findChild('findChild', '_findElement', '$');
Browser.prototype.findChildren = findChild('findChildren', '_findElements', '$$');
/**
* end class fields
*/
module.exports = Browser;
module.exports.ElementError = ElementError;