@everystate/examples 1.0.0 → 1.0.1

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 (152) hide show
  1. package/README.md +15 -0
  2. package/everyState-core/001-counter/README.md +44 -0
  3. package/everyState-core/001-counter/index.html +79 -0
  4. package/everyState-core/002-counter-improved/README.md +44 -0
  5. package/everyState-core/002-counter-improved/index.html +83 -0
  6. package/everyState-core/003-input-reactive/README.md +44 -0
  7. package/everyState-core/003-input-reactive/index.html +68 -0
  8. package/everyState-core/004-computed-state/README.md +45 -0
  9. package/everyState-core/004-computed-state/index.html +83 -0
  10. package/everyState-core/005-conditional-rendering/README.md +42 -0
  11. package/everyState-core/005-conditional-rendering/index.html +68 -0
  12. package/everyState-core/006-list-rendering/README.md +49 -0
  13. package/everyState-core/006-list-rendering/index.html +92 -0
  14. package/everyState-core/007-form-validation/README.md +52 -0
  15. package/everyState-core/007-form-validation/index.html +108 -0
  16. package/everyState-core/008-undo-redo/README.md +70 -0
  17. package/everyState-core/008-undo-redo/index.html +133 -0
  18. package/everyState-core/009-localStorage-side-effects/README.md +72 -0
  19. package/everyState-core/009-localStorage-side-effects/index.html +80 -0
  20. package/everyState-core/010-decoupled-components/README.md +74 -0
  21. package/everyState-core/010-decoupled-components/index.html +117 -0
  22. package/everyState-core/011-async-patterns/README.md +98 -0
  23. package/everyState-core/011-async-patterns/index.html +132 -0
  24. package/everyState-css/001-stateDrivenCSS/index.html +377 -0
  25. package/everyState-css/002-cssV2FullDemo/index.html +630 -0
  26. package/everyState-view/001/counter/index.css +31 -0
  27. package/everyState-view/001/counter/index.html +50 -0
  28. package/everyState-view/002/datatable/index.css +70 -0
  29. package/everyState-view/002/datatable/index.html +118 -0
  30. package/everyState-view/003/todo/index.css +260 -0
  31. package/everyState-view/003/todo/index.html +218 -0
  32. package/everyState-view/003-input-reactive/README.md +44 -0
  33. package/everyState-view/003-input-reactive/index.html +68 -0
  34. package/everyState-view/004/quotesFetcher/index.css +124 -0
  35. package/everyState-view/004/quotesFetcher/index.html +108 -0
  36. package/everyState-view/004_01/quotesFetcher/app.js +32 -0
  37. package/everyState-view/004_01/quotesFetcher/components/appHeader/appSubtitle.js +2 -0
  38. package/everyState-view/004_01/quotesFetcher/components/appHeader/appTitle.js +2 -0
  39. package/everyState-view/004_01/quotesFetcher/components/appHeader.js +9 -0
  40. package/everyState-view/004_01/quotesFetcher/components/historyHeading.js +2 -0
  41. package/everyState-view/004_01/quotesFetcher/components/historyList/histAuthor.js +2 -0
  42. package/everyState-view/004_01/quotesFetcher/components/historyList/histQuote.js +2 -0
  43. package/everyState-view/004_01/quotesFetcher/components/historyList.js +14 -0
  44. package/everyState-view/004_01/quotesFetcher/components/quoteCard/fetchButton.js +2 -0
  45. package/everyState-view/004_01/quotesFetcher/components/quoteCard/quoteAuthor.js +2 -0
  46. package/everyState-view/004_01/quotesFetcher/components/quoteCard/quoteMessage.js +2 -0
  47. package/everyState-view/004_01/quotesFetcher/components/quoteCard/quoteText.js +2 -0
  48. package/everyState-view/004_01/quotesFetcher/components/quoteCard.js +11 -0
  49. package/everyState-view/004_01/quotesFetcher/index.css +124 -0
  50. package/everyState-view/004_01/quotesFetcher/index.html +23 -0
  51. package/everyState-view/004_01/quotesFetcher/store.js +35 -0
  52. package/everyState-view/004_02/quotesFetcher/app.js +20 -0
  53. package/everyState-view/004_02/quotesFetcher/components.js +46 -0
  54. package/everyState-view/004_02/quotesFetcher/index.css +124 -0
  55. package/everyState-view/004_02/quotesFetcher/index.html +23 -0
  56. package/everyState-view/004_02/quotesFetcher/store.js +35 -0
  57. package/everyState-view/004_03/quotesFetcher/actions.js +27 -0
  58. package/everyState-view/004_03/quotesFetcher/app.js +19 -0
  59. package/everyState-view/004_03/quotesFetcher/components.js +28 -0
  60. package/everyState-view/004_03/quotesFetcher/index.css +124 -0
  61. package/everyState-view/004_03/quotesFetcher/index.html +23 -0
  62. package/everyState-view/004_03/quotesFetcher/resolve.js +34 -0
  63. package/everyState-view/004_03/quotesFetcher/store.js +11 -0
  64. package/everyState-view/004_04/quotesFetcher/actions.js +66 -0
  65. package/everyState-view/004_04/quotesFetcher/app.js +24 -0
  66. package/everyState-view/004_04/quotesFetcher/components/archive.js +40 -0
  67. package/everyState-view/004_04/quotesFetcher/components/fetcher.js +29 -0
  68. package/everyState-view/004_04/quotesFetcher/components.js +20 -0
  69. package/everyState-view/004_04/quotesFetcher/index.css +283 -0
  70. package/everyState-view/004_04/quotesFetcher/index.html +24 -0
  71. package/everyState-view/004_04/quotesFetcher/resolve.js +34 -0
  72. package/everyState-view/004_04/quotesFetcher/store.js +21 -0
  73. package/everyState-view/004_04/statedump.json +826 -0
  74. package/everyState-view/004_05/quoteExplorer/actions.js +58 -0
  75. package/everyState-view/004_05/quoteExplorer/app.js +27 -0
  76. package/everyState-view/004_05/quoteExplorer/components.js +83 -0
  77. package/everyState-view/004_05/quoteExplorer/index.css +231 -0
  78. package/everyState-view/004_05/quoteExplorer/index.html +23 -0
  79. package/everyState-view/004_05/quoteExplorer/resolve.js +50 -0
  80. package/everyState-view/004_05/quoteExplorer/store.js +33 -0
  81. package/everyState-view/004_06/quoteExplorer/actions.js +21 -0
  82. package/everyState-view/004_06/quoteExplorer/app.js +44 -0
  83. package/everyState-view/004_06/quoteExplorer/components.js +80 -0
  84. package/everyState-view/004_06/quoteExplorer/derived.js +43 -0
  85. package/everyState-view/004_06/quoteExplorer/index.css +346 -0
  86. package/everyState-view/004_06/quoteExplorer/index.html +25 -0
  87. package/everyState-view/004_06/quoteExplorer/intents.js +44 -0
  88. package/everyState-view/004_06/quoteExplorer/policies.js +25 -0
  89. package/everyState-view/004_06/quoteExplorer/resolve.js +51 -0
  90. package/everyState-view/004_06/quoteExplorer/store.js +44 -0
  91. package/everyState-view/004_07/quoteExplorer/app.js +47 -0
  92. package/everyState-view/004_07/quoteExplorer/components.js +85 -0
  93. package/everyState-view/004_07/quoteExplorer/derived.js +43 -0
  94. package/everyState-view/004_07/quoteExplorer/index.css +346 -0
  95. package/everyState-view/004_07/quoteExplorer/index.html +25 -0
  96. package/everyState-view/004_07/quoteExplorer/intents.js +51 -0
  97. package/everyState-view/004_07/quoteExplorer/policies.js +21 -0
  98. package/everyState-view/004_07/quoteExplorer/resolve.js +39 -0
  99. package/everyState-view/004_07/quoteExplorer/store.js +44 -0
  100. package/everyState-view/004_08/quoteExplorer/app.js +78 -0
  101. package/everyState-view/004_08/quoteExplorer/components.js +85 -0
  102. package/everyState-view/004_08/quoteExplorer/derived.js +43 -0
  103. package/everyState-view/004_08/quoteExplorer/index.css +346 -0
  104. package/everyState-view/004_08/quoteExplorer/index.html +25 -0
  105. package/everyState-view/004_08/quoteExplorer/intents.js +51 -0
  106. package/everyState-view/004_08/quoteExplorer/policies.js +21 -0
  107. package/everyState-view/004_08/quoteExplorer/resolve.js +39 -0
  108. package/everyState-view/004_08/quoteExplorer/store.js +44 -0
  109. package/everyState-view/004_08_V2/app.js +78 -0
  110. package/everyState-view/004_08_V2/components/appDetail.js +8 -0
  111. package/everyState-view/004_08_V2/components/appDetailBar.js +7 -0
  112. package/everyState-view/004_08_V2/components/appDetailBarClose.js +8 -0
  113. package/everyState-view/004_08_V2/components/appDetailBarCount.js +7 -0
  114. package/everyState-view/004_08_V2/components/appDetailBarHeading.js +7 -0
  115. package/everyState-view/004_08_V2/components/appDetailQuotes.js +15 -0
  116. package/everyState-view/004_08_V2/components/appHeader.js +7 -0
  117. package/everyState-view/004_08_V2/components/appHeaderSubtitle.js +7 -0
  118. package/everyState-view/004_08_V2/components/appHeaderTitle.js +7 -0
  119. package/everyState-view/004_08_V2/components/appLog.js +7 -0
  120. package/everyState-view/004_08_V2/components/appLogHeading.js +7 -0
  121. package/everyState-view/004_08_V2/components/appLogList.js +16 -0
  122. package/everyState-view/004_08_V2/components/appSearch.js +7 -0
  123. package/everyState-view/004_08_V2/components/appSearchInput.js +9 -0
  124. package/everyState-view/004_08_V2/components/appStats.js +7 -0
  125. package/everyState-view/004_08_V2/components/appStatsContent.js +8 -0
  126. package/everyState-view/004_08_V2/components/appStatsContentFavcount.js +7 -0
  127. package/everyState-view/004_08_V2/components/appStatsContentText.js +7 -0
  128. package/everyState-view/004_08_V2/components/appStatsToggle.js +8 -0
  129. package/everyState-view/004_08_V2/components/appTags.js +7 -0
  130. package/everyState-view/004_08_V2/components/appTagsLabel.js +7 -0
  131. package/everyState-view/004_08_V2/components/appTagsRow.js +15 -0
  132. package/everyState-view/004_08_V2/components/index.js +59 -0
  133. package/everyState-view/004_08_V2/components/utils/css.js +88 -0
  134. package/everyState-view/004_08_V2/components/utils/elements.js +87 -0
  135. package/everyState-view/004_08_V2/components.js +79 -0
  136. package/everyState-view/004_08_V2/derived.js +43 -0
  137. package/everyState-view/004_08_V2/index.css +350 -0
  138. package/everyState-view/004_08_V2/index.html +25 -0
  139. package/everyState-view/004_08_V2/intents.js +51 -0
  140. package/everyState-view/004_08_V2/policies.js +21 -0
  141. package/everyState-view/004_08_V2/resolve.js +39 -0
  142. package/everyState-view/004_08_V2/store.js +44 -0
  143. package/everyState-view/006/api-datatable/index.css +388 -0
  144. package/everyState-view/006/api-datatable/index.html +355 -0
  145. package/everyState-view/007/apiUsers/index.html +307 -0
  146. package/everyState-view/007-form-validation/README.md +52 -0
  147. package/everyState-view/007-form-validation/index.html +108 -0
  148. package/everyState-view/010-decoupled-components/README.md +74 -0
  149. package/everyState-view/010-decoupled-components/index.html +117 -0
  150. package/everyState-view/index.html +36 -0
  151. package/index.js +0 -5
  152. package/package.json +2 -4
