kempo-testing-framework 1.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 (62) hide show
  1. package/.github/copilot-instructions.md +105 -0
  2. package/CONTRIBUTING.md +107 -0
  3. package/README.md +293 -0
  4. package/gui/components/Collapsible.js +54 -0
  5. package/gui/components/Icon.js +151 -0
  6. package/gui/components/Logs.js +73 -0
  7. package/gui/components/SettingCheckbox.js +42 -0
  8. package/gui/components/SettingNumber.js +77 -0
  9. package/gui/components/SettingSelect.js +67 -0
  10. package/gui/components/Test.js +99 -0
  11. package/gui/components/TestFramework.js +236 -0
  12. package/gui/components/TestSuite.js +181 -0
  13. package/gui/components/TestSummary.js +189 -0
  14. package/gui/components/Theme.js +40 -0
  15. package/gui/components/settingsStore.js +46 -0
  16. package/gui/icons/fail.svg +1 -0
  17. package/gui/icons/logs.svg +1 -0
  18. package/gui/icons/pass.svg +1 -0
  19. package/gui/icons/play.svg +1 -0
  20. package/gui/icons/running.svg +1 -0
  21. package/gui/icons/scheduled.svg +1 -0
  22. package/gui/icons/settings.svg +1 -0
  23. package/gui/icons/theme-auto.svg +1 -0
  24. package/gui/icons/theme-dark.svg +1 -0
  25. package/gui/icons/theme-light.svg +1 -0
  26. package/gui/index.html +108 -0
  27. package/gui/lit-all.min.js +120 -0
  28. package/index.js +122 -0
  29. package/package.json +21 -0
  30. package/src/browserTestServer.js +115 -0
  31. package/src/cli.js +198 -0
  32. package/src/findTests.js +34 -0
  33. package/src/gui.js +249 -0
  34. package/src/runBrowserTests.js +71 -0
  35. package/src/runTestFiles.js +94 -0
  36. package/src/runTests.js +83 -0
  37. package/src/utils/logLevels.js +7 -0
  38. package/test.html +23 -0
  39. package/tests/Counter.js +34 -0
  40. package/tests/cli-flags.node-test.js +54 -0
  41. package/tests/cli-loglevel.node-test.js +40 -0
  42. package/tests/collapsible.browser-test.js +49 -0
  43. package/tests/counter.browser-test.js +141 -0
  44. package/tests/example.node-test.js +103 -0
  45. package/tests/icon.browser-test.js +54 -0
  46. package/tests/logs.browser-test.js +47 -0
  47. package/tests/setting-checkbox.browser-test.js +48 -0
  48. package/tests/setting-number.browser-test.js +54 -0
  49. package/tests/setting-select.browser-test.js +47 -0
  50. package/tests/settings-store.browser-test.js +26 -0
  51. package/tests/src-browserTestServer.node-test.js +47 -0
  52. package/tests/src-cli.node-test.js +32 -0
  53. package/tests/src-findTests.node-test.js +41 -0
  54. package/tests/src-logLevels.node-test.js +29 -0
  55. package/tests/src-runBrowserTests.node-test.js +41 -0
  56. package/tests/src-runTestFiles.node-test.js +42 -0
  57. package/tests/src-runTests.node-test.js +56 -0
  58. package/tests/test-framework.browser-test.js +65 -0
  59. package/tests/test-summary.browser-test.js +78 -0
  60. package/tests/test.browser-test.js +56 -0
  61. package/tests/testfile.browser-test.js +60 -0
  62. package/tests/theme.browser-test.js +38 -0
