@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.
- package/README.md +446 -0
- package/lib/package.json +207 -0
- package/lib/public/assets/Layouts-7IT8aFLI.js +11 -0
- package/lib/public/assets/Layouts-DPMls9vh.css +1 -0
- package/lib/public/assets/ai-settings-BbnfgdEx.js +11 -0
- package/lib/public/assets/apps-CRMrI4_p.js +16 -0
- package/lib/public/assets/apps-CcM77dgg.css +1 -0
- package/lib/public/assets/badge-B1nKs8zj.css +1 -0
- package/lib/public/assets/badge-CSvl5xIU.js +11 -0
- package/lib/public/assets/button-CJlKn4PZ.css +1 -0
- package/lib/public/assets/button-CvLaGFYj.js +26 -0
- package/lib/public/assets/calendar-6w-D6Oaw.js +6 -0
- package/lib/public/assets/clock-DcdeWBPr.js +6 -0
- package/lib/public/assets/cpu-DiSoXT9n.js +6 -0
- package/lib/public/assets/device-explorer-CajM63OJ.js +193 -0
- package/lib/public/assets/device-explorer-CxdUAoTL.css +1 -0
- package/lib/public/assets/index-ByQwMN5T.js +174 -0
- package/lib/public/assets/index-C1DBaoSh.js +1 -0
- package/lib/public/assets/index-qzCez_kk.css +1 -0
- package/lib/public/assets/lock-B23ibZmo.js +6 -0
- package/lib/public/assets/maintenance-settings-CirzA6yG.js +6 -0
- package/lib/public/assets/mouse-pointer-2-Cz76SHFb.js +6 -0
- package/lib/public/assets/plus-BBwlIevt.js +6 -0
- package/lib/public/assets/session-dashboard-C2k7FFv_.css +1 -0
- package/lib/public/assets/session-dashboard-HPDtwPOZ.js +62 -0
- package/lib/public/assets/settings-DrZsZwdc.js +1 -0
- package/lib/public/assets/trash-2-DQpvzJec.js +6 -0
- package/lib/public/assets/useSocket-Dxsqae2a.js +16 -0
- package/lib/public/assets/webhook-settings-CDPgsgkb.css +1 -0
- package/lib/public/assets/webhook-settings-Cp-B4Nrw.js +1 -0
- package/lib/public/assets/zap-DovP6iow.js +6 -0
- package/lib/public/favicon.ico +0 -0
- package/lib/public/favicon.png +0 -0
- package/lib/public/favicon.svg +9 -0
- package/lib/public/index.html +46 -0
- package/lib/public/logo.svg +17 -0
- package/lib/public/logo192.png +0 -0
- package/lib/public/logo512.png +0 -0
- package/lib/public/manifest.json +25 -0
- package/lib/public/robots.txt +3 -0
- package/lib/schema.json +348 -0
- package/lib/src/InternalHttpClient.js +212 -0
- package/lib/src/PluginContext.js +29 -0
- package/lib/src/XenonCapabilityManager.js +199 -0
- package/lib/src/app/index.js +167 -0
- package/lib/src/app/routers/apps.js +79 -0
- package/lib/src/app/routers/config.js +131 -0
- package/lib/src/app/routers/control.js +835 -0
- package/lib/src/app/routers/dashboard.js +301 -0
- package/lib/src/app/routers/grid.js +352 -0
- package/lib/src/app/routers/reservation.js +190 -0
- package/lib/src/app/routers/webhook.js +83 -0
- package/lib/src/app/swagger-docs.js +203 -0
- package/lib/src/app/swagger.js +366 -0
- package/lib/src/chromeUtils.js +148 -0
- package/lib/src/commands/handle.js +19 -0
- package/lib/src/commands/index.js +8 -0
- package/lib/src/config.js +73 -0
- package/lib/src/dashboard/asset-manager.js +84 -0
- package/lib/src/dashboard/commands.js +284 -0
- package/lib/src/dashboard/event-manager.js +699 -0
- package/lib/src/dashboard/services/app-service.js +134 -0
- package/lib/src/dashboard/services/failure-analysis-service.js +173 -0
- package/lib/src/dashboard/services/session-service.js +113 -0
- package/lib/src/data-service/CircuitBreaker.js +83 -0
- package/lib/src/data-service/config-service.js +155 -0
- package/lib/src/data-service/db.js +122 -0
- package/lib/src/data-service/device-service.js +320 -0
- package/lib/src/data-service/device-store.interface.js +2 -0
- package/lib/src/data-service/device-store.js +345 -0
- package/lib/src/data-service/pending-sessions-service.js +25 -0
- package/lib/src/data-service/pluginArgs.js +25 -0
- package/lib/src/data-service/prisma-service.js +31 -0
- package/lib/src/data-service/prisma-store.js +385 -0
- package/lib/src/data-service/queue-service.js +150 -0
- package/lib/src/data-service/web-config-service.js +130 -0
- package/lib/src/device-managers/AndroidDeviceManager.js +1155 -0
- package/lib/src/device-managers/ChromeDriverManager.js +68 -0
- package/lib/src/device-managers/HealthMonitorService.js +325 -0
- package/lib/src/device-managers/IOSDeviceManager.js +351 -0
- package/lib/src/device-managers/NodeDevices.js +82 -0
- package/lib/src/device-managers/android/AndroidStreamService.js +370 -0
- package/lib/src/device-managers/android/DeviceLockManager.js +45 -0
- package/lib/src/device-managers/cloud/CapabilityManager.js +26 -0
- package/lib/src/device-managers/cloud/Devices.js +86 -0
- package/lib/src/device-managers/iOSTracker.js +44 -0
- package/lib/src/device-managers/index.js +89 -0
- package/lib/src/device-managers/ios/IOSDiscoveryService.js +268 -0
- package/lib/src/device-managers/ios/IOSStreamService.js +893 -0
- package/lib/src/device-managers/ios/WDAClient.js +866 -0
- package/lib/src/device-utils.js +663 -0
- package/lib/src/enums/Capabilities.js +8 -0
- package/lib/src/enums/Cloud.js +11 -0
- package/lib/src/enums/Platform.js +9 -0
- package/lib/src/enums/SessionType.js +9 -0
- package/lib/src/enums/SocketEvents.js +15 -0
- package/lib/src/helpers/UniversalMjpegProxy.js +273 -0
- package/lib/src/helpers/index.js +229 -0
- package/lib/src/index.js +95 -0
- package/lib/src/interceptors/CommandInterceptor.js +524 -0
- package/lib/src/interfaces/ICloudManager.js +2 -0
- package/lib/src/interfaces/IDevice.js +2 -0
- package/lib/src/interfaces/IDeviceFilterOptions.js +2 -0
- package/lib/src/interfaces/IDeviceManager.js +2 -0
- package/lib/src/interfaces/IOptions.js +2 -0
- package/lib/src/interfaces/IPluginArgs.js +55 -0
- package/lib/src/interfaces/ISessionCapability.js +2 -0
- package/lib/src/logger.js +225 -0
- package/lib/src/plugin.js +244 -0
- package/lib/src/prisma.js +12 -0
- package/lib/src/profiling/AndroidAppProfiler.js +213 -0
- package/lib/src/proxy/wd-command-proxy.js +221 -0
- package/lib/src/scripts/generate-database-migration.js +59 -0
- package/lib/src/scripts/initialize-database.js +55 -0
- package/lib/src/scripts/install-go-ios.js +66 -0
- package/lib/src/scripts/prepare-prisma.js +89 -0
- package/lib/src/services/AICommandService.js +143 -0
- package/lib/src/services/AIService.js +466 -0
- package/lib/src/services/CleanupService.js +141 -0
- package/lib/src/services/EventBus.js +74 -0
- package/lib/src/services/InspectorService.js +395 -0
- package/lib/src/services/MetricsService.js +134 -0
- package/lib/src/services/NetworkConditioningService.js +173 -0
- package/lib/src/services/NotificationService.js +163 -0
- package/lib/src/services/RequestLogService.js +252 -0
- package/lib/src/services/ResourceIsolationService.js +122 -0
- package/lib/src/services/SecurityService.js +120 -0
- package/lib/src/services/ServerManager.js +284 -0
- package/lib/src/services/SessionHeartbeatService.js +158 -0
- package/lib/src/services/SessionLifecycleService.js +572 -0
- package/lib/src/services/SocketClient.js +71 -0
- package/lib/src/services/SocketServer.js +87 -0
- package/lib/src/services/TracingService.js +132 -0
- package/lib/src/services/VideoPipelineService.js +220 -0
- package/lib/src/services/healing/FuzzyXmlHealingProvider.js +333 -0
- package/lib/src/services/healing/HealEtalonService.js +98 -0
- package/lib/src/services/healing/HealedLocatorGenerator.js +132 -0
- package/lib/src/services/healing/HealingOrchestrator.js +165 -0
- package/lib/src/services/healing/LlmHealingProvider.js +77 -0
- package/lib/src/services/healing/OcrHealingProvider.js +119 -0
- package/lib/src/services/healing/ResilioTreeHealingProvider.js +100 -0
- package/lib/src/services/healing/VisualAiHealingProvider.js +90 -0
- package/lib/src/services/healing/types.js +12 -0
- package/lib/src/services/omni-vision/OmniVisionService.js +718 -0
- package/lib/src/services/omni-vision/VisionAssertionService.js +68 -0
- package/lib/src/sessions/CloudSession.js +42 -0
- package/lib/src/sessions/LocalSession.js +313 -0
- package/lib/src/sessions/RemoteSession.js +287 -0
- package/lib/src/sessions/SessionManager.js +238 -0
- package/lib/src/sessions/XenonSession.js +44 -0
- package/lib/src/types/CLIArgs.js +2 -0
- package/lib/src/types/CloudArgs.js +2 -0
- package/lib/src/types/CloudSchema.js +131 -0
- package/lib/src/types/DeviceType.js +2 -0
- package/lib/src/types/DeviceUpdate.js +2 -0
- package/lib/src/types/IOSDevice.js +2 -0
- package/lib/src/types/Platform.js +2 -0
- package/lib/src/types/SessionStatus.js +11 -0
- package/lib/src/validators/CapabilityValidator.js +93 -0
- package/lib/test/e2e/android/conf.spec.js +43 -0
- package/lib/test/e2e/android/conf2.spec.js +44 -0
- package/lib/test/e2e/android/conf3.spec.js +44 -0
- package/lib/test/e2e/e2ehelper.js +113 -0
- package/lib/test/e2e/hubnode/forward-request.spec.js +224 -0
- package/lib/test/e2e/hubnode/hubnode.spec.js +214 -0
- package/lib/test/e2e/ios/conf1.spec.js +39 -0
- package/lib/test/e2e/ios/conf2.spec.js +39 -0
- package/lib/test/e2e/plugin-harness.js +236 -0
- package/lib/test/e2e/plugin.spec.js +97 -0
- package/lib/test/e2e/telemetry_verification.spec.js +83 -0
- package/lib/test/e2e/video-recording-test.spec.js +63 -0
- package/lib/test/helpers/test-container.js +112 -0
- package/lib/test/integration/androidDevices.spec.js +137 -0
- package/lib/test/integration/cliArgs.js +73 -0
- package/lib/test/integration/ios/01iOSSimulator.spec.js +291 -0
- package/lib/test/integration/ios/02iOSDevices.spec.js +75 -0
- package/lib/test/integration/testHelpers.js +74 -0
- package/lib/test/unit/AndroidDeviceManager.spec.js +178 -0
- package/lib/test/unit/ChromeDriverManager.spec.js +26 -0
- package/lib/test/unit/CleanupService.spec.js +21 -0
- package/lib/test/unit/DeviceModel.spec.js +157 -0
- package/lib/test/unit/FuzzyXmlHealingProvider.test.js +294 -0
- package/lib/test/unit/GetAdbOriginal.js +42 -0
- package/lib/test/unit/HealingCascade.test.js +128 -0
- package/lib/test/unit/IOSDeviceManager.spec.js +261 -0
- package/lib/test/unit/RemoteIOs.spec.js +78 -0
- package/lib/test/unit/ResilioTreeHealingProvider.test.js +96 -0
- package/lib/test/unit/commands.spec.js +27 -0
- package/lib/test/unit/config.spec.js +27 -0
- package/lib/test/unit/device-service.spec.js +307 -0
- package/lib/test/unit/device-utils.spec.js +313 -0
- package/lib/test/unit/fixtures/device.config.js +4 -0
- package/lib/test/unit/fixtures/devices.js +89 -0
- package/lib/test/unit/helpers.spec.js +62 -0
- package/lib/test/unit/omni-vision.spec.js +100 -0
- package/lib/test/unit/plugin.spec.js +133 -0
- package/lib/tsconfig.tsbuildinfo +1 -0
- package/package.json +207 -0
- package/prisma/data.db +0 -0
- package/prisma/dev.db +0 -0
- package/prisma/dev.db-journal +0 -0
- package/prisma/migrations/20231011074725_initial_tables/migration.sql +47 -0
- package/prisma/migrations/20231226115334_update_session_log/migration.sql +2 -0
- package/prisma/migrations/20251204113710_add_video_recording_enabled/migration.sql +29 -0
- package/prisma/migrations/20251204132449_add_log_table/migration.sql +11 -0
- package/prisma/migrations/20251205050111_add_profiling_support/migration.sql +47 -0
- package/prisma/migrations/20251205050947_add_is_error_field/migration.sql +24 -0
- package/prisma/migrations/20260126201337_add_app_model/migration.sql +18 -0
- package/prisma/migrations/20260130115722_add_performance_trace_and_xenon_sync/migration.sql +2 -0
- package/prisma/migrations/20260130135114_add_device_models/migration.sql +57 -0
- package/prisma/migrations/20260130140655_make_systemport_optional/migration.sql +45 -0
- package/prisma/migrations/20260130140932_make_device_fields_optional/migration.sql +45 -0
- package/prisma/migrations/20260130141040_final_schema_fix/migration.sql +45 -0
- package/prisma/migrations/20260130143234_add_device_health_fields/migration.sql +4 -0
- package/prisma/migrations/20260130144921_add_failure_category/migration.sql +2 -0
- package/prisma/migrations/20260131151456_add_webhook_config/migration.sql +10 -0
- package/prisma/migrations/20260201094507_add_device_tags/migration.sql +11 -0
- package/prisma/migrations/20260201103410_add_managed_process/migration.sql +15 -0
- package/prisma/migrations/20260201140637_add_web_config/migration.sql +22 -0
- package/prisma/migrations/20260201162232_add_session_progress/migration.sql +2 -0
- package/prisma/migrations/20260201174231_add_total_healed_count/migration.sql +2 -0
- package/prisma/migrations/migration_lock.toml +3 -0
- package/prisma/schema.prisma +210 -0
- package/schema.json +348 -0
- package/scripts/build-xenon.sh +32 -0
- package/scripts/dev/debug-gemini.ts +44 -0
- package/scripts/generate-types-from-schema.js +86 -0
- package/scripts/install-compatible-driver.js +39 -0
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
19
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
20
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
21
|
+
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;
|
|
22
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
23
|
+
};
|
|
24
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
25
|
+
var ownKeys = function(o) {
|
|
26
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
27
|
+
var ar = [];
|
|
28
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
29
|
+
return ar;
|
|
30
|
+
};
|
|
31
|
+
return ownKeys(o);
|
|
32
|
+
};
|
|
33
|
+
return function (mod) {
|
|
34
|
+
if (mod && mod.__esModule) return mod;
|
|
35
|
+
var result = {};
|
|
36
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
37
|
+
__setModuleDefault(result, mod);
|
|
38
|
+
return result;
|
|
39
|
+
};
|
|
40
|
+
})();
|
|
41
|
+
var __metadata = (this && this.__metadata) || function (k, v) {
|
|
42
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
43
|
+
};
|
|
44
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
45
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
46
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
47
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
48
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
49
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
50
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
51
|
+
});
|
|
52
|
+
};
|
|
53
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
54
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
55
|
+
};
|
|
56
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
57
|
+
exports.HealingOrchestrator = void 0;
|
|
58
|
+
const typedi_1 = require("typedi");
|
|
59
|
+
const logger_1 = __importDefault(require("../../logger"));
|
|
60
|
+
const FuzzyXmlHealingProvider_1 = require("./FuzzyXmlHealingProvider");
|
|
61
|
+
const OcrHealingProvider_1 = require("./OcrHealingProvider");
|
|
62
|
+
const VisualAiHealingProvider_1 = require("./VisualAiHealingProvider");
|
|
63
|
+
const LlmHealingProvider_1 = require("./LlmHealingProvider");
|
|
64
|
+
const HealEtalonService_1 = require("./HealEtalonService");
|
|
65
|
+
const ResilioTreeHealingProvider_1 = require("./ResilioTreeHealingProvider");
|
|
66
|
+
let HealingOrchestrator = class HealingOrchestrator {
|
|
67
|
+
constructor(etalonService) {
|
|
68
|
+
this.etalonService = etalonService;
|
|
69
|
+
this.logger = logger_1.default.scope('HealingOrchestrator');
|
|
70
|
+
this.providers = [];
|
|
71
|
+
this.providers = [
|
|
72
|
+
new ResilioTreeHealingProvider_1.ResilioTreeHealingProvider(this.etalonService),
|
|
73
|
+
new FuzzyXmlHealingProvider_1.FuzzyXmlHealingProvider(this.etalonService),
|
|
74
|
+
new OcrHealingProvider_1.OcrHealingProvider(),
|
|
75
|
+
new VisualAiHealingProvider_1.VisualAiHealingProvider(),
|
|
76
|
+
new LlmHealingProvider_1.LlmHealingProvider(),
|
|
77
|
+
];
|
|
78
|
+
}
|
|
79
|
+
attemptHealing(sessionId, driver, strategy, selector) {
|
|
80
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
81
|
+
this.logger.info(`🚨 Self-Healing triggered for session ${sessionId}. Broken locator: ${strategy}=${selector}`);
|
|
82
|
+
// Preparation: Collect data required for healing
|
|
83
|
+
// Note: We do this once to avoid multiple expensive round-trips
|
|
84
|
+
const context = { sessionId, driver, strategy, selector };
|
|
85
|
+
try {
|
|
86
|
+
this.logger.debug('Collecting page source and screenshot for analysis...');
|
|
87
|
+
const [xml, screenshot] = yield Promise.all([driver.getPageSource(), driver.getScreenshot()]);
|
|
88
|
+
context.pageSource = xml;
|
|
89
|
+
context.screenshotBase64 = screenshot;
|
|
90
|
+
}
|
|
91
|
+
catch (err) {
|
|
92
|
+
this.logger.error(`Failed to collect healing context: ${err.message}`);
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
// Tiered Execution: Try providers in order of cost/complexity
|
|
96
|
+
for (const provider of this.providers) {
|
|
97
|
+
try {
|
|
98
|
+
this.logger.info(`Attempting Tier ${provider.tier}: ${provider.name}...`);
|
|
99
|
+
const result = yield provider.heal(context);
|
|
100
|
+
if (result) {
|
|
101
|
+
this.logger.info(`✨ Provider ${provider.name} found a match! Confidence: ${(result.confidence * 100).toFixed(0)}%`);
|
|
102
|
+
// Tier 1/2 Optimization: Stability Verification Loop
|
|
103
|
+
// We try all candidates to see which one is the most stable (semantic vs absolute)
|
|
104
|
+
if (result.candidateSelectors && result.candidateSelectors.length > 0) {
|
|
105
|
+
this.logger.debug(`Verifying ${result.candidateSelectors.length} candidate locators for stability...`);
|
|
106
|
+
for (const candidate of result.candidateSelectors) {
|
|
107
|
+
try {
|
|
108
|
+
const elements = yield context.driver.findElements('xpath', candidate);
|
|
109
|
+
if (elements.length === 1) {
|
|
110
|
+
this.logger.info(`🎯 Verified stable & unique locator: ${candidate}`);
|
|
111
|
+
result.recommendedSelector = candidate;
|
|
112
|
+
break; // Found a unique stable locator
|
|
113
|
+
}
|
|
114
|
+
else if (elements.length > 1) {
|
|
115
|
+
this.logger.debug(`⚠️ Candidate locator is not unique (${elements.length} matches): ${candidate}`);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
catch (e) {
|
|
119
|
+
this.logger.debug(`Candidate locator check failed: ${candidate}`);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
// Principal Learning: Autonomously update the etalon to prevent future failures
|
|
124
|
+
// We only do this if confidence is over 70% to avoid pollution with false positives
|
|
125
|
+
if (result.confidence > 0.7 && result.node) {
|
|
126
|
+
try {
|
|
127
|
+
this.logger.info(`🧠 Learning from healing success: updating etalon for ${selector}`);
|
|
128
|
+
// Recalculate path for ResilioTree if node is available
|
|
129
|
+
let learnedPath = null;
|
|
130
|
+
if (result.node) {
|
|
131
|
+
try {
|
|
132
|
+
const { Path } = yield Promise.resolve().then(() => __importStar(require('resiliotree')));
|
|
133
|
+
const pathNodes = [];
|
|
134
|
+
let curr = result.node;
|
|
135
|
+
while (curr) {
|
|
136
|
+
pathNodes.unshift(curr);
|
|
137
|
+
curr = curr.parent || curr.parentNode;
|
|
138
|
+
}
|
|
139
|
+
learnedPath = new Path(pathNodes).toJSON();
|
|
140
|
+
}
|
|
141
|
+
catch (e) { }
|
|
142
|
+
}
|
|
143
|
+
yield this.etalonService.saveSignature(strategy, selector, result.node, learnedPath);
|
|
144
|
+
}
|
|
145
|
+
catch (learnErr) {
|
|
146
|
+
this.logger.debug(`Failed to update etalon after healing: ${learnErr.message}`);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
return result;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
catch (err) {
|
|
153
|
+
this.logger.error(`Provider ${provider.name} failed: ${err.message}`);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
this.logger.warn(`❌ All healing tiers failed for selector: ${selector}`);
|
|
157
|
+
return null;
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
};
|
|
161
|
+
exports.HealingOrchestrator = HealingOrchestrator;
|
|
162
|
+
exports.HealingOrchestrator = HealingOrchestrator = __decorate([
|
|
163
|
+
(0, typedi_1.Service)(),
|
|
164
|
+
__metadata("design:paramtypes", [HealEtalonService_1.HealEtalonService])
|
|
165
|
+
], HealingOrchestrator);
|
|
@@ -0,0 +1,77 @@
|
|
|
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.LlmHealingProvider = void 0;
|
|
16
|
+
const types_1 = require("./types");
|
|
17
|
+
const AIService_1 = require("../AIService");
|
|
18
|
+
const logger_1 = __importDefault(require("../../logger"));
|
|
19
|
+
class LlmHealingProvider {
|
|
20
|
+
constructor() {
|
|
21
|
+
this.name = 'LLM Reasoning Provider';
|
|
22
|
+
this.tier = types_1.HealingTier.TIER_5_LLM_REASONING;
|
|
23
|
+
this.logger = logger_1.default.scope('LlmHealing');
|
|
24
|
+
}
|
|
25
|
+
heal(context) {
|
|
26
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
27
|
+
var _a, _b;
|
|
28
|
+
if (!context.pageSource) {
|
|
29
|
+
this.logger.debug('No page source available for LLM reasoning');
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
// Check if AI service is enabled before attempting
|
|
33
|
+
if (!AIService_1.AI_SERVICE.isEnabled()) {
|
|
34
|
+
this.logger.debug('AI service not enabled, skipping LLM healing');
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
try {
|
|
38
|
+
this.logger.info(`Attempting Deep LLM healing for locator: "${context.selector}"`);
|
|
39
|
+
const healingResult = yield AIService_1.AI_SERVICE.healLocator({
|
|
40
|
+
selector: context.selector,
|
|
41
|
+
strategy: context.strategy,
|
|
42
|
+
xml: context.pageSource,
|
|
43
|
+
screenshotBase64: context.screenshotBase64,
|
|
44
|
+
});
|
|
45
|
+
if (healingResult && healingResult.recommendedXpath) {
|
|
46
|
+
this.logger.info(`✅ LLM found healing path: ${healingResult.recommendedXpath}`);
|
|
47
|
+
// Try to resolve the element to ensure it exists
|
|
48
|
+
try {
|
|
49
|
+
const element = yield context.driver.findElement('xpath', healingResult.recommendedXpath);
|
|
50
|
+
return {
|
|
51
|
+
id: element.ELEMENT || element['element-6066-11e4-a52e-4f735466cecf'],
|
|
52
|
+
tier: this.tier,
|
|
53
|
+
confidence: 0.95, // Deep reasoning is typically highly accurate
|
|
54
|
+
originalSelector: context.selector,
|
|
55
|
+
recommendedSelector: healingResult.recommendedXpath,
|
|
56
|
+
message: `Found alternative element via Deep AI Reasoning: ${healingResult.reason}`,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
catch (err) {
|
|
60
|
+
this.logger.error('LLM recommended path failed to resolve on device');
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
catch (err) {
|
|
65
|
+
// Log service unavailability at debug level (expected), other errors at warn level
|
|
66
|
+
if (((_a = err.message) === null || _a === void 0 ? void 0 : _a.includes('unavailable')) || ((_b = err.message) === null || _b === void 0 ? void 0 : _b.includes('404'))) {
|
|
67
|
+
this.logger.debug(`LLM healing skipped: ${err.message}`);
|
|
68
|
+
}
|
|
69
|
+
else {
|
|
70
|
+
this.logger.warn(`Error during LLM healing: ${err.message}`);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return null;
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
exports.LlmHealingProvider = LlmHealingProvider;
|
|
@@ -0,0 +1,119 @@
|
|
|
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.OcrHealingProvider = void 0;
|
|
16
|
+
const types_1 = require("./types");
|
|
17
|
+
const tesseract_js_1 = __importDefault(require("tesseract.js"));
|
|
18
|
+
const logger_1 = __importDefault(require("../../logger"));
|
|
19
|
+
class OcrHealingProvider {
|
|
20
|
+
constructor() {
|
|
21
|
+
this.name = 'OCR Text Provider';
|
|
22
|
+
this.tier = types_1.HealingTier.TIER_3_LOCAL_OCR;
|
|
23
|
+
this.logger = logger_1.default.scope('OcrHealing');
|
|
24
|
+
}
|
|
25
|
+
heal(context) {
|
|
26
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
27
|
+
if (!context.screenshotBase64) {
|
|
28
|
+
this.logger.debug('No screenshot available for OCR matching');
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
try {
|
|
32
|
+
// Extract what text we are looking for from the selector
|
|
33
|
+
const soughtText = this.extractTextHint(context.selector);
|
|
34
|
+
if (!soughtText)
|
|
35
|
+
return null;
|
|
36
|
+
this.logger.info(`Attempting OCR search for text: "${soughtText}"`);
|
|
37
|
+
// Run OCR on the screenshot
|
|
38
|
+
const buffer = Buffer.from(context.screenshotBase64, 'base64');
|
|
39
|
+
const result = yield tesseract_js_1.default.recognize(buffer, 'eng');
|
|
40
|
+
const { words } = result.data;
|
|
41
|
+
// Look for the best word match
|
|
42
|
+
const bestWord = words
|
|
43
|
+
? words.find((w) => w.text.toLowerCase().includes(soughtText.toLowerCase()) ||
|
|
44
|
+
soughtText.toLowerCase().includes(w.text.toLowerCase()))
|
|
45
|
+
: null;
|
|
46
|
+
if (bestWord) {
|
|
47
|
+
this.logger.info(`✅ OCR found text "${bestWord.text}" at ${JSON.stringify(bestWord.bbox)}`);
|
|
48
|
+
// Convert bbox to center coordinates
|
|
49
|
+
const x = Math.round((bestWord.bbox.x0 + bestWord.bbox.x1) / 2);
|
|
50
|
+
const y = Math.round((bestWord.bbox.y0 + bestWord.bbox.y1) / 2);
|
|
51
|
+
// Try to get the REAL element at these coordinates by tapping
|
|
52
|
+
try {
|
|
53
|
+
// Use W3C Actions to tap at the OCR-detected coordinates, then find the element at that position
|
|
54
|
+
const element = yield context.driver
|
|
55
|
+
.findElement('-ios predicate string', `label CONTAINS[c] "${bestWord.text.replace(/"/g, '\\"')}"`)
|
|
56
|
+
.catch(() => null);
|
|
57
|
+
if (element) {
|
|
58
|
+
const elementId = element.ELEMENT || element['element-6066-11e4-a52e-4f735466cecf'];
|
|
59
|
+
if (elementId) {
|
|
60
|
+
this.logger.info('🎯 OCR resolved to real element via predicate search');
|
|
61
|
+
return {
|
|
62
|
+
id: elementId,
|
|
63
|
+
tier: this.tier,
|
|
64
|
+
confidence: bestWord.confidence / 100,
|
|
65
|
+
originalSelector: context.selector,
|
|
66
|
+
recommendedSelector: `ocr:text="${bestWord.text}"`,
|
|
67
|
+
message: `Found text "${bestWord.text}" via local OCR (${bestWord.confidence.toFixed(0)}% confidence)`,
|
|
68
|
+
rect: {
|
|
69
|
+
x: bestWord.bbox.x0,
|
|
70
|
+
y: bestWord.bbox.y0,
|
|
71
|
+
width: bestWord.bbox.x1 - bestWord.bbox.x0,
|
|
72
|
+
height: bestWord.bbox.y1 - bestWord.bbox.y0,
|
|
73
|
+
},
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
catch (predErr) {
|
|
79
|
+
this.logger.debug(`Predicate search failed: ${predErr.message}`);
|
|
80
|
+
}
|
|
81
|
+
// Fallback: Return coordinate-based result with virtual ID
|
|
82
|
+
// The CommandInterceptor handles rect-based results via coordinate tap
|
|
83
|
+
return {
|
|
84
|
+
id: `healed_ocr_${Date.now()}`,
|
|
85
|
+
tier: this.tier,
|
|
86
|
+
confidence: bestWord.confidence / 100,
|
|
87
|
+
originalSelector: context.selector,
|
|
88
|
+
recommendedSelector: `ocr:text="${bestWord.text}"`,
|
|
89
|
+
message: `Found text "${bestWord.text}" via local OCR (${bestWord.confidence.toFixed(0)}% confidence)`,
|
|
90
|
+
rect: {
|
|
91
|
+
x: bestWord.bbox.x0,
|
|
92
|
+
y: bestWord.bbox.y0,
|
|
93
|
+
width: bestWord.bbox.x1 - bestWord.bbox.x0,
|
|
94
|
+
height: bestWord.bbox.y1 - bestWord.bbox.y0,
|
|
95
|
+
},
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
catch (err) {
|
|
100
|
+
this.logger.error(`Error during OCR healing: ${err.message}`);
|
|
101
|
+
}
|
|
102
|
+
return null;
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
extractTextHint(selector) {
|
|
106
|
+
// If selector is //*[@text='Login'] or similar, grab 'Login'
|
|
107
|
+
const textMatch = selector.match(/text=['"]([^'"]+)['"]/i) ||
|
|
108
|
+
selector.match(/content-desc=['"]([^'"]+)['"]/i) ||
|
|
109
|
+
selector.match(/label=['"]([^'"]+)['"]/i) ||
|
|
110
|
+
selector.match(/name=['"]([^'"]+)['"]/i);
|
|
111
|
+
if (textMatch)
|
|
112
|
+
return textMatch[1];
|
|
113
|
+
// Otherwise try to grab last part of selector if it looks like words
|
|
114
|
+
const parts = selector.split(/[\/\@\[\]\=\'\"]/);
|
|
115
|
+
const lastWord = parts.reverse().find((p) => p.length > 3);
|
|
116
|
+
return lastWord || null;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
exports.OcrHealingProvider = OcrHealingProvider;
|
|
@@ -0,0 +1,100 @@
|
|
|
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.ResilioTreeHealingProvider = void 0;
|
|
16
|
+
const types_1 = require("./types");
|
|
17
|
+
const { Path, PathFinder, JSDOMParser, LCSPathDistance, HeuristicNodeDistance, } = require('resiliotree');
|
|
18
|
+
const typedi_1 = require("typedi");
|
|
19
|
+
const logger_1 = __importDefault(require("../../logger"));
|
|
20
|
+
const HealedLocatorGenerator_1 = require("./HealedLocatorGenerator");
|
|
21
|
+
class ResilioTreeHealingProvider {
|
|
22
|
+
constructor(etalonService) {
|
|
23
|
+
this.etalonService = etalonService;
|
|
24
|
+
this.name = 'ResilioTree Provider';
|
|
25
|
+
this.tier = types_1.HealingTier.TIER_1_RECOVERY; // High priority robust recovery
|
|
26
|
+
this.logger = logger_1.default.scope('ResilioTreeHealing');
|
|
27
|
+
this.generator = typedi_1.Container.get(HealedLocatorGenerator_1.HealedLocatorGenerator);
|
|
28
|
+
}
|
|
29
|
+
heal(context) {
|
|
30
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
31
|
+
if (!context.pageSource) {
|
|
32
|
+
this.logger.debug('No page source available for ResilioTree healing');
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
try {
|
|
36
|
+
const signature = yield this.etalonService.getSignature(context.selector);
|
|
37
|
+
if (!signature || !signature.path) {
|
|
38
|
+
this.logger.debug(`No ResilioTree path found for selector: ${context.selector}`);
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
this.logger.info(`Attempting robust ResilioTree recovery for: ${context.selector}`);
|
|
42
|
+
// 1. Parse current page source into ResilioTree model
|
|
43
|
+
const parser = new JSDOMParser();
|
|
44
|
+
const targetRoot = parser.parse(context.pageSource);
|
|
45
|
+
// 2. Revive the saved path from JSON
|
|
46
|
+
const savedPath = Path.fromJSON(signature.path);
|
|
47
|
+
// 3. Find the nearest node using ResilioTree's PathFinder
|
|
48
|
+
const pathDistance = new LCSPathDistance();
|
|
49
|
+
const nodeDistance = new HeuristicNodeDistance();
|
|
50
|
+
const pathFinder = new PathFinder(pathDistance, nodeDistance);
|
|
51
|
+
const nearestNode = pathFinder.findNearest(savedPath, targetRoot);
|
|
52
|
+
if (nearestNode) {
|
|
53
|
+
const candidateLocators = this.generator.generate(nearestNode);
|
|
54
|
+
const recommendedXpath = candidateLocators[0] || this.generateXpath(nearestNode);
|
|
55
|
+
this.logger.info(`ResilioTree suggested recovery XPath: ${recommendedXpath}`);
|
|
56
|
+
try {
|
|
57
|
+
const healedElement = yield context.driver.findElement('xpath', recommendedXpath);
|
|
58
|
+
if (healedElement) {
|
|
59
|
+
return {
|
|
60
|
+
id: healedElement.ELEMENT || healedElement['element-6066-11e4-a52e-4f735466cecf'],
|
|
61
|
+
originalSelector: context.selector,
|
|
62
|
+
recommendedSelector: recommendedXpath,
|
|
63
|
+
candidateSelectors: candidateLocators,
|
|
64
|
+
confidence: 0.9, // ResilioTree path matching is high confidence
|
|
65
|
+
tier: this.tier,
|
|
66
|
+
node: nearestNode,
|
|
67
|
+
message: `Recovered via ResilioTree path matching. New XPath: ${recommendedXpath}`,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
catch (e) {
|
|
72
|
+
this.logger.debug(`Driver failed to find element suggested by ResilioTree: ${recommendedXpath}`);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
catch (err) {
|
|
77
|
+
this.logger.error(`Error during ResilioTree healing: ${err.message}`);
|
|
78
|
+
}
|
|
79
|
+
return null;
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Generates an absolute XPath for a ResilioTree Node
|
|
84
|
+
*/
|
|
85
|
+
generateXpath(node) {
|
|
86
|
+
const parts = [];
|
|
87
|
+
let curr = node;
|
|
88
|
+
while (curr) {
|
|
89
|
+
const tag = curr.tag.toLowerCase();
|
|
90
|
+
// Skip JSDOM added wrapper tags for mobile compatibility
|
|
91
|
+
if (tag !== 'html' && tag !== 'body') {
|
|
92
|
+
const index = curr.index + 1; // XPath is 1-based
|
|
93
|
+
parts.unshift(`${curr.tag}[${index}]`);
|
|
94
|
+
}
|
|
95
|
+
curr = curr.parent;
|
|
96
|
+
}
|
|
97
|
+
return `/${parts.join('/')}`;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
exports.ResilioTreeHealingProvider = ResilioTreeHealingProvider;
|
|
@@ -0,0 +1,90 @@
|
|
|
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.VisualAiHealingProvider = void 0;
|
|
16
|
+
const types_1 = require("./types");
|
|
17
|
+
const AIService_1 = require("../AIService");
|
|
18
|
+
const logger_1 = __importDefault(require("../../logger"));
|
|
19
|
+
class VisualAiHealingProvider {
|
|
20
|
+
constructor() {
|
|
21
|
+
this.name = 'Visual AI Provider';
|
|
22
|
+
this.tier = types_1.HealingTier.TIER_4_VISUAL_AI;
|
|
23
|
+
this.logger = logger_1.default.scope('VisualAiHealing');
|
|
24
|
+
}
|
|
25
|
+
heal(context) {
|
|
26
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
27
|
+
var _a, _b;
|
|
28
|
+
if (!context.screenshotBase64) {
|
|
29
|
+
this.logger.debug('No screenshot available for visual AI matching');
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
// Check if AI service is enabled before attempting
|
|
33
|
+
if (!AIService_1.AI_SERVICE.isEnabled()) {
|
|
34
|
+
this.logger.debug('AI service not enabled, skipping Visual AI healing');
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
try {
|
|
38
|
+
// Describe what we are looking for based on the selector
|
|
39
|
+
const description = this.generateVisualDescription(context.selector);
|
|
40
|
+
this.logger.info(`Attempting Visual AI find for: "${description}"`);
|
|
41
|
+
const coordinates = yield AIService_1.AI_SERVICE.visualFind(context.screenshotBase64, description);
|
|
42
|
+
if (coordinates && typeof coordinates.x === 'number' && typeof coordinates.y === 'number') {
|
|
43
|
+
this.logger.info(`✅ Visual AI found element at (${coordinates.x}, ${coordinates.y})`);
|
|
44
|
+
return {
|
|
45
|
+
id: `healed_visual_${Date.now()}`,
|
|
46
|
+
tier: this.tier,
|
|
47
|
+
confidence: 0.8, // Basic vision models don't always give confidence, assuming high if found
|
|
48
|
+
originalSelector: context.selector,
|
|
49
|
+
recommendedSelector: `visual:description="${description}"`,
|
|
50
|
+
message: `Found element visually via AI Vision (${description})`,
|
|
51
|
+
rect: {
|
|
52
|
+
x: coordinates.x - 20, // Approximate bounding box
|
|
53
|
+
y: coordinates.y - 20,
|
|
54
|
+
width: 40,
|
|
55
|
+
height: 40,
|
|
56
|
+
},
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
catch (err) {
|
|
61
|
+
// Log service unavailability at debug level (expected), other errors at warn level
|
|
62
|
+
if (((_a = err.message) === null || _a === void 0 ? void 0 : _a.includes('unavailable')) || ((_b = err.message) === null || _b === void 0 ? void 0 : _b.includes('404'))) {
|
|
63
|
+
this.logger.debug(`Visual AI healing skipped: ${err.message}`);
|
|
64
|
+
}
|
|
65
|
+
else {
|
|
66
|
+
this.logger.warn(`Error during Visual AI healing: ${err.message}`);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return null;
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
generateVisualDescription(selector) {
|
|
73
|
+
// Convert selector to a descriptive string for the AI
|
|
74
|
+
// Handle iOS (@name, @label) and Android (@text, @content-desc) patterns
|
|
75
|
+
const textMatch = selector.match(/text=['"]([^'"]+)['"]/i) ||
|
|
76
|
+
selector.match(/label=['"]([^'"]+)['"]/i) ||
|
|
77
|
+
selector.match(/name=['"]([^'"]+)['"]/i) ||
|
|
78
|
+
selector.match(/content-desc=['"]([^'"]+)['"]/i);
|
|
79
|
+
if (textMatch) {
|
|
80
|
+
return `the element with text "${textMatch[1]}"`;
|
|
81
|
+
}
|
|
82
|
+
// Clean up IDs
|
|
83
|
+
const idMatch = selector.match(/id=['"]([^'"]+)['"]/i) || selector.match(/resource-id=['"]([^'"]+)['"]/i);
|
|
84
|
+
if (idMatch) {
|
|
85
|
+
return `the element with ID or identifier "${idMatch[1]}"`;
|
|
86
|
+
}
|
|
87
|
+
return `the element described by the locator "${selector}"`;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
exports.VisualAiHealingProvider = VisualAiHealingProvider;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.HealingTier = void 0;
|
|
4
|
+
var HealingTier;
|
|
5
|
+
(function (HealingTier) {
|
|
6
|
+
HealingTier[HealingTier["TIER_1_RECOVERY"] = 0] = "TIER_1_RECOVERY";
|
|
7
|
+
HealingTier[HealingTier["TIER_1_NATIVE"] = 1] = "TIER_1_NATIVE";
|
|
8
|
+
HealingTier[HealingTier["TIER_2_FUZZY_XML"] = 2] = "TIER_2_FUZZY_XML";
|
|
9
|
+
HealingTier[HealingTier["TIER_3_LOCAL_OCR"] = 3] = "TIER_3_LOCAL_OCR";
|
|
10
|
+
HealingTier[HealingTier["TIER_4_VISUAL_AI"] = 4] = "TIER_4_VISUAL_AI";
|
|
11
|
+
HealingTier[HealingTier["TIER_5_LLM_REASONING"] = 5] = "TIER_5_LLM_REASONING";
|
|
12
|
+
})(HealingTier || (exports.HealingTier = HealingTier = {}));
|