kempo-testing-framework 1.3.7 → 1.3.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/.github/workflows/publish-major.yml +39 -0
  2. package/.github/workflows/publish-minor.yml +39 -0
  3. package/.github/workflows/{publish-npm.yml → publish-patch.yml} +1 -1
  4. package/README.md +33 -0
  5. package/gui/components/TestFramework.js +6 -6
  6. package/gui/components/settingsStore.js +1 -0
  7. package/gui/index.html +7 -29
  8. package/index.js +4 -1
  9. package/package.json +1 -1
  10. package/src/browserTestServer.js +8 -2
  11. package/src/cli.js +5 -0
  12. package/src/findTests.js +11 -3
  13. package/src/gui.js +32 -2
  14. package/src/runBrowserTests.js +3 -2
  15. package/src/runTestFiles.js +5 -2
  16. package/src/runTests.js +32 -12
  17. package/tests/{cli-flags.node-test.js → cli/flags.node-test.js} +8 -0
  18. package/tests/{counter.browser-test.js → components/counter.browser-test.js} +1 -1
  19. package/tests/{custom-page.browser-test.js → examples/custom-page.browser-test.js} +1 -1
  20. package/tests/{fibonacci.test.js → examples/fibonacci.test.js} +1 -1
  21. package/tests/nested/deep/deep.node-test.js +9 -0
  22. package/tests/nested/nested.browser-test.js +9 -0
  23. package/tests/{src-browserTestServer.node-test.js → src/browserTestServer.node-test.js} +3 -3
  24. package/tests/{src-findTests.node-test.js → src/findTests.node-test.js} +20 -3
  25. package/tests/{src-logLevels.node-test.js → src/logLevels.node-test.js} +1 -1
  26. package/tests/{src-runBrowserTests.node-test.js → src/runBrowserTests.node-test.js} +4 -4
  27. package/tests/{src-runTestFiles.node-test.js → src/runTestFiles.node-test.js} +1 -1
  28. package/tests/{src-runTests.node-test.js → src/runTests.node-test.js} +1 -1
  29. package/tests/src/timeout.node-test.js +66 -0
  30. /package/tests/{cli-help.node-test.js → cli/help.node-test.js} +0 -0
  31. /package/tests/{cli-loglevel.node-test.js → cli/loglevel.node-test.js} +0 -0
  32. /package/tests/{collapsible.browser-test.js → components/collapsible.browser-test.js} +0 -0
  33. /package/tests/{icon.browser-test.js → components/icon.browser-test.js} +0 -0
  34. /package/tests/{logs.browser-test.js → components/logs.browser-test.js} +0 -0
  35. /package/tests/{setting-checkbox.browser-test.js → components/setting-checkbox.browser-test.js} +0 -0
  36. /package/tests/{setting-number.browser-test.js → components/setting-number.browser-test.js} +0 -0
  37. /package/tests/{setting-select.browser-test.js → components/setting-select.browser-test.js} +0 -0
  38. /package/tests/{settings-store.browser-test.js → components/settings-store.browser-test.js} +0 -0
  39. /package/tests/{test-framework.browser-test.js → components/test-framework.browser-test.js} +0 -0
  40. /package/tests/{test-summary.browser-test.js → components/test-summary.browser-test.js} +0 -0
  41. /package/tests/{test.browser-test.js → components/test.browser-test.js} +0 -0
  42. /package/tests/{testfile.browser-test.js → components/testfile.browser-test.js} +0 -0
  43. /package/tests/{theme.browser-test.js → components/theme.browser-test.js} +0 -0
  44. /package/tests/{example.node-test.js → examples/example.node-test.js} +0 -0
  45. /package/tests/{Counter.js → fixtures/Counter.js} +0 -0
  46. /package/tests/{custom-test-page.html → fixtures/custom-test-page.html} +0 -0
  47. /package/tests/{fibonacci.js → fixtures/fibonacci.js} +0 -0
  48. /package/tests/{src-cli.node-test.js → src/cli.node-test.js} +0 -0
