solarite 0.2.1 → 0.2.2
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 +6 -5
- package/benchmarks/naive/Solarite.min.js +0 -4
- package/benchmarks/naive/index.html +0 -14
- package/benchmarks/naive/main.js +0 -339
- package/benchmarks/naive/package-lock.json +0 -13
- package/benchmarks/naive/package.json +0 -23
- package/benchmarks/readme.md +0 -48
- package/build/build.bat +0 -4
- package/build/build.js +0 -139
- package/build/lib/rollup.min.js +0 -11
- package/build/lib/source-map.min.js +0 -1
- package/build/lib/terser.min.js +0 -1
- package/docs/index.md +0 -883
- package/docs/js/Playground.js +0 -184
- package/docs/js/codemirror/codemirror6.js +0 -32513
- package/docs/js/codemirror/themeSolarIce.js +0 -312
- package/docs/js/documentation.js +0 -32
- package/docs/js/ui/CodeEditor.js +0 -978
- package/docs/js/ui/DarkToggle.js +0 -52
- package/docs/js/ui/FlexResizer.js +0 -152
- package/docs/js/util/Draggable2.js +0 -151
- package/docs/js/util/Errors.js +0 -20
- package/docs/js/util/Html.js +0 -147
- package/docs/js/util/Icons.js +0 -623
- package/docs/js/util/Input.js +0 -253
- package/docs/js/util/Util.js +0 -88
- package/docs/js/util/delve.js +0 -43
- package/docs/media/FiraCode400.woff2 +0 -0
- package/docs/media/cabin-latin-700.woff2 +0 -0
- package/docs/media/documentation.css +0 -96
- package/docs/media/eternium.css +0 -1123
- package/docs/media/solarite-machine.webp +0 -0
- package/index.html +0 -761
- package/tests/Benchmark.test.js +0 -319
- package/tests/Solarite.test.js +0 -3838
- package/tests/Testimony.js +0 -809
- package/tests/index.html +0 -46
- package/tests/run.bat +0 -2
package/tests/Testimony.js
DELETED
|
@@ -1,809 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Provide functionality for running Deno tests in a web browser.
|
|
3
|
-
* Has no external dependencies.
|
|
4
|
-
*
|
|
5
|
-
* TODO:
|
|
6
|
-
* Make a Test interface and a Test web component.
|
|
7
|
-
* That way we can render and run the same test object separately.
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* 4. Integrate with IntelliJ file watcher so we run cmd line tests when files change.
|
|
11
|
-
* 5. Run tests from @expect doc tags.
|
|
12
|
-
* 6. Documentation - Web tests, deno tests, intellij integration
|
|
13
|
-
* 7. Add to github.
|
|
14
|
-
* 8. Command line via node
|
|
15
|
-
* 9. Support other Deno options.
|
|
16
|
-
* 11. URLs only mark which tests to include or exclude, to make url shorter
|
|
17
|
-
* 12. Auto-expand to failed tests.
|
|
18
|
-
*/
|
|
19
|
-
|
|
20
|
-
class AssertError extends Error {
|
|
21
|
-
constructor(expected, actual, op) {
|
|
22
|
-
super('Assertion Failed');
|
|
23
|
-
this.name = "AssertError";
|
|
24
|
-
this.expected = expected;
|
|
25
|
-
this.actual = actual;
|
|
26
|
-
this.op = op;
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
function assert(val) {
|
|
31
|
-
if (!val) {
|
|
32
|
-
if (Testimony.debugOnAssertFail)
|
|
33
|
-
debugger;
|
|
34
|
-
throw new AssertError(val, true);
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
Object.assign(assert, {
|
|
39
|
-
eq(expected, actual) {
|
|
40
|
-
if (!isSame(expected, actual)) { // JUnit, PhpUnit, and mocha all use the order: expected, actual.
|
|
41
|
-
if (Testimony.debugOnAssertFail)
|
|
42
|
-
debugger;
|
|
43
|
-
throw new AssertError(expected, actual, '==');
|
|
44
|
-
}
|
|
45
|
-
},
|
|
46
|
-
|
|
47
|
-
eqJson(expected, actual) {
|
|
48
|
-
if (JSON.stringify(actual) !== JSON.stringify(expected)) {
|
|
49
|
-
if (Testimony.debugOnAssertFail)
|
|
50
|
-
debugger;
|
|
51
|
-
throw new AssertError(expected, actual);
|
|
52
|
-
}
|
|
53
|
-
},
|
|
54
|
-
|
|
55
|
-
neq(val1, val2) {
|
|
56
|
-
if (val1 !== val2) {
|
|
57
|
-
if (Testimony.debugOnAssertFail)
|
|
58
|
-
debugger;
|
|
59
|
-
throw new AssertError(val1 + ' === ' + val2);
|
|
60
|
-
}
|
|
61
|
-
},
|
|
62
|
-
|
|
63
|
-
lte(val1, val2) {
|
|
64
|
-
if (val1 > val2) {
|
|
65
|
-
if (Testimony.debugOnAssertFail)
|
|
66
|
-
debugger;
|
|
67
|
-
throw new AssertError(val1 + ' > ' + val2);
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
/**
|
|
75
|
-
* https://stackoverflow.com/a/6713782/
|
|
76
|
-
* @param x
|
|
77
|
-
* @param y
|
|
78
|
-
* @return {boolean} */
|
|
79
|
-
function isSame( x, y ) {
|
|
80
|
-
if (x === y)
|
|
81
|
-
return true; // if both x and y are null or undefined and exactly the same
|
|
82
|
-
|
|
83
|
-
// if they are not strictly equal, they both need to be Objects
|
|
84
|
-
// they must have the exact same prototype chain, the closest we can do is
|
|
85
|
-
// test their constructor.
|
|
86
|
-
if (!(x instanceof Object) || !(y instanceof Object) || x.constructor !== y.constructor)
|
|
87
|
-
return false;
|
|
88
|
-
|
|
89
|
-
for (var p in x) {
|
|
90
|
-
if (!x.hasOwnProperty(p))
|
|
91
|
-
continue; // other properties were tested using x.constructor === y.constructor
|
|
92
|
-
|
|
93
|
-
if (!y.hasOwnProperty(p))
|
|
94
|
-
return false; // allows to compare x[ p ] and y[ p ] when set to undefined
|
|
95
|
-
|
|
96
|
-
if (x[p] === y[p])
|
|
97
|
-
continue; // if they have the same strict value or identity then they are equal
|
|
98
|
-
|
|
99
|
-
if (typeof x[p] !== "object" || !isSame(x[p], y[p])) // Numbers, Strings, Functions, Booleans must be strictly equal
|
|
100
|
-
return false; // Objects and Arrays must be tested recursively
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
for (p in y) // allows x[ p ] to be set to undefined
|
|
104
|
-
if (y.hasOwnProperty(p) && !x.hasOwnProperty(p))
|
|
105
|
-
return false;
|
|
106
|
-
|
|
107
|
-
return true;
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
let styleId = 1;
|
|
111
|
-
|
|
112
|
-
/**
|
|
113
|
-
* Create a single html element, node, or comment from the html string.
|
|
114
|
-
* The string will be trimmed so that an element with space before it doesn't create a text node with spaces.
|
|
115
|
-
* TODO: Allow specifying an existing div as props to bind a new set of html to it.
|
|
116
|
-
* This could be used for manually adding children.
|
|
117
|
-
* TODO: Get values and setValues() functions for form fields? Can make it work with any html element.
|
|
118
|
-
* TODO: Bind this to render() so I can render ${this.someField}
|
|
119
|
-
* TODO: Make properties, ids, events, and scoped styles only happen if a props argument is passed in, or is not false?
|
|
120
|
-
*
|
|
121
|
-
* I could use name="items[].name" to specify json structure of the result?
|
|
122
|
-
* OR:
|
|
123
|
-
*
|
|
124
|
-
* <div name="item[]">
|
|
125
|
-
* <input name="description">
|
|
126
|
-
* </div>
|
|
127
|
-
*
|
|
128
|
-
* Will give me item[0].description
|
|
129
|
-
*
|
|
130
|
-
*
|
|
131
|
-
* @param html {string|function} Must be a function that returns html if you want to call render() again later.
|
|
132
|
-
* @param doc {Document}
|
|
133
|
-
* @param props {Object|boolean} */
|
|
134
|
-
function createEl(html, props=false, doc=document) {
|
|
135
|
-
let template = doc.createElement('template');
|
|
136
|
-
let result = doc.createElement((html+'').match(/<(\w+[\w-]*)/)[1]);
|
|
137
|
-
let ids; // Track them so they can be removed later.
|
|
138
|
-
|
|
139
|
-
// Properties
|
|
140
|
-
if (typeof props === 'object')
|
|
141
|
-
for (let name in props) {
|
|
142
|
-
if (name in result)
|
|
143
|
-
throw new Error(`Property ${name} already exists.`);
|
|
144
|
-
result[name] = props[name];
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
// Render
|
|
148
|
-
result.render = () => {
|
|
149
|
-
template.innerHTML = (typeof html === 'function' ? html() : html).trim();
|
|
150
|
-
|
|
151
|
-
// Attributes and Children
|
|
152
|
-
if (props) {
|
|
153
|
-
result.innerHTML = '';
|
|
154
|
-
[...result.attributes].map(attr => result.removeAttribute(attr.name));
|
|
155
|
-
}
|
|
156
|
-
[...template.content.firstElementChild.attributes].map(attr => result.setAttribute(attr.name, attr.value));
|
|
157
|
-
[...template.content.firstElementChild.childNodes].map(child => result.append(child));
|
|
158
|
-
|
|
159
|
-
if (props) {
|
|
160
|
-
// Assign ids
|
|
161
|
-
(ids || []).map(id => delete result[id]);
|
|
162
|
-
ids = [...result.querySelectorAll('[id],[data-id]')].map(el => {
|
|
163
|
-
if (el.dataset.id in result && !(el.dataset.id in props)) // allow id's to override our custom props.
|
|
164
|
-
throw new Error(`Property ${el.dataset.id} already exists.`);
|
|
165
|
-
result[el.dataset.id] = el;
|
|
166
|
-
return [el.dataset.id];
|
|
167
|
-
});
|
|
168
|
-
|
|
169
|
-
// Bind events
|
|
170
|
-
[result, ...result.querySelectorAll('*')].map(el =>
|
|
171
|
-
[...el.attributes].filter(attr => attr.name.startsWith('on')).map(attr => {
|
|
172
|
-
el[attr.name] = e => // e.g. el.onclick = ...
|
|
173
|
-
(new Function('event', 'el', attr.value)).bind(result)(e, el) // put "event", "el", and "this" in scope for the event code.
|
|
174
|
-
})
|
|
175
|
-
);
|
|
176
|
-
|
|
177
|
-
// Scoped Styles
|
|
178
|
-
let styles = result.querySelectorAll('style');
|
|
179
|
-
if (styles.length) {
|
|
180
|
-
result.setAttribute('data-style-scope', styleId++); // TODO: re-use style id on re-render.
|
|
181
|
-
[...styles].map(style => style.textContent = style.textContent.replace(/:host(?=[^a-z\d_])/gi,
|
|
182
|
-
'[data-style-scope="' + result.getAttribute('data-style-scope') + '"]'));
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
if (typeof result.init === 'function')
|
|
188
|
-
result.init();
|
|
189
|
-
if (!ids) // if not called by init()
|
|
190
|
-
result.render();
|
|
191
|
-
|
|
192
|
-
return result;
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
// Html.encode()
|
|
196
|
-
function h(text, quotes='"') {
|
|
197
|
-
text = (text === null || text === undefined ? '' : text+'')
|
|
198
|
-
.replace(/&/g, '&')
|
|
199
|
-
.replace(/</g, '<')
|
|
200
|
-
.replace(/>/g, '>')
|
|
201
|
-
.replace(/\a0/g, ' ')
|
|
202
|
-
if (quotes.includes("'"))
|
|
203
|
-
text = text.replace(/'/g, ''');
|
|
204
|
-
if (quotes.includes('"'))
|
|
205
|
-
text = text.replace(/"/g, '"');
|
|
206
|
-
return text;
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
var HtmlRenderer = {
|
|
210
|
-
|
|
211
|
-
/**
|
|
212
|
-
* @param test {Test}
|
|
213
|
-
* @return {HTMLDivElement} */
|
|
214
|
-
render(test) {
|
|
215
|
-
|
|
216
|
-
// If updating
|
|
217
|
-
if (test.el) {
|
|
218
|
-
test.el.render();
|
|
219
|
-
for (let t2 of Object.values(test.children || {}))
|
|
220
|
-
test.el.childTests.append(HtmlRenderer.render(t2));
|
|
221
|
-
return test.el;
|
|
222
|
-
}
|
|
223
|
-
else {
|
|
224
|
-
|
|
225
|
-
let result = createEl(() => `
|
|
226
|
-
<div class="test">
|
|
227
|
-
<style>
|
|
228
|
-
:host input[type=checkbox] { width: 7.7px; appearance: none; margin: 0; color: inherit }
|
|
229
|
-
:host [data-id=expandCB]:after { content: '+'; user-select: none; cursor: pointer }
|
|
230
|
-
:host [data-id=expandCB]:checked:after { content: '-' }
|
|
231
|
-
:host [data-id=enableCB]:after { content: '\xa0'; color: #55f; font-weight: bold; text-shadow: 1px 0 0 #55f }
|
|
232
|
-
:host [data-id=enableCB]:checked:after { content: 'x' }
|
|
233
|
-
:host [data-id=status] { line-height: 1; display: inline-block; width: 7.7px }
|
|
234
|
-
:host [data-id=childTests] { padding-left: ${test.name.length ? '38.5' : '15.4'}px }
|
|
235
|
-
</style>
|
|
236
|
-
<div>
|
|
237
|
-
${test.name.length ?
|
|
238
|
-
Object.values(test.children || {}).length
|
|
239
|
-
? `<input data-id="expandCB" type="checkbox" name="x" ${test.expanded ? 'checked' : ''}
|
|
240
|
-
value="${h(test.name) || ''}" onchange="this.expandClick()">`
|
|
241
|
-
: ` `
|
|
242
|
-
: ''
|
|
243
|
-
}
|
|
244
|
-
<label>
|
|
245
|
-
[<input data-id="enableCB" type="checkbox" name="r" value="${h(test.name) || ''}"
|
|
246
|
-
${(test.getShortName()[0] === '_') ? 'data-disabled' : ''}
|
|
247
|
-
${test.enabled ? 'checked' : ''}
|
|
248
|
-
onchange="this.enableClick()">]
|
|
249
|
-
<span data-id="status">${
|
|
250
|
-
test.status === true ? `<span style="color: #0f0">✓</span>` :
|
|
251
|
-
test.status === null ? ` ` :
|
|
252
|
-
`<span style="color: red">✗</span>`
|
|
253
|
-
}</span>
|
|
254
|
-
${h(test.getShortName())}
|
|
255
|
-
</label>
|
|
256
|
-
<span style="opacity: .5">${h(test.desc) || ''}</span>
|
|
257
|
-
<div style="color: red; padding-left: 61.6px">${test.status instanceof Error ? Testimony.shortenError(test.status) : ''}</div>
|
|
258
|
-
</div>
|
|
259
|
-
<div data-id="childTests" ${test.expanded ? '' : 'style="display: none"'}></div>
|
|
260
|
-
</div>`, {
|
|
261
|
-
|
|
262
|
-
test,
|
|
263
|
-
|
|
264
|
-
init() {
|
|
265
|
-
test.el = this;
|
|
266
|
-
},
|
|
267
|
-
|
|
268
|
-
enableClick() {
|
|
269
|
-
// Check all children if this is checked.
|
|
270
|
-
[...this.childTests.querySelectorAll('[name=r]:not([data-disabled])')].map(cb => cb.checked = this.enableCB.checked);
|
|
271
|
-
|
|
272
|
-
// Make every parent checked if all its children are checked.
|
|
273
|
-
let p = this;
|
|
274
|
-
while (p = p.parentNode)
|
|
275
|
-
if (p.nodeType === 1 && p.matches('.test'))
|
|
276
|
-
p.querySelector('[name=r]').checked = ![...p.childTests.querySelectorAll('[name=r]:not([data-disabled])')].find(cb => !cb.checked);
|
|
277
|
-
},
|
|
278
|
-
|
|
279
|
-
expandClick() {
|
|
280
|
-
this.childTests.style.display = this.expandCB.checked ? '': 'none';
|
|
281
|
-
}
|
|
282
|
-
});
|
|
283
|
-
|
|
284
|
-
// Recursively add child tests
|
|
285
|
-
if (test.children)
|
|
286
|
-
for (let test2 of Object.values(test.children))
|
|
287
|
-
result.childTests.append(HtmlRenderer.render(test2));
|
|
288
|
-
|
|
289
|
-
return result;
|
|
290
|
-
}
|
|
291
|
-
}
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
// Not used.
|
|
295
|
-
var TextRenderer = {
|
|
296
|
-
|
|
297
|
-
render(test) {
|
|
298
|
-
let result = [];
|
|
299
|
-
|
|
300
|
-
// If not the root node:
|
|
301
|
-
if (test.name) {
|
|
302
|
-
|
|
303
|
-
// Color codes: https://stackoverflow.com/a/41407246
|
|
304
|
-
let status = ' ';
|
|
305
|
-
if (test.status === null)
|
|
306
|
-
status = ' ';
|
|
307
|
-
else if (test.status === true)
|
|
308
|
-
status = '\x1b[1;32m' + '✓' + '\x1b[0m'; // green, 1; for bold
|
|
309
|
-
else
|
|
310
|
-
status = '\x1b[1;31m' + '✗' + '\x1b[0m'; // red
|
|
311
|
-
|
|
312
|
-
let result2 = ' '.repeat(test.getDepth()) + `[${status}] ${test.getShortName() || ''}`;
|
|
313
|
-
if (test.desc)
|
|
314
|
-
result2 += ` \x1b[90m${test.desc}\x1b[0m`; // gray
|
|
315
|
-
if (test.status instanceof Error)
|
|
316
|
-
result2 += ' \x1b[31m' + Testimony.shortenError(test.status, '\n ') + '\x1b[0m';
|
|
317
|
-
|
|
318
|
-
result.push(result2);
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
// Recurse through children.
|
|
322
|
-
if (test.children)
|
|
323
|
-
for (let test2 of Object.values(test.children))
|
|
324
|
-
result.push(TextRenderer.render(test2));
|
|
325
|
-
|
|
326
|
-
return result.join('\n');
|
|
327
|
-
}
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
class Test {
|
|
332
|
-
name;
|
|
333
|
-
desc;
|
|
334
|
-
expanded;
|
|
335
|
-
enabled;
|
|
336
|
-
|
|
337
|
-
/**
|
|
338
|
-
* @type {boolean|Error|null}
|
|
339
|
-
* true: Test passed or all non-disabled child tests passed
|
|
340
|
-
* false: One or more child tests failed.
|
|
341
|
-
* Error: Test failed.
|
|
342
|
-
* null: Hasn't been run yet. */
|
|
343
|
-
status = null;
|
|
344
|
-
/**
|
|
345
|
-
* Every test will have either a fn OR children.
|
|
346
|
-
* @type {?function} */
|
|
347
|
-
fn = null;
|
|
348
|
-
|
|
349
|
-
/**
|
|
350
|
-
* @type {?Object<name:string, Test>} */
|
|
351
|
-
children = null;
|
|
352
|
-
|
|
353
|
-
constructor(name='', desc='', fn) {
|
|
354
|
-
this.name = name;
|
|
355
|
-
this.desc = desc;
|
|
356
|
-
this.fn = fn;
|
|
357
|
-
|
|
358
|
-
if (window.location) {
|
|
359
|
-
let url = new URL(window.location);
|
|
360
|
-
this.expanded = url.searchParams.getAll('x').includes(name);
|
|
361
|
-
|
|
362
|
-
// Enabled if this or a parent is checked
|
|
363
|
-
if (this.enabled === undefined && !this.getShortName().startsWith('_')) { // if not otherwise set, set it from url:
|
|
364
|
-
if (url.searchParams.has('allTests'))
|
|
365
|
-
this.enabled = true;
|
|
366
|
-
|
|
367
|
-
else {
|
|
368
|
-
|
|
369
|
-
let r = url.searchParams.getAll('r');
|
|
370
|
-
let parent = name;
|
|
371
|
-
do {
|
|
372
|
-
|
|
373
|
-
// Enable test if a parent is enabled.
|
|
374
|
-
if (r.includes(parent)) {
|
|
375
|
-
this.enabled = true;
|
|
376
|
-
break;
|
|
377
|
-
}
|
|
378
|
-
parent = parent.split('.').slice(0, -1).join('.')
|
|
379
|
-
} while (parent);
|
|
380
|
-
}
|
|
381
|
-
}
|
|
382
|
-
}
|
|
383
|
-
else // TODO: Get enabled tests from the command line enable arguments.
|
|
384
|
-
this.enabled = true;
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
/**
|
|
388
|
-
* Run this test or its children. */
|
|
389
|
-
async run() {
|
|
390
|
-
|
|
391
|
-
// A test to run.
|
|
392
|
-
if (this.fn && this.enabled) {
|
|
393
|
-
let result = true;
|
|
394
|
-
if (Testimony.throwOnError) {
|
|
395
|
-
result = this.fn();
|
|
396
|
-
if (result instanceof Promise)
|
|
397
|
-
result = await result;
|
|
398
|
-
if (result !== false)
|
|
399
|
-
this.status = true;
|
|
400
|
-
} else {
|
|
401
|
-
try {
|
|
402
|
-
result = await this.fn();
|
|
403
|
-
if (result instanceof Promise)
|
|
404
|
-
await result;
|
|
405
|
-
if (result !== false)
|
|
406
|
-
this.status = true;
|
|
407
|
-
} catch (e) {
|
|
408
|
-
console.log(e)
|
|
409
|
-
Testimony.failedTests.push([this.name, Testimony.shortenError(e, '\n')]);
|
|
410
|
-
this.status = e;
|
|
411
|
-
}
|
|
412
|
-
}
|
|
413
|
-
}
|
|
414
|
-
|
|
415
|
-
// A node containing other tests.
|
|
416
|
-
if (!this.fn) {
|
|
417
|
-
// TODO: Run in parallel?
|
|
418
|
-
this.status = true;
|
|
419
|
-
let hasPassingChild = false;
|
|
420
|
-
for (let child of Object.values(this.children)) {
|
|
421
|
-
await child.run();
|
|
422
|
-
if (child.enabled) {
|
|
423
|
-
if (child.status === false || child.status instanceof Error)
|
|
424
|
-
this.status = false;
|
|
425
|
-
|
|
426
|
-
// else if (child.status === null && this.status !== false)
|
|
427
|
-
// this.status = null;
|
|
428
|
-
|
|
429
|
-
else if (child.status === true)
|
|
430
|
-
hasPassingChild = true;
|
|
431
|
-
}
|
|
432
|
-
}
|
|
433
|
-
|
|
434
|
-
// Must have at least one child green checkmark to have a green checkmark.
|
|
435
|
-
if (this.status === true && !hasPassingChild)
|
|
436
|
-
this.status = null;
|
|
437
|
-
}
|
|
438
|
-
|
|
439
|
-
return this.status;
|
|
440
|
-
}
|
|
441
|
-
|
|
442
|
-
/**
|
|
443
|
-
* Set the enabled status of this test and its children, checking their checkbox.
|
|
444
|
-
* @param tests {string[]} Names of tests.
|
|
445
|
-
* @param enabled {boolean} */
|
|
446
|
-
setEnabled(tests, enabled) {
|
|
447
|
-
if (!tests || tests.includes(this.name))
|
|
448
|
-
this.enabled = enabled;
|
|
449
|
-
|
|
450
|
-
for (let childName in this.children || {}) {
|
|
451
|
-
let child = this.children[childName];
|
|
452
|
-
if (!childName.startsWith('_'))
|
|
453
|
-
child.setEnabled(tests, enabled);
|
|
454
|
-
}
|
|
455
|
-
}
|
|
456
|
-
|
|
457
|
-
/**
|
|
458
|
-
* The top level test returns a depth of 0.
|
|
459
|
-
* @returns {int} */
|
|
460
|
-
getDepth() {
|
|
461
|
-
return ((this.name || '').match(/\./g) || []).length;
|
|
462
|
-
}
|
|
463
|
-
|
|
464
|
-
/**
|
|
465
|
-
* Get the name after the last dot.
|
|
466
|
-
* @returns {string} */
|
|
467
|
-
getShortName() {
|
|
468
|
-
return /[^.]*$/.exec(this.name)[0];
|
|
469
|
-
}
|
|
470
|
-
}
|
|
471
|
-
|
|
472
|
-
var Testimony = {
|
|
473
|
-
|
|
474
|
-
debugOnAssertFail: false,
|
|
475
|
-
throwOnError: false, // throw from original location on assert fail or error.
|
|
476
|
-
expandLevel: 1,
|
|
477
|
-
|
|
478
|
-
/** @type {Test} */
|
|
479
|
-
rootTest: new Test(),
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
finished: false,
|
|
485
|
-
|
|
486
|
-
/**
|
|
487
|
-
* A map from the test name to the error.
|
|
488
|
-
* @type {[string, string][]} */
|
|
489
|
-
failedTests: [],
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
/**
|
|
494
|
-
*
|
|
495
|
-
* @param tests {?string[]} Test names. E.g. ['Main.one', 'Main.two']. If null, apply to all tests that are not disabled.
|
|
496
|
-
* @param enabled {boolean} */
|
|
497
|
-
setTestsEnabled(tests, enabled) {
|
|
498
|
-
this.rootTest.setEnabled(tests, enabled);
|
|
499
|
-
},
|
|
500
|
-
|
|
501
|
-
/**
|
|
502
|
-
* Run the root test and any of the root tests children.
|
|
503
|
-
* TODO: Separate rendering from running.
|
|
504
|
-
* @param parent {?HTMLElement}
|
|
505
|
-
* @returns {Promise<[string, Error][]>}
|
|
506
|
-
*/
|
|
507
|
-
async run(parent) {
|
|
508
|
-
this.failedTests = []; // resets
|
|
509
|
-
let renderer = parent ? HtmlRenderer : TextRenderer;
|
|
510
|
-
|
|
511
|
-
if (parent) {
|
|
512
|
-
// Expand recursively
|
|
513
|
-
function doExpand(test, expand) {
|
|
514
|
-
if (expand) {
|
|
515
|
-
test.expanded = true;
|
|
516
|
-
for (let child of Object.values(test.children || {}))
|
|
517
|
-
doExpand(child, expand - 1);
|
|
518
|
-
}
|
|
519
|
-
}
|
|
520
|
-
|
|
521
|
-
let hasXParam = new URL(location).searchParams.getAll('x').length;
|
|
522
|
-
doExpand(Testimony.rootTest, hasXParam ? 1 : this.expandLevel);
|
|
523
|
-
|
|
524
|
-
// Render empty tests
|
|
525
|
-
parent.append(renderer.render(Testimony.rootTest));
|
|
526
|
-
await new Promise(r => setTimeout(r, 1)); // allow browser to render.
|
|
527
|
-
|
|
528
|
-
// Run tests
|
|
529
|
-
await Testimony.rootTest.run();
|
|
530
|
-
|
|
531
|
-
// Update the status.
|
|
532
|
-
renderer.render(Testimony.rootTest);
|
|
533
|
-
}
|
|
534
|
-
|
|
535
|
-
// Command line
|
|
536
|
-
else {
|
|
537
|
-
await Testimony.rootTest.run();
|
|
538
|
-
console.log(renderer.render(Testimony.rootTest));
|
|
539
|
-
}
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
this.finished = true;
|
|
543
|
-
return this.failedTests;
|
|
544
|
-
},
|
|
545
|
-
|
|
546
|
-
/**
|
|
547
|
-
* Requires Deno and a regular Chrome installation.
|
|
548
|
-
* @param page {string}
|
|
549
|
-
* @param webRoot {?string}
|
|
550
|
-
* @param tests {?string[]}
|
|
551
|
-
* @param headless {boolean}
|
|
552
|
-
* @param port {int} Defaults to 8004 to not conflict with commonly used development ports like 8000 or 8080.
|
|
553
|
-
* @returns {Promise<void>} */
|
|
554
|
-
async runPage(page, webRoot=null, tests=null, headless=false, port=8004) {
|
|
555
|
-
|
|
556
|
-
/*
|
|
557
|
-
import puppeteer from 'https://esm.sh/puppeteer@13.0.0';
|
|
558
|
-
import { serve } from 'https://deno.land/std/http/server.ts';
|
|
559
|
-
import { serveFile } from 'https://deno.land/std@0.102.0/http/file_server.ts';
|
|
560
|
-
import { Launcher } from 'https://esm.sh/chrome-launcher@0.15.0';
|
|
561
|
-
*/
|
|
562
|
-
|
|
563
|
-
// Dynamically import so we only pull them in if necessary.
|
|
564
|
-
const [
|
|
565
|
-
{default: puppeteer},
|
|
566
|
-
{Launcher},
|
|
567
|
-
{serve},
|
|
568
|
-
{serveFile},
|
|
569
|
-
] = await Promise.all([
|
|
570
|
-
import('https://deno.land/x/puppeteer@16.2.0/mod.ts'),
|
|
571
|
-
import('https://esm.sh/chrome-launcher@0.15.0'),
|
|
572
|
-
import('https://deno.land/std@0.102.0/http/server.ts'),
|
|
573
|
-
import('https://deno.land/std@0.102.0/http/file_server.ts')
|
|
574
|
-
]);
|
|
575
|
-
|
|
576
|
-
const startServer = () => {
|
|
577
|
-
|
|
578
|
-
const absWebRoot = Deno.realPathSync(webRoot);
|
|
579
|
-
const server = serve({port: 8004});
|
|
580
|
-
//console.log("HTTP web server running. Access it at: http://localhost:8000/");
|
|
581
|
-
|
|
582
|
-
(async () => {
|
|
583
|
-
for await (const request of server) {
|
|
584
|
-
const url = new URL(request.url, `http://${request.headers.get("host")}`);
|
|
585
|
-
const filepath = `${absWebRoot}${url.pathname}`;
|
|
586
|
-
//console.log(filepath)
|
|
587
|
-
try {
|
|
588
|
-
const content = await serveFile(request, filepath);
|
|
589
|
-
request.respond(content);
|
|
590
|
-
} catch {
|
|
591
|
-
request.respond({status: 404, body: "File not found"});
|
|
592
|
-
}
|
|
593
|
-
}
|
|
594
|
-
})();
|
|
595
|
-
|
|
596
|
-
return server;
|
|
597
|
-
};
|
|
598
|
-
|
|
599
|
-
const stopServer = (server) => {
|
|
600
|
-
server.close();
|
|
601
|
-
};
|
|
602
|
-
|
|
603
|
-
const server = startServer();
|
|
604
|
-
|
|
605
|
-
const installations = await Launcher.getInstallations();
|
|
606
|
-
if (installations.length === 0)
|
|
607
|
-
throw new Error("No Chrome installations found.");
|
|
608
|
-
|
|
609
|
-
const executablePath = installations[0]; // Use the first found installation
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
const browser = await puppeteer.launch({headless, executablePath});
|
|
613
|
-
const browserPage = await browser.newPage();
|
|
614
|
-
let args = [];
|
|
615
|
-
|
|
616
|
-
if (!tests)
|
|
617
|
-
args.push('allTests');
|
|
618
|
-
else
|
|
619
|
-
for (let test of tests)
|
|
620
|
-
args.push(`&r=${test}`);
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
const url = `http://localhost:${port}/${page}?${args.join('&')}`;
|
|
624
|
-
//console.log(url)
|
|
625
|
-
await browserPage.goto(url);
|
|
626
|
-
|
|
627
|
-
// Wait for the tests to finish
|
|
628
|
-
await browserPage.waitForFunction(() => window.Testimony?.finished === true);
|
|
629
|
-
|
|
630
|
-
const failedTests = await browserPage.evaluate(() => window.Testimony?.failedTests);
|
|
631
|
-
this.printTestResult(failedTests);
|
|
632
|
-
|
|
633
|
-
await browser.close();
|
|
634
|
-
stopServer(server);
|
|
635
|
-
|
|
636
|
-
Deno.exit(failedTests.length ? 1 : 0);
|
|
637
|
-
},
|
|
638
|
-
|
|
639
|
-
printTestResult(failedTests) {
|
|
640
|
-
if (!failedTests.length)
|
|
641
|
-
console.log(`%cAll tests passed.`, 'color: #0c0');
|
|
642
|
-
else {
|
|
643
|
-
console.log(`These tests failed:`);
|
|
644
|
-
for (const [testName, testError] of failedTests) {
|
|
645
|
-
console.error(`${testName} - %c${testError}`, 'color: red');
|
|
646
|
-
}
|
|
647
|
-
}
|
|
648
|
-
},
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
/**
|
|
652
|
-
* Add a test.
|
|
653
|
-
*
|
|
654
|
-
* Arguments can be given in any order, except that name must occur before desc.
|
|
655
|
-
* @param name {string}
|
|
656
|
-
* @param desc {string|function()=}
|
|
657
|
-
* @param html {string|function()=}
|
|
658
|
-
* @param func {function()=} */
|
|
659
|
-
test(name, desc, html=null, func) {
|
|
660
|
-
let name2, desc2='', html2, func2;
|
|
661
|
-
for (let arg of arguments) {
|
|
662
|
-
if (typeof arg === 'function')
|
|
663
|
-
func2 = arg;
|
|
664
|
-
else if ((arg+'').trim().match(/^<[!a-z]/i)) // an open tag.
|
|
665
|
-
html2 = arg;
|
|
666
|
-
else if (!name2)
|
|
667
|
-
name2 = arg;
|
|
668
|
-
else
|
|
669
|
-
desc2 = arg || '';
|
|
670
|
-
}
|
|
671
|
-
|
|
672
|
-
// update func to create and destroy html before and after test.
|
|
673
|
-
if (html2) {
|
|
674
|
-
let oldFunc = func2;
|
|
675
|
-
|
|
676
|
-
// As an iframe.
|
|
677
|
-
if (html2.startsWith('<html') || html2.startsWith('<!')) {
|
|
678
|
-
func2 = async () => {
|
|
679
|
-
var iframe = document.createElement('iframe');
|
|
680
|
-
iframe.style.display = 'none';
|
|
681
|
-
document.body.append(iframe);
|
|
682
|
-
|
|
683
|
-
var doc = iframe.contentDocument || iframe.contentWindow.document;
|
|
684
|
-
doc.open();
|
|
685
|
-
doc.write(html2);
|
|
686
|
-
doc.close();
|
|
687
|
-
|
|
688
|
-
let result = await oldFunc(doc);
|
|
689
|
-
iframe.parentNode.removeChild(iframe);
|
|
690
|
-
return result;
|
|
691
|
-
};
|
|
692
|
-
}
|
|
693
|
-
|
|
694
|
-
// As part of the regular document
|
|
695
|
-
else {
|
|
696
|
-
func2 = async () => {
|
|
697
|
-
let el = createEl(html2);
|
|
698
|
-
document.body.append(el);
|
|
699
|
-
let result = await oldFunc(el);
|
|
700
|
-
document.body.removeChild(el);
|
|
701
|
-
return result;
|
|
702
|
-
}
|
|
703
|
-
}
|
|
704
|
-
}
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
// Add to rootTest tree.
|
|
708
|
-
let path = name.split(/\./g);
|
|
709
|
-
let pathSoFar = [];
|
|
710
|
-
let test = this.rootTest;
|
|
711
|
-
for (let item of path) {
|
|
712
|
-
pathSoFar.push(item);
|
|
713
|
-
|
|
714
|
-
if (!test.children)
|
|
715
|
-
test.children = {};
|
|
716
|
-
|
|
717
|
-
// If at leaf
|
|
718
|
-
if (pathSoFar.length === path.length)
|
|
719
|
-
test.children[item] = new Test(name2, desc2, func2);
|
|
720
|
-
|
|
721
|
-
// Create test if it doesn't exist.
|
|
722
|
-
else {
|
|
723
|
-
test.children[item] = test.children[item] || new Test(pathSoFar.join('.'));
|
|
724
|
-
test = test.children[item];
|
|
725
|
-
}
|
|
726
|
-
}
|
|
727
|
-
},
|
|
728
|
-
|
|
729
|
-
/**
|
|
730
|
-
* @param test {?Test} */
|
|
731
|
-
getAllTestNames(test=null) {
|
|
732
|
-
test = test || this.rootTest;
|
|
733
|
-
let result = [];
|
|
734
|
-
if (test.name.length)
|
|
735
|
-
result.push(test.name);
|
|
736
|
-
for (let name in test.children)
|
|
737
|
-
result.push(...this.getAllTestNames(test.children[name]));
|
|
738
|
-
return result;
|
|
739
|
-
},
|
|
740
|
-
|
|
741
|
-
// Internal functions:
|
|
742
|
-
|
|
743
|
-
/**
|
|
744
|
-
* @param error {Error}
|
|
745
|
-
* @param br {string}
|
|
746
|
-
* @returns {string} */
|
|
747
|
-
shortenError(error, br='<br> ') {
|
|
748
|
-
// slice(0, -3) to remove the 3 stacktrace lines inside Testimony.js that calls runtests.
|
|
749
|
-
let errorStack = error.stack.split(/\n/g).slice(0, -3).join('\r\n');
|
|
750
|
-
|
|
751
|
-
errorStack = errorStack.replace(/\r?\n/g, br);
|
|
752
|
-
return errorStack.replace(new RegExp(window.location.origin, 'g'), ''); // Remove server name to shorten error stack.
|
|
753
|
-
}
|
|
754
|
-
}
|
|
755
|
-
window.Testimony = Testimony; // used by command line test runner.
|
|
756
|
-
|
|
757
|
-
export default Testimony;
|
|
758
|
-
export {assert, Testimony, TextRenderer, HtmlRenderer};
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
// If Testimony.js is run directly from the command line
|
|
762
|
-
if (import.meta.main) {
|
|
763
|
-
let pages = null;
|
|
764
|
-
let imports = null;
|
|
765
|
-
let tests = null;
|
|
766
|
-
let webroot = null;
|
|
767
|
-
let headless = false;
|
|
768
|
-
for (let arg of Deno.args) {
|
|
769
|
-
|
|
770
|
-
if (arg.startsWith('--page=')) {
|
|
771
|
-
if (!pages)
|
|
772
|
-
pages = [];
|
|
773
|
-
pages.push(arg.slice('--page='.length));
|
|
774
|
-
}
|
|
775
|
-
|
|
776
|
-
else if (arg.startsWith('--import=')) {
|
|
777
|
-
if (!imports)
|
|
778
|
-
imports = [];
|
|
779
|
-
imports.push(arg.slice('--import='.length));
|
|
780
|
-
}
|
|
781
|
-
|
|
782
|
-
else if (arg.startsWith('--webroot='))
|
|
783
|
-
webroot = arg.slice('--webroot='.length);
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
else if (arg == '--headless')
|
|
787
|
-
headless = true;
|
|
788
|
-
|
|
789
|
-
else if (arg.startsWith('--')) {
|
|
790
|
-
console.error(`Unsupported arg ${arg}`);
|
|
791
|
-
Deno.exit(1);
|
|
792
|
-
}
|
|
793
|
-
|
|
794
|
-
// Capture test names to run.
|
|
795
|
-
else {
|
|
796
|
-
if (!tests)
|
|
797
|
-
tests = [];
|
|
798
|
-
tests.push(arg);
|
|
799
|
-
}
|
|
800
|
-
}
|
|
801
|
-
|
|
802
|
-
if (webroot && !pages)
|
|
803
|
-
pages = ['index.html'];
|
|
804
|
-
|
|
805
|
-
if (pages) {
|
|
806
|
-
for (let page of pages)
|
|
807
|
-
Testimony.runPage(page, webroot, tests, headless);
|
|
808
|
-
}
|
|
809
|
-
}
|