pomwright 1.3.0 → 1.5.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/AGENTS.md +37 -0
- package/CHANGELOG.md +193 -0
- package/README.md +316 -34
- package/dist/index.d.mts +1058 -132
- package/dist/index.d.ts +1058 -132
- package/dist/index.js +2309 -185
- package/dist/index.mjs +2304 -185
- package/docs/{get-locator-methods-explanation.md → v1/get-locator-methods-explanation.md} +0 -16
- package/docs/v1-to-v2-migration/bridge-migration-guide.md +159 -0
- package/docs/v1-to-v2-migration/direct-migration-guide.md +238 -0
- package/docs/v1-to-v2-migration/v1-to-v2-comparison.md +547 -0
- package/docs/v2/PageObject.md +293 -0
- package/docs/v2/composing-locator-modules.md +93 -0
- package/docs/v2/locator-registry.md +693 -0
- package/docs/v2/logging.md +168 -0
- package/docs/v2/overview.md +515 -0
- package/docs/v2/session-storage.md +160 -0
- package/index.ts +61 -9
- package/intTestV2/.env +0 -0
- package/intTestV2/fixtures/testApp.fixtures.ts +43 -0
- package/intTestV2/package.json +22 -0
- package/intTestV2/page-object-models/testApp/pages/iframe/iframe.locatorSchema.ts +24 -0
- package/intTestV2/page-object-models/testApp/pages/iframe/iframe.page.ts +17 -0
- package/intTestV2/page-object-models/testApp/pages/testPage.locatorSchema.ts +32 -0
- package/intTestV2/page-object-models/testApp/pages/testPage.page.ts +119 -0
- package/intTestV2/page-object-models/testApp/pages/testPath/[color]/color.locatorSchema.ts +29 -0
- package/intTestV2/page-object-models/testApp/pages/testPath/[color]/color.page.ts +48 -0
- package/intTestV2/page-object-models/testApp/pages/testPath/testPath.locatorSchema.ts +9 -0
- package/intTestV2/page-object-models/testApp/pages/testPath/testPath.page.ts +23 -0
- package/intTestV2/page-object-models/testApp/pages/testfilters/testfilters.locatorSchema.ts +114 -0
- package/intTestV2/page-object-models/testApp/pages/testfilters/testfilters.page.ts +23 -0
- package/intTestV2/page-object-models/testApp/testApp.base.ts +20 -0
- package/intTestV2/playwright.config.ts +54 -0
- package/intTestV2/server.js +216 -0
- package/intTestV2/test-data/staticPage/index.html +280 -0
- package/intTestV2/test-data/staticPage/w3images/avatar2.png +0 -0
- package/intTestV2/test-data/staticPage/w3images/avatar3.png +0 -0
- package/intTestV2/test-data/staticPage/w3images/avatar5.png +0 -0
- package/intTestV2/test-data/staticPage/w3images/avatar6.png +0 -0
- package/intTestV2/test-data/staticPage/w3images/forest.jpg +0 -0
- package/intTestV2/test-data/staticPage/w3images/lights.jpg +0 -0
- package/intTestV2/test-data/staticPage/w3images/mountains.jpg +0 -0
- package/intTestV2/test-data/staticPage/w3images/nature.jpg +0 -0
- package/intTestV2/test-data/staticPage/w3images/snow.jpg +0 -0
- package/intTestV2/tests/locatorRegistry/add/add.describe.spec.ts +54 -0
- package/intTestV2/tests/locatorRegistry/add/add.filter.spec.ts +143 -0
- package/intTestV2/tests/locatorRegistry/add/add.frameLocator.spec.ts +23 -0
- package/intTestV2/tests/locatorRegistry/add/add.getByAltText.spec.ts +23 -0
- package/intTestV2/tests/locatorRegistry/add/add.getById.spec.ts +45 -0
- package/intTestV2/tests/locatorRegistry/add/add.getByLabel.spec.ts +23 -0
- package/intTestV2/tests/locatorRegistry/add/add.getByPlaceholder.spec.ts +23 -0
- package/intTestV2/tests/locatorRegistry/add/add.getByRole.spec.ts +23 -0
- package/intTestV2/tests/locatorRegistry/add/add.getByTestId.spec.ts +23 -0
- package/intTestV2/tests/locatorRegistry/add/add.getByText.spec.ts +23 -0
- package/intTestV2/tests/locatorRegistry/add/add.getByTitle.spec.ts +23 -0
- package/intTestV2/tests/locatorRegistry/add/add.locator.spec.ts +23 -0
- package/intTestV2/tests/locatorRegistry/add/add.reuseExisting.spec.ts +66 -0
- package/intTestV2/tests/locatorRegistry/add/add.reuseReusable.spec.ts +311 -0
- package/intTestV2/tests/locatorRegistry/add/add.spec.ts +159 -0
- package/intTestV2/tests/locatorRegistry/filter.cycle.spec.ts +39 -0
- package/intTestV2/tests/locatorRegistry/getLocator/getLocator.spec.ts +253 -0
- package/intTestV2/tests/locatorRegistry/getLocatorSchema/getLocatorSchema.clearSteps.spec.ts +105 -0
- package/intTestV2/tests/locatorRegistry/getLocatorSchema/getLocatorSchema.describe.spec.ts +23 -0
- package/intTestV2/tests/locatorRegistry/getLocatorSchema/getLocatorSchema.filter.spec.ts +368 -0
- package/intTestV2/tests/locatorRegistry/getLocatorSchema/getLocatorSchema.getLocator.spec.ts +56 -0
- package/intTestV2/tests/locatorRegistry/getLocatorSchema/getLocatorSchema.getNestedLocator.spec.ts +175 -0
- package/intTestV2/tests/locatorRegistry/getLocatorSchema/getLocatorSchema.nth.spec.ts +60 -0
- package/intTestV2/tests/locatorRegistry/getLocatorSchema/getLocatorSchema.remove.spec.ts +32 -0
- package/intTestV2/tests/locatorRegistry/getLocatorSchema/getLocatorSchema.replace.spec.ts +24 -0
- package/intTestV2/tests/locatorRegistry/getLocatorSchema/getLocatorSchema.spec.ts +110 -0
- package/intTestV2/tests/locatorRegistry/getLocatorSchema/getLocatorSchema.update.spec.ts +322 -0
- package/intTestV2/tests/locatorRegistry/getNestedLocator/getNestedLocator.spec.ts +412 -0
- package/intTestV2/tests/locatorRegistry/registry/registry.binding.spec.ts +50 -0
- package/intTestV2/tests/locatorRegistry/validation/validation.locatorSchemaPath.spec.ts +115 -0
- package/intTestV2/tests/locatorRegistry/validation/validation.sub-path.spec.ts +45 -0
- package/intTestV2/tests/step/step.spec.ts +49 -0
- package/intTestV2/tests/testApp/color.spec.ts +15 -0
- package/intTestV2/tests/testApp/iframe.spec.ts +57 -0
- package/intTestV2/tests/testApp/testFilters.spec.ts +24 -0
- package/intTestV2/tests/testApp/testPage.spec.ts +161 -0
- package/intTestV2/tests/testApp/testPath.spec.ts +18 -0
- package/pack-build.sh +11 -0
- package/pack-test-v2.sh +36 -0
- package/package.json +10 -3
- package/playwright.base.ts +42 -0
- package/skills/README.md +56 -0
- package/skills/pomwright-v1-5-bridge-migration/SKILL.md +40 -0
- package/skills/pomwright-v1-5-bridge-migration/references/call-site-migration.md +178 -0
- package/skills/pomwright-v1-5-bridge-migration/references/schema-translation.md +183 -0
- package/skills/pomwright-v2-migration/SKILL.md +63 -0
- package/skills/pomwright-v2-migration/references/call-site-migration.md +265 -0
- package/skills/pomwright-v2-migration/references/class-migration.md +266 -0
- package/skills/pomwright-v2-migration/references/fixture-and-helpers.md +423 -0
- package/skills/pomwright-v2-migration/references/locator-registration.md +344 -0
- package/srcV2/fixture/base.fixtures.ts +23 -0
- package/srcV2/helpers/navigation.ts +153 -0
- package/srcV2/helpers/playwrightReportLogger.ts +196 -0
- package/srcV2/helpers/sessionStorage.ts +251 -0
- package/srcV2/helpers/stepDecorator.ts +106 -0
- package/srcV2/locators/index.ts +15 -0
- package/srcV2/locators/locatorQueryBuilder.ts +427 -0
- package/srcV2/locators/locatorRegistrationBuilder.ts +558 -0
- package/srcV2/locators/locatorRegistry.ts +541 -0
- package/srcV2/locators/locatorUpdateBuilder.ts +602 -0
- package/srcV2/locators/reusableLocatorBuilder.ts +200 -0
- package/srcV2/locators/types.ts +256 -0
- package/srcV2/locators/utils.ts +309 -0
- package/srcV2/locators/v1SchemaTranslator.ts +178 -0
- package/srcV2/pageObject.ts +105 -0
- /package/docs/{BaseApi-explanation.md → v1/BaseApi-explanation.md} +0 -0
- /package/docs/{BasePage-explanation.md → v1/BasePage-explanation.md} +0 -0
- /package/docs/{LocatorSchema-explanation.md → v1/LocatorSchema-explanation.md} +0 -0
- /package/docs/{LocatorSchemaPath-explanation.md → v1/LocatorSchemaPath-explanation.md} +0 -0
- /package/docs/{PlaywrightReportLogger-explanation.md → v1/PlaywrightReportLogger-explanation.md} +0 -0
- /package/docs/{intro-to-using-pomwright.md → v1/intro-to-using-pomwright.md} +0 -0
- /package/docs/{sessionStorage-methods-explanation.md → v1/sessionStorage-methods-explanation.md} +0 -0
- /package/docs/{tips-folder-structure.md → v1/tips-folder-structure.md} +0 -0
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
# Locator Registration: defineLocators and the v2 add() DSL
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
v2 locator registration uses a fluent builder API inside `defineLocators()`. The `add(path)` method returns a builder with Playwright-matching method names. No `GetByMethod` enum or schema objects are needed.
|
|
6
|
+
|
|
7
|
+
## File structure
|
|
8
|
+
|
|
9
|
+
v2 separates path types and registration into a companion `.locatorSchema.ts` file:
|
|
10
|
+
|
|
11
|
+
```ts
|
|
12
|
+
// myPage.locatorSchema.ts
|
|
13
|
+
import type { LocatorRegistry } from "pomwright";
|
|
14
|
+
|
|
15
|
+
export type Paths =
|
|
16
|
+
| "main"
|
|
17
|
+
| "main.form"
|
|
18
|
+
| "main.form.input@username"
|
|
19
|
+
| "main.form.button@submit";
|
|
20
|
+
|
|
21
|
+
export function defineLocators(registry: LocatorRegistry<Paths>) {
|
|
22
|
+
registry.add("main").locator("main");
|
|
23
|
+
registry.add("main.form").locator("form.login");
|
|
24
|
+
registry.add("main.form.input@username").getByLabel("Username");
|
|
25
|
+
registry.add("main.form.button@submit").getByRole("button", { name: "Submit" });
|
|
26
|
+
}
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
```ts
|
|
30
|
+
// myPage.page.ts
|
|
31
|
+
import { defineLocators, type Paths } from "./myPage.locatorSchema";
|
|
32
|
+
|
|
33
|
+
export default class MyPage extends PageObject<Paths> {
|
|
34
|
+
protected defineLocators(): void {
|
|
35
|
+
defineLocators(this.locatorRegistry);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
This pattern separates concerns (types/registration vs class behavior) and allows locator definitions to be shared across page objects.
|
|
41
|
+
|
|
42
|
+
## GetByMethod mapping
|
|
43
|
+
|
|
44
|
+
| v1 `locatorMethod` | v1 schema field | v1 options field | v2 method | v2 arguments |
|
|
45
|
+
|---|---|---|---|---|
|
|
46
|
+
| `GetByMethod.role` | `role` | `roleOptions` | `getByRole(role, options?)` | role value, roleOptions as second arg |
|
|
47
|
+
| `GetByMethod.text` | `text` | `textOptions` | `getByText(text, options?)` | text value, textOptions as second arg |
|
|
48
|
+
| `GetByMethod.label` | `label` | `labelOptions` | `getByLabel(text, options?)` | label value, labelOptions as second arg |
|
|
49
|
+
| `GetByMethod.placeholder` | `placeholder` | `placeholderOptions` | `getByPlaceholder(text, options?)` | placeholder value, placeholderOptions |
|
|
50
|
+
| `GetByMethod.altText` | `altText` | `altTextOptions` | `getByAltText(text, options?)` | altText value, altTextOptions |
|
|
51
|
+
| `GetByMethod.title` | `title` | `titleOptions` | `getByTitle(text, options?)` | title value, titleOptions |
|
|
52
|
+
| `GetByMethod.locator` | `locator` (string) | `locatorOptions` | `locator(selector, options?)` | locator string, locatorOptions |
|
|
53
|
+
| `GetByMethod.frameLocator` | `frameLocator` | - | `frameLocator(selector)` | frameLocator string |
|
|
54
|
+
| `GetByMethod.testId` | `testId` | - | `getByTestId(testId)` | testId value |
|
|
55
|
+
| `GetByMethod.id` | `id` | - | `getById(id)` | id string or RegExp |
|
|
56
|
+
| `GetByMethod.dataCy` | `dataCy` | - | `locator('[data-cy="value"]')` | CSS attribute selector string |
|
|
57
|
+
|
|
58
|
+
## Translation examples
|
|
59
|
+
|
|
60
|
+
### Role
|
|
61
|
+
|
|
62
|
+
```ts
|
|
63
|
+
// v1
|
|
64
|
+
this.locators.addSchema("main.button@login", {
|
|
65
|
+
role: "button",
|
|
66
|
+
roleOptions: { name: "Login" },
|
|
67
|
+
locatorMethod: GetByMethod.role,
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
// v2
|
|
71
|
+
this.add("main.button@login").getByRole("button", { name: "Login" });
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
### Locator (CSS/XPath)
|
|
75
|
+
|
|
76
|
+
```ts
|
|
77
|
+
// v1
|
|
78
|
+
this.locators.addSchema("main", {
|
|
79
|
+
locator: "main",
|
|
80
|
+
locatorMethod: GetByMethod.locator,
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
// v2
|
|
84
|
+
this.add("main").locator("main");
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### Locator with options
|
|
88
|
+
|
|
89
|
+
```ts
|
|
90
|
+
// v1
|
|
91
|
+
this.locators.addSchema("body.section@playground", {
|
|
92
|
+
locator: "section",
|
|
93
|
+
locatorOptions: { hasText: /Playground/i },
|
|
94
|
+
locatorMethod: GetByMethod.locator,
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
// v2
|
|
98
|
+
this.add("body.section@playground").locator("section", { hasText: /Playground/i });
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
### Label
|
|
102
|
+
|
|
103
|
+
```ts
|
|
104
|
+
// v1
|
|
105
|
+
this.locators.addSchema("main.form.input@username", {
|
|
106
|
+
label: "Username",
|
|
107
|
+
locatorMethod: GetByMethod.label,
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
// v2
|
|
111
|
+
this.add("main.form.input@username").getByLabel("Username");
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
### Text
|
|
115
|
+
|
|
116
|
+
```ts
|
|
117
|
+
// v1
|
|
118
|
+
this.locators.addSchema("main.heading", {
|
|
119
|
+
text: "Welcome",
|
|
120
|
+
textOptions: { exact: true },
|
|
121
|
+
locatorMethod: GetByMethod.text,
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
// v2
|
|
125
|
+
this.add("main.heading").getByText("Welcome", { exact: true });
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
### Placeholder
|
|
129
|
+
|
|
130
|
+
```ts
|
|
131
|
+
// v1
|
|
132
|
+
this.locators.addSchema("main.form.input@search", {
|
|
133
|
+
placeholder: "Search...",
|
|
134
|
+
locatorMethod: GetByMethod.placeholder,
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
// v2
|
|
138
|
+
this.add("main.form.input@search").getByPlaceholder("Search...");
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
### Title
|
|
142
|
+
|
|
143
|
+
```ts
|
|
144
|
+
// v1
|
|
145
|
+
this.locators.addSchema("topMenu.news", {
|
|
146
|
+
title: "News",
|
|
147
|
+
locatorMethod: GetByMethod.title,
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
// v2
|
|
151
|
+
this.add("topMenu.news").getByTitle("News");
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
### TestId
|
|
155
|
+
|
|
156
|
+
```ts
|
|
157
|
+
// v1
|
|
158
|
+
this.locators.addSchema("main.toggle", {
|
|
159
|
+
testId: "toggle-a",
|
|
160
|
+
locatorMethod: GetByMethod.testId,
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
// v2
|
|
164
|
+
this.add("main.toggle").getByTestId("toggle-a");
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
### FrameLocator
|
|
168
|
+
|
|
169
|
+
```ts
|
|
170
|
+
// v1
|
|
171
|
+
this.locators.addSchema("main.frame@login", {
|
|
172
|
+
frameLocator: "#auth",
|
|
173
|
+
locatorMethod: GetByMethod.frameLocator,
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
// v2
|
|
177
|
+
this.add("main.frame@login").frameLocator("#auth");
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
Terminal frameLocator paths in v2 resolve to `frameLocator.owner()` (the iframe element), not the frame itself. Non-terminal frames scope into the frame for subsequent segments. This is a behavioral change from v1.
|
|
181
|
+
|
|
182
|
+
### getById
|
|
183
|
+
|
|
184
|
+
```ts
|
|
185
|
+
// v1
|
|
186
|
+
this.locators.addSchema("main.modal@close", {
|
|
187
|
+
id: "close-modal",
|
|
188
|
+
locatorMethod: GetByMethod.id,
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
// v2
|
|
192
|
+
this.add("main.modal@close").getById("close-modal");
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
Note: v1 regex IDs use prefix match `*[id^="pattern"]`; v2 regex IDs use substring match `[id*="pattern"]` with CSS escaping. Results may differ for edge cases.
|
|
196
|
+
|
|
197
|
+
### dataCy (removed in v2)
|
|
198
|
+
|
|
199
|
+
```ts
|
|
200
|
+
// v1
|
|
201
|
+
this.locators.addSchema("main.widget", {
|
|
202
|
+
dataCy: "my-widget",
|
|
203
|
+
locatorMethod: GetByMethod.dataCy,
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
// v2 (translate to CSS attribute selector)
|
|
207
|
+
this.add("main.widget").locator('[data-cy="my-widget"]');
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
v2 does not include a built-in `data-cy` selector engine. Use `locator()` with a CSS attribute selector, or register a custom Playwright selector engine in your test fixtures.
|
|
211
|
+
|
|
212
|
+
## Filter translation
|
|
213
|
+
|
|
214
|
+
v1 `filter` property on schema -> v2 chained `.filter()` call:
|
|
215
|
+
|
|
216
|
+
```ts
|
|
217
|
+
// v1
|
|
218
|
+
this.locators.addSchema("body.section@playground", {
|
|
219
|
+
locator: "section",
|
|
220
|
+
locatorMethod: GetByMethod.locator,
|
|
221
|
+
filter: { hasText: /Playground/i },
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
// v2
|
|
225
|
+
this.add("body.section@playground").locator("section").filter({ hasText: /Playground/i });
|
|
226
|
+
|
|
227
|
+
// OR, if the filter only has hasText/hasNotText, use locator options instead:
|
|
228
|
+
this.add("body.section@playground").locator("section", { hasText: /Playground/i });
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
v2 `filter` accepts `has`/`hasNot` as **registry path strings or Playwright Locator instances**. Path strings are often preferred for registry-native references:
|
|
232
|
+
|
|
233
|
+
```ts
|
|
234
|
+
// v2 filter with has/hasNot as path references
|
|
235
|
+
this.add("main.card").locator(".card").filter({
|
|
236
|
+
has: "main.card.badge", // path string, not Locator
|
|
237
|
+
hasText: "Active",
|
|
238
|
+
});
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
## Registration with filter + nth at definition time
|
|
242
|
+
|
|
243
|
+
```ts
|
|
244
|
+
// v2 supports chaining filter and nth on add():
|
|
245
|
+
this.add("one.two").locator("div.two").filter({ hasText: "two" }).nth(0);
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
This registers the locator with a filter step and an nth step baked into the schema.
|
|
249
|
+
|
|
250
|
+
## Reusable locator patterns
|
|
251
|
+
|
|
252
|
+
### v1: LocatorSchemaWithoutPath + spread
|
|
253
|
+
|
|
254
|
+
```ts
|
|
255
|
+
const buttonSchema: LocatorSchemaWithoutPath = {
|
|
256
|
+
role: "button",
|
|
257
|
+
locatorMethod: GetByMethod.role,
|
|
258
|
+
};
|
|
259
|
+
|
|
260
|
+
this.locators.addSchema("main.button@submit", { ...buttonSchema, roleOptions: { name: "Submit" } });
|
|
261
|
+
this.locators.addSchema("main.button@cancel", { ...buttonSchema, roleOptions: { name: "Cancel" } });
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
### v2: createReusable
|
|
265
|
+
|
|
266
|
+
```ts
|
|
267
|
+
// createReusable is available on the LocatorRegistry
|
|
268
|
+
const button = this.locatorRegistry.createReusable.getByRole("button");
|
|
269
|
+
|
|
270
|
+
// Reuse with PATCH-style override (only provide options to merge)
|
|
271
|
+
this.add("main.button@submit", { reuse: button }).getByRole({ name: "Submit" });
|
|
272
|
+
this.add("main.button@cancel", { reuse: button }).getByRole({ name: "Cancel" });
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
### v2: reuse by path reference
|
|
276
|
+
|
|
277
|
+
```ts
|
|
278
|
+
// Register the first path normally
|
|
279
|
+
this.add("main.button@submit").getByRole("button", { name: "Submit" });
|
|
280
|
+
|
|
281
|
+
// Clone its definition to another path (exact clone, no override chaining)
|
|
282
|
+
this.add("main.button@cancel", { reuse: "main.button@submit" });
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
`add(path, { reuse: "existing.path" })` clones the existing schema and returns `void`, so it cannot be chained with additional overrides.
|
|
286
|
+
|
|
287
|
+
### v2: createReusable with steps
|
|
288
|
+
|
|
289
|
+
```ts
|
|
290
|
+
const activeItem = this.locatorRegistry.createReusable
|
|
291
|
+
.locator(".item")
|
|
292
|
+
.filter({ hasText: "Active" });
|
|
293
|
+
|
|
294
|
+
this.add("main.list.item@active", { reuse: activeItem });
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
## Programmatic registration with loops
|
|
298
|
+
|
|
299
|
+
v2 supports template literal types and programmatic registration for DRY code:
|
|
300
|
+
|
|
301
|
+
```ts
|
|
302
|
+
const tableVariants = ["body.table", "body.table@hexCode"] as const;
|
|
303
|
+
|
|
304
|
+
type TableVariants = (typeof tableVariants)[number];
|
|
305
|
+
type TableChildren = "row" | "row.rowheader" | "row.cell";
|
|
306
|
+
|
|
307
|
+
export type Paths = "body" | "body.heading" | TableVariants | `${TableVariants}.${TableChildren}`;
|
|
308
|
+
|
|
309
|
+
export function defineLocators(registry: LocatorRegistry<Paths>) {
|
|
310
|
+
registry.add("body").locator("body");
|
|
311
|
+
registry.add("body.heading").getByRole("heading", { name: "Your Random Color is:" });
|
|
312
|
+
|
|
313
|
+
for (const variant of tableVariants) {
|
|
314
|
+
registry.add(variant).getByRole("table");
|
|
315
|
+
registry.add(`${variant}.row`).getByRole("row");
|
|
316
|
+
registry.add(`${variant}.row.rowheader`).getByRole("rowheader");
|
|
317
|
+
registry.add(`${variant}.row.cell`).getByRole("cell");
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
## Path validation rules
|
|
323
|
+
|
|
324
|
+
v2 enforces stricter path validation than v1:
|
|
325
|
+
|
|
326
|
+
| Rule | v1 | v2 |
|
|
327
|
+
|---|---|---|
|
|
328
|
+
| Leading/trailing dots | Allowed | Rejected |
|
|
329
|
+
| Whitespace in paths | Allowed | Rejected |
|
|
330
|
+
| Empty path segments (`a..b`) | Allowed | Rejected |
|
|
331
|
+
| Duplicate registration | Overwrites silently | Throws error |
|
|
332
|
+
| Missing parent path | Allowed | Registration does not require parent-first order; chain semantics depend on registered segments |
|
|
333
|
+
|
|
334
|
+
Parent-first registration is recommended for readability and predictable chain behavior: `"main"` before `"main.form"` before `"main.form.input"`.
|
|
335
|
+
|
|
336
|
+
## describe() at registration time
|
|
337
|
+
|
|
338
|
+
```ts
|
|
339
|
+
this.add("main.button@submit")
|
|
340
|
+
.getByRole("button", { name: "Submit" })
|
|
341
|
+
.describe("The main submit button in the login form");
|
|
342
|
+
```
|
|
343
|
+
|
|
344
|
+
Adds a descriptive label to the locator (uses Playwright's `locator.describe()`).
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { test as base } from "@playwright/test";
|
|
2
|
+
import { type LogEntry, type LogLevel, PlaywrightReportLogger } from "../helpers/playwrightReportLogger";
|
|
3
|
+
|
|
4
|
+
type BaseFixtures = {
|
|
5
|
+
log: PlaywrightReportLogger;
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
export const test = base.extend<BaseFixtures>({
|
|
9
|
+
// biome-ignore lint/correctness/noEmptyPattern: Playwright does not support the use of _
|
|
10
|
+
log: async ({}, use, testInfo) => {
|
|
11
|
+
const contextName = "TestCase";
|
|
12
|
+
const sharedLogEntry: LogEntry[] = [];
|
|
13
|
+
|
|
14
|
+
const sharedLogLevel: { current: LogLevel; initial: LogLevel } =
|
|
15
|
+
testInfo.retry === 0 ? { current: "warn", initial: "warn" } : { current: "debug", initial: "debug" };
|
|
16
|
+
|
|
17
|
+
const log = new PlaywrightReportLogger(sharedLogLevel, sharedLogEntry, contextName);
|
|
18
|
+
|
|
19
|
+
await use(log);
|
|
20
|
+
|
|
21
|
+
log.attachLogsToTest(testInfo);
|
|
22
|
+
},
|
|
23
|
+
});
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { expect, type Page, test } from "@playwright/test";
|
|
2
|
+
|
|
3
|
+
type WaitUntil = NonNullable<Parameters<Page["goto"]>[1]>["waitUntil"];
|
|
4
|
+
type State = NonNullable<Parameters<Page["waitForLoadState"]>[0]>;
|
|
5
|
+
|
|
6
|
+
const DEFAULT_WAIT_UNTIL: WaitUntil = "load";
|
|
7
|
+
const DEFAULT_LOAD_STATE: State = "load";
|
|
8
|
+
|
|
9
|
+
export type NavigationOptions = {
|
|
10
|
+
waitUntil?: WaitUntil;
|
|
11
|
+
waitForLoadState?: State;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export interface NavigationString {
|
|
15
|
+
goto(urlPathOrUrl: string, options?: NavigationOptions): Promise<void>;
|
|
16
|
+
gotoThisPage(options?: NavigationOptions): Promise<void>;
|
|
17
|
+
expectThisPage(options?: NavigationOptions): Promise<void>;
|
|
18
|
+
expectAnotherPage(options?: NavigationOptions): Promise<void>;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface NavigationRegExp {
|
|
22
|
+
expectThisPage(options?: NavigationOptions): Promise<void>;
|
|
23
|
+
expectAnotherPage(options?: NavigationOptions): Promise<void>;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export type ExtractNavigationType<FullUrlType> = FullUrlType extends RegExp ? NavigationRegExp : NavigationString;
|
|
27
|
+
|
|
28
|
+
class Navigation {
|
|
29
|
+
private pageActionsToPerform: (() => Promise<void>)[];
|
|
30
|
+
private defaultOptions?: NavigationOptions;
|
|
31
|
+
|
|
32
|
+
constructor(
|
|
33
|
+
private page: Page,
|
|
34
|
+
private baseUrl: string | RegExp,
|
|
35
|
+
private urlPath: string | RegExp,
|
|
36
|
+
private fullUrl: string | RegExp,
|
|
37
|
+
private label: string,
|
|
38
|
+
actions: (() => Promise<void>)[] | null = null,
|
|
39
|
+
defaultOptions?: NavigationOptions,
|
|
40
|
+
) {
|
|
41
|
+
this.pageActionsToPerform = actions ?? [];
|
|
42
|
+
this.defaultOptions = defaultOptions;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
private resolveWaitUntil(options?: NavigationOptions) {
|
|
46
|
+
return options?.waitUntil ?? this.defaultOptions?.waitUntil ?? DEFAULT_WAIT_UNTIL;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
private resolveWaitForLoadState(options?: NavigationOptions) {
|
|
50
|
+
return options?.waitForLoadState ?? this.defaultOptions?.waitForLoadState ?? DEFAULT_LOAD_STATE;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
private async executeActions() {
|
|
54
|
+
for (const action of this.pageActionsToPerform) {
|
|
55
|
+
await action();
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Navigate to a provided URL or URL path. If the input starts with "/", the POC's baseUrl is used as a prefix.
|
|
61
|
+
* Available only when baseUrl and urlPath are strings.
|
|
62
|
+
*/
|
|
63
|
+
public async goto(urlPathOrUrl: string, options?: NavigationOptions) {
|
|
64
|
+
const waitUntil = this.resolveWaitUntil(options);
|
|
65
|
+
if (typeof this.baseUrl !== "string" || typeof this.urlPath !== "string") {
|
|
66
|
+
throw new Error("goto() is not supported when baseUrl or urlPath is a RegExp.");
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
await test.step(`${this.label}: Navigate to the provided URL or URL Path`, async () => {
|
|
70
|
+
const targetUrl = urlPathOrUrl.startsWith("/") ? `${this.baseUrl}${urlPathOrUrl}` : urlPathOrUrl;
|
|
71
|
+
await this.page.goto(targetUrl, { waitUntil });
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Navigate to this page's fullUrl and run any post-navigation actions.
|
|
77
|
+
* Available only when fullUrl is a string.
|
|
78
|
+
*/
|
|
79
|
+
public async gotoThisPage(options?: NavigationOptions) {
|
|
80
|
+
if (typeof this.fullUrl !== "string") {
|
|
81
|
+
throw new Error("gotoThisPage() is not supported when fullUrl is a RegExp.");
|
|
82
|
+
}
|
|
83
|
+
const waitUntil = this.resolveWaitUntil(options);
|
|
84
|
+
const fullUrl = this.fullUrl;
|
|
85
|
+
|
|
86
|
+
await test.step(`${this.label}: Navigate to this Page`, async () => {
|
|
87
|
+
await this.page.goto(fullUrl, { waitUntil });
|
|
88
|
+
await this.executeActions();
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Expect to be on this page. Works with both string and RegExp fullUrl values.
|
|
94
|
+
* Uses waitUntil from navigation options when waiting for URL.
|
|
95
|
+
*/
|
|
96
|
+
public async expectThisPage(options?: NavigationOptions) {
|
|
97
|
+
const waitUntil = this.resolveWaitUntil(options);
|
|
98
|
+
await test.step(`${this.label}: Expect this Page`, async () => {
|
|
99
|
+
await this.page.waitForURL(this.fullUrl, { waitUntil });
|
|
100
|
+
|
|
101
|
+
await expect(async () => {
|
|
102
|
+
if (this.fullUrl instanceof RegExp) {
|
|
103
|
+
expect(this.page.url(), `expected '${this.fullUrl}', found '${this.page.url()}'`).toMatch(this.fullUrl);
|
|
104
|
+
} else {
|
|
105
|
+
expect(this.page.url(), `expected '${this.fullUrl}', found '${this.page.url()}'`).toBe(this.fullUrl);
|
|
106
|
+
}
|
|
107
|
+
}).toPass();
|
|
108
|
+
|
|
109
|
+
await this.executeActions();
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Expect to be on any other page (i.e. not this page).
|
|
115
|
+
* Uses waitForLoadState from navigation options before validating URL.
|
|
116
|
+
*/
|
|
117
|
+
public async expectAnotherPage(options?: NavigationOptions) {
|
|
118
|
+
const waitForLoadState = this.resolveWaitForLoadState(options);
|
|
119
|
+
await test.step(`${this.label}: Expect any other Page`, async () => {
|
|
120
|
+
await this.page.waitForLoadState(waitForLoadState);
|
|
121
|
+
|
|
122
|
+
if (this.fullUrl instanceof RegExp) {
|
|
123
|
+
await expect
|
|
124
|
+
.poll(async () => this.page.url(), {
|
|
125
|
+
message: `expected url to not match '${this.fullUrl}'`,
|
|
126
|
+
})
|
|
127
|
+
.not.toMatch(this.fullUrl);
|
|
128
|
+
} else {
|
|
129
|
+
await expect
|
|
130
|
+
.poll(async () => this.page.url(), {
|
|
131
|
+
message: `expected url to not be '${this.fullUrl}', found '${this.page.url()}'`,
|
|
132
|
+
})
|
|
133
|
+
.not.toBe(this.fullUrl);
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Factory to create a navigation helper. The returned type is narrowed based on the fullUrl type.
|
|
141
|
+
*/
|
|
142
|
+
export function createNavigation<FullUrlType extends string | RegExp>(
|
|
143
|
+
page: Page,
|
|
144
|
+
baseUrl: string | RegExp,
|
|
145
|
+
urlPath: string | RegExp,
|
|
146
|
+
fullUrl: FullUrlType,
|
|
147
|
+
label: string,
|
|
148
|
+
actions: (() => Promise<void>)[] | null = null,
|
|
149
|
+
defaultOptions?: NavigationOptions,
|
|
150
|
+
): ExtractNavigationType<FullUrlType> {
|
|
151
|
+
const navigation = new Navigation(page, baseUrl, urlPath, fullUrl, label, actions, defaultOptions);
|
|
152
|
+
return navigation as ExtractNavigationType<FullUrlType>;
|
|
153
|
+
}
|