@xenon-device-management/xenon 2.13.0 → 2.13.2

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.
Files changed (112) hide show
  1. package/LICENSE +15 -0
  2. package/README.md +223 -488
  3. package/lib/package.json +1 -1
  4. package/lib/public/assets/{AnnotationOverlay-CzbFYtPI.js → AnnotationOverlay-BXruClDk.js} +1 -1
  5. package/lib/public/assets/{ApiKeyGate-CA5KFHHJ.js → ApiKeyGate-qTS4LPLC.js} +1 -1
  6. package/lib/public/assets/{BugReportButton-D-DSGg8C.js → BugReportButton-BoGhlI7F.js} +1 -1
  7. package/lib/public/assets/{DeviceMosaicView-paDEFU4B.js → DeviceMosaicView-8J0oYqMM.js} +1 -1
  8. package/lib/public/assets/{EmptyState-BE_5f8Vw.js → EmptyState-Cr17PdDv.js} +1 -1
  9. package/lib/public/assets/{FieldGroup-DqMf7Rf3.js → FieldGroup-Bvn08svv.js} +1 -1
  10. package/lib/public/assets/{FilterMenu-BQY75Su3.js → FilterMenu-DDSO8aDf.js} +1 -1
  11. package/lib/public/assets/{Menu-BiW3os9u.js → Menu-B-Qn2X3T.js} +1 -1
  12. package/lib/public/assets/{Modal-DfawTxih.js → Modal-Cg9V5JYU.js} +1 -1
  13. package/lib/public/assets/{RecordingPage-BvemyRiy.js → RecordingPage-BTUs9VTW.js} +1 -1
  14. package/lib/public/assets/{RecordingsPage-CXfw7wpd.js → RecordingsPage-DYcfxITK.js} +2 -2
  15. package/lib/public/assets/{SegmentedControl-B7m9kpom.js → SegmentedControl-Zy7suhT5.js} +1 -1
  16. package/lib/public/assets/{SettingCard-Cw6lDD0l.js → SettingCard-CWYmycoe.js} +1 -1
  17. package/lib/public/assets/{Table-DlEuFgv-.js → Table-io9fwa98.js} +1 -1
  18. package/lib/public/assets/{activity-CnLvNwae.js → activity-Bdf_uIWl.js} +1 -1
  19. package/lib/public/assets/ai-settings-Bce5SJob.js +21 -0
  20. package/lib/public/assets/{api-keys-DphSVzBr.js → api-keys-CPqtVpm3.js} +1 -1
  21. package/lib/public/assets/{apps-vUfu15lX.js → apps-BgZan0H9.js} +2 -2
  22. package/lib/public/assets/{arrow-left-CLQWAiCY.js → arrow-left-B27ckB31.js} +1 -1
  23. package/lib/public/assets/{arrow-right-K6A5e_Ee.js → arrow-right-DOF8ITCz.js} +1 -1
  24. package/lib/public/assets/{arrow-up-right-DUA_lq6f.js → arrow-up-right-eFG5KTFS.js} +1 -1
  25. package/lib/public/assets/{auth-shell-BYRHcZd6.js → auth-shell-DlC3_27j.js} +2 -2
  26. package/lib/public/assets/{builds-page-C0fTvIG8.js → builds-page-BuXpPfKo.js} +1 -1
  27. package/lib/public/assets/{button-BxnHCCsX.js → button-fIW8owMI.js} +1 -1
  28. package/lib/public/assets/{calendar-CqVsn28A.js → calendar-WNjmrF-a.js} +1 -1
  29. package/lib/public/assets/{check-R6V3bot0.js → check-BdTh1Sq7.js} +1 -1
  30. package/lib/public/assets/{chevron-right-BA2Fntu-.js → chevron-right-B4BKlsqk.js} +1 -1
  31. package/lib/public/assets/{circle-check-D4EzyGzs.js → circle-check-zJU80_EF.js} +1 -1
  32. package/lib/public/assets/{circle-x-Cf6AF2B2.js → circle-x-5Uwquo0h.js} +1 -1
  33. package/lib/public/assets/{clock-EZfet0sS.js → clock-BtUprDsZ.js} +1 -1
  34. package/lib/public/assets/{copy-Bc6P8v9V.js → copy-DGSbZI3u.js} +1 -1
  35. package/lib/public/assets/device-explorer-C-KnhMHI.js +279 -0
  36. package/lib/public/assets/{download-Bd8OGLzi.js → download-WG6l7BgD.js} +1 -1
  37. package/lib/public/assets/{forgot-password-CoE7Ugtx.js → forgot-password-DnudqFFo.js} +1 -1
  38. package/lib/public/assets/{index-BTYMAqNW.css → index-Bxoxt9sV.css} +1 -1
  39. package/lib/public/assets/{index-CHOr4JCs.js → index-DI1fGIyj.js} +43 -43
  40. package/lib/public/assets/{index-BiL0Enl_.js → index-HDQVe1gi.js} +1 -1
  41. package/lib/public/assets/{input-CJ8z3IO5.js → input-BjDRKCea.js} +1 -1
  42. package/lib/public/assets/{line-chart-CPK0ObjV.js → line-chart-C0zdgd0v.js} +1 -1
  43. package/lib/public/assets/{list-checks-mXGHUVbB.js → list-checks-BdW5CjM8.js} +1 -1
  44. package/lib/public/assets/{lock-BSXM1xvq.js → lock-Dy3L4I8E.js} +1 -1
  45. package/lib/public/assets/{login-Ct2iW066.js → login-BHbY9CLG.js} +1 -1
  46. package/lib/public/assets/maintenance-settings-zDVlOMVK.js +6 -0
  47. package/lib/public/assets/{monitor-D8WQK8md.js → monitor-8xjkeHHi.js} +1 -1
  48. package/lib/public/assets/{mouse-pointer-2-Cw2tWzDX.js → mouse-pointer-2-CXdKMXQ8.js} +1 -1
  49. package/lib/public/assets/{network-Ba_uFxWo.js → network-Cj8JOKON.js} +1 -1
  50. package/lib/public/assets/{overview-CxrZ7_6k.js → overview-CTg7F0im.js} +1 -1
  51. package/lib/public/assets/{page-header-Cm1XL49f.js → page-header--aTyJXq6.js} +1 -1
  52. package/lib/public/assets/{play-C0CuKLXj.js → play-B-FodgHV.js} +1 -1
  53. package/lib/public/assets/{plus-CIIvR_6Z.js → plus-CsIk10Cq.js} +1 -1
  54. package/lib/public/assets/{profile-page-BZ_e-Bgw.js → profile-page-DkFKkMfl.js} +1 -1
  55. package/lib/public/assets/{recording-group-store-BWeeNs2_.js → recording-group-store-WVM3hoXn.js} +1 -1
  56. package/lib/public/assets/{reset-password-lfNz8Rm1.js → reset-password-I2-zqLlI.js} +1 -1
  57. package/lib/public/assets/{runbook-page-Cnec3VSx.js → runbook-page-DJTVCJX7.js} +1 -1
  58. package/lib/public/assets/{select-CZuCnold.js → select-DvCSv25z.js} +1 -1
  59. package/lib/public/assets/{selector-detail-redirect-BG3pxFLw.js → selector-detail-redirect-BLVfMJs_.js} +1 -1
  60. package/lib/public/assets/{selector-health-page-DsyL58tb.js → selector-health-page-oqadRokn.js} +2 -2
  61. package/lib/public/assets/session-detail-page-CGn55Pvl.js +4 -0
  62. package/lib/public/assets/settings-MsVU2ukE.js +6 -0
  63. package/lib/public/assets/{stat-tile-DFhv8JoI.js → stat-tile-DV9Ix-4O.js} +1 -1
  64. package/lib/public/assets/{tablet-68J5f6zh.js → tablet-6b_SEN2X.js} +1 -1
  65. package/lib/public/assets/{teams-eHSyEpbD.js → teams-BjZyfuHh.js} +1 -1
  66. package/lib/public/assets/{trash-2-CcSsv4fS.js → trash-2-CTZpPSdd.js} +1 -1
  67. package/lib/public/assets/{upload-BYf26K71.js → upload-P3XzwQUb.js} +1 -1
  68. package/lib/public/assets/{use-builds-data-CHW-MhaP.js → use-builds-data-1cOCMf-z.js} +1 -1
  69. package/lib/public/assets/{use-password-reset-mode-DozShXkk.js → use-password-reset-mode-DgF9w088.js} +1 -1
  70. package/lib/public/assets/{users-CcSQRoK9.js → users-CiNTKUiz.js} +1 -1
  71. package/lib/public/assets/{users-BpYnpOl2.js → users-DMF9Mk5H.js} +1 -1
  72. package/lib/public/assets/{video-off-dFKgps73.js → video-off-CHp1KU_T.js} +1 -1
  73. package/lib/public/assets/webhook-settings-DiS8geIm.js +1 -0
  74. package/lib/public/assets/{zap-COC2tZaH.js → zap-DRz0tDq5.js} +1 -1
  75. package/lib/public/index.html +2 -2
  76. package/lib/schema.json +0 -25
  77. package/lib/src/app/routers/control.js +19 -4
  78. package/lib/src/app/routers/streamStartConflict.js +3 -0
  79. package/lib/src/data-service/device-service.js +40 -0
  80. package/lib/src/data-service/device-store.js +4 -1
  81. package/lib/src/data-service/prisma-store.js +2 -1
  82. package/lib/src/device-managers/ios/WDAClient.js +128 -16
  83. package/lib/src/device-utils.js +63 -13
  84. package/lib/src/middleware/deviceAccessGuard.js +63 -9
  85. package/lib/src/services/ServerManager.js +3 -0
  86. package/lib/src/services/SessionLifecycleService.js +16 -4
  87. package/lib/src/services/device-access/SessionOwnerResolver.js +26 -0
  88. package/lib/src/services/device-access/deviceAccessPolicy.js +38 -6
  89. package/lib/src/services/device-access/leaseHold.js +26 -0
  90. package/lib/src/services/device-access/ticketActorAccess.js +6 -0
  91. package/lib/src/services/lease/activeLeases.js +37 -0
  92. package/lib/src/services/recording/busy-precheck.js +24 -3
  93. package/lib/test/unit/device-access-lease.spec.js +270 -0
  94. package/lib/test/unit/lease/allocate-lease-branch.spec.js +22 -0
  95. package/lib/test/unit/lease/session-lease-proof.spec.js +59 -0
  96. package/lib/test/unit/lease-held-phone.spec.js +196 -0
  97. package/lib/test/unit/partial-config-file.spec.js +100 -0
  98. package/lib/test/unit/schema-required-args.spec.js +21 -16
  99. package/lib/test/unit/wda-client-session.spec.js +128 -0
  100. package/lib/test/unit/wda-clipboard-write.spec.js +135 -0
  101. package/lib/tsconfig.tsbuildinfo +1 -1
  102. package/package.json +1 -1
  103. package/schema.json +0 -25
  104. package/scripts/dev/readme-screenshots.js +231 -0
  105. package/scripts/generate-types-from-schema.js +6 -0
  106. package/scripts/lib/always-present-plugin-args.js +39 -0
  107. package/lib/public/assets/ai-settings-D7Te8o-a.js +0 -21
  108. package/lib/public/assets/device-explorer-BFXpRq6d.js +0 -279
  109. package/lib/public/assets/maintenance-settings-DaOs00P6.js +0 -6
  110. package/lib/public/assets/session-detail-page-ZZyGM413.js +0 -4
  111. package/lib/public/assets/settings-DGZ7B_AR.js +0 -6
  112. package/lib/public/assets/webhook-settings-DD1XjiOD.js +0 -1
