mobilewright

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Mobilewright Skill

Mobilewright 技能指南

Mobile device automation, testing, and development verification using mobilewright — a Playwright-inspired framework for iOS and Android.
Use this skill when:
  • Writing mobile app tests (E2E, regression, smoke)
  • Automating mobile device interactions (scraping, screenshots, repetitive tasks)
  • Verifying UI implementation during mobile app development — take screenshots, dump the view tree, and confirm the code you wrote produces the expected result
使用mobilewright进行移动设备自动化、测试与开发验证——这是一款受Playwright启发的iOS和Android框架。
在以下场景使用本技能:
  • 编写移动应用测试(端到端测试、回归测试、冒烟测试)
  • 自动化移动设备交互(数据爬取、截图、重复性任务)
  • 移动应用开发期间验证UI实现——截取截图、导出视图树,确认编写的代码能产生预期结果

First Steps

前期准备

Before writing any code, gather context:
  1. What are you doing? Ask the user: testing, automation script, or verifying a UI you just built?
  2. What platform? Ask the user: iOS, Android, or both?
  3. TypeScript or JavaScript? Ask the user. Both are supported.
  4. Find the bundle ID automatically. Don't ask — look for it:
    • iOS: search for
      PRODUCT_BUNDLE_IDENTIFIER
      in
      *.pbxproj
      files, or
      bundleIdentifier
      in Expo's
      app.json
      /
      app.config.js
    • Android: search for
      applicationId
      in
      build.gradle
      or
      build.gradle.kts
      , or
      package
      in
      AndroidManifest.xml
    • React Native / Expo: check
      app.json
      for
      expo.ios.bundleIdentifier
      and
      expo.android.package
    • If none found, check if there's already a
      mobilewright.config.ts
      with a
      bundleId
      set
    • Only ask the user if you cannot find it automatically
  5. Is there an existing
    mobilewright.config.ts
    ?
    If not, create one.
编写任何代码之前,先收集以下信息:
  1. 你的需求是什么? 询问用户:是测试、编写自动化脚本,还是验证刚开发的UI?
  2. 目标平台? 询问用户:iOS、Android,还是两者兼顾?
  3. 使用TypeScript还是JavaScript? 询问用户,两者均受支持。
  4. 自动查找Bundle ID 不要直接询问用户,先尝试自动查找:
    • iOS:在
      *.pbxproj
      文件中搜索
      PRODUCT_BUNDLE_IDENTIFIER
      ,或在Expo的
      app.json
      /
      app.config.js
      中搜索
      bundleIdentifier
    • Android:在
      build.gradle
      或
      build.gradle.kts
      中搜索
      applicationId
      ,或在
      AndroidManifest.xml
      中搜索
      package
    • React Native/Expo:在
      app.json
      中查看
      expo.ios.bundleIdentifier
      和
      expo.android.package
    • 如果未找到,检查是否已存在设置了
      bundleId
      的
      mobilewright.config.ts
      文件
    • 只有在自动查找失败时,才询问用户
  5. 是否已有
    mobilewright.config.ts
    ?
    如果没有,创建一个。

Project Setup

项目初始化

Scaffold a new project:
bash
npx mobilewright init
This creates
mobilewright.config.ts
and
example.test.ts
. Verify the environment:
bash
npx mobilewright doctor
初始化新项目:
bash
npx mobilewright init
此命令会创建
mobilewright.config.ts
和
example.test.ts
文件。验证环境配置:
bash
npx mobilewright doctor

CLI Commands

CLI命令

bash
npx mobilewright init                              # scaffold config + example test
npx mobilewright doctor                            # verify environment setup
npx mobilewright devices                           # list connected devices/simulators
npx mobilewright test                              # run tests
npx mobilewright screenshot                        # take a screenshot of a connected device
npx mobilewright screenshot -o login.png           # save to specific file
npx mobilewright screenshot -d <device-id>         # target a specific device
bash
npx mobilewright init                              # 生成配置文件和示例测试
npx mobilewright doctor                            # 验证环境配置
npx mobilewright devices                           # 列出已连接的设备/模拟器
npx mobilewright test                              # 运行测试
npx mobilewright screenshot                        # 截取已连接设备的屏幕截图
npx mobilewright screenshot -o login.png           # 将截图保存到指定文件
npx mobilewright screenshot -d <device-id>         # 针对指定设备截图

