@silasfmartins/testhub 1.0.0

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.
Files changed (296) hide show
  1. package/.github/copilot-instructions.md +520 -0
  2. package/biome.json +37 -0
  3. package/dist/index.d.ts +45 -0
  4. package/dist/index.js +169 -0
  5. package/dist/scripts/consumer-postinstall.d.ts +15 -0
  6. package/dist/scripts/consumer-postinstall.js +785 -0
  7. package/dist/scripts/generate-docs.d.ts +16 -0
  8. package/dist/scripts/generate-docs.js +1363 -0
  9. package/dist/scripts/generate-index.d.ts +2 -0
  10. package/dist/scripts/generate-index.js +314 -0
  11. package/dist/scripts/init-api.d.ts +2 -0
  12. package/dist/scripts/init-api.js +525 -0
  13. package/dist/scripts/init-banco.d.ts +2 -0
  14. package/dist/scripts/init-banco.js +347 -0
  15. package/dist/scripts/init-frontend.d.ts +2 -0
  16. package/dist/scripts/init-frontend.js +627 -0
  17. package/dist/scripts/init-mobile.d.ts +2 -0
  18. package/dist/scripts/init-mobile.js +481 -0
  19. package/dist/scripts/init-scenarios.d.ts +2 -0
  20. package/dist/scripts/init-scenarios.js +846 -0
  21. package/dist/scripts/init-ssh.d.ts +2 -0
  22. package/dist/scripts/init-ssh.js +639 -0
  23. package/dist/scripts/package-versions.d.ts +57 -0
  24. package/dist/scripts/package-versions.js +768 -0
  25. package/dist/scripts/postinstall.d.ts +1 -0
  26. package/dist/scripts/postinstall.js +527 -0
  27. package/dist/scripts/robust-build.d.ts +7 -0
  28. package/dist/scripts/robust-build.js +88 -0
  29. package/dist/scripts/setup-local-packages.d.ts +31 -0
  30. package/dist/scripts/setup-local-packages.js +237 -0
  31. package/dist/scripts/smart-override.d.ts +2 -0
  32. package/dist/scripts/smart-override.js +1360 -0
  33. package/dist/scripts/sync-configs.d.ts +27 -0
  34. package/dist/scripts/sync-configs.js +248 -0
  35. package/dist/scripts/test-biome-parse.d.ts +5 -0
  36. package/dist/scripts/test-biome-parse.js +84 -0
  37. package/dist/scripts/ultracite-setup.d.ts +4 -0
  38. package/dist/scripts/ultracite-setup.js +310 -0
  39. package/dist/scripts/update-all-init-scripts.d.ts +2 -0
  40. package/dist/scripts/update-all-init-scripts.js +52 -0
  41. package/dist/scripts/update-biome-schema.d.ts +15 -0
  42. package/dist/scripts/update-biome-schema.js +124 -0
  43. package/dist/src/AutoCoreFacade.d.ts +145 -0
  44. package/dist/src/AutoCoreFacade.js +217 -0
  45. package/dist/src/api/ApiActions.d.ts +297 -0
  46. package/dist/src/api/ApiActions.js +1905 -0
  47. package/dist/src/api/Certificate.d.ts +60 -0
  48. package/dist/src/api/Certificate.js +79 -0
  49. package/dist/src/api/JsonResponse.d.ts +116 -0
  50. package/dist/src/api/JsonResponse.js +206 -0
  51. package/dist/src/appium/DeviceFarmViewer.d.ts +79 -0
  52. package/dist/src/appium/DeviceFarmViewer.js +1083 -0
  53. package/dist/src/appium/MobileActions.d.ts +347 -0
  54. package/dist/src/appium/MobileActions.js +1632 -0
  55. package/dist/src/appium/MobileConnection.d.ts +160 -0
  56. package/dist/src/appium/MobileConnection.js +772 -0
  57. package/dist/src/config/envLoader.d.ts +123 -0
  58. package/dist/src/config/envLoader.js +361 -0
  59. package/dist/src/config/jest-safe-setup.d.ts +19 -0
  60. package/dist/src/config/jest-safe-setup.js +369 -0
  61. package/dist/src/config/timeouts.d.ts +32 -0
  62. package/dist/src/config/timeouts.js +38 -0
  63. package/dist/src/desktop/DesktopActions.d.ts +46 -0
  64. package/dist/src/desktop/DesktopActions.js +398 -0
  65. package/dist/src/desktop/DesktopConnection.d.ts +32 -0
  66. package/dist/src/desktop/DesktopConnection.js +84 -0
  67. package/dist/src/domain/entities/TestExecution.d.ts +117 -0
  68. package/dist/src/domain/entities/TestExecution.js +150 -0
  69. package/dist/src/domain/entities/TestReport.d.ts +114 -0
  70. package/dist/src/domain/entities/TestReport.js +179 -0
  71. package/dist/src/domain/repositories/ITestRepository.d.ts +196 -0
  72. package/dist/src/domain/repositories/ITestRepository.js +14 -0
  73. package/dist/src/domain/schemas/ValidationSchemas.d.ts +159 -0
  74. package/dist/src/domain/schemas/ValidationSchemas.js +181 -0
  75. package/dist/src/functions/errors/BaseError.d.ts +78 -0
  76. package/dist/src/functions/errors/BaseError.js +245 -0
  77. package/dist/src/functions/errors/ConfigurationError.d.ts +16 -0
  78. package/dist/src/functions/errors/ConfigurationError.js +48 -0
  79. package/dist/src/functions/errors/ErrorCatalog.d.ts +148 -0
  80. package/dist/src/functions/errors/ErrorCatalog.js +157 -0
  81. package/dist/src/functions/errors/GlobalErrorHandler.d.ts +101 -0
  82. package/dist/src/functions/errors/GlobalErrorHandler.js +281 -0
  83. package/dist/src/functions/errors/IntegrationError.d.ts +17 -0
  84. package/dist/src/functions/errors/IntegrationError.js +51 -0
  85. package/dist/src/functions/errors/SecurityError.d.ts +14 -0
  86. package/dist/src/functions/errors/SecurityError.js +42 -0
  87. package/dist/src/functions/errors/SystemError.d.ts +12 -0
  88. package/dist/src/functions/errors/SystemError.js +36 -0
  89. package/dist/src/functions/errors/ValidationError.d.ts +14 -0
  90. package/dist/src/functions/errors/ValidationError.js +61 -0
  91. package/dist/src/functions/errors/index.d.ts +12 -0
  92. package/dist/src/functions/errors/index.js +13 -0
  93. package/dist/src/global-setup.d.ts +1 -0
  94. package/dist/src/global-setup.js +1037 -0
  95. package/dist/src/helpers/BancoActions.d.ts +188 -0
  96. package/dist/src/helpers/BancoActions.js +581 -0
  97. package/dist/src/helpers/EnviromentHelper.d.ts +17 -0
  98. package/dist/src/helpers/EnviromentHelper.js +66 -0
  99. package/dist/src/helpers/ParallelExecutionHelper.d.ts +183 -0
  100. package/dist/src/helpers/ParallelExecutionHelper.js +375 -0
  101. package/dist/src/helpers/SyncSignal.d.ts +15 -0
  102. package/dist/src/helpers/SyncSignal.js +44 -0
  103. package/dist/src/hubdocs/CategoryDetector.d.ts +83 -0
  104. package/dist/src/hubdocs/CategoryDetector.js +401 -0
  105. package/dist/src/hubdocs/DirectStatementInterceptor.d.ts +54 -0
  106. package/dist/src/hubdocs/DirectStatementInterceptor.js +243 -0
  107. package/dist/src/hubdocs/ExecutionTracker.d.ts +107 -0
  108. package/dist/src/hubdocs/ExecutionTracker.js +702 -0
  109. package/dist/src/hubdocs/HubDocs.d.ts +395 -0
  110. package/dist/src/hubdocs/HubDocs.js +3586 -0
  111. package/dist/src/hubdocs/StatementMethodFilter.d.ts +71 -0
  112. package/dist/src/hubdocs/StatementMethodFilter.js +618 -0
  113. package/dist/src/hubdocs/StatementTracker.d.ts +417 -0
  114. package/dist/src/hubdocs/StatementTracker.js +2419 -0
  115. package/dist/src/hubdocs/SwaggerGenerator.d.ts +59 -0
  116. package/dist/src/hubdocs/SwaggerGenerator.js +405 -0
  117. package/dist/src/hubdocs/index.d.ts +9 -0
  118. package/dist/src/hubdocs/index.js +9 -0
  119. package/dist/src/hubdocs/types.d.ts +114 -0
  120. package/dist/src/hubdocs/types.js +5 -0
  121. package/dist/src/infrastructure/DependencyContainer.d.ts +142 -0
  122. package/dist/src/infrastructure/DependencyContainer.js +250 -0
  123. package/dist/src/infrastructure/adapters/AppiumAdapter.d.ts +168 -0
  124. package/dist/src/infrastructure/adapters/AppiumAdapter.js +468 -0
  125. package/dist/src/infrastructure/adapters/OracleAdapter.d.ts +150 -0
  126. package/dist/src/infrastructure/adapters/OracleAdapter.js +388 -0
  127. package/dist/src/infrastructure/adapters/PlaywrightAdapter.d.ts +192 -0
  128. package/dist/src/infrastructure/adapters/PlaywrightAdapter.js +382 -0
  129. package/dist/src/infrastructure/adapters/SSHAdapter.d.ts +141 -0
  130. package/dist/src/infrastructure/adapters/SSHAdapter.js +428 -0
  131. package/dist/src/interfaces.d.ts +501 -0
  132. package/dist/src/interfaces.js +25 -0
  133. package/dist/src/internal/fakes/__fake-actions__.d.ts +17 -0
  134. package/dist/src/internal/fakes/__fake-actions__.js +21 -0
  135. package/dist/src/internal/fakes/__forbidden__.d.ts +10 -0
  136. package/dist/src/internal/fakes/__forbidden__.js +18 -0
  137. package/dist/src/internal/fakes/__honeypot__.d.ts +15 -0
  138. package/dist/src/internal/fakes/__honeypot__.js +24 -0
  139. package/dist/src/octane/OctaneReporter.d.ts +13 -0
  140. package/dist/src/octane/OctaneReporter.js +61 -0
  141. package/dist/src/playwright/CryptoActions.d.ts +20 -0
  142. package/dist/src/playwright/CryptoActions.js +75 -0
  143. package/dist/src/playwright/EnhancedWebActions.d.ts +7 -0
  144. package/dist/src/playwright/EnhancedWebActions.js +65 -0
  145. package/dist/src/playwright/WebActions.d.ts +1599 -0
  146. package/dist/src/playwright/WebActions.js +11788 -0
  147. package/dist/src/playwright/actions/ActionTimeline.d.ts +36 -0
  148. package/dist/src/playwright/actions/ActionTimeline.js +101 -0
  149. package/dist/src/playwright/actions/RecoveryQueue.d.ts +82 -0
  150. package/dist/src/playwright/actions/RecoveryQueue.js +130 -0
  151. package/dist/src/playwright/actions/SelectorCache.d.ts +53 -0
  152. package/dist/src/playwright/actions/SelectorCache.js +96 -0
  153. package/dist/src/playwright/actions/index.d.ts +13 -0
  154. package/dist/src/playwright/actions/index.js +14 -0
  155. package/dist/src/playwright/actions/types.d.ts +147 -0
  156. package/dist/src/playwright/actions/types.js +5 -0
  157. package/dist/src/playwright/fixtures.d.ts +112 -0
  158. package/dist/src/playwright/fixtures.js +718 -0
  159. package/dist/src/playwright/network-logs-reporter.d.ts +7 -0
  160. package/dist/src/playwright/network-logs-reporter.js +66 -0
  161. package/dist/src/playwright/registerRecoveryWrappers.d.ts +1 -0
  162. package/dist/src/playwright/registerRecoveryWrappers.js +54 -0
  163. package/dist/src/security/BuildSecurity.d.ts +12 -0
  164. package/dist/src/security/BuildSecurity.js +138 -0
  165. package/dist/src/security/EulaProtection.d.ts +70 -0
  166. package/dist/src/security/EulaProtection.js +155 -0
  167. package/dist/src/security/HoneypotManager.d.ts +46 -0
  168. package/dist/src/security/HoneypotManager.js +234 -0
  169. package/dist/src/security/KeysManager.d.ts +36 -0
  170. package/dist/src/security/KeysManager.js +158 -0
  171. package/dist/src/security/ProofOfWorkIntegration.d.ts +64 -0
  172. package/dist/src/security/ProofOfWorkIntegration.js +206 -0
  173. package/dist/src/security/SecurityValidation.d.ts +21 -0
  174. package/dist/src/security/SecurityValidation.js +163 -0
  175. package/dist/src/security/SourceMapProtection.d.ts +55 -0
  176. package/dist/src/security/SourceMapProtection.js +220 -0
  177. package/dist/src/security/protector.d.ts +1 -0
  178. package/dist/src/security/protector.js +97 -0
  179. package/dist/src/ssh/SSHActions.d.ts +262 -0
  180. package/dist/src/ssh/SSHActions.js +790 -0
  181. package/dist/src/ssh/SSHClient.d.ts +99 -0
  182. package/dist/src/ssh/SSHClient.js +409 -0
  183. package/dist/src/statements/BaseStatement.d.ts +38 -0
  184. package/dist/src/statements/BaseStatement.js +78 -0
  185. package/dist/src/testContext/AuthStateManager.d.ts +93 -0
  186. package/dist/src/testContext/AuthStateManager.js +256 -0
  187. package/dist/src/testContext/CoverageManager.d.ts +198 -0
  188. package/dist/src/testContext/CoverageManager.js +917 -0
  189. package/dist/src/testContext/TestAnnotations.d.ts +476 -0
  190. package/dist/src/testContext/TestAnnotations.js +2647 -0
  191. package/dist/src/testContext/TestContext.d.ts +138 -0
  192. package/dist/src/testContext/TestContext.js +369 -0
  193. package/dist/src/testContext/UnifiedHtmlGenerator.d.ts +7 -0
  194. package/dist/src/testContext/UnifiedHtmlGenerator.js +264 -0
  195. package/dist/src/testContext/UnifiedReportManager.d.ts +211 -0
  196. package/dist/src/testContext/UnifiedReportManager.js +1206 -0
  197. package/dist/src/testhub/DynamicConfigManager.d.ts +121 -0
  198. package/dist/src/testhub/DynamicConfigManager.js +320 -0
  199. package/dist/src/testhub/SystemsManager.d.ts +119 -0
  200. package/dist/src/testhub/SystemsManager.js +365 -0
  201. package/dist/src/testhub/TestHubClient.d.ts +335 -0
  202. package/dist/src/testhub/TestHubClient.js +1215 -0
  203. package/dist/src/testhub/TestHubReporter.d.ts +62 -0
  204. package/dist/src/testhub/TestHubReporter.js +576 -0
  205. package/dist/src/testhub/TestHubVars.d.ts +116 -0
  206. package/dist/src/testhub/TestHubVars.js +273 -0
  207. package/dist/src/utils/ActionInterceptor.d.ts +59 -0
  208. package/dist/src/utils/ActionInterceptor.js +741 -0
  209. package/dist/src/utils/ArtifactsCompressor.d.ts +43 -0
  210. package/dist/src/utils/ArtifactsCompressor.js +181 -0
  211. package/dist/src/utils/AutoLogsFinal.d.ts +47 -0
  212. package/dist/src/utils/AutoLogsFinal.js +148 -0
  213. package/dist/src/utils/CodeGenSession.d.ts +114 -0
  214. package/dist/src/utils/CodeGenSession.js +264 -0
  215. package/dist/src/utils/ConfigLogger.d.ts +133 -0
  216. package/dist/src/utils/ConfigLogger.js +611 -0
  217. package/dist/src/utils/CustomReporter.d.ts +22 -0
  218. package/dist/src/utils/CustomReporter.js +352 -0
  219. package/dist/src/utils/DataStore.d.ts +171 -0
  220. package/dist/src/utils/DataStore.js +484 -0
  221. package/dist/src/utils/DatabaseInterceptor.d.ts +19 -0
  222. package/dist/src/utils/DatabaseInterceptor.js +295 -0
  223. package/dist/src/utils/DateHelper.d.ts +16 -0
  224. package/dist/src/utils/DateHelper.js +120 -0
  225. package/dist/src/utils/DateValidator.d.ts +4 -0
  226. package/dist/src/utils/DateValidator.js +51 -0
  227. package/dist/src/utils/DocumentGenerator.d.ts +35 -0
  228. package/dist/src/utils/DocumentGenerator.js +129 -0
  229. package/dist/src/utils/EvidenceCapture.d.ts +90 -0
  230. package/dist/src/utils/EvidenceCapture.js +600 -0
  231. package/dist/src/utils/EvidenceReportGenerator.d.ts +70 -0
  232. package/dist/src/utils/EvidenceReportGenerator.js +799 -0
  233. package/dist/src/utils/FrameManagementUtil.d.ts +42 -0
  234. package/dist/src/utils/FrameManagementUtil.js +75 -0
  235. package/dist/src/utils/GlobalStatementsInterceptor.d.ts +1 -0
  236. package/dist/src/utils/GlobalStatementsInterceptor.js +1 -0
  237. package/dist/src/utils/HTMLTemplate.d.ts +1 -0
  238. package/dist/src/utils/HTMLTemplate.js +1034 -0
  239. package/dist/src/utils/InterceptacaoMagica.d.ts +23 -0
  240. package/dist/src/utils/InterceptacaoMagica.js +365 -0
  241. package/dist/src/utils/LogSanitizer.d.ts +35 -0
  242. package/dist/src/utils/LogSanitizer.js +110 -0
  243. package/dist/src/utils/Logger.d.ts +65 -0
  244. package/dist/src/utils/Logger.js +284 -0
  245. package/dist/src/utils/McpLocalClient.d.ts +141 -0
  246. package/dist/src/utils/McpLocalClient.js +871 -0
  247. package/dist/src/utils/PDFEvidenceGenerator.d.ts +20 -0
  248. package/dist/src/utils/PDFEvidenceGenerator.js +156 -0
  249. package/dist/src/utils/SpecFileAnalyzer.d.ts +35 -0
  250. package/dist/src/utils/SpecFileAnalyzer.js +209 -0
  251. package/dist/src/utils/StatementInterceptor.d.ts +18 -0
  252. package/dist/src/utils/StatementInterceptor.js +87 -0
  253. package/dist/src/utils/StatementLogger.d.ts +33 -0
  254. package/dist/src/utils/StatementLogger.js +113 -0
  255. package/dist/src/utils/StatementsInterceptor.d.ts +1 -0
  256. package/dist/src/utils/StatementsInterceptor.js +1 -0
  257. package/dist/src/utils/TeamsFlushHook.d.ts +17 -0
  258. package/dist/src/utils/TeamsFlushHook.js +168 -0
  259. package/dist/src/utils/TerminalLogCapture.d.ts +158 -0
  260. package/dist/src/utils/TerminalLogCapture.js +531 -0
  261. package/dist/src/utils/TestMethodLogger.d.ts +70 -0
  262. package/dist/src/utils/TestMethodLogger.js +95 -0
  263. package/dist/src/utils/UnifiedTeardown.d.ts +4 -0
  264. package/dist/src/utils/UnifiedTeardown.js +400 -0
  265. package/dist/src/utils/XPathCatalog.d.ts +152 -0
  266. package/dist/src/utils/XPathCatalog.js +350 -0
  267. package/dist/src/utils/generators.d.ts +90 -0
  268. package/dist/src/utils/generators.js +167 -0
  269. package/dist/src/utils/testRecovery/ResilientPlaywright.d.ts +152 -0
  270. package/dist/src/utils/testRecovery/ResilientPlaywright.js +715 -0
  271. package/dist/src/utils/testRecovery/TestRecoveryClient.d.ts +801 -0
  272. package/dist/src/utils/testRecovery/TestRecoveryClient.js +1415 -0
  273. package/dist/src/utils/testRecovery/autoFixCode.d.ts +65 -0
  274. package/dist/src/utils/testRecovery/autoFixCode.js +32 -0
  275. package/dist/vitest.config.d.ts +2 -0
  276. package/dist/vitest.config.js +59 -0
  277. package/dist/wdio.conf.d.ts +1 -0
  278. package/dist/wdio.conf.js +420 -0
  279. package/package.json +137 -0
  280. package/protect-loader.mjs +643 -0
  281. package/scripts/consumer-postinstall.ts +975 -0
  282. package/scripts/generate-index.ts +343 -0
  283. package/scripts/init-api.ts +613 -0
  284. package/scripts/init-banco.ts +437 -0
  285. package/scripts/init-frontend.ts +727 -0
  286. package/scripts/init-mobile.ts +558 -0
  287. package/scripts/init-scenarios.ts +925 -0
  288. package/scripts/init-ssh.ts +734 -0
  289. package/scripts/package-versions.ts +978 -0
  290. package/scripts/postinstall.ts +605 -0
  291. package/scripts/smart-override.ts +1675 -0
  292. package/scripts/sync-configs.ts +302 -0
  293. package/scripts/ultracite-setup.ts +370 -0
  294. package/src/types/globals.d.ts +48 -0
  295. package/tsconfig.json +29 -0
  296. package/types/autocore-sync-signal.d.ts +10 -0