@@ -0,0 +1,196 @@
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 = __importDefault(require("chai"));
50
+ const sinon_1 = __importDefault(require("sinon"));
51
+ const sinon_chai_1 = __importDefault(require("sinon-chai"));
52
+ const ActiveLeases = __importStar(require("../../src/services/lease/activeLeases"));
53
+ const DeviceUtils = __importStar(require("../../src/device-utils"));
54
+ const DeviceService = __importStar(require("../../src/data-service/device-service"));
55
+ const db_1 = require("../../src/data-service/db");
56
+ const device_service_1 = require("../../src/data-service/device-service");
57
+ const IPluginArgs_1 = require("../../src/interfaces/IPluginArgs");
58
+ const test_container_1 = require("../helpers/test-container");
59
+ const loki_stores_1 = require("../helpers/loki-stores");
60
+ const scratch_database_1 = require("../helpers/scratch-database");
61
+ chai_1.default.use(sinon_chai_1.default);
62
+ const expect = chai_1.default.expect;
63
+ // A phone an SDK lease holds stays its lease's until the lease ends. Its only
64
+ // lock is `busy`, and three things used to undo that: the idle sweeper (which
65
+ // read the lease's creation as the phone's last command), a lease-bound
66
+ // session's release, and allocation, which offered only the first candidate
67
+ // to the lock and so stalled on a leased phone while others were free.
68
+ describe('a phone an SDK lease holds', () => {
69
+ (0, loki_stores_1.useLokiStores)();
70
+ const scratch = (0, scratch_database_1.useScratchDatabase)();
71
+ const sandbox = sinon_1.default.createSandbox();
72
+ const pluginArgs = Object.assign({}, IPluginArgs_1.DefaultPluginArgs, {
73
+ iosDeviceType: 'both',
74
+ androidDeviceType: 'both',
75
+ });
76
+ const phone = (udid, over = {}) => (Object.assign({ udid, name: udid, platform: 'android', sdk: '13', host: 'http://127.0.0.1:4723', busy: false, offline: false, userBlocked: false, state: 'device', deviceType: 'emulator', realDevice: false, systemPort: 57000, totalUtilizationTimeMilliSec: 0, sessionStartTime: 0 }, over));
77
+ const lease = (udid, over = {}) => scratch.db.lease.create({
78
+ data: Object.assign({ tokenHash: 'h', deviceUdid: udid, deviceHost: 'http://127.0.0.1:4723', actorId: 'key_owner', status: 'active', expiresAt: Date.now() + 600000, lastHeartbeatAt: Date.now(), allocatedPorts: '{}', capabilityBag: '{}' }, over),
79
+ });
80
+ const row = (udid) => __awaiter(void 0, void 0, void 0, function* () { return (yield db_1.XenonDatabase.DeviceModel).findOne({ udid }); });
81
+ const setRow = (udid, fields) => __awaiter(void 0, void 0, void 0, function* () {
82
+ return (yield db_1.XenonDatabase.DeviceModel)
83
+ .chain()
84
+ .find({ udid })
85
+ .update((d) => Object.assign(d, fields));
86
+ });
87
+ before(() => __awaiter(void 0, void 0, void 0, function* () {
88
+ yield (0, test_container_1.resetTestContainer)();
89
+ yield (0, test_container_1.setupTestContainer)();
90
+ }));
91
+ after(() => (0, test_container_1.resetTestContainer)());
92
+ beforeEach(() => __awaiter(void 0, void 0, void 0, function* () {
93
+ (yield db_1.XenonDatabase.DeviceModel).removeDataOnly();
94
+ yield scratch.db.lease.deleteMany({});
95
+ (0, test_container_1.createTestXenonManager)(Object.assign({}, pluginArgs, { maxSessions: 10, platform: 'android' }));
96
+ sandbox.stub(DeviceUtils, 'setUtilizationTime').resolves();
97
+ }));
98
+ afterEach(() => sandbox.restore());
99
+ describe('the idle sweeper', () => {
100
+ const idle = { busy: true, lastCmdExecutedAt: Date.now() - 600000 };
101
+ it('leaves a leased phone with no session alone', () => __awaiter(void 0, void 0, void 0, function* () {
102
+ yield (0, device_service_1.addNewDevice)([phone('leased-1')]);
103
+ yield setRow('leased-1', idle);
104
+ yield lease('leased-1');
105
+ const unblock = sandbox.spy(DeviceService, 'unblockDevice');
106
+ yield DeviceUtils.releaseBlockedDevices(20);
107
+ expect(unblock).not.to.have.been.called;
108
+ expect((yield row('leased-1')).busy).to.equal(true);
109
+ }));
110
+ it('frees it once the lease has lapsed', () => __awaiter(void 0, void 0, void 0, function* () {
111
+ yield (0, device_service_1.addNewDevice)([phone('lapsed-1')]);
112
+ yield setRow('lapsed-1', idle);
113
+ yield lease('lapsed-1', { expiresAt: Date.now() - 1000 });
114
+ const unblock = sandbox.spy(DeviceService, 'unblockDevice');
115
+ yield DeviceUtils.releaseBlockedDevices(20);
116
+ expect(unblock).to.have.been.calledWith('lapsed-1');
117
+ }));
118
+ it('skips its tick when it cannot tell which phones are leased', () => __awaiter(void 0, void 0, void 0, function* () {
119
+ // Failing open would free every leased phone: their last command is
120
+ // when the lease was taken.
121
+ yield (0, device_service_1.addNewDevice)([phone('plain-3')]);
122
+ yield setRow('plain-3', idle);
123
+ sandbox.stub(ActiveLeases, 'activeLeasesByDevice').rejects(new Error('db down'));
124
+ const unblock = sandbox.spy(DeviceService, 'unblockDevice');
125
+ yield DeviceUtils.releaseBlockedDevices(20);
126
+ expect(unblock).not.to.have.been.called;
127
+ }));
128
+ it('still frees an idle phone nobody leases', () => __awaiter(void 0, void 0, void 0, function* () {
129
+ yield (0, device_service_1.addNewDevice)([phone('plain-1')]);
130
+ yield setRow('plain-1', idle);
131
+ const unblock = sandbox.spy(DeviceService, 'unblockDevice');
132
+ yield DeviceUtils.releaseBlockedDevices(20);
133
+ expect(unblock).to.have.been.calledWith('plain-1');
134
+ }));
135
+ });
136
+ describe("a lease-bound session's release", () => {
137
+ it('hands the phone back to the lease, still busy', () => __awaiter(void 0, void 0, void 0, function* () {
138
+ var _a;
139
+ yield (0, device_service_1.addNewDevice)([phone('leased-2')]);
140
+ yield setRow('leased-2', { busy: true, session_id: 'sess-1' });
141
+ yield lease('leased-2');
142
+ expect(yield DeviceService.releaseSessionDevice('leased-2', 'http://127.0.0.1:4723', 'sess-1')).to.be.true;
143
+ const after = yield row('leased-2');
144
+ expect((_a = after.session_id) !== null && _a !== void 0 ? _a : null).to.equal(null);
145
+ expect(after.busy).to.equal(true);
146
+ }));
147
+ it('undoes the hand-back when the lease ended meanwhile', () => __awaiter(void 0, void 0, void 0, function* () {
148
+ yield (0, device_service_1.addNewDevice)([phone('racing-1')]);
149
+ yield setRow('racing-1', { busy: true, session_id: 'sess-3' });
150
+ // Live at the first check, gone by the second.
151
+ const lookup = sandbox.stub(ActiveLeases, 'activeLeaseOn');
152
+ lookup.onFirstCall().resolves({ id: 'l', actorId: 'k' });
153
+ lookup.resolves(null);
154
+ yield DeviceService.releaseSessionDevice('racing-1', 'http://127.0.0.1:4723', 'sess-3');
155
+ expect(lookup).to.have.been.calledTwice;
156
+ expect((yield row('racing-1')).busy).to.equal(false);
157
+ }));
158
+ it('frees a phone with no lease, as before', () => __awaiter(void 0, void 0, void 0, function* () {
159
+ yield (0, device_service_1.addNewDevice)([phone('plain-2')]);
160
+ yield setRow('plain-2', { busy: true, session_id: 'sess-2' });
161
+ yield DeviceService.releaseSessionDevice('plain-2', 'http://127.0.0.1:4723', 'sess-2');
162
+ expect((yield row('plain-2')).busy).to.equal(false);
163
+ }));
164
+ });
165
+ describe('allocation for a session', () => {
166
+ const caps = (udid) => ({
167
+ alwaysMatch: Object.assign({ platformName: 'android' }, (udid ? { 'appium:udid': udid } : {})),
168
+ firstMatch: [{}],
169
+ });
170
+ it("never locks a reserved phone's twin on another host in its place", () => __awaiter(void 0, void 0, void 0, function* () {
171
+ // Same udid on two hosts: the reserved row is listed first.
172
+ yield (0, device_service_1.addNewDevice)([
173
+ phone('twin', {
174
+ host: 'http://other:4723',
175
+ reservedUntil: Date.now() + 600000,
176
+ reservedBy: 'x',
177
+ }),
178
+ phone('twin'),
179
+ ]);
180
+ const got = yield DeviceUtils.allocateDeviceForSession(caps(), 2000, 100, pluginArgs);
181
+ expect(got.host).to.equal('http://127.0.0.1:4723');
182
+ const reserved = (yield db_1.XenonDatabase.DeviceModel).findOne({
183
+ udid: 'twin',
184
+ host: 'http://other:4723',
185
+ });
186
+ expect(reserved.busy).to.equal(false);
187
+ }));
188
+ it('skips a leased phone whose busy flag was cleared and takes a free one', () => __awaiter(void 0, void 0, void 0, function* () {
189
+ // Listed first, so the old single-candidate lock tried only it, forever.
190
+ yield (0, device_service_1.addNewDevice)([phone('a-leased'), phone('b-free')]);
191
+ yield lease('a-leased');
192
+ const got = yield DeviceUtils.allocateDeviceForSession(caps(), 2000, 100, pluginArgs);
193
+ expect(got.udid).to.equal('b-free');
194
+ }));
195
+ });
196
+ });
@@ -0,0 +1,100 @@
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
+ const chai_1 = require("chai");
16
+ const fs_1 = __importDefault(require("fs"));
17
+ const os_1 = __importDefault(require("os"));
18
+ const path_1 = __importDefault(require("path"));
19
+ const lodash_1 = __importDefault(require("lodash"));
20
+ // An Appium config file is checked against Xenon's schema.json before Appium
21
+ // fills in defaults (appium/build/lib/main.js: readConfigFile, then
22
+ // defaultsDeep(cli, file, schema defaults)). A `required` option therefore
23
+ // makes Appium refuse any file that leaves it out, even though it has a
24
+ // default. Through 2.13.1 the README's own five-option sample was refused with
25
+ // "REQUIRED must have required property 'enableJsonLogging'". These checks run
26
+ // Appium's own loader so a file that sets a few options keeps starting.
27
+ /* eslint-disable @typescript-eslint/no-var-requires -- Appium's internals have no type exports */
28
+ const appiumSchema = require('appium/build/lib/schema/schema');
29
+ const { readConfigFile } = require('appium/build/lib/config-file');
30
+ /* eslint-enable @typescript-eslint/no-var-requires */
31
+ const xenonSchema = JSON.parse(fs_1.default.readFileSync(path_1.default.join(__dirname, '..', '..', 'schema.json'), 'utf8'));
32
+ // The README's sample, verbatim.
33
+ const README_SAMPLE = `server:
34
+ use-plugins: [xenon]
35
+ plugin:
36
+ xenon:
37
+ platform: both
38
+ enableDashboard: true
39
+ maxSessions: 8
40
+ enableSelfHealing: true
41
+ buildCleanupDays: 30
42
+ `;
43
+ describe('An Appium config file for Xenon', function () {
44
+ let dir;
45
+ function load(yaml) {
46
+ return __awaiter(this, void 0, void 0, function* () {
47
+ const file = path_1.default.join(dir, `config-${Math.random().toString(36).slice(2)}.yaml`);
48
+ fs_1.default.writeFileSync(file, yaml);
49
+ return readConfigFile(file);
50
+ });
51
+ }
52
+ before(function () {
53
+ appiumSchema.resetSchema();
54
+ appiumSchema.registerSchema('plugin', 'xenon', xenonSchema);
55
+ appiumSchema.finalizeSchema();
56
+ dir = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'xenon-config-'));
57
+ });
58
+ after(function () {
59
+ appiumSchema.resetSchema();
60
+ fs_1.default.rmSync(dir, { recursive: true, force: true });
61
+ });
62
+ it('is accepted when it sets only a few options (the README sample)', function () {
63
+ return __awaiter(this, void 0, void 0, function* () {
64
+ var _a;
65
+ const result = yield load(README_SAMPLE);
66
+ (0, chai_1.expect)((_a = result.reason) !== null && _a !== void 0 ? _a : result.errors).to.be.empty;
67
+ });
68
+ });
69
+ it('is accepted when it sets no Xenon option at all', function () {
70
+ return __awaiter(this, void 0, void 0, function* () {
71
+ var _a;
72
+ const result = yield load('server:\n use-plugins: [xenon]\n');
73
+ (0, chai_1.expect)((_a = result.reason) !== null && _a !== void 0 ? _a : result.errors).to.be.empty;
74
+ });
75
+ });
76
+ it('is still refused when a value is invalid', function () {
77
+ return __awaiter(this, void 0, void 0, function* () {
78
+ const result = yield load(README_SAMPLE.replace('platform: both', 'platform: windows'));
79
+ (0, chai_1.expect)(result.errors).to.not.be.empty;
80
+ });
81
+ });
82
+ it('leaves Appium to fill every option the file leaves out with its default', function () {
83
+ return __awaiter(this, void 0, void 0, function* () {
84
+ var _a;
85
+ // What Appium's main does after a file passes: CLI args, then the file,
86
+ // then the schema's defaults.
87
+ const result = yield load(README_SAMPLE);
88
+ const defaults = appiumSchema.getDefaultsForSchema(false);
89
+ const server = lodash_1.default.defaultsDeep({}, (_a = result.config) === null || _a === void 0 ? void 0 : _a.server, defaults);
90
+ const xenon = server.plugin.xenon;
91
+ const setInFile = new Set(Object.keys(result.config.server.plugin.xenon));
92
+ (0, chai_1.expect)(xenon.maxSessions).to.equal(8); // from the file
93
+ for (const [key, prop] of Object.entries(xenonSchema.properties)) {
94
+ if ('default' in prop && !setInFile.has(key)) {
95
+ (0, chai_1.expect)(xenon[key], `${key} should take its default`).to.deep.equal(prop.default);
96
+ }
97
+ }
98
+ });
99
+ });
100
+ });
@@ -2,10 +2,13 @@
2
2
  var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
