or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

Files

docs

browser-control.mdindex.mdmobile-device.mdnetwork-api.mdpage-interaction.mdtesting-framework.mdvisual-debugging.md

mobile-device.mddocs/

0

# Mobile & Device Testing

1

2

Mobile browser emulation, device-specific testing, Android automation, and Electron app automation for comprehensive cross-platform testing.

3

4

## Capabilities

5

6

### Device Emulation

7

8

Pre-configured device descriptors for mobile and desktop browser testing.

9

10

```typescript { .api }

11

/**

12

* Collection of device descriptors for browser emulation

13

*/

14

const devices: Devices;

15

16

interface Devices {

17

/** Device descriptors indexed by name */

18

[key: string]: DeviceDescriptor;

19

}

20

21

interface DeviceDescriptor {

22

/** User agent string */

23

userAgent: string;

24

/** Screen dimensions */

25

viewport: ViewportSize;

26

/** Device pixel ratio */

27

deviceScaleFactor: number;

28

/** Mobile device flag */

29

isMobile: boolean;

30

/** Touch support flag */

31

hasTouch: boolean;

32

/** Default browser type */

33

defaultBrowserType?: 'chromium' | 'firefox' | 'webkit';

34

}

35

36

interface ViewportSize {

37

width: number;

38

height: number;

39

}

40

```

41

42

**Usage Examples:**

43

44

```typescript

45

import { devices, chromium } from 'playwright';

46

47

// Launch with device emulation

48

const browser = await chromium.launch();

49

const context = await browser.newContext({

50

...devices['iPhone 12'],

51

});

52

const page = await context.newPage();

53

54

// Common device presets

55

const iphone = devices['iPhone 12'];

56

const ipad = devices['iPad Pro'];

57

const pixel = devices['Pixel 5'];

58

const galaxy = devices['Galaxy S9+'];

59

const desktopChrome = devices['Desktop Chrome'];

60

const desktopFirefox = devices['Desktop Firefox'];

61

62

console.log('iPhone 12 viewport:', iphone.viewport);

63

console.log('Is mobile:', iphone.isMobile);

64

console.log('Has touch:', iphone.hasTouch);

65

66

// Custom device configuration

67

const customDevice = {

68

userAgent: 'Custom Device User Agent',

69

viewport: { width: 375, height: 812 },

70

deviceScaleFactor: 3,

71

isMobile: true,

72

hasTouch: true

73

};

74

75

const customContext = await browser.newContext(customDevice);

76

```

77

78

### Android Device Automation

79

80

Connect to and automate Android devices and emulators.

81

82

```typescript { .api }

83

/**

84

* Android automation interface

85

*/

86

const _android: Android;

87

88

interface Android {

89

/** List connected Android devices */

90

devices(): Promise<AndroidDevice[]>;

91

/** Set default timeout */

92

setDefaultTimeout(timeout: number): void;

93

/** Connect to device via ADB */

94

connect(wsEndpoint: string, options?: AndroidConnectOptions): Promise<AndroidDevice>;

95

}

96

97

interface AndroidDevice {

98

/** Device model */

99

model(): string;

100

/** Device serial number */

101

serial(): string;

102

/** Launch application */

103

launchBrowser(options?: AndroidLaunchOptions): Promise<BrowserContext>;

104

/** Close device connection */

105

close(): Promise<void>;

106

/** Install APK */

107

installApk(apkPath: string): Promise<void>;

108

/** Get device shell */

109

shell(command: string): Promise<Buffer>;

110

/** Push file to device */

111

push(local: string, remote: string): Promise<void>;

112

/** Pull file from device */

113

pull(remote: string, local: string): Promise<void>;

114

/** Take screenshot */

115

screenshot(): Promise<Buffer>;

116

/** Get web views */

117

webViews(): Promise<AndroidWebView[]>;

118

/** Get web view by selector */

119

webView(selector: AndroidSelector): Promise<AndroidWebView>;

120

/** Tap coordinates */

121

tap(selector: AndroidSelector): Promise<void>;

122

/** Fill text */

123

fill(selector: AndroidSelector, text: string): Promise<void>;

124

/** Press key */

125

press(key: AndroidKey): Promise<void>;

126

/** Scroll */

127

scroll(selector: AndroidSelector, direction: 'down' | 'up' | 'left' | 'right', percent: number): Promise<void>;

128

/** Wait for element */

129

wait(selector: AndroidSelector, options?: AndroidWaitOptions): Promise<AndroidElementInfo>;

130

/** Get element info */

131

info(selector: AndroidSelector): Promise<AndroidElementInfo>;

132

/** Input interface */

133

input: AndroidInput;

134

}

135

136

interface AndroidLaunchOptions {

137

/** Browser package name */

138

pkg?: string;

139

/** Browser activity */

140

activity?: string;

141

/** Wait for network idle */

142

waitForNetworkIdle?: boolean;

143

}

144

145

interface AndroidConnectOptions {

146

/** Connection timeout */

147

timeout?: number;

148

}

149

150

interface AndroidWaitOptions {

151

/** Wait timeout */

152

timeout?: number;

153

/** Element state to wait for */

154

state?: 'gone' | 'present';

155

}

156

```

