@questi0nm4rk/feats 1.0.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/README.md +101 -0
- package/dist/feats.js +27862 -0
- package/dist/plugin/bun-plugin.js +12918 -0
- package/dist/src/assertions/config-assertions.d.ts +5 -0
- package/dist/src/assertions/output-assertions.d.ts +9 -0
- package/dist/src/cli/cli-result.d.ts +6 -0
- package/dist/src/cli/cli-runner.d.ts +8 -0
- package/dist/src/feats.d.ts +16 -0
- package/dist/src/fixtures/fixture-manager.d.ts +6 -0
- package/dist/src/fixtures/fixture-project.d.ts +12 -0
- package/dist/src/parser/adapter.d.ts +5 -0
- package/dist/src/parser/models.d.ts +35 -0
- package/dist/src/plugin/bun-plugin.d.ts +3 -0
- package/dist/src/random/seeded-rng.d.ts +8 -0
- package/dist/src/registry/expression-adapter.d.ts +6 -0
- package/dist/src/registry/parameter-types.d.ts +8 -0
- package/dist/src/registry/step-definition.d.ts +7 -0
- package/dist/src/registry/step-registry.d.ts +14 -0
- package/dist/src/reporting/error-formatter.d.ts +2 -0
- package/dist/src/reporting/pending-steps.d.ts +2 -0
- package/dist/src/runner/feature-runner.d.ts +9 -0
- package/dist/src/runner/hook-runner.d.ts +12 -0
- package/dist/src/runner/scenario-runner.d.ts +3 -0
- package/dist/src/runner/tag-filter.d.ts +2 -0
- package/dist/src/state/world.d.ts +4 -0
- package/dist/tests/assertions/config-assertions.test.d.ts +1 -0
- package/dist/tests/assertions/output-assertions.test.d.ts +1 -0
- package/dist/tests/cli/cli-runner.test.d.ts +1 -0
- package/dist/tests/features/self-test.steps.d.ts +1 -0
- package/dist/tests/features/self-test.test.d.ts +1 -0
- package/dist/tests/fixtures/sample-project/src/main.d.ts +1 -0
- package/dist/tests/fixtures-manager/fixture-manager.test.d.ts +1 -0
- package/dist/tests/parser/adapter.test.d.ts +1 -0
- package/dist/tests/parser/models.test.d.ts +1 -0
- package/dist/tests/random/seeded-rng.test.d.ts +1 -0
- package/dist/tests/registry/expression-adapter.test.d.ts +1 -0
- package/dist/tests/registry/parameter-types.test.d.ts +1 -0
- package/dist/tests/registry/step-registry.test.d.ts +1 -0
- package/dist/tests/reporting/error-formatter.test.d.ts +1 -0
- package/dist/tests/reporting/pending-steps.test.d.ts +1 -0
- package/dist/tests/runner/feature-runner.test.d.ts +1 -0
- package/dist/tests/runner/hook-runner.test.d.ts +1 -0
- package/dist/tests/runner/scenario-runner.test.d.ts +1 -0
- package/package.json +43 -0
- package/src/assertions/config-assertions.ts +123 -0
- package/src/assertions/output-assertions.ts +67 -0
- package/src/cli/cli-result.ts +6 -0
- package/src/cli/cli-runner.ts +66 -0
- package/src/feats.ts +30 -0
- package/src/fixtures/fixture-manager.ts +34 -0
- package/src/fixtures/fixture-project.ts +69 -0
- package/src/parser/adapter.ts +214 -0
- package/src/parser/models.ts +70 -0
- package/src/plugin/bun-plugin.ts +18 -0
- package/src/random/seeded-rng.ts +94 -0
- package/src/registry/expression-adapter.ts +34 -0
- package/src/registry/parameter-types.ts +24 -0
- package/src/registry/step-definition.ts +12 -0
- package/src/registry/step-registry.ts +63 -0
- package/src/reporting/error-formatter.ts +17 -0
- package/src/reporting/pending-steps.ts +16 -0
- package/src/runner/feature-runner.ts +86 -0
- package/src/runner/hook-runner.ts +54 -0
- package/src/runner/scenario-runner.ts +13 -0
- package/src/runner/tag-filter.ts +109 -0
- package/src/state/world.ts +5 -0
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { CLIResult } from "@/cli/cli-result";
|
|
2
|
+
export interface OutputExpectations {
|
|
3
|
+
readonly stdout?: string | RegExp;
|
|
4
|
+
readonly stderr?: string | RegExp;
|
|
5
|
+
readonly exitCode?: number;
|
|
6
|
+
readonly contains?: string;
|
|
7
|
+
readonly notContains?: string;
|
|
8
|
+
}
|
|
9
|
+
export declare function assertOutput(result: CLIResult, expectations: OutputExpectations): void;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { CLIResult } from "@/cli/cli-result";
|
|
2
|
+
export interface RunCliOpts {
|
|
3
|
+
readonly cwd?: string;
|
|
4
|
+
readonly timeout?: number;
|
|
5
|
+
readonly env?: Record<string, string>;
|
|
6
|
+
readonly stdin?: string;
|
|
7
|
+
}
|
|
8
|
+
export declare function runCli(command: string, args?: string[], opts?: RunCliOpts): Promise<CLIResult>;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export { assertConfig } from "./assertions/config-assertions";
|
|
2
|
+
export { assertOutput } from "./assertions/output-assertions";
|
|
3
|
+
export type { CLIResult } from "./cli/cli-result";
|
|
4
|
+
export { runCli } from "./cli/cli-runner";
|
|
5
|
+
export { composeFixtures, setupFixture } from "./fixtures/fixture-manager";
|
|
6
|
+
export type { FixtureProject } from "./fixtures/fixture-project";
|
|
7
|
+
export { loadFeatures, parseFeature } from "./parser/adapter";
|
|
8
|
+
export type { DataTable, Feature, ParsedStep, Scenario, Tag } from "./parser/models";
|
|
9
|
+
export type { SeededRng } from "./random/seeded-rng";
|
|
10
|
+
export { createRng } from "./random/seeded-rng";
|
|
11
|
+
export { defineParameterType } from "./registry/parameter-types";
|
|
12
|
+
export { Given, Step, Then, When } from "./registry/step-registry";
|
|
13
|
+
export type { RunOptions } from "./runner/feature-runner";
|
|
14
|
+
export { runFeatures } from "./runner/feature-runner";
|
|
15
|
+
export { After, Before } from "./runner/hook-runner";
|
|
16
|
+
export type { World, WorldFactory } from "./state/world";
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { FixtureProject } from "@/fixtures/fixture-project";
|
|
2
|
+
export interface FixtureOpts {
|
|
3
|
+
readonly fixtureDir: string;
|
|
4
|
+
}
|
|
5
|
+
export declare function setupFixture(name: string, opts: FixtureOpts): Promise<FixtureProject>;
|
|
6
|
+
export declare function composeFixtures(names: string[], opts: FixtureOpts): Promise<FixtureProject>;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { CLIResult } from "@/cli/cli-result";
|
|
2
|
+
export interface FixtureProject {
|
|
3
|
+
readonly dir: string;
|
|
4
|
+
run(command: string, args?: string[]): Promise<CLIResult>;
|
|
5
|
+
hasFile(path: string): boolean;
|
|
6
|
+
readFile(path: string): Promise<string>;
|
|
7
|
+
readJson(path: string): Promise<unknown>;
|
|
8
|
+
readToml(path: string): Promise<unknown>;
|
|
9
|
+
listFiles(glob?: string): Promise<string[]>;
|
|
10
|
+
cleanup(): Promise<void>;
|
|
11
|
+
}
|
|
12
|
+
export declare function createFixtureProject(dir: string): FixtureProject;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
export interface Tag {
|
|
2
|
+
readonly name: string;
|
|
3
|
+
}
|
|
4
|
+
export interface StepLocation {
|
|
5
|
+
readonly uri: string;
|
|
6
|
+
readonly line: number;
|
|
7
|
+
}
|
|
8
|
+
export interface DataTable {
|
|
9
|
+
readonly rows: readonly (readonly string[])[];
|
|
10
|
+
asObjects(): Record<string, string>[];
|
|
11
|
+
asLists(): string[][];
|
|
12
|
+
}
|
|
13
|
+
export interface ParsedStep {
|
|
14
|
+
readonly keyword: "Given" | "When" | "Then" | "And" | "But";
|
|
15
|
+
readonly text: string;
|
|
16
|
+
readonly dataTable: DataTable | undefined;
|
|
17
|
+
readonly docString: string | undefined;
|
|
18
|
+
readonly location: StepLocation;
|
|
19
|
+
}
|
|
20
|
+
export interface Scenario {
|
|
21
|
+
readonly name: string;
|
|
22
|
+
readonly tags: readonly Tag[];
|
|
23
|
+
readonly steps: readonly ParsedStep[];
|
|
24
|
+
}
|
|
25
|
+
export interface Feature {
|
|
26
|
+
readonly name: string;
|
|
27
|
+
readonly description: string;
|
|
28
|
+
readonly tags: readonly Tag[];
|
|
29
|
+
readonly background: {
|
|
30
|
+
readonly steps: readonly ParsedStep[];
|
|
31
|
+
} | undefined;
|
|
32
|
+
readonly scenarios: readonly Scenario[];
|
|
33
|
+
readonly uri: string;
|
|
34
|
+
}
|
|
35
|
+
export declare function createDataTable(rows: readonly (readonly string[])[]): DataTable;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export interface SeededRng {
|
|
2
|
+
readonly seed: number;
|
|
3
|
+
nextInt(min: number, max: number): number;
|
|
4
|
+
pick<T>(items: readonly T[]): T;
|
|
5
|
+
shuffle<T>(items: readonly T[]): T[];
|
|
6
|
+
sample<T>(items: readonly T[], count: number): T[];
|
|
7
|
+
}
|
|
8
|
+
export declare function createRng(seed?: number): SeededRng;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { StepDefinition } from "@/registry/step-definition";
|
|
2
|
+
export interface MatchResult {
|
|
3
|
+
readonly definition: StepDefinition;
|
|
4
|
+
readonly args: readonly unknown[];
|
|
5
|
+
}
|
|
6
|
+
export declare function matchStep(definitions: readonly StepDefinition[], stepText: string): MatchResult;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { ParameterTypeRegistry } from "@cucumber/cucumber-expressions";
|
|
2
|
+
export declare function getParameterTypeRegistry(): ParameterTypeRegistry;
|
|
3
|
+
export declare function clearParameterTypeRegistry(): void;
|
|
4
|
+
export declare function defineParameterType(opts: {
|
|
5
|
+
name: string;
|
|
6
|
+
regexp: RegExp;
|
|
7
|
+
transformer: (value: string) => unknown;
|
|
8
|
+
}): void;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { World } from "@/state/world";
|
|
2
|
+
export type StepCallback<W extends World = World> = (world: W, ...args: unknown[]) => Promise<void> | void;
|
|
3
|
+
export interface StepDefinition<W extends World = World> {
|
|
4
|
+
readonly keyword: "Given" | "When" | "Then" | "Step";
|
|
5
|
+
readonly pattern: string;
|
|
6
|
+
readonly callback: StepCallback<W>;
|
|
7
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { StepCallback, StepDefinition } from "@/registry/step-definition";
|
|
2
|
+
import type { World } from "@/state/world";
|
|
3
|
+
export declare class StepRegistry {
|
|
4
|
+
private readonly definitions;
|
|
5
|
+
add(definition: StepDefinition): void;
|
|
6
|
+
getAll(): readonly StepDefinition[];
|
|
7
|
+
clear(): void;
|
|
8
|
+
}
|
|
9
|
+
export declare function getRegistry(): StepRegistry;
|
|
10
|
+
export declare function clearRegistry(): void;
|
|
11
|
+
export declare function Given<W extends World = World>(pattern: string, callback: StepCallback<W>): void;
|
|
12
|
+
export declare function When<W extends World = World>(pattern: string, callback: StepCallback<W>): void;
|
|
13
|
+
export declare function Then<W extends World = World>(pattern: string, callback: StepCallback<W>): void;
|
|
14
|
+
export declare function Step<W extends World = World>(pattern: string, callback: StepCallback<W>): void;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { Feature } from "@/parser/models";
|
|
2
|
+
import type { HookDefinition } from "@/runner/hook-runner";
|
|
3
|
+
import type { WorldFactory } from "@/state/world";
|
|
4
|
+
export interface RunOptions {
|
|
5
|
+
worldFactory?: WorldFactory;
|
|
6
|
+
tagFilter?: string;
|
|
7
|
+
}
|
|
8
|
+
export declare function runFeatures(features: readonly Feature[], opts?: RunOptions): void;
|
|
9
|
+
export type { HookDefinition };
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { World } from "@/state/world";
|
|
2
|
+
export interface HookDefinition {
|
|
3
|
+
readonly tagFilter: string | undefined;
|
|
4
|
+
readonly callback: (world: World) => Promise<void> | void;
|
|
5
|
+
}
|
|
6
|
+
export declare function Before(callback: (world: World) => Promise<void> | void): void;
|
|
7
|
+
export declare function Before(tagFilter: string, callback: (world: World) => Promise<void> | void): void;
|
|
8
|
+
export declare function After(callback: (world: World) => Promise<void> | void): void;
|
|
9
|
+
export declare function After(tagFilter: string, callback: (world: World) => Promise<void> | void): void;
|
|
10
|
+
export declare function getBeforeHooks(): HookDefinition[];
|
|
11
|
+
export declare function getAfterHooks(): HookDefinition[];
|
|
12
|
+
export declare function clearHooks(): void;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import "./self-test.steps";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function greet(name: string): string;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/package.json
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@questi0nm4rk/feats",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "BDD/Gherkin test framework for Bun — feature testing with typed step definitions",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"exports": {
|
|
7
|
+
".": {
|
|
8
|
+
"bun": "./src/feats.ts",
|
|
9
|
+
"import": "./dist/feats.js",
|
|
10
|
+
"types": "./dist/src/feats.d.ts"
|
|
11
|
+
},
|
|
12
|
+
"./plugin": {
|
|
13
|
+
"bun": "./src/plugin/bun-plugin.ts",
|
|
14
|
+
"import": "./dist/plugin/bun-plugin.js",
|
|
15
|
+
"types": "./dist/src/plugin/bun-plugin.d.ts"
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
"files": [
|
|
19
|
+
"dist/",
|
|
20
|
+
"src/"
|
|
21
|
+
],
|
|
22
|
+
"scripts": {
|
|
23
|
+
"test": "bun test",
|
|
24
|
+
"test:watch": "bun test --watch",
|
|
25
|
+
"typecheck": "tsc --noEmit",
|
|
26
|
+
"lint": "biome check src/ tests/",
|
|
27
|
+
"lint:fix": "biome check --write src/ tests/",
|
|
28
|
+
"build": "bun build src/feats.ts src/plugin/bun-plugin.ts --outdir dist --target bun",
|
|
29
|
+
"build:types": "tsc --emitDeclarationOnly --outDir dist"
|
|
30
|
+
},
|
|
31
|
+
"dependencies": {
|
|
32
|
+
"@cucumber/cucumber-expressions": "^19.0.0",
|
|
33
|
+
"@cucumber/gherkin": "^37.0.0",
|
|
34
|
+
"@cucumber/messages": "^32.0.0",
|
|
35
|
+
"smol-toml": "^1.5.0",
|
|
36
|
+
"yaml": "^2.7.0"
|
|
37
|
+
},
|
|
38
|
+
"devDependencies": {
|
|
39
|
+
"@biomejs/biome": "^2.4.0",
|
|
40
|
+
"@types/bun": "^1.3.0",
|
|
41
|
+
"typescript": "^5.8.0"
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { extname } from "node:path";
|
|
3
|
+
import { parse as parseToml } from "smol-toml";
|
|
4
|
+
import { parse as parseYaml } from "yaml";
|
|
5
|
+
|
|
6
|
+
export interface AssertConfigOpts {
|
|
7
|
+
readonly format?: "json" | "toml" | "yaml";
|
|
8
|
+
readonly subset?: boolean;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
type ConfigFormat = "json" | "toml" | "yaml";
|
|
12
|
+
|
|
13
|
+
function detectFormat(filePath: string): ConfigFormat {
|
|
14
|
+
const ext = extname(filePath).toLowerCase();
|
|
15
|
+
if (ext === ".json") return "json";
|
|
16
|
+
if (ext === ".toml") return "toml";
|
|
17
|
+
if (ext === ".yaml" || ext === ".yml") return "yaml";
|
|
18
|
+
throw new Error(`Cannot auto-detect config format from extension: ${ext}`);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function parseConfig(content: string, format: ConfigFormat): unknown {
|
|
22
|
+
if (format === "json") return JSON.parse(content) as unknown;
|
|
23
|
+
if (format === "toml") return parseToml(content);
|
|
24
|
+
return parseYaml(content) as unknown;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
28
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function diffValues(expected: unknown, actual: unknown, path: string, subset: boolean): string[] {
|
|
32
|
+
if (isRecord(expected) && isRecord(actual)) {
|
|
33
|
+
return diffObjects(expected, actual, path, subset);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (Array.isArray(expected) && Array.isArray(actual)) {
|
|
37
|
+
return diffArrays(expected, actual, path, subset);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const expectedStr = JSON.stringify(expected);
|
|
41
|
+
const actualStr = JSON.stringify(actual);
|
|
42
|
+
if (actualStr !== expectedStr) {
|
|
43
|
+
return [`${path}: expected ${expectedStr}, got ${actualStr}`];
|
|
44
|
+
}
|
|
45
|
+
return [];
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function diffArrays(
|
|
49
|
+
expected: unknown[],
|
|
50
|
+
actual: unknown[],
|
|
51
|
+
path: string,
|
|
52
|
+
subset: boolean,
|
|
53
|
+
): string[] {
|
|
54
|
+
const errors: string[] = [];
|
|
55
|
+
|
|
56
|
+
if (!subset && expected.length !== actual.length) {
|
|
57
|
+
errors.push(`${path}: expected array length ${expected.length}, got ${actual.length}`);
|
|
58
|
+
return errors;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
for (let i = 0; i < expected.length; i++) {
|
|
62
|
+
if (i >= actual.length) {
|
|
63
|
+
errors.push(`${path}[${i}]: expected ${JSON.stringify(expected[i])}, got undefined`);
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
errors.push(...diffValues(expected[i], actual[i], `${path}[${i}]`, subset));
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return errors;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function diffObjects(
|
|
73
|
+
expected: Record<string, unknown>,
|
|
74
|
+
actual: unknown,
|
|
75
|
+
path: string,
|
|
76
|
+
subset: boolean,
|
|
77
|
+
): string[] {
|
|
78
|
+
const errors: string[] = [];
|
|
79
|
+
|
|
80
|
+
if (!isRecord(actual)) {
|
|
81
|
+
errors.push(`${path}: expected object, got ${typeof actual}`);
|
|
82
|
+
return errors;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
for (const [key, expectedVal] of Object.entries(expected)) {
|
|
86
|
+
const fullPath = path !== "" ? `${path}.${key}` : key;
|
|
87
|
+
const actualVal = actual[key];
|
|
88
|
+
|
|
89
|
+
if (actualVal === undefined) {
|
|
90
|
+
errors.push(`${fullPath}: expected ${JSON.stringify(expectedVal)}, got undefined`);
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
errors.push(...diffValues(expectedVal, actualVal, fullPath, subset));
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (!subset) {
|
|
98
|
+
for (const key of Object.keys(actual)) {
|
|
99
|
+
if (!(key in expected)) {
|
|
100
|
+
const fullPath = path !== "" ? `${path}.${key}` : key;
|
|
101
|
+
errors.push(`${fullPath}: unexpected key in actual`);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return errors;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export function assertConfig(
|
|
110
|
+
filePath: string,
|
|
111
|
+
expected: Record<string, unknown>,
|
|
112
|
+
opts?: AssertConfigOpts,
|
|
113
|
+
): void {
|
|
114
|
+
const subset = opts?.subset ?? true;
|
|
115
|
+
const format = opts?.format ?? detectFormat(filePath);
|
|
116
|
+
const content = readFileSync(filePath, "utf-8");
|
|
117
|
+
const actual = parseConfig(content, format);
|
|
118
|
+
|
|
119
|
+
const errors = diffObjects(expected, actual, "", subset);
|
|
120
|
+
if (errors.length > 0) {
|
|
121
|
+
throw new Error(`Config assertion failed:\n ${errors.join("\n ")}`);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import type { CLIResult } from "@/cli/cli-result";
|
|
2
|
+
|
|
3
|
+
export interface OutputExpectations {
|
|
4
|
+
readonly stdout?: string | RegExp;
|
|
5
|
+
readonly stderr?: string | RegExp;
|
|
6
|
+
readonly exitCode?: number;
|
|
7
|
+
readonly contains?: string;
|
|
8
|
+
readonly notContains?: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function matchesExpectation(actual: string, expected: string | RegExp): boolean {
|
|
12
|
+
if (typeof expected === "string") {
|
|
13
|
+
return actual.includes(expected);
|
|
14
|
+
}
|
|
15
|
+
return expected.test(actual);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function serializeExpected(expected: string | RegExp): string {
|
|
19
|
+
if (typeof expected === "string") {
|
|
20
|
+
return JSON.stringify(expected);
|
|
21
|
+
}
|
|
22
|
+
return expected.toString();
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function assertOutput(result: CLIResult, expectations: OutputExpectations): void {
|
|
26
|
+
if (expectations.stdout !== undefined) {
|
|
27
|
+
if (!matchesExpectation(result.stdout, expectations.stdout)) {
|
|
28
|
+
throw new Error(
|
|
29
|
+
`stdout assertion failed: expected ${serializeExpected(expectations.stdout)}, got ${JSON.stringify(result.stdout)}`,
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (expectations.stderr !== undefined) {
|
|
35
|
+
if (!matchesExpectation(result.stderr, expectations.stderr)) {
|
|
36
|
+
throw new Error(
|
|
37
|
+
`stderr assertion failed: expected ${serializeExpected(expectations.stderr)}, got ${JSON.stringify(result.stderr)}`,
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (expectations.exitCode !== undefined) {
|
|
43
|
+
if (result.exitCode !== expectations.exitCode) {
|
|
44
|
+
throw new Error(
|
|
45
|
+
`exitCode assertion failed: expected ${expectations.exitCode}, got ${result.exitCode}`,
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const combined = result.stdout + result.stderr;
|
|
51
|
+
|
|
52
|
+
if (expectations.contains !== undefined) {
|
|
53
|
+
if (!combined.includes(expectations.contains)) {
|
|
54
|
+
throw new Error(
|
|
55
|
+
`contains assertion failed: expected output to contain ${JSON.stringify(expectations.contains)}`,
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (expectations.notContains !== undefined) {
|
|
61
|
+
if (combined.includes(expectations.notContains)) {
|
|
62
|
+
throw new Error(
|
|
63
|
+
`notContains assertion failed: expected output not to contain ${JSON.stringify(expectations.notContains)}`,
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import type { CLIResult } from "@/cli/cli-result";
|
|
2
|
+
|
|
3
|
+
export interface RunCliOpts {
|
|
4
|
+
readonly cwd?: string;
|
|
5
|
+
readonly timeout?: number;
|
|
6
|
+
readonly env?: Record<string, string>;
|
|
7
|
+
readonly stdin?: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export async function runCli(
|
|
11
|
+
command: string,
|
|
12
|
+
args?: string[],
|
|
13
|
+
opts?: RunCliOpts,
|
|
14
|
+
): Promise<CLIResult> {
|
|
15
|
+
const timeout = opts?.timeout ?? 30000;
|
|
16
|
+
|
|
17
|
+
const stdinValue: "ignore" | Uint8Array =
|
|
18
|
+
opts?.stdin !== undefined ? new TextEncoder().encode(opts.stdin) : "ignore";
|
|
19
|
+
|
|
20
|
+
const envValue = opts?.env !== undefined ? { ...process.env, ...opts.env } : process.env;
|
|
21
|
+
|
|
22
|
+
const spawnOpts = {
|
|
23
|
+
env: envValue,
|
|
24
|
+
stdin: stdinValue,
|
|
25
|
+
stdout: "pipe" as const,
|
|
26
|
+
stderr: "pipe" as const,
|
|
27
|
+
...(opts?.cwd !== undefined ? { cwd: opts.cwd } : {}),
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const proc = Bun.spawn([command, ...(args ?? [])], spawnOpts);
|
|
31
|
+
|
|
32
|
+
let timedOut = false;
|
|
33
|
+
|
|
34
|
+
const waitWithTimeout = async (): Promise<number> => {
|
|
35
|
+
return new Promise((resolve) => {
|
|
36
|
+
const timer = setTimeout(() => {
|
|
37
|
+
timedOut = true;
|
|
38
|
+
proc.kill();
|
|
39
|
+
resolve(1);
|
|
40
|
+
}, timeout);
|
|
41
|
+
|
|
42
|
+
proc.exited
|
|
43
|
+
.then((code) => {
|
|
44
|
+
clearTimeout(timer);
|
|
45
|
+
resolve(code ?? 1);
|
|
46
|
+
})
|
|
47
|
+
.catch(() => {
|
|
48
|
+
clearTimeout(timer);
|
|
49
|
+
resolve(1);
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const [exitCode, stdoutText, stderrText] = await Promise.all([
|
|
55
|
+
waitWithTimeout(),
|
|
56
|
+
new Response(proc.stdout).text(),
|
|
57
|
+
new Response(proc.stderr).text(),
|
|
58
|
+
]);
|
|
59
|
+
|
|
60
|
+
return {
|
|
61
|
+
stdout: stdoutText,
|
|
62
|
+
stderr: stderrText,
|
|
63
|
+
exitCode,
|
|
64
|
+
timedOut,
|
|
65
|
+
};
|
|
66
|
+
}
|
package/src/feats.ts
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
// Assertions
|
|
2
|
+
export { assertConfig } from "./assertions/config-assertions";
|
|
3
|
+
export { assertOutput } from "./assertions/output-assertions";
|
|
4
|
+
|
|
5
|
+
// CLI execution
|
|
6
|
+
export type { CLIResult } from "./cli/cli-result";
|
|
7
|
+
export { runCli } from "./cli/cli-runner";
|
|
8
|
+
|
|
9
|
+
// Fixtures
|
|
10
|
+
export { composeFixtures, setupFixture } from "./fixtures/fixture-manager";
|
|
11
|
+
export type { FixtureProject } from "./fixtures/fixture-project";
|
|
12
|
+
|
|
13
|
+
// Feature loading + running
|
|
14
|
+
export { loadFeatures, parseFeature } from "./parser/adapter";
|
|
15
|
+
export type { DataTable, Feature, ParsedStep, Scenario, Tag } from "./parser/models";
|
|
16
|
+
|
|
17
|
+
// Random
|
|
18
|
+
export type { SeededRng } from "./random/seeded-rng";
|
|
19
|
+
export { createRng } from "./random/seeded-rng";
|
|
20
|
+
|
|
21
|
+
// Step definitions
|
|
22
|
+
export { defineParameterType } from "./registry/parameter-types";
|
|
23
|
+
export { Given, Step, Then, When } from "./registry/step-registry";
|
|
24
|
+
export type { RunOptions } from "./runner/feature-runner";
|
|
25
|
+
export { runFeatures } from "./runner/feature-runner";
|
|
26
|
+
// Lifecycle hooks
|
|
27
|
+
export { After, Before } from "./runner/hook-runner";
|
|
28
|
+
|
|
29
|
+
// Types
|
|
30
|
+
export type { World, WorldFactory } from "./state/world";
|