@silasfmartins/testhub 1.0.4 → 1.0.6
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,373 +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
|
-
try {
|
|
1008
|
-
const initialCandidate = await WebActions.resolveCandidateForRecovery(originalElement);
|
|
1009
|
-
return await fn(initialCandidate);
|
|
1010
|
-
}
|
|
1011
|
-
catch (error) {
|
|
1012
|
-
// 🔄 Refresh DOM imediato após falha
|
|
1013
|
-
await WebActions.refreshDOM(200).catch(() => { });
|
|
1014
|
-
// 🛡️ Ativar lock global — tudo que rodar a partir daqui (click_other, navigateTo, etc.)
|
|
1015
|
-
// vai cair no guard acima e executar fn direto, sem nova recovery.
|
|
1016
|
-
WebActions._inRecovery = true;
|
|
1017
1016
|
try {
|
|
1018
|
-
const
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
const failedXPath = WebActions.extractSelector(originalElement);
|
|
1025
|
-
const detectErrorType = (err) => {
|
|
1026
|
-
const msg = err && err.message
|
|
1027
|
-
? String(err.message).toLowerCase()
|
|
1028
|
-
: String(err || "").toLowerCase();
|
|
1029
|
-
if (msg.includes("strict mode violation") ||
|
|
1030
|
-
msg.includes("multiple elements"))
|
|
1031
|
-
return "multiple_elements";
|
|
1032
|
-
if (msg.includes("timeout") ||
|
|
1033
|
-
msg.includes("timed out") ||
|
|
1034
|
-
err?.code === "ETIMEDOUT")
|
|
1035
|
-
return "timeout";
|
|
1036
|
-
if (msg.includes("detached") || msg.includes("stale"))
|
|
1037
|
-
return "stale_element";
|
|
1038
|
-
if (msg.includes("navigation") && msg.includes("wrong"))
|
|
1039
|
-
return "wrong_navigation";
|
|
1040
|
-
return "not_found";
|
|
1041
|
-
};
|
|
1042
|
-
const detectedErrorType = detectErrorType(error);
|
|
1043
|
-
// 🎯 Mapear nome do método para actionType correto do Test Recovery
|
|
1044
|
-
const normalizedActionType = methodToActionType(actionName);
|
|
1045
|
-
// Capturar contexto da página para melhor análise
|
|
1046
|
-
let pageHtml = "";
|
|
1047
|
-
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(() => { });
|
|
1048
1023
|
try {
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
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
|
+
}
|
|
1057
1064
|
}
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
// Ignorar erros de captura
|
|
1061
|
-
}
|
|
1062
|
-
// Capturar texto visível do elemento para contexto
|
|
1063
|
-
let elementText = "";
|
|
1064
|
-
try {
|
|
1065
|
-
if (TestContext.isPageInitialized() && failedXPath) {
|
|
1066
|
-
elementText =
|
|
1067
|
-
(await TestContext.getPage()
|
|
1068
|
-
.locator(failedXPath)
|
|
1069
|
-
.first()
|
|
1070
|
-
.textContent({ timeout: 2000 })
|
|
1071
|
-
.catch(() => "")) || "";
|
|
1072
|
-
elementText = elementText.trim().slice(0, 100);
|
|
1065
|
+
catch {
|
|
1066
|
+
// Ignorar erros de captura
|
|
1073
1067
|
}
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
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
|
-
await WebActions.navigateTo(a.url, "Recovery navigate", false);
|
|
1139
|
-
}
|
|
1140
|
-
catch { }
|
|
1141
|
-
break;
|
|
1142
|
-
case "refresh":
|
|
1143
|
-
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":
|
|
1144
1132
|
if (TestContext.isPageInitialized())
|
|
1145
|
-
await TestContext.getPage().
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
try {
|
|
1151
|
-
if (a.selector) {
|
|
1152
|
-
await WebActions.switchTo(a.selector, "Recovery frame switch", false);
|
|
1153
|
-
_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);
|
|
1154
1138
|
}
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
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
|
-
? candidates[candidateIndex]
|
|
1183
|
-
: suggestedXPath;
|
|
1184
|
-
if (!candidate)
|
|
1185
|
-
break;
|
|
1186
|
-
try {
|
|
1187
|
-
// 🔄 Refresh DOM antes de cada retry para capturar Shadow DOM recriado (SFA/LWC)
|
|
1188
|
-
await WebActions.refreshDOM(300);
|
|
1189
|
-
await switchToDefaultIfNeeded();
|
|
1190
|
-
// Re-executar switch_frame antes de cada tentativa
|
|
1191
|
-
if (response.actions?.length) {
|
|
1192
|
-
for (const a of response.actions) {
|
|
1193
|
-
if (a.type === "switch_frame" && a.selector) {
|
|
1194
|
-
try {
|
|
1195
|
-
await WebActions.switchTo(a.selector, "Recovery frame switch", false);
|
|
1196
|
-
_inRecoveryFrame = true;
|
|
1197
|
-
}
|
|
1198
|
-
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;
|
|
1199
1166
|
}
|
|
1200
1167
|
}
|
|
1168
|
+
catch { }
|
|
1201
1169
|
}
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1170
|
+
}
|
|
1171
|
+
const switchToDefaultIfNeeded = async () => {
|
|
1172
|
+
if (!_inRecoveryFrame)
|
|
1173
|
+
return;
|
|
1205
1174
|
try {
|
|
1206
|
-
|
|
1175
|
+
if (typeof WebActions.switchToDefault === "function") {
|
|
1176
|
+
await WebActions.switchToDefault("Recovery frame switch back", false);
|
|
1177
|
+
}
|
|
1207
1178
|
}
|
|
1208
1179
|
catch { }
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
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;
|
|
1215
1192
|
try {
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
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 },
|
|
1228
1219
|
});
|
|
1229
|
-
|
|
1230
|
-
catch { }
|
|
1231
|
-
// Auto-fix local if confidence >= 0.7 and autoFixAvailable (v2.7.44)
|
|
1232
|
-
if (response.analysis?.autoFixAvailable &&
|
|
1233
|
-
confidence >= 0.7) {
|
|
1220
|
+
// Report success (v2.7.43)
|
|
1234
1221
|
try {
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
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) {
|
|
1244
1240
|
try {
|
|
1245
|
-
const {
|
|
1246
|
-
const
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
executionSource: "Local",
|
|
1255
|
-
});
|
|
1256
|
-
autoFixConfirmed = !!(mcpFix?.success && mcpFix.fileUpdated !== false);
|
|
1257
|
-
}
|
|
1258
|
-
catch (mcpFixError) {
|
|
1259
|
-
Logger.warning(`[WebActions] Auto-fix via MCP local falhou: ${mcpFixError instanceof Error ? mcpFixError.message : String(mcpFixError)}`);
|
|
1260
|
-
}
|
|
1261
|
-
// 2) Fallback HTTP auto-fix-local quando MCP local não confirmou escrita
|
|
1262
|
-
if (!autoFixConfirmed) {
|
|
1263
|
-
const apiFix = await testRecoveryHelper.autoFixLocal({
|
|
1264
|
-
projectRoot: projRoot,
|
|
1265
|
-
oldXPath: failedXPath,
|
|
1266
|
-
newXPath: candidate,
|
|
1267
|
-
confidence,
|
|
1268
|
-
methodName: resolvedMethodName,
|
|
1269
|
-
executionSource: "Local",
|
|
1270
|
-
className: attrCtx.className,
|
|
1271
|
-
attributesFile: attrCtx.attributesFile,
|
|
1272
|
-
});
|
|
1273
|
-
autoFixConfirmed = !!(apiFix.success && apiFix.fix?.filePath);
|
|
1274
|
-
if (!autoFixConfirmed) {
|
|
1275
|
-
Logger.warning(`[WebActions] Auto-fix não confirmou escrita local para ${failedXPath} -> ${candidate}`);
|
|
1276
|
-
}
|
|
1277
|
-
}
|
|
1278
|
-
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)
|
|
1279
1250
|
try {
|
|
1280
|
-
await
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
testCaseId: meta.testCaseId,
|
|
1291
|
-
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",
|
|
1292
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 { }
|
|
1293
1301
|
}
|
|
1294
|
-
catch { }
|
|
1295
1302
|
}
|
|
1303
|
+
catch { }
|
|
1296
1304
|
}
|
|
1297
|
-
|
|
1305
|
+
return res;
|
|
1306
|
+
}
|
|
1307
|
+
catch (retryErr) {
|
|
1308
|
+
continue;
|
|
1298
1309
|
}
|
|
1299
|
-
return res;
|
|
1300
1310
|
}
|
|
1301
|
-
|
|
1302
|
-
|
|
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
|
+
});
|
|
1303
1326
|
}
|
|
1327
|
+
catch { }
|
|
1304
1328
|
}
|
|
1305
|
-
// Reportar falha após esgotar tentativas (v2.7.43)
|
|
1306
|
-
await switchToDefaultIfNeeded();
|
|
1307
|
-
try {
|
|
1308
|
-
await reportResult({
|
|
1309
|
-
logId,
|
|
1310
|
-
sessionId: TestContext.getOrCreateSessionId(),
|
|
1311
|
-
xpath: suggestedXPath,
|
|
1312
|
-
attemptNumber: maxRetries,
|
|
1313
|
-
success: false,
|
|
1314
|
-
recoverySource: "framework",
|
|
1315
|
-
executionSource: meta.executionSource,
|
|
1316
|
-
testCaseName: meta.testCaseName,
|
|
1317
|
-
testCaseId: meta.testCaseId,
|
|
1318
|
-
testMethodName: meta.testMethodName,
|
|
1319
|
-
});
|
|
1320
|
-
}
|
|
1321
|
-
catch { }
|
|
1322
1329
|
}
|
|
1323
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;
|
|
1324
1349
|
}
|
|
1325
|
-
catch (recoveryErr) {
|
|
1326
|
-
// silenciar falhas de recovery
|
|
1327
|
-
}
|
|
1328
|
-
// Enfileirar falha na RecoveryQueue para processamento assíncrono (Item 6)
|
|
1329
|
-
const failedSel = WebActions.extractSelector(originalElement);
|
|
1330
|
-
RecoveryQueue.enqueue(actionName, failedSel, error?.message || String(error), {
|
|
1331
|
-
url: TestContext.isPageInitialized()
|
|
1332
|
-
? TestContext.getPage().url()
|
|
1333
|
-
: "",
|
|
1334
|
-
sessionId: TestContext.getOrCreateSessionId(),
|
|
1335
|
-
value: valueForContext != null ? String(valueForContext) : undefined,
|
|
1336
|
-
});
|
|
1337
|
-
// Registrar falha na ActionTimeline (Item 7)
|
|
1338
|
-
ActionTimeline.record("recovery", actionName, "failure", 0, {
|
|
1339
|
-
selector: failedSel,
|
|
1340
|
-
error: error?.message || String(error),
|
|
1341
|
-
});
|
|
1342
|
-
throw error;
|
|
1343
1350
|
}
|
|
1344
1351
|
finally {
|
|
1345
|
-
// 🛡️
|
|
1346
|
-
WebActions.
|
|
1352
|
+
// 🛡️ Decrementar depth — o lock só zera quando a chamada mais externa termina.
|
|
1353
|
+
WebActions._withRecoveryDepth--;
|
|
1347
1354
|
}
|
|
1348
1355
|
}
|
|
1349
1356
|
// Helper genérico para tentar heurísticas de preenchimento/atribuição de valor
|
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.6",
|
|
5
5
|
"author": "Silas Martins Feliciano da Silva <silas.martins2041@gmail.com>",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"main": "dist/index.js",
|