@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.
- package/dist/cli/commands/dashboard.js +2 -2
- package/dist/cli/commands/dashboard.js.map +1 -1
- package/dist/cli/commands/delivery.d.ts.map +1 -1
- package/dist/cli/commands/delivery.js +22 -5
- package/dist/cli/commands/delivery.js.map +1 -1
- package/dist/cli/commands/feedback.d.ts +11 -0
- package/dist/cli/commands/feedback.d.ts.map +1 -1
- package/dist/cli/commands/feedback.js +154 -57
- package/dist/cli/commands/feedback.js.map +1 -1
- package/dist/cli/commands/login.d.ts +19 -0
- package/dist/cli/commands/login.d.ts.map +1 -0
- package/dist/cli/commands/login.js +76 -0
- package/dist/cli/commands/login.js.map +1 -0
- package/dist/cli/commands/qa-feedback.d.ts +14 -0
- package/dist/cli/commands/qa-feedback.d.ts.map +1 -0
- package/dist/cli/commands/qa-feedback.js +83 -0
- package/dist/cli/commands/qa-feedback.js.map +1 -0
- package/dist/cli/index.js +4 -0
- package/dist/cli/index.js.map +1 -1
- package/dist/dashboard/snapshot-builder.d.ts +3 -0
- package/dist/dashboard/snapshot-builder.d.ts.map +1 -1
- package/dist/dashboard/snapshot-builder.js +155 -52
- package/dist/dashboard/snapshot-builder.js.map +1 -1
- package/dist/dashboard/templates/index.html +23 -23
- package/dist/dashboard/types.d.ts +8 -0
- package/dist/dashboard/types.d.ts.map +1 -1
- package/dist/exporters/api-testcase-formatter.d.ts +55 -0
- package/dist/exporters/api-testcase-formatter.d.ts.map +1 -0
- package/dist/exporters/api-testcase-formatter.js +304 -0
- package/dist/exporters/api-testcase-formatter.js.map +1 -0
- package/dist/exporters/csv-exporter.d.ts +13 -1
- package/dist/exporters/csv-exporter.d.ts.map +1 -1
- package/dist/exporters/csv-exporter.js +35 -6
- package/dist/exporters/csv-exporter.js.map +1 -1
- package/dist/exporters/feature-parser.d.ts +4 -0
- package/dist/exporters/feature-parser.d.ts.map +1 -1
- package/dist/exporters/feature-parser.js +7 -2
- package/dist/exporters/feature-parser.js.map +1 -1
- package/dist/exporters/json-exporter.d.ts.map +1 -1
- package/dist/exporters/json-exporter.js +11 -3
- package/dist/exporters/json-exporter.js.map +1 -1
- package/dist/exporters/playwright-report-parser.d.ts +21 -2
- package/dist/exporters/playwright-report-parser.d.ts.map +1 -1
- package/dist/exporters/playwright-report-parser.js +52 -12
- package/dist/exporters/playwright-report-parser.js.map +1 -1
- package/dist/exporters/scenario-merger.d.ts.map +1 -1
- package/dist/exporters/scenario-merger.js +50 -2
- package/dist/exporters/scenario-merger.js.map +1 -1
- package/dist/exporters/spec-parser.js +1 -1
- package/dist/exporters/spec-parser.js.map +1 -1
- package/dist/exporters/test-data-resolver.d.ts +7 -0
- package/dist/exporters/test-data-resolver.d.ts.map +1 -1
- package/dist/exporters/test-data-resolver.js +13 -0
- package/dist/exporters/test-data-resolver.js.map +1 -1
- package/dist/exporters/types.d.ts +13 -0
- package/dist/exporters/types.d.ts.map +1 -1
- package/dist/exporters/xlsx-exporter.d.ts.map +1 -1
- package/dist/exporters/xlsx-exporter.js +131 -42
- package/dist/exporters/xlsx-exporter.js.map +1 -1
- package/dist/generators/gherkin-parser/index.d.ts +15 -0
- package/dist/generators/gherkin-parser/index.d.ts.map +1 -1
- package/dist/generators/gherkin-parser/index.js +33 -1
- package/dist/generators/gherkin-parser/index.js.map +1 -1
- package/dist/generators/test-generator/adapters/appium/templates/steps/actions/check-action.hbs +29 -8
- package/dist/generators/test-generator/adapters/appium/templates/steps/actions/scroll-action.hbs +21 -17
- package/dist/generators/test-generator/adapters/appium/templates/steps/actions/toggle-action.hbs +3 -0
- package/dist/generators/test-generator/adapters/appium/templates/steps/actions/uncheck-action.hbs +26 -6
- package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/is-hidden-assertion.hbs +3 -1
- package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/visible-assertion.hbs +14 -0
- package/dist/generators/test-generator/adapters/appium/templates/steps/gestures/tap-top-action.hbs +3 -0
- package/dist/generators/test-generator/adapters/appium/templates/steps/partials/locator.hbs +9 -2
- package/dist/generators/test-generator/adapters/appium/templates/test-file.hbs +115 -0
- package/dist/generators/test-generator/adapters/playwright/templates/steps/actions/table-action-in-row.hbs +1 -1
- package/dist/generators/test-generator/patterns/index.d.ts +12 -0
- package/dist/generators/test-generator/patterns/index.d.ts.map +1 -1
- package/dist/generators/test-generator/patterns/index.js +21 -1
- package/dist/generators/test-generator/patterns/index.js.map +1 -1
- package/dist/harness/script-check.d.ts.map +1 -1
- package/dist/harness/script-check.js +10 -5
- package/dist/harness/script-check.js.map +1 -1
- package/dist/orchestrator/ai-skills/writer.d.ts.map +1 -1
- package/dist/orchestrator/ai-skills/writer.js +46 -4
- package/dist/orchestrator/ai-skills/writer.js.map +1 -1
- package/dist/orchestrator/project-initializer.d.ts +14 -0
- package/dist/orchestrator/project-initializer.d.ts.map +1 -1
- package/dist/orchestrator/project-initializer.js +58 -0
- package/dist/orchestrator/project-initializer.js.map +1 -1
- package/dist/orchestrator/templates/ai-src/commands/add-flow.md +52 -5
- package/dist/orchestrator/templates/ai-src/commands/create-test.md +7 -0
- package/dist/orchestrator/templates/ai-src/commands/feedback.md +6 -6
- package/dist/orchestrator/templates/ai-src/commands/locale.md +26 -4
- package/dist/orchestrator/templates/ai-src/skills/sungen-delivery/SKILL.md +5 -4
- package/dist/orchestrator/templates/wdio.conf.ts +39 -3
- package/dist/tools/sungen-api/edit-in-editor.d.ts +15 -0
- package/dist/tools/sungen-api/edit-in-editor.d.ts.map +1 -0
- package/dist/tools/sungen-api/edit-in-editor.js +85 -0
- package/dist/tools/sungen-api/edit-in-editor.js.map +1 -0
- package/dist/tools/sungen-api/feedback-client.d.ts +24 -0
- package/dist/tools/sungen-api/feedback-client.d.ts.map +1 -0
- package/dist/tools/sungen-api/feedback-client.js +53 -0
- package/dist/tools/sungen-api/feedback-client.js.map +1 -0
- package/dist/tools/sungen-api/read-stdin.d.ts +6 -0
- package/dist/tools/sungen-api/read-stdin.d.ts.map +1 -0
- package/dist/tools/sungen-api/read-stdin.js +19 -0
- package/dist/tools/sungen-api/read-stdin.js.map +1 -0
- package/dist/tools/sungen-auth/api-base-url.d.ts +14 -0
- package/dist/tools/sungen-auth/api-base-url.d.ts.map +1 -0
- package/dist/tools/sungen-auth/api-base-url.js +28 -0
- package/dist/tools/sungen-auth/api-base-url.js.map +1 -0
- package/dist/tools/sungen-auth/credentials-store.d.ts +19 -0
- package/dist/tools/sungen-auth/credentials-store.d.ts.map +1 -0
- package/dist/tools/sungen-auth/credentials-store.js +94 -0
- package/dist/tools/sungen-auth/credentials-store.js.map +1 -0
- package/dist/tools/sungen-auth/login-flow.d.ts +33 -0
- package/dist/tools/sungen-auth/login-flow.d.ts.map +1 -0
- package/dist/tools/sungen-auth/login-flow.js +107 -0
- package/dist/tools/sungen-auth/login-flow.js.map +1 -0
- package/dist/tools/sungen-auth/loopback-server.d.ts +20 -0
- package/dist/tools/sungen-auth/loopback-server.d.ts.map +1 -0
- package/dist/tools/sungen-auth/loopback-server.js +132 -0
- package/dist/tools/sungen-auth/loopback-server.js.map +1 -0
- package/dist/tools/sungen-auth/open-browser.d.ts +10 -0
- package/dist/tools/sungen-auth/open-browser.d.ts.map +1 -0
- package/dist/tools/sungen-auth/open-browser.js +43 -0
- package/dist/tools/sungen-auth/open-browser.js.map +1 -0
- package/dist/tools/sungen-auth/pkce.d.ts +13 -0
- package/dist/tools/sungen-auth/pkce.d.ts.map +1 -0
- package/dist/tools/sungen-auth/pkce.js +27 -0
- package/dist/tools/sungen-auth/pkce.js.map +1 -0
- package/dist/tools/sungen-auth/sungen-auth-types.d.ts +41 -0
- package/dist/tools/sungen-auth/sungen-auth-types.d.ts.map +1 -0
- package/dist/tools/sungen-auth/sungen-auth-types.js +7 -0
- package/dist/tools/sungen-auth/sungen-auth-types.js.map +1 -0
- package/package.json +4 -3
- package/src/cli/commands/dashboard.ts +2 -2
- package/src/cli/commands/delivery.ts +24 -6
- package/src/cli/commands/feedback.ts +123 -51
- package/src/cli/commands/login.ts +75 -0
- package/src/cli/commands/qa-feedback.ts +77 -0
- package/src/cli/index.ts +4 -0
- package/src/dashboard/snapshot-builder.ts +146 -46
- package/src/dashboard/templates/index.html +23 -23
- package/src/dashboard/types.ts +10 -2
- package/src/exporters/api-testcase-formatter.ts +325 -0
- package/src/exporters/csv-exporter.ts +46 -7
- package/src/exporters/feature-parser.ts +7 -2
- package/src/exporters/json-exporter.ts +11 -3
- package/src/exporters/playwright-report-parser.ts +50 -11
- package/src/exporters/scenario-merger.ts +49 -3
- package/src/exporters/spec-parser.ts +1 -1
- package/src/exporters/test-data-resolver.ts +13 -0
- package/src/exporters/types.ts +13 -0
- package/src/exporters/xlsx-exporter.ts +155 -49
- package/src/generators/gherkin-parser/index.ts +44 -1
- package/src/generators/test-generator/adapters/appium/templates/steps/actions/check-action.hbs +29 -8
- package/src/generators/test-generator/adapters/appium/templates/steps/actions/scroll-action.hbs +21 -17
- package/src/generators/test-generator/adapters/appium/templates/steps/actions/toggle-action.hbs +3 -0
- package/src/generators/test-generator/adapters/appium/templates/steps/actions/uncheck-action.hbs +26 -6
- package/src/generators/test-generator/adapters/appium/templates/steps/assertions/is-hidden-assertion.hbs +3 -1
- package/src/generators/test-generator/adapters/appium/templates/steps/assertions/visible-assertion.hbs +14 -0
- package/src/generators/test-generator/adapters/appium/templates/steps/gestures/tap-top-action.hbs +3 -0
- package/src/generators/test-generator/adapters/appium/templates/steps/partials/locator.hbs +9 -2
- package/src/generators/test-generator/adapters/appium/templates/test-file.hbs +115 -0
- package/src/generators/test-generator/adapters/playwright/templates/steps/actions/table-action-in-row.hbs +1 -1
- package/src/generators/test-generator/patterns/index.ts +21 -1
- package/src/harness/script-check.ts +10 -5
- package/src/orchestrator/ai-skills/writer.ts +39 -4
- package/src/orchestrator/project-initializer.ts +59 -0
- package/src/orchestrator/templates/ai-src/commands/add-flow.md +52 -5
- package/src/orchestrator/templates/ai-src/commands/create-test.md +7 -0
- package/src/orchestrator/templates/ai-src/commands/feedback.md +6 -6
- package/src/orchestrator/templates/ai-src/commands/locale.md +26 -4
- package/src/orchestrator/templates/ai-src/skills/sungen-delivery/SKILL.md +5 -4
- package/src/orchestrator/templates/wdio.conf.ts +39 -3
- package/src/tools/sungen-api/edit-in-editor.ts +48 -0
- package/src/tools/sungen-api/feedback-client.ts +62 -0
- package/src/tools/sungen-api/read-stdin.ts +15 -0
- package/src/tools/sungen-auth/api-base-url.ts +23 -0
- package/src/tools/sungen-auth/credentials-store.ts +57 -0
- package/src/tools/sungen-auth/login-flow.ts +135 -0
- package/src/tools/sungen-auth/loopback-server.ts +113 -0
- package/src/tools/sungen-auth/open-browser.ts +42 -0
- package/src/tools/sungen-auth/pkce.ts +27 -0
- 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]
|
|
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]
|
|
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
|
*/
|
package/src/exporters/types.ts
CHANGED
|
@@ -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
|
-
*
|
|
107
|
-
* (
|
|
108
|
-
*
|
|
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
|
|
123
|
+
function renderReportHeaderBand(
|
|
111
124
|
wb: ExcelJS.Workbook,
|
|
112
|
-
|
|
113
|
-
|
|
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
|
-
//
|
|
127
|
-
|
|
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
|
-
|
|
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
|
|
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
|
*/
|
package/src/generators/test-generator/adapters/appium/templates/steps/actions/check-action.hbs
CHANGED
|
@@ -1,10 +1,31 @@
|
|
|
1
|
-
{{!-- WebdriverIO/Appium has no .check().
|
|
2
|
-
on, so the step is idempotent (re-running a passing test
|
|
3
|
-
|
|
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.
|
|
8
|
-
catch { try { __on =
|
|
9
|
-
if (
|
|
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
|
+
})();
|
package/src/generators/test-generator/adapters/appium/templates/steps/actions/scroll-action.hbs
CHANGED
|
@@ -1,22 +1,26 @@
|
|
|
1
1
|
if (driver.isIOS) {
|
|
2
|
-
// iOS (XCUITest) has no UiScrollable.
|
|
3
|
-
// below-fold element
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
|
|
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
|
-
//
|
|
15
|
-
// overshoot (which would drop a virtualized Flutter item from the a11y tree).
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
//
|
|
19
|
-
|
|
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
|
}
|
package/src/generators/test-generator/adapters/appium/templates/steps/actions/toggle-action.hbs
CHANGED
|
@@ -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
|
package/src/generators/test-generator/adapters/appium/templates/steps/actions/uncheck-action.hbs
CHANGED
|
@@ -1,8 +1,28 @@
|
|
|
1
|
-
{{!-- WebdriverIO/Appium has no .uncheck().
|
|
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.
|
|
6
|
-
catch { try { __on =
|
|
7
|
-
if (__on)
|
|
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
|
-
|
|
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}}
|
package/src/generators/test-generator/adapters/appium/templates/steps/gestures/tap-top-action.hbs
CHANGED
|
@@ -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).
|