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,32 @@
1
+ import { expect, test } from "@fixtures-v2/testApp.fixtures";
2
+ import type { Page } from "@playwright/test";
3
+ import { createRegistryWithAccessors } from "pomwright";
4
+
5
+ const createRegistry = <Paths extends string>(page: Page) => createRegistryWithAccessors<Paths>(page);
6
+
7
+ test("remove deletes definition and causes resolution to fail", async ({ page }) => {
8
+ type Paths = "root" | "root.target";
9
+
10
+ const { add, getLocatorSchema } = createRegistry<Paths>(page);
11
+
12
+ add("root").locator("div.root");
13
+ add("root.target").locator(".child");
14
+
15
+ const builder = getLocatorSchema("root.target");
16
+ builder.remove("root.target");
17
+
18
+ expect(() => builder.getNestedLocator()).toThrowError('No locator schema registered for path "root.target".');
19
+ });
20
+
21
+ test("remove on non-terminal skips the segment while allowing resolution", async ({ page }) => {
22
+ type Paths = "root" | "root.child";
23
+
24
+ const { add, getLocatorSchema } = createRegistry<Paths>(page);
25
+
26
+ add("root").locator("div.root");
27
+ add("root.child").getByRole("button", { name: "Submit" });
28
+
29
+ const locator = getLocatorSchema("root.child").remove("root").getNestedLocator();
30
+
31
+ expect(`${locator}`).toEqual("getByRole('button', { name: 'Submit' })");
32
+ });
@@ -0,0 +1,24 @@
1
+ import { expect, test } from "@fixtures-v2/testApp.fixtures";
2
+ import type { Page } from "@playwright/test";
3
+ import { createRegistryWithAccessors } from "pomwright";
4
+
5
+ const createRegistry = <Paths extends string>(page: Page) => createRegistryWithAccessors<Paths>(page);
6
+
7
+ test("replace updates definition while retaining recorded steps", async ({ page }) => {
8
+ type Paths = "root" | "root.target";
9
+
10
+ const { add, getLocatorSchema } = createRegistry<Paths>(page);
11
+
12
+ add("root").locator("div.root");
13
+ add("root.target").locator(".old").filter({ hasText: "old" });
14
+
15
+ const locator = getLocatorSchema("root.target")
16
+ .replace("root.target")
17
+ .getByRole("button", { name: "New" })
18
+ .filter("root.target", { hasText: "patched" })
19
+ .getNestedLocator();
20
+
21
+ expect(`${locator}`).toEqual(
22
+ "locator('div.root').getByRole('button', { name: 'New' }).filter({ hasText: 'old' }).filter({ hasText: 'patched' })",
23
+ );
24
+ });
@@ -0,0 +1,110 @@
1
+ import { expect, test } from "@fixtures-v2/testApp.fixtures";
2
+
3
+ test("shorthand getLocator returns the same none-nested locator as getLocatorSchema.getLocator", async ({
4
+ testPage,
5
+ }) => {
6
+ const builderLocator = testPage.getLocatorSchema("topMenu.notifications.dropdown.item").getLocator();
7
+ const shorthand = testPage.getLocator("topMenu.notifications.dropdown.item");
8
+
9
+ expect(`${shorthand}`).toEqual(`${builderLocator}`);
10
+ });
11
+
12
+ test("shorthand getNestedLocator returns the same nested locator as getLocatorSchema.getNestedLocator", async ({
13
+ testPage,
14
+ }) => {
15
+ const builderLocator = testPage.getLocatorSchema("topMenu.notifications.dropdown.item").getNestedLocator();
16
+ const shorthand = testPage.getNestedLocator("topMenu.notifications.dropdown.item");
17
+
18
+ expect(`${shorthand}`).toEqual(`${builderLocator}`);
19
+ });
20
+
21
+ test("independent builders do not share state", async ({ testFilters }) => {
22
+ const modified = testFilters
23
+ .getLocatorSchema("body.section.heading")
24
+ .update("body.section.heading")
25
+ .getByRole({ name: "hello", level: 3, exact: true })
26
+ .getNestedLocator();
27
+
28
+ const untouched = testFilters.getLocatorSchema("body.section.heading").getNestedLocator();
29
+
30
+ expect(`${modified}`).toEqual(
31
+ "locator('body').locator('section').getByRole('heading', { name: 'hello', exact: true, level: 3 })",
32
+ );
33
+ expect(`${untouched}`).toEqual("locator('body').locator('section').getByRole('heading', { level: 2 })");
34
+ });
35
+
36
+ test("demonstrate filter and index implementation in v2", async ({ page, testFilters }) => {
37
+ const schemaTwo = testFilters.getLocatorSchema("one.two");
38
+
39
+ const initialLocator = schemaTwo.getNestedLocator();
40
+ expect(`${initialLocator}`).toEqual("locator('div.one').locator('div.two').filter({ hasText: 'two' }).first()");
41
+
42
+ schemaTwo
43
+ .update("one.two")
44
+ .locator()
45
+ .clearSteps("one.two")
46
+ .filter("one.two", { hasText: "NewText" })
47
+ .filter("one.two", { hasText: "AdditionalText" })
48
+ .nth("one.two", "last");
49
+ const newLocator = schemaTwo.getNestedLocator();
50
+ expect(`${newLocator}`).toEqual(
51
+ "locator('div.one').locator('div.two').filter({ hasText: 'NewText' }).filter({ hasText: 'AdditionalText' }).last()",
52
+ );
53
+
54
+ schemaTwo
55
+ .clearSteps("one.two")
56
+ .filter("one.two", { hasText: "NewText" })
57
+ .filter("one.two", { hasText: "AdditionalText" })
58
+ .filter("one.two", { hasText: "AddedText" })
59
+ .filter("one.two", { hasText: "LastText" })
60
+ .nth("one", 0)
61
+ .nth("one.two", 1);
62
+ const updatedLocator = schemaTwo.getNestedLocator();
63
+ expect(`${updatedLocator}`).toEqual(
64
+ "locator('div.one').first().locator('div.two').filter({ hasText: 'NewText' }).filter({ hasText: 'AdditionalText' }).filter({ hasText: 'AddedText' }).filter({ hasText: 'LastText' }).nth(1)",
65
+ );
66
+
67
+ // But in vanilla Playwright we can do:
68
+
69
+ const manualLocator = page
70
+ .locator("div.one")
71
+ .locator("div.two")
72
+ .first()
73
+ .filter({ hasText: "NewText" })
74
+ .last()
75
+ .filter({ hasText: "AdditionalText" })
76
+ .filter({ hasText: "AddedText" })
77
+ .filter({ hasText: "LastText" })
78
+ .nth(1);
79
+ expect(`${manualLocator}`).toEqual(
80
+ "locator('div.one').locator('div.two').first().filter({ hasText: 'NewText' }).last().filter({ hasText: 'AdditionalText' }).filter({ hasText: 'AddedText' }).filter({ hasText: 'LastText' }).nth(1)",
81
+ );
82
+
83
+ const autoChainedLocator = testFilters
84
+ .getLocatorSchema("one.two")
85
+ .clearSteps("one.two")
86
+ .nth("one.two", 0)
87
+ .filter("one.two", { hasText: "NewText" })
88
+ .nth("one.two", -1)
89
+ .filter("one.two", { hasText: "AdditionalText" })
90
+ .filter("one.two", { hasText: "AddedText" })
91
+ .filter("one.two", { hasText: "LastText" })
92
+ .nth("one.two", 1)
93
+ .getNestedLocator();
94
+
95
+ expect(`${autoChainedLocator}`).toEqual(`${manualLocator}`);
96
+
97
+ const autoChainedLocatorWrapper = testFilters
98
+ .getLocatorSchema("one.two")
99
+ .clearSteps("one.two")
100
+ .nth("one.two", 0)
101
+ .filter("one.two", { hasText: "NewText" })
102
+ .nth("one.two", -1)
103
+ .filter("one.two", { hasText: "AdditionalText" })
104
+ .filter("one.two", { hasText: "AddedText" })
105
+ .filter("one.two", { hasText: "LastText" })
106
+ .nth("one.two", 1)
107
+ .getNestedLocator();
108
+
109
+ expect(`${autoChainedLocatorWrapper}`).toEqual(`${manualLocator}`);
110
+ });
@@ -0,0 +1,322 @@
1
+ import { expect, test } from "@fixtures-v2/testApp.fixtures";
2
+ import { LocatorRegistryInternal } from "../../../../srcV2/locators";
3
+
4
+ test("update replaces intermediate definitions without mutating registry", async ({ testFilters }) => {
5
+ const original = testFilters.getNestedLocator("body.section.heading");
6
+ expect(`${original}`).toEqual("locator('body').locator('section').getByRole('heading', { level: 2 })");
7
+
8
+ const replaced = testFilters
9
+ .getLocatorSchema("body.section.heading")
10
+ .update("body.section")
11
+ .getByRole("heading", { level: 1 })
12
+ .getNestedLocator();
13
+
14
+ expect(`${replaced}`).toEqual(
15
+ "locator('body').getByRole('heading', { level: 1 }).getByRole('heading', { level: 2 })",
16
+ );
17
+
18
+ const after = testFilters.getNestedLocator("body.section.heading");
19
+ expect(`${after}`).toEqual(`${original}`);
20
+ });
21
+
22
+ test("update operations can be chained across sub-paths", async ({ testFilters }) => {
23
+ const chained = testFilters
24
+ .getLocatorSchema("body.section")
25
+ .update("body")
26
+ .locator("SOMEBODY")
27
+ .update("body.section")
28
+ .getByRole("button", { name: "Click me!" })
29
+ .getNestedLocator();
30
+
31
+ expect(`${chained}`).toEqual("locator('SOMEBODY').getByRole('button', { name: 'Click me!' })");
32
+ });
33
+
34
+ test("update merges options without requiring full definitions", async ({ testFilters }) => {
35
+ const merged = testFilters
36
+ .getLocatorSchema("body.section.heading")
37
+ .update("body.section.heading")
38
+ .getByRole({ name: "HEADING TEXT" })
39
+ .getNestedLocator();
40
+
41
+ expect(`${merged}`).toEqual(
42
+ "locator('body').locator('section').getByRole('heading', { name: 'HEADING TEXT', level: 2 })",
43
+ );
44
+ });
45
+
46
+ test("update handles full and partial definitions from fresh builders", async ({ testFilters }) => {
47
+ const full = testFilters
48
+ .getLocatorSchema("body.section.heading")
49
+ .update("body.section.heading")
50
+ .getByRole("heading", { level: 3 })
51
+ .getNestedLocator();
52
+
53
+ const partial = testFilters
54
+ .getLocatorSchema("body.section.heading")
55
+ .update("body.section.heading")
56
+ .getByRole({ level: 3 })
57
+ .getNestedLocator();
58
+
59
+ expect(`${full}`).toEqual("locator('body').locator('section').getByRole('heading', { level: 3 })");
60
+ expect(`${partial}`).toEqual("locator('body').locator('section').getByRole('heading', { level: 3 })");
61
+ expect(full).not.toBe(partial);
62
+ });
63
+
64
+ test("update preserves registered filters on untouched segments", async ({ testFilters }) => {
65
+ const locator = testFilters
66
+ .getLocatorSchema("fictional.filter@hasNotText.filter@hasText")
67
+ .update("fictional.filter@hasNotText")
68
+ .getByRole("button", { name: "roleOptions" })
69
+ .update("fictional.filter@hasNotText.filter@hasText")
70
+ .locator("locator")
71
+ .getNestedLocator();
72
+
73
+ expect(`${locator}`).toEqual(
74
+ "getByRole('button', { name: 'roleOptions' }).filter({ hasNotText: 'hasNotText' }).locator('locator').filter({ hasText: 'hasText' })",
75
+ );
76
+ });
77
+
78
+ test("update rejects unknown sub-paths", ({ testFilters }) => {
79
+ expect(() =>
80
+ testFilters
81
+ .getLocatorSchema("body.section.heading")
82
+ // @ts-expect-error Testing invalid path handling
83
+ .update("body.section.missing")
84
+ .locator("noop"),
85
+ ).toThrow('"body.section.missing" is not a valid sub-path of "body.section.heading"');
86
+ });
87
+
88
+ test("update can mix ancestor and descendant changes without mutating registry", async ({ testFilters }) => {
89
+ const original = testFilters.getNestedLocator("body.section.heading");
90
+
91
+ const chained = testFilters
92
+ .getLocatorSchema("body.section.heading")
93
+ .update("body")
94
+ .locator("SOMEBODY")
95
+ .update("body.section.heading")
96
+ .getByRole({ name: "HEADING TEXT" })
97
+ .getNestedLocator();
98
+
99
+ expect(`${chained}`).toEqual(
100
+ "locator('SOMEBODY').locator('section').getByRole('heading', { name: 'HEADING TEXT', level: 2 })",
101
+ );
102
+
103
+ const after = testFilters.getNestedLocator("body.section.heading");
104
+ expect(`${after}`).toEqual(`${original}`);
105
+ });
106
+
107
+ test("update preserves filters on the target sub-path", async ({ testFilters }) => {
108
+ const locator = testFilters
109
+ .getLocatorSchema("fictional.filter@hasText")
110
+ .update("fictional.filter@hasText")
111
+ .locator("updated")
112
+ .getNestedLocator();
113
+
114
+ expect(`${locator}`).toEqual("locator('updated').filter({ hasText: 'hasText' })");
115
+ });
116
+
117
+ test("update patches definitions without altering chained filters or indices", async ({ testFilters }) => {
118
+ const updated = testFilters
119
+ .getLocatorSchema("body.section.button")
120
+ .filter("body.section.button", { hasText: /Click me!/ })
121
+ .nth("body.section", "first")
122
+ .update("body.section.button")
123
+ .getByRole("button", { name: "Click me!" })
124
+ .getNestedLocator();
125
+
126
+ expect(`${updated}`).toEqual(
127
+ "locator('body').locator('section').first().getByRole('button', { name: 'Click me!' }).filter({ hasText: /Click me!/ })",
128
+ );
129
+
130
+ const untouched = testFilters.getLocatorSchema("body.section.button").getNestedLocator();
131
+ expect(`${untouched}`).toEqual("locator('body').locator('section').getByRole('button')");
132
+ });
133
+
134
+ test("update can remove locator options filters", async ({ testFilters }) => {
135
+ const original = testFilters.getNestedLocator("body.section@playground");
136
+ expect(`${original}`).toEqual("locator('body').locator('section').filter({ hasText: /Playground/i })");
137
+
138
+ const locator = testFilters
139
+ .getLocatorSchema("body.section@playground")
140
+ .update("body.section@playground")
141
+ .locator({ hasText: undefined })
142
+ .getNestedLocator();
143
+
144
+ expect(`${locator}`).toEqual("locator('body').locator('section')");
145
+ });
146
+
147
+ test("update can switch locator strategies, caching all latest locator definitions and preserving state of filters and indices", async ({
148
+ page,
149
+ testFilters,
150
+ }) => {
151
+ const path = "body.section" as const;
152
+ const schema = testFilters.getLocatorSchema(path);
153
+
154
+ const initialLocator = schema.getNestedLocator();
155
+ expect(`${initialLocator}`).toEqual("locator('body').locator('section')");
156
+
157
+ const manualLocator = page.locator("body").locator("newSelector").filter({ hasText: "Text" }).first();
158
+ const locator = schema
159
+ .update(path)
160
+ .locator("newSelector")
161
+ .filter(path, { hasText: "Text" })
162
+ .nth(path, 0)
163
+ .getNestedLocator();
164
+ expect(`${locator}`).toEqual(`${manualLocator}`);
165
+
166
+ const manualRole = page
167
+ .locator("body")
168
+ .getByRole("region", { name: "Now a region" })
169
+ .filter({ hasText: "Text" })
170
+ .first();
171
+ const role = schema.update(path).getByRole("region", { name: "Now a region" }).getNestedLocator();
172
+ expect(`${role}`).toEqual(`${manualRole}`);
173
+
174
+ const manualText = page.locator("body").getByText("Text node").filter({ hasText: "Text" }).first();
175
+ const text = schema.update(path).getByText("Text node").getNestedLocator();
176
+ expect(`${text}`).toEqual(`${manualText}`);
177
+
178
+ const manualLabel = page.locator("body").getByLabel("Label").filter({ hasText: "Text" }).first();
179
+ const label = schema.update(path).getByLabel("Label").getNestedLocator();
180
+ expect(`${label}`).toEqual(`${manualLabel}`);
181
+
182
+ const manualPlaceholder = page.locator("body").getByPlaceholder("Placeholder").filter({ hasText: "Text" }).first();
183
+ const placeholder = schema.update(path).getByPlaceholder("Placeholder").getNestedLocator();
184
+ expect(`${placeholder}`).toEqual(`${manualPlaceholder}`);
185
+
186
+ const manualAltText = page.locator("body").getByAltText("Alt").filter({ hasText: "Text" }).first();
187
+ const altText = schema.update(path).getByAltText("Alt").getNestedLocator();
188
+ expect(`${altText}`).toEqual(`${manualAltText}`);
189
+
190
+ const manualTitle = page.locator("body").getByTitle("Title").filter({ hasText: "Text" }).first();
191
+ const title = schema.update(path).getByTitle("Title").getNestedLocator();
192
+ expect(`${title}`).toEqual(`${manualTitle}`);
193
+
194
+ const manualFrameLocator = page.locator("body").locator("iframe[name=child]");
195
+ const frameLocator = schema.update(path).frameLocator("iframe[name=child]").getNestedLocator();
196
+ expect(`${frameLocator}`).toEqual(`${manualFrameLocator}`);
197
+
198
+ const manualTestId = page.locator("body").getByTestId("new-test-id").filter({ hasText: "Text" }).first();
199
+ const testId = schema.update(path).getByTestId("new-test-id").getNestedLocator();
200
+ expect(`${testId}`).toEqual(`${manualTestId}`);
201
+
202
+ const manualId = page.locator("body").locator("#new-id").filter({ hasText: "Text" }).first();
203
+ const id = schema.update(path).getById("new-id").getNestedLocator();
204
+ expect(`${id}`).toEqual(`${manualId}`);
205
+
206
+ const manualDataCy = page.locator("body").locator('[data-cy="new-cy"]').filter({ hasText: "Text" }).first();
207
+ const dataCy = schema.update(path).locator('[data-cy="new-cy"]').getNestedLocator();
208
+ expect(`${dataCy}`).toEqual(`${manualDataCy}`);
209
+
210
+ const resetLocator = schema.update(path).locator().getNestedLocator();
211
+ expect(`${resetLocator}`).toEqual(`${dataCy}`);
212
+
213
+ const resetRole = schema.update(path).getByRole().getNestedLocator();
214
+ expect(`${resetRole}`).toEqual(`${role}`);
215
+
216
+ const resetText = schema.update(path).getByText().getNestedLocator();
217
+ expect(`${resetText}`).toEqual(`${text}`);
218
+
219
+ const resetLabel = schema.update(path).getByLabel().getNestedLocator();
220
+ expect(`${resetLabel}`).toEqual(`${label}`);
221
+
222
+ const resetPlaceholder = schema.update(path).getByPlaceholder().getNestedLocator();
223
+ expect(`${resetPlaceholder}`).toEqual(`${placeholder}`);
224
+
225
+ const resetAltText = schema.update(path).getByAltText().getNestedLocator();
226
+ expect(`${resetAltText}`).toEqual(`${altText}`);
227
+
228
+ const resetTitle = schema.update(path).getByTitle().getNestedLocator();
229
+ expect(`${resetTitle}`).toEqual(`${title}`);
230
+
231
+ const resetFrameLocator = schema.update(path).frameLocator().getNestedLocator();
232
+ expect(`${resetFrameLocator}`).toEqual(`${frameLocator}`);
233
+
234
+ const resetTestId = schema.update(path).getByTestId().getNestedLocator();
235
+ expect(`${resetTestId}`).toEqual(`${testId}`);
236
+
237
+ const resetId = schema.update(path).getById().getNestedLocator();
238
+ expect(`${resetId}`).toEqual(`${id}`);
239
+
240
+ const resetLocatorAgain = schema.update(path).locator().getNestedLocator();
241
+ expect(`${resetLocatorAgain}`).toEqual(`${dataCy}`);
242
+
243
+ const lastRoleClearSteps = schema
244
+ .update(path)
245
+ .getByRole()
246
+ .clearSteps("body")
247
+ .clearSteps("body.section")
248
+ .getNestedLocator();
249
+ expect(`${lastRoleClearSteps}`).toEqual("locator('body').getByRole('region', { name: 'Now a region' })");
250
+
251
+ const stillNoFiltersAndIndicesOnAdditionalSwitch = schema.update(path).locator().getNestedLocator();
252
+ expect(`${stillNoFiltersAndIndicesOnAdditionalSwitch}`).toEqual("locator('body').locator('[data-cy=\"new-cy\"]')");
253
+ });
254
+
255
+ test("update getByRole overloads preserve patch semantics without undefined placeholders", async ({ page }) => {
256
+ type LocalPath = "overload" | "overload.target";
257
+ const registry = new LocatorRegistryInternal<LocalPath>(page);
258
+
259
+ registry.add("overload").locator("body");
260
+ registry.add("overload.target").getByRole("button", { name: "initial" }).filter({ hasText: "initial" }).nth(0);
261
+
262
+ const withRoleAndOptions = await registry
263
+ .getLocatorSchema("overload.target")
264
+ .update("overload.target")
265
+ .getByRole("button", { name: "patched" })
266
+ .filter("overload.target", { hasText: "patched" })
267
+ .nth("overload.target", "last")
268
+ .getNestedLocator();
269
+
270
+ expect(`${withRoleAndOptions}`).toContain("getByRole('button', { name: 'patched' })");
271
+ expect(`${withRoleAndOptions}`).toContain("filter({ hasText: 'patched' })");
272
+ expect(`${withRoleAndOptions}`).toContain("last()");
273
+
274
+ const withOptionsOnly = await registry
275
+ .getLocatorSchema("overload.target")
276
+ .update("overload.target")
277
+ .getByRole({ name: "patched" })
278
+ .getNestedLocator();
279
+
280
+ expect(`${withOptionsOnly}`).toContain("getByRole('button', { name: 'patched' })");
281
+ expect(`${withOptionsOnly}`).toContain("filter({ hasText: 'initial' })");
282
+ expect(`${withOptionsOnly}`).toContain("first()");
283
+ });
284
+
285
+ test("update overloads cover multiple strategies and retain filter/index steps", async ({ page }) => {
286
+ type LocalPath = "update.text" | "update.locator" | "update.frame";
287
+ const registry = new LocatorRegistryInternal<LocalPath>(page);
288
+
289
+ registry.add("update.text").getByText("seed");
290
+ registry.add("update.locator").locator(".seed");
291
+ registry.add("update.frame").frameLocator("iframe[name=seed]");
292
+
293
+ const textPatched = await registry
294
+ .getLocatorSchema("update.text")
295
+ .update("update.text")
296
+ .getByText({ exact: true })
297
+ .filter("update.text", { hasText: "patched" })
298
+ .nth("update.text", 0)
299
+ .getNestedLocator();
300
+
301
+ expect(`${textPatched}`).toEqual("getByText('seed', { exact: true }).filter({ hasText: 'patched' }).first()");
302
+
303
+ const locatorPatched = await registry
304
+ .getLocatorSchema("update.locator")
305
+ .update("update.locator")
306
+ .locator({ hasText: "opt" })
307
+ .filter("update.locator", { hasText: "patched" })
308
+ .nth("update.locator", 0)
309
+ .getNestedLocator();
310
+
311
+ expect(`${locatorPatched}`).toEqual(
312
+ "locator('.seed').filter({ hasText: 'opt' }).filter({ hasText: 'patched' }).first()",
313
+ );
314
+
315
+ const framePatched = await registry
316
+ .getLocatorSchema("update.frame")
317
+ .update("update.frame")
318
+ .frameLocator()
319
+ .getNestedLocator();
320
+
321
+ expect(`${framePatched}`).toEqual("locator('iframe[name=seed]')");
322
+ });