@probolabs/playwright 1.5.0-rc.4 → 1.5.0-rc.5
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/dist/.tsbuildinfo +1 -1
- package/dist/cli.js +80 -18
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +506 -388
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +20 -2
- package/dist/index.js +506 -388
- package/dist/index.js.map +1 -1
- package/dist/types/replay-utils.d.ts.map +1 -1
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -1427,23 +1427,23 @@ function interactionToProboLib(interaction) {
|
|
|
1427
1427
|
if (interaction.action === PlaywrightAction.EXTRACT_VALUE) {
|
|
1428
1428
|
const extractVarName = getReturnValueParameterName(interaction);
|
|
1429
1429
|
return `${comment}
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1430
|
+
await test.step("${escapedStepName}", async () => {
|
|
1431
|
+
const ${extractVarName} = await probo.runStep(page, '${escapedPrompt}', null, { stepId: ${interaction.stepId} });
|
|
1432
|
+
param['${extractVarName}'] = ${extractVarName};
|
|
1433
|
+
});`;
|
|
1434
1434
|
}
|
|
1435
1435
|
else if (interaction.action === PlaywrightAction.ASSERT_CONTAINS_VALUE ||
|
|
1436
1436
|
interaction.action === PlaywrightAction.ASSERT_EXACT_VALUE) {
|
|
1437
1437
|
return `${comment}
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1438
|
+
await test.step("${escapedStepName}", async () => {
|
|
1439
|
+
await probo.runStep(page, '${escapedPrompt}', ${argument}, { stepId: ${interaction.stepId} });
|
|
1440
|
+
});`;
|
|
1441
1441
|
}
|
|
1442
1442
|
else if (interaction.action === PlaywrightAction.VISIT_URL) {
|
|
1443
1443
|
return `${comment}
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1444
|
+
await test.step("${escapedStepName}", async () => {
|
|
1445
|
+
await probo.runStep(page, '${interaction.nativeDescription}', ${argument}, { stepId: ${interaction.stepId} });
|
|
1446
|
+
});`;
|
|
1447
1447
|
}
|
|
1448
1448
|
else if (interaction.action === PlaywrightAction.ASK_AI) {
|
|
1449
1449
|
const escapedQuestion = interaction.nativeDescription.replace(/'/g, "\\'");
|
|
@@ -1461,9 +1461,9 @@ function interactionToProboLib(interaction) {
|
|
|
1461
1461
|
}
|
|
1462
1462
|
else {
|
|
1463
1463
|
return `${comment}
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1464
|
+
await test.step("${escapedStepName}", async () => {
|
|
1465
|
+
${argument ? `await probo.runStep(page, '${escapedPrompt}', ${argument}, { stepId: ${interaction.stepId} });` : `await probo.runStep(page, '${escapedPrompt}', null, { stepId: ${interaction.stepId} });`}
|
|
1466
|
+
});`;
|
|
1467
1467
|
}
|
|
1468
1468
|
}
|
|
1469
1469
|
function generateExecuteScriptCode(interaction, comment, escapedStepName) {
|
|
@@ -1490,17 +1490,79 @@ function generateExecuteScriptCode(interaction, comment, escapedStepName) {
|
|
|
1490
1490
|
param['${scriptVarName}'] = ${scriptVarName};
|
|
1491
1491
|
});`;
|
|
1492
1492
|
}
|
|
1493
|
+
/** Base indentation for steps inside the test body (8 spaces) */
|
|
1494
|
+
const STEP_BASE_INDENT = 8;
|
|
1495
|
+
/** Base indentation for steps nested inside a sequence step (10 spaces) */
|
|
1496
|
+
const SEQUENCE_INNER_INDENT = 10;
|
|
1497
|
+
/**
|
|
1498
|
+
* Re-indent a block so its minimum leading whitespace becomes targetSpaces.
|
|
1499
|
+
* Normalizes mixed indentation from interactionToNativeCode (8 spaces) and interactionToProboLib (6 spaces).
|
|
1500
|
+
*/
|
|
1501
|
+
function indentBlockTo(block, targetSpaces) {
|
|
1502
|
+
const lines = block.split('\n');
|
|
1503
|
+
const nonEmpty = lines.filter((l) => l.trim().length > 0);
|
|
1504
|
+
const minIndent = nonEmpty.length > 0
|
|
1505
|
+
? Math.min(...nonEmpty.map((l) => { var _a, _b; return ((_b = (_a = l.match(/^\s*/)) === null || _a === void 0 ? void 0 : _a[0]) !== null && _b !== void 0 ? _b : '').length; }))
|
|
1506
|
+
: 0;
|
|
1507
|
+
const toAdd = Math.max(0, targetSpaces - minIndent);
|
|
1508
|
+
return lines
|
|
1509
|
+
.map((l) => (l.trim() ? ' '.repeat(toAdd) + l : l))
|
|
1510
|
+
.join('\n');
|
|
1511
|
+
}
|
|
1512
|
+
/**
|
|
1513
|
+
* Generates steps string with sequence-aware test.step wrapping.
|
|
1514
|
+
* When SEQUENCE_START is encountered, collects interactions until SEQUENCE_END
|
|
1515
|
+
* and wraps them in test.step(<sequence name>, async () => { ... }).
|
|
1516
|
+
*/
|
|
1517
|
+
function generateStepsWithSequences(interactions, _hasAiInteractions) {
|
|
1518
|
+
const result = [];
|
|
1519
|
+
let i = 0;
|
|
1520
|
+
const baseIndent = ' '.repeat(STEP_BASE_INDENT);
|
|
1521
|
+
while (i < interactions.length) {
|
|
1522
|
+
const interaction = interactions[i];
|
|
1523
|
+
if (interaction.action === PlaywrightAction.SEQUENCE_START) {
|
|
1524
|
+
const sequenceName = interaction.nativeName ||
|
|
1525
|
+
interaction.nativeDescription ||
|
|
1526
|
+
'Unnamed sequence';
|
|
1527
|
+
const escapedName = sequenceName
|
|
1528
|
+
.replace(/\\/g, '\\\\')
|
|
1529
|
+
.replace(/"/g, '\\"');
|
|
1530
|
+
i++;
|
|
1531
|
+
const innerSteps = [];
|
|
1532
|
+
while (i < interactions.length &&
|
|
1533
|
+
interactions[i].action !== PlaywrightAction.SEQUENCE_END) {
|
|
1534
|
+
const code = isAI(interactions[i])
|
|
1535
|
+
? interactionToProboLib(interactions[i])
|
|
1536
|
+
: interactionToNativeCode(interactions[i]);
|
|
1537
|
+
innerSteps.push(code);
|
|
1538
|
+
i++;
|
|
1539
|
+
}
|
|
1540
|
+
if (i < interactions.length)
|
|
1541
|
+
i++; // skip SEQUENCE_END
|
|
1542
|
+
const indented = innerSteps
|
|
1543
|
+
.map((block) => indentBlockTo(block, SEQUENCE_INNER_INDENT))
|
|
1544
|
+
.join('\n');
|
|
1545
|
+
result.push(`${baseIndent}await test.step("${escapedName}", async () => {\n${indented}\n${baseIndent}});`);
|
|
1546
|
+
}
|
|
1547
|
+
else if (interaction.action === PlaywrightAction.SEQUENCE_END) {
|
|
1548
|
+
i++; // orphan END, skip
|
|
1549
|
+
}
|
|
1550
|
+
else {
|
|
1551
|
+
result.push(isAI(interaction)
|
|
1552
|
+
? interactionToProboLib(interaction)
|
|
1553
|
+
: interactionToNativeCode(interaction));
|
|
1554
|
+
i++;
|
|
1555
|
+
}
|
|
1556
|
+
}
|
|
1557
|
+
return result.join('\n');
|
|
1558
|
+
}
|
|
1493
1559
|
function scriptTemplate(options, settings, viewPort) {
|
|
1494
1560
|
var _a, _b, _c, _d;
|
|
1495
1561
|
const areActionsParameterized = options.rows.length > 0;
|
|
1496
1562
|
const hasAiInteractions = options.interactions.some((interaction) => isAI(interaction));
|
|
1497
1563
|
const hasScriptInteractions = options.interactions.some((interaction) => interaction.action === PlaywrightAction.EXECUTE_SCRIPT);
|
|
1498
1564
|
// const uniquelyParameterizedInteractions = uniquifyInteractionParameters(options.interactions);
|
|
1499
|
-
const steps = options.interactions
|
|
1500
|
-
if (isAI(interaction))
|
|
1501
|
-
return interactionToProboLib(interaction);
|
|
1502
|
-
return interactionToNativeCode(interaction);
|
|
1503
|
-
}).join('\n');
|
|
1565
|
+
const steps = generateStepsWithSequences(options.interactions);
|
|
1504
1566
|
// Get list of all extracted value parameter names to filter them out from the parameter table
|
|
1505
1567
|
const extractedValueNames = new Set();
|
|
1506
1568
|
options.interactions.forEach((interaction) => {
|
|
@@ -4964,8 +5026,11 @@ const gen = (suffix, blockLen, outputLen)=>createHasher(()=>new Keccak(blockLen,
|
|
|
4964
5026
|
}
|
|
4965
5027
|
|
|
4966
5028
|
class ProboPlaywright {
|
|
4967
|
-
constructor({ enableSmartSelectors = false, timeoutConfig = {}, debugLevel = ProboLogLevel.INFO, isCanceled }, page = null) {
|
|
5029
|
+
constructor({ enableSmartSelectors = false, timeoutConfig = {}, debugLevel = ProboLogLevel.INFO, isCanceled }, page = null, context) {
|
|
4968
5030
|
this.page = null;
|
|
5031
|
+
this.context = null;
|
|
5032
|
+
this.pagesByTabIndex = new Map();
|
|
5033
|
+
this.nextTabIndex = 1;
|
|
4969
5034
|
this.params = {};
|
|
4970
5035
|
this.isCanceled = null;
|
|
4971
5036
|
this.enableSmartSelectors = enableSmartSelectors;
|
|
@@ -4975,6 +5040,7 @@ class ProboPlaywright {
|
|
|
4975
5040
|
};
|
|
4976
5041
|
this.isCanceled = isCanceled || null;
|
|
4977
5042
|
this.highlighter = new Highlighter(enableSmartSelectors, false, debugLevel);
|
|
5043
|
+
this.context = context !== null && context !== void 0 ? context : null;
|
|
4978
5044
|
this.setPage(page);
|
|
4979
5045
|
proboLogger.setLogLevel(debugLevel);
|
|
4980
5046
|
}
|
|
@@ -4989,8 +5055,36 @@ class ProboPlaywright {
|
|
|
4989
5055
|
if (this.page) {
|
|
4990
5056
|
this.page.setDefaultNavigationTimeout(this.timeoutConfig.playwrightNavigationTimeout);
|
|
4991
5057
|
this.page.setDefaultTimeout(this.timeoutConfig.playwrightActionTimeout);
|
|
5058
|
+
this.pagesByTabIndex.set(0, this.page);
|
|
4992
5059
|
}
|
|
4993
5060
|
}
|
|
5061
|
+
/**
|
|
5062
|
+
* Sets the BrowserContext for multi-tab replay. Required for opensNewTab support.
|
|
5063
|
+
*/
|
|
5064
|
+
setContext(context) {
|
|
5065
|
+
this.context = context;
|
|
5066
|
+
}
|
|
5067
|
+
/**
|
|
5068
|
+
* Resets multi-tab state for a new replay. Clears captured tabs (keeps tab 0) and resets nextTabIndex.
|
|
5069
|
+
* Call at the start of replay before running interactions.
|
|
5070
|
+
*/
|
|
5071
|
+
resetMultiTabState() {
|
|
5072
|
+
// Clear captured tabs (indices > 0), keep tab 0
|
|
5073
|
+
for (const idx of this.pagesByTabIndex.keys()) {
|
|
5074
|
+
if (idx > 0) {
|
|
5075
|
+
this.pagesByTabIndex.delete(idx);
|
|
5076
|
+
}
|
|
5077
|
+
}
|
|
5078
|
+
this.nextTabIndex = 1;
|
|
5079
|
+
}
|
|
5080
|
+
/**
|
|
5081
|
+
* Gets the page for the given tab index. Tab 0 = initial page; higher indices = new tabs.
|
|
5082
|
+
*/
|
|
5083
|
+
getPageForTabIndex(tabIndex) {
|
|
5084
|
+
var _a;
|
|
5085
|
+
const page = (_a = this.pagesByTabIndex.get(tabIndex)) !== null && _a !== void 0 ? _a : this.page;
|
|
5086
|
+
return page !== null && page !== void 0 ? page : null;
|
|
5087
|
+
}
|
|
4994
5088
|
/**
|
|
4995
5089
|
* Sets the parameters object for template literal interpolation
|
|
4996
5090
|
* Stores a reference to the params object so mutations are automatically reflected
|
|
@@ -5017,417 +5111,441 @@ class ProboPlaywright {
|
|
|
5017
5111
|
* @throws Error if element is not found or validation fails
|
|
5018
5112
|
*/
|
|
5019
5113
|
async runStep(params) {
|
|
5020
|
-
|
|
5021
|
-
|
|
5022
|
-
|
|
5023
|
-
|
|
5024
|
-
|
|
5025
|
-
|
|
5026
|
-
|
|
5027
|
-
|
|
5028
|
-
|
|
5029
|
-
|
|
5030
|
-
|
|
5031
|
-
|
|
5032
|
-
|
|
5033
|
-
|
|
5034
|
-
|
|
5035
|
-
return arg;
|
|
5036
|
-
});
|
|
5037
|
-
}
|
|
5038
|
-
//trace message to help with debug
|
|
5039
|
-
proboLogger.info(`runStep(${JSON.stringify({ ...params, argument })})`);
|
|
5040
|
-
// 1. Check if we need to visit a url
|
|
5041
|
-
if (action === PlaywrightAction.VISIT_URL || action === PlaywrightAction.VISIT_BASE_URL) {
|
|
5042
|
-
try {
|
|
5043
|
-
await this.page.goto(argument, { timeout: this.timeoutConfig.playwrightNavigationTimeout });
|
|
5044
|
-
const navTracker = await NavTracker.getInstance(this.page, {
|
|
5045
|
-
waitForStabilityQuietTimeout: this.timeoutConfig.waitForStabilityQuietTimeout,
|
|
5046
|
-
waitForStabilityInitialDelay: this.timeoutConfig.waitForStabilityInitialDelay,
|
|
5047
|
-
waitForStabilityGlobalTimeout: this.timeoutConfig.waitForStabilityGlobalTimeout,
|
|
5048
|
-
waitForStabilityVerbose: this.timeoutConfig.waitForStabilityVerbose
|
|
5049
|
-
});
|
|
5050
|
-
await navTracker.waitForNavigationToStabilize();
|
|
5051
|
-
}
|
|
5052
|
-
catch (e) {
|
|
5053
|
-
throw new Error(`Failed to navigate to ${argument}`);
|
|
5054
|
-
}
|
|
5055
|
-
return;
|
|
5056
|
-
}
|
|
5057
|
-
// 2. Check if we need to assert the url
|
|
5058
|
-
if (action === PlaywrightAction.ASSERT_URL) {
|
|
5059
|
-
// wait for page to stabilize
|
|
5060
|
-
const navTracker = await NavTracker.getInstance(this.page, { waitForStabilityQuietTimeout: this.timeoutConfig.playwrightNavigationTimeout });
|
|
5061
|
-
await navTracker.waitForNavigationToStabilize();
|
|
5062
|
-
const currentUrl = await this.page.url();
|
|
5063
|
-
if (currentUrl !== argument) {
|
|
5064
|
-
throw new Error(`Assertion failed: Expected URL "${argument}" but got "${currentUrl}".`);
|
|
5114
|
+
var _a;
|
|
5115
|
+
let { action, argument = '', iframeSelector = '', elementSelector = '', smartSelector = null, smartIFrameSelector = null, annotation = '', tabIndex = 0, opensNewTab = false, } = params;
|
|
5116
|
+
// 0. Resolve page for this step (multi-tab: tabIndex selects which page to use)
|
|
5117
|
+
const stepPage = (_a = this.getPageForTabIndex(tabIndex)) !== null && _a !== void 0 ? _a : this.page;
|
|
5118
|
+
if (!stepPage) {
|
|
5119
|
+
throw new Error(`Page is not set for tabIndex ${tabIndex}`);
|
|
5120
|
+
}
|
|
5121
|
+
const originalPage = this.page;
|
|
5122
|
+
this.page = stepPage;
|
|
5123
|
+
// Bring the active tab to front so the user sees the page being interacted with
|
|
5124
|
+
await stepPage.bringToFront();
|
|
5125
|
+
try {
|
|
5126
|
+
// Interpolate argument if it's a string with template literals
|
|
5127
|
+
if (typeof argument === 'string' && argument.includes('${')) {
|
|
5128
|
+
argument = this.interpolate(argument);
|
|
5065
5129
|
}
|
|
5066
|
-
|
|
5067
|
-
|
|
5068
|
-
|
|
5069
|
-
|
|
5070
|
-
|
|
5071
|
-
|
|
5072
|
-
|
|
5073
|
-
|
|
5074
|
-
// Only capture base screenshot during apply-ai, skip during replay
|
|
5075
|
-
let baseScreenshotUrl = null;
|
|
5076
|
-
if (params.takeScreenshot && params.isApplyAIContext) {
|
|
5077
|
-
try {
|
|
5078
|
-
proboLogger.debug('Capturing base screenshot before element interaction');
|
|
5079
|
-
baseScreenshotUrl = await params.takeScreenshot(this.page, 'base');
|
|
5080
|
-
proboLogger.debug(`Base screenshot uploaded: ${baseScreenshotUrl}`);
|
|
5130
|
+
else if (Array.isArray(argument)) {
|
|
5131
|
+
// Handle array arguments (e.g., for UPLOAD_FILES or SELECT_DROPDOWN)
|
|
5132
|
+
argument = argument.map((arg) => {
|
|
5133
|
+
if (typeof arg === 'string' && arg.includes('${')) {
|
|
5134
|
+
return this.interpolate(arg);
|
|
5135
|
+
}
|
|
5136
|
+
return arg;
|
|
5137
|
+
});
|
|
5081
5138
|
}
|
|
5082
|
-
|
|
5083
|
-
|
|
5084
|
-
|
|
5139
|
+
//trace message to help with debug
|
|
5140
|
+
proboLogger.info(`runStep(${JSON.stringify({ ...params, argument })})`);
|
|
5141
|
+
// 1. Check if we need to visit a url
|
|
5142
|
+
if (action === PlaywrightAction.VISIT_URL || action === PlaywrightAction.VISIT_BASE_URL) {
|
|
5143
|
+
try {
|
|
5144
|
+
await this.page.goto(argument, { timeout: this.timeoutConfig.playwrightNavigationTimeout });
|
|
5145
|
+
const navTracker = await NavTracker.getInstance(this.page, {
|
|
5146
|
+
waitForStabilityQuietTimeout: this.timeoutConfig.waitForStabilityQuietTimeout,
|
|
5147
|
+
waitForStabilityInitialDelay: this.timeoutConfig.waitForStabilityInitialDelay,
|
|
5148
|
+
waitForStabilityGlobalTimeout: this.timeoutConfig.waitForStabilityGlobalTimeout,
|
|
5149
|
+
waitForStabilityVerbose: this.timeoutConfig.waitForStabilityVerbose
|
|
5150
|
+
});
|
|
5151
|
+
await navTracker.waitForNavigationToStabilize();
|
|
5152
|
+
}
|
|
5153
|
+
catch (e) {
|
|
5154
|
+
throw new Error(`Failed to navigate to ${argument}`);
|
|
5155
|
+
}
|
|
5156
|
+
return;
|
|
5085
5157
|
}
|
|
5086
|
-
|
|
5087
|
-
|
|
5088
|
-
|
|
5089
|
-
|
|
5090
|
-
|
|
5091
|
-
|
|
5092
|
-
|
|
5093
|
-
|
|
5094
|
-
}
|
|
5095
|
-
// Fail fast: immediately validate that the element exists for non-wait actions
|
|
5096
|
-
// Use Promise.race() for all actions to enable cancellation without sacrificing precision
|
|
5097
|
-
const locator_timeout = (action === PlaywrightAction.WAIT_FOR) ? params.timeout || 10000 : this.timeoutConfig.playwrightLocatorTimeout;
|
|
5098
|
-
// Create a cancellation promise that checks for cancellation every 100ms
|
|
5099
|
-
// This allows precise waits while still enabling cancellation for all actions
|
|
5100
|
-
const cancellationCheckInterval = 100; // Check for cancellation every 100ms
|
|
5101
|
-
// Create a promise that rejects when cancellation is detected
|
|
5102
|
-
const cancellationPromise = new Promise((_, reject) => {
|
|
5103
|
-
const checkCancellation = () => {
|
|
5104
|
-
if (this.isCanceled && this.isCanceled()) {
|
|
5105
|
-
reject(new Error('Replay execution was cancelled'));
|
|
5106
|
-
return;
|
|
5158
|
+
// 2. Check if we need to assert the url
|
|
5159
|
+
if (action === PlaywrightAction.ASSERT_URL) {
|
|
5160
|
+
// wait for page to stabilize
|
|
5161
|
+
const navTracker = await NavTracker.getInstance(this.page, { waitForStabilityQuietTimeout: this.timeoutConfig.playwrightNavigationTimeout });
|
|
5162
|
+
await navTracker.waitForNavigationToStabilize();
|
|
5163
|
+
const currentUrl = await this.page.url();
|
|
5164
|
+
if (currentUrl !== argument) {
|
|
5165
|
+
throw new Error(`Assertion failed: Expected URL "${argument}" but got "${currentUrl}".`);
|
|
5107
5166
|
}
|
|
5108
|
-
|
|
5109
|
-
|
|
5110
|
-
|
|
5111
|
-
|
|
5112
|
-
|
|
5113
|
-
|
|
5114
|
-
const waitForLocatorWithCancellation = async (targetLocator, timeout) => {
|
|
5115
|
-
return Promise.race([
|
|
5116
|
-
targetLocator.waitFor({ state: 'attached', timeout }),
|
|
5117
|
-
cancellationPromise
|
|
5118
|
-
]);
|
|
5119
|
-
};
|
|
5120
|
-
try {
|
|
5121
|
-
// Race between the actual wait and cancellation check for all actions
|
|
5122
|
-
// This allows precise waits while still enabling cancellation
|
|
5123
|
-
await waitForLocatorWithCancellation(locator, locator_timeout);
|
|
5124
|
-
}
|
|
5125
|
-
catch (e) {
|
|
5126
|
-
// If it's a cancellation error, re-throw it
|
|
5127
|
-
if (e instanceof Error && e.message === 'Replay execution was cancelled') {
|
|
5128
|
-
throw e;
|
|
5167
|
+
return;
|
|
5168
|
+
}
|
|
5169
|
+
// 3. Check if we need to type keys
|
|
5170
|
+
if (action === PlaywrightAction.TYPE_KEYS) {
|
|
5171
|
+
await this.robustTypeKeys(argument);
|
|
5172
|
+
return;
|
|
5129
5173
|
}
|
|
5130
|
-
//
|
|
5131
|
-
|
|
5174
|
+
// Capture base screenshot before element interaction (if screenshot function is provided)
|
|
5175
|
+
// Only capture base screenshot during apply-ai, skip during replay
|
|
5176
|
+
let baseScreenshotUrl = null;
|
|
5177
|
+
if (params.takeScreenshot && params.isApplyAIContext) {
|
|
5132
5178
|
try {
|
|
5133
|
-
proboLogger.
|
|
5134
|
-
|
|
5135
|
-
|
|
5136
|
-
locator = await this.getLocatorOrFrame(elementSelector, false, frameLocator);
|
|
5137
|
-
}
|
|
5138
|
-
else {
|
|
5139
|
-
locator = await this.getLocatorOrFrame(elementSelector, false);
|
|
5140
|
-
}
|
|
5141
|
-
// Race the fallback wait with cancellation as well
|
|
5142
|
-
await waitForLocatorWithCancellation(locator, 200);
|
|
5179
|
+
proboLogger.debug('Capturing base screenshot before element interaction');
|
|
5180
|
+
baseScreenshotUrl = await params.takeScreenshot(this.page, 'base');
|
|
5181
|
+
proboLogger.debug(`Base screenshot uploaded: ${baseScreenshotUrl}`);
|
|
5143
5182
|
}
|
|
5144
|
-
catch (
|
|
5145
|
-
|
|
5146
|
-
|
|
5147
|
-
throw e;
|
|
5148
|
-
}
|
|
5149
|
-
throw new Error(`Element not found with CSS selector: ${elementSelector} ${iframeSelector ? `in iframe: ${iframeSelector}` : ''} after ${locator_timeout}ms`);
|
|
5183
|
+
catch (baseError) {
|
|
5184
|
+
proboLogger.warn(`Failed to capture base screenshot: ${baseError}`);
|
|
5185
|
+
// Continue even if base screenshot fails
|
|
5150
5186
|
}
|
|
5151
5187
|
}
|
|
5152
|
-
|
|
5153
|
-
|
|
5188
|
+
// 4. Get the locator (iframe or not)
|
|
5189
|
+
const startTime = Date.now();
|
|
5190
|
+
let locator;
|
|
5191
|
+
if (iframeSelector && iframeSelector.length > 0) {
|
|
5192
|
+
const frameLocator = await this.getLocator(iframeSelector, smartIFrameSelector, true);
|
|
5193
|
+
locator = await this.getLocator(elementSelector, smartSelector, false, frameLocator);
|
|
5154
5194
|
}
|
|
5155
|
-
|
|
5156
|
-
|
|
5157
|
-
const visibleLocator = await findClosestVisibleElement(locator);
|
|
5158
|
-
if (visibleLocator) {
|
|
5159
|
-
locator = visibleLocator;
|
|
5195
|
+
else {
|
|
5196
|
+
locator = await this.getLocator(elementSelector, smartSelector);
|
|
5160
5197
|
}
|
|
5161
|
-
|
|
5162
|
-
|
|
5163
|
-
|
|
5164
|
-
|
|
5165
|
-
|
|
5166
|
-
|
|
5198
|
+
// Fail fast: immediately validate that the element exists for non-wait actions
|
|
5199
|
+
// Use Promise.race() for all actions to enable cancellation without sacrificing precision
|
|
5200
|
+
const locator_timeout = (action === PlaywrightAction.WAIT_FOR) ? params.timeout || 10000 : this.timeoutConfig.playwrightLocatorTimeout;
|
|
5201
|
+
// Create a cancellation promise that checks for cancellation every 100ms
|
|
5202
|
+
// This allows precise waits while still enabling cancellation for all actions
|
|
5203
|
+
const cancellationCheckInterval = 100; // Check for cancellation every 100ms
|
|
5204
|
+
// Create a promise that rejects when cancellation is detected
|
|
5205
|
+
const cancellationPromise = new Promise((_, reject) => {
|
|
5206
|
+
const checkCancellation = () => {
|
|
5207
|
+
if (this.isCanceled && this.isCanceled()) {
|
|
5208
|
+
reject(new Error('Replay execution was cancelled'));
|
|
5209
|
+
return;
|
|
5210
|
+
}
|
|
5211
|
+
// Continue checking until the wait completes or times out
|
|
5212
|
+
setTimeout(checkCancellation, cancellationCheckInterval);
|
|
5213
|
+
};
|
|
5214
|
+
checkCancellation();
|
|
5215
|
+
});
|
|
5216
|
+
// Helper function to wait for locator with cancellation support
|
|
5217
|
+
const waitForLocatorWithCancellation = async (targetLocator, timeout) => {
|
|
5218
|
+
return Promise.race([
|
|
5219
|
+
targetLocator.waitFor({ state: 'attached', timeout }),
|
|
5220
|
+
cancellationPromise
|
|
5221
|
+
]);
|
|
5222
|
+
};
|
|
5167
5223
|
try {
|
|
5168
|
-
//
|
|
5169
|
-
//
|
|
5170
|
-
|
|
5224
|
+
// Race between the actual wait and cancellation check for all actions
|
|
5225
|
+
// This allows precise waits while still enabling cancellation
|
|
5226
|
+
await waitForLocatorWithCancellation(locator, locator_timeout);
|
|
5227
|
+
}
|
|
5228
|
+
catch (e) {
|
|
5229
|
+
// If it's a cancellation error, re-throw it
|
|
5230
|
+
if (e instanceof Error && e.message === 'Replay execution was cancelled') {
|
|
5231
|
+
throw e;
|
|
5232
|
+
}
|
|
5233
|
+
// Timeout reached, try fallback if smart selectors are enabled
|
|
5234
|
+
if (this.enableSmartSelectors) {
|
|
5171
5235
|
try {
|
|
5172
|
-
|
|
5173
|
-
|
|
5174
|
-
|
|
5175
|
-
|
|
5176
|
-
|
|
5177
|
-
|
|
5178
|
-
|
|
5179
|
-
|
|
5180
|
-
|
|
5181
|
-
|
|
5182
|
-
// Unhighlight candidates
|
|
5183
|
-
await this.highlighter.unhighlightCached(this.page);
|
|
5236
|
+
proboLogger.warn(`Element not found with smart selector: ${JSON.stringify(smartSelector)} ${smartIFrameSelector ? `with iframe smart selector: ${JSON.stringify(smartIFrameSelector)}` : ''}. Falling back to CSS selector`, e);
|
|
5237
|
+
if (iframeSelector && iframeSelector.length > 0) {
|
|
5238
|
+
const frameLocator = await this.getLocatorOrFrame(iframeSelector, true);
|
|
5239
|
+
locator = await this.getLocatorOrFrame(elementSelector, false, frameLocator);
|
|
5240
|
+
}
|
|
5241
|
+
else {
|
|
5242
|
+
locator = await this.getLocatorOrFrame(elementSelector, false);
|
|
5243
|
+
}
|
|
5244
|
+
// Race the fallback wait with cancellation as well
|
|
5245
|
+
await waitForLocatorWithCancellation(locator, 200);
|
|
5184
5246
|
}
|
|
5185
|
-
catch (
|
|
5186
|
-
|
|
5187
|
-
|
|
5247
|
+
catch (e) {
|
|
5248
|
+
// If it's a cancellation error, re-throw it
|
|
5249
|
+
if (e instanceof Error && e.message === 'Replay execution was cancelled') {
|
|
5250
|
+
throw e;
|
|
5251
|
+
}
|
|
5252
|
+
throw new Error(`Element not found with CSS selector: ${elementSelector} ${iframeSelector ? `in iframe: ${iframeSelector}` : ''} after ${locator_timeout}ms`);
|
|
5188
5253
|
}
|
|
5189
5254
|
}
|
|
5190
|
-
|
|
5191
|
-
|
|
5255
|
+
else {
|
|
5256
|
+
throw new Error(`Element not found with CSS selector: ${elementSelector} ${iframeSelector ? `in iframe: ${iframeSelector}` : ''} after ${locator_timeout}ms`);
|
|
5257
|
+
}
|
|
5258
|
+
}
|
|
5259
|
+
if (action === PlaywrightAction.HOVER) {
|
|
5260
|
+
const visibleLocator = await findClosestVisibleElement(locator);
|
|
5261
|
+
if (visibleLocator) {
|
|
5262
|
+
locator = visibleLocator;
|
|
5263
|
+
}
|
|
5264
|
+
}
|
|
5265
|
+
// 5. Capture screenshots if screenshot function is provided
|
|
5266
|
+
const screenshotUrls = {
|
|
5267
|
+
base_screenshot_url: baseScreenshotUrl
|
|
5268
|
+
};
|
|
5269
|
+
if (params.takeScreenshot) {
|
|
5192
5270
|
try {
|
|
5193
|
-
|
|
5194
|
-
//
|
|
5195
|
-
if (
|
|
5196
|
-
|
|
5271
|
+
// Candidate elements screenshot: find and highlight candidates based on action type
|
|
5272
|
+
// Only capture candidate screenshot during apply-ai, skip during replay
|
|
5273
|
+
if (params.isApplyAIContext) {
|
|
5274
|
+
try {
|
|
5275
|
+
const elementTags = resolveElementTag(action);
|
|
5276
|
+
proboLogger.debug(`Finding candidate elements for action ${action} with tags: ${elementTags}`);
|
|
5277
|
+
await this.highlighter.findAndCacheCandidateElements(this.page, elementTags);
|
|
5278
|
+
await this.highlighter.highlightCachedElements(this.page, 'candidates');
|
|
5279
|
+
// Wait for highlight timeout to ensure highlights are visible
|
|
5280
|
+
// if (this.timeoutConfig.highlightTimeout > 0) {
|
|
5281
|
+
// await this.page.waitForTimeout(this.timeoutConfig.highlightTimeout);
|
|
5282
|
+
// }
|
|
5283
|
+
screenshotUrls.candidates_screenshot_url = await params.takeScreenshot(this.page, 'candidates');
|
|
5284
|
+
proboLogger.debug(`Candidates screenshot uploaded: ${screenshotUrls.candidates_screenshot_url}`);
|
|
5285
|
+
// Unhighlight candidates
|
|
5286
|
+
await this.highlighter.unhighlightCached(this.page);
|
|
5287
|
+
}
|
|
5288
|
+
catch (candidateError) {
|
|
5289
|
+
proboLogger.warn(`Failed to capture candidates screenshot: ${candidateError}`);
|
|
5290
|
+
// Continue even if candidate screenshot fails
|
|
5291
|
+
}
|
|
5197
5292
|
}
|
|
5198
|
-
|
|
5199
|
-
|
|
5200
|
-
// Unhighlight before performing action
|
|
5201
|
-
await this.unhighlight(locator);
|
|
5202
|
-
}
|
|
5203
|
-
catch (actualError) {
|
|
5204
|
-
proboLogger.warn(`Failed to capture actual element screenshot: ${actualError}`);
|
|
5205
|
-
// Try to unhighlight even if screenshot failed
|
|
5293
|
+
// Actual element screenshot: highlight the actual element
|
|
5294
|
+
// Always capture actual screenshot for both apply-ai and replay
|
|
5206
5295
|
try {
|
|
5296
|
+
await this.highlight(locator, annotation);
|
|
5297
|
+
// Wait for highlight timeout to ensure highlights are visible
|
|
5298
|
+
if (this.timeoutConfig.highlightTimeout > 0) {
|
|
5299
|
+
await this.page.waitForTimeout(this.timeoutConfig.highlightTimeout);
|
|
5300
|
+
}
|
|
5301
|
+
screenshotUrls.actual_interaction_screenshot_url = await params.takeScreenshot(this.page, 'actual');
|
|
5302
|
+
proboLogger.debug(`Actual element screenshot uploaded: ${screenshotUrls.actual_interaction_screenshot_url}`);
|
|
5303
|
+
// Unhighlight before performing action
|
|
5207
5304
|
await this.unhighlight(locator);
|
|
5208
5305
|
}
|
|
5209
|
-
catch (
|
|
5210
|
-
proboLogger.warn(`Failed to
|
|
5306
|
+
catch (actualError) {
|
|
5307
|
+
proboLogger.warn(`Failed to capture actual element screenshot: ${actualError}`);
|
|
5308
|
+
// Try to unhighlight even if screenshot failed
|
|
5309
|
+
try {
|
|
5310
|
+
await this.unhighlight(locator);
|
|
5311
|
+
}
|
|
5312
|
+
catch (unhighlightError) {
|
|
5313
|
+
proboLogger.warn(`Failed to unhighlight: ${unhighlightError}`);
|
|
5314
|
+
}
|
|
5315
|
+
}
|
|
5316
|
+
// Call the callback with screenshot URLs
|
|
5317
|
+
if (params.onScreenshots) {
|
|
5318
|
+
params.onScreenshots(screenshotUrls);
|
|
5211
5319
|
}
|
|
5212
5320
|
}
|
|
5213
|
-
|
|
5214
|
-
|
|
5215
|
-
|
|
5321
|
+
catch (screenshotError) {
|
|
5322
|
+
proboLogger.warn(`Screenshot capture failed: ${screenshotError}`);
|
|
5323
|
+
// Continue with action even if screenshots fail
|
|
5216
5324
|
}
|
|
5217
5325
|
}
|
|
5218
|
-
|
|
5219
|
-
|
|
5220
|
-
|
|
5221
|
-
|
|
5222
|
-
|
|
5223
|
-
|
|
5224
|
-
|
|
5225
|
-
if (this.timeoutConfig.highlightTimeout > 0) {
|
|
5226
|
-
await this.highlight(locator, annotation);
|
|
5227
|
-
await this.page.waitForTimeout(this.timeoutConfig.highlightTimeout);
|
|
5228
|
-
await this.unhighlight(locator);
|
|
5326
|
+
else {
|
|
5327
|
+
// 5. Highlight, wait, unhighlight if highlightTimeout > 0 (original behavior when no screenshots)
|
|
5328
|
+
if (this.timeoutConfig.highlightTimeout > 0) {
|
|
5329
|
+
await this.highlight(locator, annotation);
|
|
5330
|
+
await this.page.waitForTimeout(this.timeoutConfig.highlightTimeout);
|
|
5331
|
+
await this.unhighlight(locator);
|
|
5332
|
+
}
|
|
5229
5333
|
}
|
|
5230
|
-
|
|
5231
|
-
|
|
5232
|
-
|
|
5233
|
-
|
|
5234
|
-
|
|
5235
|
-
|
|
5236
|
-
|
|
5237
|
-
|
|
5238
|
-
|
|
5239
|
-
|
|
5240
|
-
|
|
5241
|
-
|
|
5242
|
-
|
|
5243
|
-
break;
|
|
5244
|
-
case PlaywrightAction.SET_SLIDER:
|
|
5245
|
-
await this.setSliderValue(locator, argument);
|
|
5246
|
-
break;
|
|
5247
|
-
case PlaywrightAction.WAIT_FOR_OTP:
|
|
5248
|
-
{
|
|
5249
|
-
const waitForOtpArgs = params;
|
|
5250
|
-
const otpInbox = typeof argument === 'string' && argument.trim() ? argument.trim() : undefined;
|
|
5251
|
-
const otpTimeout = waitForOtpArgs === null || waitForOtpArgs === void 0 ? void 0 : waitForOtpArgs.timeout;
|
|
5252
|
-
const otpCheckInterval = waitForOtpArgs === null || waitForOtpArgs === void 0 ? void 0 : waitForOtpArgs.checkInterval;
|
|
5253
|
-
const otpLookback = waitForOtpArgs === null || waitForOtpArgs === void 0 ? void 0 : waitForOtpArgs.checkRecentMessagesSinceMs;
|
|
5254
|
-
const otp = await OTP.waitForOTP({
|
|
5255
|
-
inbox: otpInbox,
|
|
5256
|
-
timeout: otpTimeout,
|
|
5257
|
-
checkInterval: otpCheckInterval,
|
|
5258
|
-
checkRecentMessagesSinceMs: otpLookback !== null && otpLookback !== void 0 ? otpLookback : 600000,
|
|
5259
|
-
});
|
|
5260
|
-
if (otp) {
|
|
5261
|
-
proboLogger.log(`✅ OTP found: ${otp}`);
|
|
5262
|
-
await locator.fill(otp, { timeout: this.timeoutConfig.playwrightActionTimeout });
|
|
5334
|
+
// 6. Action logic
|
|
5335
|
+
switch (action) {
|
|
5336
|
+
case PlaywrightAction.CLICK:
|
|
5337
|
+
case PlaywrightAction.CHECK_CHECKBOX:
|
|
5338
|
+
case PlaywrightAction.SELECT_RADIO:
|
|
5339
|
+
if (opensNewTab && this.context) {
|
|
5340
|
+
const [newPage] = await Promise.all([
|
|
5341
|
+
this.context.waitForEvent('page'),
|
|
5342
|
+
this.robustMouseAction(locator, 'click')
|
|
5343
|
+
]);
|
|
5344
|
+
const newTabIndex = this.nextTabIndex++;
|
|
5345
|
+
this.pagesByTabIndex.set(newTabIndex, newPage);
|
|
5346
|
+
proboLogger.info(`Multi-tab: captured new page for tabIndex ${newTabIndex}`);
|
|
5263
5347
|
}
|
|
5264
5348
|
else {
|
|
5265
|
-
|
|
5349
|
+
await this.robustMouseAction(locator, 'click');
|
|
5266
5350
|
}
|
|
5267
|
-
|
|
5268
|
-
|
|
5269
|
-
|
|
5270
|
-
|
|
5271
|
-
|
|
5272
|
-
|
|
5273
|
-
|
|
5274
|
-
|
|
5275
|
-
|
|
5276
|
-
|
|
5277
|
-
|
|
5278
|
-
|
|
5279
|
-
const
|
|
5280
|
-
|
|
5281
|
-
|
|
5282
|
-
|
|
5283
|
-
|
|
5284
|
-
|
|
5285
|
-
|
|
5286
|
-
|
|
5287
|
-
|
|
5288
|
-
|
|
5289
|
-
|
|
5290
|
-
|
|
5291
|
-
|
|
5292
|
-
|
|
5293
|
-
case PlaywrightAction.ASSERT_CONTAINS_VALUE:
|
|
5294
|
-
const containerText = await this.getTextValue(locator);
|
|
5295
|
-
if (!matchRegex(containerText, argument)) {
|
|
5296
|
-
throw new Error(`Validation failed. Expected text "${containerText}" to match "${argument}".`);
|
|
5297
|
-
}
|
|
5298
|
-
break;
|
|
5299
|
-
case PlaywrightAction.ASSERT_EXACT_VALUE:
|
|
5300
|
-
const actualText = await this.getTextValue(locator);
|
|
5301
|
-
if (actualText !== argument) {
|
|
5302
|
-
throw new Error(`Validation failed. Expected text "${argument}", but got "${actualText}".`);
|
|
5303
|
-
}
|
|
5304
|
-
break;
|
|
5305
|
-
case PlaywrightAction.HOVER:
|
|
5306
|
-
if (locator) {
|
|
5307
|
-
await this.robustMouseAction(locator, 'hover');
|
|
5308
|
-
}
|
|
5309
|
-
else {
|
|
5310
|
-
throw new Error('not executing HOVER because no visible ancestor found');
|
|
5311
|
-
}
|
|
5312
|
-
break;
|
|
5313
|
-
case PlaywrightAction.SCROLL_TO_ELEMENT:
|
|
5314
|
-
// Restore exact scroll positions from recording
|
|
5315
|
-
const scrollData = JSON.parse(argument);
|
|
5316
|
-
const desiredScrollTop = scrollData.scrollTop;
|
|
5317
|
-
const desiredScrollLeft = scrollData.scrollLeft;
|
|
5318
|
-
const maxAttempts = 100;
|
|
5319
|
-
try {
|
|
5320
|
-
proboLogger.log('🔄 Restoring scroll position for container:', locator, 'scrollTop:', desiredScrollTop, 'scrollLeft:', desiredScrollLeft);
|
|
5321
|
-
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
5322
|
-
// Perform scroll
|
|
5323
|
-
await locator.evaluate((el, scrollData) => {
|
|
5324
|
-
el.scrollTo({ left: scrollData.scrollLeft, top: scrollData.scrollTop, behavior: 'smooth' });
|
|
5325
|
-
}, { scrollTop: desiredScrollTop, scrollLeft: desiredScrollLeft }, { timeout: this.timeoutConfig.playwrightActionTimeout });
|
|
5326
|
-
// Wait for scroll to complete
|
|
5327
|
-
await this.page.waitForTimeout(50);
|
|
5328
|
-
// Get actual scroll positions
|
|
5329
|
-
const actualScroll = await locator.evaluate((el) => {
|
|
5330
|
-
return { scrollTop: el.scrollTop, scrollLeft: el.scrollLeft };
|
|
5331
|
-
}, { timeout: this.timeoutConfig.playwrightActionTimeout });
|
|
5332
|
-
// Compare actual vs desired
|
|
5333
|
-
const scrollTopMatch = Math.abs(actualScroll.scrollTop - desiredScrollTop) < 1;
|
|
5334
|
-
const scrollLeftMatch = Math.abs(actualScroll.scrollLeft - desiredScrollLeft) < 1;
|
|
5335
|
-
if (scrollTopMatch && scrollLeftMatch) {
|
|
5336
|
-
proboLogger.log(`🔄 Scroll position restored successfully on attempt ${attempt}`);
|
|
5337
|
-
break;
|
|
5338
|
-
}
|
|
5339
|
-
if (attempt < maxAttempts) {
|
|
5340
|
-
proboLogger.log(`🔄 Scroll position mismatch on attempt ${attempt}. Desired: (${desiredScrollTop}, ${desiredScrollLeft}), Actual: (${actualScroll.scrollTop}, ${actualScroll.scrollLeft}). Retrying...`);
|
|
5351
|
+
break;
|
|
5352
|
+
case PlaywrightAction.FILL_IN:
|
|
5353
|
+
await this.robustFill(locator, argument);
|
|
5354
|
+
break;
|
|
5355
|
+
case PlaywrightAction.SELECT_DROPDOWN:
|
|
5356
|
+
await locator.selectOption(argument, { timeout: this.timeoutConfig.playwrightActionTimeout });
|
|
5357
|
+
break;
|
|
5358
|
+
case PlaywrightAction.SET_SLIDER:
|
|
5359
|
+
await this.setSliderValue(locator, argument);
|
|
5360
|
+
break;
|
|
5361
|
+
case PlaywrightAction.WAIT_FOR_OTP:
|
|
5362
|
+
{
|
|
5363
|
+
const waitForOtpArgs = params;
|
|
5364
|
+
const otpInbox = typeof argument === 'string' && argument.trim() ? argument.trim() : undefined;
|
|
5365
|
+
const otpTimeout = waitForOtpArgs === null || waitForOtpArgs === void 0 ? void 0 : waitForOtpArgs.timeout;
|
|
5366
|
+
const otpCheckInterval = waitForOtpArgs === null || waitForOtpArgs === void 0 ? void 0 : waitForOtpArgs.checkInterval;
|
|
5367
|
+
const otpLookback = waitForOtpArgs === null || waitForOtpArgs === void 0 ? void 0 : waitForOtpArgs.checkRecentMessagesSinceMs;
|
|
5368
|
+
const otp = await OTP.waitForOTP({
|
|
5369
|
+
inbox: otpInbox,
|
|
5370
|
+
timeout: otpTimeout,
|
|
5371
|
+
checkInterval: otpCheckInterval,
|
|
5372
|
+
checkRecentMessagesSinceMs: otpLookback !== null && otpLookback !== void 0 ? otpLookback : 600000,
|
|
5373
|
+
});
|
|
5374
|
+
if (otp) {
|
|
5375
|
+
proboLogger.log(`✅ OTP found: ${otp}`);
|
|
5376
|
+
await locator.fill(otp, { timeout: this.timeoutConfig.playwrightActionTimeout });
|
|
5341
5377
|
}
|
|
5342
5378
|
else {
|
|
5343
|
-
proboLogger.
|
|
5379
|
+
proboLogger.log(`❌ OTP not found`);
|
|
5344
5380
|
}
|
|
5345
5381
|
}
|
|
5346
|
-
|
|
5347
|
-
|
|
5348
|
-
|
|
5349
|
-
|
|
5350
|
-
|
|
5351
|
-
|
|
5352
|
-
|
|
5353
|
-
|
|
5354
|
-
|
|
5355
|
-
|
|
5356
|
-
|
|
5357
|
-
|
|
5358
|
-
|
|
5359
|
-
|
|
5360
|
-
|
|
5361
|
-
|
|
5362
|
-
|
|
5363
|
-
|
|
5364
|
-
|
|
5365
|
-
|
|
5366
|
-
|
|
5367
|
-
|
|
5368
|
-
|
|
5369
|
-
|
|
5370
|
-
|
|
5371
|
-
|
|
5372
|
-
|
|
5373
|
-
|
|
5374
|
-
|
|
5375
|
-
}
|
|
5376
|
-
|
|
5377
|
-
|
|
5378
|
-
|
|
5379
|
-
|
|
5380
|
-
|
|
5381
|
-
|
|
5382
|
+
break;
|
|
5383
|
+
case PlaywrightAction.GEN_TOTP:
|
|
5384
|
+
// Use secret from argument (interpolated above) and auxiliary config (digits/algorithm) from totpConfig
|
|
5385
|
+
// If totpConfig is not provided, use defaults (digits: 6, algorithm: 'SHA1')
|
|
5386
|
+
const totpAux = params.totpConfig;
|
|
5387
|
+
const secretArg = argument;
|
|
5388
|
+
if (secretArg) {
|
|
5389
|
+
try {
|
|
5390
|
+
// Use provided config or defaults
|
|
5391
|
+
const digits = (totpAux === null || totpAux === void 0 ? void 0 : totpAux.digits) || 6;
|
|
5392
|
+
const algorithm = (totpAux === null || totpAux === void 0 ? void 0 : totpAux.algorithm) || 'SHA1';
|
|
5393
|
+
const totpCode = this.generateOTP(secretArg, digits, algorithm);
|
|
5394
|
+
proboLogger.log(`✅ TOTP generated (digits: ${digits}, algorithm: ${algorithm})`);
|
|
5395
|
+
await locator.fill(totpCode, { timeout: this.timeoutConfig.playwrightActionTimeout });
|
|
5396
|
+
}
|
|
5397
|
+
catch (error) {
|
|
5398
|
+
proboLogger.error(`❌ TOTP generation failed: ${error}`);
|
|
5399
|
+
throw new Error(`TOTP generation failed: ${error}`);
|
|
5400
|
+
}
|
|
5401
|
+
}
|
|
5402
|
+
else {
|
|
5403
|
+
proboLogger.log(`❌ Missing TOTP argument`);
|
|
5404
|
+
throw new Error(`Missing TOTP argument`);
|
|
5405
|
+
}
|
|
5406
|
+
break;
|
|
5407
|
+
case PlaywrightAction.ASSERT_CONTAINS_VALUE:
|
|
5408
|
+
const containerText = await this.getTextValue(locator);
|
|
5409
|
+
if (!matchRegex(containerText, argument)) {
|
|
5410
|
+
throw new Error(`Validation failed. Expected text "${containerText}" to match "${argument}".`);
|
|
5411
|
+
}
|
|
5412
|
+
break;
|
|
5413
|
+
case PlaywrightAction.ASSERT_EXACT_VALUE:
|
|
5414
|
+
const actualText = await this.getTextValue(locator);
|
|
5415
|
+
if (actualText !== argument) {
|
|
5416
|
+
throw new Error(`Validation failed. Expected text "${argument}", but got "${actualText}".`);
|
|
5417
|
+
}
|
|
5418
|
+
break;
|
|
5419
|
+
case PlaywrightAction.HOVER:
|
|
5420
|
+
if (locator) {
|
|
5421
|
+
await this.robustMouseAction(locator, 'hover');
|
|
5422
|
+
}
|
|
5423
|
+
else {
|
|
5424
|
+
throw new Error('not executing HOVER because no visible ancestor found');
|
|
5425
|
+
}
|
|
5426
|
+
break;
|
|
5427
|
+
case PlaywrightAction.SCROLL_TO_ELEMENT:
|
|
5428
|
+
// Restore exact scroll positions from recording
|
|
5429
|
+
const scrollData = JSON.parse(argument);
|
|
5430
|
+
const desiredScrollTop = scrollData.scrollTop;
|
|
5431
|
+
const desiredScrollLeft = scrollData.scrollLeft;
|
|
5432
|
+
const maxAttempts = 100;
|
|
5382
5433
|
try {
|
|
5383
|
-
|
|
5384
|
-
|
|
5385
|
-
|
|
5386
|
-
|
|
5387
|
-
|
|
5388
|
-
|
|
5389
|
-
|
|
5390
|
-
|
|
5391
|
-
|
|
5434
|
+
proboLogger.log('🔄 Restoring scroll position for container:', locator, 'scrollTop:', desiredScrollTop, 'scrollLeft:', desiredScrollLeft);
|
|
5435
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
5436
|
+
// Perform scroll
|
|
5437
|
+
await locator.evaluate((el, scrollData) => {
|
|
5438
|
+
el.scrollTo({ left: scrollData.scrollLeft, top: scrollData.scrollTop, behavior: 'smooth' });
|
|
5439
|
+
}, { scrollTop: desiredScrollTop, scrollLeft: desiredScrollLeft }, { timeout: this.timeoutConfig.playwrightActionTimeout });
|
|
5440
|
+
// Wait for scroll to complete
|
|
5441
|
+
await this.page.waitForTimeout(50);
|
|
5442
|
+
// Get actual scroll positions
|
|
5443
|
+
const actualScroll = await locator.evaluate((el) => {
|
|
5444
|
+
return { scrollTop: el.scrollTop, scrollLeft: el.scrollLeft };
|
|
5445
|
+
}, { timeout: this.timeoutConfig.playwrightActionTimeout });
|
|
5446
|
+
// Compare actual vs desired
|
|
5447
|
+
const scrollTopMatch = Math.abs(actualScroll.scrollTop - desiredScrollTop) < 1;
|
|
5448
|
+
const scrollLeftMatch = Math.abs(actualScroll.scrollLeft - desiredScrollLeft) < 1;
|
|
5449
|
+
if (scrollTopMatch && scrollLeftMatch) {
|
|
5450
|
+
proboLogger.log(`🔄 Scroll position restored successfully on attempt ${attempt}`);
|
|
5392
5451
|
break;
|
|
5393
5452
|
}
|
|
5394
|
-
|
|
5395
|
-
|
|
5396
|
-
|
|
5397
|
-
|
|
5398
|
-
|
|
5399
|
-
const actualPollInterval = Math.min(pollingInterval, remainingTime);
|
|
5400
|
-
if (actualPollInterval > 0) {
|
|
5401
|
-
if (!this.page) {
|
|
5402
|
-
throw new Error('Page is not set');
|
|
5403
|
-
}
|
|
5404
|
-
// Race between the timeout and cancellation check
|
|
5405
|
-
await Promise.race([
|
|
5406
|
-
this.page.waitForTimeout(actualPollInterval),
|
|
5407
|
-
cancellationPromise
|
|
5408
|
-
]);
|
|
5453
|
+
if (attempt < maxAttempts) {
|
|
5454
|
+
proboLogger.log(`🔄 Scroll position mismatch on attempt ${attempt}. Desired: (${desiredScrollTop}, ${desiredScrollLeft}), Actual: (${actualScroll.scrollTop}, ${actualScroll.scrollLeft}). Retrying...`);
|
|
5455
|
+
}
|
|
5456
|
+
else {
|
|
5457
|
+
proboLogger.warn(`🔄 Scroll position mismatch after ${maxAttempts} attempts. Desired: (${desiredScrollTop}, ${desiredScrollLeft}), Final: (${actualScroll.scrollTop}, ${actualScroll.scrollLeft})`);
|
|
5409
5458
|
}
|
|
5410
5459
|
}
|
|
5411
5460
|
}
|
|
5412
5461
|
catch (e) {
|
|
5413
|
-
|
|
5414
|
-
|
|
5415
|
-
|
|
5462
|
+
proboLogger.error('🔄 Failed to restore scroll position for container:', locator, 'scrollTop:', desiredScrollTop, 'scrollLeft:', desiredScrollLeft, 'error:', e);
|
|
5463
|
+
}
|
|
5464
|
+
break;
|
|
5465
|
+
case PlaywrightAction.UPLOAD_FILES:
|
|
5466
|
+
await locator.setInputFiles(argument, { timeout: this.timeoutConfig.playwrightActionTimeout });
|
|
5467
|
+
break;
|
|
5468
|
+
case PlaywrightAction.EXTRACT_VALUE:
|
|
5469
|
+
let extractedText = await this.getTextValue(locator);
|
|
5470
|
+
return extractedText;
|
|
5471
|
+
case PlaywrightAction.WAIT_FOR:
|
|
5472
|
+
const expectedText = argument;
|
|
5473
|
+
// Use smaller polling interval for better precision (can detect changes sooner)
|
|
5474
|
+
const pollingInterval = params.pollingInterval || 100; // Default 100ms for better precision
|
|
5475
|
+
const timeout = params.timeout || 10000; // Default 10 seconds
|
|
5476
|
+
// Create a cancellation promise that rejects when cancellation is detected
|
|
5477
|
+
// This allows us to cancel the text polling loop at any time
|
|
5478
|
+
const createCancellationPromise = () => {
|
|
5479
|
+
const cancellationCheckInterval = 50; // Check for cancellation every 50ms
|
|
5480
|
+
return new Promise((_, reject) => {
|
|
5481
|
+
const checkCancellation = () => {
|
|
5482
|
+
if (this.isCanceled && this.isCanceled()) {
|
|
5483
|
+
reject(new Error('Replay execution was cancelled'));
|
|
5484
|
+
return;
|
|
5485
|
+
}
|
|
5486
|
+
setTimeout(checkCancellation, cancellationCheckInterval);
|
|
5487
|
+
};
|
|
5488
|
+
checkCancellation();
|
|
5489
|
+
});
|
|
5490
|
+
};
|
|
5491
|
+
let textMatches = false;
|
|
5492
|
+
let currentText = '';
|
|
5493
|
+
const waitStartTime = Date.now();
|
|
5494
|
+
const cancellationPromise = createCancellationPromise();
|
|
5495
|
+
while (!textMatches && (Date.now() - waitStartTime) < timeout) {
|
|
5496
|
+
try {
|
|
5497
|
+
// Check if element is visible first
|
|
5498
|
+
const isVisible = await locator.isVisible();
|
|
5499
|
+
if (isVisible) {
|
|
5500
|
+
// Get the current text content only if element is visible
|
|
5501
|
+
currentText = await this.getTextValue(locator);
|
|
5502
|
+
// Check if the text matches (using the same logic as ASSERT_CONTAINS_VALUE)
|
|
5503
|
+
if (matchRegex(currentText, expectedText)) {
|
|
5504
|
+
textMatches = true;
|
|
5505
|
+
proboLogger.log(`✅ Wait for text completed successfully. Found: "${currentText}"`);
|
|
5506
|
+
break;
|
|
5507
|
+
}
|
|
5508
|
+
}
|
|
5509
|
+
// Text doesn't match yet or element not visible, wait for the polling interval
|
|
5510
|
+
// Use Promise.race() to allow cancellation during the wait (consistent with element attachment logic)
|
|
5511
|
+
if ((Date.now() - waitStartTime) < timeout) {
|
|
5512
|
+
const remainingTime = timeout - (Date.now() - waitStartTime);
|
|
5513
|
+
const actualPollInterval = Math.min(pollingInterval, remainingTime);
|
|
5514
|
+
if (actualPollInterval > 0) {
|
|
5515
|
+
if (!this.page) {
|
|
5516
|
+
throw new Error('Page is not set');
|
|
5517
|
+
}
|
|
5518
|
+
// Race between the timeout and cancellation check
|
|
5519
|
+
await Promise.race([
|
|
5520
|
+
this.page.waitForTimeout(actualPollInterval),
|
|
5521
|
+
cancellationPromise
|
|
5522
|
+
]);
|
|
5523
|
+
}
|
|
5524
|
+
}
|
|
5525
|
+
}
|
|
5526
|
+
catch (e) {
|
|
5527
|
+
// If it's a cancellation error, re-throw it
|
|
5528
|
+
if (e instanceof Error && e.message === 'Replay execution was cancelled') {
|
|
5529
|
+
throw e;
|
|
5530
|
+
}
|
|
5531
|
+
throw new Error(`Wait for text failed while trying to extract text from selector: ${elementSelector}${iframeSelector ? ` in iframe: ${iframeSelector}` : ''}`);
|
|
5416
5532
|
}
|
|
5417
|
-
throw new Error(`Wait for text failed while trying to extract text from selector: ${elementSelector}${iframeSelector ? ` in iframe: ${iframeSelector}` : ''}`);
|
|
5418
5533
|
}
|
|
5419
|
-
|
|
5420
|
-
|
|
5421
|
-
|
|
5422
|
-
|
|
5423
|
-
|
|
5424
|
-
|
|
5425
|
-
|
|
5426
|
-
|
|
5427
|
-
|
|
5428
|
-
|
|
5429
|
-
|
|
5430
|
-
|
|
5534
|
+
// Final cancellation check
|
|
5535
|
+
if (this.isCanceled && this.isCanceled()) {
|
|
5536
|
+
throw new Error('Replay execution was cancelled');
|
|
5537
|
+
}
|
|
5538
|
+
// Timeout reached without a match
|
|
5539
|
+
if (!textMatches) {
|
|
5540
|
+
throw new Error(`Wait for text failed. Expected "${expectedText}" to match "${currentText}" after ${timeout}ms of polling every ${pollingInterval}ms`);
|
|
5541
|
+
}
|
|
5542
|
+
break;
|
|
5543
|
+
default:
|
|
5544
|
+
throw new Error(`Unhandled action: ${action}`);
|
|
5545
|
+
}
|
|
5546
|
+
}
|
|
5547
|
+
finally {
|
|
5548
|
+
this.page = originalPage;
|
|
5431
5549
|
}
|
|
5432
5550
|
}
|
|
5433
5551
|
/**
|