effect-bdd 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/LICENSE +21 -0
- package/README.md +465 -0
- package/dist/Bdd.d.ts +285 -0
- package/dist/Bdd.d.ts.map +1 -0
- package/dist/Bdd.js +304 -0
- package/dist/Bdd.js.map +1 -0
- package/dist/Errors.d.ts +65 -0
- package/dist/Errors.d.ts.map +1 -0
- package/dist/Errors.js +58 -0
- package/dist/Errors.js.map +1 -0
- package/dist/bin.d.ts +3 -0
- package/dist/bin.d.ts.map +1 -0
- package/dist/bin.js +7 -0
- package/dist/bin.js.map +1 -0
- package/dist/index.d.ts +147 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +31 -0
- package/dist/index.js.map +1 -0
- package/dist/internal/cli/errors.d.ts +30 -0
- package/dist/internal/cli/errors.d.ts.map +1 -0
- package/dist/internal/cli/errors.js +8 -0
- package/dist/internal/cli/errors.js.map +1 -0
- package/dist/internal/cli/glob.d.ts +13 -0
- package/dist/internal/cli/glob.d.ts.map +1 -0
- package/dist/internal/cli/glob.js +29 -0
- package/dist/internal/cli/glob.js.map +1 -0
- package/dist/internal/cli/loaders.d.ts +13 -0
- package/dist/internal/cli/loaders.d.ts.map +1 -0
- package/dist/internal/cli/loaders.js +44 -0
- package/dist/internal/cli/loaders.js.map +1 -0
- package/dist/internal/cli/models.d.ts +102 -0
- package/dist/internal/cli/models.d.ts.map +1 -0
- package/dist/internal/cli/models.js +2 -0
- package/dist/internal/cli/models.js.map +1 -0
- package/dist/internal/cli/moduleLoader.d.ts +14 -0
- package/dist/internal/cli/moduleLoader.d.ts.map +1 -0
- package/dist/internal/cli/moduleLoader.js +39 -0
- package/dist/internal/cli/moduleLoader.js.map +1 -0
- package/dist/internal/cli/reporter.d.ts +22 -0
- package/dist/internal/cli/reporter.d.ts.map +1 -0
- package/dist/internal/cli/reporter.js +227 -0
- package/dist/internal/cli/reporter.js.map +1 -0
- package/dist/internal/cli/runner.d.ts +14 -0
- package/dist/internal/cli/runner.d.ts.map +1 -0
- package/dist/internal/cli/runner.js +178 -0
- package/dist/internal/cli/runner.js.map +1 -0
- package/dist/internal/cli/tagExpression.d.ts +7 -0
- package/dist/internal/cli/tagExpression.d.ts.map +1 -0
- package/dist/internal/cli/tagExpression.js +127 -0
- package/dist/internal/cli/tagExpression.js.map +1 -0
- package/dist/internal/cucumberCompiler.d.ts +5 -0
- package/dist/internal/cucumberCompiler.d.ts.map +1 -0
- package/dist/internal/cucumberCompiler.js +49 -0
- package/dist/internal/cucumberCompiler.js.map +1 -0
- package/dist/internal/expression.d.ts +18 -0
- package/dist/internal/expression.d.ts.map +1 -0
- package/dist/internal/expression.js +59 -0
- package/dist/internal/expression.js.map +1 -0
- package/dist/internal/matching.d.ts +30 -0
- package/dist/internal/matching.d.ts.map +1 -0
- package/dist/internal/matching.js +37 -0
- package/dist/internal/matching.js.map +1 -0
- package/dist/internal/parser.d.ts +54 -0
- package/dist/internal/parser.d.ts.map +1 -0
- package/dist/internal/parser.js +93 -0
- package/dist/internal/parser.js.map +1 -0
- package/dist/internal/runner.d.ts +77 -0
- package/dist/internal/runner.d.ts.map +1 -0
- package/dist/internal/runner.js +117 -0
- package/dist/internal/runner.js.map +1 -0
- package/dist/main.d.ts +23 -0
- package/dist/main.d.ts.map +1 -0
- package/dist/main.js +104 -0
- package/dist/main.js.map +1 -0
- package/package.json +102 -0
- package/src/Bdd.ts +575 -0
- package/src/Errors.ts +60 -0
- package/src/bin.ts +10 -0
- package/src/index.ts +155 -0
- package/src/internal/cli/errors.ts +20 -0
- package/src/internal/cli/glob.ts +37 -0
- package/src/internal/cli/loaders.ts +100 -0
- package/src/internal/cli/models.ts +118 -0
- package/src/internal/cli/moduleLoader.ts +41 -0
- package/src/internal/cli/reporter.ts +367 -0
- package/src/internal/cli/runner.ts +336 -0
- package/src/internal/cli/tagExpression.ts +173 -0
- package/src/internal/cucumberCompiler.ts +58 -0
- package/src/internal/expression.ts +103 -0
- package/src/internal/matching.ts +81 -0
- package/src/internal/parser.ts +155 -0
- package/src/internal/runner.ts +373 -0
- package/src/main.ts +169 -0
|
@@ -0,0 +1,373 @@
|
|
|
1
|
+
import type { Pickle, PickleDocString, PickleStep, PickleTable } from "@cucumber/messages"
|
|
2
|
+
import * as Arr from "effect/Array"
|
|
3
|
+
import * as Effect from "effect/Effect"
|
|
4
|
+
import { pipe } from "effect/Function"
|
|
5
|
+
import * as Option from "effect/Option"
|
|
6
|
+
import * as Record from "effect/Record"
|
|
7
|
+
import * as Schema from "effect/Schema"
|
|
8
|
+
import { MatchError, ParseError, StepError } from "../Errors.ts"
|
|
9
|
+
import * as Matching from "./matching.ts"
|
|
10
|
+
import * as Parser from "./parser.ts"
|
|
11
|
+
|
|
12
|
+
/** @internal */
|
|
13
|
+
export type DataTableInput = PickleTable
|
|
14
|
+
|
|
15
|
+
/** @internal */
|
|
16
|
+
export type DocStringInput = PickleDocString
|
|
17
|
+
|
|
18
|
+
/** @internal */
|
|
19
|
+
export type StepKind = "Step" | "Given" | "When" | "Then"
|
|
20
|
+
|
|
21
|
+
/** @internal */
|
|
22
|
+
export interface Matcher<A> {
|
|
23
|
+
readonly source: string
|
|
24
|
+
readonly match: (text: string) => Option.Option<A>
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** @internal */
|
|
28
|
+
export type StepArgument =
|
|
29
|
+
| {
|
|
30
|
+
readonly _tag: "TableArg"
|
|
31
|
+
readonly decode: (argument: DataTableInput) => Effect.Effect<unknown, unknown>
|
|
32
|
+
}
|
|
33
|
+
| {
|
|
34
|
+
readonly _tag: "DocStringArg"
|
|
35
|
+
readonly decode: (argument: DocStringInput) => Effect.Effect<unknown, unknown>
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** @internal */
|
|
39
|
+
export interface Transition<State, E, R> {
|
|
40
|
+
readonly kind: StepKind
|
|
41
|
+
readonly expression: Matcher<unknown>
|
|
42
|
+
readonly argument?: StepArgument
|
|
43
|
+
readonly run: (captures: unknown, argument: unknown, state: State) => Effect.Effect<State, E, R>
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** @internal */
|
|
47
|
+
export interface Feature<State, E, R> {
|
|
48
|
+
readonly name: string
|
|
49
|
+
readonly initial: State
|
|
50
|
+
readonly transitions: ReadonlyArray<Transition<State, E, R>>
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** @internal */
|
|
54
|
+
export interface Report {
|
|
55
|
+
readonly feature: string
|
|
56
|
+
readonly scenarios: ReadonlyArray<{
|
|
57
|
+
readonly name: string
|
|
58
|
+
readonly steps: number
|
|
59
|
+
readonly tags: ReadonlyArray<string>
|
|
60
|
+
}>
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** @internal */
|
|
64
|
+
export type RunError = ParseError | MatchError | StepError
|
|
65
|
+
|
|
66
|
+
/** @internal */
|
|
67
|
+
export type GherkinCompiler = Parser.GherkinCompiler
|
|
68
|
+
|
|
69
|
+
/** @internal */
|
|
70
|
+
export interface ScenarioTask<State, E, R> {
|
|
71
|
+
readonly featureDefinition: Feature<State, E, R>
|
|
72
|
+
readonly featureName: string
|
|
73
|
+
readonly scenarioName: string
|
|
74
|
+
readonly scenarioIndex: number
|
|
75
|
+
readonly scenarioLine: number
|
|
76
|
+
readonly ruleName?: string
|
|
77
|
+
readonly ruleLine?: number
|
|
78
|
+
readonly tags: ReadonlyArray<string>
|
|
79
|
+
readonly pickle: Pickle
|
|
80
|
+
readonly source: Parser.SourceIndex
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
interface ResolvedTransition<State, E, R> {
|
|
84
|
+
readonly transition: Transition<State, E, R>
|
|
85
|
+
readonly captures: unknown
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** @internal */
|
|
89
|
+
export type ScenarioReport = Report["scenarios"][number]
|
|
90
|
+
|
|
91
|
+
/** @internal */
|
|
92
|
+
export const decodeTable = <S extends Schema.Decoder<unknown, never>>(row: S) => {
|
|
93
|
+
const decode = Schema.decodeUnknownEffect(row)
|
|
94
|
+
return (table: PickleTable): Effect.Effect<ReadonlyArray<S["Type"]>, unknown> => {
|
|
95
|
+
const [headers, ...rows] = table.rows.map((row) => row.cells.map((cell) => cell.value))
|
|
96
|
+
if (headers === undefined) {
|
|
97
|
+
return Effect.succeed([])
|
|
98
|
+
}
|
|
99
|
+
return Effect.forEach(rows, (cells: ReadonlyArray<string>) => decode(rowObject(headers, cells)))
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** @internal */
|
|
104
|
+
export const decodeDocString = <S extends Schema.Decoder<unknown, never>>(schema: S) => {
|
|
105
|
+
const decode = Schema.decodeUnknownEffect(schema)
|
|
106
|
+
return (docString: PickleDocString): Effect.Effect<S["Type"], unknown> => decode(docString.content)
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** @internal */
|
|
110
|
+
export const run = <State, E, R>(
|
|
111
|
+
featureDefinition: Feature<State, E, R>,
|
|
112
|
+
source: string
|
|
113
|
+
): Effect.Effect<Report, RunError, R | Parser.GherkinCompiler> =>
|
|
114
|
+
pipe(
|
|
115
|
+
Parser.parse(source),
|
|
116
|
+
Effect.flatMap((feature) =>
|
|
117
|
+
pipe(
|
|
118
|
+
validateFeatureDefinition(featureDefinition, feature),
|
|
119
|
+
Effect.flatMap(() => Effect.forEach(buildScenarioTasks(featureDefinition, feature), runScenarioTask)),
|
|
120
|
+
Effect.map((scenarios): Report => ({
|
|
121
|
+
feature: feature.name,
|
|
122
|
+
scenarios
|
|
123
|
+
}))
|
|
124
|
+
)
|
|
125
|
+
)
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
const validateFeatureDefinition = <State, E, R>(
|
|
129
|
+
featureDefinition: Feature<State, E, R>,
|
|
130
|
+
feature: Parser.CompiledFeature
|
|
131
|
+
): Effect.Effect<void, MatchError> =>
|
|
132
|
+
featureDefinition.name === feature.name
|
|
133
|
+
? Effect.void
|
|
134
|
+
: Effect.fail(
|
|
135
|
+
new MatchError({
|
|
136
|
+
message: `Feature definition "${featureDefinition.name}" does not match Gherkin feature "${feature.name}"`,
|
|
137
|
+
scenario: "",
|
|
138
|
+
step: feature.name,
|
|
139
|
+
line: feature.line,
|
|
140
|
+
candidates: [featureDefinition.name]
|
|
141
|
+
})
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
/** @internal */
|
|
145
|
+
export const buildScenarioTasks = <State, E, R>(
|
|
146
|
+
featureDefinition: Feature<State, E, R>,
|
|
147
|
+
feature: Parser.CompiledFeature
|
|
148
|
+
): ReadonlyArray<ScenarioTask<State, E, R>> =>
|
|
149
|
+
Arr.map(feature.pickles, (pickle, scenarioIndex): ScenarioTask<State, E, R> => {
|
|
150
|
+
const source = Parser.findScenario(pickle, feature.source)
|
|
151
|
+
const rule = pipe(
|
|
152
|
+
source,
|
|
153
|
+
Option.map(({ rule }) => rule),
|
|
154
|
+
Option.getOrUndefined
|
|
155
|
+
)
|
|
156
|
+
return {
|
|
157
|
+
featureDefinition,
|
|
158
|
+
featureName: feature.name,
|
|
159
|
+
scenarioName: pickle.name,
|
|
160
|
+
scenarioIndex,
|
|
161
|
+
scenarioLine: pickle.location?.line ?? pipe(
|
|
162
|
+
source,
|
|
163
|
+
Option.map(({ scenario }) => scenario.location.line),
|
|
164
|
+
Option.getOrElse(() => feature.line)
|
|
165
|
+
),
|
|
166
|
+
...(rule === undefined ? {} : {
|
|
167
|
+
ruleName: rule.name,
|
|
168
|
+
ruleLine: rule.location.line
|
|
169
|
+
}),
|
|
170
|
+
tags: pickle.tags.map((tag) => tag.name),
|
|
171
|
+
pickle,
|
|
172
|
+
source: feature.source
|
|
173
|
+
}
|
|
174
|
+
})
|
|
175
|
+
|
|
176
|
+
/** @internal */
|
|
177
|
+
export const runScenarioTask = <State, E, R>(
|
|
178
|
+
task: ScenarioTask<State, E, R>
|
|
179
|
+
): Effect.Effect<ScenarioReport, RunError, R> =>
|
|
180
|
+
pipe(
|
|
181
|
+
runSteps(task.featureDefinition, task.scenarioName, task.pickle.steps, task.source, task.featureDefinition.initial),
|
|
182
|
+
Effect.as({
|
|
183
|
+
name: task.scenarioName,
|
|
184
|
+
steps: task.pickle.steps.length,
|
|
185
|
+
tags: task.tags
|
|
186
|
+
})
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
const runSteps = <State, E, R>(
|
|
190
|
+
featureDefinition: Feature<State, E, R>,
|
|
191
|
+
scenario: string,
|
|
192
|
+
steps: ReadonlyArray<PickleStep>,
|
|
193
|
+
source: Parser.SourceIndex,
|
|
194
|
+
state: State,
|
|
195
|
+
index = 0
|
|
196
|
+
): Effect.Effect<State, RunError, R> =>
|
|
197
|
+
index >= steps.length
|
|
198
|
+
? Effect.succeed(state)
|
|
199
|
+
: pipe(
|
|
200
|
+
runStep(featureDefinition, scenario, steps[index], source, state),
|
|
201
|
+
Effect.flatMap((state) => runSteps(featureDefinition, scenario, steps, source, state, index + 1))
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
const runStep = <State, E, R>(
|
|
205
|
+
featureDefinition: Feature<State, E, R>,
|
|
206
|
+
scenario: string,
|
|
207
|
+
step: PickleStep,
|
|
208
|
+
source: Parser.SourceIndex,
|
|
209
|
+
state: State
|
|
210
|
+
): Effect.Effect<State, RunError, R> =>
|
|
211
|
+
pipe(
|
|
212
|
+
stepKind(step, source),
|
|
213
|
+
Effect.flatMap((kind) =>
|
|
214
|
+
pipe(
|
|
215
|
+
resolve(featureDefinition, scenario, step, kind, source),
|
|
216
|
+
Effect.flatMap(({ transition, captures }) =>
|
|
217
|
+
pipe(
|
|
218
|
+
decodeArgument(transition, scenario, step, source),
|
|
219
|
+
Effect.flatMap((argument) =>
|
|
220
|
+
pipe(
|
|
221
|
+
transition.run(captures, argument, state),
|
|
222
|
+
Effect.mapError((cause) =>
|
|
223
|
+
new StepError({
|
|
224
|
+
message: `Step failed: ${step.text}`,
|
|
225
|
+
scenario,
|
|
226
|
+
step: step.text,
|
|
227
|
+
line: Parser.stepLine(step, source),
|
|
228
|
+
cause
|
|
229
|
+
})
|
|
230
|
+
)
|
|
231
|
+
)
|
|
232
|
+
)
|
|
233
|
+
)
|
|
234
|
+
)
|
|
235
|
+
)
|
|
236
|
+
)
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
const decodeArgument = <State, E, R>(
|
|
240
|
+
transition: Transition<State, E, R>,
|
|
241
|
+
scenario: string,
|
|
242
|
+
step: PickleStep,
|
|
243
|
+
source: Parser.SourceIndex
|
|
244
|
+
): Effect.Effect<unknown, MatchError> => {
|
|
245
|
+
const candidates = [transition.expression.source]
|
|
246
|
+
if (transition.argument === undefined) {
|
|
247
|
+
return hasStepArgument(step)
|
|
248
|
+
? failMatch(`Step "${step.text}" has an unexpected argument`, scenario, step, source, candidates)
|
|
249
|
+
: Effect.succeed(undefined)
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
if (transition.argument._tag === "TableArg") {
|
|
253
|
+
return step.argument?.dataTable === undefined
|
|
254
|
+
? failMatch(`Step "${step.text}" requires a DataTable`, scenario, step, source, candidates)
|
|
255
|
+
: pipe(
|
|
256
|
+
transition.argument.decode(step.argument.dataTable),
|
|
257
|
+
Effect.mapError((cause) =>
|
|
258
|
+
matchError(`Could not decode DataTable for step "${step.text}"`, scenario, step, source, candidates, cause)
|
|
259
|
+
)
|
|
260
|
+
)
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
return step.argument?.docString === undefined
|
|
264
|
+
? failMatch(`Step "${step.text}" requires a DocString`, scenario, step, source, candidates)
|
|
265
|
+
: pipe(
|
|
266
|
+
transition.argument.decode(step.argument.docString),
|
|
267
|
+
Effect.mapError((cause) =>
|
|
268
|
+
matchError(`Could not decode DocString for step "${step.text}"`, scenario, step, source, candidates, cause)
|
|
269
|
+
)
|
|
270
|
+
)
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
const resolve = <State, E, R>(
|
|
274
|
+
featureDefinition: Feature<State, E, R>,
|
|
275
|
+
scenario: string,
|
|
276
|
+
step: PickleStep,
|
|
277
|
+
kind: "Given" | "When" | "Then",
|
|
278
|
+
source: Parser.SourceIndex
|
|
279
|
+
): Effect.Effect<ResolvedTransition<State, E, R>, MatchError> => {
|
|
280
|
+
const textMatches = Matching.matchingTextTransitions(featureDefinition.transitions, step.text)
|
|
281
|
+
const matches = Matching.matchingKeywordTransitions(textMatches, kind)
|
|
282
|
+
|
|
283
|
+
return pipe(
|
|
284
|
+
matches,
|
|
285
|
+
Arr.match({
|
|
286
|
+
onEmpty: () =>
|
|
287
|
+
textMatches.length === 0
|
|
288
|
+
? failMatch(
|
|
289
|
+
`No transition matched step "${step.text}"`,
|
|
290
|
+
scenario,
|
|
291
|
+
step,
|
|
292
|
+
source,
|
|
293
|
+
Arr.map(featureDefinition.transitions, (transition) => transition.expression.source)
|
|
294
|
+
)
|
|
295
|
+
: failMatch(
|
|
296
|
+
`No ${kind} transition matched step "${step.text}"; matching text exists for ${
|
|
297
|
+
Matching.renderTransitionKinds(Arr.map(textMatches, (match) => match.transition))
|
|
298
|
+
}`,
|
|
299
|
+
scenario,
|
|
300
|
+
step,
|
|
301
|
+
source,
|
|
302
|
+
Arr.map(textMatches, (match) => match.transition.expression.source)
|
|
303
|
+
),
|
|
304
|
+
onNonEmpty: (matches) =>
|
|
305
|
+
matches.length === 1
|
|
306
|
+
? Effect.succeed(Arr.headNonEmpty(matches))
|
|
307
|
+
: failMatch(
|
|
308
|
+
`Multiple transitions matched step "${step.text}"`,
|
|
309
|
+
scenario,
|
|
310
|
+
step,
|
|
311
|
+
source,
|
|
312
|
+
Arr.map(matches, (match) => match.transition.expression.source)
|
|
313
|
+
)
|
|
314
|
+
})
|
|
315
|
+
)
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
const rowObject = (
|
|
319
|
+
headers: ReadonlyArray<string>,
|
|
320
|
+
cells: ReadonlyArray<string>
|
|
321
|
+
): Record<string, string> =>
|
|
322
|
+
pipe(
|
|
323
|
+
headers,
|
|
324
|
+
Arr.map((header, index) => [header, cells[index] ?? ""] as const),
|
|
325
|
+
Record.fromEntries
|
|
326
|
+
)
|
|
327
|
+
|
|
328
|
+
const hasStepArgument = (step: PickleStep): boolean => step.argument !== undefined
|
|
329
|
+
|
|
330
|
+
const stepKind = (
|
|
331
|
+
step: PickleStep,
|
|
332
|
+
source: Parser.SourceIndex
|
|
333
|
+
): Effect.Effect<"Given" | "When" | "Then", ParseError> =>
|
|
334
|
+
pipe(
|
|
335
|
+
Matching.concreteStepKind(step),
|
|
336
|
+
Option.match({
|
|
337
|
+
onNone: () =>
|
|
338
|
+
Effect.fail(
|
|
339
|
+
new ParseError({
|
|
340
|
+
message: `${Parser.stepKeyword(step, source)} found before a Given, When, or Then step`,
|
|
341
|
+
line: Parser.stepLine(step, source),
|
|
342
|
+
column: 1
|
|
343
|
+
})
|
|
344
|
+
),
|
|
345
|
+
onSome: Effect.succeed
|
|
346
|
+
})
|
|
347
|
+
)
|
|
348
|
+
|
|
349
|
+
const failMatch = (
|
|
350
|
+
message: string,
|
|
351
|
+
scenario: string,
|
|
352
|
+
step: PickleStep,
|
|
353
|
+
source: Parser.SourceIndex,
|
|
354
|
+
candidates: ReadonlyArray<string>,
|
|
355
|
+
cause?: unknown
|
|
356
|
+
): Effect.Effect<never, MatchError> => Effect.fail(matchError(message, scenario, step, source, candidates, cause))
|
|
357
|
+
|
|
358
|
+
const matchError = (
|
|
359
|
+
message: string,
|
|
360
|
+
scenario: string,
|
|
361
|
+
step: PickleStep,
|
|
362
|
+
source: Parser.SourceIndex,
|
|
363
|
+
candidates: ReadonlyArray<string>,
|
|
364
|
+
cause?: unknown
|
|
365
|
+
): MatchError =>
|
|
366
|
+
new MatchError({
|
|
367
|
+
message,
|
|
368
|
+
scenario,
|
|
369
|
+
step: step.text,
|
|
370
|
+
line: Parser.stepLine(step, source),
|
|
371
|
+
candidates,
|
|
372
|
+
...(cause === undefined ? {} : { cause })
|
|
373
|
+
})
|
package/src/main.ts
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
/** @internal */
|
|
2
|
+
import * as Effect from "effect/Effect"
|
|
3
|
+
import * as Layer from "effect/Layer"
|
|
4
|
+
import * as Option from "effect/Option"
|
|
5
|
+
import * as CliError from "effect/unstable/cli/CliError"
|
|
6
|
+
import * as Command from "effect/unstable/cli/Command"
|
|
7
|
+
import * as Flag from "effect/unstable/cli/Flag"
|
|
8
|
+
import PackageJson from "../package.json" with { type: "json" }
|
|
9
|
+
import { GlobResolver } from "./internal/cli/glob.ts"
|
|
10
|
+
import type { CliOptions } from "./internal/cli/models.ts"
|
|
11
|
+
import { ModuleLoader } from "./internal/cli/moduleLoader.ts"
|
|
12
|
+
import * as Reporter from "./internal/cli/reporter.ts"
|
|
13
|
+
import * as Runner from "./internal/cli/runner.ts"
|
|
14
|
+
import * as CucumberCompiler from "./internal/cucumberCompiler.ts"
|
|
15
|
+
|
|
16
|
+
const features = Flag.string("features").pipe(
|
|
17
|
+
Flag.withAlias("f"),
|
|
18
|
+
Flag.withDescription("Feature file glob. Can be supplied multiple times."),
|
|
19
|
+
Flag.between(1, Infinity)
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
const steps = Flag.string("steps").pipe(
|
|
23
|
+
Flag.withAlias("s"),
|
|
24
|
+
Flag.withDescription("Step definition module glob. Can be supplied multiple times."),
|
|
25
|
+
Flag.between(1, Infinity)
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
const reporter = Flag.choice("reporter", ["text", "html", "json", "junit"] as const).pipe(
|
|
29
|
+
Flag.withAlias("r"),
|
|
30
|
+
Flag.withDescription("Reporter to run. Can be supplied multiple times."),
|
|
31
|
+
Flag.between(0, Infinity)
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
const outputFileText = Flag.file("output-file.text").pipe(
|
|
35
|
+
Flag.withDescription("File path for the text reporter. Defaults to stdout."),
|
|
36
|
+
Flag.optional
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
const outputFileHtml = Flag.file("output-file.html").pipe(
|
|
40
|
+
Flag.withDescription("File path for the html reporter."),
|
|
41
|
+
Flag.optional
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
const outputFileJson = Flag.file("output-file.json").pipe(
|
|
45
|
+
Flag.withDescription("File path for the json reporter. Defaults to stdout."),
|
|
46
|
+
Flag.optional
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
const outputFileJunit = Flag.file("output-file.junit").pipe(
|
|
50
|
+
Flag.withDescription("File path for the junit reporter."),
|
|
51
|
+
Flag.optional
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
const parallel = Flag.integer("parallel").pipe(
|
|
55
|
+
Flag.withAlias("p"),
|
|
56
|
+
Flag.withDescription("Number of scenarios to run concurrently."),
|
|
57
|
+
Flag.filter((value) => value > 0, (value) => `Expected --parallel to be greater than 0, got ${value}`),
|
|
58
|
+
Flag.withDefault(1)
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
const verbose = Flag.boolean("verbose").pipe(
|
|
62
|
+
Flag.withAlias("v"),
|
|
63
|
+
Flag.withDescription("Print every scenario result instead of only failures and diagnostics.")
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
const tags = Flag.string("tags").pipe(
|
|
67
|
+
Flag.withAlias("t"),
|
|
68
|
+
Flag.withDescription("Cucumber-style tag expression. Can be supplied multiple times."),
|
|
69
|
+
Flag.between(0, Infinity)
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
const name = Flag.string("name").pipe(
|
|
73
|
+
Flag.withAlias("n"),
|
|
74
|
+
Flag.withDescription("Run scenarios whose feature/scenario name contains this text. Can be supplied multiple times."),
|
|
75
|
+
Flag.between(0, Infinity)
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
const failFast = Flag.boolean("fail-fast").pipe(
|
|
79
|
+
Flag.withDescription("Stop after the first failed scenario. Runs sequentially when enabled.")
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
/** @internal */
|
|
83
|
+
export const cli = Command.make(
|
|
84
|
+
"effect-bdd",
|
|
85
|
+
{
|
|
86
|
+
features,
|
|
87
|
+
steps,
|
|
88
|
+
reporter,
|
|
89
|
+
outputFileText,
|
|
90
|
+
outputFileHtml,
|
|
91
|
+
outputFileJson,
|
|
92
|
+
outputFileJunit,
|
|
93
|
+
parallel,
|
|
94
|
+
verbose,
|
|
95
|
+
tags,
|
|
96
|
+
name,
|
|
97
|
+
failFast
|
|
98
|
+
},
|
|
99
|
+
Effect.fnUntraced(function*(
|
|
100
|
+
{
|
|
101
|
+
features,
|
|
102
|
+
steps,
|
|
103
|
+
reporter,
|
|
104
|
+
outputFileText,
|
|
105
|
+
outputFileHtml,
|
|
106
|
+
outputFileJson,
|
|
107
|
+
outputFileJunit,
|
|
108
|
+
parallel,
|
|
109
|
+
verbose,
|
|
110
|
+
tags,
|
|
111
|
+
name,
|
|
112
|
+
failFast
|
|
113
|
+
}
|
|
114
|
+
) {
|
|
115
|
+
const options: CliOptions = {
|
|
116
|
+
features,
|
|
117
|
+
steps,
|
|
118
|
+
reporters: reporter.length === 0 ? ["text"] : reporter,
|
|
119
|
+
outputFiles: {
|
|
120
|
+
...(Option.isSome(outputFileText) ? { text: outputFileText.value } : {}),
|
|
121
|
+
...(Option.isSome(outputFileHtml) ? { html: outputFileHtml.value } : {}),
|
|
122
|
+
...(Option.isSome(outputFileJson) ? { json: outputFileJson.value } : {}),
|
|
123
|
+
...(Option.isSome(outputFileJunit) ? { junit: outputFileJunit.value } : {})
|
|
124
|
+
},
|
|
125
|
+
verbose,
|
|
126
|
+
filters: {
|
|
127
|
+
tags,
|
|
128
|
+
names: name,
|
|
129
|
+
failFast
|
|
130
|
+
},
|
|
131
|
+
parallel
|
|
132
|
+
}
|
|
133
|
+
const reporters = yield* Reporter.makeReporters(options.reporters, options.outputFiles, { verbose }).pipe(
|
|
134
|
+
Effect.mapError(toUserError)
|
|
135
|
+
)
|
|
136
|
+
const result = yield* Runner.run(options).pipe(
|
|
137
|
+
Effect.mapError(toUserError)
|
|
138
|
+
)
|
|
139
|
+
yield* Reporter.emitAll(reporters, result).pipe(
|
|
140
|
+
Effect.mapError(toUserError)
|
|
141
|
+
)
|
|
142
|
+
if (result.summary.failed > 0 || result.diagnostics.length > 0) {
|
|
143
|
+
return yield* Effect.fail(
|
|
144
|
+
new CliError.UserError({
|
|
145
|
+
cause: result.summary.failed > 0
|
|
146
|
+
? `${result.summary.failed} scenario(s) failed`
|
|
147
|
+
: `${result.diagnostics.length} diagnostic(s) reported`
|
|
148
|
+
})
|
|
149
|
+
)
|
|
150
|
+
}
|
|
151
|
+
})
|
|
152
|
+
).pipe(
|
|
153
|
+
Command.withDescription("Run effect-bdd feature files"),
|
|
154
|
+
Command.provide(Layer.mergeAll(GlobResolver.Live, ModuleLoader.Live, CucumberCompiler.Cucumber))
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
/** @internal */
|
|
158
|
+
export const run = Command.run(cli, {
|
|
159
|
+
version: PackageJson.version
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
const toUserError = (error: unknown): CliError.UserError => new CliError.UserError({ cause: renderUserError(error) })
|
|
163
|
+
|
|
164
|
+
const renderUserError = (error: unknown): string => {
|
|
165
|
+
if (typeof error === "object" && error !== null && "message" in error && typeof error.message === "string") {
|
|
166
|
+
return error.message
|
|
167
|
+
}
|
|
168
|
+
return String(error)
|
|
169
|
+
}
|