@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,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);
|