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.
Files changed (117) hide show
  1. package/AGENTS.md +37 -0
  2. package/CHANGELOG.md +179 -0
  3. package/README.md +316 -34
  4. package/dist/index.d.mts +1052 -30
  5. package/dist/index.d.ts +1052 -30
  6. package/dist/index.js +2263 -65
  7. package/dist/index.mjs +2260 -67
  8. package/docs/v1-to-v2-migration/bridge-migration-guide.md +159 -0
  9. package/docs/v1-to-v2-migration/direct-migration-guide.md +238 -0
  10. package/docs/v1-to-v2-migration/v1-to-v2-comparison.md +547 -0
  11. package/docs/v2/PageObject.md +293 -0
  12. package/docs/v2/composing-locator-modules.md +93 -0
  13. package/docs/v2/locator-registry.md +693 -0
  14. package/docs/v2/logging.md +168 -0
  15. package/docs/v2/overview.md +515 -0
  16. package/docs/v2/session-storage.md +160 -0
  17. package/index.ts +61 -9
  18. package/intTestV2/.env +0 -0
  19. package/intTestV2/fixtures/testApp.fixtures.ts +43 -0
  20. package/intTestV2/package.json +22 -0
  21. package/intTestV2/page-object-models/testApp/pages/iframe/iframe.locatorSchema.ts +24 -0
  22. package/intTestV2/page-object-models/testApp/pages/iframe/iframe.page.ts +17 -0
  23. package/intTestV2/page-object-models/testApp/pages/testPage.locatorSchema.ts +32 -0
  24. package/intTestV2/page-object-models/testApp/pages/testPage.page.ts +119 -0
  25. package/intTestV2/page-object-models/testApp/pages/testPath/[color]/color.locatorSchema.ts +29 -0
  26. package/intTestV2/page-object-models/testApp/pages/testPath/[color]/color.page.ts +48 -0
  27. package/intTestV2/page-object-models/testApp/pages/testPath/testPath.locatorSchema.ts +9 -0
  28. package/intTestV2/page-object-models/testApp/pages/testPath/testPath.page.ts +23 -0
  29. package/intTestV2/page-object-models/testApp/pages/testfilters/testfilters.locatorSchema.ts +114 -0
  30. package/intTestV2/page-object-models/testApp/pages/testfilters/testfilters.page.ts +23 -0
  31. package/intTestV2/page-object-models/testApp/testApp.base.ts +20 -0
  32. package/intTestV2/playwright.config.ts +54 -0
  33. package/intTestV2/server.js +216 -0
  34. package/intTestV2/test-data/staticPage/index.html +280 -0
  35. package/intTestV2/test-data/staticPage/w3images/avatar2.png +0 -0
  36. package/intTestV2/test-data/staticPage/w3images/avatar3.png +0 -0
  37. package/intTestV2/test-data/staticPage/w3images/avatar5.png +0 -0
  38. package/intTestV2/test-data/staticPage/w3images/avatar6.png +0 -0
  39. package/intTestV2/test-data/staticPage/w3images/forest.jpg +0 -0
  40. package/intTestV2/test-data/staticPage/w3images/lights.jpg +0 -0
  41. package/intTestV2/test-data/staticPage/w3images/mountains.jpg +0 -0
  42. package/intTestV2/test-data/staticPage/w3images/nature.jpg +0 -0
  43. package/intTestV2/test-data/staticPage/w3images/snow.jpg +0 -0
  44. package/intTestV2/tests/locatorRegistry/add/add.describe.spec.ts +54 -0
  45. package/intTestV2/tests/locatorRegistry/add/add.filter.spec.ts +143 -0
  46. package/intTestV2/tests/locatorRegistry/add/add.frameLocator.spec.ts +23 -0
  47. package/intTestV2/tests/locatorRegistry/add/add.getByAltText.spec.ts +23 -0
  48. package/intTestV2/tests/locatorRegistry/add/add.getById.spec.ts +45 -0
  49. package/intTestV2/tests/locatorRegistry/add/add.getByLabel.spec.ts +23 -0
  50. package/intTestV2/tests/locatorRegistry/add/add.getByPlaceholder.spec.ts +23 -0
  51. package/intTestV2/tests/locatorRegistry/add/add.getByRole.spec.ts +23 -0
  52. package/intTestV2/tests/locatorRegistry/add/add.getByTestId.spec.ts +23 -0
  53. package/intTestV2/tests/locatorRegistry/add/add.getByText.spec.ts +23 -0
  54. package/intTestV2/tests/locatorRegistry/add/add.getByTitle.spec.ts +23 -0
  55. package/intTestV2/tests/locatorRegistry/add/add.locator.spec.ts +23 -0
  56. package/intTestV2/tests/locatorRegistry/add/add.reuseExisting.spec.ts +66 -0
  57. package/intTestV2/tests/locatorRegistry/add/add.reuseReusable.spec.ts +311 -0
  58. package/intTestV2/tests/locatorRegistry/add/add.spec.ts +159 -0
  59. package/intTestV2/tests/locatorRegistry/filter.cycle.spec.ts +39 -0
  60. package/intTestV2/tests/locatorRegistry/getLocator/getLocator.spec.ts +253 -0
  61. package/intTestV2/tests/locatorRegistry/getLocatorSchema/getLocatorSchema.clearSteps.spec.ts +105 -0
  62. package/intTestV2/tests/locatorRegistry/getLocatorSchema/getLocatorSchema.describe.spec.ts +23 -0
  63. package/intTestV2/tests/locatorRegistry/getLocatorSchema/getLocatorSchema.filter.spec.ts +368 -0
  64. package/intTestV2/tests/locatorRegistry/getLocatorSchema/getLocatorSchema.getLocator.spec.ts +56 -0
  65. package/intTestV2/tests/locatorRegistry/getLocatorSchema/getLocatorSchema.getNestedLocator.spec.ts +175 -0
  66. package/intTestV2/tests/locatorRegistry/getLocatorSchema/getLocatorSchema.nth.spec.ts +60 -0
  67. package/intTestV2/tests/locatorRegistry/getLocatorSchema/getLocatorSchema.remove.spec.ts +32 -0
  68. package/intTestV2/tests/locatorRegistry/getLocatorSchema/getLocatorSchema.replace.spec.ts +24 -0
  69. package/intTestV2/tests/locatorRegistry/getLocatorSchema/getLocatorSchema.spec.ts +110 -0
  70. package/intTestV2/tests/locatorRegistry/getLocatorSchema/getLocatorSchema.update.spec.ts +322 -0
  71. package/intTestV2/tests/locatorRegistry/getNestedLocator/getNestedLocator.spec.ts +412 -0
  72. package/intTestV2/tests/locatorRegistry/registry/registry.binding.spec.ts +50 -0
  73. package/intTestV2/tests/locatorRegistry/validation/validation.locatorSchemaPath.spec.ts +115 -0
  74. package/intTestV2/tests/locatorRegistry/validation/validation.sub-path.spec.ts +45 -0
  75. package/intTestV2/tests/step/step.spec.ts +49 -0
  76. package/intTestV2/tests/testApp/color.spec.ts +15 -0
  77. package/intTestV2/tests/testApp/iframe.spec.ts +57 -0
  78. package/intTestV2/tests/testApp/testFilters.spec.ts +24 -0
  79. package/intTestV2/tests/testApp/testPage.spec.ts +161 -0
  80. package/intTestV2/tests/testApp/testPath.spec.ts +18 -0
  81. package/pack-build.sh +11 -0
  82. package/pack-test-v2.sh +36 -0
  83. package/package.json +10 -3
  84. package/playwright.base.ts +42 -0
  85. package/skills/README.md +56 -0
  86. package/skills/pomwright-v1-5-bridge-migration/SKILL.md +40 -0
  87. package/skills/pomwright-v1-5-bridge-migration/references/call-site-migration.md +178 -0
  88. package/skills/pomwright-v1-5-bridge-migration/references/schema-translation.md +183 -0
  89. package/skills/pomwright-v2-migration/SKILL.md +63 -0
  90. package/skills/pomwright-v2-migration/references/call-site-migration.md +265 -0
  91. package/skills/pomwright-v2-migration/references/class-migration.md +266 -0
  92. package/skills/pomwright-v2-migration/references/fixture-and-helpers.md +423 -0
  93. package/skills/pomwright-v2-migration/references/locator-registration.md +344 -0
  94. package/srcV2/fixture/base.fixtures.ts +23 -0
  95. package/srcV2/helpers/navigation.ts +153 -0
  96. package/srcV2/helpers/playwrightReportLogger.ts +196 -0
  97. package/srcV2/helpers/sessionStorage.ts +251 -0
  98. package/srcV2/helpers/stepDecorator.ts +106 -0
  99. package/srcV2/locators/index.ts +15 -0
  100. package/srcV2/locators/locatorQueryBuilder.ts +427 -0
  101. package/srcV2/locators/locatorRegistrationBuilder.ts +558 -0
  102. package/srcV2/locators/locatorRegistry.ts +541 -0
  103. package/srcV2/locators/locatorUpdateBuilder.ts +602 -0
  104. package/srcV2/locators/reusableLocatorBuilder.ts +200 -0
  105. package/srcV2/locators/types.ts +256 -0
  106. package/srcV2/locators/utils.ts +309 -0
  107. package/srcV2/locators/v1SchemaTranslator.ts +178 -0
  108. package/srcV2/pageObject.ts +105 -0
  109. /package/docs/{BaseApi-explanation.md → v1/BaseApi-explanation.md} +0 -0
  110. /package/docs/{BasePage-explanation.md → v1/BasePage-explanation.md} +0 -0
  111. /package/docs/{LocatorSchema-explanation.md → v1/LocatorSchema-explanation.md} +0 -0
  112. /package/docs/{LocatorSchemaPath-explanation.md → v1/LocatorSchemaPath-explanation.md} +0 -0
  113. /package/docs/{PlaywrightReportLogger-explanation.md → v1/PlaywrightReportLogger-explanation.md} +0 -0
  114. /package/docs/{get-locator-methods-explanation.md → v1/get-locator-methods-explanation.md} +0 -0
  115. /package/docs/{intro-to-using-pomwright.md → v1/intro-to-using-pomwright.md} +0 -0
  116. /package/docs/{sessionStorage-methods-explanation.md → v1/sessionStorage-methods-explanation.md} +0 -0
  117. /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.