kempo-testing-framework 1.2.4 → 1.3.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.
Files changed (54) hide show
  1. package/.github/copilot-instructions.md +105 -105
  2. package/.github/workflows/publish-npm.yml +41 -0
  3. package/CONTRIBUTING.md +107 -107
  4. package/gui/components/Collapsible.js +54 -54
  5. package/gui/components/Icon.js +151 -151
  6. package/gui/components/Logs.js +73 -73
  7. package/gui/components/SettingCheckbox.js +42 -42
  8. package/gui/components/SettingNumber.js +77 -77
  9. package/gui/components/SettingSelect.js +67 -67
  10. package/gui/components/TestFramework.js +378 -378
  11. package/gui/components/TestSuite.js +181 -181
  12. package/gui/components/TestSummary.js +189 -189
  13. package/gui/components/Theme.js +40 -40
  14. package/gui/components/settingsStore.js +46 -46
  15. package/gui/icons/fail.svg +1 -1
  16. package/gui/icons/pass.svg +1 -1
  17. package/gui/icons/running.svg +1 -1
  18. package/gui/icons/settings.svg +1 -1
  19. package/package.json +5 -1
  20. package/src/browserTestServer.js +115 -115
  21. package/src/cli.js +198 -198
  22. package/src/gui.js +15 -2
  23. package/src/runBrowserTests.js +87 -87
  24. package/src/runTestFiles.js +94 -94
  25. package/src/runTests.js +83 -83
  26. package/src/utils/logLevels.js +7 -7
  27. package/test.html +30 -30
  28. package/tests/Counter.js +33 -33
  29. package/tests/cli-flags.node-test.js +54 -54
  30. package/tests/cli-help.node-test.js +61 -61
  31. package/tests/cli-loglevel.node-test.js +40 -40
  32. package/tests/collapsible.browser-test.js +49 -49
  33. package/tests/counter.browser-test.js +140 -140
  34. package/tests/example.node-test.js +103 -103
  35. package/tests/fibonacci.js +92 -92
  36. package/tests/fibonacci.test.js +285 -285
  37. package/tests/icon.browser-test.js +54 -54
  38. package/tests/logs.browser-test.js +47 -47
  39. package/tests/setting-checkbox.browser-test.js +48 -48
  40. package/tests/setting-number.browser-test.js +54 -54
  41. package/tests/setting-select.browser-test.js +47 -47
  42. package/tests/settings-store.browser-test.js +26 -26
  43. package/tests/src-browserTestServer.node-test.js +47 -47
  44. package/tests/src-cli.node-test.js +32 -32
  45. package/tests/src-findTests.node-test.js +41 -41
  46. package/tests/src-logLevels.node-test.js +29 -29
  47. package/tests/src-runBrowserTests.node-test.js +41 -41
  48. package/tests/src-runTestFiles.node-test.js +42 -42
  49. package/tests/src-runTests.node-test.js +56 -56
  50. package/tests/test-framework.browser-test.js +65 -65
  51. package/tests/test-summary.browser-test.js +78 -78
  52. package/tests/test.browser-test.js +56 -56
  53. package/tests/testfile.browser-test.js +60 -60
  54. package/tests/theme.browser-test.js +38 -38
