@xenon-device-management/xenon 2.13.1 → 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-Cvpkk4r9.js → AnnotationOverlay-B_VNtKBH.js} +1 -1
- package/lib/public/assets/{ApiKeyGate-Bhn5ZtVM.js → ApiKeyGate-BwuaMjkX.js} +1 -1
- package/lib/public/assets/{BugReportButton-BA6aEjTC.js → BugReportButton-BAt0_pzq.js} +1 -1
- package/lib/public/assets/DeviceMosaicView-BYaQHhTf.js +2 -0
- package/lib/public/assets/{EmptyState-CgzbuUvO.js → EmptyState-BSjhk_IV.js} +1 -1
- package/lib/public/assets/{FieldGroup-BHc_5qNc.js → FieldGroup-Cb5QZsE1.js} +1 -1
- package/lib/public/assets/{FilterMenu-D7OMGuRc.js → FilterMenu-BAaSlWbW.js} +1 -1
- package/lib/public/assets/{Menu-C173JJ0l.js → Menu-JGGYrSQQ.js} +1 -1
- package/lib/public/assets/{Modal-ChvVUI3B.js → Modal-CFCp3n9d.js} +1 -1
- package/lib/public/assets/{RecordingPage-Dhw4d_O6.js → RecordingPage-CbJj_EBT.js} +1 -1
- package/lib/public/assets/{RecordingsPage-z97ftGK-.js → RecordingsPage-BJeYnygR.js} +2 -2
- package/lib/public/assets/{SegmentedControl-DbXhBGZC.js → SegmentedControl-Bg42Atjy.js} +1 -1
- package/lib/public/assets/{SettingCard-C908GgiI.js → SettingCard-Njz1pUIJ.js} +1 -1
- package/lib/public/assets/{Table-B1DB_ZN-.js → Table-BcTm5xV2.js} +1 -1
- package/lib/public/assets/{activity-Css0xNvp.js → activity-DOVdsfMQ.js} +1 -1
- package/lib/public/assets/{ai-settings-BMQK3-yN.js → ai-settings-CsuJroRf.js} +2 -2
- package/lib/public/assets/{api-keys-CbJ42eDQ.js → api-keys-BKnNSTw9.js} +1 -1
- package/lib/public/assets/{apps-DW1pw2re.js → apps-D0SC6uw6.js} +2 -2
- package/lib/public/assets/{arrow-left-Dpog9L0F.js → arrow-left-MMbTmgoE.js} +1 -1
- package/lib/public/assets/{arrow-right-CsBIlSIw.js → arrow-right-BMfnGmxW.js} +1 -1
- package/lib/public/assets/{arrow-up-right-z1tRQE4R.js → arrow-up-right-DuAftMWB.js} +1 -1
- package/lib/public/assets/{auth-shell-mXiX5vNk.js → auth-shell-nZC2JNDO.js} +2 -2
- package/lib/public/assets/{builds-page-BcFn23bE.js → builds-page-hDCgTwkS.js} +1 -1
- package/lib/public/assets/{button-Cq_zHETv.js → button-Dda4irn_.js} +1 -1
- package/lib/public/assets/{calendar-CKxABX9q.js → calendar-BMr0Q62v.js} +1 -1
- package/lib/public/assets/{check-C7xu-hGU.js → check-CZ9iw9-e.js} +1 -1
- package/lib/public/assets/{chevron-right-BiqfdCGA.js → chevron-right-BRYJJVC9.js} +1 -1
- package/lib/public/assets/{circle-check-CyBjjPxR.js → circle-check-CqAJjQ5O.js} +1 -1
- package/lib/public/assets/{circle-x-6gMRcx6z.js → circle-x-DlDJtnVz.js} +1 -1
- package/lib/public/assets/{clock-U1rDTT1h.js → clock-CpuDKysU.js} +1 -1
- package/lib/public/assets/{copy-BIM0ZX0L.js → copy-CAQydNFe.js} +1 -1
- package/lib/public/assets/device-explorer-CdO6_nrn.js +282 -0
- package/lib/public/assets/{download-DDBRXnAZ.js → download-C9XG6lLv.js} +1 -1
- package/lib/public/assets/{forgot-password-Dpg7yxM8.js → forgot-password-CGnkRgE9.js} +1 -1
- package/lib/public/assets/h264Stream-VQBphYhg.js +26 -0
- package/lib/public/assets/{index-BGBqohfQ.js → index-BbKEXUFH.js} +5 -5
- package/lib/public/assets/{index-BTYMAqNW.css → index-Bxoxt9sV.css} +1 -1
- package/lib/public/assets/{index-BQ2luT7L.js → index-DJGc1M2f.js} +1 -1
- package/lib/public/assets/{input-BvB1cjHE.js → input-Cm0tmSoc.js} +1 -1
- package/lib/public/assets/{line-chart-m78VggnW.js → line-chart-D2DyJCml.js} +1 -1
- package/lib/public/assets/{list-checks-cY7o1QTL.js → list-checks-UWf1GY99.js} +1 -1
- package/lib/public/assets/{lock-loz8txQm.js → lock-CCJv9m_I.js} +1 -1
- package/lib/public/assets/{login-BYOvz4lx.js → login-f8vOVW-U.js} +1 -1
- package/lib/public/assets/maintenance-settings-C1PrkuAH.js +6 -0
- package/lib/public/assets/{monitor-C4HPHb6s.js → monitor-B2jDgwm7.js} +1 -1
- package/lib/public/assets/{mouse-pointer-2-BkdKtCzi.js → mouse-pointer-2-DLI1RtzU.js} +1 -1
- package/lib/public/assets/{network-C5e0MIWo.js → network-ebT86qoi.js} +1 -1
- package/lib/public/assets/{overview-tlu8ijGd.js → overview-C0aykVrd.js} +1 -1
- package/lib/public/assets/{page-header-FXPlo6az.js → page-header-BjzgWz-T.js} +1 -1
- package/lib/public/assets/{play-CWdk0_Sy.js → play-DLG9t8FU.js} +1 -1
- package/lib/public/assets/{plus-B2xCnntV.js → plus-nBFSMu_X.js} +1 -1
- package/lib/public/assets/{profile-page-D080qzr4.js → profile-page-CSl0UYlv.js} +1 -1
- package/lib/public/assets/{reset-password-D3u4Mbvn.js → reset-password-LeOuHqmB.js} +1 -1
- package/lib/public/assets/{runbook-page-hPRZMy7s.js → runbook-page-CMMeQml9.js} +1 -1
- package/lib/public/assets/{select-12RThAXc.js → select-88jHcl-Z.js} +1 -1
- package/lib/public/assets/{selector-detail-redirect-uQwvV7EQ.js → selector-detail-redirect-BbXKJjZM.js} +1 -1
- package/lib/public/assets/{selector-health-page-CThoxDFO.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-B02WEte8.js → stat-tile-BV98e_Sv.js} +1 -1
- package/lib/public/assets/{tablet-Dj30VGZl.js → tablet-BiAsnFDj.js} +1 -1
- package/lib/public/assets/{teams-uL4fjTDR.js → teams-B4-jrr-1.js} +1 -1
- package/lib/public/assets/{trash-2-B54e-TfY.js → trash-2-C5RSuX0e.js} +1 -1
- package/lib/public/assets/{upload-B3k8WCSP.js → upload-BG7_rMXK.js} +1 -1
- package/lib/public/assets/{use-builds-data-BFn90NH3.js → use-builds-data-J-riQYyC.js} +1 -1
- package/lib/public/assets/{use-password-reset-mode-DL2TmWZ0.js → use-password-reset-mode-C8-eH-UG.js} +1 -1
- package/lib/public/assets/{users-CqBuxMlX.js → users-CFba5TSy.js} +1 -1
- package/lib/public/assets/{users-DjQSucKb.js → users-Cp0beJm3.js} +1 -1
- package/lib/public/assets/{video-off-T0rlt0GA.js → video-off-DDsEISjV.js} +1 -1
- package/lib/public/assets/webhook-settings-BGu32vSB.js +1 -0
- package/lib/public/assets/{zap-gBHqutZV.js → zap-l4VaXzq8.js} +1 -1
- package/lib/public/index.html +2 -2
- package/lib/schema.json +19 -46
- 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 +30 -5
- package/lib/src/app/routers/dashboard.js +29 -1
- package/lib/src/app/routers/grid.js +32 -10
- package/lib/src/app/routers/streamStartConflict.js +3 -0
- 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 +46 -1
- package/lib/src/data-service/device-store.js +66 -3
- 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 +152 -9
- 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 +143 -37
- 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/middleware/deviceAccessGuard.js +63 -9
- 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 +97 -8
- package/lib/src/services/SessionLifecycleService.js +63 -53
- package/lib/src/services/ShutdownCoordinator.js +33 -0
- package/lib/src/services/TeamService.js +20 -3
- package/lib/src/services/device-access/SessionOwnerResolver.js +26 -0
- package/lib/src/services/device-access/deviceAccessPolicy.js +38 -6
- package/lib/src/services/device-access/leaseHold.js +26 -0
- package/lib/src/services/device-access/ticketActorAccess.js +6 -0
- 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/lease/activeLeases.js +37 -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/busy-precheck.js +24 -3
- 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-access-lease.spec.js +270 -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/lease/allocate-lease-branch.spec.js +22 -0
- package/lib/test/unit/lease/session-lease-proof.spec.js +59 -0
- package/lib/test/unit/lease-held-phone.spec.js +196 -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 +114 -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/schema-required-args.spec.js +21 -16
- 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 -46
- package/scripts/generate-types-from-schema.js +7 -2
- package/scripts/lib/always-present-plugin-args.js +42 -0
- package/lib/public/assets/DeviceMosaicView-ChhHnpFQ.js +0 -2
- package/lib/public/assets/device-explorer-DSpAA_t9.js +0 -279
- package/lib/public/assets/maintenance-settings-BxbA7SkB.js +0 -6
- package/lib/public/assets/recording-group-store-B75435xR.js +0 -26
- package/lib/public/assets/session-detail-page-Bc02YjMm.js +0 -4
- package/lib/public/assets/settings-C1VIluuT.js +0 -6
- package/lib/public/assets/webhook-settings-DAHgnJyZ.js +0 -1
|
@@ -0,0 +1,161 @@
|
|
|
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
|
+
require("reflect-metadata");
|
|
16
|
+
const chai_1 = require("chai");
|
|
17
|
+
const sinon_1 = __importDefault(require("sinon"));
|
|
18
|
+
const sharp_1 = __importDefault(require("sharp"));
|
|
19
|
+
const OmniVisionService_1 = require("../../src/services/omni-vision/OmniVisionService");
|
|
20
|
+
const AIService_1 = require("../../src/services/AIService");
|
|
21
|
+
const HealingOrchestrator_1 = require("../../src/services/healing/HealingOrchestrator");
|
|
22
|
+
const types_1 = require("../../src/services/healing/types");
|
|
23
|
+
const screenScale_1 = require("../../src/services/omni-vision/screenScale");
|
|
24
|
+
/**
|
|
25
|
+
* OCR and the AI find a target in screenshot pixels. An iPhone's screenshot is
|
|
26
|
+
* 2x or 3x its screen in points, and XCUITest's W3C actions and element rects
|
|
27
|
+
* are in points, so a tap at the pixel position landed 2-3 times too far right
|
|
28
|
+
* and down. Nothing converted: not smartTap/visualTap, not the virtual
|
|
29
|
+
* elements the -custom:ai-* locators return, not the healing tiers' tap.
|
|
30
|
+
*
|
|
31
|
+
* Positions found in a screenshot are now converted to the driver's
|
|
32
|
+
* coordinates on iOS (screenshot size over window size). Android's W3C
|
|
33
|
+
* coordinates are the screenshot's pixels, so nothing changes there.
|
|
34
|
+
*/
|
|
35
|
+
describe('Omni-Vision taps in points on iOS', () => {
|
|
36
|
+
// A 3x screen: 30 x 60 pixels for a 10 x 20 point window.
|
|
37
|
+
let png3x;
|
|
38
|
+
before(() => __awaiter(void 0, void 0, void 0, function* () {
|
|
39
|
+
const image = yield (0, sharp_1.default)({
|
|
40
|
+
create: { width: 30, height: 60, channels: 3, background: { r: 255, g: 255, b: 255 } },
|
|
41
|
+
})
|
|
42
|
+
.png()
|
|
43
|
+
.toBuffer();
|
|
44
|
+
png3x = image.toString('base64');
|
|
45
|
+
}));
|
|
46
|
+
// "OK" at pixels 12..18 x 30..36: its middle is (15, 33) px, (5, 11) pt.
|
|
47
|
+
const OK_WORD = { text: 'OK', confidence: 90, bbox: { x0: 12, y0: 30, x1: 18, y1: 36 } };
|
|
48
|
+
let service;
|
|
49
|
+
let ios;
|
|
50
|
+
let android;
|
|
51
|
+
function driverFor(platformName) {
|
|
52
|
+
return {
|
|
53
|
+
sessionId: `session-${platformName}`,
|
|
54
|
+
caps: { platformName },
|
|
55
|
+
getScreenshot: sinon_1.default.stub().resolves(png3x),
|
|
56
|
+
getWindowRect: sinon_1.default.stub().resolves({ x: 0, y: 0, width: 10, height: 20 }),
|
|
57
|
+
performActions: sinon_1.default.stub().resolves(),
|
|
58
|
+
releaseActions: sinon_1.default.stub().resolves(),
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
beforeEach(() => {
|
|
62
|
+
service = new OmniVisionService_1.OmniVisionService();
|
|
63
|
+
sinon_1.default.stub(service, 'performOcr').resolves({ text: 'OK', words: [OK_WORD] });
|
|
64
|
+
ios = driverFor('iOS');
|
|
65
|
+
android = driverFor('Android');
|
|
66
|
+
});
|
|
67
|
+
afterEach(() => sinon_1.default.restore());
|
|
68
|
+
const tappedAt = (driver) => {
|
|
69
|
+
const move = driver.performActions.firstCall.args[0][0].actions[0];
|
|
70
|
+
return { x: move.x, y: move.y };
|
|
71
|
+
};
|
|
72
|
+
it('smartTap by text taps in points on iOS', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
73
|
+
const result = yield service.omniClickByText(ios, { text: 'OK' });
|
|
74
|
+
(0, chai_1.expect)(result.clicked, result.message).to.equal(true);
|
|
75
|
+
(0, chai_1.expect)(tappedAt(ios)).to.deep.equal({ x: 5, y: 11 });
|
|
76
|
+
(0, chai_1.expect)(result.target).to.deep.include({
|
|
77
|
+
x: 5,
|
|
78
|
+
y: 11,
|
|
79
|
+
rect: { x: 4, y: 10, width: 2, height: 2 },
|
|
80
|
+
});
|
|
81
|
+
}));
|
|
82
|
+
it('smartTap by text taps in pixels on Android, without asking for the window', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
83
|
+
const result = yield service.omniClickByText(android, { text: 'OK' });
|
|
84
|
+
(0, chai_1.expect)(result.clicked, result.message).to.equal(true);
|
|
85
|
+
(0, chai_1.expect)(tappedAt(android)).to.deep.equal({ x: 15, y: 33 });
|
|
86
|
+
(0, chai_1.expect)(android.getWindowRect.called).to.equal(false);
|
|
87
|
+
}));
|
|
88
|
+
it('visualTap taps in points on iOS', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
89
|
+
sinon_1.default.stub(AIService_1.AI_SERVICE, 'visualFind').resolves({ x: 15, y: 33 });
|
|
90
|
+
const result = yield service.omniClickByIcon(ios, { icon: 'the gear icon' });
|
|
91
|
+
(0, chai_1.expect)(result.clicked, result.message).to.equal(true);
|
|
92
|
+
(0, chai_1.expect)(tappedAt(ios)).to.deep.equal({ x: 5, y: 11 });
|
|
93
|
+
}));
|
|
94
|
+
it('gives the -custom:ai-text locator an element in points on iOS', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
95
|
+
var _a;
|
|
96
|
+
const [found] = yield service.findByText(ios, 'OK');
|
|
97
|
+
(0, chai_1.expect)(found.rect).to.deep.equal({ x: 4, y: 10, width: 2, height: 2 });
|
|
98
|
+
(0, chai_1.expect)((_a = service.getVirtualElement(found.id)) === null || _a === void 0 ? void 0 : _a.rect).to.deep.equal(found.rect);
|
|
99
|
+
}));
|
|
100
|
+
it("doesn't tap on iOS when the window size can't be read", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
101
|
+
ios.getWindowRect.rejects(new Error('WDA is not running'));
|
|
102
|
+
const err = yield service.omniClickByText(ios, { text: 'OK' }).then(() => null, (e) => e);
|
|
103
|
+
(0, chai_1.expect)(err === null || err === void 0 ? void 0 : err.message).to.match(/WDA is not running/);
|
|
104
|
+
(0, chai_1.expect)(ios.performActions.called).to.equal(false);
|
|
105
|
+
}));
|
|
106
|
+
it('reads the window from getWindowSize when there is no getWindowRect', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
107
|
+
const driver = {
|
|
108
|
+
caps: { platformName: 'iOS' },
|
|
109
|
+
getWindowSize: () => __awaiter(void 0, void 0, void 0, function* () { return ({ width: 15, height: 30 }); }),
|
|
110
|
+
};
|
|
111
|
+
(0, chai_1.expect)(yield (0, screenScale_1.screenScaleOf)(driver, png3x)).to.deep.equal({ x: 2, y: 2 });
|
|
112
|
+
}));
|
|
113
|
+
it("keeps pixels for a driver that isn't iOS, or can't be placed", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
114
|
+
(0, chai_1.expect)(yield (0, screenScale_1.screenScaleOf)({}, 'not an image')).to.deep.equal({ x: 1, y: 1 });
|
|
115
|
+
(0, chai_1.expect)(yield (0, screenScale_1.screenScaleOf)({ caps: { platformName: 'Android' } }, 'x')).to.deep.equal({
|
|
116
|
+
x: 1,
|
|
117
|
+
y: 1,
|
|
118
|
+
});
|
|
119
|
+
}));
|
|
120
|
+
it('converts the OCR and Visual AI healing tiers’ rect to points on iOS', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
121
|
+
const orchestrator = new HealingOrchestrator_1.HealingOrchestrator({
|
|
122
|
+
getSignature: () => __awaiter(void 0, void 0, void 0, function* () { return null; }),
|
|
123
|
+
saveSignature: () => __awaiter(void 0, void 0, void 0, function* () { return undefined; }),
|
|
124
|
+
});
|
|
125
|
+
const ocr = orchestrator.providers.find((p) => p.tier === types_1.HealingTier.TIER_3_LOCAL_OCR);
|
|
126
|
+
sinon_1.default.stub(ocr, 'heal').resolves({
|
|
127
|
+
id: 'healed_ocr_1',
|
|
128
|
+
tier: types_1.HealingTier.TIER_3_LOCAL_OCR,
|
|
129
|
+
confidence: 0.9,
|
|
130
|
+
originalSelector: '//*[@label="OK"]',
|
|
131
|
+
recommendedSelector: 'ocr:text="OK"',
|
|
132
|
+
rect: { x: 12, y: 30, width: 6, height: 6 },
|
|
133
|
+
});
|
|
134
|
+
ios.getPageSource = sinon_1.default.stub().resolves('<AppiumAUT/>');
|
|
135
|
+
const healed = yield orchestrator.attemptHealing('s1', ios, 'xpath', '//*[@label="OK"]', [3]);
|
|
136
|
+
(0, chai_1.expect)(healed === null || healed === void 0 ? void 0 : healed.rect).to.deep.equal({ x: 4, y: 10, width: 2, height: 2 });
|
|
137
|
+
}));
|
|
138
|
+
it("drops a healed virtual element whose position on an iPhone can't be worked out", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
139
|
+
const orchestrator = new HealingOrchestrator_1.HealingOrchestrator({
|
|
140
|
+
getSignature: () => __awaiter(void 0, void 0, void 0, function* () { return null; }),
|
|
141
|
+
saveSignature: () => __awaiter(void 0, void 0, void 0, function* () { return undefined; }),
|
|
142
|
+
});
|
|
143
|
+
const ocr = orchestrator.providers.find((p) => p.tier === types_1.HealingTier.TIER_3_LOCAL_OCR);
|
|
144
|
+
const found = {
|
|
145
|
+
tier: types_1.HealingTier.TIER_3_LOCAL_OCR,
|
|
146
|
+
confidence: 0.9,
|
|
147
|
+
originalSelector: '//*[@label="OK"]',
|
|
148
|
+
recommendedSelector: 'ocr:text="OK"',
|
|
149
|
+
rect: { x: 12, y: 30, width: 6, height: 6 },
|
|
150
|
+
};
|
|
151
|
+
const heal = sinon_1.default.stub(ocr, 'heal').resolves(Object.assign(Object.assign({}, found), { id: 'healed_ocr_1' }));
|
|
152
|
+
ios.getPageSource = sinon_1.default.stub().resolves('<AppiumAUT/>');
|
|
153
|
+
ios.getWindowRect.rejects(new Error('WDA is not running'));
|
|
154
|
+
(0, chai_1.expect)(yield orchestrator.attemptHealing('s1', ios, 'xpath', '//x', [3])).to.equal(null);
|
|
155
|
+
// A real element the tier resolved is kept, without the rect.
|
|
156
|
+
heal.resolves(Object.assign(Object.assign({}, found), { id: 'real-element-1' }));
|
|
157
|
+
const healed = yield orchestrator.attemptHealing('s1', ios, 'xpath', '//x', [3]);
|
|
158
|
+
(0, chai_1.expect)(healed === null || healed === void 0 ? void 0 : healed.id).to.equal('real-element-1');
|
|
159
|
+
(0, chai_1.expect)(healed === null || healed === void 0 ? void 0 : healed.rect).to.equal(undefined);
|
|
160
|
+
}));
|
|
161
|
+
});
|
|
@@ -0,0 +1,129 @@
|
|
|
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
|
+
require("reflect-metadata");
|
|
16
|
+
const chai_1 = require("chai");
|
|
17
|
+
const sinon_1 = __importDefault(require("sinon"));
|
|
18
|
+
const OmniVisionService_1 = require("../../src/services/omni-vision/OmniVisionService");
|
|
19
|
+
const AIService_1 = require("../../src/services/AIService");
|
|
20
|
+
/**
|
|
21
|
+
* `xenon: smartTap { text }` (and `omniClick`) compared the text with one OCR
|
|
22
|
+
* word at a time, so any text with a space in it ("Sign in", "Forgot
|
|
23
|
+
* password?") never matched: the answer was `clicked: false`. The
|
|
24
|
+
* `-custom:ai-text` locator had the same rule. Now the text is looked for
|
|
25
|
+
* across neighbouring words on one line, and the tap goes to the middle of
|
|
26
|
+
* the words it covers.
|
|
27
|
+
*/
|
|
28
|
+
describe('Omni-Vision text taps match phrases', () => {
|
|
29
|
+
// Tesseract's words, in screenshot pixels. One line holds "Sign in" and,
|
|
30
|
+
// far to the right, "Help"; "Sign" and "in" also appear on two lines.
|
|
31
|
+
const word = (text, x0, y0, x1, y1) => ({
|
|
32
|
+
text,
|
|
33
|
+
confidence: 90,
|
|
34
|
+
bbox: { x0, y0, x1, y1 },
|
|
35
|
+
});
|
|
36
|
+
const WORDS = [
|
|
37
|
+
word('Sign', 100, 200, 150, 224),
|
|
38
|
+
word('in', 158, 200, 180, 224),
|
|
39
|
+
word('Help', 600, 200, 650, 224),
|
|
40
|
+
word('Forgot', 100, 300, 170, 324),
|
|
41
|
+
word('password?', 178, 300, 290, 324),
|
|
42
|
+
word('Sign', 100, 400, 150, 424),
|
|
43
|
+
word('in', 100, 440, 120, 464),
|
|
44
|
+
];
|
|
45
|
+
let service;
|
|
46
|
+
let driver;
|
|
47
|
+
beforeEach(() => {
|
|
48
|
+
service = new OmniVisionService_1.OmniVisionService();
|
|
49
|
+
sinon_1.default.stub(service, 'performOcr').resolves({ text: '', words: WORDS });
|
|
50
|
+
driver = {
|
|
51
|
+
sessionId: 'session-1',
|
|
52
|
+
// An Android driver: its W3C coordinates are the screenshot's pixels.
|
|
53
|
+
caps: { platformName: 'Android' },
|
|
54
|
+
getScreenshot: sinon_1.default.stub().resolves('aGVsbG8='),
|
|
55
|
+
performActions: sinon_1.default.stub().resolves(),
|
|
56
|
+
releaseActions: sinon_1.default.stub().resolves(),
|
|
57
|
+
};
|
|
58
|
+
});
|
|
59
|
+
afterEach(() => sinon_1.default.restore());
|
|
60
|
+
const tappedAt = () => {
|
|
61
|
+
const move = driver.performActions.firstCall.args[0][0].actions[0];
|
|
62
|
+
return { x: move.x, y: move.y };
|
|
63
|
+
};
|
|
64
|
+
it('taps the middle of a two-word text', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
65
|
+
const result = yield service.omniClickByText(driver, { text: 'Sign in' });
|
|
66
|
+
(0, chai_1.expect)(result.clicked, result.message).to.equal(true);
|
|
67
|
+
(0, chai_1.expect)(result.target).to.deep.include({
|
|
68
|
+
text: 'Sign in',
|
|
69
|
+
x: 140,
|
|
70
|
+
y: 212,
|
|
71
|
+
rect: { x: 100, y: 200, width: 80, height: 24 },
|
|
72
|
+
});
|
|
73
|
+
(0, chai_1.expect)(tappedAt()).to.deep.equal({ x: 140, y: 212 });
|
|
74
|
+
}));
|
|
75
|
+
it('matches a phrase whatever its case, and part of a word at either end', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
76
|
+
var _a;
|
|
77
|
+
const result = yield service.omniClickByText(driver, { text: 'forgot PASSWORD' });
|
|
78
|
+
(0, chai_1.expect)(result.clicked, result.message).to.equal(true);
|
|
79
|
+
(0, chai_1.expect)((_a = result.target) === null || _a === void 0 ? void 0 : _a.text).to.equal('Forgot password?');
|
|
80
|
+
(0, chai_1.expect)(tappedAt()).to.deep.equal({ x: 195, y: 312 });
|
|
81
|
+
}));
|
|
82
|
+
it('does not join words on different lines, or far apart on one line', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
83
|
+
for (const text of ['in Help', 'Sign in Help', 'password? Sign']) {
|
|
84
|
+
const result = yield service.omniClickByText(driver, { text });
|
|
85
|
+
(0, chai_1.expect)(result.clicked, text).to.equal(false);
|
|
86
|
+
}
|
|
87
|
+
(0, chai_1.expect)(driver.performActions.called).to.equal(false);
|
|
88
|
+
}));
|
|
89
|
+
it('still matches a single word, as before', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
90
|
+
const result = yield service.omniClickByText(driver, { text: 'help' });
|
|
91
|
+
(0, chai_1.expect)(result.clicked, result.message).to.equal(true);
|
|
92
|
+
(0, chai_1.expect)(tappedAt()).to.deep.equal({ x: 625, y: 212 });
|
|
93
|
+
}));
|
|
94
|
+
it('finds a phrase with the -custom:ai-text locator too', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
95
|
+
const found = yield service.findByText(driver, 'Sign in');
|
|
96
|
+
(0, chai_1.expect)(found).to.have.lengthOf(1);
|
|
97
|
+
(0, chai_1.expect)(found[0].text).to.equal('Sign in');
|
|
98
|
+
(0, chai_1.expect)(found[0].rect).to.deep.equal({ x: 100, y: 200, width: 80, height: 24 });
|
|
99
|
+
}));
|
|
100
|
+
});
|
|
101
|
+
/**
|
|
102
|
+
* `xenon: smartTap { icon }` / `visualTap` answered `clicked: false`, "No visual
|
|
103
|
+
* match found", when it never looked: no AI provider, or the AI call failed.
|
|
104
|
+
* It now fails with the reason; `clicked: false` means the AI looked and found
|
|
105
|
+
* nothing.
|
|
106
|
+
*/
|
|
107
|
+
describe('Omni-Vision icon taps that cannot look', () => {
|
|
108
|
+
const driver = {
|
|
109
|
+
sessionId: 'session-1',
|
|
110
|
+
caps: { platformName: 'Android' },
|
|
111
|
+
getScreenshot: () => __awaiter(void 0, void 0, void 0, function* () { return 'aGVsbG8='; }),
|
|
112
|
+
performActions: sinon_1.default.stub().resolves(),
|
|
113
|
+
};
|
|
114
|
+
beforeEach(() => {
|
|
115
|
+
sinon_1.default.stub(AIService_1.AI_SERVICE, 'initializeProvider');
|
|
116
|
+
});
|
|
117
|
+
afterEach(() => sinon_1.default.restore());
|
|
118
|
+
it('fails when no AI provider is configured', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
119
|
+
sinon_1.default.stub(AIService_1.AI_SERVICE, 'provider').value(null);
|
|
120
|
+
const err = yield new OmniVisionService_1.OmniVisionService().omniClickByIcon(driver, { icon: 'the gear' }).then(() => null, (e) => e);
|
|
121
|
+
(0, chai_1.expect)(err === null || err === void 0 ? void 0 : err.message).to.match(/No AI provider is configured/);
|
|
122
|
+
}));
|
|
123
|
+
it('answers clicked: false when the AI looked and found nothing', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
124
|
+
sinon_1.default.stub(AIService_1.AI_SERVICE, 'visualFind').resolves(null);
|
|
125
|
+
const result = yield new OmniVisionService_1.OmniVisionService().omniClickByIcon(driver, { icon: 'the gear' });
|
|
126
|
+
(0, chai_1.expect)(result.clicked).to.equal(false);
|
|
127
|
+
(0, chai_1.expect)(result.message).to.match(/No visual match/);
|
|
128
|
+
}));
|
|
129
|
+
});
|
|
@@ -95,7 +95,7 @@ describe('OmniVisionService Unit Tests', () => {
|
|
|
95
95
|
terminate: sinon_1.default.stub().resolves(),
|
|
96
96
|
};
|
|
97
97
|
sinon_1.default.stub(tesseract_js_1.default, 'createWorker').resolves(mockWorker);
|
|
98
|
-
sinon_1.default.stub(AIService_1.AI_SERVICE, '
|
|
98
|
+
sinon_1.default.stub(AIService_1.AI_SERVICE, 'describeScreen').resolves('AI Insights text');
|
|
99
99
|
const analysis = yield omniService.analyzeScreen(mockDriver);
|
|
100
100
|
(0, chai_1.expect)(analysis.ocr.text).to.equal('Screen Text');
|
|
101
101
|
(0, chai_1.expect)(analysis.ai_insights).to.equal('AI Insights text');
|
|
@@ -0,0 +1,114 @@
|
|
|
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
|
+
const chai_1 = require("chai");
|
|
16
|
+
const fs_1 = __importDefault(require("fs"));
|
|
17
|
+
const os_1 = __importDefault(require("os"));
|
|
18
|
+
const path_1 = __importDefault(require("path"));
|
|
19
|
+
const lodash_1 = __importDefault(require("lodash"));
|
|
20
|
+
// An Appium config file is checked against Xenon's schema.json before Appium
|
|
21
|
+
// fills in defaults (appium/build/lib/main.js: readConfigFile, then
|
|
22
|
+
// defaultsDeep(cli, file, schema defaults)). A `required` option therefore
|
|
23
|
+
// makes Appium refuse any file that leaves it out, even though it has a
|
|
24
|
+
// default. Through 2.13.1 the README's own five-option sample was refused with
|
|
25
|
+
// "REQUIRED must have required property 'enableJsonLogging'". These checks run
|
|
26
|
+
// Appium's own loader so a file that sets a few options keeps starting.
|
|
27
|
+
/* eslint-disable @typescript-eslint/no-var-requires -- Appium's internals have no type exports */
|
|
28
|
+
const appiumSchema = require('appium/build/lib/schema/schema');
|
|
29
|
+
const { readConfigFile } = require('appium/build/lib/config-file');
|
|
30
|
+
/* eslint-enable @typescript-eslint/no-var-requires */
|
|
31
|
+
const xenonSchema = JSON.parse(fs_1.default.readFileSync(path_1.default.join(__dirname, '..', '..', 'schema.json'), 'utf8'));
|
|
32
|
+
// The README's sample, verbatim.
|
|
33
|
+
const README_SAMPLE = `server:
|
|
34
|
+
use-plugins: [xenon]
|
|
35
|
+
plugin:
|
|
36
|
+
xenon:
|
|
37
|
+
platform: both
|
|
38
|
+
enableDashboard: true
|
|
39
|
+
maxSessions: 8
|
|
40
|
+
enableSelfHealing: true
|
|
41
|
+
buildCleanupDays: 30
|
|
42
|
+
`;
|
|
43
|
+
describe('An Appium config file for Xenon', function () {
|
|
44
|
+
let dir;
|
|
45
|
+
function load(yaml) {
|
|
46
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
47
|
+
const file = path_1.default.join(dir, `config-${Math.random().toString(36).slice(2)}.yaml`);
|
|
48
|
+
fs_1.default.writeFileSync(file, yaml);
|
|
49
|
+
return readConfigFile(file);
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
before(function () {
|
|
53
|
+
appiumSchema.resetSchema();
|
|
54
|
+
appiumSchema.registerSchema('plugin', 'xenon', xenonSchema);
|
|
55
|
+
appiumSchema.finalizeSchema();
|
|
56
|
+
dir = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'xenon-config-'));
|
|
57
|
+
});
|
|
58
|
+
after(function () {
|
|
59
|
+
appiumSchema.resetSchema();
|
|
60
|
+
fs_1.default.rmSync(dir, { recursive: true, force: true });
|
|
61
|
+
});
|
|
62
|
+
it('is accepted when it sets only a few options (the README sample)', function () {
|
|
63
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
64
|
+
var _a;
|
|
65
|
+
const result = yield load(README_SAMPLE);
|
|
66
|
+
(0, chai_1.expect)((_a = result.reason) !== null && _a !== void 0 ? _a : result.errors).to.be.empty;
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
it('is accepted when it sets no Xenon option at all', function () {
|
|
70
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
71
|
+
var _a;
|
|
72
|
+
const result = yield load('server:\n use-plugins: [xenon]\n');
|
|
73
|
+
(0, chai_1.expect)((_a = result.reason) !== null && _a !== void 0 ? _a : result.errors).to.be.empty;
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
it('is accepted with the network capture option Xenon Control writes', function () {
|
|
77
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
78
|
+
var _a;
|
|
79
|
+
// The server's `interceptor` option is now each session's default
|
|
80
|
+
// (resolveInterceptorOptions); configs that set it must keep starting.
|
|
81
|
+
const result = yield load(`${README_SAMPLE} interceptor:\n enabled: true\n bufferSize: 50\n captureBodies: false\n`);
|
|
82
|
+
(0, chai_1.expect)((_a = result.reason) !== null && _a !== void 0 ? _a : result.errors).to.be.empty;
|
|
83
|
+
(0, chai_1.expect)(result.config.server.plugin.xenon.interceptor).to.deep.equal({
|
|
84
|
+
enabled: true,
|
|
85
|
+
bufferSize: 50,
|
|
86
|
+
captureBodies: false,
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
it('is still refused when a value is invalid', function () {
|
|
91
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
92
|
+
const result = yield load(README_SAMPLE.replace('platform: both', 'platform: windows'));
|
|
93
|
+
(0, chai_1.expect)(result.errors).to.not.be.empty;
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
it('leaves Appium to fill every option the file leaves out with its default', function () {
|
|
97
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
98
|
+
var _a;
|
|
99
|
+
// What Appium's main does after a file passes: CLI args, then the file,
|
|
100
|
+
// then the schema's defaults.
|
|
101
|
+
const result = yield load(README_SAMPLE);
|
|
102
|
+
const defaults = appiumSchema.getDefaultsForSchema(false);
|
|
103
|
+
const server = lodash_1.default.defaultsDeep({}, (_a = result.config) === null || _a === void 0 ? void 0 : _a.server, defaults);
|
|
104
|
+
const xenon = server.plugin.xenon;
|
|
105
|
+
const setInFile = new Set(Object.keys(result.config.server.plugin.xenon));
|
|
106
|
+
(0, chai_1.expect)(xenon.maxSessions).to.equal(8); // from the file
|
|
107
|
+
for (const [key, prop] of Object.entries(xenonSchema.properties)) {
|
|
108
|
+
if ('default' in prop && !setInFile.has(key)) {
|
|
109
|
+
(0, chai_1.expect)(xenon[key], `${key} should take its default`).to.deep.equal(prop.default);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
});
|
|
@@ -0,0 +1,64 @@
|
|
|
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
|
+
require("reflect-metadata");
|
|
16
|
+
const chai_1 = require("chai");
|
|
17
|
+
const sinon_1 = __importDefault(require("sinon"));
|
|
18
|
+
const typedi_1 = require("typedi");
|
|
19
|
+
const ServerManager_1 = require("../../../src/services/ServerManager");
|
|
20
|
+
const PhoneNetworkRestore_1 = require("../../../src/services/network/PhoneNetworkRestore");
|
|
21
|
+
const device_store_1 = require("../../../src/data-service/device-store");
|
|
22
|
+
const container_registration_1 = require("../../helpers/container-registration");
|
|
23
|
+
const ME = 'http://10.0.0.5:4723';
|
|
24
|
+
const NODE_ID = 'node-me';
|
|
25
|
+
/**
|
|
26
|
+
* At its start a server checks the network of the Android phones it drives:
|
|
27
|
+
* never a phone another server on the same machine drives (a hub keeps its
|
|
28
|
+
* nodes' rows), a cloud phone or an iPhone.
|
|
29
|
+
*/
|
|
30
|
+
describe('the phone network check at boot', () => {
|
|
31
|
+
let restoreRegs;
|
|
32
|
+
let cleanUpAtBoot;
|
|
33
|
+
beforeEach(() => {
|
|
34
|
+
restoreRegs = (0, container_registration_1.saveRegistrations)(PhoneNetworkRestore_1.PhoneNetworkRestore);
|
|
35
|
+
cleanUpAtBoot = sinon_1.default.stub().resolves();
|
|
36
|
+
typedi_1.Container.set(PhoneNetworkRestore_1.PhoneNetworkRestore, { cleanUpAtBoot });
|
|
37
|
+
});
|
|
38
|
+
afterEach(() => {
|
|
39
|
+
sinon_1.default.restore();
|
|
40
|
+
restoreRegs();
|
|
41
|
+
});
|
|
42
|
+
it("checks this server's own Android phones, once each", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
43
|
+
sinon_1.default.stub(device_store_1.DeviceStoreFactory, 'getStore').returns({
|
|
44
|
+
getAllDevices: () => __awaiter(void 0, void 0, void 0, function* () {
|
|
45
|
+
return [
|
|
46
|
+
{ udid: 'MINE', platform: 'android', host: ME, nodeId: NODE_ID },
|
|
47
|
+
{ udid: 'MINE', platform: 'android', host: ME, nodeId: NODE_ID },
|
|
48
|
+
{ udid: 'NO-ID', platform: 'android', host: ME },
|
|
49
|
+
{ udid: 'NODE', platform: 'android', host: 'http://10.0.0.5:4724', nodeId: 'node-b' },
|
|
50
|
+
{ udid: 'CLOUD', platform: 'android', host: ME, nodeId: NODE_ID, cloud: { name: 'x' } },
|
|
51
|
+
{ udid: 'IPHONE', platform: 'ios', host: ME, nodeId: NODE_ID },
|
|
52
|
+
];
|
|
53
|
+
}),
|
|
54
|
+
});
|
|
55
|
+
yield typedi_1.Container.get(ServerManager_1.ServerManager).cleanUpPhoneNetworks({ origin: ME, hosts: new Set([ME]) }, NODE_ID);
|
|
56
|
+
(0, chai_1.expect)(cleanUpAtBoot.calledOnce).to.equal(true);
|
|
57
|
+
(0, chai_1.expect)(cleanUpAtBoot.firstCall.args[0]).to.deep.equal(['MINE', 'NO-ID']);
|
|
58
|
+
}));
|
|
59
|
+
it('never fails the boot', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
60
|
+
sinon_1.default.stub(device_store_1.DeviceStoreFactory, 'getStore').throws(new Error('no store'));
|
|
61
|
+
yield typedi_1.Container.get(ServerManager_1.ServerManager).cleanUpPhoneNetworks({ origin: ME, hosts: new Set([ME]) }, NODE_ID);
|
|
62
|
+
(0, chai_1.expect)(cleanUpAtBoot.called).to.equal(false);
|
|
63
|
+
}));
|
|
64
|
+
});
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
require("reflect-metadata");
|
|
4
|
+
const chai_1 = require("chai");
|
|
5
|
+
const XenonCapabilityManager_1 = require("../../../src/XenonCapabilityManager");
|
|
6
|
+
const interceptorOptions_1 = require("../../../src/services/interceptor/interceptorOptions");
|
|
7
|
+
const capsOf = (alwaysMatch) => (0, XenonCapabilityManager_1.getXenonCapabilities)({ alwaysMatch, firstMatch: [{}] });
|
|
8
|
+
/**
|
|
9
|
+
* The server's `interceptor` option was declared in schema.json and shown in
|
|
10
|
+
* Xenon Control, but nothing read it: turning it on did nothing. It is now the
|
|
11
|
+
* default for this server's sessions, and a session's own capability wins
|
|
12
|
+
* field by field.
|
|
13
|
+
*/
|
|
14
|
+
describe('resolveInterceptorOptions: the server option is the default, the session decides', () => {
|
|
15
|
+
it('leaves capture off when neither the session nor the server turns it on', () => {
|
|
16
|
+
(0, chai_1.expect)((0, interceptorOptions_1.resolveInterceptorOptions)(capsOf({}), undefined).enabled).to.equal(false);
|
|
17
|
+
(0, chai_1.expect)((0, interceptorOptions_1.resolveInterceptorOptions)(capsOf({}), {}).enabled).to.equal(false);
|
|
18
|
+
});
|
|
19
|
+
it("turns capture on for a session that doesn't say, when the server's option is on", () => {
|
|
20
|
+
const opts = (0, interceptorOptions_1.resolveInterceptorOptions)(capsOf({}), { enabled: true });
|
|
21
|
+
(0, chai_1.expect)(opts.enabled).to.equal(true);
|
|
22
|
+
(0, chai_1.expect)(opts.captureBodies).to.equal(true);
|
|
23
|
+
(0, chai_1.expect)(opts.mocks).to.deep.equal([]);
|
|
24
|
+
});
|
|
25
|
+
it("lets a session turn capture off when the server's option is on", () => {
|
|
26
|
+
const object = (0, interceptorOptions_1.resolveInterceptorOptions)(capsOf({ 'xe:interceptor': { enabled: false } }), {
|
|
27
|
+
enabled: true,
|
|
28
|
+
});
|
|
29
|
+
(0, chai_1.expect)(object.enabled).to.equal(false);
|
|
30
|
+
const flat = (0, interceptorOptions_1.resolveInterceptorOptions)(capsOf({ 'xe:interceptorEnabled': false }), {
|
|
31
|
+
enabled: true,
|
|
32
|
+
});
|
|
33
|
+
(0, chai_1.expect)(flat.enabled).to.equal(false);
|
|
34
|
+
});
|
|
35
|
+
it("lets a session turn capture on when the server's option is off", () => {
|
|
36
|
+
const opts = (0, interceptorOptions_1.resolveInterceptorOptions)(capsOf({ 'xe:interceptor': { enabled: true } }), {
|
|
37
|
+
enabled: false,
|
|
38
|
+
});
|
|
39
|
+
(0, chai_1.expect)(opts.enabled).to.equal(true);
|
|
40
|
+
});
|
|
41
|
+
it("takes bufferSize and captureBodies from the server when the session doesn't set them", () => {
|
|
42
|
+
const opts = (0, interceptorOptions_1.resolveInterceptorOptions)(capsOf({ 'xe:interceptor': { enabled: true } }), {
|
|
43
|
+
bufferSize: 50,
|
|
44
|
+
captureBodies: false,
|
|
45
|
+
});
|
|
46
|
+
(0, chai_1.expect)(opts.bufferSize).to.equal(50);
|
|
47
|
+
(0, chai_1.expect)(opts.captureBodies).to.equal(false);
|
|
48
|
+
});
|
|
49
|
+
it("keeps the session's own bufferSize and captureBodies over the server's", () => {
|
|
50
|
+
const opts = (0, interceptorOptions_1.resolveInterceptorOptions)(capsOf({ 'xe:interceptor': { enabled: true, bufferSize: 7, captureBodies: true } }), { bufferSize: 50, captureBodies: false });
|
|
51
|
+
(0, chai_1.expect)(opts.bufferSize).to.equal(7);
|
|
52
|
+
(0, chai_1.expect)(opts.captureBodies).to.equal(true);
|
|
53
|
+
});
|
|
54
|
+
it('keeps mocks and host filters from the session only', () => {
|
|
55
|
+
const mocks = [{ match: { url: '**/x' }, respondWith: { status: 204 } }];
|
|
56
|
+
const opts = (0, interceptorOptions_1.resolveInterceptorOptions)(capsOf({
|
|
57
|
+
'xe:interceptor': { mocks, includeHosts: ['a.example'], excludeHosts: ['b.example'] },
|
|
58
|
+
}), { enabled: true });
|
|
59
|
+
(0, chai_1.expect)(opts.mocks).to.deep.equal(mocks);
|
|
60
|
+
(0, chai_1.expect)(opts.includeHosts).to.deep.equal(['a.example']);
|
|
61
|
+
(0, chai_1.expect)(opts.excludeHosts).to.deep.equal(['b.example']);
|
|
62
|
+
});
|
|
63
|
+
it('reads a session that sets nothing about capture as unset, not as off', () => {
|
|
64
|
+
const caps = capsOf({});
|
|
65
|
+
(0, chai_1.expect)(caps[XenonCapabilityManager_1.XENON_CAPABILITIES.INTERCEPTOR_ENABLED]).to.equal(undefined);
|
|
66
|
+
(0, chai_1.expect)(caps[XenonCapabilityManager_1.XENON_CAPABILITIES.INTERCEPTOR_CAPTURE_BODIES]).to.equal(undefined);
|
|
67
|
+
});
|
|
68
|
+
it('reads an interceptor object without `enabled` as unset', () => {
|
|
69
|
+
const caps = capsOf({ 'xe:interceptor': { bufferSize: 3 } });
|
|
70
|
+
(0, chai_1.expect)(caps[XenonCapabilityManager_1.XENON_CAPABILITIES.INTERCEPTOR_ENABLED]).to.equal(undefined);
|
|
71
|
+
(0, chai_1.expect)((0, interceptorOptions_1.resolveInterceptorOptions)(caps, { enabled: true }).enabled).to.equal(true);
|
|
72
|
+
});
|
|
73
|
+
it("reads `enabled: 'false'` as off", () => {
|
|
74
|
+
const caps = capsOf({ 'xe:interceptor': { enabled: 'false' } });
|
|
75
|
+
(0, chai_1.expect)(caps[XenonCapabilityManager_1.XENON_CAPABILITIES.INTERCEPTOR_ENABLED]).to.equal(false);
|
|
76
|
+
});
|
|
77
|
+
});
|