Configuration

配置示例

typescript
import { defineConfig } from 'mobilewright';

export default defineConfig({
  platform: 'ios',                // 'ios' or 'android'
  bundleId: 'com.example.myapp',  // app bundle ID
  deviceName: 'iPhone 16',        // regex to match device name (optional)
  timeout: 10_000,                // global timeout in ms (optional)
});
typescript
import { defineConfig } from 'mobilewright';

export default defineConfig({
  platform: 'ios',                // 'ios' 或 'android'
  bundleId: 'com.example.myapp',  // 应用Bundle ID
  deviceName: 'iPhone 16',        // 匹配设备名称的正则表达式(可选)
  timeout: 10_000,                // 全局超时时间(毫秒,可选)
});

UI Inspection — Do This First

UI检查——务必先执行此步骤

Before writing any locator, always inspect the UI tree. Do not guess at element names, labels, or types. Use
screen.viewTree()
to get the live accessibility hierarchy, then choose the best locator strategy based on what you see.
typescript
const tree = await screen.viewTree();
console.log(JSON.stringify(tree, null, 2));
This returns the full view hierarchy as JSON. Examine it to find:
  • Accessibility identifiers (
    testId
    )
  • Accessibility labels (
    label
    )
  • Element types (
    type
    )
  • Text content (
    text
    /
    value
    )
  • Placeholder text
Then pick the locator using this priority order:
编写任何定位器之前,务必先检查UI树。 不要猜测元素名称、标签或类型。使用
screen.viewTree()
获取实时可访问性层级结构,然后根据所见内容选择最佳定位策略。
typescript
const tree = await screen.viewTree();
console.log(JSON.stringify(tree, null, 2));
此方法会以JSON格式返回完整的视图层级。检查以下内容:
  • 可访问性标识符(
    testId
    )
  • 可访问性标签(
    label
    )
  • 元素类型(
    type
    )
  • 文本内容(
    text
    /
    value
    )
  • 占位符文本
然后按照以下优先级选择定位器:

Locator Priority (best to worst)

定位器优先级(从优到劣)

PriorityLocatorWhy
1
getByTestId('login-btn')
Most stable. Set by developers, won't change with copy or i18n.
2
getByRole('button', { name: 'Sign In' })
Semantic and cross-platform. Maps to native types on both iOS and Android.
3
getByLabel('Sign In')
Accessibility label. Stable if accessibility is maintained.
4
getByPlaceholder('Search...')
Good for text fields. Only available on Locator, not on Screen.
5
getByText('Sign In')
Fragile. Changes with copy updates, i18n, or minor rewording.
6
getByType('Button')
Platform-specific. Ties your code to iOS or Android element types.
优先级定位器原因
1
getByTestId('login-btn')
最稳定。由开发者设置,不会随文案更新或国际化更改而变化。
2
getByRole('button', { name: 'Sign In' })
语义化且跨平台。映射到iOS和Android的原生元素类型。
3
getByLabel('Sign In')
可访问性标签。如果可访问性维护良好,则稳定性高。
4
getByPlaceholder('Search...')
适用于文本字段。仅在Locator上可用,Screen不可用。
5
getByText('Sign In')
脆弱。会随文案更新、国际化或细微措辞变化而失效。
6
getByType('Button')
平台特定。将代码绑定到iOS或Android的元素类型。

Test File Structure

测试文件结构

Use
@mobilewright/test
which extends Playwright Test with mobile fixtures.
typescript
import { test, expect } from '@mobilewright/test';

test.use({ bundleId: 'com.example.myapp', video: 'on' });

test.beforeEach(async ({ device, bundleId }) => {
  // Fresh app state for every test
  await device.terminateApp(bundleId).catch(() => {});
  await device.launchApp(bundleId);
});

