pomwright 1.3.0 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (117) hide show
  1. package/AGENTS.md +37 -0
  2. package/CHANGELOG.md +193 -0
  3. package/README.md +316 -34
  4. package/dist/index.d.mts +1058 -132
  5. package/dist/index.d.ts +1058 -132
  6. package/dist/index.js +2309 -185
  7. package/dist/index.mjs +2304 -185
  8. package/docs/{get-locator-methods-explanation.md → v1/get-locator-methods-explanation.md} +0 -16
  9. package/docs/v1-to-v2-migration/bridge-migration-guide.md +159 -0
  10. package/docs/v1-to-v2-migration/direct-migration-guide.md +238 -0
  11. package/docs/v1-to-v2-migration/v1-to-v2-comparison.md +547 -0
  12. package/docs/v2/PageObject.md +293 -0
  13. package/docs/v2/composing-locator-modules.md +93 -0
  14. package/docs/v2/locator-registry.md +693 -0
  15. package/docs/v2/logging.md +168 -0
  16. package/docs/v2/overview.md +515 -0
  17. package/docs/v2/session-storage.md +160 -0
  18. package/index.ts +61 -9
  19. package/intTestV2/.env +0 -0
  20. package/intTestV2/fixtures/testApp.fixtures.ts +43 -0
  21. package/intTestV2/package.json +22 -0
  22. package/intTestV2/page-object-models/testApp/pages/iframe/iframe.locatorSchema.ts +24 -0
  23. package/intTestV2/page-object-models/testApp/pages/iframe/iframe.page.ts +17 -0
  24. package/intTestV2/page-object-models/testApp/pages/testPage.locatorSchema.ts +32 -0
  25. package/intTestV2/page-object-models/testApp/pages/testPage.page.ts +119 -0
  26. package/intTestV2/page-object-models/testApp/pages/testPath/[color]/color.locatorSchema.ts +29 -0
  27. package/intTestV2/page-object-models/testApp/pages/testPath/[color]/color.page.ts +48 -0
  28. package/intTestV2/page-object-models/testApp/pages/testPath/testPath.locatorSchema.ts +9 -0
  29. package/intTestV2/page-object-models/testApp/pages/testPath/testPath.page.ts +23 -0
  30. package/intTestV2/page-object-models/testApp/pages/testfilters/testfilters.locatorSchema.ts +114 -0
  31. package/intTestV2/page-object-models/testApp/pages/testfilters/testfilters.page.ts +23 -0
  32. package/intTestV2/page-object-models/testApp/testApp.base.ts +20 -0
  33. package/intTestV2/playwright.config.ts +54 -0
  34. package/intTestV2/server.js +216 -0
  35. package/intTestV2/test-data/staticPage/index.html +280 -0
  36. package/intTestV2/test-data/staticPage/w3images/avatar2.png +0 -0
  37. package/intTestV2/test-data/staticPage/w3images/avatar3.png +0 -0
  38. package/intTestV2/test-data/staticPage/w3images/avatar5.png +0 -0
  39. package/intTestV2/test-data/staticPage/w3images/avatar6.png +0 -0
  40. package/intTestV2/test-data/staticPage/w3images/forest.jpg +0 -0
  41. package/intTestV2/test-data/staticPage/w3images/lights.jpg +0 -0
  42. package/intTestV2/test-data/staticPage/w3images/mountains.jpg +0 -0
  43. package/intTestV2/test-data/staticPage/w3images/nature.jpg +0 -0
  44. package/intTestV2/test-data/staticPage/w3images/snow.jpg +0 -0
  45. package/intTestV2/tests/locatorRegistry/add/add.describe.spec.ts +54 -0
  46. package/intTestV2/tests/locatorRegistry/add/add.filter.spec.ts +143 -0
  47. package/intTestV2/tests/locatorRegistry/add/add.frameLocator.spec.ts +23 -0
  48. package/intTestV2/tests/locatorRegistry/add/add.getByAltText.spec.ts +23 -0
  49. package/intTestV2/tests/locatorRegistry/add/add.getById.spec.ts +45 -0
  50. package/intTestV2/tests/locatorRegistry/add/add.getByLabel.spec.ts +23 -0
  51. package/intTestV2/tests/locatorRegistry/add/add.getByPlaceholder.spec.ts +23 -0
  52. package/intTestV2/tests/locatorRegistry/add/add.getByRole.spec.ts +23 -0
  53. package/intTestV2/tests/locatorRegistry/add/add.getByTestId.spec.ts +23 -0
  54. package/intTestV2/tests/locatorRegistry/add/add.getByText.spec.ts +23 -0
  55. package/intTestV2/tests/locatorRegistry/add/add.getByTitle.spec.ts +23 -0
  56. package/intTestV2/tests/locatorRegistry/add/add.locator.spec.ts +23 -0
  57. package/intTestV2/tests/locatorRegistry/add/add.reuseExisting.spec.ts +66 -0
  58. package/intTestV2/tests/locatorRegistry/add/add.reuseReusable.spec.ts +311 -0
  59. package/intTestV2/tests/locatorRegistry/add/add.spec.ts +159 -0
  60. package/intTestV2/tests/locatorRegistry/filter.cycle.spec.ts +39 -0
  61. package/intTestV2/tests/locatorRegistry/getLocator/getLocator.spec.ts +253 -0
  62. package/intTestV2/tests/locatorRegistry/getLocatorSchema/getLocatorSchema.clearSteps.spec.ts +105 -0
  63. package/intTestV2/tests/locatorRegistry/getLocatorSchema/getLocatorSchema.describe.spec.ts +23 -0
  64. package/intTestV2/tests/locatorRegistry/getLocatorSchema/getLocatorSchema.filter.spec.ts +368 -0
  65. package/intTestV2/tests/locatorRegistry/getLocatorSchema/getLocatorSchema.getLocator.spec.ts +56 -0
  66. package/intTestV2/tests/locatorRegistry/getLocatorSchema/getLocatorSchema.getNestedLocator.spec.ts +175 -0
  67. package/intTestV2/tests/locatorRegistry/getLocatorSchema/getLocatorSchema.nth.spec.ts +60 -0
  68. package/intTestV2/tests/locatorRegistry/getLocatorSchema/getLocatorSchema.remove.spec.ts +32 -0
  69. package/intTestV2/tests/locatorRegistry/getLocatorSchema/getLocatorSchema.replace.spec.ts +24 -0
  70. package/intTestV2/tests/locatorRegistry/getLocatorSchema/getLocatorSchema.spec.ts +110 -0
  71. package/intTestV2/tests/locatorRegistry/getLocatorSchema/getLocatorSchema.update.spec.ts +322 -0
  72. package/intTestV2/tests/locatorRegistry/getNestedLocator/getNestedLocator.spec.ts +412 -0
  73. package/intTestV2/tests/locatorRegistry/registry/registry.binding.spec.ts +50 -0
  74. package/intTestV2/tests/locatorRegistry/validation/validation.locatorSchemaPath.spec.ts +115 -0
  75. package/intTestV2/tests/locatorRegistry/validation/validation.sub-path.spec.ts +45 -0
  76. package/intTestV2/tests/step/step.spec.ts +49 -0
  77. package/intTestV2/tests/testApp/color.spec.ts +15 -0
  78. package/intTestV2/tests/testApp/iframe.spec.ts +57 -0
  79. package/intTestV2/tests/testApp/testFilters.spec.ts +24 -0
  80. package/intTestV2/tests/testApp/testPage.spec.ts +161 -0
  81. package/intTestV2/tests/testApp/testPath.spec.ts +18 -0
  82. package/pack-build.sh +11 -0
  83. package/pack-test-v2.sh +36 -0
  84. package/package.json +10 -3
  85. package/playwright.base.ts +42 -0
  86. package/skills/README.md +56 -0
  87. package/skills/pomwright-v1-5-bridge-migration/SKILL.md +40 -0
  88. package/skills/pomwright-v1-5-bridge-migration/references/call-site-migration.md +178 -0
  89. package/skills/pomwright-v1-5-bridge-migration/references/schema-translation.md +183 -0
  90. package/skills/pomwright-v2-migration/SKILL.md +63 -0
  91. package/skills/pomwright-v2-migration/references/call-site-migration.md +265 -0
  92. package/skills/pomwright-v2-migration/references/class-migration.md +266 -0
  93. package/skills/pomwright-v2-migration/references/fixture-and-helpers.md +423 -0
  94. package/skills/pomwright-v2-migration/references/locator-registration.md +344 -0
  95. package/srcV2/fixture/base.fixtures.ts +23 -0
  96. package/srcV2/helpers/navigation.ts +153 -0
  97. package/srcV2/helpers/playwrightReportLogger.ts +196 -0
  98. package/srcV2/helpers/sessionStorage.ts +251 -0
  99. package/srcV2/helpers/stepDecorator.ts +106 -0
  100. package/srcV2/locators/index.ts +15 -0
  101. package/srcV2/locators/locatorQueryBuilder.ts +427 -0
  102. package/srcV2/locators/locatorRegistrationBuilder.ts +558 -0
  103. package/srcV2/locators/locatorRegistry.ts +541 -0
  104. package/srcV2/locators/locatorUpdateBuilder.ts +602 -0
  105. package/srcV2/locators/reusableLocatorBuilder.ts +200 -0
  106. package/srcV2/locators/types.ts +256 -0
  107. package/srcV2/locators/utils.ts +309 -0
  108. package/srcV2/locators/v1SchemaTranslator.ts +178 -0
  109. package/srcV2/pageObject.ts +105 -0
  110. /package/docs/{BaseApi-explanation.md → v1/BaseApi-explanation.md} +0 -0
  111. /package/docs/{BasePage-explanation.md → v1/BasePage-explanation.md} +0 -0
  112. /package/docs/{LocatorSchema-explanation.md → v1/LocatorSchema-explanation.md} +0 -0
  113. /package/docs/{LocatorSchemaPath-explanation.md → v1/LocatorSchemaPath-explanation.md} +0 -0
  114. /package/docs/{PlaywrightReportLogger-explanation.md → v1/PlaywrightReportLogger-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,23 @@
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
+ const errMsg = "No locator schema registered for path";
8
+
9
+ test("add getByRole to registry", async ({ page }) => {
10
+ type LocatorSchemaPaths = "buttonByRole";
11
+
12
+ const registry = createTestRegistry<LocatorSchemaPaths>(page);
13
+
14
+ expect(() => registry.get("buttonByRole")).toThrowError(`${errMsg} "buttonByRole".`);
15
+
16
+ registry.add("buttonByRole").getByRole("button", { name: "Submit" });
17
+
18
+ expect(registry.get("buttonByRole")).toEqual({
19
+ definition: { role: "button", options: { name: "Submit" }, type: "role" },
20
+ locatorSchemaPath: "buttonByRole",
21
+ steps: [],
22
+ });
23
+ });
@@ -0,0 +1,23 @@
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
+ const errMsg = "No locator schema registered for path";
8
+
9
+ test("add getByTestId to registry", async ({ page }) => {
10
+ type LocatorSchemaPaths = "elementByTestId";
11
+
12
+ const registry = createTestRegistry<LocatorSchemaPaths>(page);
13
+
14
+ expect(() => registry.get("elementByTestId")).toThrowError(`${errMsg} "elementByTestId".`);
15
+
16
+ registry.add("elementByTestId").getByTestId("login-button");
17
+
18
+ expect(registry.get("elementByTestId")).toEqual({
19
+ definition: { testId: "login-button", type: "testId" },
20
+ locatorSchemaPath: "elementByTestId",
21
+ steps: [],
22
+ });
23
+ });
@@ -0,0 +1,23 @@
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
+ const errMsg = "No locator schema registered for path";
8
+
9
+ test("add getByText to registry", async ({ page }) => {
10
+ type LocatorSchemaPaths = "elementByText";
11
+
12
+ const registry = createTestRegistry<LocatorSchemaPaths>(page);
13
+
14
+ expect(() => registry.get("elementByText")).toThrowError(`${errMsg} "elementByText".`);
15
+
16
+ registry.add("elementByText").getByText("Welcome");
17
+
18
+ expect(registry.get("elementByText")).toEqual({
19
+ definition: { text: "Welcome", type: "text" },
20
+ locatorSchemaPath: "elementByText",
21
+ steps: [],
22
+ });
23
+ });
@@ -0,0 +1,23 @@
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
+ const errMsg = "No locator schema registered for path";
8
+
9
+ test("add getByTitle to registry", async ({ page }) => {
10
+ type LocatorSchemaPaths = "elementByTitle";
11
+
12
+ const registry = createTestRegistry<LocatorSchemaPaths>(page);
13
+
14
+ expect(() => registry.get("elementByTitle")).toThrowError(`${errMsg} "elementByTitle".`);
15
+
16
+ registry.add("elementByTitle").getByTitle("Home Page");
17
+
18
+ expect(registry.get("elementByTitle")).toEqual({
19
+ definition: { text: "Home Page", type: "title" },
20
+ locatorSchemaPath: "elementByTitle",
21
+ steps: [],
22
+ });
23
+ });
@@ -0,0 +1,23 @@
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
+ const errMsg = "No locator schema registered for path";
8
+
9
+ test("add locator to registry", async ({ page }) => {
10
+ type LocatorSchemaPaths = "body";
11
+
12
+ const registry = createTestRegistry<LocatorSchemaPaths>(page);
13
+
14
+ expect(() => registry.get("body")).toThrowError(`${errMsg} "body".`);
15
+
16
+ registry.add("body").locator("body");
17
+
18
+ expect(registry.get("body")).toEqual({
19
+ definition: { selector: "body", type: "locator" },
20
+ locatorSchemaPath: "body",
21
+ steps: [],
22
+ });
23
+ });
@@ -0,0 +1,66 @@
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("add reuses with existing record by path does not have chainable methods", async ({ page }) => {
8
+ type LocatorSchemaPaths = "button" | "button.copy";
9
+
10
+ const registry = createTestRegistry<LocatorSchemaPaths>(page);
11
+
12
+ registry.add("button").getByRole("button", { name: "Submit" }).filter({ hasText: "Submit" });
13
+
14
+ const builder = registry.add("button.copy", { reuse: "button" });
15
+
16
+ expect(builder).toBeUndefined();
17
+ expect(registry.get("button.copy")).toEqual({
18
+ definition: { role: "button", options: { name: "Submit" }, type: "role" },
19
+ locatorSchemaPath: "button.copy",
20
+ steps: [{ filter: { hasText: "Submit" }, kind: "filter" }],
21
+ });
22
+ });
23
+
24
+ test("add reuse by path clones records so mutations do not leak", async ({ page }) => {
25
+ type LocatorSchemaPaths = "button" | "button.copy";
26
+
27
+ const registry = createTestRegistry<LocatorSchemaPaths>(page);
28
+
29
+ registry.add("button").getByRole("button", { name: "Submit" }).filter({ hasText: "Submit" });
30
+
31
+ registry.add("button.copy", { reuse: "button" });
32
+
33
+ expect(registry.get("button.copy")).toEqual({
34
+ definition: { role: "button", options: { name: "Submit" }, type: "role" },
35
+ locatorSchemaPath: "button.copy",
36
+ steps: [{ filter: { hasText: "Submit" }, kind: "filter" }],
37
+ });
38
+
39
+ registry.replace("button.copy", {
40
+ definition: { role: "link", options: { name: "Copy" }, type: "role" },
41
+ locatorSchemaPath: "button.copy",
42
+ steps: [{ filter: { hasText: "Copy" }, kind: "filter" }],
43
+ });
44
+
45
+ expect(registry.get("button.copy")).toEqual({
46
+ definition: { role: "link", options: { name: "Copy" }, type: "role" },
47
+ locatorSchemaPath: "button.copy",
48
+ steps: [{ filter: { hasText: "Copy" }, kind: "filter" }],
49
+ });
50
+
51
+ expect(registry.get("button")).toEqual({
52
+ definition: { role: "button", options: { name: "Submit" }, type: "role" },
53
+ locatorSchemaPath: "button",
54
+ steps: [{ filter: { hasText: "Submit" }, kind: "filter" }],
55
+ });
56
+ });
57
+
58
+ test("add reuse by path throws when the source path is missing", async ({ page }) => {
59
+ type LocatorSchemaPaths = "button" | "button.copy";
60
+
61
+ const registry = createTestRegistry<LocatorSchemaPaths>(page);
62
+
63
+ expect(() => registry.add("button.copy", { reuse: "button" })).toThrowError(
64
+ 'No locator schema registered for path "button".',
65
+ );
66
+ });
@@ -0,0 +1,311 @@
1
+ import { expect, test } from "@fixtures-v2/testApp.fixtures";
2
+ import type { Page } from "@playwright/test";
3
+ import { type FilterDefinition, LocatorRegistryInternal } from "../../../../srcV2/locators";
4
+
5
+ const createTestRegistry = <Paths extends string>(page: Page) => new LocatorRegistryInternal<Paths>(page);
6
+
7
+ test("add can reuse a reusable locator definition", async ({ page }) => {
8
+ type LocatorSchemaPaths = "heading" | "heading.first";
9
+
10
+ const registry = createTestRegistry<LocatorSchemaPaths>(page);
11
+
12
+ const h2 = registry.createReusable.getByRole("heading", { level: 2 }).filter({ hasText: /Summary/ });
13
+
14
+ registry.add("heading", { reuse: h2 });
15
+ registry.add("heading.first", { reuse: h2 }).nth(0);
16
+
17
+ expect(registry.get("heading")).toEqual({
18
+ definition: { role: "heading", options: { level: 2 }, type: "role" },
19
+ locatorSchemaPath: "heading",
20
+ steps: [{ filter: { hasText: /Summary/ }, kind: "filter" }],
21
+ });
22
+
23
+ expect(registry.get("heading.first")).toEqual({
24
+ definition: { role: "heading", options: { level: 2 }, type: "role" },
25
+ locatorSchemaPath: "heading.first",
26
+ steps: [
27
+ { filter: { hasText: /Summary/ }, kind: "filter" },
28
+ { index: 0, kind: "index" },
29
+ ],
30
+ });
31
+ });
32
+
33
+ test("reusable builder yields the same locator chain as a direct definition", async ({ page }) => {
34
+ type LocatorSchemaPaths = "heading" | "heading.reused";
35
+
36
+ const registry = createTestRegistry<LocatorSchemaPaths>(page);
37
+
38
+ const reusable = registry.createReusable.getByRole("heading", { level: 2 }).filter({ hasText: "Intro" }).nth(1);
39
+
40
+ registry.add("heading").getByRole("heading", { level: 2 }).filter({ hasText: "Intro" }).nth(1);
41
+ registry.add("heading.reused", { reuse: reusable });
42
+
43
+ const direct = registry.getLocator("heading");
44
+ const reused = registry.getLocator("heading.reused");
45
+
46
+ expect(`${reused}`).toEqual(`${direct}`);
47
+ });
48
+
49
+ test("add reuse by reusable locator patches options while preserving selector", async ({ page }) => {
50
+ type LocatorSchemaPaths = "errorMessage" | "error.invalidPassword";
51
+
52
+ const registry = createTestRegistry<LocatorSchemaPaths>(page);
53
+
54
+ const errorMessage = registry.createReusable.locator("error-message");
55
+ registry.add("errorMessage", { reuse: errorMessage });
56
+ registry.add("error.invalidPassword", { reuse: errorMessage }).locator({ hasText: /invalid password/ });
57
+
58
+ expect(registry.get("errorMessage")).toEqual({
59
+ definition: { selector: "error-message", type: "locator" },
60
+ locatorSchemaPath: "errorMessage",
61
+ steps: [],
62
+ });
63
+
64
+ expect(registry.get("error.invalidPassword")).toEqual({
65
+ definition: { options: { hasText: /invalid password/ }, selector: "error-message", type: "locator" },
66
+ locatorSchemaPath: "error.invalidPassword",
67
+ steps: [],
68
+ });
69
+ });
70
+
71
+ test("add reuse by reusable locator inherits selector and discriminant", async ({ page }) => {
72
+ type LocatorSchemaPaths = "errorMessage" | "error.invalidPassword";
73
+
74
+ const registry = createTestRegistry<LocatorSchemaPaths>(page);
75
+
76
+ const errorMessage = registry.createReusable.locator("error-message", { hasText: /invalid password/ });
77
+ registry.add("errorMessage", { reuse: errorMessage });
78
+ registry.add("error.invalidPassword", { reuse: errorMessage }).locator({ hasText: /invalid email/ });
79
+
80
+ expect(registry.get("errorMessage")).toEqual({
81
+ definition: { options: { hasText: /invalid password/ }, selector: "error-message", type: "locator" },
82
+ locatorSchemaPath: "errorMessage",
83
+ steps: [],
84
+ });
85
+
86
+ expect(registry.get("error.invalidPassword")).toEqual({
87
+ definition: { options: { hasText: /invalid email/ }, selector: "error-message", type: "locator" },
88
+ locatorSchemaPath: "error.invalidPassword",
89
+ steps: [],
90
+ });
91
+ });
92
+
93
+ test("add reuse with a reusable locator does not mutate the reusable definition", async ({ page }) => {
94
+ type LocatorSchemaPaths = "heading" | "heading.first";
95
+
96
+ const registry = createTestRegistry<LocatorSchemaPaths>(page);
97
+
98
+ const reusable = registry.createReusable.getByRole("heading", { level: 2 }).filter({ hasText: /Summary/ });
99
+
100
+ registry.add("heading", { reuse: reusable });
101
+ registry.add("heading.first", { reuse: reusable }).nth(0);
102
+
103
+ expect(reusable.steps).toEqual([{ filter: { hasText: /Summary/ }, kind: "filter" }]);
104
+ expect(registry.get("heading")).toEqual({
105
+ definition: { role: "heading", options: { level: 2 }, type: "role" },
106
+ locatorSchemaPath: "heading",
107
+ steps: [{ filter: { hasText: /Summary/ }, kind: "filter" }],
108
+ });
109
+
110
+ expect(registry.get("heading.first")).toEqual({
111
+ definition: { role: "heading", options: { level: 2 }, type: "role" },
112
+ locatorSchemaPath: "heading.first",
113
+ steps: [
114
+ { filter: { hasText: /Summary/ }, kind: "filter" },
115
+ { index: 0, kind: "index" },
116
+ ],
117
+ });
118
+ });
119
+
120
+ test("add reuse enforces matching locator type overrides", async ({ page }) => {
121
+ type LocatorSchemaPaths = "button" | "button.reuseLocator";
122
+
123
+ const registry = createTestRegistry<LocatorSchemaPaths>(page);
124
+
125
+ const button = registry.createReusable.getByRole("button", { name: "Submit" });
126
+
127
+ // @ts-expect-error mismatched locator strategies are not allowed when reusing a locator (getByText attempt on role locator)
128
+ expect(() => registry.add("button", { reuse: button }).getByText("text")).toThrowError(
129
+ 'must use the "role" strategy',
130
+ );
131
+
132
+ registry.add("button", { reuse: button });
133
+
134
+ expect(registry.get("button")).toEqual({
135
+ definition: { role: "button", options: { name: "Submit" }, type: "role" },
136
+ locatorSchemaPath: "button",
137
+ steps: [],
138
+ });
139
+
140
+ // @ts-expect-error mismatched locator strategies are not allowed when reusing a locator (getById attempt on role locator)
141
+ expect(() => registry.add("button.reuseLocator", { reuse: button }).getById("Submit")).toThrowError(
142
+ 'must use the "role" strategy',
143
+ );
144
+
145
+ registry.add("button.reuseLocator", { reuse: button }).getByRole({ name: "Submit" });
146
+
147
+ expect(registry.get("button.reuseLocator")).toEqual({
148
+ definition: { role: "button", options: { name: "Submit" }, type: "role" },
149
+ locatorSchemaPath: "button.reuseLocator",
150
+ steps: [],
151
+ });
152
+ });
153
+
154
+ test("add reuse patches role options while preserving role value", async ({ page }) => {
155
+ type LocatorSchemaPaths = "heading";
156
+
157
+ const registry = createTestRegistry<LocatorSchemaPaths>(page);
158
+ const h2 = registry.createReusable.getByRole("heading", { level: 2 });
159
+
160
+ registry.add("heading", { reuse: h2 }).getByRole({ name: "Summary" });
161
+
162
+ expect(registry.get("heading")).toEqual({
163
+ definition: { role: "heading", options: { level: 2, name: "Summary" }, type: "role" },
164
+ locatorSchemaPath: "heading",
165
+ steps: [],
166
+ });
167
+ });
168
+
169
+ test("add reuse typing narrows override methods to the matching strategy", async ({ page }) => {
170
+ type LocatorSchemaPaths = "button";
171
+
172
+ const registry = createTestRegistry<LocatorSchemaPaths>(page);
173
+ const button = registry.createReusable.getByRole("button", { name: "Submit" });
174
+ const builder = registry.add("button", { reuse: button });
175
+
176
+ // @ts-expect-error mismatched locator strategies are not exposed when reusing a locator
177
+ const _invalidOverrideMethod = builder.getByText;
178
+
179
+ builder.getByRole({ name: "Submit" });
180
+ });
181
+
182
+ test("add reuse allows only one matching locator override", async ({ page }) => {
183
+ type LocatorSchemaPaths = "heading";
184
+
185
+ const registry = createTestRegistry<LocatorSchemaPaths>(page);
186
+ const h2 = registry.createReusable.getByRole("heading", { level: 2 });
187
+
188
+ const builder = registry.add("heading", { reuse: h2 });
189
+
190
+ builder.getByRole("heading", { name: "Summary" });
191
+
192
+ expect(() => builder.getByRole("heading", { name: "Other" })).toThrowError("only one matching override is allowed");
193
+ });
194
+
195
+ test("createReusable.filter supports Playwright Locator instances for has/hasNot", async ({ page }) => {
196
+ type LocatorSchemaPaths = "item";
197
+
198
+ const registry = createTestRegistry<LocatorSchemaPaths>(page);
199
+
200
+ const seed = registry.createReusable
201
+ .getByRole("button")
202
+ .filter({ has: page.getByRole("heading", { level: 2 }) })
203
+ .filter({ hasNot: page.getByRole("heading", { level: 3 }) });
204
+
205
+ registry.add("item", { reuse: seed });
206
+
207
+ const locator = registry.getLocator("item");
208
+
209
+ expect(`${locator}`).toEqual(
210
+ "getByRole('button').filter({ has: getByRole('heading', { level: 2 }) }).filter({ hasNot: getByRole('heading', { level: 3 }) })",
211
+ );
212
+ });
213
+
214
+ test("createReusable.filter supports registry path strings for has/hasNot", async ({ page }) => {
215
+ type LocatorSchemaPaths = "item" | "heading.primary" | "heading.secondary";
216
+
217
+ const registry = createTestRegistry<LocatorSchemaPaths>(page);
218
+
219
+ registry.add("heading.primary").getByRole("heading", { level: 2 });
220
+ registry.add("heading.secondary").getByRole("heading", { level: 3 });
221
+
222
+ const seed = registry.createReusable
223
+ .getByRole("button")
224
+ .filter({ has: "heading.primary" })
225
+ .filter({ hasNot: "heading.secondary" });
226
+
227
+ registry.add("item", { reuse: seed });
228
+
229
+ const locator = registry.getLocator("item");
230
+
231
+ expect(`${locator}`).toEqual(
232
+ "getByRole('button').filter({ has: getByRole('heading', { level: 2 }) }).filter({ hasNot: getByRole('heading', { level: 3 }) })",
233
+ );
234
+ });
235
+
236
+ test("createReusable.filter rejects inline locator strategy definitions for has/hasNot", async ({ page }) => {
237
+ type LocatorSchemaPaths = "item";
238
+
239
+ const registry = createTestRegistry<LocatorSchemaPaths>(page);
240
+
241
+ registry.createReusable
242
+ .getByRole("button")
243
+ // @ts-expect-error inline locator definitions are no longer supported
244
+ .filter({ has: { type: "locator", selector: "section" } });
245
+
246
+ const unsafeFilter = {
247
+ has: { type: "locator", selector: "section" },
248
+ } as unknown as FilterDefinition<LocatorSchemaPaths, LocatorSchemaPaths>;
249
+
250
+ const unsafeSeed = registry.createReusable.getByRole("button").filter(unsafeFilter);
251
+
252
+ registry.add("item", { reuse: unsafeSeed });
253
+
254
+ expect(() => registry.getLocator("item")).toThrow(/Unsupported filter reference/);
255
+ });
256
+
257
+ test("createReusable.filter rejects locator wrappers for has/hasNot", async ({ page }) => {
258
+ type LocatorSchemaPaths = "item";
259
+
260
+ const registry = createTestRegistry<LocatorSchemaPaths>(page);
261
+
262
+ registry.createReusable
263
+ .getByRole("button")
264
+ // @ts-expect-error locator wrapper is no longer supported
265
+ .filter({ has: { locator: { type: "locator", selector: "section" } } });
266
+
267
+ const unsafeFilter = {
268
+ has: { locator: { type: "locator", selector: "section" } },
269
+ } as unknown as FilterDefinition<LocatorSchemaPaths, LocatorSchemaPaths>;
270
+
271
+ const unsafeSeed = registry.createReusable.getByRole("button").filter(unsafeFilter);
272
+
273
+ registry.add("item", { reuse: unsafeSeed });
274
+
275
+ expect(() => registry.getLocator("item")).toThrow(/Unsupported filter reference/);
276
+ });
277
+
278
+ test("createReusable.filter rejects locatorPath wrappers for has/hasNot", async ({ page }) => {
279
+ type LocatorSchemaPaths = "item";
280
+
281
+ const registry = createTestRegistry<LocatorSchemaPaths>(page);
282
+
283
+ registry.createReusable
284
+ .getByRole("button")
285
+ // @ts-expect-error locatorPath wrapper is no longer supported
286
+ .filter({ has: { locatorPath: "item" } });
287
+
288
+ const unsafeFilter = {
289
+ has: { locatorPath: "item" },
290
+ } as unknown as FilterDefinition<LocatorSchemaPaths, LocatorSchemaPaths>;
291
+
292
+ const unsafeSeed = registry.createReusable.getByRole("button").filter(unsafeFilter);
293
+
294
+ registry.add("item", { reuse: unsafeSeed });
295
+
296
+ expect(() => registry.getLocator("item")).toThrow(/Unsupported filter reference/);
297
+ });
298
+
299
+ test("createReusable.filter supports visible true/false", async ({ page }) => {
300
+ type LocatorSchemaPaths = "item";
301
+
302
+ const registry = createTestRegistry<LocatorSchemaPaths>(page);
303
+
304
+ const seed = registry.createReusable.getByRole("button").filter({ visible: true }).filter({ visible: false });
305
+
306
+ registry.add("item", { reuse: seed });
307
+
308
+ const locator = registry.getLocator("item");
309
+
310
+ expect(`${locator}`).toEqual("getByRole('button').filter({ visible: true }).filter({ visible: false })");
311
+ });
@@ -0,0 +1,159 @@
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("add regex-driven getBy* registrations to registry", async ({ page }) => {
8
+ type LocatorSchemaPaths =
9
+ | "regex.text"
10
+ | "regex.label"
11
+ | "regex.placeholder"
12
+ | "regex.altText"
13
+ | "regex.title"
14
+ | "regex.reuse.text"
15
+ | "regex.reusePath.text";
16
+
17
+ const registry = createTestRegistry<LocatorSchemaPaths>(page);
18
+
19
+ registry.add("regex.text").getByText(/Welcome/i);
20
+ registry.add("regex.label").getByLabel(/Username/i);
21
+ registry.add("regex.placeholder").getByPlaceholder(/Enter your name/i);
22
+ registry.add("regex.altText").getByAltText(/Sample Image/i);
23
+ registry.add("regex.title").getByTitle(/Home Page/i);
24
+
25
+ const reusableText = registry.createReusable.getByText(/Seeded/i);
26
+ registry.add("regex.reuse.text", { reuse: reusableText }).getByText({ exact: true });
27
+ registry.add("regex.reusePath.text", { reuse: "regex.text" });
28
+
29
+ expect(registry.get("regex.text")).toEqual({
30
+ definition: { text: /Welcome/i, type: "text" },
31
+ locatorSchemaPath: "regex.text",
32
+ steps: [],
33
+ });
34
+ expect(registry.get("regex.label")).toEqual({
35
+ definition: { text: /Username/i, type: "label" },
36
+ locatorSchemaPath: "regex.label",
37
+ steps: [],
38
+ });
39
+ expect(registry.get("regex.placeholder")).toEqual({
40
+ definition: { text: /Enter your name/i, type: "placeholder" },
41
+ locatorSchemaPath: "regex.placeholder",
42
+ steps: [],
43
+ });
44
+ expect(registry.get("regex.altText")).toEqual({
45
+ definition: { text: /Sample Image/i, type: "altText" },
46
+ locatorSchemaPath: "regex.altText",
47
+ steps: [],
48
+ });
49
+ expect(registry.get("regex.title")).toEqual({
50
+ definition: { text: /Home Page/i, type: "title" },
51
+ locatorSchemaPath: "regex.title",
52
+ steps: [],
53
+ });
54
+ expect(registry.get("regex.reuse.text")).toEqual({
55
+ definition: { text: /Seeded/i, options: { exact: true }, type: "text" },
56
+ locatorSchemaPath: "regex.reuse.text",
57
+ steps: [],
58
+ });
59
+ expect(registry.get("regex.reusePath.text")).toEqual({
60
+ definition: { text: /Welcome/i, type: "text" },
61
+ locatorSchemaPath: "regex.reusePath.text",
62
+ steps: [],
63
+ });
64
+ });
65
+
66
+ test("add prevents multiple locator type definitions", async ({ page }) => {
67
+ type LocatorSchemaPaths = "heading";
68
+
69
+ const registry = createTestRegistry<LocatorSchemaPaths>(page);
70
+
71
+ const builder = registry.add("heading").getByRole("heading", { level: 2 });
72
+
73
+ expect((builder as { getByText?: unknown }).getByText).toBeUndefined();
74
+
75
+ builder.filter({ hasText: "Heading" }).nth(0);
76
+ });
77
+
78
+ test("add typing narrows locator methods after a definition is chosen", async ({ page }) => {
79
+ type LocatorSchemaPaths = "heading";
80
+
81
+ const registry = createTestRegistry<LocatorSchemaPaths>(page);
82
+ const postDefinition = registry.add("heading").getByRole("heading", { level: 2 });
83
+
84
+ // @ts-expect-error additional locator methods are not exposed after a definition is set
85
+ const _invalidLocatorMethod = postDefinition.getByText;
86
+
87
+ postDefinition.filter({ hasText: "Heading" }).nth(0);
88
+ });
89
+
90
+ test("A LocatorSchemaPath can only be added once", async ({ page }) => {
91
+ type LocatorSchemaPaths = "body";
92
+
93
+ const registry = createTestRegistry<LocatorSchemaPaths>(page);
94
+
95
+ registry.add("body").locator("body");
96
+
97
+ const errMsg = /A locator schema with the path "body" already exists*/;
98
+
99
+ expect(() => registry.add("body").locator("someone elses body")).toThrowError(errMsg);
100
+ });
101
+
102
+ test("add getByRole overloads accept optional options and support chained steps", async ({ page }) => {
103
+ type LocalPath = "root" | "root.options" | "root.minimal";
104
+ const registry = new LocatorRegistryInternal<LocalPath>(page);
105
+
106
+ registry.add("root").locator("body");
107
+
108
+ registry.add("root.options").getByRole("button", { name: "opts" }).filter({ hasText: "filtered" }).nth("last");
109
+ registry.add("root.minimal").getByRole("button");
110
+
111
+ expect(registry.get("root.options")).toEqual({
112
+ locatorSchemaPath: "root.options",
113
+ definition: { type: "role", role: "button", options: { name: "opts" } },
114
+ steps: [
115
+ { kind: "filter", filter: { hasText: "filtered" } },
116
+ { kind: "index", index: "last" },
117
+ ],
118
+ });
119
+
120
+ expect(registry.get("root.minimal")).toEqual({
121
+ locatorSchemaPath: "root.minimal",
122
+ definition: { type: "role", role: "button" },
123
+ steps: [],
124
+ });
125
+ });
126
+
127
+ test("add overloads support other strategies with options and chained steps", async ({ page }) => {
128
+ type LocalPath = "root.text" | "root.locator" | "root.frame";
129
+
130
+ const registry = new LocatorRegistryInternal<LocalPath>(page);
131
+
132
+ registry.add("root.text").getByText("needle", { exact: true }).filter({ hasText: "filtered" }).nth(0);
133
+ registry.add("root.locator").locator(".selector", { hasText: "opt" }).filter({ hasText: "filtered" }).nth(0);
134
+ registry.add("root.frame").frameLocator("iframe[name=child]");
135
+
136
+ expect(registry.get("root.text")).toEqual({
137
+ locatorSchemaPath: "root.text",
138
+ definition: { type: "text", text: "needle", options: { exact: true } },
139
+ steps: [
140
+ { kind: "filter", filter: { hasText: "filtered" } },
141
+ { kind: "index", index: 0 },
142
+ ],
143
+ });
144
+
145
+ expect(registry.get("root.locator")).toEqual({
146
+ locatorSchemaPath: "root.locator",
147
+ definition: { type: "locator", selector: ".selector", options: { hasText: "opt" } },
148
+ steps: [
149
+ { kind: "filter", filter: { hasText: "filtered" } },
150
+ { kind: "index", index: 0 },
151
+ ],
152
+ });
153
+
154
+ expect(registry.get("root.frame")).toEqual({
155
+ locatorSchemaPath: "root.frame",
156
+ definition: { type: "frameLocator", selector: "iframe[name=child]" },
157
+ steps: [],
158
+ });
159
+ });