pw-core 1.2.0 → 1.3.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 (86) hide show
  1. package/README.md +1 -1
  2. package/dist/cli.d.ts +2 -0
  3. package/dist/cli.js +777 -0
  4. package/dist/codegen/config/context-weights.d.ts +8 -0
  5. package/dist/codegen/config/context-weights.js +11 -0
  6. package/dist/codegen/config/ignored-attributes.d.ts +1 -0
  7. package/dist/codegen/config/ignored-attributes.js +4 -0
  8. package/dist/codegen/config/ignored-classes.d.ts +1 -0
  9. package/dist/codegen/config/ignored-classes.js +144 -0
  10. package/dist/codegen/config/ignored-values.d.ts +1 -0
  11. package/dist/codegen/config/ignored-values.js +4 -0
  12. package/dist/codegen/config/index.d.ts +29 -0
  13. package/dist/codegen/config/index.js +17 -0
  14. package/dist/codegen/config/strategy-order.d.ts +1 -0
  15. package/dist/codegen/config/strategy-order.js +18 -0
  16. package/dist/codegen/config/weights.d.ts +15 -0
  17. package/dist/codegen/config/weights.js +18 -0
  18. package/dist/codegen/floating-panel/client.d.ts +1 -0
  19. package/dist/codegen/floating-panel/client.js +248 -0
  20. package/dist/codegen/floating-panel/index.d.ts +4 -0
  21. package/dist/codegen/floating-panel/index.js +20 -0
  22. package/dist/codegen/floating-panel/manager.d.ts +19 -0
  23. package/dist/codegen/floating-panel/manager.js +90 -0
  24. package/dist/codegen/floating-panel/template.d.ts +2 -0
  25. package/dist/codegen/floating-panel/template.js +58 -0
  26. package/dist/codegen/floating-panel/types.d.ts +7 -0
  27. package/dist/codegen/floating-panel/types.js +2 -0
  28. package/dist/codegen/generator/action.validator.d.ts +5 -0
  29. package/dist/codegen/generator/action.validator.js +27 -0
  30. package/dist/codegen/generator/index.d.ts +13 -0
  31. package/dist/codegen/generator/index.js +724 -0
  32. package/dist/codegen/generator/locator.restricted.d.ts +36 -0
  33. package/dist/codegen/generator/locator.restricted.js +127 -0
  34. package/dist/codegen/generator/locator.validator.d.ts +8 -0
  35. package/dist/codegen/generator/locator.validator.js +38 -0
  36. package/dist/codegen/hover-tracker/client.d.ts +1 -0
  37. package/dist/codegen/hover-tracker/client.js +75 -0
  38. package/dist/codegen/hover-tracker/index.d.ts +3 -0
  39. package/dist/codegen/hover-tracker/index.js +19 -0
  40. package/dist/codegen/hover-tracker/manager.d.ts +11 -0
  41. package/dist/codegen/hover-tracker/manager.js +28 -0
  42. package/dist/codegen/hover-tracker/types.d.ts +3 -0
  43. package/dist/codegen/hover-tracker/types.js +2 -0
  44. package/dist/codegen/index.d.ts +35 -0
  45. package/dist/codegen/index.js +1147 -0
  46. package/dist/codegen/key-utils.d.ts +23 -0
  47. package/dist/codegen/key-utils.js +68 -0
  48. package/dist/codegen/scorer/base-score.d.ts +1 -0
  49. package/dist/codegen/scorer/base-score.js +7 -0
  50. package/dist/codegen/scorer/context-score.d.ts +2 -0
  51. package/dist/codegen/scorer/context-score.js +27 -0
  52. package/dist/codegen/scorer/index.d.ts +5 -0
  53. package/dist/codegen/scorer/index.js +23 -0
  54. package/dist/codegen/scorer/locator.ranking.d.ts +15 -0
  55. package/dist/codegen/scorer/locator.ranking.js +83 -0
  56. package/dist/codegen/scorer/semantic-score.d.ts +1 -0
  57. package/dist/codegen/scorer/semantic-score.js +47 -0
  58. package/dist/codegen/types.d.ts +35 -0
  59. package/dist/codegen/types.js +2 -0
  60. package/dist/component/table.js +14 -10
  61. package/dist/constants/methods.d.ts +2 -0
  62. package/dist/constants/methods.js +33 -0
  63. package/dist/constants/strategies.d.ts +2 -0
  64. package/dist/constants/strategies.js +47 -0
  65. package/dist/page/actions/locator-actions.js +15 -4
  66. package/dist/page/assertions/verify-chain.d.ts +39 -32
  67. package/dist/page/assertions/verify-chain.js +18 -4
  68. package/dist/page/assertions/verify-helpers.js +12 -3
  69. package/dist/page/config.d.ts +109 -71
  70. package/dist/page/config.js +10 -31
  71. package/dist/page/locators/dynamic-locator-resolver.js +29 -9
  72. package/dist/page/locators/resolver.d.ts +15 -15
  73. package/dist/page/locators/resolver.js +151 -32
  74. package/dist/page/registry.d.ts +43 -6
  75. package/dist/page/registry.js +90 -18
  76. package/dist/page/typed-page.d.ts +33 -29
  77. package/dist/page/typed-page.js +40 -11
  78. package/dist/page/types/proxy-methods.d.ts +69 -54
  79. package/dist/page/types/validation.d.ts +48 -1
  80. package/dist/page/utils/caller-location.js +13 -9
  81. package/dist/page/utils/formatter.js +6 -3
  82. package/dist/types/methods.d.ts +4 -0
  83. package/dist/types/methods.js +2 -0
  84. package/dist/types/strategies.d.ts +17 -0
  85. package/dist/types/strategies.js +2 -0
  86. package/package.json +8 -1
