@sun-asterisk/sungen 3.2.31 → 3.2.32

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 (177) hide show
  1. package/dist/cli/commands/audit.d.ts.map +1 -1
  2. package/dist/cli/commands/audit.js +3 -1
  3. package/dist/cli/commands/audit.js.map +1 -1
  4. package/dist/cli/commands/capability.d.ts.map +1 -1
  5. package/dist/cli/commands/capability.js +11 -0
  6. package/dist/cli/commands/capability.js.map +1 -1
  7. package/dist/cli/commands/init.d.ts.map +1 -1
  8. package/dist/cli/commands/init.js +2 -0
  9. package/dist/cli/commands/init.js.map +1 -1
  10. package/dist/cli/commands/setup-mobile-env.d.ts +15 -0
  11. package/dist/cli/commands/setup-mobile-env.d.ts.map +1 -0
  12. package/dist/cli/commands/setup-mobile-env.js +65 -0
  13. package/dist/cli/commands/setup-mobile-env.js.map +1 -0
  14. package/dist/cli/index.js +2 -0
  15. package/dist/cli/index.js.map +1 -1
  16. package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/all-contain-assertion.hbs +1 -2
  17. package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/all-contain-element.hbs +8 -5
  18. package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/count-assertion.hbs +5 -1
  19. package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/disabled-with-filter-assertion.hbs +1 -1
  20. package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/disabled-with-role-variable-assertion.hbs +1 -1
  21. package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/hidden-with-filter-assertion.hbs +1 -1
  22. package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/hidden-with-role-variable-assertion.hbs +1 -1
  23. package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/list-item-count-assertion.hbs +5 -2
  24. package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/state-with-filter-assertion.hbs +1 -1
  25. package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/visible-filtered-assertion.hbs +1 -1
  26. package/dist/generators/test-generator/adapters/appium/templates/steps/partials/await-elements.hbs +12 -0
  27. package/dist/generators/test-generator/adapters/appium/templates/test-file.hbs +26 -0
  28. package/dist/generators/test-generator/adapters/playwright/templates/steps/actions/capture-variable.hbs +7 -1
  29. package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/row-scoped-column-assertion.hbs +23 -1
  30. package/dist/generators/test-generator/adapters/playwright/templates/steps/partials/locator-base.hbs +4 -4
  31. package/dist/generators/test-generator/adapters/playwright/templates/steps/partials/locator-strategies/default.hbs +1 -1
  32. package/dist/generators/test-generator/adapters/playwright/templates/steps/partials/locator-strategies/id.hbs +1 -1
  33. package/dist/generators/test-generator/adapters/playwright/templates/steps/partials/locator.hbs +4 -4
  34. package/dist/generators/test-generator/adapters/playwright/templates/steps/partials/url-assertion.hbs +13 -2
  35. package/dist/generators/test-generator/adapters/playwright/templates/test-file.hbs +14 -0
  36. package/dist/generators/test-generator/code-generator.d.ts.map +1 -1
  37. package/dist/generators/test-generator/code-generator.js +56 -13
  38. package/dist/generators/test-generator/code-generator.js.map +1 -1
  39. package/dist/generators/test-generator/diagnostics.d.ts +39 -2
  40. package/dist/generators/test-generator/diagnostics.d.ts.map +1 -1
  41. package/dist/generators/test-generator/diagnostics.js +38 -1
  42. package/dist/generators/test-generator/diagnostics.js.map +1 -1
  43. package/dist/generators/test-generator/patterns/index.d.ts +1 -1
  44. package/dist/generators/test-generator/patterns/index.js +1 -1
  45. package/dist/generators/test-generator/step-mapper.d.ts +35 -0
  46. package/dist/generators/test-generator/step-mapper.d.ts.map +1 -1
  47. package/dist/generators/test-generator/step-mapper.js +103 -4
  48. package/dist/generators/test-generator/step-mapper.js.map +1 -1
  49. package/dist/generators/test-generator/template-engine.d.ts.map +1 -1
  50. package/dist/generators/test-generator/template-engine.js +18 -0
  51. package/dist/generators/test-generator/template-engine.js.map +1 -1
  52. package/dist/generators/test-generator/utils/selector-resolver.d.ts +47 -0
  53. package/dist/generators/test-generator/utils/selector-resolver.d.ts.map +1 -1
  54. package/dist/generators/test-generator/utils/selector-resolver.js +17 -3
  55. package/dist/generators/test-generator/utils/selector-resolver.js.map +1 -1
  56. package/dist/harness/audit.d.ts.map +1 -1
  57. package/dist/harness/audit.js +23 -4
  58. package/dist/harness/audit.js.map +1 -1
  59. package/dist/harness/next-step.d.ts.map +1 -1
  60. package/dist/harness/next-step.js +2 -1
  61. package/dist/harness/next-step.js.map +1 -1
  62. package/dist/harness/parse.d.ts.map +1 -1
  63. package/dist/harness/parse.js +42 -7
  64. package/dist/harness/parse.js.map +1 -1
  65. package/dist/harness/spec-branches.d.ts +7 -2
  66. package/dist/harness/spec-branches.d.ts.map +1 -1
  67. package/dist/harness/spec-branches.js +62 -11
  68. package/dist/harness/spec-branches.js.map +1 -1
  69. package/dist/harness/spec-coverage.d.ts +16 -0
  70. package/dist/harness/spec-coverage.d.ts.map +1 -1
  71. package/dist/harness/spec-coverage.js +119 -10
  72. package/dist/harness/spec-coverage.js.map +1 -1
  73. package/dist/orchestrator/appium-environment-setup.d.ts +24 -0
  74. package/dist/orchestrator/appium-environment-setup.d.ts.map +1 -0
  75. package/dist/orchestrator/appium-environment-setup.js +157 -0
  76. package/dist/orchestrator/appium-environment-setup.js.map +1 -0
  77. package/dist/orchestrator/mobile-runtime-scaffolder.d.ts +2 -0
  78. package/dist/orchestrator/mobile-runtime-scaffolder.d.ts.map +1 -1
  79. package/dist/orchestrator/mobile-runtime-scaffolder.js.map +1 -1
  80. package/dist/orchestrator/project-initializer.d.ts +1 -0
  81. package/dist/orchestrator/project-initializer.d.ts.map +1 -1
  82. package/dist/orchestrator/project-initializer.js +14 -0
  83. package/dist/orchestrator/project-initializer.js.map +1 -1
  84. package/dist/orchestrator/templates/ai-src/skills/sungen-error-mapping/SKILL.md +5 -1
  85. package/dist/orchestrator/templates/ai-src/skills/sungen-gherkin-syntax/SKILL.md +55 -2
  86. package/dist/orchestrator/templates/ai-src/skills/sungen-tc-generation/SKILL.md +20 -5
  87. package/dist/orchestrator/templates/ai-src/skills/sungen-tc-review/SKILL.md +3 -3
  88. package/dist/orchestrator/templates/appium-environment/README.md +83 -0
  89. package/dist/orchestrator/templates/appium-environment/scripts/common/install-appium.ps1 +44 -0
  90. package/dist/orchestrator/templates/appium-environment/scripts/common/install-appium.sh +50 -0
  91. package/dist/orchestrator/templates/appium-environment/scripts/common/lib.ps1 +108 -0
  92. package/dist/orchestrator/templates/appium-environment/scripts/common/lib.sh +141 -0
  93. package/dist/orchestrator/templates/appium-environment/scripts/common/verify-appium.ps1 +39 -0
  94. package/dist/orchestrator/templates/appium-environment/scripts/common/verify-appium.sh +43 -0
  95. package/dist/orchestrator/templates/appium-environment/scripts/common/verify-node.ps1 +13 -0
  96. package/dist/orchestrator/templates/appium-environment/scripts/common/verify-node.sh +16 -0
  97. package/dist/orchestrator/templates/appium-environment/scripts/macos/install-android.sh +110 -0
  98. package/dist/orchestrator/templates/appium-environment/scripts/macos/install-ios.sh +32 -0
  99. package/dist/orchestrator/templates/appium-environment/scripts/macos/verify-android.sh +32 -0
  100. package/dist/orchestrator/templates/appium-environment/scripts/macos/verify-ios.sh +15 -0
  101. package/dist/orchestrator/templates/appium-environment/scripts/windows/install-android.ps1 +153 -0
  102. package/dist/orchestrator/templates/appium-environment/scripts/windows/verify-android.ps1 +26 -0
  103. package/dist/orchestrator/templates/appium-environment/setup.ps1 +58 -0
  104. package/dist/orchestrator/templates/appium-environment/setup.sh +88 -0
  105. package/dist/orchestrator/templates/specs-checked-state.d.ts +1 -1
  106. package/dist/orchestrator/templates/specs-checked-state.d.ts.map +1 -1
  107. package/dist/orchestrator/templates/specs-checked-state.js +26 -3
  108. package/dist/orchestrator/templates/specs-checked-state.js.map +1 -1
  109. package/dist/orchestrator/templates/specs-checked-state.ts +31 -6
  110. package/dist/orchestrator/templates/specs-mock.d.ts +2 -1
  111. package/dist/orchestrator/templates/specs-mock.d.ts.map +1 -1
  112. package/dist/orchestrator/templates/specs-mock.js +10 -3
  113. package/dist/orchestrator/templates/specs-mock.js.map +1 -1
  114. package/dist/orchestrator/templates/specs-mock.ts +12 -4
  115. package/package.json +3 -3
  116. package/src/cli/commands/audit.ts +2 -1
  117. package/src/cli/commands/capability.ts +12 -1
  118. package/src/cli/commands/init.ts +3 -0
  119. package/src/cli/commands/setup-mobile-env.ts +64 -0
  120. package/src/cli/index.ts +2 -0
  121. package/src/generators/test-generator/adapters/appium/templates/steps/assertions/all-contain-assertion.hbs +1 -2
  122. package/src/generators/test-generator/adapters/appium/templates/steps/assertions/all-contain-element.hbs +8 -5
  123. package/src/generators/test-generator/adapters/appium/templates/steps/assertions/count-assertion.hbs +5 -1
  124. package/src/generators/test-generator/adapters/appium/templates/steps/assertions/disabled-with-filter-assertion.hbs +1 -1
  125. package/src/generators/test-generator/adapters/appium/templates/steps/assertions/disabled-with-role-variable-assertion.hbs +1 -1
  126. package/src/generators/test-generator/adapters/appium/templates/steps/assertions/hidden-with-filter-assertion.hbs +1 -1
  127. package/src/generators/test-generator/adapters/appium/templates/steps/assertions/hidden-with-role-variable-assertion.hbs +1 -1
  128. package/src/generators/test-generator/adapters/appium/templates/steps/assertions/list-item-count-assertion.hbs +5 -2
  129. package/src/generators/test-generator/adapters/appium/templates/steps/assertions/state-with-filter-assertion.hbs +1 -1
  130. package/src/generators/test-generator/adapters/appium/templates/steps/assertions/visible-filtered-assertion.hbs +1 -1
  131. package/src/generators/test-generator/adapters/appium/templates/steps/partials/await-elements.hbs +12 -0
  132. package/src/generators/test-generator/adapters/appium/templates/test-file.hbs +26 -0
  133. package/src/generators/test-generator/adapters/playwright/templates/steps/actions/capture-variable.hbs +7 -1
  134. package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/row-scoped-column-assertion.hbs +23 -1
  135. package/src/generators/test-generator/adapters/playwright/templates/steps/partials/locator-base.hbs +4 -4
  136. package/src/generators/test-generator/adapters/playwright/templates/steps/partials/locator-strategies/default.hbs +1 -1
  137. package/src/generators/test-generator/adapters/playwright/templates/steps/partials/locator-strategies/id.hbs +1 -1
  138. package/src/generators/test-generator/adapters/playwright/templates/steps/partials/locator.hbs +4 -4
  139. package/src/generators/test-generator/adapters/playwright/templates/steps/partials/url-assertion.hbs +13 -2
  140. package/src/generators/test-generator/adapters/playwright/templates/test-file.hbs +14 -0
  141. package/src/generators/test-generator/code-generator.ts +66 -17
  142. package/src/generators/test-generator/diagnostics.ts +39 -2
  143. package/src/generators/test-generator/patterns/index.ts +1 -1
  144. package/src/generators/test-generator/step-mapper.ts +106 -5
  145. package/src/generators/test-generator/template-engine.ts +18 -0
  146. package/src/generators/test-generator/utils/selector-resolver.ts +18 -3
  147. package/src/harness/audit.ts +24 -5
  148. package/src/harness/next-step.ts +2 -1
  149. package/src/harness/parse.ts +42 -7
  150. package/src/harness/spec-branches.ts +67 -12
  151. package/src/harness/spec-coverage.ts +121 -11
  152. package/src/orchestrator/appium-environment-setup.ts +127 -0
  153. package/src/orchestrator/mobile-runtime-scaffolder.ts +2 -0
  154. package/src/orchestrator/project-initializer.ts +15 -1
  155. package/src/orchestrator/templates/ai-src/skills/sungen-error-mapping/SKILL.md +5 -1
  156. package/src/orchestrator/templates/ai-src/skills/sungen-gherkin-syntax/SKILL.md +55 -2
  157. package/src/orchestrator/templates/ai-src/skills/sungen-tc-generation/SKILL.md +20 -5
  158. package/src/orchestrator/templates/ai-src/skills/sungen-tc-review/SKILL.md +3 -3
  159. package/src/orchestrator/templates/appium-environment/README.md +83 -0
  160. package/src/orchestrator/templates/appium-environment/scripts/common/install-appium.ps1 +44 -0
  161. package/src/orchestrator/templates/appium-environment/scripts/common/install-appium.sh +50 -0
  162. package/src/orchestrator/templates/appium-environment/scripts/common/lib.ps1 +108 -0
  163. package/src/orchestrator/templates/appium-environment/scripts/common/lib.sh +141 -0
  164. package/src/orchestrator/templates/appium-environment/scripts/common/verify-appium.ps1 +39 -0
  165. package/src/orchestrator/templates/appium-environment/scripts/common/verify-appium.sh +43 -0
  166. package/src/orchestrator/templates/appium-environment/scripts/common/verify-node.ps1 +13 -0
  167. package/src/orchestrator/templates/appium-environment/scripts/common/verify-node.sh +16 -0
  168. package/src/orchestrator/templates/appium-environment/scripts/macos/install-android.sh +110 -0
  169. package/src/orchestrator/templates/appium-environment/scripts/macos/install-ios.sh +32 -0
  170. package/src/orchestrator/templates/appium-environment/scripts/macos/verify-android.sh +32 -0
  171. package/src/orchestrator/templates/appium-environment/scripts/macos/verify-ios.sh +15 -0
  172. package/src/orchestrator/templates/appium-environment/scripts/windows/install-android.ps1 +153 -0
  173. package/src/orchestrator/templates/appium-environment/scripts/windows/verify-android.ps1 +26 -0
  174. package/src/orchestrator/templates/appium-environment/setup.ps1 +58 -0
  175. package/src/orchestrator/templates/appium-environment/setup.sh +88 -0
  176. package/src/orchestrator/templates/specs-checked-state.ts +31 -6
  177. package/src/orchestrator/templates/specs-mock.ts +12 -4
