kempo-testing-framework 1.3.8 → 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.
- package/README.md +33 -0
- package/gui/components/TestFramework.js +6 -6
- package/gui/components/settingsStore.js +1 -0
- package/gui/index.html +1 -0
- package/index.js +4 -1
- package/package.json +1 -1
- package/src/browserTestServer.js +8 -2
- package/src/cli.js +5 -0
- package/src/findTests.js +11 -3
- package/src/gui.js +32 -2
- package/src/runBrowserTests.js +3 -2
- package/src/runTestFiles.js +5 -2
- package/src/runTests.js +32 -12
- package/tests/{cli-flags.node-test.js → cli/flags.node-test.js} +8 -0
- package/tests/{counter.browser-test.js → components/counter.browser-test.js} +1 -1
- package/tests/{custom-page.browser-test.js → examples/custom-page.browser-test.js} +1 -1
- package/tests/{fibonacci.test.js → examples/fibonacci.test.js} +1 -1
- package/tests/nested/deep/deep.node-test.js +9 -0
- package/tests/nested/nested.browser-test.js +9 -0
- package/tests/{src-browserTestServer.node-test.js → src/browserTestServer.node-test.js} +3 -3
- package/tests/{src-findTests.node-test.js → src/findTests.node-test.js} +20 -3
- package/tests/{src-logLevels.node-test.js → src/logLevels.node-test.js} +1 -1
- package/tests/{src-runBrowserTests.node-test.js → src/runBrowserTests.node-test.js} +4 -4
- package/tests/{src-runTestFiles.node-test.js → src/runTestFiles.node-test.js} +1 -1
- package/tests/{src-runTests.node-test.js → src/runTests.node-test.js} +1 -1
- package/tests/src/timeout.node-test.js +66 -0
- /package/tests/{cli-help.node-test.js → cli/help.node-test.js} +0 -0
- /package/tests/{cli-loglevel.node-test.js → cli/loglevel.node-test.js} +0 -0
- /package/tests/{collapsible.browser-test.js → components/collapsible.browser-test.js} +0 -0
- /package/tests/{icon.browser-test.js → components/icon.browser-test.js} +0 -0
- /package/tests/{logs.browser-test.js → components/logs.browser-test.js} +0 -0
- /package/tests/{setting-checkbox.browser-test.js → components/setting-checkbox.browser-test.js} +0 -0
- /package/tests/{setting-number.browser-test.js → components/setting-number.browser-test.js} +0 -0
- /package/tests/{setting-select.browser-test.js → components/setting-select.browser-test.js} +0 -0
- /package/tests/{settings-store.browser-test.js → components/settings-store.browser-test.js} +0 -0
- /package/tests/{test-framework.browser-test.js → components/test-framework.browser-test.js} +0 -0
- /package/tests/{test-summary.browser-test.js → components/test-summary.browser-test.js} +0 -0
- /package/tests/{test.browser-test.js → components/test.browser-test.js} +0 -0
- /package/tests/{testfile.browser-test.js → components/testfile.browser-test.js} +0 -0
- /package/tests/{theme.browser-test.js → components/theme.browser-test.js} +0 -0
- /package/tests/{example.node-test.js → examples/example.node-test.js} +0 -0
- /package/tests/{Counter.js → fixtures/Counter.js} +0 -0
- /package/tests/{custom-test-page.html → fixtures/custom-test-page.html} +0 -0
- /package/tests/{fibonacci.js → fixtures/fibonacci.js} +0 -0
- /package/tests/{src-cli.node-test.js → src/cli.node-test.js} +0 -0
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)){
|
package/gui/index.html
CHANGED
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
<div id="delaySettingRow">
|
|
18
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>
|
|
19
19
|
</div>
|
|
20
|
+
<ktf-setting-number name="timeoutMs" label="Test timeout (ms)" step="1000" min="1000" max="300000" suffix="ms"></ktf-setting-number>
|
|
20
21
|
|
|
21
22
|
<ktf-setting-select name="logLevel" label="Log Level"></ktf-setting-select>
|
|
22
23
|
|
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.
|
|
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": {
|
package/src/browserTestServer.js
CHANGED
|
@@ -109,7 +109,13 @@ export const startServer = async (_port = 3000) => {
|
|
|
109
109
|
|
|
110
110
|
export const stopServer = async () => {
|
|
111
111
|
if(server){
|
|
112
|
-
|
|
113
|
-
|
|
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
|
-
|
|
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
|
-
|
|
250
|
-
|
|
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');
|
package/src/runBrowserTests.js
CHANGED
|
@@ -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:
|
|
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
|
package/src/runTestFiles.js
CHANGED
|
@@ -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
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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,5 +1,5 @@
|
|
|
1
1
|
import http from 'http';
|
|
2
|
-
import { startServer, stopServer } from '
|
|
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 '
|
|
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,10 +1,10 @@
|
|
|
1
|
-
import runBrowserTests from '
|
|
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`);
|
|
@@ -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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
/package/tests/{setting-checkbox.browser-test.js → components/setting-checkbox.browser-test.js}
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|