test.describe('login flow', () => {
  test('user can sign in with valid credentials', async ({ screen }) => {
    await screen.getByLabel('Email').fill('user@example.com');
    await screen.getByLabel('Password').fill('password123');
    await screen.getByRole('button', { name: 'Sign In' }).tap();

    await expect(screen.getByText('Welcome back')).toBeVisible();
  });
});
Run tests:
bash
npx mobilewright test
npx mobilewright test login.test.ts              # specific file
npx mobilewright test --grep "sign in"           # run a single test by name
npx mobilewright test --reporter list            # line-by-line output, good for CI/agents
npx mobilewright test --reporter json            # machine-readable JSON output
使用
@mobilewright/test
,它基于Playwright Test扩展了移动设备相关的fixture。
typescript
import { test, expect } from '@mobilewright/test';

test.use({ bundleId: 'com.example.myapp', video: 'on' });

test.beforeEach(async ({ device, bundleId }) => {
  // 每个测试都使用全新的应用状态
  await device.terminateApp(bundleId).catch(() => {});
  await device.launchApp(bundleId);
});

test.describe('登录流程', () => {
  test('用户可使用有效凭证登录', async ({ screen }) => {
    await screen.getByLabel('Email').fill('user@example.com');
    await screen.getByLabel('Password').fill('password123');
    await screen.getByRole('button', { name: 'Sign In' }).tap();

    await expect(screen.getByText('Welcome back')).toBeVisible();
  });
});
运行测试:
bash
npx mobilewright test
npx mobilewright test login.test.ts              # 运行指定文件的测试
npx mobilewright test --grep "sign in"           # 通过名称运行单个测试
npx mobilewright test --reporter list            # 逐行输出结果,适用于CI/代理
npx mobilewright test --reporter json            # 生成机器可读的JSON输出

Available Fixtures

可用Fixture

FixtureScopeDescription
screen
testThe device screen — use this for all element interactions
device
workerDevice connection — use for app lifecycle, orientation, URLs
bundleId
testThe configured bundle ID string
Fixture作用域描述
screen
test设备屏幕——所有元素交互均使用此对象
device
worker设备连接——用于应用生命周期、方向控制、URL跳转
bundleId
test已配置的Bundle ID字符串

Test Options via
test.use()

通过
test.use()
设置测试选项

typescript
test.use({
  bundleId: 'com.example.app',    // override app bundle ID
  platform: 'android',            // override platform
  deviceId: '5A5FCFCA-...',       // target specific device
  video: 'on',                    // record video ('on' | 'retain-on-failure')
});
typescript
test.use({
  bundleId: 'com.example.app',    // 覆盖应用Bundle ID
  platform: 'android',            // 覆盖目标平台
  deviceId: '5A5FCFCA-...',       // 针对指定设备
  video: 'on',                    // 录制视频('on' | 'retain-on-failure')
});

Automation Script Structure

自动化脚本结构

For standalone scripts that don't need a test runner:
typescript
import { ios, android } from 'mobilewright';

const device = await ios.launch({ bundleId: 'com.example.myapp' });
const { screen } = device;

// Do your work
const tree = await screen.viewTree();
const screenshot = await screen.screenshot();
await screen.getByRole('button', { name: 'Start' }).tap();

// Always close when done
await device.close();
对于不需要测试运行器的独立脚本:
typescript
import { ios, android } from 'mobilewright';

const device = await ios.launch({ bundleId: 'com.example.myapp' });
const { screen } = device;

// 执行你的任务
const tree = await screen.viewTree();
const screenshot = await screen.screenshot();
await screen.getByRole('button', { name: 'Start' }).tap();

// 完成后务必关闭连接
await device.close();

Launch Options

启动选项

typescript
// Auto-discover first booted simulator
const device = await ios.launch();

// Launch a specific app
const device = await ios.launch({ bundleId: 'com.example.app' });

// Match device by name (regex)
const device = await ios.launch({ deviceName: 'My.*iPhone' });

// Target exact device
const device = await ios.launch({ deviceId: '5A5FCFCA-...' });

// List available devices
const devices = await ios.devices();
const devices = await android.devices();
typescript
// 自动发现首个已启动的模拟器
const device = await ios.launch();

// 启动指定应用
const device = await ios.launch({ bundleId: 'com.example.app' });

