kempo-testing-framework 1.1.0
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/.github/copilot-instructions.md +105 -0
- package/CONTRIBUTING.md +107 -0
- package/README.md +293 -0
- package/gui/components/Collapsible.js +54 -0
- package/gui/components/Icon.js +151 -0
- package/gui/components/Logs.js +73 -0
- package/gui/components/SettingCheckbox.js +42 -0
- package/gui/components/SettingNumber.js +77 -0
- package/gui/components/SettingSelect.js +67 -0
- package/gui/components/Test.js +99 -0
- package/gui/components/TestFramework.js +236 -0
- package/gui/components/TestSuite.js +181 -0
- package/gui/components/TestSummary.js +189 -0
- package/gui/components/Theme.js +40 -0
- package/gui/components/settingsStore.js +46 -0
- package/gui/icons/fail.svg +1 -0
- package/gui/icons/logs.svg +1 -0
- package/gui/icons/pass.svg +1 -0
- package/gui/icons/play.svg +1 -0
- package/gui/icons/running.svg +1 -0
- package/gui/icons/scheduled.svg +1 -0
- package/gui/icons/settings.svg +1 -0
- package/gui/icons/theme-auto.svg +1 -0
- package/gui/icons/theme-dark.svg +1 -0
- package/gui/icons/theme-light.svg +1 -0
- package/gui/index.html +108 -0
- package/gui/lit-all.min.js +120 -0
- package/index.js +122 -0
- package/package.json +21 -0
- package/src/browserTestServer.js +115 -0
- package/src/cli.js +198 -0
- package/src/findTests.js +34 -0
- package/src/gui.js +249 -0
- package/src/runBrowserTests.js +71 -0
- package/src/runTestFiles.js +94 -0
- package/src/runTests.js +83 -0
- package/src/utils/logLevels.js +7 -0
- package/test.html +23 -0
- package/tests/Counter.js +34 -0
- package/tests/cli-flags.node-test.js +54 -0
- package/tests/cli-loglevel.node-test.js +40 -0
- package/tests/collapsible.browser-test.js +49 -0
- package/tests/counter.browser-test.js +141 -0
- package/tests/example.node-test.js +103 -0
- package/tests/icon.browser-test.js +54 -0
- package/tests/logs.browser-test.js +47 -0
- package/tests/setting-checkbox.browser-test.js +48 -0
- package/tests/setting-number.browser-test.js +54 -0
- package/tests/setting-select.browser-test.js +47 -0
- package/tests/settings-store.browser-test.js +26 -0
- package/tests/src-browserTestServer.node-test.js +47 -0
- package/tests/src-cli.node-test.js +32 -0
- package/tests/src-findTests.node-test.js +41 -0
- package/tests/src-logLevels.node-test.js +29 -0
- package/tests/src-runBrowserTests.node-test.js +41 -0
- package/tests/src-runTestFiles.node-test.js +42 -0
- package/tests/src-runTests.node-test.js +56 -0
- package/tests/test-framework.browser-test.js +65 -0
- package/tests/test-summary.browser-test.js +78 -0
- package/tests/test.browser-test.js +56 -0
- package/tests/testfile.browser-test.js +60 -0
- package/tests/theme.browser-test.js +38 -0
package/src/gui.js
ADDED
|
@@ -0,0 +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
|
+
};
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { startServer, stopServer } from './browserTestServer.js';
|
|
2
|
+
import puppeteer from 'puppeteer';
|
|
3
|
+
import LOG_LEVELS from './utils/logLevels.js';
|
|
4
|
+
|
|
5
|
+
export default async ({
|
|
6
|
+
testFile,
|
|
7
|
+
filter = false,
|
|
8
|
+
showBrowser = false,
|
|
9
|
+
port = 3000,
|
|
10
|
+
logLevel,
|
|
11
|
+
delayMs = 0
|
|
12
|
+
}) => {
|
|
13
|
+
/*
|
|
14
|
+
* Start Test Server
|
|
15
|
+
*/
|
|
16
|
+
const url = await startServer(port);
|
|
17
|
+
|
|
18
|
+
if(logLevel >= LOG_LEVELS.VERBOSE){
|
|
19
|
+
console.log(`\x1b[90m Browser test server started at ${url}\x1b[0m`);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const sleep = (ms) => new Promise(r => setTimeout(r, ms));
|
|
23
|
+
try {
|
|
24
|
+
/*
|
|
25
|
+
* Launch Browser and Execute Tests
|
|
26
|
+
*/
|
|
27
|
+
const launchOptions = {
|
|
28
|
+
headless: !showBrowser,
|
|
29
|
+
args: ['--no-sandbox', '--disable-setuid-sandbox'],
|
|
30
|
+
defaultViewport: { width: 1600, height: 900 }
|
|
31
|
+
};
|
|
32
|
+
if (showBrowser) {
|
|
33
|
+
launchOptions.devtools = true;
|
|
34
|
+
launchOptions.args.push('--auto-open-devtools-for-tabs');
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const browser = await puppeteer.launch(launchOptions);
|
|
38
|
+
const page = await browser.newPage();
|
|
39
|
+
|
|
40
|
+
// Optional pre-delay when a visible browser is requested
|
|
41
|
+
if (showBrowser && delayMs > 0) {
|
|
42
|
+
if (logLevel >= LOG_LEVELS.VERBOSE) {
|
|
43
|
+
console.log(`\x1b[90m Applying pre-test browser delay: ${delayMs}ms\x1b[0m`);
|
|
44
|
+
}
|
|
45
|
+
await sleep(delayMs);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
await page.goto(`${url}?testFile=${testFile}&testFilter=${filter}&delay=${delayMs}`);
|
|
49
|
+
await page.waitForFunction(() => document.readyState === 'complete' || document.readyState === 'interactive');
|
|
50
|
+
await page.waitForFunction(() => window.results !== undefined);
|
|
51
|
+
const results = await page.evaluate(() => window.results);
|
|
52
|
+
|
|
53
|
+
// Optional post-delay when a visible browser is requested
|
|
54
|
+
if (showBrowser && delayMs > 0) {
|
|
55
|
+
if (logLevel >= LOG_LEVELS.VERBOSE) {
|
|
56
|
+
console.log(`\x1b[90m Applying post-test browser delay: ${delayMs}ms\x1b[0m`);
|
|
57
|
+
}
|
|
58
|
+
await sleep(delayMs);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/*
|
|
62
|
+
* Cleanup
|
|
63
|
+
*/
|
|
64
|
+
await browser.close();
|
|
65
|
+
await stopServer();
|
|
66
|
+
return results;
|
|
67
|
+
} catch(error){
|
|
68
|
+
await stopServer();
|
|
69
|
+
throw error;
|
|
70
|
+
}
|
|
71
|
+
};
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import findTests from './findTests.js';
|
|
2
|
+
import runNodeTests from './runTests.js';
|
|
3
|
+
import runBrowserTests from './runBrowserTests.js';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Runs test files and returns results without logging
|
|
8
|
+
* @param {Object} options - Configuration options
|
|
9
|
+
* @param {string} options.suiteFilter - Filter for test suites
|
|
10
|
+
* @param {string} options.testFilter - Filter for individual tests
|
|
11
|
+
* @param {boolean} options.shouldRunBrowser - Whether to run browser tests
|
|
12
|
+
* @param {boolean} options.shouldRunNode - Whether to run node tests
|
|
13
|
+
* @param {boolean} options.showBrowser - Whether to show browser during tests
|
|
14
|
+
* @param {number} options.port - Port for browser tests
|
|
15
|
+
* @param {number} options.logLevel - Log level for browser tests
|
|
16
|
+
* @param {number} options.delayMs - Optional delay in ms to apply for browser runs
|
|
17
|
+
* @param {Function} options.onNodeTestStart - Callback when a node test starts
|
|
18
|
+
* @param {Function} options.onBrowserTestStart - Callback when a browser test starts
|
|
19
|
+
* @param {string[]} options.specificFiles - Array of specific test files to run (overrides findTests)
|
|
20
|
+
* @returns {Promise<{nodeResults: Object, browserResults: Object}>}
|
|
21
|
+
*/
|
|
22
|
+
export default async ({
|
|
23
|
+
suiteFilter = '',
|
|
24
|
+
testFilter = '',
|
|
25
|
+
shouldRunBrowser = true,
|
|
26
|
+
shouldRunNode = true,
|
|
27
|
+
showBrowser = false,
|
|
28
|
+
port = 3000,
|
|
29
|
+
logLevel = 2,
|
|
30
|
+
delayMs = 0,
|
|
31
|
+
onNodeTestStart,
|
|
32
|
+
onBrowserTestStart,
|
|
33
|
+
specificFiles = null
|
|
34
|
+
}) => {
|
|
35
|
+
let nodeTests = [];
|
|
36
|
+
let browserTests = [];
|
|
37
|
+
|
|
38
|
+
if (specificFiles) {
|
|
39
|
+
// When specific files are provided, use them directly
|
|
40
|
+
for (const file of specificFiles) {
|
|
41
|
+
if (file.endsWith('.node-test.js') && shouldRunNode) {
|
|
42
|
+
nodeTests.push(file);
|
|
43
|
+
} else if (file.endsWith('.browser-test.js') && shouldRunBrowser) {
|
|
44
|
+
browserTests.push(file);
|
|
45
|
+
} else if (file.endsWith('.test.js')) {
|
|
46
|
+
// Generic test files can be both
|
|
47
|
+
if (shouldRunNode) nodeTests.push(file);
|
|
48
|
+
if (shouldRunBrowser) browserTests.push(file);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
} else {
|
|
52
|
+
// Use findTests for discovery
|
|
53
|
+
const result = await findTests(suiteFilter, testFilter, shouldRunBrowser, shouldRunNode);
|
|
54
|
+
nodeTests = result.nodeTests;
|
|
55
|
+
browserTests = result.browserTests;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/*
|
|
59
|
+
* Node Test Execution
|
|
60
|
+
*/
|
|
61
|
+
const nodeResults = {};
|
|
62
|
+
if(nodeTests.length){
|
|
63
|
+
for(const file of nodeTests){
|
|
64
|
+
if(onNodeTestStart) onNodeTestStart(file);
|
|
65
|
+
// Convert forward slashes back to OS-specific path separators for file system operations
|
|
66
|
+
const normalizedFile = file.replace(/\//g, path.sep);
|
|
67
|
+
const module = await import(`file://${path.resolve(process.cwd(), normalizedFile)}`);
|
|
68
|
+
nodeResults[file] = await runNodeTests(module, testFilter);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/*
|
|
73
|
+
* Browser Test Execution
|
|
74
|
+
*/
|
|
75
|
+
const browserResults = {};
|
|
76
|
+
if(browserTests.length){
|
|
77
|
+
for(const testFile of browserTests){
|
|
78
|
+
if(onBrowserTestStart) onBrowserTestStart(testFile);
|
|
79
|
+
browserResults[testFile] = await runBrowserTests({
|
|
80
|
+
testFile,
|
|
81
|
+
filter: testFilter, // Changed from testFilter to filter
|
|
82
|
+
showBrowser,
|
|
83
|
+
port,
|
|
84
|
+
logLevel,
|
|
85
|
+
delayMs
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return {
|
|
91
|
+
nodeResults,
|
|
92
|
+
browserResults
|
|
93
|
+
};
|
|
94
|
+
};
|
package/src/runTests.js
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
const wait = ms => new Promise(r => setTimeout(r, ms));
|
|
2
|
+
|
|
3
|
+
export default async ({
|
|
4
|
+
beforeAll = async () => {},
|
|
5
|
+
beforeEach = async () => {},
|
|
6
|
+
afterEach = async () => {},
|
|
7
|
+
afterAll = async () => {},
|
|
8
|
+
default: tests
|
|
9
|
+
} = {}, filter = false, delay = 0) => {
|
|
10
|
+
const testsToRun = filter ? Object.keys(tests).filter(name => name.trim().toLowerCase().includes(filter.trim().toLowerCase())) : Object.keys(tests);
|
|
11
|
+
if(!testsToRun.length) throw new Error('No tests found matching the filter');
|
|
12
|
+
|
|
13
|
+
/*
|
|
14
|
+
* Test Results Structure
|
|
15
|
+
*/
|
|
16
|
+
const results = {
|
|
17
|
+
beforeAllLogs: [],
|
|
18
|
+
tests: {},
|
|
19
|
+
afterAllLogs: []
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
/*
|
|
23
|
+
* Execute beforeAll Hook
|
|
24
|
+
*/
|
|
25
|
+
await beforeAll(message => {
|
|
26
|
+
results.beforeAllLogs.push({
|
|
27
|
+
message,
|
|
28
|
+
type: 'log',
|
|
29
|
+
level: 3
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
/*
|
|
34
|
+
* Execute Individual Tests
|
|
35
|
+
*/
|
|
36
|
+
for (const name of testsToRun) {
|
|
37
|
+
const result = {
|
|
38
|
+
logs: [],
|
|
39
|
+
passed: null
|
|
40
|
+
};
|
|
41
|
+
const log = (message, type = 'log', level = 3) => {
|
|
42
|
+
result.logs.push({
|
|
43
|
+
message,
|
|
44
|
+
type,
|
|
45
|
+
level
|
|
46
|
+
});
|
|
47
|
+
};
|
|
48
|
+
log(`== Starting Test "${name}" ==`, 'progress', 3);
|
|
49
|
+
log('== Before Each ==', 'progress', 3);
|
|
50
|
+
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
|
+
});
|
|
62
|
+
await afterEach(log);
|
|
63
|
+
log('== After Each ==', 'progress', 3);
|
|
64
|
+
if(!result.passed){ // If the test failed, elevate all logs to a level 2
|
|
65
|
+
result.logs.forEach(log => log.level = 2);
|
|
66
|
+
}
|
|
67
|
+
results.tests[name] = result;
|
|
68
|
+
await wait(delay);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/*
|
|
72
|
+
* Execute afterAll Hook
|
|
73
|
+
*/
|
|
74
|
+
await afterAll(message => {
|
|
75
|
+
results.afterAllLogs.push({
|
|
76
|
+
message,
|
|
77
|
+
type: 'log',
|
|
78
|
+
level: 3
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
return results;
|
|
83
|
+
};
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export default {
|
|
2
|
+
SILENT: 0, // No output except final summary
|
|
3
|
+
MINIMAL: 1, // Only test names and pass/fail status
|
|
4
|
+
NORMAL: 2, // Test names, status, and logs from failed tests
|
|
5
|
+
VERBOSE: 3, // All test output including logs from passing tests
|
|
6
|
+
DEBUG: 4 // Everything including framework internals
|
|
7
|
+
};
|
package/test.html
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>Test</title>
|
|
7
|
+
</head>
|
|
8
|
+
<body>
|
|
9
|
+
<script type="module">
|
|
10
|
+
import runTests from './runTests.js';
|
|
11
|
+
const urlParams = new URLSearchParams(window.location.search);
|
|
12
|
+
const testFile = urlParams.get('testFile');
|
|
13
|
+
const testFilter = urlParams.get('testFilter');
|
|
14
|
+
const delay = parseInt(urlParams.get('delay') || '0', 10) || 0;
|
|
15
|
+
if(!testFile){
|
|
16
|
+
throw new Error('No test file provided');
|
|
17
|
+
}
|
|
18
|
+
const testsModule = await import(`/${testFile}`);
|
|
19
|
+
const results = await runTests(testsModule, testFilter, delay);
|
|
20
|
+
window.results = results;
|
|
21
|
+
</script>
|
|
22
|
+
</body>
|
|
23
|
+
</html>
|
package/tests/Counter.js
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
class Counter extends HTMLElement {
|
|
2
|
+
constructor() {
|
|
3
|
+
super();
|
|
4
|
+
this.attachShadow({ mode: 'open' });
|
|
5
|
+
this.count = 0;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
connectedCallback() {
|
|
9
|
+
this.render();
|
|
10
|
+
this.shadowRoot.querySelector('button').addEventListener('click', this.increment);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
disconnectedCallback() {
|
|
14
|
+
const button = this.shadowRoot.querySelector('button');
|
|
15
|
+
if (button) {
|
|
16
|
+
button.removeEventListener('click', this.increment);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
increment = () => {
|
|
21
|
+
this.count++;
|
|
22
|
+
this.render();
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
render() {
|
|
26
|
+
this.shadowRoot.innerHTML = `
|
|
27
|
+
<button>Increment</button>
|
|
28
|
+
<div>Count: ${this.count}</div>
|
|
29
|
+
`;
|
|
30
|
+
this.shadowRoot.querySelector('button').addEventListener('click', this.increment);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
customElements.define('my-counter', Counter);
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { spawn } from 'child_process';
|
|
2
|
+
|
|
3
|
+
const runWithArgs = (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
|
+
if (out.includes('kempo-test flags:')) {
|
|
21
|
+
// We only need to see the flags echo. Short delay to capture the full line, then stop.
|
|
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
|
+
'maps -l 3 to logLevel 3': async ({ pass, fail }) => {
|
|
34
|
+
const out = await runWithArgs(['-l', '3']);
|
|
35
|
+
out.includes('logLevel: 3') ? pass('Parsed -l 3 to logLevel 3') : fail(`Output did not include expected logLevel=3:\n${out}`);
|
|
36
|
+
},
|
|
37
|
+
'maps -l debug to logLevel 4': async ({ pass, fail }) => {
|
|
38
|
+
const out = await runWithArgs(['-l', 'debug']);
|
|
39
|
+
out.includes('logLevel: 4') ? pass('Parsed -l debug to logLevel 4') : fail(`Output did not include expected logLevel=4:\n${out}`);
|
|
40
|
+
},
|
|
41
|
+
'maps -l v to logLevel 3': async ({ pass, fail }) => {
|
|
42
|
+
const out = await runWithArgs(['-l', 'v']);
|
|
43
|
+
out.includes('logLevel: 3') ? pass('Parsed -l v to logLevel 3') : fail(`Output did not include expected logLevel=3:\n${out}`);
|
|
44
|
+
},
|
|
45
|
+
'passes --delay value as delay flag': async ({ pass, fail }) => {
|
|
46
|
+
const out = await runWithArgs(['--delay', '2000']);
|
|
47
|
+
(out.includes("delay: '2000'") || out.includes('delay: 2000')) ? pass('Parsed --delay 2000 into flags output') : fail(`Output did not include expected delay:\n${out}`);
|
|
48
|
+
},
|
|
49
|
+
'combines log-level and delay flags': async ({ pass, fail }) => {
|
|
50
|
+
const out = await runWithArgs(['-l', 'minimal', '--delay', '250']);
|
|
51
|
+
const ok = out.includes('logLevel: 1') && (out.includes("delay: '250'") || out.includes('delay: 250'));
|
|
52
|
+
ok ? pass('Parsed and combined -l minimal with --delay 250') : fail(`Output missing expected flags:\n${out}`);
|
|
53
|
+
},
|
|
54
|
+
};
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { spawn } from 'child_process';
|
|
2
|
+
|
|
3
|
+
const stripAnsi = (s) => s.replace(/\x1b\[[0-9;]*m/g, '');
|
|
4
|
+
|
|
5
|
+
const run = (args, timeoutMs = 8000) => new Promise((resolve) => {
|
|
6
|
+
const child = spawn(process.execPath, ['index.js', ...args], {
|
|
7
|
+
cwd: process.cwd(),
|
|
8
|
+
stdio: ['ignore', 'pipe', 'pipe']
|
|
9
|
+
});
|
|
10
|
+
let out = '';
|
|
11
|
+
let resolved = false;
|
|
12
|
+
const done = () => { if (!resolved) { resolved = true; try { child.kill('SIGTERM'); } catch {} resolve(out); } };
|
|
13
|
+
child.stdout.on('data', c => { out += c.toString(); });
|
|
14
|
+
child.stderr.on('data', c => { out += c.toString(); });
|
|
15
|
+
child.on('exit', done);
|
|
16
|
+
child.on('error', done);
|
|
17
|
+
setTimeout(done, timeoutMs);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
export default {
|
|
21
|
+
'debug level prints debug preamble': async ({ pass, fail }) => {
|
|
22
|
+
// Pass suite filter as positional arg so it isn't consumed as a value for -n
|
|
23
|
+
const out = await run(['-l', 'debug', 'example']);
|
|
24
|
+
const txt = stripAnsi(out);
|
|
25
|
+
const ok = txt.includes('[Debug] Flags:') && txt.includes('[Debug] Args:') && txt.includes('[Debug] Log level: 4');
|
|
26
|
+
ok ? pass('CLI debug level printed expected debug preamble') : fail(`CLI output missing debug preamble:\n${out}`);
|
|
27
|
+
},
|
|
28
|
+
'minimal level omits beforeAll sections': async ({ pass, fail }) => {
|
|
29
|
+
const out = await run(['-l', 'minimal', 'example']);
|
|
30
|
+
const txt = stripAnsi(out);
|
|
31
|
+
const ok = !/beforeAll/.test(txt);
|
|
32
|
+
ok ? pass('Minimal log level omitted beforeAll sections') : fail(`Output contained beforeAll unexpectedly:\n${out}`);
|
|
33
|
+
},
|
|
34
|
+
'verbose shows pass lines for passing tests': async ({ pass, fail }) => {
|
|
35
|
+
const out = await run(['-l', 'verbose', 'example']);
|
|
36
|
+
const txt = stripAnsi(out);
|
|
37
|
+
const ok = /PASS\s+should handle basic string operations/.test(txt);
|
|
38
|
+
ok ? pass('Verbose level printed PASS lines for passing tests') : fail(`Verbose output missing PASS lines:\n${out}`);
|
|
39
|
+
}
|
|
40
|
+
};
|