@@ -0,0 +1,39 @@
1
+ name: Publish Major Version to npmjs
2
+
3
+ on:
4
+ workflow_dispatch:
5
+
6
+ jobs:
7
+ publish:
8
+ runs-on: ubuntu-latest
9
+ permissions:
10
+ contents: write
11
+ id-token: write # enables npm provenance
12
+ steps:
13
+ - uses: actions/checkout@v4
14
+ with:
15
+ token: ${{ secrets.GITHUB_TOKEN }}
16
+
17
+ # Setup Node and create an .npmrc that uses the token from NODE_AUTH_TOKEN
18
+ - uses: actions/setup-node@v4
19
+ with:
20
+ node-version: '20.x'
21
+ registry-url: 'https://registry.npmjs.org'
22
+
23
+ - run: npm ci
24
+
25
+ # Auto-increment major version, commit, and push
26
+ - name: Bump major version
27
+ run: |
28
+ git config --global user.name "github-actions"
29
+ git config --global user.email "github-actions@github.com"
30
+ npm version major --no-git-tag-version
31
+ git add package.json package-lock.json || true
32
+ git commit -m "ci: bump major version [skip ci]" || echo "No changes to commit"
33
+ git push origin HEAD:main || echo "No changes to push"
34
+ env:
35
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
36
+
37
+ - run: npm publish --provenance --access public
38
+ env:
39
+ NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
@@ -0,0 +1,39 @@
1
+ name: Publish Minor Version to npmjs
2
+
3
+ on:
4
+ workflow_dispatch:
5
+
6
+ jobs:
7
+ publish:
8
+ runs-on: ubuntu-latest
9
+ permissions:
10
+ contents: write
11
+ id-token: write # enables npm provenance
12
+ steps:
13
+ - uses: actions/checkout@v4
14
+ with:
15
+ token: ${{ secrets.GITHUB_TOKEN }}
16
+
17
+ # Setup Node and create an .npmrc that uses the token from NODE_AUTH_TOKEN
18
+ - uses: actions/setup-node@v4
19
+ with:
20
+ node-version: '20.x'
21
+ registry-url: 'https://registry.npmjs.org'
22
+
23
+ - run: npm ci
24
+
25
+ # Auto-increment minor version, commit, and push
26
+ - name: Bump minor version
27
+ run: |
28
+ git config --global user.name "github-actions"
29
+ git config --global user.email "github-actions@github.com"
30
+ npm version minor --no-git-tag-version
31
+ git add package.json package-lock.json || true
32
+ git commit -m "ci: bump minor version [skip ci]" || echo "No changes to commit"
33
+ git push origin HEAD:main || echo "No changes to push"
34
+ env:
35
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
36
+
37
+ - run: npm publish --provenance --access public
38
+ env:
39
+ NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
@@ -1,5 +1,5 @@
1
1
 
2
- name: Publish Package to npmjs
2
+ name: Publish Patch Version to npmjs
3
3
 
4
4
  on:
5
5
  push:
package/README.md CHANGED
@@ -56,6 +56,26 @@ Kempo supports three types of test files:
56
56
 
57
57
  If your code is intended to run in both Node and the browser, you should write a single test file named `[name].test.js.`, otherwise use the environment specific file names `[name].browser-test.js` and/or `[name]node-test.js`.
58
58
 
59
+ ## Test Organization
60
+
61
+ Kempo automatically discovers tests in the `tests` directory and supports organizing tests in subdirectories for better project structure. You can organize your tests however makes sense for your project:
62
+
63
+ ```
64
+ tests/
65
+ ├── components/ # UI component tests
66
+ │ ├── button.browser-test.js
67
+ │ └── modal.browser-test.js
68
+ ├── api/ # API-related tests
69
+ │ ├── auth.node-test.js
70
+ │ └── users.node-test.js
71
+ ├── integration/ # Integration tests
72
+ │ └── workflow.test.js
73
+ └── utils/ # Utility function tests
74
+ └── helpers.test.js
75
+ ```
76
+
77
+ The framework will recursively search all subdirectories within `tests/` to find test files, so you can nest directories as deeply as needed to match your project structure.
78
+
59
79
  ## Writing Tests
60
80
 
61
81
  ### Lifecycle Callbacks
@@ -272,6 +292,13 @@ Set the verbosity of output:
272
292
  - Specify a browser pause delay in milliseconds (applies before and after browser tests when the browser window is shown)
273
293
  - Example: `npx kempo-test -b --show-browser --delay 2000`
274
294
 
295
+ **`--timeout` or `-t`**
296
+ - Set the timeout for individual tests in milliseconds (default: 30000ms)
297
+ - Tests that run longer than this timeout will be terminated and marked as failed
298
+ - Minimum value: 1000ms (1 second)
299
+ - Example: `npx kempo-test --timeout 60000` (60 second timeout)
300
+ - Example: `npx kempo-test -t 10000` (10 second timeout)
301
+
275
302
  ### Combining Flags
276
303
 
277
304
  You can combine multiple flags for precise control:
@@ -288,6 +315,12 @@ npx kempo-test -l silent payment
288
315
 
289
316
  # Run browser tests with visible browser window, custom port and delay
290
317
  npx kempo-test -b --show-browser --port 8080 --delay 2000
318
+
319
+ # Run tests with a custom timeout for slow-running tests
320
+ npx kempo-test --timeout 60000
321
+
322
+ # Run browser tests with custom timeout and visible browser
323
+ npx kempo-test -b --show-browser --timeout 10000
291
324
  ```
292
325
 
293
326
  ## Running Specific or Filtered Tests
@@ -100,8 +100,8 @@ class TestFrameworkEl extends LitElement {
100
100
  const logsEl = el.renderRoot?.getElementById('logs');
101
101
  if (logsEl) logsEl.clear();
102
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)}`);
103
+ const { showBrowser, delayMs, timeoutMs } = getSettings();
104
+ const resp = await fetch(`/runTest?testFile=${encodeURIComponent(file)}&testNames=${encodeURIComponent(name)}&showBrowser=${!!showBrowser}&delayMs=${Number(delayMs||0)}&timeoutMs=${Number(timeoutMs||30000)}`);
105
105
  let data = null;