@@ -0,0 +1,73 @@
1
+ import { LitElement, html, css } from '../lit-all.min.js';
2
+ import './Icon.js';
3
+ import './Collapsible.js';
4
+ import { getSettings, subscribe } from './settingsStore.js';
5
+
6
+ window.customElements.define('ktf-logs', class extends LitElement {
7
+ /*
8
+ Properties
9
+ */
10
+ static properties = {
11
+ level: { type: Number }
12
+ };
13
+ #logs = [];
14
+ #unsubscribe = null;
15
+ constructor(){
16
+ super();
17
+ const initial = Number(getSettings().logLevel ?? 3);
18
+ this.level = Number.isFinite(initial) ? initial : 3;
19
+ }
20
+ connectedCallback(){
21
+ super.connectedCallback();
22
+ this.#unsubscribe = subscribe(s => {
23
+ const parsed = Number(s.logLevel ?? 3);
24
+ this.level = Number.isFinite(parsed) ? parsed : 3;
25
+ });
26
+ }
27
+ disconnectedCallback(){
28
+ super.disconnectedCallback();
29
+ if (this.#unsubscribe) this.#unsubscribe();
30
+ }
31
+ get logs(){
32
+ return this.#logs;
33
+ }
34
+
35
+ /*
36
+ Methods
37
+ */
38
+ addLog = (...logs) => {
39
+ this.#logs.push(...logs);
40
+ this.requestUpdate();
41
+ };
42
+ clear = () => {
43
+ this.#logs.length = 0;
44
+ this.requestUpdate();
45
+ };
46
+
47
+ /*
48
+ Rendering
49
+ */
50
+ render(){
51
+ const filtered = this.#logs.filter(l => {
52
+ const lvl = Number.isFinite(l?.level) ? l.level : 3;
53
+ return lvl <= this.level;
54
+ });
55
+ if (!filtered.length) return html``;
56
+ return html`
57
+ <link rel="stylesheet" href="/essential.css">
58
+ <ktf-collapsible opened>
59
+ <span slot="title"><ktf-icon name="logs"></ktf-icon> Logs</span>
60
+ <pre class="bg-alt -mx -mt p mb0 rb">${filtered.map(l => html`<div class="${l?.type || 'log'}">${l?.message ?? ''}</div>`)}</pre>
61
+ </ktf-collapsible>
62
+ `;
63
+ }
64
+
65
+ static styles = css`
66
+ :host { display:block; }
67
+ .error,.fail { color: var(--tc_danger, rgb(255, 0, 51)); }
68
+ .warning { color: var(--tc_warning, #b58900); }
69
+ .pass { color: var(--tc_success, rgb(0, 136, 0)); }
70
+ .progress { color: var(--tc_muted, #6b7280); }
71
+ .summary { color: var(--tc_primary, #3366ff); }
72
+ `;
73
+ });
@@ -0,0 +1,42 @@
1
+ import { LitElement, html } from '../lit-all.min.js';
2
+ import { getSettings, setSettings, subscribe } from './settingsStore.js';
3
+
4
+ window.customElements.define('ktf-setting-checkbox', class extends LitElement {
5
+ static properties = {
6
+ name: { type: String },
7
+ label: { type: String },
8
+ checked: { state: true },
9
+ };
10
+
11
+ createRenderRoot() { return this; }
12
+
13
+ connectedCallback() {
14
+ super.connectedCallback();
15
+ this.apply(getSettings());
16
+ this.unsub = subscribe((s) => this.apply(s));
17
+ }
18
+ disconnectedCallback() {
19
+ super.disconnectedCallback();
20
+ if (this.unsub) this.unsub();
21
+ }
22
+
23
+ apply(s) {
24
+ const v = this.name ? s?.[this.name] : undefined;
25
+ this.checked = !!v;
26
+ }
27
+
28
+ onChange(e) {
29
+ const v = !!e.target.checked;
30
+ if (this.name) setSettings({ [this.name]: v });
31
+ }
32
+
33
+ render() {
34
+ const id = `setting-${this.name || 'checkbox'}`;
35
+ return html`
36
+ <div class="d-f mb" style="align-items: center">
37
+ <input id="${id}" type="checkbox" style="font-size: 1.35rem" .checked=${!!this.checked} @change=${(e) => this.onChange(e)} />
38
+ <label for="${id}" style="line-height: 1.35rem">${this.label || ''}</label>
39
+ </div>
40
+ `;
41
+ }
42
+ });
@@ -0,0 +1,77 @@
1
+ import { LitElement, html } from '../lit-all.min.js';
2
+ import { getSettings, setSettings, subscribe } from './settingsStore.js';
3
+
4
+ window.customElements.define('ktf-setting-number', class extends LitElement {
5
+ static properties = {
6
+ name: { type: String },
7
+ label: { type: String },
8
+ min: { type: Number },
9
+ max: { type: Number },
10
+ step: { type: Number },
11
+ value: { state: true },
12
+ suffix: { type: String },
13
+ };
14
+
15
+ constructor(){
16
+ super();
17
+ this.min = 0;
18
+ this.step = 100;
19
+ this.max = 600000; // 10 minutes
20
+ this.suffix = '';
21
+ }
22
+
23
+ createRenderRoot() { return this; }
24
+
25
+ connectedCallback() {
26
+ super.connectedCallback();
27
+ this.apply(getSettings());
28
+ this.unsub = subscribe((s) => this.apply(s));
29
+ }
30
+ disconnectedCallback() {
31
+ super.disconnectedCallback();
32
+ if (this.unsub) this.unsub();
33
+ }
34
+
35
+ apply(s) {
36
+ const v = this.name ? s?.[this.name] : undefined;
37
+ this.value = (typeof v === 'number' && !Number.isNaN(v)) ? v : 0;
38
+ }
39
+
40
+ onChange(e) {
41
+ let v = e.target.value;
42
+ let n = parseInt(v, 10);
43
+ if (Number.isNaN(n)) n = 0;
44
+ if (this.min !== undefined && n < this.min) n = this.min;
45
+ if (this.max !== undefined && n > this.max) n = this.max;
46
+ if (this.name) setSettings({ [this.name]: n });
47
+ }
48
+
49
+ updated(changed) {
50
+ if (changed.has('value')) {
51
+ const id = `setting-${this.name || 'number'}`;
52
+ const el = this.querySelector(`#${id}`);
53
+ if (el && String(el.value) !== String(this.value ?? '')) el.value = String(this.value ?? '');
54
+ }
55
+ }
56
+
57
+ render() {
58
+ const id = `setting-${this.name || 'number'}`;
59
+ return html`
60
+ ${this.label ? html`<label for="${id}">${this.label}</label>` : ''}
61
+ <div class="mb">
62
+ <input
63
+ id="${id}"
64
+ type="number"
65
+ min="${this.min}"
66
+ max="${this.max}"
67
+ step="${this.step}"
68
+ .value=${String(this.value ?? '')}
69
+ @change=${(e) => this.onChange(e)}
70
+ style="width: 10rem;"
71
+ class="d-ib"
72
+ />
73
+ ${this.suffix ? html`<span class="ml-xs">${this.suffix}</span>` : ''}
74
+ </div>
75
+ `;
76
+ }
77
+ });
@@ -0,0 +1,67 @@
1
+ import { LitElement, html } from '../lit-all.min.js';
2
+ import { getSettings, setSettings, subscribe } from './settingsStore.js';
3
+
4
+ const PRESETS = {
5
+ logLevel: [
6
+ { value: '0', label: 'Silent: Summary only' },
7
+ { value: '1', label: 'Minimal: Summary and test statuses' },
8
+ { value: '2', label: 'Normal: Summary, statuses, and logs for failures' },
9
+ { value: '3', label: 'Verbose: All test logs' },
10
+ { value: '4', label: 'Debug: All test logs and framework internal logs' },
11
+ ],
12
+ theme: [
13
+ { value: 'auto', label: 'Auto (system)' },
14
+ { value: 'light', label: 'Light' },
15
+ { value: 'dark', label: 'Dark' },
16
+ ],
17
+ };
18
+
19
+ window.customElements.define('ktf-setting-select', class extends LitElement {
20
+ static properties = {
21
+ name: { type: String },
22
+ label: { type: String },
23
+ value: { state: true },
24
+ };
25
+
26
+ createRenderRoot() { return this; }
27
+
28
+ connectedCallback() {
29
+ super.connectedCallback();
30
+ this.apply(getSettings());
31
+ this.unsub = subscribe((s) => this.apply(s));
32
+ }
33
+ disconnectedCallback() {
34
+ super.disconnectedCallback();
35
+ if (this.unsub) this.unsub();
36
+ }
37
+
38
+ apply(s) {
39
+ const v = this.name ? s?.[this.name] : undefined;
40
+ this.value = v !== undefined && v !== null ? String(v) : '';
41
+ }
42
+
43
+ onChange(e) {
44
+ let v = e.target.value;
45
+ if (/^-?\d+$/.test(v)) v = parseInt(v, 10);
46
+ if (this.name) setSettings({ [this.name]: v });
47
+ }
48
+
49
+ updated(changed) {
50
+ if (changed.has('value')) {
51
+ const id = `setting-${this.name || 'select'}`;
52
+ const el = this.querySelector(`#${id}`);
53
+ if (el && el.value !== String(this.value ?? '')) el.value = String(this.value ?? '');
54
+ }
55
+ }
56
+
57
+ render() {
58
+ const options = PRESETS[this.name] || [];
59
+ const id = `setting-${this.name || 'select'}`;
60
+ return html`
61
+ ${this.label ? html`<label for="${id}">${this.label}</label>` : ''}
62
+ <select id="${id}" class="mb" .value=${this.value ?? ''} @change=${(e) => this.onChange(e)}>
63
+ ${options.map(o => html`<option value="${o.value}">${o.label}</option>`)}
64
+ </select>
65
+ `;
66
+ }
67
+ });
@@ -0,0 +1,99 @@
1
+ import { LitElement, html, css } from '../lit-all.min.js';
2
+ import { statusMap } from './TestSuite.js';
3
+ import './Icon.js';
4
+ import './Logs.js';
5
+ import './Collapsible.js';
6
+ import { subscribe } from './settingsStore.js';
7
+
8
+ window.customElements.define('ktf-test', class extends LitElement {
9
+ static properties = {
10
+ file: { type: String, reflect: true },
11
+ name: { type: String, reflect: true },
12
+ status: { type: String, reflect: true }
13
+ }
14
+ #unsubscribe = null;
15
+ constructor(){
16
+ super();
17
+ this.file = '';
18
+ this.name = '';
19
+ this.status = 'notran';
20
+ }
21
+
22
+ connectedCallback(){
23
+ super.connectedCallback();
24
+ this.#unsubscribe = subscribe(() => this.requestUpdate());
25
+ }
26
+ disconnectedCallback(){
27
+ super.disconnectedCallback();
28
+ if (this.#unsubscribe) this.#unsubscribe();
29
+ }
30
+
31
+ updated(changedProps){
32
+ if(changedProps.has('status')){
33
+ this.dispatchEvent(new CustomEvent('test_status_change', {
34
+ detail: { status: this.status, file: this.file, name: this.name },
35
+ bubbles: true,
36
+ composed: true
37
+ }));
38
+ }
39
+ }
40
+
41
+ runTest = () => {
42
+ const fw = this.closest('ktf-test-framework') || document.querySelector('ktf-test-framework');
43
+ if(fw && typeof fw.enqueueTest==='function') fw.enqueueTest({ file: this.file, name: this.name, el: this });
44
+ };
45
+
46
+ render(){
47
+ if(!this.file || !this.name) return html``;
48
+ return html`
49
+ <link rel="stylesheet" href="/essential.css">
50
+ <ktf-collapsible>
51
+ <span slot="title" id="title" class="-ml">
52
+ ${this.name}
53
+ </span>
54
+ <div slot="actions">
55
+ ${this.status==='notran' ? html`
56
+ <button class="no-btn d-ib ph" @click=${this.runTest} aria-label="Run Test" ?disabled=${this.status==='queued' || this.status==='running'}>
57
+ <ktf-icon name="play"></ktf-icon>
58
+ </button>
59
+ ` : html`
60
+ <span class="d-ib ph status-color" aria-hidden="true">
61
+ <ktf-icon name="${this.status==='queued' ? 'scheduled' : this.status}" animation="${this.status === 'running' ? 'spin' : 'none'}"></ktf-icon>
62
+ </span>
63
+ `}
64
+ </div>
65
+ <div id="details">
66
+ <div id="status">
67
+ <h6>${statusMap[this.status]}</h6>
68
+ </div>
69
+ ${this.status!=='running'?html`
70
+ <button class="primary mb" @click=${this.runTest} ?disabled=${this.status==='queued'}>Run Test</button>
71
+ `:html``}
72
+ <ktf-logs id="logs"></ktf-logs>
73
+ </div>
74
+ </ktf-collapsible>
75
+ `;
76
+ }
77
+
78
+ static styles = css`
79
+ :host {
80
+ --tc_status: var(--tc_default, inherit);
81
+ }
82
+ :host([status="running"]) {
83
+ --tc_status: var(--tc_primary, #3366ff);
84
+ }
85
+ :host([status="pass"]) {
86
+ --tc_status: var(--tc_success, rgb(0, 136, 0));
87
+ }
88
+ :host([status="fail"]) {
89
+ --tc_status: var(--tc_danger, rgb(255, 0, 51));
90
+ }
91
+ #title { font-size: 1rem; font-weight: 600; }
92
+ div[slot="actions"] { font-size: 1rem; }
93
+ #title,
94
+ #status {
95
+ color: var(--tc_status);
96
+ }
97
+ .status-color { color: var(--tc_status); }
98
+ `;
99
+ });
@@ -0,0 +1,236 @@
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
+ try {
155
+ el.status = 'running';
156
+ const tests = Array.from(el.querySelectorAll('ktf-test'));
157
+ for(const t of tests){
158
+ t.status = 'running';
159
+ const logsEl = t.renderRoot?.getElementById('logs');
160
+ if(logsEl) logsEl.clear();
161
+ }
162
+ const fileLogsEl = el.renderRoot?.getElementById('fileLogs');
163
+ if(fileLogsEl) fileLogsEl.clear();
164
+ } catch {}
165
+ const { showBrowser, delayMs } = getSettings();
166
+ const resp = await fetch(`/runTest?testFile=${encodeURIComponent(file)}&showBrowser=${!!showBrowser}&delayMs=${Number(delayMs||0)}`);
167
+ let data = null;
168
+ try { data = await resp.json(); } catch {}
169
+ if(!resp.ok || (data && data.error)){
170
+ const msg = data?.details || data?.error || `${resp.status} ${resp.statusText}`;
171
+ try {
172
+ const fileLogsEl = el.renderRoot?.getElementById('fileLogs');
173
+ if(fileLogsEl) fileLogsEl.addLog({ message: `Error running tests: ${msg}`, type: 'error', level: 3 });
174
+ const tests = Array.from(el.querySelectorAll('ktf-test'));
175
+ for(const t of tests){ t.status = 'fail'; }
176
+ el.status = 'fail';
177
+ } catch {}
178
+ return;
179
+ }
180
+ const results = data?.results || {};
181
+ const beforeAllLogs = Array.isArray(results.beforeAllLogs) ? results.beforeAllLogs : [];
182
+ const afterAllLogs = Array.isArray(results.afterAllLogs) ? results.afterAllLogs : [];
183
+ const testsMap = results.tests || {};
184
+ try {
185
+ const fileLogsEl = el.renderRoot?.getElementById('fileLogs');
186
+ const heading = msg => ({ message: msg, type: 'progress', level: 3 });
187
+ if(fileLogsEl && beforeAllLogs.length){ fileLogsEl.addLog(heading('== Before All Logs =='), ...beforeAllLogs); }
188
+ const tests = Array.from(el.querySelectorAll('ktf-test'));
189
+ for(const t of tests){
190
+ const name = t.name;
191
+ const info = testsMap[name];
192
+ const logsEl = t.renderRoot?.getElementById('logs');
193
+ if(logsEl){
194
+ const batch = [];
195
+ if(Array.isArray(info?.logs)) batch.push(...info.logs);
196
+ logsEl.addLog(...batch);
197
+ }
198
+ t.status = info?.passed ? 'pass' : 'fail';
199
+ }
200
+ if(fileLogsEl){
201
+ if(afterAllLogs.length) fileLogsEl.addLog(heading('== After All Logs =='), ...afterAllLogs);
202
+ const names = Object.keys(testsMap);
203
+ const total = names.length;
204
+ const passed = names.reduce((acc, n) => acc + (testsMap[n]?.passed ? 1 : 0), 0);
205
+ const failed = total - passed;
206
+ if(total>0){
207
+ fileLogsEl.addLog(
208
+ { message: '=== Test Summary ====', type: 'summary', level: 1 },
209
+ { message: `Total Tests: ${total}`, type: 'summary', level: 1 },
210
+ { message: `Passed: ${passed}`, type: passed>0 ? 'pass' : 'log', level: 1 },
211
+ { message: `Failed: ${failed}`, type: failed>0 ? 'fail' : 'log', level: 1 },
212
+ failed===0
213
+ ? { message: 'All tests passed!', type: 'pass', level: 1 }
214
+ : { message: 'Some tests failed. See details above.', type: 'fail', level: 1 }
215
+ );
216
+ }
217
+ }
218
+ // Recalculate suite status
219
+ const testEls = el.querySelectorAll('ktf-test');
220
+ const statuses = Array.from(testEls).map(x=>x.status);
221
+ let suiteStatus = 'notran';
222
+ if(statuses.includes('running')) suiteStatus = 'running';
223
+ else if(statuses.includes('fail')) suiteStatus = 'fail';
224
+ else if(statuses.length && statuses.every(s => s==='pass')) suiteStatus = 'pass';
225
+ el.status = suiteStatus;
226
+ } catch {}
227
+ };
228
+
229
+ /*
230
+ Rendering
231
+ */
232
+ render(){ return html`<slot></slot>`; }
233
+ }
234
+
235
+ customElements.define('ktf-test-framework', TestFrameworkEl);
236
+ export default TestFrameworkEl;