system-testing 1.0.27 → 1.0.29

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "system-testing",
3
- "version": "1.0.27",
3
+ "version": "1.0.29",
4
4
  "description": "System testing with Selenium and browsers.",
5
5
  "keywords": [
6
6
  "system",
@@ -25,7 +25,7 @@
25
25
  "test": "echo \"Error: no test specified\" && exit 1"
26
26
  },
27
27
  "dependencies": {
28
- "awaitery": "^1.0.1",
28
+ "awaitery": "^1.0.2",
29
29
  "diggerize": "^1.0.9",
30
30
  "htmlfy": "^1.0.0",
31
31
  "mime": "^4.0.7",
@@ -1,3 +1,5 @@
1
+ // @ts-check
2
+
1
3
  import Client from "scoundrel-remote-eval/src/client/index.js"
2
4
  import ClientWebSocket from "scoundrel-remote-eval/src/client/connections/web-socket/index.js"
3
5
  import {digg} from "diggerize"
@@ -5,7 +7,10 @@ import EventEmitter from "events"
5
7
 
6
8
  import SystemTestCommunicator from "./system-test-communicator.js"
7
9
 
8
- const shared = {}
10
+ /** @type {{systemTestBrowserHelper: SystemTestBrowserHelper | null}} */
11
+ const shared = {
12
+ systemTestBrowserHelper: null
13
+ }
9
14
 
10
15
  export default class SystemTestBrowserHelper {
11
16
  static current() {
@@ -17,6 +22,7 @@ export default class SystemTestBrowserHelper {
17
22
  }
18
23
 
19
24
  constructor() {
25
+ // @ts-expect-error
20
26
  this.communicator = new SystemTestCommunicator({parent: this, onCommand: this.onCommand})
21
27
  this._enabled = false
22
28
  this.events = new EventEmitter()
@@ -39,10 +45,10 @@ export default class SystemTestBrowserHelper {
39
45
  waitForScoundrelStarted() {
40
46
  return new Promise((resolve) => {
41
47
  if (this.scoundrelClient) {
42
- resolve()
48
+ resolve(undefined)
43
49
  } else {
44
50
  this.events.once("scoundrelStarted", () => {
45
- resolve()
51
+ resolve(undefined)
46
52
  })
47
53
  }
48
54
  })
@@ -76,14 +82,24 @@ export default class SystemTestBrowserHelper {
76
82
  type: "unhandledrejection",
77
83
  error: event.reason,
78
84
  errorClass: "UnhandledRejection",
79
- file: null,
80
- line: null,
81
85
  message: event.reason.message || event.reason || "Unhandled promise rejection without a message",
82
86
  url: window.location.href
83
87
  })
84
88
  })
85
89
  }
86
90
 
91
+ /**
92
+ * @param {object} data
93
+ * @param {string} [data.backtrace]
94
+ * @param {Error} [data.error]
95
+ * @param {string} [data.errorClass]
96
+ * @param {string} [data.file]
97
+ * @param {number} [data.line]
98
+ * @param {string} [data.message]
99
+ * @param {string} [data.type]
100
+ * @param {string} [data.url]
101
+ * @returns {void}
102
+ */
87
103
  handleError(data) {
88
104
  let backtrace
89
105
 
@@ -92,7 +108,7 @@ export default class SystemTestBrowserHelper {
92
108
  backtrace.shift()
93
109
  backtrace = backtrace.join("\n")
94
110
  } else if (data.file) {
95
- backtrace = [`${data.file}:${data.line}`]
111
+ backtrace = `${data.file}:${data.line}`
96
112
  }
97
113
 
98
114
  data.backtrace = backtrace
@@ -132,18 +148,35 @@ export default class SystemTestBrowserHelper {
132
148
  */
133
149
  getEvents() { return this.events }
134
150
 
151
+ /**
152
+ * @param {any[]} args
153
+ * @returns {void}
154
+ */
135
155
  fakeConsoleError = (...args) => {
136
156
  this.communicator.sendCommand({type: "console.error", value: this.consoleLogMessage(args)})
137
157
 
138
- return this.originalConsoleError(...args)
158
+ if (this.originalConsoleError) {
159
+ return this.originalConsoleError(...args)
160
+ }
139
161
  }
140
162
 
163
+ /**
164
+ * @param {any[]} args
165
+ * @returns {void}
166
+ */
141
167
  fakeConsoleLog = (...args) => {
142
168
  this.communicator.sendCommand({type: "console.log", value: this.consoleLogMessage(args)})
143
169
 
144
- return this.originalConsoleLog(...args)
170
+ if (this.originalConsoleLog) {
171
+ return this.originalConsoleLog(...args)
172
+ }
145
173
  }
146
174
 
175
+ /**
176
+ * @param {any} arg
177
+ * @param {any[]} [scannedObjects]
178
+ * @returns {any}
179
+ */
147
180
  consoleLogMessage(arg, scannedObjects = []) {
148
181
  if (Array.isArray(arg)) {
149
182
  if (scannedObjects.includes(arg)) {
@@ -166,6 +199,7 @@ export default class SystemTestBrowserHelper {
166
199
  scannedObjects.push(arg)
167
200
  }
168
201
 
202
+ /** @type {Record<string, any>} */
169
203
  const result = {}
170
204
 
171
205
  for (const key in arg) {
@@ -180,6 +214,10 @@ export default class SystemTestBrowserHelper {
180
214
  }
181
215
  }
182
216
 
217
+ /**
218
+ * @param {{data: {path: string, type: string}}} args
219
+ * @returns {Promise<{result: string} | void>}
220
+ */
183
221
  onCommand = async ({data}) => {
184
222
  if (data.type == "initialize") {
185
223
  this.events.emit("initialize")
@@ -226,6 +264,7 @@ export default class SystemTestBrowserHelper {
226
264
  * @returns {Promise<Array<Record<string, any>>>}
227
265
  */
228
266
  async sendQuery(sql) {
267
+ // @ts-expect-error
229
268
  return await this.communicator.sendCommand({type: "query", sql})
230
269
  }
231
270
  }
@@ -1,9 +1,23 @@
1
+ // @ts-check
2
+
1
3
  export default class SystemTestCommunicator {
4
+ /** @type {WebSocket | null} */
5
+ ws = null
6
+
7
+ /**
8
+ * @param {object} args
9
+ * @param {(args: Record<string, any>) => Promise<{result: string} | void>} args.onCommand
10
+ * @param {object} [args.parent]
11
+ */
2
12
  constructor({onCommand, parent}) {
3
13
  this.onCommand = onCommand
4
14
  this.parent = parent
5
15
  this._sendQueueCount = 0
16
+
17
+ /** @type {Record<string, any>} */
6
18
  this._sendQueue = []
19
+
20
+ /** @type {Record<string, {resolve: (data: any) => void, reject: (data: any) => void}>} */
7
21
  this._responses = {}
8
22
  }
9
23
 
@@ -11,15 +25,22 @@ export default class SystemTestCommunicator {
11
25
  while (this._sendQueue.length !== 0) {
12
26
  const data = this._sendQueue.shift()
13
27
 
28
+ if (!this.ws || this.ws.readyState !== 1) {
29
+ throw new Error("WebSocket is not open")
30
+ }
31
+
14
32
  this.ws.send(JSON.stringify(data))
15
33
  }
16
34
  }
17
35
 
36
+ /** @param {Error} error */
18
37
  onError = (error) => {
19
38
  console.error("onWebSocketClientError", error)
20
39
  }
21
40
 
41
+ /** @param {string} rawData */
22
42
  onMessage = async (rawData) => {
43
+ /** @type {{data: any, id: number, type: string, isTrusted?: boolean}} */
23
44
  const data = JSON.parse(rawData)
24
45
 
25
46
  if (data.isTrusted) {
@@ -30,7 +51,11 @@ export default class SystemTestCommunicator {
30
51
 
31
52
  this.respond(data.id, {result})
32
53
  } catch (error) {
33
- this.respond(data.id, {error: error.message})
54
+ if (error instanceof Error) {
55
+ this.respond(data.id, {error: error.message})
56
+ } else {
57
+ this.respond(data.id, {error: error})
58
+ }
34
59
  }
35
60
  } else if (data.type == "response") {
36
61
  const response = this._responses[data.id]
@@ -42,7 +67,7 @@ export default class SystemTestCommunicator {
42
67
  delete this._responses[data.id]
43
68
 
44
69
  if (data.data.error) {
45
- response.error(data.data.error)
70
+ response.reject(data.data.error)
46
71
  } else {
47
72
  response.resolve(data.data.result)
48
73
  }
@@ -55,6 +80,10 @@ export default class SystemTestCommunicator {
55
80
  this.flushSendQueue()
56
81
  }
57
82
 
83
+ /**
84
+ * @param {object} data
85
+ * @returns {void}
86
+ */
58
87
  send(data) {
59
88
  this._sendQueue.push(data)
60
89
 
@@ -69,15 +98,20 @@ export default class SystemTestCommunicator {
69
98
  * @returns {Promise<void>} A promise that resolves with the response data.
70
99
  */
71
100
  sendCommand(data) {
72
- return new Promise((resolve, error) => {
101
+ return new Promise((resolve, reject) => {
73
102
  const id = this._sendQueueCount
74
103
 
75
104
  this._sendQueueCount += 1
76
- this._responses[id] = {resolve, error}
105
+ this._responses[id] = {resolve, reject}
77
106
  this.send({type: "command", id, data})
78
107
  })
79
108
  }
80
109
 
110
+ /**
111
+ * @param {number} id
112
+ * @param {object} data
113
+ * @returns {void}
114
+ */
81
115
  respond(id, data) {
82
116
  this.send({type: "response", id, data})
83
117
  }
@@ -1,3 +1,5 @@
1
+ // @ts-check
2
+
1
3
  import fs from "node:fs/promises"
2
4
  import http from "node:http"
3
5
  import mime from "mime"
@@ -6,10 +8,21 @@ import url from "url"
6
8
  export default class SystemTestHttpServer {
7
9
  /** @returns {void} */
8
10
  close() {
9
- this.httpServer.close()
11
+ this.httpServer?.close()
10
12
  }
11
13
 
14
+ /**
15
+ * @param {http.IncomingMessage} request
16
+ * @param {http.ServerResponse} response
17
+ * @returns {Promise<void>}
18
+ */
12
19
  onHttpServerRequest = async (request, response) => {
20
+ if (!request.url) {
21
+ response.statusCode = 400
22
+ response.end("Bad Request")
23
+ return
24
+ }
25
+
13
26
  const parsedUrl = url.parse(request.url)
14
27
  let filePath = `${process.cwd()}/dist${parsedUrl.pathname}`
15
28
 
@@ -34,7 +47,11 @@ export default class SystemTestHttpServer {
34
47
  const mimeType = mime.getType(filePath)
35
48
 
36
49
  response.statusCode = 200
37
- response.setHeader("Content-Type", mimeType)
50
+
51
+ if (mimeType) {
52
+ response.setHeader("Content-Type", mimeType)
53
+ }
54
+
38
55
  response.end(fileContent)
39
56
  }
40
57
 
@@ -1,6 +1,7 @@
1
+ // @ts-check
2
+
1
3
  import {Builder, By, until} from "selenium-webdriver"
2
4
  import chrome from "selenium-webdriver/chrome.js"
3
- import {digg} from "diggerize"
4
5
  import fs from "node:fs/promises"
5
6
  import logging from "selenium-webdriver/lib/logging.js"
6
7
  import moment from "moment"
@@ -14,20 +15,40 @@ import {WebSocketServer} from "ws"
14
15
 
15
16
  class ElementNotFoundError extends Error { }
16
17
 
18
+ /** @type {{systemTest: SystemTest | null}} */
19
+ const shared = {
20
+ systemTest: null
21
+ }
22
+
17
23
  export default class SystemTest {
18
24
  static rootPath = "/blank?systemTest=true"
19
25
 
26
+ /** @type {SystemTestCommunicator | undefined} */
27
+ communicator = undefined
28
+ _started = false
29
+ _timeouts = 5000
30
+
20
31
  /**
21
32
  * Gets the current system test instance
22
- * @param {object} args
33
+ * @param {object} [args]
34
+ * @param {string} [args.host]
35
+ * @param {number} [args.port]
23
36
  * @returns {SystemTest}
24
37
  */
25
38
  static current(args) {
26
- if (!globalThis.systemTest) {
27
- globalThis.systemTest = new SystemTest(args)
39
+ if (!shared.systemTest) {
40
+ shared.systemTest = new SystemTest(args)
41
+ }
42
+
43
+ return shared.systemTest
44
+ }
45
+
46
+ getCommunicator() {
47
+ if (!this.communicator) {
48
+ throw new Error("Communicator hasn't been initialized yet")
28
49
  }
29
50
 
30
- return globalThis.systemTest
51
+ return this.communicator
31
52
  }
32
53
 
33
54
  /**
@@ -38,7 +59,7 @@ export default class SystemTest {
38
59
  static async run(callback) {
39
60
  const systemTest = this.current()
40
61
 
41
- await systemTest.communicator.sendCommand({type: "initialize"})
62
+ await systemTest.getCommunicator().sendCommand({type: "initialize"})
42
63
  await systemTest.dismissTo(SystemTest.rootPath)
43
64
 
44
65
  try {
@@ -53,11 +74,11 @@ export default class SystemTest {
53
74
 
54
75
  /**
55
76
  * Creates a new SystemTest instance
56
- * @param {object} args
57
- * @param {string} args.host
58
- * @param {number} args.port
77
+ * @param {object} [args]
78
+ * @param {string} [args.host]
79
+ * @param {number} [args.port]
59
80
  */
60
- constructor({host = "localhost", port = 8081, ...restArgs} = {}) {
81
+ constructor({host = "localhost", port = 8081, ...restArgs} = {host: "localhost", port: 8081}) {
61
82
  const restArgsKeys = Object.keys(restArgs)
62
83
 
63
84
  if (restArgsKeys.length > 0) {
@@ -69,15 +90,23 @@ export default class SystemTest {
69
90
  this._responses = {}
70
91
  this._sendCount = 0
71
92
  this.startScoundrel()
93
+
94
+ // @ts-expect-error
72
95
  this.communicator = new SystemTestCommunicator({onCommand: this.onCommandReceived})
73
96
  }
74
97
 
75
98
  /**
76
99
  * Gets the base selector for scoping element searches
77
- * @returns {string}
100
+ * @returns {string | undefined}
78
101
  */
79
102
  getBaseSelector() { return this._baseSelector }
80
103
 
104
+ getDriver() {
105
+ if (!this.driver) throw new Error("Driver hasn't been initialized yet")
106
+
107
+ return this.driver
108
+ }
109
+
81
110
  /**
82
111
  * Sets the base selector for scoping element searches
83
112
  * @param {string} baseSelector
@@ -115,18 +144,18 @@ export default class SystemTest {
115
144
  * Finds all elements by CSS selector
116
145
  * @param {string} selector
117
146
  * @param {object} args
118
- * @returns {import("selenium-webdriver").WebElement[]}
147
+ * @param {boolean} [args.visible]
148
+ * @param {boolean} [args.useBaseSelector]
149
+ * @returns {Promise<import("selenium-webdriver").WebElement[]>}
119
150
  */
120
151
  async all(selector, args = {}) {
121
152
  const {visible = true, useBaseSelector = true, ...restArgs} = args
122
153
  const restArgsKeys = Object.keys(restArgs)
123
154
 
124
- if (restArgsKeys.length > 0) {
125
- throw new Error(`Unknown arguments: ${restArgsKeys.join(", ")}`)
126
- }
155
+ if (restArgsKeys.length > 0) throw new Error(`Unknown arguments: ${restArgsKeys.join(", ")}`)
127
156
 
128
157
  const actualSelector = useBaseSelector ? this.getSelector(selector) : selector
129
- const elements = await this.driver.findElements(By.css(actualSelector))
158
+ const elements = await this.getDriver().findElements(By.css(actualSelector))
130
159
  const activeElements = []
131
160
 
132
161
  for (const element of elements) {
@@ -158,20 +187,24 @@ export default class SystemTest {
158
187
 
159
188
  try {
160
189
  const element = await this._findElement(elementOrIdentifier)
161
- const actions = this.driver.actions({async: true})
190
+ const actions = this.getDriver().actions({async: true})
162
191
 
163
192
  await actions.move({origin: element}).click().perform()
164
193
  break
165
194
  } catch (error) {
166
- if (error.constructor.name === "ElementNotInteractableError") {
167
- if (tries >= 3) {
168
- throw new Error(`Element ${elementOrIdentifier.constructor.name} click failed after ${tries} tries - ${error.constructor.name}: ${error.message}`)
195
+ if (error instanceof Error) {
196
+ if (error.constructor.name === "ElementNotInteractableError") {
197
+ if (tries >= 3) {
198
+ throw new Error(`Element ${elementOrIdentifier.constructor.name} click failed after ${tries} tries - ${error.constructor.name}: ${error.message}`)
199
+ } else {
200
+ await wait(50)
201
+ }
169
202
  } else {
170
- await wait(50)
203
+ // Re-throw with un-corrupted stack trace
204
+ throw new Error(`Element ${elementOrIdentifier.constructor.name} click failed - ${error.constructor.name}: ${error.message}`)
171
205
  }
172
206
  } else {
173
- // Re-throw with un-corrupted stack trace
174
- throw new Error(`Element ${elementOrIdentifier.constructor.name} click failed - ${error.constructor.name}: ${error.message}`)
207
+ throw new Error(`Element ${elementOrIdentifier.constructor.name} click failed - ${typeof error}: ${error}`)
175
208
  }
176
209
  }
177
210
  }
@@ -181,7 +214,7 @@ export default class SystemTest {
181
214
  * Finds a single element by CSS selector
182
215
  * @param {string} selector
183
216
  * @param {object} args
184
- * @returns {import("selenium-webdriver").WebElement}
217
+ * @returns {Promise<import("selenium-webdriver").WebElement>}
185
218
  */
186
219
  async find(selector, args = {}) {
187
220
  let elements
@@ -190,7 +223,11 @@ export default class SystemTest {
190
223
  elements = await this.all(selector, args)
191
224
  } catch (error) {
192
225
  // Re-throw to recover stack trace
193
- throw new Error(`${error.message} (selector: ${this.getSelector(selector)})`)
226
+ if (error instanceof Error) {
227
+ throw new Error(`${error.message} (selector: ${this.getSelector(selector)})`)
228
+ } else {
229
+ throw new Error(`${error} (selector: ${this.getSelector(selector)})`)
230
+ }
194
231
  }
195
232
 
196
233
  if (elements.length > 1) {
@@ -231,7 +268,7 @@ export default class SystemTest {
231
268
  /**
232
269
  * Finds a single element by CSS selector without waiting
233
270
  * @param {string} selector
234
- * @param {object} args
271
+ * @param {object} [args]
235
272
  * @returns {Promise<import("selenium-webdriver").WebElement>}
236
273
  */
237
274
  async findNoWait(selector, args) {
@@ -249,7 +286,7 @@ export default class SystemTest {
249
286
  * @returns {Promise<string[]>}
250
287
  */
251
288
  async getBrowserLogs() {
252
- const entries = await this.driver.manage().logs().get(logging.Type.BROWSER)
289
+ const entries = await this.getDriver().manage().logs().get(logging.Type.BROWSER)
253
290
  const browserLogs = []
254
291
 
255
292
  for (const entry of entries) {
@@ -272,7 +309,7 @@ export default class SystemTest {
272
309
  * @returns {Promise<string>}
273
310
  */
274
311
  async getCurrentUrl() {
275
- return await this.driver.getCurrentUrl()
312
+ return await this.getDriver().getCurrentUrl()
276
313
  }
277
314
 
278
315
  /**
@@ -296,33 +333,40 @@ export default class SystemTest {
296
333
 
297
334
  const element = await this._findElement(elementOrIdentifier)
298
335
 
299
- if (!element[methodName]) {
336
+ // @ts-expect-error
337
+ const candidate = element[methodName]
338
+
339
+ if (!candidate) {
300
340
  throw new Error(`${element.constructor.name} hasn't an attribute named: ${methodName}`)
301
- } else if (typeof element[methodName] != "function") {
341
+ } else if (typeof candidate != "function") {
302
342
  throw new Error(`${element.constructor.name}#${methodName} is not a function`)
303
343
  }
304
344
 
305
345
  try {
306
- return await element[methodName](...args)
346
+ return await candidate(...args)
307
347
  } catch (error) {
308
- if (error.constructor.name === "ElementNotInteractableError") {
309
- // Retry finding the element and interacting with it
310
- if (tries >= 3) {
311
- let elementDescription
312
-
313
- if (typeof elementOrIdentifier == "string") {
314
- elementDescription = `CSS selector ${elementOrIdentifier}`
348
+ if (error instanceof Error) {
349
+ if (error.constructor.name === "ElementNotInteractableError") {
350
+ // Retry finding the element and interacting with it
351
+ if (tries >= 3) {
352
+ let elementDescription
353
+
354
+ if (typeof elementOrIdentifier == "string") {
355
+ elementDescription = `CSS selector ${elementOrIdentifier}`
356
+ } else {
357
+ elementDescription = `${element.constructor.name}`
358
+ }
359
+
360
+ throw new Error(`${elementDescription} ${methodName} failed after ${tries} tries - ${error.constructor.name}: ${error.message}`)
315
361
  } else {
316
- elementDescription = `${element.constructor.name}`
362
+ await wait(50)
317
363
  }
318
-
319
- throw new Error(`${elementDescription} ${methodName} failed after ${tries} tries - ${error.constructor.name}: ${error.message}`)
320
364
  } else {
321
- await wait(50)
365
+ // Re-throw with un-corrupted stack trace
366
+ throw new Error(`${element.constructor.name} ${methodName} failed - ${error.constructor.name}: ${error.message}`)
322
367
  }
323
368
  } else {
324
- // Re-throw with un-corrupted stack trace
325
- throw new Error(`${element.constructor.name} ${methodName} failed - ${error.constructor.name}: ${error.message}`)
369
+ throw new Error(`${element.constructor.name} ${methodName} failed - ${typeof error}: ${error}`)
326
370
  }
327
371
  }
328
372
  }
@@ -340,7 +384,9 @@ export default class SystemTest {
340
384
  await this.findNoWait(selector)
341
385
  found = true
342
386
  } catch (error) {
343
- if (!error.message.startsWith("Element couldn't be found after ")) {
387
+ if (error instanceof Error && error.message.startsWith("Element couldn't be found after ")) {
388
+ // Ignore
389
+ } else {
344
390
  throw error
345
391
  }
346
392
  }
@@ -353,6 +399,7 @@ export default class SystemTest {
353
399
  /**
354
400
  * @param {string} selector
355
401
  * @param {object} args
402
+ * @param {boolean} [args.useBaseSelector]
356
403
  * @returns {Promise<void>}
357
404
  */
358
405
  async waitForNoSelector(selector, args) {
@@ -368,6 +415,7 @@ export default class SystemTest {
368
415
  try {
369
416
  const actualSelector = useBaseSelector ? this.getSelector(selector) : selector
370
417
 
418
+ // @ts-expect-error
371
419
  await this.driver.wait(until.elementIsNotVisible(By.css(actualSelector)), 0)
372
420
 
373
421
  const timeElapsed = new Date().getTime() - timeStart
@@ -376,7 +424,7 @@ export default class SystemTest {
376
424
  throw new Error(`Element still found after ${timeout}ms: ${selector}`)
377
425
  }
378
426
  } catch (error) {
379
- if (error.message.startsWith("Element couldn't be found after ")) {
427
+ if (error instanceof Error && error.message.startsWith("Element couldn't be found after ")) {
380
428
  break
381
429
  }
382
430
  }
@@ -406,9 +454,11 @@ export default class SystemTest {
406
454
  * @returns {Promise<void>}
407
455
  */
408
456
  async expectNotificationMessage(expectedNotificationMessage) {
457
+ /** @type {string[]} */
409
458
  const allDetectedNotificationMessages = []
410
459
  let foundNotificationMessageElement
411
460
 
461
+ // @ts-expect-error
412
462
  await waitFor(async () => {
413
463
  const notificationMessageElements = await this.all("[data-class='notification-message']", {useBaseSelector: false})
414
464
 
@@ -456,7 +506,7 @@ export default class SystemTest {
456
506
  * Gets the HTML of the current page
457
507
  * @returns {Promise<string>}
458
508
  */
459
- async getHTML() { return await this.driver.getPageSource() }
509
+ async getHTML() { return await this.getDriver().getPageSource() }
460
510
 
461
511
  /**
462
512
  * Starts the system test
@@ -482,9 +532,11 @@ export default class SystemTest {
482
532
  options.addArguments("--no-sandbox")
483
533
  options.addArguments("--window-size=1920,1080")
484
534
 
535
+ /** @type {import("selenium-webdriver").WebDriver} */
485
536
  this.driver = new Builder()
486
537
  .forBrowser("chrome")
487
538
  .setChromeOptions(options)
539
+ // @ts-expect-error
488
540
  .setCapability("goog:loggingPrefs", {browser: "ALL"})
489
541
  .build()
490
542
 
@@ -529,7 +581,7 @@ export default class SystemTest {
529
581
  * @returns {Promise<void>}
530
582
  */
531
583
  async driverSetTimeouts(newTimeout) {
532
- await this.driver.manage().setTimeouts({implicit: newTimeout})
584
+ await this.getDriver().manage().setTimeouts({implicit: newTimeout})
533
585
  }
534
586
 
535
587
  /**
@@ -568,7 +620,7 @@ export default class SystemTest {
568
620
 
569
621
  /**
570
622
  * Sets the on command callback
571
- * @param {function(object) : void} callback
623
+ * @param {function({type: string, data: Record<string, any>}): Promise<void>} callback
572
624
  * @returns {void}
573
625
  */
574
626
  onCommand(callback) {
@@ -577,8 +629,7 @@ export default class SystemTest {
577
629
 
578
630
  /**
579
631
  * Handles a command received from the browser
580
- * @param {object} data
581
- * @param {object} data.data
632
+ * @param {{data: {message: string, backtrace: string, type: string, value: any[]}}} args
582
633
  * @returns {Promise<any>}
583
634
  */
584
635
  onCommandReceived = async ({data}) => {
@@ -612,14 +663,18 @@ export default class SystemTest {
612
663
  /**
613
664
  * Handles a new web socket connection
614
665
  * @param {WebSocket} ws
615
- * @returns {void}
666
+ * @returns {Promise<void>}
616
667
  */
617
668
  onWebSocketConnection = async (ws) => {
618
669
  this.ws = ws
619
- this.communicator.ws = ws
620
- this.communicator.onOpen()
621
- this.ws.on("error", digg(this, "communicator", "onError"))
622
- this.ws.on("message", digg(this, "communicator", "onMessage"))
670
+ this.getCommunicator().ws = ws
671
+ this.getCommunicator().onOpen()
672
+
673
+ // @ts-expect-error
674
+ this.ws.on("error", this.communicator.onError)
675
+
676
+ // @ts-expect-error
677
+ this.ws.on("message", this.communicator.onMessage)
623
678
 
624
679
  if (this.waitForClientWebSocketPromiseResolve) {
625
680
  this.waitForClientWebSocketPromiseResolve()
@@ -632,12 +687,14 @@ export default class SystemTest {
632
687
  */
633
688
  onWebSocketClose = () => {
634
689
  this.ws = null
635
- this.communicator.ws = null
690
+ this.getCommunicator().ws = null
636
691
  }
637
692
 
638
693
  /**
639
694
  * Handles an error reported from the browser
640
695
  * @param {object} data
696
+ * @param {string} data.message
697
+ * @param {string} [data.backtrace]
641
698
  * @returns {void}
642
699
  */
643
700
  handleError(data) {
@@ -663,7 +720,7 @@ export default class SystemTest {
663
720
  this.stopScoundrel()
664
721
  this.systemTestHttpServer?.close()
665
722
  this.wss?.close()
666
- await this.driver.quit()
723
+ await this.driver?.quit()
667
724
  }
668
725
 
669
726
  /**
@@ -674,7 +731,7 @@ export default class SystemTest {
674
731
  async driverVisit(path) {
675
732
  const url = `${this.currentUrl}${path}`
676
733
 
677
- await this.driver.get(url)
734
+ await this.getDriver().get(url)
678
735
  }
679
736
 
680
737
  /**
@@ -686,7 +743,7 @@ export default class SystemTest {
686
743
 
687
744
  await fs.mkdir(path, {recursive: true})
688
745
 
689
- const imageContent = await this.driver.takeScreenshot()
746
+ const imageContent = await this.getDriver().takeScreenshot()
690
747
  const now = new Date()
691
748
  const screenshotPath = `${path}/${moment(now).format("YYYY-MM-DD-HH-MM-SS")}.png`
692
749
  const htmlPath = `${path}/${moment(now).format("YYYY-MM-DD-HH-MM-SS")}.html`
@@ -711,7 +768,7 @@ export default class SystemTest {
711
768
  * @returns {Promise<void>}
712
769
  */
713
770
  async visit(path) {
714
- await this.communicator.sendCommand({type: "visit", path})
771
+ await this.getCommunicator().sendCommand({type: "visit", path})
715
772
  }
716
773
 
717
774
  /**
@@ -720,6 +777,6 @@ export default class SystemTest {
720
777
  * @returns {Promise<void>}
721
778
  */
722
779
  async dismissTo(path) {
723
- await this.communicator.sendCommand({type: "dismissTo", path})
780
+ await this.getCommunicator().sendCommand({type: "dismissTo", path})
724
781
  }
725
782
  }