@testspectra/skills 1.2.29 → 1.2.31

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/README.md CHANGED
@@ -60,7 +60,7 @@ overrides the saved selection for a single invocation.
60
60
  | `shared-steps-and-actions` | `Step.*` business workflows vs `support/actions/*` technical utilities |
61
61
  | `lifecycle-hooks` | `global-hooks/` vs `specs/<suite>/hooks/`, execution order, failure cascade |
62
62
  | `fixtures-data` | `fixtures/*.json` and the ambient `Fixture.*` namespace |
63
- | `network-interception` | `Spectra.intercept()` mocking, polling, and failure-simulation patterns |
63
+ | `network-interception` | `Spectra.intercept()` mocking and failure simulation; `Spectra.spy()` to wait for a real API |
64
64
 
65
65
  Each skill is grounded in `docs/v2/` and the reference workspace at `cli/templates/default`, so an
66
66
  AI coding agent scaffolding TestSpectra scripts produces code that already passes `spectra lint`.
@@ -93,7 +93,8 @@ per platform, traversal keeps one API.
93
93
  Every assertion above (and every collection matcher in section 3) accepts a trailing
94
94
  `options?: { timeoutMs }` to override the adaptive fail-fast timeout for that one call, without
95
95
  raising it for every other assertion in the test — use this when a specific assertion is known to
96
- need longer, e.g. right after a deliberately delayed `Spectra.intercept(..., { delayMs })` mock:
96
+ need longer. To wait for an API, don't raise a timeout: `await spy.waitForResponse()` (or the
97
+ intercept handle's, after a deliberate `delayMs`) — see the `network-interception` skill.
97
98
 
