system-testing 1.0.16 → 1.0.18

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.16",
3
+ "version": "1.0.18",
4
4
  "description": "System testing with Selenium and browsers.",
5
5
  "keywords": [
6
6
  "system",
@@ -26,7 +26,7 @@
26
26
  "dependencies": {
27
27
  "awaitery": "^1.0.1",
28
28
  "diggerize": "^1.0.9",
29
- "htmlfy": "^0.8.1",
29
+ "htmlfy": "^1.0.0",
30
30
  "mime": "^4.0.7",
31
31
  "moment": "^2.30.1",
32
32
  "ws": "^8.18.3"
@@ -7,11 +7,18 @@ import moment from "moment"
7
7
  import {prettify} from "htmlfy"
8
8
  import SystemTestCommunicator from "./system-test-communicator.js"
9
9
  import SystemTestHttpServer from "./system-test-http-server.js"
10
+ import {waitFor} from "awaitery"
10
11
  import {WebSocketServer} from "ws"
11
12
 
12
13
  class ElementNotFoundError extends Error { }
13
14
 
14
15
  export default class SystemTest {
16
+ /**
17
+ * Gets the current system test instance
18
+ *
19
+ * @param {object} args
20
+ * @returns {SystemTest}
21
+ */
15
22
  static current(args) {
16
23
  if (!globalThis.systemTest) {
17
24
  globalThis.systemTest = new SystemTest(args)
@@ -20,6 +27,11 @@ export default class SystemTest {
20
27
  return globalThis.systemTest
21
28
  }
22
29
 
30
+ /**
31
+ * Runs a system test
32
+ *
33
+ * @param {function(SystemTest): Promise<void>} callback
34
+ */
23
35
  static async run(callback) {
24
36
  const systemTest = this.current()
25
37
 
@@ -36,6 +48,13 @@ export default class SystemTest {
36
48
  }
37
49
  }
38
50
 
51
+ /**
52
+ * Creates a new SystemTest instance
53
+ *
54
+ * @param {object} args
55
+ * @param {string} args.host
56
+ * @param {number} args.port
57
+ */
39
58
  constructor({host = "localhost", port = 8081, ...restArgs} = {}) {
40
59
  const restArgsKeys = Object.keys(restArgs)
41
60
 
@@ -50,6 +69,14 @@ export default class SystemTest {
50
69
  this._sendCount = 0
51
70
  }
52
71
 
72
+ /**
73
+ * Finds all elements by CSS selector
74
+ *
75
+ * @param {string} selector
76
+ * @param {object} args
77
+ *
78
+ * @returns {import("selenium-webdriver").WebElement[]}
79
+ */
53
80
  async all(selector, args = {}) {
54
81
  const {visible = true} = args
55
82
  const elements = await this.driver.findElements(By.css(selector))
@@ -71,13 +98,24 @@ export default class SystemTest {
71
98
  return activeElements
72
99
  }
73
100
 
74
- // Clicks an element that has children which fills out the element and would otherwise have caused a ElementClickInterceptedError
101
+ /**
102
+ * Clicks an element that has children which fills out the element and would otherwise have caused a ElementClickInterceptedError
103
+ *
104
+ * @param {import("selenium-webdriver").WebElement} element
105
+ **/
75
106
  async click(element) {
76
107
  const actions = this.driver.actions({async: true})
77
108
 
78
109
  await actions.move({origin: element}).click().perform()
79
110
  }
80
111
 
112
+ /**
113
+ * Finds a single element by CSS selector
114
+ *
115
+ * @param {string} selector
116
+ * @param {object} args
117
+ * @returns {import("selenium-webdriver").WebElement}
118
+ */
81
119
  async find(selector, args = {}) {
82
120
  let elements
83
121
 
@@ -99,8 +137,21 @@ export default class SystemTest {
99
137
  return elements[0]
100
138
  }
101
139
 
140
+ /**
141
+ * Finds a single element by test ID
142
+ *
143
+ * @param {string} testID
144
+ * @param {object} args
145
+ * @returns {import("selenium-webdriver").WebElement}
146
+ */
102
147
  async findByTestID(testID, args) { return await this.find(`[data-testid='${testID}']`, args) }
103
148
 
149
+ /**
150
+ * Finds a single element by CSS selector without waiting
151
+ *
152
+ * @param {string} selector
153
+ * @returns {import("selenium-webdriver").WebElement}
154
+ */
104
155
  async findNoWait(selector) {
105
156
  await this.driverSetTimeouts(0)
106
157
 
@@ -111,6 +162,11 @@ export default class SystemTest {
111
162
  }
112
163
  }
113
164
 
165
+ /**
166
+ * Gets browser logs
167
+ *
168
+ * @returns {Promise<string[]>}
169
+ */
114
170
  async getBrowserLogs() {
115
171
  const entries = await this.driver.manage().logs().get(logging.Type.BROWSER)
116
172
  const browserLogs = []
@@ -131,6 +187,54 @@ export default class SystemTest {
131
187
  return browserLogs
132
188
  }
133
189
 
190
+ /**
191
+ * Interacts with an element by calling a method on it with the given arguments.
192
+ * Retrying on ElementNotInteractableError.
193
+ *
194
+ * @param {import("selenium-webdriver").WebElement|string} elementOrIdentifier - The element or a CSS selector to find the element.
195
+ * @param {string} methodName - The method name to call on the element.
196
+ * @param {...any} args - Arguments to pass to the method.
197
+ *
198
+ * @returns {Promise<any>}
199
+ */
200
+ async interact(elementOrIdentifier, methodName, ...args) {
201
+ let element
202
+ let tries = 0
203
+
204
+ while (true) {
205
+ tries++
206
+
207
+ if (typeof elementOrIdentifier == "string") {
208
+ element = await this.find(elementOrIdentifier)
209
+ } else {
210
+ element = elementOrIdentifier
211
+ }
212
+
213
+ if (!element[methodName]) {
214
+ // throw new Error(`${element.constructor.name} has no method named: ${methodName}`)
215
+ }
216
+
217
+ try {
218
+ return await element[methodName](...args)
219
+ } catch (error) {
220
+ if (error.constructor.name === "ElementNotInteractableError") {
221
+ // Retry finding the element and interacting with it
222
+ if (tries >= 3) {
223
+ throw new Error(`${element.constructor.name} ${methodName} failed after ${tries} tries: ${error.message}`)
224
+ }
225
+ } else {
226
+ // Re-throw with un-corrupted stack trace
227
+ throw new Error(`${element.constructor.name} ${methodName} failed: ${error.message}`)
228
+ }
229
+ }
230
+ }
231
+ }
232
+
233
+ /**
234
+ * Expects no element to be found by CSS selector
235
+ *
236
+ * @param {string} selector
237
+ */
134
238
  async expectNoElement(selector) {
135
239
  let found = false
136
240
 
@@ -148,6 +252,11 @@ export default class SystemTest {
148
252
  }
149
253
  }
150
254
 
255
+ /**
256
+ * Gets notification messages
257
+ *
258
+ * @returns {Promise<string[]>}
259
+ */
151
260
  async notificationMessages() {
152
261
  const notificationMessageElements = await this.all("[data-class='notification-message']")
153
262
  const notificationMessageTexts = []
@@ -161,9 +270,48 @@ export default class SystemTest {
161
270
  return notificationMessageTexts
162
271
  }
163
272
 
273
+ /**
274
+ * Expects a notification message to appear and waits for it if necessary.
275
+ *
276
+ * @param {string} expectedNotificationMessage
277
+ */
278
+ async expectNotificationMessage(expectedNotificationMessage) {
279
+ const allDetectedNotificationMessages = []
280
+
281
+ await waitFor(async () => {
282
+ const notificationMessages = await this.notificationMessages()
283
+
284
+ for (const notificationMessage of notificationMessages) {
285
+ if (!allDetectedNotificationMessages.includes(notificationMessage)) {
286
+ allDetectedNotificationMessages.push(notificationMessage)
287
+ }
288
+
289
+ if (notificationMessage == expectedNotificationMessage) {
290
+ return
291
+ }
292
+ }
293
+
294
+ throw new Error(`Notification message ${expectedNotificationMessage} wasn't included in: ${allDetectedNotificationMessages.join(", ")}`)
295
+ })
296
+ }
297
+
298
+ /**
299
+ * Indicates whether the system test has been started
300
+ *
301
+ * @returns {boolean}
302
+ */
164
303
  isStarted() { return this._started }
304
+
305
+ /**
306
+ * Gets the HTML of the current page
307
+ *
308
+ * @returns {Promise<string>}
309
+ */
165
310
  async getHTML() { return await this.driver.getPageSource() }
166
311
 
312
+ /**
313
+ * Starts the system test
314
+ */
167
315
  async start() {
168
316
  if (process.env.SYSTEM_TEST_HOST == "expo-dev-server") {
169
317
  this.currentUrl = `http://${this._host}:${this._port}`
@@ -213,6 +361,9 @@ export default class SystemTest {
213
361
  this._started = true
214
362
  }
215
363
 
364
+ /**
365
+ * Restores previously set timeouts
366
+ */
216
367
  async restoreTimeouts() {
217
368
  if (!this._timeouts) {
218
369
  throw new Error("Timeouts haven't previously been set")
@@ -221,15 +372,30 @@ export default class SystemTest {
221
372
  await this.driverSetTimeouts(this._timeouts)
222
373
  }
223
374
 
375
+ /**
376
+ * Sets driver timeouts
377
+ *
378
+ * @param {number} newTimeout
379
+ */
224
380
  async driverSetTimeouts(newTimeout) {
225
381
  await this.driver.manage().setTimeouts({implicit: newTimeout})
226
382
  }
227
383
 
384
+ /**
385
+ * Sets timeouts and stores the previous timeouts
386
+ *
387
+ * @param {number} newTimeout
388
+ */
228
389
  async setTimeouts(newTimeout) {
229
390
  this._timeouts = newTimeout
230
391
  await this.restoreTimeouts()
231
392
  }
232
393
 
394
+ /**
395
+ * Waits for the client web socket to connect
396
+ *
397
+ * @returns {Promise<void>}
398
+ */
233
399
  waitForClientWebSocket() {
234
400
  return new Promise((resolve) => {
235
401
  if (this.ws) {
@@ -240,6 +406,9 @@ export default class SystemTest {
240
406
  })
241
407
  }
242
408
 
409
+ /**
410
+ * Starts the web socket server
411
+ */
243
412
  startWebSocketServer() {
244
413
  this.wss = new WebSocketServer({port: 1985})
245
414
  this.wss.on("connection", this.onWebSocketConnection)
@@ -311,18 +480,29 @@ export default class SystemTest {
311
480
  console.error(error)
312
481
  }
313
482
 
483
+ /**
484
+ * Stops the system test
485
+ */
314
486
  async stop() {
315
487
  this.systemTestHttpServer?.close()
316
488
  this.wss?.close()
317
489
  await this.driver.quit()
318
490
  }
319
491
 
492
+ /**
493
+ * Visits a path in the browser
494
+ *
495
+ * @param {string} path
496
+ */
320
497
  async driverVisit(path) {
321
498
  const url = `${this.currentUrl}${path}`
322
499
 
323
500
  await this.driver.get(url)
324
501
  }
325
502
 
503
+ /**
504
+ * Takes a screenshot, saves HTML and browser logs
505
+ */
326
506
  async takeScreenshot() {
327
507
  const path = `${process.cwd()}/tmp/screenshots`
328
508
 
@@ -346,6 +526,11 @@ export default class SystemTest {
346
526
  console.log("HTML:", htmlPath)
347
527
  }
348
528
 
529
+ /**
530
+ * Visits a path in the browser
531
+ *
532
+ * @param {string} path
533
+ */
349
534
  async visit(path) {
350
535
  await this.communicator.sendCommand({type: "visit", path})
351
536
  }
@@ -34,6 +34,16 @@ const getSystemTestBrowserHelper = () => {
34
34
  return shared.systemTestBrowserHelper
35
35
  }
36
36
 
37
+ /**
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:
44
+ * - enabled: A boolean indicating if system test mode is enabled.
45
+ * - systemTestBrowserHelper: An instance of SystemTestBrowserHelper if enabled, otherwise null.
46
+ */
37
47
  export default function useSystemTest({onInitialize, ...restArgs} = {}) {
38
48
  const router = useRouter()
39
49
  const enabled = useMemo(() => isSystemTestEnabled(), [])