@@ -1,378 +1,378 @@
1
- import { LitElement, html } from '../lit-all.min.js';
2
- import { getSettings } from './settingsStore.js';
3
-
4
- class TestFrameworkEl extends LitElement {
5
- /*
6
- Properties
7
- */
8
- static properties = { };
9
- constructor(){
10
- super();
11
- this.queue = [];
12
- this.queueKeys = new Set();
13
- this.running = false;
14
- this.runningKey = null;
15
- }
16
-
17
- /*
18
- Lifecycle
19
- */
20
- connectedCallback(){
21
- super.connectedCallback();
22
- }
23
- disconnectedCallback(){
24
- super.disconnectedCallback();
25
- }
26
-
27
- /*
28
- Utility
29
- */
30
- keyFor = (task) => task.type==='test' ? `test:${task.file}::${task.name}` : `suite:${task.file}`;
31
-
32
- /*
33
- Public Methods
34
- */
35
- enqueueTest({ file, name, el }){
36
- if(!file || !name || !el) return;
37
- const task = { type: 'test', file, name, el };
38
- const key = this.keyFor(task);
39
- if(this.runningKey===key || this.queueKeys.has(key)) return;
40
- this.queue.push(task);
41
- this.queueKeys.add(key);
42
- try { el.status = 'queued'; } catch {}
43
- this.dispatchEvent(new CustomEvent('ktf:queue-updated', { detail: { length: this.queue.length } }));
44
- this.runNext();
45
- }
46
-
47
- enqueueSuite({ file, testNames, el }){
48
- if(!file || !el) return;
49
- const names = Array.isArray(testNames)?testNames:[];
50
- const task = { type: 'suite', file, testNames: names, el };
51
- const key = this.keyFor(task);
52
- if(this.runningKey===key || this.queueKeys.has(key)) return;
53
- this.queue.push(task);
54
- this.queueKeys.add(key);
55
- try {
56
- el.status = 'queued';
57
- const tests = Array.from(el.querySelectorAll('ktf-test'));
58
- for(const t of tests){ t.status = 'queued'; }
59
- } catch {}
60
- this.dispatchEvent(new CustomEvent('ktf:queue-updated', { detail: { length: this.queue.length } }));
61
- this.runNext();
62
- }
63
-
64
- runAllSuites(){
65
- const suites = Array.from(this.querySelectorAll('ktf-test-suite'));
66
- for(const s of suites){ this.enqueueSuite({ file: s.file, testNames: s.testNames, el: s }); }
67
- };
68
-
69
- /*
70
- Internal Queue Runner
71
- */
72
- runNext = async () => {
73
- if(this.running) return;
74
- const task = this.queue.shift();
75
- if(!task) return;
76
- this.running = true;
77
- const key = this.keyFor(task);
78
- this.queueKeys.delete(key);
79
- this.runningKey = key;
80
- this.dispatchEvent(new CustomEvent('ktf:queue-updated', { detail: { length: this.queue.length } }));
81
- try {
82
- if(task.type==='test') await this.runTest(task);
83
- else await this.runSuite(task);
84
- } catch (e) {
85
- console.error(e);
86
- } finally {
87
- this.running = false;
88
- this.runningKey = null;
89
- this.dispatchEvent(new CustomEvent('ktf:queue-updated', { detail: { length: this.queue.length } }));
90
- if(this.queue.length) this.runNext();
91
- }
92
- };
93
-
94
- /*
95
- Runners
96
- */
97
- runTest = async ({ file, name, el }) => {
98
- try {
99
- el.status = 'running';
100
- const logsEl = el.renderRoot?.getElementById('logs');
101
- if (logsEl) logsEl.clear();
102
- } catch {}
103
- const { showBrowser, delayMs } = getSettings();
104
- const resp = await fetch(`/runTest?testFile=${encodeURIComponent(file)}&testNames=${encodeURIComponent(name)}&showBrowser=${!!showBrowser}&delayMs=${Number(delayMs||0)}`);
105
- let data = null;
106
- try { data = await resp.json(); } catch {}
107
- if(!resp.ok || (data && data.error)){
108
- const msg = data?.details || data?.error || `${resp.status} ${resp.statusText}`;
109
- try {
110
- const logsEl = el.renderRoot?.getElementById('logs');
111
- if (logsEl) logsEl.addLog({ message: `Error running test: ${msg}`, type: 'error', level: 3 });
112
- el.status = 'fail';
113
- } catch {}
114
- return;
115
- }
116
- const fileResults = data?.results || {};
117
- const beforeAllLogs = Array.isArray(fileResults.beforeAllLogs) ? fileResults.beforeAllLogs : [];
118
- const afterAllLogs = Array.isArray(fileResults.afterAllLogs) ? fileResults.afterAllLogs : [];
119
- const testInfo = fileResults.tests?.[name] || null;
120
- const testLogs = Array.isArray(testInfo?.logs) ? testInfo.logs : [];
121
- try {
122
- el.status = testInfo?.passed ? 'pass' : 'fail';
123
- const logsEl = el.renderRoot?.getElementById('logs');
124
- if (logsEl) {
125
- const heading = msg => ({ message: msg, type: 'progress', level: 3 });
126
- const batch = [];
127
- if (beforeAllLogs.length) batch.push(heading('== Before All Logs =='), ...beforeAllLogs);
128
- batch.push(...testLogs);
129
- if (afterAllLogs.length) batch.push(heading('== After All Logs =='), ...afterAllLogs);
130
- const testsMap = fileResults.tests || {};
131
- const names = Object.keys(testsMap);
132
- const total = names.length;
133
- const passed = names.reduce((acc, n) => acc + (testsMap[n]?.passed ? 1 : 0), 0);
134
- const failed = total - passed;
135
- if (total > 0) {
136
- batch.push(
137
- { message: '=== Test Summary ====', type: 'summary', level: 1 },
138
- { message: `Total Tests: ${total}`, type: 'summary', level: 1 },
139
- { message: `Passed: ${passed}`, type: passed > 0 ? 'pass' : 'log', level: 1 },
140
- { message: `Failed: ${failed}`, type: failed > 0 ? 'fail' : 'log', level: 1 }
141
- );
142
- batch.push(
143
- failed === 0
144
- ? { message: 'All tests passed!', type: 'pass', level: 1 }
145
- : { message: 'Some tests failed. See details above.', type: 'fail', level: 1 }
146
- );
147
- }
148
- logsEl.addLog(...batch);
149
- }
150
- } catch {}
151
- };
152
-
153
- runSuite = async ({ file, testNames, el }) => {
154
- const isUniversal = el.hasAttribute('universal');
155
-
156
- try {
157
- el.status = 'running';
158
- const tests = Array.from(el.querySelectorAll('ktf-test'));
159
- for(const t of tests){
160
- t.status = 'running';
161
- const logsEl = t.renderRoot?.getElementById('logs');
162
- if(logsEl) logsEl.clear();
163
- }
164
- const fileLogsEl = el.renderRoot?.getElementById('fileLogs');
165
- if(fileLogsEl) fileLogsEl.clear();
166
- } catch {}
167
-
168
- const { showBrowser, delayMs } = getSettings();
169
-
170
- // For universal tests, run both environments
171
- if (isUniversal && file.endsWith('.test.js')) {
172
- try {
173
- // Run both Node and Browser tests
174
- const [nodeResp, browserResp] = await Promise.all([
175
- fetch(`/runTest?testFile=${encodeURIComponent(file)}&environment=node&showBrowser=false&delayMs=${Number(delayMs||0)}`),
176
- fetch(`/runTest?testFile=${encodeURIComponent(file)}&environment=browser&showBrowser=${!!showBrowser}&delayMs=${Number(delayMs||0)}`)
177
- ]);
178
-
179
- let nodeData = null, browserData = null;
180
- try { nodeData = await nodeResp.json(); } catch {}
181
- try { browserData = await browserResp.json(); } catch {}
182
-
183
- const fileLogsEl = el.renderRoot?.getElementById('fileLogs');
184
- const heading = msg => ({ message: msg, type: 'progress', level: 3 });
185
-
186
- // Combine results from both environments
187
- let combinedResults = { tests: {}, beforeAllLogs: [], afterAllLogs: [] };
188
- let hasErrors = false;
189
-
190
- if (!nodeResp.ok || (nodeData && nodeData.error)) {
191
- hasErrors = true;
192
- const msg = nodeData?.details || nodeData?.error || `Node: ${nodeResp.status} ${nodeResp.statusText}`;
193
- if (fileLogsEl) fileLogsEl.addLog({ message: `Error running Node tests: ${msg}`, type: 'error', level: 3 });
194
- } else {
195
- const nodeResults = nodeData?.results || {};
196
- if (fileLogsEl && nodeResults.beforeAllLogs?.length) {
197
- fileLogsEl.addLog(heading('== Node Before All Logs =='), ...nodeResults.beforeAllLogs);
198
- }
199
- // Merge node test results with 'Node: ' prefix
200
- Object.entries(nodeResults.tests || {}).forEach(([testName, testInfo]) => {
201
- combinedResults.tests[testName] = {
202
- ...testInfo,
203
- logs: [
204
- heading('== Node Environment =='),
205
- ...(testInfo.logs || [])
206
- ]
207
- };
208
- });
209
- }
210
-
211
- if (!browserResp.ok || (browserData && browserData.error)) {
212
- hasErrors = true;
213
- const msg = browserData?.details || browserData?.error || `Browser: ${browserResp.status} ${browserResp.statusText}`;
214
- if (fileLogsEl) fileLogsEl.addLog({ message: `Error running Browser tests: ${msg}`, type: 'error', level: 3 });
215
- } else {
216
- const browserResults = browserData?.results || {};
217
- if (fileLogsEl && browserResults.beforeAllLogs?.length) {
218
- fileLogsEl.addLog(heading('== Browser Before All Logs =='), ...browserResults.beforeAllLogs);
219
- }
220
- // Merge browser test results
221
- Object.entries(browserResults.tests || {}).forEach(([testName, testInfo]) => {
222
- if (combinedResults.tests[testName]) {
223
- // Combine with existing node results
224
- combinedResults.tests[testName].logs.push(
225
- heading('== Browser Environment =='),
226
- ...(testInfo.logs || [])
227
- );
228
- // Test passes only if both environments pass
229
- combinedResults.tests[testName].passed = combinedResults.tests[testName].passed && testInfo.passed;
230
- } else {
231
- // Browser-only result (shouldn't happen for universal tests, but handle it)
232
- combinedResults.tests[testName] = {
233
- ...testInfo,
234
- logs: [
235
- heading('== Browser Environment =='),
236
- ...(testInfo.logs || [])
237
- ]
238
- };
239
- }
240
- });
241
- }
242
-
243
- if (hasErrors) {
244
- try {
245
- const tests = Array.from(el.querySelectorAll('ktf-test'));
246
- for(const t of tests){ t.status = 'fail'; }
247
- el.status = 'fail';
248
- } catch {}
249
- return;
250
- }
251
-
252
- // Update test elements with combined results
253
- const testsMap = combinedResults.tests;
254
- try {
255
- const tests = Array.from(el.querySelectorAll('ktf-test'));
256
- for(const t of tests){
257
- const name = t.name;
258
- const info = testsMap[name];
259
- const logsEl = t.renderRoot?.getElementById('logs');
260
- if(logsEl && info){
261
- logsEl.addLog(...(info.logs || []));
262
- }
263
- t.status = info?.passed ? 'pass' : 'fail';
264
- }
265
-
266
- if(fileLogsEl){
267
- const names = Object.keys(testsMap);
268
- const total = names.length;
269
- const passed = names.reduce((acc, n) => acc + (testsMap[n]?.passed ? 1 : 0), 0);
270
- const failed = total - passed;
271
- if(total>0){
272
- fileLogsEl.addLog(
273
- { message: '=== Universal Test Summary (Node + Browser) ====', type: 'summary', level: 1 },
274
- { message: `Total Tests: ${total}`, type: 'summary', level: 1 },
275
- { message: `Passed in Both Environments: ${passed}`, type: passed>0 ? 'pass' : 'log', level: 1 },
276
- { message: `Failed in At Least One Environment: ${failed}`, type: failed>0 ? 'fail' : 'log', level: 1 },
277
- failed===0
278
- ? { message: 'All tests passed in both environments!', type: 'pass', level: 1 }
279
- : { message: 'Some tests failed. See details above.', type: 'fail', level: 1 }
280
- );
281
- }
282
- }
283
-
284
- // Recalculate suite status
285
- const testEls = el.querySelectorAll('ktf-test');
286
- const statuses = Array.from(testEls).map(x=>x.status);
287
- let suiteStatus = 'notran';
288
- if(statuses.includes('running')) suiteStatus = 'running';
289
- else if(statuses.includes('fail')) suiteStatus = 'fail';
290
- else if(statuses.length && statuses.every(s => s==='pass')) suiteStatus = 'pass';
291
- el.status = suiteStatus;
292
- } catch {}
293
-
294
- } catch (error) {
295
- console.error('Error running universal test:', error);
296
- try {
297
- const fileLogsEl = el.renderRoot?.getElementById('fileLogs');
298
- if(fileLogsEl) fileLogsEl.addLog({ message: `Error running universal test: ${error.message}`, type: 'error', level: 3 });
299
- const tests = Array.from(el.querySelectorAll('ktf-test'));
300
- for(const t of tests){ t.status = 'fail'; }
301
- el.status = 'fail';
302
- } catch {}
303
- }
304
- return;
305
- }
306
-
307
- // Original single-environment logic for non-universal tests
308
- const resp = await fetch(`/runTest?testFile=${encodeURIComponent(file)}&showBrowser=${!!showBrowser}&delayMs=${Number(delayMs||0)}`);
309
- let data = null;
310
- try { data = await resp.json(); } catch {}
311
- if(!resp.ok || (data && data.error)){
312
- const msg = data?.details || data?.error || `${resp.status} ${resp.statusText}`;
313
- try {
314
- const fileLogsEl = el.renderRoot?.getElementById('fileLogs');
315
- if(fileLogsEl) fileLogsEl.addLog({ message: `Error running tests: ${msg}`, type: 'error', level: 3 });
316
- const tests = Array.from(el.querySelectorAll('ktf-test'));
317
- for(const t of tests){ t.status = 'fail'; }
318
- el.status = 'fail';
319
- } catch {}
320
- return;
321
- }
322
- const results = data?.results || {};
323
- const beforeAllLogs = Array.isArray(results.beforeAllLogs) ? results.beforeAllLogs : [];
324
- const afterAllLogs = Array.isArray(results.afterAllLogs) ? results.afterAllLogs : [];
325
- const testsMap = results.tests || {};
326
- try {
327
- const fileLogsEl = el.renderRoot?.getElementById('fileLogs');
328
- const heading = msg => ({ message: msg, type: 'progress', level: 3 });
329
- if(fileLogsEl && beforeAllLogs.length){ fileLogsEl.addLog(heading('== Before All Logs =='), ...beforeAllLogs); }
330
- const tests = Array.from(el.querySelectorAll('ktf-test'));
331
- for(const t of tests){
332
- const name = t.name;
333
- const info = testsMap[name];
334
- const logsEl = t.renderRoot?.getElementById('logs');
335
- if(logsEl){
336
- const batch = [];
337
- if(Array.isArray(info?.logs)) batch.push(...info.logs);
338
- logsEl.addLog(...batch);
339
- }
340
- t.status = info?.passed ? 'pass' : 'fail';
341
- }
342
- if(fileLogsEl){
343
- if(afterAllLogs.length) fileLogsEl.addLog(heading('== After All Logs =='), ...afterAllLogs);
344
- const names = Object.keys(testsMap);
345
- const total = names.length;
346
- const passed = names.reduce((acc, n) => acc + (testsMap[n]?.passed ? 1 : 0), 0);
347
- const failed = total - passed;
348
- if(total>0){
349
- fileLogsEl.addLog(
350
- { message: '=== Test Summary ====', type: 'summary', level: 1 },
351
- { message: `Total Tests: ${total}`, type: 'summary', level: 1 },
352
- { message: `Passed: ${passed}`, type: passed>0 ? 'pass' : 'log', level: 1 },
353
- { message: `Failed: ${failed}`, type: failed>0 ? 'fail' : 'log', level: 1 },
354
- failed===0
355
- ? { message: 'All tests passed!', type: 'pass', level: 1 }
356
- : { message: 'Some tests failed. See details above.', type: 'fail', level: 1 }
357
- );
358
- }
359
- }
360
- // Recalculate suite status
361
- const testEls = el.querySelectorAll('ktf-test');
362
- const statuses = Array.from(testEls).map(x=>x.status);
363
- let suiteStatus = 'notran';
364
- if(statuses.includes('running')) suiteStatus = 'running';
365
- else if(statuses.includes('fail')) suiteStatus = 'fail';
366
- else if(statuses.length && statuses.every(s => s==='pass')) suiteStatus = 'pass';
367
- el.status = suiteStatus;
368
- } catch {}
369
- };
370
-
371
- /*
372
- Rendering
373
- */
374
- render(){ return html`<slot></slot>`; }
375
- }
376
-
377
- customElements.define('ktf-test-framework', TestFrameworkEl);
378
- export default TestFrameworkEl;
1
+ import { LitElement, html } from '../lit-all.min.js';
2
+ import { getSettings } from './settingsStore.js';
3
+
4
+ class TestFrameworkEl extends LitElement {
5
+ /*
6
+ Properties
7
+ */
8
+ static properties = { };
9
+ constructor(){
10
+ super();
11
+ this.queue = [];
12
+ this.queueKeys = new Set();
13
+ this.running = false;
14
+ this.runningKey = null;
15
+ }
16
+
17
+ /*
18
+ Lifecycle
19
+ */
20
+ connectedCallback(){
21
+ super.connectedCallback();
22
+ }
23
+ disconnectedCallback(){
24
+ super.disconnectedCallback();
25
+ }
26
+
27
+ /*
28
+ Utility
29
+ */
30
+ keyFor = (task) => task.type==='test' ? `test:${task.file}::${task.name}` : `suite:${task.file}`;
31
+
32
+ /*
33
+ Public Methods
34
+ */
35
+ enqueueTest({ file, name, el }){
36
+ if(!file || !name || !el) return;
37
+ const task = { type: 'test', file, name, el };
38
+ const key = this.keyFor(task);
39
+ if(this.runningKey===key || this.queueKeys.has(key)) return;
40
+ this.queue.push(task);
41
+ this.queueKeys.add(key);
42
+ try { el.status = 'queued'; } catch {}
43
+ this.dispatchEvent(new CustomEvent('ktf:queue-updated', { detail: { length: this.queue.length } }));
44
+ this.runNext();
45
+ }
46
+
47
+ enqueueSuite({ file, testNames, el }){
48
+ if(!file || !el) return;
49
+ const names = Array.isArray(testNames)?testNames:[];
50
+ const task = { type: 'suite', file, testNames: names, el };
51
+ const key = this.keyFor(task);
52
+ if(this.runningKey===key || this.queueKeys.has(key)) return;
53
+ this.queue.push(task);
54
+ this.queueKeys.add(key);
55
+ try {
56
+ el.status = 'queued';
57
+ const tests = Array.from(el.querySelectorAll('ktf-test'));
58
+ for(const t of tests){ t.status = 'queued'; }
59
+ } catch {}
60
+ this.dispatchEvent(new CustomEvent('ktf:queue-updated', { detail: { length: this.queue.length } }));
61
+ this.runNext();
62
+ }
63
+
64
+ runAllSuites(){
65
+ const suites = Array.from(this.querySelectorAll('ktf-test-suite'));
66
+ for(const s of suites){ this.enqueueSuite({ file: s.file, testNames: s.testNames, el: s }); }
67
+ };
68
+
69
+ /*
70
+ Internal Queue Runner
71
+ */
72
+ runNext = async () => {
73
+ if(this.running) return;
74
+ const task = this.queue.shift();
75
+ if(!task) return;
76
+ this.running = true;
77
+ const key = this.keyFor(task);
78
+ this.queueKeys.delete(key);
79
+ this.runningKey = key;
80
+ this.dispatchEvent(new CustomEvent('ktf:queue-updated', { detail: { length: this.queue.length } }));
81
+ try {
82
+ if(task.type==='test') await this.runTest(task);
83
+ else await this.runSuite(task);
84
+ } catch (e) {
85
+ console.error(e);
86
+ } finally {
87
+ this.running = false;
88
+ this.runningKey = null;
89
+ this.dispatchEvent(new CustomEvent('ktf:queue-updated', { detail: { length: this.queue.length } }));
90
+ if(this.queue.length) this.runNext();
91
+ }
92
+ };
93
+
94
+ /*
95
+ Runners
96
+ */
97
+ runTest = async ({ file, name, el }) => {
98
+ try {
99
+ el.status = 'running';
100
+ const logsEl = el.renderRoot?.getElementById('logs');
101
+ if (logsEl) logsEl.clear();
102
+ } catch {}
103
+ const { showBrowser, delayMs } = getSettings();
104
+ const resp = await fetch(`/runTest?testFile=${encodeURIComponent(file)}&testNames=${encodeURIComponent(name)}&showBrowser=${!!showBrowser}&delayMs=${Number(delayMs||0)}`);
105
+ let data = null;
106
+ try { data = await resp.json(); } catch {}
107
+ if(!resp.ok || (data && data.error)){
108
+ const msg = data?.details || data?.error || `${resp.status} ${resp.statusText}`;
109
+ try {
110
+ const logsEl = el.renderRoot?.getElementById('logs');
111
+ if (logsEl) logsEl.addLog({ message: `Error running test: ${msg}`, type: 'error', level: 3 });
112
+ el.status = 'fail';
113
+ } catch {}
114
+ return;
115
+ }
116
+ const fileResults = data?.results || {};
117
+ const beforeAllLogs = Array.isArray(fileResults.beforeAllLogs) ? fileResults.beforeAllLogs : [];
118
+ const afterAllLogs = Array.isArray(fileResults.afterAllLogs) ? fileResults.afterAllLogs : [];
119
+ const testInfo = fileResults.tests?.[name] || null;
120
+ const testLogs = Array.isArray(testInfo?.logs) ? testInfo.logs : [];
121
+ try {
122
+ el.status = testInfo?.passed ? 'pass' : 'fail';
123
+ const logsEl = el.renderRoot?.getElementById('logs');
124
+ if (logsEl) {
125
+ const heading = msg => ({ message: msg, type: 'progress', level: 3 });
126
+ const batch = [];
127
+ if (beforeAllLogs.length) batch.push(heading('== Before All Logs =='), ...beforeAllLogs);
128
+ batch.push(...testLogs);
129
+ if (afterAllLogs.length) batch.push(heading('== After All Logs =='), ...afterAllLogs);
130
+ const testsMap = fileResults.tests || {};
131
+ const names = Object.keys(testsMap);
132
+ const total = names.length;
133
+ const passed = names.reduce((acc, n) => acc + (testsMap[n]?.passed ? 1 : 0), 0);
134
+ const failed = total - passed;
135
+ if (total > 0) {
136
+ batch.push(
137
+ { message: '=== Test Summary ====', type: 'summary', level: 1 },
138
+ { message: `Total Tests: ${total}`, type: 'summary', level: 1 },
139
+ { message: `Passed: ${passed}`, type: passed > 0 ? 'pass' : 'log', level: 1 },
140
+ { message: `Failed: ${failed}`, type: failed > 0 ? 'fail' : 'log', level: 1 }
141
+ );
142
+ batch.push(
143
+ failed === 0
144
+ ? { message: 'All tests passed!', type: 'pass', level: 1 }
145
+ : { message: 'Some tests failed. See details above.', type: 'fail', level: 1 }
146
+ );
147
+ }
148
+ logsEl.addLog(...batch);
149
+ }
150
+ } catch {}
151
+ };
152
+
153
+ runSuite = async ({ file, testNames, el }) => {
154
+ const isUniversal = el.hasAttribute('universal');
155
+
156
+ try {
157
+ el.status = 'running';
158
+ const tests = Array.from(el.querySelectorAll('ktf-test'));
159
+ for(const t of tests){
160
+ t.status = 'running';
161
+ const logsEl = t.renderRoot?.getElementById('logs');
162
+ if(logsEl) logsEl.clear();
163
+ }
164
+ const fileLogsEl = el.renderRoot?.getElementById('fileLogs');
165
+ if(fileLogsEl) fileLogsEl.clear();
166
+ } catch {}
167
+
168
+ const { showBrowser, delayMs } = getSettings();
169
+
170
+ // For universal tests, run both environments
171
+ if (isUniversal && file.endsWith('.test.js')) {
172
+ try {
173
+ // Run both Node and Browser tests
174
+ const [nodeResp, browserResp] = await Promise.all([
175
+ fetch(`/runTest?testFile=${encodeURIComponent(file)}&environment=node&showBrowser=false&delayMs=${Number(delayMs||0)}`),
176
+ fetch(`/runTest?testFile=${encodeURIComponent(file)}&environment=browser&showBrowser=${!!showBrowser}&delayMs=${Number(delayMs||0)}`)
177
+ ]);
178
+
179
+ let nodeData = null, browserData = null;
180
+ try { nodeData = await nodeResp.json(); } catch {}
181
+ try { browserData = await browserResp.json(); } catch {}
182
+
183
+ const fileLogsEl = el.renderRoot?.getElementById('fileLogs');
184
+ const heading = msg => ({ message: msg, type: 'progress', level: 3 });
185
+
186
+ // Combine results from both environments
187
+ let combinedResults = { tests: {}, beforeAllLogs: [], afterAllLogs: [] };
188
+ let hasErrors = false;
189
+
190
+ if (!nodeResp.ok || (nodeData && nodeData.error)) {
191
+ hasErrors = true;
192
+ const msg = nodeData?.details || nodeData?.error || `Node: ${nodeResp.status} ${nodeResp.statusText}`;
193
+ if (fileLogsEl) fileLogsEl.addLog({ message: `Error running Node tests: ${msg}`, type: 'error', level: 3 });
194
+ } else {
195
+ const nodeResults = nodeData?.results || {};
196
+ if (fileLogsEl && nodeResults.beforeAllLogs?.length) {
197
+ fileLogsEl.addLog(heading('== Node Before All Logs =='), ...nodeResults.beforeAllLogs);
198
+ }
199
+ // Merge node test results with 'Node: ' prefix
200
+ Object.entries(nodeResults.tests || {}).forEach(([testName, testInfo]) => {
201
+ combinedResults.tests[testName] = {
202
+ ...testInfo,
203
+ logs: [
204
+ heading('== Node Environment =='),
205
+ ...(testInfo.logs || [])
206
+ ]
207
+ };
208
+ });
209
+ }
210
+
211
+ if (!browserResp.ok || (browserData && browserData.error)) {
212
+ hasErrors = true;
213
+ const msg = browserData?.details || browserData?.error || `Browser: ${browserResp.status} ${browserResp.statusText}`;
214
+ if (fileLogsEl) fileLogsEl.addLog({ message: `Error running Browser tests: ${msg}`, type: 'error', level: 3 });
215
+ } else {
216
+ const browserResults = browserData?.results || {};
217
+ if (fileLogsEl && browserResults.beforeAllLogs?.length) {
218
+ fileLogsEl.addLog(heading('== Browser Before All Logs =='), ...browserResults.beforeAllLogs);
219
+ }
220
+ // Merge browser test results
221
+ Object.entries(browserResults.tests || {}).forEach(([testName, testInfo]) => {
222
+ if (combinedResults.tests[testName]) {
223
+ // Combine with existing node results
224
+ combinedResults.tests[testName].logs.push(
225
+ heading('== Browser Environment =='),
226
+ ...(testInfo.logs || [])
227
+ );
228
+ // Test passes only if both environments pass
229
+ combinedResults.tests[testName].passed = combinedResults.tests[testName].passed && testInfo.passed;
230
+ } else {
231
+ // Browser-only result (shouldn't happen for universal tests, but handle it)
232
+ combinedResults.tests[testName] = {
233
+ ...testInfo,
234
+ logs: [
235
+ heading('== Browser Environment =='),
236
+ ...(testInfo.logs || [])
237
+ ]
238
+ };
239
+ }
240
+ });
241
+ }
242
+
243
+ if (hasErrors) {
244
+ try {
245
+ const tests = Array.from(el.querySelectorAll('ktf-test'));
246
+ for(const t of tests){ t.status = 'fail'; }
247
+ el.status = 'fail';
248
+ } catch {}
249
+ return;
250
+ }
251
+
252
+ // Update test elements with combined results
253
+ const testsMap = combinedResults.tests;
254
+ try {
255
+ const tests = Array.from(el.querySelectorAll('ktf-test'));
256
+ for(const t of tests){
257
+ const name = t.name;
258
+ const info = testsMap[name];
259
+ const logsEl = t.renderRoot?.getElementById('logs');
260
+ if(logsEl && info){
261
+ logsEl.addLog(...(info.logs || []));
262
+ }
263
+ t.status = info?.passed ? 'pass' : 'fail';
264
+ }
265
+
266
+ if(fileLogsEl){
267
+ const names = Object.keys(testsMap);
268
+ const total = names.length;
269
+ const passed = names.reduce((acc, n) => acc + (testsMap[n]?.passed ? 1 : 0), 0);
270
+ const failed = total - passed;
271
+ if(total>0){
272
+ fileLogsEl.addLog(
273
+ { message: '=== Universal Test Summary (Node + Browser) ====', type: 'summary', level: 1 },
274
+ { message: `Total Tests: ${total}`, type: 'summary', level: 1 },
275
+ { message: `Passed in Both Environments: ${passed}`, type: passed>0 ? 'pass' : 'log', level: 1 },
276
+ { message: `Failed in At Least One Environment: ${failed}`, type: failed>0 ? 'fail' : 'log', level: 1 },
277
+ failed===0
278
+ ? { message: 'All tests passed in both environments!', type: 'pass', level: 1 }
279
+ : { message: 'Some tests failed. See details above.', type: 'fail', level: 1 }
280
+ );
281
+ }
282
+ }
283
+
284
+ // Recalculate suite status
285
+ const testEls = el.querySelectorAll('ktf-test');
286
+ const statuses = Array.from(testEls).map(x=>x.status);
287
+ let suiteStatus = 'notran';
288
+ if(statuses.includes('running')) suiteStatus = 'running';
289
+ else if(statuses.includes('fail')) suiteStatus = 'fail';
290
+ else if(statuses.length && statuses.every(s => s==='pass')) suiteStatus = 'pass';
291
+ el.status = suiteStatus;
292
+ } catch {}
293
+
294
+ } catch (error) {
295
+ console.error('Error running universal test:', error);
296
+ try {
297
+ const fileLogsEl = el.renderRoot?.getElementById('fileLogs');
298
+ if(fileLogsEl) fileLogsEl.addLog({ message: `Error running universal test: ${error.message}`, type: 'error', level: 3 });
299
+ const tests = Array.from(el.querySelectorAll('ktf-test'));
300
+ for(const t of tests){ t.status = 'fail'; }
301
+ el.status = 'fail';
302
+ } catch {}
303
+ }
304
+ return;
305
+ }
306
+
307
+ // Original single-environment logic for non-universal tests
308
+ const resp = await fetch(`/runTest?testFile=${encodeURIComponent(file)}&showBrowser=${!!showBrowser}&delayMs=${Number(delayMs||0)}`);
309
+ let data = null;
310
+ try { data = await resp.json(); } catch {}
311
+ if(!resp.ok || (data && data.error)){
312
+ const msg = data?.details || data?.error || `${resp.status} ${resp.statusText}`;
313
+ try {
314
+ const fileLogsEl = el.renderRoot?.getElementById('fileLogs');
315
+ if(fileLogsEl) fileLogsEl.addLog({ message: `Error running tests: ${msg}`, type: 'error', level: 3 });
316
+ const tests = Array.from(el.querySelectorAll('ktf-test'));
317
+ for(const t of tests){ t.status = 'fail'; }
318
+ el.status = 'fail';
319
+ } catch {}
320
+ return;
321
+ }
322
+ const results = data?.results || {};
323
+ const beforeAllLogs = Array.isArray(results.beforeAllLogs) ? results.beforeAllLogs : [];
324
+ const afterAllLogs = Array.isArray(results.afterAllLogs) ? results.afterAllLogs : [];
325
+ const testsMap = results.tests || {};
326
+ try {
327
+ const fileLogsEl = el.renderRoot?.getElementById('fileLogs');
328
+ const heading = msg => ({ message: msg, type: 'progress', level: 3 });
329
+ if(fileLogsEl && beforeAllLogs.length){ fileLogsEl.addLog(heading('== Before All Logs =='), ...beforeAllLogs); }
330
+ const tests = Array.from(el.querySelectorAll('ktf-test'));
331
+ for(const t of tests){
332
+ const name = t.name;
333
+ const info = testsMap[name];
334
+ const logsEl = t.renderRoot?.getElementById('logs');
335
+ if(logsEl){
336
+ const batch = [];
337
+ if(Array.isArray(info?.logs)) batch.push(...info.logs);
338
+ logsEl.addLog(...batch);
339
+ }
340
+ t.status = info?.passed ? 'pass' : 'fail';
341
+ }
342
+ if(fileLogsEl){
343
+ if(afterAllLogs.length) fileLogsEl.addLog(heading('== After All Logs =='), ...afterAllLogs);
344
+ const names = Object.keys(testsMap);
345
+ const total = names.length;
346
+ const passed = names.reduce((acc, n) => acc + (testsMap[n]?.passed ? 1 : 0), 0);
347
+ const failed = total - passed;
348
+ if(total>0){
349
+ fileLogsEl.addLog(
350
+ { message: '=== Test Summary ====', type: 'summary', level: 1 },
351
+ { message: `Total Tests: ${total}`, type: 'summary', level: 1 },
352
+ { message: `Passed: ${passed}`, type: passed>0 ? 'pass' : 'log', level: 1 },
353
+ { message: `Failed: ${failed}`, type: failed>0 ? 'fail' : 'log', level: 1 },
354
+ failed===0
355
+ ? { message: 'All tests passed!', type: 'pass', level: 1 }
356
+ : { message: 'Some tests failed. See details above.', type: 'fail', level: 1 }
357
+ );
358
+ }
359
+ }
360
+ // Recalculate suite status
361
+ const testEls = el.querySelectorAll('ktf-test');
362
+ const statuses = Array.from(testEls).map(x=>x.status);
363
+ let suiteStatus = 'notran';
364
+ if(statuses.includes('running')) suiteStatus = 'running';
365
+ else if(statuses.includes('fail')) suiteStatus = 'fail';
366
+ else if(statuses.length && statuses.every(s => s==='pass')) suiteStatus = 'pass';
367
+ el.status = suiteStatus;
368
+ } catch {}
369
+ };
370
+
371
+ /*
372
+ Rendering
373
+ */
374
+ render(){ return html`<slot></slot>`; }
375
+ }
376
+
377
+ customElements.define('ktf-test-framework', TestFrameworkEl);
378
+ export default TestFrameworkEl;