system-testing 1.0.32 → 1.0.34

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,705 @@
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/src/server/index.js";
10
+ import ServerWebSocket from "scoundrel-remote-eval/src/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