kempo-testing-framework 1.2.1 → 1.2.3
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/gui/components/SettingCheckbox.js +1 -1
- package/gui/components/TestFramework.js +142 -0
- package/gui/index.html +121 -30
- package/index.js +8 -1
- package/package.json +1 -1
- package/src/gui.js +48 -10
- package/src/utils/fibonacci.js +0 -0
- package/tests/cli-flags.node-test.js +1 -1
- package/tests/fibonacci.js +92 -0
- package/tests/fibonacci.test.js +285 -0
|
@@ -35,7 +35,7 @@ window.customElements.define('ktf-setting-checkbox', class extends LitElement {
|
|
|
35
35
|
return html`
|
|
36
36
|
<div class="d-f mb" style="align-items: center">
|
|
37
37
|
<input id="${id}" type="checkbox" style="font-size: 1.35rem" .checked=${!!this.checked} @change=${(e) => this.onChange(e)} />
|
|
38
|
-
<label for="${id}" style="line-height: 1.35rem">${this.label || ''}</label>
|
|
38
|
+
<label for="${id}" style="line-height: 1.35rem" class="pb0">${this.label || ''}</label>
|
|
39
39
|
</div>
|
|
40
40
|
`;
|
|
41
41
|
}
|
|
@@ -151,6 +151,8 @@ class TestFrameworkEl extends LitElement {
|
|
|
151
151
|
};
|
|
152
152
|
|
|
153
153
|
runSuite = async ({ file, testNames, el }) => {
|
|
154
|
+
const isUniversal = el.hasAttribute('universal');
|
|
155
|
+
|
|
154
156
|
try {
|
|
155
157
|
el.status = 'running';
|
|
156
158
|
const tests = Array.from(el.querySelectorAll('ktf-test'));
|
|
@@ -162,7 +164,147 @@ class TestFrameworkEl extends LitElement {
|
|
|
162
164
|
const fileLogsEl = el.renderRoot?.getElementById('fileLogs');
|
|
163
165
|
if(fileLogsEl) fileLogsEl.clear();
|
|
164
166
|
} catch {}
|
|
167
|
+
|
|
165
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
|
|
166
308
|
const resp = await fetch(`/runTest?testFile=${encodeURIComponent(file)}&showBrowser=${!!showBrowser}&delayMs=${Number(delayMs||0)}`);
|
|
167
309
|
let data = null;
|
|
168
310
|
try { data = await resp.json(); } catch {}
|
package/gui/index.html
CHANGED
|
@@ -5,6 +5,29 @@
|
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
6
|
<title>Kempo Testing Library GUI</title>
|
|
7
7
|
<link rel="stylesheet" href="/essential.css" />
|
|
8
|
+
<style>
|
|
9
|
+
.loading-state {
|
|
10
|
+
padding: 1rem;
|
|
11
|
+
text-align: center;
|
|
12
|
+
color: #666;
|
|
13
|
+
font-style: italic;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
.empty-state {
|
|
17
|
+
padding: 1rem;
|
|
18
|
+
color: #888;
|
|
19
|
+
background-color: #f5f5f5;
|
|
20
|
+
border-radius: 4px;
|
|
21
|
+
margin: 0.5rem 0;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
.empty-state code {
|
|
25
|
+
background-color: #e8e8e8;
|
|
26
|
+
padding: 0.2rem 0.4rem;
|
|
27
|
+
border-radius: 3px;
|
|
28
|
+
font-family: monospace;
|
|
29
|
+
}
|
|
30
|
+
</style>
|
|
8
31
|
</head>
|
|
9
32
|
<body>
|
|
10
33
|
<main>
|
|
@@ -26,12 +49,23 @@
|
|
|
26
49
|
|
|
27
50
|
<ktf-test-framework id="framework">
|
|
28
51
|
<ktf-test-summary id="globalSummary"></ktf-test-summary>
|
|
52
|
+
<div id="universalTestsContainer">
|
|
53
|
+
<h2>Universal Tests</h2>
|
|
54
|
+
<p>Tests that run in both Node and Browser environments</p>
|
|
55
|
+
<div id="universalTestsLoading" class="loading-state">Loading universal tests...</div>
|
|
56
|
+
<div id="universalTestsEmpty" class="empty-state" style="display: none;">No universal tests found. Create test files with <code>.test.js</code> extension to run tests in both environments.</div>
|
|
57
|
+
<div id="universalTests"></div>
|
|
58
|
+
</div>
|
|
29
59
|
<div id="nodeTestsContainer">
|
|
30
60
|
<h2>Node Tests</h2>
|
|
61
|
+
<div id="nodeTestsLoading" class="loading-state">Loading Node tests...</div>
|
|
62
|
+
<div id="nodeTestsEmpty" class="empty-state" style="display: none;">No Node-only tests found. Create test files with <code>.node-test.js</code> extension to run tests only in Node.</div>
|
|
31
63
|
<div id="nodeTests"></div>
|
|
32
64
|
</div>
|
|
33
65
|
<div id="browserTestsContainer">
|
|
34
66
|
<h2>Browser Tests</h2>
|
|
67
|
+
<div id="browserTestsLoading" class="loading-state">Loading browser tests...</div>
|
|
68
|
+
<div id="browserTestsEmpty" class="empty-state" style="display: none;">No browser-only tests found. Create test files with <code>.browser-test.js</code> extension to run tests only in the browser.</div>
|
|
35
69
|
<div id="browserTests"></div>
|
|
36
70
|
</div>
|
|
37
71
|
</ktf-test-framework>
|
|
@@ -70,38 +104,95 @@
|
|
|
70
104
|
applyToControls(getSettings());
|
|
71
105
|
subscribe(applyToControls);
|
|
72
106
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
107
|
+
try {
|
|
108
|
+
const testFiles = await(await fetch('/testFiles')).json();
|
|
109
|
+
|
|
110
|
+
// Get the properly categorized test files from the API
|
|
111
|
+
const nodeOnlyTests = testFiles.nodeTests || [];
|
|
112
|
+
const browserOnlyTests = testFiles.browserTests || [];
|
|
113
|
+
const universalTests = testFiles.universalTests || [];
|
|
114
|
+
|
|
115
|
+
// Load test names for browser-accessible tests (browser-only and universal)
|
|
116
|
+
const browserAccessibleTests = [...browserOnlyTests, ...universalTests];
|
|
117
|
+
for (const test of browserAccessibleTests) {
|
|
118
|
+
try {
|
|
119
|
+
const moduleUrl = `/test/${test.file}`;
|
|
120
|
+
const testModule = await import(moduleUrl);
|
|
121
|
+
test.testNames = testModule.default ? Object.keys(testModule.default) : [];
|
|
122
|
+
} catch (error) {
|
|
123
|
+
console.error(`Error importing test file ${test.file}:`, error);
|
|
124
|
+
test.testNames = [];
|
|
125
|
+
}
|
|
82
126
|
}
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
document.getElementById('
|
|
86
|
-
|
|
87
|
-
const
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
127
|
+
|
|
128
|
+
// Universal tests section (.test.js files)
|
|
129
|
+
const universalTestsLoading = document.getElementById('universalTestsLoading');
|
|
130
|
+
const universalTestsEmpty = document.getElementById('universalTestsEmpty');
|
|
131
|
+
const universalTestsContainer = document.getElementById('universalTests');
|
|
132
|
+
|
|
133
|
+
universalTestsLoading.style.display = 'none';
|
|
134
|
+
if(!universalTests?.length){
|
|
135
|
+
universalTestsEmpty.style.display = 'block';
|
|
136
|
+
} else {
|
|
137
|
+
universalTests.forEach(universalTest => {
|
|
138
|
+
const testFileElement = document.createElement('ktf-test-suite');
|
|
139
|
+
testFileElement.file = universalTest.file;
|
|
140
|
+
testFileElement.testNames = universalTest.testNames;
|
|
141
|
+
testFileElement.setAttribute('universal', 'true'); // Mark as universal for special handling
|
|
142
|
+
universalTestsContainer.appendChild(testFileElement);
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Node-only tests section
|
|
147
|
+
const nodeTestsLoading = document.getElementById('nodeTestsLoading');
|
|
148
|
+
const nodeTestsEmpty = document.getElementById('nodeTestsEmpty');
|
|
98
149
|
const nodeTestsContainer = document.getElementById('nodeTests');
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
150
|
+
|
|
151
|
+
nodeTestsLoading.style.display = 'none';
|
|
152
|
+
if(!nodeOnlyTests?.length){
|
|
153
|
+
nodeTestsEmpty.style.display = 'block';
|
|
154
|
+
} else {
|
|
155
|
+
nodeOnlyTests.forEach(nodeTest => {
|
|
156
|
+
const testFileElement = document.createElement('ktf-test-suite');
|
|
157
|
+
testFileElement.file = nodeTest.file;
|
|
158
|
+
testFileElement.testNames = nodeTest.testNames || [];
|
|
159
|
+
nodeTestsContainer.appendChild(testFileElement);
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Browser-only tests section
|
|
164
|
+
const browserTestsLoading = document.getElementById('browserTestsLoading');
|
|
165
|
+
const browserTestsEmpty = document.getElementById('browserTestsEmpty');
|
|
166
|
+
const browserTestsContainer = document.getElementById('browserTests');
|
|
167
|
+
|
|
168
|
+
browserTestsLoading.style.display = 'none';
|
|
169
|
+
if(!browserOnlyTests?.length){
|
|
170
|
+
browserTestsEmpty.style.display = 'block';
|
|
171
|
+
} else {
|
|
172
|
+
browserOnlyTests.forEach(browserTest => {
|
|
173
|
+
const testFileElement = document.createElement('ktf-test-suite');
|
|
174
|
+
testFileElement.file = browserTest.file;
|
|
175
|
+
testFileElement.testNames = browserTest.testNames;
|
|
176
|
+
browserTestsContainer.appendChild(testFileElement);
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
} catch (error) {
|
|
181
|
+
console.error('Error loading test files:', error);
|
|
182
|
+
|
|
183
|
+
// Hide loading states and show error messages
|
|
184
|
+
document.getElementById('universalTestsLoading').style.display = 'none';
|
|
185
|
+
document.getElementById('nodeTestsLoading').style.display = 'none';
|
|
186
|
+
document.getElementById('browserTestsLoading').style.display = 'none';
|
|
187
|
+
|
|
188
|
+
// Show error messages
|
|
189
|
+
const errorMsg = 'Error loading test files. Please check the console for details.';
|
|
190
|
+
document.getElementById('universalTestsEmpty').textContent = errorMsg;
|
|
191
|
+
document.getElementById('universalTestsEmpty').style.display = 'block';
|
|
192
|
+
document.getElementById('nodeTestsEmpty').textContent = errorMsg;
|
|
193
|
+
document.getElementById('nodeTestsEmpty').style.display = 'block';
|
|
194
|
+
document.getElementById('browserTestsEmpty').textContent = errorMsg;
|
|
195
|
+
document.getElementById('browserTestsEmpty').style.display = 'block';
|
|
105
196
|
}
|
|
106
197
|
</script>
|
|
107
198
|
</body>
|
package/index.js
CHANGED
|
@@ -23,7 +23,7 @@ const remainingArgs = [];
|
|
|
23
23
|
|
|
24
24
|
// Define which flags take values vs booleans
|
|
25
25
|
const valueFlags = new Set(['log-level', 'delay', 'port']);
|
|
26
|
-
const booleanFlags = new Set(['browser', 'node', 'gui', 'show-browser', 'help']);
|
|
26
|
+
const booleanFlags = new Set(['browser', 'node', 'gui', 'show-browser', 'help', 'debug-flags']);
|
|
27
27
|
|
|
28
28
|
for (let i = 0; i < args.length; i++) {
|
|
29
29
|
const arg = args[i];
|
|
@@ -144,6 +144,13 @@ EXAMPLES:
|
|
|
144
144
|
process.exit(0);
|
|
145
145
|
}
|
|
146
146
|
|
|
147
|
+
/*
|
|
148
|
+
* Debug Flags (for testing only)
|
|
149
|
+
*/
|
|
150
|
+
if (flags['debug-flags']) {
|
|
151
|
+
console.log('kempo-test flags:', flags);
|
|
152
|
+
}
|
|
153
|
+
|
|
147
154
|
/*
|
|
148
155
|
* Mode Selection and Execution
|
|
149
156
|
*/
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "kempo-testing-framework",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.3",
|
|
4
4
|
"description": "The Kempo Testing Framework is a simple testing framework built on the principle that code intended to be ran in the browser should be tested in the browser, code intended to be ran in Node should be tested in Node, and code intended to be ran in both should be tested in both. No mocking, no complexity—just testing.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"bin": {
|
package/src/gui.js
CHANGED
|
@@ -33,9 +33,28 @@ export default async (flags, args) => {
|
|
|
33
33
|
try {
|
|
34
34
|
const testFiles = await findTests('', '', true, true);
|
|
35
35
|
|
|
36
|
-
//
|
|
36
|
+
// Categorize test files properly
|
|
37
|
+
const nodeOnlyTests = [];
|
|
38
|
+
const browserOnlyTests = [];
|
|
39
|
+
const universalTests = [];
|
|
40
|
+
|
|
41
|
+
// Process all found test files and categorize them
|
|
42
|
+
const allTestFiles = new Set([...testFiles.nodeTests, ...testFiles.browserTests]);
|
|
43
|
+
|
|
44
|
+
for (const file of allTestFiles) {
|
|
45
|
+
if (file.endsWith('.test.js')) {
|
|
46
|
+
universalTests.push(file);
|
|
47
|
+
} else if (file.endsWith('.node-test.js')) {
|
|
48
|
+
nodeOnlyTests.push(file);
|
|
49
|
+
} else if (file.endsWith('.browser-test.js')) {
|
|
50
|
+
browserOnlyTests.push(file);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Extract test names from Node-accessible test files (node-only and universal)
|
|
55
|
+
const nodeAccessibleTests = [...nodeOnlyTests, ...universalTests];
|
|
37
56
|
const nodeTestsWithNames = await Promise.all(
|
|
38
|
-
|
|
57
|
+
nodeAccessibleTests.map(async file => {
|
|
39
58
|
try {
|
|
40
59
|
// Convert forward slashes back to OS-specific path separators for import
|
|
41
60
|
const normalizedFile = file.replace(/\//g, path.sep);
|
|
@@ -48,13 +67,18 @@ export default async (flags, args) => {
|
|
|
48
67
|
}
|
|
49
68
|
})
|
|
50
69
|
);
|
|
70
|
+
|
|
71
|
+
// Separate the results back into node-only and universal
|
|
72
|
+
const nodeOnlyWithNames = nodeTestsWithNames.filter(test => test.file.endsWith('.node-test.js'));
|
|
73
|
+
const universalWithNames = nodeTestsWithNames.filter(test => test.file.endsWith('.test.js'));
|
|
51
74
|
|
|
52
|
-
// Browser tests just return file names (test names will be extracted client-side)
|
|
53
|
-
const
|
|
75
|
+
// Browser-only tests just return file names (test names will be extracted client-side)
|
|
76
|
+
const browserOnlyWithNames = browserOnlyTests.map(file => ({ file, testNames: null }));
|
|
54
77
|
|
|
55
78
|
const result = {
|
|
56
|
-
nodeTests:
|
|
57
|
-
browserTests:
|
|
79
|
+
nodeTests: nodeOnlyWithNames,
|
|
80
|
+
browserTests: browserOnlyWithNames,
|
|
81
|
+
universalTests: universalWithNames
|
|
58
82
|
};
|
|
59
83
|
|
|
60
84
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
@@ -105,12 +129,25 @@ export default async (flags, args) => {
|
|
|
105
129
|
const testNames = testNamesParam ? testNamesParam.split(',') : [];
|
|
106
130
|
const showBrowserParam = url.searchParams.get('showBrowser');
|
|
107
131
|
const showBrowser = showBrowserParam === 'true';
|
|
108
|
-
|
|
132
|
+
const delayMs = Math.max(0, parseInt(url.searchParams.get('delayMs')||'0', 10) || 0);
|
|
133
|
+
const environment = url.searchParams.get('environment'); // 'node', 'browser', or null for auto-detect
|
|
109
134
|
|
|
110
135
|
try {
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
136
|
+
let isBrowserTest, isNodeTest;
|
|
137
|
+
|
|
138
|
+
if (environment === 'node') {
|
|
139
|
+
// Force Node-only execution
|
|
140
|
+
isBrowserTest = false;
|
|
141
|
+
isNodeTest = true;
|
|
142
|
+
} else if (environment === 'browser') {
|
|
143
|
+
// Force Browser-only execution
|
|
144
|
+
isBrowserTest = true;
|
|
145
|
+
isNodeTest = false;
|
|
146
|
+
} else {
|
|
147
|
+
// Auto-detect based on file extension (original behavior)
|
|
148
|
+
isBrowserTest = testFile.endsWith('.browser-test.js') || testFile.endsWith('.test.js');
|
|
149
|
+
isNodeTest = testFile.endsWith('.node-test.js') || testFile.endsWith('.test.js');
|
|
150
|
+
}
|
|
114
151
|
|
|
115
152
|
if (!isBrowserTest && !isNodeTest) {
|
|
116
153
|
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
@@ -138,6 +175,7 @@ export default async (flags, args) => {
|
|
|
138
175
|
res.end(JSON.stringify({
|
|
139
176
|
testFile,
|
|
140
177
|
testNames,
|
|
178
|
+
environment: environment || (isBrowserTest && isNodeTest ? 'both' : (isBrowserTest ? 'browser' : 'node')),
|
|
141
179
|
results: fileResults
|
|
142
180
|
}));
|
|
143
181
|
} catch (error) {
|
|
File without changes
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { spawn } from 'child_process';
|
|
2
2
|
|
|
3
3
|
const runWithArgs = (args, timeoutMs = 3000) => new Promise((resolve) => {
|
|
4
|
-
const child = spawn(process.execPath, ['index.js', ...args], {
|
|
4
|
+
const child = spawn(process.execPath, ['index.js', ...args, '--debug-flags'], {
|
|
5
5
|
cwd: process.cwd(),
|
|
6
6
|
stdio: ['ignore', 'pipe', 'pipe']
|
|
7
7
|
});
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fibonacci sequence utility functions
|
|
3
|
+
* Works in both Node.js and browser environments
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Generate the nth Fibonacci number (0-indexed)
|
|
8
|
+
* @param {number} n - The position in the sequence (0-indexed)
|
|
9
|
+
* @returns {number} The Fibonacci number at position n
|
|
10
|
+
* @throws {Error} If n is negative or not a number
|
|
11
|
+
*/
|
|
12
|
+
const fibonacci = (n) => {
|
|
13
|
+
if(typeof n !== 'number' || !Number.isInteger(n)) {
|
|
14
|
+
throw new Error('Input must be a non-negative integer');
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
if(n < 0) {
|
|
18
|
+
throw new Error('Input must be a non-negative integer');
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if(n === 0) return 0;
|
|
22
|
+
if(n === 1) return 1;
|
|
23
|
+
|
|
24
|
+
let a = 0;
|
|
25
|
+
let b = 1;
|
|
26
|
+
|
|
27
|
+
for(let i = 2; i <= n; i++) {
|
|
28
|
+
const temp = a + b;
|
|
29
|
+
a = b;
|
|
30
|
+
b = temp;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return b;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Generate a Fibonacci sequence up to n terms
|
|
38
|
+
* @param {number} count - Number of terms to generate
|
|
39
|
+
* @returns {number[]} Array containing the Fibonacci sequence
|
|
40
|
+
* @throws {Error} If count is negative or not a number
|
|
41
|
+
*/
|
|
42
|
+
const fibonacciSequence = (count) => {
|
|
43
|
+
if(typeof count !== 'number' || !Number.isInteger(count)) {
|
|
44
|
+
throw new Error('Count must be a non-negative integer');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if(count < 0) {
|
|
48
|
+
throw new Error('Count must be a non-negative integer');
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if(count === 0) return [];
|
|
52
|
+
if(count === 1) return [0];
|
|
53
|
+
if(count === 2) return [0, 1];
|
|
54
|
+
|
|
55
|
+
const sequence = [0, 1];
|
|
56
|
+
|
|
57
|
+
for(let i = 2; i < count; i++) {
|
|
58
|
+
sequence.push(sequence[i - 1] + sequence[i - 2]);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return sequence;
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Check if a number is a Fibonacci number
|
|
66
|
+
* @param {number} num - The number to check
|
|
67
|
+
* @returns {boolean} True if the number is in the Fibonacci sequence
|
|
68
|
+
*/
|
|
69
|
+
const isFibonacci = (num) => {
|
|
70
|
+
if(typeof num !== 'number' || !Number.isInteger(num) || num < 0) {
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if(num === 0 || num === 1) return true;
|
|
75
|
+
|
|
76
|
+
let a = 0;
|
|
77
|
+
let b = 1;
|
|
78
|
+
|
|
79
|
+
while (b < num) {
|
|
80
|
+
const temp = a + b;
|
|
81
|
+
a = b;
|
|
82
|
+
b = temp;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return b === num;
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
export default {
|
|
89
|
+
fibonacci,
|
|
90
|
+
fibonacciSequence,
|
|
91
|
+
isFibonacci
|
|
92
|
+
};
|
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
// Import the fibonacci utility functions
|
|
2
|
+
import fibonacciUtils from './fibonacci.js';
|
|
3
|
+
|
|
4
|
+
const { fibonacci, fibonacciSequence, isFibonacci } = fibonacciUtils;
|
|
5
|
+
|
|
6
|
+
/*
|
|
7
|
+
Lifecycle Callbacks
|
|
8
|
+
*/
|
|
9
|
+
export const beforeAll = async (log) => {
|
|
10
|
+
log('Setting up Fibonacci utility tests...');
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export const beforeEach = async (log) => {
|
|
14
|
+
log('Running Fibonacci test');
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export const afterEach = async (log) => {
|
|
18
|
+
log('Fibonacci test completed');
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export const afterAll = async (log) => {
|
|
22
|
+
log('Fibonacci utility tests finished');
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
/*
|
|
26
|
+
Test Cases
|
|
27
|
+
*/
|
|
28
|
+
export default {
|
|
29
|
+
'fibonacci - should return correct values for base cases': async ({pass, fail, log}) => {
|
|
30
|
+
try {
|
|
31
|
+
const testCases = [
|
|
32
|
+
{ input: 0, expected: 0 },
|
|
33
|
+
{ input: 1, expected: 1 },
|
|
34
|
+
{ input: 2, expected: 1 }
|
|
35
|
+
];
|
|
36
|
+
|
|
37
|
+
for(const { input, expected } of testCases) {
|
|
38
|
+
const result = fibonacci(input);
|
|
39
|
+
if(result !== expected) {
|
|
40
|
+
fail(`fibonacci(${input}) expected ${expected}, got ${result}`);
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
log(`✓ fibonacci(${input}) = ${result}`);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
pass('Base cases work correctly');
|
|
47
|
+
} catch (error) {
|
|
48
|
+
fail(`Unexpected error: ${error.message}`);
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
|
|
52
|
+
'fibonacci - should calculate correct sequence values': async ({pass, fail, log}) => {
|
|
53
|
+
try {
|
|
54
|
+
const testCases = [
|
|
55
|
+
{ input: 3, expected: 2 },
|
|
56
|
+
{ input: 4, expected: 3 },
|
|
57
|
+
{ input: 5, expected: 5 },
|
|
58
|
+
{ input: 6, expected: 8 },
|
|
59
|
+
{ input: 7, expected: 13 },
|
|
60
|
+
{ input: 8, expected: 21 },
|
|
61
|
+
{ input: 10, expected: 55 }
|
|
62
|
+
];
|
|
63
|
+
|
|
64
|
+
for(const { input, expected } of testCases) {
|
|
65
|
+
const result = fibonacci(input);
|
|
66
|
+
if(result !== expected) {
|
|
67
|
+
fail(`fibonacci(${input}) expected ${expected}, got ${result}`);
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
log(`✓ fibonacci(${input}) = ${result}`);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
pass('Sequence calculations are correct');
|
|
74
|
+
} catch (error) {
|
|
75
|
+
fail(`Unexpected error: ${error.message}`);
|
|
76
|
+
}
|
|
77
|
+
},
|
|
78
|
+
|
|
79
|
+
'fibonacci - should handle large numbers efficiently': async ({pass, fail, log}) => {
|
|
80
|
+
try {
|
|
81
|
+
const result = fibonacci(20);
|
|
82
|
+
const expected = 6765;
|
|
83
|
+
|
|
84
|
+
if(result !== expected) {
|
|
85
|
+
fail(`fibonacci(20) expected ${expected}, got ${result}`);
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
log(`✓ fibonacci(20) = ${result}`);
|
|
90
|
+
pass('Large number calculation works efficiently');
|
|
91
|
+
} catch (error) {
|
|
92
|
+
fail(`Unexpected error: ${error.message}`);
|
|
93
|
+
}
|
|
94
|
+
},
|
|
95
|
+
|
|
96
|
+
'fibonacci - should throw error for negative numbers': async ({pass, fail, log}) => {
|
|
97
|
+
try {
|
|
98
|
+
fibonacci(-1);
|
|
99
|
+
fail('Should have thrown an error for negative input');
|
|
100
|
+
} catch (error) {
|
|
101
|
+
if(error.message === 'Input must be a non-negative integer') {
|
|
102
|
+
log('✓ Properly rejects negative numbers');
|
|
103
|
+
pass('Input validation for negative numbers works');
|
|
104
|
+
} else {
|
|
105
|
+
fail(`Expected specific error message, got: ${error.message}`);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
},
|
|
109
|
+
|
|
110
|
+
'fibonacci - should throw error for non-integer inputs': async ({pass, fail, log}) => {
|
|
111
|
+
try {
|
|
112
|
+
const testInputs = [3.14, 'string', null, undefined, {}];
|
|
113
|
+
|
|
114
|
+
for(const input of testInputs) {
|
|
115
|
+
try {
|
|
116
|
+
fibonacci(input);
|
|
117
|
+
fail(`Should have thrown an error for input: ${input}`);
|
|
118
|
+
return;
|
|
119
|
+
} catch (error) {
|
|
120
|
+
if(error.message === 'Input must be a non-negative integer') {
|
|
121
|
+
log(`✓ Properly rejects invalid input: ${input}`);
|
|
122
|
+
} else {
|
|
123
|
+
fail(`Expected specific error message for ${input}, got: ${error.message}`);
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
pass('Input validation for non-integers works');
|
|
130
|
+
} catch (error) {
|
|
131
|
+
fail(`Unexpected error: ${error.message}`);
|
|
132
|
+
}
|
|
133
|
+
},
|
|
134
|
+
|
|
135
|
+
'fibonacciSequence - should return empty array for count 0': async ({pass, fail, log}) => {
|
|
136
|
+
try {
|
|
137
|
+
const result = fibonacciSequence(0);
|
|
138
|
+
|
|
139
|
+
if(!Array.isArray(result) || result.length !== 0) {
|
|
140
|
+
fail(`Expected empty array, got: ${JSON.stringify(result)}`);
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
log('✓ Returns empty array for count 0');
|
|
145
|
+
pass('Empty sequence handling works');
|
|
146
|
+
} catch (error) {
|
|
147
|
+
fail(`Unexpected error: ${error.message}`);
|
|
148
|
+
}
|
|
149
|
+
},
|
|
150
|
+
|
|
151
|
+
'fibonacciSequence - should return correct sequences': async ({pass, fail, log}) => {
|
|
152
|
+
try {
|
|
153
|
+
const testCases = [
|
|
154
|
+
{ count: 1, expected: [0] },
|
|
155
|
+
{ count: 2, expected: [0, 1] },
|
|
156
|
+
{ count: 5, expected: [0, 1, 1, 2, 3] },
|
|
157
|
+
{ count: 8, expected: [0, 1, 1, 2, 3, 5, 8, 13] }
|
|
158
|
+
];
|
|
159
|
+
|
|
160
|
+
for(const { count, expected } of testCases) {
|
|
161
|
+
const result = fibonacciSequence(count);
|
|
162
|
+
|
|
163
|
+
if(JSON.stringify(result) !== JSON.stringify(expected)) {
|
|
164
|
+
fail(`fibonacciSequence(${count}) expected ${JSON.stringify(expected)}, got ${JSON.stringify(result)}`);
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
log(`✓ fibonacciSequence(${count}) = [${result.join(', ')}]`);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
pass('Sequence generation works correctly');
|
|
172
|
+
} catch (error) {
|
|
173
|
+
fail(`Unexpected error: ${error.message}`);
|
|
174
|
+
}
|
|
175
|
+
},
|
|
176
|
+
|
|
177
|
+
'fibonacciSequence - should validate input parameters': async ({pass, fail, log}) => {
|
|
178
|
+
try {
|
|
179
|
+
const invalidInputs = [-1, 3.14, 'string', null];
|
|
180
|
+
|
|
181
|
+
for(const input of invalidInputs) {
|
|
182
|
+
try {
|
|
183
|
+
fibonacciSequence(input);
|
|
184
|
+
fail(`Should have thrown an error for input: ${input}`);
|
|
185
|
+
return;
|
|
186
|
+
} catch (error) {
|
|
187
|
+
if(error.message === 'Count must be a non-negative integer') {
|
|
188
|
+
log(`✓ Properly rejects invalid input: ${input}`);
|
|
189
|
+
} else {
|
|
190
|
+
fail(`Expected specific error message for ${input}, got: ${error.message}`);
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
pass('Sequence input validation works');
|
|
197
|
+
} catch (error) {
|
|
198
|
+
fail(`Unexpected error: ${error.message}`);
|
|
199
|
+
}
|
|
200
|
+
},
|
|
201
|
+
|
|
202
|
+
'isFibonacci - should correctly identify Fibonacci numbers': async ({pass, fail, log}) => {
|
|
203
|
+
try {
|
|
204
|
+
const fibonacciNumbers = [0, 1, 2, 3, 5, 8, 13, 21, 34, 55];
|
|
205
|
+
const nonFibonacciNumbers = [4, 6, 7, 9, 10, 11, 12, 14, 15, 16];
|
|
206
|
+
|
|
207
|
+
for(const num of fibonacciNumbers) {
|
|
208
|
+
const result = isFibonacci(num);
|
|
209
|
+
if(!result) {
|
|
210
|
+
fail(`isFibonacci(${num}) should return true, got false`);
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
log(`✓ ${num} is correctly identified as Fibonacci`);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
for(const num of nonFibonacciNumbers) {
|
|
217
|
+
const result = isFibonacci(num);
|
|
218
|
+
if(result) {
|
|
219
|
+
fail(`isFibonacci(${num}) should return false, got true`);
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
log(`✓ ${num} is correctly identified as non-Fibonacci`);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
pass('Fibonacci number identification works correctly');
|
|
226
|
+
} catch (error) {
|
|
227
|
+
fail(`Unexpected error: ${error.message}`);
|
|
228
|
+
}
|
|
229
|
+
},
|
|
230
|
+
|
|
231
|
+
'isFibonacci - should handle edge cases and invalid inputs': async ({pass, fail, log}) => {
|
|
232
|
+
try {
|
|
233
|
+
const invalidInputs = [
|
|
234
|
+
{ input: -1, expected: false, desc: 'negative number' },
|
|
235
|
+
{ input: 3.14, expected: false, desc: 'decimal number' },
|
|
236
|
+
{ input: 'string', expected: false, desc: 'string input' },
|
|
237
|
+
{ input: null, expected: false, desc: 'null input' },
|
|
238
|
+
{ input: undefined, expected: false, desc: 'undefined input' }
|
|
239
|
+
];
|
|
240
|
+
|
|
241
|
+
for(const { input, expected, desc } of invalidInputs) {
|
|
242
|
+
const result = isFibonacci(input);
|
|
243
|
+
if(result !== expected) {
|
|
244
|
+
fail(`isFibonacci(${input}) for ${desc} expected ${expected}, got ${result}`);
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
log(`✓ ${desc} handled correctly`);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
pass('Edge case handling works correctly');
|
|
251
|
+
} catch (error) {
|
|
252
|
+
fail(`Unexpected error: ${error.message}`);
|
|
253
|
+
}
|
|
254
|
+
},
|
|
255
|
+
|
|
256
|
+
'integration - should work together across all functions': async ({pass, fail, log}) => {
|
|
257
|
+
try {
|
|
258
|
+
// Generate a sequence and verify each number
|
|
259
|
+
const sequence = fibonacciSequence(10);
|
|
260
|
+
log(`Generated sequence: [${sequence.join(', ')}]`);
|
|
261
|
+
|
|
262
|
+
// Verify each number in the sequence using fibonacci function
|
|
263
|
+
for(let i = 0; i < sequence.length; i++) {
|
|
264
|
+
const expected = fibonacci(i);
|
|
265
|
+
if(sequence[i] !== expected) {
|
|
266
|
+
fail(`Sequence position ${i}: expected ${expected}, got ${sequence[i]}`);
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// Verify each number is identified as Fibonacci
|
|
272
|
+
for(const num of sequence) {
|
|
273
|
+
if(!isFibonacci(num)) {
|
|
274
|
+
fail(`Number ${num} from sequence not identified as Fibonacci`);
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
log('✓ All functions work together correctly');
|
|
280
|
+
pass('Integration test passed successfully');
|
|
281
|
+
} catch (error) {
|
|
282
|
+
fail(`Integration test failed: ${error.message}`);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
};
|