system-testing 1.0.25 → 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.
@@ -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.25",
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,9 +141,9 @@ 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
- **/
144
+ * @param {string|import("selenium-webdriver").WebElement} elementOrIdentifier
145
+ * @returns {Promise<void>}
146
+ */
154
147
  async click(elementOrIdentifier) {
155
148
  let tries = 0
156
149
 
@@ -166,13 +159,13 @@ export default class SystemTest {
166
159
  } catch (error) {
167
160
  if (error.constructor.name === "ElementNotInteractableError") {
168
161
  if (tries >= 3) {
169
- throw new Error(`Element ${element.constructor.name} click failed after ${tries} tries - ${error.constructor.name}: ${error.message}`)
162
+ throw new Error(`Element ${elementOrIdentifier.constructor.name} click failed after ${tries} tries - ${error.constructor.name}: ${error.message}`)
170
163
  } else {
171
164
  await wait(50)
172
165
  }
173
166
  } else {
174
167
  // Re-throw with un-corrupted stack trace
175
- throw new Error(`Element ${element.constructor.name} click failed - ${error.constructor.name}: ${error.message}`)
168
+ throw new Error(`Element ${elementOrIdentifier.constructor.name} click failed - ${error.constructor.name}: ${error.message}`)
176
169
  }
177
170
  }
178
171
  }
