@sun-asterisk/sungen 3.2.9 → 3.2.10-beta.1
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/dist/capabilities/discover.js +1 -1
- package/dist/capabilities/discover.js.map +1 -1
- package/dist/capabilities/registry.d.ts +9 -0
- package/dist/capabilities/registry.d.ts.map +1 -1
- package/dist/capabilities/registry.js +4 -0
- package/dist/capabilities/registry.js.map +1 -1
- package/dist/cli/commands/delivery.d.ts.map +1 -1
- package/dist/cli/commands/delivery.js +15 -5
- package/dist/cli/commands/delivery.js.map +1 -1
- package/dist/cli/commands/generate.d.ts.map +1 -1
- package/dist/cli/commands/generate.js +16 -0
- package/dist/cli/commands/generate.js.map +1 -1
- package/dist/exporters/spec-parser.d.ts.map +1 -1
- package/dist/exporters/spec-parser.js +16 -5
- package/dist/exporters/spec-parser.js.map +1 -1
- package/dist/generators/test-generator/adapters/playwright/templates/steps/actions/alert-accept-action.hbs +1 -1
- package/dist/generators/test-generator/adapters/playwright/templates/steps/actions/alert-dismiss-action.hbs +1 -1
- package/dist/generators/test-generator/adapters/playwright/templates/steps/actions/alert-fill-action.hbs +1 -1
- package/dist/generators/test-generator/adapters/playwright/templates/steps/actions/capture-row-column.hbs +2 -0
- package/dist/generators/test-generator/adapters/playwright/templates/steps/actions/click-element-with-text.hbs +1 -1
- package/dist/generators/test-generator/adapters/playwright/templates/steps/actions/click-with-alert-action.hbs +2 -0
- package/dist/generators/test-generator/adapters/playwright/templates/steps/actions/recipe-sequence.hbs +3 -0
- package/dist/generators/test-generator/adapters/playwright/templates/steps/actions/table-action-in-row-nth.hbs +2 -0
- package/dist/generators/test-generator/adapters/playwright/templates/steps/actions/unknown-element-action.hbs +1 -1
- package/dist/generators/test-generator/adapters/playwright/templates/steps/navigation/route-assertion.hbs +3 -0
- package/dist/generators/test-generator/adapters/playwright/templates/steps/navigation/wait-for-element.hbs +1 -1
- package/dist/generators/test-generator/adapters/playwright/templates/steps/navigation/wait-table-refresh.hbs +3 -0
- package/dist/generators/test-generator/adapters/playwright/templates/steps/partials/has-text-value.hbs +1 -0
- package/dist/generators/test-generator/adapters/playwright/templates/steps/partials/locator-base.hbs +1 -1
- package/dist/generators/test-generator/adapters/playwright/templates/steps/partials/locator.hbs +1 -1
- package/dist/generators/test-generator/adapters/playwright/templates/steps/partials/recipe-blur.hbs +1 -0
- package/dist/generators/test-generator/adapters/playwright/templates/steps/partials/recipe-click-text.hbs +3 -0
- package/dist/generators/test-generator/adapters/playwright/templates/steps/partials/recipe-click.hbs +1 -0
- package/dist/generators/test-generator/adapters/playwright/templates/steps/partials/recipe-expect-text.hbs +1 -0
- package/dist/generators/test-generator/adapters/playwright/templates/steps/partials/recipe-expect-value.hbs +1 -0
- package/dist/generators/test-generator/adapters/playwright/templates/steps/partials/recipe-for-each.hbs +5 -0
- package/dist/generators/test-generator/adapters/playwright/templates/steps/partials/recipe-hover.hbs +1 -0
- package/dist/generators/test-generator/adapters/playwright/templates/steps/partials/recipe-press.hbs +1 -0
- package/dist/generators/test-generator/adapters/playwright/templates/steps/partials/recipe-scroll-into-view.hbs +1 -0
- package/dist/generators/test-generator/adapters/playwright/templates/steps/partials/recipe-type.hbs +1 -0
- package/dist/generators/test-generator/adapters/playwright/templates/steps/partials/recipe-wait-for.hbs +1 -0
- package/dist/generators/test-generator/app-profile.d.ts +89 -0
- package/dist/generators/test-generator/app-profile.d.ts.map +1 -0
- package/dist/generators/test-generator/app-profile.js +157 -0
- package/dist/generators/test-generator/app-profile.js.map +1 -0
- package/dist/generators/test-generator/code-generator.d.ts +4 -0
- package/dist/generators/test-generator/code-generator.d.ts.map +1 -1
- package/dist/generators/test-generator/code-generator.js +7 -0
- package/dist/generators/test-generator/code-generator.js.map +1 -1
- package/dist/generators/test-generator/diagnostics.d.ts +36 -0
- package/dist/generators/test-generator/diagnostics.d.ts.map +1 -0
- package/dist/generators/test-generator/diagnostics.js +44 -0
- package/dist/generators/test-generator/diagnostics.js.map +1 -0
- package/dist/generators/test-generator/patterns/types.d.ts +12 -0
- package/dist/generators/test-generator/patterns/types.d.ts.map +1 -1
- package/dist/generators/test-generator/recipe-expander.d.ts +32 -0
- package/dist/generators/test-generator/recipe-expander.d.ts.map +1 -0
- package/dist/generators/test-generator/recipe-expander.js +118 -0
- package/dist/generators/test-generator/recipe-expander.js.map +1 -0
- package/dist/generators/test-generator/step-mapper.d.ts +17 -0
- package/dist/generators/test-generator/step-mapper.d.ts.map +1 -1
- package/dist/generators/test-generator/step-mapper.js +70 -0
- package/dist/generators/test-generator/step-mapper.js.map +1 -1
- package/dist/generators/test-generator/template-engine.d.ts +5 -1
- package/dist/generators/test-generator/template-engine.d.ts.map +1 -1
- package/dist/generators/test-generator/template-engine.js +27 -3
- package/dist/generators/test-generator/template-engine.js.map +1 -1
- package/dist/generators/test-generator/utils/selector-resolver.d.ts +10 -1
- package/dist/generators/test-generator/utils/selector-resolver.d.ts.map +1 -1
- package/dist/generators/test-generator/utils/selector-resolver.js +32 -0
- package/dist/generators/test-generator/utils/selector-resolver.js.map +1 -1
- package/dist/harness/catalog/drivers.yaml +1 -1
- package/dist/harness/query-catalog.d.ts +32 -2
- package/dist/harness/query-catalog.d.ts.map +1 -1
- package/dist/harness/query-catalog.js +0 -0
- package/dist/harness/query-catalog.js.map +1 -1
- package/dist/index.d.ts +5 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -1
- package/dist/index.js.map +1 -1
- package/dist/orchestrator/templates/ai-src/commands/create-data-test.md +83 -0
- package/dist/orchestrator/templates/ai-src/config/claude.md +3 -1
- package/dist/orchestrator/templates/ai-src/config/copilot.md +3 -1
- package/dist/orchestrator/templates/ai-src/skills/sungen-data-factory/SKILL.md +93 -0
- package/dist/orchestrator/templates/ai-src/skills/sungen-error-mapping/SKILL.md +12 -1
- package/dist/orchestrator/templates/ai-src/skills/sungen-gherkin-syntax/SKILL.md +43 -8
- package/dist/orchestrator/templates/ai-src/skills/sungen-selector-fix/SKILL.md +22 -0
- package/dist/orchestrator/templates/ai-src/skills/sungen-selector-keys/SKILL.md +14 -1
- package/dist/orchestrator/templates/specs-db.d.ts +201 -11
- package/dist/orchestrator/templates/specs-db.d.ts.map +1 -1
- package/dist/orchestrator/templates/specs-db.js +440 -91
- package/dist/orchestrator/templates/specs-db.js.map +1 -1
- package/dist/orchestrator/templates/specs-db.ts +463 -79
- package/dist/orchestrator/templates/specs-test-data.ts +39 -3
- package/package.json +7 -4
- package/src/capabilities/discover.ts +1 -1
- package/src/capabilities/registry.ts +13 -0
- package/src/cli/commands/delivery.ts +13 -5
- package/src/cli/commands/generate.ts +15 -0
- package/src/exporters/spec-parser.ts +17 -5
- package/src/generators/test-generator/adapters/playwright/templates/steps/actions/alert-accept-action.hbs +1 -1
- package/src/generators/test-generator/adapters/playwright/templates/steps/actions/alert-dismiss-action.hbs +1 -1
- package/src/generators/test-generator/adapters/playwright/templates/steps/actions/alert-fill-action.hbs +1 -1
- package/src/generators/test-generator/adapters/playwright/templates/steps/actions/capture-row-column.hbs +2 -0
- package/src/generators/test-generator/adapters/playwright/templates/steps/actions/click-element-with-text.hbs +1 -1
- package/src/generators/test-generator/adapters/playwright/templates/steps/actions/click-with-alert-action.hbs +2 -0
- package/src/generators/test-generator/adapters/playwright/templates/steps/actions/recipe-sequence.hbs +3 -0
- package/src/generators/test-generator/adapters/playwright/templates/steps/actions/table-action-in-row-nth.hbs +2 -0
- package/src/generators/test-generator/adapters/playwright/templates/steps/actions/unknown-element-action.hbs +1 -1
- package/src/generators/test-generator/adapters/playwright/templates/steps/navigation/route-assertion.hbs +3 -0
- package/src/generators/test-generator/adapters/playwright/templates/steps/navigation/wait-for-element.hbs +1 -1
- package/src/generators/test-generator/adapters/playwright/templates/steps/navigation/wait-table-refresh.hbs +3 -0
- package/src/generators/test-generator/adapters/playwright/templates/steps/partials/has-text-value.hbs +1 -0
- package/src/generators/test-generator/adapters/playwright/templates/steps/partials/locator-base.hbs +1 -1
- package/src/generators/test-generator/adapters/playwright/templates/steps/partials/locator.hbs +1 -1
- package/src/generators/test-generator/adapters/playwright/templates/steps/partials/recipe-blur.hbs +1 -0
- package/src/generators/test-generator/adapters/playwright/templates/steps/partials/recipe-click-text.hbs +3 -0
- package/src/generators/test-generator/adapters/playwright/templates/steps/partials/recipe-click.hbs +1 -0
- package/src/generators/test-generator/adapters/playwright/templates/steps/partials/recipe-expect-text.hbs +1 -0
- package/src/generators/test-generator/adapters/playwright/templates/steps/partials/recipe-expect-value.hbs +1 -0
- package/src/generators/test-generator/adapters/playwright/templates/steps/partials/recipe-for-each.hbs +5 -0
- package/src/generators/test-generator/adapters/playwright/templates/steps/partials/recipe-hover.hbs +1 -0
- package/src/generators/test-generator/adapters/playwright/templates/steps/partials/recipe-press.hbs +1 -0
- package/src/generators/test-generator/adapters/playwright/templates/steps/partials/recipe-scroll-into-view.hbs +1 -0
- package/src/generators/test-generator/adapters/playwright/templates/steps/partials/recipe-type.hbs +1 -0
- package/src/generators/test-generator/adapters/playwright/templates/steps/partials/recipe-wait-for.hbs +1 -0
- package/src/generators/test-generator/app-profile.ts +185 -0
- package/src/generators/test-generator/code-generator.ts +9 -0
- package/src/generators/test-generator/diagnostics.ts +51 -0
- package/src/generators/test-generator/patterns/types.ts +12 -0
- package/src/generators/test-generator/recipe-expander.ts +129 -0
- package/src/generators/test-generator/step-mapper.ts +73 -0
- package/src/generators/test-generator/template-engine.ts +29 -3
- package/src/generators/test-generator/utils/selector-resolver.ts +49 -2
- package/src/harness/catalog/drivers.yaml +1 -1
- package/src/harness/query-catalog.ts +0 -0
- package/src/index.ts +5 -2
- package/src/orchestrator/templates/ai-src/commands/create-data-test.md +83 -0
- package/src/orchestrator/templates/ai-src/config/claude.md +3 -1
- package/src/orchestrator/templates/ai-src/config/copilot.md +3 -1
- package/src/orchestrator/templates/ai-src/skills/sungen-data-factory/SKILL.md +93 -0
- package/src/orchestrator/templates/ai-src/skills/sungen-error-mapping/SKILL.md +12 -1
- package/src/orchestrator/templates/ai-src/skills/sungen-gherkin-syntax/SKILL.md +43 -8
- package/src/orchestrator/templates/ai-src/skills/sungen-selector-fix/SKILL.md +22 -0
- package/src/orchestrator/templates/ai-src/skills/sungen-selector-keys/SKILL.md +14 -1
- package/src/orchestrator/templates/specs-db.ts +463 -79
- package/src/orchestrator/templates/specs-test-data.ts +39 -3
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Compile-time diagnostics — the warning channel for "the compiler knows something
|
|
3
|
+
* and would otherwise silently discard or invent it" (spec: sungen_app_profile_spec.md §4).
|
|
4
|
+
*
|
|
5
|
+
* Codes:
|
|
6
|
+
* SG-W001 — step matched no pattern (compiles to a no-op `// TODO` comment)
|
|
7
|
+
* SG-W002 — step parsed a {{dataRef}} value but the generated code never uses it
|
|
8
|
+
* SG-W003 — `name` filter defaulted from the Gherkin reference on a name-from-content
|
|
9
|
+
* role (status/alert/…) — such a filter can never match; opt out with `name: null`
|
|
10
|
+
* SG-W005 — qa/app.yaml problem (unknown key, unknown preset, unparseable)
|
|
11
|
+
* SG-W006 — qa/app.<env>.yaml found (per-environment profiles are unsupported by design)
|
|
12
|
+
* SG-W007 — browser-alert handler step written AFTER an action step: the listener
|
|
13
|
+
* registers too late, an earlier dialog auto-dismisses with no error
|
|
14
|
+
*
|
|
15
|
+
* Diagnostics never block generation by themselves; `sungen generate --strict`
|
|
16
|
+
* turns any collected diagnostic into a non-zero exit.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
export type DiagnosticCode = 'SG-W001' | 'SG-W002' | 'SG-W003' | 'SG-W005' | 'SG-W006' | 'SG-W007';
|
|
20
|
+
|
|
21
|
+
export interface Diagnostic {
|
|
22
|
+
code: DiagnosticCode;
|
|
23
|
+
message: string;
|
|
24
|
+
feature?: string;
|
|
25
|
+
step?: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export class DiagnosticsCollector {
|
|
29
|
+
private items: Diagnostic[] = [];
|
|
30
|
+
|
|
31
|
+
add(code: DiagnosticCode, message: string, context: { feature?: string; step?: string } = {}): void {
|
|
32
|
+
this.items.push({ code, message, ...context });
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
all(): Diagnostic[] {
|
|
36
|
+
return this.items;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
get count(): number {
|
|
40
|
+
return this.items.length;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** One line per diagnostic, ready for CLI output. */
|
|
44
|
+
formatLines(): string[] {
|
|
45
|
+
return this.items.map((d) => {
|
|
46
|
+
const where = d.feature ? `${d.feature}: ` : '';
|
|
47
|
+
const step = d.step ? ` — "${d.step}"` : '';
|
|
48
|
+
return `[${d.code}] ${where}${d.message}${step}`;
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
}
|
|
@@ -44,6 +44,18 @@ export interface PatternContext {
|
|
|
44
44
|
scenarioSteps?: ParsedStep[];
|
|
45
45
|
/** Effective Gherkin keyword (Given/When/Then), resolved from And/But */
|
|
46
46
|
effectiveKeyword?: string;
|
|
47
|
+
/** App profile (qa/app.yaml merged over presets/defaults) — widget variants, recipes,
|
|
48
|
+
* loading indicator, table row conventions. Typed loosely to keep the SPI decoupled;
|
|
49
|
+
* the concrete shape is ResolvedAppProfile. */
|
|
50
|
+
appProfile?: {
|
|
51
|
+
widgetVariants: Record<string, string>;
|
|
52
|
+
recipes: Record<string, Record<string, unknown[]>>;
|
|
53
|
+
loadingIndicator: string;
|
|
54
|
+
tableRowBase: number;
|
|
55
|
+
tableHeaderIsRow: boolean;
|
|
56
|
+
portalRoles: string[];
|
|
57
|
+
contentNamedRoles: string[];
|
|
58
|
+
};
|
|
47
59
|
}
|
|
48
60
|
|
|
49
61
|
/**
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Widget-recipe expander (spec: sungen_app_profile_spec.md §3.1 [recipes]).
|
|
3
|
+
*
|
|
4
|
+
* A recipe is a LINEAR sequence of closed primitives that describes one widget
|
|
5
|
+
* intent ("select", "fill", …). Recipes come from `qa/app.yaml` `recipes:` or from
|
|
6
|
+
* driver-shipped built-ins (the reserved `builtin` preset), selected by the widget's
|
|
7
|
+
* variant name (per-element `variant:` > profile `widgets.<kind>`).
|
|
8
|
+
*
|
|
9
|
+
* This expander holds ZERO framework syntax. It resolves each primitive to a
|
|
10
|
+
* `{ partial, ...data }` NODE; the actual Playwright code lives in the per-primitive
|
|
11
|
+
* `recipe-*.hbs` partials, rendered by the template engine via `recipe-sequence.hbs`
|
|
12
|
+
* (dynamic partial lookup). The only strings it builds are locator/value EXPRESSIONS
|
|
13
|
+
* (data — the same split every resolver uses when it hands the locator template a value).
|
|
14
|
+
*
|
|
15
|
+
* Placeholders: `$trigger` = the resolved widget element; `$value` = the step's data
|
|
16
|
+
* value (a runtime marker in runtime mode, transformed to testData.get later); `$item`
|
|
17
|
+
* = the current forEach item. Throws on an unknown primitive/shape — callers fall back
|
|
18
|
+
* to the generic template.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
export interface RecipeExpandContext {
|
|
22
|
+
/** Pre-rendered locator code for the widget element (dialog/frame scoping included). */
|
|
23
|
+
triggerCode: string;
|
|
24
|
+
/** The step's value — a literal or a __SUNGEN_TD__ runtime marker. */
|
|
25
|
+
value: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** A render node: the partial name plus the data it interpolates. */
|
|
29
|
+
export interface RecipeNode {
|
|
30
|
+
partial: string;
|
|
31
|
+
[key: string]: unknown;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const escape = (s: string): string => String(s).replace(/\\/g, '\\\\').replace(/'/g, "\\'");
|
|
35
|
+
|
|
36
|
+
export function expandRecipe(primitives: unknown[], ctx: RecipeExpandContext): RecipeNode[] {
|
|
37
|
+
if (!Array.isArray(primitives) || primitives.length === 0) {
|
|
38
|
+
throw new Error('recipe intent is not a primitive list');
|
|
39
|
+
}
|
|
40
|
+
return primitives.map((p) => expandPrimitive(p, ctx, null));
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** `itemVar` is non-null inside a forEach body — `$item` renders as that identifier. */
|
|
44
|
+
function expandPrimitive(prim: unknown, ctx: RecipeExpandContext, itemVar: string | null): RecipeNode {
|
|
45
|
+
if (typeof prim !== 'object' || prim === null || Array.isArray(prim)) {
|
|
46
|
+
throw new Error(`invalid primitive: ${JSON.stringify(prim)}`);
|
|
47
|
+
}
|
|
48
|
+
const entries = Object.entries(prim as Record<string, unknown>);
|
|
49
|
+
if (entries.length !== 1) throw new Error(`a primitive must have exactly one key: ${JSON.stringify(prim)}`);
|
|
50
|
+
const [kind, arg] = entries[0];
|
|
51
|
+
|
|
52
|
+
// Resolve a `$trigger`/css target into a locator EXPRESSION (data for the partial).
|
|
53
|
+
const targetExpr = (v: unknown): string =>
|
|
54
|
+
v === '$trigger' ? ctx.triggerCode : `page.locator('${escape(String(v))}')`;
|
|
55
|
+
// Resolve a `$value`/`$item`/literal into a JS value EXPRESSION (quoted unless $item;
|
|
56
|
+
// a quoted __SUNGEN_TD__ marker is rewritten to testData.get(...) by the runtime transform).
|
|
57
|
+
const valueExpr = (v: unknown): string => {
|
|
58
|
+
if (v === '$item') {
|
|
59
|
+
if (!itemVar) throw new Error('$item is only valid inside forEach');
|
|
60
|
+
return itemVar;
|
|
61
|
+
}
|
|
62
|
+
const s = v === '$value' ? ctx.value : String(v);
|
|
63
|
+
return `'${escape(s)}'`;
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
switch (kind) {
|
|
67
|
+
case 'click':
|
|
68
|
+
return { partial: 'recipe-click', targetExpr: targetExpr(arg) };
|
|
69
|
+
case 'hover':
|
|
70
|
+
return { partial: 'recipe-hover', targetExpr: targetExpr(arg) };
|
|
71
|
+
case 'type': {
|
|
72
|
+
const v = typeof arg === 'object' && arg !== null ? (arg as any).value : arg;
|
|
73
|
+
return { partial: 'recipe-type', valueExpr: valueExpr(v ?? '$value') };
|
|
74
|
+
}
|
|
75
|
+
case 'press':
|
|
76
|
+
return { partial: 'recipe-press', key: escape(String(arg)) };
|
|
77
|
+
case 'waitFor': {
|
|
78
|
+
// `waitFor: '<css>'` → wait visible. `waitFor: { target, state }` allows an explicit
|
|
79
|
+
// state ('attached' | 'visible' | 'hidden' | 'detached'); `attached` covers the AntD
|
|
80
|
+
// case where a selected chip collapses the input to ~0px (node exists before actionable).
|
|
81
|
+
const o = (typeof arg === 'object' && arg !== null ? arg : { target: arg }) as Record<string, unknown>;
|
|
82
|
+
const state = ['attached', 'visible', 'hidden', 'detached'].includes(String(o.state)) ? String(o.state) : 'visible';
|
|
83
|
+
return { partial: 'recipe-wait-for', targetExpr: targetExpr(o.target), state };
|
|
84
|
+
}
|
|
85
|
+
case 'waitForHidden':
|
|
86
|
+
return { partial: 'recipe-wait-for', targetExpr: targetExpr(arg), state: 'hidden' };
|
|
87
|
+
case 'scrollIntoView':
|
|
88
|
+
// Playwright's own actionability scroll (not arbitrary evaluate) — nudges a lazy list
|
|
89
|
+
// to render+reveal the target before a click. Deterministic, no loop.
|
|
90
|
+
return { partial: 'recipe-scroll-into-view', targetExpr: targetExpr(arg) };
|
|
91
|
+
case 'blur':
|
|
92
|
+
// One bounded el.blur() to dismiss a widget's own overlay (AntD DatePicker panel keeps
|
|
93
|
+
// pointer-blocking after fill; Escape/force-click are unreliable). No retry loop.
|
|
94
|
+
return { partial: 'recipe-blur', targetExpr: targetExpr(arg) };
|
|
95
|
+
case 'clickText': {
|
|
96
|
+
const o = (typeof arg === 'object' && arg !== null ? arg : { text: arg }) as Record<string, unknown>;
|
|
97
|
+
const text = valueExpr(o.text ?? '$value');
|
|
98
|
+
const exact = o.exact === true;
|
|
99
|
+
// Build the option-locator expression; the partial scrolls it into view then clicks.
|
|
100
|
+
const expr = o.role
|
|
101
|
+
? `page.getByRole('${escape(String(o.role))}', { name: ${text}${exact ? ', exact: true' : ''} })`
|
|
102
|
+
: `${o.within ? `page.locator('${escape(String(o.within))}')` : 'page'}.getByText(${text}${exact ? ', { exact: true }' : ''}).first()`;
|
|
103
|
+
return { partial: 'recipe-click-text', targetExpr: expr };
|
|
104
|
+
}
|
|
105
|
+
case 'expectText': {
|
|
106
|
+
const o = (typeof arg === 'object' && arg !== null ? arg : {}) as Record<string, unknown>;
|
|
107
|
+
return { partial: 'recipe-expect-text', targetExpr: targetExpr(o.locator ?? '$trigger'), valueExpr: valueExpr(o.value ?? '$value') };
|
|
108
|
+
}
|
|
109
|
+
case 'expectValue': {
|
|
110
|
+
const o = (typeof arg === 'object' && arg !== null ? arg : {}) as Record<string, unknown>;
|
|
111
|
+
return { partial: 'recipe-expect-value', targetExpr: targetExpr(o.locator ?? '$trigger'), valueExpr: valueExpr(o.value ?? '$value') };
|
|
112
|
+
}
|
|
113
|
+
case 'forEach': {
|
|
114
|
+
const o = (typeof arg === 'object' && arg !== null ? arg : {}) as Record<string, unknown>;
|
|
115
|
+
if (itemVar) throw new Error('nested forEach is not supported');
|
|
116
|
+
const inner = o.do;
|
|
117
|
+
if (!Array.isArray(inner) || inner.length === 0) throw new Error('forEach.do must be a primitive list');
|
|
118
|
+
const source = o.in === '$values' || o.in === undefined ? '$value' : o.in;
|
|
119
|
+
return {
|
|
120
|
+
partial: 'recipe-for-each',
|
|
121
|
+
sourceExpr: valueExpr(source),
|
|
122
|
+
split: escape(String(o.split ?? '>')),
|
|
123
|
+
body: inner.map((p) => expandPrimitive(p, ctx, 'item')),
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
default:
|
|
127
|
+
throw new Error(`unknown recipe primitive "${kind}"`);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
@@ -4,6 +4,9 @@ import { adapterRegistry } from './adapters';
|
|
|
4
4
|
import { PatternRegistry, PatternContext } from './patterns';
|
|
5
5
|
import { SelectorResolver } from './utils/selector-resolver';
|
|
6
6
|
import { DataResolver } from './utils/data-resolver';
|
|
7
|
+
import { DiagnosticsCollector } from './diagnostics';
|
|
8
|
+
import { loadAppProfile, ResolvedAppProfile } from './app-profile';
|
|
9
|
+
import { capabilityRegistry } from '../../capabilities/registry';
|
|
7
10
|
|
|
8
11
|
export interface MappedStep {
|
|
9
12
|
code: string; // Generated Playwright code
|
|
@@ -34,6 +37,12 @@ export class StepMapper {
|
|
|
34
37
|
/** Row scope: when active, column assertions are scoped to this row */
|
|
35
38
|
private inRowScope: boolean = false;
|
|
36
39
|
private rowScopeTable: string = '';
|
|
40
|
+
/** SG-W007: an action step already ran in this scenario (alert handlers registered after it are too late) */
|
|
41
|
+
private sawActionStep: boolean = false;
|
|
42
|
+
/** Compile-time warning channel (SG-W001/W002); optional — absent means silent legacy behavior */
|
|
43
|
+
private diagnostics?: DiagnosticsCollector;
|
|
44
|
+
/** App profile (qa/app.yaml merged over presets/defaults) — the per-project BEHAVIOR layer */
|
|
45
|
+
private appProfile: ResolvedAppProfile;
|
|
37
46
|
|
|
38
47
|
constructor(options: { verbose?: boolean; baseURL?: string; featureName?: string; screenName?: string; featurePath?: string; runtimeData?: boolean; framework?: string } = {}) {
|
|
39
48
|
this.verbose = options.verbose ?? false;
|
|
@@ -51,11 +60,26 @@ export class StepMapper {
|
|
|
51
60
|
this.selectorResolver = new SelectorResolver(undefined, options.screenName);
|
|
52
61
|
this.dataResolver = new DataResolver(undefined, options.screenName, options.runtimeData);
|
|
53
62
|
|
|
63
|
+
// App profile (qa/app.yaml) — loaded after PatternRegistry so capability discovery has
|
|
64
|
+
// populated the preset registry (driver-ui ships native/radix/antd). Missing file →
|
|
65
|
+
// built-in defaults → pre-profile behavior.
|
|
66
|
+
this.appProfile = loadAppProfile(process.cwd(), capabilityRegistry.appProfilePresets());
|
|
67
|
+
this.templateEngine.setPersistentContext({ portalRoles: this.appProfile.portalRoles });
|
|
68
|
+
this.selectorResolver.setContentNamedRoles(this.appProfile.contentNamedRoles);
|
|
69
|
+
|
|
54
70
|
if (this.verbose) {
|
|
55
71
|
console.log(` [StepMapper] ${this.patternRegistry.getPatternCount()} patterns loaded`);
|
|
56
72
|
}
|
|
57
73
|
}
|
|
58
74
|
|
|
75
|
+
/** Attach the compile-diagnostics collector (owned by CodeGenerator) */
|
|
76
|
+
setDiagnostics(collector: DiagnosticsCollector): void {
|
|
77
|
+
this.diagnostics = collector;
|
|
78
|
+
this.selectorResolver.setDiagnostics(collector);
|
|
79
|
+
// Flush profile-load warnings (SG-W005/W006) gathered before the collector existed.
|
|
80
|
+
for (const w of this.appProfile.warnings) collector.add(w.code, w.message);
|
|
81
|
+
}
|
|
82
|
+
|
|
59
83
|
/**
|
|
60
84
|
* Set feature context for data and selector resolution
|
|
61
85
|
*/
|
|
@@ -92,6 +116,7 @@ export class StepMapper {
|
|
|
92
116
|
this.inFrameScope = false;
|
|
93
117
|
this.inRowScope = false;
|
|
94
118
|
this.rowScopeTable = '';
|
|
119
|
+
this.sawActionStep = false;
|
|
95
120
|
this.lastPrimaryKeyword = 'Given';
|
|
96
121
|
this.dataResolver.clearCaptured(); // captured vars are scenario-scoped
|
|
97
122
|
this.templateEngine.resetBaseContext();
|
|
@@ -119,6 +144,23 @@ export class StepMapper {
|
|
|
119
144
|
this.lastPrimaryKeyword = keyword;
|
|
120
145
|
}
|
|
121
146
|
|
|
147
|
+
// SG-W007 — a two-step-form browser-alert handler (`click [OK] alert`) written AFTER
|
|
148
|
+
// an action step registers its page.once('dialog') listener too late: a dialog
|
|
149
|
+
// triggered by the earlier action auto-dismisses with no error (issue #472).
|
|
150
|
+
// Fix: move the alert step BEFORE the trigger, or use the compound form
|
|
151
|
+
// `click [X] button and accept [OK] alert`.
|
|
152
|
+
const isAlertHandlerStep = step.elementType === 'alert' &&
|
|
153
|
+
/\b(click|clicks|fill|fills)\b/.test(step.text) &&
|
|
154
|
+
!/\band\s+(accept|accepts|dismiss|dismisses)\b/i.test(step.text);
|
|
155
|
+
if (isAlertHandlerStep && this.sawActionStep) {
|
|
156
|
+
this.diagnostics?.add('SG-W007',
|
|
157
|
+
'browser-alert handler registered AFTER an action step — a dialog triggered earlier auto-dismisses silently. Move the alert step before the trigger, or use `click [X] button and accept [OK] alert`',
|
|
158
|
+
{ feature: this.featureName, step: step.text });
|
|
159
|
+
} else if (!isAlertHandlerStep &&
|
|
160
|
+
/\b(click|clicks|fill|fills|select|selects|press|presses|check|checks|uncheck|unchecks|drag|drags)\b/.test(step.text)) {
|
|
161
|
+
this.sawActionStep = true;
|
|
162
|
+
}
|
|
163
|
+
|
|
122
164
|
// Frame scope directives — intercept before pattern matching
|
|
123
165
|
if (/\bswitch(?:es)?\s+to\b/i.test(step.text) &&
|
|
124
166
|
(step.elementType === 'frame' || step.elementType === 'iframe')) {
|
|
@@ -215,16 +257,46 @@ export class StepMapper {
|
|
|
215
257
|
if (this.verbose) {
|
|
216
258
|
console.log(` ✓ Pattern matched: ${step.text.substring(0, 50)}...`);
|
|
217
259
|
}
|
|
260
|
+
this.checkDataRefConsumed(step, mappedStep);
|
|
218
261
|
return mappedStep;
|
|
219
262
|
}
|
|
220
263
|
|
|
221
264
|
// No pattern matched - return TODO comment
|
|
265
|
+
this.diagnostics?.add('SG-W001', 'step matched no pattern — compiled to a no-op // TODO comment', {
|
|
266
|
+
feature: this.featureName,
|
|
267
|
+
step: step.text,
|
|
268
|
+
});
|
|
222
269
|
return {
|
|
223
270
|
code: `// TODO: Implement step: ${step.text}`,
|
|
224
271
|
comment: `Unrecognized step pattern: ${step.text}`,
|
|
225
272
|
};
|
|
226
273
|
}
|
|
227
274
|
|
|
275
|
+
/**
|
|
276
|
+
* SG-W002 — a step carried a parsed {{dataRef}} but the matched pattern's generated
|
|
277
|
+
* code never uses it (neither the runtime marker, the ${ref} fallback, nor the
|
|
278
|
+
* inline-resolved value). This is the class of bug where `with {{v}}` silently
|
|
279
|
+
* disappears from the output (e.g. issue #464). Warning-only: loose containment
|
|
280
|
+
* checks err on the side of NOT flagging.
|
|
281
|
+
*/
|
|
282
|
+
private checkDataRefConsumed(step: ParsedStep, mapped: MappedStep): void {
|
|
283
|
+
if (!step.dataRef || !this.diagnostics) return;
|
|
284
|
+
const code = mapped.code || '';
|
|
285
|
+
if (code.includes(DataResolver.encodeMarker(step.dataRef))) return; // runtime marker
|
|
286
|
+
if (code.includes(`\${${step.dataRef}}`)) return; // unresolved fallback
|
|
287
|
+
if (code.includes(step.dataRef)) return; // testData.set('ref'), comments
|
|
288
|
+
try {
|
|
289
|
+
const value = this.dataResolver.resolveData(step.dataRef, this.featureName);
|
|
290
|
+
if (value && (code.includes(value) || code.includes(value.replace(/'/g, "\\'")))) return; // inline mode
|
|
291
|
+
} catch {
|
|
292
|
+
return; // unresolvable ref → other errors surface elsewhere; don't double-report
|
|
293
|
+
}
|
|
294
|
+
this.diagnostics.add('SG-W002', `parsed {{${step.dataRef}}} but the generated code does not use it`, {
|
|
295
|
+
feature: this.featureName,
|
|
296
|
+
step: step.text,
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
|
|
228
300
|
/**
|
|
229
301
|
* Generate column assertion scoped to the current row.
|
|
230
302
|
* Uses columns config for exact cell targeting (nth), or filter fallback.
|
|
@@ -296,6 +368,7 @@ export class StepMapper {
|
|
|
296
368
|
selectorResolver: this.selectorResolver,
|
|
297
369
|
dataResolver: this.dataResolver,
|
|
298
370
|
renderLocator: (resolved) => this.templateEngine.renderStep('locator', resolved),
|
|
371
|
+
appProfile: this.appProfile,
|
|
299
372
|
baseURL: this.baseURL,
|
|
300
373
|
featureName: this.featureName,
|
|
301
374
|
screenName: this.screenName,
|
|
@@ -7,6 +7,8 @@ export class TemplateEngine {
|
|
|
7
7
|
private templatesDir: string;
|
|
8
8
|
private stepsTemplatesDir: string;
|
|
9
9
|
private baseContext: Record<string, any> = {};
|
|
10
|
+
/** Per-project context that survives scenario resets (e.g. app-profile portalRoles) */
|
|
11
|
+
private persistentContext: Record<string, any> = {};
|
|
10
12
|
|
|
11
13
|
constructor(templatesDir: string) {
|
|
12
14
|
this.templatesDir = templatesDir;
|
|
@@ -127,7 +129,17 @@ export class TemplateEngine {
|
|
|
127
129
|
Handlebars.registerHelper('pageRoot', function(this: any) {
|
|
128
130
|
// Frame scope takes precedence (frame > dialog > page)
|
|
129
131
|
if (this.inFrame) return 'frame';
|
|
130
|
-
|
|
132
|
+
// `scope: dialog` on the selector entry is the explicit opt-IN sentinel — the
|
|
133
|
+
// element is dialog-rooted even if its role is portal-exempt (issue #465).
|
|
134
|
+
const explicitDialog = this.scope === 'dialog';
|
|
135
|
+
if (!this.inDialog && !explicitDialog) return 'page';
|
|
136
|
+
// Portal-rendered roles (Radix/MUI/Headless UI/antd render option/menu/tooltip
|
|
137
|
+
// layers onto document.body) live OUTSIDE the dialog's DOM subtree — wrapping
|
|
138
|
+
// them in the dialog root guarantees a timeout. List comes from the app profile
|
|
139
|
+
// (qa/app.yaml layers.portal_roles), default DEFAULT_PORTAL_ROLES.
|
|
140
|
+
if (!explicitDialog && this.role && Array.isArray(this.portalRoles) && this.portalRoles.includes(this.role)) {
|
|
141
|
+
return 'page';
|
|
142
|
+
}
|
|
131
143
|
const partial = Handlebars.partials['dialog-root'];
|
|
132
144
|
if (partial) {
|
|
133
145
|
const fn = typeof partial === 'string' ? Handlebars.compile(partial) : partial;
|
|
@@ -152,6 +164,14 @@ export class TemplateEngine {
|
|
|
152
164
|
Handlebars.registerPartial('locator-base', locatorBaseContent);
|
|
153
165
|
}
|
|
154
166
|
|
|
167
|
+
// Register has-text-value partial — the ONE place that renders a .filter({ hasText })
|
|
168
|
+
// value, so action templates cannot drift from the exact/anchored-regex standard the
|
|
169
|
+
// assertion templates follow (issue #466).
|
|
170
|
+
const hasTextValuePath = path.join(this.stepsTemplatesDir, 'partials', 'has-text-value.hbs');
|
|
171
|
+
if (fs.existsSync(hasTextValuePath)) {
|
|
172
|
+
Handlebars.registerPartial('has-text-value', fs.readFileSync(hasTextValuePath, 'utf-8'));
|
|
173
|
+
}
|
|
174
|
+
|
|
155
175
|
// Register locator strategy partials
|
|
156
176
|
const strategiesDir = path.join(this.stepsTemplatesDir, 'partials', 'locator-strategies');
|
|
157
177
|
if (fs.existsSync(strategiesDir)) {
|
|
@@ -246,9 +266,15 @@ export class TemplateEngine {
|
|
|
246
266
|
this.baseContext = { ...this.baseContext, ...ctx };
|
|
247
267
|
}
|
|
248
268
|
|
|
249
|
-
/**
|
|
269
|
+
/** Context that survives resetBaseContext (per-project facts, e.g. app-profile portalRoles) */
|
|
270
|
+
setPersistentContext(ctx: Record<string, any>): void {
|
|
271
|
+
this.persistentContext = { ...this.persistentContext, ...ctx };
|
|
272
|
+
this.baseContext = { ...this.baseContext, ...ctx };
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/** Reset base context (e.g., on new scenario) — persistent (per-project) context stays */
|
|
250
276
|
resetBaseContext(): void {
|
|
251
|
-
this.baseContext = {};
|
|
277
|
+
this.baseContext = { ...this.persistentContext };
|
|
252
278
|
}
|
|
253
279
|
|
|
254
280
|
renderImports(options?: { runtimeData?: boolean; basePath?: string; isParallel?: boolean; needsCleanupImport?: boolean ; needsDb?: boolean; needsApi?: boolean }): string {
|
|
@@ -5,6 +5,8 @@ import { readYaml, readYamlIfExists } from '../../../utils/yaml-io';
|
|
|
5
5
|
|
|
6
6
|
import { SelectorType } from '../../../utils/selector-types';
|
|
7
7
|
import { DataResolver } from './data-resolver';
|
|
8
|
+
import { DiagnosticsCollector } from '../diagnostics';
|
|
9
|
+
import { DEFAULT_CONTENT_NAMED_ROLES } from '../app-profile';
|
|
8
10
|
|
|
9
11
|
// Structured selector format v2
|
|
10
12
|
interface SelectorEntry {
|
|
@@ -27,7 +29,9 @@ interface SelectorEntry {
|
|
|
27
29
|
ios?: SelectorEntry;
|
|
28
30
|
|
|
29
31
|
// === v2: Complex Elements ===
|
|
30
|
-
|
|
32
|
+
// Dropdown/upload subtype: 'native' | 'custom' | 'dragdrop', or a widget-recipe/variant
|
|
33
|
+
// name ('radix-select', 'office-datepicker', …) — wins over qa/app.yaml widgets.<kind>.
|
|
34
|
+
variant?: string;
|
|
31
35
|
trigger?: string; // Visible button that triggers hidden file input
|
|
32
36
|
frame?: string; // iframe selector for cross-frame elements
|
|
33
37
|
contenteditable?: boolean; // Rich text editor (contenteditable div)
|
|
@@ -96,7 +100,7 @@ export interface ResolvedSelector {
|
|
|
96
100
|
// === v2 fields ===
|
|
97
101
|
scope?: string; // Parent landmark to scope within
|
|
98
102
|
match?: 'exact' | 'partial'; // For getByText exact matching
|
|
99
|
-
variant?:
|
|
103
|
+
variant?: string; // Dropdown/upload subtype or widget-recipe name
|
|
100
104
|
trigger?: string; // Button that triggers hidden file input
|
|
101
105
|
frame?: string; // iframe selector
|
|
102
106
|
contenteditable?: boolean; // Rich text editor
|
|
@@ -117,12 +121,26 @@ export class SelectorResolver {
|
|
|
117
121
|
private featureName?: string;
|
|
118
122
|
private screenName?: string;
|
|
119
123
|
private flowMode: boolean = false;
|
|
124
|
+
/** SG-W003 channel + config: roles whose accessible name derives from content. */
|
|
125
|
+
private diagnostics?: DiagnosticsCollector;
|
|
126
|
+
private contentNamedRoles: string[] = DEFAULT_CONTENT_NAMED_ROLES;
|
|
127
|
+
private warnedContentNamed = new Set<string>();
|
|
120
128
|
|
|
121
129
|
constructor(selectorsDir?: string, screenName?: string) {
|
|
122
130
|
this.selectorsDir = selectorsDir || path.join(process.cwd(), 'qa', 'selectors', 'screens');
|
|
123
131
|
this.screenName = screenName;
|
|
124
132
|
}
|
|
125
133
|
|
|
134
|
+
/** Attach the compile-diagnostics collector (SG-W003) */
|
|
135
|
+
setDiagnostics(collector: DiagnosticsCollector): void {
|
|
136
|
+
this.diagnostics = collector;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** Override the name-from-content role list from the app profile (qa/app.yaml feedback.toast) */
|
|
140
|
+
setContentNamedRoles(roles: string[]): void {
|
|
141
|
+
this.contentNamedRoles = roles;
|
|
142
|
+
}
|
|
143
|
+
|
|
126
144
|
setFlowMode(enabled: boolean): void {
|
|
127
145
|
this.flowMode = enabled;
|
|
128
146
|
}
|
|
@@ -390,6 +408,20 @@ export class SelectorResolver {
|
|
|
390
408
|
if (elementType) {
|
|
391
409
|
const inferred = SelectorResolver.inferFromElementType(elementType, displayLabel);
|
|
392
410
|
if (inferred) {
|
|
411
|
+
// Same SG-W003 class as the explicit-entry path: an auto-inferred name on a
|
|
412
|
+
// name-from-content role ([Loading] spinner → getByRole('status', { name: 'Loading' }))
|
|
413
|
+
// can never match. Add a YAML entry with name: null to opt out.
|
|
414
|
+
if (inferred.strategy === 'role' && inferred.name && inferred.role && this.contentNamedRoles.includes(inferred.role)) {
|
|
415
|
+
const warnKey = `${inferred.role}:${displayLabel}`;
|
|
416
|
+
if (!this.warnedContentNamed.has(warnKey)) {
|
|
417
|
+
this.warnedContentNamed.add(warnKey);
|
|
418
|
+
this.diagnostics?.add(
|
|
419
|
+
'SG-W003',
|
|
420
|
+
`role "${inferred.role}" derives its accessible name from content, but "[${displayLabel}]" auto-inferred { name: '${inferred.name}' } which will likely never match. Add a selectors.yaml entry with an explicit name: or name: null`,
|
|
421
|
+
{ feature: this.featureName }
|
|
422
|
+
);
|
|
423
|
+
}
|
|
424
|
+
}
|
|
393
425
|
return inferred;
|
|
394
426
|
}
|
|
395
427
|
}
|
|
@@ -566,6 +598,21 @@ export class SelectorResolver {
|
|
|
566
598
|
});
|
|
567
599
|
|
|
568
600
|
case 'role':
|
|
601
|
+
// SG-W003 — the entry has no `name:` so the compiler defaulted it to the Gherkin
|
|
602
|
+
// reference text. For name-from-content roles (status/alert/…) that filter can
|
|
603
|
+
// never match the live element (its accessible name is computed from its content,
|
|
604
|
+
// e.g. a toast's message text) — issue #467. Opt out with `name: null`.
|
|
605
|
+
if (entry.name === undefined && name && this.contentNamedRoles.includes(value)) {
|
|
606
|
+
const warnKey = `${value}:${originalLabel}`;
|
|
607
|
+
if (!this.warnedContentNamed.has(warnKey)) {
|
|
608
|
+
this.warnedContentNamed.add(warnKey);
|
|
609
|
+
this.diagnostics?.add(
|
|
610
|
+
'SG-W003',
|
|
611
|
+
`role "${value}" derives its accessible name from content, but the entry for "${originalLabel}" has no name: — the defaulted { name: '${name}' } will likely never match. Set an explicit name: or opt out with name: null`,
|
|
612
|
+
{ feature: this.featureName }
|
|
613
|
+
);
|
|
614
|
+
}
|
|
615
|
+
}
|
|
569
616
|
return withExtras({
|
|
570
617
|
strategy: 'role',
|
|
571
618
|
role: value,
|
|
Binary file
|
package/src/index.ts
CHANGED
|
@@ -14,6 +14,9 @@ export type { DiscoveredContext } from './orchestrator/context-discovery';
|
|
|
14
14
|
|
|
15
15
|
// --- Step-pattern authoring (a driver contributes step patterns via its descriptor) ---
|
|
16
16
|
export type { PatternContext, StepPattern, StepTemplateData } from './generators/test-generator/patterns/types';
|
|
17
|
+
export { expandRecipe } from './generators/test-generator/recipe-expander';
|
|
18
|
+
export type { RecipeExpandContext, RecipeNode } from './generators/test-generator/recipe-expander';
|
|
19
|
+
export type { ResolvedAppProfile } from './generators/test-generator/app-profile';
|
|
17
20
|
export type { MappedStep } from './generators/test-generator/step-mapper';
|
|
18
21
|
export type { ParsedStep } from './generators/gherkin-parser';
|
|
19
22
|
export { getPathCode, inferPath, resolvePathVariables } from './generators/test-generator/utils/path-inference';
|
|
@@ -22,8 +25,8 @@ export { getPathCode, inferPath, resolvePathVariables } from './generators/test-
|
|
|
22
25
|
export { parseQueryOverrides } from './harness/annotation-overrides';
|
|
23
26
|
|
|
24
27
|
// --- Named-query catalog (shared: the DB driver's codegen + core's data-driven advisory lint) ---
|
|
25
|
-
export { resolveQuery, compileQuery, lintCatalog } from './harness/query-catalog';
|
|
26
|
-
export type { QueryEntry } from './harness/query-catalog';
|
|
28
|
+
export { resolveQuery, compileQuery, compileShape, isShapeEntry, lintCatalog } from './harness/query-catalog';
|
|
29
|
+
export type { QueryEntry, MongoFind, MongoPipeline } from './harness/query-catalog';
|
|
27
30
|
|
|
28
31
|
// --- Shared harness: viewpoint catalog + coverage gate / assertion depth ---
|
|
29
32
|
// (the UI capability's gateProvider composes these; they also back core's ingest + audit fallback)
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: create-data-test
|
|
3
|
+
description: "Generate standardized test-data for a screen/flow from the Data Factory catalog — valid/boundary/invalid values with CHK-* traceability (no free-form guessing)"
|
|
4
|
+
argument-hint: [screen-or-flow-name]
|
|
5
|
+
order: 25
|
|
6
|
+
claude-tools: "Read, Grep, Bash, Glob, Write, Edit, AskUserQuestion, Skill"
|
|
7
|
+
copilot-tools: "[vscode, execute, read, agent, edit, search, todo]"
|
|
8
|
+
codex-trigger: "Run when the user asks to CREATE, generate, or standardize TEST DATA / test values for a screen or flow (valid/boundary/invalid, boundary values, negative data). Uses the Data Factory driver. Not for generating the .feature scenarios themselves (that is create-test)."
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
**Input**: A screen or flow name (e.g., `/sungen:create-data-test login`), OR **no name** to do every unit (with a confirmation — see ALL mode). Load the `sungen-data-factory` skill for the full method.
|
|
12
|
+
|
|
13
|
+
## Role
|
|
14
|
+
|
|
15
|
+
You are a **Senior QA Engineer** producing **test data to a standard**, not by guessing. You do NOT invent values field-by-field. You follow the 4-source method (spec rules → field-type catalog → design technique → DB mapping), driven by the **Data Factory** capability driver. Values come from the catalog (`common/` + the project's `.overwrite`), each tagged with a `CHK-*` id that traces back to the spec. Where the catalog cannot cover a project-specific field, you **ask the QA** — you never fabricate domain data.
|
|
16
|
+
|
|
17
|
+
## Preconditions
|
|
18
|
+
|
|
19
|
+
1. **Capability present** — check `qa/capabilities.yaml`. If `data-factory` is not enabled, tell the user and run `sungen capability add data-factory` (it is runtime-free — safe to enable). Then continue.
|
|
20
|
+
2. Determine mode from `$ARGUMENTS`:
|
|
21
|
+
- A **name** given → single-unit mode (that screen/flow only).
|
|
22
|
+
- **No name** → **ALL mode** (see below — always confirm first).
|
|
23
|
+
|
|
24
|
+
## ALL mode (no name) — confirm before doing everything
|
|
25
|
+
|
|
26
|
+
If no name was given, the user wants **all** screens/flows — but this is wide, and a missing name is often a typo (they meant `create-data-test home`). So:
|
|
27
|
+
|
|
28
|
+
1. List every unit under `qa/screens/*` and `qa/flows/*`, and classify each as **new** (no `test-data/<name>.yaml` yet) vs **would-overwrite**.
|
|
29
|
+
2. Show the list and use `AskUserQuestion` to confirm: *"Generate test-data for ALL N units? (X new · Y would overwrite)"* — default to **No**.
|
|
30
|
+
3. Only proceed when the user confirms. Then run the per-unit flow below for each (the CLI `sungen data gen --all` also enforces this confirm; `--yes` after the user agrees).
|
|
31
|
+
|
|
32
|
+
## Per-unit flow (single unit, or each unit in ALL mode)
|
|
33
|
+
|
|
34
|
+
Auto-detect: `qa/flows/<name>/` → flow; else `qa/screens/<name>/` → screen. (API units carry their contract in `apis.yaml` — data there is `@cases` in create-test, not this command.)
|
|
35
|
+
|
|
36
|
+
### 1. Read the sources (Nguồn 1 + 4)
|
|
37
|
+
|
|
38
|
+
Read `qa/<screens|flows>/<name>/requirements/spec.md` and `test-viewpoint.md`. For **each input field / parameter**, extract:
|
|
39
|
+
- data **type** (map to a catalog field-type: email · password · phone · string · number-integer · number-float · currency · postcode · katakana …),
|
|
40
|
+
- `required`, format, **min/max** length or value,
|
|
41
|
+
- the real **error/message code** the spec shows when a rule is violated,
|
|
42
|
+
- any **DB mapping** (unique, cross-entity dependency) noted in the spec.
|
|
43
|
+
|
|
44
|
+
Viewpoint tells you which fields are high-priority → give them the fullest data.
|
|
45
|
+
|
|
46
|
+
### 2. Write the field-map — `qa/data-factory/<name>.fields.yaml`
|
|
47
|
+
|
|
48
|
+
This is the input the deterministic generator expands. One entry per field:
|
|
49
|
+
|
|
50
|
+
```yaml
|
|
51
|
+
screen: <name> # or: flow: <name>
|
|
52
|
+
fields:
|
|
53
|
+
- name: email
|
|
54
|
+
type: email
|
|
55
|
+
required: true
|
|
56
|
+
constraints: { maxLocal: 64, maxTotal: 254 } # project's real limits
|
|
57
|
+
errorMap: { M13: E1042, M02: E1001 } # catalog placeholder → project's real code
|
|
58
|
+
- name: password
|
|
59
|
+
type: password
|
|
60
|
+
constraints: { minLength: 8, maxLength: 32 }
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Map every placeholder error code you can (`errorMap`) to the spec's real code. Leave a code unmapped only if the spec truly has none — the lint will flag it as an open point to confirm with BA/Dev.
|
|
64
|
+
|
|
65
|
+
### 3. Blind spots — ask, do not invent
|
|
66
|
+
|
|
67
|
+
If a field's type is **not** in the catalog (a business enum, a cross-entity constraint, a project data model), **stop and ask the QA** with `AskUserQuestion` for: (1) the valid set, (2) invalid / non-existent values, (3) the error code when wrong, (4) dependencies on other fields. Record the answer in `qa/data-factory/project/<entity>.yaml` for reuse. Never fabricate domain values.
|
|
68
|
+
|
|
69
|
+
### 4. Lint the coverage
|
|
70
|
+
|
|
71
|
+
Run (local-first): `[ -x ./bin/sungen.js ] && ./bin/sungen.js data lint --screen <name> || npx sungen data lint --screen <name>`. Fix what it reports: add missing boundary values, resolve `UNRESOLVED` items (supply the constraint), map `OPEN_ERRORREF` codes. The bar is the phuong-phap §6 checklist — every required field has valid + boundary + invalid, one invalid per rule.
|
|
72
|
+
|
|
73
|
+
### 5. Generate the test-data
|
|
74
|
+
|
|
75
|
+
Run (local-first): `[ -x ./bin/sungen.js ] && ./bin/sungen.js data gen --screen <name> || npx sungen data gen --screen <name>`. If `test-data/<name>.yaml` already exists it **asks before overwriting** — respect the user's choice (in ALL mode after they confirmed, add `--yes`; use `--force` only when the user explicitly wants an unconditional overwrite). The output is standardized `valid/boundary/invalid` values with `CHK-*` traceability.
|
|
76
|
+
|
|
77
|
+
### 6. Weave into scenarios (if create-test already ran)
|
|
78
|
+
|
|
79
|
+
If the `.feature` exists, turn the generated invalid/boundary sets into data-driven `Scenario Outline` + `Examples` (`@cases`) referencing the values — following `sungen-gherkin-syntax`. Otherwise leave the standardized `test-data/<name>.yaml` for `/sungen:run-test` to consume.
|
|
80
|
+
|
|
81
|
+
## After running
|
|
82
|
+
|
|
83
|
+
Use `AskUserQuestion` to offer the next step: review the generated data, run `/sungen:create-test <name>` (if scenarios don't exist yet), or `/sungen:run-test <name>`.
|
|
@@ -18,14 +18,16 @@ You generate 3 files for sungen — a Gherkin compiler that produces Playwright
|
|
|
18
18
|
| `sungen-delivery` | Export Gherkin + Playwright results → CSV test case deliverable |
|
|
19
19
|
| `sungen-capture` | Acquire visual/design context — one skill, 4 modes: figma-mcp (Dev Mode MCP), figma-pat (URL → spec_figma.md), live (Playwright MCP scan), local (images in `requirements/ui/`) |
|
|
20
20
|
| `sungen-locale` | Bootstrap i18n — audit selectors, detect locale switch mechanism, generate test-data overlay |
|
|
21
|
+
| `sungen-data-factory` | Standardized test-data — field-type/security catalog, 4-source method, `.overwrite`, blind-spot prompts |
|
|
21
22
|
|
|
22
|
-
## Workflow (
|
|
23
|
+
## Workflow (8 AI commands)
|
|
23
24
|
|
|
24
25
|
| Command | What it does |
|
|
25
26
|
|---|---|
|
|
26
27
|
| `/sungen:add-screen <name> <path>` | Scaffold `qa/screens/<name>/` directories |
|
|
27
28
|
| `/sungen:add-flow <name> [--path <url>]` | Scaffold `qa/flows/<name>/` directories for E2E cross-screen testing |
|
|
28
29
|
| `/sungen:create-test <name>` | Generate `.feature` + `test-data.yaml` (auto-detects screen or flow) |
|
|
30
|
+
| `/sungen:create-data-test <name>` | Generate standardized test-data (valid/boundary/invalid + CHK trace) from the Data Factory catalog; no name = all units (asks to confirm) |
|
|
29
31
|
| `/sungen:review <name>` | Score syntax, coverage, viewpoint quality (auto-detects screen or flow) |
|
|
30
32
|
| `/sungen:run-test <name>` | Generate `selectors.yaml`, compile, run, auto-fix (auto-detects screen or flow) |
|
|
31
33
|
| `/sungen:delivery [name...]` | Export test cases → CSV for QA delivery (all screens if no arg) |
|
|
@@ -18,14 +18,16 @@ You generate 3 files for sungen — a Gherkin compiler that produces Playwright
|
|
|
18
18
|
| `sungen-delivery` | Export Gherkin + Playwright results → CSV test case deliverable |
|
|
19
19
|
| `sungen-capture` | Acquire visual/design context — one skill, 4 modes: figma-mcp (Dev Mode MCP), figma-pat (URL → spec_figma.md), live (Playwright MCP scan), local (images in `requirements/ui/`) |
|
|
20
20
|
| `sungen-locale` | Bootstrap i18n — audit selectors, detect locale switch mechanism, generate test-data overlay |
|
|
21
|
+
| `sungen-data-factory` | Standardized test-data — field-type/security catalog, 4-source method, `.overwrite`, blind-spot prompts |
|
|
21
22
|
|
|
22
|
-
## Workflow (
|
|
23
|
+
## Workflow (8 AI commands)
|
|
23
24
|
|
|
24
25
|
| Command | What it does |
|
|
25
26
|
|---|---|
|
|
26
27
|
| `/sungen-add-screen <name> <path>` | Scaffold `qa/screens/<name>/` directories |
|
|
27
28
|
| `/sungen-add-flow <name> [--path <url>]` | Scaffold `qa/flows/<name>/` directories for E2E cross-screen testing |
|
|
28
29
|
| `/sungen-create-test <name>` | Generate `.feature` + `test-data.yaml` (auto-detects screen or flow) |
|
|
30
|
+
| `/sungen-create-data-test <name>` | Generate standardized test-data (valid/boundary/invalid + CHK trace) from the Data Factory catalog; no name = all units (asks to confirm) |
|
|
29
31
|
| `/sungen-review <name>` | Score syntax, coverage, viewpoint quality (auto-detects screen or flow) |
|
|
30
32
|
| `/sungen-run-test <name>` | Generate `selectors.yaml`, compile, run, auto-fix (auto-detects screen or flow) |
|
|
31
33
|
| `/sungen-delivery [name...]` | Export test cases → CSV for QA delivery (all screens if no arg) |
|