@@ -154,6 +154,7 @@ two content-filtered queries can hit different rows if the table re-renders in b
154
154
  # dropdown | select | combobox → tag probed at runtime (toHaveValue native, toHaveText custom trigger)
155
155
  # 5. State: User see [T] button is disabled | checkbox is checked | dialog with {{v}} is hidden
156
156
  # 6. Attribute (toHaveAttribute): User see [T] image | link with {{v}}
157
+ # WHICH attribute comes from the entry's `attribute:` — any name, not just href/src
157
158
  # 7. Count: User see [T] row with {{count}}
158
159
  # 8. Page: User see [T] page | page with {{url}} ← `with` also asserts the query string
159
160
  # NEVER a state: `page is hidden` / `page is empty` … are NOT step forms (see Pattern 8 below)
@@ -291,7 +292,22 @@ job table:
291
292
  value: 'div.job-list'
292
293
  rows: 'div.job-row' # what EVERY row matches — NOT a row label's entry
293
294
  ```
294
- Two different keys, two meanings: `rows:` on the table = every row (the shape); a `type: locator` entry on a **row label** = ONE positioned row (`tbody tr:first-child`). A shape put on a row label fails at run time with a `toHaveCount(1)` message that points back here. A CSS-located table with no `rows:` gets `SG-W016`. On a shaped table `table row {{n}}` counts from the first data row (no header offset).
295
+ Two different keys, two meanings: `rows:` on the table = every row (the shape); a `type: locator` entry on a **row label** = ONE positioned row (`tbody tr:first-child`). A shape put on a row label fails at run time with a `toHaveCount(1)` message that points back here. A table located by CSS — or by a role that holds no rows, such as `role: list` or `role: region` — with no `rows:` gets `SG-W016`. On a shaped table `table row {{n}}` counts from the first data row (no header offset).
296
+
297
+ **A shaped row has no CELLS either, so the two cell-reading steps need one more declaration.** `see [Col] column with {{v}}` reaches the cell through `getByRole('cell')`, and `table match data:` reads its header row from `thead tr` and addresses cells as `td, th` — a `<div>` row has none of those. Name the cell on the column:
298
+ ```yaml
299
+ job table:
300
+ type: locator
301
+ value: 'div.job-list'
302
+ rows: 'div.job-row'
303
+ columns:
304
+ title:
305
+ index: 0
306
+ header: 'Title'
307
+ elements:
308
+ cell: { type: locator, value: '.job-title' } # relative to ONE row
309
+ ```
310
+ With it, `see [Title] column with {{v}}` compiles to `tableRow.locator('.job-title')` and `table match data:` stops reading `thead` altogether — the header-to-column mapping is then known at compile time, so there is nothing left to race with. Without it on a shaped table, `see [Col] column` warns (`SG-W034`) and `table match data:` is refused outright (`SG-E035`) rather than waiting out every timeout against a header row that will never exist.
295
311
 
296
312
  ### Browser storage `[web]`
297
313
 
@@ -556,6 +572,7 @@ award:
556
572
  | `page` | relative URL | — | navigation |
557
573
  | `upload` | — | — | file inputs |
558
574
  | `frame` | iframe selector | — | iframes |
575
+ | `id` | `#the-id` (a bare `the-id` also works) | — | an element whose only stable handle is its DOM id |
559
576
 
