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,309 @@
1
+ import type { Locator, Page } from "@playwright/test";
2
+ import type {
3
+ AltTextDefinition,
4
+ FrameLocatorDefinition,
5
+ IdDefinition,
6
+ IndexSelector,
7
+ LabelDefinition,
8
+ LocatorBuilderTarget,
9
+ LocatorDefinition,
10
+ LocatorStep,
11
+ LocatorStrategyDefinition,
12
+ LocatorStrategyDefinitionPatch,
13
+ PlaceholderDefinition,
14
+ RoleDefinition,
15
+ TestIdDefinition,
16
+ TextDefinition,
17
+ TitleDefinition,
18
+ } from "./types";
19
+
20
+ // Used only at runtime for messages: escape all special chars so
21
+ // whitespace, quotes, etc. are visible and testable.
22
+ export const formatLocatorSchemaPathForError = (path: string): string => {
23
+ // JSON.stringify returns a quoted string literal; strip the outer quotes.
24
+ // Example:
25
+ // path = "\n" -> JSON.stringify -> "\"\\n\"" -> "\\n"
26
+ // path = "a\u00A0b" -> "\"a\\u00a0b\"" -> "a\\u00a0b"
27
+ const json = JSON.stringify(path);
28
+ return json.slice(1, -1);
29
+ };
30
+
31
+ // Precompiled regex for "any JS whitespace OR U+0085"
32
+ const RUNTIME_WHITESPACE_REGEX = /[\s\u0085]/u;
33
+
34
+ export const validateLocatorSchemaPath = (path: string) => {
35
+ if (!path) {
36
+ throw new Error("LocatorSchemaPath string cannot be empty");
37
+ }
38
+
39
+ // Runtime: reject any Unicode whitespace (aligned with compile-time)
40
+ if (RUNTIME_WHITESPACE_REGEX.test(path)) {
41
+ const escaped = formatLocatorSchemaPathForError(path);
42
+ throw new Error(`LocatorSchemaPath string cannot contain whitespace chars: ${escaped}`);
43
+ }
44
+
45
+ if (path.startsWith(".")) {
46
+ throw new Error(`LocatorSchemaPath string cannot start with a dot: ${path}`);
47
+ }
48
+
49
+ if (path.endsWith(".")) {
50
+ throw new Error(`LocatorSchemaPath string cannot end with a dot: ${path}`);
51
+ }
52
+
53
+ if (path.includes("..")) {
54
+ throw new Error(`LocatorSchemaPath string cannot contain consecutive dots: ${path}`);
55
+ }
56
+ };
57
+
58
+ export const expandSchemaPath = (path: string): string[] => {
59
+ validateLocatorSchemaPath(path);
60
+ const parts = path.split(".");
61
+ return parts.map((_part, index) => parts.slice(0, index + 1).join("."));
62
+ };
63
+
64
+ export const cssEscape = (value: string) => {
65
+ // Simple CSS escape implementation covering common cases.
66
+ return value.replace(/([\\"'#.:;,?*+<>{}[\\]()])/g, "\\$1");
67
+ };
68
+
69
+ export const normalizeSteps = <LocatorSchemaPathType extends string, AllowedPaths extends string>(
70
+ steps?: LocatorStep<LocatorSchemaPathType, AllowedPaths>[],
71
+ ) => (steps ? steps.map((step) => ({ ...step })) : []);
72
+
73
+ export function normalizeIdValue(id: string): string;
74
+ export function normalizeIdValue(id: RegExp): RegExp;
75
+ export function normalizeIdValue(id: string | RegExp | undefined): string | RegExp | undefined;
76
+ export function normalizeIdValue(id: string | RegExp | undefined) {
77
+ if (typeof id !== "string") {
78
+ return id;
79
+ }
80
+
81
+ if (id.startsWith("#")) {
82
+ return id.slice(1);
83
+ }
84
+
85
+ if (id.startsWith("id=")) {
86
+ return id.slice("id=".length);
87
+ }
88
+
89
+ return id;
90
+ }
91
+
92
+ export const stringifyForLog = (value: unknown) => {
93
+ const seen = new WeakSet();
94
+ return JSON.stringify(
95
+ value,
96
+ (_key, current) => {
97
+ if (typeof current === "object" && current !== null) {
98
+ if (seen.has(current)) {
99
+ return "[Circular]";
100
+ }
101
+ seen.add(current);
102
+ }
103
+ if (current instanceof RegExp) {
104
+ return { type: "RegExp", source: current.source, flags: current.flags };
105
+ }
106
+ return current;
107
+ },
108
+ 2,
109
+ );
110
+ };
111
+
112
+ export const applyIndexSelector = (locator: Locator, selector: IndexSelector | null | undefined) => {
113
+ if (selector === undefined || selector === null) {
114
+ return locator;
115
+ }
116
+ if (selector === "first") {
117
+ return locator.first();
118
+ }
119
+ if (selector === "last") {
120
+ return locator.last();
121
+ }
122
+ return locator.nth(selector);
123
+ };
124
+
125
+ export const createLocator = (
126
+ target: LocatorBuilderTarget,
127
+ definition: LocatorStrategyDefinition,
128
+ ): Locator | ReturnType<Page["frameLocator"]> => {
129
+ switch (definition.type) {
130
+ case "role":
131
+ return target.getByRole(definition.role, definition.options);
132
+ case "text":
133
+ return target.getByText(definition.text, definition.options);
134
+ case "label":
135
+ return target.getByLabel(definition.text, definition.options);
136
+ case "placeholder":
137
+ return target.getByPlaceholder(definition.text, definition.options);
138
+ case "altText":
139
+ return target.getByAltText(definition.text, definition.options);
140
+ case "title":
141
+ return target.getByTitle(definition.text, definition.options);
142
+ case "locator":
143
+ return target.locator(definition.selector, definition.options);
144
+ case "frameLocator":
145
+ return target.frameLocator(definition.selector);
146
+ case "testId":
147
+ return target.getByTestId(definition.testId);
148
+ case "id": {
149
+ if (typeof definition.id === "string") {
150
+ const normalized = normalizeIdValue(definition.id);
151
+ return target.locator(`#${cssEscape(normalized ?? "")}`);
152
+ }
153
+ const pattern = definition.id.source;
154
+ const safePattern = cssEscape(pattern);
155
+ return target.locator(`[id*="${safePattern}"]`);
156
+ }
157
+ default: {
158
+ const exhaustive: never = definition;
159
+ return exhaustive;
160
+ }
161
+ }
162
+ };
163
+
164
+ export const cloneLocatorStrategyDefinition = (definition: LocatorStrategyDefinition): LocatorStrategyDefinition => {
165
+ switch (definition.type) {
166
+ case "role":
167
+ return {
168
+ type: "role",
169
+ role: definition.role,
170
+ ...(definition.options ? { options: { ...definition.options } } : {}),
171
+ };
172
+ case "text":
173
+ return {
174
+ type: "text",
175
+ text: definition.text,
176
+ ...(definition.options ? { options: { ...definition.options } } : {}),
177
+ };
178
+ case "label":
179
+ return {
180
+ type: "label",
181
+ text: definition.text,
182
+ ...(definition.options ? { options: { ...definition.options } } : {}),
183
+ };
184
+ case "placeholder":
185
+ return {
186
+ type: "placeholder",
187
+ text: definition.text,
188
+ ...(definition.options ? { options: { ...definition.options } } : {}),
189
+ };
190
+ case "altText":
191
+ return {
192
+ type: "altText",
193
+ text: definition.text,
194
+ ...(definition.options ? { options: { ...definition.options } } : {}),
195
+ };
196
+ case "title":
197
+ return {
198
+ type: "title",
199
+ text: definition.text,
200
+ ...(definition.options ? { options: { ...definition.options } } : {}),
201
+ };
202
+ case "locator":
203
+ return {
204
+ type: "locator",
205
+ selector: definition.selector,
206
+ ...(definition.options ? { options: { ...definition.options } } : {}),
207
+ };
208
+ case "frameLocator":
209
+ return { type: "frameLocator", selector: definition.selector };
210
+ case "testId":
211
+ return { type: "testId", testId: definition.testId };
212
+ case "id":
213
+ return { type: "id", id: definition.id };
214
+ default: {
215
+ const exhaustive: never = definition;
216
+ return exhaustive;
217
+ }
218
+ }
219
+ };
220
+
221
+ export const applyDefinitionPatch = (
222
+ seed: LocatorStrategyDefinition,
223
+ patch: LocatorStrategyDefinition | LocatorStrategyDefinitionPatch,
224
+ ): LocatorStrategyDefinition => {
225
+ const base = cloneLocatorStrategyDefinition(seed);
226
+
227
+ switch (patch.type) {
228
+ case "locator": {
229
+ const selector = patch.selector !== undefined ? patch.selector : (base as LocatorDefinition).selector;
230
+ const options =
231
+ patch.options || (base as LocatorDefinition).options
232
+ ? { ...(base as LocatorDefinition).options, ...patch.options }
233
+ : undefined;
234
+ return { type: "locator", selector, ...(options ? { options } : {}) } satisfies LocatorDefinition;
235
+ }
236
+ case "role": {
237
+ const role = patch.role ?? (base as RoleDefinition).role;
238
+ const options =
239
+ patch.options || (base as RoleDefinition).options
240
+ ? { ...(base as RoleDefinition).options, ...patch.options }
241
+ : undefined;
242
+ return { type: "role", role, ...(options ? { options } : {}) } satisfies RoleDefinition;
243
+ }
244
+ case "text": {
245
+ const text = patch.text ?? (base as TextDefinition).text;
246
+ const options =
247
+ patch.options || (base as TextDefinition).options
248
+ ? { ...(base as TextDefinition).options, ...patch.options }
249
+ : undefined;
250
+ return { type: "text", text, ...(options ? { options } : {}) } satisfies TextDefinition;
251
+ }
252
+ case "label": {
253
+ const text = patch.text ?? (base as LabelDefinition).text;
254
+ const options =
255
+ patch.options || (base as LabelDefinition).options
256
+ ? { ...(base as LabelDefinition).options, ...patch.options }
257
+ : undefined;
258
+ return { type: "label", text, ...(options ? { options } : {}) } satisfies LabelDefinition;
259
+ }
260
+ case "placeholder": {
261
+ const text = patch.text ?? (base as PlaceholderDefinition).text;
262
+ const options =
263
+ patch.options || (base as PlaceholderDefinition).options
264
+ ? { ...(base as PlaceholderDefinition).options, ...patch.options }
265
+ : undefined;
266
+ return { type: "placeholder", text, ...(options ? { options } : {}) } satisfies PlaceholderDefinition;
267
+ }
268
+ case "altText": {
269
+ const text = patch.text ?? (base as AltTextDefinition).text;
270
+ const options =
271
+ patch.options || (base as AltTextDefinition).options
272
+ ? { ...(base as AltTextDefinition).options, ...patch.options }
273
+ : undefined;
274
+ return { type: "altText", text, ...(options ? { options } : {}) } satisfies AltTextDefinition;
275
+ }
276
+ case "title": {
277
+ const text = patch.text ?? (base as TitleDefinition).text;
278
+ const options =
279
+ patch.options || (base as TitleDefinition).options
280
+ ? { ...(base as TitleDefinition).options, ...patch.options }
281
+ : undefined;
282
+ return { type: "title", text, ...(options ? { options } : {}) } satisfies TitleDefinition;
283
+ }
284
+ case "frameLocator": {
285
+ const selector = patch.selector !== undefined ? patch.selector : (base as FrameLocatorDefinition).selector;
286
+ return { type: "frameLocator", selector } satisfies FrameLocatorDefinition;
287
+ }
288
+ case "testId": {
289
+ const testId = patch.testId !== undefined ? patch.testId : (base as TestIdDefinition).testId;
290
+ return { type: "testId", testId } satisfies TestIdDefinition;
291
+ }
292
+ case "id": {
293
+ const id =
294
+ patch.id !== undefined ? (normalizeIdValue(patch.id) ?? (base as IdDefinition).id) : (base as IdDefinition).id;
295
+ return { type: "id", id } satisfies IdDefinition;
296
+ }
297
+ default: {
298
+ const exhaustive: never = patch;
299
+ return exhaustive;
300
+ }
301
+ }
302
+ };
303
+
304
+ export const isFrameLocatorDefinition = (definition: LocatorStrategyDefinition): definition is FrameLocatorDefinition =>
305
+ definition.type === "frameLocator";
306
+
307
+ export const isLocatorInstance = (value: unknown): value is Locator => {
308
+ return !!value && typeof value === "object" && typeof (value as Locator).filter === "function";
309
+ };
@@ -0,0 +1,178 @@
1
+ import type { Locator } from "@playwright/test";
2
+ import type { LocatorSchema } from "../../src/helpers/locatorSchema.interface";
3
+ import { GetByMethod } from "../../src/helpers/locatorSchema.interface";
4
+ import type { LocatorRegistry, LocatorRegistryInternal } from "./locatorRegistry";
5
+ import type { RegistryPath } from "./types";
6
+ import { isLocatorInstance, validateLocatorSchemaPath } from "./utils";
7
+
8
+ type RegistryWithLookup<LocatorSchemaPathType extends string> = LocatorRegistryInternal<LocatorSchemaPathType> & {
9
+ getIfExists?: (path: RegistryPath<LocatorSchemaPathType>) => unknown;
10
+ };
11
+
12
+ const getRegistryLookup = <LocatorSchemaPathType extends string>(
13
+ registry: LocatorRegistry<LocatorSchemaPathType>,
14
+ ): RegistryWithLookup<LocatorSchemaPathType> => registry as RegistryWithLookup<LocatorSchemaPathType>;
15
+
16
+ const logMissingDefinition = (path: string, field: string) => {
17
+ console.warn(
18
+ `[POMWright] Skipping v2 translation for "${path}" because "${field}" is missing. ` +
19
+ "Rewrite this locator in defineLocators() using the v2 registry.",
20
+ );
21
+ };
22
+
23
+ const logLocatorInstanceWarning = (path: string) => {
24
+ console.warn(
25
+ `[POMWright] Skipping v2 translation for "${path}" because v1 LocatorSchema.locator is a Locator instance. ` +
26
+ "Rewrite this path in defineLocators() to avoid runtime gaps during migration.",
27
+ );
28
+ };
29
+
30
+ export const addV1SchemaToV2Registry = <LocatorSchemaPathType extends string>(
31
+ registry: LocatorRegistry<LocatorSchemaPathType>,
32
+ locatorSchema: LocatorSchema,
33
+ ) => {
34
+ const path = locatorSchema.locatorSchemaPath;
35
+ validateLocatorSchemaPath(path);
36
+
37
+ const registryWithLookup = getRegistryLookup(registry);
38
+ const existing = registryWithLookup.getIfExists?.(path as RegistryPath<LocatorSchemaPathType>);
39
+ if (existing) {
40
+ return;
41
+ }
42
+
43
+ console.info(
44
+ `[POMWright] LocatorSchemaPath "${path}" is not registered in the v2 registry. ` +
45
+ "Translating and adding v1 schema to v2 Locator Registry; update this path to use registry.add in defineLocators().",
46
+ );
47
+
48
+ const registration = registry.add(path as RegistryPath<LocatorSchemaPathType>);
49
+ if (!registration) {
50
+ return;
51
+ }
52
+
53
+ let postDefinition:
54
+ | ReturnType<typeof registration.getByRole>
55
+ | ReturnType<typeof registration.getByText>
56
+ | ReturnType<typeof registration.getByLabel>
57
+ | ReturnType<typeof registration.getByPlaceholder>
58
+ | ReturnType<typeof registration.getByAltText>
59
+ | ReturnType<typeof registration.getByTitle>
60
+ | ReturnType<typeof registration.locator>
61
+ | ReturnType<typeof registration.frameLocator>
62
+ | ReturnType<typeof registration.getByTestId>
63
+ | ReturnType<typeof registration.getById>
64
+ | null = null;
65
+
66
+ switch (locatorSchema.locatorMethod) {
67
+ case GetByMethod.role: {
68
+ if (!locatorSchema.role) {
69
+ logMissingDefinition(path, "role");
70
+ return;
71
+ }
72
+ postDefinition = registration.getByRole(locatorSchema.role, locatorSchema.roleOptions);
73
+ break;
74
+ }
75
+ case GetByMethod.text: {
76
+ if (!locatorSchema.text) {
77
+ logMissingDefinition(path, "text");
78
+ return;
79
+ }
80
+ postDefinition = registration.getByText(locatorSchema.text, locatorSchema.textOptions);
81
+ break;
82
+ }
83
+ case GetByMethod.label: {
84
+ if (!locatorSchema.label) {
85
+ logMissingDefinition(path, "label");
86
+ return;
87
+ }
88
+ postDefinition = registration.getByLabel(locatorSchema.label, locatorSchema.labelOptions);
89
+ break;
90
+ }
91
+ case GetByMethod.placeholder: {
92
+ if (!locatorSchema.placeholder) {
93
+ logMissingDefinition(path, "placeholder");
94
+ return;
95
+ }
96
+ postDefinition = registration.getByPlaceholder(locatorSchema.placeholder, locatorSchema.placeholderOptions);
97
+ break;
98
+ }
99
+ case GetByMethod.altText: {
100
+ if (!locatorSchema.altText) {
101
+ logMissingDefinition(path, "altText");
102
+ return;
103
+ }
104
+ postDefinition = registration.getByAltText(locatorSchema.altText, locatorSchema.altTextOptions);
105
+ break;
106
+ }
107
+ case GetByMethod.title: {
108
+ if (!locatorSchema.title) {
109
+ logMissingDefinition(path, "title");
110
+ return;
111
+ }
112
+ postDefinition = registration.getByTitle(locatorSchema.title, locatorSchema.titleOptions);
113
+ break;
114
+ }
115
+ case GetByMethod.locator: {
116
+ if (!locatorSchema.locator) {
117
+ logMissingDefinition(path, "locator");
118
+ return;
119
+ }
120
+ if (isLocatorInstance(locatorSchema.locator)) {
121
+ logLocatorInstanceWarning(path);
122
+ return;
123
+ }
124
+ postDefinition = registration.locator(locatorSchema.locator, locatorSchema.locatorOptions);
125
+ break;
126
+ }
127
+ case GetByMethod.frameLocator: {
128
+ if (!locatorSchema.frameLocator) {
129
+ logMissingDefinition(path, "frameLocator");
130
+ return;
131
+ }
132
+ postDefinition = registration.frameLocator(locatorSchema.frameLocator);
133
+ break;
134
+ }
135
+ case GetByMethod.testId: {
136
+ if (!locatorSchema.testId) {
137
+ logMissingDefinition(path, "testId");
138
+ return;
139
+ }
140
+ postDefinition = registration.getByTestId(locatorSchema.testId);
141
+ break;
142
+ }
143
+ case GetByMethod.dataCy: {
144
+ if (!locatorSchema.dataCy) {
145
+ logMissingDefinition(path, "dataCy");
146
+ return;
147
+ }
148
+ postDefinition = registration.locator(`[data-cy="${locatorSchema.dataCy}"]`);
149
+ break;
150
+ }
151
+ case GetByMethod.id: {
152
+ if (!locatorSchema.id) {
153
+ logMissingDefinition(path, "id");
154
+ return;
155
+ }
156
+ postDefinition = registration.getById(locatorSchema.id);
157
+ break;
158
+ }
159
+ default: {
160
+ const exhaustive: never = locatorSchema.locatorMethod;
161
+ return exhaustive;
162
+ }
163
+ }
164
+
165
+ if (!postDefinition) {
166
+ return;
167
+ }
168
+
169
+ if (locatorSchema.filter && locatorSchema.locatorMethod !== GetByMethod.frameLocator) {
170
+ const filter = locatorSchema.filter as {
171
+ has?: Locator;
172
+ hasNot?: Locator;
173
+ hasText?: string | RegExp;
174
+ hasNotText?: string | RegExp;
175
+ };
176
+ postDefinition.filter(filter);
177
+ }
178
+ };
@@ -0,0 +1,105 @@
1
+ import type { Page } from "@playwright/test";
2
+ import { createNavigation, type ExtractNavigationType, type NavigationOptions } from "./helpers/navigation";
3
+ import { SessionStorage } from "./helpers/sessionStorage";
4
+ import {
5
+ type AddAccessor,
6
+ createRegistryWithAccessors,
7
+ type GetLocatorAccessor,
8
+ type GetLocatorSchemaAccessor,
9
+ type GetNestedLocatorAccessor,
10
+ type LocatorRegistry,
11
+ } from "./locators";
12
+
13
+ /**
14
+ * UrlTypeOptions define types for baseUrl and urlPath.
15
+ * string is the default type; The types can be overridden to RegExp, using the Extract...Type utility types. If either
16
+ * baseUrlType or urlPathType is set to RegExp, then fullUrlType will also be RegExp and can't be used for navigation,
17
+ * only validation through URL matching.
18
+ */
19
+ export type UrlTypeOptions = {
20
+ baseUrlType?: string | RegExp;
21
+ urlPathType?: string | RegExp;
22
+ };
23
+
24
+ export type BaseUrlTypeFromOptions<T extends UrlTypeOptions> = T extends { baseUrlType: RegExp } ? RegExp : string;
25
+ export type UrlPathTypeFromOptions<T extends UrlTypeOptions> = T extends { urlPathType: RegExp } ? RegExp : "" | string;
26
+ export type FullUrlTypeFromOptions<T extends UrlTypeOptions> = T extends
27
+ | { baseUrlType: RegExp }
28
+ | { urlPathType: RegExp }
29
+ ? RegExp
30
+ : string;
31
+
32
+ export abstract class PageObject<
33
+ LocatorSchemaPathType extends string,
34
+ Options extends UrlTypeOptions = { baseUrlType: string; urlPathType: string },
35
+ > {
36
+ readonly page: Page;
37
+ readonly baseUrl: BaseUrlTypeFromOptions<Options>;
38
+ readonly urlPath: UrlPathTypeFromOptions<Options>;
39
+ readonly fullUrl: FullUrlTypeFromOptions<Options>;
40
+ readonly label: string;
41
+ readonly sessionStorage: SessionStorage;
42
+ public readonly navigation: ExtractNavigationType<FullUrlTypeFromOptions<Options>>;
43
+ protected readonly locatorRegistry: LocatorRegistry<LocatorSchemaPathType>;
44
+ public readonly add: AddAccessor<LocatorSchemaPathType>;
45
+ public readonly getLocator: GetLocatorAccessor<LocatorSchemaPathType>;
46
+ public readonly getLocatorSchema: GetLocatorSchemaAccessor<LocatorSchemaPathType>;
47
+ public readonly getNestedLocator: GetNestedLocatorAccessor<LocatorSchemaPathType>;
48
+
49
+ protected constructor(
50
+ page: Page,
51
+ baseUrl: BaseUrlTypeFromOptions<Options>,
52
+ urlPath: UrlPathTypeFromOptions<Options>,
53
+ options?: { label?: string; navOptions?: NavigationOptions },
54
+ ) {
55
+ this.page = page;
56
+ this.baseUrl = baseUrl;
57
+ this.urlPath = urlPath;
58
+ this.fullUrl = this.composeFullUrl(baseUrl, urlPath);
59
+ const label = options?.label ?? this.constructor.name;
60
+ this.label = label;
61
+ const { registry, add, getLocator, getNestedLocator, getLocatorSchema } =
62
+ createRegistryWithAccessors<LocatorSchemaPathType>(page);
63
+ this.locatorRegistry = registry;
64
+ this.add = add;
65
+ this.getLocator = getLocator;
66
+ this.getLocatorSchema = getLocatorSchema;
67
+ this.getNestedLocator = getNestedLocator;
68
+ this.sessionStorage = new SessionStorage(page, { label });
69
+
70
+ this.defineLocators();
71
+ this.navigation = createNavigation(
72
+ this.page,
73
+ this.baseUrl,
74
+ this.urlPath,
75
+ this.fullUrl,
76
+ this.label,
77
+ this.pageActionsToPerformAfterNavigation(),
78
+ options?.navOptions,
79
+ );
80
+ }
81
+
82
+ protected abstract defineLocators(): void;
83
+ protected abstract pageActionsToPerformAfterNavigation(): (() => Promise<void>)[] | null;
84
+
85
+ private composeFullUrl(
86
+ baseUrl: BaseUrlTypeFromOptions<Options>,
87
+ urlPath: UrlPathTypeFromOptions<Options>,
88
+ ): FullUrlTypeFromOptions<Options> {
89
+ const escapeRegex = (value: string) => value.replace(/[-/\\^$*+?.()|[\]{}]/g, "\\$&");
90
+
91
+ if (typeof baseUrl === "string" && typeof urlPath === "string") {
92
+ return `${baseUrl}${urlPath}` as FullUrlTypeFromOptions<Options>;
93
+ }
94
+ if (typeof baseUrl === "string" && urlPath instanceof RegExp) {
95
+ return new RegExp(`^${escapeRegex(baseUrl)}${urlPath.source}`) as FullUrlTypeFromOptions<Options>;
96
+ }
97
+ if (baseUrl instanceof RegExp && typeof urlPath === "string") {
98
+ return new RegExp(`${baseUrl.source}${escapeRegex(urlPath)}$`) as FullUrlTypeFromOptions<Options>;
99
+ }
100
+ if (baseUrl instanceof RegExp && urlPath instanceof RegExp) {
101
+ return new RegExp(`${baseUrl.source}${urlPath.source}`) as FullUrlTypeFromOptions<Options>;
102
+ }
103
+ throw new Error("Invalid baseUrl or urlPath types. Expected string or RegExp.");
104
+ }
105
+ }