@sun-asterisk/sungen 3.2.28 → 3.2.29

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 (217) hide show
  1. package/dist/capabilities/discover.d.ts +29 -0
  2. package/dist/capabilities/discover.d.ts.map +1 -1
  3. package/dist/capabilities/discover.js +144 -10
  4. package/dist/capabilities/discover.js.map +1 -1
  5. package/dist/capabilities/registry.d.ts +18 -0
  6. package/dist/capabilities/registry.d.ts.map +1 -1
  7. package/dist/capabilities/registry.js +25 -0
  8. package/dist/capabilities/registry.js.map +1 -1
  9. package/dist/cli/commands/audit.d.ts.map +1 -1
  10. package/dist/cli/commands/audit.js +9 -0
  11. package/dist/cli/commands/audit.js.map +1 -1
  12. package/dist/cli/commands/capability.js +1 -1
  13. package/dist/cli/commands/capability.js.map +1 -1
  14. package/dist/dashboard/templates/index.html +1 -1
  15. package/dist/exporters/feature-parser.d.ts +1 -1
  16. package/dist/exporters/feature-parser.js +1 -1
  17. package/dist/exporters/matrix/build.d.ts.map +1 -1
  18. package/dist/exporters/matrix/build.js +14 -1
  19. package/dist/exporters/matrix/build.js.map +1 -1
  20. package/dist/exporters/matrix/item-labels.d.ts +35 -0
  21. package/dist/exporters/matrix/item-labels.d.ts.map +1 -0
  22. package/dist/exporters/matrix/item-labels.js +90 -0
  23. package/dist/exporters/matrix/item-labels.js.map +1 -0
  24. package/dist/exporters/matrix/render-coverage-sheet.d.ts +14 -0
  25. package/dist/exporters/matrix/render-coverage-sheet.d.ts.map +1 -0
  26. package/dist/exporters/matrix/render-coverage-sheet.js +160 -0
  27. package/dist/exporters/matrix/render-coverage-sheet.js.map +1 -0
  28. package/dist/exporters/matrix/render-overview-sheet.d.ts +20 -0
  29. package/dist/exporters/matrix/render-overview-sheet.d.ts.map +1 -0
  30. package/dist/exporters/matrix/render-overview-sheet.js +36 -0
  31. package/dist/exporters/matrix/render-overview-sheet.js.map +1 -0
  32. package/dist/exporters/matrix/render-testcase-sheet.d.ts +30 -0
  33. package/dist/exporters/matrix/render-testcase-sheet.d.ts.map +1 -0
  34. package/dist/exporters/matrix/render-testcase-sheet.js +156 -0
  35. package/dist/exporters/matrix/render-testcase-sheet.js.map +1 -0
  36. package/dist/exporters/matrix/render-xlsx.d.ts +41 -31
  37. package/dist/exporters/matrix/render-xlsx.d.ts.map +1 -1
  38. package/dist/exporters/matrix/render-xlsx.js +72 -425
  39. package/dist/exporters/matrix/render-xlsx.js.map +1 -1
  40. package/dist/exporters/matrix/testcase-summary-band.d.ts +30 -0
  41. package/dist/exporters/matrix/testcase-summary-band.d.ts.map +1 -0
  42. package/dist/exporters/matrix/testcase-summary-band.js +75 -0
  43. package/dist/exporters/matrix/testcase-summary-band.js.map +1 -0
  44. package/dist/exporters/matrix/wording.d.ts.map +1 -1
  45. package/dist/exporters/matrix/wording.js +13 -0
  46. package/dist/exporters/matrix/wording.js.map +1 -1
  47. package/dist/exporters/matrix/xlsx-cells.d.ts +38 -0
  48. package/dist/exporters/matrix/xlsx-cells.d.ts.map +1 -0
  49. package/dist/exporters/matrix/xlsx-cells.js +52 -0
  50. package/dist/exporters/matrix/xlsx-cells.js.map +1 -0
  51. package/dist/exporters/xlsx-report-builder.d.ts +12 -1
  52. package/dist/exporters/xlsx-report-builder.d.ts.map +1 -1
  53. package/dist/exporters/xlsx-report-builder.js +10 -2
  54. package/dist/exporters/xlsx-report-builder.js.map +1 -1
  55. package/dist/generators/gherkin-parser/index.d.ts +7 -0
  56. package/dist/generators/gherkin-parser/index.d.ts.map +1 -1
  57. package/dist/generators/gherkin-parser/index.js +3 -3
  58. package/dist/generators/gherkin-parser/index.js.map +1 -1
  59. package/dist/generators/test-generator/adapters/appium/os-support.d.ts +53 -0
  60. package/dist/generators/test-generator/adapters/appium/os-support.d.ts.map +1 -0
  61. package/dist/generators/test-generator/adapters/appium/os-support.js +200 -0
  62. package/dist/generators/test-generator/adapters/appium/os-support.js.map +1 -0
  63. package/dist/generators/test-generator/adapters/appium/templates/steps/actions/alert-fill-action.hbs +9 -2
  64. package/dist/generators/test-generator/adapters/appium/templates/steps/actions/capture-variable.hbs +24 -1
  65. package/dist/generators/test-generator/adapters/appium/templates/steps/actions/hover-action.hbs +6 -2
  66. package/dist/generators/test-generator/adapters/appium/templates/steps/actions/hover-element-with-text.hbs +4 -3
  67. package/dist/generators/test-generator/adapters/appium/templates/steps/actions/unknown-element-action.hbs +9 -0
  68. package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/have-value-assertion.hbs +34 -1
  69. package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/label-value-assertion.hbs +14 -6
  70. package/dist/generators/test-generator/adapters/appium/templates/steps/gestures/go-back-action.hbs +21 -0
  71. package/dist/generators/test-generator/adapters/appium/templates/steps/gestures/set-geolocation-action.hbs +16 -0
  72. package/dist/generators/test-generator/adapters/appium/templates/test-file.hbs +8 -0
  73. package/dist/generators/test-generator/adapters/playwright/templates/steps/actions/table-action-in-row-nth.hbs +4 -1
  74. package/dist/generators/test-generator/adapters/playwright/templates/steps/actions/table-action-in-row.hbs +4 -1
  75. package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/selected-assertion.hbs +7 -0
  76. package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/state-with-filter-assertion.hbs +7 -0
  77. package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/table-empty.hbs +4 -1
  78. package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/table-match-data.hbs +4 -1
  79. package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/table-row-count.hbs +4 -1
  80. package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/table-row-exists.hbs +12 -4
  81. package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/table-row-not-exists.hbs +14 -2
  82. package/dist/generators/test-generator/adapters/playwright/templates/steps/partials/locator-base.hbs +8 -8
  83. package/dist/generators/test-generator/adapters/playwright/templates/steps/partials/locator-strategies/id.hbs +1 -1
  84. package/dist/generators/test-generator/adapters/playwright/templates/steps/partials/locator-strategies/testid.hbs +1 -1
  85. package/dist/generators/test-generator/adapters/playwright/templates/steps/partials/locator.hbs +8 -8
  86. package/dist/generators/test-generator/code-generator.d.ts.map +1 -1
  87. package/dist/generators/test-generator/code-generator.js +5 -1
  88. package/dist/generators/test-generator/code-generator.js.map +1 -1
  89. package/dist/generators/test-generator/diagnostics.d.ts +38 -2
  90. package/dist/generators/test-generator/diagnostics.d.ts.map +1 -1
  91. package/dist/generators/test-generator/diagnostics.js +37 -1
  92. package/dist/generators/test-generator/diagnostics.js.map +1 -1
  93. package/dist/generators/test-generator/patterns/index.d.ts +33 -6
  94. package/dist/generators/test-generator/patterns/index.d.ts.map +1 -1
  95. package/dist/generators/test-generator/patterns/index.js +71 -7
  96. package/dist/generators/test-generator/patterns/index.js.map +1 -1
  97. package/dist/generators/test-generator/patterns/types.d.ts +8 -0
  98. package/dist/generators/test-generator/patterns/types.d.ts.map +1 -1
  99. package/dist/generators/test-generator/step-mapper.d.ts +40 -1
  100. package/dist/generators/test-generator/step-mapper.d.ts.map +1 -1
  101. package/dist/generators/test-generator/step-mapper.js +100 -17
  102. package/dist/generators/test-generator/step-mapper.js.map +1 -1
  103. package/dist/generators/test-generator/utils/selector-resolver.d.ts +23 -1
  104. package/dist/generators/test-generator/utils/selector-resolver.d.ts.map +1 -1
  105. package/dist/generators/test-generator/utils/selector-resolver.js +78 -12
  106. package/dist/generators/test-generator/utils/selector-resolver.js.map +1 -1
  107. package/dist/harness/audit.d.ts +3 -1
  108. package/dist/harness/audit.d.ts.map +1 -1
  109. package/dist/harness/audit.js +51 -28
  110. package/dist/harness/audit.js.map +1 -1
  111. package/dist/harness/capability-plan.d.ts +4 -0
  112. package/dist/harness/capability-plan.d.ts.map +1 -1
  113. package/dist/harness/capability-plan.js +32 -2
  114. package/dist/harness/capability-plan.js.map +1 -1
  115. package/dist/harness/catalog/drivers.yaml +1 -1
  116. package/dist/harness/catalog/universal-viewpoints.yaml +145 -0
  117. package/dist/harness/parse.d.ts +6 -0
  118. package/dist/harness/parse.d.ts.map +1 -1
  119. package/dist/harness/parse.js +1 -1
  120. package/dist/harness/parse.js.map +1 -1
  121. package/dist/harness/repair.d.ts.map +1 -1
  122. package/dist/harness/repair.js +8 -1
  123. package/dist/harness/repair.js.map +1 -1
  124. package/dist/harness/sensors.d.ts +48 -0
  125. package/dist/harness/sensors.d.ts.map +1 -1
  126. package/dist/harness/sensors.js +26 -1
  127. package/dist/harness/sensors.js.map +1 -1
  128. package/dist/harness/viewpoint-ledger.d.ts +10 -0
  129. package/dist/harness/viewpoint-ledger.d.ts.map +1 -1
  130. package/dist/harness/viewpoint-ledger.js +23 -2
  131. package/dist/harness/viewpoint-ledger.js.map +1 -1
  132. package/dist/orchestrator/templates/ai-src/commands/delivery.md +4 -3
  133. package/dist/orchestrator/templates/ai-src/skills/sungen-delivery/SKILL.md +14 -5
  134. package/dist/orchestrator/templates/ai-src/skills/sungen-error-mapping/SKILL.md +48 -1
  135. package/dist/orchestrator/templates/ai-src/skills/sungen-gherkin-syntax/SKILL.md +18 -3
  136. package/dist/orchestrator/templates/ai-src/skills/sungen-mobile-gestures/SKILL.md +16 -1
  137. package/dist/orchestrator/templates/ai-src/skills/sungen-selector-fix/SKILL.md +11 -0
  138. package/dist/orchestrator/templates/ai-src/skills/sungen-selector-keys/SKILL.md +10 -3
  139. package/dist/orchestrator/templates/ai-src/skills/sungen-tc-generation/SKILL.md +88 -2
  140. package/dist/orchestrator/templates/ai-src/skills/sungen-tc-review/SKILL.md +58 -1
  141. package/dist/orchestrator/templates/ai-src/skills/sungen-test-design-techniques/SKILL.md +13 -0
  142. package/dist/orchestrator/templates/ai-src/skills/sungen-viewpoint/SKILL.md +59 -10
  143. package/dist/orchestrator/templates/ai-src/skills/sungen-viewpoint/group-f-mobile-native.md +419 -0
  144. package/dist/orchestrator/templates/readme.md +1 -1
  145. package/dist/orchestrator/templates/specs-pw-shape-reporter.ts +0 -0
  146. package/dist/orchestrator/templates/wdio.conf.ts +105 -1
  147. package/package.json +3 -3
  148. package/src/capabilities/discover.ts +146 -10
  149. package/src/capabilities/registry.ts +30 -0
  150. package/src/cli/commands/audit.ts +9 -0
  151. package/src/cli/commands/capability.ts +1 -1
  152. package/src/dashboard/templates/index.html +1 -1
  153. package/src/exporters/feature-parser.ts +1 -1
  154. package/src/exporters/matrix/build.ts +13 -1
  155. package/src/exporters/matrix/item-labels.ts +78 -0
  156. package/src/exporters/matrix/render-coverage-sheet.ts +168 -0
  157. package/src/exporters/matrix/render-overview-sheet.ts +56 -0
  158. package/src/exporters/matrix/render-testcase-sheet.ts +178 -0
  159. package/src/exporters/matrix/render-xlsx.ts +67 -448
  160. package/src/exporters/matrix/testcase-summary-band.ts +79 -0
  161. package/src/exporters/matrix/wording.ts +13 -0
  162. package/src/exporters/matrix/xlsx-cells.ts +56 -0
  163. package/src/exporters/xlsx-report-builder.ts +16 -2
  164. package/src/generators/gherkin-parser/index.ts +1 -1
  165. package/src/generators/test-generator/adapters/appium/os-support.ts +176 -0
  166. package/src/generators/test-generator/adapters/appium/templates/steps/actions/alert-fill-action.hbs +9 -2
  167. package/src/generators/test-generator/adapters/appium/templates/steps/actions/capture-variable.hbs +24 -1
  168. package/src/generators/test-generator/adapters/appium/templates/steps/actions/hover-action.hbs +6 -2
  169. package/src/generators/test-generator/adapters/appium/templates/steps/actions/hover-element-with-text.hbs +4 -3
  170. package/src/generators/test-generator/adapters/appium/templates/steps/actions/unknown-element-action.hbs +9 -0
  171. package/src/generators/test-generator/adapters/appium/templates/steps/assertions/have-value-assertion.hbs +34 -1
  172. package/src/generators/test-generator/adapters/appium/templates/steps/assertions/label-value-assertion.hbs +14 -6
  173. package/src/generators/test-generator/adapters/appium/templates/steps/gestures/go-back-action.hbs +21 -0
  174. package/src/generators/test-generator/adapters/appium/templates/steps/gestures/set-geolocation-action.hbs +16 -0
  175. package/src/generators/test-generator/adapters/appium/templates/test-file.hbs +8 -0
  176. package/src/generators/test-generator/adapters/playwright/templates/steps/actions/table-action-in-row-nth.hbs +4 -1
  177. package/src/generators/test-generator/adapters/playwright/templates/steps/actions/table-action-in-row.hbs +4 -1
  178. package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/selected-assertion.hbs +7 -0
  179. package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/state-with-filter-assertion.hbs +7 -0
  180. package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/table-empty.hbs +4 -1
  181. package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/table-match-data.hbs +4 -1
  182. package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/table-row-count.hbs +4 -1
  183. package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/table-row-exists.hbs +12 -4
  184. package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/table-row-not-exists.hbs +14 -2
  185. package/src/generators/test-generator/adapters/playwright/templates/steps/partials/locator-base.hbs +8 -8
  186. package/src/generators/test-generator/adapters/playwright/templates/steps/partials/locator-strategies/id.hbs +1 -1
  187. package/src/generators/test-generator/adapters/playwright/templates/steps/partials/locator-strategies/testid.hbs +1 -1
  188. package/src/generators/test-generator/adapters/playwright/templates/steps/partials/locator.hbs +8 -8
  189. package/src/generators/test-generator/code-generator.ts +5 -1
  190. package/src/generators/test-generator/diagnostics.ts +39 -2
  191. package/src/generators/test-generator/patterns/index.ts +78 -8
  192. package/src/generators/test-generator/patterns/types.ts +8 -0
  193. package/src/generators/test-generator/step-mapper.ts +107 -17
  194. package/src/generators/test-generator/utils/selector-resolver.ts +88 -12
  195. package/src/harness/audit.ts +55 -30
  196. package/src/harness/capability-plan.ts +33 -1
  197. package/src/harness/catalog/drivers.yaml +1 -1
  198. package/src/harness/catalog/universal-viewpoints.yaml +145 -0
  199. package/src/harness/parse.ts +7 -1
  200. package/src/harness/repair.ts +8 -1
  201. package/src/harness/sensors.ts +75 -1
  202. package/src/harness/viewpoint-ledger.ts +26 -5
  203. package/src/orchestrator/templates/ai-src/commands/delivery.md +4 -3
  204. package/src/orchestrator/templates/ai-src/skills/sungen-delivery/SKILL.md +14 -5
  205. package/src/orchestrator/templates/ai-src/skills/sungen-error-mapping/SKILL.md +48 -1
  206. package/src/orchestrator/templates/ai-src/skills/sungen-gherkin-syntax/SKILL.md +18 -3
  207. package/src/orchestrator/templates/ai-src/skills/sungen-mobile-gestures/SKILL.md +16 -1
  208. package/src/orchestrator/templates/ai-src/skills/sungen-selector-fix/SKILL.md +11 -0
  209. package/src/orchestrator/templates/ai-src/skills/sungen-selector-keys/SKILL.md +10 -3
  210. package/src/orchestrator/templates/ai-src/skills/sungen-tc-generation/SKILL.md +88 -2
  211. package/src/orchestrator/templates/ai-src/skills/sungen-tc-review/SKILL.md +58 -1
  212. package/src/orchestrator/templates/ai-src/skills/sungen-test-design-techniques/SKILL.md +13 -0
  213. package/src/orchestrator/templates/ai-src/skills/sungen-viewpoint/SKILL.md +59 -10
  214. package/src/orchestrator/templates/ai-src/skills/sungen-viewpoint/group-f-mobile-native.md +419 -0
  215. package/src/orchestrator/templates/readme.md +1 -1
  216. package/src/orchestrator/templates/specs-pw-shape-reporter.ts +0 -0
  217. package/src/orchestrator/templates/wdio.conf.ts +105 -1
