diff --git a/README.md b/README.md index 99f14b7..f0da97b 100644 --- a/README.md +++ b/README.md @@ -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) | @@ -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 diff --git a/docs/src/test/fixtures.md b/docs/src/test/fixtures.md index e929c09..4f76f46 100644 --- a/docs/src/test/fixtures.md +++ b/docs/src/test/fixtures.md @@ -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` @@ -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'; diff --git a/packages/mobilewright/src/config.ts b/packages/mobilewright/src/config.ts index 3b0b930..e4f790b 100644 --- a/packages/mobilewright/src/config.ts +++ b/packages/mobilewright/src/config.ts @@ -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). */ diff --git a/packages/test/src/fixtures.test.ts b/packages/test/src/fixtures.test.ts new file mode 100644 index 0000000..ce21a43 --- /dev/null +++ b/packages/test/src/fixtures.test.ts @@ -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); + }); +}); diff --git a/packages/test/src/fixtures.ts b/packages/test/src/fixtures.ts index 1689398..ecfa5a2 100644 --- a/packages/test/src/fixtures.ts +++ b/packages/test/src/fixtures.ts @@ -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) { @@ -77,14 +98,14 @@ export const test = base.extend({ 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) => { @@ -173,7 +194,7 @@ export const test = base.extend({ } }, - 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 @@ -209,23 +230,28 @@ export const test = base.extend({ } } - 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 } } },