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.
- package/README.md +1 -1
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +777 -0
- package/dist/codegen/config/context-weights.d.ts +8 -0
- package/dist/codegen/config/context-weights.js +11 -0
- package/dist/codegen/config/ignored-attributes.d.ts +1 -0
- package/dist/codegen/config/ignored-attributes.js +4 -0
- package/dist/codegen/config/ignored-classes.d.ts +1 -0
- package/dist/codegen/config/ignored-classes.js +144 -0
- package/dist/codegen/config/ignored-values.d.ts +1 -0
- package/dist/codegen/config/ignored-values.js +4 -0
- package/dist/codegen/config/index.d.ts +29 -0
- package/dist/codegen/config/index.js +17 -0
- package/dist/codegen/config/strategy-order.d.ts +1 -0
- package/dist/codegen/config/strategy-order.js +18 -0
- package/dist/codegen/config/weights.d.ts +15 -0
- package/dist/codegen/config/weights.js +18 -0
- package/dist/codegen/floating-panel/client.d.ts +1 -0
- package/dist/codegen/floating-panel/client.js +248 -0
- package/dist/codegen/floating-panel/index.d.ts +4 -0
- package/dist/codegen/floating-panel/index.js +20 -0
- package/dist/codegen/floating-panel/manager.d.ts +19 -0
- package/dist/codegen/floating-panel/manager.js +90 -0
- package/dist/codegen/floating-panel/template.d.ts +2 -0
- package/dist/codegen/floating-panel/template.js +58 -0
- package/dist/codegen/floating-panel/types.d.ts +7 -0
- package/dist/codegen/floating-panel/types.js +2 -0
- package/dist/codegen/generator/action.validator.d.ts +5 -0
- package/dist/codegen/generator/action.validator.js +27 -0
- package/dist/codegen/generator/index.d.ts +13 -0
- package/dist/codegen/generator/index.js +724 -0
- package/dist/codegen/generator/locator.restricted.d.ts +36 -0
- package/dist/codegen/generator/locator.restricted.js +127 -0
- package/dist/codegen/generator/locator.validator.d.ts +8 -0
- package/dist/codegen/generator/locator.validator.js +38 -0
- package/dist/codegen/hover-tracker/client.d.ts +1 -0
- package/dist/codegen/hover-tracker/client.js +75 -0
- package/dist/codegen/hover-tracker/index.d.ts +3 -0
- package/dist/codegen/hover-tracker/index.js +19 -0
- package/dist/codegen/hover-tracker/manager.d.ts +11 -0
- package/dist/codegen/hover-tracker/manager.js +28 -0
- package/dist/codegen/hover-tracker/types.d.ts +3 -0
- package/dist/codegen/hover-tracker/types.js +2 -0
- package/dist/codegen/index.d.ts +35 -0
- package/dist/codegen/index.js +1147 -0
- package/dist/codegen/key-utils.d.ts +23 -0
- package/dist/codegen/key-utils.js +68 -0
- package/dist/codegen/scorer/base-score.d.ts +1 -0
- package/dist/codegen/scorer/base-score.js +7 -0
- package/dist/codegen/scorer/context-score.d.ts +2 -0
- package/dist/codegen/scorer/context-score.js +27 -0
- package/dist/codegen/scorer/index.d.ts +5 -0
- package/dist/codegen/scorer/index.js +23 -0
- package/dist/codegen/scorer/locator.ranking.d.ts +15 -0
- package/dist/codegen/scorer/locator.ranking.js +83 -0
- package/dist/codegen/scorer/semantic-score.d.ts +1 -0
- package/dist/codegen/scorer/semantic-score.js +47 -0
- package/dist/codegen/types.d.ts +35 -0
- package/dist/codegen/types.js +2 -0
- package/dist/component/table.js +14 -10
- package/dist/constants/methods.d.ts +2 -0
- package/dist/constants/methods.js +33 -0
- package/dist/constants/strategies.d.ts +2 -0
- package/dist/constants/strategies.js +47 -0
- package/dist/page/actions/locator-actions.js +15 -4
- package/dist/page/assertions/verify-chain.d.ts +39 -32
- package/dist/page/assertions/verify-chain.js +18 -4
- package/dist/page/assertions/verify-helpers.js +12 -3
- package/dist/page/config.d.ts +109 -71
- package/dist/page/config.js +10 -31
- package/dist/page/locators/dynamic-locator-resolver.js +29 -9
- package/dist/page/locators/resolver.d.ts +15 -15
- package/dist/page/locators/resolver.js +151 -32
- package/dist/page/registry.d.ts +43 -6
- package/dist/page/registry.js +90 -18
- package/dist/page/typed-page.d.ts +33 -29
- package/dist/page/typed-page.js +40 -11
- package/dist/page/types/proxy-methods.d.ts +69 -54
- package/dist/page/types/validation.d.ts +48 -1
- package/dist/page/utils/caller-location.js +13 -9
- package/dist/page/utils/formatter.js +6 -3
- package/dist/types/methods.d.ts +4 -0
- package/dist/types/methods.js +2 -0
- package/dist/types/strategies.d.ts +17 -0
- package/dist/types/strategies.js +2 -0
- 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
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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
|
-
|
|
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
|
-
|
|
26
|
-
|
|
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
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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
|
-
|
|
47
|
-
|
|
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 (
|
|
50
|
-
return ctx
|
|
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
|
|
55
|
-
|
|
56
|
-
|
|
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 =
|
|
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), {
|
|
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;
|
package/dist/page/registry.d.ts
CHANGED
|
@@ -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 :
|
|
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 :
|
|
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] :
|
|
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] :
|
|
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
|
-
|
|
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({
|
package/dist/page/registry.js
CHANGED
|
@@ -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: [
|
|
13
|
+
workerContext: [
|
|
14
|
+
async ({ browser }, use) => {
|
|
13
15
|
const context = await browser.newContext();
|
|
14
16
|
await use(context);
|
|
15
17
|
await context.close();
|
|
16
|
-
},
|
|
17
|
-
|
|
18
|
+
},
|
|
19
|
+
{ scope: 'worker' }
|
|
20
|
+
],
|
|
21
|
+
workerPage: [
|
|
22
|
+
async ({ workerContext }, use) => {
|
|
18
23
|
const page = await workerContext.newPage();
|
|
19
24
|
await use(page);
|
|
20
|
-
},
|
|
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
|
-
|
|
29
|
-
|
|
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
|
-
},
|
|
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 =
|
|
37
|
-
testRunner.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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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,
|
|
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
|
|
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> |
|
|
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> |
|
|
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> |
|
|
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> |
|
|
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> |
|
|
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 {};
|
package/dist/page/typed-page.js
CHANGED
|
@@ -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',
|
|
31
|
-
'
|
|
32
|
-
'
|
|
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);
|