157

158

**Usage Examples:**

159

160

```typescript

161

import { _android as android } from 'playwright';

162

163

// Connect to Android device

164

const devices = await android.devices();

165

const device = devices[0];

166

167

console.log('Device model:', device.model());

168

console.log('Serial:', device.serial());

169

170

// Launch browser on device

171

const context = await device.launchBrowser();

172

const page = await context.newPage();

173

await page.goto('https://example.com');

174

175

// Native Android automation

176

await device.tap({ text: 'Settings' });

177

await device.fill({ resource: 'com.android.settings:id/search' }, 'wifi');

178

await device.press('KEYCODE_ENTER');

179

180

// Take screenshot

181

const screenshot = await device.screenshot();

182

require('fs').writeFileSync('android-screenshot.png', screenshot);

183

184

// Work with WebViews

185

const webViews = await device.webViews();

186

if (webViews.length > 0) {

187

const webView = webViews[0];

188

const page = await webView.page();

189

await page.click('button');

190

}

191

192

await device.close();

193

```

194

195

### Android Input & Interaction

196

197

Handle Android-specific input methods and UI interactions.

198

199

```typescript { .api }

200

interface AndroidInput {

201

/** Tap coordinates */

202

tap(point: { x: number; y: number }): Promise<void>;

203

/** Swipe gesture */

204

swipe(from: { x: number; y: number }, to: { x: number; y: number }, steps: number): Promise<void>;

205

/** Drag gesture */

206

drag(from: { x: number; y: number }, to: { x: number; y: number }, steps: number): Promise<void>;

207

/** Press key */

208

press(key: AndroidKey): Promise<void>;

209

/** Type text */

210

type(text: string): Promise<void>;

211

}

212

213

interface AndroidWebView {

214

/** Get package name */

215

pkg(): string;

216

/** Get page instance */

217

page(): Promise<Page>;

218

}

219

220

interface AndroidSocket {

221

/** Write data to socket */

222

write(data: Buffer | string): Promise<void>;

223

/** Close socket */

224

close(): Promise<void>;

225

}

226

227

type AndroidElementInfo = {

228

/** Element bounds */

229

bounds: { x: number; y: number; width: number; height: number };

230

/** Element class */

231

clazz: string;

232

/** Element description */

233

desc: string;

234

/** Package name */

235

pkg: string;

236

/** Resource ID */

237

res: string;

238

/** Element text */

239

text: string;

240

/** Element checkable */

241

checkable: boolean;

242

/** Element checked */

243

checked: boolean;

244

/** Element clickable */

245

clickable: boolean;

246

/** Element enabled */

247

enabled: boolean;

248

/** Element focusable */

249

focusable: boolean;

250

/** Element focused */

251

focused: boolean;

252

/** Element long clickable */

253

longClickable: boolean;

254

/** Element scrollable */

255

scrollable: boolean;

256

/** Element selected */

257

selected: boolean;

258

};

259

260

type AndroidSelector = {

261

/** Select by checkable state */

262

checkable?: boolean;

263

/** Select by checked state */

264

checked?: boolean;

265

/** Select by class name */

266

clazz?: string | RegExp;

267

/** Select by clickable state */

268

clickable?: boolean;

269

/** Select by depth */

270

depth?: number;

271

/** Select by description */

272

desc?: string | RegExp;

273

/** Select by enabled state */

274

enabled?: boolean;

275

/** Select by focusable state */

276

focusable?: boolean;

277

/** Select by focused state */

278

focused?: boolean;

279

/** Select by long clickable state */

280

longClickable?: boolean;

281

/** Select by package name */

282

pkg?: string | RegExp;

283

/** Select by resource ID */

284

res?: string | RegExp;

285

/** Select by scrollable state */

286

scrollable?: boolean;

287

/** Select by selected state */

288

selected?: boolean;

289

/** Select by text content */

290

text?: string | RegExp;

291

};

292

293

type AndroidKey =

294

| 'KEYCODE_UNKNOWN' | 'KEYCODE_SOFT_LEFT' | 'KEYCODE_SOFT_RIGHT' | 'KEYCODE_HOME'

295

| 'KEYCODE_BACK' | 'KEYCODE_CALL' | 'KEYCODE_ENDCALL' | '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9'

296

| 'KEYCODE_STAR' | 'KEYCODE_POUND' | 'KEYCODE_DPAD_UP' | 'KEYCODE_DPAD_DOWN' | 'KEYCODE_DPAD_LEFT' | 'KEYCODE_DPAD_RIGHT'

297

| 'KEYCODE_DPAD_CENTER' | 'KEYCODE_VOLUME_UP' | 'KEYCODE_VOLUME_DOWN' | 'KEYCODE_POWER' | 'KEYCODE_CAMERA'

298

| 'KEYCODE_CLEAR' | 'A' | 'B' | 'C' | 'D' | 'E' | 'F' | 'G' | 'H' | 'I' | 'J' | 'K' | 'L' | 'M' | 'N' | 'O' | 'P' | 'Q' | 'R' | 'S' | 'T' | 'U' | 'V' | 'W' | 'X' | 'Y' | 'Z'

299

| 'KEYCODE_COMMA' | 'KEYCODE_PERIOD' | 'KEYCODE_ALT_LEFT' | 'KEYCODE_ALT_RIGHT' | 'KEYCODE_SHIFT_LEFT' | 'KEYCODE_SHIFT_RIGHT'

300

| 'KEYCODE_TAB' | 'KEYCODE_SPACE' | 'KEYCODE_SYM' | 'KEYCODE_EXPLORER' | 'KEYCODE_ENVELOPE' | 'KEYCODE_ENTER'

301

| 'KEYCODE_DEL' | 'KEYCODE_GRAVE' | 'KEYCODE_MINUS' | 'KEYCODE_EQUALS' | 'KEYCODE_LEFT_BRACKET' | 'KEYCODE_RIGHT_BRACKET'

302

| 'KEYCODE_BACKSLASH' | 'KEYCODE_SEMICOLON' | 'KEYCODE_APOSTROPHE' | 'KEYCODE_SLASH' | 'KEYCODE_AT' | 'KEYCODE_NUM'

303

| 'KEYCODE_HEADSETHOOK' | 'KEYCODE_FOCUS' | 'KEYCODE_PLUS' | 'KEYCODE_MENU' | 'KEYCODE_NOTIFICATION' | 'KEYCODE_SEARCH'

304

| 'KEYCODE_MEDIA_PLAY_PAUSE' | 'KEYCODE_MEDIA_STOP' | 'KEYCODE_MEDIA_NEXT' | 'KEYCODE_MEDIA_PREVIOUS' | 'KEYCODE_MEDIA_REWIND'

305

| 'KEYCODE_MEDIA_FAST_FORWARD' | 'KEYCODE_MUTE';

306

```