@@ -0,0 +1,200 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.stripComments = void 0;
37
+ exports.osSupportOf = osSupportOf;
38
+ exports.osSupportOfTemplate = osSupportOfTemplate;
39
+ const fs = __importStar(require("node:fs"));
40
+ const path = __importStar(require("node:path"));
41
+ /**
42
+ * The per-OS axis of the step-support matrix, DERIVED from each Appium template's own body.
43
+ *
44
+ * The Web|Mobile columns answer "does a template exist". They cannot answer the question that
45
+ * actually bites: a template can ship, compile, and still do nothing on one of the two mobile
46
+ * runtimes. `hide-keyboard-action` is guarded `if (!driver.isIOS && …)` — on iOS the step compiles,
47
+ * runs, and silently skips. Nothing in the matrix showed that, so nobody could see iOS drifting
48
+ * behind Android, which is exactly what the Android-first decision needs to be able to see.
49
+ *
50
+ * Derived, never declared. A hand-written per-OS column is the same mistake the spec made when it
51
+ * claimed capabilities the code did not have: two sources of truth, one of them wrong, no gate to
52
+ * catch it. Reading the template body means the column cannot lie for longer than a regenerate.
53
+ *
54
+ * A template mentioning `driver.isIOS` in a shape this file does not recognise is a THROW, never a
55
+ * default. Defaulting to 'yes' would let a new one-sided template enter the matrix reported as
56
+ * working on both — the precise failure the axis exists to prevent.
57
+ */
58
+ /**
59
+ * Strip handlebars comments so a body is judged by what it EXECUTES, never by prose ABOUT iOS.
60
+ * Several templates explain their per-OS reasoning at length in a `{{!-- … --}}` block; counting
61
+ * that as code would classify half the adapter wrongly.
62
+ */
63
+ const stripComments = (src) => src.replace(/\{\{!--[\s\S]*?--\}\}/g, '').replace(/\{\{![^}]*?\}\}/g, '');
64
+ exports.stripComments = stripComments;
65
+ /** Worse wins: a template with both a per-OS read and an iOS refusal is an iOS refusal. */
66
+ const RANK = { inherit: 0, yes: 1, noop: 2, refuses: 3 };
67
+ const worse = (a, b) => (RANK[b] > RANK[a] ? b : a);
68
+ /**
69
+ * The statement (or block) an `if (…)` condition guards.
70
+ *
71
+ * Brace-matched rather than regex-matched because the distinction that matters is whether a `throw`
72
+ * sits INSIDE the OS branch. `swipe-action` throws for a not-found element and implements iOS fully;
73
+ * a file-level `/throw/` test would read that as an iOS refusal and put a lie in the table.
74
+ */
75
+ function guardedBody(code, from) {
76
+ let i = from;
77
+ while (i < code.length && /\s/.test(code[i]))
78
+ i++;
79
+ if (code[i] !== '{') {
80
+ // Braceless form — `if (driver.isIOS) await x(); else await y();` (double-click-action).
81
+ const end = code.indexOf(';', i);
82
+ return code.slice(i, end === -1 ? code.length : end);
83
+ }
84
+ let depth = 0;
85
+ for (let j = i; j < code.length; j++) {
86
+ if (code[j] === '{')
87
+ depth++;
88
+ else if (code[j] === '}' && --depth === 0)
89
+ return code.slice(i + 1, j);
90
+ }
91
+ return code.slice(i); // unbalanced source — the caller's fail-loud path reports it
92
+ }
93
+ const startsWithThrow = (body) => /^\s*throw\b/.test(body);
94
+ /**
95
+ * Classify one `driver.isIOS` occurrence. Returns null when the shape is unrecognised, which the
96
+ * caller turns into a throw.
97
+ */
98
+ function classifyOccurrence(code, at) {
99
+ const before = code.slice(0, at).replace(/\s+$/, '');
100
+ const negated = before.endsWith('!');
101
+ const after = code.slice(at + 'driver.isIOS'.length);
102
+ // `driver.isIOS ? <ios> : <android>` — a per-OS read, both arms real. The `?` may sit on the
103
+ // NEXT line: every assertion template formats the ternary across three lines, so a same-line
104
+ // test would miss twenty templates and report them unrecognised.
105
+ if (!negated && /^\s*\?/.test(after))
106
+ return { android: 'yes', ios: 'yes' };
107
+ // Condition forms. `)` closes the `if (`, so the guarded body starts after it.
108
+ const closeParen = after.indexOf(')');
109
+ if (closeParen === -1)
110
+ return null;
111
+ // Anything other than whitespace/`&&`-chained conditions between the flag and the `)` means a
112
+ // shape this file has not been taught to read.
113
+ const between = after.slice(0, closeParen);
114
+ if (!/^[\s)]*$/.test(between) && !/^\s*&&/.test(between))
115
+ return null;
116
+ const body = guardedBody(after, closeParen + 1);
117
+ if (negated) {
118
+ // `!driver.isIOS` guards the ANDROID path.
119
+ // with a throw → Android refuses (the mirror case; none exists today, and if one is added
120
+ // it must not be silently reported as an iOS no-op)
121
+ // without → iOS is skipped with no signal at all: 'noop'
122
+ return startsWithThrow(body)
123
+ ? { android: 'refuses', ios: 'yes' }
124
+ : { android: 'yes', ios: 'noop' };
125
+ }
126
+ // `driver.isIOS` guards the iOS path: a leading throw is a refusal, anything else is real code
127
+ // (the `else` arm carries Android either way).
128
+ return startsWithThrow(body) ? { android: 'yes', ios: 'refuses' } : { android: 'yes', ios: 'yes' };
129
+ }
130
+ /**
131
+ * Per-OS support for one Appium template, read off its body.
132
+ *
133
+ * @throws when the template branches on `driver.isIOS` in an unrecognised shape — deliberately, so
134
+ * the drift gate goes red and a human teaches this file the new shape, rather than the
135
+ * matrix quietly claiming the step works on both.
136
+ */
137
+ function osSupportOf(name, src) {
138
+ const code = (0, exports.stripComments)(src);
139
+ if (!code.includes('driver.isIOS'))
140
+ return { android: 'inherit', ios: 'inherit' };
141
+ let pair = { android: 'yes', ios: 'yes' };
142
+ let seen = 0;
143
+ for (let i = code.indexOf('driver.isIOS'); i !== -1; i = code.indexOf('driver.isIOS', i + 1)) {
144
+ const one = classifyOccurrence(code, i);
145
+ if (!one) {
146
+ throw new Error(`[step-support-matrix] ${name}.hbs branches on driver.isIOS in a shape os-support.ts cannot read ` +
147
+ `(at offset ${i}). Teach classifyOccurrence() the new shape — do NOT let it default, or the ` +
148
+ `matrix will report a one-sided template as working on both.\n` +
149
+ ` context: ${JSON.stringify(code.slice(Math.max(0, i - 40), i + 80))}`);
150
+ }
151
+ seen++;
152
+ pair = { android: worse(pair.android, one.android), ios: worse(pair.ios, one.ios) };
153
+ }
154
+ if (!seen)
155
+ throw new Error(`[step-support-matrix] ${name}.hbs mentions driver.isIOS but no occurrence classified`);
156
+ return pair;
157
+ }
158
+ /** Appium step templates, the same tree `renderStep` searches. */
159
+ const TEMPLATES_DIR = path.join(__dirname, 'templates', 'steps');
160
+ const cache = new Map();
161
+ /** Locate `<name>.hbs` anywhere under the appium step tree (excluding `partials/`). */
162
+ function findTemplate(dir, file) {
163
+ for (const e of fs.readdirSync(dir, { withFileTypes: true })) {
164
+ if (e.isDirectory()) {
165
+ if (e.name === 'partials')
166
+ continue;
167
+ const hit = findTemplate(path.join(dir, e.name), file);
168
+ if (hit)
169
+ return hit;
170
+ }
171
+ else if (e.name === file) {
172
+ return path.join(dir, e.name);
173
+ }
174
+ }
175
+ return null;
176
+ }
177
+ /**
178
+ * Per-OS support for a template BY NAME, memoized — the compile-time `@platform:ios` gate asks this
179
+ * once per distinct template per run.
180
+ *
181
+ * Returns null when the appium adapter ships no such template: that is a different failure (the
182
+ * step is not a mobile step at all) which the existing template-resolution path already reports, so
183
+ * answering "unsupported on iOS" here would be a second, misleading message for one cause.
184
+ */
185
+ function osSupportOfTemplate(name) {
186
+ if (cache.has(name))
187
+ return cache.get(name) ?? null;
188
+ let pair = null;
189
+ try {
190
+ const file = findTemplate(TEMPLATES_DIR, `${name}.hbs`);
191
+ if (file)
192
+ pair = osSupportOf(name, fs.readFileSync(file, 'utf-8'));
193
+ }
194
+ catch {
195
+ pair = null; // an unreadable tree must not break codegen; the matrix gate reports it instead
196
+ }
197
+ cache.set(name, pair);
198
+ return pair;
199
+ }
200
+ //# sourceMappingURL=os-support.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"os-support.js","sourceRoot":"","sources":["../../../../../src/generators/test-generator/adapters/appium/os-support.ts"],"names":[],"mappings":"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"}
@@ -1,2 +1,9 @@
1
- {{!-- Filling a native prompt is iOS-specific and app-dependent; Android rarely has fillable alerts.
2
- Requested value: '{{fillValue}}'. Handle manually if your alert has a text field. --}}
1
+ {{!-- Filling a native prompt has no Appium implementation here. This file used to be comment-ONLY,
2
+ which made it the one member of the web-only set that failed SILENTLY: the comment is stripped,
3
+ so the step emitted nothing, ran, and did nothing — a scenario could fill a prompt and pass
4
+ having never touched it. Every other web-only step on this adapter refuses by name, and now so
5
+ does this one. `alert-fill` also declares `platforms: ['web']`, so the refusal normally arrives
6
+ at COMPILE time (SG-W020); this body is the second line of defence for any future pattern that
7
+ reaches the template without declaring its platforms.
8
+ Requested value: '{{fillValue}}' --}}
9
+ throw new Error("[sungen] filling a native prompt is not supported on the appium adapter. Accept or dismiss the alert instead, drive the app's own text field as a normal element, or tag this scenario @manual:M10.");
@@ -6,5 +6,28 @@
6
6
  const __sources = driver.isIOS