@@ -0,0 +1,3586 @@
1
+ /**
2
+ * 📚 HubDocs - Sistema Principal de Documentação Automática (Versão Funcional)
3
+ * @silasfmartins/testhub - Auto Documentation System v1.1.51
4
+ */
5
+ import fs from 'node:fs';
6
+ import path from 'node:path';
7
+ import * as yaml from 'js-yaml';
8
+ import { ExecutionTracker } from './ExecutionTracker.js';
9
+ import { StatementTracker } from './StatementTracker.js';
10
+ import { Logger } from '../utils/Logger.js';
11
+ export class AutoDocs {
12
+ static initialized = false;
13
+ static finalizationExecuted = false;
14
+ static config = {
15
+ enabled: true,
16
+ categories: ['api', 'ssh', 'db', 'ui', 'mobile'],
17
+ outputDir: './docs',
18
+ includeRequestBody: true,
19
+ includeResponseBody: true,
20
+ generateSwagger: true,
21
+ includeScreenshots: true,
22
+ maxScreenshotSize: 1920,
23
+ projectName: 'Test Hub',
24
+ };
25
+ /**
26
+ * 🚀 Inicializar o sistema AutoDocs
27
+ */
28
+ static async initialize(options) {
29
+ try {
30
+ if (AutoDocs.initialized) {
31
+ return;
32
+ }
33
+ // Aplicar configurações personalizadas
34
+ if (options?.config) {
35
+ AutoDocs.config = { ...AutoDocs.config, ...options.config };
36
+ }
37
+ if (options?.outputDir) {
38
+ AutoDocs.config.outputDir = options.outputDir;
39
+ }
40
+ if (options?.projectName) {
41
+ AutoDocs.config.projectName = options.projectName;
42
+ }
43
+ // Detectar tipo de projeto automaticamente
44
+ const projectType = AutoDocs.detectProjectType({
45
+ apis: [],
46
+ ssh: [],
47
+ db: [],
48
+ ui: [],
49
+ mobile: [],
50
+ });
51
+ // Garantir que o diretório de saída existe
52
+ await AutoDocs.ensureOutputDir();
53
+ // Inicializar ExecutionTracker
54
+ ExecutionTracker.initialize(AutoDocs.config.projectName, projectType);
55
+ // 🎯 REGISTRO GLOBAL para ApiActions
56
+ AutoDocs.registerGlobally();
57
+ AutoDocs.initialized = true;
58
+ }
59
+ catch (error) {
60
+ Logger.error('❌ AutoDocs: Erro na inicialização:', error);
61
+ }
62
+ }
63
+ /**
64
+ * 📖 Gerar documentação completa
65
+ */
66
+ static async generateDocs() {
67
+ // ✅ EVITAR EXECUÇÃO MÚLTIPLA
68
+ if (AutoDocs.finalizationExecuted) {
69
+ Logger.info('📚 AutoDocs: Documentação já foi gerada, pulando...');
70
+ return;
71
+ }
72
+ if (!AutoDocs.initialized) {
73
+ await AutoDocs.initialize();
74
+ }
75
+ try {
76
+ Logger.info('📚 AutoDocs: Iniciando geração de documentação...');
77
+ // Marcar como executado ANTES de começar
78
+ AutoDocs.finalizationExecuted = true;
79
+ // ✅ CRÍTICO: Finalizar coleta de dados PRIMEIRO
80
+ ExecutionTracker.finalize();
81
+ // ✅ NOVO: Forçar carregamento do arquivo persistido para evitar dados zerados
82
+ Logger.info('📊 AutoDocs: Forçando carregamento dos dados do arquivo...');
83
+ const fileData = ExecutionTracker.loadFromFile();
84
+ if (fileData) {
85
+ // Usar dados do arquivo diretamente
86
+ const data = fileData;
87
+ if (AutoDocs.hasDataToGenerate(data)) {
88
+ Logger.info('📊 AutoDocs: Dados encontrados, gerando documentação...');
89
+ // Gerar relatório HTML principal
90
+ await AutoDocs.generateHTMLReport(data);
91
+ // Gerar Swagger se habilitado e há APIs
92
+ if (AutoDocs.config.generateSwagger && data.apis.length > 0) {
93
+ Logger.info(`📋 AutoDocs: Gerando Swagger para ${data.apis.length} APIs...`);
94
+ await AutoDocs.generateSwaggerDocs(data.apis);
95
+ }
96
+ Logger.success(`📚 AutoDocs: Documentação gerada com sucesso em ${AutoDocs.config.outputDir}`);
97
+ Logger.info(`📊 Estatísticas: ${data.apis.length} APIs, ${data.ssh.length} SSH, ${data.db.length} DB, ${data.ui.length} UI, ${data.mobile.length} Mobile`);
98
+ }
99
+ else {
100
+ Logger.info('📚 AutoDocs: Nenhum dado encontrado para gerar documentação');
101
+ Logger.info('💡 AutoDocs: Execute seus testes para capturar dados automaticamente');
102
+ // ✅ GERAR ARQUIVO BÁSICO MESMO SEM DADOS
103
+ await AutoDocs.generateEmptyDocsReport();
104
+ Logger.success('✅ Arquivo de documentação básico gerado!');
105
+ }
106
+ }
107
+ else {
108
+ Logger.info('📚 AutoDocs: Arquivo de dados não encontrado');
109
+ Logger.info('💡 AutoDocs: Execute seus testes para capturar dados automaticamente');
110
+ // ✅ GERAR ARQUIVO BÁSICO MESMO SEM DADOS
111
+ await AutoDocs.generateEmptyDocsReport();
112
+ Logger.success('✅ Arquivo de documentação básico gerado!');
113
+ }
114
+ }
115
+ catch (error) {
116
+ Logger.error('❌ AutoDocs: Erro na geração de documentação:', error);
117
+ }
118
+ }
119
+ /**
120
+ * 📄 Gerar relatório HTML principal
121
+ */
122
+ static async generateHTMLReport(data) {
123
+ try {
124
+ const htmlContent = AutoDocs.generateHTMLContent(data);
125
+ const outputPath = path.join(AutoDocs.config.outputDir, 'index.html');
126
+ await fs.promises.writeFile(outputPath, htmlContent, 'utf-8');
127
+ Logger.info(`📄 Relatório HTML gerado: ${outputPath}`);
128
+ // ℹ️ Visualização por CTs removida - agora está no relatório unificado
129
+ }
130
+ catch (error) {
131
+ Logger.error('❌ Erro ao gerar HTML:', error);
132
+ }
133
+ }
134
+ /**
135
+ * 📄 Gerar documentação básica quando não há dados
136
+ */
137
+ static async generateEmptyDocsReport() {
138
+ try {
139
+ const htmlContent = AutoDocs.generateEmptyHTMLContent();
140
+ const outputPath = path.join(AutoDocs.config.outputDir, 'index.html');
141
+ await fs.promises.writeFile(outputPath, htmlContent, 'utf-8');
142
+ Logger.info(`📄 Documentação básica gerada: ${outputPath}`);
143
+ }
144
+ catch (error) {
145
+ Logger.error('❌ Erro ao gerar documentação básica:', error);
146
+ }
147
+ }
148
+ /**
149
+ * 🔄 Reset do sistema (APENAS EM DESENVOLVIMENTO)
150
+ */
151
+ static reset() {
152
+ // ✅ PROTEÇÃO: Só permitir reset em desenvolvimento ou com confirmação explícita
153
+ const isProduction = process.env.NODE_ENV === 'production';
154
+ const forceReset = process.env.FORCE_AUTODOCS_RESET === 'true';
155
+ if (isProduction && !forceReset) {
156
+ Logger.warning('⚠️ AutoDocs.reset: Ignorado em produção. Use FORCE_AUTODOCS_RESET=true se necessário');
157
+ return;
158
+ }
159
+ Logger.warning('⚠️ AutoDocs.reset: RESETANDO SISTEMA - Esta ação limpará todos os dados!');
160
+ AutoDocs.initialized = false;
161
+ AutoDocs.finalizationExecuted = false;
162
+ // ✅ NOVO: Só resetar ExecutionTracker se explicitamente solicitado
163
+ if (forceReset) {
164
+ ExecutionTracker.reset();
165
+ Logger.info('📚 AutoDocs.reset: ExecutionTracker também foi resetado');
166
+ }
167
+ else {
168
+ Logger.info('📚 AutoDocs.reset: ExecutionTracker preservado (use FORCE_AUTODOCS_RESET=true para resetar tudo)');
169
+ }
170
+ Logger.info('📚 AutoDocs: Sistema resetado');
171
+ }
172
+ // 🎯 Métodos de interceptação FUNCIONAIS
173
+ /**
174
+ * 🌐 Interceptar chamada de API
175
+ * DESABILITADO: Evita duplicação com ApiActions que já faz interceptação
176
+ */
177
+ static async interceptAPI(operation, endpoint, config) {
178
+ // ✅ CRÍTICO: Retornar ID vazio para evitar duplicação
179
+ // O sistema ExecutionTracker via ApiActions já faz todo o trabalho
180
+ Logger.info('🔄 AutoDocs.interceptAPI: Delegando para ExecutionTracker via ApiActions');
181
+ return `autodocs-delegated-${Date.now()}`;
182
+ }
183
+ /**
184
+ * 🔄 Atualizar resultado de API
185
+ * DESABILITADO: Evita duplicação com ExecutionTracker
186
+ */
187
+ static async updateAPIResult(docId, response, statusCode, error) {
188
+ // ✅ CRÍTICO: Não fazer update duplicado
189
+ // O ExecutionTracker via ApiActions já faz todo o trabalho
190
+ Logger.info('🔄 AutoDocs.updateAPIResult: Delegando para ExecutionTracker via ApiActions');
191
+ return;
192
+ }
193
+ /**
194
+ * 🎯 Registrar AutoDocs globalmente para ApiActions
195
+ */
196
+ static registerGlobally() {
197
+ const APIACTIONS_CHECK_DELAY = 1000;
198
+ try {
199
+ // Registrar no global para ApiActions
200
+ ;
201
+ global.AutoDocs = AutoDocs;
202
+ globalThis.AutoDocs = AutoDocs;
203
+ // ✅ INTERCEPTAÇÃO AUTOMÁTICA DE FETCH: Capturar todas as requisições HTTP
204
+ AutoDocs.setupFetchInterception();
205
+ // ✅ CRÍTICO: Verificar se ApiActions está disponível e conectar
206
+ setTimeout(() => {
207
+ try {
208
+ const apiActions = global.ApiActions ||
209
+ globalThis.ApiActions;
210
+ if (apiActions) {
211
+ process.stdout.write('✅ AutoDocs: ApiActions detectado, sistema pronto para interceptação\n');
212
+ }
213
+ else {
214
+ // Não exibir warning, pois fetch já está interceptado e funciona
215
+ }
216
+ }
217
+ catch {
218
+ // Silencioso - não precisa mostrar erro
219
+ }
220
+ }, APIACTIONS_CHECK_DELAY);
221
+ }
222
+ catch (error) {
223
+ process.stderr.write(`⚠️ AutoDocs: Erro ao registrar globalmente: ${error}\n`);
224
+ }
225
+ }
226
+ /**
227
+ * 🔗 Configurar interceptação automática de fetch
228
+ * DESABILITADO: Para evitar duplicação com ApiActions
229
+ */
230
+ static setupFetchInterception() {
231
+ // ✅ CRÍTICO: Desabilitar interceptação de fetch para evitar duplicação
232
+ // O ApiActions já faz todo o trabalho de tracking/documentação
233
+ return;
234
+ }
235
+ /**
236
+ * 🎯 Verificar se URL deve ser interceptada
237
+ * CORRIGIDO: Não interceptar URLs que já são processadas pelo ApiActions
238
+ */
239
+ static shouldInterceptURL(url) {
240
+ // ✅ CRÍTICO: Não interceptar URLs de API externas que já passam pelo ApiActions
241
+ // ApiActions já faz o tracking completo, então não precisamos duplicar
242
+ if (url.includes('api-qa1.telefonica.com.br') ||
243
+ url.includes('telefonica.com') ||
244
+ url.includes('.com.br/') ||
245
+ url.includes('/api/') ||
246
+ url.includes('/v1/') ||
247
+ url.includes('opportunity')) {
248
+ return false; // Deixar o ApiActions gerenciar essas URLs
249
+ }
250
+ // Interceptar apenas URLs específicas que não passam pelo ApiActions
251
+ return (url.includes('localhost:') &&
252
+ (url.includes('/internal/') ||
253
+ url.includes('/health') ||
254
+ url.includes('/metrics')));
255
+ }
256
+ /**
257
+ * 🔄 Processar fetch interceptado
258
+ */
259
+ static async handleInterceptedFetch(originalFetch, args, method, url, init) {
260
+ let docId = null;
261
+ try {
262
+ docId = await AutoDocs.interceptAPI(method, url, {
263
+ method,
264
+ headers: init?.headers,
265
+ payload: init?.body,
266
+ url,
267
+ });
268
+ }
269
+ catch {
270
+ // Falhou na interceptação, continuar sem documentar
271
+ }
272
+ try {
273
+ const response = await originalFetch(...args);
274
+ // Atualizar documentação com resposta
275
+ if (docId) {
276
+ await AutoDocs.updateFetchResponse(docId, response);
277
+ }
278
+ return response;
279
+ }
280
+ catch (error) {
281
+ // Atualizar com erro se documentando
282
+ if (docId) {
283
+ await AutoDocs.updateFetchError(docId, error);
284
+ }
285
+ throw error;
286
+ }
287
+ }
288
+ /**
289
+ * 📥 Atualizar resposta do fetch
290
+ */
291
+ static async updateFetchResponse(docId, response) {
292
+ try {
293
+ const responseText = await response.clone().text();
294
+ let responseData = responseText;
295
+ try {
296
+ responseData = JSON.parse(responseText);
297
+ }
298
+ catch {
299
+ // Não é JSON, usar como string
300
+ }
301
+ await AutoDocs.updateAPIResult(docId, responseData, response.status);
302
+ }
303
+ catch {
304
+ // Falhou na atualização, continuar
305
+ }
306
+ }
307
+ /**
308
+ * ❌ Atualizar erro do fetch
309
+ */
310
+ static async updateFetchError(docId, error) {
311
+ try {
312
+ await AutoDocs.updateAPIResult(docId, { error: String(error) }, 0);
313
+ }
314
+ catch {
315
+ // Falhou na atualização de erro, continuar
316
+ }
317
+ }
318
+ /**
319
+ * 🔧 NOVO: Método para forçar geração da documentação
320
+ */
321
+ static async forceGenerate() {
322
+ Logger.info('🔄 AutoDocs: Forçando geração de documentação...');
323
+ try {
324
+ // Verificar dados antes de gerar
325
+ const data = ExecutionTracker.getData();
326
+ Logger.info('🔍 AutoDocs: Dados disponíveis antes da geração:');
327
+ Logger.info(` 📋 APIs: ${data.apis.length}`);
328
+ Logger.info(` 🖥️ SSH: ${data.ssh.length}`);
329
+ Logger.info(` 🗄️ DB: ${data.db.length}`);
330
+ Logger.info(` 🎨 UI: ${data.ui.length}`);
331
+ Logger.info(` 📱 Mobile: ${data.mobile.length}`);
332
+ // Se não há dados, verificar se há arquivo temporário
333
+ if (data.apis.length === 0) {
334
+ const tempData = ExecutionTracker.loadFromFile();
335
+ if (tempData && tempData.apis.length > 0) {
336
+ Logger.info(`📁 AutoDocs: Encontrados ${tempData.apis.length} APIs no arquivo temporário`);
337
+ }
338
+ else {
339
+ Logger.info('❌ AutoDocs: Nenhum dado encontrado para gerar documentação');
340
+ Logger.info('💡 AutoDocs: Execute alguns testes com ApiActions primeiro');
341
+ }
342
+ }
343
+ await AutoDocs.generateDocs();
344
+ }
345
+ catch (error) {
346
+ Logger.error('❌ AutoDocs: Erro na geração forçada:', error);
347
+ }
348
+ }
349
+ /**
350
+ * 🖥️ Interceptar comando SSH
351
+ */
352
+ static async interceptSSH(command, host, details) {
353
+ if (!(AutoDocs.config.enabled && AutoDocs.config.categories.includes('ssh'))) {
354
+ return;
355
+ }
356
+ const sshCommand = {
357
+ command,
358
+ output: '',
359
+ host: host || process.env.SSH_HOST || 'unknown',
360
+ user: details?.user || process.env.SSH_USER || 'unknown',
361
+ timestamp: new Date().toISOString(),
362
+ testName: AutoDocs.getCurrentTestName(),
363
+ duration: 0,
364
+ success: false,
365
+ error: undefined,
366
+ };
367
+ ExecutionTracker.trackSSH(sshCommand);
368
+ }
369
+ /**
370
+ * 🗄️ Interceptar query de banco
371
+ */
372
+ static interceptDB(query, params, dbType, description, serviceName) {
373
+ if (!(AutoDocs.config.enabled && AutoDocs.config.categories.includes('db'))) {
374
+ return;
375
+ }
376
+ const dbQuery = {
377
+ testName: AutoDocs.getCurrentTestName(),
378
+ environment: process.env.NODE_ENV || 'development',
379
+ query: query.trim(),
380
+ params: params || [],
381
+ type: AutoDocs.detectQueryType(query),
382
+ database: dbType || 'oracle',
383
+ timestamp: new Date().toISOString(),
384
+ duration: 0,
385
+ success: undefined, // ✅ CORRIGIDO: Começar com undefined para melhor detecção
386
+ error: undefined,
387
+ description: description || undefined,
388
+ serviceName: serviceName || undefined,
389
+ };
390
+ Logger.info(`🗄️ [HubDocs] Interceptando query: ${query.substring(0, 50)}...`);
391
+ ExecutionTracker.trackDB(dbQuery);
392
+ }
393
+ /**
394
+ * 🔄 Atualizar resultado de query de banco
395
+ */
396
+ static updateDBResult(query, timestamp, success, duration, error, resultCount) {
397
+ if (!(AutoDocs.config.enabled && AutoDocs.config.categories.includes('db')))
398
+ return;
399
+ Logger.info(`🔄 [HubDocs] updateDBResult chamado: success=${success}, duration=${duration}ms, error=${error}`);
400
+ ExecutionTracker.updateDBResult(query, timestamp, success, duration, error, resultCount);
401
+ }
402
+ /**
403
+ * 🔄 Atualizar resultado de comando SSH
404
+ */
405
+ static updateSSHResult(command, timestamp, success, duration, exitCode, error) {
406
+ if (!(AutoDocs.config.enabled && AutoDocs.config.categories.includes('ssh')))
407
+ return;
408
+ ExecutionTracker.updateSSHResult(command, timestamp, success, duration, exitCode, error);
409
+ }
410
+ /**
411
+ * 🎨 Interceptar ação de UI
412
+ */
413
+ static async interceptUI(action, element, url, details) {
414
+ if (!(AutoDocs.config.enabled && AutoDocs.config.categories.includes('ui'))) {
415
+ return;
416
+ }
417
+ const uiAction = {
418
+ testName: AutoDocs.getCurrentTestName(),
419
+ action,
420
+ element: element || 'unknown',
421
+ selector: element || 'unknown', // O seletor é o element passado
422
+ description: details?.description || action, // 🆕 Capturar description dos details
423
+ page: url || details?.url || 'unknown',
424
+ timestamp: new Date().toISOString(),
425
+ success: false,
426
+ duration: 0,
427
+ screenshot: details?.screenshot,
428
+ error: undefined,
429
+ };
430
+ ExecutionTracker.trackUI(uiAction);
431
+ }
432
+ /**
433
+ * 🆕 Atualizar status de sucesso de uma ação UI
434
+ */
435
+ static updateActionSuccess(action, element, success, duration, error) {
436
+ if (!(AutoDocs.config.enabled && AutoDocs.config.categories.includes('ui'))) {
437
+ return;
438
+ }
439
+ try {
440
+ const data = ExecutionTracker.getData();
441
+ // Encontrar a última ação UI que corresponde ao action e element
442
+ for (let i = data.ui.length - 1; i >= 0; i--) {
443
+ const uiAction = data.ui[i];
444
+ if (uiAction.action === action && uiAction.selector === element) {
445
+ uiAction.success = success;
446
+ uiAction.duration = duration;
447
+ if (error) {
448
+ uiAction.error = error;
449
+ }
450
+ // Forçar save usando método interno
451
+ ExecutionTracker['saveToFile']();
452
+ break;
453
+ }
454
+ }
455
+ }
456
+ catch (err) {
457
+ // Ignorar erros para não quebrar funcionalidade principal
458
+ }
459
+ }
460
+ /**
461
+ * 📱 Interceptar ação mobile
462
+ */
463
+ static async interceptMobile(action, element, platform, details) {
464
+ if (!(AutoDocs.config.enabled && AutoDocs.config.categories.includes('mobile'))) {
465
+ return;
466
+ }
467
+ const mobileAction = {
468
+ testName: AutoDocs.getCurrentTestName(),
469
+ action,
470
+ element: element || 'unknown',
471
+ device: details?.device || 'unknown',
472
+ platform: platform || 'Android',
473
+ timestamp: new Date().toISOString(),
474
+ value: details?.value,
475
+ coordinates: details?.coordinates,
476
+ };
477
+ ExecutionTracker.trackMobile(mobileAction);
478
+ }
479
+ /**
480
+ * 🧹 Extrair nome limpo do CT removendo logs excessivos
481
+ */
482
+ static extractCleanCTName(ctName) {
483
+ if (!ctName)
484
+ return 'CT Sem Nome';
485
+ // Se tem menos de 50 caracteres, retorna completo
486
+ if (ctName.length <= 50)
487
+ return ctName;
488
+ // Tentar extrair apenas a parte do CT sem logs
489
+ const lines = ctName.split('\n');
490
+ if (lines.length > 0) {
491
+ const firstLine = lines[0].trim();
492
+ // Se a primeira linha parece ser um ID de CT, usar ela
493
+ if (firstLine.includes('CT') || firstLine.length < 100) {
494
+ return firstLine;
495
+ }
496
+ }
497
+ // Procurar por padrões específicos de CT
498
+ const ctMatch = ctName.match(/CT\d+\s*-\s*([^[\n{(]+)/i);
499
+ if (ctMatch)
500
+ return ctMatch[0].trim();
501
+ // Procurar por método - descrição (sem logs)
502
+ const methodMatch = ctName.match(/(\w+\(\)\s*-\s*[^[\n{(]{1,80})/i);
503
+ if (methodMatch)
504
+ return methodMatch[1].trim();
505
+ // Como último recurso, pegar apenas os primeiros 50 caracteres
506
+ return `${ctName.substring(0, 50)}...`;
507
+ }
508
+ /**
509
+ * 🔐 Escapar HTML para segurança
510
+ */
511
+ static escapeHtml(text) {
512
+ if (!text) {
513
+ return '';
514
+ }
515
+ return text
516
+ .replace(/&/g, '&amp;')
517
+ .replace(/</g, '&lt;')
518
+ .replace(/>/g, '&gt;')
519
+ .replace(/"/g, '&quot;')
520
+ .replace(/'/g, '&#39;')
521
+ .replace(/\n/g, '<br>');
522
+ }
523
+ /**
524
+ * 🖼️ Resolve caminho de imagem para data URI quando possível
525
+ */
526
+ static resolveImageSource(imagePath) {
527
+ if (!imagePath) {
528
+ return null;
529
+ }
530
+ const trimmed = imagePath.trim();
531
+ if (!trimmed) {
532
+ return null;
533
+ }
534
+ if (/^data:image\//i.test(trimmed) || /^https?:\/\//i.test(trimmed)) {
535
+ return trimmed;
536
+ }
537
+ const normalized = trimmed
538
+ .replace(/^file:\/\//i, '')
539
+ .replace(/\\/g, path.sep);
540
+ const candidates = new Set();
541
+ if (path.isAbsolute(normalized)) {
542
+ candidates.add(path.normalize(normalized));
543
+ }
544
+ else {
545
+ candidates.add(path.normalize(normalized));
546
+ candidates.add(path.join(process.cwd(), normalized));
547
+ candidates.add(path.join(process.cwd(), 'playwright-report', normalized));
548
+ candidates.add(path.join(process.cwd(), 'test-results', normalized));
549
+ }
550
+ for (const candidate of candidates) {
551
+ try {
552
+ if (fs.existsSync(candidate)) {
553
+ const buffer = fs.readFileSync(candidate);
554
+ return AutoDocs.toDataUri(candidate, buffer);
555
+ }
556
+ }
557
+ catch (error) {
558
+ Logger.warning(`⚠️ [HubDocs] Falha ao embutir imagem (${candidate}): ${error}`);
559
+ }
560
+ }
561
+ return trimmed;
562
+ }
563
+ /**
564
+ * 🧪 Converte buffer de imagem em data URI
565
+ */
566
+ static toDataUri(filePath, buffer) {
567
+ const mime = AutoDocs.detectMimeType(filePath);
568
+ return `data:${mime};base64,${buffer.toString('base64')}`;
569
+ }
570
+ /**
571
+ * 🎨 Detecta MIME-Type básico pelo sufixo do arquivo
572
+ */
573
+ static detectMimeType(filePath) {
574
+ switch (path.extname(filePath).toLowerCase()) {
575
+ case '.jpg':
576
+ case '.jpeg':
577
+ return 'image/jpeg';
578
+ case '.gif':
579
+ return 'image/gif';
580
+ case '.webp':
581
+ return 'image/webp';
582
+ case '.svg':
583
+ return 'image/svg+xml';
584
+ default:
585
+ return 'image/png';
586
+ }
587
+ }
588
+ // 🔧 Métodos utilitários
589
+ /**
590
+ * 🔍 Detectar versão do AutoCore instalada dinamicamente
591
+ */
592
+ static getProjectVersion() {
593
+ try {
594
+ // ✅ CORRIGIDO: Usar fs e path já importados no topo do arquivo
595
+ // Primeiro, tentar obter informações do projeto do usuário
596
+ const root = process.cwd();
597
+ const pkgPath = path.join(root, 'package.json');
598
+ if (fs.existsSync(pkgPath)) {
599
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
600
+ // Obter nome do projeto do usuário
601
+ const projectName = pkg.name || 'Projeto Test HUB';
602
+ // Obter versão do AutoCore instalada (igual ao UnifiedHtmlGenerator e ExecutionTracker)
603
+ const autocoreVersion = pkg.dependencies?.['@silasfmartins/testhub'] ||
604
+ pkg.devDependencies?.['@silasfmartins/testhub'] ||
605
+ '1.1.X';
606
+ Logger.info(`📦 HUBDocs: Projeto ${projectName} usando Test HUB ${autocoreVersion}`);
607
+ return `TestHub@${autocoreVersion}`;
608
+ }
609
+ // Fallback: tentar buscar em diretórios pai
610
+ let currentDir = root;
611
+ let attempts = 0;
612
+ const maxAttempts = 3;
613
+ while (attempts < maxAttempts) {
614
+ const packagePath = path.join(currentDir, 'package.json');
615
+ if (fs.existsSync(packagePath)) {
616
+ const packageContent = fs.readFileSync(packagePath, 'utf8');
617
+ const packageData = JSON.parse(packageContent);
618
+ const autocoreVersion = packageData.dependencies?.['@silasfmartins/testhub'] ||
619
+ packageData.devDependencies?.['@silasfmartins/testhub'] ||
620
+ '1.1.X';
621
+ Logger.info(`📦 HUBDocs: Test Hub ${autocoreVersion} detectado em ${packagePath}`);
622
+ return `Test Hub@${autocoreVersion}`;
623
+ }
624
+ // Subir um nível no diretório
625
+ const parentDir = path.dirname(currentDir);
626
+ if (parentDir === currentDir)
627
+ break; // Chegou na raiz
628
+ currentDir = parentDir;
629
+ attempts++;
630
+ }
631
+ // Fallback para versão padrão
632
+ Logger.warning('⚠️ AutoDocs: package.json não encontrado, usando versão padrão');
633
+ return 'Test Hub@1.1.X';
634
+ }
635
+ catch (error) {
636
+ Logger.warning(`⚠️ AutoDocs: Erro ao detectar versão do Test Hub: ${error instanceof Error ? error.message : String(error)}`);
637
+ return 'Test Hub@1.1.X';
638
+ }
639
+ }
640
+ /**
641
+ * 🎨 Gerar conteúdo HTML com dados usando TailwindCSS e funcionalidades completas
642
+ */
643
+ static generateHTMLContent(data) {
644
+ if (!data || Object.keys(data).length === 0) {
645
+ return AutoDocs.generateEmptyHTMLContent();
646
+ }
647
+ // 🔧 Detectar versão dinâmica do projeto local
648
+ const projectVersion = AutoDocs.getProjectVersion();
649
+ // 🤖 Detectar categorias automaticamente
650
+ const detectedCategories = AutoDocs.detectProjectCategories(data.apis || []);
651
+ const categoryOverview = Object.entries(detectedCategories)
652
+ .filter(([key]) => key !== 'outras')
653
+ .map(([key, catData]) => `${catData.icon} ${key.charAt(0).toUpperCase() + key.slice(1)}`)
654
+ .join(', ');
655
+ const timestamp = new Date().toLocaleString('pt-BR');
656
+ const totalItems = (data.apis?.length || 0) +
657
+ (data.ssh?.length || 0) +
658
+ (data.db?.length || 0) +
659
+ (data.ui?.length || 0) +
660
+ (data.mobile?.length || 0);
661
+ return `<!DOCTYPE html>
662
+ <html lang="pt-BR">
663
+ <head>
664
+ <meta charset="UTF-8">
665
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
666
+ <title>📚 ${AutoDocs.config.projectName} - HubDocs Inteligente</title>
667
+ <script src="https://cdn.tailwindcss.com"></script>
668
+ <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
669
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/jspdf/2.5.1/jspdf.umd.min.js"></script>
670
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/html2canvas/1.4.1/html2canvas.min.js"></script>
671
+ <script src="https://cdn.jsdelivr.net/npm/xlsx@0.18.5/dist/xlsx.full.min.js"></script>
672
+ <style>
673
+ body { font-family: 'Inter', sans-serif; background-color: #0f172a; color: #f1f5f9; min-height: 100vh; }
674
+ .light-mode { background-color: #f9fafb !important; color: #111827 !important; }
675
+ .card { background: #1e293b; border-radius: 1rem; padding: 1.5rem; box-shadow: 0 2px 8px #0002; color: #f1f5f9; }
676
+ .light-mode .card { background: #fff; color: #111827; border: 1px solid #e5e7eb; }
677
+ .btn { background: #334155; color: #fff; border: none; border-radius: 8px; padding: 0.5rem 1.2rem; font-weight: 600; cursor: pointer; margin-bottom: 0.25rem; transition: background 0.2s; }
678
+ .btn:hover { background: #475569; }
679
+ .light-mode .btn { background: #374151; color: #fff; border: 1px solid #d1d5db; }
680
+ .light-mode .btn:hover { background: #4b5563; }
681
+ .header-bar { background: #1e293b; padding: 1rem 2rem; border-radius: 1rem 1rem 0 0; margin-bottom: 1.5rem; display: flex; align-items: center; justify-content: space-between; }
682
+ .header-bar .header-title { font-size: 2rem; font-weight: 700; color: #fbbf24; }
683
+ .header-bar .header-meta { font-size: 1rem; color: #f1f5f9; }
684
+ .light-mode .header-bar { background: #f3f4f6; color: #111827; border: 1px solid #e5e7eb; }
685
+ .light-mode .header-title { color: #d97706; }
686
+ .light-mode .header-meta { color: #374151 !important; }
687
+ .footer-bar { background: #1e293b; color: #fbbf24; padding: 1rem 2rem; border-radius: 0 0 1rem 1rem; margin-top: 2rem; text-align: center; font-size: 1rem; }
688
+ .light-mode .footer-bar { background: #f3f4f6; color: #d97706; border: 1px solid #e5e7eb; }
689
+ .footer-bar a { color: #fbbf24; text-decoration: underline; }
690
+ .light-mode .footer-bar a { color: #d97706; font-weight: 600; }
691
+ .method-badge { display: inline-block; padding: 4px 12px; border-radius: 9999px; font-size: 12px; font-weight: 600; color: white; }
692
+ .method-get { background: #10b981; }
693
+ .method-post { background: #3b82f6; }
694
+ .method-put { background: #f59e0b; }
695
+ .method-delete { background: #ef4444; }
696
+ .method-patch { background: #8b5cf6; }
697
+ .status-badge { display: inline-block; padding: 4px 12px; border-radius: 9999px; font-size: 12px; font-weight: 600; }
698
+ @media (max-width: 900px) {
699
+ .grid-cols-2, .grid-cols-3, .grid-cols-4, .grid-cols-5 { grid-template-columns: 1fr !important; }
700
+ }
701
+ </style>
702
+ </head>
703
+ <body id="body-root" class="min-h-screen bg-slate-900 text-slate-100">
704
+ <div class="header-bar">
705
+ <div>
706
+ <h1 class="header-title">📚 ${AutoDocs.config.projectName} - HubDocs Inteligente</h1>
707
+ <p class="header-meta">🤖 Categorização Inteligente: ${categoryOverview || 'Sistema Adaptativo'}</p>
708
+ </div>
709
+ <div class="flex gap-2">
710
+ <button id="toggle-theme" class="btn" type="button" title="Alternar tema">🌙 Tema</button>
711
+ <button id="export-pdf" class="btn" type="button" title="Exportar PDF">📄 PDF</button>
712
+ <button id="export-csv" class="btn" type="button" title="Exportar CSV">📑 CSV</button>
713
+ <button id="export-json" class="btn" type="button" title="Exportar JSON">🗒️ JSON</button>
714
+ <button id="export-xlsx" class="btn" type="button" title="Exportar XLSX">📊 XLSX</button>
715
+ </div>
716
+ </div>
717
+
718
+ <div id="report-root" class="max-w-7xl mx-auto p-6" style="background:inherit;">
719
+ <!-- Overview Cards -->
720
+ <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-5 gap-6 mb-8">
721
+ <div class="card bg-gradient-to-br from-green-600 to-green-700">
722
+ <h3 class="text-3xl font-bold text-white">${data.apis?.length || 0}</h3>
723
+ <p class="text-green-100">🌐 APIs Inteligentes</p>
724
+ </div>
725
+ <div class="card bg-gradient-to-br from-blue-600 to-blue-700">
726
+ <h3 class="text-3xl font-bold text-white">${data.ssh?.length || 0}</h3>
727
+ <p class="text-blue-100">🖥️ Comandos SSH</p>
728
+ </div>
729
+ <div class="card bg-gradient-to-br from-purple-600 to-purple-700">
730
+ <h3 class="text-3xl font-bold text-white">${data.db?.length || 0}</h3>
731
+ <p class="text-purple-100">🗄️ Queries Database</p>
732
+ </div>
733
+ <div class="card bg-gradient-to-br from-orange-600 to-orange-700">
734
+ <h3 class="text-3xl font-bold text-white">${data.ui?.length || 0}</h3>
735
+ <p class="text-orange-100">🎨 Ações UI</p>
736
+ </div>
737
+ <div class="card bg-gradient-to-br from-pink-600 to-pink-700">
738
+ <h3 class="text-3xl font-bold text-white">${data.mobile?.length || 0}</h3>
739
+ <p class="text-pink-100">📱 Ações Mobile</p>
740
+ </div>
741
+ </div>
742
+
743
+ <!-- Intelligent System Info -->
744
+ <div class="card mb-8 bg-gradient-to-r from-blue-600 to-purple-600">
745
+ <h3 class="text-xl font-bold text-white mb-4">🤖 Sistema de Categorização Inteligente</h3>
746
+ <div class="text-white space-y-2">
747
+ <p><span class="font-semibold">📊 Total de Itens:</span> ${totalItems}</p>
748
+ <p><span class="font-semibold">🎯 Categorias Detectadas:</span> ${categoryOverview || 'Analisando padrões...'}</p>
749
+ <p><span class="font-semibold">🕐 Gerado em:</span> ${timestamp}</p>
750
+ <p class="text-sm text-blue-100 mt-3">✨ Sistema adapta-se automaticamente aos padrões dos seus testes, detectando categorias empresariais como Customer, Billing, Catalog, Simulation, Workflow e mais!</p>
751
+ </div>
752
+ </div>
753
+
754
+ <!-- Agrupamento Inteligente por Categoria -->
755
+ ${AutoDocs.generateGroupedSections(data)}
756
+ </div>
757
+
758
+ <div class="footer-bar">
759
+ Desenvolvido por <strong>HubDocs Inteligente (${projectVersion})</strong> &mdash; Sistema de Categorização Automática com IA.<br>
760
+ <span>Documentação gerada automaticamente em ${timestamp}. <a href="#" onclick="alert('Sistema AutoDocs: Categorização inteligente baseada em padrões empresariais')">📚 Sobre a IA</a></span>
761
+ </div>
762
+
763
+ <script>
764
+ document.getElementById('toggle-theme').onclick = () => {
765
+ document.getElementById('body-root').classList.toggle('light-mode')
766
+ document.querySelector('.header-bar').classList.toggle('light-mode')
767
+ document.querySelector('.footer-bar').classList.toggle('light-mode')
768
+ document.querySelectorAll('.card').forEach(card => card.classList.toggle('light-mode'))
769
+ const meta = document.querySelector('.header-meta')
770
+ if (document.getElementById('body-root').classList.contains('light-mode')) {
771
+ meta && (meta.style.color = '#222')
772
+ } else {
773
+ meta && (meta.style.color = '#f1f5f9')
774
+ }
775
+ }
776
+
777
+ // 🆕 Função para expandir/colapsar seções
778
+ function toggleSection(sectionKey) {
779
+ const section = document.getElementById('section-' + sectionKey)
780
+ const btn = document.getElementById('btn-' + sectionKey)
781
+
782
+ if (section && btn) {
783
+ if (section.style.display === 'none') {
784
+ section.style.display = 'block'
785
+ btn.innerHTML = '📋 Ocultar'
786
+ } else {
787
+ section.style.display = 'none'
788
+ btn.innerHTML = '📋 Detalhes'
789
+ }
790
+ }
791
+ }
792
+
793
+ function getAllData() {
794
+ return {
795
+ apis: ${JSON.stringify(data.apis || [])},
796
+ ssh: ${JSON.stringify(data.ssh || [])},
797
+ db: ${JSON.stringify(data.db || [])},
798
+ ui: ${JSON.stringify(data.ui || [])},
799
+ mobile: ${JSON.stringify(data.mobile || [])},
800
+ detectedCategories: ${JSON.stringify(detectedCategories)},
801
+ timestamp: '${timestamp}',
802
+ projectName: '${AutoDocs.config.projectName}'
803
+ }
804
+ }
805
+
806
+ document.getElementById('export-pdf').onclick = async () => {
807
+ console.log('🔵 [PDF] Botão clicado - Iniciando geração do PDF do AutoDocs...');
808
+
809
+ const element = document.getElementById('report-root');
810
+ if (!element) {
811
+ console.error('❌ [PDF] Elemento report-root não encontrado!');
812
+ alert('❌ Erro: Elemento do relatório não encontrado!');
813
+ return;
814
+ }
815
+
816
+ const hasImages = element.querySelectorAll('img').length > 0;
817
+ if (hasImages) {
818
+ console.log('📸 [PDF] Imagens detectadas - preparando incorporação em base64 para ambiente offline');
819
+ }
820
+
821
+ // Verificar se jsPDF está carregado
822
+ if (typeof jspdf === 'undefined') {
823
+ console.error('❌ [PDF] jsPDF não está carregado!');
824
+ alert('❌ Erro: Biblioteca jsPDF não carregada. Recarregue a página.');
825
+ return;
826
+ }
827
+ console.log('✅ [PDF] jsPDF carregado com sucesso');
828
+
829
+ // 🆕 EXPANDIR TODAS AS SEÇÕES antes de gerar PDF
830
+ const originalStates = {
831
+ groups: [],
832
+ apis: [],
833
+ screenshots: []
834
+ };
835
+
836
+ // Expandir todos os grupos/seções colapsáveis
837
+ const allGroups = element.querySelectorAll('[id*="group-"], [id*="section-"], [id*="action-"]');
838
+ allGroups.forEach((group) => {
839
+ originalStates.groups.push(group.style.display);
840
+ group.style.display = 'block';
841
+ });
842
+
843
+ // Expandir detalhes de APIs
844
+ const allApiDetails = element.querySelectorAll('.api-details, .ssh-details, .db-details, .mobile-details');
845
+ allApiDetails.forEach((detail) => {
846
+ originalStates.apis.push(detail.style.display);
847
+ detail.style.display = 'block';
848
+ });
849
+
850
+ // Aguardar imagens carregarem
851
+ const images = element.querySelectorAll('img');
852
+ console.log('📸 Total de imagens encontradas:', images.length);
853
+
854
+ await Promise.all(Array.from(images).map(img => {
855
+ if (img.complete) return Promise.resolve();
856
+ return new Promise(resolve => {
857
+ img.onload = resolve;
858
+ img.onerror = resolve;
859
+ setTimeout(resolve, 2000);
860
+ });
861
+ }));
862
+
863
+ console.log('✅ Imagens carregadas');
864
+ await new Promise(resolve => setTimeout(resolve, 300));
865
+
866
+ // Preparar elemento para PDF
867
+ const originalBg = element.style.background;
868
+ const originalColor = element.style.color;
869
+ element.style.background = '#fff';
870
+ element.style.color = '#222';
871
+
872
+ console.log('📄 [PDF] Iniciando geração do PDF...');
873
+
874
+ try {
875
+ // Usar html2canvas para capturar o conteúdo
876
+ const canvas = await html2canvas(element, {
877
+ scale: 2,
878
+ useCORS: false,
879
+ allowTaint: true,
880
+ backgroundColor: '#fff',
881
+ logging: false,
882
+ imageTimeout: 0
883
+ });
884
+
885
+ // Criar PDF com jsPDF
886
+ const { jsPDF } = jspdf;
887
+ const pdf = new jsPDF({
888
+ orientation: 'portrait',
889
+ unit: 'mm',
890
+ format: 'a4'
891
+ });
892
+
893
+ const imgData = canvas.toDataURL('image/jpeg', 0.98);
894
+ const imgWidth = 210;
895
+ const pageHeight = 297;
896
+ const imgHeight = (canvas.height * imgWidth) / canvas.width;
897
+ let heightLeft = imgHeight;
898
+ let position = 0;
899
+
900
+ pdf.addImage(imgData, 'JPEG', 0, position, imgWidth, imgHeight);
901
+ heightLeft -= pageHeight;
902
+
903
+ while (heightLeft > 0) {
904
+ position = heightLeft - imgHeight;
905
+ pdf.addPage();
906
+ pdf.addImage(imgData, 'JPEG', 0, position, imgWidth, imgHeight);
907
+ heightLeft -= pageHeight;
908
+ }
909
+
910
+ pdf.save('autodocs-inteligente.pdf');
911
+
912
+ console.log('✅ PDF do AutoDocs gerado com sucesso!');
913
+ alert('✅ PDF do AutoDocs gerado com sucesso! Verifique sua pasta de Downloads.');
914
+ } catch (error) {
915
+ console.error('❌ Erro ao gerar PDF do AutoDocs:', error);
916
+ alert('❌ Erro ao gerar PDF: ' + error.message);
917
+ } finally {
918
+ // Restaurar estilos originais
919
+ element.style.background = originalBg;
920
+ element.style.color = originalColor;
921
+
922
+ // Restaurar estados originais das seções
923
+ allGroups.forEach((group, index) => {
924
+ group.style.display = originalStates.groups[index] || '';
925
+ });
926
+
927
+ allApiDetails.forEach((detail, index) => {
928
+ detail.style.display = originalStates.apis[index] || '';
929
+ });
930
+ }
931
+ };
932
+
933
+ document.getElementById('export-csv').onclick = () => {
934
+ const allData = getAllData()
935
+ const csvData = []
936
+
937
+ // Headers
938
+ csvData.push(['Tipo', 'Nome', 'URL/Comando', 'Status', 'Duração', 'Teste', 'Timestamp'])
939
+
940
+ // APIs
941
+ allData.apis.forEach(api => {
942
+ csvData.push(['API', api.testName, api.url, api.success ? 'Sucesso' : 'Erro', api.duration + 'ms', api.testName, new Date(api.timestamp).toLocaleString('pt-BR')])
943
+ })
944
+
945
+ // SSH
946
+ allData.ssh.forEach(ssh => {
947
+ csvData.push(['SSH', ssh.command, ssh.host, ssh.success ? 'Sucesso' : 'Erro', ssh.duration + 'ms', ssh.testName, new Date(ssh.timestamp).toLocaleString('pt-BR')])
948
+ })
949
+
950
+ const csv = csvData.map(row => row.join(';')).join('\\n')
951
+ const blob = new Blob([csv], { type: 'text/csv' })
952
+ const a = document.createElement('a')
953
+ a.href = URL.createObjectURL(blob)
954
+ a.download = 'autodocs-inteligente.csv'
955
+ a.click()
956
+ }
957
+
958
+ document.getElementById('export-json').onclick = () => {
959
+ const blob = new Blob([JSON.stringify(getAllData(), null, 2)], { type: 'application/json' })
960
+ const a = document.createElement('a')
961
+ a.href = URL.createObjectURL(blob)
962
+ a.download = 'autodocs-inteligente.json'
963
+ a.click()
964
+ }
965
+
966
+ document.getElementById('export-xlsx').onclick = () => {
967
+ try {
968
+ const allData = getAllData()
969
+ const workbook = XLSX.utils.book_new()
970
+ let hasData = false
971
+
972
+ // Sheet APIs
973
+ if (allData.apis && allData.apis.length > 0) {
974
+ const apiSheet = XLSX.utils.json_to_sheet(allData.apis.map(api => ({
975
+ 'Teste': api.testName || 'N/A',
976
+ 'Método': api.method || 'N/A',
977
+ 'URL': api.url || 'N/A',
978
+ 'Status Code': api.statusCode || 'N/A',
979
+ 'Sucesso': api.success ? 'Sim' : 'Não',
980
+ 'Duração (ms)': api.duration || 0,
981
+ 'Ambiente': api.environment || 'N/A',
982
+ 'Timestamp': new Date(api.timestamp).toLocaleString('pt-BR')
983
+ })))
984
+ XLSX.utils.book_append_sheet(workbook, apiSheet, 'APIs')
985
+ hasData = true
986
+ }
987
+
988
+ // Sheet SSH
989
+ if (allData.ssh && allData.ssh.length > 0) {
990
+ const sshSheet = XLSX.utils.json_to_sheet(allData.ssh.map(ssh => ({
991
+ 'Teste': ssh.testName || 'N/A',
992
+ 'Comando': ssh.command || 'N/A',
993
+ 'Host': ssh.host || 'N/A',
994
+ 'Usuário': ssh.user || 'N/A',
995
+ 'Sucesso': ssh.success ? 'Sim' : 'Não',
996
+ 'Duração (ms)': ssh.duration || 0,
997
+ 'Timestamp': new Date(ssh.timestamp).toLocaleString('pt-BR')
998
+ })))
999
+ XLSX.utils.book_append_sheet(workbook, sshSheet, 'SSH')
1000
+ hasData = true
1001
+ }
1002
+
1003
+ // Sheet Database
1004
+ if (allData.db && allData.db.length > 0) {
1005
+ const dbSheet = XLSX.utils.json_to_sheet(allData.db.map(db => ({
1006
+ 'Teste': db.testName || 'N/A',
1007
+ 'Query': db.query ? db.query.substring(0, 100) : 'N/A',
1008
+ 'Tipo': db.type || 'N/A',
1009
+ 'Database': db.database || 'N/A',
1010
+ 'Sucesso': db.success ? 'Sim' : 'Não',
1011
+ 'Duração (ms)': db.duration || 0,
1012
+ 'Resultado Count': db.resultCount || 0,
1013
+ 'Timestamp': new Date(db.timestamp).toLocaleString('pt-BR')
1014
+ })))
1015
+ XLSX.utils.book_append_sheet(workbook, dbSheet, 'Database')
1016
+ hasData = true
1017
+ }
1018
+
1019
+ // Sheet UI
1020
+ if (allData.ui && allData.ui.length > 0) {
1021
+ const uiSheet = XLSX.utils.json_to_sheet(allData.ui.map(ui => ({
1022
+ 'Teste': ui.testName || 'N/A',
1023
+ 'Ação': ui.action || 'N/A',
1024
+ 'Elemento': ui.element || 'N/A',
1025
+ 'Página': ui.page || 'N/A',
1026
+ 'Sucesso': ui.success ? 'Sim' : 'Não',
1027
+ 'Duração (ms)': ui.duration || 0,
1028
+ 'Timestamp': new Date(ui.timestamp).toLocaleString('pt-BR')
1029
+ })))
1030
+ XLSX.utils.book_append_sheet(workbook, uiSheet, 'UI')
1031
+ hasData = true
1032
+ }
1033
+
1034
+ // Sheet Mobile
1035
+ if (allData.mobile && allData.mobile.length > 0) {
1036
+ const mobileSheet = XLSX.utils.json_to_sheet(allData.mobile.map(mobile => ({
1037
+ 'Teste': mobile.testName || 'N/A',
1038
+ 'Ação': mobile.action || 'N/A',
1039
+ 'Elemento': mobile.element || 'N/A',
1040
+ 'Device': mobile.device || 'N/A',
1041
+ 'Platform': mobile.platform || 'N/A',
1042
+ 'Timestamp': new Date(mobile.timestamp).toLocaleString('pt-BR')
1043
+ })))
1044
+ XLSX.utils.book_append_sheet(workbook, mobileSheet, 'Mobile')
1045
+ hasData = true
1046
+ }
1047
+
1048
+ // Verificar se há dados
1049
+ if (!hasData) {
1050
+ alert('⚠️ Nenhum dado disponível para exportar. Execute testes primeiro!')
1051
+ return
1052
+ }
1053
+
1054
+ // Gerar arquivo
1055
+ XLSX.writeFile(workbook, 'autodocs-inteligente.xlsx')
1056
+ console.log('✅ XLSX gerado com sucesso!')
1057
+ } catch (error) {
1058
+ console.error('❌ Erro ao gerar XLSX:', error)
1059
+ alert('❌ Erro ao gerar XLSX: ' + error.message)
1060
+ }
1061
+ }
1062
+
1063
+ // ✅ NOVA FUNÇÃO: Carregar e renderizar CNs e CTs do ct-execution-data.json
1064
+ async function loadCtData() {
1065
+ try {
1066
+ const response = await fetch('./ct-execution-data.json')
1067
+ if (!response.ok) {
1068
+ document.getElementById('cn-container').innerHTML = \`
1069
+ <div class="bg-yellow-900/20 border border-yellow-600 rounded p-4">
1070
+ <p class="text-yellow-400">⚠️ Nenhum arquivo <strong>ct-execution-data.json</strong> encontrado.</p>
1071
+ <p class="text-gray-400 text-sm mt-2">Execute testes primeiro para gerar os dados.</p>
1072
+ </div>
1073
+ \`
1074
+ return
1075
+ }
1076
+
1077
+ const ctData = await response.json()
1078
+
1079
+ // ✅ SUPORTAR ESTRUTURA NOVA: { tests: [{ cts: [...] }] }
1080
+ let ctArray = []
1081
+ if (ctData.tests && Array.isArray(ctData.tests)) {
1082
+ // Nova estrutura: extrair todos os CTs de todos os testes
1083
+ ctData.tests.forEach(test => {
1084
+ if (test.cts && Array.isArray(test.cts)) {
1085
+ ctArray.push(...test.cts.map(ct => ({
1086
+ ...ct,
1087
+ cn: test.testName || ct.cn || 'Sem CN'
1088
+ })))
1089
+ }
1090
+ })
1091
+ } else {
1092
+ // Estrutura antiga (fallback): Object.values
1093
+ ctArray = Object.values(ctData)
1094
+ }
1095
+
1096
+ if (ctArray.length === 0) {
1097
+ document.getElementById('cn-container').innerHTML = \`
1098
+ <div class="bg-gray-800 rounded p-4">
1099
+ <p class="text-gray-400">📊 Nenhum CT executado ainda.</p>
1100
+ </div>
1101
+ \`
1102
+ return
1103
+ }
1104
+
1105
+ // Agrupar CTs por CN (Cenário de Negócio)
1106
+ const cnMap = {}
1107
+ ctArray.forEach(ct => {
1108
+ const cnName = ct.cn || 'Sem CN'
1109
+ if (!cnMap[cnName]) {
1110
+ cnMap[cnName] = {
1111
+ name: cnName,
1112
+ cts: [],
1113
+ totalPassed: 0,
1114
+ totalFailed: 0,
1115
+ totalSkipped: 0,
1116
+ totalRunning: 0,
1117
+ totalDuration: 0
1118
+ }
1119
+ }
1120
+ cnMap[cnName].cts.push(ct)
1121
+ if (ct.status === 'passed') cnMap[cnName].totalPassed++
1122
+ if (ct.status === 'failed') cnMap[cnName].totalFailed++
1123
+ if (ct.status === 'skipped') cnMap[cnName].totalSkipped++
1124
+ if (ct.status === 'running') cnMap[cnName].totalRunning++
1125
+ cnMap[cnName].totalDuration += (ct.duration || 0)
1126
+ })
1127
+
1128
+ // Renderizar CNs
1129
+ const cnHtml = Object.values(cnMap).map((cn, cnIdx) => \`
1130
+ <div class="bg-slate-800 rounded-lg p-4 border-l-4 \${cn.totalFailed > 0 ? 'border-red-500' : cn.totalPassed === cn.cts.length ? 'border-green-500' : 'border-yellow-500'}">
1131
+ <div class="flex items-start justify-between mb-3">
1132
+ <div class="flex-1">
1133
+ <h4 class="text-lg font-bold text-cyan-400">\${cnIdx + 1}. \${cn.name}</h4>
1134
+ <div class="flex gap-3 mt-2 text-sm">
1135
+ <span class="text-green-400">✅ \${cn.totalPassed} Passed</span>
1136
+ <span class="text-red-400">❌ \${cn.totalFailed} Failed</span>
1137
+ <span class="text-yellow-400">⚠️ \${cn.totalSkipped} Skipped</span>
1138
+ \${cn.totalRunning > 0 ? \`<span class="text-blue-400">🔄 \${cn.totalRunning} Running</span>\` : ''}
1139
+ <span class="text-gray-400">⏱️ \${cn.totalDuration}ms total</span>
1140
+ </div>
1141
+ </div>
1142
+ <button onclick="toggleCn('cn-\${cnIdx}')" id="btn-cn-\${cnIdx}" class="btn text-xs" type="button">📋 Ver CTs (\${cn.cts.length})</button>
1143
+ </div>
1144
+ <div id="cn-\${cnIdx}" style="display: none;" class="space-y-2 mt-4">
1145
+ \${cn.cts.map((ct, ctIdx) => \`
1146
+ <div class="bg-slate-700 rounded p-3">
1147
+ <div class="flex items-start justify-between">
1148
+ <div class="flex-1">
1149
+ <div class="flex items-center gap-2">
1150
+ <span class="\${ct.status === 'passed' ? 'text-green-400' : ct.status === 'failed' ? 'text-red-400' : ct.status === 'skipped' ? 'text-yellow-400' : 'text-blue-400'} text-lg">
1151
+ \${ct.status === 'passed' ? '✅' : ct.status === 'failed' ? '❌' : ct.status === 'skipped' ? '⚠️' : '🔄'}
1152
+ </span>
1153
+ <span class="font-mono text-sm text-yellow-300">\${ct.id || 'CT' + (ctIdx + 1)}</span>
1154
+ <span class="text-gray-300">\${ct.name}</span>
1155
+ </div>
1156
+ <div class="flex items-center gap-3 mt-2 text-xs">
1157
+ <span class="\${ct.duration > 5000 ? 'text-red-400' : ct.duration > 2000 ? 'text-yellow-400' : 'text-green-400'}">
1158
+ ⏱️ \${ct.duration || 0}ms
1159
+ </span>
1160
+ \${ct.statements && ct.statements.length > 0 ? \`<span class="text-cyan-400">📝 \${ct.statements.length} Statements</span>\` : ''}
1161
+ \${ct.executedActions && ct.executedActions.length > 0 ? \`<span class="text-purple-400">🎬 \${ct.executedActions.length} Actions</span>\` : ''}
1162
+ </div>
1163
+ </div>
1164
+ </div>
1165
+ </div>
1166
+ \`).join('')}
1167
+ </div>
1168
+ </div>
1169
+ \`).join('')
1170
+
1171
+ document.getElementById('cn-container').innerHTML = cnHtml
1172
+ } catch (error) {
1173
+ console.error('Erro ao carregar ct-execution-data.json:', error)
1174
+ document.getElementById('cn-container').innerHTML = \`
1175
+ <div class="bg-red-900/20 border border-red-600 rounded p-4">
1176
+ <p class="text-red-400">❌ Erro ao carregar dados dos CTs.</p>
1177
+ <p class="text-gray-400 text-sm mt-2">\${error.message}</p>
1178
+ </div>
1179
+ \`
1180
+ }
1181
+ }
1182
+
1183
+ function toggleCn(cnId) {
1184
+ const section = document.getElementById(cnId)
1185
+ const btn = document.getElementById('btn-' + cnId)
1186
+ if (section && btn) {
1187
+ if (section.style.display === 'none') {
1188
+ section.style.display = 'block'
1189
+ btn.innerHTML = btn.innerHTML.replace('Ver CTs', 'Ocultar CTs')
1190
+ } else {
1191
+ section.style.display = 'none'
1192
+ btn.innerHTML = btn.innerHTML.replace('Ocultar CTs', 'Ver CTs')
1193
+ }
1194
+ }
1195
+ }
1196
+
1197
+ // Carregar CTs automaticamente ao abrir a página
1198
+ loadCtData()
1199
+
1200
+ // Load saved theme
1201
+ const savedTheme = localStorage.getItem('theme');
1202
+ if (savedTheme === 'light') {
1203
+ document.getElementById('toggle-theme').click()
1204
+ }
1205
+ </script>
1206
+ </body>
1207
+ </html>`;
1208
+ }
1209
+ /**
1210
+ * 🤖 Detectar tipo de projeto automaticamente
1211
+ */
1212
+ static detectProjectType(data) {
1213
+ const hasAPIs = data.apis && data.apis.length > 0;
1214
+ const hasSSH = data.ssh && data.ssh.length > 0;
1215
+ const hasDB = data.db && data.db.length > 0;
1216
+ const hasUI = data.ui && data.ui.length > 0;
1217
+ const hasMobile = data.mobile && data.mobile.length > 0;
1218
+ const components = [];
1219
+ if (hasAPIs)
1220
+ components.push('API');
1221
+ if (hasSSH)
1222
+ components.push('SSH');
1223
+ if (hasDB)
1224
+ components.push('Banco');
1225
+ if (hasUI)
1226
+ components.push('Frontend');
1227
+ if (hasMobile)
1228
+ components.push('Mobile');
1229
+ if (components.length === 1)
1230
+ return components[0];
1231
+ if (components.length > 1)
1232
+ return 'Mixed';
1233
+ return 'Unknown';
1234
+ }
1235
+ /**
1236
+ * 🔧 Detectar tipo de query SQL
1237
+ */
1238
+ static detectQueryType(query) {
1239
+ const upperQuery = query.trim().toUpperCase();
1240
+ if (upperQuery.startsWith('SELECT'))
1241
+ return 'SELECT';
1242
+ if (upperQuery.startsWith('INSERT'))
1243
+ return 'INSERT';
1244
+ if (upperQuery.startsWith('UPDATE'))
1245
+ return 'UPDATE';
1246
+ if (upperQuery.startsWith('DELETE'))
1247
+ return 'DELETE';
1248
+ if (upperQuery.startsWith('CREATE'))
1249
+ return 'DDL';
1250
+ if (upperQuery.startsWith('DROP'))
1251
+ return 'DDL';
1252
+ if (upperQuery.startsWith('ALTER'))
1253
+ return 'DDL';
1254
+ return 'OTHER';
1255
+ }
1256
+ /**
1257
+ * 🔧 Verificar se há dados para gerar
1258
+ */
1259
+ static hasDataToGenerate(data) {
1260
+ return (data.apis.length > 0 ||
1261
+ data.ssh.length > 0 ||
1262
+ data.db.length > 0 ||
1263
+ data.ui.length > 0 ||
1264
+ data.mobile.length > 0);
1265
+ }
1266
+ /**
1267
+ * 📝 Obter nome do teste atual
1268
+ */
1269
+ static getCurrentTestName() {
1270
+ try {
1271
+ // Definir tipos para as propriedades globais
1272
+ const globalAny = global;
1273
+ // Para Playwright Test Runner
1274
+ if (globalAny.testInfo?.title) {
1275
+ return globalAny.testInfo.title;
1276
+ }
1277
+ // Para Jest/Vitest
1278
+ if (globalAny.expect?.getState) {
1279
+ const state = globalAny.expect.getState();
1280
+ if (state?.currentTestName) {
1281
+ return state.currentTestName;
1282
+ }
1283
+ }
1284
+ // Para WebDriverIO
1285
+ if (globalAny.browser?.testName) {
1286
+ return globalAny.browser.testName;
1287
+ }
1288
+ // Fallback genérico
1289
+ const stack = new Error('test-name-detection').stack;
1290
+ if (stack) {
1291
+ const testMatch = stack.match(/at.*\/(.*\.(?:test|spec)\.(?:js|ts))/);
1292
+ if (testMatch?.[1]) {
1293
+ return testMatch[1];
1294
+ }
1295
+ }
1296
+ return 'unknown-test';
1297
+ }
1298
+ catch {
1299
+ return 'unknown-test';
1300
+ }
1301
+ }
1302
+ /**
1303
+ * 📊 Gerar seções agrupadas inteligentemente
1304
+ */
1305
+ static generateGroupedSections(data) {
1306
+ const sections = [];
1307
+ // Agrupar por prioridade e relevância
1308
+ const groupOrder = [
1309
+ {
1310
+ key: 'apis',
1311
+ data: data.apis || [],
1312
+ title: '🌐 APIs & Endpoints',
1313
+ generator: 'generateAPISection',
1314
+ },
1315
+ {
1316
+ key: 'ssh',
1317
+ data: data.ssh || [],
1318
+ title: '🖥️ Comandos SSH',
1319
+ generator: 'generateSSHSection',
1320
+ },
1321
+ {
1322
+ key: 'db',
1323
+ data: data.db || [],
1324
+ title: '🗄️ Consultas Database',
1325
+ generator: 'generateDBSection',
1326
+ },
1327
+ {
1328
+ key: 'ui',
1329
+ data: data.ui || [],
1330
+ title: '🎨 Interface de Usuário',
1331
+ generator: 'generateUISection',
1332
+ },
1333
+ {
1334
+ key: 'mobile',
1335
+ data: data.mobile || [],
1336
+ title: '📱 Ações Mobile',
1337
+ generator: 'generateMobileSection',
1338
+ },
1339
+ ];
1340
+ let hasAnyData = false;
1341
+ const groupedContent = groupOrder
1342
+ .map((group) => {
1343
+ if (group.data.length === 0)
1344
+ return '';
1345
+ hasAnyData = true;
1346
+ // Determinar estatísticas do grupo
1347
+ const successCount = group.data.filter((item) => item.success !== false).length;
1348
+ const successRate = ((successCount / group.data.length) * 100).toFixed(1);
1349
+ // Agrupar por teste se disponível
1350
+ const byTest = group.data.reduce((acc, item) => {
1351
+ const testName = item.testName || 'Teste não identificado';
1352
+ if (!acc[testName])
1353
+ acc[testName] = [];
1354
+ acc[testName].push(item);
1355
+ return acc;
1356
+ }, {});
1357
+ const testCount = Object.keys(byTest).length;
1358
+ return `
1359
+ <div class="card mb-8" style="border-left: 4px solid ${AutoDocs.getGroupColor(group.key)}">
1360
+ <div class="flex items-center justify-between mb-6">
1361
+ <div>
1362
+ <h2 class="text-2xl font-bold mb-2" style="color: ${AutoDocs.getGroupColor(group.key)}">
1363
+ ${group.title}
1364
+ </h2>
1365
+ <div class="flex items-center gap-4 text-sm text-gray-400">
1366
+ <span>📊 ${group.data.length} itens</span>
1367
+ <span>🧪 ${testCount} teste${testCount > 1 ? 's' : ''}</span>
1368
+ <span class="flex items-center gap-1">
1369
+ <span class="${successRate === '100.0' ? 'text-green-400' : Number.parseFloat(successRate) >= 80 ? 'text-yellow-400' : 'text-red-400'}">
1370
+ ${successRate === '100.0' ? '✅' : Number.parseFloat(successRate) >= 80 ? '⚠️' : '❌'} ${successRate}% sucesso
1371
+ </span>
1372
+ </span>
1373
+ </div>
1374
+ </div>
1375
+ <button onclick="toggleSection('${group.key}')" class="btn text-sm">
1376
+ <span id="btn-${group.key}">📋 Detalhes</span>
1377
+ </button>
1378
+ </div>
1379
+
1380
+ <!-- Resumo por Teste -->
1381
+ <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 mb-6">
1382
+ ${Object.entries(byTest)
1383
+ .map(([testName, items]) => {
1384
+ const totalDuration = items.reduce((sum, item) => sum + (item.duration || 0), 0);
1385
+ return `
1386
+ <div class="bg-gray-800 rounded-lg p-4">
1387
+ <h4 class="font-semibold text-blue-400 mb-2 truncate" title="${testName}">
1388
+ 🧪 ${testName}
1389
+ </h4>
1390
+ <div class="text-sm text-gray-300">
1391
+ <div class="flex justify-between mb-1">
1392
+ <span>Total:</span>
1393
+ <span class="font-mono">${items.length}</span>
1394
+ </div>
1395
+ <div class="flex justify-between mb-1">
1396
+ <span>Sucesso:</span>
1397
+ <span class="font-mono text-green-400">${items.filter((i) => i.success !== false).length}</span>
1398
+ </div>
1399
+ <div class="flex justify-between mb-1">
1400
+ <span>Taxa:</span>
1401
+ <span class="font-mono ${((items.filter((i) => i.success !== false).length / items.length) * 100) >= 80 ? 'text-green-400' : 'text-yellow-400'}">
1402
+ ${((items.filter((i) => i.success !== false).length / items.length) * 100).toFixed(1)}%
1403
+ </span>
1404
+ </div>
1405
+ <div class="flex justify-between">
1406
+ <span>⏱️ Duração:</span>
1407
+ <span class="font-mono text-blue-400">${(totalDuration / 1000).toFixed(2)}s</span>
1408
+ </div>
1409
+ </div>
1410
+ </div>
1411
+ `;
1412
+ })
1413
+ .join('')}
1414
+ </div>
1415
+
1416
+ <!-- Seção detalhada (colapsável) -->
1417
+ <div id="section-${group.key}" style="display: none;">
1418
+ ${AutoDocs[group.generator](group.data)}
1419
+ </div>
1420
+ </div>
1421
+ `;
1422
+ })
1423
+ .filter((section) => section !== '');
1424
+ if (!hasAnyData) {
1425
+ return `
1426
+ <div class="card text-center py-12">
1427
+ <div class="text-6xl mb-4">📄</div>
1428
+ <h2 class="text-2xl font-bold text-gray-400 mb-2">Nenhum dado encontrado</h2>
1429
+ <p class="text-gray-500">Execute alguns testes para ver a documentação automática aparecer aqui!</p>
1430
+ </div>
1431
+ `;
1432
+ }
1433
+ return groupedContent.join('');
1434
+ }
1435
+ /**
1436
+ * 🎨 Obter cor do grupo
1437
+ */
1438
+ static getGroupColor(groupKey) {
1439
+ const colors = {
1440
+ apis: '#10b981', // verde
1441
+ ssh: '#3b82f6', // azul
1442
+ db: '#8b5cf6', // roxo
1443
+ ui: '#f59e0b', // laranja
1444
+ mobile: '#ec4899', // rosa
1445
+ };
1446
+ return colors[groupKey] || '#6b7280';
1447
+ }
1448
+ /**
1449
+ * 📊 Gerar overview do projeto
1450
+ */
1451
+ static generateProjectOverview(projectType, data, detectedCategories) {
1452
+ const typeIcons = {
1453
+ API: '🌐',
1454
+ SSH: '🖥️',
1455
+ Banco: '🗄️',
1456
+ Frontend: '🎨',
1457
+ Mobile: '📱',
1458
+ Mixed: '🔧',
1459
+ Unknown: '❓',
1460
+ };
1461
+ const categoryCount = Object.keys(detectedCategories).length;
1462
+ const apiCategories = Object.entries(detectedCategories)
1463
+ .filter(([key]) => key !== 'outras')
1464
+ .map(([key, data]) => `${data.icon} ${key.charAt(0).toUpperCase() + key.slice(1)}`)
1465
+ .join(', ');
1466
+ return `
1467
+ <div class="card mb-6">
1468
+ <h2 class="text-2xl font-bold text-blue-400 mb-4 flex items-center gap-3">
1469
+ ${typeIcons[projectType] || '🔧'} Project Overview - ${projectType}
1470
+ </h2>
1471
+ <div class="grid grid-cols-1 md:grid-cols-2 gap-6">
1472
+ <div>
1473
+ <h3 class="text-lg font-semibold text-green-400 mb-3">📋 Componentes Detectados</h3>
1474
+ <div class="space-y-2">
1475
+ ${data.apis.length > 0 ? `<div class="flex items-center gap-2"><span class="text-green-400">✅</span> APIs (${data.apis.length})</div>` : ''}
1476
+ ${data.ssh.length > 0 ? `<div class="flex items-center gap-2"><span class="text-green-400">✅</span> SSH (${data.ssh.length})</div>` : ''}
1477
+ ${data.db.length > 0 ? `<div class="flex items-center gap-2"><span class="text-green-400">✅</span> Banco (${data.db.length})</div>` : ''}
1478
+ ${data.ui.length > 0 ? `<div class="flex items-center gap-2"><span class="text-green-400">✅</span> Frontend (${data.ui.length})</div>` : ''}
1479
+ ${data.mobile.length > 0 ? `<div class="flex items-center gap-2"><span class="text-green-400">✅</span> Mobile (${data.mobile.length})</div>` : ''}
1480
+ </div>
1481
+ </div>
1482
+ ${data.apis.length > 0
1483
+ ? `
1484
+ <div>
1485
+ <h3 class="text-lg font-semibold text-purple-400 mb-3">🤖 Categorias API Detectadas</h3>
1486
+ <p class="text-slate-400 text-sm mb-2">Sistema inteligente detectou ${categoryCount} categorias:</p>
1487
+ <div class="text-sm text-slate-300">
1488
+ ${apiCategories || 'Nenhuma categoria específica detectada'}
1489
+ </div>
1490
+ </div>
1491
+ `
1492
+ : ''}
1493
+ </div>
1494
+ </div>
1495
+ `;
1496
+ }
1497
+ /**
1498
+ * 🧭 Gerar menu de navegação
1499
+ */
1500
+ static generateNavigationMenu(data) {
1501
+ const sections = [];
1502
+ if (data.apis.length > 0)
1503
+ sections.push({ id: 'apis', name: '🌐 APIs', count: data.apis.length });
1504
+ if (data.ssh.length > 0)
1505
+ sections.push({ id: 'ssh', name: '🖥️ SSH', count: data.ssh.length });
1506
+ if (data.db.length > 0)
1507
+ sections.push({ id: 'database', name: '🗄️ Banco', count: data.db.length });
1508
+ if (data.ui.length > 0)
1509
+ sections.push({
1510
+ id: 'frontend',
1511
+ name: '🎨 Frontend',
1512
+ count: data.ui.length,
1513
+ });
1514
+ if (data.mobile.length > 0)
1515
+ sections.push({
1516
+ id: 'mobile',
1517
+ name: '📱 Mobile',
1518
+ count: data.mobile.length,
1519
+ });
1520
+ if (sections.length === 0)
1521
+ return '';
1522
+ return `
1523
+ <div class="card mb-6">
1524
+ <h3 class="text-lg font-semibold text-yellow-400 mb-4">🧭 Navegação Rápida</h3>
1525
+ <div class="flex flex-wrap gap-3">
1526
+ ${sections
1527
+ .map((section) => `
1528
+ <button onclick="jumpToSection('${section.id}')" class="btn bg-slate-700 hover:bg-slate-600">
1529
+ ${section.name} (${section.count})
1530
+ </button>
1531
+ `)
1532
+ .join('')}
1533
+ </div>
1534
+ </div>
1535
+ `;
1536
+ }
1537
+ /**
1538
+ * 📊 Gerar estatísticas gerais
1539
+ */
1540
+ static generateOverallStats(data) {
1541
+ const totalItems = data.apis.length +
1542
+ data.ssh.length +
1543
+ data.db.length +
1544
+ data.ui.length +
1545
+ data.mobile.length;
1546
+ const successfulAPIs = data.apis.filter((api) => api.success).length;
1547
+ const failedAPIs = data.apis.length - successfulAPIs;
1548
+ const successRate = data.apis.length > 0
1549
+ ? Math.round((successfulAPIs / data.apis.length) * 100)
1550
+ : 0;
1551
+ return `
1552
+ <div class="card mb-8">
1553
+ <h3 class="text-2xl font-bold text-green-400 mb-6">📊 Estatísticas Gerais do Projeto</h3>
1554
+ <div class="grid grid-cols-1 md:grid-cols-4 gap-4">
1555
+ <div class="bg-blue-800 rounded-lg p-4 text-center">
1556
+ <div class="text-3xl font-bold text-white">${totalItems}</div>
1557
+ <div class="text-blue-200 text-sm">Total de Testes</div>
1558
+ </div>
1559
+ <div class="bg-green-800 rounded-lg p-4 text-center">
1560
+ <div class="text-3xl font-bold text-white">${successfulAPIs}</div>
1561
+ <div class="text-green-200 text-sm">APIs Sucesso</div>
1562
+ </div>
1563
+ <div class="bg-red-800 rounded-lg p-4 text-center">
1564
+ <div class="text-3xl font-bold text-white">${failedAPIs}</div>
1565
+ <div class="text-red-200 text-sm">APIs Falha</div>
1566
+ </div>
1567
+ <div class="bg-purple-800 rounded-lg p-4 text-center">
1568
+ <div class="text-3xl font-bold text-white">${successRate}%</div>
1569
+ <div class="text-purple-200 text-sm">Taxa de Sucesso</div>
1570
+ </div>
1571
+ </div>
1572
+ </div>
1573
+ `;
1574
+ }
1575
+ /**
1576
+ <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-5 gap-6 mb-8">
1577
+ <div class="card bg-gradient-to-br from-green-600 to-green-700">
1578
+ <h3 class="text-3xl font-bold text-white">${data.apis.length}</h3>
1579
+ <p class="text-green-100">🌐 APIs Documentadas</p>
1580
+ </div>
1581
+ <div class="card bg-gradient-to-br from-blue-600 to-blue-700">
1582
+ <h3 class="text-3xl font-bold text-white">${data.ssh.length}</h3>
1583
+ <p class="text-blue-100">🖥️ Comandos SSH</p>
1584
+ </div>
1585
+ <div class="card bg-gradient-to-br from-purple-600 to-purple-700">
1586
+ <h3 class="text-3xl font-bold text-white">${data.db.length}</h3>
1587
+ <p class="text-purple-100">🗄️ Queries Database</p>
1588
+ </div>
1589
+ <div class="card bg-gradient-to-br from-orange-600 to-orange-700">
1590
+ <h3 class="text-3xl font-bold text-white">${data.ui.length}</h3>
1591
+ <p class="text-orange-100">🎨 Ações UI</p>
1592
+ </div>
1593
+ <div class="card bg-gradient-to-br from-pink-600 to-pink-700">
1594
+ <h3 class="text-3xl font-bold text-white">${data.mobile.length}</h3>
1595
+ <p class="text-pink-100">📱 Ações Mobile</p>
1596
+ </div>
1597
+ </div>
1598
+
1599
+ ${AutoDocs.generateAPISection(data.apis)}
1600
+ ${AutoDocs.generateSSHSection(data.ssh)}
1601
+ ${AutoDocs.generateDBSection(data.db)}
1602
+ ${AutoDocs.generateUISection(data.ui)}
1603
+ ${AutoDocs.generateMobileSection(data.mobile)}
1604
+
1605
+ <!-- Footer -->
1606
+ <div class="footer-bar">
1607
+ <p>Documentação gerada automaticamente pelo AutoDocs (${projectVersion}) em ${new Date().toLocaleString('pt-BR')}</p>
1608
+ </div>
1609
+ </div>
1610
+
1611
+ <script>
1612
+ function toggleTheme() {
1613
+ document.body.classList.toggle('light-mode');
1614
+ localStorage.setItem('theme', document.body.classList.contains('light-mode') ? 'light' : 'dark');
1615
+ }
1616
+
1617
+ function exportData(format) {
1618
+ const data = ${JSON.stringify(data)};
1619
+
1620
+ if (format === 'pdf') {
1621
+ const element = document.querySelector('.container');
1622
+
1623
+ html2canvas(element, {
1624
+ scale: 2,
1625
+ useCORS: false,
1626
+ allowTaint: true,
1627
+ backgroundColor: '#fff'
1628
+ }).then(canvas => {
1629
+ const { jsPDF } = jspdf;
1630
+ const pdf = new jsPDF({
1631
+ orientation: 'portrait',
1632
+ unit: 'in',
1633
+ format: 'letter'
1634
+ });
1635
+
1636
+ const imgData = canvas.toDataURL('image/jpeg', 0.98);
1637
+ const imgWidth = 8.5;
1638
+ const pageHeight = 11;
1639
+ const imgHeight = (canvas.height * imgWidth) / canvas.width;
1640
+ let heightLeft = imgHeight;
1641
+ let position = 0;
1642
+
1643
+ pdf.addImage(imgData, 'JPEG', 0, position, imgWidth, imgHeight);
1644
+ heightLeft -= pageHeight;
1645
+
1646
+ while (heightLeft > 0) {
1647
+ position = heightLeft - imgHeight;
1648
+ pdf.addPage();
1649
+ pdf.addImage(imgData, 'JPEG', 0, position, imgWidth, imgHeight);
1650
+ heightLeft -= pageHeight;
1651
+ }
1652
+
1653
+ pdf.save('${AutoDocs.config.projectName}_autodocs.pdf');
1654
+ });
1655
+ } else if (format === 'json') {
1656
+ const dataStr = JSON.stringify(data, null, 2);
1657
+ const dataBlob = new Blob([dataStr], {type: 'application/json'});
1658
+ const url = URL.createObjectURL(dataBlob);
1659
+ const link = document.createElement('a');
1660
+ link.href = url;
1661
+ link.download = '${AutoDocs.config.projectName}_autodocs.json';
1662
+ link.click();
1663
+ }
1664
+ }
1665
+
1666
+ // Load saved theme
1667
+ const savedTheme = localStorage.getItem('theme');
1668
+ if (savedTheme === 'light') {
1669
+ document.body.classList.add('light-mode');
1670
+ }
1671
+ </script>
1672
+ </body>
1673
+ </html>`
1674
+ }
1675
+
1676
+ /**
1677
+ * 📊 Gerar visualização por CTs (Cenários de Teste)
1678
+ * CORRIGIDO: Usar a mesma estratégia do UnifiedReportManager
1679
+ */
1680
+ static async generateCTViewHTMLContent(data) {
1681
+ // 🔧 CORREÇÃO: Usar a mesma abordagem do UnifiedReportManager
1682
+ Logger.info('📊 HubDocs: Coletando dados dos CTs...');
1683
+ // Busca direta do StatementTracker (mesma estratégia do UnifiedReportManager)
1684
+ const allExecutions = StatementTracker.getExecutions();
1685
+ const totalTests = Array.from(allExecutions.values()).length;
1686
+ Logger.info(`📊 HubDocs: Total de testes encontrados: ${totalTests}`);
1687
+ let totalCTs = 0;
1688
+ let passedCTs = 0;
1689
+ let failedCTs = 0;
1690
+ let skippedCTs = 0;
1691
+ let runningCTs = 0;
1692
+ const ctsByTest = new Map();
1693
+ if (totalTests > 0) {
1694
+ Logger.success('✅ HubDocs: Processando testes e seus CTs...');
1695
+ // ✅ CORREÇÃO: Cada execution tem um array de CTs dentro
1696
+ Array.from(allExecutions.values()).forEach((testExecution) => {
1697
+ const testName = testExecution.testName || 'Teste Geral';
1698
+ const cts = testExecution.cts || [];
1699
+ Logger.info(`📋 HubDocs: Teste "${testName}" tem ${cts.length} CTs`);
1700
+ if (!ctsByTest.has(testName)) {
1701
+ ctsByTest.set(testName, []);
1702
+ }
1703
+ // Processar cada CT do teste
1704
+ cts.forEach((ct, ctIndex) => {
1705
+ const ctData = {
1706
+ ctId: `CT${(totalCTs + 1).toString().padStart(3, '0')}`,
1707
+ ctName: ct.name || ct.ctName || 'Unknown CT',
1708
+ ctOrder: ctIndex + 1,
1709
+ status: ct.status,
1710
+ duration: ct.duration || 0,
1711
+ timestamp: ct.startTime
1712
+ ? new Date(ct.startTime).toISOString()
1713
+ : new Date().toISOString(),
1714
+ error: ct.error,
1715
+ className: ct.name?.split('.')[0] || 'Unknown',
1716
+ methodName: ct.name?.split('.')[1] || ct.name || 'unknown',
1717
+ statements: ct.statements || [], // ✅ NOVO: Incluir Statements
1718
+ executedActions: ct.executedActions || [], // ✅ NOVO: Incluir Actions do CT
1719
+ logs: ct.logs || [], // ✅ NOVO: Incluir Logs do CT
1720
+ };
1721
+ ctsByTest.get(testName).push(ctData);
1722
+ totalCTs++;
1723
+ // ✅ CORRIGIDO: Tratar "running" como "failed" para contadores
1724
+ const finalStatus = ct.status === 'running' ? 'failed' : ct.status;
1725
+ switch (finalStatus) {
1726
+ case 'passed':
1727
+ passedCTs++;
1728
+ break;
1729
+ case 'failed':
1730
+ failedCTs++;
1731
+ break;
1732
+ case 'skipped':
1733
+ skippedCTs++;
1734
+ break;
1735
+ }
1736
+ });
1737
+ });
1738
+ Logger.success(`✅ HubDocs CTs: Total=${totalCTs}, Passou=${passedCTs}, Falhou=${failedCTs}, Skipped=${skippedCTs}`);
1739
+ }
1740
+ else {
1741
+ Logger.warning('⚠️ HubDocs: Nenhum teste encontrado no StatementTracker');
1742
+ }
1743
+ const timestamp = new Date().toLocaleString('pt-BR');
1744
+ return `<!DOCTYPE html>
1745
+ <html lang="pt-BR">
1746
+ <head>
1747
+ <meta charset="UTF-8">
1748
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
1749
+ <title>📋 ${AutoDocs.config.projectName} - Visualização por CTs</title>
1750
+ <script src="https://cdn.tailwindcss.com"></script>
1751
+ <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
1752
+ <style>
1753
+ body { font-family: 'Inter', sans-serif; background-color: #0f172a; color: #f1f5f9; min-height: 100vh; }
1754
+ .light-mode { background-color: #f9fafb !important; color: #111827 !important; }
1755
+ .card { background: #1e293b; border-radius: 1rem; padding: 1.5rem; box-shadow: 0 2px 8px #0002; color: #f1f5f9; }
1756
+ .light-mode .card { background: #fff; color: #111827; border: 1px solid #e5e7eb; }
1757
+ .btn { background: #334155; color: #fff; border: none; border-radius: 8px; padding: 0.5rem 1.2rem; font-weight: 600; cursor: pointer; margin: 0.25rem; transition: background 0.2s; }
1758
+ .btn:hover { background: #475569; }
1759
+ .light-mode .btn { background: #374151; color: #fff; }
1760
+ .status-pass { color: #22c55e; font-weight: 700; }
1761
+ .status-fail { color: #ef4444; font-weight: 700; }
1762
+ .status-skip { color: #fbbf24; font-weight: 700; }
1763
+ .status-running { color: #8b5cf6; font-weight: 700; }
1764
+ .ct-card { background: #1e293b; border-radius: 0.5rem; padding: 1rem; margin: 0.5rem 0; border-left: 4px solid #3b82f6; }
1765
+ .ct-card.passed { border-left-color: #22c55e; }
1766
+ .ct-card.failed { border-left-color: #ef4444; }
1767
+ .ct-card.skipped { border-left-color: #fbbf24; }
1768
+ .ct-card.running { border-left-color: #8b5cf6; }
1769
+ .light-mode .ct-card { background: #f8fafc; color: #111827; }
1770
+ .test-section { background: #0f172a; border-radius: 1rem; padding: 1.5rem; margin: 1rem 0; border: 1px solid #374151; }
1771
+ .light-mode .test-section { background: #ffffff; border-color: #e5e7eb; color: #111827; }
1772
+ .header-bar { background: #1e293b; padding: 1rem 2rem; border-radius: 1rem 1rem 0 0; margin-bottom: 1.5rem; display: flex; align-items: center; justify-content: space-between; }
1773
+ .header-bar .header-title { font-size: 2rem; font-weight: 700; color: #fbbf24; }
1774
+ .light-mode .header-bar { background: #f3f4f6; border: 1px solid #e5e7eb; }
1775
+ .light-mode .header-title { color: #d97706; }
1776
+ .btn-sm { padding: 0.25rem 0.75rem; font-size: 0.75rem; }
1777
+ .bg-blue-600 { background-color: #2563eb; }
1778
+ .bg-blue-600:hover { background-color: #1d4ed8; }
1779
+ .hidden { display: none; }
1780
+ .overflow-x-auto { overflow-x: auto; }
1781
+ .break-all { word-break: break-all; }
1782
+ .whitespace-pre-wrap { white-space: pre-wrap; }
1783
+ </style>
1784
+ </head>
1785
+ <body id="body-root">
1786
+ <div class="header-bar">
1787
+ <div class="header-title">📋 Visualização por CTs</div>
1788
+ <div class="flex gap-2">
1789
+ <button onclick="window.location.href='index.html'" class="btn">📚 Visão Tradicional</button>
1790
+ <button id="toggle-theme" class="btn">🌙 Tema</button>
1791
+ </div>
1792
+ </div>
1793
+
1794
+ <div class="max-w-7xl mx-auto p-6">
1795
+ <div class="grid grid-cols-1 md:grid-cols-4 gap-4 mb-8">
1796
+ <div class="card flex flex-col items-center">
1797
+ <span class="text-2xl font-bold">${totalCTs}</span>
1798
+ <span class="text-slate-400">Total CTs</span>
1799
+ </div>
1800
+ <div class="card flex flex-col items-center">
1801
+ <span class="text-2xl font-bold text-green-400">${passedCTs}</span>
1802
+ <span class="text-green-400">Passou</span>
1803
+ </div>
1804
+ <div class="card flex flex-col items-center">
1805
+ <span class="text-2xl font-bold text-red-400">${failedCTs}</span>
1806
+ <span class="text-red-400">Falhou</span>
1807
+ </div>
1808
+ <div class="card flex flex-col items-center">
1809
+ <span class="text-2xl font-bold text-yellow-400">${skippedCTs}</span>
1810
+ <span class="text-yellow-400">Pulado</span>
1811
+ </div>
1812
+ </div>
1813
+
1814
+ ${Array.from(ctsByTest.entries())
1815
+ .map(([testName, ctList]) => `
1816
+ <div class="test-section">
1817
+ <h2 class="text-2xl font-bold mb-4 text-blue-400">
1818
+ 🧪 ${testName}
1819
+ <span class="text-sm font-normal text-gray-400">
1820
+ (${ctList.filter((ct) => ct.status === 'passed').length}/${ctList.length} CTs - ${(ctList.reduce((sum, ct) => sum + (ct.duration || 0), 0) / 1000).toFixed(2)}s)
1821
+ </span>
1822
+ </h2>
1823
+
1824
+ <div class="grid grid-cols-1 gap-3">
1825
+ ${ctList
1826
+ .map((ct, index) => `
1827
+ <div class="ct-card ${ct.status}">
1828
+ <div class="flex justify-between items-start">
1829
+ <div class="flex-1">
1830
+ <div class="font-mono text-sm text-blue-300">CT #${ct.ctOrder || index + 1}</div>
1831
+ <div class="font-semibold text-lg mb-2">${AutoDocs.extractCleanCTName(ct.ctName)}</div>
1832
+ <div class="text-sm text-gray-300">
1833
+ <span class="mr-4">📦 ${ct.className}</span>
1834
+ <span>⏱️ ${((ct.duration || 0) / 1000).toFixed(2)}s</span>
1835
+ </div>
1836
+
1837
+ ${ct.statements && ct.statements.length > 0
1838
+ ? `
1839
+ <div class="mt-3 p-3 bg-slate-800 rounded">
1840
+ <div class="font-semibold text-sm mb-2 text-blue-300">📝 Statements (${ct.statements.length}):</div>
1841
+ ${ct.statements
1842
+ .map((stmt, stmtIdx) => `
1843
+ <div class="ml-4 text-xs text-slate-300 py-1 border-l-2 border-slate-600 pl-2">
1844
+ ${stmtIdx + 1}. ${stmt.className || ''}.${stmt.methodName || stmt.name || 'unknown'}
1845
+ <span class="ml-2 ${stmt.status === 'passed' ? 'text-green-400' : stmt.status === 'failed' ? 'text-red-400' : 'text-yellow-400'}">
1846
+ (${stmt.status === 'passed' ? '✅' : stmt.status === 'failed' ? '❌' : '⏳'} ${stmt.duration ? stmt.duration + 'ms' : '0ms'})
1847
+ </span>
1848
+ ${stmt.error ? `<div class="text-red-400 text-xs mt-1">❌ ${AutoDocs.escapeHtml(stmt.error)}</div>` : ''}
1849
+ </div>
1850
+ `)
1851
+ .join('')}
1852
+ </div>
1853
+ `
1854
+ : ''}
1855
+
1856
+ ${ct.executedActions &&
1857
+ ct.executedActions.length > 0
1858
+ ? `
1859
+ <div class="mt-3 p-3 bg-slate-800 rounded">
1860
+ <div class="font-semibold text-sm mb-2 text-purple-300">🎬 Actions (${ct.executedActions.length}):</div>
1861
+ ${ct.executedActions
1862
+ .map((action, actIdx) => `
1863
+ <div class="ml-4 text-xs text-slate-300 py-1">
1864
+ ${actIdx + 1}. [${action.type}] ${action.description}
1865
+ <span class="ml-2 ${action.success ? 'text-green-400' : 'text-red-400'}">
1866
+ (${action.success ? '✅' : '❌'} ${action.duration ? action.duration + 'ms' : ''})
1867
+ </span>
1868
+ </div>
1869
+ `)
1870
+ .join('')}
1871
+ </div>
1872
+ `
1873
+ : ''}
1874
+
1875
+ ${ct.logs && ct.logs.length > 0
1876
+ ? `
1877
+ <div class="mt-3 p-3 bg-slate-800 rounded">
1878
+ <div class="flex justify-between items-center mb-2">
1879
+ <div class="font-semibold text-sm text-cyan-300">📋 Logs (${ct.logs.length}):</div>
1880
+ <button
1881
+ onclick="toggleLogs('logs-${testName.replace(/[^a-zA-Z0-9]/g, '')}-${index}')"
1882
+ id="btn-logs-${testName.replace(/[^a-zA-Z0-9]/g, '')}-${index}"
1883
+ class="btn btn-sm bg-slate-600 hover:bg-slate-700 text-xs"
1884
+ >
1885
+ 👁️ Ver Logs
1886
+ </button>
1887
+ </div>
1888
+ <div id="logs-${testName.replace(/[^a-zA-Z0-9]/g, '')}-${index}" class="hidden max-h-96 overflow-y-auto">
1889
+ ${ct.logs
1890
+ .map((log, logIdx) => `
1891
+ <div class="text-xs text-slate-300 py-1 font-mono border-l-2 border-slate-600 pl-2 hover:bg-slate-700">
1892
+ <span class="text-slate-500">${logIdx + 1}.</span> ${AutoDocs.escapeHtml(log.replace('[LOG] ', ''))}
1893
+ </div>
1894
+ `)
1895
+ .join('')}
1896
+ </div>
1897
+ </div>
1898
+ `
1899
+ : ''}
1900
+
1901
+ ${ct.ctName && ct.ctName.length > 50
1902
+ ? `
1903
+ <div class="mt-3">
1904
+ <button
1905
+ onclick="toggleCTDetails('ct-details-${testName.replace(/[^a-zA-Z0-9]/g, '')}-${index}')"
1906
+ class="btn btn-sm bg-blue-600 hover:bg-blue-700 text-xs"
1907
+ >
1908
+ 📋 Ver Detalhes
1909
+ </button>
1910
+ <div id="ct-details-${testName.replace(/[^a-zA-Z0-9]/g, '')}-${index}" class="hidden mt-3 p-3 bg-slate-800 rounded border-l-4 border-blue-500">
1911
+ <div class="text-sm text-slate-300">
1912
+ <strong>🔍 Detalhes Completos:</strong>
1913
+ <pre class="mt-2 text-xs whitespace-pre-wrap break-all overflow-x-auto bg-slate-900 p-2 rounded">${AutoDocs.escapeHtml(ct.ctName)}</pre>
1914
+ </div>
1915
+ </div>
1916
+ </div>
1917
+ `
1918
+ : ''}
1919
+ </div>
1920
+ <div class="text-right">
1921
+ <div class="
1922
+ ${ct.status === 'passed'
1923
+ ? 'status-pass'
1924
+ : ct.status === 'failed'
1925
+ ? 'status-fail'
1926
+ : ct.status === 'skipped'
1927
+ ? 'status-skip'
1928
+ : 'status-running'}
1929
+ ">
1930
+ ${ct.status === 'passed'
1931
+ ? '✅ PASS'
1932
+ : ct.status === 'failed'
1933
+ ? '❌ FAIL'
1934
+ : ct.status === 'skipped'
1935
+ ? '⏭️ SKIP'
1936
+ : '🔄 RUNNING'}
1937
+ </div>
1938
+ </div>
1939
+ </div>
1940
+ ${ct.error
1941
+ ? `
1942
+ <div class="mt-3 p-3 bg-red-900 bg-opacity-50 rounded border-l-4 border-red-500">
1943
+ <div class="text-red-300 text-sm">
1944
+ <strong>❌ Erro:</strong> ${AutoDocs.escapeHtml(ct.error)}
1945
+ </div>
1946
+ </div>
1947
+ `
1948
+ : ''}
1949
+ </div>
1950
+ `)
1951
+ .join('')}
1952
+ </div>
1953
+ </div>
1954
+ `)
1955
+ .join('')}
1956
+
1957
+ ${totalCTs === 0
1958
+ ? `
1959
+ <div class="card text-center">
1960
+ <h3 class="text-xl font-bold mb-4">📋 Nenhum CT Encontrado</h3>
1961
+ <p class="text-gray-400 mb-4">Execute seus testes com statements para ver os CTs aqui.</p>
1962
+ <p class="text-sm text-gray-500">
1963
+ Os CTs são capturados automaticamente quando você usa:
1964
+ <code class="bg-gray-800 px-2 py-1 rounded">await new StatementXXX().method()</code>
1965
+ </p>
1966
+ </div>
1967
+ `
1968
+ : ''}
1969
+ </div>
1970
+
1971
+ <div class="bg-slate-800 text-amber-400 p-4 text-center mt-8">
1972
+ Desenvolvido por <strong>Test HUB</strong> | Visualização por CTs gerada em ${timestamp}
1973
+ </div>
1974
+
1975
+ <script>
1976
+ document.getElementById('toggle-theme').onclick = () => {
1977
+ document.getElementById('body-root').classList.toggle('light-mode')
1978
+ document.querySelector('.header-bar').classList.toggle('light-mode')
1979
+ document.querySelectorAll('.card').forEach(el => el.classList.toggle('light-mode'))
1980
+ document.querySelectorAll('.ct-card').forEach(el => el.classList.toggle('light-mode'))
1981
+ document.querySelectorAll('.test-section').forEach(el => el.classList.toggle('light-mode'))
1982
+ }
1983
+
1984
+ // Função para expandir/recolher detalhes do CT
1985
+ function toggleCTDetails(elementId) {
1986
+ const element = document.getElementById(elementId)
1987
+ const button = element?.previousElementSibling
1988
+
1989
+ if (element) {
1990
+ if (element.classList.contains('hidden')) {
1991
+ element.classList.remove('hidden')
1992
+ if (button) button.textContent = '📋 Ocultar Detalhes'
1993
+ } else {
1994
+ element.classList.add('hidden')
1995
+ if (button) button.textContent = '📋 Ver Detalhes'
1996
+ }
1997
+ }
1998
+ }
1999
+
2000
+ // ✅ NOVO: Função para expandir/recolher logs do CT
2001
+ function toggleLogs(elementId) {
2002
+ const element = document.getElementById(elementId)
2003
+ const button = document.getElementById('btn-' + elementId)
2004
+
2005
+ if (element && button) {
2006
+ if (element.classList.contains('hidden')) {
2007
+ element.classList.remove('hidden')
2008
+ button.textContent = '👁️ Ocultar Logs'
2009
+ } else {
2010
+ element.classList.add('hidden')
2011
+ button.textContent = '👁️ Ver Logs'
2012
+ }
2013
+ }
2014
+ }
2015
+ </script>
2016
+ </body>
2017
+ </html>`;
2018
+ }
2019
+ /**
2020
+ * �🎨 Gerar conteúdo HTML vazio
2021
+ */
2022
+ static generateEmptyHTMLContent() {
2023
+ const timestamp = new Date().toISOString();
2024
+ return `<!DOCTYPE html>
2025
+ <html lang="pt-BR">
2026
+ <head>
2027
+ <meta charset="UTF-8">
2028
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
2029
+ <title>📚 ${AutoDocs.config.projectName} - Documentação AutoDocs</title>
2030
+ <style>
2031
+ body { font-family: 'Inter', sans-serif; background-color: #0f172a; color: #f1f5f9; min-height: 100vh; }
2032
+ .light-mode { background-color: #f9fafb !important; color: #111827 !important; }
2033
+ .card { background: #1e293b; border-radius: 1rem; padding: 1.5rem; box-shadow: 0 2px 8px #0002; color: #f1f5f9; }
2034
+ .light-mode .card { background: #fff; color: #111827; border: 1px solid #e5e7eb; }
2035
+ .btn { background: #334155; color: #fff; border: none; border-radius: 8px; padding: 0.5rem 1.2rem; font-weight: 600; cursor: pointer; margin-bottom: 0.25rem; transition: background 0.2s; }
2036
+ .btn:hover { background: #475569; }
2037
+ .light-mode .btn { background: #374151; color: #fff; border: 1px solid #d1d5db; }
2038
+ .light-mode .btn:hover { background: #4b5563; }
2039
+ .header-bar { background: #1e293b; padding: 1rem 2rem; border-radius: 1rem 1rem 0 0; margin-bottom: 1.5rem; display: flex; align-items: center; justify-content: space-between; }
2040
+ .header-bar .header-title { font-size: 2rem; font-weight: 700; color: #fbbf24; }
2041
+ .header-bar .header-meta { font-size: 1rem; color: #f1f5f9; }
2042
+ .light-mode .header-bar { background: #f3f4f6; color: #111827; border: 1px solid #e5e7eb; }
2043
+ .light-mode .header-title { color: #d97706; }
2044
+ .light-mode .header-meta { color: #374151 !important; }
2045
+ .footer-bar { background: #1e293b; color: #fbbf24; padding: 1rem 2rem; border-radius: 0 0 1rem 1rem; margin-top: 2rem; text-align: center; font-size: 1rem; }
2046
+ .light-mode .footer-bar { background: #f3f4f6; color: #d97706; border: 1px solid #e5e7eb; }
2047
+ </style>
2048
+ <script src="https://cdn.tailwindcss.com"></script>
2049
+ </head>
2050
+ <body class="min-h-screen bg-slate-900 text-slate-100">
2051
+ <div class="container mx-auto px-4 py-8 max-w-7xl">
2052
+ <!-- Header -->
2053
+ <div class="header-bar">
2054
+ <div>
2055
+ <h1 class="header-title">📚 ${AutoDocs.config.projectName}</h1>
2056
+ </div>
2057
+ <div class="flex gap-2">
2058
+ <button onclick="toggleTheme()" class="btn">🌙/☀️ Tema</button>
2059
+ </div>
2060
+ </div>
2061
+
2062
+ <!-- Empty State -->
2063
+ <div class="text-center py-16">
2064
+ <div class="card max-w-2xl mx-auto">
2065
+ <h2 class="text-3xl font-bold text-green-400 mb-6">🚀 Sistema AutoDocs Ativo</h2>
2066
+ <p class="text-slate-300 mb-8">O sistema de documentação automática está funcionando corretamente!</p>
2067
+
2068
+ <div class="bg-slate-700 p-6 rounded-lg border-l-4 border-green-400 mb-8">
2069
+ <h3 class="text-xl font-bold text-green-400 mb-4">📊 Como funciona</h3>
2070
+ <p class="text-slate-300 mb-4">O AutoDocs detecta automaticamente e documenta:</p>
2071
+ <ul class="text-left space-y-2 text-slate-300">
2072
+ <li class="flex items-center gap-2"><span class="text-green-400">🌐</span> Chamadas de API</li>
2073
+ <li class="flex items-center gap-2"><span class="text-cyan-400">🖥️</span> Comandos SSH</li>
2074
+ <li class="flex items-center gap-2"><span class="text-purple-400">🗄️</span> Queries de banco de dados</li>
2075
+ <li class="flex items-center gap-2"><span class="text-indigo-400">🎨</span> Ações de interface (UI)</li>
2076
+ <li class="flex items-center gap-2"><span class="text-pink-400">📱</span> Interações mobile</li>
2077
+ </ul>
2078
+ </div>
2079
+
2080
+ <div class="bg-gradient-to-r from-green-600 to-blue-600 p-6 rounded-lg text-white">
2081
+ <p class="text-lg font-semibold">Execute seus testes para ver a documentação sendo gerada automaticamente!</p>
2082
+ </div>
2083
+ </div>
2084
+ </div>
2085
+
2086
+ <!-- Footer -->
2087
+ <div class="footer-bar">
2088
+ <p>Documentação gerada automaticamente pelo AutoDocs (${AutoDocs.getProjectVersion()}) em ${new Date(timestamp).toLocaleString('pt-BR')}</p>
2089
+ </div>
2090
+ </div>
2091
+
2092
+ <script>
2093
+ function toggleTheme() {
2094
+ document.body.classList.toggle('light-mode');
2095
+ localStorage.setItem('theme', document.body.classList.contains('light-mode') ? 'light' : 'dark');
2096
+ }
2097
+
2098
+ // Load saved theme
2099
+ const savedTheme = localStorage.getItem('theme');
2100
+ if (savedTheme === 'light') {
2101
+ document.body.classList.add('light-mode');
2102
+ }
2103
+ </script>
2104
+ </body>
2105
+ </html>`;
2106
+ }
2107
+ /**
2108
+ * 🌐 Gerar seção de APIs com detalhes completos
2109
+ */
2110
+ static generateAPISection(apis) {
2111
+ if (apis.length === 0) {
2112
+ return '';
2113
+ }
2114
+ // 🤖 Detectar categorias automaticamente baseado nos endpoints executados
2115
+ const detectedCategories = AutoDocs.detectProjectCategories(apis);
2116
+ // Categorizar APIs usando detecção inteligente
2117
+ const categorizedApis = apis.reduce((acc, api) => {
2118
+ const category = AutoDocs.categorizeEndpointDynamic(api.url, detectedCategories);
2119
+ if (!acc[category]) {
2120
+ acc[category] = [];
2121
+ }
2122
+ acc[category].push(api);
2123
+ return acc;
2124
+ }, {});
2125
+ // Gerar seções categorizadas
2126
+ const categorySections = Object.entries(categorizedApis)
2127
+ .map(([category, categoryApis]) => {
2128
+ const categoryKey = category.toLowerCase();
2129
+ const config = detectedCategories[categoryKey] || detectedCategories['outras'];
2130
+ const apiItems = categoryApis
2131
+ .map((api) => {
2132
+ const statusIcon = api.success ? '✅' : '❌';
2133
+ const durationColor = api.duration > 5000 ? 'text-orange-400' : 'text-green-400';
2134
+ return `
2135
+ <div class="card bg-slate-800 hover:bg-slate-700 transition-all duration-200 transform hover:-translate-y-1">
2136
+ <div class="flex flex-wrap items-center justify-between mb-4">
2137
+ <div class="flex items-center gap-3">
2138
+ <span class="method-badge method-${api.method.toLowerCase()}">${api.method}</span>
2139
+ <span class="font-mono text-blue-300 font-semibold break-all">${api.url}</span>
2140
+ </div>
2141
+ <span class="status-badge ${api.success ? 'bg-green-600' : 'bg-red-600'} text-white px-3 py-1 rounded-full text-sm">
2142
+ ${statusIcon} ${api.statusCode}
2143
+ </span>
2144
+ </div>
2145
+
2146
+ <div class="grid grid-cols-1 md:grid-cols-3 gap-4 mb-4 text-sm">
2147
+ <div class="flex items-center gap-2 text-slate-300">
2148
+ <span>📝</span>
2149
+ <span>Teste: <span class="text-yellow-400">${api.testName}</span></span>
2150
+ </div>
2151
+ <div class="flex items-center gap-2 text-slate-300">
2152
+ <span>🌍</span>
2153
+ <span>Ambiente: <span class="text-blue-400">${api.environment}</span></span>
2154
+ </div>
2155
+ <div class="flex items-center gap-2 text-slate-300">
2156
+ <span>⏱️</span>
2157
+ <span>Duração: <span class="${durationColor}">${api.duration}ms</span></span>
2158
+ </div>
2159
+ </div>
2160
+
2161
+ ${api.headers && Object.keys(api.headers).length > 0
2162
+ ? `
2163
+ <details class="mb-4">
2164
+ <summary class="cursor-pointer font-semibold text-purple-400 hover:text-purple-300 transition-colors">
2165
+ 📤 Headers da Requisição
2166
+ </summary>
2167
+ <pre class="bg-slate-900 text-purple-300 p-4 rounded-lg mt-2 overflow-x-auto text-sm font-mono">${JSON.stringify(api.headers, null, 2)}</pre>
2168
+ </details>
2169
+ `
2170
+ : ''}
2171
+
2172
+ ${api.payload
2173
+ ? `
2174
+ <details class="mb-4">
2175
+ <summary class="cursor-pointer font-semibold text-green-400 hover:text-green-300 transition-colors">
2176
+ 📋 Payload da Requisição (Exemplo Real)
2177
+ </summary>
2178
+ <pre class="bg-slate-900 text-green-300 p-4 rounded-lg mt-2 overflow-x-auto text-sm font-mono">${JSON.stringify(api.payload, null, 2)}</pre>
2179
+ </details>
2180
+ `
2181
+ : ''}
2182
+
2183
+ ${api.response
2184
+ ? `
2185
+ <details class="mb-4">
2186
+ <summary class="cursor-pointer font-semibold text-blue-400 hover:text-blue-300 transition-colors">
2187
+ 📥 Resposta da API (Exemplo Real)
2188
+ </summary>
2189
+ <pre class="bg-slate-900 text-blue-300 p-4 rounded-lg mt-2 overflow-x-auto text-sm font-mono">${JSON.stringify(api.response, null, 2)}</pre>
2190
+ </details>
2191
+ `
2192
+ : ''}
2193
+
2194
+ ${api.error
2195
+ ? `
2196
+ <details class="mb-4">
2197
+ <summary class="cursor-pointer font-semibold text-red-400 hover:text-red-300 transition-colors">
2198
+ ❌ Erro
2199
+ </summary>
2200
+ <div class="bg-red-900 border border-red-700 text-red-300 p-4 rounded-lg mt-2">
2201
+ <pre class="text-sm font-mono overflow-x-auto">${api.error}</pre>
2202
+ </div>
2203
+ </details>
2204
+ `
2205
+ : ''}
2206
+
2207
+ <div class="text-xs text-slate-400 mt-4 pt-4 border-t border-slate-700">
2208
+ 🕐 ${new Date(api.timestamp).toLocaleString('pt-BR')}
2209
+ </div>
2210
+ </div>
2211
+ `;
2212
+ })
2213
+ .join('');
2214
+ const successCount = categoryApis.filter((api) => api.success).length;
2215
+ const failureCount = categoryApis.length - successCount;
2216
+ return `
2217
+ <div class="mb-8 category-section">
2218
+ <h3 class="text-xl font-bold text-blue-400 mb-4 flex items-center gap-3 border-l-4 border-blue-400 pl-4">
2219
+ ${config.icon} ${category} (${categoryApis.length} APIs)
2220
+ </h3>
2221
+ <p class="text-slate-400 mb-4 italic">${config.description}</p>
2222
+ <div class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4">
2223
+ <div class="bg-green-800 rounded-lg p-3">
2224
+ <div class="text-green-200 text-sm">✅ Sucessos</div>
2225
+ <div class="text-lg font-bold text-white">${successCount}</div>
2226
+ </div>
2227
+ <div class="bg-red-800 rounded-lg p-3">
2228
+ <div class="text-red-200 text-sm">❌ Falhas</div>
2229
+ <div class="text-lg font-bold text-white">${failureCount}</div>
2230
+ </div>
2231
+ </div>
2232
+ <div class="space-y-4">
2233
+ ${apiItems}
2234
+ </div>
2235
+ </div>
2236
+ `;
2237
+ })
2238
+ .join('');
2239
+ const totalSuccessCount = apis.filter((api) => api.success).length;
2240
+ const totalFailureCount = apis.length - totalSuccessCount;
2241
+ const avgDuration = apis.reduce((sum, api) => sum + (api.duration || 0), 0) / apis.length;
2242
+ // Gerar overview das categorias detectadas
2243
+ const categoryOverview = Object.entries(detectedCategories)
2244
+ .filter(([key]) => key !== 'outras')
2245
+ .map(([key, data]) => `<span class="inline-block bg-slate-700 px-3 py-1 rounded-full text-sm text-slate-300 mr-2 mb-2">${data.icon} ${key.charAt(0).toUpperCase() + key.slice(1)}</span>`)
2246
+ .join('');
2247
+ return `
2248
+ <div class="mb-8">
2249
+ <h2 class="text-3xl font-bold text-green-400 mb-6 flex items-center gap-3 border-l-4 border-green-400 pl-4">
2250
+ 🌐 APIs Documentadas - Detecção Inteligente (${apis.length} total)
2251
+ </h2>
2252
+
2253
+ <div class="bg-slate-800 rounded-lg p-4 mb-6">
2254
+ <h4 class="text-lg font-semibold text-blue-400 mb-3">🤖 Categorias Detectadas Automaticamente:</h4>
2255
+ <div class="flex flex-wrap">
2256
+ ${categoryOverview}
2257
+ </div>
2258
+ </div>
2259
+
2260
+ <div class="grid grid-cols-1 md:grid-cols-3 gap-4 mb-8">
2261
+ <div class="bg-green-800 rounded-lg p-4">
2262
+ <div class="text-green-200 text-sm">✅ Total Sucessos</div>
2263
+ <div class="text-3xl font-bold text-white">${totalSuccessCount}</div>
2264
+ </div>
2265
+ <div class="bg-red-800 rounded-lg p-4">
2266
+ <div class="text-red-200 text-sm">❌ Total Falhas</div>
2267
+ <div class="text-3xl font-bold text-white">${totalFailureCount}</div>
2268
+ </div>
2269
+ <div class="bg-blue-800 rounded-lg p-4">
2270
+ <div class="text-blue-200 text-sm">⏱️ Tempo Médio</div>
2271
+ <div class="text-3xl font-bold text-white">${Math.round(avgDuration)}ms</div>
2272
+ </div>
2273
+ </div>
2274
+
2275
+ ${categorySections}
2276
+ </div>
2277
+ `;
2278
+ }
2279
+ /**
2280
+ * 🖥️ Gerar seção SSH com detalhes completos
2281
+ */
2282
+ static generateSSHSection(sshCommands) {
2283
+ if (sshCommands.length === 0) {
2284
+ return '';
2285
+ }
2286
+ // 🚫 Filtrar comandos técnicos que não são verdadeiros CTs
2287
+ const filteredSSHCommands = sshCommands.filter((ssh) => {
2288
+ const command = ssh.command?.toLowerCase() || '';
2289
+ // Filtrar comandos técnicos (read, write, connect, etc.)
2290
+ const technicalCommands = [
2291
+ 'read:',
2292
+ 'write:',
2293
+ 'connect:',
2294
+ 'disconnect:',
2295
+ 'execute:',
2296
+ 'setup:',
2297
+ 'init:',
2298
+ 'cleanup:',
2299
+ ];
2300
+ return !technicalCommands.some((tech) => command.startsWith(tech));
2301
+ });
2302
+ if (filteredSSHCommands.length === 0) {
2303
+ return '';
2304
+ }
2305
+ const sshItems = filteredSSHCommands
2306
+ .map((ssh) => {
2307
+ // ✅ CORRIGIR: Tratar success undefined como sucesso (se não tem error)
2308
+ const isSuccess = ssh.success !== false && !ssh.error;
2309
+ const statusIcon = isSuccess ? '✅' : '❌';
2310
+ const durationColor = (ssh.duration || 0) > 3000 ? 'text-orange-400' : 'text-green-400';
2311
+ return `
2312
+ <div class="card bg-slate-800 hover:bg-slate-700 transition-all duration-200 transform hover:-translate-y-1">
2313
+ <div class="flex flex-wrap items-center justify-between mb-4">
2314
+ <div class="flex items-center gap-3">
2315
+ <span class="bg-cyan-600 text-white px-3 py-1 rounded-full text-sm font-bold">SSH</span>
2316
+ <span class="font-mono text-cyan-300 font-semibold break-all">🖥️ ${ssh.command}</span>
2317
+ </div>
2318
+ <span class="status-badge ${isSuccess ? 'bg-green-600' : 'bg-red-600'} text-white px-3 py-1 rounded-full text-sm">
2319
+ ${statusIcon} ${isSuccess ? 'Sucesso' : 'Erro'}
2320
+ </span>
2321
+ </div>
2322
+
2323
+ <div class="grid grid-cols-1 md:grid-cols-4 gap-4 mb-4 text-sm">
2324
+ <div class="flex items-center gap-2 text-slate-300">
2325
+ <span>🖥️</span>
2326
+ <span>Host: <span class="text-cyan-400">${ssh.host}</span></span>
2327
+ </div>
2328
+ <div class="flex items-center gap-2 text-slate-300">
2329
+ <span>👤</span>
2330
+ <span>Usuário: <span class="text-blue-400">${ssh.user}</span></span>
2331
+ </div>
2332
+ <div class="flex items-center gap-2 text-slate-300">
2333
+ <span>📝</span>
2334
+ <span>Teste: <span class="text-yellow-400">${ssh.testName}</span></span>
2335
+ </div>
2336
+ <div class="flex items-center gap-2 text-slate-300">
2337
+ <span>⏱️</span>
2338
+ <span>Duração: <span class="${durationColor}">${ssh.duration || 0}ms</span></span>
2339
+ </div>
2340
+ </div>
2341
+
2342
+ ${ssh.output
2343
+ ? `
2344
+ <details class="mb-4">
2345
+ <summary class="cursor-pointer font-semibold text-green-400 hover:text-green-300 transition-colors">
2346
+ 📤 Output do Comando
2347
+ </summary>
2348
+ <pre class="bg-slate-900 text-green-300 p-4 rounded-lg mt-2 overflow-x-auto text-sm font-mono">${ssh.output}</pre>
2349
+ </details>
2350
+ `
2351
+ : ''}
2352
+
2353
+ ${ssh.error
2354
+ ? `
2355
+ <details class="mb-4">
2356
+ <summary class="cursor-pointer font-semibold text-red-400 hover:text-red-300 transition-colors">
2357
+ ❌ Erro
2358
+ </summary>
2359
+ <div class="bg-red-900 border border-red-700 text-red-300 p-4 rounded-lg mt-2">
2360
+ <pre class="text-sm font-mono overflow-x-auto">${ssh.error}</pre>
2361
+ </div>
2362
+ </details>
2363
+ `
2364
+ : ''}
2365
+
2366
+ <div class="text-xs text-slate-400 mt-4 pt-4 border-t border-slate-700">
2367
+ 🕐 ${new Date(ssh.timestamp).toLocaleString('pt-BR')}
2368
+ </div>
2369
+ </div>
2370
+ `;
2371
+ })
2372
+ .join('');
2373
+ // ✅ CORRIGIR: Contar sucessos considerando success undefined como sucesso
2374
+ const successCount = filteredSSHCommands.filter((ssh) => ssh.success !== false && !ssh.error).length;
2375
+ const failureCount = filteredSSHCommands.length - successCount;
2376
+ return `
2377
+ <div class="mb-8">
2378
+ <h2 class="text-2xl font-bold text-cyan-400 mb-6 flex items-center gap-3 border-l-4 border-cyan-400 pl-4">
2379
+ 🖥️ Comandos SSH (${filteredSSHCommands.length})
2380
+ </h2>
2381
+ <div class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-6">
2382
+ <div class="bg-green-800 rounded-lg p-4">
2383
+ <div class="text-green-200 text-sm">✅ Sucessos</div>
2384
+ <div class="text-2xl font-bold text-white">${successCount}</div>
2385
+ </div>
2386
+ <div class="bg-red-800 rounded-lg p-4">
2387
+ <div class="text-red-200 text-sm">❌ Falhas</div>
2388
+ <div class="text-2xl font-bold text-white">${failureCount}</div>
2389
+ </div>
2390
+ </div>
2391
+ <div class="space-y-6">
2392
+ ${sshItems}
2393
+ </div>
2394
+ </div>
2395
+ `;
2396
+ }
2397
+ /**
2398
+ * 🎨 Obter classe TailwindCSS para tipo de query
2399
+ */
2400
+ static getQueryTypeBadgeClass(type) {
2401
+ switch (type.toUpperCase()) {
2402
+ case 'SELECT':
2403
+ return 'bg-green-600';
2404
+ case 'INSERT':
2405
+ return 'bg-blue-600';
2406
+ case 'UPDATE':
2407
+ return 'bg-orange-600';
2408
+ case 'DELETE':
2409
+ return 'bg-red-600';
2410
+ case 'CREATE':
2411
+ return 'bg-purple-600';
2412
+ case 'ALTER':
2413
+ return 'bg-yellow-600';
2414
+ case 'DROP':
2415
+ return 'bg-red-800';
2416
+ default:
2417
+ return 'bg-gray-600';
2418
+ }
2419
+ }
2420
+ /**
2421
+ * 🗄️ Gerar seção Database com detalhes completos
2422
+ */
2423
+ static generateDBSection(dbQueries) {
2424
+ if (dbQueries.length === 0) {
2425
+ return '';
2426
+ }
2427
+ const dbItems = dbQueries
2428
+ .map((db) => {
2429
+ // ✅ CORRIGIDO: Melhorar detecção de sucesso
2430
+ // Se success não está definido, assumir sucesso se não há erro e há duração
2431
+ const isSuccess = db.success !== undefined ? db.success : !db.error && db.duration > 0;
2432
+ Logger.info(`🗄️ [HubDocs] Query: ${db.query.substring(0, 30)}... | success: ${db.success} | error: ${db.error} | duration: ${db.duration} | isSuccess: ${isSuccess}`);
2433
+ const statusIcon = isSuccess ? '✅' : '❌';
2434
+ const typeColor = AutoDocs.getQueryTypeBadgeClass(db.type);
2435
+ const queryPreview = db.query.length > 100 ? `${db.query.substring(0, 100)}...` : db.query;
2436
+ const durationColor = db.duration > 2000 ? 'text-orange-400' : 'text-green-400';
2437
+ return `
2438
+ <div class="card bg-slate-800 hover:bg-slate-700 transition-all duration-200 transform hover:-translate-y-1">
2439
+ <div class="flex flex-wrap items-center justify-between mb-4">
2440
+ <div class="flex items-center gap-3">
2441
+ <span class="${typeColor} text-white px-3 py-1 rounded-full text-sm font-bold">${db.type}</span>
2442
+ <span class="font-mono text-purple-300 font-semibold break-all">${queryPreview}</span>
2443
+ </div>
2444
+ <span class="status-badge ${isSuccess ? 'bg-green-600' : 'bg-red-600'} text-white px-3 py-1 rounded-full text-sm">
2445
+ ${statusIcon} ${isSuccess ? 'Sucesso' : 'Erro'}
2446
+ </span>
2447
+ </div>
2448
+
2449
+ ${db.description
2450
+ ? `
2451
+ <div class="mb-4 p-3 bg-slate-900 rounded-lg border-l-4 border-blue-500">
2452
+ <div class="flex items-start gap-2">
2453
+ <span class="text-blue-400 text-lg flex-shrink-0">📖</span>
2454
+ <div>
2455
+ <p class="text-slate-300 text-sm font-medium">Descrição do Serviço</p>
2456
+ <p class="text-blue-300 text-base mt-1">${db.description}</p>
2457
+ </div>
2458
+ </div>
2459
+ </div>
2460
+ `
2461
+ : ''}
2462
+
2463
+ <div class="grid grid-cols-1 md:grid-cols-4 gap-4 mb-4 text-sm">
2464
+ <div class="flex items-center gap-2 text-slate-300">
2465
+ <span>🗄️</span>
2466
+ <span>Database: <span class="text-purple-400">${db.database}</span></span>
2467
+ </div>
2468
+ <div class="flex items-center gap-2 text-slate-300">
2469
+ <span>📝</span>
2470
+ <span>Teste: <span class="text-yellow-400">${db.testName}</span></span>
2471
+ </div>
2472
+ <div class="flex items-center gap-2 text-slate-300">
2473
+ <span>🌍</span>
2474
+ <span>Ambiente: <span class="text-blue-400">${db.environment}</span></span>
2475
+ </div>
2476
+ <div class="flex items-center gap-2 text-slate-300">
2477
+ <span>⏱️</span>
2478
+ <span>Duração: <span class="${durationColor}">${db.duration || 0}ms</span></span>
2479
+ </div>
2480
+ ${db.serviceName
2481
+ ? `
2482
+ <div class="flex items-center gap-2 text-slate-300 md:col-span-2">
2483
+ <span>🔧</span>
2484
+ <span>Serviço: <span class="text-cyan-400">${db.serviceName}</span></span>
2485
+ </div>
2486
+ `
2487
+ : ''}
2488
+ </div>
2489
+
2490
+ <details class="mb-4">
2491
+ <summary class="cursor-pointer font-semibold text-purple-400 hover:text-purple-300 transition-colors">
2492
+ 📋 Query Completa
2493
+ </summary>
2494
+ <pre class="bg-slate-900 text-purple-300 p-4 rounded-lg mt-2 overflow-x-auto text-sm font-mono">${db.query}</pre>
2495
+ </details>
2496
+
2497
+ ${db.params && db.params.length > 0
2498
+ ? `
2499
+ <details class="mb-4">
2500
+ <summary class="cursor-pointer font-semibold text-orange-400 hover:text-orange-300 transition-colors">
2501
+ 🔧 Parâmetros
2502
+ </summary>
2503
+ <pre class="bg-slate-900 text-orange-300 p-4 rounded-lg mt-2 overflow-x-auto text-sm font-mono">${JSON.stringify(db.params, null, 2)}</pre>
2504
+ </details>
2505
+ `
2506
+ : ''}
2507
+
2508
+ ${db.error && !isSuccess
2509
+ ? `
2510
+ <details class="mb-4">
2511
+ <summary class="cursor-pointer font-semibold text-red-400 hover:text-red-300 transition-colors">
2512
+ ❌ Erro
2513
+ </summary>
2514
+ <div class="bg-red-900 border border-red-700 text-red-300 p-4 rounded-lg mt-2">
2515
+ <pre class="text-sm font-mono overflow-x-auto">${db.error}</pre>
2516
+ </div>
2517
+ </details>
2518
+ `
2519
+ : ''}
2520
+
2521
+ <div class="text-xs text-slate-400 mt-4 pt-4 border-t border-slate-700">
2522
+ 🕐 ${new Date(db.timestamp).toLocaleString('pt-BR')}
2523
+ </div>
2524
+ </div>
2525
+ `;
2526
+ })
2527
+ .join('');
2528
+ // ✅ CORRIGIDO: Usar lógica melhorada de detecção de sucesso
2529
+ const successCount = dbQueries.filter((db) => db.success !== undefined ? db.success : !db.error && db.duration > 0).length;
2530
+ const failureCount = dbQueries.length - successCount;
2531
+ const avgDuration = dbQueries.reduce((sum, db) => sum + (db.duration || 0), 0) /
2532
+ dbQueries.length;
2533
+ return `
2534
+ <div class="mb-8">
2535
+ <h2 class="text-2xl font-bold text-purple-400 mb-6 flex items-center gap-3 border-l-4 border-purple-400 pl-4">
2536
+ 🗄️ Queries Database (${dbQueries.length})
2537
+ </h2>
2538
+ <div class="grid grid-cols-1 md:grid-cols-3 gap-4 mb-6">
2539
+ <div class="bg-green-800 rounded-lg p-4">
2540
+ <div class="text-green-200 text-sm">✅ Sucessos</div>
2541
+ <div class="text-2xl font-bold text-white">${successCount}</div>
2542
+ </div>
2543
+ <div class="bg-red-800 rounded-lg p-4">
2544
+ <div class="text-red-200 text-sm">❌ Falhas</div>
2545
+ <div class="text-2xl font-bold text-white">${failureCount}</div>
2546
+ </div>
2547
+ <div class="bg-orange-800 rounded-lg p-4">
2548
+ <div class="text-orange-200 text-sm">⏱️ Tempo médio</div>
2549
+ <div class="text-2xl font-bold text-white">${Math.round(avgDuration)}ms</div>
2550
+ </div>
2551
+ </div>
2552
+ <div class="space-y-6">
2553
+ ${dbItems}
2554
+ </div>
2555
+ </div>
2556
+ `;
2557
+ }
2558
+ /**
2559
+ * 🎨 Obter classe TailwindCSS para ação de UI
2560
+ */
2561
+ static getUIActionBadgeClass(action) {
2562
+ switch (action.toLowerCase()) {
2563
+ case 'click':
2564
+ return 'bg-blue-600';
2565
+ case 'type':
2566
+ case 'fill':
2567
+ return 'bg-green-600';
2568
+ case 'navigate':
2569
+ return 'bg-purple-600';
2570
+ case 'wait':
2571
+ return 'bg-yellow-600';
2572
+ case 'scroll':
2573
+ return 'bg-indigo-600';
2574
+ case 'hover':
2575
+ return 'bg-cyan-600';
2576
+ case 'select':
2577
+ return 'bg-orange-600';
2578
+ default:
2579
+ return 'bg-gray-600';
2580
+ }
2581
+ }
2582
+ /**
2583
+ * 🎨 Gerar seção UI com detalhes completos
2584
+ */
2585
+ static generateUISection(uiActions) {
2586
+ if (uiActions.length === 0) {
2587
+ return '';
2588
+ }
2589
+ const uiItems = uiActions
2590
+ .map((ui) => {
2591
+ const statusIcon = ui.success ? '✅' : '❌';
2592
+ const actionColor = AutoDocs.getUIActionBadgeClass(ui.action);
2593
+ const durationColor = ui.duration > 2000 ? 'text-orange-400' : 'text-green-400';
2594
+ const screenshotSrc = AutoDocs.resolveImageSource(ui.screenshot);
2595
+ return `
2596
+ <div class="card bg-slate-800 hover:bg-slate-700 transition-all duration-200 transform hover:-translate-y-1">
2597
+ <div class="flex flex-wrap items-center justify-between mb-4">
2598
+ <div class="flex items-center gap-3">
2599
+ <span class="${actionColor} text-white px-3 py-1 rounded-full text-sm font-bold">${ui.action}</span>
2600
+ <span class="font-mono text-indigo-300 font-semibold break-all">${ui.element}</span>
2601
+ ${ui.methodName && ui.methodName !== 'unknown-method'
2602
+ ? `
2603
+ <span class="bg-purple-600 text-white px-2 py-1 rounded text-xs">📋 ${ui.methodName}</span>
2604
+ `
2605
+ : ''}
2606
+ </div>
2607
+ <span class="status-badge ${ui.success ? 'bg-green-600' : 'bg-red-600'} text-white px-3 py-1 rounded-full text-sm">
2608
+ ${statusIcon} ${ui.success ? 'Sucesso' : 'Erro'}
2609
+ </span>
2610
+ </div>
2611
+
2612
+ <div class="grid grid-cols-1 md:grid-cols-3 gap-4 mb-4 text-sm">
2613
+ <div class="flex items-center gap-2 text-slate-300">
2614
+ <span>🌐</span>
2615
+ <span>Página: <span class="text-indigo-400">${ui.page}</span></span>
2616
+ </div>
2617
+ <div class="flex items-center gap-2 text-slate-300">
2618
+ <span>📝</span>
2619
+ <span>Teste: <span class="text-yellow-400">${ui.testName}</span></span>
2620
+ </div>
2621
+ <div class="flex items-center gap-2 text-slate-300">
2622
+ <span>⏱️</span>
2623
+ <span>Duração: <span class="${durationColor}">${ui.duration || 0}ms</span></span>
2624
+ </div>
2625
+ </div>
2626
+
2627
+ ${ui.selector
2628
+ ? `
2629
+ <details class="mb-4">
2630
+ <summary class="cursor-pointer font-semibold text-indigo-400 hover:text-indigo-300 transition-colors">
2631
+ 🎯 Seletor
2632
+ ${ui.selectorType ? `<span class="ml-2 bg-cyan-600 text-white px-2 py-1 rounded text-xs">${ui.selectorType}</span>` : ''}
2633
+ </summary>
2634
+ <pre class="bg-slate-900 text-indigo-300 p-4 rounded-lg mt-2 overflow-x-auto text-sm font-mono">${ui.selector}</pre>
2635
+ </details>
2636
+ `
2637
+ : ''}
2638
+
2639
+ ${screenshotSrc
2640
+ ? `
2641
+ <details class="mb-4">
2642
+ <summary class="cursor-pointer font-semibold text-green-400 hover:text-green-300 transition-colors">
2643
+ 📸 Screenshot
2644
+ </summary>
2645
+ <div class="mt-2">
2646
+ <img src="${screenshotSrc}" alt="Screenshot da ação" class="max-w-full rounded-lg border border-slate-600">
2647
+ </div>
2648
+ </details>
2649
+ `
2650
+ : ''}
2651
+
2652
+ ${ui.error
2653
+ ? `
2654
+ <details class="mb-4">
2655
+ <summary class="cursor-pointer font-semibold text-red-400 hover:text-red-300 transition-colors">
2656
+ ❌ Erro
2657
+ </summary>
2658
+ <div class="bg-red-900 border border-red-700 text-red-300 p-4 rounded-lg mt-2">
2659
+ <pre class="text-sm font-mono overflow-x-auto">${ui.error}</pre>
2660
+ </div>
2661
+ </details>
2662
+ `
2663
+ : ''}
2664
+
2665
+ <div class="text-xs text-slate-400 mt-4 pt-4 border-t border-slate-700">
2666
+ 🕐 ${new Date(ui.timestamp).toLocaleString('pt-BR')}
2667
+ </div>
2668
+ </div>
2669
+ `;
2670
+ })
2671
+ .join('');
2672
+ const successCount = uiActions.filter((ui) => ui.success).length;
2673
+ const failureCount = uiActions.length - successCount;
2674
+ return `
2675
+ <div class="mb-8">
2676
+ <h2 class="text-2xl font-bold text-indigo-400 mb-6 flex items-center gap-3 border-l-4 border-indigo-400 pl-4">
2677
+ 🎨 Ações UI (${uiActions.length})
2678
+ </h2>
2679
+ <div class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-6">
2680
+ <div class="bg-green-800 rounded-lg p-4">
2681
+ <div class="text-green-200 text-sm">✅ Sucessos</div>
2682
+ <div class="text-2xl font-bold text-white">${successCount}</div>
2683
+ </div>
2684
+ <div class="bg-red-800 rounded-lg p-4">
2685
+ <div class="text-red-200 text-sm">❌ Falhas</div>
2686
+ <div class="text-2xl font-bold text-white">${failureCount}</div>
2687
+ </div>
2688
+ </div>
2689
+ <div class="space-y-6">
2690
+ ${uiItems}
2691
+ </div>
2692
+ </div>
2693
+ `;
2694
+ }
2695
+ /**
2696
+ * 📱 Gerar seção Mobile com detalhes completos
2697
+ */
2698
+ static generateMobileSection(mobileActions) {
2699
+ if (mobileActions.length === 0) {
2700
+ return '';
2701
+ }
2702
+ const mobileItems = mobileActions
2703
+ .map((mobile) => {
2704
+ const platformColor = mobile.platform === 'iOS' ? 'bg-blue-600' : 'bg-green-600';
2705
+ const actionColor = AutoDocs.getMobileActionBadgeClass(mobile.action);
2706
+ const screenshotSrc = AutoDocs.resolveImageSource(mobile.screenshot);
2707
+ return `
2708
+ <div class="card bg-slate-800 hover:bg-slate-700 transition-all duration-200 transform hover:-translate-y-1">
2709
+ <div class="flex flex-wrap items-center gap-3 mb-4">
2710
+ <span class="${platformColor} text-white px-3 py-1 rounded-full text-sm font-bold">${mobile.platform}</span>
2711
+ <span class="${actionColor} text-white px-3 py-1 rounded-full text-sm font-bold">${mobile.action}</span>
2712
+ <span class="font-mono text-pink-300 font-semibold break-all">${mobile.element}</span>
2713
+ </div>
2714
+
2715
+ <div class="grid grid-cols-1 md:grid-cols-3 gap-4 mb-4 text-sm">
2716
+ <div class="flex items-center gap-2 text-slate-300">
2717
+ <span>📱</span>
2718
+ <span>Device: <span class="text-pink-400">${mobile.device}</span></span>
2719
+ </div>
2720
+ <div class="flex items-center gap-2 text-slate-300">
2721
+ <span>📝</span>
2722
+ <span>Teste: <span class="text-yellow-400">${mobile.testName}</span></span>
2723
+ </div>
2724
+ <div class="flex items-center gap-2 text-slate-300">
2725
+ <span>🕐</span>
2726
+ <span>Timestamp: <span class="text-blue-400">${new Date(mobile.timestamp).toLocaleString('pt-BR')}</span></span>
2727
+ </div>
2728
+ </div>
2729
+
2730
+ ${mobile.value
2731
+ ? `
2732
+ <div class="bg-slate-700 p-3 rounded-lg mb-4">
2733
+ <div class="flex items-center gap-2 text-slate-300">
2734
+ <span class="text-green-400">💬</span>
2735
+ <span>Valor: <span class="text-green-300 font-mono">${mobile.value}</span></span>
2736
+ </div>
2737
+ </div>
2738
+ `
2739
+ : ''}
2740
+
2741
+ ${mobile.coordinates
2742
+ ? `
2743
+ <div class="bg-slate-700 p-3 rounded-lg mb-4">
2744
+ <div class="flex items-center gap-2 text-slate-300">
2745
+ <span class="text-orange-400">📍</span>
2746
+ <span>Coordenadas: <span class="text-orange-300 font-mono">x: ${mobile.coordinates.x}, y: ${mobile.coordinates.y}</span></span>
2747
+ </div>
2748
+ </div>
2749
+ `
2750
+ : ''}
2751
+
2752
+ ${screenshotSrc
2753
+ ? `
2754
+ <div class="bg-slate-700 p-3 rounded-lg mb-4">
2755
+ <div class="flex items-center gap-2 text-slate-300 mb-3">
2756
+ <span class="text-purple-400">📸</span>
2757
+ <span>Screenshot da Ação:</span>
2758
+ </div>
2759
+ <div class="bg-slate-600 p-2 rounded-lg">
2760
+ <img
2761
+ src="${screenshotSrc}"
2762
+ alt="Screenshot da ação ${mobile.action} no elemento ${mobile.element}"
2763
+ class="w-full max-w-md mx-auto rounded-lg shadow-lg cursor-pointer hover:shadow-xl transition-shadow duration-200"
2764
+ onclick="window.open(this.src, '_blank')"
2765
+ loading="lazy"
2766
+ />
2767
+ <p class="text-xs text-slate-400 text-center mt-2">Clique na imagem para visualizar em tamanho completo</p>
2768
+ </div>
2769
+ </div>
2770
+ `
2771
+ : ''}
2772
+ </div>
2773
+ `;
2774
+ })
2775
+ .join('');
2776
+ return `
2777
+ <div class="mb-8">
2778
+ <h2 class="text-2xl font-bold text-pink-400 mb-6 flex items-center gap-3 border-l-4 border-pink-400 pl-4">
2779
+ 📱 Ações Mobile (${mobileActions.length})
2780
+ </h2>
2781
+ <div class="space-y-6">
2782
+ ${mobileItems}
2783
+ </div>
2784
+ </div>
2785
+ `;
2786
+ }
2787
+ /**
2788
+ * 🎨 Obter classe TailwindCSS para ação mobile
2789
+ */
2790
+ static getMobileActionBadgeClass(action) {
2791
+ switch (action.toLowerCase()) {
2792
+ case 'tap':
2793
+ return 'bg-green-600';
2794
+ case 'type':
2795
+ return 'bg-blue-600';
2796
+ case 'swipe':
2797
+ return 'bg-orange-600';
2798
+ case 'scroll':
2799
+ return 'bg-purple-600';
2800
+ case 'wait':
2801
+ return 'bg-yellow-600';
2802
+ case 'pinch':
2803
+ return 'bg-red-600';
2804
+ case 'rotate':
2805
+ return 'bg-indigo-600';
2806
+ default:
2807
+ return 'bg-gray-600';
2808
+ }
2809
+ }
2810
+ /**
2811
+ * 🎨 Obter cor do tipo de query
2812
+ */
2813
+ static getQueryTypeColor(type) {
2814
+ const colors = {
2815
+ SELECT: '#2196F3',
2816
+ INSERT: '#4CAF50',
2817
+ UPDATE: '#FF9800',
2818
+ DELETE: '#f44336',
2819
+ DDL: '#9C27B0',
2820
+ OTHER: '#607D8B',
2821
+ };
2822
+ return colors[type] || colors.OTHER;
2823
+ }
2824
+ /**
2825
+ * 🎨 Obter cor da ação UI
2826
+ */
2827
+ static getUIActionColor(action) {
2828
+ const colors = {
2829
+ click: '#4CAF50',
2830
+ type: '#2196F3',
2831
+ wait: '#FF9800',
2832
+ navigate: '#9C27B0',
2833
+ validate: '#00BCD4',
2834
+ };
2835
+ return colors[action.toLowerCase()] || '#607D8B';
2836
+ }
2837
+ /**
2838
+ * 🎨 Obter cor da ação mobile
2839
+ */
2840
+ static getMobileActionColor(action) {
2841
+ const colors = {
2842
+ tap: '#4CAF50',
2843
+ type: '#2196F3',
2844
+ swipe: '#FF9800',
2845
+ scroll: '#9C27B0',
2846
+ wait: '#607D8B',
2847
+ };
2848
+ return colors[action.toLowerCase()] || '#795548';
2849
+ }
2850
+ /**
2851
+ * 📋 Gerar documentação Swagger detalhada
2852
+ */
2853
+ static async generateSwaggerDocs(apis) {
2854
+ try {
2855
+ const swaggerDoc = {
2856
+ openapi: '3.0.0',
2857
+ info: {
2858
+ title: `${AutoDocs.config.projectName} - API Documentation`,
2859
+ version: '1.0.0',
2860
+ description: 'Documentação automática gerada pelo AutoDocs durante execução dos testes',
2861
+ contact: {
2862
+ name: 'Test Hub Framework',
2863
+ url: 'http://brtlvlty0559pl:3002/',
2864
+ },
2865
+ },
2866
+ servers: AutoDocs.extractServers(apis),
2867
+ paths: AutoDocs.generateDetailedSwaggerPaths(apis),
2868
+ components: {
2869
+ schemas: AutoDocs.generateSwaggerSchemas(apis),
2870
+ securitySchemes: {
2871
+ ApiKeyAuth: {
2872
+ type: 'apiKey',
2873
+ in: 'header',
2874
+ name: 'X-API-Key',
2875
+ },
2876
+ BearerAuth: {
2877
+ type: 'http',
2878
+ scheme: 'bearer',
2879
+ },
2880
+ },
2881
+ },
2882
+ tags: AutoDocs.generateSwaggerTags(apis),
2883
+ };
2884
+ const outputPath = path.join(AutoDocs.config.outputDir, 'swagger.json');
2885
+ await fs.promises.writeFile(outputPath, JSON.stringify(swaggerDoc, null, 2), 'utf-8');
2886
+ // Gerar também versão YAML
2887
+ const yamlPath = path.join(AutoDocs.config.outputDir, 'openapi.yaml');
2888
+ const yamlContent = AutoDocs.convertToYaml(swaggerDoc);
2889
+ await fs.promises.writeFile(yamlPath, yamlContent, 'utf-8');
2890
+ Logger.info(`📋 Swagger gerado: ${outputPath}`);
2891
+ Logger.info(`📋 OpenAPI YAML gerado: ${yamlPath}`);
2892
+ }
2893
+ catch (error) {
2894
+ Logger.error('❌ Erro ao gerar Swagger:', error);
2895
+ }
2896
+ }
2897
+ /**
2898
+ * 🖥️ Extrair servidores das URLs das APIs
2899
+ */
2900
+ static extractServers(apis) {
2901
+ const servers = new Set();
2902
+ apis.forEach((api) => {
2903
+ try {
2904
+ // 🎯 Lidar com URLs relativas e absolutas
2905
+ if (api.url.startsWith('http://') || api.url.startsWith('https://')) {
2906
+ const url = new URL(api.url);
2907
+ servers.add(`${url.protocol}//${url.host}`);
2908
+ }
2909
+ // URLs relativas não têm servidor, ignorar
2910
+ }
2911
+ catch {
2912
+ // Ignorar URLs inválidas
2913
+ }
2914
+ });
2915
+ return Array.from(servers).map((server) => ({
2916
+ url: server,
2917
+ description: 'Servidor detectado automaticamente',
2918
+ }));
2919
+ }
2920
+ /**
2921
+ * 🛤️ Gerar paths detalhados do Swagger
2922
+ */
2923
+ static generateDetailedSwaggerPaths(apis) {
2924
+ const paths = {};
2925
+ apis.forEach((api) => {
2926
+ try {
2927
+ // 🎯 Lidar com URLs relativas e absolutas
2928
+ let pathKey;
2929
+ if (api.url.startsWith('http://') || api.url.startsWith('https://')) {
2930
+ // URL absoluta - extrair pathname
2931
+ const url = new URL(api.url);
2932
+ pathKey = url.pathname || '/unknown';
2933
+ }
2934
+ else {
2935
+ // URL relativa - usar diretamente (remover query string se houver)
2936
+ pathKey = api.url.split('?')[0] || '/unknown';
2937
+ }
2938
+ const method = api.method.toLowerCase();
2939
+ if (!paths[pathKey]) {
2940
+ paths[pathKey] = {};
2941
+ }
2942
+ paths[pathKey][method] = {
2943
+ summary: `${api.method} ${pathKey}`,
2944
+ description: `Detectado no teste: ${api.testName}\\nExecução: ${api.timestamp}`,
2945
+ tags: [AutoDocs.categorizeEndpoint(pathKey)],
2946
+ parameters: AutoDocs.extractParameters(api),
2947
+ requestBody: api.payload
2948
+ ? {
2949
+ content: {
2950
+ 'application/json': {
2951
+ schema: AutoDocs.inferSchema(api.payload),
2952
+ example: api.payload,
2953
+ },
2954
+ },
2955
+ }
2956
+ : undefined,
2957
+ responses: {
2958
+ [api.statusCode || '200']: {
2959
+ description: api.success ? 'Sucesso' : 'Erro',
2960
+ content: api.response
2961
+ ? {
2962
+ 'application/json': {
2963
+ schema: AutoDocs.inferSchema(api.response),
2964
+ example: api.response,
2965
+ },
2966
+ }
2967
+ : undefined,
2968
+ },
2969
+ },
2970
+ 'x-test-info': {
2971
+ testName: api.testName,
2972
+ environment: api.environment,
2973
+ duration: `${api.duration}ms`,
2974
+ success: api.success,
2975
+ timestamp: api.timestamp,
2976
+ },
2977
+ };
2978
+ }
2979
+ catch (error) {
2980
+ Logger.warning(`⚠️ Erro ao processar API para Swagger: ${api.url} ${error}`);
2981
+ }
2982
+ });
2983
+ return paths;
2984
+ }
2985
+ /**
2986
+ * 📊 Gerar schemas do Swagger baseado nos dados
2987
+ */
2988
+ static generateSwaggerSchemas(apis) {
2989
+ const schemas = {};
2990
+ apis.forEach((api) => {
2991
+ // Schema do payload
2992
+ if (api.payload) {
2993
+ const schemaName = AutoDocs.generateSchemaName(api.url, 'Request');
2994
+ schemas[schemaName] = AutoDocs.inferSchema(api.payload);
2995
+ }
2996
+ // Schema da response
2997
+ if (api.response) {
2998
+ const schemaName = AutoDocs.generateSchemaName(api.url, 'Response');
2999
+ schemas[schemaName] = AutoDocs.inferSchema(api.response);
3000
+ }
3001
+ });
3002
+ return schemas;
3003
+ }
3004
+ /**
3005
+ * 🏷️ Gerar tags do Swagger
3006
+ */
3007
+ static generateSwaggerTags(apis) {
3008
+ const tags = new Set();
3009
+ apis.forEach((api) => {
3010
+ try {
3011
+ // 🎯 Lidar com URLs relativas e absolutas
3012
+ let pathname;
3013
+ if (api.url.startsWith('http://') || api.url.startsWith('https://')) {
3014
+ const url = new URL(api.url);
3015
+ pathname = url.pathname;
3016
+ }
3017
+ else {
3018
+ pathname = api.url.split('?')[0]; // URL relativa
3019
+ }
3020
+ const tag = AutoDocs.categorizeEndpoint(pathname);
3021
+ tags.add(tag);
3022
+ }
3023
+ catch {
3024
+ tags.add('Geral');
3025
+ }
3026
+ });
3027
+ // 🏷️ Descrições detalhadas para cada categoria
3028
+ const tagDescriptions = {
3029
+ Cliente: 'APIs relacionadas a consulta e gestão de dados do cliente, produtos associados, contratos e histórico',
3030
+ Catalogo: 'APIs para consulta de produtos, dispositivos, portfolios, preços e ofertas disponíveis',
3031
+ Fatura: 'APIs para consulta de faturas, cobranças e downloads de documentos financeiros',
3032
+ Simulação: 'APIs para criação, consulta e gestão de simulações de venda e negociação',
3033
+ Documentação: 'APIs para geração de documentos, contratos e processos de aceite digital',
3034
+ Workflow: 'APIs para gestão de processos de aprovação, esteira de tramitação e solicitações de serviço',
3035
+ Auxiliar: 'APIs auxiliares para consultas de operadoras, endereços e validações gerais',
3036
+ Autenticação: 'APIs para autenticação, autorização e gestão de tokens',
3037
+ Monitoramento: 'APIs para health checks, status do sistema e monitoramento',
3038
+ Geral: 'APIs gerais e não categorizadas',
3039
+ };
3040
+ return Array.from(tags).map((tag) => ({
3041
+ name: tag,
3042
+ description: tagDescriptions[tag] || `Endpoints da categoria ${tag}`,
3043
+ }));
3044
+ }
3045
+ /**
3046
+ * 🤖 Detectar categorias automaticamente baseado nos endpoints executados
3047
+ */
3048
+ static detectProjectCategories(apis) {
3049
+ const urlPatterns = {};
3050
+ // Analisar todas as URLs para detectar padrões
3051
+ apis.forEach((api) => {
3052
+ const url = api.url.toLowerCase();
3053
+ const pathSegments = url
3054
+ .split('/')
3055
+ .filter((segment) => segment && !segment.includes('http'));
3056
+ pathSegments.forEach((segment) => {
3057
+ // Remover números e IDs para detectar padrões
3058
+ const cleanSegment = segment
3059
+ .replace(/\d+/g, '')
3060
+ .replace(/[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}/g, '');
3061
+ if (cleanSegment.length > 2) {
3062
+ if (!urlPatterns[cleanSegment]) {
3063
+ urlPatterns[cleanSegment] = [];
3064
+ }
3065
+ urlPatterns[cleanSegment].push(url);
3066
+ }
3067
+ });
3068
+ });
3069
+ // Detectar categorias baseado na frequência e padrões conhecidos
3070
+ const detectedCategories = {};
3071
+ // Padrões empresariais conhecidos
3072
+ const knownPatterns = {
3073
+ customer: {
3074
+ variations: ['customer', 'customers', 'cliente', 'clientes'],
3075
+ icon: '👥',
3076
+ description: 'Operações relacionadas ao gerenciamento de clientes e dados corporativos',
3077
+ },
3078
+ catalog: {
3079
+ variations: [
3080
+ 'catalog',
3081
+ 'catalogo',
3082
+ 'products',
3083
+ 'devices',
3084
+ 'portfolio',
3085
+ 'offers',
3086
+ 'producttypecode',
3087
+ 'typetrading',
3088
+ ],
3089
+ icon: '📦',
3090
+ description: 'Gestão de produtos, dispositivos e portfólio de ofertas',
3091
+ },
3092
+ billing: {
3093
+ variations: ['billing', 'invoices', 'fatura', 'faturas', 'cobranca'],
3094
+ icon: '💰',
3095
+ description: 'Processos de faturamento e gestão de pagamentos',
3096
+ },
3097
+ simulation: {
3098
+ variations: ['simulation', 'simulations', 'simulacao', 'simulacoes'],
3099
+ icon: '🎯',
3100
+ description: 'Simulações de vendas e análises comerciais',
3101
+ },
3102
+ file: {
3103
+ variations: [
3104
+ 'file',
3105
+ 'files',
3106
+ 'document',
3107
+ 'documents',
3108
+ 'documentacao',
3109
+ 'upload',
3110
+ ],
3111
+ icon: '📄',
3112
+ description: 'Gestão de arquivos e documentação',
3113
+ },
3114
+ workflow: {
3115
+ variations: [
3116
+ 'workflow',
3117
+ 'approval',
3118
+ 'esteira',
3119
+ 'tramitacao',
3120
+ 'servicerequest',
3121
+ ],
3122
+ icon: '🔄',
3123
+ description: 'Processos de aprovação e fluxos de trabalho',
3124
+ },
3125
+ account: {
3126
+ variations: ['account', 'accounts', 'conta', 'contas', 'sobjects'],
3127
+ icon: '🏢',
3128
+ description: 'Gestão de contas e informações corporativas',
3129
+ },
3130
+ service: {
3131
+ variations: ['service', 'services', 'servico', 'servicos', 'ss'],
3132
+ icon: '🛠️',
3133
+ description: 'Serviços e requisições de suporte',
3134
+ },
3135
+ order: {
3136
+ variations: ['order', 'orders', 'pedido', 'pedidos'],
3137
+ icon: '📋',
3138
+ description: 'Gestão de pedidos e ordens',
3139
+ },
3140
+ address: {
3141
+ variations: ['address', 'addresses', 'endereco', 'enderecos'],
3142
+ icon: '📍',
3143
+ description: 'Gestão de endereços e localização',
3144
+ },
3145
+ auth: {
3146
+ variations: ['auth', 'login', 'token', 'authentication'],
3147
+ icon: '🔐',
3148
+ description: 'Autenticação e autorização',
3149
+ },
3150
+ monitoring: {
3151
+ variations: ['health', 'status', 'ping', 'monitor'],
3152
+ icon: '📊',
3153
+ description: 'Monitoramento e status do sistema',
3154
+ },
3155
+ };
3156
+ // Detectar categorias baseado nos padrões encontrados
3157
+ for (const [segment, urls] of Object.entries(urlPatterns)) {
3158
+ if (urls.length >= 1) {
3159
+ // Se aparecer pelo menos uma vez
3160
+ for (const [categoryKey, categoryData] of Object.entries(knownPatterns)) {
3161
+ if (categoryData.variations.some((variation) => segment.includes(variation) || variation.includes(segment))) {
3162
+ if (!detectedCategories[categoryKey]) {
3163
+ detectedCategories[categoryKey] = {
3164
+ pattern: categoryData.variations,
3165
+ description: categoryData.description,
3166
+ icon: categoryData.icon,
3167
+ };
3168
+ }
3169
+ break;
3170
+ }
3171
+ }
3172
+ }
3173
+ }
3174
+ // Se não encontrou categorias específicas, criar categorias genéricas baseadas nos segmentos mais comuns
3175
+ if (Object.keys(detectedCategories).length === 0) {
3176
+ const sortedSegments = Object.entries(urlPatterns)
3177
+ .sort(([, a], [, b]) => b.length - a.length)
3178
+ .slice(0, 5);
3179
+ sortedSegments.forEach(([segment, urls], index) => {
3180
+ detectedCategories[segment] = {
3181
+ pattern: [segment],
3182
+ description: `Operações relacionadas a ${segment}`,
3183
+ icon: ['🔵', '🟢', '🟡', '🟠', '🔴'][index] || '⚪',
3184
+ };
3185
+ });
3186
+ }
3187
+ // Adicionar categoria "Outras" para endpoints não categorizados
3188
+ detectedCategories['outras'] = {
3189
+ pattern: [],
3190
+ description: 'Outros endpoints não categorizados',
3191
+ icon: '⚡',
3192
+ };
3193
+ return detectedCategories;
3194
+ }
3195
+ /**
3196
+ * 🏷️ Categorizar endpoint com base nas categorias detectadas dinamicamente
3197
+ */
3198
+ static categorizeEndpointDynamic(url, detectedCategories) {
3199
+ const lowerUrl = url.toLowerCase();
3200
+ // Verificar contra categorias detectadas automaticamente
3201
+ for (const [categoryName, categoryData] of Object.entries(detectedCategories)) {
3202
+ if (categoryName === 'outras')
3203
+ continue; // Pular a categoria "outras" na verificação
3204
+ if (categoryData.pattern.some((pattern) => lowerUrl.includes(`/${pattern}/`) ||
3205
+ lowerUrl.includes(`/${pattern}`) ||
3206
+ lowerUrl.includes(pattern))) {
3207
+ return categoryName.charAt(0).toUpperCase() + categoryName.slice(1);
3208
+ }
3209
+ }
3210
+ return 'Outras';
3211
+ }
3212
+ /**
3213
+ * 🎯 Categorizar endpoint
3214
+ */
3215
+ static categorizeEndpoint(path) {
3216
+ // 🏢 Categorização baseada nos padrões da empresa
3217
+ // Cliente - Operações relacionadas ao cliente
3218
+ if (path.includes('/customers/') || path.includes('/customer/'))
3219
+ return 'Cliente';
3220
+ // Catálogo - Produtos, devices, portfolios, preços
3221
+ if (path.includes('/products/') ||
3222
+ path.includes('/devices/') ||
3223
+ path.includes('/portfolio/') ||
3224
+ path.includes('/productType') ||
3225
+ path.includes('/typeTrading') ||
3226
+ path.includes('/offersCatalog'))
3227
+ return 'Catalogo';
3228
+ // Fatura - Faturas e cobranças
3229
+ if (path.includes('/invoices/') ||
3230
+ path.includes('/billing/') ||
3231
+ path.includes('/payment/'))
3232
+ return 'Fatura';
3233
+ // Simulação - Simulações de venda e negociação
3234
+ if (path.includes('/simulacoes/') ||
3235
+ path.includes('/simulation/') ||
3236
+ path.includes('/quote/'))
3237
+ return 'Simulação';
3238
+ // Documentação - Geração de documentos e contratos
3239
+ if (path.includes('/documentation/') ||
3240
+ path.includes('/documents/') ||
3241
+ path.includes('/gerarDocumentacao') ||
3242
+ path.includes('/aceite') ||
3243
+ path.includes('/gedoc'))
3244
+ return 'Documentação';
3245
+ // Workflow - Processos de aprovação e esteira
3246
+ if (path.includes('/workflow/') ||
3247
+ path.includes('/approval/') ||
3248
+ path.includes('/esteira') ||
3249
+ path.includes('/solicitacoes'))
3250
+ return 'Workflow';
3251
+ // Auxiliar - Serviços de apoio e consultas gerais
3252
+ if (path.includes('/auxiliares/') ||
3253
+ path.includes('/operadoras/') ||
3254
+ path.includes('/geographicAddress') ||
3255
+ path.includes('/validate'))
3256
+ return 'Auxiliar';
3257
+ // Autenticação
3258
+ if (path.includes('/auth/') ||
3259
+ path.includes('/login/') ||
3260
+ path.includes('/token/'))
3261
+ return 'Autenticação';
3262
+ // Health checks e monitoramento
3263
+ if (path.includes('/health') ||
3264
+ path.includes('/status') ||
3265
+ path.includes('/ping'))
3266
+ return 'Monitoramento';
3267
+ // Fallback genérico
3268
+ return 'Geral';
3269
+ }
3270
+ /**
3271
+ * 📄 Converter para YAML
3272
+ */
3273
+ static convertToYaml(obj) {
3274
+ try {
3275
+ // Limpar dados problemáticos antes da conversão
3276
+ const cleanedObj = AutoDocs.cleanObjectForYaml(obj, false);
3277
+ // Usar biblioteca js-yaml para conversão correta
3278
+ const yamlContent = yaml.dump(cleanedObj, {
3279
+ indent: 2,
3280
+ lineWidth: -1,
3281
+ noRefs: true,
3282
+ sortKeys: false,
3283
+ quotingType: '"',
3284
+ forceQuotes: false,
3285
+ noCompatMode: true,
3286
+ });
3287
+ return `# OpenAPI Specification
3288
+ # Gerado automaticamente pelo AutoDocs
3289
+
3290
+ ${yamlContent}`;
3291
+ }
3292
+ catch (error) {
3293
+ Logger.error('❌ Erro ao converter para YAML:', error);
3294
+ // Fallback para conversão simples
3295
+ return `# OpenAPI Specification
3296
+ # Gerado automaticamente pelo AutoDocs
3297
+
3298
+ openapi: "3.0.0"
3299
+ info:
3300
+ title: "API Documentation"
3301
+ version: "1.0.0"
3302
+ description: "Erro na conversão YAML - ${error}"
3303
+ `;
3304
+ }
3305
+ }
3306
+ /**
3307
+ * 🧹 Limpar objeto para conversão YAML
3308
+ */
3309
+ static cleanObjectForYaml(obj, isPathsSection = false) {
3310
+ if (obj === null || obj === undefined) {
3311
+ return obj;
3312
+ }
3313
+ if (typeof obj === 'string') {
3314
+ // Limpar caracteres problemáticos em strings
3315
+ return obj
3316
+ .replace(/\\/g, '\\\\') // Escapar barras invertidas
3317
+ .replace(/\n/g, '\\n') // Escapar quebras de linha
3318
+ .replace(/\t/g, '\\t') // Escapar tabs
3319
+ .replace(/\r/g, '\\r'); // Escapar carriage returns
3320
+ }
3321
+ if (Array.isArray(obj)) {
3322
+ return obj.map((item) => AutoDocs.cleanObjectForYaml(item, false));
3323
+ }
3324
+ if (typeof obj === 'object') {
3325
+ const cleaned = {};
3326
+ Object.keys(obj).forEach((key) => {
3327
+ // 🔧 CORREÇÃO: Preservar paths do OpenAPI intactos
3328
+ let cleanKey;
3329
+ if (isPathsSection && key.startsWith('/')) {
3330
+ // Para paths do OpenAPI, manter o formato original
3331
+ cleanKey = key;
3332
+ }
3333
+ else if (key === 'paths') {
3334
+ // Quando encontrar a seção paths, marcar para tratamento especial
3335
+ cleaned[key] = AutoDocs.cleanObjectForYaml(obj[key], true);
3336
+ return;
3337
+ }
3338
+ else {
3339
+ // Para outras chaves, limpar normalmente
3340
+ cleanKey = key.replace(/[^\w\-_]/g, '_');
3341
+ }
3342
+ cleaned[cleanKey] = AutoDocs.cleanObjectForYaml(obj[key], false);
3343
+ });
3344
+ return cleaned;
3345
+ }
3346
+ return obj;
3347
+ }
3348
+ /**
3349
+ * 🔍 Inferir schema de um objeto
3350
+ * ✅ CORRIGIDO: Tipos válidos OpenAPI 3.0 (array, boolean, integer, number, object, string)
3351
+ * ✅ CORRIGIDO: required só é adicionado se tiver pelo menos 1 item
3352
+ */
3353
+ static inferSchema(obj) {
3354
+ // 🔧 null -> string com nullable: true (OpenAPI 3.0 não suporta type: 'null')
3355
+ if (obj === null || obj === undefined) {
3356
+ return { type: 'string', nullable: true };
3357
+ }
3358
+ if (typeof obj === 'string')
3359
+ return { type: 'string', example: obj };
3360
+ if (typeof obj === 'number') {
3361
+ // Verificar se é inteiro ou decimal
3362
+ return Number.isInteger(obj)
3363
+ ? { type: 'integer', example: obj }
3364
+ : { type: 'number', example: obj };
3365
+ }
3366
+ if (typeof obj === 'boolean')
3367
+ return { type: 'boolean', example: obj };
3368
+ if (Array.isArray(obj)) {
3369
+ return {
3370
+ type: 'array',
3371
+ items: obj.length > 0 ? AutoDocs.inferSchema(obj[0]) : { type: 'string' },
3372
+ };
3373
+ }
3374
+ if (typeof obj === 'object') {
3375
+ const properties = {};
3376
+ const keys = Object.keys(obj);
3377
+ keys.forEach((key) => {
3378
+ properties[key] = AutoDocs.inferSchema(obj[key]);
3379
+ });
3380
+ // 🔧 FIX: required só é adicionado se tiver pelo menos 1 item (OpenAPI exige min 1)
3381
+ const schema = {
3382
+ type: 'object',
3383
+ properties,
3384
+ };
3385
+ // Só adicionar required se tiver itens
3386
+ if (keys.length > 0) {
3387
+ schema.required = keys;
3388
+ }
3389
+ return schema;
3390
+ }
3391
+ return { type: 'string' };
3392
+ }
3393
+ /**
3394
+ * 📝 Gerar nome de schema
3395
+ * ✅ CORRIGIDO: Sanitiza caracteres inválidos (OpenAPI só permite A-Z a-z 0-9 - . _)
3396
+ */
3397
+ static generateSchemaName(url, suffix) {
3398
+ try {
3399
+ // 🎯 Lidar com URLs relativas e absolutas
3400
+ let pathname;
3401
+ if (url.startsWith('http://') || url.startsWith('https://')) {
3402
+ const urlObj = new URL(url);
3403
+ pathname = urlObj.pathname;
3404
+ }
3405
+ else {
3406
+ pathname = url.split('?')[0]; // URL relativa
3407
+ }
3408
+ const pathParts = pathname.split('/').filter((p) => p);
3409
+ let name = pathParts.length > 0 ? pathParts[pathParts.length - 1] : 'Unknown';
3410
+ // 🔧 FIX: Sanitizar nome - OpenAPI só permite: A-Z a-z 0-9 - . _
3411
+ // Substituir caracteres inválidos (|, %, @, #, etc.) por underscore
3412
+ name = AutoDocs.sanitizeSchemaName(name);
3413
+ return `${name}${suffix}`;
3414
+ }
3415
+ catch {
3416
+ return `Unknown${suffix}`;
3417
+ }
3418
+ }
3419
+ /**
3420
+ * 🔧 Sanitizar nome de schema para OpenAPI
3421
+ * OpenAPI só permite: A-Z a-z 0-9 - . _
3422
+ */
3423
+ static sanitizeSchemaName(name) {
3424
+ // Decodificar URL encoding primeiro (%7C -> |)
3425
+ let decoded = name;
3426
+ try {
3427
+ decoded = decodeURIComponent(name);
3428
+ }
3429
+ catch {
3430
+ // Se falhar, usar o nome original
3431
+ }
3432
+ // Substituir caracteres inválidos por underscore
3433
+ // Caracteres válidos: A-Z a-z 0-9 - . _
3434
+ return decoded
3435
+ .replace(/[|]/g, '_OR_') // | -> _OR_
3436
+ .replace(/@/g, '_AT_') // @ -> _AT_
3437
+ .replace(/#/g, '_HASH_') // # -> _HASH_
3438
+ .replace(/%/g, '_PCT_') // % -> _PCT_
3439
+ .replace(/[^A-Za-z0-9._-]/g, '_') // Outros inválidos -> _
3440
+ .replace(/_+/g, '_') // Múltiplos underscores -> um só
3441
+ .replace(/^_|_$/g, ''); // Remover underscores no início/fim
3442
+ }
3443
+ /**
3444
+ * 🔧 Extrair parâmetros da API
3445
+ */
3446
+ static extractParameters(api) {
3447
+ const parameters = [];
3448
+ try {
3449
+ // 🎯 Lidar com URLs relativas e absolutas
3450
+ let searchParams;
3451
+ if (api.url.startsWith('http://') || api.url.startsWith('https://')) {
3452
+ const url = new URL(api.url);
3453
+ searchParams = url.searchParams;
3454
+ }
3455
+ else {
3456
+ // URL relativa - extrair query string manualmente
3457
+ const queryString = api.url.split('?')[1];
3458
+ searchParams = new URLSearchParams(queryString || '');
3459
+ }
3460
+ // Query parameters
3461
+ searchParams.forEach((value, key) => {
3462
+ parameters.push({
3463
+ name: key,
3464
+ in: 'query',
3465
+ schema: { type: 'string' },
3466
+ example: value,
3467
+ });
3468
+ });
3469
+ // Headers importantes
3470
+ if (api.headers) {
3471
+ Object.keys(api.headers).forEach((header) => {
3472
+ if (!['content-type', 'user-agent', 'accept'].includes(header.toLowerCase())) {
3473
+ parameters.push({
3474
+ name: header,
3475
+ in: 'header',
3476
+ schema: { type: 'string' },
3477
+ example: api.headers[header],
3478
+ });
3479
+ }
3480
+ });
3481
+ }
3482
+ }
3483
+ catch {
3484
+ // Ignorar erros de parsing
3485
+ }
3486
+ return parameters;
3487
+ }
3488
+ /**
3489
+ * 🛤️ Gerar paths do Swagger
3490
+ */
3491
+ static generateSwaggerPaths(apis) {
3492
+ const paths = {};
3493
+ apis.forEach((api) => {
3494
+ const pathKey = api.url.replace(/https?:\/\/[^/]+/, '') || '/unknown';
3495
+ const method = api.method.toLowerCase();
3496
+ if (!paths[pathKey]) {
3497
+ paths[pathKey] = {};
3498
+ }
3499
+ paths[pathKey][method] = {
3500
+ summary: `${api.method} ${pathKey}`,
3501
+ description: `Detectado no teste: ${api.testName}`,
3502
+ responses: {
3503
+ [api.statusCode || '200']: {
3504
+ description: 'Response',
3505
+ },
3506
+ },
3507
+ };
3508
+ });
3509
+ return paths;
3510
+ }
3511
+ static async ensureOutputDir() {
3512
+ try {
3513
+ await fs.promises.mkdir(AutoDocs.config.outputDir, { recursive: true });
3514
+ }
3515
+ catch (error) {
3516
+ Logger.warning(`⚠️ AutoDocs: Erro ao criar diretório de saída: ${error}`);
3517
+ }
3518
+ }
3519
+ }
3520
+ // 🚀 AUTO-INICIALIZAÇÃO COMPLETA: Sistema totalmente automático
3521
+ try {
3522
+ // Evitar múltiplas inicializações - usar flag global
3523
+ const globalFlag = 'AUTOCORE_AUTODOCS_INITIALIZED';
3524
+ const isAlreadyInitialized = global[globalFlag] === true;
3525
+ if (!isAlreadyInitialized) {
3526
+ // Detectar ambiente Playwright/Teste de forma mais robusta
3527
+ const isPlaywrightEnvironment = process.env.PLAYWRIGHT_TEST_BASE_URL !== undefined ||
3528
+ process.env.PWTEST_SKIP_TEST_OUTPUT !== undefined ||
3529
+ process.env.NODE_ENV === 'test' ||
3530
+ process.env.VITEST === 'true' ||
3531
+ process.env.JEST_WORKER_ID !== undefined ||
3532
+ typeof globalThis.test === 'function' ||
3533
+ typeof globalThis.expect === 'function' ||
3534
+ typeof globalThis.it === 'function' ||
3535
+ typeof globalThis.describe === 'function' ||
3536
+ process.argv.some((arg) => arg.includes('playwright') ||
3537
+ arg.includes('vitest') ||
3538
+ arg.includes('jest'));
3539
+ if (isPlaywrightEnvironment) {
3540
+ // Marcar como inicializado globalmente
3541
+ ;
3542
+ global[globalFlag] = true;
3543
+ // Auto-inicialização no próximo tick
3544
+ process.nextTick(async () => {
3545
+ try {
3546
+ await AutoDocs.initialize({
3547
+ projectName: process.env.npm_package_name || 'Test Hub',
3548
+ outputDir: './docs',
3549
+ config: {
3550
+ enabled: true,
3551
+ categories: ['api', 'ssh', 'db', 'ui', 'mobile'],
3552
+ generateSwagger: true,
3553
+ includeRequestBody: true,
3554
+ includeResponseBody: true,
3555
+ },
3556
+ });
3557
+ }
3558
+ catch (error) {
3559
+ process.stderr.write(` ⚠️ AutoDocs: Erro na auto-inicialização: ${error}\n`);
3560
+ }
3561
+ });
3562
+ // Auto-finalização quando o processo termina - APENAS UMA VEZ
3563
+ const globalFlagFinalization = 'AUTOCORE_AUTODOCS_FINALIZED';
3564
+ const autoFinalize = async () => {
3565
+ if (global[globalFlagFinalization] === true) {
3566
+ return; // Evitar execuções duplicadas
3567
+ }
3568
+ ;
3569
+ global[globalFlagFinalization] = true;
3570
+ try {
3571
+ process.stdout.write('🏁 AutoDocs: Finalizando e gerando documentação automática...\n');
3572
+ await AutoDocs.generateDocs();
3573
+ process.stdout.write('✅ AutoDocs: Documentação gerada automaticamente em ./docs\n');
3574
+ }
3575
+ catch (error) {
3576
+ process.stderr.write(`❌ AutoDocs: Erro na finalização automática: ${error}\n`);
3577
+ }
3578
+ };
3579
+ // Hook de finalização única - evitar múltiplas execuções
3580
+ process.once('beforeExit', autoFinalize);
3581
+ }
3582
+ }
3583
+ }
3584
+ catch {
3585
+ // Ignorar erros de auto-inicialização silenciosamente
3586
+ }