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/internal/redact.js
CHANGED
|
@@ -63,24 +63,31 @@ function compile(values) {
|
|
|
63
63
|
});
|
|
64
64
|
// `split` on a capturing pattern returns the markers at the odd indexes; those pass through untouched.
|
|
65
65
|
const redact = (text) => text.split(known).map((piece, index) => (index % 2 === 1 ? piece : rewrite(piece))).join('');
|
|
66
|
-
const valueKeys = entries.map(([, value]) =>
|
|
66
|
+
const valueKeys = entries.map(([, value]) => readingKey(value).key);
|
|
67
|
+
// A reader trims a value's leading whitespace as it trims any text's, so a cut part may begin past it.
|
|
68
|
+
const leadingForms = entries.map(([, value], index) => {
|
|
69
|
+
const key = valueKeys[index] ?? '';
|
|
70
|
+
return secretLength(value.trim()) >= MIN_SECRET_LENGTH && key.trimStart() !== key ? [key, key.trimStart()] : [key];
|
|
71
|
+
});
|
|
67
72
|
const redactCut = (text) => {
|
|
68
73
|
// Only what follows the last marker can end in a value cut short.
|
|
69
74
|
const tail = text.split(known).at(-1) ?? '';
|
|
70
|
-
const tailKey =
|
|
75
|
+
const tailKey = readingKey(tail);
|
|
71
76
|
let cut = 0;
|
|
72
77
|
let marker = '';
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
cut
|
|
77
|
-
|
|
78
|
+
leadingForms.forEach((forms, index) => {
|
|
79
|
+
for (const value of forms) {
|
|
80
|
+
const length = leadingPartAtEnd(tailKey.key, value, Math.min(FRAGMENT_LENGTH, Math.ceil(value.length / 2)));
|
|
81
|
+
if (length > cut) {
|
|
82
|
+
cut = length;
|
|
83
|
+
marker = markers[index] ?? '';
|
|
84
|
+
}
|
|
78
85
|
}
|
|
79
86
|
});
|
|
80
87
|
if (cut === 0)
|
|
81
88
|
return redact(text);
|
|
82
89
|
// An occurrence the whole-value pass would rewrite across the fragment's start joins the fragment.
|
|
83
|
-
let start =
|
|
90
|
+
let start = tailKey.at(tailKey.key.length - cut);
|
|
84
91
|
pattern.lastIndex = 0;
|
|
85
92
|
for (let match = pattern.exec(tail); match !== null && match.index < start; match = pattern.exec(tail)) {
|
|
86
93
|
if (match.index + match[0].length > start) {
|
|
@@ -221,6 +228,87 @@ function caseKey(text) {
|
|
|
221
228
|
out += ch.charCodeAt(0) < 128 ? ch.toLowerCase() : caseKeyOf(ch);
|
|
222
229
|
return out;
|
|
223
230
|
}
|
|
231
|
+
/** Whitespace a reader rewrites: a run of two or more, or one character other than a plain space. */
|
|
232
|
+
const COLLAPSIBLE = /\s\s|[^\S ]/u;
|
|
233
|
+
/** UTF-16 units joined into one string at a time when the key is rebuilt. */
|
|
234
|
+
const KEY_CHUNK = 8192;
|
|
235
|
+
/**
|
|
236
|
+
* `text` as a cut or a fragment of a value is compared: `caseKey`, with each
|
|
237
|
+
* whitespace run read as one space, as an engine that collapses whitespace
|
|
238
|
+
* (`normalizeText`) shows it before cutting. A value holding a line break, a
|
|
239
|
+
* tab, a CRLF, a no-break space, or a run of spaces then agrees with what a
|
|
240
|
+
* collapsed text keeps of it, and a stretch of the key maps back to the text
|
|
241
|
+
* through `at`. `caseKey` keeps every index, so text with nothing to collapse
|
|
242
|
+
* is its own key. Otherwise the key is rebuilt unit by unit into a typed
|
|
243
|
+
* array, and `at` keeps one pair of offsets per collapsed run: a trace text
|
|
244
|
+
* of many megabytes costs a few times its size, never an object per unit.
|
|
245
|
+
*/
|
|
246
|
+
function readingKey(text) {
|
|
247
|
+
const cased = caseKey(text);
|
|
248
|
+
if (!COLLAPSIBLE.test(cased))
|
|
249
|
+
return { key: cased, at: (index) => index };
|
|
250
|
+
const units = new Uint16Array(cased.length);
|
|
251
|
+
// Pairs of where a stretch after a collapsed run starts, in the key and in the text.
|
|
252
|
+
let breaks = new Int32Array(64);
|
|
253
|
+
let pairs = 0;
|
|
254
|
+
let length = 0;
|
|
255
|
+
for (let index = 0; index < cased.length; index += 1) {
|
|
256
|
+
const unit = cased.charCodeAt(index);
|
|
257
|
+
if (!isWhitespace(unit)) {
|
|
258
|
+
units[length++] = unit;
|
|
259
|
+
continue;
|
|
260
|
+
}
|
|
261
|
+
let end = index + 1;
|
|
262
|
+
while (end < cased.length && isWhitespace(cased.charCodeAt(end)))
|
|
263
|
+
end += 1;
|
|
264
|
+
units[length++] = 0x20;
|
|
265
|
+
if (end - index > 1 || unit !== 0x20) {
|
|
266
|
+
if (2 * pairs + 2 > breaks.length) {
|
|
267
|
+
const grown = new Int32Array(breaks.length * 2);
|
|
268
|
+
grown.set(breaks);
|
|
269
|
+
breaks = grown;
|
|
270
|
+
}
|
|
271
|
+
breaks[2 * pairs] = length;
|
|
272
|
+
breaks[2 * pairs + 1] = end;
|
|
273
|
+
pairs += 1;
|
|
274
|
+
}
|
|
275
|
+
index = end - 1;
|
|
276
|
+
}
|
|
277
|
+
const chunks = [];
|
|
278
|
+
for (let start = 0; start < length; start += KEY_CHUNK) {
|
|
279
|
+
chunks.push(String.fromCharCode(...units.subarray(start, Math.min(length, start + KEY_CHUNK))));
|
|
280
|
+
}
|
|
281
|
+
return {
|
|
282
|
+
key: chunks.join(''),
|
|
283
|
+
at: (index) => {
|
|
284
|
+
// The last stretch starting at or before `index`; before the first run the key and the text agree.
|
|
285
|
+
let low = -1;
|
|
286
|
+
let high = pairs - 1;
|
|
287
|
+
while (low < high) {
|
|
288
|
+
const middle = (low + high + 1) >> 1;
|
|
289
|
+
if ((breaks[2 * middle] ?? 0) <= index)
|
|
290
|
+
low = middle;
|
|
291
|
+
else
|
|
292
|
+
high = middle - 1;
|
|
293
|
+
}
|
|
294
|
+
return low === -1 ? index : (breaks[2 * low + 1] ?? 0) + index - (breaks[2 * low] ?? 0);
|
|
295
|
+
},
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
/** Whether a UTF-16 unit is one `\s` matches: what `normalizeText` collapses. */
|
|
299
|
+
function isWhitespace(unit) {
|
|
300
|
+
return (unit === 0x20 ||
|
|
301
|
+
(unit >= 0x09 && unit <= 0x0d) ||
|
|
302
|
+
unit === 0xa0 ||
|
|
303
|
+
unit === 0x1680 ||
|
|
304
|
+
(unit >= 0x2000 && unit <= 0x200a) ||
|
|
305
|
+
unit === 0x2028 ||
|
|
306
|
+
unit === 0x2029 ||
|
|
307
|
+
unit === 0x202f ||
|
|
308
|
+
unit === 0x205f ||
|
|
309
|
+
unit === 0x3000 ||
|
|
310
|
+
unit === 0xfeff);
|
|
311
|
+
}
|
|
224
312
|
/** One non-ASCII character's `caseKey`, the same length as `ch`. */
|
|
225
313
|
function caseKeyOf(ch) {
|
|
226
314
|
const upper = sameLength(ch.toUpperCase(), ch);
|
|
@@ -306,8 +394,9 @@ function leadingPartAtEnd(text, value, minimum) {
|
|
|
306
394
|
/**
|
|
307
395
|
* Every run of `FRAGMENT_LENGTH` characters of every value, mapped to the
|
|
308
396
|
* index of the first value holding it; values come longest first, so a run
|
|
309
|
-
* two values share names the longer one. The values come as `
|
|
310
|
-
* reads them, so a run in any case
|
|
397
|
+
* two values share names the longer one. The values come as `readingKey`
|
|
398
|
+
* reads them, so a run in any case or with its whitespace collapsed finds
|
|
399
|
+
* its owner.
|
|
311
400
|
*/
|
|
312
401
|
function fragmentOwners(values) {
|
|
313
402
|
const owners = new Map();
|
|
@@ -323,29 +412,30 @@ function fragmentOwners(values) {
|
|
|
323
412
|
/**
|
|
324
413
|
* `text` with every stretch whose windows of `FRAGMENT_LENGTH` characters
|
|
325
414
|
* each occur in a value replaced by the marker of the value owning its first
|
|
326
|
-
* window. Windows are looked up by `
|
|
327
|
-
* fragment in another case
|
|
328
|
-
*
|
|
329
|
-
*
|
|
415
|
+
* window. Windows are looked up by `readingKey`, as `owners` is keyed, so a
|
|
416
|
+
* fragment in another case or with another whitespace run is one too, and
|
|
417
|
+
* the stretch is cut back out of `text` whole, its whitespace runs included.
|
|
418
|
+
* The stretch grows one window at a time, so the scan is linear in the text
|
|
419
|
+
* and a run spanning two values becomes one marker.
|
|
330
420
|
*/
|
|
331
421
|
function rewriteFragments(text, owners, markers) {
|
|
332
422
|
if (owners.size === 0)
|
|
333
423
|
return text;
|
|
334
|
-
const key =
|
|
424
|
+
const { key, at } = readingKey(text);
|
|
335
425
|
let out = '';
|
|
336
426
|
let kept = 0;
|
|
337
427
|
let start = 0;
|
|
338
|
-
while (start + FRAGMENT_LENGTH <=
|
|
428
|
+
while (start + FRAGMENT_LENGTH <= key.length) {
|
|
339
429
|
const owner = owners.get(key.slice(start, start + FRAGMENT_LENGTH));
|
|
340
430
|
if (owner === undefined) {
|
|
341
431
|
start += 1;
|
|
342
432
|
continue;
|
|
343
433
|
}
|
|
344
434
|
let end = start + FRAGMENT_LENGTH;
|
|
345
|
-
while (end <
|
|
435
|
+
while (end < key.length && owners.has(key.slice(end + 1 - FRAGMENT_LENGTH, end + 1)))
|
|
346
436
|
end += 1;
|
|
347
|
-
out += `${text.slice(kept, start)}${markers[owner] ?? ''}`;
|
|
348
|
-
kept = end;
|
|
437
|
+
out += `${text.slice(kept, at(start))}${markers[owner] ?? ''}`;
|
|
438
|
+
kept = at(end);
|
|
349
439
|
start = end;
|
|
350
440
|
}
|
|
351
441
|
return out + text.slice(kept);
|
|
@@ -437,6 +527,10 @@ export class SecretLedger {
|
|
|
437
527
|
}
|
|
438
528
|
/** Bound so it can be handed out as a plain function. */
|
|
439
529
|
redact = (text) => this.compile().redact(text);
|
|
530
|
+
/** Whether no value is registered, so there is nothing to redact. */
|
|
531
|
+
get isEmpty() {
|
|
532
|
+
return this.values.length === 0;
|
|
533
|
+
}
|
|
440
534
|
/** Every registered name and value, in registration order. */
|
|
441
535
|
entries() {
|
|
442
536
|
return this.values.map(([name, value]) => [name, value]);
|
|
@@ -449,9 +543,12 @@ export class SecretLedger {
|
|
|
449
543
|
* `FRAGMENT_LENGTH` characters, or half of a value shorter than twice that.
|
|
450
544
|
* A cut is one position, so a boundary this short seldom matches plain text
|
|
451
545
|
* by chance. Matched as the value is written, in any case (`caseKey`), as
|
|
452
|
-
* a CSS `text-transform` shows it
|
|
453
|
-
*
|
|
454
|
-
*
|
|
546
|
+
* a CSS `text-transform` shows it, and with every whitespace run on both
|
|
547
|
+
* sides read as one space (`readingKey`) and the value's leading whitespace
|
|
548
|
+
* dropped where whole-value matching trims it (`valuePattern`), as an
|
|
549
|
+
* engine that collapses text before cutting it shows it: the cut falls on
|
|
550
|
+
* text as the engine read it, before any serializer spells it. A case
|
|
551
|
+
* mapping that changes length (`ß` to `SS`) is not followed. The part is found before whole values are
|
|
455
552
|
* rewritten, so a value that starts with another registered value is not
|
|
456
553
|
* half rewritten as the shorter one, and an occurrence running into the
|
|
457
554
|
* part joins its marker. Bound like `redact`.
|
|
@@ -463,7 +560,7 @@ export class SecretLedger {
|
|
|
463
560
|
* becomes its marker. For a recording that keeps what an engine read raw
|
|
464
561
|
* (a Playwright trace holds the page's cut text and selections), where a
|
|
465
562
|
* value cut or selected partway through survives whole-value matching.
|
|
466
|
-
* Matched
|
|
563
|
+
* Matched in any case and with whitespace runs collapsed, like `redactCut`. A
|
|
467
564
|
* base64 or base64url run that decodes to text holding a value or a
|
|
468
565
|
* fragment is rewritten whole (a basic-auth header the engine sent). Bound
|
|
469
566
|
* like `redact`.
|
package/dist/internal/time.d.ts
CHANGED
|
@@ -12,6 +12,15 @@ export declare class Deadline {
|
|
|
12
12
|
export declare function sleep(ms: number, signal?: AbortSignal): Promise<void>;
|
|
13
13
|
/** Canonical polling cadence for locator and assertion loops. */
|
|
14
14
|
export declare const POLL_INTERVAL_MS = 100;
|
|
15
|
+
/**
|
|
16
|
+
* Whether `cause` is a read a wait's deadline cut off: an operation timeout
|
|
17
|
+
* of a read that started with less than one poll tick of the wait left, so
|
|
18
|
+
* it timed out because the wait did, not because the app stopped answering.
|
|
19
|
+
* A read that started with more and still timed out hung, and its timeout is
|
|
20
|
+
* the failure. Reads the engine error a locator failure wraps as well as a
|
|
21
|
+
* bare one.
|
|
22
|
+
*/
|
|
23
|
+
export declare function cutOffAtDeadline(cause: unknown, startedWithMs: number): boolean;
|
|
15
24
|
/** How long a negated assertion must hold before it passes. */
|
|
16
25
|
export declare const NEGATION_GRACE_MS = 1000;
|
|
17
26
|
export interface PollConditionOptions {
|
|
@@ -19,17 +28,30 @@ export interface PollConditionOptions {
|
|
|
19
28
|
readonly signal: AbortSignal;
|
|
20
29
|
readonly negated: boolean;
|
|
21
30
|
/**
|
|
22
|
-
* Evaluates the positive condition once
|
|
23
|
-
*
|
|
24
|
-
* the
|
|
31
|
+
* Evaluates the positive condition once, every read bounded by
|
|
32
|
+
* `deadline`: a read on a longer budget outlasts the poll. Returns
|
|
33
|
+
* undefined when the condition cannot be evaluated yet: the positive poll
|
|
34
|
+
* keeps waiting and the negation grace window resets.
|
|
25
35
|
*/
|
|
26
36
|
evaluate(): Promise<boolean | undefined>;
|
|
27
|
-
|
|
37
|
+
/** Builds the poll's failure; `cause` is the read the deadline cut off, when one did. */
|
|
38
|
+
onTimeout(cause?: unknown): Error | Promise<Error>;
|
|
28
39
|
}
|
|
29
40
|
/**
|
|
30
41
|
* Polls a condition until it holds (or, when negated, until its negation has
|
|
31
42
|
* held continuously for the negation grace window), throwing the caller's
|
|
32
43
|
* error at the deadline.
|
|
44
|
+
*
|
|
45
|
+
* A negation holds from the moment the read that first saw it was issued, so
|
|
46
|
+
* a slow read counts toward the window. A budget shorter than the window
|
|
47
|
+
* still has to be satisfiable: the negation then only needs to hold for the
|
|
48
|
+
* whole budget, and passes at the deadline on what it has seen, since a read
|
|
49
|
+
* past the deadline has no budget left. A read the deadline cut off (see
|
|
50
|
+
* `cutOffAtDeadline`) after an earlier one completed saw nothing: the poll
|
|
51
|
+
* ends there. A negation then passes if it has held long enough by the
|
|
52
|
+
* time the read is cut off: the read left less than one poll tick unseen, no
|
|
53
|
+
* more than the gap between any two reads, so a short budget still passes at
|
|
54
|
+
* its deadline on what it saw.
|
|
33
55
|
*/
|
|
34
56
|
export declare function pollCondition(options: PollConditionOptions): Promise<void>;
|
|
35
57
|
/** Races a promise against a timeout; on timeout invokes onTimeout to build the error. */
|
package/dist/internal/time.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/** Deadline and cancellation helpers. */
|
|
2
|
-
import { E2EError } from './errors.js';
|
|
2
|
+
import { asEngineError, E2EError } from './errors.js';
|
|
3
3
|
export class Deadline {
|
|
4
4
|
endsAt;
|
|
5
5
|
constructor(timeoutMs, now = Date.now()) {
|
|
@@ -41,35 +41,79 @@ export function sleep(ms, signal) {
|
|
|
41
41
|
}
|
|
42
42
|
/** Canonical polling cadence for locator and assertion loops. */
|
|
43
43
|
export const POLL_INTERVAL_MS = 100;
|
|
44
|
+
/**
|
|
45
|
+
* Whether `cause` is a read a wait's deadline cut off: an operation timeout
|
|
46
|
+
* of a read that started with less than one poll tick of the wait left, so
|
|
47
|
+
* it timed out because the wait did, not because the app stopped answering.
|
|
48
|
+
* A read that started with more and still timed out hung, and its timeout is
|
|
49
|
+
* the failure. Reads the engine error a locator failure wraps as well as a
|
|
50
|
+
* bare one.
|
|
51
|
+
*/
|
|
52
|
+
export function cutOffAtDeadline(cause, startedWithMs) {
|
|
53
|
+
if (startedWithMs >= POLL_INTERVAL_MS)
|
|
54
|
+
return false;
|
|
55
|
+
const engineError = asEngineError(cause) ?? asEngineError(cause instanceof Error ? cause.cause : undefined);
|
|
56
|
+
return engineError?.code === 'OPERATION_TIMEOUT';
|
|
57
|
+
}
|
|
44
58
|
/** How long a negated assertion must hold before it passes. */
|
|
45
59
|
export const NEGATION_GRACE_MS = 1000;
|
|
46
60
|
/**
|
|
47
61
|
* Polls a condition until it holds (or, when negated, until its negation has
|
|
48
62
|
* held continuously for the negation grace window), throwing the caller's
|
|
49
63
|
* error at the deadline.
|
|
64
|
+
*
|
|
65
|
+
* A negation holds from the moment the read that first saw it was issued, so
|
|
66
|
+
* a slow read counts toward the window. A budget shorter than the window
|
|
67
|
+
* still has to be satisfiable: the negation then only needs to hold for the
|
|
68
|
+
* whole budget, and passes at the deadline on what it has seen, since a read
|
|
69
|
+
* past the deadline has no budget left. A read the deadline cut off (see
|
|
70
|
+
* `cutOffAtDeadline`) after an earlier one completed saw nothing: the poll
|
|
71
|
+
* ends there. A negation then passes if it has held long enough by the
|
|
72
|
+
* time the read is cut off: the read left less than one poll tick unseen, no
|
|
73
|
+
* more than the gap between any two reads, so a short budget still passes at
|
|
74
|
+
* its deadline on what it saw.
|
|
50
75
|
*/
|
|
51
76
|
export async function pollCondition(options) {
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
const grace = Math.min(NEGATION_GRACE_MS,
|
|
55
|
-
let
|
|
77
|
+
const { deadline, negated } = options;
|
|
78
|
+
const startedAt = Date.now();
|
|
79
|
+
const grace = Math.min(NEGATION_GRACE_MS, deadline.remaining(startedAt));
|
|
80
|
+
let readAt = startedAt;
|
|
81
|
+
let holdingSince;
|
|
82
|
+
const holds = (now) => holdingSince !== undefined && now - holdingSince >= grace;
|
|
83
|
+
let sampled = false;
|
|
56
84
|
for (;;) {
|
|
57
|
-
const
|
|
58
|
-
|
|
59
|
-
|
|
85
|
+
const startedWithMs = deadline.remaining(readAt);
|
|
86
|
+
let value;
|
|
87
|
+
try {
|
|
88
|
+
value = await options.evaluate();
|
|
89
|
+
sampled = true;
|
|
90
|
+
}
|
|
91
|
+
catch (cause) {
|
|
92
|
+
if (!sampled || !cutOffAtDeadline(cause, startedWithMs))
|
|
93
|
+
throw cause;
|
|
94
|
+
if (negated && holds(Math.min(Date.now(), deadline.endsAt)))
|
|
60
95
|
return;
|
|
96
|
+
throw await options.onTimeout(cause);
|
|
61
97
|
}
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
if (Date.now() - negatedTrueSince >= grace)
|
|
98
|
+
if (!negated) {
|
|
99
|
+
if (value === true)
|
|
65
100
|
return;
|
|
66
101
|
}
|
|
67
102
|
else {
|
|
68
|
-
|
|
103
|
+
holdingSince = value === false ? (holdingSince ?? readAt) : undefined;
|
|
69
104
|
}
|
|
70
|
-
|
|
105
|
+
const now = Date.now();
|
|
106
|
+
if (holds(now))
|
|
107
|
+
return;
|
|
108
|
+
if (deadline.expired(now))
|
|
71
109
|
throw await options.onTimeout();
|
|
72
|
-
await sleep(POLL_INTERVAL_MS, options.signal);
|
|
110
|
+
await sleep(negated ? Math.min(POLL_INTERVAL_MS, deadline.remaining(now)) : POLL_INTERVAL_MS, options.signal);
|
|
111
|
+
readAt = Date.now();
|
|
112
|
+
if (negated && deadline.expired(readAt)) {
|
|
113
|
+
if (holds(readAt))
|
|
114
|
+
return;
|
|
115
|
+
throw await options.onTimeout();
|
|
116
|
+
}
|
|
73
117
|
}
|
|
74
118
|
}
|
|
75
119
|
/** Races a promise against a timeout; on timeout invokes onTimeout to build the error. */
|
package/dist/internal/urls.d.ts
CHANGED
|
@@ -23,10 +23,12 @@ export declare function isLoopbackHost(hostname: string): boolean;
|
|
|
23
23
|
/** True when environment may default to `test` (loopback, .localhost, .test hosts). */
|
|
24
24
|
export declare function isImplicitTestHost(hostname: string): boolean;
|
|
25
25
|
/**
|
|
26
|
-
* Resolves a navigation URL against the base and refuses
|
|
27
|
-
*
|
|
28
|
-
* a click can reach one just as well, so a
|
|
29
|
-
* would guard nothing.
|
|
26
|
+
* Resolves a navigation URL against the base and refuses every scheme but
|
|
27
|
+
* http(s), except the exact `about:blank`. Returns the absolute URL string.
|
|
28
|
+
* Any http(s) origin is admitted: a click can reach one just as well, so a
|
|
29
|
+
* gate on typed navigation alone would guard nothing. The rule is an
|
|
30
|
+
* allowlist because a browser wraps and nests schemes (`view-source:file:`,
|
|
31
|
+
* `blob:`, `filesystem:`), and a list of forbidden ones misses the wrapper.
|
|
30
32
|
*/
|
|
31
33
|
export declare function resolveNavigationUrl(input: string, base: NormalizedBaseUrl | undefined): {
|
|
32
34
|
url: string;
|
|
@@ -53,5 +55,7 @@ export declare function sameSite(url: string | URL, site: string): boolean;
|
|
|
53
55
|
* Relative expected strings resolve against the base URL; string comparison is
|
|
54
56
|
* exact after WHATWG serialization; regexps test the complete serialized URL.
|
|
55
57
|
* Anything else is `INVALID_ARGUMENT`, the `toTextPattern` rule.
|
|
58
|
+
* `ignoreCase` is Playwright's: `true` compares a string case-insensitively
|
|
59
|
+
* and adds the `i` flag to a regexp, `false` removes it.
|
|
56
60
|
*/
|
|
57
|
-
export declare function urlMatches(current: string, expected: string | RegExp, baseHref: string): boolean;
|
|
61
|
+
export declare function urlMatches(current: string, expected: string | RegExp, baseHref: string, ignoreCase?: boolean): boolean;
|
package/dist/internal/urls.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/** URL normalization and origin policy helpers. */
|
|
2
2
|
import { ConfigurationError } from './errors.js';
|
|
3
3
|
import { testPattern } from './regexp.js';
|
|
4
|
-
import { toTextPattern } from './text.js';
|
|
4
|
+
import { toTextPattern, withIgnoreCase } from './text.js';
|
|
5
5
|
/**
|
|
6
6
|
* Parses and normalizes the app base URL. Rejects userinfo, query, and
|
|
7
7
|
* fragment. WHATWG parsing handles IDNA ASCII hosts, dot segments, and
|
|
@@ -94,12 +94,16 @@ export function isLoopbackHost(hostname) {
|
|
|
94
94
|
export function isImplicitTestHost(hostname) {
|
|
95
95
|
return isLoopbackHost(hostname) || hostname.endsWith('.test');
|
|
96
96
|
}
|
|
97
|
-
const
|
|
97
|
+
const NAVIGABLE_PROTOCOLS = new Set(['http:', 'https:']);
|
|
98
|
+
/** The one non-http(s) page navigation opens: empty, with no local or inline content. */
|
|
99
|
+
const BLANK_PAGE = 'about:blank';
|
|
98
100
|
/**
|
|
99
|
-
* Resolves a navigation URL against the base and refuses
|
|
100
|
-
*
|
|
101
|
-
* a click can reach one just as well, so a
|
|
102
|
-
* would guard nothing.
|
|
101
|
+
* Resolves a navigation URL against the base and refuses every scheme but
|
|
102
|
+
* http(s), except the exact `about:blank`. Returns the absolute URL string.
|
|
103
|
+
* Any http(s) origin is admitted: a click can reach one just as well, so a
|
|
104
|
+
* gate on typed navigation alone would guard nothing. The rule is an
|
|
105
|
+
* allowlist because a browser wraps and nests schemes (`view-source:file:`,
|
|
106
|
+
* `blob:`, `filesystem:`), and a list of forbidden ones misses the wrapper.
|
|
103
107
|
*/
|
|
104
108
|
export function resolveNavigationUrl(input, base) {
|
|
105
109
|
let url;
|
|
@@ -114,7 +118,7 @@ export function resolveNavigationUrl(input, base) {
|
|
|
114
118
|
}
|
|
115
119
|
throw new ConfigurationError('POLICY_DENIED', `malformed URL: ${input}`);
|
|
116
120
|
}
|
|
117
|
-
if (
|
|
121
|
+
if (!NAVIGABLE_PROTOCOLS.has(url.protocol) && url.href !== BLANK_PAGE) {
|
|
118
122
|
throw new ConfigurationError('POLICY_DENIED', `forbidden URL scheme: ${url.protocol}`);
|
|
119
123
|
}
|
|
120
124
|
return { url: url.href };
|
|
@@ -160,9 +164,11 @@ export function sameSite(url, site) {
|
|
|
160
164
|
* Relative expected strings resolve against the base URL; string comparison is
|
|
161
165
|
* exact after WHATWG serialization; regexps test the complete serialized URL.
|
|
162
166
|
* Anything else is `INVALID_ARGUMENT`, the `toTextPattern` rule.
|
|
167
|
+
* `ignoreCase` is Playwright's: `true` compares a string case-insensitively
|
|
168
|
+
* and adds the `i` flag to a regexp, `false` removes it.
|
|
163
169
|
*/
|
|
164
|
-
export function urlMatches(current, expected, baseHref) {
|
|
165
|
-
const pattern = toTextPattern(expected);
|
|
170
|
+
export function urlMatches(current, expected, baseHref, ignoreCase) {
|
|
171
|
+
const pattern = withIgnoreCase(toTextPattern(expected), ignoreCase);
|
|
166
172
|
if (pattern.kind === 'regexp') {
|
|
167
173
|
return testPattern(pattern.source, pattern.flags, serializeForComparison(current));
|
|
168
174
|
}
|
|
@@ -173,7 +179,10 @@ export function urlMatches(current, expected, baseHref) {
|
|
|
173
179
|
catch {
|
|
174
180
|
return false;
|
|
175
181
|
}
|
|
176
|
-
|
|
182
|
+
const actual = serializeForComparison(current);
|
|
183
|
+
return ignoreCase === true
|
|
184
|
+
? actual.toLowerCase() === expectedUrl.href.toLowerCase()
|
|
185
|
+
: actual === expectedUrl.href;
|
|
177
186
|
}
|
|
178
187
|
function serializeForComparison(url) {
|
|
179
188
|
try {
|
package/dist/locator/engine.d.ts
CHANGED
|
@@ -18,6 +18,11 @@ export declare function attributeOf(node: SemanticNode, name: string): string |
|
|
|
18
18
|
* empty or absent.
|
|
19
19
|
*/
|
|
20
20
|
export declare function denySecureRead(nodes: readonly SemanticNode[], label: string): void;
|
|
21
|
+
/**
|
|
22
|
+
* What a frame the engine reports missing means to a resolve: `wait` treats
|
|
23
|
+
* it like a node that is not there yet, `empty` answers zero matches.
|
|
24
|
+
*/
|
|
25
|
+
type MissingFrame = 'wait' | 'empty';
|
|
21
26
|
interface LocatorEngineOptions {
|
|
22
27
|
readonly session: TargetSession;
|
|
23
28
|
/** The running phase's signal and deadline, read per operation. */
|
|
@@ -95,8 +100,13 @@ export declare class LocatorEngine {
|
|
|
95
100
|
/** Reads every current match at once, for `allTextContents`: a missing frame reads as no matches. */
|
|
96
101
|
readAllNow(expression: LocatorExpression): Promise<readonly SemanticNode[]>;
|
|
97
102
|
private readEvery;
|
|
98
|
-
/**
|
|
99
|
-
|
|
103
|
+
/**
|
|
104
|
+
* Reads one node for polling surfaces; returns null while zero matches.
|
|
105
|
+
* A poll that waits for absence passes `missingFrame: 'empty'`: nothing
|
|
106
|
+
* under a frame that is not in the document is attached, so the sample is
|
|
107
|
+
* zero matches rather than a wait for the frame.
|
|
108
|
+
*/
|
|
109
|
+
tryRead(expression: LocatorExpression, deadline: Deadline, missingFrame?: MissingFrame): Promise<{
|
|
100
110
|
node: SemanticNode | null;
|
|
101
111
|
count: number;
|
|
102
112
|
}>;
|
package/dist/locator/engine.js
CHANGED
|
@@ -3,7 +3,7 @@ import {} from '../engine/surface.js';
|
|
|
3
3
|
import { asEngineError, ConfigurationError, E2EError, TestError, translateEngineError, } from '../internal/errors.js';
|
|
4
4
|
import { requireKey } from '../internal/keys.js';
|
|
5
5
|
import { describeExpression, expressionHints } from './expression.js';
|
|
6
|
-
import { Deadline, POLL_INTERVAL_MS, sleep } from '../internal/time.js';
|
|
6
|
+
import { cutOffAtDeadline, Deadline, POLL_INTERVAL_MS, sleep } from '../internal/time.js';
|
|
7
7
|
/** Canonical visibility predicate over a semantic node snapshot. */
|
|
8
8
|
export function isNodeVisible(node) {
|
|
9
9
|
return node !== null && node.states?.hidden !== true;
|
|
@@ -96,16 +96,22 @@ export class LocatorEngine {
|
|
|
96
96
|
* matches, and a read that promised not to wait must not wait for it.
|
|
97
97
|
*/
|
|
98
98
|
async resolveOnce(expression, deadline, missingFrame = 'wait') {
|
|
99
|
+
let retried;
|
|
99
100
|
for (;;) {
|
|
101
|
+
const startedWithMs = deadline.remaining();
|
|
100
102
|
try {
|
|
101
103
|
return await this.session.locate(expression, this.operationWithin(deadline));
|
|
102
104
|
}
|
|
103
105
|
catch (cause) {
|
|
106
|
+
// A retry the deadline cut off saw nothing new: the wait ends on the failure it was retrying.
|
|
107
|
+
if (retried !== undefined && cutOffAtDeadline(cause, startedWithMs))
|
|
108
|
+
throw translateLocatorError(retried, expression);
|
|
104
109
|
const engineError = asEngineError(cause);
|
|
105
110
|
if (engineError?.retryable === true) {
|
|
106
111
|
if (missingFrame === 'empty' && engineError.code === 'FRAME_NOT_FOUND')
|
|
107
112
|
return [];
|
|
108
113
|
if (!deadline.expired()) {
|
|
114
|
+
retried = cause;
|
|
109
115
|
await sleep(POLL_INTERVAL_MS, this.signal);
|
|
110
116
|
continue;
|
|
111
117
|
}
|
|
@@ -120,15 +126,31 @@ export class LocatorEngine {
|
|
|
120
126
|
*/
|
|
121
127
|
async resolveExactlyOne(expression, deadline) {
|
|
122
128
|
const startedMs = Date.now();
|
|
129
|
+
// The wait as it happened: a deadline capped by the test's remaining
|
|
130
|
+
// budget waited less than the action timeout, and the report says so.
|
|
131
|
+
const notFound = (cause) => new TestError('LOCATOR_NOT_FOUND', `locator matched no nodes within ${describeExpression(expression)}`, {
|
|
132
|
+
details: locatorDetails(expression, Date.now() - startedMs),
|
|
133
|
+
...(cause === undefined ? {} : { cause }),
|
|
134
|
+
});
|
|
135
|
+
let sampled = false;
|
|
123
136
|
for (;;) {
|
|
124
|
-
const
|
|
137
|
+
const startedWithMs = deadline.remaining();
|
|
138
|
+
let refs;
|
|
139
|
+
try {
|
|
140
|
+
refs = await this.resolveOnce(expression, deadline);
|
|
141
|
+
}
|
|
142
|
+
catch (cause) {
|
|
143
|
+
// A resolve the deadline cut off, after one that matched nothing, is the wait running out.
|
|
144
|
+
if (sampled && cutOffAtDeadline(cause, startedWithMs))
|
|
145
|
+
throw notFound(cause);
|
|
146
|
+
throw cause;
|
|
147
|
+
}
|
|
148
|
+
sampled = true;
|
|
149
|
+
const ref = assertSingle(refs, expression);
|
|
125
150
|
if (ref !== null)
|
|
126
151
|
return ref;
|
|
127
|
-
if (deadline.expired())
|
|
128
|
-
|
|
129
|
-
// budget waited less than the action timeout, and the report says so.
|
|
130
|
-
throw new TestError('LOCATOR_NOT_FOUND', `locator matched no nodes within ${describeExpression(expression)}`, { details: locatorDetails(expression, Date.now() - startedMs) });
|
|
131
|
-
}
|
|
152
|
+
if (deadline.expired())
|
|
153
|
+
throw notFound();
|
|
132
154
|
await sleep(POLL_INTERVAL_MS, this.signal);
|
|
133
155
|
}
|
|
134
156
|
}
|
|
@@ -213,9 +235,14 @@ export class LocatorEngine {
|
|
|
213
235
|
}
|
|
214
236
|
}
|
|
215
237
|
}
|
|
216
|
-
/**
|
|
217
|
-
|
|
218
|
-
|
|
238
|
+
/**
|
|
239
|
+
* Reads one node for polling surfaces; returns null while zero matches.
|
|
240
|
+
* A poll that waits for absence passes `missingFrame: 'empty'`: nothing
|
|
241
|
+
* under a frame that is not in the document is attached, so the sample is
|
|
242
|
+
* zero matches rather than a wait for the frame.
|
|
243
|
+
*/
|
|
244
|
+
async tryRead(expression, deadline, missingFrame = 'wait') {
|
|
245
|
+
return this.readOne(expression, deadline, missingFrame);
|
|
219
246
|
}
|
|
220
247
|
/**
|
|
221
248
|
* Reads the one current match at once, for `isVisible` and `isHidden`:
|
|
@@ -270,16 +297,30 @@ export class LocatorEngine {
|
|
|
270
297
|
if (this.session.actions.has('scrollIntoView')) {
|
|
271
298
|
await this.performUntil(expression, { kind: 'scrollIntoView' }, deadline);
|
|
272
299
|
}
|
|
300
|
+
const notVisible = (cause) => new TestError('LOCATOR_NOT_FOUND', `locator did not become visible with a box to act within: ${describeExpression(expression)}`, {
|
|
301
|
+
details: locatorDetails(expression, Date.now() - startedMs),
|
|
302
|
+
...(cause === undefined ? {} : { cause }),
|
|
303
|
+
});
|
|
304
|
+
let sampled = false;
|
|
273
305
|
for (;;) {
|
|
274
|
-
const
|
|
306
|
+
const startedWithMs = deadline.remaining();
|
|
307
|
+
let node;
|
|
308
|
+
try {
|
|
309
|
+
({ node } = await this.tryRead(expression, deadline));
|
|
310
|
+
}
|
|
311
|
+
catch (cause) {
|
|
312
|
+
if (sampled && cutOffAtDeadline(cause, startedWithMs))
|
|
313
|
+
throw notVisible(cause);
|
|
314
|
+
throw cause;
|
|
315
|
+
}
|
|
316
|
+
sampled = true;
|
|
275
317
|
const rect = isNodeVisible(node) ? node.rect : undefined;
|
|
276
318
|
if (rect !== undefined) {
|
|
277
319
|
await this.dispatchAt({ x: rect.x + offset.x, y: rect.y + offset.y }, action, deadline);
|
|
278
320
|
return;
|
|
279
321
|
}
|
|
280
|
-
if (deadline.expired())
|
|
281
|
-
throw
|
|
282
|
-
}
|
|
322
|
+
if (deadline.expired())
|
|
323
|
+
throw notVisible();
|
|
283
324
|
await sleep(POLL_INTERVAL_MS, this.signal);
|
|
284
325
|
}
|
|
285
326
|
}
|