@@ -1,5 +1,8 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.lookupIndexCache = exports.locatorStrategies = void 0;
4
+ exports.cleanKey = cleanKey;
5
+ exports.buildLookupIndex = buildLookupIndex;
3
6
  exports.defineLocators = defineLocators;
4
7
  exports.resolveLocator = resolveLocator;
5
8
  exports.locator = locator;
@@ -9,51 +12,162 @@ const config_1 = require("../config");
9
12
  const formatter_1 = require("../utils/formatter");
10
13
  const dynamic_locator_resolver_1 = require("./dynamic-locator-resolver");
11
14
  const caller_location_1 = require("../utils/caller-location");
12
- function defineLocators(instance, context, config) {
13
- if (config.testIds) {
14
- const expanded = (0, dynamic_locator_resolver_1.getExpandedTestIds)(config.testIds);
15
+ exports.locatorStrategies = {
16
+ testId: { resolver: 'getByTestId' },
17
+ selector: { resolver: 'locator' },
18
+ text: { resolver: 'getByText' },
19
+ label: { resolver: 'getByLabel' },
20
+ title: { resolver: 'getByTitle' },
21
+ placeholder: { resolver: 'getByPlaceholder' },
22
+ altText: { resolver: 'getByAltText' }
23
+ };
24
+ // Add all roles dynamically
25
+ for (const role of config_1.rolesList) {
26
+ exports.locatorStrategies[role] = { resolver: 'getByRole', role };
27
+ }
28
+ function cleanKey(s) {
29
+ let clean = s;
30
+ const suffixes = ['Btn', 'Button', 'Link', 'Input', 'Checkbox', 'Option', 'Title', 'Label', 'Value', 'Text', 'Wrapper', 'Container', 'Card', 'Item'];
31
+ for (const suffix of suffixes) {
32
+ clean = clean.replace(new RegExp(`[-_]?${suffix}$`, 'i'), '');
33
+ }
34
+ clean = clean.replace(/[^a-zA-Z0-9]/g, '').toLowerCase();
35
+ clean = clean.replace(/^\d+/, '').replace(/\d+$/, '');
36
+ return clean;
37
+ }
38
+ exports.lookupIndexCache = new WeakMap();
39
+ function buildLookupIndex(config) {
40
+ const index = new Map();
41
+ const seenInStrategies = new Map();
42
+ const addKey = (key, strategy, value) => {
43
+ const cleanedKey = cleanKey(key);
44
+ if (seenInStrategies.has(cleanedKey)) {
45
+ const existing = seenInStrategies.get(cleanedKey);
46
+ if (!existing.includes(strategy)) {
47
+ existing.push(strategy);
48
+ }
49
+ }
50
+ else {
51
+ seenInStrategies.set(cleanedKey, [strategy]);
52
+ }
53
+ index.set(cleanedKey, { strategy, value });
54
+ };
55
+ // 1. Process testId / testIds (backward compatibility)
56
+ const testIdConfig = config.testId || config.testIds;
57
+ if (testIdConfig) {
58
+ const expanded = (0, dynamic_locator_resolver_1.getExpandedTestIds)(testIdConfig);
15
59
  for (const key of Object.keys(expanded)) {
16
- Object.defineProperty(instance, key, {
17
- get: () => {
18
- return context.getByTestId(expanded[key]);
19
- },
20
- enumerable: true,
21
- configurable: true,
22
- });
60
+ addKey(key, 'testId', expanded[key]);
23
61
  }
24
62
  }
25
- if (config.selectors) {
26
- const expanded = (0, dynamic_locator_resolver_1.getExpandedTestIds)(config.selectors);
63
+ // 2. Process selector / selectors (backward compatibility)
64
+ const selectorConfig = config.selector || config.selectors;
65
+ if (selectorConfig) {
66
+ const expanded = (0, dynamic_locator_resolver_1.getExpandedTestIds)(selectorConfig);
27
67
  for (const key of Object.keys(expanded)) {
28
- Object.defineProperty(instance, key, {
29
- get: () => {
30
- return context.locator(expanded[key]);
31
- },
32
- enumerable: true,
33
- configurable: true,
34
- });
68
+ addKey(key, 'selector', expanded[key]);
69
+ }
70
+ }
71
+ // 3. Process all other strategies
72
+ for (const strategy of config_1.strategyList) {
73
+ if (strategy === 'testId' || strategy === 'selector')
74
+ continue;
75
+ const items = config[strategy];
76
+ if (Array.isArray(items)) {
77
+ for (const item of items) {
78
+ if (typeof item === 'string') {
79
+ addKey(item, strategy, item);
80
+ }
81
+ }
35
82
  }
36
83
  }
84
+ // 4. Duplicate Check & Validation
85
+ for (const [key, strategies] of seenInStrategies.entries()) {
86
+ if (strategies.length > 1) {
87
+ throw new Error(`Duplicate locator value "${key}"\n\nFound in\n\n${strategies.join('\n\n')}\n\nLocator values must be globally unique.`);
88
+ }
89
+ }
90
+ return index;
91
+ }
92
+ function defineLocators(instance, context, config) {
93
+ let index = exports.lookupIndexCache.get(config);
94
+ if (!index) {
95
+ index = buildLookupIndex(config);
96
+ exports.lookupIndexCache.set(config, index);
97
+ }
98
+ for (const key of index.keys()) {
99
+ Object.defineProperty(instance, key, {
100
+ get: () => {
101
+ return resolveLocator(context, config, key);
102
+ },
103
+ enumerable: true,
104
+ configurable: true
105
+ });
106
+ }
37
107
  }
38
108
  function resolveLocator(context, config, target, options) {
39
109
  if (typeof target !== 'string')
40
110
  return target;
41
111
  const targetStr = target;
42
- const parts = targetStr.split('.');
43
- const expandedTestIds = config.testIds ? (0, dynamic_locator_resolver_1.getExpandedTestIds)(config.testIds) : undefined;
44
- const expandedSelectors = config.selectors ? (0, dynamic_locator_resolver_1.getExpandedTestIds)(config.selectors) : undefined;
112
+ let index = exports.lookupIndexCache.get(config);
113
+ if (!index) {
114
+ index = buildLookupIndex(config);
115
+ exports.lookupIndexCache.set(config, index);
116
+ }
117
+ const toRegex = (val) => {
118
+ const escaped = val.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&');
119
+ return new RegExp(escaped.replace(/\s+/g, '\\s+'), 'i');
120
+ };
45
121
  const resolveSingle = (key, ctx) => {
46
- if (expandedTestIds && key in expandedTestIds) {
47
- return ctx.getByTestId(expandedTestIds[key]);
122
+ const cleaned = cleanKey(key);
123
+ const meta = index.get(cleaned);
124
+ if (!meta) {
125
+ throw new Error(`Locator key '${key}' is not defined in page object configuration.`);
126
+ }
127
+ const strategy = exports.locatorStrategies[meta.strategy];
128
+ if (!strategy) {
129
+ throw new Error(`Unknown locator strategy '${meta.strategy}' for key '${key}'.`);
130
+ }
131
+ const { resolver, role } = strategy;
132
+ if (resolver === 'getByRole') {
133
+ const val = typeof meta.value === 'string' ? toRegex(meta.value) : meta.value;
134
+ const roleOptions = { name: val };
135
+ if (options) {
136
+ const allowedOptions = ['exact', 'checked', 'disabled', 'expanded', 'includeHidden', 'level', 'pressed', 'selected'];
137
+ for (const opt of allowedOptions) {
138
+ if (opt in options) {
139
+ roleOptions[opt] = options[opt];
140
+ }
141
+ }
142
+ }
143
+ return ctx[resolver](role, roleOptions);
48
144
  }
49
- if (expandedSelectors && key in expandedSelectors) {
50
- return ctx.locator(expandedSelectors[key]);
145
+ else if (resolver === 'locator') {
146
+ return ctx[resolver](meta.value);
147
+ }
148
+ else {
149
+ const opt = {};
150
+ if (options && 'exact' in options) {
151
+ opt.exact = options.exact;
152
+ }
153
+ const val = typeof meta.value === 'string' ? toRegex(meta.value) : meta.value;
154
+ return ctx[resolver](val, opt);
51
155
  }
52
- throw new Error(`Locator key '${key}' is not defined in testIds or selectors.`);
53
156
  };
54
- let loc = resolveSingle(parts[0], context);
55
- for (let i = 1; i < parts.length; i++) {
56
- loc = resolveSingle(parts[i], loc);
157
+ let loc;
158
+ const cleanedTarget = cleanKey(targetStr);
159
+ if (index.has(cleanedTarget)) {
160
+ loc = resolveSingle(targetStr, context);
161
+ }
162
+ else if (targetStr.includes('.')) {
163
+ const parts = targetStr.split('.');
164
+ loc = resolveSingle(parts[0], context);
165
+ for (let i = 1; i < parts.length; i++) {
166
+ loc = resolveSingle(parts[i], loc);
167
+ }
168
+ }
169
+ else {
170
+ loc = resolveSingle(targetStr, context);
57
171
  }
58
172
  let resolved = loc;
59
173
  if (options?.hasText !== undefined) {
@@ -73,7 +187,9 @@ function resolveLocator(context, config, target, options) {
73
187
  function locator(context, config, target, options) {
74
188
  const resolved = resolveLocator(context, config, target, { raw: true });
75
189
  const filtered = options ? resolved.filter(options) : resolved;
76
- const loc = (options && typeof options === 'object' && 'nth' in options && options.nth !== undefined) ? filtered.nth(options.nth) : filtered;
190
+ const loc = options && typeof options === 'object' && 'nth' in options && options.nth !== undefined
191
+ ? filtered.nth(options.nth)
192
+ : filtered;
77
193
  return wrapLocatorWithProxy(loc);
78
194
  }
79
195
  function wrapLocatorWithProxy(loc) {
@@ -116,7 +232,10 @@ function wrapLocatorWithProxy(loc) {
116
232
  }, { box: true, location: (0, caller_location_1.getCallerLocation)() });
117
233
  }
118
234
  }
119
- return test_1.test.step(stepName, () => val.apply(targetLoc, args), { box: true, location: (0, caller_location_1.getCallerLocation)() });
235
+ return test_1.test.step(stepName, () => val.apply(targetLoc, args), {
236
+ box: true,
237
+ location: (0, caller_location_1.getCallerLocation)()
238
+ });
120
239
  };
121
240
  }
122
241
  return val;
@@ -6,17 +6,27 @@ export interface PageConstructor<C extends PageConfig> {
6
6
  new (page: Page): TypedPage<C>;
7
7
  }
8
8
  export type PageRegistryTest<T extends Record<string, PageConfig>, P, W, O = {}> = TestType<P & {
9
- [K in keyof T]: K extends keyof O ? O[K] extends new (page: Page, ...args: any[]) => infer R ? R : NonNullable<T[K]['Class']> extends new (page: Page, ...args: any[]) => infer R2 ? R2 : TypedPage<T[K]> : NonNullable<T[K]['Class']> extends new (page: Page, ...args: any[]) => infer R ? R : TypedPage<T[K]>;
9
+ [K in keyof T]: K extends keyof O ? O[K] extends new (page: Page, ...args: any[]) => infer R ? R : TypedPage<T[K]> : TypedPage<T[K]>;
10
+ } & {
11
+ [K in keyof T as K extends `worker${infer R}` ? Uncapitalize<R> : never]: K extends keyof O ? O[K] extends new (page: Page, ...args: any[]) => infer R ? R : TypedPage<T[K]> : TypedPage<T[K]>;
10
12
  }, W & {
11
- [K in keyof T as `worker${Capitalize<K & string>}`]: K extends keyof O ? O[K] extends new (page: Page, ...args: any[]) => infer R ? R : NonNullable<T[K]['Class']> extends new (page: Page, ...args: any[]) => infer R2 ? R2 : TypedPage<T[K]> : NonNullable<T[K]['Class']> extends new (page: Page, ...args: any[]) => infer R ? R : TypedPage<T[K]>;
13
+ [K in keyof T as K extends `worker${string}` ? K : `worker${Capitalize<K & string>}`]: K extends keyof O ? O[K] extends new (page: Page, ...args: any[]) => infer R ? R : TypedPage<T[K]> : TypedPage<T[K]>;
12
14
  } & {
13
15
  workerPage: Page;
14
16
  }> & {
15
17
  pages: {
16
- [K in keyof T]: K extends keyof O ? O[K] : NonNullable<T[K]['Class']> extends new (page: Page, ...args: any[]) => any ? NonNullable<T[K]['Class']> : PageConstructor<T[K]>;
18
+ [K in keyof T]: K extends keyof O ? O[K] : PageConstructor<T[K]>;
19
+ } & {
20
+ [K in keyof T as K extends `worker${infer R}` ? Uncapitalize<R> : never]: K extends keyof O ? O[K] : PageConstructor<T[K]>;
21
+ } & {
22
+ [K in keyof T as K extends `worker${string}` ? K : `worker${Capitalize<K & string>}`]: K extends keyof O ? O[K] : PageConstructor<T[K]>;
17
23
  };
18
24
  classes: {
19
- [K in keyof T]: K extends keyof O ? O[K] : NonNullable<T[K]['Class']> extends new (page: Page, ...args: any[]) => any ? NonNullable<T[K]['Class']> : PageConstructor<T[K]>;
25
+ [K in keyof T]: K extends keyof O ? O[K] : PageConstructor<T[K]>;
26
+ } & {
27
+ [K in keyof T as K extends `worker${infer R}` ? Uncapitalize<R> : never]: K extends keyof O ? O[K] : PageConstructor<T[K]>;
28
+ } & {
29
+ [K in keyof T as K extends `worker${string}` ? K : `worker${Capitalize<K & string>}`]: K extends keyof O ? O[K] : PageConstructor<T[K]>;
20
30
  };
21
31
  extend<O2 extends Partial<{
22
32
  [K in keyof T]: new (page: Page, ...args: any[]) => any;
@@ -33,8 +43,35 @@ export type PageRegistryTest<T extends Record<string, PageConfig>, P, W, O = {}>
33
43
  *
34
44
  * // returns the extended test runner directly
35
45
  * const test = createPageRegistry({
36
- * login: { ...config, Class: LoginPage },
37
- * });
46
+ codegenPage: {
47
+ url: '/',
48
+ selectors: {
49
+ '{item}': {
50
+ item: ['why-pw-core', 'why-not-playwright', 'registry', 'typed-page', 'features'],
51
+ selector: '[data-parent-id="{item}"]'
52
+ },
53
+ copyToClipboardBtn: '.page',
54
+ dynamicLocators: 'text="Dynamic Locators" >> nth=0',
55
+ rootrailthumbright: '#rootRailThumbRight',
56
+ 'text{item}': {
57
+ item: ['Auto Steps', 'Capabilities >> nth=0', 'Secret Masking'],
58
+ selector: 'text="{item}"'
59
+ },
60
+ tshirts: '.title-title',
61
+ tShirts: 'internal:role=link[name="T-Shirts"s]'
62
+ },
63
+ testIds: {
64
+ qaWorkspaceBtn: 'active-workspace-btn',
65
+ wsOptionApp: 'ws-option-app'
66
+ }
67
+ },
68
+ blankPage: {
69
+ url: '/',
70
+ selectors: {
71
+ element: '[data-pw-cursor="pointer"]'
72
+ }
73
+ }
74
+ });
38
75
  *
39
76
  * // returns page classes only when accessing .pages
40
77
  * const pages = createPageRegistry({
@@ -6,43 +6,86 @@ Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.createPageRegistry = createPageRegistry;
7
7
  const test_1 = __importDefault(require("@playwright/test"));
8
8
  const typed_page_1 = require("./typed-page");
9
+ const resolver_1 = require("./locators/resolver");
9
10
  function createPageRegistryWithClasses(registry, classes, base) {
10
11
  const fixtures = {
11
12
  // Shared worker context and page
12
- workerContext: [async ({ browser }, use) => {
13
+ workerContext: [
14
+ async ({ browser }, use) => {
13
15
  const context = await browser.newContext();
14
16
  await use(context);
15
17
  await context.close();
16
- }, { scope: 'worker' }],
17
- workerPage: [async ({ workerContext }, use) => {
18
+ },
19
+ { scope: 'worker' }
20
+ ],
21
+ workerPage: [
22
+ async ({ workerContext }, use) => {
18
23
  const page = await workerContext.newPage();
19
24
  await use(page);
20
- }, { scope: 'worker' }]
25
+ },
26
+ { scope: 'worker' }
27
+ ]
21
28
  };
22
29
  for (const key of Object.keys(registry)) {
30
+ // Page fixture for key
23
31
  fixtures[key] = async ({ page }, use) => {
24
32
  const PageClass = classes[key];
25
33
  const instance = new PageClass(page);
26
34
  await use(instance);
27
35
  };
28
- const workerKey = `worker${key.charAt(0).toUpperCase()}${key.slice(1)}`;
29
- fixtures[workerKey] = [async ({ workerPage }, use) => {
36
+ // Page fixture for un-prefixed key (if key starts with worker)
37
+ if (key.startsWith('worker')) {
38
+ const unPrefixed = key.slice(6).charAt(0).toLowerCase() + key.slice(7);
39
+ if (unPrefixed && !fixtures[unPrefixed]) {
40
+ fixtures[unPrefixed] = async ({ page }, use) => {
41
+ const PageClass = classes[key];
42
+ const instance = new PageClass(page);
43
+ await use(instance);
44
+ };
45
+ }
46
+ }
47
+ // Worker fixture for worker-prefixed key
48
+ const workerKey = key.startsWith('worker') ? key : `worker${key.charAt(0).toUpperCase()}${key.slice(1)}`;
49
+ fixtures[workerKey] = [
50
+ async ({ workerPage }, use) => {
30
51
  const PageClass = classes[key];
31
52
  const instance = new PageClass(workerPage);
32
53
  await use(instance);
33
- }, { scope: 'worker' }];
54
+ },
55
+ { scope: 'worker' }
56
+ ];
57
+ }
58
+ // Copy worker and non-worker aliases in pages and classes dictionaries
59
+ const extendedClasses = { ...classes };
60
+ for (const key of Object.keys(classes)) {
61
+ if (key.startsWith('worker')) {
62
+ const unPrefixed = key.slice(6).charAt(0).toLowerCase() + key.slice(7);
63
+ if (unPrefixed && !extendedClasses[unPrefixed]) {
64
+ extendedClasses[unPrefixed] = classes[key];
65
+ }
66
+ }
67
+ else {
68
+ const workerKey = `worker${key.charAt(0).toUpperCase()}${key.slice(1)}`;
69
+ if (!extendedClasses[workerKey]) {
70
+ extendedClasses[workerKey] = classes[key];
71
+ }
72
+ }
34
73
  }
35
74
  const testRunner = base.extend(fixtures);
36
- testRunner.pages = classes;
37
- testRunner.classes = classes;
75
+ testRunner.pages = extendedClasses;
76
+ testRunner.classes = extendedClasses;
38
77
  const originalExtend = testRunner.extend.bind(testRunner);
39
78
  testRunner.extend = (overrides, customBase = test_1.default) => {
40
79
  if (!overrides) {
41
80
  return originalExtend();
42
81
  }
43
- const isPlaywrightFixtures = Object.values(overrides).every(val => (typeof val === 'function' && !val.toString().startsWith('class')) || Array.isArray(val));
82
+ const isPlaywrightFixtures = Object.values(overrides).every((val) => (typeof val === 'function' && !val.toString().startsWith('class')) || Array.isArray(val));
44
83
  if (isPlaywrightFixtures) {
45
- return originalExtend(overrides);
84
+ const runner = originalExtend(overrides);
85
+ runner.pages = extendedClasses;
86
+ runner.classes = extendedClasses;
87
+ runner.extend = testRunner.extend;
88
+ return runner;
46
89
  }
47
90
  const newClasses = { ...classes };
48
91
  for (const key of Object.keys(overrides)) {
@@ -65,8 +108,35 @@ function createPageRegistryWithClasses(registry, classes, base) {
65
108
  *
66
109
  * // returns the extended test runner directly
67
110
  * const test = createPageRegistry({
68
- * login: { ...config, Class: LoginPage },
69
- * });
111
+ codegenPage: {
112
+ url: '/',
113
+ selectors: {
114
+ '{item}': {
115
+ item: ['why-pw-core', 'why-not-playwright', 'registry', 'typed-page', 'features'],
116
+ selector: '[data-parent-id="{item}"]'
117
+ },
118
+ copyToClipboardBtn: '.page',
119
+ dynamicLocators: 'text="Dynamic Locators" >> nth=0',
120
+ rootrailthumbright: '#rootRailThumbRight',
121
+ 'text{item}': {
122
+ item: ['Auto Steps', 'Capabilities >> nth=0', 'Secret Masking'],
123
+ selector: 'text="{item}"'
124
+ },
125
+ tshirts: '.title-title',
126
+ tShirts: 'internal:role=link[name="T-Shirts"s]'
127
+ },
128
+ testIds: {
129
+ qaWorkspaceBtn: 'active-workspace-btn',
130
+ wsOptionApp: 'ws-option-app'
131
+ }
132
+ },
133
+ blankPage: {
134
+ url: '/',
135
+ selectors: {
136
+ element: '[data-pw-cursor="pointer"]'
137
+ }
138
+ }
139
+ });
70
140
  *
71
141
  * // returns page classes only when accessing .pages
72
142
  * const pages = createPageRegistry({
@@ -87,11 +157,13 @@ function createPageRegistry(registry) {
87
157
  const classes = {};
88
158
  for (const key of Object.keys(registry)) {
89
159
  const config = registry[key];
90
- classes[key] = config.Class || (class extends typed_page_1.TypedPage {
91
- constructor(page) {
92
- super(page, config);
93
- }
94
- });
160
+ (0, resolver_1.buildLookupIndex)(config);
161
+ classes[key] =
162
+ class extends typed_page_1.TypedPage {
163
+ constructor(page) {
164
+ super(page, config);
165
+ }
166
+ };
95
167
  }
96
168
  return createPageRegistryWithClasses(registry, classes, test_1.default);
97
169
  }
@@ -1,12 +1,7 @@
1
1
  import { Page, Locator, expect as playwrightExpect } from '@playwright/test';
2
- import { TypedLocators, ProxyLocatorMethods, PageKeys, ChainedKeys, DynamicSelectorEntry } from './config';
3
- import type { DynamicLocatorEntry } from './locators/dynamic-locator-resolver';
2
+ import { PageConfig, TypedLocators, ProxyLocatorMethods, PageKeys, ChainedKeys, ModifyOptionsForTarget, UniversalKeys, SemanticKeys } from './config';
4
3
  import { VerifyFn, AssertionsMethod } from './assertions/verify-chain';
5
- declare class TypedPageClass<T extends {
6
- testIds?: Record<string, string | DynamicLocatorEntry>;
7
- selectors?: Record<string, string | DynamicSelectorEntry>;
8
- url?: string;
9
- }> {
4
+ declare class TypedPageClass<T extends PageConfig> {
10
5
  readonly page: Page;
11
6
  readonly context: Page | Locator;
12
7
  protected readonly config: T;
@@ -31,50 +26,59 @@ declare class TypedPageClass<T extends {
31
26
  waitForLoadState(state?: Parameters<Page['waitForLoadState']>[0], options?: Parameters<Page['waitForLoadState']>[1]): Promise<void>;
32
27
  /** Wait for navigation to a URL. Defaults to the page config URL. */
33
28
  waitForURL(urlOrOptions?: string | RegExp | Parameters<Page['waitForURL']>[1], options?: Parameters<Page['waitForURL']>[1]): Promise<void>;
34
- resolveLocator(target: PageKeys<T> | ChainedKeys<T> | Locator, options?: {
29
+ resolveLocator<Target extends PageKeys<T> | Locator>(target: Target, options?: ModifyOptionsForTarget<T, Target, {
35
30
  nth?: number;
36
31
  raw?: boolean;
37
32
  hasText?: string | RegExp;
38
- }): Locator;
33
+ }>): Locator;
39
34
  /** Resolve a config key to a proxied Locator with step-wrapped methods. */
40
- locator(target: PageKeys<T> | ChainedKeys<T> | Locator, options?: Parameters<Locator['filter']>[0] & {
35
+ locator(target: PageKeys<T> | Locator, options?: Parameters<Locator['filter']>[0] & {
36
+ nth?: number;
37
+ }): Locator;
38
+ /** Chain multiple locators sequentially up to depth 3. */
39
+ chain<P1 extends UniversalKeys<T>, P2 extends PageKeys<T>>(p1: P1, p2: P2, options?: {
41
40
  nth?: number;
41
+ raw?: boolean;
42
+ hasText?: string | RegExp;
43
+ }): Locator;
44
+ chain<P1 extends UniversalKeys<T>, P2 extends SemanticKeys<T>, P3 extends UniversalKeys<T>>(p1: P1, p2: P2, p3: P3, options?: {
45
+ nth?: number;
46
+ raw?: boolean;
47
+ hasText?: string | RegExp;
48
+ }): Locator;
49
+ chain<P1 extends UniversalKeys<T>, P2 extends UniversalKeys<T>, P3 extends PageKeys<T>>(p1: P1, p2: P2, p3: P3, options?: {
50
+ nth?: number;
51
+ raw?: boolean;
52
+ hasText?: string | RegExp;
53
+ }): Locator;
54
+ chain(path: ChainedKeys<T>, options?: {
55
+ nth?: number;
56
+ raw?: boolean;
57
+ hasText?: string | RegExp;
42
58
  }): Locator;
43
59
  /** Chainable assertions on config keys. Supports `.soft` for soft assertions. */
44
60
  get verify(): VerifyFn<T> & {
45
61
  soft: VerifyFn<T>;
46
62
  };
47
- verifyHidden(target: PageKeys<T> | ChainedKeys<T> | Locator, options?: Parameters<ReturnType<typeof playwrightExpect<Locator>>['toBeHidden']>[0] & {
63
+ verifyHidden(target: PageKeys<T> | Locator, options?: Parameters<ReturnType<typeof playwrightExpect<Locator>>['toBeHidden']>[0] & {
48
64
  nth?: number;
49
65
  message?: string;
50
66
  }): Promise<void>;
51
- verifyEnabled(target: PageKeys<T> | ChainedKeys<T> | Locator, options?: Parameters<ReturnType<typeof playwrightExpect<Locator>>['toBeEnabled']>[0] & {
67
+ verifyEnabled(target: PageKeys<T> | Locator, options?: Parameters<ReturnType<typeof playwrightExpect<Locator>>['toBeEnabled']>[0] & {
52
68
  nth?: number;
53
69
  message?: string;
54
70
  }): Promise<void>;
55
- verifyDisabled(target: PageKeys<T> | ChainedKeys<T> | Locator, options?: Parameters<ReturnType<typeof playwrightExpect<Locator>>['toBeDisabled']>[0] & {
71
+ verifyDisabled(target: PageKeys<T> | Locator, options?: Parameters<ReturnType<typeof playwrightExpect<Locator>>['toBeDisabled']>[0] & {
56
72
  nth?: number;
57
73
  message?: string;
58
74
  }): Promise<void>;
59
- expect(target: PageKeys<T> | ChainedKeys<T> | Locator, message?: string): ReturnType<typeof playwrightExpect<Locator>>;
75
+ expect(target: PageKeys<T> | Locator, message?: string): ReturnType<typeof playwrightExpect<Locator>>;
60
76
  }
61
- export type TypedPageType<T extends {
62
- testIds?: Record<string, string | DynamicLocatorEntry>;
63
- selectors?: Record<string, string | DynamicSelectorEntry>;
64
- url?: string;
65
- }> = TypedPageClass<T> & TypedLocators<T> & ProxyLocatorMethods<T> & AssertionsMethod<T>;
77
+ export type TypedPageType<T extends PageConfig> = TypedPageClass<T> & TypedLocators<T> & ProxyLocatorMethods<T> & AssertionsMethod<T>;
66
78
  export declare const TypedPage: {
67
- new <T extends {
68
- testIds?: Record<string, string | DynamicLocatorEntry>;
69
- selectors?: Record<string, string | DynamicSelectorEntry>;
70
- url?: string;
71
- }>(context: Page | Locator, config: T, options?: {
79
+ new <T extends PageConfig>(context: Page | Locator, config: T, options?: {
72
80
  timeout?: number;
73
81
  }): TypedPageType<T>;
74
82
  };
75
- export type TypedPage<T extends {
76
- testIds?: Record<string, string | DynamicLocatorEntry>;
77
- selectors?: Record<string, string | DynamicSelectorEntry>;
78
- url?: string;
79
- }> = TypedPageType<T>;
83
+ export type TypedPage<T extends PageConfig> = TypedPageType<T>;
80
84
  export {};
@@ -17,9 +17,7 @@ class TypedPageClass {
17
17
  timeout;
18
18
  constructor(context, config, options) {
19
19
  this.context = context;
20
- this.page = typeof context.page === 'function'
21
- ? context.page()
22
- : context;
20
+ this.page = typeof context.page === 'function' ? context.page() : context;
23
21
  this.config = config;
24
22
  this.url = config.url;
25
23
  this.timeout = options?.timeout;
@@ -27,9 +25,20 @@ class TypedPageClass {
27
25
  (0, locator_actions_1.defineActionMethods)(this, this.resolveLocator.bind(this), this.timeout);
28
26
  // Auto-wrap subclass methods with Playwright test.step
29
27
  const builtInMethods = new Set([
30
- 'constructor', 'goto', 'verifyURL', 'verifyTitle', 'reload',
31
- 'waitForLoadState', 'waitForURL', 'resolveLocator', 'locator',
32
- 'verify', 'verifyHidden', 'verifyEnabled', 'verifyDisabled', 'expect'
28
+ 'constructor',
29
+ 'goto',
30
+ 'verifyURL',
31
+ 'verifyTitle',
32
+ 'reload',
33
+ 'waitForLoadState',
34
+ 'waitForURL',
35
+ 'resolveLocator',
36
+ 'locator',
37
+ 'verify',
38
+ 'verifyHidden',
39
+ 'verifyEnabled',
40
+ 'verifyDisabled',
41
+ 'expect'
33
42
  ]);
34
43
  let proto = Object.getPrototypeOf(this);
35
44
  while (proto && proto !== Object.prototype && proto !== TypedPageClass.prototype) {
@@ -40,12 +49,12 @@ class TypedPageClass {
40
49
  const originalFn = desc.value;
41
50
  Object.defineProperty(this, prop, {
42
51
  value: function (...args) {
43
- const words = prop.split(/(?=[A-Z])/).map(w => w.toLowerCase());
52
+ const words = prop.split(/(?=[A-Z])/).map((w) => w.toLowerCase());
44
53
  if (words.length > 0) {
45
54
  words[0] = words[0].charAt(0).toUpperCase() + words[0].slice(1);
46
55
  }
47
56
  const sentence = words.join(' ');
48
- const formattedArgs = args.map(arg => {
57
+ const formattedArgs = args.map((arg) => {
49
58
  if (typeof arg === 'object' && arg !== null) {
50
59
  try {
51
60
  return JSON.stringify(arg);
@@ -56,9 +65,7 @@ class TypedPageClass {
56
65
  }
57
66
  return String(arg);
58
67
  });
59
- const stepName = formattedArgs.length > 0
60
- ? `${sentence} - ${formattedArgs.join(' - ')}`
61
- : sentence;
68
+ const stepName = formattedArgs.length > 0 ? `${sentence} - ${formattedArgs.join(' - ')}` : sentence;
62
69
  return test_1.test.step(stepName, () => {
63
70
  return originalFn.apply(this, args);
64
71
  });
@@ -103,6 +110,28 @@ class TypedPageClass {
103
110
  locator(target, options) {
104
111
  return (0, resolver_1.locator)(this.context, this.config, target, options);
105
112
  }
113
+ chain(...args) {
114
+ let options = undefined;
115
+ const keys = [];
116
+ for (const arg of args) {
117
+ if (typeof arg === 'string') {
118
+ if (arg.includes('.')) {
119
+ keys.push(...arg.split('.'));
120
+ }
121
+ else {
122
+ keys.push(arg);
123
+ }
124
+ }
125
+ else if (typeof arg === 'object' && arg !== null) {
126
+ options = arg;
127
+ }
128
+ }
129
+ let loc = (0, resolver_1.resolveLocator)(this.context, this.config, keys[0], options);
130
+ for (let i = 1; i < keys.length; i++) {
131
+ loc = (0, resolver_1.resolveLocator)(loc, this.config, keys[i], options);
132
+ }
133
+ return loc;
134
+ }
106
135
  /** Chainable assertions on config keys. Supports `.soft` for soft assertions. */
107
136
  get verify() {
108
137
  const fn = (target, opts) => (0, verify_chain_1.createVerifyChain)(this.resolveLocator.bind(this), target, opts, false);