system-testing 1.0.31 → 1.0.33

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.31",
3
+ "version": "1.0.33",
4
4
  "description": "System testing with Selenium and browsers.",
5
5
  "keywords": [
6
6
  "system",
@@ -26,6 +26,7 @@
26
26
  "typecheck": "tsc --noEmit"
27
27
  },
28
28
  "dependencies": {
29
+ "@types/selenium-webdriver": "^4.35.4",
29
30
  "awaitery": "^1.0.2",
30
31
  "diggerize": "^1.0.9",
31
32
  "htmlfy": "^1.0.0",
@@ -1,6 +1,14 @@
1
1
  // @ts-check
2
2
 
3
3
  export default class SystemTestCommunicator {
4
+ /** @type {Record<string, {resolve: (data: any) => void, reject: (data: any) => void}>} */
5
+ _responses = {}
6
+
7
+ /** @type {Record<string, any>} */
8
+ _sendQueue = []
9
+
10
+ _sendQueueCount = 0
11
+
4
12
  /** @type {WebSocket | null} */
5
13
  ws = null
6
14
 
@@ -12,13 +20,6 @@ export default class SystemTestCommunicator {
12
20
  constructor({onCommand, parent}) {
13
21
  this.onCommand = onCommand
14
22
  this.parent = parent
15
- this._sendQueueCount = 0
16
-
17
- /** @type {Record<string, any>} */
18
- this._sendQueue = []
19
-
20
- /** @type {Record<string, {resolve: (data: any) => void, reject: (data: any) => void}>} */
21
- this._responses = {}
22
23
  }
23
24
 
24
25
  flushSendQueue() {
@@ -81,20 +82,20 @@ export default class SystemTestCommunicator {
81
82
  }
82
83
 
83
84
  /**
84
- * @param {object} data
85
+ * @param {Record<string, any>} data
85
86
  * @returns {void}
86
87
  */
87
88
  send(data) {
88
89
  this._sendQueue.push(data)
89
90
 
90
- if (this.ws?.readyState == 1) {
91
+ if (this.ws.readyState == 1) {
91
92
  this.flushSendQueue()
92
93
  }
93
94
  }
94
95
 
95
96
  /**
96
97
  * Sends a command and returns a promise that resolves with the response.
97
- * @param {object} data - The command data to send.
98
+ * @param {Record<string, any>} data - The command data to send.
98
99
  * @returns {Promise<void>} A promise that resolves with the response data.
99
100
  */
100
101
  sendCommand(data) {
@@ -103,13 +104,14 @@ export default class SystemTestCommunicator {
103
104
 
104
105
  this._sendQueueCount += 1
105
106
  this._responses[id] = {resolve, reject}
107
+
106
108
  this.send({type: "command", id, data})
107
109
  })
108
110
  }
109
111
 
110
112
  /**
111
113
  * @param {number} id
112
- * @param {object} data
114
+ * @param {Record<string, any>} data
113
115
  * @returns {void}
114
116
  */
115
117
  respond(id, data) {
@@ -8,7 +8,11 @@ import url from "url"
8
8
  export default class SystemTestHttpServer {
9
9
  /** @returns {void} */
10
10
  close() {
11
- this.httpServer?.close()
11
+ if (!this.httpServer) {
12
+ throw new Error("HTTP server is not initialized")
13
+ }
14
+
15
+ this.httpServer.close()
12
16
  }
13
17
 
14
18
  /**
@@ -1,7 +1,8 @@
1
1
  // @ts-check
2
2
 
3
- import {Builder, By, until} from "selenium-webdriver"
3
+ import {Builder, By} from "selenium-webdriver"
4
4
  import chrome from "selenium-webdriver/chrome.js"
5
+ import {digg} from "diggerize"
5
6
  import fs from "node:fs/promises"
6
7
  import logging from "selenium-webdriver/lib/logging.js"
7
8
  import moment from "moment"
@@ -15,17 +16,17 @@ import {WebSocketServer} from "ws"
15
16
 
16
17
  class ElementNotFoundError extends Error { }
17
18
 
18
- /** @type {{systemTest: SystemTest | null}} */
19
- const shared = {
20
- systemTest: null
21
- }
22
-
23
19
  export default class SystemTest {
24
20
  static rootPath = "/blank?systemTest=true"
25
21
 
26
22
  /** @type {SystemTestCommunicator | undefined} */
27
23
  communicator = undefined
24
+
25
+ /** @type {import("selenium-webdriver").WebDriver | undefined} */
26
+ driver = undefined
27
+
28
28
  _started = false
29
+ _driverTimeouts = 5000
29
30
  _timeouts = 5000
30
31
 
31
32
  /**
@@ -36,11 +37,11 @@ export default class SystemTest {
36
37
  * @returns {SystemTest}
37
38
  */
38
39
  static current(args) {
39
- if (!shared.systemTest) {
40
- shared.systemTest = new SystemTest(args)
40
+ if (!globalThis.systemTest) {
41
+ globalThis.systemTest = new SystemTest(args)
41
42
  }
42
43
 
43
- return shared.systemTest
44
+ return globalThis.systemTest
44
45
  }
45
46
 
46
47
  getCommunicator() {
@@ -87,7 +88,10 @@ export default class SystemTest {
87
88
 
88
89
  this._host = host
89
90
  this._port = port
91
+
92
+ /** @type {Record<number, object>} */
90
93
  this._responses = {}
94
+
91
95
  this._sendCount = 0
92
96
  this.startScoundrel()
93
97
  this.communicator = new SystemTestCommunicator({onCommand: this.onCommandReceived})
@@ -99,7 +103,11 @@ export default class SystemTest {
99
103
  */
100
104
  getBaseSelector() { return this._baseSelector }
101
105
 
106
+ /**
107
+ * @returns {import("selenium-webdriver").WebDriver}
108
+ */
102
109
  getDriver() {
110
+ if (!this) throw new Error("No this?")
103
111
  if (!this.driver) throw new Error("Driver hasn't been initialized yet")
104
112
 
105
113
  return this.driver
@@ -125,6 +133,8 @@ export default class SystemTest {
125
133
  * @returns {void}
126
134
  */
127
135
  startScoundrel() {
136
+ if (this.wss) throw new Error("Scoundrel server already started")
137
+
128
138
  this.wss = new WebSocketServer({port: 8090})
129
139
  this.serverWebSocket = new ServerWebSocket(this.wss)
130
140
  this.server = new Server(this.serverWebSocket)
@@ -142,18 +152,38 @@ export default class SystemTest {
142
152
  * Finds all elements by CSS selector
143
153
  * @param {string} selector
144
154
  * @param {object} args
155
+ * @param {number} [args.timeout]
145
156
  * @param {boolean} [args.visible]
146
157
  * @param {boolean} [args.useBaseSelector]
147
158
  * @returns {Promise<import("selenium-webdriver").WebElement[]>}
148
159
  */
149
160
  async all(selector, args = {}) {
150
- const {visible = true, useBaseSelector = true, ...restArgs} = args
161
+ const {visible = true, timeout, useBaseSelector = true, ...restArgs} = args
151
162
  const restArgsKeys = Object.keys(restArgs)
163
+ let actualTimeout
164
+
165
+ if (timeout === undefined) {
166
+ actualTimeout = this._driverTimeouts
167
+ } else {
168
+ actualTimeout = timeout
169
+ }
152
170
 
153
171
  if (restArgsKeys.length > 0) throw new Error(`Unknown arguments: ${restArgsKeys.join(", ")}`)
154
172
 
155
173
  const actualSelector = useBaseSelector ? this.getSelector(selector) : selector
156
- const elements = await this.getDriver().findElements(By.css(actualSelector))
174
+ const getElements = async () => await this.getDriver().findElements(By.css(actualSelector))
175
+ let elements = []
176
+
177
+ if (actualTimeout == 0) {
178
+ elements = await getElements()
179
+ } else {
180
+ await this.getDriver().wait(async () => {
181
+ elements = await getElements()
182
+
183
+ return elements.length > 0
184
+ }, actualTimeout)
185
+ }
186
+
157
187
  const activeElements = []
158
188
 
159
189
  for (const element of elements) {
@@ -215,13 +245,17 @@ export default class SystemTest {
215
245
  * @returns {Promise<import("selenium-webdriver").WebElement>}
216
246
  */
217
247
  async find(selector, args = {}) {
218
- let elements
248
+ let elements = []
219
249
 
220
250
  try {
221
251
  elements = await this.all(selector, args)
222
252
  } catch (error) {
223
253
  // Re-throw to recover stack trace
224
254
  if (error instanceof Error) {
255
+ if (error.message.startsWith("Wait timed out after")) {
256
+ elements = []
257
+ }
258
+
225
259
  throw new Error(`${error.message} (selector: ${this.getSelector(selector)})`)
226
260
  } else {
227
261
  throw new Error(`${error} (selector: ${this.getSelector(selector)})`)
@@ -330,16 +364,16 @@ export default class SystemTest {
330
364
  tries++
331
365
 
332
366
  const element = await this._findElement(elementOrIdentifier)
333
- const candidate = element[methodName]
334
367
 
335
- if (!candidate) {
368
+ if (!element[methodName]) {
336
369
  throw new Error(`${element.constructor.name} hasn't an attribute named: ${methodName}`)
337
- } else if (typeof candidate != "function") {
370
+ } else if (typeof element[methodName] != "function") {
338
371
  throw new Error(`${element.constructor.name}#${methodName} is not a function`)
339
372
  }
340
373
 
341
374
  try {
342
- return await candidate(...args)
375
+ // Dont call with candidate, because that will bind the function wrong.
376
+ return await element[methodName](...args)
343
377
  } catch (error) {
344
378
  if (error instanceof Error) {
345
379
  if (error.constructor.name === "ElementNotInteractableError") {
@@ -399,31 +433,30 @@ export default class SystemTest {
399
433
  * @returns {Promise<void>}
400
434
  */
401
435
  async waitForNoSelector(selector, args) {
402
- const timeStart = new Date().getTime()
403
- const timeout = this.getTimeouts()
404
436
  const {useBaseSelector, ...restArgs} = args
405
437
 
406
438
  if (Object.keys(restArgs).length > 0) {
407
439
  throw new Error(`Unexpected args: ${Object.keys(restArgs).join(", ")}`)
408
440
  }
409
441
 
410
- while (true) {
411
- try {
412
- const actualSelector = useBaseSelector ? this.getSelector(selector) : selector
413
-
414
- await this.driver.wait(until.elementIsNotVisible(By.css(actualSelector)), 0)
442
+ const actualSelector = useBaseSelector ? this.getSelector(selector) : selector
415
443
 
416
- const timeElapsed = new Date().getTime() - timeStart
444
+ await this.getDriver().wait(
445
+ async () => {
446
+ const elements = await this.getDriver().findElements(By.css(actualSelector))
417
447
 
418
- if (timeElapsed > timeout) {
419
- throw new Error(`Element still found after ${timeout}ms: ${selector}`)
420
- }
421
- } catch (error) {
422
- if (error instanceof Error && error.message.startsWith("Element couldn't be found after ")) {
423
- break
448
+ // Not found at all
449
+ if (elements.length === 0) {
450
+ return true
424
451
  }
425
- }
426
- }
452
+
453
+ // Found but not visible
454
+ const isDisplayed = await elements[0].isDisplayed()
455
+
456
+ return !isDisplayed
457
+ },
458
+ this.getTimeouts()
459
+ )
427
460
  }
428
461
 
429
462
  /**
@@ -526,10 +559,10 @@ export default class SystemTest {
526
559
  options.addArguments("--no-sandbox")
527
560
  options.addArguments("--window-size=1920,1080")
528
561
 
529
- /** @type {import("selenium-webdriver").WebDriver} */
530
562
  this.driver = new Builder()
531
563
  .forBrowser("chrome")
532
564
  .setChromeOptions(options)
565
+ // @ts-expect-error
533
566
  .setCapability("goog:loggingPrefs", {browser: "ALL"})
534
567
  .build()
535
568
 
@@ -574,6 +607,7 @@ export default class SystemTest {
574
607
  * @returns {Promise<void>}
575
608
  */
576
609
  async driverSetTimeouts(newTimeout) {
610
+ this._driverTimeouts = newTimeout
577
611
  await this.getDriver().manage().setTimeouts({implicit: newTimeout})
578
612
  }
579
613
 
@@ -664,10 +698,10 @@ export default class SystemTest {
664
698
  this.getCommunicator().onOpen()
665
699
 
666
700
  // @ts-expect-error
667
- this.ws.on("error", this.communicator.onError)
701
+ this.ws.on("error", digg(this, "communicator", "onError"))
668
702
 
669
703
  // @ts-expect-error
670
- this.ws.on("message", this.communicator.onMessage)
704
+ this.ws.on("message", digg(this, "communicator", "onMessage"))
671
705
 
672
706
  if (this.waitForClientWebSocketPromiseResolve) {
673
707
  this.waitForClientWebSocketPromiseResolve()
package/tsconfig.json CHANGED
@@ -8,6 +8,7 @@
8
8
  "module": "nodenext",
9
9
 
10
10
  "lib": ["dom", "ES2020"],
11
+ "sourceMap": true,
11
12
  "types": ["node"],
12
13
  "skipLibCheck": true
13
14
  }