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