@xenon-device-management/xenon 1.1.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 (228) hide show
  1. package/README.md +446 -0
  2. package/lib/package.json +207 -0
  3. package/lib/public/assets/Layouts-7IT8aFLI.js +11 -0
  4. package/lib/public/assets/Layouts-DPMls9vh.css +1 -0
  5. package/lib/public/assets/ai-settings-BbnfgdEx.js +11 -0
  6. package/lib/public/assets/apps-CRMrI4_p.js +16 -0
  7. package/lib/public/assets/apps-CcM77dgg.css +1 -0
  8. package/lib/public/assets/badge-B1nKs8zj.css +1 -0
  9. package/lib/public/assets/badge-CSvl5xIU.js +11 -0
  10. package/lib/public/assets/button-CJlKn4PZ.css +1 -0
  11. package/lib/public/assets/button-CvLaGFYj.js +26 -0
  12. package/lib/public/assets/calendar-6w-D6Oaw.js +6 -0
  13. package/lib/public/assets/clock-DcdeWBPr.js +6 -0
  14. package/lib/public/assets/cpu-DiSoXT9n.js +6 -0
  15. package/lib/public/assets/device-explorer-CajM63OJ.js +193 -0
  16. package/lib/public/assets/device-explorer-CxdUAoTL.css +1 -0
  17. package/lib/public/assets/index-ByQwMN5T.js +174 -0
  18. package/lib/public/assets/index-C1DBaoSh.js +1 -0
  19. package/lib/public/assets/index-qzCez_kk.css +1 -0
  20. package/lib/public/assets/lock-B23ibZmo.js +6 -0
  21. package/lib/public/assets/maintenance-settings-CirzA6yG.js +6 -0
  22. package/lib/public/assets/mouse-pointer-2-Cz76SHFb.js +6 -0
  23. package/lib/public/assets/plus-BBwlIevt.js +6 -0
  24. package/lib/public/assets/session-dashboard-C2k7FFv_.css +1 -0
  25. package/lib/public/assets/session-dashboard-HPDtwPOZ.js +62 -0
  26. package/lib/public/assets/settings-DrZsZwdc.js +1 -0
  27. package/lib/public/assets/trash-2-DQpvzJec.js +6 -0
  28. package/lib/public/assets/useSocket-Dxsqae2a.js +16 -0
  29. package/lib/public/assets/webhook-settings-CDPgsgkb.css +1 -0
  30. package/lib/public/assets/webhook-settings-Cp-B4Nrw.js +1 -0
  31. package/lib/public/assets/zap-DovP6iow.js +6 -0
  32. package/lib/public/favicon.ico +0 -0
  33. package/lib/public/favicon.png +0 -0
  34. package/lib/public/favicon.svg +9 -0
  35. package/lib/public/index.html +46 -0
  36. package/lib/public/logo.svg +17 -0
  37. package/lib/public/logo192.png +0 -0
  38. package/lib/public/logo512.png +0 -0
  39. package/lib/public/manifest.json +25 -0
  40. package/lib/public/robots.txt +3 -0
  41. package/lib/schema.json +348 -0
  42. package/lib/src/InternalHttpClient.js +212 -0
  43. package/lib/src/PluginContext.js +29 -0
  44. package/lib/src/XenonCapabilityManager.js +199 -0
  45. package/lib/src/app/index.js +167 -0
  46. package/lib/src/app/routers/apps.js +79 -0
  47. package/lib/src/app/routers/config.js +131 -0
  48. package/lib/src/app/routers/control.js +835 -0
  49. package/lib/src/app/routers/dashboard.js +301 -0
  50. package/lib/src/app/routers/grid.js +352 -0
  51. package/lib/src/app/routers/reservation.js +190 -0
  52. package/lib/src/app/routers/webhook.js +83 -0
  53. package/lib/src/app/swagger-docs.js +203 -0
  54. package/lib/src/app/swagger.js +366 -0
  55. package/lib/src/chromeUtils.js +148 -0
  56. package/lib/src/commands/handle.js +19 -0
  57. package/lib/src/commands/index.js +8 -0
  58. package/lib/src/config.js +73 -0
  59. package/lib/src/dashboard/asset-manager.js +84 -0
  60. package/lib/src/dashboard/commands.js +284 -0
  61. package/lib/src/dashboard/event-manager.js +699 -0
  62. package/lib/src/dashboard/services/app-service.js +134 -0
  63. package/lib/src/dashboard/services/failure-analysis-service.js +173 -0
  64. package/lib/src/dashboard/services/session-service.js +113 -0
  65. package/lib/src/data-service/CircuitBreaker.js +83 -0
  66. package/lib/src/data-service/config-service.js +155 -0
  67. package/lib/src/data-service/db.js +122 -0
  68. package/lib/src/data-service/device-service.js +320 -0
  69. package/lib/src/data-service/device-store.interface.js +2 -0
  70. package/lib/src/data-service/device-store.js +345 -0
  71. package/lib/src/data-service/pending-sessions-service.js +25 -0
  72. package/lib/src/data-service/pluginArgs.js +25 -0
  73. package/lib/src/data-service/prisma-service.js +31 -0
  74. package/lib/src/data-service/prisma-store.js +385 -0
  75. package/lib/src/data-service/queue-service.js +150 -0
  76. package/lib/src/data-service/web-config-service.js +130 -0
  77. package/lib/src/device-managers/AndroidDeviceManager.js +1155 -0
  78. package/lib/src/device-managers/ChromeDriverManager.js +68 -0
  79. package/lib/src/device-managers/HealthMonitorService.js +325 -0
  80. package/lib/src/device-managers/IOSDeviceManager.js +351 -0
  81. package/lib/src/device-managers/NodeDevices.js +82 -0
  82. package/lib/src/device-managers/android/AndroidStreamService.js +370 -0
  83. package/lib/src/device-managers/android/DeviceLockManager.js +45 -0
  84. package/lib/src/device-managers/cloud/CapabilityManager.js +26 -0
  85. package/lib/src/device-managers/cloud/Devices.js +86 -0
  86. package/lib/src/device-managers/iOSTracker.js +44 -0
  87. package/lib/src/device-managers/index.js +89 -0
  88. package/lib/src/device-managers/ios/IOSDiscoveryService.js +268 -0
  89. package/lib/src/device-managers/ios/IOSStreamService.js +893 -0
  90. package/lib/src/device-managers/ios/WDAClient.js +866 -0
  91. package/lib/src/device-utils.js +663 -0
  92. package/lib/src/enums/Capabilities.js +8 -0
  93. package/lib/src/enums/Cloud.js +11 -0
  94. package/lib/src/enums/Platform.js +9 -0
  95. package/lib/src/enums/SessionType.js +9 -0
  96. package/lib/src/enums/SocketEvents.js +15 -0
  97. package/lib/src/helpers/UniversalMjpegProxy.js +273 -0
  98. package/lib/src/helpers/index.js +229 -0
  99. package/lib/src/index.js +95 -0
  100. package/lib/src/interceptors/CommandInterceptor.js +524 -0
  101. package/lib/src/interfaces/ICloudManager.js +2 -0
  102. package/lib/src/interfaces/IDevice.js +2 -0
  103. package/lib/src/interfaces/IDeviceFilterOptions.js +2 -0
  104. package/lib/src/interfaces/IDeviceManager.js +2 -0
  105. package/lib/src/interfaces/IOptions.js +2 -0
  106. package/lib/src/interfaces/IPluginArgs.js +55 -0
  107. package/lib/src/interfaces/ISessionCapability.js +2 -0
  108. package/lib/src/logger.js +225 -0
  109. package/lib/src/plugin.js +244 -0
  110. package/lib/src/prisma.js +12 -0
  111. package/lib/src/profiling/AndroidAppProfiler.js +213 -0
  112. package/lib/src/proxy/wd-command-proxy.js +221 -0
  113. package/lib/src/scripts/generate-database-migration.js +59 -0
  114. package/lib/src/scripts/initialize-database.js +55 -0
  115. package/lib/src/scripts/install-go-ios.js +66 -0
  116. package/lib/src/scripts/prepare-prisma.js +89 -0
  117. package/lib/src/services/AICommandService.js +143 -0
  118. package/lib/src/services/AIService.js +466 -0
  119. package/lib/src/services/CleanupService.js +141 -0
  120. package/lib/src/services/EventBus.js +74 -0
  121. package/lib/src/services/InspectorService.js +395 -0
  122. package/lib/src/services/MetricsService.js +134 -0
  123. package/lib/src/services/NetworkConditioningService.js +173 -0
  124. package/lib/src/services/NotificationService.js +163 -0
  125. package/lib/src/services/RequestLogService.js +252 -0
  126. package/lib/src/services/ResourceIsolationService.js +122 -0
  127. package/lib/src/services/SecurityService.js +120 -0
  128. package/lib/src/services/ServerManager.js +284 -0
  129. package/lib/src/services/SessionHeartbeatService.js +158 -0
  130. package/lib/src/services/SessionLifecycleService.js +572 -0
  131. package/lib/src/services/SocketClient.js +71 -0
  132. package/lib/src/services/SocketServer.js +87 -0
  133. package/lib/src/services/TracingService.js +132 -0
  134. package/lib/src/services/VideoPipelineService.js +220 -0
  135. package/lib/src/services/healing/FuzzyXmlHealingProvider.js +333 -0
  136. package/lib/src/services/healing/HealEtalonService.js +98 -0
  137. package/lib/src/services/healing/HealedLocatorGenerator.js +132 -0
  138. package/lib/src/services/healing/HealingOrchestrator.js +165 -0
  139. package/lib/src/services/healing/LlmHealingProvider.js +77 -0
  140. package/lib/src/services/healing/OcrHealingProvider.js +119 -0
  141. package/lib/src/services/healing/ResilioTreeHealingProvider.js +100 -0
  142. package/lib/src/services/healing/VisualAiHealingProvider.js +90 -0
  143. package/lib/src/services/healing/types.js +12 -0
  144. package/lib/src/services/omni-vision/OmniVisionService.js +718 -0
  145. package/lib/src/services/omni-vision/VisionAssertionService.js +68 -0
  146. package/lib/src/sessions/CloudSession.js +42 -0
  147. package/lib/src/sessions/LocalSession.js +313 -0
  148. package/lib/src/sessions/RemoteSession.js +287 -0
  149. package/lib/src/sessions/SessionManager.js +238 -0
  150. package/lib/src/sessions/XenonSession.js +44 -0
  151. package/lib/src/types/CLIArgs.js +2 -0
  152. package/lib/src/types/CloudArgs.js +2 -0
  153. package/lib/src/types/CloudSchema.js +131 -0
  154. package/lib/src/types/DeviceType.js +2 -0
  155. package/lib/src/types/DeviceUpdate.js +2 -0
  156. package/lib/src/types/IOSDevice.js +2 -0
  157. package/lib/src/types/Platform.js +2 -0
  158. package/lib/src/types/SessionStatus.js +11 -0
  159. package/lib/src/validators/CapabilityValidator.js +93 -0
  160. package/lib/test/e2e/android/conf.spec.js +43 -0
  161. package/lib/test/e2e/android/conf2.spec.js +44 -0
  162. package/lib/test/e2e/android/conf3.spec.js +44 -0
  163. package/lib/test/e2e/e2ehelper.js +113 -0
  164. package/lib/test/e2e/hubnode/forward-request.spec.js +224 -0
  165. package/lib/test/e2e/hubnode/hubnode.spec.js +214 -0
  166. package/lib/test/e2e/ios/conf1.spec.js +39 -0
  167. package/lib/test/e2e/ios/conf2.spec.js +39 -0
  168. package/lib/test/e2e/plugin-harness.js +236 -0
  169. package/lib/test/e2e/plugin.spec.js +97 -0
  170. package/lib/test/e2e/telemetry_verification.spec.js +83 -0
  171. package/lib/test/e2e/video-recording-test.spec.js +63 -0
  172. package/lib/test/helpers/test-container.js +112 -0
  173. package/lib/test/integration/androidDevices.spec.js +137 -0
  174. package/lib/test/integration/cliArgs.js +73 -0
  175. package/lib/test/integration/ios/01iOSSimulator.spec.js +291 -0
  176. package/lib/test/integration/ios/02iOSDevices.spec.js +75 -0
  177. package/lib/test/integration/testHelpers.js +74 -0
  178. package/lib/test/unit/AndroidDeviceManager.spec.js +178 -0
  179. package/lib/test/unit/ChromeDriverManager.spec.js +26 -0
  180. package/lib/test/unit/CleanupService.spec.js +21 -0
  181. package/lib/test/unit/DeviceModel.spec.js +157 -0
  182. package/lib/test/unit/FuzzyXmlHealingProvider.test.js +294 -0
  183. package/lib/test/unit/GetAdbOriginal.js +42 -0
  184. package/lib/test/unit/HealingCascade.test.js +128 -0
  185. package/lib/test/unit/IOSDeviceManager.spec.js +261 -0
  186. package/lib/test/unit/RemoteIOs.spec.js +78 -0
  187. package/lib/test/unit/ResilioTreeHealingProvider.test.js +96 -0
  188. package/lib/test/unit/commands.spec.js +27 -0
  189. package/lib/test/unit/config.spec.js +27 -0
  190. package/lib/test/unit/device-service.spec.js +307 -0
  191. package/lib/test/unit/device-utils.spec.js +313 -0
  192. package/lib/test/unit/fixtures/device.config.js +4 -0
  193. package/lib/test/unit/fixtures/devices.js +89 -0
  194. package/lib/test/unit/helpers.spec.js +62 -0
  195. package/lib/test/unit/omni-vision.spec.js +100 -0
  196. package/lib/test/unit/plugin.spec.js +133 -0
  197. package/lib/tsconfig.tsbuildinfo +1 -0
  198. package/package.json +207 -0
  199. package/prisma/data.db +0 -0
  200. package/prisma/dev.db +0 -0
  201. package/prisma/dev.db-journal +0 -0
  202. package/prisma/migrations/20231011074725_initial_tables/migration.sql +47 -0
  203. package/prisma/migrations/20231226115334_update_session_log/migration.sql +2 -0
  204. package/prisma/migrations/20251204113710_add_video_recording_enabled/migration.sql +29 -0
  205. package/prisma/migrations/20251204132449_add_log_table/migration.sql +11 -0
  206. package/prisma/migrations/20251205050111_add_profiling_support/migration.sql +47 -0
  207. package/prisma/migrations/20251205050947_add_is_error_field/migration.sql +24 -0
  208. package/prisma/migrations/20260126201337_add_app_model/migration.sql +18 -0
  209. package/prisma/migrations/20260130115722_add_performance_trace_and_xenon_sync/migration.sql +2 -0
  210. package/prisma/migrations/20260130135114_add_device_models/migration.sql +57 -0
  211. package/prisma/migrations/20260130140655_make_systemport_optional/migration.sql +45 -0
  212. package/prisma/migrations/20260130140932_make_device_fields_optional/migration.sql +45 -0
  213. package/prisma/migrations/20260130141040_final_schema_fix/migration.sql +45 -0
  214. package/prisma/migrations/20260130143234_add_device_health_fields/migration.sql +4 -0
  215. package/prisma/migrations/20260130144921_add_failure_category/migration.sql +2 -0
  216. package/prisma/migrations/20260131151456_add_webhook_config/migration.sql +10 -0
  217. package/prisma/migrations/20260201094507_add_device_tags/migration.sql +11 -0
  218. package/prisma/migrations/20260201103410_add_managed_process/migration.sql +15 -0
  219. package/prisma/migrations/20260201140637_add_web_config/migration.sql +22 -0
  220. package/prisma/migrations/20260201162232_add_session_progress/migration.sql +2 -0
  221. package/prisma/migrations/20260201174231_add_total_healed_count/migration.sql +2 -0
  222. package/prisma/migrations/migration_lock.toml +3 -0
  223. package/prisma/schema.prisma +210 -0
  224. package/schema.json +348 -0
  225. package/scripts/build-xenon.sh +32 -0
  226. package/scripts/dev/debug-gemini.ts +44 -0
  227. package/scripts/generate-types-from-schema.js +86 -0
  228. package/scripts/install-compatible-driver.js +39 -0