@@ -180,7 +173,6 @@ export default class SystemTest {
180
173
 
181
174
  /**
182
175
  * Finds a single element by CSS selector
183
- *
184
176
  * @param {string} selector
185
177
  * @param {object} args
186
178
  * @returns {import("selenium-webdriver").WebElement}
@@ -208,13 +200,16 @@ export default class SystemTest {
208
200
 
209
201
  /**
210
202
  * Finds a single element by test ID
211
- *
212
203
  * @param {string} testID
213
204
  * @param {object} args
214
- * @returns {import("selenium-webdriver").WebElement}
205
+ * @returns {Promise<import("selenium-webdriver").WebElement>}
215
206
  */
216
207
  async findByTestID(testID, args) { return await this.find(`[data-testid='${testID}']`, args) }
217
208
 
209
+ /**
210
+ * @param {string|import("selenium-webdriver").WebElement} elementOrIdentifier
211
+ * @returns {Promise<import("selenium-webdriver").WebElement>}
212
+ */
218
213
  async _findElement(elementOrIdentifier) {
219
214
  let element
220
215
 
@@ -229,9 +224,9 @@ export default class SystemTest {
229
224
 
230
225
  /**
231
226
  * Finds a single element by CSS selector without waiting
232
- *
233
227
  * @param {string} selector
234
- * @returns {import("selenium-webdriver").WebElement}
228
+ * @param {object} args
229
+ * @returns {Promise<import("selenium-webdriver").WebElement>}
235
230
  */
236
231
  async findNoWait(selector, args) {
237
232
  await this.driverSetTimeouts(0)
@@ -245,7 +240,6 @@ export default class SystemTest {
245
240
 
246
241
  /**
247
242
  * Gets browser logs
248
- *
249
243
  * @returns {Promise<string[]>}
250
244
  */
251
245
  async getBrowserLogs() {
@@ -268,20 +262,24 @@ export default class SystemTest {
268
262
  return browserLogs
269
263
  }
270
264
 
265
+ /**
266
+ * @returns {Promise<string>}
267
+ */
271
268
  async getCurrentUrl() {
272
269
  return await this.driver.getCurrentUrl()
273
270
  }
274
271
 
272
+ /**
273
+ * @returns {number}
274
+ */
275
275
  getTimeouts() { return this._timeouts }
276
276
 
277
277
  /**
278
278
  * Interacts with an element by calling a method on it with the given arguments.
279
279
  * Retrying on ElementNotInteractableError.
280
- *
281
280
  * @param {import("selenium-webdriver").WebElement|string} elementOrIdentifier - The element or a CSS selector to find the element.
282
281
  * @param {string} methodName - The method name to call on the element.
283
282
  * @param {...any} args - Arguments to pass to the method.
284
- *
285
283
  * @returns {Promise<any>}
286
284
  */
287
285
  async interact(elementOrIdentifier, methodName, ...args) {
@@ -326,8 +324,8 @@ export default class SystemTest {
326
324
 
327
325
  /**
328
326
  * Expects no element to be found by CSS selector
329
- *
330
327
  * @param {string} selector
328
+ * @returns {Promise<void>}
331
329
  */
332
330
  async expectNoElement(selector) {
333
331
  let found = false
@@ -346,9 +344,41 @@ export default class SystemTest {
346
344
  }
347
345
  }
348
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
+
349
380
  /**
350
381
  * Gets notification messages
351
- *
352
382
  * @returns {Promise<string[]>}
353
383
  */
354
384
  async notificationMessages() {
@@ -366,45 +396,65 @@ export default class SystemTest {
366
396
 
367
397
  /**
368
398
  * Expects a notification message to appear and waits for it if necessary.
369
- *
370
399
  * @param {string} expectedNotificationMessage
400
+ * @returns {Promise<void>}
371
401
  */
372
402
  async expectNotificationMessage(expectedNotificationMessage) {
373
403
  const allDetectedNotificationMessages = []
404
+ let foundNotificationMessageElement
374
405
 
375
406
  await waitFor(async () => {
376
- 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()
377
411
 
378
- for (const notificationMessage of notificationMessages) {
379
412
  if (!allDetectedNotificationMessages.includes(notificationMessage)) {
380
413
  allDetectedNotificationMessages.push(notificationMessage)
381
414
  }
382
415
 
383
416
  if (notificationMessage == expectedNotificationMessage) {
417
+ foundNotificationMessageElement = notificationMessageElement
384
418
  return
385
419
  }
386
420
  }
387
421
 
388
422
  throw new Error(`Notification message ${expectedNotificationMessage} wasn't included in: ${allDetectedNotificationMessages.join(", ")}`)
389
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})
390
441
  }
391
442
 
392
443
  /**
393
444
  * Indicates whether the system test has been started
394
- *
395
445
  * @returns {boolean}
396
446
  */
397
447
  isStarted() { return this._started }
398
448
 
399
449
  /**
400
450
  * Gets the HTML of the current page
401
- *
402
451
  * @returns {Promise<string>}
403
452
  */
404
453
  async getHTML() { return await this.driver.getPageSource() }
405
454
 
406
455
  /**
407
456
  * Starts the system test
457
+ * @returns {Promise<void>}
408
458
  */
409
459
  async start() {
410
460
  if (process.env.SYSTEM_TEST_HOST == "expo-dev-server") {
@@ -444,8 +494,7 @@ export default class SystemTest {
444
494
  await this.find("body > #root", {useBaseSelector: false})
445
495
  await this.find("[data-testid='systemTestingComponent']", {visible: null, useBaseSelector: false})
446
496
  } catch (error) {
447
- await systemTest.takeScreenshot()
448
-
497
+ await this.takeScreenshot()
449
498
  throw error
450
499
  }
451
500
 
@@ -453,11 +502,12 @@ export default class SystemTest {
453
502
  await this.waitForClientWebSocket()
454
503
 
455
504
  this._started = true
456
- systemTest.setBaseSelector("[data-testid='systemTestingComponent'][data-focussed='true']")
505
+ this.setBaseSelector("[data-testid='systemTestingComponent'][data-focussed='true']")
457
506
  }
458
507
 
459
508
  /**
460
509
  * Restores previously set timeouts
510
+ * @returns {Promise<void>}
461
511
  */
462
512
  async restoreTimeouts() {
463
513
  if (!this.getTimeouts()) {
@@ -469,8 +519,8 @@ export default class SystemTest {
469
519
 
470
520
  /**
471
521
  * Sets driver timeouts
472
- *
473
522
  * @param {number} newTimeout
523
+ * @returns {Promise<void>}
474
524
  */
475
525
  async driverSetTimeouts(newTimeout) {
476
526
  await this.driver.manage().setTimeouts({implicit: newTimeout})
@@ -478,8 +528,8 @@ export default class SystemTest {
478
528
 
479
529
  /**
480
530
  * Sets timeouts and stores the previous timeouts
481
- *
482
531
  * @param {number} newTimeout
532
+ * @returns {Promise<void>}
483
533
  */
484
534
  async setTimeouts(newTimeout) {
485
535
  this._timeouts = newTimeout
@@ -488,7 +538,6 @@ export default class SystemTest {
488
538
 
489
539
  /**
490
540
  * Waits for the client web socket to connect
491
- *
492
541
  * @returns {Promise<void>}
493
542
  */
494
543
  waitForClientWebSocket() {
@@ -503,6 +552,7 @@ export default class SystemTest {
503
552
 
504
553
  /**
505
554
  * Starts the web socket server
555
+ * @returns {void}
506
556
  */
507
557
  startWebSocketServer() {
508
558
  this.wss = new WebSocketServer({port: 1985})
@@ -512,6 +562,8 @@ export default class SystemTest {
512
562
 
513
563
  /**
514
564
  * Sets the on command callback
565
+ * @param {function(object) : void} callback
566
+ * @returns {void}
515
567
  */
516
568
  onCommand(callback) {
517
569
  this._onCommandCallback = callback
@@ -519,8 +571,8 @@ export default class SystemTest {
519
571
 
520
572
  /**
521
573
  * Handles a command received from the browser
522
- *
523
- * @param {Object} data
574
+ * @param {object} data
575
+ * @param {object} data.data
524
576
  * @returns {Promise<any>}
525
577
  */
526
578
  onCommandReceived = async ({data}) => {
@@ -553,8 +605,8 @@ export default class SystemTest {
553
605
 
554
606
  /**
555
607
  * Handles a new web socket connection
556
- *
557
608
  * @param {WebSocket} ws
609
+ * @returns {void}
558
610
  */
559
611
  onWebSocketConnection = async (ws) => {
560
612
  this.ws = ws
@@ -569,6 +621,9 @@ export default class SystemTest {
569
621
  }
570
622
  }
571
623
 
624
+ /**
625
+ * @returns {void}
626
+ */
572
627
  onWebSocketClose = () => {
573
628
  this.ws = null
574
629
  this.communicator.ws = null
@@ -576,8 +631,8 @@ export default class SystemTest {
576
631
 
577
632
  /**
578
633
  * Handles an error reported from the browser
579
- *
580
- * @param {Object} data
634
+ * @param {object} data
635
+ * @returns {void}
581
636
  */
582
637
  handleError(data) {
583
638
  if (data.message.includes("Minified React error #419")) {
@@ -596,6 +651,7 @@ export default class SystemTest {
596
651
 
597
652
  /**
598
653
  * Stops the system test
654
+ * @returns {Promise<void>}
599
655
  */
600
656
  async stop() {
601
657
  this.stopScoundrel()
@@ -606,8 +662,8 @@ export default class SystemTest {
606
662
 
607
663
  /**
608
664
  * Visits a path in the browser
609
- *
610
665
  * @param {string} path
666
+ * @returns {Promise<void>}
611
667
  */
612
668
  async driverVisit(path) {
613
669
  const url = `${this.currentUrl}${path}`
@@ -617,6 +673,7 @@ export default class SystemTest {
617
673
 
618
674
  /**
619
675
  * Takes a screenshot, saves HTML and browser logs
676
+ * @returns {Promise<void>}
620
677
  */
621
678
  async takeScreenshot() {
622
679
  const path = `${process.cwd()}/tmp/screenshots`
@@ -644,8 +701,8 @@ export default class SystemTest {
644
701
 
645
702
  /**
646
703
  * Visits a path in the browser
647
- *
648
704
  * @param {string} path
705
+ * @returns {Promise<void>}
649
706
  */
650
707
  async visit(path) {
651
708
  await this.communicator.sendCommand({type: "visit", path})
@@ -653,8 +710,8 @@ export default class SystemTest {
653
710
 
654
711
  /**
655
712
  * Dismisses to a path in the browser
656
- *
657
713
  * @param {string} path
714
+ * @returns {Promise<void>}
658
715
  */
659
716
  async dismissTo(path) {
660
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
  */