pomwright 1.4.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 +179 -0
- package/README.md +316 -34
- package/dist/index.d.mts +1052 -30
- package/dist/index.d.ts +1052 -30
- package/dist/index.js +2263 -65
- package/dist/index.mjs +2260 -67
- 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/{get-locator-methods-explanation.md → v1/get-locator-methods-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,693 @@
|
|
|
1
|
+
# Locator Registry (v2)
|
|
2
|
+
|
|
3
|
+
## Table of contents
|
|
4
|
+
|
|
5
|
+
1. [Locator Registry overview](#locator-registry-overview)
|
|
6
|
+
2. [Mental model](#mental-model)
|
|
7
|
+
3. [Path types and validation](#path-types-and-validation)
|
|
8
|
+
1. [Compile-time path constraints](#compile-time-path-constraints)
|
|
9
|
+
2. [Runtime validation rules](#runtime-validation-rules)
|
|
10
|
+
3. [Common path mistakes and errors](#common-path-mistakes-and-errors)
|
|
11
|
+
4. [Factory and accessors](#factory-and-accessors)
|
|
12
|
+
1. [`createRegistryWithAccessors`](#createregistrywithaccessors)
|
|
13
|
+
2. [Accessor types](#accessor-types)
|
|
14
|
+
3. [Public `LocatorRegistry` vs internal registry](#public-locatorregistry-vs-internal-registry)
|
|
15
|
+
5. [Registering locators with `add`](#registering-locators-with-add)
|
|
16
|
+
1. [All strategy methods](#all-strategy-methods)
|
|
17
|
+
2. [Post-definition chain methods](#post-definition-chain-methods)
|
|
18
|
+
3. [One-strategy-per-registration rule](#one-strategy-per-registration-rule)
|
|
19
|
+
4. [Duplicate path behavior](#duplicate-path-behavior)
|
|
20
|
+
6. [`getById` deep dive](#getbyid-deep-dive)
|
|
21
|
+
1. [String normalization](#string-normalization)
|
|
22
|
+
2. [RegExp behavior and escaping](#regexp-behavior-and-escaping)
|
|
23
|
+
3. [Examples](#examples)
|
|
24
|
+
7. [`filter` deep dive](#filter-deep-dive)
|
|
25
|
+
1. [Accepted `has`/`hasNot` reference forms](#accepted-hashasnot-reference-forms)
|
|
26
|
+
2. [Path examples](#path-examples)
|
|
27
|
+
3. [Frame-locator restrictions in filters](#frame-locator-restrictions-in-filters)
|
|
28
|
+
8. [Resolution APIs](#resolution-apis)
|
|
29
|
+
1. [`getLocator(path)`](#getlocatorpath)
|
|
30
|
+
2. [`getNestedLocator(path)`](#getnestedlocatorpath)
|
|
31
|
+
3. [Frame behavior: terminal vs non-terminal](#frame-behavior-terminal-vs-non-terminal)
|
|
32
|
+
4. [Sparse chain behavior](#sparse-chain-behavior)
|
|
33
|
+
9. [Query builder: `getLocatorSchema(path)`](#query-builder-getlocatorschemapath)
|
|
34
|
+
1. [Clone semantics (registry is not mutated)](#clone-semantics-registry-is-not-mutated)
|
|
35
|
+
2. [Sub-path scope rules](#sub-path-scope-rules)
|
|
36
|
+
3. [Method-by-method reference](#method-by-method-reference)
|
|
37
|
+
4. [Default sub-path behavior table](#default-sub-path-behavior-table)
|
|
38
|
+
10. [`update` and `replace` semantics](#update-and-replace-semantics)
|
|
39
|
+
1. [`update`: PATCH-style merge](#update-patch-style-merge)
|
|
40
|
+
2. [`replace`: POST-style overwrite](#replace-post-style-overwrite)
|
|
41
|
+
3. [Required fields and type switching](#required-fields-and-type-switching)
|
|
42
|
+
11. [`remove` and tombstones](#remove-and-tombstones)
|
|
43
|
+
1. [Non-terminal removal](#non-terminal-removal)
|
|
44
|
+
2. [Terminal removal behavior](#terminal-removal-behavior)
|
|
45
|
+
3. [Rehydrating removed segments](#rehydrating-removed-segments)
|
|
46
|
+
12. [Reusable locators: `createReusable`](#reusable-locators-createreusable)
|
|
47
|
+
1. [Seed creation](#seed-creation)
|
|
48
|
+
2. [Reuse by object seed](#reuse-by-object-seed)
|
|
49
|
+
3. [Reuse by existing path string](#reuse-by-existing-path-string)
|
|
50
|
+
4. [Reuse patch rules and limitations](#reuse-patch-rules-and-limitations)
|
|
51
|
+
5. [Seed immutability](#seed-immutability)
|
|
52
|
+
13. [`describe` semantics](#describe-semantics)
|
|
53
|
+
1. [Registry-level descriptions](#registry-level-descriptions)
|
|
54
|
+
2. [Query-level overrides](#query-level-overrides)
|
|
55
|
+
14. [Error reference and troubleshooting](#error-reference-and-troubleshooting)
|
|
56
|
+
15. [Tested behavioral guarantees](#tested-behavioral-guarantees)
|
|
57
|
+
16. [Migration notes from v1](#migration-notes-from-v1)
|
|
58
|
+
17. [Best practices](#best-practices)
|
|
59
|
+
|
|
60
|
+
---
|
|
61
|
+
|
|
62
|
+
## Locator Registry overview
|
|
63
|
+
|
|
64
|
+
The v2 Locator Registry is a typed, fluent DSL for defining and resolving Playwright locators.
|
|
65
|
+
|
|
66
|
+
At a high level it provides:
|
|
67
|
+
|
|
68
|
+
- **Typed locator paths** (`"root.section.button"`) validated both at compile time and runtime.
|
|
69
|
+
- **Fluent registration** (`add(path).getByRole(...).filter(...).nth(...)`).
|
|
70
|
+
- **Two resolution modes**:
|
|
71
|
+
- `getLocator(path)` resolves only the terminal segment.
|
|
72
|
+
- `getNestedLocator(path)` resolves the full chain.
|
|
73
|
+
- **Clone-based query mutation** (`getLocatorSchema(path)`) for temporary overrides (`update`, `replace`, `remove`, `filter`, `nth`, `clearSteps`, `describe`) without mutating the registry.
|
|
74
|
+
|
|
75
|
+
You can use it directly via `createRegistryWithAccessors(page)` or through `PageObject` (`this.add`, `this.getLocator`, `this.getNestedLocator`, `this.getLocatorSchema`).
|
|
76
|
+
|
|
77
|
+
---
|
|
78
|
+
|
|
79
|
+
## Mental model
|
|
80
|
+
|
|
81
|
+
Think of the registry as:
|
|
82
|
+
|
|
83
|
+
1. A map of **path -> locator definition** (`getByRole`, `locator`, `frameLocator`, etc.).
|
|
84
|
+
2. A map of **path -> ordered steps** (`filter` / `nth`).
|
|
85
|
+
3. An optional **path description** (`describe`).
|
|
86
|
+
|
|
87
|
+
When resolving:
|
|
88
|
+
|
|
89
|
+
- `getLocator(path)` applies only the terminal definition + terminal steps.
|
|
90
|
+
- `getNestedLocator(path)` traverses the chain (`a`, `a.b`, `a.b.c`) and applies each registered segment in order.
|
|
91
|
+
|
|
92
|
+
When querying with `getLocatorSchema(path)`:
|
|
93
|
+
|
|
94
|
+
- A mutable clone is created for the chain.
|
|
95
|
+
- All updates are local to the clone.
|
|
96
|
+
- The underlying registry remains unchanged.
|
|
97
|
+
|
|
98
|
+
---
|
|
99
|
+
|
|
100
|
+
## Path types and validation
|
|
101
|
+
|
|
102
|
+
### Compile-time path constraints
|
|
103
|
+
|
|
104
|
+
Paths are generic string literal unions that are checked using v2 type utilities.
|
|
105
|
+
|
|
106
|
+
Rules:
|
|
107
|
+
|
|
108
|
+
- Path cannot be empty.
|
|
109
|
+
- Path cannot start or end with `.`.
|
|
110
|
+
- Path cannot contain consecutive dots (`..`).
|
|
111
|
+
- Path cannot contain Unicode whitespace characters.
|
|
112
|
+
|
|
113
|
+
```ts
|
|
114
|
+
type Paths =
|
|
115
|
+
| "main"
|
|
116
|
+
| "main.form@login"
|
|
117
|
+
| "main.form@login.input@username"
|
|
118
|
+
| "main.form@login.input@password"
|
|
119
|
+
| "main.button@login";
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
If invalid literals are included in the union, type errors are surfaced where the registry factory is instantiated.
|
|
123
|
+
|
|
124
|
+
### Runtime validation rules
|
|
125
|
+
|
|
126
|
+
Runtime validation applies the same structure checks:
|
|
127
|
+
|
|
128
|
+
- empty string
|
|
129
|
+
- leading dot
|
|
130
|
+
- trailing dot
|
|
131
|
+
- consecutive dots
|
|
132
|
+
- whitespace characters
|
|
133
|
+
|
|
134
|
+
This means even if a path reaches runtime as a plain string, it is still validated.
|
|
135
|
+
|
|
136
|
+
### Common path mistakes and errors
|
|
137
|
+
|
|
138
|
+
Typical runtime errors:
|
|
139
|
+
|
|
140
|
+
- `LocatorSchemaPath string cannot be empty`
|
|
141
|
+
- `LocatorSchemaPath string cannot start with a dot: .foo`
|
|
142
|
+
- `LocatorSchemaPath string cannot end with a dot: foo.`
|
|
143
|
+
- `LocatorSchemaPath string cannot contain consecutive dots: foo..bar`
|
|
144
|
+
- `LocatorSchemaPath string cannot contain whitespace chars: ...`
|
|
145
|
+
|
|
146
|
+
---
|
|
147
|
+
|
|
148
|
+
## Factory and accessors
|
|
149
|
+
|
|
150
|
+
### `createRegistryWithAccessors`
|
|
151
|
+
|
|
152
|
+
`createRegistryWithAccessors(page)` creates a registry instance plus bound helpers.
|
|
153
|
+
|
|
154
|
+
```ts
|
|
155
|
+
import { createRegistryWithAccessors } from "pomwright";
|
|
156
|
+
|
|
157
|
+
type Paths = "main" | "main.button";
|
|
158
|
+
|
|
159
|
+
const { registry, add, getLocator, getNestedLocator, getLocatorSchema } =
|
|
160
|
+
createRegistryWithAccessors<Paths>(page);
|
|
161
|
+
|
|
162
|
+
add("main").locator("main");
|
|
163
|
+
add("main.button").getByRole("button", { name: "Save" });
|
|
164
|
+
|
|
165
|
+
await getNestedLocator("main.button").click();
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
### Accessor types
|
|
169
|
+
|
|
170
|
+
The package exports accessor types so you can inject/wire them in custom classes or fixtures:
|
|
171
|
+
|
|
172
|
+
- `AddAccessor<Paths>`
|
|
173
|
+
- `GetLocatorAccessor<Paths>`
|
|
174
|
+
- `GetNestedLocatorAccessor<Paths>`
|
|
175
|
+
- `GetLocatorSchemaAccessor<Paths>`
|
|
176
|
+
|
|
177
|
+
```ts
|
|
178
|
+
import type {
|
|
179
|
+
AddAccessor,
|
|
180
|
+
GetLocatorAccessor,
|
|
181
|
+
GetNestedLocatorAccessor,
|
|
182
|
+
GetLocatorSchemaAccessor,
|
|
183
|
+
LocatorRegistry,
|
|
184
|
+
} from "pomwright";
|
|
185
|
+
|
|
186
|
+
class MyPage<Paths extends string> {
|
|
187
|
+
constructor(
|
|
188
|
+
public readonly registry: LocatorRegistry<Paths>,
|
|
189
|
+
public readonly add: AddAccessor<Paths>,
|
|
190
|
+
public readonly getLocator: GetLocatorAccessor<Paths>,
|
|
191
|
+
public readonly getNestedLocator: GetNestedLocatorAccessor<Paths>,
|
|
192
|
+
public readonly getLocatorSchema: GetLocatorSchemaAccessor<Paths>,
|
|
193
|
+
) {}
|
|
194
|
+
}
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
### Public `LocatorRegistry` vs internal registry
|
|
198
|
+
|
|
199
|
+
The **public** `LocatorRegistry` intentionally exposes only:
|
|
200
|
+
|
|
201
|
+
- `add`
|
|
202
|
+
- `createReusable`
|
|
203
|
+
- `getLocator`
|
|
204
|
+
- `getNestedLocator`
|
|
205
|
+
- `getLocatorSchema`
|
|
206
|
+
|
|
207
|
+
Internal lifecycle methods (`register`, `replace`, `get`, `unregister`) exist on the internal implementation and are intentionally not part of the public API.
|
|
208
|
+
|
|
209
|
+
---
|
|
210
|
+
|
|
211
|
+
## Registering locators with `add`
|
|
212
|
+
|
|
213
|
+
`add(path)` begins a registration chain.
|
|
214
|
+
|
|
215
|
+
```ts
|
|
216
|
+
registry.add("main").locator("main");
|
|
217
|
+
registry.add("main.button@login").getByRole("button", { name: "Login" });
|
|
218
|
+
registry.add("main.form@login").getByRole("form", { name: "Login" });
|
|
219
|
+
registry.add("main.form@login.input@username").getByLabel("Username");
|
|
220
|
+
registry.add("main.form@login.input@password").getByLabel("Password");
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
### All strategy methods
|
|
224
|
+
|
|
225
|
+
Each registration can choose exactly one strategy method:
|
|
226
|
+
|
|
227
|
+
- `getByRole(role, options?)`
|
|
228
|
+
- `getByText(text, options?)`
|
|
229
|
+
- `getByLabel(text, options?)`
|
|
230
|
+
- `getByPlaceholder(text, options?)`
|
|
231
|
+
- `getByAltText(text, options?)`
|
|
232
|
+
- `getByTitle(text, options?)`
|
|
233
|
+
- `locator(selector, options?)`
|
|
234
|
+
- `frameLocator(selector)`
|
|
235
|
+
- `getByTestId(testId)`
|
|
236
|
+
- `getById(id)`
|
|
237
|
+
|
|
238
|
+
### Post-definition chain methods
|
|
239
|
+
|
|
240
|
+
After a strategy is set, you can chain:
|
|
241
|
+
|
|
242
|
+
- `filter(filterDefinition)`
|
|
243
|
+
- `nth(index)` where index is `number | "first" | "last"`
|
|
244
|
+
- `describe(description)`
|
|
245
|
+
|
|
246
|
+
```ts
|
|
247
|
+
registry
|
|
248
|
+
.add("main.list.item")
|
|
249
|
+
.getByRole("listitem", { name: /Row/ })
|
|
250
|
+
.filter({ hasText: "Row" })
|
|
251
|
+
.nth("last")
|
|
252
|
+
.describe("Last matching row");
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
### One-strategy-per-registration rule
|
|
256
|
+
|
|
257
|
+
Without reuse, calling a second strategy in the same registration throws.
|
|
258
|
+
|
|
259
|
+
```ts
|
|
260
|
+
registry.add("main.button").getByRole("button");
|
|
261
|
+
// ❌ Later trying to set .locator(...) for same add-chain is invalid.
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
### Duplicate path behavior
|
|
265
|
+
|
|
266
|
+
A path can only be registered once. Attempting to register the same path again throws with details about existing vs attempted schema.
|
|
267
|
+
|
|
268
|
+
---
|
|
269
|
+
|
|
270
|
+
## `getById` deep dive
|
|
271
|
+
|
|
272
|
+
### String normalization
|
|
273
|
+
|
|
274
|
+
For string IDs, v2 normalizes:
|
|
275
|
+
|
|
276
|
+
- `"#login"` -> `"login"`
|
|
277
|
+
- `"id=login"` -> `"login"`
|
|
278
|
+
|
|
279
|
+
Then resolves as `locator('#login')` with CSS escaping.
|
|
280
|
+
|
|
281
|
+
### RegExp behavior and escaping
|
|
282
|
+
|
|
283
|
+
For RegExp IDs, v2 uses the regex source and resolves as a substring selector:
|
|
284
|
+
|
|
285
|
+
- `getById(/panel-/)` -> `locator('[id*="panel-"]')` (escaped)
|
|
286
|
+
|
|
287
|
+
This is **substring** matching of the regex source string in the `id` attribute, not runtime regex evaluation in the browser selector engine.
|
|
288
|
+
|
|
289
|
+
### Examples
|
|
290
|
+
|
|
291
|
+
```ts
|
|
292
|
+
registry.add("modal.close").getById("close-modal");
|
|
293
|
+
registry.add("modal.close2").getById("#close-modal");
|
|
294
|
+
registry.add("modal.dynamic").getById(/modal-/);
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
---
|
|
298
|
+
|
|
299
|
+
## `filter` deep dive
|
|
300
|
+
|
|
301
|
+
### Accepted `has`/`hasNot` reference forms
|
|
302
|
+
|
|
303
|
+
For `filter({ has })` and `filter({ hasNot })`, v2 accepts:
|
|
304
|
+
|
|
305
|
+
1. A Playwright `Locator`
|
|
306
|
+
2. A registry path string (e.g. `"main.section.heading"`)
|
|
307
|
+
|
|
308
|
+
Also supports standard Playwright `hasText` / `hasNotText` options.
|
|
309
|
+
|
|
310
|
+
> **Tip:** Because `filter` accepts Playwright `Locator` instances, you can also pass locators returned from
|
|
311
|
+
> `registry.getLocator(path)`, `registry.getNestedLocator(path)`, or
|
|
312
|
+
> `registry.getLocatorSchema(path)...getLocator()` / `getNestedLocator()` in addition to `page.locator(...)`
|
|
313
|
+
> and `page.getBy...(...)` calls.
|
|
314
|
+
|
|
315
|
+
When you pass a **path string**, the registry resolves that path directly by building a locator for the terminal
|
|
316
|
+
definition and its own recorded steps (`filter(...)`, `nth(...)`) only. Ancestor segments are **not** chained. If you
|
|
317
|
+
want ancestor chaining, pass a Playwright `Locator` built from `registry.getNestedLocator(path)` or from a query
|
|
318
|
+
builder `registry.getNestedLocator(path)...getNestedLocator()` which also resolves the chain.
|
|
319
|
+
|
|
320
|
+
Be careful to avoid **cyclic filter references** (for example, when `pathA` uses `filter({ has: "pathB" })` and `pathB`
|
|
321
|
+
eventually filters back to `pathA`). Cycles are detected and throw with:
|
|
322
|
+
`Detected cyclic filter reference while resolving "${context.rootPath}": "${path}".`
|
|
323
|
+
|
|
324
|
+
### Path examples
|
|
325
|
+
|
|
326
|
+
```ts
|
|
327
|
+
registry.add("main").locator("main");
|
|
328
|
+
registry.add("main.section").locator("section");
|
|
329
|
+
registry.add("main.section.heading").getByRole("heading", { level: 2 });
|
|
330
|
+
|
|
331
|
+
registry
|
|
332
|
+
.add("main.section.warning")
|
|
333
|
+
.locator(".warning")
|
|
334
|
+
.filter({ has: "main.section.heading" })
|
|
335
|
+
.filter({ hasNot: "main.section" })
|
|
336
|
+
.filter({ hasText: /Warning/i });
|
|
337
|
+
```
|
|
338
|
+
|
|
339
|
+
### Frame-locator restrictions in filters
|
|
340
|
+
|
|
341
|
+
Inline frame locator definitions are not supported as filter references. If you need to target a frame, resolve a
|
|
342
|
+
registry path so it yields the frame **owner locator** (the iframe element) or pass a Playwright
|
|
343
|
+
`page.frameLocator(...).owner()` locator to `has`/`hasNot`.
|
|
344
|
+
|
|
345
|
+
---
|
|
346
|
+
|
|
347
|
+
## Resolution APIs
|
|
348
|
+
|
|
349
|
+
### `getLocator(path)`
|
|
350
|
+
|
|
351
|
+
Resolves terminal-only:
|
|
352
|
+
|
|
353
|
+
```ts
|
|
354
|
+
registry.add("main.form.username").getByLabel("Username");
|
|
355
|
+
const terminal = registry.getLocator("main.form.username");
|
|
356
|
+
// Equivalent to direct getByLabel("Username") from page context.
|
|
357
|
+
```
|
|
358
|
+
|
|
359
|
+
### `getNestedLocator(path)`
|
|
360
|
+
|
|
361
|
+
Resolves the full registered chain:
|
|
362
|
+
|
|
363
|
+
```ts
|
|
364
|
+
registry.add("main").locator("main");
|
|
365
|
+
registry.add("main.form").getByRole("form", { name: "Login" });
|
|
366
|
+
registry.add("main.form.username").getByLabel("Username");
|
|
367
|
+
|
|
368
|
+
const nested = registry.getNestedLocator("main.form.username");
|
|
369
|
+
// locator("main").getByRole("form", { name: "Login" }).getByLabel("Username")
|
|
370
|
+
```
|
|
371
|
+
|
|
372
|
+
### Frame behavior: terminal vs non-terminal
|
|
373
|
+
|
|
374
|
+
If a segment is `frameLocator`:
|
|
375
|
+
|
|
376
|
+
- **Non-terminal frame segment**: resolution context enters frame for descendants.
|
|
377
|
+
- **Terminal frame segment**: resolves to the iframe **owner locator**, not the frame target.
|
|
378
|
+
|
|
379
|
+
```ts
|
|
380
|
+
registry.add("shell.frame@login").frameLocator("iframe#login");
|
|
381
|
+
registry.add("shell.frame@login.username").getByLabel("Username");
|
|
382
|
+
|
|
383
|
+
const frameOwner = registry.getNestedLocator("shell.frame@login");
|
|
384
|
+
const insideFrame = registry.getNestedLocator("shell.frame@login.username");
|
|
385
|
+
```
|
|
386
|
+
|
|
387
|
+
### Sparse chain behavior
|
|
388
|
+
|
|
389
|
+
During nested resolution, chain parts/sub-paths that are not registered are skipped, except for the terminal chain/path which throws if no locator definition is registered.
|
|
390
|
+
|
|
391
|
+
This enables partial/sparse registration patterns, but for readability and maintainability you should generally prefer explicit ancestor registration.
|
|
392
|
+
|
|
393
|
+
---
|
|
394
|
+
|
|
395
|
+
## Query builder: `getLocatorSchema(path)`
|
|
396
|
+
|
|
397
|
+
### Clone semantics (registry is not mutated)
|
|
398
|
+
|
|
399
|
+
`getLocatorSchema(path)` creates a mutable clone of the selected chain.
|
|
400
|
+
|
|
401
|
+
Any `filter`, `nth`, `clearSteps`, `update`, `replace`, `remove`, or `describe` call affects only the builder instance.
|
|
402
|
+
|
|
403
|
+
```ts
|
|
404
|
+
const builder = registry.getLocatorSchema("main.form.username");
|
|
405
|
+
const patched = builder.update().getByLabel("Username", { exact: true }).getNestedLocator();
|
|
406
|
+
|
|
407
|
+
// Registry remains unchanged.
|
|
408
|
+
```
|
|
409
|
+
|
|
410
|
+
### Sub-path scope rules
|
|
411
|
+
|
|
412
|
+
For a builder rooted at `"a.b.c"`, valid sub-paths are chain segments within that root (`"a"`, `"a.b"`, `"a.b.c"`) **if they exist in the builder clone**.
|
|
413
|
+
|
|
414
|
+
If a sub-path is not valid in that context, methods throw:
|
|
415
|
+
|
|
416
|
+
- `"<subPath>" is not a valid sub-path of "<rootPath>".`
|
|
417
|
+
|
|
418
|
+
### Method-by-method reference
|
|
419
|
+
|
|
420
|
+
On query builder:
|
|
421
|
+
|
|
422
|
+
- `filter(subPath?, filter)`
|
|
423
|
+
- `nth(subPath?, index)`
|
|
424
|
+
- `clearSteps(subPath?)`
|
|
425
|
+
- `describe(description)` (terminal path only)
|
|
426
|
+
- `update(subPath?)` -> returns update builder (PATCH semantics)
|
|
427
|
+
- `replace(subPath?)` -> returns update builder in replace mode (POST semantics)
|
|
428
|
+
- `remove(subPath?)`
|
|
429
|
+
- `getLocator()`
|
|
430
|
+
- `getNestedLocator()`
|
|
431
|
+
|
|
432
|
+
Examples:
|
|
433
|
+
|
|
434
|
+
```ts
|
|
435
|
+
const locator = registry
|
|
436
|
+
.getLocatorSchema("main.form.button")
|
|
437
|
+
.filter("main.form", { hasText: "Login" })
|
|
438
|
+
.nth("main.form.button", "first")
|
|
439
|
+
.getNestedLocator();
|
|
440
|
+
|
|
441
|
+
const noSteps = registry
|
|
442
|
+
.getLocatorSchema("main.form.button")
|
|
443
|
+
.clearSteps("main.form.button")
|
|
444
|
+
.getNestedLocator();
|
|
445
|
+
```
|
|
446
|
+
|
|
447
|
+
### Default sub-path behavior table
|
|
448
|
+
|
|
449
|
+
If `subPath` is omitted, the terminal path passed to `getLocatorSchema(path)` is used.
|
|
450
|
+
|
|
451
|
+
| Method | Optional subPath? | Omitted behavior |
|
|
452
|
+
| --- | --- | --- |
|
|
453
|
+
| `filter(subPath?, filter)` | Yes | Uses terminal path |
|
|
454
|
+
| `nth(subPath?, index)` | Yes | Uses terminal path |
|
|
455
|
+
| `clearSteps(subPath?)` | Yes | Uses terminal path |
|
|
456
|
+
| `update(subPath?)` | Yes | Uses terminal path |
|
|
457
|
+
| `replace(subPath?)` | Yes | Uses terminal path |
|
|
458
|
+
| `remove(subPath?)` | Yes | Uses terminal path |
|
|
459
|
+
| `describe(description)` | N/A | Always terminal path |
|
|
460
|
+
|
|
461
|
+
---
|
|
462
|
+
|
|
463
|
+
## `update` and `replace` semantics
|
|
464
|
+
|
|
465
|
+
### `update`: PATCH-style merge
|
|
466
|
+
|
|
467
|
+
`update(subPath?)` merges fields into the existing definition for that sub-path.
|
|
468
|
+
|
|
469
|
+
- Omitted required fields may be inherited from current/baseline definitions.
|
|
470
|
+
- Options are merged where applicable.
|
|
471
|
+
|
|
472
|
+
```ts
|
|
473
|
+
const updated = registry
|
|
474
|
+
.getLocatorSchema("main.button")
|
|
475
|
+
.update()
|
|
476
|
+
.getByRole({ name: "Sign in" })
|
|
477
|
+
.getNestedLocator();
|
|
478
|
+
```
|
|
479
|
+
|
|
480
|
+
### `replace`: POST-style overwrite
|
|
481
|
+
|
|
482
|
+
`replace(subPath?)` requires enough data to build a complete definition for the chosen strategy.
|
|
483
|
+
|
|
484
|
+
```ts
|
|
485
|
+
const replaced = registry
|
|
486
|
+
.getLocatorSchema("main.button")
|
|
487
|
+
.replace()
|
|
488
|
+
.locator("button.primary", { hasText: "Sign in" })
|
|
489
|
+
.getNestedLocator();
|
|
490
|
+
```
|
|
491
|
+
|
|
492
|
+
### Required fields and type switching
|
|
493
|
+
|
|
494
|
+
Important behavior:
|
|
495
|
+
|
|
496
|
+
- `update` can switch strategy type, but the target strategy must have required fields resolved via provided values or available baselines.
|
|
497
|
+
- `replace` requires the required field for the target strategy in the replacement call chain.
|
|
498
|
+
- `update` / `replace` only change definitions, not steps. Use `filter` / `nth` / `clearSteps` for steps.
|
|
499
|
+
|
|
500
|
+
---
|
|
501
|
+
|
|
502
|
+
## `remove` and tombstones
|
|
503
|
+
|
|
504
|
+
### Non-terminal removal
|
|
505
|
+
|
|
506
|
+
Removing an ancestor sub-path soft-deletes that segment in the builder clone. During nested resolution, removed non-terminal segments are skipped.
|
|
507
|
+
|
|
508
|
+
### Terminal removal behavior
|
|
509
|
+
|
|
510
|
+
If terminal segment is removed and not restored, resolving throws (no schema for terminal path).
|
|
511
|
+
|
|
512
|
+
```ts
|
|
513
|
+
const builder = registry.getLocatorSchema("main.form.username").remove();
|
|
514
|
+
// builder.getNestedLocator() => throws
|
|
515
|
+
```
|
|
516
|
+
|
|
517
|
+
### Rehydrating removed segments
|
|
518
|
+
|
|
519
|
+
A removed segment can be repopulated within the same builder chain via `update` or `replace`, then resolved successfully.
|
|
520
|
+
|
|
521
|
+
```ts
|
|
522
|
+
const locator = registry
|
|
523
|
+
.getLocatorSchema("main.form.username")
|
|
524
|
+
.remove()
|
|
525
|
+
.replace()
|
|
526
|
+
.getByLabel("Email")
|
|
527
|
+
.getNestedLocator();
|
|
528
|
+
```
|
|
529
|
+
|
|
530
|
+
---
|
|
531
|
+
|
|
532
|
+
## Reusable locators: `createReusable`
|
|
533
|
+
|
|
534
|
+
### Seed creation
|
|
535
|
+
|
|
536
|
+
`createReusable` provides the same strategy entry methods as `add`.
|
|
537
|
+
|
|
538
|
+
```ts
|
|
539
|
+
const h2 = registry.createReusable.getByRole("heading", { level: 2 }).filter({ hasText: /Summary/ });
|
|
540
|
+
const firstRow = registry.createReusable.locator("tr").nth(0).describe("First row");
|
|
541
|
+
```
|
|
542
|
+
|
|
543
|
+
### Reuse by object seed
|
|
544
|
+
|
|
545
|
+
Pass a reusable seed to `add` via `{ reuse: seed }`.
|
|
546
|
+
|
|
547
|
+
```ts
|
|
548
|
+
registry.add("card.title", { reuse: h2 });
|
|
549
|
+
registry.add("card.title@first", { reuse: h2 }).nth(0);
|
|
550
|
+
```
|
|
551
|
+
|
|
552
|
+
### Reuse by existing path string
|
|
553
|
+
|
|
554
|
+
Pass a previously registered path to clone that path definition/steps/description as-is.
|
|
555
|
+
|
|
556
|
+
```ts
|
|
557
|
+
registry.add("errors.invalidPassword").getByText("Invalid password");
|
|
558
|
+
registry.add("main.form.error@invalidPassword", { reuse: "errors.invalidPassword" });
|
|
559
|
+
```
|
|
560
|
+
|
|
561
|
+
Note: path-based reuse registers immediately and does not return a chainable builder.
|
|
562
|
+
|
|
563
|
+
### Reuse patch rules and limitations
|
|
564
|
+
|
|
565
|
+
With object-seed reuse:
|
|
566
|
+
|
|
567
|
+
- Seeded definition is persisted first.
|
|
568
|
+
- You may provide **one matching-strategy override** (same discriminant/type).
|
|
569
|
+
- Mismatched strategy overrides throw.
|
|
570
|
+
- More than one strategy override in that chain throws.
|
|
571
|
+
- `filter` / `nth` / `describe` may still be chained.
|
|
572
|
+
|
|
573
|
+
```ts
|
|
574
|
+
const seed = registry.createReusable.getByRole("heading", { level: 2 });
|
|
575
|
+
|
|
576
|
+
registry.add("heading.summary", { reuse: seed }).getByRole({ name: "Summary" }); // ✅
|
|
577
|
+
|
|
578
|
+
// ❌ mismatched strategy, throws at runtime and compile-time should also guide against this
|
|
579
|
+
// registry.add("heading.bad", { reuse: seed }).locator("h2");
|
|
580
|
+
```
|
|
581
|
+
|
|
582
|
+
### Seed immutability
|
|
583
|
+
|
|
584
|
+
Using a seed in multiple `add(..., { reuse: seed })` chains does not mutate the seed object itself.
|
|
585
|
+
|
|
586
|
+
---
|
|
587
|
+
|
|
588
|
+
## `describe` semantics
|
|
589
|
+
|
|
590
|
+
### Registry-level descriptions
|
|
591
|
+
|
|
592
|
+
Calling `.describe(...)` on `add` stores description with the path definition and applies it to resolved terminal locator.
|
|
593
|
+
|
|
594
|
+
```ts
|
|
595
|
+
registry
|
|
596
|
+
.add("main.submit")
|
|
597
|
+
.getByRole("button", { name: "Submit" })
|
|
598
|
+
.describe("Primary submit button");
|
|
599
|
+
```
|
|
600
|
+
|
|
601
|
+
### Query-level overrides
|
|
602
|
+
|
|
603
|
+
Calling `.describe(...)` on query builder overrides description only for that builder resolution and does not mutate the stored registry description.
|
|
604
|
+
|
|
605
|
+
```ts
|
|
606
|
+
const override = registry
|
|
607
|
+
.getLocatorSchema("main.submit")
|
|
608
|
+
.describe("Temporary override")
|
|
609
|
+
.getLocator();
|
|
610
|
+
```
|
|
611
|
+
|
|
612
|
+
---
|
|
613
|
+
|
|
614
|
+
## Error reference and troubleshooting
|
|
615
|
+
|
|
616
|
+
Common errors and what they usually mean:
|
|
617
|
+
|
|
618
|
+
- `No locator schema registered for path "...".`
|
|
619
|
+
- Path was never registered, was removed on builder, or is terminal tombstone.
|
|
620
|
+
- `"..." is not a valid sub-path of "...".`
|
|
621
|
+
- Query method targeted a path outside the builder’s chain context.
|
|
622
|
+
- `A locator schema with the path "..." already exists.`
|
|
623
|
+
- Duplicate registration for same path.
|
|
624
|
+
- `A locator definition must be provided before applying filters or indices for "...".`
|
|
625
|
+
- `filter`/`nth` called before strategy during registration.
|
|
626
|
+
- `A locator definition for "..." has already been provided; only one locator type can be set for a registration.`
|
|
627
|
+
- Multiple strategy calls on a non-reuse registration.
|
|
628
|
+
- `The locator definition for "..." must use the "..." strategy when reusing a locator.`
|
|
629
|
+
- Mismatched strategy override while using `{ reuse: seed }`.
|
|
630
|
+
- `A locator definition for "..." was already provided from reuse; only one matching override is allowed.`
|
|
631
|
+
- More than one seeded strategy override attempted.
|
|
632
|
+
- `Frame locators cannot be used as filter locators.`
|
|
633
|
+
- Inline filter `has`/`hasNot` passed a frame locator definition.
|
|
634
|
+
- `Detected cyclic filter reference while resolving "...": "...".`
|
|
635
|
+
- A `has`/`hasNot` path reference re-entered an active filter resolution; break/fix the loop or pass a resolved locator.
|
|
636
|
+
|
|
637
|
+
Troubleshooting checklist:
|
|
638
|
+
|
|
639
|
+
1. Confirm path literal is valid and registered.
|
|
640
|
+
2. Confirm query sub-path is in the same chain root.
|
|
641
|
+
3. Confirm reuse override strategy matches seed type.
|
|
642
|
+
4. Confirm terminal path still exists if `remove()` was called.
|
|
643
|
+
|
|
644
|
+
---
|
|
645
|
+
|
|
646
|
+
## Tested behavioral guarantees
|
|
647
|
+
|
|
648
|
+
The v2 integration suite (`intTestV2`) verifies Locator Registry behavior in depth, including:
|
|
649
|
+
|
|
650
|
+
- path validation (compile-time + runtime)
|
|
651
|
+
- sub-path validation
|
|
652
|
+
- all registration strategies
|
|
653
|
+
- `filter` behavior, including `has`/`hasNot` variants
|
|
654
|
+
- `nth` chaining and ordering
|
|
655
|
+
- `describe` behavior
|
|
656
|
+
- `update` / `replace` / `remove`
|
|
657
|
+
- `clearSteps`
|
|
658
|
+
- reusable locators and reuse constraints
|
|
659
|
+
- frame locator behavior
|
|
660
|
+
|
|
661
|
+
For implementation-level examples, see the Locator Registry tests under `intTestV2/tests/locatorRegistry`.
|
|
662
|
+
|
|
663
|
+
---
|
|
664
|
+
|
|
665
|
+
## Migration notes from v1
|
|
666
|
+
|
|
667
|
+
Key v1 -> v2 Locator Registry differences:
|
|
668
|
+
|
|
669
|
+
- v2 uses fluent registration (`add(...).getBy...`) instead of v1 object schemas.
|
|
670
|
+
- `getLocator` / `getNestedLocator` are synchronous in v2.
|
|
671
|
+
- v1 index maps for nested lookup are replaced by query-builder `.nth(...)`.
|
|
672
|
+
- v1 `addFilter` style maps to v2 `.filter(...)`.
|
|
673
|
+
- v2 query builder adds `replace`, `remove`, `clearSteps`, and `describe`.
|
|
674
|
+
- frame terminal behavior is explicit (terminal frame resolves to owner locator).
|
|
675
|
+
|
|
676
|
+
See migration docs in `docs/v1-to-v2-migration` for side-by-side mappings and staged migration guidance.
|
|
677
|
+
|
|
678
|
+
---
|
|
679
|
+
|
|
680
|
+
## Best practices
|
|
681
|
+
|
|
682
|
+
1. **Prefer explicit ancestor registration**
|
|
683
|
+
- Sparse chains are supported, but explicit ancestors improve readability.
|
|
684
|
+
2. **Use descriptive path names**
|
|
685
|
+
- Favor semantic segments (`form@login.input@username`) over anonymous names.
|
|
686
|
+
3. **Use `getLocatorSchema` for temporary overrides**
|
|
687
|
+
- Keep registry definitions stable; mutate clones for scenario-specific behavior.
|
|
688
|
+
4. **Use reusable seeds for repeated patterns**
|
|
689
|
+
- Centralize repeated strategy + steps, then reuse with a single matching override.
|
|
690
|
+
5. **Keep filters intentional**
|
|
691
|
+
- Be explicit with `has`/`hasNot` references; prefer path-based references for readability.
|
|
692
|
+
6. **Document intentional strategy switching in query updates**
|
|
693
|
+
- Type switches are powerful; use them deliberately and clearly in test code.
|