@testspectra/skills 1.1.0-rc.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE.md +48 -0
- package/README.md +78 -0
- package/bin/spectra-skills.js +7 -0
- package/dist/commands/add.d.ts +1 -0
- package/dist/commands/add.js +44 -0
- package/dist/commands/init.d.ts +1 -0
- package/dist/commands/init.js +54 -0
- package/dist/commands/list.d.ts +1 -0
- package/dist/commands/list.js +22 -0
- package/dist/commands/remove.d.ts +1 -0
- package/dist/commands/remove.js +26 -0
- package/dist/commands/update.d.ts +1 -0
- package/dist/commands/update.js +30 -0
- package/dist/detector.d.ts +35 -0
- package/dist/detector.js +129 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +119 -0
- package/dist/registry.d.ts +8 -0
- package/dist/registry.js +48 -0
- package/dist/skills/fixtures-data/SKILL.md +109 -0
- package/dist/skills/lifecycle-hooks/SKILL.md +114 -0
- package/dist/skills/matchers-and-assertions/SKILL.md +123 -0
- package/dist/skills/network-interception/SKILL.md +115 -0
- package/dist/skills/page-objects-and-selectors/SKILL.md +107 -0
- package/dist/skills/shared-steps-and-actions/SKILL.md +184 -0
- package/dist/skills/spec-and-suite-authoring/SKILL.md +132 -0
- package/dist/skills/workspace-structure/SKILL.md +181 -0
- package/package.json +59 -0
- package/skills/fixtures-data/SKILL.md +109 -0
- package/skills/lifecycle-hooks/SKILL.md +114 -0
- package/skills/matchers-and-assertions/SKILL.md +123 -0
- package/skills/network-interception/SKILL.md +115 -0
- package/skills/page-objects-and-selectors/SKILL.md +107 -0
- package/skills/shared-steps-and-actions/SKILL.md +184 -0
- package/skills/spec-and-suite-authoring/SKILL.md +132 -0
- package/skills/workspace-structure/SKILL.md +181 -0
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: lifecycle-hooks
|
|
3
|
+
description: TestSpectra lifecycle hooks — global-hooks/ (batch-run only) vs specs/<suite>/hooks/ (before, beforeEach, afterEach, after), their execution order in single-test vs global-batch runs, and the failure-cascade rules. Use whenever adding setup/teardown logic instead of duplicating it inside individual test cases.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Skill: Suite & Global Lifecycle Hooks
|
|
7
|
+
|
|
8
|
+
Use this skill whenever you need setup/teardown logic (auth, DB seeding, cookie clearing, failure
|
|
9
|
+
screenshots) that shouldn't be duplicated inside every `it()` body.
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## 1. Two Hook Levels — Know the Difference
|
|
14
|
+
|
|
15
|
+
| Hook | Level | Scope | When it runs |
|
|
16
|
+
| :------------------------------- | :----- | :----------------- | :----------------------------------------------------------------------------------------------- |
|
|
17
|
+
| `global-hooks/before` | Global | Entire run session | **Once**, before all suites — **Global Batch Run / CI only**, never during single-test debugging |
|
|
18
|
+
| `global-hooks/after` | Global | Entire run session | **Once**, after all suites — Global Batch Run only |
|
|
19
|
+
| `specs/<suite>/hooks/before` | Suite | Test suite | Once before the first test case in the suite |
|
|
20
|
+
| `specs/<suite>/hooks/beforeEach` | Suite | Test case | Before every test case in the suite |
|
|
21
|
+
| `specs/<suite>/hooks/afterEach` | Suite | Test case | After every test case in the suite |
|
|
22
|
+
| `specs/<suite>/hooks/after` | Suite | Test suite | Once after the last test case in the suite |
|
|
23
|
+
|
|
24
|
+
**Global hooks are skipped when running/debugging a single test case** — put anything a single
|
|
25
|
+
test genuinely depends on (navigating to `/login`, clearing storage) in a **suite** hook instead,
|
|
26
|
+
not a global one, or the test will fail/behave differently when run in isolation.
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
## 2. Directory Layout
|
|
31
|
+
|
|
32
|
+
```
|
|
33
|
+
workspace-root/
|
|
34
|
+
├── global-hooks/
|
|
35
|
+
│ ├── before/
|
|
36
|
+
│ │ ├── web.hook.ts
|
|
37
|
+
│ │ └── mobile.hook.ts
|
|
38
|
+
│ └── after/
|
|
39
|
+
│ ├── web.hook.ts
|
|
40
|
+
│ └── mobile.hook.ts
|
|
41
|
+
└── specs/
|
|
42
|
+
└── authentication/
|
|
43
|
+
├── suite.md
|
|
44
|
+
├── hooks/
|
|
45
|
+
│ ├── before/web.hook.ts
|
|
46
|
+
│ ├── beforeEach/web.hook.ts # e.g. clear cookies, navigate to /login
|
|
47
|
+
│ ├── afterEach/web.hook.ts # e.g. failure screenshot, clear storage
|
|
48
|
+
│ └── after/web.hook.ts
|
|
49
|
+
├── TC-0001-valid-login/
|
|
50
|
+
└── TC-0002-invalid-password/
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
---
|
|
54
|
+
|
|
55
|
+
## 3. Execution Order
|
|
56
|
+
|
|
57
|
+
### Single Test Case Run (VS Code CodeLens `▶ Run on <Target>`)
|
|
58
|
+
|
|
59
|
+
```
|
|
60
|
+
suite/hooks/before()
|
|
61
|
+
└─► suite/hooks/beforeEach() ─► it('TC-0001') ─► suite/hooks/afterEach()
|
|
62
|
+
suite/hooks/after()
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Global hooks do **not** run here.
|
|
66
|
+
|
|
67
|
+
### Global Batch Run (`▶ Global Run` / CI/CD)
|
|
68
|
+
|
|
69
|
+
```
|
|
70
|
+
global-hooks/before() (once, whole batch)
|
|
71
|
+
├─► Suite 1: before() → [beforeEach → TC → afterEach]* → after()
|
|
72
|
+
├─► Suite 2: before() → [beforeEach → TC → afterEach]* → after()
|
|
73
|
+
global-hooks/after() (once, whole batch)
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
---
|
|
77
|
+
|
|
78
|
+
## 4. Implementation: Anonymous Async Default Export
|
|
79
|
+
|
|
80
|
+
Same convention as steps/actions — one anonymous async function, default export, zero manual
|
|
81
|
+
imports:
|
|
82
|
+
|
|
83
|
+
```typescript
|
|
84
|
+
// specs/authentication/hooks/beforeEach/web.hook.ts
|
|
85
|
+
export default async function () {
|
|
86
|
+
await Spectra.navigate('/auth/login');
|
|
87
|
+
await Spectra.browser.clearLocalStorage();
|
|
88
|
+
await Spectra.browser.clearCookies();
|
|
89
|
+
}
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
```typescript
|
|
93
|
+
// global-hooks/before/web.hook.ts
|
|
94
|
+
export default async function () {
|
|
95
|
+
const response = await fetch(`${process.env.BASE_URL}/health`);
|
|
96
|
+
if (!response.ok) {
|
|
97
|
+
throw new Error(`Target environment is not healthy: ${response.statusText}`);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
---
|
|
103
|
+
|
|
104
|
+
## 5. Failure Cascade Rules
|
|
105
|
+
|
|
106
|
+
1. **`beforeEach` fails** → that test case is marked `failed` (Hook Failure), its `it()` body is
|
|
107
|
+
skipped, but `afterEach` still runs for cleanup.
|
|
108
|
+
2. **Suite `before` fails** → every test case in that suite is marked `skipped`/`error`; suite
|
|
109
|
+
`after` still runs to clean up partial resources.
|
|
110
|
+
3. **`global-hooks/before` fails** → the entire batch run aborts immediately; `global-hooks/after`
|
|
111
|
+
still runs to finalize cleanup.
|
|
112
|
+
|
|
113
|
+
Write hook bodies defensively with this cascade in mind — teardown logic in `after`/`afterEach`
|
|
114
|
+
must not assume `before`/`beforeEach` fully succeeded.
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: matchers-and-assertions
|
|
3
|
+
description: Full reference of the TestSpectra element action/assertion API (shouldBeVisible, click, type...), collection matchers (shouldHaveLength...), and global Spectra.* / Spectra.browser.* commands. Use whenever writing or reviewing the body of an it() test script, step, or action.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Skill: Matchers, Actions & the Global `Spectra` API
|
|
7
|
+
|
|
8
|
+
Use this skill as the canonical vocabulary reference whenever you write the body of a test script
|
|
9
|
+
(`it()`), a shared step, or a custom action. TestSpectra never uses raw WebdriverIO/Jest matcher
|
|
10
|
+
names (`toBeDisplayed`, `setValue`, `$`) in authored code — always use the TestSpectra syntax below.
|
|
11
|
+
Every call is automatically traced (`trackCommand()`) and emitted as one natural-English log line
|
|
12
|
+
per action/assertion in `.testspectra/reports/`.
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## 1. Element Action Commands (called on a Page Object getter)
|
|
17
|
+
|
|
18
|
+
| TestSpectra syntax | Semantic log output |
|
|
19
|
+
| :----------------------------------- | :-------------------------------------- |
|
|
20
|
+
| `await el.click()` | `Click on element "{el}"` |
|
|
21
|
+
| `await el.doubleClick()` | `Double-click on {el}` |
|
|
22
|
+
| `await el.rightClick()` | `Right-click on {el}` |
|
|
23
|
+
| `await el.type(text, options?)` | `Type "{text}" into element "{el}"` |
|
|
24
|
+
| `await el.clear()` | `Clear text in {el}` |
|
|
25
|
+
| `await el.select(option)` | `Select option "{option}" in {el}` |
|
|
26
|
+
| `await el.hover()` | `Hover over {el}` |
|
|
27
|
+
| `await source.dragDrop(dest)` | `Drag {source} and drop onto {dest}` |
|
|
28
|
+
| `await el.scrollIntoView()` | `Scroll {el} into view` |
|
|
29
|
+
| `await el.longPress(durationMs)` | `Long-press on {el} for {durationMs}ms` |
|
|
30
|
+
| `await el.waitForElement(timeoutMs)` | `Wait for {el} to be displayed` |
|
|
31
|
+
|
|
32
|
+
`type()` accepts an options object, e.g. `{ clearFirst: true }` to clear the field before typing.
|
|
33
|
+
|
|
34
|
+
---
|
|
35
|
+
|
|
36
|
+
## 2. Element Assertions (called on a Page Object getter)
|
|
37
|
+
|
|
38
|
+
| TestSpectra syntax | Semantic log output |
|
|
39
|
+
| :---------------------------------------- | :------------------------------------------------------- |
|
|
40
|
+
| `await el.shouldBeVisible()` | `Expect element "{el}" to be visible` |
|
|
41
|
+
| `await el.shouldNotBeVisible()` | `Expect element "{el}" not to be visible` |
|
|
42
|
+
| `await el.shouldExist()` | `Expect element "{el}" to exist in DOM` |
|
|
43
|
+
| `await el.shouldNotExist()` | `Expect element "{el}" not to exist in DOM` |
|
|
44
|
+
| `await el.shouldBeClickable()` | `Expect element "{el}" to be clickable` |
|
|
45
|
+
| `await el.shouldNotBeClickable()` | `Expect element "{el}" not to be clickable` |
|
|
46
|
+
| `await el.shouldBeEnabled()` | `Expect element "{el}" to be enabled` |
|
|
47
|
+
| `await el.shouldBeDisabled()` | `Expect element "{el}" to be disabled` |
|
|
48
|
+
| `await el.shouldBeChecked()` | `Expect element "{el}" to be checked` |
|
|
49
|
+
| `await el.shouldNotBeChecked()` | `Expect element "{el}" not to be checked` |
|
|
50
|
+
| `await el.shouldHaveText(text)` | `Expect element "{el}" to have text "{text}"` |
|
|
51
|
+
| `await el.shouldContainText(substr)` | `Expect element "{el}" to contain text "{substr}"` |
|
|
52
|
+
| `await el.shouldHaveValue(val)` | `Expect element "{el}" to have value "{val}"` |
|
|
53
|
+
| `await el.shouldHaveAttribute(attr, val)` | `Expect element "{el}" attribute "{attr}" to be "{val}"` |
|
|
54
|
+
| `await el.shouldBeFocused()` | `Expect element "{el}" to be focused` |
|
|
55
|
+
|
|
56
|
+
---
|
|
57
|
+
|
|
58
|
+
## 3. Collection Matchers (on `Spectra.getAll(selector)` getters)
|
|
59
|
+
|
|
60
|
+
| TestSpectra syntax | Semantic log output |
|
|
61
|
+
| :------------------------------------------- | :------------------------------------------------------ |
|
|
62
|
+
| `await col.shouldHaveLength(n)` | `Expect collection "{selector}" count to equal {n}` |
|
|
63
|
+
| `await col.shouldNotHaveLength(n)` | `Expect collection "{selector}" count not to equal {n}` |
|
|
64
|
+
| `await col.shouldHaveLengthGreaterThan(min)` | `count to be greater than {min}` |
|
|
65
|
+
| `await col.shouldHaveLengthLessThan(max)` | `count to be less than {max}` |
|
|
66
|
+
| `await col.shouldBeEmpty()` | `Expect collection "{selector}" to be empty` |
|
|
67
|
+
| `await col.shouldNotBeEmpty()` | `Expect collection "{selector}" not to be empty` |
|
|
68
|
+
|
|
69
|
+
---
|
|
70
|
+
|
|
71
|
+
## 4. Global `Spectra.*` Commands
|
|
72
|
+
|
|
73
|
+
| Category | Syntax |
|
|
74
|
+
| :------------- | :------------------------------------------------------------------------------------------------------- |
|
|
75
|
+
| Navigation | `await Spectra.navigate("/login")` / `.back()` / `.forward()` / `.refresh()` |
|
|
76
|
+
| Element query | `Spectra.get(selector)` / `Spectra.getAll(selector)` |
|
|
77
|
+
| Window | `await Spectra.setViewport(1920, 1080)` |
|
|
78
|
+
| Timing | `await Spectra.wait(1000)` |
|
|
79
|
+
| Keyboard | `await Spectra.pressKey("Enter")` |
|
|
80
|
+
| Scroll | `await Spectra.scroll({ direction: "down", pixels: 400 })` |
|
|
81
|
+
| Mobile gesture | `await Spectra.swipe({ direction: "left", distance: 300 })` |
|
|
82
|
+
| Network mock | `const mock = await Spectra.intercept({ url, method, response })` — see the `network-interception` skill |
|
|
83
|
+
|
|
84
|
+
## 5. `Spectra.browser.*` — Browser/Context Level
|
|
85
|
+
|
|
86
|
+
| Syntax | Purpose |
|
|
87
|
+
| :-------------------------------------------------- | :------------------------------ |
|
|
88
|
+
| `await Spectra.browser.clearCookies()` | Clear all cookies |
|
|
89
|
+
| `await Spectra.browser.clearLocalStorage()` | Clear localStorage |
|
|
90
|
+
| `await Spectra.browser.shouldHaveUrl(url)` | Assert exact URL |
|
|
91
|
+
| `await Spectra.browser.shouldContainUrl(substr)` | Assert URL contains substring |
|
|
92
|
+
| `await Spectra.browser.shouldHaveTitle(title)` | Assert exact document title |
|
|
93
|
+
| `await Spectra.browser.shouldContainTitle(substr)` | Assert title contains substring |
|
|
94
|
+
| `await Spectra.browser.shouldBeLoaded()` | Assert page fully loaded |
|
|
95
|
+
| `await Spectra.browser.shouldHaveNoConsoleErrors()` | Assert zero console errors |
|
|
96
|
+
|
|
97
|
+
---
|
|
98
|
+
|
|
99
|
+
## 6. Full Example
|
|
100
|
+
|
|
101
|
+
```typescript
|
|
102
|
+
it('should login as administrator and assert dashboard metrics', async () => {
|
|
103
|
+
await Spectra.navigate('/auth/login');
|
|
104
|
+
await Spectra.browser.clearCookies();
|
|
105
|
+
|
|
106
|
+
await LoginPage.emailInput.type(Fixture.users.admin.email, { clearFirst: true });
|
|
107
|
+
await LoginPage.passwordInput.type(Fixture.users.admin.password);
|
|
108
|
+
await LoginPage.submitButton.click();
|
|
109
|
+
|
|
110
|
+
await DashboardPage.header.shouldBeVisible();
|
|
111
|
+
await DashboardPage.welcomeMsg.shouldHaveText('Welcome back, Admin');
|
|
112
|
+
await DashboardPage.statCards.shouldHaveLength(4);
|
|
113
|
+
await LoginPage.loadingSpinner.shouldNotBeVisible();
|
|
114
|
+
|
|
115
|
+
await Spectra.browser.shouldHaveUrl('https://app.testspectra.dev/dashboard');
|
|
116
|
+
await Spectra.browser.shouldHaveNoConsoleErrors();
|
|
117
|
+
});
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
This is engine-agnostic: the exact same syntax dispatches to Chrome CDP on `web.test.ts` and to the
|
|
121
|
+
native Android TCP driver on `android.test.ts` — never branch authoring code per platform inside a
|
|
122
|
+
shared file; instead put platform-specific implementations in the correctly-suffixed file (see the
|
|
123
|
+
`workspace-structure` skill).
|
|
@@ -0,0 +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.
|
|
@@ -0,0 +1,107 @@
|
|
|
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. Never Use Raw `$` / `$$` / `browser` / `driver`
|
|
102
|
+
|
|
103
|
+
TestSpectra encapsulates the entire runtime into a single global `Spectra` object. There is no raw
|
|
104
|
+
WebdriverIO `$`, `$$`, `browser`, or `driver` in test-authoring code — always go through
|
|
105
|
+
`Spectra.get()` / `Spectra.getAll()` for locators and `Spectra.*` / `Spectra.browser.*` for
|
|
106
|
+
everything else (navigation, gestures, storage, network). See the `matchers-and-assertions` skill
|
|
107
|
+
for the full command surface.
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: shared-steps-and-actions
|
|
3
|
+
description: The difference between TestSpectra Shared Steps (Step.*, high-level business workflows, documented in spec.md via @step) and Spectra Actions (support/actions, low-level technical utilities) — directory layout, the mandatory anonymous async default export convention, and ambient type generation. Use whenever extracting reusable logic out of a test script.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Skill: Shared Steps vs. Custom Actions
|
|
7
|
+
|
|
8
|
+
Use this skill when you're about to extract repeated logic (e.g. login, checkout) out of test
|
|
9
|
+
scripts, or when deciding whether new reusable logic belongs under `support/steps/` or
|
|
10
|
+
`support/actions/`.
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## 1. Which One Do I Need?
|
|
15
|
+
|
|
16
|
+
| Attribute | Shared Steps (`Step.*`) | Custom Actions (`Spectra.*` / on-element helpers) |
|
|
17
|
+
| :---------------- | :--------------------------------------------- | :---------------------------------------------------------------------------- |
|
|
18
|
+
| Abstraction level | High-level business workflow (login, checkout) | Low-level technical utility |
|
|
19
|
+
| Directory | `support/steps/<stepName>/` | `support/actions/<actionName>/` |
|
|
20
|
+
| Spec mapping | Documented as `@step:<name>` in `spec.md` | Just called inline, not listed in spec steps |
|
|
21
|
+
| Metadata file | `step.md` (YAML frontmatter + description) | None — implementation file only |
|
|
22
|
+
| Files | `step.md` + `web.step.ts` [+ `mobile.step.ts`] | `web.action.ts` [+ `mobile.action.ts`] |
|
|
23
|
+
| Invocation | `await Step.loginAsAdmin(params)` | Called as a helper bound to an element/page, e.g. inside a Page Object method |
|
|
24
|
+
|
|
25
|
+
Rule of thumb: if it's a multi-step _business_ flow a human would describe in one sentence
|
|
26
|
+
("log in as admin"), it's a **Step**. If it's a small technical helper reused across Page Objects
|
|
27
|
+
(a resilient click retry, filling a pair of fields), it's an **Action**.
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
## 2. Directory Layout
|
|
32
|
+
|
|
33
|
+
```
|
|
34
|
+
support/
|
|
35
|
+
├── steps/
|
|
36
|
+
│ └── loginAsAdmin/
|
|
37
|
+
│ ├── step.md # Metadata, docs, parameter types
|
|
38
|
+
│ ├── web.step.ts
|
|
39
|
+
│ └── mobile.step.ts # optional
|
|
40
|
+
└── actions/
|
|
41
|
+
└── fillCredentials/
|
|
42
|
+
├── web.action.ts
|
|
43
|
+
└── mobile.action.ts
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Both use **camelCase** directory/function names (`loginAsAdmin`, `fillCredentials`,
|
|
47
|
+
`applyDiscount`, `verifyOtp`) — never kebab-case or snake_case.
|
|
48
|
+
|
|
49
|
+
---
|
|
50
|
+
|
|
51
|
+
## 3. `step.md` Metadata
|
|
52
|
+
|
|
53
|
+
```markdown
|
|
54
|
+
---
|
|
55
|
+
id: step-login-as-admin
|
|
56
|
+
name: loginAsAdmin
|
|
57
|
+
description: Authenticates as an administrator and asserts landing dashboard
|
|
58
|
+
platform:
|
|
59
|
+
- web
|
|
60
|
+
- mobile
|
|
61
|
+
tags:
|
|
62
|
+
- auth
|
|
63
|
+
- admin
|
|
64
|
+
parameters:
|
|
65
|
+
- name: email
|
|
66
|
+
type: string
|
|
67
|
+
required: false
|
|
68
|
+
description: Admin email address (defaults to Fixture.users.admin.email)
|
|
69
|
+
- name: password
|
|
70
|
+
type: string
|
|
71
|
+
required: false
|
|
72
|
+
description: Admin password (defaults to Fixture.users.admin.password)
|
|
73
|
+
---
|
|
74
|
+
|
|
75
|
+
# Login as Admin Step
|
|
76
|
+
|
|
77
|
+
## Purpose
|
|
78
|
+
|
|
79
|
+
Provides a standard, hardened administrative login sequence for web and mobile platforms.
|
|
80
|
+
|
|
81
|
+
## Prerequisites
|
|
82
|
+
|
|
83
|
+
- User fixture file exists at `fixtures/users.json`.
|
|
84
|
+
- Browser is navigated to the base URL or login route.
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
---
|
|
88
|
+
|
|
89
|
+
## 4. Implementation: Anonymous Async Default Export (Mandatory)
|
|
90
|
+
|
|
91
|
+
Every `*.step.ts` and `*.action.ts` file exports **one anonymous async function as its default
|
|
92
|
+
export** — no named exports, no `test()`/`it()` blocks. This is enforced by the linter
|
|
93
|
+
(`testspectra/require-anonymous-default-export`).
|
|
94
|
+
|
|
95
|
+
```typescript
|
|
96
|
+
// support/steps/loginAsAdmin/web.step.ts
|
|
97
|
+
export default async function (params?: { email?: string; password?: string }) {
|
|
98
|
+
const email = params?.email ?? Fixture.users.admin.email;
|
|
99
|
+
const password = params?.password ?? Fixture.users.admin.password;
|
|
100
|
+
|
|
101
|
+
await Spectra.navigate('/auth/login');
|
|
102
|
+
await LoginPage.emailInput.type(email, { clearFirst: true });
|
|
103
|
+
await LoginPage.passwordInput.type(password);
|
|
104
|
+
await LoginPage.submitButton.click();
|
|
105
|
+
|
|
106
|
+
await DashboardPage.header.shouldBeVisible();
|
|
107
|
+
}
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
Custom Actions follow the same shape and are commonly written with a `this` receiver so they can
|
|
111
|
+
be attached/bound as element/page helpers:
|
|
112
|
+
|
|
113
|
+
```typescript
|
|
114
|
+
// support/actions/fillCredentials/web.action.ts
|
|
115
|
+
export default async function (this: any, email: string, token: string) {
|
|
116
|
+
await this.get('#demo-email-input-pg').type(email);
|
|
117
|
+
await this.get('#demo-token-input-pg').type(token);
|
|
118
|
+
}
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
```typescript
|
|
122
|
+
// support/actions/fillCredentials/mobile.action.ts
|
|
123
|
+
export default async function (this: any, email: string, token: string) {
|
|
124
|
+
await this.get('~pg-email-input').type(email);
|
|
125
|
+
await this.get('~pg-token-input').type(token);
|
|
126
|
+
}
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
Zero-import applies here too: never manually `import` a Page Object, `Fixture`, or `Spectra` — see
|
|
130
|
+
the `workspace-structure` skill.
|
|
131
|
+
|
|
132
|
+
---
|
|
133
|
+
|
|
134
|
+
## 5. Type Generation & Invocation
|
|
135
|
+
|
|
136
|
+
The `TypeGenerator` scans `support/steps/` and emits the `Step` ambient namespace automatically:
|
|
137
|
+
|
|
138
|
+
```typescript
|
|
139
|
+
declare global {
|
|
140
|
+
namespace Step {
|
|
141
|
+
function loginAsAdmin(params?: { email?: string; password?: string }): Promise<void>;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
### In `spec.md` (documents the step as part of the human-readable flow):
|
|
147
|
+
|
|
148
|
+
```markdown
|
|
149
|
+
## Test Steps
|
|
150
|
+
|
|
151
|
+
1. @step:loginAsAdmin
|
|
152
|
+
2. Navigate to user settings page
|
|
153
|
+
3. Update profile avatar
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
### In `web.test.ts` (invokes it with zero manual imports):
|
|
157
|
+
|
|
158
|
+
```typescript
|
|
159
|
+
it('should update admin profile avatar', async () => {
|
|
160
|
+
await Step.loginAsAdmin();
|
|
161
|
+
|
|
162
|
+
await SettingsPage.openProfile();
|
|
163
|
+
await SettingsPage.avatarInput.type('avatar.png');
|
|
164
|
+
await SettingsPage.successToast.shouldBeVisible();
|
|
165
|
+
});
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
Every `@step:<name>` in `spec.md` must have a matching `Step.<name>()` call in the paired test
|
|
169
|
+
script, or `spectra lint` reports `testspectra/missing-step-call`. See the
|
|
170
|
+
`spec-and-suite-authoring` skill for the full linter rule matrix.
|
|
171
|
+
|
|
172
|
+
---
|
|
173
|
+
|
|
174
|
+
## 6. Renaming a Step or Action
|
|
175
|
+
|
|
176
|
+
Never rename a step/action folder by hand. Use:
|
|
177
|
+
|
|
178
|
+
```
|
|
179
|
+
spectra refactor step loginAsAdmin authenticateAdmin
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
This atomically: renames `support/steps/loginAsAdmin/` → `support/steps/authenticateAdmin/`,
|
|
183
|
+
updates `name:` in `step.md`, rewrites every `@step:loginAsAdmin` in every `spec.md`, rewrites
|
|
184
|
+
every `Step.loginAsAdmin()` call site, and regenerates `.testspectra/types/steps.d.ts`.
|