system-testing 1.0.35 → 1.0.37
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/build/index.js +3 -0
- package/{dist → build}/system-test-browser-helper.d.ts +2 -2
- package/build/system-test-browser-helper.js +238 -0
- package/build/system-test-communicator.js +105 -0
- package/build/system-test-http-server.js +66 -0
- package/{dist → build}/system-test.d.ts +15 -24
- package/build/system-test.js +696 -0
- package/build/use-system-test.js +79 -0
- package/package.json +10 -6
- package/dist/index.js +0 -3
- package/dist/index.js.map +0 -1
- package/dist/system-test-browser-helper.js +0 -238
- package/dist/system-test-browser-helper.js.map +0 -1
- package/dist/system-test-communicator.js +0 -105
- package/dist/system-test-communicator.js.map +0 -1
- package/dist/system-test-http-server.js +0 -66
- package/dist/system-test-http-server.js.map +0 -1
- package/dist/system-test.js +0 -705
- package/dist/system-test.js.map +0 -1
- package/dist/use-system-test.js +0 -79
- package/dist/use-system-test.js.map +0 -1
- /package/{dist → build}/index.d.ts +0 -0
- /package/{dist → build}/system-test-communicator.d.ts +0 -0
- /package/{dist → build}/system-test-http-server.d.ts +0 -0
- /package/{dist → build}/use-system-test.d.ts +0 -0
package/dist/system-test.js
DELETED
|
@@ -1,705 +0,0 @@
|
|
|
1
|
-
// @ts-check
|
|
2
|
-
import { Builder, By } from "selenium-webdriver";
|
|
3
|
-
import chrome from "selenium-webdriver/chrome.js";
|
|
4
|
-
import { digg } from "diggerize";
|
|
5
|
-
import fs from "node:fs/promises";
|
|
6
|
-
import logging from "selenium-webdriver/lib/logging.js";
|
|
7
|
-
import moment from "moment";
|
|
8
|
-
import { prettify } from "htmlfy";
|
|
9
|
-
import Server from "scoundrel-remote-eval/build/server/index.js";
|
|
10
|
-
import ServerWebSocket from "scoundrel-remote-eval/build/server/connections/web-socket/index.js";
|
|
11
|
-
import SystemTestCommunicator from "./system-test-communicator.js";
|
|
12
|
-
import SystemTestHttpServer from "./system-test-http-server.js";
|
|
13
|
-
import { wait, waitFor } from "awaitery";
|
|
14
|
-
import { WebSocketServer } from "ws";
|
|
15
|
-
class ElementNotFoundError extends Error {
|
|
16
|
-
}
|
|
17
|
-
class SystemTest {
|
|
18
|
-
/**
|
|
19
|
-
* Gets the current system test instance
|
|
20
|
-
* @param {object} [args]
|
|
21
|
-
* @param {string} [args.host]
|
|
22
|
-
* @param {number} [args.port]
|
|
23
|
-
* @returns {SystemTest}
|
|
24
|
-
*/
|
|
25
|
-
static current(args) {
|
|
26
|
-
if (!globalThis.systemTest) {
|
|
27
|
-
globalThis.systemTest = new SystemTest(args);
|
|
28
|
-
}
|
|
29
|
-
return globalThis.systemTest;
|
|
30
|
-
}
|
|
31
|
-
getCommunicator() {
|
|
32
|
-
if (!this.communicator) {
|
|
33
|
-
throw new Error("Communicator hasn't been initialized yet");
|
|
34
|
-
}
|
|
35
|
-
return this.communicator;
|
|
36
|
-
}
|
|
37
|
-
/**
|
|
38
|
-
* Runs a system test
|
|
39
|
-
* @param {function(SystemTest): Promise<void>} callback
|
|
40
|
-
* @returns {Promise<void>}
|
|
41
|
-
*/
|
|
42
|
-
static async run(callback) {
|
|
43
|
-
const systemTest = this.current();
|
|
44
|
-
await systemTest.getCommunicator().sendCommand({ type: "initialize" });
|
|
45
|
-
await systemTest.dismissTo(SystemTest.rootPath);
|
|
46
|
-
try {
|
|
47
|
-
await systemTest.findByTestID("blankText", { useBaseSelector: false });
|
|
48
|
-
await callback(systemTest);
|
|
49
|
-
}
|
|
50
|
-
catch (error) {
|
|
51
|
-
await systemTest.takeScreenshot();
|
|
52
|
-
throw error;
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
/**
|
|
56
|
-
* Creates a new SystemTest instance
|
|
57
|
-
* @param {object} [args]
|
|
58
|
-
* @param {string} [args.host]
|
|
59
|
-
* @param {number} [args.port]
|
|
60
|
-
*/
|
|
61
|
-
constructor({ host = "localhost", port = 8081, ...restArgs } = { host: "localhost", port: 8081 }) {
|
|
62
|
-
/** @type {SystemTestCommunicator | undefined} */
|
|
63
|
-
this.communicator = undefined;
|
|
64
|
-
/** @type {import("selenium-webdriver").WebDriver | undefined} */
|
|
65
|
-
this.driver = undefined;
|
|
66
|
-
this._started = false;
|
|
67
|
-
this._driverTimeouts = 5000;
|
|
68
|
-
this._timeouts = 5000;
|
|
69
|
-
/**
|
|
70
|
-
* Handles a command received from the browser
|
|
71
|
-
* @param {{data: {message: string, backtrace: string, type: string, value: any[]}}} args
|
|
72
|
-
* @returns {Promise<any>}
|
|
73
|
-
*/
|
|
74
|
-
this.onCommandReceived = async ({ data }) => {
|
|
75
|
-
const type = data.type;
|
|
76
|
-
let result;
|
|
77
|
-
if (type == "console.error") {
|
|
78
|
-
const errorMessage = data.value[0];
|
|
79
|
-
let showMessage = true;
|
|
80
|
-
if (errorMessage.includes("Minified React error #419")) {
|
|
81
|
-
showMessage = false;
|
|
82
|
-
}
|
|
83
|
-
if (showMessage) {
|
|
84
|
-
console.error("Browser error", ...data.value);
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
else if (type == "console.log") {
|
|
88
|
-
console.log("Browser log", ...data.value);
|
|
89
|
-
}
|
|
90
|
-
else if (type == "error" || data.type == "unhandledrejection") {
|
|
91
|
-
this.handleError(data);
|
|
92
|
-
}
|
|
93
|
-
else if (this._onCommandCallback) {
|
|
94
|
-
result = await this._onCommandCallback({ data, type });
|
|
95
|
-
}
|
|
96
|
-
else {
|
|
97
|
-
console.error(`onWebSocketClientMessage unknown data (type ${type})`, data);
|
|
98
|
-
}
|
|
99
|
-
return result;
|
|
100
|
-
};
|
|
101
|
-
/**
|
|
102
|
-
* Handles a new web socket connection
|
|
103
|
-
* @param {WebSocket} ws
|
|
104
|
-
* @returns {Promise<void>}
|
|
105
|
-
*/
|
|
106
|
-
this.onWebSocketConnection = async (ws) => {
|
|
107
|
-
this.ws = ws;
|
|
108
|
-
this.getCommunicator().ws = ws;
|
|
109
|
-
this.getCommunicator().onOpen();
|
|
110
|
-
// @ts-expect-error
|
|
111
|
-
this.ws.on("error", digg(this, "communicator", "onError"));
|
|
112
|
-
// @ts-expect-error
|
|
113
|
-
this.ws.on("message", digg(this, "communicator", "onMessage"));
|
|
114
|
-
if (this.waitForClientWebSocketPromiseResolve) {
|
|
115
|
-
this.waitForClientWebSocketPromiseResolve();
|
|
116
|
-
delete this.waitForClientWebSocketPromiseResolve;
|
|
117
|
-
}
|
|
118
|
-
};
|
|
119
|
-
/**
|
|
120
|
-
* @returns {void}
|
|
121
|
-
*/
|
|
122
|
-
this.onWebSocketClose = () => {
|
|
123
|
-
this.ws = null;
|
|
124
|
-
this.getCommunicator().ws = null;
|
|
125
|
-
};
|
|
126
|
-
const restArgsKeys = Object.keys(restArgs);
|
|
127
|
-
if (restArgsKeys.length > 0) {
|
|
128
|
-
throw new Error(`Unknown arguments: ${restArgsKeys.join(", ")}`);
|
|
129
|
-
}
|
|
130
|
-
this._host = host;
|
|
131
|
-
this._port = port;
|
|
132
|
-
/** @type {Record<number, object>} */
|
|
133
|
-
this._responses = {};
|
|
134
|
-
this._sendCount = 0;
|
|
135
|
-
this.startScoundrel();
|
|
136
|
-
this.communicator = new SystemTestCommunicator({ onCommand: this.onCommandReceived });
|
|
137
|
-
}
|
|
138
|
-
/**
|
|
139
|
-
* Gets the base selector for scoping element searches
|
|
140
|
-
* @returns {string | undefined}
|
|
141
|
-
*/
|
|
142
|
-
getBaseSelector() { return this._baseSelector; }
|
|
143
|
-
/**
|
|
144
|
-
* @returns {import("selenium-webdriver").WebDriver}
|
|
145
|
-
*/
|
|
146
|
-
getDriver() {
|
|
147
|
-
if (!this)
|
|
148
|
-
throw new Error("No this?");
|
|
149
|
-
if (!this.driver)
|
|
150
|
-
throw new Error("Driver hasn't been initialized yet");
|
|
151
|
-
return this.driver;
|
|
152
|
-
}
|
|
153
|
-
/**
|
|
154
|
-
* Sets the base selector for scoping element searches
|
|
155
|
-
* @param {string} baseSelector
|
|
156
|
-
*/
|
|
157
|
-
setBaseSelector(baseSelector) { this._baseSelector = baseSelector; }
|
|
158
|
-
/**
|
|
159
|
-
* Gets a selector scoped to the base selector
|
|
160
|
-
* @param {string} selector
|
|
161
|
-
* @returns {string}
|
|
162
|
-
*/
|
|
163
|
-
getSelector(selector) {
|
|
164
|
-
return this.getBaseSelector() ? `${this.getBaseSelector()} ${selector}` : selector;
|
|
165
|
-
}
|
|
166
|
-
/**
|
|
167
|
-
* Starts Scoundrel server which the browser connects to for remote evaluation in the browser
|
|
168
|
-
* @returns {void}
|
|
169
|
-
*/
|
|
170
|
-
startScoundrel() {
|
|
171
|
-
if (this.wss)
|
|
172
|
-
throw new Error("Scoundrel server already started");
|
|
173
|
-
this.wss = new WebSocketServer({ port: 8090 });
|
|
174
|
-
this.serverWebSocket = new ServerWebSocket(this.wss);
|
|
175
|
-
this.server = new Server(this.serverWebSocket);
|
|
176
|
-
}
|
|
177
|
-
/**
|
|
178
|
-
* @returns {void}
|
|
179
|
-
*/
|
|
180
|
-
stopScoundrel() {
|
|
181
|
-
this.server?.close();
|
|
182
|
-
this.wss?.close();
|
|
183
|
-
}
|
|
184
|
-
/**
|
|
185
|
-
* Finds all elements by CSS selector
|
|
186
|
-
* @param {string} selector
|
|
187
|
-
* @param {object} args
|
|
188
|
-
* @param {number} [args.timeout]
|
|
189
|
-
* @param {boolean} [args.visible]
|
|
190
|
-
* @param {boolean} [args.useBaseSelector]
|
|
191
|
-
* @returns {Promise<import("selenium-webdriver").WebElement[]>}
|
|
192
|
-
*/
|
|
193
|
-
async all(selector, args = {}) {
|
|
194
|
-
const { visible = true, timeout, useBaseSelector = true, ...restArgs } = args;
|
|
195
|
-
const restArgsKeys = Object.keys(restArgs);
|
|
196
|
-
let actualTimeout;
|
|
197
|
-
if (timeout === undefined) {
|
|
198
|
-
actualTimeout = this._driverTimeouts;
|
|
199
|
-
}
|
|
200
|
-
else {
|
|
201
|
-
actualTimeout = timeout;
|
|
202
|
-
}
|
|
203
|
-
if (restArgsKeys.length > 0)
|
|
204
|
-
throw new Error(`Unknown arguments: ${restArgsKeys.join(", ")}`);
|
|
205
|
-
const actualSelector = useBaseSelector ? this.getSelector(selector) : selector;
|
|
206
|
-
const getElements = async () => await this.getDriver().findElements(By.css(actualSelector));
|
|
207
|
-
let elements = [];
|
|
208
|
-
if (actualTimeout == 0) {
|
|
209
|
-
elements = await getElements();
|
|
210
|
-
}
|
|
211
|
-
else {
|
|
212
|
-
await this.getDriver().wait(async () => {
|
|
213
|
-
elements = await getElements();
|
|
214
|
-
return elements.length > 0;
|
|
215
|
-
}, actualTimeout);
|
|
216
|
-
}
|
|
217
|
-
const activeElements = [];
|
|
218
|
-
for (const element of elements) {
|
|
219
|
-
let keep = true;
|
|
220
|
-
if (visible === true || visible === false) {
|
|
221
|
-
const isDisplayed = await element.isDisplayed();
|
|
222
|
-
if (visible && !isDisplayed)
|
|
223
|
-
keep = false;
|
|
224
|
-
if (!visible && isDisplayed)
|
|
225
|
-
keep = false;
|
|
226
|
-
}
|
|
227
|
-
if (keep)
|
|
228
|
-
activeElements.push(element);
|
|
229
|
-
}
|
|
230
|
-
return activeElements;
|
|
231
|
-
}
|
|
232
|
-
/**
|
|
233
|
-
* Clicks an element that has children which fills out the element and would otherwise have caused a ElementClickInterceptedError
|
|
234
|
-
* @param {string|import("selenium-webdriver").WebElement} elementOrIdentifier
|
|
235
|
-
* @returns {Promise<void>}
|
|
236
|
-
*/
|
|
237
|
-
async click(elementOrIdentifier) {
|
|
238
|
-
let tries = 0;
|
|
239
|
-
while (true) {
|
|
240
|
-
tries++;
|
|
241
|
-
try {
|
|
242
|
-
const element = await this._findElement(elementOrIdentifier);
|
|
243
|
-
const actions = this.getDriver().actions({ async: true });
|
|
244
|
-
await actions.move({ origin: element }).click().perform();
|
|
245
|
-
break;
|
|
246
|
-
}
|
|
247
|
-
catch (error) {
|
|
248
|
-
if (error instanceof Error) {
|
|
249
|
-
if (error.constructor.name === "ElementNotInteractableError") {
|
|
250
|
-
if (tries >= 3) {
|
|
251
|
-
throw new Error(`Element ${elementOrIdentifier.constructor.name} click failed after ${tries} tries - ${error.constructor.name}: ${error.message}`);
|
|
252
|
-
}
|
|
253
|
-
else {
|
|
254
|
-
await wait(50);
|
|
255
|
-
}
|
|
256
|
-
}
|
|
257
|
-
else {
|
|
258
|
-
// Re-throw with un-corrupted stack trace
|
|
259
|
-
throw new Error(`Element ${elementOrIdentifier.constructor.name} click failed - ${error.constructor.name}: ${error.message}`);
|
|
260
|
-
}
|
|
261
|
-
}
|
|
262
|
-
else {
|
|
263
|
-
throw new Error(`Element ${elementOrIdentifier.constructor.name} click failed - ${typeof error}: ${error}`);
|
|
264
|
-
}
|
|
265
|
-
}
|
|
266
|
-
}
|
|
267
|
-
}
|
|
268
|
-
/**
|
|
269
|
-
* Finds a single element by CSS selector
|
|
270
|
-
* @param {string} selector
|
|
271
|
-
* @param {object} args
|
|
272
|
-
* @returns {Promise<import("selenium-webdriver").WebElement>}
|
|
273
|
-
*/
|
|
274
|
-
async find(selector, args = {}) {
|
|
275
|
-
let elements = [];
|
|
276
|
-
try {
|
|
277
|
-
elements = await this.all(selector, args);
|
|
278
|
-
}
|
|
279
|
-
catch (error) {
|
|
280
|
-
// Re-throw to recover stack trace
|
|
281
|
-
if (error instanceof Error) {
|
|
282
|
-
if (error.message.startsWith("Wait timed out after")) {
|
|
283
|
-
elements = [];
|
|
284
|
-
}
|
|
285
|
-
throw new Error(`${error.message} (selector: ${this.getSelector(selector)})`);
|
|
286
|
-
}
|
|
287
|
-
else {
|
|
288
|
-
throw new Error(`${error} (selector: ${this.getSelector(selector)})`);
|
|
289
|
-
}
|
|
290
|
-
}
|
|
291
|
-
if (elements.length > 1) {
|
|
292
|
-
throw new Error(`More than 1 elements (${elements.length}) was found by CSS: ${this.getSelector(selector)}`);
|
|
293
|
-
}
|
|
294
|
-
if (!elements[0]) {
|
|
295
|
-
throw new ElementNotFoundError(`Element couldn't be found after ${(this.getTimeouts() / 1000).toFixed(2)}s by CSS: ${this.getSelector(selector)}`);
|
|
296
|
-
}
|
|
297
|
-
return elements[0];
|
|
298
|
-
}
|
|
299
|
-
/**
|
|
300
|
-
* Finds a single element by test ID
|
|
301
|
-
* @param {string} testID
|
|
302
|
-
* @param {object} args
|
|
303
|
-
* @returns {Promise<import("selenium-webdriver").WebElement>}
|
|
304
|
-
*/
|
|
305
|
-
async findByTestID(testID, args) { return await this.find(`[data-testid='${testID}']`, args); }
|
|
306
|
-
/**
|
|
307
|
-
* @param {string|import("selenium-webdriver").WebElement} elementOrIdentifier
|
|
308
|
-
* @returns {Promise<import("selenium-webdriver").WebElement>}
|
|
309
|
-
*/
|
|
310
|
-
async _findElement(elementOrIdentifier) {
|
|
311
|
-
let element;
|
|
312
|
-
if (typeof elementOrIdentifier == "string") {
|
|
313
|
-
element = await this.find(elementOrIdentifier);
|
|
314
|
-
}
|
|
315
|
-
else {
|
|
316
|
-
element = elementOrIdentifier;
|
|
317
|
-
}
|
|
318
|
-
return element;
|
|
319
|
-
}
|
|
320
|
-
/**
|
|
321
|
-
* Finds a single element by CSS selector without waiting
|
|
322
|
-
* @param {string} selector
|
|
323
|
-
* @param {object} [args]
|
|
324
|
-
* @returns {Promise<import("selenium-webdriver").WebElement>}
|
|
325
|
-
*/
|
|
326
|
-
async findNoWait(selector, args) {
|
|
327
|
-
await this.driverSetTimeouts(0);
|
|
328
|
-
try {
|
|
329
|
-
return await this.find(selector, args);
|
|
330
|
-
}
|
|
331
|
-
finally {
|
|
332
|
-
await this.restoreTimeouts();
|
|
333
|
-
}
|
|
334
|
-
}
|
|
335
|
-
/**
|
|
336
|
-
* Gets browser logs
|
|
337
|
-
* @returns {Promise<string[]>}
|
|
338
|
-
*/
|
|
339
|
-
async getBrowserLogs() {
|
|
340
|
-
const entries = await this.getDriver().manage().logs().get(logging.Type.BROWSER);
|
|
341
|
-
const browserLogs = [];
|
|
342
|
-
for (const entry of entries) {
|
|
343
|
-
const messageMatch = entry.message.match(/^(.+) (\d+):(\d+) (.+)$/);
|
|
344
|
-
let message;
|
|
345
|
-
if (messageMatch) {
|
|
346
|
-
message = messageMatch[4];
|
|
347
|
-
}
|
|
348
|
-
else {
|
|
349
|
-
message = entry.message;
|
|
350
|
-
}
|
|
351
|
-
browserLogs.push(`${entry.level.name}: ${message}`);
|
|
352
|
-
}
|
|
353
|
-
return browserLogs;
|
|
354
|
-
}
|
|
355
|
-
/**
|
|
356
|
-
* @returns {Promise<string>}
|
|
357
|
-
*/
|
|
358
|
-
async getCurrentUrl() {
|
|
359
|
-
return await this.getDriver().getCurrentUrl();
|
|
360
|
-
}
|
|
361
|
-
/**
|
|
362
|
-
* @returns {number}
|
|
363
|
-
*/
|
|
364
|
-
getTimeouts() { return this._timeouts; }
|
|
365
|
-
/**
|
|
366
|
-
* Interacts with an element by calling a method on it with the given arguments.
|
|
367
|
-
* Retrying on ElementNotInteractableError.
|
|
368
|
-
* @param {import("selenium-webdriver").WebElement|string} elementOrIdentifier - The element or a CSS selector to find the element.
|
|
369
|
-
* @param {string} methodName - The method name to call on the element.
|
|
370
|
-
* @param {...any} args - Arguments to pass to the method.
|
|
371
|
-
* @returns {Promise<any>}
|
|
372
|
-
*/
|
|
373
|
-
async interact(elementOrIdentifier, methodName, ...args) {
|
|
374
|
-
let tries = 0;
|
|
375
|
-
while (true) {
|
|
376
|
-
tries++;
|
|
377
|
-
const element = await this._findElement(elementOrIdentifier);
|
|
378
|
-
if (!element[methodName]) {
|
|
379
|
-
throw new Error(`${element.constructor.name} hasn't an attribute named: ${methodName}`);
|
|
380
|
-
}
|
|
381
|
-
else if (typeof element[methodName] != "function") {
|
|
382
|
-
throw new Error(`${element.constructor.name}#${methodName} is not a function`);
|
|
383
|
-
}
|
|
384
|
-
try {
|
|
385
|
-
// Dont call with candidate, because that will bind the function wrong.
|
|
386
|
-
return await element[methodName](...args);
|
|
387
|
-
}
|
|
388
|
-
catch (error) {
|
|
389
|
-
if (error instanceof Error) {
|
|
390
|
-
if (error.constructor.name === "ElementNotInteractableError") {
|
|
391
|
-
// Retry finding the element and interacting with it
|
|
392
|
-
if (tries >= 3) {
|
|
393
|
-
let elementDescription;
|
|
394
|
-
if (typeof elementOrIdentifier == "string") {
|
|
395
|
-
elementDescription = `CSS selector ${elementOrIdentifier}`;
|
|
396
|
-
}
|
|
397
|
-
else {
|
|
398
|
-
elementDescription = `${element.constructor.name}`;
|
|
399
|
-
}
|
|
400
|
-
throw new Error(`${elementDescription} ${methodName} failed after ${tries} tries - ${error.constructor.name}: ${error.message}`);
|
|
401
|
-
}
|
|
402
|
-
else {
|
|
403
|
-
await wait(50);
|
|
404
|
-
}
|
|
405
|
-
}
|
|
406
|
-
else {
|
|
407
|
-
// Re-throw with un-corrupted stack trace
|
|
408
|
-
throw new Error(`${element.constructor.name} ${methodName} failed - ${error.constructor.name}: ${error.message}`);
|
|
409
|
-
}
|
|
410
|
-
}
|
|
411
|
-
else {
|
|
412
|
-
throw new Error(`${element.constructor.name} ${methodName} failed - ${typeof error}: ${error}`);
|
|
413
|
-
}
|
|
414
|
-
}
|
|
415
|
-
}
|
|
416
|
-
}
|
|
417
|
-
/**
|
|
418
|
-
* Expects no element to be found by CSS selector
|
|
419
|
-
* @param {string} selector
|
|
420
|
-
* @returns {Promise<void>}
|
|
421
|
-
*/
|
|
422
|
-
async expectNoElement(selector) {
|
|
423
|
-
let found = false;
|
|
424
|
-
try {
|
|
425
|
-
await this.findNoWait(selector);
|
|
426
|
-
found = true;
|
|
427
|
-
}
|
|
428
|
-
catch (error) {
|
|
429
|
-
if (error instanceof Error && error.message.startsWith("Element couldn't be found after ")) {
|
|
430
|
-
// Ignore
|
|
431
|
-
}
|
|
432
|
-
else {
|
|
433
|
-
throw error;
|
|
434
|
-
}
|
|
435
|
-
}
|
|
436
|
-
if (found) {
|
|
437
|
-
throw new Error(`Expected not to find: ${selector}`);
|
|
438
|
-
}
|
|
439
|
-
}
|
|
440
|
-
/**
|
|
441
|
-
* @param {string} selector
|
|
442
|
-
* @param {object} args
|
|
443
|
-
* @param {boolean} [args.useBaseSelector]
|
|
444
|
-
* @returns {Promise<void>}
|
|
445
|
-
*/
|
|
446
|
-
async waitForNoSelector(selector, args) {
|
|
447
|
-
const { useBaseSelector, ...restArgs } = args;
|
|
448
|
-
if (Object.keys(restArgs).length > 0) {
|
|
449
|
-
throw new Error(`Unexpected args: ${Object.keys(restArgs).join(", ")}`);
|
|
450
|
-
}
|
|
451
|
-
const actualSelector = useBaseSelector ? this.getSelector(selector) : selector;
|
|
452
|
-
await this.getDriver().wait(async () => {
|
|
453
|
-
const elements = await this.getDriver().findElements(By.css(actualSelector));
|
|
454
|
-
// Not found at all
|
|
455
|
-
if (elements.length === 0) {
|
|
456
|
-
return true;
|
|
457
|
-
}
|
|
458
|
-
// Found but not visible
|
|
459
|
-
const isDisplayed = await elements[0].isDisplayed();
|
|
460
|
-
return !isDisplayed;
|
|
461
|
-
}, this.getTimeouts());
|
|
462
|
-
}
|
|
463
|
-
/**
|
|
464
|
-
* Gets notification messages
|
|
465
|
-
* @returns {Promise<string[]>}
|
|
466
|
-
*/
|
|
467
|
-
async notificationMessages() {
|
|
468
|
-
const notificationMessageElements = await this.all("[data-class='notification-message']", { useBaseSelector: false });
|
|
469
|
-
const notificationMessageTexts = [];
|
|
470
|
-
for (const notificationMessageElement of notificationMessageElements) {
|
|
471
|
-
const text = await notificationMessageElement.getText();
|
|
472
|
-
notificationMessageTexts.push(text);
|
|
473
|
-
}
|
|
474
|
-
return notificationMessageTexts;
|
|
475
|
-
}
|
|
476
|
-
/**
|
|
477
|
-
* Expects a notification message to appear and waits for it if necessary.
|
|
478
|
-
* @param {string} expectedNotificationMessage
|
|
479
|
-
* @returns {Promise<void>}
|
|
480
|
-
*/
|
|
481
|
-
async expectNotificationMessage(expectedNotificationMessage) {
|
|
482
|
-
/** @type {string[]} */
|
|
483
|
-
const allDetectedNotificationMessages = [];
|
|
484
|
-
let foundNotificationMessageElement;
|
|
485
|
-
await waitFor(async () => {
|
|
486
|
-
const notificationMessageElements = await this.all("[data-class='notification-message']", { useBaseSelector: false });
|
|
487
|
-
for (const notificationMessageElement of notificationMessageElements) {
|
|
488
|
-
const notificationMessage = await notificationMessageElement.getText();
|
|
489
|
-
if (!allDetectedNotificationMessages.includes(notificationMessage)) {
|
|
490
|
-
allDetectedNotificationMessages.push(notificationMessage);
|
|
491
|
-
}
|
|
492
|
-
if (notificationMessage == expectedNotificationMessage) {
|
|
493
|
-
foundNotificationMessageElement = notificationMessageElement;
|
|
494
|
-
return;
|
|
495
|
-
}
|
|
496
|
-
}
|
|
497
|
-
throw new Error(`Notification message ${expectedNotificationMessage} wasn't included in: ${allDetectedNotificationMessages.join(", ")}`);
|
|
498
|
-
});
|
|
499
|
-
if (foundNotificationMessageElement) {
|
|
500
|
-
await this.interact(foundNotificationMessageElement, "click"); // Dismiss the notification message
|
|
501
|
-
}
|
|
502
|
-
}
|
|
503
|
-
/**
|
|
504
|
-
* @returns {Promise<void>}
|
|
505
|
-
*/
|
|
506
|
-
async dismissNotificationMessages() {
|
|
507
|
-
const notificationMessageElements = await this.all("[data-class='notification-message']", { useBaseSelector: false });
|
|
508
|
-
for (const notificationMessageElement of notificationMessageElements) {
|
|
509
|
-
await this.interact(notificationMessageElement, "click");
|
|
510
|
-
}
|
|
511
|
-
await this.waitForNoSelector("[data-class='notification-message']", { useBaseSelector: false });
|
|
512
|
-
}
|
|
513
|
-
/**
|
|
514
|
-
* Indicates whether the system test has been started
|
|
515
|
-
* @returns {boolean}
|
|
516
|
-
*/
|
|
517
|
-
isStarted() { return this._started; }
|
|
518
|
-
/**
|
|
519
|
-
* Gets the HTML of the current page
|
|
520
|
-
* @returns {Promise<string>}
|
|
521
|
-
*/
|
|
522
|
-
async getHTML() { return await this.getDriver().getPageSource(); }
|
|
523
|
-
/**
|
|
524
|
-
* Starts the system test
|
|
525
|
-
* @returns {Promise<void>}
|
|
526
|
-
*/
|
|
527
|
-
async start() {
|
|
528
|
-
if (process.env.SYSTEM_TEST_HOST == "expo-dev-server") {
|
|
529
|
-
this.currentUrl = `http://${this._host}:${this._port}`;
|
|
530
|
-
}
|
|
531
|
-
else if (process.env.SYSTEM_TEST_HOST == "dist") {
|
|
532
|
-
this.currentUrl = `http://${this._host}:1984`;
|
|
533
|
-
this.systemTestHttpServer = new SystemTestHttpServer();
|
|
534
|
-
await this.systemTestHttpServer.start();
|
|
535
|
-
}
|
|
536
|
-
else {
|
|
537
|
-
throw new Error("Please set SYSTEM_TEST_HOST to 'expo-dev-server' or 'dist'");
|
|
538
|
-
}
|
|
539
|
-
const options = new chrome.Options();
|
|
540
|
-
options.addArguments("--disable-dev-shm-usage");
|
|
541
|
-
options.addArguments("--disable-gpu");
|
|
542
|
-
options.addArguments("--headless=new");
|
|
543
|
-
options.addArguments("--no-sandbox");
|
|
544
|
-
options.addArguments("--window-size=1920,1080");
|
|
545
|
-
this.driver = new Builder()
|
|
546
|
-
.forBrowser("chrome")
|
|
547
|
-
.setChromeOptions(options)
|
|
548
|
-
// @ts-expect-error
|
|
549
|
-
.setCapability("goog:loggingPrefs", { browser: "ALL" })
|
|
550
|
-
.build();
|
|
551
|
-
await this.setTimeouts(5000);
|
|
552
|
-
// Web socket server to communicate with browser
|
|
553
|
-
await this.startWebSocketServer();
|
|
554
|
-
// Visit the root page and wait for Expo to be loaded and the app to appear
|
|
555
|
-
await this.driverVisit(SystemTest.rootPath);
|
|
556
|
-
try {
|
|
557
|
-
await this.find("body > #root", { useBaseSelector: false });
|
|
558
|
-
await this.find("[data-testid='systemTestingComponent']", { visible: null, useBaseSelector: false });
|
|
559
|
-
}
|
|
560
|
-
catch (error) {
|
|
561
|
-
await this.takeScreenshot();
|
|
562
|
-
throw error;
|
|
563
|
-
}
|
|
564
|
-
// Wait for client to connect
|
|
565
|
-
await this.waitForClientWebSocket();
|
|
566
|
-
this._started = true;
|
|
567
|
-
this.setBaseSelector("[data-testid='systemTestingComponent'][data-focussed='true']");
|
|
568
|
-
}
|
|
569
|
-
/**
|
|
570
|
-
* Restores previously set timeouts
|
|
571
|
-
* @returns {Promise<void>}
|
|
572
|
-
*/
|
|
573
|
-
async restoreTimeouts() {
|
|
574
|
-
if (!this.getTimeouts()) {
|
|
575
|
-
throw new Error("Timeouts haven't previously been set");
|
|
576
|
-
}
|
|
577
|
-
await this.driverSetTimeouts(this.getTimeouts());
|
|
578
|
-
}
|
|
579
|
-
/**
|
|
580
|
-
* Sets driver timeouts
|
|
581
|
-
* @param {number} newTimeout
|
|
582
|
-
* @returns {Promise<void>}
|
|
583
|
-
*/
|
|
584
|
-
async driverSetTimeouts(newTimeout) {
|
|
585
|
-
this._driverTimeouts = newTimeout;
|
|
586
|
-
await this.getDriver().manage().setTimeouts({ implicit: newTimeout });
|
|
587
|
-
}
|
|
588
|
-
/**
|
|
589
|
-
* Sets timeouts and stores the previous timeouts
|
|
590
|
-
* @param {number} newTimeout
|
|
591
|
-
* @returns {Promise<void>}
|
|
592
|
-
*/
|
|
593
|
-
async setTimeouts(newTimeout) {
|
|
594
|
-
this._timeouts = newTimeout;
|
|
595
|
-
await this.restoreTimeouts();
|
|
596
|
-
}
|
|
597
|
-
/**
|
|
598
|
-
* Waits for the client web socket to connect
|
|
599
|
-
* @returns {Promise<void>}
|
|
600
|
-
*/
|
|
601
|
-
waitForClientWebSocket() {
|
|
602
|
-
return new Promise((resolve) => {
|
|
603
|
-
if (this.ws) {
|
|
604
|
-
resolve();
|
|
605
|
-
}
|
|
606
|
-
this.waitForClientWebSocketPromiseResolve = resolve;
|
|
607
|
-
});
|
|
608
|
-
}
|
|
609
|
-
/**
|
|
610
|
-
* Starts the web socket server
|
|
611
|
-
* @returns {void}
|
|
612
|
-
*/
|
|
613
|
-
startWebSocketServer() {
|
|
614
|
-
this.wss = new WebSocketServer({ port: 1985 });
|
|
615
|
-
this.wss.on("connection", this.onWebSocketConnection);
|
|
616
|
-
this.wss.on("close", this.onWebSocketClose);
|
|
617
|
-
}
|
|
618
|
-
/**
|
|
619
|
-
* Sets the on command callback
|
|
620
|
-
* @param {function({type: string, data: Record<string, any>}): Promise<void>} callback
|
|
621
|
-
* @returns {void}
|
|
622
|
-
*/
|
|
623
|
-
onCommand(callback) {
|
|
624
|
-
this._onCommandCallback = callback;
|
|
625
|
-
}
|
|
626
|
-
/**
|
|
627
|
-
* Handles an error reported from the browser
|
|
628
|
-
* @param {object} data
|
|
629
|
-
* @param {string} data.message
|
|
630
|
-
* @param {string} [data.backtrace]
|
|
631
|
-
* @returns {void}
|
|
632
|
-
*/
|
|
633
|
-
handleError(data) {
|
|
634
|
-
if (data.message.includes("Minified React error #419")) {
|
|
635
|
-
// Ignore this error message
|
|
636
|
-
return;
|
|
637
|
-
}
|
|
638
|
-
const error = new Error(`Browser error: ${data.message}`);
|
|
639
|
-
if (data.backtrace) {
|
|
640
|
-
error.stack = `${error.message}\n${data.backtrace}`;
|
|
641
|
-
}
|
|
642
|
-
console.error(error);
|
|
643
|
-
}
|
|
644
|
-
/**
|
|
645
|
-
* Stops the system test
|
|
646
|
-
* @returns {Promise<void>}
|
|
647
|
-
*/
|
|
648
|
-
async stop() {
|
|
649
|
-
this.stopScoundrel();
|
|
650
|
-
this.systemTestHttpServer?.close();
|
|
651
|
-
this.wss?.close();
|
|
652
|
-
await this.driver?.quit();
|
|
653
|
-
}
|
|
654
|
-
/**
|
|
655
|
-
* Visits a path in the browser
|
|
656
|
-
* @param {string} path
|
|
657
|
-
* @returns {Promise<void>}
|
|
658
|
-
*/
|
|
659
|
-
async driverVisit(path) {
|
|
660
|
-
const url = `${this.currentUrl}${path}`;
|
|
661
|
-
await this.getDriver().get(url);
|
|
662
|
-
}
|
|
663
|
-
/**
|
|
664
|
-
* Takes a screenshot, saves HTML and browser logs
|
|
665
|
-
* @returns {Promise<void>}
|
|
666
|
-
*/
|
|
667
|
-
async takeScreenshot() {
|
|
668
|
-
const path = `${process.cwd()}/tmp/screenshots`;
|
|
669
|
-
await fs.mkdir(path, { recursive: true });
|
|
670
|
-
const imageContent = await this.getDriver().takeScreenshot();
|
|
671
|
-
const now = new Date();
|
|
672
|
-
const screenshotPath = `${path}/${moment(now).format("YYYY-MM-DD-HH-MM-SS")}.png`;
|
|
673
|
-
const htmlPath = `${path}/${moment(now).format("YYYY-MM-DD-HH-MM-SS")}.html`;
|
|
674
|
-
const logsPath = `${path}/${moment(now).format("YYYY-MM-DD-HH-MM-SS")}.logs.txt`;
|
|
675
|
-
const logsText = await this.getBrowserLogs();
|
|
676
|
-
const html = await this.getHTML();
|
|
677
|
-
const htmlPretty = prettify(html);
|
|
678
|
-
await fs.writeFile(htmlPath, htmlPretty);
|
|
679
|
-
await fs.writeFile(logsPath, logsText.join("\n"));
|
|
680
|
-
await fs.writeFile(screenshotPath, imageContent, "base64");
|
|
681
|
-
console.log("Current URL:", await this.getCurrentUrl());
|
|
682
|
-
console.log("Logs:", logsPath);
|
|
683
|
-
console.log("Screenshot:", screenshotPath);
|
|
684
|
-
console.log("HTML:", htmlPath);
|
|
685
|
-
}
|
|
686
|
-
/**
|
|
687
|
-
* Visits a path in the browser
|
|
688
|
-
* @param {string} path
|
|
689
|
-
* @returns {Promise<void>}
|
|
690
|
-
*/
|
|
691
|
-
async visit(path) {
|
|
692
|
-
await this.getCommunicator().sendCommand({ type: "visit", path });
|
|
693
|
-
}
|
|
694
|
-
/**
|
|
695
|
-
* Dismisses to a path in the browser
|
|
696
|
-
* @param {string} path
|
|
697
|
-
* @returns {Promise<void>}
|
|
698
|
-
*/
|
|
699
|
-
async dismissTo(path) {
|
|
700
|
-
await this.getCommunicator().sendCommand({ type: "dismissTo", path });
|
|
701
|
-
}
|
|
702
|
-
}
|
|
703
|
-
SystemTest.rootPath = "/blank?systemTest=true";
|
|
704
|
-
export default SystemTest;
|
|
705
|
-
//# sourceMappingURL=system-test.js.map
|