98
99
  ```typescript
99
100
  await ReportPage.statusBadge.shouldHaveText('Ready', { timeoutMs: 5000 });
@@ -153,6 +154,7 @@ collection carry the scope forward too. No behavior difference on web vs. Androi
153
154
  | Scroll | `await Spectra.scroll({ direction: "down", pixels: 400 })` |
154
155
  | Mobile gesture | `await Spectra.swipe({ direction: "left", distance: 300 })` |
155
156
  | Network mock | `const mock = await Spectra.intercept({ url, method, response })` — see the `network-interception` skill |
157
+ | Network spy | `const api = await Spectra.spy(pattern, method); ...; await api.waitForResponse()` — see the `network-interception` skill |
156
158
 
157
159
  ## 6. `Spectra.browser.*` — Browser/Context Level
158
160
 
@@ -1,6 +1,6 @@
1
1
  ---
2
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/shouldHaveCallCount assertions. Use whenever a test needs to mock, stub, or assert on an API call instead of hitting the real backend.
3
+ description: The Spectra.intercept() and Spectra.spy() APIs in TestSpectra — InterceptRule/MockResponse/MockHandle shapes, respondOnce vs respondWith sequential polling, aborting requests, waiting for a real API with spy().waitForResponse(), and waitForCall/shouldHaveCallCount/shouldHaveStatus/shouldHaveRequestBody assertions. Use whenever a test needs to wait for, mock, stub, or assert on an API call.
4
4
  ---
5
5
 
6
6
  # Skill: Network Interception (`Spectra.intercept`)
@@ -31,7 +31,10 @@ interface MockHandle {
31
31
  respondOnce(response: MockResponse): this; // one-time FIFO-queued response
32
32
  abort(errorCode?: 'Failed' | 'Aborted' | 'TimedOut' | 'ConnectionReset'): this;
33
33
  waitForCall(options?: { timeout?: number; count?: number }): Promise<InterceptedRequest>;
34
+ waitForResponse(options?: { timeout?: number; count?: number }): Promise<CDPNetworkEntry>; // the answer delivered, after delayMs
34
35
  shouldHaveCallCount(count: number, options?: { timeout?: number }): Promise<void>; // polls; 0 proves no request went out
36
+ shouldHaveStatus(status: number, options?: { count?: number; timeout?: number }): Promise<void>; // default: the last answer
37
+ shouldHaveRequestBody(expected: string | object, options?: { timeout?: number }): Promise<void>; // object: JSON subset; string: substring
35
38
  readonly callCount: number;
36
39
  readonly calls: InterceptedRequest[];
37
40
  }
@@ -52,7 +55,11 @@ await Spectra.intercept({
52
55
  url: '**/api/v1/brands/*',
53
56
  method: 'PATCH',
54
57
  response: async (request) => {
55
- const reply = await fetch(request.url, { method: 'PATCH', headers: { 'content-type': 'application/json' }, body: request.postData });
58
+ const reply = await fetch(request.url, {
59
+ method: 'PATCH',
60
+ headers: { 'content-type': 'application/json' },
61
+ body: request.postData,
62
+ });
56
63
  return { status: reply.status, body: await reply.text(), delayMs: 3000 };
57
64
  },
58
65
  });
@@ -123,7 +130,42 @@ it('should handle offline mode gracefully when sync fails', async () => {
123
130
 
124
131
  ---
125
132
 
126
- ## 5. Conventions
133
+ ## 5. Pattern: Waiting for an API (`Spectra.spy`)
134
+
135
+ Wait for an API with a spy, never with a guessed duration (`Spectra.sleep`). The spy leaves the
136
+ request alone: the real server answers. Make it before the step that sends the request.
137
+
138
+ ```typescript
139
+ const verify = await Spectra.spy('**/v2/iam/auth/otp/verify', 'POST'); // method default: 'ALL'
140
+ await OtpPage.boxes.setValue('000000');
141
+ await verify.waitForResponse(); // default timeout: the implicit wait
142
+ await verify.shouldHaveStatus(422);
143
+ await verify.shouldHaveRequestBody({ code: '000000' });
144
+ await OtpPage.error.shouldHaveText('Invalid code');
145
+ ```
146
+
147
+ The spy handle has the same `waitForResponse` / `shouldHaveCallCount` / `shouldHaveStatus` /
148
+ `shouldHaveRequestBody` / `callCount` / `calls` as above. Only requests answered after the spy is
149
+ made count. `requestBody` is cut to 500 characters: expect a part of a long body as a string.
150
+
151
+ Intercept with `delayMs` only when the step reads the page while the request is in flight; then
152
+ `waitForResponse()` before checking the result:
153
+
154
+ ```typescript
155
+ const save = await Spectra.intercept({
156
+ url: '**/api/v1/brands',
157
+ method: 'POST',
158
+ response: { status: 201, body: {}, delayMs: 3000 },
159
+ });
160
+ await BrandPage.saveButton.click();
161
+ await BrandPage.saveButton.shouldBeDisabled(); // read mid-request
162
+ await save.waitForResponse();
163
+ await BrandPage.toast.shouldHaveText('Brand created');
164
+ ```
165
+
166
+ ---
167
+
168
+ ## 6. Conventions
127
169
 
128
170
  - Prefer glob URL patterns (`'**/api/v1/...'`) over exact origin strings so mocks survive
129
171
  environment/base-URL changes between local, staging, and CI.
@@ -131,6 +173,8 @@ it('should handle offline mode gracefully when sync fails', async () => {
131
173
  - Only mock what the test is actually about — don't blanket-intercept every domain; the linter and
132
174
  reviewers expect `monitoredDomains` in `spectra.config.ts` (see the `workspace-structure` skill)
133
175
  to reflect real traffic, and over-mocking hides real integration regressions.
176
+ - Wait for an API with `Spectra.spy(...).waitForResponse()`, not a guessed duration. Intercept +
177
+ `delayMs` only when the step reads the page mid-request, then `waitForResponse()` before checking the result.
134
178
  - Always assert on the mock (`waitForCall` / `shouldHaveCallCount(n)`) when the test's purpose is to
135
179
  verify the request was made — don't just assert on UI state and silently hope the mock fired. A
136
180
  blocked submit: `shouldHaveCallCount(0)` after the `should*` on the state the app settles in.
@@ -136,6 +136,14 @@ actual for platform "android": add android.action.ts (or the common one) in …`
136
136
  is never loaded.
137
137
  - An entity that is the same everywhere has no contract: one `common.*` file.
138
138
 
139
+ **Split at the smallest unit.** When a page object, step or flow is the same on every platform but
140
+ one step, keep it one `common.*` file and put only that step behind a contract. Name the contract
141
+ for what the step achieves, not how one platform does it (`passBotCheck`, not `solveRecaptcha`);
142
+ a platform where the step does not exist gets an actual that does nothing. Never copy a whole page
143
+ object or script per platform for one differing step. A script stays `web.test.ts` (or
144
+ `mobile.test.ts`) only when its subject is a mechanism that one platform has (a captcha's caption,
145
+ a browser tab), not when it merely passes through one.
146
+
139
147
  ---
