@pcamarajr/scout 0.13.0 → 0.15.0

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/store.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"store.js","sourceRoot":"","sources":["../src/store.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"store.js","sourceRoot":"","sources":["../src/store.ts"],"names":[],"mappings":"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"}
package/dist/types.d.ts CHANGED
@@ -1,13 +1,75 @@
1
- /** How a recorded step finds its element on replay. */
1
+ /**
2
+ * How a recorded step finds its element on replay. A Target is derived at record
3
+ * time by the selector preference ladder (see `runner/selector-ladder.ts`): the
4
+ * most stable strategy that *uniquely* matched the live element becomes the
5
+ * primary location fields, the other unique strategies are kept as ordered
6
+ * {@link Target.fallbacks}, and {@link Target.fragile} flags the case where only
7
+ * a positional CSS path was available. Exactly one primary strategy is set:
8
+ * `testId`, `role`+`name`, `text`, or `css`.
9
+ */
2
10
  export interface Target {
3
- /** ARIA role + accessible name — preferred, resilient to DOM refactors */
11
+ /** ARIA role + accessible name — resilient to DOM refactors */
4
12
  role?: string;
5
13
  name?: string;
6
- /** CSS path fallback when role+name is not unique */
14
+ /** CSS path a stable `id` selector or, as a last resort, a positional path */
7
15
  css?: string;
16
+ /**
17
+ * data-testid (or the configured testid attribute) — the sturdiest handle:
18
+ * stable across DOM refactors and the strategy for elements OUTSIDE the
19
+ * accessibility tree (a gesture layer, an overlay `<div>` with no role/name).
20
+ * Resolved via Playwright's `getByTestId`. Top rung of the ladder.
21
+ */
22
+ testId?: string;
23
+ /**
24
+ * Visible-text anchor — resolved via Playwright's `getByText` (exact). Used
25
+ * when an element has no testid/id/role+name but a stable, unique visible
26
+ * label. Below role+name on the ladder, above a positional CSS path.
27
+ */
28
+ text?: string;
29
+ /**
30
+ * True when the primary strategy is a POSITIONAL CSS path (the ladder bottomed
31
+ * out) — the recorded step is fragile: it replays today but breaks as soon as
32
+ * the DOM shifts. Surfaced as a warning at record time and in the run report so
33
+ * a stable handle (a data-testid, a unique role/name) can be added.
34
+ */
35
+ fragile?: boolean;
36
+ /**
37
+ * Other ladder strategies that ALSO uniquely matched the element at record
38
+ * time, ordered most-stable first. On replay, if the primary strategy no
39
+ * longer resolves, these are tried in order — deterministically, with no LLM
40
+ * ("--no-heal" semantics preserved). Absent on steps recorded before this
41
+ * feature (backward compatible) and when no alternative uniquely matched.
42
+ */
43
+ fallbacks?: Target[];
8
44
  /** Human-readable label for reports */
9
45
  description: string;
10
46
  }
47
+ /**
48
+ * Matcher for a state assertion on an element found by a {@link Target}. Asserts
49
+ * VISUAL/structural state that text- and URL-based checks can't reach: a class
50
+ * token, an attribute, or a computed style. Its reason for being is the opacity
51
+ * toggle pattern — a control kept in the DOM but hidden with `opacity:0`, which
52
+ * Playwright's visibility (and therefore `assertNotVisible`) still counts as
53
+ * VISIBLE. Assert `opacity-0`/computed `opacity === "0"` instead. Every provided
54
+ * check must hold; polled until they do or the timeout elapses, so it is
55
+ * deterministic on replay.
56
+ */
57
+ export interface ElementStateMatcher {
58
+ /** A class token that MUST be present (e.g. "opacity-0"). Membership, not full-string equality. */
59
+ hasClass?: string;
60
+ /** A class token that must NOT be present (e.g. "opacity-100"). */
61
+ notHasClass?: string;
62
+ /** An attribute that must be present; with `value`, it must equal `value`. */
63
+ attribute?: {
64
+ name: string;
65
+ value?: string;
66
+ };
67
+ /** A computed style that must equal `value` (e.g. property "opacity", value "0"). */
68
+ computedStyle?: {
69
+ property: string;
70
+ value: string;
71
+ };
72
+ }
11
73
  /** HTTP status family used by network assertions when an exact code is too strict. */
12
74
  export type StatusClass = "2xx" | "3xx" | "4xx" | "5xx";
13
75
  /**
@@ -51,6 +113,45 @@ export interface Viewport {
51
113
  hasTouch?: boolean;
52
114
  userAgent?: string;
53
115
  }
116
+ /** Explicit viewport dimensions for device emulation. */
117
+ export interface ViewportSize {
118
+ width: number;
119
+ height: number;
120
+ }
121
+ /**
122
+ * Per-scenario device / user-agent emulation, resolved from a scenario's spec
123
+ * (profile default + file frontmatter + per-`##` override, merged per field;
124
+ * scenario wins over profile). It is a context-creation parameter — re-resolved
125
+ * fresh from the spec each run and applied at `browser.newContext()` — NEVER a
126
+ * recordable Step, so replay re-reads the frontmatter and stays deterministic
127
+ * (like storageState/cookies/permissions).
128
+ *
129
+ * Its reason for being is UI gated on user-agent / device detection — an
130
+ * "Add to Home Screen" sheet that only renders under an iOS-Safari UA, a layout
131
+ * branch that keys off `navigator.maxTouchPoints`. Without it the runner always
132
+ * launches desktop Chromium with its default UA, so those flows can never pass.
133
+ *
134
+ * `device` names a Playwright device descriptor (a key of the `devices`
135
+ * registry, e.g. "iPhone 14"), validated against the registry at parse time.
136
+ * The individual fields (`userAgent`, `viewport`, `deviceScaleFactor`,
137
+ * `isMobile`, `hasTouch`) compose on top of (or without) `device` — an
138
+ * explicit field always wins over the named device's value. None of these are
139
+ * secrets, so no `$ENV:VAR` resolution is applied (unlike cookies/storage).
140
+ */
141
+ export interface ScenarioDevice {
142
+ /** Playwright device registry key (e.g. "iPhone 14"); the base for overrides. */
143
+ device?: string;
144
+ /** User-agent string; wins over the named device's UA. */
145
+ userAgent?: string;
146
+ /** Viewport dimensions; win over the named device's viewport. */
147
+ viewport?: ViewportSize;
148
+ /** Device scale factor; wins over the named device's value. */
149
+ deviceScaleFactor?: number;
150
+ /** Whether to emulate a mobile device (meta viewport, touch events). */
151
+ isMobile?: boolean;
152
+ /** Whether the device supports touch. */
153
+ hasTouch?: boolean;
154
+ }
54
155
  /**
55
156
  * One cookie applied to the browser context before the scenario runs. Resolved
56
157
  * fresh from the spec each run (profile default + per-scenario frontmatter /
@@ -157,7 +258,11 @@ export type Step = {
157
258
  kind: "assertNotVisible";
158
259
  text: string;
159
260
  timeout?: number;
160
- } | {
261
+ } | ({
262
+ kind: "assertState";
263
+ target: Target;
264
+ timeout?: number;
265
+ } & ElementStateMatcher) | {
161
266
  kind: "assertUrl";
162
267
  pattern: string;
163
268
  timeout?: number;
@@ -222,6 +327,14 @@ export interface Scenario {
222
327
  * only at launch.
223
328
  */
224
329
  storage?: ScenarioStorage;
330
+ /**
331
+ * Device / user-agent emulation for the scenario (profile default + file
332
+ * frontmatter + per-section override, merged per field). A context-creation
333
+ * parameter re-resolved each run and applied at `newContext()` — never baked
334
+ * into the recorded script. Composes over the resolved viewport: the device's
335
+ * fields win over the viewport's for UA/metrics/size.
336
+ */
337
+ device?: ScenarioDevice;
225
338
  /** Source spec file, relative to the project root */
226
339
  file: string;
227
340
  }
