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
@@ -0,0 +1,19 @@
1
+ /** Normalized subset of a Playwright recorder action used for spec generation. */
2
+ export interface RecordedAction {
3
+ name: string;
4
+ nth?: number;
5
+ clickCount?: number;
6
+ modifiers?: string[];
7
+ text?: string;
8
+ files?: unknown;
9
+ key?: string;
10
+ options?: unknown;
11
+ substring?: boolean;
12
+ checked?: boolean;
13
+ value?: string;
14
+ ariaSnapshot?: string;
15
+ }
16
+ /**
17
+ * Formats a recorded user action into executable Playwright test code using typed page methods.
18
+ */
19
+ export declare function formatActionCall(pageKey: string, elementKey: string, action: RecordedAction, isTextSelector?: boolean, textValue?: string): string;
@@ -0,0 +1,160 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.formatActionCall = formatActionCall;
4
+ /**
5
+ * Formats a recorded user action into executable Playwright test code using typed page methods.
6
+ */
7
+ function formatActionCall(pageKey, elementKey, action, isTextSelector = false, textValue) {
8
+ const getLocatorExpression = () => {
9
+ if (isTextSelector && textValue) {
10
+ let expr = `${pageKey}.page.getByText('${textValue.replace(/'/g, "\\'")}')`;
11
+ if (action.nth !== undefined && action.nth !== 0) {
12
+ expr += `.nth(${action.nth})`;
13
+ }
14
+ return expr;
15
+ }
16
+ return null;
17
+ };
18
+ // Escape single quotes in elementKey so generated code like click('I\'m Feeling Lucky') is valid JS
19
+ const safeKey = elementKey.replace(/'/g, "\\'");
20
+ switch (action.name) {
21
+ case 'click': {
22
+ let method = 'click';
23
+ if (action.clickCount === 2)
24
+ method = 'dblclick';
25
+ const opts = [];
26
+ if (action.modifiers && action.modifiers.length) {
27
+ opts.push(`modifiers: [${action.modifiers.map((m) => `'${m}'`).join(', ')}]`);
28
+ }
29
+ const locatorExpr = getLocatorExpression();
30
+ if (locatorExpr) {
31
+ const optsStr = opts.length ? `{ ${opts.join(', ')} }` : '';
32
+ return ` await ${locatorExpr}.${method}(${optsStr});`;
33
+ }
34
+ if (action.nth !== undefined && action.nth !== 0) {
35
+ opts.push(`nth: ${action.nth}`);
36
+ }
37
+ const optsStr = opts.length ? `, { ${opts.join(', ')} }` : '';
38
+ return ` await ${pageKey}.${method}('${safeKey}'${optsStr});`;
39
+ }
40
+ case 'hover': {
41
+ const locatorExpr = getLocatorExpression();
42
+ if (locatorExpr) {
43
+ return ` await ${locatorExpr}.hover();`;
44
+ }
45
+ const optsStr = action.nth !== undefined && action.nth !== 0 ? `, { nth: ${action.nth} }` : '';
46
+ return ` await ${pageKey}.hover('${safeKey}'${optsStr});`;
47
+ }
48
+ case 'check': {
49
+ const locatorExpr = getLocatorExpression();
50
+ if (locatorExpr) {
51
+ return ` await ${locatorExpr}.check();`;
52
+ }
53
+ const optsStr = action.nth !== undefined && action.nth !== 0 ? `, { nth: ${action.nth} }` : '';
54
+ return ` await ${pageKey}.check('${safeKey}'${optsStr});`;
55
+ }
56
+ case 'uncheck': {
57
+ const locatorExpr = getLocatorExpression();
58
+ if (locatorExpr) {
59
+ return ` await ${locatorExpr}.uncheck();`;
60
+ }
61
+ const optsStr = action.nth !== undefined && action.nth !== 0 ? `, { nth: ${action.nth} }` : '';
62
+ return ` await ${pageKey}.uncheck('${safeKey}'${optsStr});`;
63
+ }
64
+ case 'fill': {
65
+ const locatorExpr = getLocatorExpression();
66
+ if (locatorExpr) {
67
+ return ` await ${locatorExpr}.fill('${action.text.replace(/'/g, "\\'")}');`;
68
+ }
69
+ const optsStr = action.nth !== undefined && action.nth !== 0 ? `, { nth: ${action.nth} }` : '';
70
+ return ` await ${pageKey}.fill('${safeKey}', '${action.text.replace(/'/g, "\\'")}'${optsStr});`;
71
+ }
72
+ case 'setInputFiles': {
73
+ const locatorExpr = getLocatorExpression();
74
+ if (locatorExpr) {
75
+ return ` await ${locatorExpr}.setInputFiles(${JSON.stringify(action.files)});`;
76
+ }
77
+ const optsStr = action.nth !== undefined && action.nth !== 0 ? `, { nth: ${action.nth} }` : '';
78
+ return ` await ${pageKey}.setInputFiles('${safeKey}', ${JSON.stringify(action.files)}${optsStr});`;
79
+ }
80
+ case 'press': {
81
+ const modifiers = Array.isArray(action.modifiers) ? action.modifiers : [];
82
+ const shortcut = [...modifiers, action.key].join('+');
83
+ const locatorExpr = getLocatorExpression();
84
+ if (locatorExpr) {
85
+ return ` await ${locatorExpr}.press('${shortcut}');`;
86
+ }
87
+ const optsStr = action.nth !== undefined && action.nth !== 0 ? `, { nth: ${action.nth} }` : '';
88
+ return ` await ${pageKey}.press('${safeKey}', '${shortcut}'${optsStr});`;
89
+ }
90
+ case 'navigate':
91
+ return ` await ${pageKey}.goto();`;
92
+ case 'select': {
93
+ const locatorExpr = getLocatorExpression();
94
+ if (locatorExpr) {
95
+ return ` await ${locatorExpr}.selectOption(${JSON.stringify(action.options)});`;
96
+ }
97
+ const optsStr = action.nth !== undefined && action.nth !== 0 ? `, { nth: ${action.nth} }` : '';
98
+ return ` await ${pageKey}.selectOption('${safeKey}', ${JSON.stringify(action.options)}${optsStr});`;
99
+ }
100
+ case 'assertText': {
101
+ const locatorExpr = getLocatorExpression();
102
+ if (locatorExpr) {
103
+ return ` await expect(${locatorExpr}).${action.substring ? 'toContainText' : 'toHaveText'}('${action.text.replace(/'/g, "\\'")}');`;
104
+ }
105
+ const opts = [];
106
+ if (action.nth !== undefined && action.nth !== 0)
107
+ opts.push(`nth: ${action.nth}`);
108
+ const optsStr = opts.length ? `, { ${opts.join(', ')} }` : '';
109
+ return ` await ${pageKey}.verify('${safeKey}'${optsStr}).${action.substring ? 'toContainText' : 'toHaveText'}('${action.text.replace(/'/g, "\\'")}');`;
110
+ }
111
+ case 'assertChecked': {
112
+ const locatorExpr = getLocatorExpression();
113
+ if (locatorExpr) {
114
+ return ` await expect(${locatorExpr})${action.checked ? '' : '.not'}.toBeChecked();`;
115
+ }
116
+ const opts = [];
117
+ if (action.nth !== undefined && action.nth !== 0)
118
+ opts.push(`nth: ${action.nth}`);
119
+ const optsStr = opts.length ? `, { ${opts.join(', ')} }` : '';
120
+ return ` await ${pageKey}.verify('${safeKey}'${optsStr})${action.checked ? '' : '.not'}.toBeChecked();`;
121
+ }
122
+ case 'assertVisible': {
123
+ const locatorExpr = getLocatorExpression();
124
+ if (locatorExpr) {
125
+ return ` await expect(${locatorExpr}).toBeVisible();`;
126
+ }
127
+ const opts = [];
128
+ if (action.nth !== undefined && action.nth !== 0)
129
+ opts.push(`nth: ${action.nth}`);
130
+ const optsStr = opts.length ? `, { ${opts.join(', ')} }` : '';
131
+ return ` await ${pageKey}.verify('${safeKey}'${optsStr});`;
132
+ }
133
+ case 'assertValue': {
134
+ const locatorExpr = getLocatorExpression();
135
+ if (locatorExpr) {
136
+ const assertion = action.value ? `toHaveValue('${action.value.replace(/'/g, "\\'")}')` : `toBeEmpty()`;
137
+ return ` await expect(${locatorExpr}).${assertion};`;
138
+ }
139
+ const opts = [];
140
+ if (action.nth !== undefined && action.nth !== 0)
141
+ opts.push(`nth: ${action.nth}`);
142
+ const optsStr = opts.length ? `, { ${opts.join(', ')} }` : '';
143
+ const assertion = action.value ? `toHaveValue('${action.value.replace(/'/g, "\\'")}')` : `toBeEmpty()`;
144
+ return ` await ${pageKey}.verify('${safeKey}'${optsStr}).${assertion};`;
145
+ }
146
+ case 'assertSnapshot': {
147
+ const locatorExpr = getLocatorExpression();
148
+ if (locatorExpr) {
149
+ return ` await expect(${locatorExpr}).toMatchAriaSnapshot(\`\n${action.ariaSnapshot}\`);`;
150
+ }
151
+ const opts = [];
152
+ if (action.nth !== undefined && action.nth !== 0)
153
+ opts.push(`nth: ${action.nth}`);
154
+ const optsStr = opts.length ? `, { ${opts.join(', ')} }` : '';
155
+ return ` await ${pageKey}.verify('${safeKey}'${optsStr}).toMatchAriaSnapshot(\`\n${action.ariaSnapshot}\`);`;
156
+ }
157
+ default:
158
+ return ` // Unsupported action: ${action.name} on ${elementKey}`;
159
+ }
160
+ }
@@ -0,0 +1,23 @@
1
+ import type { Page } from '@playwright/test';
2
+ import { type MatchResult } from './registry-matcher';
3
+ import type { LocatorCandidate, RegistryRoot } from './types';
4
+ import type { RecordedAction } from './action-formatter';
5
+ export interface ProcessedAction {
6
+ pageKey: string;
7
+ elementKey: string;
8
+ action: RecordedAction & {
9
+ selector: string;
10
+ };
11
+ matchResult: MatchResult | null;
12
+ smartLocator: LocatorCandidate | null;
13
+ }
14
+ /**
15
+ * Checks if a selector points to pw-core's floating recorder panel.
16
+ */
17
+ export declare function isOwnPanelSelector(page: Page, selector?: string): Promise<boolean>;
18
+ /**
19
+ * Resolves a recorded action to its matching page key, element key, and registry match details.
20
+ */
21
+ export declare function processRecordedAction(page: Page, rawAction: RecordedAction & {
22
+ selector: string;
23
+ }, currentUrl: string, currentTitle: string, registryObj: RegistryRoot, overrideMode: boolean): Promise<ProcessedAction | null>;
@@ -0,0 +1,112 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.isOwnPanelSelector = isOwnPanelSelector;
4
+ exports.processRecordedAction = processRecordedAction;
5
+ const selector_parser_1 = require("./selector-parser");
6
+ const registry_matcher_1 = require("./registry-matcher");
7
+ const index_1 = require("./generator/index");
8
+ const action_validator_1 = require("./generator/action.validator");
9
+ /**
10
+ * Checks if a selector points to pw-core's floating recorder panel.
11
+ */
12
+ async function isOwnPanelSelector(page, selector) {
13
+ if (!selector)
14
+ return false;
15
+ const lowerSel = selector.toLowerCase();
16
+ if (lowerSel.includes('pw-core') ||
17
+ lowerSel.includes('pwcore') ||
18
+ lowerSel.includes('new test') ||
19
+ lowerSel.includes('add serial') ||
20
+ lowerSel.includes('new-serial') ||
21
+ lowerSel.includes('new-test')) {
22
+ return true;
23
+ }
24
+ try {
25
+ return await page
26
+ .locator(selector)
27
+ .evaluate((element) => {
28
+ const el = element;
29
+ return el.id === 'pw-core-codegen-panel' || el.closest('#pw-core-codegen-panel') !== null;
30
+ }, null, { timeout: 500 })
31
+ .catch(() => false);
32
+ }
33
+ catch {
34
+ // Return false if element evaluation throws or disconnected
35
+ return false;
36
+ }
37
+ }
38
+ /**
39
+ * Resolves a recorded action to its matching page key, element key, and registry match details.
40
+ */
41
+ async function processRecordedAction(page, rawAction, currentUrl, currentTitle, registryObj, overrideMode) {
42
+ const action = { ...rawAction };
43
+ action.name = await (0, action_validator_1.normalizeActionName)(page, action.selector, action.name);
44
+ if (action.name === 'openPage' || action.name === 'closePage') {
45
+ return null;
46
+ }
47
+ if (await isOwnPanelSelector(page, action.selector)) {
48
+ return null;
49
+ }
50
+ let pageKey = (0, registry_matcher_1.findPageKey)(currentUrl, currentTitle, registryObj, overrideMode);
51
+ if (!registryObj[pageKey]) {
52
+ let effectiveUrl = '/';
53
+ try {
54
+ const u = new URL(currentUrl);
55
+ const hash = u.hash && u.hash.startsWith('#/') ? u.hash.slice(1) : '';
56
+ const targetUrl = hash && hash !== '/' ? hash : u.pathname;
57
+ const clean = targetUrl.split('?')[0].split('#')[0];
58
+ effectiveUrl = clean.startsWith('/') ? clean : '/' + clean;
59
+ }
60
+ catch {
61
+ // Fall back to default root path '/' if URL is malformed or relative
62
+ }
63
+ registryObj[pageKey] = { url: effectiveUrl };
64
+ }
65
+ let elementKey = 'element';
66
+ let matchResult = null;
67
+ let smartLocator = null;
68
+ if (action.selector) {
69
+ let selectorToUse = action.selector;
70
+ smartLocator = await (0, index_1.generateSmartLocator)(page, action.selector);
71
+ if (smartLocator) {
72
+ selectorToUse = smartLocator.selector;
73
+ }
74
+ const parsed = (0, selector_parser_1.extractNthFromSelector)(selectorToUse);
75
+ selectorToUse = parsed.baseSelector;
76
+ if (parsed.nth !== undefined) {
77
+ action.nth = parsed.nth;
78
+ }
79
+ matchResult = (0, registry_matcher_1.findElementKey)(selectorToUse, pageKey, registryObj, overrideMode, {
80
+ targetTestId: smartLocator?.targetTestId,
81
+ targetId: smartLocator?.targetId,
82
+ targetParentId: smartLocator?.targetParentId,
83
+ overrideType: action.name === 'check' || action.name === 'uncheck' ? 'checkbox' : undefined
84
+ });
85
+ pageKey = matchResult.pageKey;
86
+ elementKey = matchResult.elementKey;
87
+ // Use context-aware generatedKey if it produced a better name and the match is new
88
+ if (matchResult.isNew &&
89
+ (matchResult.type === 'testId' || matchResult.type === 'selector') &&
90
+ smartLocator?.generatedKey &&
91
+ smartLocator.generatedKey.length > 2) {
92
+ const proposedKey = smartLocator.generatedKey;
93
+ const targetConfig = registryObj[pageKey];
94
+ const existingKeys = new Set([
95
+ ...Object.keys(targetConfig?.testId || {}),
96
+ ...Object.keys(targetConfig?.testIds || {}),
97
+ ...Object.keys(targetConfig?.selector || {}),
98
+ ...Object.keys(targetConfig?.selectors || {})
99
+ ]);
100
+ if (!existingKeys.has(proposedKey)) {
101
+ elementKey = proposedKey;
102
+ }
103
+ }
104
+ }
105
+ return {
106
+ pageKey,
107
+ elementKey,
108
+ action,
109
+ matchResult,
110
+ smartLocator
111
+ };
112
+ }
@@ -1 +1,14 @@
1
- export declare function clientInjectFloatingPanel(idx: string, fileName: string, hasSteps: boolean, cssStyle: string, htmlContent: string): void;
1
+ declare global {
2
+ interface Window {
3
+ __pwCoreStartNewTest?: () => void;
4
+ __pwCoreStartNewSerialTest?: () => void;
5
+ }
6
+ }
7
+ export interface FloatingPanelPayload {
8
+ idx: string;
9
+ fileName: string;
10
+ hasSteps: boolean;
11
+ cssStyle: string;
12
+ htmlContent: string;
13
+ }
14
+ export declare function clientInjectFloatingPanel(payload: FloatingPanelPayload): void;
@@ -1,22 +1,28 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.clientInjectFloatingPanel = clientInjectFloatingPanel;
4
- function clientInjectFloatingPanel(idx, fileName, hasSteps, cssStyle, htmlContent) {
4
+ function clientInjectFloatingPanel(payload) {
5
+ function safeSessionStorage(action, key, value) {
6
+ try {
7
+ if (action === 'get')
8
+ return sessionStorage.getItem(key);
9
+ if (action === 'set' && value !== undefined)
10
+ sessionStorage.setItem(key, value);
11
+ if (action === 'remove')
12
+ sessionStorage.removeItem(key);
13
+ }
14
+ catch { }
15
+ return null;
16
+ }
17
+ const { idx, hasSteps, cssStyle, htmlContent } = payload;
5
18
  if (!document.body) {
6
- setTimeout(() => clientInjectFloatingPanel(idx, fileName, hasSteps, cssStyle, htmlContent), 50);
19
+ window.addEventListener('DOMContentLoaded', () => clientInjectFloatingPanel(payload), { once: true });
7
20
  return;
8
21
  }
9
- let sessionHasSteps = false;
10
- try {
11
- sessionHasSteps = sessionStorage.getItem('pw-core-has-steps') === 'true';
12
- }
13
- catch (e) { }
22
+ const sessionHasSteps = safeSessionStorage('get', 'pw-core-has-steps') === 'true';
14
23
  const effectiveHasSteps = hasSteps || sessionHasSteps;
15
24
  if (effectiveHasSteps) {
16
- try {
17
- sessionStorage.setItem('pw-core-has-steps', 'true');
18
- }
19
- catch (e) { }
25
+ safeSessionStorage('set', 'pw-core-has-steps', 'true');
20
26
  }
21
27
  const numEl = document.getElementById('pw-core-test-num');
22
28
  const existingNewTestBtn = document.getElementById('pw-core-new-test-btn');
@@ -128,10 +134,7 @@ function clientInjectFloatingPanel(idx, fileName, hasSteps, cssStyle, htmlConten
128
134
  document.addEventListener('mousemove', onMouseMove);
129
135
  document.addEventListener('mouseup', onMouseUp);
130
136
  const enableButtons = () => {
131
- try {
132
- sessionStorage.setItem('pw-core-has-steps', 'true');
133
- }
134
- catch (e) { }
137
+ safeSessionStorage('set', 'pw-core-has-steps', 'true');
135
138
  const newTestBtn = document.getElementById('pw-core-new-test-btn');
136
139
  const newSerialBtn = document.getElementById('pw-core-new-serial-btn');
137
140
  if (newTestBtn && newTestBtn.disabled) {
@@ -211,14 +214,10 @@ function clientInjectFloatingPanel(idx, fileName, hasSteps, cssStyle, htmlConten
211
214
  e.preventDefault();
212
215
  e.stopPropagation();
213
216
  if (btn.id === 'pw-core-new-test-btn') {
214
- try {
215
- sessionStorage.removeItem('pw-core-has-steps');
216
- }
217
- catch (err) { }
217
+ safeSessionStorage('remove', 'pw-core-has-steps');
218
218
  optimisticUpdate('test');
219
219
  const trigger = () => {
220
220
  if (window.__pwCoreStartNewTest) {
221
- ;
222
221
  window.__pwCoreStartNewTest();
223
222
  }
224
223
  else {
@@ -228,14 +227,10 @@ function clientInjectFloatingPanel(idx, fileName, hasSteps, cssStyle, htmlConten
228
227
  trigger();
229
228
  }
230
229
  else if (btn.id === 'pw-core-new-serial-btn') {
231
- try {
232
- sessionStorage.removeItem('pw-core-has-steps');
233
- }
234
- catch (err) { }
230
+ safeSessionStorage('remove', 'pw-core-has-steps');
235
231
  optimisticUpdate('serial');
236
232
  const trigger = () => {
237
233
  if (window.__pwCoreStartNewSerialTest) {
238
- ;
239
234
  window.__pwCoreStartNewSerialTest();
240
235
  }
241
236
  else {
@@ -19,26 +19,29 @@ class FloatingPanelManager {
19
19
  return this.options.getDisplayTestIndex();
20
20
  });
21
21
  await this.context.exposeFunction('__pwCoreStartNewTest', () => {
22
- console.log('DEBUG [cli]: __pwCoreStartNewTest exposed function called in browser context');
23
22
  return this.options.onStartNewTest();
24
23
  });
25
24
  await this.context.exposeFunction('__pwCoreStartNewSerialTest', () => {
26
- console.log('DEBUG [cli]: __pwCoreStartNewSerialTest exposed function called in browser context');
27
25
  return this.options.onStartNewSerialTest();
28
26
  });
29
27
  // 2. Listen for page events to automatically inject the panel
30
- this.context.on('page', (p) => {
31
- p.on('load', () => this.inject(p).catch(() => { }));
32
- p.on('domcontentloaded', () => this.inject(p).catch(() => { }));
28
+ const setupPage = (p) => {
29
+ p.on('load', () => this.inject(p));
30
+ p.on('domcontentloaded', () => this.inject(p));
33
31
  p.on('framenavigated', (frame) => {
34
32
  if (frame === p.mainFrame()) {
35
33
  // Wait briefly for SPA framework to mount before injecting/updating
36
34
  setTimeout(() => {
37
- this.inject(p).catch(() => { });
35
+ this.inject(p);
38
36
  }, 200);
39
37
  }
40
38
  });
41
- });
39
+ this.inject(p);
40
+ };
41
+ this.context.on('page', setupPage);
42
+ for (const p of this.context.pages()) {
43
+ setupPage(p);
44
+ }
42
45
  }
43
46
  /**
44
47
  * Inject or update the floating panel on a single page.
@@ -56,26 +59,17 @@ class FloatingPanelManager {
56
59
  : 'Disabled: Record at least one action/assertion to add a serial test';
57
60
  const disabledAttr = hasSteps ? '' : ' disabled style="opacity:0.35; cursor:not-allowed;"';
58
61
  const htmlContent = (0, template_1.getFloatingPanelHtml)(idx, fileName, newTestTitle, newSerialTitle, disabledAttr);
59
- const fnStr = client_1.clientInjectFloatingPanel.toString();
60
- // Evaluate the client injection function in the page context
61
- await page
62
- .evaluate(({ idx, fileName, hasSteps, cssStyle, htmlContent, fnStr }) => {
63
- const fn = new Function(`return ${fnStr}`)();
64
- fn(idx, fileName, hasSteps, cssStyle, htmlContent);
65
- }, {
62
+ // Evaluate the client injection function directly in the page context via Playwright
63
+ await page.evaluate(client_1.clientInjectFloatingPanel, {
66
64
  idx,
67
65
  fileName,
68
66
  hasSteps,
69
67
  cssStyle: template_1.FLOATING_PANEL_STYLE,
70
- htmlContent,
71
- fnStr
72
- })
73
- .catch((err) => {
74
- console.error(`DEBUG [cli]: Failed to inject/update floating panel on page ${page.url()}:`, err);
68
+ htmlContent
75
69
  });
76
70
  }
77
- catch (err) {
78
- console.error(`DEBUG [cli]: Exception in injectFloatingPanel wrapper:`, err);
71
+ catch {
72
+ // Non-fatal: ignore injection failure on navigation frames and allow recording to proceed
79
73
  }
80
74
  }
81
75
  /**
@@ -83,7 +77,7 @@ class FloatingPanelManager {
83
77
  */
84
78
  async injectAll() {
85
79
  for (const p of this.context.pages()) {
86
- await this.inject(p).catch(() => { });
80
+ await this.inject(p);
87
81
  }
88
82
  }
89
83
  }
@@ -1,5 +1,6 @@
1
+ import type { Page } from '@playwright/test';
1
2
  /**
2
3
  * Normalizes action name to ensure check/uncheck are only used on checkboxes.
3
4
  * Otherwise, falls back to click.
4
5
  */
5
- export declare function normalizeActionName(page: any, selector: string, actionName: string): Promise<string>;
6
+ export declare function normalizeActionName(page: Page, selector: string, actionName: string): Promise<string>;
@@ -13,7 +13,8 @@ async function normalizeActionName(page, selector, actionName) {
13
13
  return 'click';
14
14
  }
15
15
  try {
16
- const isCheckbox = await page.locator(selector).evaluate((el) => {
16
+ const isCheckbox = await page.locator(selector).evaluate((element) => {
17
+ const el = element;
17
18
  return el.tagName.toLowerCase() === 'input' && el.type === 'checkbox';
18
19
  }, null, { timeout: 500 }).catch(() => false);
19
20
  if (!isCheckbox) {
@@ -0,0 +1,7 @@
1
+ import type { Page } from '@playwright/test';
2
+ import { LocatorCandidate } from '../types';
3
+ import type { DomScanResult } from './dom-scanner';
4
+ /**
5
+ * Transforms raw scanned candidates into fully scored and uniqueness-validated LocatorCandidates.
6
+ */
7
+ export declare function scoreAndRankCandidates(page: Page, scanResult: DomScanResult): Promise<LocatorCandidate[]>;