|
| 1 | +# React Native Metro tests |
| 2 | + |
| 3 | +These tests check how the SDK behaves in React Native apps when optional dependencies are installed or missing: `fast-text-encoding`, `react-native-get-random-values` and `@react-native-async-storage/async-storage`. They run on a Metro version with the dependency-map bug (Metro 0.80, React Native 0.73) and on one with the fix (Metro 0.87, React Native 0.87). See `lib/import.react_native/README.md` for the bug and how the SDK's optional import shims avoid it. |
| 4 | + |
| 5 | +Run them with: |
| 6 | + |
| 7 | +```sh |
| 8 | +npm run test-rn-metro |
| 9 | +``` |
| 10 | + |
| 11 | +To test a specific package or a subset: `node tests/react-native-metro/run.mjs [--sdk <package.tgz>] [--fixture rn-0.73] [--case P2]`. |
| 12 | + |
| 13 | +## How it works |
| 14 | + |
| 15 | +For each case, the test app in `app/` is bundled with a real Metro and React Native's own Metro config, then run in Node: |
| 16 | + |
| 17 | +1. **The SDK is packed and installed** into `.sdk/` the way an app installs it: `npm pack` (which builds it, as `npm publish` does), with its dependencies and without its optional peer dependencies. |
| 18 | +2. **The bundle is built** by `build.cjs` with the fixture's Metro, through `metro-config`'s `loadConfig` as the React Native CLI does. `metro.config.base.js` wraps the fixture's `@react-native/metro-config`, so transforms, inline requires, the serializer and React Native's JS polyfills (including `ErrorUtils`) match an app build. It changes two things: |
| 19 | + - **React Native's startup module is skipped** (`getModulesRunBeforeMainModule`), because it needs native code. |
| 20 | + - **`resolveRequest` decides which optional packages exist.** An installed package resolves to the real `fast-text-encoding` or to a stand-in from `stubs/`, for packages that need native code. A missing package fails to resolve, so Metro handles it as an optional dependency that isn't installed. netinfo always resolves to its stand-in, and bundling `react-native` itself fails the build. |
| 21 | +3. **The bundle runs in a fresh Node `vm` context.** It only has standard JS built-ins, so there is no `TextEncoder` or `crypto`, like Hermes before React Native 0.74. Cases with `nativeTextEncoder` add Node's `TextEncoder`. |
| 22 | +4. **The app records what happened and `run.mjs` checks it against the case's expectation.** The app records which implementation provides `TextEncoder` and `crypto.getRandomValues`, decisions, dispatched events, and any errors. Decisions must match `EXPECTED_DECISIONS` in `cases.mjs` exactly, so failed decisions don't pass. In failure cases, every recorded error must be the expected one. |
| 23 | + |
| 24 | +Metro's `require` is part of the bundle (`metro-runtime`), so the bundle's modules are loaded by the same code as in an app. Two things make the run behave like an app: |
| 25 | + |
| 26 | +- **`app/setup.js` installs a non-throwing `ErrorUtils` handler.** React Native's startup code replaces the polyfill's handler, which rethrows, with one that reports the error (red box or fatal) and returns. Without it, a failed `require` outside module loading would be rethrown to the caller instead of reported as fatal, and the test couldn't tell the two apart. The test treats a recorded fatal error as the red box. |
| 27 | +- **The SDK is loaded on a later tick (`app/index.js`),** when no module is loading, as app code does after startup. |
| 28 | + |
| 29 | +## Files |
| 30 | + |
| 31 | +| Path | Purpose | |
| 32 | +|---|---| |
| 33 | +| `run.mjs` | Packs and installs the SDK, builds the bundles, runs and checks every case, prints a results table | |
| 34 | +| `cases.mjs` | The cases and their expected results | |
| 35 | +| `build.cjs` | Builds one bundle with a fixture's Metro | |
| 36 | +| `metro.config.base.js` | Metro config shared by the fixtures | |
| 37 | +| `app/` | The test app: `index.js` (entry), `setup.js` (error handler, custom polyfills), `test.js` (the test), `datafile.json` | |
| 38 | +| `stubs/` | Stand-ins for netinfo, async-storage (v2 with `multiGet`, v3 with `getMany`) and `react-native-get-random-values` | |
| 39 | +| `fixtures/rn-0.73`, `fixtures/rn-0.87` | The Metro setup of each React Native version: `@react-native/metro-config`, `metro`, `metro-config`, `@babel/core`, `@babel/runtime` and `fast-text-encoding`, pinned by the committed lockfiles | |
| 40 | + |
| 41 | +`.sdk/` and `.out/` (the bundles) are generated and not committed. |
| 42 | + |
| 43 | +## Adding a case |
| 44 | + |
| 45 | +Add an entry to `CASES` in `cases.mjs`. `modules` sets which optional packages are installed, `runtime` sets what the app does at runtime, and `expect` is one of: |
| 46 | +- `works`: which implementation of `TextEncoder` and `getRandomValues` the SDK must use; |
| 47 | +- `fatal`: the error reported while the SDK loads; |
| 48 | +- `caught`: the error thrown to the app's code. |
| 49 | + |
| 50 | +The comment at the top of `cases.mjs` lists every option. |
| 51 | + |
| 52 | +## Updating a fixture |
| 53 | + |
| 54 | +Change the versions in the fixture's `package.json` to what that React Native version uses, then run `npm install` in the fixture folder and commit the updated lockfile. A fixture only needs React Native's Metro setup, not the `react-native` package. |
0 commit comments