brut-js 0.0.1 → 0.0.4
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 +5 -1
- package/specs/AjaxSubmit.spec.js +8 -5
- package/specs/Autosubmit.spec.js +1 -1
- package/specs/ConfirmSubmit.spec.js +1 -1
- package/specs/ConstraintViolationMessage.spec.js +1 -1
- package/specs/ConstraintViolationMessages.spec.js +1 -1
- package/specs/Form.spec.js +1 -1
- package/specs/I18nTranslation.spec.js +1 -1
- package/specs/LocaleDetection.spec.js +2 -2
- package/specs/Message.spec.js +1 -1
- package/specs/SpecHelper.js +23 -0
- package/specs/Tabs.spec.js +1 -1
- package/src/testing/AssetMetadata.js +35 -0
- package/src/testing/AssetMetadataLoader.js +25 -0
- package/src/testing/CustomElementTest.js +235 -0
- package/src/testing/DOMCreator.js +45 -0
- package/src/testing/index.js +13 -313
- package/.projections.json +0 -10
package/package.json
CHANGED
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "brut-js",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.4",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"keywords": [ "WebComponents", "Custom Elements" ],
|
|
6
6
|
"bugs": {
|
|
7
7
|
"url": "https://github.com/thirdtank/brut-js/issues",
|
|
8
8
|
"email": "davec@naildrivin5.com"
|
|
9
9
|
},
|
|
10
|
+
"exports": {
|
|
11
|
+
".": "./src/index.js",
|
|
12
|
+
"./testing": "./src/testing/index.js"
|
|
13
|
+
},
|
|
10
14
|
"repository": "https://github.com/thirdtank/brut-js",
|
|
11
15
|
"author": {
|
|
12
16
|
"name": "David Copeland",
|
package/specs/AjaxSubmit.spec.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { withHTML
|
|
1
|
+
import { withHTML } from "./SpecHelper.js"
|
|
2
2
|
|
|
3
3
|
describe("<brut-ajax-submit>", () => {
|
|
4
4
|
withHTML(`
|
|
@@ -12,7 +12,9 @@ describe("<brut-ajax-submit>", () => {
|
|
|
12
12
|
`).onFetch( "/foo", [
|
|
13
13
|
{ then: { status: 200 }},
|
|
14
14
|
]
|
|
15
|
-
).test("submits the form, setting various attributes during the lifecycle",
|
|
15
|
+
).test("submits the form, setting various attributes during the lifecycle",
|
|
16
|
+
({document,assert,fetchRequests,waitForSetTimeout,readRequestBodyIntoString}) => {
|
|
17
|
+
|
|
16
18
|
const element = document.querySelector("brut-ajax-submit")
|
|
17
19
|
const button = element.querySelector("button")
|
|
18
20
|
const text = document.querySelector("input[name=some-text]")
|
|
@@ -80,7 +82,8 @@ describe("<brut-ajax-submit>", () => {
|
|
|
80
82
|
{ then: { status: 500 }},
|
|
81
83
|
{ then: { status: 200 }},
|
|
82
84
|
]
|
|
83
|
-
).test("submits the form after a retry",
|
|
85
|
+
).test("submits the form after a retry",
|
|
86
|
+
({document,assert,fetchRequests,waitForSetTimeout,readRequestBodyIntoString}) => {
|
|
84
87
|
const element = document.querySelector("brut-ajax-submit")
|
|
85
88
|
const button = element.querySelector("button")
|
|
86
89
|
const text = document.querySelector("input[name=some-text]")
|
|
@@ -124,7 +127,7 @@ describe("<brut-ajax-submit>", () => {
|
|
|
124
127
|
{ then: { status: 500 }},
|
|
125
128
|
{ then: { status: 500 }},
|
|
126
129
|
]
|
|
127
|
-
).test("when too many failures, submits the form the old-fashioned way", ({document,assert,fetchRequests}) => {
|
|
130
|
+
).test("when too many failures, submits the form the old-fashioned way", ({document,assert,fetchRequests, waitForSetTimeout}) => {
|
|
128
131
|
const form = document.querySelector("form")
|
|
129
132
|
const element = form.querySelector("brut-ajax-submit")
|
|
130
133
|
const button = element.querySelector("button")
|
|
@@ -195,7 +198,7 @@ Error that should be ignored
|
|
|
195
198
|
}
|
|
196
199
|
},
|
|
197
200
|
]
|
|
198
|
-
).test("when we get a 422, parses the result from the server", ({document,window,assert,fetchRequests}) => {
|
|
201
|
+
).test("when we get a 422, parses the result from the server", ({document,window,assert,fetchRequests,waitForSetTimeout}) => {
|
|
199
202
|
const form = document.querySelector("form")
|
|
200
203
|
const element = form.querySelector("brut-ajax-submit")
|
|
201
204
|
const button = element.querySelector("button")
|
package/specs/Autosubmit.spec.js
CHANGED
package/specs/Form.spec.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { withHTML
|
|
1
|
+
import { withHTML } from "./SpecHelper.js"
|
|
2
2
|
|
|
3
3
|
describe("<brut-locale-detection>", () => {
|
|
4
4
|
withHTML(`
|
|
@@ -6,7 +6,7 @@ describe("<brut-locale-detection>", () => {
|
|
|
6
6
|
`).onFetch( "/locale", [
|
|
7
7
|
{ then: { status: 200 }},
|
|
8
8
|
]
|
|
9
|
-
).test("Receives the locale and timeZone from the browser", ({document,assert,fetchRequests}) => {
|
|
9
|
+
).test("Receives the locale and timeZone from the browser", ({document,assert,fetchRequests,readRequestBodyIntoJSON}) => {
|
|
10
10
|
|
|
11
11
|
assert.equal(1,fetchRequests.length)
|
|
12
12
|
return readRequestBodyIntoJSON(fetchRequests[0]).then( (json) => {
|
package/specs/Message.spec.js
CHANGED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { createTestBasedOnHTML } from "../src/testing/index.js"
|
|
2
|
+
|
|
3
|
+
import path from "node:path"
|
|
4
|
+
import fs from "node:fs"
|
|
5
|
+
|
|
6
|
+
const __dirname = import.meta.dirname
|
|
7
|
+
|
|
8
|
+
const appRoot = path.resolve(__dirname)
|
|
9
|
+
const publicRoot = path.resolve(appRoot,"public")
|
|
10
|
+
const assetMetadataFilePath = path.resolve(appRoot,"config","asset_metadata.json")
|
|
11
|
+
const assetMetadata = JSON.parse(fs.readFileSync(assetMetadataFilePath))
|
|
12
|
+
|
|
13
|
+
const withHTML = (html) => {
|
|
14
|
+
return createTestBasedOnHTML({
|
|
15
|
+
html,
|
|
16
|
+
assetMetadata,
|
|
17
|
+
publicRoot
|
|
18
|
+
})
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export {
|
|
22
|
+
withHTML,
|
|
23
|
+
}
|
package/specs/Tabs.spec.js
CHANGED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import fs from "node:fs"
|
|
2
|
+
import path from "node:path"
|
|
3
|
+
/**
|
|
4
|
+
* Provides structured access to the asset metadata for a BrutRB app. This is used to allow custom elements
|
|
5
|
+
* to be defined in JSDOM the same way they are in a real browser.
|
|
6
|
+
*
|
|
7
|
+
* @memberof testing
|
|
8
|
+
*/
|
|
9
|
+
class AssetMetadata {
|
|
10
|
+
constructor(parsedJSON, publicRoot) {
|
|
11
|
+
this.assetMetadata = parsedJSON.asset_metadata
|
|
12
|
+
this.publicRoot = publicRoot
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
scriptURLs() {
|
|
16
|
+
return Object.entries(this.assetMetadata[".js"]).map( (entry) => {
|
|
17
|
+
return entry[0]
|
|
18
|
+
})
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
fileContainingScriptURL(scriptURL) {
|
|
22
|
+
const file = Object.entries(this.assetMetadata[".js"]).find( (entry) => {
|
|
23
|
+
return entry[0] == scriptURL
|
|
24
|
+
})
|
|
25
|
+
if (!file || !file[1]) {
|
|
26
|
+
return null
|
|
27
|
+
}
|
|
28
|
+
let relativePath = file[1]
|
|
29
|
+
if (relativePath[0] == "/") {
|
|
30
|
+
relativePath = relativePath.slice(1)
|
|
31
|
+
}
|
|
32
|
+
return fs.readFileSync(path.resolve(this.publicRoot,relativePath))
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
export default AssetMetadata
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { ResourceLoader } from "jsdom"
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* An JSDOM resource loader based on BrutRB asset metadata.
|
|
5
|
+
*
|
|
6
|
+
* @memberof testing
|
|
7
|
+
*/
|
|
8
|
+
class AssetMetadataLoader extends ResourceLoader {
|
|
9
|
+
constructor(assetMetadata) {
|
|
10
|
+
super()
|
|
11
|
+
this.assetMetadata = assetMetadata
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
fetch(url,options) {
|
|
15
|
+
const parsedURL = new URL(url)
|
|
16
|
+
const jsContents = this.assetMetadata.fileContainingScriptURL(parsedURL.pathname)
|
|
17
|
+
if (jsContents) {
|
|
18
|
+
return Promise.resolve(jsContents)
|
|
19
|
+
}
|
|
20
|
+
else {
|
|
21
|
+
return super.fetch(url,options)
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
export default AssetMetadataLoader
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
import DOMCreator from "./DOMCreator.js"
|
|
2
|
+
import assert from "assert"
|
|
3
|
+
/**
|
|
4
|
+
* The class that implements a test case. Typically, an instance of this is created for you and you call `test` on that to write your
|
|
5
|
+
* test case.
|
|
6
|
+
* @memberof testing
|
|
7
|
+
*/
|
|
8
|
+
class CustomElementTest {
|
|
9
|
+
constructor(html, queryString, assetMetadata) {
|
|
10
|
+
this.html = html
|
|
11
|
+
this.queryString = queryString
|
|
12
|
+
this.fetchBehavior = {}
|
|
13
|
+
this.assetMetadata = assetMetadata
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Configure a query string to be present when the custom elements are defined and connected.
|
|
18
|
+
*/
|
|
19
|
+
andQueryString(queryString) {
|
|
20
|
+
this.queryString = queryString
|
|
21
|
+
return this
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Configure behavior when #{link external:fetch} is called, since it's not implemented by JSDom or NodeJS.
|
|
26
|
+
*
|
|
27
|
+
* @param {String} url - the URL that is expected. This should be a relative URL, as that is all that is currently supported. This
|
|
28
|
+
* URL must match exactly to a `fetch` call.
|
|
29
|
+
* @param {Object} behavior - an object describing what you want to happen when `url` is fetched. This currently supports only a
|
|
30
|
+
* few rudimentary behaviors:
|
|
31
|
+
* * `{then: { ok: { text: "some text" } } }` - This will return an "ok" response whose body is the given text, available only as
|
|
32
|
+
* text.
|
|
33
|
+
* * `{then: { status: XXX, text: "some text" }}` - This will return the given http status, with `ok` as true if it's 2xx or 3xx. If `text` is given, that will be available as text only.
|
|
34
|
+
*/
|
|
35
|
+
onFetch(url,behavior) {
|
|
36
|
+
if (!this.fetchBehavior[url]) {
|
|
37
|
+
this.fetchBehavior[url] = {
|
|
38
|
+
numCalls: 0,
|
|
39
|
+
responses: [],
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
if (behavior instanceof Array) {
|
|
43
|
+
behavior.forEach( (b) => {
|
|
44
|
+
this.fetchBehavior[url].responses.push(b)
|
|
45
|
+
})
|
|
46
|
+
}
|
|
47
|
+
else {
|
|
48
|
+
this.fetchBehavior[url].responses.push(behavior)
|
|
49
|
+
}
|
|
50
|
+
return this
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Comment out a test without using code comments */
|
|
54
|
+
xtest() {
|
|
55
|
+
return this
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
/** Declare a test to run with the previously-defined HTML, query string, and fetch behavior.
|
|
60
|
+
*
|
|
61
|
+
* @param {String} description - a description of the test.
|
|
62
|
+
* @param {testCodeCallback} testCode - a function containing the code for your test.
|
|
63
|
+
*/
|
|
64
|
+
test(description,testCode) {
|
|
65
|
+
it(description, () => {
|
|
66
|
+
const domCreator = new DOMCreator(this.assetMetadata)
|
|
67
|
+
const dom = domCreator.create({
|
|
68
|
+
html: this.html,
|
|
69
|
+
queryString: this.queryString
|
|
70
|
+
})
|
|
71
|
+
const fetchRequests = []
|
|
72
|
+
|
|
73
|
+
dom.window.Request = Request
|
|
74
|
+
dom.window.fetch = (request) => {
|
|
75
|
+
const url = new URL(request.url)
|
|
76
|
+
const path = url.pathname + url.search
|
|
77
|
+
const behaviors = this.fetchBehavior[path]
|
|
78
|
+
if (!behaviors) {
|
|
79
|
+
throw `fetch() called with ${path}, which was not configured`
|
|
80
|
+
}
|
|
81
|
+
if (behaviors.numCalls > behaviors.responses.length) {
|
|
82
|
+
throw `fetch() called ${behaviors.numCalls} times, but we only have ${behaviors.response.length} responses configured`
|
|
83
|
+
}
|
|
84
|
+
const behavior = behaviors.responses[behaviors.numCalls]
|
|
85
|
+
behaviors.numCalls++
|
|
86
|
+
|
|
87
|
+
let promise = null
|
|
88
|
+
|
|
89
|
+
if (behavior.then) {
|
|
90
|
+
if (behavior.then.ok) {
|
|
91
|
+
if (behavior.then.ok.text) {
|
|
92
|
+
const response = {
|
|
93
|
+
ok: true,
|
|
94
|
+
text: () => {
|
|
95
|
+
return Promise.resolve(behavior.then.ok.text)
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
promise = Promise.resolve(response)
|
|
99
|
+
}
|
|
100
|
+
else {
|
|
101
|
+
throw `unknown fetch behavior: expected then.ok.text: ${JSON.stringify(behavior)}`
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
else if (behavior.then.status) {
|
|
105
|
+
let ok = false
|
|
106
|
+
if ((behavior.then.status >= 200) && (behavior.then.status < 400) ){
|
|
107
|
+
ok = true
|
|
108
|
+
}
|
|
109
|
+
const response = {
|
|
110
|
+
ok: ok,
|
|
111
|
+
status: behavior.then.status,
|
|
112
|
+
text: () => {
|
|
113
|
+
return Promise.resolve(behavior.then.text)
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
promise = Promise.resolve(response)
|
|
117
|
+
}
|
|
118
|
+
else {
|
|
119
|
+
throw `unknown fetch behavior: expected then.ok or then.status: ${JSON.stringify(behavior)}`
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
else {
|
|
123
|
+
throw `unknown fetch behavior: expected then: ${JSON.stringify(behavior)}`
|
|
124
|
+
}
|
|
125
|
+
request.promiseReturned = promise
|
|
126
|
+
fetchRequests.push(request)
|
|
127
|
+
return promise
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const window = dom.window
|
|
131
|
+
const document = window.document
|
|
132
|
+
let returnValue = null
|
|
133
|
+
return new Promise( (resolve, reject) => {
|
|
134
|
+
dom.window.addEventListener("load", () => {
|
|
135
|
+
try {
|
|
136
|
+
const paramsGivenToTest = {
|
|
137
|
+
window,
|
|
138
|
+
document,
|
|
139
|
+
assert,
|
|
140
|
+
fetchRequests,
|
|
141
|
+
waitForSetTimeout: this.#waitForSetTimeout,
|
|
142
|
+
readRequestBodyIntoJSON: this.#readRequestBodyIntoJSON,
|
|
143
|
+
readRequestBodyIntoString: this.#readRequestBodyIntoString,
|
|
144
|
+
}
|
|
145
|
+
returnValue = testCode(paramsGivenToTest)
|
|
146
|
+
if (returnValue) {
|
|
147
|
+
resolve(returnValue)
|
|
148
|
+
}
|
|
149
|
+
else {
|
|
150
|
+
resolve()
|
|
151
|
+
}
|
|
152
|
+
} catch (e) {
|
|
153
|
+
reject(e)
|
|
154
|
+
}
|
|
155
|
+
})
|
|
156
|
+
})
|
|
157
|
+
})
|
|
158
|
+
return this
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** Used to wait for a few milliseconds before performing further assertions.
|
|
162
|
+
* This can be useful when a custom element has `setTimeout` calls
|
|
163
|
+
*/
|
|
164
|
+
#waitForSetTimeout = (ms) => {
|
|
165
|
+
return new Promise( (resolve) => {
|
|
166
|
+
setTimeout(resolve,ms)
|
|
167
|
+
})
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Given a fetch request (available via `fetchRequests` passed to your test), turn the body into JSON
|
|
172
|
+
* and return a promise with the JSON. To use this in a test, you must include your assertions inside
|
|
173
|
+
* the `then` of the returned promise and you *must* return that from your test.
|
|
174
|
+
*
|
|
175
|
+
* @example
|
|
176
|
+
* withHTML(
|
|
177
|
+
* "<div-makes-a-fetch>"
|
|
178
|
+
* ).onFetch("/foo", { then: { status: 200 }}
|
|
179
|
+
* ).test("a test",({assert,fetchRequests}) => {
|
|
180
|
+
* assert.equal(fetchRequests.length,1)
|
|
181
|
+
* return readRequestBodyIntoJSON(fetchRequests[0]).then( (json) => {
|
|
182
|
+
* assert.equal(json["foo"],"bar")
|
|
183
|
+
* })
|
|
184
|
+
* })
|
|
185
|
+
*/
|
|
186
|
+
#readRequestBodyIntoJSON = (request) => {
|
|
187
|
+
return this.#readRequestBodyIntoString(request).then( (string) => {
|
|
188
|
+
try {
|
|
189
|
+
const json = JSON.parse(string)
|
|
190
|
+
return Promise.resolve(json)
|
|
191
|
+
}
|
|
192
|
+
catch (e) {
|
|
193
|
+
assert(false,`'${string}' could not be parsed as JSON`)
|
|
194
|
+
}
|
|
195
|
+
return Promise.resolve()
|
|
196
|
+
})
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Given a fetch request (available via `fetchRequests` passed to your test), turn the body into a string
|
|
201
|
+
* and return a promise with the string. To use this in a test, you must include your assertions inside
|
|
202
|
+
* the `then` of the returned promise and you *must* return that from your test.
|
|
203
|
+
*
|
|
204
|
+
* @example
|
|
205
|
+
* withHTML(
|
|
206
|
+
* "<div-makes-a-fetch>"
|
|
207
|
+
* ).onFetch("/foo", { then: { status: 200 }}
|
|
208
|
+
* ).test("a test",({assert,fetchRequests}) => {
|
|
209
|
+
* assert.equal(fetchRequests.length,1)
|
|
210
|
+
* return readRequestBodyIntoString(fetchRequests[0]).then( (string) => {
|
|
211
|
+
* assert.equal(string,"foo")
|
|
212
|
+
* })
|
|
213
|
+
* })
|
|
214
|
+
*/
|
|
215
|
+
#readRequestBodyIntoString = (request) => {
|
|
216
|
+
const reader = request.body.getReader()
|
|
217
|
+
const utf8Decoder = new TextDecoder("utf-8");
|
|
218
|
+
let string = ""
|
|
219
|
+
const parse = ({value,done}) => {
|
|
220
|
+
if (done) {
|
|
221
|
+
return Promise.resolve(string)
|
|
222
|
+
}
|
|
223
|
+
else {
|
|
224
|
+
if (value) {
|
|
225
|
+
string = string + utf8Decoder.decode(value, { stream: true })
|
|
226
|
+
}
|
|
227
|
+
return reader.read().then(parse)
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
return reader.read().then(parse)
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
}
|
|
235
|
+
export default CustomElementTest
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { JSDOM, VirtualConsole } from "jsdom"
|
|
2
|
+
|
|
3
|
+
import AssetMetadataLoader from "./AssetMetadataLoader.js"
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Creates a JSDOM based on the the given HTML and query string.
|
|
7
|
+
*
|
|
8
|
+
* @memberof testing
|
|
9
|
+
*/
|
|
10
|
+
class DOMCreator {
|
|
11
|
+
constructor(assetMetadata) {
|
|
12
|
+
this.assetMetadata = assetMetadata
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
create({html,queryString}) {
|
|
16
|
+
|
|
17
|
+
const resourceLoader = new AssetMetadataLoader(this.assetMetadata)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
const url = "http://example.com" + ( queryString ? `?${queryString}` : "" )
|
|
21
|
+
|
|
22
|
+
const scripts = this.assetMetadata.scriptURLs().map( (url) => `<script src="${url}"></script>` )
|
|
23
|
+
const virtualConsole = new VirtualConsole()
|
|
24
|
+
virtualConsole.sendTo(console);
|
|
25
|
+
return new JSDOM(
|
|
26
|
+
`<!DOCTYPE html>
|
|
27
|
+
<html>
|
|
28
|
+
<head>
|
|
29
|
+
${scripts}
|
|
30
|
+
</head>
|
|
31
|
+
<body>
|
|
32
|
+
${html}
|
|
33
|
+
</body>
|
|
34
|
+
</html>
|
|
35
|
+
`,{
|
|
36
|
+
resources: "usable",
|
|
37
|
+
runScripts: "dangerously",
|
|
38
|
+
includeNodeLocations: true,
|
|
39
|
+
resources: resourceLoader,
|
|
40
|
+
url: url
|
|
41
|
+
}
|
|
42
|
+
)
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
export default DOMCreator
|
package/src/testing/index.js
CHANGED
|
@@ -21,328 +21,28 @@
|
|
|
21
21
|
*
|
|
22
22
|
* @module testing
|
|
23
23
|
*/
|
|
24
|
-
import
|
|
25
|
-
import
|
|
26
|
-
import path from "node:path"
|
|
27
|
-
import assert from "assert"
|
|
28
|
-
|
|
29
|
-
const __dirname = import.meta.dirname
|
|
30
|
-
|
|
31
|
-
const appRoot = path.resolve(__dirname,"..","..","specs")
|
|
32
|
-
const publicRoot = path.resolve(appRoot,"public")
|
|
33
|
-
|
|
34
|
-
class AssetMetadata {
|
|
35
|
-
constructor(parsedJSON, publicRoot) {
|
|
36
|
-
this.assetMetadata = parsedJSON.asset_metadata
|
|
37
|
-
this.publicRoot = publicRoot
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
scriptURLs() {
|
|
41
|
-
return Object.entries(this.assetMetadata[".js"]).map( (entry) => {
|
|
42
|
-
return entry[0]
|
|
43
|
-
})
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
fileContainingScriptURL(scriptURL) {
|
|
47
|
-
const file = Object.entries(this.assetMetadata[".js"]).find( (entry) => {
|
|
48
|
-
return entry[0] == scriptURL
|
|
49
|
-
})
|
|
50
|
-
if (!file || !file[1]) {
|
|
51
|
-
return null
|
|
52
|
-
}
|
|
53
|
-
let relativePath = file[1]
|
|
54
|
-
if (relativePath[0] == "/") {
|
|
55
|
-
relativePath = relativePath.slice(1)
|
|
56
|
-
}
|
|
57
|
-
return fs.readFileSync(path.resolve(this.publicRoot,relativePath))
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
const assetMetadata = new AssetMetadata(
|
|
62
|
-
JSON.parse(fs.readFileSync(path.resolve(appRoot,"config","asset_metadata.json"))),
|
|
63
|
-
publicRoot,
|
|
64
|
-
)
|
|
65
|
-
|
|
66
|
-
class AssetMetadataLoader extends ResourceLoader {
|
|
67
|
-
constructor(assetMetadata) {
|
|
68
|
-
super()
|
|
69
|
-
this.assetMetadata = assetMetadata
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
fetch(url,options) {
|
|
73
|
-
const parsedURL = new URL(url)
|
|
74
|
-
const jsContents = this.assetMetadata.fileContainingScriptURL(parsedURL.pathname)
|
|
75
|
-
if (jsContents) {
|
|
76
|
-
return Promise.resolve(jsContents)
|
|
77
|
-
}
|
|
78
|
-
else {
|
|
79
|
-
return super.fetch(url,options)
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
const resourceLoader = new AssetMetadataLoader(assetMetadata)
|
|
85
|
-
|
|
86
|
-
const createDOM = (html, queryString) => {
|
|
87
|
-
|
|
88
|
-
const url = "http://example.com" + ( queryString ? `?${queryString}` : "" )
|
|
89
|
-
|
|
90
|
-
const scripts = assetMetadata.scriptURLs().map( (url) => `<script src="${url}"></script>` )
|
|
91
|
-
const virtualConsole = new VirtualConsole()
|
|
92
|
-
virtualConsole.sendTo(console);
|
|
93
|
-
return new JSDOM(
|
|
94
|
-
`<!DOCTYPE html>
|
|
95
|
-
<html>
|
|
96
|
-
<head>
|
|
97
|
-
${scripts}
|
|
98
|
-
</head>
|
|
99
|
-
<body>
|
|
100
|
-
${html}
|
|
101
|
-
</body>
|
|
102
|
-
</html>
|
|
103
|
-
`,{
|
|
104
|
-
resources: "usable",
|
|
105
|
-
runScripts: "dangerously",
|
|
106
|
-
includeNodeLocations: true,
|
|
107
|
-
resources: resourceLoader,
|
|
108
|
-
url: url
|
|
109
|
-
}
|
|
110
|
-
)
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
/**
|
|
114
|
-
* Returned by {@link withHTML} as the basis for writing tests for custom elements. You won't create
|
|
115
|
-
* instances of this class. Rather, you'll be given an instance from `withHTML` and likely call
|
|
116
|
-
* `test` on it.
|
|
117
|
-
*
|
|
118
|
-
* @see withHTML
|
|
119
|
-
*/
|
|
120
|
-
class CustomElementTest {
|
|
121
|
-
constructor(html, queryString) {
|
|
122
|
-
this.html = html
|
|
123
|
-
this.queryString = queryString
|
|
124
|
-
this.fetchBehavior = {}
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
/**
|
|
128
|
-
* Configure a query string to be present when the custom elements are defined and connected.
|
|
129
|
-
*/
|
|
130
|
-
andQueryString(queryString) {
|
|
131
|
-
this.queryString = queryString
|
|
132
|
-
return this
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
/**
|
|
136
|
-
* Configure behavior when #{link external:fetch} is called, since it's not implemented by JSDom or NodeJS.
|
|
137
|
-
*
|
|
138
|
-
* @param {String} url - the URL that is expected. This should be a relative URL, as that is all that is currently supported. This
|
|
139
|
-
* URL must match exactly to a `fetch` call.
|
|
140
|
-
* @param {Object} behavior - an object describing what you want to happen when `url` is fetched. This currently supports only a
|
|
141
|
-
* few rudimentary behaviors:
|
|
142
|
-
* * `{then: { ok: { text: "some text" } } }` - This will return an "ok" response whose body is the given text, available only as
|
|
143
|
-
* text.
|
|
144
|
-
* * `{then: { status: XXX, text: "some text" }}` - This will return the given http status, with `ok` as true if it's 2xx or 3xx. If `text` is given, that will be available as text only.
|
|
145
|
-
*/
|
|
146
|
-
onFetch(url,behavior) {
|
|
147
|
-
if (!this.fetchBehavior[url]) {
|
|
148
|
-
this.fetchBehavior[url] = {
|
|
149
|
-
numCalls: 0,
|
|
150
|
-
responses: [],
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
if (behavior instanceof Array) {
|
|
154
|
-
behavior.forEach( (b) => {
|
|
155
|
-
this.fetchBehavior[url].responses.push(b)
|
|
156
|
-
})
|
|
157
|
-
}
|
|
158
|
-
else {
|
|
159
|
-
this.fetchBehavior[url].responses.push(behavior)
|
|
160
|
-
}
|
|
161
|
-
return this
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
/** Comment out a test without using code comments */
|
|
165
|
-
xtest() {
|
|
166
|
-
return this
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
/** Declare a test to run with the previously-defined HTML, query string, and fetch behavior.
|
|
171
|
-
*
|
|
172
|
-
* @param {String} description - a description of the test.
|
|
173
|
-
* @param {testCodeCallback} testCode - a function containing the code for your test.
|
|
174
|
-
*/
|
|
175
|
-
test(description,testCode) {
|
|
176
|
-
it(description, () => {
|
|
177
|
-
const dom = createDOM(this.html,this.queryString)
|
|
178
|
-
const fetchRequests = []
|
|
179
|
-
|
|
180
|
-
dom.window.Request = Request
|
|
181
|
-
dom.window.fetch = (request) => {
|
|
182
|
-
const url = new URL(request.url)
|
|
183
|
-
const path = url.pathname + url.search
|
|
184
|
-
const behaviors = this.fetchBehavior[path]
|
|
185
|
-
if (!behaviors) {
|
|
186
|
-
throw `fetch() called with ${path}, which was not configured`
|
|
187
|
-
}
|
|
188
|
-
if (behaviors.numCalls > behaviors.responses.length) {
|
|
189
|
-
throw `fetch() called ${behaviors.numCalls} times, but we only have ${behaviors.response.length} responses configured`
|
|
190
|
-
}
|
|
191
|
-
const behavior = behaviors.responses[behaviors.numCalls]
|
|
192
|
-
behaviors.numCalls++
|
|
193
|
-
|
|
194
|
-
let promise = null
|
|
195
|
-
|
|
196
|
-
if (behavior.then) {
|
|
197
|
-
if (behavior.then.ok) {
|
|
198
|
-
if (behavior.then.ok.text) {
|
|
199
|
-
const response = {
|
|
200
|
-
ok: true,
|
|
201
|
-
text: () => {
|
|
202
|
-
return Promise.resolve(behavior.then.ok.text)
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
promise = Promise.resolve(response)
|
|
206
|
-
}
|
|
207
|
-
else {
|
|
208
|
-
throw `unknown fetch behavior: expected then.ok.text: ${JSON.stringify(behavior)}`
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
else if (behavior.then.status) {
|
|
212
|
-
let ok = false
|
|
213
|
-
if ((behavior.then.status >= 200) && (behavior.then.status < 400) ){
|
|
214
|
-
ok = true
|
|
215
|
-
}
|
|
216
|
-
const response = {
|
|
217
|
-
ok: ok,
|
|
218
|
-
status: behavior.then.status,
|
|
219
|
-
text: () => {
|
|
220
|
-
return Promise.resolve(behavior.then.text)
|
|
221
|
-
}
|
|
222
|
-
}
|
|
223
|
-
promise = Promise.resolve(response)
|
|
224
|
-
}
|
|
225
|
-
else {
|
|
226
|
-
throw `unknown fetch behavior: expected then.ok or then.status: ${JSON.stringify(behavior)}`
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
|
-
else {
|
|
230
|
-
throw `unknown fetch behavior: expected then: ${JSON.stringify(behavior)}`
|
|
231
|
-
}
|
|
232
|
-
request.promiseReturned = promise
|
|
233
|
-
fetchRequests.push(request)
|
|
234
|
-
return promise
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
const window = dom.window
|
|
238
|
-
const document = window.document
|
|
239
|
-
let returnValue = null
|
|
240
|
-
return new Promise( (resolve, reject) => {
|
|
241
|
-
dom.window.addEventListener("load", () => {
|
|
242
|
-
try {
|
|
243
|
-
returnValue = testCode({window,document,assert,fetchRequests})
|
|
244
|
-
if (returnValue) {
|
|
245
|
-
resolve(returnValue)
|
|
246
|
-
}
|
|
247
|
-
else {
|
|
248
|
-
resolve()
|
|
249
|
-
}
|
|
250
|
-
} catch (e) {
|
|
251
|
-
reject(e)
|
|
252
|
-
}
|
|
253
|
-
})
|
|
254
|
-
})
|
|
255
|
-
})
|
|
256
|
-
return this
|
|
257
|
-
}
|
|
258
|
-
}
|
|
24
|
+
import AssetMetadata from "./AssetMetadata.js"
|
|
25
|
+
import CustomElementTest from "./CustomElementTest.js"
|
|
259
26
|
|
|
260
27
|
/**
|
|
261
|
-
*
|
|
262
|
-
*
|
|
28
|
+
* Bootstraps a test based on some HTML and configuration about where the bundled custom elements are. It's recommended that you
|
|
29
|
+
* create the method `withHTML` in your `SpecHelper.js` file to set up the asset metadata stuff.
|
|
263
30
|
*
|
|
264
31
|
* This returns a {@link module:testing~CustomElementTest}, on which you can call additional setup methods, or start defining tests with the
|
|
265
32
|
* {@link module:testing~CustomElementTest#test} method.
|
|
266
33
|
*
|
|
267
|
-
* @
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
* Given a fetch request (available via `fetchRequests` passed to your test), turn the body into JSON
|
|
273
|
-
* and return a promise with the JSON. To use this in a test, you must include your assertions inside
|
|
274
|
-
* the `then` of the returned promise and you *must* return that from your test.
|
|
275
|
-
*
|
|
276
|
-
* @example
|
|
277
|
-
* withHTML(
|
|
278
|
-
* "<div-makes-a-fetch>"
|
|
279
|
-
* ).onFetch("/foo", { then: { status: 200 }}
|
|
280
|
-
* ).test("a test",({assert,fetchRequests}) => {
|
|
281
|
-
* assert.equal(fetchRequests.length,1)
|
|
282
|
-
* return readRequestBodyIntoJSON(fetchRequests[0]).then( (json) => {
|
|
283
|
-
* assert.equal(json["foo"],"bar")
|
|
284
|
-
* })
|
|
285
|
-
* })
|
|
286
|
-
*/
|
|
287
|
-
const readRequestBodyIntoJSON = (request) => {
|
|
288
|
-
return readRequestBodyIntoString(request).then( (string) => {
|
|
289
|
-
try {
|
|
290
|
-
const json = JSON.parse(string)
|
|
291
|
-
return Promise.resolve(json)
|
|
292
|
-
}
|
|
293
|
-
catch (e) {
|
|
294
|
-
assert(false,`'${string}' could not be parsed as JSON`)
|
|
295
|
-
}
|
|
296
|
-
return Promise.resolve()
|
|
297
|
-
})
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
/**
|
|
301
|
-
* Given a fetch request (available via `fetchRequests` passed to your test), turn the body into a string
|
|
302
|
-
* and return a promise with the string. To use this in a test, you must include your assertions inside
|
|
303
|
-
* the `then` of the returned promise and you *must* return that from your test.
|
|
34
|
+
* @param {String} html - HTML that should be in the document for the test
|
|
35
|
+
* @param {Object} assetMetadata - a JSON object describing where the bundles are. This is needed to allow JSDOM to load the custom
|
|
36
|
+
* elements as if it were served up by a webserver
|
|
37
|
+
* @param {String} publicRoot - The root to where JS files. When using this in a BrutRB web app, this would be where your bundled
|
|
38
|
+
* files are placed for serving.
|
|
304
39
|
*
|
|
305
|
-
* @
|
|
306
|
-
* withHTML(
|
|
307
|
-
* "<div-makes-a-fetch>"
|
|
308
|
-
* ).onFetch("/foo", { then: { status: 200 }}
|
|
309
|
-
* ).test("a test",({assert,fetchRequests}) => {
|
|
310
|
-
* assert.equal(fetchRequests.length,1)
|
|
311
|
-
* return readRequestBodyIntoString(fetchRequests[0]).then( (string) => {
|
|
312
|
-
* assert.equal(string,"foo")
|
|
313
|
-
* })
|
|
314
|
-
* })
|
|
315
|
-
*/
|
|
316
|
-
const readRequestBodyIntoString = (request) => {
|
|
317
|
-
const reader = request.body.getReader()
|
|
318
|
-
const utf8Decoder = new TextDecoder("utf-8");
|
|
319
|
-
let string = ""
|
|
320
|
-
const parse = ({value,done}) => {
|
|
321
|
-
if (done) {
|
|
322
|
-
return Promise.resolve(string)
|
|
323
|
-
}
|
|
324
|
-
else {
|
|
325
|
-
if (value) {
|
|
326
|
-
string = string + utf8Decoder.decode(value, { stream: true })
|
|
327
|
-
}
|
|
328
|
-
return reader.read().then(parse)
|
|
329
|
-
}
|
|
330
|
-
}
|
|
331
|
-
return reader.read().then(parse)
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
/** Used to wait for a few milliseconds before performing further assertions.
|
|
335
|
-
* This can be useful when a custom element has `setTimeout` calls
|
|
40
|
+
* @see module:testing~CustomElementTest
|
|
336
41
|
*/
|
|
337
|
-
const
|
|
338
|
-
return new
|
|
339
|
-
setTimeout(resolve,ms)
|
|
340
|
-
})
|
|
42
|
+
const createTestBasedOnHTML = ({html,assetMetadata,publicRoot}) => {
|
|
43
|
+
return new CustomElementTest(html,null,new AssetMetadata(assetMetadata,publicRoot))
|
|
341
44
|
}
|
|
342
45
|
|
|
343
46
|
export {
|
|
344
|
-
|
|
345
|
-
readRequestBodyIntoJSON,
|
|
346
|
-
readRequestBodyIntoString,
|
|
347
|
-
waitForSetTimeout,
|
|
47
|
+
createTestBasedOnHTML
|
|
348
48
|
}
|