system-testing 1.0.27 → 1.0.30
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 +9 -4
- package/peak_flow.yml +1 -0
- package/src/system-test-browser-helper.js +46 -8
- package/src/system-test-communicator.js +38 -4
- package/src/system-test-http-server.js +19 -2
- package/src/system-test.js +112 -62
- package/src/use-system-test.js +2 -2
- package/tsconfig.json +14 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "system-testing",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.30",
|
|
4
4
|
"description": "System testing with Selenium and browsers.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"system",
|
|
@@ -22,10 +22,11 @@
|
|
|
22
22
|
"main": "src/index.js",
|
|
23
23
|
"scripts": {
|
|
24
24
|
"lint": "eslint",
|
|
25
|
-
"test": "echo \"Error: no test specified\" && exit 1"
|
|
25
|
+
"test": "echo \"Error: no test specified\" && exit 1",
|
|
26
|
+
"typecheck": "tsc --noEmit"
|
|
26
27
|
},
|
|
27
28
|
"dependencies": {
|
|
28
|
-
"awaitery": "^1.0.
|
|
29
|
+
"awaitery": "^1.0.2",
|
|
29
30
|
"diggerize": "^1.0.9",
|
|
30
31
|
"htmlfy": "^1.0.0",
|
|
31
32
|
"mime": "^4.0.7",
|
|
@@ -34,12 +35,16 @@
|
|
|
34
35
|
"ws": "^8.18.3"
|
|
35
36
|
},
|
|
36
37
|
"peerDependencies": {
|
|
38
|
+
"@kaspernj/api-maker": "^1.0.2053",
|
|
39
|
+
"expo-router": "^6.0.19",
|
|
37
40
|
"selenium-webdriver": "^4.34.0"
|
|
38
41
|
},
|
|
39
42
|
"devDependencies": {
|
|
40
43
|
"@eslint/js": "^9.39.1",
|
|
44
|
+
"@types/node": "^25.0.2",
|
|
41
45
|
"eslint": "^9.39.1",
|
|
42
46
|
"eslint-plugin-jsdoc": "^61.4.1",
|
|
43
|
-
"globals": "^16.5.0"
|
|
47
|
+
"globals": "^16.5.0",
|
|
48
|
+
"typescript": "^5.9.3"
|
|
44
49
|
}
|
|
45
50
|
}
|
package/peak_flow.yml
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
1
3
|
import Client from "scoundrel-remote-eval/src/client/index.js"
|
|
2
4
|
import ClientWebSocket from "scoundrel-remote-eval/src/client/connections/web-socket/index.js"
|
|
3
5
|
import {digg} from "diggerize"
|
|
@@ -5,7 +7,10 @@ import EventEmitter from "events"
|
|
|
5
7
|
|
|
6
8
|
import SystemTestCommunicator from "./system-test-communicator.js"
|
|
7
9
|
|
|
8
|
-
|
|
10
|
+
/** @type {{systemTestBrowserHelper: SystemTestBrowserHelper | null}} */
|
|
11
|
+
const shared = {
|
|
12
|
+
systemTestBrowserHelper: null
|
|
13
|
+
}
|
|
9
14
|
|
|
10
15
|
export default class SystemTestBrowserHelper {
|
|
11
16
|
static current() {
|
|
@@ -39,10 +44,10 @@ export default class SystemTestBrowserHelper {
|
|
|
39
44
|
waitForScoundrelStarted() {
|
|
40
45
|
return new Promise((resolve) => {
|
|
41
46
|
if (this.scoundrelClient) {
|
|
42
|
-
resolve()
|
|
47
|
+
resolve(undefined)
|
|
43
48
|
} else {
|
|
44
49
|
this.events.once("scoundrelStarted", () => {
|
|
45
|
-
resolve()
|
|
50
|
+
resolve(undefined)
|
|
46
51
|
})
|
|
47
52
|
}
|
|
48
53
|
})
|
|
@@ -76,14 +81,24 @@ export default class SystemTestBrowserHelper {
|
|
|
76
81
|
type: "unhandledrejection",
|
|
77
82
|
error: event.reason,
|
|
78
83
|
errorClass: "UnhandledRejection",
|
|
79
|
-
file: null,
|
|
80
|
-
line: null,
|
|
81
84
|
message: event.reason.message || event.reason || "Unhandled promise rejection without a message",
|
|
82
85
|
url: window.location.href
|
|
83
86
|
})
|
|
84
87
|
})
|
|
85
88
|
}
|
|
86
89
|
|
|
90
|
+
/**
|
|
91
|
+
* @param {object} data
|
|
92
|
+
* @param {string} [data.backtrace]
|
|
93
|
+
* @param {Error} [data.error]
|
|
94
|
+
* @param {string} [data.errorClass]
|
|
95
|
+
* @param {string} [data.file]
|
|
96
|
+
* @param {number} [data.line]
|
|
97
|
+
* @param {string} [data.message]
|
|
98
|
+
* @param {string} [data.type]
|
|
99
|
+
* @param {string} [data.url]
|
|
100
|
+
* @returns {void}
|
|
101
|
+
*/
|
|
87
102
|
handleError(data) {
|
|
88
103
|
let backtrace
|
|
89
104
|
|
|
@@ -92,7 +107,7 @@ export default class SystemTestBrowserHelper {
|
|
|
92
107
|
backtrace.shift()
|
|
93
108
|
backtrace = backtrace.join("\n")
|
|
94
109
|
} else if (data.file) {
|
|
95
|
-
backtrace =
|
|
110
|
+
backtrace = `${data.file}:${data.line}`
|
|
96
111
|
}
|
|
97
112
|
|
|
98
113
|
data.backtrace = backtrace
|
|
@@ -132,18 +147,35 @@ export default class SystemTestBrowserHelper {
|
|
|
132
147
|
*/
|
|
133
148
|
getEvents() { return this.events }
|
|
134
149
|
|
|
150
|
+
/**
|
|
151
|
+
* @param {any[]} args
|
|
152
|
+
* @returns {void}
|
|
153
|
+
*/
|
|
135
154
|
fakeConsoleError = (...args) => {
|
|
136
155
|
this.communicator.sendCommand({type: "console.error", value: this.consoleLogMessage(args)})
|
|
137
156
|
|
|
138
|
-
|
|
157
|
+
if (this.originalConsoleError) {
|
|
158
|
+
return this.originalConsoleError(...args)
|
|
159
|
+
}
|
|
139
160
|
}
|
|
140
161
|
|
|
162
|
+
/**
|
|
163
|
+
* @param {any[]} args
|
|
164
|
+
* @returns {void}
|
|
165
|
+
*/
|
|
141
166
|
fakeConsoleLog = (...args) => {
|
|
142
167
|
this.communicator.sendCommand({type: "console.log", value: this.consoleLogMessage(args)})
|
|
143
168
|
|
|
144
|
-
|
|
169
|
+
if (this.originalConsoleLog) {
|
|
170
|
+
return this.originalConsoleLog(...args)
|
|
171
|
+
}
|
|
145
172
|
}
|
|
146
173
|
|
|
174
|
+
/**
|
|
175
|
+
* @param {any} arg
|
|
176
|
+
* @param {any[]} [scannedObjects]
|
|
177
|
+
* @returns {any}
|
|
178
|
+
*/
|
|
147
179
|
consoleLogMessage(arg, scannedObjects = []) {
|
|
148
180
|
if (Array.isArray(arg)) {
|
|
149
181
|
if (scannedObjects.includes(arg)) {
|
|
@@ -166,6 +198,7 @@ export default class SystemTestBrowserHelper {
|
|
|
166
198
|
scannedObjects.push(arg)
|
|
167
199
|
}
|
|
168
200
|
|
|
201
|
+
/** @type {Record<string, any>} */
|
|
169
202
|
const result = {}
|
|
170
203
|
|
|
171
204
|
for (const key in arg) {
|
|
@@ -180,6 +213,10 @@ export default class SystemTestBrowserHelper {
|
|
|
180
213
|
}
|
|
181
214
|
}
|
|
182
215
|
|
|
216
|
+
/**
|
|
217
|
+
* @param {{data: {path: string, type: string}}} args
|
|
218
|
+
* @returns {Promise<{result: string} | void>}
|
|
219
|
+
*/
|
|
183
220
|
onCommand = async ({data}) => {
|
|
184
221
|
if (data.type == "initialize") {
|
|
185
222
|
this.events.emit("initialize")
|
|
@@ -226,6 +263,7 @@ export default class SystemTestBrowserHelper {
|
|
|
226
263
|
* @returns {Promise<Array<Record<string, any>>>}
|
|
227
264
|
*/
|
|
228
265
|
async sendQuery(sql) {
|
|
266
|
+
// @ts-expect-error
|
|
229
267
|
return await this.communicator.sendCommand({type: "query", sql})
|
|
230
268
|
}
|
|
231
269
|
}
|
|
@@ -1,9 +1,23 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
1
3
|
export default class SystemTestCommunicator {
|
|
4
|
+
/** @type {WebSocket | null} */
|
|
5
|
+
ws = null
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* @param {object} args
|
|
9
|
+
* @param {(args: Record<string, any>) => Promise<{result: string} | void>} args.onCommand
|
|
10
|
+
* @param {object} [args.parent]
|
|
11
|
+
*/
|
|
2
12
|
constructor({onCommand, parent}) {
|
|
3
13
|
this.onCommand = onCommand
|
|
4
14
|
this.parent = parent
|
|
5
15
|
this._sendQueueCount = 0
|
|
16
|
+
|
|
17
|
+
/** @type {Record<string, any>} */
|
|
6
18
|
this._sendQueue = []
|
|
19
|
+
|
|
20
|
+
/** @type {Record<string, {resolve: (data: any) => void, reject: (data: any) => void}>} */
|
|
7
21
|
this._responses = {}
|
|
8
22
|
}
|
|
9
23
|
|
|
@@ -11,15 +25,22 @@ export default class SystemTestCommunicator {
|
|
|
11
25
|
while (this._sendQueue.length !== 0) {
|
|
12
26
|
const data = this._sendQueue.shift()
|
|
13
27
|
|
|
28
|
+
if (!this.ws || this.ws.readyState !== 1) {
|
|
29
|
+
throw new Error("WebSocket is not open")
|
|
30
|
+
}
|
|
31
|
+
|
|
14
32
|
this.ws.send(JSON.stringify(data))
|
|
15
33
|
}
|
|
16
34
|
}
|
|
17
35
|
|
|
36
|
+
/** @param {Error} error */
|
|
18
37
|
onError = (error) => {
|
|
19
38
|
console.error("onWebSocketClientError", error)
|
|
20
39
|
}
|
|
21
40
|
|
|
41
|
+
/** @param {string} rawData */
|
|
22
42
|
onMessage = async (rawData) => {
|
|
43
|
+
/** @type {{data: any, id: number, type: string, isTrusted?: boolean}} */
|
|
23
44
|
const data = JSON.parse(rawData)
|
|
24
45
|
|
|
25
46
|
if (data.isTrusted) {
|
|
@@ -30,7 +51,11 @@ export default class SystemTestCommunicator {
|
|
|
30
51
|
|
|
31
52
|
this.respond(data.id, {result})
|
|
32
53
|
} catch (error) {
|
|
33
|
-
|
|
54
|
+
if (error instanceof Error) {
|
|
55
|
+
this.respond(data.id, {error: error.message})
|
|
56
|
+
} else {
|
|
57
|
+
this.respond(data.id, {error: error})
|
|
58
|
+
}
|
|
34
59
|
}
|
|
35
60
|
} else if (data.type == "response") {
|
|
36
61
|
const response = this._responses[data.id]
|
|
@@ -42,7 +67,7 @@ export default class SystemTestCommunicator {
|
|
|
42
67
|
delete this._responses[data.id]
|
|
43
68
|
|
|
44
69
|
if (data.data.error) {
|
|
45
|
-
response.
|
|
70
|
+
response.reject(data.data.error)
|
|
46
71
|
} else {
|
|
47
72
|
response.resolve(data.data.result)
|
|
48
73
|
}
|
|
@@ -55,6 +80,10 @@ export default class SystemTestCommunicator {
|
|
|
55
80
|
this.flushSendQueue()
|
|
56
81
|
}
|
|
57
82
|
|
|
83
|
+
/**
|
|
84
|
+
* @param {object} data
|
|
85
|
+
* @returns {void}
|
|
86
|
+
*/
|
|
58
87
|
send(data) {
|
|
59
88
|
this._sendQueue.push(data)
|
|
60
89
|
|
|
@@ -69,15 +98,20 @@ export default class SystemTestCommunicator {
|
|
|
69
98
|
* @returns {Promise<void>} A promise that resolves with the response data.
|
|
70
99
|
*/
|
|
71
100
|
sendCommand(data) {
|
|
72
|
-
return new Promise((resolve,
|
|
101
|
+
return new Promise((resolve, reject) => {
|
|
73
102
|
const id = this._sendQueueCount
|
|
74
103
|
|
|
75
104
|
this._sendQueueCount += 1
|
|
76
|
-
this._responses[id] = {resolve,
|
|
105
|
+
this._responses[id] = {resolve, reject}
|
|
77
106
|
this.send({type: "command", id, data})
|
|
78
107
|
})
|
|
79
108
|
}
|
|
80
109
|
|
|
110
|
+
/**
|
|
111
|
+
* @param {number} id
|
|
112
|
+
* @param {object} data
|
|
113
|
+
* @returns {void}
|
|
114
|
+
*/
|
|
81
115
|
respond(id, data) {
|
|
82
116
|
this.send({type: "response", id, data})
|
|
83
117
|
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
1
3
|
import fs from "node:fs/promises"
|
|
2
4
|
import http from "node:http"
|
|
3
5
|
import mime from "mime"
|
|
@@ -6,10 +8,21 @@ import url from "url"
|
|
|
6
8
|
export default class SystemTestHttpServer {
|
|
7
9
|
/** @returns {void} */
|
|
8
10
|
close() {
|
|
9
|
-
this.httpServer
|
|
11
|
+
this.httpServer?.close()
|
|
10
12
|
}
|
|
11
13
|
|
|
14
|
+
/**
|
|
15
|
+
* @param {http.IncomingMessage} request
|
|
16
|
+
* @param {http.ServerResponse} response
|
|
17
|
+
* @returns {Promise<void>}
|
|
18
|
+
*/
|
|
12
19
|
onHttpServerRequest = async (request, response) => {
|
|
20
|
+
if (!request.url) {
|
|
21
|
+
response.statusCode = 400
|
|
22
|
+
response.end("Bad Request")
|
|
23
|
+
return
|
|
24
|
+
}
|
|
25
|
+
|
|
13
26
|
const parsedUrl = url.parse(request.url)
|
|
14
27
|
let filePath = `${process.cwd()}/dist${parsedUrl.pathname}`
|
|
15
28
|
|
|
@@ -34,7 +47,11 @@ export default class SystemTestHttpServer {
|
|
|
34
47
|
const mimeType = mime.getType(filePath)
|
|
35
48
|
|
|
36
49
|
response.statusCode = 200
|
|
37
|
-
|
|
50
|
+
|
|
51
|
+
if (mimeType) {
|
|
52
|
+
response.setHeader("Content-Type", mimeType)
|
|
53
|
+
}
|
|
54
|
+
|
|
38
55
|
response.end(fileContent)
|
|
39
56
|
}
|
|
40
57
|
|
package/src/system-test.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
1
3
|
import {Builder, By, until} from "selenium-webdriver"
|
|
2
4
|
import chrome from "selenium-webdriver/chrome.js"
|
|
3
|
-
import {digg} from "diggerize"
|
|
4
5
|
import fs from "node:fs/promises"
|
|
5
6
|
import logging from "selenium-webdriver/lib/logging.js"
|
|
6
7
|
import moment from "moment"
|
|
@@ -14,20 +15,40 @@ import {WebSocketServer} from "ws"
|
|
|
14
15
|
|
|
15
16
|
class ElementNotFoundError extends Error { }
|
|
16
17
|
|
|
18
|
+
/** @type {{systemTest: SystemTest | null}} */
|
|
19
|
+
const shared = {
|
|
20
|
+
systemTest: null
|
|
21
|
+
}
|
|
22
|
+
|
|
17
23
|
export default class SystemTest {
|
|
18
24
|
static rootPath = "/blank?systemTest=true"
|
|
19
25
|
|
|
26
|
+
/** @type {SystemTestCommunicator | undefined} */
|
|
27
|
+
communicator = undefined
|
|
28
|
+
_started = false
|
|
29
|
+
_timeouts = 5000
|
|
30
|
+
|
|
20
31
|
/**
|
|
21
32
|
* Gets the current system test instance
|
|
22
|
-
* @param {object} args
|
|
33
|
+
* @param {object} [args]
|
|
34
|
+
* @param {string} [args.host]
|
|
35
|
+
* @param {number} [args.port]
|
|
23
36
|
* @returns {SystemTest}
|
|
24
37
|
*/
|
|
25
38
|
static current(args) {
|
|
26
|
-
if (!
|
|
27
|
-
|
|
39
|
+
if (!shared.systemTest) {
|
|
40
|
+
shared.systemTest = new SystemTest(args)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return shared.systemTest
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
getCommunicator() {
|
|
47
|
+
if (!this.communicator) {
|
|
48
|
+
throw new Error("Communicator hasn't been initialized yet")
|
|
28
49
|
}
|
|
29
50
|
|
|
30
|
-
return
|
|
51
|
+
return this.communicator
|
|
31
52
|
}
|
|
32
53
|
|
|
33
54
|
/**
|
|
@@ -38,7 +59,7 @@ export default class SystemTest {
|
|
|
38
59
|
static async run(callback) {
|
|
39
60
|
const systemTest = this.current()
|
|
40
61
|
|
|
41
|
-
await systemTest.
|
|
62
|
+
await systemTest.getCommunicator().sendCommand({type: "initialize"})
|
|
42
63
|
await systemTest.dismissTo(SystemTest.rootPath)
|
|
43
64
|
|
|
44
65
|
try {
|
|
@@ -53,11 +74,11 @@ export default class SystemTest {
|
|
|
53
74
|
|
|
54
75
|
/**
|
|
55
76
|
* Creates a new SystemTest instance
|
|
56
|
-
* @param {object} args
|
|
57
|
-
* @param {string} args.host
|
|
58
|
-
* @param {number} args.port
|
|
77
|
+
* @param {object} [args]
|
|
78
|
+
* @param {string} [args.host]
|
|
79
|
+
* @param {number} [args.port]
|
|
59
80
|
*/
|
|
60
|
-
constructor({host = "localhost", port = 8081, ...restArgs} = {}) {
|
|
81
|
+
constructor({host = "localhost", port = 8081, ...restArgs} = {host: "localhost", port: 8081}) {
|
|
61
82
|
const restArgsKeys = Object.keys(restArgs)
|
|
62
83
|
|
|
63
84
|
if (restArgsKeys.length > 0) {
|
|
@@ -74,10 +95,16 @@ export default class SystemTest {
|
|
|
74
95
|
|
|
75
96
|
/**
|
|
76
97
|
* Gets the base selector for scoping element searches
|
|
77
|
-
* @returns {string}
|
|
98
|
+
* @returns {string | undefined}
|
|
78
99
|
*/
|
|
79
100
|
getBaseSelector() { return this._baseSelector }
|
|
80
101
|
|
|
102
|
+
getDriver() {
|
|
103
|
+
if (!this.driver) throw new Error("Driver hasn't been initialized yet")
|
|
104
|
+
|
|
105
|
+
return this.driver
|
|
106
|
+
}
|
|
107
|
+
|
|
81
108
|
/**
|
|
82
109
|
* Sets the base selector for scoping element searches
|
|
83
110
|
* @param {string} baseSelector
|
|
@@ -115,18 +142,18 @@ export default class SystemTest {
|
|
|
115
142
|
* Finds all elements by CSS selector
|
|
116
143
|
* @param {string} selector
|
|
117
144
|
* @param {object} args
|
|
118
|
-
* @
|
|
145
|
+
* @param {boolean} [args.visible]
|
|
146
|
+
* @param {boolean} [args.useBaseSelector]
|
|
147
|
+
* @returns {Promise<import("selenium-webdriver").WebElement[]>}
|
|
119
148
|
*/
|
|
120
149
|
async all(selector, args = {}) {
|
|
121
150
|
const {visible = true, useBaseSelector = true, ...restArgs} = args
|
|
122
151
|
const restArgsKeys = Object.keys(restArgs)
|
|
123
152
|
|
|
124
|
-
if (restArgsKeys.length > 0) {
|
|
125
|
-
throw new Error(`Unknown arguments: ${restArgsKeys.join(", ")}`)
|
|
126
|
-
}
|
|
153
|
+
if (restArgsKeys.length > 0) throw new Error(`Unknown arguments: ${restArgsKeys.join(", ")}`)
|
|
127
154
|
|
|
128
155
|
const actualSelector = useBaseSelector ? this.getSelector(selector) : selector
|
|
129
|
-
const elements = await this.
|
|
156
|
+
const elements = await this.getDriver().findElements(By.css(actualSelector))
|
|
130
157
|
const activeElements = []
|
|
131
158
|
|
|
132
159
|
for (const element of elements) {
|
|
@@ -158,20 +185,24 @@ export default class SystemTest {
|
|
|
158
185
|
|
|
159
186
|
try {
|
|
160
187
|
const element = await this._findElement(elementOrIdentifier)
|
|
161
|
-
const actions = this.
|
|
188
|
+
const actions = this.getDriver().actions({async: true})
|
|
162
189
|
|
|
163
190
|
await actions.move({origin: element}).click().perform()
|
|
164
191
|
break
|
|
165
192
|
} catch (error) {
|
|
166
|
-
if (error
|
|
167
|
-
if (
|
|
168
|
-
|
|
193
|
+
if (error instanceof Error) {
|
|
194
|
+
if (error.constructor.name === "ElementNotInteractableError") {
|
|
195
|
+
if (tries >= 3) {
|
|
196
|
+
throw new Error(`Element ${elementOrIdentifier.constructor.name} click failed after ${tries} tries - ${error.constructor.name}: ${error.message}`)
|
|
197
|
+
} else {
|
|
198
|
+
await wait(50)
|
|
199
|
+
}
|
|
169
200
|
} else {
|
|
170
|
-
|
|
201
|
+
// Re-throw with un-corrupted stack trace
|
|
202
|
+
throw new Error(`Element ${elementOrIdentifier.constructor.name} click failed - ${error.constructor.name}: ${error.message}`)
|
|
171
203
|
}
|
|
172
204
|
} else {
|
|
173
|
-
|
|
174
|
-
throw new Error(`Element ${elementOrIdentifier.constructor.name} click failed - ${error.constructor.name}: ${error.message}`)
|
|
205
|
+
throw new Error(`Element ${elementOrIdentifier.constructor.name} click failed - ${typeof error}: ${error}`)
|
|
175
206
|
}
|
|
176
207
|
}
|
|
177
208
|
}
|
|
@@ -181,7 +212,7 @@ export default class SystemTest {
|
|
|
181
212
|
* Finds a single element by CSS selector
|
|
182
213
|
* @param {string} selector
|
|
183
214
|
* @param {object} args
|
|
184
|
-
* @returns {import("selenium-webdriver").WebElement}
|
|
215
|
+
* @returns {Promise<import("selenium-webdriver").WebElement>}
|
|
185
216
|
*/
|
|
186
217
|
async find(selector, args = {}) {
|
|
187
218
|
let elements
|
|
@@ -190,7 +221,11 @@ export default class SystemTest {
|
|
|
190
221
|
elements = await this.all(selector, args)
|
|
191
222
|
} catch (error) {
|
|
192
223
|
// Re-throw to recover stack trace
|
|
193
|
-
|
|
224
|
+
if (error instanceof Error) {
|
|
225
|
+
throw new Error(`${error.message} (selector: ${this.getSelector(selector)})`)
|
|
226
|
+
} else {
|
|
227
|
+
throw new Error(`${error} (selector: ${this.getSelector(selector)})`)
|
|
228
|
+
}
|
|
194
229
|
}
|
|
195
230
|
|
|
196
231
|
if (elements.length > 1) {
|
|
@@ -231,7 +266,7 @@ export default class SystemTest {
|
|
|
231
266
|
/**
|
|
232
267
|
* Finds a single element by CSS selector without waiting
|
|
233
268
|
* @param {string} selector
|
|
234
|
-
* @param {object} args
|
|
269
|
+
* @param {object} [args]
|
|
235
270
|
* @returns {Promise<import("selenium-webdriver").WebElement>}
|
|
236
271
|
*/
|
|
237
272
|
async findNoWait(selector, args) {
|
|
@@ -249,7 +284,7 @@ export default class SystemTest {
|
|
|
249
284
|
* @returns {Promise<string[]>}
|
|
250
285
|
*/
|
|
251
286
|
async getBrowserLogs() {
|
|
252
|
-
const entries = await this.
|
|
287
|
+
const entries = await this.getDriver().manage().logs().get(logging.Type.BROWSER)
|
|
253
288
|
const browserLogs = []
|
|
254
289
|
|
|
255
290
|
for (const entry of entries) {
|
|
@@ -272,7 +307,7 @@ export default class SystemTest {
|
|
|
272
307
|
* @returns {Promise<string>}
|
|
273
308
|
*/
|
|
274
309
|
async getCurrentUrl() {
|
|
275
|
-
return await this.
|
|
310
|
+
return await this.getDriver().getCurrentUrl()
|
|
276
311
|
}
|
|
277
312
|
|
|
278
313
|
/**
|
|
@@ -295,34 +330,39 @@ export default class SystemTest {
|
|
|
295
330
|
tries++
|
|
296
331
|
|
|
297
332
|
const element = await this._findElement(elementOrIdentifier)
|
|
333
|
+
const candidate = element[methodName]
|
|
298
334
|
|
|
299
|
-
if (!
|
|
335
|
+
if (!candidate) {
|
|
300
336
|
throw new Error(`${element.constructor.name} hasn't an attribute named: ${methodName}`)
|
|
301
|
-
} else if (typeof
|
|
337
|
+
} else if (typeof candidate != "function") {
|
|
302
338
|
throw new Error(`${element.constructor.name}#${methodName} is not a function`)
|
|
303
339
|
}
|
|
304
340
|
|
|
305
341
|
try {
|
|
306
|
-
return await
|
|
342
|
+
return await candidate(...args)
|
|
307
343
|
} catch (error) {
|
|
308
|
-
if (error
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
344
|
+
if (error instanceof Error) {
|
|
345
|
+
if (error.constructor.name === "ElementNotInteractableError") {
|
|
346
|
+
// Retry finding the element and interacting with it
|
|
347
|
+
if (tries >= 3) {
|
|
348
|
+
let elementDescription
|
|
349
|
+
|
|
350
|
+
if (typeof elementOrIdentifier == "string") {
|
|
351
|
+
elementDescription = `CSS selector ${elementOrIdentifier}`
|
|
352
|
+
} else {
|
|
353
|
+
elementDescription = `${element.constructor.name}`
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
throw new Error(`${elementDescription} ${methodName} failed after ${tries} tries - ${error.constructor.name}: ${error.message}`)
|
|
315
357
|
} else {
|
|
316
|
-
|
|
358
|
+
await wait(50)
|
|
317
359
|
}
|
|
318
|
-
|
|
319
|
-
throw new Error(`${elementDescription} ${methodName} failed after ${tries} tries - ${error.constructor.name}: ${error.message}`)
|
|
320
360
|
} else {
|
|
321
|
-
|
|
361
|
+
// Re-throw with un-corrupted stack trace
|
|
362
|
+
throw new Error(`${element.constructor.name} ${methodName} failed - ${error.constructor.name}: ${error.message}`)
|
|
322
363
|
}
|
|
323
364
|
} else {
|
|
324
|
-
|
|
325
|
-
throw new Error(`${element.constructor.name} ${methodName} failed - ${error.constructor.name}: ${error.message}`)
|
|
365
|
+
throw new Error(`${element.constructor.name} ${methodName} failed - ${typeof error}: ${error}`)
|
|
326
366
|
}
|
|
327
367
|
}
|
|
328
368
|
}
|
|
@@ -340,7 +380,9 @@ export default class SystemTest {
|
|
|
340
380
|
await this.findNoWait(selector)
|
|
341
381
|
found = true
|
|
342
382
|
} catch (error) {
|
|
343
|
-
if (
|
|
383
|
+
if (error instanceof Error && error.message.startsWith("Element couldn't be found after ")) {
|
|
384
|
+
// Ignore
|
|
385
|
+
} else {
|
|
344
386
|
throw error
|
|
345
387
|
}
|
|
346
388
|
}
|
|
@@ -353,6 +395,7 @@ export default class SystemTest {
|
|
|
353
395
|
/**
|
|
354
396
|
* @param {string} selector
|
|
355
397
|
* @param {object} args
|
|
398
|
+
* @param {boolean} [args.useBaseSelector]
|
|
356
399
|
* @returns {Promise<void>}
|
|
357
400
|
*/
|
|
358
401
|
async waitForNoSelector(selector, args) {
|
|
@@ -376,7 +419,7 @@ export default class SystemTest {
|
|
|
376
419
|
throw new Error(`Element still found after ${timeout}ms: ${selector}`)
|
|
377
420
|
}
|
|
378
421
|
} catch (error) {
|
|
379
|
-
if (error.message.startsWith("Element couldn't be found after ")) {
|
|
422
|
+
if (error instanceof Error && error.message.startsWith("Element couldn't be found after ")) {
|
|
380
423
|
break
|
|
381
424
|
}
|
|
382
425
|
}
|
|
@@ -406,6 +449,7 @@ export default class SystemTest {
|
|
|
406
449
|
* @returns {Promise<void>}
|
|
407
450
|
*/
|
|
408
451
|
async expectNotificationMessage(expectedNotificationMessage) {
|
|
452
|
+
/** @type {string[]} */
|
|
409
453
|
const allDetectedNotificationMessages = []
|
|
410
454
|
let foundNotificationMessageElement
|
|
411
455
|
|
|
@@ -456,7 +500,7 @@ export default class SystemTest {
|
|
|
456
500
|
* Gets the HTML of the current page
|
|
457
501
|
* @returns {Promise<string>}
|
|
458
502
|
*/
|
|
459
|
-
async getHTML() { return await this.
|
|
503
|
+
async getHTML() { return await this.getDriver().getPageSource() }
|
|
460
504
|
|
|
461
505
|
/**
|
|
462
506
|
* Starts the system test
|
|
@@ -482,6 +526,7 @@ export default class SystemTest {
|
|
|
482
526
|
options.addArguments("--no-sandbox")
|
|
483
527
|
options.addArguments("--window-size=1920,1080")
|
|
484
528
|
|
|
529
|
+
/** @type {import("selenium-webdriver").WebDriver} */
|
|
485
530
|
this.driver = new Builder()
|
|
486
531
|
.forBrowser("chrome")
|
|
487
532
|
.setChromeOptions(options)
|
|
@@ -529,7 +574,7 @@ export default class SystemTest {
|
|
|
529
574
|
* @returns {Promise<void>}
|
|
530
575
|
*/
|
|
531
576
|
async driverSetTimeouts(newTimeout) {
|
|
532
|
-
await this.
|
|
577
|
+
await this.getDriver().manage().setTimeouts({implicit: newTimeout})
|
|
533
578
|
}
|
|
534
579
|
|
|
535
580
|
/**
|
|
@@ -568,7 +613,7 @@ export default class SystemTest {
|
|
|
568
613
|
|
|
569
614
|
/**
|
|
570
615
|
* Sets the on command callback
|
|
571
|
-
* @param {function(
|
|
616
|
+
* @param {function({type: string, data: Record<string, any>}): Promise<void>} callback
|
|
572
617
|
* @returns {void}
|
|
573
618
|
*/
|
|
574
619
|
onCommand(callback) {
|
|
@@ -577,8 +622,7 @@ export default class SystemTest {
|
|
|
577
622
|
|
|
578
623
|
/**
|
|
579
624
|
* Handles a command received from the browser
|
|
580
|
-
* @param {
|
|
581
|
-
* @param {object} data.data
|
|
625
|
+
* @param {{data: {message: string, backtrace: string, type: string, value: any[]}}} args
|
|
582
626
|
* @returns {Promise<any>}
|
|
583
627
|
*/
|
|
584
628
|
onCommandReceived = async ({data}) => {
|
|
@@ -612,14 +656,18 @@ export default class SystemTest {
|
|
|
612
656
|
/**
|
|
613
657
|
* Handles a new web socket connection
|
|
614
658
|
* @param {WebSocket} ws
|
|
615
|
-
* @returns {void}
|
|
659
|
+
* @returns {Promise<void>}
|
|
616
660
|
*/
|
|
617
661
|
onWebSocketConnection = async (ws) => {
|
|
618
662
|
this.ws = ws
|
|
619
|
-
this.
|
|
620
|
-
this.
|
|
621
|
-
|
|
622
|
-
|
|
663
|
+
this.getCommunicator().ws = ws
|
|
664
|
+
this.getCommunicator().onOpen()
|
|
665
|
+
|
|
666
|
+
// @ts-expect-error
|
|
667
|
+
this.ws.on("error", this.communicator.onError)
|
|
668
|
+
|
|
669
|
+
// @ts-expect-error
|
|
670
|
+
this.ws.on("message", this.communicator.onMessage)
|
|
623
671
|
|
|
624
672
|
if (this.waitForClientWebSocketPromiseResolve) {
|
|
625
673
|
this.waitForClientWebSocketPromiseResolve()
|
|
@@ -632,12 +680,14 @@ export default class SystemTest {
|
|
|
632
680
|
*/
|
|
633
681
|
onWebSocketClose = () => {
|
|
634
682
|
this.ws = null
|
|
635
|
-
this.
|
|
683
|
+
this.getCommunicator().ws = null
|
|
636
684
|
}
|
|
637
685
|
|
|
638
686
|
/**
|
|
639
687
|
* Handles an error reported from the browser
|
|
640
688
|
* @param {object} data
|
|
689
|
+
* @param {string} data.message
|
|
690
|
+
* @param {string} [data.backtrace]
|
|
641
691
|
* @returns {void}
|
|
642
692
|
*/
|
|
643
693
|
handleError(data) {
|
|
@@ -663,7 +713,7 @@ export default class SystemTest {
|
|
|
663
713
|
this.stopScoundrel()
|
|
664
714
|
this.systemTestHttpServer?.close()
|
|
665
715
|
this.wss?.close()
|
|
666
|
-
await this.driver
|
|
716
|
+
await this.driver?.quit()
|
|
667
717
|
}
|
|
668
718
|
|
|
669
719
|
/**
|
|
@@ -674,7 +724,7 @@ export default class SystemTest {
|
|
|
674
724
|
async driverVisit(path) {
|
|
675
725
|
const url = `${this.currentUrl}${path}`
|
|
676
726
|
|
|
677
|
-
await this.
|
|
727
|
+
await this.getDriver().get(url)
|
|
678
728
|
}
|
|
679
729
|
|
|
680
730
|
/**
|
|
@@ -686,7 +736,7 @@ export default class SystemTest {
|
|
|
686
736
|
|
|
687
737
|
await fs.mkdir(path, {recursive: true})
|
|
688
738
|
|
|
689
|
-
const imageContent = await this.
|
|
739
|
+
const imageContent = await this.getDriver().takeScreenshot()
|
|
690
740
|
const now = new Date()
|
|
691
741
|
const screenshotPath = `${path}/${moment(now).format("YYYY-MM-DD-HH-MM-SS")}.png`
|
|
692
742
|
const htmlPath = `${path}/${moment(now).format("YYYY-MM-DD-HH-MM-SS")}.html`
|
|
@@ -711,7 +761,7 @@ export default class SystemTest {
|
|
|
711
761
|
* @returns {Promise<void>}
|
|
712
762
|
*/
|
|
713
763
|
async visit(path) {
|
|
714
|
-
await this.
|
|
764
|
+
await this.getCommunicator().sendCommand({type: "visit", path})
|
|
715
765
|
}
|
|
716
766
|
|
|
717
767
|
/**
|
|
@@ -720,6 +770,6 @@ export default class SystemTest {
|
|
|
720
770
|
* @returns {Promise<void>}
|
|
721
771
|
*/
|
|
722
772
|
async dismissTo(path) {
|
|
723
|
-
await this.
|
|
773
|
+
await this.getCommunicator().sendCommand({type: "dismissTo", path})
|
|
724
774
|
}
|
|
725
775
|
}
|
package/src/use-system-test.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import qs from "qs"
|
|
2
|
-
import SystemTestBrowserHelper from "./system-test-browser-helper"
|
|
2
|
+
import SystemTestBrowserHelper from "./system-test-browser-helper.js"
|
|
3
3
|
import {useCallback, useMemo} from "react"
|
|
4
4
|
import useEventEmitter from "@kaspernj/api-maker/build/use-event-emitter.js"
|
|
5
5
|
import {useRouter} from "expo-router"
|
|
@@ -46,7 +46,7 @@ function getSystemTestBrowserHelper() {
|
|
|
46
46
|
* @param {function() : void} options.onInitialize - A callback function that is called when the system test browser helper is initialized.
|
|
47
47
|
* @returns {{enabled: boolean, systemTestBrowserHelper: SystemTestBrowserHelper}}
|
|
48
48
|
*/
|
|
49
|
-
export default function useSystemTest({onInitialize, ...restArgs} = {}) {
|
|
49
|
+
export default function useSystemTest({onInitialize, ...restArgs} = {onInitialize: undefined}) {
|
|
50
50
|
const router = useRouter()
|
|
51
51
|
const enabled = useMemo(() => isSystemTestEnabled(), [])
|
|
52
52
|
const systemTestBrowserHelper = enabled ? getSystemTestBrowserHelper() : null
|
package/tsconfig.json
ADDED