140
148
 
141
149
  ## 5. `spectra.config.ts`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@testspectra/skills",
3
- "version": "1.2.29",
3
+ "version": "1.2.31",
4
4
  "description": "Installer & manager for modular AI agent skills that standardize TestSpectra script authoring (specs, page objects, steps, actions, hooks, fixtures, matchers)",
5
5
  "keywords": [
6
6
  "agents",
@@ -93,7 +93,8 @@ per platform, traversal keeps one API.
93
93
  Every assertion above (and every collection matcher in section 3) accepts a trailing
94
94
  `options?: { timeoutMs }` to override the adaptive fail-fast timeout for that one call, without
95
95
  raising it for every other assertion in the test — use this when a specific assertion is known to
96
- need longer, e.g. right after a deliberately delayed `Spectra.intercept(..., { delayMs })` mock:
96
+ need longer. To wait for an API, don't raise a timeout: `await spy.waitForResponse()` (or the
97
+ intercept handle's, after a deliberate `delayMs`) — see the `network-interception` skill.
97
98
 
98
99
  ```typescript
99
100
  await ReportPage.statusBadge.shouldHaveText('Ready', { timeoutMs: 5000 });
@@ -153,6 +154,7 @@ collection carry the scope forward too. No behavior difference on web vs. Androi
153
154
  | Scroll | `await Spectra.scroll({ direction: "down", pixels: 400 })` |
154
155
  | Mobile gesture | `await Spectra.swipe({ direction: "left", distance: 300 })` |
155
156
  | Network mock | `const mock = await Spectra.intercept({ url, method, response })` — see the `network-interception` skill |
157
+ | Network spy | `const api = await Spectra.spy(pattern, method); ...; await api.waitForResponse()` — see the `network-interception` skill |
156
158
 
157
159
  ## 6. `Spectra.browser.*` — Browser/Context Level
158
160
 
@@ -1,6 +1,6 @@
1
1
  ---
2
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/shouldHaveCallCount assertions. Use whenever a test needs to mock, stub, or assert on an API call instead of hitting the real backend.
3
+ description: The Spectra.intercept() and Spectra.spy() APIs in TestSpectra — InterceptRule/MockResponse/MockHandle shapes, respondOnce vs respondWith sequential polling, aborting requests, waiting for a real API with spy().waitForResponse(), and waitForCall/shouldHaveCallCount/shouldHaveStatus/shouldHaveRequestBody assertions. Use whenever a test needs to wait for, mock, stub, or assert on an API call.
4
4
  ---
5
5
 
6
6
  # Skill: Network Interception (`Spectra.intercept`)
@@ -31,7 +31,10 @@ interface MockHandle {
31
31
  respondOnce(response: MockResponse): this; // one-time FIFO-queued response
32
32
  abort(errorCode?: 'Failed' | 'Aborted' | 'TimedOut' | 'ConnectionReset'): this;
33
33
  waitForCall(options?: { timeout?: number; count?: number }): Promise<InterceptedRequest>;
34
+ waitForResponse(options?: { timeout?: number; count?: number }): Promise<CDPNetworkEntry>; // the answer delivered, after delayMs
34
35
  shouldHaveCallCount(count: number, options?: { timeout?: number }): Promise<void>; // polls; 0 proves no request went out
36
+ shouldHaveStatus(status: number, options?: { count?: number; timeout?: number }): Promise<void>; // default: the last answer
37
+ shouldHaveRequestBody(expected: string | object, options?: { timeout?: number }): Promise<void>; // object: JSON subset; string: substring
35
38
  readonly callCount: number;
36
39
  readonly calls: InterceptedRequest[];
37
40
  }
@@ -52,7 +55,11 @@ await Spectra.intercept({
52
55
  url: '**/api/v1/brands/*',
53
56
  method: 'PATCH',
54
57
  response: async (request) => {
55
- const reply = await fetch(request.url, { method: 'PATCH', headers: { 'content-type': 'application/json' }, body: request.postData });
58
+ const reply = await fetch(request.url, {
59
+ method: 'PATCH',
60
+ headers: { 'content-type': 'application/json' },
61
+ body: request.postData,
62
+ });
56
63
  return { status: reply.status, body: await reply.text(), delayMs: 3000 };
57
64
  },
58
65
  });
@@ -123,7 +130,42 @@ it('should handle offline mode gracefully when sync fails', async () => {
123
130
 
124
131
  ---
125
132
 
126
- ## 5. Conventions
133
+ ## 5. Pattern: Waiting for an API (`Spectra.spy`)
134
+
135
+ Wait for an API with a spy, never with a guessed duration (`Spectra.sleep`). The spy leaves the
136
+ request alone: the real server answers. Make it before the step that sends the request.
137
+
138
+ ```typescript
139
+ const verify = await Spectra.spy('**/v2/iam/auth/otp/verify', 'POST'); // method default: 'ALL'
140
+ await OtpPage.boxes.setValue('000000');
141
+ await verify.waitForResponse(); // default timeout: the implicit wait
142
+ await verify.shouldHaveStatus(422);
143
+ await verify.shouldHaveRequestBody({ code: '000000' });
144
+ await OtpPage.error.shouldHaveText('Invalid code');
145
+ ```
146
+
147
+ The spy handle has the same `waitForResponse` / `shouldHaveCallCount` / `shouldHaveStatus` /
148
+ `shouldHaveRequestBody` / `callCount` / `calls` as above. Only requests answered after the spy is
149
+ made count. `requestBody` is cut to 500 characters: expect a part of a long body as a string.
150
+
151
+ Intercept with `delayMs` only when the step reads the page while the request is in flight; then
152
+ `waitForResponse()` before checking the result:
153
+
154
+ ```typescript
155
+ const save = await Spectra.intercept({
156
+ url: '**/api/v1/brands',
157
+ method: 'POST',
158
+ response: { status: 201, body: {}, delayMs: 3000 },
159
+ });
160
+ await BrandPage.saveButton.click();
161
+ await BrandPage.saveButton.shouldBeDisabled(); // read mid-request
162
+ await save.waitForResponse();
163
+ await BrandPage.toast.shouldHaveText('Brand created');
164
+ ```
165
+
166
+ ---
167
+
168
+ ## 6. Conventions
127
169
 
128
170
  - Prefer glob URL patterns (`'**/api/v1/...'`) over exact origin strings so mocks survive
129
171
  environment/base-URL changes between local, staging, and CI.
@@ -131,6 +173,8 @@ it('should handle offline mode gracefully when sync fails', async () => {
131
173
  - Only mock what the test is actually about — don't blanket-intercept every domain; the linter and
132
174
  reviewers expect `monitoredDomains` in `spectra.config.ts` (see the `workspace-structure` skill)
133
175
  to reflect real traffic, and over-mocking hides real integration regressions.
176
+ - Wait for an API with `Spectra.spy(...).waitForResponse()`, not a guessed duration. Intercept +
177
+ `delayMs` only when the step reads the page mid-request, then `waitForResponse()` before checking the result.
134
178
  - Always assert on the mock (`waitForCall` / `shouldHaveCallCount(n)`) when the test's purpose is to
135
179
  verify the request was made — don't just assert on UI state and silently hope the mock fired. A
136
180
  blocked submit: `shouldHaveCallCount(0)` after the `should*` on the state the app settles in.
@@ -136,6 +136,14 @@ actual for platform "android": add android.action.ts (or the common one) in …`
136
136
  is never loaded.
137
137
  - An entity that is the same everywhere has no contract: one `common.*` file.
138
138
 
139
+ **Split at the smallest unit.** When a page object, step or flow is the same on every platform but
140
+ one step, keep it one `common.*` file and put only that step behind a contract. Name the contract
141
+ for what the step achieves, not how one platform does it (`passBotCheck`, not `solveRecaptcha`);
142
+ a platform where the step does not exist gets an actual that does nothing. Never copy a whole page
143
+ object or script per platform for one differing step. A script stays `web.test.ts` (or
144
+ `mobile.test.ts`) only when its subject is a mechanism that one platform has (a captcha's caption,
145
+ a browser tab), not when it merely passes through one.
146
+
139
147
  ---
140
148
 
141
149
  ## 5. `spectra.config.ts`