solarite 0.1.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.
Files changed (63) hide show
  1. package/build/build.bat +3 -0
  2. package/build/build.js +139 -0
  3. package/build/lib/rollup.min.js +11 -0
  4. package/build/lib/source-map.min.js +1 -0
  5. package/build/lib/terser.min.js +1 -0
  6. package/dist/Solarite-debug.js +4143 -0
  7. package/dist/Solarite.js +3740 -0
  8. package/dist/Solarite.min.js +4 -0
  9. package/docs/index.md +423 -0
  10. package/docs/js/Playground.js +184 -0
  11. package/docs/js/codemirror/codemirror6.js +32036 -0
  12. package/docs/js/codemirror/themeSolarIce.js +312 -0
  13. package/docs/js/documentation.js +32 -0
  14. package/docs/js/ui/CodeEditor.js +840 -0
  15. package/docs/js/ui/DarkToggle.js +52 -0
  16. package/docs/js/ui/FlexResizer.js +142 -0
  17. package/docs/js/util/Draggable2.js +151 -0
  18. package/docs/js/util/Errors.js +9 -0
  19. package/docs/js/util/Html.js +147 -0
  20. package/docs/js/util/Icons.js +623 -0
  21. package/docs/js/util/Input.js +253 -0
  22. package/docs/js/util/Util.js +88 -0
  23. package/docs/js/util/delve.js +43 -0
  24. package/docs/media/FiraCode400.woff2 +0 -0
  25. package/docs/media/cabin-latin-700.woff2 +0 -0
  26. package/docs/media/documentation.css +93 -0
  27. package/docs/media/eternium.css +1123 -0
  28. package/docs/media/solarite-machine.webp +0 -0
  29. package/index.html +325 -0
  30. package/package.json +33 -0
  31. package/readme.md +3 -0
  32. package/src/solarite/ExprPath.js +554 -0
  33. package/src/solarite/MultiValueMap.js +65 -0
  34. package/src/solarite/NodeGroup.js +706 -0
  35. package/src/solarite/NodeGroupManager.js +582 -0
  36. package/src/solarite/Shell.js +307 -0
  37. package/src/solarite/Solarite.js +19 -0
  38. package/src/solarite/Template.js +85 -0
  39. package/src/solarite/Util.js +264 -0
  40. package/src/solarite/createSolarite.js +267 -0
  41. package/src/solarite/getArg.js +99 -0
  42. package/src/solarite/hash.js +101 -0
  43. package/src/solarite/r.js +143 -0
  44. package/src/solarite/udomdiff.js +233 -0
  45. package/src/solarite/watch.js +302 -0
  46. package/src/solarite/watch2.js +439 -0
  47. package/src/unused/FastLookupArray.js +54 -0
  48. package/src/unused/Hashes.js +339 -0
  49. package/src/unused/InUse.test.js +92 -0
  50. package/src/unused/InUseMap.js +98 -0
  51. package/src/unused/LinkedList.js +117 -0
  52. package/src/unused/LinkedList.test.js +115 -0
  53. package/src/unused/Perf.js +47 -0
  54. package/src/unused/Template.js +108 -0
  55. package/src/util/Errors.js +9 -0
  56. package/src/util/Util.js +88 -0
  57. package/src/util/delve.js +43 -0
  58. package/tests/Benchmark.test.js +319 -0
  59. package/tests/NodeGroup.test.js +115 -0
  60. package/tests/Shell.test.js +75 -0
  61. package/tests/Solarite.test.js +2896 -0
  62. package/tests/Testimony.js +602 -0
  63. package/tests/index.html +75 -0
