@mobilewright/inspector 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +25 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +46 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/device-manager.d.ts +71 -0
- package/dist/lib/device-manager.d.ts.map +1 -0
- package/dist/lib/device-manager.js +142 -0
- package/dist/lib/device-manager.js.map +1 -0
- package/dist/lib/locator-derivation.d.ts +30 -0
- package/dist/lib/locator-derivation.d.ts.map +1 -0
- package/dist/lib/locator-derivation.js +69 -0
- package/dist/lib/locator-derivation.js.map +1 -0
- package/dist/lib/logger.d.ts +10 -0
- package/dist/lib/logger.d.ts.map +1 -0
- package/dist/lib/logger.js +14 -0
- package/dist/lib/logger.js.map +1 -0
- package/dist/routes/devices.d.ts +7 -0
- package/dist/routes/devices.d.ts.map +1 -0
- package/dist/routes/devices.js +43 -0
- package/dist/routes/devices.js.map +1 -0
- package/dist/routes/inspect.d.ts +7 -0
- package/dist/routes/inspect.d.ts.map +1 -0
- package/dist/routes/inspect.js +79 -0
- package/dist/routes/inspect.js.map +1 -0
- package/package.json +42 -0
- package/public/css/app.css +453 -0
- package/public/index.html +67 -0
- package/public/js/app.js +572 -0
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { type MobilewrightLauncher } from './lib/device-manager.js';
|
|
2
|
+
export type { MobilewrightLauncher };
|
|
3
|
+
/** Options passed to {@link start}. */
|
|
4
|
+
export interface InspectorOptions {
|
|
5
|
+
/** iOS launcher from mobilewright. */
|
|
6
|
+
ios: MobilewrightLauncher;
|
|
7
|
+
/** Android launcher from mobilewright. */
|
|
8
|
+
android: MobilewrightLauncher;
|
|
9
|
+
/** HTTP port to listen on. Defaults to 4621. */
|
|
10
|
+
port?: number;
|
|
11
|
+
}
|
|
12
|
+
/** Handle returned by {@link start} to retrieve the server URL and shut it down. */
|
|
13
|
+
export interface InspectorServer {
|
|
14
|
+
/** The URL the inspector is listening on, e.g. `http://localhost:4621`. */
|
|
15
|
+
url: string;
|
|
16
|
+
/** Gracefully close the device connection and stop the HTTP server. */
|
|
17
|
+
close: () => Promise<void>;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Start the Mobilewright Inspector HTTP server.
|
|
21
|
+
* Pass the ios and android launcher objects from mobilewright so the inspector
|
|
22
|
+
* can list and connect to devices without a circular dependency.
|
|
23
|
+
*/
|
|
24
|
+
export declare function start({ ios, android, port }: InspectorOptions): Promise<InspectorServer>;
|
|
25
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAKA,OAAO,EAAiB,KAAK,oBAAoB,EAAE,MAAM,yBAAyB,CAAC;AAInF,YAAY,EAAE,oBAAoB,EAAE,CAAC;AAErC,uCAAuC;AACvC,MAAM,WAAW,gBAAgB;IAC/B,sCAAsC;IACtC,GAAG,EAAE,oBAAoB,CAAC;IAC1B,0CAA0C;IAC1C,OAAO,EAAE,oBAAoB,CAAC;IAC9B,gDAAgD;IAChD,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED,oFAAoF;AACpF,MAAM,WAAW,eAAe;IAC9B,2EAA2E;IAC3E,GAAG,EAAE,MAAM,CAAC;IACZ,uEAAuE;IACvE,KAAK,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CAC5B;AAED;;;;GAIG;AACH,wBAAsB,KAAK,CAAC,EAAE,GAAG,EAAE,OAAO,EAAE,IAAW,EAAE,EAAE,gBAAgB,GAAG,OAAO,CAAC,eAAe,CAAC,CAwCrG"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import express from 'express';
|
|
2
|
+
import { fileURLToPath } from 'node:url';
|
|
3
|
+
import { dirname, join } from 'node:path';
|
|
4
|
+
import http from 'node:http';
|
|
5
|
+
import { DeviceManager } from './lib/device-manager.js';
|
|
6
|
+
import { createDevicesRouter } from './routes/devices.js';
|
|
7
|
+
import { createInspectRouter } from './routes/inspect.js';
|
|
8
|
+
/**
|
|
9
|
+
* Start the Mobilewright Inspector HTTP server.
|
|
10
|
+
* Pass the ios and android launcher objects from mobilewright so the inspector
|
|
11
|
+
* can list and connect to devices without a circular dependency.
|
|
12
|
+
*/
|
|
13
|
+
export async function start({ ios, android, port = 4621 }) {
|
|
14
|
+
const publicDir = join(dirname(fileURLToPath(import.meta.url)), '..', 'public');
|
|
15
|
+
const app = express();
|
|
16
|
+
app.use(express.json());
|
|
17
|
+
app.use(express.static(publicDir));
|
|
18
|
+
app.get('/health', (_req, res) => res.json({ ok: true }));
|
|
19
|
+
const deviceManager = new DeviceManager({ ios, android });
|
|
20
|
+
app.use('/api/devices', createDevicesRouter(deviceManager));
|
|
21
|
+
app.use('/api', createInspectRouter(deviceManager));
|
|
22
|
+
// 4-argument signature is required by Express to treat this as an error handler
|
|
23
|
+
app.use((err, _req, res, _next) => {
|
|
24
|
+
res.status(500).json({ error: err.message });
|
|
25
|
+
});
|
|
26
|
+
const server = http.createServer(app);
|
|
27
|
+
await new Promise((resolve, reject) => {
|
|
28
|
+
server.once('error', reject);
|
|
29
|
+
server.listen(port, '127.0.0.1', () => resolve());
|
|
30
|
+
});
|
|
31
|
+
const assignedPort = server.address().port;
|
|
32
|
+
const url = `http://127.0.0.1:${assignedPort}`;
|
|
33
|
+
/** Gracefully drain the device connection and shut down the HTTP server. */
|
|
34
|
+
async function close() {
|
|
35
|
+
await new Promise(resolve => {
|
|
36
|
+
server.close(() => resolve());
|
|
37
|
+
server.closeAllConnections();
|
|
38
|
+
});
|
|
39
|
+
await Promise.race([
|
|
40
|
+
deviceManager.close().catch(() => { }),
|
|
41
|
+
new Promise(r => setTimeout(r, 3000)),
|
|
42
|
+
]);
|
|
43
|
+
}
|
|
44
|
+
return { url, close };
|
|
45
|
+
}
|
|
46
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import type { Device } from '@mobilewright/core';
|
|
2
|
+
import type { DeviceInfo } from '@mobilewright/protocol';
|
|
3
|
+
/** Platform launcher injected from mobilewright to avoid a circular dependency. */
|
|
4
|
+
export interface MobilewrightLauncher {
|
|
5
|
+
/** List all connected/booted devices for this platform. */
|
|
6
|
+
devices(): Promise<DeviceInfo[]>;
|
|
7
|
+
/** Launch and connect to a specific device by id. */
|
|
8
|
+
launch(opts: {
|
|
9
|
+
deviceId: string;
|
|
10
|
+
autoStart?: boolean;
|
|
11
|
+
autoAppLaunch?: boolean;
|
|
12
|
+
}): Promise<Device>;
|
|
13
|
+
}
|
|
14
|
+
/** Discriminated union of error codes thrown by DeviceManager. */
|
|
15
|
+
export type DeviceErrorCode = 'blocked' | 'in_progress' | 'not_found' | 'connect_failed';
|
|
16
|
+
/** Structured error thrown by DeviceManager for expected failure modes. */
|
|
17
|
+
export declare class DeviceError extends Error {
|
|
18
|
+
/** Machine-readable error code indicating the failure reason. */
|
|
19
|
+
readonly code: DeviceErrorCode;
|
|
20
|
+
/** @param message Human-readable description. @param code Machine-readable failure reason. */
|
|
21
|
+
constructor(message: string, code: DeviceErrorCode);
|
|
22
|
+
}
|
|
23
|
+
/** DeviceInfo tagged with its platform, returned by listDevices(). */
|
|
24
|
+
export type TaggedDeviceInfo = DeviceInfo & {
|
|
25
|
+
platform: 'ios' | 'android';
|
|
26
|
+
};
|
|
27
|
+
/** Minimal device identity record held by DeviceManager while a device is active. */
|
|
28
|
+
export type DeviceInfoRecord = {
|
|
29
|
+
id: string;
|
|
30
|
+
platform: 'ios' | 'android';
|
|
31
|
+
};
|
|
32
|
+
/**
|
|
33
|
+
* Manages a single active device connection shared across the inspect lifecycle.
|
|
34
|
+
* Coordinates concurrent select() and inspect operations via in-flight flags.
|
|
35
|
+
*/
|
|
36
|
+
export declare class DeviceManager {
|
|
37
|
+
#private;
|
|
38
|
+
/** @param ios iOS launcher from mobilewright. @param android Android launcher from mobilewright. */
|
|
39
|
+
constructor({ ios, android }: {
|
|
40
|
+
ios: MobilewrightLauncher;
|
|
41
|
+
android: MobilewrightLauncher;
|
|
42
|
+
});
|
|
43
|
+
/**
|
|
44
|
+
* List all connected/booted devices across both platforms.
|
|
45
|
+
* Each platform is queried independently so a failure on one does not hide the other.
|
|
46
|
+
*/
|
|
47
|
+
listDevices(): Promise<TaggedDeviceInfo[]>;
|
|
48
|
+
/**
|
|
49
|
+
* Connect to a device, closing any previous connection first.
|
|
50
|
+
* Throws DeviceError if an inspect is in flight, a select is already in progress,
|
|
51
|
+
* or the previous device cannot be cleanly disconnected.
|
|
52
|
+
*/
|
|
53
|
+
select(deviceId: string, platform: 'ios' | 'android'): Promise<Device>;
|
|
54
|
+
/**
|
|
55
|
+
* Mark the start of an inspect operation.
|
|
56
|
+
* Returns false if an inspect is already in flight or a device switch is in progress.
|
|
57
|
+
*/
|
|
58
|
+
beginInspect(): boolean;
|
|
59
|
+
/** Clear the inspect-in-flight flag set by beginInspect(). */
|
|
60
|
+
endInspect(): void;
|
|
61
|
+
/**
|
|
62
|
+
* Close the active device connection. Safe to call with no active device.
|
|
63
|
+
* Throws if the underlying driver close fails; state is only cleared on success.
|
|
64
|
+
*/
|
|
65
|
+
close(): Promise<void>;
|
|
66
|
+
/** The currently connected device, or null if none selected. */
|
|
67
|
+
get device(): Device | null;
|
|
68
|
+
/** Id and platform of the currently connected device, or null if none selected. */
|
|
69
|
+
get deviceInfo(): DeviceInfoRecord | null;
|
|
70
|
+
}
|
|
71
|
+
//# sourceMappingURL=device-manager.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"device-manager.d.ts","sourceRoot":"","sources":["../../src/lib/device-manager.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,oBAAoB,CAAC;AACjD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,wBAAwB,CAAC;AAGzD,mFAAmF;AACnF,MAAM,WAAW,oBAAoB;IACnC,2DAA2D;IAC3D,OAAO,IAAI,OAAO,CAAC,UAAU,EAAE,CAAC,CAAC;IACjC,qDAAqD;IACrD,MAAM,CAAC,IAAI,EAAE;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,SAAS,CAAC,EAAE,OAAO,CAAC;QAAC,aAAa,CAAC,EAAE,OAAO,CAAA;KAAE,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;CACnG;AAED,kEAAkE;AAClE,MAAM,MAAM,eAAe,GAAG,SAAS,GAAG,aAAa,GAAG,WAAW,GAAG,gBAAgB,CAAC;AAEzF,2EAA2E;AAC3E,qBAAa,WAAY,SAAQ,KAAK;IACpC,iEAAiE;IACjE,QAAQ,CAAC,IAAI,EAAE,eAAe,CAAC;IAE/B,8FAA8F;gBAClF,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,eAAe;CAKnD;AAED,sEAAsE;AACtE,MAAM,MAAM,gBAAgB,GAAG,UAAU,GAAG;IAAE,QAAQ,EAAE,KAAK,GAAG,SAAS,CAAA;CAAE,CAAC;AAE5E,qFAAqF;AACrF,MAAM,MAAM,gBAAgB,GAAG;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,KAAK,GAAG,SAAS,CAAA;CAAE,CAAC;AAE3E;;;GAGG;AACH,qBAAa,aAAa;;IAgBxB,oGAAoG;gBACxF,EAAE,GAAG,EAAE,OAAO,EAAE,EAAE;QAAE,GAAG,EAAE,oBAAoB,CAAC;QAAC,OAAO,EAAE,oBAAoB,CAAA;KAAE;IAK1F;;;OAGG;IACG,WAAW,IAAI,OAAO,CAAC,gBAAgB,EAAE,CAAC;IAahD;;;;OAIG;IACG,MAAM,CAAC,QAAQ,EAAE,MAAM,EAAE,QAAQ,EAAE,KAAK,GAAG,SAAS,GAAG,OAAO,CAAC,MAAM,CAAC;IAsC5E;;;OAGG;IACH,YAAY,IAAI,OAAO;IAMvB,8DAA8D;IAC9D,UAAU,IAAI,IAAI;IAIlB;;;OAGG;IACG,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAe5B,gEAAgE;IAChE,IAAI,MAAM,IAAI,MAAM,GAAG,IAAI,CAA+B;IAE1D,mFAAmF;IACnF,IAAI,UAAU,IAAI,gBAAgB,GAAG,IAAI,CAAmC;CAC7E"}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { logger } from './logger.js';
|
|
2
|
+
/** Structured error thrown by DeviceManager for expected failure modes. */
|
|
3
|
+
export class DeviceError extends Error {
|
|
4
|
+
/** @param message Human-readable description. @param code Machine-readable failure reason. */
|
|
5
|
+
constructor(message, code) {
|
|
6
|
+
super(message);
|
|
7
|
+
this.name = 'DeviceError';
|
|
8
|
+
this.code = code;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Manages a single active device connection shared across the inspect lifecycle.
|
|
13
|
+
* Coordinates concurrent select() and inspect operations via in-flight flags.
|
|
14
|
+
*/
|
|
15
|
+
export class DeviceManager {
|
|
16
|
+
/** iOS launcher instance. */
|
|
17
|
+
#ios;
|
|
18
|
+
/** Android launcher instance. */
|
|
19
|
+
#android;
|
|
20
|
+
/** Currently connected device driver, or null when no device is selected. */
|
|
21
|
+
#activeDevice = null;
|
|
22
|
+
/** Identity of the currently connected device, or null when no device is selected. */
|
|
23
|
+
#activeDeviceInfo = null;
|
|
24
|
+
/** True while an inspect operation is in progress; blocks concurrent select(). */
|
|
25
|
+
#inspectInFlight = false;
|
|
26
|
+
/** True while a select() call is awaiting launcher.launch(); blocks concurrent select(). */
|
|
27
|
+
#selecting = false;
|
|
28
|
+
/** True after close() is called; prevents new connections after shutdown. */
|
|
29
|
+
#closed = false;
|
|
30
|
+
/** @param ios iOS launcher from mobilewright. @param android Android launcher from mobilewright. */
|
|
31
|
+
constructor({ ios, android }) {
|
|
32
|
+
this.#ios = ios;
|
|
33
|
+
this.#android = android;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* List all connected/booted devices across both platforms.
|
|
37
|
+
* Each platform is queried independently so a failure on one does not hide the other.
|
|
38
|
+
*/
|
|
39
|
+
async listDevices() {
|
|
40
|
+
const [iosResult, androidResult] = await Promise.allSettled([
|
|
41
|
+
this.#ios.devices(),
|
|
42
|
+
this.#android.devices(),
|
|
43
|
+
]);
|
|
44
|
+
if (iosResult.status === 'rejected')
|
|
45
|
+
logger.warn(`iOS device list failed: ${iosResult.reason?.message}`);
|
|
46
|
+
if (androidResult.status === 'rejected')
|
|
47
|
+
logger.warn(`Android device list failed: ${androidResult.reason?.message}`);
|
|
48
|
+
return [
|
|
49
|
+
...(iosResult.status === 'fulfilled' ? iosResult.value.map(d => ({ ...d, platform: 'ios' })) : []),
|
|
50
|
+
...(androidResult.status === 'fulfilled' ? androidResult.value.map(d => ({ ...d, platform: 'android' })) : []),
|
|
51
|
+
];
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Connect to a device, closing any previous connection first.
|
|
55
|
+
* Throws DeviceError if an inspect is in flight, a select is already in progress,
|
|
56
|
+
* or the previous device cannot be cleanly disconnected.
|
|
57
|
+
*/
|
|
58
|
+
async select(deviceId, platform) {
|
|
59
|
+
if (this.#closed)
|
|
60
|
+
throw new DeviceError('DeviceManager is closed', 'connect_failed');
|
|
61
|
+
if (this.#inspectInFlight)
|
|
62
|
+
throw new DeviceError('Device switch blocked: inspect in progress', 'blocked');
|
|
63
|
+
if (this.#selecting)
|
|
64
|
+
throw new DeviceError('Device switch already in progress', 'in_progress');
|
|
65
|
+
this.#selecting = true;
|
|
66
|
+
try {
|
|
67
|
+
if (this.#activeDevice) {
|
|
68
|
+
logger.info(`Closing previous device ${this.#activeDeviceInfo?.id}`);
|
|
69
|
+
try {
|
|
70
|
+
await this.#activeDevice.close();
|
|
71
|
+
this.#activeDevice = null;
|
|
72
|
+
this.#activeDeviceInfo = null;
|
|
73
|
+
}
|
|
74
|
+
catch (err) {
|
|
75
|
+
logger.error(`Failed to close device ${this.#activeDeviceInfo?.id}: ${err.message}`);
|
|
76
|
+
throw new DeviceError(err.message, 'connect_failed');
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
logger.info(`Connecting to ${platform} device ${deviceId}`);
|
|
80
|
+
const launcher = platform === 'ios' ? this.#ios : this.#android;
|
|
81
|
+
const launched = await launcher.launch({ deviceId, autoStart: true, autoAppLaunch: false });
|
|
82
|
+
if (this.#closed) {
|
|
83
|
+
try {
|
|
84
|
+
await launched.close();
|
|
85
|
+
}
|
|
86
|
+
catch { }
|
|
87
|
+
throw new DeviceError('DeviceManager closed during connect', 'connect_failed');
|
|
88
|
+
}
|
|
89
|
+
this.#activeDevice = launched;
|
|
90
|
+
this.#activeDeviceInfo = { id: deviceId, platform };
|
|
91
|
+
logger.info(`Connected to ${deviceId}`);
|
|
92
|
+
return this.#activeDevice;
|
|
93
|
+
}
|
|
94
|
+
catch (err) {
|
|
95
|
+
if (err instanceof DeviceError)
|
|
96
|
+
throw err;
|
|
97
|
+
logger.error(`Failed to connect to ${deviceId}: ${err.message}`);
|
|
98
|
+
throw new DeviceError(err.message, 'connect_failed');
|
|
99
|
+
}
|
|
100
|
+
finally {
|
|
101
|
+
this.#selecting = false;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Mark the start of an inspect operation.
|
|
106
|
+
* Returns false if an inspect is already in flight or a device switch is in progress.
|
|
107
|
+
*/
|
|
108
|
+
beginInspect() {
|
|
109
|
+
if (this.#inspectInFlight || this.#selecting)
|
|
110
|
+
return false;
|
|
111
|
+
this.#inspectInFlight = true;
|
|
112
|
+
return true;
|
|
113
|
+
}
|
|
114
|
+
/** Clear the inspect-in-flight flag set by beginInspect(). */
|
|
115
|
+
endInspect() {
|
|
116
|
+
this.#inspectInFlight = false;
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Close the active device connection. Safe to call with no active device.
|
|
120
|
+
* Throws if the underlying driver close fails; state is only cleared on success.
|
|
121
|
+
*/
|
|
122
|
+
async close() {
|
|
123
|
+
this.#closed = true;
|
|
124
|
+
if (this.#activeDevice) {
|
|
125
|
+
logger.info(`Closing device ${this.#activeDeviceInfo?.id}`);
|
|
126
|
+
try {
|
|
127
|
+
await this.#activeDevice.close();
|
|
128
|
+
this.#activeDevice = null;
|
|
129
|
+
this.#activeDeviceInfo = null;
|
|
130
|
+
}
|
|
131
|
+
catch (err) {
|
|
132
|
+
logger.error(`Failed to close device ${this.#activeDeviceInfo?.id}: ${err.message}`);
|
|
133
|
+
throw new DeviceError(err.message, 'connect_failed');
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
/** The currently connected device, or null if none selected. */
|
|
138
|
+
get device() { return this.#activeDevice; }
|
|
139
|
+
/** Id and platform of the currently connected device, or null if none selected. */
|
|
140
|
+
get deviceInfo() { return this.#activeDeviceInfo; }
|
|
141
|
+
}
|
|
142
|
+
//# sourceMappingURL=device-manager.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"device-manager.js","sourceRoot":"","sources":["../../src/lib/device-manager.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { ViewNode } from '@mobilewright/protocol';
|
|
2
|
+
/** Discriminated union of the four locator strategies mobilewright supports. */
|
|
3
|
+
export type Locator = {
|
|
4
|
+
kind: 'testId';
|
|
5
|
+
value: string;
|
|
6
|
+
} | {
|
|
7
|
+
kind: 'role';
|
|
8
|
+
value: string;
|
|
9
|
+
name: string | undefined;
|
|
10
|
+
} | {
|
|
11
|
+
kind: 'label';
|
|
12
|
+
value: string;
|
|
13
|
+
} | {
|
|
14
|
+
kind: 'text';
|
|
15
|
+
value: string;
|
|
16
|
+
};
|
|
17
|
+
/**
|
|
18
|
+
* Derive the best mobilewright locator for a single ViewNode.
|
|
19
|
+
* Returns null when no supported locator field is present.
|
|
20
|
+
*/
|
|
21
|
+
export declare function deriveLocator(node: ViewNode): Locator | null;
|
|
22
|
+
/**
|
|
23
|
+
* Flatten a ViewNode tree depth-first and annotate each node with its best locator.
|
|
24
|
+
* Nodes with no locatable field are included with locator: null.
|
|
25
|
+
*/
|
|
26
|
+
export declare function deriveElementList(roots: ViewNode[]): Array<{
|
|
27
|
+
node: ViewNode;
|
|
28
|
+
locator: Locator | null;
|
|
29
|
+
}>;
|
|
30
|
+
//# sourceMappingURL=locator-derivation.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"locator-derivation.d.ts","sourceRoot":"","sources":["../../src/lib/locator-derivation.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,wBAAwB,CAAC;AAkBvD,gFAAgF;AAChF,MAAM,MAAM,OAAO,GACf;IAAE,IAAI,EAAE,QAAQ,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,GACjC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAG,KAAK,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,GAAG,SAAS,CAAA;CAAE,GAC3D;IAAE,IAAI,EAAE,OAAO,CAAC;IAAE,KAAK,EAAE,MAAM,CAAA;CAAE,GACjC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAG,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC;AAiBtC;;;GAGG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,QAAQ,GAAG,OAAO,GAAG,IAAI,CAgB5D;AAED;;;GAGG;AACH,wBAAgB,iBAAiB,CAC/B,KAAK,EAAE,QAAQ,EAAE,GAChB,KAAK,CAAC;IAAE,IAAI,EAAE,QAAQ,CAAC;IAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAAA;CAAE,CAAC,CAapD"}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
// Derives the best mobilewright locator for every node in a ViewNode tree.
|
|
2
|
+
// Priority: Test ID > Role > Label > Text. Mirrors @mobilewright/core query-engine.ts.
|
|
3
|
+
// If mobilewright changes matching rules, update ROLE_TYPE_MAP below to stay in sync.
|
|
4
|
+
/** Maps mobilewright role names to the node type strings that resolve to each role. */
|
|
5
|
+
const ROLE_TYPE_MAP = {
|
|
6
|
+
button: ['button', 'imagebutton'],
|
|
7
|
+
textfield: ['textfield', 'securetextfield', 'edittext', 'searchfield', 'reactedittext'],
|
|
8
|
+
text: ['statictext', 'textview', 'text', 'reacttextview'],
|
|
9
|
+
image: ['image', 'imageview', 'reactimageview'],
|
|
10
|
+
switch: ['switch', 'toggle'],
|
|
11
|
+
checkbox: ['checkbox'],
|
|
12
|
+
slider: ['slider', 'seekbar'],
|
|
13
|
+
list: ['table', 'collectionview', 'listview', 'recyclerview', 'scrollview', 'reactscrollview'],
|
|
14
|
+
listitem: ['cell', 'linearlayout', 'relativelayout'],
|
|
15
|
+
tab: ['tab', 'tabbar'],
|
|
16
|
+
link: ['link'],
|
|
17
|
+
header: ['navigationbar', 'toolbar', 'header'],
|
|
18
|
+
};
|
|
19
|
+
/** Map node.type to a mobilewright role string. Returns null for unmapped types. */
|
|
20
|
+
function deriveRole(node) {
|
|
21
|
+
const type = (node.type ?? '').toLowerCase();
|
|
22
|
+
if (type === 'reactviewgroup') {
|
|
23
|
+
const isClickable = node.raw?.['clickable'] === 'true' || node.raw?.['accessible'] === 'true';
|
|
24
|
+
return isClickable ? 'button' : null;
|
|
25
|
+
}
|
|
26
|
+
for (const [role, types] of Object.entries(ROLE_TYPE_MAP)) {
|
|
27
|
+
if (types.includes(type))
|
|
28
|
+
return role;
|
|
29
|
+
}
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Derive the best mobilewright locator for a single ViewNode.
|
|
34
|
+
* Returns null when no supported locator field is present.
|
|
35
|
+
*/
|
|
36
|
+
export function deriveLocator(node) {
|
|
37
|
+
const testId = node.identifier || node.resourceId;
|
|
38
|
+
if (testId)
|
|
39
|
+
return { kind: 'testId', value: testId };
|
|
40
|
+
const role = deriveRole(node);
|
|
41
|
+
if (role) {
|
|
42
|
+
const name = node.label || node.text || undefined;
|
|
43
|
+
return { kind: 'role', value: role, name };
|
|
44
|
+
}
|
|
45
|
+
if (node.label)
|
|
46
|
+
return { kind: 'label', value: node.label };
|
|
47
|
+
const text = node.text ?? (node.value != null ? String(node.value) : undefined);
|
|
48
|
+
if (text)
|
|
49
|
+
return { kind: 'text', value: text };
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Flatten a ViewNode tree depth-first and annotate each node with its best locator.
|
|
54
|
+
* Nodes with no locatable field are included with locator: null.
|
|
55
|
+
*/
|
|
56
|
+
export function deriveElementList(roots) {
|
|
57
|
+
const result = [];
|
|
58
|
+
/** Depth-first recursive walk, pushing each visited node to result. */
|
|
59
|
+
function walk(nodes) {
|
|
60
|
+
for (const node of nodes) {
|
|
61
|
+
result.push({ node, locator: deriveLocator(node) });
|
|
62
|
+
if (node.children?.length)
|
|
63
|
+
walk(node.children);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
walk(roots);
|
|
67
|
+
return result;
|
|
68
|
+
}
|
|
69
|
+
//# sourceMappingURL=locator-derivation.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"locator-derivation.js","sourceRoot":"","sources":["../../src/lib/locator-derivation.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/** Structured logger that writes timestamped lines to stderr. */
|
|
2
|
+
export declare const logger: {
|
|
3
|
+
/** Log an informational message. */
|
|
4
|
+
info: (msg: string) => void;
|
|
5
|
+
/** Log a warning. */
|
|
6
|
+
warn: (msg: string) => void;
|
|
7
|
+
/** Log an error. */
|
|
8
|
+
error: (msg: string) => void;
|
|
9
|
+
};
|
|
10
|
+
//# sourceMappingURL=logger.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"logger.d.ts","sourceRoot":"","sources":["../../src/lib/logger.ts"],"names":[],"mappings":"AAKA,iEAAiE;AACjE,eAAO,MAAM,MAAM;IACjB,oCAAoC;gBACvB,MAAM;IACnB,qBAAqB;gBACR,MAAM;IACnB,oBAAoB;iBACP,MAAM;CACpB,CAAC"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/** Write a single timestamped log line to stderr. */
|
|
2
|
+
function log(level, msg) {
|
|
3
|
+
process.stderr.write(`[${new Date().toISOString()}] ${level} ${msg}\n`);
|
|
4
|
+
}
|
|
5
|
+
/** Structured logger that writes timestamped lines to stderr. */
|
|
6
|
+
export const logger = {
|
|
7
|
+
/** Log an informational message. */
|
|
8
|
+
info: (msg) => log('INFO ', msg),
|
|
9
|
+
/** Log a warning. */
|
|
10
|
+
warn: (msg) => log('WARN ', msg),
|
|
11
|
+
/** Log an error. */
|
|
12
|
+
error: (msg) => log('ERROR', msg),
|
|
13
|
+
};
|
|
14
|
+
//# sourceMappingURL=logger.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"logger.js","sourceRoot":"","sources":["../../src/lib/logger.ts"],"names":[],"mappings":"AAAA,qDAAqD;AACrD,SAAS,GAAG,CAAC,KAAa,EAAE,GAAW;IACrC,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,KAAK,KAAK,IAAI,GAAG,IAAI,CAAC,CAAC;AAC1E,CAAC;AAED,iEAAiE;AACjE,MAAM,CAAC,MAAM,MAAM,GAAG;IACpB,oCAAoC;IACpC,IAAI,EAAG,CAAC,GAAW,EAAE,EAAE,CAAC,GAAG,CAAC,OAAO,EAAE,GAAG,CAAC;IACzC,qBAAqB;IACrB,IAAI,EAAG,CAAC,GAAW,EAAE,EAAE,CAAC,GAAG,CAAC,OAAO,EAAE,GAAG,CAAC;IACzC,oBAAoB;IACpB,KAAK,EAAE,CAAC,GAAW,EAAE,EAAE,CAAC,GAAG,CAAC,OAAO,EAAE,GAAG,CAAC;CAC1C,CAAC"}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { DeviceManager } from '../lib/device-manager.js';
|
|
2
|
+
/**
|
|
3
|
+
* Express router for device listing and selection.
|
|
4
|
+
* GET /api/devices, POST /api/devices/:id/select
|
|
5
|
+
*/
|
|
6
|
+
export declare function createDevicesRouter(deviceManager: DeviceManager): import("express-serve-static-core").Router;
|
|
7
|
+
//# sourceMappingURL=devices.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"devices.d.ts","sourceRoot":"","sources":["../../src/routes/devices.ts"],"names":[],"mappings":"AACA,OAAO,EAAe,aAAa,EAAE,MAAM,0BAA0B,CAAC;AAEtE;;;GAGG;AACH,wBAAgB,mBAAmB,CAAC,aAAa,EAAE,aAAa,8CAsC/D"}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { Router } from 'express';
|
|
2
|
+
import { DeviceError } from '../lib/device-manager.js';
|
|
3
|
+
/**
|
|
4
|
+
* Express router for device listing and selection.
|
|
5
|
+
* GET /api/devices, POST /api/devices/:id/select
|
|
6
|
+
*/
|
|
7
|
+
export function createDevicesRouter(deviceManager) {
|
|
8
|
+
const router = Router();
|
|
9
|
+
// GET /api/devices
|
|
10
|
+
router.get('/', async (_req, res) => {
|
|
11
|
+
try {
|
|
12
|
+
const devices = await deviceManager.listDevices();
|
|
13
|
+
res.json({ devices, activeId: deviceManager.deviceInfo?.id ?? null });
|
|
14
|
+
}
|
|
15
|
+
catch (err) {
|
|
16
|
+
res.status(500).json({ error: err.message });
|
|
17
|
+
}
|
|
18
|
+
});
|
|
19
|
+
// POST /api/devices/:id/select body: { platform: 'ios' | 'android' }
|
|
20
|
+
router.post('/:id/select', async (req, res) => {
|
|
21
|
+
const { id } = req.params;
|
|
22
|
+
const { platform } = (req.body ?? {});
|
|
23
|
+
if (platform !== 'ios' && platform !== 'android') {
|
|
24
|
+
res.status(400).json({ error: 'platform must be \'ios\' or \'android\'' });
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
try {
|
|
28
|
+
const devices = await deviceManager.listDevices();
|
|
29
|
+
if (!devices.some(d => d.id === id && d.platform === platform)) {
|
|
30
|
+
res.status(404).json({ error: `Device '${id}' not found` });
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
await deviceManager.select(id, platform);
|
|
34
|
+
res.json({ ok: true });
|
|
35
|
+
}
|
|
36
|
+
catch (err) {
|
|
37
|
+
const status = err instanceof DeviceError && (err.code === 'blocked' || err.code === 'in_progress') ? 409 : 500;
|
|
38
|
+
res.status(status).json({ error: err.message });
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
return router;
|
|
42
|
+
}
|
|
43
|
+
//# sourceMappingURL=devices.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"devices.js","sourceRoot":"","sources":["../../src/routes/devices.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,SAAS,CAAC;AACjC,OAAO,EAAE,WAAW,EAAiB,MAAM,0BAA0B,CAAC;AAEtE;;;GAGG;AACH,MAAM,UAAU,mBAAmB,CAAC,aAA4B;IAC9D,MAAM,MAAM,GAAG,MAAM,EAAE,CAAC;IAExB,mBAAmB;IACnB,MAAM,CAAC,GAAG,CAAC,GAAG,EAAE,KAAK,EAAE,IAAI,EAAE,GAAG,EAAE,EAAE;QAClC,IAAI,CAAC;YACH,MAAM,OAAO,GAAG,MAAM,aAAa,CAAC,WAAW,EAAE,CAAC;YAClD,GAAG,CAAC,IAAI,CAAC,EAAE,OAAO,EAAE,QAAQ,EAAE,aAAa,CAAC,UAAU,EAAE,EAAE,IAAI,IAAI,EAAE,CAAC,CAAC;QACxE,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAG,GAAa,CAAC,OAAO,EAAE,CAAC,CAAC;QAC1D,CAAC;IACH,CAAC,CAAC,CAAC;IAEH,sEAAsE;IACtE,MAAM,CAAC,IAAI,CAAC,aAAa,EAAE,KAAK,EAAE,GAAG,EAAE,GAAG,EAAE,EAAE;QAC5C,MAAM,EAAE,EAAE,EAAE,GAAG,GAAG,CAAC,MAAM,CAAC;QAC1B,MAAM,EAAE,QAAQ,EAAE,GAAG,CAAC,GAAG,CAAC,IAAI,IAAI,EAAE,CAA0B,CAAC;QAE/D,IAAI,QAAQ,KAAK,KAAK,IAAI,QAAQ,KAAK,SAAS,EAAE,CAAC;YACjD,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,yCAAyC,EAAE,CAAC,CAAC;YAC3E,OAAO;QACT,CAAC;QAED,IAAI,CAAC;YACH,MAAM,OAAO,GAAG,MAAM,aAAa,CAAC,WAAW,EAAE,CAAC;YAClD,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,IAAI,CAAC,CAAC,QAAQ,KAAK,QAAQ,CAAC,EAAE,CAAC;gBAC/D,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,WAAW,EAAE,aAAa,EAAE,CAAC,CAAC;gBAC5D,OAAO;YACT,CAAC;YACD,MAAM,aAAa,CAAC,MAAM,CAAC,EAAE,EAAE,QAAQ,CAAC,CAAC;YACzC,GAAG,CAAC,IAAI,CAAC,EAAE,EAAE,EAAE,IAAI,EAAE,CAAC,CAAC;QACzB,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,MAAM,MAAM,GAAG,GAAG,YAAY,WAAW,IAAI,CAAC,GAAG,CAAC,IAAI,KAAK,SAAS,IAAI,GAAG,CAAC,IAAI,KAAK,aAAa,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC;YAChH,GAAG,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAG,GAAa,CAAC,OAAO,EAAE,CAAC,CAAC;QAC7D,CAAC;IACH,CAAC,CAAC,CAAC;IAEH,OAAO,MAAM,CAAC;AAChB,CAAC"}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { DeviceManager } from '../lib/device-manager.js';
|
|
2
|
+
/**
|
|
3
|
+
* Express router for the inspect endpoint.
|
|
4
|
+
* GET /api/inspect — returns screenshot + element list from the same device moment.
|
|
5
|
+
*/
|
|
6
|
+
export declare function createInspectRouter(deviceManager: DeviceManager): import("express-serve-static-core").Router;
|
|
7
|
+
//# sourceMappingURL=inspect.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"inspect.d.ts","sourceRoot":"","sources":["../../src/routes/inspect.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,aAAa,EAAE,MAAM,0BAA0B,CAAC;AAMzD;;;GAGG;AACH,wBAAgB,mBAAmB,CAAC,aAAa,EAAE,aAAa,8CA2C/D"}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { Router } from 'express';
|
|
2
|
+
import { deriveElementList } from '../lib/locator-derivation.js';
|
|
3
|
+
import { logger } from '../lib/logger.js';
|
|
4
|
+
const MAX_RETRIES = 3;
|
|
5
|
+
const RETRY_DELAY_MS = 1500;
|
|
6
|
+
const ATTEMPT_TIMEOUT_MS = 10_000;
|
|
7
|
+
/**
|
|
8
|
+
* Express router for the inspect endpoint.
|
|
9
|
+
* GET /api/inspect — returns screenshot + element list from the same device moment.
|
|
10
|
+
*/
|
|
11
|
+
export function createInspectRouter(deviceManager) {
|
|
12
|
+
const router = Router();
|
|
13
|
+
// GET /api/inspect
|
|
14
|
+
// Returns screenshot + element list from the same moment (no drift).
|
|
15
|
+
router.get('/inspect', async (_req, res) => {
|
|
16
|
+
const device = deviceManager.device;
|
|
17
|
+
if (!device) {
|
|
18
|
+
res.status(409).json({ error: 'No device selected' });
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
if (!deviceManager.beginInspect()) {
|
|
22
|
+
res.status(503).json({ error: 'Inspect already in progress' });
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
try {
|
|
26
|
+
const { screenshotBuffer, tree, size } = await attemptWithRetry(device);
|
|
27
|
+
const elements = deriveElementList(tree).map(({ node, locator }, index) => ({
|
|
28
|
+
index,
|
|
29
|
+
type: node.type,
|
|
30
|
+
label: node.label ?? null,
|
|
31
|
+
text: node.text ?? null,
|
|
32
|
+
bounds: node.bounds,
|
|
33
|
+
isVisible: node.isVisible,
|
|
34
|
+
locator,
|
|
35
|
+
}));
|
|
36
|
+
res.json({
|
|
37
|
+
screenshot: `data:image/png;base64,${screenshotBuffer.toString('base64')}`,
|
|
38
|
+
screen: { width: size.width, height: size.height },
|
|
39
|
+
elements,
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
catch (err) {
|
|
43
|
+
logger.error(`Inspect failed: ${err.message}`);
|
|
44
|
+
res.status(500).json({ error: err.message });
|
|
45
|
+
}
|
|
46
|
+
finally {
|
|
47
|
+
deviceManager.endInspect();
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
return router;
|
|
51
|
+
}
|
|
52
|
+
/** Run screenshot + viewTree + screenSize together, retrying up to MAX_RETRIES times on failure. */
|
|
53
|
+
async function attemptWithRetry(device) {
|
|
54
|
+
let lastErr;
|
|
55
|
+
for (let i = 0; i < MAX_RETRIES; i++) {
|
|
56
|
+
try {
|
|
57
|
+
return await withTimeout(Promise.all([device.screen.screenshot(), device.screen.viewTree(), device.screenSize()]).then(([screenshotBuffer, tree, size]) => ({ screenshotBuffer, tree, size })), ATTEMPT_TIMEOUT_MS, `Device operation timed out after ${ATTEMPT_TIMEOUT_MS}ms`);
|
|
58
|
+
}
|
|
59
|
+
catch (err) {
|
|
60
|
+
lastErr = err;
|
|
61
|
+
logger.warn(`Inspect attempt ${i + 1}/${MAX_RETRIES} failed: ${err.message}`);
|
|
62
|
+
if (i < MAX_RETRIES - 1)
|
|
63
|
+
await new Promise(r => setTimeout(r, RETRY_DELAY_MS));
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
throw lastErr;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Race promise against a ms deadline. Rejects with message on timeout.
|
|
70
|
+
* Note: cannot cancel the underlying promise; it continues running until the driver times out.
|
|
71
|
+
*/
|
|
72
|
+
function withTimeout(promise, ms, message) {
|
|
73
|
+
let timer;
|
|
74
|
+
return Promise.race([
|
|
75
|
+
promise,
|
|
76
|
+
new Promise((_, reject) => { timer = setTimeout(() => reject(new Error(message)), ms); }),
|
|
77
|
+
]).finally(() => clearTimeout(timer));
|
|
78
|
+
}
|
|
79
|
+
//# sourceMappingURL=inspect.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"inspect.js","sourceRoot":"","sources":["../../src/routes/inspect.ts"],"names":[],"mappings":"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"}
|