@specific.dev/spectest 0.66.0 → 0.68.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/browser.js +42 -1
- package/dist/components/supabase.d.ts +0 -14
- package/dist/components/supabase.js +2 -8
- package/dist/daemon.js +155 -35
- package/dist/harness/intercept.d.ts +22 -0
- package/dist/harness/intercept.js +29 -0
- package/dist/harness/wrapper-rules.d.ts +149 -0
- package/dist/harness/wrapper-rules.js +422 -0
- package/dist/index.d.ts +52 -16
- package/dist/index.js +76 -17
- package/dist/locator-errors.d.ts +19 -10
- package/dist/locator-errors.js +80 -20
- package/dist/locator-hints.d.ts +96 -0
- package/dist/locator-hints.js +403 -0
- package/dist/locator.d.ts +22 -0
- package/dist/locator.js +63 -9
- package/dist/page-snapshot.d.ts +42 -0
- package/dist/page-snapshot.js +149 -0
- package/dist/recorder.d.ts +16 -0
- package/dist/text-match.d.ts +39 -0
- package/dist/text-match.js +239 -0
- package/package.json +1 -1
- package/src/browser.ts +43 -1
- package/src/components/supabase.ts +2 -20
- package/src/daemon.ts +171 -34
- package/src/harness/intercept.test.ts +36 -0
- package/src/harness/intercept.ts +40 -0
- package/src/harness/wrapper-rules.test.ts +170 -0
- package/src/harness/wrapper-rules.ts +547 -0
- package/src/index.ts +159 -32
- package/src/locator-errors.test.ts +99 -11
- package/src/locator-errors.ts +98 -19
- package/src/locator-hints.test.ts +188 -0
- package/src/locator-hints.ts +514 -0
- package/src/locator.ts +72 -9
- package/src/page-snapshot.test.ts +100 -0
- package/src/page-snapshot.ts +180 -0
- package/src/recorder.ts +16 -0
- package/src/text-match.test.ts +132 -0
- package/src/text-match.ts +285 -0
package/dist/index.js
CHANGED
|
@@ -39,7 +39,8 @@ export { S3Client } from "./s3.js";
|
|
|
39
39
|
// own browser. See `mcp.ts`.
|
|
40
40
|
export { McpHttpError, McpRpcError, McpAuthDeniedError, } from "./mcp.js";
|
|
41
41
|
import { isLocator, getLocatorProbe, isBrowserSession, getBrowserProbe, DEFAULT_ACTION_TIMEOUT_MS, } from "./locator.js";
|
|
42
|
-
import { formatWaited, locatorFailureMessage } from "./locator-errors.js";
|
|
42
|
+
import { classifyLocatorFailure, formatWaited, locatorFailureMessage, } from "./locator-errors.js";
|
|
43
|
+
import { containsText, containsTextArray, escapeInvisible, matchesText, matchesTextArray, textDifferenceNote, } from "./text-match.js";
|
|
43
44
|
import { describeUrlPattern, matchesUrl } from "./url-match.js";
|
|
44
45
|
import { applyCoverageAdapters, validateCoverage } from "./coverage.js";
|
|
45
46
|
import { resolveExistingProjectPath } from "./project-files.js";
|
|
@@ -674,14 +675,33 @@ function buildLocatorMatchers(loc, negated, message) {
|
|
|
674
675
|
// matches a hidden element fail identically — so the count is read once,
|
|
675
676
|
// at failure time only. `toHaveCount` is left alone: "expected count 2,
|
|
676
677
|
// got 0" already says it, and better.
|
|
678
|
+
//
|
|
679
|
+
// Either way the message ends with the near-miss block — what the page
|
|
680
|
+
// holds that is close to what was asked for (see locator-hints.ts) — so
|
|
681
|
+
// the author does not have to run again to find out.
|
|
682
|
+
const nearMiss = async () => {
|
|
683
|
+
try {
|
|
684
|
+
return await probe.nearMiss();
|
|
685
|
+
}
|
|
686
|
+
catch {
|
|
687
|
+
return "";
|
|
688
|
+
}
|
|
689
|
+
};
|
|
677
690
|
const elementFailure = async (err) => {
|
|
678
|
-
if (err !== undefined)
|
|
679
|
-
|
|
691
|
+
if (err !== undefined) {
|
|
692
|
+
const failure = classifyLocatorFailure(probe.label, err);
|
|
693
|
+
if (!failure)
|
|
694
|
+
return undefined;
|
|
695
|
+
return failure.kind === "no-match"
|
|
696
|
+
? failure.message + (await nearMiss())
|
|
697
|
+
: failure.message;
|
|
698
|
+
}
|
|
680
699
|
if (expectsGone || matcher === "toHaveCount")
|
|
681
700
|
return undefined;
|
|
682
701
|
try {
|
|
683
702
|
if ((await probe.count()) === 0) {
|
|
684
|
-
return `No element matches ${probe.label} (waited ${formatWaited(budget)})
|
|
703
|
+
return (`No element matches ${probe.label} (waited ${formatWaited(budget)})` +
|
|
704
|
+
(await nearMiss()));
|
|
685
705
|
}
|
|
686
706
|
}
|
|
687
707
|
catch {
|
|
@@ -754,7 +774,22 @@ function buildLocatorMatchers(loc, negated, message) {
|
|
|
754
774
|
}
|
|
755
775
|
};
|
|
756
776
|
const not = negated ? " not" : "";
|
|
757
|
-
|
|
777
|
+
// The element's text, read the way the caller asked for it. `useInnerText`
|
|
778
|
+
// is playwright's option and means the same here: what the page renders,
|
|
779
|
+
// rather than every character in the subtree.
|
|
780
|
+
const readText = async (opts) => opts?.useInnerText
|
|
781
|
+
? await probe.innerText(opts.timeout)
|
|
782
|
+
: ((await probe.textContent(opts?.timeout)) ?? "");
|
|
783
|
+
const readTexts = async (opts) => opts?.useInnerText ? await probe.allInnerTexts() : await probe.allTextContents();
|
|
784
|
+
const isArrayExpectation = (v) => Array.isArray(v);
|
|
785
|
+
/** Expected values render as themselves; a RegExp renders as its source. */
|
|
786
|
+
const fmtExpected = (v) => v instanceof RegExp
|
|
787
|
+
? String(v)
|
|
788
|
+
: Array.isArray(v)
|
|
789
|
+
? `[${v.map(fmtExpected).join(", ")}]`
|
|
790
|
+
: fmt(v);
|
|
791
|
+
/** What goes on the assertion event: a RegExp cannot be serialized. */
|
|
792
|
+
const expectedValue = (v) => (v instanceof RegExp ? String(v) : Array.isArray(v) ? v.map(expectedValue) : v);
|
|
758
793
|
return {
|
|
759
794
|
toBeVisible: (opts) => run("toBeVisible", opts?.timeout, async () => {
|
|
760
795
|
const v = await probe.isVisible();
|
|
@@ -765,17 +800,34 @@ function buildLocatorMatchers(loc, negated, message) {
|
|
|
765
800
|
return { satisfied: !v, actual: v };
|
|
766
801
|
}, () => `expected ${probe.label}${not} to be hidden`),
|
|
767
802
|
toHaveText: (expected, opts) => run("toHaveText", opts?.timeout, async () => {
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
803
|
+
if (isArrayExpectation(expected)) {
|
|
804
|
+
const texts = await readTexts(opts);
|
|
805
|
+
return { satisfied: matchesTextArray(texts, expected, opts), actual: texts };
|
|
806
|
+
}
|
|
807
|
+
const t = await readText(opts);
|
|
808
|
+
return { satisfied: matchesText(t, expected, opts), actual: t };
|
|
809
|
+
}, (a) => `expected ${probe.label}${not} to have text ${fmtExpected(expected)}, got ${fmt(a)}` +
|
|
810
|
+
textDifferenceNote(a, expected, "equal"), expectedValue(expected)),
|
|
771
811
|
toContainText: (expected, opts) => run("toContainText", opts?.timeout, async () => {
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
812
|
+
if (isArrayExpectation(expected)) {
|
|
813
|
+
const texts = await readTexts(opts);
|
|
814
|
+
return { satisfied: containsTextArray(texts, expected, opts), actual: texts };
|
|
815
|
+
}
|
|
816
|
+
const t = await readText(opts);
|
|
817
|
+
return { satisfied: containsText(t, expected, opts), actual: t };
|
|
818
|
+
}, (a) => `expected ${probe.label}${not} to contain text ${fmtExpected(expected)}, got ${fmt(a)}` +
|
|
819
|
+
textDifferenceNote(a, expected, "contains"), expectedValue(expected)),
|
|
820
|
+
// Deliberately NOT whitespace-normalized: an input's value is data the
|
|
821
|
+
// user typed or the app set, not rendered text, and playwright compares
|
|
822
|
+
// it exactly for the same reason. When that exactness is what failed,
|
|
823
|
+
// `textDifferenceNote` says so instead of leaving two identical-looking
|
|
824
|
+
// strings on screen.
|
|
775
825
|
toHaveValue: (expected, opts) => run("toHaveValue", opts?.timeout, async () => {
|
|
776
826
|
const v = await probe.inputValue(opts?.timeout);
|
|
777
|
-
|
|
778
|
-
|
|
827
|
+
const satisfied = expected instanceof RegExp ? expected.test(v) : v === expected;
|
|
828
|
+
return { satisfied, actual: v };
|
|
829
|
+
}, (a) => `expected ${probe.label}${not} to have value ${fmtExpected(expected)}, got ${fmt(a)}` +
|
|
830
|
+
textDifferenceNote(a, expected, "equal"), expectedValue(expected)),
|
|
779
831
|
toHaveCount: (expected, opts) => run("toHaveCount", opts?.timeout, async () => {
|
|
780
832
|
const c = await probe.count();
|
|
781
833
|
return { satisfied: c === expected, actual: c };
|
|
@@ -949,7 +1001,8 @@ function buildCore(actual, tag, negated, message, pendingError) {
|
|
|
949
1001
|
return {
|
|
950
1002
|
toBe(expected) {
|
|
951
1003
|
const exp = readRaw(expected);
|
|
952
|
-
run("toBe", Object.is(actual, exp), `expected ${fmt(actual)}${negated ? " not" : ""} to be ${fmt(exp)}
|
|
1004
|
+
run("toBe", Object.is(actual, exp), `expected ${fmt(actual)}${negated ? " not" : ""} to be ${fmt(exp)}` +
|
|
1005
|
+
textDifferenceNote(actual, exp, "equal"), exp);
|
|
953
1006
|
},
|
|
954
1007
|
toEqual(expected) {
|
|
955
1008
|
const exp = readRaw(expected);
|
|
@@ -960,7 +1013,8 @@ function buildCore(actual, tag, negated, message, pendingError) {
|
|
|
960
1013
|
catch {
|
|
961
1014
|
equal = false;
|
|
962
1015
|
}
|
|
963
|
-
run("toEqual", equal, `expected ${fmt(actual)}${negated ? " not" : ""} to deep-equal ${fmt(exp)}
|
|
1016
|
+
run("toEqual", equal, `expected ${fmt(actual)}${negated ? " not" : ""} to deep-equal ${fmt(exp)}` +
|
|
1017
|
+
textDifferenceNote(actual, exp, "equal"), exp);
|
|
964
1018
|
},
|
|
965
1019
|
toBeTruthy() {
|
|
966
1020
|
run("toBeTruthy", !!actual, `expected ${fmt(actual)}${negated ? " not" : ""} to be truthy`);
|
|
@@ -997,7 +1051,8 @@ function buildCore(actual, tag, negated, message, pendingError) {
|
|
|
997
1051
|
}
|
|
998
1052
|
});
|
|
999
1053
|
}
|
|
1000
|
-
run("toContain", contained, `expected ${fmt(actual)}${negated ? " not" : ""} to contain ${fmt(exp)}
|
|
1054
|
+
run("toContain", contained, `expected ${fmt(actual)}${negated ? " not" : ""} to contain ${fmt(exp)}` +
|
|
1055
|
+
textDifferenceNote(actual, exp, "contains"), exp);
|
|
1001
1056
|
},
|
|
1002
1057
|
toMatch(re) {
|
|
1003
1058
|
run("toMatch", typeof actual === "string" && re.test(actual), `expected ${fmt(actual)}${negated ? " not" : ""} to match ${re}`, String(re));
|
|
@@ -1032,8 +1087,12 @@ function buildCore(actual, tag, negated, message, pendingError) {
|
|
|
1032
1087
|
};
|
|
1033
1088
|
}
|
|
1034
1089
|
function fmt(v) {
|
|
1090
|
+
// A string is escaped down to its invisible characters, so two values that
|
|
1091
|
+
// print the same on screen do not print the same in a failure. Without it a
|
|
1092
|
+
// no-break space and a space are the same three characters wide, and the
|
|
1093
|
+
// message reads "expected "15 000 kr", got "15 000 kr"".
|
|
1035
1094
|
if (typeof v === "string")
|
|
1036
|
-
return JSON.stringify(v);
|
|
1095
|
+
return escapeInvisible(JSON.stringify(v));
|
|
1037
1096
|
if (typeof v === "bigint")
|
|
1038
1097
|
return `${v}n`;
|
|
1039
1098
|
if (v === undefined)
|
package/dist/locator-errors.d.ts
CHANGED
|
@@ -1,21 +1,30 @@
|
|
|
1
1
|
/** A wait's duration, in the units a reader thinks in: "800ms", "5s", "1.5s". */
|
|
2
2
|
export declare function formatWaited(ms: number): string;
|
|
3
|
+
/** How a locator failed. `no-match` is the only kind worth asking the page
|
|
4
|
+
* about (see locator-hints.ts) — for every other kind the element was found
|
|
5
|
+
* and near misses would be noise. */
|
|
6
|
+
export type LocatorFailureKind = "no-match" | "state" | "strict";
|
|
7
|
+
export interface LocatorFailure {
|
|
8
|
+
message: string;
|
|
9
|
+
kind: LocatorFailureKind;
|
|
10
|
+
}
|
|
3
11
|
/**
|
|
4
|
-
*
|
|
5
|
-
*
|
|
12
|
+
* What to fail with, or `undefined` when `err` is not a locator failure we
|
|
13
|
+
* understand (in which case the caller must leave it alone).
|
|
6
14
|
*
|
|
7
15
|
* `label` is the locator's human chain label, the same string the timeline
|
|
8
16
|
* step shows.
|
|
9
17
|
*/
|
|
18
|
+
export declare function classifyLocatorFailure(label: string, err: unknown): LocatorFailure | undefined;
|
|
19
|
+
/** The sentence alone. See {@link classifyLocatorFailure}. */
|
|
10
20
|
export declare function locatorFailureMessage(label: string, err: unknown): string | undefined;
|
|
11
21
|
/**
|
|
12
|
-
*
|
|
13
|
-
*
|
|
22
|
+
* The rewrite above, plus the near-miss block for the one failure that can
|
|
23
|
+
* carry one — a locator that matched nothing.
|
|
14
24
|
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
* then the timeout wall right under it.
|
|
25
|
+
* `hints` is only called in that case, so a page query is never made for a
|
|
26
|
+
* failure it could not explain (a disabled element, a covered one, an
|
|
27
|
+
* ambiguous match). It is expected to be best-effort itself; anything it
|
|
28
|
+
* throws leaves the plain sentence standing.
|
|
20
29
|
*/
|
|
21
|
-
export declare function
|
|
30
|
+
export declare function rewriteLocatorErrorWithHints(err: unknown, label: string, hints: () => Promise<string>): Promise<unknown>;
|
package/dist/locator-errors.js
CHANGED
|
@@ -8,10 +8,15 @@
|
|
|
8
8
|
// as an infrastructure problem and says nothing about the page.
|
|
9
9
|
//
|
|
10
10
|
// This module turns those into one sentence that names the element and what
|
|
11
|
-
// was wrong with it.
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
11
|
+
// was wrong with it. A strict-mode violation — the opposite failure, where the
|
|
12
|
+
// locator matched too much — is rewritten too, keeping playwright's own list
|
|
13
|
+
// of matches, which is the useful half of it. Anything else (a closed page, an
|
|
14
|
+
// assertion of ours) is passed through untouched: the rule is that a message is
|
|
15
|
+
// only ever replaced when we have something better to say.
|
|
16
|
+
//
|
|
17
|
+
// `classifyLocatorFailure` also says WHICH failure it was, because only one of
|
|
18
|
+
// them — a locator that matched nothing — can be explained further by asking
|
|
19
|
+
// the page what it holds (see locator-hints.ts).
|
|
15
20
|
//
|
|
16
21
|
// The call log is read from the error's own `log` array (playwright attaches
|
|
17
22
|
// it) and falls back to parsing the message, since only the message survives
|
|
@@ -80,38 +85,72 @@ function actionabilityReason(log) {
|
|
|
80
85
|
}
|
|
81
86
|
return undefined;
|
|
82
87
|
}
|
|
88
|
+
/** Playwright's strict-mode violation: the locator matched more than one
|
|
89
|
+
* element and refused to act. It is an ordinary `Error`, not a
|
|
90
|
+
* `TimeoutError`, and its message already lists the matches — which is the
|
|
91
|
+
* useful half, so it is kept and only the wall of text around it is cut. */
|
|
92
|
+
function parseStrictViolation(err, label) {
|
|
93
|
+
const e = err;
|
|
94
|
+
if (!e || typeof e.message !== "string")
|
|
95
|
+
return undefined;
|
|
96
|
+
if (!e.message.includes("strict mode violation"))
|
|
97
|
+
return undefined;
|
|
98
|
+
const m = /resolved to (\d+) elements/.exec(e.message);
|
|
99
|
+
if (!m)
|
|
100
|
+
return undefined;
|
|
101
|
+
const matches = e.message
|
|
102
|
+
.split("\n")
|
|
103
|
+
.filter((l) => /^\s*\d+\)\s/.test(l))
|
|
104
|
+
.slice(0, 3)
|
|
105
|
+
.map((l) => ` - ${l.trim().replace(/^\d+\)\s*/, "")}`);
|
|
106
|
+
const head = `${label} matches ${m[1]} elements, so it is ambiguous` +
|
|
107
|
+
" — narrow it with .first(), .nth(i), .filter({ hasText }) or a more specific query";
|
|
108
|
+
return {
|
|
109
|
+
kind: "strict",
|
|
110
|
+
message: matches.length ? `${head}:\n${matches.join("\n")}` : head,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
83
113
|
/**
|
|
84
|
-
*
|
|
85
|
-
*
|
|
114
|
+
* What to fail with, or `undefined` when `err` is not a locator failure we
|
|
115
|
+
* understand (in which case the caller must leave it alone).
|
|
86
116
|
*
|
|
87
117
|
* `label` is the locator's human chain label, the same string the timeline
|
|
88
118
|
* step shows.
|
|
89
119
|
*/
|
|
90
|
-
export function
|
|
120
|
+
export function classifyLocatorFailure(label, err) {
|
|
121
|
+
const strict = parseStrictViolation(err, label);
|
|
122
|
+
if (strict)
|
|
123
|
+
return strict;
|
|
91
124
|
const detail = parseTimeout(err);
|
|
92
125
|
if (!detail)
|
|
93
126
|
return undefined;
|
|
94
127
|
const waited = `(waited ${formatWaited(detail.timeoutMs)})`;
|
|
95
|
-
if (!resolved(detail.log))
|
|
96
|
-
return `No element matches ${label} ${waited}
|
|
128
|
+
if (!resolved(detail.log)) {
|
|
129
|
+
return { kind: "no-match", message: `No element matches ${label} ${waited}` };
|
|
130
|
+
}
|
|
97
131
|
// It matched, so the failure is about the element's state.
|
|
132
|
+
const state = (message) => ({ kind: "state", message });
|
|
98
133
|
switch (waitedForState(detail.log)) {
|
|
99
134
|
case "hidden":
|
|
100
|
-
return `Element ${label} is still visible ${waited}
|
|
135
|
+
return state(`Element ${label} is still visible ${waited}`);
|
|
101
136
|
case "detached":
|
|
102
|
-
return `Element ${label} is still attached to the page ${waited}
|
|
137
|
+
return state(`Element ${label} is still attached to the page ${waited}`);
|
|
103
138
|
case "visible":
|
|
104
|
-
return `Element ${label} is not visible ${waited}
|
|
139
|
+
return state(`Element ${label} is not visible ${waited}`);
|
|
105
140
|
default:
|
|
106
141
|
break;
|
|
107
142
|
}
|
|
108
143
|
const reason = actionabilityReason(detail.log);
|
|
109
|
-
return reason
|
|
144
|
+
return state(reason
|
|
110
145
|
? `Element ${label} ${reason} ${waited}`
|
|
111
|
-
: `Element ${label} never became ready for this action ${waited}
|
|
146
|
+
: `Element ${label} never became ready for this action ${waited}`);
|
|
147
|
+
}
|
|
148
|
+
/** The sentence alone. See {@link classifyLocatorFailure}. */
|
|
149
|
+
export function locatorFailureMessage(label, err) {
|
|
150
|
+
return classifyLocatorFailure(label, err)?.message;
|
|
112
151
|
}
|
|
113
152
|
/**
|
|
114
|
-
* Replace a locator
|
|
153
|
+
* Replace a locator failure's message in place and hand the error back, so the
|
|
115
154
|
* caller can `throw` it unchanged in every other respect.
|
|
116
155
|
*
|
|
117
156
|
* The error is mutated rather than wrapped: its stack holds the frames of the
|
|
@@ -120,11 +159,8 @@ export function locatorFailureMessage(label, err) {
|
|
|
120
159
|
* too — otherwise the CLI's failure block would print the friendly message and
|
|
121
160
|
* then the timeout wall right under it.
|
|
122
161
|
*/
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
if (message === undefined)
|
|
126
|
-
return err;
|
|
127
|
-
const e = err;
|
|
162
|
+
/** Put `message` on `err`, in the message and in the stack's copy of it. */
|
|
163
|
+
function setErrorMessage(e, message) {
|
|
128
164
|
const old = e.message;
|
|
129
165
|
if (typeof e.stack === "string") {
|
|
130
166
|
for (const [header, replacement] of [
|
|
@@ -140,3 +176,27 @@ export function rewriteLocatorError(err, label) {
|
|
|
140
176
|
e.message = message;
|
|
141
177
|
return e;
|
|
142
178
|
}
|
|
179
|
+
/**
|
|
180
|
+
* The rewrite above, plus the near-miss block for the one failure that can
|
|
181
|
+
* carry one — a locator that matched nothing.
|
|
182
|
+
*
|
|
183
|
+
* `hints` is only called in that case, so a page query is never made for a
|
|
184
|
+
* failure it could not explain (a disabled element, a covered one, an
|
|
185
|
+
* ambiguous match). It is expected to be best-effort itself; anything it
|
|
186
|
+
* throws leaves the plain sentence standing.
|
|
187
|
+
*/
|
|
188
|
+
export async function rewriteLocatorErrorWithHints(err, label, hints) {
|
|
189
|
+
const failure = classifyLocatorFailure(label, err);
|
|
190
|
+
if (!failure)
|
|
191
|
+
return err;
|
|
192
|
+
let message = failure.message;
|
|
193
|
+
if (failure.kind === "no-match") {
|
|
194
|
+
try {
|
|
195
|
+
message += await hints();
|
|
196
|
+
}
|
|
197
|
+
catch {
|
|
198
|
+
/* A diagnostic must never replace the failure it explains. */
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
return setErrorMessage(err, message);
|
|
202
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import type { Page } from "playwright-core";
|
|
2
|
+
/** What the author asked for — the last selecting step of a failed chain. */
|
|
3
|
+
export interface HintTarget {
|
|
4
|
+
kind: "role" | "text" | "label" | "placeholder" | "altText" | "title" | "testId" | "other";
|
|
5
|
+
/** The ARIA role, for `kind: "role"`. */
|
|
6
|
+
role?: string;
|
|
7
|
+
/** The string the author asked for. Absent for a RegExp query, which we
|
|
8
|
+
* make no suggestions from — a near-miss on a pattern is not a near-miss. */
|
|
9
|
+
query?: string;
|
|
10
|
+
}
|
|
11
|
+
/** One role/name pair from the page's accessibility tree. */
|
|
12
|
+
export interface AriaCandidate {
|
|
13
|
+
role: string;
|
|
14
|
+
name: string;
|
|
15
|
+
}
|
|
16
|
+
/** An interactive element whose visible text is not its accessible name. */
|
|
17
|
+
export interface NameMismatch {
|
|
18
|
+
tag: string;
|
|
19
|
+
text: string;
|
|
20
|
+
name: string;
|
|
21
|
+
/** The attribute the name came from: `aria-label`, `aria-labelledby`, … */
|
|
22
|
+
from: string;
|
|
23
|
+
}
|
|
24
|
+
/** What the DOM pass brings back. Every list is deduplicated and capped. */
|
|
25
|
+
export interface DomFacts {
|
|
26
|
+
texts: string[];
|
|
27
|
+
testIds: string[];
|
|
28
|
+
placeholders: string[];
|
|
29
|
+
labels: string[];
|
|
30
|
+
titles: string[];
|
|
31
|
+
alts: string[];
|
|
32
|
+
mismatches: NameMismatch[];
|
|
33
|
+
}
|
|
34
|
+
export declare const EMPTY_DOM_FACTS: DomFacts;
|
|
35
|
+
/** The locator a suggestion proposes, as data — so the ranking stays pure and
|
|
36
|
+
* testable and only `nearMissHints` touches a page. */
|
|
37
|
+
export type SuggestedLocator = {
|
|
38
|
+
m: "getByRole";
|
|
39
|
+
role: string;
|
|
40
|
+
name: string;
|
|
41
|
+
} | {
|
|
42
|
+
m: "getByText";
|
|
43
|
+
text: string;
|
|
44
|
+
} | {
|
|
45
|
+
m: "getByLabel";
|
|
46
|
+
text: string;
|
|
47
|
+
} | {
|
|
48
|
+
m: "getByPlaceholder";
|
|
49
|
+
text: string;
|
|
50
|
+
} | {
|
|
51
|
+
m: "getByAltText";
|
|
52
|
+
text: string;
|
|
53
|
+
} | {
|
|
54
|
+
m: "getByTitle";
|
|
55
|
+
text: string;
|
|
56
|
+
} | {
|
|
57
|
+
m: "getByTestId";
|
|
58
|
+
id: string;
|
|
59
|
+
};
|
|
60
|
+
export interface Suggestion {
|
|
61
|
+
/** What is on the page, in the reader's terms. */
|
|
62
|
+
fact: string;
|
|
63
|
+
/** The locator that selects it. */
|
|
64
|
+
locator: SuggestedLocator;
|
|
65
|
+
/** Higher is closer. Ranking only; never shown. */
|
|
66
|
+
score: number;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* How close `candidate` is to what the author asked for, from 0 (unrelated)
|
|
70
|
+
* to 4 (the same string). Everything at or above `NEAR` is worth showing.
|
|
71
|
+
*/
|
|
72
|
+
export declare function closeness(query: string, candidate: string): number;
|
|
73
|
+
/** Render one verified suggestion as the line the author reads. */
|
|
74
|
+
export declare function suggestionLine(s: Suggestion, matches: number): string;
|
|
75
|
+
/**
|
|
76
|
+
* Rank what the page holds against what the author asked for. Pure: the
|
|
77
|
+
* caller collects `aria`/`dom` and verifies the winners.
|
|
78
|
+
*/
|
|
79
|
+
export declare function buildSuggestions(target: HintTarget, aria: AriaCandidate[], dom: DomFacts): Suggestion[];
|
|
80
|
+
/**
|
|
81
|
+
* Pull `- button "Save"` lines out of an ARIA snapshot.
|
|
82
|
+
*
|
|
83
|
+
* The snapshot is YAML, but only its leading token carries what we need, so
|
|
84
|
+
* this reads it line by line rather than pulling in a parser. A line we do not
|
|
85
|
+
* recognise is skipped — the worst case is one fewer suggestion.
|
|
86
|
+
*/
|
|
87
|
+
export declare function parseAriaSnapshot(yaml: string): AriaCandidate[];
|
|
88
|
+
/**
|
|
89
|
+
* The lines to append to a "no element matches" failure — at most
|
|
90
|
+
* {@link MAX_SUGGESTIONS}, each verified against the live page, and empty
|
|
91
|
+
* whenever the page holds nothing close.
|
|
92
|
+
*/
|
|
93
|
+
export declare function nearMissHints(page: Page, target: HintTarget): Promise<string[]>;
|
|
94
|
+
/** The block appended under a failure sentence, or `""` when there is
|
|
95
|
+
* nothing to add. */
|
|
96
|
+
export declare function formatHints(lines: string[]): string;
|