560
577
  Options: `nth` `exact` `scope` `match` `variant` `frame` `contenteditable` `columns` `checked` `selected`
561
578
 
@@ -569,6 +586,39 @@ when the state differs, then assert it; `is checked` / `is unchecked` / `is sele
569
586
  the same entry (`is focused`, …) keep their native matchers. The value must be ONE plain class token or
570
587
  attribute name — whitespace, a quote or a backslash is refused at compile time, naming the entry.
571
588
 
589
+ `attribute:` names the attribute an `image`/`link` assertion reads, and it takes ANY attribute name
590
+ straight from the YAML — nothing is hardcoded to `href`/`src`. To assert several attributes of ONE
591
+ element, declare several keys with the SAME locator and different `attribute:`; each key is then a
592
+ separate reference in the Gherkin:
593
+ ```yaml
594
+ job link: # the link's TEXT
595
+ type: locator
596
+ value: 'a.job-link'
597
+ job link href: # the same element, its href
598
+ type: locator
599
+ value: 'a.job-link'
600
+ attribute: href
601
+ job link target:
602
+ type: locator
603
+ value: 'a.job-link'
604
+ attribute: target
605
+ ```
606
+
607
+ **A `page` value that is an ABSOLUTE URL now anchors the HOST as well as the path.** `value:
608
+ 'https://admin.example.com/jobs'` means "that path on that host": `see [X] page`, `is on [X] page`
609
+ and `should remain on` all fail on `https://admin-stg.example.com/jobs`. Before, only `wait for [X]
610
+ page` checked the host and the assertions matched the path on any host at all — so an assertion
611
+ naming one environment silently passed in another. If the page is the same in every environment,
612
+ declare it **relative** (`value: '/jobs'`) and let `baseURL` supply the host; keep the absolute form
613
+ for a destination that really is on another host.
614
+
615
+ `anchor:` is a `page` entry's rendered-content proof: `Then User see [X] page` asserts the URL AND
616
+ that the anchor element is present, so a route guard that leaves the URL alone cannot pass it. Three
617
+ things it does NOT do, each verified rather than assumed: it is read only by the `Then`-side page
618
+ assertion, so `Given User is on [X] page` ignores it; the `should see route` / `should remain on`
619
+ forms ignore it too — they compile through the route assertion, which is never handed an anchor; and
620
+ nothing warns when a page entry declares one and no step asserts that page.
621
+
572
622
  `scope` (e.g. `scope: dialog`) is `[web]`-effective only — no Appium template reads `inDialog`, so
573
623
  on mobile a dialog-scoped ref still resolves against the whole screen (`SG-W021`).
574
624
 
@@ -724,7 +774,9 @@ npx playwright test --grep "@smoke|@high" # smoke OR high
724
774
  | Serial (default) | `test.describe.serial()` | Shared | `beforeAll` (1 goto) | Skip remaining |
725
775
  | `@parallel` | `test.describe()` | Fresh per test | `beforeEach` (N goto) | Continue |
726
776
 
727
- **`@parallel` is required** when a feature has multiple auth groups (e.g., `@auth:user` + `@no-auth` scenarios). Serial mode uses one shared browser context and cannot mix auth roles. The compiler will error if `@parallel` is missing in this case.
777
+ **Multiple auth groups need the default isolation.** A feature mixing `@auth:user` with `@no-auth` compiles as-is — features are parallel by default, and `@parallel` only names that default. What breaks it is `@serial`: one shared browser context cannot carry two different sessions, so the compiler errors and asks you to drop `@serial` or split the feature.
778
+
779
+ `@no-auth` is a demand for an **empty** session, not merely the absence of `@auth:`. Its scenarios compile into their own `test.describe('no-auth')` with an inline empty `storageState`, so they stay signed out even when `playwright.config.ts` sets a global one. An untagged scenario is different again: it inherits whatever that config says.
728
780
 
729
781
  ### `@flow` tag (E2E cross-screen testing)
730
782
 
@@ -806,6 +858,7 @@ dashboard:
806
858
  | Assertion clause the pattern drops | `Then User see [Code] text matches "<format>"` compiling to a bare `toBeVisible()` | Use the exact supported phrasing for the clause (see the `matches` / `contains` / `has text` forms above). A clause that gets swallowed leaves an assertion that passes whatever the element says, so the step is rejected with `SG-E023` instead. If the step still compiles bare, you are on an older global sungen — run `npx sungen` |
807
859
  | Mobile lifecycle step with no app id | `When User restart the app` in a feature whose `Path:` carries only a route | Declare the app id on the Feature: `Path: com.example.app/.MainActivity \| com.example.app`. The lifecycle steps resolve the app from `Path`; without it the whole spec would die at runtime, so the step is rejected with `SG-E024`. For a mobile unit `--path` carries the APP ID, `--reach` carries the navigation recipe |
808
860
  | Missing target type | `fill [email] with {{v}}` | `fill [email] field with {{v}}` |
861
+ | Asserting a cell with the row filter | `Then User see [Status] text in [Job Table] table with {{v}}` | The single `with` is read as the EXPECTED VALUE, not as a row filter, so the assertion runs against the WHOLE table and can pass on a row the sentence never named. Use the two-`with` form — `Then User see [Status] text with <expected> in [Job Row] row with {{v}}` — pointing the second `[…]` at a ROW entry, not at the table entry (a filter applied to the table narrows nothing). Warned with `SG-W032`. A `fill` is NOT flagged: a write needs a value, so its `with {{v}}` is that value and the step is an ordinary container-scoped write |
809
862
  | Background with scope | `Background: ... And User is on [X] dialog` | Use `@steps` + `@extend` for scope-dependent flows |
810
863
  | `is on` after When | `When ... And User is on [X] dialog` | `And User see [X] dialog` or separate Given |
811
864
  | Literal URL navigate | `User navigate to "/dashboard"` | `User is on [Dashboard] page` (add page selector in `selectors.yaml`) |
@@ -56,7 +56,7 @@ Scenario: VP-VAL-002 An email in an invalid format shows the format error
56
56
  2. **ENV-BOUND** (must exist in the test environment; harmless if leaked): seeded ids, registered emails, valid tokens → keep in yaml with a `# ENV-BOUND: <what to seed>` marker; per-environment values go in `<unit>.<env>.yaml`.
