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 +2 -2
- package/src/system-test.js +186 -1
- package/src/use-system-test.js +10 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "system-testing",
|
|
3
|
-
"version": "1.0.
|
|
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.
|
|
29
|
+
"htmlfy": "^1.0.0",
|
|
30
30
|
"mime": "^4.0.7",
|
|
31
31
|
"moment": "^2.30.1",
|
|
32
32
|
"ws": "^8.18.3"
|
package/src/system-test.js
CHANGED
|
@@ -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
|
-
|
|
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
|
}
|
package/src/use-system-test.js
CHANGED
|
@@ -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(), [])
|