pomwright 1.1.1 → 1.3.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/CHANGELOG.md +612 -0
- package/README.md +41 -312
- package/dist/index.d.mts +528 -128
- package/dist/index.d.ts +528 -128
- package/dist/index.js +333 -125
- package/dist/index.mjs +330 -122
- package/docs/BaseApi-explanation.md +63 -0
- package/docs/BasePage-explanation.md +96 -0
- package/docs/LocatorSchema-explanation.md +271 -0
- package/docs/LocatorSchemaPath-explanation.md +165 -0
- package/docs/PlaywrightReportLogger-explanation.md +56 -0
- package/docs/get-locator-methods-explanation.md +266 -0
- package/docs/intro-to-using-pomwright.md +899 -0
- package/docs/sessionStorage-methods-explanation.md +38 -0
- package/docs/tips-folder-structure.md +38 -0
- package/index.ts +2 -2
- package/package.json +8 -8
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
# Working with Locator Schemas
|
|
2
|
+
|
|
3
|
+
`BasePage` exposes two layers of helpers for working with locator schemas:
|
|
4
|
+
|
|
5
|
+
1. **Wrapper shortcuts** – `getLocator()` and `getNestedLocator()` resolve locators in a single call. They are great when you just need a locator and do not have to tweak the schema.
|
|
6
|
+
2. **`getLocatorSchema()` chains** – return a deep copy of the schemas that make up the requested path so you can update selectors, add filters, or reuse the chain with different parameters.
|
|
7
|
+
|
|
8
|
+
The sections below show how both approaches fit together.
|
|
9
|
+
|
|
10
|
+
## Example set-up
|
|
11
|
+
|
|
12
|
+
The snippets that follow use the simple page object and fixture below.
|
|
13
|
+
|
|
14
|
+
### Create a Page Object Class
|
|
15
|
+
|
|
16
|
+
```ts
|
|
17
|
+
import { Page, TestInfo } from "@playwright/test";
|
|
18
|
+
import { BasePage, GetByMethod, PlaywrightReportLogger } from "pomwright";
|
|
19
|
+
|
|
20
|
+
type LocatorSchemaPath =
|
|
21
|
+
| "content"
|
|
22
|
+
| "content.heading"
|
|
23
|
+
| "content.region.details"
|
|
24
|
+
| "content.region.details.button.edit";
|
|
25
|
+
|
|
26
|
+
export default class Profile extends BasePage<LocatorSchemaPath> {
|
|
27
|
+
constructor(page: Page, testInfo: TestInfo, pwrl: PlaywrightReportLogger) {
|
|
28
|
+
super(page, testInfo, "https://someDomain.com", "/profile", Profile.name, pwrl);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
protected initLocatorSchemas() {
|
|
32
|
+
this.locators.addSchema("content", {
|
|
33
|
+
locator: ".main-content",
|
|
34
|
+
locatorMethod: GetByMethod.locator
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
this.locators.addSchema("content.heading", {
|
|
38
|
+
role: "heading",
|
|
39
|
+
roleOptions: {
|
|
40
|
+
name: "Your Profile"
|
|
41
|
+
},
|
|
42
|
+
locatorMethod: GetByMethod.role
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
this.locators.addSchema("content.region.details", {
|
|
46
|
+
role: "region",
|
|
47
|
+
roleOptions: {
|
|
48
|
+
name: "Profile Details"
|
|
49
|
+
},
|
|
50
|
+
locatorMethod: GetByMethod.role
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
this.locators.addSchema("content.region.details.button.edit", {
|
|
54
|
+
role: "button",
|
|
55
|
+
roleOptions: {
|
|
56
|
+
name: "Edit"
|
|
57
|
+
},
|
|
58
|
+
locatorMethod: GetByMethod.role
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### Provide the page object through a fixture
|
|
65
|
+
|
|
66
|
+
```ts
|
|
67
|
+
import { test as base } from "pomwright";
|
|
68
|
+
import Profile from "./profile";
|
|
69
|
+
|
|
70
|
+
type Fixtures = {
|
|
71
|
+
profile: Profile;
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
export const test = base.extend<Fixtures>({
|
|
75
|
+
profile: async ({ page, log }, use, testInfo) => {
|
|
76
|
+
const profile = new Profile(page, testInfo, log);
|
|
77
|
+
await use(profile);
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
### Use the fixture in a test
|
|
83
|
+
|
|
84
|
+
```ts
|
|
85
|
+
import { test } from "./fixtures";
|
|
86
|
+
|
|
87
|
+
test("load profile", async ({ profile }) => {
|
|
88
|
+
await profile.page.goto(profile.fullUrl);
|
|
89
|
+
});
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
## BasePage wrapper shortcuts
|
|
93
|
+
|
|
94
|
+
The wrapper methods call `getLocatorSchema(path)` internally and immediately resolve the locator. Use them when the stored schema already matches the element you need.
|
|
95
|
+
|
|
96
|
+
### `getLocator(path)`
|
|
97
|
+
|
|
98
|
+
Returns the locator for the final schema in the chain. This is equivalent to `getLocatorSchema(path).getLocator()`.
|
|
99
|
+
|
|
100
|
+
```ts
|
|
101
|
+
test("click edit button with a single locator", async ({ profile }) => {
|
|
102
|
+
await profile.page.waitForURL(profile.fullUrl);
|
|
103
|
+
|
|
104
|
+
const editButton = await profile.getLocator("content.region.details.button.edit");
|
|
105
|
+
await editButton.click();
|
|
106
|
+
});
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
### `getNestedLocator(path, indices?)`
|
|
110
|
+
|
|
111
|
+
Builds a nested locator by chaining every schema that makes up the path. Optional indices let you pick a specific occurrence of any segment.
|
|
112
|
+
|
|
113
|
+
```ts
|
|
114
|
+
test("click edit button with a nested locator", async ({ profile }) => {
|
|
115
|
+
await profile.page.waitForURL(profile.fullUrl);
|
|
116
|
+
|
|
117
|
+
const editButton = await profile.getNestedLocator("content.region.details.button.edit");
|
|
118
|
+
await editButton.click();
|
|
119
|
+
});
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
When a path represents repeating elements, provide sub-path keys to select the right instance:
|
|
123
|
+
|
|
124
|
+
```ts
|
|
125
|
+
test("specify index for nested locators", async ({ profile }) => {
|
|
126
|
+
await profile.page.waitForURL(profile.fullUrl);
|
|
127
|
+
|
|
128
|
+
const editButton = await profile.getNestedLocator(
|
|
129
|
+
"content.region.details.button.edit",
|
|
130
|
+
{
|
|
131
|
+
"content.region.details": 0,
|
|
132
|
+
"content.region.details.button.edit": 1
|
|
133
|
+
}
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
await editButton.click();
|
|
137
|
+
});
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
> Legacy numeric indices are still supported but will be removed in a future major version. Prefer keyed sub paths as shown above.
|
|
141
|
+
|
|
142
|
+
## Building chains with `getLocatorSchema()`
|
|
143
|
+
|
|
144
|
+
`getLocatorSchema(path)` returns a deep copy of every schema that makes up the `path` plus chainable helpers for refining the locator. All manipulations happen on the copy, so the original definitions inside the page object stay immutable.
|
|
145
|
+
|
|
146
|
+
```ts
|
|
147
|
+
const chain = profile.getLocatorSchema("content.region.details.button.edit");
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
### `update(subPath, partial)`
|
|
151
|
+
|
|
152
|
+
Override one or more properties of any schema in the chain. Calls can be chained and are applied in order. This is how you adapt selectors to dynamic states without mutating the stored definitions.
|
|
153
|
+
|
|
154
|
+
```ts
|
|
155
|
+
const editButton = await profile
|
|
156
|
+
.getLocatorSchema("content.region.details.button.edit")
|
|
157
|
+
.update("content.region.details.button.edit", {
|
|
158
|
+
roleOptions: { name: "Edit details" }
|
|
159
|
+
})
|
|
160
|
+
.getNestedLocator();
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
You can also update intermediate segments without touching the rest of the path:
|
|
164
|
+
|
|
165
|
+
```ts
|
|
166
|
+
await profile
|
|
167
|
+
.getLocatorSchema("content.region.details.button.edit")
|
|
168
|
+
.update("content.region.details", {
|
|
169
|
+
locator: ".profile-details",
|
|
170
|
+
locatorMethod: GetByMethod.locator
|
|
171
|
+
})
|
|
172
|
+
.getNestedLocator();
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
Chain multiple `update` calls when you need to adjust different LocatorSchema in the chain:
|
|
176
|
+
|
|
177
|
+
```ts
|
|
178
|
+
test("make multiple versions of a locator", async ({ profile }) => {
|
|
179
|
+
await profile.page.waitForURL(profile.fullUrl);
|
|
180
|
+
|
|
181
|
+
const edgeCaseNestedLocator = profile
|
|
182
|
+
.getLocatorSchema("content.region.details.button.edit")
|
|
183
|
+
.update("content.region.details", {
|
|
184
|
+
roleOptions: { name: "Payment Info" }
|
|
185
|
+
})
|
|
186
|
+
.update("content.region.details.button.edit", {
|
|
187
|
+
roleOptions: { name: "Update" }
|
|
188
|
+
})
|
|
189
|
+
.getNestedLocator();
|
|
190
|
+
|
|
191
|
+
await edgeCaseNestedLocator.click();
|
|
192
|
+
});
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
### `addFilter(subPath, filterOptions)`
|
|
196
|
+
|
|
197
|
+
Adds filters in the same way as Playwright's [`locator.filter()`](https://playwright.dev/docs/api/class-locator#locator-filter). Multiple filters are merged in the order you call them.
|
|
198
|
+
|
|
199
|
+
```ts
|
|
200
|
+
const resetButton = await profile
|
|
201
|
+
.getLocatorSchema("content.region.details.button.edit")
|
|
202
|
+
.addFilter("content.region.details", { hasText: /Profile Details/i })
|
|
203
|
+
// .addFilter(...)
|
|
204
|
+
.getNestedLocator();
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
### `getNestedLocator(indices?)`
|
|
208
|
+
|
|
209
|
+
Resolves the full chain to a Playwright locator. Provide optional indices if you need to target a specific occurrence. This method is the same one the wrapper shortcut delegates to, but it keeps any updates or filters you applied earlier in the chain.
|
|
210
|
+
|
|
211
|
+
```ts
|
|
212
|
+
const nestedLocator = await profile
|
|
213
|
+
.getLocatorSchema("content.region.details.button.edit")
|
|
214
|
+
//.update(...)
|
|
215
|
+
//.addFilter(...)
|
|
216
|
+
.getNestedLocator({ "content.region": 1 }); // equivalent to .nth(1) (second occurance)
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
### `getLocator()`
|
|
220
|
+
|
|
221
|
+
Resolves only the final schema in the chain, i.e. the LocatorSchema which the full LocatorSchemaPath points to. The resulting locator is identical to calling `getNestedLocator()` on a chain that contains a single schema, but it is often more expressive when you only care about the last segment or need to manually chain given some edge case. Same as getNestedLocator, it keeps any updates or filters you applied earlier in the chain.
|
|
222
|
+
|
|
223
|
+
```ts
|
|
224
|
+
const badge = await profile
|
|
225
|
+
.getLocatorSchema("content.region.details.button.edit")
|
|
226
|
+
//.update(...)
|
|
227
|
+
//.addFilter(...)
|
|
228
|
+
.getLocator();
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
### Reusing a chain
|
|
232
|
+
|
|
233
|
+
Because each call to `getLocatorSchema()` returns a deep copy, you can derive multiple locators without affecting the original definitions.
|
|
234
|
+
|
|
235
|
+
```ts
|
|
236
|
+
const editButtonSchema = profile.getLocatorSchema("content.region.details.button.edit");
|
|
237
|
+
|
|
238
|
+
const editButton = await editButtonSchema.getLocator();
|
|
239
|
+
await editButton.click();
|
|
240
|
+
|
|
241
|
+
editButtonSchema.update("content.region.details.button.edit", {
|
|
242
|
+
roleOptions: { name: "Edit details" }
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
const editButtonUpdated = await editButtonSchema.getNestedLocator();
|
|
246
|
+
await editButtonUpdated.click();
|
|
247
|
+
|
|
248
|
+
// Calling profile.getLocatorSchema("content.region.details.button.edit") again
|
|
249
|
+
// returns a fresh deep copy of the original schema chain.
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
## Migrating from deprecated helpers
|
|
253
|
+
|
|
254
|
+
Earlier versions exposed index-based helpers. They continue to work but are deprecated in favour of the sub-path syntax described above.
|
|
255
|
+
|
|
256
|
+
```ts
|
|
257
|
+
// old
|
|
258
|
+
await profile.getNestedLocator("content.region.details.button.save", { 4: 2 });
|
|
259
|
+
|
|
260
|
+
// new
|
|
261
|
+
await profile.getNestedLocator("content.region.details.button.save", {
|
|
262
|
+
"content.region.details.button.save": 2
|
|
263
|
+
});
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
Switching to sub-path keys makes updates easier when `LocatorSchemaPath` strings change, and TypeScript will warn you if you mistype a path. While improving readability.
|