@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
- /** Lock global: quando true, withRecovery executa fn direto sem entrar em recovery */
176
- private static _inRecovery;
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
- /** Lock global: quando true, withRecovery executa fn direto sem entrar em recovery */
877
- static _inRecovery = false;
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
- // 🛡️ Guard: se estiver em recovery, executar fn direto sem novo ciclo.
981
- // Impede a cadeia click→recovery→click_other→click→recovery→click_other→... (stack overflow)
982
- if (WebActions._inRecovery) {
983
- const initialCandidate = await WebActions.resolveCandidateForRecovery(originalElement);
984
- return await fn(initialCandidate);
985
- }
986
- // 🔄 Refresh DOM antes da primeira tentativa garantir DOM fresco para SPAs/Shadow DOM
987
- await WebActions.refreshDOM(150).catch(() => { });
988
- // 🎯 Catalog pre-resolve + lazy auto-refresh (Item 18/20 v2.7.44)
989
- if (typeof originalElement === "string") {
990
- try {
991
- WebActions.ensureCatalogExtractor();
992
- let resolved = WebActions.resolveXPathFromCatalog(originalElement);
993
- if (!resolved && TestContext.isPageInitialized()) {
994
- const pageUrl = TestContext.getPage().url();
995
- const urlKey = pageUrl.split("?")[0].split("#")[0].toLowerCase();
996
- if (!WebActions._catalogPopulatedPages.has(urlKey)) {
997
- WebActions._catalogPopulatedPages.add(urlKey);
998
- await XPathCatalog.refresh(pageUrl).catch(() => { });
999
- resolved = WebActions.resolveXPathFromCatalog(originalElement);
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
- if (resolved)
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 MIN_CONF = Number(process.env.AUTOCORE_RECOVERY_MIN_CONFIDENCE || "0.7");
1019
- const maxRetries = Number(process.env.AUTOCORE_RECOVERY_MAX_RETRIES || "8");
1020
- const meta = getRecoveryMetadata();
1021
- const pageUrl = TestContext.isPageInitialized()
1022
- ? TestContext.getPage().url()
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
- if (TestContext.isPageInitialized()) {
1050
- const page = TestContext.getPage();
1051
- // Capturar HTML (limitado a 50KB para performance)
1052
- pageHtml = await page.content().catch(() => "");
1053
- if (pageHtml.length > 50000)
1054
- pageHtml = pageHtml.substring(0, 50000) + "... (truncated)";
1055
- // Capturar estrutura básica
1056
- pageStructure = await WebActions.extractBasicPageStructure(page).catch(() => undefined);
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
- catch {
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
- catch { }
1076
- // Obter filledInputs/filledFields do singleton (v2.7.43)
1077
- let filledInputs = [];
1078
- let filledFields = [];
1079
- try {
1080
- const { testRecoveryHelper } = await import("../utils/testRecovery/TestRecoveryClient.js");
1081
- filledInputs = testRecoveryHelper.getFilledInputs();
1082
- filledFields = testRecoveryHelper.getFilledFields();
1083
- }
1084
- catch { }
1085
- // Obter metadados de página a partir do stack trace (item 20 v2.7.44)
1086
- const pageMeta = WebActions.getCallerPageMetadata();
1087
- const response = await analyzeFailure({
1088
- url: pageUrl || "",
1089
- failedXPath,
1090
- errorType: detectedErrorType,
1091
- actionType: normalizedActionType,
1092
- sessionId: TestContext.getOrCreateSessionId(),
1093
- executionSource: meta.executionSource,
1094
- testCaseName: meta.testCaseName,
1095
- testCaseId: meta.testCaseId,
1096
- testMethodName: meta.testMethodName,
1097
- context: {
1098
- text: elementText || undefined,
1099
- value: valueForContext != null ? String(valueForContext) : undefined,
1100
- originalMethod: actionName,
1101
- filledInputs,
1102
- filledFields,
1103
- pageFileName: pageMeta.pageFileName,
1104
- pageName: pageMeta.pageName,
1105
- pageObjectName: pageMeta.pageObjectName,
1106
- },
1107
- pageHtml: pageHtml || undefined,
1108
- pageStructure,
1109
- });
1110
- if (response && response.analysis && response.analysis.suggestedXPath) {
1111
- const { suggestedXPath, confidence } = response.analysis;
1112
- if (confidence >= MIN_CONF) {
1113
- const alternatives = response.analysis.alternatives || [];
1114
- const candidates = [];
1115
- if (suggestedXPath)
1116
- candidates.push(suggestedXPath);
1117
- if (Array.isArray(alternatives) && alternatives.length)
1118
- candidates.push(...alternatives);
1119
- // executar actions sugeridas antes das tentativas (v2.7.43)
1120
- let _inRecoveryFrame = false;
1121
- if (response.actions && response.actions.length) {
1122
- for (const a of response.actions) {
1123
- try {
1124
- switch (a.type) {
1125
- case "wait":
1126
- if (TestContext.isPageInitialized())
1127
- await TestContext.getPage().waitForTimeout(a.waitMs || 1000);
1128
- break;
1129
- case "click_other":
1130
- try {
1131
- await WebActions.click(a.selector || "", "", false);
1132
- }
1133
- catch { }
1134
- break;
1135
- case "navigate":
1136
- try {
1137
- if (a.url)
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().reload();
1146
- }
1147
- catch { }
1148
- break;
1149
- case "switch_frame":
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
- catch { }
1157
- break;
1158
- default:
1159
- break;
1160
- }
1161
- }
1162
- catch { }
1163
- }
1164
- }
1165
- const switchToDefaultIfNeeded = async () => {
1166
- if (!_inRecoveryFrame)
1167
- return;
1168
- try {
1169
- if (typeof WebActions.switchToDefault === "function") {
1170
- await WebActions.switchToDefault("Recovery frame switch back", false);
1171
- }
1172
- }
1173
- catch { }
1174
- _inRecoveryFrame = false;
1175
- };
1176
- // Ordem recomendada (v2.7.43):
1177
- // 1. suggestedXPath → 2. alternatives[] → report-result → auto-fix-local
1178
- const logId = response.logId;
1179
- for (let attempt = 2; attempt <= Math.max(2, maxRetries); attempt++) {
1180
- const candidateIndex = Math.min(attempt - 2, candidates.length - 1);
1181
- const candidate = candidates.length
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
- const resolvedCandidate = await WebActions.resolveCandidateForRecovery(candidate);
1203
- const res = await fn(resolvedCandidate);
1204
- await switchToDefaultIfNeeded();
1170
+ }
1171
+ const switchToDefaultIfNeeded = async () => {
1172
+ if (!_inRecoveryFrame)
1173
+ return;
1205
1174
  try {
1206
- await markSuccess(TestContext.getOrCreateSessionId(), attempt);
1175
+ if (typeof WebActions.switchToDefault === "function") {
1176
+ await WebActions.switchToDefault("Recovery frame switch back", false);
1177
+ }
1207
1178
  }
1208
1179
  catch { }
1209
- // Registrar recovery bem-sucedido na timeline (Item 7)
1210
- ActionTimeline.record("recovery", actionName, "retry", 0, {
1211
- selector: candidate,
1212
- metadata: { attempt },
1213
- });
1214
- // Report success (v2.7.43)
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
- await reportResult({
1217
- logId,
1218
- sessionId: TestContext.getOrCreateSessionId(),
1219
- xpath: candidate,
1220
- attemptNumber: attempt,
1221
- success: true,
1222
- autoFixed: false,
1223
- recoverySource: "framework",
1224
- executionSource: meta.executionSource,
1225
- testCaseName: meta.testCaseName,
1226
- testCaseId: meta.testCaseId,
1227
- testMethodName: meta.testMethodName,
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
- const { testRecoveryHelper } = await import("../utils/testRecovery/TestRecoveryClient.js");
1236
- const projRoot = testRecoveryHelper.getProjectRoot() ||
1237
- process.env.AUTOCORE_PROJECT_ROOT ||
1238
- process.cwd();
1239
- // Enriquecer auto-fix com contexto de Attributes/Page (item 7/12 v2.7.44)
1240
- const attrCtx = WebActions.getCallerAttributesContext();
1241
- const resolvedMethodName = attrCtx.methodName || meta.testMethodName;
1242
- let autoFixConfirmed = false;
1243
- // 1) MCP local-first (preferencial)
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 { fixXPathViaLocalMcp } = await import("../utils/McpLocalClient.js");
1246
- const mcpFix = await fixXPathViaLocalMcp({
1247
- projectRoot: projRoot,
1248
- oldXPath: failedXPath,
1249
- newXPath: candidate,
1250
- confidence,
1251
- className: attrCtx.className,
1252
- attributesFile: attrCtx.attributesFile,
1253
- methodName: resolvedMethodName,
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 reportResult({
1281
- logId,
1282
- sessionId: TestContext.getOrCreateSessionId(),
1283
- xpath: candidate,
1284
- attemptNumber: attempt,
1285
- success: true,
1286
- autoFixed: true,
1287
- recoverySource: "framework",
1288
- executionSource: meta.executionSource,
1289
- testCaseName: meta.testCaseName,
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
- catch { }
1305
+ return res;
1306
+ }
1307
+ catch (retryErr) {
1308
+ continue;
1298
1309
  }
1299
- return res;
1300
1310
  }
1301
- catch (retryErr) {
1302
- continue;
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
- // 🛡️ Liberar lock — só quem ativou o lock pode desativar.
1346
- WebActions._inRecovery = false;
1352
+ // 🛡️ Decrementar deptho lock 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",
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",