@sun-asterisk/sungen 3.2.18 → 3.2.20

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.
Files changed (264) hide show
  1. package/dist/capabilities/discover.d.ts.map +1 -1
  2. package/dist/capabilities/discover.js +4 -1
  3. package/dist/capabilities/discover.js.map +1 -1
  4. package/dist/cli/commands/add.d.ts.map +1 -1
  5. package/dist/cli/commands/add.js +4 -2
  6. package/dist/cli/commands/add.js.map +1 -1
  7. package/dist/cli/commands/dashboard.d.ts.map +1 -1
  8. package/dist/cli/commands/dashboard.js +10 -1
  9. package/dist/cli/commands/dashboard.js.map +1 -1
  10. package/dist/cli/commands/delivery.d.ts.map +1 -1
  11. package/dist/cli/commands/delivery.js +19 -1
  12. package/dist/cli/commands/delivery.js.map +1 -1
  13. package/dist/cli/commands/generate.d.ts.map +1 -1
  14. package/dist/cli/commands/generate.js +10 -1
  15. package/dist/cli/commands/generate.js.map +1 -1
  16. package/dist/dashboard/html-renderer.d.ts.map +1 -1
  17. package/dist/dashboard/html-renderer.js +7 -2
  18. package/dist/dashboard/html-renderer.js.map +1 -1
  19. package/dist/dashboard/snapshot-builder.d.ts.map +1 -1
  20. package/dist/dashboard/snapshot-builder.js +3 -0
  21. package/dist/dashboard/snapshot-builder.js.map +1 -1
  22. package/dist/dashboard/types.d.ts +4 -0
  23. package/dist/dashboard/types.d.ts.map +1 -1
  24. package/dist/exporters/api-environment.d.ts.map +1 -1
  25. package/dist/exporters/api-environment.js +4 -24
  26. package/dist/exporters/api-environment.js.map +1 -1
  27. package/dist/exporters/api-testcase-formatter.d.ts +6 -0
  28. package/dist/exporters/api-testcase-formatter.d.ts.map +1 -1
  29. package/dist/exporters/api-testcase-formatter.js +11 -4
  30. package/dist/exporters/api-testcase-formatter.js.map +1 -1
  31. package/dist/exporters/csv-exporter.d.ts.map +1 -1
  32. package/dist/exporters/csv-exporter.js +40 -9
  33. package/dist/exporters/csv-exporter.js.map +1 -1
  34. package/dist/exporters/env-qa.d.ts +13 -0
  35. package/dist/exporters/env-qa.d.ts.map +1 -0
  36. package/dist/exporters/env-qa.js +82 -0
  37. package/dist/exporters/env-qa.js.map +1 -0
  38. package/dist/exporters/feature-parser.d.ts.map +1 -1
  39. package/dist/exporters/feature-parser.js +16 -33
  40. package/dist/exporters/feature-parser.js.map +1 -1
  41. package/dist/exporters/json-exporter.d.ts.map +1 -1
  42. package/dist/exporters/json-exporter.js +57 -20
  43. package/dist/exporters/json-exporter.js.map +1 -1
  44. package/dist/exporters/matrix/build.d.ts.map +1 -1
  45. package/dist/exporters/matrix/build.js +15 -6
  46. package/dist/exporters/matrix/build.js.map +1 -1
  47. package/dist/exporters/matrix/gates.d.ts.map +1 -1
  48. package/dist/exporters/matrix/gates.js +3 -2
  49. package/dist/exporters/matrix/gates.js.map +1 -1
  50. package/dist/exporters/matrix/step-buckets.d.ts +13 -0
  51. package/dist/exporters/matrix/step-buckets.d.ts.map +1 -0
  52. package/dist/exporters/matrix/step-buckets.js +7 -0
  53. package/dist/exporters/matrix/step-buckets.js.map +1 -0
  54. package/dist/exporters/qa-env-secret-denylist.d.ts +43 -0
  55. package/dist/exporters/qa-env-secret-denylist.d.ts.map +1 -0
  56. package/dist/exporters/qa-env-secret-denylist.js +143 -0
  57. package/dist/exporters/qa-env-secret-denylist.js.map +1 -0
  58. package/dist/exporters/scenario-merger.d.ts +20 -13
  59. package/dist/exporters/scenario-merger.d.ts.map +1 -1
  60. package/dist/exporters/scenario-merger.js +55 -30
  61. package/dist/exporters/scenario-merger.js.map +1 -1
  62. package/dist/exporters/step-formatter.d.ts +42 -33
  63. package/dist/exporters/step-formatter.d.ts.map +1 -1
  64. package/dist/exporters/step-formatter.js +68 -68
  65. package/dist/exporters/step-formatter.js.map +1 -1
  66. package/dist/exporters/test-data-resolver.d.ts +15 -0
  67. package/dist/exporters/test-data-resolver.d.ts.map +1 -1
  68. package/dist/exporters/test-data-resolver.js +34 -1
  69. package/dist/exporters/test-data-resolver.js.map +1 -1
  70. package/dist/exporters/types.d.ts +9 -8
  71. package/dist/exporters/types.d.ts.map +1 -1
  72. package/dist/generators/test-generator/adapters/adapter-interface.d.ts +2 -0
  73. package/dist/generators/test-generator/adapters/adapter-interface.d.ts.map +1 -1
  74. package/dist/generators/test-generator/adapters/appium/appium-adapter.d.ts +2 -0
  75. package/dist/generators/test-generator/adapters/appium/appium-adapter.d.ts.map +1 -1
  76. package/dist/generators/test-generator/adapters/appium/appium-adapter.js.map +1 -1
  77. package/dist/generators/test-generator/adapters/appium/templates/steps/actions/keyboard-global-action.hbs +3 -1
  78. package/dist/generators/test-generator/adapters/playwright/playwright-adapter.d.ts +2 -0
  79. package/dist/generators/test-generator/adapters/playwright/playwright-adapter.d.ts.map +1 -1
  80. package/dist/generators/test-generator/adapters/playwright/playwright-adapter.js.map +1 -1
  81. package/dist/generators/test-generator/adapters/playwright/templates/imports.hbs +6 -0
  82. package/dist/generators/test-generator/adapters/playwright/templates/steps/actions/keyboard-global-action.hbs +3 -1
  83. package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/page-assertion.hbs +1 -1
  84. package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/storage-key-assertion.hbs +4 -0
  85. package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/tab-order-assertion.hbs +30 -0
  86. package/dist/generators/test-generator/adapters/playwright/templates/steps/navigation/navigation.hbs +4 -0
  87. package/dist/generators/test-generator/adapters/playwright/templates/steps/navigation/route-assertion.hbs +1 -4
  88. package/dist/generators/test-generator/adapters/playwright/templates/steps/partials/dialog-root.hbs +1 -1
  89. package/dist/generators/test-generator/adapters/playwright/templates/steps/partials/url-assertion.hbs +15 -0
  90. package/dist/generators/test-generator/code-generator.d.ts.map +1 -1
  91. package/dist/generators/test-generator/code-generator.js +78 -7
  92. package/dist/generators/test-generator/code-generator.js.map +1 -1
  93. package/dist/generators/test-generator/diagnostics.d.ts +16 -1
  94. package/dist/generators/test-generator/diagnostics.d.ts.map +1 -1
  95. package/dist/generators/test-generator/diagnostics.js +15 -0
  96. package/dist/generators/test-generator/diagnostics.js.map +1 -1
  97. package/dist/generators/test-generator/recipe-expander.d.ts +1 -1
  98. package/dist/generators/test-generator/recipe-expander.d.ts.map +1 -1
  99. package/dist/generators/test-generator/recipe-expander.js +1 -1
  100. package/dist/generators/test-generator/recipe-expander.js.map +1 -1
  101. package/dist/generators/test-generator/step-mapper.d.ts +41 -0
  102. package/dist/generators/test-generator/step-mapper.d.ts.map +1 -1
  103. package/dist/generators/test-generator/step-mapper.js +161 -17
  104. package/dist/generators/test-generator/step-mapper.js.map +1 -1
  105. package/dist/generators/test-generator/template-engine.d.ts +2 -0
  106. package/dist/generators/test-generator/template-engine.d.ts.map +1 -1
  107. package/dist/generators/test-generator/template-engine.js +1 -1
  108. package/dist/generators/test-generator/template-engine.js.map +1 -1
  109. package/dist/generators/test-generator/utils/data-resolver.d.ts +18 -1
  110. package/dist/generators/test-generator/utils/data-resolver.d.ts.map +1 -1
  111. package/dist/generators/test-generator/utils/data-resolver.js +28 -4
  112. package/dist/generators/test-generator/utils/data-resolver.js.map +1 -1
  113. package/dist/generators/test-generator/utils/runtime-data-transformer.d.ts +2 -2
  114. package/dist/generators/test-generator/utils/runtime-data-transformer.d.ts.map +1 -1
  115. package/dist/generators/test-generator/utils/runtime-data-transformer.js +56 -11
  116. package/dist/generators/test-generator/utils/runtime-data-transformer.js.map +1 -1
  117. package/dist/generators/test-generator/utils/selector-resolver.d.ts +18 -2
  118. package/dist/generators/test-generator/utils/selector-resolver.d.ts.map +1 -1
  119. package/dist/generators/test-generator/utils/selector-resolver.js +29 -6
  120. package/dist/generators/test-generator/utils/selector-resolver.js.map +1 -1
  121. package/dist/harness/audit.d.ts.map +1 -1
  122. package/dist/harness/audit.js +8 -7
  123. package/dist/harness/audit.js.map +1 -1
  124. package/dist/harness/capability-plan.d.ts.map +1 -1
  125. package/dist/harness/capability-plan.js +2 -3
  126. package/dist/harness/capability-plan.js.map +1 -1
  127. package/dist/harness/catalog/drivers.yaml +1 -1
  128. package/dist/harness/challenge.d.ts.map +1 -1
  129. package/dist/harness/challenge.js +1 -55
  130. package/dist/harness/challenge.js.map +1 -1
  131. package/dist/harness/data-driven-lint.d.ts.map +1 -1
  132. package/dist/harness/data-driven-lint.js +14 -9
  133. package/dist/harness/data-driven-lint.js.map +1 -1
  134. package/dist/harness/depth-lint.d.ts.map +1 -1
  135. package/dist/harness/depth-lint.js +1 -3
  136. package/dist/harness/depth-lint.js.map +1 -1
  137. package/dist/harness/flow-check.d.ts.map +1 -1
  138. package/dist/harness/flow-check.js +9 -7
  139. package/dist/harness/flow-check.js.map +1 -1
  140. package/dist/harness/flow-plan.d.ts.map +1 -1
  141. package/dist/harness/flow-plan.js +4 -3
  142. package/dist/harness/flow-plan.js.map +1 -1
  143. package/dist/harness/manifest.js +2 -2
  144. package/dist/harness/manifest.js.map +1 -1
  145. package/dist/harness/parse.d.ts +12 -0
  146. package/dist/harness/parse.d.ts.map +1 -1
  147. package/dist/harness/parse.js +28 -1
  148. package/dist/harness/parse.js.map +1 -1
  149. package/dist/harness/script-check.d.ts.map +1 -1
  150. package/dist/harness/script-check.js +61 -31
  151. package/dist/harness/script-check.js.map +1 -1
  152. package/dist/harness/secret-scan.d.ts +3 -1
  153. package/dist/harness/secret-scan.d.ts.map +1 -1
  154. package/dist/harness/secret-scan.js +27 -11
  155. package/dist/harness/secret-scan.js.map +1 -1
  156. package/dist/harness/sensors.d.ts.map +1 -1
  157. package/dist/harness/sensors.js +15 -4
  158. package/dist/harness/sensors.js.map +1 -1
  159. package/dist/harness/trace.js +1 -1
  160. package/dist/harness/trace.js.map +1 -1
  161. package/dist/harness/unit-paths.d.ts +16 -0
  162. package/dist/harness/unit-paths.d.ts.map +1 -1
  163. package/dist/harness/unit-paths.js +39 -2
  164. package/dist/harness/unit-paths.js.map +1 -1
  165. package/dist/index.d.ts +1 -0
  166. package/dist/index.d.ts.map +1 -1
  167. package/dist/index.js +3 -1
  168. package/dist/index.js.map +1 -1
  169. package/dist/orchestrator/screen-manager.d.ts.map +1 -1
  170. package/dist/orchestrator/screen-manager.js +8 -2
  171. package/dist/orchestrator/screen-manager.js.map +1 -1
  172. package/dist/orchestrator/templates/ai-src/commands/dashboard.md +2 -1
  173. package/dist/orchestrator/templates/ai-src/commands/run-test.md +1 -1
  174. package/dist/orchestrator/templates/ai-src/skills/sungen-delivery/SKILL.md +8 -0
  175. package/dist/orchestrator/templates/ai-src/skills/sungen-error-mapping/SKILL.md +4 -0
  176. package/dist/orchestrator/templates/ai-src/skills/sungen-gherkin-syntax/SKILL.md +79 -2
  177. package/dist/orchestrator/templates/ai-src/skills/sungen-selector-fix/SKILL.md +7 -1
  178. package/dist/orchestrator/templates/ai-src/skills/sungen-selector-keys/SKILL.md +12 -3
  179. package/dist/orchestrator/templates/playwright.config.ts +33 -2
  180. package/dist/orchestrator/templates/specs-mock.d.ts +66 -0
  181. package/dist/orchestrator/templates/specs-mock.d.ts.map +1 -0
  182. package/dist/orchestrator/templates/specs-mock.js +137 -0
  183. package/dist/orchestrator/templates/specs-mock.js.map +1 -0
  184. package/dist/orchestrator/templates/specs-mock.ts +179 -0
  185. package/dist/orchestrator/templates/specs-url-assert.d.ts +67 -0
  186. package/dist/orchestrator/templates/specs-url-assert.d.ts.map +1 -0
  187. package/dist/orchestrator/templates/specs-url-assert.js +121 -0
  188. package/dist/orchestrator/templates/specs-url-assert.js.map +1 -0
  189. package/dist/orchestrator/templates/specs-url-assert.ts +134 -0
  190. package/dist/orchestrator/test-data-guide.d.ts.map +1 -1
  191. package/dist/orchestrator/test-data-guide.js +4 -0
  192. package/dist/orchestrator/test-data-guide.js.map +1 -1
  193. package/package.json +3 -3
  194. package/src/capabilities/discover.ts +4 -1
  195. package/src/cli/commands/add.ts +4 -2
  196. package/src/cli/commands/dashboard.ts +10 -1
  197. package/src/cli/commands/delivery.ts +19 -1
  198. package/src/cli/commands/generate.ts +9 -1
  199. package/src/dashboard/html-renderer.ts +8 -2
  200. package/src/dashboard/snapshot-builder.ts +3 -0
  201. package/src/dashboard/types.ts +4 -0
  202. package/src/exporters/api-environment.ts +3 -20
  203. package/src/exporters/api-testcase-formatter.ts +8 -2
  204. package/src/exporters/csv-exporter.ts +47 -8
  205. package/src/exporters/env-qa.ts +41 -0
  206. package/src/exporters/feature-parser.ts +17 -38
  207. package/src/exporters/json-exporter.ts +71 -24
  208. package/src/exporters/matrix/build.ts +15 -6
  209. package/src/exporters/matrix/gates.ts +3 -2
  210. package/src/exporters/matrix/step-buckets.ts +15 -0
  211. package/src/exporters/qa-env-secret-denylist.ts +118 -0
  212. package/src/exporters/scenario-merger.ts +80 -42
  213. package/src/exporters/step-formatter.ts +67 -66
  214. package/src/exporters/test-data-resolver.ts +34 -1
  215. package/src/exporters/types.ts +9 -8
  216. package/src/generators/test-generator/adapters/adapter-interface.ts +1 -1
  217. package/src/generators/test-generator/adapters/appium/appium-adapter.ts +1 -1
  218. package/src/generators/test-generator/adapters/appium/templates/steps/actions/keyboard-global-action.hbs +3 -1
  219. package/src/generators/test-generator/adapters/playwright/playwright-adapter.ts +1 -1
  220. package/src/generators/test-generator/adapters/playwright/templates/imports.hbs +6 -0
  221. package/src/generators/test-generator/adapters/playwright/templates/steps/actions/keyboard-global-action.hbs +3 -1
  222. package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/page-assertion.hbs +1 -1
  223. package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/storage-key-assertion.hbs +4 -0
  224. package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/tab-order-assertion.hbs +30 -0
  225. package/src/generators/test-generator/adapters/playwright/templates/steps/navigation/navigation.hbs +4 -0
  226. package/src/generators/test-generator/adapters/playwright/templates/steps/navigation/route-assertion.hbs +1 -4
  227. package/src/generators/test-generator/adapters/playwright/templates/steps/partials/dialog-root.hbs +1 -1
  228. package/src/generators/test-generator/adapters/playwright/templates/steps/partials/url-assertion.hbs +15 -0
  229. package/src/generators/test-generator/code-generator.ts +91 -8
  230. package/src/generators/test-generator/diagnostics.ts +16 -1
  231. package/src/generators/test-generator/recipe-expander.ts +2 -2
  232. package/src/generators/test-generator/step-mapper.ts +160 -15
  233. package/src/generators/test-generator/template-engine.ts +2 -2
  234. package/src/generators/test-generator/utils/data-resolver.ts +27 -5
  235. package/src/generators/test-generator/utils/runtime-data-transformer.ts +57 -11
  236. package/src/generators/test-generator/utils/selector-resolver.ts +29 -5
  237. package/src/harness/audit.ts +10 -9
  238. package/src/harness/capability-plan.ts +2 -2
  239. package/src/harness/catalog/drivers.yaml +1 -1
  240. package/src/harness/challenge.ts +2 -3
  241. package/src/harness/data-driven-lint.ts +13 -9
  242. package/src/harness/depth-lint.ts +2 -4
  243. package/src/harness/flow-check.ts +9 -7
  244. package/src/harness/flow-plan.ts +4 -3
  245. package/src/harness/manifest.ts +3 -3
  246. package/src/harness/parse.ts +28 -1
  247. package/src/harness/script-check.ts +61 -33
  248. package/src/harness/secret-scan.ts +23 -8
  249. package/src/harness/sensors.ts +15 -4
  250. package/src/harness/trace.ts +2 -2
  251. package/src/harness/unit-paths.ts +39 -2
  252. package/src/index.ts +1 -0
  253. package/src/orchestrator/screen-manager.ts +7 -2
  254. package/src/orchestrator/templates/ai-src/commands/dashboard.md +2 -1
  255. package/src/orchestrator/templates/ai-src/commands/run-test.md +1 -1
  256. package/src/orchestrator/templates/ai-src/skills/sungen-delivery/SKILL.md +8 -0
  257. package/src/orchestrator/templates/ai-src/skills/sungen-error-mapping/SKILL.md +4 -0
  258. package/src/orchestrator/templates/ai-src/skills/sungen-gherkin-syntax/SKILL.md +79 -2
  259. package/src/orchestrator/templates/ai-src/skills/sungen-selector-fix/SKILL.md +7 -1
  260. package/src/orchestrator/templates/ai-src/skills/sungen-selector-keys/SKILL.md +12 -3
  261. package/src/orchestrator/templates/playwright.config.ts +33 -2
  262. package/src/orchestrator/templates/specs-mock.ts +179 -0
  263. package/src/orchestrator/templates/specs-url-assert.ts +134 -0
  264. package/src/orchestrator/test-data-guide.ts +4 -0
