system-testing 1.0.0
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 +36 -0
- package/src/system-test-browser-helper.js +150 -0
- package/src/system-test-communicator.js +79 -0
- package/src/system-test-http-server.js +54 -0
- package/src/system-test.js +334 -0
package/package.json
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "system-testing",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "System testing with Selenium and browsers.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"system",
|
|
7
|
+
"testing",
|
|
8
|
+
"selenium",
|
|
9
|
+
"chrome"
|
|
10
|
+
],
|
|
11
|
+
"homepage": "https://github.com/kaspernj/system-testing#readme",
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://github.com/kaspernj/system-testing/issues"
|
|
14
|
+
},
|
|
15
|
+
"repository": {
|
|
16
|
+
"type": "git",
|
|
17
|
+
"url": "git+https://github.com/kaspernj/system-testing.git"
|
|
18
|
+
},
|
|
19
|
+
"license": "ISC",
|
|
20
|
+
"author": "kasper@diestoeckels.de",
|
|
21
|
+
"type": "module",
|
|
22
|
+
"main": "src/index.js",
|
|
23
|
+
"scripts": {
|
|
24
|
+
"test": "echo \"Error: no test specified\" && exit 1"
|
|
25
|
+
},
|
|
26
|
+
"dependencies": {
|
|
27
|
+
"diggerize": "^1.0.9",
|
|
28
|
+
"htmlfy": "^0.8.1",
|
|
29
|
+
"mime": "^4.0.7",
|
|
30
|
+
"moment": "^2.30.1",
|
|
31
|
+
"ws": "^8.18.3"
|
|
32
|
+
},
|
|
33
|
+
"peerDependencies": {
|
|
34
|
+
"selenium-webdriver": "^4.34.0"
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import {digg} from "diggerize"
|
|
2
|
+
import EventEmitter from "events"
|
|
3
|
+
|
|
4
|
+
import SystemTestCommunicator from "./system-test-communicator"
|
|
5
|
+
|
|
6
|
+
const shared = {}
|
|
7
|
+
|
|
8
|
+
export default class SystemTestBrowserHelper {
|
|
9
|
+
static current() {
|
|
10
|
+
if (!shared.systemTestBrowserHelper) {
|
|
11
|
+
throw new Error("No current SystemTestBrowserHelper set")
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
return shared.systemTestBrowserHelper
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
constructor() {
|
|
18
|
+
this.communicator = new SystemTestCommunicator({parent: this, onCommand: this.onCommand})
|
|
19
|
+
this._enabled = false
|
|
20
|
+
this.events = new EventEmitter()
|
|
21
|
+
|
|
22
|
+
shared.systemTestBrowserHelper = this
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
connectOnError() {
|
|
26
|
+
window.addEventListener("error", (event) => {
|
|
27
|
+
this.handleError({
|
|
28
|
+
type: "error",
|
|
29
|
+
error: event.error,
|
|
30
|
+
errorClass: event.error?.name,
|
|
31
|
+
file: event.filename,
|
|
32
|
+
line: event.lineno,
|
|
33
|
+
message: event.message || "Unknown error",
|
|
34
|
+
url: window.location.href
|
|
35
|
+
})
|
|
36
|
+
})
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
connectUnhandledRejection() {
|
|
40
|
+
window.addEventListener("unhandledrejection", (event) => {
|
|
41
|
+
this.handleError({
|
|
42
|
+
type: "unhandledrejection",
|
|
43
|
+
error: event.reason,
|
|
44
|
+
errorClass: "UnhandledRejection",
|
|
45
|
+
file: null,
|
|
46
|
+
line: null,
|
|
47
|
+
message: event.reason.message || "Unhandled promise rejection",
|
|
48
|
+
url: window.location.href
|
|
49
|
+
})
|
|
50
|
+
})
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
handleError(data) {
|
|
54
|
+
let backtrace
|
|
55
|
+
|
|
56
|
+
if (data.error && data.error.stack) {
|
|
57
|
+
backtrace = data.error.stack
|
|
58
|
+
} else if (data.file) {
|
|
59
|
+
backtrace = [`${data.file}:${data.line}`]
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
data.backtrace = backtrace
|
|
63
|
+
|
|
64
|
+
this.communicator.sendCommand(data)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
connectWebSocket() {
|
|
68
|
+
this.ws = new WebSocket("ws://localhost:1985") // eslint-disable-line no-undef
|
|
69
|
+
|
|
70
|
+
this.communicator.ws = this.ws
|
|
71
|
+
this.ws.addEventListener("error", digg(this, "communicator", "onError"))
|
|
72
|
+
this.ws.addEventListener("open", digg(this, "communicator", "onOpen"))
|
|
73
|
+
this.ws.addEventListener("message", (event) => this.communicator.onMessage(event.data))
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
enableOnBrowser() {
|
|
77
|
+
this._enabled = true
|
|
78
|
+
this.connectWebSocket()
|
|
79
|
+
this.connectOnError()
|
|
80
|
+
this.connectUnhandledRejection()
|
|
81
|
+
this.overrideConsoleLog()
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
getEnabled = () => this._enabled
|
|
85
|
+
getEvents = () => this.events
|
|
86
|
+
|
|
87
|
+
fakeConsoleError = (...args) => {
|
|
88
|
+
this.communicator.sendCommand({type: "console.error", value: this.consoleLogMessage(args)})
|
|
89
|
+
|
|
90
|
+
return this.originalConsoleError(...args)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
fakeConsoleLog = (...args) => {
|
|
94
|
+
this.communicator.sendCommand({type: "console.log", value: this.consoleLogMessage(args)})
|
|
95
|
+
|
|
96
|
+
return this.originalConsoleLog(...args)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
consoleLogMessage(arg) {
|
|
100
|
+
if (Array.isArray(arg)) {
|
|
101
|
+
const result = []
|
|
102
|
+
|
|
103
|
+
for (const value of arg) {
|
|
104
|
+
result.push(this.consoleLogMessage(value))
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return result
|
|
108
|
+
} else if (Object.prototype.toString.call(arg) === '[object Object]') {
|
|
109
|
+
const result = {}
|
|
110
|
+
|
|
111
|
+
for (const key in arg) {
|
|
112
|
+
result[key] = this.consoleLogMessage(arg[key])
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return result
|
|
116
|
+
} else if (typeof arg == "object") {
|
|
117
|
+
return `[object ${arg?.constructor?.name}]`
|
|
118
|
+
} else {
|
|
119
|
+
return arg
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
onCommand = async ({data}) => {
|
|
124
|
+
if (data.type == "initialize") {
|
|
125
|
+
this.events.emit("initialize")
|
|
126
|
+
|
|
127
|
+
return {result: "initialized"}
|
|
128
|
+
} else if (data.type == "visit") {
|
|
129
|
+
this.events.emit("navigate", {path: data.path})
|
|
130
|
+
} else {
|
|
131
|
+
throw new Error(`Unknown command type for SystemTestBrowserHelper: ${data.type}`)
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
overrideConsoleLog() {
|
|
136
|
+
if (this.originalConsoleError || this.originalConsoleLog) {
|
|
137
|
+
throw new Error("Console methods has already been overridden!")
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
this.originalConsoleError = console.error
|
|
141
|
+
this.originalConsoleLog = console.log
|
|
142
|
+
|
|
143
|
+
console.error = this.fakeConsoleError
|
|
144
|
+
console.log = this.fakeConsoleLog
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
async sendQuery(sql) {
|
|
148
|
+
return await this.communicator.sendCommand({type: "query", sql})
|
|
149
|
+
}
|
|
150
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
export default class SystemTestCommunicator {
|
|
2
|
+
constructor({onCommand, parent}) {
|
|
3
|
+
this.onCommand = onCommand
|
|
4
|
+
this.parent = parent
|
|
5
|
+
this._sendQueueCount = 0
|
|
6
|
+
this._sendQueue = []
|
|
7
|
+
this._responses = {}
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
flushSendQueue() {
|
|
11
|
+
while (this._sendQueue.length !== 0) {
|
|
12
|
+
const data = this._sendQueue.shift()
|
|
13
|
+
|
|
14
|
+
this.ws.send(JSON.stringify(data))
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
onError = (error) => {
|
|
19
|
+
console.error("onWebSocketClientError", error)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
onMessage = async (rawData) => {
|
|
23
|
+
const data = JSON.parse(rawData)
|
|
24
|
+
|
|
25
|
+
if (data.isTrusted) {
|
|
26
|
+
// Ignore
|
|
27
|
+
} else if (data.type == "command") {
|
|
28
|
+
try {
|
|
29
|
+
const result = await this.onCommand({data: data.data})
|
|
30
|
+
|
|
31
|
+
this.respond(data.id, {result})
|
|
32
|
+
} catch (error) {
|
|
33
|
+
this.respond(data.id, {error: error.message})
|
|
34
|
+
}
|
|
35
|
+
} else if (data.type == "response") {
|
|
36
|
+
const response = this._responses[data.id]
|
|
37
|
+
|
|
38
|
+
if (!response) {
|
|
39
|
+
throw new Error(`No such response: ${data.id}`)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
delete this._responses[data.id]
|
|
43
|
+
|
|
44
|
+
if (data.data.error) {
|
|
45
|
+
response.error(data.data.error)
|
|
46
|
+
} else {
|
|
47
|
+
response.resolve(data.data.result)
|
|
48
|
+
}
|
|
49
|
+
} else {
|
|
50
|
+
throw new Error(`Unknown type for SystemTestCommunicator: ${data.type}: ${JSON.stringify(data)}`)
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
onOpen = () => {
|
|
55
|
+
this.flushSendQueue()
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
send(data) {
|
|
59
|
+
this._sendQueue.push(data)
|
|
60
|
+
|
|
61
|
+
if (this.ws?.readyState == 1) {
|
|
62
|
+
this.flushSendQueue()
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
sendCommand(data) {
|
|
67
|
+
return new Promise((resolve, error) => {
|
|
68
|
+
const id = this._sendQueueCount
|
|
69
|
+
|
|
70
|
+
this._sendQueueCount += 1
|
|
71
|
+
this._responses[id] = {resolve, error}
|
|
72
|
+
this.send({type: "command", id, data})
|
|
73
|
+
})
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
respond(id, data) {
|
|
77
|
+
this.send({type: "response", id, data})
|
|
78
|
+
}
|
|
79
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import fs from "node:fs/promises"
|
|
2
|
+
import http from "node:http"
|
|
3
|
+
import mime from "mime"
|
|
4
|
+
import url from "url"
|
|
5
|
+
|
|
6
|
+
export default class SystemTestHttpServer {
|
|
7
|
+
close() {
|
|
8
|
+
this.httpServer.close()
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
onHttpServerRequest = async (request, response) => {
|
|
12
|
+
const parsedUrl = url.parse(request.url)
|
|
13
|
+
let filePath = `${this.basePath}/dist${parsedUrl.pathname}`
|
|
14
|
+
|
|
15
|
+
if (filePath.endsWith("/")) {
|
|
16
|
+
filePath += "index.html"
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
let fileExists
|
|
20
|
+
|
|
21
|
+
try {
|
|
22
|
+
await fs.stat(filePath)
|
|
23
|
+
fileExists = true
|
|
24
|
+
} catch (_error) {
|
|
25
|
+
fileExists = false
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (!fileExists) {
|
|
29
|
+
filePath = `${this.basePath}/dist/index.html`
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const fileContent = await fs.readFile(filePath)
|
|
33
|
+
const mimeType = mime.lookup(filePath)
|
|
34
|
+
|
|
35
|
+
response.statusCode = 200
|
|
36
|
+
response.setHeader("Content-Type", mimeType)
|
|
37
|
+
response.end(fileContent)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async start() {
|
|
41
|
+
this.basePath = await fs.realpath(`${__dirname}/../..`) // eslint-disable-line no-undef
|
|
42
|
+
|
|
43
|
+
await this.startHttpServer()
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
startHttpServer() {
|
|
47
|
+
return new Promise((resolve) => {
|
|
48
|
+
this.httpServer = http.createServer(this.onHttpServerRequest)
|
|
49
|
+
this.httpServer.listen(1984, "localhost", () => {
|
|
50
|
+
resolve()
|
|
51
|
+
})
|
|
52
|
+
})
|
|
53
|
+
}
|
|
54
|
+
}
|
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
import {Builder, By} from "selenium-webdriver"
|
|
2
|
+
import chrome from "selenium-webdriver/chrome"
|
|
3
|
+
import {digg} from "diggerize"
|
|
4
|
+
import fs from "node:fs/promises"
|
|
5
|
+
import logging from "selenium-webdriver/lib/logging"
|
|
6
|
+
import moment from "moment"
|
|
7
|
+
import {prettify} from "htmlfy"
|
|
8
|
+
import SystemTestCommunicator from "./system-test-communicator"
|
|
9
|
+
import SystemTestHttpServer from "./system-test-http-server"
|
|
10
|
+
import {WebSocketServer} from "ws"
|
|
11
|
+
|
|
12
|
+
class ElementNotFoundError extends Error { }
|
|
13
|
+
|
|
14
|
+
export default class SystemTest {
|
|
15
|
+
static current() {
|
|
16
|
+
if (!globalThis.systemTest) {
|
|
17
|
+
globalThis.systemTest = new SystemTest()
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return globalThis.systemTest
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
static async run(callback) {
|
|
24
|
+
const systemTest = this.current()
|
|
25
|
+
|
|
26
|
+
await systemTest.communicator.sendCommand({type: "initialize"})
|
|
27
|
+
await systemTest.visit("/blank")
|
|
28
|
+
await systemTest.findByTestID("blankText")
|
|
29
|
+
|
|
30
|
+
try {
|
|
31
|
+
await callback(systemTest)
|
|
32
|
+
} catch (error) {
|
|
33
|
+
await systemTest.takeScreenshot()
|
|
34
|
+
|
|
35
|
+
throw error
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
constructor() {
|
|
40
|
+
this.communicator = new SystemTestCommunicator({onCommand: this.onCommand})
|
|
41
|
+
this._responses = {}
|
|
42
|
+
this._sendCount = 0
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async all(selector, args = {}) {
|
|
46
|
+
const {visible = true} = args
|
|
47
|
+
const elements = await this.driver.findElements(By.css(selector))
|
|
48
|
+
const activeElements = []
|
|
49
|
+
|
|
50
|
+
for (const element of elements) {
|
|
51
|
+
let keep = true
|
|
52
|
+
|
|
53
|
+
if (visible === true || visible === false) {
|
|
54
|
+
const isDisplayed = await element.isDisplayed()
|
|
55
|
+
|
|
56
|
+
if (visible && !isDisplayed) keep = false
|
|
57
|
+
if (!visible && isDisplayed) keep = false
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (keep) activeElements.push(element)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return activeElements
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Clicks an element that has children which fills out the element and would otherwise have caused a ElementClickInterceptedError
|
|
67
|
+
async click(element) {
|
|
68
|
+
const actions = this.driver.actions({async: true})
|
|
69
|
+
|
|
70
|
+
await actions.move({origin: element}).click().perform()
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async find(selector, args = {}) {
|
|
74
|
+
const elements = await this.all(selector, args)
|
|
75
|
+
|
|
76
|
+
if (elements.length > 1) {
|
|
77
|
+
throw new Error(`More than 1 elements (${elements.length}) was found by CSS: ${selector}`)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (!elements[0]) {
|
|
81
|
+
throw new ElementNotFoundError(`Element couldn't be found by CSS: ${selector}`)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return elements[0]
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
findByTestID = async (testID, args) => await this.find(`[data-testid='${testID}']`, args)
|
|
88
|
+
|
|
89
|
+
async findNoWait(selector) {
|
|
90
|
+
await this.driverSetTimeouts(0)
|
|
91
|
+
|
|
92
|
+
try {
|
|
93
|
+
return await this.find(selector)
|
|
94
|
+
} finally {
|
|
95
|
+
await this.restoreTimeouts()
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async getBrowserLogs() {
|
|
100
|
+
const entries = await this.driver.manage().logs().get(logging.Type.BROWSER)
|
|
101
|
+
const browserLogs = []
|
|
102
|
+
|
|
103
|
+
for (const entry of entries) {
|
|
104
|
+
const messageMatch = entry.message.match(/^(.+) (\d+):(\d+) (.+)$/)
|
|
105
|
+
let message
|
|
106
|
+
|
|
107
|
+
if (messageMatch) {
|
|
108
|
+
message = messageMatch[4]
|
|
109
|
+
} else {
|
|
110
|
+
message = entry.message
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
browserLogs.push(`${entry.level.name}: ${message}`)
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return browserLogs
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
async expectNoElement(selector) {
|
|
120
|
+
let found = false
|
|
121
|
+
|
|
122
|
+
try {
|
|
123
|
+
await this.findNoWait(selector)
|
|
124
|
+
found = true
|
|
125
|
+
} catch (error) {
|
|
126
|
+
if (!error.message.startsWith("Element couldn't be found by CSS:")) {
|
|
127
|
+
throw error
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (found) {
|
|
132
|
+
throw new Error(`Expected not to find: ${selector}`)
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
async notificationMessages() {
|
|
137
|
+
const notificationMessageElements = await this.all("[data-class='notification-message']")
|
|
138
|
+
const notificationMessageTexts = []
|
|
139
|
+
|
|
140
|
+
for (const notificationMessageElement of notificationMessageElements) {
|
|
141
|
+
const text = await notificationMessageElement.getText()
|
|
142
|
+
|
|
143
|
+
notificationMessageTexts.push(text)
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return notificationMessageTexts
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
isStarted() {
|
|
150
|
+
return this._started
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
async getHTML() {
|
|
154
|
+
return await this.driver.getPageSource()
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
async start() {
|
|
158
|
+
if (process.env.SYSTEM_TEST_HOST == "expo-dev-server") {
|
|
159
|
+
this.currentUrl = "http://localhost:8081"
|
|
160
|
+
} else if (process.env.SYSTEM_TEST_HOST == "dist") {
|
|
161
|
+
this.currentUrl = "http://localhost:1984"
|
|
162
|
+
this.systemTestHttpServer = new SystemTestHttpServer()
|
|
163
|
+
|
|
164
|
+
await this.systemTestHttpServer.start()
|
|
165
|
+
} else {
|
|
166
|
+
throw new Error("Please set SYSTEM_TEST_HOST to 'expo-dev-server' or 'dist'")
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const options = new chrome.Options()
|
|
170
|
+
|
|
171
|
+
options.addArguments("--disable-dev-shm-usage")
|
|
172
|
+
options.addArguments("--disable-gpu")
|
|
173
|
+
options.addArguments("--headless=new")
|
|
174
|
+
options.addArguments("--no-sandbox")
|
|
175
|
+
options.addArguments("--window-size=1920,1080")
|
|
176
|
+
|
|
177
|
+
this.driver = new Builder()
|
|
178
|
+
.forBrowser("chrome")
|
|
179
|
+
.setChromeOptions(options)
|
|
180
|
+
.setCapability("goog:loggingPrefs", {browser: "ALL"})
|
|
181
|
+
.build()
|
|
182
|
+
|
|
183
|
+
this.setTimeouts(4000)
|
|
184
|
+
|
|
185
|
+
// Web socket server to communicate with browser
|
|
186
|
+
await this.startWebSocketServer()
|
|
187
|
+
|
|
188
|
+
// Visit the root page and wait for Expo to be loaded and the app to appear
|
|
189
|
+
await this.driverVisit("/?systemTest=true")
|
|
190
|
+
await this.find("body > #root")
|
|
191
|
+
await this.find("[data-component='flash-notifications-container']", {visible: null})
|
|
192
|
+
|
|
193
|
+
// Wait for client to connect
|
|
194
|
+
await this.waitForClientWebSocket()
|
|
195
|
+
|
|
196
|
+
this._started = true
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
async restoreTimeouts() {
|
|
200
|
+
if (!this._timeouts) {
|
|
201
|
+
throw new Error("Timeouts haven't previously been set")
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
await this.driverSetTimeouts(this._timeouts)
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
async driverSetTimeouts(newTimeout) {
|
|
208
|
+
await this.driver.manage().setTimeouts({implicit: newTimeout})
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
async setTimeouts(newTimeout) {
|
|
212
|
+
this._timeouts = newTimeout
|
|
213
|
+
await this.restoreTimeouts()
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
waitForClientWebSocket() {
|
|
217
|
+
return new Promise((resolve) => {
|
|
218
|
+
if (this.ws) {
|
|
219
|
+
resolve()
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
this.waitForClientWebSocketPromiseResolve = resolve
|
|
223
|
+
})
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
startWebSocketServer() {
|
|
227
|
+
this.wss = new WebSocketServer({port: 1985})
|
|
228
|
+
this.wss.on("connection", this.onWebSocketConnection)
|
|
229
|
+
this.wss.on("close", this.onWebSocketClose)
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
onCommand = async ({data}) => {
|
|
233
|
+
const type = data.type
|
|
234
|
+
let result
|
|
235
|
+
|
|
236
|
+
if (type == "console.error") {
|
|
237
|
+
const errorMessage = data.value[0]
|
|
238
|
+
let showMessage = true
|
|
239
|
+
|
|
240
|
+
if (errorMessage.includes("Minified React error #419")) {
|
|
241
|
+
showMessage = false
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
if (showMessage) {
|
|
245
|
+
console.error("Browser error", ...data.value)
|
|
246
|
+
}
|
|
247
|
+
} else if (type == "console.log") {
|
|
248
|
+
console.log("Browser log", ...data.value)
|
|
249
|
+
} else if (type == "error" || data.type == "unhandledrejection") {
|
|
250
|
+
this.handleError(data)
|
|
251
|
+
} else if (type == "query") {
|
|
252
|
+
await Configuration.current().getDatabasePool().withConnection(async (db) => {
|
|
253
|
+
result = await db.query(data.sql)
|
|
254
|
+
})
|
|
255
|
+
} else {
|
|
256
|
+
console.error(`onWebSocketClientMessage unknown data (type ${type})`, data)
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
return result
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
onWebSocketConnection = async (ws) => {
|
|
263
|
+
this.ws = ws
|
|
264
|
+
this.communicator.ws = ws
|
|
265
|
+
this.communicator.onOpen()
|
|
266
|
+
this.ws.on("error", digg(this, "communicator", "onError"))
|
|
267
|
+
this.ws.on("message", digg(this, "communicator", "onMessage"))
|
|
268
|
+
|
|
269
|
+
if (this.waitForClientWebSocketPromiseResolve) {
|
|
270
|
+
this.waitForClientWebSocketPromiseResolve()
|
|
271
|
+
delete this.waitForClientWebSocketPromiseResolve
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
onWebSocketClose = () => {
|
|
276
|
+
this.ws = null
|
|
277
|
+
this.communicator.ws = null
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
handleError(data) {
|
|
281
|
+
if (data.message.includes("Minified React error #419")) {
|
|
282
|
+
// Ignore this error message
|
|
283
|
+
return
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
setTimeout(() => { // eslint-disable-line no-undef
|
|
287
|
+
const error = new Error(`Browser error: ${data.message}`)
|
|
288
|
+
|
|
289
|
+
if (data.trace) {
|
|
290
|
+
const errorTrace = error.trace
|
|
291
|
+
|
|
292
|
+
error.trace = `${data.trace}${errorTrace}`
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
throw error
|
|
296
|
+
}, 0)
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
async stop() {
|
|
300
|
+
this.systemTestHttpServer?.close()
|
|
301
|
+
this.wss?.close()
|
|
302
|
+
await this.driver.quit()
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
async driverVisit(path) {
|
|
306
|
+
const url = `${this.currentUrl}${path}`
|
|
307
|
+
|
|
308
|
+
await this.driver.get(url)
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
async takeScreenshot() {
|
|
312
|
+
const basePath = await fs.realpath(`${__dirname}/../..`) // eslint-disable-line no-undef
|
|
313
|
+
const path = `${basePath}/tmp/screenshots`
|
|
314
|
+
|
|
315
|
+
await fs.mkdir(path, {recursive: true})
|
|
316
|
+
|
|
317
|
+
const imageContent = await this.driver.takeScreenshot()
|
|
318
|
+
const now = new Date()
|
|
319
|
+
const screenshotPath = `${path}/${moment(now).format("YYYY-MM-DD-HH-MM-SS")}.png`
|
|
320
|
+
const htmlPath = `${path}/${moment(now).format("YYYY-MM-DD-HH-MM-SS")}.html`
|
|
321
|
+
const html = await this.getHTML()
|
|
322
|
+
const htmlPretty = prettify(html)
|
|
323
|
+
|
|
324
|
+
await fs.writeFile(screenshotPath, imageContent, "base64")
|
|
325
|
+
await fs.writeFile(htmlPath, htmlPretty)
|
|
326
|
+
|
|
327
|
+
console.log("Screenshot:", screenshotPath)
|
|
328
|
+
console.log("HTML:", htmlPath)
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
async visit(path) {
|
|
332
|
+
await this.communicator.sendCommand({type: "visit", path})
|
|
333
|
+
}
|
|
334
|
+
}
|