kempo-testing-framework 1.1.0 → 1.2.1
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/index.js +38 -4
- package/package.json +1 -1
- package/src/findTests.js +40 -34
- package/src/gui.js +249 -249
- package/src/runBrowserTests.js +19 -3
- package/test.html +18 -10
- package/tests/cli-help.node-test.js +61 -0
- package/tests/src-runBrowserTests.node-test.js +5 -5
package/index.js
CHANGED
|
@@ -10,7 +10,8 @@ const shortFlagMap = {
|
|
|
10
10
|
'd': 'delay',
|
|
11
11
|
'p': 'port',
|
|
12
12
|
'g': 'gui',
|
|
13
|
-
'w': 'show-browser'
|
|
13
|
+
'w': 'show-browser',
|
|
14
|
+
'h': 'help'
|
|
14
15
|
};
|
|
15
16
|
|
|
16
17
|
/*
|
|
@@ -22,7 +23,7 @@ const remainingArgs = [];
|
|
|
22
23
|
|
|
23
24
|
// Define which flags take values vs booleans
|
|
24
25
|
const valueFlags = new Set(['log-level', 'delay', 'port']);
|
|
25
|
-
const booleanFlags = new Set(['browser', 'node', 'gui', 'show-browser']);
|
|
26
|
+
const booleanFlags = new Set(['browser', 'node', 'gui', 'show-browser', 'help']);
|
|
26
27
|
|
|
27
28
|
for (let i = 0; i < args.length; i++) {
|
|
28
29
|
const arg = args[i];
|
|
@@ -108,6 +109,41 @@ if (Object.prototype.hasOwnProperty.call(flags, 'log-level')) {
|
|
|
108
109
|
delete flags['log-level'];
|
|
109
110
|
}
|
|
110
111
|
|
|
112
|
+
/*
|
|
113
|
+
* Help Display
|
|
114
|
+
*/
|
|
115
|
+
if (flags.help || flags.h) {
|
|
116
|
+
console.log(`
|
|
117
|
+
Kempo Testing Framework
|
|
118
|
+
|
|
119
|
+
USAGE:
|
|
120
|
+
kempo-test [OPTIONS] [SUITE_FILTER] [TEST_FILTER]
|
|
121
|
+
|
|
122
|
+
OPTIONS:
|
|
123
|
+
-h, --help Show this help message and exit
|
|
124
|
+
-b, --browser Run only browser tests
|
|
125
|
+
-n, --node Run only node tests
|
|
126
|
+
-l, --log-level LEVEL Set log level (0-4 or silent/minimal/normal/verbose/debug)
|
|
127
|
+
-d, --delay MS Add delay between tests in milliseconds
|
|
128
|
+
-p, --port PORT Set port for test server (default: 3000)
|
|
129
|
+
-g, --gui Launch GUI mode
|
|
130
|
+
-w, --show-browser Show browser window during tests
|
|
131
|
+
|
|
132
|
+
ARGUMENTS:
|
|
133
|
+
SUITE_FILTER Filter test suites by name
|
|
134
|
+
TEST_FILTER Filter individual tests by name
|
|
135
|
+
|
|
136
|
+
EXAMPLES:
|
|
137
|
+
kempo-test # Run all tests
|
|
138
|
+
kempo-test --browser # Run only browser tests
|
|
139
|
+
kempo-test -l verbose # Run with verbose logging
|
|
140
|
+
kempo-test --gui # Launch GUI mode
|
|
141
|
+
kempo-test myComponent # Run tests for 'myComponent' suite
|
|
142
|
+
kempo-test myComponent myTest # Run specific test in specific suite
|
|
143
|
+
`);
|
|
144
|
+
process.exit(0);
|
|
145
|
+
}
|
|
146
|
+
|
|
111
147
|
/*
|
|
112
148
|
* Mode Selection and Execution
|
|
113
149
|
*/
|
|
@@ -115,8 +151,6 @@ if (flags.gui) {
|
|
|
115
151
|
const { default: gui } = await import('./src/gui.js');
|
|
116
152
|
await gui(flags, remainingArgs);
|
|
117
153
|
} else {
|
|
118
|
-
// Log flags to verify capture
|
|
119
|
-
console.log('kempo-test flags:', flags);
|
|
120
154
|
const { default: cli } = await import('./src/cli.js');
|
|
121
155
|
await cli(flags, remainingArgs);
|
|
122
156
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "kempo-testing-framework",
|
|
3
|
-
"version": "1.1
|
|
3
|
+
"version": "1.2.1",
|
|
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/findTests.js
CHANGED
|
@@ -1,34 +1,40 @@
|
|
|
1
|
-
import fs from 'fs/promises';
|
|
2
|
-
import path from 'path';
|
|
3
|
-
|
|
4
|
-
const findFiles = async (
|
|
5
|
-
const results = [];
|
|
6
|
-
|
|
7
|
-
for (const
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
entry
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
1
|
+
import fs from 'fs/promises';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
|
|
4
|
+
const findFiles = async (testDirs, filter = '') => {
|
|
5
|
+
const results = [];
|
|
6
|
+
|
|
7
|
+
for (const dir of testDirs) {
|
|
8
|
+
try {
|
|
9
|
+
await fs.access(dir);
|
|
10
|
+
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
11
|
+
|
|
12
|
+
for (const entry of entries) {
|
|
13
|
+
if (entry.isFile() &&
|
|
14
|
+
entry.name.endsWith('test.js') &&
|
|
15
|
+
entry.name.includes(filter)) {
|
|
16
|
+
const entryPath = path.join(dir, entry.name);
|
|
17
|
+
// Normalize path to always use forward slashes for consistency across platforms
|
|
18
|
+
const relativePath = path.relative(process.cwd(), entryPath).replace(/\\/g, '/');
|
|
19
|
+
results.push(relativePath);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
} catch (error) {
|
|
23
|
+
// Directory doesn't exist, skip it
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return results;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export default async (suiteFilter, testFilter, browser = true, node = true) => {
|
|
30
|
+
const testDirectories = ['tests', 'test']; // Only look in these directories
|
|
31
|
+
const files = await findFiles(testDirectories, suiteFilter);
|
|
32
|
+
|
|
33
|
+
const nodeTests = files.filter(file => (file.endsWith('.test.js') || file.endsWith('.node-test.js')) && node);
|
|
34
|
+
const browserTests = files.filter(file => (file.endsWith('.test.js') || file.endsWith('.browser-test.js')) && browser);
|
|
35
|
+
|
|
36
|
+
return {
|
|
37
|
+
nodeTests,
|
|
38
|
+
browserTests
|
|
39
|
+
};
|
|
40
|
+
};
|
package/src/gui.js
CHANGED
|
@@ -1,249 +1,249 @@
|
|
|
1
|
-
import http from 'http';
|
|
2
|
-
import { readFile } from 'fs/promises';
|
|
3
|
-
import { fileURLToPath } from 'url';
|
|
4
|
-
import path from 'path';
|
|
5
|
-
import { exec } from 'child_process';
|
|
6
|
-
import { platform } from 'os';
|
|
7
|
-
import findTests from './findTests.js';
|
|
8
|
-
import runTestFiles from './runTestFiles.js';
|
|
9
|
-
|
|
10
|
-
const __filename = fileURLToPath(import.meta.url);
|
|
11
|
-
const __dirname = path.dirname(__filename);
|
|
12
|
-
|
|
13
|
-
export default async (flags, args) => {
|
|
14
|
-
const server = http.createServer(async (req, res) => {
|
|
15
|
-
const basePath = req.url.split('?')[0];
|
|
16
|
-
|
|
17
|
-
/*
|
|
18
|
-
Custom API Endpoints
|
|
19
|
-
*/
|
|
20
|
-
|
|
21
|
-
/*
|
|
22
|
-
Serve essential.css
|
|
23
|
-
*/
|
|
24
|
-
if(basePath === '/essential.css'){
|
|
25
|
-
res.writeHead(200, { 'Content-Type': 'text/css' });
|
|
26
|
-
res.end(await readFile(path.join(__dirname, '../node_modules/essentialcss/dist/essential.min.css'), 'utf8'));
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
/*
|
|
30
|
-
Get list of all test files with their test names
|
|
31
|
-
*/
|
|
32
|
-
else if(basePath === '/testFiles'){
|
|
33
|
-
try {
|
|
34
|
-
const testFiles = await findTests('', '', true, true);
|
|
35
|
-
|
|
36
|
-
// Extract test names from Node test files (safe to import in Node.js)
|
|
37
|
-
const nodeTestsWithNames = await Promise.all(
|
|
38
|
-
testFiles.nodeTests.map(async file => {
|
|
39
|
-
try {
|
|
40
|
-
// Convert forward slashes back to OS-specific path separators for import
|
|
41
|
-
const normalizedFile = file.replace(/\//g, path.sep);
|
|
42
|
-
const module = await import(`file://${path.resolve(process.cwd(), normalizedFile)}`);
|
|
43
|
-
const testNames = module.default ? Object.keys(module.default) : [];
|
|
44
|
-
return { file, testNames };
|
|
45
|
-
} catch (error) {
|
|
46
|
-
console.error(`Error loading Node test file ${file}:`, error);
|
|
47
|
-
return { file, testNames: [], error: error.message };
|
|
48
|
-
}
|
|
49
|
-
})
|
|
50
|
-
);
|
|
51
|
-
|
|
52
|
-
// Browser tests just return file names (test names will be extracted client-side)
|
|
53
|
-
const browserTestsWithNames = testFiles.browserTests.map(file => ({ file, testNames: null }));
|
|
54
|
-
|
|
55
|
-
const result = {
|
|
56
|
-
nodeTests: nodeTestsWithNames,
|
|
57
|
-
browserTests: browserTestsWithNames
|
|
58
|
-
};
|
|
59
|
-
|
|
60
|
-
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
61
|
-
res.end(JSON.stringify(result));
|
|
62
|
-
} catch (error) {
|
|
63
|
-
console.error('Error finding test files:', error);
|
|
64
|
-
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
65
|
-
res.end(JSON.stringify({ error: 'Failed to find test files' }));
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
/*
|
|
70
|
-
Serve test files as ES modules for dynamic imports
|
|
71
|
-
*/
|
|
72
|
-
else if(basePath.startsWith('/test/')){
|
|
73
|
-
// URL format: /test/path/to/testfile.js
|
|
74
|
-
const testPath = basePath.substring(6); // Remove '/test/' prefix
|
|
75
|
-
|
|
76
|
-
try {
|
|
77
|
-
// Convert forward slashes back to OS-specific path separators for file reading
|
|
78
|
-
const normalizedFile = testPath.replace(/\//g, path.sep);
|
|
79
|
-
const filePath = path.resolve(process.cwd(), normalizedFile);
|
|
80
|
-
|
|
81
|
-
// Security check: ensure the file is within the project directory
|
|
82
|
-
if (!filePath.startsWith(process.cwd())) {
|
|
83
|
-
res.writeHead(403, { 'Content-Type': 'application/json' });
|
|
84
|
-
res.end(JSON.stringify({ error: 'Access denied' }));
|
|
85
|
-
return;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
const fileContent = await readFile(filePath, 'utf8');
|
|
89
|
-
res.writeHead(200, { 'Content-Type': 'application/javascript' });
|
|
90
|
-
res.end(fileContent);
|
|
91
|
-
} catch (error) {
|
|
92
|
-
console.error(`Error reading test file ${testPath}:`, error);
|
|
93
|
-
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
94
|
-
res.end(JSON.stringify({ error: 'Test file not found' }));
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
/*
|
|
99
|
-
Run a testFile
|
|
100
|
-
*/
|
|
101
|
-
else if(basePath == '/runTest'){
|
|
102
|
-
const url = new URL(req.url, `http://${req.headers.host}`);
|
|
103
|
-
const testFile = url.searchParams.get('testFile');
|
|
104
|
-
const testNamesParam = url.searchParams.get('testNames');
|
|
105
|
-
const testNames = testNamesParam ? testNamesParam.split(',') : [];
|
|
106
|
-
const showBrowserParam = url.searchParams.get('showBrowser');
|
|
107
|
-
const showBrowser = showBrowserParam === 'true';
|
|
108
|
-
const delayMs = Math.max(0, parseInt(url.searchParams.get('delayMs')||'0', 10) || 0);
|
|
109
|
-
|
|
110
|
-
try {
|
|
111
|
-
// Determine if this is a browser or node test based on file extension
|
|
112
|
-
const isBrowserTest = testFile.endsWith('.browser-test.js');
|
|
113
|
-
const isNodeTest = testFile.endsWith('.node-test.js');
|
|
114
|
-
|
|
115
|
-
if (!isBrowserTest && !isNodeTest) {
|
|
116
|
-
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
117
|
-
res.end(JSON.stringify({ error: 'Unknown test file type' }));
|
|
118
|
-
return;
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
// Run the specific test file using the specificFiles parameter
|
|
122
|
-
const { nodeResults, browserResults } = await runTestFiles({
|
|
123
|
-
testFilter: (testNames && testNames.length === 1) ? testNames[0] : '',
|
|
124
|
-
shouldRunBrowser: isBrowserTest,
|
|
125
|
-
shouldRunNode: isNodeTest,
|
|
126
|
-
showBrowser, // pass through from query
|
|
127
|
-
port: 3001,
|
|
128
|
-
logLevel: 2,
|
|
129
|
-
delayMs,
|
|
130
|
-
specificFiles: [testFile]
|
|
131
|
-
});
|
|
132
|
-
|
|
133
|
-
// Return the results for the specific test file
|
|
134
|
-
const results = isBrowserTest ? browserResults : nodeResults;
|
|
135
|
-
const fileResults = results[testFile] || { tests: {} };
|
|
136
|
-
|
|
137
|
-
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
138
|
-
res.end(JSON.stringify({
|
|
139
|
-
testFile,
|
|
140
|
-
testNames,
|
|
141
|
-
results: fileResults
|
|
142
|
-
}));
|
|
143
|
-
} catch (error) {
|
|
144
|
-
console.error(`Error running test file ${testFile}:`, error);
|
|
145
|
-
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
146
|
-
res.end(JSON.stringify({ error: 'Failed to run test file', details: error.message }));
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
/*
|
|
151
|
-
Return 404 for common browser requests that we don't need to handle
|
|
152
|
-
*/
|
|
153
|
-
else if(['/favicon.ico', '/.well-known/appspecific/com.chrome.devtools.json'].includes(basePath)){
|
|
154
|
-
res.writeHead(404);
|
|
155
|
-
res.end('');
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
// Serve static files from the gui directory (HTML, JS, CSS, images, etc.)
|
|
159
|
-
else {
|
|
160
|
-
// Serve static files from gui directory
|
|
161
|
-
try {
|
|
162
|
-
let filePath;
|
|
163
|
-
|
|
164
|
-
// If requesting root, serve index.html
|
|
165
|
-
if (basePath === '/') {
|
|
166
|
-
filePath = path.join(__dirname, '../gui/index.html');
|
|
167
|
-
} else {
|
|
168
|
-
// Remove leading slash and serve from gui directory
|
|
169
|
-
const requestedFile = basePath.substring(1);
|
|
170
|
-
filePath = path.join(__dirname, '../gui', requestedFile);
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
// Security check: ensure the resolved path is within the gui directory
|
|
174
|
-
const guiDir = path.resolve(__dirname, '../gui');
|
|
175
|
-
const resolvedPath = path.resolve(filePath);
|
|
176
|
-
if (!resolvedPath.startsWith(guiDir)) {
|
|
177
|
-
res.writeHead(403, { 'Content-Type': 'text/plain' });
|
|
178
|
-
res.end('Access denied');
|
|
179
|
-
return;
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
/*
|
|
183
|
-
Static File Content Type Mapping
|
|
184
|
-
*/
|
|
185
|
-
const ext = path.extname(filePath).toLowerCase();
|
|
186
|
-
let contentType = 'text/plain';
|
|
187
|
-
switch (ext) {
|
|
188
|
-
case '.html': contentType = 'text/html'; break;
|
|
189
|
-
case '.js': contentType = 'application/javascript'; break;
|
|
190
|
-
case '.css': contentType = 'text/css'; break;
|
|
191
|
-
case '.json': contentType = 'application/json'; break;
|
|
192
|
-
case '.png': contentType = 'image/png'; break;
|
|
193
|
-
case '.jpg': case '.jpeg': contentType = 'image/jpeg'; break;
|
|
194
|
-
case '.svg': contentType = 'image/svg+xml'; break;
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
const fileContent = await readFile(filePath, ext === '.png' || ext === '.jpg' || ext === '.jpeg' ? null : 'utf8');
|
|
198
|
-
res.writeHead(200, { 'Content-Type': contentType });
|
|
199
|
-
res.end(fileContent);
|
|
200
|
-
} catch (error) {
|
|
201
|
-
res.writeHead(404, { 'Content-Type': 'text/plain' });
|
|
202
|
-
res.end('File not found');
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
});
|
|
206
|
-
|
|
207
|
-
/*
|
|
208
|
-
Server Startup and Browser Launch
|
|
209
|
-
*/
|
|
210
|
-
const port = flags['gui-port'] || 4000;
|
|
211
|
-
server.listen(port);
|
|
212
|
-
const startUrl = `http://localhost:${port}`;
|
|
213
|
-
console.log(`Server running at ${startUrl}`);
|
|
214
|
-
|
|
215
|
-
// Show platform-specific instructions for stopping the server
|
|
216
|
-
const platformName = platform();
|
|
217
|
-
const stopKey = platformName === 'darwin' ? 'Cmd+C' : 'Ctrl+C';
|
|
218
|
-
console.log(`Press ${stopKey} to stop the testing ui`);
|
|
219
|
-
|
|
220
|
-
const openBrowser = url => {
|
|
221
|
-
const platformName = platform();
|
|
222
|
-
let command;
|
|
223
|
-
|
|
224
|
-
switch(platformName){
|
|
225
|
-
case 'win32':
|
|
226
|
-
command = `start ${url}`;
|
|
227
|
-
break;
|
|
228
|
-
case 'darwin':
|
|
229
|
-
command = `open ${url}`;
|
|
230
|
-
break;
|
|
231
|
-
case 'linux':
|
|
232
|
-
command = `xdg-open ${url}`;
|
|
233
|
-
break;
|
|
234
|
-
default:
|
|
235
|
-
console.log(`Unable to open browser automatically on this platform: ${platformName}`);
|
|
236
|
-
console.log(`Please manually open your browser and navigate to: ${url}`);
|
|
237
|
-
return;
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
exec(command, error => {
|
|
241
|
-
if (error) {
|
|
242
|
-
console.log(`Failed to open browser: ${error.message}`);
|
|
243
|
-
console.log(`Please manually open your browser and navigate to: ${url}`);
|
|
244
|
-
}
|
|
245
|
-
});
|
|
246
|
-
};
|
|
247
|
-
|
|
248
|
-
openBrowser(startUrl);
|
|
249
|
-
};
|
|
1
|
+
import http from 'http';
|
|
2
|
+
import { readFile } from 'fs/promises';
|
|
3
|
+
import { fileURLToPath } from 'url';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
import { exec } from 'child_process';
|
|
6
|
+
import { platform } from 'os';
|
|
7
|
+
import findTests from './findTests.js';
|
|
8
|
+
import runTestFiles from './runTestFiles.js';
|
|
9
|
+
|
|
10
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
11
|
+
const __dirname = path.dirname(__filename);
|
|
12
|
+
|
|
13
|
+
export default async (flags, args) => {
|
|
14
|
+
const server = http.createServer(async (req, res) => {
|
|
15
|
+
const basePath = req.url.split('?')[0];
|
|
16
|
+
|
|
17
|
+
/*
|
|
18
|
+
Custom API Endpoints
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
/*
|
|
22
|
+
Serve essential.css
|
|
23
|
+
*/
|
|
24
|
+
if(basePath === '/essential.css'){
|
|
25
|
+
res.writeHead(200, { 'Content-Type': 'text/css' });
|
|
26
|
+
res.end(await readFile(path.join(__dirname, '../node_modules/essentialcss/dist/essential.min.css'), 'utf8'));
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/*
|
|
30
|
+
Get list of all test files with their test names
|
|
31
|
+
*/
|
|
32
|
+
else if(basePath === '/testFiles'){
|
|
33
|
+
try {
|
|
34
|
+
const testFiles = await findTests('', '', true, true);
|
|
35
|
+
|
|
36
|
+
// Extract test names from Node test files (safe to import in Node.js)
|
|
37
|
+
const nodeTestsWithNames = await Promise.all(
|
|
38
|
+
testFiles.nodeTests.map(async file => {
|
|
39
|
+
try {
|
|
40
|
+
// Convert forward slashes back to OS-specific path separators for import
|
|
41
|
+
const normalizedFile = file.replace(/\//g, path.sep);
|
|
42
|
+
const module = await import(`file://${path.resolve(process.cwd(), normalizedFile)}`);
|
|
43
|
+
const testNames = module.default ? Object.keys(module.default) : [];
|
|
44
|
+
return { file, testNames };
|
|
45
|
+
} catch (error) {
|
|
46
|
+
console.error(`Error loading Node test file ${file}:`, error);
|
|
47
|
+
return { file, testNames: [], error: error.message };
|
|
48
|
+
}
|
|
49
|
+
})
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
// Browser tests just return file names (test names will be extracted client-side)
|
|
53
|
+
const browserTestsWithNames = testFiles.browserTests.map(file => ({ file, testNames: null }));
|
|
54
|
+
|
|
55
|
+
const result = {
|
|
56
|
+
nodeTests: nodeTestsWithNames,
|
|
57
|
+
browserTests: browserTestsWithNames
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
61
|
+
res.end(JSON.stringify(result));
|
|
62
|
+
} catch (error) {
|
|
63
|
+
console.error('Error finding test files:', error);
|
|
64
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
65
|
+
res.end(JSON.stringify({ error: 'Failed to find test files' }));
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/*
|
|
70
|
+
Serve test files as ES modules for dynamic imports
|
|
71
|
+
*/
|
|
72
|
+
else if(basePath.startsWith('/test/')){
|
|
73
|
+
// URL format: /test/path/to/testfile.js
|
|
74
|
+
const testPath = basePath.substring(6); // Remove '/test/' prefix
|
|
75
|
+
|
|
76
|
+
try {
|
|
77
|
+
// Convert forward slashes back to OS-specific path separators for file reading
|
|
78
|
+
const normalizedFile = testPath.replace(/\//g, path.sep);
|
|
79
|
+
const filePath = path.resolve(process.cwd(), normalizedFile);
|
|
80
|
+
|
|
81
|
+
// Security check: ensure the file is within the project directory
|
|
82
|
+
if (!filePath.startsWith(process.cwd())) {
|
|
83
|
+
res.writeHead(403, { 'Content-Type': 'application/json' });
|
|
84
|
+
res.end(JSON.stringify({ error: 'Access denied' }));
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const fileContent = await readFile(filePath, 'utf8');
|
|
89
|
+
res.writeHead(200, { 'Content-Type': 'application/javascript' });
|
|
90
|
+
res.end(fileContent);
|
|
91
|
+
} catch (error) {
|
|
92
|
+
console.error(`Error reading test file ${testPath}:`, error);
|
|
93
|
+
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
94
|
+
res.end(JSON.stringify({ error: 'Test file not found' }));
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/*
|
|
99
|
+
Run a testFile
|
|
100
|
+
*/
|
|
101
|
+
else if(basePath == '/runTest'){
|
|
102
|
+
const url = new URL(req.url, `http://${req.headers.host}`);
|
|
103
|
+
const testFile = url.searchParams.get('testFile');
|
|
104
|
+
const testNamesParam = url.searchParams.get('testNames');
|
|
105
|
+
const testNames = testNamesParam ? testNamesParam.split(',') : [];
|
|
106
|
+
const showBrowserParam = url.searchParams.get('showBrowser');
|
|
107
|
+
const showBrowser = showBrowserParam === 'true';
|
|
108
|
+
const delayMs = Math.max(0, parseInt(url.searchParams.get('delayMs')||'0', 10) || 0);
|
|
109
|
+
|
|
110
|
+
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');
|
|
114
|
+
|
|
115
|
+
if (!isBrowserTest && !isNodeTest) {
|
|
116
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
117
|
+
res.end(JSON.stringify({ error: 'Unknown test file type' }));
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Run the specific test file using the specificFiles parameter
|
|
122
|
+
const { nodeResults, browserResults } = await runTestFiles({
|
|
123
|
+
testFilter: (testNames && testNames.length === 1) ? testNames[0] : '',
|
|
124
|
+
shouldRunBrowser: isBrowserTest,
|
|
125
|
+
shouldRunNode: isNodeTest,
|
|
126
|
+
showBrowser, // pass through from query
|
|
127
|
+
port: 3001,
|
|
128
|
+
logLevel: 2,
|
|
129
|
+
delayMs,
|
|
130
|
+
specificFiles: [testFile]
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
// Return the results for the specific test file
|
|
134
|
+
const results = isBrowserTest ? browserResults : nodeResults;
|
|
135
|
+
const fileResults = results[testFile] || { tests: {} };
|
|
136
|
+
|
|
137
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
138
|
+
res.end(JSON.stringify({
|
|
139
|
+
testFile,
|
|
140
|
+
testNames,
|
|
141
|
+
results: fileResults
|
|
142
|
+
}));
|
|
143
|
+
} catch (error) {
|
|
144
|
+
console.error(`Error running test file ${testFile}:`, error);
|
|
145
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
146
|
+
res.end(JSON.stringify({ error: 'Failed to run test file', details: error.message }));
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/*
|
|
151
|
+
Return 404 for common browser requests that we don't need to handle
|
|
152
|
+
*/
|
|
153
|
+
else if(['/favicon.ico', '/.well-known/appspecific/com.chrome.devtools.json'].includes(basePath)){
|
|
154
|
+
res.writeHead(404);
|
|
155
|
+
res.end('');
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Serve static files from the gui directory (HTML, JS, CSS, images, etc.)
|
|
159
|
+
else {
|
|
160
|
+
// Serve static files from gui directory
|
|
161
|
+
try {
|
|
162
|
+
let filePath;
|
|
163
|
+
|
|
164
|
+
// If requesting root, serve index.html
|
|
165
|
+
if (basePath === '/') {
|
|
166
|
+
filePath = path.join(__dirname, '../gui/index.html');
|
|
167
|
+
} else {
|
|
168
|
+
// Remove leading slash and serve from gui directory
|
|
169
|
+
const requestedFile = basePath.substring(1);
|
|
170
|
+
filePath = path.join(__dirname, '../gui', requestedFile);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Security check: ensure the resolved path is within the gui directory
|
|
174
|
+
const guiDir = path.resolve(__dirname, '../gui');
|
|
175
|
+
const resolvedPath = path.resolve(filePath);
|
|
176
|
+
if (!resolvedPath.startsWith(guiDir)) {
|
|
177
|
+
res.writeHead(403, { 'Content-Type': 'text/plain' });
|
|
178
|
+
res.end('Access denied');
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/*
|
|
183
|
+
Static File Content Type Mapping
|
|
184
|
+
*/
|
|
185
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
186
|
+
let contentType = 'text/plain';
|
|
187
|
+
switch (ext) {
|
|
188
|
+
case '.html': contentType = 'text/html'; break;
|
|
189
|
+
case '.js': contentType = 'application/javascript'; break;
|
|
190
|
+
case '.css': contentType = 'text/css'; break;
|
|
191
|
+
case '.json': contentType = 'application/json'; break;
|
|
192
|
+
case '.png': contentType = 'image/png'; break;
|
|
193
|
+
case '.jpg': case '.jpeg': contentType = 'image/jpeg'; break;
|
|
194
|
+
case '.svg': contentType = 'image/svg+xml'; break;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const fileContent = await readFile(filePath, ext === '.png' || ext === '.jpg' || ext === '.jpeg' ? null : 'utf8');
|
|
198
|
+
res.writeHead(200, { 'Content-Type': contentType });
|
|
199
|
+
res.end(fileContent);
|
|
200
|
+
} catch (error) {
|
|
201
|
+
res.writeHead(404, { 'Content-Type': 'text/plain' });
|
|
202
|
+
res.end('File not found');
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
/*
|
|
208
|
+
Server Startup and Browser Launch
|
|
209
|
+
*/
|
|
210
|
+
const port = flags['gui-port'] || 4000;
|
|
211
|
+
server.listen(port);
|
|
212
|
+
const startUrl = `http://localhost:${port}`;
|
|
213
|
+
console.log(`Server running at ${startUrl}`);
|
|
214
|
+
|
|
215
|
+
// Show platform-specific instructions for stopping the server
|
|
216
|
+
const platformName = platform();
|
|
217
|
+
const stopKey = platformName === 'darwin' ? 'Cmd+C' : 'Ctrl+C';
|
|
218
|
+
console.log(`Press ${stopKey} to stop the testing ui`);
|
|
219
|
+
|
|
220
|
+
const openBrowser = url => {
|
|
221
|
+
const platformName = platform();
|
|
222
|
+
let command;
|
|
223
|
+
|
|
224
|
+
switch(platformName){
|
|
225
|
+
case 'win32':
|
|
226
|
+
command = `start ${url}`;
|
|
227
|
+
break;
|
|
228
|
+
case 'darwin':
|
|
229
|
+
command = `open ${url}`;
|
|
230
|
+
break;
|
|
231
|
+
case 'linux':
|
|
232
|
+
command = `xdg-open ${url}`;
|
|
233
|
+
break;
|
|
234
|
+
default:
|
|
235
|
+
console.log(`Unable to open browser automatically on this platform: ${platformName}`);
|
|
236
|
+
console.log(`Please manually open your browser and navigate to: ${url}`);
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
exec(command, error => {
|
|
241
|
+
if (error) {
|
|
242
|
+
console.log(`Failed to open browser: ${error.message}`);
|
|
243
|
+
console.log(`Please manually open your browser and navigate to: ${url}`);
|
|
244
|
+
}
|
|
245
|
+
});
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
openBrowser(startUrl);
|
|
249
|
+
};
|
package/src/runBrowserTests.js
CHANGED
|
@@ -45,9 +45,25 @@ export default async ({
|
|
|
45
45
|
await sleep(delayMs);
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
48
|
+
await page.goto(`${url}?testFile=${testFile}&testFilter=${filter}&delay=${delayMs}`);
|
|
49
|
+
await page.waitForFunction(() => document.readyState === 'complete' || document.readyState === 'interactive');
|
|
50
|
+
|
|
51
|
+
// Ensure the test page has focus when browser is visible
|
|
52
|
+
if (showBrowser) {
|
|
53
|
+
await page.bringToFront();
|
|
54
|
+
if (logLevel >= LOG_LEVELS.VERBOSE) {
|
|
55
|
+
console.log(`\x1b[90m Bringing test page to front\x1b[0m`);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Check for any errors first
|
|
60
|
+
const hasError = await page.evaluate(() => window.error !== undefined);
|
|
61
|
+
if (hasError) {
|
|
62
|
+
const error = await page.evaluate(() => window.error);
|
|
63
|
+
throw new Error(`Browser test error: ${error}`);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
await page.waitForFunction(() => window.results !== undefined, { timeout: 20000 });
|
|
51
67
|
const results = await page.evaluate(() => window.results);
|
|
52
68
|
|
|
53
69
|
// Optional post-delay when a visible browser is requested
|
package/test.html
CHANGED
|
@@ -8,16 +8,24 @@
|
|
|
8
8
|
<body>
|
|
9
9
|
<script type="module">
|
|
10
10
|
import runTests from './runTests.js';
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
11
|
+
|
|
12
|
+
(async () => {
|
|
13
|
+
try {
|
|
14
|
+
const urlParams = new URLSearchParams(window.location.search);
|
|
15
|
+
const testFile = urlParams.get('testFile');
|
|
16
|
+
const testFilter = urlParams.get('testFilter');
|
|
17
|
+
const delay = parseInt(urlParams.get('delay') || '0', 10) || 0;
|
|
18
|
+
if(!testFile){
|
|
19
|
+
throw new Error('No test file provided');
|
|
20
|
+
}
|
|
21
|
+
const testsModule = await import(`/${testFile}`);
|
|
22
|
+
const results = await runTests(testsModule, testFilter, delay);
|
|
23
|
+
window.results = results;
|
|
24
|
+
} catch (error) {
|
|
25
|
+
console.error('Browser test error:', error);
|
|
26
|
+
window.error = error.message || String(error);
|
|
27
|
+
}
|
|
28
|
+
})();
|
|
21
29
|
</script>
|
|
22
30
|
</body>
|
|
23
31
|
</html>
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { spawn } from 'child_process';
|
|
2
|
+
|
|
3
|
+
const runWithHelp = (args, timeoutMs = 3000) => new Promise((resolve) => {
|
|
4
|
+
const child = spawn(process.execPath, ['index.js', ...args], {
|
|
5
|
+
cwd: process.cwd(),
|
|
6
|
+
stdio: ['ignore', 'pipe', 'pipe']
|
|
7
|
+
});
|
|
8
|
+
let out = '';
|
|
9
|
+
let resolved = false;
|
|
10
|
+
|
|
11
|
+
const finish = () => {
|
|
12
|
+
if (resolved) return;
|
|
13
|
+
resolved = true;
|
|
14
|
+
try { child.kill('SIGTERM'); } catch {}
|
|
15
|
+
resolve(out);
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
child.stdout.on('data', (chunk) => {
|
|
19
|
+
out += chunk.toString();
|
|
20
|
+
// Help should exit quickly, check for help content
|
|
21
|
+
if (out.includes('Kempo Testing Framework') && out.includes('USAGE:')) {
|
|
22
|
+
setTimeout(finish, 50);
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
child.stderr.on('data', (chunk) => { out += chunk.toString(); });
|
|
26
|
+
child.on('error', () => finish());
|
|
27
|
+
child.on('exit', () => finish());
|
|
28
|
+
|
|
29
|
+
setTimeout(finish, timeoutMs);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
export default {
|
|
33
|
+
'--help shows help message and exits': async ({ pass, fail }) => {
|
|
34
|
+
const out = await runWithHelp(['--help']);
|
|
35
|
+
const hasHelp = out.includes('Kempo Testing Framework') &&
|
|
36
|
+
out.includes('USAGE:') &&
|
|
37
|
+
out.includes('OPTIONS:') &&
|
|
38
|
+
out.includes('--help');
|
|
39
|
+
const noTestRun = !out.includes('kempo-test flags:') && !out.includes('Test Summary');
|
|
40
|
+
|
|
41
|
+
if (hasHelp && noTestRun) {
|
|
42
|
+
pass('--help shows help message and exits without running tests');
|
|
43
|
+
} else {
|
|
44
|
+
fail(`Help output incorrect:\nHas help: ${hasHelp}\nNo test run: ${noTestRun}\nOutput:\n${out}`);
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
'-h shows help message and exits': async ({ pass, fail }) => {
|
|
48
|
+
const out = await runWithHelp(['-h']);
|
|
49
|
+
const hasHelp = out.includes('Kempo Testing Framework') &&
|
|
50
|
+
out.includes('USAGE:') &&
|
|
51
|
+
out.includes('OPTIONS:') &&
|
|
52
|
+
out.includes('--help');
|
|
53
|
+
const noTestRun = !out.includes('kempo-test flags:') && !out.includes('Test Summary');
|
|
54
|
+
|
|
55
|
+
if (hasHelp && noTestRun) {
|
|
56
|
+
pass('-h shows help message and exits without running tests');
|
|
57
|
+
} else {
|
|
58
|
+
fail(`Help output incorrect:\nHas help: ${hasHelp}\nNo test run: ${noTestRun}\nOutput:\n${out}`);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
};
|
|
@@ -23,17 +23,17 @@ export default {
|
|
|
23
23
|
try {
|
|
24
24
|
// Use a single test to avoid between-test delays dominating timings
|
|
25
25
|
const filter = 'Counter component should be defined';
|
|
26
|
-
const delay =
|
|
26
|
+
const delay = 500; // Reduced from 1000ms to avoid timeouts
|
|
27
27
|
const start1 = Date.now();
|
|
28
28
|
await runBrowserTests({ testFile: 'tests/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
31
|
await runBrowserTests({ testFile: 'tests/counter.browser-test.js', filter, showBrowser: true, port: 3113, logLevel: 2, delayMs: delay });
|
|
32
32
|
const t2 = Date.now() - start2;
|
|
33
|
-
// Headful should incur roughly +
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
33
|
+
// Headful should incur roughly +1000ms (pre+post). Allow slack for env variance.
|
|
34
|
+
log(`Timing — headless=${t1}ms, headful=${t2}ms, delta=${t2 - t1}ms`);
|
|
35
|
+
if (t2 - t1 >= 800) pass('Headful mode applied pre/post delay as expected');
|
|
36
|
+
else fail(`Timing not increased as expected: headless=${t1}ms headful=${t2}ms`);
|
|
37
37
|
} catch (e) {
|
|
38
38
|
fail(e.stack || String(e));
|
|
39
39
|
}
|