@silasfmartins/testhub 1.0.5 → 1.0.7
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.
|
@@ -172,8 +172,10 @@ export declare class WebActions {
|
|
|
172
172
|
}>;
|
|
173
173
|
/** Timestamp do último refreshDOM (debounce para evitar múltiplas chamadas consecutivas) */
|
|
174
174
|
private static _lastDOMRefreshTime;
|
|
175
|
-
/**
|
|
176
|
-
|
|
175
|
+
/** Contador de profundidade: >0 significa que estamos dentro de withRecovery.
|
|
176
|
+
* Só a chamada mais externa (depth===1) executa refreshDOM, catalog e recovery.
|
|
177
|
+
* Todas as chamadas internas/sequenciais executam fn direto. */
|
|
178
|
+
private static _withRecoveryDepth;
|
|
177
179
|
/**
|
|
178
180
|
* 🔄 Força atualização/estabilização do DOM antes de localizar/interagir com elementos.
|
|
179
181
|
* Essencial para SPAs (ex.: Salesforce Lightning/Vlocity) que recriam Shadow DOM a cada interação.
|
|
@@ -873,8 +873,10 @@ export class WebActions {
|
|
|
873
873
|
}
|
|
874
874
|
/** Timestamp do último refreshDOM (debounce para evitar múltiplas chamadas consecutivas) */
|
|
875
875
|
static _lastDOMRefreshTime = 0;
|
|
876
|
-
/**
|
|
877
|
-
|
|
876
|
+
/** Contador de profundidade: >0 significa que estamos dentro de withRecovery.
|
|
877
|
+
* Só a chamada mais externa (depth===1) executa refreshDOM, catalog e recovery.
|
|
878
|
+
* Todas as chamadas internas/sequenciais executam fn direto. */
|
|
879
|
+
static _withRecoveryDepth = 0;
|
|
878
880
|
/**
|
|
879
881
|
* 🔄 Força atualização/estabilização do DOM antes de localizar/interagir com elementos.
|
|
880
882
|
* Essencial para SPAs (ex.: Salesforce Lightning/Vlocity) que recriam Shadow DOM a cada interação.
|
|
@@ -977,379 +979,378 @@ export class WebActions {
|
|
|
977
979
|
}
|
|
978
980
|
}
|
|
979
981
|
static async withRecovery(originalElement, actionName, fn, valueForContext) {
|
|
980
|
-
// 🛡️
|
|
981
|
-
//
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
982
|
+
// 🛡️ Incrementar depth ANTES de qualquer coisa.
|
|
983
|
+
// Garante que chamadas sequenciais dentro de click() (strategy A, B, C...)
|
|
984
|
+
// e chamadas recursivas (click_other→click) todas incrementam o contador.
|
|
985
|
+
WebActions._withRecoveryDepth++;
|
|
986
|
+
try {
|
|
987
|
+
// Se depth > 1, estamos dentro de uma cadeia de recovery — executar fn direto.
|
|
988
|
+
// Isso cobre: (a) chamadas recursivas via click_other, (b) chamadas sequenciais
|
|
989
|
+
// de strategies dentro do mesmo click() enquanto o depth do caller ainda está ativo.
|
|
990
|
+
if (WebActions._withRecoveryDepth > 1) {
|
|
991
|
+
const initialCandidate = await WebActions.resolveCandidateForRecovery(originalElement);
|
|
992
|
+
return await fn(initialCandidate);
|
|
993
|
+
}
|
|
994
|
+
// === Daqui para baixo, só a chamada mais externa (depth===1) executa ===
|
|
995
|
+
// 🔄 Refresh DOM antes da primeira tentativa
|
|
996
|
+
await WebActions.refreshDOM(150).catch(() => { });
|
|
997
|
+
// 🎯 Catalog pre-resolve + lazy auto-refresh
|
|
998
|
+
if (typeof originalElement === "string") {
|
|
999
|
+
try {
|
|
1000
|
+
WebActions.ensureCatalogExtractor();
|
|
1001
|
+
let resolved = WebActions.resolveXPathFromCatalog(originalElement);
|
|
1002
|
+
if (!resolved && TestContext.isPageInitialized()) {
|
|
1003
|
+
const pageUrl = TestContext.getPage().url();
|
|
1004
|
+
const urlKey = pageUrl.split("?")[0].split("#")[0].toLowerCase();
|
|
1005
|
+
if (!WebActions._catalogPopulatedPages.has(urlKey)) {
|
|
1006
|
+
WebActions._catalogPopulatedPages.add(urlKey);
|
|
1007
|
+
await XPathCatalog.refresh(pageUrl).catch(() => { });
|
|
1008
|
+
resolved = WebActions.resolveXPathFromCatalog(originalElement);
|
|
1009
|
+
}
|
|
1000
1010
|
}
|
|
1011
|
+
if (resolved)
|
|
1012
|
+
originalElement = resolved;
|
|
1001
1013
|
}
|
|
1002
|
-
|
|
1003
|
-
originalElement = resolved;
|
|
1014
|
+
catch { }
|
|
1004
1015
|
}
|
|
1005
|
-
catch { }
|
|
1006
|
-
}
|
|
1007
|
-
// 🛡️ Capturar estado do lock ANTES de entrar no try/catch.
|
|
1008
|
-
// Somente quem mudou _inRecovery de false→true pode resetar no finally.
|
|
1009
|
-
const wasAlreadyInRecovery = WebActions._inRecovery;
|
|
1010
|
-
try {
|
|
1011
|
-
const initialCandidate = await WebActions.resolveCandidateForRecovery(originalElement);
|
|
1012
|
-
return await fn(initialCandidate);
|
|
1013
|
-
}
|
|
1014
|
-
catch (error) {
|
|
1015
|
-
// 🔄 Refresh DOM imediato após falha
|
|
1016
|
-
await WebActions.refreshDOM(200).catch(() => { });
|
|
1017
|
-
// 🛡️ Ativar lock — click_other/navigateTo/switchTo dentro deste recovery
|
|
1018
|
-
// vão cair no guard acima e executar fn direto, sem nova recovery.
|
|
1019
|
-
WebActions._inRecovery = true;
|
|
1020
1016
|
try {
|
|
1021
|
-
const
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
const failedXPath = WebActions.extractSelector(originalElement);
|
|
1028
|
-
const detectErrorType = (err) => {
|
|
1029
|
-
const msg = err && err.message
|
|
1030
|
-
? String(err.message).toLowerCase()
|
|
1031
|
-
: String(err || "").toLowerCase();
|
|
1032
|
-
if (msg.includes("strict mode violation") ||
|
|
1033
|
-
msg.includes("multiple elements"))
|
|
1034
|
-
return "multiple_elements";
|
|
1035
|
-
if (msg.includes("timeout") ||
|
|
1036
|
-
msg.includes("timed out") ||
|
|
1037
|
-
err?.code === "ETIMEDOUT")
|
|
1038
|
-
return "timeout";
|
|
1039
|
-
if (msg.includes("detached") || msg.includes("stale"))
|
|
1040
|
-
return "stale_element";
|
|
1041
|
-
if (msg.includes("navigation") && msg.includes("wrong"))
|
|
1042
|
-
return "wrong_navigation";
|
|
1043
|
-
return "not_found";
|
|
1044
|
-
};
|
|
1045
|
-
const detectedErrorType = detectErrorType(error);
|
|
1046
|
-
// 🎯 Mapear nome do método para actionType correto do Test Recovery
|
|
1047
|
-
const normalizedActionType = methodToActionType(actionName);
|
|
1048
|
-
// Capturar contexto da página para melhor análise
|
|
1049
|
-
let pageHtml = "";
|
|
1050
|
-
let pageStructure = undefined;
|
|
1017
|
+
const initialCandidate = await WebActions.resolveCandidateForRecovery(originalElement);
|
|
1018
|
+
return await fn(initialCandidate);
|
|
1019
|
+
}
|
|
1020
|
+
catch (error) {
|
|
1021
|
+
// 🔄 Refresh DOM imediato após falha
|
|
1022
|
+
await WebActions.refreshDOM(200).catch(() => { });
|
|
1051
1023
|
try {
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1024
|
+
const MIN_CONF = Number(process.env.AUTOCORE_RECOVERY_MIN_CONFIDENCE || "0.7");
|
|
1025
|
+
const maxRetries = Number(process.env.AUTOCORE_RECOVERY_MAX_RETRIES || "8");
|
|
1026
|
+
const meta = getRecoveryMetadata();
|
|
1027
|
+
const pageUrl = TestContext.isPageInitialized()
|
|
1028
|
+
? TestContext.getPage().url()
|
|
1029
|
+
: "";
|
|
1030
|
+
const failedXPath = WebActions.extractSelector(originalElement);
|
|
1031
|
+
const detectErrorType = (err) => {
|
|
1032
|
+
const msg = err && err.message
|
|
1033
|
+
? String(err.message).toLowerCase()
|
|
1034
|
+
: String(err || "").toLowerCase();
|
|
1035
|
+
if (msg.includes("strict mode violation") ||
|
|
1036
|
+
msg.includes("multiple elements"))
|
|
1037
|
+
return "multiple_elements";
|
|
1038
|
+
if (msg.includes("timeout") ||
|
|
1039
|
+
msg.includes("timed out") ||
|
|
1040
|
+
err?.code === "ETIMEDOUT")
|
|
1041
|
+
return "timeout";
|
|
1042
|
+
if (msg.includes("detached") || msg.includes("stale"))
|
|
1043
|
+
return "stale_element";
|
|
1044
|
+
if (msg.includes("navigation") && msg.includes("wrong"))
|
|
1045
|
+
return "wrong_navigation";
|
|
1046
|
+
return "not_found";
|
|
1047
|
+
};
|
|
1048
|
+
const detectedErrorType = detectErrorType(error);
|
|
1049
|
+
// 🎯 Mapear nome do método para actionType correto do Test Recovery
|
|
1050
|
+
const normalizedActionType = methodToActionType(actionName);
|
|
1051
|
+
// Capturar contexto da página para melhor análise
|
|
1052
|
+
let pageHtml = "";
|
|
1053
|
+
let pageStructure = undefined;
|
|
1054
|
+
try {
|
|
1055
|
+
if (TestContext.isPageInitialized()) {
|
|
1056
|
+
const page = TestContext.getPage();
|
|
1057
|
+
// Capturar HTML (limitado a 50KB para performance)
|
|
1058
|
+
pageHtml = await page.content().catch(() => "");
|
|
1059
|
+
if (pageHtml.length > 50000)
|
|
1060
|
+
pageHtml = pageHtml.substring(0, 50000) + "... (truncated)";
|
|
1061
|
+
// Capturar estrutura básica
|
|
1062
|
+
pageStructure = await WebActions.extractBasicPageStructure(page).catch(() => undefined);
|
|
1063
|
+
}
|
|
1060
1064
|
}
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
// Ignorar erros de captura
|
|
1064
|
-
}
|
|
1065
|
-
// Capturar texto visível do elemento para contexto
|
|
1066
|
-
let elementText = "";
|
|
1067
|
-
try {
|
|
1068
|
-
if (TestContext.isPageInitialized() && failedXPath) {
|
|
1069
|
-
elementText =
|
|
1070
|
-
(await TestContext.getPage()
|
|
1071
|
-
.locator(failedXPath)
|
|
1072
|
-
.first()
|
|
1073
|
-
.textContent({ timeout: 2000 })
|
|
1074
|
-
.catch(() => "")) || "";
|
|
1075
|
-
elementText = elementText.trim().slice(0, 100);
|
|
1065
|
+
catch {
|
|
1066
|
+
// Ignorar erros de captura
|
|
1076
1067
|
}
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
await WebActions.navigateTo(a.url, "Recovery navigate", false);
|
|
1142
|
-
}
|
|
1143
|
-
catch { }
|
|
1144
|
-
break;
|
|
1145
|
-
case "refresh":
|
|
1146
|
-
try {
|
|
1068
|
+
// Capturar texto visível do elemento para contexto
|
|
1069
|
+
let elementText = "";
|
|
1070
|
+
try {
|
|
1071
|
+
if (TestContext.isPageInitialized() && failedXPath) {
|
|
1072
|
+
elementText =
|
|
1073
|
+
(await TestContext.getPage()
|
|
1074
|
+
.locator(failedXPath)
|
|
1075
|
+
.first()
|
|
1076
|
+
.textContent({ timeout: 2000 })
|
|
1077
|
+
.catch(() => "")) || "";
|
|
1078
|
+
elementText = elementText.trim().slice(0, 100);
|
|
1079
|
+
}
|
|
1080
|
+
}
|
|
1081
|
+
catch { }
|
|
1082
|
+
// Obter filledInputs/filledFields do singleton (v2.7.43)
|
|
1083
|
+
let filledInputs = [];
|
|
1084
|
+
let filledFields = [];
|
|
1085
|
+
try {
|
|
1086
|
+
const { testRecoveryHelper } = await import("../utils/testRecovery/TestRecoveryClient.js");
|
|
1087
|
+
filledInputs = testRecoveryHelper.getFilledInputs();
|
|
1088
|
+
filledFields = testRecoveryHelper.getFilledFields();
|
|
1089
|
+
}
|
|
1090
|
+
catch { }
|
|
1091
|
+
// Obter metadados de página a partir do stack trace (item 20 v2.7.44)
|
|
1092
|
+
const pageMeta = WebActions.getCallerPageMetadata();
|
|
1093
|
+
const response = await analyzeFailure({
|
|
1094
|
+
url: pageUrl || "",
|
|
1095
|
+
failedXPath,
|
|
1096
|
+
errorType: detectedErrorType,
|
|
1097
|
+
actionType: normalizedActionType,
|
|
1098
|
+
sessionId: TestContext.getOrCreateSessionId(),
|
|
1099
|
+
executionSource: meta.executionSource,
|
|
1100
|
+
testCaseName: meta.testCaseName,
|
|
1101
|
+
testCaseId: meta.testCaseId,
|
|
1102
|
+
testMethodName: meta.testMethodName,
|
|
1103
|
+
context: {
|
|
1104
|
+
text: elementText || undefined,
|
|
1105
|
+
value: valueForContext != null ? String(valueForContext) : undefined,
|
|
1106
|
+
originalMethod: actionName,
|
|
1107
|
+
filledInputs,
|
|
1108
|
+
filledFields,
|
|
1109
|
+
pageFileName: pageMeta.pageFileName,
|
|
1110
|
+
pageName: pageMeta.pageName,
|
|
1111
|
+
pageObjectName: pageMeta.pageObjectName,
|
|
1112
|
+
},
|
|
1113
|
+
pageHtml: pageHtml || undefined,
|
|
1114
|
+
pageStructure,
|
|
1115
|
+
});
|
|
1116
|
+
if (response && response.analysis && response.analysis.suggestedXPath) {
|
|
1117
|
+
const { suggestedXPath, confidence } = response.analysis;
|
|
1118
|
+
if (confidence >= MIN_CONF) {
|
|
1119
|
+
const alternatives = response.analysis.alternatives || [];
|
|
1120
|
+
const candidates = [];
|
|
1121
|
+
if (suggestedXPath)
|
|
1122
|
+
candidates.push(suggestedXPath);
|
|
1123
|
+
if (Array.isArray(alternatives) && alternatives.length)
|
|
1124
|
+
candidates.push(...alternatives);
|
|
1125
|
+
// executar actions sugeridas antes das tentativas (v2.7.43)
|
|
1126
|
+
let _inRecoveryFrame = false;
|
|
1127
|
+
if (response.actions && response.actions.length) {
|
|
1128
|
+
for (const a of response.actions) {
|
|
1129
|
+
try {
|
|
1130
|
+
switch (a.type) {
|
|
1131
|
+
case "wait":
|
|
1147
1132
|
if (TestContext.isPageInitialized())
|
|
1148
|
-
await TestContext.getPage().
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
try {
|
|
1154
|
-
if (a.selector) {
|
|
1155
|
-
await WebActions.switchTo(a.selector, "Recovery frame switch", false);
|
|
1156
|
-
_inRecoveryFrame = true;
|
|
1133
|
+
await TestContext.getPage().waitForTimeout(a.waitMs || 1000);
|
|
1134
|
+
break;
|
|
1135
|
+
case "click_other":
|
|
1136
|
+
try {
|
|
1137
|
+
await WebActions.click(a.selector || "", "", false);
|
|
1157
1138
|
}
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
? candidates[candidateIndex]
|
|
1186
|
-
: suggestedXPath;
|
|
1187
|
-
if (!candidate)
|
|
1188
|
-
break;
|
|
1189
|
-
try {
|
|
1190
|
-
// 🔄 Refresh DOM antes de cada retry para capturar Shadow DOM recriado (SFA/LWC)
|
|
1191
|
-
await WebActions.refreshDOM(300);
|
|
1192
|
-
await switchToDefaultIfNeeded();
|
|
1193
|
-
// Re-executar switch_frame antes de cada tentativa
|
|
1194
|
-
if (response.actions?.length) {
|
|
1195
|
-
for (const a of response.actions) {
|
|
1196
|
-
if (a.type === "switch_frame" && a.selector) {
|
|
1197
|
-
try {
|
|
1198
|
-
await WebActions.switchTo(a.selector, "Recovery frame switch", false);
|
|
1199
|
-
_inRecoveryFrame = true;
|
|
1200
|
-
}
|
|
1201
|
-
catch { }
|
|
1139
|
+
catch { }
|
|
1140
|
+
break;
|
|
1141
|
+
case "navigate":
|
|
1142
|
+
try {
|
|
1143
|
+
if (a.url)
|
|
1144
|
+
await WebActions.navigateTo(a.url, "Recovery navigate", false);
|
|
1145
|
+
}
|
|
1146
|
+
catch { }
|
|
1147
|
+
break;
|
|
1148
|
+
case "refresh":
|
|
1149
|
+
try {
|
|
1150
|
+
if (TestContext.isPageInitialized())
|
|
1151
|
+
await TestContext.getPage().reload();
|
|
1152
|
+
}
|
|
1153
|
+
catch { }
|
|
1154
|
+
break;
|
|
1155
|
+
case "switch_frame":
|
|
1156
|
+
try {
|
|
1157
|
+
if (a.selector) {
|
|
1158
|
+
await WebActions.switchTo(a.selector, "Recovery frame switch", false);
|
|
1159
|
+
_inRecoveryFrame = true;
|
|
1160
|
+
}
|
|
1161
|
+
}
|
|
1162
|
+
catch { }
|
|
1163
|
+
break;
|
|
1164
|
+
default:
|
|
1165
|
+
break;
|
|
1202
1166
|
}
|
|
1203
1167
|
}
|
|
1168
|
+
catch { }
|
|
1204
1169
|
}
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1170
|
+
}
|
|
1171
|
+
const switchToDefaultIfNeeded = async () => {
|
|
1172
|
+
if (!_inRecoveryFrame)
|
|
1173
|
+
return;
|
|
1208
1174
|
try {
|
|
1209
|
-
|
|
1175
|
+
if (typeof WebActions.switchToDefault === "function") {
|
|
1176
|
+
await WebActions.switchToDefault("Recovery frame switch back", false);
|
|
1177
|
+
}
|
|
1210
1178
|
}
|
|
1211
1179
|
catch { }
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1180
|
+
_inRecoveryFrame = false;
|
|
1181
|
+
};
|
|
1182
|
+
// Ordem recomendada (v2.7.43):
|
|
1183
|
+
// 1. suggestedXPath → 2. alternatives[] → report-result → auto-fix-local
|
|
1184
|
+
const logId = response.logId;
|
|
1185
|
+
for (let attempt = 2; attempt <= Math.max(2, maxRetries); attempt++) {
|
|
1186
|
+
const candidateIndex = Math.min(attempt - 2, candidates.length - 1);
|
|
1187
|
+
const candidate = candidates.length
|
|
1188
|
+
? candidates[candidateIndex]
|
|
1189
|
+
: suggestedXPath;
|
|
1190
|
+
if (!candidate)
|
|
1191
|
+
break;
|
|
1218
1192
|
try {
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1193
|
+
// 🔄 Refresh DOM antes de cada retry para capturar Shadow DOM recriado (SFA/LWC)
|
|
1194
|
+
await WebActions.refreshDOM(300);
|
|
1195
|
+
await switchToDefaultIfNeeded();
|
|
1196
|
+
// Re-executar switch_frame antes de cada tentativa
|
|
1197
|
+
if (response.actions?.length) {
|
|
1198
|
+
for (const a of response.actions) {
|
|
1199
|
+
if (a.type === "switch_frame" && a.selector) {
|
|
1200
|
+
try {
|
|
1201
|
+
await WebActions.switchTo(a.selector, "Recovery frame switch", false);
|
|
1202
|
+
_inRecoveryFrame = true;
|
|
1203
|
+
}
|
|
1204
|
+
catch { }
|
|
1205
|
+
}
|
|
1206
|
+
}
|
|
1207
|
+
}
|
|
1208
|
+
const resolvedCandidate = await WebActions.resolveCandidateForRecovery(candidate);
|
|
1209
|
+
const res = await fn(resolvedCandidate);
|
|
1210
|
+
await switchToDefaultIfNeeded();
|
|
1211
|
+
try {
|
|
1212
|
+
await markSuccess(TestContext.getOrCreateSessionId(), attempt);
|
|
1213
|
+
}
|
|
1214
|
+
catch { }
|
|
1215
|
+
// Registrar recovery bem-sucedido na timeline (Item 7)
|
|
1216
|
+
ActionTimeline.record("recovery", actionName, "retry", 0, {
|
|
1217
|
+
selector: candidate,
|
|
1218
|
+
metadata: { attempt },
|
|
1231
1219
|
});
|
|
1232
|
-
|
|
1233
|
-
catch { }
|
|
1234
|
-
// Auto-fix local if confidence >= 0.7 and autoFixAvailable (v2.7.44)
|
|
1235
|
-
if (response.analysis?.autoFixAvailable &&
|
|
1236
|
-
confidence >= 0.7) {
|
|
1220
|
+
// Report success (v2.7.43)
|
|
1237
1221
|
try {
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1222
|
+
await reportResult({
|
|
1223
|
+
logId,
|
|
1224
|
+
sessionId: TestContext.getOrCreateSessionId(),
|
|
1225
|
+
xpath: candidate,
|
|
1226
|
+
attemptNumber: attempt,
|
|
1227
|
+
success: true,
|
|
1228
|
+
autoFixed: false,
|
|
1229
|
+
recoverySource: "framework",
|
|
1230
|
+
executionSource: meta.executionSource,
|
|
1231
|
+
testCaseName: meta.testCaseName,
|
|
1232
|
+
testCaseId: meta.testCaseId,
|
|
1233
|
+
testMethodName: meta.testMethodName,
|
|
1234
|
+
});
|
|
1235
|
+
}
|
|
1236
|
+
catch { }
|
|
1237
|
+
// Auto-fix local if confidence >= 0.7 and autoFixAvailable (v2.7.44)
|
|
1238
|
+
if (response.analysis?.autoFixAvailable &&
|
|
1239
|
+
confidence >= 0.7) {
|
|
1247
1240
|
try {
|
|
1248
|
-
const {
|
|
1249
|
-
const
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
executionSource: "Local",
|
|
1258
|
-
});
|
|
1259
|
-
autoFixConfirmed = !!(mcpFix?.success && mcpFix.fileUpdated !== false);
|
|
1260
|
-
}
|
|
1261
|
-
catch (mcpFixError) {
|
|
1262
|
-
Logger.warning(`[WebActions] Auto-fix via MCP local falhou: ${mcpFixError instanceof Error ? mcpFixError.message : String(mcpFixError)}`);
|
|
1263
|
-
}
|
|
1264
|
-
// 2) Fallback HTTP auto-fix-local quando MCP local não confirmou escrita
|
|
1265
|
-
if (!autoFixConfirmed) {
|
|
1266
|
-
const apiFix = await testRecoveryHelper.autoFixLocal({
|
|
1267
|
-
projectRoot: projRoot,
|
|
1268
|
-
oldXPath: failedXPath,
|
|
1269
|
-
newXPath: candidate,
|
|
1270
|
-
confidence,
|
|
1271
|
-
methodName: resolvedMethodName,
|
|
1272
|
-
executionSource: "Local",
|
|
1273
|
-
className: attrCtx.className,
|
|
1274
|
-
attributesFile: attrCtx.attributesFile,
|
|
1275
|
-
});
|
|
1276
|
-
autoFixConfirmed = !!(apiFix.success && apiFix.fix?.filePath);
|
|
1277
|
-
if (!autoFixConfirmed) {
|
|
1278
|
-
Logger.warning(`[WebActions] Auto-fix não confirmou escrita local para ${failedXPath} -> ${candidate}`);
|
|
1279
|
-
}
|
|
1280
|
-
}
|
|
1281
|
-
if (autoFixConfirmed) {
|
|
1241
|
+
const { testRecoveryHelper } = await import("../utils/testRecovery/TestRecoveryClient.js");
|
|
1242
|
+
const projRoot = testRecoveryHelper.getProjectRoot() ||
|
|
1243
|
+
process.env.AUTOCORE_PROJECT_ROOT ||
|
|
1244
|
+
process.cwd();
|
|
1245
|
+
// Enriquecer auto-fix com contexto de Attributes/Page (item 7/12 v2.7.44)
|
|
1246
|
+
const attrCtx = WebActions.getCallerAttributesContext();
|
|
1247
|
+
const resolvedMethodName = attrCtx.methodName || meta.testMethodName;
|
|
1248
|
+
let autoFixConfirmed = false;
|
|
1249
|
+
// 1) MCP local-first (preferencial)
|
|
1282
1250
|
try {
|
|
1283
|
-
await
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
testCaseId: meta.testCaseId,
|
|
1294
|
-
testMethodName: meta.testMethodName,
|
|
1251
|
+
const { fixXPathViaLocalMcp } = await import("../utils/McpLocalClient.js");
|
|
1252
|
+
const mcpFix = await fixXPathViaLocalMcp({
|
|
1253
|
+
projectRoot: projRoot,
|
|
1254
|
+
oldXPath: failedXPath,
|
|
1255
|
+
newXPath: candidate,
|
|
1256
|
+
confidence,
|
|
1257
|
+
className: attrCtx.className,
|
|
1258
|
+
attributesFile: attrCtx.attributesFile,
|
|
1259
|
+
methodName: resolvedMethodName,
|
|
1260
|
+
executionSource: "Local",
|
|
1295
1261
|
});
|
|
1262
|
+
autoFixConfirmed = !!(mcpFix?.success && mcpFix.fileUpdated !== false);
|
|
1263
|
+
}
|
|
1264
|
+
catch (mcpFixError) {
|
|
1265
|
+
Logger.warning(`[WebActions] Auto-fix via MCP local falhou: ${mcpFixError instanceof Error ? mcpFixError.message : String(mcpFixError)}`);
|
|
1266
|
+
}
|
|
1267
|
+
// 2) Fallback HTTP auto-fix-local quando MCP local não confirmou escrita
|
|
1268
|
+
if (!autoFixConfirmed) {
|
|
1269
|
+
const apiFix = await testRecoveryHelper.autoFixLocal({
|
|
1270
|
+
projectRoot: projRoot,
|
|
1271
|
+
oldXPath: failedXPath,
|
|
1272
|
+
newXPath: candidate,
|
|
1273
|
+
confidence,
|
|
1274
|
+
methodName: resolvedMethodName,
|
|
1275
|
+
executionSource: "Local",
|
|
1276
|
+
className: attrCtx.className,
|
|
1277
|
+
attributesFile: attrCtx.attributesFile,
|
|
1278
|
+
});
|
|
1279
|
+
autoFixConfirmed = !!(apiFix.success && apiFix.fix?.filePath);
|
|
1280
|
+
if (!autoFixConfirmed) {
|
|
1281
|
+
Logger.warning(`[WebActions] Auto-fix não confirmou escrita local para ${failedXPath} -> ${candidate}`);
|
|
1282
|
+
}
|
|
1283
|
+
}
|
|
1284
|
+
if (autoFixConfirmed) {
|
|
1285
|
+
try {
|
|
1286
|
+
await reportResult({
|
|
1287
|
+
logId,
|
|
1288
|
+
sessionId: TestContext.getOrCreateSessionId(),
|
|
1289
|
+
xpath: candidate,
|
|
1290
|
+
attemptNumber: attempt,
|
|
1291
|
+
success: true,
|
|
1292
|
+
autoFixed: true,
|
|
1293
|
+
recoverySource: "framework",
|
|
1294
|
+
executionSource: meta.executionSource,
|
|
1295
|
+
testCaseName: meta.testCaseName,
|
|
1296
|
+
testCaseId: meta.testCaseId,
|
|
1297
|
+
testMethodName: meta.testMethodName,
|
|
1298
|
+
});
|
|
1299
|
+
}
|
|
1300
|
+
catch { }
|
|
1296
1301
|
}
|
|
1297
|
-
catch { }
|
|
1298
1302
|
}
|
|
1303
|
+
catch { }
|
|
1299
1304
|
}
|
|
1300
|
-
|
|
1305
|
+
return res;
|
|
1306
|
+
}
|
|
1307
|
+
catch (retryErr) {
|
|
1308
|
+
continue;
|
|
1301
1309
|
}
|
|
1302
|
-
return res;
|
|
1303
1310
|
}
|
|
1304
|
-
|
|
1305
|
-
|
|
1311
|
+
// Reportar falha após esgotar tentativas (v2.7.43)
|
|
1312
|
+
await switchToDefaultIfNeeded();
|
|
1313
|
+
try {
|
|
1314
|
+
await reportResult({
|
|
1315
|
+
logId,
|
|
1316
|
+
sessionId: TestContext.getOrCreateSessionId(),
|
|
1317
|
+
xpath: suggestedXPath,
|
|
1318
|
+
attemptNumber: maxRetries,
|
|
1319
|
+
success: false,
|
|
1320
|
+
recoverySource: "framework",
|
|
1321
|
+
executionSource: meta.executionSource,
|
|
1322
|
+
testCaseName: meta.testCaseName,
|
|
1323
|
+
testCaseId: meta.testCaseId,
|
|
1324
|
+
testMethodName: meta.testMethodName,
|
|
1325
|
+
});
|
|
1306
1326
|
}
|
|
1327
|
+
catch { }
|
|
1307
1328
|
}
|
|
1308
|
-
// Reportar falha após esgotar tentativas (v2.7.43)
|
|
1309
|
-
await switchToDefaultIfNeeded();
|
|
1310
|
-
try {
|
|
1311
|
-
await reportResult({
|
|
1312
|
-
logId,
|
|
1313
|
-
sessionId: TestContext.getOrCreateSessionId(),
|
|
1314
|
-
xpath: suggestedXPath,
|
|
1315
|
-
attemptNumber: maxRetries,
|
|
1316
|
-
success: false,
|
|
1317
|
-
recoverySource: "framework",
|
|
1318
|
-
executionSource: meta.executionSource,
|
|
1319
|
-
testCaseName: meta.testCaseName,
|
|
1320
|
-
testCaseId: meta.testCaseId,
|
|
1321
|
-
testMethodName: meta.testMethodName,
|
|
1322
|
-
});
|
|
1323
|
-
}
|
|
1324
|
-
catch { }
|
|
1325
1329
|
}
|
|
1326
1330
|
}
|
|
1331
|
+
catch (recoveryErr) {
|
|
1332
|
+
// silenciar falhas de recovery
|
|
1333
|
+
}
|
|
1334
|
+
// Enfileirar falha na RecoveryQueue para processamento assíncrono (Item 6)
|
|
1335
|
+
const failedSel = WebActions.extractSelector(originalElement);
|
|
1336
|
+
RecoveryQueue.enqueue(actionName, failedSel, error?.message || String(error), {
|
|
1337
|
+
url: TestContext.isPageInitialized()
|
|
1338
|
+
? TestContext.getPage().url()
|
|
1339
|
+
: "",
|
|
1340
|
+
sessionId: TestContext.getOrCreateSessionId(),
|
|
1341
|
+
value: valueForContext != null ? String(valueForContext) : undefined,
|
|
1342
|
+
});
|
|
1343
|
+
// Registrar falha na ActionTimeline (Item 7)
|
|
1344
|
+
ActionTimeline.record("recovery", actionName, "failure", 0, {
|
|
1345
|
+
selector: failedSel,
|
|
1346
|
+
error: error?.message || String(error),
|
|
1347
|
+
});
|
|
1348
|
+
throw error;
|
|
1327
1349
|
}
|
|
1328
|
-
catch (recoveryErr) {
|
|
1329
|
-
// silenciar falhas de recovery
|
|
1330
|
-
}
|
|
1331
|
-
// Enfileirar falha na RecoveryQueue para processamento assíncrono (Item 6)
|
|
1332
|
-
const failedSel = WebActions.extractSelector(originalElement);
|
|
1333
|
-
RecoveryQueue.enqueue(actionName, failedSel, error?.message || String(error), {
|
|
1334
|
-
url: TestContext.isPageInitialized()
|
|
1335
|
-
? TestContext.getPage().url()
|
|
1336
|
-
: "",
|
|
1337
|
-
sessionId: TestContext.getOrCreateSessionId(),
|
|
1338
|
-
value: valueForContext != null ? String(valueForContext) : undefined,
|
|
1339
|
-
});
|
|
1340
|
-
// Registrar falha na ActionTimeline (Item 7)
|
|
1341
|
-
ActionTimeline.record("recovery", actionName, "failure", 0, {
|
|
1342
|
-
selector: failedSel,
|
|
1343
|
-
error: error?.message || String(error),
|
|
1344
|
-
});
|
|
1345
|
-
throw error;
|
|
1346
1350
|
}
|
|
1347
1351
|
finally {
|
|
1348
|
-
// 🛡️
|
|
1349
|
-
|
|
1350
|
-
if (!wasAlreadyInRecovery) {
|
|
1351
|
-
WebActions._inRecovery = false;
|
|
1352
|
-
}
|
|
1352
|
+
// 🛡️ Decrementar depth — o lock só zera quando a chamada mais externa termina.
|
|
1353
|
+
WebActions._withRecoveryDepth--;
|
|
1353
1354
|
}
|
|
1354
1355
|
}
|
|
1355
1356
|
// Helper genérico para tentar heurísticas de preenchimento/atribuição de valor
|
|
@@ -1,38 +1,94 @@
|
|
|
1
1
|
import { WebActions } from './WebActions.js';
|
|
2
2
|
import { Logger } from '../utils/Logger.js';
|
|
3
|
-
// Register wrappers that call WebActions.withRecovery around public static methods
|
|
4
|
-
// This file is imported once during AutoCore initialization to enable global recovery.
|
|
5
3
|
(function registerRecoveryWrappers() {
|
|
6
4
|
try {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
'
|
|
10
|
-
'
|
|
11
|
-
'
|
|
12
|
-
'
|
|
13
|
-
'
|
|
14
|
-
'
|
|
15
|
-
'
|
|
16
|
-
'
|
|
17
|
-
'
|
|
18
|
-
'
|
|
19
|
-
'
|
|
20
|
-
'
|
|
21
|
-
'
|
|
22
|
-
'
|
|
23
|
-
'
|
|
24
|
-
'
|
|
5
|
+
// Whitelist: only these user-facing action methods get recovery wrapping
|
|
6
|
+
const actionMethods = new Set([
|
|
7
|
+
'click',
|
|
8
|
+
'doubleClick',
|
|
9
|
+
'rightClick',
|
|
10
|
+
'clickAndHold',
|
|
11
|
+
'clickAndWait',
|
|
12
|
+
'clickMenuOption',
|
|
13
|
+
'clickAlert',
|
|
14
|
+
'waitAndClickAlert',
|
|
15
|
+
'setText',
|
|
16
|
+
'clearAndType',
|
|
17
|
+
'clearField',
|
|
18
|
+
'typeSlowly',
|
|
19
|
+
'setPhone',
|
|
20
|
+
'setPhoneRobust',
|
|
21
|
+
'setDate',
|
|
22
|
+
'setDateNative',
|
|
23
|
+
'setDateRobust',
|
|
24
|
+
'setFiles',
|
|
25
|
+
'setInnerHTML',
|
|
26
|
+
'setInnerText',
|
|
27
|
+
'selectOption',
|
|
28
|
+
'selectOptionByXPath',
|
|
29
|
+
'selectDropdownOption',
|
|
30
|
+
'selectFromList',
|
|
31
|
+
'selectTableOption',
|
|
32
|
+
'checkCheckbox',
|
|
33
|
+
'uncheckCheckbox',
|
|
34
|
+
'hover',
|
|
35
|
+
'focus',
|
|
36
|
+
'mouseMove',
|
|
37
|
+
'pressKey',
|
|
38
|
+
'pressWithModifier',
|
|
39
|
+
'navigateTo',
|
|
40
|
+
'switchTo',
|
|
41
|
+
'switchToDefault',
|
|
42
|
+
'switchToWindowByTitle',
|
|
43
|
+
'switchToWindowByUrl',
|
|
44
|
+
'closeWindowAndSwitchBack',
|
|
45
|
+
'closePage',
|
|
46
|
+
'closeAllTabs',
|
|
47
|
+
'goBack',
|
|
48
|
+
'goForward',
|
|
49
|
+
'refresh',
|
|
50
|
+
'hardRefresh',
|
|
51
|
+
'scrollTo',
|
|
52
|
+
'scrollToElement',
|
|
53
|
+
'dragAndDrop',
|
|
54
|
+
'validateObject',
|
|
55
|
+
'validateObjectNotExists',
|
|
56
|
+
'validateObjectCount',
|
|
57
|
+
'validateObjectNotExistsCount',
|
|
58
|
+
'validateEquals',
|
|
59
|
+
'validateCheckboxChecked',
|
|
60
|
+
'verifyCondition',
|
|
61
|
+
'verifyExists',
|
|
62
|
+
'waitForVisible',
|
|
63
|
+
'waitForElement',
|
|
64
|
+
'waitForClickable',
|
|
65
|
+
'waitForElementVisible',
|
|
66
|
+
'waitForElementDetached',
|
|
67
|
+
'waitForLoad',
|
|
68
|
+
'waitForNavigation',
|
|
69
|
+
'waitForNetworkIdle',
|
|
70
|
+
'waitForPageReady',
|
|
71
|
+
'waitForAlert',
|
|
72
|
+
'getText',
|
|
73
|
+
'getTextContent',
|
|
74
|
+
'getAttribute',
|
|
75
|
+
'getFieldValue',
|
|
76
|
+
'getUrl',
|
|
77
|
+
'isVisible',
|
|
78
|
+
'isEnabled',
|
|
79
|
+
'countElements',
|
|
80
|
+
'toHaveCount',
|
|
81
|
+
'takeScreenshot',
|
|
82
|
+
'executeJavaScript',
|
|
83
|
+
'executeScriptOnElement',
|
|
84
|
+
'setViewportSize',
|
|
85
|
+
'fixedWait',
|
|
25
86
|
]);
|
|
26
|
-
const
|
|
27
|
-
const candidates = staticNames.filter((n) => {
|
|
28
|
-
if (exclude.has(n))
|
|
29
|
-
return false;
|
|
30
|
-
const v = WebActions[n];
|
|
31
|
-
return typeof v === 'function';
|
|
32
|
-
});
|
|
33
|
-
for (const name of candidates) {
|
|
87
|
+
for (const name of actionMethods) {
|
|
34
88
|
const orig = WebActions[name];
|
|
35
|
-
if (
|
|
89
|
+
if (typeof orig !== 'function')
|
|
90
|
+
continue;
|
|
91
|
+
if (orig.__recovery_wrapped)
|
|
36
92
|
continue;
|
|
37
93
|
const wrapped = function (...args) {
|
|
38
94
|
const originalElement = args.length ? args[0] : undefined;
|
|
@@ -48,7 +104,6 @@ import { Logger } from '../utils/Logger.js';
|
|
|
48
104
|
}
|
|
49
105
|
}
|
|
50
106
|
catch (e) {
|
|
51
|
-
// eslint-disable-next-line no-console
|
|
52
107
|
Logger.warning(`⚠️ [WebActions] Failed to register recovery wrappers: ${e}`);
|
|
53
108
|
}
|
|
54
109
|
})();
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@silasfmartins/testhub",
|
|
3
3
|
"description": "Biblioteca de utilitários para automação de testes",
|
|
4
|
-
"version": "1.0.
|
|
4
|
+
"version": "1.0.7",
|
|
5
5
|
"author": "Silas Martins Feliciano da Silva <silas.martins2041@gmail.com>",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"main": "dist/index.js",
|