@sun-asterisk/sungen 3.2.3 → 3.2.5

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 (184) hide show
  1. package/dist/cli/commands/dashboard.js +2 -2
  2. package/dist/cli/commands/dashboard.js.map +1 -1
  3. package/dist/cli/commands/delivery.d.ts.map +1 -1
  4. package/dist/cli/commands/delivery.js +22 -5
  5. package/dist/cli/commands/delivery.js.map +1 -1
  6. package/dist/cli/commands/feedback.d.ts +11 -0
  7. package/dist/cli/commands/feedback.d.ts.map +1 -1
  8. package/dist/cli/commands/feedback.js +154 -57
  9. package/dist/cli/commands/feedback.js.map +1 -1
  10. package/dist/cli/commands/login.d.ts +19 -0
  11. package/dist/cli/commands/login.d.ts.map +1 -0
  12. package/dist/cli/commands/login.js +76 -0
  13. package/dist/cli/commands/login.js.map +1 -0
  14. package/dist/cli/commands/qa-feedback.d.ts +14 -0
  15. package/dist/cli/commands/qa-feedback.d.ts.map +1 -0
  16. package/dist/cli/commands/qa-feedback.js +83 -0
  17. package/dist/cli/commands/qa-feedback.js.map +1 -0
  18. package/dist/cli/index.js +4 -0
  19. package/dist/cli/index.js.map +1 -1
  20. package/dist/dashboard/snapshot-builder.d.ts +3 -0
  21. package/dist/dashboard/snapshot-builder.d.ts.map +1 -1
  22. package/dist/dashboard/snapshot-builder.js +155 -52
  23. package/dist/dashboard/snapshot-builder.js.map +1 -1
  24. package/dist/dashboard/templates/index.html +23 -23
  25. package/dist/dashboard/types.d.ts +8 -0
  26. package/dist/dashboard/types.d.ts.map +1 -1
  27. package/dist/exporters/api-testcase-formatter.d.ts +55 -0
  28. package/dist/exporters/api-testcase-formatter.d.ts.map +1 -0
  29. package/dist/exporters/api-testcase-formatter.js +304 -0
  30. package/dist/exporters/api-testcase-formatter.js.map +1 -0
  31. package/dist/exporters/csv-exporter.d.ts +13 -1
  32. package/dist/exporters/csv-exporter.d.ts.map +1 -1
  33. package/dist/exporters/csv-exporter.js +35 -6
  34. package/dist/exporters/csv-exporter.js.map +1 -1
  35. package/dist/exporters/feature-parser.d.ts +4 -0
  36. package/dist/exporters/feature-parser.d.ts.map +1 -1
  37. package/dist/exporters/feature-parser.js +7 -2
  38. package/dist/exporters/feature-parser.js.map +1 -1
  39. package/dist/exporters/json-exporter.d.ts.map +1 -1
  40. package/dist/exporters/json-exporter.js +11 -3
  41. package/dist/exporters/json-exporter.js.map +1 -1
  42. package/dist/exporters/playwright-report-parser.d.ts +21 -2
  43. package/dist/exporters/playwright-report-parser.d.ts.map +1 -1
  44. package/dist/exporters/playwright-report-parser.js +52 -12
  45. package/dist/exporters/playwright-report-parser.js.map +1 -1
  46. package/dist/exporters/scenario-merger.d.ts.map +1 -1
  47. package/dist/exporters/scenario-merger.js +50 -2
  48. package/dist/exporters/scenario-merger.js.map +1 -1
  49. package/dist/exporters/spec-parser.js +1 -1
  50. package/dist/exporters/spec-parser.js.map +1 -1
  51. package/dist/exporters/test-data-resolver.d.ts +7 -0
  52. package/dist/exporters/test-data-resolver.d.ts.map +1 -1
  53. package/dist/exporters/test-data-resolver.js +13 -0
  54. package/dist/exporters/test-data-resolver.js.map +1 -1
  55. package/dist/exporters/types.d.ts +13 -0
  56. package/dist/exporters/types.d.ts.map +1 -1
  57. package/dist/exporters/xlsx-exporter.d.ts.map +1 -1
  58. package/dist/exporters/xlsx-exporter.js +131 -42
  59. package/dist/exporters/xlsx-exporter.js.map +1 -1
  60. package/dist/generators/gherkin-parser/index.d.ts +15 -0
  61. package/dist/generators/gherkin-parser/index.d.ts.map +1 -1
  62. package/dist/generators/gherkin-parser/index.js +33 -1
  63. package/dist/generators/gherkin-parser/index.js.map +1 -1
  64. package/dist/generators/test-generator/adapters/appium/templates/steps/actions/check-action.hbs +29 -8
  65. package/dist/generators/test-generator/adapters/appium/templates/steps/actions/scroll-action.hbs +21 -17
  66. package/dist/generators/test-generator/adapters/appium/templates/steps/actions/toggle-action.hbs +3 -0
  67. package/dist/generators/test-generator/adapters/appium/templates/steps/actions/uncheck-action.hbs +26 -6
  68. package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/is-hidden-assertion.hbs +3 -1
  69. package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/visible-assertion.hbs +14 -0
  70. package/dist/generators/test-generator/adapters/appium/templates/steps/gestures/tap-top-action.hbs +3 -0
  71. package/dist/generators/test-generator/adapters/appium/templates/steps/partials/locator.hbs +9 -2
  72. package/dist/generators/test-generator/adapters/appium/templates/test-file.hbs +115 -0
  73. package/dist/generators/test-generator/adapters/playwright/templates/steps/actions/table-action-in-row.hbs +1 -1
  74. package/dist/generators/test-generator/patterns/index.d.ts +12 -0
  75. package/dist/generators/test-generator/patterns/index.d.ts.map +1 -1
  76. package/dist/generators/test-generator/patterns/index.js +21 -1
  77. package/dist/generators/test-generator/patterns/index.js.map +1 -1
  78. package/dist/harness/script-check.d.ts.map +1 -1
  79. package/dist/harness/script-check.js +10 -5
  80. package/dist/harness/script-check.js.map +1 -1
  81. package/dist/orchestrator/ai-skills/writer.d.ts.map +1 -1
  82. package/dist/orchestrator/ai-skills/writer.js +46 -4
  83. package/dist/orchestrator/ai-skills/writer.js.map +1 -1
  84. package/dist/orchestrator/project-initializer.d.ts +14 -0
  85. package/dist/orchestrator/project-initializer.d.ts.map +1 -1
  86. package/dist/orchestrator/project-initializer.js +58 -0
  87. package/dist/orchestrator/project-initializer.js.map +1 -1
  88. package/dist/orchestrator/templates/ai-src/commands/add-flow.md +52 -5
  89. package/dist/orchestrator/templates/ai-src/commands/create-test.md +7 -0
  90. package/dist/orchestrator/templates/ai-src/commands/feedback.md +6 -6
  91. package/dist/orchestrator/templates/ai-src/commands/locale.md +26 -4
  92. package/dist/orchestrator/templates/ai-src/skills/sungen-delivery/SKILL.md +5 -4
  93. package/dist/orchestrator/templates/wdio.conf.ts +39 -3
  94. package/dist/tools/sungen-api/edit-in-editor.d.ts +15 -0
  95. package/dist/tools/sungen-api/edit-in-editor.d.ts.map +1 -0
  96. package/dist/tools/sungen-api/edit-in-editor.js +85 -0
  97. package/dist/tools/sungen-api/edit-in-editor.js.map +1 -0
  98. package/dist/tools/sungen-api/feedback-client.d.ts +24 -0
  99. package/dist/tools/sungen-api/feedback-client.d.ts.map +1 -0
  100. package/dist/tools/sungen-api/feedback-client.js +53 -0
  101. package/dist/tools/sungen-api/feedback-client.js.map +1 -0
  102. package/dist/tools/sungen-api/read-stdin.d.ts +6 -0
  103. package/dist/tools/sungen-api/read-stdin.d.ts.map +1 -0
  104. package/dist/tools/sungen-api/read-stdin.js +19 -0
  105. package/dist/tools/sungen-api/read-stdin.js.map +1 -0
  106. package/dist/tools/sungen-auth/api-base-url.d.ts +14 -0
  107. package/dist/tools/sungen-auth/api-base-url.d.ts.map +1 -0
  108. package/dist/tools/sungen-auth/api-base-url.js +28 -0
  109. package/dist/tools/sungen-auth/api-base-url.js.map +1 -0
  110. package/dist/tools/sungen-auth/credentials-store.d.ts +19 -0
  111. package/dist/tools/sungen-auth/credentials-store.d.ts.map +1 -0
  112. package/dist/tools/sungen-auth/credentials-store.js +94 -0
  113. package/dist/tools/sungen-auth/credentials-store.js.map +1 -0
  114. package/dist/tools/sungen-auth/login-flow.d.ts +33 -0
  115. package/dist/tools/sungen-auth/login-flow.d.ts.map +1 -0
  116. package/dist/tools/sungen-auth/login-flow.js +107 -0
  117. package/dist/tools/sungen-auth/login-flow.js.map +1 -0
  118. package/dist/tools/sungen-auth/loopback-server.d.ts +20 -0
  119. package/dist/tools/sungen-auth/loopback-server.d.ts.map +1 -0
  120. package/dist/tools/sungen-auth/loopback-server.js +132 -0
  121. package/dist/tools/sungen-auth/loopback-server.js.map +1 -0
  122. package/dist/tools/sungen-auth/open-browser.d.ts +10 -0
  123. package/dist/tools/sungen-auth/open-browser.d.ts.map +1 -0
  124. package/dist/tools/sungen-auth/open-browser.js +43 -0
  125. package/dist/tools/sungen-auth/open-browser.js.map +1 -0
  126. package/dist/tools/sungen-auth/pkce.d.ts +13 -0
  127. package/dist/tools/sungen-auth/pkce.d.ts.map +1 -0
  128. package/dist/tools/sungen-auth/pkce.js +27 -0
  129. package/dist/tools/sungen-auth/pkce.js.map +1 -0
  130. package/dist/tools/sungen-auth/sungen-auth-types.d.ts +41 -0
  131. package/dist/tools/sungen-auth/sungen-auth-types.d.ts.map +1 -0
  132. package/dist/tools/sungen-auth/sungen-auth-types.js +7 -0
  133. package/dist/tools/sungen-auth/sungen-auth-types.js.map +1 -0
  134. package/package.json +4 -3
  135. package/src/cli/commands/dashboard.ts +2 -2
  136. package/src/cli/commands/delivery.ts +24 -6
  137. package/src/cli/commands/feedback.ts +123 -51
  138. package/src/cli/commands/login.ts +75 -0
  139. package/src/cli/commands/qa-feedback.ts +77 -0
  140. package/src/cli/index.ts +4 -0
  141. package/src/dashboard/snapshot-builder.ts +146 -46
  142. package/src/dashboard/templates/index.html +23 -23
  143. package/src/dashboard/types.ts +10 -2
  144. package/src/exporters/api-testcase-formatter.ts +325 -0
  145. package/src/exporters/csv-exporter.ts +46 -7
  146. package/src/exporters/feature-parser.ts +7 -2
  147. package/src/exporters/json-exporter.ts +11 -3
  148. package/src/exporters/playwright-report-parser.ts +50 -11
  149. package/src/exporters/scenario-merger.ts +49 -3
  150. package/src/exporters/spec-parser.ts +1 -1
  151. package/src/exporters/test-data-resolver.ts +13 -0
  152. package/src/exporters/types.ts +13 -0
  153. package/src/exporters/xlsx-exporter.ts +155 -49
  154. package/src/generators/gherkin-parser/index.ts +44 -1
  155. package/src/generators/test-generator/adapters/appium/templates/steps/actions/check-action.hbs +29 -8
  156. package/src/generators/test-generator/adapters/appium/templates/steps/actions/scroll-action.hbs +21 -17
  157. package/src/generators/test-generator/adapters/appium/templates/steps/actions/toggle-action.hbs +3 -0
  158. package/src/generators/test-generator/adapters/appium/templates/steps/actions/uncheck-action.hbs +26 -6
  159. package/src/generators/test-generator/adapters/appium/templates/steps/assertions/is-hidden-assertion.hbs +3 -1
  160. package/src/generators/test-generator/adapters/appium/templates/steps/assertions/visible-assertion.hbs +14 -0
  161. package/src/generators/test-generator/adapters/appium/templates/steps/gestures/tap-top-action.hbs +3 -0
  162. package/src/generators/test-generator/adapters/appium/templates/steps/partials/locator.hbs +9 -2
  163. package/src/generators/test-generator/adapters/appium/templates/test-file.hbs +115 -0
  164. package/src/generators/test-generator/adapters/playwright/templates/steps/actions/table-action-in-row.hbs +1 -1
  165. package/src/generators/test-generator/patterns/index.ts +21 -1
  166. package/src/harness/script-check.ts +10 -5
  167. package/src/orchestrator/ai-skills/writer.ts +39 -4
  168. package/src/orchestrator/project-initializer.ts +59 -0
  169. package/src/orchestrator/templates/ai-src/commands/add-flow.md +52 -5
  170. package/src/orchestrator/templates/ai-src/commands/create-test.md +7 -0
  171. package/src/orchestrator/templates/ai-src/commands/feedback.md +6 -6
  172. package/src/orchestrator/templates/ai-src/commands/locale.md +26 -4
  173. package/src/orchestrator/templates/ai-src/skills/sungen-delivery/SKILL.md +5 -4
  174. package/src/orchestrator/templates/wdio.conf.ts +39 -3
  175. package/src/tools/sungen-api/edit-in-editor.ts +48 -0
  176. package/src/tools/sungen-api/feedback-client.ts +62 -0
  177. package/src/tools/sungen-api/read-stdin.ts +15 -0
  178. package/src/tools/sungen-auth/api-base-url.ts +23 -0
  179. package/src/tools/sungen-auth/credentials-store.ts +57 -0
  180. package/src/tools/sungen-auth/login-flow.ts +135 -0
  181. package/src/tools/sungen-auth/loopback-server.ts +113 -0
  182. package/src/tools/sungen-auth/open-browser.ts +42 -0
  183. package/src/tools/sungen-auth/pkce.ts +27 -0
  184. package/src/tools/sungen-auth/sungen-auth-types.ts +45 -0
