browsecraft-runner 0.1.2 → 0.3.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/dist/index.cjs CHANGED
@@ -1,7 +1,7 @@
1
1
  'use strict';
2
2
 
3
- var path = require('path');
4
3
  var fs = require('fs');
4
+ var path = require('path');
5
5
 
6
6
  // src/runner.ts
7
7
  var TestRunner = class {
@@ -24,9 +24,11 @@ var TestRunner = class {
24
24
  console.log(' Run "browsecraft init" to create an example test.\n');
25
25
  return 0;
26
26
  }
27
- console.log(`
27
+ console.log(
28
+ `
28
29
  Browsecraft - Running ${files.length} test file${files.length > 1 ? "s" : ""}
29
- `);
30
+ `
31
+ );
30
32
  const allResults = [];
31
33
  let bail = false;
32
34
  for (const file of files) {
@@ -68,7 +70,7 @@ var TestRunner = class {
68
70
  };
69
71
  allResults.push(errorResult);
70
72
  console.log(` ${this.getStatusIcon("failed")} ${errorResult.title}`);
71
- console.log(` ${errorResult.error.message}`);
73
+ console.log(` ${errorResult.error?.message}`);
72
74
  }
73
75
  console.log("");
74
76
  if (this.options.bail && allResults.some((r) => r.status === "failed")) {
@@ -96,7 +98,7 @@ var TestRunner = class {
96
98
  const files = [];
97
99
  const pattern = this.options.config.testMatch;
98
100
  const extMatch = pattern.match(/\.\{([^}]+)\}$/);
99
- const extensions = extMatch ? extMatch[1].split(",").map((e) => e.trim()) : ["ts", "js"];
101
+ const extensions = extMatch ? extMatch[1]?.split(",").map((e) => e.trim()) ?? ["ts", "js"] : ["ts", "js"];
100
102
  const suffixMatch = pattern.match(/\*(\.[^{*]+)\./);
101
103
  const suffix = suffixMatch ? suffixMatch[1] : ".test";
102
104
  this.walkDir(dir, files, extensions, suffix);
@@ -122,9 +124,7 @@ var TestRunner = class {
122
124
  if (stat.isDirectory()) {
123
125
  this.walkDir(fullPath, results, extensions, suffix);
124
126
  } else if (stat.isFile()) {
125
- const matchesPattern = extensions.some(
126
- (ext) => entry.endsWith(`${suffix}.${ext}`)
127
- );
127
+ const matchesPattern = extensions.some((ext) => entry.endsWith(`${suffix}.${ext}`));
128
128
  if (matchesPattern) {
129
129
  results.push(fullPath);
130
130
  }
@@ -1 +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"]}
1
+ {"version":3,"sources":["../src/runner.ts"],"names":["relative","resolve","existsSync","readdirSync","join","statSync"],"mappings":";;;;;;AAmCO,IAAM,aAAN,MAAiB;AAAA,EACf,OAAA;AAAA,EAER,YAAY,OAAA,EAAwB;AACnC,IAAA,IAAA,CAAK,OAAA,GAAU,OAAA;AAAA,EAChB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQA,MAAM,GAAA,CACL,QAAA,EACA,WAAA,EACkB;AAClB,IAAA,MAAM,SAAA,GAAY,KAAK,GAAA,EAAI;AAG3B,IAAA,MAAM,KAAA,GAAQ,KAAK,aAAA,EAAc;AACjC,IAAA,IAAI,KAAA,CAAM,WAAW,CAAA,EAAG;AACvB,MAAA,OAAA,CAAQ,IAAI,4BAA4B,CAAA;AACxC,MAAA,OAAA,CAAQ,IAAI,CAAA,gBAAA,EAAmB,IAAA,CAAK,OAAA,CAAQ,MAAA,CAAO,SAAS,CAAA,CAAE,CAAA;AAC9D,MAAA,OAAA,CAAQ,IAAI,uDAAuD,CAAA;AACnE,MAAA,OAAO,CAAA;AAAA,IACR;AAEA,IAAA,OAAA,CAAQ,GAAA;AAAA,MACP;AAAA,wBAAA,EAA6B,MAAM,MAAM,CAAA,UAAA,EAAa,MAAM,MAAA,GAAS,CAAA,GAAI,MAAM,EAAE;AAAA;AAAA,KAClF;AAGA,IAAA,MAAM,aAA2B,EAAC;AAClC,IAAA,IAAI,IAAA,GAAO,KAAA;AAEX,IAAA,KAAA,MAAW,QAAQ,KAAA,EAAO;AACzB,MAAA,IAAI,IAAA,EAAM;AAEV,MAAA,MAAM,OAAA,GAAUA,aAAA,CAAS,OAAA,CAAQ,GAAA,IAAO,IAAI,CAAA;AAC5C,MAAA,OAAA,CAAQ,GAAA,CAAI,CAAA,EAAA,EAAK,OAAO,CAAA,CAAE,CAAA;AAE1B,MAAA,IAAI;AACH,QAAA,MAAM,KAAA,GAAQ,MAAM,QAAA,CAAS,IAAI,CAAA;AAGjC,QAAA,MAAM,aAAA,GAAgB,IAAA,CAAK,OAAA,CAAQ,IAAA,GAChC,MAAM,MAAA,CAAO,CAAC,CAAA,KAAM,CAAA,CAAE,MAAM,QAAA,CAAS,IAAA,CAAK,OAAA,CAAQ,IAAK,CAAC,CAAA,GACxD,KAAA;AAGH,QAAA,MAAM,UAAU,aAAA,CAAc,IAAA,CAAK,CAAC,CAAA,KAAM,EAAE,IAAI,CAAA;AAChD,QAAA,MAAM,UAAA,GAAa,UAAU,aAAA,CAAc,MAAA,CAAO,CAAC,CAAA,KAAM,CAAA,CAAE,IAAI,CAAA,GAAI,aAAA;AAGnE,QAAA,KAAA,MAAW,QAAQ,UAAA,EAAY;AAC9B,UAAA,IAAI,MAAA,GAAS,MAAM,WAAA,CAAY,IAAI,CAAA;AAGnC,UAAA,MAAM,aAAa,IAAA,CAAK,OAAA,CAAQ,OAAA,IAAW,IAAA,CAAK,QAAQ,MAAA,CAAO,OAAA;AAC/D,UAAA,IAAI,UAAA,GAAa,CAAA;AAEjB,UAAA,OAAO,MAAA,CAAO,MAAA,KAAW,QAAA,IAAY,UAAA,GAAa,UAAA,EAAY;AAC7D,YAAA,UAAA,EAAA;AACA,YAAA,MAAA,GAAS,MAAM,YAAY,IAAI,CAAA;AAAA,UAChC;AAEA,UAAA,IAAI,aAAa,CAAA,EAAG;AACnB,YAAA,MAAA,CAAO,OAAA,GAAU,UAAA;AAAA,UAClB;AAEA,UAAA,UAAA,CAAW,KAAK,MAAM,CAAA;AAGtB,UAAA,MAAM,MAAA,GAAS,IAAA,CAAK,aAAA,CAAc,MAAA,CAAO,MAAM,CAAA;AAC/C,UAAA,MAAM,SAAA,GAAY,MAAA,CAAO,SAAA,CAAU,MAAA,GAAS,CAAA,GAAI,CAAA,EAAG,MAAA,CAAO,SAAA,CAAU,IAAA,CAAK,KAAK,CAAC,CAAA,GAAA,CAAA,GAAQ,EAAA;AACvF,UAAA,MAAM,WAAW,MAAA,CAAO,MAAA,KAAW,YAAY,CAAA,EAAA,EAAK,MAAA,CAAO,QAAQ,CAAA,GAAA,CAAA,GAAQ,EAAA;AAE3E,UAAA,OAAA,CAAQ,GAAA,CAAI,CAAA,IAAA,EAAO,MAAM,CAAA,CAAA,EAAI,SAAS,GAAG,MAAA,CAAO,KAAK,CAAA,EAAG,QAAQ,CAAA,CAAE,CAAA;AAElE,UAAA,IAAI,MAAA,CAAO,MAAA,KAAW,QAAA,IAAY,MAAA,CAAO,KAAA,EAAO;AAC/C,YAAA,OAAA,CAAQ,GAAA,CAAI,CAAA,MAAA,EAAS,MAAA,CAAO,KAAA,CAAM,OAAO,CAAA,CAAE,CAAA;AAAA,UAC5C;AAAA,QACD;AAAA,MACD,SAAS,KAAA,EAAO;AACf,QAAA,MAAM,WAAA,GAA0B;AAAA,UAC/B,KAAA,EAAO,mBAAmB,OAAO,CAAA,CAAA;AAAA,UACjC,WAAW,EAAC;AAAA,UACZ,MAAA,EAAQ,QAAA;AAAA,UACR,QAAA,EAAU,CAAA;AAAA,UACV,KAAA,EAAO,iBAAiB,KAAA,GAAQ,KAAA,GAAQ,IAAI,KAAA,CAAM,MAAA,CAAO,KAAK,CAAC;AAAA,SAChE;AACA,QAAA,UAAA,CAAW,KAAK,WAAW,CAAA;AAC3B,QAAA,OAAA,CAAQ,GAAA,CAAI,OAAO,IAAA,CAAK,aAAA,CAAc,QAAQ,CAAC,CAAA,CAAA,EAAI,WAAA,CAAY,KAAK,CAAA,CAAE,CAAA;AACtE,QAAA,OAAA,CAAQ,GAAA,CAAI,CAAA,MAAA,EAAS,WAAA,CAAY,KAAA,EAAO,OAAO,CAAA,CAAE,CAAA;AAAA,MAClD;AAEA,MAAA,OAAA,CAAQ,IAAI,EAAE,CAAA;AAGd,MAAA,IAAI,IAAA,CAAK,OAAA,CAAQ,IAAA,IAAQ,UAAA,CAAW,IAAA,CAAK,CAAC,CAAA,KAAM,CAAA,CAAE,MAAA,KAAW,QAAQ,CAAA,EAAG;AACvE,QAAA,IAAA,GAAO,IAAA;AAAA,MACR;AAAA,IACD;AAGA,IAAA,MAAM,UAAU,IAAA,CAAK,SAAA,CAAU,YAAY,IAAA,CAAK,GAAA,KAAQ,SAAS,CAAA;AACjE,IAAA,IAAA,CAAK,aAAa,OAAO,CAAA;AAEzB,IAAA,OAAO,OAAA,CAAQ,MAAA,GAAS,CAAA,GAAI,CAAA,GAAI,CAAA;AAAA,EACjC;AAAA;AAAA;AAAA;AAAA,EAMA,aAAA,GAA0B;AAEzB,IAAA,IAAI,KAAK,OAAA,CAAQ,KAAA,IAAS,KAAK,OAAA,CAAQ,KAAA,CAAM,SAAS,CAAA,EAAG;AACxD,MAAA,OAAO,KAAK,OAAA,CAAQ,KAAA,CAAM,IAAI,CAAC,CAAA,KAAMC,aAAQ,OAAA,CAAQ,GAAA,EAAI,EAAG,CAAC,CAAC,CAAA,CAAE,MAAA,CAAO,CAAC,CAAA,KAAMC,aAAA,CAAW,CAAC,CAAC,CAAA;AAAA,IAC5F;AAGA,IAAA,MAAM,GAAA,GAAM,QAAQ,GAAA,EAAI;AACxB,IAAA,OAAO,IAAA,CAAK,cAAc,GAAG,CAAA;AAAA,EAC9B;AAAA;AAAA;AAAA;AAAA,EAKQ,cAAc,GAAA,EAAuB;AAC5C,IAAA,MAAM,QAAkB,EAAC;AACzB,IAAA,MAAM,OAAA,GAAU,IAAA,CAAK,OAAA,CAAQ,MAAA,CAAO,SAAA;AAGpC,IAAA,MAAM,QAAA,GAAW,OAAA,CAAQ,KAAA,CAAM,gBAAgB,CAAA;AAC/C,IAAA,MAAM,UAAA,GAAa,WACf,QAAA,CAAS,CAAC,GAAG,KAAA,CAAM,GAAG,EAAE,GAAA,CAAI,CAAC,MAAM,CAAA,CAAE,IAAA,EAAM,CAAA,IAAK,CAAC,MAAM,IAAI,CAAA,GAC5D,CAAC,IAAA,EAAM,IAAI,CAAA;AAGd,IAAA,MAAM,WAAA,GAAc,OAAA,CAAQ,KAAA,CAAM,gBAAgB,CAAA;AAClD,IAAA,MAAM,MAAA,GAAS,WAAA,GAAc,WAAA,CAAY,CAAC,CAAA,GAAK,OAAA;AAE/C,IAAA,IAAA,CAAK,OAAA,CAAQ,GAAA,EAAK,KAAA,EAAO,UAAA,EAAY,MAAM,CAAA;AAC3C,IAAA,OAAO,MAAM,IAAA,EAAK;AAAA,EACnB;AAAA,EAEQ,OAAA,CAAQ,GAAA,EAAa,OAAA,EAAmB,UAAA,EAAsB,MAAA,EAAsB;AAC3F,IAAA,MAAM,IAAA,mBAAO,IAAI,GAAA,CAAI,CAAC,cAAA,EAAgB,QAAQ,cAAA,EAAgB,MAAA,EAAQ,UAAA,EAAY,QAAQ,CAAC,CAAA;AAE3F,IAAA,IAAI,OAAA;AACJ,IAAA,IAAI;AACH,MAAA,OAAA,GAAUC,eAAY,GAAG,CAAA;AAAA,IAC1B,CAAA,CAAA,MAAQ;AACP,MAAA;AAAA,IACD;AAEA,IAAA,KAAA,MAAW,SAAS,OAAA,EAAS;AAC5B,MAAA,IAAI,IAAA,CAAK,GAAA,CAAI,KAAK,CAAA,EAAG;AAErB,MAAA,MAAM,QAAA,GAAWC,SAAA,CAAK,GAAA,EAAK,KAAK,CAAA;AAChC,MAAA,IAAI,IAAA;AAEJ,MAAA,IAAI;AACH,QAAA,IAAA,GAAOC,YAAS,QAAQ,CAAA;AAAA,MACzB,CAAA,CAAA,MAAQ;AACP,QAAA;AAAA,MACD;AAEA,MAAA,IAAI,IAAA,CAAK,aAAY,EAAG;AACvB,QAAA,IAAA,CAAK,OAAA,CAAQ,QAAA,EAAU,OAAA,EAAS,UAAA,EAAY,MAAM,CAAA;AAAA,MACnD,CAAA,MAAA,IAAW,IAAA,CAAK,MAAA,EAAO,EAAG;AAEzB,QAAA,MAAM,cAAA,GAAiB,UAAA,CAAW,IAAA,CAAK,CAAC,GAAA,KAAQ,KAAA,CAAM,QAAA,CAAS,CAAA,EAAG,MAAM,CAAA,CAAA,EAAI,GAAG,CAAA,CAAE,CAAC,CAAA;AAClF,QAAA,IAAI,cAAA,EAAgB;AACnB,UAAA,OAAA,CAAQ,KAAK,QAAQ,CAAA;AAAA,QACtB;AAAA,MACD;AAAA,IACD;AAAA,EACD;AAAA;AAAA;AAAA;AAAA,EAMQ,cAAc,MAAA,EAAwB;AAC7C,IAAA,QAAQ,MAAA;AAAQ,MACf,KAAK,QAAA;AACJ,QAAA,OAAO,kBAAA;AAAA,MACR,KAAK,QAAA;AACJ,QAAA,OAAO,kBAAA;AAAA,MACR,KAAK,SAAA;AACJ,QAAA,OAAO,kBAAA;AAAA,MACR;AACC,QAAA,OAAO,GAAA;AAAA;AACT,EACD;AAAA,EAEQ,SAAA,CAAU,SAAuB,aAAA,EAAmC;AAC3E,IAAA,OAAO;AAAA,MACN,OAAO,OAAA,CAAQ,MAAA;AAAA,MACf,MAAA,EAAQ,QAAQ,MAAA,CAAO,CAAC,MAAM,CAAA,CAAE,MAAA,KAAW,QAAQ,CAAA,CAAE,MAAA;AAAA,MACrD,MAAA,EAAQ,QAAQ,MAAA,CAAO,CAAC,MAAM,CAAA,CAAE,MAAA,KAAW,QAAQ,CAAA,CAAE,MAAA;AAAA,MACrD,OAAA,EAAS,QAAQ,MAAA,CAAO,CAAC,MAAM,CAAA,CAAE,MAAA,KAAW,SAAS,CAAA,CAAE,MAAA;AAAA,MACvD,QAAA,EAAU,aAAA;AAAA,MACV;AAAA,KACD;AAAA,EACD;AAAA,EAEQ,aAAa,OAAA,EAA2B;AAC/C,IAAA,MAAM,EAAE,KAAA,EAAO,MAAA,EAAQ,MAAA,EAAQ,OAAA,EAAS,UAAS,GAAI,OAAA;AAErD,IAAA,OAAA,CAAQ,IAAI,kOAAyC,CAAA;AAErD,IAAA,MAAM,QAAkB,EAAC;AACzB,IAAA,IAAI,SAAS,CAAA,EAAG,KAAA,CAAM,IAAA,CAAK,CAAA,QAAA,EAAW,MAAM,CAAA,cAAA,CAAgB,CAAA;AAC5D,IAAA,IAAI,SAAS,CAAA,EAAG,KAAA,CAAM,IAAA,CAAK,CAAA,QAAA,EAAW,MAAM,CAAA,cAAA,CAAgB,CAAA;AAC5D,IAAA,IAAI,UAAU,CAAA,EAAG,KAAA,CAAM,IAAA,CAAK,CAAA,QAAA,EAAW,OAAO,CAAA,eAAA,CAAiB,CAAA;AAE/D,IAAA,OAAA,CAAQ,GAAA,CAAI,YAAY,KAAA,CAAM,IAAA,CAAK,IAAI,CAAC,CAAA,EAAA,EAAK,KAAK,CAAA,OAAA,CAAS,CAAA;AAC3D,IAAA,OAAA,CAAQ,IAAI,CAAA,SAAA,EAAY,IAAA,CAAK,cAAA,CAAe,QAAQ,CAAC,CAAA,CAAE,CAAA;AACvD,IAAA,OAAA,CAAQ,IAAI,EAAE,CAAA;AAEd,IAAA,IAAI,SAAS,CAAA,EAAG;AACf,MAAA,OAAA,CAAQ,IAAI,uCAAuC,CAAA;AAAA,IACpD,CAAA,MAAA,IAAW,UAAU,CAAA,EAAG;AACvB,MAAA,OAAA,CAAQ,IAAI,wBAAwB,CAAA;AAAA,IACrC,CAAA,MAAO;AACN,MAAA,OAAA,CAAQ,IAAI,sCAAsC,CAAA;AAAA,IACnD;AAAA,EACD;AAAA,EAEQ,eAAe,EAAA,EAAoB;AAC1C,IAAA,IAAI,EAAA,GAAK,GAAA,EAAM,OAAO,CAAA,EAAG,EAAE,CAAA,EAAA,CAAA;AAC3B,IAAA,IAAI,EAAA,GAAK,KAAQ,OAAO,CAAA,EAAA,CAAI,KAAK,GAAA,EAAM,OAAA,CAAQ,CAAC,CAAC,CAAA,CAAA,CAAA;AACjD,IAAA,MAAM,OAAA,GAAU,IAAA,CAAK,KAAA,CAAM,EAAA,GAAK,GAAM,CAAA;AACtC,IAAA,MAAM,OAAA,GAAA,CAAY,EAAA,GAAK,GAAA,GAAU,GAAA,EAAM,QAAQ,CAAC,CAAA;AAChD,IAAA,OAAO,CAAA,EAAG,OAAO,CAAA,EAAA,EAAK,OAAO,CAAA,CAAA,CAAA;AAAA,EAC9B;AACD","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 { existsSync, readdirSync, statSync } from 'node:fs';\nimport { join, relative, resolve } from 'node:path';\nimport { pathToFileURL } from 'node:url';\nimport type { RunSummary, RunnerOptions, TestResult } 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\t\t\t`\\n Browsecraft - Running ${files.length} test file${files.length > 1 ? 's' : ''}\\n`,\n\t\t);\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 ? filteredTests.filter((t) => t.only) : 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 ? `${result.suitePath.join(' > ')} > ` : '';\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()) ?? ['ts', 'js'])\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) => entry.endsWith(`${suffix}.${ext}`));\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':\n\t\t\t\treturn '\\x1b[32m+\\x1b[0m';\n\t\t\tcase 'failed':\n\t\t\t\treturn '\\x1b[31mx\\x1b[0m';\n\t\t\tcase 'skipped':\n\t\t\t\treturn '\\x1b[33m-\\x1b[0m';\n\t\t\tdefault:\n\t\t\t\treturn ' ';\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.js CHANGED
@@ -1,5 +1,5 @@
1
- import { relative, resolve, join } from 'path';
2
1
  import { existsSync, readdirSync, statSync } from 'fs';
2
+ import { relative, resolve, join } from 'path';
3
3
 
4
4
  // src/runner.ts
5
5
  var TestRunner = class {
@@ -22,9 +22,11 @@ var TestRunner = class {
22
22
  console.log(' Run "browsecraft init" to create an example test.\n');
23
23
  return 0;
24
24
  }
25
- console.log(`
25
+ console.log(
26
+ `
26
27
  Browsecraft - Running ${files.length} test file${files.length > 1 ? "s" : ""}
27
- `);
28
+ `
29
+ );
28
30
  const allResults = [];
29
31
  let bail = false;
30
32
  for (const file of files) {
@@ -66,7 +68,7 @@ var TestRunner = class {
66
68
  };
67
69
  allResults.push(errorResult);
68
70
  console.log(` ${this.getStatusIcon("failed")} ${errorResult.title}`);
69
- console.log(` ${errorResult.error.message}`);
71
+ console.log(` ${errorResult.error?.message}`);
70
72
  }
71
73
  console.log("");
72
74
  if (this.options.bail && allResults.some((r) => r.status === "failed")) {
@@ -94,7 +96,7 @@ var TestRunner = class {
94
96
  const files = [];
95
97
  const pattern = this.options.config.testMatch;
96
98
  const extMatch = pattern.match(/\.\{([^}]+)\}$/);
97
- const extensions = extMatch ? extMatch[1].split(",").map((e) => e.trim()) : ["ts", "js"];
99
+ const extensions = extMatch ? extMatch[1]?.split(",").map((e) => e.trim()) ?? ["ts", "js"] : ["ts", "js"];
98
100
  const suffixMatch = pattern.match(/\*(\.[^{*]+)\./);
99
101
  const suffix = suffixMatch ? suffixMatch[1] : ".test";
100
102
  this.walkDir(dir, files, extensions, suffix);
@@ -120,9 +122,7 @@ var TestRunner = class {
120
122
  if (stat.isDirectory()) {
121
123
  this.walkDir(fullPath, results, extensions, suffix);
122
124
  } else if (stat.isFile()) {
123
- const matchesPattern = extensions.some(
124
- (ext) => entry.endsWith(`${suffix}.${ext}`)
125
- );
125
+ const matchesPattern = extensions.some((ext) => entry.endsWith(`${suffix}.${ext}`));
126
126
  if (matchesPattern) {
127
127
  results.push(fullPath);
128
128
  }
package/dist/index.js.map CHANGED
@@ -1 +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"]}
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 { existsSync, readdirSync, statSync } from 'node:fs';\nimport { join, relative, resolve } from 'node:path';\nimport { pathToFileURL } from 'node:url';\nimport type { RunSummary, RunnerOptions, TestResult } 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\t\t\t`\\n Browsecraft - Running ${files.length} test file${files.length > 1 ? 's' : ''}\\n`,\n\t\t);\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 ? filteredTests.filter((t) => t.only) : 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 ? `${result.suitePath.join(' > ')} > ` : '';\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()) ?? ['ts', 'js'])\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) => entry.endsWith(`${suffix}.${ext}`));\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':\n\t\t\t\treturn '\\x1b[32m+\\x1b[0m';\n\t\t\tcase 'failed':\n\t\t\t\treturn '\\x1b[31mx\\x1b[0m';\n\t\t\tcase 'skipped':\n\t\t\t\treturn '\\x1b[33m-\\x1b[0m';\n\t\t\tdefault:\n\t\t\t\treturn ' ';\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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "browsecraft-runner",
3
- "version": "0.1.2",
3
+ "version": "0.3.0",
4
4
  "description": "Test runner and CLI for Browsecraft",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",
@@ -22,13 +22,13 @@
22
22
  "access": "public"
23
23
  },
24
24
  "dependencies": {
25
- "browsecraft-bidi": "0.1.2"
25
+ "browsecraft-bidi": "0.3.0"
26
26
  },
27
27
  "devDependencies": {
28
- "@types/node": "^22.10.0",
28
+ "@types/node": "^25.3.0",
29
29
  "tsup": "^8.3.5",
30
30
  "typescript": "^5.7.2",
31
- "vitest": "^2.1.0",
31
+ "vitest": "^4.0.18",
32
32
  "rimraf": "^6.0.1"
33
33
  },
34
34
  "license": "MIT",