system-testing 1.0.24 → 1.0.26

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/README.md ADDED
@@ -0,0 +1,63 @@
1
+ # System testing
2
+
3
+ Rails inspired system testing for Expo apps.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ npm install --save-dev system-testing
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ ```js
14
+ import retry from "awaitery/src/retry.js"
15
+ import SystemTest from "system-testing/src/system-test.js"
16
+ import wait from "awaitery/src/wait.js"
17
+ import waitFor from "awaitery/src/wait-for.js"
18
+
19
+ import createUser from "@/src/testing/create-user.js"
20
+ import initialize from "@/src/initialize"
21
+ import Option from "@/src/models/option"
22
+
23
+ describe("Sign in page", () => {
24
+ test("it navigates to the sign in page and signs in", async () => {
25
+ await initialize()
26
+
27
+ await SystemTest.run(async (systemTest) => {
28
+ await createUser(userAttributes)
29
+
30
+ await systemTest.visit("/")
31
+ await systemTest.findByTestID("frontpageScreen", {useBaseSelector: false})
32
+ await wait(250)
33
+
34
+ await retry(async () => {
35
+ await systemTest.click("[data-testid='signInButton']")
36
+ await systemTest.findByTestID("app/sign-in")
37
+ })
38
+
39
+ await systemTest.interact("[data-testid='signInEmailInput']", "sendKeys", "user@example.com")
40
+ await systemTest.interact("[data-testid='signInPasswordInput']", "sendKeys", "password")
41
+
42
+ const emailInputValue = await systemTest.interact("[data-testid='signInEmailInput']", "getAttribute", "value")
43
+ const passwordInputValue = await systemTest.interact("[data-testid='signInPasswordInput']", "getAttribute", "value")
44
+
45
+ expect(emailInputValue).toEqual("user@example.com")
46
+ expect(passwordInputValue).toEqual("password")
47
+
48
+ await systemTest.click("[data-testid='signInSubmitButton']")
49
+ await systemTest.expectNotificationMessage("You were signed in.")
50
+
51
+ await waitFor(async () => {
52
+ const optionUserID = await Option.findBy({key: "userID"})
53
+
54
+ if (!optionUserID) {
55
+ throw new Error("Option for user ID didn't exist")
56
+ }
57
+
58
+ expect(optionUserID.value()).toEqual("805")
59
+ })
60
+ })
61
+ })
62
+ })
63
+ ```
@@ -0,0 +1,23 @@
1
+ import js from "@eslint/js"
2
+ import {jsdoc} from "eslint-plugin-jsdoc"
3
+ import globals from "globals"
4
+ import { defineConfig } from "eslint/config"
5
+
6
+ export default defineConfig([
7
+ {
8
+ files: ["**/*.{js,mjs,cjs}"],
9
+ plugins: {js},
10
+ extends: ["js/recommended"],
11
+ languageOptions: {
12
+ globals: {...globals.browser, ...globals.node}
13
+ }
14
+ },
15
+ jsdoc({
16
+ config: "flat/recommended",
17
+ rules: {
18
+ "jsdoc/reject-any-type": "off",
19
+ "jsdoc/require-param-description": "off",
20
+ "jsdoc/require-returns-description": "off"
21
+ }
22
+ })
23
+ ])
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "system-testing",
3
- "version": "1.0.24",
3
+ "version": "1.0.26",
4
4
  "description": "System testing with Selenium and browsers.",
5
5
  "keywords": [
6
6
  "system",
@@ -21,6 +21,7 @@
21
21
  "type": "module",
22
22
  "main": "src/index.js",
23
23
  "scripts": {
24
+ "lint": "eslint",
24
25
  "test": "echo \"Error: no test specified\" && exit 1"
25
26
  },
26
27
  "dependencies": {
@@ -34,5 +35,11 @@
34
35
  },
35
36
  "peerDependencies": {
36
37
  "selenium-webdriver": "^4.34.0"
38
+ },
39
+ "devDependencies": {
40
+ "@eslint/js": "^9.39.1",
41
+ "eslint": "^9.39.1",
42
+ "eslint-plugin-jsdoc": "^61.4.1",
43
+ "globals": "^16.5.0"
37
44
  }
38
45
  }
