@testspectra/matchers 1.0.69 → 1.1.0-rc.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.
Files changed (51) hide show
  1. package/LICENSE.md +48 -0
  2. package/dist/__tests__/intercept.test.d.ts +1 -0
  3. package/dist/__tests__/intercept.test.js +183 -0
  4. package/dist/__tests__/matcher-contracts.test.d.ts +1 -0
  5. package/dist/__tests__/matcher-contracts.test.js +498 -0
  6. package/dist/__tests__/matchers.test.d.ts +1 -0
  7. package/dist/__tests__/matchers.test.js +161 -0
  8. package/dist/contract.d.ts +141 -0
  9. package/dist/contract.js +10 -0
  10. package/dist/index.d.ts +4 -21
  11. package/dist/index.js +4 -21
  12. package/dist/intercept/cdp-handler.d.ts +22 -0
  13. package/dist/intercept/cdp-handler.js +123 -0
  14. package/dist/intercept/index.d.ts +4 -0
  15. package/dist/intercept/index.js +4 -0
  16. package/dist/intercept/mock-handle.d.ts +49 -0
  17. package/dist/intercept/mock-handle.js +124 -0
  18. package/dist/intercept/mock-registry.d.ts +18 -0
  19. package/dist/intercept/mock-registry.js +97 -0
  20. package/dist/intercept/types.d.ts +43 -0
  21. package/dist/intercept/types.js +1 -0
  22. package/dist/matchers.d.ts +15 -4
  23. package/dist/matchers.js +609 -81
  24. package/dist/proto.d.ts +18 -283
  25. package/dist/proto.js +1 -114
  26. package/dist/reporter.d.ts +30 -0
  27. package/dist/reporter.js +97 -0
  28. package/dist/runner/collection.d.ts +33 -20
  29. package/dist/runner/collection.js +104 -26
  30. package/dist/runner/single.d.ts +125 -34
  31. package/dist/runner/single.js +290 -55
  32. package/dist/semantic.d.ts +11 -0
  33. package/dist/semantic.js +141 -0
  34. package/dist/spectra.d.ts +32 -8
  35. package/dist/spectra.js +157 -40
  36. package/dist/types.d.ts +1226 -28
  37. package/package.json +13 -8
  38. package/src/contract.ts +182 -0
  39. package/src/index.ts +4 -22
  40. package/src/proto.ts +22 -433
  41. package/src/runtime/assertions.ts +450 -0
  42. package/src/runtime/element_actions.ts +169 -0
  43. package/src/runtime/element_proxy.ts +159 -0
  44. package/src/runtime/element_state.ts +91 -0
  45. package/src/runtime/spectra.ts +109 -0
  46. package/src/types.ts +1424 -96
  47. package/tsconfig.json +2 -2
  48. package/src/matchers.ts +0 -146
  49. package/src/runner/collection.ts +0 -153
  50. package/src/runner/single.ts +0 -301
  51. package/src/spectra.ts +0 -376