307

308

**Usage Examples:**

309

310

```typescript

311

// Android input interactions

312

await device.input.tap({ x: 100, y: 200 });

313

await device.input.swipe(

314

{ x: 100, y: 500 },

315

{ x: 100, y: 200 },

316

10

317

);

318

319

// Press hardware keys

320

await device.input.press('KEYCODE_HOME');

321

await device.input.press('KEYCODE_BACK');

322

await device.input.press('KEYCODE_VOLUME_UP');

323

324

// Type text

325

await device.input.type('Hello Android');

326

327

// Complex selector

328

const element = await device.wait({

329

pkg: /com\.android\.settings/,

330

text: /Wi.*Fi/,

331

clickable: true

332

});

333

334

console.log('Found element:', element.text);

335

console.log('Element bounds:', element.bounds);

336

337

// WebView automation

338

const webViews = await device.webViews();

339

for (const webView of webViews) {

340

console.log('WebView package:', webView.pkg());

341

const page = await webView.page();

342

const title = await page.title();

343

console.log('WebView title:', title);

344

}

345

```

346

347

### Electron Application Automation

348

349

Automate Electron desktop applications.

350

351

```typescript { .api }

352

/**

353

* Electron automation interface

354

*/

355

const _electron: Electron;

356

357

interface Electron {

358

/** Launch Electron application */

359

launch(options: ElectronLaunchOptions): Promise<ElectronApplication>;

360

}

361

362

interface ElectronApplication {

363

/** Get BrowserWindow instance */

364

browserWindow(page: Page): Promise<JSHandle>;

365

/** Close application */

366

close(): Promise<void>;

367

/** Get application context */

368

context(): BrowserContext;

369

/** Evaluate in main process */

370

evaluate<R, Arg>(pageFunction: PageFunction<Arg, R>, arg: Arg): Promise<R>;

371

evaluate<R>(pageFunction: PageFunction<void, R>, arg?: any): Promise<R>;

372

/** First window */

373

firstWindow(options?: ElectronApplicationFirstWindowOptions): Promise<Page>;

374

/** Get all windows */

375

windows(): Page[];

376

/** Wait for event */

377

waitForEvent(event: string, optionsOrPredicate?: WaitForEventOptions): Promise<any>;

378

/** Get process */

379

process(): ChildProcess;

380

}

381

382

interface ElectronLaunchOptions {

383

/** Path to Electron app */

384

executablePath?: string;

385

/** Application arguments */

386

args?: string[];

387

/** Working directory */

388

cwd?: string;

389

/** Environment variables */

390

env?: { [key: string]: string | number | boolean };

391

/** Connection timeout */

392

timeout?: number;

393

/** Accept downloads */

394

acceptDownloads?: boolean;

395

/** Bypass CSP */

396

bypassCSP?: boolean;

397

/** Color scheme */

398

colorScheme?: 'light' | 'dark' | 'no-preference';

399

/** Extra HTTP headers */

400

extraHTTPHeaders?: { [key: string]: string };

401

/** Geolocation */

402

geolocation?: Geolocation;

403

/** HTTP credentials */

404

httpCredentials?: HTTPCredentials;

405

/** Ignore HTTPS errors */

406

ignoreHTTPSErrors?: boolean;

407

/** Locale */

408

locale?: string;

409

/** Offline mode */

410

offline?: boolean;

411

/** Permissions */

412

permissions?: string[];

413

/** Proxy */

414

proxy?: ProxySettings;

415

/** Record HAR */

416

recordHar?: { omitContent?: boolean; path: string };

417

/** Record video */

418

recordVideo?: { dir: string; size?: ViewportSize };

419

/** Reduced motion */

420

reducedMotion?: 'reduce' | 'no-preference';

421

/** Service workers */

422

serviceWorkers?: 'allow' | 'block';

423

/** Storage state */

424

storageState?: string | { cookies: Cookie[]; origins: any[] };

425

/** Strict selectors */

426

strictSelectors?: boolean;

427

/** Timezone */

428

timezoneId?: string;

429

/** User agent */

430

userAgent?: string;

431

/** Viewport */

432

viewport?: ViewportSize | null;

433

}

434

435

interface ElectronApplicationFirstWindowOptions {

436

/** Wait timeout */

437

timeout?: number;

438

}

439

```

