system-testing 1.0.27 → 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
|
|
@@ -132,18 +148,35 @@ export default class SystemTestBrowserHelper {
|
|
|
132
148
|
*/
|
|
133
149
|
getEvents() { return this.events }
|
|
134
150
|
|
|
151
|
+
/**
|
|
152
|
+
* @param {any[]} args
|
|
153
|
+
* @returns {void}
|
|
154
|
+
*/
|
|
135
155
|
fakeConsoleError = (...args) => {
|
|
136
156
|
this.communicator.sendCommand({type: "console.error", value: this.consoleLogMessage(args)})
|
|
137
157
|
|
|
138
|
-
|
|
158
|
+
if (this.originalConsoleError) {
|
|
159
|
+
return this.originalConsoleError(...args)
|
|
160
|
+
}
|
|
139
161
|
}
|
|
140
162
|
|
|
163
|
+
/**
|
|
164
|
+
* @param {any[]} args
|
|
165
|
+
* @returns {void}
|
|
166
|
+
*/
|
|
141
167
|
fakeConsoleLog = (...args) => {
|
|
142
168
|
this.communicator.sendCommand({type: "console.log", value: this.consoleLogMessage(args)})
|
|
143
169
|
|
|
144
|
-
|
|
170
|
+
if (this.originalConsoleLog) {
|
|
171
|
+
return this.originalConsoleLog(...args)
|
|
172
|
+
}
|
|
145
173
|
}
|
|
146
174
|
|
|
175
|
+
/**
|
|
176
|
+
* @param {any} arg
|
|
177
|
+
* @param {any[]} [scannedObjects]
|
|
178
|
+
* @returns {any}
|
|
179
|
+
*/
|
|
147
180
|
consoleLogMessage(arg, scannedObjects = []) {
|
|
148
181
|
if (Array.isArray(arg)) {
|
|
149
182
|
if (scannedObjects.includes(arg)) {
|
|
@@ -166,6 +199,7 @@ export default class SystemTestBrowserHelper {
|
|
|
166
199
|
scannedObjects.push(arg)
|
|
167
200
|
}
|
|
168
201
|
|
|
202
|
+
/** @type {Record<string, any>} */
|
|
169
203
|
const result = {}
|
|
170
204
|
|
|
171
205
|
for (const key in arg) {
|
|
@@ -180,6 +214,10 @@ export default class SystemTestBrowserHelper {
|
|
|
180
214
|
}
|
|
181
215
|
}
|
|
182
216
|
|
|
217
|
+
/**
|
|
218
|
+
* @param {{data: {path: string, type: string}}} args
|
|
219
|
+
* @returns {Promise<{result: string} | void>}
|
|
220
|
+
*/
|
|
183
221
|
onCommand = async ({data}) => {
|
|
184
222
|
if (data.type == "initialize") {
|
|
185
223
|
this.events.emit("initialize")
|
|
@@ -226,6 +264,7 @@ export default class SystemTestBrowserHelper {
|
|
|
226
264
|
* @returns {Promise<Array<Record<string, any>>>}
|
|
227
265
|
*/
|
|
228
266
|
async sendQuery(sql) {
|
|
267
|
+
// @ts-expect-error
|
|
229
268
|
return await this.communicator.sendCommand({type: "query", sql})
|
|
230
269
|
}
|
|
231
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,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) {
|
|
@@ -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
|
|
@@ -115,18 +144,18 @@ export default class SystemTest {
|
|
|
115
144
|
* Finds all elements by CSS selector
|
|
116
145
|
* @param {string} selector
|
|
117
146
|
* @param {object} args
|
|
118
|
-
* @
|
|
147
|
+
* @param {boolean} [args.visible]
|
|
148
|
+
* @param {boolean} [args.useBaseSelector]
|
|
149
|
+
* @returns {Promise<import("selenium-webdriver").WebElement[]>}
|
|
119
150
|
*/
|
|
120
151
|
async all(selector, args = {}) {
|
|
121
152
|
const {visible = true, useBaseSelector = true, ...restArgs} = args
|
|
122
153
|
const restArgsKeys = Object.keys(restArgs)
|
|
123
154
|
|
|
124
|
-
if (restArgsKeys.length > 0) {
|
|
125
|
-
throw new Error(`Unknown arguments: ${restArgsKeys.join(", ")}`)
|
|
126
|
-
}
|
|
155
|
+
if (restArgsKeys.length > 0) throw new Error(`Unknown arguments: ${restArgsKeys.join(", ")}`)
|
|
127
156
|
|
|
128
157
|
const actualSelector = useBaseSelector ? this.getSelector(selector) : selector
|
|
129
|
-
const elements = await this.
|
|
158
|
+
const elements = await this.getDriver().findElements(By.css(actualSelector))
|
|
130
159
|
const activeElements = []
|
|
131
160
|
|
|
132
161
|
for (const element of elements) {
|
|
@@ -158,20 +187,24 @@ export default class SystemTest {
|
|
|
158
187
|
|
|
159
188
|
try {
|
|
160
189
|
const element = await this._findElement(elementOrIdentifier)
|
|
161
|
-
const actions = this.
|
|
190
|
+
const actions = this.getDriver().actions({async: true})
|
|
162
191
|
|
|
163
192
|
await actions.move({origin: element}).click().perform()
|
|
164
193
|
break
|
|
165
194
|
} catch (error) {
|
|
166
|
-
if (error
|
|
167
|
-
if (
|
|
168
|
-
|
|
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
|
+
}
|
|
169
202
|
} else {
|
|
170
|
-
|
|
203
|
+
// Re-throw with un-corrupted stack trace
|
|
204
|
+
throw new Error(`Element ${elementOrIdentifier.constructor.name} click failed - ${error.constructor.name}: ${error.message}`)
|
|
171
205
|
}
|
|
172
206
|
} else {
|
|
173
|
-
|
|
174
|
-
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}`)
|
|
175
208
|
}
|
|
176
209
|
}
|
|
177
210
|
}
|
|
@@ -181,7 +214,7 @@ export default class SystemTest {
|
|
|
181
214
|
* Finds a single element by CSS selector
|
|
182
215
|
* @param {string} selector
|
|
183
216
|
* @param {object} args
|
|
184
|
-
* @returns {import("selenium-webdriver").WebElement}
|
|
217
|
+
* @returns {Promise<import("selenium-webdriver").WebElement>}
|
|
185
218
|
*/
|
|
186
219
|
async find(selector, args = {}) {
|
|
187
220
|
let elements
|
|
@@ -190,7 +223,11 @@ export default class SystemTest {
|
|
|
190
223
|
elements = await this.all(selector, args)
|
|
191
224
|
} catch (error) {
|
|
192
225
|
// Re-throw to recover stack trace
|
|
193
|
-
|
|
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
|
+
}
|
|
194
231
|
}
|
|
195
232
|
|
|
196
233
|
if (elements.length > 1) {
|
|
@@ -231,7 +268,7 @@ export default class SystemTest {
|
|
|
231
268
|
/**
|
|
232
269
|
* Finds a single element by CSS selector without waiting
|
|
233
270
|
* @param {string} selector
|
|
234
|
-
* @param {object} args
|
|
271
|
+
* @param {object} [args]
|
|
235
272
|
* @returns {Promise<import("selenium-webdriver").WebElement>}
|
|
236
273
|
*/
|
|
237
274
|
async findNoWait(selector, args) {
|
|
@@ -249,7 +286,7 @@ export default class SystemTest {
|
|
|
249
286
|
* @returns {Promise<string[]>}
|
|
250
287
|
*/
|
|
251
288
|
async getBrowserLogs() {
|
|
252
|
-
const entries = await this.
|
|
289
|
+
const entries = await this.getDriver().manage().logs().get(logging.Type.BROWSER)
|
|
253
290
|
const browserLogs = []
|
|
254
291
|
|
|
255
292
|
for (const entry of entries) {
|
|
@@ -272,7 +309,7 @@ export default class SystemTest {
|
|
|
272
309
|
* @returns {Promise<string>}
|
|
273
310
|
*/
|
|
274
311
|
async getCurrentUrl() {
|
|
275
|
-
return await this.
|
|
312
|
+
return await this.getDriver().getCurrentUrl()
|
|
276
313
|
}
|
|
277
314
|
|
|
278
315
|
/**
|
|
@@ -296,33 +333,40 @@ export default class SystemTest {
|
|
|
296
333
|
|
|
297
334
|
const element = await this._findElement(elementOrIdentifier)
|
|
298
335
|
|
|
299
|
-
|
|
336
|
+
// @ts-expect-error
|
|
337
|
+
const candidate = element[methodName]
|
|
338
|
+
|
|
339
|
+
if (!candidate) {
|
|
300
340
|
throw new Error(`${element.constructor.name} hasn't an attribute named: ${methodName}`)
|
|
301
|
-
} else if (typeof
|
|
341
|
+
} else if (typeof candidate != "function") {
|
|
302
342
|
throw new Error(`${element.constructor.name}#${methodName} is not a function`)
|
|
303
343
|
}
|
|
304
344
|
|
|
305
345
|
try {
|
|
306
|
-
return await
|
|
346
|
+
return await candidate(...args)
|
|
307
347
|
} catch (error) {
|
|
308
|
-
if (error
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
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}`)
|
|
315
361
|
} else {
|
|
316
|
-
|
|
362
|
+
await wait(50)
|
|
317
363
|
}
|
|
318
|
-
|
|
319
|
-
throw new Error(`${elementDescription} ${methodName} failed after ${tries} tries - ${error.constructor.name}: ${error.message}`)
|
|
320
364
|
} else {
|
|
321
|
-
|
|
365
|
+
// Re-throw with un-corrupted stack trace
|
|
366
|
+
throw new Error(`${element.constructor.name} ${methodName} failed - ${error.constructor.name}: ${error.message}`)
|
|
322
367
|
}
|
|
323
368
|
} else {
|
|
324
|
-
|
|
325
|
-
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}`)
|
|
326
370
|
}
|
|
327
371
|
}
|
|
328
372
|
}
|
|
@@ -340,7 +384,9 @@ export default class SystemTest {
|
|
|
340
384
|
await this.findNoWait(selector)
|
|
341
385
|
found = true
|
|
342
386
|
} catch (error) {
|
|
343
|
-
if (
|
|
387
|
+
if (error instanceof Error && error.message.startsWith("Element couldn't be found after ")) {
|
|
388
|
+
// Ignore
|
|
389
|
+
} else {
|
|
344
390
|
throw error
|
|
345
391
|
}
|
|
346
392
|
}
|
|
@@ -353,6 +399,7 @@ export default class SystemTest {
|
|
|
353
399
|
/**
|
|
354
400
|
* @param {string} selector
|
|
355
401
|
* @param {object} args
|
|
402
|
+
* @param {boolean} [args.useBaseSelector]
|
|
356
403
|
* @returns {Promise<void>}
|
|
357
404
|
*/
|
|
358
405
|
async waitForNoSelector(selector, args) {
|
|
@@ -368,6 +415,7 @@ export default class SystemTest {
|
|
|
368
415
|
try {
|
|
369
416
|
const actualSelector = useBaseSelector ? this.getSelector(selector) : selector
|
|
370
417
|
|
|
418
|
+
// @ts-expect-error
|
|
371
419
|
await this.driver.wait(until.elementIsNotVisible(By.css(actualSelector)), 0)
|
|
372
420
|
|
|
373
421
|
const timeElapsed = new Date().getTime() - timeStart
|
|
@@ -376,7 +424,7 @@ export default class SystemTest {
|
|
|
376
424
|
throw new Error(`Element still found after ${timeout}ms: ${selector}`)
|
|
377
425
|
}
|
|
378
426
|
} catch (error) {
|
|
379
|
-
if (error.message.startsWith("Element couldn't be found after ")) {
|
|
427
|
+
if (error instanceof Error && error.message.startsWith("Element couldn't be found after ")) {
|
|
380
428
|
break
|
|
381
429
|
}
|
|
382
430
|
}
|
|
@@ -406,9 +454,11 @@ export default class SystemTest {
|
|
|
406
454
|
* @returns {Promise<void>}
|
|
407
455
|
*/
|
|
408
456
|
async expectNotificationMessage(expectedNotificationMessage) {
|
|
457
|
+
/** @type {string[]} */
|
|
409
458
|
const allDetectedNotificationMessages = []
|
|
410
459
|
let foundNotificationMessageElement
|
|
411
460
|
|
|
461
|
+
// @ts-expect-error
|
|
412
462
|
await waitFor(async () => {
|
|
413
463
|
const notificationMessageElements = await this.all("[data-class='notification-message']", {useBaseSelector: false})
|
|
414
464
|
|
|
@@ -456,7 +506,7 @@ export default class SystemTest {
|
|
|
456
506
|
* Gets the HTML of the current page
|
|
457
507
|
* @returns {Promise<string>}
|
|
458
508
|
*/
|
|
459
|
-
async getHTML() { return await this.
|
|
509
|
+
async getHTML() { return await this.getDriver().getPageSource() }
|
|
460
510
|
|
|
461
511
|
/**
|
|
462
512
|
* Starts the system test
|
|
@@ -482,9 +532,11 @@ export default class SystemTest {
|
|
|
482
532
|
options.addArguments("--no-sandbox")
|
|
483
533
|
options.addArguments("--window-size=1920,1080")
|
|
484
534
|
|
|
535
|
+
/** @type {import("selenium-webdriver").WebDriver} */
|
|
485
536
|
this.driver = new Builder()
|
|
486
537
|
.forBrowser("chrome")
|
|
487
538
|
.setChromeOptions(options)
|
|
539
|
+
// @ts-expect-error
|
|
488
540
|
.setCapability("goog:loggingPrefs", {browser: "ALL"})
|
|
489
541
|
.build()
|
|
490
542
|
|
|
@@ -529,7 +581,7 @@ export default class SystemTest {
|
|
|
529
581
|
* @returns {Promise<void>}
|
|
530
582
|
*/
|
|
531
583
|
async driverSetTimeouts(newTimeout) {
|
|
532
|
-
await this.
|
|
584
|
+
await this.getDriver().manage().setTimeouts({implicit: newTimeout})
|
|
533
585
|
}
|
|
534
586
|
|
|
535
587
|
/**
|
|
@@ -568,7 +620,7 @@ export default class SystemTest {
|
|
|
568
620
|
|
|
569
621
|
/**
|
|
570
622
|
* Sets the on command callback
|
|
571
|
-
* @param {function(
|
|
623
|
+
* @param {function({type: string, data: Record<string, any>}): Promise<void>} callback
|
|
572
624
|
* @returns {void}
|
|
573
625
|
*/
|
|
574
626
|
onCommand(callback) {
|
|
@@ -577,8 +629,7 @@ export default class SystemTest {
|
|
|
577
629
|
|
|
578
630
|
/**
|
|
579
631
|
* Handles a command received from the browser
|
|
580
|
-
* @param {
|
|
581
|
-
* @param {object} data.data
|
|
632
|
+
* @param {{data: {message: string, backtrace: string, type: string, value: any[]}}} args
|
|
582
633
|
* @returns {Promise<any>}
|
|
583
634
|
*/
|
|
584
635
|
onCommandReceived = async ({data}) => {
|
|
@@ -612,14 +663,18 @@ export default class SystemTest {
|
|
|
612
663
|
/**
|
|
613
664
|
* Handles a new web socket connection
|
|
614
665
|
* @param {WebSocket} ws
|
|
615
|
-
* @returns {void}
|
|
666
|
+
* @returns {Promise<void>}
|
|
616
667
|
*/
|
|
617
668
|
onWebSocketConnection = async (ws) => {
|
|
618
669
|
this.ws = ws
|
|
619
|
-
this.
|
|
620
|
-
this.
|
|
621
|
-
|
|
622
|
-
|
|
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)
|
|
623
678
|
|
|
624
679
|
if (this.waitForClientWebSocketPromiseResolve) {
|
|
625
680
|
this.waitForClientWebSocketPromiseResolve()
|
|
@@ -632,12 +687,14 @@ export default class SystemTest {
|
|
|
632
687
|
*/
|
|
633
688
|
onWebSocketClose = () => {
|
|
634
689
|
this.ws = null
|
|
635
|
-
this.
|
|
690
|
+
this.getCommunicator().ws = null
|
|
636
691
|
}
|
|
637
692
|
|
|
638
693
|
/**
|
|
639
694
|
* Handles an error reported from the browser
|
|
640
695
|
* @param {object} data
|
|
696
|
+
* @param {string} data.message
|
|
697
|
+
* @param {string} [data.backtrace]
|
|
641
698
|
* @returns {void}
|
|
642
699
|
*/
|
|
643
700
|
handleError(data) {
|
|
@@ -663,7 +720,7 @@ export default class SystemTest {
|
|
|
663
720
|
this.stopScoundrel()
|
|
664
721
|
this.systemTestHttpServer?.close()
|
|
665
722
|
this.wss?.close()
|
|
666
|
-
await this.driver
|
|
723
|
+
await this.driver?.quit()
|
|
667
724
|
}
|
|
668
725
|
|
|
669
726
|
/**
|
|
@@ -674,7 +731,7 @@ export default class SystemTest {
|
|
|
674
731
|
async driverVisit(path) {
|
|
675
732
|
const url = `${this.currentUrl}${path}`
|
|
676
733
|
|
|
677
|
-
await this.
|
|
734
|
+
await this.getDriver().get(url)
|
|
678
735
|
}
|
|
679
736
|
|
|
680
737
|
/**
|
|
@@ -686,7 +743,7 @@ export default class SystemTest {
|
|
|
686
743
|
|
|
687
744
|
await fs.mkdir(path, {recursive: true})
|
|
688
745
|
|
|
689
|
-
const imageContent = await this.
|
|
746
|
+
const imageContent = await this.getDriver().takeScreenshot()
|
|
690
747
|
const now = new Date()
|
|
691
748
|
const screenshotPath = `${path}/${moment(now).format("YYYY-MM-DD-HH-MM-SS")}.png`
|
|
692
749
|
const htmlPath = `${path}/${moment(now).format("YYYY-MM-DD-HH-MM-SS")}.html`
|
|
@@ -711,7 +768,7 @@ export default class SystemTest {
|
|
|
711
768
|
* @returns {Promise<void>}
|
|
712
769
|
*/
|
|
713
770
|
async visit(path) {
|
|
714
|
-
await this.
|
|
771
|
+
await this.getCommunicator().sendCommand({type: "visit", path})
|
|
715
772
|
}
|
|
716
773
|
|
|
717
774
|
/**
|
|
@@ -720,6 +777,6 @@ export default class SystemTest {
|
|
|
720
777
|
* @returns {Promise<void>}
|
|
721
778
|
*/
|
|
722
779
|
async dismissTo(path) {
|
|
723
|
-
await this.
|
|
780
|
+
await this.getCommunicator().sendCommand({type: "dismissTo", path})
|
|
724
781
|
}
|
|
725
782
|
}
|