-
Notifications
You must be signed in to change notification settings - Fork 51
Expand file tree
/
Copy pathdevice.ts
More file actions
155 lines (127 loc) · 5.08 KB
/
Copy pathdevice.ts
File metadata and controls
155 lines (127 loc) · 5.08 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
import type {
AnimationScales,
AppInfo,
ConnectionConfig,
LaunchOptions,
MobilewrightDriver,
Orientation,
RecordingOptions,
RecordingResult,
Session,
} from '@mobilewright/protocol';
import { Screen } from './screen.js';
import type { LocatorOptions } from './locator.js';
import { retryUntil } from './poll.js';
const LAUNCH_APP_TIMEOUT = 20_000;
export interface DeviceOptions {
locatorDefaults?: LocatorOptions;
}
export class Device {
readonly driver: MobilewrightDriver;
private cleanupCallbacks: Array<() => Promise<void>> = [];
private _screen: Screen | null = null;
private readonly opts: DeviceOptions;
constructor(driver: MobilewrightDriver, opts: DeviceOptions = {}) {
this.driver = driver;
this.opts = opts;
}
/** Register a callback to run on close(). Used by launchers for cleanup. */
onClose(callback: () => Promise<void>): void {
this.cleanupCallbacks.push(callback);
}
// ─── Connection lifecycle ────────────────────────────────────
async connect(config: ConnectionConfig): Promise<Session> {
return this.driver.connect(config);
}
async disconnect(): Promise<void> {
await this.driver.disconnect();
}
/** Full cleanup: disconnect + run any registered cleanup callbacks. */
async close(): Promise<void> {
await this.disconnect();
for (const cb of this.cleanupCallbacks) {
await cb();
}
this.cleanupCallbacks = [];
}
get screen(): Screen {
this._screen ??= new Screen(this.driver, this.opts.locatorDefaults);
return this._screen;
}
// ─── Device control ──────────────────────────────────────────
async getOrientation(): Promise<Orientation> {
return this.driver.getOrientation();
}
async setOrientation(orientation: Orientation): Promise<void> {
return this.driver.setOrientation(orientation);
}
async openUrl(url: string): Promise<void> {
return this.driver.openUrl(url);
}
/** Alias for openUrl — matches Playwright's page.goto(). */
async goto(url: string): Promise<void> {
return this.openUrl(url);
}
// ─── App control ─────────────────────────────────────────────
async launchApp(bundleId: string, opts?: LaunchOptions): Promise<void> {
await this.driver.launchApp(bundleId, opts);
if (opts?.noWaitAfter) {
return;
}
try {
await retryUntil(
() => this.getForegroundApp(),
(app) => app.bundleId === bundleId,
LAUNCH_APP_TIMEOUT,
`launchApp: timed out waiting for "${bundleId}" to be in foreground`,
);
} catch (err) {
if (String(err).includes('could not determine foreground app')) {
// mobilecli's WebSocket RPC path for device.apps.foreground fails on
// some Android devices even though the app launched successfully.
// Warn and continue rather than failing the launch entirely.
console.warn(`[mobilewright] warning: could not verify "${bundleId}" reached foreground — proceeding anyway. This is a known mobilecli issue on some Android devices.`);
return;
}
throw err;
}
}
async terminateApp(bundleId: string): Promise<void> {
return this.driver.terminateApp(bundleId);
}
async listApps(): Promise<AppInfo[]> {
return this.driver.listApps();
}
async getForegroundApp(): Promise<AppInfo> {
return this.driver.getForegroundApp();
}
async installApp(path: string): Promise<void> {
return this.driver.installApp(path);
}
async uninstallApp(bundleId: string): Promise<void> {
return this.driver.uninstallApp(bundleId);
}
// ─── Recording ─────────────────────────────────────────────────
async startRecording(opts: RecordingOptions): Promise<void> {
return this.driver.startRecording(opts);
}
async stopRecording(): Promise<RecordingResult> {
return this.driver.stopRecording();
}
// ─── Android animations ─────────────────────────────────────────
// uiautomator dump fails on continuously animated screens (e.g. a
// ride-searching page with a SurfaceView/TextureView animation that
// never settles). Disabling the three system animation scales before
// calling getViewHierarchy() lets the dump succeed.
//
// disableAnimations() saves and returns the current scales so the
// caller can restore the exact originals via enableAnimations(saved).
async disableAnimations(): Promise<AnimationScales> {
const saved = await this.driver.getAnimationScales();
await this.driver.setAnimationScales({ window: 0, transition: 0, animator: 0 });
return saved;
}
async enableAnimations(scales: AnimationScales = { window: 1, transition: 1, animator: 1 }): Promise<void> {
await this.driver.setAnimationScales(scales);
}
}