e2e 0.16.0 → 0.18.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/README.md +10 -0
- package/dist/.build.json +1 -1
- package/dist/agent/act.js +7 -4
- package/dist/agent/action-dispatcher.js +4 -5
- package/dist/agent/actions.d.ts +32 -23
- package/dist/agent/actions.js +38 -24
- package/dist/agent/ai-sdk.js +5 -4
- package/dist/agent/error.js +3 -3
- package/dist/agent/invocation.js +3 -4
- package/dist/agent/model/sdk.js +2 -0
- package/dist/agent/model/tool-choice.d.ts +9 -0
- package/dist/agent/model/tool-choice.js +14 -0
- package/dist/agent/observation-feed.d.ts +5 -6
- package/dist/agent/observation-feed.js +8 -11
- package/dist/agent/observation.d.ts +40 -11
- package/dist/agent/observation.js +109 -59
- package/dist/agent/phases.d.ts +7 -1
- package/dist/agent/phases.js +12 -3
- package/dist/agent/point-tap.d.ts +8 -8
- package/dist/agent/point-tap.js +1 -1
- package/dist/agent/prompts.js +3 -0
- package/dist/agent/replay.d.ts +19 -8
- package/dist/agent/replay.js +29 -29
- package/dist/agent/scroll-target.d.ts +2 -3
- package/dist/agent/step-cache.d.ts +43 -15
- package/dist/agent/step-cache.js +156 -67
- package/dist/agent/tool-loop.js +94 -5
- package/dist/cache/anchors.d.ts +72 -23
- package/dist/cache/anchors.js +233 -60
- package/dist/cache/context.d.ts +25 -14
- package/dist/cache/context.js +50 -8
- package/dist/cache/decide.d.ts +2 -2
- package/dist/cache/decide.js +4 -6
- package/dist/cache/identity.d.ts +18 -4
- package/dist/cache/identity.js +9 -4
- package/dist/cache/recorder.d.ts +15 -1
- package/dist/cache/recorder.js +36 -23
- package/dist/cache/rekeyed.d.ts +46 -0
- package/dist/cache/rekeyed.js +89 -0
- package/dist/cache/relocate.d.ts +14 -14
- package/dist/cache/relocate.js +33 -25
- package/dist/cache/route.d.ts +32 -33
- package/dist/cache/route.js +94 -50
- package/dist/cache/store.d.ts +6 -0
- package/dist/cache/store.js +21 -1
- package/dist/cache/trace.d.ts +53 -14
- package/dist/cache/trace.js +76 -30
- package/dist/cli/bin.js +2 -2
- package/dist/cli/index.js +43 -32
- package/dist/cli/init/engines.d.ts +1 -3
- package/dist/cli/init/engines.js +2 -2
- package/dist/cli/init/gateways.d.ts +1 -1
- package/dist/cli/init/gateways.js +11 -1
- package/dist/cli/init/mcp-config.d.ts +2 -1
- package/dist/cli/init/mcp-config.js +8 -3
- package/dist/cli/init/sibling-versions.json +1 -1
- package/dist/cli/init/versions.d.ts +0 -8
- package/dist/cli/init/versions.js +1 -10
- package/dist/cli/mcp.d.ts +3 -2
- package/dist/cli/mcp.js +52 -16
- package/dist/cli/run-output.d.ts +17 -0
- package/dist/cli/run-output.js +45 -0
- package/dist/collect/collect.js +8 -4
- package/dist/collect/registry.js +27 -16
- package/dist/config/compiled-files.d.ts +36 -0
- package/dist/config/compiled-files.js +50 -0
- package/dist/config/diagnose.js +21 -19
- package/dist/config/esm-hooks.d.ts +39 -44
- package/dist/config/esm-hooks.js +238 -63
- package/dist/config/load.d.ts +1 -7
- package/dist/config/load.js +23 -66
- package/dist/config/register.d.ts +8 -0
- package/dist/config/register.js +9 -0
- package/dist/config/resolve.d.ts +1 -0
- package/dist/config/resolve.js +33 -1
- package/dist/config/specifiers.d.ts +19 -0
- package/dist/config/specifiers.js +33 -0
- package/dist/config/tsconfig.d.ts +56 -0
- package/dist/config/tsconfig.js +115 -0
- package/dist/config/typescript.d.ts +19 -0
- package/dist/config/typescript.js +253 -0
- package/dist/engine/contract.d.ts +1 -0
- package/dist/engine/index.d.ts +4 -2
- package/dist/engine/session.js +2 -2
- package/dist/expect/async.js +4 -2
- package/dist/expect/poll-scope.d.ts +52 -0
- package/dist/expect/poll-scope.js +113 -0
- package/dist/expect/poll.js +33 -6
- package/dist/expect/values.js +11 -4
- package/dist/internal/abandoned.d.ts +11 -0
- package/dist/internal/abandoned.js +23 -0
- package/dist/internal/brands.d.ts +2 -0
- package/dist/internal/brands.js +2 -0
- package/dist/internal/client-identity.d.ts +9 -0
- package/dist/internal/client-identity.js +14 -0
- package/dist/internal/errors.d.ts +10 -3
- package/dist/internal/errors.js +23 -8
- package/dist/internal/node-version.d.ts +16 -7
- package/dist/internal/node-version.js +50 -15
- package/dist/internal/package-manager.js +1 -1
- package/dist/internal/realm-slot.d.ts +5 -4
- package/dist/internal/redact.d.ts +9 -4
- package/dist/internal/redact.js +120 -23
- package/dist/internal/time.d.ts +26 -4
- package/dist/internal/time.js +58 -14
- package/dist/internal/urls.d.ts +9 -5
- package/dist/internal/urls.js +19 -10
- package/dist/locator/engine.d.ts +12 -2
- package/dist/locator/engine.js +55 -14
- package/dist/locator/screen.js +59 -9
- package/dist/mcp/catalog.d.ts +3 -2
- package/dist/mcp/catalog.js +6 -6
- package/dist/mcp/output.d.ts +59 -0
- package/dist/mcp/output.js +129 -0
- package/dist/mcp/server.d.ts +4 -0
- package/dist/mcp/server.js +9 -12
- package/dist/mcp/session.d.ts +8 -1
- package/dist/mcp/session.js +56 -15
- package/dist/mcp/tools.js +7 -5
- package/dist/oauth/chatgpt.js +3 -23
- package/dist/oauth/cli.js +1 -0
- package/dist/oauth/copilot.d.ts +5 -4
- package/dist/oauth/copilot.js +80 -7
- package/dist/oauth/grok.js +2 -1
- package/dist/oauth/models.js +2 -1
- package/dist/oauth/opencode-console.d.ts +10 -0
- package/dist/oauth/opencode-console.js +133 -0
- package/dist/oauth/providers/github-copilot.d.ts +12 -0
- package/dist/oauth/providers/github-copilot.js +77 -23
- package/dist/oauth/providers/opencode-console.d.ts +31 -0
- package/dist/oauth/providers/opencode-console.js +148 -0
- package/dist/oauth/providers.d.ts +2 -6
- package/dist/oauth/providers.js +3 -7
- package/dist/oauth/responses.d.ts +9 -0
- package/dist/oauth/responses.js +23 -0
- package/dist/report/build.d.ts +26 -0
- package/dist/report/build.js +16 -8
- package/dist/report/failure-text.d.ts +6 -1
- package/dist/report/failure-text.js +14 -4
- package/dist/report/format.d.ts +10 -5
- package/dist/report/format.js +12 -5
- package/dist/report/junit.js +11 -12
- package/dist/report/list.d.ts +12 -0
- package/dist/report/list.js +61 -24
- package/dist/report/markdown.d.ts +1 -1
- package/dist/report/markdown.js +37 -14
- package/dist/report/repeats.d.ts +6 -1
- package/dist/report/repeats.js +12 -3
- package/dist/run/artifacts.d.ts +22 -4
- package/dist/run/artifacts.js +71 -3
- package/dist/run/execute.d.ts +34 -5
- package/dist/run/execute.js +93 -44
- package/dist/run/extended-fixtures.js +6 -2
- package/dist/run/failure-evidence.js +10 -4
- package/dist/run/fixtures.js +6 -8
- package/dist/run/in-process.d.ts +2 -0
- package/dist/run/in-process.js +52 -10
- package/dist/run/last-run.d.ts +42 -8
- package/dist/run/last-run.js +121 -14
- package/dist/run/process-output.d.ts +73 -0
- package/dist/run/process-output.js +130 -0
- package/dist/run/provision.d.ts +2 -0
- package/dist/run/provision.js +1 -0
- package/dist/run/realm.d.ts +4 -1
- package/dist/run/realm.js +20 -6
- package/dist/run/records.d.ts +20 -4
- package/dist/run/records.js +26 -0
- package/dist/run/retry.d.ts +10 -1
- package/dist/run/retry.js +30 -12
- package/dist/run/runner.d.ts +9 -1
- package/dist/run/runner.js +101 -39
- package/dist/run/scheduler.js +255 -19
- package/dist/run/secrecy.d.ts +22 -19
- package/dist/run/secrecy.js +22 -18
- package/dist/run/serial.d.ts +75 -2
- package/dist/run/serial.js +243 -59
- package/dist/run/standalone.js +1 -1
- package/dist/run/steps.d.ts +0 -6
- package/dist/run/steps.js +3 -20
- package/dist/run/trace-redaction.d.ts +5 -6
- package/dist/run/trace-redaction.js +5 -6
- package/dist/run/worker/entry.js +40 -18
- package/dist/run/worker/handle.js +10 -5
- package/dist/run/worker/protocol.d.ts +85 -3
- package/dist/run/worker/session.d.ts +2 -0
- package/dist/run/worker/session.js +29 -0
- package/dist/telemetry/environment.d.ts +2 -1
- package/dist/telemetry/environment.js +12 -2
- package/dist/telemetry/events.js +1 -0
- package/dist/telemetry/project.d.ts +5 -6
- package/dist/telemetry/project.js +6 -11
- package/dist/telemetry/store.d.ts +5 -5
- package/dist/telemetry/store.js +5 -7
- package/dist/telemetry/telemetry.d.ts +2 -3
- package/dist/telemetry/telemetry.js +2 -4
- package/dist/types.d.ts +15 -6
- package/docs/agent-steps.mdx +1 -0
- package/docs/agents.mdx +7 -3
- package/docs/api-testing.mdx +2 -1
- package/docs/authentication.mdx +6 -3
- package/docs/browser.mdx +49 -12
- package/docs/bug-bash.mdx +2 -1
- package/docs/cache.mdx +66 -19
- package/docs/ci.mdx +14 -9
- package/docs/coding-agents.mdx +2 -1
- package/docs/core-concepts.mdx +208 -0
- package/docs/debugging.mdx +25 -12
- package/docs/decision-models.mdx +142 -0
- package/docs/example-projects.mdx +32 -0
- package/docs/executors.mdx +7 -2
- package/docs/explore.mdx +1 -0
- package/docs/github.mdx +14 -5
- package/docs/index.mdx +9 -5
- package/docs/integrations/eas.mdx +1 -0
- package/docs/integrations/index.mdx +1 -0
- package/docs/integrations/kernel.mdx +3 -2
- package/docs/migrate/cypress.mdx +26 -3
- package/docs/migrate/detox.mdx +24 -1
- package/docs/migrate/maestro.mdx +28 -4
- package/docs/migrate/playwright.mdx +44 -18
- package/docs/migrate/selenium.mdx +27 -3
- package/docs/mobile-ci.mdx +6 -5
- package/docs/mobile.mdx +96 -8
- package/docs/models.mdx +60 -9
- package/docs/quickstart.mdx +28 -16
- package/docs/reference/agent.mdx +13 -3
- package/docs/reference/app.mdx +2 -1
- package/docs/reference/cli.mdx +41 -18
- package/docs/reference/config.mdx +67 -13
- package/docs/reference/engine.mdx +25 -13
- package/docs/reference/environment.mdx +10 -2
- package/docs/reference/errors.mdx +13 -10
- package/docs/reference/expect.mdx +36 -13
- package/docs/reference/mcp.mdx +21 -8
- package/docs/reference/mobile.mdx +24 -13
- package/docs/reference/reporters.mdx +9 -2
- package/docs/reference/screen.mdx +49 -16
- package/docs/reference/test.mdx +32 -5
- package/docs/reference/web.mdx +133 -23
- package/docs/security.mdx +58 -42
- package/docs/subscriptions.mdx +92 -12
- package/docs/telemetry.mdx +9 -6
- package/docs/tools.mdx +1 -0
- package/docs/{starting-your-app.mdx → web.mdx} +157 -11
- package/docs/writing-an-engine.mdx +2 -1
- package/package.json +25 -8
- package/schema/report-v1.schema.json +14 -1
- package/skills/e2e/SKILL.md +5 -3
- package/skills/e2e/references/agent.md +33 -11
- package/skills/e2e/references/debugging.md +4 -4
- package/skills/e2e/references/mcp.md +8 -3
- package/skills/e2e/references/running.md +15 -9
- package/skills/e2e/references/setup.md +45 -16
- package/skills/e2e/references/writing-tests.md +35 -18
- package/docs/viewports.mdx +0 -146
- package/docs/writing-tests.mdx +0 -256
package/dist/cache/relocate.js
CHANGED
|
@@ -21,7 +21,7 @@ import { containerKey, describeTarget, parentsOf } from '../agent/actions.js';
|
|
|
21
21
|
* change, because an entry recorded under different rules could relocate to a
|
|
22
22
|
* different node.
|
|
23
23
|
*/
|
|
24
|
-
export const REPLAY_POLICY_VERSION = 'conservative/
|
|
24
|
+
export const REPLAY_POLICY_VERSION = 'conservative/6';
|
|
25
25
|
/**
|
|
26
26
|
* The share of the viewport a scrolled node must have covered when it was
|
|
27
27
|
* addressed (`ScrollAction.spans`) to scroll as the viewport does once it
|
|
@@ -33,18 +33,13 @@ export const MAIN_LIST_SHARE = 0.5;
|
|
|
33
33
|
const IDENTITY_FIELDS = ['role', 'name', 'testId', 'placeholder', 'inputPurpose'];
|
|
34
34
|
/** Text is identity too when neither a test id nor a name was recorded. */
|
|
35
35
|
const IDENTITY_FIELDS_WITH_TEXT = [...IDENTITY_FIELDS, 'text'];
|
|
36
|
-
/**
|
|
37
|
-
* Whether a descriptor can identify a node at all. Role or selector alone
|
|
38
|
-
* cannot: a wrong match acts on the wrong control, so such a descriptor is
|
|
39
|
-
* not relocatable and, as an anchor, would prove nothing.
|
|
40
|
-
*/
|
|
41
36
|
/**
|
|
42
37
|
* A descriptor with nothing to identify the control by: no test id, name,
|
|
43
|
-
* text, or placeholder, only a role.
|
|
44
|
-
* these. Such a descriptor relocates by its place
|
|
45
|
-
* controls of its kind,
|
|
46
|
-
*
|
|
47
|
-
*
|
|
38
|
+
* text, or placeholder, only a role. An icon button and a form built
|
|
39
|
+
* without labels are made of these. Such a descriptor relocates by its place
|
|
40
|
+
* among the unnamed controls of its kind, and it is matched strictly
|
|
41
|
+
* (`fieldsIdentical`): an unnamed textbox must never stand in for a named
|
|
42
|
+
* one.
|
|
48
43
|
*/
|
|
49
44
|
function isAnonymous(descriptor) {
|
|
50
45
|
return (descriptor.testId === undefined &&
|
|
@@ -52,8 +47,21 @@ function isAnonymous(descriptor) {
|
|
|
52
47
|
descriptor.text === undefined &&
|
|
53
48
|
descriptor.placeholder === undefined);
|
|
54
49
|
}
|
|
50
|
+
/**
|
|
51
|
+
* Whether a descriptor can identify a node at all. Role or selector alone
|
|
52
|
+
* cannot: a wrong match acts on the wrong control, so such a descriptor is
|
|
53
|
+
* not relocatable and, as an anchor, would prove nothing. An anonymous one
|
|
54
|
+
* is relocatable by its recorded place only when that place means something
|
|
55
|
+
* on the next screen: it was the one unnamed control of its role, or it sits
|
|
56
|
+
* in a named container (`within`) that tells it from its twins. A place
|
|
57
|
+
* counted among twins with no container is an order, and rows that reorder
|
|
58
|
+
* would hand the action to the wrong one, so such a target hands off.
|
|
59
|
+
*/
|
|
55
60
|
export function isRelocatableDescriptor(descriptor) {
|
|
56
|
-
|
|
61
|
+
if (!isAnonymous(descriptor))
|
|
62
|
+
return true;
|
|
63
|
+
const { position } = descriptor;
|
|
64
|
+
return descriptor.role !== undefined && position !== undefined && (position.of === 1 || descriptor.within !== undefined);
|
|
57
65
|
}
|
|
58
66
|
/** The semantic tier of a descriptor: every identity field but the test id. */
|
|
59
67
|
function withoutTestId(descriptor) {
|
|
@@ -102,17 +110,17 @@ function fieldsIdentical(recorded, candidate, fields) {
|
|
|
102
110
|
*/
|
|
103
111
|
const projections = new WeakMap();
|
|
104
112
|
/** Projects every node of an observation the way the recorder described its targets. */
|
|
105
|
-
|
|
113
|
+
function describeNodes(nodes) {
|
|
106
114
|
const cached = projections.get(nodes);
|
|
107
|
-
if (cached !== undefined
|
|
108
|
-
return cached
|
|
115
|
+
if (cached !== undefined)
|
|
116
|
+
return cached;
|
|
109
117
|
const described = [];
|
|
110
118
|
for (const [id, node] of nodes) {
|
|
111
|
-
const descriptor = describeTarget(node
|
|
119
|
+
const descriptor = describeTarget(node);
|
|
112
120
|
if (descriptor !== undefined)
|
|
113
121
|
described.push({ id, descriptor });
|
|
114
122
|
}
|
|
115
|
-
projections.set(nodes,
|
|
123
|
+
projections.set(nodes, described);
|
|
116
124
|
return described;
|
|
117
125
|
}
|
|
118
126
|
/**
|
|
@@ -124,8 +132,8 @@ export function describeNodes(nodes, options) {
|
|
|
124
132
|
* same twins and noted which one it acted on, so the same count of twins
|
|
125
133
|
* resolves to the same one; any other count diverges as before.
|
|
126
134
|
*/
|
|
127
|
-
export function relocateDescriptor(descriptor, nodes
|
|
128
|
-
const matches = matchingIds(descriptor, nodes
|
|
135
|
+
export function relocateDescriptor(descriptor, nodes) {
|
|
136
|
+
const matches = matchingIds(descriptor, nodes);
|
|
129
137
|
if (matches.length === 0)
|
|
130
138
|
return { kind: 'failed', failure: 'target-not-found' };
|
|
131
139
|
const { position } = descriptor;
|
|
@@ -146,8 +154,8 @@ export function relocateDescriptor(descriptor, nodes, options) {
|
|
|
146
154
|
* matches. The recorder uses the same projection to notice, before it writes a
|
|
147
155
|
* target, that the description alone would not tell the target from its twins.
|
|
148
156
|
*/
|
|
149
|
-
function matchingIds(descriptor, nodes
|
|
150
|
-
const candidates = describeNodes(nodes
|
|
157
|
+
function matchingIds(descriptor, nodes) {
|
|
158
|
+
const candidates = describeNodes(nodes);
|
|
151
159
|
// A recorded container key must hold: the same "Delete" in another row is
|
|
152
160
|
// a different control. Checked against the tree the candidates came from,
|
|
153
161
|
// never guessed.
|
|
@@ -155,7 +163,7 @@ function matchingIds(descriptor, nodes, options) {
|
|
|
155
163
|
? candidates
|
|
156
164
|
: (() => {
|
|
157
165
|
const parents = parentsOf(nodes);
|
|
158
|
-
return candidates.filter((candidate) => containerKey(candidate.id, nodes, parents
|
|
166
|
+
return candidates.filter((candidate) => containerKey(candidate.id, nodes, parents) === descriptor.within);
|
|
159
167
|
})();
|
|
160
168
|
for (const tier of descriptorTiers(descriptor)) {
|
|
161
169
|
const matches = tierMatches(tier, keyed);
|
|
@@ -183,8 +191,8 @@ const MAX_POSITIONED_TWINS = 1000;
|
|
|
183
191
|
* alone. Recorded with the target so a replay can tell one "Set up" button per
|
|
184
192
|
* card apart without an app change; a distinct label makes it unnecessary.
|
|
185
193
|
*/
|
|
186
|
-
export function describePosition(node, within, nodes
|
|
187
|
-
const described = describeTarget(node
|
|
194
|
+
export function describePosition(node, within, nodes) {
|
|
195
|
+
const described = describeTarget(node);
|
|
188
196
|
if (described === undefined || (described.role === undefined && isAnonymous(described)))
|
|
189
197
|
return undefined;
|
|
190
198
|
// An anonymous target has no identity to match alone; its position is what
|
|
@@ -192,7 +200,7 @@ export function describePosition(node, within, nodes, options) {
|
|
|
192
200
|
// it is the only one, and described with a placeholder position to do so.
|
|
193
201
|
const anonymous = isAnonymous(described);
|
|
194
202
|
const probe = { ...described, ...(within === undefined ? {} : { within }), ...(anonymous ? { position: { index: 0, of: 1 } } : {}) };
|
|
195
|
-
const ids = matchingIds(probe, nodes
|
|
203
|
+
const ids = matchingIds(probe, nodes);
|
|
196
204
|
if (ids.length < (anonymous ? 1 : 2) || ids.length > MAX_POSITIONED_TWINS)
|
|
197
205
|
return undefined;
|
|
198
206
|
const index = ids.indexOf(node.ref.id);
|
package/dist/cache/route.d.ts
CHANGED
|
@@ -4,43 +4,42 @@
|
|
|
4
4
|
* The replay precondition asks "is the app on the screen this recording
|
|
5
5
|
* began on", and the postcondition "did we land where the recording landed".
|
|
6
6
|
* A URL answers neither directly: it carries the id the app minted for this
|
|
7
|
-
* run's record,
|
|
8
|
-
*
|
|
9
|
-
*
|
|
7
|
+
* run's record, a cache buster, a session token, a fragment. All of that
|
|
8
|
+
* changes between two visits to the same screen, and none of it is the
|
|
9
|
+
* screen.
|
|
10
10
|
*
|
|
11
|
-
* So a location is reduced to its route: the
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
11
|
+
* So a location is reduced to its route: the origin it is on, the pathname's
|
|
12
|
+
* segments, and the query's parameters, with every segment or parameter
|
|
13
|
+
* value that looks minted per record (a uuid, a hex or digit run, a long
|
|
14
|
+
* token mixing letters and digits, a prefixed record id, text with
|
|
15
|
+
* whitespace) replaced by `:id`.
|
|
16
|
+
* Every other query value is part of the screen: `?mode=unsafe` is not
|
|
17
|
+
* `?mode=safe`, and `?tab=notes` shows other controls than no tab. The
|
|
18
|
+
* fragment is dropped, except that an app routing in the fragment
|
|
19
|
+
* (`#/companies/1`) routes by that path and its query. A value the call
|
|
20
|
+
* marked with `unique()` needs no rule here: the recording spells it as a
|
|
17
21
|
* placeholder in every spelling a URL gives it, its slug included
|
|
18
22
|
* (`cache/template.ts`), and replay fills in this run's value on both sides.
|
|
19
23
|
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
24
|
+
* The origin is relative to the app (`appLocation`): a location on the app's
|
|
25
|
+
* own origin is kept as its path, so a recording made against one deployment
|
|
26
|
+
* of the app replays on another that shares its `app.identity`, while a
|
|
27
|
+
* screen on any other origin keeps its origin and is another route.
|
|
28
|
+
*
|
|
29
|
+
* Two routes are the same screen only when they read alike. A literal that
|
|
30
|
+
* differs, a slug the runner never saw the value of, is another screen at
|
|
31
|
+
* both ends of a step: nothing recorded can tell a record's slug from a
|
|
32
|
+
* route word, and a screen that looks alike on another route (a settings
|
|
33
|
+
* page and a profile page sharing one layout) is exactly where a wrong link
|
|
34
|
+
* lands. A device engine reports a screen title in place of a URL, which is
|
|
35
|
+
* already a route and compares as itself.
|
|
28
36
|
*/
|
|
29
|
-
export type Route = {
|
|
30
|
-
readonly kind: 'url';
|
|
31
|
-
readonly segments: readonly string[];
|
|
32
|
-
} | {
|
|
33
|
-
readonly kind: 'opaque';
|
|
34
|
-
readonly location: string;
|
|
35
|
-
};
|
|
36
|
-
export type RouteVerdict = 'same' | 'different' | 'undecided';
|
|
37
|
-
/** The route of a location. */
|
|
38
|
-
export declare function routeOf(location: string): Route;
|
|
39
|
-
/** A route as one line, for tests and diagnostics: `/companies/:id`, or the opaque location. */
|
|
40
|
-
export declare function routeKey(route: Route): string;
|
|
41
37
|
/**
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
38
|
+
* A location as the cache compares it: a URL on the app's own origin as its
|
|
39
|
+
* path, query, and fragment, so a recording follows the app to another
|
|
40
|
+
* deployment of it; anything else (another origin, a device screen title)
|
|
41
|
+
* as it is.
|
|
45
42
|
*/
|
|
46
|
-
export declare function
|
|
43
|
+
export declare function appLocation(location: string, appOrigin: string | undefined): string;
|
|
44
|
+
/** Whether two locations name the same screen: one origin, and segments and query that read alike. */
|
|
45
|
+
export declare function sameRoute(recorded: string, live: string): boolean;
|
package/dist/cache/route.js
CHANGED
|
@@ -4,43 +4,66 @@
|
|
|
4
4
|
* The replay precondition asks "is the app on the screen this recording
|
|
5
5
|
* began on", and the postcondition "did we land where the recording landed".
|
|
6
6
|
* A URL answers neither directly: it carries the id the app minted for this
|
|
7
|
-
* run's record,
|
|
8
|
-
*
|
|
9
|
-
*
|
|
7
|
+
* run's record, a cache buster, a session token, a fragment. All of that
|
|
8
|
+
* changes between two visits to the same screen, and none of it is the
|
|
9
|
+
* screen.
|
|
10
10
|
*
|
|
11
|
-
* So a location is reduced to its route: the
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
11
|
+
* So a location is reduced to its route: the origin it is on, the pathname's
|
|
12
|
+
* segments, and the query's parameters, with every segment or parameter
|
|
13
|
+
* value that looks minted per record (a uuid, a hex or digit run, a long
|
|
14
|
+
* token mixing letters and digits, a prefixed record id, text with
|
|
15
|
+
* whitespace) replaced by `:id`.
|
|
16
|
+
* Every other query value is part of the screen: `?mode=unsafe` is not
|
|
17
|
+
* `?mode=safe`, and `?tab=notes` shows other controls than no tab. The
|
|
18
|
+
* fragment is dropped, except that an app routing in the fragment
|
|
19
|
+
* (`#/companies/1`) routes by that path and its query. A value the call
|
|
20
|
+
* marked with `unique()` needs no rule here: the recording spells it as a
|
|
17
21
|
* placeholder in every spelling a URL gives it, its slug included
|
|
18
22
|
* (`cache/template.ts`), and replay fills in this run's value on both sides.
|
|
19
23
|
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
24
|
+
* The origin is relative to the app (`appLocation`): a location on the app's
|
|
25
|
+
* own origin is kept as its path, so a recording made against one deployment
|
|
26
|
+
* of the app replays on another that shares its `app.identity`, while a
|
|
27
|
+
* screen on any other origin keeps its origin and is another route.
|
|
28
|
+
*
|
|
29
|
+
* Two routes are the same screen only when they read alike. A literal that
|
|
30
|
+
* differs, a slug the runner never saw the value of, is another screen at
|
|
31
|
+
* both ends of a step: nothing recorded can tell a record's slug from a
|
|
32
|
+
* route word, and a screen that looks alike on another route (a settings
|
|
33
|
+
* page and a profile page sharing one layout) is exactly where a wrong link
|
|
34
|
+
* lands. A device engine reports a screen title in place of a URL, which is
|
|
35
|
+
* already a route and compares as itself.
|
|
28
36
|
*/
|
|
29
37
|
const PARAM = ':id';
|
|
30
38
|
/**
|
|
31
39
|
* A segment shaped like a value the app mints per record: a uuid, a hex or
|
|
32
40
|
* digit run, a long token carrying letters and at least two digits (a route
|
|
33
|
-
* word such as `companies-v2` has one),
|
|
34
|
-
*
|
|
35
|
-
*
|
|
41
|
+
* word such as `companies-v2` has one), a prefixed record id (letters, a
|
|
42
|
+
* dash, then a digit tail of two or more: `PROJ-016`, `INV-2041`; a single
|
|
43
|
+
* digit as in `page-2` is a route word), or text with whitespace, which no
|
|
44
|
+
* router puts in a path. Any other capitalized or dashed word is none of
|
|
45
|
+
* these.
|
|
36
46
|
*/
|
|
37
47
|
const MINTED_SEGMENT = [
|
|
38
48
|
/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i,
|
|
39
49
|
/^[0-9a-f]{8,}$/i,
|
|
40
50
|
/^\d+$/,
|
|
41
51
|
/^(?=(?:[^\d]*\d){2})(?=.*[A-Za-z])[A-Za-z0-9_-]{12,}$/,
|
|
52
|
+
/^[A-Za-z]+-\d{2,}$/,
|
|
42
53
|
/\s/,
|
|
43
54
|
];
|
|
55
|
+
/**
|
|
56
|
+
* A query value is minted on the same rules, with the wider alphabet a
|
|
57
|
+
* value carries that a path segment cannot: a signed token or a timestamp
|
|
58
|
+
* (`eyJhbGciOi.x1.y2`, `2026-10-01T10:00:00Z`) has dots, colons, and base64,
|
|
59
|
+
* and a date (`?from=2026-10-01`, a default an app fills in with today) has
|
|
60
|
+
* no letter at all.
|
|
61
|
+
*/
|
|
62
|
+
const MINTED_VALUE = [
|
|
63
|
+
...MINTED_SEGMENT,
|
|
64
|
+
/^(?=(?:[^\d]*\d){2})(?=.*[A-Za-z])[\w.~+/=:-]{12,}$/,
|
|
65
|
+
/^\d{4}-\d{2}-\d{2}(?:[T ]\d{2}:\d{2}(?::\d{2}(?:\.\d+)?)?(?:Z|[+-]\d{2}:?\d{2})?)?$/,
|
|
66
|
+
];
|
|
44
67
|
function decodeSegment(raw) {
|
|
45
68
|
try {
|
|
46
69
|
return decodeURIComponent(raw);
|
|
@@ -50,15 +73,30 @@ function decodeSegment(raw) {
|
|
|
50
73
|
}
|
|
51
74
|
}
|
|
52
75
|
/**
|
|
53
|
-
*
|
|
54
|
-
*
|
|
76
|
+
* A location as the cache compares it: a URL on the app's own origin as its
|
|
77
|
+
* path, query, and fragment, so a recording follows the app to another
|
|
78
|
+
* deployment of it; anything else (another origin, a device screen title)
|
|
79
|
+
* as it is.
|
|
80
|
+
*/
|
|
81
|
+
export function appLocation(location, appOrigin) {
|
|
82
|
+
if (appOrigin === undefined || !URL.canParse(location))
|
|
83
|
+
return location;
|
|
84
|
+
const url = new URL(location);
|
|
85
|
+
return url.origin === appOrigin ? `${url.pathname}${url.search}${url.hash}` : location;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* The parts of a location its route is made of, or undefined when the
|
|
89
|
+
* location is not a web address (a device engine's screen title, a
|
|
90
|
+
* browser's own page).
|
|
55
91
|
*/
|
|
56
|
-
function
|
|
92
|
+
function locationParts(location) {
|
|
93
|
+
let origin;
|
|
57
94
|
let rest = location;
|
|
58
95
|
if (URL.canParse(location)) {
|
|
59
96
|
const url = new URL(location);
|
|
60
97
|
if (url.protocol !== 'http:' && url.protocol !== 'https:')
|
|
61
98
|
return undefined;
|
|
99
|
+
origin = url.origin;
|
|
62
100
|
rest = `${url.pathname}${url.search}${url.hash}`;
|
|
63
101
|
}
|
|
64
102
|
else if (!location.startsWith('/')) {
|
|
@@ -66,42 +104,48 @@ function pathnameOf(location) {
|
|
|
66
104
|
}
|
|
67
105
|
const hashAt = rest.indexOf('#');
|
|
68
106
|
const fragment = hashAt === -1 ? '' : rest.slice(hashAt + 1);
|
|
69
|
-
const
|
|
70
|
-
|
|
107
|
+
const [documentPath, documentSearch] = splitOnce(hashAt === -1 ? rest : rest.slice(0, hashAt), '?');
|
|
108
|
+
if (!fragment.startsWith('/'))
|
|
109
|
+
return { ...(origin === undefined ? {} : { origin }), pathname: documentPath, search: documentSearch };
|
|
110
|
+
const [fragmentPath, fragmentSearch] = splitOnce(fragment, '?');
|
|
111
|
+
return {
|
|
112
|
+
...(origin === undefined ? {} : { origin }),
|
|
113
|
+
pathname: fragmentPath,
|
|
114
|
+
search: [documentSearch, fragmentSearch].filter((part) => part !== '').join('&'),
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
function splitOnce(text, separator) {
|
|
118
|
+
const at = text.indexOf(separator);
|
|
119
|
+
return at === -1 ? [text, ''] : [text.slice(0, at), text.slice(at + 1)];
|
|
120
|
+
}
|
|
121
|
+
/** A query's parameters as route terms, `key=value` with a minted key or value as `:id`, sorted. */
|
|
122
|
+
function queryTerms(search) {
|
|
123
|
+
const terms = [];
|
|
124
|
+
for (const [key, value] of new URLSearchParams(search)) {
|
|
125
|
+
const minted = (text) => text !== '' && MINTED_VALUE.some((pattern) => pattern.test(text));
|
|
126
|
+
terms.push(`${minted(key) ? PARAM : key}=${minted(value) ? PARAM : value}`);
|
|
127
|
+
}
|
|
128
|
+
return terms.toSorted();
|
|
71
129
|
}
|
|
72
130
|
/** The route of a location. */
|
|
73
|
-
|
|
74
|
-
const
|
|
75
|
-
if (
|
|
131
|
+
function routeOf(location) {
|
|
132
|
+
const parts = locationParts(location);
|
|
133
|
+
if (parts === undefined)
|
|
76
134
|
return { kind: 'opaque', location };
|
|
77
|
-
const segments = pathname
|
|
135
|
+
const segments = parts.pathname
|
|
78
136
|
.split('/')
|
|
79
137
|
.filter((raw) => raw !== '')
|
|
80
138
|
.map((raw) => {
|
|
81
139
|
const text = decodeSegment(raw);
|
|
82
140
|
return MINTED_SEGMENT.some((pattern) => pattern.test(text)) ? PARAM : text;
|
|
83
141
|
});
|
|
84
|
-
return { kind: 'url', segments };
|
|
142
|
+
return { kind: 'url', ...(parts.origin === undefined ? {} : { origin: parts.origin }), segments, query: queryTerms(parts.search) };
|
|
85
143
|
}
|
|
86
|
-
/**
|
|
87
|
-
export function
|
|
88
|
-
return
|
|
144
|
+
/** Whether two locations name the same screen: one origin, and segments and query that read alike. */
|
|
145
|
+
export function sameRoute(recorded, live) {
|
|
146
|
+
return routeIdentity(routeOf(recorded)) === routeIdentity(routeOf(live));
|
|
89
147
|
}
|
|
90
|
-
/**
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
* the screen must settle, and `different` otherwise.
|
|
94
|
-
*/
|
|
95
|
-
export function compareRoutes(recorded, live) {
|
|
96
|
-
if (recorded.kind === 'opaque' || live.kind === 'opaque') {
|
|
97
|
-
return recorded.kind === 'opaque' && live.kind === 'opaque' && recorded.location === live.location ? 'same' : 'different';
|
|
98
|
-
}
|
|
99
|
-
if (recorded.segments.length !== live.segments.length)
|
|
100
|
-
return 'different';
|
|
101
|
-
let differing = 0;
|
|
102
|
-
for (const [index, segment] of recorded.segments.entries()) {
|
|
103
|
-
if (segment !== live.segments[index])
|
|
104
|
-
differing += 1;
|
|
105
|
-
}
|
|
106
|
-
return differing === 0 ? 'same' : differing === 1 ? 'undecided' : 'different';
|
|
148
|
+
/** A route's parts as one comparable string, a slash inside a segment kept inside it. */
|
|
149
|
+
function routeIdentity(route) {
|
|
150
|
+
return JSON.stringify(route.kind === 'opaque' ? [route.location] : [route.origin ?? null, route.segments, route.query]);
|
|
107
151
|
}
|
package/dist/cache/store.d.ts
CHANGED
|
@@ -59,6 +59,12 @@ export declare class FileCacheStore implements CacheStore {
|
|
|
59
59
|
bytes: number;
|
|
60
60
|
} | undefined>;
|
|
61
61
|
delete(keyHash: string): Promise<void>;
|
|
62
|
+
/**
|
|
63
|
+
* The key hashes of the digest-named files in the directory, readable or
|
|
64
|
+
* not; none when the directory does not exist. Not part of `CacheStore`:
|
|
65
|
+
* only `cache.strict` lists a store, and only this one.
|
|
66
|
+
*/
|
|
67
|
+
keyHashes(): Promise<string[]>;
|
|
62
68
|
/**
|
|
63
69
|
* Resolves one entry path. The key hash is the only caller-supplied path
|
|
64
70
|
* component and must be a bare digest, so traversal is impossible by
|
package/dist/cache/store.js
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
* The interface exists so a remote store — the cloud's shared cache — can
|
|
14
14
|
* replace the filesystem without the harness noticing.
|
|
15
15
|
*/
|
|
16
|
-
import { mkdir, readFile, stat, unlink } from 'node:fs/promises';
|
|
16
|
+
import { mkdir, readdir, readFile, stat, unlink } from 'node:fs/promises';
|
|
17
17
|
import { join } from 'node:path';
|
|
18
18
|
import { writeFileAtomic } from '../internal/atomic-write.js';
|
|
19
19
|
import { errorMessage } from '../internal/errors.js';
|
|
@@ -83,6 +83,26 @@ export class FileCacheStore {
|
|
|
83
83
|
return;
|
|
84
84
|
await unlink(path).catch(() => undefined);
|
|
85
85
|
}
|
|
86
|
+
/**
|
|
87
|
+
* The key hashes of the digest-named files in the directory, readable or
|
|
88
|
+
* not; none when the directory does not exist. Not part of `CacheStore`:
|
|
89
|
+
* only `cache.strict` lists a store, and only this one.
|
|
90
|
+
*/
|
|
91
|
+
async keyHashes() {
|
|
92
|
+
let names;
|
|
93
|
+
try {
|
|
94
|
+
names = await readdir(this.directory);
|
|
95
|
+
}
|
|
96
|
+
catch (cause) {
|
|
97
|
+
if (cause.code === 'ENOENT')
|
|
98
|
+
return [];
|
|
99
|
+
throw cause;
|
|
100
|
+
}
|
|
101
|
+
return names.flatMap((name) => {
|
|
102
|
+
const keyHash = name.endsWith('.json') ? name.slice(0, -'.json'.length) : undefined;
|
|
103
|
+
return keyHash !== undefined && KEY_HASH.test(keyHash) ? [keyHash] : [];
|
|
104
|
+
});
|
|
105
|
+
}
|
|
86
106
|
/**
|
|
87
107
|
* Resolves one entry path. The key hash is the only caller-supplied path
|
|
88
108
|
* component and must be a bare digest, so traversal is impossible by
|
package/dist/cache/trace.d.ts
CHANGED
|
@@ -32,26 +32,36 @@ export declare const MAX_TRACE_INPUT_CHARS = 4096;
|
|
|
32
32
|
/** Caps prose at `maxChars`, marking the cut with an ellipsis. */
|
|
33
33
|
export declare function bound(text: string, maxChars: number): string;
|
|
34
34
|
/**
|
|
35
|
-
* Cap on recorded
|
|
36
|
-
* appeared on screen between the first observation and the
|
|
37
|
-
* a same-screen mutation rarely has more
|
|
38
|
-
* the whole screen keeps the first few
|
|
35
|
+
* Cap on recorded anchors, per side of the delta. Anchors are the step's own
|
|
36
|
+
* delta, what appeared on screen between the first observation and the
|
|
37
|
+
* passing one and what vanished, so a same-screen mutation rarely has more
|
|
38
|
+
* than a handful; a step that changes the whole screen keeps the first few,
|
|
39
|
+
* announcements first (`cache/anchors.ts`).
|
|
39
40
|
*/
|
|
40
41
|
export declare const MAX_TRACE_ANCHORS = 8;
|
|
41
42
|
/** Longest a replay waits for the recorded end state to return. */
|
|
42
43
|
export declare const MAX_TRACE_END_WAIT_MS = 120000;
|
|
43
44
|
/**
|
|
44
45
|
* What a trace was recorded for, in the terms a person uses: the test, the
|
|
45
|
-
* target,
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
46
|
+
* target, the digest of the instruction, and, on entries recorded since they
|
|
47
|
+
* were added, the rest of what names the step within its test. None of it is
|
|
48
|
+
* replay input — every one of these fields is already in the key, so a
|
|
49
|
+
* mismatch is a miss before an entry is ever read — and none of it is trusted
|
|
50
|
+
* as such. It exists so a file named after a digest can say which step it
|
|
51
|
+
* belongs to, and so `cache.strict` can tell a step whose key changed under
|
|
52
|
+
* its recording from a step that was never recorded.
|
|
49
53
|
*/
|
|
50
54
|
export interface TraceProvenance {
|
|
51
55
|
readonly testId: string;
|
|
52
56
|
readonly targetId: string;
|
|
53
57
|
/** SHA-256 of the normalized instruction (`cache/identity.ts`). */
|
|
54
58
|
readonly instructionDigest: string;
|
|
59
|
+
/** SHA-256/JCS of the step's params as the key digests them, a placeholder for each `unique()` value. */
|
|
60
|
+
readonly paramsDigest?: string;
|
|
61
|
+
/** Zero-based occurrence of the step among its repeats in the attempt. */
|
|
62
|
+
readonly callIndex?: number;
|
|
63
|
+
/** Name of the configured agent the step ran with. */
|
|
64
|
+
readonly agent?: string;
|
|
55
65
|
}
|
|
56
66
|
/**
|
|
57
67
|
* How a recorded action addressed its node, independent of observation ids.
|
|
@@ -82,7 +92,23 @@ export interface TraceTargetDescriptor {
|
|
|
82
92
|
* twin that appeared or vanished still diverges instead of guessing.
|
|
83
93
|
*/
|
|
84
94
|
readonly position?: TracePosition;
|
|
95
|
+
/**
|
|
96
|
+
* An anchor's states that were on, sorted; anchors only. A switch turned
|
|
97
|
+
* on is the same control before and after, so its state is what tells
|
|
98
|
+
* the step's effect from a tap that did nothing.
|
|
99
|
+
*/
|
|
100
|
+
readonly states?: readonly TraceAnchorState[];
|
|
101
|
+
/**
|
|
102
|
+
* An anchor's current input value, redacted and bounded like its text;
|
|
103
|
+
* anchors only, and never a secure field's, which is never observed. A
|
|
104
|
+
* field typed into or an option selected is the same control before and
|
|
105
|
+
* after, so its value is the step's effect.
|
|
106
|
+
*/
|
|
107
|
+
readonly value?: string;
|
|
85
108
|
}
|
|
109
|
+
/** The node states an anchor records: the ones a step sets on a control it leaves on screen. */
|
|
110
|
+
export declare const TRACE_ANCHOR_STATES: readonly ['checked', 'expanded', 'pressed', 'selected'];
|
|
111
|
+
export type TraceAnchorState = (typeof TRACE_ANCHOR_STATES)[number];
|
|
86
112
|
interface ActionBase {
|
|
87
113
|
/** One-line prose summary — the only view a mid-step hand-off notice shows. */
|
|
88
114
|
readonly summary: string;
|
|
@@ -245,7 +271,7 @@ export interface ToolGapAction extends ActionBase {
|
|
|
245
271
|
export type RecordedAction = NodeAction | TypeAction | TypeSecretAction | PressAction | SelectAction | CheckAction | UploadAction | DragAction | ScrollAction | ScrollUntilAction | NavigateAction | BackAction | PointAction | TypeTextAction | PressKeyAction | DismissKeyboardAction | ToolGapAction;
|
|
246
272
|
export interface ActionTrace {
|
|
247
273
|
readonly actions: readonly RecordedAction[];
|
|
248
|
-
/** Executor that produced the trace
|
|
274
|
+
/** Executor that produced the trace: provenance, never part of the key, which names the agent instead. */
|
|
249
275
|
readonly executor: {
|
|
250
276
|
readonly name: string;
|
|
251
277
|
readonly version?: string;
|
|
@@ -254,12 +280,17 @@ export interface ActionTrace {
|
|
|
254
280
|
readonly recordedFor?: TraceProvenance;
|
|
255
281
|
/** The recorded run's verdict summary. */
|
|
256
282
|
readonly summary: string;
|
|
257
|
-
/**
|
|
283
|
+
/**
|
|
284
|
+
* Location when the step began, as the cache compares it (`appLocation`):
|
|
285
|
+
* a path on the app's own origin, else the whole location. A precondition
|
|
286
|
+
* unless the trace opens with navigate.
|
|
287
|
+
*/
|
|
258
288
|
readonly startPath?: string;
|
|
259
289
|
/**
|
|
260
|
-
* Location
|
|
261
|
-
* A full replay self-finalizes only while
|
|
262
|
-
* a recorded flow whose destination changed
|
|
290
|
+
* Location when the step passed, spelled like `startPath`: the trace's
|
|
291
|
+
* deterministic postcondition. A full replay self-finalizes only while
|
|
292
|
+
* the live route still matches; a recorded flow whose destination changed
|
|
293
|
+
* hands off instead of passing.
|
|
263
294
|
*/
|
|
264
295
|
readonly endPath?: string;
|
|
265
296
|
/**
|
|
@@ -271,6 +302,14 @@ export interface ActionTrace {
|
|
|
271
302
|
* instead of passing on mechanics alone.
|
|
272
303
|
*/
|
|
273
304
|
readonly endAnchors?: readonly TraceTargetDescriptor[];
|
|
305
|
+
/**
|
|
306
|
+
* Descriptors of nodes that were on screen when the step began and were
|
|
307
|
+
* gone when it passed: a delete, a dismissed dialog, a cleared draft. A
|
|
308
|
+
* full replay self-finalizes only while every one of them is gone again,
|
|
309
|
+
* so a removal that did not happen hands off instead of passing with no
|
|
310
|
+
* anchor to check.
|
|
311
|
+
*/
|
|
312
|
+
readonly goneAnchors?: readonly TraceTargetDescriptor[];
|
|
274
313
|
/**
|
|
275
314
|
* How long the recorded run took from its first action to its passing
|
|
276
315
|
* verdict, plus a margin. A replay waits up to this long for the anchors to
|
|
@@ -295,7 +334,7 @@ export declare function buildTraceEntry(payload: ActionTrace): TraceEntry;
|
|
|
295
334
|
*/
|
|
296
335
|
export declare function readTraceEntry(document: unknown): TraceEntry | undefined;
|
|
297
336
|
/** Every field of a target descriptor; two descriptors equal on all of them name the same target. */
|
|
298
|
-
export declare const DESCRIPTOR_FIELDS: readonly ['role', 'name', 'text', 'testId', 'placeholder', 'selector', 'inputPurpose', 'within'];
|
|
337
|
+
export declare const DESCRIPTOR_FIELDS: readonly ['role', 'name', 'text', 'testId', 'placeholder', 'selector', 'inputPurpose', 'within', 'value'];
|
|
299
338
|
/** A string rewrite; `undefined` means the text cannot be rewritten and the whole trace is unusable. */
|
|
300
339
|
export type TraceTextMap = (text: string) => string | undefined;
|
|
301
340
|
/**
|