@xenon-device-management/xenon 2.15.0 → 2.17.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 +38 -26
- package/lib/package.json +2 -1
- package/lib/public/assets/{AnnotationOverlay-Btq3dZRI.js → AnnotationOverlay-CjmMCS0S.js} +1 -1
- package/lib/public/assets/{ApiKeyGate-C9OoAgg_.js → ApiKeyGate-B4Ob0v3C.js} +1 -1
- package/lib/public/assets/{DeviceMosaicView-DBC_xczB.js → DeviceMosaicView-DTVOB_NO.js} +1 -1
- package/lib/public/assets/{EmptyState-DqWU2-vM.js → EmptyState-DcEG-oAi.js} +1 -1
- package/lib/public/assets/{FieldGroup-CveQ3xrA.js → FieldGroup-CtnCDshE.js} +1 -1
- package/lib/public/assets/{FilterMenu-BfKDSfHz.js → FilterMenu-BK0dE9cc.js} +1 -1
- package/lib/public/assets/{Menu-CtdCybzA.js → Menu-eKsmLpKS.js} +1 -1
- package/lib/public/assets/{Modal-Bsvj1tm2.js → Modal-DPwop3Lz.js} +1 -1
- package/lib/public/assets/{RecordingPage-CRofOiyx.js → RecordingPage-CfgSkZkB.js} +1 -1
- package/lib/public/assets/{RecordingsPage-jNQHb2IR.js → RecordingsPage-Bzl9pTUY.js} +1 -1
- package/lib/public/assets/{SegmentedControl-C8HtTha-.js → SegmentedControl-B_TZTrPm.js} +1 -1
- package/lib/public/assets/{SettingCard-D8K1LZNo.js → SettingCard-DlmvZJPM.js} +1 -1
- package/lib/public/assets/{Table-zNTX92Pn.js → Table-BigPWKXi.js} +1 -1
- package/lib/public/assets/{activity-CJ8LwZq9.js → activity-CvuZdQYu.js} +1 -1
- package/lib/public/assets/{ai-settings-CgxQZqrc.js → ai-settings-lI2UJ2n7.js} +1 -1
- package/lib/public/assets/{api-keys-ECpo-zTo.js → api-keys-CPgf9XJM.js} +1 -1
- package/lib/public/assets/{apps-BjGymyf3.js → apps-DIzOvtUp.js} +1 -1
- package/lib/public/assets/{arrow-left-BWfCbeHP.js → arrow-left-DiP5ywBT.js} +1 -1
- package/lib/public/assets/{arrow-right-DC7UGf_n.js → arrow-right-DtW_8hm5.js} +1 -1
- package/lib/public/assets/{arrow-up-right-BOjwybDk.js → arrow-up-right-C89FbNU0.js} +1 -1
- package/lib/public/assets/{auth-shell-CxMTo9aH.js → auth-shell-B7sUDb--.js} +2 -2
- package/lib/public/assets/{builds-page-DOCm4HkA.js → builds-page-BBhvrMfA.js} +1 -1
- package/lib/public/assets/{button-Cg08-EMT.js → button-DugsdUw1.js} +1 -1
- package/lib/public/assets/{calendar-CA1ikLJs.js → calendar-grKd08ZH.js} +1 -1
- package/lib/public/assets/{check-BMTaH72W.js → check-ORjyNfnX.js} +1 -1
- package/lib/public/assets/{chevron-right-B2QOv9Nx.js → chevron-right-Ei2FxUXq.js} +1 -1
- package/lib/public/assets/{circle-check-CYXBoUqT.js → circle-check-Day0HjW4.js} +1 -1
- package/lib/public/assets/{circle-x-DTwnSAnl.js → circle-x-CHxSRLHp.js} +1 -1
- package/lib/public/assets/{clock-C8_HViQP.js → clock-BDHewWTx.js} +1 -1
- package/lib/public/assets/{copy-Be7LNMXV.js → copy-CTwT4I3D.js} +1 -1
- package/lib/public/assets/{device-explorer-BOqtfGXz.js → device-explorer-DlhrKJ-g.js} +1 -1
- package/lib/public/assets/{download-B56lwxWP.js → download-D3bjz_vr.js} +1 -1
- package/lib/public/assets/{forgot-password-CCU6bzLn.js → forgot-password-BNBLi023.js} +1 -1
- package/lib/public/assets/{h264Stream-C-OtmIuk.js → h264Stream-C51Au2i_.js} +1 -1
- package/lib/public/assets/index-Dk5J6HB1.js +219 -0
- package/lib/public/assets/{index-B9KQ6LsY.js → index-DncSz5LG.js} +1 -1
- package/lib/public/assets/{index-CfxsUslp.js → index-Z7_BY3pJ.js} +1 -1
- package/lib/public/assets/{input-CZHFjbSg.js → input-bTXM4l09.js} +1 -1
- package/lib/public/assets/{line-chart-Gl_Dz3X_.js → line-chart-B3YmVYyr.js} +1 -1
- package/lib/public/assets/{list-checks-CCgjeyGY.js → list-checks-BClKEj1-.js} +1 -1
- package/lib/public/assets/{lock-CkOwgDcx.js → lock-CZsenX3D.js} +1 -1
- package/lib/public/assets/{login-D5cc4w7M.js → login-B6A6tOp9.js} +1 -1
- package/lib/public/assets/{maintenance-settings-t7qr3fvX.js → maintenance-settings-CmCJGy7O.js} +1 -1
- package/lib/public/assets/{monitor-DEJixKQO.js → monitor-BdTNEglU.js} +1 -1
- package/lib/public/assets/{mouse-pointer-2-CtOrk4M8.js → mouse-pointer-2-BM8CaLtt.js} +1 -1
- package/lib/public/assets/{network-Dii9qdvk.js → network-BS6vUc1x.js} +1 -1
- package/lib/public/assets/{overview-rolAACrO.js → overview-Yfk_Y7g3.js} +1 -1
- package/lib/public/assets/{page-header-COQZxJfR.js → page-header-DhXCwpml.js} +1 -1
- package/lib/public/assets/{play-DrN1Se_R.js → play-D08gP1td.js} +1 -1
- package/lib/public/assets/{plus-5aq-9uRx.js → plus-BKboorfa.js} +1 -1
- package/lib/public/assets/{profile-page-6i5kY6kF.js → profile-page-Cd9NodrA.js} +1 -1
- package/lib/public/assets/{reset-password-sp2IBrc7.js → reset-password-acLDyyDW.js} +1 -1
- package/lib/public/assets/{runbook-page-DJDjeopU.js → runbook-page-CWLkC1Fd.js} +153 -102
- package/lib/public/assets/{select-Bpyn7rWO.js → select-DZoRKNti.js} +1 -1
- package/lib/public/assets/{selector-detail-redirect-BH74vmKv.js → selector-detail-redirect-DS4qrt84.js} +1 -1
- package/lib/public/assets/{selector-health-page-Cw_g218H.js → selector-health-page-TpmN61G3.js} +1 -1
- package/lib/public/assets/session-detail-page-C0wYaXqy.js +4 -0
- package/lib/public/assets/{settings-C3uyGhyc.js → settings-CxPGEY2j.js} +1 -1
- package/lib/public/assets/{stat-tile-xVqAKIlI.js → stat-tile-DUpyYgl-.js} +1 -1
- package/lib/public/assets/{tablet-BxUScAeO.js → tablet-SA02BJNQ.js} +1 -1
- package/lib/public/assets/{teams-B5ljGkY2.js → teams-jamD0Kxr.js} +1 -1
- package/lib/public/assets/{trash-2-B701i8sy.js → trash-2-DO8fv6w4.js} +1 -1
- package/lib/public/assets/{upload-C2gr44RS.js → upload-CaiGCStb.js} +1 -1
- package/lib/public/assets/{use-builds-data-NeQOFEoy.js → use-builds-data-jS-fcvVf.js} +1 -1
- package/lib/public/assets/{use-password-reset-mode-DEzJq5ef.js → use-password-reset-mode-BzoenegM.js} +1 -1
- package/lib/public/assets/{users-MY6eNe6s.js → users-Bnw7Vs_B.js} +1 -1
- package/lib/public/assets/{users-D_vv_yz4.js → users-DHvDrriG.js} +1 -1
- package/lib/public/assets/{video-off-OK1lNZQn.js → video-off-FEvy0w5U.js} +1 -1
- package/lib/public/assets/{webhook-settings-BU-C9lBf.js → webhook-settings-CBVB5lxi.js} +1 -1
- package/lib/public/assets/{zap-DO9ry1iz.js → zap-BMrTJO6M.js} +1 -1
- package/lib/public/index.html +1 -1
- package/lib/schema.json +11 -7
- package/lib/src/InternalHttpClient.js +27 -5
- package/lib/src/XenonCapabilityManager.js +10 -3
- package/lib/src/app/index.js +2 -0
- package/lib/src/app/openapi/grid.yaml +102 -0
- package/lib/src/app/openapi/sessions.yaml +7 -5
- package/lib/src/app/routers/auth.js +2 -1
- package/lib/src/app/routers/users.js +2 -1
- package/lib/src/app/ws/nodeSocketRelay.js +77 -11
- package/lib/src/dashboard/commandLogFields.js +42 -0
- package/lib/src/dashboard/event-manager.js +23 -154
- package/lib/src/dashboard/services/failure-analysis-service.js +8 -86
- package/lib/src/dashboard/services/failureCategories.js +264 -0
- package/lib/src/device-managers/AndroidDeviceManager.js +61 -23
- package/lib/src/device-managers/IOSDeviceManager.js +10 -0
- package/lib/src/device-utils.js +25 -8
- package/lib/src/gateway/forwardToNode.js +4 -11
- package/lib/src/gateway/hubSessionToken.js +27 -4
- package/lib/src/gateway/nodeAsk.js +57 -0
- package/lib/src/gateway/nodeSessionDeviceLogs.js +35 -0
- package/lib/src/gateway/nodeWebDriverUrl.js +3 -5
- package/lib/src/helpers/outboundProxy.js +219 -0
- package/lib/src/index.js +21 -0
- package/lib/src/interceptors/CommandInterceptor.js +13 -15
- package/lib/src/interceptors/xenonScripts.js +28 -7
- package/lib/src/logger.js +3 -1
- package/lib/src/plugin.js +12 -4
- package/lib/src/scripts/database-check.js +3 -3
- package/lib/src/scripts/initialize-database.js +29 -33
- package/lib/src/scripts/run-migrations.js +320 -25
- package/lib/src/services/ApiKeyService.js +16 -5
- package/lib/src/services/InterceptorService.js +6 -2
- package/lib/src/services/OrphanSweeper.js +6 -0
- package/lib/src/services/SelectorStateService.js +11 -7
- package/lib/src/services/SelectorVerificationJob.js +6 -2
- package/lib/src/services/SessionLifecycleService.js +52 -23
- package/lib/src/services/SocketClient.js +11 -0
- package/lib/src/services/SocketServer.js +361 -42
- package/lib/src/services/TeamService.js +5 -1
- package/lib/src/services/TracingService.js +171 -60
- package/lib/src/services/UserService.js +14 -3
- package/lib/src/services/UserSessionService.js +16 -4
- package/lib/src/services/healing/HealingOrchestrator.js +3 -1
- package/lib/src/services/identity/identityChanges.js +41 -0
- package/lib/src/services/interceptor/CertManager.js +50 -16
- package/lib/src/services/logcat/NodeDeviceLogStore.js +99 -0
- package/lib/src/services/logcat/NodeDeviceLogsCollector.js +164 -0
- package/lib/src/services/logcat/SessionDeviceLogs.js +279 -37
- package/lib/src/services/logcat/deviceLogBook.js +5 -4
- package/lib/src/services/logcat/iosDriverLog.js +178 -0
- package/lib/src/services/logcat/nodeDeviceLogs.js +91 -0
- package/lib/src/services/metrics/nodeMetrics.js +20 -49
- package/lib/src/services/selector-health/SelectorVisibilityResolver.js +121 -0
- package/lib/src/services/token/JwtKeyService.js +8 -2
- package/lib/src/services/token/mcpTokenTtl.js +27 -0
- package/lib/src/sessions/LocalSession.js +29 -1
- package/lib/src/sessions/RemoteSession.js +23 -4
- package/lib/src/sessions/SessionManager.js +33 -9
- package/lib/test/helpers/driver-log.js +38 -0
- package/lib/test/helpers/fake-http-proxy.js +106 -0
- package/lib/test/helpers/fake-logcat.js +80 -0
- package/lib/test/helpers/hub-node-pair.js +227 -0
- package/lib/test/helpers/otlp-probe.js +107 -0
- package/lib/test/integration/androidDevices.spec.js +1 -1
- package/lib/test/integration/ios/01iOSSimulator.spec.js +1 -1
- package/lib/test/integration/ios/02iOSDevices.spec.js +1 -1
- package/lib/test/integration/selector-state-flow.spec.js +5 -5
- package/lib/test/unit/XenonCapabilityManager.spec.js +23 -0
- package/lib/test/unit/android-adb-retry.spec.js +112 -0
- package/lib/test/unit/command-log-network-capture.spec.js +191 -0
- package/lib/test/unit/dashboard-events-device-logs.spec.js +74 -11
- package/lib/test/unit/dashboard-events-team-scope.spec.js +6 -3
- package/lib/test/unit/database-check.spec.js +1 -0
- package/lib/test/unit/device-utils.spec.js +13 -4
- package/lib/test/unit/event-log-session-data.spec.js +11 -4
- package/lib/test/unit/failure-analysis.spec.js +22 -8
- package/lib/test/unit/failure-categories.spec.js +24 -22
- package/lib/test/unit/failure-categorization.spec.js +393 -0
- package/lib/test/unit/healing-tier-gate.spec.js +3 -2
- package/lib/test/unit/hub-node-gateway-appium-server.spec.js +72 -1
- package/lib/test/unit/hub-node-iphone-readiness.spec.js +134 -0
- package/lib/test/unit/hub-node-session-device-logs.spec.js +194 -0
- package/lib/test/unit/hub-node-session-metrics.spec.js +24 -192
- package/lib/test/unit/hub-remote-session-heartbeat.spec.js +8 -1
- package/lib/test/unit/interceptor/CertManager.spec.js +89 -0
- package/lib/test/unit/ios-driver-log.spec.js +168 -0
- package/lib/test/unit/ios-simulator-session-readiness.spec.js +187 -0
- package/lib/test/unit/ios-simulator-session-teardown.spec.js +158 -0
- package/lib/test/unit/logcat-ws.spec.js +78 -37
- package/lib/test/unit/logger-json-levels.spec.js +44 -0
- package/lib/test/unit/mcp-token-ttl.spec.js +38 -0
- package/lib/test/unit/node-device-log-store.spec.js +102 -0
- package/lib/test/unit/node-device-logs-collector.spec.js +343 -0
- package/lib/test/unit/node-gateway-create-appium-server.spec.js +8 -1
- package/lib/test/unit/node-hub-session-owner.spec.js +8 -1
- package/lib/test/unit/node-session-device-logs-route.spec.js +103 -0
- package/lib/test/unit/observability-example-loki.spec.js +25 -0
- package/lib/test/unit/openapi-for-site.spec.js +80 -0
- package/lib/test/unit/outbound-proxy.spec.js +320 -0
- package/lib/test/unit/port-exhaustion.spec.js +2 -1
- package/lib/test/unit/proxy-option.spec.js +358 -0
- package/lib/test/unit/run-migrations-database.spec.js +440 -0
- package/lib/test/unit/run-migrations-fail-fast.test.js +41 -27
- package/lib/test/unit/run-migrations.test.js +264 -0
- package/lib/test/unit/selector-health-actions.spec.js +3 -1
- package/lib/test/unit/selector-state-service.spec.js +72 -23
- package/lib/test/unit/selector-verification-clean-build.spec.js +1 -1
- package/lib/test/unit/selector-verification-job.spec.js +15 -6
- package/lib/test/unit/selector-verification-missed-regression.spec.js +2 -2
- package/lib/test/unit/selector-visibility-resolver.spec.js +209 -0
- package/lib/test/unit/session-device-logs-hub-node.spec.js +340 -0
- package/lib/test/unit/session-device-logs-ios.spec.js +180 -0
- package/lib/test/unit/session-device-logs-lifecycle.spec.js +137 -0
- package/lib/test/unit/session-device-logs.spec.js +43 -96
- package/lib/test/unit/session-end-failure-analysis.spec.js +5 -5
- package/lib/test/unit/session-log-indexes.spec.js +16 -1
- package/lib/test/unit/socket-team-scope.integration.spec.js +749 -0
- package/lib/test/unit/socketServer.scopedEmit.spec.js +237 -5
- package/lib/test/unit/tracing-export-routing.spec.js +137 -0
- package/lib/test/unit/tracing-session-spans.spec.js +337 -0
- package/lib/test/unit/unexpected-shutdown-reason.spec.js +2 -2
- package/lib/tsconfig.tsbuildinfo +1 -1
- package/package.json +2 -1
- package/schema.json +11 -7
- package/scripts/dev/readme-screenshots.js +276 -23
- package/scripts/dev/screenshot-fixtures/logcat.js +157 -0
- package/scripts/dev/screenshot-fixtures/phone-screen.html +469 -0
- package/scripts/dev/screenshot-fixtures/session.js +242 -0
- package/scripts/export-openapi.js +32 -0
- package/scripts/lib/openapi-for-site.js +79 -0
- package/lib/public/assets/index-BOxe43p1.js +0 -219
- package/lib/public/assets/session-detail-page-AZJE6XXE.js +0 -4
- package/lib/src/scripts/prepare-prisma.js +0 -89
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
5
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
6
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
7
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
8
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
|
+
});
|
|
10
|
+
};
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
require("reflect-metadata");
|
|
13
|
+
const chai_1 = require("chai");
|
|
14
|
+
const typedi_1 = require("typedi");
|
|
15
|
+
const container_registration_1 = require("../helpers/container-registration");
|
|
16
|
+
const scratch_database_1 = require("../helpers/scratch-database");
|
|
17
|
+
const selector_health_fixture_1 = require("../helpers/selector-health-fixture");
|
|
18
|
+
const SelectorVisibilityResolver_1 = require("../../src/services/selector-health/SelectorVisibilityResolver");
|
|
19
|
+
const KEY = { strategy: 'xpath', selector: '//x' };
|
|
20
|
+
const MEMBER_A = { userId: 'u-a', teamIds: ['team-a'] };
|
|
21
|
+
describe('SelectorVisibilityResolver: who may see a selector, for its live events', () => {
|
|
22
|
+
it('TypeDI builds it, as the socket server gets it (Container.get)', () => {
|
|
23
|
+
// A function-typed constructor parameter would make Container.get throw.
|
|
24
|
+
const restore = (0, container_registration_1.saveRegistrations)(SelectorVisibilityResolver_1.SelectorVisibilityResolver);
|
|
25
|
+
try {
|
|
26
|
+
(0, chai_1.expect)(typedi_1.Container.get(SelectorVisibilityResolver_1.SelectorVisibilityResolver)).to.be.instanceOf(SelectorVisibilityResolver_1.SelectorVisibilityResolver);
|
|
27
|
+
}
|
|
28
|
+
finally {
|
|
29
|
+
restore();
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
describe('caching', () => {
|
|
33
|
+
let clock;
|
|
34
|
+
let scopes;
|
|
35
|
+
let checks;
|
|
36
|
+
let visible;
|
|
37
|
+
function resolver(over = {}) {
|
|
38
|
+
return new SelectorVisibilityResolver_1.SelectorVisibilityResolver(Object.assign({ sessionScope: (viewer) => __awaiter(this, void 0, void 0, function* () {
|
|
39
|
+
var _a;
|
|
40
|
+
scopes.push((_a = viewer.userId) !== null && _a !== void 0 ? _a : '');
|
|
41
|
+
return { OR: [{ device_udid: { in: viewer.teamIds } }] };
|
|
42
|
+
}), healedIn: (key) => __awaiter(this, void 0, void 0, function* () {
|
|
43
|
+
checks.push(key.selector);
|
|
44
|
+
return visible;
|
|
45
|
+
}), now: () => clock }, over));
|
|
46
|
+
}
|
|
47
|
+
beforeEach(() => {
|
|
48
|
+
clock = 1000000;
|
|
49
|
+
scopes = [];
|
|
50
|
+
checks = [];
|
|
51
|
+
visible = true;
|
|
52
|
+
});
|
|
53
|
+
it('asks once per viewer and selector while the answer is fresh, then again', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
54
|
+
const r = resolver();
|
|
55
|
+
(0, chai_1.expect)(yield r.canSee(KEY, MEMBER_A)).to.equal(true);
|
|
56
|
+
visible = false;
|
|
57
|
+
(0, chai_1.expect)(yield r.canSee(KEY, MEMBER_A), 'still the cached answer').to.equal(true);
|
|
58
|
+
(0, chai_1.expect)(checks).to.have.length(1);
|
|
59
|
+
clock += SelectorVisibilityResolver_1.SELECTOR_VISIBILITY_TTL_MS;
|
|
60
|
+
(0, chai_1.expect)(yield r.canSee(KEY, MEMBER_A), 'asked again after the TTL').to.equal(false);
|
|
61
|
+
(0, chai_1.expect)(checks).to.have.length(2);
|
|
62
|
+
}));
|
|
63
|
+
it('concurrent questions share one lookup', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
64
|
+
const r = resolver();
|
|
65
|
+
const answers = yield Promise.all([
|
|
66
|
+
r.canSee(KEY, MEMBER_A),
|
|
67
|
+
r.canSee(KEY, { userId: 'u-a', teamIds: ['team-a'] }),
|
|
68
|
+
r.canSee(KEY, MEMBER_A),
|
|
69
|
+
]);
|
|
70
|
+
(0, chai_1.expect)(answers).to.deep.equal([true, true, true]);
|
|
71
|
+
(0, chai_1.expect)(checks).to.have.length(1);
|
|
72
|
+
(0, chai_1.expect)(scopes).to.have.length(1);
|
|
73
|
+
}));
|
|
74
|
+
it("a viewer's sessions are read once for all their selectors", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
75
|
+
const r = resolver();
|
|
76
|
+
yield r.canSee(KEY, MEMBER_A);
|
|
77
|
+
yield r.canSee({ strategy: 'id', selector: 'other' }, MEMBER_A);
|
|
78
|
+
(0, chai_1.expect)(scopes).to.deep.equal(['u-a']);
|
|
79
|
+
(0, chai_1.expect)(checks).to.deep.equal(['//x', 'other']);
|
|
80
|
+
}));
|
|
81
|
+
it('the same teams in another order are the same viewer; another user or team is not', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
82
|
+
const r = resolver();
|
|
83
|
+
yield r.canSee(KEY, { userId: 'u-a', teamIds: ['team-a', 'team-b'] });
|
|
84
|
+
yield r.canSee(KEY, { userId: 'u-a', teamIds: ['team-b', 'team-a'] });
|
|
85
|
+
(0, chai_1.expect)(checks, 'team order').to.have.length(1);
|
|
86
|
+
yield r.canSee(KEY, { userId: 'u-b', teamIds: ['team-a', 'team-b'] });
|
|
87
|
+
yield r.canSee(KEY, { userId: 'u-a', teamIds: ['team-a'] });
|
|
88
|
+
(0, chai_1.expect)(checks, 'another user, other teams').to.have.length(3);
|
|
89
|
+
}));
|
|
90
|
+
it('a selector with no strategy is its own selector, not one with the same text', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
91
|
+
const r = resolver();
|
|
92
|
+
yield r.canSee({ strategy: '', selector: '//x' }, MEMBER_A);
|
|
93
|
+
yield r.canSee({ strategy: 'xpath', selector: '//x' }, MEMBER_A);
|
|
94
|
+
(0, chai_1.expect)(checks).to.have.length(2);
|
|
95
|
+
}));
|
|
96
|
+
it('a lookup that fails answers no, and is asked again next time', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
97
|
+
let fail = true;
|
|
98
|
+
const r = resolver({
|
|
99
|
+
healedIn: (key) => __awaiter(void 0, void 0, void 0, function* () {
|
|
100
|
+
checks.push(key.selector);
|
|
101
|
+
if (fail)
|
|
102
|
+
throw new Error('database gone');
|
|
103
|
+
return true;
|
|
104
|
+
}),
|
|
105
|
+
});
|
|
106
|
+
(0, chai_1.expect)(yield r.canSee(KEY, MEMBER_A)).to.equal(false);
|
|
107
|
+
fail = false;
|
|
108
|
+
(0, chai_1.expect)(yield r.canSee(KEY, MEMBER_A)).to.equal(true);
|
|
109
|
+
(0, chai_1.expect)(checks).to.have.length(2);
|
|
110
|
+
}));
|
|
111
|
+
it("a viewer's sessions that can't be read answer no, and are read again next time", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
112
|
+
let fail = true;
|
|
113
|
+
const r = resolver({
|
|
114
|
+
sessionScope: (viewer) => __awaiter(void 0, void 0, void 0, function* () {
|
|
115
|
+
var _a;
|
|
116
|
+
scopes.push((_a = viewer.userId) !== null && _a !== void 0 ? _a : '');
|
|
117
|
+
if (fail)
|
|
118
|
+
throw new Error('database gone');
|
|
119
|
+
return undefined;
|
|
120
|
+
}),
|
|
121
|
+
});
|
|
122
|
+
(0, chai_1.expect)(yield r.canSee(KEY, MEMBER_A)).to.equal(false);
|
|
123
|
+
fail = false;
|
|
124
|
+
(0, chai_1.expect)(yield r.canSee(KEY, MEMBER_A)).to.equal(true);
|
|
125
|
+
(0, chai_1.expect)(scopes).to.have.length(2);
|
|
126
|
+
}));
|
|
127
|
+
it('a lookup that hangs answers no after the timeout, and is asked again next time', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
128
|
+
let hang = true;
|
|
129
|
+
const r = resolver({
|
|
130
|
+
healedIn: (key) => {
|
|
131
|
+
checks.push(key.selector);
|
|
132
|
+
return hang ? new Promise(() => undefined) : Promise.resolve(true);
|
|
133
|
+
},
|
|
134
|
+
lookupTimeoutMs: 20,
|
|
135
|
+
});
|
|
136
|
+
(0, chai_1.expect)(yield r.canSee(KEY, MEMBER_A)).to.equal(false);
|
|
137
|
+
hang = false;
|
|
138
|
+
(0, chai_1.expect)(yield r.canSee(KEY, MEMBER_A)).to.equal(true);
|
|
139
|
+
(0, chai_1.expect)(checks).to.have.length(2);
|
|
140
|
+
}));
|
|
141
|
+
});
|
|
142
|
+
describe("REST's rule, on a real database (Selector Health's own fixture)", () => {
|
|
143
|
+
const scratch = (0, scratch_database_1.useScratchDatabase)({ wholeSuite: true });
|
|
144
|
+
const UNPLUGGED = 'sh-phone-unplugged';
|
|
145
|
+
before(() => __awaiter(void 0, void 0, void 0, function* () {
|
|
146
|
+
yield (0, selector_health_fixture_1.seedSelectorHealth)(scratch.db);
|
|
147
|
+
// Priya's session on a phone that has since gone: no Device row.
|
|
148
|
+
yield scratch.db.session.create({
|
|
149
|
+
data: {
|
|
150
|
+
id: 'sh-s-unplugged',
|
|
151
|
+
device_udid: UNPLUGGED,
|
|
152
|
+
device_name: 'Gone',
|
|
153
|
+
device_platform: 'android',
|
|
154
|
+
desired_capabilities: '{}',
|
|
155
|
+
session_capabilities: '{}',
|
|
156
|
+
node_id: 'localhost',
|
|
157
|
+
has_live_video: false,
|
|
158
|
+
device_version: '14',
|
|
159
|
+
status: 'passed',
|
|
160
|
+
user_id: selector_health_fixture_1.USER.priya,
|
|
161
|
+
},
|
|
162
|
+
});
|
|
163
|
+
yield scratch.db.sessionLog.create({
|
|
164
|
+
data: {
|
|
165
|
+
session_id: 'sh-s-unplugged',
|
|
166
|
+
url: '/element',
|
|
167
|
+
method: 'POST',
|
|
168
|
+
title: 'findElement',
|
|
169
|
+
response: '{}',
|
|
170
|
+
command_name: 'findElement',
|
|
171
|
+
is_healed: true,
|
|
172
|
+
original_strategy: 'xpath',
|
|
173
|
+
original_selector: '//unplugged/only',
|
|
174
|
+
},
|
|
175
|
+
});
|
|
176
|
+
}));
|
|
177
|
+
const xpath = (selector) => ({ strategy: 'xpath', selector });
|
|
178
|
+
const teamA = { userId: selector_health_fixture_1.USER.priya, teamIds: [selector_health_fixture_1.TEAM.a] };
|
|
179
|
+
const teamB = { userId: selector_health_fixture_1.USER.alex, teamIds: [selector_health_fixture_1.TEAM.b] };
|
|
180
|
+
const noTeam = { userId: 'sh-u-nobody', teamIds: [] };
|
|
181
|
+
it("a team's member sees the selectors healed on their team's phone or a shared one, not another team's", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
182
|
+
const r = new SelectorVisibilityResolver_1.SelectorVisibilityResolver();
|
|
183
|
+
(0, chai_1.expect)(yield r.canSee({ strategy: 'id', selector: selector_health_fixture_1.SEL.warm }, teamA), 'team A phone').to.equal(true);
|
|
184
|
+
(0, chai_1.expect)(yield r.canSee(xpath(selector_health_fixture_1.SEL.hot), teamA), 'shared phone').to.equal(true);
|
|
185
|
+
(0, chai_1.expect)(yield r.canSee(xpath(selector_health_fixture_1.SEL.bOnly), teamA), "team B's phone").to.equal(false);
|
|
186
|
+
(0, chai_1.expect)(yield r.canSee(xpath(selector_health_fixture_1.SEL.bOnly), teamB)).to.equal(true);
|
|
187
|
+
(0, chai_1.expect)(yield r.canSee({ strategy: 'id', selector: selector_health_fixture_1.SEL.warm }, teamB)).to.equal(false);
|
|
188
|
+
}));
|
|
189
|
+
it('a member of no team sees only what healed on a shared phone', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
190
|
+
const r = new SelectorVisibilityResolver_1.SelectorVisibilityResolver();
|
|
191
|
+
(0, chai_1.expect)(yield r.canSee(xpath(selector_health_fixture_1.SEL.hot), noTeam)).to.equal(true);
|
|
192
|
+
(0, chai_1.expect)(yield r.canSee(xpath(selector_health_fixture_1.SEL.muted), noTeam)).to.equal(false);
|
|
193
|
+
(0, chai_1.expect)(yield r.canSee(xpath(selector_health_fixture_1.SEL.mutedB), noTeam)).to.equal(false);
|
|
194
|
+
}));
|
|
195
|
+
it('a heal recorded with no strategy is strategy ""', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
196
|
+
const r = new SelectorVisibilityResolver_1.SelectorVisibilityResolver();
|
|
197
|
+
(0, chai_1.expect)(yield r.canSee({ strategy: '', selector: selector_health_fixture_1.SEL.legacy }, noTeam)).to.equal(true);
|
|
198
|
+
}));
|
|
199
|
+
it("a phone that has gone leaves its sessions' selectors to their owner", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
200
|
+
const r = new SelectorVisibilityResolver_1.SelectorVisibilityResolver();
|
|
201
|
+
(0, chai_1.expect)(yield r.canSee(xpath('//unplugged/only'), teamA), 'Priya owns it').to.equal(true);
|
|
202
|
+
(0, chai_1.expect)(yield r.canSee(xpath('//unplugged/only'), teamB)).to.equal(false);
|
|
203
|
+
}));
|
|
204
|
+
it('an unknown selector is seen by no one', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
205
|
+
const r = new SelectorVisibilityResolver_1.SelectorVisibilityResolver();
|
|
206
|
+
(0, chai_1.expect)(yield r.canSee(xpath('//never/healed'), teamA)).to.equal(false);
|
|
207
|
+
}));
|
|
208
|
+
});
|
|
209
|
+
});
|
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
5
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
6
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
7
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
8
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
|
+
});
|
|
10
|
+
};
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
require("reflect-metadata");
|
|
13
|
+
const chai_1 = require("chai");
|
|
14
|
+
const SessionDeviceLogs_1 = require("../../src/services/logcat/SessionDeviceLogs");
|
|
15
|
+
const NodeDeviceLogStore_1 = require("../../src/services/logcat/NodeDeviceLogStore");
|
|
16
|
+
const NodeDeviceLogsCollector_1 = require("../../src/services/logcat/NodeDeviceLogsCollector");
|
|
17
|
+
const nodeDeviceLogs_1 = require("../../src/services/logcat/nodeDeviceLogs");
|
|
18
|
+
const deviceLogBook_1 = require("../../src/services/logcat/deviceLogBook");
|
|
19
|
+
const scratch_database_1 = require("../helpers/scratch-database");
|
|
20
|
+
const fake_logcat_1 = require("../helpers/fake-logcat");
|
|
21
|
+
const driver_log_1 = require("../helpers/driver-log");
|
|
22
|
+
const quiet = { info: () => undefined, warn: () => undefined, debug: () => undefined };
|
|
23
|
+
/** A node's service: its own phone's log stream fake, its rows held in `store`. */
|
|
24
|
+
class NodeSideDeviceLogs extends SessionDeviceLogs_1.SessionDeviceLogs {
|
|
25
|
+
constructor(logcat, store, unclaimed = 60000) {
|
|
26
|
+
super();
|
|
27
|
+
this.logcat = logcat;
|
|
28
|
+
this.store = store;
|
|
29
|
+
this.unclaimed = unclaimed;
|
|
30
|
+
this.writes = 0;
|
|
31
|
+
}
|
|
32
|
+
context() {
|
|
33
|
+
return {
|
|
34
|
+
pluginArgs: { bindHostOrIp: '127.0.0.1', hub: 'http://127.0.0.1:4724' },
|
|
35
|
+
port: 4725,
|
|
36
|
+
nodeId: 'node-1',
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
streams() {
|
|
40
|
+
return this.logcat;
|
|
41
|
+
}
|
|
42
|
+
readClock() {
|
|
43
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
44
|
+
return deviceLogBook_1.UNKNOWN_CLOCK;
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
nodeStore() {
|
|
48
|
+
return this.store;
|
|
49
|
+
}
|
|
50
|
+
unclaimedMs() {
|
|
51
|
+
return this.unclaimed;
|
|
52
|
+
}
|
|
53
|
+
writeLines() {
|
|
54
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
55
|
+
this.writes += 1;
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
/** A hub's service: no phone of its own here; it asks a node every 20 ms. */
|
|
60
|
+
class HubSideDeviceLogs extends SessionDeviceLogs_1.SessionDeviceLogs {
|
|
61
|
+
context() {
|
|
62
|
+
return { pluginArgs: { bindHostOrIp: '127.0.0.1' }, port: 4724, nodeId: 'hub-1' };
|
|
63
|
+
}
|
|
64
|
+
collectorFor(source, onLines, storedTail) {
|
|
65
|
+
return new NodeDeviceLogsCollector_1.NodeDeviceLogsCollector({
|
|
66
|
+
source,
|
|
67
|
+
onLines,
|
|
68
|
+
storedTail,
|
|
69
|
+
support: new nodeDeviceLogs_1.NodeDeviceLogsSupport(),
|
|
70
|
+
logger: quiet,
|
|
71
|
+
intervalMs: 20,
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
/** The phone as the node files it, and as the hub has it from the node's report. */
|
|
76
|
+
const NODE_PHONE = {
|
|
77
|
+
udid: 'phone-1',
|
|
78
|
+
platform: 'android',
|
|
79
|
+
host: 'http://127.0.0.1:4725',
|
|
80
|
+
nodeId: 'node-1',
|
|
81
|
+
};
|
|
82
|
+
/** The node as the hub's collector sees it: the node's own store, behind its route. */
|
|
83
|
+
const nodeAt = (store, sessionId) => ({
|
|
84
|
+
nodeOrigin: () => 'http://127.0.0.1:4725',
|
|
85
|
+
nodeDeviceLogs: (after) => __awaiter(void 0, void 0, void 0, function* () { return ({ kind: 'answer', answer: store.read(sessionId, after) }); }),
|
|
86
|
+
});
|
|
87
|
+
const row = (n) => ({
|
|
88
|
+
message: `line ${n}`,
|
|
89
|
+
timestamp: new Date(1700000000000 + n),
|
|
90
|
+
});
|
|
91
|
+
describe('A node records its phone’s device log for its hub', function () {
|
|
92
|
+
this.timeout(60000);
|
|
93
|
+
let logcat;
|
|
94
|
+
let store;
|
|
95
|
+
beforeEach(() => {
|
|
96
|
+
logcat = new fake_logcat_1.FakeLogcatStreams();
|
|
97
|
+
store = new NodeDeviceLogStore_1.NodeDeviceLogStore();
|
|
98
|
+
});
|
|
99
|
+
afterEach(() => logcat.cleanup());
|
|
100
|
+
it('holds the lines for the hub, whatever its dashboard setting, and writes none', () => __awaiter(this, void 0, void 0, function* () {
|
|
101
|
+
const logs = new NodeSideDeviceLogs(logcat, store);
|
|
102
|
+
const since = Date.now() - 60000;
|
|
103
|
+
yield logs.start({ sessionId: 'n1', device: NODE_PHONE, since });
|
|
104
|
+
logcat.proc.stdout.write((0, fake_logcat_1.lineAt)(since - 600000, 'before the session'));
|
|
105
|
+
logcat.proc.stdout.write((0, fake_logcat_1.lineAt)(since + 1000, 'first'));
|
|
106
|
+
logcat.proc.stdout.write((0, fake_logcat_1.lineAt)(since + 2000, 'FATAL EXCEPTION: main', 'E', 'AndroidRuntime'));
|
|
107
|
+
yield (0, fake_logcat_1.settle)();
|
|
108
|
+
const held = store.read('n1', null);
|
|
109
|
+
(0, chai_1.expect)(held.state).to.equal('recording');
|
|
110
|
+
(0, chai_1.expect)(held.lines.map((l) => l.seq)).to.deep.equal([1, 2]);
|
|
111
|
+
(0, chai_1.expect)(held.lines[1].message).to.match(/ E AndroidRuntime: FATAL EXCEPTION: main$/);
|
|
112
|
+
(0, chai_1.expect)(held.lines[1].timestamp).to.equal(since + 2000);
|
|
113
|
+
yield logs.stop('n1');
|
|
114
|
+
(0, chai_1.expect)(store.read('n1', 2).state).to.equal('ended');
|
|
115
|
+
(0, chai_1.expect)(logs.writes).to.equal(0);
|
|
116
|
+
}));
|
|
117
|
+
it('holds an iPhone’s lines from its driver’s log, for the hub', () => __awaiter(this, void 0, void 0, function* () {
|
|
118
|
+
const logs = new NodeSideDeviceLogs(logcat, store);
|
|
119
|
+
const driverLog = new driver_log_1.TestDriverLog();
|
|
120
|
+
const iphone = Object.assign(Object.assign({}, NODE_PHONE), { udid: '00008110-000A', platform: 'ios', realDevice: true });
|
|
121
|
+
yield logs.start({
|
|
122
|
+
sessionId: 'n-ios',
|
|
123
|
+
device: iphone,
|
|
124
|
+
since: Date.now() - 1000,
|
|
125
|
+
driverLog,
|
|
126
|
+
});
|
|
127
|
+
driverLog.line('Oct 5 20:31:07 iPhone Shop[4127] <Error>: payment failed');
|
|
128
|
+
const held = store.read('n-ios', null);
|
|
129
|
+
(0, chai_1.expect)(held.state).to.equal('recording');
|
|
130
|
+
(0, chai_1.expect)(held.lines.map((l) => l.message)).to.deep.equal([
|
|
131
|
+
'Oct 5 20:31:07 iPhone Shop[4127] <Error>: payment failed',
|
|
132
|
+
]);
|
|
133
|
+
(0, chai_1.expect)(logs.writes).to.equal(0);
|
|
134
|
+
yield logs.stop('n-ios');
|
|
135
|
+
(0, chai_1.expect)(store.read('n-ios', 1).state).to.equal('ended');
|
|
136
|
+
}));
|
|
137
|
+
it('keeps the session’s line limit, so a hub is sent no more than its own phone’s session keeps', () => __awaiter(this, void 0, void 0, function* () {
|
|
138
|
+
const logs = new NodeSideDeviceLogs(logcat, store);
|
|
139
|
+
const since = Date.now() - 60000;
|
|
140
|
+
yield logs.start({ sessionId: 'n2', device: NODE_PHONE, since });
|
|
141
|
+
for (let i = 0; i < deviceLogBook_1.DEVICE_LOG_LINE_LIMIT + 5; i++) {
|
|
142
|
+
logcat.proc.stdout.write((0, fake_logcat_1.lineAt)(since + 1000 + i, `line ${i}`));
|
|
143
|
+
}
|
|
144
|
+
logcat.proc.stdout.write((0, fake_logcat_1.lineAt)(since + 30000, 'the end', 'E', 'AndroidRuntime'));
|
|
145
|
+
const held = () => store.entries.get('n2').lines.length;
|
|
146
|
+
yield (0, fake_logcat_1.until)(() => held() === deviceLogBook_1.DEVICE_LOG_LINE_LIMIT + 2, 20000);
|
|
147
|
+
yield logs.stop('n2');
|
|
148
|
+
let after = null;
|
|
149
|
+
const all = [];
|
|
150
|
+
for (;;) {
|
|
151
|
+
const page = store.read('n2', after);
|
|
152
|
+
all.push(...page.lines.map((l) => l.message));
|
|
153
|
+
if (!page.more)
|
|
154
|
+
break;
|
|
155
|
+
after = page.lines[page.lines.length - 1].seq;
|
|
156
|
+
}
|
|
157
|
+
// The limit, a note that only errors are kept from there, the error, and
|
|
158
|
+
// a note that the 5 other lines were left out.
|
|
159
|
+
(0, chai_1.expect)(all).to.have.length(deviceLogBook_1.DEVICE_LOG_LINE_LIMIT + 3);
|
|
160
|
+
(0, chai_1.expect)(all[deviceLogBook_1.DEVICE_LOG_LINE_LIMIT]).to.contain('only errors are kept');
|
|
161
|
+
(0, chai_1.expect)(all[deviceLogBook_1.DEVICE_LOG_LINE_LIMIT + 1]).to.match(/ E AndroidRuntime: the end$/);
|
|
162
|
+
(0, chai_1.expect)(all[deviceLogBook_1.DEVICE_LOG_LINE_LIMIT + 2]).to.contain('5 device log lines were left out');
|
|
163
|
+
}));
|
|
164
|
+
it('stops recording a session no hub has asked about', () => __awaiter(this, void 0, void 0, function* () {
|
|
165
|
+
var _a, _b, _c;
|
|
166
|
+
const logs = new NodeSideDeviceLogs(logcat, store, 30);
|
|
167
|
+
yield logs.start({ sessionId: 'n3', device: NODE_PHONE, since: Date.now() });
|
|
168
|
+
(0, chai_1.expect)((_a = logcat.getMultiplexer(NODE_PHONE.udid)) === null || _a === void 0 ? void 0 : _a.clientCount).to.equal(1);
|
|
169
|
+
yield (0, fake_logcat_1.until)(() => !logs.isRecording('n3'));
|
|
170
|
+
(0, chai_1.expect)((_c = (_b = logcat.getMultiplexer(NODE_PHONE.udid)) === null || _b === void 0 ? void 0 : _b.clientCount) !== null && _c !== void 0 ? _c : 0).to.equal(0);
|
|
171
|
+
(0, chai_1.expect)(store.read('n3', null)).to.deep.equal({ state: 'off', lines: [], more: false });
|
|
172
|
+
// Ending it later does nothing more.
|
|
173
|
+
yield logs.stop('n3');
|
|
174
|
+
(0, chai_1.expect)(store.read('n3', null).state).to.equal('off');
|
|
175
|
+
}));
|
|
176
|
+
it('records to the end once a hub has asked', () => __awaiter(this, void 0, void 0, function* () {
|
|
177
|
+
const logs = new NodeSideDeviceLogs(logcat, store, 30);
|
|
178
|
+
const since = Date.now() - 1000;
|
|
179
|
+
yield logs.start({ sessionId: 'n4', device: NODE_PHONE, since });
|
|
180
|
+
store.read('n4', null);
|
|
181
|
+
yield new Promise((r) => setTimeout(r, 80));
|
|
182
|
+
(0, chai_1.expect)(logs.isRecording('n4')).to.equal(true);
|
|
183
|
+
logcat.proc.stdout.write((0, fake_logcat_1.lineAt)(Date.now(), 'still here'));
|
|
184
|
+
yield (0, fake_logcat_1.settle)();
|
|
185
|
+
(0, chai_1.expect)(store.read('n4', null).lines.map((l) => l.message)).to.have.length(1);
|
|
186
|
+
yield logs.stop('n4');
|
|
187
|
+
}));
|
|
188
|
+
it('keeps a session that ended before any hub asked, for the hub’s last ask', () => __awaiter(this, void 0, void 0, function* () {
|
|
189
|
+
const logs = new NodeSideDeviceLogs(logcat, store, 30);
|
|
190
|
+
const since = Date.now() - 1000;
|
|
191
|
+
yield logs.start({ sessionId: 'n5', device: NODE_PHONE, since });
|
|
192
|
+
logcat.proc.stdout.write((0, fake_logcat_1.lineAt)(Date.now(), 'a short session'));
|
|
193
|
+
yield (0, fake_logcat_1.settle)();
|
|
194
|
+
yield logs.stop('n5');
|
|
195
|
+
yield new Promise((r) => setTimeout(r, 80));
|
|
196
|
+
const held = store.read('n5', null);
|
|
197
|
+
(0, chai_1.expect)(held.state).to.equal('ended');
|
|
198
|
+
(0, chai_1.expect)(held.lines).to.have.length(1);
|
|
199
|
+
}));
|
|
200
|
+
});
|
|
201
|
+
describe('A hub writes a node session’s device log', function () {
|
|
202
|
+
this.timeout(60000);
|
|
203
|
+
const scratch = (0, scratch_database_1.useScratchDatabase)();
|
|
204
|
+
let node;
|
|
205
|
+
let count = 0;
|
|
206
|
+
let sessionId;
|
|
207
|
+
beforeEach(() => __awaiter(this, void 0, void 0, function* () {
|
|
208
|
+
count += 1;
|
|
209
|
+
sessionId = `hub-dl-${count}`;
|
|
210
|
+
node = new NodeDeviceLogStore_1.NodeDeviceLogStore();
|
|
211
|
+
node.begin(sessionId);
|
|
212
|
+
yield scratch.db.session.create({
|
|
213
|
+
data: {
|
|
214
|
+
id: sessionId,
|
|
215
|
+
device_udid: NODE_PHONE.udid,
|
|
216
|
+
device_platform: 'android',
|
|
217
|
+
device_version: '10',
|
|
218
|
+
desired_capabilities: '{}',
|
|
219
|
+
session_capabilities: '{}',
|
|
220
|
+
node_id: 'node-1',
|
|
221
|
+
has_live_video: false,
|
|
222
|
+
},
|
|
223
|
+
});
|
|
224
|
+
}));
|
|
225
|
+
const stored = () => scratch.db.log.findMany({
|
|
226
|
+
where: { session_id: sessionId, log_type: 'DEVICE' },
|
|
227
|
+
orderBy: { createdAt: 'asc' },
|
|
228
|
+
});
|
|
229
|
+
it('collects only a node’s phone, through its session, and never a cloud provider’s', () => {
|
|
230
|
+
const hub = new HubSideDeviceLogs();
|
|
231
|
+
const source = nodeAt(node, sessionId);
|
|
232
|
+
(0, chai_1.expect)(hub.appliesTo(NODE_PHONE, source)).to.equal(true);
|
|
233
|
+
(0, chai_1.expect)(hub.appliesTo(NODE_PHONE)).to.equal(false);
|
|
234
|
+
(0, chai_1.expect)(hub.appliesTo(Object.assign(Object.assign({}, NODE_PHONE), { cloud: 'browserstack' }), source)).to.equal(false);
|
|
235
|
+
(0, chai_1.expect)(hub.appliesTo(Object.assign(Object.assign({}, NODE_PHONE), { platform: 'ios' }), source)).to.equal(true);
|
|
236
|
+
(0, chai_1.expect)(hub.appliesTo(Object.assign(Object.assign({}, NODE_PHONE), { platform: 'windows' }), source)).to.equal(false);
|
|
237
|
+
// A node never collects from another server.
|
|
238
|
+
(0, chai_1.expect)(new NodeSideDeviceLogs(new fake_logcat_1.FakeLogcatStreams(), node).appliesTo(Object.assign(Object.assign({}, NODE_PHONE), { nodeId: 'node-2', host: 'http://10.0.0.9:4725' }), source)).to.equal(false);
|
|
239
|
+
});
|
|
240
|
+
it('writes the lines as the node gives them, in its order, and the last ones at the end', () => __awaiter(this, void 0, void 0, function* () {
|
|
241
|
+
const hub = new HubSideDeviceLogs();
|
|
242
|
+
node.add(sessionId, [row(1), row(2)]);
|
|
243
|
+
yield hub.start({ sessionId, device: NODE_PHONE, source: nodeAt(node, sessionId) });
|
|
244
|
+
yield (0, fake_logcat_1.until)(() => node.claimed(sessionId));
|
|
245
|
+
yield new Promise((r) => setTimeout(r, 50));
|
|
246
|
+
(0, chai_1.expect)((yield stored()).map((r) => r.message)).to.deep.equal(['line 1', 'line 2']);
|
|
247
|
+
// The node's last lines, then its session ends before the hub's.
|
|
248
|
+
node.add(sessionId, [row(3), row(4)]);
|
|
249
|
+
node.end(sessionId);
|
|
250
|
+
yield hub.stop(sessionId);
|
|
251
|
+
const rows = yield stored();
|
|
252
|
+
(0, chai_1.expect)(rows.map((r) => r.message)).to.deep.equal(['line 1', 'line 2', 'line 3', 'line 4']);
|
|
253
|
+
// Each line keeps the node's time; each row its own `createdAt`, in order.
|
|
254
|
+
(0, chai_1.expect)(rows.map((r) => r.timestamp.getTime())).to.deep.equal([1, 2, 3, 4].map((n) => row(n).timestamp.getTime()));
|
|
255
|
+
const created = rows.map((r) => r.createdAt.getTime());
|
|
256
|
+
(0, chai_1.expect)(created.every((t, i) => i === 0 || t > created[i - 1]), String(created)).to.equal(true);
|
|
257
|
+
}));
|
|
258
|
+
it('goes on after a restart after the newest line stored, without writing it twice', () => __awaiter(this, void 0, void 0, function* () {
|
|
259
|
+
// Before the restart the hub wrote lines 1 to 3, the last two from an
|
|
260
|
+
// answer whose lines the node still holds: it drops them only when the
|
|
261
|
+
// next ask names them.
|
|
262
|
+
const later = Date.now() + 60000;
|
|
263
|
+
yield scratch.db.log.createMany({
|
|
264
|
+
data: [1, 2, 3].map((n) => ({
|
|
265
|
+
session_id: sessionId,
|
|
266
|
+
log_type: 'DEVICE',
|
|
267
|
+
message: row(n).message,
|
|
268
|
+
timestamp: row(n).timestamp,
|
|
269
|
+
createdAt: new Date(later + n),
|
|
270
|
+
})),
|
|
271
|
+
});
|
|
272
|
+
node.add(sessionId, [1, 2, 3, 4, 5].map(row));
|
|
273
|
+
node.read(sessionId, 1);
|
|
274
|
+
const hub = new HubSideDeviceLogs();
|
|
275
|
+
yield hub.start({
|
|
276
|
+
sessionId,
|
|
277
|
+
device: NODE_PHONE,
|
|
278
|
+
source: nodeAt(node, sessionId),
|
|
279
|
+
resume: true,
|
|
280
|
+
});
|
|
281
|
+
node.end(sessionId);
|
|
282
|
+
yield hub.stop(sessionId);
|
|
283
|
+
const rows = yield stored();
|
|
284
|
+
(0, chai_1.expect)(rows.map((r) => r.message)).to.deep.equal([1, 2, 3, 4, 5].map((n) => `line ${n}`));
|
|
285
|
+
(0, chai_1.expect)(rows[3].createdAt.getTime()).to.be.greaterThan(later + 3);
|
|
286
|
+
}));
|
|
287
|
+
it('a second stop waits for the first one’s last lines', () => __awaiter(this, void 0, void 0, function* () {
|
|
288
|
+
// A node slow to answer: the first stop is still in its last ask.
|
|
289
|
+
const slow = nodeAt(node, sessionId);
|
|
290
|
+
const source = {
|
|
291
|
+
nodeOrigin: slow.nodeOrigin,
|
|
292
|
+
nodeDeviceLogs: (after) => __awaiter(this, void 0, void 0, function* () {
|
|
293
|
+
yield new Promise((r) => setTimeout(r, 50));
|
|
294
|
+
return slow.nodeDeviceLogs(after);
|
|
295
|
+
}),
|
|
296
|
+
};
|
|
297
|
+
const hub = new HubSideDeviceLogs();
|
|
298
|
+
node.add(sessionId, [row(1)]);
|
|
299
|
+
yield hub.start({ sessionId, device: NODE_PHONE, source });
|
|
300
|
+
yield (0, fake_logcat_1.until)(() => node.claimed(sessionId));
|
|
301
|
+
node.add(sessionId, [row(2)]);
|
|
302
|
+
node.end(sessionId);
|
|
303
|
+
const first = hub.stop(sessionId);
|
|
304
|
+
// The failure analysis, after a second ending's stop, reads them all.
|
|
305
|
+
yield hub.stop(sessionId);
|
|
306
|
+
(0, chai_1.expect)((yield stored()).map((r) => r.message)).to.deep.equal(['line 1', 'line 2']);
|
|
307
|
+
yield first;
|
|
308
|
+
}));
|
|
309
|
+
it('takes the node’s last lines though the session ends while collection resumes', () => __awaiter(this, void 0, void 0, function* () {
|
|
310
|
+
class SlowToResume extends HubSideDeviceLogs {
|
|
311
|
+
storedTail(id) {
|
|
312
|
+
const _super = Object.create(null, {
|
|
313
|
+
storedTail: { get: () => super.storedTail }
|
|
314
|
+
});
|
|
315
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
316
|
+
yield new Promise((r) => setTimeout(r, 50));
|
|
317
|
+
return _super.storedTail.call(this, id);
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
const hub = new SlowToResume();
|
|
322
|
+
node.add(sessionId, [row(1), row(2)]);
|
|
323
|
+
node.end(sessionId);
|
|
324
|
+
void hub.start({
|
|
325
|
+
sessionId,
|
|
326
|
+
device: NODE_PHONE,
|
|
327
|
+
source: nodeAt(node, sessionId),
|
|
328
|
+
resume: true,
|
|
329
|
+
});
|
|
330
|
+
yield hub.stop(sessionId);
|
|
331
|
+
(0, chai_1.expect)((yield stored()).map((r) => r.message)).to.deep.equal(['line 1', 'line 2']);
|
|
332
|
+
}));
|
|
333
|
+
it('collects nothing from a node that doesn’t record the session', () => __awaiter(this, void 0, void 0, function* () {
|
|
334
|
+
const hub = new HubSideDeviceLogs();
|
|
335
|
+
node.forget(sessionId);
|
|
336
|
+
yield hub.start({ sessionId, device: NODE_PHONE, source: nodeAt(node, sessionId) });
|
|
337
|
+
yield hub.stop(sessionId);
|
|
338
|
+
(0, chai_1.expect)(yield stored()).to.deep.equal([]);
|
|
339
|
+
}));
|
|
340
|
+
});
|