pw-core 1.2.1 → 1.3.1
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 +2 -1
- package/dist/cli.js +145 -421
- package/dist/codegen/action-formatter.d.ts +19 -0
- package/dist/codegen/action-formatter.js +160 -0
- package/dist/codegen/action-processor.d.ts +23 -0
- package/dist/codegen/action-processor.js +112 -0
- package/dist/codegen/floating-panel/client.d.ts +14 -1
- package/dist/codegen/floating-panel/client.js +20 -25
- package/dist/codegen/floating-panel/manager.js +16 -22
- package/dist/codegen/generator/action.validator.d.ts +2 -1
- package/dist/codegen/generator/action.validator.js +2 -1
- package/dist/codegen/generator/candidate-scorer.d.ts +7 -0
- package/dist/codegen/generator/candidate-scorer.js +206 -0
- package/dist/codegen/generator/dom-scanner.d.ts +39 -0
- package/dist/codegen/generator/dom-scanner.js +429 -0
- package/dist/codegen/generator/id-stability.d.ts +6 -0
- package/dist/codegen/generator/id-stability.js +38 -0
- package/dist/codegen/generator/index.d.ts +8 -10
- package/dist/codegen/generator/index.js +23 -678
- package/dist/codegen/generator/key-builder.d.ts +13 -0
- package/dist/codegen/generator/key-builder.js +32 -0
- package/dist/codegen/hover-tracker/client.d.ts +5 -0
- package/dist/codegen/hover-tracker/client.js +14 -15
- package/dist/codegen/hover-tracker/manager.js +2 -6
- package/dist/codegen/index.d.ts +9 -32
- package/dist/codegen/index.js +9 -1128
- package/dist/codegen/key-utils.d.ts +16 -0
- package/dist/codegen/key-utils.js +29 -1
- package/dist/codegen/project-finder.d.ts +29 -0
- package/dist/codegen/project-finder.js +283 -0
- package/dist/codegen/registry-matcher.d.ts +27 -0
- package/dist/codegen/registry-matcher.js +254 -0
- package/dist/codegen/registry-store.d.ts +52 -0
- package/dist/codegen/registry-store.js +652 -0
- package/dist/codegen/selector-parser.d.ts +16 -0
- package/dist/codegen/selector-parser.js +121 -0
- package/dist/codegen/types.d.ts +18 -1
- package/dist/codegen/uniqueness.d.ts +3 -0
- package/dist/codegen/uniqueness.js +15 -0
- package/dist/component/table.d.ts +2 -2
- package/dist/component/table.js +7 -6
- package/dist/index.d.ts +3 -0
- package/dist/index.js +19 -0
- package/dist/page/actions/locator-actions.d.ts +6 -10
- package/dist/page/actions/locator-actions.js +24 -23
- package/dist/page/assertions/verify-chain.d.ts +11 -13
- package/dist/page/assertions/verify-chain.js +26 -7
- package/dist/page/assertions/verify-helpers.d.ts +5 -15
- package/dist/page/config.d.ts +25 -25
- package/dist/page/locators/dynamic-locator-resolver.js +22 -9
- package/dist/page/locators/resolver.d.ts +17 -11
- package/dist/page/locators/resolver.js +84 -78
- package/dist/page/registry.d.ts +35 -35
- package/dist/page/registry.js +86 -82
- package/dist/page/typed-page.d.ts +1 -0
- package/dist/page/typed-page.js +22 -12
- package/dist/page/types/proxy-methods.d.ts +11 -19
- package/dist/page/types/validation.d.ts +1 -1
- package/dist/page/utils/formatter.d.ts +3 -3
- package/dist/page/utils/formatter.js +5 -2
- package/package.json +6 -3
|
@@ -0,0 +1,652 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.findRegistryCallBounds = findRegistryCallBounds;
|
|
37
|
+
exports.parseRegistry = parseRegistry;
|
|
38
|
+
exports.matchDynamicEntry = matchDynamicEntry;
|
|
39
|
+
exports.findCommonPrefix = findCommonPrefix;
|
|
40
|
+
exports.collapsePageDict = collapsePageDict;
|
|
41
|
+
exports.serializeRegistryObject = serializeRegistryObject;
|
|
42
|
+
exports.writeRegistry = writeRegistry;
|
|
43
|
+
const fs = __importStar(require("fs"));
|
|
44
|
+
const path = __importStar(require("path"));
|
|
45
|
+
const key_utils_1 = require("./key-utils");
|
|
46
|
+
/**
|
|
47
|
+
* Finds the character bounds `(startIdx, endIdx)` of the argument inside `createPageRegistry(...)`.
|
|
48
|
+
* Robustly ignores parentheses inside strings, template literals, and comments.
|
|
49
|
+
*/
|
|
50
|
+
function findRegistryCallBounds(content) {
|
|
51
|
+
const matchIndex = content.indexOf('createPageRegistry(');
|
|
52
|
+
if (matchIndex === -1)
|
|
53
|
+
return null;
|
|
54
|
+
const startIdx = matchIndex + 'createPageRegistry('.length;
|
|
55
|
+
let parenDepth = 0;
|
|
56
|
+
let inSingleQuote = false;
|
|
57
|
+
let inDoubleQuote = false;
|
|
58
|
+
let inTemplateLiteral = false;
|
|
59
|
+
let inLineComment = false;
|
|
60
|
+
let inBlockComment = false;
|
|
61
|
+
let isEscaped = false;
|
|
62
|
+
for (let i = startIdx; i < content.length; i++) {
|
|
63
|
+
const char = content[i];
|
|
64
|
+
const prevChar = i > 0 ? content[i - 1] : '';
|
|
65
|
+
if (inLineComment) {
|
|
66
|
+
if (char === '\n')
|
|
67
|
+
inLineComment = false;
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
if (inBlockComment) {
|
|
71
|
+
if (prevChar === '*' && char === '/')
|
|
72
|
+
inBlockComment = false;
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
if (inSingleQuote) {
|
|
76
|
+
if (char === "'" && !isEscaped)
|
|
77
|
+
inSingleQuote = false;
|
|
78
|
+
isEscaped = char === '\\' && !isEscaped;
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
if (inDoubleQuote) {
|
|
82
|
+
if (char === '"' && !isEscaped)
|
|
83
|
+
inDoubleQuote = false;
|
|
84
|
+
isEscaped = char === '\\' && !isEscaped;
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
if (inTemplateLiteral) {
|
|
88
|
+
if (char === '`' && !isEscaped)
|
|
89
|
+
inTemplateLiteral = false;
|
|
90
|
+
isEscaped = char === '\\' && !isEscaped;
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
isEscaped = false;
|
|
94
|
+
// Check for comment starts
|
|
95
|
+
if (char === '/' && i + 1 < content.length) {
|
|
96
|
+
const nextChar = content[i + 1];
|
|
97
|
+
if (nextChar === '/') {
|
|
98
|
+
inLineComment = true;
|
|
99
|
+
i++;
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
if (nextChar === '*') {
|
|
103
|
+
inBlockComment = true;
|
|
104
|
+
i++;
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
// Check for string starts
|
|
109
|
+
if (char === "'") {
|
|
110
|
+
inSingleQuote = true;
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
if (char === '"') {
|
|
114
|
+
inDoubleQuote = true;
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
if (char === '`') {
|
|
118
|
+
inTemplateLiteral = true;
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
// Check parentheses
|
|
122
|
+
if (char === '(') {
|
|
123
|
+
parenDepth++;
|
|
124
|
+
}
|
|
125
|
+
else if (char === ')') {
|
|
126
|
+
if (parenDepth === 0) {
|
|
127
|
+
return { startIdx, endIdx: i };
|
|
128
|
+
}
|
|
129
|
+
parenDepth--;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return null;
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Parses a `registry.ts` file by evaluating the object literal passed to `createPageRegistry`.
|
|
136
|
+
* Returns an empty object if the file does not exist yet.
|
|
137
|
+
*/
|
|
138
|
+
function parseRegistry(filePath) {
|
|
139
|
+
if (!fs.existsSync(filePath)) {
|
|
140
|
+
return {};
|
|
141
|
+
}
|
|
142
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
143
|
+
const bounds = findRegistryCallBounds(content);
|
|
144
|
+
if (!bounds) {
|
|
145
|
+
throw new Error(`[pw-core] Failed to parse registry in ${filePath}: "createPageRegistry" call not found.`);
|
|
146
|
+
}
|
|
147
|
+
const objectLiteralText = content.substring(bounds.startIdx, bounds.endIdx).trim();
|
|
148
|
+
try {
|
|
149
|
+
return new Function(`return (${objectLiteralText})`)();
|
|
150
|
+
}
|
|
151
|
+
catch (e) {
|
|
152
|
+
throw new Error(`[pw-core] Error evaluating registry configuration in ${filePath}: ${e.message}`);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Checks if a string value matches a dynamic registry entry pattern.
|
|
157
|
+
*/
|
|
158
|
+
function matchDynamicEntry(val, entry, isTestId) {
|
|
159
|
+
const pattern = isTestId ? entry.testId : entry.selector;
|
|
160
|
+
if (typeof pattern !== 'string')
|
|
161
|
+
return { matches: false };
|
|
162
|
+
const placeholders = Object.keys(entry).filter((k) => k !== 'testId' && k !== 'selector');
|
|
163
|
+
if (placeholders.length === 0)
|
|
164
|
+
return { matches: false };
|
|
165
|
+
const placeholder = placeholders[0];
|
|
166
|
+
let placeholderPattern = `{${placeholder}}`;
|
|
167
|
+
if (!pattern.includes(placeholderPattern)) {
|
|
168
|
+
placeholderPattern = placeholder;
|
|
169
|
+
}
|
|
170
|
+
const token = '__PLACEHOLDER_REGEX__';
|
|
171
|
+
const tempPattern = pattern.replace(placeholderPattern, token);
|
|
172
|
+
const escaped = tempPattern.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&');
|
|
173
|
+
const regexStr = '^' + escaped.replace(token, '([a-zA-Z0-9_ -]+)') + '$';
|
|
174
|
+
try {
|
|
175
|
+
const regex = new RegExp(regexStr);
|
|
176
|
+
const match = val.match(regex);
|
|
177
|
+
if (match) {
|
|
178
|
+
return {
|
|
179
|
+
matches: true,
|
|
180
|
+
matchedValue: match[1],
|
|
181
|
+
placeholderName: placeholder
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
catch {
|
|
186
|
+
// Ignore invalid dynamic regex pattern compilation
|
|
187
|
+
}
|
|
188
|
+
return { matches: false };
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* Finds the common prefix between hyphen/underscore delimited string values.
|
|
192
|
+
*/
|
|
193
|
+
function findCommonPrefix(values) {
|
|
194
|
+
if (values.length === 0)
|
|
195
|
+
return '';
|
|
196
|
+
const first = values[0];
|
|
197
|
+
const parts = first.split(/[-_]/);
|
|
198
|
+
let common = '';
|
|
199
|
+
for (let i = 0; i < parts.length; i++) {
|
|
200
|
+
const candidate = parts.slice(0, i + 1).join('-');
|
|
201
|
+
const matchesAll = values.every((v) => v.startsWith(candidate + '-') || v.startsWith(candidate + '_'));
|
|
202
|
+
if (matchesAll) {
|
|
203
|
+
common = candidate;
|
|
204
|
+
}
|
|
205
|
+
else {
|
|
206
|
+
break;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
return common;
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* Collapses similar static entries into dynamic parameterized entries in a locator dictionary.
|
|
213
|
+
*/
|
|
214
|
+
function collapsePageDict(dict, isTestId, keyReplacements) {
|
|
215
|
+
const result = {};
|
|
216
|
+
const dynamicEntries = {};
|
|
217
|
+
const staticEntries = {};
|
|
218
|
+
for (const k of Object.keys(dict)) {
|
|
219
|
+
const item = dict[k];
|
|
220
|
+
if (typeof item === 'object' && item !== null) {
|
|
221
|
+
dynamicEntries[k] = item;
|
|
222
|
+
}
|
|
223
|
+
else if (typeof item === 'string') {
|
|
224
|
+
staticEntries[k] = item;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
const collapsedKeys = new Set();
|
|
228
|
+
// 1. Try to merge static entries into existing dynamic entries first
|
|
229
|
+
for (const dk of Object.keys(dynamicEntries)) {
|
|
230
|
+
const entry = dynamicEntries[dk];
|
|
231
|
+
const placeholders = Object.keys(entry).filter((pk) => pk !== 'testId' && pk !== 'selector');
|
|
232
|
+
if (placeholders.length === 0)
|
|
233
|
+
continue;
|
|
234
|
+
const placeholder = placeholders[0];
|
|
235
|
+
for (const k of Object.keys(staticEntries)) {
|
|
236
|
+
const val = staticEntries[k];
|
|
237
|
+
const matchRes = matchDynamicEntry(val, entry, isTestId);
|
|
238
|
+
if (matchRes.matches && matchRes.matchedValue) {
|
|
239
|
+
const placeholderValues = entry[placeholder];
|
|
240
|
+
if (!placeholderValues.includes(matchRes.matchedValue)) {
|
|
241
|
+
placeholderValues.push(matchRes.matchedValue);
|
|
242
|
+
}
|
|
243
|
+
collapsedKeys.add(k);
|
|
244
|
+
const capValue = matchRes.matchedValue.charAt(0).toUpperCase() + matchRes.matchedValue.slice(1);
|
|
245
|
+
let newKey = dk.replace(`{${placeholder}}`, capValue);
|
|
246
|
+
newKey = newKey.charAt(0).toLowerCase() + newKey.slice(1);
|
|
247
|
+
keyReplacements[k] = newKey;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
const groups = {};
|
|
252
|
+
const staticKeys = Object.keys(staticEntries);
|
|
253
|
+
for (const k of staticKeys) {
|
|
254
|
+
if (collapsedKeys.has(k))
|
|
255
|
+
continue;
|
|
256
|
+
const val = staticEntries[k];
|
|
257
|
+
const isCssOrSpecial = val.includes('[') ||
|
|
258
|
+
val.includes(']') ||
|
|
259
|
+
val.includes('=') ||
|
|
260
|
+
val.includes(':') ||
|
|
261
|
+
val.includes('.') ||
|
|
262
|
+
val.includes('#');
|
|
263
|
+
const idSelectorMatch = val.match(/^#([a-zA-Z0-9_-]+)[-_]([a-zA-Z0-9_-]+)$/);
|
|
264
|
+
const classSelectorMatch = val.match(/^\.([a-zA-Z0-9_-]+)[-_]([a-zA-Z0-9_-]+)$/);
|
|
265
|
+
const testIdMatch = !isCssOrSpecial ? val.match(/^(.*?)[-_]([^-]+)$/) : null;
|
|
266
|
+
const attrMatch = val.match(/^\[([a-zA-Z0-9_-]+)="([^"]+)"\]$/);
|
|
267
|
+
const roleMatch = val.match(/^internal:role=([a-zA-Z0-9_-]+)\[name="([^"]+)"i\]$/);
|
|
268
|
+
const textMatch = val.match(/^(?:text|internal:text)=["']?([^"']+)["']?i?$/);
|
|
269
|
+
if (idSelectorMatch) {
|
|
270
|
+
const prefix = idSelectorMatch[1];
|
|
271
|
+
const suffix = idSelectorMatch[2];
|
|
272
|
+
const groupKey = `idSelector:${prefix}`;
|
|
273
|
+
if (!groups[groupKey]) {
|
|
274
|
+
groups[groupKey] = {
|
|
275
|
+
keys: [],
|
|
276
|
+
values: [],
|
|
277
|
+
prefix,
|
|
278
|
+
suffixPattern: 'idSelector'
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
groups[groupKey].keys.push(k);
|
|
282
|
+
groups[groupKey].values.push(suffix);
|
|
283
|
+
}
|
|
284
|
+
else if (classSelectorMatch) {
|
|
285
|
+
const prefix = classSelectorMatch[1];
|
|
286
|
+
const suffix = classSelectorMatch[2];
|
|
287
|
+
const groupKey = `classSelector:${prefix}`;
|
|
288
|
+
if (!groups[groupKey]) {
|
|
289
|
+
groups[groupKey] = {
|
|
290
|
+
keys: [],
|
|
291
|
+
values: [],
|
|
292
|
+
prefix,
|
|
293
|
+
suffixPattern: 'classSelector'
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
groups[groupKey].keys.push(k);
|
|
297
|
+
groups[groupKey].values.push(suffix);
|
|
298
|
+
}
|
|
299
|
+
else if (testIdMatch) {
|
|
300
|
+
const prefix = testIdMatch[1];
|
|
301
|
+
const suffix = testIdMatch[2];
|
|
302
|
+
const groupKey = `testId:${prefix}`;
|
|
303
|
+
if (!groups[groupKey]) {
|
|
304
|
+
groups[groupKey] = {
|
|
305
|
+
keys: [],
|
|
306
|
+
values: [],
|
|
307
|
+
prefix,
|
|
308
|
+
suffixPattern: 'testId'
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
groups[groupKey].keys.push(k);
|
|
312
|
+
groups[groupKey].values.push(suffix);
|
|
313
|
+
}
|
|
314
|
+
else if (attrMatch) {
|
|
315
|
+
const attrName = attrMatch[1];
|
|
316
|
+
const attrValue = attrMatch[2];
|
|
317
|
+
const groupKey = `attr:${attrName}`;
|
|
318
|
+
if (!groups[groupKey]) {
|
|
319
|
+
groups[groupKey] = {
|
|
320
|
+
keys: [],
|
|
321
|
+
values: [],
|
|
322
|
+
prefix: attrName,
|
|
323
|
+
suffixPattern: 'attr'
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
groups[groupKey].keys.push(k);
|
|
327
|
+
groups[groupKey].values.push(attrValue);
|
|
328
|
+
}
|
|
329
|
+
else if (roleMatch) {
|
|
330
|
+
const roleVal = roleMatch[1];
|
|
331
|
+
const nameVal = roleMatch[2];
|
|
332
|
+
const groupKey = `role:${roleVal}`;
|
|
333
|
+
if (!groups[groupKey]) {
|
|
334
|
+
groups[groupKey] = {
|
|
335
|
+
keys: [],
|
|
336
|
+
values: [],
|
|
337
|
+
prefix: roleVal,
|
|
338
|
+
suffixPattern: 'role'
|
|
339
|
+
};
|
|
340
|
+
}
|
|
341
|
+
groups[groupKey].keys.push(k);
|
|
342
|
+
groups[groupKey].values.push(nameVal);
|
|
343
|
+
}
|
|
344
|
+
else if (textMatch) {
|
|
345
|
+
const textValue = textMatch[1];
|
|
346
|
+
const groupKey = `text:text`;
|
|
347
|
+
if (!groups[groupKey]) {
|
|
348
|
+
groups[groupKey] = {
|
|
349
|
+
keys: [],
|
|
350
|
+
values: [],
|
|
351
|
+
prefix: 'text',
|
|
352
|
+
suffixPattern: 'text'
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
groups[groupKey].keys.push(k);
|
|
356
|
+
groups[groupKey].values.push(textValue);
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
for (const gk of Object.keys(groups)) {
|
|
360
|
+
const group = groups[gk];
|
|
361
|
+
if (group.keys.length >= 2 || (group.keys.length === 1 && group.values.every((v) => /^\d+$/.test(v)))) {
|
|
362
|
+
let placeholder = 'item';
|
|
363
|
+
if (group.values.every((v) => /^\d+$/.test(v))) {
|
|
364
|
+
placeholder = 'id';
|
|
365
|
+
}
|
|
366
|
+
else if (group.prefix.toLowerCase().includes('filter') ||
|
|
367
|
+
group.prefix.toLowerCase().includes('button') ||
|
|
368
|
+
group.prefix.toLowerCase().includes('btn')) {
|
|
369
|
+
placeholder = 'btn';
|
|
370
|
+
}
|
|
371
|
+
let dynamicKey = '';
|
|
372
|
+
let entry = {};
|
|
373
|
+
if (group.suffixPattern === 'testId') {
|
|
374
|
+
dynamicKey = (0, key_utils_1.toRegistryKey)(group.prefix) + `{${placeholder}}`;
|
|
375
|
+
entry = {
|
|
376
|
+
[placeholder]: group.values,
|
|
377
|
+
testId: group.prefix + '-' + placeholder
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
else if (group.suffixPattern === 'idSelector') {
|
|
381
|
+
dynamicKey = (0, key_utils_1.toRegistryKey)(group.prefix) + `{${placeholder}}`;
|
|
382
|
+
entry = {
|
|
383
|
+
[placeholder]: group.values,
|
|
384
|
+
selector: `#${group.prefix}-{${placeholder}}`
|
|
385
|
+
};
|
|
386
|
+
}
|
|
387
|
+
else if (group.suffixPattern === 'classSelector') {
|
|
388
|
+
dynamicKey = (0, key_utils_1.toRegistryKey)(group.prefix) + `{${placeholder}}`;
|
|
389
|
+
entry = {
|
|
390
|
+
[placeholder]: group.values,
|
|
391
|
+
selector: `.${group.prefix}-{${placeholder}}`
|
|
392
|
+
};
|
|
393
|
+
}
|
|
394
|
+
else if (group.suffixPattern === 'attr') {
|
|
395
|
+
const commonPrefix = findCommonPrefix(group.values);
|
|
396
|
+
if (commonPrefix) {
|
|
397
|
+
dynamicKey = (0, key_utils_1.toRegistryKey)(commonPrefix) + `{${placeholder}}`;
|
|
398
|
+
entry = {
|
|
399
|
+
[placeholder]: group.values.map((v) => v.substring(commonPrefix.length + 1)),
|
|
400
|
+
selector: `[${group.prefix}="${commonPrefix}-{${placeholder}}"]`
|
|
401
|
+
};
|
|
402
|
+
}
|
|
403
|
+
else {
|
|
404
|
+
dynamicKey = `{${placeholder}}`;
|
|
405
|
+
entry = {
|
|
406
|
+
[placeholder]: group.values,
|
|
407
|
+
selector: `[${group.prefix}="{${placeholder}}"]`
|
|
408
|
+
};
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
else if (group.suffixPattern === 'role') {
|
|
412
|
+
const rolePlaceholder = group.prefix === 'button' ? 'btn' : 'item';
|
|
413
|
+
dynamicKey = `{${rolePlaceholder}}`;
|
|
414
|
+
entry = {
|
|
415
|
+
[rolePlaceholder]: group.values,
|
|
416
|
+
selector: `internal:role=${group.prefix}[name="{${rolePlaceholder}}"i]`
|
|
417
|
+
};
|
|
418
|
+
}
|
|
419
|
+
else if (group.suffixPattern === 'text') {
|
|
420
|
+
dynamicKey = `{${placeholder}}`;
|
|
421
|
+
entry = {
|
|
422
|
+
[placeholder]: group.values,
|
|
423
|
+
selector: `text="{${placeholder}}"`
|
|
424
|
+
};
|
|
425
|
+
}
|
|
426
|
+
if (dynamicKey) {
|
|
427
|
+
dynamicEntries[dynamicKey] = entry;
|
|
428
|
+
group.keys.forEach((oldKey, idx) => {
|
|
429
|
+
collapsedKeys.add(oldKey);
|
|
430
|
+
const generatedValues = entry[placeholder];
|
|
431
|
+
const val = Array.isArray(generatedValues) ? generatedValues[idx] : group.values[idx];
|
|
432
|
+
let newKey = '';
|
|
433
|
+
if (dynamicKey === `{${placeholder}}`) {
|
|
434
|
+
newKey = val;
|
|
435
|
+
}
|
|
436
|
+
else {
|
|
437
|
+
const capValue = val.charAt(0).toUpperCase() + val.slice(1);
|
|
438
|
+
newKey = dynamicKey.replace(`{${placeholder}}`, capValue);
|
|
439
|
+
newKey = newKey.charAt(0).toLowerCase() + newKey.slice(1);
|
|
440
|
+
}
|
|
441
|
+
keyReplacements[oldKey] = newKey;
|
|
442
|
+
});
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
for (const k of Object.keys(dict)) {
|
|
447
|
+
if (collapsedKeys.has(k))
|
|
448
|
+
continue;
|
|
449
|
+
if (dynamicEntries[k]) {
|
|
450
|
+
result[k] = dynamicEntries[k];
|
|
451
|
+
}
|
|
452
|
+
else {
|
|
453
|
+
result[k] = dict[k];
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
for (const k of Object.keys(dynamicEntries)) {
|
|
457
|
+
if (!result[k]) {
|
|
458
|
+
result[k] = dynamicEntries[k];
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
return result;
|
|
462
|
+
}
|
|
463
|
+
/**
|
|
464
|
+
* Serializes a JavaScript value into formatted TypeScript source text for registry files.
|
|
465
|
+
*/
|
|
466
|
+
function serializeRegistryObject(obj, indent = 2, isTestIdsOrSelectors = false) {
|
|
467
|
+
const spaces = ' '.repeat(indent);
|
|
468
|
+
if (obj === null)
|
|
469
|
+
return 'null';
|
|
470
|
+
if (obj === undefined)
|
|
471
|
+
return 'undefined';
|
|
472
|
+
if (typeof obj === 'string') {
|
|
473
|
+
return `'${obj.replace(/'/g, "\\'")}'`;
|
|
474
|
+
}
|
|
475
|
+
if (typeof obj === 'number' || typeof obj === 'boolean') {
|
|
476
|
+
return String(obj);
|
|
477
|
+
}
|
|
478
|
+
if (Array.isArray(obj)) {
|
|
479
|
+
return '[' + obj.map((x) => serializeRegistryObject(x, indent, false)).join(', ') + ']';
|
|
480
|
+
}
|
|
481
|
+
if (typeof obj === 'object') {
|
|
482
|
+
const record = obj;
|
|
483
|
+
let keys = Object.keys(record);
|
|
484
|
+
if (isTestIdsOrSelectors) {
|
|
485
|
+
keys = keys.sort((a, b) => a.localeCompare(b));
|
|
486
|
+
}
|
|
487
|
+
else if (indent === 4) {
|
|
488
|
+
const order = [
|
|
489
|
+
'url', 'testId', 'testIds', 'selector', 'selectors',
|
|
490
|
+
'text', 'label', 'title', 'placeholder', 'altText',
|
|
491
|
+
'button', 'checkbox', 'radio', 'heading', 'link', 'dialog',
|
|
492
|
+
'textbox', 'searchbox', 'combobox', 'list', 'listbox',
|
|
493
|
+
'menu', 'menuitem', 'option', 'row', 'cell', 'grid', 'gridcell',
|
|
494
|
+
'tab', 'tabpanel', 'switch', 'progressbar', 'status', 'tooltip',
|
|
495
|
+
'tree', 'treeitem', 'banner', 'navigation', 'article', 'main', 'form', 'region'
|
|
496
|
+
];
|
|
497
|
+
keys = keys.sort((a, b) => {
|
|
498
|
+
const idxA = order.indexOf(a);
|
|
499
|
+
const idxB = order.indexOf(b);
|
|
500
|
+
if (idxA !== -1 && idxB !== -1)
|
|
501
|
+
return idxA - idxB;
|
|
502
|
+
if (idxA !== -1)
|
|
503
|
+
return -1;
|
|
504
|
+
if (idxB !== -1)
|
|
505
|
+
return 1;
|
|
506
|
+
return a.localeCompare(b);
|
|
507
|
+
});
|
|
508
|
+
}
|
|
509
|
+
if (keys.length === 0)
|
|
510
|
+
return '{}';
|
|
511
|
+
let res = '{\n';
|
|
512
|
+
keys.forEach((key, index) => {
|
|
513
|
+
const formattedKey = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(key) ? key : `'${key.replace(/'/g, "\\'")}'`;
|
|
514
|
+
const nextIsTestIdsOrSelectors = isTestIdsOrSelectors || key === 'testId' || key === 'testIds' || key === 'selector' || key === 'selectors';
|
|
515
|
+
res += `${spaces} ${formattedKey}: ${serializeRegistryObject(record[key], indent + 2, nextIsTestIdsOrSelectors)}${index < keys.length - 1 ? ',\n' : '\n'}`;
|
|
516
|
+
});
|
|
517
|
+
res += `${spaces}}`;
|
|
518
|
+
return res;
|
|
519
|
+
}
|
|
520
|
+
return 'undefined';
|
|
521
|
+
}
|
|
522
|
+
/**
|
|
523
|
+
* Updates or appends page object definitions inside a `registry.ts` file and updates any referencing test specs.
|
|
524
|
+
*/
|
|
525
|
+
function writeRegistry(filePath, registryObj, overrideMode, usedPageKeys) {
|
|
526
|
+
try {
|
|
527
|
+
const keyReplacements = {};
|
|
528
|
+
const usedSet = usedPageKeys instanceof Set ? usedPageKeys : new Set(usedPageKeys ?? []);
|
|
529
|
+
const codegenKeys = Object.keys(registryObj).filter((k) => {
|
|
530
|
+
const pageConfig = registryObj[k];
|
|
531
|
+
const isUsed = usedSet.has(k);
|
|
532
|
+
if (pageConfig && !isUsed) {
|
|
533
|
+
let hasLocators = false;
|
|
534
|
+
for (const sk of Object.keys(pageConfig)) {
|
|
535
|
+
const val = pageConfig[sk];
|
|
536
|
+
if (sk !== 'url' && val && typeof val === 'object' && Object.keys(val).length > 0) {
|
|
537
|
+
hasLocators = true;
|
|
538
|
+
break;
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
if (!hasLocators)
|
|
542
|
+
return false;
|
|
543
|
+
}
|
|
544
|
+
const isDefaultCodegen = k.startsWith('codegen_') || k === 'codegenPage';
|
|
545
|
+
if (isUsed) {
|
|
546
|
+
return true;
|
|
547
|
+
}
|
|
548
|
+
if (overrideMode) {
|
|
549
|
+
return true;
|
|
550
|
+
}
|
|
551
|
+
return isDefaultCodegen;
|
|
552
|
+
});
|
|
553
|
+
for (const codegenKey of codegenKeys) {
|
|
554
|
+
const pageConfig = registryObj[codegenKey];
|
|
555
|
+
if (pageConfig) {
|
|
556
|
+
if (pageConfig.testId) {
|
|
557
|
+
pageConfig.testId = collapsePageDict(pageConfig.testId, true, keyReplacements);
|
|
558
|
+
}
|
|
559
|
+
if (pageConfig.testIds) {
|
|
560
|
+
pageConfig.testIds = collapsePageDict(pageConfig.testIds, true, keyReplacements);
|
|
561
|
+
}
|
|
562
|
+
if (pageConfig.selector) {
|
|
563
|
+
pageConfig.selector = collapsePageDict(pageConfig.selector, false, keyReplacements);
|
|
564
|
+
}
|
|
565
|
+
if (pageConfig.selectors) {
|
|
566
|
+
pageConfig.selectors = collapsePageDict(pageConfig.selectors, false, keyReplacements);
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
let content = fs.readFileSync(filePath, 'utf8');
|
|
571
|
+
for (const codegenKey of codegenKeys) {
|
|
572
|
+
const escapedKey = codegenKey.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
573
|
+
const keyPattern = new RegExp(`(['"\`]?${escapedKey}['"\`]?\\s*:\\s*\\{)`);
|
|
574
|
+
const match = content.match(keyPattern);
|
|
575
|
+
if (match && match.index !== undefined) {
|
|
576
|
+
const startIdx = match.index + match[0].length - 1;
|
|
577
|
+
let braceCount = 0;
|
|
578
|
+
let endIdx = -1;
|
|
579
|
+
for (let i = startIdx; i < content.length; i++) {
|
|
580
|
+
if (content[i] === '{')
|
|
581
|
+
braceCount++;
|
|
582
|
+
else if (content[i] === '}') {
|
|
583
|
+
braceCount--;
|
|
584
|
+
if (braceCount === 0) {
|
|
585
|
+
endIdx = i + 1;
|
|
586
|
+
break;
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
if (endIdx !== -1) {
|
|
591
|
+
const updatedObjectText = serializeRegistryObject(registryObj[codegenKey], 2);
|
|
592
|
+
content = content.substring(0, startIdx) + updatedObjectText + content.substring(endIdx);
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
else {
|
|
596
|
+
const bounds = findRegistryCallBounds(content);
|
|
597
|
+
if (bounds) {
|
|
598
|
+
const lastBraceIdx = content.lastIndexOf('}', bounds.endIdx);
|
|
599
|
+
if (lastBraceIdx !== -1) {
|
|
600
|
+
const formattedKey = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(codegenKey)
|
|
601
|
+
? codegenKey
|
|
602
|
+
: `'${codegenKey}'`;
|
|
603
|
+
const serializedPage = ` ${formattedKey}: ${serializeRegistryObject(registryObj[codegenKey], 2)}`;
|
|
604
|
+
let lastCharIdx = lastBraceIdx - 1;
|
|
605
|
+
while (lastCharIdx > bounds.startIdx && /\s/.test(content[lastCharIdx])) {
|
|
606
|
+
lastCharIdx--;
|
|
607
|
+
}
|
|
608
|
+
if (content[lastCharIdx] === '}' && content[lastCharIdx + 1] !== ',') {
|
|
609
|
+
content = content.substring(0, lastCharIdx + 1) + ',\n' + serializedPage + '\n' + content.substring(lastBraceIdx);
|
|
610
|
+
}
|
|
611
|
+
else {
|
|
612
|
+
const beforeBrace = content.substring(bounds.startIdx, lastBraceIdx).trim();
|
|
613
|
+
const needsComma = beforeBrace.length > 0 && !beforeBrace.endsWith(',') && !beforeBrace.endsWith('{');
|
|
614
|
+
const insertText = (needsComma ? ',\n' : '\n') + serializedPage + '\n';
|
|
615
|
+
content = content.substring(0, lastBraceIdx) + insertText + content.substring(lastBraceIdx);
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
fs.writeFileSync(filePath, content, 'utf8');
|
|
622
|
+
if (Object.keys(keyReplacements).length > 0) {
|
|
623
|
+
const testsDir = path.join(path.dirname(filePath), '../tests');
|
|
624
|
+
if (fs.existsSync(testsDir)) {
|
|
625
|
+
const files = fs.readdirSync(testsDir);
|
|
626
|
+
for (const file of files) {
|
|
627
|
+
if (file.endsWith('.spec.ts') || file.endsWith('.test.ts')) {
|
|
628
|
+
const specPath = path.join(testsDir, file);
|
|
629
|
+
let specContent = fs.readFileSync(specPath, 'utf8');
|
|
630
|
+
let modified = false;
|
|
631
|
+
for (const oldKey of Object.keys(keyReplacements)) {
|
|
632
|
+
const newKey = keyReplacements[oldKey];
|
|
633
|
+
const regex = new RegExp(`(['"\`])${oldKey}(['"\`])`, 'g');
|
|
634
|
+
const before = specContent;
|
|
635
|
+
specContent = specContent.replace(regex, `$1${newKey}$2`);
|
|
636
|
+
if (specContent !== before)
|
|
637
|
+
modified = true;
|
|
638
|
+
}
|
|
639
|
+
if (modified) {
|
|
640
|
+
fs.writeFileSync(specPath, specContent, 'utf8');
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
return keyReplacements;
|
|
647
|
+
}
|
|
648
|
+
catch (e) {
|
|
649
|
+
console.error(`[pw-core] Error writing registry configuration to ${filePath}:`, e);
|
|
650
|
+
throw e;
|
|
651
|
+
}
|
|
652
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { SelectorStrategyType } from './types';
|
|
2
|
+
export interface SelectorMatch {
|
|
3
|
+
type: SelectorStrategyType;
|
|
4
|
+
val: string;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Parses a Playwright or CSS selector string and extracts its semantic strategy type and value.
|
|
8
|
+
*/
|
|
9
|
+
export declare function getSelectorValue(selector: string): SelectorMatch;
|
|
10
|
+
/**
|
|
11
|
+
* Extracts any `>> nth=N` or `>> first()` suffix from a Playwright selector.
|
|
12
|
+
*/
|
|
13
|
+
export declare function extractNthFromSelector(selector: string): {
|
|
14
|
+
baseSelector: string;
|
|
15
|
+
nth?: number;
|
|
16
|
+
};
|