@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.
- package/LICENSE.md +48 -48
- package/README.md +78 -78
- package/bin/spectra-skills.js +7 -7
- package/dist/index.js +29 -29
- package/dist/skills/fixtures-data/SKILL.md +109 -109
- package/dist/skills/lifecycle-hooks/SKILL.md +114 -114
- package/dist/skills/matchers-and-assertions/SKILL.md +156 -156
- package/dist/skills/network-interception/SKILL.md +115 -115
- package/dist/skills/page-objects-and-selectors/SKILL.md +132 -132
- package/dist/skills/shared-steps-and-actions/SKILL.md +184 -184
- package/dist/skills/spec-and-suite-authoring/SKILL.md +132 -132
- package/dist/skills/workspace-structure/SKILL.md +193 -193
- package/package.json +11 -12
- package/skills/fixtures-data/SKILL.md +109 -109
- package/skills/lifecycle-hooks/SKILL.md +114 -114
- package/skills/matchers-and-assertions/SKILL.md +156 -156
- package/skills/network-interception/SKILL.md +115 -115
- package/skills/page-objects-and-selectors/SKILL.md +132 -132
- package/skills/shared-steps-and-actions/SKILL.md +184 -184
- package/skills/spec-and-suite-authoring/SKILL.md +132 -132
- package/skills/workspace-structure/SKILL.md +193 -193
|
@@ -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.
|