440

441

**Usage Examples:**

442

443

```typescript

444

import { _electron as electron } from 'playwright';

445

446

// Launch Electron app

447

const electronApp = await electron.launch({

448

args: ['./my-electron-app']

449

});

450

451

// Get first window

452

const window = await electronApp.firstWindow();

453

await window.waitForLoadState();

454

455

// Interact with main process

456

const version = await electronApp.evaluate(async ({ app }) => {

457

return app.getVersion();

458

});

459

460

console.log('App version:', version);

461

462

// Interact with renderer process (window)

463

await window.click('#menu-file');

464

await window.click('#menu-open');

465

466

// Get BrowserWindow handle

467

const browserWindow = await electronApp.browserWindow(window);

468

const isMaximized = await browserWindow.evaluate(win => win.isMaximized());

469

470

console.log('Window maximized:', isMaximized);

471

472

// Handle multiple windows

473

const allWindows = electronApp.windows();

474

console.log('Window count:', allWindows.length);

475

476

// Wait for new window

477

electronApp.on('window', async (page) => {

478

console.log('New window opened:', await page.title());

479

});

480

481

await electronApp.close();

482

```

483

484

### Device-Specific Testing Patterns

485

486

Common patterns for mobile and device testing.

487

488

```typescript { .api }

489

interface BrowserContext {

490

/** Add geolocation override */

491

setGeolocation(geolocation: Geolocation | null): Promise<void>;

492

/** Set offline mode */

493

setOffline(offline: boolean): Promise<void>;

494

/** Override permissions */

495

grantPermissions(permissions: string[], options?: BrowserContextGrantPermissionsOptions): Promise<void>;

496

/** Clear permissions */

497

clearPermissions(): Promise<void>;

498

}

499

500

interface Page {

501

/** Emulate media features */

502

emulateMedia(options?: PageEmulateMediaOptions): Promise<void>;

503

/** Set viewport size */

504

setViewportSize(viewportSize: ViewportSize): Promise<void>;

505

}

506

507

interface PageEmulateMediaOptions {

508

/** Media type */

509

media?: 'screen' | 'print' | null;

510

/** Color scheme */

511

colorScheme?: 'light' | 'dark' | 'no-preference' | null;

512

/** Reduced motion */

513

reducedMotion?: 'reduce' | 'no-preference' | null;

514

/** Forced colors */

515

forcedColors?: 'active' | 'none' | null;

516

}

517

518

interface BrowserContextGrantPermissionsOptions {

519

/** Origin for permissions */

520

origin?: string;

521

}

522

```

