Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -300,6 +300,8 @@ All options:
| `deviceId` | `string` | Explicit device UDID (optional) |
| `deviceName` | `RegExp` | RegExp to match device name (optional) |
| `timeout` | `number` | Global locator timeout in ms (optional) |
| `screenshot` | `'on' \| 'on-failure' \| 'off'` | Attach screenshots to the report (`'on-failure'` by default) |
| `viewTree` | `'on' \| 'on-failure' \| 'off'` | Attach accessibility tree JSON to the report (`'off'` by default) |
| `testDir` | `string` | Directory to search for test files (optional) |
| `testMatch` | `string \| RegExp \| Array` | Glob patterns for test files (optional) |
| `reporter` | `'list' \| 'html' \| 'json' \| 'junit' \| Array` | Reporter to use (optional) |
Expand Down Expand Up @@ -333,7 +335,7 @@ test('can sign in', async ({ device, screen, bundleId }) => {
});
```

The `device` fixture connects once per worker (reading from `mobilewright.config.ts`) and calls `device.close()` after all tests complete. The `screen` fixture provides `device.screen` to each test, with automatic screenshot-on-failure and optional video recording.
The `device` fixture connects once per worker (reading from `mobilewright.config.ts`) and calls `device.close()` after all tests complete. The `screen` fixture provides `device.screen` to each test, with configurable screenshot and view-tree attachments plus optional video recording.

## CLI

Expand Down
5 changes: 3 additions & 2 deletions docs/src/test/fixtures.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ test('shows welcome message', async ({ screen }) => {
});
```

The `screen` fixture is scoped to each test. It also handles video recording and captures a screenshot on test failure, attaching both to the test report.
The `screen` fixture is scoped to each test. It also handles video recording and can attach screenshots and accessibility view trees to the test report based on your configured modes.

### `device`

Expand Down Expand Up @@ -50,7 +50,8 @@ You can override the following settings per-test or per-project, in addition to
| `bundleId` | `string` | App bundle identifier |
| `installApps` | `string \| string[]` | App paths (APK/IPA) to install before launching |
| `autoAppLaunch` | `boolean` | Automatically launch the app after connecting. Default: `true` |
| `viewTree` | `'on-failure' \| 'off'` | Attach the accessibility view tree as JSON to the report when a test fails. Default: `'off'` |
| `screenshot` | `'on' \| 'on-failure' \| 'off'` | Attach screenshots to the report for every test (`'on'`) or only failed tests (`'on-failure'`). Default: `'on-failure'` |
| `viewTree` | `'on' \| 'on-failure' \| 'off'` | Attach the accessibility view tree as JSON for every test (`'on'`) or only failed tests (`'on-failure'`). Default: `'off'` |

