> ## Documentation Index
> Fetch the complete documentation index at: https://patch.codemagic.io/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Testing your app

> Run Jest tests for a React Native app that uses the Patch SDK, and drive it into OTA states such as "update ready" or "failed install".

The SDK resolves its native module when it is imported, through `TurboModuleRegistry.getEnforcing`. Under Jest there is no native binary, so any test file that imports the SDK, directly or through one of your components, fails before a test runs:

```text
TurboModuleRegistry.getEnforcing(...): 'NativeCodemagicPatch' could not be found.
```

The package ships a fake of that native module for Jest. The real SDK runs on top of it, so `sync()`, `checkForUpdate()` and the rest behave as they do in the app.

## Set up Jest

Keep the preset you already use, add the setup file, and add `@codemagic` to the allowlist your `transformIgnorePatterns` already has:

```js
// jest.config.js
module.exports = {
  preset: '@react-native/jest-preset', // or 'react-native' on older RN, or 'jest-expo'
  setupFiles: ['@codemagic/react-native-patch/jest-setup'],
  transformIgnorePatterns: [
    'node_modules/(?!((jest-)?react-native|@react-native(-community)?|@codemagic)/)',
  ],
};
```

Under Jest the package resolves to its TypeScript source, as React Native itself does, so it has to be transformed. The setup file installs the default scenario: the app runs the store binary and the server has no update. That is all a test needs if it only has to load the SDK.

## Drive the SDK into an OTA state

For another state, install a fake with a seed, then load a fresh SDK with `loadSdk()`. Each test starts with its own fake:

```ts

test('store binary, nothing installed', async () => {
  installPatchFake();
  const Patch = await loadSdk();
  expect(await Patch.getRunningBundleUpdateMetadata()).toBeNull();
});

test('running a release', async () => {
  installPatchFake({ running: { label: 'v4', releaseNotes: 'Fixes' } });
  const Patch = await loadSdk();
  expect(await Patch.getRunningBundleUpdateMetadata()).toMatchObject({ label: 'v4' });
});

test('first launch of a new release, before notifyAppReady()', async () => {
  installPatchFake({
    running: { label: 'v5' },
    runningUnconfirmed: true,
    previous: { label: 'v4' }, // what native would roll back to
  });
  const Patch = await loadSdk();
  await expect(Patch.notifyAppReady()).resolves.toBeUndefined();
});

test('the server offers the release that failed on this device', async () => {
  installPatchFake({
    running: { label: 'v4' },
    failedInstall: { label: 'v5' },
    server: { release: { label: 'v5' } },
  });
  const Patch = await loadSdk();
  const check = await Patch.checkForUpdate();
  expect(check.remotePackage?.previouslyFailed).toBe(true);
  expect(await Patch.sync()).toBe('up-to-date');
});
```

The seed also takes `binaryVersion`, `deploymentKey`, `isMandatory` on a package, and `server.latestBinaryVersion` for a store update. For the full shape, see [`client/src/fake/types.ts`](https://github.com/codemagic-ci-cd/codemagic-patch/blob/main/client/src/fake/types.ts).

### An update ready for the next launch

A device never cold-boots into "update ready", so there is no seed for it. Reach it the way the app does, by installing a server release:

```ts
test('an update is ready for the next launch', async () => {
  installPatchFake({ server: { release: { label: 'v5' } } });
  const Patch = await loadSdk();
  const status = await Patch.sync({
    installMode: Patch.InstallMode.ON_NEXT_RESTART,
    // A mandatory release installs IMMEDIATE unless this says otherwise.
    mandatoryInstallMode: Patch.InstallMode.ON_NEXT_RESTART,
  });
  expect(status).toBe('update-installed');
  expect(await Patch.isNextVersionReady()).toBe(true);
});
```

`sync()` calls `notifyAppReady()` first, so it confirms an unconfirmed running release. To keep it unconfirmed, use [`checkForUpdate()`, `downloadUpdate()` and `installUpdate()`](/docs/setup/manual-control) instead. `restartApp()` resolves without reloading JS: afterwards the SDK still reports the update as ready, not running. To test the next launch, install a fake seeded with that release as `running`.

## Failures and server changes

`installPatchFake()` returns a handle. `fake.fail(method, error)` makes every later call of one native method reject until you pass `null`. `fake.setServer()` replaces what later update checks get:

```ts
test('a download error', async () => {
  const fake = installPatchFake({ server: { release: { label: 'v5' } } });
  fake.fail('downloadUpdate', new Error('Connection reset'));
  const Patch = await loadSdk();
  expect(await Patch.sync()).toBe('error');

  fake.fail('downloadUpdate', null);
  fake.setServer({ release: null });
  expect(await Patch.sync()).toBe('up-to-date');
});
```

To make the update check itself fail, seed `server: { error }` or fail `fetchManifest`.

## Keep scenarios isolated

- **Install before the SDK is imported.** An SDK instance reads the native module and boot state once, when it is imported. `installPatchFake()` only affects SDK instances imported after it, so call `loadSdk()` after installing, or install before importing the code under test.
- **One scenario at a time.** A test file has one current fake, and `loadSdk()` does not reset it: failures, server answers and downloaded packages stay. Start each scenario with a new `installPatchFake()` and `loadSdk()`.
- **Require SDK users after `loadSdk()`.** `loadSdk()` resets Jest's module registry and imports a fresh SDK. Anything else that uses the SDK, such as your components, must be required after it to share that instance.
- **Keep React on one side of the reset.** The reset clears every module, React included. For a component test, either require React, the renderer and the component after `loadSdk()`, or install the scenario in a module imported first in the test file and skip `loadSdk()`, so nothing is loaded twice.

## Vitest

The same fake works under Vitest. Add the plugin, which points the package at its TypeScript source so Vitest's module mocks reach the SDK, and the setup file, which installs the store-binary scenario:

```ts
// vitest.config.ts

export default defineConfig({
  plugins: [codemagicPatch()],
  test: { setupFiles: ['@codemagic/react-native-patch/vitest-setup'] },
});
```

Then use `installPatchFake` and `loadSdk` from `@codemagic/react-native-patch/vitest` as above. The adapter replaces `react-native` with a mock that has only what the SDK uses (`TurboModuleRegistry`, `AppState`, `DeviceEventEmitter`), so render components with `react-dom` in a `jsdom` environment. Vitest does not reset React on `loadSdk()`, so the test and the SDK share one React instance.

## Not covered yet

The fake covers the React Native SDK only, not Capacitor. It does not cover code signing (it reports no signing key), download progress, rollout percentages, reverting to the store binary's bundle, or activating an installed update on restart.
