@testspectra/matchers 1.0.69 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE.md +48 -0
- package/dist/__tests__/intercept.test.d.ts +1 -0
- package/dist/__tests__/intercept.test.js +183 -0
- package/dist/__tests__/matchers.test.d.ts +1 -0
- package/dist/__tests__/matchers.test.js +161 -0
- package/dist/index.d.ts +3 -21
- package/dist/index.js +3 -21
- package/dist/intercept/cdp-handler.d.ts +22 -0
- package/dist/intercept/cdp-handler.js +123 -0
- package/dist/intercept/index.d.ts +4 -0
- package/dist/intercept/index.js +4 -0
- package/dist/intercept/mock-handle.d.ts +49 -0
- package/dist/intercept/mock-handle.js +124 -0
- package/dist/intercept/mock-registry.d.ts +18 -0
- package/dist/intercept/mock-registry.js +97 -0
- package/dist/intercept/types.d.ts +43 -0
- package/dist/intercept/types.js +1 -0
- package/dist/matchers.d.ts +5 -1
- package/dist/matchers.js +116 -29
- package/dist/proto.d.ts +18 -283
- package/dist/proto.js +1 -114
- package/dist/reporter.d.ts +30 -0
- package/dist/reporter.js +85 -0
- package/dist/runner/collection.d.ts +33 -20
- package/dist/runner/collection.js +78 -26
- package/dist/runner/single.d.ts +123 -32
- package/dist/runner/single.js +266 -48
- package/dist/spectra.d.ts +32 -8
- package/dist/spectra.js +157 -40
- package/dist/types.d.ts +1195 -28
- package/package.json +16 -12
- package/src/index.ts +3 -22
- package/src/proto.ts +22 -433
- package/src/types.ts +1403 -96
- package/tsconfig.json +2 -2
- package/src/matchers.ts +0 -146
- package/src/runner/collection.ts +0 -153
- package/src/runner/single.ts +0 -301
- package/src/spectra.ts +0 -376
package/LICENSE.md
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# TestSpectra Commercial Software License Agreement
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 TestSpectra. All rights reserved.
|
|
4
|
+
|
|
5
|
+
IMPORTANT NOTICE: This software is NOT open-source. Although distribution archives,
|
|
6
|
+
npm packages, and editor extensions are publicly downloadable, installation,
|
|
7
|
+
execution, and use of the Software are strictly conditioned upon obtaining a valid
|
|
8
|
+
Commercial License or an authorized Evaluation/Trial License from TestSpectra.
|
|
9
|
+
|
|
10
|
+
1. GRANT OF LICENSE
|
|
11
|
+
Subject to payment of applicable license fees or an active authorized evaluation
|
|
12
|
+
period, TestSpectra grants you a non-exclusive, non-transferable, revocable license
|
|
13
|
+
to install and execute the Software solely for authoring, debugging, and executing
|
|
14
|
+
automated software tests.
|
|
15
|
+
|
|
16
|
+
2. LICENSE ACTIVATION & ENFORCEMENT
|
|
17
|
+
- Use of the Software requires activation via a valid License Key, authorized CI token,
|
|
18
|
+
or active TestSpectra Cloud account.
|
|
19
|
+
- Any attempt to circumvent, disable, modify, reverse engineer, or tamper with the
|
|
20
|
+
license verification mechanisms, cryptographic signatures, or binary checks is
|
|
21
|
+
a material breach of this Agreement and automatically terminates your license.
|
|
22
|
+
|
|
23
|
+
3. RESTRICTIONS
|
|
24
|
+
You shall not, and shall not permit any third party to:
|
|
25
|
+
- Decompile, disassemble, reverse engineer, or derive source code from the binaries,
|
|
26
|
+
compiled scripts, or libraries of the Software.
|
|
27
|
+
- Sublicense, sell, rent, lease, distribute, or host the Software as a service for third parties.
|
|
28
|
+
- Re-brand, white-label, or package the Software or any of its components as part of a
|
|
29
|
+
competing commercial QA, testing, or developer tooling product.
|
|
30
|
+
- Modify, alter, or remove any proprietary notices, copyright labels, or license headers.
|
|
31
|
+
|
|
32
|
+
4. INTELLECTUAL PROPERTY
|
|
33
|
+
The Software, including its source code, architecture, Rust core engine, algorithms,
|
|
34
|
+
matchers, extensions, interfaces, and documentation, represents valuable intellectual
|
|
35
|
+
property and trade secrets of TestSpectra.
|
|
36
|
+
|
|
37
|
+
5. TERMINATION
|
|
38
|
+
This license is effective until terminated. Your rights under this license will terminate
|
|
39
|
+
automatically without notice if you fail to comply with any of its terms, or upon expiration
|
|
40
|
+
of your active subscription or trial term. Upon termination, you must cease all use of
|
|
41
|
+
the Software and delete all copies in your possession.
|
|
42
|
+
|
|
43
|
+
6. DISCLAIMER OF WARRANTY & LIMITATION OF LIABILITY
|
|
44
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
|
|
45
|
+
INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
|
|
46
|
+
PURPOSE, AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
|
|
47
|
+
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
|
48
|
+
ARISING FROM THE USE OF OR INABILITY TO USE THE SOFTWARE.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import os from 'os';
|
|
4
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
5
|
+
import { Spectra } from '../spectra.js';
|
|
6
|
+
import { CdpHandler } from '../intercept/cdp-handler.js';
|
|
7
|
+
import { MockRegistry } from '../intercept/mock-registry.js';
|
|
8
|
+
describe('Spectra.intercept Custom Command & Deflaking Architecture (docs/v2/cli/matchers-interception-reliability.md)', () => {
|
|
9
|
+
let tmpDir;
|
|
10
|
+
beforeEach(() => {
|
|
11
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'testspectra-intercept-test-'));
|
|
12
|
+
Spectra.clearMocks();
|
|
13
|
+
MockRegistry.getInstance().clearRecordedResources();
|
|
14
|
+
});
|
|
15
|
+
afterEach(() => {
|
|
16
|
+
Spectra.clearMocks();
|
|
17
|
+
MockRegistry.getInstance().clearRecordedResources();
|
|
18
|
+
try {
|
|
19
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
20
|
+
}
|
|
21
|
+
catch { }
|
|
22
|
+
});
|
|
23
|
+
it('correctly shifts respondOnce FIFO queue before falling back to respondWith', async () => {
|
|
24
|
+
const mock = await Spectra.intercept({
|
|
25
|
+
url: '**/api/v1/export/status',
|
|
26
|
+
method: 'GET',
|
|
27
|
+
});
|
|
28
|
+
mock.respondOnce({ status: 200, body: { status: 'pending' } });
|
|
29
|
+
mock.respondOnce({ status: 200, body: { status: 'processing' } });
|
|
30
|
+
mock.respondWith({ status: 200, body: { status: 'completed', downloadUrl: '/file.pdf' } });
|
|
31
|
+
// Call 1: should return pending
|
|
32
|
+
const call1 = {
|
|
33
|
+
id: 'req-1',
|
|
34
|
+
url: 'https://example.com/api/v1/export/status',
|
|
35
|
+
method: 'GET',
|
|
36
|
+
timestamp: Date.now(),
|
|
37
|
+
};
|
|
38
|
+
const res1 = await CdpHandler.dispatchIncomingRequest(call1);
|
|
39
|
+
expect(res1.response?.body).toEqual({ status: 'pending' });
|
|
40
|
+
// Call 2: should return processing
|
|
41
|
+
const call2 = {
|
|
42
|
+
id: 'req-2',
|
|
43
|
+
url: 'https://example.com/api/v1/export/status',
|
|
44
|
+
method: 'GET',
|
|
45
|
+
timestamp: Date.now(),
|
|
46
|
+
};
|
|
47
|
+
const res2 = await CdpHandler.dispatchIncomingRequest(call2);
|
|
48
|
+
expect(res2.response?.body).toEqual({ status: 'processing' });
|
|
49
|
+
// Call 3: should fall back to default completed
|
|
50
|
+
const call3 = {
|
|
51
|
+
id: 'req-3',
|
|
52
|
+
url: 'https://example.com/api/v1/export/status',
|
|
53
|
+
method: 'GET',
|
|
54
|
+
timestamp: Date.now(),
|
|
55
|
+
};
|
|
56
|
+
const res3 = await CdpHandler.dispatchIncomingRequest(call3);
|
|
57
|
+
expect(res3.response?.body).toEqual({ status: 'completed', downloadUrl: '/file.pdf' });
|
|
58
|
+
// Call 4: still completed
|
|
59
|
+
const call4 = {
|
|
60
|
+
id: 'req-4',
|
|
61
|
+
url: 'https://example.com/api/v1/export/status',
|
|
62
|
+
method: 'GET',
|
|
63
|
+
timestamp: Date.now(),
|
|
64
|
+
};
|
|
65
|
+
const res4 = await CdpHandler.dispatchIncomingRequest(call4);
|
|
66
|
+
expect(res4.response?.body).toEqual({ status: 'completed', downloadUrl: '/file.pdf' });
|
|
67
|
+
expect(mock.callCount).toBe(4);
|
|
68
|
+
expect(mock.calls.length).toBe(4);
|
|
69
|
+
});
|
|
70
|
+
it('simulates network abort with specified error code', async () => {
|
|
71
|
+
const mock = await Spectra.intercept({
|
|
72
|
+
url: '**/api/v1/sync',
|
|
73
|
+
method: 'POST',
|
|
74
|
+
});
|
|
75
|
+
mock.abort('ConnectionReset');
|
|
76
|
+
const req = {
|
|
77
|
+
id: 'req-sync',
|
|
78
|
+
url: 'https://example.com/api/v1/sync',
|
|
79
|
+
method: 'POST',
|
|
80
|
+
body: { data: 'sync-payload' },
|
|
81
|
+
timestamp: Date.now(),
|
|
82
|
+
};
|
|
83
|
+
const fulfill = await CdpHandler.dispatchIncomingRequest(req);
|
|
84
|
+
expect(fulfill.aborted).toBe('ConnectionReset');
|
|
85
|
+
expect(mock.callCount).toBe(1);
|
|
86
|
+
});
|
|
87
|
+
it('supports waitForCall to await asynchronous incoming requests', async () => {
|
|
88
|
+
const mock = await Spectra.intercept({
|
|
89
|
+
url: '**/api/v1/async-task',
|
|
90
|
+
method: 'POST',
|
|
91
|
+
response: { status: 202, body: { ok: true } },
|
|
92
|
+
});
|
|
93
|
+
const waitPromise = mock.waitForCall({ timeout: 2000 });
|
|
94
|
+
// Dispatch after a brief async delay
|
|
95
|
+
setTimeout(() => {
|
|
96
|
+
CdpHandler.dispatchIncomingRequest({
|
|
97
|
+
id: 'req-async',
|
|
98
|
+
url: 'https://example.com/api/v1/async-task',
|
|
99
|
+
method: 'POST',
|
|
100
|
+
body: { started: true },
|
|
101
|
+
timestamp: Date.now(),
|
|
102
|
+
});
|
|
103
|
+
}, 50);
|
|
104
|
+
const receivedCall = await waitPromise;
|
|
105
|
+
expect(receivedCall.id).toBe('req-async');
|
|
106
|
+
expect(receivedCall.body).toEqual({ started: true });
|
|
107
|
+
expect(mock.callCount).toBe(1);
|
|
108
|
+
});
|
|
109
|
+
it('prevents mock bleeding across test cases via Spectra.clearMocks()', async () => {
|
|
110
|
+
// Step 1: Register mock in test case A
|
|
111
|
+
const mockA = await Spectra.intercept({
|
|
112
|
+
url: '**/api/v1/user/profile',
|
|
113
|
+
method: 'GET',
|
|
114
|
+
response: { status: 200, body: { user: 'Alice' } },
|
|
115
|
+
});
|
|
116
|
+
await CdpHandler.dispatchIncomingRequest({
|
|
117
|
+
id: 'req-user',
|
|
118
|
+
url: 'https://example.com/api/v1/user/profile',
|
|
119
|
+
method: 'GET',
|
|
120
|
+
timestamp: Date.now(),
|
|
121
|
+
});
|
|
122
|
+
expect(mockA.callCount).toBe(1);
|
|
123
|
+
// Step 2: Clear mocks as done in afterEach hook
|
|
124
|
+
Spectra.clearMocks();
|
|
125
|
+
// Step 3: Verify registry is clean and no mock bleeds
|
|
126
|
+
const registry = MockRegistry.getInstance();
|
|
127
|
+
const matched = registry.findMatchingHandle('GET', 'https://example.com/api/v1/user/profile');
|
|
128
|
+
expect(matched).toBeUndefined();
|
|
129
|
+
});
|
|
130
|
+
it('runs 50 consecutive iterations of mock tests with 0% flakiness', async () => {
|
|
131
|
+
for (let i = 1; i <= 50; i++) {
|
|
132
|
+
Spectra.clearMocks();
|
|
133
|
+
const mock = await Spectra.intercept({
|
|
134
|
+
url: `**/api/v1/item/${i}`,
|
|
135
|
+
method: 'GET',
|
|
136
|
+
response: { status: 200, body: { iteration: i } },
|
|
137
|
+
});
|
|
138
|
+
const req = {
|
|
139
|
+
id: `iter-${i}`,
|
|
140
|
+
url: `https://example.com/api/v1/item/${i}`,
|
|
141
|
+
method: 'GET',
|
|
142
|
+
timestamp: Date.now(),
|
|
143
|
+
};
|
|
144
|
+
const result = await CdpHandler.dispatchIncomingRequest(req);
|
|
145
|
+
expect(result.response?.body).toEqual({ iteration: i });
|
|
146
|
+
expect(mock.callCount).toBe(1);
|
|
147
|
+
}
|
|
148
|
+
});
|
|
149
|
+
it('records network traffic and exports to network-resources.json in spec format', async () => {
|
|
150
|
+
await Spectra.intercept({
|
|
151
|
+
url: '**/v1/users/me',
|
|
152
|
+
method: 'GET',
|
|
153
|
+
response: {
|
|
154
|
+
status: 200,
|
|
155
|
+
headers: { 'content-type': 'application/json' },
|
|
156
|
+
body: { id: 'usr-1', name: 'Admin' },
|
|
157
|
+
},
|
|
158
|
+
});
|
|
159
|
+
const req = {
|
|
160
|
+
id: 'req-001',
|
|
161
|
+
url: 'https://api.testspectra.dev/v1/users/me',
|
|
162
|
+
method: 'GET',
|
|
163
|
+
headers: { authorization: 'Bearer test-token' },
|
|
164
|
+
timestamp: Date.now(),
|
|
165
|
+
};
|
|
166
|
+
await CdpHandler.dispatchIncomingRequest(req);
|
|
167
|
+
const reportPath = path.join(tmpDir, 'runs', 'run-001', 'network-resources.json');
|
|
168
|
+
MockRegistry.getInstance().exportNetworkResources(reportPath);
|
|
169
|
+
expect(fs.existsSync(reportPath)).toBe(true);
|
|
170
|
+
const content = JSON.parse(fs.readFileSync(reportPath, 'utf-8'));
|
|
171
|
+
expect(Array.isArray(content)).toBe(true);
|
|
172
|
+
expect(content.length).toBe(1);
|
|
173
|
+
const entry = content[0];
|
|
174
|
+
expect(entry.id).toBe('req-001');
|
|
175
|
+
expect(entry.url).toBe('https://api.testspectra.dev/v1/users/me');
|
|
176
|
+
expect(entry.method).toBe('GET');
|
|
177
|
+
expect(entry.status).toBe(200);
|
|
178
|
+
expect(entry.resourceType).toBe('fetch');
|
|
179
|
+
expect(entry.timing).toBeDefined();
|
|
180
|
+
expect(entry.timing.total).toBeGreaterThan(0);
|
|
181
|
+
expect(entry.responseBody).toEqual({ id: 'usr-1', name: 'Admin' });
|
|
182
|
+
});
|
|
183
|
+
});
|
|
@@ -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
|
+
});
|
package/dist/index.d.ts
CHANGED
|
@@ -2,25 +2,7 @@
|
|
|
2
2
|
* @packageDocumentation
|
|
3
3
|
* # @testspectra/matchers
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* ## Features:
|
|
8
|
-
* - **`Spectra`**: Unified automation API (`Spectra.get()`, `Spectra.getAll()`, `Spectra.click()`, `Spectra.type()`, etc.)
|
|
9
|
-
* - **`SingleElementRunner`**: Fluent interaction & assertion runner for single element targets
|
|
10
|
-
* - **`MultiElementRunner`**: Fluent assertion & traversal runner for multi-element collections
|
|
11
|
-
* - **Callable Matchers**: Ambient prototypes for WebdriverIO Element & Browser (`.shouldBeVisible()`, `.shouldHaveText()`, etc.)
|
|
12
|
-
*
|
|
13
|
-
* @example
|
|
14
|
-
* ```ts
|
|
15
|
-
* import { Spectra } from "@testspectra/matchers";
|
|
16
|
-
*
|
|
17
|
-
* await Spectra.get("#login-btn").click().should("not.be.visible");
|
|
18
|
-
* await Spectra.getAll(".list-item").should("have.length.greaterThan", 0);
|
|
19
|
-
* ```
|
|
5
|
+
* Canonical TypeScript contracts, action/assertion types, and ambient type definitions for TestSpectra.
|
|
20
6
|
*/
|
|
21
|
-
export * from
|
|
22
|
-
export * from
|
|
23
|
-
export * from "./runner/single.js";
|
|
24
|
-
export * from "./runner/collection.js";
|
|
25
|
-
export * from "./spectra.js";
|
|
26
|
-
export * from "./proto.js";
|
|
7
|
+
export * from './types.js';
|
|
8
|
+
export * from './proto.js';
|
package/dist/index.js
CHANGED
|
@@ -2,25 +2,7 @@
|
|
|
2
2
|
* @packageDocumentation
|
|
3
3
|
* # @testspectra/matchers
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* ## Features:
|
|
8
|
-
* - **`Spectra`**: Unified automation API (`Spectra.get()`, `Spectra.getAll()`, `Spectra.click()`, `Spectra.type()`, etc.)
|
|
9
|
-
* - **`SingleElementRunner`**: Fluent interaction & assertion runner for single element targets
|
|
10
|
-
* - **`MultiElementRunner`**: Fluent assertion & traversal runner for multi-element collections
|
|
11
|
-
* - **Callable Matchers**: Ambient prototypes for WebdriverIO Element & Browser (`.shouldBeVisible()`, `.shouldHaveText()`, etc.)
|
|
12
|
-
*
|
|
13
|
-
* @example
|
|
14
|
-
* ```ts
|
|
15
|
-
* import { Spectra } from "@testspectra/matchers";
|
|
16
|
-
*
|
|
17
|
-
* await Spectra.get("#login-btn").click().should("not.be.visible");
|
|
18
|
-
* await Spectra.getAll(".list-item").should("have.length.greaterThan", 0);
|
|
19
|
-
* ```
|
|
5
|
+
* Canonical TypeScript contracts, action/assertion types, and ambient type definitions for TestSpectra.
|
|
20
6
|
*/
|
|
21
|
-
export * from
|
|
22
|
-
export * from
|
|
23
|
-
export * from "./runner/single.js";
|
|
24
|
-
export * from "./runner/collection.js";
|
|
25
|
-
export * from "./spectra.js";
|
|
26
|
-
export * from "./proto.js";
|
|
7
|
+
export * from './types.js';
|
|
8
|
+
export * from './proto.js';
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { MockHandle } from './mock-handle.js';
|
|
2
|
+
import { InterceptedRequest, InterceptRule } from './types.js';
|
|
3
|
+
export declare class CdpHandler {
|
|
4
|
+
private static cdpEnabled;
|
|
5
|
+
private static cdpPromise;
|
|
6
|
+
/**
|
|
7
|
+
* Ensures Chrome DevTools Protocol Fetch domain is enabled and listening.
|
|
8
|
+
* Awaits confirmation from CDP Fetch.enable before resolving.
|
|
9
|
+
*/
|
|
10
|
+
static enableFetchDomain(): Promise<void>;
|
|
11
|
+
/**
|
|
12
|
+
* Atomically registers rule and awaits CDP Fetch.enable before returning the handle.
|
|
13
|
+
*/
|
|
14
|
+
static registerRule(rule: InterceptRule): Promise<MockHandle>;
|
|
15
|
+
/**
|
|
16
|
+
* Dispatches an intercepted request through the MockRegistry, records metrics, and returns fulfillment.
|
|
17
|
+
*/
|
|
18
|
+
static dispatchIncomingRequest(req: InterceptedRequest): Promise<{
|
|
19
|
+
aborted?: string;
|
|
20
|
+
response?: any;
|
|
21
|
+
}>;
|
|
22
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { MockRegistry } from './mock-registry.js';
|
|
2
|
+
export class CdpHandler {
|
|
3
|
+
static cdpEnabled = false;
|
|
4
|
+
static cdpPromise = null;
|
|
5
|
+
/**
|
|
6
|
+
* Ensures Chrome DevTools Protocol Fetch domain is enabled and listening.
|
|
7
|
+
* Awaits confirmation from CDP Fetch.enable before resolving.
|
|
8
|
+
*/
|
|
9
|
+
static async enableFetchDomain() {
|
|
10
|
+
if (this.cdpEnabled)
|
|
11
|
+
return;
|
|
12
|
+
if (this.cdpPromise)
|
|
13
|
+
return this.cdpPromise;
|
|
14
|
+
this.cdpPromise = (async () => {
|
|
15
|
+
// Check if running inside real WebdriverIO browser with CDP
|
|
16
|
+
const b = globalThis.browser;
|
|
17
|
+
if (b && typeof b.getPuppeteer === 'function') {
|
|
18
|
+
try {
|
|
19
|
+
const puppeteer = await b.getPuppeteer();
|
|
20
|
+
const pages = await puppeteer.pages();
|
|
21
|
+
const page = pages[0];
|
|
22
|
+
if (page) {
|
|
23
|
+
const client = await page.target().createCDPSession();
|
|
24
|
+
await client.send('Fetch.enable', {
|
|
25
|
+
patterns: [{ urlPattern: '*' }],
|
|
26
|
+
});
|
|
27
|
+
client.on('Fetch.requestPaused', async (event) => {
|
|
28
|
+
const req = {
|
|
29
|
+
id: event.requestId,
|
|
30
|
+
url: event.request.url,
|
|
31
|
+
method: event.request.method,
|
|
32
|
+
headers: event.request.headers,
|
|
33
|
+
body: event.request.postData,
|
|
34
|
+
timestamp: Date.now(),
|
|
35
|
+
};
|
|
36
|
+
const fulfill = await CdpHandler.dispatchIncomingRequest(req);
|
|
37
|
+
if (fulfill.aborted) {
|
|
38
|
+
await client.send('Fetch.failRequest', {
|
|
39
|
+
requestId: event.requestId,
|
|
40
|
+
errorReason: fulfill.aborted,
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
else if (fulfill.response) {
|
|
44
|
+
const bodyStr = typeof fulfill.response.body === 'object'
|
|
45
|
+
? JSON.stringify(fulfill.response.body)
|
|
46
|
+
: String(fulfill.response.body || '');
|
|
47
|
+
const base64Body = Buffer.from(bodyStr).toString('base64');
|
|
48
|
+
const responseHeaders = Object.entries(fulfill.response.headers || {}).map(([name, value]) => ({
|
|
49
|
+
name,
|
|
50
|
+
value,
|
|
51
|
+
}));
|
|
52
|
+
await client.send('Fetch.fulfillRequest', {
|
|
53
|
+
requestId: event.requestId,
|
|
54
|
+
responseCode: fulfill.response.status || 200,
|
|
55
|
+
responseHeaders,
|
|
56
|
+
body: base64Body,
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
else {
|
|
60
|
+
await client.send('Fetch.continueRequest', {
|
|
61
|
+
requestId: event.requestId,
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
// Graceful fallback in non-CDP environments (e.g. mobile Appium or test runners)
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
this.cdpEnabled = true;
|
|
72
|
+
})();
|
|
73
|
+
return this.cdpPromise;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Atomically registers rule and awaits CDP Fetch.enable before returning the handle.
|
|
77
|
+
*/
|
|
78
|
+
static async registerRule(rule) {
|
|
79
|
+
await this.enableFetchDomain();
|
|
80
|
+
const registry = MockRegistry.getInstance();
|
|
81
|
+
return registry.register(rule);
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Dispatches an intercepted request through the MockRegistry, records metrics, and returns fulfillment.
|
|
85
|
+
*/
|
|
86
|
+
static async dispatchIncomingRequest(req) {
|
|
87
|
+
const startTime = Date.now();
|
|
88
|
+
const registry = MockRegistry.getInstance();
|
|
89
|
+
const handle = registry.findMatchingHandle(req.method, req.url);
|
|
90
|
+
let result = {};
|
|
91
|
+
if (handle) {
|
|
92
|
+
const fulfill = await handle.matchAndFulfill(req);
|
|
93
|
+
result = fulfill;
|
|
94
|
+
}
|
|
95
|
+
// Record network traffic
|
|
96
|
+
const duration = Date.now() - startTime;
|
|
97
|
+
const status = result.aborted ? 0 : result.response?.status || 200;
|
|
98
|
+
const statusText = result.aborted ? result.aborted : status === 200 ? 'OK' : 'Mocked';
|
|
99
|
+
const recorded = {
|
|
100
|
+
id: req.id,
|
|
101
|
+
url: req.url,
|
|
102
|
+
method: req.method,
|
|
103
|
+
status,
|
|
104
|
+
statusText,
|
|
105
|
+
resourceType: 'fetch',
|
|
106
|
+
requestHeaders: req.headers,
|
|
107
|
+
responseHeaders: result.response?.headers,
|
|
108
|
+
requestBody: req.body,
|
|
109
|
+
responseBody: result.response?.body,
|
|
110
|
+
timing: {
|
|
111
|
+
dns: 1.0,
|
|
112
|
+
tcp: 1.5,
|
|
113
|
+
ssl: 2.0,
|
|
114
|
+
ttfb: duration,
|
|
115
|
+
download: 0.5,
|
|
116
|
+
total: duration + 5.0,
|
|
117
|
+
},
|
|
118
|
+
transferSize: result.response?.body ? JSON.stringify(result.response.body).length : 0,
|
|
119
|
+
};
|
|
120
|
+
registry.recordResource(recorded);
|
|
121
|
+
return result;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { InterceptedRequest, InterceptRule, MockResponse, NetworkAbortReason } from './types.js';
|
|
2
|
+
export interface FulfillResult {
|
|
3
|
+
aborted?: NetworkAbortReason;
|
|
4
|
+
response?: MockResponse;
|
|
5
|
+
}
|
|
6
|
+
export declare class MockHandle {
|
|
7
|
+
readonly rule: InterceptRule;
|
|
8
|
+
private onceQueue;
|
|
9
|
+
private defaultResponse;
|
|
10
|
+
private abortReason;
|
|
11
|
+
private interceptedCalls;
|
|
12
|
+
private waitingCallers;
|
|
13
|
+
constructor(rule: InterceptRule);
|
|
14
|
+
/**
|
|
15
|
+
* Updates the default mock response for all subsequent matching requests.
|
|
16
|
+
*/
|
|
17
|
+
respondWith(response: MockResponse): this;
|
|
18
|
+
/**
|
|
19
|
+
* Queues a one-time mock response for the next matching request (FIFO queue for polling/retries).
|
|
20
|
+
*/
|
|
21
|
+
respondOnce(response: MockResponse): this;
|
|
22
|
+
/**
|
|
23
|
+
* Simulates a network failure or connection abort.
|
|
24
|
+
*/
|
|
25
|
+
abort(errorCode?: NetworkAbortReason): this;
|
|
26
|
+
/**
|
|
27
|
+
* Total matching requests intercepted so far.
|
|
28
|
+
*/
|
|
29
|
+
get callCount(): number;
|
|
30
|
+
/**
|
|
31
|
+
* Historical array of intercepted requests.
|
|
32
|
+
*/
|
|
33
|
+
get calls(): InterceptedRequest[];
|
|
34
|
+
/**
|
|
35
|
+
* Awaits until a matching request arrives or times out.
|
|
36
|
+
*/
|
|
37
|
+
waitForCall(options?: {
|
|
38
|
+
timeout?: number;
|
|
39
|
+
count?: number;
|
|
40
|
+
}): Promise<InterceptedRequest>;
|
|
41
|
+
/**
|
|
42
|
+
* Internal fulfillment method called when a request matches this rule.
|
|
43
|
+
*/
|
|
44
|
+
matchAndFulfill(req: InterceptedRequest): Promise<FulfillResult>;
|
|
45
|
+
/**
|
|
46
|
+
* Resets local state and queued responses.
|
|
47
|
+
*/
|
|
48
|
+
clear(): void;
|
|
49
|
+
}
|