// 通过名称匹配设备(正则表达式)
const device = await ios.launch({ deviceName: 'My.*iPhone' });

// 针对指定设备
const device = await ios.launch({ deviceId: '5A5FCFCA-...' });

// 列出可用设备
const devices = await ios.devices();
const devices = await android.devices();

Development Verification

开发验证

When developing a mobile app, use mobilewright to verify your UI implementation:
typescript
import { ios } from 'mobilewright';

const device = await ios.launch({ bundleId: 'com.example.myapp' });
const { screen } = device;

// Take a screenshot to verify the UI looks correct
const screenshot = await screen.screenshot();

// Dump the view tree to verify elements exist and have correct properties
const tree = await screen.viewTree();

// Verify specific elements are present and have expected content
const title = await screen.getByRole('text', { name: 'Welcome' }).getText();
const isButtonEnabled = await screen.getByTestId('submit-btn').isEnabled();

await device.close();
开发移动应用时,使用mobilewright验证UI实现:
typescript
import { ios } from 'mobilewright';

const device = await ios.launch({ bundleId: 'com.example.myapp' });
const { screen } = device;

// 截取截图以验证UI显示正确
const screenshot = await screen.screenshot();

// 导出视图树以验证元素存在且属性正确
const tree = await screen.viewTree();

// 验证特定元素存在且内容符合预期
const title = await screen.getByRole('text', { name: 'Welcome' }).getText();
const isButtonEnabled = await screen.getByTestId('submit-btn').isEnabled();

await device.close();

API Quick Reference

API快速参考

Screen — Locator Factories

Screen — 定位器工厂

typescript
screen.getByLabel('Email')                          // accessibility label
screen.getByTestId('login-button')                  // accessibility identifier
screen.getByText('Welcome')                         // exact text match
screen.getByText(/welcome/i)                        // regex match
screen.getByText('welcome', { exact: false })       // substring match
screen.getByType('TextField')                       // native element type
screen.getByRole('button', { name: 'Sign In' })     // semantic role + name
typescript
screen.getByLabel('Email')                          // 可访问性标签
screen.getByTestId('login-button')                  // 可访问性标识符
screen.getByText('Welcome')                         // 精确文本匹配
screen.getByText(/welcome/i)                        // 正则表达式匹配
screen.getByText('welcome', { exact: false })       // 子串匹配
screen.getByType('TextField')                       // 原生元素类型
screen.getByRole('button', { name: 'Sign In' })     // 语义化角色+名称

Screen — Direct Actions

Screen — 直接操作

typescript
await screen.screenshot()                            // PNG buffer
await screen.screenshot({ format: 'jpeg', quality: 80 })
await screen.swipe('up')                             // scroll down
await screen.swipe('down', { distance: 300, duration: 500 })
await screen.pressButton('HOME')
await screen.goBack()                                // press BACK (Android only, no-op on iOS)
await screen.viewTree()                              // full view hierarchy as JSON
typescript
await screen.screenshot()                            // PNG格式缓冲区
await screen.screenshot({ format: 'jpeg', quality: 80 })
await screen.swipe('up')                             // 向下滚动
await screen.swipe('down', { distance: 300, duration: 500 })
await screen.pressButton('HOME')
await screen.goBack()                                // 按下返回键(仅Android可用,iOS无操作)
await screen.viewTree()                              // 以JSON格式返回完整视图层级

Locator — Actions

Locator — 操作

All actions auto-wait for the element to be visible, enabled, and stable.
typescript
await locator.tap()
await locator.doubleTap()
await locator.longPress({ duration: 1000 })
await locator.fill('hello@example.com')              // tap to focus + type
所有操作会自动等待元素变为可见、可用且稳定。
typescript
await locator.tap()
await locator.doubleTap()
await locator.longPress({ duration: 1000 })
await locator.fill('hello@example.com')              // 点击聚焦并输入文本

Locator — Queries

Locator — 查询

