@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.
Files changed (36) hide show
  1. package/LICENSE.md +48 -0
  2. package/README.md +78 -0
  3. package/bin/spectra-skills.js +7 -0
  4. package/dist/commands/add.d.ts +1 -0
  5. package/dist/commands/add.js +44 -0
  6. package/dist/commands/init.d.ts +1 -0
  7. package/dist/commands/init.js +54 -0
  8. package/dist/commands/list.d.ts +1 -0
  9. package/dist/commands/list.js +22 -0
  10. package/dist/commands/remove.d.ts +1 -0
  11. package/dist/commands/remove.js +26 -0
  12. package/dist/commands/update.d.ts +1 -0
  13. package/dist/commands/update.js +30 -0
  14. package/dist/detector.d.ts +35 -0
  15. package/dist/detector.js +129 -0
  16. package/dist/index.d.ts +8 -0
  17. package/dist/index.js +119 -0
  18. package/dist/registry.d.ts +8 -0
  19. package/dist/registry.js +48 -0
  20. package/dist/skills/fixtures-data/SKILL.md +109 -0
  21. package/dist/skills/lifecycle-hooks/SKILL.md +114 -0
  22. package/dist/skills/matchers-and-assertions/SKILL.md +123 -0
  23. package/dist/skills/network-interception/SKILL.md +115 -0
  24. package/dist/skills/page-objects-and-selectors/SKILL.md +107 -0
  25. package/dist/skills/shared-steps-and-actions/SKILL.md +184 -0
  26. package/dist/skills/spec-and-suite-authoring/SKILL.md +132 -0
  27. package/dist/skills/workspace-structure/SKILL.md +181 -0
  28. package/package.json +59 -0
  29. package/skills/fixtures-data/SKILL.md +109 -0
  30. package/skills/lifecycle-hooks/SKILL.md +114 -0
  31. package/skills/matchers-and-assertions/SKILL.md +123 -0
  32. package/skills/network-interception/SKILL.md +115 -0
  33. package/skills/page-objects-and-selectors/SKILL.md +107 -0
  34. package/skills/shared-steps-and-actions/SKILL.md +184 -0
  35. package/skills/spec-and-suite-authoring/SKILL.md +132 -0
  36. package/skills/workspace-structure/SKILL.md +181 -0
