pw-element-interactions 0.0.9 → 0.1.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/README.md +121 -117
- package/dist/fixture/BaseFixture.js +10 -0
- package/dist/interactions/Interaction.js +5 -3
- package/dist/interactions/Navigation.d.ts +8 -4
- package/dist/interactions/Navigation.js +17 -5
- package/dist/interactions/Verification.js +6 -6
- package/dist/logger/Logger.d.ts +25 -0
- package/dist/logger/Logger.js +66 -0
- package/dist/steps/CommonSteps.d.ts +0 -145
- package/dist/steps/CommonSteps.js +38 -172
- package/dist/utils/ElementUtilities.js +3 -3
- package/package.json +8 -3
- package/scripts/postinstall.js +1 -1
- package/skills/pw-element-interactions.md +235 -0
- package/skills/SKILL.md +0 -322
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: pw-element-interactions
|
|
3
|
+
description: >
|
|
4
|
+
Use this skill whenever writing, editing, or generating Playwright tests! Triggers on any mention of
|
|
5
|
+
Playwright tests, pw-element-interactions or pw-element-repository packages, the Steps API, ElementRepository, ElementInteractions, baseFixture,
|
|
6
|
+
ContextStore, page-repository.json, or any request to write, fix, or add to a Playwright test in this project.
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# pw-element-interactions — Agent Skill
|
|
10
|
+
|
|
11
|
+
A two-package Playwright framework that fully decouples **element acquisition** (`pw-element-repository`) from **element interaction** (`pw-element-interactions`). Tests reference elements by plain strings (`'HomePage'`, `'submitButton'`); raw selectors never appear in test code.
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## 🚨 ABSOLUTE RULES — READ BEFORE DOING ANYTHING ELSE
|
|
16
|
+
|
|
17
|
+
These rules are non-negotiable and override any perceived helpfulness or initiative:
|
|
18
|
+
|
|
19
|
+
### 1. NEVER write tests unless explicitly asked
|
|
20
|
+
- NEVER create, write, or scaffold a test file unless the user has directly asked for it in this conversation.
|
|
21
|
+
- NEVER infer that tests are needed from context, file structure, or prior messages.
|
|
22
|
+
- If unsure whether the user wants a test written, **ask first. Do not write first.**
|
|
23
|
+
- When asked to write tests, ALWAYS start by producing a brief plan — test file(s), scenarios, and locators needed — and wait for the user to approve it before writing anything.
|
|
24
|
+
- If the plan covers more than one test file, suggest splitting into separate sessions (one per file) before proceeding.
|
|
25
|
+
|
|
26
|
+
### 2. NEVER edit `page-repository.json` without explicit permission
|
|
27
|
+
- NEVER add, modify, or delete entries in `page-repository.json` (or any locator JSON file) without the user explicitly approving the change.
|
|
28
|
+
- If new locators are needed, **show the user exactly what you intend to add** and wait for a clear "yes" before touching the file.
|
|
29
|
+
|
|
30
|
+
### 3. NEVER invent selectors — use Playwright MCP to inspect the live site
|
|
31
|
+
- NEVER guess or invent CSS selectors, XPath, IDs, or text values.
|
|
32
|
+
- ALWAYS use the Playwright MCP to navigate to the page and inspect the real DOM before adding any locator.
|
|
33
|
+
- If the Playwright MCP is not connected, stop and tell the user: *"I need the Playwright MCP to inspect the site. Please add it to your Claude Code MCP settings and restart."* Do not proceed until it is available.
|
|
34
|
+
|
|
35
|
+
### 4. NEVER invent type definitions or API shapes
|
|
36
|
+
- NEVER create `.d.ts` stubs or type shims for `pw-element-interactions` or `pw-element-repository`.
|
|
37
|
+
- If a type is missing, report the problem to the user and ask how to proceed. Do not work around it silently.
|
|
38
|
+
|
|
39
|
+
### 5. ALWAYS inspect a screenshot when a test fails
|
|
40
|
+
- The base fixture automatically captures a `failure-screenshot` on every failed test — run `npx playwright show-report` and open the report in a browser using Playwright MCP or a browser MCP to view it.
|
|
41
|
+
- Ensure `reporter: 'html'` is set in `playwright.config.ts` — this is required for `failure-screenshot` attachments to appear in the report.
|
|
42
|
+
- If the report is not accessible, use the Playwright MCP to take a screenshot of the current page state manually.
|
|
43
|
+
- NEVER attempt to fix a failing test based solely on the error message or stack trace — always verify visually first.
|
|
44
|
+
- Describe what you see in the screenshot to the user, then propose a fix based on the visual evidence.
|
|
45
|
+
- If the screenshot suggests a selector problem, re-inspect the live DOM via Playwright MCP before touching `page-repository.json`.
|
|
46
|
+
- After a fully passing run, do NOT open the report unless the user asks.
|
|
47
|
+
|
|
48
|
+
### 6. Before creating or modifying `playwright.config.ts`, read the existing file first — do not overwrite it.
|
|
49
|
+
|
|
50
|
+
---
|
|
51
|
+
|
|
52
|
+
## 1. Adding Locators
|
|
53
|
+
|
|
54
|
+
All selectors live in `tests/data/page-repository.json`.
|
|
55
|
+
|
|
56
|
+
```json
|
|
57
|
+
{
|
|
58
|
+
"pages": [
|
|
59
|
+
{
|
|
60
|
+
"name": "HomePage",
|
|
61
|
+
"elements": [
|
|
62
|
+
{
|
|
63
|
+
"elementName": "submitButton",
|
|
64
|
+
"selector": {
|
|
65
|
+
"css": "button[data-test='submit']",
|
|
66
|
+
"xpath": "//button[@data-test='submit']",
|
|
67
|
+
"id": "submit-btn",
|
|
68
|
+
"text": "Submit"
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
]
|
|
72
|
+
}
|
|
73
|
+
]
|
|
74
|
+
}
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
**Naming conventions:**
|
|
78
|
+
- `name` — PascalCase page identifier, e.g. `CheckoutPage`, `ProductDetailsPage`
|
|
79
|
+
- `elementName` — camelCase element identifier, e.g. `submitButton`, `galleryImages`
|
|
80
|
+
|
|
81
|
+
---
|
|
82
|
+
|
|
83
|
+
## 2. Setup — Fixtures
|
|
84
|
+
|
|
85
|
+
Before writing `tests/fixtures/base.ts`, **read it first if it already exists** — do not overwrite it without checking. The `baseFixture` automatically includes screenshot-on-failure capture, so no extension is needed:
|
|
86
|
+
|
|
87
|
+
```ts
|
|
88
|
+
// tests/fixtures/base.ts
|
|
89
|
+
import { test as base } from '@playwright/test';
|
|
90
|
+
import { baseFixture } from 'pw-element-interactions';
|
|
91
|
+
|
|
92
|
+
export const test = baseFixture(base, 'tests/data/page-repository.json');
|
|
93
|
+
export { expect } from '@playwright/test';
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
```ts
|
|
97
|
+
// tests/example.spec.ts
|
|
98
|
+
import { test } from '../fixtures/base';
|
|
99
|
+
|
|
100
|
+
test('example', async ({ steps }) => {
|
|
101
|
+
await steps.navigateTo('https://example.com/');
|
|
102
|
+
await steps.click('HomePage', 'submitButton');
|
|
103
|
+
});
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
---
|
|
107
|
+
|
|
108
|
+
## 3. Steps API
|
|
109
|
+
|
|
110
|
+
Every method takes `pageName` and `elementName` as its first two arguments, matching keys in your JSON file.
|
|
111
|
+
|
|
112
|
+
### 🧭 Navigation
|
|
113
|
+
|
|
114
|
+
Relative URLs are resolved against `baseURL` from `playwright.config.ts`. If a relative URL is passed and `baseURL` is not configured, an error will be thrown.
|
|
115
|
+
|
|
116
|
+
```ts
|
|
117
|
+
await steps.navigateTo('https://example.com/path');
|
|
118
|
+
await steps.refresh();
|
|
119
|
+
await steps.backOrForward('BACKWARDS'); // or 'FORWARDS'
|
|
120
|
+
await steps.setViewport(1280, 720);
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
### 🖱️ Interaction
|
|
124
|
+
|
|
125
|
+
```ts
|
|
126
|
+
await steps.click('PageName', 'elementName');
|
|
127
|
+
await steps.clickWithoutScrolling('PageName', 'elementName');
|
|
128
|
+
await steps.clickIfPresent('PageName', 'elementName');
|
|
129
|
+
await steps.clickRandom('PageName', 'elementName');
|
|
130
|
+
await steps.hover('PageName', 'elementName');
|
|
131
|
+
await steps.scrollIntoView('PageName', 'elementName');
|
|
132
|
+
await steps.fill('PageName', 'elementName', 'my input');
|
|
133
|
+
await steps.typeSequentially('PageName', 'elementName', 'my input');
|
|
134
|
+
await steps.typeSequentially('PageName', 'elementName', 'my input', 50); // custom delay ms
|
|
135
|
+
await steps.uploadFile('PageName', 'elementName', 'tests/fixtures/file.pdf');
|
|
136
|
+
await steps.dragAndDrop('PageName', 'elementName', { target: otherLocator });
|
|
137
|
+
await steps.dragAndDrop('PageName', 'elementName', { xOffset: 100, yOffset: 0 });
|
|
138
|
+
await steps.dragAndDropListedElement('PageName', 'elementName', 'Item Label', { target: otherLocator });
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
For dropdown selection, import `DropdownSelectType` at the top of your test file:
|
|
142
|
+
|
|
143
|
+
```ts
|
|
144
|
+
import { DropdownSelectType } from 'pw-element-interactions';
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
Then use it in your test:
|
|
148
|
+
|
|
149
|
+
```ts
|
|
150
|
+
const value1 = await steps.selectDropdown('PageName', 'elementName'); // random (default)
|
|
151
|
+
const value2 = await steps.selectDropdown('PageName', 'elementName', { type: DropdownSelectType.RANDOM }); // explicit random
|
|
152
|
+
const value3 = await steps.selectDropdown('PageName', 'elementName', { type: DropdownSelectType.VALUE, value: 'xl' }); // by value
|
|
153
|
+
const value4 = await steps.selectDropdown('PageName', 'elementName', { type: DropdownSelectType.INDEX, index: 2 }); // by index
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
### 📊 Data Extraction
|
|
157
|
+
|
|
158
|
+
```ts
|
|
159
|
+
const text = await steps.getText('PageName', 'elementName');
|
|
160
|
+
const href = await steps.getAttribute('PageName', 'elementName', 'href');
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
### ✅ Verification
|
|
164
|
+
|
|
165
|
+
```ts
|
|
166
|
+
await steps.verifyPresence('PageName', 'elementName');
|
|
167
|
+
await steps.verifyAbsence('PageName', 'elementName');
|
|
168
|
+
await steps.verifyText('PageName', 'elementName', 'Expected text');
|
|
169
|
+
await steps.verifyText('PageName', 'elementName', undefined, { notEmpty: true });
|
|
170
|
+
await steps.verifyCount('PageName', 'elementName', { exact: 3 });
|
|
171
|
+
await steps.verifyCount('PageName', 'elementName', { greaterThan: 0 });
|
|
172
|
+
await steps.verifyCount('PageName', 'elementName', { lessThan: 10 });
|
|
173
|
+
await steps.verifyImages('PageName', 'elementName');
|
|
174
|
+
await steps.verifyImages('PageName', 'elementName', false); // skip scroll-into-view
|
|
175
|
+
await steps.verifyUrlContains('/dashboard');
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
### ⏳ Waiting
|
|
179
|
+
|
|
180
|
+
```ts
|
|
181
|
+
await steps.waitForState('PageName', 'elementName'); // default: 'visible'
|
|
182
|
+
await steps.waitForState('PageName', 'elementName', 'hidden');
|
|
183
|
+
await steps.waitForState('PageName', 'elementName', 'attached');
|
|
184
|
+
await steps.waitForState('PageName', 'elementName', 'detached');
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
---
|
|
188
|
+
|
|
189
|
+
## 4. Accessing the Repository Directly
|
|
190
|
+
|
|
191
|
+
Use `repo` when you need to filter by visible text, iterate all matches, or pick a random item:
|
|
192
|
+
|
|
193
|
+
```ts
|
|
194
|
+
test('navigate to Forms', async ({ page, repo, steps }) => {
|
|
195
|
+
await steps.navigateTo('https://example.com/');
|
|
196
|
+
const formsLink = await repo.getByText(page, 'HomePage', 'categories', 'Forms');
|
|
197
|
+
await formsLink?.click();
|
|
198
|
+
await steps.verifyAbsence('HomePage', 'categories');
|
|
199
|
+
});
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
### Repository API
|
|
203
|
+
|
|
204
|
+
```ts
|
|
205
|
+
await repo.get(page, 'PageName', 'elementName');
|
|
206
|
+
await repo.getAll(page, 'PageName', 'elementName');
|
|
207
|
+
await repo.getRandom(page, 'PageName', 'elementName');
|
|
208
|
+
await repo.getByText(page, 'PageName', 'elementName', 'Desired Text');
|
|
209
|
+
repo.getSelector('PageName', 'elementName'); // sync, returns raw selector string
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
---
|
|
213
|
+
|
|
214
|
+
### ⚠️ NEVER use index-based element access — always target by context
|
|
215
|
+
|
|
216
|
+
NEVER access elements by hardcoded index (e.g. `elements[1]`, `elements[3]`). Order can change and will silently break tests. Always identify elements by visible text, labels, attributes, or sibling content:
|
|
217
|
+
|
|
218
|
+
```ts
|
|
219
|
+
// ❌ Fragile — breaks if order changes
|
|
220
|
+
const nameCell = tableRows[1]?.locator('td:first-child');
|
|
221
|
+
|
|
222
|
+
// ✅ Robust — finds the element by its meaningful label
|
|
223
|
+
const nameRow = await repo.getByText(page, 'FormsPage', 'submissionEntries', 'Name');
|
|
224
|
+
const nameValue = await nameRow?.locator('td:nth-child(2)').textContent();
|
|
225
|
+
expect(nameValue?.trim()).toBe(testData.name);
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
Before writing any verification logic against a list or table, inspect the live page via Playwright MCP to understand the structure and identify the most stable way to target each element. If no meaningful context exists to distinguish elements, stop and ask the user how to proceed.
|
|
229
|
+
|
|
230
|
+
---
|
|
231
|
+
|
|
232
|
+
## 5. Workflow
|
|
233
|
+
|
|
234
|
+
- After any fix, feature, or test is confirmed working, run a `git commit` with a clear message before moving on.
|
|
235
|
+
- Do not batch multiple successes into a single commit.
|
package/skills/SKILL.md
DELETED
|
@@ -1,322 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: skills
|
|
3
|
-
description: >
|
|
4
|
-
Use this skill whenever writing or generating Playwright tests in a project that uses
|
|
5
|
-
pw-element-interactions and pw-element-repository. Triggers on any request to write a
|
|
6
|
-
test, add a locator, create a page object, use the Steps API, or interact with elements
|
|
7
|
-
using this stack. Also use when asked to add entries to a page-repository JSON file,
|
|
8
|
-
use fixtures, select dropdowns, verify elements, wait for states, or perform any
|
|
9
|
-
browser interaction through this framework. Always consult this skill before generating
|
|
10
|
-
test code or locator JSON — do not guess API shapes or invent method signatures.
|
|
11
|
-
---
|
|
12
|
-
|
|
13
|
-
# pw-element-interactions — Test Authoring Reference
|
|
14
|
-
|
|
15
|
-
This framework separates **where elements are defined** from **how they are used**. Selectors live in a JSON file; tests reference elements by readable string keys. No raw CSS or XPath ever appears in test code.
|
|
16
|
-
|
|
17
|
-
---
|
|
18
|
-
|
|
19
|
-
## 0. Understanding the Website Structure
|
|
20
|
-
|
|
21
|
-
Before writing tests or adding locators for an unfamiliar page or component, use the **Playwright MCP** to inspect the live site. This is the only reliable way to discover real selectors, element hierarchy, and page behaviour — do not guess or invent selectors from memory.
|
|
22
|
-
|
|
23
|
-
Typical uses:
|
|
24
|
-
- Navigating to a page and reading its DOM to find the right CSS selector or text for a new locator entry
|
|
25
|
-
- Verifying that an element is actually present and visible before writing an assertion
|
|
26
|
-
- Understanding the flow between pages before writing a multi-step test
|
|
27
|
-
|
|
28
|
-
If the Playwright MCP is not connected, ask the user to install it before proceeding:
|
|
29
|
-
|
|
30
|
-
```
|
|
31
|
-
I need the Playwright MCP to inspect the site and find accurate selectors.
|
|
32
|
-
Please install it by adding the following to your Claude Code MCP settings:
|
|
33
|
-
|
|
34
|
-
{
|
|
35
|
-
"mcpServers": {
|
|
36
|
-
"playwright": {
|
|
37
|
-
"command": "npx",
|
|
38
|
-
"args": ["@playwright/mcp@latest"]
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
Then restart Claude Code and try again.
|
|
44
|
-
```
|
|
45
|
-
|
|
46
|
-
Do not attempt to write locators or test steps for unknown pages without first using the Playwright MCP to confirm the structure.
|
|
47
|
-
|
|
48
|
-
---
|
|
49
|
-
|
|
50
|
-
## 1. Adding Locators
|
|
51
|
-
|
|
52
|
-
All selectors are stored in a single JSON file (commonly `tests/data/page-repository.json`). Each page groups its elements by name. Provide as many selector strategies as you like — the repository uses the first one that resolves:
|
|
53
|
-
|
|
54
|
-
```json
|
|
55
|
-
{
|
|
56
|
-
"pages": [
|
|
57
|
-
{
|
|
58
|
-
"name": "HomePage",
|
|
59
|
-
"elements": [
|
|
60
|
-
{
|
|
61
|
-
"elementName": "submitButton",
|
|
62
|
-
"selector": {
|
|
63
|
-
"css": "button[data-test='submit']",
|
|
64
|
-
"xpath": "//button[@data-test='submit']",
|
|
65
|
-
"id": "submit-btn",
|
|
66
|
-
"text": "Submit"
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
]
|
|
70
|
-
}
|
|
71
|
-
]
|
|
72
|
-
}
|
|
73
|
-
```
|
|
74
|
-
|
|
75
|
-
**Naming conventions:**
|
|
76
|
-
- `name` — PascalCase page identifier, e.g. `CheckoutPage`, `ProductDetailsPage`
|
|
77
|
-
- `elementName` — camelCase element identifier, e.g. `submitButton`, `gallery-images`
|
|
78
|
-
|
|
79
|
-
---
|
|
80
|
-
|
|
81
|
-
## 2. Setup
|
|
82
|
-
|
|
83
|
-
### Option A — Fixtures (recommended)
|
|
84
|
-
|
|
85
|
-
Define once in a fixture file; every test gets `steps`, `repo`, `interactions`, and `contextStore` for free:
|
|
86
|
-
|
|
87
|
-
```ts
|
|
88
|
-
// tests/fixtures/base.ts
|
|
89
|
-
import { test as base } from '@playwright/test';
|
|
90
|
-
import { baseFixture } from 'pw-element-interactions';
|
|
91
|
-
|
|
92
|
-
export const test = baseFixture(base, 'tests/data/page-repository.json');
|
|
93
|
-
export { expect } from '@playwright/test';
|
|
94
|
-
```
|
|
95
|
-
|
|
96
|
-
```ts
|
|
97
|
-
// tests/checkout.spec.ts
|
|
98
|
-
import { test } from '../fixtures/base';
|
|
99
|
-
|
|
100
|
-
test('complete checkout', async ({ steps }) => {
|
|
101
|
-
await steps.navigateTo('/checkout');
|
|
102
|
-
await steps.click('CheckoutPage', 'submitButton');
|
|
103
|
-
});
|
|
104
|
-
```
|
|
105
|
-
|
|
106
|
-
### Option B — Manual initialisation
|
|
107
|
-
|
|
108
|
-
```ts
|
|
109
|
-
import { ElementRepository } from 'pw-element-repository';
|
|
110
|
-
import { Steps } from 'pw-element-interactions';
|
|
111
|
-
|
|
112
|
-
const repo = new ElementRepository('tests/data/page-repository.json', 15000);
|
|
113
|
-
const steps = new Steps(page, repo);
|
|
114
|
-
```
|
|
115
|
-
|
|
116
|
-
---
|
|
117
|
-
|
|
118
|
-
## 3. Steps API
|
|
119
|
-
|
|
120
|
-
Every method takes `pageName` and `elementName` as its first two arguments, matching keys in your JSON file.
|
|
121
|
-
|
|
122
|
-
### 🧭 Navigation
|
|
123
|
-
|
|
124
|
-
```ts
|
|
125
|
-
await steps.navigateTo('/path');
|
|
126
|
-
await steps.refresh();
|
|
127
|
-
await steps.backOrForward('BACKWARDS'); // or 'FORWARDS'
|
|
128
|
-
await steps.setViewport(1280, 720);
|
|
129
|
-
```
|
|
130
|
-
|
|
131
|
-
### 🖱️ Interaction
|
|
132
|
-
|
|
133
|
-
```ts
|
|
134
|
-
// Standard click — waits for visible, stable, actionable
|
|
135
|
-
await steps.click('PageName', 'elementName');
|
|
136
|
-
|
|
137
|
-
// Click without scrolling — use for elements behind sticky headers or overlays
|
|
138
|
-
await steps.clickWithoutScrolling('PageName', 'elementName');
|
|
139
|
-
|
|
140
|
-
// Click only if the element is visible — silently skips if not (e.g. cookie banners)
|
|
141
|
-
await steps.clickIfPresent('PageName', 'elementName');
|
|
142
|
-
|
|
143
|
-
// Click a random element from a matched list (e.g. product cards)
|
|
144
|
-
await steps.clickRandom('PageName', 'elementName');
|
|
145
|
-
|
|
146
|
-
// Hover to trigger a tooltip or dropdown
|
|
147
|
-
await steps.hover('PageName', 'elementName');
|
|
148
|
-
|
|
149
|
-
// Scroll element into view
|
|
150
|
-
await steps.scrollIntoView('PageName', 'elementName');
|
|
151
|
-
|
|
152
|
-
// Clear and type text
|
|
153
|
-
await steps.fill('PageName', 'elementName', 'my input');
|
|
154
|
-
|
|
155
|
-
// Type character by character — use for OTP inputs or fields with keyup listeners
|
|
156
|
-
await steps.typeSequentially('PageName', 'elementName', 'my input');
|
|
157
|
-
await steps.typeSequentially('PageName', 'elementName', 'my input', 50); // custom delay ms
|
|
158
|
-
|
|
159
|
-
// Upload a file
|
|
160
|
-
await steps.uploadFile('PageName', 'elementName', 'tests/fixtures/file.pdf');
|
|
161
|
-
|
|
162
|
-
// Select from a <select> dropdown — returns the selected value
|
|
163
|
-
import { DropdownSelectType } from 'pw-element-interactions';
|
|
164
|
-
|
|
165
|
-
const value = await steps.selectDropdown('PageName', 'elementName'); // random (default)
|
|
166
|
-
const value = await steps.selectDropdown('PageName', 'elementName', { type: DropdownSelectType.RANDOM });
|
|
167
|
-
const value = await steps.selectDropdown('PageName', 'elementName', { type: DropdownSelectType.VALUE, value: 'xl' });
|
|
168
|
-
const value = await steps.selectDropdown('PageName', 'elementName', { type: DropdownSelectType.INDEX, index: 2 });
|
|
169
|
-
|
|
170
|
-
// Drag and drop
|
|
171
|
-
await steps.dragAndDrop('PageName', 'elementName', { target: otherLocator });
|
|
172
|
-
await steps.dragAndDrop('PageName', 'elementName', { xOffset: 100, yOffset: 0 });
|
|
173
|
-
|
|
174
|
-
// Drag a specific listed item by its text
|
|
175
|
-
await steps.dragAndDropListedElement('PageName', 'elementName', 'Item Label', { target: otherLocator });
|
|
176
|
-
```
|
|
177
|
-
|
|
178
|
-
### 📊 Data Extraction
|
|
179
|
-
|
|
180
|
-
```ts
|
|
181
|
-
const text = await steps.getText('PageName', 'elementName'); // trimmed text; '' if null
|
|
182
|
-
const href = await steps.getAttribute('PageName', 'elementName', 'href'); // null if absent
|
|
183
|
-
```
|
|
184
|
-
|
|
185
|
-
### ✅ Verification
|
|
186
|
-
|
|
187
|
-
```ts
|
|
188
|
-
// Presence / absence
|
|
189
|
-
await steps.verifyPresence('PageName', 'elementName');
|
|
190
|
-
await steps.verifyAbsence('PageName', 'elementName');
|
|
191
|
-
|
|
192
|
-
// Text — exact match or non-empty check
|
|
193
|
-
await steps.verifyText('PageName', 'elementName', 'Expected text');
|
|
194
|
-
await steps.verifyText('PageName', 'elementName', undefined, { notEmpty: true });
|
|
195
|
-
|
|
196
|
-
// Count
|
|
197
|
-
await steps.verifyCount('PageName', 'elementName', { exact: 3 });
|
|
198
|
-
await steps.verifyCount('PageName', 'elementName', { greaterThan: 0 });
|
|
199
|
-
await steps.verifyCount('PageName', 'elementName', { lessThan: 10 });
|
|
200
|
-
|
|
201
|
-
// Images — checks visibility, valid src, naturalWidth > 0, and browser decode()
|
|
202
|
-
await steps.verifyImages('PageName', 'elementName');
|
|
203
|
-
await steps.verifyImages('PageName', 'elementName', false); // skip scroll-into-view
|
|
204
|
-
|
|
205
|
-
// URL
|
|
206
|
-
await steps.verifyUrlContains('/dashboard');
|
|
207
|
-
```
|
|
208
|
-
|
|
209
|
-
### ⏳ Waiting
|
|
210
|
-
|
|
211
|
-
```ts
|
|
212
|
-
await steps.waitForState('PageName', 'elementName'); // default: 'visible'
|
|
213
|
-
await steps.waitForState('PageName', 'elementName', 'hidden');
|
|
214
|
-
await steps.waitForState('PageName', 'elementName', 'attached');
|
|
215
|
-
await steps.waitForState('PageName', 'elementName', 'detached');
|
|
216
|
-
```
|
|
217
|
-
|
|
218
|
-
---
|
|
219
|
-
|
|
220
|
-
## 4. Accessing the Repository Directly
|
|
221
|
-
|
|
222
|
-
When you need more than a simple locator lookup — filtering by visible text, iterating all matches, or picking a random item — use `repo` directly alongside `steps`:
|
|
223
|
-
|
|
224
|
-
```ts
|
|
225
|
-
test('navigate to Forms', async ({ page, repo, steps }) => {
|
|
226
|
-
await steps.navigateTo('/');
|
|
227
|
-
|
|
228
|
-
// Resolve a locator by the visible text it contains
|
|
229
|
-
const formsLink = await repo.getByText(page, 'HomePage', 'categories', 'Forms');
|
|
230
|
-
await formsLink?.click();
|
|
231
|
-
|
|
232
|
-
await steps.verifyAbsence('HomePage', 'categories');
|
|
233
|
-
});
|
|
234
|
-
```
|
|
235
|
-
|
|
236
|
-
### Repository API
|
|
237
|
-
|
|
238
|
-
```ts
|
|
239
|
-
// Single locator — waits for DOM attachment
|
|
240
|
-
await repo.get(page, 'PageName', 'elementName');
|
|
241
|
-
|
|
242
|
-
// All matching locators — use for iteration
|
|
243
|
-
await repo.getAll(page, 'PageName', 'elementName');
|
|
244
|
-
|
|
245
|
-
// A random locator from the matched set — waits for visibility
|
|
246
|
-
await repo.getRandom(page, 'PageName', 'elementName');
|
|
247
|
-
|
|
248
|
-
// First locator whose visible text contains the given string
|
|
249
|
-
await repo.getByText(page, 'PageName', 'elementName', 'Desired Text');
|
|
250
|
-
|
|
251
|
-
// Sync — returns the raw selector string, e.g. "css=.btn"
|
|
252
|
-
repo.getSelector('PageName', 'elementName');
|
|
253
|
-
```
|
|
254
|
-
|
|
255
|
-
---
|
|
256
|
-
|
|
257
|
-
## 5. Complete Test Example
|
|
258
|
-
|
|
259
|
-
```ts
|
|
260
|
-
import { test } from '../fixtures/base';
|
|
261
|
-
import { DropdownSelectType } from 'pw-element-interactions';
|
|
262
|
-
|
|
263
|
-
test('add product to cart and verify', async ({ page, repo, steps }) => {
|
|
264
|
-
await steps.navigateTo('/');
|
|
265
|
-
|
|
266
|
-
// Click a category by its visible label
|
|
267
|
-
const accessories = await repo.getByText(page, 'HomePage', 'categories', 'Accessories');
|
|
268
|
-
await accessories?.click();
|
|
269
|
-
|
|
270
|
-
// Pick a random product
|
|
271
|
-
await steps.clickRandom('AccessoriesPage', 'productCards');
|
|
272
|
-
await steps.verifyUrlContains('/product/');
|
|
273
|
-
|
|
274
|
-
// Select a size from the dropdown
|
|
275
|
-
const size = await steps.selectDropdown('ProductPage', 'sizeSelector', {
|
|
276
|
-
type: DropdownSelectType.RANDOM,
|
|
277
|
-
});
|
|
278
|
-
console.log(`Selected size: ${size}`);
|
|
279
|
-
|
|
280
|
-
// Verify the image gallery loaded correctly
|
|
281
|
-
await steps.verifyCount('ProductPage', 'galleryImages', { greaterThan: 0 });
|
|
282
|
-
await steps.verifyImages('ProductPage', 'galleryImages');
|
|
283
|
-
|
|
284
|
-
// Verify product title is not blank
|
|
285
|
-
await steps.verifyText('ProductPage', 'productTitle', undefined, { notEmpty: true });
|
|
286
|
-
|
|
287
|
-
// Add to cart and wait for confirmation
|
|
288
|
-
await steps.click('ProductPage', 'addToCartButton');
|
|
289
|
-
await steps.waitForState('ProductPage', 'confirmationModal', 'visible');
|
|
290
|
-
});
|
|
291
|
-
```
|
|
292
|
-
|
|
293
|
-
---
|
|
294
|
-
|
|
295
|
-
## 6. Extending Fixtures
|
|
296
|
-
|
|
297
|
-
Add your own fixtures on top of the base without losing `steps`, `repo`, or `contextStore`:
|
|
298
|
-
|
|
299
|
-
```ts
|
|
300
|
-
// tests/fixtures/base.ts
|
|
301
|
-
import { test as base } from '@playwright/test';
|
|
302
|
-
import { baseFixture } from 'pw-element-interactions';
|
|
303
|
-
import { AuthService } from '../services/AuthService';
|
|
304
|
-
|
|
305
|
-
type MyFixtures = { authService: AuthService };
|
|
306
|
-
|
|
307
|
-
export const test = baseFixture(base, 'tests/data/page-repository.json')
|
|
308
|
-
.extend<MyFixtures>({
|
|
309
|
-
authService: async ({ page }, use) => {
|
|
310
|
-
await use(new AuthService(page));
|
|
311
|
-
},
|
|
312
|
-
});
|
|
313
|
-
|
|
314
|
-
export { expect } from '@playwright/test';
|
|
315
|
-
```
|
|
316
|
-
|
|
317
|
-
```ts
|
|
318
|
-
test('authenticated flow', async ({ steps, authService }) => {
|
|
319
|
-
await authService.login('user@test.com', 'secret');
|
|
320
|
-
await steps.verifyUrlContains('/dashboard');
|
|
321
|
-
});
|
|
322
|
-
```
|