typescript
await locator.getText()       // visible text / label
await locator.getValue()      // input value
await locator.isVisible()     // boolean
await locator.isEnabled()     // boolean
await locator.isSelected()    // boolean
await locator.isFocused()     // boolean
await locator.isChecked()     // boolean
typescript
await locator.getText()       // 可见文本/标签
await locator.getValue()      // 输入框值
await locator.isVisible()     // 布尔值
await locator.isEnabled()     // 布尔值
await locator.isSelected()    // 布尔值
await locator.isFocused()     // 布尔值
await locator.isChecked()     // 布尔值

Locator — Collection

Locator — 集合操作

Work with multiple matching elements:
typescript
await locator.count()         // number of matching elements
await locator.all()           // array of Locator, one per match
locator.first()               // first matching element
locator.last()                // last matching element
locator.nth(2)                // element at index (0-based, negative counts from end)
Example — tap the third item in a list:
typescript
await screen.getByType('Cell').nth(2).tap();
Example — verify the number of search results:
typescript
const count = await screen.getByType('Cell').count();
expect(count).toBe(5);
处理多个匹配元素:
typescript
await locator.count()         // 匹配元素数量
await locator.all()           // Locator数组,每个元素对应一个匹配项
locator.first()               // 第一个匹配元素
locator.last()                // 最后一个匹配元素
locator.nth(2)                // 指定索引的元素(从0开始,负数从末尾计数)
示例——点击列表中的第三个项:
typescript
await screen.getByType('Cell').nth(2).tap();
示例——验证搜索结果数量:
typescript
const count = await screen.getByType('Cell').count();
expect(count).toBe(5);

Locator — Chaining

Locator — 链式调用

Scope queries within a parent element's bounds:
typescript
const row = screen.getByType('Cell');
await row.getByRole('button', { name: 'Delete' }).tap();
await row.getByPlaceholder('Enter name').fill('Arthur');
Note:
getByPlaceholder
is only available on Locator (chained), not directly on Screen.
在父元素范围内查询:
typescript
const row = screen.getByType('Cell');
await row.getByRole('button', { name: 'Delete' }).tap();
await row.getByPlaceholder('Enter name').fill('Arthur');
注意:
getByPlaceholder
仅在链式调用的Locator上可用,无法直接在Screen上使用。

Locator — Waiting

Locator — 等待

typescript
await locator.waitFor({ state: 'visible' })
await locator.waitFor({ state: 'hidden' })
await locator.waitFor({ state: 'enabled' })
await locator.waitFor({ state: 'disabled', timeout: 10_000 })
typescript
await locator.waitFor({ state: 'visible' })
await locator.waitFor({ state: 'hidden' })
await locator.waitFor({ state: 'enabled' })
await locator.waitFor({ state: 'disabled', timeout: 10_000 })

Assertions

断言

All assertions auto-retry until satisfied or timeout. Use
.not
for negation.
typescript
await expect(locator).toBeVisible();
await expect(locator).not.toBeVisible();
await expect(locator).toBeEnabled();
await expect(locator).toBeDisabled();
await expect(locator).toHaveText('Welcome back!');
await expect(locator).toHaveText(/welcome/i);
await expect(locator).toContainText('back');
await expect(locator).toHaveValue('user@example.com');
await expect(locator).toBeHidden();
await expect(locator).toBeChecked();
await expect(locator).toBeSelected();
await expect(locator).toBeFocused();
await expect(locator).toBeVisible({ timeout: 10_000 });
所有断言会自动重试直到满足条件或超时。使用
.not
进行否定断言。
typescript
await expect(locator).toBeVisible();
await expect(locator).not.toBeVisible();
await expect(locator).toBeEnabled();
await expect(locator).toBeDisabled();
await expect(locator).toHaveText('Welcome back!');
await expect(locator).toHaveText(/welcome/i);
await expect(locator).toContainText('back');
await expect(locator).toHaveValue('user@example.com');
await expect(locator).toBeHidden();
await expect(locator).toBeChecked();
await expect(locator).toBeSelected();
await expect(locator).toBeFocused();
await expect(locator).toBeVisible({ timeout: 10_000 });

Device

Device