+ var _a;
5
6
  Object.defineProperty(exports, "__esModule", { value: true });
6
7
  const chai_1 = require("chai");
7
8
  const fs_1 = __importDefault(require("fs"));
8
9
  const path_1 = __importDefault(require("path"));
10
+ // eslint-disable-next-line @typescript-eslint/no-var-requires -- a plain Node script's list
11
+ const { ALWAYS_PRESENT_PLUGIN_ARGS } = require('../../scripts/lib/always-present-plugin-args');
9
12
  // Adding a plugin arg to schema.json's `required` list is a BREAKING change for
10
13
  // every existing config file. Appium validates the config against this schema
11
14
  // and refuses to start when a required key is absent:
@@ -18,28 +21,30 @@ const path_1 = __importDefault(require("path"));
18
21
  // in 1.12.0 — an already-running server would not come back up after upgrading,
19
22
  // with no way to tell from the message that the fix is to edit a YAML file.
20
23
  //
21
- // An arg with a default must be optional: the schema default and the code's own
22
- // destructuring fallback already supply the value.
24
+ // An arg with a default must be optional: Appium fills in schema defaults after
25
+ // it validates the file, and the plugin merges DefaultPluginArgs as well. Until
26
+ // 2.13.1, 23 args were both required and defaulted, so a config file had to list
27
+ // all of them; even the README's five-option sample was refused. None is now.
23
28
  const schema = JSON.parse(fs_1.default.readFileSync(path_1.default.join(__dirname, '..', '..', 'schema.json'), 'utf8'));
