@xenon-device-management/xenon 2.9.2 → 2.10.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-B87n4pUu.js → AnnotationOverlay-LzfXiiZJ.js} +1 -1
- package/lib/public/assets/{ApiKeyGate-CNZJH6Cs.js → ApiKeyGate-BzFDp4mK.js} +1 -1
- package/lib/public/assets/{BugReportButton-DQ-R87ZV.js → BugReportButton-BpgWvmZo.js} +1 -1
- package/lib/public/assets/{DeviceMosaicView-dX3e7O-w.js → DeviceMosaicView-CCB-egSH.js} +2 -2
- package/lib/public/assets/{EmptyState-D3fG2GHY.js → EmptyState-DkUVmANo.js} +1 -1
- package/lib/public/assets/{FieldGroup-CUmbJZxI.js → FieldGroup-mbfnfTql.js} +1 -1
- package/lib/public/assets/{FilterMenu-Dm2c-UCk.js → FilterMenu-BH5usz4Y.js} +1 -1
- package/lib/public/assets/{Menu-Dl4uXMkH.js → Menu-Cbo9mdTk.js} +1 -1
- package/lib/public/assets/{Modal-DNmn2RDs.js → Modal-D6XfQHPr.js} +1 -1
- package/lib/public/assets/{RecordingPage-Dn3By34o.js → RecordingPage-CmFPkWqH.js} +1 -1
- package/lib/public/assets/{RecordingsPage-lCUIM4pm.js → RecordingsPage-CSlKk2qC.js} +1 -1
- package/lib/public/assets/{SegmentedControl-BdA_mHiZ.js → SegmentedControl-BFW9Dlqd.js} +1 -1
- package/lib/public/assets/{SettingCard-Dx3WhK7w.js → SettingCard-C4DioTTi.js} +1 -1
- package/lib/public/assets/{Table-D_2lhvRM.js → Table-DrKjlL00.js} +1 -1
- package/lib/public/assets/{activity-cNHHOKx5.js → activity-C9bE7Jm8.js} +1 -1
- package/lib/public/assets/{ai-settings-t6RMrDkg.js → ai-settings-DRYic4oO.js} +1 -1
- package/lib/public/assets/{api-keys-CK_qyQvW.js → api-keys-CaQU0g7c.js} +1 -1
- package/lib/public/assets/{apps-DCiI3UOb.js → apps-c1gUFmmK.js} +1 -1
- package/lib/public/assets/{arrow-left-CnYs8HVw.js → arrow-left-DMvAWA8r.js} +1 -1
- package/lib/public/assets/{arrow-right-Cc9qm6UG.js → arrow-right-BEQgxmKT.js} +1 -1
- package/lib/public/assets/{arrow-up-right-e7yBOb-c.js → arrow-up-right-Ba0e2GeU.js} +1 -1
- package/lib/public/assets/{auth-shell-DKrtcIjG.js → auth-shell-COODQnc7.js} +2 -2
- package/lib/public/assets/{builds-page-CRd5oEz6.js → builds-page-DZQgWBUs.js} +4 -4
- package/lib/public/assets/{button-C0caCjJB.js → button-WW9j43wR.js} +1 -1
- package/lib/public/assets/{calendar-BqOOkln_.js → calendar-COUs9VxF.js} +1 -1
- package/lib/public/assets/{check-DYSXRG79.js → check-D8ifHIJR.js} +1 -1
- package/lib/public/assets/{chevron-right-hCNu0CA-.js → chevron-right-BjKVUlB_.js} +1 -1
- package/lib/public/assets/{circle-check-DnQ263fa.js → circle-check-CfP79QE2.js} +1 -1
- package/lib/public/assets/{circle-x-BaRKZyz9.js → circle-x-BZbFCixZ.js} +1 -1
- package/lib/public/assets/{clock-XP3iBAhg.js → clock-BuTm35Qg.js} +1 -1
- package/lib/public/assets/{copy-D8d9UzWE.js → copy-W00zUBwN.js} +1 -1
- package/lib/public/assets/{device-explorer-pdM3L9TC.js → device-explorer-BEpS-Y-_.js} +5 -5
- package/lib/public/assets/{download-SaN9-umX.js → download-CGz-MS3y.js} +1 -1
- package/lib/public/assets/{forgot-password-DfUhEZW5.js → forgot-password-DG7SqVgU.js} +1 -1
- package/lib/public/assets/{index-C8pvxG1f.css → index-DQa193-A.css} +1 -1
- package/lib/public/assets/{index-IefGCFQQ.js → index-DWHTp1YP.js} +1 -1
- package/lib/public/assets/{index-CzaIyL9t.js → index-EkaQM1DK.js} +2 -2
- package/lib/public/assets/{input-BqFzAQT3.js → input-ChaTXeCq.js} +1 -1
- package/lib/public/assets/{keycodes-DDV2OlpQ.js → keycodes-BhbgeW_r.js} +10 -5
- package/lib/public/assets/line-chart-9P3RxScy.js +1 -0
- package/lib/public/assets/list-checks-_23IIEEL.js +11 -0
- package/lib/public/assets/{lock-Eye1CG8p.js → lock-B2JaqqRI.js} +1 -1
- package/lib/public/assets/{login-Co1IXuen.js → login-BEWIfLfC.js} +1 -1
- package/lib/public/assets/{maintenance-settings-D1fw6iqm.js → maintenance-settings-BGJIJQ-k.js} +1 -1
- package/lib/public/assets/{monitor-U5Y-PxW5.js → monitor-BrwMY4G0.js} +1 -1
- package/lib/public/assets/{mouse-pointer-2-CDAwphUm.js → mouse-pointer-2-DsWDMyrY.js} +1 -1
- package/lib/public/assets/{network-BP_U4iDZ.js → network-DfIhNVJv.js} +1 -1
- package/lib/public/assets/overview-B1mePY2_.js +6 -0
- package/lib/public/assets/{page-header-ZdzNEJp9.js → page-header-DoD3TOzZ.js} +1 -1
- package/lib/public/assets/{plus-1k0sxXyU.js → plus-BGvWvpBB.js} +1 -1
- package/lib/public/assets/{profile-page-B_GWKRy3.js → profile-page-CvV5aWvl.js} +1 -1
- package/lib/public/assets/{reset-password-DizCTkdw.js → reset-password-CGVd31LR.js} +1 -1
- package/lib/public/assets/{runbook-page-DWzRiOLZ.js → runbook-page-Bq2cvwb5.js} +1 -1
- package/lib/public/assets/{select-CiRW6v17.js → select-CTcD4jWR.js} +1 -1
- package/lib/public/assets/selector-detail-redirect-19odlD4B.js +1 -0
- package/lib/public/assets/selector-health-page-LqIsLrjW.js +11 -0
- package/lib/public/assets/session-detail-page-DQzb8_fq.js +4 -0
- package/lib/public/assets/{settings-DhwZoWv7.js → settings-C-EnGIqs.js} +1 -1
- package/lib/public/assets/stat-tile-BY0RDTRm.js +1 -0
- package/lib/public/assets/{tablet-CPJV3dBq.js → tablet-B4cFM-5B.js} +1 -1
- package/lib/public/assets/{teams-D0IBa2Ov.js → teams-BFe-RpYd.js} +1 -1
- package/lib/public/assets/time-D-gjmE1o.js +1 -0
- package/lib/public/assets/{trash-2-u8MlXUBD.js → trash-2-CJCKBOP9.js} +1 -1
- package/lib/public/assets/{upload-Cq8L3kpw.js → upload-DidWkbld.js} +1 -1
- package/lib/public/assets/use-builds-data-fnfxatOd.js +8 -0
- package/lib/public/assets/{use-password-reset-mode-Dbr2HEfw.js → use-password-reset-mode-iPen3tmC.js} +1 -1
- package/lib/public/assets/{users-CZqCE511.js → users-CrcjmRdf.js} +1 -1
- package/lib/public/assets/{users-DxLHYmFm.js → users-DBUv0CJx.js} +1 -1
- package/lib/public/assets/{video-off-mh1EEzP9.js → video-off-eh69-FKB.js} +1 -1
- package/lib/public/assets/view-state-CP9dE1UI.js +1 -0
- package/lib/public/assets/{webhook-settings-BYuOib-Q.js → webhook-settings-CQoSshBn.js} +1 -1
- package/lib/public/assets/{zap-BOpUur0M.js → zap-CICtWiLn.js} +1 -1
- package/lib/public/index.html +2 -2
- package/lib/schema.json +5 -0
- package/lib/src/app/routers/dashboard.js +86 -40
- package/lib/src/app/routers/selector-health.js +75 -0
- package/lib/src/app/swagger-docs.js +77 -8
- package/lib/src/dashboard/event-manager.js +15 -99
- package/lib/src/generated/client/edge.js +27 -3
- package/lib/src/generated/client/index-browser.js +24 -0
- package/lib/src/generated/client/index.d.ts +8207 -5283
- package/lib/src/generated/client/index.js +27 -3
- package/lib/src/generated/client/package.json +1 -1
- package/lib/src/generated/client/schema.prisma +43 -9
- package/lib/src/generated/client/wasm.js +24 -0
- package/lib/src/prisma.js +2 -0
- package/lib/src/services/CleanupService.js +1 -0
- package/lib/src/services/NotificationService.js +3 -4
- package/lib/src/services/SelectorStateService.js +151 -113
- package/lib/src/services/SelectorVerificationJob.js +14 -9
- package/lib/src/services/metrics/AndroidMetricsSampler.js +94 -0
- package/lib/src/services/metrics/IOSMetricsSampler.js +120 -0
- package/lib/src/services/metrics/SessionMetricsService.js +238 -0
- package/lib/src/services/metrics/androidMetrics.js +94 -0
- package/lib/src/services/metrics/iosMetrics.js +26 -0
- package/lib/src/services/metrics/metricsBody.js +37 -0
- package/lib/src/services/metrics/types.js +19 -0
- package/lib/src/services/selector-health/access.js +66 -0
- package/lib/src/services/selector-health/healingTrend.js +49 -0
- package/lib/src/services/selector-health/people.js +27 -0
- package/lib/src/services/selector-health/selectorDetail.js +198 -0
- package/lib/src/services/selector-health/selectorKeys.js +36 -0
- package/lib/src/services/selector-health/selectorList.js +333 -0
- package/lib/src/services/selector-health/stateView.js +67 -0
- package/lib/test/helpers/scratch-database.js +2 -0
- package/lib/test/helpers/selector-health-fixture.js +216 -0
- package/lib/test/integration/team-visibility-sessions.spec.js +1 -0
- package/lib/test/unit/CleanupService.spec.js +28 -2
- package/lib/test/unit/dashboard-events-metrics.spec.js +146 -0
- package/lib/test/unit/healing-no-cost.spec.js +134 -0
- package/lib/test/unit/healing-state-endpoints.spec.js +70 -0
- package/lib/test/unit/healing-trend.spec.js +118 -0
- package/lib/test/unit/selector-event-model.spec.js +47 -0
- package/lib/test/unit/selector-health-actions.spec.js +106 -0
- package/lib/test/unit/selector-health-detail.spec.js +131 -0
- package/lib/test/unit/selector-health-list.spec.js +185 -0
- package/lib/test/unit/selector-state-service.spec.js +74 -1
- package/lib/test/unit/selector-verification-job.spec.js +38 -0
- package/lib/test/unit/session-metrics-parse.spec.js +138 -0
- package/lib/test/unit/session-metrics-route.spec.js +140 -0
- package/lib/test/unit/session-metrics-samplers.spec.js +248 -0
- package/lib/test/unit/session-metrics-service.spec.js +215 -0
- package/lib/tsconfig.tsbuildinfo +1 -1
- package/package.json +1 -1
- package/prisma/migrations/20261002120000_session_metric/migration.sql +17 -0
- package/prisma/migrations/20261002130000_selector_event/migration.sql +13 -0
- package/prisma/schema.prisma +43 -9
- package/schema.json +5 -0
- package/lib/public/assets/chevron-left-BSG6mcpo.js +0 -6
- package/lib/public/assets/funnel-OefHE-54.js +0 -6
- package/lib/public/assets/layers-a3GNz2DA.js +0 -6
- package/lib/public/assets/list-checks-DMrMAFDt.js +0 -6
- package/lib/public/assets/overview-CsPyPv10.js +0 -1
- package/lib/public/assets/selector-detail-page-pA913STP.js +0 -1
- package/lib/public/assets/selector-health-BlICzZlk.css +0 -1
- package/lib/public/assets/selector-health-CHLX8KqY.js +0 -16
- package/lib/public/assets/selector-health-page-vVkg8Mii.js +0 -16
- package/lib/public/assets/session-detail-page-BCD49zDi.js +0 -4
- package/lib/public/assets/stat-tile-BaE76cnK.js +0 -8
- package/lib/public/assets/time-B8FIgwPE.js +0 -1
- package/lib/src/profiling/AndroidAppProfiler.js +0 -213
- /package/lib/public/assets/{session-detail-page-Bzzc3bMl.css → line-chart-Bzzc3bMl.css} +0 -0
|
@@ -0,0 +1,106 @@
|
|
|
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 supertest_1 = __importDefault(require("supertest"));
|
|
18
|
+
const typedi_1 = require("typedi");
|
|
19
|
+
const scratch_database_1 = require("../helpers/scratch-database");
|
|
20
|
+
const selector_health_fixture_1 = require("../helpers/selector-health-fixture");
|
|
21
|
+
const SelectorStateService_1 = require("../../src/services/SelectorStateService");
|
|
22
|
+
describe('POST /healing/selector/state (real queries)', function () {
|
|
23
|
+
this.timeout(90000);
|
|
24
|
+
const scratch = (0, scratch_database_1.useScratchDatabase)();
|
|
25
|
+
before(() => __awaiter(this, void 0, void 0, function* () {
|
|
26
|
+
yield (0, selector_health_fixture_1.seedSelectorHealth)(scratch.db);
|
|
27
|
+
// The service's transactions use the scratch database's own client.
|
|
28
|
+
typedi_1.Container.set(SelectorStateService_1.SelectorStateService, new SelectorStateService_1.SelectorStateService(scratch.db, { emitToDashboard: () => undefined }));
|
|
29
|
+
}));
|
|
30
|
+
after(() => {
|
|
31
|
+
typedi_1.Container.remove(SelectorStateService_1.SelectorStateService);
|
|
32
|
+
});
|
|
33
|
+
const act = (auth, body) => (0, supertest_1.default)((0, selector_health_fixture_1.selectorHealthApp)(auth)).post('/healing/selector/state').send(body);
|
|
34
|
+
it('lets a member mute a selector they can see, recording them and their reason', () => __awaiter(this, void 0, void 0, function* () {
|
|
35
|
+
const res = yield act(selector_health_fixture_1.MEMBER_A, {
|
|
36
|
+
original_strategy: 'id',
|
|
37
|
+
original_selector: selector_health_fixture_1.SEL.warm,
|
|
38
|
+
action: 'mute',
|
|
39
|
+
reason: ' Moving to the new cart ',
|
|
40
|
+
});
|
|
41
|
+
(0, chai_1.expect)(res.status).to.equal(200);
|
|
42
|
+
const state = yield scratch.db.selectorState.findUnique({
|
|
43
|
+
where: {
|
|
44
|
+
original_strategy_original_selector: {
|
|
45
|
+
original_strategy: 'id',
|
|
46
|
+
original_selector: selector_health_fixture_1.SEL.warm,
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
});
|
|
50
|
+
(0, chai_1.expect)(state === null || state === void 0 ? void 0 : state.status).to.equal('muted');
|
|
51
|
+
const events = yield scratch.db.selectorEvent.findMany({
|
|
52
|
+
where: { original_selector: selector_health_fixture_1.SEL.warm },
|
|
53
|
+
});
|
|
54
|
+
(0, chai_1.expect)(events.map((e) => [e.action, e.user_id, e.reason])).to.deep.equal([
|
|
55
|
+
['muted', selector_health_fixture_1.USER.priya, 'Moving to the new cart'],
|
|
56
|
+
]);
|
|
57
|
+
}));
|
|
58
|
+
it("refuses a member another team's selector, as if it didn't exist", () => __awaiter(this, void 0, void 0, function* () {
|
|
59
|
+
const res = yield act(selector_health_fixture_1.MEMBER_A, {
|
|
60
|
+
original_strategy: 'xpath',
|
|
61
|
+
original_selector: selector_health_fixture_1.SEL.bOnly,
|
|
62
|
+
action: 'mark_fixed',
|
|
63
|
+
});
|
|
64
|
+
(0, chai_1.expect)(res.status).to.equal(404);
|
|
65
|
+
(0, chai_1.expect)(res.body).to.deep.equal({ error: 'not_found', message: 'Selector not found' });
|
|
66
|
+
(0, chai_1.expect)(yield scratch.db.selectorState.count({ where: { original_selector: selector_health_fixture_1.SEL.bOnly } })).to.equal(0);
|
|
67
|
+
}));
|
|
68
|
+
it('lets an admin act on any selector', () => __awaiter(this, void 0, void 0, function* () {
|
|
69
|
+
const res = yield act(selector_health_fixture_1.ADMIN, {
|
|
70
|
+
original_strategy: 'xpath',
|
|
71
|
+
original_selector: selector_health_fixture_1.SEL.bOnly,
|
|
72
|
+
action: 'mark_fixed',
|
|
73
|
+
});
|
|
74
|
+
(0, chai_1.expect)(res.status).to.equal(200);
|
|
75
|
+
(0, chai_1.expect)(res.body.state.status).to.equal('pending');
|
|
76
|
+
}));
|
|
77
|
+
it('refuses a caller without the sessions scope', () => __awaiter(this, void 0, void 0, function* () {
|
|
78
|
+
const res = yield act(selector_health_fixture_1.READ_ONLY_A, {
|
|
79
|
+
original_strategy: 'xpath',
|
|
80
|
+
original_selector: selector_health_fixture_1.SEL.hot,
|
|
81
|
+
action: 'mute',
|
|
82
|
+
});
|
|
83
|
+
(0, chai_1.expect)(res.status).to.equal(403);
|
|
84
|
+
}));
|
|
85
|
+
it('marks fixed a selector recorded with no strategy', () => __awaiter(this, void 0, void 0, function* () {
|
|
86
|
+
const res = yield act(selector_health_fixture_1.MEMBER_A, {
|
|
87
|
+
original_strategy: '',
|
|
88
|
+
original_selector: selector_health_fixture_1.SEL.legacy,
|
|
89
|
+
action: 'mark_fixed',
|
|
90
|
+
});
|
|
91
|
+
(0, chai_1.expect)(res.status).to.equal(200);
|
|
92
|
+
const state = yield scratch.db.selectorState.findFirst({
|
|
93
|
+
where: { original_selector: selector_health_fixture_1.SEL.legacy },
|
|
94
|
+
});
|
|
95
|
+
(0, chai_1.expect)(state).to.include({ original_strategy: '', status: 'pending' });
|
|
96
|
+
}));
|
|
97
|
+
it('refuses a reason over 500 characters', () => __awaiter(this, void 0, void 0, function* () {
|
|
98
|
+
const res = yield act(selector_health_fixture_1.MEMBER_A, {
|
|
99
|
+
original_strategy: 'xpath',
|
|
100
|
+
original_selector: selector_health_fixture_1.SEL.hot,
|
|
101
|
+
action: 'mute',
|
|
102
|
+
reason: 'x'.repeat(501),
|
|
103
|
+
});
|
|
104
|
+
(0, chai_1.expect)(res.status).to.equal(400);
|
|
105
|
+
}));
|
|
106
|
+
});
|
|
@@ -0,0 +1,131 @@
|
|
|
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 supertest_1 = __importDefault(require("supertest"));
|
|
18
|
+
const scratch_database_1 = require("../helpers/scratch-database");
|
|
19
|
+
const selector_health_fixture_1 = require("../helpers/selector-health-fixture");
|
|
20
|
+
const selectorDetail_1 = require("../../src/services/selector-health/selectorDetail");
|
|
21
|
+
describe('selector panel: the query', () => {
|
|
22
|
+
it('needs a selector, and reads the rest with defaults', () => {
|
|
23
|
+
(0, chai_1.expect)((0, selectorDetail_1.parseSelectorDetailQuery)({})).to.equal(null);
|
|
24
|
+
(0, chai_1.expect)((0, selectorDetail_1.parseSelectorDetailQuery)({ selector: '//a' })).to.deep.equal({
|
|
25
|
+
strategy: '',
|
|
26
|
+
selector: '//a',
|
|
27
|
+
days: 30,
|
|
28
|
+
tz: 0,
|
|
29
|
+
});
|
|
30
|
+
(0, chai_1.expect)((0, selectorDetail_1.parseSelectorDetailQuery)({ strategy: 'xpath', selector: '//a', days: '7', tz: '330' })).to.deep.equal({ strategy: 'xpath', selector: '//a', days: 7, tz: 330 });
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
describe('GET /healing/selectors/detail (real queries)', function () {
|
|
34
|
+
this.timeout(90000);
|
|
35
|
+
const scratch = (0, scratch_database_1.useScratchDatabase)();
|
|
36
|
+
before(() => __awaiter(this, void 0, void 0, function* () {
|
|
37
|
+
yield (0, selector_health_fixture_1.seedSelectorHealth)(scratch.db);
|
|
38
|
+
}));
|
|
39
|
+
const detail = (auth, query) => (0, supertest_1.default)((0, selector_health_fixture_1.selectorHealthApp)(auth)).get('/healing/selectors/detail').query(query);
|
|
40
|
+
it('answers everything the panel shows', () => __awaiter(this, void 0, void 0, function* () {
|
|
41
|
+
const res = yield detail(selector_health_fixture_1.ADMIN, { strategy: 'xpath', selector: selector_health_fixture_1.SEL.hot, days: '30', tz: '0' });
|
|
42
|
+
(0, chai_1.expect)(res.status).to.equal(200);
|
|
43
|
+
const b = res.body;
|
|
44
|
+
(0, chai_1.expect)(b).to.deep.include({
|
|
45
|
+
strategy: 'xpath',
|
|
46
|
+
selector: selector_health_fixture_1.SEL.hot,
|
|
47
|
+
days: 30,
|
|
48
|
+
heals: 4,
|
|
49
|
+
sessions: 2,
|
|
50
|
+
timeSpentMs: 18000,
|
|
51
|
+
canAct: true,
|
|
52
|
+
});
|
|
53
|
+
(0, chai_1.expect)(b.suggestions.map((s) => [
|
|
54
|
+
s.selector,
|
|
55
|
+
s.strategy,
|
|
56
|
+
s.count,
|
|
57
|
+
s.share,
|
|
58
|
+
s.methods,
|
|
59
|
+
])).to.deep.equal([
|
|
60
|
+
[selector_health_fixture_1.FIX.hot, 'xpath', 3, 0.75, ['Visual AI']],
|
|
61
|
+
[selector_health_fixture_1.FIX.hotLlm, 'accessibility id', 1, 0.25, ['LLM']],
|
|
62
|
+
]);
|
|
63
|
+
(0, chai_1.expect)(b.suggestions[0].averageConfidence).to.be.closeTo(0.9, 1e-9);
|
|
64
|
+
(0, chai_1.expect)(b.platforms).to.deep.equal([{ name: 'android', count: 4 }]);
|
|
65
|
+
(0, chai_1.expect)(b.builds).to.deep.equal([
|
|
66
|
+
{ id: selector_health_fixture_1.BUILD.id, name: selector_health_fixture_1.BUILD.name, count: 3 },
|
|
67
|
+
{ id: null, name: 'No build', count: 1 },
|
|
68
|
+
]);
|
|
69
|
+
(0, chai_1.expect)(b.devices).to.deep.equal([{ udid: selector_health_fixture_1.PHONE.shared, name: 'Shared Pixel', count: 4 }]);
|
|
70
|
+
(0, chai_1.expect)(b.recent).to.have.length(4);
|
|
71
|
+
(0, chai_1.expect)(b.recent[0]).to.include({
|
|
72
|
+
sessionId: 'sh-s-shared-1',
|
|
73
|
+
buildId: selector_health_fixture_1.BUILD.id,
|
|
74
|
+
device: 'Shared Pixel',
|
|
75
|
+
method: 'Visual AI',
|
|
76
|
+
healedSelector: selector_health_fixture_1.FIX.hot,
|
|
77
|
+
});
|
|
78
|
+
(0, chai_1.expect)(b.recent[3]).to.include({ sessionId: 'sh-s-shared-2', buildId: null, method: 'LLM' });
|
|
79
|
+
(0, chai_1.expect)(b.daily).to.have.length(31);
|
|
80
|
+
(0, chai_1.expect)(b.daily.reduce((s, d) => s + d.heals, 0)).to.equal(4);
|
|
81
|
+
(0, chai_1.expect)(b.state).to.include({ status: 'active', brokeAgain: 2 });
|
|
82
|
+
(0, chai_1.expect)(b.activity).to.deep.equal([]);
|
|
83
|
+
}));
|
|
84
|
+
it('shows who did what, newest first, naming nobody for a user who is gone', () => __awaiter(this, void 0, void 0, function* () {
|
|
85
|
+
const b = (yield detail(selector_health_fixture_1.ADMIN, { strategy: 'xpath', selector: selector_health_fixture_1.SEL.fixed })).body;
|
|
86
|
+
(0, chai_1.expect)(b.activity.map((a) => [a.action, a.by])).to.deep.equal([
|
|
87
|
+
['verified', null],
|
|
88
|
+
['marked_fixed', { id: selector_health_fixture_1.USER.gone, name: null }],
|
|
89
|
+
]);
|
|
90
|
+
(0, chai_1.expect)(b.state).to.deep.include({ status: 'resolved', fixedBy: { id: selector_health_fixture_1.USER.gone, name: null } });
|
|
91
|
+
}));
|
|
92
|
+
it("gives a member a muted selector's reason, and who muted it", () => __awaiter(this, void 0, void 0, function* () {
|
|
93
|
+
const b = (yield detail(selector_health_fixture_1.MEMBER_A, { strategy: 'xpath', selector: selector_health_fixture_1.SEL.muted })).body;
|
|
94
|
+
(0, chai_1.expect)(b.activity).to.have.length(1);
|
|
95
|
+
(0, chai_1.expect)(b.activity[0]).to.deep.include({
|
|
96
|
+
action: 'muted',
|
|
97
|
+
by: { id: selector_health_fixture_1.USER.priya, name: 'Priya' },
|
|
98
|
+
reason: 'Screen being redesigned',
|
|
99
|
+
});
|
|
100
|
+
(0, chai_1.expect)(b.state).to.deep.include({
|
|
101
|
+
mutedBy: { id: selector_health_fixture_1.USER.priya, name: 'Priya' },
|
|
102
|
+
muteReason: 'Screen being redesigned',
|
|
103
|
+
});
|
|
104
|
+
}));
|
|
105
|
+
it("answers another team's selector exactly as one that doesn't exist", () => __awaiter(this, void 0, void 0, function* () {
|
|
106
|
+
const hidden = yield detail(selector_health_fixture_1.MEMBER_A, { strategy: 'xpath', selector: selector_health_fixture_1.SEL.bOnly });
|
|
107
|
+
const missing = yield detail(selector_health_fixture_1.MEMBER_A, { strategy: 'xpath', selector: '//nothing/here' });
|
|
108
|
+
(0, chai_1.expect)(hidden.status).to.equal(404);
|
|
109
|
+
(0, chai_1.expect)(hidden.body).to.deep.equal({ error: 'not_found', message: 'Selector not found' });
|
|
110
|
+
(0, chai_1.expect)(missing.status).to.equal(404);
|
|
111
|
+
(0, chai_1.expect)(missing.body).to.deep.equal(hidden.body);
|
|
112
|
+
}));
|
|
113
|
+
it('opens a heal recorded with no strategy, and a selector full of punctuation', () => __awaiter(this, void 0, void 0, function* () {
|
|
114
|
+
(0, chai_1.expect)((yield detail(selector_health_fixture_1.MEMBER_A, { strategy: '', selector: selector_health_fixture_1.SEL.legacy })).body.heals).to.equal(1);
|
|
115
|
+
(0, chai_1.expect)((yield detail(selector_health_fixture_1.MEMBER_A, { strategy: 'xpath', selector: selector_health_fixture_1.SEL.odd })).body.heals).to.equal(1);
|
|
116
|
+
}));
|
|
117
|
+
it("counts days in the caller's time zone", () => __awaiter(this, void 0, void 0, function* () {
|
|
118
|
+
const b = (yield detail(selector_health_fixture_1.ADMIN, { strategy: 'xpath', selector: selector_health_fixture_1.SEL.hot, days: '7', tz: '330' }))
|
|
119
|
+
.body;
|
|
120
|
+
(0, chai_1.expect)(b.daily).to.have.length(8);
|
|
121
|
+
for (const d of b.daily)
|
|
122
|
+
(0, chai_1.expect)((d.t + 330 * 60000) % selector_health_fixture_1.DAY).to.equal(0);
|
|
123
|
+
}));
|
|
124
|
+
it('needs a selector', () => __awaiter(this, void 0, void 0, function* () {
|
|
125
|
+
(0, chai_1.expect)((yield detail(selector_health_fixture_1.ADMIN, {})).status).to.equal(400);
|
|
126
|
+
}));
|
|
127
|
+
it('says a read-only caller cannot act', () => __awaiter(this, void 0, void 0, function* () {
|
|
128
|
+
const b = (yield detail(selector_health_fixture_1.READ_ONLY_A, { strategy: 'xpath', selector: selector_health_fixture_1.SEL.hot })).body;
|
|
129
|
+
(0, chai_1.expect)(b.canAct).to.equal(false);
|
|
130
|
+
}));
|
|
131
|
+
});
|
|
@@ -0,0 +1,185 @@
|
|
|
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 supertest_1 = __importDefault(require("supertest"));
|
|
18
|
+
const scratch_database_1 = require("../helpers/scratch-database");
|
|
19
|
+
const selector_health_fixture_1 = require("../helpers/selector-health-fixture");
|
|
20
|
+
const selectorList_1 = require("../../src/services/selector-health/selectorList");
|
|
21
|
+
describe('selector list: the query', () => {
|
|
22
|
+
it('reads every field, and falls back for anything it does not know', () => {
|
|
23
|
+
(0, chai_1.expect)((0, selectorList_1.parseSelectorListQuery)({})).to.deep.equal({
|
|
24
|
+
tab: 'fix',
|
|
25
|
+
days: 30,
|
|
26
|
+
q: '',
|
|
27
|
+
platform: null,
|
|
28
|
+
method: null,
|
|
29
|
+
sort: 'heals',
|
|
30
|
+
page: 1,
|
|
31
|
+
pageSize: 50,
|
|
32
|
+
});
|
|
33
|
+
(0, chai_1.expect)((0, selectorList_1.parseSelectorListQuery)({
|
|
34
|
+
tab: 'muted',
|
|
35
|
+
days: '7',
|
|
36
|
+
q: ' confirm ',
|
|
37
|
+
platform: 'ios',
|
|
38
|
+
method: 'LLM',
|
|
39
|
+
sort: 'time',
|
|
40
|
+
page: '3',
|
|
41
|
+
pageSize: '20',
|
|
42
|
+
})).to.deep.equal({
|
|
43
|
+
tab: 'muted',
|
|
44
|
+
days: 7,
|
|
45
|
+
q: 'confirm',
|
|
46
|
+
platform: 'ios',
|
|
47
|
+
method: 'LLM',
|
|
48
|
+
sort: 'time',
|
|
49
|
+
page: 3,
|
|
50
|
+
pageSize: 20,
|
|
51
|
+
});
|
|
52
|
+
(0, chai_1.expect)((0, selectorList_1.parseSelectorListQuery)({
|
|
53
|
+
tab: 'active',
|
|
54
|
+
days: '9999',
|
|
55
|
+
sort: 'random',
|
|
56
|
+
page: '-2',
|
|
57
|
+
pageSize: '1000',
|
|
58
|
+
})).to.include({ tab: 'fix', days: 365, sort: 'heals', page: 1, pageSize: 100 });
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
describe('GET /healing/selectors (real queries)', function () {
|
|
62
|
+
this.timeout(90000);
|
|
63
|
+
const scratch = (0, scratch_database_1.useScratchDatabase)();
|
|
64
|
+
before(() => __awaiter(this, void 0, void 0, function* () {
|
|
65
|
+
yield (0, selector_health_fixture_1.seedSelectorHealth)(scratch.db);
|
|
66
|
+
}));
|
|
67
|
+
const list = (auth, qs = '') => (0, supertest_1.default)((0, selector_health_fixture_1.selectorHealthApp)(auth)).get(`/healing/selectors${qs}`);
|
|
68
|
+
const names = (res) => res.body.items.map((i) => i.selector);
|
|
69
|
+
it('lists what to fix, most heals first, with every tab counted', () => __awaiter(this, void 0, void 0, function* () {
|
|
70
|
+
const res = yield list(selector_health_fixture_1.ADMIN);
|
|
71
|
+
(0, chai_1.expect)(res.status).to.equal(200);
|
|
72
|
+
(0, chai_1.expect)(names(res)).to.deep.equal([selector_health_fixture_1.SEL.hot, selector_health_fixture_1.SEL.bOnly, selector_health_fixture_1.SEL.warm, selector_health_fixture_1.SEL.legacy, selector_health_fixture_1.SEL.odd]);
|
|
73
|
+
(0, chai_1.expect)(res.body.counts).to.deep.equal({ fix: 5, verifying: 1, fixed: 1, muted: 2 });
|
|
74
|
+
(0, chai_1.expect)(res.body).to.include({
|
|
75
|
+
tab: 'fix',
|
|
76
|
+
days: 30,
|
|
77
|
+
page: 1,
|
|
78
|
+
pageSize: 50,
|
|
79
|
+
total: 5,
|
|
80
|
+
canAct: true,
|
|
81
|
+
});
|
|
82
|
+
}));
|
|
83
|
+
it('describes each selector: heals, sessions, time, method, suggestion and status', () => __awaiter(this, void 0, void 0, function* () {
|
|
84
|
+
const hot = (yield list(selector_health_fixture_1.ADMIN)).body.items[0];
|
|
85
|
+
(0, chai_1.expect)(hot).to.deep.include({
|
|
86
|
+
strategy: 'xpath',
|
|
87
|
+
selector: selector_health_fixture_1.SEL.hot,
|
|
88
|
+
heals: 4,
|
|
89
|
+
sessions: 2,
|
|
90
|
+
timeSpentMs: 18000,
|
|
91
|
+
topMethod: 'Visual AI',
|
|
92
|
+
});
|
|
93
|
+
(0, chai_1.expect)(hot.suggestion).to.deep.equal({ selector: selector_health_fixture_1.FIX.hot, strategy: 'xpath', share: 0.75 });
|
|
94
|
+
(0, chai_1.expect)(hot.state).to.include({ status: 'active', brokeAgain: 2 });
|
|
95
|
+
(0, chai_1.expect)(Date.now() - Date.parse(hot.lastHealedAt)).to.be.within(0.9 * selector_health_fixture_1.HOUR, 1.5 * selector_health_fixture_1.HOUR);
|
|
96
|
+
}));
|
|
97
|
+
it('lists a heal with no strategy under an empty one', () => __awaiter(this, void 0, void 0, function* () {
|
|
98
|
+
const legacy = (yield list(selector_health_fixture_1.ADMIN)).body.items.find((i) => i.selector === selector_health_fixture_1.SEL.legacy);
|
|
99
|
+
(0, chai_1.expect)(legacy).to.deep.include({ strategy: '', heals: 1 });
|
|
100
|
+
}));
|
|
101
|
+
it('shows a member only selectors healed in sessions they can see, and counts only those', () => __awaiter(this, void 0, void 0, function* () {
|
|
102
|
+
const res = yield list(selector_health_fixture_1.MEMBER_A);
|
|
103
|
+
(0, chai_1.expect)(names(res)).to.deep.equal([selector_health_fixture_1.SEL.hot, selector_health_fixture_1.SEL.warm, selector_health_fixture_1.SEL.legacy, selector_health_fixture_1.SEL.odd]);
|
|
104
|
+
(0, chai_1.expect)(res.body.counts).to.deep.equal({ fix: 4, verifying: 1, fixed: 1, muted: 1 });
|
|
105
|
+
}));
|
|
106
|
+
it('sorts by most recent and by most time spent healing', () => __awaiter(this, void 0, void 0, function* () {
|
|
107
|
+
(0, chai_1.expect)(names(yield list(selector_health_fixture_1.ADMIN, '?sort=recent'))).to.deep.equal([
|
|
108
|
+
selector_health_fixture_1.SEL.hot,
|
|
109
|
+
selector_health_fixture_1.SEL.legacy,
|
|
110
|
+
selector_health_fixture_1.SEL.odd,
|
|
111
|
+
selector_health_fixture_1.SEL.bOnly,
|
|
112
|
+
selector_health_fixture_1.SEL.warm,
|
|
113
|
+
]);
|
|
114
|
+
(0, chai_1.expect)(names(yield list(selector_health_fixture_1.ADMIN, '?sort=time'))).to.deep.equal([
|
|
115
|
+
selector_health_fixture_1.SEL.warm,
|
|
116
|
+
selector_health_fixture_1.SEL.hot,
|
|
117
|
+
selector_health_fixture_1.SEL.bOnly,
|
|
118
|
+
selector_health_fixture_1.SEL.legacy,
|
|
119
|
+
selector_health_fixture_1.SEL.odd,
|
|
120
|
+
]);
|
|
121
|
+
}));
|
|
122
|
+
it('searches the selector and its suggested fix, taking the text literally', () => __awaiter(this, void 0, void 0, function* () {
|
|
123
|
+
(0, chai_1.expect)(names(yield list(selector_health_fixture_1.ADMIN, '?q=CONFIRM'))).to.deep.equal([selector_health_fixture_1.SEL.hot]);
|
|
124
|
+
(0, chai_1.expect)(names(yield list(selector_health_fixture_1.ADMIN, `?q=${encodeURIComponent('cart_total_v2')}`))).to.deep.equal([
|
|
125
|
+
selector_health_fixture_1.SEL.warm,
|
|
126
|
+
]);
|
|
127
|
+
(0, chai_1.expect)(names(yield list(selector_health_fixture_1.ADMIN, `?q=${encodeURIComponent('%')}`))).to.deep.equal([selector_health_fixture_1.SEL.odd]);
|
|
128
|
+
}));
|
|
129
|
+
it('filters what to fix by platform and by healing method, keeping the counts whole', () => __awaiter(this, void 0, void 0, function* () {
|
|
130
|
+
(0, chai_1.expect)(names(yield list(selector_health_fixture_1.ADMIN, '?platform=ios'))).to.deep.equal([selector_health_fixture_1.SEL.bOnly]);
|
|
131
|
+
const llm = yield list(selector_health_fixture_1.ADMIN, '?method=LLM');
|
|
132
|
+
(0, chai_1.expect)(names(llm)).to.deep.equal([selector_health_fixture_1.SEL.hot]);
|
|
133
|
+
(0, chai_1.expect)(llm.body.items[0]).to.deep.include({ heals: 1, topMethod: 'LLM' });
|
|
134
|
+
(0, chai_1.expect)(llm.body.counts.fix).to.equal(5);
|
|
135
|
+
}));
|
|
136
|
+
it('pages, and shows the last page for a page past the end', () => __awaiter(this, void 0, void 0, function* () {
|
|
137
|
+
(0, chai_1.expect)(names(yield list(selector_health_fixture_1.ADMIN, '?pageSize=2&page=2'))).to.deep.equal([selector_health_fixture_1.SEL.warm, selector_health_fixture_1.SEL.legacy]);
|
|
138
|
+
const past = yield list(selector_health_fixture_1.ADMIN, '?pageSize=2&page=99');
|
|
139
|
+
(0, chai_1.expect)(past.body.page).to.equal(3);
|
|
140
|
+
(0, chai_1.expect)(past.body.total).to.equal(5);
|
|
141
|
+
(0, chai_1.expect)(names(past)).to.deep.equal([selector_health_fixture_1.SEL.odd]);
|
|
142
|
+
}));
|
|
143
|
+
it('lists selectors being verified, with their progress and who marked them fixed', () => __awaiter(this, void 0, void 0, function* () {
|
|
144
|
+
const res = yield list(selector_health_fixture_1.MEMBER_A, '?tab=verifying');
|
|
145
|
+
(0, chai_1.expect)(names(res)).to.deep.equal([selector_health_fixture_1.SEL.pending]);
|
|
146
|
+
(0, chai_1.expect)(res.body.items[0].heals).to.equal(1);
|
|
147
|
+
(0, chai_1.expect)(res.body.items[0].state).to.deep.include({
|
|
148
|
+
status: 'pending',
|
|
149
|
+
cleanBuilds: 2,
|
|
150
|
+
fixedBy: { id: selector_health_fixture_1.USER.priya, name: 'Priya' },
|
|
151
|
+
});
|
|
152
|
+
}));
|
|
153
|
+
it('lists selectors fixed in the period, with or without a heal in it', () => __awaiter(this, void 0, void 0, function* () {
|
|
154
|
+
(0, chai_1.expect)(names(yield list(selector_health_fixture_1.ADMIN, '?tab=fixed'))).to.deep.equal([selector_health_fixture_1.SEL.fixed]);
|
|
155
|
+
(0, chai_1.expect)(names(yield list(selector_health_fixture_1.ADMIN, '?tab=fixed&days=90'))).to.deep.equal([selector_health_fixture_1.SEL.fixed, selector_health_fixture_1.SEL.fixedOld]);
|
|
156
|
+
const week = yield list(selector_health_fixture_1.ADMIN, '?tab=fixed&days=7');
|
|
157
|
+
(0, chai_1.expect)(names(week)).to.deep.equal([selector_health_fixture_1.SEL.fixed]);
|
|
158
|
+
(0, chai_1.expect)(week.body.items[0]).to.deep.include({
|
|
159
|
+
heals: 0,
|
|
160
|
+
sessions: 0,
|
|
161
|
+
lastHealedAt: null,
|
|
162
|
+
suggestion: null,
|
|
163
|
+
});
|
|
164
|
+
}));
|
|
165
|
+
it('names nobody for an action by a user who is gone', () => __awaiter(this, void 0, void 0, function* () {
|
|
166
|
+
const fixed = (yield list(selector_health_fixture_1.ADMIN, '?tab=fixed')).body.items[0];
|
|
167
|
+
(0, chai_1.expect)(fixed.state.fixedBy).to.deep.equal({ id: selector_health_fixture_1.USER.gone, name: null });
|
|
168
|
+
}));
|
|
169
|
+
it('lists muted selectors with who muted them and why, only those a member can see', () => __awaiter(this, void 0, void 0, function* () {
|
|
170
|
+
const res = yield list(selector_health_fixture_1.MEMBER_A, '?tab=muted');
|
|
171
|
+
(0, chai_1.expect)(names(res)).to.deep.equal([selector_health_fixture_1.SEL.muted]);
|
|
172
|
+
(0, chai_1.expect)(res.body.items[0].state).to.deep.include({
|
|
173
|
+
status: 'muted',
|
|
174
|
+
mutedBy: { id: selector_health_fixture_1.USER.priya, name: 'Priya' },
|
|
175
|
+
muteReason: 'Screen being redesigned',
|
|
176
|
+
});
|
|
177
|
+
(0, chai_1.expect)(names(yield list(selector_health_fixture_1.ADMIN, '?tab=muted'))).to.deep.equal([selector_health_fixture_1.SEL.muted, selector_health_fixture_1.SEL.mutedB]);
|
|
178
|
+
}));
|
|
179
|
+
it('searches a status tab by selector', () => __awaiter(this, void 0, void 0, function* () {
|
|
180
|
+
(0, chai_1.expect)(names(yield list(selector_health_fixture_1.ADMIN, '?tab=muted&q=team-b'))).to.deep.equal([selector_health_fixture_1.SEL.mutedB]);
|
|
181
|
+
}));
|
|
182
|
+
it('tells a caller without the sessions scope that they cannot act', () => __awaiter(this, void 0, void 0, function* () {
|
|
183
|
+
(0, chai_1.expect)((yield list(selector_health_fixture_1.READ_ONLY_A)).body.canAct).to.equal(false);
|
|
184
|
+
}));
|
|
185
|
+
});
|
|
@@ -23,14 +23,18 @@ const SocketEvents_1 = require("../../src/enums/SocketEvents");
|
|
|
23
23
|
* the resolved value explicitly.
|
|
24
24
|
*/
|
|
25
25
|
function buildPrismaStub() {
|
|
26
|
-
|
|
26
|
+
const stub = {
|
|
27
27
|
selectorState: {
|
|
28
28
|
findUnique: sinon_1.default.stub(),
|
|
29
29
|
upsert: sinon_1.default.stub(),
|
|
30
30
|
update: sinon_1.default.stub(),
|
|
31
31
|
delete: sinon_1.default.stub(),
|
|
32
32
|
},
|
|
33
|
+
selectorEvent: { create: sinon_1.default.stub().resolves({}) },
|
|
33
34
|
};
|
|
35
|
+
// A transaction runs its callback against the same tables, as Prisma's does.
|
|
36
|
+
stub.$transaction = sinon_1.default.stub().callsFake((fn) => fn(stub));
|
|
37
|
+
return stub;
|
|
34
38
|
}
|
|
35
39
|
function buildSocketStub() {
|
|
36
40
|
return {
|
|
@@ -410,4 +414,73 @@ describe('SelectorStateService', () => {
|
|
|
410
414
|
(0, chai_1.expect)(result).to.equal(null);
|
|
411
415
|
}));
|
|
412
416
|
});
|
|
417
|
+
describe('what each change records', () => {
|
|
418
|
+
const ctx = { strategy: STRATEGY, selector: SELECTOR, apiKeyId: API_KEY_ID, userId: 'user-1' };
|
|
419
|
+
const recorded = () => prismaStub.selectorEvent.create.firstCall.args[0].data;
|
|
420
|
+
it('records who marked it fixed, in the same transaction as the status', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
421
|
+
prismaStub.selectorState.findUnique.resolves(null);
|
|
422
|
+
prismaStub.selectorState.upsert.resolves(makeRow({ status: 'pending', fixed_at: new Date() }));
|
|
423
|
+
yield svc.markFixed(ctx);
|
|
424
|
+
(0, chai_1.expect)(prismaStub.$transaction.calledOnce).to.equal(true);
|
|
425
|
+
(0, chai_1.expect)(recorded()).to.deep.equal({
|
|
426
|
+
original_strategy: STRATEGY,
|
|
427
|
+
original_selector: SELECTOR,
|
|
428
|
+
action: 'marked_fixed',
|
|
429
|
+
user_id: 'user-1',
|
|
430
|
+
reason: null,
|
|
431
|
+
});
|
|
432
|
+
(0, chai_1.expect)(prismaStub.selectorEvent.create.calledAfter(prismaStub.selectorState.upsert)).to.equal(true);
|
|
433
|
+
}));
|
|
434
|
+
it('fails the change, and tells nobody, when its record cannot be written', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
435
|
+
prismaStub.selectorState.findUnique.resolves(null);
|
|
436
|
+
prismaStub.selectorState.upsert.resolves(makeRow({ status: 'pending' }));
|
|
437
|
+
prismaStub.selectorEvent.create.rejects(new Error('disk full'));
|
|
438
|
+
let thrown;
|
|
439
|
+
try {
|
|
440
|
+
yield svc.markFixed(ctx);
|
|
441
|
+
}
|
|
442
|
+
catch (e) {
|
|
443
|
+
thrown = e;
|
|
444
|
+
}
|
|
445
|
+
(0, chai_1.expect)(thrown.message).to.equal('disk full');
|
|
446
|
+
(0, chai_1.expect)(socketStub.emitToDashboard.called).to.equal(false);
|
|
447
|
+
}));
|
|
448
|
+
it('records the mute with its reason', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
449
|
+
prismaStub.selectorState.upsert.resolves(makeRow({ status: 'muted', muted_at: new Date() }));
|
|
450
|
+
yield svc.mute(Object.assign(Object.assign({}, ctx), { reason: 'Screen being redesigned' }));
|
|
451
|
+
(0, chai_1.expect)(recorded()).to.include({
|
|
452
|
+
action: 'muted',
|
|
453
|
+
user_id: 'user-1',
|
|
454
|
+
reason: 'Screen being redesigned',
|
|
455
|
+
});
|
|
456
|
+
}));
|
|
457
|
+
it('records an unmute, and nothing for a selector that is not muted', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
458
|
+
prismaStub.selectorState.findUnique.resolves(makeRow({ status: 'muted', muted_at: new Date() }));
|
|
459
|
+
prismaStub.selectorState.delete.resolves(makeRow());
|
|
460
|
+
yield svc.unmute(ctx);
|
|
461
|
+
(0, chai_1.expect)(recorded()).to.include({ action: 'unmuted', user_id: 'user-1' });
|
|
462
|
+
prismaStub.selectorEvent.create.resetHistory();
|
|
463
|
+
prismaStub.selectorState.findUnique.resolves(makeRow({ status: 'active' }));
|
|
464
|
+
yield svc.unmute(ctx);
|
|
465
|
+
(0, chai_1.expect)(prismaStub.selectorEvent.create.called).to.equal(false);
|
|
466
|
+
}));
|
|
467
|
+
it('records a cancelled verification', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
468
|
+
prismaStub.selectorState.findUnique.resolves(makeRow({ status: 'pending', fixed_at: new Date() }));
|
|
469
|
+
prismaStub.selectorState.delete.resolves(makeRow());
|
|
470
|
+
yield svc.cancelVerification(ctx);
|
|
471
|
+
(0, chai_1.expect)(recorded()).to.include({ action: 'verification_cancelled', user_id: 'user-1' });
|
|
472
|
+
}));
|
|
473
|
+
it('records a selector breaking again, with nobody as its person', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
474
|
+
prismaStub.selectorState.findUnique.resolves(makeRow({ status: 'resolved', resolved_at: new Date() }));
|
|
475
|
+
prismaStub.selectorState.update.resolves(makeRow({ status: 'active', regression_count: 1 }));
|
|
476
|
+
yield svc.onHealRecorded({ strategy: STRATEGY, selector: SELECTOR, sessionId: SESSION_ID });
|
|
477
|
+
(0, chai_1.expect)(recorded()).to.include({ action: 'broke_again', user_id: null, reason: null });
|
|
478
|
+
}));
|
|
479
|
+
it('records nothing for a heal on a selector with no status', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
480
|
+
prismaStub.selectorState.findUnique.resolves(null);
|
|
481
|
+
yield svc.onHealRecorded({ strategy: STRATEGY, selector: SELECTOR, sessionId: SESSION_ID });
|
|
482
|
+
(0, chai_1.expect)(prismaStub.$transaction.called).to.equal(false);
|
|
483
|
+
(0, chai_1.expect)(prismaStub.selectorEvent.create.called).to.equal(false);
|
|
484
|
+
}));
|
|
485
|
+
});
|
|
413
486
|
});
|
|
@@ -40,8 +40,12 @@ describe('SelectorVerificationJob.run', () => {
|
|
|
40
40
|
findMany: sinon_1.default.stub(),
|
|
41
41
|
update: sinon_1.default.stub().resolves({}),
|
|
42
42
|
},
|
|
43
|
+
selectorEvent: { create: sinon_1.default.stub().resolves({}) },
|
|
43
44
|
$queryRaw: sinon_1.default.stub(),
|
|
44
45
|
};
|
|
46
|
+
prismaStub.$transaction = sinon_1.default
|
|
47
|
+
.stub()
|
|
48
|
+
.callsFake((fn) => fn(prismaStub));
|
|
45
49
|
socketStub = { emitToDashboard: sinon_1.default.stub() };
|
|
46
50
|
job = new SelectorVerificationJob_1.SelectorVerificationJob(prismaStub, socketStub);
|
|
47
51
|
});
|
|
@@ -203,4 +207,38 @@ describe('SelectorVerificationJob.run', () => {
|
|
|
203
207
|
yield job.run();
|
|
204
208
|
(0, chai_1.expect)(prismaStub.$queryRaw.called).to.be.false;
|
|
205
209
|
}));
|
|
210
|
+
it('records the verification, with nobody as its person', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
211
|
+
prismaStub.selectorState.findMany.resolves([
|
|
212
|
+
{
|
|
213
|
+
id: 'r-1',
|
|
214
|
+
original_strategy: 'xpath',
|
|
215
|
+
original_selector: '//x',
|
|
216
|
+
status: 'pending',
|
|
217
|
+
fixed_at: new Date('2026-04-20T00:00:00Z'),
|
|
218
|
+
clean_builds_count: 2,
|
|
219
|
+
},
|
|
220
|
+
]);
|
|
221
|
+
prismaStub.$queryRaw.resolves([
|
|
222
|
+
{ build_id: 'b-1', healed: 0 },
|
|
223
|
+
{ build_id: 'b-2', healed: 0 },
|
|
224
|
+
{ build_id: 'b-3', healed: 0 },
|
|
225
|
+
]);
|
|
226
|
+
prismaStub.selectorState.update.resolves({
|
|
227
|
+
id: 'r-1',
|
|
228
|
+
original_strategy: 'xpath',
|
|
229
|
+
original_selector: '//x',
|
|
230
|
+
status: 'resolved',
|
|
231
|
+
clean_builds_count: 3,
|
|
232
|
+
resolved_at: new Date(),
|
|
233
|
+
});
|
|
234
|
+
yield job.run();
|
|
235
|
+
(0, chai_1.expect)(prismaStub.selectorEvent.create.calledOnce).to.equal(true);
|
|
236
|
+
(0, chai_1.expect)(prismaStub.selectorEvent.create.firstCall.args[0].data).to.deep.equal({
|
|
237
|
+
original_strategy: 'xpath',
|
|
238
|
+
original_selector: '//x',
|
|
239
|
+
action: 'verified',
|
|
240
|
+
user_id: null,
|
|
241
|
+
reason: null,
|
|
242
|
+
});
|
|
243
|
+
}));
|
|
206
244
|
});
|