@@ -0,0 +1,117 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>010 Decoupled Components - EveryState</title>
7
+ <style>
8
+ .component { border: 2px solid #333; padding: 20px; margin: 10px 0; }
9
+ </style>
10
+ <script type="importmap">
11
+ { "imports": {
12
+ "@everystate/core": "../../../everystate-core/index.js",
13
+ "@everystate/perf": "../../../everystate-perf/index.js"
14
+ }}
15
+ </script>
16
+ </head>
17
+ <body>
18
+ <h1>Decoupled Components</h1>
19
+ <p>Three independent "components" sharing state without knowing about each other.</p>
20
+
21
+ <div class="component">
22
+ <h2>Component A: Writer</h2>
23
+ <input type="text" id="messageInput" placeholder="Type a message...">
24
+ <button id="sendBtn">Send</button>
25
+ </div>
26
+
27
+ <div class="component">
28
+ <h2>Component B: Reader</h2>
29
+ <p><strong>Received:</strong> <span id="messageDisplay">(nothing yet)</span></p>
30
+ <p><small>Message count: <span id="messageCount">0</span></small></p>
31
+ </div>
32
+
33
+ <div class="component">
34
+ <h2>Component C: Logger</h2>
35
+ <ul id="messageLog"></ul>
36
+ </div>
37
+
38
+ <hr>
39
+ <button id="runTests">Run Tests</button>
40
+ <pre id="testLog"></pre>
41
+
42
+ <script type="module">
43
+ import { createEveryState } from '@everystate/core';
44
+ import { createPerfMonitor, mountOverlay } from '@everystate/perf';
45
+
46
+ const store = createEveryState({ message: '', messageCount: 0 });
47
+ const perf = createPerfMonitor(store);
48
+ mountOverlay(perf, document.body);
49
+
50
+ // Component A: Writer
51
+ document.getElementById('sendBtn').onclick = () => {
52
+ const input = document.getElementById('messageInput');
53
+ const value = input.value.trim();
54
+ if (value) {
55
+ store.set('message', value);
56
+ store.set('messageCount', store.get('messageCount') + 1);
57
+ input.value = '';
58
+ }
59
+ };
60
+
61
+ document.getElementById('messageInput').onkeypress = (e) => {
62
+ if (e.key === 'Enter') document.getElementById('sendBtn').click();
63
+ };
64
+
65
+ // Component B: Reader
66
+ store.subscribe('message', (value) => {
67
+ document.getElementById('messageDisplay').textContent = value || '(nothing yet)';
68
+ });
69
+
70
+ store.subscribe('messageCount', (value) => {
71
+ document.getElementById('messageCount').textContent = value;
72
+ });
73
+
74
+ // Component C: Logger
75
+ store.subscribe('message', (value) => {
76
+ if (value) {
77
+ const li = document.createElement('li');
78
+ li.textContent = `[${new Date().toLocaleTimeString()}] ${value}`;
79
+ document.getElementById('messageLog').appendChild(li);
80
+ }
81
+ });
82
+
83
+ // Event Sequence Tests
84
+ document.getElementById('runTests').onclick = () => {
85
+ const results = [];
86
+ let pass = 0, fail = 0;
87
+ function assert(label, cond) {
88
+ if (cond) { pass++; results.push(' OK ' + label); }
89
+ else { fail++; results.push(' FAIL ' + label); }
90
+ }
91
+
92
+ store.set('message', 'test-msg');
93
+ assert('reader shows message', document.getElementById('messageDisplay').textContent === 'test-msg');
94
+
95
+ store.set('messageCount', 0);
96
+ store.set('messageCount', store.get('messageCount') + 1);
97
+ assert('count incremented', store.get('messageCount') === 1);
98
+ assert('count DOM updated', document.getElementById('messageCount').textContent === '1');
99
+
100
+ // Multiple subscribers on same path
101
+ let fires = 0;
102
+ const u1 = store.subscribe('message', () => fires++);
103
+ const u2 = store.subscribe('message', () => fires++);
104
+ store.set('message', 'multi');
105
+ assert('multiple subs both fire', fires === 2);
106
+ u1(); u2();
107
+
108
+ assert('logger has entries', document.getElementById('messageLog').querySelectorAll('li').length > 0);
109
+ assert('perf tracked', perf.report().summary.totalSets > 0);
110
+
111
+ store.set('message', '');
112
+ store.set('messageCount', 0);
113
+ document.getElementById('testLog').textContent = results.join('\n') + `\n\n${pass} passed, ${fail} failed`;
114
+ };
115
+ </script>
116
+ </body>
117
+ </html>
@@ -0,0 +1,98 @@
1
+ # 011 Async Patterns - Debouncing & Loading States
2
+
3
+ Demonstrates how to handle async operations with EveryState.
4
+
5
+ ## What's Here
6
+
7
+ - **Debounced search:** Wait for user to stop typing before searching
8
+ - **Loading states:** Show loading indicator during async operations
9
+ - **Error handling:** Graceful failure with try/catch
10
+ - **No async library needed** - just Promises and state
11
+
12
+ ## How It Works
13
+
14
+ ### Debounced Search
15
+
16
+ ```javascript
17
+ let searchTimeout = null;
18
+
19
+ store.subscribe('searchQuery', async (value) => {
20
+ clearTimeout(searchTimeout);
21
+
22
+ store.set('isSearching', true);
23
+
24
+ // Wait 300ms before searching
25
+ searchTimeout = setTimeout(async () => {
26
+ const result = await performSearch(value);
27
+ store.set('searchResult', result);
28
+ store.set('isSearching', false);
29
+ }, 300);
30
+ });
31
+ ```
32
+
33
+ ### Loading States
34
+
35
+ ```javascript
36
+ document.getElementById('fetchBtn').onclick = async () => {
37
+ store.set('isLoading', true);
38
+
39
+ try {
40
+ const data = await fetchData();
41
+ store.set('apiData', data);
42
+ } finally {
43
+ store.set('isLoading', false);
44
+ }
45
+ };
46
+ ```
47
+
48
+ ## Key Insight
49
+
50
+ **Async is just state changes over time.**
51
+
52
+ Pattern for any async operation:
53
+ 1. Set loading state to `true`
54
+ 2. Perform async operation
55
+ 3. Set result state
56
+ 4. Set loading state to `false`
57
+
58
+ Other frameworks need:
59
+ - React: `useState` + `useEffect` + cleanup functions
60
+ - Vue: `ref` + `watch` + async watchers
61
+ - Svelte: Reactive statements + `#await` blocks
62
+
63
+ **EveryState:** Just use async/await in subscribers. That's it.
64
+
65
+ ## Patterns Demonstrated
66
+
67
+ ### 1. Debouncing
68
+ Wait for user to stop typing before triggering expensive operations.
69
+
70
+ ### 2. Loading Indicators
71
+ Show feedback during async operations to improve UX.
72
+
73
+ ### 3. Disabling Actions
74
+ Prevent duplicate requests by disabling buttons during loading.
75
+
76
+ ### 4. Cleanup
77
+ Clear timeouts to prevent race conditions.
78
+
79
+ ## Run It
80
+
81
+ Open `index.html`:
82
+ - Type in the search box (notice the 300ms delay)
83
+ - Click "Fetch Data" (notice the loading state)
84
+
85
+ ## Try This
86
+
87
+ Open DevTools Network tab and throttle to "Slow 3G" to see loading states more clearly.
88
+
89
+ ## Real-World Usage
90
+
91
+ This pattern works for:
92
+ - ✅ API calls
93
+ - ✅ Autocomplete
94
+ - ✅ Form submission
95
+ - ✅ File uploads
96
+ - ✅ Any async operation
97
+
98
+ Just manage loading/error/success states in the store.
@@ -0,0 +1,132 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>011 Async Patterns - EveryState</title>
7
+ <script type="importmap">
8
+ { "imports": {
9
+ "@everystate/core": "../../../everystate-core/index.js",
10
+ "@everystate/perf": "../../../everystate-perf/index.js"
11
+ }}
12
+ </script>
13
+ </head>
14
+ <body>
15
+ <h1>Async Patterns</h1>
16
+
17
+ <h2>Debounced Search</h2>
18
+ <input type="text" id="searchInput" placeholder="Type to search...">
19
+ <p id="searchStatus"></p>
20
+ <p><strong>Search result:</strong> <span id="searchResult">(type something)</span></p>
21
+
22
+ <hr>
23
+
24
+ <h2>Simulated API Call</h2>
25
+ <button id="fetchBtn">Fetch Data</button>
26
+ <p id="loadingStatus"></p>
27
+ <p><strong>Data:</strong> <span id="apiData">(not loaded)</span></p>
28
+
29
+ <hr>
30
+ <button id="runTests">Run Tests</button>
31
+ <pre id="testLog"></pre>
32
+
33
+ <script type="module">
34
+ import { createEveryState } from '@everystate/core';
35
+ import { createPerfMonitor, mountOverlay } from '@everystate/perf';
36
+
37
+ const store = createEveryState({
38
+ searchQuery: '', searchResult: '', isSearching: false,
39
+ apiData: null, isLoading: false
40
+ });
41
+ const perf = createPerfMonitor(store);
42
+ mountOverlay(perf, document.body);
43
+
44
+ let searchTimeout = null;
45
+
46
+ const performSearch = (query) => new Promise((resolve) => {
47
+ setTimeout(() => resolve(`Results for "${query}": Found ${query.length} items`), 500);
48
+ });
49
+
50
+ store.subscribe('searchQuery', async (value) => {
51
+ clearTimeout(searchTimeout);
52
+ if (!value) { store.set('searchResult', ''); store.set('isSearching', false); return; }
53
+ store.set('isSearching', true);
54
+ searchTimeout = setTimeout(async () => {
55
+ const result = await performSearch(value);
56
+ store.set('searchResult', result);
57
+ store.set('isSearching', false);
58
+ }, 300);
59
+ });
60
+
61
+ store.subscribe('isSearching', (value) => {
62
+ document.getElementById('searchStatus').textContent = value ? 'Searching...' : '';
63
+ });
64
+
65
+ store.subscribe('searchResult', (value) => {
66
+ document.getElementById('searchResult').textContent = value || '(type something)';
67
+ });
68
+
69
+ document.getElementById('searchInput').oninput = (e) => { store.set('searchQuery', e.target.value); };
70
+
71
+ const fetchData = () => new Promise((resolve) => {
72
+ setTimeout(() => resolve({ id: Math.floor(Math.random() * 1000), timestamp: new Date().toLocaleTimeString() }), 1000);
73
+ });
74
+
75
+ store.subscribe('isLoading', (value) => {
76
+ document.getElementById('loadingStatus').textContent = value ? 'Loading...' : '';
77
+ document.getElementById('fetchBtn').disabled = value;
78
+ });
79
+
80
+ store.subscribe('apiData', (value) => {
81
+ if (value) document.getElementById('apiData').textContent = `ID: ${value.id}, Time: ${value.timestamp}`;
82
+ });
83
+
84
+ document.getElementById('fetchBtn').onclick = async () => {
85
+ store.set('isLoading', true);
86
+ try {
87
+ const data = await fetchData();
88
+ store.set('apiData', data);
89
+ } catch (error) {
90
+ console.error('Fetch failed:', error);
91
+ } finally {
92
+ store.set('isLoading', false);
93
+ }
94
+ };
95
+
96
+ // Event Sequence Tests
97
+ document.getElementById('runTests').onclick = () => {
98
+ const results = [];
99
+ let pass = 0, fail = 0;
100
+ function assert(label, cond) {
101
+ if (cond) { pass++; results.push(' OK ' + label); }
102
+ else { fail++; results.push(' FAIL ' + label); }
103
+ }
104
+
105
+ // Sync tests (async search tested manually)
106
+ store.set('isLoading', true);
107
+ assert('isLoading set', store.get('isLoading') === true);
108
+ assert('button disabled while loading', document.getElementById('fetchBtn').disabled === true);
109
+
110
+ store.set('isLoading', false);
111
+ assert('button enabled after load', document.getElementById('fetchBtn').disabled === false);
112
+
113
+ store.set('searchQuery', 'test');
114
+ assert('searchQuery stored', store.get('searchQuery') === 'test');
115
+ assert('isSearching set', store.get('isSearching') === true);
116
+
117
+ // Wildcard test
118
+ let wildFired = false;
119
+ const unsub = store.subscribe('*', () => { wildFired = true; });
120
+ store.set('isLoading', true);
121
+ assert('global wildcard fires', wildFired);
122
+ unsub();
123
+
124
+ assert('perf tracked', perf.report().summary.totalSets > 0);
125
+
126
+ store.set('searchQuery', '');
127
+ store.set('isLoading', false);
128
+ document.getElementById('testLog').textContent = results.join('\n') + `\n\n${pass} passed, ${fail} failed`;
129
+ };
130
+ </script>
131
+ </body>
132
+ </html>