24
- /** Args already both required and defaulted when this guard was added. */
25
- const KNOWN_REQUIRED_WITH_DEFAULT = 23;
29
+ const required = (_a = schema.required) !== null && _a !== void 0 ? _a : [];
26
30
  describe('schema.json required args', () => {
27
- it('does not grow the set of args that are required despite having a default', () => {
31
+ it('requires no arg that has a default', () => {
28
32
  // A default means "you may omit this", which is the opposite of required.
29
- // 23 args already sit in both lists; they only work because the Mac app
30
- // writes every required key into the config it generates, so a hand-written
31
- // or older config is already fragile against them. That is pre-existing
32
- // debt, not something to fix in a hotfix — but the set must not GROW,
33
- // because each addition breaks every config written before it.
34
- const contradictory = schema.required.filter((key) => schema.properties[key] && 'default' in schema.properties[key]);
35
- (0, chai_1.expect)(contradictory.length, `New required-with-default arg(s): ${contradictory.slice(KNOWN_REQUIRED_WITH_DEFAULT).join(', ')}. ` +
36
- 'An existing config that omits them fails Appium config validation and the server ' +
37
- 'will not start. Give the arg a default and leave it out of `required`.').to.equal(KNOWN_REQUIRED_WITH_DEFAULT);
33
+ const contradictory = required.filter((key) => schema.properties[key] && 'default' in schema.properties[key]);
34
+ (0, chai_1.expect)(contradictory, 'An existing config that omits these fails Appium config validation and the server ' +
35
+ 'will not start. Give the arg a default and leave it out of `required`.').to.deep.equal([]);
38
36
  });
39
37
  it('keeps every required arg present in properties', () => {
40
- const missing = schema.required.filter((key) => !schema.properties[key]);
38
+ const missing = required.filter((key) => !schema.properties[key]);
41
39
  (0, chai_1.expect)(missing, 'required lists an arg that does not exist').to.deep.equal([]);
42
40
  });
41
+ it('types as always present only args that have a default', () => {
42
+ // IPluginArgs keeps these fields non-optional (scripts/generate-types-from-schema.js
43
+ // marks them required for type generation only). That is true only while each
44
+ // exists and has a default for Appium and DefaultPluginArgs to fill in.
45
+ const wrong = ALWAYS_PRESENT_PLUGIN_ARGS.filter((key) => !schema.properties[key] || !('default' in schema.properties[key]));
46
+ (0, chai_1.expect)(wrong, 'listed as always present but missing or without a default').to.deep.equal([]);
47
+ });
43
48
  it('does not require the recording retention args', () => {
44
49
  // Named explicitly: this is the regression that took a live server down.
45
50
  for (const key of [
@@ -48,7 +53,7 @@ describe('schema.json required args', () => {
48
53
  'recordingFailedCleanupDays',
49
54
  ]) {
50
55
  (0, chai_1.expect)(schema.properties[key], `${key} should exist`).to.be.an('object');
51
- (0, chai_1.expect)(schema.required, `${key} must stay optional`).to.not.include(key);
56
+ (0, chai_1.expect)(required, `${key} must stay optional`).to.not.include(key);
52
57
  }
53
58
  });
54
59
  });
@@ -0,0 +1,128 @@
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 axios_1 = __importDefault(require("axios"));
18
+ const sinon_1 = __importDefault(require("sinon"));
19
+ const typedi_1 = require("typedi");
20
+ const IOSStreamService_1 = __importDefault(require("../../src/device-managers/ios/IOSStreamService"));
21
+ const WDAClient_1 = require("../../src/device-managers/ios/WDAClient");
22
+ const device_store_1 = require("../../src/data-service/device-store");
23
+ /**
24
+ * Each live preview creates its own WebDriverAgent session, and WDAClient kept
25
+ * the session it had seen first. After the preview restarted (device control
26
+ * closed and opened again), every hardware button went to the old session:
27
+ * WDA answered 404 "invalid session id", which pressButton and homescreen
28
+ * treat as "not supported" rather than a dead session, so Home and the volume
29
+ * buttons answered 500 until a lock or a swipe happened to renew it. A key
30
+ * the iPhone does have was also reported as one it doesn't.
31
+ */
32
+ describe('WDAClient follows the live preview’s WebDriverAgent session', () => {
33
+ const PHONE = '00008110-00084CE80E51401E';
34
+ const PORT = 28100;
35
+ let urls;
36
+ // The session WebDriverAgent has now, and the one the preview recorded.
37
+ let live;
38
+ let previews;
39
+ // Whether WDA names its live session when it refuses a dead one, as WDA does.
40
+ let namesLiveSession;
41
+ const deadSession = () => Object.assign(new Error('Request failed with status code 404'), {
42
+ response: {
43
+ status: 404,
44
+ data: {
45
+ value: { error: 'invalid session id', message: 'Session does not exist' },
46
+ sessionId: namesLiveSession ? live : null,
47
+ },
48
+ },
49
+ });
50
+ const unknownCommand = () => Object.assign(new Error('Request failed with status code 404'), {
51
+ response: {
52
+ status: 404,
53
+ data: { value: { error: 'unknown command' }, sessionId: live },
54
+ },
55
+ });
56
+ beforeEach(() => {
57
+ urls = [];
58
+ namesLiveSession = true;
59
+ sinon_1.default.stub(device_store_1.DeviceStoreFactory, 'getStore').returns({
60
+ findDevice: () => __awaiter(void 0, void 0, void 0, function* () { return ({ udid: PHONE, realDevice: true }); }),
61
+ });
62
+ const real = typedi_1.Container.get.bind(typedi_1.Container);
63
+ sinon_1.default.stub(typedi_1.Container, 'get').callsFake((token) => {
64
+ if (token === IOSStreamService_1.default) {
65
+ return {
66
+ getStreamStatus: () => ({ status: 'running', wdaPort: PORT }),
67
+ getWDASessionId: () => previews,
68
+ setWDASessionId: (_udid, sid) => {
69
+ previews = sid || undefined;
70
+ },
71
+ };
72
+ }
73
+ return real(token);
74
+ });
75
+ sinon_1.default.stub(axios_1.default, 'post').callsFake(((url) => __awaiter(void 0, void 0, void 0, function* () {
76
+ var _a;
77
+ urls.push(url);
78
+ const session = (_a = /\/session\/([^/]+)\//.exec(url)) === null || _a === void 0 ? void 0 : _a[1];
79
+ if (session && session !== live)
80
+ throw deadSession();
81
+ // As WDA does: a button press needs a session, and the home screen
82
+ // is reached only without one.
83
+ if (!session && url.endsWith('/wda/pressButton'))
84
+ throw unknownCommand();
85
+ if (session && url.endsWith('/wda/homescreen'))
86
+ throw unknownCommand();
87
+ return { status: 200, data: { value: null, sessionId: live } };
88
+ })));
89
+ });
90
+ afterEach(() => sinon_1.default.restore());
91
+ it('presses a button in the restarted preview’s session', () => __awaiter(void 0, void 0, void 0, function* () {
92
+ const wda = new WDAClient_1.WDAClient();
93
+ live = previews = 'FIRST';
94
+ yield wda.pressKey(PHONE, 'volumeup');
95
+ // Device control closed and opened again: a new preview, a new session.
96
+ live = previews = 'SECOND';
97
+ urls = [];
98
+ yield wda.pressKey(PHONE, 'volumeup');
99
+ (0, chai_1.expect)(urls).to.deep.equal([`http://127.0.0.1:${PORT}/session/SECOND/wda/pressButton`]);
100
+ }));
101
+ it('moves to the session WebDriverAgent names when the one it knew is gone', () => __awaiter(void 0, void 0, void 0, function* () {
102
+ const wda = new WDAClient_1.WDAClient();
103
+ live = previews = 'FIRST';
104
+ yield wda.pressKey(PHONE, 'home');
105
+ // WDA started a new session that nothing here recorded.
106
+ live = 'SECOND';
107
+ urls = [];
108
+ yield wda.pressKey(PHONE, 'home');
109
+ (0, chai_1.expect)(urls[urls.length - 1]).to.equal(`http://127.0.0.1:${PORT}/session/SECOND/wda/pressButton`);
110
+ (0, chai_1.expect)(previews).to.equal('SECOND');
111
+ }));
112
+ it('does not call a dead session a key the iPhone lacks', () => __awaiter(void 0, void 0, void 0, function* () {
113
+ live = 'SECOND';
114
+ previews = 'FIRST';
115
+ namesLiveSession = false;
116
+ const err = yield new WDAClient_1.WDAClient().pressKey(PHONE, 'camera').then(() => null, (e) => e);
117
+ (0, chai_1.expect)(err).to.be.an('error');
118
+ (0, chai_1.expect)(err).not.to.be.instanceOf(WDAClient_1.UnsupportedKeyError);
119
+ }));
120
+ // Every caller sent it inside the session and got 404 "unknown command":
121
+ // the Home key's fallback, the clipboard read, and putting the home screen
122
+ // back after a clipboard write, which left WDA's black screen in front.
123
+ it('goes to the home screen without a session, the only way WDA takes it', () => __awaiter(void 0, void 0, void 0, function* () {
124
+ live = previews = 'FIRST';
125
+ yield new WDAClient_1.WDAClient().sendWDACommand(PHONE, 'post', '/wda/homescreen', {});
126
+ (0, chai_1.expect)(urls).to.deep.equal([`http://127.0.0.1:${PORT}/wda/homescreen`]);
127
+ }));
128
+ });
@@ -0,0 +1,135 @@
1
+ "use strict";
2
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
+ return new (P || (P = Promise))(function (resolve, reject) {
5
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
9
+ });
10
+ };
11
+ var __importDefault = (this && this.__importDefault) || function (mod) {
12
+ return (mod && mod.__esModule) ? mod : { "default": mod };
13
+ };
14
+ Object.defineProperty(exports, "__esModule", { value: true });
15
+ require("reflect-metadata");
16
+ const chai_1 = require("chai");
17
+ const sinon_1 = __importDefault(require("sinon"));
18
+ const typedi_1 = require("typedi");
19
+ const IOSStreamService_1 = __importDefault(require("../../src/device-managers/ios/IOSStreamService"));
20
+ const WDAClient_1 = require("../../src/device-managers/ios/WDAClient");
21
+ const device_store_1 = require("../../src/data-service/device-store");
22
+ /**
23
+ * iOS ignores a pasteboard write from an app in the background, and
24
+ * WebDriverAgent still answers success. Device control's "Write to device"
25
+ * on an iPhone reported success and left the clipboard as it was (checked on
26
+ * an iPhone 14 Plus, iOS 26.5). The write now brings WDA's own app forward,
27
+ * reads the text back, and puts back the app that was in front.
28
+ *
29
+ * Bringing WDA's app forward waits for it to go idle, which it never does, so
30
+ * the call took about 21 s. WDA's idle wait is turned off for that step and
31
+ * put back after.
32
+ */
33
+ describe('WDAClient writes an iPhone’s clipboard', () => {
34
+ const PHONE = '00008110-00084CE80E51401E';
35
+ const WDA = 'com.qasecret.WebDriverAgentRunner.xctrunner';
36
+ const b64 = (s) => Buffer.from(s).toString('base64');
37
+ // The phone as iOS behaves: only the app in front can write the pasteboard.
38
+ let phone;
39
+ let calls;
40
+ let idleWaitAtActivation;
41
+ let unreadableSettings;
42
+ beforeEach(() => {
43
+ idleWaitAtActivation = [];
44
+ unreadableSettings = false;
45
+ phone = {
46
+ front: 'com.apple.springboard',
47
+ pasteboard: '',
48
+ idleWait: 10,
49
+ takesWrites: true,
50
+ realDevice: true,
51
+ };
52
+ calls = [];
53
+ sinon_1.default.stub(device_store_1.DeviceStoreFactory, 'getStore').returns({
54
+ findDevice: () => __awaiter(void 0, void 0, void 0, function* () { return ({ udid: PHONE, realDevice: phone.realDevice }); }),
55
+ });
56
+ const real = typedi_1.Container.get.bind(typedi_1.Container);
57
+ sinon_1.default.stub(typedi_1.Container, 'get').callsFake((token) => {
58
+ if (token === IOSStreamService_1.default)
59
+ return { detectWDABundleId: () => __awaiter(void 0, void 0, void 0, function* () { return WDA; }) };
60
+ return real(token);
61
+ });
62
+ sinon_1.default.stub(WDAClient_1.WDAClient.prototype, 'sendWDACommand').callsFake(((_udid, method, endpoint, data) => __awaiter(void 0, void 0, void 0, function* () {
63
+ calls.push(`${method} ${endpoint}`);
64
+ const ok = (value = null) => ({ status: 200, data: { value } });
65
+ switch (`${method} ${endpoint}`) {
66
+ case 'get /wda/activeAppInfo':
67
+ return ok({ bundleId: phone.front });
68
+ case 'get /appium/settings':
69
+ if (unreadableSettings)
70
+ throw new Error('WDA request failed: /appium/settings');
71
+ return ok({ waitForIdleTimeout: phone.idleWait });
72
+ case 'post /appium/settings':
73
+ phone.idleWait = data.settings.waitForIdleTimeout;
74
+ return ok();
75
+ case 'post /wda/apps/activate':
76
+ if (data.bundleId === WDA)
77
+ idleWaitAtActivation.push(phone.idleWait);
78
+ phone.front = data.bundleId;
79
+ return ok();
80
+ case 'post /wda/homescreen':
81
+ phone.front = 'com.apple.springboard';
82
+ return ok();
83
+ case 'post /wda/setPasteboard':
84
+ if (phone.front === WDA && phone.takesWrites) {
85
+ phone.pasteboard = Buffer.from(data.content, 'base64').toString();
86
+ }
87
+ return ok(); // success either way, as WDA answers
88
+ case 'post /wda/getPasteboard':
89
+ return ok(phone.front === WDA ? b64(phone.pasteboard) : '');
90
+ default:
91
+ throw new Error(`unexpected ${method} ${endpoint}`);
92
+ }
93
+ })));
94
+ });
95
+ afterEach(() => sinon_1.default.restore());
96
+ it('writes the clipboard while WebDriverAgent is in the background', () => __awaiter(void 0, void 0, void 0, function* () {
97
+ yield new WDAClient_1.WDAClient().setClipboard(PHONE, 'hello from Xenon');
98
+ (0, chai_1.expect)(phone.pasteboard).to.equal('hello from Xenon');
99
+ }));
100
+ it('puts the home screen back when that was in front', () => __awaiter(void 0, void 0, void 0, function* () {
101
+ yield new WDAClient_1.WDAClient().setClipboard(PHONE, 'x');
102
+ (0, chai_1.expect)(phone.front).to.equal('com.apple.springboard');
103
+ }));
104
+ it('puts back the app that was in front', () => __awaiter(void 0, void 0, void 0, function* () {
105
+ phone.front = 'com.apple.Preferences';
106
+ yield new WDAClient_1.WDAClient().setClipboard(PHONE, 'x');
107
+ (0, chai_1.expect)(phone.front).to.equal('com.apple.Preferences');
108
+ }));
109
+ it('brings WebDriverAgent forward without its idle wait, and restores the wait', () => __awaiter(void 0, void 0, void 0, function* () {
110
+ phone.idleWait = 7;
111
+ yield new WDAClient_1.WDAClient().setClipboard(PHONE, 'x');
112
+ (0, chai_1.expect)(idleWaitAtActivation).to.deep.equal([0]);
113
+ (0, chai_1.expect)(phone.idleWait).to.equal(7);
114
+ }));
115
+ it('fails when the iPhone did not take the text', () => __awaiter(void 0, void 0, void 0, function* () {
116
+ phone.takesWrites = false;
117
+ const err = yield new WDAClient_1.WDAClient().setClipboard(PHONE, 'lost').then(() => null, (e) => e);
118
+ (0, chai_1.expect)(err === null || err === void 0 ? void 0 : err.message).to.equal('The iPhone didn’t take the clipboard text.');
119
+ (0, chai_1.expect)(phone.front).to.equal('com.apple.springboard');
120
+ (0, chai_1.expect)(phone.idleWait).to.equal(10);
121
+ }));
122
+ it('writes a simulator’s clipboard directly', () => __awaiter(void 0, void 0, void 0, function* () {
123
+ phone.realDevice = false;
124
+ phone.front = WDA; // a simulator's pasteboard takes the write from anywhere
125
+ yield new WDAClient_1.WDAClient().setClipboard(PHONE, 'sim');
126
+ (0, chai_1.expect)(calls).to.deep.equal(['post /wda/setPasteboard']);
127
+ }));
128
+ it('leaves the idle wait alone when it can’t read it, since it couldn’t put it back', () => __awaiter(void 0, void 0, void 0, function* () {
129
+ unreadableSettings = true;
130
+ yield new WDAClient_1.WDAClient().setClipboard(PHONE, 'slow but safe');
131
+ (0, chai_1.expect)(idleWaitAtActivation).to.deep.equal([10]);
132
+ (0, chai_1.expect)(calls).not.to.include('post /appium/settings');
133
+ (0, chai_1.expect)(phone.pasteboard).to.equal('slow but safe');
134
+ }));
135
+ });