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.
- package/.github/copilot-instructions.md +105 -105
- package/.github/workflows/publish-npm.yml +41 -0
- package/CONTRIBUTING.md +107 -107
- package/gui/components/Collapsible.js +54 -54
- package/gui/components/Icon.js +151 -151
- package/gui/components/Logs.js +73 -73
- package/gui/components/SettingCheckbox.js +42 -42
- package/gui/components/SettingNumber.js +77 -77
- package/gui/components/SettingSelect.js +67 -67
- package/gui/components/TestFramework.js +378 -378
- package/gui/components/TestSuite.js +181 -181
- package/gui/components/TestSummary.js +189 -189
- package/gui/components/Theme.js +40 -40
- package/gui/components/settingsStore.js +46 -46
- package/gui/icons/fail.svg +1 -1
- package/gui/icons/pass.svg +1 -1
- package/gui/icons/running.svg +1 -1
- package/gui/icons/settings.svg +1 -1
- package/package.json +5 -1
- package/src/browserTestServer.js +115 -115
- package/src/cli.js +198 -198
- package/src/gui.js +15 -2
- package/src/runBrowserTests.js +87 -87
- package/src/runTestFiles.js +94 -94
- package/src/runTests.js +83 -83
- package/src/utils/logLevels.js +7 -7
- package/test.html +30 -30
- package/tests/Counter.js +33 -33
- package/tests/cli-flags.node-test.js +54 -54
- package/tests/cli-help.node-test.js +61 -61
- package/tests/cli-loglevel.node-test.js +40 -40
- package/tests/collapsible.browser-test.js +49 -49
- package/tests/counter.browser-test.js +140 -140
- package/tests/example.node-test.js +103 -103
- package/tests/fibonacci.js +92 -92
- package/tests/fibonacci.test.js +285 -285
- package/tests/icon.browser-test.js +54 -54
- package/tests/logs.browser-test.js +47 -47
- package/tests/setting-checkbox.browser-test.js +48 -48
- package/tests/setting-number.browser-test.js +54 -54
- package/tests/setting-select.browser-test.js +47 -47
- package/tests/settings-store.browser-test.js +26 -26
- package/tests/src-browserTestServer.node-test.js +47 -47
- package/tests/src-cli.node-test.js +32 -32
- package/tests/src-findTests.node-test.js +41 -41
- package/tests/src-logLevels.node-test.js +29 -29
- package/tests/src-runBrowserTests.node-test.js +41 -41
- package/tests/src-runTestFiles.node-test.js +42 -42
- package/tests/src-runTests.node-test.js +56 -56
- package/tests/test-framework.browser-test.js +65 -65
- package/tests/test-summary.browser-test.js +78 -78
- package/tests/test.browser-test.js +56 -56
- package/tests/testfile.browser-test.js +60 -60
- 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;
|