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,49 @@
1
+ // Browser tests for Collapsible component
2
+
3
+ const nextTick = () => new Promise(r => setTimeout(r));
4
+
5
+ export const beforeEach = async () => {
6
+ document.body.innerHTML = '';
7
+ try { localStorage.removeItem('ktf_settings'); } catch {}
8
+ };
9
+
10
+ export default {
11
+ 'Collapsible toggles content visibility': async ({ pass, fail, log }) => {
12
+ try {
13
+ await import('/gui/components/Collapsible.js');
14
+ const el = document.createElement('ktf-collapsible');
15
+ el.appendChild(document.createElement('div')).textContent = 'Content';
16
+ document.body.appendChild(el);
17
+ await nextTick();
18
+ const btn = el.shadowRoot.querySelector('button.title');
19
+ const openedBefore = el.hasAttribute('opened') || el.opened;
20
+ const hasSlotBefore = !!el.shadowRoot.querySelector('div.bt');
21
+ log(`Before toggle — opened:${openedBefore} slot:${hasSlotBefore}`);
22
+ btn.click();
23
+ await nextTick();
24
+ const openedAfter = el.opened === true;
25
+ const hasSlotAfter = !!el.shadowRoot.querySelector('div.bt');
26
+ log(`After toggle — opened:${openedAfter} slot:${hasSlotAfter}`);
27
+ if (!openedBefore && !hasSlotBefore && openedAfter && hasSlotAfter) pass('Collapsible opened and revealed slotted content');
28
+ else fail('Collapsible did not toggle as expected');
29
+ } catch (e) { fail(e.stack || String(e)); }
30
+ },
31
+
32
+ 'Collapsible respects opened attribute and can close': async ({ pass, fail, log }) => {
33
+ try {
34
+ await import('/gui/components/Collapsible.js');
35
+ const el = document.createElement('ktf-collapsible');
36
+ el.setAttribute('opened', '');
37
+ el.appendChild(document.createElement('div')).textContent = 'Content';
38
+ document.body.appendChild(el);
39
+ await nextTick();
40
+ const btn = el.shadowRoot.querySelector('button.title');
41
+ const initiallyOpen = el.opened === true;
42
+ log(`Initially open:${initiallyOpen}`);
43
+ btn.click();
44
+ await nextTick();
45
+ const afterClose = el.opened === false;
46
+ if (initiallyOpen && afterClose) pass('Opened attribute respected and component can close'); else fail('opened attribute not respected or cannot close');
47
+ } catch (e) { fail(e.stack || String(e)); }
48
+ }
49
+ };
@@ -0,0 +1,141 @@
1
+ import './Counter.js';
2
+
3
+ const wait = ms => new Promise(r=>setTimeout(r,ms));
4
+
5
+ export const beforeAll = (log) => {
6
+ log('Setting up browser test environment for Counter component');
7
+ }
8
+
9
+ export const beforeEach = (log) => {
10
+ log('Setting up test fixture - creating counter element');
11
+ const $counter = document.createElement('my-counter');
12
+ $counter.id = 'test-counter';
13
+ document.body.appendChild($counter);
14
+ }
15
+
16
+ export const afterEach = (log) => {
17
+ log('Cleaning up - removing counter element');
18
+ const $counter = document.getElementById('test-counter');
19
+ if ($counter) {
20
+ $counter.remove();
21
+ }
22
+ }
23
+
24
+ export const afterAll = (log) => {
25
+ log('Cleaning up browser test environment');
26
+ }
27
+
28
+ export default {
29
+ 'Counter component should be defined': ({pass, fail, log}) => {
30
+ log('Checking if my-counter element is defined');
31
+ if (window.customElements.get('my-counter')) {
32
+ pass('Counter component is properly registered');
33
+ } else {
34
+ fail('Counter component is not registered as a custom element');
35
+ }
36
+ },
37
+
38
+ 'Counter should render with initial count of 0': ({pass, fail, log}) => {
39
+ const $counter = document.getElementById('test-counter');
40
+ log('Checking initial counter value');
41
+
42
+ if (!$counter) {
43
+ fail('Counter element not found');
44
+ return;
45
+ }
46
+
47
+ // Check shadow DOM content
48
+ const countDisplay = $counter.shadowRoot.querySelector('div');
49
+ if (countDisplay && countDisplay.textContent.includes('Count: 0')) {
50
+ pass('Counter initializes with count of 0');
51
+ } else {
52
+ fail(`Counter does not show the expected initial value. Found: ${countDisplay?.textContent}`);
53
+ }
54
+ },
55
+
56
+ 'Counter should increment when clicked': async ({pass, fail, log}) => {
57
+ const counter = document.getElementById('test-counter');
58
+ log('Testing click increment functionality');
59
+
60
+ if (!counter) {
61
+ fail('Counter element not found');
62
+ return;
63
+ }
64
+
65
+ const button = counter.shadowRoot.querySelector('button');
66
+
67
+ // Initial state check
68
+ let countDisplay = counter.shadowRoot.querySelector('div');
69
+ const initialText = countDisplay.textContent;
70
+ log(`Initial state: ${initialText}`);
71
+
72
+ // Trigger click event
73
+ button.click();
74
+
75
+ // Check updated state
76
+ countDisplay = counter.shadowRoot.querySelector('div');
77
+ const updatedText = countDisplay.textContent;
78
+ log(`After click: ${updatedText}`);
79
+
80
+ if (updatedText.includes('Count: 1')) {
81
+ pass('Counter successfully incremented to 1 after click');
82
+ } else {
83
+ fail(`Counter did not increment properly. Expected 'Count: 1' but found '${updatedText}'`);
84
+ }
85
+ },
86
+
87
+ 'Counter should increment multiple times': async ({pass, fail, log}) => {
88
+ const counter = document.getElementById('test-counter');
89
+ log('Testing multiple increments');
90
+
91
+ if (!counter) {
92
+ fail('Counter element not found');
93
+ return;
94
+ }
95
+
96
+ const button = counter.shadowRoot.querySelector('button');
97
+
98
+ // Simply click the button multiple times
99
+ for (let i = 0; i < 3; i++) {
100
+ button.click();
101
+ await wait(100);
102
+ log(`After click ${i+1}, counter shows: ${counter.shadowRoot.querySelector('div').textContent}`);
103
+ }
104
+
105
+ const countDisplay = counter.shadowRoot.querySelector('div');
106
+ const countText = countDisplay.textContent;
107
+
108
+ if (countText.includes('Count: 3')) {
109
+ pass('Counter successfully incremented multiple times to 3');
110
+ } else {
111
+ fail(`Counter did not increment correctly after multiple clicks. Expected 'Count: 3' but found '${countText}'`);
112
+ }
113
+ },
114
+
115
+ 'Counter should maintain internal state': ({pass, fail, log}) => {
116
+ const counter = document.getElementById('test-counter');
117
+ log('Testing internal state management');
118
+
119
+ if (!counter) {
120
+ fail('Counter element not found');
121
+ return;
122
+ }
123
+
124
+ // Check initial internal state
125
+ if (counter.count === 0) {
126
+ // Manually update the internal state
127
+ counter.count = 42;
128
+ counter.render();
129
+
130
+ // Verify the rendered output matches the internal state
131
+ const countDisplay = counter.shadowRoot.querySelector('div');
132
+ if (countDisplay.textContent.includes('Count: 42')) {
133
+ pass('Counter maintains and displays internal state correctly');
134
+ } else {
135
+ fail(`Counter does not display the expected state. Expected 'Count: 42' but found '${countDisplay.textContent}'`);
136
+ }
137
+ } else {
138
+ fail(`Initial counter state is not 0, found: ${counter.count}`);
139
+ }
140
+ }
141
+ };
@@ -0,0 +1,103 @@
1
+ // Example Node test file for Kempo Testing Framework
2
+ // This demonstrates the basic structure and features
3
+
4
+ /* Export the optional lifecycle callbacks */
5
+ export const beforeAll = async (log) => {
6
+ log('Setting up Node test environment...');
7
+ // Setup that runs once before all tests
8
+ }
9
+
10
+ export const beforeEach = async (log) => {
11
+ log('Setting up for each test');
12
+ // Setup that runs before each individual test
13
+ }
14
+
15
+ export const afterEach = async (log) => {
16
+ log('Cleaning up after each test');
17
+ // Cleanup that runs after each individual test
18
+ }
19
+
20
+ export const afterAll = async (log) => {
21
+ log('Cleaning up Node test environment...');
22
+ // Cleanup that runs once after all tests
23
+ }
24
+
25
+ /* Export the Tests */
26
+ export default {
27
+ 'should handle basic string operations': async ({pass, fail, log}) => {
28
+ const input = 'hello world';
29
+ const expected = 'HELLO WORLD';
30
+ const result = input.toUpperCase();
31
+
32
+ log(`Testing string conversion: "${input}" -> "${result}"`);
33
+
34
+ if (result === expected) {
35
+ log('✓ String conversion works correctly');
36
+ pass('String uppercase conversion test passed');
37
+ } else {
38
+ fail(`Expected "${expected}", got "${result}"`);
39
+ }
40
+ },
41
+
42
+ 'should validate array operations': async ({pass, fail, log}) => {
43
+ const numbers = [1, 2, 3, 4, 5];
44
+ const doubled = numbers.map(n => n * 2);
45
+ const expected = [2, 4, 6, 8, 10];
46
+
47
+ log(`Testing array mapping: [${numbers}] -> [${doubled}]`);
48
+
49
+ if (JSON.stringify(doubled) === JSON.stringify(expected)) {
50
+ log('✓ Array mapping works correctly');
51
+ pass('Array mapping test passed');
52
+ } else {
53
+ fail(`Expected [${expected}], got [${doubled}]`);
54
+ }
55
+ },
56
+
57
+ 'should handle async operations': async ({pass, fail, log}) => {
58
+ log('Testing async operation...');
59
+
60
+ const delay = (ms) => new Promise(resolve => setTimeout(resolve, ms));
61
+ const start = Date.now();
62
+
63
+ await delay(100);
64
+
65
+ const elapsed = Date.now() - start;
66
+ log(`Async delay completed in ${elapsed}ms`);
67
+
68
+ if (elapsed >= 100 && elapsed < 200) {
69
+ log('✓ Async operation timing is correct');
70
+ pass('Async operation test passed');
71
+ } else {
72
+ fail(`Expected delay of ~100ms, got ${elapsed}ms`);
73
+ }
74
+ },
75
+
76
+ 'should handle error conditions': async ({pass, fail, log}) => {
77
+ log('Testing error handling...');
78
+
79
+ try {
80
+ JSON.parse('invalid json');
81
+ fail('Should have thrown an error for invalid JSON');
82
+ } catch (error) {
83
+ log(`✓ Caught expected error: ${error.message}`);
84
+ pass('Error handling test passed');
85
+ }
86
+ },
87
+
88
+ 'should work with Node.js specific features': async ({pass, fail, log}) => {
89
+ // Test Node.js specific functionality
90
+ const process_exists = typeof process !== 'undefined';
91
+ const has_global = typeof global !== 'undefined';
92
+
93
+ log(`Process object exists: ${process_exists}`);
94
+ log(`Global object available: ${has_global}`);
95
+
96
+ if (process_exists && has_global) {
97
+ log('✓ Node.js environment detected correctly');
98
+ pass('Node.js environment test passed');
99
+ } else {
100
+ fail('Node.js environment features not available');
101
+ }
102
+ }
103
+ };
@@ -0,0 +1,54 @@
1
+ // Browser tests for Icon component
2
+
3
+ const nextTick = () => new Promise(r => setTimeout(r));
4
+
5
+ export const beforeEach = async () => {
6
+ document.body.innerHTML = '';
7
+ try { localStorage.removeItem('ktf_settings'); } catch {}
8
+ };
9
+
10
+ export default {
11
+ 'Icon loads via fetch and caches results': async ({ pass, fail, log }) => {
12
+ try {
13
+ await import('/gui/components/Icon.js');
14
+ const originalFetch = window.fetch;
15
+ let calls = 0;
16
+ window.fetch = async (url) => {
17
+ calls++;
18
+ if (String(url).endsWith('/icons/test.svg')) {
19
+ return { ok: true, text: async () => '<svg><g id="ok"/></svg>' };
20
+ }
21
+ return { ok: false, text: async () => '' };
22
+ };
23
+ const a = document.createElement('ktf-icon');
24
+ a.name = 'test';
25
+ document.body.appendChild(a);
26
+ await nextTick(); await nextTick();
27
+ const firstLoaded = a.shadowRoot.innerHTML.includes('id="ok"');
28
+ log(`Network calls after first icon: ${calls}`);
29
+ window.fetch = async () => { throw new Error('blocked'); };
30
+ const b = document.createElement('ktf-icon');
31
+ b.name = 'test';
32
+ document.body.appendChild(b);
33
+ await nextTick(); await nextTick();
34
+ const secondLoaded = b.shadowRoot.innerHTML.includes('id="ok"');
35
+ window.fetch = originalFetch;
36
+ if (firstLoaded && secondLoaded) pass('Icon fetched once and loaded cached SVG subsequently'); else fail('Icon did not cache/fetch as expected');
37
+ } catch (e) { fail(e.stack || String(e)); }
38
+ },
39
+
40
+ 'Icon falls back when fetch fails': async ({ pass, fail }) => {
41
+ try {
42
+ await import('/gui/components/Icon.js');
43
+ const originalFetch = window.fetch;
44
+ window.fetch = async () => ({ ok: false, text: async () => '' });
45
+ const a = document.createElement('ktf-icon');
46
+ a.name = 'does-not-exist';
47
+ document.body.appendChild(a);
48
+ await nextTick(); await nextTick();
49
+ const hasSvg = a.shadowRoot.innerHTML.includes('<svg');
50
+ window.fetch = originalFetch;
51
+ if (hasSvg) pass('Fallback SVG rendered when fetch failed'); else fail('Fallback SVG missing when fetch failed');
52
+ } catch (e) { fail(e.stack || String(e)); }
53
+ }
54
+ };
@@ -0,0 +1,47 @@
1
+ // Browser tests for Logs component
2
+
3
+ const nextTick = () => new Promise(r => setTimeout(r));
4
+
5
+ export const beforeEach = async () => {
6
+ document.body.innerHTML = '';
7
+ try { localStorage.removeItem('ktf_settings'); } catch {}
8
+ };
9
+
10
+ export default {
11
+ 'Logs filters by level and reacts to settings changes': async ({ pass, fail, log }) => {
12
+ try {
13
+ const { set: setSettings } = (await import('/gui/components/settingsStore.js')).default?.KTF_SETTINGS || window.KTF_SETTINGS || { set: () => {} };
14
+ await import('/gui/components/Logs.js');
15
+ const logs = document.createElement('ktf-logs');
16
+ document.body.appendChild(logs);
17
+ await nextTick();
18
+ logs.addLog({ message: 'L0', level: 0 }, { message: 'L2', level: 2 }, { message: 'L4', level: 4 });
19
+ await nextTick();
20
+ const text1 = logs.shadowRoot.textContent;
21
+ const cond1 = text1.includes('L0') && text1.includes('L2') && !text1.includes('L4');
22
+ log(`Initial filter text: ${text1.replace(/\s+/g,' ').trim()}`);
23
+ window.KTF_SETTINGS.set({ logLevel: 4 });
24
+ await nextTick();
25
+ const text2 = logs.shadowRoot.textContent;
26
+ const cond2 = text2.includes('L4');
27
+ if (cond1 && cond2) pass('Logs filtered by level and reacted to settings change'); else fail('Filtering by level or settings reaction failed');
28
+ } catch (e) { fail(e.stack || String(e)); }
29
+ },
30
+
31
+ 'Logs clear removes previous entries': async ({ pass, fail, log }) => {
32
+ try {
33
+ await import('/gui/components/Logs.js');
34
+ const logs = document.createElement('ktf-logs');
35
+ document.body.appendChild(logs);
36
+ await nextTick();
37
+ logs.addLog({ message: 'A', level: 2 }, { message: 'B', level: 2 });
38
+ await nextTick();
39
+ const before = logs.shadowRoot.textContent.includes('A') && logs.shadowRoot.textContent.includes('B');
40
+ logs.clear();
41
+ await nextTick();
42
+ const after = logs.shadowRoot.textContent.trim().length === 0 || !logs.shadowRoot.textContent.includes('A');
43
+ log(`Before clear contained A/B: ${before}, after clear emptyish: ${after}`);
44
+ if (before && after) pass('Logs.clear() removed previous entries'); else fail('clear did not remove entries');
45
+ } catch (e) { fail(e.stack || String(e)); }
46
+ }
47
+ };
@@ -0,0 +1,48 @@
1
+ // Browser tests for SettingCheckbox component
2
+
3
+ const nextTick = () => new Promise(r => setTimeout(r));
4
+
5
+ export const beforeEach = async () => {
6
+ document.body.innerHTML = '';
7
+ try { localStorage.removeItem('ktf_settings'); } catch {}
8
+ };
9
+
10
+ export default {
11
+ 'SettingCheckbox reflects and updates settings': async ({ pass, fail, log }) => {
12
+ try {
13
+ await import('/gui/components/settingsStore.js');
14
+ await import('/gui/components/SettingCheckbox.js');
15
+ window.KTF_SETTINGS.set({ showBrowser: false });
16
+ const el = document.createElement('ktf-setting-checkbox');
17
+ el.name = 'showBrowser';
18
+ el.label = 'Show Browser';
19
+ document.body.appendChild(el);
20
+ await nextTick();
21
+ const input = el.querySelector('input[type="checkbox"]');
22
+ const before = input.checked === false;
23
+ input.checked = true; input.dispatchEvent(new Event('change'));
24
+ await nextTick();
25
+ const after = window.KTF_SETTINGS.get().showBrowser === true;
26
+ log(`Checkbox before:${before} after:${after}`);
27
+ if (before && after) pass('SettingCheckbox updated setting from false to true'); else fail('SettingCheckbox did not update setting');
28
+ } catch (e) { fail(e.stack || String(e)); }
29
+ },
30
+
31
+ 'SettingCheckbox unchecks updates back to false': async ({ pass, fail, log }) => {
32
+ try {
33
+ await import('/gui/components/settingsStore.js');
34
+ await import('/gui/components/SettingCheckbox.js');
35
+ window.KTF_SETTINGS.set({ showBrowser: true });
36
+ const el = document.createElement('ktf-setting-checkbox');
37
+ el.name = 'showBrowser';
38
+ document.body.appendChild(el);
39
+ await nextTick();
40
+ const input = el.querySelector('input[type="checkbox"]');
41
+ input.checked = false; input.dispatchEvent(new Event('change'));
42
+ await nextTick();
43
+ const after = window.KTF_SETTINGS.get().showBrowser === false;
44
+ log(`Setting after uncheck:${after}`);
45
+ if (after) pass('SettingCheckbox updated setting back to false'); else fail('Uncheck did not update setting');
46
+ } catch (e) { fail(e.stack || String(e)); }
47
+ }
48
+ };
@@ -0,0 +1,54 @@
1
+ // Browser tests for SettingNumber component
2
+
3
+ const nextTick = () => new Promise(r => setTimeout(r));
4
+
5
+ export const beforeEach = async () => {
6
+ document.body.innerHTML = '';
7
+ try { localStorage.removeItem('ktf_settings'); } catch {}
8
+ };
9
+
10
+ export default {
11
+ 'SettingNumber reflects and updates settings with clamping': async ({ pass, fail, log }) => {
12
+ try {
13
+ const store = await import('/gui/components/settingsStore.js');
14
+ await import('/gui/components/SettingNumber.js');
15
+ store.setSettings({ delayMs: 500 });
16
+ const el = document.createElement('ktf-setting-number');
17
+ el.name = 'delayMs';
18
+ el.label = 'Delay';
19
+ el.min = 0; el.max = 1000; el.step = 100; el.suffix = 'ms';
20
+ document.body.appendChild(el);
21
+ await nextTick();
22
+ const input = el.querySelector('input[type="number"]');
23
+ const before = String(input.value) === '500';
24
+ input.value = '1200'; input.dispatchEvent(new Event('change'));
25
+ await nextTick();
26
+ const clamped = store.getSettings().delayMs === 1000;
27
+ input.value = 'abc'; input.dispatchEvent(new Event('change'));
28
+ await nextTick();
29
+ const nanToZero = store.getSettings().delayMs === 0;
30
+ log(`before:${before} clamped:${clamped} nanToZero:${nanToZero}`);
31
+ if (before && clamped && nanToZero) pass('SettingNumber reflected, clamped, and sanitized values');
32
+ else fail('SettingNumber did not reflect/clamp/sanitize as expected');
33
+ } catch (e) { fail(e.stack || String(e)); }
34
+ },
35
+
36
+ 'SettingNumber input syncs when settings change externally': async ({ pass, fail }) => {
37
+ try {
38
+ const store = await import('/gui/components/settingsStore.js');
39
+ await import('/gui/components/SettingNumber.js');
40
+ store.setSettings({ delayMs: 0 });
41
+ const el = document.createElement('ktf-setting-number');
42
+ el.name = 'delayMs';
43
+ document.body.appendChild(el);
44
+ await nextTick();
45
+ const input = el.querySelector('input[type="number"]');
46
+ const initial = String(input.value) === '0';
47
+ store.setSettings({ delayMs: 250 });
48
+ await nextTick();
49
+ const updated = String(input.value) === '250';
50
+ if (initial && updated) pass('SettingNumber stayed in sync with external setting change');
51
+ else fail('SettingNumber did not sync input after settings change');
52
+ } catch (e) { fail(e.stack || String(e)); }
53
+ }
54
+ };
@@ -0,0 +1,47 @@
1
+ // Browser tests for SettingSelect component
2
+
3
+ const nextTick = () => new Promise(r => setTimeout(r));
4
+
5
+ export const beforeEach = async () => {
6
+ document.body.innerHTML = '';
7
+ try { localStorage.removeItem('ktf_settings'); } catch {}
8
+ };
9
+
10
+ export default {
11
+ 'SettingSelect reflects and updates settings': async ({ pass, fail, log }) => {
12
+ try {
13
+ await import('/gui/components/settingsStore.js');
14
+ await import('/gui/components/SettingSelect.js');
15
+ window.KTF_SETTINGS.set({ logLevel: 2 });
16
+ const el = document.createElement('ktf-setting-select');
17
+ el.name = 'logLevel';
18
+ el.label = 'Log Level';
19
+ document.body.appendChild(el);
20
+ await nextTick();
21
+ const select = el.querySelector('select');
22
+ const before = String(select.value) === '2';
23
+ select.value = '1'; select.dispatchEvent(new Event('change'));
24
+ await nextTick();
25
+ const after = window.KTF_SETTINGS.get().logLevel === 1;
26
+ log(`Select value before:${before} after:${after}`);
27
+ if (before && after) pass('SettingSelect updated setting from 2 to 1'); else fail('SettingSelect did not update setting');
28
+ } catch (e) { fail(e.stack || String(e)); }
29
+ },
30
+
31
+ 'SettingSelect stays in sync when settings change externally': async ({ pass, fail }) => {
32
+ try {
33
+ await import('/gui/components/settingsStore.js');
34
+ await import('/gui/components/SettingSelect.js');
35
+ window.KTF_SETTINGS.set({ logLevel: 1 });
36
+ const el = document.createElement('ktf-setting-select');
37
+ el.name = 'logLevel';
38
+ document.body.appendChild(el);
39
+ await nextTick();
40
+ const select = el.querySelector('select');
41
+ window.KTF_SETTINGS.set({ logLevel: 4 });
42
+ await nextTick();
43
+ const synced = String(select.value) === '4';
44
+ if (synced) pass('SettingSelect synced after external settings change'); else fail('Select did not sync after external change');
45
+ } catch (e) { fail(e.stack || String(e)); }
46
+ }
47
+ };
@@ -0,0 +1,26 @@
1
+ // Browser tests for settingsStore helpers
2
+
3
+ const nextTick = () => new Promise(r => setTimeout(r));
4
+
5
+ export const beforeEach = async () => {
6
+ document.body.innerHTML = '';
7
+ try { localStorage.removeItem('ktf_settings'); } catch {}
8
+ };
9
+
10
+ export default {
11
+ 'settingsStore get/set persists to localStorage and dispatches event': async ({ pass, fail }) => {
12
+ try {
13
+ const store = await import('/gui/components/settingsStore.js');
14
+ const before = store.getSettings();
15
+ const p = new Promise(resolve => {
16
+ const unsub = store.subscribe((s) => { unsub(); resolve(s); });
17
+ });
18
+ store.setSettings({ theme: 'dark', logLevel: 4 });
19
+ const evtState = await p;
20
+ const raw = localStorage.getItem('ktf_settings');
21
+ const parsed = JSON.parse(raw || '{}');
22
+ const ok = evtState.theme === 'dark' && evtState.logLevel === 4 && parsed.theme === 'dark' && parsed.logLevel === 4;
23
+ ok ? pass('settingsStore persisted and dispatched change event') : fail(`state:${JSON.stringify(evtState)} raw:${raw}`);
24
+ } catch (e) { fail(e.stack || String(e)); }
25
+ }
26
+ };
@@ -0,0 +1,47 @@
1
+ import http from 'http';
2
+ import { startServer, stopServer } from '../src/browserTestServer.js';
3
+
4
+ const get = (port, path) => new Promise((resolve, reject) => {
5
+ const req = http.request({ hostname: 'localhost', port, path, method: 'GET' }, (res) => {
6
+ let data = '';
7
+ res.on('data', chunk => { data += chunk.toString(); });
8
+ res.on('end', () => resolve({ status: res.statusCode, body: data }));
9
+ });
10
+ req.on('error', reject);
11
+ req.end();
12
+ });
13
+
14
+ export default {
15
+ 'serves index html at /': async ({ pass, fail, log }) => {
16
+ const port = 3101;
17
+ try {
18
+ const url = await startServer(port);
19
+ const { status, body } = await get(port, '/');
20
+ log(`GET / -> ${status}`);
21
+ if (status === 200 && /<title>Test<\/title>/.test(body)) pass('Browser test server served index.html');
22
+ else fail(`Bad response from /: status=${status} body=${body?.slice(0,80)}`);
23
+ } catch (e) {
24
+ fail(e.stack || String(e));
25
+ } finally {
26
+ await stopServer();
27
+ }
28
+ },
29
+ 'serves test runner script and static files, 404s missing': async ({ pass, fail, log }) => {
30
+ const port = 3101;
31
+ try {
32
+ await startServer(port);
33
+ const a = await get(port, '/runTests.js');
34
+ const b = await get(port, '/tests/counter.browser-test.js');
35
+ const c = await get(port, '/does-not-exist.js');
36
+ log(`Statuses: /runTests.js=${a.status} /tests/counter.browser-test.js=${b.status} /does-not-exist.js=${c.status}`);
37
+ const ok = a.status === 200 && /export default|const wait/.test(a.body)
38
+ && b.status === 200 && /Counter component/.test(b.body)
39
+ && c.status === 404;
40
+ ok ? pass('Static and test files served; missing files 404 as expected') : fail(`Unexpected statuses: a=${a.status}, b=${b.status}, c=${c.status}`);
41
+ } catch (e) {
42
+ fail(e.stack || String(e));
43
+ } finally {
44
+ await stopServer();
45
+ }
46
+ }
47
+ };
@@ -0,0 +1,32 @@
1
+ import { spawn } from 'child_process';
2
+
3
+ const stripAnsi = (s) => s.replace(/\x1b\[[0-9;]*m/g, '');
4
+
5
+ const run = (args, timeoutMs = 10000) => new Promise((resolve) => {
6
+ const child = spawn(process.execPath, ['index.js', ...args], {
7
+ cwd: process.cwd(),
8
+ stdio: ['ignore', 'pipe', 'pipe']
9
+ });
10
+ let out = '';
11
+ const finish = () => { try { child.kill('SIGTERM'); } catch {} resolve(out); };
12
+ child.stdout.on('data', c => { out += c.toString(); });
13
+ child.stderr.on('data', c => { out += c.toString(); });
14
+ child.on('exit', finish);
15
+ setTimeout(finish, timeoutMs);
16
+ });
17
+
18
+ export default {
19
+ 'passes delay to browser runner and prints delay logs in verbose': async ({ pass, fail }) => {
20
+ const out = await run(['-b', '-w', '-l', 'verbose', '-d', '200', 'counter']);
21
+ const txt = stripAnsi(out);
22
+ const ok = /Applying pre-test browser delay: 200ms/.test(txt) && /Applying post-test browser delay: 200ms/.test(txt);
23
+ ok ? pass('CLI printed verbose delay logs for browser run') : fail(`Unexpected CLI output:\n${out}`);
24
+ },
25
+ 'node-only ignores delay and still runs filtered tests': async ({ pass, fail }) => {
26
+ const out = await run(['-n', '-l', 'minimal', 'example']);
27
+ const txt = stripAnsi(out);
28
+ const hasSummary = /=== Test Summary ===/.test(txt) && /Total Tests:\s*\d+/.test(txt);
29
+ const noBrowser = !/=== Browser Test Results ===/.test(txt) && !/Running Browser test:/.test(txt);
30
+ hasSummary && noBrowser ? pass('Node-only run produced summary without browser section') : fail(`Unexpected CLI output:\n${out}`);
31
+ }
32
+ };