pw-core 1.2.0 → 1.2.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 -1
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +777 -0
- package/dist/codegen/config/context-weights.d.ts +8 -0
- package/dist/codegen/config/context-weights.js +11 -0
- package/dist/codegen/config/ignored-attributes.d.ts +1 -0
- package/dist/codegen/config/ignored-attributes.js +4 -0
- package/dist/codegen/config/ignored-classes.d.ts +1 -0
- package/dist/codegen/config/ignored-classes.js +144 -0
- package/dist/codegen/config/ignored-values.d.ts +1 -0
- package/dist/codegen/config/ignored-values.js +4 -0
- package/dist/codegen/config/index.d.ts +29 -0
- package/dist/codegen/config/index.js +17 -0
- package/dist/codegen/config/strategy-order.d.ts +1 -0
- package/dist/codegen/config/strategy-order.js +18 -0
- package/dist/codegen/config/weights.d.ts +15 -0
- package/dist/codegen/config/weights.js +18 -0
- package/dist/codegen/floating-panel/client.d.ts +1 -0
- package/dist/codegen/floating-panel/client.js +248 -0
- package/dist/codegen/floating-panel/index.d.ts +4 -0
- package/dist/codegen/floating-panel/index.js +20 -0
- package/dist/codegen/floating-panel/manager.d.ts +19 -0
- package/dist/codegen/floating-panel/manager.js +90 -0
- package/dist/codegen/floating-panel/template.d.ts +2 -0
- package/dist/codegen/floating-panel/template.js +58 -0
- package/dist/codegen/floating-panel/types.d.ts +7 -0
- package/dist/codegen/floating-panel/types.js +2 -0
- package/dist/codegen/generator/action.validator.d.ts +5 -0
- package/dist/codegen/generator/action.validator.js +27 -0
- package/dist/codegen/generator/index.d.ts +13 -0
- package/dist/codegen/generator/index.js +724 -0
- package/dist/codegen/generator/locator.restricted.d.ts +36 -0
- package/dist/codegen/generator/locator.restricted.js +127 -0
- package/dist/codegen/generator/locator.validator.d.ts +8 -0
- package/dist/codegen/generator/locator.validator.js +38 -0
- package/dist/codegen/hover-tracker/client.d.ts +1 -0
- package/dist/codegen/hover-tracker/client.js +75 -0
- package/dist/codegen/hover-tracker/index.d.ts +3 -0
- package/dist/codegen/hover-tracker/index.js +19 -0
- package/dist/codegen/hover-tracker/manager.d.ts +11 -0
- package/dist/codegen/hover-tracker/manager.js +28 -0
- package/dist/codegen/hover-tracker/types.d.ts +3 -0
- package/dist/codegen/hover-tracker/types.js +2 -0
- package/dist/codegen/index.d.ts +35 -0
- package/dist/codegen/index.js +1147 -0
- package/dist/codegen/key-utils.d.ts +23 -0
- package/dist/codegen/key-utils.js +68 -0
- package/dist/codegen/scorer/base-score.d.ts +1 -0
- package/dist/codegen/scorer/base-score.js +7 -0
- package/dist/codegen/scorer/context-score.d.ts +2 -0
- package/dist/codegen/scorer/context-score.js +27 -0
- package/dist/codegen/scorer/index.d.ts +5 -0
- package/dist/codegen/scorer/index.js +23 -0
- package/dist/codegen/scorer/locator.ranking.d.ts +15 -0
- package/dist/codegen/scorer/locator.ranking.js +83 -0
- package/dist/codegen/scorer/semantic-score.d.ts +1 -0
- package/dist/codegen/scorer/semantic-score.js +47 -0
- package/dist/codegen/types.d.ts +35 -0
- package/dist/codegen/types.js +2 -0
- package/dist/component/table.js +14 -10
- package/dist/constants/methods.d.ts +2 -0
- package/dist/constants/methods.js +33 -0
- package/dist/constants/strategies.d.ts +2 -0
- package/dist/constants/strategies.js +47 -0
- package/dist/page/actions/locator-actions.js +15 -4
- package/dist/page/assertions/verify-chain.d.ts +39 -32
- package/dist/page/assertions/verify-chain.js +18 -4
- package/dist/page/assertions/verify-helpers.js +12 -3
- package/dist/page/config.d.ts +109 -71
- package/dist/page/config.js +10 -31
- package/dist/page/locators/dynamic-locator-resolver.js +29 -9
- package/dist/page/locators/resolver.d.ts +15 -15
- package/dist/page/locators/resolver.js +151 -32
- package/dist/page/registry.d.ts +43 -6
- package/dist/page/registry.js +90 -18
- package/dist/page/typed-page.d.ts +33 -29
- package/dist/page/typed-page.js +40 -11
- package/dist/page/types/proxy-methods.d.ts +69 -54
- package/dist/page/types/validation.d.ts +48 -1
- package/dist/page/utils/caller-location.js +13 -9
- package/dist/page/utils/formatter.js +6 -3
- package/dist/types/methods.d.ts +4 -0
- package/dist/types/methods.js +2 -0
- package/dist/types/strategies.d.ts +17 -0
- package/dist/types/strategies.js +2 -0
- package/package.json +8 -1
package/dist/cli.js
ADDED
|
@@ -0,0 +1,777 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
4
|
+
if (k2 === undefined) k2 = k;
|
|
5
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
6
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
7
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
8
|
+
}
|
|
9
|
+
Object.defineProperty(o, k2, desc);
|
|
10
|
+
}) : (function(o, m, k, k2) {
|
|
11
|
+
if (k2 === undefined) k2 = k;
|
|
12
|
+
o[k2] = m[k];
|
|
13
|
+
}));
|
|
14
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
15
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
16
|
+
}) : function(o, v) {
|
|
17
|
+
o["default"] = v;
|
|
18
|
+
});
|
|
19
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
20
|
+
var ownKeys = function(o) {
|
|
21
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
22
|
+
var ar = [];
|
|
23
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
24
|
+
return ar;
|
|
25
|
+
};
|
|
26
|
+
return ownKeys(o);
|
|
27
|
+
};
|
|
28
|
+
return function (mod) {
|
|
29
|
+
if (mod && mod.__esModule) return mod;
|
|
30
|
+
var result = {};
|
|
31
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
32
|
+
__setModuleDefault(result, mod);
|
|
33
|
+
return result;
|
|
34
|
+
};
|
|
35
|
+
})();
|
|
36
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
37
|
+
const fs = __importStar(require("fs"));
|
|
38
|
+
const path = __importStar(require("path"));
|
|
39
|
+
const test_1 = require("@playwright/test");
|
|
40
|
+
const codegen_1 = require("./codegen");
|
|
41
|
+
const floating_panel_1 = require("./codegen/floating-panel");
|
|
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) };
|
|
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
|
+
}
|
|
77
|
+
}
|
|
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
|
+
}
|
|
112
|
+
}
|
|
113
|
+
catch (e) { }
|
|
114
|
+
}
|
|
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');
|
|
121
|
+
}
|
|
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]);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
catch (e) { }
|
|
131
|
+
}
|
|
132
|
+
return cwd;
|
|
133
|
+
}
|
|
134
|
+
function getNextTestIndex(testDir) {
|
|
135
|
+
if (!fs.existsSync(testDir))
|
|
136
|
+
return 1;
|
|
137
|
+
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
|
+
}
|
|
148
|
+
}
|
|
149
|
+
return max + 1;
|
|
150
|
+
}
|
|
151
|
+
catch (e) {
|
|
152
|
+
return 1;
|
|
153
|
+
}
|
|
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)}`;
|
|
172
|
+
}
|
|
173
|
+
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'));
|
|
177
|
+
}
|
|
178
|
+
catch (e) { }
|
|
179
|
+
const args = process.argv.slice(2).filter((arg) => arg !== 'codegen');
|
|
180
|
+
let url = '';
|
|
181
|
+
let output = '';
|
|
182
|
+
const overrideMode = !args.includes('--safe');
|
|
183
|
+
const cleanArgs = args.filter((arg) => arg !== '--safe');
|
|
184
|
+
for (let i = 0; i < cleanArgs.length; i++) {
|
|
185
|
+
if (cleanArgs[i] === '--url' || cleanArgs[i] === '-u') {
|
|
186
|
+
url = cleanArgs[++i];
|
|
187
|
+
}
|
|
188
|
+
else if (cleanArgs[i] === '--output' || cleanArgs[i] === '-o') {
|
|
189
|
+
output = cleanArgs[++i];
|
|
190
|
+
}
|
|
191
|
+
else if (!cleanArgs[i].startsWith('-') && !url) {
|
|
192
|
+
url = cleanArgs[i];
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
let registryFilePath = (0, codegen_1.findRegistryFile)(process.cwd());
|
|
196
|
+
if (!registryFilePath) {
|
|
197
|
+
const isTS = fs.existsSync(path.join(process.cwd(), 'tsconfig.json'));
|
|
198
|
+
const ext = isTS ? 'ts' : 'js';
|
|
199
|
+
let targetDir = process.cwd();
|
|
200
|
+
if (fs.existsSync(path.join(process.cwd(), 'src'))) {
|
|
201
|
+
targetDir = path.join(process.cwd(), 'src', 'pages');
|
|
202
|
+
}
|
|
203
|
+
else if (fs.existsSync(path.join(process.cwd(), 'pages'))) {
|
|
204
|
+
targetDir = path.join(process.cwd(), 'pages');
|
|
205
|
+
}
|
|
206
|
+
fs.mkdirSync(targetDir, { recursive: true });
|
|
207
|
+
registryFilePath = path.join(targetDir, `registry.${ext}`);
|
|
208
|
+
const templateContent = `import { createPageRegistry } from 'pw-core/page';
|
|
209
|
+
|
|
210
|
+
export const registry = createPageRegistry({
|
|
211
|
+
});
|
|
212
|
+
`;
|
|
213
|
+
fs.writeFileSync(registryFilePath, templateContent, 'utf8');
|
|
214
|
+
console.log(`Created registry file at: ${registryFilePath}`);
|
|
215
|
+
}
|
|
216
|
+
else {
|
|
217
|
+
console.log(`Found registry at: ${registryFilePath}`);
|
|
218
|
+
}
|
|
219
|
+
if (!url) {
|
|
220
|
+
const configPath = (0, codegen_1.findPlaywrightConfig)(process.cwd());
|
|
221
|
+
if (configPath) {
|
|
222
|
+
url = getBaseUrl(path.dirname(configPath));
|
|
223
|
+
}
|
|
224
|
+
else {
|
|
225
|
+
let configDir = process.cwd();
|
|
226
|
+
let current = path.dirname(registryFilePath);
|
|
227
|
+
while (current !== path.parse(current).root) {
|
|
228
|
+
if (fs.existsSync(path.join(current, 'playwright.config.ts')) ||
|
|
229
|
+
fs.existsSync(path.join(current, 'playwright.config.js'))) {
|
|
230
|
+
configDir = current;
|
|
231
|
+
break;
|
|
232
|
+
}
|
|
233
|
+
current = path.dirname(current);
|
|
234
|
+
}
|
|
235
|
+
url = getBaseUrl(configDir);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
let registryObj = (0, codegen_1.parseRegistry)(registryFilePath);
|
|
239
|
+
const testDir = path.join(getTestDir(process.cwd()), 'codegen');
|
|
240
|
+
fs.mkdirSync(testDir, { recursive: true });
|
|
241
|
+
let currentTestIndex = getNextTestIndex(testDir);
|
|
242
|
+
let activeOutput = output || `${currentTestIndex}.recorded.test.ts`;
|
|
243
|
+
let outputFilePath = path.resolve(testDir, activeOutput);
|
|
244
|
+
console.log(`Live spec output will be written to: ${outputFilePath}`);
|
|
245
|
+
const browser = await test_1.chromium.launch({ headless: false });
|
|
246
|
+
const context = await browser.newContext();
|
|
247
|
+
let testStartedAt = new Date();
|
|
248
|
+
let isSerialSuite = false;
|
|
249
|
+
const testsInCurrentFile = [];
|
|
250
|
+
const recordedSteps = [];
|
|
251
|
+
const usedPageKeys = new Set();
|
|
252
|
+
const panelManager = new floating_panel_1.FloatingPanelManager(context, {
|
|
253
|
+
getDisplayTestIndex: () => {
|
|
254
|
+
if (isSerialSuite) {
|
|
255
|
+
return `${currentTestIndex}.${testsInCurrentFile.length + 1}`;
|
|
256
|
+
}
|
|
257
|
+
return `${currentTestIndex}`;
|
|
258
|
+
},
|
|
259
|
+
getOutputFileName: () => {
|
|
260
|
+
return path.basename(outputFilePath);
|
|
261
|
+
},
|
|
262
|
+
hasSteps: () => recordedSteps.length > 0,
|
|
263
|
+
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
|
+
}
|
|
269
|
+
const keyReplacements = (0, codegen_1.writeRegistry)(registryFilePath, registryObj, overrideMode, usedPageKeys);
|
|
270
|
+
applyReplacements(keyReplacements);
|
|
271
|
+
updateSpecFile();
|
|
272
|
+
// Reset serial state for new test file
|
|
273
|
+
isSerialSuite = false;
|
|
274
|
+
testsInCurrentFile.length = 0;
|
|
275
|
+
// 2. Increment test index and update outputFilePath
|
|
276
|
+
currentTestIndex++;
|
|
277
|
+
testStartedAt = new Date();
|
|
278
|
+
const extension = path.extname(output || '1.recorded.test.ts') || '.ts';
|
|
279
|
+
const baseName = path.basename(output || '1.recorded.test.ts', extension);
|
|
280
|
+
let newOutputName = `${currentTestIndex}.recorded.test.ts`;
|
|
281
|
+
if (output && output !== '1.recorded.test.ts' && output !== 'recorded.spec.ts') {
|
|
282
|
+
newOutputName = `${baseName}_${currentTestIndex}${extension}`;
|
|
283
|
+
}
|
|
284
|
+
outputFilePath = path.resolve(testDir, newOutputName);
|
|
285
|
+
console.log(`\n>>> STARTING NEW TEST: ${outputFilePath} <<<\n`);
|
|
286
|
+
// 3. Clear steps and usedPageKeys for the new test session
|
|
287
|
+
recordedSteps.length = 0;
|
|
288
|
+
usedPageKeys.clear();
|
|
289
|
+
// 4. Re-inject panel on all open pages with the updated index
|
|
290
|
+
await panelManager.injectAll();
|
|
291
|
+
},
|
|
292
|
+
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}`);
|
|
299
|
+
isSerialSuite = true;
|
|
300
|
+
// Save current steps as a test case
|
|
301
|
+
const testName = `${currentTestIndex}.${testsInCurrentFile.length + 1}. Recorded Test`;
|
|
302
|
+
testsInCurrentFile.push({
|
|
303
|
+
name: testName,
|
|
304
|
+
steps: [...recordedSteps],
|
|
305
|
+
usedKeys: new Set(usedPageKeys)
|
|
306
|
+
});
|
|
307
|
+
// Clear active steps and usedPageKeys for the next serial test
|
|
308
|
+
recordedSteps.length = 0;
|
|
309
|
+
usedPageKeys.clear();
|
|
310
|
+
// Write the current state to the file
|
|
311
|
+
updateSpecFile();
|
|
312
|
+
// Re-inject/update panel on all open pages
|
|
313
|
+
await panelManager.injectAll();
|
|
314
|
+
}
|
|
315
|
+
});
|
|
316
|
+
await panelManager.initialize();
|
|
317
|
+
const updateSpecFile = () => {
|
|
318
|
+
let importPath = path.relative(path.dirname(outputFilePath), registryFilePath).replace(/\\/g, '/');
|
|
319
|
+
if (!importPath.startsWith('.')) {
|
|
320
|
+
importPath = './' + importPath;
|
|
321
|
+
}
|
|
322
|
+
importPath = importPath.replace(/\.(ts|js)$/, '');
|
|
323
|
+
const dateStr = formatRecordingDate(testStartedAt);
|
|
324
|
+
let fileContent = '';
|
|
325
|
+
if (isSerialSuite) {
|
|
326
|
+
const cases = [];
|
|
327
|
+
// Add previously completed test cases
|
|
328
|
+
for (const tc of testsInCurrentFile) {
|
|
329
|
+
if (tc.steps.length === 0)
|
|
330
|
+
continue;
|
|
331
|
+
const pageListStr = Array.from(tc.usedKeys).map(toWorkerKey).join(', ');
|
|
332
|
+
const tcSteps = tc.steps.map(s => {
|
|
333
|
+
let code = s.code;
|
|
334
|
+
for (const pk of Array.from(tc.usedKeys)) {
|
|
335
|
+
code = code.replace(new RegExp(`\\b${pk}\\.`, 'g'), `${toWorkerKey(pk)}.`);
|
|
336
|
+
}
|
|
337
|
+
return code.split('\n').map(line => ' ' + line.trim()).join('\n');
|
|
338
|
+
}).join('\n');
|
|
339
|
+
cases.push(` scenario('${tc.name}', async ({ ${pageListStr || 'workerPage'} }) => {
|
|
340
|
+
${tcSteps}
|
|
341
|
+
});`);
|
|
342
|
+
}
|
|
343
|
+
// Add the currently recording test case
|
|
344
|
+
if (recordedSteps.length > 0) {
|
|
345
|
+
const currentName = `${currentTestIndex}.${testsInCurrentFile.length + 1}. Recorded Test`;
|
|
346
|
+
const pageListStr = Array.from(usedPageKeys).map(toWorkerKey).join(', ');
|
|
347
|
+
const currentSteps = recordedSteps.map(s => {
|
|
348
|
+
let code = s.code;
|
|
349
|
+
for (const pk of Array.from(usedPageKeys)) {
|
|
350
|
+
code = code.replace(new RegExp(`\\b${pk}\\.`, 'g'), `${toWorkerKey(pk)}.`);
|
|
351
|
+
}
|
|
352
|
+
return code.split('\n').map(line => ' ' + line.trim()).join('\n');
|
|
353
|
+
}).join('\n');
|
|
354
|
+
cases.push(` scenario('${currentName}', async ({ ${pageListStr || 'workerPage'} }) => {
|
|
355
|
+
${currentSteps}
|
|
356
|
+
});`);
|
|
357
|
+
}
|
|
358
|
+
fileContent = `import { expect } from '@playwright/test';
|
|
359
|
+
import { registry as scenario } from '${importPath}';
|
|
360
|
+
|
|
361
|
+
// Recorded on ${dateStr}
|
|
362
|
+
scenario.describe.serial('Recorded Serial Suite', () => {
|
|
363
|
+
${cases.join('\n\n')}
|
|
364
|
+
});
|
|
365
|
+
`;
|
|
366
|
+
}
|
|
367
|
+
else {
|
|
368
|
+
const pageListStr = Array.from(usedPageKeys).join(', ');
|
|
369
|
+
fileContent = `import { expect } from '@playwright/test';
|
|
370
|
+
import { registry as scenario } from '${importPath}';
|
|
371
|
+
|
|
372
|
+
// Recorded on ${dateStr}
|
|
373
|
+
scenario('${currentTestIndex}. Recorded Test', async ({ ${pageListStr || 'codegenPage'} }) => {
|
|
374
|
+
${recordedSteps.map((s) => s.code).join('\n')}
|
|
375
|
+
});
|
|
376
|
+
`;
|
|
377
|
+
}
|
|
378
|
+
fs.writeFileSync(outputFilePath, fileContent, 'utf8');
|
|
379
|
+
};
|
|
380
|
+
const applyReplacements = (keyReplacements) => {
|
|
381
|
+
if (!keyReplacements || Object.keys(keyReplacements).length === 0)
|
|
382
|
+
return;
|
|
383
|
+
for (const step of recordedSteps) {
|
|
384
|
+
for (const oldKey of Object.keys(keyReplacements)) {
|
|
385
|
+
const newKey = keyReplacements[oldKey];
|
|
386
|
+
const regex = new RegExp(`(['"\`])${oldKey}(['"\`])`, 'g');
|
|
387
|
+
step.code = step.code.replace(regex, `$1${newKey}$2`);
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
};
|
|
391
|
+
const finalize = async () => {
|
|
392
|
+
while (actionQueue.length > 0 || processing) {
|
|
393
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
394
|
+
}
|
|
395
|
+
const keyReplacements = (0, codegen_1.writeRegistry)(registryFilePath, registryObj, overrideMode, usedPageKeys);
|
|
396
|
+
applyReplacements(keyReplacements);
|
|
397
|
+
updateSpecFile();
|
|
398
|
+
};
|
|
399
|
+
// Initialize spec file immediately
|
|
400
|
+
updateSpecFile();
|
|
401
|
+
let lastActionSignature = '';
|
|
402
|
+
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
|
+
let currentUrl = url || '';
|
|
430
|
+
let currentTitle = '';
|
|
431
|
+
const eventSink = {
|
|
432
|
+
actionAdded: async (page, data, code) => {
|
|
433
|
+
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')
|
|
438
|
+
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
|
+
}
|
|
521
|
+
const actionSignature = `${pageKey}.${elementKey}.${action.name}.${action.text || action.key || action.value || ''}.${action.nth ?? ''}`;
|
|
522
|
+
const now = Date.now();
|
|
523
|
+
const threshold = action.name === 'click' ? 1200 : 500;
|
|
524
|
+
if (actionSignature === lastActionSignature && now - lastActionTime < threshold) {
|
|
525
|
+
console.log(`DEBUG [cli]: Deduplicated consecutive action: ${actionSignature}`);
|
|
526
|
+
return;
|
|
527
|
+
}
|
|
528
|
+
lastActionSignature = actionSignature;
|
|
529
|
+
lastActionTime = now;
|
|
530
|
+
usedPageKeys.add(pageKey);
|
|
531
|
+
const generatedCode = (0, codegen_1.formatActionCall)(pageKey, elementKey, action);
|
|
532
|
+
console.log('DEBUG [cli]: generatedCode:', generatedCode);
|
|
533
|
+
recordedSteps.push({ pageKey, code: generatedCode });
|
|
534
|
+
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
|
+
}
|
|
571
|
+
}
|
|
572
|
+
const keyReplacements = (0, codegen_1.writeRegistry)(registryFilePath, registryObj, overrideMode, usedPageKeys);
|
|
573
|
+
applyReplacements(keyReplacements);
|
|
574
|
+
}
|
|
575
|
+
updateSpecFile();
|
|
576
|
+
await panelManager.injectAll();
|
|
577
|
+
});
|
|
578
|
+
},
|
|
579
|
+
actionUpdated: async (page, data, code) => {
|
|
580
|
+
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')
|
|
585
|
+
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
|
+
}
|
|
645
|
+
usedPageKeys.add(pageKey);
|
|
646
|
+
const generatedCode = (0, codegen_1.formatActionCall)(pageKey, elementKey, action);
|
|
647
|
+
console.log('DEBUG [cli]: generatedCode updated:', generatedCode);
|
|
648
|
+
lastActionSignature = `${pageKey}.${elementKey}.${action.name}.${action.text || action.key || action.value || ''}.${action.nth ?? ''}`;
|
|
649
|
+
lastActionTime = Date.now();
|
|
650
|
+
if (recordedSteps.length > 0) {
|
|
651
|
+
recordedSteps[recordedSteps.length - 1] = {
|
|
652
|
+
pageKey,
|
|
653
|
+
code: generatedCode
|
|
654
|
+
};
|
|
655
|
+
}
|
|
656
|
+
else {
|
|
657
|
+
recordedSteps.push({ pageKey, code: generatedCode });
|
|
658
|
+
}
|
|
659
|
+
updateSpecFile();
|
|
660
|
+
await panelManager.injectAll();
|
|
661
|
+
});
|
|
662
|
+
},
|
|
663
|
+
signalAdded: (page, data) => {
|
|
664
|
+
console.log('DEBUG [cli]: eventSink.signalAdded called');
|
|
665
|
+
}
|
|
666
|
+
};
|
|
667
|
+
const hoverTracker = new hover_tracker_1.HoverTrackerManager(context, {
|
|
668
|
+
onRecordHover: async (selector) => {
|
|
669
|
+
if (selector && (selector.includes('pw-core') || selector.includes('pwCore') || selector.includes('New Test')))
|
|
670
|
+
return;
|
|
671
|
+
enqueueAction(async () => {
|
|
672
|
+
const pages = context.pages();
|
|
673
|
+
const activePage = pages[pages.length - 1] || page;
|
|
674
|
+
if (!activePage)
|
|
675
|
+
return;
|
|
676
|
+
const smartLocator = await (0, codegen_1.generateSmartLocator)(activePage, selector);
|
|
677
|
+
if (!smartLocator)
|
|
678
|
+
return;
|
|
679
|
+
let pageKey = (0, codegen_1.findPageKey)(activePage.url(), await activePage.title(), registryObj, overrideMode);
|
|
680
|
+
let elementKey = 'codegenPage';
|
|
681
|
+
let selectorToUse = smartLocator.selector;
|
|
682
|
+
const parsed = extractNthFromSelector(selectorToUse);
|
|
683
|
+
selectorToUse = parsed.baseSelector;
|
|
684
|
+
const hoverAction = { name: 'hover' };
|
|
685
|
+
if (parsed.nth !== undefined) {
|
|
686
|
+
hoverAction.nth = parsed.nth;
|
|
687
|
+
}
|
|
688
|
+
const matchResult = (0, codegen_1.findElementKey)(selectorToUse, pageKey, registryObj, overrideMode);
|
|
689
|
+
pageKey = matchResult.pageKey;
|
|
690
|
+
elementKey = matchResult.elementKey;
|
|
691
|
+
const generatedCode = (0, codegen_1.formatActionCall)(pageKey, elementKey, hoverAction);
|
|
692
|
+
const actionSignature = `${pageKey}.${elementKey}.hover..${hoverAction.nth ?? ''}`;
|
|
693
|
+
const now = Date.now();
|
|
694
|
+
if (actionSignature === lastActionSignature && now - lastActionTime < 800) {
|
|
695
|
+
return;
|
|
696
|
+
}
|
|
697
|
+
lastActionSignature = actionSignature;
|
|
698
|
+
lastActionTime = now;
|
|
699
|
+
usedPageKeys.add(pageKey);
|
|
700
|
+
console.log('DEBUG [cli]: Hover action recorded:', generatedCode);
|
|
701
|
+
recordedSteps.push({ pageKey, code: generatedCode });
|
|
702
|
+
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
|
+
}
|
|
731
|
+
}
|
|
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
|
+
}
|
|
739
|
+
}
|
|
740
|
+
const keyReplacements = (0, codegen_1.writeRegistry)(registryFilePath, registryObj, overrideMode, usedPageKeys);
|
|
741
|
+
applyReplacements(keyReplacements);
|
|
742
|
+
}
|
|
743
|
+
updateSpecFile();
|
|
744
|
+
await panelManager.injectAll();
|
|
745
|
+
});
|
|
746
|
+
}
|
|
747
|
+
});
|
|
748
|
+
await hoverTracker.initialize();
|
|
749
|
+
const page = await context.newPage();
|
|
750
|
+
page.on('close', async () => {
|
|
751
|
+
console.log('Page closed. Closing browser...');
|
|
752
|
+
await finalize();
|
|
753
|
+
await browser.close().catch(() => { });
|
|
754
|
+
process.exit(0);
|
|
755
|
+
});
|
|
756
|
+
// Track page URL and title on every navigation (real, hash, or pushState)
|
|
757
|
+
page.on('framenavigated', async (frame) => {
|
|
758
|
+
if (frame === page.mainFrame()) {
|
|
759
|
+
currentUrl = page.url();
|
|
760
|
+
try {
|
|
761
|
+
currentTitle = await page.title();
|
|
762
|
+
}
|
|
763
|
+
catch (e) { }
|
|
764
|
+
console.log(`DEBUG [cli]: navigated → url="${currentUrl}" title="${currentTitle}"`);
|
|
765
|
+
}
|
|
766
|
+
});
|
|
767
|
+
if (url && typeof url === 'string') {
|
|
768
|
+
await page.goto(url.startsWith('http') ? url : `http://${url}`);
|
|
769
|
+
}
|
|
770
|
+
await context._enableRecorder({ language: 'javascript', mode: 'recording', recorderMode: 'api' }, eventSink);
|
|
771
|
+
browser.on('disconnected', async () => {
|
|
772
|
+
console.log('Browser closed. Codegen complete!');
|
|
773
|
+
await finalize();
|
|
774
|
+
process.exit(0);
|
|
775
|
+
});
|
|
776
|
+
}
|
|
777
|
+
main().catch(console.error);
|