Loading...
Loading...
Compare original and translation side by side
PRODUCT_BUNDLE_IDENTIFIER*.pbxprojbundleIdentifierapp.jsonapp.config.jsapplicationIdbuild.gradlebuild.gradle.ktspackageAndroidManifest.xmlapp.jsonexpo.ios.bundleIdentifierexpo.android.packagemobilewright.config.tsbundleIdmobilewright.config.ts*.pbxprojPRODUCT_BUNDLE_IDENTIFIERapp.jsonapp.config.jsbundleIdentifierbuild.gradlebuild.gradle.ktsapplicationIdAndroidManifest.xmlpackageapp.jsonexpo.ios.bundleIdentifierexpo.android.packagebundleIdmobilewright.config.tsmobilewright.config.tsnpx mobilewright initmobilewright.config.tsexample.test.tsnpx mobilewright doctornpx mobilewright initmobilewright.config.tsexample.test.tsnpx mobilewright doctornpx 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 devicenpx 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> # 针对指定设备截图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)
});import { defineConfig } from 'mobilewright';
export default defineConfig({
platform: 'ios', // 'ios' 或 'android'
bundleId: 'com.example.myapp', // 应用Bundle ID
deviceName: 'iPhone 16', // 匹配设备名称的正则表达式(可选)
timeout: 10_000, // 全局超时时间(毫秒,可选)
});screen.viewTree()const tree = await screen.viewTree();
console.log(JSON.stringify(tree, null, 2));testIdlabeltypetextvaluescreen.viewTree()const tree = await screen.viewTree();
console.log(JSON.stringify(tree, null, 2));testIdlabeltypetextvalue| Priority | Locator | Why |
|---|---|---|
| 1 | | Most stable. Set by developers, won't change with copy or i18n. |
| 2 | | Semantic and cross-platform. Maps to native types on both iOS and Android. |
| 3 | | Accessibility label. Stable if accessibility is maintained. |
| 4 | | Good for text fields. Only available on Locator, not on Screen. |
| 5 | | Fragile. Changes with copy updates, i18n, or minor rewording. |
| 6 | | Platform-specific. Ties your code to iOS or Android element types. |
| 优先级 | 定位器 | 原因 |
|---|---|---|
| 1 | | 最稳定。由开发者设置,不会随文案更新或国际化更改而变化。 |
| 2 | | 语义化且跨平台。映射到iOS和Android的原生元素类型。 |
| 3 | | 可访问性标签。如果可访问性维护良好,则稳定性高。 |
| 4 | | 适用于文本字段。仅在Locator上可用,Screen不可用。 |
| 5 | | 脆弱。会随文案更新、国际化或细微措辞变化而失效。 |
| 6 | | 平台特定。将代码绑定到iOS或Android的元素类型。 |
@mobilewright/testimport { 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();
});
});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/testimport { 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();
});
});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输出| Fixture | Scope | Description |
|---|---|---|
| test | The device screen — use this for all element interactions |
| worker | Device connection — use for app lifecycle, orientation, URLs |
| test | The configured bundle ID string |
| Fixture | 作用域 | 描述 |
|---|---|---|
| test | 设备屏幕——所有元素交互均使用此对象 |
| worker | 设备连接——用于应用生命周期、方向控制、URL跳转 |
| test | 已配置的Bundle ID字符串 |
test.use()test.use()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')
});test.use({
bundleId: 'com.example.app', // 覆盖应用Bundle ID
platform: 'android', // 覆盖目标平台
deviceId: '5A5FCFCA-...', // 针对指定设备
video: 'on', // 录制视频('on' | 'retain-on-failure')
});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();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();// 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();// 自动发现首个已启动的模拟器
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();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();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();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 + namescreen.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' }) // 语义化角色+名称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 JSONawait 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格式返回完整视图层级await locator.tap()
await locator.doubleTap()
await locator.longPress({ duration: 1000 })
await locator.fill('hello@example.com') // tap to focus + typeawait locator.tap()
await locator.doubleTap()
await locator.longPress({ duration: 1000 })
await locator.fill('hello@example.com') // 点击聚焦并输入文本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() // booleanawait locator.getText() // 可见文本/标签
await locator.getValue() // 输入框值
await locator.isVisible() // 布尔值
await locator.isEnabled() // 布尔值
await locator.isSelected() // 布尔值
await locator.isFocused() // 布尔值
await locator.isChecked() // 布尔值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)await screen.getByType('Cell').nth(2).tap();const count = await screen.getByType('Cell').count();
expect(count).toBe(5);await locator.count() // 匹配元素数量
await locator.all() // Locator数组,每个元素对应一个匹配项
locator.first() // 第一个匹配元素
locator.last() // 最后一个匹配元素
locator.nth(2) // 指定索引的元素(从0开始,负数从末尾计数)await screen.getByType('Cell').nth(2).tap();const count = await screen.getByType('Cell').count();
expect(count).toBe(5);const row = screen.getByType('Cell');
await row.getByRole('button', { name: 'Delete' }).tap();
await row.getByPlaceholder('Enter name').fill('Arthur');getByPlaceholderconst row = screen.getByType('Cell');
await row.getByRole('button', { name: 'Delete' }).tap();
await row.getByPlaceholder('Enter name').fill('Arthur');getByPlaceholderawait locator.waitFor({ state: 'visible' })
await locator.waitFor({ state: 'hidden' })
await locator.waitFor({ state: 'enabled' })
await locator.waitFor({ state: 'disabled', timeout: 10_000 })await locator.waitFor({ state: 'visible' })
await locator.waitFor({ state: 'hidden' })
await locator.waitFor({ state: 'enabled' })
await locator.waitFor({ state: 'disabled', timeout: 10_000 }).notawait 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 });.notawait 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 });// 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();// 应用生命周期
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();getByRole| Role | iOS | Android |
|---|---|---|
| Button, ImageButton | Button, ImageButton, ReactViewGroup* |
| TextField, SecureTextField, SearchField | EditText, ReactEditText |
| StaticText | TextView, Text, ReactTextView |
| Image | ImageView, ReactImageView |
| Switch | Switch, Toggle |
| -- | Checkbox |
| Slider | SeekBar |
| Table, CollectionView, ScrollView | ListView, RecyclerView, ReactScrollView |
| NavigationBar | Toolbar |
| Link | Link |
buttonclickable="true"accessible="true"getByRole| 角色 | iOS | Android |
|---|---|---|
| Button, ImageButton | Button, ImageButton, ReactViewGroup* |
| TextField, SecureTextField, SearchField | EditText, ReactEditText |
| StaticText | TextView, Text, ReactTextView |
| Image | ImageView, ReactImageView |
| Switch | Switch, Toggle |
| -- | Checkbox |
| Slider | SeekBar |
| Table, CollectionView, ScrollView | ListView, RecyclerView, ReactScrollView |
| NavigationBar | Toolbar |
| Link | Link |
clickable="true"accessible="true"buttontest.beforeEach(async ({ device, bundleId }) => {
await device.terminateApp(bundleId).catch(() => {});
await device.launchApp(bundleId);
});test.beforeEach(async ({ device, bundleId }) => {
await device.terminateApp(bundleId).catch(() => {});
await device.launchApp(bundleId);
});@mobilewright/testtest.afterEach(async ({ screen }, testInfo) => {
const screenshot = await screen.screenshot();
await testInfo.attach('screenshot', { body: screenshot, contentType: 'image/png' });
});@mobilewright/testtest.afterEach(async ({ screen }, testInfo) => {
const screenshot = await screen.screenshot();
await testInfo.attach('screenshot', { body: screenshot, contentType: 'image/png' });
});await screen.swipe('up'); // scroll down to reveal content below
await expect(screen.getByText('Footer Content')).toBeVisible();await screen.swipe('up'); // 向下滚动以显示下方内容
await expect(screen.getByText('Footer Content')).toBeVisible();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();
});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();
});screen.viewTree()await expect(locator).toBeVisible()await locator.isVisible()terminateApplaunchApp{ exact: false }screen.viewTree()await expect(locator).toBeVisible()await locator.isVisible()terminateApplaunchApp{ exact: false }screen.tap(x, y)await sleep(2000)waitFortap()fill()await locator.waitFor({ state: 'visible' }); await locator.tap();await locator.tap()isVisible()expect(locator).toBeVisible()screen.viewTree()getByRole('button')getByType('UIButton')screen.tap(x, y)await sleep(2000)waitFortap()fill()await locator.waitFor({ state: 'visible' }); await locator.tap();await locator.tap()isVisible()expect(locator).toBeVisible()screen.viewTree()getByRole('button')getByType('UIButton')