@itmetsam/samurai-framework 0.21.2 → 0.21.3
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 +1 -1
- package/dist/assert/locator-assertions.d.ts +4 -0
- package/dist/assert/locator-assertions.js +10 -2
- package/dist/assert/text-match.d.ts +2 -2
- package/dist/assert/text-match.js +2 -2
- package/dist/browser/dialog.js +4 -1
- package/dist/browser/init-script.d.ts +11 -0
- package/dist/browser/init-script.js +29 -0
- package/dist/browser/page-locators.d.ts +18 -0
- package/dist/browser/page-locators.js +19 -0
- package/dist/browser/page-permissions.d.ts +6 -0
- package/dist/browser/page-permissions.js +15 -0
- package/dist/browser/page-settings.d.ts +3 -0
- package/dist/browser/page-settings.js +4 -0
- package/dist/browser/page.d.ts +29 -13
- package/dist/browser/page.js +46 -45
- package/dist/config/variables.js +2 -1
- package/dist/locator/accessible-name.d.ts +22 -0
- package/dist/locator/accessible-name.js +128 -0
- package/dist/locator/action-timeout-error.js +5 -1
- package/dist/locator/actionability.d.ts +3 -2
- package/dist/locator/actionability.js +4 -4
- package/dist/locator/attribute-locator.d.ts +13 -0
- package/dist/locator/attribute-locator.js +27 -0
- package/dist/locator/chain-resolver.d.ts +12 -3
- package/dist/locator/chain-resolver.js +85 -15
- package/dist/locator/check-actions.d.ts +15 -0
- package/dist/locator/check-actions.js +44 -0
- package/dist/locator/combine-step.d.ts +5 -0
- package/dist/locator/combine-step.js +15 -0
- package/dist/locator/control-probe.d.ts +10 -0
- package/dist/locator/control-probe.js +50 -0
- package/dist/locator/element-access.d.ts +24 -0
- package/dist/locator/element-access.js +53 -0
- package/dist/locator/element-actions.d.ts +17 -0
- package/dist/locator/element-actions.js +51 -0
- package/dist/locator/element-reads.d.ts +29 -4
- package/dist/locator/element-reads.js +83 -2
- package/dist/locator/element-state.d.ts +5 -0
- package/dist/locator/element-state.js +2 -0
- package/dist/locator/file-actions.d.ts +2 -2
- package/dist/locator/filter-step.d.ts +24 -0
- package/dist/locator/filter-step.js +72 -0
- package/dist/locator/label-locator.d.ts +1 -1
- package/dist/locator/locator.d.ts +94 -29
- package/dist/locator/locator.js +183 -151
- package/dist/locator/page-scripts.d.ts +2 -0
- package/dist/locator/page-scripts.js +6 -0
- package/dist/locator/page-search.d.ts +11 -2
- package/dist/locator/page-search.js +15 -3
- package/dist/locator/page-step.d.ts +16 -0
- package/dist/locator/page-step.js +35 -0
- package/dist/locator/role-locator.d.ts +65 -0
- package/dist/locator/role-locator.js +125 -0
- package/dist/locator/role-table.d.ts +18 -0
- package/dist/locator/role-table.js +190 -0
- package/dist/locator/select-actions.d.ts +13 -0
- package/dist/locator/select-actions.js +73 -0
- package/dist/locator/selector-string.d.ts +7 -0
- package/dist/locator/selector-string.js +27 -0
- package/dist/locator/selector.d.ts +47 -15
- package/dist/locator/selector.js +83 -12
- package/dist/locator/strict-mode.d.ts +6 -0
- package/dist/locator/strict-mode.js +6 -0
- package/dist/locator/text-locator.d.ts +1 -1
- package/dist/network/url-match.js +3 -1
- package/dist/types/config.d.ts +7 -0
- package/package.json +1 -1
package/dist/locator/selector.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { buildRoleSelector, isNativeRole, } from "./role-locator.js";
|
|
1
2
|
function nonEmpty(value, what) {
|
|
2
3
|
if (value.trim() === "")
|
|
3
4
|
throw new TypeError(`${what} must not be empty`);
|
|
@@ -9,8 +10,26 @@ export function xpathSelector(value) {
|
|
|
9
10
|
export function cssSelector(value) {
|
|
10
11
|
return { kind: "css", value: nonEmpty(value, "a css selector") };
|
|
11
12
|
}
|
|
12
|
-
function
|
|
13
|
-
|
|
13
|
+
export function nthSelector(index) {
|
|
14
|
+
if (!Number.isInteger(index)) {
|
|
15
|
+
throw new TypeError(`nth(): index must be an integer, got ${index}`);
|
|
16
|
+
}
|
|
17
|
+
return { kind: "nth", index };
|
|
18
|
+
}
|
|
19
|
+
function textMatch(value, what, { exact = true, match, ignoreCase }) {
|
|
20
|
+
if (value instanceof RegExp) {
|
|
21
|
+
// The page tests it as written; g and y would make a repeated test depend on lastIndex
|
|
22
|
+
const plain = new RegExp(value.source, value.flags.replace(/[gy]/g, ""));
|
|
23
|
+
return { value: plain, match: "full", ignoreCase: false };
|
|
24
|
+
}
|
|
25
|
+
if (typeof value !== "string") {
|
|
26
|
+
throw new TypeError(`${what} must be a string or a RegExp`);
|
|
27
|
+
}
|
|
28
|
+
return {
|
|
29
|
+
value: nonEmpty(value, what),
|
|
30
|
+
match: match ?? (exact ? "full" : "partial"),
|
|
31
|
+
ignoreCase: ignoreCase ?? !exact,
|
|
32
|
+
};
|
|
14
33
|
}
|
|
15
34
|
export function textSelector(value, options = {}) {
|
|
16
35
|
return { kind: "text", ...textMatch(value, "the text", options) };
|
|
@@ -18,14 +37,24 @@ export function textSelector(value, options = {}) {
|
|
|
18
37
|
export function labelSelector(value, options = {}) {
|
|
19
38
|
return { kind: "label", ...textMatch(value, "the label", options) };
|
|
20
39
|
}
|
|
40
|
+
const ATTRIBUTE_WHAT = {
|
|
41
|
+
placeholder: "the placeholder",
|
|
42
|
+
alt: "the alt text",
|
|
43
|
+
title: "the title",
|
|
44
|
+
};
|
|
45
|
+
export function attributeSelector(attribute, value, options = {}) {
|
|
46
|
+
return {
|
|
47
|
+
kind: "attribute",
|
|
48
|
+
attribute,
|
|
49
|
+
...textMatch(value, ATTRIBUTE_WHAT[attribute], options),
|
|
50
|
+
};
|
|
51
|
+
}
|
|
21
52
|
export function testIdSelector(value) {
|
|
22
53
|
return { kind: "testid", value: nonEmpty(value, "the test id") };
|
|
23
54
|
}
|
|
24
|
-
export function roleSelector(role,
|
|
55
|
+
export function roleSelector(role, options = {}) {
|
|
25
56
|
nonEmpty(role, "the role");
|
|
26
|
-
return
|
|
27
|
-
? { kind: "role", role }
|
|
28
|
-
: { kind: "role", role, name };
|
|
57
|
+
return buildRoleSelector(role, options);
|
|
29
58
|
}
|
|
30
59
|
/**
|
|
31
60
|
* Normalises an xpath. Relative xpaths get a leading `//` (`.//` when `scoped`, i.e. searching inside
|
|
@@ -48,6 +77,9 @@ export function toBiDiLocator(selector, scoped) {
|
|
|
48
77
|
case "css":
|
|
49
78
|
return { type: "css", value: selector.value };
|
|
50
79
|
case "text":
|
|
80
|
+
if (typeof selector.value !== "string") {
|
|
81
|
+
throw new Error("a RegExp text is resolved by the framework, not by the browser");
|
|
82
|
+
}
|
|
51
83
|
return {
|
|
52
84
|
type: "innerText",
|
|
53
85
|
value: selector.value,
|
|
@@ -55,6 +87,9 @@ export function toBiDiLocator(selector, scoped) {
|
|
|
55
87
|
ignoreCase: selector.ignoreCase,
|
|
56
88
|
};
|
|
57
89
|
case "role":
|
|
90
|
+
if (!isNativeRole(selector) || typeof selector.name === "object") {
|
|
91
|
+
throw new Error("this role search is resolved by the framework, not by the browser");
|
|
92
|
+
}
|
|
58
93
|
return {
|
|
59
94
|
type: "accessibility",
|
|
60
95
|
value: {
|
|
@@ -69,9 +104,14 @@ export function toBiDiLocator(selector, scoped) {
|
|
|
69
104
|
};
|
|
70
105
|
// The locator resolves these two itself, so a chain never hands them to the browser
|
|
71
106
|
case "label":
|
|
72
|
-
|
|
107
|
+
case "attribute":
|
|
108
|
+
throw new Error(`${selector.kind} is resolved by the framework, not by the browser`);
|
|
73
109
|
case "nth":
|
|
74
110
|
throw new Error("nth is resolved by the framework, not by the browser");
|
|
111
|
+
case "filter":
|
|
112
|
+
case "and":
|
|
113
|
+
case "or":
|
|
114
|
+
throw new Error(`${selector.kind} is resolved by the framework, not by the browser`);
|
|
75
115
|
}
|
|
76
116
|
}
|
|
77
117
|
/** A short name for error messages */
|
|
@@ -87,19 +127,50 @@ export function describeSelector(selector, scoped) {
|
|
|
87
127
|
selector.match === "partial" && "partial",
|
|
88
128
|
selector.ignoreCase && "ignoring case",
|
|
89
129
|
].filter(Boolean);
|
|
90
|
-
return `${selector.kind}=${
|
|
130
|
+
return `${selector.kind}=${describeValue(selector.value)}${flags.length ? ` (${flags.join(", ")})` : ""}`;
|
|
131
|
+
}
|
|
132
|
+
case "attribute": {
|
|
133
|
+
const flags = [
|
|
134
|
+
selector.match === "partial" && "partial",
|
|
135
|
+
selector.ignoreCase && "ignoring case",
|
|
136
|
+
].filter(Boolean);
|
|
137
|
+
return `${selector.attribute}=${describeValue(selector.value)}${flags.length ? ` (${flags.join(", ")})` : ""}`;
|
|
91
138
|
}
|
|
92
139
|
case "testid":
|
|
93
140
|
return `testid=${JSON.stringify(selector.value)}`;
|
|
94
141
|
case "role":
|
|
95
|
-
return
|
|
96
|
-
(selector.name === undefined
|
|
97
|
-
? ""
|
|
98
|
-
: `[name=${JSON.stringify(selector.name)}]`));
|
|
142
|
+
return `role=${selector.role}${describeRoleOptions(selector)}`;
|
|
99
143
|
case "nth":
|
|
100
144
|
return `nth=${selector.index}`;
|
|
145
|
+
case "filter":
|
|
146
|
+
return `filter(${describeFilter(selector)})`;
|
|
147
|
+
case "and":
|
|
148
|
+
case "or":
|
|
149
|
+
return `(${describeChain(selector.left)}) ${selector.kind} (${describeChain(selector.right)})`;
|
|
101
150
|
}
|
|
102
151
|
}
|
|
152
|
+
function describeValue(value) {
|
|
153
|
+
return typeof value === "string" ? JSON.stringify(value) : String(value);
|
|
154
|
+
}
|
|
155
|
+
function describeRoleOptions({ kind: _kind, role: _role, name, exact, ...rest }) {
|
|
156
|
+
const parts = [
|
|
157
|
+
name !== undefined && `name=${describeValue(name)}`,
|
|
158
|
+
exact === false && "exact=false",
|
|
159
|
+
...Object.entries(rest).map(([key, value]) => `${key}=${String(value)}`),
|
|
160
|
+
].filter(Boolean);
|
|
161
|
+
return parts.length ? `[${parts.join(", ")}]` : "";
|
|
162
|
+
}
|
|
163
|
+
function describeFilter(tests) {
|
|
164
|
+
return [
|
|
165
|
+
tests.hasText !== undefined && `hasText=${describeValue(tests.hasText)}`,
|
|
166
|
+
tests.hasNotText !== undefined &&
|
|
167
|
+
`hasNotText=${describeValue(tests.hasNotText)}`,
|
|
168
|
+
tests.has && `has=${describeChain(tests.has)}`,
|
|
169
|
+
tests.hasNot && `hasNot=${describeChain(tests.hasNot)}`,
|
|
170
|
+
]
|
|
171
|
+
.filter(Boolean)
|
|
172
|
+
.join(", ");
|
|
173
|
+
}
|
|
103
174
|
export function describeChain(chain) {
|
|
104
175
|
return chain
|
|
105
176
|
.map((selector, index) => describeSelector(selector, index > 0))
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/** Whether a locator throws when it matches several elements; asked when an action or read needs its element */
|
|
2
|
+
export type StrictSource = () => Promise<boolean>;
|
|
3
|
+
/** The default: the first match is used */
|
|
4
|
+
export declare const NOT_STRICT: StrictSource;
|
|
5
|
+
/** The error a strict locator throws for an action or read that found several elements */
|
|
6
|
+
export declare function strictViolation(method: string, count: number, selector: string): Error;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/** The default: the first match is used */
|
|
2
|
+
export const NOT_STRICT = () => Promise.resolve(false);
|
|
3
|
+
/** The error a strict locator throws for an action or read that found several elements */
|
|
4
|
+
export function strictViolation(method, count, selector) {
|
|
5
|
+
return new Error(`${method}(): strict mode violation, ${count} elements match ${selector}`);
|
|
6
|
+
}
|
|
@@ -10,4 +10,4 @@
|
|
|
10
10
|
*
|
|
11
11
|
* A string, so the transpiler cannot inject helpers that do not exist in the page.
|
|
12
12
|
*/
|
|
13
|
-
export declare const TEXT_LOCATE = "(starts, value, match, ignoreCase) => {\n const skipped = new Set([\"SCRIPT\", \"STYLE\", \"NOSCRIPT\", \"TEMPLATE\", \"HEAD\", \"TITLE\", \"META\", \"LINK\"]);\n \n const
|
|
13
|
+
export declare const TEXT_LOCATE = "(starts, value, match, ignoreCase) => {\n const skipped = new Set([\"SCRIPT\", \"STYLE\", \"NOSCRIPT\", \"TEMPLATE\", \"HEAD\", \"TITLE\", \"META\", \"LINK\"]);\n \n const collapse = (text) => String(text).replace(/\\s+/g, \" \").trim();\n const normalise = (text) => {\n const collapsed = collapse(text);\n return ignoreCase ? collapsed.toLowerCase() : collapsed;\n };\n const pattern = typeof value === \"string\" ? null : new RegExp(value.source, value.flags.replace(/[gy]/g, \"\"));\n const wanted = pattern ? \"\" : normalise(value);\n const textMatches = (text) => {\n if (pattern) return pattern.test(collapse(text));\n const normal = normalise(text);\n return match === \"full\" ? normal === wanted : normal.includes(wanted);\n };\n const roots = starts.length > 0 ? starts : [document];\n const seen = new Set();\n const matches = [];\n for (const root of roots) {\n for (const el of root.querySelectorAll(\"*\")) {\n if (seen.has(el) || skipped.has(el.tagName.toUpperCase())) continue;\n seen.add(el);\n if (textMatches(el.innerText ?? el.textContent ?? \"\")) matches.push(el);\n }\n }\n const hasMatchingDescendant = new Set();\n for (const el of matches) {\n for (let parent = el.parentElement; parent; parent = parent.parentElement) hasMatchingDescendant.add(parent);\n }\n return matches\n .filter((el) => !hasMatchingDescendant.has(el))\n .sort((a, b) => (a === b ? 0 : a.compareDocumentPosition(b) & 4 ? -1 : 1));\n}";
|
|
@@ -24,7 +24,9 @@ function globToRegExp(glob) {
|
|
|
24
24
|
/** Whether `url` matches `pattern` */
|
|
25
25
|
export function matchUrl(pattern, url) {
|
|
26
26
|
if (typeof pattern === "string") {
|
|
27
|
-
return isExactString(pattern)
|
|
27
|
+
return isExactString(pattern)
|
|
28
|
+
? pattern === url
|
|
29
|
+
: globToRegExp(pattern).test(url);
|
|
28
30
|
}
|
|
29
31
|
// A global or sticky RegExp remembers where it stopped; every match must start fresh
|
|
30
32
|
pattern.lastIndex = 0;
|
package/dist/types/config.d.ts
CHANGED
|
@@ -142,6 +142,13 @@ export interface SamuraiTestConfig {
|
|
|
142
142
|
* Left out, a run is headless when the `CI` environment variable is set and shows a window otherwise
|
|
143
143
|
*/
|
|
144
144
|
headless?: boolean;
|
|
145
|
+
/**
|
|
146
|
+
* Make an action or read on a locator that matches several elements throw
|
|
147
|
+
* `name(): strict mode violation, N elements match ...` instead of using the first one. Narrow the locator
|
|
148
|
+
* (`first()`, `nth()`, `filter()`) or turn it off for one locator with `locator.strict(false)`
|
|
149
|
+
* @default false
|
|
150
|
+
*/
|
|
151
|
+
strictSelectors?: boolean;
|
|
145
152
|
};
|
|
146
153
|
/** What to do with the browser's console output and uncaught page exceptions */
|
|
147
154
|
logs?: {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@itmetsam/samurai-framework",
|
|
3
|
-
"version": "0.21.
|
|
3
|
+
"version": "0.21.3",
|
|
4
4
|
"description": "Browser test framework over WebDriver BiDi: auto-waiting locators, environments and secrets, a recorder, and a command line.",
|
|
5
5
|
"main": "dist/api.js",
|
|
6
6
|
"exports": {
|