system-testing 1.0.26 → 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.26",
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
@@ -100,6 +116,9 @@ export default class SystemTestBrowserHelper {
100
116
  this.communicator.sendCommand(data)
101
117
  }
102
118
 
119
+ /**
120
+ * @returns {void}
121
+ */
103
122
  connectWebSocket() {
104
123
  this.ws = new WebSocket("ws://localhost:1985")
105
124
  this.communicator.ws = this.ws
@@ -108,6 +127,9 @@ export default class SystemTestBrowserHelper {
108
127
  this.ws.addEventListener("message", (event) => this.communicator.onMessage(event.data))
109
128
  }
110
129
 
130
+ /**
131
+ * @returns {void}
132
+ */
111
133
  enableOnBrowser() {
112
134
  this._enabled = true
113
135
  this.connectWebSocket()
@@ -116,21 +138,45 @@ export default class SystemTestBrowserHelper {
116
138
  this.overrideConsoleLog()
117
139
  }
118
140
 
141
+ /**
142
+ * @returns {boolean}
143
+ */
119
144
  getEnabled() { return this._enabled }
145
+
146
+ /**
147
+ * @returns {EventEmitter}
148
+ */
120
149
  getEvents() { return this.events }
121
150
 
151
+ /**
152
+ * @param {any[]} args
153
+ * @returns {void}
154
+ */
122
155
  fakeConsoleError = (...args) => {
123
156
  this.communicator.sendCommand({type: "console.error", value: this.consoleLogMessage(args)})
124
157
 
125
- return this.originalConsoleError(...args)
158
+ if (this.originalConsoleError) {
159
+ return this.originalConsoleError(...args)
160
+ }
126
161
  }
127
162
 
163
+ /**
164
+ * @param {any[]} args
165
+ * @returns {void}
166
+ */
128
167
  fakeConsoleLog = (...args) => {
129
168
  this.communicator.sendCommand({type: "console.log", value: this.consoleLogMessage(args)})
130
169
 
131
- return this.originalConsoleLog(...args)
170
+ if (this.originalConsoleLog) {
171
+ return this.originalConsoleLog(...args)
172
+ }
132
173
  }
133
174
 
175
+ /**
176
+ * @param {any} arg
177
+ * @param {any[]} [scannedObjects]
178
+ * @returns {any}
179
+ */
134
180
  consoleLogMessage(arg, scannedObjects = []) {
135
181
  if (Array.isArray(arg)) {
136
182
  if (scannedObjects.includes(arg)) {
@@ -153,6 +199,7 @@ export default class SystemTestBrowserHelper {
153
199
  scannedObjects.push(arg)
154
200
  }
155
201
 
202
+ /** @type {Record<string, any>} */
156
203
  const result = {}
157
204
 
158
205
  for (const key in arg) {
@@ -167,6 +214,10 @@ export default class SystemTestBrowserHelper {
167
214
  }
168
215
  }
169
216
 
217
+ /**
218
+ * @param {{data: {path: string, type: string}}} args
219
+ * @returns {Promise<{result: string} | void>}
220
+ */
170
221
  onCommand = async ({data}) => {
171
222
  if (data.type == "initialize") {
172
223
  this.events.emit("initialize")
@@ -185,10 +236,17 @@ export default class SystemTestBrowserHelper {
185
236
  }
186
237
  }
187
238
 
239
+ /**
240
+ * @param {function() : void} callback
241
+ * @returns {void}
242
+ */
188
243
  onInitialize(callback) {
189
244
  this._onInitializeCallback = callback
190
245
  }
191
246
 
247
+ /**
248
+ * @returns {void}
249
+ */
192
250
  overrideConsoleLog() {
193
251
  if (this.originalConsoleError || this.originalConsoleLog) {
194
252
  throw new Error("Console methods has already been overridden!")
@@ -201,7 +259,12 @@ export default class SystemTestBrowserHelper {
201
259
  console.log = this.fakeConsoleLog
202
260
  }
203
261
 
262
+ /**
263
+ * @param {string} sql
264
+ * @returns {Promise<Array<Record<string, any>>>}
265
+ */
204
266
  async sendQuery(sql) {
267
+ // @ts-expect-error
205
268
  return await this.communicator.sendCommand({type: "query", sql})
206
269
  }
207
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,14 +1,28 @@
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"
4
6
  import url from "url"
5
7
 
6
8
  export default class SystemTestHttpServer {
9
+ /** @returns {void} */
7
10
  close() {
8
- this.httpServer.close()
11
+ this.httpServer?.close()
9
12
  }
10
13
 
14
+ /**
15
+ * @param {http.IncomingMessage} request
16
+ * @param {http.ServerResponse} response
17
+ * @returns {Promise<void>}
18
+ */
11
19
  onHttpServerRequest = async (request, response) => {
20
+ if (!request.url) {
21
+ response.statusCode = 400
22
+ response.end("Bad Request")
23
+ return
24
+ }
25
+
12
26
  const parsedUrl = url.parse(request.url)
13
27
  let filePath = `${process.cwd()}/dist${parsedUrl.pathname}`
14
28
 
@@ -33,15 +47,21 @@ export default class SystemTestHttpServer {
33
47
  const mimeType = mime.getType(filePath)
34
48
 
35
49
  response.statusCode = 200
36
- response.setHeader("Content-Type", mimeType)
50
+
51
+ if (mimeType) {
52
+ response.setHeader("Content-Type", mimeType)
53
+ }
54
+
37
55
  response.end(fileContent)
38
56
  }
39
57
 
58
+ /** @returns {Promise<void>} */
40
59
  async start() {
41
60
  this.basePath = await fs.realpath(`${__dirname}/../..`)
42
61
  await this.startHttpServer()
43
62
  }
44
63
 
64
+ /** @returns {Promise<void>} */
45
65
  startHttpServer() {
46
66
  return new Promise((resolve) => {
47
67
  this.httpServer = http.createServer(this.onHttpServerRequest)
@@ -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)
28
41
  }
29
42
 
30
- return globalThis.systemTest
43
+ return shared.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
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
@@ -93,13 +122,19 @@ export default class SystemTest {
93
122
  return this.getBaseSelector() ? `${this.getBaseSelector()} ${selector}` : selector
94
123
  }
95
124
 
96
- /** Starts Scoundrel server which the browser connects to for remote evaluation in the browser */
125
+ /**
126
+ * Starts Scoundrel server which the browser connects to for remote evaluation in the browser
127
+ * @returns {void}
128
+ */
97
129
  startScoundrel() {
98
130
  this.wss = new WebSocketServer({port: 8090})
99
131
  this.serverWebSocket = new ServerWebSocket(this.wss)
100
132
  this.server = new Server(this.serverWebSocket)
101
133
  }
102
134
 
135
+ /**
136
+ * @returns {void}
137
+ */
103
138
  stopScoundrel() {
104
139
  this.server?.close()
105
140
  this.wss?.close()
@@ -109,18 +144,18 @@ export default class SystemTest {
109
144
  * Finds all elements by CSS selector
110
145
  * @param {string} selector
111
146
  * @param {object} args
112
- * @returns {import("selenium-webdriver").WebElement[]}
147
+ * @param {boolean} [args.visible]
148
+ * @param {boolean} [args.useBaseSelector]
149
+ * @returns {Promise<import("selenium-webdriver").WebElement[]>}
113
150
  */
114
151
  async all(selector, args = {}) {
115
152
  const {visible = true, useBaseSelector = true, ...restArgs} = args
116
153
  const restArgsKeys = Object.keys(restArgs)
117
154
 
118
- if (restArgsKeys.length > 0) {
119
- throw new Error(`Unknown arguments: ${restArgsKeys.join(", ")}`)
120
- }
155
+ if (restArgsKeys.length > 0) throw new Error(`Unknown arguments: ${restArgsKeys.join(", ")}`)
121
156
 
122
157
  const actualSelector = useBaseSelector ? this.getSelector(selector) : selector
123
- const elements = await this.driver.findElements(By.css(actualSelector))
158
+ const elements = await this.getDriver().findElements(By.css(actualSelector))
124
159
  const activeElements = []
125
160
 
126
161
  for (const element of elements) {
@@ -152,20 +187,24 @@ export default class SystemTest {
152
187
 
153
188
  try {
154
189
  const element = await this._findElement(elementOrIdentifier)
155
- const actions = this.driver.actions({async: true})
190
+ const actions = this.getDriver().actions({async: true})
156
191
 
157
192
  await actions.move({origin: element}).click().perform()
158
193
  break
159
194
  } catch (error) {
160
- if (error.constructor.name === "ElementNotInteractableError") {
161
- if (tries >= 3) {
162
- 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
+ }
163
202
  } else {
164
- 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}`)
165
205
  }
166
206
  } else {
167
- // Re-throw with un-corrupted stack trace
168
- 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}`)
169
208
  }
170
209
  }
171
210
  }
@@ -175,7 +214,7 @@ export default class SystemTest {
175
214
  * Finds a single element by CSS selector
176
215
  * @param {string} selector
177
216
  * @param {object} args
178
- * @returns {import("selenium-webdriver").WebElement}
217
+ * @returns {Promise<import("selenium-webdriver").WebElement>}
179
218
  */
180
219
  async find(selector, args = {}) {
181
220
  let elements
@@ -184,7 +223,11 @@ export default class SystemTest {
184
223
  elements = await this.all(selector, args)
185
224
  } catch (error) {
186
225
  // Re-throw to recover stack trace
187
- 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
+ }
188
231
  }
189
232
 
190
233
  if (elements.length > 1) {
@@ -225,7 +268,7 @@ export default class SystemTest {
225
268
  /**
226
269
  * Finds a single element by CSS selector without waiting
227
270
  * @param {string} selector
228
- * @param {object} args
271
+ * @param {object} [args]
229
272
  * @returns {Promise<import("selenium-webdriver").WebElement>}
230
273
  */
231
274
  async findNoWait(selector, args) {
@@ -243,7 +286,7 @@ export default class SystemTest {
243
286
  * @returns {Promise<string[]>}
244
287
  */
245
288
  async getBrowserLogs() {
246
- const entries = await this.driver.manage().logs().get(logging.Type.BROWSER)
289
+ const entries = await this.getDriver().manage().logs().get(logging.Type.BROWSER)
247
290
  const browserLogs = []
248
291
 
249
292
  for (const entry of entries) {
@@ -266,7 +309,7 @@ export default class SystemTest {
266
309
  * @returns {Promise<string>}
267
310
  */
268
311
  async getCurrentUrl() {
269
- return await this.driver.getCurrentUrl()
312
+ return await this.getDriver().getCurrentUrl()
270
313
  }
271
314
 
272
315
  /**
@@ -290,33 +333,40 @@ export default class SystemTest {
290
333
 
291
334
  const element = await this._findElement(elementOrIdentifier)
292
335
 
293
- if (!element[methodName]) {
336
+ // @ts-expect-error
337
+ const candidate = element[methodName]
338
+
339
+ if (!candidate) {
294
340
  throw new Error(`${element.constructor.name} hasn't an attribute named: ${methodName}`)
295
- } else if (typeof element[methodName] != "function") {
341
+ } else if (typeof candidate != "function") {
296
342
  throw new Error(`${element.constructor.name}#${methodName} is not a function`)
297
343
  }
298
344
 
299
345
  try {
300
- return await element[methodName](...args)
346
+ return await candidate(...args)
301
347
  } catch (error) {
302
- if (error.constructor.name === "ElementNotInteractableError") {
303
- // Retry finding the element and interacting with it
304
- if (tries >= 3) {
305
- let elementDescription
306
-
307
- if (typeof elementOrIdentifier == "string") {
308
- 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}`)
309
361
  } else {
310
- elementDescription = `${element.constructor.name}`
362
+ await wait(50)
311
363
  }
312
-
313
- throw new Error(`${elementDescription} ${methodName} failed after ${tries} tries - ${error.constructor.name}: ${error.message}`)
314
364
  } else {
315
- 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}`)
316
367
  }
317
368
  } else {
318
- // Re-throw with un-corrupted stack trace
319
- 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}`)
320
370
  }
321
371
  }
322
372
  }
@@ -334,7 +384,9 @@ export default class SystemTest {
334
384
  await this.findNoWait(selector)
335
385
  found = true
336
386
  } catch (error) {
337
- 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 {
338
390
  throw error
339
391
  }
340
392
  }
@@ -347,6 +399,7 @@ export default class SystemTest {
347
399
  /**
348
400
  * @param {string} selector
349
401
  * @param {object} args
402
+ * @param {boolean} [args.useBaseSelector]
350
403
  * @returns {Promise<void>}
351
404
  */
352
405
  async waitForNoSelector(selector, args) {
@@ -362,6 +415,7 @@ export default class SystemTest {
362
415
  try {
363
416
  const actualSelector = useBaseSelector ? this.getSelector(selector) : selector
364
417
 
418
+ // @ts-expect-error
365
419
  await this.driver.wait(until.elementIsNotVisible(By.css(actualSelector)), 0)
366
420
 
367
421
  const timeElapsed = new Date().getTime() - timeStart
@@ -370,7 +424,7 @@ export default class SystemTest {
370
424
  throw new Error(`Element still found after ${timeout}ms: ${selector}`)
371
425
  }
372
426
  } catch (error) {
373
- if (error.message.startsWith("Element couldn't be found after ")) {
427
+ if (error instanceof Error && error.message.startsWith("Element couldn't be found after ")) {
374
428
  break
375
429
  }
376
430
  }
@@ -400,9 +454,11 @@ export default class SystemTest {
400
454
  * @returns {Promise<void>}
401
455
  */
402
456
  async expectNotificationMessage(expectedNotificationMessage) {
457
+ /** @type {string[]} */
403
458
  const allDetectedNotificationMessages = []
404
459
  let foundNotificationMessageElement
405
460
 
461
+ // @ts-expect-error
406
462
  await waitFor(async () => {
407
463
  const notificationMessageElements = await this.all("[data-class='notification-message']", {useBaseSelector: false})
408
464
 
@@ -450,7 +506,7 @@ export default class SystemTest {
450
506
  * Gets the HTML of the current page
451
507
  * @returns {Promise<string>}
452
508
  */
453
- async getHTML() { return await this.driver.getPageSource() }
509
+ async getHTML() { return await this.getDriver().getPageSource() }
454
510
 
455
511
  /**
456
512
  * Starts the system test
@@ -476,9 +532,11 @@ export default class SystemTest {
476
532
  options.addArguments("--no-sandbox")
477
533
  options.addArguments("--window-size=1920,1080")
478
534
 
535
+ /** @type {import("selenium-webdriver").WebDriver} */
479
536
  this.driver = new Builder()
480
537
  .forBrowser("chrome")
481
538
  .setChromeOptions(options)
539
+ // @ts-expect-error
482
540
  .setCapability("goog:loggingPrefs", {browser: "ALL"})
483
541
  .build()
484
542
 
@@ -523,7 +581,7 @@ export default class SystemTest {
523
581
  * @returns {Promise<void>}
524
582
  */
525
583
  async driverSetTimeouts(newTimeout) {
526
- await this.driver.manage().setTimeouts({implicit: newTimeout})
584
+ await this.getDriver().manage().setTimeouts({implicit: newTimeout})
527
585
  }
528
586
 
529
587
  /**
@@ -562,7 +620,7 @@ export default class SystemTest {
562
620
 
563
621
  /**
564
622
  * Sets the on command callback
565
- * @param {function(object) : void} callback
623
+ * @param {function({type: string, data: Record<string, any>}): Promise<void>} callback
566
624
  * @returns {void}
567
625
  */
568
626
  onCommand(callback) {
@@ -571,8 +629,7 @@ export default class SystemTest {
571
629
 
572
630
  /**
573
631
  * Handles a command received from the browser
574
- * @param {object} data
575
- * @param {object} data.data
632
+ * @param {{data: {message: string, backtrace: string, type: string, value: any[]}}} args
576
633
  * @returns {Promise<any>}
577
634
  */
578
635
  onCommandReceived = async ({data}) => {
@@ -606,14 +663,18 @@ export default class SystemTest {
606
663
  /**
607
664
  * Handles a new web socket connection
608
665
  * @param {WebSocket} ws
609
- * @returns {void}
666
+ * @returns {Promise<void>}
610
667
  */
611
668
  onWebSocketConnection = async (ws) => {
612
669
  this.ws = ws
613
- this.communicator.ws = ws
614
- this.communicator.onOpen()
615
- this.ws.on("error", digg(this, "communicator", "onError"))
616
- 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)
617
678
 
618
679
  if (this.waitForClientWebSocketPromiseResolve) {
619
680
  this.waitForClientWebSocketPromiseResolve()
@@ -626,12 +687,14 @@ export default class SystemTest {
626
687
  */
627
688
  onWebSocketClose = () => {
628
689
  this.ws = null
629
- this.communicator.ws = null
690
+ this.getCommunicator().ws = null
630
691
  }
631
692
 
632
693
  /**
633
694
  * Handles an error reported from the browser
634
695
  * @param {object} data
696
+ * @param {string} data.message
697
+ * @param {string} [data.backtrace]
635
698
  * @returns {void}
636
699
  */
637
700
  handleError(data) {
@@ -657,7 +720,7 @@ export default class SystemTest {
657
720
  this.stopScoundrel()
658
721
  this.systemTestHttpServer?.close()
659
722
  this.wss?.close()
660
- await this.driver.quit()
723
+ await this.driver?.quit()
661
724
  }
662
725
 
663
726
  /**
@@ -668,7 +731,7 @@ export default class SystemTest {
668
731
  async driverVisit(path) {
669
732
  const url = `${this.currentUrl}${path}`
670
733
 
671
- await this.driver.get(url)
734
+ await this.getDriver().get(url)
672
735
  }
673
736
 
674
737
  /**
@@ -680,7 +743,7 @@ export default class SystemTest {
680
743
 
681
744
  await fs.mkdir(path, {recursive: true})
682
745
 
683
- const imageContent = await this.driver.takeScreenshot()
746
+ const imageContent = await this.getDriver().takeScreenshot()
684
747
  const now = new Date()
685
748
  const screenshotPath = `${path}/${moment(now).format("YYYY-MM-DD-HH-MM-SS")}.png`
686
749
  const htmlPath = `${path}/${moment(now).format("YYYY-MM-DD-HH-MM-SS")}.html`
@@ -705,7 +768,7 @@ export default class SystemTest {
705
768
  * @returns {Promise<void>}
706
769
  */
707
770
  async visit(path) {
708
- await this.communicator.sendCommand({type: "visit", path})
771
+ await this.getCommunicator().sendCommand({type: "visit", path})
709
772
  }
710
773
 
711
774
  /**
@@ -714,6 +777,6 @@ export default class SystemTest {
714
777
  * @returns {Promise<void>}
715
778
  */
716
779
  async dismissTo(path) {
717
- await this.communicator.sendCommand({type: "dismissTo", path})
780
+ await this.getCommunicator().sendCommand({type: "dismissTo", path})
718
781
  }
719
782
  }
@@ -9,7 +9,10 @@ const shared = {
9
9
  systemTestBrowserHelper: null
10
10
  }
11
11
 
12
- const isSystemTestEnabled = () => {
12
+ /**
13
+ * @returns {boolean}
14
+ */
15
+ function isSystemTestEnabled() {
13
16
  let enabled = false
14
17
  const initialUrl = globalThis.location?.href
15
18
 
@@ -25,7 +28,10 @@ const isSystemTestEnabled = () => {
25
28
  return enabled
26
29
  }
27
30
 
28
- const getSystemTestBrowserHelper = () => {
31
+ /**
32
+ * @returns {SystemTestBrowserHelper}
33
+ */
34
+ function getSystemTestBrowserHelper() {
29
35
  if (!shared.systemTestBrowserHelper) {
30
36
  shared.systemTestBrowserHelper = new SystemTestBrowserHelper()
31
37
  shared.systemTestBrowserHelper.enableOnBrowser()
@@ -38,9 +44,7 @@ const getSystemTestBrowserHelper = () => {
38
44
  * A hook that provides system test capabilities.
39
45
  * @param {object} options - Options for the hook.
40
46
  * @param {function() : void} options.onInitialize - A callback function that is called when the system test browser helper is initialized.
41
- * @returns {object} An object containing:
42
- * - enabled: A boolean indicating if system test mode is enabled.
43
- * - systemTestBrowserHelper: An instance of SystemTestBrowserHelper if enabled, otherwise null.
47
+ * @returns {{enabled: boolean, systemTestBrowserHelper: SystemTestBrowserHelper}}
44
48
  */
45
49
  export default function useSystemTest({onInitialize, ...restArgs} = {}) {
46
50
  const router = useRouter()