create-backbone-template 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +33 -0
- package/bin/create-backbone-template.js +5 -0
- package/package.json +30 -0
- package/src/create-backbone-template.js +204 -0
- package/template/.agents/skills/agent-browser/SKILL.md +55 -0
- package/template/.agents/skills/create-plan/SKILL.md +52 -0
- package/template/.agents/skills/create-plan/agents/openai.yaml +4 -0
- package/template/.agents/skills/create-pr-presentation/SKILL.md +86 -0
- package/template/.agents/skills/create-pr-presentation/agents/openai.yaml +4 -0
- package/template/.agents/skills/implement-plan/SKILL.md +26 -0
- package/template/.agents/skills/implement-plan/agents/openai.yaml +4 -0
- package/template/.agents/skills/review-plan/SKILL.md +38 -0
- package/template/.agents/skills/review-plan/agents/openai.yaml +4 -0
- package/template/.env.schema +30 -0
- package/template/.env.test +6 -0
- package/template/.oxlintrc.json +67 -0
- package/template/.vscode/extensions.json +3 -0
- package/template/.vscode/settings.json +23 -0
- package/template/AGENTS.md +55 -0
- package/template/Cargo.lock +2648 -0
- package/template/Cargo.toml +29 -0
- package/template/Justfile +140 -0
- package/template/README.md +72 -0
- package/template/TODO.md +1 -0
- package/template/_gitignore +12 -0
- package/template/buf.gen.yaml +7 -0
- package/template/buf.yaml +10 -0
- package/template/client/.oxfmtrc.json +8 -0
- package/template/client/.oxlintrc.json +57 -0
- package/template/client/README.md +19 -0
- package/template/client/_gitignore +5 -0
- package/template/client/index.html +12 -0
- package/template/client/package.json +47 -0
- package/template/client/packages/design-system/package.json +19 -0
- package/template/client/packages/design-system/src/index.ts +2 -0
- package/template/client/packages/design-system-basic/package.json +18 -0
- package/template/client/packages/design-system-basic/src/button.stories.tsx +50 -0
- package/template/client/packages/design-system-basic/src/button.tsx +26 -0
- package/template/client/packages/design-system-basic/src/empty-state.stories.tsx +18 -0
- package/template/client/packages/design-system-basic/src/empty-state.tsx +17 -0
- package/template/client/packages/design-system-basic/src/form-field.stories.tsx +15 -0
- package/template/client/packages/design-system-basic/src/form-field.tsx +10 -0
- package/template/client/packages/design-system-basic/src/form.stories.tsx +27 -0
- package/template/client/packages/design-system-basic/src/form.tsx +9 -0
- package/template/client/packages/design-system-basic/src/heading.stories.tsx +14 -0
- package/template/client/packages/design-system-basic/src/heading.tsx +25 -0
- package/template/client/packages/design-system-basic/src/index.tsx +15 -0
- package/template/client/packages/design-system-basic/src/inline.stories.tsx +13 -0
- package/template/client/packages/design-system-basic/src/inline.tsx +5 -0
- package/template/client/packages/design-system-basic/src/layout.stories.tsx +24 -0
- package/template/client/packages/design-system-basic/src/layout.tsx +14 -0
- package/template/client/packages/design-system-basic/src/loader.stories.tsx +8 -0
- package/template/client/packages/design-system-basic/src/loader.tsx +11 -0
- package/template/client/packages/design-system-basic/src/navigation.stories.tsx +16 -0
- package/template/client/packages/design-system-basic/src/navigation.tsx +18 -0
- package/template/client/packages/design-system-basic/src/notice.stories.tsx +13 -0
- package/template/client/packages/design-system-basic/src/notice.tsx +5 -0
- package/template/client/packages/design-system-basic/src/stack.stories.tsx +17 -0
- package/template/client/packages/design-system-basic/src/stack.tsx +5 -0
- package/template/client/packages/design-system-basic/src/styles.css +254 -0
- package/template/client/packages/design-system-basic/src/text-input.stories.tsx +13 -0
- package/template/client/packages/design-system-basic/src/text-input.tsx +5 -0
- package/template/client/packages/design-system-basic/src/text.stories.tsx +21 -0
- package/template/client/packages/design-system-basic/src/text.tsx +5 -0
- package/template/client/packages/design-system-contract/package.json +15 -0
- package/template/client/packages/design-system-contract/src/button.ts +10 -0
- package/template/client/packages/design-system-contract/src/empty-state.ts +9 -0
- package/template/client/packages/design-system-contract/src/form-field.ts +9 -0
- package/template/client/packages/design-system-contract/src/form.ts +9 -0
- package/template/client/packages/design-system-contract/src/heading.ts +9 -0
- package/template/client/packages/design-system-contract/src/index.ts +13 -0
- package/template/client/packages/design-system-contract/src/inline.ts +7 -0
- package/template/client/packages/design-system-contract/src/layout.ts +8 -0
- package/template/client/packages/design-system-contract/src/loader.ts +7 -0
- package/template/client/packages/design-system-contract/src/navigation.ts +13 -0
- package/template/client/packages/design-system-contract/src/notice.ts +8 -0
- package/template/client/packages/design-system-contract/src/stack.ts +8 -0
- package/template/client/packages/design-system-contract/src/text-input.ts +5 -0
- package/template/client/packages/design-system-contract/src/text.ts +9 -0
- package/template/client/packages/design-system-lint/fixtures/invalid/external-ui-import.tsx +5 -0
- package/template/client/packages/design-system-lint/fixtures/invalid/raw-dom-jsx.tsx +3 -0
- package/template/client/packages/design-system-lint/fixtures/invalid/two-violations.tsx +7 -0
- package/template/client/packages/design-system-lint/fixtures/valid/design-system-only.tsx +13 -0
- package/template/client/packages/design-system-lint/package.json +23 -0
- package/template/client/packages/design-system-lint/src/check-design-system-architecture.ts +22 -0
- package/template/client/packages/design-system-lint/src/design-system-architecture.ts +286 -0
- package/template/client/packages/design-system-lint/src/oxlint-plugin.ts +11 -0
- package/template/client/packages/design-system-lint/src/page-architecture.ts +382 -0
- package/template/client/packages/design-system-lint/src/rules.ts +111 -0
- package/template/client/packages/design-system-lint/test/design-system-architecture.test.ts +243 -0
- package/template/client/packages/design-system-lint/test/oxlint-fixtures.test.ts +159 -0
- package/template/client/packages/design-system-lint/test/page-architecture.test.ts +175 -0
- package/template/client/packages/design-system-lint/test/rules.test.ts +65 -0
- package/template/client/packages/design-system-lint/tsconfig.json +29 -0
- package/template/client/src/App.tsx +77 -0
- package/template/client/src/design-system-components.test.tsx +75 -0
- package/template/client/src/gen/helloworld/v1/helloworld_pb.ts +63 -0
- package/template/client/src/main.tsx +18 -0
- package/template/client/src/pages/hello/hello-page.stories.tsx +20 -0
- package/template/client/src/pages/hello/hello-page.test.tsx +90 -0
- package/template/client/src/pages/hello/hello-page.tsx +126 -0
- package/template/client/src/pages/page.ts +20 -0
- package/template/client/src/testing/create-preview-events.test.ts +36 -0
- package/template/client/src/testing/create-preview-events.ts +30 -0
- package/template/client/src/vite-env.d.ts +1 -0
- package/template/client/tsconfig.json +32 -0
- package/template/client/vite.config.ts +21 -0
- package/template/client/vite.ladle.config.ts +5 -0
- package/template/e2e/.gherkin-lintrc +20 -0
- package/template/e2e/.oxfmtrc.json +15 -0
- package/template/e2e/.oxlintrc.json +37 -0
- package/template/e2e/_gitignore +4 -0
- package/template/e2e/features/helloworld.feature +10 -0
- package/template/e2e/package.json +42 -0
- package/template/e2e/playwright.config.ts +16 -0
- package/template/e2e/support/app-gherkin.ts +4 -0
- package/template/e2e/support/fixtures.ts +236 -0
- package/template/e2e/support/gherkin-fixtures/duplicate-id.feature +9 -0
- package/template/e2e/support/gherkin-fixtures/duplicate-id.spec.ts +7 -0
- package/template/e2e/support/gherkin-fixtures/extra-implementation.spec.ts +7 -0
- package/template/e2e/support/gherkin-fixtures/extra-step.spec.ts +10 -0
- package/template/e2e/support/gherkin-fixtures/happy-path.spec.ts +4 -0
- package/template/e2e/support/gherkin-fixtures/missing-id.feature +4 -0
- package/template/e2e/support/gherkin-fixtures/missing-id.spec.ts +7 -0
- package/template/e2e/support/gherkin-fixtures/missing-implementation.spec.ts +7 -0
- package/template/e2e/support/gherkin-fixtures/missing-step.spec.ts +7 -0
- package/template/e2e/support/gherkin-fixtures/playwright.config.ts +7 -0
- package/template/e2e/support/gherkin-fixtures/scenario-outline.feature +9 -0
- package/template/e2e/support/gherkin-fixtures/scenario-outline.spec.ts +7 -0
- package/template/e2e/support/gherkin-fixtures/step-mismatch.spec.ts +9 -0
- package/template/e2e/support/gherkin-fixtures/valid-implementations.ts +23 -0
- package/template/e2e/support/gherkin-fixtures/valid-scenarios.feature +26 -0
- package/template/e2e/support/gherkin.test.ts +184 -0
- package/template/e2e/support/gherkin.ts +321 -0
- package/template/e2e/support/oxlint-plugin.test.ts +328 -0
- package/template/e2e/support/oxlint-plugin.ts +485 -0
- package/template/e2e/tests/helloworld.spec.ts +39 -0
- package/template/e2e/tsconfig.json +26 -0
- package/template/e2e/tsconfig.oxlint-plugin.json +12 -0
- package/template/package.json +9 -0
- package/template/pnpm-lock.yaml +10723 -0
- package/template/pnpm-workspace.yaml +8 -0
- package/template/pr-slide/README.md +95 -0
- package/template/pr-slide/package.json +23 -0
- package/template/pr-slide/src/cli.js +262 -0
- package/template/pr-slide/src/generate-pr-deck.js +833 -0
- package/template/pr-slide/src/git-context.js +91 -0
- package/template/pr-slide/src/presentation-paths.js +9 -0
- package/template/pr-slide/src/presentations.js +53 -0
- package/template/pr-slide/test/generate-pr-deck.test.js +118 -0
- package/template/pr-slide/test/presentation-paths.test.js +14 -0
- package/template/pr-slide/test/presentations.test.js +50 -0
- package/template/proto/helloworld/v1/helloworld.proto +15 -0
- package/template/scripts/run-e2e.sh +10 -0
- package/template/server/Cargo.toml +26 -0
- package/template/server/build.rs +9 -0
- package/template/server/dylint/backbone_server_lints/.cargo/config.toml +6 -0
- package/template/server/dylint/backbone_server_lints/Cargo.lock +1581 -0
- package/template/server/dylint/backbone_server_lints/Cargo.toml +21 -0
- package/template/server/dylint/backbone_server_lints/README.md +5 -0
- package/template/server/dylint/backbone_server_lints/_gitignore +1 -0
- package/template/server/dylint/backbone_server_lints/rust-toolchain +3 -0
- package/template/server/dylint/backbone_server_lints/src/lib.rs +612 -0
- package/template/server/dylint/backbone_server_lints/ui/lib.rs +4 -0
- package/template/server/dylint/backbone_server_lints/ui/lib.stderr +10 -0
- package/template/server/dylint/backbone_server_lints/ui/long_file.rs +303 -0
- package/template/server/dylint/backbone_server_lints/ui/long_file.stderr +6 -0
- package/template/server/dylint/backbone_server_lints/ui/main.rs +59 -0
- package/template/server/dylint/backbone_server_lints/ui/main.stderr +85 -0
- package/template/server/migrations/20260520120000_create_projects.sql +12 -0
- package/template/server/migrations/20260524160000_create_hello_world_inputs.sql +12 -0
- package/template/server/src/config.rs +27 -0
- package/template/server/src/db/hello_world.rs +34 -0
- package/template/server/src/db/hello_world_tests.rs +11 -0
- package/template/server/src/db/mod.rs +39 -0
- package/template/server/src/lib.rs +10 -0
- package/template/server/src/main.rs +43 -0
- package/template/server/src/rpc/greeter/mod.rs +31 -0
- package/template/server/src/rpc/greeter/say_hello.rs +27 -0
- package/template/server/src/rpc/mod.rs +8 -0
- package/template/server/src/state.rs +13 -0
- package/template/skills-lock.json +11 -0
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import assert from "node:assert/strict"
|
|
2
|
+
import { spawn } from "node:child_process"
|
|
3
|
+
import fs from "node:fs"
|
|
4
|
+
import os from "node:os"
|
|
5
|
+
import path from "node:path"
|
|
6
|
+
import test from "node:test"
|
|
7
|
+
|
|
8
|
+
const fixtureConfig = "support/gherkin-fixtures/playwright.config.ts"
|
|
9
|
+
|
|
10
|
+
type FixtureResult = {
|
|
11
|
+
status: number | null
|
|
12
|
+
output: string
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
type FixtureCase = {
|
|
16
|
+
fixture: string
|
|
17
|
+
name: string
|
|
18
|
+
assertResult: (result: FixtureResult) => void
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const fixtureCases: FixtureCase[] = [
|
|
22
|
+
{
|
|
23
|
+
fixture: "happy-path",
|
|
24
|
+
name: "passes when Gherkin and implementation steps match",
|
|
25
|
+
assertResult(result) {
|
|
26
|
+
assert.equal(result.status, 0, result.output)
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
fixture: "step-mismatch",
|
|
31
|
+
name: "fails when an implementation step name differs from Gherkin",
|
|
32
|
+
assertResult(result) {
|
|
33
|
+
assert.notEqual(result.status, 0)
|
|
34
|
+
assert.match(result.output, /Step mismatch/)
|
|
35
|
+
assert.match(result.output, /Expected \\"Given the documented step name\\"/)
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
fixture: "missing-step",
|
|
40
|
+
name: "fails when a Gherkin step is not implemented",
|
|
41
|
+
assertResult(result) {
|
|
42
|
+
assert.notEqual(result.status, 0)
|
|
43
|
+
assert.match(result.output, /Missing implemented step\(s\)/)
|
|
44
|
+
assert.match(result.output, /Given the documented step is not implemented/)
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
fixture: "extra-step",
|
|
49
|
+
name: "fails when the implementation has an extra step",
|
|
50
|
+
assertResult(result) {
|
|
51
|
+
assert.notEqual(result.status, 0)
|
|
52
|
+
assert.match(result.output, /Unexpected extra step/)
|
|
53
|
+
assert.match(result.output, /Then an extra implementation step runs/)
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
fixture: "missing-id",
|
|
58
|
+
name: "fails when a scenario is missing an id tag",
|
|
59
|
+
assertResult(result) {
|
|
60
|
+
assert.notEqual(result.status, 0)
|
|
61
|
+
assert.match(result.output, /missing a required @id: tag/)
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
fixture: "duplicate-id",
|
|
66
|
+
name: "fails when scenario ids are duplicated",
|
|
67
|
+
assertResult(result) {
|
|
68
|
+
assert.notEqual(result.status, 0)
|
|
69
|
+
assert.match(result.output, /Duplicate scenario id \\"fixture\.duplicate-id\\"/)
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
fixture: "missing-implementation",
|
|
74
|
+
name: "fails when a scenario has no implementation",
|
|
75
|
+
assertResult(result) {
|
|
76
|
+
assert.notEqual(result.status, 0)
|
|
77
|
+
assert.match(result.output, /Missing implementation\(s\)/)
|
|
78
|
+
assert.match(result.output, /fixture\.missing-implementation/)
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
fixture: "extra-implementation",
|
|
83
|
+
name: "fails when an implementation has no scenario",
|
|
84
|
+
assertResult(result) {
|
|
85
|
+
assert.notEqual(result.status, 0)
|
|
86
|
+
assert.match(result.output, /Extra implementation\(s\)/)
|
|
87
|
+
assert.match(result.output, /fixture\.unused-implementation/)
|
|
88
|
+
},
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
fixture: "scenario-outline",
|
|
92
|
+
name: "fails when a scenario outline is used",
|
|
93
|
+
assertResult(result) {
|
|
94
|
+
assert.notEqual(result.status, 0)
|
|
95
|
+
assert.match(result.output, /uses Scenario Outline, which is not supported/)
|
|
96
|
+
},
|
|
97
|
+
},
|
|
98
|
+
]
|
|
99
|
+
|
|
100
|
+
test("validates Gherkin adapter fixtures", async () => {
|
|
101
|
+
const results = await Promise.all(
|
|
102
|
+
fixtureCases.map(async (fixtureCase) => ({
|
|
103
|
+
fixtureCase,
|
|
104
|
+
result: await runFixture(fixtureCase.fixture),
|
|
105
|
+
})),
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
for (const { fixtureCase, result } of results) {
|
|
109
|
+
fixtureCase.assertResult(result)
|
|
110
|
+
}
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
async function runFixture(name: string): Promise<FixtureResult> {
|
|
114
|
+
const outputDir = fs.mkdtempSync(path.join(os.tmpdir(), "backbone-gherkin-adapter-"))
|
|
115
|
+
const jsonOutput = path.join(outputDir, "playwright-report.json")
|
|
116
|
+
|
|
117
|
+
try {
|
|
118
|
+
const result = await spawnFixture(name, jsonOutput)
|
|
119
|
+
|
|
120
|
+
return {
|
|
121
|
+
status: result.status,
|
|
122
|
+
output: [
|
|
123
|
+
result.error ? `${result.error}` : "",
|
|
124
|
+
result.stdout,
|
|
125
|
+
result.stderr,
|
|
126
|
+
fs.existsSync(jsonOutput) ? fs.readFileSync(jsonOutput, "utf8") : "",
|
|
127
|
+
].join("\n"),
|
|
128
|
+
}
|
|
129
|
+
} finally {
|
|
130
|
+
fs.rmSync(outputDir, { force: true, recursive: true })
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
type SpawnFixtureResult = {
|
|
135
|
+
error?: Error
|
|
136
|
+
status: number | null
|
|
137
|
+
stderr: string
|
|
138
|
+
stdout: string
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function spawnFixture(name: string, jsonOutput: string): Promise<SpawnFixtureResult> {
|
|
142
|
+
return new Promise((resolve) => {
|
|
143
|
+
const child = spawn(
|
|
144
|
+
"pnpm",
|
|
145
|
+
[
|
|
146
|
+
"exec",
|
|
147
|
+
"playwright",
|
|
148
|
+
"test",
|
|
149
|
+
`support/gherkin-fixtures/${name}.spec.ts`,
|
|
150
|
+
"--config",
|
|
151
|
+
fixtureConfig,
|
|
152
|
+
"--reporter=json",
|
|
153
|
+
],
|
|
154
|
+
{
|
|
155
|
+
cwd: new URL("..", import.meta.url),
|
|
156
|
+
env: {
|
|
157
|
+
...process.env,
|
|
158
|
+
PLAYWRIGHT_JSON_OUTPUT_NAME: jsonOutput,
|
|
159
|
+
},
|
|
160
|
+
},
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
let stderr = ""
|
|
164
|
+
let stdout = ""
|
|
165
|
+
|
|
166
|
+
child.stderr?.setEncoding("utf8")
|
|
167
|
+
child.stderr?.on("data", (chunk: string) => {
|
|
168
|
+
stderr += chunk
|
|
169
|
+
})
|
|
170
|
+
|
|
171
|
+
child.stdout?.setEncoding("utf8")
|
|
172
|
+
child.stdout?.on("data", (chunk: string) => {
|
|
173
|
+
stdout += chunk
|
|
174
|
+
})
|
|
175
|
+
|
|
176
|
+
child.on("error", (error) => {
|
|
177
|
+
resolve({ error, status: null, stderr, stdout })
|
|
178
|
+
})
|
|
179
|
+
|
|
180
|
+
child.on("close", (status) => {
|
|
181
|
+
resolve({ status, stderr, stdout })
|
|
182
|
+
})
|
|
183
|
+
})
|
|
184
|
+
}
|
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs"
|
|
2
|
+
import path from "node:path"
|
|
3
|
+
import { fileURLToPath } from "node:url"
|
|
4
|
+
|
|
5
|
+
import { generateMessages } from "@cucumber/gherkin"
|
|
6
|
+
import {
|
|
7
|
+
IdGenerator,
|
|
8
|
+
SourceMediaType,
|
|
9
|
+
type Envelope,
|
|
10
|
+
type Scenario as GherkinScenario,
|
|
11
|
+
} from "@cucumber/messages"
|
|
12
|
+
import {
|
|
13
|
+
test as baseTest,
|
|
14
|
+
type PlaywrightTestArgs,
|
|
15
|
+
type PlaywrightTestOptions,
|
|
16
|
+
type TestInfo,
|
|
17
|
+
} from "@playwright/test"
|
|
18
|
+
|
|
19
|
+
type ScenarioStep = <T>(name: string, body: () => T | Promise<T>) => Promise<T>
|
|
20
|
+
|
|
21
|
+
export type ScenarioRuntime = {
|
|
22
|
+
readonly id: string
|
|
23
|
+
readonly name: string
|
|
24
|
+
step: ScenarioStep
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
type ScenarioImplementationArgs<ExtraArgs extends object = object> = PlaywrightTestArgs &
|
|
28
|
+
Pick<PlaywrightTestOptions, "baseURL"> & {
|
|
29
|
+
scenario: ScenarioRuntime
|
|
30
|
+
} & ExtraArgs
|
|
31
|
+
|
|
32
|
+
export type ScenarioImplementation<ExtraArgs extends object = object> = (
|
|
33
|
+
args: ScenarioImplementationArgs<ExtraArgs>,
|
|
34
|
+
testInfo: TestInfo,
|
|
35
|
+
) => void | Promise<void>
|
|
36
|
+
|
|
37
|
+
type ScenarioDefinition = {
|
|
38
|
+
id: string
|
|
39
|
+
name: string
|
|
40
|
+
location: string
|
|
41
|
+
steps: string[]
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const idTagPrefix = "@id:"
|
|
45
|
+
|
|
46
|
+
type FeatureTest = Pick<typeof baseTest, "describe" | "extend">
|
|
47
|
+
|
|
48
|
+
export const feature = createFeature(baseTest)
|
|
49
|
+
|
|
50
|
+
export function createFeature<ExtraArgs extends object>(test: FeatureTest) {
|
|
51
|
+
return function registerFeature(
|
|
52
|
+
featureFile: string,
|
|
53
|
+
implementations: Record<string, ScenarioImplementation<ExtraArgs>>,
|
|
54
|
+
): void {
|
|
55
|
+
const featurePath = resolveFeaturePath(featureFile)
|
|
56
|
+
const parsedFeature = parseFeature(featurePath)
|
|
57
|
+
|
|
58
|
+
validateImplementations(featurePath, parsedFeature.scenarios, implementations)
|
|
59
|
+
|
|
60
|
+
test.describe(parsedFeature.name, () => {
|
|
61
|
+
for (const scenarioDefinition of parsedFeature.scenarios) {
|
|
62
|
+
const scenarioTest = test.extend<{ scenario: ScenarioRuntime }>({
|
|
63
|
+
scenario: [
|
|
64
|
+
// oxlint-disable-next-line no-empty-pattern -- Playwright fixtures require object destructuring here.
|
|
65
|
+
async ({}, use) => {
|
|
66
|
+
const scenario = createScenarioRuntime(scenarioDefinition)
|
|
67
|
+
let implementationFailed = false
|
|
68
|
+
|
|
69
|
+
try {
|
|
70
|
+
await use(scenario)
|
|
71
|
+
} catch (error) {
|
|
72
|
+
implementationFailed = true
|
|
73
|
+
throw error
|
|
74
|
+
} finally {
|
|
75
|
+
if (!implementationFailed) {
|
|
76
|
+
scenario.assertComplete()
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
},
|
|
80
|
+
{ auto: true },
|
|
81
|
+
],
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
const implementation = implementations[scenarioDefinition.id]
|
|
85
|
+
|
|
86
|
+
if (implementation === undefined) {
|
|
87
|
+
throw new Error(`Missing implementation for scenario id "${scenarioDefinition.id}".`)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const runScenario = scenarioTest as unknown as (
|
|
91
|
+
name: string,
|
|
92
|
+
implementation: ScenarioImplementation<ExtraArgs>,
|
|
93
|
+
) => void
|
|
94
|
+
|
|
95
|
+
runScenario(scenarioDefinition.name, implementation)
|
|
96
|
+
}
|
|
97
|
+
})
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function parseFeature(featurePath: string): { name: string; scenarios: ScenarioDefinition[] } {
|
|
102
|
+
const source = readFileSync(featurePath, "utf8")
|
|
103
|
+
const envelopes = generateMessages(
|
|
104
|
+
source,
|
|
105
|
+
featurePath,
|
|
106
|
+
SourceMediaType.TEXT_X_CUCUMBER_GHERKIN_PLAIN,
|
|
107
|
+
{
|
|
108
|
+
includeGherkinDocument: true,
|
|
109
|
+
includePickles: false,
|
|
110
|
+
includeSource: false,
|
|
111
|
+
newId: IdGenerator.incrementing(),
|
|
112
|
+
},
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
assertNoParseErrors(featurePath, envelopes)
|
|
116
|
+
|
|
117
|
+
const gherkinDocument = envelopes.find((envelope) => envelope.gherkinDocument)?.gherkinDocument
|
|
118
|
+
const gherkinFeature = gherkinDocument?.feature
|
|
119
|
+
|
|
120
|
+
if (!gherkinFeature) {
|
|
121
|
+
throw new Error(`Feature file ${featurePath} does not contain a Feature.`)
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const scenarios: ScenarioDefinition[] = []
|
|
125
|
+
|
|
126
|
+
for (const child of gherkinFeature.children) {
|
|
127
|
+
if (child.background) {
|
|
128
|
+
throw new Error(
|
|
129
|
+
`${featurePath}:${child.background.location.line} uses Background, which is not supported.`,
|
|
130
|
+
)
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (child.scenario) {
|
|
134
|
+
scenarios.push(parseScenario(featurePath, child.scenario))
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (child.rule) {
|
|
138
|
+
for (const ruleChild of child.rule.children) {
|
|
139
|
+
if (ruleChild.background) {
|
|
140
|
+
throw new Error(
|
|
141
|
+
`${featurePath}:${ruleChild.background.location.line} uses Background, which is not supported.`,
|
|
142
|
+
)
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (ruleChild.scenario) {
|
|
146
|
+
scenarios.push(parseScenario(featurePath, ruleChild.scenario))
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
assertUniqueScenarioIds(featurePath, scenarios)
|
|
153
|
+
|
|
154
|
+
return {
|
|
155
|
+
name: gherkinFeature.name,
|
|
156
|
+
scenarios,
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function parseScenario(featurePath: string, scenario: GherkinScenario): ScenarioDefinition {
|
|
161
|
+
if (scenario.examples.length > 0) {
|
|
162
|
+
throw new Error(
|
|
163
|
+
`${featurePath}:${scenario.location.line} uses Scenario Outline, which is not supported.`,
|
|
164
|
+
)
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const ids = scenario.tags
|
|
168
|
+
.map((tag) => tag.name)
|
|
169
|
+
.filter((tagName) => tagName.startsWith(idTagPrefix))
|
|
170
|
+
.map((tagName) => tagName.slice(idTagPrefix.length))
|
|
171
|
+
|
|
172
|
+
if (ids.length === 0) {
|
|
173
|
+
throw new Error(
|
|
174
|
+
`${featurePath}:${scenario.location.line} is missing a required ${idTagPrefix} tag.`,
|
|
175
|
+
)
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (ids.length > 1) {
|
|
179
|
+
throw new Error(`${featurePath}:${scenario.location.line} has multiple ${idTagPrefix} tags.`)
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const [id] = ids
|
|
183
|
+
|
|
184
|
+
if (id === undefined) {
|
|
185
|
+
throw new Error(
|
|
186
|
+
`${featurePath}:${scenario.location.line} is missing a required ${idTagPrefix} tag.`,
|
|
187
|
+
)
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return {
|
|
191
|
+
id,
|
|
192
|
+
name: scenario.name,
|
|
193
|
+
location: `${featurePath}:${scenario.location.line}`,
|
|
194
|
+
steps: scenario.steps.map((step) => `${step.keyword}${step.text}`),
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function createScenarioRuntime(
|
|
199
|
+
scenarioDefinition: ScenarioDefinition,
|
|
200
|
+
): ScenarioRuntime & { assertComplete: () => void } {
|
|
201
|
+
let nextStepIndex = 0
|
|
202
|
+
|
|
203
|
+
return {
|
|
204
|
+
id: scenarioDefinition.id,
|
|
205
|
+
name: scenarioDefinition.name,
|
|
206
|
+
async step<T>(name: string, body: () => T | Promise<T>): Promise<T> {
|
|
207
|
+
const expectedStep = scenarioDefinition.steps[nextStepIndex]
|
|
208
|
+
|
|
209
|
+
if (expectedStep === undefined) {
|
|
210
|
+
throw new Error(
|
|
211
|
+
`Unexpected extra step in ${scenarioDefinition.location}: "${name}". ` +
|
|
212
|
+
`The feature only defines ${scenarioDefinition.steps.length} step(s).`,
|
|
213
|
+
)
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
if (name !== expectedStep) {
|
|
217
|
+
throw new Error(
|
|
218
|
+
`Step mismatch in ${scenarioDefinition.location} at step ${nextStepIndex + 1}. ` +
|
|
219
|
+
`Expected "${expectedStep}", but the implementation called "${name}".`,
|
|
220
|
+
)
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
nextStepIndex += 1
|
|
224
|
+
return baseTest.step(name, body)
|
|
225
|
+
},
|
|
226
|
+
assertComplete() {
|
|
227
|
+
const missingSteps = scenarioDefinition.steps.slice(nextStepIndex)
|
|
228
|
+
|
|
229
|
+
if (missingSteps.length > 0) {
|
|
230
|
+
throw new Error(
|
|
231
|
+
`Missing implemented step(s) in ${scenarioDefinition.location}: ${missingSteps
|
|
232
|
+
.map((step) => `"${step}"`)
|
|
233
|
+
.join(", ")}.`,
|
|
234
|
+
)
|
|
235
|
+
}
|
|
236
|
+
},
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function validateImplementations(
|
|
241
|
+
featurePath: string,
|
|
242
|
+
scenarios: ScenarioDefinition[],
|
|
243
|
+
implementations: Record<string, unknown>,
|
|
244
|
+
): void {
|
|
245
|
+
const expectedIds = new Set(scenarios.map((scenario) => scenario.id))
|
|
246
|
+
const implementedIds = new Set(Object.keys(implementations))
|
|
247
|
+
|
|
248
|
+
const missingIds = [...expectedIds].filter((id) => !implementedIds.has(id))
|
|
249
|
+
if (missingIds.length > 0) {
|
|
250
|
+
throw new Error(`Missing implementation(s) for ${featurePath}: ${missingIds.join(", ")}.`)
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const extraIds = [...implementedIds].filter((id) => !expectedIds.has(id))
|
|
254
|
+
if (extraIds.length > 0) {
|
|
255
|
+
throw new Error(`Extra implementation(s) for ${featurePath}: ${extraIds.join(", ")}.`)
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function assertUniqueScenarioIds(featurePath: string, scenarios: ScenarioDefinition[]): void {
|
|
260
|
+
const seen = new Map<string, ScenarioDefinition>()
|
|
261
|
+
|
|
262
|
+
for (const scenario of scenarios) {
|
|
263
|
+
const existingScenario = seen.get(scenario.id)
|
|
264
|
+
|
|
265
|
+
if (existingScenario) {
|
|
266
|
+
throw new Error(
|
|
267
|
+
`Duplicate scenario id "${scenario.id}" in ${featurePath}: ` +
|
|
268
|
+
`${existingScenario.location} and ${scenario.location}.`,
|
|
269
|
+
)
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
seen.set(scenario.id, scenario)
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function assertNoParseErrors(featurePath: string, envelopes: readonly Envelope[]): void {
|
|
277
|
+
const parseErrors = envelopes
|
|
278
|
+
.map((envelope) => envelope.parseError)
|
|
279
|
+
.filter((parseError) => parseError !== undefined)
|
|
280
|
+
|
|
281
|
+
if (parseErrors.length > 0) {
|
|
282
|
+
throw new Error(
|
|
283
|
+
`Could not parse ${featurePath}: ${parseErrors
|
|
284
|
+
.map((parseError) => parseError.message)
|
|
285
|
+
.join("; ")}`,
|
|
286
|
+
)
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function resolveFeaturePath(featureFile: string): string {
|
|
291
|
+
if (path.isAbsolute(featureFile)) {
|
|
292
|
+
return featureFile
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
return path.resolve(path.dirname(resolveCallerPath()), featureFile)
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function resolveCallerPath(): string {
|
|
299
|
+
const stack = new Error().stack?.split("\n") ?? []
|
|
300
|
+
|
|
301
|
+
for (const line of stack) {
|
|
302
|
+
const candidate = parseStackLinePath(line)
|
|
303
|
+
|
|
304
|
+
if (candidate && !candidate.endsWith("/support/gherkin.ts")) {
|
|
305
|
+
return candidate
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
return path.join(process.cwd(), "playwright.config.ts")
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function parseStackLinePath(line: string): string | undefined {
|
|
313
|
+
const match = line.match(/\(?((?:file:\/\/)?\/.*?):\d+:\d+\)?$/)
|
|
314
|
+
const rawPath = match?.[1]
|
|
315
|
+
|
|
316
|
+
if (!rawPath) {
|
|
317
|
+
return undefined
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
return rawPath.startsWith("file://") ? fileURLToPath(rawPath) : rawPath
|
|
321
|
+
}
|