Skip to content

Commit da80cb8

Browse files
committed
Add hooks design document
Analyzes four options for test setup/teardown in funee: 1. Scenario-level setup/teardown - too limited 2. Scenario groups - reintroduces file-level thinking 3. Resource pattern (wrapper functions) - RECOMMENDED 4. Fixture pattern - too much framework magic Recommends Option 3 (Resource pattern) because: - Function-centric: Everything is just functions - Watch mode compatible: Setup/teardown inside closure - Composable: Standard function composition - No framework magic: Users write and understand the code - Type-safe: Standard TypeScript inference Includes: - defineResource helper for convenience - using() helper for composing multiple resources - Full examples showing simple and complex cases - Integration details for runScenarios and watch mode
1 parent 7e71c9c commit da80cb8

9 files changed

Lines changed: 760 additions & 0 deletions

File tree

funee-lib/assertions/contains.ts

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
/**
2+
* Containment assertion for strings and arrays.
3+
*/
4+
5+
import { Assertion } from "./Assertion.ts";
6+
import { AssertionError } from "./AssertionError.ts";
7+
8+
/**
9+
* Deep equality check for containment comparison.
10+
*/
11+
const isDeepEqual = (a: any, b: any): boolean => {
12+
if (a === b) return true;
13+
if (a === null || b === null) return false;
14+
if (typeof a !== typeof b) return false;
15+
16+
if (typeof a === "object" && typeof b === "object") {
17+
const aKeys = Object.keys(a);
18+
const bKeys = Object.keys(b);
19+
20+
if (aKeys.length !== bKeys.length) return false;
21+
22+
for (const key of aKeys) {
23+
if (!isDeepEqual(a[key], b[key])) {
24+
return false;
25+
}
26+
}
27+
return true;
28+
}
29+
30+
return false;
31+
};
32+
33+
/**
34+
* Assert that a value contains the expected item.
35+
*
36+
* For strings: checks if the string contains the expected substring.
37+
* For arrays: checks if the array contains the expected item (using deep equality for objects).
38+
*
39+
* @example
40+
* await assertThat("hello world", contains("world")); // pass
41+
* await assertThat([1, 2, 3], contains(2)); // pass
42+
* await assertThat([{a: 1}, {b: 2}], contains({a: 1})); // pass (deep equality)
43+
*/
44+
export const contains = (expected: any): Assertion<string | any[]> => {
45+
return (actual: string | any[]): void => {
46+
if (typeof actual === "string") {
47+
if (typeof expected !== "string") {
48+
throw AssertionError({
49+
message: "Expected string to contain a string, but got " + typeof expected,
50+
actual,
51+
expected,
52+
operator: "contains"
53+
});
54+
}
55+
if (!actual.includes(expected)) {
56+
throw AssertionError({
57+
message: "Expected string to contain " + JSON.stringify(expected) + " but got " + JSON.stringify(actual),
58+
actual,
59+
expected,
60+
operator: "contains"
61+
});
62+
}
63+
} else if (Array.isArray(actual)) {
64+
const found = actual.some((item) => isDeepEqual(item, expected));
65+
if (!found) {
66+
throw AssertionError({
67+
message: "Expected array to contain " + JSON.stringify(expected) + " but got " + JSON.stringify(actual),
68+
actual,
69+
expected,
70+
operator: "contains"
71+
});
72+
}
73+
} else {
74+
throw AssertionError({
75+
message: "Expected string or array but got " + typeof actual,
76+
actual,
77+
expected,
78+
operator: "contains"
79+
});
80+
}
81+
};
82+
};
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
/**
2+
* Numeric greater than assertion.
3+
*/
4+
5+
import { Assertion } from "./Assertion.ts";
6+
import { AssertionError } from "./AssertionError.ts";
7+
8+
/**
9+
* Assert that a number is greater than the expected value.
10+
*
11+
* @example
12+
* await assertThat(5, greaterThan(3)); // pass
13+
* await assertThat(10, greaterThan(10)); // fail
14+
*/
15+
export const greaterThan = (expected: number): Assertion<number> => {
16+
return (actual: number): void => {
17+
if (typeof actual !== "number") {
18+
throw AssertionError({
19+
message: "Expected a number but got " + typeof actual,
20+
actual,
21+
expected: "> " + expected,
22+
operator: "greaterThan"
23+
});
24+
}
25+
if (!(actual > expected)) {
26+
throw AssertionError({
27+
message: "Expected " + actual + " to be greater than " + expected,
28+
actual,
29+
expected: "> " + expected,
30+
operator: "greaterThan"
31+
});
32+
}
33+
};
34+
};
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
/**
2+
* Numeric greater than or equal assertion.
3+
*/
4+
5+
import { Assertion } from "./Assertion.ts";
6+
import { AssertionError } from "./AssertionError.ts";
7+
8+
/**
9+
* Assert that a number is greater than or equal to the expected value.
10+
*
11+
* @example
12+
* await assertThat(5, greaterThanOrEqual(5)); // pass
13+
* await assertThat(6, greaterThanOrEqual(5)); // pass
14+
* await assertThat(4, greaterThanOrEqual(5)); // fail
15+
*/
16+
export const greaterThanOrEqual = (expected: number): Assertion<number> => {
17+
return (actual: number): void => {
18+
if (typeof actual !== "number") {
19+
throw AssertionError({
20+
message: "Expected a number but got " + typeof actual,
21+
actual,
22+
expected: ">= " + expected,
23+
operator: "greaterThanOrEqual"
24+
});
25+
}
26+
if (!(actual >= expected)) {
27+
throw AssertionError({
28+
message: "Expected " + actual + " to be greater than or equal to " + expected,
29+
actual,
30+
expected: ">= " + expected,
31+
operator: "greaterThanOrEqual"
32+
});
33+
}
34+
};
35+
};

