kempo-testing-framework 1.2.2 → 1.2.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.
@@ -7,7 +7,7 @@
7
7
 
8
8
  ### GUI
9
9
 
10
- All files served by the GUI should be in the `gui/` directory, with the exception of scripts shared with the CLI, custom endpoints, and node_modules like essential.css (which should have custom endpoints).
10
+ All files served by the GUI should be in the `gui/` directory, with the exception of scripts shared with the CLI, custom endpoints, and node_modules like kempo.css (which should have custom endpoints).
11
11
 
12
12
  ## Coding Style Guidelines
13
13
 
package/CONTRIBUTING.md CHANGED
@@ -7,7 +7,7 @@
7
7
 
8
8
  ### GUI
9
9
 
10
- All files served by the GUI should be in the `gui/` directory, with the exception of scripts shared with the CLI, custom endpoints, and node_modules like essential.css (which should have custom endpoints).
10
+ All files served by the GUI should be in the `gui/` directory, with the exception of scripts shared with the CLI, custom endpoints, and node_modules like kempo.css (which should have custom endpoints).
11
11
 
12
12
  ## Coding Style Guidelines
13
13
 
@@ -28,7 +28,7 @@ window.customElements.define('ktf-collapsible', class extends LitElement {
28
28
  */
29
29
  render(){
30
30
  return html`
31
- <link rel="stylesheet" href="/essential.css">
31
+ <link rel="stylesheet" href="/kempo.css">
32
32
  <div class="header">
33
33
  <div class="actions">
34
34
  <slot name="actions"></slot>
@@ -54,7 +54,7 @@ window.customElements.define('ktf-logs', class extends LitElement {
54
54
  });
55
55
  if (!filtered.length) return html``;
56
56
  return html`
57
- <link rel="stylesheet" href="/essential.css">
57
+ <link rel="stylesheet" href="/kempo.css">
58
58
  <ktf-collapsible opened>
59
59
  <span slot="title"><ktf-icon name="logs"></ktf-icon> Logs</span>
60
60
  <pre class="bg-alt -mx -mt p mb0 rb">${filtered.map(l => html`<div class="${l?.type || 'log'}">${l?.message ?? ''}</div>`)}</pre>
@@ -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
  }
@@ -46,7 +46,7 @@ window.customElements.define('ktf-test', class extends LitElement {
46
46
  render(){
47
47
  if(!this.file || !this.name) return html``;
48
48
  return html`
49
- <link rel="stylesheet" href="/essential.css">
49
+ <link rel="stylesheet" href="/kempo.css">
50
50
  <ktf-collapsible>
51
51
  <span slot="title" id="title" class="-ml">
52
52
  ${this.name}
@@ -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 {}
@@ -136,7 +136,7 @@ class TestSuiteEl extends LitElement {
136
136
  if(!this.file || !this.testNames.length) return html``;
137
137
  const titleText = this.file.replace('tests/', '').replace('.node-test.js', '').replace('.browser-test.js', '').replace('.test.js', '');
138
138
  return html`
139
- <link rel="stylesheet" href="/essential.css">
139
+ <link rel="stylesheet" href="/kempo.css">
140
140
  <ktf-collapsible>
141
141
  <span slot="title" id="title" class="-ml">${titleText}</span>
142
142
  <div slot="actions">
@@ -126,7 +126,7 @@ window.customElements.define('ktf-test-summary', class extends LitElement {
126
126
  const f = this.fileCounts;
127
127
  const t = this.testCounts;
128
128
  return html`
129
- <link rel="stylesheet" href="/essential.css">
129
+ <link rel="stylesheet" href="/kempo.css">
130
130
  <ktf-collapsible opened>
131
131
  <span slot="title" class="-ml"><b>Test Summary</b></span>
132
132
  <div slot="actions">
package/gui/index.html CHANGED
@@ -4,7 +4,30 @@
4
4
  <meta charset="UTF-8">
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
6
  <title>Kempo Testing Library GUI</title>
7
- <link rel="stylesheet" href="/essential.css" />
7
+ <link rel="stylesheet" href="/kempo.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
- const testFiles = await(await fetch('/testFiles')).json();
74
- for (const browserTest of testFiles.browserTests) {
75
- try {
76
- const moduleUrl = `/test/${browserTest.file}`;
77
- const testModule = await import(moduleUrl);
78
- browserTest.testNames = testModule.default ? Object.keys(testModule.default) : [];
79
- } catch (error) {
80
- console.error(`Error importing test file ${browserTest.file}:`, error);
81
- browserTest.testNames = [];
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
- if(!testFiles?.browserTests?.length){
85
- document.getElementById('browserTestsContainer').style.display = 'none';
86
- } else {
87
- const browserTestsContainer = document.getElementById('browserTests');
88
- testFiles.browserTests.forEach(browserTest => {
89
- const testFileElement = document.createElement('ktf-test-suite');
90
- testFileElement.file = browserTest.file;
91
- testFileElement.testNames = browserTest.testNames;
92
- browserTestsContainer.appendChild(testFileElement);
93
- });
94
- }
95
- if(!testFiles?.nodeTests?.length){
96
- document.getElementById('nodeTestsContainer').style.display = 'none';
97
- } else {
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
- testFiles.nodeTests.forEach(nodeTest => {
100
- const testFileElement = document.createElement('ktf-test-suite');
101
- testFileElement.file = nodeTest.file;
102
- testFileElement.testNames = nodeTest.testNames || [];
103
- nodeTestsContainer.appendChild(testFileElement);
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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kempo-testing-framework",
3
- "version": "1.2.2",
3
+ "version": "1.2.4",
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": {
@@ -15,7 +15,7 @@
15
15
  "license": "ISC",
16
16
  "type": "module",
17
17
  "dependencies": {
18
- "essentialcss": "^3.0.0",
18
+ "kempo-css": "^1.0.2",
19
19
  "puppeteer": "^24.9.0"
20
20
  }
21
21
  }
@@ -27,15 +27,15 @@ export const startServer = async (_port = 3000) => {
27
27
  const filePath = path.join(__dirname, 'runTests.js');
28
28
  res.end(await readFile(filePath, 'utf8'));
29
29
  }
30
- // Serve essential.css like the GUI server does
31
- else if (basePath === '/essential.css') {
30
+ // Serve kempo.css like the GUI server does
31
+ else if (basePath === '/kempo.css') {
32
32
  try {
33
- const cssPath = path.join(__dirname, '../node_modules/essentialcss/dist/essential.min.css');
33
+ const cssPath = path.join(__dirname, '../node_modules/kempo-css/dist/kempo.min.css');
34
34
  const css = await readFile(cssPath, 'utf8');
35
35
  res.writeHead(200, { 'Content-Type': 'text/css' });
36
36
  res.end(css);
37
37
  } catch (error) {
38
- console.error(`Error serving essential.css:`, error);
38
+ console.error(`Error serving kempo.css:`, error);
39
39
  res.writeHead(404);
40
40
  res.end('Not found');
41
41
  }
package/src/gui.js CHANGED
@@ -19,11 +19,11 @@ export default async (flags, args) => {
19
19
  */
20
20
 
21
21
  /*
22
- Serve essential.css
22
+ Serve kempo.css
23
23
  */
24
- if(basePath === '/essential.css'){
24
+ if(basePath === '/kempo.css'){
25
25
  res.writeHead(200, { 'Content-Type': 'text/css' });
26
- res.end(await readFile(path.join(__dirname, '../node_modules/essentialcss/dist/essential.min.css'), 'utf8'));
26
+ res.end(await readFile(path.join(__dirname, '../node_modules/kempo-css/dist/kempo.min.css'), 'utf8'));
27
27
  }
28
28
 
29
29
  /*
@@ -33,9 +33,28 @@ export default async (flags, args) => {
33
33
  try {
34
34
  const testFiles = await findTests('', '', true, true);
35
35
 
36
- // Extract test names from Node test files (safe to import in Node.js)
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
- testFiles.nodeTests.map(async file => {
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 browserTestsWithNames = testFiles.browserTests.map(file => ({ file, testNames: null }));
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: nodeTestsWithNames,
57
- browserTests: browserTestsWithNames
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
- const delayMs = Math.max(0, parseInt(url.searchParams.get('delayMs')||'0', 10) || 0);
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
- // Determine if this is a browser or node test based on file extension
112
- const isBrowserTest = testFile.endsWith('.browser-test.js') || testFile.endsWith('.test.js');
113
- const isNodeTest = testFile.endsWith('.node-test.js') || testFile.endsWith('.test.js');
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
@@ -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
+ };