Skip to content
Merged
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
57 changes: 57 additions & 0 deletions .github/ISSUE_TEMPLATE/bug_report.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
---
name: Bug report
about: Create a report to help us fix the issue
title: ''
labels: ''
assignees: ''
---

<!--
## Have you read the "Debugging SDKs" doc yet?

There are common setup gotchas that happen with Percy's SDKs, it would be worth reading
the debugging document, which might already answer your question:
https://www.browserstack.com/docs/percy/integrate/percy-sdk-workflow#debugging-sdks

## Reach out to Percy support instead?

If you’re having issues that _aren’t SDK bugs_, it would be best for you to
reach out to support instead: support@percy.io or
https://www.browserstack.com/contact#technical-support
-->

## The problem

Briefly describe the issue you are experiencing (or the feature you want to see
added to Percy). Tell us what you were trying to do and what happened
instead. Remember, this is _not_ a place to ask questions. For that, go to
https://github.com/percy/cli/discussions/new

## Environment

- Node version:
- `@percy/cli` version:
- `@percy/detox` version:
- Detox version:
- React Native version:
- Platform (Android emulator, iOS simulator, real device):
- OS version:
- If needed, a build or snapshot ID:

## Details

If necessary, describe the problem you have been experiencing in more detail.

## Debug logs

If you are reporting a bug, _always_ include logs! [Give the "Debugging SDKs"
document a quick read for how to gather logs](https://www.browserstack.com/docs/percy/integrate/percy-sdk-workflow#debugging-sdks)

Please do not trim or edit these logs, often times there are hints in the full
logs that help debug what is going on.

## Code to reproduce issue

Given the nature of testing/environment bugs, it’s best to try and isolate the
issue in a reproducible repo. This will make it much easier for us to diagnose
and fix.
4 changes: 2 additions & 2 deletions .github/workflows/Semgrep.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,15 +20,15 @@ jobs:
name: semgrep/ci
runs-on: ubuntu-latest
container:
image: returntocorp/semgrep
image: semgrep/semgrep
if: (github.actor != 'dependabot[bot]')
steps:
- uses: actions/checkout@v4
- run: semgrep ci --sarif --output=semgrep.sarif
env:
SEMGREP_RULES: p/default
- name: Upload SARIF file for GitHub Advanced Security Dashboard
uses: github/codeql-action/upload-sarif@v3
uses: github/codeql-action/upload-sarif@v4
with:
sarif_file: semgrep.sarif
if: always()
2 changes: 1 addition & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ jobs:
registry-url: 'https://registry.npmjs.org'
cache: 'npm'
- run: npm ci
- run: npm test
- run: npm run test:coverage
- run: npm publish --access public
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
4 changes: 2 additions & 2 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ jobs:
strategy:
matrix:
os: [ubuntu-latest]
node: [18, 20]
node: [20, 22]
runs-on: ${{ matrix.os }}
steps:
- uses: actions-ecosystem/action-regex-match@v2
Expand Down Expand Up @@ -48,4 +48,4 @@ jobs:
cd "$WORKSPACE"
npm link @percy/cli
npx percy --version
- run: npm test
- run: npm run test:coverage
16 changes: 16 additions & 0 deletions .github/workflows/typecheck.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
name: Typecheck
on: push
permissions:
contents: read
jobs:
typecheck:
name: Typecheck
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: 'npm'
- run: npm ci
- run: npm run test:types
Comment thread
github-advanced-security[bot] marked this conversation as resolved.
Fixed
1 change: 1 addition & 0 deletions .nvmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
20
6 changes: 5 additions & 1 deletion .nycrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,9 @@
"include": ["index.js", "percy/**/*.js"],
"exclude": ["test/**", "**/*.test.js"],
"reporter": ["text", "html", "lcov"],
"check-coverage": false
"check-coverage": true,
"branches": 100,
"lines": 100,
"functions": 100,
"statements": 100
}
2 changes: 1 addition & 1 deletion CODEOWNERS
Original file line number Diff line number Diff line change
@@ -1 +1 @@
* @percy/percy-sdk-maintainers
* @percy/percy-product-reviewers
133 changes: 87 additions & 46 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,115 +1,156 @@
# @percy/detox