@@ -13,7 +13,7 @@
13
13
  */
14
14
  import * as fs from 'fs';
15
15
  import * as path from 'path';
16
- import { reportSlug } from './unit-paths';
16
+ import { featureFilesFor, reportSlug } from './unit-paths';
17
17
  import { segmentRuns, latestRunEvents, LedgerEvent } from './ledger';
18
18
  import { readTextFile } from './read-text';
19
19
 
@@ -96,7 +96,7 @@ export function buildTrace(screenDir: string, screenName: string): TraceReport {
96
96
  const scRaw = readJson(path.join(process.cwd(), '.sungen', 'reports', `${reportSlug(screenName)}-script-check.json`));
97
97
  const drift = scRaw ? scRaw.drift : null;
98
98
 
99
- const manual = parseManual(path.join(screenDir, 'features', `${screenName}.feature`));
99
+ const manual = featureFilesFor(screenDir, screenName).flatMap(parseManual);
100
100
 
101
101
  // Bottlenecks
102
102
  const bottlenecks: string[] = [];
@@ -1,14 +1,51 @@
1
1
  /**
2
2
  * Unit-path helpers (api-flow fix). A unit id may be a bare name (`orders`, `login`) or a nested
3
- * api-flow id (`flows/<flow>`). Two derivations the harness/CLI need:
3
+ * api-flow id (`flows/<flow>`). Three derivations the harness/CLI need:
4
4
  * - featureBasename: the `.feature` filename — the LAST path segment (`flows/x` → `x`), so
5
5
  * `<dir>/features/<basename>.feature` resolves (the bug: the full id looked for
6
6
  * `features/flows/x.feature` → 0 scenarios).
7
+ * - featureFilesFor: EVERY `.feature` file of the unit — a unit is a directory, not a file.
7
8
  * - reportSlug: a flat key for `.sungen/reports/<slug>-*.json` + `.sungen/ledger/<slug>.jsonl`
8
9
  * (`flows/x` → `flows-x`), so artifacts never nest under a `flows/` subdir and read/write agree.
9
- * Bare names (no slash) are unchanged by both → no regression for screens/flows/areas.
10
+ * Bare names (no slash) are unchanged by all three → no regression for screens/flows/areas.
10
11
  */
12
+ import * as fs from 'fs';
11
13
  import * as path from 'path';
12
14
 
13
15
  export const featureBasename = (unit: string): string => path.basename(unit);
14
16
  export const reportSlug = (unit: string): string => unit.replace(/[\\/]+/g, '-');
17
+
18
+ /**
19
+ * Every `.feature` file belonging to one unit, in a stable order.
20
+ *
21
+ * A unit is a DIRECTORY of features, not one file. `sungen add --screen x --feature y`
22
+ * scaffolds `x-y.feature` beside `x.feature`, and `--path` without `--feature` names the
23
+ * file after the URL segment — so `<unit>.feature` may be one of several, or absent
24
+ * entirely. `generate` and `delivery` have always enumerated the directory; the harness
25
+ * read the single canonical name and scored only that file, silently dropping every
26
+ * sub-feature from coverage/depth/traceability with no warning.
27
+ *
28
+ * Order: the canonical `<unit>.feature` first so report ordering stays put when a
29
+ * sub-feature is added later, then the rest alphabetically. Falls back to the canonical
30
+ * path when the directory is missing or empty, so callers keep the "file not found"
31
+ * behaviour (and error text) they already have.
32
+ */
33
+ export function featureFilesFor(unitDir: string, unit: string): string[] {
34
+ const dir = path.join(unitDir, 'features');
35
+ const canonicalName = `${featureBasename(unit)}.feature`;
36
+ const canonical = path.join(dir, canonicalName);
37
+
38
+ let names: string[];
39
+ try {
40
+ names = fs.readdirSync(dir).filter((f) => f.endsWith('.feature'));
41
+ } catch {
42
+ return [canonical]; // no features/ dir — let the caller report the missing file
43
+ }
44
+ if (names.length === 0) return [canonical];
45
+
46
+ names.sort((a, b) => a.localeCompare(b, 'en'));
47
+ return [
48
+ ...names.filter((n) => n === canonicalName),
49
+ ...names.filter((n) => n !== canonicalName),
50
+ ].map((n) => path.join(dir, n));
51
+ }
package/src/index.ts CHANGED
@@ -20,6 +20,7 @@ export type { ResolvedAppProfile } from './generators/test-generator/app-profile
20
20
  export type { MappedStep } from './generators/test-generator/step-mapper';
21
21
  export type { ParsedStep } from './generators/gherkin-parser';
22
22
  export { getPathCode, inferPath, resolvePathVariables } from './generators/test-generator/utils/path-inference';
23
+ export { LIST_MEMBER_ROLES } from './generators/test-generator/utils/selector-resolver';
23
24
 
24
25
  // --- Precondition-annotation override grammar (shared by the @query / @api driver codegen) ---
25
26
  export { parseQueryOverrides } from './harness/annotation-overrides';
@@ -8,6 +8,7 @@ import * as path from 'path';
8
8
  import { chromium } from '@playwright/test';
9
9
  import { readCapabilities } from '../harness/capability';
10
10
  import { testDataHeader, ensureTestDataGuideFiles } from './test-data-guide';
11
+ import { resolveWebBaseUrl } from '../exporters/env-qa';
11
12
 
12
13
  export interface ScreenOptions {
13
14
  name: string;
@@ -304,8 +305,12 @@ export class ScreenManager {
304
305
  if (!fs.existsSync(configPath)) return null;
305
306
 
306
307
  const content = fs.readFileSync(configPath, 'utf-8');
307
- const match = content.match(/baseURL:\s*['"]([^'"]+)['"]/);
308
- return match ? match[1] : null;
308
+ // Both config shapes: legacy literal and env-aware resolveBaseUrl('x') wrapper.
309
+ const match = content.match(/baseURL:\s*(?:resolveBaseUrl\(\s*)?['"]([^'"]+)['"]/);
310
+ if (!match) return null;
311
+ // SUNGEN_BASE_URL (shell > .env.qa.<SUNGEN_ENV> > .env.qa) overrides the static
312
+ // value — screenshots must capture the same host the tests will run against.
313
+ return resolveWebBaseUrl(this.cwd, match[1]);
309
314
  }
310
315
 
311
316
  private validateScreenName(name: string): void {
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: dashboard
3
3
  description: "Build a single-file HTML dashboard with test cases + pass/fail results, history trends, compare runs, and CSV/XLSX export."
4
- argument-hint: "[screen-name...] (omit for all)"
4
+ argument-hint: "[screen-name...] [--env <env>] (omit names for all)"
5
5
  order: 60
6
6
  claude-tools: "Bash, Read, AskUserQuestion"
7
7
  copilot-tools: "[read, execute, edit, vscode/askQuestions]"
@@ -16,6 +16,7 @@ You are a **QA Reporting Engineer**. Your job is to invoke the deterministic `su
16
16
  Parse **screens** from `$ARGUMENTS`:
17
17
  - If empty → CLI will include **all** screens and flows.
18
18
  - If provided → pass them through.
19
+ - **`--env <env>`** — optional (accept `--locale` as an alias). Pass through as `sungen dashboard --env <env>`: the dashboard is built for that environment/locale only — it picks the `<name>-test-result.<env>.json` files, and Trends/Compare only see history snapshots of the same env (a dev run never blends into the uat trend line). Omit → the base environment.
19
20
 
20
21
  ## Steps
21
22
 
@@ -61,7 +61,7 @@ Stop here for mobile — do not continue to the web path.
61
61
 
62
62
  Parse from `$ARGUMENTS`:
63
63
  - **name** — screen or flow name. If missing, ask the user.
64
- - **`--env <locale>`** — optional. Sets `SUNGEN_ENV=<locale>` for the test run so the runtime test-data resolver merges `<name>.<locale>.yaml` over the base, and `playwright.config.ts` writes results to `<name>-test-result.<locale>.json`. Accept `--locale <locale>` as an alias.
64
+ - **`--env <locale>`** — optional. Sets `SUNGEN_ENV=<locale>` for the test run so the runtime test-data resolver merges `<name>.<locale>.yaml` over the base, and `playwright.config.ts` writes results to `<name>-test-result.<locale>.json`. Accept `--locale <locale>` as an alias. The value can also be a plain ENVIRONMENT name (`dev`, `uat`, `stg`): the same overlay applies to test-data AND `.env.qa.<env>` overlays `.env.qa` (secrets, API/DB datasource URLs); when `SUNGEN_BASE_URL` is declared there, the web baseURL switches too — no config edit, no recompile.
65
65
  - **`--ios`**, **`--android`**, **`--both`** (mobile only) — optional. Sets `MOBILE_PLATFORM` for the test run. Default: `android` if not specified. Parsed into `MOBILE_PLATFORM=<value>` and used in WebdriverIO test runs.
66
66
 
67
67
  If `--env` is passed but no value follows, ask the user which locale to use.
@@ -99,6 +99,12 @@ scheme (`SEC-123`, `PER-345`, `MS-HP-001`) is addressed the same way, and the ca
99
99
  exist in test-data, so Gate D does not ask for them: the capture step renders as an instruction
100
100
  (`Note the X text for later (as "v")`) and later references read `the captured v`.
101
101
 
102
+ **Dynamic vars**: `{{$timestamp}}`, `{{$uuid}}`, `{{$random:min:max}}`, `{{$date}}`, `{{$datetime}}`
103
+ are resolved by TestDataLoader at execution time — including when nested inside a test-data value
104
+ (`seed: "Summary intro {{$timestamp}}"`). They have no static value to print, so the report names
105
+ them instead: `<unique timestamp>`, `<unique id>`, `<random 1-100>`, `<today's date>`,
106
+ `<current date-time>`. An unknown `{{$foo}}` is left raw on purpose — it is a typo and Gate D says so.
107
+
102
108
  **Data fidelity**: invisible test data is made visible, never normalized — `''` → `(empty)`,
103
109
  whitespace-only → `(5 spaces)`, padded → `" value "` quoted verbatim. A trim/collapse here would
104
110
  silently break the whitespace tests it describes.
@@ -172,6 +178,8 @@ The CLI reads the **per-target result file first** (co-located with `.spec.ts`),
172
178
  | Test Environment | `playwright.config.ts` baseURL + project name |
173
179
  | Note | Error message + trace path (for failed tests) |
174
180
 
181
+ **Runtime-secret redaction (all cells, all unit kinds):** a failed test's error message carries the RESOLVED `${QA_*}` credential (runtime substituted it before asserting), and cell content can too. The exporter builds a per-unit denylist — env vars referenced as `${QA_*}` in the unit's test data whose NAME looks sensitive (`QA_ADMIN_PASSWORD` yes; `QA_BASE_URL`, `QA_ADMIN_EMAIL` no) — resolving values from shell/CI env > `.env.qa.<SUNGEN_ENV>` > `.env.qa`, and erases every occurrence (`***`) from Note, dashboard errorMessage, Steps, Expected results and Test Data. Value-based only: fake demo values (`password123`), error-message strings (`Password is required`) and element names (`[Password]`) stay readable. If a sensitive ref can't be resolved while the run has failures, delivery prints a "redaction coverage incomplete" warning — set the variable in the env before exporting. `sungen delivery` also runs the same secret scan as `generate` and warns if test-data holds a literal token-shaped value.
182
+
175
183
  ---
176
184
 
177
185
  ## XLSX sheets — Overview + Auto / Manual split
@@ -95,6 +95,10 @@ needs any of these, it is a **finding for QA** — surface it in the run summary
95
95
  | `SG-W008` | `@manual` scenario incomplete in either direction: (a) HAS Gherkin steps but NO `Then` — a dangling `When` is invalid Gherkin, comments can NOT substitute; (b) documentation-style (NO steps) but its comments lack numbered action lines and/or a labeled oracle line — prose paragraphs / rationale-only comments export EMPTY Steps + Expected cells | Two valid shapes only: complete the Gherkin with a `Then`, or write the FULL procedure in the `# Tester verifies:` block as NUMBERED lines — action steps first (`1. <action>`), then labeled `Observable:`/`Oracle:`/`Expected:` lines (the number is what fills Steps, the label is what fills Expected results) |
96
96
  | `SG-W009` | test-data references `${QA_<NAME>}` (a SECRET env ref) but the name is declared in neither `.env.qa` nor `.env.qa.example` — the test throws "environment variable not set" the moment the key is used. (Only the `${QA_*}` shape is checked; any other `${...}` is literal data and never flagged.) | Add `QA_<NAME>=<value>` to `.env.qa` (gitignored) and list the NAME in `.env.qa.example` (the committed checklist). See `qa/TEST-DATA-GUIDE.md` |
97
97
  | `SG-W010` | A row step's `[Ref]` looks POSITIONAL (`First…`/`Last…`/`Top…`/`nth`) but has no selectors.yaml entry — the label is display-only, so the step checks existence ANYWHERE in the table; an ordering/sort-stability rule would pass even when the row moved | Declare the row in selectors.yaml (a `type: locator` value RELATIVE to the table, e.g. `first contact row:` → `value: 'tbody tr:first-child'`) — the step then asserts `toContainText` on that exact row. This is a selectors.yaml-only fix — allowed inside the run-test auto-fix loop |
98
+ | `SG-W011` | A storage assertion written as `expect … in local/session storage` — `expect` steps only read `{{response}}` refs, so this compiles to a silent no-op | Rewrite using the supported grammar: `User see [KEY] in local storage exists/…` |
99
+ | `SG-W012` | A mock-install step written AFTER a navigation step in the same block — `page.route()` registered after `goto()` misses every request fired during page load | Move the mock-install step before the navigation, or into `Background` |
100
+ | `SG-W013` | A page assertion (`see [X] page` / `is on [X] page`) whose `[Ref]` has no `type: page` selector entry (or collides with a non-page entry) — the step falls back to the feature's own path (or `/<ref>/`) instead of `X`'s real URL, so the anchored assertion can never pass | Declare a `type: page` entry for `[Ref]` with its real URL; if the key collides with another type, disambiguate with a `--type` suffix (`sungen-selector-keys` § Collision rule) |
101
+ | `SG-W014` | `[X] page with {{v}}` where `{{v}}`'s base test-data value carries no query and no fragment — the step checks the PATH only, asserting less than it reads as | Informational — pass a value like `?q=…` if you meant to assert a query, or drop `with {{v}}` for a bare page |
98
102
 
99
103
  ### Runtime error → `Test data "<key>" references ${QA_*} but the environment variable is not set`
100
104
 
@@ -62,7 +62,7 @@ User click [T] button and dismiss [Cancel] alert # compiler registers the liste
62
62
  User click [OK | Cancel] alert # two-step form: must come BEFORE the trigger
63
63
  User fill [T] alert with {{v}}
64
64
  User see [message text] alert
65
- User press Escape key | Enter on [T] field
65
+ User press Escape key | [Enter] key | Tab key 5 times | Enter on [T] field
66
66
  User wait for N seconds | [T] page
67
67
  User wait for [T] TYPE is visible | hidden | enabled | disabled # ANY reference (v3.3)
68
68
  User wait for [T] TYPE with {{v}} # until it shows the value
@@ -103,9 +103,43 @@ two content-filtered queries can hit different rows if the table re-renders in b
103
103
  # 5. State: User see [T] button is disabled | checkbox is checked | dialog with {{v}} is hidden
104
104
  # 6. Attribute (toHaveAttribute): User see [T] image | link with {{v}}
105
105
  # 7. Count: User see [T] row with {{count}}
106
- # 8. Page: User see [T] page
106
+ # 8. Page: User see [T] page | page with {{url}} ← `with` also asserts the query string
107
107
  ```
108
108
 
109
+ **Pattern 8 — the page assertion judges pathname AND query together.** Both `see [T] page` and
110
+ `is on [T] page` (Then-side) compile to ONE web-first predicate — `toHaveURL(urlMatches({…}))`,
111
+ which retries like any other assertion — comparing the **exact** pathname (anchored, trailing slash
112
+ normalised) plus the query:
113
+
114
+ | step | asserts |
115
+ |---|---|
116
+ | `Then User see [Search] page` | pathname `/vi/search` **and no query at all** — `/vi/search?x=1` FAILS |
117
+ | `… page with {{v}}`, `v` = `?q=abc` | pathname + **every declared param must match**; extra params are tolerated (a UTM/tracking param cannot turn the test red); a repeated param (`?tag=a&tag=b`) matches per value, so param ORDER never matters |
118
+ | `… page with {{v}}`, `v` = `?q=abc#tab` | the fragment is asserted too |
119
+ | `… page with {{v}}`, `v` = `/vi/search?q=abc` | only the query part is used — the page selector already owns the path |
120
+ | `… page with {{v}}`, `v` = `/vi/search` (no query) | **path only**, and the compiler warns (`SG-W014`): the step asserts less than it reads as |
121
+ | `… page with {{v}}`, `v` empty or key missing at run time | **throws, naming the key** — an unusable value is never a silent pass |
122
+
123
+ So pass `with {{v}}` whenever the URL carries state worth proving, and leave it off when the page
124
+ must be reached clean. A dynamic segment in the selector value (`/users/:id/profile`) wildcards to
125
+ `[^/]+` on its own, still anchored — `/users/42/profile/edit` does not match. Params compare
126
+ decoded on both sides, so a deliberately malformed query still asserts what was written.
127
+
128
+ Two asymmetries worth knowing rather than discovering:
129
+
130
+ 1. **`Given`/`When` navigate, `Then` asserts** — so they read `{{v}}` differently. `Then` uses only
131
+ its query/fragment; `Given User is on [T] page with {{v}}` also honours a path the value carries
132
+ (`/vi/other?q=1` navigates to `/vi/other?q=1`, and a value repeating the page's own path no
133
+ longer doubles it). Anything that is not a path, query, or fragment is appended **verbatim** —
134
+ nothing is encoded or normalised, so a test can still drive a deliberately malformed query.
135
+ 2. **A repeated param matches as a subset per key**, which is the same rule as "extra params are
136
+ tolerated": every value you declare must be present, and the URL may carry more.
137
+
138
+ The predicate itself lives in `specs/url-assert.ts` (auto-generated, `DO NOT EDIT`). If `[T]` has no
139
+ `type: page` selector entry — or its key collides with a non-page entry, so `value` is something like
140
+ `button` rather than a URL — the step falls back to another path and cannot match the real URL. The
141
+ compiler warns (`SG-W013`); declare the page (or rename the colliding key) instead of chasing the red.
142
+
109
143
  ### Collection / all-card (P5)
110
144
 
111
145
  ```
@@ -142,6 +176,49 @@ first contact row:
142
176
  ```
143
177
  → compiles to `expect(table.locator('tbody tr:first-child')).toContainText(v)` — the exact row must hold the value — and still enters row scope for `[Col] column` checks.
144
178
 
179
+ ### Browser storage (web)
180
+
181
+ ```
182
+ Then User see [KEY] in local storage exists
183
+ Then User see [KEY] in local storage does not exist
184
+ Then User see [KEY] in local storage is not empty
185
+ Then User see [KEY] in local storage has length {{n}}
186
+ Then User see key matching "PATTERN" in local storage # regex over key names
187
+ # same five forms with `session storage`
188
+ ```
189
+
190
+ `[KEY]` is a **storage key, not a selector** — never add it to selectors.yaml. It may embed `{{vars}}`: `[{{exclusive_code}}_ACCESS_TOKEN]`. The check runs inside the browser and returns only a boolean, so a failure message never contains the stored value (safe for tokens). There is deliberately no `equals {{expected}}` form. ⚠️ `expect [KEY] in local storage …` is NOT valid (`expect` reads `{{response}}` refs only) — the compiler warns SG-W011.
191
+
192
+ ### Tab order (web only)
193
+
194
+ ```
195
+ Then User see tab order:
196
+ | Ref |
197
+ | [Station Dropdown] |
198
+ | [Departure Date] |
199
+ | [Search Button] button |
200
+ ```
201
+
202
+ Focuses row 1 (the pinned origin — there is no separate `focus [X]` step) and asserts it actually HOLDS focus (a non-focusable ref fails loudly), then presses Tab per following row and asserts it receives focus. Cell refs ARE selector references (resolved via selectors.yaml; optional element type after the ref). Requires the `| Ref |` header and ≥2 element rows — a missing header or single row is a compile error, never an empty pass. A mismatch reports the expected ref + the actual focused element (shadow-DOM-aware tag/role/name). Limitations: web only (no Appium); declare tab order OUTSIDE `use dialog`/frame scope (cell locators render page-rooted — put `scope: dialog` on the selector ENTRIES if the elements live in a dialog); focus traps / dynamic comboboxes / `tabindex=-1` reordering / WebKit differences may need `@manual` keyboard-only audits — those stay legitimate manuals.
203
+
204
+ ### Network mocking (optional Mock Driver — `sungen capability add mock`)
205
+
206
+ ```gherkin
207
+ @mock
208
+ Scenario: search shows the error state on server failure
209
+ Given [reservations-check-500] mock is active # install a NAMED catalog entry
210
+ When User click [Search] button
211
+ Then User see [Error Toast] message
212
+ And User see [reservations-check-500] mock called {{n}} times # exact count; "called 1 time" also valid
213
+ ```
214
+
215
+ `[name]` is a **catalog reference, not a selector** — entries live in `qa/screens/<name>/mock/mocks.yaml` (`method`, `url` glob or `re:` regex, `status`, optional `headers`/`body`/`delay_ms`; a `"{{var}}"` body reads test-data at runtime). No inline `[GET /path]` forms. Rules:
216
+ - **Fail-on-unmocked (default):** once a scenario has any mock, unmatched fetch/XHR requests FAIL the test (documents/assets still load). Opt out per scenario with `@mock:passthrough`.
217
+ - **Mocks that must intercept page-load requests go in Background, BEFORE the navigation step** — installing after a navigation warns SG-W012 (load-time requests were not intercepted).
218
+ - No `abort` entry (a network error is not a timeout) — simulate timeouts with a large `delay_ms`.
219
+ - mocks.yaml is committed: credentials in bodies must be `${QA_*}` refs, never pasted DevTools responses (the secret scan covers `mock/*.yaml`).
220
+ - Web only. Requires the driver: without it, `@mock` is a hard compile error pointing at `sungen capability add mock`.
221
+
145
222
  ### Database verification (optional Data Driver)
146
223
 
147
224
  Read-only DB-state checks. **Prefer named queries** — SQL lives in `qa/screens/<screen>/database/queries.yaml` (reviewed once, parameterized). Invoke with the `@query:<name>` annotation; it binds the result rows to `{{name}}`, then assert with `expect`:
@@ -55,6 +55,12 @@ When running Phase 0 for a **flow** (`qa/flows/<name>/`), check existing screen
55
55
  1. **Confirm with the user**: *"Generate selectors from the live page via Playwright MCP now?"* — offer **Yes, scan live page** / **Skip (use existing selectors.yaml)** / **Cancel**.
56
56
  {{/cap}}
57
57
  2. **Collect references**: parse the `.feature` file for every `[Reference]` element + its type (e.g. `[Submit] button`, `[Email] field`). Deduplicate.
58
+
59
+ ⛔ **Non-selector bracket refs — never hunt the DOM for these, never write a selectors.yaml entry:**
60
+ - `[KEY] in local storage …` / `[KEY] in session storage …` — a **storage key**, checked via `page.evaluate`, not an element (the key may embed `{{vars}}`, e.g. `[{{code}}_ACCESS_TOKEN]`).
61
+ - Bracket refs consumed by other non-DOM step families (`@mock` route refs) follow the same rule: if the step's grammar does not target a UI element, the bracket content is NOT a selector.
62
+ Writing a fabricated entry for these breaks the real step (the pattern parses the bracket itself) and pollutes selectors.yaml.
63
+ ✅ **Tab-order table cells ARE selector refs** — each `| [Ref] |` row under `Then User see tab order:` resolves through selectors.yaml like any element (verify them once; they are the same refs the scenario's other steps use — don't double-count them as new elements).
58
64
  3. **Ensure page selector**: if missing, ask user for URL path and write it first.
59
65
  4. **Navigate**:
60
66
  - Read `baseURL` from `playwright.config.ts`.
@@ -91,7 +97,7 @@ When running Phase 0 for a **flow** (`qa/flows/<name>/`), check existing screen
91
97
  - Loading indicator for `wait for [T] table to refresh` → `feedback.loading.indicator` (skip if the app sets `aria-busy`).
92
98
  - **Every qa/app.yaml edit MUST carry provenance** in `detection:`: who/when (`detected_by`) and the observed evidence per key (`evidence:`). A profile without evidence is a guess a human cannot review.
93
99
  8. **Merge, don't overwrite**: preserve the page selector and any user-authored entries in `selectors.yaml`. Only add missing keys.
94
- - **Type-collision check** (see `sungen-selector-keys` § Collision rule): before writing a base key, check whether it already exists with a *different* `type`. If so, give **both** entries a `--type` suffix instead of clobbering or sharing the base key — a base-key entry of the wrong type gets picked up by any step referencing that label. Watch `page` especially: a `[X] page` step reads `value` as a URL, so if `X` already holds a button (`value: 'button'`) the assertion compiles to `toHaveURL(/button/)`. Write `X--page` (URL from the live page / feature `Path:`) alongside `X--button`.
100
+ - **Type-collision check** (see `sungen-selector-keys` § Collision rule): before writing a base key, check whether it already exists with a *different* `type`. If so, give **both** entries a `--type` suffix instead of clobbering or sharing the base key — a base-key entry of the wrong type gets picked up by any step referencing that label. Watch `page` especially: a `[X] page` step reads `value` as a URL, so if `X` already holds a button (`value: 'button'`) the compiler won't use the button's value as a URL, but it can't invent the real one either — it falls back to the feature's own path and warns `SG-W013`, asserting a page that isn't `X`'s. Write `X--page` (URL from the live page / feature `Path:`) alongside `X--button`.
95
101
  9. **Show summary + confirm**: list the keys that will be added, ask the user to approve, then write the file.
96
102
  10. **Compile**: **Screen**: `sungen generate --screen <screen>`. **Flow**: `sungen generate --flow <flow>`. Then proceed to Phase 1.
97
103
 
@@ -97,12 +97,15 @@ its own `--type` suffix. A key match alone does not carry type: the resolver wal
97
97
  picked up by any step referencing that label, regardless of the type the step expects.
98
98
 
99
99
  The `page` type is the sharpest trap. A page step (`Then User see [X] page`) reads the entry's
100
- `value` as a **URL**. If key `X` already holds a `role`/button entry, the page step resolves that
101
- button and emits `toHaveURL(/button/)` — a literal button label treated as a URL path.
100
+ `value` as a **URL**. If key `X` already holds a `role`/button entry, the compiler guards the type
101
+ it does NOT emit `toHaveURL(/button/)` — but it can't invent the real URL either, so it falls
102
+ back to the feature's own path (or `/<ref>/`) and warns `SG-W013`: a test that reads as asserting
103
+ `X`'s page but never can, since the fallback path is anchored and almost certainly wrong.
102
104
 
103
105
  ```yaml
104
106
  # Gherkin: `When User click [読取結果一覧] button` AND `Then User see [読取結果一覧] page`
105
- # WRONG — one base key, page step inherits the button's value ('button') as a URL:
107
+ # WRONG — one base key: the page step can't use the button's value, falls back to the feature path
108
+ # instead, and the compiler warns SG-W013 rather than asserting the real page:
106
109
  読取結果一覧:
107
110
  type: 'role'
108
111
  value: 'button'
@@ -254,6 +257,12 @@ success toast:
254
257
 
255
258
  Use this for every toast/alert/spinner content assertion. Do NOT fall back to `type: locator, value: '[role="status"]'` — `name: null` keeps role semantics.
256
259
 
260
+ **List-member roles need no opt-out.** For `option`, `listitem` and `cell` the compiler never defaults
261
+ the name at all — those name themselves from their content, and the value being asserted is already
262
+ carried by `.filter({ hasText })`. Omit `name:` and the locator comes out as `getByRole('option')`.
263
+ Declare `name:` only when the DOM really does expose an accessible name worth filtering on; an
264
+ explicit one is always honoured.
265
+
257
266
  ### Types requiring YAML entry (no auto-infer)
258
267
 
259
268
  These types need explicit `selectors.yaml` entries:
@@ -1,4 +1,33 @@
1
1
  import { defineConfig, devices } from '@playwright/test';
2
+ import * as fs from 'fs';
3
+ import * as path from 'path';
4
+
5
+ /**
6
+ * Web baseURL per environment — one compiled suite runs on dev/uat/stg by
7
+ * switching SUNGEN_ENV, no config edit and no recompile.
8
+ *
9
+ * Precedence (first hit wins):
10
+ * 1. `SUNGEN_BASE_URL` from the shell/CI env
11
+ * 2. `SUNGEN_BASE_URL=` in `.env.qa.<SUNGEN_ENV>` (e.g. `.env.qa.uat`)
12
+ * 3. `SUNGEN_BASE_URL=` in `.env.qa`
13
+ * 4. the static fallback below (written by `sungen init --base-url`)
14
+ *
15
+ * Mirrors how the runtime resolves `${QA_*}` test-data refs and API/DB
16
+ * datasource URLs, so every layer of a run agrees on the environment.
17
+ */
18
+ function resolveBaseUrl(fallback: string): string {
19
+ if (process.env.SUNGEN_BASE_URL) return process.env.SUNGEN_BASE_URL;
20
+ for (const name of [`.env.qa.${process.env.SUNGEN_ENV || ''}`, '.env.qa']) {
21
+ if (name.endsWith('.')) continue;
22
+ const p = path.join(process.cwd(), name);
23
+ if (!fs.existsSync(p)) continue;
24
+ for (const line of fs.readFileSync(p, 'utf8').split('\n')) {
25
+ const m = line.match(/^\s*SUNGEN_BASE_URL\s*=\s*(.*?)\s*$/);
26
+ if (m) return m[1].replace(/^["']|["']$/g, '');
27
+ }
28
+ }
29
+ return fallback;
30
+ }
2
31
 
3
32
  /**
4
33
  * Resolve the JSON reporter output path.
@@ -61,8 +90,10 @@ export default defineConfig({
61
90
  ],
62
91
  /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
63
92
  use: {
64
- /* Base URL to use in actions like `await page.goto('')`. */
65
- baseURL: 'https://example.com',
93
+ /* Base URL to use in actions like `await page.goto('')`. The quoted value is the
94
+ static fallback (pre-filled by `sungen init --base-url`); SUNGEN_BASE_URL in the
95
+ shell or .env.qa[.<SUNGEN_ENV>] overrides it per environment. */
96
+ baseURL: resolveBaseUrl('https://example.com'),
66
97
 
67
98
  /* Per-action timeout (click, fill, etc.) */
68
99
  actionTimeout: 10_000,
@@ -0,0 +1,179 @@
1
+ /* eslint-disable */
2
+ /**
3
+ * Sungen Mock Driver — runtime network-mocking helper (auto-generated into specs/mock.ts).
4
+ *
5
+ * One module singleton (same import shape as db/api); ALL state lives per-page in a
6
+ * WeakMap<Page, MockState>, so routes and call counters follow the page's lifetime on
7
+ * both isolation paths: a fresh page per test destroys them for free, and the @serial
8
+ * shared page is reset by the compiler-emitted mock.reset(page) at the end of each
9
+ * @mock scenario (scoped — only routes THIS router installed are unrouted; a blanket
10
+ * page.unrouteAll() would also kill Background-installed mocks and third-party routes).
11
+ *
12
+ * Blocking posture (fail-on-unmocked, the project default): the first install() also
13
+ * registers a catch-all route. Playwright matches routes last-registered-first, so
14
+ * registering the catch-all FIRST makes every named mock win over it. The catch-all
15
+ * only judges fetch/XHR/websocket traffic — documents/scripts/styles/images pass, the
16
+ * app shell must still load; what is being isolated is API traffic. An unmatched API
17
+ * request aborts and throws (fails the test) unless the scenario opted out via
18
+ * @mock:passthrough (compiled to mock.setPassthrough(page)).
19
+ *
20
+ * DO NOT EDIT — regenerated by `sungen generate`.
21
+ */
22
+ import type { Page, Route } from '@playwright/test';
23
+
24
+ export interface MockConfig {
25
+ name: string;
26
+ method: string; // HTTP verb or '*'
27
+ url: string; // page.route glob, or 're:<pattern>' for a regex
28
+ status: number;
29
+ headers?: Record<string, string>;
30
+ body?: string; // pre-serialized by the compiler (JSON string)
31
+ delay_ms?: number;
32
+ }
33
+
34
+ interface InstalledRoute {
35
+ pattern: string | RegExp;
36
+ handler: (route: Route) => Promise<void>;
37
+ }
38
+
39
+ interface MockState {
40
+ counts: Map<string, number>; // per-mock handled-request counters
41
+ installed: Set<string>; // names installed in this scenario (callCount guard)
42
+ routes: InstalledRoute[]; // exactly what WE routed (scoped reset)
43
+ guardInstalled: boolean;
44
+ passthrough: boolean;
45
+ violations: string[]; // unmocked requests the guard aborted (fail-on-unmocked)
46
+ /** @serial only: routes[0..N) / names installed by Background (beforeAll) persist across
47
+ * scenarios — reset() removes only what a scenario added on top. -1 = no snapshot
48
+ * (the default per-test path), where reset() drops the whole state. */
49
+ backgroundRouteCount: number;
50
+ backgroundNames: Set<string>;
51
+ }
52
+
53
+ /** Resource types the fail-on-unmocked guard judges; everything else passes through.
54
+ * NOTE: page.route() does NOT intercept WebSocket connections (that is
55
+ * page.routeWebSocket, a separate API) — the shipped posture is fetch/XHR only. */
56
+ const GUARDED_TYPES = new Set(['fetch', 'xhr']);
57
+
58
+ class MockRouter {
59
+ private state = new WeakMap<Page, MockState>();
60
+
61
+ private stateFor(page: Page): MockState {
62
+ let st = this.state.get(page);
63
+ if (!st) {
64
+ st = { counts: new Map(), installed: new Set(), routes: [], guardInstalled: false, passthrough: false, violations: [], backgroundRouteCount: -1, backgroundNames: new Set() };
65
+ this.state.set(page, st);
66
+ }
67
+ return st;
68
+ }
69
+
70
+ /** `@mock:passthrough` — unmatched requests reach the real backend for this page/scenario.
71
+ * Also forgives violations already recorded (Background installs + goto run BEFORE the
72
+ * scenario body can set this flag — boot-time requests of an opted-out scenario must not
73
+ * fail its assertNoUnmocked verdict). The compiler additionally arms the flag inside
74
+ * beforeEach via test.info().tags, before the Background installs, so boot requests are
75
+ * not even aborted on the default path. */
76
+ setPassthrough(page: Page): void {
77
+ const st = this.stateFor(page);
78
+ st.passthrough = true;
79
+ st.violations.length = 0;
80
+ }
81
+
82
+ /** Install one catalog entry. The first install also arms the fail-on-unmocked guard. */
83
+ async install(page: Page, cfg: MockConfig): Promise<void> {
84
+ const st = this.stateFor(page);
85
+ if (!st.guardInstalled) {
86
+ st.guardInstalled = true;
87
+ const guard = async (route: Route) => {
88
+ const req = route.request();
89
+ if (!GUARDED_TYPES.has(req.resourceType()) || st.passthrough) return route.fallback();
90
+ // Record + abort — NEVER throw here: a route handler runs outside any test
91
+ // stack, so a throw surfaces as an unhandled rejection (a hard process crash
92
+ // outside @playwright/test, flaky attribution inside it). The compiler emits
93
+ // `await mock.assertNoUnmocked(page)` at the end of every @mock scenario,
94
+ // which fails the test deterministically with the FULL violation list.
95
+ st.violations.push(`${req.method()} ${req.url()}`);
96
+ await route.abort('failed');
97
+ };
98
+ await page.route('**/*', guard); // FIRST → matched last → every named mock wins
99
+ st.routes.push({ pattern: '**/*', handler: guard });
100
+ }
101
+
102
+ const pattern = cfg.url.startsWith('re:') ? new RegExp(cfg.url.slice(3)) : cfg.url;
103
+ const handler = async (route: Route) => {
104
+ if (cfg.method !== '*' && route.request().method() !== cfg.method) return route.fallback();
105
+ st.counts.set(cfg.name, (st.counts.get(cfg.name) ?? 0) + 1);
106
+ if (cfg.delay_ms) await new Promise((r) => setTimeout(r, cfg.delay_ms));
107
+ // A delayed fulfill can race a page close / reset at teardown — that race must
108
+ // not surface as an unhandled rejection from inside a route handler.
109
+ try { await route.fulfill({ status: cfg.status, headers: cfg.headers, body: cfg.body }); } catch { /* page closing */ }
110
+ };
111
+ await page.route(pattern, handler);
112
+ st.routes.push({ pattern, handler });
113
+ st.installed.add(cfg.name);
114
+ if (!st.counts.has(cfg.name)) st.counts.set(cfg.name, 0);
115
+ }
116
+
117
+ /** Handled-request count for a mock installed in THIS scenario — throws for a name
118
+ * that was never installed here (an exact 0 on an uninstalled mock must be a loud
119
+ * error, not a passing assertion). */
120
+ callCount(page: Page, name: string): number {
121
+ const st = this.state.get(page);
122
+ if (!st || !st.installed.has(name)) {
123
+ throw new Error(`mock "${name}" was never installed in this scenario — add \`Given [${name}] mock is active\``);
124
+ }
125
+ return st.counts.get(name) ?? 0;
126
+ }
127
+
128
+ /** Fail-on-unmocked verdict — emitted by the compiler as the LAST statement of every
129
+ * @mock scenario. Throws with the full list of aborted unmatched requests; a scenario
130
+ * whose app swallowed those failed fetches must not stay green for the wrong reason. */
131
+ async assertNoUnmocked(page: Page): Promise<void> {
132
+ const st = this.state.get(page);
133
+ if (!st || st.violations.length === 0) return;
134
+ const list = st.violations.slice();
135
+ st.violations.length = 0;
136
+ throw new Error(
137
+ `${list.length} unmocked request(s) hit the fail-on-unmocked guard:\n - ${list.join('\n - ')}\n` +
138
+ 'Add entries to mock/mocks.yaml, or opt out with @mock:passthrough.');
139
+ }
140
+
141
+ /** @serial only, emitted by the compiler at the END of the Background block (beforeAll):
142
+ * everything installed so far — Background mocks and the guard — persists across the
143
+ * feature's scenarios; reset() removes only what each scenario added on top. Without
144
+ * this, scenario 1's reset would kill the Background mocks and scenarios 2+ would run
145
+ * against the REAL backend with no guard — the silent-green class this driver exists
146
+ * to prevent. */
147
+ snapshotBackground(page: Page): void {
148
+ const st = this.stateFor(page);
149
+ st.backgroundRouteCount = st.routes.length;
150
+ st.backgroundNames = new Set(st.installed);
151
+ }
152
+
153
+ /** Scoped teardown for the @serial shared page: unroute exactly what THIS scenario
154
+ * installed (Background routes captured by snapshotBackground persist), zero every
155
+ * counter, clear flags/violations. Emitted at the end of each @mock scenario on the
156
+ * serial path; the default per-test path never needs it (page dies with the test). */
157
+ async reset(page: Page): Promise<void> {
158
+ const st = this.state.get(page);
159
+ if (!st) return;
160
+ if (st.backgroundRouteCount < 0) {
161
+ // No Background snapshot → drop everything (the pre-snapshot / default behavior).
162
+ for (const r of st.routes) {
163
+ try { await page.unroute(r.pattern, r.handler); } catch { /* page may be closing */ }
164
+ }
165
+ this.state.delete(page);
166
+ return;
167
+ }
168
+ for (const r of st.routes.slice(st.backgroundRouteCount)) {
169
+ try { await page.unroute(r.pattern, r.handler); } catch { /* page may be closing */ }
170
+ }
171
+ st.routes.length = st.backgroundRouteCount;
172
+ st.installed = new Set(st.backgroundNames);
173
+ st.counts = new Map();
174
+ st.violations.length = 0;
175
+ st.passthrough = false;
176
+ }
177
+ }
178
+
179
+ export const mock = new MockRouter();