523

524

**Usage Examples:**

525

526

```typescript

527

// Test responsive design

528

const context = await browser.newContext({

529

...devices['iPhone 12'],

530

viewport: { width: 375, height: 812 }

531

});

532

533

const page = await context.newPage();

534

await page.goto('https://example.com');

535

536

// Test different orientations

537

await page.setViewportSize({ width: 812, height: 375 }); // Landscape

538

await page.screenshot({ path: 'landscape.png' });

539

540

await page.setViewportSize({ width: 375, height: 812 }); // Portrait

541

await page.screenshot({ path: 'portrait.png' });

542

543

// Test geolocation

544

await context.setGeolocation({

545

latitude: 37.7749,

546

longitude: -122.4194

547

});

548

await context.grantPermissions(['geolocation']);

549

550

// Test offline behavior

551

await context.setOffline(true);

552

await page.reload();

553

await context.setOffline(false);

554

555

// Test dark mode

556

await page.emulateMedia({ colorScheme: 'dark' });

557

await page.screenshot({ path: 'dark-mode.png' });

558

559

// Test print media

560

await page.emulateMedia({ media: 'print' });

561

await page.screenshot({ path: 'print-view.png' });

562

563

// Test reduced motion

564

await page.emulateMedia({ reducedMotion: 'reduce' });

565

```

566

567

## Types

568

569

### Mobile Testing Configuration

570

571

```typescript { .api }

572

interface Geolocation {

573

/** Latitude coordinate */

574

latitude: number;

575

/** Longitude coordinate */

576

longitude: number;

577

/** Location accuracy in meters */

578

accuracy?: number;

579

}

580

581

interface ProxySettings {

582

/** Proxy server URL */

583

server: string;

584

/** Bypass proxy for these patterns */

585

bypass?: string;

586

/** Username for authentication */

587

username?: string;

588

/** Password for authentication */

589

password?: string;

590

}

591

592

interface WaitForEventOptions {

593

/** Event timeout */

594

timeout?: number;

595

/** Event predicate */

596

predicate?: Function;

597

}

598

599

type PageFunction<Arg, R> = string | ((arg: Arg) => R | Promise<R>);

600

```