@testspectra/matchers 1.1.10 → 1.1.11-rc.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/dist/matchers.d.ts +3 -10
- package/dist/matchers.js +135 -576
- package/dist/reporter.js +1 -13
- package/dist/runner/collection.js +6 -32
- package/dist/runner/single.d.ts +2 -2
- package/dist/runner/single.js +7 -24
- package/package.json +1 -1
- package/testspectra-matchers-1.1.11-rc.3.tgz +0 -0
- package/dist/__tests__/matcher-contracts.test.d.ts +0 -1
- package/dist/__tests__/matcher-contracts.test.js +0 -498
- package/dist/semantic.d.ts +0 -11
- package/dist/semantic.js +0 -141
- package/testspectra-matchers-1.1.10.tgz +0 -0
package/dist/matchers.js
CHANGED
|
@@ -5,31 +5,16 @@
|
|
|
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
|
-
*/
|
|
15
8
|
export async function resolveElement(target) {
|
|
16
9
|
if (typeof target === 'string') {
|
|
17
|
-
|
|
18
|
-
|
|
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.`);
|
|
10
|
+
const el = await $(target);
|
|
11
|
+
return el;
|
|
27
12
|
}
|
|
28
13
|
const el = await target;
|
|
29
14
|
return el;
|
|
30
15
|
}
|
|
31
16
|
/**
|
|
32
|
-
* Executes a single element matcher against the
|
|
17
|
+
* Executes a single element matcher against the WebdriverIO browser / element context.
|
|
33
18
|
*
|
|
34
19
|
* @param target - Optional element target (not required for browser-level assertions).
|
|
35
20
|
* @param matcher - Single element matcher string.
|
|
@@ -37,531 +22,151 @@ export async function resolveElement(target) {
|
|
|
37
22
|
*/
|
|
38
23
|
export async function executeSingleMatcher(target, matcher, args) {
|
|
39
24
|
const isBrowserLevel = matcher === 'have.url' || matcher === 'contain.url' || matcher === 'have.title' || matcher === 'contain.title';
|
|
40
|
-
const b = typeof browser !== 'undefined' ? browser : globalThis.browser;
|
|
41
25
|
if (isBrowserLevel) {
|
|
42
26
|
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') {
|
|
102
27
|
switch (matcher) {
|
|
103
|
-
case '
|
|
104
|
-
await
|
|
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]);
|
|
28
|
+
case 'have.url':
|
|
29
|
+
await expect(browser).toHaveUrl(expected);
|
|
184
30
|
break;
|
|
185
|
-
case '
|
|
186
|
-
await
|
|
31
|
+
case 'contain.url':
|
|
32
|
+
await expect(browser).toHaveUrl(expect.stringContaining(expected));
|
|
187
33
|
break;
|
|
188
|
-
case 'have.
|
|
189
|
-
await
|
|
34
|
+
case 'have.title':
|
|
35
|
+
await expect(browser).toHaveTitle(expected);
|
|
190
36
|
break;
|
|
191
|
-
case '
|
|
192
|
-
await
|
|
37
|
+
case 'contain.title':
|
|
38
|
+
await expect(browser).toHaveTitle(expect.stringContaining(expected));
|
|
193
39
|
break;
|
|
194
|
-
default:
|
|
195
|
-
throw new Error(`Unsupported matcher: ${matcher}`);
|
|
196
40
|
}
|
|
197
41
|
return;
|
|
198
42
|
}
|
|
199
|
-
|
|
200
|
-
|
|
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;
|
|
43
|
+
if (!target) {
|
|
44
|
+
throw new Error(`Assertion matcher '${matcher}' requires an element target.`);
|
|
285
45
|
}
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
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]);
|
|
46
|
+
const el = await resolveElement(target);
|
|
47
|
+
switch (matcher) {
|
|
48
|
+
case 'be.visible':
|
|
49
|
+
await expect(el).toBeDisplayed();
|
|
50
|
+
break;
|
|
51
|
+
case 'not.be.visible':
|
|
52
|
+
await expect(el).not.toBeDisplayed();
|
|
53
|
+
break;
|
|
54
|
+
case 'exist':
|
|
55
|
+
await expect(el).toExist();
|
|
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]);
|
|
451
119
|
}
|
|
452
|
-
|
|
453
|
-
|
|
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]);
|
|
120
|
+
else {
|
|
121
|
+
await expect(el).toHaveAttribute(args[0]);
|
|
465
122
|
}
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
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;
|
|
123
|
+
break;
|
|
124
|
+
case 'not.have.attr':
|
|
125
|
+
await expect(el).not.toHaveAttribute(args[0]);
|
|
126
|
+
break;
|
|
127
|
+
case 'have.css':
|
|
128
|
+
await expect(el).toHaveStyle({ [args[0]]: args[1] });
|
|
129
|
+
break;
|
|
130
|
+
case 'not.have.css':
|
|
131
|
+
await expect(el).not.toHaveStyle({ [args[0]]: args[1] });
|
|
132
|
+
break;
|
|
133
|
+
default:
|
|
134
|
+
throw new Error(`Unsupported matcher: ${matcher}`);
|
|
491
135
|
}
|
|
492
136
|
}
|
|
493
137
|
/**
|
|
494
|
-
* Executes a multi-element collection matcher against $$.
|
|
138
|
+
* Executes a multi-element collection matcher against WebdriverIO $$.
|
|
495
139
|
*
|
|
496
140
|
* @param selector - CSS/XPath selector for the collection.
|
|
497
141
|
* @param matcher - Multi-element matcher string.
|
|
498
142
|
* @param args - Expected values or count limits.
|
|
499
143
|
*/
|
|
500
144
|
export async function executeMultiMatcher(selector, matcher, args) {
|
|
501
|
-
const elements =
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
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}`);
|
|
145
|
+
const elements = await $$(selector);
|
|
146
|
+
switch (matcher) {
|
|
147
|
+
case 'have.length':
|
|
148
|
+
await expect(elements).toBeElementsArrayOfSize(args[0]);
|
|
149
|
+
break;
|
|
150
|
+
case 'not.have.length':
|
|
151
|
+
await expect(elements).not.toBeElementsArrayOfSize(args[0]);
|
|
152
|
+
break;
|
|
153
|
+
case 'have.length.greaterThan':
|
|
154
|
+
await expect(elements).toBeElementsArrayOfSize({ gte: args[0] + 1 });
|
|
155
|
+
break;
|
|
156
|
+
case 'have.length.lessThan':
|
|
157
|
+
await expect(elements).toBeElementsArrayOfSize({ lte: Math.max(0, args[0] - 1) });
|
|
158
|
+
break;
|
|
159
|
+
case 'be.empty':
|
|
160
|
+
await expect(elements).toBeElementsArrayOfSize(0);
|
|
161
|
+
break;
|
|
162
|
+
case 'not.be.empty':
|
|
163
|
+
await expect(elements).toBeElementsArrayOfSize({ gte: 1 });
|
|
164
|
+
break;
|
|
165
|
+
case 'exist':
|
|
166
|
+
await expect(elements).toBeElementsArrayOfSize({ gte: 1 });
|
|
167
|
+
break;
|
|
168
|
+
default:
|
|
169
|
+
throw new Error(`Unsupported multi-element matcher: ${matcher}`);
|
|
565
170
|
}
|
|
566
171
|
}
|
|
567
172
|
/**
|
|
@@ -570,71 +175,25 @@ export async function executeMultiMatcher(selector, matcher, args) {
|
|
|
570
175
|
export async function executeBrowserAssertion(assertion, args = []) {
|
|
571
176
|
const b = typeof browser !== 'undefined' ? browser : globalThis.browser;
|
|
572
177
|
if (!b)
|
|
573
|
-
throw new Error('
|
|
574
|
-
const exp = typeof expect !== 'undefined' ? expect : globalThis.expect;
|
|
178
|
+
throw new Error('WebdriverIO browser instance is not available.');
|
|
575
179
|
switch (assertion) {
|
|
576
180
|
case 'shouldHaveUrl':
|
|
577
|
-
|
|
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
|
-
}
|
|
181
|
+
await expect(b).toHaveUrl(args[0]);
|
|
585
182
|
break;
|
|
586
183
|
case 'shouldContainUrl':
|
|
587
|
-
|
|
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
|
-
}
|
|
184
|
+
await expect(b).toHaveUrl(new RegExp(String(args[0]).replace(/[.*+?^${}()|[\]\\]/g, '\\$&')));
|
|
595
185
|
break;
|
|
596
186
|
case 'shouldHaveTitle':
|
|
597
|
-
|
|
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
|
-
}
|
|
187
|
+
await expect(b).toHaveTitle(args[0]);
|
|
611
188
|
break;
|
|
612
189
|
case 'shouldContainTitle':
|
|
613
|
-
|
|
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
|
-
}
|
|
190
|
+
await expect(b).toHaveTitle(new RegExp(String(args[0]).replace(/[.*+?^${}()|[\]\\]/g, '\\$&')));
|
|
627
191
|
break;
|
|
628
192
|
case 'shouldBePageLoaded':
|
|
629
|
-
|
|
630
|
-
await b.
|
|
631
|
-
|
|
632
|
-
|
|
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
|
-
}
|
|
193
|
+
await b.waitUntil(async () => {
|
|
194
|
+
const state = await b.execute(() => document.readyState);
|
|
195
|
+
return state === 'complete';
|
|
196
|
+
}, { timeout: 10000, timeoutMsg: 'Expected page to be fully loaded (document.readyState === "complete")' });
|
|
638
197
|
break;
|
|
639
198
|
case 'shouldHaveNoConsoleErrors':
|
|
640
199
|
try {
|