57
57
  3. **SECRET** (real login / harmful if leaked): NEVER write the real value — reference `${QA_<NAME>}` (uppercase, `QA_` prefix mandatory; only that shape resolves from `.env.qa` at runtime, any other `${...}` stays literal). Whenever you emit a new `${QA_*}` ref, ALSO append the name to `.env.qa.example`. Note: "password" is not automatically SECRET — a made-up password probing validation is INVENTED; only a real account's password is SECRET (and `@auth:role` via `sungen makeauth` usually removes the need entirely).
58
58
 
59
- - `@parallel` required when mixing `@auth:X` + `@no-auth` in the same feature
59
+ - Mixing `@auth:X` + `@no-auth` in one feature is fine by default; just never tag that feature `@serial`
60
60
  → Playwright shares browser context per worker; auth state leaks across scenarios without isolation.
61
61
 
62
62
  - XSS ≠ SQL injection — generate as **2 separate scenarios**, never merge
@@ -568,7 +568,7 @@ and a real oracle, not a shrug: write the steps a human follows and the value th
568
568
 
569
569
  Add cleanup tags per the `sungen-gherkin-syntax` Cleanup table. Key rules:
570
570
  - **Always `@cleanup:overlay`** if ANY section opens a dialog; **always `@cleanup:forms`** if the screen has inline search, filter, or editable forms.
571
- - **`@parallel`** is required when mixing auth groups (`@auth:X` + `@no-auth`); recommended for validation-heavy features needing a clean form state per scenario.
571
+ - **`@parallel`** names the default isolation, so mixing auth groups (`@auth:X` + `@no-auth`) needs no tag at all — only `@serial` breaks it, because one shared context cannot hold two sessions. Still worth stating explicitly on validation-heavy features that want a clean form state per scenario.
572
572
  - **`@afterEach`** hook only when `@cleanup:*` tags aren't enough (feature-specific reset logic).
573
573
 
574
574
  ---
@@ -838,9 +838,24 @@ is the common failure. Every navigation target the spec names needs a click-thro
838
838
  (`NAV-TARGET-ABSENT`), even when this flow does not consume the destination — a context param
839
839
  handed to another screen is that screen's input, and the handoff is where it goes wrong.
840
840
 
841
- **A deny case needs its allow case.** "permission denied → content hidden" passes just as happily
842
- when the content is hidden for everyone. Pair it with "permission granted → content shown", the
843
- way a boundary needs `min-1 / min / max / max+1` (`PERMISSION-PAIR-MISSING`).
841
+ **A hidden case needs its shown case.** "permission denied → content hidden" passes just as
842
+ happily when the content is hidden for everyone. Pair it with "permission granted → content
843
+ shown", the way a boundary needs `min-1 / min / max / max+1` (`PERMISSION-PAIR-MISSING`).
844
+
845
+ The same holds for ANY condition, not just permission: an element you assert `is hidden` in one
846
+ state must be asserted PRESENT in some other state, or nothing distinguishes a working rule from a
847
+ feature that is broken for everyone — and a mistyped selector matches nothing, so `is hidden`
848
+ passes forever on its own (`VISIBILITY-PAIR-MISSING`). Conditional buttons, empty-vs-populated
849
+ lists, the active tab, a radio state: "show A or B, never both" is a very common implementation
850
+ defect, and only the pair catches it.
851
+
852
+ **A requirement that states both directions is TWO requirements.** "`X` MUST be shown when no
853
+ score is set; MUST NOT be shown once a score exists" is one line and two mutually-exclusive states,
854
+ so one scenario cannot prove it — tagging `@spec:FR-013` on the positive case closes the ID, not
855
+ the rule. Write one case per direction; `sungen audit` counts them as two coverage units and
856
+ reports the unproved half as `SPEC-CLAUSE-UNCOVERED`. The gate only presses a requirement that
857
+ NAMES its element and speaks about visibility or enabledness — but the habit is worth keeping for
858
+ every opposing rule, gated or not.
844
859
 
845
860
  **State that ARRIVES with the data is a different case from state the test creates.** A
