@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
- /** 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,379 +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→... (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
987
- await WebActions.refreshDOM(150).catch(() => { });
988
- // 🎯 Catalog pre-resolve + lazy auto-refresh
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
- // 🛡️ 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 MIN_CONF = Number(process.env.AUTOCORE_RECOVERY_MIN_CONFIDENCE || "0.7");
1022
- const maxRetries = Number(process.env.AUTOCORE_RECOVERY_MAX_RETRIES || "8");
1023
- const meta = getRecoveryMetadata();
1024
- const pageUrl = TestContext.isPageInitialized()
1025
- ? TestContext.getPage().url()
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
- if (TestContext.isPageInitialized()) {
1053
- const page = TestContext.getPage();
1054
- // Capturar HTML (limitado a 50KB para performance)
1055
- pageHtml = await page.content().catch(() => "");
1056
- if (pageHtml.length > 50000)
1057
- pageHtml = pageHtml.substring(0, 50000) + "... (truncated)";
1058
- // Capturar estrutura básica
1059
- 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
+ }
1060
1064
  }
1061
- }
1062
- catch {
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
- catch { }
1079
- // Obter filledInputs/filledFields do singleton (v2.7.43)
1080
- let filledInputs = [];
1081
- let filledFields = [];
1082
- try {
1083
- const { testRecoveryHelper } = await import("../utils/testRecovery/TestRecoveryClient.js");
1084
- filledInputs = testRecoveryHelper.getFilledInputs();
1085
- filledFields = testRecoveryHelper.getFilledFields();
1086
- }
1087
- catch { }
1088
- // Obter metadados de página a partir do stack trace (item 20 v2.7.44)
1089
- const pageMeta = WebActions.getCallerPageMetadata();
1090
- const response = await analyzeFailure({
1091
- url: pageUrl || "",
1092
- failedXPath,
1093
- errorType: detectedErrorType,
1094
- actionType: normalizedActionType,
1095
- sessionId: TestContext.getOrCreateSessionId(),
1096
- executionSource: meta.executionSource,
1097
- testCaseName: meta.testCaseName,
1098
- testCaseId: meta.testCaseId,
1099
- testMethodName: meta.testMethodName,
1100
- context: {
1101
- text: elementText || undefined,
1102
- value: valueForContext != null ? String(valueForContext) : undefined,
1103
- originalMethod: actionName,
1104
- filledInputs,
1105
- filledFields,
1106
- pageFileName: pageMeta.pageFileName,
1107
- pageName: pageMeta.pageName,
1108
- pageObjectName: pageMeta.pageObjectName,
1109
- },
1110
- pageHtml: pageHtml || undefined,
1111
- pageStructure,
1112
- });
1113
- if (response && response.analysis && response.analysis.suggestedXPath) {
1114
- const { suggestedXPath, confidence } = response.analysis;
1115
- if (confidence >= MIN_CONF) {
1116
- const alternatives = response.analysis.alternatives || [];
1117
- const candidates = [];
1118
- if (suggestedXPath)
1119
- candidates.push(suggestedXPath);
1120
- if (Array.isArray(alternatives) && alternatives.length)
1121
- candidates.push(...alternatives);
1122
- // executar actions sugeridas antes das tentativas (v2.7.43)
1123
- let _inRecoveryFrame = false;
1124
- if (response.actions && response.actions.length) {
1125
- for (const a of response.actions) {
1126
- try {
1127
- switch (a.type) {
1128
- case "wait":
1129
- if (TestContext.isPageInitialized())
1130
- await TestContext.getPage().waitForTimeout(a.waitMs || 1000);
1131
- break;
1132
- case "click_other":
1133
- try {
1134
- await WebActions.click(a.selector || "", "", false);
1135
- }
1136
- catch { }
1137
- break;
1138
- case "navigate":
1139
- try {
1140
- if (a.url)
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().reload();
1149
- }
1150
- catch { }
1151
- break;
1152
- case "switch_frame":
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
- catch { }
1160
- break;
1161
- default:
1162
- break;
1163
- }
1164
- }
1165
- catch { }
1166
- }
1167
- }
1168
- const switchToDefaultIfNeeded = async () => {
1169
- if (!_inRecoveryFrame)
1170
- return;
1171
- try {
1172
- if (typeof WebActions.switchToDefault === "function") {
1173
- await WebActions.switchToDefault("Recovery frame switch back", false);
1174
- }
1175
- }
1176
- catch { }
1177
- _inRecoveryFrame = false;
1178
- };
1179
- // Ordem recomendada (v2.7.43):
1180
- // 1. suggestedXPath → 2. alternatives[] → report-result → auto-fix-local
1181
- const logId = response.logId;
1182
- for (let attempt = 2; attempt <= Math.max(2, maxRetries); attempt++) {
1183
- const candidateIndex = Math.min(attempt - 2, candidates.length - 1);
1184
- const candidate = candidates.length
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
- const resolvedCandidate = await WebActions.resolveCandidateForRecovery(candidate);
1206
- const res = await fn(resolvedCandidate);
1207
- await switchToDefaultIfNeeded();
1170
+ }
1171
+ const switchToDefaultIfNeeded = async () => {
1172
+ if (!_inRecoveryFrame)
1173
+ return;
1208
1174
  try {
1209
- await markSuccess(TestContext.getOrCreateSessionId(), attempt);
1175
+ if (typeof WebActions.switchToDefault === "function") {
1176
+ await WebActions.switchToDefault("Recovery frame switch back", false);
1177
+ }
1210
1178
  }
1211
1179
  catch { }
1212
- // Registrar recovery bem-sucedido na timeline (Item 7)
1213
- ActionTimeline.record("recovery", actionName, "retry", 0, {
1214
- selector: candidate,
1215
- metadata: { attempt },
1216
- });
1217
- // 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;
1218
1192
  try {
1219
- await reportResult({
1220
- logId,
1221
- sessionId: TestContext.getOrCreateSessionId(),
1222
- xpath: candidate,
1223
- attemptNumber: attempt,
1224
- success: true,
1225
- autoFixed: false,
1226
- recoverySource: "framework",
1227
- executionSource: meta.executionSource,
1228
- testCaseName: meta.testCaseName,
1229
- testCaseId: meta.testCaseId,
1230
- 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 },
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
- const { testRecoveryHelper } = await import("../utils/testRecovery/TestRecoveryClient.js");
1239
- const projRoot = testRecoveryHelper.getProjectRoot() ||
1240
- process.env.AUTOCORE_PROJECT_ROOT ||
1241
- process.cwd();
1242
- // Enriquecer auto-fix com contexto de Attributes/Page (item 7/12 v2.7.44)
1243
- const attrCtx = WebActions.getCallerAttributesContext();
1244
- const resolvedMethodName = attrCtx.methodName || meta.testMethodName;
1245
- let autoFixConfirmed = false;
1246
- // 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) {
1247
1240
  try {
1248
- const { fixXPathViaLocalMcp } = await import("../utils/McpLocalClient.js");
1249
- const mcpFix = await fixXPathViaLocalMcp({
1250
- projectRoot: projRoot,
1251
- oldXPath: failedXPath,
1252
- newXPath: candidate,
1253
- confidence,
1254
- className: attrCtx.className,
1255
- attributesFile: attrCtx.attributesFile,
1256
- methodName: resolvedMethodName,
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 reportResult({
1284
- logId,
1285
- sessionId: TestContext.getOrCreateSessionId(),
1286
- xpath: candidate,
1287
- attemptNumber: attempt,
1288
- success: true,
1289
- autoFixed: true,
1290
- recoverySource: "framework",
1291
- executionSource: meta.executionSource,
1292
- testCaseName: meta.testCaseName,
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
- catch { }
1305
+ return res;
1306
+ }
1307
+ catch (retryErr) {
1308
+ continue;
1301
1309
  }
1302
- return res;
1303
1310
  }
1304
- catch (retryErr) {
1305
- 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
+ });
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
- // 🛡️ resetar o lock se ESTE withRecovery foi quem ativou.
1349
- // Se já estava em recovery antes (wasAlreadyInRecovery), não tocar no lock.
1350
- if (!wasAlreadyInRecovery) {
1351
- WebActions._inRecovery = false;
1352
- }
1352
+ // 🛡️ Decrementar depth o lock 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
- const exclude = new Set([
8
- 'withRecovery',
9
- 'extractSelector',
10
- 'detectSelectorType',
11
- 'getLocatorInContext',
12
- 'isInFrame',
13
- 'getCurrentFrameSelector',
14
- 'resolveWaitVisibilityOptions',
15
- 'getCallerMethodName',
16
- 'interceptForAutoDocs',
17
- 'updateAutoDocsStatus',
18
- 'getPage',
19
- 'ensurePageInitialized',
20
- 'logToUnified',
21
- 'addActionMetadata',
22
- 'prototype',
23
- 'length',
24
- 'name'
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 staticNames = Object.getOwnPropertyNames(WebActions);
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 (!orig || orig.__recovery_wrapped)
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.5",
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",