@unotest/web 0.9.1 → 0.11.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.
@@ -69,6 +69,65 @@ Disambiguate multi-matches with `.filter({hasText: '…'})` or
69
69
  `.nth(N)`. The linter flags index-based picking
70
70
  (`lint:disambig-by-index`) because element order is brittle.
71
71
 
72
+ ## Discovery: snapshot mode vs ground mode
73
+
74
+ Which discovery tool exists depends on the server's mode — check the
75
+ tool list once. `get_page_snapshot` present — snapshot mode. Absent
76
+ (the server runs with `UNOTEST_SNAPSHOT=0`, ground mode) — it does not
77
+ exist, do NOT search or retry for it. In ground mode you normally need
78
+ no discovery call at all — act directly with an **intent locator**;
79
+ the runner grounds it inside the same call:
80
+
81
+ ```
82
+ locator: {kind:"locator", steps:[{kind:"intent",
83
+ intent:"<the element in the user's words>",
84
+ ordinal?: "first" | "last" | N}]}
85
+ ```
86
+
87
+ Positions go in `ordinal` (typed), never in the intent text. Name the
88
+ target itself, not its page region. Lookups for when you must look
89
+ before acting: `ground_element {intent, ordinal?}` (element line +
90
+ top-20 candidates) and `find_element {role, name, near?}` (exact ARIA
91
+ query). Any "re-`get_page_snapshot`" advice below reads as "retry the
92
+ intent step / re-ground via `ground_element`" in ground mode.
93
+
94
+ ## Batching: `explore_steps`
95
+
96
+ When the next several steps are known in advance, send ONE
97
+ `explore_steps {explorationId?, steps:[…]}` call — up to 25 steps, each
98
+ with the same shape as `explore_step`. Sequential; stops at the first
99
+ failure; the reply carries per-step status + `firstError`; only the
100
+ steps that executed are recorded.
101
+
102
+ Every step whose locator was an intent replies with `groundedTo` — the
103
+ element line the intent resolved to. Check the target there rather than
104
+ pre-probing with `ground_element`. A step that CHANGED the address also
105
+ replies with `url`: the last one in the batch is where you ended up, so
106
+ `get_active_context` / `get_url` afterwards re-reads what you have.
107
+
108
+ Record checks as assert steps — `assert_text {locator, text, options?:
109
+ {exact}}`, `assert_visible {locator}`, `assert_hidden {locator}`,
110
+ `assert_value {locator, value}`, `assert_count {locator, count}`,
111
+ `assert_url {pattern}`. When the brief names no check, verify with the
112
+ text it DID give you — the last thing you were told to click is on the
113
+ page you land on, so assert that name inside the same batch, quoting
114
+ the text alone (`"<name>"`, not `heading "<name>"` — the words often
115
+ live in a row or a link, and a role word then grounds to nothing);
116
+ don't spend calls hunting for something assertable.
117
+ `assert_url` is for briefs that name the destination URL. On a checkbox
118
+ / radio / switch `assert_value`
119
+ takes `"true"` / `"false"` and asserts the checked state — no CSS
120
+ `:checked` selector. They execute live, so a recorded assert has
121
+ already passed against the real page; never hand-edit assertions into
122
+ the generated file. The final batch of a scenario can pass `autoRun:
123
+ true`: when every step succeeds, a verification step is recorded and
124
+ the draft has no blocking warnings (FRAGILE_LOCATOR passes and is
125
+ reported in the reply; DYNAMIC_TEXT / NO_DSL_PRIMITIVE block), the same
126
+ call saves the scenario, resets the context and runs it
127
+ (`autoRun.run.next.outcome === "completed"` is the pass signal);
128
+ otherwise `autoRun.status: "skipped"` + reason and the manual
129
+ generate → save → run flow applies.
130
+
72
131
  ## Recording: name the `section` like a test-plan line
73
132
 
74
133
  Each `explore_step`/`explore_record` carries a `section` that becomes a
@@ -176,6 +235,11 @@ result = evaluate(
176
235
  );
177
236
  ```
178
237
 
238
+ Index access reads data off a **bare variable** only — `obj.prop[i]`
239
+ does not parse; assign the member first:
240
+ `m = state.matches; last = m[m.length - 1];`. `arr[i] = v` stays
241
+ rejected.
242
+
179
243
  ## Sandbox primitives
180
244
 
181
245
  For test setup / teardown that touches the host machine, the registry
@@ -187,9 +251,13 @@ exposes four primitives — **but the consumer project's
187
251
  - **`dbQuery(sql, …params)`** / **`dbExec(sql, …params)`** —
188
252
  parameterized SQL against `sandbox.database` (postgres / mysql /
189
253
  sqlite, picked by URL scheme).
190
- - **`apiCall(method, path, body?, headers?)`** — HTTP fetch against
191
- `sandbox.apiBaseUrl`. **Path-only** — absolute URLs throw to prevent
192
- scenarios from hitting arbitrary hosts.
254
+ - **`apiCall(method, path, body?, headers?, opts?)`** — HTTP fetch
255
+ against `sandbox.apiBaseUrl`. **Path-only** — absolute URLs throw to
256
+ prevent scenarios from hitting arbitrary hosts. Multipart upload:
257
+ pass `upload('fixtures/doc.pdf', {field?, fields?})` as the body
258
+ (relative path inside the project / `sandbox.uploadDir`; don't set
259
+ Content-Type yourself). A JSON body with a `file` key posts as plain
260
+ JSON — there is no key-name magic.
193
261
 
194
262
  If a primitive throws with "sandbox.X is not configured", tell the
195
263
  user to add it to `unotest.config.*`. Do not infer secrets from the