846
861
  discriminator field you can only observe at load (an item already favourited before the page
@@ -105,7 +105,7 @@ Hard-fail gate. Any ✗ is a syntax error — must be fixed; do not score furthe
105
105
 
106
106
  - [ ] Exactly **one** priority tag per scenario (`@high`/`@normal`/`@low`…)
107
107
  - [ ] `@manual`/`@auth:role`/`@no-auth`/`@cleanup:*`/`@parallel`/`@flow` used in the right context
108
- - [ ] `@parallel` is REQUIRED when a feature mixes auth groups (`@auth:user` + `@no-auth`)
108
+ - [ ] A feature mixing auth groups (`@auth:user` + `@no-auth`) is NOT `@serial` — parallel is the default and one shared context cannot hold two sessions
109
109
  - [ ] `@flow`: `[Screen:Element]` namespace is consistent; YAML keys quoted with the colon (`"login:submit":`)
110
110
 
111
111
  ### A8. Mobile (`@platform:mobile/android/ios` units only)
@@ -321,7 +321,7 @@ Do NOT deduct points when a `.feature` lacks the following viewpoints (Playwrigh
321
321
  12. **Orphan key in test-data** (the advisory lint after `sungen generate` says `defined but never referenced`; a `@query`/`@api` param bound by name, a `@manual` comment ref or a `_*` metadata key is not one) → delete it or bind it into a scenario. **Unbound `{{x}}` in a step** (the same lint says `is not a test-data key and nothing binds it`) → add the key or fix the ref — left alone it runs as the literal text `${x}`.
322
322
  13. **Unnecessary CSS/locator selector** → switch to role+name/label/text; keys use spaces.
323
323
  14. **Scope crammed into `Background`** → split into `@steps`/`@extend`.
324
- 15. **Mixed auth groups missing `@parallel`** → add `@parallel`.
324
+ 15. **Mixed auth groups on a `@serial` feature** → remove `@serial`, or split one feature per auth role.
325
325
  16. **Missing secondary behaviors** — spec defines tiebreaker, fallback rule, or secondary sort but no scenario tests it? Add 1 `@high` TC per rule.
326
326
  17. **Missing concurrency scenarios** — spec or test-viewpoint mentions multi-tab, multi-user, or simultaneous actions but no `@manual` scenario exists? Add 1 `@manual` TC per risk (`@normal` by default; `@high` if data integrity at risk).
327
327
  18. **Mobile: `see [X] page` oracle** → replace with a marker element on the destination screen + the origin's marker `is hidden` (SG-W022 — the original passes having proven nothing).
@@ -363,7 +363,7 @@ If `selectors/<name>.yaml` exists, count lines matching `@needs-live-verify` (pr
363
363
  - [ ] Assertions follow the 8 patterns, no redundant `is visible`
364
364
  - [ ] Every {{var}} exists in test-data.yaml and no orphan keys — both reported by the advisory data-driven lint at `sungen generate` (not `audit`); an unbound {{var}} compiles to a literal placeholder, never a compile error
365
365
  - [ ] Selector keys follow sungen-selector-keys; locator only as last resort
366
- - [ ] Tags valid; @parallel when mixing auth groups
366
+ - [ ] Tags valid; no `@serial` on a feature that mixes auth groups
367
367
  - Syntax errors found: <n>
368
368
 
369
369
  ### Coverage Matrix (Layer B)
@@ -0,0 +1,83 @@
1
+ # appium-environment/ — one command to a working Appium machine
2
+
3
+ Emitted by `sungen init --mobile` (or `sungen capability add mobile`), which also runs it once.
4
+ It provisions everything **below** the Appium layer — the part QA used to do by hand — and ends with
5
+ `appium driver doctor`. Every step is **check → install → verify**: a satisfied step is skipped, so the
6
+ bundle is safe to re-run any time (a new machine, a teammate's clone, after an OS update).
7
+
8
+ ```bash
9
+ sungen setup-mobile-env # any OS — emits this folder if missing, then runs it
10
+ ./appium-environment/setup.sh # macOS, directly
11
+ powershell -NoProfile -ExecutionPolicy Bypass -File .\appium-environment\setup.ps1 # Windows, directly
12
+ ```
13
+
14
+ | Flag (macOS) | Flag (Windows) | Effect |
15
+ |---|---|---|
16
+ | `--check` | `-Check` | report what is missing, install nothing (side-effect free) |
17
+ | `--skip-android` | `-SkipAndroid` | skip JDK / Android SDK / uiautomator2 |
18
+ | `--skip-ios` | — | skip Xcode CLT / applesimutils / xcuitest (iOS needs a Mac) |
19
+ | `--no-profile` | `-NoProfile` | do not write env vars to your shell profile / User environment |
20
+
21
+ Exit code: `0` ready (or only warnings) · `1` something failed · `2` unsupported OS / bad flag.
22
+ `sungen init` treats a non-zero exit as a warning — the project scaffold is already complete.
23
+
24
+ ## What it installs (and what it only checks)
25
+
26
+ | Step | macOS (`setup.sh`) | Windows (`setup.ps1`) |
27
+ |---|---|---|
28
+ | Node.js ≥ 18 | check only (sungen needs it already) | check only |
29
+ | Package manager | Homebrew — installed if missing | winget — check only (App Installer, Microsoft Store) |
30
+ | JDK ≥ 17 | `brew install openjdk@21` (an existing JDK ≥ 17 is kept) | `winget install Microsoft.OpenJDK.21` (existing JDK ≥ 17 kept) |
31
+ | Android SDK root | existing `ANDROID_HOME` / Android Studio SDK / `brew install --cask android-commandlinetools` | existing / `%LOCALAPPDATA%\Android\Sdk` / Google's command-line tools zip |
32
+ | `platform-tools` (adb) + `emulator` | `sdkmanager` | `sdkmanager` |
33
+ | Env vars | marked block in `~/.zshrc` (or `~/.bash_profile`) | User environment (`ANDROID_HOME`, `ANDROID_SDK_ROOT`, `JAVA_HOME`, `Path`) |
34
+ | Android Studio | **check + hint only** (optional — AVD manager / on-screen emulator) | same |
35
+ | Xcode Command Line Tools | `xcode-select --install` (Apple's dialog — finish it, re-run) | — |
36
+ | Xcode.app | **check + hint only** (App Store; required for iOS) | — |
37
+ | Xcode license | `sudo xcodebuild -license accept` | — |
38
+ | `applesimutils` | `brew tap wix/brew && brew install applesimutils` | — |
39
+ | Appium | **project-local** (`node_modules/.bin/appium`, from `@sungen/driver-mobile`) — never global | same |
40
+ | Appium drivers | `uiautomator2` + `xcuitest` into `~/.appium` | `uiautomator2` |
41
+ | Health | `adb`, env vars, devices, Simulators, `appium driver doctor` | `adb`, env vars, devices, `appium driver doctor` |
42
+
43
+ Nothing is installed globally with npm. GUI apps (Xcode.app, Android Studio) are never installed for you —
44
+ the step tells you exactly where to get them and re-running the script picks up from there.
45
+
46
+ ## The env-var block
47
+
48
+ macOS: the lines live between `# >>> sungen mobile env >>>` and `# <<< sungen mobile env <<<` in your
49
+ shell profile; re-running rewrites only that block, and it is skipped entirely when your shell already
50
+ exports matching values. To undo, delete the block. Windows: plain User-scope variables — undo in
51
+ *System Properties → Environment Variables*.
52
+
53
+ **Open a new terminal after the first run** so `PATH` / `JAVA_HOME` / `ANDROID_HOME` apply.
54
+
55
+ ## Known limitations
56
+
57
+ - **Linux** is not provisioned (the entry point prints a notice and exits 0). Android on Linux: install a
58
+ JDK ≥ 17 and the Android command-line tools with your package manager, export the same variables, then
59
+ `sungen capability add mobile` for the project-local Appium + `uiautomator2`.
60
+ - **`sungen update` does not refresh this folder.** To pick up a newer bundle, delete `appium-environment/`
61
+ and run `sungen capability add mobile` again (all other files are skipped because they already exist).
62
+ - Windows downloads Google's command-line tools by a **pinned build number** (`$CmdlineToolsBuild` at the
63
+ top of `scripts/windows/install-android.ps1`). If the download 404s, bump it from
64
+ https://developer.android.com/studio#command-line-tools-only.
65
+ - No AVD / Simulator is created and no phone is paired — plug in a device (USB debugging on) or start an
66
+ emulator before `/sungen:run-test`.
67
+
68
+ ## Manual fallback (the steps this bundle automates)
69
+
70
+ - Android: https://sungen.sun-asterisk.vn/docs/mobile/appium-setup-android
71
+ - iOS: https://sungen.sun-asterisk.vn/docs/mobile/appium-setup-ios
72
+ - Overview + troubleshooting: https://sungen.sun-asterisk.vn/docs/mobile/appium-setup ·
73
+ https://sungen.sun-asterisk.vn/docs/mobile/mobile-troubleshooting
74
+
75
+ ## Layout
76
+
77
+ ```
78
+ appium-environment/
79
+ ├── setup.sh · setup.ps1 entry points (OS check, flags, step order, summary)
80
+ ├── scripts/common/ lib.{sh,ps1} step runner · verify-node · install-appium · verify-appium
81
+ ├── scripts/macos/ install-android · verify-android · install-ios · verify-ios
82
+ └── scripts/windows/ install-android · verify-android
83
+ ```
@@ -0,0 +1,44 @@
1
+ # install-appium.ps1 - Appium stays PROJECT-LOCAL (a devDependency pulled in by @sungen/driver-mobile),
2
+ # never `npm i -g appium`; the platform driver (uiautomator2) lives in ~\.appium and is installed
3
+ # through that local CLI. Mirrors what `sungen init --mobile` does in TypeScript, so this script also
4
+ # works on a fresh clone where a teammate ran init.
5
+ if (-not $script:SungenEnvLibLoaded) { . (Join-Path $PSScriptRoot 'lib.ps1') }
6
+
7
+ $AppiumCmd = $script:SungenEnvAppiumCmd
8
+
9
+ function Get-AppiumVersion { if (Test-Path $AppiumCmd) { ((& $AppiumCmd -v) 2>$null | Select-Object -First 1) } }
10
+ Invoke-Step -Title 'Appium (project-local)' -Check {
11
+ if (Test-Path $AppiumCmd) { "appium $(Get-AppiumVersion) (project-local: node_modules\.bin\appium)" }
12
+ } -Install {
13
+ # Only re-install when the project already declares the mobile driver - otherwise the fix is
14
+ # `sungen capability add mobile`, a consent-gated sungen command, not this script's call.
15
+ $pkg = Join-Path $script:ProjectDir 'package.json'
16
+ if ((Test-Path $pkg) -and ((Get-Content $pkg -Raw) -match '"@sungen/driver-mobile"')) {
17
+ Push-Location $script:ProjectDir; try { Invoke-Native npm @('install') } finally { Pop-Location }
18
+ } else { return 'needs-user' }
19
+ } -Verify {
20
+ if (Test-Path $AppiumCmd) { "appium $(Get-AppiumVersion)" }
21
+ } -Hint "Appium is project-local (never global). Add the mobile driver + runtime with:`n sungen capability add mobile"
22
+
23
+ # --- platform driver -------------------------------------------------------------------------------
24
+ function Get-InstalledDrivers {
25
+ $raw = (& $AppiumCmd driver list --installed --json) 2>$null | Out-String
26
+ if (-not $raw.Trim()) { return $null }
27
+ try { return ($raw | ConvertFrom-Json) } catch { return $null }
28
+ }
29
+ function Get-DriverDetail([string]$Name) {
30
+ $j = Get-InstalledDrivers
31
+ if ($j -and ($j.PSObject.Properties.Name -contains $Name)) { "$Name $($j.$Name.version) in ~\.appium" }
32
+ }
33
+
34
+ if (Test-Path $AppiumCmd) {
35
+ if (-not $script:SungenEnvSkipAndroid) {
36
+ $script:CurrentDriver = 'uiautomator2'
37
+ Invoke-Step -Title "Appium driver: $($script:CurrentDriver)" -Check { Get-DriverDetail $script:CurrentDriver } `
38
+ -Install { Invoke-Native $AppiumCmd @('driver', 'install', $script:CurrentDriver) } `
39
+ -Verify { Get-DriverDetail $script:CurrentDriver } `
40
+ -Hint "retry: npx appium driver install $($script:CurrentDriver)"
41
+ }
42
+ } else {
43
+ Write-Warn 'Appium platform driver (uiautomator2)' 'skipped - Appium is not installed in this project yet (see the step above)'
44
+ }
@@ -0,0 +1,50 @@
1
+ #!/usr/bin/env bash
2
+ # install-appium.sh — Appium stays PROJECT-LOCAL (a devDependency pulled in by @sungen/driver-mobile),
3
+ # never `npm i -g appium`; the platform drivers (uiautomator2 / xcuitest) live in ~/.appium and are
4
+ # installed through that local CLI. Mirrors what `sungen init --mobile` does in TypeScript, so this
5
+ # script also works on a fresh clone where a teammate ran init.
6
+ [ -n "${SUNGEN_ENV_LIB_LOADED:-}" ] || . "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/lib.sh"
7
+
8
+ APPIUM_BIN=$SUNGEN_ENV_APPIUM_BIN
9
+
10
+ appium_check() {
11
+ [ -x "$APPIUM_BIN" ] || return 1
12
+ printf 'appium %s (project-local: node_modules/.bin/appium)' "$("$APPIUM_BIN" -v 2>/dev/null)"
13
+ }
14
+ appium_install() {
15
+ # Only re-install when the project already declares the mobile driver — otherwise the fix is
16
+ # `sungen capability add mobile`, which is a consent-gated sungen command, not this script's call.
17
+ if grep -q '"@sungen/driver-mobile"' "$PROJECT_DIR/package.json" 2>/dev/null; then
18
+ (cd "$PROJECT_DIR" && npm install)
19
+ else
20
+ return 2
21
+ fi
22
+ }
23
+ appium_verify() {
24
+ appium_check || { printf 'Appium is project-local (never global). Add the mobile driver + runtime with:\n sungen capability add mobile'; return 1; }
25
+ }
26
+ run_step "Appium (project-local)" appium_check appium_install appium_verify
27
+
28
+ # ── platform drivers — one run_step per driver, CURRENT_DRIVER carries the name (bash 3.2: no closures)
29
+ installed_drivers_json() { "$APPIUM_BIN" driver list --installed --json 2>/dev/null; }
30
+ drv_check() {
31
+ local json ver
32
+ json=$(installed_drivers_json) || return 1
33
+ printf '%s' "$json" | grep -q "\"$CURRENT_DRIVER\"" || return 1
34
+ ver=$(printf '%s' "$json" | node -e "let s='';process.stdin.on('data',d=>s+=d).on('end',()=>{try{const j=JSON.parse(s);process.stdout.write((j['$CURRENT_DRIVER']||{}).version||'')}catch{}})" 2>/dev/null)
35
+ printf '%s %s in ~/.appium' "$CURRENT_DRIVER" "${ver:-}"
36
+ }
37
+ drv_install() { "$APPIUM_BIN" driver install "$CURRENT_DRIVER"; }
38
+ drv_verify() { drv_check || { printf 'retry: npx appium driver install %s' "$CURRENT_DRIVER"; return 1; }; }
39
+
40
+ if [ -x "$APPIUM_BIN" ]; then
41
+ APPIUM_DRIVERS="uiautomator2"
42
+ [ "$SUNGEN_ENV_SKIP_ANDROID" = 1 ] && APPIUM_DRIVERS=""
43
+ # xcuitest builds WebDriverAgent with Xcode at run time → only meaningful on a Mac.
44
+ if [ "$(uname -s)" = Darwin ] && [ "$SUNGEN_ENV_SKIP_IOS" != 1 ]; then APPIUM_DRIVERS="$APPIUM_DRIVERS xcuitest"; fi
45
+ for CURRENT_DRIVER in $APPIUM_DRIVERS; do
46
+ run_step "Appium driver: $CURRENT_DRIVER" drv_check drv_install drv_verify
47
+ done
48
+ else
49
+ log_warn "Appium platform drivers (uiautomator2 / xcuitest)" "skipped — Appium is not installed in this project yet (see the step above)"
50
+ fi
@@ -0,0 +1,108 @@
1
+ # lib.ps1 - shared step runner for the sungen appium-environment bundle (Windows side).
2
+ # Dot-sourced by setup.ps1 and by every scripts\**\*.ps1; never run directly.
3
+ # Windows PowerShell 5.1 compatible: no ternary, no ??, ASCII only (5.1 reads BOM-less files as ANSI).
4
+ #
5
+ # Step model (see docs: Mobile -> Appium setup -> "One command"):
6
+ # Check -> returns a detail string when already satisfied, $null otherwise (side-effect free)
7
+ # Install -> idempotent; output streams to the console; returns 'needs-user' when the user must finish it
8
+ # Verify -> returns a detail string when satisfied, $null otherwise; -Hint is shown on failure
9
+
10
+ if ($script:SungenEnvLibLoaded) { return }
11
+ $script:SungenEnvLibLoaded = $true
12
+ $ErrorActionPreference = 'Continue'
13
+
14
+ # Flags - set by setup.ps1's parameters, read by every step.
15
+ if ($null -eq $script:SungenEnvCheckOnly) { $script:SungenEnvCheckOnly = $false }
16
+ if ($null -eq $script:SungenEnvSkipAndroid) { $script:SungenEnvSkipAndroid = $false }
17
+ if ($null -eq $script:SungenEnvNoProfile) { $script:SungenEnvNoProfile = $false }
18
+ # Flipped to $true when the User environment actually changed.
19
+ $script:SungenEnvProfileChanged = $false
20
+
21
+ # Paths: <project>\appium-environment\scripts\common\lib.ps1 -> bundle = ..\.., project = bundle\..
22
+ $script:SungenEnvBundleDir = (Resolve-Path (Join-Path $PSScriptRoot '..\..')).Path
23
+ if (-not $script:ProjectDir) { $script:ProjectDir = (Resolve-Path (Join-Path $script:SungenEnvBundleDir '..')).Path }
24
+ # Appium is project-local by design (never `npm i -g appium`) - see install-appium.ps1.
25
+ $script:SungenEnvAppiumCmd = Join-Path $script:ProjectDir 'node_modules\.bin\appium.cmd'
26
+
27
+ $script:CountOk = 0; $script:CountSkip = 0; $script:CountWarn = 0; $script:CountFail = 0
28
+
29
+ function Write-Section([string]$Title) { Write-Host ''; Write-Host $Title -ForegroundColor White }
30
+ function Write-Detail([string]$Text) {
31
+ if (-not $Text) { return }
32
+ foreach ($line in ($Text -split "`r?`n")) { if ($line) { Write-Host " $line" -ForegroundColor DarkGray } }
33
+ }
34
+ function Write-Ok([string]$Title, [string]$Detail) { $script:CountOk++; Write-Host "[ok] $Title" -ForegroundColor Green; Write-Detail $Detail }
35
+ function Write-Skip([string]$Title, [string]$Detail) { $script:CountSkip++; Write-Host "[skip] $Title" -ForegroundColor DarkGray; Write-Detail $Detail }
36
+ function Write-Warn([string]$Title, [string]$Detail) { $script:CountWarn++; Write-Host "[warn] $Title" -ForegroundColor Yellow; Write-Detail $Detail }
37
+ function Write-Fail([string]$Title, [string]$Detail) { $script:CountFail++; Write-Host "[fail] $Title" -ForegroundColor Red; Write-Detail $Detail }
38
+ function Write-Install([string]$Title) { Write-Host "[install] $Title" -ForegroundColor Cyan }
39
+
40
+ # Invoke-Native <exe> <args...> - run a native command, throw on a non-zero exit code.
41
+ function Invoke-Native([string]$File, [string[]]$Arguments) {
42
+ & $File @Arguments
43
+ if ($LASTEXITCODE -ne 0) { throw "$File exited with code $LASTEXITCODE" }
44
+ }
45
+
46
+ # Invoke-Step - an install-type step.
47
+ function Invoke-Step {
48
+ param([string]$Title, [scriptblock]$Check, [scriptblock]$Install, [scriptblock]$Verify, [string]$Hint)
49
+ $found = $null; try { $found = & $Check } catch { $found = $null }
50
+ if ($found) { Write-Skip $Title "$found"; return }
51
+ if ($script:SungenEnvCheckOnly) { Write-Fail $Title 'not installed - run without -Check to install'; return }
52
+ Write-Install $Title
53
+ $needsUser = $false; $err = $null
54
+ try { $r = & $Install; if ("$r" -eq 'needs-user') { $needsUser = $true } } catch { $err = $_.Exception.Message }
55
+ $ok = $null; try { $ok = & $Verify } catch { $ok = $null }
56
+ if ($ok) { Write-Ok $Title "$ok"; return }
57
+ $msg = $Hint; if ($err) { $msg = "$err`n$Hint" }
58
+ if ($needsUser) { Write-Warn $Title $msg } else { Write-Fail $Title $msg }
59
+ }
60
+
61
+ # Invoke-Check - a verification-only step (ok / fail, or -Warn for informational checks).
62
+ function Invoke-Check {
63
+ param([string]$Title, [scriptblock]$Check, [string]$Hint, [switch]$Warn)
64
+ $out = $null; try { $out = & $Check } catch { $out = $null }
65
+ if ($out) { Write-Ok $Title "$out"; return }
66
+ if ($Warn) { Write-Warn $Title $Hint } else { Write-Fail $Title $Hint }
67
+ }
68
+
69
+ # Invoke-Guided - GUI installs this bundle never performs itself: skip when present, else warn with the next action.
70
+ function Invoke-Guided {
71
+ param([string]$Title, [scriptblock]$Check, [string]$Guidance)
72
+ $found = $null; try { $found = & $Check } catch { $found = $null }
73
+ if ($found) { Write-Skip $Title "$found" } else { Write-Warn $Title $Guidance }
74
+ }
75
+
76
+ # --- user environment (the Windows counterpart of the shell-profile block) ------------------------
77
+ # Update-SessionPath - pick up PATH changes made by installers (winget) without a new console.
78
+ function Update-SessionPath {
79
+ $env:Path = [Environment]::GetEnvironmentVariable('Path', 'Machine') + ';' + [Environment]::GetEnvironmentVariable('Path', 'User')
80
+ }
81
+ # Set-UserEnv <name> <value> - user-scope variable (what `setx` does, without its 1024-char truncation). $true when changed.
82
+ function Set-UserEnv([string]$Name, [string]$Value) {
83
+ if ([Environment]::GetEnvironmentVariable($Name, 'User') -eq $Value) { Set-Item -Path "env:$Name" -Value $Value; return $false }
84
+ [Environment]::SetEnvironmentVariable($Name, $Value, 'User')
85
+ Set-Item -Path "env:$Name" -Value $Value
86
+ $script:SungenEnvProfileChanged = $true
87
+ return $true
88
+ }
89
+ # Add-UserPath <dir> - append to the user-scope Path once; also to this session. $true when changed.
90
+ function Add-UserPath([string]$Dir) {
91
+ $current = [Environment]::GetEnvironmentVariable('Path', 'User')
92
+ $parts = @(); if ($current) { $parts = @($current -split ';' | Where-Object { $_ }) }
93
+ if (-not (($env:Path -split ';') -contains $Dir)) { $env:Path = "$env:Path;$Dir" }
94
+ if ($parts -contains $Dir) { return $false }
95
+ [Environment]::SetEnvironmentVariable('Path', (($parts + $Dir) -join ';'), 'User')
96
+ $script:SungenEnvProfileChanged = $true
97
+ return $true
98
+ }
99
+
100
+ # Write-Summary - tallies + the "new terminal" notice. Returns 1 when anything failed, else 0.
101
+ function Write-Summary {
102
+ Write-Host ''
103
+ Write-Host "$($script:CountOk) ok, $($script:CountSkip) skipped, $($script:CountWarn) warned, $($script:CountFail) failed" -ForegroundColor White
104
+ if ($script:SungenEnvProfileChanged) { Write-Host '-> Open a NEW terminal so Path / JAVA_HOME / ANDROID_HOME apply.' -ForegroundColor Yellow }
105
+ if ($script:CountWarn -gt 0) { Write-Host ' warnings need a manual step - do it, then re-run this script.' }
106
+ if ($script:CountFail -gt 0) { return 1 }
107
+ return 0
108
+ }
@@ -0,0 +1,141 @@
1
+ #!/usr/bin/env bash
2
+ # lib.sh — shared step runner for the sungen appium-environment bundle (macOS / Linux side).
3
+ # Sourced by setup.sh and by every scripts/**/*.sh; never run directly.
4
+ # Bash 3.2 compatible (the /bin/bash that ships with macOS): no associative arrays, no ${var,,}.
5
+ #
6
+ # Step model (see docs: Mobile → Appium setup → "One command"):
7
+ # check → exit 0 when already satisfied (side-effect free — this is all `--check` runs)
8
+ # install → idempotent; output streams to the terminal; exit 2 = started but needs a user action
9
+ # verify → exit 0 when satisfied; stdout is the detail (ok) or the hint (fail)
10
+
11
+ [ -n "${SUNGEN_ENV_LIB_LOADED:-}" ] && return 0
12
+ SUNGEN_ENV_LIB_LOADED=1
13
+
14
+ # Flags — set by setup.sh's argument parser, read by every step.
15
+ SUNGEN_ENV_CHECK_ONLY=${SUNGEN_ENV_CHECK_ONLY:-0}
16
+ SUNGEN_ENV_SKIP_ANDROID=${SUNGEN_ENV_SKIP_ANDROID:-0}
17
+ SUNGEN_ENV_SKIP_IOS=${SUNGEN_ENV_SKIP_IOS:-0}
18
+ SUNGEN_ENV_NO_PROFILE=${SUNGEN_ENV_NO_PROFILE:-0}
19
+ # Flipped to 1 by write_profile_block when the shell profile actually changed.
20
+ SUNGEN_ENV_PROFILE_CHANGED=0
21
+
22
+ # Paths: <project>/appium-environment/scripts/common/lib.sh → bundle = ../.., project = bundle/..
23
+ SUNGEN_ENV_BUNDLE_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)
24
+ PROJECT_DIR=${PROJECT_DIR:-$(cd "$SUNGEN_ENV_BUNDLE_DIR/.." && pwd)}
25
+ # Appium is project-local by design (never `npm i -g appium`) — see install-appium.sh.
26
+ SUNGEN_ENV_APPIUM_BIN="$PROJECT_DIR/node_modules/.bin/appium"
27
+
28
+ # Environment as it was BEFORE this run — lets the profile step recognise a machine whose own
29
+ # shell profile already exports everything (then no block is written).
30
+ SUNGEN_ENV_INITIAL_ANDROID_HOME=${ANDROID_HOME:-}
31
+ SUNGEN_ENV_INITIAL_JAVA_HOME=${JAVA_HOME:-}
32
+ SUNGEN_ENV_INITIAL_ADB=$(command -v adb 2>/dev/null || true)
33
+
34
+ COUNT_OK=0; COUNT_SKIP=0; COUNT_WARN=0; COUNT_FAIL=0
35
+
36
+ if [ -t 1 ]; then
37
+ C_OK=$'\033[32m'; C_WARN=$'\033[33m'; C_FAIL=$'\033[31m'; C_DIM=$'\033[2m'; C_BOLD=$'\033[1m'; C_RESET=$'\033[0m'
38
+ else
39
+ C_OK=''; C_WARN=''; C_FAIL=''; C_DIM=''; C_BOLD=''; C_RESET=''
40
+ fi
41
+
42
+ have() { command -v "$1" >/dev/null 2>&1; }
43
+ # version_major "v22.17.1" → 22
44
+ version_major() { printf '%s' "$1" | sed -E 's/^[^0-9]*([0-9]+).*/\1/'; }
45
+ # java_major_of <java binary> → 21 (empty when not runnable). "1.8.0_x" → 1, so JDK 8 fails a ≥17 gate.
46
+ java_major_of() { "$1" -version 2>&1 | head -n1 | sed -E 's/^[^"]*"([0-9]+)(\.[0-9]+)*.*$/\1/' | grep -E '^[0-9]+$'; }
47
+ # first_line <multi-line string>
48
+ first_line() { printf '%s' "${1%%$'\n'*}"; }
49
+
50
+ section() { printf '\n%s%s%s\n' "$C_BOLD" "$1" "$C_RESET"; }
51
+ _detail() { [ -n "$1" ] && printf ' %s%s%s\n' "$C_DIM" "$1" "$C_RESET"; return 0; }
52
+ _detail_lines() { printf '%s\n' "$1" | while IFS= read -r line; do [ -n "$line" ] && _detail "$line"; done; return 0; }
53
+
54
+ log_ok() { COUNT_OK=$((COUNT_OK+1)); printf '%s✓ ok%s %s\n' "$C_OK" "$C_RESET" "$1"; _detail_lines "${2:-}"; }
55
+ log_skip() { COUNT_SKIP=$((COUNT_SKIP+1)); printf '%s– skip%s %s\n' "$C_DIM" "$C_RESET" "$1"; _detail_lines "${2:-}"; }
56
+ log_warn() { COUNT_WARN=$((COUNT_WARN+1)); printf '%s⚠ warn%s %s\n' "$C_WARN" "$C_RESET" "$1"; _detail_lines "${2:-}"; }
57
+ log_fail() { COUNT_FAIL=$((COUNT_FAIL+1)); printf '%s✗ fail%s %s\n' "$C_FAIL" "$C_RESET" "$1"; _detail_lines "${2:-}"; }
58
+ log_install() { printf '📦 install %s\n' "$1"; }
59
+
60
+ # run_step <title> <check_fn> <install_fn> <verify_fn> — an install-type step.
61
+ run_step() {
62
+ local title=$1 check_fn=$2 install_fn=$3 verify_fn=$4 found out rc
63
+ if found=$("$check_fn" 2>/dev/null); then log_skip "$title" "${found:-already installed}"; return 0; fi
64
+ if [ "$SUNGEN_ENV_CHECK_ONLY" = 1 ]; then log_fail "$title" "not installed — run without --check to install"; return 0; fi
65
+ log_install "$title"
66
+ "$install_fn"; rc=$?
67
+ if out=$("$verify_fn" 2>&1); then log_ok "$title" "$out"; return 0; fi
68
+ if [ "$rc" -eq 2 ]; then log_warn "$title" "${out:-needs a manual step — re-run setup.sh afterwards}"
69
+ else log_fail "$title" "${out:-install did not complete}"; fi
70
+ return 0
71
+ }
72
+
73
+ # run_check <title> <check_fn> <hint> [warn] — a verification-only step (ok / fail, or warn for informational checks).
74
+ run_check() {
75
+ local title=$1 check_fn=$2 hint=$3 level=${4:-fail} out msg
76
+ if out=$("$check_fn" 2>&1); then log_ok "$title" "$out"; return 0; fi
77
+ msg=$hint; [ -n "$out" ] && msg="$out"$'\n'"$hint"
78
+ if [ "$level" = warn ]; then log_warn "$title" "$msg"; else log_fail "$title" "$msg"; fi
79
+ return 0
80
+ }
81
+
82
+ # run_guided <title> <check_fn> <guidance> — GUI / App Store installs this bundle never performs itself.
83
+ run_guided() {
84
+ local title=$1 check_fn=$2 guidance=$3 found
85
+ if found=$("$check_fn" 2>/dev/null); then log_skip "$title" "${found:-already installed}"; return 0; fi
86
+ log_warn "$title" "$guidance"
87
+ return 0
88
+ }
89
+
90
+ # ── shell profile block ────────────────────────────────────────────────────────────────────────
91
+ PROFILE_BEGIN='# >>> sungen mobile env >>>'
92
+ PROFILE_END='# <<< sungen mobile env <<<'
93
+ # zsh → ~/.zshrc, bash → ~/.bash_profile, anything else → ~/.profile
94
+ profile_file() {
95
+ case "${SHELL:-}" in
96
+ *zsh) printf '%s' "$HOME/.zshrc" ;;
97
+ *bash) printf '%s' "$HOME/.bash_profile" ;;
98
+ *) printf '%s' "$HOME/.profile" ;;
99
+ esac
100
+ }
101
+ profile_block_content() {
102
+ local file; file=$(profile_file); [ -f "$file" ] || return 0
103
+ awk -v b="$PROFILE_BEGIN" -v e="$PROFILE_END" '$0==b{f=1;next} $0==e{f=0} f' "$file"
104
+ }
105
+ # write_profile_block <content> — append the marked block, replace it when different, keep it when identical.
106
+ # Returns 0 = unchanged, 3 = written. Sets PROFILE_MSG. Must NOT be called in a subshell (it sets globals).
107
+ write_profile_block() {
108
+ local content=$1 file tmp
109
+ file=$(profile_file); [ -f "$file" ] || : > "$file"
110
+ if [ "$(profile_block_content)" = "$content" ]; then PROFILE_MSG="block already up to date in $file"; return 0; fi
111
+ tmp=$(mktemp)
112
+ awk -v b="$PROFILE_BEGIN" -v e="$PROFILE_END" '$0==b{f=1;next} $0==e{f=0;next} !f' "$file" > "$tmp"
113
+ [ -s "$tmp" ] && [ -n "$(tail -c1 "$tmp")" ] && printf '\n' >> "$tmp"
114
+ { cat "$tmp"; printf '%s\n%s\n%s\n' "$PROFILE_BEGIN" "$content" "$PROFILE_END"; } > "$file"
115
+ rm -f "$tmp"
116
+ SUNGEN_ENV_PROFILE_CHANGED=1
117
+ PROFILE_MSG="written to $file — open a new terminal to apply"
118
+ return 3
119
+ }
120
+ # profile_step <title> <content> — the profile block as a step (honours --no-profile and --check).
121
+ profile_step() {
122
+ local title=$1 content=$2
123
+ if [ "$SUNGEN_ENV_NO_PROFILE" = 1 ]; then log_skip "$title" "--no-profile"; return 0; fi
124
+ if [ "$SUNGEN_ENV_CHECK_ONLY" = 1 ]; then
125
+ if [ "$(profile_block_content)" = "$content" ]; then log_skip "$title" "block already up to date in $(profile_file)"
126
+ else log_fail "$title" "block missing or outdated in $(profile_file) — run without --check to write it"; fi
127
+ return 0
128
+ fi
129
+ write_profile_block "$content"
130
+ case $? in 3) log_ok "$title" "$PROFILE_MSG" ;; *) log_skip "$title" "$PROFILE_MSG" ;; esac
131
+ return 0
132
+ }
133
+
134
+ # print_summary — tallies + the "new terminal" notice. Exit 1 when anything failed.
135
+ print_summary() {
136
+ printf '\n%s%d ok · %d skipped · %d warned · %d failed%s\n' "$C_BOLD" "$COUNT_OK" "$COUNT_SKIP" "$COUNT_WARN" "$COUNT_FAIL" "$C_RESET"
137
+ [ "$SUNGEN_ENV_PROFILE_CHANGED" = 1 ] && printf '%s→ Open a NEW terminal so PATH / JAVA_HOME / ANDROID_HOME apply.%s\n' "$C_WARN" "$C_RESET"
138
+ [ "$COUNT_WARN" -gt 0 ] && printf ' warnings need a manual step — do it, then re-run this script.\n'
139
+ [ "$COUNT_FAIL" -gt 0 ] && return 1
140
+ return 0
141
+ }