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