@xenon-device-management/xenon 2.17.0 → 2.17.1
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/lib/package.json +2 -3
- package/lib/public/assets/{AnnotationOverlay-CjmMCS0S.js → AnnotationOverlay-CB8njQvS.js} +1 -1
- package/lib/public/assets/{ApiKeyGate-B4Ob0v3C.js → ApiKeyGate-B-804QBS.js} +1 -1
- package/lib/public/assets/DeviceMosaicView-BsDvfjR1.js +2 -0
- package/lib/public/assets/{EmptyState-DcEG-oAi.js → EmptyState-WzLw1cEV.js} +1 -1
- package/lib/public/assets/{FieldGroup-CtnCDshE.js → FieldGroup-CaQNXgcE.js} +1 -1
- package/lib/public/assets/{FilterMenu-BK0dE9cc.js → FilterMenu-C9gAiypL.js} +1 -1
- package/lib/public/assets/{Menu-eKsmLpKS.js → Menu-CfTti3rw.js} +1 -1
- package/lib/public/assets/{Modal-DPwop3Lz.js → Modal-BcZUFhDm.js} +1 -1
- package/lib/public/assets/{RecordingPage-CfgSkZkB.js → RecordingPage-B_pbl3UE.js} +1 -1
- package/lib/public/assets/{RecordingsPage-Bzl9pTUY.js → RecordingsPage-BkVhplE3.js} +1 -1
- package/lib/public/assets/{SegmentedControl-B_TZTrPm.js → SegmentedControl-CDLMLNl5.js} +1 -1
- package/lib/public/assets/{SettingCard-DlmvZJPM.js → SettingCard-Lyw5CB1F.js} +1 -1
- package/lib/public/assets/{Table-BigPWKXi.js → Table-CK414bOU.js} +1 -1
- package/lib/public/assets/{activity-CvuZdQYu.js → activity-pEyAdE-M.js} +1 -1
- package/lib/public/assets/{ai-settings-lI2UJ2n7.js → ai-settings-CIhS7MMq.js} +1 -1
- package/lib/public/assets/{api-keys-CPgf9XJM.js → api-keys-D2J44Ebv.js} +1 -1
- package/lib/public/assets/{apps-DIzOvtUp.js → apps-CCV3RZY8.js} +1 -1
- package/lib/public/assets/{arrow-left-DiP5ywBT.js → arrow-left-DaEUmERk.js} +1 -1
- package/lib/public/assets/{arrow-right-DtW_8hm5.js → arrow-right-CnRXHXit.js} +1 -1
- package/lib/public/assets/{arrow-up-right-C89FbNU0.js → arrow-up-right-BGCS7zhi.js} +1 -1
- package/lib/public/assets/{auth-shell-B7sUDb--.js → auth-shell-CpW1zsxD.js} +2 -2
- package/lib/public/assets/{builds-page-BBhvrMfA.js → builds-page-B-xdzjMx.js} +1 -1
- package/lib/public/assets/{button-DugsdUw1.js → button-B5QkOVa4.js} +1 -1
- package/lib/public/assets/{calendar-grKd08ZH.js → calendar-gX_1EKue.js} +1 -1
- package/lib/public/assets/{check-ORjyNfnX.js → check-MIM4THvH.js} +1 -1
- package/lib/public/assets/{chevron-right-Ei2FxUXq.js → chevron-right-BZIp3-9t.js} +1 -1
- package/lib/public/assets/{circle-check-Day0HjW4.js → circle-check-Crt-G_uy.js} +1 -1
- package/lib/public/assets/{circle-x-CHxSRLHp.js → circle-x-C8GvvXVt.js} +1 -1
- package/lib/public/assets/{clock-BDHewWTx.js → clock-MFKV4qGa.js} +1 -1
- package/lib/public/assets/{copy-CTwT4I3D.js → copy-BU-Z7mBK.js} +1 -1
- package/lib/public/assets/device-explorer-D-78Twa0.js +282 -0
- package/lib/public/assets/{download-D3bjz_vr.js → download-DIoLCCYv.js} +1 -1
- package/lib/public/assets/{forgot-password-BNBLi023.js → forgot-password-vNYlPnia.js} +1 -1
- package/lib/public/assets/{h264Stream-C51Au2i_.js → h264Stream-BLcSplMU.js} +1 -1
- package/lib/public/assets/{index-Dk5J6HB1.js → index-BpqyclNY.js} +2 -2
- package/lib/public/assets/{index-DncSz5LG.js → index-Hl1OL-k_.js} +1 -1
- package/lib/public/assets/{index-Z7_BY3pJ.js → index-QbukVVrl.js} +1 -1
- package/lib/public/assets/{input-bTXM4l09.js → input-DHH3fHer.js} +1 -1
- package/lib/public/assets/{line-chart-B3YmVYyr.js → line-chart-PdViEphw.js} +1 -1
- package/lib/public/assets/{list-checks-BClKEj1-.js → list-checks-DI_heIg9.js} +1 -1
- package/lib/public/assets/{lock-CZsenX3D.js → lock-DVP_dL6J.js} +1 -1
- package/lib/public/assets/{login-B6A6tOp9.js → login-B8vtTDcr.js} +1 -1
- package/lib/public/assets/{maintenance-settings-CmCJGy7O.js → maintenance-settings-BMa5EfgI.js} +1 -1
- package/lib/public/assets/{monitor-BdTNEglU.js → monitor-CtzX_RkD.js} +1 -1
- package/lib/public/assets/{mouse-pointer-2-BM8CaLtt.js → mouse-pointer-2-BA09sdS9.js} +1 -1
- package/lib/public/assets/{network-BS6vUc1x.js → network-B_F9Nox-.js} +1 -1
- package/lib/public/assets/{overview-Yfk_Y7g3.js → overview-kWCYMHMV.js} +1 -1
- package/lib/public/assets/{page-header-DhXCwpml.js → page-header-CAbH1O1a.js} +1 -1
- package/lib/public/assets/{play-D08gP1td.js → play-sg95hLqA.js} +1 -1
- package/lib/public/assets/{plus-BKboorfa.js → plus-BT9mk2gj.js} +1 -1
- package/lib/public/assets/{profile-page-Cd9NodrA.js → profile-page-DZOz5Ypc.js} +1 -1
- package/lib/public/assets/{reset-password-acLDyyDW.js → reset-password-CqeImlEO.js} +1 -1
- package/lib/public/assets/{runbook-page-CWLkC1Fd.js → runbook-page-U_8TSysj.js} +1 -1
- package/lib/public/assets/{select-DZoRKNti.js → select-Cx4JDzsR.js} +1 -1
- package/lib/public/assets/{selector-detail-redirect-DS4qrt84.js → selector-detail-redirect-DiMRe7DW.js} +1 -1
- package/lib/public/assets/{selector-health-page-TpmN61G3.js → selector-health-page-CKR5URll.js} +1 -1
- package/lib/public/assets/{session-detail-page-C0wYaXqy.js → session-detail-page-DuIVzTW_.js} +1 -1
- package/lib/public/assets/{settings-CxPGEY2j.js → settings-B5fuvgTP.js} +1 -1
- package/lib/public/assets/{stat-tile-DUpyYgl-.js → stat-tile-CtHX1dkh.js} +1 -1
- package/lib/public/assets/{tablet-SA02BJNQ.js → tablet-DCGD0GQt.js} +1 -1
- package/lib/public/assets/{teams-jamD0Kxr.js → teams-MzlELlw9.js} +1 -1
- package/lib/public/assets/{trash-2-DO8fv6w4.js → trash-2-BIRm2KVs.js} +1 -1
- package/lib/public/assets/{upload-CaiGCStb.js → upload-C-lgyswH.js} +1 -1
- package/lib/public/assets/{use-builds-data-jS-fcvVf.js → use-builds-data-IOydXA8d.js} +1 -1
- package/lib/public/assets/{use-password-reset-mode-BzoenegM.js → use-password-reset-mode-CIVXG7Me.js} +1 -1
- package/lib/public/assets/{users-Bnw7Vs_B.js → users-hwK5XGIy.js} +1 -1
- package/lib/public/assets/{users-DHvDrriG.js → users-ivNCjeus.js} +1 -1
- package/lib/public/assets/{video-off-FEvy0w5U.js → video-off-DvcIEsdZ.js} +1 -1
- package/lib/public/assets/{webhook-settings-CBVB5lxi.js → webhook-settings-DukgCe1u.js} +1 -1
- package/lib/public/assets/{zap-BMrTJO6M.js → zap-BE9gacFj.js} +1 -1
- package/lib/public/index.html +1 -1
- package/lib/src/app/openapi/control.yaml +69 -4
- package/lib/src/app/routers/control.js +24 -5
- package/lib/src/device-managers/ios/IOSStreamService.js +147 -18
- package/lib/src/device-utils.js +50 -22
- package/lib/src/gateway/internalCall.js +33 -5
- package/lib/src/gateway/sessionGateway.js +9 -4
- package/lib/src/helpers/index.js +9 -6
- package/lib/src/interceptors/CommandInterceptor.js +10 -0
- package/lib/src/services/ServerManager.js +25 -4
- package/lib/src/services/UserService.js +8 -3
- package/lib/src/sessions/appiumUmbrella.js +11 -0
- package/lib/src/sessions/sessionMemory.js +11 -2
- package/lib/test/unit/UserService.test.js +8 -0
- package/lib/test/unit/boot-device-sync.spec.js +195 -0
- package/lib/test/unit/hub-node-gateway-appium-server.spec.js +21 -0
- package/lib/test/unit/install-scripts.spec.js +98 -0
- package/lib/test/unit/internal-call.spec.js +99 -0
- package/lib/test/unit/internal-calls-session-record.spec.js +312 -0
- package/lib/test/unit/ios-simulator-preview.spec.js +315 -0
- package/lib/test/unit/is-xenon-running.spec.js +10 -0
- package/lib/tsconfig.tsbuildinfo +1 -1
- package/package.json +2 -3
- package/lib/public/assets/DeviceMosaicView-DTVOB_NO.js +0 -2
- package/lib/public/assets/device-explorer-DlhrKJ-g.js +0 -282
|
@@ -13,6 +13,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
13
13
|
};
|
|
14
14
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
15
|
const chai_1 = require("chai");
|
|
16
|
+
const http_1 = __importDefault(require("http"));
|
|
16
17
|
const path_1 = __importDefault(require("path"));
|
|
17
18
|
const sinon_1 = __importDefault(require("sinon"));
|
|
18
19
|
const express_1 = __importDefault(require("express"));
|
|
@@ -166,6 +167,104 @@ describe('internal calls (/wd-internal with the per-process secret)', () => {
|
|
|
166
167
|
}
|
|
167
168
|
(0, chai_1.expect)(ran).to.deep.equal([]);
|
|
168
169
|
}));
|
|
170
|
+
/**
|
|
171
|
+
* The plugin's handle, deep inside Appium's route, is never given the
|
|
172
|
+
* request, so an accepted call's context is how it knows the command
|
|
173
|
+
* is Xenon's own and not the test's.
|
|
174
|
+
*/
|
|
175
|
+
describe('the plugin can tell an internal call from a test’s command', () => {
|
|
176
|
+
let seen;
|
|
177
|
+
let release;
|
|
178
|
+
function labSeeing() {
|
|
179
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
180
|
+
seen = [];
|
|
181
|
+
release = undefined;
|
|
182
|
+
const app = makeExpress();
|
|
183
|
+
app.use(express_1.default.json());
|
|
184
|
+
// A route that, like Appium's, answers after awaiting the driver.
|
|
185
|
+
app.get('/wd/hub/session/:sessionId/source', (req, res) => __awaiter(this, void 0, void 0, function* () {
|
|
186
|
+
if (req.query.hold)
|
|
187
|
+
yield new Promise((resolve) => (release = resolve));
|
|
188
|
+
yield new Promise((resolve) => setTimeout(resolve, 5));
|
|
189
|
+
seen.push({ url: req.originalUrl, inside: (0, internalCall_1.isInsideInternalCall)() });
|
|
190
|
+
res.json({ value: '<page/>' });
|
|
191
|
+
}));
|
|
192
|
+
(0, registerCommandAuth_1.registerCommandAuth)(app, { basePath: '/wd/hub' }, {
|
|
193
|
+
enabled: () => false,
|
|
194
|
+
authDisabled: () => false,
|
|
195
|
+
verifier: new commandCaller_1.CommandCallerVerifier({
|
|
196
|
+
verifyKeyPair: sinon_1.default.stub().resolves(null),
|
|
197
|
+
verifyBearer: sinon_1.default.stub().resolves(null),
|
|
198
|
+
}),
|
|
199
|
+
ownerOf,
|
|
200
|
+
logger: { info: () => undefined, warn: () => undefined, error: () => undefined },
|
|
201
|
+
});
|
|
202
|
+
app.use((_req, res) => res.status(404).json(UNKNOWN_ROUTE));
|
|
203
|
+
return loopback.serve(app);
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
it('inside an accepted call, across the route’s awaits', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
207
|
+
const app = yield labSeeing();
|
|
208
|
+
yield (0, loopbackRequest_1.default)(app)
|
|
209
|
+
.get('/wd/hub/wd-internal/session/s1/source')
|
|
210
|
+
.set((0, internalCall_1.internalCallHeaders)());
|
|
211
|
+
(0, chai_1.expect)(seen).to.deep.equal([{ url: '/wd/hub/session/s1/source', inside: true }]);
|
|
212
|
+
(0, chai_1.expect)((0, internalCall_1.isInsideInternalCall)(), 'outside any request').to.equal(false);
|
|
213
|
+
}));
|
|
214
|
+
it('never for a test’s command, a refused call, or a request after one', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
215
|
+
const app = yield labSeeing();
|
|
216
|
+
yield (0, loopbackRequest_1.default)(app).get('/wd/hub/session/s1/source').set((0, internalCall_1.internalCallHeaders)());
|
|
217
|
+
yield (0, loopbackRequest_1.default)(app)
|
|
218
|
+
.get('/wd/hub/wd-internal/session/s1/source')
|
|
219
|
+
.set((0, internalCall_1.internalCallHeaders)())
|
|
220
|
+
.expect(200);
|
|
221
|
+
yield (0, loopbackRequest_1.default)(app)
|
|
222
|
+
.get('/wd/hub/wd-internal/session/s1/source')
|
|
223
|
+
.set(internalCall_1.INTERNAL_CALL_HEADER, 'not-the-secret')
|
|
224
|
+
.expect(404);
|
|
225
|
+
yield (0, loopbackRequest_1.default)(app).get('/wd/hub/session/s1/source').expect(200);
|
|
226
|
+
(0, chai_1.expect)(seen.map((s) => s.inside)).to.deep.equal([false, true, false]);
|
|
227
|
+
}));
|
|
228
|
+
it('never for a request on the same kept-alive connection after one', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
229
|
+
const server = yield labSeeing();
|
|
230
|
+
const port = server.address().port;
|
|
231
|
+
const agent = new http_1.default.Agent({ keepAlive: true, maxSockets: 1 });
|
|
232
|
+
const get = (path, headers) => new Promise((resolve, reject) => {
|
|
233
|
+
const req = http_1.default.get({ host: '127.0.0.1', port, path, headers, agent }, (res) => {
|
|
234
|
+
res.resume();
|
|
235
|
+
res.on('end', () => resolve(req.reusedSocket));
|
|
236
|
+
});
|
|
237
|
+
req.on('error', reject);
|
|
238
|
+
});
|
|
239
|
+
try {
|
|
240
|
+
yield get('/wd/hub/wd-internal/session/s1/source', (0, internalCall_1.internalCallHeaders)());
|
|
241
|
+
(0, chai_1.expect)(yield get('/wd/hub/session/s1/source', {}), 'the socket was reused').to.equal(true);
|
|
242
|
+
}
|
|
243
|
+
finally {
|
|
244
|
+
agent.destroy();
|
|
245
|
+
}
|
|
246
|
+
(0, chai_1.expect)(seen.map((s) => s.inside)).to.deep.equal([true, false]);
|
|
247
|
+
}));
|
|
248
|
+
it('never for a test’s command running alongside one', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
249
|
+
const app = yield labSeeing();
|
|
250
|
+
const internal = (0, loopbackRequest_1.default)(app)
|
|
251
|
+
.get('/wd/hub/wd-internal/session/s1/source?hold=1')
|
|
252
|
+
.set((0, internalCall_1.internalCallHeaders)())
|
|
253
|
+
.then((r) => r);
|
|
254
|
+
for (let i = 0; i < 100 && !release; i++)
|
|
255
|
+
yield new Promise((r) => setTimeout(r, 10));
|
|
256
|
+
const held = release;
|
|
257
|
+
if (!held)
|
|
258
|
+
throw new Error('the internal call never reached its route');
|
|
259
|
+
yield (0, loopbackRequest_1.default)(app).get('/wd/hub/session/s1/source').expect(200);
|
|
260
|
+
held();
|
|
261
|
+
yield internal;
|
|
262
|
+
(0, chai_1.expect)(seen).to.deep.equal([
|
|
263
|
+
{ url: '/wd/hub/session/s1/source', inside: false },
|
|
264
|
+
{ url: '/wd/hub/session/s1/source?hold=1', inside: true },
|
|
265
|
+
]);
|
|
266
|
+
}));
|
|
267
|
+
});
|
|
169
268
|
it('puts the internal-call layer ahead of the session layer, both ahead of the routes', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
170
269
|
const { raw } = yield lab();
|
|
171
270
|
const stack = (0, insertBeforeRoutes_1.routerStackOf)(raw);
|
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
36
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
37
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
38
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
39
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
40
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
41
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
42
|
+
});
|
|
43
|
+
};
|
|
44
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
45
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
46
|
+
};
|
|
47
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
48
|
+
require("reflect-metadata");
|
|
49
|
+
const chai_1 = require("chai");
|
|
50
|
+
const http_1 = __importDefault(require("http"));
|
|
51
|
+
const sinon_1 = __importDefault(require("sinon"));
|
|
52
|
+
const loopbackRequest_1 = __importDefault(require("../helpers/loopbackRequest"));
|
|
53
|
+
const plugin_1 = require("../../src/plugin");
|
|
54
|
+
const registerCommandAuth_1 = require("../../src/app/registerCommandAuth");
|
|
55
|
+
const SessionManager_1 = require("../../src/sessions/SessionManager");
|
|
56
|
+
const LocalSession_1 = require("../../src/sessions/LocalSession");
|
|
57
|
+
const event_manager_1 = require("../../src/dashboard/event-manager");
|
|
58
|
+
const NotificationService_1 = require("../../src/services/NotificationService");
|
|
59
|
+
const AIService_1 = require("../../src/services/AIService");
|
|
60
|
+
const failure_analysis_service_1 = require("../../src/dashboard/services/failure-analysis-service");
|
|
61
|
+
const deviceService = __importStar(require("../../src/data-service/device-service"));
|
|
62
|
+
const SessionStatus_1 = require("../../src/types/SessionStatus");
|
|
63
|
+
const scratch_database_1 = require("../helpers/scratch-database");
|
|
64
|
+
const loki_stores_1 = require("../helpers/loki-stores");
|
|
65
|
+
const appium_umbrella_1 = require("../helpers/appium-umbrella");
|
|
66
|
+
/**
|
|
67
|
+
* Xenon's own calls to a session it runs, in Appium 3's own server(): Appium's
|
|
68
|
+
* umbrella driver with Xenon's plugin and the production session gateway, on
|
|
69
|
+
* a dashboard-on server, over a scratch database.
|
|
70
|
+
*
|
|
71
|
+
* A LocalSession asks its driver in-process first. When that fails it asks
|
|
72
|
+
* again over HTTP, at `<basePath>/wd-internal/session/<id>/...` with the
|
|
73
|
+
* per-process secret (gateway/internalCall.ts), and that request goes through
|
|
74
|
+
* Appium's route, the umbrella and the plugin's `handle` like a test's.
|
|
75
|
+
* Through 2.16 the plugin couldn't tell: the marker was on the Express request,
|
|
76
|
+
* which `handle` is never given. So a performance recording's stop the driver
|
|
77
|
+
* refused at the end of a session was recorded as a failed `execute` of the
|
|
78
|
+
* session, and the session, whose own commands had all passed, ended failed:
|
|
79
|
+
* "marked as FAILED due to error in command: execute", the driver's refusal
|
|
80
|
+
* as its reason, failure analysis, a `session_failed` webhook. Observed on a
|
|
81
|
+
* simulator on 2026-10-05 (2.16 no longer stops one there, #503); on an
|
|
82
|
+
* iPhone whenever the recording's start failed.
|
|
83
|
+
*/
|
|
84
|
+
const SESSION = 'internal-calls-session';
|
|
85
|
+
const UDID = 'iphone-1';
|
|
86
|
+
const BASE = '/wd/hub';
|
|
87
|
+
const REFUSAL = 'No Time Profiler recording was started on this device';
|
|
88
|
+
const quiet = { info: () => undefined, warn: () => undefined, error: () => undefined };
|
|
89
|
+
/** An iPhone driver as far as these calls go: it refuses the performance recording. */
|
|
90
|
+
function iPhoneLikeDriverClass() {
|
|
91
|
+
const { BaseDriver, errors } = (0, appium_umbrella_1.appiumBaseDriver)();
|
|
92
|
+
return class IPhoneLikeDriver extends BaseDriver {
|
|
93
|
+
constructor() {
|
|
94
|
+
super(...arguments);
|
|
95
|
+
this.calls = [];
|
|
96
|
+
this.pageSourceFailures = 0;
|
|
97
|
+
}
|
|
98
|
+
getUrl() {
|
|
99
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
100
|
+
this.calls.push('getUrl');
|
|
101
|
+
return 'https://app.example/';
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
execute(script) {
|
|
105
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
106
|
+
this.calls.push(`execute ${script}`);
|
|
107
|
+
if (script === 'mobile: startPerfRecord' || script === 'mobile: stopPerfRecord') {
|
|
108
|
+
throw new errors.UnknownError(REFUSAL);
|
|
109
|
+
}
|
|
110
|
+
return null;
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
getPageSource() {
|
|
114
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
115
|
+
this.calls.push('getPageSource');
|
|
116
|
+
if (this.pageSourceFailures > 0) {
|
|
117
|
+
this.pageSourceFailures--;
|
|
118
|
+
throw new errors.UnknownError('The page source took too long');
|
|
119
|
+
}
|
|
120
|
+
if (this.held)
|
|
121
|
+
yield this.held;
|
|
122
|
+
return '<XCUIElementTypeApplication name="Shop"/>';
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
/** Commands waiting in base-driver's queue, read as base-driver reads it. */
|
|
126
|
+
queued() {
|
|
127
|
+
var _a, _b, _c, _d;
|
|
128
|
+
return (_d = (_c = (_b = (_a = this.commandsQueueGuard) === null || _a === void 0 ? void 0 : _a.queues) === null || _b === void 0 ? void 0 : _b[BaseDriver.name]) === null || _c === void 0 ? void 0 : _c.length) !== null && _d !== void 0 ? _d : 0;
|
|
129
|
+
}
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
describe('Xenon’s own calls to a session (/wd-internal), in Appium 3’s own server()', function () {
|
|
133
|
+
this.timeout(60000);
|
|
134
|
+
const scratch = (0, scratch_database_1.useScratchDatabase)();
|
|
135
|
+
(0, loki_stores_1.usePrismaStores)();
|
|
136
|
+
let appiumLogs;
|
|
137
|
+
let server;
|
|
138
|
+
let url;
|
|
139
|
+
let inner;
|
|
140
|
+
let session;
|
|
141
|
+
let notified;
|
|
142
|
+
let isHubBefore;
|
|
143
|
+
before(() => {
|
|
144
|
+
appiumLogs = (0, appium_umbrella_1.quietAppiumLogs)();
|
|
145
|
+
});
|
|
146
|
+
after(() => appiumLogs.restore());
|
|
147
|
+
beforeEach(() => __awaiter(this, void 0, void 0, function* () {
|
|
148
|
+
// A hub or a standalone server, with the dashboard recording its sessions.
|
|
149
|
+
isHubBefore = plugin_1.XenonPlugin.IS_HUB;
|
|
150
|
+
plugin_1.XenonPlugin.IS_HUB = true;
|
|
151
|
+
notified = sinon_1.default.stub(NotificationService_1.NotificationService.prototype, 'notifySessionFailed').resolves();
|
|
152
|
+
// A failed session's analysis never reaches an AI provider from here.
|
|
153
|
+
sinon_1.default.stub(AIService_1.AI_SERVICE, 'isEnabled').returns(false);
|
|
154
|
+
yield scratch.db.sessionLog.deleteMany({});
|
|
155
|
+
yield scratch.db.log.deleteMany({});
|
|
156
|
+
yield scratch.db.session.deleteMany({});
|
|
157
|
+
const baseDriver = (0, appium_umbrella_1.appiumBaseDriver)();
|
|
158
|
+
const umbrella = (0, appium_umbrella_1.umbrellaWith)([[plugin_1.XenonPlugin, 'xenon']]);
|
|
159
|
+
umbrella.args.plugin = { xenon: { enableDashboard: true } };
|
|
160
|
+
server = yield baseDriver.server({
|
|
161
|
+
routeConfiguringFunction: baseDriver.routeConfiguringFunction(umbrella),
|
|
162
|
+
port: 0,
|
|
163
|
+
hostname: '127.0.0.1',
|
|
164
|
+
basePath: BASE,
|
|
165
|
+
cliArgs: { basePath: BASE },
|
|
166
|
+
serverUpdaters: [
|
|
167
|
+
(app) => (0, registerCommandAuth_1.registerSessionGateway)(app, { basePath: BASE }, (0, registerCommandAuth_1.commandAuthDeps)({ enabled: () => false, authDisabled: () => false, logger: quiet })),
|
|
168
|
+
],
|
|
169
|
+
});
|
|
170
|
+
const port = server.address().port;
|
|
171
|
+
url = `http://127.0.0.1:${port}`;
|
|
172
|
+
// Where a LocalSession's loopback calls go: the server it runs on.
|
|
173
|
+
Object.assign(umbrella.opts, { address: '127.0.0.1', port, basePath: BASE });
|
|
174
|
+
// The session as Appium holds it once created.
|
|
175
|
+
const Driver = iPhoneLikeDriverClass();
|
|
176
|
+
inner = new Driver({}, false);
|
|
177
|
+
inner.sessionId = SESSION;
|
|
178
|
+
inner.caps = { platformName: 'iOS', automationName: appium_umbrella_1.FAKE_AUTOMATION, udid: UDID };
|
|
179
|
+
inner.newCommandTimeoutMs = 0;
|
|
180
|
+
umbrella.sessions[SESSION] = inner;
|
|
181
|
+
// ... and as Xenon records it.
|
|
182
|
+
yield scratch.db.session.create({
|
|
183
|
+
data: {
|
|
184
|
+
id: SESSION,
|
|
185
|
+
desired_capabilities: '{}',
|
|
186
|
+
session_capabilities: '{}',
|
|
187
|
+
node_id: 'node-1',
|
|
188
|
+
has_live_video: false,
|
|
189
|
+
device_udid: UDID,
|
|
190
|
+
device_platform: 'ios',
|
|
191
|
+
device_version: '18.0',
|
|
192
|
+
},
|
|
193
|
+
});
|
|
194
|
+
session = new LocalSession_1.LocalSession({
|
|
195
|
+
sessionId: SESSION,
|
|
196
|
+
device: { udid: UDID, platform: 'ios', realDevice: true, host: url },
|
|
197
|
+
sessionResponse: {},
|
|
198
|
+
xenonOption: {},
|
|
199
|
+
driver: umbrella,
|
|
200
|
+
});
|
|
201
|
+
SessionManager_1.SESSION_MANAGER.addSession(SESSION, session);
|
|
202
|
+
}));
|
|
203
|
+
afterEach(() => __awaiter(this, void 0, void 0, function* () {
|
|
204
|
+
SessionManager_1.SESSION_MANAGER.removeSession(SESSION);
|
|
205
|
+
plugin_1.XenonPlugin.IS_HUB = isHubBefore;
|
|
206
|
+
sinon_1.default.restore();
|
|
207
|
+
server.closeAllConnections();
|
|
208
|
+
yield new Promise((resolve) => http_1.default.Server.prototype.close.call(server, () => resolve()));
|
|
209
|
+
}));
|
|
210
|
+
/** The session's recorded commands, oldest first. */
|
|
211
|
+
function recorded() {
|
|
212
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
213
|
+
const rows = yield scratch.db.sessionLog.findMany({
|
|
214
|
+
where: { session_id: SESSION },
|
|
215
|
+
orderBy: { createdAt: 'asc' },
|
|
216
|
+
});
|
|
217
|
+
return rows.map((row) => ({ command: row.command_name, failed: row.is_error }));
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
/** The session's end as every ending reaches it, and what its row says then. */
|
|
221
|
+
function end() {
|
|
222
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
223
|
+
yield event_manager_1.DASHBORD_EVENT_MANAGER.onSessionStopped(SESSION);
|
|
224
|
+
const row = yield scratch.db.session.findUniqueOrThrow({ where: { id: SESSION } });
|
|
225
|
+
return { status: row.status, reason: row.failure_reason };
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
/** Waits for a condition the test can't be told about. */
|
|
229
|
+
function until(condition) {
|
|
230
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
231
|
+
for (let i = 0; i < 200 && !condition(); i++)
|
|
232
|
+
yield new Promise((r) => setTimeout(r, 10));
|
|
233
|
+
(0, chai_1.expect)(condition(), 'waited 2 s').to.equal(true);
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
/** A command the test sends, through the public route. */
|
|
237
|
+
const testCommand = () => (0, loopbackRequest_1.default)(url).get(`${BASE}/session/${SESSION}/url`);
|
|
238
|
+
it('a performance recording’s stop the driver refuses doesn’t fail a session whose commands all passed', () => __awaiter(this, void 0, void 0, function* () {
|
|
239
|
+
yield testCommand().expect(200);
|
|
240
|
+
(0, chai_1.expect)(yield session.stopPerformanceRecording()).to.equal(null);
|
|
241
|
+
// The stop was asked in-process, then over the loopback, which reached the driver.
|
|
242
|
+
(0, chai_1.expect)(inner.calls).to.deep.equal([
|
|
243
|
+
'getUrl',
|
|
244
|
+
'execute mobile: stopPerfRecord',
|
|
245
|
+
'execute mobile: stopPerfRecord',
|
|
246
|
+
]);
|
|
247
|
+
(0, chai_1.expect)(yield recorded()).to.deep.equal([{ command: 'getUrl', failed: false }]);
|
|
248
|
+
(0, chai_1.expect)(yield end()).to.deep.equal({ status: SessionStatus_1.SessionStatus.SUCCESS, reason: null });
|
|
249
|
+
(0, chai_1.expect)(notified.called, 'a session_failed webhook').to.equal(false);
|
|
250
|
+
}));
|
|
251
|
+
it('nor does a refused start of the recording, at the session’s first moments', () => __awaiter(this, void 0, void 0, function* () {
|
|
252
|
+
yield session.startPerformanceRecording();
|
|
253
|
+
(0, chai_1.expect)(inner.calls).to.deep.equal([
|
|
254
|
+
'execute mobile: startPerfRecord',
|
|
255
|
+
'execute mobile: startPerfRecord',
|
|
256
|
+
]);
|
|
257
|
+
yield testCommand().expect(200);
|
|
258
|
+
(0, chai_1.expect)(yield recorded()).to.deep.equal([{ command: 'getUrl', failed: false }]);
|
|
259
|
+
(0, chai_1.expect)(yield end()).to.deep.equal({ status: SessionStatus_1.SessionStatus.SUCCESS, reason: null });
|
|
260
|
+
}));
|
|
261
|
+
it('a page source read over the loopback is not put in the session’s command log', () => __awaiter(this, void 0, void 0, function* () {
|
|
262
|
+
inner.pageSourceFailures = 1;
|
|
263
|
+
(0, chai_1.expect)(yield session.getPageSource()).to.equal('<XCUIElementTypeApplication name="Shop"/>');
|
|
264
|
+
(0, chai_1.expect)(inner.calls).to.deep.equal(['getPageSource', 'getPageSource']);
|
|
265
|
+
(0, chai_1.expect)(yield recorded()).to.deep.equal([]);
|
|
266
|
+
}));
|
|
267
|
+
it('a page source read that fails over the loopback too is not a failed command of the session', () => __awaiter(this, void 0, void 0, function* () {
|
|
268
|
+
inner.pageSourceFailures = 2;
|
|
269
|
+
yield session.getPageSource().then(() => chai_1.expect.fail('the read succeeded'), (error) => { var _a; return (0, chai_1.expect)((_a = error.response) === null || _a === void 0 ? void 0 : _a.status).to.equal(500); });
|
|
270
|
+
(0, chai_1.expect)(inner.calls).to.deep.equal(['getPageSource', 'getPageSource']);
|
|
271
|
+
(0, chai_1.expect)(yield recorded()).to.deep.equal([]);
|
|
272
|
+
(0, chai_1.expect)(yield end()).to.deep.equal({ status: SessionStatus_1.SessionStatus.SUCCESS, reason: null });
|
|
273
|
+
}));
|
|
274
|
+
it('isn’t the session’s activity: only the test’s commands reset its idle clock', () => __awaiter(this, void 0, void 0, function* () {
|
|
275
|
+
const touched = sinon_1.default.stub(deviceService, 'updateCmdExecutedTime').resolves();
|
|
276
|
+
inner.pageSourceFailures = 1;
|
|
277
|
+
yield session.stopPerformanceRecording();
|
|
278
|
+
yield session.getPageSource();
|
|
279
|
+
(0, chai_1.expect)(inner.calls).to.have.length(4);
|
|
280
|
+
(0, chai_1.expect)(touched.called).to.equal(false);
|
|
281
|
+
yield testCommand().expect(200);
|
|
282
|
+
(0, chai_1.expect)(touched.calledOnceWith(SESSION)).to.equal(true);
|
|
283
|
+
}));
|
|
284
|
+
it('a test’s command queued in the driver behind one is recorded, as the test’s', () => __awaiter(this, void 0, void 0, function* () {
|
|
285
|
+
let release = () => { };
|
|
286
|
+
inner.held = new Promise((resolve) => (release = resolve));
|
|
287
|
+
inner.pageSourceFailures = 1;
|
|
288
|
+
// Xenon's read reaches the driver over the loopback and holds its queue.
|
|
289
|
+
const read = session.getPageSource();
|
|
290
|
+
yield until(() => inner.calls.length === 2);
|
|
291
|
+
// The test's command waits in the driver's queue behind it, and runs when
|
|
292
|
+
// Xenon's read lets go.
|
|
293
|
+
const command = testCommand().then((res) => res);
|
|
294
|
+
yield until(() => inner.queued() === 1);
|
|
295
|
+
release();
|
|
296
|
+
(0, chai_1.expect)(yield read).to.equal('<XCUIElementTypeApplication name="Shop"/>');
|
|
297
|
+
(0, chai_1.expect)((yield command).status).to.equal(200);
|
|
298
|
+
(0, chai_1.expect)(inner.calls).to.deep.equal(['getPageSource', 'getPageSource', 'getUrl']);
|
|
299
|
+
(0, chai_1.expect)(yield recorded()).to.deep.equal([{ command: 'getUrl', failed: false }]);
|
|
300
|
+
}));
|
|
301
|
+
it('the same refusal of the test’s own command is recorded and fails the session, as before', () => __awaiter(this, void 0, void 0, function* () {
|
|
302
|
+
yield (0, loopbackRequest_1.default)(url)
|
|
303
|
+
.post(`${BASE}/session/${SESSION}/execute/sync`)
|
|
304
|
+
.send({ script: 'mobile: stopPerfRecord', args: [{ profileName: 'Time Profiler' }] })
|
|
305
|
+
.expect(500);
|
|
306
|
+
(0, chai_1.expect)(yield recorded()).to.deep.equal([{ command: 'execute', failed: true }]);
|
|
307
|
+
(0, chai_1.expect)(yield end()).to.deep.equal({ status: SessionStatus_1.SessionStatus.FAILED, reason: REFUSAL });
|
|
308
|
+
(0, chai_1.expect)(notified.calledOnce, 'a session_failed webhook').to.equal(true);
|
|
309
|
+
// The analysis the failure started ends with the test, not after it.
|
|
310
|
+
yield (0, failure_analysis_service_1.explainSessionFailure)(SESSION);
|
|
311
|
+
}));
|
|
312
|
+
});
|