-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathcommand-builder.ts
More file actions
268 lines (242 loc) · 11.6 KB
/
Copy pathcommand-builder.ts
File metadata and controls
268 lines (242 loc) · 11.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
import * as path from "node:path";
import { TestExecutionOptions, FeatureExecutionOptions } from "../types";
import { ExtensionConfig } from "./extension-config";
import { resolveWorkerCount } from "../commands/prompt-worker-count";
import { shellQuote } from "../utils/shell";
import { Logger } from "../utils/logger";
// Callers sometimes pass "" for an unknown scenario/outline name; treating it as a real name
// would emit --grep "" and run the entire suite.
function nonEmpty(value: string | undefined): string | undefined {
return value !== undefined && value !== "" ? value : undefined;
}
/**
* Builds shell commands to drive playwright-bdd.
*
* playwright-bdd's flow:
* 1. `bddgen` reads .feature files and emits Playwright spec files under .features-gen/ (configurable).
* 2. `playwright test` runs those generated specs.
*
* Newer versions can run codegen automatically via `defineBddProject` in playwright.config.ts,
* in which case `bddgen` is unnecessary — set `playwrightBddRunner.bddgenCommand` to an empty
* string to skip it.
*
* Targeting:
* - Tags → `bddgen --tags "<expr>"` (filters which specs get generated)
* - Scenario → `playwright test --grep "<name>"`
* - Feature → `playwright test --grep "<feature name>"` (or by generated spec path if known)
*
* Playwright-bdd does not support line-number selection the way behave does, so we fall back to
* matching by scenario name via --grep. The line number is informational only.
*/
export class CommandBuilder {
private forceParallel = false;
private forceParallelWorkers: number | undefined;
private _lastForcedWorkers: number | undefined;
constructor(
private readonly config: ExtensionConfig,
private readonly logger: Logger
) {}
public static create(config: ExtensionConfig, logger: Logger): CommandBuilder {
return new CommandBuilder(config, logger);
}
public setForceParallel(value: boolean, workers?: number): void {
this.forceParallel = value;
this.forceParallelWorkers = value ? workers : undefined;
if (value) {
this._lastForcedWorkers = workers;
}
}
public isForceParallel(): boolean {
return this.forceParallel;
}
public get lastForcedWorkers(): number | undefined {
return this._lastForcedWorkers;
}
public buildScenarioCommand(options: TestExecutionOptions): string {
const parts: string[] = [];
const gen = this.buildBddgen(options.tags);
if (gen) {parts.push(gen);}
parts.push(this.buildPlaywright(options, /*greppedByName*/ true));
return parts.join(" && ");
}
/**
* Same as {@link buildScenarioCommand} but split into its bddgen and playwright halves, so the
* executor can run bddgen FIRST and then resolve a precise `<spec>:<pwTestLine>` target from the
* freshly generated spec before running playwright (mirrors {@link buildDebugCommandParts}).
* `bddgenCommand` is undefined when bddgen is disabled (empty config) or generation is delegated
* to `defineBddProject`.
*/
public buildScenarioCommandParts(
options: TestExecutionOptions
): { bddgenCommand: string | undefined; playwrightCommand: string } {
return {
bddgenCommand: this.buildBddgen(options.tags),
playwrightCommand: this.buildPlaywright(options, /*greppedByName*/ true),
};
}
public buildFeatureCommand(options: FeatureExecutionOptions): string {
const parts: string[] = [];
const gen = this.buildBddgen(options.tags);
if (gen) {parts.push(gen);}
const playwrightParts: string[] = [this.config.playwrightCommand];
// Prefer grepping by the Feature title: playwright-bdd names the generated `describe` after
// it, so the title appears verbatim in Playwright's grep target. This is far more precise
// than the filename basename, which matched unrelated features whose titles merely contained
// the filename (file `sample.feature` matched the "Sample feature" of another file). We keep
// it unanchored because Playwright's grep target may be prefixed by the spec file path — an
// `^` anchor would then match nothing. Basename stays as a last-resort fallback.
const grep = options.featureName
? this.gripPattern(options.featureName)
: this.gripPattern(path.basename(options.filePath).replace(/\.feature$/, ""));
if (grep) {playwrightParts.push("--grep", this.quote(grep));}
this.appendCommonFlags(playwrightParts, {
reporter: options.reporter,
parallel: this.config.parallelExecution,
dryRun: options.dryRun ?? this.config.dryRun,
});
parts.push(playwrightParts.join(" "));
return parts.join(" && ");
}
public buildTagCommand(tag: string): string {
const parts: string[] = [];
const gen = this.buildBddgen(tag);
if (gen) {parts.push(gen);}
const playwrightParts: string[] = [this.config.playwrightCommand];
this.appendCommonFlags(playwrightParts, {
reporter: this.config.reporter,
parallel: this.config.parallelExecution,
dryRun: this.config.dryRun,
});
parts.push(playwrightParts.join(" "));
return parts.join(" && ");
}
/**
* Debug command, split into its bddgen and playwright halves. The executor runs bddgen
* itself (so the generated specs exist before breakpoints are mirrored into them) and then
* launches ONLY the playwright half under VS Code's JS debugger via a `node-terminal`
* configuration, so breakpoints in step-definition files are hit. We do NOT add Playwright's
* `--debug` flag here — that opens the Playwright Inspector and pauses there instead of in
* VS Code.
*/
public buildDebugCommandParts(
options: TestExecutionOptions
): { bddgenCommand: string | undefined; playwrightCommand: string } {
const bddgenCommand = this.buildBddgen(options.tags);
const playwrightParts: string[] = [this.config.playwrightCommand];
if (options.specLineTarget) {
// Preferred: target the exact generated test by `<spec>:<pwTestLine>`. This is the only way
// to debug a single Scenario Outline example row (grep on the source title can't isolate one).
playwrightParts.push(this.quote(options.specLineTarget));
} else if (options.scenarioName) {
playwrightParts.push("--grep", this.quote(this.gripPattern(options.scenarioName, options.outlineName)));
} else {
// No specific scenario (e.g. debugging a whole feature file): narrow to the feature's
// generated spec by its basename, mirroring buildFeatureCommand.
const base = path.basename(options.filePath).replace(/\.feature$/, "");
if (base) {playwrightParts.push("--grep", this.quote(this.gripPattern(base)));}
}
if (options.jsonReportPath) {
// The debugged run reports through PLAYWRIGHT_JSON_OUTPUT_NAME (file output); keep the
// user-visible reporter alongside json so the terminal output stays legible.
const reporter = this.config.reporter;
const reporters = reporter ? `${reporter},json` : "json";
playwrightParts.push(`--reporter=${reporters}`);
}
return { bddgenCommand, playwrightCommand: playwrightParts.join(" ") };
}
public buildAllTestsCommand(): string {
const parts: string[] = [];
const gen = this.buildBddgen(this.config.tags);
if (gen) {parts.push(gen);}
const playwrightParts: string[] = [this.config.playwrightCommand];
this.appendCommonFlags(playwrightParts, {
reporter: this.config.reporter,
parallel: this.config.parallelExecution,
dryRun: this.config.dryRun,
});
parts.push(playwrightParts.join(" "));
return parts.join(" && ");
}
/**
* Build the playwright test command for a single scenario; used by both run and debug paths.
*/
private buildPlaywright(options: TestExecutionOptions, greppedByName: boolean): string {
const parts: string[] = [this.config.playwrightCommand];
// Grep by the scenario name, or — when targeting a whole Scenario Outline (the Test Explorer
// outline node passes only `outlineName`) — by the outline name, which matches every expanded
// example row. Without this, an outline run with no scenarioName produced no `--grep` and ran
// the entire suite.
if (greppedByName && options.specLineTarget) {
// Preferred: precise `<spec>:<pwTestLine>` target (see TestExecutionOptions.specLineTarget).
// Falls through to name-grep below only when no spec line could be resolved.
parts.push(this.quote(options.specLineTarget));
} else {
const grepName = nonEmpty(options.scenarioName) ?? nonEmpty(options.outlineName);
if (greppedByName && grepName) {
parts.push("--grep", this.quote(this.gripPattern(grepName, options.outlineName)));
}
}
this.appendCommonFlags(parts, {
reporter: options.reporter,
parallel: this.config.parallelExecution,
dryRun: options.dryRun ?? this.config.dryRun,
});
return parts.join(" ");
}
private buildBddgen(tagExpression?: string): string | undefined {
const cmd = this.config.bddgenCommand.trim();
if (!cmd) {return undefined;}
const effective = tagExpression ?? this.config.tags;
if (effective && effective.trim() !== "") {
return `${cmd} --tags ${this.quote(effective)}`;
}
return cmd;
}
private appendCommonFlags(
parts: string[],
opts: { reporter?: string | undefined; parallel?: boolean | undefined; dryRun?: boolean | undefined }
): void {
if (opts.dryRun) {parts.push("--list");}
if (this.forceParallel) {
const workers = this.forceParallelWorkers ?? resolveWorkerCount(this.config, this.logger);
parts.push(`--workers=${workers}`);
} else if (opts.parallel) {
parts.push(`--workers=${resolveWorkerCount(this.config, this.logger)}`);
}
// When useConfigReporters is set, defer entirely to the reporters declared in the user's
// Playwright config — injecting any `--reporter` here would override them (a CLI --reporter
// replaces the config's reporter array), dropping their custom reporter.
if (this.config.useConfigReporters) {
return;
}
// Always emit the reporter explicitly (including the default `list`). When the executor
// later appends `--reporter=json` for result mapping, Playwright keeps both reporters —
// omitting `list` here would let `--reporter=json` clobber the implicit default and leave
// stdout (and therefore the Test Explorer output panel) empty.
const reporter = opts.reporter ?? this.config.reporter;
if (reporter) {
parts.push(`--reporter=${reporter}`);
}
}
/**
* Escape characters that have meaning in a Playwright --grep regex. When `outlineName` is
* provided, we grep by the outline name verbatim so a single run targets every expanded row
* of that outline.
*/
private gripPattern(scenarioName: string, outlineName?: string): string {
const base = nonEmpty(outlineName) ?? scenarioName;
// Escape regex specials, THEN turn Gherkin `<placeholders>` into `.*` wildcards. playwright-bdd
// expands an outline's example rows into tests whose titles have the placeholders substituted
// (`<role>` → `admin`), so grepping the literal `<role>` only ever matched the parent describe —
// and the `<`/`>` are redirection operators in cmd.exe / PowerShell, which mangled the command
// on Windows and made the run find no tests at all. Wildcarding both fixes the match and drops
// the shell-hostile characters. Order matters: escape first (placeholder names rarely contain
// specials, but the surrounding text may), then substitute the (unescaped) `<...>` tokens.
return base
.replaceAll(/[.*+?^${}()|[\]\\]/g, "\\$&")
.replaceAll(/<[^>]*>/g, ".*");
}
private quote(value: string): string {
return shellQuote(value);
}
}