package/peak_flow.yml ADDED
@@ -0,0 +1,4 @@
1
+ before_script:
2
+ - npm install
3
+ script:
4
+ - npm run lint
@@ -101,8 +101,7 @@ export default class SystemTestBrowserHelper {
101
101
  }
102
102
 
103
103
  connectWebSocket() {
104
- this.ws = new WebSocket("ws://localhost:1985") // eslint-disable-line no-undef
105
-
104
+ this.ws = new WebSocket("ws://localhost:1985")
106
105
  this.communicator.ws = this.ws
107
106
  this.ws.addEventListener("error", digg(this, "communicator", "onError"))
108
107
  this.ws.addEventListener("open", digg(this, "communicator", "onOpen"))
@@ -65,9 +65,8 @@ export default class SystemTestCommunicator {
65
65
 
66
66
  /**
67
67
  * Sends a command and returns a promise that resolves with the response.
68
- *
69
- * @param {Object} data - The command data to send.
70
- * @returns {Promise} A promise that resolves with the response data.
68
+ * @param {object} data - The command data to send.
69
+ * @returns {Promise<void>} A promise that resolves with the response data.
71
70
  */
72
71
  sendCommand(data) {
73
72
  return new Promise((resolve, error) => {
@@ -21,7 +21,7 @@ export default class SystemTestHttpServer {
21
21
  try {
22
22
  await fs.stat(filePath)
23
23
  fileExists = true
24
- } catch (_error) {
24
+ } catch (_error) { // eslint-disable-line no-unused-vars
25
25
  fileExists = false
26
26
  }
27
27
 
@@ -38,8 +38,7 @@ export default class SystemTestHttpServer {
38
38
  }
39
39
 
40
40
  async start() {
41
- this.basePath = await fs.realpath(`${__dirname}/../..`) // eslint-disable-line no-undef
42
-
41
+ this.basePath = await fs.realpath(`${__dirname}/../..`)
43
42
  await this.startHttpServer()
44
43
  }
45
44
 
@@ -1,4 +1,4 @@
1
- import {Builder, By} from "selenium-webdriver"
1
+ import {Builder, By, until} from "selenium-webdriver"
2
2
  import chrome from "selenium-webdriver/chrome.js"
3
3
  import {digg} from "diggerize"
4
4
  import fs from "node:fs/promises"
@@ -19,7 +19,6 @@ export default class SystemTest {
19
19
 
20
20
  /**
21
21
  * Gets the current system test instance
22
- *
23
22
  * @param {object} args
24
23
  * @returns {SystemTest}
25
24
  */
@@ -33,8 +32,8 @@ export default class SystemTest {
33
32
 
34
33
  /**
35
34
  * Runs a system test
36
- *
37
35
  * @param {function(SystemTest): Promise<void>} callback
36
+ * @returns {Promise<void>}
38
37
  */
39
38
  static async run(callback) {
40
39
  const systemTest = this.current()
@@ -54,7 +53,6 @@ export default class SystemTest {
54
53
 
55
54
  /**
56
55
  * Creates a new SystemTest instance
57
- *
58
56
  * @param {object} args
59
57
  * @param {string} args.host
60
58
  * @param {number} args.port
@@ -76,21 +74,18 @@ export default class SystemTest {
76
74
 
77
75
  /**
78
76
  * Gets the base selector for scoping element searches
79
- *
80
77
  * @returns {string}
81
78
  */
82
79
  getBaseSelector() { return this._baseSelector }
83
80
 
84
81
  /**
85
82
  * Sets the base selector for scoping element searches
86
- *
87
83
  * @param {string} baseSelector
88
84
  */
89
85
  setBaseSelector(baseSelector) { this._baseSelector = baseSelector }
90
86
 
91
87
  /**
92
88
  * Gets a selector scoped to the base selector
93
- *
94
89
  * @param {string} selector
95
90
  * @returns {string}
96
91
  */
@@ -112,10 +107,8 @@ export default class SystemTest {
112
107
 
113
108
  /**
114
109
  * Finds all elements by CSS selector
115
- *
116
110
  * @param {string} selector
117
111
  * @param {object} args
118
- *
119
112
  * @returns {import("selenium-webdriver").WebElement[]}
120
113
  */
121
114
  async all(selector, args = {}) {
@@ -148,22 +141,38 @@ export default class SystemTest {
148
141
 
149
142
  /**
150
143
  * Clicks an element that has children which fills out the element and would otherwise have caused a ElementClickInterceptedError
151
- *
152
- * @param {import("selenium-webdriver").WebElement} element
153
- **/
154
- async click(element) {
155
- if (typeof element == "string") {
156
- element = await this.find(element)
157
- }
144
+ * @param {string|import("selenium-webdriver").WebElement} elementOrIdentifier
145
+ * @returns {Promise<void>}
146
+ */
147
+ async click(elementOrIdentifier) {
148
+ let tries = 0
149
+
150
+ while (true) {
151
+ tries++
158
152
 
159
- const actions = this.driver.actions({async: true})
153
+ try {
154
+ const element = await this._findElement(elementOrIdentifier)
155
+ const actions = this.driver.actions({async: true})
160
156
 
161
- await actions.move({origin: element}).click().perform()
157
+ await actions.move({origin: element}).click().perform()
158
+ break
159
+ } 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}`)
163
+ } else {
164
+ await wait(50)
165
+ }
166
+ } else {
167
+ // Re-throw with un-corrupted stack trace
168
+ throw new Error(`Element ${elementOrIdentifier.constructor.name} click failed - ${error.constructor.name}: ${error.message}`)
169
+ }
170
+ }
171
+ }
162
172
  }
163
173
 
164
174
  /**
165
175
  * Finds a single element by CSS selector
166
- *
167
176
  * @param {string} selector
168
177
  * @param {object} args
169
178
  * @returns {import("selenium-webdriver").WebElement}
@@ -191,18 +200,33 @@ export default class SystemTest {
191
200
 
192
201
  /**
193
202
  * Finds a single element by test ID
194
- *
195
203
  * @param {string} testID
196
204
  * @param {object} args
197
- * @returns {import("selenium-webdriver").WebElement}
205
+ * @returns {Promise<import("selenium-webdriver").WebElement>}
198
206
  */
199
207
  async findByTestID(testID, args) { return await this.find(`[data-testid='${testID}']`, args) }
200
208
 
209
+ /**
210
+ * @param {string|import("selenium-webdriver").WebElement} elementOrIdentifier
211
+ * @returns {Promise<import("selenium-webdriver").WebElement>}
212
+ */
213
+ async _findElement(elementOrIdentifier) {
214
+ let element
215
+
216
+ if (typeof elementOrIdentifier == "string") {
217
+ element = await this.find(elementOrIdentifier)
218
+ } else {
219
+ element = elementOrIdentifier
220
+ }
221
+
222
+ return element
223
+ }
224
+
201
225
  /**
202
226
  * Finds a single element by CSS selector without waiting
203
- *
204
227
  * @param {string} selector
205
- * @returns {import("selenium-webdriver").WebElement}
228
+ * @param {object} args
229
+ * @returns {Promise<import("selenium-webdriver").WebElement>}
206
230
  */
207
231
  async findNoWait(selector, args) {
208
232
  await this.driverSetTimeouts(0)
@@ -216,7 +240,6 @@ export default class SystemTest {
216
240
 
217
241
  /**
218
242
  * Gets browser logs
219
- *
220
243
  * @returns {Promise<string[]>}
221
244
  */
222
245
  async getBrowserLogs() {
@@ -239,37 +262,38 @@ export default class SystemTest {
239
262
  return browserLogs
240
263
  }
241
264
 
265
+ /**
266
+ * @returns {Promise<string>}
267
+ */
242
268
  async getCurrentUrl() {
243
269
  return await this.driver.getCurrentUrl()
244
270
  }
245
271
 
272
+ /**
273
+ * @returns {number}
274
+ */
246
275
  getTimeouts() { return this._timeouts }
247
276
 
248
277
  /**
249
278
  * Interacts with an element by calling a method on it with the given arguments.
250
279
  * Retrying on ElementNotInteractableError.
251
- *
252
280
  * @param {import("selenium-webdriver").WebElement|string} elementOrIdentifier - The element or a CSS selector to find the element.
253
281
  * @param {string} methodName - The method name to call on the element.
254
282
  * @param {...any} args - Arguments to pass to the method.
255
- *
256
283
  * @returns {Promise<any>}
257
284
  */
258
285
  async interact(elementOrIdentifier, methodName, ...args) {
259
- let element
260
286
  let tries = 0
261
287
 
262
288
  while (true) {
263
289
  tries++
264
290
 
265
- if (typeof elementOrIdentifier == "string") {
266
- element = await this.find(elementOrIdentifier)
267
- } else {
268
- element = elementOrIdentifier
269
- }
291
+ const element = await this._findElement(elementOrIdentifier)
270
292
 
271
293
  if (!element[methodName]) {
272
- // throw new Error(`${element.constructor.name} has no method named: ${methodName}`)
294
+ throw new Error(`${element.constructor.name} hasn't an attribute named: ${methodName}`)
295
+ } else if (typeof element[methodName] != "function") {
296
+ throw new Error(`${element.constructor.name}#${methodName} is not a function`)
273
297
  }
274
298
 
275
299
  try {
@@ -278,9 +302,17 @@ export default class SystemTest {
278
302
  if (error.constructor.name === "ElementNotInteractableError") {
279
303
  // Retry finding the element and interacting with it
280
304
  if (tries >= 3) {
281
- throw new Error(`${element.constructor.name} ${methodName} failed after ${tries} tries - ${error.constructor.name}: ${error.message}`)
305
+ let elementDescription
306
+
307
+ if (typeof elementOrIdentifier == "string") {
308
+ elementDescription = `CSS selector ${elementOrIdentifier}`
309
+ } else {
310
+ elementDescription = `${element.constructor.name}`
311
+ }
312
+
313
+ throw new Error(`${elementDescription} ${methodName} failed after ${tries} tries - ${error.constructor.name}: ${error.message}`)
282
314
  } else {
283
- await wait(100)
315
+ await wait(50)
284
316
  }
285
317
  } else {
286
318
  // Re-throw with un-corrupted stack trace
@@ -292,8 +324,8 @@ export default class SystemTest {
292
324
 
293
325
  /**
294
326
  * Expects no element to be found by CSS selector
295
- *
296
327
  * @param {string} selector
328
+ * @returns {Promise<void>}
297
329
  */
298
330
  async expectNoElement(selector) {
299
331
  let found = false
@@ -312,9 +344,41 @@ export default class SystemTest {
312
344
  }
313
345
  }
314
346
 
347
+ /**
348
+ * @param {string} selector
349
+ * @param {object} args
350
+ * @returns {Promise<void>}
351
+ */
352
+ async waitForNoSelector(selector, args) {
353
+ const timeStart = new Date().getTime()
354
+ const timeout = this.getTimeouts()
355
+ const {useBaseSelector, ...restArgs} = args
356
+
357
+ if (Object.keys(restArgs).length > 0) {
358
+ throw new Error(`Unexpected args: ${Object.keys(restArgs).join(", ")}`)
359
+ }
360
+
361
+ while (true) {
362
+ try {
363
+ const actualSelector = useBaseSelector ? this.getSelector(selector) : selector
364
+
365
+ await this.driver.wait(until.elementIsNotVisible(By.css(actualSelector)), 0)
366
+
367
+ const timeElapsed = new Date().getTime() - timeStart
368
+
369
+ if (timeElapsed > timeout) {
370
+ throw new Error(`Element still found after ${timeout}ms: ${selector}`)
371
+ }
372
+ } catch (error) {
373
+ if (error.message.startsWith("Element couldn't be found after ")) {
374
+ break
375
+ }
376
+ }
377
+ }
378
+ }
379
+
315
380
  /**
316
381
  * Gets notification messages
317
- *
318
382
  * @returns {Promise<string[]>}
319
383
  */
320
384
  async notificationMessages() {
@@ -332,45 +396,65 @@ export default class SystemTest {
332
396
 
333
397
  /**
334
398
  * Expects a notification message to appear and waits for it if necessary.
335
- *
336
399
  * @param {string} expectedNotificationMessage
400
+ * @returns {Promise<void>}
337
401
  */
338
402
  async expectNotificationMessage(expectedNotificationMessage) {
339
403
  const allDetectedNotificationMessages = []
404
+ let foundNotificationMessageElement
340
405
 
341
406
  await waitFor(async () => {
342
- const notificationMessages = await this.notificationMessages()
407
+ const notificationMessageElements = await this.all("[data-class='notification-message']", {useBaseSelector: false})
408
+
409
+ for (const notificationMessageElement of notificationMessageElements) {
410
+ const notificationMessage = await notificationMessageElement.getText()
343
411
 
344
- for (const notificationMessage of notificationMessages) {
345
412
  if (!allDetectedNotificationMessages.includes(notificationMessage)) {
346
413
  allDetectedNotificationMessages.push(notificationMessage)
347
414
  }
348
415
 
349
416
  if (notificationMessage == expectedNotificationMessage) {
417
+ foundNotificationMessageElement = notificationMessageElement
350
418
  return
351
419
  }
352
420
  }
353
421
 
354
422
  throw new Error(`Notification message ${expectedNotificationMessage} wasn't included in: ${allDetectedNotificationMessages.join(", ")}`)
355
423
  })
424
+
425
+ if (foundNotificationMessageElement) {
426
+ await this.interact(foundNotificationMessageElement, "click") // Dismiss the notification message
427
+ }
428
+ }
429
+
430
+ /**
431
+ * @returns {Promise<void>}
432
+ */
433
+ async dismissNotificationMessages() {
434
+ const notificationMessageElements = await this.all("[data-class='notification-message']", {useBaseSelector: false})
435
+
436
+ for (const notificationMessageElement of notificationMessageElements) {
437
+ await this.interact(notificationMessageElement, "click")
438
+ }
439
+
440
+ await this.waitForNoSelector("[data-class='notification-message']", {useBaseSelector: false})
356
441
  }
357
442
 
358
443
  /**
359
444
  * Indicates whether the system test has been started
360
- *
361
445
  * @returns {boolean}
362
446
  */
363
447
  isStarted() { return this._started }
364
448
 
365
449
  /**
366
450
  * Gets the HTML of the current page
367
- *
368
451
  * @returns {Promise<string>}
369
452
  */
370
453
  async getHTML() { return await this.driver.getPageSource() }
371
454
 
372
455
  /**
373
456
  * Starts the system test
457
+ * @returns {Promise<void>}
374
458
  */
375
459
  async start() {
376
460
  if (process.env.SYSTEM_TEST_HOST == "expo-dev-server") {
@@ -410,8 +494,7 @@ export default class SystemTest {
410
494
  await this.find("body > #root", {useBaseSelector: false})
411
495
  await this.find("[data-testid='systemTestingComponent']", {visible: null, useBaseSelector: false})
412
496
  } catch (error) {
413
- await systemTest.takeScreenshot()
414
-
497
+ await this.takeScreenshot()
415
498
  throw error
416
499
  }
417
500
 
@@ -419,11 +502,12 @@ export default class SystemTest {
419
502
  await this.waitForClientWebSocket()
420
503
 
421
504
  this._started = true
422
- systemTest.setBaseSelector("[data-testid='systemTestingComponent'][data-focussed='true']")
505
+ this.setBaseSelector("[data-testid='systemTestingComponent'][data-focussed='true']")
423
506
  }
424
507
 
425
508
  /**
426
509
  * Restores previously set timeouts
510
+ * @returns {Promise<void>}
427
511
  */
428
512
  async restoreTimeouts() {
429
513
  if (!this.getTimeouts()) {
@@ -435,8 +519,8 @@ export default class SystemTest {
435
519
 
436
520
  /**
437
521
  * Sets driver timeouts
438
- *
439
522
  * @param {number} newTimeout
523
+ * @returns {Promise<void>}
440
524
  */
441
525
  async driverSetTimeouts(newTimeout) {
442
526
  await this.driver.manage().setTimeouts({implicit: newTimeout})
@@ -444,8 +528,8 @@ export default class SystemTest {
444
528
 
445
529
  /**
446
530
  * Sets timeouts and stores the previous timeouts
447
- *
448
531
  * @param {number} newTimeout
532
+ * @returns {Promise<void>}
449
533
  */
450
534
  async setTimeouts(newTimeout) {
451
535
  this._timeouts = newTimeout
@@ -454,7 +538,6 @@ export default class SystemTest {
454
538
 
455
539
  /**
456
540
  * Waits for the client web socket to connect
457
- *
458
541
  * @returns {Promise<void>}
459
542
  */
460
543
  waitForClientWebSocket() {
@@ -469,6 +552,7 @@ export default class SystemTest {
469
552
 
470
553
  /**
471
554
  * Starts the web socket server
555
+ * @returns {void}
472
556
  */
473
557
  startWebSocketServer() {
474
558
  this.wss = new WebSocketServer({port: 1985})
@@ -478,6 +562,8 @@ export default class SystemTest {
478
562
 
479
563
  /**
480
564
  * Sets the on command callback
565
+ * @param {function(object) : void} callback
566
+ * @returns {void}
481
567
  */
482
568
  onCommand(callback) {
483
569
  this._onCommandCallback = callback
@@ -485,8 +571,8 @@ export default class SystemTest {
485
571
 
486
572
  /**
487
573
  * Handles a command received from the browser
488
- *
489
- * @param {Object} data
574
+ * @param {object} data
575
+ * @param {object} data.data
490
576
  * @returns {Promise<any>}
491
577
  */
492
578
  onCommandReceived = async ({data}) => {
@@ -519,8 +605,8 @@ export default class SystemTest {
519
605
 
520
606
  /**
521
607
  * Handles a new web socket connection
522
- *
523
608
  * @param {WebSocket} ws
609
+ * @returns {void}
524
610
  */
525
611
  onWebSocketConnection = async (ws) => {
526
612
  this.ws = ws
@@ -535,6 +621,9 @@ export default class SystemTest {
535
621
  }
536
622
  }
537
623
 
624
+ /**
625
+ * @returns {void}
626
+ */
538
627
  onWebSocketClose = () => {
539
628
  this.ws = null
540
629
  this.communicator.ws = null
@@ -542,8 +631,8 @@ export default class SystemTest {
542
631
 
543
632
  /**
544
633
  * Handles an error reported from the browser
545
- *
546
- * @param {Object} data
634
+ * @param {object} data
635
+ * @returns {void}
547
636
  */
548
637
  handleError(data) {
549
638
  if (data.message.includes("Minified React error #419")) {
@@ -562,6 +651,7 @@ export default class SystemTest {
562
651
 
563
652
  /**
564
653
  * Stops the system test
654
+ * @returns {Promise<void>}
565
655
  */
566
656
  async stop() {
567
657
  this.stopScoundrel()
@@ -572,8 +662,8 @@ export default class SystemTest {
572
662
 
573
663
  /**
574
664
  * Visits a path in the browser
575
- *
576
665
  * @param {string} path
666
+ * @returns {Promise<void>}
577
667
  */
578
668
  async driverVisit(path) {
579
669
  const url = `${this.currentUrl}${path}`
@@ -583,6 +673,7 @@ export default class SystemTest {
583
673
 
584
674
  /**
585
675
  * Takes a screenshot, saves HTML and browser logs
676
+ * @returns {Promise<void>}
586
677
  */
587
678
  async takeScreenshot() {
588
679
  const path = `${process.cwd()}/tmp/screenshots`
@@ -610,8 +701,8 @@ export default class SystemTest {
610
701
 
611
702
  /**
612
703
  * Visits a path in the browser
613
- *
614
704
  * @param {string} path
705
+ * @returns {Promise<void>}
615
706
  */
616
707
  async visit(path) {
617
708
  await this.communicator.sendCommand({type: "visit", path})
@@ -619,8 +710,8 @@ export default class SystemTest {
619
710
 
620
711
  /**
621
712
  * Dismisses to a path in the browser
622
- *
623
713
  * @param {string} path
714
+ * @returns {Promise<void>}
624
715
  */
625
716
  async dismissTo(path) {
626
717
  await this.communicator.sendCommand({type: "dismissTo", path})
@@ -36,11 +36,9 @@ const getSystemTestBrowserHelper = () => {
36
36
 
37
37
  /**
38
38
  * A hook that provides system test capabilities.
39
- *
40
- * @param {Object} options - Options for the hook.
41
- * @param {Function} options.onInitialize - A callback function that is called when the system test browser helper is initialized.
42
- *
43
- * @returns {Object} An object containing:
39
+ * @param {object} options - Options for the hook.
40
+ * @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:
44
42
  * - enabled: A boolean indicating if system test mode is enabled.
45
43
  * - systemTestBrowserHelper: An instance of SystemTestBrowserHelper if enabled, otherwise null.
46
44
  */