```typescript
import { test } from '@mobilewright/test';
Expand Down
6 changes: 4 additions & 2 deletions packages/mobilewright/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,8 +96,10 @@ export interface MobilewrightConfig {
installApps?: string | string[];
/** Automatically launch the app after connecting. Default: true. */
autoAppLaunch?: boolean;
/** Attach the accessibility tree as JSON to the test report. 'on-failure' attaches on test failure, 'off' disables. Default: 'off'. */
viewTree?: 'on-failure' | 'off';
/** Attach test screenshots to the report. 'on' attaches for every test, 'on-failure' only for failed tests, 'off' disables. Default: 'on-failure'. */
screenshot?: 'on' | 'on-failure' | 'off';
/** Attach the accessibility tree as JSON to the test report. 'on' attaches for every test, 'on-failure' only for failed tests, 'off' disables. Default: 'off'. */
viewTree?: 'on' | 'on-failure' | 'off';
/** mobilecli server URL (use for remote servers). */
url?: string;
/** Path to mobilecli binary (if not on PATH). */
Expand Down
33 changes: 33 additions & 0 deletions packages/test/src/fixtures.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { test, expect } from '@playwright/test';
import { resolveArtifactMode, shouldAttachArtifact } from './fixtures.js';

test.describe('artifact mode helpers', () => {
test('resolveArtifactMode returns configured mode', () => {
expect(resolveArtifactMode('on', 'screenshot', 'on-failure')).toBe('on');
expect(resolveArtifactMode('on-failure', 'viewTree', 'off')).toBe('on-failure');
expect(resolveArtifactMode('off', 'viewTree', 'on')).toBe('off');
});

test('resolveArtifactMode falls back to default', () => {
expect(resolveArtifactMode(undefined, 'screenshot', 'on-failure')).toBe('on-failure');
expect(resolveArtifactMode(undefined, 'viewTree', 'off')).toBe('off');
});

test('resolveArtifactMode rejects invalid values', () => {
expect(() => resolveArtifactMode('always', 'screenshot', 'on-failure')).toThrow(
/Invalid screenshot value/,
);
expect(() => resolveArtifactMode(123, 'viewTree', 'off')).toThrow(
/Invalid viewTree value/,
);
});

test('shouldAttachArtifact uses mode and test outcome', () => {
expect(shouldAttachArtifact('on', false)).toBe(true);
expect(shouldAttachArtifact('on', true)).toBe(true);
expect(shouldAttachArtifact('on-failure', false)).toBe(false);
expect(shouldAttachArtifact('on-failure', true)).toBe(true);
expect(shouldAttachArtifact('off', false)).toBe(false);
expect(shouldAttachArtifact('off', true)).toBe(false);
});
});
68 changes: 47 additions & 21 deletions packages/test/src/fixtures.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,10 +50,31 @@ type MobilewrightTestFixtures = {
platform: 'ios' | 'android' | undefined;
deviceName: RegExp | undefined;
installApps: string | string[] | undefined;
viewTree: 'on-failure' | 'off';
screenshotMode: ArtifactMode;
viewTree: ArtifactMode;
device: Device;
};

type ArtifactMode = 'on' | 'on-failure' | 'off';

export function resolveArtifactMode(
rawValue: unknown,
optionName: 'screenshot' | 'viewTree',
defaultValue: ArtifactMode,
): ArtifactMode {
const value = rawValue ?? defaultValue;
if (value === 'on' || value === 'on-failure' || value === 'off') {
return value;
}
throw new Error(
`Invalid ${optionName} value: "${String(value)}". Must be "on", "on-failure", or "off".`,
);
}

export function shouldAttachArtifact(mode: ArtifactMode, failed: boolean): boolean {
return mode === 'on' || (mode === 'on-failure' && failed);
}

let cachedClient: DevicePoolClient | undefined;
function getClient(): DevicePoolClient {
if (!cachedClient) {
Expand All @@ -77,14 +98,14 @@ export const test = base.extend<MobilewrightTestFixtures>({
deviceName: [undefined, { option: true }],
installApps: [undefined, { option: true }],

screenshotMode: [async ({}, use, testInfo) => {
const config = await loadConfig(process.cwd(), testInfo.config.configFile);
await use(resolveArtifactMode(config.screenshot, 'screenshot', 'on-failure'));
}, { option: true }],

viewTree: [async ({}, use, testInfo) => {
const config = await loadConfig(process.cwd(), testInfo.config.configFile);
const value = config.viewTree ?? 'off';
if (value !== 'on-failure' && value !== 'off') {
throw new Error(`Invalid viewTree value: "${value}". Must be "on-failure" or "off".`);
}

await use(value);
await use(resolveArtifactMode(config.viewTree, 'viewTree', 'off'));
}, { option: true }],

device: async ({ platform, deviceName, bundleId, autoAppLaunch, installApps }, use, testInfo) => {
Expand Down Expand Up @@ -173,7 +194,7 @@ export const test = base.extend<MobilewrightTestFixtures>({
}
},

screen: async ({ device, video, viewTree }, use, testInfo) => {
screen: async ({ device, video, screenshotMode, viewTree }, use, testInfo) => {
const videoMode = typeof video === 'object' ? video.mode : video;
const shouldRecord = videoMode === 'on' || videoMode === 'retain-on-failure';
const videoPath = shouldRecord
Expand Down Expand Up @@ -209,23 +230,28 @@ export const test = base.extend<MobilewrightTestFixtures>({
}
}

if (testInfo.status !== testInfo.expectedStatus) {
const failed = testInfo.status !== testInfo.expectedStatus;

if (shouldAttachArtifact(screenshotMode, failed)) {
try {
const screenshot = await device.screen.screenshot();
await testInfo.attach('screenshot-on-failure', { body: screenshot, contentType: 'image/png' });
const screenshotBuffer = await device.screen.screenshot();
const attachmentName = failed ? 'screenshot-on-failure' : 'screenshot';
await testInfo.attach(attachmentName, { body: screenshotBuffer, contentType: 'image/png' });
} catch {
// device may be disconnected
}
if (viewTree === 'on-failure') {
try {
const tree = await device.screen.viewTree();
await testInfo.attach('view-tree-on-failure', {
body: Buffer.from(JSON.stringify(tree, null, 2)),
contentType: 'application/json',
});
} catch {
// device may be disconnected
}
}

if (shouldAttachArtifact(viewTree, failed)) {
try {
const tree = await device.screen.viewTree();
const attachmentName = failed ? 'view-tree-on-failure' : 'view-tree';
await testInfo.attach(attachmentName, {
body: Buffer.from(JSON.stringify(tree, null, 2)),
contentType: 'application/json',
});
} catch {
// device may be disconnected
}
}
},
Expand Down