sefiutils 1.0.46 → 1.0.47
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.
- package/lib/seUtils.js +4 -4
- package/lib/seUtils.js.map +1 -1
- package/package.json +3 -2
- package/src/execScripts/fiber.js +211 -0
- package/src/execScripts/getButtonsAndHRefs.js +15 -0
- package/src/fiberUtils.ts +146 -0
- package/src/globalSetup.ts +116 -0
- package/src/globalTeardown.ts +22 -0
- package/src/localSetup.ts +128 -0
- package/src/localTeardown.ts +14 -0
- package/src/log.ts +19 -0
- package/src/seUtils.ts +836 -0
package/src/seUtils.ts
ADDED
|
@@ -0,0 +1,836 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* (C) Jens von Pilgrim, 2023
|
|
3
|
+
*/
|
|
4
|
+
import dotenv from "dotenv";
|
|
5
|
+
dotenv.config() // read ".env"
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
import * as fs from "fs";
|
|
9
|
+
import * as fsPromises from "fs/promises";
|
|
10
|
+
import path from "path";
|
|
11
|
+
import { By, Locator, WebDriver, WebElement } from 'selenium-webdriver';
|
|
12
|
+
|
|
13
|
+
export function getRemoteAddressAndPort(): { address: string; port: number; } {
|
|
14
|
+
const address = process.env.SEFI_SELENIUM_REMOTE_URL || "http://localhost:4444/wd/hub";
|
|
15
|
+
const port = Number(address.match(/:(\d+)\/?/)?.[1] || 80);
|
|
16
|
+
return { address, port };
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Compares a given string with either a string or a regex.
|
|
21
|
+
* @param str string to compare
|
|
22
|
+
* @param stringOrRegEx string or regex to compare with
|
|
23
|
+
* @param options (ignoreCase is ignored if stringOrRegEx is a regex)
|
|
24
|
+
* @returns
|
|
25
|
+
*/
|
|
26
|
+
export function cmpStrings(str: string | undefined, stringOrRegEx: undefined | string | RegExp, options?: { ignoreCase?: boolean }) {
|
|
27
|
+
if (stringOrRegEx === null || stringOrRegEx === undefined) {
|
|
28
|
+
return str === undefined || str === null;
|
|
29
|
+
}
|
|
30
|
+
if (str === null || str === undefined) {
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (stringOrRegEx instanceof RegExp) {
|
|
35
|
+
return stringOrRegEx.test(str!);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return (options?.ignoreCase)
|
|
39
|
+
? str!.toLocaleLowerCase() === (stringOrRegEx as string)!.toLocaleLowerCase()
|
|
40
|
+
: str === stringOrRegEx;
|
|
41
|
+
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export async function execScript<T>(driver: WebDriver, scriptName: string, fct: string): Promise<T> {
|
|
45
|
+
const scriptPath = path.join(path.dirname(__filename), scriptName);
|
|
46
|
+
const script = fs.readFileSync(scriptPath, "utf-8");
|
|
47
|
+
const res = await driver.executeScript(script + `
|
|
48
|
+
return ${fct}();
|
|
49
|
+
`);
|
|
50
|
+
return res as T;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface Bounds {
|
|
54
|
+
top: number;
|
|
55
|
+
left: number;
|
|
56
|
+
bottom: number;
|
|
57
|
+
right: number;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
export function boundsToString(bounds: Bounds) {
|
|
62
|
+
return `(${bounds.left};${bounds.top})x(${bounds.right};${bounds.bottom})`
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function toFloor(bounds: Bounds[]) {
|
|
66
|
+
const floors: Bounds[] = [];
|
|
67
|
+
for (const b of bounds) {
|
|
68
|
+
floors.push({
|
|
69
|
+
top: Math.floor(b.top),
|
|
70
|
+
bottom: Math.floor(b.bottom),
|
|
71
|
+
left: Math.floor(b.left),
|
|
72
|
+
right: Math.floor(b.right)
|
|
73
|
+
})
|
|
74
|
+
}
|
|
75
|
+
return floors;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Coordinates are sometimes floats. We compare two numbers with a delta of 1 to ignore rounding problems.
|
|
80
|
+
*/
|
|
81
|
+
export function circa(n1: number, n2: number) {
|
|
82
|
+
const delta = n1 - n2;
|
|
83
|
+
return Math.abs(delta) < 1;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
export async function waitFor<T>(callback: () => Promise<T>, timeout = 1000, intervalTime = 100): Promise<T> {
|
|
89
|
+
// try first time
|
|
90
|
+
const start = new Date().getTime();
|
|
91
|
+
try {
|
|
92
|
+
const result = await callback();
|
|
93
|
+
return result;
|
|
94
|
+
} catch (error) {
|
|
95
|
+
// ignore error
|
|
96
|
+
}
|
|
97
|
+
// if error, try until timeout ms have passed
|
|
98
|
+
return new Promise<T>((resolve, reject) => {
|
|
99
|
+
const interval = setInterval(async () => {
|
|
100
|
+
try {
|
|
101
|
+
const result = await callback();
|
|
102
|
+
clearInterval(interval);
|
|
103
|
+
resolve(result);
|
|
104
|
+
} catch (error) {
|
|
105
|
+
const duration = new Date().getTime() - start;
|
|
106
|
+
if (duration > timeout) {
|
|
107
|
+
clearInterval(interval);
|
|
108
|
+
reject(error);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}, intervalTime);
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Takes screenshot of current window and saves it to the given file.
|
|
118
|
+
* The folder is created if it does not yet exists.
|
|
119
|
+
*/
|
|
120
|
+
export async function saveScreenshot(driver: WebDriver, fileName: string): Promise<void> {
|
|
121
|
+
const pngBase64 = await driver.takeScreenshot();
|
|
122
|
+
const dirName = path.dirname(fileName);
|
|
123
|
+
if (!fs.existsSync(dirName)) {
|
|
124
|
+
await fsPromises.mkdir(dirName, { recursive: true });
|
|
125
|
+
}
|
|
126
|
+
await fsPromises.writeFile(fileName, pngBase64, 'base64');
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Helper function for printing out an element (tag, id if present)
|
|
133
|
+
*/
|
|
134
|
+
export async function describe(we: WebElement) {
|
|
135
|
+
let s = await we.getTagName();
|
|
136
|
+
const id = await we.getAttribute("id");
|
|
137
|
+
if (id) {
|
|
138
|
+
s += "#" + id;
|
|
139
|
+
} else {
|
|
140
|
+
const clazz = await we.getAttribute("class");
|
|
141
|
+
if (clazz) {
|
|
142
|
+
s += "." + clazz;
|
|
143
|
+
}
|
|
144
|
+
let text = await we.getText();
|
|
145
|
+
s += ` ("${text.substring(0, 7)}${text.length > 7 ? '…' : ''})"`;
|
|
146
|
+
}
|
|
147
|
+
return s;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Gets the element by means of `WebElement` by its id attribute,
|
|
152
|
+
* either of the whole document (if driver is given) or of the given parent element.
|
|
153
|
+
*/
|
|
154
|
+
export async function getElementById(driverOrParent: WebDriver | WebElement, id: string): Promise<WebElement> {
|
|
155
|
+
const el = await driverOrParent.findElement(By.id(id));
|
|
156
|
+
return el;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Gets the first element by means of `WebElement` by its class attribute,
|
|
161
|
+
* either of the whole document (if driver is given) or of the given parent element.
|
|
162
|
+
*/
|
|
163
|
+
export async function getElementByClass(driverOrParent: WebDriver | WebElement, clazz: string): Promise<WebElement> {
|
|
164
|
+
const el = await driverOrParent.findElement(By.className(clazz));
|
|
165
|
+
return el;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Gets all element by means of `WebElement` by its class attribute,
|
|
170
|
+
* either of the whole document (if driver is given) or of the given parent element.
|
|
171
|
+
*/
|
|
172
|
+
export async function getElementsByClass(driverOrParent: WebDriver | WebElement, clazz: string): Promise<WebElement[]> {
|
|
173
|
+
const els = await driverOrParent.findElements(By.className(clazz));
|
|
174
|
+
return els;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Gets the first element by means of `WebElement` by its tag name,
|
|
179
|
+
* either of the whole document (if driver is given) or of the given parent element.
|
|
180
|
+
*/
|
|
181
|
+
export async function getElementByTag(driverOrParent: WebDriver | WebElement, tagName: string): Promise<WebElement> {
|
|
182
|
+
const el = await driverOrParent.findElement(By.css(tagName));
|
|
183
|
+
return el;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Gets all elements by means of `WebElement` by its tag name,
|
|
188
|
+
* either of the whole document (if driver is given) or of the given parent element.
|
|
189
|
+
*/
|
|
190
|
+
export async function getElementsByTag(driverOrParent: WebDriver | WebElement, tagName: string): Promise<WebElement[]> {
|
|
191
|
+
const els = await driverOrParent.findElements(By.css(tagName));
|
|
192
|
+
return els;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Gets all elements by means of `WebElement` by its tag name,
|
|
197
|
+
* either of the whole document (if driver is given) or of the given parent element.
|
|
198
|
+
*/
|
|
199
|
+
export async function getElementsByTags(driverOrParent: WebDriver | WebElement, tagNames: string[]): Promise<WebElement[]> {
|
|
200
|
+
const els: WebElement[] = [];
|
|
201
|
+
for (const tagName of tagNames) {
|
|
202
|
+
const elByTag = await driverOrParent.findElements(By.css(tagName));
|
|
203
|
+
els.push(...elByTag);
|
|
204
|
+
}
|
|
205
|
+
return els;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Returns first element containing given text or undefined,
|
|
210
|
+
* either of the whole document (if driver is given) or of the given parent element.
|
|
211
|
+
*
|
|
212
|
+
* Test in browser console: `$x("//*[contains(text(), 'Hello')]")`
|
|
213
|
+
*/
|
|
214
|
+
export async function getElementContainingText(driverOrParent: WebDriver | WebElement, text: string): Promise<WebElement | undefined> {
|
|
215
|
+
try {
|
|
216
|
+
// cf. https://stackoverflow.com/questions/3655549/xpath-containstext-some-string-doesnt-work-when-used-with-node-with-more
|
|
217
|
+
//const el = await driver.findElement(By.xpath(`//*[contains(text(), "${text}")]`));
|
|
218
|
+
const el = await driverOrParent.findElement(By.xpath(`//*[text()[contains(., '${text}')]]`));
|
|
219
|
+
return el;
|
|
220
|
+
} catch (error) {
|
|
221
|
+
return undefined;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Returns the bounding client rect (the bounding box relative to the viewport)
|
|
228
|
+
* of an element (found by its id attribute) by means of a JavaScript call.
|
|
229
|
+
*/
|
|
230
|
+
export async function getBoundingClientRect(driver: WebDriver, id: string): Promise<DOMRect> {
|
|
231
|
+
const bcr = await driver.executeScript('return document.getElementById("' + id + '").getBoundingClientRect()');
|
|
232
|
+
return bcr as DOMRect;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Returns all {@link getBoundingClientRect} values of the elements
|
|
237
|
+
* found in the given element list.
|
|
238
|
+
* The element list is computed by means of JavaScript code,
|
|
239
|
+
* e.g. `document.getElementsByClassName('grid').item(0).children`.
|
|
240
|
+
*
|
|
241
|
+
* @param elementList JavaScript code to determine the element list
|
|
242
|
+
*/
|
|
243
|
+
export async function getBoundingClientRects(driver: WebDriver, jsElementList: string): Promise<DOMRect[]> {
|
|
244
|
+
const bcr = await driver.executeScript(
|
|
245
|
+
`return (()=>{
|
|
246
|
+
const elements = ${jsElementList};
|
|
247
|
+
const bounds=[];
|
|
248
|
+
for (let el of elements) { bounds.push(el.getBoundingClientRect()) }
|
|
249
|
+
return bounds;
|
|
250
|
+
})()`) as DOMRect[];
|
|
251
|
+
return bcr;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Returns the node content's rect, in particular of the text, of a element with the given id.
|
|
256
|
+
*/
|
|
257
|
+
export async function getNodeContentsRect(driver: WebDriver, id: string): Promise<DOMRect> {
|
|
258
|
+
const bcr = await driver.executeScript(
|
|
259
|
+
`return (() => {
|
|
260
|
+
const range = document.createRange();
|
|
261
|
+
range.selectNodeContents(document.getElementById("${id}"));
|
|
262
|
+
return range.getClientRects()[0]
|
|
263
|
+
})()`) as DOMRect;
|
|
264
|
+
return bcr;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Returns the node content's rect, in particular of the text, of multiple elements
|
|
269
|
+
* (selected via evalutation of the jsElementString).
|
|
270
|
+
*/
|
|
271
|
+
export async function getNodeContentsRects(driver: WebDriver, jsElementList: string): Promise<DOMRect[]> {
|
|
272
|
+
const bcr = await driver.executeScript(
|
|
273
|
+
`return (() => {
|
|
274
|
+
const elements = ${jsElementList};
|
|
275
|
+
const bounds=[];
|
|
276
|
+
for (let el of elements) {
|
|
277
|
+
const range = document.createRange();
|
|
278
|
+
range.selectNodeContents(el);
|
|
279
|
+
bounds.push(range.getClientRects()[0])
|
|
280
|
+
}
|
|
281
|
+
return bounds
|
|
282
|
+
})()`) as DOMRect[];
|
|
283
|
+
return bcr;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Returns the rects using the driver API.
|
|
288
|
+
*/
|
|
289
|
+
export async function getRects(driver: WebDriver, locator: Locator): Promise<Bounds[]> {
|
|
290
|
+
const elements = await driver.findElements(locator);
|
|
291
|
+
const bounds: Bounds[] = [];
|
|
292
|
+
for (const element of elements) {
|
|
293
|
+
const rect = await element.getRect();
|
|
294
|
+
bounds.push({
|
|
295
|
+
top: rect.x, left: rect.y,
|
|
296
|
+
bottom: rect.x + rect.width, right: rect.y + rect.height
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
return bounds;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Returns the rects of the first child (of each found element) using the driver API.
|
|
304
|
+
*/
|
|
305
|
+
export async function getRectsOfContent(driver: WebDriver, locator: Locator): Promise<Bounds[]> {
|
|
306
|
+
const elements = await driver.findElements(locator);
|
|
307
|
+
const bounds: Bounds[] = [];
|
|
308
|
+
for (const element of elements) {
|
|
309
|
+
const rect = await element.getRect();
|
|
310
|
+
bounds.push({
|
|
311
|
+
top: rect.x, left: rect.y,
|
|
312
|
+
bottom: rect.x + rect.width, right: rect.y + rect.height
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
return bounds;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* Returns all text content of the elements
|
|
323
|
+
* found in the given element list.
|
|
324
|
+
* The elements list is computed by means of JavaScript code,
|
|
325
|
+
* e.g. `document.getElementsByClassName('grid').item(0).children`.
|
|
326
|
+
*
|
|
327
|
+
* This is useful in combination with {@link getBoundingClientRects} in
|
|
328
|
+
* order to describe the items.
|
|
329
|
+
*
|
|
330
|
+
* @param elementList JavaScript code to determine the element list
|
|
331
|
+
*/
|
|
332
|
+
export async function getTextContents(driver: WebDriver, jsElementList: string): Promise<string[]> {
|
|
333
|
+
const tcs = await driver.executeScript(
|
|
334
|
+
`const elements = ${jsElementList};
|
|
335
|
+
const tcs=[];
|
|
336
|
+
for (let el of elements) { tcs.push(el.textContent) }
|
|
337
|
+
return tcs;
|
|
338
|
+
`) as string[];
|
|
339
|
+
return tcs;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* Returns the text content of the whole document.
|
|
344
|
+
*/
|
|
345
|
+
export async function getDocumentTextContent(driver: WebDriver): Promise<string> {
|
|
346
|
+
return await driver.executeScript(`return document.documentElement.textContent`)
|
|
347
|
+
};
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* Returns all computed styles of the elements
|
|
351
|
+
* found in the given element list.
|
|
352
|
+
* The elements list is computed by means of JavaScript code,
|
|
353
|
+
* e.g. `document.getElementsByClassName('grid').item(0).children`.
|
|
354
|
+
*
|
|
355
|
+
* @param elementList JavaScript code to determine the element list
|
|
356
|
+
*/
|
|
357
|
+
export async function getComputedStyles(driver: WebDriver, jsElementList: string, style: string): Promise<string[]> {
|
|
358
|
+
const tcs = await driver.executeScript(
|
|
359
|
+
`const elements = ${jsElementList};
|
|
360
|
+
const tcs=[];
|
|
361
|
+
for (let el of elements) { tcs.push(getComputedStyle(el).${style}) }
|
|
362
|
+
return tcs;
|
|
363
|
+
`) as string[];
|
|
364
|
+
return tcs;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
|
|
368
|
+
/**
|
|
369
|
+
* Returns the inner size of the windows bounding client rect (the bounding box relative to the viewport)
|
|
370
|
+
* of an element (found by its is attribute) by means of a JavaScript call.
|
|
371
|
+
* The window size contains the scrollbars. I.e. it is a bit larger than {@link getViewPortSize}.
|
|
372
|
+
*/
|
|
373
|
+
export async function getInnerSizeOfWindow(driver: WebDriver): Promise<{ width: number, height: number }> {
|
|
374
|
+
const w = await driver.executeScript('return window.innerWidth') as number;
|
|
375
|
+
const h = await driver.executeScript('return window.innerHeight') as number;
|
|
376
|
+
return { width: w, height: h };
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/**
|
|
380
|
+
* Returns the viewport size, which is the inner window size but not with the scrollbars.
|
|
381
|
+
* I.e. the viewport size is a bit smaller than {@link getInnerSizeOfWindow} (if scrollbars are visible).
|
|
382
|
+
* The value is retrieved by means of JavaScript.
|
|
383
|
+
*/
|
|
384
|
+
export async function getViewPortSize(driver: WebDriver): Promise<{ width: number, height: number }> {
|
|
385
|
+
const [clientWidthOfHTML, clientHeightOfHTML] = await Promise.all([
|
|
386
|
+
driver.executeScript("return document.documentElement.clientWidth"),
|
|
387
|
+
driver.executeScript("return document.documentElement.clientHeight")]) as number[];
|
|
388
|
+
return { width: clientWidthOfHTML, height: clientHeightOfHTML };
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
/**
|
|
392
|
+
* Returns true if the element identified by its id attribute is with the view port.
|
|
393
|
+
* That does not necessarily means that it is visible, since its display or visible attribute may
|
|
394
|
+
* hide the element.
|
|
395
|
+
*/
|
|
396
|
+
export async function isInViewPort(driver: WebDriver, id: string) {
|
|
397
|
+
const innerSize = await getInnerSizeOfWindow(driver);
|
|
398
|
+
const [x, y] = await Promise.all([
|
|
399
|
+
driver.executeScript('return window.scrollX'),
|
|
400
|
+
driver.executeScript('return window.scrollY')]) as number[];
|
|
401
|
+
const bounds = await getBoundingClientRect(driver, id);
|
|
402
|
+
return bounds.x + bounds.width >= x && bounds.y + bounds.height >= y
|
|
403
|
+
&& bounds.x < x + innerSize.width && bounds.y < y + innerSize.height
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
/**
|
|
407
|
+
* Sets the window so that the {@link getViewPortSize} matches the given width and height.
|
|
408
|
+
*/
|
|
409
|
+
export async function setBrowserWindowSize(driver: WebDriver, width: number, height: number): Promise<void> {
|
|
410
|
+
// const rect = await driver.manage().window().getRect();
|
|
411
|
+
await waitForSetRect(driver, width, height);
|
|
412
|
+
// await driver.manage().window().setRect({ width: width, height: height });
|
|
413
|
+
// return waitFor(async () => {
|
|
414
|
+
// const rectAfter = await driver.manage().window().getRect();
|
|
415
|
+
|
|
416
|
+
// if (check && (rectAfter.width != width || rectAfter.height != height)) {
|
|
417
|
+
// throw new Error(`Cannot set window to size (${width}, ${height}). Size is probably too small or too large, got ${rectAfter.width}/${rectAfter.height}, was before ${rect.width}/${rect.height}.`);
|
|
418
|
+
// }
|
|
419
|
+
// });
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
async function waitForSetRect(driver: WebDriver, width: number, height: number): Promise<void> {
|
|
423
|
+
const rectBefore = await driver.manage().window().getRect();
|
|
424
|
+
|
|
425
|
+
await driver.manage().window().setRect({ width: width, height: height });
|
|
426
|
+
return waitFor(async () => {
|
|
427
|
+
const rectAfter = await driver.manage().window().getRect();
|
|
428
|
+
if (rectAfter.width != width || rectAfter.height != height) {
|
|
429
|
+
throw new Error(`Cannot set window to size (${width}, ${height}). Size is probably too small or too large, got ${rectAfter.width}/${rectAfter.height}, was before ${rectBefore.width}/${rectBefore.height}.`);
|
|
430
|
+
}
|
|
431
|
+
});
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
/**
|
|
435
|
+
* Sets the window so that the {@link getViewPortSize} matches the given width and height. Warning: This does not work reliably.
|
|
436
|
+
*/
|
|
437
|
+
export async function setViewPortSize(driver: WebDriver, width: number, height: number): Promise<void> {
|
|
438
|
+
const rect = await driver.manage().window().getRect();
|
|
439
|
+
const innerBefore = await getInnerSizeOfWindow(driver);
|
|
440
|
+
const deltaX = rect.width - innerBefore.width;
|
|
441
|
+
const deltaY = rect.height - innerBefore.height;
|
|
442
|
+
await waitForSetRect(driver, width + deltaX, height + deltaY);
|
|
443
|
+
|
|
444
|
+
const [innerAfter, viewport] = await Promise.all([getInnerSizeOfWindow(driver), getViewPortSize(driver)]);
|
|
445
|
+
|
|
446
|
+
// adjust scrollbars if necessary
|
|
447
|
+
const sbHeight = innerAfter.height - viewport.height;
|
|
448
|
+
const sbWidth = innerAfter.width - viewport.width;
|
|
449
|
+
await waitForSetRect(driver, width + deltaX + sbWidth, height + deltaY + sbHeight);
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
export async function getInheritedBackgroundColorsByJS(driver: WebDriver, nodelist: string) {
|
|
453
|
+
const colors = await driver.executeScript(`
|
|
454
|
+
const list = ${nodelist};
|
|
455
|
+
function getDefaultBackground() {
|
|
456
|
+
var div = document.createElement("div")
|
|
457
|
+
document.head.appendChild(div)
|
|
458
|
+
var bg = getComputedStyle(div).backgroundColor
|
|
459
|
+
document.head.removeChild(div)
|
|
460
|
+
return bg
|
|
461
|
+
}
|
|
462
|
+
const defaultStyle = getDefaultBackground()
|
|
463
|
+
function getInheritedBackgroundColor(el) {
|
|
464
|
+
var backgroundColor = getComputedStyle(el).backgroundColor
|
|
465
|
+
if (backgroundColor != defaultStyle) return backgroundColor
|
|
466
|
+
if (!el.parentElement) return defaultStyle
|
|
467
|
+
return getInheritedBackgroundColor(el.parentElement)
|
|
468
|
+
}
|
|
469
|
+
const colors = []
|
|
470
|
+
for (let e of list) {
|
|
471
|
+
colors.push(getInheritedBackgroundColor(e));
|
|
472
|
+
}
|
|
473
|
+
return colors;
|
|
474
|
+
`) as string[];
|
|
475
|
+
return colors;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
export async function getInheritedColorsByJS(driver: WebDriver, nodelist: string) {
|
|
479
|
+
const colors = await driver.executeScript(`
|
|
480
|
+
const list = ${nodelist};
|
|
481
|
+
function getDefaultColor() {
|
|
482
|
+
var div = document.createElement("div")
|
|
483
|
+
document.head.appendChild(div)
|
|
484
|
+
var bg = getComputedStyle(div).color
|
|
485
|
+
document.head.removeChild(div)
|
|
486
|
+
return bg
|
|
487
|
+
}
|
|
488
|
+
const defaultStyle = getDefaultColor()
|
|
489
|
+
function getInheritedColor(el) {
|
|
490
|
+
var color = getComputedStyle(el).color
|
|
491
|
+
if (color != defaultStyle) return color
|
|
492
|
+
if (!el.parentElement) return defaultStyle
|
|
493
|
+
return getInheritedColor(el.parentElement)
|
|
494
|
+
}
|
|
495
|
+
const colors = []
|
|
496
|
+
for (let e of list) {
|
|
497
|
+
colors.push(getInheritedColor(e));
|
|
498
|
+
}
|
|
499
|
+
return colors;
|
|
500
|
+
`) as string[];
|
|
501
|
+
return colors;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
function rgb2hex(rgba: string) {
|
|
505
|
+
return "#" +
|
|
506
|
+
rgba.match(/^rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*(\d+\.{0,1}\d*))?\)$/)!
|
|
507
|
+
.slice(1)
|
|
508
|
+
.map((n, i) => (i === 3
|
|
509
|
+
? Math.round(parseFloat(n) * 255)
|
|
510
|
+
: parseFloat(n)).toString(16).padStart(2, '0').replace('NaN', ''))
|
|
511
|
+
.join('');
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
export async function getAllTextNodes(driver: WebDriver): Promise<{ text: string; color: string; background: string; line: string; }[]> {
|
|
515
|
+
const res = await driver.executeScript(`
|
|
516
|
+
|
|
517
|
+
const TRANSPARENT = "rgba(0, 0, 0, 0)";
|
|
518
|
+
const all = [];
|
|
519
|
+
const view = document.defaultView;
|
|
520
|
+
function collect(e) {
|
|
521
|
+
const styles = view.getComputedStyle(e);
|
|
522
|
+
const pStyles = view.getComputedStyle(e.parentElement);
|
|
523
|
+
const childNodes = e.childNodes;
|
|
524
|
+
for (let i=0; i<childNodes.length; i++) {
|
|
525
|
+
const node = childNodes.item(i);
|
|
526
|
+
if (node instanceof Element) {
|
|
527
|
+
collect(node);
|
|
528
|
+
} else if (node instanceof Text && node.textContent.trim().length>0) {
|
|
529
|
+
all.push({
|
|
530
|
+
text: node.textContent.trim(),
|
|
531
|
+
color: styles?.color!==TRANSPARENT?styles?.color:pStyles?.color,
|
|
532
|
+
background: styles?.backgroundColor!==TRANSPARENT?styles?.backgroundColor:pStyles?.backgroundColor,
|
|
533
|
+
line: styles?.textDecorationLine==='none'?pStyles?.textDecorationLine:styles?.textDecorationLine
|
|
534
|
+
});
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
collect(document.body);
|
|
539
|
+
|
|
540
|
+
|
|
541
|
+
return all;
|
|
542
|
+
`) as { text: string, color: string, background: string, line: string }[];
|
|
543
|
+
|
|
544
|
+
res.forEach(e => {
|
|
545
|
+
e.color = rgb2hex(e.color);
|
|
546
|
+
e.background = rgb2hex(e.background);
|
|
547
|
+
});
|
|
548
|
+
|
|
549
|
+
|
|
550
|
+
return res;
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
/**
|
|
554
|
+
* Retrieves all buttons (element button or input with property submit or button) or anchors with href.
|
|
555
|
+
* @param driver
|
|
556
|
+
* @returns
|
|
557
|
+
*/
|
|
558
|
+
export async function getButtonsAndHRefs(driver: WebDriver): Promise<WebElement[]> {
|
|
559
|
+
const elements: WebElement[] = await execScript(driver,
|
|
560
|
+
"execScripts/getButtonsAndHRefs.js", "getButtonsAndHRefs");
|
|
561
|
+
return elements;
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
/**
|
|
565
|
+
* Retrieves a button or link with the given text.
|
|
566
|
+
* The text is matched case insensitive and may be a substring of the button/link text.
|
|
567
|
+
* If there are multiple matches, the best one is returned using {@link findBestMatchingIndex}.
|
|
568
|
+
* @param driver
|
|
569
|
+
* @param variations Variations in order of priority
|
|
570
|
+
* @returns the matching element or undefined
|
|
571
|
+
*/
|
|
572
|
+
export async function findButtonOrHRefWithText(driver: WebDriver, variations: string[]): Promise<WebElement | undefined> {
|
|
573
|
+
const clickableElements = (await getButtonsAndHRefs(driver))
|
|
574
|
+
const candidates: WebElement[] = [];
|
|
575
|
+
const texts: string[] = [];
|
|
576
|
+
for (const el of clickableElements) {
|
|
577
|
+
const text = (await el.getText());
|
|
578
|
+
texts.push(text);
|
|
579
|
+
candidates.push(el);
|
|
580
|
+
}
|
|
581
|
+
if (candidates.length === 1) {
|
|
582
|
+
return candidates[0];
|
|
583
|
+
} else {
|
|
584
|
+
const index = findBestMatchingIndex(texts, variations);
|
|
585
|
+
if (index >= 0) {
|
|
586
|
+
return candidates[index];
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
return undefined;
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
/**
|
|
593
|
+
* Returns a submit button or any other button (if onlySubmit is false).
|
|
594
|
+
* If there is only one submit button, it is returned.
|
|
595
|
+
* If there are multiple (submit) buttons, the best one is returned using {@link findBestMatchingIndex}.
|
|
596
|
+
* If there is no best matching text, the position hint is used.
|
|
597
|
+
* If the position hint is less 0 or greater than the number of submit buttons, the first one is returned.
|
|
598
|
+
* @param textCandidates
|
|
599
|
+
* @param posHint
|
|
600
|
+
* @param onlySubmit only submit buttons are considered, otherwise, all buttons are considered
|
|
601
|
+
* @returns a button or undefined
|
|
602
|
+
*/
|
|
603
|
+
export async function findSubmitButton(driver: WebDriver, textCandidates: string[], posHint = -1, onlySubmit = true): Promise<WebElement | undefined> {
|
|
604
|
+
const clickableElements = (await getButtonsAndHRefs(driver));
|
|
605
|
+
const candidates: WebElement[] = [];
|
|
606
|
+
const onlySubmitCandidates: WebElement[] = [];
|
|
607
|
+
for (const el of clickableElements) {
|
|
608
|
+
const type = (await el.getAttribute("type"))?.toLowerCase();
|
|
609
|
+
const tag = (await el.getTagName())?.toLowerCase();
|
|
610
|
+
if (tag === "button"|| type == "submit" || type == "button") {
|
|
611
|
+
if (type === "submit" || !onlySubmit) {
|
|
612
|
+
candidates.push(el);
|
|
613
|
+
}
|
|
614
|
+
if (type === "submit") {
|
|
615
|
+
onlySubmitCandidates.push(el);
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
if (candidates.length <= 1) {
|
|
620
|
+
return candidates[0]; // maybe undefined
|
|
621
|
+
}
|
|
622
|
+
if (onlySubmitCandidates.length === 1) {
|
|
623
|
+
return onlySubmitCandidates[0];
|
|
624
|
+
}
|
|
625
|
+
const texts: string[] = [];
|
|
626
|
+
for (const el of candidates) {
|
|
627
|
+
const text = await el.getText();
|
|
628
|
+
texts.push(text);
|
|
629
|
+
}
|
|
630
|
+
const index = findBestMatchingIndex(texts, textCandidates);
|
|
631
|
+
if (index >= 0) {
|
|
632
|
+
return candidates[index];
|
|
633
|
+
}
|
|
634
|
+
if (posHint >= 0 && posHint < candidates.length) {
|
|
635
|
+
return candidates[posHint];
|
|
636
|
+
}
|
|
637
|
+
return candidates[0]; // maybe undefined
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
/**
|
|
641
|
+
* Scrolls to the element and clicks it.
|
|
642
|
+
* If element is not defined, an error is thrown.
|
|
643
|
+
*/
|
|
644
|
+
export async function scrollIntoViewAndClick(element: WebElement) {
|
|
645
|
+
if (!element) {
|
|
646
|
+
throw new Error("Element is undefined");
|
|
647
|
+
}
|
|
648
|
+
const driver = element.getDriver();
|
|
649
|
+
await driver.executeScript("arguments[0].scrollIntoView(true);", element);
|
|
650
|
+
await new Promise(r => setTimeout(r, 700)); // let the page update, this should be super fast
|
|
651
|
+
await element.click();
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
/**
|
|
655
|
+
* Returns best matching candidate index or -1 if none matches.
|
|
656
|
+
* Best matching means that the text of the candidate is either
|
|
657
|
+
* - equal to one of the variations
|
|
658
|
+
* - equal to one of the variations ignoring case
|
|
659
|
+
* - contains one of the variations
|
|
660
|
+
* - contains one of the variations ignoring case
|
|
661
|
+
* - has the smallest edit distance to one of the variations
|
|
662
|
+
* (lower case, only variations and candidates with a length twice as long as maxDistance are considered
|
|
663
|
+
* @param candidates The candidates of which the index is to be returned
|
|
664
|
+
* @param variations The variations in order of priority
|
|
665
|
+
* @param maxDistance The maximum edit distance to be considered
|
|
666
|
+
*/
|
|
667
|
+
export function findBestMatchingIndex(candidates: string[], variations: string[], maxDistance = 2): number {
|
|
668
|
+
|
|
669
|
+
const lcVariations = variations.map(v => v.toLowerCase());
|
|
670
|
+
const lcCandidates = candidates.map(c => c.toLowerCase());
|
|
671
|
+
|
|
672
|
+
const minLength = maxDistance * 2;
|
|
673
|
+
|
|
674
|
+
const searches = [
|
|
675
|
+
() => candidates.findIndex(c => variations.includes(c)),
|
|
676
|
+
() => lcCandidates.findIndex(c => lcVariations.includes(c)),
|
|
677
|
+
() => candidates.findIndex(c => variations.some(v => c.includes(v))),
|
|
678
|
+
() => lcCandidates.findIndex(c => lcVariations.some(v => c.includes(v))),
|
|
679
|
+
() => {
|
|
680
|
+
let bestDist = Number.MAX_SAFE_INTEGER;
|
|
681
|
+
let bestIndex = -1;
|
|
682
|
+
for (const variation of variations) {
|
|
683
|
+
if (variation.length >= minLength) {
|
|
684
|
+
for (let index = 0; index < candidates.length; index++) {
|
|
685
|
+
const candidate = candidates[index];
|
|
686
|
+
if (candidate.length >= minLength) {
|
|
687
|
+
const dist = editDistance(candidate, variation, maxDistance);
|
|
688
|
+
if (dist <= 1) { // short cut
|
|
689
|
+
return index;
|
|
690
|
+
}
|
|
691
|
+
if (dist < bestDist && dist <= maxDistance) {
|
|
692
|
+
bestDist = dist;
|
|
693
|
+
bestIndex = index;
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
return bestIndex;
|
|
700
|
+
}
|
|
701
|
+
]
|
|
702
|
+
|
|
703
|
+
for (const search of searches) {
|
|
704
|
+
const index = search();
|
|
705
|
+
if (index >= 0) {
|
|
706
|
+
return index;
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
return -1;
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
/**
|
|
714
|
+
* Returns the best matching input element identified by the given label candidates.
|
|
715
|
+
* If the label is found, the input element should be either a child of that label or the attribute for and id should be used.
|
|
716
|
+
* If no such input element is found, the input element of the given type is found by position.
|
|
717
|
+
* If nothing is found, undefined is returned.
|
|
718
|
+
*
|
|
719
|
+
* @param labelCandidates The label candidates in order of priority
|
|
720
|
+
* @param typeHints The type (and tag) hints in order of priority, e.g. text, password, or textarea (or select, if findSelectAsWell is true)
|
|
721
|
+
* @param positionHint The position hint, e.g. 0 for first, 1 for second, etc.;
|
|
722
|
+
* -1 is default and means, the position hint is ignored and the first match
|
|
723
|
+
* (if label is not found) according to the type hint is used.
|
|
724
|
+
* The position is always counting all input elements without regard to the type.
|
|
725
|
+
* @package findSelectAsWell If true, select elements are also considered; otherwise, only input and textarea elements are considered
|
|
726
|
+
*/
|
|
727
|
+
export async function getInputElementByLabel(driver: WebDriver, labelCandidates: string[], typeHints: string[], positionHint = -1, findSelectAsWell = false) {
|
|
728
|
+
const tagsToFind = ["input", "textarea"];
|
|
729
|
+
if (findSelectAsWell) {
|
|
730
|
+
tagsToFind.push("select");
|
|
731
|
+
}
|
|
732
|
+
try {
|
|
733
|
+
const weLabels = await getElementsByTag(driver, "label");
|
|
734
|
+
if (weLabels.length >= 1) {
|
|
735
|
+
const txtLabels: string[] = [];
|
|
736
|
+
for (const weLabel of weLabels) {
|
|
737
|
+
try {
|
|
738
|
+
const text = await weLabel.getText();
|
|
739
|
+
txtLabels.push(text);
|
|
740
|
+
} catch (error) {
|
|
741
|
+
txtLabels.push("");
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
const idxCandidate = findBestMatchingIndex(txtLabels, labelCandidates);
|
|
745
|
+
if (idxCandidate >= 0) {
|
|
746
|
+
// field is child of label
|
|
747
|
+
try {
|
|
748
|
+
const inputFields = await getElementsByTags(weLabels[idxCandidate], tagsToFind);
|
|
749
|
+
if (inputFields.length > 0) {
|
|
750
|
+
return inputFields[0];
|
|
751
|
+
}
|
|
752
|
+
} catch (error) {
|
|
753
|
+
// expected
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
// for and id is used:
|
|
757
|
+
const forAttribute = (await weLabels[idxCandidate].getAttribute("for")) || "";
|
|
758
|
+
if (forAttribute !== "") {
|
|
759
|
+
const inputFields = await getElementsByTags(driver, tagsToFind);
|
|
760
|
+
for (const webElement of inputFields) {
|
|
761
|
+
const id = await webElement.getAttribute("id");
|
|
762
|
+
if (id === forAttribute) {
|
|
763
|
+
return webElement;
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
// try to find input field by type
|
|
771
|
+
const inputFields = await getElementsByTags(driver, tagsToFind);
|
|
772
|
+
if (inputFields.length >= 1) {
|
|
773
|
+
let pos = 0;
|
|
774
|
+
for (const webElement of inputFields) {
|
|
775
|
+
const type = (await webElement.getAttribute("type"))?.toLowerCase();
|
|
776
|
+
const tag = (await webElement.getTagName()).toLowerCase();
|
|
777
|
+
if (typeHints.includes(type) || typeHints.includes(tag)) {
|
|
778
|
+
if (pos >= positionHint) {
|
|
779
|
+
return webElement;
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
pos++;
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
} catch (error) {
|
|
786
|
+
console.log("Error in getInputElementByLabel", error);
|
|
787
|
+
}
|
|
788
|
+
return undefined;
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
// from https://github.com/tj/commander.js/blob/master/lib/suggestSimilar.js
|
|
792
|
+
|
|
793
|
+
|
|
794
|
+
function editDistance(a: string, b: string, maxDistance: number) {
|
|
795
|
+
// https://en.wikipedia.org/wiki/Damerau–Levenshtein_distance
|
|
796
|
+
// Calculating optimal string alignment distance, no substring is edited more than once.
|
|
797
|
+
// (Simple implementation.)
|
|
798
|
+
|
|
799
|
+
// Quick early exit, return worst case.
|
|
800
|
+
if (Math.abs(a.length - b.length) > maxDistance) return Math.max(a.length, b.length);
|
|
801
|
+
|
|
802
|
+
// distance between prefix substrings of a and b
|
|
803
|
+
const d = [];
|
|
804
|
+
|
|
805
|
+
// pure deletions turn a into empty string
|
|
806
|
+
for (let i = 0; i <= a.length; i++) {
|
|
807
|
+
d[i] = [i];
|
|
808
|
+
}
|
|
809
|
+
// pure insertions turn empty string into b
|
|
810
|
+
for (let j = 0; j <= b.length; j++) {
|
|
811
|
+
d[0][j] = j;
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
// fill matrix
|
|
815
|
+
for (let j = 1; j <= b.length; j++) {
|
|
816
|
+
for (let i = 1; i <= a.length; i++) {
|
|
817
|
+
let cost = 1;
|
|
818
|
+
if (a[i - 1] === b[j - 1]) {
|
|
819
|
+
cost = 0;
|
|
820
|
+
} else {
|
|
821
|
+
cost = 1;
|
|
822
|
+
}
|
|
823
|
+
d[i][j] = Math.min(
|
|
824
|
+
d[i - 1][j] + 1, // deletion
|
|
825
|
+
d[i][j - 1] + 1, // insertion
|
|
826
|
+
d[i - 1][j - 1] + cost // substitution
|
|
827
|
+
);
|
|
828
|
+
// transposition
|
|
829
|
+
if (i > 1 && j > 1 && a[i - 1] === b[j - 2] && a[i - 2] === b[j - 1]) {
|
|
830
|
+
d[i][j] = Math.min(d[i][j], d[i - 2][j - 2] + 1);
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
return d[a.length][b.length];
|
|
836
|
+
}
|