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.
- package/.github/copilot-instructions.md +105 -0
- package/CONTRIBUTING.md +107 -0
- package/README.md +293 -0
- package/gui/components/Collapsible.js +54 -0
- package/gui/components/Icon.js +151 -0
- package/gui/components/Logs.js +73 -0
- package/gui/components/SettingCheckbox.js +42 -0
- package/gui/components/SettingNumber.js +77 -0
- package/gui/components/SettingSelect.js +67 -0
- package/gui/components/Test.js +99 -0
- package/gui/components/TestFramework.js +236 -0
- package/gui/components/TestSuite.js +181 -0
- package/gui/components/TestSummary.js +189 -0
- package/gui/components/Theme.js +40 -0
- package/gui/components/settingsStore.js +46 -0
- package/gui/icons/fail.svg +1 -0
- package/gui/icons/logs.svg +1 -0
- package/gui/icons/pass.svg +1 -0
- package/gui/icons/play.svg +1 -0
- package/gui/icons/running.svg +1 -0
- package/gui/icons/scheduled.svg +1 -0
- package/gui/icons/settings.svg +1 -0
- package/gui/icons/theme-auto.svg +1 -0
- package/gui/icons/theme-dark.svg +1 -0
- package/gui/icons/theme-light.svg +1 -0
- package/gui/index.html +108 -0
- package/gui/lit-all.min.js +120 -0
- package/index.js +122 -0
- package/package.json +21 -0
- package/src/browserTestServer.js +115 -0
- package/src/cli.js +198 -0
- package/src/findTests.js +34 -0
- package/src/gui.js +249 -0
- package/src/runBrowserTests.js +71 -0
- package/src/runTestFiles.js +94 -0
- package/src/runTests.js +83 -0
- package/src/utils/logLevels.js +7 -0
- package/test.html +23 -0
- package/tests/Counter.js +34 -0
- package/tests/cli-flags.node-test.js +54 -0
- package/tests/cli-loglevel.node-test.js +40 -0
- package/tests/collapsible.browser-test.js +49 -0
- package/tests/counter.browser-test.js +141 -0
- package/tests/example.node-test.js +103 -0
- package/tests/icon.browser-test.js +54 -0
- package/tests/logs.browser-test.js +47 -0
- package/tests/setting-checkbox.browser-test.js +48 -0
- package/tests/setting-number.browser-test.js +54 -0
- package/tests/setting-select.browser-test.js +47 -0
- package/tests/settings-store.browser-test.js +26 -0
- package/tests/src-browserTestServer.node-test.js +47 -0
- package/tests/src-cli.node-test.js +32 -0
- package/tests/src-findTests.node-test.js +41 -0
- package/tests/src-logLevels.node-test.js +29 -0
- package/tests/src-runBrowserTests.node-test.js +41 -0
- package/tests/src-runTestFiles.node-test.js +42 -0
- package/tests/src-runTests.node-test.js +56 -0
- package/tests/test-framework.browser-test.js +65 -0
- package/tests/test-summary.browser-test.js +78 -0
- package/tests/test.browser-test.js +56 -0
- package/tests/testfile.browser-test.js +60 -0
- package/tests/theme.browser-test.js +38 -0
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import findTests from '../src/findTests.js';
|
|
2
|
+
|
|
3
|
+
export default {
|
|
4
|
+
'finds node and browser test files with no filters': async ({ pass, fail, log }) => {
|
|
5
|
+
try {
|
|
6
|
+
const { nodeTests, browserTests } = await findTests('', '', true, true);
|
|
7
|
+
log(`Found node tests: ${nodeTests.length}, browser tests: ${browserTests.length}`);
|
|
8
|
+
const hasCounter = browserTests.some(f => f.endsWith('tests/counter.browser-test.js'));
|
|
9
|
+
const hasExample = nodeTests.some(f => f.endsWith('tests/example.node-test.js'));
|
|
10
|
+
if (hasCounter && hasExample) pass('Discovered expected canonical files (counter + example)');
|
|
11
|
+
else fail(`Missing expected files. node: ${JSON.stringify(nodeTests)}, browser: ${JSON.stringify(browserTests)}`);
|
|
12
|
+
} catch (e) {
|
|
13
|
+
fail(e.stack || String(e));
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
'honors suite filter for file substring': async ({ pass, fail, log }) => {
|
|
17
|
+
try {
|
|
18
|
+
const { nodeTests, browserTests } = await findTests('counter', '', true, true);
|
|
19
|
+
log(`Filtered node tests: ${nodeTests.join(', ')}`);
|
|
20
|
+
log(`Filtered browser tests: ${browserTests.join(', ')}`);
|
|
21
|
+
const okNode = nodeTests.every(f => f.includes('counter') === true) || nodeTests.length === 0;
|
|
22
|
+
const okBrowser = browserTests.every(f => f.includes('counter') === true);
|
|
23
|
+
okNode && okBrowser ? pass('Suite filter correctly applied to file names') : fail(`Unexpected filtering. node: ${nodeTests}, browser: ${browserTests}`);
|
|
24
|
+
} catch (e) {
|
|
25
|
+
fail(e.stack || String(e));
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
'respects environment toggles': async ({ pass, fail, log }) => {
|
|
29
|
+
try {
|
|
30
|
+
const a = await findTests('', '', false, true); // node only
|
|
31
|
+
const b = await findTests('', '', true, false); // browser only
|
|
32
|
+
log(`Node-only counts: node=${a.nodeTests.length}, browser=${a.browserTests.length}`);
|
|
33
|
+
log(`Browser-only counts: node=${b.nodeTests.length}, browser=${b.browserTests.length}`);
|
|
34
|
+
const okA = a.browserTests.length === 0 && a.nodeTests.length > 0;
|
|
35
|
+
const okB = b.nodeTests.length === 0 && b.browserTests.length > 0;
|
|
36
|
+
okA && okB ? pass('Environment toggles respected (node-only / browser-only)') : fail(`Env mismatch: a=${JSON.stringify(a)}, b=${JSON.stringify(b)}`);
|
|
37
|
+
} catch (e) {
|
|
38
|
+
fail(e.stack || String(e));
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import LOG_LEVELS from '../src/utils/logLevels.js';
|
|
2
|
+
|
|
3
|
+
export default {
|
|
4
|
+
'defines expected numeric levels': async ({ pass, fail, log }) => {
|
|
5
|
+
try {
|
|
6
|
+
const want = { SILENT: 0, MINIMAL: 1, NORMAL: 2, VERBOSE: 3, DEBUG: 4 };
|
|
7
|
+
log(`Actual levels: ${JSON.stringify(LOG_LEVELS)}`);
|
|
8
|
+
const ok = want.SILENT === LOG_LEVELS.SILENT &&
|
|
9
|
+
want.MINIMAL === LOG_LEVELS.MINIMAL &&
|
|
10
|
+
want.NORMAL === LOG_LEVELS.NORMAL &&
|
|
11
|
+
want.VERBOSE === LOG_LEVELS.VERBOSE &&
|
|
12
|
+
want.DEBUG === LOG_LEVELS.DEBUG;
|
|
13
|
+
ok ? pass('Log level constants match expected numeric values') : fail(`Unexpected values: ${JSON.stringify(LOG_LEVELS)}`);
|
|
14
|
+
} catch (e) {
|
|
15
|
+
fail(e.stack || String(e));
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
'level ordering is ascending': async ({ pass, fail, log }) => {
|
|
19
|
+
try {
|
|
20
|
+
const arr = [LOG_LEVELS.SILENT, LOG_LEVELS.MINIMAL, LOG_LEVELS.NORMAL, LOG_LEVELS.VERBOSE, LOG_LEVELS.DEBUG];
|
|
21
|
+
const sorted = [...arr].sort((a,b)=>a-b);
|
|
22
|
+
log(`Order: ${arr.join(' < ')}`);
|
|
23
|
+
const ok = JSON.stringify(arr) === JSON.stringify(sorted);
|
|
24
|
+
ok ? pass('Log levels are in ascending order') : fail(`Not ascending: ${arr}`);
|
|
25
|
+
} catch (e) {
|
|
26
|
+
fail(e.stack || String(e));
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
};
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import runBrowserTests from '../src/runBrowserTests.js';
|
|
2
|
+
|
|
3
|
+
export default {
|
|
4
|
+
'runs a specific browser test headless and returns results': async ({ pass, fail, log }) => {
|
|
5
|
+
try {
|
|
6
|
+
const res = await runBrowserTests({
|
|
7
|
+
testFile: 'tests/counter.browser-test.js',
|
|
8
|
+
filter: '',
|
|
9
|
+
showBrowser: false,
|
|
10
|
+
port: 3111,
|
|
11
|
+
logLevel: 2,
|
|
12
|
+
delayMs: 0
|
|
13
|
+
});
|
|
14
|
+
const names = Object.keys(res.tests || {});
|
|
15
|
+
log(`Ran browser tests: ${names.join(', ')}`);
|
|
16
|
+
if (names.length >= 3 && res.beforeAllLogs?.length >= 1 && res.afterAllLogs?.length >= 1) pass('Headless run produced tests and lifecycle logs');
|
|
17
|
+
else fail(`Unexpected browser test results: ${JSON.stringify(res)}`);
|
|
18
|
+
} catch (e) {
|
|
19
|
+
fail(e.stack || String(e));
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
'applies pre/post delay only when showBrowser is true': async ({ pass, fail, log }) => {
|
|
23
|
+
try {
|
|
24
|
+
// Use a single test to avoid between-test delays dominating timings
|
|
25
|
+
const filter = 'Counter component should be defined';
|
|
26
|
+
const delay = 1000;
|
|
27
|
+
const start1 = Date.now();
|
|
28
|
+
await runBrowserTests({ testFile: 'tests/counter.browser-test.js', filter, showBrowser: false, port: 3112, logLevel: 2, delayMs: delay });
|
|
29
|
+
const t1 = Date.now() - start1;
|
|
30
|
+
const start2 = Date.now();
|
|
31
|
+
await runBrowserTests({ testFile: 'tests/counter.browser-test.js', filter, showBrowser: true, port: 3113, logLevel: 2, delayMs: delay });
|
|
32
|
+
const t2 = Date.now() - start2;
|
|
33
|
+
// Headful should incur roughly +2000ms (pre+post). Allow slack for env variance.
|
|
34
|
+
log(`Timing — headless=${t1}ms, headful=${t2}ms, delta=${t2 - t1}ms`);
|
|
35
|
+
if (t2 - t1 >= 1500) pass('Headful mode applied pre/post delay as expected');
|
|
36
|
+
else fail(`Timing not increased as expected: headless=${t1}ms headful=${t2}ms`);
|
|
37
|
+
} catch (e) {
|
|
38
|
+
fail(e.stack || String(e));
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
};
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import runTestFiles from '../src/runTestFiles.js';
|
|
2
|
+
|
|
3
|
+
export default {
|
|
4
|
+
'discovers and runs node tests only when requested': async ({ pass, fail, log }) => {
|
|
5
|
+
try {
|
|
6
|
+
const { nodeResults, browserResults } = await runTestFiles({
|
|
7
|
+
suiteFilter: 'example',
|
|
8
|
+
testFilter: '',
|
|
9
|
+
shouldRunBrowser: false,
|
|
10
|
+
shouldRunNode: true,
|
|
11
|
+
showBrowser: false,
|
|
12
|
+
port: 3000,
|
|
13
|
+
logLevel: 2
|
|
14
|
+
});
|
|
15
|
+
log(`Node files run: ${Object.keys(nodeResults).length}, browser files run: ${Object.keys(browserResults).length}`);
|
|
16
|
+
const onlyNode = Object.keys(browserResults).length === 0 && Object.keys(nodeResults).length >= 1;
|
|
17
|
+
onlyNode ? pass('Ran node tests only when requested (browser skipped)') : fail(`Unexpected results: node=${Object.keys(nodeResults)}, browser=${Object.keys(browserResults)}`);
|
|
18
|
+
} catch (e) {
|
|
19
|
+
fail(e.stack || String(e));
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
'passes filter into node runs': async ({ pass, fail, log }) => {
|
|
23
|
+
try {
|
|
24
|
+
const { nodeResults } = await runTestFiles({
|
|
25
|
+
suiteFilter: 'example',
|
|
26
|
+
testFilter: 'async',
|
|
27
|
+
shouldRunBrowser: false,
|
|
28
|
+
shouldRunNode: true,
|
|
29
|
+
showBrowser: false,
|
|
30
|
+
port: 3000,
|
|
31
|
+
logLevel: 2
|
|
32
|
+
});
|
|
33
|
+
const [file, res] = Object.entries(nodeResults)[0];
|
|
34
|
+
if (!res) return fail('No node results returned');
|
|
35
|
+
const names = Object.keys(res.tests || {});
|
|
36
|
+
log(`Filtered test names: ${names.join(', ')}`);
|
|
37
|
+
names.length === 1 && names[0].toLowerCase().includes('async') ? pass('Per-file test filter applied to node run') : fail(`Unexpected filtered tests: ${names}`);
|
|
38
|
+
} catch (e) {
|
|
39
|
+
fail(e.stack || String(e));
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
};
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import runTests from '../src/runTests.js';
|
|
2
|
+
|
|
3
|
+
const mkSuite = () => ({
|
|
4
|
+
beforeAll: async (log) => { log('before all'); },
|
|
5
|
+
beforeEach: async (log) => { log('before each'); },
|
|
6
|
+
afterEach: async (log) => { log('after each'); },
|
|
7
|
+
afterAll: async (log) => { log('after all'); },
|
|
8
|
+
default: {
|
|
9
|
+
'one passes': async ({ pass, log }) => { log('running one'); pass('ok'); },
|
|
10
|
+
'two passes': async ({ pass, log }) => { log('running two'); pass('ok'); },
|
|
11
|
+
}
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
export default {
|
|
15
|
+
'runs all tests and records logs': async ({ pass, fail, log }) => {
|
|
16
|
+
try {
|
|
17
|
+
const res = await runTests(mkSuite(), false, 0);
|
|
18
|
+
const names = Object.keys(res.tests);
|
|
19
|
+
log(`Discovered tests: ${names.join(', ')}`);
|
|
20
|
+
if (names.length !== 2) return fail(`Expected 2 tests, got ${names.length} [${names.join(', ')}]`);
|
|
21
|
+
const hasBeforeAll = res.beforeAllLogs.some(l => l.message.includes('before all'));
|
|
22
|
+
const hasAfterAll = res.afterAllLogs.some(l => l.message.includes('after all'));
|
|
23
|
+
const eachs = Object.values(res.tests).flatMap(r => r.logs.map(l=>l.message));
|
|
24
|
+
const hasBefores = eachs.some(m => m.includes('== Before Each =='));
|
|
25
|
+
const hasAfters = eachs.some(m => m.includes('== After Each =='));
|
|
26
|
+
log(`Lifecycle logs — beforeAll:${hasBeforeAll} afterAll:${hasAfterAll} beforeEach:${hasBefores} afterEach:${hasAfters}`);
|
|
27
|
+
if (hasBeforeAll && hasAfterAll && hasBefores && hasAfters) pass('Recorded lifecycle and per-test logs as expected');
|
|
28
|
+
else fail('Missing expected lifecycle log sections');
|
|
29
|
+
} catch (e) {
|
|
30
|
+
fail(e.stack || String(e));
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
'applies per-test delay': async ({ pass, fail, log }) => {
|
|
34
|
+
try {
|
|
35
|
+
const t0 = Date.now();
|
|
36
|
+
await runTests(mkSuite(), false, 200);
|
|
37
|
+
const elapsed = Date.now() - t0;
|
|
38
|
+
log(`Elapsed with delay: ${elapsed}ms`);
|
|
39
|
+
if (elapsed >= 200 && elapsed < 800) pass(`Applied per-test delay successfully (~${elapsed}ms >= 200ms)`);
|
|
40
|
+
else fail(`Unexpected timing with delay=200ms: ${elapsed}ms`);
|
|
41
|
+
} catch (e) {
|
|
42
|
+
fail(e.stack || String(e));
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
'filters tests by substring': async ({ pass, fail, log }) => {
|
|
46
|
+
try {
|
|
47
|
+
const res = await runTests(mkSuite(), 'two', 0);
|
|
48
|
+
const names = Object.keys(res.tests);
|
|
49
|
+
log(`Filtered tests: ${names.join(', ')}`);
|
|
50
|
+
if (names.length === 1 && names[0].includes('two')) pass(`Filtered to expected test: ${names[0]}`);
|
|
51
|
+
else fail(`Unexpected filtered set: [${names.join(', ')}]`);
|
|
52
|
+
} catch (e) {
|
|
53
|
+
fail(e.stack || String(e));
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
};
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
// Browser tests for TestFramework 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
|
+
'TestFramework queues and deduplicates tasks, updates statuses': async ({ pass, fail }) => {
|
|
12
|
+
try {
|
|
13
|
+
await import('/gui/components/settingsStore.js');
|
|
14
|
+
await import('/gui/components/Collapsible.js');
|
|
15
|
+
await import('/gui/components/Logs.js');
|
|
16
|
+
await import('/gui/components/Icon.js');
|
|
17
|
+
await import('/gui/components/Test.js');
|
|
18
|
+
await import('/gui/components/TestSuite.js');
|
|
19
|
+
await import('/gui/components/TestFramework.js');
|
|
20
|
+
|
|
21
|
+
const fw = document.createElement('ktf-test-framework');
|
|
22
|
+
const suite = document.createElement('ktf-test-suite');
|
|
23
|
+
suite.file = 'tests/example.node-test.js';
|
|
24
|
+
suite.testNames = ['X','Y'];
|
|
25
|
+
document.body.appendChild(fw);
|
|
26
|
+
fw.appendChild(suite);
|
|
27
|
+
await nextTick(); await nextTick();
|
|
28
|
+
|
|
29
|
+
const [t1, t2] = suite.querySelectorAll('ktf-test');
|
|
30
|
+
|
|
31
|
+
// Stub fetch to simulate run responses quickly
|
|
32
|
+
const originalFetch = window.fetch;
|
|
33
|
+
window.fetch = async (url) => {
|
|
34
|
+
// Return a minimal results payload depending on testNames query
|
|
35
|
+
const u = new URL(String(url), window.location.origin);
|
|
36
|
+
const testNames = u.searchParams.get('testNames');
|
|
37
|
+
if (testNames) {
|
|
38
|
+
const name = testNames;
|
|
39
|
+
return { ok: true, json: async () => ({ results: { tests: { [name]: { passed: true, logs: [{ message: `PASS ${name}`, type: 'pass', level: 2 }] } }, beforeAllLogs: [], afterAllLogs: [] } }) };
|
|
40
|
+
}
|
|
41
|
+
// Suite run: both tests pass
|
|
42
|
+
return { ok: true, json: async () => ({ results: { tests: { X: { passed: true, logs: [] }, Y: { passed: true, logs: [] } }, beforeAllLogs: [], afterAllLogs: [] } }) };
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
// Enqueue duplicate test; ensure dedupe keeps one queued (status leaves 'notran')
|
|
46
|
+
fw.enqueueTest({ file: suite.file, name: 'X', el: t1 });
|
|
47
|
+
fw.enqueueTest({ file: suite.file, name: 'X', el: t1 });
|
|
48
|
+
await nextTick();
|
|
49
|
+
const queuedOnce = t1.status !== 'notran';
|
|
50
|
+
|
|
51
|
+
// Run individual test via queue processing; wait until it passes
|
|
52
|
+
for (let i=0;i<20 && t1.status !== 'pass'; i++){ await nextTick(); }
|
|
53
|
+
const xPassed = t1.status === 'pass';
|
|
54
|
+
|
|
55
|
+
// Enqueue suite; children should go queued then pass
|
|
56
|
+
fw.enqueueSuite({ file: suite.file, testNames: ['X','Y'], el: suite });
|
|
57
|
+
for (let i=0;i<30 && !(suite.status === 'pass' && t2.status === 'pass'); i++){ await nextTick(); }
|
|
58
|
+
const suiteOk = suite.status === 'pass' && t2.status === 'pass';
|
|
59
|
+
|
|
60
|
+
window.fetch = originalFetch;
|
|
61
|
+
if (queuedOnce && xPassed && suiteOk) pass('TestFramework deduped queue and updated statuses');
|
|
62
|
+
else fail('TestFramework queue or status updates failed');
|
|
63
|
+
} catch (e) { fail(e.stack || String(e)); }
|
|
64
|
+
}
|
|
65
|
+
};
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
// Browser tests for TestSummary 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
|
+
'TestSummary initializes counts from child suites and tests': async ({ pass, fail }) => {
|
|
12
|
+
try {
|
|
13
|
+
await import('/gui/components/settingsStore.js');
|
|
14
|
+
await import('/gui/components/Collapsible.js');
|
|
15
|
+
await import('/gui/components/Logs.js');
|
|
16
|
+
await import('/gui/components/Icon.js');
|
|
17
|
+
await import('/gui/components/Test.js');
|
|
18
|
+
await import('/gui/components/TestSuite.js');
|
|
19
|
+
await import('/gui/components/TestSummary.js');
|
|
20
|
+
await import('/gui/components/TestFramework.js');
|
|
21
|
+
|
|
22
|
+
const fw = document.createElement('ktf-test-framework');
|
|
23
|
+
const summary = document.createElement('ktf-test-summary');
|
|
24
|
+
const suite = document.createElement('ktf-test-suite');
|
|
25
|
+
suite.file = 'tests/example.node-test.js';
|
|
26
|
+
suite.testNames = ['A','B'];
|
|
27
|
+
fw.appendChild(summary);
|
|
28
|
+
fw.appendChild(suite);
|
|
29
|
+
document.body.appendChild(fw);
|
|
30
|
+
await nextTick(); await nextTick();
|
|
31
|
+
|
|
32
|
+
const text = summary.shadowRoot.textContent.replace(/\s+/g,' ');
|
|
33
|
+
const filesTotal = /Files[\s\S]*Total:\s*1/.test(text);
|
|
34
|
+
const testsTotal = /Tests[\s\S]*Total:\s*2/.test(text);
|
|
35
|
+
if (filesTotal && testsTotal) pass('TestSummary counted files and tests on init'); else fail(`Counts missing:\n${text}`);
|
|
36
|
+
} catch (e) { fail(e.stack || String(e)); }
|
|
37
|
+
},
|
|
38
|
+
|
|
39
|
+
'TestSummary updates status, counts, and queue length': async ({ pass, fail }) => {
|
|
40
|
+
try {
|
|
41
|
+
await import('/gui/components/settingsStore.js');
|
|
42
|
+
await import('/gui/components/Collapsible.js');
|
|
43
|
+
await import('/gui/components/Logs.js');
|
|
44
|
+
await import('/gui/components/Icon.js');
|
|
45
|
+
await import('/gui/components/Test.js');
|
|
46
|
+
await import('/gui/components/TestSuite.js');
|
|
47
|
+
await import('/gui/components/TestSummary.js');
|
|
48
|
+
await import('/gui/components/TestFramework.js');
|
|
49
|
+
|
|
50
|
+
const fw = document.createElement('ktf-test-framework');
|
|
51
|
+
const summary = document.createElement('ktf-test-summary');
|
|
52
|
+
const suite = document.createElement('ktf-test-suite');
|
|
53
|
+
suite.file = 'tests/example.node-test.js';
|
|
54
|
+
suite.testNames = ['A','B'];
|
|
55
|
+
fw.appendChild(summary);
|
|
56
|
+
fw.appendChild(suite);
|
|
57
|
+
document.body.appendChild(fw);
|
|
58
|
+
await nextTick(); await nextTick();
|
|
59
|
+
|
|
60
|
+
// Simulate queue updates
|
|
61
|
+
fw.dispatchEvent(new CustomEvent('ktf:queue-updated', { detail: { length: 2 } }));
|
|
62
|
+
await nextTick();
|
|
63
|
+
const queueShown = summary.shadowRoot.textContent.includes('Queue: 2');
|
|
64
|
+
|
|
65
|
+
// Simulate status changes on tests to drive summary
|
|
66
|
+
const [t1, t2] = suite.querySelectorAll('ktf-test');
|
|
67
|
+
t1.status = 'running';
|
|
68
|
+
await nextTick();
|
|
69
|
+
const running = summary.getAttribute('status') === 'running' || summary.status === 'running';
|
|
70
|
+
t1.status = 'pass'; t2.status = 'pass';
|
|
71
|
+
await nextTick(); await nextTick();
|
|
72
|
+
const passOk = summary.getAttribute('status') === 'pass' || summary.status === 'pass';
|
|
73
|
+
|
|
74
|
+
if (queueShown && running && passOk) pass('TestSummary reflected queue and status transitions');
|
|
75
|
+
else fail('TestSummary did not update as expected');
|
|
76
|
+
} catch (e) { fail(e.stack || String(e)); }
|
|
77
|
+
}
|
|
78
|
+
};
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
// Browser tests for Test 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
|
+
'Test renders and shows play icon before run': async ({ pass, fail }) => {
|
|
12
|
+
try {
|
|
13
|
+
await import('/gui/components/settingsStore.js');
|
|
14
|
+
await import('/gui/components/Collapsible.js');
|
|
15
|
+
await import('/gui/components/Logs.js');
|
|
16
|
+
await import('/gui/components/Icon.js');
|
|
17
|
+
await import('/gui/components/Test.js');
|
|
18
|
+
|
|
19
|
+
const t = document.createElement('ktf-test');
|
|
20
|
+
t.name = 'Sample Test';
|
|
21
|
+
t.file = 'tests/example.node-test.js';
|
|
22
|
+
document.body.appendChild(t);
|
|
23
|
+
await nextTick();
|
|
24
|
+
|
|
25
|
+
const hasPlay = !!t.shadowRoot.querySelector('div[slot="actions"] ktf-icon[name="play"]');
|
|
26
|
+
if (hasPlay) pass('Test component renders with initial play icon'); else fail('Missing play icon on Test component');
|
|
27
|
+
} catch (e) { fail(e.stack || String(e)); }
|
|
28
|
+
},
|
|
29
|
+
|
|
30
|
+
'Test status updates through running/pass/fail': async ({ pass, fail }) => {
|
|
31
|
+
try {
|
|
32
|
+
await import('/gui/components/settingsStore.js');
|
|
33
|
+
await import('/gui/components/Collapsible.js');
|
|
34
|
+
await import('/gui/components/Logs.js');
|
|
35
|
+
await import('/gui/components/Icon.js');
|
|
36
|
+
await import('/gui/components/Test.js');
|
|
37
|
+
|
|
38
|
+
const t = document.createElement('ktf-test');
|
|
39
|
+
t.name = 'Sample Test';
|
|
40
|
+
t.file = 'tests/example.node-test.js';
|
|
41
|
+
document.body.appendChild(t);
|
|
42
|
+
await nextTick();
|
|
43
|
+
|
|
44
|
+
t.status = 'running';
|
|
45
|
+
await nextTick();
|
|
46
|
+
const running = t.getAttribute('status') === 'running' || t.status === 'running';
|
|
47
|
+
t.status = 'pass';
|
|
48
|
+
await nextTick();
|
|
49
|
+
const passOk = t.getAttribute('status') === 'pass' || t.status === 'pass';
|
|
50
|
+
t.status = 'fail';
|
|
51
|
+
await nextTick();
|
|
52
|
+
const failOk = t.getAttribute('status') === 'fail' || t.status === 'fail';
|
|
53
|
+
if (running && passOk && failOk) pass('Test component reflected status transitions running→pass→fail'); else fail('Status transitions failed to reflect on Test');
|
|
54
|
+
} catch (e) { fail(e.stack || String(e)); }
|
|
55
|
+
}
|
|
56
|
+
};
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
// Browser tests for TestSuite 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
|
+
'TestSuite renders children and initial play icon': async ({ pass, fail }) => {
|
|
12
|
+
try {
|
|
13
|
+
await import('/gui/components/settingsStore.js');
|
|
14
|
+
await import('/gui/components/Collapsible.js');
|
|
15
|
+
await import('/gui/components/Logs.js');
|
|
16
|
+
await import('/gui/components/Icon.js');
|
|
17
|
+
await import('/gui/components/Test.js');
|
|
18
|
+
await import('/gui/components/TestSuite.js');
|
|
19
|
+
|
|
20
|
+
const tf = document.createElement('ktf-test-suite');
|
|
21
|
+
tf.file = 'tests/example.node-test.js';
|
|
22
|
+
tf.testNames = ['A', 'B'];
|
|
23
|
+
document.body.appendChild(tf);
|
|
24
|
+
await nextTick(); await nextTick();
|
|
25
|
+
|
|
26
|
+
const hasPlay = !!tf.shadowRoot.querySelector('div[slot="actions"] ktf-icon[name="play"]');
|
|
27
|
+
if (hasPlay) pass('TestSuite renders children and shows initial play icon'); else fail('Missing play icon on TestSuite');
|
|
28
|
+
} catch (e) { fail(e.stack || String(e)); }
|
|
29
|
+
},
|
|
30
|
+
|
|
31
|
+
'TestSuite aggregates status from children': async ({ pass, fail }) => {
|
|
32
|
+
try {
|
|
33
|
+
await import('/gui/components/settingsStore.js');
|
|
34
|
+
await import('/gui/components/Collapsible.js');
|
|
35
|
+
await import('/gui/components/Logs.js');
|
|
36
|
+
await import('/gui/components/Icon.js');
|
|
37
|
+
await import('/gui/components/Test.js');
|
|
38
|
+
await import('/gui/components/TestSuite.js');
|
|
39
|
+
|
|
40
|
+
const tf = document.createElement('ktf-test-suite');
|
|
41
|
+
tf.file = 'tests/example.node-test.js';
|
|
42
|
+
tf.testNames = ['A', 'B'];
|
|
43
|
+
document.body.appendChild(tf);
|
|
44
|
+
await nextTick(); await nextTick();
|
|
45
|
+
|
|
46
|
+
const children = tf.querySelectorAll('ktf-test');
|
|
47
|
+
children[0].status = 'running';
|
|
48
|
+
await nextTick();
|
|
49
|
+
const statusRunning = tf.getAttribute('status') === 'running' || tf.status === 'running';
|
|
50
|
+
children[0].status = 'pass';
|
|
51
|
+
children[1].status = 'fail';
|
|
52
|
+
await nextTick();
|
|
53
|
+
const statusFail = tf.getAttribute('status') === 'fail' || tf.status === 'fail';
|
|
54
|
+
children[1].status = 'pass';
|
|
55
|
+
await nextTick();
|
|
56
|
+
const statusPass = tf.getAttribute('status') === 'pass' || tf.status === 'pass';
|
|
57
|
+
if (statusRunning && statusFail && statusPass) pass('TestSuite aggregated status from children correctly'); else fail('Status propagation failed on TestSuite');
|
|
58
|
+
} catch (e) { fail(e.stack || String(e)); }
|
|
59
|
+
}
|
|
60
|
+
};
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
// Browser tests for Theme 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
|
+
'Theme applies document theme on settings change': async ({ pass, fail }) => {
|
|
12
|
+
try {
|
|
13
|
+
await import('/gui/components/settingsStore.js');
|
|
14
|
+
await import('/gui/components/Theme.js');
|
|
15
|
+
const el = document.createElement('ktf-theme');
|
|
16
|
+
document.body.appendChild(el);
|
|
17
|
+
await nextTick();
|
|
18
|
+
window.KTF_SETTINGS.set({ theme: 'dark' });
|
|
19
|
+
await nextTick();
|
|
20
|
+
const themeAttr = document.documentElement.getAttribute('theme');
|
|
21
|
+
if (themeAttr === 'dark') pass('Theme component applied document theme to dark'); else fail(`Theme not applied: ${themeAttr}`);
|
|
22
|
+
} catch (e) { fail(e.stack || String(e)); }
|
|
23
|
+
},
|
|
24
|
+
|
|
25
|
+
'Theme switches back to light': async ({ pass, fail }) => {
|
|
26
|
+
try {
|
|
27
|
+
await import('/gui/components/settingsStore.js');
|
|
28
|
+
await import('/gui/components/Theme.js');
|
|
29
|
+
const el = document.createElement('ktf-theme');
|
|
30
|
+
document.body.appendChild(el);
|
|
31
|
+
await nextTick();
|
|
32
|
+
window.KTF_SETTINGS.set({ theme: 'light' });
|
|
33
|
+
await nextTick();
|
|
34
|
+
const themeAttr = document.documentElement.getAttribute('theme');
|
|
35
|
+
if (themeAttr === 'light') pass('Theme component applied document theme back to light'); else fail(`Theme not applied: ${themeAttr}`);
|
|
36
|
+
} catch (e) { fail(e.stack || String(e)); }
|
|
37
|
+
}
|
|
38
|
+
};
|