@@ -0,0 +1,333 @@
1
+ "use strict";
2
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
+ return new (P || (P = Promise))(function (resolve, reject) {
5
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
9
+ });
10
+ };
11
+ var __importDefault = (this && this.__importDefault) || function (mod) {
12
+ return (mod && mod.__esModule) ? mod : { "default": mod };
13
+ };
14
+ Object.defineProperty(exports, "__esModule", { value: true });
15
+ exports.FuzzyXmlHealingProvider = void 0;
16
+ const types_1 = require("./types");
17
+ // @ts-ignore
18
+ const xmldom_1 = require("xmldom");
19
+ // @ts-ignore
20
+ const xpath_1 = require("xpath");
21
+ const typedi_1 = require("typedi");
22
+ const logger_1 = __importDefault(require("../../logger"));
23
+ const HealedLocatorGenerator_1 = require("./HealedLocatorGenerator");
24
+ class FuzzyXmlHealingProvider {
25
+ constructor(etalonService) {
26
+ this.etalonService = etalonService;
27
+ this.name = 'Fuzzy XML Provider';
28
+ this.tier = types_1.HealingTier.TIER_2_FUZZY_XML;
29
+ this.logger = logger_1.default.scope('FuzzyXmlHealing');
30
+ this.generator = typedi_1.Container.get(HealedLocatorGenerator_1.HealedLocatorGenerator);
31
+ }
32
+ heal(context) {
33
+ return __awaiter(this, void 0, void 0, function* () {
34
+ if (!context.pageSource) {
35
+ this.logger.debug('No page source available for fuzzy matching');
36
+ return null;
37
+ }
38
+ try {
39
+ const dom = new xmldom_1.DOMParser().parseFromString(context.pageSource);
40
+ // TIER 2+ Optimization: Use Baseline Signature if available
41
+ let etalon = null;
42
+ if (this.etalonService) {
43
+ etalon = yield this.etalonService.getSignature(context.selector);
44
+ }
45
+ const keywords = this.extractKeywords(context.selector);
46
+ if (keywords.length === 0 && !etalon)
47
+ return null;
48
+ if (etalon) {
49
+ this.logger.info('Baseline signature found for locator. Using weighted recovery...');
50
+ }
51
+ else {
52
+ this.logger.info(`No baseline found. Attempting fuzzy match for keywords: ${keywords.join(', ')}`);
53
+ }
54
+ // Find all elements with text or attributes matching keywords or etalon
55
+ const nodes = (0, xpath_1.select)('//*', dom);
56
+ let bestMatch = null;
57
+ let highestScore = 0;
58
+ for (const node of nodes) {
59
+ const score = this.calculateScore(node, keywords, etalon);
60
+ if (score > highestScore && score > 0.5) {
61
+ // Threshold 50% for POC robustness
62
+ highestScore = score;
63
+ bestMatch = node;
64
+ }
65
+ }
66
+ if (bestMatch) {
67
+ const candidateLocators = this.generator.generate(bestMatch);
68
+ // Also add the absolute XPath as the ultimate fallback
69
+ const absXpath = this.getAbsoluteXpath(bestMatch);
70
+ if (!candidateLocators.includes(absXpath)) {
71
+ candidateLocators.push(absXpath);
72
+ }
73
+ this.logger.info(`✅ Found fuzzy match with score ${highestScore.toFixed(2)}. Trying ${candidateLocators.length} candidate locators...`);
74
+ // Try ALL candidate locators until one actually resolves
75
+ for (const candidateXpath of candidateLocators) {
76
+ try {
77
+ this.logger.debug(` Trying candidate: ${candidateXpath}`);
78
+ const element = yield context.driver.findElement('xpath', candidateXpath);
79
+ const elementId = element.ELEMENT || element['element-6066-11e4-a52e-4f735466cecf'];
80
+ if (elementId) {
81
+ this.logger.info(`🎯 Resolved via candidate: ${candidateXpath}`);
82
+ return {
83
+ id: elementId,
84
+ tier: this.tier,
85
+ confidence: highestScore,
86
+ originalSelector: context.selector,
87
+ recommendedSelector: candidateXpath,
88
+ candidateSelectors: candidateLocators,
89
+ node: bestMatch,
90
+ message: `Found element via fuzzy matching (${(highestScore * 100).toFixed(0)}% confidence). Healed locator: ${candidateXpath}`,
91
+ };
92
+ }
93
+ }
94
+ catch (err) {
95
+ this.logger.debug(` Candidate failed: ${candidateXpath}`);
96
+ }
97
+ }
98
+ this.logger.warn(`All ${candidateLocators.length} candidate locators failed to resolve for fuzzy match.`);
99
+ }
100
+ }
101
+ catch (err) {
102
+ this.logger.error(`Error during fuzzy healing: ${err.message}`);
103
+ }
104
+ return null;
105
+ });
106
+ }
107
+ extractKeywords(selector) {
108
+ // Extract words from selector, ignoring XPath syntax and common keywords
109
+ const blacklist = ['and', 'or', 'text', 'contains', 'xpath', 'element'];
110
+ const cleaned = selector.replace(/[\/\@\[\]\=\'\"]/g, ' ');
111
+ return cleaned.split(/\s+/).filter((s) => s.length > 2 && !blacklist.includes(s.toLowerCase()));
112
+ }
113
+ calculateScore(node, keywords, etalon = null) {
114
+ const ANCHORS = {
115
+ 'content-desc': 2.0, // Android
116
+ 'resource-id': 1.5, // Android/iOS
117
+ 'accessibility-id': 2.0, // iOS
118
+ label: 1.5, // iOS
119
+ name: 1.5, // iOS — elevated: @name is the primary iOS identifier
120
+ id: 1.0, // Android
121
+ hint: 0.4, // Android
122
+ value: 0.5, // iOS
123
+ };
124
+ const nodeAttrs = node.attributes || [];
125
+ let totalScore = 0;
126
+ let totalWeight = 0;
127
+ // === CRITICAL GATE: Tag Name Match ===
128
+ // When we have a baseline etalon, we KNOW the exact element type.
129
+ // XCUIElementTypeButton should NEVER match XCUIElementTypeImage or XCUIElementTypeApplication.
130
+ if (etalon) {
131
+ const etalonTag = etalon.nodeName.toLowerCase();
132
+ const nodeTag = node.nodeName.toLowerCase();
133
+ // Exact match required when etalon is present (no fuzzy allowed for tag)
134
+ if (etalonTag !== nodeTag && etalonTag !== 'xcuielementtypeany' && etalonTag !== 'unknown') {
135
+ return 0; // Immediate rejection — wrong element type
136
+ }
137
+ // Tag matches perfectly
138
+ totalWeight += 1.0;
139
+ totalScore += 1.0;
140
+ }
141
+ else {
142
+ // No etalon: use fuzzy tag matching with keywords
143
+ const tagWeight = 0.5;
144
+ const tagSim = this.getBestSimilarity(node.nodeName, keywords);
145
+ totalWeight += tagWeight;
146
+ totalScore += tagSim * tagWeight;
147
+ }
148
+ // === 2. Semantic Text Matching ===
149
+ // Check text content and text-like attributes against baseline or keywords
150
+ const textWeight = etalon ? 0.5 : 1.0;
151
+ const textTargets = etalon
152
+ ? [etalon.attributes['text'], etalon.attributes['label'], etalon.attributes['name']].filter(Boolean)
153
+ : keywords;
154
+ let bestTextSim = 0;
155
+ if (textTargets.length > 0) {
156
+ bestTextSim = this.getBestSimilarity(node.textContent || '', textTargets);
157
+ // Also check text-like attributes: name, label, text, value
158
+ for (const attrName of ['text', 'name', 'label', 'value']) {
159
+ const attrVal = this.getAttrValue(node, attrName);
160
+ if (attrVal) {
161
+ const sim = this.getBestSimilarity(attrVal, textTargets);
162
+ if (sim > bestTextSim)
163
+ bestTextSim = sim;
164
+ }
165
+ }
166
+ }
167
+ totalWeight += textWeight;
168
+ totalScore += bestTextSim * textWeight;
169
+ // === 3. Anchor Attribute Matching ===
170
+ for (let i = 0; i < nodeAttrs.length; i++) {
171
+ const attr = nodeAttrs[i];
172
+ const attrName = attr.name.toLowerCase();
173
+ // Skip non-anchor, spatial, and already-handled attrs
174
+ if ([
175
+ 'x',
176
+ 'y',
177
+ 'width',
178
+ 'height',
179
+ 'text',
180
+ 'type',
181
+ 'enabled',
182
+ 'visible',
183
+ 'accessible',
184
+ 'index',
185
+ 'traits',
186
+ 'processId',
187
+ 'bundleId',
188
+ ].includes(attrName))
189
+ continue;
190
+ if (ANCHORS[attrName]) {
191
+ const weight = ANCHORS[attrName];
192
+ const etalonValue = etalon === null || etalon === void 0 ? void 0 : etalon.attributes[attrName];
193
+ const target = etalonValue ? [etalonValue] : keywords;
194
+ let sim = this.getBestSimilarity(attr.value, target);
195
+ // Exact match with etalon gets perfect score
196
+ if (etalonValue && attr.value === etalonValue) {
197
+ sim = 1.0;
198
+ }
199
+ totalWeight += weight;
200
+ totalScore += sim * weight;
201
+ }
202
+ }
203
+ // === 4. SPATIAL MATCHING: The Decisive Signal ===
204
+ // When the etalon has X/Y coordinates, position is the STRONGEST signal.
205
+ // Elements rarely move on screen; text/names change far more often.
206
+ if (etalon && etalon.attributes['x'] !== undefined) {
207
+ const spatialWeight = 5.0; // Very high — position is king
208
+ let spatialScore = 0;
209
+ const ex = parseInt(etalon.attributes['x']);
210
+ const ey = parseInt(etalon.attributes['y']);
211
+ // Get candidate node coordinates — handle both iOS and Android formats
212
+ let nx = -999;
213
+ let ny = -999;
214
+ // iOS format: separate x, y attributes
215
+ const iosX = this.getAttrValue(node, 'x');
216
+ const iosY = this.getAttrValue(node, 'y');
217
+ if (iosX !== null && iosY !== null) {
218
+ nx = parseInt(iosX);
219
+ ny = parseInt(iosY);
220
+ }
221
+ else {
222
+ // Android format: bounds="[x1,y1][x2,y2]" — compute top-left (matches getElementRect)
223
+ const bounds = this.getAttrValue(node, 'bounds');
224
+ if (bounds) {
225
+ const match = bounds.match(/\[(\d+),(\d+)\]\[(\d+),(\d+)\]/);
226
+ if (match) {
227
+ nx = parseInt(match[1]); // x1 (left)
228
+ ny = parseInt(match[2]); // y1 (top)
229
+ }
230
+ }
231
+ }
232
+ if (nx !== -999 && ny !== -999) {
233
+ const dist = Math.sqrt(Math.pow(ex - nx, 2) + Math.pow(ey - ny, 2));
234
+ if (dist < 5) {
235
+ spatialScore = 1.0; // Near-perfect position match
236
+ }
237
+ else if (dist < 20) {
238
+ spatialScore = 0.85; // Very close
239
+ }
240
+ else if (dist < 50) {
241
+ spatialScore = 0.5; // Same general area
242
+ }
243
+ else if (dist < 100) {
244
+ spatialScore = 0.2; // Nearby
245
+ }
246
+ // > 100px: no spatial score
247
+ }
248
+ totalWeight += spatialWeight;
249
+ totalScore += spatialScore * spatialWeight;
250
+ }
251
+ const finalScore = totalWeight > 0 ? totalScore / totalWeight : 0;
252
+ // Diagnostic logging for debugging
253
+ if (finalScore > 0.3) {
254
+ this.logger.debug(`Score [${node.nodeName}]: final=${finalScore.toFixed(2)} ` +
255
+ `(total=${totalScore.toFixed(2)}/${totalWeight.toFixed(1)}) ` +
256
+ `name=${this.getAttrValue(node, 'name') || '-'} ` +
257
+ `label=${this.getAttrValue(node, 'label') || '-'} ` +
258
+ `X=${this.getAttrValue(node, 'x')},Y=${this.getAttrValue(node, 'y')}`);
259
+ }
260
+ return finalScore;
261
+ }
262
+ getAttrValue(node, name) {
263
+ if (typeof node.getAttribute === 'function') {
264
+ const val = node.getAttribute(name);
265
+ if (val)
266
+ return val;
267
+ }
268
+ const nodeAttrs = node.attributes || [];
269
+ for (let i = 0; i < nodeAttrs.length; i++) {
270
+ if (nodeAttrs[i].name.toLowerCase() === name)
271
+ return nodeAttrs[i].value;
272
+ }
273
+ return null;
274
+ }
275
+ getBestSimilarity(value, keywords) {
276
+ const lowerValue = (value || '').toLowerCase().trim();
277
+ if (!lowerValue)
278
+ return 0;
279
+ let best = 0;
280
+ for (const word of keywords) {
281
+ const lowerWord = word.toLowerCase().trim();
282
+ const sim = this.stringSimilarity(lowerValue, lowerWord);
283
+ if (sim > best)
284
+ best = sim;
285
+ // Bonus for exact containment or very high similarity
286
+ if (lowerValue.includes(lowerWord) && best < 0.7)
287
+ best = 0.7;
288
+ }
289
+ return best;
290
+ }
291
+ stringSimilarity(str1, str2) {
292
+ const s1 = str1.toLowerCase().trim();
293
+ const s2 = str2.toLowerCase().trim();
294
+ if (s1 === s2)
295
+ return 1;
296
+ // Simple overlap for very short strings
297
+ if (s1.length < 2 || s2.length < 2)
298
+ return s1.includes(s2) || s2.includes(s1) ? 0.7 : 0;
299
+ const bigrams1 = new Set();
300
+ for (let i = 0; i < s1.length - 1; i++)
301
+ bigrams1.add(s1.substring(i, i + 2));
302
+ const bigrams2 = new Set();
303
+ for (let i = 0; i < s2.length - 1; i++)
304
+ bigrams2.add(s2.substring(i, i + 2));
305
+ let intersection = 0;
306
+ for (const b of bigrams1) {
307
+ if (bigrams2.has(b))
308
+ intersection++;
309
+ }
310
+ return (2 * intersection) / (bigrams1.size + bigrams2.size);
311
+ }
312
+ getAbsoluteXpath(node) {
313
+ const parts = [];
314
+ let current = node;
315
+ while (current && current.nodeType === 1) {
316
+ // Node.ELEMENT_NODE
317
+ let index = 0;
318
+ let sibling = current.previousSibling;
319
+ while (sibling) {
320
+ if (sibling.nodeType === 1 && sibling.nodeName === current.nodeName) {
321
+ index++;
322
+ }
323
+ sibling = sibling.previousSibling;
324
+ }
325
+ const tagName = current.nodeName;
326
+ const pathIndex = index > 0 ? `[${index + 1}]` : '';
327
+ parts.unshift(`${tagName}${pathIndex}`);
328
+ current = current.parentNode;
329
+ }
330
+ return `/${parts.join('/')}`;
331
+ }
332
+ }
333
+ exports.FuzzyXmlHealingProvider = FuzzyXmlHealingProvider;
@@ -0,0 +1,98 @@
1
+ "use strict";
2
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
3
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
4
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
5
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
6
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
7
+ };
8
+ var __metadata = (this && this.__metadata) || function (k, v) {
9
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
10
+ };
11
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
12
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
13
+ return new (P || (P = Promise))(function (resolve, reject) {
14
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
15
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
16
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
17
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
18
+ });
19
+ };
20
+ var __importDefault = (this && this.__importDefault) || function (mod) {
21
+ return (mod && mod.__esModule) ? mod : { "default": mod };
22
+ };
23
+ Object.defineProperty(exports, "__esModule", { value: true });
24
+ exports.HealEtalonService = void 0;
25
+ const typedi_1 = require("typedi");
26
+ const device_store_1 = require("../../data-service/device-store");
27
+ const logger_1 = __importDefault(require("../../logger"));
28
+ let HealEtalonService = class HealEtalonService {
29
+ constructor() {
30
+ this.logger = logger_1.default.scope('HealEtalonService');
31
+ this.store = device_store_1.DeviceStoreFactory.getHealEtalonStore();
32
+ }
33
+ /**
34
+ * Saves or updates a signature for a successful locator
35
+ */
36
+ saveSignature(strategy, selector, node, path) {
37
+ return __awaiter(this, void 0, void 0, function* () {
38
+ try {
39
+ const attributes = {};
40
+ // Extract identifying attributes
41
+ const nodeAttrs = node.attributes || [];
42
+ const anchorNames = [
43
+ 'content-desc',
44
+ 'resource-id',
45
+ 'text',
46
+ 'name',
47
+ 'id',
48
+ 'hint',
49
+ 'label',
50
+ 'accessibility-id',
51
+ 'value',
52
+ 'x',
53
+ 'y',
54
+ 'width',
55
+ 'height',
56
+ ];
57
+ for (let i = 0; i < nodeAttrs.length; i++) {
58
+ const attr = nodeAttrs[i];
59
+ if (anchorNames.includes(attr.name.toLowerCase())) {
60
+ attributes[attr.name.toLowerCase()] = attr.value;
61
+ }
62
+ }
63
+ const signature = {
64
+ selector,
65
+ strategy,
66
+ attributes,
67
+ nodeName: node.nodeName || 'Unknown',
68
+ path,
69
+ lastSeen: Date.now(),
70
+ };
71
+ yield this.store.saveSignature(signature);
72
+ this.logger.debug(`Saved signature for locator: ${selector}`);
73
+ }
74
+ catch (err) {
75
+ this.logger.error(`Failed to save signature: ${err.message}`);
76
+ }
77
+ });
78
+ }
79
+ /**
80
+ * Retrieves a signature for a locator
81
+ */
82
+ getSignature(selector) {
83
+ return __awaiter(this, void 0, void 0, function* () {
84
+ try {
85
+ return yield this.store.getSignature(selector);
86
+ }
87
+ catch (err) {
88
+ this.logger.error(`Failed to retrieve signature: ${err.message}`);
89
+ return null;
90
+ }
91
+ });
92
+ }
93
+ };
94
+ exports.HealEtalonService = HealEtalonService;
95
+ exports.HealEtalonService = HealEtalonService = __decorate([
96
+ (0, typedi_1.Service)(),
97
+ __metadata("design:paramtypes", [])
98
+ ], HealEtalonService);
@@ -0,0 +1,132 @@
1
+ "use strict";
2
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
3
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
4
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
5
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
6
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
7
+ };
8
+ var __importDefault = (this && this.__importDefault) || function (mod) {
9
+ return (mod && mod.__esModule) ? mod : { "default": mod };
10
+ };
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.HealedLocatorGenerator = void 0;
13
+ const typedi_1 = require("typedi");
14
+ const logger_1 = __importDefault(require("../../logger"));
15
+ let HealedLocatorGenerator = class HealedLocatorGenerator {
16
+ constructor() {
17
+ this.logger = logger_1.default.scope('LocatorGenerator');
18
+ }
19
+ /**
20
+ * Generates a set of candidate locators for a healed node, ranked by stability.
21
+ */
22
+ generate(node) {
23
+ const candidates = [];
24
+ const tagName = node.nodeName || node.tag;
25
+ if (!tagName)
26
+ return [];
27
+ const attrs = {};
28
+ if (node.attributes) {
29
+ for (let i = 0; i < node.attributes.length; i++) {
30
+ const attr = node.attributes[i];
31
+ attrs[attr.name.toLowerCase()] = attr.value;
32
+ }
33
+ }
34
+ else if (node.attrs) {
35
+ // Handle ResilioTree node format if different
36
+ Object.assign(attrs, node.attrs);
37
+ }
38
+ // 1. Name attribute (maps to accessibilityIdentifier on iOS — THE most reliable iOS locator)
39
+ if (attrs['name']) {
40
+ candidates.push(`//${tagName}[@name='${attrs['name']}']`);
41
+ candidates.push(`//*[@name='${attrs['name']}']`);
42
+ }
43
+ // 2. Label attribute (maps to accessibilityLabel on iOS — stable and user-visible)
44
+ if (attrs['label']) {
45
+ candidates.push(`//${tagName}[@label='${attrs['label']}']`);
46
+ candidates.push(`//*[@label='${attrs['label']}']`);
47
+ }
48
+ // 3. Content-Desc (Very stable in Android)
49
+ if (attrs['content-desc']) {
50
+ candidates.push(`//*[contains(@content-desc, '${attrs['content-desc']}')]`);
51
+ candidates.push(`//${tagName}[@content-desc='${attrs['content-desc']}']`);
52
+ }
53
+ // 4. Resource-ID (Stable in Android)
54
+ if (attrs['resource-id']) {
55
+ candidates.push(`//*[@resource-id='${attrs['resource-id']}']`);
56
+ }
57
+ // 5. Value attribute (iOS form elements)
58
+ if (attrs['value'] && attrs['value'].length > 1 && attrs['value'].length < 80) {
59
+ candidates.push(`//${tagName}[@value='${attrs['value']}']`);
60
+ }
61
+ // 6. Text content — NOTE: @text does NOT work in WDA/XCUITest XPath!
62
+ // Use it only as a contains() match for Android, never as a primary iOS locator.
63
+ const textValue = attrs['text'];
64
+ if (textValue && textValue.length > 2 && textValue.length < 50) {
65
+ // For Android only — iOS should be handled by @name and @label above
66
+ candidates.push(`//${tagName}[@text='${textValue}']`);
67
+ candidates.push(`//*[contains(@text, '${textValue}')]`);
68
+ }
69
+ // 7. Accessibility ID (used in Appium, may differ from @name)
70
+ if (attrs['accessibility-id']) {
71
+ candidates.push(`//${tagName}[@accessibility-id='${attrs['accessibility-id']}']`);
72
+ candidates.push(`//*[@accessibility-id='${attrs['accessibility-id']}']`);
73
+ }
74
+ // 8. Parent-Relative
75
+ const parent = node.parentNode || node.parent;
76
+ if (parent) {
77
+ const parentTag = parent.nodeName || parent.tag;
78
+ const parentResourceId = (parent.attributes && this.getAttr(parent, 'resource-id')) ||
79
+ (parent.attrs && parent.attrs['resource-id']);
80
+ const parentName = (parent.attributes && this.getAttr(parent, 'name')) ||
81
+ (parent.attrs && parent.attrs['name']);
82
+ if (parentResourceId) {
83
+ candidates.push(`//*[@resource-id='${parentResourceId}']//${tagName}`);
84
+ }
85
+ else if (parentName && parentName.length < 80) {
86
+ candidates.push(`//*[@name='${parentName}']//${tagName}`);
87
+ }
88
+ else if (parentTag && parentTag !== 'HTML' && parentTag !== 'BODY') {
89
+ candidates.push(`//${parentTag}//${tagName}`);
90
+ }
91
+ }
92
+ // 6. Absolute XPath (Fallback)
93
+ candidates.push(this.generateAbsoluteXpath(node));
94
+ // Return unique non-empty candidates
95
+ return [...new Set(candidates)].filter(Boolean);
96
+ }
97
+ getAttr(node, name) {
98
+ if (!node.attributes)
99
+ return null;
100
+ for (let i = 0; i < node.attributes.length; i++) {
101
+ if (node.attributes[i].name.toLowerCase() === name)
102
+ return node.attributes[i].value;
103
+ }
104
+ return null;
105
+ }
106
+ generateAbsoluteXpath(node) {
107
+ const parts = [];
108
+ let current = node;
109
+ while (current && current.nodeType === 1) {
110
+ let index = 0;
111
+ let sibling = current.previousSibling;
112
+ while (sibling) {
113
+ if (sibling.nodeType === 1 && sibling.nodeName === current.nodeName) {
114
+ index++;
115
+ }
116
+ sibling = sibling.previousSibling;
117
+ }
118
+ const tag = current.nodeName || current.tag;
119
+ // Skip wrapper tags for cleaner XPath
120
+ if (tag.toLowerCase() !== 'html' && tag.toLowerCase() !== 'body') {
121
+ const pathIndex = index > 0 ? `[${index + 1}]` : '';
122
+ parts.unshift(`${tag}${pathIndex}`);
123
+ }
124
+ current = current.parentNode || current.parent;
125
+ }
126
+ return `/${parts.join('/')}`;
127
+ }
128
+ };
129
+ exports.HealedLocatorGenerator = HealedLocatorGenerator;
130
+ exports.HealedLocatorGenerator = HealedLocatorGenerator = __decorate([
131
+ (0, typedi_1.Service)()
132
+ ], HealedLocatorGenerator);