@@ -9,7 +9,43 @@
9
9
  */
10
10
 
11
11
  import { FeatureMetadata, OrderedStep, ScenarioMetadata, SpecFileData, SpecTest } from './types';
12
- import { isStepsBaseScenario, isSampleScaffoldScenario } from './feature-parser';
12
+ import { isStepsBaseScenario, isSampleScaffoldScenario, extractTestcaseType } from './feature-parser';
13
+
14
+ /**
15
+ * Parse a @manual scenario's `# Tester verifies:` comment block into procedure vs
16
+ * expected. Each item is a numbered line, optionally prefixed with a label:
17
+ * 1. Setup: … 2. Action: … → procedure (Steps column)
18
+ * 3. Observable: … 4. Oracle: … → expected (Expected Results column)
19
+ * A label matching observ|oracle|expect|result|then|assert → Expected; anything else
20
+ * (Setup/Action/Precondition/unlabelled) → Steps. A non-numbered line following a
21
+ * numbered item is a continuation (e.g. a wrapped URL) and is appended to it. Pre-amble
22
+ * lines (the `MANUAL:` rationale, the `Tester verifies:` header, separators) are ignored.
23
+ */
24
+ function parseManualComments(comments: string[]): { steps: OrderedStep[]; expected: OrderedStep[] } {
25
+ const steps: OrderedStep[] = [];
26
+ const expected: OrderedStep[] = [];
27
+ let last: OrderedStep | null = null;
28
+ const isExpectedLabel = (l: string) => /observ|oracle|expect|result|then|assert/i.test(l);
29
+ for (const raw of comments) {
30
+ const line = raw.trim();
31
+ if (!line) continue;
32
+ // Section divider — all divider chars (`-----`) OR a titled rule (`--- Foo ---`).
33
+ // A run of 2+ leading divider chars is never a step or a continuation.
34
+ if (/^[-=*_]{2,}/.test(line)) { last = null; continue; }
35
+ const m = line.match(/^\d+[.)]\s*(?:([A-Za-z][A-Za-z /]*?):\s*)?(.+)$/);
36
+ if (m) {
37
+ const label = (m[1] || '').trim();
38
+ const text = label ? `${label}: ${m[2].trim()}` : m[2].trim();
39
+ const step: OrderedStep = { text, bucket: isExpectedLabel(label) ? 'then' : 'when' };
40
+ (step.bucket === 'then' ? expected : steps).push(step);
41
+ last = step;
42
+ } else if (last) {
43
+ last.text += ' ' + line; // continuation of the previous numbered item
44
+ }
45
+ // else: pre-amble before any numbered item (rationale / header) → skip
46
+ }
47
+ return { steps, expected };
48
+ }
13
49
 