106
106
  try { data = await resp.json(); } catch {}
107
107
  if(!resp.ok || (data && data.error)){
@@ -165,15 +165,15 @@ class TestFrameworkEl extends LitElement {
165
165
  if(fileLogsEl) fileLogsEl.clear();
166
166
  } catch {}
167
167
 
168
- const { showBrowser, delayMs } = getSettings();
168
+ const { showBrowser, delayMs, timeoutMs } = getSettings();
169
169
 
170
170
  // For universal tests, run both environments
171
171
  if (isUniversal && file.endsWith('.test.js')) {
172
172
  try {
173
173
  // Run both Node and Browser tests
174
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)}`)
175
+ fetch(`/runTest?testFile=${encodeURIComponent(file)}&environment=node&showBrowser=false&delayMs=${Number(delayMs||0)}&timeoutMs=${Number(timeoutMs||30000)}`),
176
+ fetch(`/runTest?testFile=${encodeURIComponent(file)}&environment=browser&showBrowser=${!!showBrowser}&delayMs=${Number(delayMs||0)}&timeoutMs=${Number(timeoutMs||30000)}`)
177
177
  ]);
178
178
 
179
179
  let nodeData = null, browserData = null;
@@ -305,7 +305,7 @@ class TestFrameworkEl extends LitElement {
305
305
  }
306
306
 
307
307
  // Original single-environment logic for non-universal tests
308
- const resp = await fetch(`/runTest?testFile=${encodeURIComponent(file)}&showBrowser=${!!showBrowser}&delayMs=${Number(delayMs||0)}`);
308
+ const resp = await fetch(`/runTest?testFile=${encodeURIComponent(file)}&showBrowser=${!!showBrowser}&delayMs=${Number(delayMs||0)}&timeoutMs=${Number(timeoutMs||30000)}`);
309
309
  let data = null;
310
310
  try { data = await resp.json(); } catch {}
311
311
  if(!resp.ok || (data && data.error)){
@@ -5,6 +5,7 @@ const DEFAULTS = {
5
5
  logLevel: 3,
6
6
  theme: 'auto',
7
7
  delayMs: 0,
8
+ timeoutMs: 30000,
8
9
  };
9
10
 
10
11
  let state = (() => {
package/gui/index.html CHANGED
@@ -5,29 +5,6 @@
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="/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>
31
8
  </head>
32
9
  <body>
33
10
  <main>
@@ -40,6 +17,7 @@
40
17
  <div id="delaySettingRow">
41
18
  <ktf-setting-number name="delayMs" label="Delay (ms) between tests and around browser runs" step="100" min="0" max="600000" suffix="ms"></ktf-setting-number>
42
19
  </div>
20
+ <ktf-setting-number name="timeoutMs" label="Test timeout (ms)" step="1000" min="1000" max="300000" suffix="ms"></ktf-setting-number>
43
21
 
44
22
  <ktf-setting-select name="logLevel" label="Log Level"></ktf-setting-select>
45
23
 
@@ -52,20 +30,20 @@
52
30
  <div id="universalTestsContainer">
53
31
  <h2>Universal Tests</h2>
54
32
  <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>
33
+ <div id="universalTestsLoading" class="p ta-center tc-muted" style="font-style: italic;">Loading universal tests...</div>
34
+ <div id="universalTestsEmpty" class="p bg-alt r my" style="display: none;">No universal tests found. Create test files with <code>.test.js</code> extension to run tests in both environments.</div>
57
35
  <div id="universalTests"></div>
58
36
  </div>
59
37
  <div id="nodeTestsContainer">
60
38
  <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>
39
+ <div id="nodeTestsLoading" class="p ta-center tc-muted" style="font-style: italic;">Loading Node tests...</div>
40
+ <div id="nodeTestsEmpty" class="p bg-alt r my" 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>
63
41
  <div id="nodeTests"></div>
64
42
  </div>
65
43
  <div id="browserTestsContainer">
66
44
  <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>
45
+ <div id="browserTestsLoading" class="p ta-center tc-muted" style="font-style: italic;">Loading browser tests...</div>
46
+ <div id="browserTestsEmpty" class="p bg-alt r my" 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>
69
47
  <div id="browserTests"></div>
70
48
  </div>
71
49
  </ktf-test-framework>
package/index.js CHANGED
@@ -11,6 +11,7 @@ const shortFlagMap = {
11
11
  'p': 'port',
12
12
  'g': 'gui',
13
13
  'w': 'show-browser',
14
+ 't': 'timeout',
14
15
  'h': 'help'
15
16
  };
16
17
 
@@ -22,7 +23,7 @@ const flags = {};
22
23
  const remainingArgs = [];
23
24
 
24
25
  // Define which flags take values vs booleans
25
- const valueFlags = new Set(['log-level', 'delay', 'port']);
26
+ const valueFlags = new Set(['log-level', 'delay', 'port', 'timeout', 'gui-port']);
26
27
  const booleanFlags = new Set(['browser', 'node', 'gui', 'show-browser', 'help', 'debug-flags']);
27
28
 
28
29
  for (let i = 0; i < args.length; i++) {
@@ -125,6 +126,7 @@ OPTIONS:
125
126
  -n, --node Run only node tests
126
127
  -l, --log-level LEVEL Set log level (0-4 or silent/minimal/normal/verbose/debug)
127
128
  -d, --delay MS Add delay between tests in milliseconds
129
+ -t, --timeout MS Set timeout for individual tests in milliseconds (default: 30000)
128
130
  -p, --port PORT Set port for test server (default: 3000)
129
131
  -g, --gui Launch GUI mode
130
132
  -w, --show-browser Show browser window during tests
@@ -137,6 +139,7 @@ EXAMPLES:
137
139
  kempo-test # Run all tests
138
140
  kempo-test --browser # Run only browser tests
139
141
  kempo-test -l verbose # Run with verbose logging
142
+ kempo-test -t 60000 # Set 60-second timeout for tests
140
143
  kempo-test --gui # Launch GUI mode
141
144
  kempo-test myComponent # Run tests for 'myComponent' suite
142
145
  kempo-test myComponent myTest # Run specific test in specific suite
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kempo-testing-framework",
3
- "version": "1.3.7",
3
+ "version": "1.3.9",
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": {
@@ -109,7 +109,13 @@ export const startServer = async (_port = 3000) => {
109
109
 
110
110
  export const stopServer = async () => {
111
111
  if(server){
112
- await new Promise(resolve => server.close(resolve));
113
- server = null;
112
+ // Close all connections and stop accepting new ones
113
+ await new Promise((resolve) => {
114
+ server.closeAllConnections?.(); // Close keep-alive connections if method exists
115
+ server.close(() => {
116
+ server = null;
117
+ resolve();
118
+ });
119
+ });
114
120
  }
115
121
  };
package/src/cli.js CHANGED
@@ -30,6 +30,8 @@ export default async (flags, args) => {
30
30
  const port = flags.port ? parseInt(flags.port, 10) : 3000;
31
31
  // Parse delay (milliseconds). Non-numeric or missing => 0
32
32
  const delayMs = Number.isFinite(parseInt(flags.delay, 10)) ? Math.max(0, parseInt(flags.delay, 10)) : 0;
33
+ // Parse timeout (milliseconds). Non-numeric or missing => 30000 (30 seconds)
34
+ const timeoutMs = Number.isFinite(parseInt(flags.timeout, 10)) ? Math.max(1000, parseInt(flags.timeout, 10)) : 30000;
33
35
 
34
36
  /*
35
37
  * Color Configuration
@@ -57,6 +59,7 @@ export default async (flags, args) => {
57
59
  port,
58
60
  logLevel,
59
61
  delayMs,
62
+ timeoutMs,
60
63
  onNodeTestStart: logLevel > LOG_LEVELS.MINIMAL ? (file) => {
61
64
  console.log(`${colors.cyan}Running Node test: ${file}${colors.reset}`);
62
65
  } : undefined,
@@ -192,7 +195,9 @@ const displaySummary = (nodeResults, browserResults, colors) => {
192
195
  console.log(`${colors.red}Failed:${colors.reset} ${failedTests}`);
193
196
  if(failedTests === 0){
194
197
  console.log(`\n${colors.green}${colors.bright}All tests passed!${colors.reset}`);
198
+ process.exit(0);
195
199
  } else {
196
200
  console.log(`\n${colors.red}${colors.bright}Some tests failed. See details above.${colors.reset}`);
201
+ process.exit(1);
197
202
  }
198
203
  };
package/src/findTests.js CHANGED
@@ -4,24 +4,32 @@ import path from 'path';
4
4
  const findFiles = async (testDirs, filter = '') => {
5
5
  const results = [];
6
6
 
7
- for (const dir of testDirs) {
7
+ const searchDirectory = async (dir) => {
8
8
  try {
9
9
  await fs.access(dir);
10
10
  const entries = await fs.readdir(dir, { withFileTypes: true });
11
11
 
12
12
  for (const entry of entries) {
13
+ const entryPath = path.join(dir, entry.name);
14
+
13
15
  if (entry.isFile() &&
14
16
  entry.name.endsWith('test.js') &&
15
17
  entry.name.includes(filter)) {
16
- const entryPath = path.join(dir, entry.name);
17
18
  // Normalize path to always use forward slashes for consistency across platforms
18
19
  const relativePath = path.relative(process.cwd(), entryPath).replace(/\\/g, '/');
19
20
  results.push(relativePath);
21
+ } else if (entry.isDirectory()) {
22
+ // Recursively search subdirectories
23
+ await searchDirectory(entryPath);
20
24
  }
21
25
  }
22
26
  } catch (error) {
23
- // Directory doesn't exist, skip it
27
+ // Directory doesn't exist or access denied, skip it
24
28
  }
29
+ };
30
+
31
+ for (const dir of testDirs) {
32
+ await searchDirectory(dir);
25
33
  }
26
34
  return results;
27
35
  };
package/src/gui.js CHANGED
@@ -143,6 +143,7 @@ export default async (flags, args) => {
143
143
  const showBrowserParam = url.searchParams.get('showBrowser');
144
144
  const showBrowser = showBrowserParam === 'true';
145
145
  const delayMs = Math.max(0, parseInt(url.searchParams.get('delayMs')||'0', 10) || 0);
146
+ const timeoutMs = Math.max(1000, parseInt(url.searchParams.get('timeoutMs')||'30000', 10) || 30000);
146
147
  const environment = url.searchParams.get('environment'); // 'node', 'browser', or null for auto-detect
147
148
 
148
149
  try {
@@ -177,6 +178,7 @@ export default async (flags, args) => {
177
178
  port: 3001,
178
179
  logLevel: 2,
179
180
  delayMs,
181
+ timeoutMs,
180
182
  specificFiles: [testFile]
181
183
  });
182
184
 
@@ -246,8 +248,36 @@ export default async (flags, args) => {
246
248
  }
247
249
 
248
250
  const fileContent = await readFile(filePath, ext === '.png' || ext === '.jpg' || ext === '.jpeg' ? null : 'utf8');
249
- res.writeHead(200, { 'Content-Type': contentType });
250
- res.end(fileContent);
251
+
252
+ // For index.html, inject initial settings from CLI flags
253
+ if (basePath === '/' && ext === '.html') {
254
+ const timeoutMs = Number.isFinite(parseInt(flags.timeout, 10)) ? Math.max(1000, parseInt(flags.timeout, 10)) : 30000;
255
+ const initialSettings = {
256
+ timeoutMs: timeoutMs
257
+ };
258
+
259
+ // Inject settings into the HTML
260
+ const settingsScript = `<script>
261
+ // Apply initial CLI settings
262
+ if (typeof Storage !== "undefined") {
263
+ try {
264
+ const currentSettings = JSON.parse(localStorage.getItem('ktf_settings') || '{}');
265
+ const initialSettings = ${JSON.stringify(initialSettings)};
266
+ const mergedSettings = { ...currentSettings, ...initialSettings };
267
+ localStorage.setItem('ktf_settings', JSON.stringify(mergedSettings));
268
+ } catch (e) {
269
+ console.warn('Failed to set initial settings:', e);
270
+ }
271
+ }
272
+ </script>`;
273
+
274
+ const modifiedContent = fileContent.replace('</head>', `${settingsScript}\n</head>`);
275
+ res.writeHead(200, { 'Content-Type': contentType });
276
+ res.end(modifiedContent);
277
+ } else {
278
+ res.writeHead(200, { 'Content-Type': contentType });
279
+ res.end(fileContent);
280
+ }
251
281
  } catch (error) {
252
282
  res.writeHead(404, { 'Content-Type': 'text/plain' });
253
283
  res.end('File not found');
@@ -10,7 +10,8 @@ export default async ({
10
10
  showBrowser = false,
11
11
  port = 3000,
12
12
  logLevel,
13
- delayMs = 0
13
+ delayMs = 0,
14
+ timeoutMs = 30000
14
15
  }) => {
15
16
  /*
16
17
  * Start Test Server
@@ -120,7 +121,7 @@ export default async ({
120
121
  throw new Error(`Browser test error: ${error}`);
121
122
  }
122
123
 
123
- await page.waitForFunction(() => window.results !== undefined, { timeout: 20000 });
124
+ await page.waitForFunction(() => window.results !== undefined, { timeout: timeoutMs });
124
125
  const results = await page.evaluate(() => window.results);
125
126
 
126
127
  // Optional post-delay when a visible browser is requested
@@ -14,6 +14,7 @@ import path from 'path';
14
14
  * @param {number} options.port - Port for browser tests
15
15
  * @param {number} options.logLevel - Log level for browser tests
16
16
  * @param {number} options.delayMs - Optional delay in ms to apply for browser runs
17
+ * @param {number} options.timeoutMs - Timeout in ms for individual tests (default: 30000)
17
18
  * @param {Function} options.onNodeTestStart - Callback when a node test starts
18
19
  * @param {Function} options.onBrowserTestStart - Callback when a browser test starts
19
20
  * @param {string[]} options.specificFiles - Array of specific test files to run (overrides findTests)
@@ -28,6 +29,7 @@ export default async ({
28
29
  port = 3000,
29
30
  logLevel = 2,
30
31
  delayMs = 0,
32
+ timeoutMs = 30000,
31
33
  onNodeTestStart,
32
34
  onBrowserTestStart,
33
35
  specificFiles = null
@@ -65,7 +67,7 @@ export default async ({
65
67
  // Convert forward slashes back to OS-specific path separators for file system operations
66
68
  const normalizedFile = file.replace(/\//g, path.sep);
67
69
  const module = await import(`file://${path.resolve(process.cwd(), normalizedFile)}`);
68
- nodeResults[file] = await runNodeTests(module, testFilter);
70
+ nodeResults[file] = await runNodeTests(module, testFilter, 0, timeoutMs);
69
71
  }