typescript
// App lifecycle
await device.launchApp('com.example.app');
await device.launchApp('com.example.app', { locale: 'fr_FR' });
await device.terminateApp('com.example.app');
await device.installApp('/path/to/app.ipa');
await device.uninstallApp('com.example.app');
const apps = await device.listApps();
const foreground = await device.getForegroundApp();

// Navigation / deep links
await device.goto('myapp://settings');
await device.openUrl('https://example.com');

// Orientation
await device.setOrientation('landscape');
const orientation = await device.getOrientation();

// Recording
await device.startRecording({ output: 'recording.mp4' });
await device.stopRecording();

// Cleanup
await device.close();
typescript
// 应用生命周期
await device.launchApp('com.example.app');
await device.launchApp('com.example.app', { locale: 'fr_FR' });
await device.terminateApp('com.example.app');
await device.installApp('/path/to/app.ipa');
await device.uninstallApp('com.example.app');
const apps = await device.listApps();
const foreground = await device.getForegroundApp();

// 导航/深度链接
await device.goto('myapp://settings');
await device.openUrl('https://example.com');

// 屏幕方向
await device.setOrientation('landscape');
const orientation = await device.getOrientation();

// 录制
await device.startRecording({ output: 'recording.mp4' });
await device.stopRecording();

// 清理
await device.close();

Role Mapping

角色映射

getByRole
maps semantic roles to platform-specific types:
RoleiOSAndroid
button
Button, ImageButtonButton, ImageButton, ReactViewGroup*
textfield
TextField, SecureTextField, SearchFieldEditText, ReactEditText
text
StaticTextTextView, Text, ReactTextView
image
ImageImageView, ReactImageView
switch
SwitchSwitch, Toggle
checkbox
--Checkbox
slider
SliderSeekBar
list
Table, CollectionView, ScrollViewListView, RecyclerView, ReactScrollView
header
NavigationBarToolbar
link
LinkLink
* ReactViewGroup matches
button
only when
clickable="true"
or
accessible="true"
.
Falls back to direct type matching if no mapping exists.
getByRole
将语义化角色映射到平台特定的元素类型:
角色iOSAndroid
button
Button, ImageButtonButton, ImageButton, ReactViewGroup*
textfield
TextField, SecureTextField, SearchFieldEditText, ReactEditText
text
StaticTextTextView, Text, ReactTextView
image
ImageImageView, ReactImageView
switch
SwitchSwitch, Toggle
checkbox
--Checkbox
slider
SliderSeekBar
list
Table, CollectionView, ScrollViewListView, RecyclerView, ReactScrollView
header
NavigationBarToolbar
link
LinkLink
* 仅当
clickable="true"
或
accessible="true"
时,ReactViewGroup才会匹配
button
角色。
如果没有对应的映射,则回退到直接匹配元素类型。

Common Patterns

常见模式

Fresh App State Per Test

每个测试使用全新应用状态

typescript
test.beforeEach(async ({ device, bundleId }) => {
  await device.terminateApp(bundleId).catch(() => {});
  await device.launchApp(bundleId);
});
typescript
test.beforeEach(async ({ device, bundleId }) => {
  await device.terminateApp(bundleId).catch(() => {});
  await device.launchApp(bundleId);
});

Screenshot on Failure (automatic)

测试失败时自动截图

The
@mobilewright/test
fixture automatically captures a screenshot when a test fails. For manual screenshots:
typescript
test.afterEach(async ({ screen }, testInfo) => {
  const screenshot = await screen.screenshot();
  await testInfo.attach('screenshot', { body: screenshot, contentType: 'image/png' });
});
@mobilewright/test
fixture会在测试失败时自动截取截图。如需手动截图:
typescript
test.afterEach(async ({ screen }, testInfo) => {
  const screenshot = await screen.screenshot();
  await testInfo.attach('screenshot', { body: screenshot, contentType: 'image/png' });
});

Scrolling to Find Off-Screen Elements

滚动查找屏幕外元素

typescript
await screen.swipe('up');  // scroll down to reveal content below
await expect(screen.getByText('Footer Content')).toBeVisible();
typescript
await screen.swipe('up');  // 向下滚动以显示下方内容
await expect(screen.getByText('Footer Content')).toBeVisible();

Extracting Helpers for Repeated Flows

