pw-core 1.3.0 → 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.
Files changed (61) hide show
  1. package/README.md +1 -0
  2. package/dist/cli.js +145 -421
  3. package/dist/codegen/action-formatter.d.ts +19 -0
  4. package/dist/codegen/action-formatter.js +160 -0
  5. package/dist/codegen/action-processor.d.ts +23 -0
  6. package/dist/codegen/action-processor.js +112 -0
  7. package/dist/codegen/floating-panel/client.d.ts +14 -1
  8. package/dist/codegen/floating-panel/client.js +20 -25
  9. package/dist/codegen/floating-panel/manager.js +16 -22
  10. package/dist/codegen/generator/action.validator.d.ts +2 -1
  11. package/dist/codegen/generator/action.validator.js +2 -1
  12. package/dist/codegen/generator/candidate-scorer.d.ts +7 -0
  13. package/dist/codegen/generator/candidate-scorer.js +206 -0
  14. package/dist/codegen/generator/dom-scanner.d.ts +39 -0
  15. package/dist/codegen/generator/dom-scanner.js +429 -0
  16. package/dist/codegen/generator/id-stability.d.ts +6 -0
  17. package/dist/codegen/generator/id-stability.js +38 -0
  18. package/dist/codegen/generator/index.d.ts +8 -10
  19. package/dist/codegen/generator/index.js +23 -678
  20. package/dist/codegen/generator/key-builder.d.ts +13 -0
  21. package/dist/codegen/generator/key-builder.js +32 -0
  22. package/dist/codegen/hover-tracker/client.d.ts +5 -0
  23. package/dist/codegen/hover-tracker/client.js +14 -15
  24. package/dist/codegen/hover-tracker/manager.js +2 -6
  25. package/dist/codegen/index.d.ts +9 -32
  26. package/dist/codegen/index.js +9 -1128
  27. package/dist/codegen/key-utils.d.ts +16 -0
  28. package/dist/codegen/key-utils.js +29 -1
  29. package/dist/codegen/project-finder.d.ts +29 -0
  30. package/dist/codegen/project-finder.js +283 -0
  31. package/dist/codegen/registry-matcher.d.ts +27 -0
  32. package/dist/codegen/registry-matcher.js +254 -0
  33. package/dist/codegen/registry-store.d.ts +52 -0
  34. package/dist/codegen/registry-store.js +652 -0
  35. package/dist/codegen/selector-parser.d.ts +16 -0
  36. package/dist/codegen/selector-parser.js +121 -0
  37. package/dist/codegen/types.d.ts +18 -1
  38. package/dist/codegen/uniqueness.d.ts +3 -0
  39. package/dist/codegen/uniqueness.js +15 -0
  40. package/dist/component/table.d.ts +2 -2
  41. package/dist/component/table.js +7 -6
  42. package/dist/index.d.ts +3 -0
  43. package/dist/index.js +19 -0
  44. package/dist/page/actions/locator-actions.d.ts +6 -10
  45. package/dist/page/actions/locator-actions.js +24 -23
  46. package/dist/page/assertions/verify-chain.d.ts +11 -13
  47. package/dist/page/assertions/verify-chain.js +26 -7
  48. package/dist/page/assertions/verify-helpers.d.ts +5 -15
  49. package/dist/page/config.d.ts +25 -25
  50. package/dist/page/locators/dynamic-locator-resolver.js +22 -9
  51. package/dist/page/locators/resolver.d.ts +17 -11
  52. package/dist/page/locators/resolver.js +84 -78
  53. package/dist/page/registry.d.ts +35 -35
  54. package/dist/page/registry.js +86 -82
  55. package/dist/page/typed-page.d.ts +1 -0
  56. package/dist/page/typed-page.js +22 -12
  57. package/dist/page/types/proxy-methods.d.ts +11 -19
  58. package/dist/page/types/validation.d.ts +1 -1
  59. package/dist/page/utils/formatter.d.ts +3 -3
  60. package/dist/page/utils/formatter.js +5 -2
  61. 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
- const action_validator_1 = require("./codegen/generator/action.validator");
44
- function extractNthFromSelector(selector) {
45
- const nthMatch = selector.match(/\s*>>\s*nth=(\d+)/i);
46
- if (nthMatch) {
47
- const baseSelector = selector.replace(/\s*>>\s*nth=\d+/i, '').trim();
48
- return { baseSelector, nth: parseInt(nthMatch[1], 10) };
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
- if (selector.endsWith(' >> first()')) {
51
- const baseSelector = selector.substring(0, selector.length - ' >> first()'.length).trim();
52
- return { baseSelector, nth: 0 };
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
- catch (e) { }
79
- }
80
- return env;
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
- return '';
116
- }
117
- function getTestDir(cwd) {
118
- let configPath = path.join(cwd, 'playwright.config.ts');
119
- if (!fs.existsSync(configPath)) {
120
- configPath = path.join(cwd, 'playwright.config.js');
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
- if (fs.existsSync(configPath)) {
123
- try {
124
- const content = fs.readFileSync(configPath, 'utf8');
125
- const testDirMatch = content.match(/testDir:\s*['"`](.*?)['"`]/);
126
- if (testDirMatch && testDirMatch[1]) {
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
- catch (e) { }
78
+ else {
79
+ pageConfig[matchResult.type] = [matchResult.val];
80
+ }
131
81
  }
132
- return cwd;
133
82
  }
134
- function getNextTestIndex(testDir) {
135
- if (!fs.existsSync(testDir))
136
- return 1;
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 files = fs.readdirSync(testDir);
139
- let max = 0;
140
- for (const file of files) {
141
- const match = file.match(/^(\d+)\.recorded\.test\.(ts|js)$/);
142
- if (match) {
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
- function formatRecordingDate(date) {
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
- console.log('RESOLVED @playwright/test:', require.resolve('@playwright/test'));
175
- try {
176
- console.log('RESOLVED playwright-core:', require.resolve('playwright-core'));
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
- catch (e) { }
179
- const args = process.argv.slice(2).filter((arg) => arg !== 'codegen');
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
- let registryObj = (0, codegen_1.parseRegistry)(registryFilePath);
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
- let activeOutput = output || `${currentTestIndex}.recorded.test.ts`;
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
- console.log('DEBUG [cli]: onStartNewTest callback triggered on CLI host');
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
- console.log('DEBUG [cli]: onStartNewSerialTest callback triggered on CLI host');
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
- while (actionQueue.length > 0 || processing) {
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, code) => {
361
+ actionAdded: async (page, data) => {
433
362
  enqueueAction(async () => {
434
- const action = data.action;
435
- action.name = await (0, action_validator_1.normalizeActionName)(page, action.selector, action.name);
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
- if (action.selector) {
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
- if (!registryObj[pageKey]) {
536
- let pathname = '/';
537
- try {
538
- const u = new URL(page.url());
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, code) => {
393
+ actionUpdated: async (page, data) => {
580
394
  enqueueAction(async () => {
581
- const action = data.action;
582
- action.name = await (0, action_validator_1.normalizeActionName)(page, action.selector, action.name);
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
- if (action.selector) {
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: (page, data) => {
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
- if (!registryObj[pageKey]) {
704
- let pathname = '/';
705
- try {
706
- const u = new URL(page.url());
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
- else {
733
- if (!registryObj[pageKey][matchResult.type]) {
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 (e) { }
764
- console.log(`DEBUG [cli]: navigated url="${currentUrl}" title="${currentTitle}"`);
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 () => {