browsecraft 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/chunk-77HRTGXZ.js +2004 -0
- package/dist/chunk-77HRTGXZ.js.map +1 -0
- package/dist/chunk-KIPQFK3Y.cjs +2021 -0
- package/dist/chunk-KIPQFK3Y.cjs.map +1 -0
- package/dist/cli.cjs +297 -0
- package/dist/cli.cjs.map +1 -0
- package/dist/cli.d.cts +1 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +295 -0
- package/dist/cli.js.map +1 -0
- package/dist/index.cjs +831 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +984 -0
- package/dist/index.d.ts +984 -0
- package/dist/index.js +632 -0
- package/dist/index.js.map +1 -0
- package/package.json +72 -0
package/dist/cli.cjs
ADDED
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
var chunkKIPQFK3Y_cjs = require('./chunk-KIPQFK3Y.cjs');
|
|
5
|
+
var path = require('path');
|
|
6
|
+
var fs = require('fs');
|
|
7
|
+
var url = require('url');
|
|
8
|
+
var browsecraftRunner = require('browsecraft-runner');
|
|
9
|
+
|
|
10
|
+
var VERSION = "0.1.0";
|
|
11
|
+
async function main() {
|
|
12
|
+
const args = process.argv.slice(2);
|
|
13
|
+
if (args.length === 0 || args.includes("--help") || args.includes("-h")) {
|
|
14
|
+
printHelp();
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
if (args.includes("--version") || args.includes("-v")) {
|
|
18
|
+
console.log(`browsecraft v${VERSION}`);
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
const command = args[0];
|
|
22
|
+
switch (command) {
|
|
23
|
+
case "test":
|
|
24
|
+
await runTests(args.slice(1));
|
|
25
|
+
break;
|
|
26
|
+
case "init":
|
|
27
|
+
await initProject();
|
|
28
|
+
break;
|
|
29
|
+
default:
|
|
30
|
+
if (command && (command.endsWith(".ts") || command.endsWith(".js"))) {
|
|
31
|
+
await runTests(args);
|
|
32
|
+
} else {
|
|
33
|
+
console.error(`Unknown command: ${command}`);
|
|
34
|
+
console.error('Run "browsecraft --help" for usage information.');
|
|
35
|
+
process.exit(1);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
async function runTests(args) {
|
|
40
|
+
const flags = parseFlags(args);
|
|
41
|
+
const filePatterns = args.filter((a) => !a.startsWith("--"));
|
|
42
|
+
const userConfig = await loadConfig();
|
|
43
|
+
const config = chunkKIPQFK3Y_cjs.resolveConfig(userConfig);
|
|
44
|
+
if (flags.headed || flags.headless === false) {
|
|
45
|
+
config.headless = false;
|
|
46
|
+
}
|
|
47
|
+
if (flags.browser) {
|
|
48
|
+
config.browser = flags.browser;
|
|
49
|
+
}
|
|
50
|
+
if (flags.workers !== void 0) {
|
|
51
|
+
config.workers = flags.workers;
|
|
52
|
+
}
|
|
53
|
+
if (flags.timeout !== void 0) {
|
|
54
|
+
config.timeout = flags.timeout;
|
|
55
|
+
}
|
|
56
|
+
if (flags.retries !== void 0) {
|
|
57
|
+
config.retries = flags.retries;
|
|
58
|
+
}
|
|
59
|
+
if (flags.debug) {
|
|
60
|
+
config.debug = true;
|
|
61
|
+
}
|
|
62
|
+
const runnerOptions = {
|
|
63
|
+
config,
|
|
64
|
+
files: filePatterns.length > 0 ? filePatterns : void 0,
|
|
65
|
+
grep: flags.grep,
|
|
66
|
+
bail: flags.bail
|
|
67
|
+
};
|
|
68
|
+
const runner = new browsecraftRunner.TestRunner(runnerOptions);
|
|
69
|
+
const loadFile = async (file) => {
|
|
70
|
+
const startIdx = chunkKIPQFK3Y_cjs.testRegistry.length;
|
|
71
|
+
if (file.endsWith(".ts") || file.endsWith(".mts")) {
|
|
72
|
+
await ensureTypeScriptLoader();
|
|
73
|
+
}
|
|
74
|
+
const fileUrl = url.pathToFileURL(file).href;
|
|
75
|
+
await import(fileUrl);
|
|
76
|
+
return chunkKIPQFK3Y_cjs.testRegistry.slice(startIdx).map((tc) => ({
|
|
77
|
+
title: tc.title,
|
|
78
|
+
suitePath: tc.suitePath,
|
|
79
|
+
skip: tc.skip,
|
|
80
|
+
only: tc.only,
|
|
81
|
+
options: tc.options,
|
|
82
|
+
fn: tc.fn
|
|
83
|
+
}));
|
|
84
|
+
};
|
|
85
|
+
let sharedBrowser;
|
|
86
|
+
try {
|
|
87
|
+
sharedBrowser = await chunkKIPQFK3Y_cjs.Browser.launch({
|
|
88
|
+
browser: config.browser,
|
|
89
|
+
headless: config.headless,
|
|
90
|
+
executablePath: config.executablePath,
|
|
91
|
+
debug: config.debug,
|
|
92
|
+
timeout: config.timeout
|
|
93
|
+
});
|
|
94
|
+
} catch (err) {
|
|
95
|
+
console.error(`Failed to launch browser: ${err instanceof Error ? err.message : String(err)}`);
|
|
96
|
+
process.exit(1);
|
|
97
|
+
}
|
|
98
|
+
const executeTest = async (test) => {
|
|
99
|
+
return chunkKIPQFK3Y_cjs.runTest(test, sharedBrowser);
|
|
100
|
+
};
|
|
101
|
+
try {
|
|
102
|
+
const exitCode = await runner.run(loadFile, executeTest);
|
|
103
|
+
await sharedBrowser.close().catch(() => {
|
|
104
|
+
});
|
|
105
|
+
process.exit(exitCode);
|
|
106
|
+
} catch (err) {
|
|
107
|
+
await sharedBrowser?.close().catch(() => {
|
|
108
|
+
});
|
|
109
|
+
throw err;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
async function initProject() {
|
|
113
|
+
const cwd = process.cwd();
|
|
114
|
+
console.log("\n Browsecraft - Project Setup\n");
|
|
115
|
+
const configPath = path.join(cwd, "browsecraft.config.ts");
|
|
116
|
+
if (!fs.existsSync(configPath)) {
|
|
117
|
+
fs.writeFileSync(configPath, `import { defineConfig } from 'browsecraft';
|
|
118
|
+
|
|
119
|
+
export default defineConfig({
|
|
120
|
+
// Browser to use: 'chrome' | 'firefox' | 'edge'
|
|
121
|
+
browser: 'chrome',
|
|
122
|
+
|
|
123
|
+
// Run tests in headless mode
|
|
124
|
+
headless: true,
|
|
125
|
+
|
|
126
|
+
// Base URL for page.goto() calls
|
|
127
|
+
// baseURL: 'http://localhost:3000',
|
|
128
|
+
|
|
129
|
+
// Global timeout for actions (ms)
|
|
130
|
+
timeout: 30_000,
|
|
131
|
+
|
|
132
|
+
// Take screenshots on failure
|
|
133
|
+
screenshot: 'on-failure',
|
|
134
|
+
});
|
|
135
|
+
`);
|
|
136
|
+
console.log(" Created browsecraft.config.ts");
|
|
137
|
+
} else {
|
|
138
|
+
console.log(" browsecraft.config.ts already exists, skipping");
|
|
139
|
+
}
|
|
140
|
+
const testsDir = path.join(cwd, "tests");
|
|
141
|
+
if (!fs.existsSync(testsDir)) {
|
|
142
|
+
fs.mkdirSync(testsDir, { recursive: true });
|
|
143
|
+
}
|
|
144
|
+
const exampleTest = path.join(testsDir, "example.test.ts");
|
|
145
|
+
if (!fs.existsSync(exampleTest)) {
|
|
146
|
+
fs.writeFileSync(exampleTest, `import { test, expect } from 'browsecraft';
|
|
147
|
+
|
|
148
|
+
test('homepage has correct title', async ({ page }) => {
|
|
149
|
+
await page.goto('https://example.com');
|
|
150
|
+
await expect(page).toHaveTitle('Example Domain');
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
test('can navigate to more info', async ({ page }) => {
|
|
154
|
+
await page.goto('https://example.com');
|
|
155
|
+
await page.click('More information');
|
|
156
|
+
await expect(page).toHaveURL(/iana\\.org/);
|
|
157
|
+
});
|
|
158
|
+
`);
|
|
159
|
+
console.log(" Created tests/example.test.ts");
|
|
160
|
+
} else {
|
|
161
|
+
console.log(" tests/example.test.ts already exists, skipping");
|
|
162
|
+
}
|
|
163
|
+
const gitignorePath = path.join(cwd, ".gitignore");
|
|
164
|
+
if (fs.existsSync(gitignorePath)) {
|
|
165
|
+
const content = await import('fs').then((fs) => fs.readFileSync(gitignorePath, "utf-8"));
|
|
166
|
+
if (!content.includes(".browsecraft")) {
|
|
167
|
+
fs.writeFileSync(gitignorePath, `${content.trimEnd()}
|
|
168
|
+
|
|
169
|
+
# Browsecraft
|
|
170
|
+
.browsecraft/
|
|
171
|
+
`);
|
|
172
|
+
console.log(" Updated .gitignore");
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
console.log("\n Setup complete! Run your first test:\n");
|
|
176
|
+
console.log(" npx browsecraft test\n");
|
|
177
|
+
}
|
|
178
|
+
async function loadConfig() {
|
|
179
|
+
const cwd = process.cwd();
|
|
180
|
+
const candidates = [
|
|
181
|
+
"browsecraft.config.ts",
|
|
182
|
+
"browsecraft.config.js",
|
|
183
|
+
"browsecraft.config.mjs",
|
|
184
|
+
"browsecraft.config.mts"
|
|
185
|
+
];
|
|
186
|
+
for (const name of candidates) {
|
|
187
|
+
const configPath = path.resolve(cwd, name);
|
|
188
|
+
if (fs.existsSync(configPath)) {
|
|
189
|
+
try {
|
|
190
|
+
const mod = await import(configPath);
|
|
191
|
+
return mod.default ?? mod;
|
|
192
|
+
} catch {
|
|
193
|
+
console.warn(`Warning: Could not load ${name}. Using defaults.`);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
return void 0;
|
|
198
|
+
}
|
|
199
|
+
var tsLoaderRegistered = false;
|
|
200
|
+
async function ensureTypeScriptLoader() {
|
|
201
|
+
if (tsLoaderRegistered) return;
|
|
202
|
+
const execArgs = process.execArgv.join(" ");
|
|
203
|
+
if (execArgs.includes("tsx") || execArgs.includes("ts-node") || execArgs.includes("loader") || process.versions.bun) {
|
|
204
|
+
tsLoaderRegistered = true;
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
for (const loader of ["tsx/esm", "ts-node/esm"]) {
|
|
208
|
+
try {
|
|
209
|
+
const { register } = await import('module');
|
|
210
|
+
if (typeof register === "function") {
|
|
211
|
+
register(loader, url.pathToFileURL("./"));
|
|
212
|
+
tsLoaderRegistered = true;
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
} catch {
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
console.error(
|
|
219
|
+
"\n Error: Cannot import TypeScript test files.\n Install tsx (recommended) or ts-node:\n\n npm install -D tsx\n\n Or run browsecraft with tsx:\n\n npx tsx node_modules/.bin/browsecraft test\n"
|
|
220
|
+
);
|
|
221
|
+
process.exit(1);
|
|
222
|
+
}
|
|
223
|
+
function parseFlags(args) {
|
|
224
|
+
const flags = {};
|
|
225
|
+
for (let i = 0; i < args.length; i++) {
|
|
226
|
+
const arg = args[i];
|
|
227
|
+
switch (arg) {
|
|
228
|
+
case "--headed":
|
|
229
|
+
flags.headed = true;
|
|
230
|
+
break;
|
|
231
|
+
case "--headless":
|
|
232
|
+
flags.headless = true;
|
|
233
|
+
break;
|
|
234
|
+
case "--debug":
|
|
235
|
+
flags.debug = true;
|
|
236
|
+
break;
|
|
237
|
+
case "--bail":
|
|
238
|
+
flags.bail = true;
|
|
239
|
+
break;
|
|
240
|
+
case "--browser":
|
|
241
|
+
flags.browser = args[++i];
|
|
242
|
+
break;
|
|
243
|
+
case "--workers":
|
|
244
|
+
flags.workers = Number.parseInt(args[++i] ?? "1", 10);
|
|
245
|
+
break;
|
|
246
|
+
case "--timeout":
|
|
247
|
+
flags.timeout = Number.parseInt(args[++i] ?? "30000", 10);
|
|
248
|
+
break;
|
|
249
|
+
case "--retries":
|
|
250
|
+
flags.retries = Number.parseInt(args[++i] ?? "0", 10);
|
|
251
|
+
break;
|
|
252
|
+
case "--grep":
|
|
253
|
+
case "-g":
|
|
254
|
+
flags.grep = args[++i];
|
|
255
|
+
break;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
return flags;
|
|
259
|
+
}
|
|
260
|
+
function printHelp() {
|
|
261
|
+
console.log(`
|
|
262
|
+
browsecraft v${VERSION} -- AI-native browser testing
|
|
263
|
+
|
|
264
|
+
Usage:
|
|
265
|
+
browsecraft test [files...] [options]
|
|
266
|
+
browsecraft init
|
|
267
|
+
|
|
268
|
+
Commands:
|
|
269
|
+
test Run browser tests
|
|
270
|
+
init Create a new project with example config and test
|
|
271
|
+
|
|
272
|
+
Options:
|
|
273
|
+
--browser <name> Browser to use: chrome, firefox, edge (default: chrome)
|
|
274
|
+
--headed Run in headed mode (show the browser)
|
|
275
|
+
--headless Run in headless mode (default)
|
|
276
|
+
--workers <n> Number of parallel workers (default: half CPU cores)
|
|
277
|
+
--timeout <ms> Global timeout in milliseconds (default: 30000)
|
|
278
|
+
--retries <n> Retry failed tests n times (default: 0)
|
|
279
|
+
--grep <pattern> Only run tests matching pattern
|
|
280
|
+
--bail Stop after first failure
|
|
281
|
+
--debug Enable verbose debug logging
|
|
282
|
+
-h, --help Show this help message
|
|
283
|
+
-v, --version Show version
|
|
284
|
+
|
|
285
|
+
Examples:
|
|
286
|
+
browsecraft test # Run all tests
|
|
287
|
+
browsecraft test tests/login.test.ts # Run specific file
|
|
288
|
+
browsecraft test --headed --browser firefox
|
|
289
|
+
browsecraft test --grep "login" --bail
|
|
290
|
+
`);
|
|
291
|
+
}
|
|
292
|
+
main().catch((err) => {
|
|
293
|
+
console.error("Fatal error:", err.message);
|
|
294
|
+
process.exit(1);
|
|
295
|
+
});
|
|
296
|
+
//# sourceMappingURL=cli.cjs.map
|
|
297
|
+
//# sourceMappingURL=cli.cjs.map
|
package/dist/cli.cjs.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/cli.ts"],"names":["resolveConfig","TestRunner","testRegistry","pathToFileURL","Browser","runTest","join","existsSync","writeFileSync","mkdirSync","resolve"],"mappings":"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","file":"cli.cjs","sourcesContent":["#!/usr/bin/env node\n// ============================================================================\n// Browsecraft - CLI\n// The command-line interface for running tests and initializing projects.\n//\n// npx browsecraft test # Run all tests\n// npx browsecraft test login.test.ts # Run specific file\n// npx browsecraft init # Scaffold a new project\n// npx browsecraft --help # Show help\n// ============================================================================\n\nimport { resolve, join } from 'node:path';\nimport { existsSync, writeFileSync, mkdirSync } from 'node:fs';\nimport { pathToFileURL } from 'node:url';\nimport { TestRunner, type RunnerOptions, type RunnableTest } from 'browsecraft-runner';\nimport { resolveConfig, type UserConfig } from './config.js';\nimport { testRegistry, runTest, runAfterAllHooks, type TestCase } from './test.js';\nimport { Browser } from './browser.js';\n\nconst VERSION = '0.1.0';\n\nasync function main() {\n\tconst args = process.argv.slice(2);\n\n\tif (args.length === 0 || args.includes('--help') || args.includes('-h')) {\n\t\tprintHelp();\n\t\treturn;\n\t}\n\n\tif (args.includes('--version') || args.includes('-v')) {\n\t\tconsole.log(`browsecraft v${VERSION}`);\n\t\treturn;\n\t}\n\n\tconst command = args[0];\n\n\tswitch (command) {\n\t\tcase 'test':\n\t\t\tawait runTests(args.slice(1));\n\t\t\tbreak;\n\t\tcase 'init':\n\t\t\tawait initProject();\n\t\t\tbreak;\n\t\tdefault:\n\t\t\t// If no command, assume it's a file path to test\n\t\t\tif (command && (command.endsWith('.ts') || command.endsWith('.js'))) {\n\t\t\t\tawait runTests(args);\n\t\t\t} else {\n\t\t\t\tconsole.error(`Unknown command: ${command}`);\n\t\t\t\tconsole.error('Run \"browsecraft --help\" for usage information.');\n\t\t\t\tprocess.exit(1);\n\t\t\t}\n\t}\n}\n\n// ---------------------------------------------------------------------------\n// Test Command\n// ---------------------------------------------------------------------------\n\nasync function runTests(args: string[]) {\n\t// Parse CLI flags\n\tconst flags = parseFlags(args);\n\tconst filePatterns = args.filter(a => !a.startsWith('--'));\n\n\t// Load config file if it exists\n\tconst userConfig = await loadConfig();\n\tconst config = resolveConfig(userConfig);\n\n\t// Apply CLI overrides\n\tif (flags.headed || flags.headless === false) {\n\t\tconfig.headless = false;\n\t}\n\tif (flags.browser) {\n\t\tconfig.browser = flags.browser as any;\n\t}\n\tif (flags.workers !== undefined) {\n\t\tconfig.workers = flags.workers;\n\t}\n\tif (flags.timeout !== undefined) {\n\t\tconfig.timeout = flags.timeout;\n\t}\n\tif (flags.retries !== undefined) {\n\t\tconfig.retries = flags.retries;\n\t}\n\tif (flags.debug) {\n\t\tconfig.debug = true;\n\t}\n\n\t// Set up runner options\n\tconst runnerOptions: RunnerOptions = {\n\t\tconfig,\n\t\tfiles: filePatterns.length > 0 ? filePatterns : undefined,\n\t\tgrep: flags.grep,\n\t\tbail: flags.bail,\n\t};\n\n\t// Run tests\n\tconst runner = new TestRunner(runnerOptions);\n\n\t// loadFile callback: imports the test file and returns registered tests\n\tconst loadFile = async (file: string): Promise<RunnableTest[]> => {\n\t\tconst startIdx = testRegistry.length;\n\n\t\t// For TypeScript files, register a TypeScript loader if available\n\t\tif (file.endsWith('.ts') || file.endsWith('.mts')) {\n\t\t\tawait ensureTypeScriptLoader();\n\t\t}\n\n\t\tconst fileUrl = pathToFileURL(file).href;\n\t\tawait import(fileUrl);\n\t\treturn testRegistry.slice(startIdx).map(tc => ({\n\t\t\ttitle: tc.title,\n\t\t\tsuitePath: tc.suitePath,\n\t\t\tskip: tc.skip,\n\t\t\tonly: tc.only,\n\t\t\toptions: tc.options,\n\t\t\tfn: tc.fn as (fixtures: unknown) => Promise<void>,\n\t\t}));\n\t};\n\n\t// Launch a shared browser for all tests\n\tlet sharedBrowser: Browser | undefined;\n\n\ttry {\n\t\tsharedBrowser = await Browser.launch({\n\t\t\tbrowser: config.browser,\n\t\t\theadless: config.headless,\n\t\t\texecutablePath: config.executablePath,\n\t\t\tdebug: config.debug,\n\t\t\ttimeout: config.timeout,\n\t\t});\n\t} catch (err) {\n\t\tconsole.error(`Failed to launch browser: ${err instanceof Error ? err.message : String(err)}`);\n\t\tprocess.exit(1);\n\t}\n\n\t// executeTest callback: runs a single test with fixture setup/teardown\n\tconst executeTest = async (test: RunnableTest) => {\n\t\treturn runTest(test as unknown as TestCase, sharedBrowser);\n\t};\n\n\ttry {\n\t\tconst exitCode = await runner.run(loadFile, executeTest);\n\t\tawait sharedBrowser.close().catch(() => {});\n\t\tprocess.exit(exitCode);\n\t} catch (err) {\n\t\tawait sharedBrowser?.close().catch(() => {});\n\t\tthrow err;\n\t}\n}\n\n// ---------------------------------------------------------------------------\n// Init Command\n// ---------------------------------------------------------------------------\n\nasync function initProject() {\n\tconst cwd = process.cwd();\n\n\tconsole.log('\\n Browsecraft - Project Setup\\n');\n\n\t// Create config file\n\tconst configPath = join(cwd, 'browsecraft.config.ts');\n\tif (!existsSync(configPath)) {\n\t\twriteFileSync(configPath, `import { defineConfig } from 'browsecraft';\n\nexport default defineConfig({\n // Browser to use: 'chrome' | 'firefox' | 'edge'\n browser: 'chrome',\n\n // Run tests in headless mode\n headless: true,\n\n // Base URL for page.goto() calls\n // baseURL: 'http://localhost:3000',\n\n // Global timeout for actions (ms)\n timeout: 30_000,\n\n // Take screenshots on failure\n screenshot: 'on-failure',\n});\n`);\n\t\tconsole.log(' Created browsecraft.config.ts');\n\t} else {\n\t\tconsole.log(' browsecraft.config.ts already exists, skipping');\n\t}\n\n\t// Create example test\n\tconst testsDir = join(cwd, 'tests');\n\tif (!existsSync(testsDir)) {\n\t\tmkdirSync(testsDir, { recursive: true });\n\t}\n\n\tconst exampleTest = join(testsDir, 'example.test.ts');\n\tif (!existsSync(exampleTest)) {\n\t\twriteFileSync(exampleTest, `import { test, expect } from 'browsecraft';\n\ntest('homepage has correct title', async ({ page }) => {\n await page.goto('https://example.com');\n await expect(page).toHaveTitle('Example Domain');\n});\n\ntest('can navigate to more info', async ({ page }) => {\n await page.goto('https://example.com');\n await page.click('More information');\n await expect(page).toHaveURL(/iana\\\\.org/);\n});\n`);\n\t\tconsole.log(' Created tests/example.test.ts');\n\t} else {\n\t\tconsole.log(' tests/example.test.ts already exists, skipping');\n\t}\n\n\t// Add .browsecraft to .gitignore\n\tconst gitignorePath = join(cwd, '.gitignore');\n\tif (existsSync(gitignorePath)) {\n\t\tconst content = await import('node:fs').then(fs => fs.readFileSync(gitignorePath, 'utf-8'));\n\t\tif (!content.includes('.browsecraft')) {\n\t\t\twriteFileSync(gitignorePath, `${content.trimEnd()}\\n\\n# Browsecraft\\n.browsecraft/\\n`);\n\t\t\tconsole.log(' Updated .gitignore');\n\t\t}\n\t}\n\n\tconsole.log('\\n Setup complete! Run your first test:\\n');\n\tconsole.log(' npx browsecraft test\\n');\n}\n\n// ---------------------------------------------------------------------------\n// Config Loading\n// ---------------------------------------------------------------------------\n\nasync function loadConfig(): Promise<UserConfig | undefined> {\n\tconst cwd = process.cwd();\n\tconst candidates = [\n\t\t'browsecraft.config.ts',\n\t\t'browsecraft.config.js',\n\t\t'browsecraft.config.mjs',\n\t\t'browsecraft.config.mts',\n\t];\n\n\tfor (const name of candidates) {\n\t\tconst configPath = resolve(cwd, name);\n\t\tif (existsSync(configPath)) {\n\t\t\ttry {\n\t\t\t\t// For .ts files, we need tsx or ts-node to be available\n\t\t\t\tconst mod = await import(configPath);\n\t\t\t\treturn mod.default ?? mod;\n\t\t\t} catch {\n\t\t\t\t// Config file exists but couldn't be loaded -- continue with defaults\n\t\t\t\tconsole.warn(`Warning: Could not load ${name}. Using defaults.`);\n\t\t\t}\n\t\t}\n\t}\n\n\treturn undefined;\n}\n\n// ---------------------------------------------------------------------------\n// TypeScript Loader\n// ---------------------------------------------------------------------------\n\nlet tsLoaderRegistered = false;\n\n/**\n * Ensure a TypeScript loader is registered so that .ts test files can be imported.\n * Tries tsx first (fastest), then ts-node, then falls back to a helpful error.\n */\nasync function ensureTypeScriptLoader(): Promise<void> {\n\tif (tsLoaderRegistered) return;\n\n\t// Check if we're already running under a TS loader (e.g., `tsx`, `ts-node`, `bun`)\n\t// In that case, .ts imports already work\n\tconst execArgs = process.execArgv.join(' ');\n\tif (\n\t\texecArgs.includes('tsx') ||\n\t\texecArgs.includes('ts-node') ||\n\t\texecArgs.includes('loader') ||\n\t\tprocess.versions.bun // Bun handles TS natively\n\t) {\n\t\ttsLoaderRegistered = true;\n\t\treturn;\n\t}\n\n\t// Try to dynamically register tsx or ts-node\n\tfor (const loader of ['tsx/esm', 'ts-node/esm']) {\n\t\ttry {\n\t\t\t// Node 20.6+ supports module.register()\n\t\t\tconst { register } = await import('node:module');\n\t\t\tif (typeof register === 'function') {\n\t\t\t\tregister(loader, pathToFileURL('./'));\n\t\t\t\ttsLoaderRegistered = true;\n\t\t\t\treturn;\n\t\t\t}\n\t\t} catch {\n\t\t\t// Loader not available, try next\n\t\t}\n\t}\n\n\t// If neither tsx nor ts-node is available, give a helpful error\n\tconsole.error(\n\t\t'\\n Error: Cannot import TypeScript test files.\\n' +\n\t\t' Install tsx (recommended) or ts-node:\\n\\n' +\n\t\t' npm install -D tsx\\n\\n' +\n\t\t' Or run browsecraft with tsx:\\n\\n' +\n\t\t' npx tsx node_modules/.bin/browsecraft test\\n',\n\t);\n\tprocess.exit(1);\n}\n\n// ---------------------------------------------------------------------------\n// Flag parsing\n// ---------------------------------------------------------------------------\n\ninterface CLIFlags {\n\theaded?: boolean;\n\theadless?: boolean;\n\tbrowser?: string;\n\tworkers?: number;\n\ttimeout?: number;\n\tretries?: number;\n\tgrep?: string;\n\tbail?: boolean;\n\tdebug?: boolean;\n}\n\nfunction parseFlags(args: string[]): CLIFlags {\n\tconst flags: CLIFlags = {};\n\n\tfor (let i = 0; i < args.length; i++) {\n\t\tconst arg = args[i]!;\n\t\tswitch (arg) {\n\t\t\tcase '--headed':\n\t\t\t\tflags.headed = true;\n\t\t\t\tbreak;\n\t\t\tcase '--headless':\n\t\t\t\tflags.headless = true;\n\t\t\t\tbreak;\n\t\t\tcase '--debug':\n\t\t\t\tflags.debug = true;\n\t\t\t\tbreak;\n\t\t\tcase '--bail':\n\t\t\t\tflags.bail = true;\n\t\t\t\tbreak;\n\t\t\tcase '--browser':\n\t\t\t\tflags.browser = args[++i];\n\t\t\t\tbreak;\n\t\t\tcase '--workers':\n\t\t\t\tflags.workers = Number.parseInt(args[++i] ?? '1', 10);\n\t\t\t\tbreak;\n\t\t\tcase '--timeout':\n\t\t\t\tflags.timeout = Number.parseInt(args[++i] ?? '30000', 10);\n\t\t\t\tbreak;\n\t\t\tcase '--retries':\n\t\t\t\tflags.retries = Number.parseInt(args[++i] ?? '0', 10);\n\t\t\t\tbreak;\n\t\t\tcase '--grep':\n\t\t\tcase '-g':\n\t\t\t\tflags.grep = args[++i];\n\t\t\t\tbreak;\n\t\t}\n\t}\n\n\treturn flags;\n}\n\n// ---------------------------------------------------------------------------\n// Help\n// ---------------------------------------------------------------------------\n\nfunction printHelp() {\n\tconsole.log(`\n browsecraft v${VERSION} -- AI-native browser testing\n\n Usage:\n browsecraft test [files...] [options]\n browsecraft init\n\n Commands:\n test Run browser tests\n init Create a new project with example config and test\n\n Options:\n --browser <name> Browser to use: chrome, firefox, edge (default: chrome)\n --headed Run in headed mode (show the browser)\n --headless Run in headless mode (default)\n --workers <n> Number of parallel workers (default: half CPU cores)\n --timeout <ms> Global timeout in milliseconds (default: 30000)\n --retries <n> Retry failed tests n times (default: 0)\n --grep <pattern> Only run tests matching pattern\n --bail Stop after first failure\n --debug Enable verbose debug logging\n -h, --help Show this help message\n -v, --version Show version\n\n Examples:\n browsecraft test # Run all tests\n browsecraft test tests/login.test.ts # Run specific file\n browsecraft test --headed --browser firefox\n browsecraft test --grep \"login\" --bail\n`);\n}\n\n// ---------------------------------------------------------------------------\n// Entry point\n// ---------------------------------------------------------------------------\n\nmain().catch((err) => {\n\tconsole.error('Fatal error:', err.message);\n\tprocess.exit(1);\n});\n"]}
|
package/dist/cli.d.cts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
package/dist/cli.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { resolveConfig, Browser, testRegistry, runTest } from './chunk-77HRTGXZ.js';
|
|
3
|
+
import { join, resolve } from 'path';
|
|
4
|
+
import { existsSync, writeFileSync, mkdirSync } from 'fs';
|
|
5
|
+
import { pathToFileURL } from 'url';
|
|
6
|
+
import { TestRunner } from 'browsecraft-runner';
|
|
7
|
+
|
|
8
|
+
var VERSION = "0.1.0";
|
|
9
|
+
async function main() {
|
|
10
|
+
const args = process.argv.slice(2);
|
|
11
|
+
if (args.length === 0 || args.includes("--help") || args.includes("-h")) {
|
|
12
|
+
printHelp();
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
if (args.includes("--version") || args.includes("-v")) {
|
|
16
|
+
console.log(`browsecraft v${VERSION}`);
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
const command = args[0];
|
|
20
|
+
switch (command) {
|
|
21
|
+
case "test":
|
|
22
|
+
await runTests(args.slice(1));
|
|
23
|
+
break;
|
|
24
|
+
case "init":
|
|
25
|
+
await initProject();
|
|
26
|
+
break;
|
|
27
|
+
default:
|
|
28
|
+
if (command && (command.endsWith(".ts") || command.endsWith(".js"))) {
|
|
29
|
+
await runTests(args);
|
|
30
|
+
} else {
|
|
31
|
+
console.error(`Unknown command: ${command}`);
|
|
32
|
+
console.error('Run "browsecraft --help" for usage information.');
|
|
33
|
+
process.exit(1);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
async function runTests(args) {
|
|
38
|
+
const flags = parseFlags(args);
|
|
39
|
+
const filePatterns = args.filter((a) => !a.startsWith("--"));
|
|
40
|
+
const userConfig = await loadConfig();
|
|
41
|
+
const config = resolveConfig(userConfig);
|
|
42
|
+
if (flags.headed || flags.headless === false) {
|
|
43
|
+
config.headless = false;
|
|
44
|
+
}
|
|
45
|
+
if (flags.browser) {
|
|
46
|
+
config.browser = flags.browser;
|
|
47
|
+
}
|
|
48
|
+
if (flags.workers !== void 0) {
|
|
49
|
+
config.workers = flags.workers;
|
|
50
|
+
}
|
|
51
|
+
if (flags.timeout !== void 0) {
|
|
52
|
+
config.timeout = flags.timeout;
|
|
53
|
+
}
|
|
54
|
+
if (flags.retries !== void 0) {
|
|
55
|
+
config.retries = flags.retries;
|
|
56
|
+
}
|
|
57
|
+
if (flags.debug) {
|
|
58
|
+
config.debug = true;
|
|
59
|
+
}
|
|
60
|
+
const runnerOptions = {
|
|
61
|
+
config,
|
|
62
|
+
files: filePatterns.length > 0 ? filePatterns : void 0,
|
|
63
|
+
grep: flags.grep,
|
|
64
|
+
bail: flags.bail
|
|
65
|
+
};
|
|
66
|
+
const runner = new TestRunner(runnerOptions);
|
|
67
|
+
const loadFile = async (file) => {
|
|
68
|
+
const startIdx = testRegistry.length;
|
|
69
|
+
if (file.endsWith(".ts") || file.endsWith(".mts")) {
|
|
70
|
+
await ensureTypeScriptLoader();
|
|
71
|
+
}
|
|
72
|
+
const fileUrl = pathToFileURL(file).href;
|
|
73
|
+
await import(fileUrl);
|
|
74
|
+
return testRegistry.slice(startIdx).map((tc) => ({
|
|
75
|
+
title: tc.title,
|
|
76
|
+
suitePath: tc.suitePath,
|
|
77
|
+
skip: tc.skip,
|
|
78
|
+
only: tc.only,
|
|
79
|
+
options: tc.options,
|
|
80
|
+
fn: tc.fn
|
|
81
|
+
}));
|
|
82
|
+
};
|
|
83
|
+
let sharedBrowser;
|
|
84
|
+
try {
|
|
85
|
+
sharedBrowser = await Browser.launch({
|
|
86
|
+
browser: config.browser,
|
|
87
|
+
headless: config.headless,
|
|
88
|
+
executablePath: config.executablePath,
|
|
89
|
+
debug: config.debug,
|
|
90
|
+
timeout: config.timeout
|
|
91
|
+
});
|
|
92
|
+
} catch (err) {
|
|
93
|
+
console.error(`Failed to launch browser: ${err instanceof Error ? err.message : String(err)}`);
|
|
94
|
+
process.exit(1);
|
|
95
|
+
}
|
|
96
|
+
const executeTest = async (test) => {
|
|
97
|
+
return runTest(test, sharedBrowser);
|
|
98
|
+
};
|
|
99
|
+
try {
|
|
100
|
+
const exitCode = await runner.run(loadFile, executeTest);
|
|
101
|
+
await sharedBrowser.close().catch(() => {
|
|
102
|
+
});
|
|
103
|
+
process.exit(exitCode);
|
|
104
|
+
} catch (err) {
|
|
105
|
+
await sharedBrowser?.close().catch(() => {
|
|
106
|
+
});
|
|
107
|
+
throw err;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
async function initProject() {
|
|
111
|
+
const cwd = process.cwd();
|
|
112
|
+
console.log("\n Browsecraft - Project Setup\n");
|
|
113
|
+
const configPath = join(cwd, "browsecraft.config.ts");
|
|
114
|
+
if (!existsSync(configPath)) {
|
|
115
|
+
writeFileSync(configPath, `import { defineConfig } from 'browsecraft';
|
|
116
|
+
|
|
117
|
+
export default defineConfig({
|
|
118
|
+
// Browser to use: 'chrome' | 'firefox' | 'edge'
|
|
119
|
+
browser: 'chrome',
|
|
120
|
+
|
|
121
|
+
// Run tests in headless mode
|
|
122
|
+
headless: true,
|
|
123
|
+
|
|
124
|
+
// Base URL for page.goto() calls
|
|
125
|
+
// baseURL: 'http://localhost:3000',
|
|
126
|
+
|
|
127
|
+
// Global timeout for actions (ms)
|
|
128
|
+
timeout: 30_000,
|
|
129
|
+
|
|
130
|
+
// Take screenshots on failure
|
|
131
|
+
screenshot: 'on-failure',
|
|
132
|
+
});
|
|
133
|
+
`);
|
|
134
|
+
console.log(" Created browsecraft.config.ts");
|
|
135
|
+
} else {
|
|
136
|
+
console.log(" browsecraft.config.ts already exists, skipping");
|
|
137
|
+
}
|
|
138
|
+
const testsDir = join(cwd, "tests");
|
|
139
|
+
if (!existsSync(testsDir)) {
|
|
140
|
+
mkdirSync(testsDir, { recursive: true });
|
|
141
|
+
}
|
|
142
|
+
const exampleTest = join(testsDir, "example.test.ts");
|
|
143
|
+
if (!existsSync(exampleTest)) {
|
|
144
|
+
writeFileSync(exampleTest, `import { test, expect } from 'browsecraft';
|
|
145
|
+
|
|
146
|
+
test('homepage has correct title', async ({ page }) => {
|
|
147
|
+
await page.goto('https://example.com');
|
|
148
|
+
await expect(page).toHaveTitle('Example Domain');
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
test('can navigate to more info', async ({ page }) => {
|
|
152
|
+
await page.goto('https://example.com');
|
|
153
|
+
await page.click('More information');
|
|
154
|
+
await expect(page).toHaveURL(/iana\\.org/);
|
|
155
|
+
});
|
|
156
|
+
`);
|
|
157
|
+
console.log(" Created tests/example.test.ts");
|
|
158
|
+
} else {
|
|
159
|
+
console.log(" tests/example.test.ts already exists, skipping");
|
|
160
|
+
}
|
|
161
|
+
const gitignorePath = join(cwd, ".gitignore");
|
|
162
|
+
if (existsSync(gitignorePath)) {
|
|
163
|
+
const content = await import('fs').then((fs) => fs.readFileSync(gitignorePath, "utf-8"));
|
|
164
|
+
if (!content.includes(".browsecraft")) {
|
|
165
|
+
writeFileSync(gitignorePath, `${content.trimEnd()}
|
|
166
|
+
|
|
167
|
+
# Browsecraft
|
|
168
|
+
.browsecraft/
|
|
169
|
+
`);
|
|
170
|
+
console.log(" Updated .gitignore");
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
console.log("\n Setup complete! Run your first test:\n");
|
|
174
|
+
console.log(" npx browsecraft test\n");
|
|
175
|
+
}
|
|
176
|
+
async function loadConfig() {
|
|
177
|
+
const cwd = process.cwd();
|
|
178
|
+
const candidates = [
|
|
179
|
+
"browsecraft.config.ts",
|
|
180
|
+
"browsecraft.config.js",
|
|
181
|
+
"browsecraft.config.mjs",
|
|
182
|
+
"browsecraft.config.mts"
|
|
183
|
+
];
|
|
184
|
+
for (const name of candidates) {
|
|
185
|
+
const configPath = resolve(cwd, name);
|
|
186
|
+
if (existsSync(configPath)) {
|
|
187
|
+
try {
|
|
188
|
+
const mod = await import(configPath);
|
|
189
|
+
return mod.default ?? mod;
|
|
190
|
+
} catch {
|
|
191
|
+
console.warn(`Warning: Could not load ${name}. Using defaults.`);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
return void 0;
|
|
196
|
+
}
|
|
197
|
+
var tsLoaderRegistered = false;
|
|
198
|
+
async function ensureTypeScriptLoader() {
|
|
199
|
+
if (tsLoaderRegistered) return;
|
|
200
|
+
const execArgs = process.execArgv.join(" ");
|
|
201
|
+
if (execArgs.includes("tsx") || execArgs.includes("ts-node") || execArgs.includes("loader") || process.versions.bun) {
|
|
202
|
+
tsLoaderRegistered = true;
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
for (const loader of ["tsx/esm", "ts-node/esm"]) {
|
|
206
|
+
try {
|
|
207
|
+
const { register } = await import('module');
|
|
208
|
+
if (typeof register === "function") {
|
|
209
|
+
register(loader, pathToFileURL("./"));
|
|
210
|
+
tsLoaderRegistered = true;
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
} catch {
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
console.error(
|
|
217
|
+
"\n Error: Cannot import TypeScript test files.\n Install tsx (recommended) or ts-node:\n\n npm install -D tsx\n\n Or run browsecraft with tsx:\n\n npx tsx node_modules/.bin/browsecraft test\n"
|
|
218
|
+
);
|
|
219
|
+
process.exit(1);
|
|
220
|
+
}
|
|
221
|
+
function parseFlags(args) {
|
|
222
|
+
const flags = {};
|
|
223
|
+
for (let i = 0; i < args.length; i++) {
|
|
224
|
+
const arg = args[i];
|
|
225
|
+
switch (arg) {
|
|
226
|
+
case "--headed":
|
|
227
|
+
flags.headed = true;
|
|
228
|
+
break;
|
|
229
|
+
case "--headless":
|
|
230
|
+
flags.headless = true;
|
|
231
|
+
break;
|
|
232
|
+
case "--debug":
|
|
233
|
+
flags.debug = true;
|
|
234
|
+
break;
|
|
235
|
+
case "--bail":
|
|
236
|
+
flags.bail = true;
|
|
237
|
+
break;
|
|
238
|
+
case "--browser":
|
|
239
|
+
flags.browser = args[++i];
|
|
240
|
+
break;
|
|
241
|
+
case "--workers":
|
|
242
|
+
flags.workers = Number.parseInt(args[++i] ?? "1", 10);
|
|
243
|
+
break;
|
|
244
|
+
case "--timeout":
|
|
245
|
+
flags.timeout = Number.parseInt(args[++i] ?? "30000", 10);
|
|
246
|
+
break;
|
|
247
|
+
case "--retries":
|
|
248
|
+
flags.retries = Number.parseInt(args[++i] ?? "0", 10);
|
|
249
|
+
break;
|
|
250
|
+
case "--grep":
|
|
251
|
+
case "-g":
|
|
252
|
+
flags.grep = args[++i];
|
|
253
|
+
break;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
return flags;
|
|
257
|
+
}
|
|
258
|
+
function printHelp() {
|
|
259
|
+
console.log(`
|
|
260
|
+
browsecraft v${VERSION} -- AI-native browser testing
|
|
261
|
+
|
|
262
|
+
Usage:
|
|
263
|
+
browsecraft test [files...] [options]
|
|
264
|
+
browsecraft init
|
|
265
|
+
|
|
266
|
+
Commands:
|
|
267
|
+
test Run browser tests
|
|
268
|
+
init Create a new project with example config and test
|
|
269
|
+
|
|
270
|
+
Options:
|
|
271
|
+
--browser <name> Browser to use: chrome, firefox, edge (default: chrome)
|
|
272
|
+
--headed Run in headed mode (show the browser)
|
|
273
|
+
--headless Run in headless mode (default)
|
|
274
|
+
--workers <n> Number of parallel workers (default: half CPU cores)
|
|
275
|
+
--timeout <ms> Global timeout in milliseconds (default: 30000)
|
|
276
|
+
--retries <n> Retry failed tests n times (default: 0)
|
|
277
|
+
--grep <pattern> Only run tests matching pattern
|
|
278
|
+
--bail Stop after first failure
|
|
279
|
+
--debug Enable verbose debug logging
|
|
280
|
+
-h, --help Show this help message
|
|
281
|
+
-v, --version Show version
|
|
282
|
+
|
|
283
|
+
Examples:
|
|
284
|
+
browsecraft test # Run all tests
|
|
285
|
+
browsecraft test tests/login.test.ts # Run specific file
|
|
286
|
+
browsecraft test --headed --browser firefox
|
|
287
|
+
browsecraft test --grep "login" --bail
|
|
288
|
+
`);
|
|
289
|
+
}
|
|
290
|
+
main().catch((err) => {
|
|
291
|
+
console.error("Fatal error:", err.message);
|
|
292
|
+
process.exit(1);
|
|
293
|
+
});
|
|
294
|
+
//# sourceMappingURL=cli.js.map
|
|
295
|
+
//# sourceMappingURL=cli.js.map
|