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
package/dist/cli.js
CHANGED
|
@@ -40,143 +40,92 @@ const test_1 = require("@playwright/test");
|
|
|
40
40
|
const codegen_1 = require("./codegen");
|
|
41
41
|
const floating_panel_1 = require("./codegen/floating-panel");
|
|
42
42
|
const hover_tracker_1 = require("./codegen/hover-tracker");
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
43
|
+
/**
|
|
44
|
+
* Adds a newly discovered locator element into the page configuration inside registryObj.
|
|
45
|
+
*/
|
|
46
|
+
function registerNewElementInRegistry(registryObj, pageKey, elementKey, matchResult, fallbackUrl) {
|
|
47
|
+
if (!registryObj[pageKey]) {
|
|
48
|
+
registryObj[pageKey] = { url: fallbackUrl };
|
|
49
49
|
}
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
return { baseSelector: selector };
|
|
55
|
-
}
|
|
56
|
-
function parseDotEnv(cwd) {
|
|
57
|
-
const env = {};
|
|
58
|
-
const envPath = path.join(cwd, '.env');
|
|
59
|
-
if (fs.existsSync(envPath)) {
|
|
60
|
-
try {
|
|
61
|
-
const content = fs.readFileSync(envPath, 'utf8');
|
|
62
|
-
const lines = content.split(/\r?\n/);
|
|
63
|
-
for (const line of lines) {
|
|
64
|
-
const trimmed = line.trim();
|
|
65
|
-
if (!trimmed || trimmed.startsWith('#'))
|
|
66
|
-
continue;
|
|
67
|
-
const index = trimmed.indexOf('=');
|
|
68
|
-
if (index > 0) {
|
|
69
|
-
const key = trimmed.slice(0, index).trim();
|
|
70
|
-
let val = trimmed.slice(index + 1).trim();
|
|
71
|
-
if ((val.startsWith("'") && val.endsWith("'")) || (val.startsWith('"') && val.endsWith('"'))) {
|
|
72
|
-
val = val.slice(1, -1);
|
|
73
|
-
}
|
|
74
|
-
env[key] = val;
|
|
75
|
-
}
|
|
76
|
-
}
|
|
50
|
+
const pageConfig = registryObj[pageKey];
|
|
51
|
+
if (matchResult.type === 'testId') {
|
|
52
|
+
if (pageConfig.testIds) {
|
|
53
|
+
pageConfig.testIds[elementKey] = matchResult.val;
|
|
77
54
|
}
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
function getBaseUrl(cwd) {
|
|
83
|
-
const env = parseDotEnv(cwd);
|
|
84
|
-
const envUrl = process.env.URL || env['URL'];
|
|
85
|
-
if (envUrl) {
|
|
86
|
-
return envUrl;
|
|
87
|
-
}
|
|
88
|
-
let configPath = path.join(cwd, 'playwright.config.ts');
|
|
89
|
-
if (!fs.existsSync(configPath)) {
|
|
90
|
-
configPath = path.join(cwd, 'playwright.config.js');
|
|
91
|
-
}
|
|
92
|
-
if (fs.existsSync(configPath)) {
|
|
93
|
-
try {
|
|
94
|
-
const content = fs.readFileSync(configPath, 'utf8');
|
|
95
|
-
const stringMatch = content.match(/baseURL:\s*['"`](.*?)['"`]/);
|
|
96
|
-
if (stringMatch && stringMatch[1]) {
|
|
97
|
-
return stringMatch[1];
|
|
98
|
-
}
|
|
99
|
-
if (/baseURL:\s*(?:env|ENV)\.url/.test(content)) {
|
|
100
|
-
let envFilePath = path.join(cwd, 'src', 'utils', 'env.ts');
|
|
101
|
-
if (!fs.existsSync(envFilePath)) {
|
|
102
|
-
envFilePath = path.join(cwd, 'src', 'utils', 'env.js');
|
|
103
|
-
}
|
|
104
|
-
if (fs.existsSync(envFilePath)) {
|
|
105
|
-
const envContent = fs.readFileSync(envFilePath, 'utf8');
|
|
106
|
-
const stringMatches = [...envContent.matchAll(/['"`](https?:\/\/.*?)['"`]/g)];
|
|
107
|
-
if (stringMatches.length > 0) {
|
|
108
|
-
return stringMatches[0][1];
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
}
|
|
55
|
+
else {
|
|
56
|
+
if (!pageConfig.testId)
|
|
57
|
+
pageConfig.testId = {};
|
|
58
|
+
pageConfig.testId[elementKey] = matchResult.val;
|
|
112
59
|
}
|
|
113
|
-
catch (e) { }
|
|
114
60
|
}
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
61
|
+
else if (matchResult.type === 'selector') {
|
|
62
|
+
if (pageConfig.selectors) {
|
|
63
|
+
pageConfig.selectors[elementKey] = matchResult.val;
|
|
64
|
+
}
|
|
65
|
+
else {
|
|
66
|
+
if (!pageConfig.selector)
|
|
67
|
+
pageConfig.selector = {};
|
|
68
|
+
pageConfig.selector[elementKey] = matchResult.val;
|
|
69
|
+
}
|
|
121
70
|
}
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
return path.resolve(cwd, testDirMatch[1]);
|
|
71
|
+
else {
|
|
72
|
+
const list = pageConfig[matchResult.type];
|
|
73
|
+
if (Array.isArray(list)) {
|
|
74
|
+
if (!list.includes(matchResult.val)) {
|
|
75
|
+
list.push(matchResult.val);
|
|
128
76
|
}
|
|
129
77
|
}
|
|
130
|
-
|
|
78
|
+
else {
|
|
79
|
+
pageConfig[matchResult.type] = [matchResult.val];
|
|
80
|
+
}
|
|
131
81
|
}
|
|
132
|
-
return cwd;
|
|
133
82
|
}
|
|
134
|
-
function
|
|
135
|
-
|
|
136
|
-
|
|
83
|
+
function printHelp() {
|
|
84
|
+
console.log(`
|
|
85
|
+
Usage: pw-core <command> [options]
|
|
86
|
+
|
|
87
|
+
Commands:
|
|
88
|
+
codegen [url] Start interactive Playwright test codegen with page registry
|
|
89
|
+
|
|
90
|
+
Options:
|
|
91
|
+
-u, --url <url> Target URL to record against
|
|
92
|
+
-o, --output <file> Output test file path (default: <index>.recorded.test.ts)
|
|
93
|
+
--safe Run in safe mode (append only, no existing key overrides)
|
|
94
|
+
-h, --help Show help
|
|
95
|
+
-v, --version Show version number
|
|
96
|
+
`);
|
|
97
|
+
}
|
|
98
|
+
function printVersion() {
|
|
137
99
|
try {
|
|
138
|
-
const
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
const num = parseInt(match[1], 10);
|
|
144
|
-
if (num > max) {
|
|
145
|
-
max = num;
|
|
146
|
-
}
|
|
147
|
-
}
|
|
100
|
+
const pkgPath = path.resolve(__dirname, '../package.json');
|
|
101
|
+
if (fs.existsSync(pkgPath)) {
|
|
102
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
|
|
103
|
+
console.log(`pw-core v${pkg.version}`);
|
|
104
|
+
return;
|
|
148
105
|
}
|
|
149
|
-
return max + 1;
|
|
150
|
-
}
|
|
151
|
-
catch (e) {
|
|
152
|
-
return 1;
|
|
153
106
|
}
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
const day = date.getDate();
|
|
157
|
-
const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
|
|
158
|
-
const month = months[date.getMonth()];
|
|
159
|
-
const year = date.getFullYear();
|
|
160
|
-
let hours = date.getHours();
|
|
161
|
-
const minutes = String(date.getMinutes()).padStart(2, '0');
|
|
162
|
-
const seconds = String(date.getSeconds()).padStart(2, '0');
|
|
163
|
-
const ampm = hours >= 12 ? 'PM' : 'AM';
|
|
164
|
-
hours = hours % 12;
|
|
165
|
-
hours = hours ? hours : 12;
|
|
166
|
-
return `${day} ${month} ${year}, ${hours}:${minutes}:${seconds} ${ampm}`;
|
|
167
|
-
}
|
|
168
|
-
function toWorkerKey(key) {
|
|
169
|
-
if (key.startsWith('worker'))
|
|
170
|
-
return key;
|
|
171
|
-
return `worker${key.charAt(0).toUpperCase()}${key.slice(1)}`;
|
|
107
|
+
catch { }
|
|
108
|
+
console.log('pw-core v1.3.1');
|
|
172
109
|
}
|
|
173
110
|
async function main() {
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
111
|
+
const rawArgs = process.argv.slice(2);
|
|
112
|
+
if (rawArgs.includes('-v') || rawArgs.includes('--version')) {
|
|
113
|
+
printVersion();
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
if (rawArgs.includes('-h') || rawArgs.includes('--help')) {
|
|
117
|
+
printHelp();
|
|
118
|
+
return;
|
|
177
119
|
}
|
|
178
|
-
|
|
179
|
-
|
|
120
|
+
const command = rawArgs.find((arg) => !arg.startsWith('-'));
|
|
121
|
+
if (command !== 'codegen') {
|
|
122
|
+
if (command) {
|
|
123
|
+
console.error(`Unknown command: "${command}"\n`);
|
|
124
|
+
}
|
|
125
|
+
printHelp();
|
|
126
|
+
process.exit(command ? 1 : 0);
|
|
127
|
+
}
|
|
128
|
+
const args = rawArgs.filter((arg) => arg !== 'codegen');
|
|
180
129
|
let url = '';
|
|
181
130
|
let output = '';
|
|
182
131
|
const overrideMode = !args.includes('--safe');
|
|
@@ -219,7 +168,7 @@ export const registry = createPageRegistry({
|
|
|
219
168
|
if (!url) {
|
|
220
169
|
const configPath = (0, codegen_1.findPlaywrightConfig)(process.cwd());
|
|
221
170
|
if (configPath) {
|
|
222
|
-
url = getBaseUrl(path.dirname(configPath));
|
|
171
|
+
url = (0, codegen_1.getBaseUrl)(path.dirname(configPath));
|
|
223
172
|
}
|
|
224
173
|
else {
|
|
225
174
|
let configDir = process.cwd();
|
|
@@ -232,14 +181,14 @@ export const registry = createPageRegistry({
|
|
|
232
181
|
}
|
|
233
182
|
current = path.dirname(current);
|
|
234
183
|
}
|
|
235
|
-
url = getBaseUrl(configDir);
|
|
184
|
+
url = (0, codegen_1.getBaseUrl)(configDir);
|
|
236
185
|
}
|
|
237
186
|
}
|
|
238
|
-
|
|
239
|
-
const testDir = path.join(getTestDir(process.cwd()), 'codegen');
|
|
187
|
+
const registryObj = (0, codegen_1.parseRegistry)(registryFilePath);
|
|
188
|
+
const testDir = path.join((0, codegen_1.getTestDir)(process.cwd()), 'codegen');
|
|
240
189
|
fs.mkdirSync(testDir, { recursive: true });
|
|
241
|
-
let currentTestIndex = getNextTestIndex(testDir);
|
|
242
|
-
|
|
190
|
+
let currentTestIndex = (0, codegen_1.getNextTestIndex)(testDir);
|
|
191
|
+
const activeOutput = output || `${currentTestIndex}.recorded.test.ts`;
|
|
243
192
|
let outputFilePath = path.resolve(testDir, activeOutput);
|
|
244
193
|
console.log(`Live spec output will be written to: ${outputFilePath}`);
|
|
245
194
|
const browser = await test_1.chromium.launch({ headless: false });
|
|
@@ -249,6 +198,34 @@ export const registry = createPageRegistry({
|
|
|
249
198
|
const testsInCurrentFile = [];
|
|
250
199
|
const recordedSteps = [];
|
|
251
200
|
const usedPageKeys = new Set();
|
|
201
|
+
const actionQueue = [];
|
|
202
|
+
let processing = false;
|
|
203
|
+
const waitForActionQueue = async (intervalMs) => {
|
|
204
|
+
while (actionQueue.length > 0 || processing) {
|
|
205
|
+
await new Promise((resolve) => setTimeout(resolve, intervalMs));
|
|
206
|
+
}
|
|
207
|
+
};
|
|
208
|
+
const enqueueAction = (task) => {
|
|
209
|
+
actionQueue.push(task);
|
|
210
|
+
processQueue();
|
|
211
|
+
};
|
|
212
|
+
const processQueue = async () => {
|
|
213
|
+
if (processing)
|
|
214
|
+
return;
|
|
215
|
+
processing = true;
|
|
216
|
+
while (actionQueue.length > 0) {
|
|
217
|
+
const task = actionQueue.shift();
|
|
218
|
+
if (task) {
|
|
219
|
+
try {
|
|
220
|
+
await task();
|
|
221
|
+
}
|
|
222
|
+
catch (err) {
|
|
223
|
+
console.error('Error processing action queue task:', err);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
processing = false;
|
|
228
|
+
};
|
|
252
229
|
const panelManager = new floating_panel_1.FloatingPanelManager(context, {
|
|
253
230
|
getDisplayTestIndex: () => {
|
|
254
231
|
if (isSerialSuite) {
|
|
@@ -261,18 +238,12 @@ export const registry = createPageRegistry({
|
|
|
261
238
|
},
|
|
262
239
|
hasSteps: () => recordedSteps.length > 0,
|
|
263
240
|
onStartNewTest: async () => {
|
|
264
|
-
|
|
265
|
-
// Wait for any pending page actions to be fully processed first so we don't lose the click that triggered the navigation/action
|
|
266
|
-
while (actionQueue.length > 0 || processing) {
|
|
267
|
-
await new Promise((resolve) => setTimeout(resolve, 20));
|
|
268
|
-
}
|
|
241
|
+
await waitForActionQueue(20);
|
|
269
242
|
const keyReplacements = (0, codegen_1.writeRegistry)(registryFilePath, registryObj, overrideMode, usedPageKeys);
|
|
270
243
|
applyReplacements(keyReplacements);
|
|
271
244
|
updateSpecFile();
|
|
272
|
-
// Reset serial state for new test file
|
|
273
245
|
isSerialSuite = false;
|
|
274
246
|
testsInCurrentFile.length = 0;
|
|
275
|
-
// 2. Increment test index and update outputFilePath
|
|
276
247
|
currentTestIndex++;
|
|
277
248
|
testStartedAt = new Date();
|
|
278
249
|
const extension = path.extname(output || '1.recorded.test.ts') || '.ts';
|
|
@@ -283,33 +254,22 @@ export const registry = createPageRegistry({
|
|
|
283
254
|
}
|
|
284
255
|
outputFilePath = path.resolve(testDir, newOutputName);
|
|
285
256
|
console.log(`\n>>> STARTING NEW TEST: ${outputFilePath} <<<\n`);
|
|
286
|
-
// 3. Clear steps and usedPageKeys for the new test session
|
|
287
257
|
recordedSteps.length = 0;
|
|
288
258
|
usedPageKeys.clear();
|
|
289
|
-
// 4. Re-inject panel on all open pages with the updated index
|
|
290
259
|
await panelManager.injectAll();
|
|
291
260
|
},
|
|
292
261
|
onStartNewSerialTest: async () => {
|
|
293
|
-
|
|
294
|
-
// Wait for any pending page actions to be fully processed first so we don't lose the click that triggered the navigation/action
|
|
295
|
-
while (actionQueue.length > 0 || processing) {
|
|
296
|
-
await new Promise((resolve) => setTimeout(resolve, 20));
|
|
297
|
-
}
|
|
298
|
-
console.log(`DEBUG [cli]: __pwCoreStartNewSerialTest triggered. currentTestIndex=${currentTestIndex}, testsCount=${testsInCurrentFile.length}, activeSteps=${recordedSteps.length}`);
|
|
262
|
+
await waitForActionQueue(20);
|
|
299
263
|
isSerialSuite = true;
|
|
300
|
-
// Save current steps as a test case
|
|
301
264
|
const testName = `${currentTestIndex}.${testsInCurrentFile.length + 1}. Recorded Test`;
|
|
302
265
|
testsInCurrentFile.push({
|
|
303
266
|
name: testName,
|
|
304
267
|
steps: [...recordedSteps],
|
|
305
268
|
usedKeys: new Set(usedPageKeys)
|
|
306
269
|
});
|
|
307
|
-
// Clear active steps and usedPageKeys for the next serial test
|
|
308
270
|
recordedSteps.length = 0;
|
|
309
271
|
usedPageKeys.clear();
|
|
310
|
-
// Write the current state to the file
|
|
311
272
|
updateSpecFile();
|
|
312
|
-
// Re-inject/update panel on all open pages
|
|
313
273
|
await panelManager.injectAll();
|
|
314
274
|
}
|
|
315
275
|
});
|
|
@@ -320,36 +280,34 @@ export const registry = createPageRegistry({
|
|
|
320
280
|
importPath = './' + importPath;
|
|
321
281
|
}
|
|
322
282
|
importPath = importPath.replace(/\.(ts|js)$/, '');
|
|
323
|
-
const dateStr = formatRecordingDate(testStartedAt);
|
|
283
|
+
const dateStr = (0, codegen_1.formatRecordingDate)(testStartedAt);
|
|
324
284
|
let fileContent = '';
|
|
325
285
|
if (isSerialSuite) {
|
|
326
286
|
const cases = [];
|
|
327
|
-
// Add previously completed test cases
|
|
328
287
|
for (const tc of testsInCurrentFile) {
|
|
329
288
|
if (tc.steps.length === 0)
|
|
330
289
|
continue;
|
|
331
|
-
const pageListStr = Array.from(tc.usedKeys).map(toWorkerKey).join(', ');
|
|
332
|
-
const tcSteps = tc.steps.map(s => {
|
|
290
|
+
const pageListStr = Array.from(tc.usedKeys).map(codegen_1.toWorkerKey).join(', ');
|
|
291
|
+
const tcSteps = tc.steps.map((s) => {
|
|
333
292
|
let code = s.code;
|
|
334
293
|
for (const pk of Array.from(tc.usedKeys)) {
|
|
335
|
-
code = code.replace(new RegExp(`\\b${pk}\\.`, 'g'), `${toWorkerKey(pk)}.`);
|
|
294
|
+
code = code.replace(new RegExp(`\\b${pk}\\.`, 'g'), `${(0, codegen_1.toWorkerKey)(pk)}.`);
|
|
336
295
|
}
|
|
337
|
-
return code.split('\n').map(line => ' ' + line.trim()).join('\n');
|
|
296
|
+
return code.split('\n').map((line) => ' ' + line.trim()).join('\n');
|
|
338
297
|
}).join('\n');
|
|
339
298
|
cases.push(` scenario('${tc.name}', async ({ ${pageListStr || 'workerPage'} }) => {
|
|
340
299
|
${tcSteps}
|
|
341
300
|
});`);
|
|
342
301
|
}
|
|
343
|
-
// Add the currently recording test case
|
|
344
302
|
if (recordedSteps.length > 0) {
|
|
345
303
|
const currentName = `${currentTestIndex}.${testsInCurrentFile.length + 1}. Recorded Test`;
|
|
346
|
-
const pageListStr = Array.from(usedPageKeys).map(toWorkerKey).join(', ');
|
|
347
|
-
const currentSteps = recordedSteps.map(s => {
|
|
304
|
+
const pageListStr = Array.from(usedPageKeys).map(codegen_1.toWorkerKey).join(', ');
|
|
305
|
+
const currentSteps = recordedSteps.map((s) => {
|
|
348
306
|
let code = s.code;
|
|
349
307
|
for (const pk of Array.from(usedPageKeys)) {
|
|
350
|
-
code = code.replace(new RegExp(`\\b${pk}\\.`, 'g'), `${toWorkerKey(pk)}.`);
|
|
308
|
+
code = code.replace(new RegExp(`\\b${pk}\\.`, 'g'), `${(0, codegen_1.toWorkerKey)(pk)}.`);
|
|
351
309
|
}
|
|
352
|
-
return code.split('\n').map(line => ' ' + line.trim()).join('\n');
|
|
310
|
+
return code.split('\n').map((line) => ' ' + line.trim()).join('\n');
|
|
353
311
|
}).join('\n');
|
|
354
312
|
cases.push(` scenario('${currentName}', async ({ ${pageListStr || 'workerPage'} }) => {
|
|
355
313
|
${currentSteps}
|
|
@@ -389,186 +347,42 @@ ${recordedSteps.map((s) => s.code).join('\n')}
|
|
|
389
347
|
}
|
|
390
348
|
};
|
|
391
349
|
const finalize = async () => {
|
|
392
|
-
|
|
393
|
-
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
394
|
-
}
|
|
350
|
+
await waitForActionQueue(50);
|
|
395
351
|
const keyReplacements = (0, codegen_1.writeRegistry)(registryFilePath, registryObj, overrideMode, usedPageKeys);
|
|
396
352
|
applyReplacements(keyReplacements);
|
|
397
353
|
updateSpecFile();
|
|
398
354
|
};
|
|
399
|
-
// Initialize spec file immediately
|
|
400
355
|
updateSpecFile();
|
|
401
356
|
let lastActionSignature = '';
|
|
402
357
|
let lastActionTime = 0;
|
|
403
|
-
const actionQueue = [];
|
|
404
|
-
let processing = false;
|
|
405
|
-
const enqueueAction = (task) => {
|
|
406
|
-
actionQueue.push(task);
|
|
407
|
-
processQueue();
|
|
408
|
-
};
|
|
409
|
-
const processQueue = async () => {
|
|
410
|
-
if (processing)
|
|
411
|
-
return;
|
|
412
|
-
processing = true;
|
|
413
|
-
while (actionQueue.length > 0) {
|
|
414
|
-
const task = actionQueue.shift();
|
|
415
|
-
if (task) {
|
|
416
|
-
try {
|
|
417
|
-
await task();
|
|
418
|
-
}
|
|
419
|
-
catch (err) {
|
|
420
|
-
console.error('Error processing action queue task:', err);
|
|
421
|
-
}
|
|
422
|
-
}
|
|
423
|
-
}
|
|
424
|
-
processing = false;
|
|
425
|
-
};
|
|
426
|
-
// Track the current page URL and title as the user navigates.
|
|
427
|
-
// data.frame.url / data.frame.title are always undefined in this Playwright build,
|
|
428
|
-
// so we maintain our own state that is kept up-to-date via page navigation events.
|
|
429
358
|
let currentUrl = url || '';
|
|
430
359
|
let currentTitle = '';
|
|
431
360
|
const eventSink = {
|
|
432
|
-
actionAdded: async (page, data
|
|
361
|
+
actionAdded: async (page, data) => {
|
|
433
362
|
enqueueAction(async () => {
|
|
434
|
-
const
|
|
435
|
-
|
|
436
|
-
console.log('DEBUG [cli]: eventSink.actionAdded called for action:', action.name, 'selector:', action.selector);
|
|
437
|
-
if (action.name === 'openPage' || action.name === 'closePage')
|
|
363
|
+
const processed = await (0, codegen_1.processRecordedAction)(page, data.action, currentUrl, currentTitle, registryObj, overrideMode);
|
|
364
|
+
if (!processed)
|
|
438
365
|
return;
|
|
439
|
-
|
|
440
|
-
const lowerSel = action.selector.toLowerCase();
|
|
441
|
-
if (lowerSel.includes('pw-core') ||
|
|
442
|
-
lowerSel.includes('pwcore') ||
|
|
443
|
-
lowerSel.includes('new test') ||
|
|
444
|
-
lowerSel.includes('add serial') ||
|
|
445
|
-
lowerSel.includes('new-serial') ||
|
|
446
|
-
lowerSel.includes('new-test')) {
|
|
447
|
-
console.log(`DEBUG [cli]: Ignoring own panel action (string match) in actionAdded: selector="${action.selector}"`);
|
|
448
|
-
return;
|
|
449
|
-
}
|
|
450
|
-
try {
|
|
451
|
-
const isOurPanel = await page.locator(action.selector).evaluate((el) => {
|
|
452
|
-
return el.id === 'pw-core-codegen-panel' || el.closest('#pw-core-codegen-panel') !== null;
|
|
453
|
-
}, null, { timeout: 500 }).catch(() => false);
|
|
454
|
-
if (isOurPanel) {
|
|
455
|
-
console.log(`DEBUG [cli]: Ignoring own panel action in actionAdded: selector="${action.selector}"`);
|
|
456
|
-
return;
|
|
457
|
-
}
|
|
458
|
-
}
|
|
459
|
-
catch (e) { }
|
|
460
|
-
}
|
|
461
|
-
let pageKey = (0, codegen_1.findPageKey)(currentUrl, currentTitle, registryObj, overrideMode);
|
|
462
|
-
console.log(`DEBUG [cli]: url="${currentUrl}" title="${currentTitle}" → pageKey="${pageKey}"`);
|
|
463
|
-
if (!registryObj[pageKey]) {
|
|
464
|
-
let effectiveUrl = '/';
|
|
465
|
-
try {
|
|
466
|
-
const u = new URL(currentUrl);
|
|
467
|
-
const hash = u.hash && u.hash.startsWith('#/') ? u.hash.slice(1) : '';
|
|
468
|
-
const targetUrl = (hash && hash !== '/') ? hash : u.pathname;
|
|
469
|
-
// Strip any nested query params or nested hash IDs from targetUrl
|
|
470
|
-
const clean = targetUrl.split('?')[0].split('#')[0];
|
|
471
|
-
effectiveUrl = clean.startsWith('/') ? clean : '/' + clean;
|
|
472
|
-
}
|
|
473
|
-
catch (e) { }
|
|
474
|
-
registryObj[pageKey] = { url: effectiveUrl };
|
|
475
|
-
}
|
|
476
|
-
let elementKey = 'element';
|
|
477
|
-
let actionNth = undefined;
|
|
478
|
-
let matchResult = null;
|
|
479
|
-
if (action.selector) {
|
|
480
|
-
let selectorToUse = action.selector;
|
|
481
|
-
const smartLocator = await (0, codegen_1.generateSmartLocator)(page, action.selector);
|
|
482
|
-
if (smartLocator) {
|
|
483
|
-
console.log(`DEBUG [cli]: smart locator: ${smartLocator.locator} (strategy: ${smartLocator.strategy}, base: ${smartLocator.baseScore}, semantic: ${smartLocator.semanticScore}, context: ${smartLocator.contextScore}, total: ${smartLocator.totalScore})`);
|
|
484
|
-
if (smartLocator.nearbyText)
|
|
485
|
-
console.log(`DEBUG [cli]: nearbyText: "${smartLocator.nearbyText}"`);
|
|
486
|
-
if (smartLocator.accessibleName)
|
|
487
|
-
console.log(`DEBUG [cli]: accessibleName: "${smartLocator.accessibleName}"`);
|
|
488
|
-
if (smartLocator.generatedKey)
|
|
489
|
-
console.log(`DEBUG [cli]: generatedKey: "${smartLocator.generatedKey}"`);
|
|
490
|
-
selectorToUse = smartLocator.selector;
|
|
491
|
-
}
|
|
492
|
-
const parsed = extractNthFromSelector(selectorToUse);
|
|
493
|
-
selectorToUse = parsed.baseSelector;
|
|
494
|
-
actionNth = parsed.nth;
|
|
495
|
-
if (actionNth !== undefined) {
|
|
496
|
-
action.nth = actionNth;
|
|
497
|
-
}
|
|
498
|
-
matchResult = (0, codegen_1.findElementKey)(selectorToUse, pageKey, registryObj, overrideMode, {
|
|
499
|
-
targetTestId: smartLocator?.targetTestId,
|
|
500
|
-
targetId: smartLocator?.targetId,
|
|
501
|
-
targetParentId: smartLocator?.targetParentId,
|
|
502
|
-
overrideType: (action.name === 'check' || action.name === 'uncheck') ? 'checkbox' : undefined
|
|
503
|
-
});
|
|
504
|
-
pageKey = matchResult.pageKey;
|
|
505
|
-
elementKey = matchResult.elementKey;
|
|
506
|
-
// Use context-aware generatedKey if it produced a better name and the match is new
|
|
507
|
-
// Only for key-based strategies (testId/selector); array strategies must keep original text
|
|
508
|
-
if (matchResult.isNew && (matchResult.type === 'testId' || matchResult.type === 'selector') && smartLocator?.generatedKey && smartLocator.generatedKey.length > 2) {
|
|
509
|
-
const proposedKey = smartLocator.generatedKey;
|
|
510
|
-
const existingKeys = new Set([
|
|
511
|
-
...Object.keys(registryObj[pageKey]?.testId || {}),
|
|
512
|
-
...Object.keys(registryObj[pageKey]?.testIds || {}),
|
|
513
|
-
...Object.keys(registryObj[pageKey]?.selector || {}),
|
|
514
|
-
...Object.keys(registryObj[pageKey]?.selectors || {})
|
|
515
|
-
]);
|
|
516
|
-
if (!existingKeys.has(proposedKey)) {
|
|
517
|
-
elementKey = proposedKey;
|
|
518
|
-
}
|
|
519
|
-
}
|
|
520
|
-
}
|
|
366
|
+
const { pageKey, elementKey, action, matchResult } = processed;
|
|
521
367
|
const actionSignature = `${pageKey}.${elementKey}.${action.name}.${action.text || action.key || action.value || ''}.${action.nth ?? ''}`;
|
|
522
368
|
const now = Date.now();
|
|
523
369
|
const threshold = action.name === 'click' ? 1200 : 500;
|
|
524
370
|
if (actionSignature === lastActionSignature && now - lastActionTime < threshold) {
|
|
525
|
-
console.log(`DEBUG [cli]: Deduplicated consecutive action: ${actionSignature}`);
|
|
526
371
|
return;
|
|
527
372
|
}
|
|
528
373
|
lastActionSignature = actionSignature;
|
|
529
374
|
lastActionTime = now;
|
|
530
375
|
usedPageKeys.add(pageKey);
|
|
531
376
|
const generatedCode = (0, codegen_1.formatActionCall)(pageKey, elementKey, action);
|
|
532
|
-
console.log('DEBUG [cli]: generatedCode:', generatedCode);
|
|
533
377
|
recordedSteps.push({ pageKey, code: generatedCode });
|
|
534
378
|
if (matchResult && matchResult.isNew) {
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
pathname = u.pathname;
|
|
540
|
-
}
|
|
541
|
-
catch (e) { }
|
|
542
|
-
registryObj[pageKey] = { url: pathname };
|
|
543
|
-
}
|
|
544
|
-
if (matchResult.type === 'testId') {
|
|
545
|
-
if (registryObj[pageKey].testIds) {
|
|
546
|
-
registryObj[pageKey].testIds[elementKey] = matchResult.val;
|
|
547
|
-
}
|
|
548
|
-
else {
|
|
549
|
-
if (!registryObj[pageKey].testId)
|
|
550
|
-
registryObj[pageKey].testId = {};
|
|
551
|
-
registryObj[pageKey].testId[elementKey] = matchResult.val;
|
|
552
|
-
}
|
|
553
|
-
}
|
|
554
|
-
else if (matchResult.type === 'selector') {
|
|
555
|
-
if (registryObj[pageKey].selectors) {
|
|
556
|
-
registryObj[pageKey].selectors[elementKey] = matchResult.val;
|
|
557
|
-
}
|
|
558
|
-
else {
|
|
559
|
-
if (!registryObj[pageKey].selector)
|
|
560
|
-
registryObj[pageKey].selector = {};
|
|
561
|
-
registryObj[pageKey].selector[elementKey] = matchResult.val;
|
|
562
|
-
}
|
|
563
|
-
}
|
|
564
|
-
else {
|
|
565
|
-
if (!registryObj[pageKey][matchResult.type]) {
|
|
566
|
-
registryObj[pageKey][matchResult.type] = [];
|
|
567
|
-
}
|
|
568
|
-
if (!registryObj[pageKey][matchResult.type].includes(matchResult.val)) {
|
|
569
|
-
registryObj[pageKey][matchResult.type].push(matchResult.val);
|
|
570
|
-
}
|
|
379
|
+
let pathname = '/';
|
|
380
|
+
try {
|
|
381
|
+
const u = new URL(page.url());
|
|
382
|
+
pathname = u.pathname;
|
|
571
383
|
}
|
|
384
|
+
catch { }
|
|
385
|
+
registerNewElementInRegistry(registryObj, pageKey, elementKey, matchResult, pathname);
|
|
572
386
|
const keyReplacements = (0, codegen_1.writeRegistry)(registryFilePath, registryObj, overrideMode, usedPageKeys);
|
|
573
387
|
applyReplacements(keyReplacements);
|
|
574
388
|
}
|
|
@@ -576,75 +390,14 @@ ${recordedSteps.map((s) => s.code).join('\n')}
|
|
|
576
390
|
await panelManager.injectAll();
|
|
577
391
|
});
|
|
578
392
|
},
|
|
579
|
-
actionUpdated: async (page, data
|
|
393
|
+
actionUpdated: async (page, data) => {
|
|
580
394
|
enqueueAction(async () => {
|
|
581
|
-
const
|
|
582
|
-
|
|
583
|
-
console.log('DEBUG [cli]: eventSink.actionUpdated called for action:', action.name, 'selector:', action.selector);
|
|
584
|
-
if (action.name === 'openPage' || action.name === 'closePage')
|
|
395
|
+
const processed = await (0, codegen_1.processRecordedAction)(page, data.action, currentUrl, currentTitle, registryObj, overrideMode);
|
|
396
|
+
if (!processed)
|
|
585
397
|
return;
|
|
586
|
-
|
|
587
|
-
const lowerSel = action.selector.toLowerCase();
|
|
588
|
-
if (lowerSel.includes('pw-core') ||
|
|
589
|
-
lowerSel.includes('pwcore') ||
|
|
590
|
-
lowerSel.includes('new test') ||
|
|
591
|
-
lowerSel.includes('add serial') ||
|
|
592
|
-
lowerSel.includes('new-serial') ||
|
|
593
|
-
lowerSel.includes('new-test')) {
|
|
594
|
-
console.log(`DEBUG [cli]: Ignoring own panel action (string match) in actionUpdated: selector="${action.selector}"`);
|
|
595
|
-
return;
|
|
596
|
-
}
|
|
597
|
-
try {
|
|
598
|
-
const isOurPanel = await page.locator(action.selector).evaluate((el) => {
|
|
599
|
-
return el.id === 'pw-core-codegen-panel' || el.closest('#pw-core-codegen-panel') !== null;
|
|
600
|
-
}, null, { timeout: 500 }).catch(() => false);
|
|
601
|
-
if (isOurPanel) {
|
|
602
|
-
console.log(`DEBUG [cli]: Ignoring own panel action in actionUpdated: selector="${action.selector}"`);
|
|
603
|
-
return;
|
|
604
|
-
}
|
|
605
|
-
}
|
|
606
|
-
catch (e) { }
|
|
607
|
-
}
|
|
608
|
-
let pageKey = (0, codegen_1.findPageKey)(currentUrl, currentTitle, registryObj, overrideMode);
|
|
609
|
-
if (!registryObj[pageKey]) {
|
|
610
|
-
let effectiveUrl = '/';
|
|
611
|
-
try {
|
|
612
|
-
const u = new URL(currentUrl);
|
|
613
|
-
const hash = u.hash && u.hash.startsWith('#/') ? u.hash.slice(1) : '';
|
|
614
|
-
const targetUrl = (hash && hash !== '/') ? hash : u.pathname;
|
|
615
|
-
// Strip any nested query params or nested hash IDs from targetUrl
|
|
616
|
-
const clean = targetUrl.split('?')[0].split('#')[0];
|
|
617
|
-
effectiveUrl = clean.startsWith('/') ? clean : '/' + clean;
|
|
618
|
-
}
|
|
619
|
-
catch (e) { }
|
|
620
|
-
registryObj[pageKey] = { url: effectiveUrl };
|
|
621
|
-
}
|
|
622
|
-
let elementKey = 'element';
|
|
623
|
-
let actionNth = undefined;
|
|
624
|
-
if (action.selector) {
|
|
625
|
-
let selectorToUse = action.selector;
|
|
626
|
-
const smartLocator = await (0, codegen_1.generateSmartLocator)(page, action.selector);
|
|
627
|
-
if (smartLocator) {
|
|
628
|
-
selectorToUse = smartLocator.selector;
|
|
629
|
-
}
|
|
630
|
-
const parsed = extractNthFromSelector(selectorToUse);
|
|
631
|
-
selectorToUse = parsed.baseSelector;
|
|
632
|
-
actionNth = parsed.nth;
|
|
633
|
-
if (actionNth !== undefined) {
|
|
634
|
-
action.nth = actionNth;
|
|
635
|
-
}
|
|
636
|
-
const matchResult = (0, codegen_1.findElementKey)(selectorToUse, pageKey, registryObj, overrideMode, {
|
|
637
|
-
targetTestId: smartLocator?.targetTestId,
|
|
638
|
-
targetId: smartLocator?.targetId,
|
|
639
|
-
targetParentId: smartLocator?.targetParentId,
|
|
640
|
-
overrideType: (action.name === 'check' || action.name === 'uncheck') ? 'checkbox' : undefined
|
|
641
|
-
});
|
|
642
|
-
pageKey = matchResult.pageKey;
|
|
643
|
-
elementKey = matchResult.elementKey;
|
|
644
|
-
}
|
|
398
|
+
const { pageKey, elementKey, action } = processed;
|
|
645
399
|
usedPageKeys.add(pageKey);
|
|
646
400
|
const generatedCode = (0, codegen_1.formatActionCall)(pageKey, elementKey, action);
|
|
647
|
-
console.log('DEBUG [cli]: generatedCode updated:', generatedCode);
|
|
648
401
|
lastActionSignature = `${pageKey}.${elementKey}.${action.name}.${action.text || action.key || action.value || ''}.${action.nth ?? ''}`;
|
|
649
402
|
lastActionTime = Date.now();
|
|
650
403
|
if (recordedSteps.length > 0) {
|
|
@@ -660,9 +413,7 @@ ${recordedSteps.map((s) => s.code).join('\n')}
|
|
|
660
413
|
await panelManager.injectAll();
|
|
661
414
|
});
|
|
662
415
|
},
|
|
663
|
-
signalAdded: (
|
|
664
|
-
console.log('DEBUG [cli]: eventSink.signalAdded called');
|
|
665
|
-
}
|
|
416
|
+
signalAdded: (_page, _data) => { }
|
|
666
417
|
};
|
|
667
418
|
const hoverTracker = new hover_tracker_1.HoverTrackerManager(context, {
|
|
668
419
|
onRecordHover: async (selector) => {
|
|
@@ -679,7 +430,7 @@ ${recordedSteps.map((s) => s.code).join('\n')}
|
|
|
679
430
|
let pageKey = (0, codegen_1.findPageKey)(activePage.url(), await activePage.title(), registryObj, overrideMode);
|
|
680
431
|
let elementKey = 'codegenPage';
|
|
681
432
|
let selectorToUse = smartLocator.selector;
|
|
682
|
-
const parsed = extractNthFromSelector(selectorToUse);
|
|
433
|
+
const parsed = (0, codegen_1.extractNthFromSelector)(selectorToUse);
|
|
683
434
|
selectorToUse = parsed.baseSelector;
|
|
684
435
|
const hoverAction = { name: 'hover' };
|
|
685
436
|
if (parsed.nth !== undefined) {
|
|
@@ -697,46 +448,17 @@ ${recordedSteps.map((s) => s.code).join('\n')}
|
|
|
697
448
|
lastActionSignature = actionSignature;
|
|
698
449
|
lastActionTime = now;
|
|
699
450
|
usedPageKeys.add(pageKey);
|
|
700
|
-
console.log('DEBUG [cli]: Hover action recorded:', generatedCode);
|
|
701
451
|
recordedSteps.push({ pageKey, code: generatedCode });
|
|
702
452
|
if (matchResult && matchResult.isNew) {
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
pathname = u.pathname;
|
|
708
|
-
}
|
|
709
|
-
catch (e) { }
|
|
710
|
-
registryObj[pageKey] = { url: pathname };
|
|
711
|
-
}
|
|
712
|
-
if (matchResult.type === 'testId') {
|
|
713
|
-
if (registryObj[pageKey].testIds) {
|
|
714
|
-
registryObj[pageKey].testIds[elementKey] = matchResult.val;
|
|
715
|
-
}
|
|
716
|
-
else {
|
|
717
|
-
if (!registryObj[pageKey].testId)
|
|
718
|
-
registryObj[pageKey].testId = {};
|
|
719
|
-
registryObj[pageKey].testId[elementKey] = matchResult.val;
|
|
720
|
-
}
|
|
721
|
-
}
|
|
722
|
-
else if (matchResult.type === 'selector') {
|
|
723
|
-
if (registryObj[pageKey].selectors) {
|
|
724
|
-
registryObj[pageKey].selectors[elementKey] = matchResult.val;
|
|
725
|
-
}
|
|
726
|
-
else {
|
|
727
|
-
if (!registryObj[pageKey].selector)
|
|
728
|
-
registryObj[pageKey].selector = {};
|
|
729
|
-
registryObj[pageKey].selector[elementKey] = matchResult.val;
|
|
730
|
-
}
|
|
453
|
+
let pathname = '/';
|
|
454
|
+
try {
|
|
455
|
+
const u = new URL(page.url());
|
|
456
|
+
pathname = u.pathname;
|
|
731
457
|
}
|
|
732
|
-
|
|
733
|
-
if
|
|
734
|
-
registryObj[pageKey][matchResult.type] = [];
|
|
735
|
-
}
|
|
736
|
-
if (!registryObj[pageKey][matchResult.type].includes(matchResult.val)) {
|
|
737
|
-
registryObj[pageKey][matchResult.type].push(matchResult.val);
|
|
738
|
-
}
|
|
458
|
+
catch {
|
|
459
|
+
// Fall back to '/' if page URL cannot be parsed
|
|
739
460
|
}
|
|
461
|
+
registerNewElementInRegistry(registryObj, pageKey, elementKey, matchResult, pathname);
|
|
740
462
|
const keyReplacements = (0, codegen_1.writeRegistry)(registryFilePath, registryObj, overrideMode, usedPageKeys);
|
|
741
463
|
applyReplacements(keyReplacements);
|
|
742
464
|
}
|
|
@@ -747,25 +469,27 @@ ${recordedSteps.map((s) => s.code).join('\n')}
|
|
|
747
469
|
});
|
|
748
470
|
await hoverTracker.initialize();
|
|
749
471
|
const page = await context.newPage();
|
|
472
|
+
await panelManager.inject(page);
|
|
750
473
|
page.on('close', async () => {
|
|
751
474
|
console.log('Page closed. Closing browser...');
|
|
752
475
|
await finalize();
|
|
753
476
|
await browser.close().catch(() => { });
|
|
754
477
|
process.exit(0);
|
|
755
478
|
});
|
|
756
|
-
// Track page URL and title on every navigation (real, hash, or pushState)
|
|
757
479
|
page.on('framenavigated', async (frame) => {
|
|
758
480
|
if (frame === page.mainFrame()) {
|
|
759
481
|
currentUrl = page.url();
|
|
760
482
|
try {
|
|
761
483
|
currentTitle = await page.title();
|
|
762
484
|
}
|
|
763
|
-
catch
|
|
764
|
-
|
|
485
|
+
catch {
|
|
486
|
+
// Ignore title fetch error if page context is actively transitioning
|
|
487
|
+
}
|
|
765
488
|
}
|
|
766
489
|
});
|
|
767
490
|
if (url && typeof url === 'string') {
|
|
768
491
|
await page.goto(url.startsWith('http') ? url : `http://${url}`);
|
|
492
|
+
await panelManager.inject(page);
|
|
769
493
|
}
|
|
770
494
|
await context._enableRecorder({ language: 'javascript', mode: 'recording', recorderMode: 'api' }, eventSink);
|
|
771
495
|
browser.on('disconnected', async () => {
|