提取重复流程的辅助函数

typescript
async function login(screen: any, email: string, password: string) {
  await screen.getByLabel('Email').fill(email);
  await screen.getByLabel('Password').fill(password);
  await screen.getByRole('button', { name: 'Sign In' }).tap();
}

test('logged in user sees dashboard', async ({ screen }) => {
  await login(screen, 'user@test.com', 'pass123');
  await expect(screen.getByText('Dashboard')).toBeVisible();
});
typescript
async function login(screen: any, email: string, password: string) {
  await screen.getByLabel('Email').fill(email);
  await screen.getByLabel('Password').fill(password);
  await screen.getByRole('button', { name: 'Sign In' }).tap();
}

test('登录用户可查看仪表盘', async ({ screen }) => {
  await login(screen, 'user@test.com', 'pass123');
  await expect(screen.getByText('Dashboard')).toBeVisible();
});

Rules

规则

Always Do

务必遵循

  • Inspect the UI tree first. Run
    screen.viewTree()
    before writing locators. Never guess.
  • Use retry assertions.
    await expect(locator).toBeVisible()
    retries automatically.
    await locator.isVisible()
    does not.
  • Use locators, not coordinates. Locators are resilient to layout changes.
  • Relaunch the app between tests.
    terminateApp
    +
    launchApp
    ensures clean state.
  • Use
    { exact: false }
    for flexible text matching
    when exact wording may vary.
  • Extract repeated navigation into helper functions to keep tests readable.
  • 先检查UI树。编写定位器前运行
    screen.viewTree()
    。绝不猜测元素信息。
  • 使用重试断言。
    await expect(locator).toBeVisible()
    会自动重试,而
    await locator.isVisible()
    不会。
  • 使用定位器,而非坐标。定位器能适应布局变化。
  • 测试间重启应用。
    terminateApp
    +
    launchApp
    确保测试状态干净。
  • 使用
    { exact: false }
    进行灵活文本匹配
    ,当文案可能变化时。
  • 将重复导航逻辑提取为辅助函数,保持测试可读性。

Never Do

绝对禁止

  • Never use
    screen.tap(x, y)
    for element interaction.
    Raw coordinates break across devices, screen sizes, and OS versions. Always find the element with a locator.
  • Never hardcode timeouts. Don't
    await sleep(2000)
    hoping an element appears. Use auto-waiting actions and retry assertions — they handle timing automatically.
  • Never use
    waitFor
    before an action.
    Actions like
    tap()
    and
    fill()
    already auto-wait. Writing
    await locator.waitFor({ state: 'visible' }); await locator.tap();
    is redundant — just
    await locator.tap()
    .
  • Never use
    isVisible()
    for test assertions.
    It returns a boolean at one instant. Use
    expect(locator).toBeVisible()
    which retries until the assertion is satisfied.
  • Never screenshot and parse images to find elements. Use
    screen.viewTree()
    to inspect the accessibility tree programmatically.
  • Never hardcode platform-specific types when a role exists. Use
    getByRole('button')
    instead of
    getByType('UIButton')
    .
  • 绝不使用
    screen.tap(x, y)
    进行元素交互
    。原始坐标在不同设备、屏幕尺寸和操作系统版本上会失效。务必使用定位器查找元素。
  • 绝不硬编码超时时间。不要使用
    await sleep(2000)
    等待元素出现。使用自动等待的操作和重试断言——它们会自动处理时序问题。
  • 绝不在操作前使用
    waitFor
    。
    tap()
    和
    fill()
    等操作已经包含自动等待。编写
    await locator.waitFor({ state: 'visible' }); await locator.tap();
    是冗余的——直接使用
    await locator.tap()
    即可。
  • 绝不使用
    isVisible()
    进行测试断言
    。它仅返回某一时刻的布尔值。使用
    expect(locator).toBeVisible()
    ,它会重试直到断言满足。
  • 绝不通过截图解析图像查找元素。使用
    screen.viewTree()
    以编程方式检查可访问性树。
  • 当存在角色选项时,绝不硬编码平台特定元素类型。使用
    getByRole('button')
    而非
    getByType('UIButton')
    。