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.
- package/README.md +1 -0
- package/dist/cli.js +145 -421
- package/dist/codegen/action-formatter.d.ts +19 -0
- package/dist/codegen/action-formatter.js +160 -0
- package/dist/codegen/action-processor.d.ts +23 -0
- package/dist/codegen/action-processor.js +112 -0
- package/dist/codegen/floating-panel/client.d.ts +14 -1
- package/dist/codegen/floating-panel/client.js +20 -25
- package/dist/codegen/floating-panel/manager.js +16 -22
- package/dist/codegen/generator/action.validator.d.ts +2 -1
- package/dist/codegen/generator/action.validator.js +2 -1
- package/dist/codegen/generator/candidate-scorer.d.ts +7 -0
- package/dist/codegen/generator/candidate-scorer.js +206 -0
- package/dist/codegen/generator/dom-scanner.d.ts +39 -0
- package/dist/codegen/generator/dom-scanner.js +429 -0
- package/dist/codegen/generator/id-stability.d.ts +6 -0
- package/dist/codegen/generator/id-stability.js +38 -0
- package/dist/codegen/generator/index.d.ts +8 -10
- package/dist/codegen/generator/index.js +23 -678
- package/dist/codegen/generator/key-builder.d.ts +13 -0
- package/dist/codegen/generator/key-builder.js +32 -0
- package/dist/codegen/hover-tracker/client.d.ts +5 -0
- package/dist/codegen/hover-tracker/client.js +14 -15
- package/dist/codegen/hover-tracker/manager.js +2 -6
- package/dist/codegen/index.d.ts +9 -32
- package/dist/codegen/index.js +9 -1128
- package/dist/codegen/key-utils.d.ts +16 -0
- package/dist/codegen/key-utils.js +29 -1
- package/dist/codegen/project-finder.d.ts +29 -0
- package/dist/codegen/project-finder.js +283 -0
- package/dist/codegen/registry-matcher.d.ts +27 -0
- package/dist/codegen/registry-matcher.js +254 -0
- package/dist/codegen/registry-store.d.ts +52 -0
- package/dist/codegen/registry-store.js +652 -0
- package/dist/codegen/selector-parser.d.ts +16 -0
- package/dist/codegen/selector-parser.js +121 -0
- package/dist/codegen/types.d.ts +18 -1
- package/dist/codegen/uniqueness.d.ts +3 -0
- package/dist/codegen/uniqueness.js +15 -0
- package/dist/component/table.d.ts +2 -2
- package/dist/component/table.js +7 -6
- package/dist/index.d.ts +3 -0
- package/dist/index.js +19 -0
- package/dist/page/actions/locator-actions.d.ts +6 -10
- package/dist/page/actions/locator-actions.js +24 -23
- package/dist/page/assertions/verify-chain.d.ts +11 -13
- package/dist/page/assertions/verify-chain.js +26 -7
- package/dist/page/assertions/verify-helpers.d.ts +5 -15
- package/dist/page/config.d.ts +25 -25
- package/dist/page/locators/dynamic-locator-resolver.js +22 -9
- package/dist/page/locators/resolver.d.ts +17 -11
- package/dist/page/locators/resolver.js +84 -78
- package/dist/page/registry.d.ts +35 -35
- package/dist/page/registry.js +86 -82
- package/dist/page/typed-page.d.ts +1 -0
- package/dist/page/typed-page.js +22 -12
- package/dist/page/types/proxy-methods.d.ts +11 -19
- package/dist/page/types/validation.d.ts +1 -1
- package/dist/page/utils/formatter.d.ts +3 -3
- package/dist/page/utils/formatter.js +5 -2
- package/package.json +6 -3
|
@@ -0,0 +1,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
|
-
|
|
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(
|
|
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
|
-
|
|
19
|
+
window.addEventListener('DOMContentLoaded', () => clientInjectFloatingPanel(payload), { once: true });
|
|
7
20
|
return;
|
|
8
21
|
}
|
|
9
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
31
|
-
p.on('load', () => this.inject(p)
|
|
32
|
-
p.on('domcontentloaded', () => this.inject(p)
|
|
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)
|
|
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
|
-
|
|
60
|
-
|
|
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
|
|
78
|
-
|
|
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)
|
|
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:
|
|
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((
|
|
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[]>;
|