@@ -250,4 +363,24 @@ export interface RunResult {
250
363
  * but it is NOT a UI judgment — rerun instead of debugging the app.
251
364
  */
252
365
  runnerFailure?: string;
366
+ /**
367
+ * Interaction steps whose recorded selector is a positional CSS path (the
368
+ * ladder bottomed out). Populated from the persisted/verified script so the
369
+ * fragility is visible at record time (CLI output + report), not discovered on
370
+ * a broken replay days later. Empty/absent when every step has a stable handle.
371
+ */
372
+ fragileSteps?: FragileStep[];
373
+ /**
374
+ * Notes emitted during deterministic replay when a step's primary selector no
375
+ * longer resolved and a recorded fallback did — no LLM involved. Absent when
376
+ * no fallback was needed (the common case).
377
+ */
378
+ usedFallbacks?: string[];
379
+ }
380
+ /** One recorded step that replays on a fragile positional selector. */
381
+ export interface FragileStep {
382
+ /** 1-based position of the step in the recorded script. */
383
+ step: number;
384
+ /** Human-readable description of the step (from `describeStep`). */
385
+ description: string;
253
386
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pcamarajr/scout",
3
- "version": "0.13.0",
3
+ "version": "0.15.0",
4
4
  "description": "Self-healing browser QA: natural-language scenarios verified by an AI agent, replayed deterministically in CI",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -53,9 +53,9 @@ Logged-in subscriber opens ep 3; the episode plays with no paywall.
53
53
 
54
54
  Rules that matter when you author:
55
55
 
56
- - **Frontmatter** (YAML, optional): `feature` (defaults to the filename), `profile` (default auth profile), `tags`, `viewports`, `cookies`, `storage`.
56
+ - **Frontmatter** (YAML, optional): `feature` (defaults to the filename), `profile` (default auth profile), `tags`, `viewports`, `cookies`, `storage`, `device`.
57
57
  - **Each `## heading` is one scenario.** Its logical slug is `<file-slug>/<scenario-slug>` (e.g. `paywall/free-user-hits-paywall-on-ep-3`) and must be unique across the suite. Duplicate headings in a file, or a scenario with no body text, are hard errors.
58
- - **Per-scenario overrides:** immediately under a heading you may place `profile:`, `notes:`, `tags:`, `viewports:`, `grantPermissions:`, `denyPermissions:`, `geolocation:`, `cookies:`, and `storage:` lines (before the prose) to override the file-level defaults.
58
+ - **Per-scenario overrides:** immediately under a heading you may place `profile:`, `notes:`, `tags:`, `viewports:`, `grantPermissions:`, `denyPermissions:`, `geolocation:`, `cookies:`, `storage:`, and `device:` lines (before the prose) to override the file-level defaults.
59
59
  - **Body = flow + expected behavior, in plain language.** Describe what the user does and what must (or must not) be true. No CSS selectors, no Playwright code — the agent discovers the real elements at run time and records them.
60
60
  - A `.scout.md` whose every `##` lives inside a fenced ```` ``` ```` block parses as **zero scenarios** (that is how `example.scout.md` documents the format without polluting the suite).
61
61
 
@@ -196,6 +196,38 @@ Notes:
196
196
  - **Secrets:** a value may use the `$ENV:VAR` placeholder — resolved at launch, so the secret never lands in the committed spec or the agent's context.
197
197
  - **Fail-fast:** an unknown field (only `local`/`session`/`remove` are allowed), a non-string value, or a malformed inline token is a hard error — a silently skipped storage precondition would produce a misleading verdict.
198
198
 
199
+ ## Device / user-agent emulation
200
+
201
+ Some UI is gated on **device or user-agent detection** — an "Add to Home Screen" sheet that only renders under an iOS-Safari UA, a layout branch that keys off touch support. By default Scout launches desktop Chromium with its own UA, so those flows can never pass. Declare a `device:` and Scout emulates it at browser launch, for both the AI run and deterministic replay. Like `cookies`/`storage`/`storageState`, it's a context-creation parameter, never a recorded step — replay re-reads the frontmatter, so it stays deterministic.
202
+
203
+ `device` names a **Playwright device descriptor** (see the [device registry](https://playwright.dev/docs/emulation#devices), e.g. `iPhone 14`, `Pixel 7`). Individual fields — `userAgent`, `viewport` (`{ width, height }`), `deviceScaleFactor`, `isMobile`, `hasTouch` — compose on top of (or without) the named device; an explicit field always wins over the device's value.
204
+
205
+ Two forms. In the **frontmatter** (file default) or a **profile** (shared base in `scout.config.json`), `device:` is an object:
206
+
207
+ ```markdown
208
+ ---
209
+ feature: Add to Home Screen
210
+ device:
211
+ device: iPhone 14 # a Playwright device name (the base)
212
+ userAgent: "Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) …" # optional override
213
+ ---
214
+
215
+ ## Sheet renders under iOS Safari
216
+ Open the app; the Add-to-Home-Screen sheet appears.
217
+
218
+ ## Same flow on Android
219
+ device: Pixel 7 # per-scenario inline override: a device name
220
+ Open the app; the native install banner appears instead.
221
+ ```
222
+
223
+ Notes:
224
+
225
+ - **Inline override form:** the per-`##` override is a single line naming a device — `device: <device name>`. It carries a device name only; individual overrides (`userAgent`, `viewport`, …) belong in the frontmatter/profile because a heading override line can't carry a nested YAML object.
226
+ - **Merge:** a profile's device is the base; the file frontmatter and then the per-scenario override win, **per field** (an inline `device: Pixel 7` keeps a file-level `userAgent`). The `viewport` field is replaced wholesale (it's a width/height pair).
227
+ - **Composes over the viewport:** the resolved device layers on top of the run's viewport — its `viewport`/UA/`isMobile`/`hasTouch`/`deviceScaleFactor` win over the viewport's. The named viewport still determines which sizes the scenario fans out in and the run identity (`<slug>@<viewport>`).
228
+ - **Not secrets:** these fields are plain values, so there is no `$ENV:VAR` resolution (unlike cookies/storage).
229
+ - **Fail-fast:** an unknown field, an unknown device name, or a bad shape is a hard error at parse — a silently ignored device would produce a misleading verdict.
230
+
199
231
  ## New tabs / popups
