@xenon-device-management/xenon 2.4.0 → 2.6.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/lib/package.json +1 -1
- package/lib/public/assets/{AnnotationOverlay-CpoWcXKP.js → AnnotationOverlay-PMj0TMCJ.js} +1 -1
- package/lib/public/assets/{ApiKeyGate-DAYuoJDl.js → ApiKeyGate-DXo-ecxe.js} +1 -1
- package/lib/public/assets/BugReportButton-DY9ltq27.js +15 -0
- package/lib/public/assets/{DeviceMosaicView-C-sJRz63.js → DeviceMosaicView-CT7nA_re.js} +1 -1
- package/lib/public/assets/{EmptyState-B_JRYedM.js → EmptyState-DX4jwZ9l.js} +1 -1
- package/lib/public/assets/{FieldGroup-CorXlSLU.js → FieldGroup-Czk8gPhv.js} +1 -1
- package/lib/public/assets/{FilterMenu-BKv95DEc.js → FilterMenu-DTblXm4v.js} +1 -1
- package/lib/public/assets/{Menu-Cpy5yMwg.js → Menu-CdGgXICe.js} +1 -1
- package/lib/public/assets/{Modal-Djp4NvyZ.js → Modal-CtzTciQY.js} +1 -1
- package/lib/public/assets/{RecordingPage-CP57OKw9.js → RecordingPage-BjJH7AyA.js} +1 -1
- package/lib/public/assets/{RecordingsPage-BjrTit38.js → RecordingsPage-BkgX2xkA.js} +1 -1
- package/lib/public/assets/{SegmentedControl-MGTzJhBf.js → SegmentedControl-DEXgl8Qw.js} +1 -1
- package/lib/public/assets/{SettingCard-CYBPhKPe.js → SettingCard-BnwAoJhk.js} +1 -1
- package/lib/public/assets/{Table-CdKQ9hnU.js → Table-hmNY-ICS.js} +1 -1
- package/lib/public/assets/{activity-QsRK21QF.js → activity-Bmuu2fcZ.js} +1 -1
- package/lib/public/assets/{ai-settings-Dn6DtRn7.js → ai-settings-uAzHwRK_.js} +1 -1
- package/lib/public/assets/{api-keys-DYyf17eU.js → api-keys-CFZeXLs7.js} +1 -1
- package/lib/public/assets/{apps-HKFKTe1J.js → apps-DgwQRGg6.js} +1 -1
- package/lib/public/assets/{arrow-left-BkA7oMRe.js → arrow-left-D5x8OLs-.js} +1 -1
- package/lib/public/assets/{arrow-right-BsSr58EU.js → arrow-right-DRYGEjl5.js} +1 -1
- package/lib/public/assets/{arrow-up-right-B7TEV_Gr.js → arrow-up-right-DLtHJsd6.js} +1 -1
- package/lib/public/assets/{auth-shell-BlmNHGcG.js → auth-shell-D_TLCi0Y.js} +2 -2
- package/lib/public/assets/builds-page-CwKR7w5o.js +11 -0
- package/lib/public/assets/{button-BIwCF2bR.js → button-CbVMa9PL.js} +1 -1
- package/lib/public/assets/{calendar-CMLnXC7N.js → calendar-DgmwU-Oq.js} +1 -1
- package/lib/public/assets/{check-zugO3p5U.js → check-Du5vvLoJ.js} +1 -1
- package/lib/public/assets/{chevron-left-sCqxpTY6.js → chevron-left-CiRlzB7w.js} +1 -1
- package/lib/public/assets/{chevron-right-Bb59xwZc.js → chevron-right-i93nTPHX.js} +1 -1
- package/lib/public/assets/{circle-check-DsvxbnTg.js → circle-check-DxYO7yp5.js} +1 -1
- package/lib/public/assets/{circle-x-w69AbmbW.js → circle-x-Dw1DFg0I.js} +1 -1
- package/lib/public/assets/{clock-C6XOs4bc.js → clock-0q-GqRU0.js} +1 -1
- package/lib/public/assets/{copy-pHk-SWBM.js → copy-Ds-F075f.js} +1 -1
- package/lib/public/assets/{device-explorer-BDq38IcO.js → device-explorer-Dsldlx2g.js} +54 -64
- package/lib/public/assets/{download-CAHtPZmY.js → download-BdJEWL8b.js} +1 -1
- package/lib/public/assets/{forgot-password-cmvhGOUu.js → forgot-password-D7eNuw-V.js} +1 -1
- package/lib/public/assets/{funnel-BrDuhOtM.js → funnel-CY3Ovebz.js} +1 -1
- package/lib/public/assets/{index-C-Q4QkXb.js → index-BpJwb7cQ.js} +1 -1
- package/lib/public/assets/{index-hymrhlEZ.css → index-C8pvxG1f.css} +1 -1
- package/lib/public/assets/{index-BQCXPOXt.js → index-tW_VASh2.js} +2 -2
- package/lib/public/assets/{input-DnUVHYax.js → input-DvCYrL_q.js} +1 -1
- package/lib/public/assets/{keycodes-CmOptFVa.js → keycodes-CyOBCkU6.js} +1 -1
- package/lib/public/assets/{layers-DRtz_bzF.js → layers-ChEcsAc2.js} +1 -1
- package/lib/public/assets/list-checks-EWcywAEC.js +6 -0
- package/lib/public/assets/{lock-DF2VnPFm.js → lock-DmkRlmQB.js} +1 -1
- package/lib/public/assets/{login-DHYffYa4.js → login-BXeYWAn-.js} +1 -1
- package/lib/public/assets/{maintenance-settings-r0uPsJ6n.js → maintenance-settings-Cd37WZ5e.js} +1 -1
- package/lib/public/assets/{monitor-7nt9oGNx.js → monitor-BTqiDmvk.js} +1 -1
- package/lib/public/assets/{mouse-pointer-2-ys1MKVr2.js → mouse-pointer-2-BrwX4M5d.js} +1 -1
- package/lib/public/assets/{network-B8m-F79k.js → network-8uySyPSM.js} +1 -1
- package/lib/public/assets/{overview-BHEntlXz.js → overview-CdBGYC95.js} +1 -1
- package/lib/public/assets/{page-header-CuFXVUop.js → page-header-BYzd5kfp.js} +1 -1
- package/lib/public/assets/{plus-DUSlMac2.js → plus-BIekJsya.js} +1 -1
- package/lib/public/assets/{profile-page-CBamPy_h.js → profile-page-a-nQ7xFX.js} +1 -1
- package/lib/public/assets/{reset-password-DG0vZl-o.js → reset-password-7Gbj29w1.js} +1 -1
- package/lib/public/assets/{runbook-page-5S3T-I4g.js → runbook-page-Bgd9u7W6.js} +1 -1
- package/lib/public/assets/{select-f0ci1QC7.js → select-DEpRzm_3.js} +1 -1
- package/lib/public/assets/{selector-detail-page-BXA9LUf9.js → selector-detail-page-CvqjXfvp.js} +1 -1
- package/lib/public/assets/{selector-health-TyMPgAWo.js → selector-health-BEzkwE_K.js} +1 -1
- package/lib/public/assets/{selector-health-page-uBlXy33G.js → selector-health-page-Dl_IE1Je.js} +1 -1
- package/lib/public/assets/session-detail-page-CuF_EwRE.js +4 -0
- package/lib/public/assets/{settings-IfOFbqtv.js → settings-CxLw5tlF.js} +1 -1
- package/lib/public/assets/stat-tile-DFuoxV5Y.js +6 -0
- package/lib/public/assets/{tablet-lmFk4qxW.js → tablet-DJBOerD6.js} +1 -1
- package/lib/public/assets/{teams-CwMSjVVW.js → teams-CDQQdVRl.js} +1 -1
- package/lib/public/assets/{trash-2-BKsgmBS6.js → trash-2-B_zQvjI3.js} +1 -1
- package/lib/public/assets/{upload-DXH47sQS.js → upload-9FV8q3NB.js} +1 -1
- package/lib/public/assets/{use-password-reset-mode-DU98dxpc.js → use-password-reset-mode-Cx5GHMnk.js} +1 -1
- package/lib/public/assets/{users-V5O64rVx.js → users-DC0IoOuZ.js} +1 -1
- package/lib/public/assets/{users-eXWwQZjX.js → users-ep-SOGaV.js} +1 -1
- package/lib/public/assets/{video-off-BuoIHEYy.js → video-off-BmUg-gKW.js} +1 -1
- package/lib/public/assets/{webhook-settings-ClsxsKnC.js → webhook-settings-CwUBzLGC.js} +1 -1
- package/lib/public/assets/{zap-I10vdQXd.js → zap-CBld_5xf.js} +1 -1
- package/lib/public/index.html +2 -2
- package/lib/src/app/routers/dashboard.js +45 -3
- package/lib/src/app/routers/sessionInsights.js +221 -0
- package/lib/test/unit/session-list-insights.spec.js +354 -0
- package/lib/tsconfig.tsbuildinfo +1 -1
- package/package.json +1 -1
- package/lib/public/assets/BugReportButton-DEXyENUv.js +0 -10
- package/lib/public/assets/builds-page-E4WIKtl8.js +0 -6
- package/lib/public/assets/session-detail-page-DECim5lh.js +0 -3
- package/lib/public/assets/status-pill-outline-grXLf8I8.js +0 -1
|
@@ -65,6 +65,7 @@ const path_1 = __importDefault(require("path"));
|
|
|
65
65
|
const config_1 = require("../../config");
|
|
66
66
|
const sessionVisibility_1 = require("../../services/device-access/sessionVisibility");
|
|
67
67
|
const HealEtalonService_1 = require("../../services/healing/HealEtalonService");
|
|
68
|
+
const sessionInsights_1 = require("./sessionInsights");
|
|
68
69
|
const logger_1 = __importDefault(require("../../logger"));
|
|
69
70
|
const MJPEG_PROXY_CACHE = new Map();
|
|
70
71
|
const authOf = (request) => request.auth;
|
|
@@ -100,10 +101,29 @@ function isValidSession(request, response, next) {
|
|
|
100
101
|
}
|
|
101
102
|
});
|
|
102
103
|
}
|
|
104
|
+
// A `since` that isn't a date is refused, rather than ignored into a list
|
|
105
|
+
// of every session.
|
|
106
|
+
function sinceOf(request, response) {
|
|
107
|
+
try {
|
|
108
|
+
return (0, sessionInsights_1.parseSince)(request.query.since);
|
|
109
|
+
}
|
|
110
|
+
catch (err) {
|
|
111
|
+
if (!(err instanceof sessionInsights_1.InvalidSinceError))
|
|
112
|
+
throw err;
|
|
113
|
+
response.status(400).json({ error: 'invalid_since', message: err.message });
|
|
114
|
+
return undefined;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
103
117
|
function getSessions(request, response) {
|
|
104
118
|
return __awaiter(this, void 0, void 0, function* () {
|
|
105
119
|
const { buildId, query, status, platform } = request.query;
|
|
120
|
+
const since = sinceOf(request, response);
|
|
121
|
+
if (since === undefined)
|
|
122
|
+
return;
|
|
106
123
|
const where = {};
|
|
124
|
+
if (since) {
|
|
125
|
+
where.createdAt = { gte: since };
|
|
126
|
+
}
|
|
107
127
|
if (buildId) {
|
|
108
128
|
where.build_id = buildId;
|
|
109
129
|
}
|
|
@@ -132,7 +152,26 @@ function getSessions(request, response) {
|
|
|
132
152
|
where: visible ? { AND: [where, visible] } : where,
|
|
133
153
|
take: 500,
|
|
134
154
|
});
|
|
135
|
-
return response.status(200).json(sessions);
|
|
155
|
+
return response.status(200).json(yield (0, sessionInsights_1.describeSessions)(sessions));
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
// The Sessions page's summary strip: the period since `since` (all time
|
|
159
|
+
// without it) and the one before it, over the sessions the caller may see,
|
|
160
|
+
// of one build when `buildId` is given.
|
|
161
|
+
function getSessionSummary(request, response) {
|
|
162
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
163
|
+
const since = sinceOf(request, response);
|
|
164
|
+
if (since === undefined)
|
|
165
|
+
return;
|
|
166
|
+
const { buildId } = request.query;
|
|
167
|
+
const visible = yield (0, sessionVisibility_1.visibleSessionWhere)(authOf(request));
|
|
168
|
+
const scope = {
|
|
169
|
+
AND: [
|
|
170
|
+
...(visible ? [visible] : []),
|
|
171
|
+
...(typeof buildId === 'string' && buildId ? [{ build_id: buildId }] : []),
|
|
172
|
+
],
|
|
173
|
+
};
|
|
174
|
+
return response.status(200).json(yield (0, sessionInsights_1.summarizeSessions)(scope, since));
|
|
136
175
|
});
|
|
137
176
|
}
|
|
138
177
|
function getBuilds(request, response) {
|
|
@@ -154,7 +193,7 @@ function getBuilds(request, response) {
|
|
|
154
193
|
});
|
|
155
194
|
const visibleBuilds = visible ? builds.filter((b) => b.sessions.length > 0) : builds;
|
|
156
195
|
// Principal formatting: Add a flat summary object for the frontend
|
|
157
|
-
const formattedBuilds = visibleBuilds.map((b) => (Object.assign(Object.assign({}, b), { _count: { sessions: b.sessions.length }, sessionCount: b.sessions.length, passedCount: b.sessions.filter((s) =>
|
|
196
|
+
const formattedBuilds = visibleBuilds.map((b) => (Object.assign(Object.assign({}, b), { _count: { sessions: b.sessions.length }, sessionCount: b.sessions.length, passedCount: b.sessions.filter((s) => (0, sessionInsights_1.sessionOutcome)(s.status) === 'passed').length, failedCount: b.sessions.filter((s) => (0, sessionInsights_1.sessionOutcome)(s.status) === 'failed').length, runningCount: b.sessions.filter((s) => (0, sessionInsights_1.sessionOutcome)(s.status) === 'running').length, sessions: undefined })));
|
|
158
197
|
return response.status(200).json(formattedBuilds);
|
|
159
198
|
});
|
|
160
199
|
}
|
|
@@ -168,7 +207,9 @@ function getSessionById(request, response) {
|
|
|
168
207
|
if (!session) {
|
|
169
208
|
return response.status(404).json({ error: true, message: 'Session not found' });
|
|
170
209
|
}
|
|
171
|
-
|
|
210
|
+
// Who ran it and where, as the list says (describeSessions).
|
|
211
|
+
const [described] = yield (0, sessionInsights_1.describeSessions)([session]);
|
|
212
|
+
return response.status(200).json(described);
|
|
172
213
|
});
|
|
173
214
|
}
|
|
174
215
|
function getSessionLogs(request, response) {
|
|
@@ -1074,6 +1115,7 @@ function register(router) {
|
|
|
1074
1115
|
router.use('/session/:sessionId', isValidSession);
|
|
1075
1116
|
router.use((0, roleGuard_1.roleGuard)('MEMBER'));
|
|
1076
1117
|
router.get('/session', getSessions);
|
|
1118
|
+
router.get('/session-summary', getSessionSummary);
|
|
1077
1119
|
router.get('/session/:sessionId', getSessionById);
|
|
1078
1120
|
router.get('/build', getBuilds);
|
|
1079
1121
|
build_export_1.default.register(router);
|
|
@@ -0,0 +1,221 @@
|
|
|
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
|
+
exports.InvalidSinceError = void 0;
|
|
13
|
+
exports.sessionOutcome = sessionOutcome;
|
|
14
|
+
exports.parseSince = parseSince;
|
|
15
|
+
exports.describeSessions = describeSessions;
|
|
16
|
+
exports.percentile = percentile;
|
|
17
|
+
exports.summarizeSessions = summarizeSessions;
|
|
18
|
+
const typedi_1 = require("typedi");
|
|
19
|
+
const prisma_1 = require("../../prisma");
|
|
20
|
+
const PluginContext_1 = require("../../PluginContext");
|
|
21
|
+
const localDeviceHosts_1 = require("../../device-managers/localDeviceHosts");
|
|
22
|
+
/**
|
|
23
|
+
* The verdict of a persisted `Session.status`. The dashboard's
|
|
24
|
+
* `sessionStatusBucket` (web/src/components/builds/derive.ts) is the same
|
|
25
|
+
* rule, so a build's counts and its rows agree.
|
|
26
|
+
*/
|
|
27
|
+
function sessionOutcome(status) {
|
|
28
|
+
switch (status) {
|
|
29
|
+
case 'success':
|
|
30
|
+
case 'passed':
|
|
31
|
+
case 'ended':
|
|
32
|
+
return 'passed';
|
|
33
|
+
case 'failed':
|
|
34
|
+
case 'error':
|
|
35
|
+
case 'timeout':
|
|
36
|
+
return 'failed';
|
|
37
|
+
case 'running':
|
|
38
|
+
return 'running';
|
|
39
|
+
default:
|
|
40
|
+
return 'other';
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
class InvalidSinceError extends Error {
|
|
44
|
+
constructor() {
|
|
45
|
+
super('since must be an ISO 8601 date');
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
exports.InvalidSinceError = InvalidSinceError;
|
|
49
|
+
/** The `since` query parameter: null when absent; a date, or InvalidSinceError. */
|
|
50
|
+
function parseSince(raw) {
|
|
51
|
+
if (raw === undefined || raw === '')
|
|
52
|
+
return null;
|
|
53
|
+
const at = typeof raw === 'string' ? Date.parse(raw) : NaN;
|
|
54
|
+
if (!Number.isFinite(at))
|
|
55
|
+
throw new InvalidSinceError();
|
|
56
|
+
return new Date(at);
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Each listed session with who ran it (`owner`) and where (`ranOn`): 'here'
|
|
60
|
+
* for this server, the node's host for another server's phone, null when
|
|
61
|
+
* that isn't known any more. Four queries for the whole list, never one per
|
|
62
|
+
* row. Members can't read the users list, so the dashboard can't name owners
|
|
63
|
+
* itself.
|
|
64
|
+
*/
|
|
65
|
+
function describeSessions(rows) {
|
|
66
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
67
|
+
const [owners, places] = yield Promise.all([ownersOf(rows), placesOf(rows)]);
|
|
68
|
+
return rows.map((row) => {
|
|
69
|
+
var _a, _b;
|
|
70
|
+
return (Object.assign(Object.assign({}, row), { owner: (_a = owners.get(row.id)) !== null && _a !== void 0 ? _a : null, ranOn: (_b = places.get(row.id)) !== null && _b !== void 0 ? _b : null }));
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
// SessionOwnerResolver.ownerOf's rule, for a list: the row's user, else, for
|
|
75
|
+
// a row written before user_id existed, its key's owner.
|
|
76
|
+
function ownersOf(rows) {
|
|
77
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
78
|
+
var _a;
|
|
79
|
+
const legacyKeyIds = [
|
|
80
|
+
...new Set(rows.filter((r) => !r.user_id && r.api_key_id).map((r) => r.api_key_id)),
|
|
81
|
+
];
|
|
82
|
+
const keyOwner = new Map();
|
|
83
|
+
if (legacyKeyIds.length > 0) {
|
|
84
|
+
const keys = yield prisma_1.prisma.apiKey.findMany({
|
|
85
|
+
where: { id: { in: legacyKeyIds } },
|
|
86
|
+
select: { id: true, userId: true },
|
|
87
|
+
});
|
|
88
|
+
for (const key of keys)
|
|
89
|
+
keyOwner.set(key.id, key.userId);
|
|
90
|
+
}
|
|
91
|
+
const ownerId = (r) => { var _a; return (_a = r.user_id) !== null && _a !== void 0 ? _a : (r.api_key_id ? keyOwner.get(r.api_key_id) : undefined); };
|
|
92
|
+
const userIds = [...new Set(rows.map(ownerId).filter((id) => !!id))];
|
|
93
|
+
const users = userIds.length > 0
|
|
94
|
+
? yield prisma_1.prisma.user.findMany({
|
|
95
|
+
where: { id: { in: userIds } },
|
|
96
|
+
select: { id: true, name: true, email: true },
|
|
97
|
+
})
|
|
98
|
+
: [];
|
|
99
|
+
const byId = new Map(users.map((u) => [u.id, { name: u.name || u.email, email: u.email }]));
|
|
100
|
+
const out = new Map();
|
|
101
|
+
for (const row of rows) {
|
|
102
|
+
const owner = byId.get((_a = ownerId(row)) !== null && _a !== void 0 ? _a : '');
|
|
103
|
+
if (owner)
|
|
104
|
+
out.set(row.id, owner);
|
|
105
|
+
}
|
|
106
|
+
return out;
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
// Node ids are new on every boot, so a session's node_id names its server
|
|
110
|
+
// only until that server restarts. Past that, its phone's row says where it
|
|
111
|
+
// is: the row of that node id, else the phone's only row. A udid on more than
|
|
112
|
+
// one server (an emulator's) with no row of that node id isn't guessed.
|
|
113
|
+
function placesOf(rows) {
|
|
114
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
115
|
+
var _a;
|
|
116
|
+
const context = typedi_1.Container.get(PluginContext_1.PluginContext);
|
|
117
|
+
const nodeIds = [...new Set(rows.map((r) => r.node_id).filter(Boolean))];
|
|
118
|
+
const udids = [...new Set(rows.map((r) => r.device_udid).filter(Boolean))];
|
|
119
|
+
if (udids.length === 0 && nodeIds.length === 0)
|
|
120
|
+
return new Map();
|
|
121
|
+
const devices = yield prisma_1.prisma.device.findMany({
|
|
122
|
+
where: { OR: [{ nodeId: { in: nodeIds } }, { udid: { in: udids } }] },
|
|
123
|
+
select: { udid: true, host: true, nodeId: true },
|
|
124
|
+
});
|
|
125
|
+
const local = (0, localDeviceHosts_1.localDeviceHosts)(context.pluginArgs, context.port);
|
|
126
|
+
const out = new Map();
|
|
127
|
+
for (const row of rows) {
|
|
128
|
+
if (row.node_id && row.node_id === context.nodeId) {
|
|
129
|
+
out.set(row.id, 'here');
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
const sameUdid = devices.filter((d) => d.udid === row.device_udid);
|
|
133
|
+
const device = (_a = sameUdid.find((d) => d.nodeId && d.nodeId === row.node_id)) !== null && _a !== void 0 ? _a : (sameUdid.length === 1 ? sameUdid[0] : undefined);
|
|
134
|
+
if (!device)
|
|
135
|
+
continue;
|
|
136
|
+
out.set(row.id, (0, localDeviceHosts_1.isOwnDevice)(local, context.nodeId, device) ? 'here' : hostLabel(device.host));
|
|
137
|
+
}
|
|
138
|
+
return out;
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
/** A host as people read it: no scheme, no trailing slash. */
|
|
142
|
+
function hostLabel(host) {
|
|
143
|
+
return host.replace(/^[a-z][a-z0-9+.-]*:\/\//i, '').replace(/\/+$/, '');
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Durations come from at most this many of the period's newest ended
|
|
147
|
+
* sessions, so a long period costs one bounded read; past it they are an
|
|
148
|
+
* estimate from the newest.
|
|
149
|
+
*/
|
|
150
|
+
const DURATION_SAMPLE = 10000;
|
|
151
|
+
/** Nearest-rank percentile of ascending values; null for none. */
|
|
152
|
+
function percentile(sorted, p) {
|
|
153
|
+
if (sorted.length === 0)
|
|
154
|
+
return null;
|
|
155
|
+
const rank = Math.max(1, Math.ceil((p / 100) * sorted.length));
|
|
156
|
+
return sorted[rank - 1];
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* The Sessions page's summary strip: the period from `since` to now, the
|
|
160
|
+
* period of the same length before it, and what runs now, over the sessions
|
|
161
|
+
* `scope` admits (the caller's visibility, and a build when one is chosen).
|
|
162
|
+
* Without `since` the period is all time and there is none before it.
|
|
163
|
+
*/
|
|
164
|
+
function summarizeSessions(scope_1, since_1) {
|
|
165
|
+
return __awaiter(this, arguments, void 0, function* (scope, since, now = Date.now()) {
|
|
166
|
+
const within = (from, to) => {
|
|
167
|
+
const createdAt = {};
|
|
168
|
+
if (from)
|
|
169
|
+
createdAt.gte = from;
|
|
170
|
+
if (to)
|
|
171
|
+
createdAt.lt = to;
|
|
172
|
+
return Object.keys(createdAt).length ? { AND: [scope, { createdAt }] } : scope;
|
|
173
|
+
};
|
|
174
|
+
const counts = (where) => __awaiter(this, void 0, void 0, function* () {
|
|
175
|
+
// groupBy's generated type can't be satisfied with a `where` built at
|
|
176
|
+
// run time; the result's shape is fixed by `by` and `_count`.
|
|
177
|
+
const groups = (yield prisma_1.prisma.session.groupBy({
|
|
178
|
+
by: ['status'],
|
|
179
|
+
where,
|
|
180
|
+
_count: { _all: true },
|
|
181
|
+
}));
|
|
182
|
+
const out = { total: 0, passed: 0, failed: 0, running: 0 };
|
|
183
|
+
for (const g of groups) {
|
|
184
|
+
out.total += g._count._all;
|
|
185
|
+
const outcome = sessionOutcome(g.status);
|
|
186
|
+
if (outcome !== 'other')
|
|
187
|
+
out[outcome] += g._count._all;
|
|
188
|
+
}
|
|
189
|
+
return out;
|
|
190
|
+
});
|
|
191
|
+
const current = within(since, null);
|
|
192
|
+
const previousFrom = since ? new Date(since.getTime() - (now - since.getTime())) : null;
|
|
193
|
+
const [currentCounts, previous, ended, running] = yield Promise.all([
|
|
194
|
+
counts(current),
|
|
195
|
+
since ? counts(within(previousFrom, since)) : Promise.resolve(null),
|
|
196
|
+
prisma_1.prisma.session.findMany({
|
|
197
|
+
where: { AND: [current, { endTime: { not: null } }, { status: { not: 'running' } }] },
|
|
198
|
+
select: { startTime: true, endTime: true },
|
|
199
|
+
orderBy: { createdAt: 'desc' },
|
|
200
|
+
take: DURATION_SAMPLE,
|
|
201
|
+
}),
|
|
202
|
+
prisma_1.prisma.session.findMany({
|
|
203
|
+
where: { AND: [scope, { status: 'running' }] },
|
|
204
|
+
select: { device_udid: true },
|
|
205
|
+
}),
|
|
206
|
+
]);
|
|
207
|
+
const durations = ended
|
|
208
|
+
.map((s) => (s.endTime ? s.endTime.getTime() - s.startTime.getTime() : -1))
|
|
209
|
+
.filter((ms) => ms >= 0)
|
|
210
|
+
.sort((a, b) => a - b);
|
|
211
|
+
return {
|
|
212
|
+
since: since ? since.toISOString() : null,
|
|
213
|
+
current: Object.assign(Object.assign({}, currentCounts), { medianMs: percentile(durations, 50), p90Ms: percentile(durations, 90) }),
|
|
214
|
+
previous,
|
|
215
|
+
runningNow: {
|
|
216
|
+
sessions: running.length,
|
|
217
|
+
devices: new Set(running.map((s) => s.device_udid)).size,
|
|
218
|
+
},
|
|
219
|
+
};
|
|
220
|
+
});
|
|
221
|
+
}
|
|
@@ -0,0 +1,354 @@
|
|
|
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 typedi_1 = require("typedi");
|
|
19
|
+
const loopbackRequest_1 = __importDefault(require("../helpers/loopbackRequest"));
|
|
20
|
+
const scratch_database_1 = require("../helpers/scratch-database");
|
|
21
|
+
const PluginContext_1 = require("../../src/PluginContext");
|
|
22
|
+
const IPluginArgs_1 = require("../../src/interfaces/IPluginArgs");
|
|
23
|
+
const dashboard_1 = __importDefault(require("../../src/app/routers/dashboard"));
|
|
24
|
+
/**
|
|
25
|
+
* What the Sessions page reads besides the rows themselves: the period filter
|
|
26
|
+
* on the list, who ran each session and where, the period summary, and the
|
|
27
|
+
* per-build counts. Real queries against a scratch database, because each of
|
|
28
|
+
* these is a Prisma filter a stub would only echo.
|
|
29
|
+
*/
|
|
30
|
+
describe('Sessions page data', () => {
|
|
31
|
+
const scratch = (0, scratch_database_1.useScratchDatabase)();
|
|
32
|
+
const HOUR = 60 * 60 * 1000;
|
|
33
|
+
const MIN = 60 * 1000;
|
|
34
|
+
let context;
|
|
35
|
+
let saved;
|
|
36
|
+
let now;
|
|
37
|
+
let caller;
|
|
38
|
+
// An admin: teamIds undefined, so every session is visible.
|
|
39
|
+
const ADMIN = { kind: 'user-session', userId: 'admin-1', role: 'SUPER_ADMIN', scopes: 'admin' };
|
|
40
|
+
beforeEach(() => __awaiter(void 0, void 0, void 0, function* () {
|
|
41
|
+
context = typedi_1.Container.get(PluginContext_1.PluginContext);
|
|
42
|
+
saved = Object.assign({}, context);
|
|
43
|
+
context.setContext(Object.assign(Object.assign({}, IPluginArgs_1.DefaultPluginArgs), { bindHostOrIp: '127.0.0.1' }), 4723, 'hub-1', '');
|
|
44
|
+
now = Date.now();
|
|
45
|
+
caller = ADMIN;
|
|
46
|
+
for (const model of ['session', 'build', 'apiKey', 'user', 'device']) {
|
|
47
|
+
yield scratch.db[model].deleteMany({});
|
|
48
|
+
}
|
|
49
|
+
}));
|
|
50
|
+
afterEach(() => {
|
|
51
|
+
Object.assign(context, saved);
|
|
52
|
+
});
|
|
53
|
+
const app = () => {
|
|
54
|
+
const a = (0, express_1.default)();
|
|
55
|
+
a.use(express_1.default.json());
|
|
56
|
+
a.use((req, _res, next) => {
|
|
57
|
+
req.auth = caller;
|
|
58
|
+
next();
|
|
59
|
+
});
|
|
60
|
+
dashboard_1.default.register(a);
|
|
61
|
+
return a;
|
|
62
|
+
};
|
|
63
|
+
let seq = 0;
|
|
64
|
+
const session = (fields) => {
|
|
65
|
+
seq += 1;
|
|
66
|
+
return scratch.db.session.create({
|
|
67
|
+
data: Object.assign({ id: `s-${seq}`, status: 'success', desired_capabilities: '{}', session_capabilities: '{}', node_id: 'hub-1', has_live_video: false, device_udid: 'phone-1', device_platform: 'android', device_version: '14' }, fields),
|
|
68
|
+
});
|
|
69
|
+
};
|
|
70
|
+
const ago = (ms) => new Date(now - ms);
|
|
71
|
+
const user = (id, name, email) => scratch.db.user.create({
|
|
72
|
+
data: { id, name, email, passwordHash: 'x', accessKey: `ak-${id}` },
|
|
73
|
+
});
|
|
74
|
+
describe('GET /session', () => {
|
|
75
|
+
it('keeps the sessions created since the given time, newest first', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
76
|
+
yield session({ id: 'old', createdAt: ago(3 * HOUR) });
|
|
77
|
+
yield session({ id: 'recent', createdAt: ago(30 * MIN) });
|
|
78
|
+
yield session({ id: 'newest', createdAt: ago(5 * MIN) });
|
|
79
|
+
const res = yield (0, loopbackRequest_1.default)(app()).get(`/session?since=${ago(HOUR).toISOString()}`);
|
|
80
|
+
(0, chai_1.expect)(res.status).to.equal(200);
|
|
81
|
+
(0, chai_1.expect)(res.body.map((s) => s.id)).to.deep.equal(['newest', 'recent']);
|
|
82
|
+
const all = yield (0, loopbackRequest_1.default)(app()).get('/session');
|
|
83
|
+
(0, chai_1.expect)(all.body.map((s) => s.id)).to.deep.equal(['newest', 'recent', 'old']);
|
|
84
|
+
}));
|
|
85
|
+
it('refuses a since that is not a date', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
86
|
+
const res = yield (0, loopbackRequest_1.default)(app()).get('/session?since=yesterday-ish');
|
|
87
|
+
(0, chai_1.expect)(res.status).to.equal(400);
|
|
88
|
+
(0, chai_1.expect)(res.body.error).to.equal('invalid_since');
|
|
89
|
+
}));
|
|
90
|
+
it('names who ran each session: its user, or an older row’s key owner', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
91
|
+
yield user('u-priya', 'Priya Shah', 'priya@example.com');
|
|
92
|
+
yield user('u-alex', '', 'alex@example.com');
|
|
93
|
+
yield scratch.db.apiKey.create({
|
|
94
|
+
data: { id: 'key-1', name: 'ci', keyHash: 'h1', scopes: 'sessions', userId: 'u-alex' },
|
|
95
|
+
});
|
|
96
|
+
yield session({ id: 'by-user', user_id: 'u-priya', createdAt: ago(4 * MIN) });
|
|
97
|
+
yield session({ id: 'by-key', api_key_id: 'key-1', createdAt: ago(3 * MIN) });
|
|
98
|
+
yield session({ id: 'gone-user', user_id: 'u-deleted', createdAt: ago(2 * MIN) });
|
|
99
|
+
yield session({ id: 'nobody', createdAt: ago(1 * MIN) });
|
|
100
|
+
const res = yield (0, loopbackRequest_1.default)(app()).get('/session');
|
|
101
|
+
const owner = Object.fromEntries(res.body.map((s) => [s.id, s.owner]));
|
|
102
|
+
(0, chai_1.expect)(owner['by-user']).to.deep.equal({ name: 'Priya Shah', email: 'priya@example.com' });
|
|
103
|
+
// No name on record: the email stands in for it.
|
|
104
|
+
(0, chai_1.expect)(owner['by-key']).to.deep.equal({
|
|
105
|
+
name: 'alex@example.com',
|
|
106
|
+
email: 'alex@example.com',
|
|
107
|
+
});
|
|
108
|
+
(0, chai_1.expect)(owner['gone-user']).to.equal(null);
|
|
109
|
+
(0, chai_1.expect)(owner['nobody']).to.equal(null);
|
|
110
|
+
}));
|
|
111
|
+
it('says where each session ran: here, a node by its host, or not known', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
112
|
+
yield scratch.db.device.create({
|
|
113
|
+
data: {
|
|
114
|
+
udid: 'own-phone',
|
|
115
|
+
host: 'http://127.0.0.1:4723',
|
|
116
|
+
nodeId: 'hub-1',
|
|
117
|
+
platform: 'android',
|
|
118
|
+
},
|
|
119
|
+
});
|
|
120
|
+
yield scratch.db.device.create({
|
|
121
|
+
data: {
|
|
122
|
+
udid: 'node-phone',
|
|
123
|
+
host: 'http://10.0.0.9:4725',
|
|
124
|
+
nodeId: 'node-2',
|
|
125
|
+
platform: 'ios',
|
|
126
|
+
},
|
|
127
|
+
});
|
|
128
|
+
yield session({
|
|
129
|
+
id: 'this-boot',
|
|
130
|
+
node_id: 'hub-1',
|
|
131
|
+
device_udid: 'own-phone',
|
|
132
|
+
createdAt: ago(5 * MIN),
|
|
133
|
+
});
|
|
134
|
+
// Node ids are new on every boot, so an older session of this server's
|
|
135
|
+
// own phone carries an id nothing matches any more; its phone's row says.
|
|
136
|
+
yield session({
|
|
137
|
+
id: 'last-boot',
|
|
138
|
+
node_id: 'old-boot',
|
|
139
|
+
device_udid: 'own-phone',
|
|
140
|
+
createdAt: ago(4 * MIN),
|
|
141
|
+
});
|
|
142
|
+
yield session({
|
|
143
|
+
id: 'on-node',
|
|
144
|
+
node_id: 'node-2',
|
|
145
|
+
device_udid: 'node-phone',
|
|
146
|
+
createdAt: ago(3 * MIN),
|
|
147
|
+
});
|
|
148
|
+
yield session({
|
|
149
|
+
id: 'node-restarted',
|
|
150
|
+
node_id: 'node-2-old',
|
|
151
|
+
device_udid: 'node-phone',
|
|
152
|
+
createdAt: ago(2 * MIN),
|
|
153
|
+
});
|
|
154
|
+
yield session({
|
|
155
|
+
id: 'phone-gone',
|
|
156
|
+
node_id: 'node-9',
|
|
157
|
+
device_udid: 'unplugged',
|
|
158
|
+
createdAt: ago(1 * MIN),
|
|
159
|
+
});
|
|
160
|
+
const res = yield (0, loopbackRequest_1.default)(app()).get('/session');
|
|
161
|
+
const ranOn = Object.fromEntries(res.body.map((s) => [s.id, s.ranOn]));
|
|
162
|
+
(0, chai_1.expect)(ranOn).to.deep.equal({
|
|
163
|
+
'this-boot': 'here',
|
|
164
|
+
'last-boot': 'here',
|
|
165
|
+
'on-node': '10.0.0.9:4725',
|
|
166
|
+
'node-restarted': '10.0.0.9:4725',
|
|
167
|
+
'phone-gone': null,
|
|
168
|
+
});
|
|
169
|
+
}));
|
|
170
|
+
it('does not guess when the same udid is on more than one server', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
171
|
+
yield scratch.db.device.create({
|
|
172
|
+
data: {
|
|
173
|
+
udid: 'emulator-5554',
|
|
174
|
+
host: 'http://127.0.0.1:4723',
|
|
175
|
+
nodeId: 'hub-1',
|
|
176
|
+
platform: 'android',
|
|
177
|
+
},
|
|
178
|
+
});
|
|
179
|
+
yield scratch.db.device.create({
|
|
180
|
+
data: {
|
|
181
|
+
udid: 'emulator-5554',
|
|
182
|
+
host: 'http://10.0.0.9:4725',
|
|
183
|
+
nodeId: 'node-2',
|
|
184
|
+
platform: 'android',
|
|
185
|
+
},
|
|
186
|
+
});
|
|
187
|
+
yield session({ id: 'which', node_id: 'old-boot', device_udid: 'emulator-5554' });
|
|
188
|
+
const res = yield (0, loopbackRequest_1.default)(app()).get('/session');
|
|
189
|
+
(0, chai_1.expect)(res.body[0].ranOn).to.equal(null);
|
|
190
|
+
}));
|
|
191
|
+
});
|
|
192
|
+
describe('GET /session/:id', () => {
|
|
193
|
+
// The detail page's header names who ran the session and where, as its row does.
|
|
194
|
+
it('says who ran the session and where, as the list does', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
195
|
+
yield user('u-priya', 'Priya Shah', 'priya@example.com');
|
|
196
|
+
yield scratch.db.device.create({
|
|
197
|
+
data: {
|
|
198
|
+
udid: 'node-phone',
|
|
199
|
+
host: 'http://10.0.0.9:4725',
|
|
200
|
+
nodeId: 'node-2',
|
|
201
|
+
platform: 'ios',
|
|
202
|
+
},
|
|
203
|
+
});
|
|
204
|
+
yield session({
|
|
205
|
+
id: 'one',
|
|
206
|
+
user_id: 'u-priya',
|
|
207
|
+
node_id: 'node-2',
|
|
208
|
+
device_udid: 'node-phone',
|
|
209
|
+
});
|
|
210
|
+
const res = yield (0, loopbackRequest_1.default)(app()).get('/session/one');
|
|
211
|
+
(0, chai_1.expect)(res.status).to.equal(200);
|
|
212
|
+
(0, chai_1.expect)(res.body.id).to.equal('one');
|
|
213
|
+
(0, chai_1.expect)(res.body.owner).to.deep.equal({ name: 'Priya Shah', email: 'priya@example.com' });
|
|
214
|
+
(0, chai_1.expect)(res.body.ranOn).to.equal('10.0.0.9:4725');
|
|
215
|
+
}));
|
|
216
|
+
});
|
|
217
|
+
describe('GET /session-summary', () => {
|
|
218
|
+
it('summarizes the period and the one before it, of the same length', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
219
|
+
const build = yield scratch.db.build.create({ data: { name: 'nightly' } });
|
|
220
|
+
const inPeriod = (status, minutesAgo, durationS, udid = 'phone-1') => session({
|
|
221
|
+
status,
|
|
222
|
+
build_id: build.id,
|
|
223
|
+
device_udid: udid,
|
|
224
|
+
createdAt: ago(minutesAgo * MIN),
|
|
225
|
+
startTime: ago(minutesAgo * MIN),
|
|
226
|
+
endTime: durationS === undefined ? null : new Date(now - minutesAgo * MIN + durationS * 1000),
|
|
227
|
+
});
|
|
228
|
+
// The period: the last hour.
|
|
229
|
+
yield inPeriod('success', 50, 10);
|
|
230
|
+
yield inPeriod('passed', 45, 20);
|
|
231
|
+
yield inPeriod('ended', 40, 30);
|
|
232
|
+
yield inPeriod('failed', 35, 40);
|
|
233
|
+
yield inPeriod('error', 30, 50);
|
|
234
|
+
yield inPeriod('timeout', 25, 600);
|
|
235
|
+
yield inPeriod('unmarked', 20, 5);
|
|
236
|
+
yield inPeriod('running', 10, undefined, 'phone-1');
|
|
237
|
+
yield inPeriod('running', 5, undefined, 'phone-2');
|
|
238
|
+
// The hour before it.
|
|
239
|
+
yield inPeriod('success', 90, 10);
|
|
240
|
+
yield inPeriod('failed', 80, 10);
|
|
241
|
+
// Older than both, still running: running now, but in neither period.
|
|
242
|
+
yield inPeriod('running', 300, undefined, 'phone-1');
|
|
243
|
+
const res = yield (0, loopbackRequest_1.default)(app()).get(`/session-summary?since=${ago(HOUR).toISOString()}`);
|
|
244
|
+
(0, chai_1.expect)(res.status).to.equal(200);
|
|
245
|
+
(0, chai_1.expect)(res.body.since).to.equal(ago(HOUR).toISOString());
|
|
246
|
+
(0, chai_1.expect)(res.body.current).to.deep.equal({
|
|
247
|
+
total: 9,
|
|
248
|
+
passed: 3,
|
|
249
|
+
failed: 3,
|
|
250
|
+
running: 2,
|
|
251
|
+
// Of every session in it that has ended, verdict or not: 5, 10, 20,
|
|
252
|
+
// 30, 40, 50 and 600 s. Nearest rank: the 4th and the 7th of 7.
|
|
253
|
+
medianMs: 30000,
|
|
254
|
+
p90Ms: 600000,
|
|
255
|
+
});
|
|
256
|
+
(0, chai_1.expect)(res.body.previous).to.deep.equal({ total: 2, passed: 1, failed: 1, running: 0 });
|
|
257
|
+
(0, chai_1.expect)(res.body.runningNow).to.deep.equal({ sessions: 3, devices: 2 });
|
|
258
|
+
}));
|
|
259
|
+
it('covers one build, with no period before it, when asked for a build', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
260
|
+
const mine = yield scratch.db.build.create({ data: { name: 'mine' } });
|
|
261
|
+
const other = yield scratch.db.build.create({ data: { name: 'other' } });
|
|
262
|
+
yield session({
|
|
263
|
+
build_id: mine.id,
|
|
264
|
+
status: 'success',
|
|
265
|
+
startTime: ago(2 * MIN),
|
|
266
|
+
endTime: ago(MIN),
|
|
267
|
+
});
|
|
268
|
+
yield session({ build_id: mine.id, status: 'running' });
|
|
269
|
+
yield session({ build_id: other.id, status: 'failed' });
|
|
270
|
+
yield session({ build_id: other.id, status: 'running', device_udid: 'phone-9' });
|
|
271
|
+
const res = yield (0, loopbackRequest_1.default)(app()).get(`/session-summary?buildId=${mine.id}`);
|
|
272
|
+
(0, chai_1.expect)(res.body.since).to.equal(null);
|
|
273
|
+
(0, chai_1.expect)(res.body.current).to.deep.equal({
|
|
274
|
+
total: 2,
|
|
275
|
+
passed: 1,
|
|
276
|
+
failed: 0,
|
|
277
|
+
running: 1,
|
|
278
|
+
medianMs: 60000,
|
|
279
|
+
p90Ms: 60000,
|
|
280
|
+
});
|
|
281
|
+
(0, chai_1.expect)(res.body.previous).to.equal(null);
|
|
282
|
+
(0, chai_1.expect)(res.body.runningNow).to.deep.equal({ sessions: 1, devices: 1 });
|
|
283
|
+
}));
|
|
284
|
+
it('has no durations to report when nothing has finished', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
285
|
+
yield session({ status: 'running' });
|
|
286
|
+
const res = yield (0, loopbackRequest_1.default)(app()).get('/session-summary');
|
|
287
|
+
(0, chai_1.expect)(res.body.current.medianMs).to.equal(null);
|
|
288
|
+
(0, chai_1.expect)(res.body.current.p90Ms).to.equal(null);
|
|
289
|
+
}));
|
|
290
|
+
it('counts only the sessions the caller may see', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
291
|
+
const team = yield scratch.db.team.create({ data: { name: 'b-team' } });
|
|
292
|
+
yield scratch.db.device.create({
|
|
293
|
+
data: {
|
|
294
|
+
udid: 'shared',
|
|
295
|
+
host: 'http://127.0.0.1:4723',
|
|
296
|
+
nodeId: 'hub-1',
|
|
297
|
+
platform: 'android',
|
|
298
|
+
},
|
|
299
|
+
});
|
|
300
|
+
yield scratch.db.device.create({
|
|
301
|
+
data: {
|
|
302
|
+
udid: 'team-b',
|
|
303
|
+
host: 'http://127.0.0.1:4723',
|
|
304
|
+
nodeId: 'hub-1',
|
|
305
|
+
platform: 'android',
|
|
306
|
+
teamId: team.id,
|
|
307
|
+
},
|
|
308
|
+
});
|
|
309
|
+
yield session({ device_udid: 'shared', status: 'success' });
|
|
310
|
+
yield session({ device_udid: 'team-b', status: 'failed' });
|
|
311
|
+
yield session({ device_udid: 'team-b', status: 'running' });
|
|
312
|
+
caller = {
|
|
313
|
+
kind: 'user-session',
|
|
314
|
+
userId: 'member-1',
|
|
315
|
+
role: 'MEMBER',
|
|
316
|
+
scopes: 'sessions',
|
|
317
|
+
teamIds: [],
|
|
318
|
+
};
|
|
319
|
+
const res = yield (0, loopbackRequest_1.default)(app()).get('/session-summary');
|
|
320
|
+
(0, chai_1.expect)(res.body.current).to.include({ total: 1, passed: 1, failed: 0, running: 0 });
|
|
321
|
+
(0, chai_1.expect)(res.body.runningNow).to.deep.equal({ sessions: 0, devices: 0 });
|
|
322
|
+
}));
|
|
323
|
+
it('refuses a since that is not a date', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
324
|
+
const res = yield (0, loopbackRequest_1.default)(app()).get('/session-summary?since=soon');
|
|
325
|
+
(0, chai_1.expect)(res.status).to.equal(400);
|
|
326
|
+
(0, chai_1.expect)(res.body.error).to.equal('invalid_since');
|
|
327
|
+
}));
|
|
328
|
+
});
|
|
329
|
+
describe('GET /build', () => {
|
|
330
|
+
// The page's pass/fail bar and the session rows must agree on a verdict.
|
|
331
|
+
it('counts every failing status as failed and every passing one as passed', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
332
|
+
const build = yield scratch.db.build.create({ data: { name: 'nightly' } });
|
|
333
|
+
for (const status of [
|
|
334
|
+
'success',
|
|
335
|
+
'passed',
|
|
336
|
+
'ended',
|
|
337
|
+
'failed',
|
|
338
|
+
'error',
|
|
339
|
+
'timeout',
|
|
340
|
+
'running',
|
|
341
|
+
'unmarked',
|
|
342
|
+
]) {
|
|
343
|
+
yield session({ build_id: build.id, status });
|
|
344
|
+
}
|
|
345
|
+
const res = yield (0, loopbackRequest_1.default)(app()).get('/build');
|
|
346
|
+
(0, chai_1.expect)(res.body[0]).to.include({
|
|
347
|
+
sessionCount: 8,
|
|
348
|
+
passedCount: 3,
|
|
349
|
+
failedCount: 3,
|
|
350
|
+
runningCount: 1,
|
|
351
|
+
});
|
|
352
|
+
}));
|
|
353
|
+
});
|
|
354
|
+
});
|