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,485 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs"
|
|
2
|
+
import path from "node:path"
|
|
3
|
+
|
|
4
|
+
import { generateMessages } from "@cucumber/gherkin"
|
|
5
|
+
import { IdGenerator, SourceMediaType, type Scenario as GherkinScenario } from "@cucumber/messages"
|
|
6
|
+
|
|
7
|
+
type RuleContext = {
|
|
8
|
+
cwd: string
|
|
9
|
+
filename: string
|
|
10
|
+
report(report: { message: string; node: AstNode }): void
|
|
11
|
+
sourceCode?: {
|
|
12
|
+
ast: unknown
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
type RuleModule = {
|
|
17
|
+
meta: {
|
|
18
|
+
docs: {
|
|
19
|
+
description: string
|
|
20
|
+
}
|
|
21
|
+
type: "problem"
|
|
22
|
+
}
|
|
23
|
+
create(context: RuleContext): Record<"CallExpression", (node: AstNode) => void>
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
type AstNode = {
|
|
27
|
+
type: string
|
|
28
|
+
[key: string]: unknown
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
type FeatureScenario = {
|
|
32
|
+
id: string
|
|
33
|
+
location: string
|
|
34
|
+
name: string
|
|
35
|
+
steps: string[]
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
type Implementation = {
|
|
39
|
+
id: string
|
|
40
|
+
keyNode: AstNode
|
|
41
|
+
steps: ImplementedStep[]
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
type ImplementedStep = {
|
|
45
|
+
name: string
|
|
46
|
+
node: AstNode
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const idTagPrefix = "@id:"
|
|
50
|
+
|
|
51
|
+
const validGherkinFeature: RuleModule = {
|
|
52
|
+
meta: {
|
|
53
|
+
type: "problem",
|
|
54
|
+
docs: {
|
|
55
|
+
description:
|
|
56
|
+
"Validate that Playwright feature(...) implementations match their Gherkin files.",
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
create(context) {
|
|
60
|
+
return {
|
|
61
|
+
CallExpression(node) {
|
|
62
|
+
if (!isIdentifier(node["callee"], "feature")) {
|
|
63
|
+
return
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
lintFeatureCall(context, node)
|
|
67
|
+
},
|
|
68
|
+
}
|
|
69
|
+
},
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export default {
|
|
73
|
+
meta: {
|
|
74
|
+
name: "backbone-e2e",
|
|
75
|
+
},
|
|
76
|
+
rules: {
|
|
77
|
+
"valid-gherkin-feature": validGherkinFeature,
|
|
78
|
+
},
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function lintFeatureCall(context: RuleContext, node: AstNode): void {
|
|
82
|
+
const [featurePathArgument, implementationsArgument] =
|
|
83
|
+
(node["arguments"] as unknown[] | undefined) ?? []
|
|
84
|
+
|
|
85
|
+
if (!isStringLiteral(featurePathArgument)) {
|
|
86
|
+
report(context, node, "feature(...) must use a string literal feature path.")
|
|
87
|
+
return
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (!isObjectExpression(implementationsArgument)) {
|
|
91
|
+
report(context, node, "feature(...) must use an object literal implementation map.")
|
|
92
|
+
return
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const featurePath = path.resolve(path.dirname(context.filename), featurePathArgument.value)
|
|
96
|
+
|
|
97
|
+
if (!existsSync(featurePath)) {
|
|
98
|
+
report(context, featurePathArgument, `Feature file does not exist: ${featurePath}.`)
|
|
99
|
+
return
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const scenarios = parseFeatureFile(featurePath, context, featurePathArgument)
|
|
103
|
+
const implementations = parseImplementationMap(context, implementationsArgument)
|
|
104
|
+
|
|
105
|
+
lintImplementationCoverage(context, scenarios, implementations)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function parseFeatureFile(
|
|
109
|
+
featurePath: string,
|
|
110
|
+
context: RuleContext,
|
|
111
|
+
reportNode: AstNode,
|
|
112
|
+
): FeatureScenario[] {
|
|
113
|
+
const source = readFileSync(featurePath, "utf8")
|
|
114
|
+
const envelopes = generateMessages(
|
|
115
|
+
source,
|
|
116
|
+
featurePath,
|
|
117
|
+
SourceMediaType.TEXT_X_CUCUMBER_GHERKIN_PLAIN,
|
|
118
|
+
{
|
|
119
|
+
includeGherkinDocument: true,
|
|
120
|
+
includePickles: false,
|
|
121
|
+
includeSource: false,
|
|
122
|
+
newId: IdGenerator.incrementing(),
|
|
123
|
+
},
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
for (const envelope of envelopes) {
|
|
127
|
+
if (envelope.parseError) {
|
|
128
|
+
report(context, reportNode, envelope.parseError.message)
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const feature = envelopes.find((envelope) => envelope.gherkinDocument)?.gherkinDocument?.feature
|
|
133
|
+
|
|
134
|
+
if (!feature) {
|
|
135
|
+
return []
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const scenarios: FeatureScenario[] = []
|
|
139
|
+
|
|
140
|
+
for (const child of feature.children) {
|
|
141
|
+
if (child.scenario) {
|
|
142
|
+
scenarios.push(parseScenario(featurePath, child.scenario, context, reportNode))
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (child.rule) {
|
|
146
|
+
for (const ruleChild of child.rule.children) {
|
|
147
|
+
if (ruleChild.scenario) {
|
|
148
|
+
scenarios.push(parseScenario(featurePath, ruleChild.scenario, context, reportNode))
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
lintDuplicateScenarioIds(context, reportNode, scenarios)
|
|
155
|
+
|
|
156
|
+
return scenarios
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function parseScenario(
|
|
160
|
+
featurePath: string,
|
|
161
|
+
scenario: GherkinScenario,
|
|
162
|
+
context: RuleContext,
|
|
163
|
+
reportNode: AstNode,
|
|
164
|
+
): FeatureScenario {
|
|
165
|
+
const idTags = scenario.tags
|
|
166
|
+
.map((tag) => tag.name)
|
|
167
|
+
.filter((tagName) => tagName.startsWith(idTagPrefix))
|
|
168
|
+
const location = `${featurePath}:${scenario.location.line}`
|
|
169
|
+
|
|
170
|
+
if (scenario.keyword === "Scenario Outline") {
|
|
171
|
+
report(context, reportNode, `${location} uses Scenario Outline, which is not supported.`)
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (idTags.length !== 1) {
|
|
175
|
+
report(
|
|
176
|
+
context,
|
|
177
|
+
reportNode,
|
|
178
|
+
`${location} must have exactly one ${idTagPrefix} tag, found ${idTags.length}.`,
|
|
179
|
+
)
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return {
|
|
183
|
+
id: idTags[0]?.slice(idTagPrefix.length) ?? "",
|
|
184
|
+
location,
|
|
185
|
+
name: scenario.name,
|
|
186
|
+
steps: scenario.steps.map((step) => `${step.keyword}${step.text}`),
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function lintDuplicateScenarioIds(
|
|
191
|
+
context: RuleContext,
|
|
192
|
+
reportNode: AstNode,
|
|
193
|
+
scenarios: FeatureScenario[],
|
|
194
|
+
): void {
|
|
195
|
+
const scenariosById = new Map<string, FeatureScenario[]>()
|
|
196
|
+
|
|
197
|
+
for (const scenario of scenarios) {
|
|
198
|
+
if (scenario.id === "") {
|
|
199
|
+
continue
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const scenariosWithId = scenariosById.get(scenario.id) ?? []
|
|
203
|
+
scenariosWithId.push(scenario)
|
|
204
|
+
scenariosById.set(scenario.id, scenariosWithId)
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
for (const [id, scenariosWithId] of scenariosById) {
|
|
208
|
+
if (scenariosWithId.length <= 1) {
|
|
209
|
+
continue
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const firstScenario = scenariosWithId[0]
|
|
213
|
+
const duplicateScenario = scenariosWithId[1]
|
|
214
|
+
|
|
215
|
+
if (firstScenario === undefined || duplicateScenario === undefined) {
|
|
216
|
+
continue
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
report(
|
|
220
|
+
context,
|
|
221
|
+
reportNode,
|
|
222
|
+
`Duplicate scenario id "${id}" at ${duplicateScenario.location}; first used at ${firstScenario.location}.`,
|
|
223
|
+
)
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function parseImplementationMap(
|
|
228
|
+
context: RuleContext,
|
|
229
|
+
implementationsArgument: AstNode,
|
|
230
|
+
): Implementation[] {
|
|
231
|
+
const implementations: Implementation[] = []
|
|
232
|
+
|
|
233
|
+
for (const property of (implementationsArgument["properties"] as unknown[] | undefined) ?? []) {
|
|
234
|
+
if (!isProperty(property)) {
|
|
235
|
+
report(
|
|
236
|
+
context,
|
|
237
|
+
implementationsArgument,
|
|
238
|
+
"implementation map entries must be property assignments.",
|
|
239
|
+
)
|
|
240
|
+
continue
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const id = propertyNameText(property.key)
|
|
244
|
+
|
|
245
|
+
if (!id) {
|
|
246
|
+
report(context, property.key, "implementation ids must be string literal property names.")
|
|
247
|
+
continue
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
if (!isFunctionExpression(property.value)) {
|
|
251
|
+
report(context, property.value, `implementation "${id}" must be a function.`)
|
|
252
|
+
continue
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
implementations.push({
|
|
256
|
+
id,
|
|
257
|
+
keyNode: property.key,
|
|
258
|
+
steps: readScenarioSteps(context, property.value),
|
|
259
|
+
})
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
return implementations
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function readScenarioSteps(context: RuleContext, implementation: AstNode): ImplementedStep[] {
|
|
266
|
+
const steps: ImplementedStep[] = []
|
|
267
|
+
|
|
268
|
+
visit(implementation, (node) => {
|
|
269
|
+
if (!isScenarioStepCall(node)) {
|
|
270
|
+
return
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
const [stepName] = (node["arguments"] as unknown[] | undefined) ?? []
|
|
274
|
+
|
|
275
|
+
if (!isStringLiteral(stepName)) {
|
|
276
|
+
report(context, node, "scenario.step(...) must use a string literal step name.")
|
|
277
|
+
return
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
steps.push({
|
|
281
|
+
name: stepName.value,
|
|
282
|
+
node: stepName,
|
|
283
|
+
})
|
|
284
|
+
})
|
|
285
|
+
|
|
286
|
+
return steps
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function lintImplementationCoverage(
|
|
290
|
+
context: RuleContext,
|
|
291
|
+
scenarios: FeatureScenario[],
|
|
292
|
+
implementations: Implementation[],
|
|
293
|
+
): void {
|
|
294
|
+
const implementationsById = new Map(
|
|
295
|
+
implementations.map((implementation) => [implementation.id, implementation]),
|
|
296
|
+
)
|
|
297
|
+
const scenariosById = new Map(scenarios.map((scenario) => [scenario.id, scenario]))
|
|
298
|
+
|
|
299
|
+
for (const scenario of scenarios) {
|
|
300
|
+
if (scenario.id === "") {
|
|
301
|
+
continue
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
const implementation = implementationsById.get(scenario.id)
|
|
305
|
+
|
|
306
|
+
if (!implementation) {
|
|
307
|
+
report(
|
|
308
|
+
context,
|
|
309
|
+
implementations[0]?.keyNode ?? contextNode(context),
|
|
310
|
+
`scenario "${scenario.name}" has no implementation for id "${scenario.id}".`,
|
|
311
|
+
)
|
|
312
|
+
continue
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
lintScenarioSteps(context, scenario, implementation)
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
for (const implementation of implementations) {
|
|
319
|
+
if (!scenariosById.has(implementation.id)) {
|
|
320
|
+
report(
|
|
321
|
+
context,
|
|
322
|
+
implementation.keyNode,
|
|
323
|
+
`implementation id "${implementation.id}" has no matching scenario.`,
|
|
324
|
+
)
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
function lintScenarioSteps(
|
|
330
|
+
context: RuleContext,
|
|
331
|
+
scenario: FeatureScenario,
|
|
332
|
+
implementation: Implementation,
|
|
333
|
+
): void {
|
|
334
|
+
const remainingImplementedSteps = countValues(implementation.steps.map((step) => step.name))
|
|
335
|
+
const missingSteps: string[] = []
|
|
336
|
+
|
|
337
|
+
for (const expectedStep of scenario.steps) {
|
|
338
|
+
const remainingCount = remainingImplementedSteps.get(expectedStep) ?? 0
|
|
339
|
+
|
|
340
|
+
if (remainingCount > 0) {
|
|
341
|
+
remainingImplementedSteps.set(expectedStep, remainingCount - 1)
|
|
342
|
+
} else {
|
|
343
|
+
missingSteps.push(expectedStep)
|
|
344
|
+
report(
|
|
345
|
+
context,
|
|
346
|
+
implementation.keyNode,
|
|
347
|
+
`missing implemented step for "${scenario.id}" from ${scenario.location}: ${quote(expectedStep)}.`,
|
|
348
|
+
)
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
const remainingExpectedSteps = countValues(scenario.steps)
|
|
353
|
+
const unexpectedSteps: ImplementedStep[] = []
|
|
354
|
+
|
|
355
|
+
for (const implementedStep of implementation.steps) {
|
|
356
|
+
const remainingCount = remainingExpectedSteps.get(implementedStep.name) ?? 0
|
|
357
|
+
|
|
358
|
+
if (remainingCount > 0) {
|
|
359
|
+
remainingExpectedSteps.set(implementedStep.name, remainingCount - 1)
|
|
360
|
+
} else {
|
|
361
|
+
unexpectedSteps.push(implementedStep)
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
if (missingSteps.length === 0 && unexpectedSteps.length > 0) {
|
|
366
|
+
report(
|
|
367
|
+
context,
|
|
368
|
+
implementation.keyNode,
|
|
369
|
+
`extra implemented step(s) for "${scenario.id}" not present in ${scenario.location}: ` +
|
|
370
|
+
`${unexpectedSteps.map((step) => quote(step.name)).join(", ")}.`,
|
|
371
|
+
)
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
for (const unexpectedStep of unexpectedSteps) {
|
|
375
|
+
report(
|
|
376
|
+
context,
|
|
377
|
+
unexpectedStep.node,
|
|
378
|
+
`unexpected implemented step for "${scenario.id}": ${quote(unexpectedStep.name)}. ` +
|
|
379
|
+
"No step with this label exists in the feature scenario.",
|
|
380
|
+
)
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
function countValues(values: string[]): Map<string, number> {
|
|
385
|
+
const counts = new Map<string, number>()
|
|
386
|
+
|
|
387
|
+
for (const value of values) {
|
|
388
|
+
counts.set(value, (counts.get(value) ?? 0) + 1)
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
return counts
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
function isScenarioStepCall(node: AstNode): boolean {
|
|
395
|
+
if (node.type !== "CallExpression" || !isNode(node["callee"])) {
|
|
396
|
+
return false
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
const callee = node["callee"]
|
|
400
|
+
|
|
401
|
+
return (
|
|
402
|
+
callee.type === "MemberExpression" &&
|
|
403
|
+
callee["computed"] === false &&
|
|
404
|
+
isIdentifier(callee["object"], "scenario") &&
|
|
405
|
+
isIdentifier(callee["property"], "step")
|
|
406
|
+
)
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
function isIdentifier(value: unknown, name: string): value is AstNode {
|
|
410
|
+
return isNode(value) && value.type === "Identifier" && value["name"] === name
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
function isStringLiteral(value: unknown): value is AstNode & { value: string } {
|
|
414
|
+
return isNode(value) && value.type === "Literal" && typeof value["value"] === "string"
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
function isObjectExpression(value: unknown): value is AstNode {
|
|
418
|
+
return isNode(value) && value.type === "ObjectExpression"
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
function isProperty(value: unknown): value is AstNode & { key: AstNode; value: AstNode } {
|
|
422
|
+
return (
|
|
423
|
+
isNode(value) && value.type === "Property" && isNode(value["key"]) && isNode(value["value"])
|
|
424
|
+
)
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
function isFunctionExpression(value: unknown): boolean {
|
|
428
|
+
return (
|
|
429
|
+
isNode(value) &&
|
|
430
|
+
(value.type === "ArrowFunctionExpression" || value.type === "FunctionExpression")
|
|
431
|
+
)
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
function propertyNameText(name: AstNode): string | undefined {
|
|
435
|
+
if (isStringLiteral(name)) {
|
|
436
|
+
return name.value
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
return undefined
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
function visit(node: AstNode, callback: (node: AstNode) => void, seen = new Set<AstNode>()): void {
|
|
443
|
+
if (seen.has(node)) {
|
|
444
|
+
return
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
seen.add(node)
|
|
448
|
+
callback(node)
|
|
449
|
+
|
|
450
|
+
for (const [key, value] of Object.entries(node)) {
|
|
451
|
+
if (key === "parent" || key === "loc" || key === "range" || key === "start" || key === "end") {
|
|
452
|
+
continue
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
if (Array.isArray(value)) {
|
|
456
|
+
for (const item of value) {
|
|
457
|
+
if (isNode(item)) {
|
|
458
|
+
visit(item, callback, seen)
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
} else if (isNode(value)) {
|
|
462
|
+
visit(value, callback, seen)
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
function isNode(value: unknown): value is AstNode {
|
|
468
|
+
return (
|
|
469
|
+
typeof value === "object" &&
|
|
470
|
+
value !== null &&
|
|
471
|
+
typeof (value as { type?: unknown }).type === "string"
|
|
472
|
+
)
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
function report(context: RuleContext, node: AstNode, message: string): void {
|
|
476
|
+
context.report({ node, message })
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
function contextNode(context: RuleContext): AstNode {
|
|
480
|
+
return context.sourceCode?.ast as AstNode
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
function quote(value: string): string {
|
|
484
|
+
return `"${value}"`
|
|
485
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import assert from "node:assert/strict"
|
|
2
|
+
|
|
3
|
+
import { expect } from "@playwright/test"
|
|
4
|
+
import { feature } from "../support/app-gherkin"
|
|
5
|
+
|
|
6
|
+
feature("../features/helloworld.feature", {
|
|
7
|
+
"hello.say-hello": async ({ db, page, scenario, serverUrl }) => {
|
|
8
|
+
await scenario.step("Given the Rust server is healthy", async () => {
|
|
9
|
+
await expect
|
|
10
|
+
.poll(async () => {
|
|
11
|
+
const response = await page.request.get(new URL("/health", serverUrl).toString())
|
|
12
|
+
return response.ok()
|
|
13
|
+
})
|
|
14
|
+
.toBe(true)
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
await scenario.step("And the visitor is on the hello page", async () => {
|
|
18
|
+
await page.goto("/")
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
await scenario.step("Then they see the default hello message", async () => {
|
|
22
|
+
await expect(page.getByRole("heading", { name: "ConnectRPC helloworld" })).toBeVisible()
|
|
23
|
+
await expect(page.getByText("Hello, World!")).toBeVisible()
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
await scenario.step("When they ask to greet Playwright", async () => {
|
|
27
|
+
await page.getByLabel("Name").fill("Playwright")
|
|
28
|
+
await page.getByRole("button", { name: "Say hello" }).click()
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
await scenario.step("Then they see the Playwright greeting", async () => {
|
|
32
|
+
await expect(page.getByText("Hello, Playwright!")).toBeVisible()
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
await scenario.step("And the Playwright input is saved", async () => {
|
|
36
|
+
assert.deepEqual(db.listHelloWorldInputs(), ["Playwright"])
|
|
37
|
+
})
|
|
38
|
+
},
|
|
39
|
+
})
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"lib": ["DOM", "ES2022"],
|
|
5
|
+
"module": "ESNext",
|
|
6
|
+
"moduleResolution": "Bundler",
|
|
7
|
+
"strict": true,
|
|
8
|
+
"exactOptionalPropertyTypes": true,
|
|
9
|
+
"noUncheckedIndexedAccess": true,
|
|
10
|
+
"noImplicitOverride": true,
|
|
11
|
+
"noPropertyAccessFromIndexSignature": true,
|
|
12
|
+
"noImplicitReturns": true,
|
|
13
|
+
"noFallthroughCasesInSwitch": true,
|
|
14
|
+
"noUnusedLocals": true,
|
|
15
|
+
"noUnusedParameters": true,
|
|
16
|
+
"noUncheckedSideEffectImports": true,
|
|
17
|
+
"verbatimModuleSyntax": true,
|
|
18
|
+
"moduleDetection": "force",
|
|
19
|
+
"allowUnreachableCode": false,
|
|
20
|
+
"allowUnusedLabels": false,
|
|
21
|
+
"noEmit": true,
|
|
22
|
+
"skipLibCheck": true,
|
|
23
|
+
"types": ["node"]
|
|
24
|
+
},
|
|
25
|
+
"include": ["playwright.config.ts", "support/**/*.ts", "tests/**/*.ts"]
|
|
26
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
{
|
|
2
|
+
"extends": "./tsconfig.json",
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
"declaration": true,
|
|
5
|
+
"module": "NodeNext",
|
|
6
|
+
"moduleResolution": "NodeNext",
|
|
7
|
+
"noEmit": false,
|
|
8
|
+
"outDir": "support/dist",
|
|
9
|
+
"rootDir": "support"
|
|
10
|
+
},
|
|
11
|
+
"include": ["support/oxlint-plugin.ts"]
|
|
12
|
+
}
|