@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,294 @@
|
|
|
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
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
const chai_1 = require("chai");
|
|
13
|
+
const types_1 = require("../../src/services/healing/types");
|
|
14
|
+
const FuzzyXmlHealingProvider_1 = require("../../src/services/healing/FuzzyXmlHealingProvider");
|
|
15
|
+
const typedi_1 = require("typedi");
|
|
16
|
+
const HealedLocatorGenerator_1 = require("../../src/services/healing/HealedLocatorGenerator");
|
|
17
|
+
// Ensure DI container has the generator registered
|
|
18
|
+
typedi_1.Container.set(HealedLocatorGenerator_1.HealedLocatorGenerator, new HealedLocatorGenerator_1.HealedLocatorGenerator());
|
|
19
|
+
describe('FuzzyXmlHealingProvider', () => {
|
|
20
|
+
let provider;
|
|
21
|
+
let mockEtalonService;
|
|
22
|
+
// iOS page source: "Truck" button has been renamed to "Fleet"
|
|
23
|
+
const iosPageSource = `<?xml version="1.0" encoding="UTF-8"?>
|
|
24
|
+
<AppiumAUT>
|
|
25
|
+
<XCUIElementTypeApplication type="XCUIElementTypeApplication" name="Food Truck" label="Food Truck" enabled="true" visible="true" accessible="false" x="0" y="0" width="428" height="926">
|
|
26
|
+
<XCUIElementTypeWindow type="XCUIElementTypeWindow" enabled="true" visible="true" accessible="false" x="0" y="0" width="428" height="926">
|
|
27
|
+
<XCUIElementTypeOther type="XCUIElementTypeOther" enabled="true" visible="true" accessible="false" x="0" y="0" width="428" height="926">
|
|
28
|
+
<XCUIElementTypeButton type="XCUIElementTypeButton" name="Fleet" label="Fleet" enabled="true" visible="true" accessible="true" x="20" y="161" width="388" height="44" />
|
|
29
|
+
<XCUIElementTypeButton type="XCUIElementTypeButton" name="Deliveries" label="Deliveries" enabled="true" visible="true" accessible="true" x="20" y="205" width="388" height="44" />
|
|
30
|
+
<XCUIElementTypeButton type="XCUIElementTypeButton" name="Social Feed" label="Social Feed" enabled="true" visible="true" accessible="true" x="20" y="249" width="388" height="44" />
|
|
31
|
+
<XCUIElementTypeImage type="XCUIElementTypeImage" name="truck.box" label="truck.box" enabled="true" visible="true" accessible="false" x="36" y="215" width="20" height="20" />
|
|
32
|
+
<XCUIElementTypeStaticText type="XCUIElementTypeStaticText" name="Deliveries" label="Deliveries" enabled="true" visible="true" accessible="false" x="72" y="205" width="300" height="44" />
|
|
33
|
+
</XCUIElementTypeOther>
|
|
34
|
+
</XCUIElementTypeWindow>
|
|
35
|
+
</XCUIElementTypeApplication>
|
|
36
|
+
</AppiumAUT>`;
|
|
37
|
+
// Android page source: "Submit" button renamed to "Send"
|
|
38
|
+
const androidPageSource = `<?xml version="1.0" encoding="UTF-8"?>
|
|
39
|
+
<hierarchy>
|
|
40
|
+
<android.widget.FrameLayout index="0" bounds="[0,0][1080,1920]">
|
|
41
|
+
<android.widget.LinearLayout index="0" bounds="[0,0][1080,1920]">
|
|
42
|
+
<android.widget.Button index="0" text="Send" resource-id="com.example:id/submit_btn" content-desc="Send action" bounds="[48,1584][1032,1680]" />
|
|
43
|
+
<android.widget.TextView index="1" text="Hello World" bounds="[48,1700][1032,1750]" />
|
|
44
|
+
<android.widget.ImageView index="2" content-desc="Logo" bounds="[400,100][680,300]" />
|
|
45
|
+
</android.widget.LinearLayout>
|
|
46
|
+
</android.widget.FrameLayout>
|
|
47
|
+
</hierarchy>`;
|
|
48
|
+
beforeEach(() => {
|
|
49
|
+
mockEtalonService = {
|
|
50
|
+
getSignature: (_selector) => __awaiter(void 0, void 0, void 0, function* () { return null; }),
|
|
51
|
+
saveSignature: () => __awaiter(void 0, void 0, void 0, function* () { }),
|
|
52
|
+
};
|
|
53
|
+
provider = new FuzzyXmlHealingProvider_1.FuzzyXmlHealingProvider(mockEtalonService);
|
|
54
|
+
});
|
|
55
|
+
// ============================================================
|
|
56
|
+
// TEST 1: First candidate fails, second candidate succeeds (Fallback)
|
|
57
|
+
// ============================================================
|
|
58
|
+
it('should fall back to second candidate when first candidate fails', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
59
|
+
// Etalon: We knew this element as "Orders" at position (20, 205)
|
|
60
|
+
mockEtalonService.getSignature = () => __awaiter(void 0, void 0, void 0, function* () {
|
|
61
|
+
return ({
|
|
62
|
+
selector: " //XCUIElementTypeButton[@name='Orders']",
|
|
63
|
+
strategy: 'xpath',
|
|
64
|
+
attributes: {
|
|
65
|
+
name: 'Orders',
|
|
66
|
+
label: 'Orders',
|
|
67
|
+
x: '20',
|
|
68
|
+
y: '205',
|
|
69
|
+
width: '388',
|
|
70
|
+
height: '44',
|
|
71
|
+
},
|
|
72
|
+
nodeName: 'XCUIElementTypeButton',
|
|
73
|
+
lastSeen: Date.now(),
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
const triedCandidates = [];
|
|
77
|
+
const mockDriver = {
|
|
78
|
+
findElement: (_strategy, selector) => __awaiter(void 0, void 0, void 0, function* () {
|
|
79
|
+
triedCandidates.push(selector);
|
|
80
|
+
// FIRST candidate fails (simulating a locator that doesn't resolve)
|
|
81
|
+
if (triedCandidates.length === 1) {
|
|
82
|
+
throw new Error('no such element');
|
|
83
|
+
}
|
|
84
|
+
// SECOND candidate succeeds
|
|
85
|
+
return { ELEMENT: 'healed-via-fallback-123' };
|
|
86
|
+
}),
|
|
87
|
+
};
|
|
88
|
+
const context = {
|
|
89
|
+
sessionId: 'test-session',
|
|
90
|
+
driver: mockDriver,
|
|
91
|
+
strategy: 'xpath',
|
|
92
|
+
selector: " //XCUIElementTypeButton[@name='Orders']",
|
|
93
|
+
pageSource: iosPageSource,
|
|
94
|
+
};
|
|
95
|
+
const result = yield provider.heal(context);
|
|
96
|
+
// Verify healing succeeded
|
|
97
|
+
(0, chai_1.expect)(result).to.not.be.null;
|
|
98
|
+
(0, chai_1.expect)(result === null || result === void 0 ? void 0 : result.id).to.equal('healed-via-fallback-123');
|
|
99
|
+
(0, chai_1.expect)(result === null || result === void 0 ? void 0 : result.tier).to.equal(types_1.HealingTier.TIER_2_FUZZY_XML);
|
|
100
|
+
// Verify that at least 2 candidates were tried (first failed, second succeeded)
|
|
101
|
+
(0, chai_1.expect)(triedCandidates.length).to.be.greaterThanOrEqual(2);
|
|
102
|
+
console.log(` ✓ Tried ${triedCandidates.length} candidates before success:`);
|
|
103
|
+
triedCandidates.forEach((c, i) => console.log(` ${i + 1}. ${c}`));
|
|
104
|
+
}));
|
|
105
|
+
// ============================================================
|
|
106
|
+
// TEST 2: Tag gate rejects wrong element types
|
|
107
|
+
// ============================================================
|
|
108
|
+
it('should reject wrong element types via tag gate (XCUIElementTypeImage should not match XCUIElementTypeButton)', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
109
|
+
// Etalon: We're looking for a Button
|
|
110
|
+
mockEtalonService.getSignature = () => __awaiter(void 0, void 0, void 0, function* () {
|
|
111
|
+
return ({
|
|
112
|
+
selector: " //XCUIElementTypeButton[@name='Orders']",
|
|
113
|
+
strategy: 'xpath',
|
|
114
|
+
attributes: {
|
|
115
|
+
name: 'Orders',
|
|
116
|
+
label: 'Orders',
|
|
117
|
+
x: '20',
|
|
118
|
+
y: '205',
|
|
119
|
+
},
|
|
120
|
+
nodeName: 'XCUIElementTypeButton',
|
|
121
|
+
lastSeen: Date.now(),
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
let resolvedSelector = '';
|
|
125
|
+
const mockDriver = {
|
|
126
|
+
findElement: (_strategy, selector) => __awaiter(void 0, void 0, void 0, function* () {
|
|
127
|
+
resolvedSelector = selector;
|
|
128
|
+
return { ELEMENT: 'correct-button-456' };
|
|
129
|
+
}),
|
|
130
|
+
};
|
|
131
|
+
const context = {
|
|
132
|
+
sessionId: 'test-session',
|
|
133
|
+
driver: mockDriver,
|
|
134
|
+
strategy: 'xpath',
|
|
135
|
+
selector: " //XCUIElementTypeButton[@name='Orders']",
|
|
136
|
+
pageSource: iosPageSource,
|
|
137
|
+
};
|
|
138
|
+
const result = yield provider.heal(context);
|
|
139
|
+
(0, chai_1.expect)(result).to.not.be.null;
|
|
140
|
+
// The resolved element should be a Button (Deliveries), NOT an Image (truck.box)
|
|
141
|
+
if (resolvedSelector) {
|
|
142
|
+
(0, chai_1.expect)(resolvedSelector).to.not.contain('truck.box');
|
|
143
|
+
(0, chai_1.expect)(resolvedSelector).to.not.contain('XCUIElementTypeImage');
|
|
144
|
+
console.log(` ✓ Resolved to: ${resolvedSelector} (not truck.box image)`);
|
|
145
|
+
}
|
|
146
|
+
}));
|
|
147
|
+
// ============================================================
|
|
148
|
+
// TEST 3: Spatial matching picks correct element at same position
|
|
149
|
+
// ============================================================
|
|
150
|
+
it('should select Deliveries (at Y=205) over Fleet (at Y=161) based on spatial match', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
151
|
+
// Etalon: "Orders" was at (20, 205) — Deliveries is at the same position
|
|
152
|
+
mockEtalonService.getSignature = () => __awaiter(void 0, void 0, void 0, function* () {
|
|
153
|
+
return ({
|
|
154
|
+
selector: " //XCUIElementTypeButton[@name='Orders']",
|
|
155
|
+
strategy: 'xpath',
|
|
156
|
+
attributes: {
|
|
157
|
+
name: 'Orders',
|
|
158
|
+
label: 'Orders',
|
|
159
|
+
x: '20',
|
|
160
|
+
y: '205',
|
|
161
|
+
width: '388',
|
|
162
|
+
height: '44',
|
|
163
|
+
},
|
|
164
|
+
nodeName: 'XCUIElementTypeButton',
|
|
165
|
+
lastSeen: Date.now(),
|
|
166
|
+
});
|
|
167
|
+
});
|
|
168
|
+
let resolvedSelector = '';
|
|
169
|
+
const mockDriver = {
|
|
170
|
+
findElement: (_strategy, selector) => __awaiter(void 0, void 0, void 0, function* () {
|
|
171
|
+
resolvedSelector = selector;
|
|
172
|
+
return { ELEMENT: 'deliveries-789' };
|
|
173
|
+
}),
|
|
174
|
+
};
|
|
175
|
+
const context = {
|
|
176
|
+
sessionId: 'test-session',
|
|
177
|
+
driver: mockDriver,
|
|
178
|
+
strategy: 'xpath',
|
|
179
|
+
selector: " //XCUIElementTypeButton[@name='Orders']",
|
|
180
|
+
pageSource: iosPageSource,
|
|
181
|
+
};
|
|
182
|
+
const result = yield provider.heal(context);
|
|
183
|
+
(0, chai_1.expect)(result).to.not.be.null;
|
|
184
|
+
(0, chai_1.expect)(result === null || result === void 0 ? void 0 : result.id).to.equal('deliveries-789');
|
|
185
|
+
// Should resolve to Deliveries, NOT Fleet
|
|
186
|
+
(0, chai_1.expect)(resolvedSelector).to.contain('Deliveries');
|
|
187
|
+
(0, chai_1.expect)(resolvedSelector).to.not.contain('Fleet');
|
|
188
|
+
console.log(` ✓ Spatial match selected: ${resolvedSelector} (correct position)`);
|
|
189
|
+
}));
|
|
190
|
+
// ============================================================
|
|
191
|
+
// TEST 4: Android bounds format parsing works for spatial matching
|
|
192
|
+
// ============================================================
|
|
193
|
+
it('should parse Android bounds="[x1,y1][x2,y2]" format for spatial matching', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
194
|
+
// Etalon: "Submit" was at (48, 1584) on Android
|
|
195
|
+
mockEtalonService.getSignature = () => __awaiter(void 0, void 0, void 0, function* () {
|
|
196
|
+
return ({
|
|
197
|
+
selector: "//android.widget.Button[@text='Submit']",
|
|
198
|
+
strategy: 'xpath',
|
|
199
|
+
attributes: {
|
|
200
|
+
text: 'Submit',
|
|
201
|
+
'resource-id': 'com.example:id/submit_btn',
|
|
202
|
+
x: '48',
|
|
203
|
+
y: '1584',
|
|
204
|
+
width: '984',
|
|
205
|
+
height: '96',
|
|
206
|
+
},
|
|
207
|
+
nodeName: 'android.widget.Button',
|
|
208
|
+
lastSeen: Date.now(),
|
|
209
|
+
});
|
|
210
|
+
});
|
|
211
|
+
let resolvedSelector = '';
|
|
212
|
+
const mockDriver = {
|
|
213
|
+
findElement: (_strategy, selector) => __awaiter(void 0, void 0, void 0, function* () {
|
|
214
|
+
resolvedSelector = selector;
|
|
215
|
+
return { ELEMENT: 'android-healed-101' };
|
|
216
|
+
}),
|
|
217
|
+
};
|
|
218
|
+
const context = {
|
|
219
|
+
sessionId: 'test-session',
|
|
220
|
+
driver: mockDriver,
|
|
221
|
+
strategy: 'xpath',
|
|
222
|
+
selector: "//android.widget.Button[@text='Submit']",
|
|
223
|
+
pageSource: androidPageSource,
|
|
224
|
+
};
|
|
225
|
+
const result = yield provider.heal(context);
|
|
226
|
+
(0, chai_1.expect)(result).to.not.be.null;
|
|
227
|
+
(0, chai_1.expect)(result === null || result === void 0 ? void 0 : result.id).to.equal('android-healed-101');
|
|
228
|
+
// Should NOT match ImageView (Logo at y=100) — tag gate eliminates it
|
|
229
|
+
(0, chai_1.expect)(resolvedSelector).to.not.contain('ImageView');
|
|
230
|
+
console.log(` ✓ Android spatial match resolved: ${resolvedSelector}`);
|
|
231
|
+
}));
|
|
232
|
+
// ============================================================
|
|
233
|
+
// TEST 5: All candidates fail returns null
|
|
234
|
+
// ============================================================
|
|
235
|
+
it('should return null when ALL candidate locators fail', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
236
|
+
mockEtalonService.getSignature = () => __awaiter(void 0, void 0, void 0, function* () {
|
|
237
|
+
return ({
|
|
238
|
+
selector: " //XCUIElementTypeButton[@name='Orders']",
|
|
239
|
+
strategy: 'xpath',
|
|
240
|
+
attributes: {
|
|
241
|
+
name: 'Orders',
|
|
242
|
+
label: 'Orders',
|
|
243
|
+
x: '20',
|
|
244
|
+
y: '205',
|
|
245
|
+
},
|
|
246
|
+
nodeName: 'XCUIElementTypeButton',
|
|
247
|
+
lastSeen: Date.now(),
|
|
248
|
+
});
|
|
249
|
+
});
|
|
250
|
+
let attemptCount = 0;
|
|
251
|
+
const mockDriver = {
|
|
252
|
+
findElement: () => __awaiter(void 0, void 0, void 0, function* () {
|
|
253
|
+
attemptCount++;
|
|
254
|
+
throw new Error('no such element');
|
|
255
|
+
}),
|
|
256
|
+
};
|
|
257
|
+
const context = {
|
|
258
|
+
sessionId: 'test-session',
|
|
259
|
+
driver: mockDriver,
|
|
260
|
+
strategy: 'xpath',
|
|
261
|
+
selector: " //XCUIElementTypeButton[@name='Orders']",
|
|
262
|
+
pageSource: iosPageSource,
|
|
263
|
+
};
|
|
264
|
+
const result = yield provider.heal(context);
|
|
265
|
+
(0, chai_1.expect)(result).to.be.null;
|
|
266
|
+
// Should have tried multiple candidates before giving up
|
|
267
|
+
(0, chai_1.expect)(attemptCount).to.be.greaterThan(1);
|
|
268
|
+
console.log(` ✓ Tried ${attemptCount} candidates, all failed → returned null correctly`);
|
|
269
|
+
}));
|
|
270
|
+
// ============================================================
|
|
271
|
+
// TEST 6: No etalon — keyword-only fallback works
|
|
272
|
+
// ============================================================
|
|
273
|
+
it('should work without etalon using keyword-only fuzzy matching', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
274
|
+
// No etalon stored — pure keyword matching
|
|
275
|
+
mockEtalonService.getSignature = () => __awaiter(void 0, void 0, void 0, function* () { return null; });
|
|
276
|
+
const mockDriver = {
|
|
277
|
+
findElement: (_strategy, _selector) => __awaiter(void 0, void 0, void 0, function* () {
|
|
278
|
+
return { ELEMENT: 'keyword-match-202' };
|
|
279
|
+
}),
|
|
280
|
+
};
|
|
281
|
+
// Looking for something with "Deliveries" as a keyword
|
|
282
|
+
const context = {
|
|
283
|
+
sessionId: 'test-session',
|
|
284
|
+
driver: mockDriver,
|
|
285
|
+
strategy: 'xpath',
|
|
286
|
+
selector: "//XCUIElementTypeButton[@name='Deliveries']",
|
|
287
|
+
pageSource: iosPageSource,
|
|
288
|
+
};
|
|
289
|
+
const result = yield provider.heal(context);
|
|
290
|
+
(0, chai_1.expect)(result).to.not.be.null;
|
|
291
|
+
(0, chai_1.expect)(result === null || result === void 0 ? void 0 : result.id).to.equal('keyword-match-202');
|
|
292
|
+
console.log(` ✓ Keyword-only match succeeded: ${result === null || result === void 0 ? void 0 : result.recommendedSelector}`);
|
|
293
|
+
}));
|
|
294
|
+
});
|
|
@@ -0,0 +1,42 @@
|
|
|
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.getAdbOriginal = getAdbOriginal;
|
|
16
|
+
const appium_adb_1 = __importDefault(require("appium-adb"));
|
|
17
|
+
function getAdbOriginal() {
|
|
18
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
19
|
+
return yield appium_adb_1.default.createADB({
|
|
20
|
+
udid: null,
|
|
21
|
+
appDeviceReadyTimeout: null,
|
|
22
|
+
useKeystore: null,
|
|
23
|
+
keystorePath: null,
|
|
24
|
+
keystorePassword: null,
|
|
25
|
+
keyAlias: null,
|
|
26
|
+
keyPassword: null,
|
|
27
|
+
curDeviceId: null,
|
|
28
|
+
emulatorPort: null,
|
|
29
|
+
logcat: null,
|
|
30
|
+
instrumentProc: null,
|
|
31
|
+
suppressKillServer: null,
|
|
32
|
+
jars: {},
|
|
33
|
+
adbPort: 5037,
|
|
34
|
+
adbHost: null,
|
|
35
|
+
adbExecTimeout: 20000,
|
|
36
|
+
remoteAppsCacheLimit: 10,
|
|
37
|
+
buildToolsVersion: null,
|
|
38
|
+
allowOfflineDevices: false,
|
|
39
|
+
allowDelayAdb: true,
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
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
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
const chai_1 = require("chai");
|
|
13
|
+
const HealingOrchestrator_1 = require("../../src/services/healing/HealingOrchestrator");
|
|
14
|
+
const types_1 = require("../../src/services/healing/types");
|
|
15
|
+
const typedi_1 = require("typedi");
|
|
16
|
+
const HealEtalonService_1 = require("../../src/services/healing/HealEtalonService");
|
|
17
|
+
const HealedLocatorGenerator_1 = require("../../src/services/healing/HealedLocatorGenerator");
|
|
18
|
+
// Mock Tesseract
|
|
19
|
+
const TesseractMock = {
|
|
20
|
+
recognize: () => __awaiter(void 0, void 0, void 0, function* () {
|
|
21
|
+
return ({
|
|
22
|
+
data: {
|
|
23
|
+
words: [
|
|
24
|
+
{
|
|
25
|
+
text: 'Login',
|
|
26
|
+
confidence: 90,
|
|
27
|
+
bbox: { x0: 100, y0: 100, x1: 200, y1: 150 },
|
|
28
|
+
},
|
|
29
|
+
],
|
|
30
|
+
},
|
|
31
|
+
});
|
|
32
|
+
}),
|
|
33
|
+
};
|
|
34
|
+
// We need to override the import or the class behavior for the test
|
|
35
|
+
// Since OcrHealingProvider imports Tesseract directly, we'll mock the heal method for this specific test
|
|
36
|
+
// to avoid complex dependency mocking of tesseract.js in a POC environment.
|
|
37
|
+
describe('Healing Orchestrator - Cascade Strategy', () => {
|
|
38
|
+
let orchestrator;
|
|
39
|
+
let mockEtalonService;
|
|
40
|
+
let mockGenerator;
|
|
41
|
+
beforeEach(() => {
|
|
42
|
+
typedi_1.Container.reset();
|
|
43
|
+
mockEtalonService = {
|
|
44
|
+
getSignature: () => __awaiter(void 0, void 0, void 0, function* () { return null; }),
|
|
45
|
+
saveSignature: () => __awaiter(void 0, void 0, void 0, function* () { }),
|
|
46
|
+
};
|
|
47
|
+
mockGenerator = new HealedLocatorGenerator_1.HealedLocatorGenerator();
|
|
48
|
+
typedi_1.Container.set(HealEtalonService_1.HealEtalonService, mockEtalonService);
|
|
49
|
+
typedi_1.Container.set(HealedLocatorGenerator_1.HealedLocatorGenerator, mockGenerator);
|
|
50
|
+
orchestrator = new HealingOrchestrator_1.HealingOrchestrator(mockEtalonService);
|
|
51
|
+
});
|
|
52
|
+
it('should cascade to OCR (Tier 3) when Fuzzy XML (Tier 2) fails to find a high-confidence match', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
53
|
+
// 1. Setup a page source that has NO matching text or buttons for "Login"
|
|
54
|
+
// This will cause Fuzzy XML Provider to return null (or score below 0.5 threshold)
|
|
55
|
+
const emptyPageSource = '<?xml version="1.0" encoding="UTF-8"?><AppiumAUT><XCUIElementTypeWindow /></AppiumAUT>';
|
|
56
|
+
// 2. Setup a screenshot (dummy base64)
|
|
57
|
+
const dummyScreenshot = 'fake-base64-screenshot';
|
|
58
|
+
const mockDriver = {
|
|
59
|
+
getPageSource: () => __awaiter(void 0, void 0, void 0, function* () { return emptyPageSource; }),
|
|
60
|
+
getScreenshot: () => __awaiter(void 0, void 0, void 0, function* () { return dummyScreenshot; }),
|
|
61
|
+
findElements: () => __awaiter(void 0, void 0, void 0, function* () { return []; }),
|
|
62
|
+
findElement: (strategy, selector) => __awaiter(void 0, void 0, void 0, function* () {
|
|
63
|
+
// If called via OCR tap-resolution predicate:
|
|
64
|
+
if (strategy === '-ios predicate string' && selector.includes('Login')) {
|
|
65
|
+
return { ELEMENT: 'real-element-from-ocr' };
|
|
66
|
+
}
|
|
67
|
+
throw new Error('no such element');
|
|
68
|
+
}),
|
|
69
|
+
};
|
|
70
|
+
// 3. Mock the OcrHealingProvider behavior for this test to avoid Tesseract native issues
|
|
71
|
+
// We'll replace the OCR provider in the orchestrator with a mock one that succeeds
|
|
72
|
+
const ocrProvider = orchestrator.providers.find((p) => p.tier === types_1.HealingTier.TIER_3_LOCAL_OCR);
|
|
73
|
+
ocrProvider.heal = (context) => __awaiter(void 0, void 0, void 0, function* () {
|
|
74
|
+
if (context.selector.includes('Login')) {
|
|
75
|
+
return {
|
|
76
|
+
id: 'healed-via-ocr',
|
|
77
|
+
tier: types_1.HealingTier.TIER_3_LOCAL_OCR,
|
|
78
|
+
confidence: 0.9,
|
|
79
|
+
originalSelector: context.selector,
|
|
80
|
+
recommendedSelector: 'ocr:text="Login"',
|
|
81
|
+
rect: { x: 100, y: 100, width: 100, height: 50 },
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
return null;
|
|
85
|
+
});
|
|
86
|
+
// 4. Trigger healing for a broken "Login" button
|
|
87
|
+
const result = yield orchestrator.attemptHealing('test-session', mockDriver, 'xpath', "//XCUIElementTypeButton[@name='Login']");
|
|
88
|
+
// 5. Verification
|
|
89
|
+
(0, chai_1.expect)(result).to.not.be.null;
|
|
90
|
+
(0, chai_1.expect)(result === null || result === void 0 ? void 0 : result.tier).to.equal(types_1.HealingTier.TIER_3_LOCAL_OCR);
|
|
91
|
+
(0, chai_1.expect)(result === null || result === void 0 ? void 0 : result.id).to.equal('healed-via-ocr');
|
|
92
|
+
console.log(` ✓ Cascaed successful! Fuzzy XML failed, but OCR found the element: ${(result === null || result === void 0 ? void 0 : result.message) || 'OCR result'}`);
|
|
93
|
+
}));
|
|
94
|
+
it('should continue to LLM Reasoning (Tier 5) if all preceding tiers fail', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
95
|
+
const emptyPageSource = '<?xml version="1.0" encoding="UTF-8"?><AppiumAUT><XCUIElementTypeWindow /></AppiumAUT>';
|
|
96
|
+
const mockDriver = {
|
|
97
|
+
getPageSource: () => __awaiter(void 0, void 0, void 0, function* () { return emptyPageSource; }),
|
|
98
|
+
getScreenshot: () => __awaiter(void 0, void 0, void 0, function* () { return 'fake-screenshot'; }),
|
|
99
|
+
findElement: (strategy, selector) => __awaiter(void 0, void 0, void 0, function* () {
|
|
100
|
+
if (selector === '//llm-healed')
|
|
101
|
+
return { ELEMENT: 'llm-element' };
|
|
102
|
+
throw new Error('not found');
|
|
103
|
+
}),
|
|
104
|
+
};
|
|
105
|
+
// Mock ALL providers before LLM to return null
|
|
106
|
+
orchestrator.providers.forEach((p) => {
|
|
107
|
+
if (p.tier < types_1.HealingTier.TIER_5_LLM_REASONING) {
|
|
108
|
+
p.heal = () => __awaiter(void 0, void 0, void 0, function* () { return null; });
|
|
109
|
+
}
|
|
110
|
+
else {
|
|
111
|
+
// Tier 5 Mock
|
|
112
|
+
p.heal = () => __awaiter(void 0, void 0, void 0, function* () {
|
|
113
|
+
return ({
|
|
114
|
+
id: 'llm-element',
|
|
115
|
+
tier: types_1.HealingTier.TIER_5_LLM_REASONING,
|
|
116
|
+
confidence: 0.95,
|
|
117
|
+
originalSelector: '//broken',
|
|
118
|
+
recommendedSelector: '//llm-healed',
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
const result = yield orchestrator.attemptHealing('test-session', mockDriver, 'xpath', '//broken');
|
|
124
|
+
(0, chai_1.expect)(result === null || result === void 0 ? void 0 : result.tier).to.equal(types_1.HealingTier.TIER_5_LLM_REASONING);
|
|
125
|
+
(0, chai_1.expect)(result === null || result === void 0 ? void 0 : result.id).to.equal('llm-element');
|
|
126
|
+
console.log(' ✓ Cascade reached Tier 5 (LLM) correctly');
|
|
127
|
+
}));
|
|
128
|
+
});
|