system-testing 1.0.26 → 1.0.29
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.
|
|
3
|
+
"version": "1.0.29",
|
|
4
4
|
"description": "System testing with Selenium and browsers.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"system",
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
"test": "echo \"Error: no test specified\" && exit 1"
|
|
26
26
|
},
|
|
27
27
|
"dependencies": {
|
|
28
|
-
"awaitery": "^1.0.
|
|
28
|
+
"awaitery": "^1.0.2",
|
|
29
29
|
"diggerize": "^1.0.9",
|
|
30
30
|
"htmlfy": "^1.0.0",
|
|
31
31
|
"mime": "^4.0.7",
|
|
@@ -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() {
|
|
@@ -17,6 +22,7 @@ export default class SystemTestBrowserHelper {
|
|
|
17
22
|
}
|
|
18
23
|
|
|
19
24
|
constructor() {
|
|
25
|
+
// @ts-expect-error
|
|
20
26
|
this.communicator = new SystemTestCommunicator({parent: this, onCommand: this.onCommand})
|
|
21
27
|
this._enabled = false
|
|
22
28
|
this.events = new EventEmitter()
|
|
@@ -39,10 +45,10 @@ export default class SystemTestBrowserHelper {
|
|
|
39
45
|
waitForScoundrelStarted() {
|
|
40
46
|
return new Promise((resolve) => {
|
|
41
47
|
if (this.scoundrelClient) {
|
|
42
|
-
resolve()
|
|
48
|
+
resolve(undefined)
|
|
43
49
|
} else {
|
|
44
50
|
this.events.once("scoundrelStarted", () => {
|
|
45
|
-
resolve()
|
|
51
|
+
resolve(undefined)
|
|
46
52
|
})
|
|
47
53
|
}
|
|
48
54
|
})
|
|
@@ -76,14 +82,24 @@ export default class SystemTestBrowserHelper {
|
|
|
76
82
|
type: "unhandledrejection",
|
|
77
83
|
error: event.reason,
|
|
78
84
|
errorClass: "UnhandledRejection",
|
|
79
|
-
file: null,
|
|
80
|
-
line: null,
|
|
81
85
|
message: event.reason.message || event.reason || "Unhandled promise rejection without a message",
|
|
82
86
|
url: window.location.href
|
|
83
87
|
})
|
|
84
88
|
})
|
|
85
89
|
}
|
|
86
90
|
|
|
91
|
+
/**
|
|
92
|
+
* @param {object} data
|
|
93
|
+
* @param {string} [data.backtrace]
|
|
94
|
+
* @param {Error} [data.error]
|
|
95
|
+
* @param {string} [data.errorClass]
|
|
96
|
+
* @param {string} [data.file]
|
|
97
|
+
* @param {number} [data.line]
|
|
98
|
+
* @param {string} [data.message]
|
|
99
|
+
* @param {string} [data.type]
|
|
100
|
+
* @param {string} [data.url]
|
|
101
|
+
* @returns {void}
|
|
102
|
+
*/
|
|
87
103
|
handleError(data) {
|
|
88
104
|
let backtrace
|
|
89
105
|
|
|
@@ -92,7 +108,7 @@ export default class SystemTestBrowserHelper {
|
|
|
92
108
|
backtrace.shift()
|
|
93
109
|
backtrace = backtrace.join("\n")
|
|
94
110
|
} else if (data.file) {
|
|
95
|
-
backtrace =
|
|
111
|
+
backtrace = `${data.file}:${data.line}`
|
|
96
112
|
}
|
|
97
113
|
|
|
98
114
|
data.backtrace = backtrace
|
|
@@ -100,6 +116,9 @@ export default class SystemTestBrowserHelper {
|
|
|
100
116
|
this.communicator.sendCommand(data)
|
|
101
117
|
}
|
|
102
118
|
|
|
119
|
+
/**
|
|
120
|
+
* @returns {void}
|
|
121
|
+
*/
|
|
103
122
|
connectWebSocket() {
|
|
104
123
|
this.ws = new WebSocket("ws://localhost:1985")
|
|
105
124
|
this.communicator.ws = this.ws
|
|
@@ -108,6 +127,9 @@ export default class SystemTestBrowserHelper {
|
|
|
108
127
|
this.ws.addEventListener("message", (event) => this.communicator.onMessage(event.data))
|
|
109
128
|
}
|
|
110
129
|
|
|
130
|
+
/**
|
|
131
|
+
* @returns {void}
|
|
132
|
+
*/
|
|
111
133
|
enableOnBrowser() {
|
|
112
134
|
this._enabled = true
|
|
113
135
|
this.connectWebSocket()
|
|
@@ -116,21 +138,45 @@ export default class SystemTestBrowserHelper {
|
|
|
116
138
|
this.overrideConsoleLog()
|
|
117
139
|
}
|
|
118
140
|
|
|
141
|
+
/**
|
|
142
|
+
* @returns {boolean}
|
|
143
|
+
*/
|
|
119
144
|
getEnabled() { return this._enabled }
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* @returns {EventEmitter}
|
|
148
|
+
*/
|
|
120
149
|
getEvents() { return this.events }
|
|
121
150
|
|
|
151
|
+
/**
|
|
152
|
+
* @param {any[]} args
|
|
153
|
+
* @returns {void}
|
|
154
|
+
*/
|
|
122
155
|
fakeConsoleError = (...args) => {
|
|
123
156
|
this.communicator.sendCommand({type: "console.error", value: this.consoleLogMessage(args)})
|
|
124
157
|
|
|
125
|
-
|
|
158
|
+
if (this.originalConsoleError) {
|
|
159
|
+
return this.originalConsoleError(...args)
|
|
160
|
+
}
|
|
126
161
|
}
|
|
127
162
|
|
|
163
|
+
/**
|
|
164
|
+
* @param {any[]} args
|
|
165
|
+
* @returns {void}
|
|
166
|
+
*/
|
|
128
167
|
fakeConsoleLog = (...args) => {
|
|
129
168
|
this.communicator.sendCommand({type: "console.log", value: this.consoleLogMessage(args)})
|
|
130
169
|
|
|
131
|
-
|
|
170
|
+
if (this.originalConsoleLog) {
|
|
171
|
+
return this.originalConsoleLog(...args)
|
|
172
|
+
}
|
|
132
173
|
}
|
|
133
174
|
|
|
175
|
+
/**
|
|
176
|
+
* @param {any} arg
|
|
177
|
+
* @param {any[]} [scannedObjects]
|
|
178
|
+
* @returns {any}
|
|
179
|
+
*/
|
|
134
180
|
consoleLogMessage(arg, scannedObjects = []) {
|
|
135
181
|
if (Array.isArray(arg)) {
|
|
136
182
|
if (scannedObjects.includes(arg)) {
|
|
@@ -153,6 +199,7 @@ export default class SystemTestBrowserHelper {
|
|
|
153
199
|
scannedObjects.push(arg)
|
|
154
200
|
}
|
|
155
201
|
|
|
202
|
+
/** @type {Record<string, any>} */
|
|
156
203
|
const result = {}
|
|
157
204
|
|
|
158
205
|
for (const key in arg) {
|
|
@@ -167,6 +214,10 @@ export default class SystemTestBrowserHelper {
|
|
|
167
214
|
}
|
|
168
215
|
}
|
|
169
216
|
|
|
217
|
+
/**
|
|
218
|
+
* @param {{data: {path: string, type: string}}} args
|
|
219
|
+
* @returns {Promise<{result: string} | void>}
|
|
220
|
+
*/
|
|
170
221
|
onCommand = async ({data}) => {
|
|
171
222
|
if (data.type == "initialize") {
|
|
172
223
|
this.events.emit("initialize")
|
|
@@ -185,10 +236,17 @@ export default class SystemTestBrowserHelper {
|
|
|
185
236
|
}
|
|
186
237
|
}
|
|
187
238
|
|
|
239
|
+
/**
|
|
240
|
+
* @param {function() : void} callback
|
|
241
|
+
* @returns {void}
|
|
242
|
+
*/
|
|
188
243
|
onInitialize(callback) {
|
|
189
244
|
this._onInitializeCallback = callback
|
|
190
245
|
}
|
|
191
246
|
|
|
247
|
+
/**
|
|
248
|
+
* @returns {void}
|
|
249
|
+
*/
|
|
192
250
|
overrideConsoleLog() {
|
|
193
251
|
if (this.originalConsoleError || this.originalConsoleLog) {
|
|
194
252
|
throw new Error("Console methods has already been overridden!")
|
|
@@ -201,7 +259,12 @@ export default class SystemTestBrowserHelper {
|
|
|
201
259
|
console.log = this.fakeConsoleLog
|
|
202
260
|
}
|
|
203
261
|
|
|
262
|
+
/**
|
|
263
|
+
* @param {string} sql
|
|
264
|
+
* @returns {Promise<Array<Record<string, any>>>}
|
|
265
|
+
*/
|
|
204
266
|
async sendQuery(sql) {
|
|
267
|
+
// @ts-expect-error
|
|
205
268
|
return await this.communicator.sendCommand({type: "query", sql})
|
|
206
269
|
}
|
|
207
270
|
}
|
|
@@ -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,14 +1,28 @@
|
|
|
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"
|
|
4
6
|
import url from "url"
|
|
5
7
|
|
|
6
8
|
export default class SystemTestHttpServer {
|
|
9
|
+
/** @returns {void} */
|
|
7
10
|
close() {
|
|
8
|
-
this.httpServer
|
|
11
|
+
this.httpServer?.close()
|
|
9
12
|
}
|
|
10
13
|
|
|
14
|
+
/**
|
|
15
|
+
* @param {http.IncomingMessage} request
|
|
16
|
+
* @param {http.ServerResponse} response
|
|
17
|
+
* @returns {Promise<void>}
|
|
18
|
+
*/
|
|
11
19
|
onHttpServerRequest = async (request, response) => {
|
|
20
|
+
if (!request.url) {
|
|
21
|
+
response.statusCode = 400
|
|
22
|
+
response.end("Bad Request")
|
|
23
|
+
return
|
|
24
|
+
}
|
|
25
|
+
|
|
12
26
|
const parsedUrl = url.parse(request.url)
|
|
13
27
|
let filePath = `${process.cwd()}/dist${parsedUrl.pathname}`
|
|
14
28
|
|
|
@@ -33,15 +47,21 @@ export default class SystemTestHttpServer {
|
|
|
33
47
|
const mimeType = mime.getType(filePath)
|
|
34
48
|
|
|
35
49
|
response.statusCode = 200
|
|
36
|
-
|
|
50
|
+
|
|
51
|
+
if (mimeType) {
|
|
52
|
+
response.setHeader("Content-Type", mimeType)
|
|
53
|
+
}
|
|
54
|
+
|
|
37
55
|
response.end(fileContent)
|
|
38
56
|
}
|
|
39
57
|
|
|
58
|
+
/** @returns {Promise<void>} */
|
|
40
59
|
async start() {
|
|
41
60
|
this.basePath = await fs.realpath(`${__dirname}/../..`)
|
|
42
61
|
await this.startHttpServer()
|
|
43
62
|
}
|
|
44
63
|
|
|
64
|
+
/** @returns {Promise<void>} */
|
|
45
65
|
startHttpServer() {
|
|
46
66
|
return new Promise((resolve) => {
|
|
47
67
|
this.httpServer = http.createServer(this.onHttpServerRequest)
|
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)
|
|
28
41
|
}
|
|
29
42
|
|
|
30
|
-
return
|
|
43
|
+
return shared.systemTest
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
getCommunicator() {
|
|
47
|
+
if (!this.communicator) {
|
|
48
|
+
throw new Error("Communicator hasn't been initialized yet")
|
|
49
|
+
}
|
|
50
|
+
|
|
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) {
|
|
@@ -69,15 +90,23 @@ export default class SystemTest {
|
|
|
69
90
|
this._responses = {}
|
|
70
91
|
this._sendCount = 0
|
|
71
92
|
this.startScoundrel()
|
|
93
|
+
|
|
94
|
+
// @ts-expect-error
|
|
72
95
|
this.communicator = new SystemTestCommunicator({onCommand: this.onCommandReceived})
|
|
73
96
|
}
|
|
74
97
|
|
|
75
98
|
/**
|
|
76
99
|
* Gets the base selector for scoping element searches
|
|
77
|
-
* @returns {string}
|
|
100
|
+
* @returns {string | undefined}
|
|
78
101
|
*/
|
|
79
102
|
getBaseSelector() { return this._baseSelector }
|
|
80
103
|
|
|
104
|
+
getDriver() {
|
|
105
|
+
if (!this.driver) throw new Error("Driver hasn't been initialized yet")
|
|
106
|
+
|
|
107
|
+
return this.driver
|
|
108
|
+
}
|
|
109
|
+
|
|
81
110
|
/**
|
|
82
111
|
* Sets the base selector for scoping element searches
|
|
83
112
|
* @param {string} baseSelector
|
|
@@ -93,13 +122,19 @@ export default class SystemTest {
|
|
|
93
122
|
return this.getBaseSelector() ? `${this.getBaseSelector()} ${selector}` : selector
|
|
94
123
|
}
|
|
95
124
|
|
|
96
|
-
/**
|
|
125
|
+
/**
|
|
126
|
+
* Starts Scoundrel server which the browser connects to for remote evaluation in the browser
|
|
127
|
+
* @returns {void}
|
|
128
|
+
*/
|
|
97
129
|
startScoundrel() {
|
|
98
130
|
this.wss = new WebSocketServer({port: 8090})
|
|
99
131
|
this.serverWebSocket = new ServerWebSocket(this.wss)
|
|
100
132
|
this.server = new Server(this.serverWebSocket)
|
|
101
133
|
}
|
|
102
134
|
|
|
135
|
+
/**
|
|
136
|
+
* @returns {void}
|
|
137
|
+
*/
|
|
103
138
|
stopScoundrel() {
|
|
104
139
|
this.server?.close()
|
|
105
140
|
this.wss?.close()
|
|
@@ -109,18 +144,18 @@ export default class SystemTest {
|
|
|
109
144
|
* Finds all elements by CSS selector
|
|
110
145
|
* @param {string} selector
|
|
111
146
|
* @param {object} args
|
|
112
|
-
* @
|
|
147
|
+
* @param {boolean} [args.visible]
|
|
148
|
+
* @param {boolean} [args.useBaseSelector]
|
|
149
|
+
* @returns {Promise<import("selenium-webdriver").WebElement[]>}
|
|
113
150
|
*/
|
|
114
151
|
async all(selector, args = {}) {
|
|
115
152
|
const {visible = true, useBaseSelector = true, ...restArgs} = args
|
|
116
153
|
const restArgsKeys = Object.keys(restArgs)
|
|
117
154
|
|
|
118
|
-
if (restArgsKeys.length > 0) {
|
|
119
|
-
throw new Error(`Unknown arguments: ${restArgsKeys.join(", ")}`)
|
|
120
|
-
}
|
|
155
|
+
if (restArgsKeys.length > 0) throw new Error(`Unknown arguments: ${restArgsKeys.join(", ")}`)
|
|
121
156
|
|
|
122
157
|
const actualSelector = useBaseSelector ? this.getSelector(selector) : selector
|
|
123
|
-
const elements = await this.
|
|
158
|
+
const elements = await this.getDriver().findElements(By.css(actualSelector))
|
|
124
159
|
const activeElements = []
|
|
125
160
|
|
|
126
161
|
for (const element of elements) {
|
|
@@ -152,20 +187,24 @@ export default class SystemTest {
|
|
|
152
187
|
|
|
153
188
|
try {
|
|
154
189
|
const element = await this._findElement(elementOrIdentifier)
|
|
155
|
-
const actions = this.
|
|
190
|
+
const actions = this.getDriver().actions({async: true})
|
|
156
191
|
|
|
157
192
|
await actions.move({origin: element}).click().perform()
|
|
158
193
|
break
|
|
159
194
|
} catch (error) {
|
|
160
|
-
if (error
|
|
161
|
-
if (
|
|
162
|
-
|
|
195
|
+
if (error instanceof Error) {
|
|
196
|
+
if (error.constructor.name === "ElementNotInteractableError") {
|
|
197
|
+
if (tries >= 3) {
|
|
198
|
+
throw new Error(`Element ${elementOrIdentifier.constructor.name} click failed after ${tries} tries - ${error.constructor.name}: ${error.message}`)
|
|
199
|
+
} else {
|
|
200
|
+
await wait(50)
|
|
201
|
+
}
|
|
163
202
|
} else {
|
|
164
|
-
|
|
203
|
+
// Re-throw with un-corrupted stack trace
|
|
204
|
+
throw new Error(`Element ${elementOrIdentifier.constructor.name} click failed - ${error.constructor.name}: ${error.message}`)
|
|
165
205
|
}
|
|
166
206
|
} else {
|
|
167
|
-
|
|
168
|
-
throw new Error(`Element ${elementOrIdentifier.constructor.name} click failed - ${error.constructor.name}: ${error.message}`)
|
|
207
|
+
throw new Error(`Element ${elementOrIdentifier.constructor.name} click failed - ${typeof error}: ${error}`)
|
|
169
208
|
}
|
|
170
209
|
}
|
|
171
210
|
}
|
|
@@ -175,7 +214,7 @@ export default class SystemTest {
|
|
|
175
214
|
* Finds a single element by CSS selector
|
|
176
215
|
* @param {string} selector
|
|
177
216
|
* @param {object} args
|
|
178
|
-
* @returns {import("selenium-webdriver").WebElement}
|
|
217
|
+
* @returns {Promise<import("selenium-webdriver").WebElement>}
|
|
179
218
|
*/
|
|
180
219
|
async find(selector, args = {}) {
|
|
181
220
|
let elements
|
|
@@ -184,7 +223,11 @@ export default class SystemTest {
|
|
|
184
223
|
elements = await this.all(selector, args)
|
|
185
224
|
} catch (error) {
|
|
186
225
|
// Re-throw to recover stack trace
|
|
187
|
-
|
|
226
|
+
if (error instanceof Error) {
|
|
227
|
+
throw new Error(`${error.message} (selector: ${this.getSelector(selector)})`)
|
|
228
|
+
} else {
|
|
229
|
+
throw new Error(`${error} (selector: ${this.getSelector(selector)})`)
|
|
230
|
+
}
|
|
188
231
|
}
|
|
189
232
|
|
|
190
233
|
if (elements.length > 1) {
|
|
@@ -225,7 +268,7 @@ export default class SystemTest {
|
|
|
225
268
|
/**
|
|
226
269
|
* Finds a single element by CSS selector without waiting
|
|
227
270
|
* @param {string} selector
|
|
228
|
-
* @param {object} args
|
|
271
|
+
* @param {object} [args]
|
|
229
272
|
* @returns {Promise<import("selenium-webdriver").WebElement>}
|
|
230
273
|
*/
|
|
231
274
|
async findNoWait(selector, args) {
|
|
@@ -243,7 +286,7 @@ export default class SystemTest {
|
|
|
243
286
|
* @returns {Promise<string[]>}
|
|
244
287
|
*/
|
|
245
288
|
async getBrowserLogs() {
|
|
246
|
-
const entries = await this.
|
|
289
|
+
const entries = await this.getDriver().manage().logs().get(logging.Type.BROWSER)
|
|
247
290
|
const browserLogs = []
|
|
248
291
|
|
|
249
292
|
for (const entry of entries) {
|
|
@@ -266,7 +309,7 @@ export default class SystemTest {
|
|
|
266
309
|
* @returns {Promise<string>}
|
|
267
310
|
*/
|
|
268
311
|
async getCurrentUrl() {
|
|
269
|
-
return await this.
|
|
312
|
+
return await this.getDriver().getCurrentUrl()
|
|
270
313
|
}
|
|
271
314
|
|
|
272
315
|
/**
|
|
@@ -290,33 +333,40 @@ export default class SystemTest {
|
|
|
290
333
|
|
|
291
334
|
const element = await this._findElement(elementOrIdentifier)
|
|
292
335
|
|
|
293
|
-
|
|
336
|
+
// @ts-expect-error
|
|
337
|
+
const candidate = element[methodName]
|
|
338
|
+
|
|
339
|
+
if (!candidate) {
|
|
294
340
|
throw new Error(`${element.constructor.name} hasn't an attribute named: ${methodName}`)
|
|
295
|
-
} else if (typeof
|
|
341
|
+
} else if (typeof candidate != "function") {
|
|
296
342
|
throw new Error(`${element.constructor.name}#${methodName} is not a function`)
|
|
297
343
|
}
|
|
298
344
|
|
|
299
345
|
try {
|
|
300
|
-
return await
|
|
346
|
+
return await candidate(...args)
|
|
301
347
|
} catch (error) {
|
|
302
|
-
if (error
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
348
|
+
if (error instanceof Error) {
|
|
349
|
+
if (error.constructor.name === "ElementNotInteractableError") {
|
|
350
|
+
// Retry finding the element and interacting with it
|
|
351
|
+
if (tries >= 3) {
|
|
352
|
+
let elementDescription
|
|
353
|
+
|
|
354
|
+
if (typeof elementOrIdentifier == "string") {
|
|
355
|
+
elementDescription = `CSS selector ${elementOrIdentifier}`
|
|
356
|
+
} else {
|
|
357
|
+
elementDescription = `${element.constructor.name}`
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
throw new Error(`${elementDescription} ${methodName} failed after ${tries} tries - ${error.constructor.name}: ${error.message}`)
|
|
309
361
|
} else {
|
|
310
|
-
|
|
362
|
+
await wait(50)
|
|
311
363
|
}
|
|
312
|
-
|
|
313
|
-
throw new Error(`${elementDescription} ${methodName} failed after ${tries} tries - ${error.constructor.name}: ${error.message}`)
|
|
314
364
|
} else {
|
|
315
|
-
|
|
365
|
+
// Re-throw with un-corrupted stack trace
|
|
366
|
+
throw new Error(`${element.constructor.name} ${methodName} failed - ${error.constructor.name}: ${error.message}`)
|
|
316
367
|
}
|
|
317
368
|
} else {
|
|
318
|
-
|
|
319
|
-
throw new Error(`${element.constructor.name} ${methodName} failed - ${error.constructor.name}: ${error.message}`)
|
|
369
|
+
throw new Error(`${element.constructor.name} ${methodName} failed - ${typeof error}: ${error}`)
|
|
320
370
|
}
|
|
321
371
|
}
|
|
322
372
|
}
|
|
@@ -334,7 +384,9 @@ export default class SystemTest {
|
|
|
334
384
|
await this.findNoWait(selector)
|
|
335
385
|
found = true
|
|
336
386
|
} catch (error) {
|
|
337
|
-
if (
|
|
387
|
+
if (error instanceof Error && error.message.startsWith("Element couldn't be found after ")) {
|
|
388
|
+
// Ignore
|
|
389
|
+
} else {
|
|
338
390
|
throw error
|
|
339
391
|
}
|
|
340
392
|
}
|
|
@@ -347,6 +399,7 @@ export default class SystemTest {
|
|
|
347
399
|
/**
|
|
348
400
|
* @param {string} selector
|
|
349
401
|
* @param {object} args
|
|
402
|
+
* @param {boolean} [args.useBaseSelector]
|
|
350
403
|
* @returns {Promise<void>}
|
|
351
404
|
*/
|
|
352
405
|
async waitForNoSelector(selector, args) {
|
|
@@ -362,6 +415,7 @@ export default class SystemTest {
|
|
|
362
415
|
try {
|
|
363
416
|
const actualSelector = useBaseSelector ? this.getSelector(selector) : selector
|
|
364
417
|
|
|
418
|
+
// @ts-expect-error
|
|
365
419
|
await this.driver.wait(until.elementIsNotVisible(By.css(actualSelector)), 0)
|
|
366
420
|
|
|
367
421
|
const timeElapsed = new Date().getTime() - timeStart
|
|
@@ -370,7 +424,7 @@ export default class SystemTest {
|
|
|
370
424
|
throw new Error(`Element still found after ${timeout}ms: ${selector}`)
|
|
371
425
|
}
|
|
372
426
|
} catch (error) {
|
|
373
|
-
if (error.message.startsWith("Element couldn't be found after ")) {
|
|
427
|
+
if (error instanceof Error && error.message.startsWith("Element couldn't be found after ")) {
|
|
374
428
|
break
|
|
375
429
|
}
|
|
376
430
|
}
|
|
@@ -400,9 +454,11 @@ export default class SystemTest {
|
|
|
400
454
|
* @returns {Promise<void>}
|
|
401
455
|
*/
|
|
402
456
|
async expectNotificationMessage(expectedNotificationMessage) {
|
|
457
|
+
/** @type {string[]} */
|
|
403
458
|
const allDetectedNotificationMessages = []
|
|
404
459
|
let foundNotificationMessageElement
|
|
405
460
|
|
|
461
|
+
// @ts-expect-error
|
|
406
462
|
await waitFor(async () => {
|
|
407
463
|
const notificationMessageElements = await this.all("[data-class='notification-message']", {useBaseSelector: false})
|
|
408
464
|
|
|
@@ -450,7 +506,7 @@ export default class SystemTest {
|
|
|
450
506
|
* Gets the HTML of the current page
|
|
451
507
|
* @returns {Promise<string>}
|
|
452
508
|
*/
|
|
453
|
-
async getHTML() { return await this.
|
|
509
|
+
async getHTML() { return await this.getDriver().getPageSource() }
|
|
454
510
|
|
|
455
511
|
/**
|
|
456
512
|
* Starts the system test
|
|
@@ -476,9 +532,11 @@ export default class SystemTest {
|
|
|
476
532
|
options.addArguments("--no-sandbox")
|
|
477
533
|
options.addArguments("--window-size=1920,1080")
|
|
478
534
|
|
|
535
|
+
/** @type {import("selenium-webdriver").WebDriver} */
|
|
479
536
|
this.driver = new Builder()
|
|
480
537
|
.forBrowser("chrome")
|
|
481
538
|
.setChromeOptions(options)
|
|
539
|
+
// @ts-expect-error
|
|
482
540
|
.setCapability("goog:loggingPrefs", {browser: "ALL"})
|
|
483
541
|
.build()
|
|
484
542
|
|
|
@@ -523,7 +581,7 @@ export default class SystemTest {
|
|
|
523
581
|
* @returns {Promise<void>}
|
|
524
582
|
*/
|
|
525
583
|
async driverSetTimeouts(newTimeout) {
|
|
526
|
-
await this.
|
|
584
|
+
await this.getDriver().manage().setTimeouts({implicit: newTimeout})
|
|
527
585
|
}
|
|
528
586
|
|
|
529
587
|
/**
|
|
@@ -562,7 +620,7 @@ export default class SystemTest {
|
|
|
562
620
|
|
|
563
621
|
/**
|
|
564
622
|
* Sets the on command callback
|
|
565
|
-
* @param {function(
|
|
623
|
+
* @param {function({type: string, data: Record<string, any>}): Promise<void>} callback
|
|
566
624
|
* @returns {void}
|
|
567
625
|
*/
|
|
568
626
|
onCommand(callback) {
|
|
@@ -571,8 +629,7 @@ export default class SystemTest {
|
|
|
571
629
|
|
|
572
630
|
/**
|
|
573
631
|
* Handles a command received from the browser
|
|
574
|
-
* @param {
|
|
575
|
-
* @param {object} data.data
|
|
632
|
+
* @param {{data: {message: string, backtrace: string, type: string, value: any[]}}} args
|
|
576
633
|
* @returns {Promise<any>}
|
|
577
634
|
*/
|
|
578
635
|
onCommandReceived = async ({data}) => {
|
|
@@ -606,14 +663,18 @@ export default class SystemTest {
|
|
|
606
663
|
/**
|
|
607
664
|
* Handles a new web socket connection
|
|
608
665
|
* @param {WebSocket} ws
|
|
609
|
-
* @returns {void}
|
|
666
|
+
* @returns {Promise<void>}
|
|
610
667
|
*/
|
|
611
668
|
onWebSocketConnection = async (ws) => {
|
|
612
669
|
this.ws = ws
|
|
613
|
-
this.
|
|
614
|
-
this.
|
|
615
|
-
|
|
616
|
-
|
|
670
|
+
this.getCommunicator().ws = ws
|
|
671
|
+
this.getCommunicator().onOpen()
|
|
672
|
+
|
|
673
|
+
// @ts-expect-error
|
|
674
|
+
this.ws.on("error", this.communicator.onError)
|
|
675
|
+
|
|
676
|
+
// @ts-expect-error
|
|
677
|
+
this.ws.on("message", this.communicator.onMessage)
|
|
617
678
|
|
|
618
679
|
if (this.waitForClientWebSocketPromiseResolve) {
|
|
619
680
|
this.waitForClientWebSocketPromiseResolve()
|
|
@@ -626,12 +687,14 @@ export default class SystemTest {
|
|
|
626
687
|
*/
|
|
627
688
|
onWebSocketClose = () => {
|
|
628
689
|
this.ws = null
|
|
629
|
-
this.
|
|
690
|
+
this.getCommunicator().ws = null
|
|
630
691
|
}
|
|
631
692
|
|
|
632
693
|
/**
|
|
633
694
|
* Handles an error reported from the browser
|
|
634
695
|
* @param {object} data
|
|
696
|
+
* @param {string} data.message
|
|
697
|
+
* @param {string} [data.backtrace]
|
|
635
698
|
* @returns {void}
|
|
636
699
|
*/
|
|
637
700
|
handleError(data) {
|
|
@@ -657,7 +720,7 @@ export default class SystemTest {
|
|
|
657
720
|
this.stopScoundrel()
|
|
658
721
|
this.systemTestHttpServer?.close()
|
|
659
722
|
this.wss?.close()
|
|
660
|
-
await this.driver
|
|
723
|
+
await this.driver?.quit()
|
|
661
724
|
}
|
|
662
725
|
|
|
663
726
|
/**
|
|
@@ -668,7 +731,7 @@ export default class SystemTest {
|
|
|
668
731
|
async driverVisit(path) {
|
|
669
732
|
const url = `${this.currentUrl}${path}`
|
|
670
733
|
|
|
671
|
-
await this.
|
|
734
|
+
await this.getDriver().get(url)
|
|
672
735
|
}
|
|
673
736
|
|
|
674
737
|
/**
|
|
@@ -680,7 +743,7 @@ export default class SystemTest {
|
|
|
680
743
|
|
|
681
744
|
await fs.mkdir(path, {recursive: true})
|
|
682
745
|
|
|
683
|
-
const imageContent = await this.
|
|
746
|
+
const imageContent = await this.getDriver().takeScreenshot()
|
|
684
747
|
const now = new Date()
|
|
685
748
|
const screenshotPath = `${path}/${moment(now).format("YYYY-MM-DD-HH-MM-SS")}.png`
|
|
686
749
|
const htmlPath = `${path}/${moment(now).format("YYYY-MM-DD-HH-MM-SS")}.html`
|
|
@@ -705,7 +768,7 @@ export default class SystemTest {
|
|
|
705
768
|
* @returns {Promise<void>}
|
|
706
769
|
*/
|
|
707
770
|
async visit(path) {
|
|
708
|
-
await this.
|
|
771
|
+
await this.getCommunicator().sendCommand({type: "visit", path})
|
|
709
772
|
}
|
|
710
773
|
|
|
711
774
|
/**
|
|
@@ -714,6 +777,6 @@ export default class SystemTest {
|
|
|
714
777
|
* @returns {Promise<void>}
|
|
715
778
|
*/
|
|
716
779
|
async dismissTo(path) {
|
|
717
|
-
await this.
|
|
780
|
+
await this.getCommunicator().sendCommand({type: "dismissTo", path})
|
|
718
781
|
}
|
|
719
782
|
}
|
package/src/use-system-test.js
CHANGED
|
@@ -9,7 +9,10 @@ const shared = {
|
|
|
9
9
|
systemTestBrowserHelper: null
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
-
|
|
12
|
+
/**
|
|
13
|
+
* @returns {boolean}
|
|
14
|
+
*/
|
|
15
|
+
function isSystemTestEnabled() {
|
|
13
16
|
let enabled = false
|
|
14
17
|
const initialUrl = globalThis.location?.href
|
|
15
18
|
|
|
@@ -25,7 +28,10 @@ const isSystemTestEnabled = () => {
|
|
|
25
28
|
return enabled
|
|
26
29
|
}
|
|
27
30
|
|
|
28
|
-
|
|
31
|
+
/**
|
|
32
|
+
* @returns {SystemTestBrowserHelper}
|
|
33
|
+
*/
|
|
34
|
+
function getSystemTestBrowserHelper() {
|
|
29
35
|
if (!shared.systemTestBrowserHelper) {
|
|
30
36
|
shared.systemTestBrowserHelper = new SystemTestBrowserHelper()
|
|
31
37
|
shared.systemTestBrowserHelper.enableOnBrowser()
|
|
@@ -38,9 +44,7 @@ const getSystemTestBrowserHelper = () => {
|
|
|
38
44
|
* A hook that provides system test capabilities.
|
|
39
45
|
* @param {object} options - Options for the hook.
|
|
40
46
|
* @param {function() : void} options.onInitialize - A callback function that is called when the system test browser helper is initialized.
|
|
41
|
-
* @returns {
|
|
42
|
-
* - enabled: A boolean indicating if system test mode is enabled.
|
|
43
|
-
* - systemTestBrowserHelper: An instance of SystemTestBrowserHelper if enabled, otherwise null.
|
|
47
|
+
* @returns {{enabled: boolean, systemTestBrowserHelper: SystemTestBrowserHelper}}
|
|
44
48
|
*/
|
|
45
49
|
export default function useSystemTest({onInitialize, ...restArgs} = {}) {
|
|
46
50
|
const router = useRouter()
|