@xenon-device-management/xenon 2.13.2 → 2.14.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 +11 -5
- package/lib/package.json +1 -1
- package/lib/public/assets/{AnnotationOverlay-BXruClDk.js → AnnotationOverlay-B_VNtKBH.js} +1 -1
- package/lib/public/assets/{ApiKeyGate-qTS4LPLC.js → ApiKeyGate-BwuaMjkX.js} +1 -1
- package/lib/public/assets/{BugReportButton-BoGhlI7F.js → BugReportButton-BAt0_pzq.js} +1 -1
- package/lib/public/assets/DeviceMosaicView-BYaQHhTf.js +2 -0
- package/lib/public/assets/{EmptyState-Cr17PdDv.js → EmptyState-BSjhk_IV.js} +1 -1
- package/lib/public/assets/{FieldGroup-Bvn08svv.js → FieldGroup-Cb5QZsE1.js} +1 -1
- package/lib/public/assets/{FilterMenu-DDSO8aDf.js → FilterMenu-BAaSlWbW.js} +1 -1
- package/lib/public/assets/{Menu-B-Qn2X3T.js → Menu-JGGYrSQQ.js} +1 -1
- package/lib/public/assets/{Modal-Cg9V5JYU.js → Modal-CFCp3n9d.js} +1 -1
- package/lib/public/assets/{RecordingPage-BTUs9VTW.js → RecordingPage-CbJj_EBT.js} +1 -1
- package/lib/public/assets/{RecordingsPage-DYcfxITK.js → RecordingsPage-BJeYnygR.js} +2 -2
- package/lib/public/assets/{SegmentedControl-Zy7suhT5.js → SegmentedControl-Bg42Atjy.js} +1 -1
- package/lib/public/assets/{SettingCard-CWYmycoe.js → SettingCard-Njz1pUIJ.js} +1 -1
- package/lib/public/assets/{Table-io9fwa98.js → Table-BcTm5xV2.js} +1 -1
- package/lib/public/assets/{activity-Bdf_uIWl.js → activity-DOVdsfMQ.js} +1 -1
- package/lib/public/assets/{ai-settings-Bce5SJob.js → ai-settings-CsuJroRf.js} +2 -2
- package/lib/public/assets/{api-keys-CPqtVpm3.js → api-keys-BKnNSTw9.js} +1 -1
- package/lib/public/assets/{apps-BgZan0H9.js → apps-D0SC6uw6.js} +2 -2
- package/lib/public/assets/{arrow-left-B27ckB31.js → arrow-left-MMbTmgoE.js} +1 -1
- package/lib/public/assets/{arrow-right-DOF8ITCz.js → arrow-right-BMfnGmxW.js} +1 -1
- package/lib/public/assets/{arrow-up-right-eFG5KTFS.js → arrow-up-right-DuAftMWB.js} +1 -1
- package/lib/public/assets/{auth-shell-DlC3_27j.js → auth-shell-nZC2JNDO.js} +2 -2
- package/lib/public/assets/{builds-page-BuXpPfKo.js → builds-page-hDCgTwkS.js} +1 -1
- package/lib/public/assets/{button-fIW8owMI.js → button-Dda4irn_.js} +1 -1
- package/lib/public/assets/{calendar-WNjmrF-a.js → calendar-BMr0Q62v.js} +1 -1
- package/lib/public/assets/{check-BdTh1Sq7.js → check-CZ9iw9-e.js} +1 -1
- package/lib/public/assets/{chevron-right-B4BKlsqk.js → chevron-right-BRYJJVC9.js} +1 -1
- package/lib/public/assets/{circle-check-zJU80_EF.js → circle-check-CqAJjQ5O.js} +1 -1
- package/lib/public/assets/{circle-x-5Uwquo0h.js → circle-x-DlDJtnVz.js} +1 -1
- package/lib/public/assets/{clock-BtUprDsZ.js → clock-CpuDKysU.js} +1 -1
- package/lib/public/assets/{copy-DGSbZI3u.js → copy-CAQydNFe.js} +1 -1
- package/lib/public/assets/device-explorer-CdO6_nrn.js +282 -0
- package/lib/public/assets/{download-WG6l7BgD.js → download-C9XG6lLv.js} +1 -1
- package/lib/public/assets/{forgot-password-DnudqFFo.js → forgot-password-CGnkRgE9.js} +1 -1
- package/lib/public/assets/h264Stream-VQBphYhg.js +26 -0
- package/lib/public/assets/{index-DI1fGIyj.js → index-BbKEXUFH.js} +5 -5
- package/lib/public/assets/{index-HDQVe1gi.js → index-DJGc1M2f.js} +1 -1
- package/lib/public/assets/{input-BjDRKCea.js → input-Cm0tmSoc.js} +1 -1
- package/lib/public/assets/{line-chart-C0zdgd0v.js → line-chart-D2DyJCml.js} +1 -1
- package/lib/public/assets/{list-checks-BdW5CjM8.js → list-checks-UWf1GY99.js} +1 -1
- package/lib/public/assets/{lock-Dy3L4I8E.js → lock-CCJv9m_I.js} +1 -1
- package/lib/public/assets/{login-BHbY9CLG.js → login-f8vOVW-U.js} +1 -1
- package/lib/public/assets/maintenance-settings-C1PrkuAH.js +6 -0
- package/lib/public/assets/{monitor-8xjkeHHi.js → monitor-B2jDgwm7.js} +1 -1
- package/lib/public/assets/{mouse-pointer-2-CXdKMXQ8.js → mouse-pointer-2-DLI1RtzU.js} +1 -1
- package/lib/public/assets/{network-Cj8JOKON.js → network-ebT86qoi.js} +1 -1
- package/lib/public/assets/{overview-CTg7F0im.js → overview-C0aykVrd.js} +1 -1
- package/lib/public/assets/{page-header--aTyJXq6.js → page-header-BjzgWz-T.js} +1 -1
- package/lib/public/assets/{play-B-FodgHV.js → play-DLG9t8FU.js} +1 -1
- package/lib/public/assets/{plus-CsIk10Cq.js → plus-nBFSMu_X.js} +1 -1
- package/lib/public/assets/{profile-page-DkFKkMfl.js → profile-page-CSl0UYlv.js} +1 -1
- package/lib/public/assets/{reset-password-I2-zqLlI.js → reset-password-LeOuHqmB.js} +1 -1
- package/lib/public/assets/{runbook-page-DJTVCJX7.js → runbook-page-CMMeQml9.js} +1 -1
- package/lib/public/assets/{select-DvCSv25z.js → select-88jHcl-Z.js} +1 -1
- package/lib/public/assets/{selector-detail-redirect-BLVfMJs_.js → selector-detail-redirect-BbXKJjZM.js} +1 -1
- package/lib/public/assets/{selector-health-page-oqadRokn.js → selector-health-page-B4rA1Xkv.js} +2 -2
- package/lib/public/assets/session-detail-page-COL_RZxw.js +4 -0
- package/lib/public/assets/settings-GF6ZMSQ6.js +6 -0
- package/lib/public/assets/{stat-tile-DV9Ix-4O.js → stat-tile-BV98e_Sv.js} +1 -1
- package/lib/public/assets/{tablet-6b_SEN2X.js → tablet-BiAsnFDj.js} +1 -1
- package/lib/public/assets/{teams-BjZyfuHh.js → teams-B4-jrr-1.js} +1 -1
- package/lib/public/assets/{trash-2-CTZpPSdd.js → trash-2-C5RSuX0e.js} +1 -1
- package/lib/public/assets/{upload-P3XzwQUb.js → upload-BG7_rMXK.js} +1 -1
- package/lib/public/assets/{use-builds-data-1cOCMf-z.js → use-builds-data-J-riQYyC.js} +1 -1
- package/lib/public/assets/{use-password-reset-mode-DgF9w088.js → use-password-reset-mode-C8-eH-UG.js} +1 -1
- package/lib/public/assets/{users-DMF9Mk5H.js → users-CFba5TSy.js} +1 -1
- package/lib/public/assets/{users-CiNTKUiz.js → users-Cp0beJm3.js} +1 -1
- package/lib/public/assets/{video-off-CHp1KU_T.js → video-off-DDsEISjV.js} +1 -1
- package/lib/public/assets/webhook-settings-BGu32vSB.js +1 -0
- package/lib/public/assets/{zap-DRz0tDq5.js → zap-l4VaXzq8.js} +1 -1
- package/lib/public/index.html +1 -1
- package/lib/schema.json +19 -21
- package/lib/src/XenonCapabilityManager.js +22 -5
- package/lib/src/app/openapi/control.yaml +71 -20
- package/lib/src/app/openapi/grid.yaml +23 -5
- package/lib/src/app/openapi/platform.yaml +136 -28
- package/lib/src/app/routers/control.js +12 -2
- package/lib/src/app/routers/dashboard.js +29 -1
- package/lib/src/app/routers/grid.js +32 -10
- package/lib/src/app/routers/streamType.js +7 -2
- package/lib/src/app/routers/webhook.js +8 -4
- package/lib/src/config.js +50 -5
- package/lib/src/dashboard/commands.js +185 -141
- package/lib/src/dashboard/event-manager.js +39 -6
- package/lib/src/data-service/appleFamily.js +43 -0
- package/lib/src/data-service/db.js +11 -0
- package/lib/src/data-service/device-service.js +6 -1
- package/lib/src/data-service/device-store.js +62 -2
- package/lib/src/data-service/deviceClaims.js +37 -0
- package/lib/src/data-service/deviceFieldOwners.js +25 -11
- package/lib/src/data-service/deviceSettings.js +79 -0
- package/lib/src/data-service/prisma-store.js +150 -8
- package/lib/src/data-service/web-config-service.js +43 -92
- package/lib/src/device-managers/AndroidDeviceManager.js +9 -31
- package/lib/src/device-managers/ios/WDAClient.js +44 -43
- package/lib/src/device-managers/shellCommands.js +96 -0
- package/lib/src/device-utils.js +96 -40
- package/lib/src/generated/client/edge.js +17 -3
- package/lib/src/generated/client/index-browser.js +14 -0
- package/lib/src/generated/client/index.d.ts +2054 -379
- package/lib/src/generated/client/index.js +17 -3
- package/lib/src/generated/client/package.json +1 -1
- package/lib/src/generated/client/schema.prisma +35 -7
- package/lib/src/generated/client/wasm.js +14 -0
- package/lib/src/interceptors/CommandInterceptor.js +29 -8
- package/lib/src/interceptors/xenonScripts.js +55 -0
- package/lib/src/interfaces/IPluginArgs.js +1 -2
- package/lib/src/plugin.js +15 -1
- package/lib/src/prisma.js +1 -1
- package/lib/src/scripts/database-check.js +124 -0
- package/lib/src/services/AIService.js +99 -0
- package/lib/src/services/CleanupService.js +6 -1
- package/lib/src/services/InterceptorService.js +70 -13
- package/lib/src/services/NetworkConditioningService.js +85 -88
- package/lib/src/services/NotificationService.js +36 -29
- package/lib/src/services/OrphanSweeper.js +5 -0
- package/lib/src/services/PortAllocator.js +5 -0
- package/lib/src/services/ServerManager.js +94 -8
- package/lib/src/services/SessionLifecycleService.js +47 -49
- package/lib/src/services/ShutdownCoordinator.js +33 -0
- package/lib/src/services/TeamService.js +20 -3
- package/lib/src/services/healing/HealingOrchestrator.js +32 -1
- package/lib/src/services/interceptor/AndroidProxyAdapter.js +18 -0
- package/lib/src/services/interceptor/interceptorOptions.js +44 -0
- package/lib/src/services/network/AndroidRadios.js +71 -0
- package/lib/src/services/network/PhoneNetworkLedger.js +184 -0
- package/lib/src/services/network/PhoneNetworkRestore.js +330 -0
- package/lib/src/services/network/adbForPhone.js +60 -0
- package/lib/src/services/network/phoneNetworkLock.js +20 -0
- package/lib/src/services/network/xenonProxy.js +107 -0
- package/lib/src/services/omni-vision/OmniVisionService.js +53 -43
- package/lib/src/services/omni-vision/VisionAssertionService.js +28 -25
- package/lib/src/services/omni-vision/ocrTextMatch.js +105 -0
- package/lib/src/services/omni-vision/screenScale.js +68 -0
- package/lib/src/services/recording/concurrency-gate.js +12 -3
- package/lib/src/services/settings/SelfHealingSwitch.js +70 -0
- package/lib/src/services/settings/labSettings.js +149 -0
- package/lib/src/services/webhookEvents.js +173 -0
- package/lib/test/helpers/fake-adb.js +58 -0
- package/lib/test/helpers/fake-device-table.js +63 -24
- package/lib/test/helpers/scratch-database.js +1 -0
- package/lib/test/unit/TeamService.test.js +3 -0
- package/lib/test/unit/XenonCapabilityManager.spec.js +4 -2
- package/lib/test/unit/apple-family.spec.js +36 -0
- package/lib/test/unit/cleanup-settings-db.spec.js +103 -0
- package/lib/test/unit/cleanup-settings.spec.js +244 -0
- package/lib/test/unit/dashboard-api-paths.spec.js +101 -0
- package/lib/test/unit/dashboard-commands.spec.js +249 -0
- package/lib/test/unit/dashboard-events-team-scope.spec.js +9 -6
- package/lib/test/unit/database-check.spec.js +108 -0
- package/lib/test/unit/default-plugin-args.spec.js +33 -0
- package/lib/test/unit/device-offline-webhook.spec.js +81 -0
- package/lib/test/unit/device-setting-columns.spec.js +66 -0
- package/lib/test/unit/device-settings-loki.spec.js +87 -0
- package/lib/test/unit/device-settings-survive.spec.js +555 -0
- package/lib/test/unit/device-sync-lost-update.spec.js +5 -0
- package/lib/test/unit/emulator-boot.spec.js +91 -0
- package/lib/test/unit/hub-node-gateway-appium-server.spec.js +48 -0
- package/lib/test/unit/hub-node-same-host.spec.js +5 -0
- package/lib/test/unit/iphone-ipad-only.spec.js +132 -0
- package/lib/test/unit/json-logging-option.spec.js +74 -0
- package/lib/test/unit/lab-settings.spec.js +218 -0
- package/lib/test/unit/max-sessions.spec.js +240 -0
- package/lib/test/unit/ocr-text-match.spec.js +44 -0
- package/lib/test/unit/omni-vision-analyze-screen.spec.js +72 -0
- package/lib/test/unit/omni-vision-ios-points.spec.js +161 -0
- package/lib/test/unit/omni-vision-tap-text.spec.js +129 -0
- package/lib/test/unit/omni-vision.spec.js +1 -1
- package/lib/test/unit/partial-config-file.spec.js +14 -0
- package/lib/test/unit/phone-network/boot-clean-up.spec.js +64 -0
- package/lib/test/unit/phone-network/interceptor-options.spec.js +77 -0
- package/lib/test/unit/phone-network/interceptor-proxy-restore.spec.js +145 -0
- package/lib/test/unit/phone-network/network-conditioning.spec.js +145 -0
- package/lib/test/unit/phone-network/phone-network-restore.spec.js +246 -0
- package/lib/test/unit/phone-network/session-end-paths.spec.js +182 -0
- package/lib/test/unit/phone-network/session-start.spec.js +110 -0
- package/lib/test/unit/phone-network/xenon-proxy.spec.js +69 -0
- package/lib/test/unit/plugin.spec.js +3 -3
- package/lib/test/unit/recording-options.spec.js +181 -0
- package/lib/test/unit/self-healing-boot.spec.js +75 -0
- package/lib/test/unit/self-healing-setting.spec.js +272 -0
- package/lib/test/unit/session-failed-webhook.spec.js +170 -0
- package/lib/test/unit/shell-commands.spec.js +133 -0
- package/lib/test/unit/socketServer.scopedEmit.spec.js +2 -0
- package/lib/test/unit/stream-start-pipelines.spec.js +210 -0
- package/lib/test/unit/stream-status-type.spec.js +4 -0
- package/lib/test/unit/vision-assertion.spec.js +118 -0
- package/lib/test/unit/wda-client-shell.spec.js +133 -0
- package/lib/test/unit/web-config-service.spec.js +79 -0
- package/lib/test/unit/webhook-delivery.spec.js +24 -0
- package/lib/test/unit/webhook-payloads.spec.js +273 -0
- package/lib/test/unit/xenon-execute-scripts.spec.js +138 -0
- package/lib/tsconfig.tsbuildinfo +1 -1
- package/lib/web/src/components/webhook-settings/webhookEventVariables.json +30 -0
- package/package.json +1 -1
- package/prisma/migrations/20261004140000_keep_device_settings/migration.sql +19 -0
- package/prisma/schema.prisma +35 -7
- package/schema.json +19 -21
- package/scripts/generate-types-from-schema.js +1 -2
- package/scripts/lib/always-present-plugin-args.js +4 -1
- package/lib/public/assets/DeviceMosaicView-8J0oYqMM.js +0 -2
- package/lib/public/assets/device-explorer-C-KnhMHI.js +0 -279
- package/lib/public/assets/maintenance-settings-zDVlOMVK.js +0 -6
- package/lib/public/assets/recording-group-store-WVM3hoXn.js +0 -26
- package/lib/public/assets/session-detail-page-CGn55Pvl.js +0 -4
- package/lib/public/assets/settings-MsVU2ukE.js +0 -6
- package/lib/public/assets/webhook-settings-DiS8geIm.js +0 -1
|
@@ -0,0 +1,107 @@
|
|
|
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.isUnsetProxy = isUnsetProxy;
|
|
16
|
+
exports.parseProxySetting = parseProxySetting;
|
|
17
|
+
exports.localIPv4Addresses = localIPv4Addresses;
|
|
18
|
+
exports.pointsAtThisMachine = pointsAtThisMachine;
|
|
19
|
+
exports.isPortListening = isPortListening;
|
|
20
|
+
exports.isDeadCaptureProxy = isDeadCaptureProxy;
|
|
21
|
+
const net_1 = __importDefault(require("net"));
|
|
22
|
+
const os_1 = __importDefault(require("os"));
|
|
23
|
+
/**
|
|
24
|
+
* Reading a phone's global HTTP proxy (`settings get global http_proxy`), and
|
|
25
|
+
* telling one Xenon's network capture left behind from one somebody set on
|
|
26
|
+
* purpose.
|
|
27
|
+
*/
|
|
28
|
+
/** Android's ways of saying "no proxy": unset (`null`), empty, or `:0`. */
|
|
29
|
+
function isUnsetProxy(value) {
|
|
30
|
+
const v = (value !== null && value !== void 0 ? value : '').trim();
|
|
31
|
+
return v === '' || v === 'null' || v === ':0';
|
|
32
|
+
}
|
|
33
|
+
/** `host:port`, or null for no proxy or anything else. */
|
|
34
|
+
function parseProxySetting(value) {
|
|
35
|
+
if (isUnsetProxy(value))
|
|
36
|
+
return null;
|
|
37
|
+
const v = value.trim();
|
|
38
|
+
const colon = v.lastIndexOf(':');
|
|
39
|
+
if (colon <= 0)
|
|
40
|
+
return null;
|
|
41
|
+
const host = v.slice(0, colon);
|
|
42
|
+
const portText = v.slice(colon + 1);
|
|
43
|
+
if (!/^\d+$/.test(portText))
|
|
44
|
+
return null;
|
|
45
|
+
const port = Number(portText);
|
|
46
|
+
if (port < 1 || port > 65535)
|
|
47
|
+
return null;
|
|
48
|
+
return { host, port };
|
|
49
|
+
}
|
|
50
|
+
/** This machine's own IPv4 addresses, as a phone on its network reaches it. */
|
|
51
|
+
function localIPv4Addresses() {
|
|
52
|
+
const out = [];
|
|
53
|
+
for (const infos of Object.values(os_1.default.networkInterfaces())) {
|
|
54
|
+
for (const info of infos !== null && infos !== void 0 ? infos : []) {
|
|
55
|
+
if (info.family === 'IPv4' && !info.internal)
|
|
56
|
+
out.push(info.address);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return out;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Whether a phone that uses `host` as its proxy reaches this machine: through
|
|
63
|
+
* `adb reverse` (127.0.0.1, the phone itself), an emulator's alias for its
|
|
64
|
+
* host (10.0.2.2), or this machine's own address (the interceptor's LAN
|
|
65
|
+
* fallback). These are the only hosts the interceptor ever sets.
|
|
66
|
+
*/
|
|
67
|
+
function pointsAtThisMachine(host, ownAddresses) {
|
|
68
|
+
const h = host.trim().toLowerCase();
|
|
69
|
+
return h === '127.0.0.1' || h === 'localhost' || h === '10.0.2.2' || ownAddresses.includes(h);
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Whether something on this machine accepts a TCP connection on `port`.
|
|
73
|
+
* Anything but a refusal counts as yes (a timeout too): only a port known to be
|
|
74
|
+
* dead may have a proxy that points at it cleared.
|
|
75
|
+
*/
|
|
76
|
+
function isPortListening(port, timeoutMs = 1000) {
|
|
77
|
+
return new Promise((resolve) => {
|
|
78
|
+
const socket = net_1.default.connect({ host: '127.0.0.1', port });
|
|
79
|
+
const done = (answer) => {
|
|
80
|
+
socket.removeAllListeners();
|
|
81
|
+
socket.destroy();
|
|
82
|
+
resolve(answer);
|
|
83
|
+
};
|
|
84
|
+
socket.setTimeout(timeoutMs, () => done(true));
|
|
85
|
+
socket.once('connect', () => done(true));
|
|
86
|
+
socket.once('error', (err) => done(err.code !== 'ECONNREFUSED'));
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Whether `value` is a proxy Xenon's network capture set and nobody serves any
|
|
91
|
+
* more: it points at this machine, on one of this server's capture ports, and
|
|
92
|
+
* nothing answers there. A phone with such a proxy has no network at all. A
|
|
93
|
+
* proxy that points anywhere else, or at a port that answers (a capture that
|
|
94
|
+
* is running, maybe another Xenon's on this machine), is never one.
|
|
95
|
+
*/
|
|
96
|
+
function isDeadCaptureProxy(value_1, captureRange_1) {
|
|
97
|
+
return __awaiter(this, arguments, void 0, function* (value, captureRange, probe = isPortListening) {
|
|
98
|
+
const proxy = parseProxySetting(value);
|
|
99
|
+
if (!proxy)
|
|
100
|
+
return false;
|
|
101
|
+
if (!pointsAtThisMachine(proxy.host, localIPv4Addresses()))
|
|
102
|
+
return false;
|
|
103
|
+
if (proxy.port < captureRange[0] || proxy.port > captureRange[1])
|
|
104
|
+
return false;
|
|
105
|
+
return !(yield probe(proxy.port));
|
|
106
|
+
});
|
|
107
|
+
}
|
|
@@ -59,6 +59,8 @@ const sharp_1 = __importDefault(require("sharp"));
|
|
|
59
59
|
const AIService_1 = require("../AIService");
|
|
60
60
|
const logger_1 = __importDefault(require("../../logger"));
|
|
61
61
|
const crypto_1 = __importDefault(require("crypto"));
|
|
62
|
+
const ocrTextMatch_1 = require("./ocrTextMatch");
|
|
63
|
+
const screenScale_1 = require("./screenScale");
|
|
62
64
|
// Mobile UIs have sparse text on complex backgrounds - reduce contrast to preserve readability
|
|
63
65
|
const DEFAULT_CONTRAST = 1.2; // Slightly above 1.0 for mild enhancement (was 0.8 which was too aggressive)
|
|
64
66
|
const UPSCALE_THRESHOLD = 800; // Only upscale very small images (was 1500)
|
|
@@ -379,10 +381,17 @@ let OmniVisionService = OmniVisionService_1 = class OmniVisionService {
|
|
|
379
381
|
}
|
|
380
382
|
});
|
|
381
383
|
}
|
|
384
|
+
/** The OCR words with usable boxes, in the screenshot's pixels. */
|
|
385
|
+
wordBoxes(words) {
|
|
386
|
+
return words.map((w) => this.normalizeWordBBox(w)).filter(Boolean);
|
|
387
|
+
}
|
|
382
388
|
/**
|
|
383
|
-
* Proactive OCR Search: Finds elements matching text even if not in XML
|
|
384
|
-
|
|
385
|
-
|
|
389
|
+
* Proactive OCR Search: Finds elements matching text even if not in XML.
|
|
390
|
+
*
|
|
391
|
+
* The text may be several words ("Sign in"); see ocrTextMatch.ts. Each
|
|
392
|
+
* element's rect is in the driver's coordinates (points on iOS), like a
|
|
393
|
+
* real element's, so tapping it lands on the text.
|
|
394
|
+
*
|
|
386
395
|
* `throwOnError`: fail instead of answering "no match" when the screenshot
|
|
387
396
|
* or OCR fails. Device control's "Test locator" asks for it; an Appium
|
|
388
397
|
* findElement doesn't, and turns the empty list into NoSuchElement.
|
|
@@ -398,17 +407,15 @@ let OmniVisionService = OmniVisionService_1 = class OmniVisionService {
|
|
|
398
407
|
this.logger.warn(`OCR proactive search yielded no words. Text content: "${ocrText.trim().substring(0, 100)}..."`);
|
|
399
408
|
return [];
|
|
400
409
|
}
|
|
401
|
-
const matches =
|
|
410
|
+
const matches = (0, ocrTextMatch_1.findText)(this.wordBoxes(words), String(text !== null && text !== void 0 ? text : '')).filter((m) => m.confidence > 60);
|
|
411
|
+
if (matches.length === 0)
|
|
412
|
+
return [];
|
|
413
|
+
const scale = yield (0, screenScale_1.screenScaleOf)(driver, screenshot);
|
|
402
414
|
return matches.map((m) => {
|
|
403
415
|
const el = {
|
|
404
416
|
id: `omni_ocr_${Date.now()}_${Math.random().toString(36).substr(2, 5)}`,
|
|
405
417
|
text: m.text,
|
|
406
|
-
rect: {
|
|
407
|
-
x: m.bbox.x0,
|
|
408
|
-
y: m.bbox.y0,
|
|
409
|
-
width: m.bbox.x1 - m.bbox.x0,
|
|
410
|
-
height: m.bbox.y1 - m.bbox.y0,
|
|
411
|
-
},
|
|
418
|
+
rect: (0, screenScale_1.toDriverRect)({ x: m.x0, y: m.y0, width: m.x1 - m.x0, height: m.y1 - m.y0 }, scale),
|
|
412
419
|
confidence: m.confidence / 100,
|
|
413
420
|
};
|
|
414
421
|
this.virtualElementStore.set(el.id, el);
|
|
@@ -433,14 +440,12 @@ let OmniVisionService = OmniVisionService_1 = class OmniVisionService {
|
|
|
433
440
|
const screenshot = yield driver.getScreenshot();
|
|
434
441
|
const coordinates = yield AIService_1.AI_SERVICE.visualFind(screenshot, iconDescription, opts);
|
|
435
442
|
if (coordinates) {
|
|
443
|
+
// The AI answers in the screenshot's pixels; the rect is the driver's
|
|
444
|
+
// (points on iOS), like a real element's.
|
|
445
|
+
const scale = yield (0, screenScale_1.screenScaleOf)(driver, screenshot);
|
|
436
446
|
const el = {
|
|
437
447
|
id: `omni_ai_${Date.now()}`,
|
|
438
|
-
rect: {
|
|
439
|
-
x: coordinates.x - 20,
|
|
440
|
-
y: coordinates.y - 20,
|
|
441
|
-
width: 40,
|
|
442
|
-
height: 40,
|
|
443
|
-
},
|
|
448
|
+
rect: (0, screenScale_1.toDriverRect)({ x: coordinates.x - 20, y: coordinates.y - 20, width: 40, height: 40 }, scale),
|
|
444
449
|
confidence: 0.85,
|
|
445
450
|
};
|
|
446
451
|
this.virtualElementStore.set(el.id, el);
|
|
@@ -460,6 +465,7 @@ let OmniVisionService = OmniVisionService_1 = class OmniVisionService {
|
|
|
460
465
|
*/
|
|
461
466
|
analyzeScreen(driver) {
|
|
462
467
|
return __awaiter(this, void 0, void 0, function* () {
|
|
468
|
+
var _a;
|
|
463
469
|
this.logger.info('Performing full screen analysis...');
|
|
464
470
|
try {
|
|
465
471
|
const screenshot = yield driver.getScreenshot();
|
|
@@ -470,32 +476,28 @@ let OmniVisionService = OmniVisionService_1 = class OmniVisionService {
|
|
|
470
476
|
this.validateBuffer(buffer, 'Screen Analysis');
|
|
471
477
|
// 1. Get all text via OCR using the robust worker flow
|
|
472
478
|
const { text: ocrText, words } = yield this.performOcr(buffer);
|
|
473
|
-
// 2. Ask AI
|
|
474
|
-
|
|
479
|
+
// 2. Ask the AI provider about the screen, with the screenshot. Its
|
|
480
|
+
// failure leaves the OCR answer standing, and says why there are no
|
|
481
|
+
// insights. Through 2.13.2 this sent the screenshot's base64 as a file
|
|
482
|
+
// path, so the provider got no image and a "why did this test fail"
|
|
483
|
+
// prompt, and answered about no screen at all.
|
|
484
|
+
let aiInsights = null;
|
|
485
|
+
let aiError;
|
|
475
486
|
try {
|
|
476
|
-
|
|
477
|
-
sessionId: driver.sessionId,
|
|
478
|
-
failureReason: 'Screen Analysis Request',
|
|
479
|
-
commandLogs: [],
|
|
480
|
-
deviceLogs: [],
|
|
481
|
-
screenshotPath: screenshot, // In a real scenario, we'd save this to a file first or pass base64
|
|
482
|
-
});
|
|
487
|
+
aiInsights = yield AIService_1.AI_SERVICE.describeScreen(screenshot);
|
|
483
488
|
}
|
|
484
489
|
catch (aiErr) {
|
|
485
|
-
|
|
490
|
+
aiError = (_a = aiErr === null || aiErr === void 0 ? void 0 : aiErr.message) !== null && _a !== void 0 ? _a : String(aiErr);
|
|
491
|
+
this.logger.warn(`AI insights skipped: ${aiError}`);
|
|
486
492
|
}
|
|
487
|
-
return {
|
|
488
|
-
timestamp: new Date().toISOString(),
|
|
489
|
-
ocr: {
|
|
493
|
+
return Object.assign({ timestamp: new Date().toISOString(), ocr: {
|
|
490
494
|
text: ocrText,
|
|
491
495
|
words: words.map((w) => ({
|
|
492
496
|
text: w.text || '',
|
|
493
497
|
confidence: w.confidence || 0,
|
|
494
498
|
bbox: w.bbox || { x0: 0, y0: 0, x1: 0, y1: 0 },
|
|
495
499
|
})),
|
|
496
|
-
},
|
|
497
|
-
ai_insights: aiAnalysis,
|
|
498
|
-
};
|
|
500
|
+
}, ai_insights: aiInsights }, (aiError !== undefined ? { ai_insights_error: aiError } : {}));
|
|
499
501
|
}
|
|
500
502
|
catch (err) {
|
|
501
503
|
this.logger.error(`Screen analysis failed: ${err.message}`);
|
|
@@ -511,6 +513,11 @@ let OmniVisionService = OmniVisionService_1 = class OmniVisionService {
|
|
|
511
513
|
}
|
|
512
514
|
/**
|
|
513
515
|
* Omni-Click: Find a target by OCR text and click its center using W3C actions.
|
|
516
|
+
*
|
|
517
|
+
* The text may be several words ("Sign in"); see ocrTextMatch.ts. The tap
|
|
518
|
+
* and `target` are in the driver's coordinates (points on iOS). A failed
|
|
519
|
+
* screenshot or OCR, or a screen size that can't be read on iOS, fails;
|
|
520
|
+
* `clicked: false` means the text was looked for and not found.
|
|
514
521
|
*/
|
|
515
522
|
omniClickByText(driver, req) {
|
|
516
523
|
return __awaiter(this, void 0, void 0, function* () {
|
|
@@ -533,22 +540,21 @@ let OmniVisionService = OmniVisionService_1 = class OmniVisionService {
|
|
|
533
540
|
const screenshot = yield driver.getScreenshot();
|
|
534
541
|
const buffer = Buffer.from(screenshot, 'base64');
|
|
535
542
|
const { words } = yield this.performOcr(buffer);
|
|
536
|
-
const
|
|
537
|
-
const matches = normalized
|
|
538
|
-
.filter((w) => w.text.toLowerCase().includes(text.toLowerCase()))
|
|
539
|
-
.sort((a, b) => (b.confidence || 0) - (a.confidence || 0) || a.y0 - b.y0 || a.x0 - b.x0);
|
|
543
|
+
const matches = (0, ocrTextMatch_1.findText)(this.wordBoxes(words), text).sort((a, b) => (b.confidence || 0) - (a.confidence || 0) || a.y0 - b.y0 || a.x0 - b.x0);
|
|
540
544
|
if (matches.length === 0) {
|
|
541
545
|
return { clicked: false, message: `No OCR match found for text "${text}"` };
|
|
542
546
|
}
|
|
543
547
|
const pick = matches[Math.min(index - 1, matches.length - 1)];
|
|
548
|
+
const scale = yield (0, screenScale_1.screenScaleOf)(driver, screenshot);
|
|
549
|
+
const found = (0, screenScale_1.toDriverRect)({ x: pick.x0, y: pick.y0, width: pick.x1 - pick.x0, height: pick.y1 - pick.y0 }, scale);
|
|
544
550
|
const rect = {
|
|
545
|
-
x: Math.max(0, Math.round(
|
|
546
|
-
y: Math.max(0, Math.round(
|
|
547
|
-
width: Math.max(1, Math.round(
|
|
548
|
-
height: Math.max(1, Math.round(
|
|
551
|
+
x: Math.max(0, Math.round(found.x)),
|
|
552
|
+
y: Math.max(0, Math.round(found.y)),
|
|
553
|
+
width: Math.max(1, Math.round(found.width)),
|
|
554
|
+
height: Math.max(1, Math.round(found.height)),
|
|
549
555
|
};
|
|
550
|
-
const cx = Math.round(
|
|
551
|
-
const cy = Math.round(
|
|
556
|
+
const cx = Math.round(found.x + found.width / 2);
|
|
557
|
+
const cy = Math.round(found.y + found.height / 2);
|
|
552
558
|
// Click via touch actions
|
|
553
559
|
yield driver.performActions([
|
|
554
560
|
{
|
|
@@ -583,6 +589,10 @@ let OmniVisionService = OmniVisionService_1 = class OmniVisionService {
|
|
|
583
589
|
}
|
|
584
590
|
/**
|
|
585
591
|
* Omni-Click by Icon: Find a target by visual description and click its center.
|
|
592
|
+
*
|
|
593
|
+
* Fails when it can't look (no AI provider, a failed screenshot or AI call);
|
|
594
|
+
* `clicked: false` means the AI looked and found nothing. Through 2.13.2 every
|
|
595
|
+
* one of those answered `clicked: false`, "No visual match found".
|
|
586
596
|
*/
|
|
587
597
|
omniClickByIcon(driver, req) {
|
|
588
598
|
return __awaiter(this, void 0, void 0, function* () {
|
|
@@ -596,7 +606,7 @@ let OmniVisionService = OmniVisionService_1 = class OmniVisionService {
|
|
|
596
606
|
};
|
|
597
607
|
}
|
|
598
608
|
this.logger.info(`Omni-Click: searching for icon "${icon}" via AI...`);
|
|
599
|
-
const element = yield this.findByIcon(driver, icon);
|
|
609
|
+
const element = yield this.findByIcon(driver, icon, { throwOnError: true });
|
|
600
610
|
if (!element) {
|
|
601
611
|
return { clicked: false, message: `No visual match found for icon "${icon}"` };
|
|
602
612
|
}
|
|
@@ -20,6 +20,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
20
20
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
21
21
|
exports.VisionAssertionService = void 0;
|
|
22
22
|
const typedi_1 = require("typedi");
|
|
23
|
+
const base_driver_1 = require("@appium/base-driver");
|
|
23
24
|
const AIService_1 = require("../AIService");
|
|
24
25
|
const logger_1 = __importDefault(require("../../logger"));
|
|
25
26
|
let VisionAssertionService = class VisionAssertionService {
|
|
@@ -27,38 +28,40 @@ let VisionAssertionService = class VisionAssertionService {
|
|
|
27
28
|
this.logger = logger_1.default.scope('VisionAssertion');
|
|
28
29
|
}
|
|
29
30
|
/**
|
|
30
|
-
*
|
|
31
|
+
* `xenon: assertVisualState`: whether `instruction`, a condition in plain
|
|
32
|
+
* words, holds on the device's screen now, as the AI provider judges it from
|
|
33
|
+
* a screenshot.
|
|
34
|
+
*
|
|
35
|
+
* Answers `{ result, message }` only with the provider's own verdict.
|
|
36
|
+
* Whenever there is none it fails, saying the condition was not checked: no
|
|
37
|
+
* condition, no screenshot, no AI provider, a failed or rate-limited call,
|
|
38
|
+
* or an answer that isn't true or false. It never answers `false` for "could
|
|
39
|
+
* not check": a test asserting that something is absent would pass. Through
|
|
40
|
+
* 2.13.2 it answered `{ result: true, message: 'Assertion placeholder' }` in
|
|
41
|
+
* all of those cases.
|
|
31
42
|
*/
|
|
32
43
|
assertState(driver, instruction) {
|
|
33
44
|
return __awaiter(this, void 0, void 0, function* () {
|
|
34
|
-
|
|
45
|
+
var _a;
|
|
46
|
+
const condition = typeof instruction === 'string' ? instruction.trim() : '';
|
|
47
|
+
if (!condition) {
|
|
48
|
+
throw new base_driver_1.errors.InvalidArgumentError('assertVisualState needs a condition to check, as a string or { instruction }, ' +
|
|
49
|
+
"for example 'The cart is empty'.");
|
|
50
|
+
}
|
|
51
|
+
this.logger.info(`Asserting visual state: "${condition}"`);
|
|
52
|
+
let screenshot;
|
|
35
53
|
try {
|
|
36
|
-
|
|
37
|
-
const prompt = `
|
|
38
|
-
You are a visual testing quality agent.
|
|
39
|
-
Your task is to verify if the following condition is true based on the provided screenshot.
|
|
40
|
-
|
|
41
|
-
Condition: "${instruction}"
|
|
42
|
-
|
|
43
|
-
Response Format: JSON only, strictly { "result": boolean, "message": "detailed explanation of what you see" }.
|
|
44
|
-
`;
|
|
45
|
-
const response = yield AIService_1.AI_SERVICE.visualFind(screenshot, prompt);
|
|
46
|
-
// Note: We use visualFind which calls analyze. We expect a JSON response.
|
|
47
|
-
// Since analyze returns raw text, we attempt to parse it.
|
|
48
|
-
if (response && response.result !== undefined) {
|
|
49
|
-
return {
|
|
50
|
-
result: response.result,
|
|
51
|
-
message: response.message || 'Assertion evaluated successfully.',
|
|
52
|
-
};
|
|
53
|
-
}
|
|
54
|
-
// Fallback: If AI_SERVICE.visualFind was used literally, it might not return what we expect.
|
|
55
|
-
// Let's assume a more direct path if needed, but for now we'll stick to this pattern.
|
|
56
|
-
return { result: true, message: 'Assertion placeholder' };
|
|
54
|
+
screenshot = yield driver.getScreenshot();
|
|
57
55
|
}
|
|
58
56
|
catch (err) {
|
|
59
|
-
|
|
60
|
-
|
|
57
|
+
throw new Error(`No screenshot could be taken, so the condition was not checked: ${(_a = err === null || err === void 0 ? void 0 : err.message) !== null && _a !== void 0 ? _a : err}`);
|
|
58
|
+
}
|
|
59
|
+
if (!screenshot) {
|
|
60
|
+
throw new Error('No screenshot could be taken, so the condition was not checked.');
|
|
61
61
|
}
|
|
62
|
+
const verdict = yield AIService_1.AI_SERVICE.assertVisual(screenshot, condition);
|
|
63
|
+
this.logger.info(`"${condition}": ${verdict.result} (${verdict.message})`);
|
|
64
|
+
return verdict;
|
|
62
65
|
});
|
|
63
66
|
}
|
|
64
67
|
};
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Finding a text, one word or several, among the words OCR read off a
|
|
4
|
+
* screenshot. Tesseract gives one box per word, so a text with a space in it
|
|
5
|
+
* ("Sign in") is looked for across neighbouring words on one line, and its
|
|
6
|
+
* box is the box around the words it covers.
|
|
7
|
+
*
|
|
8
|
+
* Pure: boxes in, boxes out, in the screenshot's pixels.
|
|
9
|
+
*/
|
|
10
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
11
|
+
exports.wordRuns = wordRuns;
|
|
12
|
+
exports.findText = findText;
|
|
13
|
+
/**
|
|
14
|
+
* Two words further apart than this many times their height are not
|
|
15
|
+
* neighbours: a space is about a third of a word's height, and a gap this
|
|
16
|
+
* wide is another column, such as "Cancel" and "Done" at either end of a bar.
|
|
17
|
+
*/
|
|
18
|
+
const MAX_GAP_IN_HEIGHTS = 1.5;
|
|
19
|
+
const normalize = (text) => text.toLowerCase().replace(/\s+/g, ' ').trim();
|
|
20
|
+
const heightOf = (w) => Math.max(1, w.y1 - w.y0);
|
|
21
|
+
/** Whether a word sits on a line: their heights overlap by at least half the shorter one. */
|
|
22
|
+
function onLine(line, word) {
|
|
23
|
+
const overlap = Math.min(line.y1, word.y1) - Math.max(line.y0, word.y0);
|
|
24
|
+
return overlap > 0 && overlap >= Math.min(heightOf(line), heightOf(word)) / 2;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* The words in runs of neighbours: on one line, left to right, with no wide
|
|
28
|
+
* gap between them. Runs are in reading order.
|
|
29
|
+
*/
|
|
30
|
+
function wordRuns(words) {
|
|
31
|
+
const lines = [];
|
|
32
|
+
for (const word of [...words].sort((a, b) => a.y0 - b.y0 || a.x0 - b.x0)) {
|
|
33
|
+
const line = lines.find((l) => onLine(l, word));
|
|
34
|
+
if (line) {
|
|
35
|
+
line.words.push(word);
|
|
36
|
+
line.y0 = Math.min(line.y0, word.y0);
|
|
37
|
+
line.y1 = Math.max(line.y1, word.y1);
|
|
38
|
+
}
|
|
39
|
+
else {
|
|
40
|
+
lines.push({ y0: word.y0, y1: word.y1, words: [word] });
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
const runs = [];
|
|
44
|
+
for (const line of lines.sort((a, b) => a.y0 - b.y0)) {
|
|
45
|
+
let run = [];
|
|
46
|
+
for (const word of line.words.sort((a, b) => a.x0 - b.x0)) {
|
|
47
|
+
const previous = run[run.length - 1];
|
|
48
|
+
const gap = previous ? word.x0 - previous.x1 : 0;
|
|
49
|
+
if (previous && gap > MAX_GAP_IN_HEIGHTS * Math.max(heightOf(previous), heightOf(word))) {
|
|
50
|
+
runs.push(run);
|
|
51
|
+
run = [];
|
|
52
|
+
}
|
|
53
|
+
run.push(word);
|
|
54
|
+
}
|
|
55
|
+
if (run.length)
|
|
56
|
+
runs.push(run);
|
|
57
|
+
}
|
|
58
|
+
return runs;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Every place `text` appears, ignoring case and runs of spaces, within a word
|
|
62
|
+
* or across neighbouring words: "Sign in" in "Sign" "in", "password" in
|
|
63
|
+
* "password?". Each match covers the words it touches: its box surrounds
|
|
64
|
+
* them, its text is theirs joined with spaces, and its confidence is the
|
|
65
|
+
* lowest of theirs. A one-word text matches inside one word, as it always
|
|
66
|
+
* did. Matches are in reading order.
|
|
67
|
+
*/
|
|
68
|
+
function findText(words, text) {
|
|
69
|
+
const wanted = normalize(text);
|
|
70
|
+
if (!wanted)
|
|
71
|
+
return [];
|
|
72
|
+
const matches = [];
|
|
73
|
+
for (const run of wordRuns(words)) {
|
|
74
|
+
// The run as one string, remembering where each word sits in it.
|
|
75
|
+
let line = '';
|
|
76
|
+
const spans = [];
|
|
77
|
+
for (const word of run) {
|
|
78
|
+
if (line)
|
|
79
|
+
line += ' ';
|
|
80
|
+
const start = line.length;
|
|
81
|
+
line += normalize(word.text);
|
|
82
|
+
spans.push({ start, end: line.length });
|
|
83
|
+
}
|
|
84
|
+
const seen = new Set();
|
|
85
|
+
for (let at = line.indexOf(wanted); at !== -1; at = line.indexOf(wanted, at + 1)) {
|
|
86
|
+
const end = at + wanted.length;
|
|
87
|
+
const first = spans.findIndex((s) => s.end > at);
|
|
88
|
+
const last = spans.findIndex((s) => s.end >= end);
|
|
89
|
+
const key = `${first}-${last}`;
|
|
90
|
+
if (first === -1 || last === -1 || seen.has(key))
|
|
91
|
+
continue;
|
|
92
|
+
seen.add(key);
|
|
93
|
+
const covered = run.slice(first, last + 1);
|
|
94
|
+
matches.push({
|
|
95
|
+
x0: Math.min(...covered.map((w) => w.x0)),
|
|
96
|
+
y0: Math.min(...covered.map((w) => w.y0)),
|
|
97
|
+
x1: Math.max(...covered.map((w) => w.x1)),
|
|
98
|
+
y1: Math.max(...covered.map((w) => w.y1)),
|
|
99
|
+
text: covered.map((w) => w.text).join(' '),
|
|
100
|
+
confidence: Math.min(...covered.map((w) => w.confidence)),
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return matches;
|
|
105
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
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.SAME_SCALE = void 0;
|
|
16
|
+
exports.driverUsesPoints = driverUsesPoints;
|
|
17
|
+
exports.screenScaleOf = screenScaleOf;
|
|
18
|
+
exports.toDriverRect = toDriverRect;
|
|
19
|
+
const sharp_1 = __importDefault(require("sharp"));
|
|
20
|
+
exports.SAME_SCALE = { x: 1, y: 1 };
|
|
21
|
+
/**
|
|
22
|
+
* Whether the driver's coordinates are points rather than the screenshot's
|
|
23
|
+
* pixels. A driver Xenon can't place (device control's screenshot-only
|
|
24
|
+
* stand-in, which never taps) keeps pixels.
|
|
25
|
+
*/
|
|
26
|
+
function driverUsesPoints(driver) {
|
|
27
|
+
var _a, _b, _c, _d;
|
|
28
|
+
const platform = String((_d = (_b = (_a = driver === null || driver === void 0 ? void 0 : driver.caps) === null || _a === void 0 ? void 0 : _a.platformName) !== null && _b !== void 0 ? _b : (_c = driver === null || driver === void 0 ? void 0 : driver.opts) === null || _c === void 0 ? void 0 : _c.platformName) !== null && _d !== void 0 ? _d : '');
|
|
29
|
+
return ['ios', 'tvos'].includes(platform.toLowerCase());
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* The scale between `screenshotBase64` and the driver's coordinates:
|
|
33
|
+
* 1 unless the driver uses points, then the screenshot's size over the
|
|
34
|
+
* window's. Throws when that can't be worked out on a driver that uses
|
|
35
|
+
* points, since a tap in pixels there lands in the wrong place.
|
|
36
|
+
*/
|
|
37
|
+
function screenScaleOf(driver, screenshotBase64) {
|
|
38
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
39
|
+
if (!driverUsesPoints(driver))
|
|
40
|
+
return exports.SAME_SCALE;
|
|
41
|
+
const image = yield (0, sharp_1.default)(Buffer.from(screenshotBase64, 'base64'))
|
|
42
|
+
.metadata()
|
|
43
|
+
.catch((err) => {
|
|
44
|
+
throw new Error(`The screenshot's size could not be read: ${err.message}`);
|
|
45
|
+
});
|
|
46
|
+
const window = typeof driver.getWindowRect === 'function'
|
|
47
|
+
? yield driver.getWindowRect()
|
|
48
|
+
: yield driver.getWindowSize();
|
|
49
|
+
const x = Number(image.width) / Number(window === null || window === void 0 ? void 0 : window.width);
|
|
50
|
+
const y = Number(image.height) / Number(window === null || window === void 0 ? void 0 : window.height);
|
|
51
|
+
if (!(Number.isFinite(x) && x > 0 && Number.isFinite(y) && y > 0)) {
|
|
52
|
+
throw new Error(`The screen size could not be worked out (screenshot ${image.width}x${image.height}, ` +
|
|
53
|
+
`window ${window === null || window === void 0 ? void 0 : window.width}x${window === null || window === void 0 ? void 0 : window.height}).`);
|
|
54
|
+
}
|
|
55
|
+
return { x, y };
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
/** A rect found in the screenshot, in the driver's coordinates. */
|
|
59
|
+
function toDriverRect(rect, scale) {
|
|
60
|
+
if (scale.x === 1 && scale.y === 1)
|
|
61
|
+
return rect;
|
|
62
|
+
return {
|
|
63
|
+
x: rect.x / scale.x,
|
|
64
|
+
y: rect.y / scale.y,
|
|
65
|
+
width: rect.width / scale.x,
|
|
66
|
+
height: rect.height / scale.y,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
@@ -18,10 +18,19 @@ const config_1 = require("../../config");
|
|
|
18
18
|
* and are NOT counted against this gate — they bypass it entirely.
|
|
19
19
|
*/
|
|
20
20
|
let ConcurrencyGate = class ConcurrencyGate {
|
|
21
|
-
|
|
22
|
-
|
|
21
|
+
/**
|
|
22
|
+
* An explicit `limit` is fixed. Without one the gate follows
|
|
23
|
+
* `config.maxConcurrentRecordings` as it stands at each admission, not as it
|
|
24
|
+
* stood when the gate was built: the gate is created on first use, and the
|
|
25
|
+
* `maxConcurrentRecordings` option is applied at boot.
|
|
26
|
+
*/
|
|
27
|
+
constructor(fixedLimit) {
|
|
28
|
+
this.fixedLimit = fixedLimit;
|
|
23
29
|
this.active = new Set();
|
|
24
|
-
|
|
30
|
+
}
|
|
31
|
+
get limit() {
|
|
32
|
+
var _a, _b;
|
|
33
|
+
return (_b = (_a = this.fixedLimit) !== null && _a !== void 0 ? _a : config_1.config.maxConcurrentRecordings) !== null && _b !== void 0 ? _b : 4;
|
|
25
34
|
}
|
|
26
35
|
/**
|
|
27
36
|
* Atomic admission: either ALL recordingIds are admitted, or none are.
|
|
@@ -0,0 +1,70 @@
|
|
|
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 __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
9
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
10
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
11
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
12
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
13
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
14
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
15
|
+
});
|
|
16
|
+
};
|
|
17
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
18
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
19
|
+
};
|
|
20
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
21
|
+
exports.SelfHealingSwitch = void 0;
|
|
22
|
+
const typedi_1 = require("typedi");
|
|
23
|
+
const logger_1 = __importDefault(require("../../logger"));
|
|
24
|
+
const web_config_service_1 = require("../../data-service/web-config-service");
|
|
25
|
+
const labSettings_1 = require("./labSettings");
|
|
26
|
+
/**
|
|
27
|
+
* Whether self-healing is on, for the command interceptor, which asks at every
|
|
28
|
+
* command and so must never read the database.
|
|
29
|
+
*
|
|
30
|
+
* The value saved on the Settings page (the `WebConfig` row `enableSelfHealing`)
|
|
31
|
+
* is kept here, loaded once at boot and replaced when `POST /config` saves it.
|
|
32
|
+
* The rule is the one every lab setting follows (`selfHealingEnabled`): the
|
|
33
|
+
* saved value, else the plugin option the server started with, else on. The
|
|
34
|
+
* interceptor passes the options it was given, so the startup half is always
|
|
35
|
+
* the live one.
|
|
36
|
+
*
|
|
37
|
+
* Through 2.13 the toggle was stored nowhere and the interceptor read the
|
|
38
|
+
* startup options alone, so switching healing off in the dashboard did nothing.
|
|
39
|
+
*/
|
|
40
|
+
let SelfHealingSwitch = class SelfHealingSwitch {
|
|
41
|
+
/** Whether a failed findElement goes on to the healing tiers, now. */
|
|
42
|
+
isEnabled(startup) {
|
|
43
|
+
return (0, labSettings_1.selfHealingEnabled)(startup.enableSelfHealing, this.saved);
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Reads the saved value, once, at boot. A database that can't be read leaves
|
|
47
|
+
* the startup option in charge, as it does for the cleanup job, and is said
|
|
48
|
+
* once here rather than at every command.
|
|
49
|
+
*/
|
|
50
|
+
load() {
|
|
51
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
52
|
+
try {
|
|
53
|
+
const { enableSelfHealing } = yield typedi_1.Container.get(web_config_service_1.WebConfigService).getConfig();
|
|
54
|
+
this.saved = typeof enableSelfHealing === 'boolean' ? enableSelfHealing : undefined;
|
|
55
|
+
}
|
|
56
|
+
catch (err) {
|
|
57
|
+
this.saved = undefined;
|
|
58
|
+
logger_1.default.warn(`Could not read the saved self-healing setting, using the startup option: ${err === null || err === void 0 ? void 0 : err.message}`);
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
/** What `POST /config` just saved, in force for the next command. */
|
|
63
|
+
set(saved) {
|
|
64
|
+
this.saved = saved;
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
exports.SelfHealingSwitch = SelfHealingSwitch;
|
|
68
|
+
exports.SelfHealingSwitch = SelfHealingSwitch = __decorate([
|
|
69
|
+
(0, typedi_1.Service)()
|
|
70
|
+
], SelfHealingSwitch);
|