@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,1083 @@
1
+ /**
2
+ * 📱 Sistema de Visualização Integrada para DeviceFarm
3
+ * @description Integra visualização local com o DeviceFarm remoto com detecção automática de projetos consumidores
4
+ * @author AutoCore Team
5
+ * @version 2.0.0
6
+ */
7
+ import { copyFileSync, existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync, } from 'node:fs';
8
+ import { basename, join, resolve } from 'node:path';
9
+ import { execSync } from 'node:child_process';
10
+ import { cwd } from 'node:process';
11
+ // Logger simples para evitar dependências circulares
12
+ const SimpleLogger = {
13
+ info: (message) => console.log(`ℹ️ ${message}`),
14
+ warning: (message) => console.warn(`⚠️ ${message}`),
15
+ error: (message) => console.error(`❌ ${message}`),
16
+ };
17
+ /**
18
+ * 📱 DeviceFarm Viewer com suporte a projetos consumidores
19
+ */
20
+ export class DeviceFarmViewer {
21
+ // Estado interno
22
+ static isActive = false;
23
+ static session = null;
24
+ static stepCounter = 0;
25
+ static consumerProjectPath = null;
26
+ /**
27
+ * 🎯 Configura projeto consumidor automaticamente
28
+ */
29
+ static async setupConsumerProject() {
30
+ try {
31
+ const currentPath = cwd();
32
+ // Detectar se estamos em um projeto consumidor (não autocore)
33
+ if (!currentPath.includes('autocore') &&
34
+ existsSync(join(currentPath, 'package.json'))) {
35
+ const packageJson = JSON.parse(readFileSync(join(currentPath, 'package.json'), 'utf8'));
36
+ // Verificar se tem dependência do autocore
37
+ const hasAutocore = packageJson.dependencies?.['comp-autocore-v1'] ||
38
+ packageJson.devDependencies?.['comp-autocore-v1'];
39
+ if (hasAutocore) {
40
+ DeviceFarmViewer.consumerProjectPath = currentPath;
41
+ SimpleLogger.info(`🎯 Projeto consumidor detectado: ${basename(currentPath)}`);
42
+ // Criar diretório de screenshots no consumidor se não existir
43
+ const consumerScreenshotPath = join(currentPath, 'test-results', 'mobile-screenshots');
44
+ if (!existsSync(consumerScreenshotPath)) {
45
+ mkdirSync(consumerScreenshotPath, { recursive: true });
46
+ }
47
+ }
48
+ }
49
+ }
50
+ catch (error) {
51
+ SimpleLogger.warning(`⚠️ Erro ao configurar projeto consumidor: ${String(error)}`);
52
+ }
53
+ }
54
+ /**
55
+ * 🔄 Sincroniza screenshots do projeto consumidor para o viewer
56
+ */
57
+ static async syncConsumerScreenshots() {
58
+ if (!DeviceFarmViewer.consumerProjectPath) {
59
+ return;
60
+ }
61
+ try {
62
+ const consumerScreenshotPath = join(DeviceFarmViewer.consumerProjectPath, 'test-results', 'mobile-screenshots');
63
+ const viewerScreenshotPath = join('./test-results/mobile-viewer/screenshots');
64
+ // Criar diretório do viewer se não existir
65
+ if (!existsSync(viewerScreenshotPath)) {
66
+ mkdirSync(viewerScreenshotPath, { recursive: true });
67
+ }
68
+ // Sincronizar screenshots
69
+ if (existsSync(consumerScreenshotPath)) {
70
+ const screenshots = readdirSync(consumerScreenshotPath).filter((file) => file.endsWith('.png') || file.endsWith('.jpg'));
71
+ let syncedCount = 0;
72
+ for (const screenshot of screenshots) {
73
+ const sourcePath = join(consumerScreenshotPath, screenshot);
74
+ const destPath = join(viewerScreenshotPath, screenshot);
75
+ // Copiar apenas se não existir ou se for mais recente
76
+ if (!existsSync(destPath) ||
77
+ statSync(sourcePath).mtime > statSync(destPath).mtime) {
78
+ copyFileSync(sourcePath, destPath);
79
+ syncedCount++;
80
+ // Adicionar ao viewer se ainda não estiver
81
+ DeviceFarmViewer.addScreenshotFromSync(screenshot, `Sync ${syncedCount}`);
82
+ }
83
+ }
84
+ if (syncedCount > 0) {
85
+ SimpleLogger.info(`🔄 Sincronizados ${syncedCount} screenshots do projeto consumidor`);
86
+ }
87
+ }
88
+ }
89
+ catch (error) {
90
+ SimpleLogger.warning(`⚠️ Erro ao sincronizar screenshots: ${String(error)}`);
91
+ }
92
+ }
93
+ /**
94
+ * 📸 Adiciona screenshot sincronizado
95
+ */
96
+ static addScreenshotFromSync(filename, description) {
97
+ if (!(DeviceFarmViewer.isActive && DeviceFarmViewer.session))
98
+ return;
99
+ // Verificar se já existe
100
+ const exists = DeviceFarmViewer.session.screenshots.some((s) => s.filename === filename);
101
+ if (exists)
102
+ return;
103
+ const screenshot = {
104
+ timestamp: new Date().toISOString(),
105
+ filename,
106
+ description,
107
+ step: ++DeviceFarmViewer.stepCounter,
108
+ base64: DeviceFarmViewer.loadScreenshotAsDataUri(filename),
109
+ };
110
+ DeviceFarmViewer.session.screenshots.push(screenshot);
111
+ DeviceFarmViewer.saveSessionData();
112
+ }
113
+ /**
114
+ * 🚀 Inicia o viewer integrado
115
+ */
116
+ static async start(deviceId = 'unknown') {
117
+ try {
118
+ // Configurar projeto consumidor
119
+ await DeviceFarmViewer.setupConsumerProject();
120
+ DeviceFarmViewer.isActive = true;
121
+ DeviceFarmViewer.stepCounter = 0;
122
+ // Criar diretórios necessários
123
+ DeviceFarmViewer.ensureDirectories();
124
+ // Inicializar sessão
125
+ DeviceFarmViewer.session = {
126
+ deviceId,
127
+ startTime: new Date().toISOString(),
128
+ status: 'WAITING',
129
+ screenshots: [],
130
+ stepCount: 0,
131
+ lastUpdate: new Date().toISOString(),
132
+ consumerProject: DeviceFarmViewer.consumerProjectPath
133
+ ? basename(DeviceFarmViewer.consumerProjectPath)
134
+ : undefined,
135
+ deviceFarmUrl: `https://devicefarm-qa.com.br/#/watcher?device=${deviceId}`,
136
+ };
137
+ // Salvar dados iniciais
138
+ await DeviceFarmViewer.saveSessionData();
139
+ // Gerar HTML integrado
140
+ const htmlPath = await DeviceFarmViewer.generateIntegratedHtml();
141
+ // ✅ ABERTURA AUTOMÁTICA DO HTML (controlada por variável de ambiente)
142
+ // Abre APENAS se MOBILE_UI_MODE=true (comandos ui:*) OU se DISABLE_DEVICEFARM_AUTOOPEN não está definido como true
143
+ const isUiMode = process.env.MOBILE_UI_MODE === 'true';
144
+ const autoOpenDisabled = process.env.DISABLE_DEVICEFARM_AUTOOPEN === 'true';
145
+ const shouldAutoOpen = isUiMode && !autoOpenDisabled;
146
+ if (shouldAutoOpen) {
147
+ try {
148
+ const absoluteHtmlPath = resolve(htmlPath);
149
+ const fileUrl = `file:///${absoluteHtmlPath.replace(/\\/g, '/')}`;
150
+ // Tentar abrir no navegador padrão do sistema
151
+ if (process.platform === 'win32') {
152
+ execSync(`start "" "${fileUrl}"`);
153
+ }
154
+ else if (process.platform === 'darwin') {
155
+ execSync(`open "${fileUrl}"`);
156
+ }
157
+ else {
158
+ execSync(`xdg-open "${fileUrl}"`);
159
+ }
160
+ console.log(`🌐 HTML aberto automaticamente no navegador: ${fileUrl}`);
161
+ }
162
+ catch (openError) {
163
+ SimpleLogger.warning(`Não foi possível abrir automaticamente o HTML: ${String(openError)}`);
164
+ console.log(`📋 Abra manualmente: ${resolve(htmlPath)}`);
165
+ }
166
+ }
167
+ else {
168
+ SimpleLogger.info(`📋 Auto-abertura desabilitada. Arquivo disponível em: ${resolve(htmlPath)}`);
169
+ }
170
+ console.log(`🎥 DeviceFarm Viewer iniciado: ${htmlPath}`);
171
+ return htmlPath;
172
+ }
173
+ catch (error) {
174
+ SimpleLogger.error(`❌ Erro ao iniciar DeviceFarm Viewer: ${String(error)}`);
175
+ throw error;
176
+ }
177
+ }
178
+ /**
179
+ * 🎨 Gera HTML integrado com DeviceFarm com dados embutidos (SEM SERVIDOR)
180
+ */
181
+ static generateIntegratedHtml() {
182
+ const htmlPath = join('./test-results/mobile-viewer', 'devicefarm-integrated-viewer.html');
183
+ const deviceId = DeviceFarmViewer.session?.deviceId || 'unknown';
184
+ DeviceFarmViewer.ensureBase64ForScreenshots();
185
+ // ✅ HTML COMPLETAMENTE OFFLINE - Dados embutidos no JavaScript
186
+ const htmlContent = [
187
+ '<!DOCTYPE html>',
188
+ '<html lang="pt-BR">',
189
+ '<head>',
190
+ ' <meta charset="UTF-8">',
191
+ ' <meta name="viewport" content="width=device-width, initial-scale=1.0">',
192
+ ' <title>📱 DeviceFarm Live Viewer - AUTO REFRESH</title>',
193
+ ' <script src="https://cdn.tailwindcss.com"></script>',
194
+ ' <script src="https://cdnjs.cloudflare.com/ajax/libs/jspdf/2.5.1/jspdf.umd.min.js"></script>',
195
+ ' <script src="https://cdnjs.cloudflare.com/ajax/libs/html2canvas/1.4.1/html2canvas.min.js"></script>',
196
+ ' <script src="https://cdn.jsdelivr.net/npm/xlsx@0.18.5/dist/xlsx.full.min.js"></script>',
197
+ ' <style>',
198
+ ' @keyframes pulse-live { 0%, 100% { opacity: 1; transform: scale(1); } 50% { opacity: 0.7; transform: scale(1.05); } }',
199
+ ' .live-pulse { animation: pulse-live 1.5s infinite; }',
200
+ ' body { font-family: "Inter", "Segoe UI", sans-serif; transition: all 0.3s ease; }',
201
+ ' .light-mode { background: linear-gradient(135deg, #f8fafc 0%, #e2e8f0 50%, #cbd5e1 100%) !important; color: #1e293b !important; }',
202
+ ' .light-mode .text-white { color: #1e293b !important; }',
203
+ ' .light-mode .text-slate-200 { color: #475569 !important; }',
204
+ ' .light-mode .text-slate-300 { color: #64748b !important; }',
205
+ ' .light-mode .text-slate-400 { color: #94a3b8 !important; }',
206
+ ' .light-mode .text-yellow-400 { color: #d97706 !important; }',
207
+ ' .light-mode .bg-slate-900\\/50 { background: rgba(248, 250, 252, 0.9) !important; border-color: rgba(203, 213, 225, 0.5) !important; }',
208
+ ' .light-mode .bg-slate-800\\/60 { background: rgba(241, 245, 249, 0.8) !important; border-color: rgba(203, 213, 225, 0.6) !important; }',
209
+ ' .light-mode .bg-slate-800\\/40 { background: rgba(248, 250, 252, 0.7) !important; border-color: rgba(203, 213, 225, 0.5) !important; }',
210
+ ' .light-mode .bg-slate-900\\/30 { background: rgba(241, 245, 249, 0.6) !important; border-color: rgba(203, 213, 225, 0.5) !important; }',
211
+ ' .light-mode .bg-slate-700\\/30 { background: rgba(226, 232, 240, 0.7) !important; border-color: rgba(203, 213, 225, 0.6) !important; }',
212
+ ' .light-mode .border-slate-700\\/50 { border-color: rgba(203, 213, 225, 0.5) !important; }',
213
+ ' .light-mode .border-slate-600\\/50 { border-color: rgba(203, 213, 225, 0.6) !important; }',
214
+ ' .light-mode .border-slate-600\\/30 { border-color: rgba(203, 213, 225, 0.4) !important; }',
215
+ ' .btn-export { transition: all 0.2s ease; }',
216
+ ' .btn-export:hover { transform: translateY(-1px); }',
217
+ ' </style>',
218
+ '</head>',
219
+ '<body id="viewer-root" class="bg-gradient-to-br from-slate-900 via-slate-800 to-slate-700 text-white min-h-screen transition-all duration-300">',
220
+ ' <!-- Header com estilo moderno -->',
221
+ ' <div class="bg-slate-900/50 backdrop-blur-sm border-b border-slate-700/50 px-8 py-4">',
222
+ ' <div class="flex justify-between items-center">',
223
+ ' <h1 class="text-2xl font-bold text-yellow-400 flex items-center gap-2">',
224
+ ' 📱 <span>DeviceFarm Live Viewer</span>',
225
+ ' </h1>',
226
+ ' <div class="flex gap-4 text-sm">',
227
+ ' <div class="bg-slate-800/60 px-4 py-2 rounded-lg border border-slate-600/50">',
228
+ ' <span class="text-slate-300">Device:</span> ',
229
+ ' <span id="device-id" class="text-white font-semibold">' +
230
+ deviceId +
231
+ '</span>',
232
+ ' </div>',
233
+ ' <div class="bg-slate-800/60 px-4 py-2 rounded-lg border border-slate-600/50">',
234
+ ' <span class="text-slate-300">Screenshots:</span> ',
235
+ ' <span id="screenshot-count" class="text-emerald-400 font-bold">0</span>',
236
+ ' </div>',
237
+ ' <div class="bg-slate-800/60 px-4 py-2 rounded-lg border border-slate-600/50">',
238
+ ' <span class="text-slate-300">Status:</span> ',
239
+ ' <span id="session-status" class="text-blue-400 font-semibold">LOADING</span>',
240
+ ' </div>',
241
+ ' <div class="bg-green-500/20 border border-green-500/30 px-4 py-2 rounded-lg live-pulse flex items-center gap-2">',
242
+ ' <span class="text-green-400 font-bold">🔄 AUTO-REFRESH:</span>',
243
+ ' <select id="refresh-interval" class="bg-slate-700 text-white px-3 py-1 rounded border border-slate-500 font-semibold cursor-pointer">',
244
+ ' <option value="1">1s</option>',
245
+ ' <option value="5">5s</option>',
246
+ ' <option value="10">10s</option>',
247
+ ' <option value="20" selected>20s</option>',
248
+ ' <option value="30">30s</option>',
249
+ ' <option value="60">1min</option>',
250
+ ' </select>',
251
+ ' <span id="countdown" class="text-yellow-400 font-mono text-sm">20s</span>',
252
+ ' </div>',
253
+ ' </div>',
254
+ ' </div>',
255
+ ' </div>',
256
+ '',
257
+ ' <!-- Barra de controles e exportação -->',
258
+ ' <div class="bg-slate-800/30 border-b border-slate-700/30 px-8 py-3">',
259
+ ' <div class="flex gap-2 justify-end">',
260
+ ' <button id="toggle-theme" class="bg-slate-700 hover:bg-slate-600 text-white px-4 py-2 rounded-lg font-medium transition-all duration-200 btn-export flex items-center gap-2" title="Alternar tema">',
261
+ ' 🌙 <span>Tema</span>',
262
+ ' </button>',
263
+ ' <button id="export-pdf" class="bg-red-600 hover:bg-red-700 text-white px-4 py-2 rounded-lg font-medium transition-all duration-200 btn-export flex items-center gap-2" title="Exportar PDF">',
264
+ ' 📄 <span>PDF</span>',
265
+ ' </button>',
266
+ ' <button id="export-json" class="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-lg font-medium transition-all duration-200 btn-export flex items-center gap-2" title="Exportar JSON">',
267
+ ' 🗒️ <span>JSON</span>',
268
+ ' </button>',
269
+ ' <button id="export-xlsx" class="bg-green-600 hover:bg-green-700 text-white px-4 py-2 rounded-lg font-medium transition-all duration-200 btn-export flex items-center gap-2" title="Exportar XLSX">',
270
+ ' 📊 <span>XLSX</span>',
271
+ ' </button>',
272
+ ' </div>',
273
+ ' </div>',
274
+ '',
275
+ ' <!-- Conteúdo principal -->',
276
+ ' <div class="flex h-[calc(100vh-140px)]">',
277
+ ' <!-- Painel esquerdo - Screenshot principal -->',
278
+ ' <div class="flex-1 lg:flex-[2] p-6">',
279
+ ' <div class="bg-slate-800/40 backdrop-blur-sm border border-slate-700/50 rounded-2xl p-6 h-full shadow-2xl">',
280
+ ' <!-- Container de aguardo -->',
281
+ ' <div id="waiting-container" class="flex flex-col items-center justify-center h-full text-center">',
282
+ ' <div class="bg-slate-700/30 p-8 rounded-xl border border-slate-600/30 shadow-lg">',
283
+ ' <div class="text-6xl mb-4">⏳</div>',
284
+ ' <h3 class="text-xl font-semibold text-slate-200 mb-2">Aguardando screenshots...</h3>',
285
+ ' <p class="text-slate-400">🔄 Atualizando automaticamente</p>',
286
+ ' </div>',
287
+ ' </div>',
288
+ '',
289
+ ' <!-- Container do screenshot -->',
290
+ ' <div id="screenshot-container" class="flex flex-col items-center justify-center h-full" style="display: none;">',
291
+ ' <div class="bg-slate-900/50 p-4 rounded-xl border border-slate-600/30 max-w-full max-h-full shadow-xl">',
292
+ ' <img id="current-screenshot" src="" alt="Screenshot atual" ',
293
+ ' class="max-w-full max-h-[70vh] rounded-lg shadow-2xl border border-slate-600/30">',
294
+ ' <div id="screenshot-info" class="mt-4 text-center">',
295
+ ' <div id="screenshot-description" class="text-lg font-medium text-slate-200 mb-1">-</div>',
296
+ ' <div id="screenshot-timestamp" class="text-sm text-slate-400">-</div>',
297
+ ' </div>',
298
+ ' </div>',
299
+ ' </div>',
300
+ ' </div>',
301
+ '',
302
+ ' <!-- Botões de ação -->',
303
+ ' <div class="flex gap-3 mt-4">',
304
+ ' <button onclick="openDeviceFarm()" ',
305
+ ' class="bg-blue-600 hover:bg-blue-700 text-white px-6 py-3 rounded-xl font-semibold transition-all duration-200 flex items-center gap-2 shadow-lg hover:shadow-xl hover:scale-105">',
306
+ ' 🌐 Abrir DeviceFarm',
307
+ ' </button>',
308
+ ' <button onclick="refreshData()" ',
309
+ ' class="bg-emerald-600 hover:bg-emerald-700 text-white px-6 py-3 rounded-xl font-semibold transition-all duration-200 flex items-center gap-2 shadow-lg hover:shadow-xl hover:scale-105">',
310
+ ' 🔄 Recarregar',
311
+ ' </button>',
312
+ ' </div>',
313
+ ' </div>',
314
+ ' <!-- Painel direito - Lista de screenshots -->',
315
+ ' <div class="w-full lg:w-80 xl:w-96 p-6 bg-slate-900/30 backdrop-blur-sm border-l border-slate-700/50 overflow-y-auto">',
316
+ ' <h3 class="text-xl font-bold text-yellow-400 mb-4 flex items-center gap-2">',
317
+ ' 📸 <span>Screenshots em Tempo Real</span>',
318
+ ' </h3>',
319
+ '',
320
+ ' <!-- Info box -->',
321
+ ' <div class="bg-slate-800/60 border border-slate-600/40 rounded-xl p-4 mb-6">',
322
+ ' <div class="text-green-400 font-semibold mb-2 flex items-center gap-2">',
323
+ ' ✅ <span>Auto-Refresh Ativo</span>',
324
+ ' </div>',
325
+ ' <div class="text-sm text-slate-300 space-y-1">',
326
+ ' <div>📱 Página atualiza automaticamente a cada 3 segundos</div>',
327
+ ' <div>📸 Screenshots aparecem conforme o teste executa</div>',
328
+ ' </div>',
329
+ ' </div>',
330
+ '',
331
+ ' <!-- Lista de screenshots -->',
332
+ ' <div id="screenshots-list" class="space-y-3">',
333
+ ' <div class="text-slate-400 text-center py-8">',
334
+ ' 📷 Screenshots aparecerão aqui conforme o teste executa',
335
+ ' </div>',
336
+ ' </div>',
337
+ ' </div>',
338
+ ' </div>',
339
+ '',
340
+ ' <script>',
341
+ ' // ✅ DADOS EMBUTIDOS - Atualizados dinamicamente',
342
+ ' let sessionData = ' +
343
+ JSON.stringify(DeviceFarmViewer.session || {
344
+ deviceId,
345
+ startTime: new Date().toISOString(),
346
+ status: 'WAITING',
347
+ screenshots: [],
348
+ stepCount: 0,
349
+ lastUpdate: new Date().toISOString(),
350
+ }, null, 8) +
351
+ ';',
352
+ '',
353
+ ' // ⏱️ CONTROLE DE AUTO-REFRESH',
354
+ ' let refreshInterval = 20; // segundos (padrão)',
355
+ ' let countdownSeconds = refreshInterval;',
356
+ ' let countdownTimer = null;',
357
+ ' let refreshTimer = null;',
358
+ '',
359
+ ' function startAutoRefresh() {',
360
+ ' // Limpar timers existentes',
361
+ ' if (countdownTimer) clearInterval(countdownTimer);',
362
+ ' if (refreshTimer) clearTimeout(refreshTimer);',
363
+ ' ',
364
+ ' // Resetar countdown',
365
+ ' countdownSeconds = refreshInterval;',
366
+ ' updateCountdown();',
367
+ ' ',
368
+ ' // Countdown visual',
369
+ ' countdownTimer = setInterval(() => {',
370
+ ' countdownSeconds--;',
371
+ ' updateCountdown();',
372
+ ' ',
373
+ ' if (countdownSeconds <= 0) {',
374
+ ' clearInterval(countdownTimer);',
375
+ ' }',
376
+ ' }, 1000);',
377
+ ' ',
378
+ ' // Timer de refresh',
379
+ ' refreshTimer = setTimeout(() => {',
380
+ ' location.reload();',
381
+ ' }, refreshInterval * 1000);',
382
+ ' }',
383
+ '',
384
+ ' function updateCountdown() {',
385
+ ' const countdownEl = document.getElementById("countdown");',
386
+ ' if (countdownEl) {',
387
+ ' if (countdownSeconds >= 60) {',
388
+ ' countdownEl.textContent = Math.floor(countdownSeconds / 60) + "min " + (countdownSeconds % 60) + "s";',
389
+ ' } else {',
390
+ ' countdownEl.textContent = countdownSeconds + "s";',
391
+ ' }',
392
+ ' }',
393
+ ' }',
394
+ '',
395
+ ' function updateUI() {',
396
+ ' if (!sessionData) return;',
397
+ ' ',
398
+ ' document.getElementById("device-id").textContent = sessionData.deviceId || "' +
399
+ deviceId +
400
+ '";',
401
+ ' document.getElementById("screenshot-count").textContent = sessionData.screenshots ? sessionData.screenshots.length : 0;',
402
+ ' document.getElementById("session-status").textContent = sessionData.status || "WAITING";',
403
+ ' ',
404
+ ' updateScreenshots();',
405
+ ' ',
406
+ ' // 🆕 SEMPRE MOSTRAR SCREENSHOT (se houver)',
407
+ ' const screenshotContainer = document.getElementById("screenshot-container");',
408
+ ' const waitingContainer = document.getElementById("waiting-container");',
409
+ ' ',
410
+ ' if (sessionData.screenshots && sessionData.screenshots.length > 0) {',
411
+ ' if (waitingContainer) waitingContainer.style.display = "none";',
412
+ ' if (screenshotContainer) screenshotContainer.style.display = "flex";',
413
+ ' showLatestScreenshot();',
414
+ ' } else {',
415
+ ' if (waitingContainer) waitingContainer.style.display = "flex";',
416
+ ' if (screenshotContainer) screenshotContainer.style.display = "none";',
417
+ ' }',
418
+ ' }',
419
+ '',
420
+ ' function updateScreenshots() {',
421
+ ' const screenshotsList = document.getElementById("screenshots-list");',
422
+ ' ',
423
+ ' if (!sessionData.screenshots || sessionData.screenshots.length === 0) {',
424
+ ' screenshotsList.innerHTML = "<div class=\\"text-slate-400 text-center py-8\\">📷 Screenshots aparecerão aqui conforme o teste executa</div>";',
425
+ ' return;',
426
+ ' }',
427
+ ' ',
428
+ ' const screenshots = sessionData.screenshots.slice().reverse();',
429
+ ' let html = "";',
430
+ ' for (let i = 0; i < screenshots.length; i++) {',
431
+ ' const s = screenshots[i];',
432
+ ' html += "<div class=\\"bg-slate-800/40 border border-slate-600/30 rounded-xl p-4 cursor-pointer transition-all duration-200 hover:bg-slate-700/50 hover:border-slate-500/50 hover:shadow-lg\\" onclick=\\"showScreenshot(\'" + s.filename + "\', \'" + s.description + "\', \'" + s.timestamp + "\')\\">";',
433
+ ' html += "<div class=\\"text-slate-200 font-medium mb-1\\"><span class=\\"text-yellow-400 font-semibold\\">Step " + s.step + ":</span> " + s.description + "</div>";',
434
+ ' html += "<div class=\\"text-xs text-slate-400\\">" + formatTime(s.timestamp) + "</div>";',
435
+ ' html += "</div>";',
436
+ ' }',
437
+ ' screenshotsList.innerHTML = html;',
438
+ ' }',
439
+ '',
440
+ ' function showLatestScreenshot() {',
441
+ ' if (sessionData.screenshots && sessionData.screenshots.length > 0) {',
442
+ ' const latest = sessionData.screenshots[sessionData.screenshots.length - 1];',
443
+ ' showScreenshot(latest.filename, latest.description, latest.timestamp);',
444
+ ' }',
445
+ ' }',
446
+ '',
447
+ ' function showScreenshot(filename, description, timestamp) {',
448
+ ' const container = document.getElementById("screenshot-container");',
449
+ ' const img = document.getElementById("current-screenshot");',
450
+ ' const desc = document.getElementById("screenshot-description");',
451
+ ' const time = document.getElementById("screenshot-timestamp");',
452
+ ' ',
453
+ ' const screenshotItem = sessionData && sessionData.screenshots ? sessionData.screenshots.find(s => s.filename === filename) : null;',
454
+ ' const base64Data = screenshotItem && screenshotItem.base64 ? screenshotItem.base64 : null;',
455
+ ' const screenshotPath = base64Data || "./screenshots/" + filename;',
456
+ ' ',
457
+ ' img.onload = function() {',
458
+ ' container.style.display = "block";',
459
+ ' desc.textContent = description;',
460
+ ' time.textContent = formatTime(timestamp);',
461
+ ' console.log("📸 Screenshot carregado:", screenshotPath);',
462
+ ' };',
463
+ ' ',
464
+ ' img.onerror = function() {',
465
+ ' if (base64Data) {',
466
+ ' return;',
467
+ ' }',
468
+ ' console.log("⚠️ Screenshot não encontrado:", screenshotPath);',
469
+ ' const altPath = "../mobile-screenshots/" + filename;',
470
+ ' img.src = altPath;',
471
+ ' img.onerror = function() {',
472
+ ' console.log("⚠️ Screenshot alternativo não encontrado:", altPath);',
473
+ ' desc.textContent = "⚠️ " + description + " (imagem não encontrada)";',
474
+ ' time.textContent = formatTime(timestamp);',
475
+ ' container.style.display = "block";',
476
+ ' };',
477
+ ' };',
478
+ ' ',
479
+ ' img.src = screenshotPath;',
480
+ ' }',
481
+ '',
482
+ ' function formatTime(timestamp) {',
483
+ ' try {',
484
+ ' return new Date(timestamp).toLocaleString("pt-BR");',
485
+ ' } catch {',
486
+ ' return timestamp;',
487
+ ' }',
488
+ ' }',
489
+ '',
490
+ ' function openDeviceFarm() {',
491
+ ' if (sessionData && sessionData.deviceFarmUrl) {',
492
+ ' window.open(sessionData.deviceFarmUrl, "_blank");',
493
+ ' } else {',
494
+ ' window.open("https://devicefarm-qa.com.br/#/watcher?device=' +
495
+ deviceId +
496
+ '", "_blank");',
497
+ ' }',
498
+ ' }',
499
+ '',
500
+ ' function refreshData() {',
501
+ ' location.reload();',
502
+ ' }',
503
+ '',
504
+ ' // 🎨 MODO CLARO/ESCURO',
505
+ ' function toggleTheme() {',
506
+ ' const root = document.getElementById("viewer-root");',
507
+ ' root.classList.toggle("light-mode");',
508
+ ' ',
509
+ ' // Atualizar classes TailwindCSS para modo claro',
510
+ ' if (root.classList.contains("light-mode")) {',
511
+ ' root.className = root.className.replace("bg-gradient-to-br from-slate-900 via-slate-800 to-slate-700", "bg-gradient-to-br from-gray-50 via-gray-100 to-gray-200");',
512
+ ' } else {',
513
+ ' root.className = root.className.replace("bg-gradient-to-br from-gray-50 via-gray-100 to-gray-200", "bg-gradient-to-br from-slate-900 via-slate-800 to-slate-700");',
514
+ ' }',
515
+ ' }',
516
+ '',
517
+ ' // 📊 FUNÇÕES DE EXPORTAÇÃO',
518
+ ' function getSessionData() {',
519
+ ' return {',
520
+ ' deviceId: sessionData.deviceId,',
521
+ ' status: sessionData.status,',
522
+ ' startTime: sessionData.startTime,',
523
+ ' endTime: sessionData.endTime,',
524
+ ' screenshots: sessionData.screenshots,',
525
+ ' stepCount: sessionData.stepCount,',
526
+ ' lastUpdate: sessionData.lastUpdate,',
527
+ ' exportDate: new Date().toISOString()',
528
+ ' };',
529
+ ' }',
530
+ '',
531
+ ' async function exportToPDF() {',
532
+ ' console.log("🔵 [PDF] Botão clicado - Iniciando geração do PDF do DeviceFarm...");',
533
+ ' ',
534
+ ' const element = document.getElementById("viewer-root");',
535
+ ' if (!element) {',
536
+ ' console.error("❌ [PDF] Elemento viewer-root não encontrado!");',
537
+ ' alert("❌ Erro: Elemento do relatório não encontrado!");',
538
+ ' return;',
539
+ ' }',
540
+ ' ',
541
+ ' const hasImages = element.querySelectorAll("img").length > 0;',
542
+ ' if (hasImages) {',
543
+ ' console.log("📸 [PDF] Imagens detectadas - preparando incorporação para ambiente offline");',
544
+ ' }',
545
+ ' ',
546
+ ' const originalBg = element.style.background;',
547
+ ' const originalColor = element.style.color;',
548
+ ' ',
549
+ ' // 🆕 Mostrar container de screenshot se houver screenshots',
550
+ ' const screenshotContainer = document.getElementById("screenshot-container");',
551
+ ' const waitingContainer = document.getElementById("waiting-container");',
552
+ ' const originalScreenshotDisplay = screenshotContainer ? screenshotContainer.style.display : "";',
553
+ ' const originalWaitingDisplay = waitingContainer ? waitingContainer.style.display : "";',
554
+ ' ',
555
+ ' if (sessionData && sessionData.screenshots && sessionData.screenshots.length > 0) {',
556
+ ' if (screenshotContainer) screenshotContainer.style.display = "block";',
557
+ ' if (waitingContainer) waitingContainer.style.display = "none";',
558
+ ' ',
559
+ ' // Carregar screenshot mais recente',
560
+ ' const latest = sessionData.screenshots[sessionData.screenshots.length - 1];',
561
+ ' showScreenshot(latest.filename, latest.description, latest.timestamp);',
562
+ ' }',
563
+ ' ',
564
+ ' // Aguardar imagens carregarem',
565
+ ' const images = element.querySelectorAll("img");',
566
+ ' console.log("📸 Total de imagens encontradas:", images.length);',
567
+ ' ',
568
+ ' await Promise.all(Array.from(images).map(img => {',
569
+ ' if (img.complete) return Promise.resolve();',
570
+ ' return new Promise(resolve => {',
571
+ ' img.onload = resolve;',
572
+ ' img.onerror = resolve;',
573
+ ' setTimeout(resolve, 2000);',
574
+ ' });',
575
+ ' }));',
576
+ ' ',
577
+ ' console.log("✅ Imagens aguardadas");',
578
+ ' await new Promise(resolve => setTimeout(resolve, 500));',
579
+ ' ',
580
+ ' // Configurar para PDF',
581
+ ' element.style.background = "#fff";',
582
+ ' element.style.color = "#222";',
583
+ ' ',
584
+ ' // Verificar se jsPDF está disponível',
585
+ ' if (typeof jspdf === "undefined") {',
586
+ ' console.error("❌ jsPDF não está carregado!");',
587
+ ' alert("❌ Erro: Biblioteca jsPDF não carregada. Recarregue a página e tente novamente.");',
588
+ ' // Restaurar estilos',
589
+ ' element.style.background = originalBg;',
590
+ ' element.style.color = originalColor;',
591
+ ' if (screenshotContainer) screenshotContainer.style.display = originalScreenshotDisplay;',
592
+ ' if (waitingContainer) waitingContainer.style.display = originalWaitingDisplay;',
593
+ ' return;',
594
+ ' }',
595
+ ' ',
596
+ ' console.log("📄 Iniciando geração do PDF...");',
597
+ ' ',
598
+ ' try {',
599
+ ' // Usar html2canvas para capturar o conteúdo como imagem',
600
+ ' const canvas = await html2canvas(element, {',
601
+ ' scale: 2,',
602
+ ' useCORS: false,',
603
+ ' allowTaint: true,',
604
+ ' backgroundColor: "#fff",',
605
+ ' logging: false,',
606
+ ' imageTimeout: 0',
607
+ ' });',
608
+ ' ',
609
+ ' // Criar PDF com jsPDF',
610
+ ' const { jsPDF } = jspdf;',
611
+ ' const pdf = new jsPDF({',
612
+ ' orientation: "portrait",',
613
+ ' unit: "mm",',
614
+ ' format: "a4"',
615
+ ' });',
616
+ ' ',
617
+ ' const imgData = canvas.toDataURL("image/jpeg", 0.98);',
618
+ ' const imgWidth = 210; // A4 width in mm',
619
+ ' const pageHeight = 297; // A4 height in mm',
620
+ ' const imgHeight = (canvas.height * imgWidth) / canvas.width;',
621
+ ' let heightLeft = imgHeight;',
622
+ ' let position = 0;',
623
+ ' ',
624
+ ' // Adicionar primeira página',
625
+ ' pdf.addImage(imgData, "JPEG", 0, position, imgWidth, imgHeight);',
626
+ ' heightLeft -= pageHeight;',
627
+ ' ',
628
+ ' // Adicionar páginas extras se necessário',
629
+ ' while (heightLeft > 0) {',
630
+ ' position = heightLeft - imgHeight;',
631
+ ' pdf.addPage();',
632
+ ' pdf.addImage(imgData, "JPEG", 0, position, imgWidth, imgHeight);',
633
+ ' heightLeft -= pageHeight;',
634
+ ' }',
635
+ ' ',
636
+ ' // Salvar PDF',
637
+ ' pdf.save(`devicefarm-viewer-${sessionData.deviceId}-${new Date().toISOString().split("T")[0]}.pdf`);',
638
+ ' ',
639
+ ' console.log("✅ PDF gerado com sucesso!");',
640
+ ' alert("✅ PDF gerado com sucesso! Verifique sua pasta de Downloads.");',
641
+ ' } catch (error) {',
642
+ ' console.error("❌ Erro ao gerar PDF:", error);',
643
+ ' alert("❌ Erro ao gerar PDF: " + (error.message || error));',
644
+ ' } finally {',
645
+ ' // Restaurar estilos',
646
+ ' element.style.background = originalBg;',
647
+ ' element.style.color = originalColor;',
648
+ ' if (screenshotContainer) screenshotContainer.style.display = originalScreenshotDisplay;',
649
+ ' if (waitingContainer) waitingContainer.style.display = originalWaitingDisplay;',
650
+ ' }',
651
+ ' }',
652
+ '',
653
+ ' function exportToJSON() {',
654
+ ' const data = getSessionData();',
655
+ ' const blob = new Blob([JSON.stringify(data, null, 2)], { type: "application/json" });',
656
+ ' const url = URL.createObjectURL(blob);',
657
+ ' const a = document.createElement("a");',
658
+ ' a.href = url;',
659
+ ' a.download = `devicefarm-session-${sessionData.deviceId}-${new Date().toISOString().split("T")[0]}.json`;',
660
+ ' a.click();',
661
+ ' URL.revokeObjectURL(url);',
662
+ ' }',
663
+ '',
664
+ ' function exportToXLSX() {',
665
+ ' const screenshots = sessionData.screenshots || [];',
666
+ ' const data = screenshots.map(s => ({',
667
+ ' "Step": s.step,',
668
+ ' "Descrição": s.description,',
669
+ ' "Arquivo": s.filename,',
670
+ ' "Timestamp": formatTime(s.timestamp)',
671
+ ' }));',
672
+ ' ',
673
+ ' // Adicionar informações da sessão',
674
+ ' const sessionInfo = [{',
675
+ ' "Step": "INFO",',
676
+ ' "Descrição": `Device: ${sessionData.deviceId}`,',
677
+ ' "Arquivo": `Status: ${sessionData.status}`,',
678
+ ' "Timestamp": `Total Screenshots: ${screenshots.length}`',
679
+ ' }];',
680
+ ' ',
681
+ ' const allData = [...sessionInfo, ...data];',
682
+ ' const ws = XLSX.utils.json_to_sheet(allData);',
683
+ ' const wb = XLSX.utils.book_new();',
684
+ ' XLSX.utils.book_append_sheet(wb, ws, "DeviceFarm Session");',
685
+ ' XLSX.writeFile(wb, `devicefarm-session-${sessionData.deviceId}-${new Date().toISOString().split("T")[0]}.xlsx`);',
686
+ ' }',
687
+ '',
688
+ ' // 🎯 EVENT LISTENERS',
689
+ ' document.getElementById("toggle-theme").onclick = toggleTheme;',
690
+ ' document.getElementById("export-pdf").onclick = exportToPDF;',
691
+ ' document.getElementById("export-json").onclick = exportToJSON;',
692
+ ' document.getElementById("export-xlsx").onclick = exportToXLSX;',
693
+ '',
694
+ ' // 🆕 Listener para mudança no intervalo de refresh',
695
+ ' document.getElementById("refresh-interval").onchange = function(e) {',
696
+ ' refreshInterval = parseInt(e.target.value);',
697
+ ' console.log("🔄 Intervalo de refresh alterado para:", refreshInterval, "segundos");',
698
+ ' startAutoRefresh(); // Reiniciar com novo intervalo',
699
+ ' };',
700
+ '',
701
+ ' // ✅ AUTO-INICIALIZAR',
702
+ ' document.addEventListener("DOMContentLoaded", function() {',
703
+ ' updateUI();',
704
+ ' startAutoRefresh(); // Iniciar auto-refresh',
705
+ ' console.log("📱 DeviceFarm Viewer inicializado em modo offline");',
706
+ ' console.log("🔄 Auto-refresh configurado para", refreshInterval, "segundos");',
707
+ ' });',
708
+ ' </script>',
709
+ '</body>',
710
+ '</html>',
711
+ ].join('\n');
712
+ // Salvar arquivo HTML
713
+ writeFileSync(htmlPath, htmlContent, 'utf8');
714
+ return htmlPath;
715
+ }
716
+ /**
717
+ * 📄 JavaScript para polling e atualização dinâmica
718
+ */
719
+ static getViewerJavaScript(deviceId) {
720
+ return [
721
+ ' <script>',
722
+ ' let sessionData = null;',
723
+ ' let lastUpdateTime = 0;',
724
+ ' let refreshInterval = null;',
725
+ ' let testFinished = false;',
726
+ ' let noActivityCount = 0;',
727
+ ' let currentPollingInterval = 3000; // Inicia com 3 segundos',
728
+ ' ',
729
+ ' function startDataPolling() {',
730
+ ' refreshInterval = setInterval(loadSessionData, currentPollingInterval);',
731
+ ' loadSessionData();',
732
+ ' }',
733
+ ' ',
734
+ ' function adjustPollingRate() {',
735
+ ' if (testFinished) {',
736
+ ' console.log("🏁 Teste finalizado - parando polling");',
737
+ ' if (refreshInterval) {',
738
+ ' clearInterval(refreshInterval);',
739
+ ' refreshInterval = null;',
740
+ ' }',
741
+ ' return;',
742
+ ' }',
743
+ ' ',
744
+ ' // Se não houve atividade por muito tempo, aumentar intervalo',
745
+ ' if (noActivityCount > 5) {',
746
+ ' if (currentPollingInterval < 10000) {',
747
+ ' currentPollingInterval = 10000; // 10 segundos',
748
+ ' console.log("⏱️ Reduzindo frequência de polling para 10s (sem atividade)");',
749
+ ' // Recriar intervalo com nova frequência',
750
+ ' if (refreshInterval) {',
751
+ ' clearInterval(refreshInterval);',
752
+ ' }',
753
+ ' refreshInterval = setInterval(loadSessionData, currentPollingInterval);',
754
+ ' }',
755
+ ' }',
756
+ ' }',
757
+ ' ',
758
+ ' async function loadSessionData() {',
759
+ ' try {',
760
+ ' const response = await fetch("./session-data.json?t=" + Date.now());',
761
+ ' if (response.ok) {',
762
+ ' const data = await response.json();',
763
+ ' if (data && data.lastUpdate !== lastUpdateTime) {',
764
+ ' sessionData = data;',
765
+ ' lastUpdateTime = data.lastUpdate;',
766
+ ' noActivityCount = 0; // Reset contador',
767
+ ' ',
768
+ ' // Verificar se o teste terminou',
769
+ ' if (data.status === "FINISHED") {',
770
+ ' testFinished = true;',
771
+ ' console.log("🏁 Status FINISHED detectado");',
772
+ ' }',
773
+ ' ',
774
+ ' updateUI();',
775
+ ' console.log("📊 Dados atualizados:", data);',
776
+ ' } else {',
777
+ ' noActivityCount++;',
778
+ ' adjustPollingRate();',
779
+ ' }',
780
+ ' } else {',
781
+ ' console.log("📊 Aguardando dados da sessão...");',
782
+ ' noActivityCount++;',
783
+ ' adjustPollingRate();',
784
+ ' }',
785
+ ' } catch (error) {',
786
+ ' console.log("⚠️ Erro ao carregar dados:", error);',
787
+ ' noActivityCount++;',
788
+ ' adjustPollingRate();',
789
+ ' }',
790
+ ' }',
791
+ ' ',
792
+ ' function updateUI() {',
793
+ ' if (!sessionData) return;',
794
+ ' ',
795
+ ' document.getElementById("device-id").textContent = sessionData.deviceId || "' +
796
+ deviceId +
797
+ '";',
798
+ ' document.getElementById("screenshot-count").textContent = sessionData.screenshots ? sessionData.screenshots.length : 0;',
799
+ ' document.getElementById("session-status").textContent = sessionData.status || "UNKNOWN";',
800
+ ' ',
801
+ ' updateScreenshots();',
802
+ ' ',
803
+ ' if (sessionData.screenshots && sessionData.screenshots.length > 0) {',
804
+ ' document.getElementById("waiting-container").style.display = "none";',
805
+ ' showLatestScreenshot();',
806
+ ' }',
807
+ ' }',
808
+ ' ',
809
+ ' function updateScreenshots() {',
810
+ ' const screenshotsList = document.getElementById("screenshots-list");',
811
+ ' ',
812
+ ' if (!sessionData.screenshots || sessionData.screenshots.length === 0) {',
813
+ ' screenshotsList.innerHTML = "<p style=\\"opacity: 0.7;\\">📷 Screenshots aparecerão aqui conforme o teste executa</p>";',
814
+ ' return;',
815
+ ' }',
816
+ ' ',
817
+ ' const screenshots = sessionData.screenshots.slice().reverse();',
818
+ ' let html = "";',
819
+ ' for (let i = 0; i < screenshots.length; i++) {',
820
+ ' const s = screenshots[i];',
821
+ ' html += "<div class=\\"screenshot-item\\" onclick=\\"showScreenshot(\'" + s.filename + "\', \'" + s.description + "\', \'" + s.timestamp + "\')\\">";',
822
+ ' html += "<div><strong>Step " + s.step + ":</strong> " + s.description + "</div>";',
823
+ ' html += "<div style=\\"font-size: 0.8em; opacity: 0.7;\\">" + formatTime(s.timestamp) + "</div>";',
824
+ ' html += "</div>";',
825
+ ' }',
826
+ ' screenshotsList.innerHTML = html;',
827
+ ' }',
828
+ ' ',
829
+ ' function showLatestScreenshot() {',
830
+ ' if (sessionData.screenshots && sessionData.screenshots.length > 0) {',
831
+ ' const latest = sessionData.screenshots[sessionData.screenshots.length - 1];',
832
+ ' showScreenshot(latest.filename, latest.description, latest.timestamp);',
833
+ ' }',
834
+ ' }',
835
+ ' ',
836
+ ' function showScreenshot(filename, description, timestamp) {',
837
+ ' const container = document.getElementById("screenshot-container");',
838
+ ' const img = document.getElementById("current-screenshot");',
839
+ ' const desc = document.getElementById("screenshot-description");',
840
+ ' const time = document.getElementById("screenshot-timestamp");',
841
+ ' ',
842
+ ' const screenshotItem = sessionData && sessionData.screenshots ? sessionData.screenshots.find(s => s.filename === filename) : null;',
843
+ ' const base64Data = screenshotItem && screenshotItem.base64 ? screenshotItem.base64 : null;',
844
+ ' const screenshotPath = base64Data || "./screenshots/" + filename;',
845
+ ' ',
846
+ ' img.onload = function() {',
847
+ ' container.style.display = "block";',
848
+ ' desc.textContent = description;',
849
+ ' time.textContent = formatTime(timestamp);',
850
+ ' console.log("📸 Screenshot carregado:", screenshotPath);',
851
+ ' };',
852
+ ' ',
853
+ ' img.onerror = function() {',
854
+ ' if (base64Data) {',
855
+ ' return;',
856
+ ' }',
857
+ ' console.log("⚠️ Screenshot não encontrado:", screenshotPath);',
858
+ ' const altPath = "../mobile-screenshots/" + filename;',
859
+ ' img.src = altPath;',
860
+ ' img.onerror = function() {',
861
+ ' console.log("⚠️ Screenshot alternativo não encontrado:", altPath);',
862
+ ' desc.textContent = "⚠️ " + description + " (imagem não encontrada)";',
863
+ ' time.textContent = formatTime(timestamp);',
864
+ ' container.style.display = "block";',
865
+ ' };',
866
+ ' };',
867
+ ' ',
868
+ ' img.src = screenshotPath;',
869
+ ' }',
870
+ ' ',
871
+ ' function formatTime(timestamp) {',
872
+ ' try {',
873
+ ' return new Date(timestamp).toLocaleString("pt-BR");',
874
+ ' } catch {',
875
+ ' return timestamp;',
876
+ ' }',
877
+ ' }',
878
+ ' ',
879
+ ' function openDeviceFarm() {',
880
+ ' if (sessionData && sessionData.deviceFarmUrl) {',
881
+ ' window.open(sessionData.deviceFarmUrl, "_blank");',
882
+ ' } else {',
883
+ ' window.open("https://devicefarm-qa.com.br/#/watcher?device=' +
884
+ deviceId +
885
+ '", "_blank");',
886
+ ' }',
887
+ ' }',
888
+ ' ',
889
+ ' function refreshData() {',
890
+ ' loadSessionData();',
891
+ ' }',
892
+ ' ',
893
+ ' window.onload = function() {',
894
+ ' console.log("🎥 DeviceFarm Viewer carregado - iniciando polling...");',
895
+ ' startDataPolling();',
896
+ ' };',
897
+ ' ',
898
+ ' window.onbeforeunload = function() {',
899
+ ' if (refreshInterval) {',
900
+ ' clearInterval(refreshInterval);',
901
+ ' }',
902
+ ' };',
903
+ ' </script>',
904
+ ].join('\n');
905
+ }
906
+ /**
907
+ * 📸 Adiciona screenshot (chamado automaticamente por MobileActions)
908
+ */
909
+ static addScreenshot(filename, description) {
910
+ if (!(DeviceFarmViewer.isActive && DeviceFarmViewer.session)) {
911
+ return;
912
+ }
913
+ try {
914
+ DeviceFarmViewer.stepCounter++;
915
+ const screenshot = {
916
+ timestamp: new Date().toISOString(),
917
+ filename,
918
+ description,
919
+ step: DeviceFarmViewer.stepCounter,
920
+ base64: DeviceFarmViewer.loadScreenshotAsDataUri(filename),
921
+ };
922
+ DeviceFarmViewer.session.screenshots.push(screenshot);
923
+ DeviceFarmViewer.session.stepCount = DeviceFarmViewer.stepCounter;
924
+ DeviceFarmViewer.session.lastUpdate = new Date().toISOString();
925
+ DeviceFarmViewer.session.status = 'ACTIVE';
926
+ // ✅ COPIA SCREENSHOT PARA PASTA DO VIEWER
927
+ DeviceFarmViewer.copyScreenshotToViewer(filename);
928
+ // Salvar dados atualizados
929
+ DeviceFarmViewer.saveSessionData();
930
+ // 🔄 REGENERAR HTML COM DADOS ATUALIZADOS
931
+ try {
932
+ DeviceFarmViewer.generateIntegratedHtml();
933
+ }
934
+ catch (err) {
935
+ SimpleLogger.warning(`Erro ao regenerar HTML: ${String(err)}`);
936
+ }
937
+ // Sincronizar screenshots do projeto consumidor
938
+ DeviceFarmViewer.syncConsumerScreenshots();
939
+ console.log(`📸 Screenshot #${DeviceFarmViewer.stepCounter}: ${description} - HTML atualizado`);
940
+ }
941
+ catch (error) {
942
+ SimpleLogger.warning(`Erro ao adicionar screenshot: ${String(error)}`);
943
+ }
944
+ }
945
+ /**
946
+ * 📋 Copia screenshot para pasta do viewer para exibição no HTML
947
+ */
948
+ static copyScreenshotToViewer(filename) {
949
+ try {
950
+ const sourcePath = join('./test-results/mobile-screenshots', filename);
951
+ const targetPath = join('./test-results/mobile-viewer/screenshots', filename);
952
+ if (existsSync(sourcePath)) {
953
+ copyFileSync(sourcePath, targetPath);
954
+ }
955
+ else {
956
+ SimpleLogger.warning(`Screenshot não encontrado: ${sourcePath}`);
957
+ }
958
+ }
959
+ catch (error) {
960
+ SimpleLogger.warning(`Erro ao copiar screenshot: ${String(error)}`);
961
+ }
962
+ }
963
+ /**
964
+ * � Garante que todos os screenshots possuam dados base64 embutidos
965
+ */
966
+ static ensureBase64ForScreenshots() {
967
+ if (!DeviceFarmViewer.session) {
968
+ return;
969
+ }
970
+ DeviceFarmViewer.session.screenshots =
971
+ DeviceFarmViewer.session.screenshots.map((item) => {
972
+ if (!item.base64) {
973
+ item.base64 =
974
+ DeviceFarmViewer.loadScreenshotAsDataUri(item.filename) ||
975
+ item.base64;
976
+ }
977
+ return item;
978
+ });
979
+ }
980
+ /**
981
+ * 🖼️ Converte screenshot em data URI para uso offline
982
+ */
983
+ static loadScreenshotAsDataUri(filename) {
984
+ if (!filename) {
985
+ return undefined;
986
+ }
987
+ const candidates = new Set();
988
+ candidates.add(join('./test-results/mobile-viewer/screenshots', filename));
989
+ candidates.add(join('./test-results/mobile-screenshots', filename));
990
+ if (DeviceFarmViewer.consumerProjectPath) {
991
+ candidates.add(join(DeviceFarmViewer.consumerProjectPath, 'test-results', 'mobile-viewer', 'screenshots', filename));
992
+ candidates.add(join(DeviceFarmViewer.consumerProjectPath, 'test-results', 'mobile-screenshots', filename));
993
+ }
994
+ for (const candidate of candidates) {
995
+ try {
996
+ if (existsSync(candidate)) {
997
+ const buffer = readFileSync(candidate);
998
+ const mime = DeviceFarmViewer.detectImageMimeType(candidate);
999
+ return `data:${mime};base64,${buffer.toString('base64')}`;
1000
+ }
1001
+ }
1002
+ catch (error) {
1003
+ SimpleLogger.warning(`⚠️ Erro ao embutir screenshot (${candidate}): ${String(error)}`);
1004
+ }
1005
+ }
1006
+ return undefined;
1007
+ }
1008
+ /**
1009
+ * 🧪 Detecta MIME-Type baseado na extensão do arquivo
1010
+ */
1011
+ static detectImageMimeType(filePath) {
1012
+ const ext = filePath.toLowerCase().split('.').pop();
1013
+ switch (ext) {
1014
+ case 'jpg':
1015
+ case 'jpeg':
1016
+ return 'image/jpeg';
1017
+ case 'gif':
1018
+ return 'image/gif';
1019
+ case 'webp':
1020
+ return 'image/webp';
1021
+ case 'bmp':
1022
+ return 'image/bmp';
1023
+ default:
1024
+ return 'image/png';
1025
+ }
1026
+ }
1027
+ /**
1028
+ * �🛑 Para o viewer
1029
+ */
1030
+ static async stop() {
1031
+ if (!(DeviceFarmViewer.isActive && DeviceFarmViewer.session))
1032
+ return;
1033
+ try {
1034
+ DeviceFarmViewer.session.status = 'FINISHED';
1035
+ DeviceFarmViewer.session.endTime = new Date().toISOString();
1036
+ DeviceFarmViewer.session.lastUpdate = new Date().toISOString();
1037
+ await DeviceFarmViewer.saveSessionData();
1038
+ DeviceFarmViewer.isActive = false;
1039
+ SimpleLogger.info(`🛑 Viewer finalizado - Total: ${DeviceFarmViewer.stepCounter} screenshots`);
1040
+ }
1041
+ catch (error) {
1042
+ SimpleLogger.warning(`⚠️ Erro ao finalizar viewer: ${String(error)}`);
1043
+ }
1044
+ }
1045
+ /**
1046
+ * 📁 Criar diretórios necessários
1047
+ */
1048
+ static ensureDirectories() {
1049
+ const dirs = [
1050
+ './test-results/mobile-screenshots',
1051
+ './test-results/mobile-viewer',
1052
+ './test-results/mobile-viewer/screenshots',
1053
+ ];
1054
+ dirs.forEach((dir) => {
1055
+ if (!existsSync(dir)) {
1056
+ mkdirSync(dir, { recursive: true });
1057
+ }
1058
+ });
1059
+ }
1060
+ /**
1061
+ * 💾 Salva dados da sessão
1062
+ */
1063
+ static saveSessionData() {
1064
+ try {
1065
+ const jsonPath = join('./test-results/mobile-viewer', 'session-data.json');
1066
+ DeviceFarmViewer.ensureBase64ForScreenshots();
1067
+ writeFileSync(jsonPath, JSON.stringify(DeviceFarmViewer.session, null, 2), 'utf8');
1068
+ }
1069
+ catch (error) {
1070
+ SimpleLogger.warning(`⚠️ Erro ao salvar dados da sessão: ${String(error)}`);
1071
+ }
1072
+ }
1073
+ /**
1074
+ * 📄 Estado atual (para debugging)
1075
+ */
1076
+ static getStatus() {
1077
+ return {
1078
+ isActive: DeviceFarmViewer.isActive,
1079
+ stepCounter: DeviceFarmViewer.stepCounter,
1080
+ consumerProject: DeviceFarmViewer.consumerProjectPath,
1081
+ };
1082
+ }
1083
+ }