14
50
  export interface MergedScenario {
15
51
  /** Scenario metadata from .feature */
@@ -76,6 +112,15 @@ export function mergeFeatureAndSpec(
76
112
  const ownPreThen = firstThenIdx >= 0 ? ownOrdered.slice(0, firstThenIdx) : ownOrdered;
77
113
  const ownPostThen = firstThenIdx >= 0 ? ownOrdered.slice(firstThenIdx) : [];
78
114
 
115
+ // A @manual scenario carries no Gherkin steps — its tester procedure lives in the
116
+ // `# Tester verifies:` comment block. Parse it into Steps (Setup/Action) + Expected
117
+ // (Observable/Oracle) so the delivery report shows the real manual procedure, not
118
+ // just the Background nav. No-op for automated scenarios (comments stay notes).
119
+ const manualParsed =
120
+ extractTestcaseType(scenario.tags) === 'Manual' && scenario.comments?.length
121
+ ? parseManualComments(scenario.comments)
122
+ : { steps: [] as OrderedStep[], expected: [] as OrderedStep[] };
123
+
79
124
  result.push({
80
125
  feature: scenario,
81
126
  spec: specTest,
@@ -98,8 +143,9 @@ export function mergeFeatureAndSpec(
98
143
  ...feature.backgroundOrderedSteps,
99
144
  ...(base?.orderedSteps ?? []),
100
145
  ...ownPreThen,
146
+ ...manualParsed.steps,
101
147
  ],
102
- resolvedExpected: ownPostThen,
148
+ resolvedExpected: [...ownPostThen, ...manualParsed.expected],
103
149
  });
104
150
  }
105
151
 
@@ -116,7 +162,7 @@ function findMatchingSpecTest(scenario: ScenarioMetadata, tests: SpecTest[]): Sp
116
162
  if (exact) return exact;
117
163
 
118
164
  // Fallback: match by VP-ID prefix (ignore trailing text differences)
119
- const vpMatch = scenario.name.match(/^(VP-[A-Z]+-\d+[a-zA-Z]?)/);
165
+ const vpMatch = scenario.name.match(/^(VP-[A-Z][A-Z0-9]*-\d+[a-zA-Z]?)/);
120
166
  if (vpMatch) {
121
167
  const vpId = vpMatch[1];
122
168
  const byVp = tests.find((t) => t.vpId === vpId);
@@ -146,7 +146,7 @@ function classifyBlock(block: CodeBlock): 'precondition' | 'step' | 'expectation
146
146
  * Try to extract VP ID (e.g., VP-UI-001) from the start of a scenario title.
147
147
  */
148
148
  function extractVpId(title: string): string | undefined {
149
- const match = title.match(/^(VP-[A-Z]+-\d+[a-zA-Z]?)\b/);
149
+ const match = title.match(/^(VP-[A-Z][A-Z0-9]*-\d+[a-zA-Z]?)\b/);
150
150
  return match ? match[1] : undefined;
151
151
  }
152
152
 
@@ -133,6 +133,19 @@ function truncate(s: string, max: number): string {
133
133
  return s.substring(0, max - 1) + '…';
134
134
  }
135
135
 
136
+ /**
137
+ * Resolve `{{var}}` test-data placeholders in a step / expected-result line to their concrete values,
138
+ * so the delivery report shows what QA actually verifies (e.g. `text contains {{app_version}}` →
139
+ * `text contains Version 1.0.0`). Dotted keys (`{{a.b}}`) are supported. A key absent from the
140
+ * test-data map (or resolving to empty) is left as the literal `{{var}}` — never blanked.
141
+ */
142
+ export function substituteTestDataVars(text: string, testData: Record<string, string>): string {
143
+ return text.replace(/\{\{\s*([\w.]+)\s*\}\}/g, (m, key: string) => {
144
+ const v = testData[key];
145
+ return v === undefined || v === null || v === '' ? m : String(v);
146
+ });
147
+ }
148
+
136
149
  /**
137
150
  * Resolve the test-data file path for a given screen.
138
151
  */
@@ -88,6 +88,12 @@ export interface ScenarioMetadata {
88
88
  * Use this when ordering matters — e.g. mid-scenario `Given` after `When`.
89
89
  */
90
90
  orderedSteps: OrderedStep[];
91
+ /**
92
+ * Comment lines inside the scenario body (leading `# ` stripped). For a @manual
93
+ * scenario the tester procedure lives here (as `# Tester verifies:` comments), not
94
+ * as Gherkin steps — the merger parses these into Steps/Expected for the report.
95
+ */
96
+ comments?: string[];
91
97
  }
92
98
 
93
99
  /**
@@ -164,5 +170,12 @@ export interface ApiCatalogEntry {
164
170
  description?: string;
165
171
  body?: unknown;
166
172
  params?: unknown;
173
+ /** Per-endpoint header templates (e.g. authorization: "Bearer :token"); `:param` tokens
174
+ * bind at runtime from the same args as the body/path. */
175
+ headers?: Record<string, string>;
176
+ /** Request-body wire format — decides the Content-Type the request sends.
177
+ * Absent/`json` → application/json, `form` → application/x-www-form-urlencoded,
178
+ * `multipart` → multipart/form-data. */
179
+ encoding?: 'json' | 'form' | 'multipart';
167
180
  expect?: { status?: number | string };
168
181
  }
@@ -30,6 +30,15 @@ const META_FILL = 'FFDCE6F1'; // soft blue for meta rows
30
30
  const CATEGORY_FILL = 'FFD9D2E9'; // same lavender for group dividers (Accessing / GUI / Function)
31
31
  const BORDER_COLOR = 'FFBFBFBF';
32
32
 
33
+ // Shared masthead + table styling (BM-2-901-13 header band, summary band, Overview table).
34
+ const TIMES = 'Times New Roman';
35
+ const ARIAL_FONT = 'Arial';
36
+ const BLACK = { argb: 'FF000000' };
37
+ const WHITE = { argb: 'FFFFFFFF' };
38
+ const LAVENDER = { argb: 'FFD9D2E9' };
39
+ const thinBlack = { style: 'thin' as const, color: BLACK };
40
+ const allBordersBlack = { top: thinBlack, left: thinBlack, bottom: thinBlack, right: thinBlack };
41
+
33
42
  type AnyCell = ExcelJS.Cell;
34
43
  type AnyRow = ExcelJS.Row;
35
44
 
@@ -96,6 +105,9 @@ export function renderXlsxMultiSheet(sheets: LocaleSheet[]): ExcelJS.Workbook {
96
105
  const wb = new ExcelJS.Workbook();
97
106
  wb.creator = 'sungen delivery';
98
107
  wb.created = new Date();
108
+ // Overview first — a per-sheet roll-up (Passed/Failed/Pending/N/A/Remain/Total)
109
+ // plus a grand-total row, so QA sees the whole picture before the detail sheets.
110
+ addOverviewSheet(wb, sheets);
99
111
  for (const s of sheets) {
100
112
  addTestcaseSheet(wb, s.sheetName, s.summary, s.rows, s.specLink);
101
113
  }
@@ -103,57 +115,23 @@ export function renderXlsxMultiSheet(sheets: LocaleSheet[]): ExcelJS.Workbook {
103
115
  }
104
116
 
105
117
  /**
106
- * Append one BM-2-901-13 testcase sheet to the given workbook. Sheet content
107
- * (layout, summary band, data rows, auto-filter, protection) is identical to
108
- * the single-sheet `renderXlsx` output only the sheet name differs.
118
+ * Render the shared BM-2-901-13 masthead (rows 1-4) onto a worksheet:
119
+ * Sun* logo (A1:C3), TESTCASE title (D1:F3), No/Version/Issue-Date block
120
+ * (G1:H3), company line (A4:F4), ISO line (G4:H4). Every sheet in a delivery
121
+ * workbook — Overview, Auto, Manual — carries this same header band.
109
122
  */
110
- function addTestcaseSheet(
123
+ function renderReportHeaderBand(
111
124
  wb: ExcelJS.Workbook,
112
- sheetName: string,
113
- summary: ScreenSummary,
114
- rows: TestCaseRow[],
115
- specLink: string,
125
+ ws: ExcelJS.Worksheet,
126
+ titleLabel: string,
116
127
  ): void {
117
- const ws = wb.addWorksheet(sheetName, {
118
- views: [{ state: 'frozen', ySplit: 0, xSplit: 0 }],
119
- });
120
-
121
128
  const issueDate = (() => {
122
129
  const d = new Date();
123
130
  return `${String(d.getDate()).padStart(2, '0')}/${String(d.getMonth() + 1).padStart(2, '0')}/${d.getFullYear()}`;
124
131
  })();
125
132
 
126
- // -- Column widths matching template_report.xlsx (Sample sheet) --
127
- ws.columns = [
128
- { width: 20 }, // A — TC ID (wider to fit long flow IDs like FLOW-KUDO-…)
129
- { width: 12.5 }, // B — Screen/Function
130
- { width: 15.38 }, // C — Big item
131
- { width: 16.25 }, // D — Medium item
132
- { width: 17 }, // E — Test Object
133
- { width: 25.5 }, // F — Pre-condition
134
- { width: 24.88 }, // G — Test Data
135
- { width: 24.88 }, // H — Steps
136
- { width: 41 }, // I — Expected results
137
- { width: 12 }, // J — Priority
138
- { width: 13.5 }, // K — Testcase type
139
- { width: 15.88 }, // L — Test Result
140
- { width: 13.5 }, // M — Executed Date
141
- { width: 16.5 }, // N — Test Executor
142
- { width: 23 }, // O — Test Environment
143
- { width: 29 }, // P — Note
144
- ];
145
-
146
- // -- Top metadata band (rows 1-4) — 1:1 with template_report.xlsx --
147
- const TIMES = 'Times New Roman';
148
- const ARIAL_FONT = 'Arial';
149
- const BLACK = { argb: 'FF000000' };
150
- const WHITE = { argb: 'FFFFFFFF' };
151
- const LAVENDER = { argb: 'FFD9D2E9' };
152
- const thinBlack = { style: 'thin' as const, color: BLACK };
153
- const allBordersBlack = { top: thinBlack, left: thinBlack, bottom: thinBlack, right: thinBlack };
154
-
155
- // Ensure rows 1..8 exist before merging.
156
- for (let r = 1; r <= 8; r++) ws.getRow(r);
133
+ // Ensure rows 1..4 exist before merging.
134
+ for (let r = 1; r <= 4; r++) ws.getRow(r);
157
135
 
158
136
  ws.mergeCells('A1:C3');
159
137
  ws.mergeCells('D1:F3');
@@ -174,10 +152,6 @@ function addTestcaseSheet(
174
152
  extension: 'png',
175
153
  });
176
154
  // Centre a fixed-size 90×51 logo inside A1:C3 using absolute EMU offsets.
177
- // Col widths (A=20, B=12.5, C=15.38) → pixels via Excel's `w*7+5` rule:
178
- // A=145, B=92.5, C=112.66 ⇒ A1:C3 ≈ 350 px wide.
179
- // Left padding for a 90-px image = (350−90)/2 ≈ 130 px = 1,237,500 EMU.
180
- // 3 default rows × 20 px = 60 px; top padding ≈ 4.5 px ≈ 42,862 EMU.
181
155
  ws.addImage(imageId, {
182
156
  tl: { nativeCol: 0, nativeColOff: 1237500, nativeRow: 0, nativeRowOff: 42862 } as unknown as ExcelJS.Anchor,
183
157
  ext: { width: 90, height: 51 },
@@ -186,7 +160,6 @@ function addTestcaseSheet(
186
160
  } catch { /* logo is decorative — never block export */ }
187
161
 
188
162
  // D1 (TESTCASE title)
189
- const titleLabel = `${summary.screen.toUpperCase()} TESTCASE`;
190
163
  const d1 = ws.getCell('D1');
191
164
  d1.value = titleLabel;
192
165
  d1.font = { bold: true, size: 18, name: TIMES };
@@ -227,6 +200,134 @@ function addTestcaseSheet(
227
200
  g4.font = { bold: true, size: 10, name: TIMES };
228
201
  g4.alignment = { horizontal: 'right', vertical: 'middle', wrapText: true };
229
202
  g4.border = allBordersBlack;
203
+ }
204
+
205
+ // ---------------------------------------------------------------------------
206
+ // Overview sheet — the first tab in every multi-sheet delivery workbook.
207
+ // Rolls up each testcase sheet's Passed/Failed/Pending/N/A/Remain/Total into
208
+ // one summary table (+ a grand-total row) under the shared masthead. Reads
209
+ // only each sheet's summary — never touches the detail sheets themselves.
210
+ // ---------------------------------------------------------------------------
211
+
212
+ /** Column headers for the Overview roll-up table (cols A..H). */
213
+ const OVERVIEW_HEADERS = ['#', 'Sheet name', 'Passed', 'Failed', 'Pending', 'N/A', 'Remain', 'Total'];
214
+
215
+ /**
216
+ * Prepend an "Overview" worksheet summarizing every sheet in `sheets`.
217
+ *
218
+ * `Remain = Total − Passed − N/A` (i.e. Failed + Pending) so the number
219
+ * reconciles with the "Remaining" cell in each detail sheet's summary band.
220
+ */
221
+ function addOverviewSheet(wb: ExcelJS.Workbook, sheets: LocaleSheet[]): void {
222
+ const ws = wb.addWorksheet('Overview');
223
+
224
+ // Table starts at column B row 7 (A is a thin left margin). Widths keep the
225
+ // A..H masthead readable AND the count columns tidy.
226
+ ws.columns = [
227
+ { width: 4 }, // A — left margin
228
+ { width: 6 }, // B — #
229
+ { width: 26 }, // C — Sheet name
230
+ { width: 13 }, // D — Passed
231
+ { width: 13 }, // E — Failed
232
+ { width: 13 }, // F — Pending
233
+ { width: 13 }, // G — N/A
234
+ { width: 13 }, // H — Remain
235
+ { width: 13 }, // I — Total
236
+ ];
237
+
238
+ const titleScreen = sheets[0]?.summary.screen ?? '';
239
+ renderReportHeaderBand(wb, ws, `${titleScreen.toUpperCase()} TESTCASE`);
240
+
241
+ // Roll-up table: header on row 7, first column B (index 2).
242
+ const HEADER_ROW = 7;
243
+ const FIRST_COL = 2; // column B
244
+ const headerRow = ws.getRow(HEADER_ROW);
245
+ headerRow.height = 20;
246
+ OVERVIEW_HEADERS.forEach((label, i) => {
247
+ const c = headerRow.getCell(FIRST_COL + i);
248
+ c.value = label;
249
+ c.font = { bold: true, size: 10, color: BLACK, name: ARIAL_FONT };
250
+ c.fill = { type: 'pattern', pattern: 'solid', fgColor: LAVENDER };
251
+ c.alignment = { horizontal: 'center', vertical: 'middle', wrapText: true };
252
+ c.border = allBordersBlack;
253
+ });
254
+
255
+ const totals = { passed: 0, failed: 0, pending: 0, na: 0, remain: 0, total: 0 };
256
+
257
+ const writeDataRow = (rowIdx: number, values: ExcelJS.CellValue[], isTotal: boolean): void => {
258
+ const row = ws.getRow(rowIdx);
259
+ values.forEach((v, i) => {
260
+ const c = row.getCell(FIRST_COL + i);
261
+ c.value = v;
262
+ c.font = { bold: isTotal, size: 10, color: BLACK, name: ARIAL_FONT };
263
+ // The '#' + 'Sheet name' columns read left; the counts centre like the band.
264
+ c.alignment = { horizontal: i <= 1 ? 'left' : 'center', vertical: 'middle' };
265
+ c.border = allBordersBlack;
266
+ if (isTotal) c.fill = { type: 'pattern', pattern: 'solid', fgColor: LAVENDER };
267
+ });
268
+ };
269
+
270
+ let rowIdx = HEADER_ROW + 1;
271
+ sheets.forEach((s, i) => {
272
+ const su = s.summary;
273
+ const remain = Math.max(0, su.total - su.passed - su.na);
274
+ writeDataRow(rowIdx, [i + 1, s.sheetName, su.passed, su.failed, su.pending, su.na, remain, su.total], false);
275
+ totals.passed += su.passed;
276
+ totals.failed += su.failed;
277
+ totals.pending += su.pending;
278
+ totals.na += su.na;
279
+ totals.remain += remain;
280
+ totals.total += su.total;
281
+ rowIdx += 1;
282
+ });
283
+
284
+ writeDataRow(
285
+ rowIdx,
286
+ ['', 'Total', totals.passed, totals.failed, totals.pending, totals.na, totals.remain, totals.total],
287
+ true,
288
+ );
289
+ }
290
+
291
+ /**
292
+ * Append one BM-2-901-13 testcase sheet to the given workbook. Sheet content
293
+ * (layout, summary band, data rows, auto-filter, protection) is identical to
294
+ * the single-sheet `renderXlsx` output — only the sheet name differs.
295
+ */
296
+ function addTestcaseSheet(
297
+ wb: ExcelJS.Workbook,
298
+ sheetName: string,
299
+ summary: ScreenSummary,
300
+ rows: TestCaseRow[],
301
+ specLink: string,
302
+ ): void {
303
+ const ws = wb.addWorksheet(sheetName, {
304
+ views: [{ state: 'frozen', ySplit: 0, xSplit: 0 }],
305
+ });
306
+
307
+ // -- Column widths matching template_report.xlsx (Sample sheet) --
308
+ ws.columns = [
309
+ { width: 20 }, // A — TC ID (wider to fit long flow IDs like FLOW-KUDO-…)
310
+ { width: 12.5 }, // B — Screen/Function
311
+ { width: 15.38 }, // C — Big item
312
+ { width: 16.25 }, // D — Medium item
313
+ { width: 17 }, // E — Test Object
314
+ { width: 25.5 }, // F — Pre-condition
315
+ { width: 24.88 }, // G — Test Data
316
+ { width: 24.88 }, // H — Steps
317
+ { width: 41 }, // I — Expected results
318
+ { width: 12 }, // J — Priority
319
+ { width: 13.5 }, // K — Testcase type
320
+ { width: 15.88 }, // L — Test Result
321
+ { width: 13.5 }, // M — Executed Date
322
+ { width: 16.5 }, // N — Test Executor
323
+ { width: 23 }, // O — Test Environment
324
+ { width: 29 }, // P — Note
325
+ ];
326
+
327
+ // -- Top metadata band (rows 1-4) — shared BM-2-901-13 masthead --
328
+ renderReportHeaderBand(wb, ws, `${summary.screen.toUpperCase()} TESTCASE`);
329
+ // Materialize rows 5..8 so the summary band + row-9 spacer land at the right offsets.
330
+ for (let r = 5; r <= 8; r++) ws.getRow(r);
230
331
 
231
332
  // -- Row 5: spacer (height auto-fit). --
232
333
 
@@ -348,7 +449,12 @@ function addTestcaseSheet(
348
449
  const emittedGroups = new Set<string>();
349
450
 
350
451
  function emitGroup(groupName: string, groupRows: TestCaseRow[]): void {
351
- const divider = ws.addRow(['', groupName]);
452
+ // Column A (TC ID) must stay TRULY blank on a divider never write '' here. COUNTA counts
453
+ // empty-string cells, so an '' in A made the generated total (=COUNTA(A15:A…)) over-count by
454
+ // one per group divider when recalculated (e.g. in Google Sheets). Set only the group name in B;
455
+ // the lavender band still spans every column via the fill loop below (style only, no value).
456
+ const divider = ws.addRow([]);
457
+ divider.getCell(2).value = groupName;
352
458
  divider.getCell(2).font = { bold: true };
353
459
  divider.eachCell({ includeEmpty: false }, (cell) => {
354
460
  cell.fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: CATEGORY_FILL } };
@@ -37,6 +37,13 @@ export interface ParsedScenario {
37
37
  stepsName?: string; // set when scenario has @steps:<name> — marks it as a reusable block
38
38
  extendsName?: string; // set when scenario has @extend:<name> — merges base steps inline
39
39
  hookType?: 'beforeAll' | 'afterEach' | 'afterAll'; // set when scenario has @beforeAll/@afterEach/@afterAll
40
+ /**
41
+ * Comment lines inside the scenario body (between its `Scenario:` line and the next
42
+ * scenario), leading `# ` stripped. `@cucumber/gherkin` keeps comments in a separate
43
+ * top-level array; we range-attach them here so the exporter can surface a @manual
44
+ * scenario's `# Tester verifies:` procedure (its only "steps") in the delivery report.
45
+ */
46
+ comments?: string[];
40
47
  }
41
48
 
42
49
  export interface ParsedFeature {
@@ -82,17 +89,53 @@ export class GherkinParser {
82
89
  const pathMatch = description.match(/^\s*Path:\s*(.+)$/m);
83
90
  const path = pathMatch ? pathMatch[1].trim() : undefined;
84
91
 
92
+ const scenarios = this.parseScenarios(feature);
93
+ this.attachScenarioComments(gherkinDocument, feature, scenarios);
94
+
85
95
  return {
86
96
  name: feature.name,
87
97
  description,
88
98
  path,
89
99
  tags,
90
100
  background: this.parseBackground(feature),
91
- scenarios: this.parseScenarios(feature),
101
+ scenarios,
92
102
  sourceFile: filePath,
93
103
  };
94
104
  }
95
105
 
106
+ /**
107
+ * Range-attach top-level Gherkin comments to the scenario whose body contains them.
108
+ * A comment belongs to scenario i when its line is after scenario i's `Scenario:` line
109
+ * and before scenario i+1's header (its first tag line, or its `Scenario:` line). The
110
+ * leading `# ` is stripped. Used to surface a @manual scenario's tester-procedure comments
111
+ * (its only "steps") in the delivery report.
112
+ */
113
+ private attachScenarioComments(
114
+ doc: Messages.GherkinDocument,
115
+ feature: Messages.Feature,
116
+ scenarios: ParsedScenario[]
117
+ ): void {
118
+ const comments = doc.comments || [];
119
+ if (!comments.length) return;
120
+ const scenChildren = feature.children
121
+ .filter((c) => c.scenario)
122
+ .map((c) => c.scenario!);
123
+ // Same filter/order as parseScenarios → indices line up with `scenarios`.
124
+ for (let i = 0; i < scenChildren.length && i < scenarios.length; i++) {
125
+ const startLine = scenChildren[i].location.line;
126
+ const next = scenChildren[i + 1];
127
+ let endLine = Number.POSITIVE_INFINITY;
128
+ if (next) {
129
+ const tagLines = (next.tags || []).map((t) => t.location.line);
130
+ endLine = Math.min(next.location.line, ...(tagLines.length ? tagLines : [next.location.line]));
131
+ }
132
+ const body = comments
133
+ .filter((cm) => cm.location.line > startLine && cm.location.line < endLine)
134
+ .map((cm) => cm.text.replace(/^\s*#\s?/, '').replace(/\s+$/, ''));
135
+ if (body.length) scenarios[i].comments = body;
136
+ }
137
+ }
138
+
96
139
  /**
97
140
  * Parse background steps
98
141
  */
@@ -1,10 +1,31 @@
1
- {{!-- WebdriverIO/Appium has no .check(). Read the current state and tap only if it's not already
2
- on, so the step is idempotent (re-running a passing test won't flip it back off). Falls through
3
- to a plain tap if the state can't be read. --}}
4
- {
1
+ {{!-- WebdriverIO/Appium has no .check(). Ensure the toggle ends up ON: read the current state and
2
+ tap only if it's not already on, so the step is idempotent (re-running a passing test, or
3
+ running under `noReset` with unknown persisted state, won't flip it back off). State is read
4
+ the same way the checked/unchecked assertions read it — `checked` attribute on Android,
5
+ `value` attribute on iOS ('1' = on) — since that's what actually carries state on a Flutter
6
+ composite row; `isSelected()` is a fallback for widgets where it doesn't. The tap itself uses
7
+ the trailing-edge coordinate method (not a centre .click()): on a full-width composite row the
8
+ a11y node merges the label with the switch, so a centre tap lands on the label and doesn't
9
+ flip the control. --}}
10
+ await (async () => {
5
11
  const __cb = await {{> locator}};
12
+ // Fail loud with the REAL cause when the element never resolved — getElementRect(undefined)
13
+ // otherwise throws a cryptic `Malformed type for "elementId"` that masks the not-found (#430).
14
+ if (!__cb.elementId) throw new Error((__cb as any).error?.message || `Element not found: ${__cb.selector}`);
6
15
  let __on = false;
7
- try { __on = await __cb.isSelected(); }
8
- catch { try { __on = (await __cb.getAttribute('checked')) === 'true'; } catch { /* state unreadable — tap anyway */ } }
9
- if (!__on) await __cb.click();
10
- }
16
+ try { __on = driver.isIOS ? (await __cb.getAttribute('value')) === '1' : (await __cb.getAttribute('checked')) === 'true'; }
17
+ catch { try { __on = await __cb.isSelected(); } catch { /* state unreadable — tap anyway */ } }
18
+ if (__on) return;
19
+
20
+ const r = await driver.getElementRect(__cb.elementId);
21
+ if (r && r.width) {
22
+ // Trailing switch: ~right edge, vertical centre. The inset clears the label/track padding while
23
+ // staying on the switch even on a full-width composite row.
24
+ const x = Math.round(r.x + r.width - Math.max(48, r.width * 0.08));
25
+ const y = Math.round(r.y + r.height / 2);
26
+ if (driver.isIOS) { await driver.execute('mobile: tap', { x, y }); }
27
+ else { await driver.execute('mobile: clickGesture', { x, y }); }
28
+ } else {
29
+ await __cb.click();
30
+ }
31
+ })();
@@ -1,22 +1,26 @@
1
1
  if (driver.isIOS) {
2
- // iOS (XCUITest) has no UiScrollable. Swipe up in bounded steps until the target is on screen (a
3
- // below-fold element isn't in the XCUITest tree until scrolled). A settle `pause` between swipes
4
- // paces the loop so it doesn't flood `execute/sync` (rapid back-to-back swipes are a common source
5
- // of flakiness; issue #394). Bounded (cap 10) so a missing target can't loop forever. NOTE: this
6
- // does NOT fix an app that crashes mid-scroll on its own — that's an app-stability issue, not codegen.
7
- const __t = $({{> appium-selector-expr}});
8
- for (let __i = 0; __i < 10 && !(await __t.isDisplayed().catch(() => false)); __i++) {
9
- await driver.execute('mobile: swipe', { direction: 'up' });
10
- await driver.pause(400); // settle: let the app render between swipes (don't flood execute/sync)
11
- }
2
+ // iOS (XCUITest) has no UiScrollable. Delegate to __ensureVisible: geometry-gated small drags
3
+ // (a below-fold Flutter element stays in the tree with a 0x0 rect and isDisplayed() is unreliable
4
+ // on XCUITest, so a full-screen-swipe loop gated on isDisplayed overshoots the target it swiped
5
+ // clean past section headers on the converter Settings screen). NOTE: this does NOT fix an app
6
+ // that crashes mid-scroll on its own — that's an app-stability issue, not codegen (issue #394).
7
+ await __ensureVisible({{> appium-selector-expr}});
12
8
  } else {
9
+ const __t = $({{> appium-selector-expr}});
13
10
  {{#if (eq strategy 'accessibility-id')}}
14
- // Android UiScrollable scrolls until the target's content-desc is on screen, then stops — no
15
- // overshoot (which would drop a virtualized Flutter item from the a11y tree). Robust on Flutter/RN.
16
- await $('android=new UiScrollable(new UiSelector().scrollable(true)).scrollIntoView(new UiSelector().descriptionContains("{{escapeQuotes value}}"))');
17
- {{else}}
18
- // Generic fallback: framework-agnostic moderate scroll gesture (wdio .scrollIntoView() can't always
19
- // find a native ScrollView on Flutter/RN). Modest percent avoids over-scrolling a virtualized list.
20
- await driver.execute('mobile: scrollGesture', { left: 60, top: 420, width: 960, height: 1320, direction: 'down', percent: 0.6 });
11
+ // Optional first try: UiScrollable scrolls until the target's content-desc is on screen, then
12
+ // stops — no overshoot (which would drop a virtualized Flutter item from the a11y tree). This
13
+ // no-ops (rather than throwing past callers) when the app exposes no `scrollable(true)` node
14
+ // common on Flutter/Compose, where the scroll view isn't a native Android ScrollView — so it
15
+ // can't be relied on alone; the bounded loop below is what actually guarantees reachability.
16
+ await $('android=new UiScrollable(new UiSelector().scrollable(true)).scrollIntoView(new UiSelector().descriptionContains("{{escapeQuotes value}}"))').catch(() => {}); // no scrollable node — fall through to the gesture loop
21
17
  {{/if}}
18
+ // Bounded scroll-gesture loop (mirrors the iOS branch above): a single 0.6-percent scroll can
19
+ // land a near item but not a deeper one below the fold, so keep scrolling down until the target
20
+ // is displayed. Capped (10) so a missing target can't loop forever; a short pause between scrolls
21
+ // lets a virtualized Flutter/RN list render before the next gesture.
22
+ for (let __i = 0; __i < 10 && !(await __t.isDisplayed().catch(() => false)); __i++) {
23
+ await driver.execute('mobile: scrollGesture', { left: 60, top: 420, width: 960, height: 1320, direction: 'down', percent: 0.6 });
24
+ await driver.pause(400); // settle: let the app render between scrolls (don't flood execute/sync)
25
+ }
22
26
  }
@@ -6,6 +6,9 @@
6
6
  on/off state idempotently instead. --}}
7
7
  await (async () => {
8
8
  const e = await {{> locator}};
9
+ // Fail loud with the REAL cause when the element never resolved — getElementRect(undefined)
10
+ // otherwise throws a cryptic `Malformed type for "elementId"` that masks the not-found (#430).
11
+ if (!e.elementId) throw new Error((e as any).error?.message || `Element not found: ${e.selector}`);
9
12
  const r = await driver.getElementRect(e.elementId);
10
13
  if (r && r.width) {
11
14
  // Trailing switch: ~right edge, vertical centre. The inset clears the label/track padding while
@@ -1,8 +1,28 @@
1
- {{!-- WebdriverIO/Appium has no .uncheck(). Tap only if currently on, so the step is idempotent. --}}
2
- {
1
+ {{!-- WebdriverIO/Appium has no .uncheck(). Ensure the toggle ends up OFF: tap only if currently on,
2
+ so the step is idempotent under repeat runs / persisted `noReset` state. State read + tap
3
+ method mirror check-action.hbs (see there for why): platform-correct attribute read
4
+ (`checked`/`value`) with an isSelected() fallback, then a trailing-edge coordinate tap rather
5
+ than a centre .click() (a centre tap on a full-width composite row hits the label, not the
6
+ switch). --}}
7
+ await (async () => {
3
8
  const __cb = await {{> locator}};
9
+ // Fail loud with the REAL cause when the element never resolved — getElementRect(undefined)
10
+ // otherwise throws a cryptic `Malformed type for "elementId"` that masks the not-found (#430).
11
+ if (!__cb.elementId) throw new Error((__cb as any).error?.message || `Element not found: ${__cb.selector}`);
4
12
  let __on = false;
5
- try { __on = await __cb.isSelected(); }
6
- catch { try { __on = (await __cb.getAttribute('checked')) === 'true'; } catch { /* state unreadable — leave as-is */ } }
7
- if (__on) await __cb.click();
8
- }
13
+ try { __on = driver.isIOS ? (await __cb.getAttribute('value')) === '1' : (await __cb.getAttribute('checked')) === 'true'; }
14
+ catch { try { __on = await __cb.isSelected(); } catch { /* state unreadable — leave as-is */ } }
15
+ if (!__on) return;
16
+
17
+ const r = await driver.getElementRect(__cb.elementId);
18
+ if (r && r.width) {
19
+ // Trailing switch: ~right edge, vertical centre. The inset clears the label/track padding while
20
+ // staying on the switch even on a full-width composite row.
21
+ const x = Math.round(r.x + r.width - Math.max(48, r.width * 0.08));
22
+ const y = Math.round(r.y + r.height / 2);
23
+ if (driver.isIOS) { await driver.execute('mobile: tap', { x, y }); }
24
+ else { await driver.execute('mobile: clickGesture', { x, y }); }
25
+ } else {
26
+ await __cb.click();
27
+ }
28
+ })();
@@ -1 +1,3 @@
1
- await expect({{> locator}}).not.toBeDisplayed();
1
+ {{!-- plain=true: no scroll-into-view. An element asserted hidden is expected absent, so hunting for
2
+ it by scrolling is pointless and would strand later same-scenario steps that sit above. --}}
3
+ await expect({{> locator plain=true}}).not.toBeDisplayed();
@@ -1 +1,15 @@
1
+ {{!-- iOS: XCUITest's `visible` attribute false-negatives on real, on-screen elements (snapshot
2
+ artifact, amplified on Flutter) — the reliable oracle there is GEOMETRY (an off-screen
3
+ Flutter element keeps a 0x0 rect). Simple-case selectors route through __assertVisible
4
+ (scrolls into view, then asserts existence + a real on-screen rect on iOS; plain
5
+ toBeDisplayed on Android). Parent/scope/nth chains keep toBeDisplayed() — those are
6
+ container-scoped lookups the bare-selector helper can't express. --}}
7
+ {{~#if parentLocator~}}
1
8
  await expect({{> locator}}).toBeDisplayed();
9
+ {{~else if scope~}}
10
+ await expect({{> locator}}).toBeDisplayed();
11
+ {{~else if nth~}}
12
+ await expect({{> locator}}).toBeDisplayed();
13
+ {{~else~}}
14
+ await __assertVisible({{> appium-selector-expr}});
15
+ {{~/if}}
@@ -1,5 +1,8 @@
1
1
  await (async () => {
2
2
  const e = await {{> locator}};
3
+ // Fail loud with the REAL cause when the element never resolved — getElementRect(undefined)
4
+ // otherwise throws a cryptic `Malformed type for "elementId"` that masks the not-found (#430).
5
+ if (!e.elementId) throw new Error((e as any).error?.message || `Element not found: ${e.selector}`);
3
6
  // Visible-point tap: tap near the TOP of the element (its centre may sit under a floating bottom
4
7
  // bar). getElementRect is W3C-standard → works on BOTH Android and iOS (the old `bounds` attribute
5
8
  // is Android-only and throws on iOS).