@leera.io/qa-runner 1.0.4 → 1.0.6
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.
|
@@ -245,6 +245,10 @@ export class Adb {
|
|
|
245
245
|
async pidof(serial, pkg) {
|
|
246
246
|
return parsePidof(await this.shell(serial, ['pidof', assertPackage(pkg)], 10_000).catch(() => ''));
|
|
247
247
|
}
|
|
248
|
+
/** Drops adb's connection to a device so it reconnects (the fix for one stuck `offline`). */
|
|
249
|
+
async reconnect(serial) {
|
|
250
|
+
await this.run(['-s', assertSerial(serial), 'reconnect'], 15_000).catch(() => undefined);
|
|
251
|
+
}
|
|
248
252
|
async killEmulator(serial) {
|
|
249
253
|
await this.run(['-s', assertSerial(serial), 'emu', 'kill'], 15_000).catch(() => undefined);
|
|
250
254
|
}
|
|
@@ -140,6 +140,28 @@ export async function waitForBoot(adb, serial, timeoutMs, failed = () => null) {
|
|
|
140
140
|
await sleep(2_000);
|
|
141
141
|
}
|
|
142
142
|
}
|
|
143
|
+
/**
|
|
144
|
+
* Waits for a device adb lost for a moment (a busy emulator, a USB reconnect) to come back and
|
|
145
|
+
* finish booting. A device still `offline` after a few seconds gets its connection reset once.
|
|
146
|
+
*/
|
|
147
|
+
export async function waitForDeviceBack(adb, serial, { timeoutMs = 120_000, nudgeAfterMs = 10_000, intervalMs = 2_000 } = {}) {
|
|
148
|
+
const deadline = Date.now() + timeoutMs;
|
|
149
|
+
const nudgeAt = Date.now() + nudgeAfterMs;
|
|
150
|
+
let nudged = false;
|
|
151
|
+
for (;;) {
|
|
152
|
+
const state = (await adb.devices().catch(() => [])).find((d) => d.serial === serial)?.state;
|
|
153
|
+
if (state === 'device' && (await adb.getprop(serial, 'sys.boot_completed').catch(() => '')) === '1')
|
|
154
|
+
return;
|
|
155
|
+
if (state === 'offline' && !nudged && Date.now() >= nudgeAt) {
|
|
156
|
+
nudged = true;
|
|
157
|
+
await adb.reconnect(serial);
|
|
158
|
+
}
|
|
159
|
+
if (Date.now() > deadline) {
|
|
160
|
+
throw new Error(`${serial} is ${state ?? 'not connected'} and did not come back within ${Math.round(timeoutMs / 1000)} s`);
|
|
161
|
+
}
|
|
162
|
+
await sleep(intervalMs);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
143
165
|
export async function shutdownEmulator(adb, serial, timeoutMs = 60_000) {
|
|
144
166
|
bootedHere.delete(serial);
|
|
145
167
|
await adb.killEmulator(serial);
|
|
@@ -7,6 +7,7 @@ import { bundledDrivers } from '../appium/drivers.js';
|
|
|
7
7
|
import { AppNotAvailable, resolveAppFile } from '../builds/install.js';
|
|
8
8
|
import { androidInstallable } from '../builds/unpack.js';
|
|
9
9
|
import { Adb, resolveAndroidSdk } from '../devices/android/adb.js';
|
|
10
|
+
import { waitForDeviceBack } from '../devices/android/emulator.js';
|
|
10
11
|
import { leaseAndroidDevice, listAndroidDevices } from '../devices/android/index.js';
|
|
11
12
|
import { LogcatSink } from '../devices/android/logcat.js';
|
|
12
13
|
import { dismissSystemErrorDialog } from '../devices/android/system-dialog.js';
|
|
@@ -33,6 +34,33 @@ export function appiumLogLines(text, redact) {
|
|
|
33
34
|
});
|
|
34
35
|
return { lines: kept, dropped: all.length - kept.length };
|
|
35
36
|
}
|
|
37
|
+
/** adb's words for a device it lost for the moment: `offline`, or missing from its list. */
|
|
38
|
+
export function lostDevice(error) {
|
|
39
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
40
|
+
return /\bdevice offline\b|\bdevice ('[^']*' )?not found\b/i.test(message);
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Opens the Appium session, and once more when adb lost the device while Appium set it up. A busy
|
|
44
|
+
* emulator or a USB reconnect drops the device for a few seconds, and Appium (which retries
|
|
45
|
+
* "not found" but not "offline") then fails the whole session. `recover` waits for the device;
|
|
46
|
+
* when it does not come back, the first error stands.
|
|
47
|
+
*/
|
|
48
|
+
export async function startSession(open, recover) {
|
|
49
|
+
try {
|
|
50
|
+
return await open();
|
|
51
|
+
}
|
|
52
|
+
catch (error) {
|
|
53
|
+
if (!lostDevice(error))
|
|
54
|
+
throw error;
|
|
55
|
+
try {
|
|
56
|
+
await recover();
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
throw error;
|
|
60
|
+
}
|
|
61
|
+
return open();
|
|
62
|
+
}
|
|
63
|
+
}
|
|
36
64
|
/** `android.sdk_path` from config.json, when the runner config has one. */
|
|
37
65
|
export function configuredSdkPath(paths) {
|
|
38
66
|
try {
|
|
@@ -191,14 +219,19 @@ export class AndroidAppiumDriver {
|
|
|
191
219
|
const url = await this.appium.ensure();
|
|
192
220
|
this.systemPort = await SYSTEM_PORTS.take();
|
|
193
221
|
this.mjpegPort = await MJPEG_PORTS.take();
|
|
194
|
-
|
|
222
|
+
const capabilities = uiautomator2Capabilities({
|
|
195
223
|
serial,
|
|
196
224
|
appPackage: app.id,
|
|
197
225
|
appActivity: app.launch_activity,
|
|
198
226
|
systemPort: this.systemPort,
|
|
199
227
|
mjpegServerPort: this.mjpegPort,
|
|
200
228
|
newCommandTimeoutSecs: Math.max(300, Math.ceil((this.ctx.session?.idle_timeout_seconds ?? 0) + 60)),
|
|
201
|
-
})
|
|
229
|
+
});
|
|
230
|
+
const adb = this.adb;
|
|
231
|
+
this.client = await startSession(() => openSession(url, capabilities), async () => {
|
|
232
|
+
this.ctx.log.warn('adb lost the device while Appium started the session; waiting for it to retry', { serial });
|
|
233
|
+
await waitForDeviceBack(adb, serial);
|
|
234
|
+
});
|
|
202
235
|
}
|
|
203
236
|
catch (error) {
|
|
204
237
|
const log = this.appium.logSince(logOffset, 2_000).trim().split('\n').slice(-5).join(' | ');
|
package/package.json
CHANGED