200
232
 
201
233
  When a click opens a new tab (a `target="_blank"` link or `window.open`), Scout's `browser_click` result flags it. The agent then calls **`browser_switch_tab`** to move control to that tab; with no argument it switches to the newest tab, or pass a `urlGlob` (e.g. `**/booking**`) to target a specific one. The switch waits for the tab to finish loading, then becomes a recorded `switchTab` step that replays deterministically.
@@ -206,6 +238,40 @@ Console and network observers are **per-tab**: assertions (`browser_assert_no_co
206
238
 
207
239
  Beyond *absence* of errors, you can assert a **specific log was emitted** (e.g. a `DEBUG:[...]` line gated behind a debug flag). `browser_assert_console_message` requires a message on the **active tab** that contains **all** of the given substrings **within a single message**, optionally constrained to a `type` (`log`, `debug`, `error`, …). Inspect first (`browser_inspect_logs` now also lists `log`/`debug`/`info` messages), then assert on a **stable** substring — a prefix like `DEBUG:[FEATURE/x]`, never a volatile value — so the check tolerates unrelated console noise. It becomes a recorded step that fails the deterministic replay if the log goes missing.
208
240
 
241
+ ## Clicking role-less elements (by `data-testid`)
242
+
243
+ `browser_click` only takes a numbered `[ref]` from the accessibility snapshot, so an element with **no ARIA role or name** — a gesture/tap layer, an overlay `<div data-testid="…">`, a purely visual control — never gets a `[ref]` and can't be clicked that way. When you hit one, use **`browser_click_selector`**: it clicks by `data-testid` (**preferred** — stabler than a CSS path) or, if there is none, a `css` selector. It records as a plain deterministic `click` step, so replay needs no LLM. Reach for it only for elements the snapshot can't reference; a normal button still goes through `browser_click`.
244
+
245
+ ## How selectors are recorded (the preference ladder)
246
+
247
+ You never write selectors — Scout derives them at record time. For every interaction step (click, fill, select) it resolves the target element and walks a **preference ladder**, recording the most stable strategy that *uniquely* matches the live element:
248
+
249
+ 1. **`data-testid`** (or the configured testid attribute) — on the element or a close stable ancestor. The sturdiest handle; survives DOM refactors.
250
+ 2. **`id`** — but only a hand-authored-looking one. Framework-generated ids are rejected (any run of 3+ digits, or a `radix-` / `react-` / `headlessui-` / `mui-` / React `useId` `:r…` prefix), because they change between renders.
251
+ 3. **role + accessible name** — the accessible name is **computed from the live DOM** at record time (the real ARIA name), never guessed from the visible label. This matters: a button reading "Buy" with `aria-label="Purchase now"` is recorded with name **"Purchase now"** — the computed name — so the selector actually resolves on replay. (Guessing "Buy" from the visible label is exactly the mistake this prevents.)
252
+ 4. **visible text** — a stable, unique text anchor.
253
+ 5. **positional CSS path** — the last resort (`main > section:nth-of-type(3) > a:nth-of-type(2)`), used only when nothing above uniquely matched.
254
+
255
+ The chosen strategy becomes the step's primary selector; the other strategies that *also* uniquely matched are kept as ordered **fallbacks** (below).
256
+
257
+ **The testid attribute is configurable.** It defaults to `data-testid`; set `"testIdAttribute": "data-test"` (or `data-qa`, …) in `scout.config.json` if the app uses a different convention — the ladder reads it and `getByTestId` resolves it on replay.
258
+
259
+ ### Fragile selectors — a warning at record time, not a failure
260
+
261
+ When the ladder **bottoms out at a positional CSS path**, the step is marked **fragile**: it replays today but breaks the moment the DOM shifts. Scout makes this visible immediately — `scout go` prints a warning and the run report lists the fragile steps:
262
+
263
+ > `step 4 (click main > a:nth-of-type(2)) recorded with a positional selector — add a data-testid or a unique role/name to make replay robust.`
264
+
265
+ This is deliberate: fragility surfaces **at record time**, not days later as a red CI replay. The fix lives in the **app**, not the spec — add a `data-testid` (or a unique, accessible role/name) to the element, then re-record with `scout go --ai -s <slug>`. Don't treat the warning as noise: a suite full of positional selectors is a suite about to rot.
266
+
267
+ ### Fallback selectors — deterministic retry, no LLM
268
+
269
+ Each interaction step also stores the other ladder strategies that uniquely matched, as an ordered **fallback list**. On deterministic replay, if the primary selector no longer resolves, Scout tries the fallbacks in order **before failing** — a deterministic retry with **no AI involved**, so `--no-heal` semantics are fully preserved (this is *not* healing). When a fallback rescues a step, `scout go` and the report log which one (`step 3: testid "go" → fallback css #real`) — a nudge to re-record and refresh the primary. Scripts recorded before this feature (no fallbacks) keep replaying unchanged — the format is backward compatible.
270
+
271
+ ## Asserting visual/structural state (opacity, class, attribute)
272
+
273
+ `browser_assert` covers text and URL, but it **can't confirm an element is hidden by `opacity:0`** — Playwright counts an opacity-hidden node (still in the DOM, still laid out) as *visible*, so a `notVisibleText`-style check would false-pass. For show/hide toggles and other purely visual state, use **`browser_assert_state`**: locate the element by `data-testid` (preferred) or `css`, then assert one or more of a class token (`hasClass` / `notHasClass`, e.g. `opacity-0` vs `opacity-100`), an `attribute` (present, or equal to a value like `aria-expanded=true`), or a `computedStyle` (e.g. `opacity` = `0`). It polls until every check holds or the timeout, then records a deterministic `assertState` step. This is what lets a scenario verify a reveal/hide control that stays mounted — describe the toggle in plain prose ("the drawer becomes hidden") and the agent records the class/style check.
274
+
209
275
  ## Base URL and secrets
210
276
 
211
277
  There is a **default base URL** set at `scout init` (in `scout.config.json`). It can be **overridden per run** without editing the file:
@@ -21,6 +21,6 @@ without running it.
21
21
 
22
22
  **Read `AGENTS.md` at the repo root — it is the canonical, always-current guide**
23
23
  to the authoring loop, the `.scout.md` format, per-scenario overrides (viewports,
24
- permissions, `cookies:` and `storage:` preconditions), base-URL/secret handling,
24
+ permissions, `cookies:`, `storage:` and `device:` preconditions), base-URL/secret handling,
25
25
  verdicts, and failure triage. Follow it. For commands and flags, run
26
26
  `scout --help` / `scout <command> --help`.
@@ -17,6 +17,6 @@ the **real** verdict — never claim a scenario passes without running it.
17
17
 
18
18
  **Read `AGENTS.md` at the repo root** — it is the canonical, always-current
19
19
  guide to the authoring loop, the `.scout.md` format, per-scenario overrides
20
- (viewports, permissions, `cookies:` and `storage:` preconditions),
20
+ (viewports, permissions, `cookies:`, `storage:` and `device:` preconditions),
21
21
  base-URL/secret handling, verdicts, and failure triage. For commands and flags,
22
22
  run `scout --help` / `scout <command> --help`.