@@ -0,0 +1,498 @@
1
+ import { describe, it, expect, vi } from 'vitest';
2
+ // Helper polling simulation resembling worker.ts pollCondition
3
+ async function pollCondition(fn, timeoutMs = 100, intervalMs = 10) {
4
+ const start = Date.now();
5
+ while (Date.now() - start < timeoutMs) {
6
+ try {
7
+ if (await fn())
8
+ return true;
9
+ }
10
+ catch { }
11
+ await new Promise((r) => setTimeout(r, intervalMs));
12
+ }
13
+ try {
14
+ return await fn();
15
+ }
16
+ catch {
17
+ return false;
18
+ }
19
+ }
20
+ // Factory to create a test SingleElementProxy backed by simulated DOM element state
21
+ function createTestElementProxy(selector, state) {
22
+ const proxy = {
23
+ selector,
24
+ async isDisplayed() {
25
+ return state.displayed ?? true;
26
+ },
27
+ async isExisting() {
28
+ return state.existing ?? true;
29
+ },
30
+ async isEnabled() {
31
+ return state.enabled ?? true;
32
+ },
33
+ async isSelected() {
34
+ return state.selected ?? false;
35
+ },
36
+ async isFocused() {
37
+ return state.focused ?? false;
38
+ },
39
+ async getText() {
40
+ return state.text ?? '';
41
+ },
42
+ async getValue() {
43
+ return state.value ?? '';
44
+ },
45
+ async getAttribute(name) {
46
+ return state.attributes?.[name] ?? null;
47
+ },
48
+ async getCSSProperty(property) {
49
+ return { property, value: state.css?.[property] ?? '', parsed: {} };
50
+ },
51
+ async click() { },
52
+ async doubleClick() { },
53
+ async rightClick() { },
54
+ async hover() { },
55
+ async focus() { },
56
+ async clear() { },
57
+ async type(_text) { },
58
+ async select(_option) { },
59
+ async selectByAttribute(_attribute, _value) { },
60
+ async selectByValue(_value) { },
61
+ async selectByIndex(_index) { },
62
+ async selectByVisibleText(_text) { },
63
+ async dragDrop(_destination) { },
64
+ async scrollIntoView() { },
65
+ async swipe(_options) { },
66
+ async longPress(_options) { },
67
+ async waitForElement(_timeoutMs) { },
68
+ async waitForExist(_timeoutMs) { },
69
+ async waitForDisplayed(_timeoutMs) { },
70
+ async waitForEnabled(_timeoutMs) { },
71
+ // Matcher implementations identical to worker.ts logic
72
+ async shouldBeVisible() {
73
+ const ok = await pollCondition(async () => await this.isDisplayed());
74
+ if (!ok)
75
+ throw new Error(`Expected element "${selector}" to be visible`);
76
+ },
77
+ async shouldNotBeVisible() {
78
+ const ok = await pollCondition(async () => !(await this.isDisplayed()));
79
+ if (!ok)
80
+ throw new Error(`Expected element "${selector}" not to be visible`);
81
+ },
82
+ async shouldExist() {
83
+ const ok = await pollCondition(async () => await this.isExisting());
84
+ if (!ok)
85
+ throw new Error(`Expected element "${selector}" to exist in DOM`);
86
+ },
87
+ async shouldNotExist() {
88
+ const ok = await pollCondition(async () => !(await this.isExisting()));
89
+ if (!ok)
90
+ throw new Error(`Expected element "${selector}" not to exist in DOM`);
91
+ },
92
+ async shouldBeEnabled() {
93
+ const ok = await pollCondition(async () => await this.isEnabled());
94
+ if (!ok)
95
+ throw new Error(`Expected element "${selector}" to be enabled`);
96
+ },
97
+ async shouldBeDisabled() {
98
+ const ok = await pollCondition(async () => !(await this.isEnabled()));
99
+ if (!ok)
100
+ throw new Error(`Expected element "${selector}" to be disabled`);
101
+ },
102
+ async shouldBeChecked() {
103
+ const ok = await pollCondition(async () => await this.isSelected());
104
+ if (!ok)
105
+ throw new Error(`Expected element "${selector}" to be checked`);
106
+ },
107
+ async shouldNotBeChecked() {
108
+ const ok = await pollCondition(async () => !(await this.isSelected()));
109
+ if (!ok)
110
+ throw new Error(`Expected element "${selector}" not to be checked`);
111
+ },
112
+ async shouldBeFocused() {
113
+ const ok = await pollCondition(async () => await this.isFocused());
114
+ if (!ok)
115
+ throw new Error(`Expected element "${selector}" to be focused`);
116
+ },
117
+ async shouldNotBeFocused() {
118
+ const ok = await pollCondition(async () => !(await this.isFocused()));
119
+ if (!ok)
120
+ throw new Error(`Expected element "${selector}" not to be focused`);
121
+ },
122
+ async shouldHaveText(expected) {
123
+ let actual = '';
124
+ const ok = await pollCondition(async () => {
125
+ actual = await this.getText();
126
+ if (expected instanceof RegExp)
127
+ return expected.test(actual);
128
+ return actual === String(expected) || actual.trim() === String(expected).trim();
129
+ });
130
+ if (!ok) {
131
+ if (expected instanceof RegExp)
132
+ throw new Error(`Expected element "${selector}" text to match ${expected}, but got "${actual}"`);
133
+ throw new Error(`Expected element "${selector}" to have text "${expected}", but got "${actual}"`);
134
+ }
135
+ },
136
+ async shouldNotHaveText(expected) {
137
+ let actual = '';
138
+ const ok = await pollCondition(async () => {
139
+ actual = await this.getText();
140
+ if (expected instanceof RegExp)
141
+ return !expected.test(actual);
142
+ return actual !== String(expected) && actual.trim() !== String(expected).trim();
143
+ });
144
+ if (!ok) {
145
+ if (expected instanceof RegExp)
146
+ throw new Error(`Expected element "${selector}" text not to match ${expected}`);
147
+ throw new Error(`Expected element "${selector}" not to have text "${expected}"`);
148
+ }
149
+ },
150
+ async shouldContainText(substring) {
151
+ let actual = '';
152
+ const ok = await pollCondition(async () => {
153
+ actual = await this.getText();
154
+ return actual.includes(String(substring));
155
+ });
156
+ if (!ok)
157
+ throw new Error(`Expected element "${selector}" to contain text "${substring}", but got "${actual}"`);
158
+ },
159
+ async shouldNotContainText(substring) {
160
+ let actual = '';
161
+ const ok = await pollCondition(async () => {
162
+ actual = await this.getText();
163
+ return !actual.includes(String(substring));
164
+ });
165
+ if (!ok)
166
+ throw new Error(`Expected element "${selector}" not to contain text "${substring}"`);
167
+ },
168
+ async shouldHaveValue(value) {
169
+ let actual = '';
170
+ const ok = await pollCondition(async () => {
171
+ actual = await this.getValue();
172
+ return actual === String(value);
173
+ });
174
+ if (!ok)
175
+ throw new Error(`Expected element "${selector}" to have value "${value}", but got "${actual}"`);
176
+ },
177
+ async shouldNotHaveValue(value) {
178
+ const ok = await pollCondition(async () => {
179
+ const actual = await this.getValue();
180
+ return actual !== String(value);
181
+ });
182
+ if (!ok)
183
+ throw new Error(`Expected element "${selector}" not to have value "${value}"`);
184
+ },
185
+ async shouldContainValue(value) {
186
+ let actual = '';
187
+ const ok = await pollCondition(async () => {
188
+ actual = await this.getValue();
189
+ return actual.includes(String(value));
190
+ });
191
+ if (!ok)
192
+ throw new Error(`Expected element "${selector}" to contain value "${value}", but got "${actual}"`);
193
+ },
194
+ async shouldNotContainValue(value) {
195
+ const ok = await pollCondition(async () => {
196
+ const actual = await this.getValue();
197
+ return !actual.includes(String(value));
198
+ });
199
+ if (!ok)
200
+ throw new Error(`Expected element "${selector}" not to contain value "${value}"`);
201
+ },
202
+ async shouldHaveClass(className) {
203
+ const ok = await pollCondition(async () => state.classes?.includes(className) ?? false);
204
+ if (!ok)
205
+ throw new Error(`Expected element "${selector}" to have class "${className}"`);
206
+ },
207
+ async shouldNotHaveClass(className) {
208
+ const ok = await pollCondition(async () => !(state.classes?.includes(className) ?? false));
209
+ if (!ok)
210
+ throw new Error(`Expected element "${selector}" not to have class "${className}"`);
211
+ },
212
+ async shouldHaveAttribute(name, value) {
213
+ let actual = null;
214
+ const ok = await pollCondition(async () => {
215
+ actual = await this.getAttribute(name);
216
+ if (value !== undefined)
217
+ return actual === String(value);
218
+ return actual !== null;
219
+ });
220
+ if (!ok) {
221
+ if (value !== undefined)
222
+ throw new Error(`Expected element "${selector}" to have attribute "${name}" = "${value}", but got "${actual}"`);
223
+ throw new Error(`Expected element "${selector}" to have attribute "${name}"`);
224
+ }
225
+ },
226
+ async shouldNotHaveAttribute(name) {
227
+ const ok = await pollCondition(async () => {
228
+ const actual = await this.getAttribute(name);
229
+ return actual === null;
230
+ });
231
+ if (!ok)
232
+ throw new Error(`Expected element "${selector}" not to have attribute "${name}"`);
233
+ },
234
+ async shouldHaveCss(property, value) {
235
+ let actualCss = '';
236
+ const ok = await pollCondition(async () => {
237
+ const css = await this.getCSSProperty(property);
238
+ actualCss = css.value;
239
+ return actualCss === value;
240
+ });
241
+ if (!ok)
242
+ throw new Error(`Expected element "${selector}" to have CSS "${property}" = "${value}", but got "${actualCss}"`);
243
+ },
244
+ async shouldNotHaveCss(property, value) {
245
+ const ok = await pollCondition(async () => {
246
+ const css = await this.getCSSProperty(property);
247
+ return css.value !== value;
248
+ });
249
+ if (!ok)
250
+ throw new Error(`Expected element "${selector}" not to have CSS "${property}" = "${value}"`);
251
+ },
252
+ };
253
+ return proxy;
254
+ }
255
+ // Factory to create a test CollectionProxy backed by simulated count
256
+ function createTestCollectionProxy(selector, itemsCount) {
257
+ return {
258
+ selector,
259
+ async count() {
260
+ return itemsCount;
261
+ },
262
+ get length() {
263
+ return this.count();
264
+ },
265
+ first() {
266
+ return createTestElementProxy(`${selector}:first-child`, {});
267
+ },
268
+ last() {
269
+ return createTestElementProxy(`${selector}:last-child`, {});
270
+ },
271
+ nth(index) {
272
+ return createTestElementProxy(`${selector}:nth-child(${index})`, {});
273
+ },
274
+ async shouldHaveLength(expected) {
275
+ let actual = 0;
276
+ const ok = await pollCondition(async () => {
277
+ actual = await this.count();
278
+ return actual === expected;
279
+ });
280
+ if (!ok)
281
+ throw new Error(`Expected collection "${selector}" count to equal ${expected}, but got ${actual}`);
282
+ },
283
+ async shouldNotHaveLength(expected) {
284
+ const ok = await pollCondition(async () => {
285
+ const actual = await this.count();
286
+ return actual !== expected;
287
+ });
288
+ if (!ok)
289
+ throw new Error(`Expected collection "${selector}" count not to equal ${expected}`);
290
+ },
291
+ async shouldHaveLengthGreaterThan(expected) {
292
+ let actual = 0;
293
+ const ok = await pollCondition(async () => {
294
+ actual = await this.count();
295
+ return actual > expected;
296
+ });
297
+ if (!ok)
298
+ throw new Error(`Expected collection "${selector}" count to be greater than ${expected}, but got ${actual}`);
299
+ },
300
+ async shouldHaveLengthLessThan(expected) {
301
+ let actual = 0;
302
+ const ok = await pollCondition(async () => {
303
+ actual = await this.count();
304
+ return actual < expected;
305
+ });
306
+ if (!ok)
307
+ throw new Error(`Expected collection "${selector}" count to be less than ${expected}, but got ${actual}`);
308
+ },
309
+ async shouldBeEmpty() {
310
+ let actual = 0;
311
+ const ok = await pollCondition(async () => {
312
+ actual = await this.count();
313
+ return actual === 0;
314
+ });
315
+ if (!ok)
316
+ throw new Error(`Expected collection "${selector}" to be empty, but got ${actual}`);
317
+ },
318
+ async shouldNotBeEmpty() {
319
+ const ok = await pollCondition(async () => {
320
+ const actual = await this.count();
321
+ return actual > 0;
322
+ });
323
+ if (!ok)
324
+ throw new Error(`Expected collection "${selector}" not to be empty`);
325
+ },
326
+ };
327
+ }
328
+ describe('Worker Matchers & Static Actions Verification Suite', () => {
329
+ describe('Single Element Visibility & Existence Matchers', () => {
330
+ it('passes shouldBeVisible when element is displayed, fails and throws when not displayed', async () => {
331
+ const visibleEl = createTestElementProxy('#btn-visible', { displayed: true });
332
+ await expect(visibleEl.shouldBeVisible()).resolves.toBeUndefined();
333
+ const hiddenEl = createTestElementProxy('#btn-hidden', { displayed: false });
334
+ await expect(hiddenEl.shouldBeVisible()).rejects.toThrow('Expected element "#btn-hidden" to be visible');
335
+ });
336
+ it('passes shouldNotBeVisible when element is hidden, fails and throws when displayed', async () => {
337
+ const hiddenEl = createTestElementProxy('#btn-hidden', { displayed: false });
338
+ await expect(hiddenEl.shouldNotBeVisible()).resolves.toBeUndefined();
339
+ const visibleEl = createTestElementProxy('#btn-visible', { displayed: true });
340
+ await expect(visibleEl.shouldNotBeVisible()).rejects.toThrow('Expected element "#btn-visible" not to be visible');
341
+ });
342
+ it('passes shouldExist when element is in DOM, fails and throws when missing', async () => {
343
+ const domEl = createTestElementProxy('#card', { existing: true });
344
+ await expect(domEl.shouldExist()).resolves.toBeUndefined();
345
+ const missingEl = createTestElementProxy('#missing-card', { existing: false });
346
+ await expect(missingEl.shouldExist()).rejects.toThrow('Expected element "#missing-card" to exist in DOM');
347
+ });
348
+ it('passes shouldNotExist when element is missing, fails and throws when element exists', async () => {
349
+ const missingEl = createTestElementProxy('#missing-card', { existing: false });
350
+ await expect(missingEl.shouldNotExist()).resolves.toBeUndefined();
351
+ const domEl = createTestElementProxy('#card', { existing: true });
352
+ await expect(domEl.shouldNotExist()).rejects.toThrow('Expected element "#card" not to exist in DOM');
353
+ });
354
+ });
355
+ describe('Single Element State Matchers (Enabled, Checked, Focused)', () => {
356
+ it('verifies shouldBeEnabled and shouldBeDisabled strictly', async () => {
357
+ const enabledEl = createTestElementProxy('#submit-btn', { enabled: true });
358
+ await expect(enabledEl.shouldBeEnabled()).resolves.toBeUndefined();
359
+ await expect(enabledEl.shouldBeDisabled()).rejects.toThrow('Expected element "#submit-btn" to be disabled');
360
+ const disabledEl = createTestElementProxy('#disabled-btn', { enabled: false });
361
+ await expect(disabledEl.shouldBeDisabled()).resolves.toBeUndefined();
362
+ await expect(disabledEl.shouldBeEnabled()).rejects.toThrow('Expected element "#disabled-btn" to be enabled');
363
+ });
364
+ it('verifies shouldBeChecked and shouldNotBeChecked strictly', async () => {
365
+ const checkedCheckbox = createTestElementProxy('#agree', { selected: true });
366
+ await expect(checkedCheckbox.shouldBeChecked()).resolves.toBeUndefined();
367
+ await expect(checkedCheckbox.shouldNotBeChecked()).rejects.toThrow('Expected element "#agree" not to be checked');
368
+ const uncheckedCheckbox = createTestElementProxy('#disagree', { selected: false });
369
+ await expect(uncheckedCheckbox.shouldNotBeChecked()).resolves.toBeUndefined();
370
+ await expect(uncheckedCheckbox.shouldBeChecked()).rejects.toThrow('Expected element "#disagree" to be checked');
371
+ });
372
+ it('verifies shouldBeFocused and shouldNotBeFocused strictly', async () => {
373
+ const focusedInput = createTestElementProxy('#email-input', { focused: true });
374
+ await expect(focusedInput.shouldBeFocused()).resolves.toBeUndefined();
375
+ await expect(focusedInput.shouldNotBeFocused()).rejects.toThrow('Expected element "#email-input" not to be focused');
376
+ const unfocusedInput = createTestElementProxy('#password-input', { focused: false });
377
+ await expect(unfocusedInput.shouldNotBeFocused()).resolves.toBeUndefined();
378
+ await expect(unfocusedInput.shouldBeFocused()).rejects.toThrow('Expected element "#password-input" to be focused');
379
+ });
380
+ });
381
+ describe('Content Matchers (Text, Value, Classes, Attributes, CSS)', () => {
382
+ it('strictly checks text equality and fails on mismatch', async () => {
383
+ const textEl = createTestElementProxy('#heading', { text: 'Welcome to TestSpectra' });
384
+ await expect(textEl.shouldHaveText('Welcome to TestSpectra')).resolves.toBeUndefined();
385
+ await expect(textEl.shouldHaveText(/Welcome to \w+/)).resolves.toBeUndefined();
386
+ await expect(textEl.shouldContainText('TestSpectra')).resolves.toBeUndefined();
387
+ // Negative assertions
388
+ await expect(textEl.shouldNotHaveText('Goodbye')).resolves.toBeUndefined();
389
+ await expect(textEl.shouldNotContainText('Cypress')).resolves.toBeUndefined();
390
+ // Failing conditions MUST throw and not falsely pass
391
+ await expect(textEl.shouldHaveText('Wrong Text')).rejects.toThrow('Expected element "#heading" to have text "Wrong Text", but got "Welcome to TestSpectra"');
392
+ await expect(textEl.shouldContainText('Selenium')).rejects.toThrow('Expected element "#heading" to contain text "Selenium", but got "Welcome to TestSpectra"');
393
+ await expect(textEl.shouldNotHaveText('Welcome to TestSpectra')).rejects.toThrow('Expected element "#heading" not to have text "Welcome to TestSpectra"');
394
+ await expect(textEl.shouldNotContainText('TestSpectra')).rejects.toThrow('Expected element "#heading" not to contain text "TestSpectra"');
395
+ });
396
+ it('strictly checks input values and fails on mismatch', async () => {
397
+ const inputEl = createTestElementProxy('#username', { value: 'admin_testspectra' });
398
+ await expect(inputEl.shouldHaveValue('admin_testspectra')).resolves.toBeUndefined();
399
+ await expect(inputEl.shouldContainValue('admin_')).resolves.toBeUndefined();
400
+ await expect(inputEl.shouldNotHaveValue('guest')).resolves.toBeUndefined();
401
+ await expect(inputEl.shouldNotContainValue('root')).resolves.toBeUndefined();
402
+ await expect(inputEl.shouldHaveValue('wrong_user')).rejects.toThrow('Expected element "#username" to have value "wrong_user", but got "admin_testspectra"');
403
+ await expect(inputEl.shouldNotHaveValue('admin_testspectra')).rejects.toThrow('Expected element "#username" not to have value "admin_testspectra"');
404
+ });
405
+ it('strictly checks classes and attributes', async () => {
406
+ const badge = createTestElementProxy('#status-badge', {
407
+ classes: ['badge', 'badge-success', 'active'],
408
+ attributes: { id: 'status-badge', 'data-role': 'indicator', disabled: 'false' },
409
+ css: { color: 'rgb(0, 128, 0)' },
410
+ });
411
+ await expect(badge.shouldHaveClass('badge-success')).resolves.toBeUndefined();
412
+ await expect(badge.shouldNotHaveClass('badge-danger')).resolves.toBeUndefined();
413
+ await expect(badge.shouldHaveAttribute('data-role', 'indicator')).resolves.toBeUndefined();
414
+ await expect(badge.shouldNotHaveAttribute('data-unknown')).resolves.toBeUndefined();
415
+ await expect(badge.shouldHaveCss('color', 'rgb(0, 128, 0)')).resolves.toBeUndefined();
416
+ // Fails when class/attribute is missing or incorrect
417
+ await expect(badge.shouldHaveClass('badge-danger')).rejects.toThrow('Expected element "#status-badge" to have class "badge-danger"');
418
+ await expect(badge.shouldNotHaveClass('badge-success')).rejects.toThrow('Expected element "#status-badge" not to have class "badge-success"');
419
+ await expect(badge.shouldHaveAttribute('data-role', 'wrong-val')).rejects.toThrow('Expected element "#status-badge" to have attribute "data-role" = "wrong-val", but got "indicator"');
420
+ await expect(badge.shouldNotHaveAttribute('data-role')).rejects.toThrow('Expected element "#status-badge" not to have attribute "data-role"');
421
+ });
422
+ });
423
+ describe('Collection Matchers ($$ / Spectra.getAll)', () => {
424
+ it('strictly checks collection lengths and emptiness', async () => {
425
+ const items = createTestCollectionProxy('.post-item', 5);
426
+ await expect(items.shouldHaveLength(5)).resolves.toBeUndefined();
427
+ await expect(items.shouldHaveLengthGreaterThan(3)).resolves.toBeUndefined();
428
+ await expect(items.shouldHaveLengthLessThan(10)).resolves.toBeUndefined();
429
+ await expect(items.shouldNotBeEmpty()).resolves.toBeUndefined();
430
+ // Failing conditions MUST throw
431
+ await expect(items.shouldHaveLength(0)).rejects.toThrow('Expected collection ".post-item" count to equal 0, but got 5');
432
+ await expect(items.shouldHaveLengthGreaterThan(10)).rejects.toThrow('Expected collection ".post-item" count to be greater than 10, but got 5');
433
+ await expect(items.shouldHaveLengthLessThan(3)).rejects.toThrow('Expected collection ".post-item" count to be less than 3, but got 5');
434
+ await expect(items.shouldBeEmpty()).rejects.toThrow('Expected collection ".post-item" to be empty, but got 5');
435
+ const emptyItems = createTestCollectionProxy('.empty-list', 0);
436
+ await expect(emptyItems.shouldBeEmpty()).resolves.toBeUndefined();
437
+ await expect(emptyItems.shouldNotBeEmpty()).rejects.toThrow('Expected collection ".empty-list" not to be empty');
438
+ });
439
+ });
440
+ describe('Spectra Static Automation API (Single Global Object)', () => {
441
+ it('correctly maps Spectra static methods to underlying actions without error', async () => {
442
+ const elementMock = createTestElementProxy('#test-btn', { text: 'Click Me', value: 'hello' });
443
+ const clickSpy = vi.spyOn(elementMock, 'click');
444
+ const typeSpy = vi.spyOn(elementMock, 'type');
445
+ const clearSpy = vi.spyOn(elementMock, 'clear');
446
+ const mockSpectra = {
447
+ get: (target) => (typeof target === 'string' ? elementMock : target),
448
+ getAll: (selector) => createTestCollectionProxy(selector, 2),
449
+ navigate: async (_url) => { },
450
+ back: async () => { },
451
+ forward: async () => { },
452
+ refresh: async () => { },
453
+ setViewport: async (_w, _h) => { },
454
+ wait: async (ms) => {
455
+ await new Promise((r) => setTimeout(r, ms));
456
+ },
457
+ click: async (target) => {
458
+ const el = typeof target === 'string' ? elementMock : target;
459
+ await el.click();
460
+ },
461
+ doubleClick: async () => { },
462
+ rightClick: async () => { },
463
+ type: async (target, text) => {
464
+ const el = typeof target === 'string' ? elementMock : target;
465
+ await el.type(text);
466
+ },
467
+ clear: async (target) => {
468
+ const el = typeof target === 'string' ? elementMock : target;
469
+ await el.clear();
470
+ },
471
+ select: async () => { },
472
+ hover: async () => { },
473
+ dragDrop: async () => { },
474
+ scrollIntoView: async () => { },
475
+ scroll: async () => { },
476
+ swipe: async () => { },
477
+ longPress: async () => { },
478
+ pressKey: async () => { },
479
+ waitForElement: async () => { },
480
+ intercept: async () => ({ respondWith: async () => { }, callCount: () => 0 }),
481
+ clearMocks: () => { },
482
+ browser: {},
483
+ };
484
+ // Test actions call underlying proxy methods
485
+ await mockSpectra.click('#test-btn');
486
+ expect(clickSpy).toHaveBeenCalled();
487
+ await mockSpectra.type('#test-btn', 'Test input');
488
+ expect(typeSpy).toHaveBeenCalledWith('Test input');
489
+ await mockSpectra.clear('#test-btn');
490
+ expect(clearSpy).toHaveBeenCalled();
491
+ // Test wait execution timing
492
+ const start = Date.now();
493
+ await mockSpectra.wait(20);
494
+ const elapsed = Date.now() - start;
495
+ expect(elapsed).toBeGreaterThanOrEqual(15);
496
+ });
497
+ });
498
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,161 @@
1
+ import { describe, it, expect, vi } from 'vitest';
2
+ import { Spectra, SingleElementRunner, MultiElementRunner, installPrototypes } from '../index.js';
3
+ describe('Matchers Assertion Library & Receiver API', () => {
4
+ describe('SingleElementRunner - 26 Receiver Assertion Methods', () => {
5
+ it('should queue visibility and DOM existence assertions', () => {
6
+ const runner = new SingleElementRunner('#hero');
7
+ runner.shouldBeVisible().shouldNotBeVisible().shouldExist().shouldNotExist();
8
+ const assertions = runner._assertions;
9
+ expect(assertions).toHaveLength(4);
10
+ expect(assertions[0].matcher).toBe('be.visible');
11
+ expect(assertions[1].matcher).toBe('not.be.visible');
12
+ expect(assertions[2].matcher).toBe('exist');
13
+ expect(assertions[3].matcher).toBe('not.exist');
14
+ });
15
+ it('should queue interactivity and state assertions', () => {
16
+ const runner = new SingleElementRunner('#btn');
17
+ runner
18
+ .shouldBeClickable()
19
+ .shouldNotBeClickable()
20
+ .shouldBeEnabled()
21
+ .shouldBeDisabled()
22
+ .shouldBeChecked()
23
+ .shouldNotBeChecked()
24
+ .shouldBeFocused()
25
+ .shouldNotBeFocused();
26
+ const assertions = runner._assertions;
27
+ expect(assertions).toHaveLength(8);
28
+ expect(assertions[0].matcher).toBe('be.clickable');
29
+ expect(assertions[1].matcher).toBe('not.be.clickable');
30
+ expect(assertions[2].matcher).toBe('be.enabled');
31
+ expect(assertions[3].matcher).toBe('be.disabled');
32
+ expect(assertions[4].matcher).toBe('be.checked');
33
+ expect(assertions[5].matcher).toBe('not.be.checked');
34
+ expect(assertions[6].matcher).toBe('be.focused');
35
+ expect(assertions[7].matcher).toBe('not.be.focused');
36
+ });
37
+ it('should queue text and value assertions with arguments', () => {
38
+ const runner = new SingleElementRunner('#input');
39
+ runner
40
+ .shouldHaveText('Welcome')
41
+ .shouldNotHaveText('Goodbye')
42
+ .shouldContainText('come')
43
+ .shouldNotContainText('bye')
44
+ .shouldHaveValue('admin')
45
+ .shouldNotHaveValue('guest')
46
+ .shouldContainValue('adm')
47
+ .shouldNotContainValue('gst');
48
+ const assertions = runner._assertions;
49
+ expect(assertions).toHaveLength(8);
50
+ expect(assertions[0]).toEqual({ target: '#input', matcher: 'have.text', args: ['Welcome'] });
51
+ expect(assertions[1]).toEqual({ target: '#input', matcher: 'not.have.text', args: ['Goodbye'] });
52
+ expect(assertions[2]).toEqual({ target: '#input', matcher: 'contain.text', args: ['come'] });
53
+ expect(assertions[3]).toEqual({ target: '#input', matcher: 'not.contain.text', args: ['bye'] });
54
+ expect(assertions[4]).toEqual({ target: '#input', matcher: 'have.value', args: ['admin'] });
55
+ expect(assertions[5]).toEqual({ target: '#input', matcher: 'not.have.value', args: ['guest'] });
56
+ expect(assertions[6]).toEqual({ target: '#input', matcher: 'contain.value', args: ['adm'] });
57
+ expect(assertions[7]).toEqual({ target: '#input', matcher: 'not.contain.value', args: ['gst'] });
58
+ });
59
+ it('should queue attribute, class, and CSS assertions', () => {
60
+ const runner = new SingleElementRunner('#card');
61
+ runner
62
+ .shouldHaveAttribute('data-testid', 'test-card')
63
+ .shouldNotHaveAttribute('disabled')
64
+ .shouldHaveClass('active')
65
+ .shouldNotHaveClass('hidden')
66
+ .shouldHaveCss('color', 'red')
67
+ .shouldNotHaveCss('display', 'none');
68
+ const assertions = runner._assertions;
69
+ expect(assertions).toHaveLength(6);
70
+ expect(assertions[0]).toEqual({ target: '#card', matcher: 'have.attr', args: ['data-testid', 'test-card'] });
71
+ expect(assertions[1]).toEqual({ target: '#card', matcher: 'not.have.attr', args: ['disabled'] });
72
+ expect(assertions[2]).toEqual({ target: '#card', matcher: 'have.class', args: ['active'] });
73
+ expect(assertions[3]).toEqual({ target: '#card', matcher: 'not.have.class', args: ['hidden'] });
74
+ expect(assertions[4]).toEqual({ target: '#card', matcher: 'have.css', args: ['color', 'red'] });
75
+ expect(assertions[5]).toEqual({ target: '#card', matcher: 'not.have.css', args: ['display', 'none'] });
76
+ });
77
+ });
78
+ describe('SingleElementRunner - Actions', () => {
79
+ it('should support action method chaining and label assignment', () => {
80
+ const runner = new SingleElementRunner('#target');
81
+ expect(runner.click()).toBe(runner);
82
+ expect(runner._actionPayload.key).toBe('click');
83
+ expect(runner.doubleClick()).toBe(runner);
84
+ expect(runner._actionPayload.key).toBe('doubleClick');
85
+ expect(runner.rightClick()).toBe(runner);
86
+ expect(runner._actionPayload.key).toBe('rightClick');
87
+ expect(runner.scrollIntoView()).toBe(runner);
88
+ expect(runner._actionPayload.key).toBe('scrollIntoView');
89
+ expect(runner.dragDrop('#dest')).toBe(runner);
90
+ expect(runner._actionPayload.key).toBe('dragDrop');
91
+ });
92
+ it('should support action to semantic assertion chaining', () => {
93
+ const runner = new SingleElementRunner('#login-btn');
94
+ runner.click().shouldNotBeVisible();
95
+ expect(runner._actionPayload.key).toBe('click');
96
+ const assertions = runner._assertions;
97
+ expect(assertions).toHaveLength(1);
98
+ expect(assertions[0].matcher).toBe('not.be.visible');
99
+ });
100
+ });
101
+ describe('MultiElementRunner - 6 Collection Receiver Assertions', () => {
102
+ it('should queue all 6 collection assertions', () => {
103
+ const runner = new MultiElementRunner('.product-item');
104
+ runner
105
+ .shouldHaveLength(4)
106
+ .shouldNotHaveLength(0)
107
+ .shouldHaveLengthGreaterThan(2)
108
+ .shouldHaveLengthLessThan(10)
109
+ .shouldBeEmpty()
110
+ .shouldNotBeEmpty();
111
+ const assertions = runner._assertions;
112
+ expect(assertions).toHaveLength(6);
113
+ expect(assertions[0]).toEqual({ matcher: 'have.length', args: [4] });
114
+ expect(assertions[1]).toEqual({ matcher: 'not.have.length', args: [0] });
115
+ expect(assertions[2]).toEqual({ matcher: 'have.length.greaterThan', args: [2] });
116
+ expect(assertions[3]).toEqual({ matcher: 'have.length.lessThan', args: [10] });
117
+ expect(assertions[4]).toEqual({ matcher: 'be.empty', args: [] });
118
+ expect(assertions[5]).toEqual({ matcher: 'not.be.empty', args: [] });
119
+ });
120
+ it('should navigate collection items with first, last, eq', () => {
121
+ const collection = Spectra.getAll('.list-item');
122
+ const first = collection.first();
123
+ const last = collection.last();
124
+ const second = collection.eq(1);
125
+ expect(first).toBeInstanceOf(SingleElementRunner);
126
+ expect(last).toBeInstanceOf(SingleElementRunner);
127
+ expect(second).toBeInstanceOf(SingleElementRunner);
128
+ });
129
+ });
130
+ describe('Spectra.browser - Context Assertions & Utilities', () => {
131
+ it('should expose all browser assertions and utilities', () => {
132
+ expect(typeof Spectra.browser.shouldHaveUrl).toBe('function');
133
+ expect(typeof Spectra.browser.shouldContainUrl).toBe('function');
134
+ expect(typeof Spectra.browser.shouldHaveTitle).toBe('function');
135
+ expect(typeof Spectra.browser.shouldContainTitle).toBe('function');
136
+ expect(typeof Spectra.browser.shouldBePageLoaded).toBe('function');
137
+ expect(typeof Spectra.browser.shouldHaveNoConsoleErrors).toBe('function');
138
+ expect(typeof Spectra.browser.clearCookies).toBe('function');
139
+ expect(typeof Spectra.browser.clearLocalStorage).toBe('function');
140
+ });
141
+ });
142
+ describe('Prototypes & Promise Unwrapping', () => {
143
+ it('should install receiver methods on mock WebdriverIO browser', () => {
144
+ const addedCommands = {};
145
+ const mockBrowser = {
146
+ addCommand: vi.fn((name, fn) => {
147
+ addedCommands[name] = fn;
148
+ }),
149
+ };
150
+ installPrototypes(mockBrowser);
151
+ expect(mockBrowser.addCommand).toHaveBeenCalled();
152
+ expect(addedCommands['should']).toBeUndefined();
153
+ expect(addedCommands['shouldBeVisible']).toBeDefined();
154
+ expect(addedCommands['shouldHaveText']).toBeDefined();
155
+ expect(addedCommands['shouldHaveLength']).toBeUndefined(); // collection only
156
+ expect(addedCommands['shouldHaveUrl']).toBeDefined();
157
+ // Check Promise.prototype has shouldBeVisible attached
158
+ expect(typeof Promise.prototype.shouldBeVisible).toBe('function');
159
+ });
160
+ });
161
+ });