@@ -0,0 +1,48 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
5
+ function parseSkillContent(id, content) {
6
+ let name = id;
7
+ let description = 'TestSpectra script-authoring skill';
8
+ const frontmatterMatch = content.match(/^---\s*\n([\s\S]*?)\n---\s*\n/);
9
+ if (frontmatterMatch) {
10
+ const fm = frontmatterMatch[1];
11
+ const nameMatch = fm.match(/^name:\s*(.+)$/m);
12
+ const descMatch = fm.match(/^description:\s*(.+)$/m);
13
+ if (nameMatch)
14
+ name = nameMatch[1].trim();
15
+ if (descMatch)
16
+ description = descMatch[1].trim();
17
+ }
18
+ return { id, name, description, content };
19
+ }
20
+ function findSkillsDir() {
21
+ // dist/registry.js -> ../skills ; src/registry.ts (ts-node/dev) -> ../skills
22
+ const candidates = [
23
+ path.resolve(__dirname, '../skills'),
24
+ path.resolve(__dirname, '../../skills'),
25
+ ];
26
+ return candidates.find((dir) => fs.existsSync(dir));
27
+ }
28
+ export function loadAllSkills() {
29
+ const skillsDir = findSkillsDir();
30
+ if (!skillsDir)
31
+ return [];
32
+ const entries = fs.readdirSync(skillsDir, { withFileTypes: true });
33
+ const skills = [];
34
+ for (const entry of entries) {
35
+ if (!entry.isDirectory())
36
+ continue;
37
+ const skillMdPath = path.join(skillsDir, entry.name, 'SKILL.md');
38
+ if (fs.existsSync(skillMdPath)) {
39
+ const content = fs.readFileSync(skillMdPath, 'utf-8');
40
+ skills.push(parseSkillContent(entry.name, content));
41
+ }
42
+ }
43
+ return skills.sort((a, b) => a.id.localeCompare(b.id));
44
+ }
45
+ export function getSkill(id) {
46
+ const all = loadAllSkills();
47
+ return all.find((s) => s.id === id || s.name === id);
48
+ }
@@ -0,0 +1,109 @@
1
+ ---
2
+ name: fixtures-data
3
+ description: TestSpectra test data fixtures — JSON files under fixtures/, auto-typed globally under Fixture.* with zero imports, and how to reference/refactor them from test scripts, steps, and hooks. Use whenever a test needs static mock data (credentials, catalog items, payloads, locales).
4
+ ---
5
+
6
+ # Skill: Fixture & Test Data Management
7
+
8
+ Use this skill whenever a test case, shared step, or hook needs static data — user credentials,
9
+ product catalogs, mock API payloads, localization strings.
10
+
11
+ ---
12
+
13
+ ## 1. Directory & Storage
14
+
15
+ ```
16
+ workspace-root/
17
+ └── fixtures/
18
+ ├── users.json # User roles & credential sets
19
+ ├── products.json # Catalog items & SKU metadata
20
+ ├── api-payloads.json # Mock response payloads
21
+ └── locales.json # Localization dictionaries
22
+ ```
23
+
24
+ ```json
25
+ // fixtures/users.json
26
+ {
27
+ "admin": {
28
+ "id": "usr-admin-01",
29
+ "name": "System Administrator",
30
+ "email": "admin@testspectra.dev",
31
+ "password": "Password123!",
32
+ "role": "admin"
33
+ },
34
+ "standardUser": {
35
+ "id": "usr-std-02",
36
+ "name": "Jane Doe",
37
+ "email": "jane.doe@testspectra.dev",
38
+ "password": "Password123!",
39
+ "role": "member"
40
+ }
41
+ }
42
+ ```
43
+
44
+ ---
45
+
46
+ ## 2. Ambient Typing (Zero Import)
47
+
48
+ The `TypeGenerator` watches `fixtures/` and emits `.testspectra/types/fixtures.d.ts` automatically
49
+ whenever a fixture file is created/updated:
50
+
51
+ ```typescript
52
+ declare global {
53
+ namespace Fixture {
54
+ const users: typeof import('../../fixtures/users.json');
55
+ const products: typeof import('../../fixtures/products.json');
56
+ }
57
+ }
58
+ ```
59
+
60
+ Never hand-write this file, and never `import` a fixture JSON file directly — always access it via
61
+ the global `Fixture.<fileBaseName>` namespace, exactly like Page Objects and `Step.*`.
62
+
63
+ ---
64
+
65
+ ## 3. Usage
66
+
67
+ ### In test scripts
68
+
69
+ ```typescript
70
+ it('should login with admin credentials from fixture', async () => {
71
+ await Spectra.navigate('/auth/login');
72
+ await LoginPage.emailInput.type(Fixture.users.admin.email, { clearFirst: true });
73
+ await LoginPage.passwordInput.type(Fixture.users.admin.password);
74
+ await LoginPage.submitButton.click();
75
+
76
+ await DashboardPage.welcomeMsg.shouldContainText(Fixture.users.admin.name);
77
+ });
78
+ ```
79
+
80
+ ### In shared steps
81
+
82
+ ```typescript
83
+ // support/steps/loginUser/web.step.ts
84
+ export default async function (userType: 'admin' | 'standardUser' = 'standardUser') {
85
+ const user = Fixture.users[userType];
86
+ await Spectra.navigate('/auth/login');
87
+ await LoginPage.emailInput.type(user.email, { clearFirst: true });
88
+ await LoginPage.passwordInput.type(user.password);
89
+ await LoginPage.submitButton.click();
90
+ }
91
+ ```
92
+
93
+ Fixtures are freely usable inside hooks too (e.g. `global-hooks/before` seeding a DB via `fetch`
94
+ with `Fixture.apiPayloads.seedUser`).
95
+
96
+ ---
97
+
98
+ ## 4. Renaming a Fixture
99
+
100
+ Never rename a fixture file by hand. Use:
101
+
102
+ ```
103
+ spectra refactor fixture users accounts
104
+ ```
105
+
106
+ This renames `fixtures/users.json` → `fixtures/accounts.json`, rewrites every `Fixture.users`
107
+ reference across `specs/**/*.test.ts`, `support/steps/**/*.step.ts`, and `hooks/**` to
108
+ `Fixture.accounts`, and regenerates `.testspectra/types/fixtures.d.ts` immediately with 0
109
+ TypeScript errors.
@@ -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.