Skip to content

Commit 106e0d6

Browse files
committed
✅ Add directive system for snippet test control
Implement comprehensive directive system for controlling snippet behavior via HTML comment annotations in markdown. Control directives (skip, description, timeout), assertion directives (expect-error, expect-output, expect-return, etc.), and transformation directives (await, return-statement) enable flexible test configuration directly in documentation.
1 parent fcea6f7 commit 106e0d6

File tree

13 files changed

+385
-0
lines changed

13 files changed

+385
-0
lines changed

test/snippets/directives/await.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
/**
2+
* await directive - wraps code in async IIFE
3+
*/
4+
5+
import type { TransformDirective } from "./types.js"
6+
7+
export const awaitDirective: TransformDirective = {
8+
type: "transform",
9+
name: "await",
10+
description: "Automatically wrap code in an async IIFE for top-level await",
11+
handler: (snippet, _directive) => {
12+
// Mark that this snippet needs async wrapping
13+
// The actual transformation happens in the transformer module
14+
return {
15+
code: snippet.code,
16+
modified: false // Transformation will be done by transformer
17+
}
18+
}
19+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
/**
2+
* description directive
3+
*/
4+
5+
import type { ControlDirective } from "./types.js"
6+
7+
export const description: ControlDirective = {
8+
type: "control",
9+
name: "description",
10+
handler: () => {
11+
// Description is purely for documentation, doesn't affect execution
12+
return null
13+
}
14+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
/**
2+
* expect-error directive
3+
*/
4+
5+
import type { AssertionDirective } from "./types.js"
6+
7+
export const expectError: AssertionDirective = {
8+
type: "assertion",
9+
name: "expect-error",
10+
handler: (executionResult, directive) => {
11+
if (!executionResult.error) {
12+
return {
13+
passed: false,
14+
message: "Expected snippet to throw an error, but it succeeded",
15+
}
16+
}
17+
18+
if (typeof directive.args === "string") {
19+
const expectedMessage = directive.args
20+
if (!executionResult.error.message.includes(expectedMessage)) {
21+
return {
22+
passed: false,
23+
message: `Expected error message to include "${expectedMessage}"`,
24+
expected: expectedMessage,
25+
actual: executionResult.error.message,
26+
}
27+
}
28+
}
29+
30+
return { passed: true }
31+
}
32+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
/**
2+
* expect-no-error directive
3+
*/
4+
5+
import type { AssertionDirective } from "./types.js"
6+
7+
export const expectNoError: AssertionDirective = {
8+
type: "assertion",
9+
name: "expect-no-error",
10+
handler: executionResult => {
11+
if (executionResult.error) {
12+
return {
13+
passed: false,
14+
message: "Expected snippet to succeed, but it threw an error",
15+
actual: executionResult.error.message,
16+
}
17+
}
18+
19+
return { passed: true }
20+
}
21+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
/**
2+
* expect-output directive
3+
*/
4+
5+
import type { AssertionDirective } from "./types.js"
6+
7+
export const expectOutput: AssertionDirective = {
8+
type: "assertion",
9+
name: "expect-output",
10+
handler: (executionResult, directive) => {
11+
const output = executionResult.logs.join("\n")
12+
const expected = String(directive.args)
13+
14+
if (!output.includes(expected)) {
15+
return {
16+
passed: false,
17+
message: `Expected output to include "${expected}"`,
18+
expected,
19+
actual: output,
20+
}
21+
}
22+
23+
return { passed: true }
24+
}
25+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
/**
2+
* expect-output-regex directive
3+
*/
4+
5+
import type { AssertionDirective } from "./types.js"
6+
7+
export const expectOutputRegex: AssertionDirective = {
8+
type: "assertion",
9+
name: "expect-output-regex",
10+
handler: (executionResult, directive) => {
11+
const output = executionResult.logs.join("\n")
12+
const pattern = directive.args as RegExp
13+
14+
if (!pattern.test(output)) {
15+
return {
16+
passed: false,
17+
message: `Expected output to match ${pattern}`,
18+
expected: pattern,
19+
actual: output,
20+
}
21+
}
22+
23+
return { passed: true }
24+
}
25+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
/**
2+
* expect-return directive
3+
*/
4+
5+
import type { AssertionDirective } from "./types.js"
6+
7+
export const expectReturn: AssertionDirective = {
8+
type: "assertion",
9+
name: "expect-return",
10+
handler: (executionResult, directive) => {
11+
const expected = String(directive.args)
12+
13+
// Handle undefined/null return values explicitly
14+
if (executionResult.returnValue === undefined || executionResult.returnValue === null) {
15+
return {
16+
passed: false,
17+
message: `Expected return value to include "${expected}", but got ${executionResult.returnValue}`,
18+
expected,
19+
actual: executionResult.returnValue,
20+
}
21+
}
22+
23+
const returnValueStr = JSON.stringify(executionResult.returnValue)
24+
25+
if (!returnValueStr.includes(expected)) {
26+
return {
27+
passed: false,
28+
message: `Expected return value to include "${expected}"`,
29+
expected,
30+
actual: returnValueStr,
31+
}
32+
}
33+
34+
return { passed: true }
35+
}
36+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
/**
2+
* expect-return-json directive
3+
*/
4+
5+
import type { AssertionDirective } from "./types.js"
6+
import { parseAssertionSpec, executeAssertion } from "../assertions.js"
7+
8+
export const expectReturnJson: AssertionDirective = {
9+
type: "assertion",
10+
name: "expect-return-json",
11+
handler: (executionResult, directive) => {
12+
const spec = parseAssertionSpec(directive.args as string | Record<string, unknown>)
13+
return executeAssertion(spec.type, executionResult.returnValue, spec.args)
14+
}
15+
}

test/snippets/directives/index.ts

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
/**
2+
* Directive registry - exports all directive handlers
3+
*/
4+
5+
export * from "./types.js"
6+
export { skip } from "./skip.js"
7+
export { description } from "./description.js"
8+
export { expectError } from "./expectError.js"
9+
export { expectNoError } from "./expectNoError.js"
10+
export { expectOutput } from "./expectOutput.js"
11+
export { expectOutputRegex } from "./expectOutputRegex.js"
12+
export { expectReturn } from "./expectReturn.js"
13+
export { expectReturnJson } from "./expectReturnJson.js"
14+
export { awaitDirective } from "./await.js"
15+
export { timeoutDirective } from "./timeout.js"
16+
export { returnStatementDirective } from "./returnStatement.js"
17+
18+
import type { AnyDirective } from "./types.js"
19+
import { skip } from "./skip.js"
20+
import { description } from "./description.js"
21+
import { expectError } from "./expectError.js"
22+
import { expectNoError } from "./expectNoError.js"
23+
import { expectOutput } from "./expectOutput.js"
24+
import { expectOutputRegex } from "./expectOutputRegex.js"
25+
import { expectReturn } from "./expectReturn.js"
26+
import { expectReturnJson } from "./expectReturnJson.js"
27+
import { awaitDirective } from "./await.js"
28+
import { timeoutDirective } from "./timeout.js"
29+
import { returnStatementDirective } from "./returnStatement.js"
30+
31+
/**
32+
* All built-in directives
33+
*/
34+
export const allDirectives: AnyDirective[] = [
35+
skip,
36+
description,
37+
expectError,
38+
expectNoError,
39+
expectOutput,
40+
expectOutputRegex,
41+
expectReturn,
42+
expectReturnJson,
43+
awaitDirective,
44+
timeoutDirective,
45+
returnStatementDirective,
46+
]
47+
48+
/**
49+
* Directives grouped by type
50+
*/
51+
export const directives = {
52+
control: [skip, description],
53+
assertion: [expectError, expectNoError, expectOutput, expectOutputRegex, expectReturn, expectReturnJson],
54+
transform: [awaitDirective, returnStatementDirective],
55+
config: [timeoutDirective],
56+
}
57+
58+
/**
59+
* Get directive by name
60+
*/
61+
export function getDirectiveByName(name: string): AnyDirective | undefined {
62+
return allDirectives.find(d => d.name === name)
63+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
/**
2+
* return-statement directive - adds return statement for variable
3+
*/
4+
5+
import type { TransformDirective } from "./types.js"
6+
7+
export const returnStatementDirective: TransformDirective = {
8+
type: "transform",
9+
name: "return-statement",
10+
description: "Specify a custom variable to return when the snippet ends with a variable assignment",
11+
handler: (snippet, _directive) => {
12+
// Mark that this snippet needs return statement injection
13+
// The actual transformation happens in the transformer module
14+
return {
15+
code: snippet.code,
16+
modified: false // Transformation will be done by transformer
17+
}
18+
}
19+
}

0 commit comments

Comments
 (0)