pomwright 1.5.0 → 1.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,19 @@
1
1
  # pomwright
2
2
 
3
+ ## 1.5.1
4
+
5
+ ### Patch Changes
6
+
7
+ - [#40](https://github.com/DyHex/POMWright/pull/40) [`c8232d2`](https://github.com/DyHex/POMWright/commit/c8232d21e2d7f0149042f4844037c67289d26068) Thanks [@DyHex](https://github.com/DyHex)! - # Summary
8
+
9
+ This PR focuses on preventative quality improvements to v2 locator-registry typing and internal safety.
10
+
11
+ ## What changed
12
+
13
+ - Refined compile-time diagnostics for invalid locator schema paths so errors are clearer and more actionable at the literal argument level.
14
+ - Hardened internal registry retrieval behavior (`get` / `getIfExists`) for safer, more predictable handling of missing paths and record cloning semantics.
15
+ - Added both compile-time and runtime guards to reject path-based reuse when the source path equals the target registration path `registry.add(path, { reuse: path })`.
16
+
3
17
  ## 1.5.0
4
18
 
5
19
  ### Minor Changes
package/dist/index.d.mts CHANGED
@@ -1232,6 +1232,11 @@ declare class ReusableLocatorFactory<LocatorSchemaPathType extends string> {
1232
1232
  private create;
1233
1233
  }
1234
1234
 
1235
+ type PathArgument<Paths extends string, Path extends Paths> = LocatorSchemaPathFormat<Path> extends Path ? Path : LocatorSchemaPathFormat<Path>;
1236
+ type InvalidReusePath<Path extends string> = [
1237
+ `Invalid reuse path, reuse path cannot be the same as registration path: ${Path}`
1238
+ ];
1239
+ type ReusePathArgument<Paths extends string, Path extends Paths, ReusePath extends Paths> = Exclude<ReusePath, Path> extends never ? InvalidReusePath<Path> : PathArgument<Paths, ReusePath>;
1235
1240
  declare class LocatorRegistryInternal<LocatorSchemaPathType extends string> {
1236
1241
  private readonly page;
1237
1242
  private readonly schemas;
@@ -1267,13 +1272,11 @@ declare class LocatorRegistryInternal<LocatorSchemaPathType extends string> {
1267
1272
  * .nth("last");
1268
1273
  * ```
1269
1274
  */
1270
- add(path: RegistryPath<LocatorSchemaPathType>): LocatorRegistrationPreDefinitionBuilder<LocatorSchemaPathType, RegistryPath<LocatorSchemaPathType>, false>;
1271
- add(path: RegistryPath<LocatorSchemaPathType>, options: {
1272
- reuse: RegistryPath<LocatorSchemaPathType>;
1273
- }): void;
1274
- add<Reuse extends ReusableLocator<LocatorSchemaPathType, RegistryPath<LocatorSchemaPathType>, LocatorStrategyDefinition["type"]>>(path: RegistryPath<LocatorSchemaPathType>, options: {
1275
- reuse: Reuse;
1276
- }): LocatorRegistrationSeededBuilderForType<LocatorSchemaPathType, RegistryPath<LocatorSchemaPathType>, Reuse["type"]>;
1275
+ add<Path extends LocatorSchemaPathType, Reuse extends LocatorSchemaPathType | ReusableLocator<LocatorSchemaPathType, RegistryPath<LocatorSchemaPathType>, LocatorStrategyDefinition["type"]> | undefined = undefined>(path: PathArgument<LocatorSchemaPathType, Path>, ...args: Reuse extends undefined ? [] : [
1276
+ options: {
1277
+ reuse: Reuse extends LocatorSchemaPathType ? ReusePathArgument<LocatorSchemaPathType, Path, Reuse> : Extract<Reuse, ReusableLocator<LocatorSchemaPathType, RegistryPath<LocatorSchemaPathType>, LocatorStrategyDefinition["type"]>>;
1278
+ }
1279
+ ]): Reuse extends undefined ? LocatorRegistrationPreDefinitionBuilder<LocatorSchemaPathType, RegistryPath<LocatorSchemaPathType>, false> : Reuse extends LocatorSchemaPathType ? void : LocatorRegistrationSeededBuilderForType<LocatorSchemaPathType, RegistryPath<LocatorSchemaPathType>, Extract<Reuse, ReusableLocator<LocatorSchemaPathType, RegistryPath<LocatorSchemaPathType>, LocatorStrategyDefinition["type"]>>["type"]>;
1277
1280
  register(path: RegistryPath<LocatorSchemaPathType>, record: LocatorSchemaRecord<LocatorSchemaPathType, RegistryPath<LocatorSchemaPathType>>): void;
1278
1281
  replace(path: RegistryPath<LocatorSchemaPathType>, record: LocatorSchemaRecord<LocatorSchemaPathType, RegistryPath<LocatorSchemaPathType>>): void;
1279
1282
  get(path: RegistryPath<LocatorSchemaPathType>): LocatorSchemaRecord<LocatorSchemaPathType, RegistryPath<LocatorSchemaPathType>>;
@@ -1297,7 +1300,7 @@ declare class LocatorRegistryInternal<LocatorSchemaPathType extends string> {
1297
1300
  * const locator = builder.getNestedLocator();
1298
1301
  * ```
1299
1302
  */
1300
- getLocatorSchema<Path extends RegistryPath<LocatorSchemaPathType>>(path: Path): LocatorQueryBuilderPublic<LocatorSchemaPathType, Path>;
1303
+ getLocatorSchema<Path extends LocatorSchemaPathType>(path: PathArgument<LocatorSchemaPathType, Path>): LocatorQueryBuilderPublic<LocatorSchemaPathType, Extract<Path, RegistryPath<LocatorSchemaPathType>>>;
1301
1304
  /**
1302
1305
  * Resolves the Playwright {@link Locator} for the provided path, applying only the terminal
1303
1306
  * definition and its recorded steps (no ancestor chaining). Throws if the path is not registered.
@@ -1308,7 +1311,7 @@ declare class LocatorRegistryInternal<LocatorSchemaPathType extends string> {
1308
1311
  * await button.click();
1309
1312
  * ```
1310
1313
  */
1311
- getLocator<Path extends RegistryPath<LocatorSchemaPathType>>(path: Path): Locator;
1314
+ getLocator<Path extends LocatorSchemaPathType>(path: PathArgument<LocatorSchemaPathType, Path>): Locator;
1312
1315
  /**
1313
1316
  * Resolves a chained Playwright {@link Locator} for a nested path, traversing each registered
1314
1317
  * segment and applying their steps in order. Throws if any segment in the chain is missing.
@@ -1319,7 +1322,7 @@ declare class LocatorRegistryInternal<LocatorSchemaPathType extends string> {
1319
1322
  * await expect(nested).toBeVisible();
1320
1323
  * ```
1321
1324
  */
1322
- getNestedLocator<Path extends RegistryPath<LocatorSchemaPathType>>(path: Path): Locator;
1325
+ getNestedLocator<Path extends LocatorSchemaPathType>(path: PathArgument<LocatorSchemaPathType, Path>): Locator;
1323
1326
  buildLocatorChain(path: RegistryPath<LocatorSchemaPathType>, definitions: Map<string, LocatorStrategyDefinition>, steps: Map<string, LocatorStep<RegistryPath<LocatorSchemaPathType>, RegistryPath<LocatorSchemaPathType>>[]>, tombstones?: Set<string>, terminalDescription?: LocatorDescription, context?: {
1324
1327
  rootPath: RegistryPath<LocatorSchemaPathType>;
1325
1328
  resolvingPaths: Set<string>;
@@ -1334,10 +1337,10 @@ declare class LocatorRegistryInternal<LocatorSchemaPathType extends string> {
1334
1337
  }[];
1335
1338
  };
1336
1339
  }
1337
- type GetLocatorAccessor<LocatorSchemaPathType extends string> = <Path extends RegistryPath<LocatorSchemaPathType>>(path: Path) => Locator;
1340
+ type GetLocatorAccessor<LocatorSchemaPathType extends string> = <Path extends LocatorSchemaPathType>(path: LocatorSchemaPathFormat<Path> extends Path ? Path : LocatorSchemaPathFormat<Path>) => Locator;
1338
1341
  type AddAccessor<LocatorSchemaPathType extends string> = LocatorRegistryInternal<LocatorSchemaPathType>["add"];
1339
- type GetLocatorSchemaAccessor<LocatorSchemaPathType extends string> = <Path extends RegistryPath<LocatorSchemaPathType>>(path: Path) => LocatorQueryBuilderPublic<LocatorSchemaPathType, Path>;
1340
- type GetNestedLocatorAccessor<LocatorSchemaPathType extends string> = <Path extends RegistryPath<LocatorSchemaPathType>>(path: Path) => Locator;
1342
+ type GetLocatorSchemaAccessor<LocatorSchemaPathType extends string> = <Path extends LocatorSchemaPathType>(path: LocatorSchemaPathFormat<Path> extends Path ? Path : LocatorSchemaPathFormat<Path>) => LocatorQueryBuilderPublic<LocatorSchemaPathType, Extract<Path, RegistryPath<LocatorSchemaPathType>>>;
1343
+ type GetNestedLocatorAccessor<LocatorSchemaPathType extends string> = <Path extends LocatorSchemaPathType>(path: LocatorSchemaPathFormat<Path> extends Path ? Path : LocatorSchemaPathFormat<Path>) => Locator;
1341
1344
  type LocatorRegistry<LocatorSchemaPathType extends string> = Pick<LocatorRegistryInternal<LocatorSchemaPathType>, "add" | "createReusable" | "getLocator" | "getLocatorSchema" | "getNestedLocator">;
1342
1345
  /**
1343
1346
  * Creates a v2 locator registry bound to a Playwright {@link Page} and returns the registry plus
@@ -1359,15 +1362,9 @@ type LocatorRegistry<LocatorSchemaPathType extends string> = Pick<LocatorRegistr
1359
1362
  */
1360
1363
  declare const createRegistryWithAccessors: <Paths extends string>(page: Page) => {
1361
1364
  readonly registry: LocatorRegistry<Paths>;
1362
- readonly add: {
1363
- (path: RegistryPath<Paths>): LocatorRegistrationPreDefinitionBuilder<Paths, RegistryPath<Paths>, false>;
1364
- (path: RegistryPath<Paths>, options: {
1365
- reuse: RegistryPath<Paths>;
1366
- }): void;
1367
- <Reuse extends ReusableLocator<Paths, RegistryPath<Paths>, "role" | "text" | "label" | "placeholder" | "altText" | "title" | "locator" | "frameLocator" | "testId" | "id">>(path: RegistryPath<Paths>, options: {
1368
- reuse: Reuse;
1369
- }): LocatorRegistrationSeededBuilderForType<Paths, RegistryPath<Paths>, Reuse["type"]>;
1370
- };
1365
+ readonly add: <Path extends Paths, Reuse extends Paths | ReusableLocator<Paths, RegistryPath<Paths>, "role" | "text" | "label" | "placeholder" | "altText" | "title" | "locator" | "frameLocator" | "testId" | "id"> | undefined = undefined>(path: PathArgument<Paths, Path>, ...args: Reuse extends undefined ? [] : [options: {
1366
+ reuse: Reuse extends Paths ? ReusePathArgument<Paths, Path, Reuse> : Extract<Reuse, ReusableLocator<Paths, RegistryPath<Paths>, "role" | "text" | "label" | "placeholder" | "altText" | "title" | "locator" | "frameLocator" | "testId" | "id">>;
1367
+ }]) => Reuse extends undefined ? LocatorRegistrationPreDefinitionBuilder<Paths, RegistryPath<Paths>, false> : Reuse extends Paths ? void : LocatorRegistrationSeededBuilderForType<Paths, RegistryPath<Paths>, Extract<Reuse, ReusableLocator<Paths, RegistryPath<Paths>, "role" | "text" | "label" | "placeholder" | "altText" | "title" | "locator" | "frameLocator" | "testId" | "id">>["type"]>;
1371
1368
  readonly getLocator: GetLocatorAccessor<Paths>;
1372
1369
  readonly getNestedLocator: GetNestedLocatorAccessor<Paths>;
1373
1370
  readonly getLocatorSchema: GetLocatorSchemaAccessor<Paths>;
package/dist/index.d.ts CHANGED
@@ -1232,6 +1232,11 @@ declare class ReusableLocatorFactory<LocatorSchemaPathType extends string> {
1232
1232
  private create;
1233
1233
  }
1234
1234
 
1235
+ type PathArgument<Paths extends string, Path extends Paths> = LocatorSchemaPathFormat<Path> extends Path ? Path : LocatorSchemaPathFormat<Path>;
1236
+ type InvalidReusePath<Path extends string> = [
1237
+ `Invalid reuse path, reuse path cannot be the same as registration path: ${Path}`
1238
+ ];
1239
+ type ReusePathArgument<Paths extends string, Path extends Paths, ReusePath extends Paths> = Exclude<ReusePath, Path> extends never ? InvalidReusePath<Path> : PathArgument<Paths, ReusePath>;
1235
1240
  declare class LocatorRegistryInternal<LocatorSchemaPathType extends string> {
1236
1241
  private readonly page;
1237
1242
  private readonly schemas;
@@ -1267,13 +1272,11 @@ declare class LocatorRegistryInternal<LocatorSchemaPathType extends string> {
1267
1272
  * .nth("last");
1268
1273
  * ```
1269
1274
  */
1270
- add(path: RegistryPath<LocatorSchemaPathType>): LocatorRegistrationPreDefinitionBuilder<LocatorSchemaPathType, RegistryPath<LocatorSchemaPathType>, false>;
1271
- add(path: RegistryPath<LocatorSchemaPathType>, options: {
1272
- reuse: RegistryPath<LocatorSchemaPathType>;
1273
- }): void;
1274
- add<Reuse extends ReusableLocator<LocatorSchemaPathType, RegistryPath<LocatorSchemaPathType>, LocatorStrategyDefinition["type"]>>(path: RegistryPath<LocatorSchemaPathType>, options: {
1275
- reuse: Reuse;
1276
- }): LocatorRegistrationSeededBuilderForType<LocatorSchemaPathType, RegistryPath<LocatorSchemaPathType>, Reuse["type"]>;
1275
+ add<Path extends LocatorSchemaPathType, Reuse extends LocatorSchemaPathType | ReusableLocator<LocatorSchemaPathType, RegistryPath<LocatorSchemaPathType>, LocatorStrategyDefinition["type"]> | undefined = undefined>(path: PathArgument<LocatorSchemaPathType, Path>, ...args: Reuse extends undefined ? [] : [
1276
+ options: {
1277
+ reuse: Reuse extends LocatorSchemaPathType ? ReusePathArgument<LocatorSchemaPathType, Path, Reuse> : Extract<Reuse, ReusableLocator<LocatorSchemaPathType, RegistryPath<LocatorSchemaPathType>, LocatorStrategyDefinition["type"]>>;
1278
+ }
1279
+ ]): Reuse extends undefined ? LocatorRegistrationPreDefinitionBuilder<LocatorSchemaPathType, RegistryPath<LocatorSchemaPathType>, false> : Reuse extends LocatorSchemaPathType ? void : LocatorRegistrationSeededBuilderForType<LocatorSchemaPathType, RegistryPath<LocatorSchemaPathType>, Extract<Reuse, ReusableLocator<LocatorSchemaPathType, RegistryPath<LocatorSchemaPathType>, LocatorStrategyDefinition["type"]>>["type"]>;
1277
1280
  register(path: RegistryPath<LocatorSchemaPathType>, record: LocatorSchemaRecord<LocatorSchemaPathType, RegistryPath<LocatorSchemaPathType>>): void;
1278
1281
  replace(path: RegistryPath<LocatorSchemaPathType>, record: LocatorSchemaRecord<LocatorSchemaPathType, RegistryPath<LocatorSchemaPathType>>): void;
1279
1282
  get(path: RegistryPath<LocatorSchemaPathType>): LocatorSchemaRecord<LocatorSchemaPathType, RegistryPath<LocatorSchemaPathType>>;
@@ -1297,7 +1300,7 @@ declare class LocatorRegistryInternal<LocatorSchemaPathType extends string> {
1297
1300
  * const locator = builder.getNestedLocator();
1298
1301
  * ```
1299
1302
  */
1300
- getLocatorSchema<Path extends RegistryPath<LocatorSchemaPathType>>(path: Path): LocatorQueryBuilderPublic<LocatorSchemaPathType, Path>;
1303
+ getLocatorSchema<Path extends LocatorSchemaPathType>(path: PathArgument<LocatorSchemaPathType, Path>): LocatorQueryBuilderPublic<LocatorSchemaPathType, Extract<Path, RegistryPath<LocatorSchemaPathType>>>;
1301
1304
  /**
1302
1305
  * Resolves the Playwright {@link Locator} for the provided path, applying only the terminal
1303
1306
  * definition and its recorded steps (no ancestor chaining). Throws if the path is not registered.
@@ -1308,7 +1311,7 @@ declare class LocatorRegistryInternal<LocatorSchemaPathType extends string> {
1308
1311
  * await button.click();
1309
1312
  * ```
1310
1313
  */
1311
- getLocator<Path extends RegistryPath<LocatorSchemaPathType>>(path: Path): Locator;
1314
+ getLocator<Path extends LocatorSchemaPathType>(path: PathArgument<LocatorSchemaPathType, Path>): Locator;
1312
1315
  /**
1313
1316
  * Resolves a chained Playwright {@link Locator} for a nested path, traversing each registered
1314
1317
  * segment and applying their steps in order. Throws if any segment in the chain is missing.
@@ -1319,7 +1322,7 @@ declare class LocatorRegistryInternal<LocatorSchemaPathType extends string> {
1319
1322
  * await expect(nested).toBeVisible();
1320
1323
  * ```
1321
1324
  */
1322
- getNestedLocator<Path extends RegistryPath<LocatorSchemaPathType>>(path: Path): Locator;
1325
+ getNestedLocator<Path extends LocatorSchemaPathType>(path: PathArgument<LocatorSchemaPathType, Path>): Locator;
1323
1326
  buildLocatorChain(path: RegistryPath<LocatorSchemaPathType>, definitions: Map<string, LocatorStrategyDefinition>, steps: Map<string, LocatorStep<RegistryPath<LocatorSchemaPathType>, RegistryPath<LocatorSchemaPathType>>[]>, tombstones?: Set<string>, terminalDescription?: LocatorDescription, context?: {
1324
1327
  rootPath: RegistryPath<LocatorSchemaPathType>;
1325
1328
  resolvingPaths: Set<string>;
@@ -1334,10 +1337,10 @@ declare class LocatorRegistryInternal<LocatorSchemaPathType extends string> {
1334
1337
  }[];
1335
1338
  };
1336
1339
  }
1337
- type GetLocatorAccessor<LocatorSchemaPathType extends string> = <Path extends RegistryPath<LocatorSchemaPathType>>(path: Path) => Locator;
1340
+ type GetLocatorAccessor<LocatorSchemaPathType extends string> = <Path extends LocatorSchemaPathType>(path: LocatorSchemaPathFormat<Path> extends Path ? Path : LocatorSchemaPathFormat<Path>) => Locator;
1338
1341
  type AddAccessor<LocatorSchemaPathType extends string> = LocatorRegistryInternal<LocatorSchemaPathType>["add"];
1339
- type GetLocatorSchemaAccessor<LocatorSchemaPathType extends string> = <Path extends RegistryPath<LocatorSchemaPathType>>(path: Path) => LocatorQueryBuilderPublic<LocatorSchemaPathType, Path>;
1340
- type GetNestedLocatorAccessor<LocatorSchemaPathType extends string> = <Path extends RegistryPath<LocatorSchemaPathType>>(path: Path) => Locator;
1342
+ type GetLocatorSchemaAccessor<LocatorSchemaPathType extends string> = <Path extends LocatorSchemaPathType>(path: LocatorSchemaPathFormat<Path> extends Path ? Path : LocatorSchemaPathFormat<Path>) => LocatorQueryBuilderPublic<LocatorSchemaPathType, Extract<Path, RegistryPath<LocatorSchemaPathType>>>;
1343
+ type GetNestedLocatorAccessor<LocatorSchemaPathType extends string> = <Path extends LocatorSchemaPathType>(path: LocatorSchemaPathFormat<Path> extends Path ? Path : LocatorSchemaPathFormat<Path>) => Locator;
1341
1344
  type LocatorRegistry<LocatorSchemaPathType extends string> = Pick<LocatorRegistryInternal<LocatorSchemaPathType>, "add" | "createReusable" | "getLocator" | "getLocatorSchema" | "getNestedLocator">;
1342
1345
  /**
1343
1346
  * Creates a v2 locator registry bound to a Playwright {@link Page} and returns the registry plus
@@ -1359,15 +1362,9 @@ type LocatorRegistry<LocatorSchemaPathType extends string> = Pick<LocatorRegistr
1359
1362
  */
1360
1363
  declare const createRegistryWithAccessors: <Paths extends string>(page: Page) => {
1361
1364
  readonly registry: LocatorRegistry<Paths>;
1362
- readonly add: {
1363
- (path: RegistryPath<Paths>): LocatorRegistrationPreDefinitionBuilder<Paths, RegistryPath<Paths>, false>;
1364
- (path: RegistryPath<Paths>, options: {
1365
- reuse: RegistryPath<Paths>;
1366
- }): void;
1367
- <Reuse extends ReusableLocator<Paths, RegistryPath<Paths>, "role" | "text" | "label" | "placeholder" | "altText" | "title" | "locator" | "frameLocator" | "testId" | "id">>(path: RegistryPath<Paths>, options: {
1368
- reuse: Reuse;
1369
- }): LocatorRegistrationSeededBuilderForType<Paths, RegistryPath<Paths>, Reuse["type"]>;
1370
- };
1365
+ readonly add: <Path extends Paths, Reuse extends Paths | ReusableLocator<Paths, RegistryPath<Paths>, "role" | "text" | "label" | "placeholder" | "altText" | "title" | "locator" | "frameLocator" | "testId" | "id"> | undefined = undefined>(path: PathArgument<Paths, Path>, ...args: Reuse extends undefined ? [] : [options: {
1366
+ reuse: Reuse extends Paths ? ReusePathArgument<Paths, Path, Reuse> : Extract<Reuse, ReusableLocator<Paths, RegistryPath<Paths>, "role" | "text" | "label" | "placeholder" | "altText" | "title" | "locator" | "frameLocator" | "testId" | "id">>;
1367
+ }]) => Reuse extends undefined ? LocatorRegistrationPreDefinitionBuilder<Paths, RegistryPath<Paths>, false> : Reuse extends Paths ? void : LocatorRegistrationSeededBuilderForType<Paths, RegistryPath<Paths>, Extract<Reuse, ReusableLocator<Paths, RegistryPath<Paths>, "role" | "text" | "label" | "placeholder" | "altText" | "title" | "locator" | "frameLocator" | "testId" | "id">>["type"]>;
1371
1368
  readonly getLocator: GetLocatorAccessor<Paths>;
1372
1369
  readonly getNestedLocator: GetNestedLocatorAccessor<Paths>;
1373
1370
  readonly getLocatorSchema: GetLocatorSchemaAccessor<Paths>;
package/dist/index.js CHANGED
@@ -2498,20 +2498,45 @@ var LocatorRegistryInternal = class {
2498
2498
  ...record.description !== void 0 ? { description: record.description } : {}
2499
2499
  };
2500
2500
  }
2501
- add(path, options) {
2501
+ /**
2502
+ * Registers a locator schema at the provided dot-delimited path.
2503
+ * Accepts exactly one locator strategy (`getByRole`, `locator`, etc.) and any number of
2504
+ * `filter`/`nth` steps chained in call order. When `{ reuse }` is supplied, the seeded
2505
+ * definition is applied first and one matching override is allowed as a PATCH of the seed;
2506
+ * otherwise, calling multiple locator strategies will throw.
2507
+ *
2508
+ * @example
2509
+ * ```ts
2510
+ * registry
2511
+ * .add("list.item")
2512
+ * .getByRole("listitem", { name: /Row/ })
2513
+ * .filter({ hasText: "Row" })
2514
+ * .nth("last");
2515
+ * ```
2516
+ */
2517
+ add(path, ...args) {
2518
+ const options = args[0];
2502
2519
  const reuse = options?.reuse;
2503
- if (!reuse) {
2520
+ if (reuse === void 0) {
2504
2521
  return new LocatorRegistrationBuilder(
2505
2522
  this,
2506
2523
  path
2507
2524
  );
2508
2525
  }
2509
2526
  if (typeof reuse === "string") {
2527
+ const targetPath = path;
2528
+ if (reuse === targetPath) {
2529
+ throw new Error(`Locator reuse path cannot be the same as registration path: "${targetPath}".`);
2530
+ }
2510
2531
  const sourceRecord = this.get(reuse);
2511
- const cloned = this.cloneRecordForReuse(sourceRecord, path);
2512
- this.register(path, cloned);
2532
+ const cloned = this.cloneRecordForReuse(sourceRecord, targetPath);
2533
+ this.register(targetPath, cloned);
2513
2534
  return void 0;
2514
2535
  }
2536
+ if (Array.isArray(reuse)) {
2537
+ const [reason] = reuse;
2538
+ throw new Error(`Invalid reuse path configuration for "${path}": ${reason}`);
2539
+ }
2515
2540
  const reusedRecord = this.cloneRecordForReuse(
2516
2541
  {
2517
2542
  locatorSchemaPath: path,
@@ -2562,7 +2587,7 @@ Existing Schema: ${errorDetails}`);
2562
2587
  }
2563
2588
  return {
2564
2589
  locatorSchemaPath: record.locatorSchemaPath,
2565
- definition: record.definition,
2590
+ definition: cloneLocatorStrategyDefinition(record.definition),
2566
2591
  steps: normalizeSteps(record.steps),
2567
2592
  ...record.description !== void 0 ? { description: record.description } : {}
2568
2593
  };
@@ -2577,7 +2602,7 @@ Existing Schema: ${errorDetails}`);
2577
2602
  }
2578
2603
  return {
2579
2604
  locatorSchemaPath: record.locatorSchemaPath,
2580
- definition: record.definition,
2605
+ definition: cloneLocatorStrategyDefinition(record.definition),
2581
2606
  steps: normalizeSteps(record.steps),
2582
2607
  ...record.description !== void 0 ? { description: record.description } : {}
2583
2608
  };
@@ -2656,7 +2681,10 @@ Existing Schema: ${errorDetails}`);
2656
2681
  * ```
2657
2682
  */
2658
2683
  getLocatorSchema(path) {
2659
- return new LocatorQueryBuilder(this, path);
2684
+ return new LocatorQueryBuilder(
2685
+ this,
2686
+ path
2687
+ );
2660
2688
  }
2661
2689
  /**
2662
2690
  * Resolves the Playwright {@link Locator} for the provided path, applying only the terminal
@@ -2759,7 +2787,6 @@ Existing Schema: ${errorDetails}`);
2759
2787
  }
2760
2788
  };
2761
2789
  var createRegistryWithAccessors = (page) => {
2762
- const _assertValidPaths = true;
2763
2790
  const registryInstance = new LocatorRegistryInternal(page);
2764
2791
  const add = registryInstance.add.bind(registryInstance);
2765
2792
  const getLocator = registryInstance.getLocator.bind(registryInstance);
package/dist/index.mjs CHANGED
@@ -2462,20 +2462,45 @@ var LocatorRegistryInternal = class {
2462
2462
  ...record.description !== void 0 ? { description: record.description } : {}
2463
2463
  };
2464
2464
  }
2465
- add(path, options) {
2465
+ /**
2466
+ * Registers a locator schema at the provided dot-delimited path.
2467
+ * Accepts exactly one locator strategy (`getByRole`, `locator`, etc.) and any number of
2468
+ * `filter`/`nth` steps chained in call order. When `{ reuse }` is supplied, the seeded
2469
+ * definition is applied first and one matching override is allowed as a PATCH of the seed;
2470
+ * otherwise, calling multiple locator strategies will throw.
2471
+ *
2472
+ * @example
2473
+ * ```ts
2474
+ * registry
2475
+ * .add("list.item")
2476
+ * .getByRole("listitem", { name: /Row/ })
2477
+ * .filter({ hasText: "Row" })
2478
+ * .nth("last");
2479
+ * ```
2480
+ */
2481
+ add(path, ...args) {
2482
+ const options = args[0];
2466
2483
  const reuse = options?.reuse;
2467
- if (!reuse) {
2484
+ if (reuse === void 0) {
2468
2485
  return new LocatorRegistrationBuilder(
2469
2486
  this,
2470
2487
  path
2471
2488
  );
2472
2489
  }
2473
2490
  if (typeof reuse === "string") {
2491
+ const targetPath = path;
2492
+ if (reuse === targetPath) {
2493
+ throw new Error(`Locator reuse path cannot be the same as registration path: "${targetPath}".`);
2494
+ }
2474
2495
  const sourceRecord = this.get(reuse);
2475
- const cloned = this.cloneRecordForReuse(sourceRecord, path);
2476
- this.register(path, cloned);
2496
+ const cloned = this.cloneRecordForReuse(sourceRecord, targetPath);
2497
+ this.register(targetPath, cloned);
2477
2498
  return void 0;
2478
2499
  }
2500
+ if (Array.isArray(reuse)) {
2501
+ const [reason] = reuse;
2502
+ throw new Error(`Invalid reuse path configuration for "${path}": ${reason}`);
2503
+ }
2479
2504
  const reusedRecord = this.cloneRecordForReuse(
2480
2505
  {
2481
2506
  locatorSchemaPath: path,
@@ -2526,7 +2551,7 @@ Existing Schema: ${errorDetails}`);
2526
2551
  }
2527
2552
  return {
2528
2553
  locatorSchemaPath: record.locatorSchemaPath,
2529
- definition: record.definition,
2554
+ definition: cloneLocatorStrategyDefinition(record.definition),
2530
2555
  steps: normalizeSteps(record.steps),
2531
2556
  ...record.description !== void 0 ? { description: record.description } : {}
2532
2557
  };
@@ -2541,7 +2566,7 @@ Existing Schema: ${errorDetails}`);
2541
2566
  }
2542
2567
  return {
2543
2568
  locatorSchemaPath: record.locatorSchemaPath,
2544
- definition: record.definition,
2569
+ definition: cloneLocatorStrategyDefinition(record.definition),
2545
2570
  steps: normalizeSteps(record.steps),
2546
2571
  ...record.description !== void 0 ? { description: record.description } : {}
2547
2572
  };
@@ -2620,7 +2645,10 @@ Existing Schema: ${errorDetails}`);
2620
2645
  * ```
2621
2646
  */
2622
2647
  getLocatorSchema(path) {
2623
- return new LocatorQueryBuilder(this, path);
2648
+ return new LocatorQueryBuilder(
2649
+ this,
2650
+ path
2651
+ );
2624
2652
  }
2625
2653
  /**
2626
2654
  * Resolves the Playwright {@link Locator} for the provided path, applying only the terminal
@@ -2723,7 +2751,6 @@ Existing Schema: ${errorDetails}`);
2723
2751
  }
2724
2752
  };
2725
2753
  var createRegistryWithAccessors = (page) => {
2726
- const _assertValidPaths = true;
2727
2754
  const registryInstance = new LocatorRegistryInternal(page);
2728
2755
  const add = registryInstance.add.bind(registryInstance);
2729
2756
  const getLocator = registryInstance.getLocator.bind(registryInstance);
@@ -119,7 +119,9 @@ type Paths =
119
119
  | "main.button@login";
120
120
  ```
121
121
 
122
- If invalid literals are included in the union, type errors are surfaced where the registry factory is instantiated.
122
+ If invalid literals are included in the union, TypeScript blocks those values at locator accessor argument usage
123
+ (`add`, `getLocator`, `getNestedLocator`, `getLocatorSchema`) with path-format diagnostics while preserving
124
+ valid path autocomplete suggestions. Runtime validation still applies for non-literal strings.
123
125
 
124
126
  ### Runtime validation rules
125
127
 
@@ -13,7 +13,7 @@
13
13
  "@types/express": "^5.0.3",
14
14
  "@types/node": "^24.5.2",
15
15
  "express": "^5.1.0",
16
- "pomwright": "file:../pomwright-1.4.0.tgz"
16
+ "pomwright": "file:../pomwright-1.5.0.tgz"
17
17
  },
18
18
  "dependencies": {
19
19
  "dotenv": "^17.2.2"
@@ -0,0 +1,76 @@
1
+ import { expect, test } from "@fixtures-v2/testApp.fixtures";
2
+ import type { Page } from "@playwright/test";
3
+ import { LocatorRegistryInternal } from "../../../../srcV2/locators";
4
+
5
+ const createTestRegistry = <Paths extends string>(page: Page) => new LocatorRegistryInternal<Paths>(page);
6
+
7
+ test("registry.get returns a cloned definition copy", async ({ page }) => {
8
+ type LocatorSchemaPaths = "button";
9
+
10
+ const registry = createTestRegistry<LocatorSchemaPaths>(page);
11
+
12
+ registry.add("button").getByRole("button", { name: "Submit", exact: true });
13
+
14
+ const firstRecord = registry.get("button");
15
+
16
+ expect(firstRecord).toEqual({
17
+ locatorSchemaPath: "button",
18
+ definition: {
19
+ type: "role",
20
+ role: "button",
21
+ options: { name: "Submit", exact: true },
22
+ },
23
+ steps: [],
24
+ });
25
+
26
+ if (firstRecord.definition.type !== "role") {
27
+ throw new Error("Expected role definition for mutation test");
28
+ }
29
+
30
+ firstRecord.definition.options = { name: "Mutated" };
31
+
32
+ const secondRecord = registry.get("button");
33
+
34
+ expect(secondRecord).toEqual({
35
+ locatorSchemaPath: "button",
36
+ definition: {
37
+ type: "role",
38
+ role: "button",
39
+ options: { name: "Submit", exact: true },
40
+ },
41
+ steps: [],
42
+ });
43
+ });
44
+
45
+ test("registry.getIfExists returns a cloned definition copy", async ({ page }) => {
46
+ type LocatorSchemaPaths = "banner";
47
+
48
+ const registry = createTestRegistry<LocatorSchemaPaths>(page);
49
+
50
+ registry.add("banner").locator("section.banner", { hasText: "Welcome" });
51
+
52
+ const firstRecord = registry.getIfExists("banner");
53
+ expect(firstRecord).toBeDefined();
54
+
55
+ if (!firstRecord || firstRecord.definition.type !== "locator") {
56
+ throw new Error("Expected locator definition for mutation test");
57
+ }
58
+
59
+ firstRecord.definition.options = { hasText: "Mutated" };
60
+ if (!firstRecord.steps) {
61
+ throw new Error("Expected steps array on cloned record");
62
+ }
63
+ firstRecord.steps.push({ kind: "index", index: 0 });
64
+
65
+ const secondRecord = registry.getIfExists("banner");
66
+
67
+ expect(secondRecord).toEqual({
68
+ locatorSchemaPath: "banner",
69
+ definition: {
70
+ type: "locator",
71
+ selector: "section.banner",
72
+ options: { hasText: "Welcome" },
73
+ },
74
+ steps: [],
75
+ });
76
+ });
@@ -21,6 +21,47 @@ test("add reuses with existing record by path does not have chainable methods",
21
21
  });
22
22
  });
23
23
 
24
+ test("add reuse by path with empty string does not silently skip reuse", async ({ page }) => {
25
+ type LocatorSchemaPaths = "button";
26
+
27
+ const registry = createTestRegistry<LocatorSchemaPaths>(page);
28
+
29
+ // @ts-expect-error - testing runtime error when reuse is an empty string
30
+ expect(() => registry.add("button", { reuse: "" })).toThrowError('No locator schema registered for path "".');
31
+ });
32
+
33
+ test("add reuse by path throws when source path matches target path", async ({ page }) => {
34
+ type LocatorSchemaPaths = "button";
35
+
36
+ const registry = createTestRegistry<LocatorSchemaPaths>(page);
37
+
38
+ // @ts-expect-error - testing runtime error when reuse path is the same as registration path
39
+ expect(() => registry.add("button", { reuse: "button" })).toThrowError(
40
+ 'Locator reuse path cannot be the same as registration path: "button".',
41
+ );
42
+ });
43
+
44
+ test("add reuse by path reports compile-time fallback reason when forced at runtime", async ({ page }) => {
45
+ type LocatorSchemaPaths = "button" | "button.copy";
46
+
47
+ const registry = createTestRegistry<LocatorSchemaPaths>(page);
48
+
49
+ const addUnsafe = registry.add as unknown as (
50
+ path: string,
51
+ options: {
52
+ reuse: [string];
53
+ },
54
+ ) => void;
55
+
56
+ expect(() =>
57
+ addUnsafe("button.copy", {
58
+ reuse: ["Invalid reuse path, reuse path cannot be the same as registration path: button.copy"],
59
+ }),
60
+ ).toThrowError(
61
+ 'Invalid reuse path configuration for "button.copy": Invalid reuse path, reuse path cannot be the same as registration path: button.copy',
62
+ );
63
+ });
64
+
24
65
  test("add reuse by path clones records so mutations do not leak", async ({ page }) => {
25
66
  type LocatorSchemaPaths = "button" | "button.copy";
26
67
 
@@ -0,0 +1,86 @@
1
+ import type { Page } from "@playwright/test";
2
+ import { createRegistryWithAccessors } from "pomwright";
3
+
4
+ declare const page: Page;
5
+
6
+ type ValidPaths = "valid" | "validReuse";
7
+
8
+ const validRegistryFactoryResult = createRegistryWithAccessors<ValidPaths>(page);
9
+
10
+ validRegistryFactoryResult.add("valid").locator("body");
11
+ // @ts-expect-error invalid path literal should fail at the accessor argument with path-format details
12
+ validRegistryFactoryResult.add("does.not.exist").locator("body");
13
+ // @ts-expect-error invalid path literal should fail at the accessor argument with path-format details
14
+ validRegistryFactoryResult.add("valid", { reuse: "does.not.exist" });
15
+ // @ts-expect-error reuse path cannot be the same as registration path
16
+ validRegistryFactoryResult.add("validReuse", { reuse: "validReuse" });
17
+ // @ts-expect-error invalid path literal should fail at the accessor argument with path-format details
18
+ validRegistryFactoryResult.getLocator("does.not.exist");
19
+ // @ts-expect-error invalid path literal should fail at the accessor argument with path-format details
20
+ validRegistryFactoryResult.getLocatorSchema("does.not.exist");
21
+ // @ts-expect-error invalid path literal should fail at the accessor argument with path-format details
22
+ validRegistryFactoryResult.getNestedLocator("does.not.exist");
23
+
24
+ type InvalidPaths = ValidPaths | "" | ".leading" | "trailing." | "double..dot" | "contains whitespace";
25
+
26
+ const invalidRegistryFactoryResult = createRegistryWithAccessors<InvalidPaths>(page);
27
+
28
+ invalidRegistryFactoryResult.add("valid").locator("body");
29
+ // @ts-expect-error invalid path literal should fail at the accessor argument with path-format details
30
+ invalidRegistryFactoryResult.add("").locator("body");
31
+ // @ts-expect-error invalid path literal should fail at the accessor argument with path-format details
32
+ invalidRegistryFactoryResult.add(".leading").locator("body");
33
+ // @ts-expect-error invalid path literal should fail at the accessor argument with path-format details
34
+ invalidRegistryFactoryResult.add("trailing.").locator("body");
35
+ // @ts-expect-error invalid path literal should fail at the accessor argument with path-format details
36
+ invalidRegistryFactoryResult.add("double..dot").locator("body");
37
+ // @ts-expect-error invalid path literal should fail at the accessor argument with path-format details
38
+ invalidRegistryFactoryResult.add("contains whitespace").locator("body");
39
+
40
+ invalidRegistryFactoryResult.add("validReuse", { reuse: "valid" });
41
+ // @ts-expect-error invalid path literal should fail at the accessor argument with path-format details
42
+ invalidRegistryFactoryResult.add("validReuse", { reuse: "" });
43
+ // @ts-expect-error invalid path literal should fail at the accessor argument with path-format details
44
+ invalidRegistryFactoryResult.add("validReuse", { reuse: ".leading" });
45
+ // @ts-expect-error invalid path literal should fail at the accessor argument with path-format details
46
+ invalidRegistryFactoryResult.add("validReuse", { reuse: "trailing." });
47
+ // @ts-expect-error invalid path literal should fail at the accessor argument with path-format details
48
+ invalidRegistryFactoryResult.add("validReuse", { reuse: "double..dot" });
49
+ // @ts-expect-error invalid path literal should fail at the accessor argument with path-format details
50
+ invalidRegistryFactoryResult.add("validReuse", { reuse: "contains whitespace" });
51
+
52
+ invalidRegistryFactoryResult.getLocator("valid");
53
+ // @ts-expect-error invalid path literal should fail at the accessor argument with path-format details
54
+ invalidRegistryFactoryResult.getLocator("");
55
+ // @ts-expect-error invalid path literal should fail at the accessor argument with path-format details
56
+ invalidRegistryFactoryResult.getLocator(".leading");
57
+ // @ts-expect-error invalid path literal should fail at the accessor argument with path-format details
58
+ invalidRegistryFactoryResult.getLocator("trailing.");
59
+ // @ts-expect-error invalid path literal should fail at the accessor argument with path-format details
60
+ invalidRegistryFactoryResult.getLocator("double..dot");
61
+ // @ts-expect-error invalid path literal should fail at the accessor argument with path-format details
62
+ invalidRegistryFactoryResult.getLocator("contains whitespace");
63
+
64
+ invalidRegistryFactoryResult.getLocatorSchema("valid");
65
+ // @ts-expect-error invalid path literal should fail at the accessor argument with path-format details
66
+ invalidRegistryFactoryResult.getLocatorSchema("");
67
+ // @ts-expect-error invalid path literal should fail at the accessor argument with path-format details
68
+ invalidRegistryFactoryResult.getLocatorSchema(".leading");
69
+ // @ts-expect-error invalid path literal should fail at the accessor argument with path-format details
70
+ invalidRegistryFactoryResult.getLocatorSchema("trailing.");
71
+ // @ts-expect-error invalid path literal should fail at the accessor argument with path-format details
72
+ invalidRegistryFactoryResult.getLocatorSchema("double..dot");
73
+ // @ts-expect-error invalid path literal should fail at the accessor argument with path-format details
74
+ invalidRegistryFactoryResult.getLocatorSchema("contains whitespace");
75
+
76
+ invalidRegistryFactoryResult.getNestedLocator("valid");
77
+ // @ts-expect-error invalid path literal should fail at the accessor argument with path-format details
78
+ invalidRegistryFactoryResult.getNestedLocator("");
79
+ // @ts-expect-error invalid path literal should fail at the accessor argument with path-format details
80
+ invalidRegistryFactoryResult.getNestedLocator(".leading");
81
+ // @ts-expect-error invalid path literal should fail at the accessor argument with path-format details
82
+ invalidRegistryFactoryResult.getNestedLocator("trailing.");
83
+ // @ts-expect-error invalid path literal should fail at the accessor argument with path-format details
84
+ invalidRegistryFactoryResult.getNestedLocator("double..dot");
85
+ // @ts-expect-error invalid path literal should fail at the accessor argument with path-format details
86
+ invalidRegistryFactoryResult.getNestedLocator("contains whitespace");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pomwright",
3
- "version": "1.5.0",
3
+ "version": "1.5.1",
4
4
  "description": "POMWright is a complementary test framework for Playwright written in TypeScript.",
5
5
  "repository": {
6
6
  "type": "git",
@@ -12,7 +12,7 @@ import type {
12
12
  IndexSelector,
13
13
  LocatorBuilderTarget,
14
14
  LocatorDescription,
15
- LocatorSchemaPathErrors,
15
+ LocatorSchemaPathFormat,
16
16
  LocatorSchemaRecord,
17
17
  LocatorStep,
18
18
  LocatorStrategyDefinition,
@@ -33,6 +33,21 @@ import {
33
33
  validateLocatorSchemaPath,
34
34
  } from "./utils";
35
35
 
36
+ type PathArgument<Paths extends string, Path extends Paths> = LocatorSchemaPathFormat<Path> extends Path
37
+ ? Path
38
+ : LocatorSchemaPathFormat<Path>;
39
+
40
+ type InvalidReusePath<Path extends string> = [
41
+ `Invalid reuse path, reuse path cannot be the same as registration path: ${Path}`,
42
+ ];
43
+
44
+ type ReusePathArgument<Paths extends string, Path extends Paths, ReusePath extends Paths> = Exclude<
45
+ ReusePath,
46
+ Path
47
+ > extends never
48
+ ? InvalidReusePath<Path>
49
+ : PathArgument<Paths, ReusePath>;
50
+
36
51
  export class LocatorRegistryInternal<LocatorSchemaPathType extends string> {
37
52
  private readonly schemas = new Map<
38
53
  RegistryPath<LocatorSchemaPathType>,
@@ -94,50 +109,82 @@ export class LocatorRegistryInternal<LocatorSchemaPathType extends string> {
94
109
  * .nth("last");
95
110
  * ```
96
111
  */
97
- add(
98
- path: RegistryPath<LocatorSchemaPathType>,
99
- ): LocatorRegistrationPreDefinitionBuilder<LocatorSchemaPathType, RegistryPath<LocatorSchemaPathType>, false>;
100
- add(path: RegistryPath<LocatorSchemaPathType>, options: { reuse: RegistryPath<LocatorSchemaPathType> }): void;
101
112
  add<
102
- Reuse extends ReusableLocator<
103
- LocatorSchemaPathType,
104
- RegistryPath<LocatorSchemaPathType>,
105
- LocatorStrategyDefinition["type"]
106
- >,
113
+ Path extends LocatorSchemaPathType,
114
+ Reuse extends
115
+ | LocatorSchemaPathType
116
+ | ReusableLocator<LocatorSchemaPathType, RegistryPath<LocatorSchemaPathType>, LocatorStrategyDefinition["type"]>
117
+ | undefined = undefined,
107
118
  >(
108
- path: RegistryPath<LocatorSchemaPathType>,
109
- options: { reuse: Reuse },
110
- ): LocatorRegistrationSeededBuilderForType<LocatorSchemaPathType, RegistryPath<LocatorSchemaPathType>, Reuse["type"]>;
111
- add(
112
- path: RegistryPath<LocatorSchemaPathType>,
113
- options?: {
114
- reuse?:
115
- | ReusableLocator<LocatorSchemaPathType, RegistryPath<LocatorSchemaPathType>, LocatorStrategyDefinition["type"]>
116
- | RegistryPath<LocatorSchemaPathType>;
117
- },
118
- ):
119
- | LocatorRegistrationPreDefinitionBuilder<LocatorSchemaPathType, RegistryPath<LocatorSchemaPathType>, false>
120
- | LocatorRegistrationSeededBuilderForType<
121
- LocatorSchemaPathType,
122
- RegistryPath<LocatorSchemaPathType>,
123
- LocatorStrategyDefinition["type"]
124
- >
125
- | undefined {
119
+ path: PathArgument<LocatorSchemaPathType, Path>,
120
+ ...args: Reuse extends undefined
121
+ ? []
122
+ : [
123
+ options: {
124
+ reuse: Reuse extends LocatorSchemaPathType
125
+ ? ReusePathArgument<LocatorSchemaPathType, Path, Reuse>
126
+ : Extract<
127
+ Reuse,
128
+ ReusableLocator<
129
+ LocatorSchemaPathType,
130
+ RegistryPath<LocatorSchemaPathType>,
131
+ LocatorStrategyDefinition["type"]
132
+ >
133
+ >;
134
+ },
135
+ ]
136
+ ): Reuse extends undefined
137
+ ? LocatorRegistrationPreDefinitionBuilder<LocatorSchemaPathType, RegistryPath<LocatorSchemaPathType>, false>
138
+ : Reuse extends LocatorSchemaPathType
139
+ ? void
140
+ : LocatorRegistrationSeededBuilderForType<
141
+ LocatorSchemaPathType,
142
+ RegistryPath<LocatorSchemaPathType>,
143
+ Extract<
144
+ Reuse,
145
+ ReusableLocator<
146
+ LocatorSchemaPathType,
147
+ RegistryPath<LocatorSchemaPathType>,
148
+ LocatorStrategyDefinition["type"]
149
+ >
150
+ >["type"]
151
+ > {
152
+ const options = args[0] as
153
+ | {
154
+ reuse?:
155
+ | ReusableLocator<
156
+ LocatorSchemaPathType,
157
+ RegistryPath<LocatorSchemaPathType>,
158
+ LocatorStrategyDefinition["type"]
159
+ >
160
+ | LocatorSchemaPathType
161
+ | InvalidReusePath<RegistryPath<LocatorSchemaPathType>>;
162
+ }
163
+ | undefined;
126
164
  const reuse = options?.reuse;
127
165
 
128
- if (!reuse) {
166
+ if (reuse === undefined) {
129
167
  return new LocatorRegistrationBuilder<LocatorSchemaPathType, RegistryPath<LocatorSchemaPathType>, false>(
130
168
  this,
131
- path,
132
- );
169
+ path as RegistryPath<LocatorSchemaPathType>,
170
+ ) as never;
133
171
  }
134
172
 
135
173
  if (typeof reuse === "string") {
174
+ const targetPath = path as RegistryPath<LocatorSchemaPathType>;
175
+ if (reuse === targetPath) {
176
+ throw new Error(`Locator reuse path cannot be the same as registration path: "${targetPath}".`);
177
+ }
136
178
  const sourceRecord = this.get(reuse as RegistryPath<LocatorSchemaPathType>);
137
- const cloned = this.cloneRecordForReuse(sourceRecord, path);
179
+ const cloned = this.cloneRecordForReuse(sourceRecord, targetPath);
180
+
181
+ this.register(targetPath, cloned);
182
+ return undefined as never;
183
+ }
138
184
 
139
- this.register(path, cloned);
140
- return undefined;
185
+ if (Array.isArray(reuse)) {
186
+ const [reason] = reuse;
187
+ throw new Error(`Invalid reuse path configuration for "${path}": ${reason}`);
141
188
  }
142
189
 
143
190
  const reusedRecord: LocatorSchemaRecord<
@@ -145,24 +192,24 @@ export class LocatorRegistryInternal<LocatorSchemaPathType extends string> {
145
192
  RegistryPath<LocatorSchemaPathType>
146
193
  > = this.cloneRecordForReuse(
147
194
  {
148
- locatorSchemaPath: path,
195
+ locatorSchemaPath: path as RegistryPath<LocatorSchemaPathType>,
149
196
  definition: reuse.definition,
150
197
  steps: reuse.steps,
151
198
  description: reuse.description,
152
199
  },
153
- path,
200
+ path as RegistryPath<LocatorSchemaPathType>,
154
201
  );
155
202
 
156
203
  return new LocatorRegistrationBuilder<LocatorSchemaPathType, RegistryPath<LocatorSchemaPathType>, true>(
157
204
  this,
158
- path,
205
+ path as RegistryPath<LocatorSchemaPathType>,
159
206
  {
160
207
  initialDefinition: reusedRecord.definition,
161
208
  initialSteps: reusedRecord.steps,
162
209
  reuseType: reusedRecord.definition.type,
163
210
  initialDescription: reusedRecord.description,
164
211
  },
165
- ).persistSeededDefinition();
212
+ ).persistSeededDefinition() as never;
166
213
  }
167
214
 
168
215
  register(
@@ -204,7 +251,7 @@ export class LocatorRegistryInternal<LocatorSchemaPathType extends string> {
204
251
  }
205
252
  return {
206
253
  locatorSchemaPath: record.locatorSchemaPath,
207
- definition: record.definition,
254
+ definition: cloneLocatorStrategyDefinition(record.definition),
208
255
  steps: normalizeSteps(record.steps),
209
256
  ...(record.description !== undefined ? { description: record.description } : {}),
210
257
  } satisfies LocatorSchemaRecord<LocatorSchemaPathType, RegistryPath<LocatorSchemaPathType>>;
@@ -219,12 +266,12 @@ export class LocatorRegistryInternal<LocatorSchemaPathType extends string> {
219
266
  ): LocatorSchemaRecord<LocatorSchemaPathType, RegistryPath<LocatorSchemaPathType>> | undefined {
220
267
  const record = this.schemas.get(path);
221
268
  if (!record) {
222
- return undefined;
269
+ return undefined as never;
223
270
  }
224
271
 
225
272
  return {
226
273
  locatorSchemaPath: record.locatorSchemaPath,
227
- definition: record.definition,
274
+ definition: cloneLocatorStrategyDefinition(record.definition),
228
275
  steps: normalizeSteps(record.steps),
229
276
  ...(record.description !== undefined ? { description: record.description } : {}),
230
277
  } satisfies LocatorSchemaRecord<LocatorSchemaPathType, RegistryPath<LocatorSchemaPathType>>;
@@ -345,10 +392,13 @@ export class LocatorRegistryInternal<LocatorSchemaPathType extends string> {
345
392
  * const locator = builder.getNestedLocator();
346
393
  * ```
347
394
  */
348
- getLocatorSchema<Path extends RegistryPath<LocatorSchemaPathType>>(
349
- path: Path,
350
- ): LocatorQueryBuilderPublic<LocatorSchemaPathType, Path> {
351
- return new LocatorQueryBuilder<LocatorSchemaPathType, Path>(this, path);
395
+ getLocatorSchema<Path extends LocatorSchemaPathType>(
396
+ path: PathArgument<LocatorSchemaPathType, Path>,
397
+ ): LocatorQueryBuilderPublic<LocatorSchemaPathType, Extract<Path, RegistryPath<LocatorSchemaPathType>>> {
398
+ return new LocatorQueryBuilder<LocatorSchemaPathType, Extract<Path, RegistryPath<LocatorSchemaPathType>>>(
399
+ this,
400
+ path as Extract<Path, RegistryPath<LocatorSchemaPathType>>,
401
+ );
352
402
  }
353
403
 
354
404
  /**
@@ -361,7 +411,7 @@ export class LocatorRegistryInternal<LocatorSchemaPathType extends string> {
361
411
  * await button.click();
362
412
  * ```
363
413
  */
364
- getLocator<Path extends RegistryPath<LocatorSchemaPathType>>(path: Path): Locator {
414
+ getLocator<Path extends LocatorSchemaPathType>(path: PathArgument<LocatorSchemaPathType, Path>): Locator {
365
415
  return this.getLocatorSchema(path).getLocator();
366
416
  }
367
417
 
@@ -375,7 +425,7 @@ export class LocatorRegistryInternal<LocatorSchemaPathType extends string> {
375
425
  * await expect(nested).toBeVisible();
376
426
  * ```
377
427
  */
378
- getNestedLocator<Path extends RegistryPath<LocatorSchemaPathType>>(path: Path): Locator {
428
+ getNestedLocator<Path extends LocatorSchemaPathType>(path: PathArgument<LocatorSchemaPathType, Path>): Locator {
379
429
  return this.getLocatorSchema(path).getNestedLocator();
380
430
  }
381
431
 
@@ -485,24 +535,18 @@ export class LocatorRegistryInternal<LocatorSchemaPathType extends string> {
485
535
  }
486
536
  }
487
537
 
488
- export type GetLocatorAccessor<LocatorSchemaPathType extends string> = <
489
- Path extends RegistryPath<LocatorSchemaPathType>,
490
- >(
491
- path: Path,
538
+ export type GetLocatorAccessor<LocatorSchemaPathType extends string> = <Path extends LocatorSchemaPathType>(
539
+ path: LocatorSchemaPathFormat<Path> extends Path ? Path : LocatorSchemaPathFormat<Path>,
492
540
  ) => Locator;
493
541
 
494
542
  export type AddAccessor<LocatorSchemaPathType extends string> = LocatorRegistryInternal<LocatorSchemaPathType>["add"];
495
543
 
496
- export type GetLocatorSchemaAccessor<LocatorSchemaPathType extends string> = <
497
- Path extends RegistryPath<LocatorSchemaPathType>,
498
- >(
499
- path: Path,
500
- ) => LocatorQueryBuilderPublic<LocatorSchemaPathType, Path>;
544
+ export type GetLocatorSchemaAccessor<LocatorSchemaPathType extends string> = <Path extends LocatorSchemaPathType>(
545
+ path: LocatorSchemaPathFormat<Path> extends Path ? Path : LocatorSchemaPathFormat<Path>,
546
+ ) => LocatorQueryBuilderPublic<LocatorSchemaPathType, Extract<Path, RegistryPath<LocatorSchemaPathType>>>;
501
547
 
502
- export type GetNestedLocatorAccessor<LocatorSchemaPathType extends string> = <
503
- Path extends RegistryPath<LocatorSchemaPathType>,
504
- >(
505
- path: Path,
548
+ export type GetNestedLocatorAccessor<LocatorSchemaPathType extends string> = <Path extends LocatorSchemaPathType>(
549
+ path: LocatorSchemaPathFormat<Path> extends Path ? Path : LocatorSchemaPathFormat<Path>,
506
550
  ) => Locator;
507
551
 
508
552
  export type LocatorRegistry<LocatorSchemaPathType extends string> = Pick<
@@ -529,8 +573,6 @@ export type LocatorRegistry<LocatorSchemaPathType extends string> = Pick<
529
573
  * ```
530
574
  */
531
575
  export const createRegistryWithAccessors = <Paths extends string>(page: Page) => {
532
- type PathErrors = LocatorSchemaPathErrors<Paths>;
533
- const _assertValidPaths: PathErrors extends never ? true : never = true as PathErrors extends never ? true : never;
534
576
  const registryInstance = new LocatorRegistryInternal<Paths>(page);
535
577
  const add: LocatorRegistryInternal<Paths>["add"] = registryInstance.add.bind(registryInstance);
536
578
  const getLocator: GetLocatorAccessor<Paths> = registryInstance.getLocator.bind(registryInstance);