[![npm](https://img.shields.io/npm/v/@percy/detox.svg)](https://www.npmjs.com/package/@percy/detox)
[![Test](https://github.com/percy/percy-detox/actions/workflows/test.yml/badge.svg)](https://github.com/percy/percy-detox/actions/workflows/test.yml)

Percy visual testing SDK for [Detox](https://wix.github.io/Detox/) — React Native E2E.
[Percy](https://percy.io) visual testing SDK for [Detox](https://wix.github.io/Detox/) — the gray-box E2E framework for React Native apps.

## Install
## Quick start

```sh
npm install --save-dev @percy/cli @percy/detox
```

Requires **Node ≥ 18** and **Detox ≥ 20.32.0**.

## Usage
Add a screenshot call to any Detox test:

```js
const { device, element, by } = require('detox');
const percyScreenshot = require('@percy/detox');

describe('Home flow', () => {
it('renders home', async () => {
await percyScreenshot(device, 'Home');
});

it('captures hero', async () => {
await percyScreenshot(element(by.id('hero')), 'Hero');
describe('Login flow', () => {
it('renders the welcome screen', async () => {
await expect(element(by.id('welcome'))).toBeVisible();
await percyScreenshot(device, 'Welcome screen');
});
});
```

Run tests under Percy CLI's `app:exec`:
Run under Percy CLI's `app:exec` with an **App-type** project token (`app_...`):

```sh
export PERCY_TOKEN=<your project token>
percy app:exec -- detox test -c ios.sim.debug
export PERCY_TOKEN=<app_...>
npx percy app:exec -- npx detox test -c android.emu.debug
```

A working example app is available at [percy/example-percy-detox](https://github.com/percy/example-percy-detox).

## Requirements

- Node ≥ 18
- Detox ≥ 20.32.0
- An App-type Percy project (token starts with `app_`)

## Supported execution targets

| Target | Status |
|---|---|
| Android — local emulator | ✅ Fully supported |
| Android — local USB-attached real device | ✅ Same local driver as emulator |
| iOS — local simulator | ✅ Supported |
| iOS — local real device | ⚠️ Works in principle; uncommon |

## API

```js
await percyScreenshot(deviceOrElement, name, options)
await percyScreenshot(deviceOrElement, name, options?)
```

- **`deviceOrElement`** — the Detox `device` object, or an element handle from `element(by.id(...))`.
- **`name`** — unique snapshot name.
- **`options`** (all optional):
- **`name`** — unique snapshot name within the build.
- **`options`** *(all optional)*:

| Option | Type | Notes |
| Option | Type | Description |
|---|---|---|
| `fullscreen` | `boolean` | Crop status/nav bars off the comparison. |
| `fullPage` | `boolean` | Accepted for API parity; produces a single-tile capture (warns once). |
| `fullscreen` | `boolean` | Treat the screenshot as full-screen (no chrome cropping). |
| `fullPage` | `boolean` | Accepted for API parity; produces a single-tile capture (warns once per process). |
| `deviceName` | `string` | Override auto-detected device name. |
| `osVersion` | `string` | Override auto-detected OS version. |
| `orientation` | `'portrait' \| 'landscape'` | Override; defaults to `'portrait'`. |
| `statusBarHeight` | `number` | Pixel override; default `0`. |
| `navigationBarHeight` | `number` | Pixel override; default `0`. |
| `ignoreRegionIds` | `string[]` | Mask elements found via `by.id(testID)`. |
| `ignoreRegionElements` | `DetoxElement[]` | Mask pre-matched element handles. |
| `customIgnoreRegions` | `{top,bottom,left,right}[]` | Mask rectangles (coordinates in points). |
| `ignoreRegionIds` | `string[]` | Mask elements located via `by.id(testID)`. |
| `ignoreRegionElements` | `DetoxElement[]` | Mask pre-matched Detox element handles. |
| `customIgnoreRegions` | `{top,bottom,left,right}[]` | Mask rectangles in **point** coordinates. |
| `considerRegionIds` | `string[]` | Restrict diff to these elements. |
| `considerRegionElements` | `DetoxElement[]` | Restrict diff to these handles. |
| `customConsiderRegions` | `{top,bottom,left,right}[]` | Restrict diff to these rects (points). |
| `testCase` | `string` | Group snapshot in the Percy review UI. |
| `labels` | `string` | Comma-separated label chips. |
| `labels` | `string` | Comma-separated label chips for the snapshot. |
| `sync` | `boolean` | Block until the Percy build finalizes. |
| `getSessionId` | `() => string \| null \| Promise<string \| null>` | Resolve BrowserStack session ID for the POA debug link (see below). |
| `getBuildId` | `() => string \| null \| Promise<string \| null>` | Resolve BrowserStack build ID. |

Coordinate space: **all region options accept point coordinates**. The SDK multiplies by the device scale factor. Do not pre-multiply.
**Coordinate space:** all region options accept **point** coordinates. The SDK multiplies by the device scale factor automatically. Do not pre-multiply.

## Examples

## BrowserStack App Automate (Detox Android)
### Element-level capture

```js
const hero = element(by.id('hero'));
await expect(hero).toBeVisible();
await percyScreenshot(hero, 'Hero element');
```

BrowserStack runs Detox Android tests on real devices. Percy shows a debug-URL link to the BrowserStack session when both the session ID and build ID are available. Because BrowserStack does **not** auto-inject these into the test process, pass them via a callback — typically calling the BrowserStack REST API with your credentials:
### Ignore dynamic regions

```js
const fetch = require('node-fetch');

await percyScreenshot(device, 'Home', {
getSessionId: async () => {
// Your pipeline resolves this — for example, by calling
// https://api-cloud.browserstack.com/app-automate/detox/v2/android/sessions/{id}.json
return process.env.MY_BS_SESSION_ID || null;
},
getBuildId: async () => process.env.MY_BS_BUILD_ID || null
await percyScreenshot(device, 'Dashboard', {
ignoreRegionIds: ['live-clock', 'unread-count'],
customIgnoreRegions: [{ top: 0, bottom: 100, left: 0, right: 414 }]
});
```

Fallback: `BROWSERSTACK_SESSION_ID` and `BROWSERSTACK_BUILD_ID` environment variables are read if no callback is provided. Returning explicit `null` from the callback suppresses the env fallback.
### Group snapshots by test case

iOS POA is **not supported** — Detox does not run on BrowserStack real iOS devices.
```js
await percyScreenshot(device, 'Login screen', { testCase: 'auth-suite' });
```

## Configuration

| Environment variable | Effect |
|---|---|
| `PERCY_TOKEN` | Required. App-type Percy project token (starts with `app_`). |
| `PERCY_IGNORE_ERRORS` | If `'true'`, screenshot failures are logged and swallowed instead of failing the test. |
| `PERCY_LOGLEVEL` | `silent`, `info`, `debug`. Defaults to `info`. |
| `PERCY_METRICS` | If `'true'`, collect per-screenshot timing telemetry. |

## Debugging

If you run into issues, the first place to look is the [Debugging SDKs](https://www.browserstack.com/docs/percy/integrate/percy-sdk-workflow#debugging-sdks) guide — it covers log levels, common setup gotchas, and how to gather logs for a bug report.

To enable verbose output:

```sh
PERCY_LOGLEVEL=debug npx percy app:exec -- npx detox test -c android.emu.debug
```

## Migration from `@percy/appium-app`

| `@percy/appium-app` option | `@percy/detox` equivalent |
|---|---|
| `ignoreRegionXpaths` | **Rejected** — Detox has no XPath. Use `ignoreRegionIds`. |
| `ignoreRegionAccessibilityIds` | `ignoreRegionIds` (collapsed). |
| `ignoreRegionAccessibilityIds` | `ignoreRegionIds`. |
| `ignoreRegionAppiumElements` | `ignoreRegionElements`. |
| `considerRegionXpaths` | **Rejected** — use `considerRegionIds`. |
| `considerRegionAccessibilityIds` | `considerRegionIds`. |
| `considerRegionAppiumElements` | `considerRegionElements`. |
| `scrollableXpath`, `scrollableId`, `screenLengths`, `topScrollviewOffset`, `bottomScrollviewOffset`, `androidScrollAreaPercentage`, `scrollSpeed`, `iosOptimizedFullpage` | **Rejected** — fullPage scroll-and-stitch is not supported for Detox. |
| `scrollableXpath`, `scrollableId`, `screenLengths`, `topScrollviewOffset`, `bottomScrollviewOffset`, `androidScrollAreaPercentage`, `scrollSpeed`, `iosOptimizedFullpage` | **Rejected** — fullPage scroll-and-stitch is not supported on Detox. |
| `thTestCaseExecutionId` | **Rejected** — TestHub is not supported. |

Passing a rejected key throws `TypeError` with a migration hint — nothing is silently ignored.

## Known limitations

- `element.takeScreenshot` misses TextureView, GLSurfaceView, Skia canvases, and other hardware-accelerated content on Android ([wix/Detox#4489](https://github.com/wix/Detox/issues/4489)). Use `device.takeScreenshot` for surfaces backed by these views.
- `element.takeScreenshot` misses TextureView, GLSurfaceView, Skia canvases, and other hardware-accelerated content on Android ([wix/Detox#4489](https://github.com/wix/Detox/issues/4489)). Use `device.takeScreenshot` for views backed by these surfaces.
- iOS: `atIndex(n).getAttributes()` is silently ignored ([wix/Detox#4633](https://github.com/wix/Detox/issues/4633)). When multi-match occurs, `@percy/detox` uses the first result and logs a warning.
- fullPage is accepted but produces a single-tile capture — Detox has no server-side stitching channel (unlike BrowserStack's Appium executor).
- iOS scale factor is inferred from PNG width: `>=1080 → 3x`, otherwise `2x`. iPad retina (2x at 1536+) may be misclassified; override with `deviceName` + a manual `scaleFactor` override (planned).
- `fullPage` is accepted but produces a single-tile capture — Detox has no server-side stitching channel (unlike BrowserStack's Appium executor).
- iOS scale factor is inferred from PNG width: `>=1080 → 3x`, otherwise `2x`. iPad retina at 2x with width 1536+ may be misclassified; override with explicit options or open an issue.

## Resources

- [Example app + tests](https://github.com/percy/example-percy-detox)
- [Percy App Percy docs](https://www.browserstack.com/docs/percy/integrate/overview)
- [Detox docs](https://wix.github.io/Detox/)

## License

Expand Down
8 changes: 6 additions & 2 deletions index.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
const utils = require('@percy/sdk-utils');
const { GenericProvider } = require('./percy/providers/genericProvider');
const { GenericProvider, CLIENT_INFO, ENV_INFO } = require('./percy/providers/genericProvider');
const { classifyArg, rejectUnknownOptions } = require('./percy/util/validations');
const postFailedEvents = require('./percy/util/postFailedEvents');
const { TimeIt } = require('./percy/util/timing');
const log = require('./percy/util/log');

const percyScreenshot = async function percyScreenshot(deviceOrElement, name, options = {}) {
Expand Down Expand Up @@ -30,7 +31,7 @@ const percyScreenshot = async function percyScreenshot(deviceOrElement, name, op
const provider = new GenericProvider(deviceOrElement, { argKind });
let response;
try {
response = await provider.screenshot(name, options);
response = await TimeIt.run('percyScreenshot', () => provider.screenshot(name, options));
} catch (e) {
log.error(`[${name}] failed to take screenshot: ${e.message}`);
if (e.stack) log.debug(e.stack);
Expand Down Expand Up @@ -60,4 +61,7 @@ percyScreenshot.isDetoxDevice = function isDetoxDevice(arg) {
}
};

percyScreenshot.CLIENT_INFO = CLIENT_INFO;
percyScreenshot.ENV_INFO = ENV_INFO;

module.exports = percyScreenshot;
Loading
Loading