@testspectra/skills 1.1.8-rc.2 → 1.1.8-rc.21

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.
@@ -1,115 +1,115 @@
1
- ---
2
- name: network-interception
3
- description: The Spectra.intercept() API for deterministic network mocking in TestSpectra — InterceptRule/MockResponse/MockHandle shapes, respondOnce vs respondWith sequential polling, aborting requests, and waitForCall/callCount assertions. Use whenever a test needs to mock, stub, or assert on an API call instead of hitting the real backend.
4
- ---
5
-
6
- # Skill: Network Interception (`Spectra.intercept`)
7
-
8
- Use this skill whenever a test needs deterministic control over a network response — mocking an
9
- API, simulating an error, or asserting a call happened — rather than depending on a real backend.
10
-
11
- ---
12
-
13
- ## 1. Rule & Handle Shapes
14
-
15
- ```typescript
16
- interface InterceptRule {
17
- url: string | RegExp; // supports glob, e.g. '**/api/v1/payments/charge'
18
- method?: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' | 'ALL';
19
- response?: MockResponse | ((request: InterceptedRequest) => MockResponse | Promise<MockResponse>);
20
- }
21
-
22
- interface MockResponse {
23
- status?: number; // default: 200
24
- headers?: Record<string, string>;
25
- body?: any;
26
- delayMs?: number;
27
- }
28
-
29
- interface MockHandle {
30
- respondWith(response: MockResponse): this; // default response for all subsequent matches
31
- respondOnce(response: MockResponse): this; // one-time FIFO-queued response
32
- abort(errorCode?: 'Failed' | 'Aborted' | 'TimedOut' | 'ConnectionReset'): this;
33
- waitForCall(options?: { timeout?: number; count?: number }): Promise<InterceptedRequest>;
34
- readonly callCount: number;
35
- readonly calls: InterceptedRequest[];
36
- }
37
- ```
38
-
39
- `Spectra.intercept(rule)` is `async` and returns a `MockHandle` — always `await` it before
40
- interacting with the UI that triggers the call.
41
-
42
- ---
43
-
44
- ## 2. Pattern: Single-Shot Static Mock
45
-
46
- ```typescript
47
- it('should mock payment failure response gracefully', async () => {
48
- const paymentMock = await Spectra.intercept({
49
- url: '**/api/v1/payments/charge',
50
- method: 'POST',
51
- response: {
52
- status: 402,
53
- headers: { 'content-type': 'application/json' },
54
- body: { error: 'insufficient_funds', message: 'Your card was declined due to insufficient funds.' },
55
- },
56
- });
57
-
58
- await CheckoutPage.submitPaymentButton.click();
59
-
60
- await paymentMock.waitForCall({ timeout: 5000 });
61
- expect(paymentMock.callCount).toBe(1);
62
-
63
- await CheckoutPage.errorBanner.shouldBeVisible();
64
- await CheckoutPage.errorBanner.shouldContainText('insufficient funds');
65
- });
66
- ```
67
-
68
- ---
69
-
70
- ## 3. Pattern: Multi-Response Sequential Polling
71
-
72
- Use `respondOnce` (FIFO queue, consumed one per matching request) plus a trailing `respondWith`
73
- default for status-polling / retry flows:
74
-
75
- ```typescript
76
- it('should poll export job status until completed', async () => {
77
- const jobMock = await Spectra.intercept({ url: '**/api/v1/export/job-99', method: 'GET' });
78
-
79
- jobMock.respondOnce({ status: 200, body: { status: 'pending' } });
80
- jobMock.respondOnce({ status: 200, body: { status: 'processing' } });
81
- jobMock.respondWith({ status: 200, body: { status: 'completed', downloadUrl: '/files/report.pdf' } });
82
-
83
- await ExportPage.startExportButton.click();
84
- await ExportPage.downloadLink.shouldBeVisible();
85
- });
86
- ```
87
-
88
- ---
89
-
90
- ## 4. Pattern: Simulating a Network Failure
91
-
92
- ```typescript
93
- it('should handle offline mode gracefully when sync fails', async () => {
94
- const syncMock = await Spectra.intercept({ url: '**/api/v1/sync' });
95
- syncMock.abort('ConnectionReset');
96
-
97
- await DashboardPage.syncButton.click();
98
- await DashboardPage.offlineBanner.shouldBeVisible();
99
- });
100
- ```
101
-
102
- ---
103
-
104
- ## 5. Conventions
105
-
106
- - Prefer glob URL patterns (`'**/api/v1/...'`) over exact origin strings so mocks survive
107
- environment/base-URL changes between local, staging, and CI.
108
- - Only mock what the test is actually about — don't blanket-intercept every domain; the linter and
109
- reviewers expect `monitoredDomains` in `spectra.config.ts` (see the `workspace-structure` skill)
110
- to reflect real traffic, and over-mocking hides real integration regressions.
111
- - Always assert on the mock (`waitForCall` / `callCount`) when the test's purpose is to verify the
112
- request was made — don't just assert on UI state and silently hope the mock fired.
113
- - Every real (non-mocked) network call made during a run is separately captured and written to
114
- `.testspectra/reports/network-resources.json` for post-run diagnostics — you don't need to record
115
- this yourself.
1
+ ---
2
+ name: network-interception
3
+ description: The Spectra.intercept() API for deterministic network mocking in TestSpectra — InterceptRule/MockResponse/MockHandle shapes, respondOnce vs respondWith sequential polling, aborting requests, and waitForCall/callCount assertions. Use whenever a test needs to mock, stub, or assert on an API call instead of hitting the real backend.
4
+ ---
5
+
6
+ # Skill: Network Interception (`Spectra.intercept`)
7
+
8
+ Use this skill whenever a test needs deterministic control over a network response — mocking an
9
+ API, simulating an error, or asserting a call happened — rather than depending on a real backend.
10
+
11
+ ---
12
+
13
+ ## 1. Rule & Handle Shapes
14
+
15
+ ```typescript
16
+ interface InterceptRule {
17
+ url: string | RegExp; // supports glob, e.g. '**/api/v1/payments/charge'
18
+ method?: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' | 'ALL';
19
+ response?: MockResponse | ((request: InterceptedRequest) => MockResponse | Promise<MockResponse>);
20
+ }
21
+
22
+ interface MockResponse {
23
+ status?: number; // default: 200
24
+ headers?: Record<string, string>;
25
+ body?: any;
26
+ delayMs?: number;
27
+ }
28
+
29
+ interface MockHandle {
30
+ respondWith(response: MockResponse): this; // default response for all subsequent matches
31
+ respondOnce(response: MockResponse): this; // one-time FIFO-queued response
32
+ abort(errorCode?: 'Failed' | 'Aborted' | 'TimedOut' | 'ConnectionReset'): this;
33
+ waitForCall(options?: { timeout?: number; count?: number }): Promise<InterceptedRequest>;
34
+ readonly callCount: number;
35
+ readonly calls: InterceptedRequest[];
36
+ }
37
+ ```
38
+
39
+ `Spectra.intercept(rule)` is `async` and returns a `MockHandle` — always `await` it before
40
+ interacting with the UI that triggers the call.
41
+
42
+ ---
43
+
44
+ ## 2. Pattern: Single-Shot Static Mock
45
+
46
+ ```typescript
47
+ it('should mock payment failure response gracefully', async () => {
48
+ const paymentMock = await Spectra.intercept({
49
+ url: '**/api/v1/payments/charge',
50
+ method: 'POST',
51
+ response: {
52
+ status: 402,
53
+ headers: { 'content-type': 'application/json' },
54
+ body: { error: 'insufficient_funds', message: 'Your card was declined due to insufficient funds.' },
55
+ },
56
+ });
57
+
58
+ await CheckoutPage.submitPaymentButton.click();
59
+
60
+ await paymentMock.waitForCall({ timeout: 5000 });
61
+ expect(paymentMock.callCount).toBe(1);
62
+
63
+ await CheckoutPage.errorBanner.shouldBeVisible();
64
+ await CheckoutPage.errorBanner.shouldContainText('insufficient funds');
65
+ });
66
+ ```
67
+
68
+ ---
69
+
70
+ ## 3. Pattern: Multi-Response Sequential Polling
71
+
72
+ Use `respondOnce` (FIFO queue, consumed one per matching request) plus a trailing `respondWith`
73
+ default for status-polling / retry flows:
74
+
75
+ ```typescript
76
+ it('should poll export job status until completed', async () => {
77
+ const jobMock = await Spectra.intercept({ url: '**/api/v1/export/job-99', method: 'GET' });
78
+
79
+ jobMock.respondOnce({ status: 200, body: { status: 'pending' } });
80
+ jobMock.respondOnce({ status: 200, body: { status: 'processing' } });
81
+ jobMock.respondWith({ status: 200, body: { status: 'completed', downloadUrl: '/files/report.pdf' } });
82
+
83
+ await ExportPage.startExportButton.click();
84
+ await ExportPage.downloadLink.shouldBeVisible();
85
+ });
86
+ ```
87
+
88
+ ---
89
+
90
+ ## 4. Pattern: Simulating a Network Failure
91
+
92
+ ```typescript
93
+ it('should handle offline mode gracefully when sync fails', async () => {
94
+ const syncMock = await Spectra.intercept({ url: '**/api/v1/sync' });
95
+ syncMock.abort('ConnectionReset');
96
+
97
+ await DashboardPage.syncButton.click();
98
+ await DashboardPage.offlineBanner.shouldBeVisible();
99
+ });
100
+ ```
101
+
102
+ ---
103
+
104
+ ## 5. Conventions
105
+
106
+ - Prefer glob URL patterns (`'**/api/v1/...'`) over exact origin strings so mocks survive
107
+ environment/base-URL changes between local, staging, and CI.
108
+ - Only mock what the test is actually about — don't blanket-intercept every domain; the linter and
109
+ reviewers expect `monitoredDomains` in `spectra.config.ts` (see the `workspace-structure` skill)
110
+ to reflect real traffic, and over-mocking hides real integration regressions.
111
+ - Always assert on the mock (`waitForCall` / `callCount`) when the test's purpose is to verify the
112
+ request was made — don't just assert on UI state and silently hope the mock fired.
113
+ - Every real (non-mocked) network call made during a run is separately captured and written to
114
+ `.testspectra/reports/network-resources.json` for post-run diagnostics — you don't need to record
115
+ this yourself.
@@ -1,132 +1,132 @@
1
- ---
2
- name: page-objects-and-selectors
3
- description: How to write a TestSpectra Page Object (Spectra.get / Spectra.getAll singleton pattern) and how the ~accessibility-id vs #native-id selector contract resolves identically across Web, Android, and iOS. Use whenever declaring element locators or a new Page Object.
4
- ---
5
-
6
- # Skill: Page Objects & the Selector Contract
7
-
8
- Use this skill whenever you add a new Page Object, add a getter to an existing one, or need to
9
- pick a selector string.
10
-
11
- ---
12
-
13
- ## 1. Page Object Pattern
14
-
15
- A Page Object is a class with getter properties that return `Spectra.get(selector)` /
16
- `Spectra.getAll(selector)`, exported as a singleton default export, and consumed with **zero
17
- imports** anywhere else in the workspace (see the `workspace-structure` skill).
18
-
19
- ```typescript
20
- // page-objects/MatchersPage/web.ts
21
- class MatchersPage {
22
- get visibleElement() {
23
- return Spectra.get('#demo-visible-el');
24
- }
25
- get hiddenElement() {
26
- return Spectra.get('#demo-hidden-el');
27
- }
28
- get existingElement() {
29
- return Spectra.get('#demo-existing-el');
30
- }
31
- get productCards() {
32
- return Spectra.getAll('.demo-product-card');
33
- } // collection
34
-
35
- async open(section = '') {
36
- await Spectra.navigate('/' + (section ? '#' + section : ''));
37
- }
38
- }
39
-
40
- export default new MatchersPage();
41
- ```
42
-
43
- Rules:
44
-
45
- - One getter per element/collection; name the getter for what the element _is_ (`submitButton`,
46
- `statCards`), not for the selector string.
47
- - `Spectra.get(selector)` → single element; `Spectra.getAll(selector)` → collection (used with
48
- collection matchers like `.shouldHaveLength(n)`).
49
- - Add reusable navigation/composite helpers as regular `async` methods on the same class (e.g.
50
- `open()` above), not as standalone actions, when they are specific to that one page.
51
- - File suffix follows the platform-suffix rule: `web.ts` for CSS selectors, `mobile.ts` /
52
- `android.ts` / `ios.ts` for native selectors targeting the same logical page.
53
-
54
- ---
55
-
56
- ## 2. Selector Contract: `~name` vs `#name`
57
-
58
- TestSpectra resolves selectors identically across every platform — a selector written once means
59
- the same thing everywhere. Each prefix maps to **exactly one** attribute; there is no silent
60
- cross-matching.
61
-
62
- | Selector | Meaning | Web | Android | iOS |
63
- | :----------------- | :------------------- | :---------------------- | :------------------------------- | :----------------------- |
64
- | `~name` | **accessibility id** | `[aria-label="name"]` | `content-desc` | accessibility identifier |
65
- | `#name` | **native id** | CSS `#name` | `resource-id` | — (n/a) |
66
- | `//xpath` | raw XPath | delegated to the driver | delegated to the on-device agent | n/a |
67
- | `name` (no prefix) | lenient fallback | n/a | native id, then accessibility id | n/a |
68
-
69
- ### Which one to use
70
-
71
- - **Prefer `~name`** for anything a user can perceive — it's the portable, semantic, recommended
72
- default across all platforms.
73
- - **Use `#name`** only when the element has a stable test/native id and no accessibility label
74
- (React Native `testID`, Android `resource-id`, CSS `id`).
75
- - Set the _matching_ attribute on the app/page element so the selector actually resolves:
76
-
77
- ```tsx
78
- // React Native — these resolve DIFFERENT selectors:
79
- <Pressable accessibilityLabel="submit-button" /> // → Spectra.get('~submit-button')
80
- <Pressable testID="submit-button" /> // → Spectra.get('#submit-button')
81
- ```
82
-
83
- ```html
84
- <!-- Web -->
85
- <button aria-label="submit-button">…</button>
86
- <!-- → Spectra.get('~submit-button') -->
87
- <button id="submit-button">…</button>
88
- <!-- → Spectra.get('#submit-button') -->
89
- ```
90
-
91
- - When `testID` and `accessibilityLabel` are set to the same value, `~name` and `#name` are
92
- interchangeable; they diverge the moment the two attributes differ.
93
- - Avoid the unprefixed lenient form (`name`) in new tests — it's Android-only leniency kept for
94
- backwards compatibility, not part of the cross-platform contract. Always write an explicit `~`
95
- or `#`.
96
- - Avoid raw `//xpath` unless there is genuinely no stable id/label to hook into — it's brittle and
97
- bypasses the fast, cached full-hierarchy snapshot lookup that `~`/`#` selectors use on Android.
98
-
99
- ---
100
-
101
- ## 3. Scoping a Lookup Instead of Writing a Compound Selector
102
-
103
- When the same selector repeats across rows (a list of cards, a table), **do not** reach for a
104
- compound CSS selector (`.card:nth-child(3) .buy-btn`) or an index baked into an xpath — those are
105
- brittle and don't have a cross-platform equivalent (Android has no CSS nth-child). Chain
106
- `.get(childSelector)` / `.getAll(childSelector)` off an already-resolved element instead; it
107
- scopes the lookup to that element's own subtree:
108
-
109
- ```typescript
110
- class ProductsPage {
111
- get cards() {
112
- return Spectra.getAll('.product-card');
113
- }
114
- cardBuyButton(index: number) {
115
- return this.cards.nth(index).get('.buy-btn');
116
- }
117
- }
118
- ```
119
-
120
- This is a Page Object concern, not a new selector prefix — `~`/`#`/xpath all still apply as the
121
- child selector, resolved within the parent's subtree instead of the whole document/screen. See the
122
- `matchers-and-assertions` skill for the full `.get()`/`.getAll()` chaining behavior.
123
-
124
- ---
125
-
126
- ## 4. Never Use Raw `$` / `$$` / `browser` / `driver`
127
-
128
- TestSpectra encapsulates the entire runtime into a single global `Spectra` object. There is no raw
129
- WebdriverIO `$`, `$$`, `browser`, or `driver` in test-authoring code — always go through
130
- `Spectra.get()` / `Spectra.getAll()` for locators and `Spectra.*` / `Spectra.browser.*` for
131
- everything else (navigation, gestures, storage, network). See the `matchers-and-assertions` skill
132
- for the full command surface.
1
+ ---
2
+ name: page-objects-and-selectors
3
+ description: How to write a TestSpectra Page Object (Spectra.get / Spectra.getAll singleton pattern) and how the ~accessibility-id vs #native-id selector contract resolves identically across Web, Android, and iOS. Use whenever declaring element locators or a new Page Object.
4
+ ---
5
+
6
+ # Skill: Page Objects & the Selector Contract
7
+
8
+ Use this skill whenever you add a new Page Object, add a getter to an existing one, or need to
9
+ pick a selector string.
10
+
11
+ ---
12
+
13
+ ## 1. Page Object Pattern
14
+
15
+ A Page Object is a class with getter properties that return `Spectra.get(selector)` /
16
+ `Spectra.getAll(selector)`, exported as a singleton default export, and consumed with **zero
17
+ imports** anywhere else in the workspace (see the `workspace-structure` skill).
18
+
19
+ ```typescript
20
+ // page-objects/MatchersPage/web.ts
21
+ class MatchersPage {
22
+ get visibleElement() {
23
+ return Spectra.get('#demo-visible-el');
24
+ }
25
+ get hiddenElement() {
26
+ return Spectra.get('#demo-hidden-el');
27
+ }
28
+ get existingElement() {
29
+ return Spectra.get('#demo-existing-el');
30
+ }
31
+ get productCards() {
32
+ return Spectra.getAll('.demo-product-card');
33
+ } // collection
34
+
35
+ async open(section = '') {
36
+ await Spectra.navigate('/' + (section ? '#' + section : ''));
37
+ }
38
+ }
39
+
40
+ export default new MatchersPage();
41
+ ```
42
+
43
+ Rules:
44
+
45
+ - One getter per element/collection; name the getter for what the element _is_ (`submitButton`,
46
+ `statCards`), not for the selector string.
47
+ - `Spectra.get(selector)` → single element; `Spectra.getAll(selector)` → collection (used with
48
+ collection matchers like `.shouldHaveLength(n)`).
49
+ - Add reusable navigation/composite helpers as regular `async` methods on the same class (e.g.
50
+ `open()` above), not as standalone actions, when they are specific to that one page.
51
+ - File suffix follows the platform-suffix rule: `web.ts` for CSS selectors, `mobile.ts` /
52
+ `android.ts` / `ios.ts` for native selectors targeting the same logical page.
53
+
54
+ ---
55
+
56
+ ## 2. Selector Contract: `~name` vs `#name`
57
+
58
+ TestSpectra resolves selectors identically across every platform — a selector written once means
59
+ the same thing everywhere. Each prefix maps to **exactly one** attribute; there is no silent
60
+ cross-matching.
61
+
62
+ | Selector | Meaning | Web | Android | iOS |
63
+ | :----------------- | :------------------- | :---------------------- | :------------------------------- | :----------------------- |
64
+ | `~name` | **accessibility id** | `[aria-label="name"]` | `content-desc` | accessibility identifier |
65
+ | `#name` | **native id** | CSS `#name` | `resource-id` | — (n/a) |
66
+ | `//xpath` | raw XPath | delegated to the driver | delegated to the on-device agent | n/a |
67
+ | `name` (no prefix) | lenient fallback | n/a | native id, then accessibility id | n/a |
68
+
69
+ ### Which one to use
70
+
71
+ - **Prefer `~name`** for anything a user can perceive — it's the portable, semantic, recommended
72
+ default across all platforms.
73
+ - **Use `#name`** only when the element has a stable test/native id and no accessibility label
74
+ (React Native `testID`, Android `resource-id`, CSS `id`).
75
+ - Set the _matching_ attribute on the app/page element so the selector actually resolves:
76
+
77
+ ```tsx
78
+ // React Native — these resolve DIFFERENT selectors:
79
+ <Pressable accessibilityLabel="submit-button" /> // → Spectra.get('~submit-button')
80
+ <Pressable testID="submit-button" /> // → Spectra.get('#submit-button')
81
+ ```
82
+
83
+ ```html
84
+ <!-- Web -->
85
+ <button aria-label="submit-button">…</button>
86
+ <!-- → Spectra.get('~submit-button') -->
87
+ <button id="submit-button">…</button>
88
+ <!-- → Spectra.get('#submit-button') -->
89
+ ```
90
+
91
+ - When `testID` and `accessibilityLabel` are set to the same value, `~name` and `#name` are
92
+ interchangeable; they diverge the moment the two attributes differ.
93
+ - Avoid the unprefixed lenient form (`name`) in new tests — it's Android-only leniency kept for
94
+ backwards compatibility, not part of the cross-platform contract. Always write an explicit `~`
95
+ or `#`.
96
+ - Avoid raw `//xpath` unless there is genuinely no stable id/label to hook into — it's brittle and
97
+ bypasses the fast, cached full-hierarchy snapshot lookup that `~`/`#` selectors use on Android.
98
+
99
+ ---
100
+
101
+ ## 3. Scoping a Lookup Instead of Writing a Compound Selector
102
+
103
+ When the same selector repeats across rows (a list of cards, a table), **do not** reach for a
104
+ compound CSS selector (`.card:nth-child(3) .buy-btn`) or an index baked into an xpath — those are
105
+ brittle and don't have a cross-platform equivalent (Android has no CSS nth-child). Chain
106
+ `.get(childSelector)` / `.getAll(childSelector)` off an already-resolved element instead; it
107
+ scopes the lookup to that element's own subtree:
108
+
109
+ ```typescript
110
+ class ProductsPage {
111
+ get cards() {
112
+ return Spectra.getAll('.product-card');
113
+ }
114
+ cardBuyButton(index: number) {
115
+ return this.cards.nth(index).get('.buy-btn');
116
+ }
117
+ }
118
+ ```
119
+
120
+ This is a Page Object concern, not a new selector prefix — `~`/`#`/xpath all still apply as the
121
+ child selector, resolved within the parent's subtree instead of the whole document/screen. See the
122
+ `matchers-and-assertions` skill for the full `.get()`/`.getAll()` chaining behavior.
123
+
124
+ ---
125
+
126
+ ## 4. Never Use Raw `$` / `$$` / `browser` / `driver`
127
+
128
+ TestSpectra encapsulates the entire runtime into a single global `Spectra` object. There is no raw
129
+ WebdriverIO `$`, `$$`, `browser`, or `driver` in test-authoring code — always go through
130
+ `Spectra.get()` / `Spectra.getAll()` for locators and `Spectra.*` / `Spectra.browser.*` for
131
+ everything else (navigation, gestures, storage, network). See the `matchers-and-assertions` skill
132
+ for the full command surface.