@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,181 @@
|
|
|
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 plugin_1 = require("../../src/plugin");
|
|
20
|
+
const ServerManager_1 = require("../../src/services/ServerManager");
|
|
21
|
+
const PluginContext_1 = require("../../src/PluginContext");
|
|
22
|
+
const concurrency_gate_1 = require("../../src/services/recording/concurrency-gate");
|
|
23
|
+
const config_1 = require("../../src/config");
|
|
24
|
+
const logger_1 = __importDefault(require("../../src/logger"));
|
|
25
|
+
/**
|
|
26
|
+
* `maxConcurrentRecordings` and `recordingsAssetsPath` are declared in
|
|
27
|
+
* schema.json, so Appium accepts them in a config file, but nothing read them:
|
|
28
|
+
* only the XENON_MAX_CONCURRENT_RECORDINGS and XENON_RECORDINGS_ASSETS_PATH
|
|
29
|
+
* environment variables reached src/config.ts. The option now wins, the
|
|
30
|
+
* variable is the fallback, and both are applied at boot before anything that
|
|
31
|
+
* reads them (the artifact store, the concurrency gate).
|
|
32
|
+
*/
|
|
33
|
+
describe('the recording options', () => {
|
|
34
|
+
let saved;
|
|
35
|
+
let env;
|
|
36
|
+
let statics;
|
|
37
|
+
beforeEach(() => {
|
|
38
|
+
saved = { max: config_1.config.maxConcurrentRecordings, path: config_1.config.recordingsAssetsPath };
|
|
39
|
+
env = {
|
|
40
|
+
max: process.env.XENON_MAX_CONCURRENT_RECORDINGS,
|
|
41
|
+
path: process.env.XENON_RECORDINGS_ASSETS_PATH,
|
|
42
|
+
};
|
|
43
|
+
delete process.env.XENON_MAX_CONCURRENT_RECORDINGS;
|
|
44
|
+
delete process.env.XENON_RECORDINGS_ASSETS_PATH;
|
|
45
|
+
statics = {
|
|
46
|
+
nodeId: plugin_1.XenonPlugin.NODE_ID,
|
|
47
|
+
port: plugin_1.XenonPlugin.port,
|
|
48
|
+
basePath: plugin_1.XenonPlugin.nodeBasePath,
|
|
49
|
+
};
|
|
50
|
+
});
|
|
51
|
+
afterEach(() => {
|
|
52
|
+
sinon_1.default.restore();
|
|
53
|
+
(0, config_1.updateConfig)({ maxConcurrentRecordings: saved.max, recordingsAssetsPath: saved.path });
|
|
54
|
+
for (const [name, value] of [
|
|
55
|
+
['XENON_MAX_CONCURRENT_RECORDINGS', env.max],
|
|
56
|
+
['XENON_RECORDINGS_ASSETS_PATH', env.path],
|
|
57
|
+
]) {
|
|
58
|
+
if (value === undefined)
|
|
59
|
+
delete process.env[name];
|
|
60
|
+
else
|
|
61
|
+
process.env[name] = value;
|
|
62
|
+
}
|
|
63
|
+
plugin_1.XenonPlugin.NODE_ID = statics.nodeId;
|
|
64
|
+
plugin_1.XenonPlugin.port = statics.port;
|
|
65
|
+
plugin_1.XenonPlugin.nodeBasePath = statics.basePath;
|
|
66
|
+
});
|
|
67
|
+
/**
|
|
68
|
+
* Boots as far as the database step and says what the options were when it
|
|
69
|
+
* began: the artifact store is built there from `recordingsAssetsPath`.
|
|
70
|
+
*/
|
|
71
|
+
function seenAtDatabaseStep(pluginArgs) {
|
|
72
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
73
|
+
const seen = {};
|
|
74
|
+
const boot = ServerManager_1.ServerManager.prototype;
|
|
75
|
+
sinon_1.default.stub(boot, 'resolvePluginArgs').resolves(pluginArgs);
|
|
76
|
+
sinon_1.default.stub(logger_1.default, 'banner');
|
|
77
|
+
sinon_1.default.stub(PluginContext_1.PluginContext.prototype, 'setContext');
|
|
78
|
+
sinon_1.default.stub(boot, 'syncDatabaseAndAIConfig').resolves();
|
|
79
|
+
sinon_1.default.stub(boot, 'initializeCoreSubsystems').callsFake(() => __awaiter(this, void 0, void 0, function* () {
|
|
80
|
+
seen.max = config_1.config.maxConcurrentRecordings;
|
|
81
|
+
seen.path = config_1.config.recordingsAssetsPath;
|
|
82
|
+
throw new Error('boot stopped by the test');
|
|
83
|
+
}));
|
|
84
|
+
yield typedi_1.Container.get(ServerManager_1.ServerManager)
|
|
85
|
+
.updateServer({}, null, { port: 4723 })
|
|
86
|
+
.catch((err) => (0, chai_1.expect)(err.message).to.equal('boot stopped by the test'));
|
|
87
|
+
return seen;
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
describe('applied at boot', () => {
|
|
91
|
+
it('takes the maxConcurrentRecordings option', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
92
|
+
const seen = yield seenAtDatabaseStep({ maxConcurrentRecordings: 2 });
|
|
93
|
+
(0, chai_1.expect)(seen.max).to.equal(2);
|
|
94
|
+
}));
|
|
95
|
+
it('takes the recordingsAssetsPath option', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
96
|
+
const seen = yield seenAtDatabaseStep({ recordingsAssetsPath: '/srv/xenon/recordings' });
|
|
97
|
+
(0, chai_1.expect)(seen.path).to.equal('/srv/xenon/recordings');
|
|
98
|
+
}));
|
|
99
|
+
it('falls back to the environment variables when the options are not set', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
100
|
+
process.env.XENON_MAX_CONCURRENT_RECORDINGS = '7';
|
|
101
|
+
process.env.XENON_RECORDINGS_ASSETS_PATH = '/mnt/recordings';
|
|
102
|
+
const seen = yield seenAtDatabaseStep({});
|
|
103
|
+
(0, chai_1.expect)(seen).to.deep.equal({ max: 7, path: '/mnt/recordings' });
|
|
104
|
+
}));
|
|
105
|
+
it('lets the option win over the environment variable', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
106
|
+
process.env.XENON_MAX_CONCURRENT_RECORDINGS = '7';
|
|
107
|
+
process.env.XENON_RECORDINGS_ASSETS_PATH = '/mnt/recordings';
|
|
108
|
+
const seen = yield seenAtDatabaseStep({
|
|
109
|
+
maxConcurrentRecordings: 3,
|
|
110
|
+
recordingsAssetsPath: '/srv/xenon/recordings',
|
|
111
|
+
});
|
|
112
|
+
(0, chai_1.expect)(seen).to.deep.equal({ max: 3, path: '/srv/xenon/recordings' });
|
|
113
|
+
}));
|
|
114
|
+
it('keeps the default cap of 4 when neither is set', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
115
|
+
const seen = yield seenAtDatabaseStep({});
|
|
116
|
+
(0, chai_1.expect)(seen.max).to.equal(4);
|
|
117
|
+
}));
|
|
118
|
+
it('ignores a cap that is not a whole number of at least 1', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
119
|
+
for (const bad of [0, -2, 2.5, NaN, '3', null]) {
|
|
120
|
+
const seen = yield seenAtDatabaseStep({ maxConcurrentRecordings: bad });
|
|
121
|
+
(0, chai_1.expect)(seen.max, `option ${JSON.stringify(bad)}`).to.equal(4);
|
|
122
|
+
sinon_1.default.restore();
|
|
123
|
+
}
|
|
124
|
+
}));
|
|
125
|
+
it('ignores an environment cap that is not a whole number of at least 1', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
126
|
+
process.env.XENON_MAX_CONCURRENT_RECORDINGS = 'many';
|
|
127
|
+
const seen = yield seenAtDatabaseStep({});
|
|
128
|
+
(0, chai_1.expect)(seen.max).to.equal(4);
|
|
129
|
+
}));
|
|
130
|
+
it('ignores a blank recordingsAssetsPath', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
131
|
+
const seen = yield seenAtDatabaseStep({ recordingsAssetsPath: ' ' });
|
|
132
|
+
(0, chai_1.expect)(seen.path).to.equal(saved.path);
|
|
133
|
+
}));
|
|
134
|
+
});
|
|
135
|
+
describe('the concurrency gate', () => {
|
|
136
|
+
it('reads the cap when it admits, not when it is built', () => {
|
|
137
|
+
(0, config_1.updateConfig)({ maxConcurrentRecordings: 4 });
|
|
138
|
+
const gate = new concurrency_gate_1.ConcurrencyGate();
|
|
139
|
+
(0, config_1.updateConfig)({ maxConcurrentRecordings: 1 });
|
|
140
|
+
(0, chai_1.expect)(gate.getLimit()).to.equal(1);
|
|
141
|
+
(0, chai_1.expect)(gate.tryAcquire(['r1'])).to.equal(true);
|
|
142
|
+
(0, chai_1.expect)(gate.tryAcquire(['r2'])).to.equal(false);
|
|
143
|
+
});
|
|
144
|
+
it('keeps an explicit limit over the configured one', () => {
|
|
145
|
+
(0, config_1.updateConfig)({ maxConcurrentRecordings: 4 });
|
|
146
|
+
(0, chai_1.expect)(new concurrency_gate_1.ConcurrencyGate(2).getLimit()).to.equal(2);
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
describe('resolving the recording options', () => {
|
|
151
|
+
describe('maxConcurrentRecordings', () => {
|
|
152
|
+
it('takes the option, then the variable, then 4', () => {
|
|
153
|
+
(0, chai_1.expect)((0, config_1.resolveMaxConcurrentRecordings)(2, '7')).to.equal(2);
|
|
154
|
+
(0, chai_1.expect)((0, config_1.resolveMaxConcurrentRecordings)(undefined, '7')).to.equal(7);
|
|
155
|
+
(0, chai_1.expect)((0, config_1.resolveMaxConcurrentRecordings)(undefined, undefined)).to.equal(4);
|
|
156
|
+
});
|
|
157
|
+
it('passes over a value that is not a whole number of at least 1', () => {
|
|
158
|
+
for (const bad of [0, -1, 1.5, NaN, Infinity, '2', null, {}]) {
|
|
159
|
+
(0, chai_1.expect)((0, config_1.resolveMaxConcurrentRecordings)(bad, '7'), JSON.stringify(bad)).to.equal(7);
|
|
160
|
+
}
|
|
161
|
+
for (const bad of ['', ' ', '0', '-3', '2.5', 'many', 'NaN']) {
|
|
162
|
+
(0, chai_1.expect)((0, config_1.resolveMaxConcurrentRecordings)(undefined, bad), JSON.stringify(bad)).to.equal(4);
|
|
163
|
+
}
|
|
164
|
+
});
|
|
165
|
+
});
|
|
166
|
+
describe('recordingsAssetsPath', () => {
|
|
167
|
+
it('takes the option, then the variable, then the default folder', () => {
|
|
168
|
+
(0, chai_1.expect)((0, config_1.resolveRecordingsAssetsPath)('/a', '/b')).to.equal('/a');
|
|
169
|
+
(0, chai_1.expect)((0, config_1.resolveRecordingsAssetsPath)(undefined, '/b')).to.equal('/b');
|
|
170
|
+
(0, chai_1.expect)((0, config_1.resolveRecordingsAssetsPath)(undefined, undefined)).to.match(/\.cache\/xenon\/assets\/sessions\/recordings$/);
|
|
171
|
+
});
|
|
172
|
+
it('counts a blank value as unset', () => {
|
|
173
|
+
(0, chai_1.expect)((0, config_1.resolveRecordingsAssetsPath)(' ', '/b')).to.equal('/b');
|
|
174
|
+
(0, chai_1.expect)((0, config_1.resolveRecordingsAssetsPath)(undefined, '')).to.match(/recordings$/);
|
|
175
|
+
(0, chai_1.expect)((0, config_1.resolveRecordingsAssetsPath)(42, undefined)).to.match(/recordings$/);
|
|
176
|
+
});
|
|
177
|
+
it('trims what it is given', () => {
|
|
178
|
+
(0, chai_1.expect)((0, config_1.resolveRecordingsAssetsPath)(' /a ', undefined)).to.equal('/a');
|
|
179
|
+
});
|
|
180
|
+
});
|
|
181
|
+
});
|
|
@@ -0,0 +1,75 @@
|
|
|
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 plugin_1 = require("../../src/plugin");
|
|
20
|
+
const ServerManager_1 = require("../../src/services/ServerManager");
|
|
21
|
+
const PluginContext_1 = require("../../src/PluginContext");
|
|
22
|
+
const SelfHealingSwitch_1 = require("../../src/services/settings/SelfHealingSwitch");
|
|
23
|
+
const logger_1 = __importDefault(require("../../src/logger"));
|
|
24
|
+
/**
|
|
25
|
+
* The self-healing value saved on the Settings page is read once at boot, so a
|
|
26
|
+
* restart doesn't put the server back on the option it was started with. It
|
|
27
|
+
* has to be loaded once the database is ready (migrations ran) and before the
|
|
28
|
+
* first route can answer or the first command arrive.
|
|
29
|
+
*/
|
|
30
|
+
describe('loading the saved self-healing setting, at boot', () => {
|
|
31
|
+
let statics;
|
|
32
|
+
beforeEach(() => {
|
|
33
|
+
statics = {
|
|
34
|
+
nodeId: plugin_1.XenonPlugin.NODE_ID,
|
|
35
|
+
port: plugin_1.XenonPlugin.port,
|
|
36
|
+
basePath: plugin_1.XenonPlugin.nodeBasePath,
|
|
37
|
+
};
|
|
38
|
+
});
|
|
39
|
+
afterEach(() => {
|
|
40
|
+
sinon_1.default.restore();
|
|
41
|
+
plugin_1.XenonPlugin.NODE_ID = statics.nodeId;
|
|
42
|
+
plugin_1.XenonPlugin.port = statics.port;
|
|
43
|
+
plugin_1.XenonPlugin.nodeBasePath = statics.basePath;
|
|
44
|
+
});
|
|
45
|
+
it('runs once the database is ready, before routes are registered', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
46
|
+
const steps = [];
|
|
47
|
+
const step = (name) => () => {
|
|
48
|
+
steps.push(name);
|
|
49
|
+
return Promise.resolve();
|
|
50
|
+
};
|
|
51
|
+
const boot = ServerManager_1.ServerManager.prototype;
|
|
52
|
+
sinon_1.default.stub(boot, 'resolvePluginArgs').resolves({});
|
|
53
|
+
sinon_1.default.stub(logger_1.default, 'banner');
|
|
54
|
+
sinon_1.default.stub(PluginContext_1.PluginContext.prototype, 'setContext');
|
|
55
|
+
sinon_1.default.stub(boot, 'syncDatabaseAndAIConfig').callsFake(step('database config'));
|
|
56
|
+
sinon_1.default.stub(boot, 'initializeCoreSubsystems').callsFake(step('database and migrations'));
|
|
57
|
+
// The real one kills every go-ios process on the machine.
|
|
58
|
+
sinon_1.default.stub(boot, 'reapLeftoverGoIos').callsFake(step('reap'));
|
|
59
|
+
sinon_1.default.stub(SelfHealingSwitch_1.SelfHealingSwitch.prototype, 'load').callsFake(step('self-healing setting'));
|
|
60
|
+
sinon_1.default.stub(boot, 'registerRoutes').callsFake(() => {
|
|
61
|
+
steps.push('routes');
|
|
62
|
+
throw new Error('boot stopped by the test');
|
|
63
|
+
});
|
|
64
|
+
yield typedi_1.Container.get(ServerManager_1.ServerManager)
|
|
65
|
+
.updateServer({}, null, { port: 4723 })
|
|
66
|
+
.then(() => chai_1.expect.fail('the stubbed routes step should have stopped the boot'), (err) => (0, chai_1.expect)(err.message).to.equal('boot stopped by the test'));
|
|
67
|
+
(0, chai_1.expect)(steps).to.deep.equal([
|
|
68
|
+
'database config',
|
|
69
|
+
'database and migrations',
|
|
70
|
+
'reap',
|
|
71
|
+
'self-healing setting',
|
|
72
|
+
'routes',
|
|
73
|
+
]);
|
|
74
|
+
}));
|
|
75
|
+
});
|
|
@@ -0,0 +1,272 @@
|
|
|
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 express_1 = __importDefault(require("express"));
|
|
18
|
+
const sinon_1 = __importDefault(require("sinon"));
|
|
19
|
+
const typedi_1 = require("typedi");
|
|
20
|
+
const loopbackRequest_1 = __importDefault(require("../helpers/loopbackRequest"));
|
|
21
|
+
const dashboard_1 = __importDefault(require("../../src/app/routers/dashboard"));
|
|
22
|
+
const PluginContext_1 = require("../../src/PluginContext");
|
|
23
|
+
const CommandInterceptor_1 = require("../../src/interceptors/CommandInterceptor");
|
|
24
|
+
const HealingOrchestrator_1 = require("../../src/services/healing/HealingOrchestrator");
|
|
25
|
+
const SelfHealingSwitch_1 = require("../../src/services/settings/SelfHealingSwitch");
|
|
26
|
+
const web_config_service_1 = require("../../src/data-service/web-config-service");
|
|
27
|
+
const IPluginArgs_1 = require("../../src/interfaces/IPluginArgs");
|
|
28
|
+
const event_manager_1 = require("../../src/dashboard/event-manager");
|
|
29
|
+
const SessionManager_1 = require("../../src/sessions/SessionManager");
|
|
30
|
+
const logger_1 = __importDefault(require("../../src/logger"));
|
|
31
|
+
const container_registration_1 = require("../helpers/container-registration");
|
|
32
|
+
const scratch_database_1 = require("../helpers/scratch-database");
|
|
33
|
+
/**
|
|
34
|
+
* The Settings page's "AI self-healing" toggle used to do nothing: POST /config
|
|
35
|
+
* stored only the fields its table knew, GET /config never sent the switch back
|
|
36
|
+
* (so the page always showed Enabled), and CommandInterceptor read the copy of
|
|
37
|
+
* the plugin options the server started with, which nothing updates.
|
|
38
|
+
*
|
|
39
|
+
* It follows the other lab settings now: a value saved in the dashboard wins
|
|
40
|
+
* over the startup option, which wins over the default, and applies to the very
|
|
41
|
+
* next command without a restart. The interceptor runs on every command, so it
|
|
42
|
+
* reads an in-memory value (SelfHealingSwitch), never the database.
|
|
43
|
+
*/
|
|
44
|
+
describe('the AI self-healing switch', () => {
|
|
45
|
+
const scratch = (0, scratch_database_1.useScratchDatabase)();
|
|
46
|
+
let context;
|
|
47
|
+
let startedWith;
|
|
48
|
+
let restoreContainer;
|
|
49
|
+
let healingSwitch;
|
|
50
|
+
const startup = (over = {}) => {
|
|
51
|
+
context.pluginArgs = Object.assign(Object.assign({}, IPluginArgs_1.DefaultPluginArgs), over);
|
|
52
|
+
return context.pluginArgs;
|
|
53
|
+
};
|
|
54
|
+
beforeEach(() => __awaiter(void 0, void 0, void 0, function* () {
|
|
55
|
+
context = typedi_1.Container.get(PluginContext_1.PluginContext);
|
|
56
|
+
startedWith = context.pluginArgs;
|
|
57
|
+
restoreContainer = (0, container_registration_1.saveRegistrations)(HealingOrchestrator_1.HealingOrchestrator, SelfHealingSwitch_1.SelfHealingSwitch);
|
|
58
|
+
// A switch of its own, as at boot, so no saved value leaks in from another spec.
|
|
59
|
+
typedi_1.Container.set(SelfHealingSwitch_1.SelfHealingSwitch, new SelfHealingSwitch_1.SelfHealingSwitch());
|
|
60
|
+
healingSwitch = typedi_1.Container.get(SelfHealingSwitch_1.SelfHealingSwitch);
|
|
61
|
+
yield scratch.db.webConfig.deleteMany({});
|
|
62
|
+
}));
|
|
63
|
+
afterEach(() => {
|
|
64
|
+
context.pluginArgs = startedWith;
|
|
65
|
+
restoreContainer();
|
|
66
|
+
sinon_1.default.restore();
|
|
67
|
+
});
|
|
68
|
+
function app() {
|
|
69
|
+
const a = (0, express_1.default)();
|
|
70
|
+
a.use(express_1.default.json());
|
|
71
|
+
a.use((req, _res, next) => {
|
|
72
|
+
req.auth = {
|
|
73
|
+
kind: 'user-session',
|
|
74
|
+
userId: 'usr_x',
|
|
75
|
+
role: 'SUPER_ADMIN',
|
|
76
|
+
scopes: 'admin,devices,sessions,read',
|
|
77
|
+
rateLimit: 1000,
|
|
78
|
+
};
|
|
79
|
+
next();
|
|
80
|
+
});
|
|
81
|
+
dashboard_1.default.register(a);
|
|
82
|
+
return a;
|
|
83
|
+
}
|
|
84
|
+
const save = (body) => (0, loopbackRequest_1.default)(app()).post('/config').send(body).timeout(5000);
|
|
85
|
+
const read = () => (0, loopbackRequest_1.default)(app()).get('/config').timeout(5000);
|
|
86
|
+
describe('as the Settings page saves and reads it', () => {
|
|
87
|
+
it('is kept: what POST /config saves, GET /config sends back', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
88
|
+
startup();
|
|
89
|
+
const saved = yield save({ enableSelfHealing: false });
|
|
90
|
+
(0, chai_1.expect)(saved.status).to.equal(200);
|
|
91
|
+
const shown = yield read();
|
|
92
|
+
(0, chai_1.expect)(shown.status).to.equal(200);
|
|
93
|
+
(0, chai_1.expect)(shown.body.enableSelfHealing).to.equal(false);
|
|
94
|
+
(0, chai_1.expect)(yield typedi_1.Container.get(web_config_service_1.WebConfigService).getConfig()).to.deep.equal({
|
|
95
|
+
enableSelfHealing: false,
|
|
96
|
+
});
|
|
97
|
+
}));
|
|
98
|
+
it('can be turned back on', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
99
|
+
startup();
|
|
100
|
+
yield save({ enableSelfHealing: false });
|
|
101
|
+
yield save({ enableSelfHealing: true });
|
|
102
|
+
(0, chai_1.expect)((yield read()).body.enableSelfHealing).to.equal(true);
|
|
103
|
+
}));
|
|
104
|
+
it('is on, and says so, when nothing was saved and the server was started with nothing', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
105
|
+
startup();
|
|
106
|
+
const shown = yield read();
|
|
107
|
+
(0, chai_1.expect)(shown.body.enableSelfHealing).to.equal(true);
|
|
108
|
+
(0, chai_1.expect)(shown.body.defaults.enableSelfHealing).to.equal(true);
|
|
109
|
+
}));
|
|
110
|
+
it('shows the option the server started with when nothing was saved', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
111
|
+
startup({ enableSelfHealing: false });
|
|
112
|
+
(0, chai_1.expect)((yield read()).body.enableSelfHealing).to.equal(false);
|
|
113
|
+
}));
|
|
114
|
+
it('shows a saved value over the option the server started with', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
115
|
+
startup({ enableSelfHealing: false });
|
|
116
|
+
yield save({ enableSelfHealing: true });
|
|
117
|
+
(0, chai_1.expect)((yield read()).body.enableSelfHealing).to.equal(true);
|
|
118
|
+
}));
|
|
119
|
+
it('is saved on its own, without touching the other settings', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
120
|
+
startup();
|
|
121
|
+
yield save({ buildCleanupDays: 7 });
|
|
122
|
+
yield save({ enableSelfHealing: false });
|
|
123
|
+
(0, chai_1.expect)(yield typedi_1.Container.get(web_config_service_1.WebConfigService).getConfig()).to.deep.equal({
|
|
124
|
+
buildCleanupDays: 7,
|
|
125
|
+
enableSelfHealing: false,
|
|
126
|
+
});
|
|
127
|
+
}));
|
|
128
|
+
for (const bad of ['false', 'no', 0, 1, null, {}, []]) {
|
|
129
|
+
it(`refuses ${JSON.stringify(bad)} with a 400 naming the setting, and saves nothing`, () => __awaiter(void 0, void 0, void 0, function* () {
|
|
130
|
+
startup();
|
|
131
|
+
const refused = yield save({ enableSelfHealing: bad, buildCleanupDays: 9 });
|
|
132
|
+
(0, chai_1.expect)(refused.status).to.equal(400);
|
|
133
|
+
(0, chai_1.expect)(refused.body).to.include({ error: 'invalid_setting', field: 'enableSelfHealing' });
|
|
134
|
+
(0, chai_1.expect)(yield typedi_1.Container.get(web_config_service_1.WebConfigService).getConfig()).to.deep.equal({});
|
|
135
|
+
}));
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
describe('on a running interceptor', () => {
|
|
139
|
+
const noSuchElement = () => Object.assign(new Error('NoSuchElement: An element could not be located'), {
|
|
140
|
+
name: 'NoSuchElementError',
|
|
141
|
+
});
|
|
142
|
+
let attemptHealing;
|
|
143
|
+
let pluginArgs;
|
|
144
|
+
beforeEach(() => {
|
|
145
|
+
pluginArgs = startup();
|
|
146
|
+
attemptHealing = sinon_1.default.stub().resolves(null);
|
|
147
|
+
typedi_1.Container.set(HealingOrchestrator_1.HealingOrchestrator, { attemptHealing });
|
|
148
|
+
});
|
|
149
|
+
/** A findElement the driver answers with NoSuchElement, as a broken selector does. */
|
|
150
|
+
function findMissing(sessionId) {
|
|
151
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
152
|
+
let thrown;
|
|
153
|
+
try {
|
|
154
|
+
yield typedi_1.Container.get(CommandInterceptor_1.CommandInterceptor).handle(() => __awaiter(this, void 0, void 0, function* () {
|
|
155
|
+
throw noSuchElement();
|
|
156
|
+
}), { sessionId }, 'findElement', ['xpath', '//android.widget.Button[@text="Gone"]', sessionId], pluginArgs, false);
|
|
157
|
+
}
|
|
158
|
+
catch (err) {
|
|
159
|
+
thrown = err;
|
|
160
|
+
}
|
|
161
|
+
return thrown;
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
it('heals a missing element while the switch is on, as it always did', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
165
|
+
const thrown = yield findMissing('sess-on');
|
|
166
|
+
(0, chai_1.expect)(attemptHealing.calledOnce).to.equal(true);
|
|
167
|
+
// Nothing healed it, so the test still sees its own NoSuchElement.
|
|
168
|
+
(0, chai_1.expect)(thrown === null || thrown === void 0 ? void 0 : thrown.name).to.equal('NoSuchElementError');
|
|
169
|
+
}));
|
|
170
|
+
it('stops healing for the next command once off is saved, with no restart', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
171
|
+
yield findMissing('sess-1');
|
|
172
|
+
(0, chai_1.expect)(attemptHealing.callCount).to.equal(1);
|
|
173
|
+
(0, chai_1.expect)((yield save({ enableSelfHealing: false })).status).to.equal(200);
|
|
174
|
+
const thrown = yield findMissing('sess-1');
|
|
175
|
+
(0, chai_1.expect)(attemptHealing.callCount).to.equal(1);
|
|
176
|
+
// The failure reaches the test untouched, which is the point of turning it off.
|
|
177
|
+
(0, chai_1.expect)(thrown === null || thrown === void 0 ? void 0 : thrown.name).to.equal('NoSuchElementError');
|
|
178
|
+
}));
|
|
179
|
+
it('heals again once on is saved after off', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
180
|
+
yield save({ enableSelfHealing: false });
|
|
181
|
+
yield findMissing('sess-2');
|
|
182
|
+
(0, chai_1.expect)(attemptHealing.called).to.equal(false);
|
|
183
|
+
yield save({ enableSelfHealing: true });
|
|
184
|
+
yield findMissing('sess-2');
|
|
185
|
+
(0, chai_1.expect)(attemptHealing.calledOnce).to.equal(true);
|
|
186
|
+
}));
|
|
187
|
+
it('does not read the database for a command', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
188
|
+
yield save({ enableSelfHealing: false });
|
|
189
|
+
const reads = sinon_1.default.spy(typedi_1.Container.get(web_config_service_1.WebConfigService), 'getConfig');
|
|
190
|
+
yield findMissing('sess-3');
|
|
191
|
+
yield findMissing('sess-3');
|
|
192
|
+
(0, chai_1.expect)(reads.called).to.equal(false);
|
|
193
|
+
}));
|
|
194
|
+
it('follows the startup option when nothing is saved', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
195
|
+
pluginArgs = startup({ enableSelfHealing: false });
|
|
196
|
+
yield findMissing('sess-4');
|
|
197
|
+
(0, chai_1.expect)(attemptHealing.called).to.equal(false);
|
|
198
|
+
}));
|
|
199
|
+
it('lets a saved on beat a startup off', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
200
|
+
pluginArgs = startup({ enableSelfHealing: false });
|
|
201
|
+
yield save({ enableSelfHealing: true });
|
|
202
|
+
yield findMissing('sess-5');
|
|
203
|
+
(0, chai_1.expect)(attemptHealing.calledOnce).to.equal(true);
|
|
204
|
+
}));
|
|
205
|
+
it("still hands a session's own healing tiers to the healing it runs", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
206
|
+
sinon_1.default.stub(SessionManager_1.SESSION_MANAGER, 'getSession').returns({
|
|
207
|
+
getCapabilities: () => ({ 'xe:options': { healingTiers: [1, 2] } }),
|
|
208
|
+
});
|
|
209
|
+
const driver = { sessionId: 'sess-6' };
|
|
210
|
+
yield typedi_1.Container.get(CommandInterceptor_1.CommandInterceptor)
|
|
211
|
+
.handle(() => __awaiter(void 0, void 0, void 0, function* () {
|
|
212
|
+
throw noSuchElement();
|
|
213
|
+
}), driver, 'findElement', ['id', 'gone', 'sess-6'], pluginArgs, false)
|
|
214
|
+
.catch(() => undefined);
|
|
215
|
+
(0, chai_1.expect)(attemptHealing.calledOnce).to.equal(true);
|
|
216
|
+
(0, chai_1.expect)(attemptHealing.firstCall.args).to.deep.equal(['sess-6', driver, 'id', 'gone', [1, 2]]);
|
|
217
|
+
}));
|
|
218
|
+
describe('and what it learns from a found element', () => {
|
|
219
|
+
let learn;
|
|
220
|
+
beforeEach(() => {
|
|
221
|
+
sinon_1.default.stub(event_manager_1.DASHBORD_EVENT_MANAGER, 'afterSessionCommand').resolves();
|
|
222
|
+
learn = sinon_1.default.stub(CommandInterceptor_1.CommandInterceptor.prototype, 'triggerLearning');
|
|
223
|
+
});
|
|
224
|
+
const afterFind = () => typedi_1.Container.get(CommandInterceptor_1.CommandInterceptor).runPostCommandHooks('sess-7', 'findElement', {}, ['id', 'there'], { ELEMENT: 'el-1' }, pluginArgs);
|
|
225
|
+
it('learns the selector while the switch is on', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
226
|
+
yield afterFind();
|
|
227
|
+
(0, chai_1.expect)(learn.calledOnce).to.equal(true);
|
|
228
|
+
}));
|
|
229
|
+
it('learns nothing once off is saved', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
230
|
+
yield save({ enableSelfHealing: false });
|
|
231
|
+
yield afterFind();
|
|
232
|
+
(0, chai_1.expect)(learn.called).to.equal(false);
|
|
233
|
+
}));
|
|
234
|
+
});
|
|
235
|
+
});
|
|
236
|
+
describe('at boot', () => {
|
|
237
|
+
it('is the saved value, over the startup option', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
238
|
+
const pluginArgs = startup({ enableSelfHealing: true });
|
|
239
|
+
yield scratch.db.webConfig.create({
|
|
240
|
+
data: { id: 'enableSelfHealing', name: 'enableSelfHealing', value: 'false' },
|
|
241
|
+
});
|
|
242
|
+
yield healingSwitch.load();
|
|
243
|
+
(0, chai_1.expect)(healingSwitch.isEnabled(pluginArgs)).to.equal(false);
|
|
244
|
+
}));
|
|
245
|
+
it('is the startup option when nothing is saved', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
246
|
+
yield healingSwitch.load();
|
|
247
|
+
(0, chai_1.expect)(healingSwitch.isEnabled(startup({ enableSelfHealing: false }))).to.equal(false);
|
|
248
|
+
(0, chai_1.expect)(healingSwitch.isEnabled(startup({ enableSelfHealing: true }))).to.equal(true);
|
|
249
|
+
}));
|
|
250
|
+
it('is on when neither was set', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
251
|
+
yield healingSwitch.load();
|
|
252
|
+
(0, chai_1.expect)(healingSwitch.isEnabled({})).to.equal(true);
|
|
253
|
+
}));
|
|
254
|
+
it('is the startup option, and says so once, when the saved value cannot be read', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
255
|
+
const warn = sinon_1.default.stub(logger_1.default, 'warn');
|
|
256
|
+
sinon_1.default.stub(typedi_1.Container.get(web_config_service_1.WebConfigService), 'getConfig').rejects(new Error('db is down'));
|
|
257
|
+
yield healingSwitch.load();
|
|
258
|
+
const off = startup({ enableSelfHealing: false });
|
|
259
|
+
const on = startup({ enableSelfHealing: true });
|
|
260
|
+
(0, chai_1.expect)(healingSwitch.isEnabled(off)).to.equal(false);
|
|
261
|
+
(0, chai_1.expect)(healingSwitch.isEnabled(on)).to.equal(true);
|
|
262
|
+
// However many commands follow, it is said once, at the load.
|
|
263
|
+
(0, chai_1.expect)(warn.calledOnce).to.equal(true);
|
|
264
|
+
(0, chai_1.expect)(String(warn.firstCall.args[0])).to.match(/self-healing/i);
|
|
265
|
+
(0, chai_1.expect)(String(warn.firstCall.args[0])).to.include('db is down');
|
|
266
|
+
}));
|
|
267
|
+
it('uses the startup option until it has loaded', () => {
|
|
268
|
+
(0, chai_1.expect)(healingSwitch.isEnabled({ enableSelfHealing: false })).to.equal(false);
|
|
269
|
+
(0, chai_1.expect)(healingSwitch.isEnabled({ enableSelfHealing: true })).to.equal(true);
|
|
270
|
+
});
|
|
271
|
+
});
|
|
272
|
+
});
|