7
7
  ? {{#if (eq capture 'inputValue')}}[await __el.getAttribute('value'), await __el.getAttribute('label')]{{else}}[await __el.getAttribute('label'), await __el.getAttribute('value')]{{/if}}
8
8
  : [await __el.getText(), await __el.getAttribute('content-desc')];
9
- testData.set('{{escapeQuotes varName}}', String(__sources.find(s => s && String(s).trim() !== '') ?? '').trim());
9
+ let __captured = String(__sources.find(s => s && String(s).trim() !== '') ?? '').trim();
10
+ {{#if (eq capture 'inputValue')}}
11
+ {{!-- iOS placeholder guard. Some WebDriverAgent versions echo an empty UITextField's
12
+ placeholder back as its @value (the XCUITest driver documents @value as
13
+ "a complex attribute whose calculation algorithm depends on the actual element type"
14
+ and promises nothing for an empty field). Unguarded, an empty field captures its
15
+ PLACEHOLDER into testData under the author's variable name — and because a capture
16
+ feeds later steps, that corruption propagates silently instead of failing loudly.
17
+ Unlike the assertion path there is no expected value to gate on, so this check is
18
+ unconditional on iOS: one extra round-trip buys a captured value that is not a lie. --}}
19
+ if (driver.isIOS && __captured !== '') {
20
+ let __placeholder = null;
21
+ try { __placeholder = await __el.getAttribute('placeholderValue'); } catch { /* attribute unsupported on this node */ }
22
+ if (__placeholder && String(__placeholder).trim() === __captured) {
23
+ {{!-- Leave a breadcrumb. Unlike the assertion path — which throws at the field in
24
+ question — a discarded capture succeeds silently and the empty string travels to
25
+ whatever step later consumes it, possibly in another file. That downstream failure
26
+ carries nothing linking it back here, so say it out loud now. --}}
27
+ console.warn(` ! [sungen] {{escapeQuotes varName}} read back as its own placeholder ("${__captured}") — captured empty instead`);
28
+ __captured = '';
29
+ }
30
+ }
31
+ {{/if}}
32
+ testData.set('{{escapeQuotes varName}}', __captured);
10
33
  }
@@ -1,2 +1,6 @@
1
- {{!-- No hover on touch devices — no-op. (Hover-revealed content is normally already visible on mobile;
2
- if a tap is actually required, use `User tap [X]` instead.) --}}
1
+ {{!-- No hover on a touch screen. This template used to hold only this comment, so the step compiled
2
+ to an empty line: the scenario claimed to exercise a hover, exercised nothing, and passed.
3
+ `hover-element` now declares platforms: ['web'], so a mobile project is refused at SG-W020
4
+ before reaching here — this throw is the second source agreeing with the first, the treatment
5
+ alert-fill-action already has. --}}
6
+ throw new Error("[sungen] hover is not available on the appium adapter — a touch screen has no hover state. Content a hover reveals is normally already visible on mobile; if the interaction is genuinely required, use `User tap [X]`, otherwise tag the scenario @manual:M10.");
@@ -1,3 +1,4 @@
1
- {{!-- No hover on touch devices — no-op, mirrors hover-action.hbs. (A text-filtered hover target
2
- is normally already visible on mobile; if a tap on the matching element is actually
3
- required, use `User tap [X] item with {{value}}` instead.) --}}
1
+ {{!-- No hover on a touch screen; mirrors hover-action.hbs. Text-filtering the target changes which
2
+ element is picked, not whether hovering it means anything. Braces are avoided in the message
3
+ below on purpose: handlebars would interpolate a literal mustache away to an empty string. --}}
4
+ throw new Error("[sungen] hover is not available on the appium adapter — a touch screen has no hover state. To act on the item matching a value, tap it instead (`User tap [X] item with` your data ref); if the hover itself is the thing under test, tag the scenario @manual:M10.");
@@ -1,2 +1,11 @@
1
+ {{#if (eq action "hover")}}
2
+ {{!-- This template is reached by a MIXED pattern that serves click, double-click AND hover, so it
3
+ cannot be declared web-only without deleting tap from mobile. It also ignored `action` and
4
+ always clicked — which turned `hover [Target X] with {{ref}}` into a TAP on mobile. That is
5
+ worse than the silent no-op the pure hover patterns had: a tap has side effects a hover never
6
+ had, so the scenario changed app state while claiming to hover. Refuse instead. --}}
7
+ throw new Error("[sungen] hover is not available on the appium adapter — a touch screen has no hover state, and this step would otherwise have tapped the element instead. Use `User tap` if acting on it is what you meant, or tag the scenario @manual:M10.");
8
+ {{else}}
1
9
  {{!-- Fallback for an unrecognized action: tap an element by text / content-desc. --}}
2
10
  await $('//*[contains(@content-desc,"{{escapeQuotes dataValue}}") or contains(@text,"{{escapeQuotes dataValue}}")]').click();
11
+ {{/if}}
@@ -1 +1,34 @@
1
- await expect({{> locator}}).toHaveValue('{{escapeQuotes dataValue}}');
1
+ {{!-- Input-value assertion (mobile). The web template asserts toHaveValue(), which
2
+ expect-webdriverio implements as toHaveElementProperty('value') → GET
3
+ element/:id/property/value. That is a DOM command: UiAutomator2 does not implement
4
+ getElementProperty and answers 404 for every call, so the assertion used to fail
5
+ whatever the field held. Read the native value instead, exactly as
6
+ actions/capture-variable.hbs does for `capture: 'inputValue'`:
7
+ · iOS — @value first, then @label (this is the input's OWN value, not its label)
8
+ · Android — getText() (GET element/:id/text), then @content-desc
9
+ `probeNative` is deliberately ignored: it drives a DOM tagName probe for ambiguous
10
+ dropdown/select/combobox targets on web, and a native tree has no tagName to probe. --}}
11
+ {
12
+ const __el = await {{> locator}};
13
+ await __el.waitForDisplayed();
14
+ const __expected = String('{{escapeQuotes dataValue}}').trim();
15
+ const __sources = driver.isIOS
16
+ ? [await __el.getAttribute('value'), await __el.getAttribute('label')]
17
+ : [await __el.getText(), await __el.getAttribute('content-desc')];
18
+ let __actual = String(__sources.find(s => s && String(s).trim() !== '') ?? '').trim();
19
+ {{!-- iOS only, and only when we EXPECT empty: some WebDriverAgent versions echo an empty
20
+ UITextField's placeholder back as its @value. The XCUITest driver documents @value as
21
+ "a complex attribute whose calculation algorithm depends on the actual element type"
22
+ and promises nothing for an empty field, so the behaviour varies by WDA version.
23
+ @placeholderValue is a supported XCUITest attribute; an exact match means the field is
24
+ empty and we simply read its placeholder. Gated on __expected === '' so a non-empty
25
+ assertion pays no extra round-trip and its behaviour is bit-for-bit unchanged. --}}
26
+ if (driver.isIOS && __expected === '' && __actual !== '') {
27
+ let __placeholder = null;
28
+ try { __placeholder = await __el.getAttribute('placeholderValue'); } catch { /* attribute unsupported on this node */ }
29
+ if (__placeholder && String(__placeholder).trim() === __actual) __actual = '';
30
+ }
31
+ if (__actual !== __expected) {
32
+ throw new Error(`[sungen] Expected value "${__expected}" but element reads "${__actual}"`);
33
+ }
34
+ }
@@ -1,12 +1,20 @@
1
- {{!-- Label-value assertion (mobile): locate a visible text node carrying the value.
2
- Android renders text into @text; iOS (RN/Flutter) into @label/@value. The web variant
3
- matches "label: value" as one string — native trees keep label and value as separate
4
- nodes, so only the value is matched here. The expected value must stay a standalone
5
- quoted literal so the runtime-data transform can swap it for testData.get(...). --}}
1
+ {{!-- Label-value assertion (mobile), NO selectors.yaml entry: locate a visible node carrying
2
+ the value anywhere on screen. The web variant matches "label: value" as one string —
3
+ native trees keep label and value as separate nodes, so only the value is matched here.
4
+ Declare an entry for the ref to assert a specific element instead (#666).
5
+
6
+ Android publishes text into @text (native/RN) or @content-desc (Flutter Semantics), and
7
+ which one is populated is a property of the toolkit, not of the widget. This used to be
8
+ `UiSelector().text(...)`, which cannot read @content-desc at all and so missed on every
9
+ Flutter screen; UiSelector has no OR, hence the xpath. iOS scatters it across
10
+ label/value, which the predicate already ORs.
11
+
12
+ The expected value must stay a standalone quoted literal so the runtime-data transform
13
+ can swap it for testData.get(...). --}}
6
14
  {
7
15
  const __expected = String('{{escapeQuotes dataValue}}').trim();
8
16
  const __sel = driver.isIOS
9
17
  ? `-ios predicate string:label == "${__expected}" OR value == "${__expected}"`
10
- : `android=new UiSelector().text("${__expected}")`;
18
+ : `//*[@text="${__expected}" or @content-desc="${__expected}"]`;
11
19
  await expect($(__sel)).toBeDisplayed();
12
20
  }
@@ -0,0 +1,21 @@
1
+ {{!-- The OS/hardware back gesture. NOT the in-app Back button, and the difference is the whole
2
+ point: Android's system back does NOT run the screen's own handler unless the app wires it
3
+ (onBackPressed / BackHandler), so a defect on this path is invisible to a `tap [Back]`
4
+ scenario. That is exactly why `viewpoint-ledger.ts` treats the in-app control as a wrong
5
+ substitution for the "OS/hardware back gesture" theme (#626) — and why this template has to
6
+ exist for that theme to have any answer other than @manual.
7
+
8
+ Android: KEYCODE_BACK through the execute alias rather than `driver.pressKeyCode(4)`, for the
9
+ same reason `location-services-action` uses `mobile: isGpsEnabled` — WDIO v9 dropped the
10
+ legacy JSONWP wrappers, while a driver extension call cannot be removed from under us.
11
+
12
+ iOS: there IS no hardware back. `driver.back()` is WebDriverAgent's NAVIGATION back (it drives
13
+ the nav-bar back affordance), not the user's edge swipe-back gesture. Both OSes are therefore
14
+ implemented — the matrix reads ✅/✅ and the semantic difference is stated here and in the
15
+ matrix note, rather than dressed up as a refusal it is not. --}}
16
+ if (driver.isIOS) {
17
+ // WDA navigation-back — NOT the user's swipe-back gesture; iOS exposes no hardware back key.
18
+ await driver.back();
19
+ } else {
20
+ await driver.execute('mobile: pressKey', { keycode: 4 }); // KEYCODE_BACK — the real hardware back
21
+ }
@@ -1,2 +1,18 @@
1
+ {{#if missing}}
2
+ {{!-- No coordinates in the step and no data reference to read. Refuse by name: this used to default
3
+ to latitude 0 / longitude 0, which is Null Island — a real place, so the test passed there. --}}
4
+ throw new Error("[sungen] cannot set location: no coordinates and no data reference in \"{{escapeQuotes missing}}\". Write the numbers inline (`User set location to 10.7769, 106.7009`), or point at a test-data key holding \"<latitude> <longitude>\".");
5
+ {{else}}
6
+ {{#if dataRef}}
7
+ {{!-- Coordinates read from test-data AT RUN TIME, so one compiled spec serves every environment. --}}
8
+ await (async () => {
9
+ const __raw = String(testData.get('{{dataRef}}'));
10
+ const __nums = __raw.match(/-?\d+(?:\.\d+)?/g) || [];
11
+ if (__nums.length < 2) throw new Error(`[sungen] test-data '{{dataRef}}' = "${__raw}" — setting a location needs two numbers, "<latitude> <longitude>"; found ${__nums.length}.`);
12
+ await driver.setGeoLocation({ latitude: Number(__nums[0]), longitude: Number(__nums[1]), altitude: 0 });
13
+ })();
14
+ {{else}}
1
15
  {{!-- Mock the device GPS location. --}}
2
16
  await driver.setGeoLocation({ latitude: {{latitude}}, longitude: {{longitude}}, altitude: 0 });
17
+ {{/if}}
18
+ {{/if}}
@@ -1,6 +1,14 @@
1
1
  {{#if platform}}
2
2
  // sungen:platform={{platform}}
3
3
  {{/if}}
4
+ {{#if screenshotOnFailure}}
5
+ // @screenshot:on-failure — used to be web-only and silently dropped on mobile.
6
+ // wdio.conf.ts's afterTest hook already captures screenshot + page source + device log for EVERY
7
+ // mobile failure, so the tag is not what turns evidence on. What it declares is that this feature's
8
+ // evidence is ESSENTIAL: it survives SUNGEN_MOBILE_EVIDENCE=off, which otherwise suppresses capture
9
+ // for the run. Set at module load, so it covers every scenario in this spec.
10
+ (globalThis as Record<string, unknown>).__sungenForceEvidence = true;
11
+ {{/if}}
4
12
  {{#if runtimeData}}
5
13
 
6
14
  const testData = TestDataLoader.load('{{screenName}}', '{{featureFileName}}');
@@ -1,2 +1,5 @@
1
- { const tableRow = {{> locator}}.getByRole('row').nth({{{rowIndexExpr}}});
1
+ {{!-- Row universe: `rows:` on the table entry (what EVERY row matches — a <div> list has no
2
+ role=row, so the ARIA chain below is 0 elements and this assertion would be vacuous, issue #688)
3
+ wins; otherwise the historical ARIA chain, byte-identical. --}}
4
+ { const tableRow = {{> locator}}{{#if rows}}.locator('{{escapeQuotes rows}}'){{else}}.getByRole('row'){{/if}}.nth({{{rowIndexExpr}}});
2
5
  await {{{actionLocator}}}.{{action}}(); }
@@ -1,2 +1,5 @@
1
- { const tableRow = {{> locator}}.getByRole('row').filter({ hasText: '{{escapeQuotes filterValue}}' });
1
+ {{!-- Row universe: `rows:` on the table entry (what EVERY row matches — a <div> list has no
2
+ role=row, so the ARIA chain below is 0 elements and this assertion would be vacuous, issue #688)
3
+ wins; otherwise the historical ARIA chain, byte-identical. --}}
4
+ { const tableRow = {{> locator}}{{#if rows}}.locator('{{escapeQuotes rows}}'){{else}}.getByRole('row'){{/if}}.filter({ hasText: '{{escapeQuotes filterValue}}' });
2
5
  await {{{actionLocator}}}.{{action}}(); }
@@ -9,6 +9,13 @@ await expect({{> locator}}).toBeChecked();
9
9
  { const el = {{> locator}};
10
10
  if ((await el.getAttribute('aria-selected')) !== null) {
11
11
  await expect(el).toHaveAttribute('aria-selected', 'true');
12
+ } else if ((await el.getAttribute('aria-current')) !== null) {
13
+ {{!-- aria-current marks the CURRENT item of a navigation set (WAI-ARIA 1.1): a Next.js tab strip
14
+ of plain <a> links carries aria-current="page" and has neither aria-selected nor a checked
15
+ state, so toBeChecked() threw "Not a checkbox or radio button" on a correct step. Any value
16
+ but "false" ("page", "step", "true", …) means current. Probed AFTER aria-selected: a real
17
+ role=tab owns the stronger signal. --}}
18
+ await expect(el).toHaveAttribute('aria-current', /^(?!false$)./);
12
19
  } else {
13
20
  await expect(el).toBeChecked();
14
21
  }
@@ -4,6 +4,13 @@
4
4
  { const el = {{> locator-base}}.filter({ hasText: {{> has-text-value dataValue=dataValue exact=exact}} }){{> locator-nth}};
5
5
  if ((await el.getAttribute('aria-selected')) !== null) {
6
6
  await expect(el).toHaveAttribute('aria-selected', 'true');
7
+ } else if ((await el.getAttribute('aria-current')) !== null) {
8
+ {{!-- aria-current marks the CURRENT item of a navigation set (WAI-ARIA 1.1): a Next.js tab strip
9
+ of plain <a> links carries aria-current="page" and has neither aria-selected nor a checked
10
+ state, so toBeChecked() threw "Not a checkbox or radio button" on a correct step. Any value
11
+ but "false" ("page", "step", "true", …) means current. Probed AFTER aria-selected: a real
12
+ role=tab owns the stronger signal. --}}
13
+ await expect(el).toHaveAttribute('aria-current', /^(?!false$)./);
7
14
  } else {
8
15
  await expect(el).toBeChecked();
9
16
  }
@@ -1 +1,4 @@
1
- await expect({{> locator}}.locator('tbody').getByRole('row', { includeHidden: true }).filter({ visible: true })).toHaveCount(0);
1
+ {{!-- Row universe: `rows:` on the table entry (what EVERY row matches — a <div> list has no
2
+ role=row, so the ARIA chain below is 0 elements and this assertion would be vacuous, issue #688)
3
+ wins; otherwise the historical ARIA chain, byte-identical. --}}
4
+ await expect({{> locator}}{{#if rows}}.locator('{{escapeQuotes rows}}'){{else}}.locator('tbody').getByRole('row', { includeHidden: true }){{/if}}.filter({ visible: true })).toHaveCount(0);
@@ -1,6 +1,9 @@
1
1
  {
2
2
  const table = {{> locator}};
3
- const rows = table.locator('tbody').getByRole('row', { includeHidden: true }).filter({ visible: true });
3
+ {{!-- Row universe: `rows:` on the table entry (what EVERY row matches — a <div> list has no
4
+ role=row, so the ARIA chain below is 0 elements and this assertion would be vacuous, issue #688)
5
+ wins; otherwise the historical ARIA chain, byte-identical. --}}
6
+ const rows = table{{#if rows}}.locator('{{escapeQuotes rows}}'){{else}}.locator('tbody').getByRole('row', { includeHidden: true }){{/if}}.filter({ visible: true });
4
7
  {{#each assertions}}
5
8
  {{{this}}}
6
9
  {{/each~}}
@@ -1 +1,4 @@
1
- await expect({{> locator}}.locator('tbody').getByRole('row', { includeHidden: true }).filter({ visible: true })).toHaveCount({{expectedCount}});
1
+ {{!-- Row universe: `rows:` on the table entry (what EVERY row matches — a <div> list has no
2
+ role=row, so the ARIA chain below is 0 elements and this assertion would be vacuous, issue #688)
3
+ wins; otherwise the historical ARIA chain, byte-identical. --}}
4
+ await expect({{> locator}}{{#if rows}}.locator('{{escapeQuotes rows}}'){{else}}.locator('tbody').getByRole('row', { includeHidden: true }){{/if}}.filter({ visible: true })).toHaveCount({{expectedCount}});
@@ -1,17 +1,25 @@
1
1
  {{~#if rowSelector}}
2
+ {{!-- Declared row LABEL = one positioned row. The toHaveCount(1) guard is what turns "I used the
3
+ row label as the row SHAPE" (div.job-row, issue #688) into a named error instead of a
4
+ strict-mode message pointing at the wrong line. --}}
2
5
  {{~#if isGiven}}
3
- const tableRow = {{> locator}}.locator('{{rowSelector}}');
6
+ const tableRow = {{> locator}}.locator('{{escapeQuotes rowSelector}}');
7
+ await expect(tableRow, 'row label "[{{escapeQuotes rowLabel}}]" must position exactly ONE row (its selectors.yaml entry is relative to the table) — if this selector describes what EVERY row looks like, declare it as `rows:` on the TABLE entry instead').toHaveCount(1);
4
8
  await tableRow.filter({ hasText: '{{escapeQuotes filterValue}}' }).waitFor();
5
9
  {{~else}}
6
- const tableRow = {{> locator}}.locator('{{rowSelector}}');
10
+ const tableRow = {{> locator}}.locator('{{escapeQuotes rowSelector}}');
11
+ await expect(tableRow, 'row label "[{{escapeQuotes rowLabel}}]" must position exactly ONE row (its selectors.yaml entry is relative to the table) — if this selector describes what EVERY row looks like, declare it as `rows:` on the TABLE entry instead').toHaveCount(1);
7
12
  await expect(tableRow).toContainText('{{escapeQuotes filterValue}}');
8
13
  {{~/if}}
9
14
  {{~else}}
15
+ {{!-- Row universe: `rows:` on the table entry (what EVERY row matches — a <div> list has no
16
+ role=row, so the ARIA chain below is 0 elements and this assertion would be vacuous, issue #688)
17
+ wins; otherwise the historical ARIA chain, byte-identical. --}}
10
18
  {{~#if isGiven}}
11
- const tableRow = {{> locator}}.getByRole('row', { includeHidden: true }).filter({ hasText: '{{escapeQuotes filterValue}}' }).filter({ visible: true });
19
+ const tableRow = {{> locator}}{{#if rows}}.locator('{{escapeQuotes rows}}'){{else}}.getByRole('row', { includeHidden: true }){{/if}}.filter({ hasText: '{{escapeQuotes filterValue}}' }).filter({ visible: true });
12
20
  await tableRow.waitFor();
13
21
  {{~else}}
14
- const tableRow = {{> locator}}.getByRole('row', { includeHidden: true }).filter({ hasText: '{{escapeQuotes filterValue}}' }).filter({ visible: true });
22
+ const tableRow = {{> locator}}{{#if rows}}.locator('{{escapeQuotes rows}}'){{else}}.getByRole('row', { includeHidden: true }){{/if}}.filter({ hasText: '{{escapeQuotes filterValue}}' }).filter({ visible: true });
15
23
  await expect(tableRow).toBeVisible();
16
24
  {{~/if}}
17
25
  {{~/if}}
@@ -1,5 +1,17 @@
1
+ {{~#if rowSelector}}
2
+ {{!-- Declared row LABEL = one positioned row (same contract as the exists twin). The row must not
3
+ hold the value. Block-scoped: this step does not enter row scope, so it must not redeclare
4
+ the function-level tableRow the exists step binds. --}}
5
+ { const tableRow = {{> locator}}.locator('{{escapeQuotes rowSelector}}');
6
+ await expect(tableRow, 'row label "[{{escapeQuotes rowLabel}}]" must position exactly ONE row (its selectors.yaml entry is relative to the table) — if this selector describes what EVERY row looks like, declare it as `rows:` on the TABLE entry instead').toHaveCount(1);
7
+ await expect(tableRow).not.toContainText('{{escapeQuotes filterValue}}'); }
8
+ {{~else}}
9
+ {{!-- Row universe: `rows:` on the table entry (what EVERY row matches — a <div> list has no
10
+ role=row, so the ARIA chain below is 0 elements and this assertion would be vacuous, issue #688)
11
+ wins; otherwise the historical ARIA chain, byte-identical. --}}
1
12
  {{~#if isGiven}}
2
- await {{> locator}}.getByRole('row', { includeHidden: true }).filter({ hasText: '{{escapeQuotes filterValue}}' }).filter({ visible: true }).waitFor({ state: 'hidden' });
13
+ await {{> locator}}{{#if rows}}.locator('{{escapeQuotes rows}}'){{else}}.getByRole('row', { includeHidden: true }){{/if}}.filter({ hasText: '{{escapeQuotes filterValue}}' }).filter({ visible: true }).waitFor({ state: 'hidden' });
3
14
  {{~else}}
4
- await expect({{> locator}}.getByRole('row', { includeHidden: true }).filter({ hasText: '{{escapeQuotes filterValue}}' }).filter({ visible: true })).toHaveCount(0);
15
+ await expect({{> locator}}{{#if rows}}.locator('{{escapeQuotes rows}}'){{else}}.getByRole('row', { includeHidden: true }){{/if}}.filter({ hasText: '{{escapeQuotes filterValue}}' }).filter({ visible: true })).toHaveCount(0);
16
+ {{~/if}}
5
17
  {{~/if}}
@@ -1,22 +1,22 @@
1
1
  {{#if parentLocator}}{{parentLocator}}.{{#switch strategy~}}
2
- {{~#case 'testid'}}getByTestId('{{value}}'){{/case~}}
2
+ {{~#case 'testid'}}getByTestId('{{escapeQuotes value}}'){{/case~}}
3
3
  {{~#case 'role'}}{{#if name}}getByRole('{{role}}', { name: '{{escapeQuotes name}}'{{#if exact}}, exact: true{{/if}} }){{else}}getByRole('{{role}}'{{#if exact}}, { exact: true }{{/if}}){{/if}}{{/case~}}
4
4
  {{~#case 'placeholder'}}getByPlaceholder('{{escapeQuotes value}}'{{#if exact}}, { exact: true }{{/if}}){{/case~}}
5
5
  {{~#case 'label'}}getByLabel('{{escapeQuotes value}}'{{#if exact}}, { exact: true }{{/if}}){{/case~}}
6
6
  {{~#case 'text'}}getByText('{{escapeQuotes value}}'{{#if exact}}, { exact: true }{{/if}}){{/case~}}
7
- {{~#case 'locator'}}locator('{{value}}'){{/case~}}
8
- {{~#case 'id'}}locator('{{value}}'){{/case~}}
9
- {{~#default}}locator('{{value}}'){{/default~}}
7
+ {{~#case 'locator'}}locator('{{escapeQuotes value}}'){{/case~}}
8
+ {{~#case 'id'}}locator('{{escapeQuotes value}}'){{/case~}}
9
+ {{~#default}}locator('{{escapeQuotes value}}'){{/default~}}
10
10
  {{~/switch}}{{else~}}
11
11
  {{#if scope}}{{#if (eq scope "dialog")}}{{pageRoot}}.{{else}}{{pageRoot}}.getByLabel('{{escapeQuotes scope}}').{{/if}}{{#switch strategy~}}
12
- {{~#case 'testid'}}getByTestId('{{value}}'){{/case~}}
12
+ {{~#case 'testid'}}getByTestId('{{escapeQuotes value}}'){{/case~}}
13
13
  {{~#case 'role'}}{{#if name}}getByRole('{{role}}', { name: '{{escapeQuotes name}}'{{#if exact}}, exact: true{{/if}} }){{else}}getByRole('{{role}}'{{#if exact}}, { exact: true }{{/if}}){{/if}}{{/case~}}
14
14
  {{~#case 'placeholder'}}getByPlaceholder('{{escapeQuotes value}}'{{#if exact}}, { exact: true }{{/if}}){{/case~}}
15
15
  {{~#case 'label'}}getByLabel('{{escapeQuotes value}}'{{#if exact}}, { exact: true }{{/if}}){{/case~}}
16
16
  {{~#case 'text'}}getByText('{{escapeQuotes value}}'{{#if exact}}, { exact: true }{{/if}}){{/case~}}
17
- {{~#case 'locator'}}locator('{{value}}'){{/case~}}
18
- {{~#case 'id'}}locator('{{value}}'){{/case~}}
19
- {{~#default}}locator('{{value}}'){{/default~}}
17
+ {{~#case 'locator'}}locator('{{escapeQuotes value}}'){{/case~}}
18
+ {{~#case 'id'}}locator('{{escapeQuotes value}}'){{/case~}}
19
+ {{~#default}}locator('{{escapeQuotes value}}'){{/default~}}
20
20
  {{~/switch}}{{else}}{{#switch strategy~}}
21
21
  {{~#case 'testid'}}{{> locator-strategies/testid}}{{/case~}}
22
22
  {{~#case 'role'}}{{> locator-strategies/role}}{{/case~}}
@@ -1 +1 @@
1
- page.locator('{{selector}}')
1
+ page.locator('{{escapeQuotes selector}}')
@@ -1 +1 @@
1
- {{pageRoot}}.getByTestId('{{value}}')
1
+ {{pageRoot}}.getByTestId('{{escapeQuotes value}}')