70
72
  }
71
73
 
@@ -82,7 +84,8 @@ export default async ({
82
84
  showBrowser,
83
85
  port,
84
86
  logLevel,
85
- delayMs
87
+ delayMs,
88
+ timeoutMs
86
89
  });
87
90
  }
88
91
  }
package/src/runTests.js CHANGED
@@ -1,12 +1,21 @@
1
1
  const wait = ms => new Promise(r => setTimeout(r, ms));
2
2
 
3
+ const withTimeout = (promise, timeoutMs, testName) => {
4
+ return Promise.race([
5
+ promise,
6
+ new Promise((_, reject) =>
7
+ setTimeout(() => reject(new Error(`Test "${testName}" timed out after ${timeoutMs}ms`)), timeoutMs)
8
+ )
9
+ ]);
10
+ };
11
+
3
12
  export default async ({
4
13
  beforeAll = async () => {},
5
14
  beforeEach = async () => {},
6
15
  afterEach = async () => {},
7
16
  afterAll = async () => {},
8
17
  default: tests
9
- } = {}, filter = false, delay = 0) => {
18
+ } = {}, filter = false, delay = 0, timeoutMs = 30000) => {
10
19
  const testsToRun = filter ? Object.keys(tests).filter(name => name.trim().toLowerCase().includes(filter.trim().toLowerCase())) : Object.keys(tests);
11
20
  if(!testsToRun.length) throw new Error('No tests found matching the filter');
12
21
 
@@ -48,17 +57,28 @@ export default async ({
48
57
  log(`== Starting Test "${name}" ==`, 'progress', 3);
49
58
  log('== Before Each ==', 'progress', 3);
50
59
  await beforeEach(log);
51
- await tests[name]({
52
- log,
53
- pass: message => {
54
- result.passed = true;
55
- log(message, 'pass', 2); // was 3; make visible at NORMAL
56
- },
57
- fail: message => {
58
- result.passed = false;
59
- log(message, 'fail', 2);
60
- }
61
- });
60
+
61
+ try {
62
+ await withTimeout(
63
+ tests[name]({
64
+ log,
65
+ pass: message => {
66
+ result.passed = true;
67
+ log(message, 'pass', 2); // was 3; make visible at NORMAL
68
+ },
69
+ fail: message => {
70
+ result.passed = false;
71
+ log(message, 'fail', 2);
72
+ }
73
+ }),
74
+ timeoutMs,
75
+ name
76
+ );
77
+ } catch (error) {
78
+ result.passed = false;
79
+ log(error.message, 'fail', 2);
80
+ }
81
+
62
82
  await afterEach(log);
63
83
  log('== After Each ==', 'progress', 3);
64
84
  if(!result.passed){ // If the test failed, elevate all logs to a level 2
@@ -51,4 +51,12 @@ export default {
51
51
  const ok = out.includes('logLevel: 1') && (out.includes("delay: '250'") || out.includes('delay: 250'));
52
52
  ok ? pass('Parsed and combined -l minimal with --delay 250') : fail(`Output missing expected flags:\n${out}`);
53
53
  },
54
+ 'passes --timeout value as timeout flag': async ({ pass, fail }) => {
55
+ const out = await runWithArgs(['--timeout', '5000']);
56
+ (out.includes("timeout: '5000'") || out.includes('timeout: 5000')) ? pass('Parsed --timeout 5000 into flags output') : fail(`Output did not include expected timeout:\n${out}`);
57
+ },
58
+ 'passes -t as short form of timeout flag': async ({ pass, fail }) => {
59
+ const out = await runWithArgs(['-t', '15000']);
60
+ (out.includes("timeout: '15000'") || out.includes('timeout: 15000')) ? pass('Parsed -t 15000 into flags output') : fail(`Output did not include expected timeout:\n${out}`);
61
+ }
54
62
  };
@@ -1,4 +1,4 @@
1
- import './Counter.js';
1
+ import '../fixtures/Counter.js';
2
2
 
3
3
  const wait = ms => new Promise(r=>setTimeout(r,ms));
4
4
 
@@ -1,4 +1,4 @@
1
- export const page = './custom-test-page.html';
1
+ export const page = '../fixtures/custom-test-page.html';
2
2
 
3
3
  export default {
4
4
  'should find custom element in custom page': ({ pass, fail }) => {
@@ -1,5 +1,5 @@
1
1
  // Import the fibonacci utility functions
2
- import fibonacciUtils from './fibonacci.js';
2
+ import fibonacciUtils from '../fixtures/fibonacci.js';
3
3
 
4
4
  const { fibonacci, fibonacciSequence, isFibonacci } = fibonacciUtils;
5
5
 
@@ -0,0 +1,9 @@
1
+ export default {
2
+ 'deeply nested test should be found': ({ pass, fail }) => {
3
+ if(1 == true){
4
+ pass('This test is in a deeply nested subdirectory');
5
+ } else {
6
+ fail('How did this happen?');
7
+ }
8
+ }
9
+ };
@@ -0,0 +1,9 @@
1
+ export default {
2
+ 'nested test should be found': ({ pass, fail }) => {
3
+ if(1 == true){
4
+ pass('This test is in a subdirectory');
5
+ } else {
6
+ fail('How did this happen?');
7
+ }
8
+ }
9
+ };
@@ -1,5 +1,5 @@
1
1
  import http from 'http';
2
- import { startServer, stopServer } from '../src/browserTestServer.js';
2
+ import { startServer, stopServer } from '../../src/browserTestServer.js';
3
3
 
4
4
  const get = (port, path) => new Promise((resolve, reject) => {
5
5
  const req = http.request({ hostname: 'localhost', port, path, method: 'GET' }, (res) => {
@@ -31,9 +31,9 @@ export default {
31
31
  try {
32
32
  await startServer(port);
33
33
  const a = await get(port, '/runTests.js');
34
- const b = await get(port, '/tests/counter.browser-test.js');
34
+ const b = await get(port, '/tests/components/counter.browser-test.js');
35
35
  const c = await get(port, '/does-not-exist.js');
36
- log(`Statuses: /runTests.js=${a.status} /tests/counter.browser-test.js=${b.status} /does-not-exist.js=${c.status}`);
36
+ log(`Statuses: /runTests.js=${a.status} /tests/components/counter.browser-test.js=${b.status} /does-not-exist.js=${c.status}`);
37
37
  const ok = a.status === 200 && /export default|const wait/.test(a.body)
38
38
  && b.status === 200 && /Counter component/.test(b.body)
39
39
  && c.status === 404;
@@ -1,12 +1,12 @@
1
- import findTests from '../src/findTests.js';
1
+ import findTests from '../../src/findTests.js';
2
2
 
3
3
  export default {
4
4
  'finds node and browser test files with no filters': async ({ pass, fail, log }) => {
5
5
  try {
6
6
  const { nodeTests, browserTests } = await findTests('', '', true, true);
7
7
  log(`Found node tests: ${nodeTests.length}, browser tests: ${browserTests.length}`);
8
- const hasCounter = browserTests.some(f => f.endsWith('tests/counter.browser-test.js'));
9
- const hasExample = nodeTests.some(f => f.endsWith('tests/example.node-test.js'));
8
+ const hasCounter = browserTests.some(f => f.endsWith('tests/components/counter.browser-test.js'));
9
+ const hasExample = nodeTests.some(f => f.endsWith('tests/examples/example.node-test.js'));
10
10
  if (hasCounter && hasExample) pass('Discovered expected canonical files (counter + example)');
11
11
  else fail(`Missing expected files. node: ${JSON.stringify(nodeTests)}, browser: ${JSON.stringify(browserTests)}`);
12
12
  } catch (e) {
@@ -37,5 +37,22 @@ export default {
37
37
  } catch (e) {
38
38
  fail(e.stack || String(e));
39
39
  }
40
+ },
41
+ 'finds tests in nested subdirectories': async ({ pass, fail, log }) => {
42
+ try {
43
+ const { nodeTests, browserTests } = await findTests('', '', true, true);
44
+ log(`All node tests: ${nodeTests.length}, browser tests: ${browserTests.length}`);
45
+
46
+ const hasNestedBrowser = browserTests.some(f => f.includes('tests/nested/nested.browser-test.js'));
47
+ const hasDeepNode = nodeTests.some(f => f.includes('tests/nested/deep/deep.node-test.js'));
48
+
49
+ if (hasNestedBrowser && hasDeepNode) {
50
+ pass('Found tests in nested subdirectories');
51
+ } else {
52
+ fail(`Missing nested tests. hasNestedBrowser: ${hasNestedBrowser}, hasDeepNode: ${hasDeepNode}`);
53
+ }
54
+ } catch (e) {
55
+ fail(e.stack || String(e));
56
+ }
40
57
  }
41
58
  };
@@ -1,4 +1,4 @@
1
- import LOG_LEVELS from '../src/utils/logLevels.js';
1
+ import LOG_LEVELS from '../../src/utils/logLevels.js';
2
2
 
3
3
  export default {
4
4
  'defines expected numeric levels': async ({ pass, fail, log }) => {
@@ -1,10 +1,10 @@
1
- import runBrowserTests from '../src/runBrowserTests.js';
1
+ import runBrowserTests from '../../src/runBrowserTests.js';
2
2
 
3
3
  export default {
4
4
  'runs a specific browser test headless and returns results': async ({ pass, fail, log }) => {
5
5
  try {
6
6
  const res = await runBrowserTests({
7
- testFile: 'tests/counter.browser-test.js',
7
+ testFile: 'tests/components/counter.browser-test.js',
8
8
  filter: '',
9
9
  showBrowser: false,
10
10
  port: 3111,
@@ -25,10 +25,10 @@ export default {
25
25
  const filter = 'Counter component should be defined';
26
26
  const delay = 500; // Reduced from 1000ms to avoid timeouts
27
27
  const start1 = Date.now();
28
- await runBrowserTests({ testFile: 'tests/counter.browser-test.js', filter, showBrowser: false, port: 3112, logLevel: 2, delayMs: delay });
28
+ await runBrowserTests({ testFile: 'tests/components/counter.browser-test.js', filter, showBrowser: false, port: 3112, logLevel: 2, delayMs: delay });
29
29
  const t1 = Date.now() - start1;
30
30
  const start2 = Date.now();
31
- await runBrowserTests({ testFile: 'tests/counter.browser-test.js', filter, showBrowser: true, port: 3113, logLevel: 2, delayMs: delay });
31
+ await runBrowserTests({ testFile: 'tests/components/counter.browser-test.js', filter, showBrowser: true, port: 3113, logLevel: 2, delayMs: delay });
32
32
  const t2 = Date.now() - start2;
33
33
  // Headful should incur roughly +1000ms (pre+post). Allow slack for env variance.
34
34
  log(`Timing — headless=${t1}ms, headful=${t2}ms, delta=${t2 - t1}ms`);
@@ -1,4 +1,4 @@
1
- import runTestFiles from '../src/runTestFiles.js';
1
+ import runTestFiles from '../../src/runTestFiles.js';
2
2
 
3
3
  export default {
4
4
  'discovers and runs node tests only when requested': async ({ pass, fail, log }) => {
@@ -1,4 +1,4 @@
1
- import runTests from '../src/runTests.js';
1
+ import runTests from '../../src/runTests.js';
2
2
 
3
3
  const mkSuite = () => ({
4
4
  beforeAll: async (log) => { log('before all'); },
@@ -0,0 +1,66 @@
1
+ import runTests from '../../src/runTests.js';
2
+
3
+ export default {
4
+ 'should timeout hanging test with custom timeout': async ({ pass, fail, log }) => {
5
+ try {
6
+ const hangingTest = {
7
+ default: {
8
+ 'hanging test': () => {
9
+ // Return a promise that never resolves
10
+ return new Promise(() => {});
11
+ }
12
+ }
13
+ };
14
+
15
+ const start = Date.now();
16
+ const results = await runTests(hangingTest, false, 0, 2000); // 2 second timeout for faster testing
17
+ const duration = Date.now() - start;
18
+
19
+ log(`Test completed in ${duration}ms`);
20
+
21
+ // Should have timed out and marked as failed
22
+ const testResult = results.tests['hanging test'];
23
+ if (testResult && !testResult.passed && duration >= 1900 && duration <= 3000) {
24
+ // Check if the failure message mentions timeout
25
+ const hasTimeoutMessage = testResult.logs.some(log => log.message.includes('timed out'));
26
+ if (hasTimeoutMessage) {
27
+ pass('Test correctly timed out and reported timeout error');
28
+ } else {
29
+ fail('Test failed but timeout message not found in logs');
30
+ }
31
+ } else {
32
+ fail(`Test should have timed out. passed: ${testResult?.passed}, duration: ${duration}ms`);
33
+ }
34
+ } catch (e) {
35
+ fail(e.stack || String(e));
36
+ }
37
+ },
38
+
39
+ 'should complete normal test within timeout': async ({ pass, fail, log }) => {
40
+ try {
41
+ const normalTest = {
42
+ default: {
43
+ 'quick test': ({ pass }) => {
44
+ pass('This test completes quickly');
45
+ }
46
+ }
47
+ };
48
+
49
+ const start = Date.now();
50
+ const results = await runTests(normalTest, false, 0, 2000); // 2 second timeout
51
+ const duration = Date.now() - start;
52
+
53
+ log(`Test completed in ${duration}ms`);
54
+
55
+ // Should complete quickly and pass
56
+ const testResult = results.tests['quick test'];
57
+ if (testResult && testResult.passed && duration < 1000) {
58
+ pass('Normal test completed successfully within timeout');
59
+ } else {
60
+ fail(`Normal test should have passed quickly. passed: ${testResult?.passed}, duration: ${duration}ms`);
61
+ }
62
+ } catch (e) {
63
+ fail(e.stack || String(e));
64
+ }
65
+ }
66
+ };
File without changes
File without changes