browsecraft-runner 0.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/LICENSE +21 -0
- package/dist/index.cjs +188 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +102 -0
- package/dist/index.d.ts +102 -0
- package/dist/index.js +186 -0
- package/dist/index.js.map +1 -0
- package/package.json +55 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Browsecraft Contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var path = require('path');
|
|
4
|
+
var fs = require('fs');
|
|
5
|
+
|
|
6
|
+
// src/runner.ts
|
|
7
|
+
var TestRunner = class {
|
|
8
|
+
options;
|
|
9
|
+
constructor(options) {
|
|
10
|
+
this.options = options;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Run all tests and return an exit code (0 = success, 1 = failure).
|
|
14
|
+
*
|
|
15
|
+
* @param loadFile - Load a test file (triggers test registration). Returns registered tests.
|
|
16
|
+
* @param executeTest - Execute a single test and return its result.
|
|
17
|
+
*/
|
|
18
|
+
async run(loadFile, executeTest) {
|
|
19
|
+
const startTime = Date.now();
|
|
20
|
+
const files = this.discoverFiles();
|
|
21
|
+
if (files.length === 0) {
|
|
22
|
+
console.log("\n No test files found.\n");
|
|
23
|
+
console.log(` Test pattern: ${this.options.config.testMatch}`);
|
|
24
|
+
console.log(' Run "browsecraft init" to create an example test.\n');
|
|
25
|
+
return 0;
|
|
26
|
+
}
|
|
27
|
+
console.log(`
|
|
28
|
+
Browsecraft - Running ${files.length} test file${files.length > 1 ? "s" : ""}
|
|
29
|
+
`);
|
|
30
|
+
const allResults = [];
|
|
31
|
+
let bail = false;
|
|
32
|
+
for (const file of files) {
|
|
33
|
+
if (bail) break;
|
|
34
|
+
const relPath = path.relative(process.cwd(), file);
|
|
35
|
+
console.log(` ${relPath}`);
|
|
36
|
+
try {
|
|
37
|
+
const tests = await loadFile(file);
|
|
38
|
+
const filteredTests = this.options.grep ? tests.filter((t) => t.title.includes(this.options.grep)) : tests;
|
|
39
|
+
const hasOnly = filteredTests.some((t) => t.only);
|
|
40
|
+
const testsToRun = hasOnly ? filteredTests.filter((t) => t.only) : filteredTests;
|
|
41
|
+
for (const test of testsToRun) {
|
|
42
|
+
let result = await executeTest(test);
|
|
43
|
+
const maxRetries = test.options.retries ?? this.options.config.retries;
|
|
44
|
+
let retryCount = 0;
|
|
45
|
+
while (result.status === "failed" && retryCount < maxRetries) {
|
|
46
|
+
retryCount++;
|
|
47
|
+
result = await executeTest(test);
|
|
48
|
+
}
|
|
49
|
+
if (retryCount > 0) {
|
|
50
|
+
result.retries = retryCount;
|
|
51
|
+
}
|
|
52
|
+
allResults.push(result);
|
|
53
|
+
const prefix = this.getStatusIcon(result.status);
|
|
54
|
+
const suiteName = result.suitePath.length > 0 ? `${result.suitePath.join(" > ")} > ` : "";
|
|
55
|
+
const duration = result.status !== "skipped" ? ` (${result.duration}ms)` : "";
|
|
56
|
+
console.log(` ${prefix} ${suiteName}${result.title}${duration}`);
|
|
57
|
+
if (result.status === "failed" && result.error) {
|
|
58
|
+
console.log(` ${result.error.message}`);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
} catch (error) {
|
|
62
|
+
const errorResult = {
|
|
63
|
+
title: `Failed to load: ${relPath}`,
|
|
64
|
+
suitePath: [],
|
|
65
|
+
status: "failed",
|
|
66
|
+
duration: 0,
|
|
67
|
+
error: error instanceof Error ? error : new Error(String(error))
|
|
68
|
+
};
|
|
69
|
+
allResults.push(errorResult);
|
|
70
|
+
console.log(` ${this.getStatusIcon("failed")} ${errorResult.title}`);
|
|
71
|
+
console.log(` ${errorResult.error.message}`);
|
|
72
|
+
}
|
|
73
|
+
console.log("");
|
|
74
|
+
if (this.options.bail && allResults.some((r) => r.status === "failed")) {
|
|
75
|
+
bail = true;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
const summary = this.summarize(allResults, Date.now() - startTime);
|
|
79
|
+
this.printSummary(summary);
|
|
80
|
+
return summary.failed > 0 ? 1 : 0;
|
|
81
|
+
}
|
|
82
|
+
// -----------------------------------------------------------------------
|
|
83
|
+
// File Discovery
|
|
84
|
+
// -----------------------------------------------------------------------
|
|
85
|
+
discoverFiles() {
|
|
86
|
+
if (this.options.files && this.options.files.length > 0) {
|
|
87
|
+
return this.options.files.map((f) => path.resolve(process.cwd(), f)).filter((f) => fs.existsSync(f));
|
|
88
|
+
}
|
|
89
|
+
const cwd = process.cwd();
|
|
90
|
+
return this.findTestFiles(cwd);
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Find test files matching the configured pattern.
|
|
94
|
+
*/
|
|
95
|
+
findTestFiles(dir) {
|
|
96
|
+
const files = [];
|
|
97
|
+
const pattern = this.options.config.testMatch;
|
|
98
|
+
const extMatch = pattern.match(/\.\{([^}]+)\}$/);
|
|
99
|
+
const extensions = extMatch ? extMatch[1].split(",").map((e) => e.trim()) : ["ts", "js"];
|
|
100
|
+
const suffixMatch = pattern.match(/\*(\.[^{*]+)\./);
|
|
101
|
+
const suffix = suffixMatch ? suffixMatch[1] : ".test";
|
|
102
|
+
this.walkDir(dir, files, extensions, suffix);
|
|
103
|
+
return files.sort();
|
|
104
|
+
}
|
|
105
|
+
walkDir(dir, results, extensions, suffix) {
|
|
106
|
+
const skip = /* @__PURE__ */ new Set(["node_modules", "dist", ".browsecraft", ".git", "coverage", ".turbo"]);
|
|
107
|
+
let entries;
|
|
108
|
+
try {
|
|
109
|
+
entries = fs.readdirSync(dir);
|
|
110
|
+
} catch {
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
for (const entry of entries) {
|
|
114
|
+
if (skip.has(entry)) continue;
|
|
115
|
+
const fullPath = path.join(dir, entry);
|
|
116
|
+
let stat;
|
|
117
|
+
try {
|
|
118
|
+
stat = fs.statSync(fullPath);
|
|
119
|
+
} catch {
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
if (stat.isDirectory()) {
|
|
123
|
+
this.walkDir(fullPath, results, extensions, suffix);
|
|
124
|
+
} else if (stat.isFile()) {
|
|
125
|
+
const matchesPattern = extensions.some(
|
|
126
|
+
(ext) => entry.endsWith(`${suffix}.${ext}`)
|
|
127
|
+
);
|
|
128
|
+
if (matchesPattern) {
|
|
129
|
+
results.push(fullPath);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
// -----------------------------------------------------------------------
|
|
135
|
+
// Reporting
|
|
136
|
+
// -----------------------------------------------------------------------
|
|
137
|
+
getStatusIcon(status) {
|
|
138
|
+
switch (status) {
|
|
139
|
+
case "passed":
|
|
140
|
+
return "\x1B[32m+\x1B[0m";
|
|
141
|
+
case "failed":
|
|
142
|
+
return "\x1B[31mx\x1B[0m";
|
|
143
|
+
case "skipped":
|
|
144
|
+
return "\x1B[33m-\x1B[0m";
|
|
145
|
+
default:
|
|
146
|
+
return " ";
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
summarize(results, totalDuration) {
|
|
150
|
+
return {
|
|
151
|
+
total: results.length,
|
|
152
|
+
passed: results.filter((r) => r.status === "passed").length,
|
|
153
|
+
failed: results.filter((r) => r.status === "failed").length,
|
|
154
|
+
skipped: results.filter((r) => r.status === "skipped").length,
|
|
155
|
+
duration: totalDuration,
|
|
156
|
+
results
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
printSummary(summary) {
|
|
160
|
+
const { total, passed, failed, skipped, duration } = summary;
|
|
161
|
+
console.log(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
|
|
162
|
+
const parts = [];
|
|
163
|
+
if (passed > 0) parts.push(`\x1B[32m${passed} passed\x1B[0m`);
|
|
164
|
+
if (failed > 0) parts.push(`\x1B[31m${failed} failed\x1B[0m`);
|
|
165
|
+
if (skipped > 0) parts.push(`\x1B[33m${skipped} skipped\x1B[0m`);
|
|
166
|
+
console.log(` Tests: ${parts.join(", ")} (${total} total)`);
|
|
167
|
+
console.log(` Time: ${this.formatDuration(duration)}`);
|
|
168
|
+
console.log("");
|
|
169
|
+
if (failed > 0) {
|
|
170
|
+
console.log(" \x1B[31mSome tests failed.\x1B[0m\n");
|
|
171
|
+
} else if (total === 0) {
|
|
172
|
+
console.log(" No tests were run.\n");
|
|
173
|
+
} else {
|
|
174
|
+
console.log(" \x1B[32mAll tests passed!\x1B[0m\n");
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
formatDuration(ms) {
|
|
178
|
+
if (ms < 1e3) return `${ms}ms`;
|
|
179
|
+
if (ms < 6e4) return `${(ms / 1e3).toFixed(1)}s`;
|
|
180
|
+
const minutes = Math.floor(ms / 6e4);
|
|
181
|
+
const seconds = (ms % 6e4 / 1e3).toFixed(1);
|
|
182
|
+
return `${minutes}m ${seconds}s`;
|
|
183
|
+
}
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
exports.TestRunner = TestRunner;
|
|
187
|
+
//# sourceMappingURL=index.cjs.map
|
|
188
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/runner.ts"],"names":["relative","resolve","existsSync","readdirSync","join","statSync"],"mappings":"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","file":"index.cjs","sourcesContent":["// ============================================================================\n// Browsecraft Runner - Test Runner\n// Discovers test files, loads them, executes tests, reports results.\n//\n// The runner does NOT import 'browsecraft' to avoid circular deps.\n// Test execution is delegated via callbacks provided by the CLI.\n// ============================================================================\n\nimport { resolve, relative, join } from 'node:path';\nimport { existsSync, readdirSync, statSync } from 'node:fs';\nimport { pathToFileURL } from 'node:url';\nimport type { RunnerOptions, TestResult, RunSummary } from './types.js';\n\n/** A test case as passed to the runner from the browsecraft package */\nexport interface RunnableTest {\n\ttitle: string;\n\tsuitePath: string[];\n\tskip: boolean;\n\tonly: boolean;\n\toptions: { timeout?: number; retries?: number; tags?: string[] };\n\tfn: (fixtures: unknown) => Promise<void>;\n}\n\n/** Callback that the CLI provides to execute a single test */\nexport type TestExecutor = (test: RunnableTest) => Promise<TestResult>;\n\n/**\n * TestRunner discovers test files, loads them, and coordinates execution.\n *\n * Used by the CLI:\n * ```ts\n * const runner = new TestRunner({ config });\n * const exitCode = await runner.run(getTests, executeTest);\n * ```\n */\nexport class TestRunner {\n\tprivate options: RunnerOptions;\n\n\tconstructor(options: RunnerOptions) {\n\t\tthis.options = options;\n\t}\n\n\t/**\n\t * Run all tests and return an exit code (0 = success, 1 = failure).\n\t *\n\t * @param loadFile - Load a test file (triggers test registration). Returns registered tests.\n\t * @param executeTest - Execute a single test and return its result.\n\t */\n\tasync run(\n\t\tloadFile: (file: string) => Promise<RunnableTest[]>,\n\t\texecuteTest: TestExecutor,\n\t): Promise<number> {\n\t\tconst startTime = Date.now();\n\n\t\t// Step 1: Discover test files\n\t\tconst files = this.discoverFiles();\n\t\tif (files.length === 0) {\n\t\t\tconsole.log('\\n No test files found.\\n');\n\t\t\tconsole.log(` Test pattern: ${this.options.config.testMatch}`);\n\t\t\tconsole.log(' Run \"browsecraft init\" to create an example test.\\n');\n\t\t\treturn 0;\n\t\t}\n\n\t\tconsole.log(`\\n Browsecraft - Running ${files.length} test file${files.length > 1 ? 's' : ''}\\n`);\n\n\t\t// Step 2: Load and run each test file\n\t\tconst allResults: TestResult[] = [];\n\t\tlet bail = false;\n\n\t\tfor (const file of files) {\n\t\t\tif (bail) break;\n\n\t\t\tconst relPath = relative(process.cwd(), file);\n\t\t\tconsole.log(` ${relPath}`);\n\n\t\t\ttry {\n\t\t\t\tconst tests = await loadFile(file);\n\n\t\t\t\t// Apply grep filter\n\t\t\t\tconst filteredTests = this.options.grep\n\t\t\t\t\t? tests.filter(t => t.title.includes(this.options.grep!))\n\t\t\t\t\t: tests;\n\n\t\t\t\t// Check for .only tests\n\t\t\t\tconst hasOnly = filteredTests.some(t => t.only);\n\t\t\t\tconst testsToRun = hasOnly\n\t\t\t\t\t? filteredTests.filter(t => t.only)\n\t\t\t\t\t: filteredTests;\n\n\t\t\t\t// Run each test\n\t\t\t\tfor (const test of testsToRun) {\n\t\t\t\t\tlet result = await executeTest(test);\n\n\t\t\t\t\t// Handle retries\n\t\t\t\t\tconst maxRetries = test.options.retries ?? this.options.config.retries;\n\t\t\t\t\tlet retryCount = 0;\n\n\t\t\t\t\twhile (result.status === 'failed' && retryCount < maxRetries) {\n\t\t\t\t\t\tretryCount++;\n\t\t\t\t\t\tresult = await executeTest(test);\n\t\t\t\t\t}\n\n\t\t\t\t\tif (retryCount > 0) {\n\t\t\t\t\t\tresult.retries = retryCount;\n\t\t\t\t\t}\n\n\t\t\t\t\tallResults.push(result);\n\n\t\t\t\t\t// Print result\n\t\t\t\t\tconst prefix = this.getStatusIcon(result.status);\n\t\t\t\t\tconst suiteName = result.suitePath.length > 0\n\t\t\t\t\t\t? `${result.suitePath.join(' > ')} > `\n\t\t\t\t\t\t: '';\n\t\t\t\t\tconst duration = result.status !== 'skipped' ? ` (${result.duration}ms)` : '';\n\n\t\t\t\t\tconsole.log(` ${prefix} ${suiteName}${result.title}${duration}`);\n\n\t\t\t\t\tif (result.status === 'failed' && result.error) {\n\t\t\t\t\t\tconsole.log(` ${result.error.message}`);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} catch (error) {\n\t\t\t\tconst errorResult: TestResult = {\n\t\t\t\t\ttitle: `Failed to load: ${relPath}`,\n\t\t\t\t\tsuitePath: [],\n\t\t\t\t\tstatus: 'failed',\n\t\t\t\t\tduration: 0,\n\t\t\t\t\terror: error instanceof Error ? error : new Error(String(error)),\n\t\t\t\t};\n\t\t\t\tallResults.push(errorResult);\n\t\t\t\tconsole.log(` ${this.getStatusIcon('failed')} ${errorResult.title}`);\n\t\t\t\tconsole.log(` ${errorResult.error!.message}`);\n\t\t\t}\n\n\t\t\tconsole.log('');\n\n\t\t\t// Check bail\n\t\t\tif (this.options.bail && allResults.some(r => r.status === 'failed')) {\n\t\t\t\tbail = true;\n\t\t\t}\n\t\t}\n\n\t\t// Step 3: Print summary\n\t\tconst summary = this.summarize(allResults, Date.now() - startTime);\n\t\tthis.printSummary(summary);\n\n\t\treturn summary.failed > 0 ? 1 : 0;\n\t}\n\n\t// -----------------------------------------------------------------------\n\t// File Discovery\n\t// -----------------------------------------------------------------------\n\n\tdiscoverFiles(): string[] {\n\t\t// If specific files are provided, use those\n\t\tif (this.options.files && this.options.files.length > 0) {\n\t\t\treturn this.options.files.map(f => resolve(process.cwd(), f)).filter(f => existsSync(f));\n\t\t}\n\n\t\t// Otherwise, find files matching the test pattern\n\t\tconst cwd = process.cwd();\n\t\treturn this.findTestFiles(cwd);\n\t}\n\n\t/**\n\t * Find test files matching the configured pattern.\n\t */\n\tprivate findTestFiles(dir: string): string[] {\n\t\tconst files: string[] = [];\n\t\tconst pattern = this.options.config.testMatch;\n\n\t\t// Extract extensions from pattern like '*.test.{ts,js,mts,mjs}'\n\t\tconst extMatch = pattern.match(/\\.\\{([^}]+)\\}$/);\n\t\tconst extensions = extMatch\n\t\t\t? extMatch[1]!.split(',').map(e => e.trim())\n\t\t\t: ['ts', 'js'];\n\n\t\t// Extract the suffix part (e.g., '.test')\n\t\tconst suffixMatch = pattern.match(/\\*(\\.[^{*]+)\\./);\n\t\tconst suffix = suffixMatch ? suffixMatch[1]! : '.test';\n\n\t\tthis.walkDir(dir, files, extensions, suffix);\n\t\treturn files.sort();\n\t}\n\n\tprivate walkDir(dir: string, results: string[], extensions: string[], suffix: string): void {\n\t\tconst skip = new Set(['node_modules', 'dist', '.browsecraft', '.git', 'coverage', '.turbo']);\n\n\t\tlet entries: string[];\n\t\ttry {\n\t\t\tentries = readdirSync(dir);\n\t\t} catch {\n\t\t\treturn;\n\t\t}\n\n\t\tfor (const entry of entries) {\n\t\t\tif (skip.has(entry)) continue;\n\n\t\t\tconst fullPath = join(dir, entry);\n\t\t\tlet stat: ReturnType<typeof statSync>;\n\n\t\t\ttry {\n\t\t\t\tstat = statSync(fullPath);\n\t\t\t} catch {\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tif (stat.isDirectory()) {\n\t\t\t\tthis.walkDir(fullPath, results, extensions, suffix);\n\t\t\t} else if (stat.isFile()) {\n\t\t\t\t// Check if file matches pattern like \"foo.test.ts\"\n\t\t\t\tconst matchesPattern = extensions.some(ext =>\n\t\t\t\t\tentry.endsWith(`${suffix}.${ext}`),\n\t\t\t\t);\n\t\t\t\tif (matchesPattern) {\n\t\t\t\t\tresults.push(fullPath);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\t// -----------------------------------------------------------------------\n\t// Reporting\n\t// -----------------------------------------------------------------------\n\n\tprivate getStatusIcon(status: string): string {\n\t\tswitch (status) {\n\t\t\tcase 'passed': return '\\x1b[32m+\\x1b[0m';\n\t\t\tcase 'failed': return '\\x1b[31mx\\x1b[0m';\n\t\t\tcase 'skipped': return '\\x1b[33m-\\x1b[0m';\n\t\t\tdefault: return ' ';\n\t\t}\n\t}\n\n\tprivate summarize(results: TestResult[], totalDuration: number): RunSummary {\n\t\treturn {\n\t\t\ttotal: results.length,\n\t\t\tpassed: results.filter(r => r.status === 'passed').length,\n\t\t\tfailed: results.filter(r => r.status === 'failed').length,\n\t\t\tskipped: results.filter(r => r.status === 'skipped').length,\n\t\t\tduration: totalDuration,\n\t\t\tresults,\n\t\t};\n\t}\n\n\tprivate printSummary(summary: RunSummary): void {\n\t\tconst { total, passed, failed, skipped, duration } = summary;\n\n\t\tconsole.log(' ─────────────────────────────────────');\n\n\t\tconst parts: string[] = [];\n\t\tif (passed > 0) parts.push(`\\x1b[32m${passed} passed\\x1b[0m`);\n\t\tif (failed > 0) parts.push(`\\x1b[31m${failed} failed\\x1b[0m`);\n\t\tif (skipped > 0) parts.push(`\\x1b[33m${skipped} skipped\\x1b[0m`);\n\n\t\tconsole.log(` Tests: ${parts.join(', ')} (${total} total)`);\n\t\tconsole.log(` Time: ${this.formatDuration(duration)}`);\n\t\tconsole.log('');\n\n\t\tif (failed > 0) {\n\t\t\tconsole.log(' \\x1b[31mSome tests failed.\\x1b[0m\\n');\n\t\t} else if (total === 0) {\n\t\t\tconsole.log(' No tests were run.\\n');\n\t\t} else {\n\t\t\tconsole.log(' \\x1b[32mAll tests passed!\\x1b[0m\\n');\n\t\t}\n\t}\n\n\tprivate formatDuration(ms: number): string {\n\t\tif (ms < 1000) return `${ms}ms`;\n\t\tif (ms < 60_000) return `${(ms / 1000).toFixed(1)}s`;\n\t\tconst minutes = Math.floor(ms / 60_000);\n\t\tconst seconds = ((ms % 60_000) / 1000).toFixed(1);\n\t\treturn `${minutes}m ${seconds}s`;\n\t}\n}\n"]}
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/** Configuration shape (mirrors browsecraft's BrowsecraftConfig) */
|
|
2
|
+
interface BrowsecraftConfig {
|
|
3
|
+
browser: 'chrome' | 'firefox' | 'edge';
|
|
4
|
+
headless: boolean;
|
|
5
|
+
timeout: number;
|
|
6
|
+
retries: number;
|
|
7
|
+
screenshot: 'always' | 'on-failure' | 'never';
|
|
8
|
+
baseURL: string;
|
|
9
|
+
executablePath?: string;
|
|
10
|
+
viewport: {
|
|
11
|
+
width: number;
|
|
12
|
+
height: number;
|
|
13
|
+
};
|
|
14
|
+
/** Start the browser window maximized (headed mode only, default: false) */
|
|
15
|
+
maximized: boolean;
|
|
16
|
+
workers: number;
|
|
17
|
+
testMatch: string;
|
|
18
|
+
outputDir: string;
|
|
19
|
+
ai: 'auto' | 'off' | {
|
|
20
|
+
provider: 'github-models';
|
|
21
|
+
model?: string;
|
|
22
|
+
token?: string;
|
|
23
|
+
};
|
|
24
|
+
debug: boolean;
|
|
25
|
+
}
|
|
26
|
+
/** Options for the test runner */
|
|
27
|
+
interface RunnerOptions {
|
|
28
|
+
/** Resolved config */
|
|
29
|
+
config: BrowsecraftConfig;
|
|
30
|
+
/** Specific files to run (overrides testMatch) */
|
|
31
|
+
files?: string[];
|
|
32
|
+
/** Filter tests by name pattern */
|
|
33
|
+
grep?: string;
|
|
34
|
+
/** Stop after first failure */
|
|
35
|
+
bail?: boolean;
|
|
36
|
+
}
|
|
37
|
+
/** Result of a single test execution */
|
|
38
|
+
interface TestResult {
|
|
39
|
+
title: string;
|
|
40
|
+
suitePath: string[];
|
|
41
|
+
status: 'passed' | 'failed' | 'skipped';
|
|
42
|
+
duration: number;
|
|
43
|
+
error?: Error;
|
|
44
|
+
retries?: number;
|
|
45
|
+
}
|
|
46
|
+
/** Summary of a full test run */
|
|
47
|
+
interface RunSummary {
|
|
48
|
+
total: number;
|
|
49
|
+
passed: number;
|
|
50
|
+
failed: number;
|
|
51
|
+
skipped: number;
|
|
52
|
+
duration: number;
|
|
53
|
+
results: TestResult[];
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** A test case as passed to the runner from the browsecraft package */
|
|
57
|
+
interface RunnableTest {
|
|
58
|
+
title: string;
|
|
59
|
+
suitePath: string[];
|
|
60
|
+
skip: boolean;
|
|
61
|
+
only: boolean;
|
|
62
|
+
options: {
|
|
63
|
+
timeout?: number;
|
|
64
|
+
retries?: number;
|
|
65
|
+
tags?: string[];
|
|
66
|
+
};
|
|
67
|
+
fn: (fixtures: unknown) => Promise<void>;
|
|
68
|
+
}
|
|
69
|
+
/** Callback that the CLI provides to execute a single test */
|
|
70
|
+
type TestExecutor = (test: RunnableTest) => Promise<TestResult>;
|
|
71
|
+
/**
|
|
72
|
+
* TestRunner discovers test files, loads them, and coordinates execution.
|
|
73
|
+
*
|
|
74
|
+
* Used by the CLI:
|
|
75
|
+
* ```ts
|
|
76
|
+
* const runner = new TestRunner({ config });
|
|
77
|
+
* const exitCode = await runner.run(getTests, executeTest);
|
|
78
|
+
* ```
|
|
79
|
+
*/
|
|
80
|
+
declare class TestRunner {
|
|
81
|
+
private options;
|
|
82
|
+
constructor(options: RunnerOptions);
|
|
83
|
+
/**
|
|
84
|
+
* Run all tests and return an exit code (0 = success, 1 = failure).
|
|
85
|
+
*
|
|
86
|
+
* @param loadFile - Load a test file (triggers test registration). Returns registered tests.
|
|
87
|
+
* @param executeTest - Execute a single test and return its result.
|
|
88
|
+
*/
|
|
89
|
+
run(loadFile: (file: string) => Promise<RunnableTest[]>, executeTest: TestExecutor): Promise<number>;
|
|
90
|
+
discoverFiles(): string[];
|
|
91
|
+
/**
|
|
92
|
+
* Find test files matching the configured pattern.
|
|
93
|
+
*/
|
|
94
|
+
private findTestFiles;
|
|
95
|
+
private walkDir;
|
|
96
|
+
private getStatusIcon;
|
|
97
|
+
private summarize;
|
|
98
|
+
private printSummary;
|
|
99
|
+
private formatDuration;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export { type BrowsecraftConfig, type RunSummary, type RunnableTest, type RunnerOptions, type TestExecutor, type TestResult, TestRunner };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/** Configuration shape (mirrors browsecraft's BrowsecraftConfig) */
|
|
2
|
+
interface BrowsecraftConfig {
|
|
3
|
+
browser: 'chrome' | 'firefox' | 'edge';
|
|
4
|
+
headless: boolean;
|
|
5
|
+
timeout: number;
|
|
6
|
+
retries: number;
|
|
7
|
+
screenshot: 'always' | 'on-failure' | 'never';
|
|
8
|
+
baseURL: string;
|
|
9
|
+
executablePath?: string;
|
|
10
|
+
viewport: {
|
|
11
|
+
width: number;
|
|
12
|
+
height: number;
|
|
13
|
+
};
|
|
14
|
+
/** Start the browser window maximized (headed mode only, default: false) */
|
|
15
|
+
maximized: boolean;
|
|
16
|
+
workers: number;
|
|
17
|
+
testMatch: string;
|
|
18
|
+
outputDir: string;
|
|
19
|
+
ai: 'auto' | 'off' | {
|
|
20
|
+
provider: 'github-models';
|
|
21
|
+
model?: string;
|
|
22
|
+
token?: string;
|
|
23
|
+
};
|
|
24
|
+
debug: boolean;
|
|
25
|
+
}
|
|
26
|
+
/** Options for the test runner */
|
|
27
|
+
interface RunnerOptions {
|
|
28
|
+
/** Resolved config */
|
|
29
|
+
config: BrowsecraftConfig;
|
|
30
|
+
/** Specific files to run (overrides testMatch) */
|
|
31
|
+
files?: string[];
|
|
32
|
+
/** Filter tests by name pattern */
|
|
33
|
+
grep?: string;
|
|
34
|
+
/** Stop after first failure */
|
|
35
|
+
bail?: boolean;
|
|
36
|
+
}
|
|
37
|
+
/** Result of a single test execution */
|
|
38
|
+
interface TestResult {
|
|
39
|
+
title: string;
|
|
40
|
+
suitePath: string[];
|
|
41
|
+
status: 'passed' | 'failed' | 'skipped';
|
|
42
|
+
duration: number;
|
|
43
|
+
error?: Error;
|
|
44
|
+
retries?: number;
|
|
45
|
+
}
|
|
46
|
+
/** Summary of a full test run */
|
|
47
|
+
interface RunSummary {
|
|
48
|
+
total: number;
|
|
49
|
+
passed: number;
|
|
50
|
+
failed: number;
|
|
51
|
+
skipped: number;
|
|
52
|
+
duration: number;
|
|
53
|
+
results: TestResult[];
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** A test case as passed to the runner from the browsecraft package */
|
|
57
|
+
interface RunnableTest {
|
|
58
|
+
title: string;
|
|
59
|
+
suitePath: string[];
|
|
60
|
+
skip: boolean;
|
|
61
|
+
only: boolean;
|
|
62
|
+
options: {
|
|
63
|
+
timeout?: number;
|
|
64
|
+
retries?: number;
|
|
65
|
+
tags?: string[];
|
|
66
|
+
};
|
|
67
|
+
fn: (fixtures: unknown) => Promise<void>;
|
|
68
|
+
}
|
|
69
|
+
/** Callback that the CLI provides to execute a single test */
|
|
70
|
+
type TestExecutor = (test: RunnableTest) => Promise<TestResult>;
|
|
71
|
+
/**
|
|
72
|
+
* TestRunner discovers test files, loads them, and coordinates execution.
|
|
73
|
+
*
|
|
74
|
+
* Used by the CLI:
|
|
75
|
+
* ```ts
|
|
76
|
+
* const runner = new TestRunner({ config });
|
|
77
|
+
* const exitCode = await runner.run(getTests, executeTest);
|
|
78
|
+
* ```
|
|
79
|
+
*/
|
|
80
|
+
declare class TestRunner {
|
|
81
|
+
private options;
|
|
82
|
+
constructor(options: RunnerOptions);
|
|
83
|
+
/**
|
|
84
|
+
* Run all tests and return an exit code (0 = success, 1 = failure).
|
|
85
|
+
*
|
|
86
|
+
* @param loadFile - Load a test file (triggers test registration). Returns registered tests.
|
|
87
|
+
* @param executeTest - Execute a single test and return its result.
|
|
88
|
+
*/
|
|
89
|
+
run(loadFile: (file: string) => Promise<RunnableTest[]>, executeTest: TestExecutor): Promise<number>;
|
|
90
|
+
discoverFiles(): string[];
|
|
91
|
+
/**
|
|
92
|
+
* Find test files matching the configured pattern.
|
|
93
|
+
*/
|
|
94
|
+
private findTestFiles;
|
|
95
|
+
private walkDir;
|
|
96
|
+
private getStatusIcon;
|
|
97
|
+
private summarize;
|
|
98
|
+
private printSummary;
|
|
99
|
+
private formatDuration;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export { type BrowsecraftConfig, type RunSummary, type RunnableTest, type RunnerOptions, type TestExecutor, type TestResult, TestRunner };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import { relative, resolve, join } from 'path';
|
|
2
|
+
import { existsSync, readdirSync, statSync } from 'fs';
|
|
3
|
+
|
|
4
|
+
// src/runner.ts
|
|
5
|
+
var TestRunner = class {
|
|
6
|
+
options;
|
|
7
|
+
constructor(options) {
|
|
8
|
+
this.options = options;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Run all tests and return an exit code (0 = success, 1 = failure).
|
|
12
|
+
*
|
|
13
|
+
* @param loadFile - Load a test file (triggers test registration). Returns registered tests.
|
|
14
|
+
* @param executeTest - Execute a single test and return its result.
|
|
15
|
+
*/
|
|
16
|
+
async run(loadFile, executeTest) {
|
|
17
|
+
const startTime = Date.now();
|
|
18
|
+
const files = this.discoverFiles();
|
|
19
|
+
if (files.length === 0) {
|
|
20
|
+
console.log("\n No test files found.\n");
|
|
21
|
+
console.log(` Test pattern: ${this.options.config.testMatch}`);
|
|
22
|
+
console.log(' Run "browsecraft init" to create an example test.\n');
|
|
23
|
+
return 0;
|
|
24
|
+
}
|
|
25
|
+
console.log(`
|
|
26
|
+
Browsecraft - Running ${files.length} test file${files.length > 1 ? "s" : ""}
|
|
27
|
+
`);
|
|
28
|
+
const allResults = [];
|
|
29
|
+
let bail = false;
|
|
30
|
+
for (const file of files) {
|
|
31
|
+
if (bail) break;
|
|
32
|
+
const relPath = relative(process.cwd(), file);
|
|
33
|
+
console.log(` ${relPath}`);
|
|
34
|
+
try {
|
|
35
|
+
const tests = await loadFile(file);
|
|
36
|
+
const filteredTests = this.options.grep ? tests.filter((t) => t.title.includes(this.options.grep)) : tests;
|
|
37
|
+
const hasOnly = filteredTests.some((t) => t.only);
|
|
38
|
+
const testsToRun = hasOnly ? filteredTests.filter((t) => t.only) : filteredTests;
|
|
39
|
+
for (const test of testsToRun) {
|
|
40
|
+
let result = await executeTest(test);
|
|
41
|
+
const maxRetries = test.options.retries ?? this.options.config.retries;
|
|
42
|
+
let retryCount = 0;
|
|
43
|
+
while (result.status === "failed" && retryCount < maxRetries) {
|
|
44
|
+
retryCount++;
|
|
45
|
+
result = await executeTest(test);
|
|
46
|
+
}
|
|
47
|
+
if (retryCount > 0) {
|
|
48
|
+
result.retries = retryCount;
|
|
49
|
+
}
|
|
50
|
+
allResults.push(result);
|
|
51
|
+
const prefix = this.getStatusIcon(result.status);
|
|
52
|
+
const suiteName = result.suitePath.length > 0 ? `${result.suitePath.join(" > ")} > ` : "";
|
|
53
|
+
const duration = result.status !== "skipped" ? ` (${result.duration}ms)` : "";
|
|
54
|
+
console.log(` ${prefix} ${suiteName}${result.title}${duration}`);
|
|
55
|
+
if (result.status === "failed" && result.error) {
|
|
56
|
+
console.log(` ${result.error.message}`);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
} catch (error) {
|
|
60
|
+
const errorResult = {
|
|
61
|
+
title: `Failed to load: ${relPath}`,
|
|
62
|
+
suitePath: [],
|
|
63
|
+
status: "failed",
|
|
64
|
+
duration: 0,
|
|
65
|
+
error: error instanceof Error ? error : new Error(String(error))
|
|
66
|
+
};
|
|
67
|
+
allResults.push(errorResult);
|
|
68
|
+
console.log(` ${this.getStatusIcon("failed")} ${errorResult.title}`);
|
|
69
|
+
console.log(` ${errorResult.error.message}`);
|
|
70
|
+
}
|
|
71
|
+
console.log("");
|
|
72
|
+
if (this.options.bail && allResults.some((r) => r.status === "failed")) {
|
|
73
|
+
bail = true;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
const summary = this.summarize(allResults, Date.now() - startTime);
|
|
77
|
+
this.printSummary(summary);
|
|
78
|
+
return summary.failed > 0 ? 1 : 0;
|
|
79
|
+
}
|
|
80
|
+
// -----------------------------------------------------------------------
|
|
81
|
+
// File Discovery
|
|
82
|
+
// -----------------------------------------------------------------------
|
|
83
|
+
discoverFiles() {
|
|
84
|
+
if (this.options.files && this.options.files.length > 0) {
|
|
85
|
+
return this.options.files.map((f) => resolve(process.cwd(), f)).filter((f) => existsSync(f));
|
|
86
|
+
}
|
|
87
|
+
const cwd = process.cwd();
|
|
88
|
+
return this.findTestFiles(cwd);
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Find test files matching the configured pattern.
|
|
92
|
+
*/
|
|
93
|
+
findTestFiles(dir) {
|
|
94
|
+
const files = [];
|
|
95
|
+
const pattern = this.options.config.testMatch;
|
|
96
|
+
const extMatch = pattern.match(/\.\{([^}]+)\}$/);
|
|
97
|
+
const extensions = extMatch ? extMatch[1].split(",").map((e) => e.trim()) : ["ts", "js"];
|
|
98
|
+
const suffixMatch = pattern.match(/\*(\.[^{*]+)\./);
|
|
99
|
+
const suffix = suffixMatch ? suffixMatch[1] : ".test";
|
|
100
|
+
this.walkDir(dir, files, extensions, suffix);
|
|
101
|
+
return files.sort();
|
|
102
|
+
}
|
|
103
|
+
walkDir(dir, results, extensions, suffix) {
|
|
104
|
+
const skip = /* @__PURE__ */ new Set(["node_modules", "dist", ".browsecraft", ".git", "coverage", ".turbo"]);
|
|
105
|
+
let entries;
|
|
106
|
+
try {
|
|
107
|
+
entries = readdirSync(dir);
|
|
108
|
+
} catch {
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
for (const entry of entries) {
|
|
112
|
+
if (skip.has(entry)) continue;
|
|
113
|
+
const fullPath = join(dir, entry);
|
|
114
|
+
let stat;
|
|
115
|
+
try {
|
|
116
|
+
stat = statSync(fullPath);
|
|
117
|
+
} catch {
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
if (stat.isDirectory()) {
|
|
121
|
+
this.walkDir(fullPath, results, extensions, suffix);
|
|
122
|
+
} else if (stat.isFile()) {
|
|
123
|
+
const matchesPattern = extensions.some(
|
|
124
|
+
(ext) => entry.endsWith(`${suffix}.${ext}`)
|
|
125
|
+
);
|
|
126
|
+
if (matchesPattern) {
|
|
127
|
+
results.push(fullPath);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
// -----------------------------------------------------------------------
|
|
133
|
+
// Reporting
|
|
134
|
+
// -----------------------------------------------------------------------
|
|
135
|
+
getStatusIcon(status) {
|
|
136
|
+
switch (status) {
|
|
137
|
+
case "passed":
|
|
138
|
+
return "\x1B[32m+\x1B[0m";
|
|
139
|
+
case "failed":
|
|
140
|
+
return "\x1B[31mx\x1B[0m";
|
|
141
|
+
case "skipped":
|
|
142
|
+
return "\x1B[33m-\x1B[0m";
|
|
143
|
+
default:
|
|
144
|
+
return " ";
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
summarize(results, totalDuration) {
|
|
148
|
+
return {
|
|
149
|
+
total: results.length,
|
|
150
|
+
passed: results.filter((r) => r.status === "passed").length,
|
|
151
|
+
failed: results.filter((r) => r.status === "failed").length,
|
|
152
|
+
skipped: results.filter((r) => r.status === "skipped").length,
|
|
153
|
+
duration: totalDuration,
|
|
154
|
+
results
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
printSummary(summary) {
|
|
158
|
+
const { total, passed, failed, skipped, duration } = summary;
|
|
159
|
+
console.log(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
|
|
160
|
+
const parts = [];
|
|
161
|
+
if (passed > 0) parts.push(`\x1B[32m${passed} passed\x1B[0m`);
|
|
162
|
+
if (failed > 0) parts.push(`\x1B[31m${failed} failed\x1B[0m`);
|
|
163
|
+
if (skipped > 0) parts.push(`\x1B[33m${skipped} skipped\x1B[0m`);
|
|
164
|
+
console.log(` Tests: ${parts.join(", ")} (${total} total)`);
|
|
165
|
+
console.log(` Time: ${this.formatDuration(duration)}`);
|
|
166
|
+
console.log("");
|
|
167
|
+
if (failed > 0) {
|
|
168
|
+
console.log(" \x1B[31mSome tests failed.\x1B[0m\n");
|
|
169
|
+
} else if (total === 0) {
|
|
170
|
+
console.log(" No tests were run.\n");
|
|
171
|
+
} else {
|
|
172
|
+
console.log(" \x1B[32mAll tests passed!\x1B[0m\n");
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
formatDuration(ms) {
|
|
176
|
+
if (ms < 1e3) return `${ms}ms`;
|
|
177
|
+
if (ms < 6e4) return `${(ms / 1e3).toFixed(1)}s`;
|
|
178
|
+
const minutes = Math.floor(ms / 6e4);
|
|
179
|
+
const seconds = (ms % 6e4 / 1e3).toFixed(1);
|
|
180
|
+
return `${minutes}m ${seconds}s`;
|
|
181
|
+
}
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
export { TestRunner };
|
|
185
|
+
//# sourceMappingURL=index.js.map
|
|
186
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/runner.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["// ============================================================================\n// Browsecraft Runner - Test Runner\n// Discovers test files, loads them, executes tests, reports results.\n//\n// The runner does NOT import 'browsecraft' to avoid circular deps.\n// Test execution is delegated via callbacks provided by the CLI.\n// ============================================================================\n\nimport { resolve, relative, join } from 'node:path';\nimport { existsSync, readdirSync, statSync } from 'node:fs';\nimport { pathToFileURL } from 'node:url';\nimport type { RunnerOptions, TestResult, RunSummary } from './types.js';\n\n/** A test case as passed to the runner from the browsecraft package */\nexport interface RunnableTest {\n\ttitle: string;\n\tsuitePath: string[];\n\tskip: boolean;\n\tonly: boolean;\n\toptions: { timeout?: number; retries?: number; tags?: string[] };\n\tfn: (fixtures: unknown) => Promise<void>;\n}\n\n/** Callback that the CLI provides to execute a single test */\nexport type TestExecutor = (test: RunnableTest) => Promise<TestResult>;\n\n/**\n * TestRunner discovers test files, loads them, and coordinates execution.\n *\n * Used by the CLI:\n * ```ts\n * const runner = new TestRunner({ config });\n * const exitCode = await runner.run(getTests, executeTest);\n * ```\n */\nexport class TestRunner {\n\tprivate options: RunnerOptions;\n\n\tconstructor(options: RunnerOptions) {\n\t\tthis.options = options;\n\t}\n\n\t/**\n\t * Run all tests and return an exit code (0 = success, 1 = failure).\n\t *\n\t * @param loadFile - Load a test file (triggers test registration). Returns registered tests.\n\t * @param executeTest - Execute a single test and return its result.\n\t */\n\tasync run(\n\t\tloadFile: (file: string) => Promise<RunnableTest[]>,\n\t\texecuteTest: TestExecutor,\n\t): Promise<number> {\n\t\tconst startTime = Date.now();\n\n\t\t// Step 1: Discover test files\n\t\tconst files = this.discoverFiles();\n\t\tif (files.length === 0) {\n\t\t\tconsole.log('\\n No test files found.\\n');\n\t\t\tconsole.log(` Test pattern: ${this.options.config.testMatch}`);\n\t\t\tconsole.log(' Run \"browsecraft init\" to create an example test.\\n');\n\t\t\treturn 0;\n\t\t}\n\n\t\tconsole.log(`\\n Browsecraft - Running ${files.length} test file${files.length > 1 ? 's' : ''}\\n`);\n\n\t\t// Step 2: Load and run each test file\n\t\tconst allResults: TestResult[] = [];\n\t\tlet bail = false;\n\n\t\tfor (const file of files) {\n\t\t\tif (bail) break;\n\n\t\t\tconst relPath = relative(process.cwd(), file);\n\t\t\tconsole.log(` ${relPath}`);\n\n\t\t\ttry {\n\t\t\t\tconst tests = await loadFile(file);\n\n\t\t\t\t// Apply grep filter\n\t\t\t\tconst filteredTests = this.options.grep\n\t\t\t\t\t? tests.filter(t => t.title.includes(this.options.grep!))\n\t\t\t\t\t: tests;\n\n\t\t\t\t// Check for .only tests\n\t\t\t\tconst hasOnly = filteredTests.some(t => t.only);\n\t\t\t\tconst testsToRun = hasOnly\n\t\t\t\t\t? filteredTests.filter(t => t.only)\n\t\t\t\t\t: filteredTests;\n\n\t\t\t\t// Run each test\n\t\t\t\tfor (const test of testsToRun) {\n\t\t\t\t\tlet result = await executeTest(test);\n\n\t\t\t\t\t// Handle retries\n\t\t\t\t\tconst maxRetries = test.options.retries ?? this.options.config.retries;\n\t\t\t\t\tlet retryCount = 0;\n\n\t\t\t\t\twhile (result.status === 'failed' && retryCount < maxRetries) {\n\t\t\t\t\t\tretryCount++;\n\t\t\t\t\t\tresult = await executeTest(test);\n\t\t\t\t\t}\n\n\t\t\t\t\tif (retryCount > 0) {\n\t\t\t\t\t\tresult.retries = retryCount;\n\t\t\t\t\t}\n\n\t\t\t\t\tallResults.push(result);\n\n\t\t\t\t\t// Print result\n\t\t\t\t\tconst prefix = this.getStatusIcon(result.status);\n\t\t\t\t\tconst suiteName = result.suitePath.length > 0\n\t\t\t\t\t\t? `${result.suitePath.join(' > ')} > `\n\t\t\t\t\t\t: '';\n\t\t\t\t\tconst duration = result.status !== 'skipped' ? ` (${result.duration}ms)` : '';\n\n\t\t\t\t\tconsole.log(` ${prefix} ${suiteName}${result.title}${duration}`);\n\n\t\t\t\t\tif (result.status === 'failed' && result.error) {\n\t\t\t\t\t\tconsole.log(` ${result.error.message}`);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} catch (error) {\n\t\t\t\tconst errorResult: TestResult = {\n\t\t\t\t\ttitle: `Failed to load: ${relPath}`,\n\t\t\t\t\tsuitePath: [],\n\t\t\t\t\tstatus: 'failed',\n\t\t\t\t\tduration: 0,\n\t\t\t\t\terror: error instanceof Error ? error : new Error(String(error)),\n\t\t\t\t};\n\t\t\t\tallResults.push(errorResult);\n\t\t\t\tconsole.log(` ${this.getStatusIcon('failed')} ${errorResult.title}`);\n\t\t\t\tconsole.log(` ${errorResult.error!.message}`);\n\t\t\t}\n\n\t\t\tconsole.log('');\n\n\t\t\t// Check bail\n\t\t\tif (this.options.bail && allResults.some(r => r.status === 'failed')) {\n\t\t\t\tbail = true;\n\t\t\t}\n\t\t}\n\n\t\t// Step 3: Print summary\n\t\tconst summary = this.summarize(allResults, Date.now() - startTime);\n\t\tthis.printSummary(summary);\n\n\t\treturn summary.failed > 0 ? 1 : 0;\n\t}\n\n\t// -----------------------------------------------------------------------\n\t// File Discovery\n\t// -----------------------------------------------------------------------\n\n\tdiscoverFiles(): string[] {\n\t\t// If specific files are provided, use those\n\t\tif (this.options.files && this.options.files.length > 0) {\n\t\t\treturn this.options.files.map(f => resolve(process.cwd(), f)).filter(f => existsSync(f));\n\t\t}\n\n\t\t// Otherwise, find files matching the test pattern\n\t\tconst cwd = process.cwd();\n\t\treturn this.findTestFiles(cwd);\n\t}\n\n\t/**\n\t * Find test files matching the configured pattern.\n\t */\n\tprivate findTestFiles(dir: string): string[] {\n\t\tconst files: string[] = [];\n\t\tconst pattern = this.options.config.testMatch;\n\n\t\t// Extract extensions from pattern like '*.test.{ts,js,mts,mjs}'\n\t\tconst extMatch = pattern.match(/\\.\\{([^}]+)\\}$/);\n\t\tconst extensions = extMatch\n\t\t\t? extMatch[1]!.split(',').map(e => e.trim())\n\t\t\t: ['ts', 'js'];\n\n\t\t// Extract the suffix part (e.g., '.test')\n\t\tconst suffixMatch = pattern.match(/\\*(\\.[^{*]+)\\./);\n\t\tconst suffix = suffixMatch ? suffixMatch[1]! : '.test';\n\n\t\tthis.walkDir(dir, files, extensions, suffix);\n\t\treturn files.sort();\n\t}\n\n\tprivate walkDir(dir: string, results: string[], extensions: string[], suffix: string): void {\n\t\tconst skip = new Set(['node_modules', 'dist', '.browsecraft', '.git', 'coverage', '.turbo']);\n\n\t\tlet entries: string[];\n\t\ttry {\n\t\t\tentries = readdirSync(dir);\n\t\t} catch {\n\t\t\treturn;\n\t\t}\n\n\t\tfor (const entry of entries) {\n\t\t\tif (skip.has(entry)) continue;\n\n\t\t\tconst fullPath = join(dir, entry);\n\t\t\tlet stat: ReturnType<typeof statSync>;\n\n\t\t\ttry {\n\t\t\t\tstat = statSync(fullPath);\n\t\t\t} catch {\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tif (stat.isDirectory()) {\n\t\t\t\tthis.walkDir(fullPath, results, extensions, suffix);\n\t\t\t} else if (stat.isFile()) {\n\t\t\t\t// Check if file matches pattern like \"foo.test.ts\"\n\t\t\t\tconst matchesPattern = extensions.some(ext =>\n\t\t\t\t\tentry.endsWith(`${suffix}.${ext}`),\n\t\t\t\t);\n\t\t\t\tif (matchesPattern) {\n\t\t\t\t\tresults.push(fullPath);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\t// -----------------------------------------------------------------------\n\t// Reporting\n\t// -----------------------------------------------------------------------\n\n\tprivate getStatusIcon(status: string): string {\n\t\tswitch (status) {\n\t\t\tcase 'passed': return '\\x1b[32m+\\x1b[0m';\n\t\t\tcase 'failed': return '\\x1b[31mx\\x1b[0m';\n\t\t\tcase 'skipped': return '\\x1b[33m-\\x1b[0m';\n\t\t\tdefault: return ' ';\n\t\t}\n\t}\n\n\tprivate summarize(results: TestResult[], totalDuration: number): RunSummary {\n\t\treturn {\n\t\t\ttotal: results.length,\n\t\t\tpassed: results.filter(r => r.status === 'passed').length,\n\t\t\tfailed: results.filter(r => r.status === 'failed').length,\n\t\t\tskipped: results.filter(r => r.status === 'skipped').length,\n\t\t\tduration: totalDuration,\n\t\t\tresults,\n\t\t};\n\t}\n\n\tprivate printSummary(summary: RunSummary): void {\n\t\tconst { total, passed, failed, skipped, duration } = summary;\n\n\t\tconsole.log(' ─────────────────────────────────────');\n\n\t\tconst parts: string[] = [];\n\t\tif (passed > 0) parts.push(`\\x1b[32m${passed} passed\\x1b[0m`);\n\t\tif (failed > 0) parts.push(`\\x1b[31m${failed} failed\\x1b[0m`);\n\t\tif (skipped > 0) parts.push(`\\x1b[33m${skipped} skipped\\x1b[0m`);\n\n\t\tconsole.log(` Tests: ${parts.join(', ')} (${total} total)`);\n\t\tconsole.log(` Time: ${this.formatDuration(duration)}`);\n\t\tconsole.log('');\n\n\t\tif (failed > 0) {\n\t\t\tconsole.log(' \\x1b[31mSome tests failed.\\x1b[0m\\n');\n\t\t} else if (total === 0) {\n\t\t\tconsole.log(' No tests were run.\\n');\n\t\t} else {\n\t\t\tconsole.log(' \\x1b[32mAll tests passed!\\x1b[0m\\n');\n\t\t}\n\t}\n\n\tprivate formatDuration(ms: number): string {\n\t\tif (ms < 1000) return `${ms}ms`;\n\t\tif (ms < 60_000) return `${(ms / 1000).toFixed(1)}s`;\n\t\tconst minutes = Math.floor(ms / 60_000);\n\t\tconst seconds = ((ms % 60_000) / 1000).toFixed(1);\n\t\treturn `${minutes}m ${seconds}s`;\n\t}\n}\n"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "browsecraft-runner",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Test runner and CLI for Browsecraft",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.cjs",
|
|
7
|
+
"module": "./dist/index.js",
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"import": "./dist/index.js",
|
|
13
|
+
"require": "./dist/index.cjs"
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
"files": [
|
|
17
|
+
"dist",
|
|
18
|
+
"LICENSE"
|
|
19
|
+
],
|
|
20
|
+
"sideEffects": false,
|
|
21
|
+
"publishConfig": {
|
|
22
|
+
"access": "public"
|
|
23
|
+
},
|
|
24
|
+
"dependencies": {
|
|
25
|
+
"browsecraft-bidi": "0.1.0"
|
|
26
|
+
},
|
|
27
|
+
"devDependencies": {
|
|
28
|
+
"@types/node": "^22.10.0",
|
|
29
|
+
"tsup": "^8.3.5",
|
|
30
|
+
"typescript": "^5.7.2",
|
|
31
|
+
"vitest": "^2.1.0",
|
|
32
|
+
"rimraf": "^6.0.1"
|
|
33
|
+
},
|
|
34
|
+
"license": "MIT",
|
|
35
|
+
"author": "Browsecraft Contributors",
|
|
36
|
+
"repository": {
|
|
37
|
+
"type": "git",
|
|
38
|
+
"url": "https://github.com/rik9564/browsecraft.git",
|
|
39
|
+
"directory": "packages/browsecraft-runner"
|
|
40
|
+
},
|
|
41
|
+
"homepage": "https://github.com/rik9564/browsecraft#readme",
|
|
42
|
+
"bugs": {
|
|
43
|
+
"url": "https://github.com/rik9564/browsecraft/issues"
|
|
44
|
+
},
|
|
45
|
+
"engines": {
|
|
46
|
+
"node": ">=20.0.0"
|
|
47
|
+
},
|
|
48
|
+
"scripts": {
|
|
49
|
+
"build": "tsup",
|
|
50
|
+
"dev": "tsup --watch",
|
|
51
|
+
"typecheck": "tsc --noEmit",
|
|
52
|
+
"test": "vitest run",
|
|
53
|
+
"clean": "rimraf dist"
|
|
54
|
+
}
|
|
55
|
+
}
|