@vijaypjavvadi/bdd2pw 3.0.0 → 3.2.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/CHANGELOG.md +203 -0
- package/dist/emitters/facade.d.ts +16 -0
- package/dist/emitters/facade.js +29 -3
- package/dist/emitters/facade.js.map +1 -1
- package/dist/index.js +55 -3
- package/dist/index.js.map +1 -1
- package/dist/llm/anthropicClient.js +9 -0
- package/dist/llm/anthropicClient.js.map +1 -1
- package/dist/parser/jsonScenarioParser.d.ts +55 -0
- package/dist/parser/jsonScenarioParser.js +169 -0
- package/dist/parser/jsonScenarioParser.js.map +1 -0
- package/dist/repo/projectScaffolder.d.ts +6 -0
- package/dist/repo/projectScaffolder.js +1 -0
- package/dist/repo/projectScaffolder.js.map +1 -1
- package/dist/reports/metaSidecar.d.ts +60 -0
- package/dist/reports/metaSidecar.js +86 -0
- package/dist/reports/metaSidecar.js.map +1 -0
- package/dist/reports/specMerge.d.ts +60 -0
- package/dist/reports/specMerge.js +131 -0
- package/dist/reports/specMerge.js.map +1 -0
- package/dist/transformers/stepMatcher.js +9 -0
- package/dist/transformers/stepMatcher.js.map +1 -1
- package/dist/transformers/stepNormalizer.js +11 -0
- package/dist/transformers/stepNormalizer.js.map +1 -1
- package/dist/transformers/visibilityRules.d.ts +37 -0
- package/dist/transformers/visibilityRules.js +197 -0
- package/dist/transformers/visibilityRules.js.map +1 -0
- package/dist/types.d.ts +42 -0
- package/package.json +2 -2
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* v3.2.0 — TestForge handoff Issue 8. Accept structured JSON scenarios
|
|
3
|
+
* in addition to .feature files.
|
|
4
|
+
*
|
|
5
|
+
* Input shape (TestForge's TCG output):
|
|
6
|
+
*
|
|
7
|
+
* {
|
|
8
|
+
* "name": "Login with valid credentials",
|
|
9
|
+
* "kind": "ui",
|
|
10
|
+
* "heuristic": "happy path",
|
|
11
|
+
* "preconditions": ["user is registered"],
|
|
12
|
+
* "actions": ["navigate to login page", "fill username", ...],
|
|
13
|
+
* "expected": ["dashboard is visible"],
|
|
14
|
+
* "data": { "username": "student", "password": "Password123" }
|
|
15
|
+
* }
|
|
16
|
+
*
|
|
17
|
+
* Either a single object or an array of objects is accepted at the
|
|
18
|
+
* top level. The converter synthesises Gherkin steps:
|
|
19
|
+
*
|
|
20
|
+
* - preconditions → `Given <text>`
|
|
21
|
+
* - actions → `When <text>` (first), `And <text>` (rest)
|
|
22
|
+
* - expected → `Then <text>` (first), `And <text>` (rest)
|
|
23
|
+
*
|
|
24
|
+
* `data` is appended to each action step that contains a templating
|
|
25
|
+
* placeholder like `<username>`; otherwise it's surfaced as a Gherkin
|
|
26
|
+
* doc-string attached to the first action step.
|
|
27
|
+
*
|
|
28
|
+
* The synthesised FeatureIR is identical in shape to what
|
|
29
|
+
* gherkinParser produces, so downstream code (stepMatcher, emitTestFile)
|
|
30
|
+
* treats both paths uniformly.
|
|
31
|
+
*/
|
|
32
|
+
import type { FeatureIR } from "../types";
|
|
33
|
+
export interface JsonScenario {
|
|
34
|
+
name: string;
|
|
35
|
+
kind?: "ui" | "api" | "mixed" | string;
|
|
36
|
+
heuristic?: string;
|
|
37
|
+
preconditions?: string[];
|
|
38
|
+
actions?: string[];
|
|
39
|
+
expected?: string[];
|
|
40
|
+
data?: Record<string, string | number | boolean>;
|
|
41
|
+
/** Optional explicit tags. If omitted, derived from `kind`. */
|
|
42
|
+
tags?: string[];
|
|
43
|
+
}
|
|
44
|
+
/** Detect whether a file path is a JSON-scenario input. */
|
|
45
|
+
export declare function isJsonScenarioFile(file: string): boolean;
|
|
46
|
+
/**
|
|
47
|
+
* Read a JSON scenarios file and convert it to a FeatureIR. The
|
|
48
|
+
* feature name is derived from the file basename (matching gherkin
|
|
49
|
+
* convention: `login.feature` → "login"). Each top-level scenario in
|
|
50
|
+
* the JSON becomes a ScenarioIR.
|
|
51
|
+
*
|
|
52
|
+
* Returns a fully-formed FeatureIR — sourceFile points at the JSON
|
|
53
|
+
* file so the rest of the pipeline can reference the input correctly.
|
|
54
|
+
*/
|
|
55
|
+
export declare function parseJsonScenarios(filePath: string): Promise<FeatureIR>;
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* v3.2.0 — TestForge handoff Issue 8. Accept structured JSON scenarios
|
|
4
|
+
* in addition to .feature files.
|
|
5
|
+
*
|
|
6
|
+
* Input shape (TestForge's TCG output):
|
|
7
|
+
*
|
|
8
|
+
* {
|
|
9
|
+
* "name": "Login with valid credentials",
|
|
10
|
+
* "kind": "ui",
|
|
11
|
+
* "heuristic": "happy path",
|
|
12
|
+
* "preconditions": ["user is registered"],
|
|
13
|
+
* "actions": ["navigate to login page", "fill username", ...],
|
|
14
|
+
* "expected": ["dashboard is visible"],
|
|
15
|
+
* "data": { "username": "student", "password": "Password123" }
|
|
16
|
+
* }
|
|
17
|
+
*
|
|
18
|
+
* Either a single object or an array of objects is accepted at the
|
|
19
|
+
* top level. The converter synthesises Gherkin steps:
|
|
20
|
+
*
|
|
21
|
+
* - preconditions → `Given <text>`
|
|
22
|
+
* - actions → `When <text>` (first), `And <text>` (rest)
|
|
23
|
+
* - expected → `Then <text>` (first), `And <text>` (rest)
|
|
24
|
+
*
|
|
25
|
+
* `data` is appended to each action step that contains a templating
|
|
26
|
+
* placeholder like `<username>`; otherwise it's surfaced as a Gherkin
|
|
27
|
+
* doc-string attached to the first action step.
|
|
28
|
+
*
|
|
29
|
+
* The synthesised FeatureIR is identical in shape to what
|
|
30
|
+
* gherkinParser produces, so downstream code (stepMatcher, emitTestFile)
|
|
31
|
+
* treats both paths uniformly.
|
|
32
|
+
*/
|
|
33
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
34
|
+
if (k2 === undefined) k2 = k;
|
|
35
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
36
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
37
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
38
|
+
}
|
|
39
|
+
Object.defineProperty(o, k2, desc);
|
|
40
|
+
}) : (function(o, m, k, k2) {
|
|
41
|
+
if (k2 === undefined) k2 = k;
|
|
42
|
+
o[k2] = m[k];
|
|
43
|
+
}));
|
|
44
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
45
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
46
|
+
}) : function(o, v) {
|
|
47
|
+
o["default"] = v;
|
|
48
|
+
});
|
|
49
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
50
|
+
var ownKeys = function(o) {
|
|
51
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
52
|
+
var ar = [];
|
|
53
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
54
|
+
return ar;
|
|
55
|
+
};
|
|
56
|
+
return ownKeys(o);
|
|
57
|
+
};
|
|
58
|
+
return function (mod) {
|
|
59
|
+
if (mod && mod.__esModule) return mod;
|
|
60
|
+
var result = {};
|
|
61
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
62
|
+
__setModuleDefault(result, mod);
|
|
63
|
+
return result;
|
|
64
|
+
};
|
|
65
|
+
})();
|
|
66
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
67
|
+
exports.isJsonScenarioFile = isJsonScenarioFile;
|
|
68
|
+
exports.parseJsonScenarios = parseJsonScenarios;
|
|
69
|
+
const fs = __importStar(require("fs-extra"));
|
|
70
|
+
const path = __importStar(require("path"));
|
|
71
|
+
/** Detect whether a file path is a JSON-scenario input. */
|
|
72
|
+
function isJsonScenarioFile(file) {
|
|
73
|
+
return /\.json$/i.test(file);
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Read a JSON scenarios file and convert it to a FeatureIR. The
|
|
77
|
+
* feature name is derived from the file basename (matching gherkin
|
|
78
|
+
* convention: `login.feature` → "login"). Each top-level scenario in
|
|
79
|
+
* the JSON becomes a ScenarioIR.
|
|
80
|
+
*
|
|
81
|
+
* Returns a fully-formed FeatureIR — sourceFile points at the JSON
|
|
82
|
+
* file so the rest of the pipeline can reference the input correctly.
|
|
83
|
+
*/
|
|
84
|
+
async function parseJsonScenarios(filePath) {
|
|
85
|
+
const raw = await fs.readFile(filePath, "utf8");
|
|
86
|
+
let parsed;
|
|
87
|
+
try {
|
|
88
|
+
parsed = JSON.parse(raw);
|
|
89
|
+
}
|
|
90
|
+
catch (err) {
|
|
91
|
+
throw new Error(`JSON scenarios file ${filePath} is not valid JSON: ${err.message}`);
|
|
92
|
+
}
|
|
93
|
+
const list = Array.isArray(parsed)
|
|
94
|
+
? parsed
|
|
95
|
+
: [parsed];
|
|
96
|
+
const scenarios = list.map(jsonScenarioToScenarioIR);
|
|
97
|
+
const featureName = path.basename(filePath, path.extname(filePath));
|
|
98
|
+
return {
|
|
99
|
+
name: featureName,
|
|
100
|
+
description: undefined,
|
|
101
|
+
background: undefined,
|
|
102
|
+
scenarios,
|
|
103
|
+
tags: [],
|
|
104
|
+
sourceFile: filePath,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Convert one JSON scenario object to a ScenarioIR. The action/expected
|
|
109
|
+
* arrays are flattened into a Gherkin step list following standard BDD
|
|
110
|
+
* convention: first step in each group gets the strong keyword
|
|
111
|
+
* (Given/When/Then), subsequent steps get `And`.
|
|
112
|
+
*/
|
|
113
|
+
function jsonScenarioToScenarioIR(s) {
|
|
114
|
+
if (!s || typeof s !== "object" || typeof s.name !== "string") {
|
|
115
|
+
throw new Error(`Each JSON scenario must have a string "name" field. Received: ${JSON.stringify(s)}`);
|
|
116
|
+
}
|
|
117
|
+
const steps = [];
|
|
118
|
+
for (let i = 0; i < (s.preconditions?.length ?? 0); i++) {
|
|
119
|
+
steps.push({
|
|
120
|
+
keyword: i === 0 ? "Given" : "And",
|
|
121
|
+
text: s.preconditions[i],
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
for (let i = 0; i < (s.actions?.length ?? 0); i++) {
|
|
125
|
+
const text = applyDataTemplating(s.actions[i], s.data);
|
|
126
|
+
steps.push({
|
|
127
|
+
keyword: i === 0 ? "When" : "And",
|
|
128
|
+
text,
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
for (let i = 0; i < (s.expected?.length ?? 0); i++) {
|
|
132
|
+
steps.push({
|
|
133
|
+
keyword: i === 0 ? "Then" : "And",
|
|
134
|
+
text: applyDataTemplating(s.expected[i], s.data),
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
// Tag derivation: explicit `tags` wins; otherwise derive from `kind`
|
|
138
|
+
// so `kind: "api"` produces `@api` automatically. Same for ui/mixed.
|
|
139
|
+
const tags = s.tags ?? deriveTagsFromKind(s.kind);
|
|
140
|
+
return {
|
|
141
|
+
name: s.name,
|
|
142
|
+
steps,
|
|
143
|
+
tags,
|
|
144
|
+
examples: undefined,
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
function deriveTagsFromKind(kind) {
|
|
148
|
+
if (!kind)
|
|
149
|
+
return [];
|
|
150
|
+
if (kind === "mixed")
|
|
151
|
+
return ["@ui", "@api"];
|
|
152
|
+
return [`@${kind}`];
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Replace `<placeholder>` tokens in a step text with values from the
|
|
156
|
+
* scenario's `data` object. Leaves the step alone when no placeholders
|
|
157
|
+
* are present. Same convention as Cucumber's Scenario Outline.
|
|
158
|
+
*/
|
|
159
|
+
function applyDataTemplating(text, data) {
|
|
160
|
+
if (!data)
|
|
161
|
+
return text;
|
|
162
|
+
return text.replace(/<([a-zA-Z_][\w]*)>/g, (full, key) => {
|
|
163
|
+
if (Object.prototype.hasOwnProperty.call(data, key)) {
|
|
164
|
+
return String(data[key]);
|
|
165
|
+
}
|
|
166
|
+
return full; // leave unreplaced placeholders intact
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
//# sourceMappingURL=jsonScenarioParser.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"jsonScenarioParser.js","sourceRoot":"","sources":["../../src/parser/jsonScenarioParser.ts"],"names":[],"mappings":"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"}
|
|
@@ -26,6 +26,12 @@ export interface ScaffoldProjectOptions {
|
|
|
26
26
|
projectName?: string;
|
|
27
27
|
/** When true, also write lib/heal.ts and patch tsconfig paths. */
|
|
28
28
|
selfHealing?: boolean;
|
|
29
|
+
/**
|
|
30
|
+
* v3.2.0 — pin emitted devDependency versions when set to `exact`.
|
|
31
|
+
* Default `caret` matches existing behavior. See TestForge handoff
|
|
32
|
+
* Issue 9 and pw-emit v1.3.0.
|
|
33
|
+
*/
|
|
34
|
+
dependencyStrategy?: "caret" | "exact";
|
|
29
35
|
}
|
|
30
36
|
export interface ScaffoldProjectResult {
|
|
31
37
|
filesWritten: string[];
|
|
@@ -63,6 +63,7 @@ async function scaffoldProject(opts) {
|
|
|
63
63
|
templatesDir: opts.templatesDir,
|
|
64
64
|
baseUrl: opts.baseUrl,
|
|
65
65
|
projectName: opts.projectName,
|
|
66
|
+
dependencyStrategy: opts.dependencyStrategy,
|
|
66
67
|
});
|
|
67
68
|
const filesWritten = [...result.filesWritten];
|
|
68
69
|
const warnings = [...result.warnings];
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"projectScaffolder.js","sourceRoot":"","sources":["../../src/repo/projectScaffolder.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;;;;GAiBG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
1
|
+
{"version":3,"file":"projectScaffolder.js","sourceRoot":"","sources":["../../src/repo/projectScaffolder.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* v3.2.0 — TestForge handoff Issue 10. Emit a `<spec-stem>.spec.meta.json`
|
|
3
|
+
* file alongside each generated `.spec.ts` describing every step's
|
|
4
|
+
* semantic intent. Lets downstream tooling (visual regression, defect
|
|
5
|
+
* analysis, self-healing pipelines) consume bdd2pw's understanding
|
|
6
|
+
* without re-parsing the TypeScript output.
|
|
7
|
+
*
|
|
8
|
+
* Sidecar shape (stable across patch versions):
|
|
9
|
+
*
|
|
10
|
+
* {
|
|
11
|
+
* "version": "<bdd2pw version>",
|
|
12
|
+
* "source": "<.feature path relative to repo>",
|
|
13
|
+
* "scenarios": [
|
|
14
|
+
* {
|
|
15
|
+
* "name": "Login with valid credentials",
|
|
16
|
+
* "tags": ["@positive"],
|
|
17
|
+
* "steps": [
|
|
18
|
+
* { "id": "0001", "keyword": "Given", "text": "...",
|
|
19
|
+
* "intent": "navigation", "locator": "page",
|
|
20
|
+
* "assertion": "goto" },
|
|
21
|
+
* ...
|
|
22
|
+
* ]
|
|
23
|
+
* }
|
|
24
|
+
* ]
|
|
25
|
+
* }
|
|
26
|
+
*
|
|
27
|
+
* `intent` is one of: navigation | interaction | assertion | api |
|
|
28
|
+
* compound | todo. Inferred from the binding shape (pomCall vs
|
|
29
|
+
* assertion vs customBody vs warning). When the binding carries
|
|
30
|
+
* `apiContext`, intent is `api` regardless of which sub-field is set.
|
|
31
|
+
*/
|
|
32
|
+
import type { StepBinding } from "../types";
|
|
33
|
+
export interface MetaSidecarStep {
|
|
34
|
+
id: string;
|
|
35
|
+
keyword: string;
|
|
36
|
+
text: string;
|
|
37
|
+
intent: "navigation" | "interaction" | "assertion" | "api" | "compound" | "todo";
|
|
38
|
+
/** Resolved locator string when the binding has one. */
|
|
39
|
+
locator?: string;
|
|
40
|
+
/** Matcher name or method name when the binding has one. */
|
|
41
|
+
assertion?: string;
|
|
42
|
+
}
|
|
43
|
+
export interface MetaSidecarScenario {
|
|
44
|
+
name: string;
|
|
45
|
+
tags?: string[];
|
|
46
|
+
steps: MetaSidecarStep[];
|
|
47
|
+
}
|
|
48
|
+
export interface MetaSidecar {
|
|
49
|
+
version: string;
|
|
50
|
+
source: string;
|
|
51
|
+
scenarios: MetaSidecarScenario[];
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Convert a list of bindings (one scenario's worth) into the meta
|
|
55
|
+
* sidecar's `steps` array. IDs are zero-padded 4 digits, sequential
|
|
56
|
+
* in source order — same convention as the optional step boundary
|
|
57
|
+
* markers (v3.1.0 Issue 5), so post-processors can correlate the
|
|
58
|
+
* sidecar to the in-source markers when both features are enabled.
|
|
59
|
+
*/
|
|
60
|
+
export declare function bindingsToMetaSteps(bindings: StepBinding[]): MetaSidecarStep[];
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* v3.2.0 — TestForge handoff Issue 10. Emit a `<spec-stem>.spec.meta.json`
|
|
4
|
+
* file alongside each generated `.spec.ts` describing every step's
|
|
5
|
+
* semantic intent. Lets downstream tooling (visual regression, defect
|
|
6
|
+
* analysis, self-healing pipelines) consume bdd2pw's understanding
|
|
7
|
+
* without re-parsing the TypeScript output.
|
|
8
|
+
*
|
|
9
|
+
* Sidecar shape (stable across patch versions):
|
|
10
|
+
*
|
|
11
|
+
* {
|
|
12
|
+
* "version": "<bdd2pw version>",
|
|
13
|
+
* "source": "<.feature path relative to repo>",
|
|
14
|
+
* "scenarios": [
|
|
15
|
+
* {
|
|
16
|
+
* "name": "Login with valid credentials",
|
|
17
|
+
* "tags": ["@positive"],
|
|
18
|
+
* "steps": [
|
|
19
|
+
* { "id": "0001", "keyword": "Given", "text": "...",
|
|
20
|
+
* "intent": "navigation", "locator": "page",
|
|
21
|
+
* "assertion": "goto" },
|
|
22
|
+
* ...
|
|
23
|
+
* ]
|
|
24
|
+
* }
|
|
25
|
+
* ]
|
|
26
|
+
* }
|
|
27
|
+
*
|
|
28
|
+
* `intent` is one of: navigation | interaction | assertion | api |
|
|
29
|
+
* compound | todo. Inferred from the binding shape (pomCall vs
|
|
30
|
+
* assertion vs customBody vs warning). When the binding carries
|
|
31
|
+
* `apiContext`, intent is `api` regardless of which sub-field is set.
|
|
32
|
+
*/
|
|
33
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
34
|
+
exports.bindingsToMetaSteps = bindingsToMetaSteps;
|
|
35
|
+
/**
|
|
36
|
+
* Classify a step binding's intent. Order of checks matters:
|
|
37
|
+
* 1. apiContext flag → "api" (overrides every other shape since API
|
|
38
|
+
* bindings often carry customBody + assertion together).
|
|
39
|
+
* 2. warning-only → "todo".
|
|
40
|
+
* 3. pomCall with method "goto" → "navigation".
|
|
41
|
+
* 4. pomCall otherwise → "interaction".
|
|
42
|
+
* 5. assertion → "assertion".
|
|
43
|
+
* 6. customBody → "compound".
|
|
44
|
+
* 7. fallback → "todo".
|
|
45
|
+
*/
|
|
46
|
+
function classifyIntent(b) {
|
|
47
|
+
if (b.apiContext)
|
|
48
|
+
return "api";
|
|
49
|
+
if (!b.pomCall && !b.assertion && !b.customBody && b.warning)
|
|
50
|
+
return "todo";
|
|
51
|
+
if (b.pomCall) {
|
|
52
|
+
return b.pomCall.method === "goto" ? "navigation" : "interaction";
|
|
53
|
+
}
|
|
54
|
+
if (b.assertion)
|
|
55
|
+
return "assertion";
|
|
56
|
+
if (b.customBody)
|
|
57
|
+
return "compound";
|
|
58
|
+
return "todo";
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Convert a list of bindings (one scenario's worth) into the meta
|
|
62
|
+
* sidecar's `steps` array. IDs are zero-padded 4 digits, sequential
|
|
63
|
+
* in source order — same convention as the optional step boundary
|
|
64
|
+
* markers (v3.1.0 Issue 5), so post-processors can correlate the
|
|
65
|
+
* sidecar to the in-source markers when both features are enabled.
|
|
66
|
+
*/
|
|
67
|
+
function bindingsToMetaSteps(bindings) {
|
|
68
|
+
return bindings.map((b, idx) => {
|
|
69
|
+
const step = {
|
|
70
|
+
id: String(idx + 1).padStart(4, "0"),
|
|
71
|
+
keyword: b.step.keyword,
|
|
72
|
+
text: b.step.text,
|
|
73
|
+
intent: classifyIntent(b),
|
|
74
|
+
};
|
|
75
|
+
if (b.pomCall) {
|
|
76
|
+
step.locator = `${b.pomCall.page}.${b.pomCall.method}`;
|
|
77
|
+
step.assertion = b.pomCall.method;
|
|
78
|
+
}
|
|
79
|
+
else if (b.assertion) {
|
|
80
|
+
step.locator = b.assertion.locator;
|
|
81
|
+
step.assertion = b.assertion.matcher;
|
|
82
|
+
}
|
|
83
|
+
return step;
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
//# sourceMappingURL=metaSidecar.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"metaSidecar.js","sourceRoot":"","sources":["../../src/reports/metaSidecar.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;;AAwDH,kDAmBC;AAhDD;;;;;;;;;;GAUG;AACH,SAAS,cAAc,CAAC,CAAc;IACpC,IAAI,CAAC,CAAC,UAAU;QAAE,OAAO,KAAK,CAAC;IAC/B,IAAI,CAAC,CAAC,CAAC,OAAO,IAAI,CAAC,CAAC,CAAC,SAAS,IAAI,CAAC,CAAC,CAAC,UAAU,IAAI,CAAC,CAAC,OAAO;QAAE,OAAO,MAAM,CAAC;IAC5E,IAAI,CAAC,CAAC,OAAO,EAAE,CAAC;QACd,OAAO,CAAC,CAAC,OAAO,CAAC,MAAM,KAAK,MAAM,CAAC,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,aAAa,CAAC;IACpE,CAAC;IACD,IAAI,CAAC,CAAC,SAAS;QAAE,OAAO,WAAW,CAAC;IACpC,IAAI,CAAC,CAAC,UAAU;QAAE,OAAO,UAAU,CAAC;IACpC,OAAO,MAAM,CAAC;AAChB,CAAC;AAED;;;;;;GAMG;AACH,SAAgB,mBAAmB,CACjC,QAAuB;IAEvB,OAAO,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,GAAG,EAAE,EAAE;QAC7B,MAAM,IAAI,GAAoB;YAC5B,EAAE,EAAE,MAAM,CAAC,GAAG,GAAG,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,EAAE,GAAG,CAAC;YACpC,OAAO,EAAE,CAAC,CAAC,IAAI,CAAC,OAAO;YACvB,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC,IAAI;YACjB,MAAM,EAAE,cAAc,CAAC,CAAC,CAAC;SAC1B,CAAC;QACF,IAAI,CAAC,CAAC,OAAO,EAAE,CAAC;YACd,IAAI,CAAC,OAAO,GAAG,GAAG,CAAC,CAAC,OAAO,CAAC,IAAI,IAAI,CAAC,CAAC,OAAO,CAAC,MAAM,EAAE,CAAC;YACvD,IAAI,CAAC,SAAS,GAAG,CAAC,CAAC,OAAO,CAAC,MAAM,CAAC;QACpC,CAAC;aAAM,IAAI,CAAC,CAAC,SAAS,EAAE,CAAC;YACvB,IAAI,CAAC,OAAO,GAAG,CAAC,CAAC,SAAS,CAAC,OAAO,CAAC;YACnC,IAAI,CAAC,SAAS,GAAG,CAAC,CAAC,SAAS,CAAC,OAAO,CAAC;QACvC,CAAC;QACD,OAAO,IAAI,CAAC;IACd,CAAC,CAAC,CAAC;AACL,CAAC"}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* v3.2.0 — TestForge handoff Issue 7. Preserve user-edited blocks
|
|
3
|
+
* across regeneration.
|
|
4
|
+
*
|
|
5
|
+
* Two artefacts make this work:
|
|
6
|
+
*
|
|
7
|
+
* 1. **A generated-header marker.** Every spec emitted with `merge`
|
|
8
|
+
* enabled gets a leading
|
|
9
|
+
* `// bdd2pw:generated v=<version> source=<feature>` comment, so a
|
|
10
|
+
* future run can recognise its own output.
|
|
11
|
+
*
|
|
12
|
+
* 2. **User-block markers.** Anywhere in the spec, the user can wrap
|
|
13
|
+
* hand-edited code in:
|
|
14
|
+
*
|
|
15
|
+
* // bdd2pw:user-block id="custom-locator"
|
|
16
|
+
* await this.special = page.locator("[data-thing]");
|
|
17
|
+
* // bdd2pw:end-user-block
|
|
18
|
+
*
|
|
19
|
+
* During regeneration, blocks with the same `id` are spliced back
|
|
20
|
+
* into the new output. Blocks whose id is absent from the new
|
|
21
|
+
* output are appended at end of file under a clearly-labelled
|
|
22
|
+
* "stale user blocks" section so nothing is lost silently.
|
|
23
|
+
*
|
|
24
|
+
* The merge is conservative — it never touches the LINES INSIDE
|
|
25
|
+
* user blocks; everything else is fresh output.
|
|
26
|
+
*
|
|
27
|
+
* Why id-keyed splicing instead of line-based diff? Line numbers
|
|
28
|
+
* shift every time the input feature changes. Id-keyed markers
|
|
29
|
+
* survive arbitrary reordering and renaming.
|
|
30
|
+
*/
|
|
31
|
+
/** Detect whether a previously-emitted spec was produced with merge on. */
|
|
32
|
+
export declare function isMergeAnnotated(existing: string): boolean;
|
|
33
|
+
/** Extract every user-block in `existing`, keyed by id. */
|
|
34
|
+
export declare function extractUserBlocks(existing: string): Map<string, string>;
|
|
35
|
+
/**
|
|
36
|
+
* Splice preserved user blocks into a freshly-rendered spec.
|
|
37
|
+
*
|
|
38
|
+
* - `next` is the new bdd2pw output (no user blocks yet).
|
|
39
|
+
* - `preserved` is the id → block-text map extracted from the
|
|
40
|
+
* PREVIOUS spec via `extractUserBlocks`.
|
|
41
|
+
*
|
|
42
|
+
* For each preserved block: if `next` contains a placeholder line
|
|
43
|
+
* `// bdd2pw:user-block id="<id>"` ... `// bdd2pw:end-user-block`
|
|
44
|
+
* (which bdd2pw never emits by itself — see note below), replace it.
|
|
45
|
+
* Otherwise append the block under a "stale user blocks" footer so
|
|
46
|
+
* the user can salvage / delete it on their own.
|
|
47
|
+
*
|
|
48
|
+
* Note: bdd2pw doesn't insert empty user-block placeholders by
|
|
49
|
+
* default — users opt in by editing a generated spec and adding the
|
|
50
|
+
* markers themselves. So in practice the splice path here is
|
|
51
|
+
* usually the "append at end" branch, which is intentional — once
|
|
52
|
+
* a user edits a spec under merge, the markers they added live
|
|
53
|
+
* forever inside the spec until they remove them.
|
|
54
|
+
*/
|
|
55
|
+
export declare function mergeUserBlocks(next: string, preserved: Map<string, string>): string;
|
|
56
|
+
/**
|
|
57
|
+
* Prepend the generated header to a fresh spec so future merges can
|
|
58
|
+
* detect that the file is bdd2pw-managed.
|
|
59
|
+
*/
|
|
60
|
+
export declare function prependGeneratedHeader(contents: string, version: string, source: string): string;
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* v3.2.0 — TestForge handoff Issue 7. Preserve user-edited blocks
|
|
4
|
+
* across regeneration.
|
|
5
|
+
*
|
|
6
|
+
* Two artefacts make this work:
|
|
7
|
+
*
|
|
8
|
+
* 1. **A generated-header marker.** Every spec emitted with `merge`
|
|
9
|
+
* enabled gets a leading
|
|
10
|
+
* `// bdd2pw:generated v=<version> source=<feature>` comment, so a
|
|
11
|
+
* future run can recognise its own output.
|
|
12
|
+
*
|
|
13
|
+
* 2. **User-block markers.** Anywhere in the spec, the user can wrap
|
|
14
|
+
* hand-edited code in:
|
|
15
|
+
*
|
|
16
|
+
* // bdd2pw:user-block id="custom-locator"
|
|
17
|
+
* await this.special = page.locator("[data-thing]");
|
|
18
|
+
* // bdd2pw:end-user-block
|
|
19
|
+
*
|
|
20
|
+
* During regeneration, blocks with the same `id` are spliced back
|
|
21
|
+
* into the new output. Blocks whose id is absent from the new
|
|
22
|
+
* output are appended at end of file under a clearly-labelled
|
|
23
|
+
* "stale user blocks" section so nothing is lost silently.
|
|
24
|
+
*
|
|
25
|
+
* The merge is conservative — it never touches the LINES INSIDE
|
|
26
|
+
* user blocks; everything else is fresh output.
|
|
27
|
+
*
|
|
28
|
+
* Why id-keyed splicing instead of line-based diff? Line numbers
|
|
29
|
+
* shift every time the input feature changes. Id-keyed markers
|
|
30
|
+
* survive arbitrary reordering and renaming.
|
|
31
|
+
*/
|
|
32
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
33
|
+
exports.isMergeAnnotated = isMergeAnnotated;
|
|
34
|
+
exports.extractUserBlocks = extractUserBlocks;
|
|
35
|
+
exports.mergeUserBlocks = mergeUserBlocks;
|
|
36
|
+
exports.prependGeneratedHeader = prependGeneratedHeader;
|
|
37
|
+
/** Match a user-block open marker plus capture its id and body. */
|
|
38
|
+
const USER_BLOCK_RE = /\/\/\s*bdd2pw:user-block\s+id\s*=\s*"([^"]+)"[ \t]*\r?\n([\s\S]*?)\/\/\s*bdd2pw:end-user-block/g;
|
|
39
|
+
/** Header comment we emit at the top of merge-enabled specs. */
|
|
40
|
+
function generatedHeader(version, source) {
|
|
41
|
+
return `// bdd2pw:generated v=${version} source=${source}`;
|
|
42
|
+
}
|
|
43
|
+
/** Detect whether a previously-emitted spec was produced with merge on. */
|
|
44
|
+
function isMergeAnnotated(existing) {
|
|
45
|
+
return /^\/\/\s*bdd2pw:generated\s/.test(existing.trimStart());
|
|
46
|
+
}
|
|
47
|
+
/** Extract every user-block in `existing`, keyed by id. */
|
|
48
|
+
function extractUserBlocks(existing) {
|
|
49
|
+
const out = new Map();
|
|
50
|
+
const re = new RegExp(USER_BLOCK_RE.source, "g");
|
|
51
|
+
let m;
|
|
52
|
+
while ((m = re.exec(existing)) !== null) {
|
|
53
|
+
const id = m[1];
|
|
54
|
+
const body = m[2];
|
|
55
|
+
// Stash the WHOLE block including markers so the splice keeps
|
|
56
|
+
// them in the output. (If we only captured `body`, a subsequent
|
|
57
|
+
// regen would lose the markers and the next regen wouldn't see
|
|
58
|
+
// the block.)
|
|
59
|
+
const full = m[0];
|
|
60
|
+
out.set(id, full);
|
|
61
|
+
}
|
|
62
|
+
return out;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Splice preserved user blocks into a freshly-rendered spec.
|
|
66
|
+
*
|
|
67
|
+
* - `next` is the new bdd2pw output (no user blocks yet).
|
|
68
|
+
* - `preserved` is the id → block-text map extracted from the
|
|
69
|
+
* PREVIOUS spec via `extractUserBlocks`.
|
|
70
|
+
*
|
|
71
|
+
* For each preserved block: if `next` contains a placeholder line
|
|
72
|
+
* `// bdd2pw:user-block id="<id>"` ... `// bdd2pw:end-user-block`
|
|
73
|
+
* (which bdd2pw never emits by itself — see note below), replace it.
|
|
74
|
+
* Otherwise append the block under a "stale user blocks" footer so
|
|
75
|
+
* the user can salvage / delete it on their own.
|
|
76
|
+
*
|
|
77
|
+
* Note: bdd2pw doesn't insert empty user-block placeholders by
|
|
78
|
+
* default — users opt in by editing a generated spec and adding the
|
|
79
|
+
* markers themselves. So in practice the splice path here is
|
|
80
|
+
* usually the "append at end" branch, which is intentional — once
|
|
81
|
+
* a user edits a spec under merge, the markers they added live
|
|
82
|
+
* forever inside the spec until they remove them.
|
|
83
|
+
*/
|
|
84
|
+
function mergeUserBlocks(next, preserved) {
|
|
85
|
+
if (preserved.size === 0)
|
|
86
|
+
return next;
|
|
87
|
+
// For each preserved id, replace any matching open/close marker in
|
|
88
|
+
// `next`, otherwise queue for append.
|
|
89
|
+
let merged = next;
|
|
90
|
+
const stale = [];
|
|
91
|
+
for (const [id, block] of preserved.entries()) {
|
|
92
|
+
const placeholder = new RegExp(`\\/\\/\\s*bdd2pw:user-block\\s+id\\s*=\\s*"${escapeForRegex(id)}"[ \\t]*\\r?\\n[\\s\\S]*?\\/\\/\\s*bdd2pw:end-user-block`, "g");
|
|
93
|
+
if (placeholder.test(merged)) {
|
|
94
|
+
merged = merged.replace(placeholder, block);
|
|
95
|
+
}
|
|
96
|
+
else {
|
|
97
|
+
stale.push({ id, block });
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
if (stale.length === 0)
|
|
101
|
+
return merged;
|
|
102
|
+
const footer = [
|
|
103
|
+
"",
|
|
104
|
+
"// ── bdd2pw:stale-user-blocks ──────────────────────────────",
|
|
105
|
+
"// The following user-block sections existed in the previous",
|
|
106
|
+
"// generated spec but no matching id was found in the freshly-",
|
|
107
|
+
"// generated output. They're preserved here so you don't lose",
|
|
108
|
+
"// them. Move them inline (or delete them) and the next merge",
|
|
109
|
+
"// will pick up the change.",
|
|
110
|
+
...stale.map((s) => s.block),
|
|
111
|
+
"// ── bdd2pw:end-stale-user-blocks ──────────────────────────",
|
|
112
|
+
"",
|
|
113
|
+
].join("\n");
|
|
114
|
+
return merged + footer;
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Prepend the generated header to a fresh spec so future merges can
|
|
118
|
+
* detect that the file is bdd2pw-managed.
|
|
119
|
+
*/
|
|
120
|
+
function prependGeneratedHeader(contents, version, source) {
|
|
121
|
+
const header = generatedHeader(version, source);
|
|
122
|
+
// Idempotent: if the contents already start with our header, don't
|
|
123
|
+
// duplicate it.
|
|
124
|
+
if (isMergeAnnotated(contents))
|
|
125
|
+
return contents;
|
|
126
|
+
return `${header}\n${contents}`;
|
|
127
|
+
}
|
|
128
|
+
function escapeForRegex(s) {
|
|
129
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
130
|
+
}
|
|
131
|
+
//# sourceMappingURL=specMerge.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"specMerge.js","sourceRoot":"","sources":["../../src/reports/specMerge.ts"],"names":[],"mappings":"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"}
|
|
@@ -22,6 +22,7 @@ const naming_1 = require("../utils/naming");
|
|
|
22
22
|
const stepNormalizer_1 = require("./stepNormalizer");
|
|
23
23
|
const stepNormalizer_2 = require("./stepNormalizer");
|
|
24
24
|
const apiRules_1 = require("./apiRules");
|
|
25
|
+
const visibilityRules_1 = require("./visibilityRules");
|
|
25
26
|
/**
|
|
26
27
|
* Match every step in every scenario to a POM call / assertion / TODO.
|
|
27
28
|
* Background steps are also threaded through if the orchestrator passes
|
|
@@ -85,6 +86,14 @@ const RULES = [
|
|
|
85
86
|
// at the top guarantees deterministic interception before any UI rule
|
|
86
87
|
// accidentally chews on an API-ish phrase. See src/transformers/apiRules.ts.
|
|
87
88
|
...apiRules_1.API_RULES,
|
|
89
|
+
// v3.1.0 — visibility rules go SECOND. TestForge handoff Issue 1:
|
|
90
|
+
// steps like "the user's name or profile indicator is visible in the
|
|
91
|
+
// UI" were falling through to the URL-slug rules and producing
|
|
92
|
+
// always-failing `toHaveURL(/.../)` assertions. These rules intercept
|
|
93
|
+
// anything matching "<noun> is visible/displayed/shown/appears" and
|
|
94
|
+
// either resolve a POM locator or land a clean TODO — never a URL
|
|
95
|
+
// regex. See src/transformers/visibilityRules.ts.
|
|
96
|
+
...visibilityRules_1.VISIBILITY_RULES,
|
|
88
97
|
// 1. Navigation: "I am on the login page" / "I navigate to /foo"
|
|
89
98
|
{
|
|
90
99
|
pattern: new RegExp(`^(?:${SUBJ} )?(?:am on|navigate to|navigates? to|open|opens?|visit|visits?|go to|goes? to|am at|is at) (?:the )?(?:.+?\\s+)?["']?([^"']+?)["']?(?: page)?$`, "i"),
|