@testspectra/matchers 1.1.11-rc.5 → 1.1.13
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/__tests__/matcher-contracts.test.d.ts +1 -0
- package/dist/__tests__/matcher-contracts.test.js +498 -0
- package/dist/contract.d.ts +5 -42
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/matchers.d.ts +10 -3
- package/dist/matchers.js +576 -135
- package/dist/proto.d.ts +1 -0
- package/dist/reporter.js +13 -1
- package/dist/runner/collection.js +32 -6
- package/dist/runner/single.d.ts +2 -2
- package/dist/runner/single.js +24 -7
- package/dist/semantic.d.ts +11 -0
- package/dist/semantic.js +141 -0
- package/dist/types.d.ts +34 -1
- package/dist/worker_config.d.ts +97 -0
- package/dist/worker_config.js +17 -0
- package/package.json +1 -1
- package/src/contract.ts +5 -36
- package/src/index.ts +1 -0
- package/src/proto.ts +2 -0
- package/src/types.ts +37 -0
- package/src/worker_config.ts +102 -0
- package/testspectra-matchers-1.1.13.tgz +0 -0
- package/tsconfig.json +1 -1
- package/src/runtime/assertions.ts +0 -454
- package/src/runtime/element_actions.ts +0 -169
- package/src/runtime/element_proxy.ts +0 -199
- package/src/runtime/element_state.ts +0 -94
- package/src/runtime/spectra.ts +0 -110
- package/testspectra-matchers-1.1.11-rc.5.tgz +0 -0
package/dist/matchers.js
CHANGED
|
@@ -5,16 +5,31 @@
|
|
|
5
5
|
* @param target - Selector string or element instance.
|
|
6
6
|
* @returns Resolved WebdriverIO.Element instance.
|
|
7
7
|
*/
|
|
8
|
+
/**
|
|
9
|
+
* Resolves an ElementTarget (selector string, WebdriverIO.Element, Playwright Locator, or ChainablePromiseElement)
|
|
10
|
+
* to an actionable element instance.
|
|
11
|
+
*
|
|
12
|
+
* @param target - Selector string or element instance.
|
|
13
|
+
* @returns Resolved element or locator instance.
|
|
14
|
+
*/
|
|
8
15
|
export async function resolveElement(target) {
|
|
9
16
|
if (typeof target === 'string') {
|
|
10
|
-
|
|
11
|
-
|
|
17
|
+
if (typeof globalThis.$ === 'function') {
|
|
18
|
+
const el = await globalThis.$(target);
|
|
19
|
+
return el;
|
|
20
|
+
}
|
|
21
|
+
const b = globalThis.browser;
|
|
22
|
+
if (b && typeof b.$ === 'function') {
|
|
23
|
+
const el = await b.$(target);
|
|
24
|
+
return el;
|
|
25
|
+
}
|
|
26
|
+
throw new Error(`Cannot resolve selector "${target}": No global '$' or browser context available.`);
|
|
12
27
|
}
|
|
13
28
|
const el = await target;
|
|
14
29
|
return el;
|
|
15
30
|
}
|
|
16
31
|
/**
|
|
17
|
-
* Executes a single element matcher against the
|
|
32
|
+
* Executes a single element matcher against the active runner context (Playwright or WebdriverIO).
|
|
18
33
|
*
|
|
19
34
|
* @param target - Optional element target (not required for browser-level assertions).
|
|
20
35
|
* @param matcher - Single element matcher string.
|
|
@@ -22,151 +37,531 @@ export async function resolveElement(target) {
|
|
|
22
37
|
*/
|
|
23
38
|
export async function executeSingleMatcher(target, matcher, args) {
|
|
24
39
|
const isBrowserLevel = matcher === 'have.url' || matcher === 'contain.url' || matcher === 'have.title' || matcher === 'contain.title';
|
|
40
|
+
const b = typeof browser !== 'undefined' ? browser : globalThis.browser;
|
|
25
41
|
if (isBrowserLevel) {
|
|
26
42
|
const expected = args[0];
|
|
43
|
+
const exp = typeof expect !== 'undefined' ? expect : globalThis.expect;
|
|
44
|
+
if (exp && typeof exp(b)?.toHaveUrl === 'function') {
|
|
45
|
+
switch (matcher) {
|
|
46
|
+
case 'have.url':
|
|
47
|
+
await exp(b).toHaveUrl(expected);
|
|
48
|
+
break;
|
|
49
|
+
case 'contain.url':
|
|
50
|
+
await exp(b).toHaveUrl(exp.stringContaining
|
|
51
|
+
? exp.stringContaining(expected)
|
|
52
|
+
: new RegExp(String(expected).replace(/[.*+?^${}()|[\]\\]/g, '\\$&')));
|
|
53
|
+
break;
|
|
54
|
+
case 'have.title':
|
|
55
|
+
await exp(b).toHaveTitle(expected);
|
|
56
|
+
break;
|
|
57
|
+
case 'contain.title':
|
|
58
|
+
await exp(b).toHaveTitle(exp.stringContaining
|
|
59
|
+
? exp.stringContaining(expected)
|
|
60
|
+
: new RegExp(String(expected).replace(/[.*+?^${}()|[\]\\]/g, '\\$&')));
|
|
61
|
+
break;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
else {
|
|
65
|
+
// Direct polling fallback (works with Playwright Page or generic browser)
|
|
66
|
+
const currentUrl = b?.getUrl
|
|
67
|
+
? await b.getUrl()
|
|
68
|
+
: b?.url
|
|
69
|
+
? typeof b.url === 'function'
|
|
70
|
+
? await b.url()
|
|
71
|
+
: b.url
|
|
72
|
+
: '';
|
|
73
|
+
const currentTitle = b?.getTitle
|
|
74
|
+
? await b.getTitle()
|
|
75
|
+
: b?.title
|
|
76
|
+
? typeof b.title === 'function'
|
|
77
|
+
? await b.title()
|
|
78
|
+
: b.title
|
|
79
|
+
: '';
|
|
80
|
+
if (matcher === 'have.url' && currentUrl !== expected) {
|
|
81
|
+
throw new Error(`Expected URL to be "${expected}", but got "${currentUrl}"`);
|
|
82
|
+
}
|
|
83
|
+
if (matcher === 'contain.url' && !currentUrl.includes(expected)) {
|
|
84
|
+
throw new Error(`Expected URL to contain "${expected}", but got "${currentUrl}"`);
|
|
85
|
+
}
|
|
86
|
+
if (matcher === 'have.title' && currentTitle !== expected) {
|
|
87
|
+
throw new Error(`Expected title to be "${expected}", but got "${currentTitle}"`);
|
|
88
|
+
}
|
|
89
|
+
if (matcher === 'contain.title' && !currentTitle.includes(expected)) {
|
|
90
|
+
throw new Error(`Expected title to contain "${expected}", but got "${currentTitle}"`);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
if (!target) {
|
|
96
|
+
throw new Error(`Assertion matcher '${matcher}' requires an element target.`);
|
|
97
|
+
}
|
|
98
|
+
const el = await resolveElement(target);
|
|
99
|
+
const exp = typeof expect !== 'undefined' ? expect : globalThis.expect;
|
|
100
|
+
// Branch 1: WebdriverIO expect-webdriverio
|
|
101
|
+
if (exp && typeof exp(el)?.toBeDisplayed === 'function') {
|
|
27
102
|
switch (matcher) {
|
|
28
|
-
case '
|
|
29
|
-
await
|
|
103
|
+
case 'be.visible':
|
|
104
|
+
await exp(el).toBeDisplayed();
|
|
105
|
+
break;
|
|
106
|
+
case 'not.be.visible':
|
|
107
|
+
await exp(el).not.toBeDisplayed();
|
|
108
|
+
break;
|
|
109
|
+
case 'exist':
|
|
110
|
+
await exp(el).toExist();
|
|
111
|
+
break;
|
|
112
|
+
case 'not.exist':
|
|
113
|
+
await exp(el).not.toExist();
|
|
114
|
+
break;
|
|
115
|
+
case 'be.clickable':
|
|
116
|
+
await exp(el).toBeClickable();
|
|
117
|
+
break;
|
|
118
|
+
case 'not.be.clickable':
|
|
119
|
+
await exp(el).not.toBeClickable();
|
|
120
|
+
break;
|
|
121
|
+
case 'be.enabled':
|
|
122
|
+
await exp(el).toBeEnabled();
|
|
123
|
+
break;
|
|
124
|
+
case 'be.disabled':
|
|
125
|
+
await exp(el).toBeDisabled();
|
|
126
|
+
break;
|
|
127
|
+
case 'be.checked':
|
|
128
|
+
case 'be.selected':
|
|
129
|
+
await exp(el).toBeSelected();
|
|
130
|
+
break;
|
|
131
|
+
case 'not.be.checked':
|
|
132
|
+
case 'not.be.selected':
|
|
133
|
+
await exp(el).not.toBeSelected();
|
|
134
|
+
break;
|
|
135
|
+
case 'be.focused':
|
|
136
|
+
await exp(el).toBeFocused();
|
|
137
|
+
break;
|
|
138
|
+
case 'not.be.focused':
|
|
139
|
+
await exp(el).not.toBeFocused();
|
|
140
|
+
break;
|
|
141
|
+
case 'have.value':
|
|
142
|
+
await exp(el).toHaveValue(args[0]);
|
|
143
|
+
break;
|
|
144
|
+
case 'not.have.value':
|
|
145
|
+
await exp(el).not.toHaveValue(args[0]);
|
|
146
|
+
break;
|
|
147
|
+
case 'contain.value':
|
|
148
|
+
await exp(el).toHaveValue(exp.stringContaining
|
|
149
|
+
? exp.stringContaining(args[0])
|
|
150
|
+
: new RegExp(String(args[0]).replace(/[.*+?^${}()|[\]\\]/g, '\\$&')));
|
|
151
|
+
break;
|
|
152
|
+
case 'not.contain.value':
|
|
153
|
+
await exp(el).not.toHaveValue(exp.stringContaining
|
|
154
|
+
? exp.stringContaining(args[0])
|
|
155
|
+
: new RegExp(String(args[0]).replace(/[.*+?^${}()|[\]\\]/g, '\\$&')));
|
|
156
|
+
break;
|
|
157
|
+
case 'have.text':
|
|
158
|
+
await exp(el).toHaveText(args[0]);
|
|
159
|
+
break;
|
|
160
|
+
case 'not.have.text':
|
|
161
|
+
await exp(el).not.toHaveText(args[0]);
|
|
162
|
+
break;
|
|
163
|
+
case 'contain.text':
|
|
164
|
+
await exp(el).toHaveText(exp.stringContaining
|
|
165
|
+
? exp.stringContaining(args[0])
|
|
166
|
+
: new RegExp(String(args[0]).replace(/[.*+?^${}()|[\]\\]/g, '\\$&')));
|
|
167
|
+
break;
|
|
168
|
+
case 'not.contain.text':
|
|
169
|
+
await exp(el).not.toHaveText(exp.stringContaining
|
|
170
|
+
? exp.stringContaining(args[0])
|
|
171
|
+
: new RegExp(String(args[0]).replace(/[.*+?^${}()|[\]\\]/g, '\\$&')));
|
|
172
|
+
break;
|
|
173
|
+
case 'have.class':
|
|
174
|
+
await exp(el).toHaveElementClass(args[0]);
|
|
175
|
+
break;
|
|
176
|
+
case 'not.have.class':
|
|
177
|
+
await exp(el).not.toHaveElementClass(args[0]);
|
|
178
|
+
break;
|
|
179
|
+
case 'have.attr':
|
|
180
|
+
if (args.length >= 2)
|
|
181
|
+
await exp(el).toHaveAttribute(args[0], args[1]);
|
|
182
|
+
else
|
|
183
|
+
await exp(el).toHaveAttribute(args[0]);
|
|
30
184
|
break;
|
|
31
|
-
case '
|
|
32
|
-
await
|
|
185
|
+
case 'not.have.attr':
|
|
186
|
+
await exp(el).not.toHaveAttribute(args[0]);
|
|
33
187
|
break;
|
|
34
|
-
case 'have.
|
|
35
|
-
await
|
|
188
|
+
case 'have.css':
|
|
189
|
+
await exp(el).toHaveStyle({ [args[0]]: args[1] });
|
|
36
190
|
break;
|
|
37
|
-
case '
|
|
38
|
-
await
|
|
191
|
+
case 'not.have.css':
|
|
192
|
+
await exp(el).not.toHaveStyle({ [args[0]]: args[1] });
|
|
39
193
|
break;
|
|
194
|
+
default:
|
|
195
|
+
throw new Error(`Unsupported matcher: ${matcher}`);
|
|
40
196
|
}
|
|
41
197
|
return;
|
|
42
198
|
}
|
|
43
|
-
|
|
44
|
-
|
|
199
|
+
// Branch 2: Playwright expect(locator)
|
|
200
|
+
if (exp && typeof exp(el)?.toBeVisible === 'function') {
|
|
201
|
+
switch (matcher) {
|
|
202
|
+
case 'be.visible':
|
|
203
|
+
await exp(el).toBeVisible();
|
|
204
|
+
break;
|
|
205
|
+
case 'not.be.visible':
|
|
206
|
+
await exp(el).not.toBeVisible();
|
|
207
|
+
break;
|
|
208
|
+
case 'exist':
|
|
209
|
+
await exp(el).toBeAttached();
|
|
210
|
+
break;
|
|
211
|
+
case 'not.exist':
|
|
212
|
+
await exp(el).not.toBeAttached();
|
|
213
|
+
break;
|
|
214
|
+
case 'be.clickable':
|
|
215
|
+
case 'be.enabled':
|
|
216
|
+
await exp(el).toBeEnabled();
|
|
217
|
+
break;
|
|
218
|
+
case 'not.be.clickable':
|
|
219
|
+
case 'be.disabled':
|
|
220
|
+
await exp(el).toBeDisabled();
|
|
221
|
+
break;
|
|
222
|
+
case 'be.checked':
|
|
223
|
+
case 'be.selected':
|
|
224
|
+
await exp(el).toBeChecked();
|
|
225
|
+
break;
|
|
226
|
+
case 'not.be.checked':
|
|
227
|
+
case 'not.be.selected':
|
|
228
|
+
await exp(el).not.toBeChecked();
|
|
229
|
+
break;
|
|
230
|
+
case 'be.focused':
|
|
231
|
+
await exp(el).toBeFocused();
|
|
232
|
+
break;
|
|
233
|
+
case 'not.be.focused':
|
|
234
|
+
await exp(el).not.toBeFocused();
|
|
235
|
+
break;
|
|
236
|
+
case 'have.value':
|
|
237
|
+
await exp(el).toHaveValue(args[0]);
|
|
238
|
+
break;
|
|
239
|
+
case 'not.have.value':
|
|
240
|
+
await exp(el).not.toHaveValue(args[0]);
|
|
241
|
+
break;
|
|
242
|
+
case 'contain.value':
|
|
243
|
+
await exp(el).toHaveValue(new RegExp(String(args[0]).replace(/[.*+?^${}()|[\]\\]/g, '\\$&')));
|
|
244
|
+
break;
|
|
245
|
+
case 'not.contain.value':
|
|
246
|
+
await exp(el).not.toHaveValue(new RegExp(String(args[0]).replace(/[.*+?^${}()|[\]\\]/g, '\\$&')));
|
|
247
|
+
break;
|
|
248
|
+
case 'have.text':
|
|
249
|
+
await exp(el).toHaveText(args[0]);
|
|
250
|
+
break;
|
|
251
|
+
case 'not.have.text':
|
|
252
|
+
await exp(el).not.toHaveText(args[0]);
|
|
253
|
+
break;
|
|
254
|
+
case 'contain.text':
|
|
255
|
+
await exp(el).toContainText(args[0]);
|
|
256
|
+
break;
|
|
257
|
+
case 'not.contain.text':
|
|
258
|
+
await exp(el).not.toContainText(args[0]);
|
|
259
|
+
break;
|
|
260
|
+
case 'have.class':
|
|
261
|
+
await exp(el).toHaveClass(new RegExp(`(^|\\s)${String(args[0]).replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}(\\s|$)`));
|
|
262
|
+
break;
|
|
263
|
+
case 'not.have.class':
|
|
264
|
+
await exp(el).not.toHaveClass(new RegExp(`(^|\\s)${String(args[0]).replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}(\\s|$)`));
|
|
265
|
+
break;
|
|
266
|
+
case 'have.attr':
|
|
267
|
+
if (args.length >= 2)
|
|
268
|
+
await exp(el).toHaveAttribute(args[0], args[1]);
|
|
269
|
+
else
|
|
270
|
+
await exp(el).toHaveAttribute(args[0], /.*/);
|
|
271
|
+
break;
|
|
272
|
+
case 'not.have.attr':
|
|
273
|
+
await exp(el).not.toHaveAttribute(args[0], /.*/);
|
|
274
|
+
break;
|
|
275
|
+
case 'have.css':
|
|
276
|
+
await exp(el).toHaveCSS(args[0], args[1]);
|
|
277
|
+
break;
|
|
278
|
+
case 'not.have.css':
|
|
279
|
+
await exp(el).not.toHaveCSS(args[0], args[1]);
|
|
280
|
+
break;
|
|
281
|
+
default:
|
|
282
|
+
throw new Error(`Unsupported matcher: ${matcher}`);
|
|
283
|
+
}
|
|
284
|
+
return;
|
|
45
285
|
}
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
break;
|
|
57
|
-
case 'not.exist':
|
|
58
|
-
await expect(el).not.toExist();
|
|
59
|
-
break;
|
|
60
|
-
case 'be.clickable':
|
|
61
|
-
await expect(el).toBeClickable();
|
|
62
|
-
break;
|
|
63
|
-
case 'not.be.clickable':
|
|
64
|
-
await expect(el).not.toBeClickable();
|
|
65
|
-
break;
|
|
66
|
-
case 'be.enabled':
|
|
67
|
-
await expect(el).toBeEnabled();
|
|
68
|
-
break;
|
|
69
|
-
case 'be.disabled':
|
|
70
|
-
await expect(el).toBeDisabled();
|
|
71
|
-
break;
|
|
72
|
-
case 'be.checked':
|
|
73
|
-
case 'be.selected':
|
|
74
|
-
await expect(el).toBeSelected();
|
|
75
|
-
break;
|
|
76
|
-
case 'not.be.checked':
|
|
77
|
-
case 'not.be.selected':
|
|
78
|
-
await expect(el).not.toBeSelected();
|
|
79
|
-
break;
|
|
80
|
-
case 'be.focused':
|
|
81
|
-
await expect(el).toBeFocused();
|
|
82
|
-
break;
|
|
83
|
-
case 'not.be.focused':
|
|
84
|
-
await expect(el).not.toBeFocused();
|
|
85
|
-
break;
|
|
86
|
-
case 'have.value':
|
|
87
|
-
await expect(el).toHaveValue(args[0]);
|
|
88
|
-
break;
|
|
89
|
-
case 'not.have.value':
|
|
90
|
-
await expect(el).not.toHaveValue(args[0]);
|
|
91
|
-
break;
|
|
92
|
-
case 'contain.value':
|
|
93
|
-
await expect(el).toHaveValue(expect.stringContaining(args[0]));
|
|
94
|
-
break;
|
|
95
|
-
case 'not.contain.value':
|
|
96
|
-
await expect(el).not.toHaveValue(expect.stringContaining(args[0]));
|
|
97
|
-
break;
|
|
98
|
-
case 'have.text':
|
|
99
|
-
await expect(el).toHaveText(args[0]);
|
|
100
|
-
break;
|
|
101
|
-
case 'not.have.text':
|
|
102
|
-
await expect(el).not.toHaveText(args[0]);
|
|
103
|
-
break;
|
|
104
|
-
case 'contain.text':
|
|
105
|
-
await expect(el).toHaveText(expect.stringContaining(args[0]));
|
|
106
|
-
break;
|
|
107
|
-
case 'not.contain.text':
|
|
108
|
-
await expect(el).not.toHaveText(expect.stringContaining(args[0]));
|
|
109
|
-
break;
|
|
110
|
-
case 'have.class':
|
|
111
|
-
await expect(el).toHaveElementClass(args[0]);
|
|
112
|
-
break;
|
|
113
|
-
case 'not.have.class':
|
|
114
|
-
await expect(el).not.toHaveElementClass(args[0]);
|
|
115
|
-
break;
|
|
116
|
-
case 'have.attr':
|
|
117
|
-
if (args.length >= 2) {
|
|
118
|
-
await expect(el).toHaveAttribute(args[0], args[1]);
|
|
286
|
+
// Branch 3: Universal fallback evaluation with auto-waiting polling
|
|
287
|
+
const passed = await pollUntil(async () => {
|
|
288
|
+
const currentEl = typeof target === 'string' ? await resolveElement(target) : el;
|
|
289
|
+
switch (matcher) {
|
|
290
|
+
case 'be.visible': {
|
|
291
|
+
return typeof currentEl.isDisplayed === 'function'
|
|
292
|
+
? await currentEl.isDisplayed()
|
|
293
|
+
: typeof currentEl.isVisible === 'function'
|
|
294
|
+
? await currentEl.isVisible()
|
|
295
|
+
: true;
|
|
119
296
|
}
|
|
120
|
-
|
|
121
|
-
|
|
297
|
+
case 'not.be.visible': {
|
|
298
|
+
const v = typeof currentEl.isDisplayed === 'function'
|
|
299
|
+
? await currentEl.isDisplayed()
|
|
300
|
+
: typeof currentEl.isVisible === 'function'
|
|
301
|
+
? await currentEl.isVisible()
|
|
302
|
+
: false;
|
|
303
|
+
return !v;
|
|
122
304
|
}
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
305
|
+
case 'exist': {
|
|
306
|
+
if (typeof currentEl.isExisting === 'function')
|
|
307
|
+
return await currentEl.isExisting();
|
|
308
|
+
if (typeof currentEl.count === 'function')
|
|
309
|
+
return (await currentEl.count()) > 0;
|
|
310
|
+
if (currentEl.locator?.count)
|
|
311
|
+
return (await currentEl.locator.count()) > 0;
|
|
312
|
+
return true;
|
|
313
|
+
}
|
|
314
|
+
case 'not.exist': {
|
|
315
|
+
if (typeof currentEl.isExisting === 'function')
|
|
316
|
+
return !(await currentEl.isExisting());
|
|
317
|
+
if (typeof currentEl.count === 'function')
|
|
318
|
+
return (await currentEl.count()) === 0;
|
|
319
|
+
if (currentEl.locator?.count)
|
|
320
|
+
return (await currentEl.locator.count()) === 0;
|
|
321
|
+
return false;
|
|
322
|
+
}
|
|
323
|
+
case 'be.clickable':
|
|
324
|
+
case 'be.enabled': {
|
|
325
|
+
return typeof currentEl.isEnabled === 'function' ? await currentEl.isEnabled() : true;
|
|
326
|
+
}
|
|
327
|
+
case 'not.be.clickable':
|
|
328
|
+
case 'be.disabled': {
|
|
329
|
+
return typeof currentEl.isEnabled === 'function' ? !(await currentEl.isEnabled()) : false;
|
|
330
|
+
}
|
|
331
|
+
case 'be.checked':
|
|
332
|
+
case 'be.selected': {
|
|
333
|
+
return typeof currentEl.isSelected === 'function'
|
|
334
|
+
? await currentEl.isSelected()
|
|
335
|
+
: typeof currentEl.isChecked === 'function'
|
|
336
|
+
? await currentEl.isChecked()
|
|
337
|
+
: false;
|
|
338
|
+
}
|
|
339
|
+
case 'not.be.checked':
|
|
340
|
+
case 'not.be.selected': {
|
|
341
|
+
const sel = typeof currentEl.isSelected === 'function'
|
|
342
|
+
? await currentEl.isSelected()
|
|
343
|
+
: typeof currentEl.isChecked === 'function'
|
|
344
|
+
? await currentEl.isChecked()
|
|
345
|
+
: true;
|
|
346
|
+
return !sel;
|
|
347
|
+
}
|
|
348
|
+
case 'be.focused': {
|
|
349
|
+
return typeof currentEl.isFocused === 'function' ? await currentEl.isFocused() : false;
|
|
350
|
+
}
|
|
351
|
+
case 'not.be.focused': {
|
|
352
|
+
return typeof currentEl.isFocused === 'function' ? !(await currentEl.isFocused()) : true;
|
|
353
|
+
}
|
|
354
|
+
case 'have.value': {
|
|
355
|
+
const val = typeof currentEl.getValue === 'function'
|
|
356
|
+
? await currentEl.getValue()
|
|
357
|
+
: typeof currentEl.inputValue === 'function'
|
|
358
|
+
? await currentEl.inputValue()
|
|
359
|
+
: '';
|
|
360
|
+
return val === args[0];
|
|
361
|
+
}
|
|
362
|
+
case 'not.have.value': {
|
|
363
|
+
const val = typeof currentEl.getValue === 'function'
|
|
364
|
+
? await currentEl.getValue()
|
|
365
|
+
: typeof currentEl.inputValue === 'function'
|
|
366
|
+
? await currentEl.inputValue()
|
|
367
|
+
: '';
|
|
368
|
+
return val !== args[0];
|
|
369
|
+
}
|
|
370
|
+
case 'contain.value': {
|
|
371
|
+
const val = typeof currentEl.getValue === 'function'
|
|
372
|
+
? await currentEl.getValue()
|
|
373
|
+
: typeof currentEl.inputValue === 'function'
|
|
374
|
+
? await currentEl.inputValue()
|
|
375
|
+
: '';
|
|
376
|
+
return String(val).includes(args[0]);
|
|
377
|
+
}
|
|
378
|
+
case 'not.contain.value': {
|
|
379
|
+
const val = typeof currentEl.getValue === 'function'
|
|
380
|
+
? await currentEl.getValue()
|
|
381
|
+
: typeof currentEl.inputValue === 'function'
|
|
382
|
+
? await currentEl.inputValue()
|
|
383
|
+
: '';
|
|
384
|
+
return !String(val).includes(args[0]);
|
|
385
|
+
}
|
|
386
|
+
case 'have.text': {
|
|
387
|
+
const text = typeof currentEl.getText === 'function'
|
|
388
|
+
? await currentEl.getText()
|
|
389
|
+
: typeof currentEl.textContent === 'function'
|
|
390
|
+
? await currentEl.textContent()
|
|
391
|
+
: '';
|
|
392
|
+
return text.trim() === String(args[0]).trim();
|
|
393
|
+
}
|
|
394
|
+
case 'not.have.text': {
|
|
395
|
+
const text = typeof currentEl.getText === 'function'
|
|
396
|
+
? await currentEl.getText()
|
|
397
|
+
: typeof currentEl.textContent === 'function'
|
|
398
|
+
? await currentEl.textContent()
|
|
399
|
+
: '';
|
|
400
|
+
return text.trim() !== String(args[0]).trim();
|
|
401
|
+
}
|
|
402
|
+
case 'contain.text': {
|
|
403
|
+
const text = typeof currentEl.getText === 'function'
|
|
404
|
+
? await currentEl.getText()
|
|
405
|
+
: typeof currentEl.textContent === 'function'
|
|
406
|
+
? await currentEl.textContent()
|
|
407
|
+
: '';
|
|
408
|
+
return text.includes(String(args[0]));
|
|
409
|
+
}
|
|
410
|
+
case 'not.contain.text': {
|
|
411
|
+
const text = typeof currentEl.getText === 'function'
|
|
412
|
+
? await currentEl.getText()
|
|
413
|
+
: typeof currentEl.textContent === 'function'
|
|
414
|
+
? await currentEl.textContent()
|
|
415
|
+
: '';
|
|
416
|
+
return !text.includes(String(args[0]));
|
|
417
|
+
}
|
|
418
|
+
case 'have.class': {
|
|
419
|
+
const cls = typeof currentEl.getAttribute === 'function' ? (await currentEl.getAttribute('class')) || '' : '';
|
|
420
|
+
return cls.split(/\s+/).includes(args[0]);
|
|
421
|
+
}
|
|
422
|
+
case 'not.have.class': {
|
|
423
|
+
const cls = typeof currentEl.getAttribute === 'function' ? (await currentEl.getAttribute('class')) || '' : '';
|
|
424
|
+
return !cls.split(/\s+/).includes(args[0]);
|
|
425
|
+
}
|
|
426
|
+
case 'have.attr': {
|
|
427
|
+
const attrVal = typeof currentEl.getAttribute === 'function' ? await currentEl.getAttribute(args[0]) : null;
|
|
428
|
+
if (args.length >= 2)
|
|
429
|
+
return attrVal === args[1];
|
|
430
|
+
return attrVal !== null;
|
|
431
|
+
}
|
|
432
|
+
case 'not.have.attr': {
|
|
433
|
+
const attrVal = typeof currentEl.getAttribute === 'function' ? await currentEl.getAttribute(args[0]) : null;
|
|
434
|
+
if (args.length >= 2)
|
|
435
|
+
return attrVal !== args[1];
|
|
436
|
+
return attrVal === null;
|
|
437
|
+
}
|
|
438
|
+
case 'have.css': {
|
|
439
|
+
const cssVal = typeof currentEl.getCSSProperty === 'function'
|
|
440
|
+
? (await currentEl.getCSSProperty(args[0]))?.value
|
|
441
|
+
: typeof currentEl.evaluate === 'function'
|
|
442
|
+
? await currentEl.evaluate((node, prop) => window.getComputedStyle(node).getPropertyValue(prop), args[0])
|
|
443
|
+
: null;
|
|
444
|
+
if (!cssVal)
|
|
445
|
+
return false;
|
|
446
|
+
const norm = (v) => String(v ?? '')
|
|
447
|
+
.replace(/\s+/g, '')
|
|
448
|
+
.replace(/rgba\((.+?),1\)/, 'rgb($1)')
|
|
449
|
+
.toLowerCase();
|
|
450
|
+
return norm(cssVal) === norm(args[1]);
|
|
451
|
+
}
|
|
452
|
+
case 'not.have.css': {
|
|
453
|
+
const cssVal = typeof currentEl.getCSSProperty === 'function'
|
|
454
|
+
? (await currentEl.getCSSProperty(args[0]))?.value
|
|
455
|
+
: typeof currentEl.evaluate === 'function'
|
|
456
|
+
? await currentEl.evaluate((node, prop) => window.getComputedStyle(node).getPropertyValue(prop), args[0])
|
|
457
|
+
: null;
|
|
458
|
+
if (!cssVal)
|
|
459
|
+
return true;
|
|
460
|
+
const norm = (v) => String(v ?? '')
|
|
461
|
+
.replace(/\s+/g, '')
|
|
462
|
+
.replace(/rgba\((.+?),1\)/, 'rgb($1)')
|
|
463
|
+
.toLowerCase();
|
|
464
|
+
return norm(cssVal) !== norm(args[1]);
|
|
465
|
+
}
|
|
466
|
+
default:
|
|
467
|
+
throw new Error(`Unsupported matcher: ${matcher}`);
|
|
468
|
+
}
|
|
469
|
+
}, 5000);
|
|
470
|
+
if (!passed) {
|
|
471
|
+
throw new Error(`Assertion matcher '${matcher}' failed for target within 5000ms timeout.`);
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
async function pollUntil(condition, timeout = 5000, interval = 50) {
|
|
475
|
+
const start = Date.now();
|
|
476
|
+
while (Date.now() - start < timeout) {
|
|
477
|
+
try {
|
|
478
|
+
if (await condition())
|
|
479
|
+
return true;
|
|
480
|
+
}
|
|
481
|
+
catch {
|
|
482
|
+
// ignore and retry
|
|
483
|
+
}
|
|
484
|
+
await new Promise((r) => setTimeout(r, interval));
|
|
485
|
+
}
|
|
486
|
+
try {
|
|
487
|
+
return await condition();
|
|
488
|
+
}
|
|
489
|
+
catch {
|
|
490
|
+
return false;
|
|
135
491
|
}
|
|
136
492
|
}
|
|
137
493
|
/**
|
|
138
|
-
* Executes a multi-element collection matcher against
|
|
494
|
+
* Executes a multi-element collection matcher against $$.
|
|
139
495
|
*
|
|
140
496
|
* @param selector - CSS/XPath selector for the collection.
|
|
141
497
|
* @param matcher - Multi-element matcher string.
|
|
142
498
|
* @param args - Expected values or count limits.
|
|
143
499
|
*/
|
|
144
500
|
export async function executeMultiMatcher(selector, matcher, args) {
|
|
145
|
-
const elements = await
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
501
|
+
const elements = typeof globalThis.$$ === 'function' ? await globalThis.$$(selector) : [];
|
|
502
|
+
const exp = typeof expect !== 'undefined' ? expect : globalThis.expect;
|
|
503
|
+
if (exp && typeof exp(elements)?.toBeElementsArrayOfSize === 'function') {
|
|
504
|
+
switch (matcher) {
|
|
505
|
+
case 'have.length':
|
|
506
|
+
await exp(elements).toBeElementsArrayOfSize(args[0]);
|
|
507
|
+
break;
|
|
508
|
+
case 'not.have.length':
|
|
509
|
+
await exp(elements).not.toBeElementsArrayOfSize(args[0]);
|
|
510
|
+
break;
|
|
511
|
+
case 'have.length.greaterThan':
|
|
512
|
+
await exp(elements).toBeElementsArrayOfSize({ gte: args[0] + 1 });
|
|
513
|
+
break;
|
|
514
|
+
case 'have.length.lessThan':
|
|
515
|
+
await exp(elements).toBeElementsArrayOfSize({ lte: Math.max(0, args[0] - 1) });
|
|
516
|
+
break;
|
|
517
|
+
case 'be.empty':
|
|
518
|
+
await exp(elements).toBeElementsArrayOfSize(0);
|
|
519
|
+
break;
|
|
520
|
+
case 'not.be.empty':
|
|
521
|
+
await exp(elements).toBeElementsArrayOfSize({ gte: 1 });
|
|
522
|
+
break;
|
|
523
|
+
case 'exist':
|
|
524
|
+
await exp(elements).toBeElementsArrayOfSize({ gte: 1 });
|
|
525
|
+
break;
|
|
526
|
+
default:
|
|
527
|
+
throw new Error(`Unsupported multi-element matcher: ${matcher}`);
|
|
528
|
+
}
|
|
529
|
+
return;
|
|
530
|
+
}
|
|
531
|
+
// Playwright Locator / Array length fallback with polling
|
|
532
|
+
const passed = await pollUntil(async () => {
|
|
533
|
+
const currentElements = typeof globalThis.$$ === 'function' ? await globalThis.$$(selector) : [];
|
|
534
|
+
const count = Array.isArray(currentElements)
|
|
535
|
+
? currentElements.length
|
|
536
|
+
: typeof currentElements.count === 'function'
|
|
537
|
+
? await currentElements.count()
|
|
538
|
+
: 0;
|
|
539
|
+
switch (matcher) {
|
|
540
|
+
case 'have.length':
|
|
541
|
+
return count === args[0];
|
|
542
|
+
case 'not.have.length':
|
|
543
|
+
return count !== args[0];
|
|
544
|
+
case 'have.length.greaterThan':
|
|
545
|
+
return count > args[0];
|
|
546
|
+
case 'have.length.lessThan':
|
|
547
|
+
return count < args[0];
|
|
548
|
+
case 'be.empty':
|
|
549
|
+
return count === 0;
|
|
550
|
+
case 'not.be.empty':
|
|
551
|
+
case 'exist':
|
|
552
|
+
return count > 0;
|
|
553
|
+
default:
|
|
554
|
+
return false;
|
|
555
|
+
}
|
|
556
|
+
}, 5000);
|
|
557
|
+
if (!passed) {
|
|
558
|
+
const currentElements = typeof globalThis.$$ === 'function' ? await globalThis.$$(selector) : [];
|
|
559
|
+
const count = Array.isArray(currentElements)
|
|
560
|
+
? currentElements.length
|
|
561
|
+
: typeof currentElements.count === 'function'
|
|
562
|
+
? await currentElements.count()
|
|
563
|
+
: 0;
|
|
564
|
+
throw new Error(`Assertion '${matcher}' failed for collection '${selector}' within 5000ms. Current count: ${count}`);
|
|
170
565
|
}
|
|
171
566
|
}
|
|
172
567
|
/**
|
|
@@ -175,25 +570,71 @@ export async function executeMultiMatcher(selector, matcher, args) {
|
|
|
175
570
|
export async function executeBrowserAssertion(assertion, args = []) {
|
|
176
571
|
const b = typeof browser !== 'undefined' ? browser : globalThis.browser;
|
|
177
572
|
if (!b)
|
|
178
|
-
throw new Error('
|
|
573
|
+
throw new Error('Browser instance is not available.');
|
|
574
|
+
const exp = typeof expect !== 'undefined' ? expect : globalThis.expect;
|
|
179
575
|
switch (assertion) {
|
|
180
576
|
case 'shouldHaveUrl':
|
|
181
|
-
|
|
577
|
+
if (exp && typeof exp(b)?.toHaveUrl === 'function') {
|
|
578
|
+
await exp(b).toHaveUrl(args[0]);
|
|
579
|
+
}
|
|
580
|
+
else {
|
|
581
|
+
const url = b.getUrl ? await b.getUrl() : b.url ? (typeof b.url === 'function' ? await b.url() : b.url) : '';
|
|
582
|
+
if (url !== args[0])
|
|
583
|
+
throw new Error(`Expected URL to be "${args[0]}", but got "${url}"`);
|
|
584
|
+
}
|
|
182
585
|
break;
|
|
183
586
|
case 'shouldContainUrl':
|
|
184
|
-
|
|
587
|
+
if (exp && typeof exp(b)?.toHaveUrl === 'function') {
|
|
588
|
+
await exp(b).toHaveUrl(new RegExp(String(args[0]).replace(/[.*+?^${}()|[\]\\]/g, '\\$&')));
|
|
589
|
+
}
|
|
590
|
+
else {
|
|
591
|
+
const url = b.getUrl ? await b.getUrl() : b.url ? (typeof b.url === 'function' ? await b.url() : b.url) : '';
|
|
592
|
+
if (!url.includes(args[0]))
|
|
593
|
+
throw new Error(`Expected URL to contain "${args[0]}", but got "${url}"`);
|
|
594
|
+
}
|
|
185
595
|
break;
|
|
186
596
|
case 'shouldHaveTitle':
|
|
187
|
-
|
|
597
|
+
if (exp && typeof exp(b)?.toHaveTitle === 'function') {
|
|
598
|
+
await exp(b).toHaveTitle(args[0]);
|
|
599
|
+
}
|
|
600
|
+
else {
|
|
601
|
+
const title = b.getTitle
|
|
602
|
+
? await b.getTitle()
|
|
603
|
+
: b.title
|
|
604
|
+
? typeof b.title === 'function'
|
|
605
|
+
? await b.title()
|
|
606
|
+
: b.title
|
|
607
|
+
: '';
|
|
608
|
+
if (title !== args[0])
|
|
609
|
+
throw new Error(`Expected title to be "${args[0]}", but got "${title}"`);
|
|
610
|
+
}
|
|
188
611
|
break;
|
|
189
612
|
case 'shouldContainTitle':
|
|
190
|
-
|
|
613
|
+
if (exp && typeof exp(b)?.toHaveTitle === 'function') {
|
|
614
|
+
await exp(b).toHaveTitle(new RegExp(String(args[0]).replace(/[.*+?^${}()|[\]\\]/g, '\\$&')));
|
|
615
|
+
}
|
|
616
|
+
else {
|
|
617
|
+
const title = b.getTitle
|
|
618
|
+
? await b.getTitle()
|
|
619
|
+
: b.title
|
|
620
|
+
? typeof b.title === 'function'
|
|
621
|
+
? await b.title()
|
|
622
|
+
: b.title
|
|
623
|
+
: '';
|
|
624
|
+
if (!title.includes(args[0]))
|
|
625
|
+
throw new Error(`Expected title to contain "${args[0]}", but got "${title}"`);
|
|
626
|
+
}
|
|
191
627
|
break;
|
|
192
628
|
case 'shouldBePageLoaded':
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
629
|
+
if (b.waitUntil) {
|
|
630
|
+
await b.waitUntil(async () => {
|
|
631
|
+
const state = await b.execute(() => document.readyState);
|
|
632
|
+
return state === 'complete';
|
|
633
|
+
}, { timeout: 10000, timeoutMsg: 'Expected page to be fully loaded (document.readyState === "complete")' });
|
|
634
|
+
}
|
|
635
|
+
else if (b.waitForLoadState) {
|
|
636
|
+
await b.waitForLoadState('domcontentloaded');
|
|
637
|
+
}
|
|
197
638
|
break;
|
|
198
639
|
case 'shouldHaveNoConsoleErrors':
|
|
199
640
|
try {
|