@@ -0,0 +1,602 @@
1
+ /**
2
+ * Provide functionality for running Deno tests in a web browser.
3
+ * Has no external dependencies.
4
+ *
5
+ * TODO:
6
+ * 4. Integrate with IntelliJ file watcher so we run cmd line tests when files change.
7
+ * 5. Run tests from @expect doc tags.
8
+ * 6. Documentation - Web tests, deno tests, intellij integration
9
+ * 7. Add to github.
10
+ * 8. Command line via node
11
+ * 9. Support other Deno options.
12
+ * 11. URLs only mark which tests to include or exclude, to make url shorter
13
+ * 12. Auto-expand to failed tests.
14
+ */
15
+
16
+ class AssertError extends Error {
17
+ constructor(expected, actual, op) {
18
+ super('Assertion Failed');
19
+ this.name = "AssertError";
20
+ this.expected = expected;
21
+ this.actual = actual;
22
+ this.op = op;
23
+ }
24
+ }
25
+
26
+ function assert(val) {
27
+ if (!val) {
28
+ if (Testimony.debugOnAssertFail)
29
+ debugger;
30
+ throw new AssertError(val, true);
31
+ }
32
+ }
33
+
34
+ Object.assign(assert, {
35
+ eq(expected, actual) {
36
+ if (!isSame(expected, actual)) { // JUnit, PhpUnit, and mocha all use the order: expected, actual.
37
+ if (Testimony.debugOnAssertFail)
38
+ debugger;
39
+ throw new AssertError(expected, actual, '==');
40
+ }
41
+ },
42
+
43
+ eqJson(expected, actual) {
44
+ if (JSON.stringify(actual) !== JSON.stringify(expected)) {
45
+ if (Testimony.debugOnAssertFail)
46
+ debugger;
47
+ throw new AssertError(expected, actual);
48
+ }
49
+ },
50
+
51
+ neq(val1, val2) {
52
+ if (val1 !== val2) {
53
+ if (Testimony.debugOnAssertFail)
54
+ debugger;
55
+ throw new AssertError(val1 + ' === ' + val2);
56
+ }
57
+ },
58
+
59
+ lte(val1, val2) {
60
+ if (val1 > val2) {
61
+ if (Testimony.debugOnAssertFail)
62
+ debugger;
63
+ throw new AssertError(val1 + ' > ' + val2);
64
+ }
65
+ }
66
+ });
67
+
68
+
69
+
70
+ /**
71
+ * https://stackoverflow.com/a/6713782/
72
+ * @param x
73
+ * @param y
74
+ * @return {boolean} */
75
+ function isSame( x, y ) {
76
+ if (x === y)
77
+ return true; // if both x and y are null or undefined and exactly the same
78
+
79
+ // if they are not strictly equal, they both need to be Objects
80
+ // they must have the exact same prototype chain, the closest we can do is
81
+ // test their constructor.
82
+ if (!(x instanceof Object) || !(y instanceof Object) || x.constructor !== y.constructor)
83
+ return false;
84
+
85
+ for (var p in x) {
86
+ if (!x.hasOwnProperty(p))
87
+ continue; // other properties were tested using x.constructor === y.constructor
88
+
89
+ if (!y.hasOwnProperty(p))
90
+ return false; // allows to compare x[ p ] and y[ p ] when set to undefined
91
+
92
+ if (x[p] === y[p])
93
+ continue; // if they have the same strict value or identity then they are equal
94
+
95
+ if (typeof x[p] !== "object" || !isSame(x[p], y[p])) // Numbers, Strings, Functions, Booleans must be strictly equal
96
+ return false; // Objects and Arrays must be tested recursively
97
+ }
98
+
99
+ for (p in y) // allows x[ p ] to be set to undefined
100
+ if (y.hasOwnProperty(p) && !x.hasOwnProperty(p))
101
+ return false;
102
+
103
+ return true;
104
+ }
105
+
106
+ let styleId = 1;
107
+
108
+ /**
109
+ * Create a single html element, node, or comment from the html string.
110
+ * The string will be trimmed so that an element with space before it doesn't create a text node with spaces.
111
+ * TODO: Allow specifying an existing div as props to bind a new set of html to it.
112
+ * This could be used for manually adding children.
113
+ * TODO: Get values and setValues() functions for form fields? Can make it work with any html element.
114
+ * TODO: Bind this to render() so I can render ${this.someField}
115
+ * TODO: Make properties, ids, events, and scoped styles only happen if a props argument is passed in, or is not false?
116
+ *
117
+ * I could use name="items[].name" to specify json structure of the result?
118
+ * OR:
119
+ *
120
+ * <div name="item[]">
121
+ * <input name="description">
122
+ * </div>
123
+ *
124
+ * Will give me item[0].description
125
+ *
126
+ *
127
+ * @param html {string|function} Must be a function that returns html if you want to call render() again later.
128
+ * @param doc {Document}
129
+ * @param props {Object|boolean} */
130
+ function createEl(html, props=false, doc=document) {
131
+ let template = doc.createElement('template');
132
+ let result = doc.createElement((html+'').match(/<(\w+[\w-]*)/)[1]);
133
+ let ids; // Track them so they can be removed later.
134
+
135
+ // Properties
136
+ if (typeof props === 'object')
137
+ for (let name in props) {
138
+ if (name in result)
139
+ throw new Error(`Property ${name} already exists.`);
140
+ result[name] = props[name];
141
+ }
142
+
143
+ // Render
144
+ result.render = () => {
145
+ template.innerHTML = (typeof html === 'function' ? html() : html).trim();
146
+
147
+ // Attributes and Children
148
+ if (props) {
149
+ result.innerHTML = '';
150
+ [...result.attributes].map(attr => result.removeAttribute(attr.name));
151
+ }
152
+ [...template.content.firstElementChild.attributes].map(attr => result.setAttribute(attr.name, attr.value));
153
+ [...template.content.firstElementChild.childNodes].map(child => result.append(child));
154
+
155
+ if (props) {
156
+ // Assign ids
157
+ (ids || []).map(id => delete result[id]);
158
+ ids = [...result.querySelectorAll('[id],[data-id]')].map(el => {
159
+ if (el.dataset.id in result && !(el.dataset.id in props)) // allow id's to override our custom props.
160
+ throw new Error(`Property ${el.dataset.id} already exists.`);
161
+ result[el.dataset.id] = el;
162
+ return [el.dataset.id];
163
+ });
164
+
165
+ // Bind events
166
+ [result, ...result.querySelectorAll('*')].map(el =>
167
+ [...el.attributes].filter(attr => attr.name.startsWith('on')).map(attr => {
168
+ el[attr.name] = e => // e.g. el.onclick = ...
169
+ (new Function('event', 'el', attr.value)).bind(result)(e, el) // put "event", "el", and "this" in scope for the event code.
170
+ })
171
+ );
172
+
173
+ // Scoped Styles
174
+ let styles = result.querySelectorAll('style');
175
+ if (styles.length) {
176
+ result.setAttribute('data-style-scope', styleId++); // TODO: re-use style id on re-render.
177
+ [...styles].map(style => style.textContent = style.textContent.replace(/:host(?=[^a-z\d_])/gi,
178
+ '[data-style-scope="' + result.getAttribute('data-style-scope') + '"]'));
179
+ }
180
+ }
181
+ }
182
+
183
+ if (typeof result.init === 'function')
184
+ result.init();
185
+ if (!ids) // if not called by init()
186
+ result.render();
187
+
188
+ return result;
189
+ }
190
+
191
+ // Html.encode()
192
+ function h(text, quotes='"') {
193
+ text = (text === null || text === undefined ? '' : text+'')
194
+ .replace(/&/g, '&amp;')
195
+ .replace(/</g, '&lt;')
196
+ .replace(/>/g, '&gt;')
197
+ .replace(/\a0/g, '&nbsp;')
198
+ if (quotes.includes("'"))
199
+ text = text.replace(/'/g, '&apos;');
200
+ if (quotes.includes('"'))
201
+ text = text.replace(/"/g, '&quot;');
202
+ return text;
203
+ }
204
+
205
+ var HtmlRenderer = {
206
+
207
+ /**
208
+ * @param test {Test}
209
+ * @return {HTMLDivElement} */
210
+ render(test) {
211
+
212
+ // If updating
213
+ if (test.el) {
214
+ test.el.render();
215
+ for (let t2 of Object.values(test.children || {}))
216
+ test.el.childTests.append(HtmlRenderer.render(t2));
217
+ return test.el;
218
+ }
219
+ else {
220
+
221
+ let result = createEl(() => `
222
+ <div class="test">
223
+ <style>
224
+ :host input[type=checkbox] { width: 7.7px; appearance: none; margin: 0; color: inherit }
225
+ :host [data-id=expandCB]:after { content: '+'; user-select: none; cursor: pointer }
226
+ :host [data-id=expandCB]:checked:after { content: '-' }
227
+ :host [data-id=enableCB]:after { content: '\xa0'; color: #55f; font-weight: bold; text-shadow: 1px 0 0 #55f }
228
+ :host [data-id=enableCB]:checked:after { content: 'x' }
229
+ :host [data-id=status] { line-height: 1; display: inline-block; width: 7.7px }
230
+ :host [data-id=childTests] { padding-left: ${test.name.length ? '38.5' : '15.4'}px }
231
+ </style>
232
+ <div>
233
+ ${test.name.length ?
234
+ Object.values(test.children || {}).length
235
+ ? `<input data-id="expandCB" type="checkbox" name="x" ${test.expanded ? 'checked' : ''}
236
+ value="${h(test.name) || ''}" onchange="this.expandClick()">`
237
+ : `&nbsp;`
238
+ : ''
239
+ }
240
+ <label>
241
+ [<input data-id="enableCB" type="checkbox" name="r" value="${h(test.name) || ''}"
242
+ ${(test.getShortName()[0] === '_') ? 'data-disabled' : ''}
243
+ ${test.enabled ? 'checked' : ''}
244
+ onchange="this.enableClick()">]
245
+ <span data-id="status">${
246
+ test.status === true ? `<span style="color: #0f0">✓</span>` :
247
+ test.status === null ? `&nbsp;` :
248
+ `<span style="color: red">✗</span>`
249
+ }</span>
250
+ ${h(test.getShortName())}
251
+ </label>
252
+ <span style="opacity: .5">${h(test.desc) || ''}</span>
253
+ <div style="color: red; padding-left: 61.6px">${test.status instanceof Error ? Testimony.shortenError(test.status) : ''}</div>
254
+ </div>
255
+ <div data-id="childTests" ${test.expanded ? '' : 'style="display: none"'}></div>
256
+ </div>`, {
257
+
258
+ test,
259
+
260
+ init() {
261
+ test.el = this;
262
+ },
263
+
264
+ enableClick() {
265
+ // Check all children if this is checked.
266
+ [...this.childTests.querySelectorAll('[name=r]:not([data-disabled])')].map(cb => cb.checked = this.enableCB.checked);
267
+
268
+ // Make every parent checked if all its children are checked.
269
+ let p = this;
270
+ while (p = p.parentNode)
271
+ if (p.nodeType === 1 && p.matches('.test'))
272
+ p.querySelector('[name=r]').checked = ![...p.childTests.querySelectorAll('[name=r]:not([data-disabled])')].find(cb => !cb.checked);
273
+ },
274
+
275
+ expandClick() {
276
+ this.childTests.style.display = this.expandCB.checked ? '': 'none';
277
+ }
278
+ });
279
+
280
+ // Recursively add child tests
281
+ if (test.children)
282
+ for (let test2 of Object.values(test.children))
283
+ result.childTests.append(HtmlRenderer.render(test2));
284
+
285
+ return result;
286
+ }
287
+ }
288
+ }
289
+
290
+ // Not used since Deno renders the tests.
291
+ var TextRenderer = {
292
+
293
+ render(test) {
294
+ let result = [];
295
+
296
+ // If not the root node:
297
+ if (test.name) {
298
+
299
+ // Color codes: https://stackoverflow.com/a/41407246
300
+ let status = ' ';
301
+ if (test.status === null)
302
+ status = ' ';
303
+ else if (test.status === true)
304
+ status = '\x1b[1;32m' + '✓' + '\x1b[0m'; // green, 1; for bold
305
+ else
306
+ status = '\x1b[1;31m' + '✗' + '\x1b[0m'; // red
307
+
308
+ let result2 = ' '.repeat(test.getDepth()) + `[${status}] ${test.getShortName() || ''}`;
309
+ if (test.desc)
310
+ result2 += ` \x1b[90m${test.desc}\x1b[0m`; // gray
311
+ if (test.status instanceof Error)
312
+ result2 += ' \x1b[31m' + Testimony.shortenError(test.status, '\n ') + '\x1b[0m';
313
+
314
+ result.push(result2);
315
+ }
316
+
317
+ // Recurse through children.
318
+ if (test.children)
319
+ for (let test2 of Object.values(test.children))
320
+ result.push(TextRenderer.render(test2));
321
+
322
+ return result.join('\n');
323
+ }
324
+ }
325
+
326
+
327
+ class Test {
328
+ name;
329
+ desc;
330
+ expanded;
331
+ enable;
332
+
333
+ /**
334
+ * @type {boolean|Error|null}
335
+ * true: Test passed or all child tests passed
336
+ * false: One or more child tests failed.
337
+ * Error: Test failed.
338
+ * null: Hasn't been run yet. */
339
+ status = null;
340
+
341
+ /**
342
+ * Every test will have either a fn OR children.
343
+ * @type {?function} */
344
+ fn = null;
345
+
346
+ /**
347
+ * @type {?Object<name:string, Test>} */
348
+ children = null;
349
+
350
+ constructor(name='', desc='', fn) {
351
+ this.name = name;
352
+ this.desc = desc;
353
+ this.fn = fn;
354
+
355
+ if (window.location) {
356
+ let url = new URL(window.location);
357
+ this.expanded = url.searchParams.getAll('x').includes(name);
358
+
359
+ // Enabled if this or a parent is checked
360
+ this.enabled = false;
361
+ let r = url.searchParams.getAll('r');
362
+ let parent = name;
363
+ do {
364
+ if (r.includes(parent) && !this.getShortName().startsWith('_')) {
365
+ this.enabled = true;
366
+ break;
367
+ }
368
+ parent = parent.split('.').slice(0, -1).join('.')
369
+ } while (parent);
370
+ }
371
+ else // TODO: Get enabled tests from the command line enable arguments.
372
+ this.enabled = true;
373
+ }
374
+
375
+ /**
376
+ * Run this test or its children. */
377
+ async run() {
378
+
379
+ // A test to run.
380
+ if (this.fn && this.enabled) {
381
+ let result = true;
382
+ if (Testimony.throwOnError) {
383
+ result = this.fn();
384
+ if (result instanceof Promise)
385
+ result = await result;
386
+ if (result !== false)
387
+ this.status = true;
388
+ } else {
389
+ try {
390
+ result = await this.fn();
391
+ if (result instanceof Promise)
392
+ await result;
393
+ if (result !== false)
394
+ this.status = true;
395
+ } catch (e) {
396
+ this.status = e;
397
+ }
398
+ }
399
+ }
400
+
401
+ // A node containing other tests.
402
+ if (!this.fn) {
403
+ // TODO: Run in parallel?
404
+ this.status = true;
405
+ let hasPassingChild = false;
406
+ for (let child of Object.values(this.children)) {
407
+ await child.run();
408
+ if (child.enabled) {
409
+ if (child.status === false || child.status instanceof Error)
410
+ this.status = false;
411
+
412
+ else if (child.status === null && this.status !== false)
413
+ this.status = null;
414
+
415
+ else if (child.status === true)
416
+ hasPassingChild = true;
417
+ }
418
+ }
419
+
420
+ // Must have at least one child green checkmark to have a green checkmark.
421
+ if (this.status === true && !hasPassingChild)
422
+ this.status = null;
423
+ }
424
+ }
425
+
426
+ getDepth() {
427
+ return ((this.name || '').match(/\./g) || []).length;
428
+ }
429
+
430
+ getShortName() {
431
+ return /[^.]*$/.exec(this.name)[0];
432
+ }
433
+ }
434
+
435
+
436
+ var Testimony = {
437
+
438
+ debugOnAssertFail: false,
439
+ throwOnError: false, // throw from original location on assert fail or error.
440
+ expandLevel: 1,
441
+
442
+ /** @type {Test} */
443
+ rootTest: new Test(),
444
+
445
+ async run(parent) {
446
+ let renderer = parent ? HtmlRenderer : TextRenderer;
447
+
448
+ if (parent) {
449
+ // Expand
450
+ function doExpand(test, expand) {
451
+ if (expand) {
452
+ test.expanded = true;
453
+ for (let child of Object.values(test.children || {}))
454
+ doExpand(child, expand - 1);
455
+ }
456
+ }
457
+
458
+ let hasXParam = new URL(location).searchParams.getAll('x').length;
459
+ doExpand(Testimony.rootTest, hasXParam ? 1 : this.expandLevel);
460
+
461
+ // Render empty tests
462
+ parent.append(renderer.render(Testimony.rootTest));
463
+ await new Promise(r => setTimeout(r, 1)); // allow browser to render.
464
+
465
+ // Run tests
466
+ await Testimony.rootTest.run();
467
+
468
+ // Update the status.
469
+ renderer.render(Testimony.rootTest);
470
+ }
471
+
472
+ else {
473
+ await Testimony.rootTest.run();
474
+ console.log(renderer.render(Testimony.rootTest));
475
+ }
476
+ },
477
+
478
+ /**
479
+ * Add a test.
480
+ *
481
+ * Arguments can be given in any order, except that name must occur before desc.
482
+ * @param name {string}
483
+ * @param desc {string|function()=}
484
+ * @param html {string|function()=}
485
+ * @param func {function()=} */
486
+ test(name, desc, html=null, func) {
487
+ let name2, desc2='', html2, func2;
488
+ for (let arg of arguments) {
489
+ if (typeof arg === 'function')
490
+ func2 = arg;
491
+ else if ((arg+'').trim().match(/^<[!a-z]/i)) // an open tag.
492
+ html2 = arg;
493
+ else if (!name2)
494
+ name2 = arg;
495
+ else
496
+ desc2 = arg || '';
497
+ }
498
+
499
+ // update func to create and destroy html before and after test.
500
+ if (html2) {
501
+ let oldFunc = func2;
502
+
503
+ // As an iframe.
504
+ if (html2.startsWith('<html') || html2.startsWith('<!')) {
505
+ func2 = async () => {
506
+ var iframe = document.createElement('iframe');
507
+ iframe.style.display = 'none';
508
+ document.body.append(iframe);
509
+
510
+ var doc = iframe.contentDocument || iframe.contentWindow.document;
511
+ doc.open();
512
+ doc.write(html2);
513
+ doc.close();
514
+
515
+ let result = await oldFunc(doc);
516
+ iframe.parentNode.removeChild(iframe);
517
+ return result;
518
+ };
519
+ }
520
+
521
+ // As part of the regular document
522
+ else {
523
+ func2 = async () => {
524
+ let el = createEl(html2);
525
+ document.body.append(el);
526
+ let result = await oldFunc(el);
527
+ document.body.removeChild(el);
528
+ return result;
529
+ }
530
+ }
531
+ }
532
+
533
+ if (globalThis.Deno) {
534
+ Deno.test(name2, func2);
535
+ }
536
+ else {
537
+
538
+ // Add to rootTest tree.
539
+ let path = name.split(/\./g);
540
+ let pathSoFar = [];
541
+ let test = this.rootTest;
542
+ for (let item of path) {
543
+ pathSoFar.push(item);
544
+
545
+ if (!test.children)
546
+ test.children = {};
547
+
548
+ // If at leaf
549
+ if (pathSoFar.length === path.length)
550
+ test.children[item] = new Test(name2, desc2, func2);
551
+
552
+ // Create test if it doesn't exist.
553
+ else {
554
+ test.children[item] = test.children[item] || new Test(pathSoFar.join('.'));
555
+ test = test.children[item];
556
+ }
557
+ }
558
+ }
559
+ },
560
+
561
+ // Internal functions:
562
+
563
+ shortenError(error, br='<br>&nbsp;&nbsp;') {
564
+ // slice(0, -3) to remove the 3 stacktrace lines inside Testimony.js that calls runtests.
565
+ let errorStack = error.stack.split(/\n/g).slice(0, -3).join('\r\n');
566
+
567
+ errorStack = errorStack.replace(/\r?\n/g, br);
568
+ return errorStack.replace(new RegExp(window.location.origin, 'g'), ''); // Remove server name to shorten error stack.
569
+ },
570
+
571
+ /**
572
+ * TODO: This doesn't wor0 because there's no DOMRect.
573
+ * I should conslut the jsdom docs and mabye try the "Executing scripts" section
574
+ * to run the tests inside the jsdom document?
575
+ *
576
+ * Used only when running from the command line.
577
+ * Define document object to allow us to run all modules from the command line. */
578
+ async enableJsDom() {
579
+ if (!globalThis.document) {
580
+ await (async () => {
581
+ let { default: jsdom} = await import('https://dev.jspm.io/jsdom');
582
+ let dom = new jsdom.JSDOM(`<!DOCTYPE html>`, {
583
+ pretendToBeVisual: true,
584
+ resources: 'usable'
585
+ });
586
+ let window = dom.window;
587
+
588
+ for (let name in window)
589
+ globalThis[name] = window[name];
590
+
591
+ /*let module =*/ /*import('https://deno.land/std@0.73.0/testing/asserts.ts');*/
592
+
593
+ // Sleep is required for JSDom to resolve its promises before tests begin.
594
+ await new Promise(resolve => setTimeout(resolve, 10));
595
+ })()
596
+ }
597
+ },
598
+ }
599
+ await Testimony.enableJsDom();
600
+
601
+ export default Testimony;
602
+ export {assert, Testimony, TextRenderer, HtmlRenderer};
@@ -0,0 +1,75 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <title>Solarite Tests ✨</title>
6
+ <!-- 💫💥🎇✨☀ -->
7
+ <link rel="icon" href="data:,">
8
+ <style>
9
+ body { font: 14px Consolas; margin: 0; padding: 10px; box-sizing: border-box; min-height: 100vh }
10
+ table { border-collapse: collapse }
11
+ td { vertical-align: top; padding: 0; white-space: normal }
12
+ td:first-child { white-space: nowrap }
13
+ label { user-select: none }
14
+ button { border-radius: 10px; border: 2px solid grey; padding: 2px 15px; outline: 0; cursor: pointer }
15
+ @media (prefers-color-scheme: dark) {
16
+ body { background: #000; color: #ddd }
17
+ button { background: #333; color: white }
18
+ }
19
+ </style>
20
+ </head>
21
+
22
+ <body>
23
+ <script type="module">
24
+ import {Testimony} from './Testimony.js';
25
+ import './Shell.test.js';
26
+ import './NodeGroup.test.js';
27
+ import './Solarite.test.js';
28
+ import './Benchmark.test.js';
29
+
30
+ // For testing:
31
+ window.getHtml = (item, includeComments=false) => {
32
+ if (!item)
33
+ return item;
34
+
35
+ if (item.fragment)
36
+ item = item.fragment; // Shell
37
+ if (item instanceof DocumentFragment)
38
+ item = [...item.childNodes]
39
+
40
+ else if (item.getNodes)
41
+ item = item.getNodes()
42
+
43
+ let result;
44
+ if (Array.isArray(item)) {
45
+ if (!includeComments)
46
+ item = item.filter(n => n.nodeType !==8)
47
+
48
+ result = item.map(n => n.nodeType === 8 ? `<!--${n.textContent}-->` : n.outerHTML || n.textContent).join('|')
49
+ }
50
+
51
+
52
+ else
53
+ result = item.outerHTML || item.textContent
54
+
55
+ if (!includeComments)
56
+ result = result.replace(/(<|\x3C)!--(.*?)-->/g, '')
57
+
58
+ // Remove whitespace between tags, so we can write simpler tests.
59
+ return result.replace(/^\s+</g, '<').replace(/>\s+</g, '><').replace(/>\s+$/g, '>');
60
+ }
61
+
62
+ Testimony.throwOnError = false;
63
+ Testimony.debugOnAssertFail = true;
64
+ Testimony.run(document.getElementById('tests'));
65
+ </script>
66
+ <form>
67
+ <button>Run Tests</button>
68
+ <div id="tests"></div>
69
+ <br>
70
+ <button>Run Tests</button>
71
+ </form>
72
+ </body>
73
+
74
+ </html>
75
+