funee-lib/assertions/index.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,3 +35,11 @@ export { assertThat } from "./assertThat.ts";
3535
export { is } from "./is.ts";
3636
export { not } from "./not.ts";
3737
export { both } from "./both.ts";
38+
39+
// Matchers
40+
export { contains } from "./contains.ts";
41+
export { matches } from "./matches.ts";
42+
export { greaterThan } from "./greaterThan.ts";
43+
export { lessThan } from "./lessThan.ts";
44+
export { greaterThanOrEqual } from "./greaterThanOrEqual.ts";
45+
export { lessThanOrEqual } from "./lessThanOrEqual.ts";

funee-lib/assertions/lessThan.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
/**
2+
* Numeric less than assertion.
3+
*/
4+
5+
import { Assertion } from "./Assertion.ts";
6+
import { AssertionError } from "./AssertionError.ts";
7+
8+
/**
9+
* Assert that a number is less than the expected value.
10+
*
11+
* @example
12+
* await assertThat(2, lessThan(10)); // pass
13+
* await assertThat(10, lessThan(10)); // fail
14+
*/
15+
export const lessThan = (expected: number): Assertion<number> => {
16+
return (actual: number): void => {
17+
if (typeof actual !== "number") {
18+
throw AssertionError({
19+
message: "Expected a number but got " + typeof actual,
20+
actual,
21+
expected: "< " + expected,
22+
operator: "lessThan"
23+
});
24+
}
25+
if (!(actual < expected)) {
26+
throw AssertionError({
27+
message: "Expected " + actual + " to be less than " + expected,
28+
actual,
29+
expected: "< " + expected,
30+
operator: "lessThan"
31+
});
32+
}
33+
};
34+
};
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
/**
2+
* Numeric less than or equal assertion.
3+
*/
4+
5+
import { Assertion } from "./Assertion.ts";
6+
import { AssertionError } from "./AssertionError.ts";
7+
8+
/**
9+
* Assert that a number is less than or equal to the expected value.
10+
*
11+
* @example
12+
* await assertThat(5, lessThanOrEqual(5)); // pass
13+
* await assertThat(4, lessThanOrEqual(5)); // pass
14+
* await assertThat(6, lessThanOrEqual(5)); // fail
15+
*/
16+
export const lessThanOrEqual = (expected: number): Assertion<number> => {
17+
return (actual: number): void => {
18+
if (typeof actual !== "number") {
19+
throw AssertionError({
20+
message: "Expected a number but got " + typeof actual,
21+
actual,
22+
expected: "<= " + expected,
23+
operator: "lessThanOrEqual"
24+
});
25+
}
26+
if (!(actual <= expected)) {
27+
throw AssertionError({
28+
message: "Expected " + actual + " to be less than or equal to " + expected,
29+
actual,
30+
expected: "<= " + expected,
31+
operator: "lessThanOrEqual"
32+
});
33+
}
34+
};
35+
};

funee-lib/assertions/matches.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
/**
2+
* Regex matching assertion.
3+
*/
4+
5+
import { Assertion } from "./Assertion.ts";
6+
import { AssertionError } from "./AssertionError.ts";
7+
8+
/**
9+
* Assert that a string matches the expected regular expression.
10+
*
11+
* @example
12+
* await assertThat("hello123", matches(/\d+/)); // pass
13+
* await assertThat("abc", matches(/^[a-z]+$/)); // pass
14+
*/
15+
export const matches = (pattern: RegExp): Assertion<string> => {
16+
return (actual: string): void => {
17+
if (typeof actual !== "string") {
18+
throw AssertionError({
19+
message: "Expected a string but got " + typeof actual,
20+
actual,
21+
expected: pattern.toString(),
22+
operator: "matches"
23+
});
24+
}
25+
if (!pattern.test(actual)) {
26+
throw AssertionError({
27+
message: "Expected " + JSON.stringify(actual) + " to match " + pattern.toString(),
28+
actual,
29+
expected: pattern.toString(),
30+
operator: "matches"
31+
});
32+
}
33+
};
34+
};

funee-lib/index.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -354,6 +354,13 @@ export {
354354
assert,
355355
strictEqual,
356356
deepEqual,
357+
// Matchers
358+
contains,
359+
matches,
360+
greaterThan,
361+
lessThan,
362+
greaterThanOrEqual,
363+
lessThanOrEqual,
357364
} from "./assertions/index.ts";
358365

359366
// ============================================================================

0 commit comments

Comments
 (0)