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 +63 -0
- package/eslint.config.js +23 -0
- package/package.json +8 -1
- package/peak_flow.yml +4 -0
- package/src/system-test-browser-helper.js +1 -2
- package/src/system-test-communicator.js +2 -3
- package/src/system-test-http-server.js +2 -3
- package/src/system-test.js +147 -56
- package/src/use-system-test.js +3 -5
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
|
+
```
|
package/eslint.config.js
ADDED
|
@@ -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.
|
|
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
|
@@ -101,8 +101,7 @@ export default class SystemTestBrowserHelper {
|
|
|
101
101
|
}
|
|
102
102
|
|
|
103
103
|
connectWebSocket() {
|
|
104
|
-
this.ws = new WebSocket("ws://localhost:1985")
|
|
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
|
-
* @
|
|
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}/../..`)
|
|
42
|
-
|
|
41
|
+
this.basePath = await fs.realpath(`${__dirname}/../..`)
|
|
43
42
|
await this.startHttpServer()
|
|
44
43
|
}
|
|
45
44
|
|
package/src/system-test.js
CHANGED
|
@@ -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
|
-
* @
|
|
153
|
-
|
|
154
|
-
async click(
|
|
155
|
-
|
|
156
|
-
|
|
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
|
-
|
|
153
|
+
try {
|
|
154
|
+
const element = await this._findElement(elementOrIdentifier)
|
|
155
|
+
const actions = this.driver.actions({async: true})
|
|
160
156
|
|
|
161
|
-
|
|
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
|
-
* @
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
|
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
|
|
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
|
-
|
|
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 {
|
|
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
|
-
* @
|
|
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})
|
package/src/use-system-test.js
CHANGED
|
@@ -36,11 +36,9 @@ const getSystemTestBrowserHelper = () => {
|
|
|
36
36
|
|
|
37
37
|
/**
|
|
38
38
|
* A hook that provides system test capabilities.
|
|
39
|
-
*
|
|
40
|
-
* @param {
|
|
41
|
-
* @
|
|
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
|
*/
|