diff --git a/AGENTS.md b/AGENTS.md index 989b4f699..223aab95b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -3,7 +3,7 @@ `@testing-library/react-native` is a TypeScript/Jest library for testing React Native components with user-focused testing patterns. > [!IMPORTANT] -> Never run git commands that create commits, push, or modify the index/history (`git commit`, `git push`, `git add`, `git rm`, `git reset`, `git rebase`, `git merge`, `git stash`, `git tag`, etc.). Only make working-tree changes and read-only git inspections; the human stages and commits. See [Git, releases, and PR workflow](agents/git-workflow.md). +> Never run git commands that create commits, push, or modify the index/history. The human stages and commits. See [Agent rules](#agent-rules). - Package manager: `yarn` (`yarn@4.11.0`) - Common commands: @@ -14,11 +14,31 @@ - `yarn format:check` - `yarn build` - `yarn validate` -- Task-specific guidance: - - [Architecture and API design](agents/architecture.md) - - [Build, validation, and repo layout](agents/build-and-validation.md) - - [TypeScript and code style](agents/code-style.md) - - [Testing conventions](agents/testing.md) - - [Native event propagation (bubbling vs direct)](agents/native-events.md) - - [Example app regeneration](agents/example-apps.md) - - [Git, releases, and PR workflow](agents/git-workflow.md) +- Contributor guides (shared with human contributors, see also [CONTRIBUTING.md](CONTRIBUTING.md)): + - [Architecture and API design](contributing/architecture.md) + - [Build, validation, and repo layout](contributing/build-and-validation.md) + - [TypeScript and code style](contributing/code-style.md) + - [Testing conventions](contributing/testing.md) + - [Native event propagation (bubbling vs direct)](contributing/native-events.md) + - [Native state for uncontrolled components](contributing/native-state.md) + - [Event dispatch (`fireEvent` vs `userEvent`)](contributing/event-dispatch.md) + - [Accessibility model](contributing/accessibility.md) + - [Async, `act`, and timers](contributing/async-and-timers.md) + - [Example app regeneration](contributing/example-apps.md) + - [Git, releases, and PR workflow](contributing/git-workflow.md) + +## Agent rules + +### Git restrictions + +- Never run git commands that create commits, push, or modify the index/history. The human owns these actions. +- Forbidden commands include (non-exhaustive): `git commit`, `git push`, `git add`, `git rm`, `git mv`, `git restore --staged`, `git reset`, `git rebase`, `git merge`, `git cherry-pick`, `git stash`, `git commit --amend`, and `git tag`. +- Read-only inspection is fine: `git status`, `git log`, `git diff`, `git show`, `git blame`. +- When conflicts or staging are involved, resolve file contents in the working tree only, then hand off to the human to stage and commit. Describe the exact commands you would run instead of running them. + +### PR draft + +- Maintain `PR.txt` at the repository root using the structure from `.github/PULL_REQUEST_TEMPLATE.md`. +- Keep `PR.txt` aligned with the current branch diff relative to `origin/main`. +- Include tests actually run and any known validation gaps in `PR.txt`. +- Do not commit `PR.txt`. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 9033c236d..e0f59fee4 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -14,46 +14,25 @@ The core team works directly on GitHub and all work is public. 1. Fork the repo and create your branch from `main` (a guide on [how to fork a repository](https://help.github.com/articles/fork-a-repo/)). 2. Run `yarn` to setup the development environment. -3. Do the changes you want and test them out in the example app before sending a pull request. - -### Commit message convention - -We prefix our commit messages with one of the following to signify the kind of change: - -- `fix`: bug fixes, e.g. fix incorrect error message. -- `feat`: new features, e.g. add useful API. -- `refactor`: code/structure refactor, e.g. new folder structure. -- `docs`: changes into documentation, e.g. add usage example for `getByText`. -- `test`: adding or updating tests, eg unit, snapshot testing. -- `chore`: tooling changes, e.g. change circle ci config. -- `BREAKING`: for changes that break existing usage, e.g. change API. - -Our pre-commit hooks verify that your commit message matches this format when committing. - -### Linting and tests - -We use TypeScript for type checking, `eslint` and `oxfmt` for linting and formatting the code, and `jest` for testing. Our pre-commit hooks verify that the linter and tests pass when committing. You can also run the following commands manually: - -- `yarn typecheck`: run TypeScript compiler on all files. -- `yarn lint`: run eslint. -- `yarn test`: run tests. - -### Sending a pull request - -When you're sending a pull request: - -- Prefer small pull requests focused on one change. -- Verify that `typecheck`, `eslint` and tests are passing. -- Preview the documentation to make sure it looks good. -- Follow the pull request template when opening a pull request. - -### Publishing a release - -We use [release-it](https://github.com/release-it/release-it) to publish a release. It takes care of versioning, changelog generation, and publishing to NPM. - -```sh -yarn release -``` +3. Make your changes, add tests, and try them out in the example app. +4. Run `yarn validate` to type check, test, lint, and check formatting. CI runs the same checks on your pull request. +5. Open a pull request following the [pull request guidelines](contributing/git-workflow.md#pull-requests). + +### Contributor guides + +Detailed guides live in [`contributing/`](contributing/). They are written for both human contributors and AI coding agents: + +- [Architecture and API design](contributing/architecture.md): project goals, API design principles, and host component detection +- [Build, validation, and repo layout](contributing/build-and-validation.md): commands, package docs generation, folder structure +- [TypeScript and code style](contributing/code-style.md): lint and formatting rules +- [Testing conventions](contributing/testing.md): how the library's own tests are organized +- [Native event propagation](contributing/native-events.md): which React Native events bubble and which are direct +- [Native state](contributing/native-state.md): how RNTL simulates state that lives in native views, like `TextInput` text and scroll position +- [Event dispatch](contributing/event-dispatch.md): how `fireEvent` and `userEvent` find handlers and which events they send +- [Accessibility model](contributing/accessibility.md): hidden elements, roles, accessible names, and state used by queries and matchers +- [Async, `act`, and timers](contributing/async-and-timers.md): `act` environment, fake timer detection, `waitFor`, `userEvent` delays, and cleanup +- [Example app regeneration](contributing/example-apps.md): upgrading the Expo apps in `examples/` +- [Git, releases, and PR workflow](contributing/git-workflow.md): commit message convention, pull requests, releases ## Reporting issues diff --git a/agents/architecture.md b/agents/architecture.md deleted file mode 100644 index 142f56a4a..000000000 --- a/agents/architecture.md +++ /dev/null @@ -1,21 +0,0 @@ -# Architecture And API Design - -## Project overview - -`@testing-library/react-native` provides utilities for testing React Native components in ways that resemble real usage and avoid implementation details. - -- Tech stack: TypeScript, React Native, Jest -- Core principle: the closer tests are to real usage, the more confidence they provide -- Runtime model: the library simulates the React Native runtime on top of `test-renderer` - -## API design principles - -- Prefer a small public API surface. -- Expose underlying React and `react-reconciler` capabilities when Testing Libraries need them. -- Render host elements only. -- Provide escape hatches to fibers when needed. - -## Key entry points - -- `src/pure.ts`: side-effect-free core logic without auto-cleanup -- `src/index.ts`: main entry point that re-exports `pure` and adds auto-cleanup side effects diff --git a/agents/build-and-validation.md b/agents/build-and-validation.md deleted file mode 100644 index e2dd812ef..000000000 --- a/agents/build-and-validation.md +++ /dev/null @@ -1,37 +0,0 @@ -# Build, Validation, And Repo Layout - -## Common commands - -- Install dependencies: `yarn install` -- Run tests: `yarn test` -- Run tests in CI mode: `yarn test:ci` -- Type check: `yarn typecheck` -- Lint source files: `yarn lint` -- Check formatting: `yarn format:check` -- Validate the main package: `yarn validate` -- Build the package: `yarn build` -- Regenerate package docs: `yarn docs:generate` -- Check package docs are in sync: `yarn docs:check` - -## Command notes - -- `yarn lint` runs ESLint on `src`. -- `yarn validate` runs typecheck, tests, lint, and oxfmt checks for the main package. -- `yarn build` cleans `dist/`, transpiles source with Babel, and emits TypeScript declarations. - -## Documentation - -- The docs under `website/docs/` are the source of truth. The `docs/api/*.md` files are - generated from them — never edit those by hand. -- After making any documentation changes, run `yarn docs:generate` to regenerate the package - docs, and commit the regenerated files alongside your `website/` edits. `yarn docs:check` - (part of `validate:all`) fails if they are out of sync. - -## Repo layout - -- `src/`: source code -- `src/pure.ts`: core logic without side effects -- `src/index.ts`: main entry point with auto-cleanup side effects -- `examples/`: example React Native apps -- `website/`: documentation site -- `codemods/`: codemod implementations and tests diff --git a/agents/code-style.md b/agents/code-style.md deleted file mode 100644 index fd56b1fcf..000000000 --- a/agents/code-style.md +++ /dev/null @@ -1,12 +0,0 @@ -# TypeScript And Code Style - -## Linting and formatting - -- ESLint uses `@callstack/eslint-config` together with `typescript-eslint`. -- Important enforced rules include `no-console` and consistent type imports. -- oxfmt is the formatter. -- Formatting defaults include single quotes and trailing commas. - -## Imports - -- Keep imports sorted with `oxfmt`. diff --git a/agents/example-apps.md b/agents/example-apps.md deleted file mode 100644 index fff2edac1..000000000 --- a/agents/example-apps.md +++ /dev/null @@ -1,61 +0,0 @@ -# Example App Regeneration - -## General workflow - -- Regenerate Expo example apps in a temporary directory, then copy the fresh scaffold into place. -- Before replacing an example app, move the current app directory to `/tmp` so repo-specific files can be restored selectively. -- Avoid image churn: preserve existing tracked image assets unless the task explicitly asks to update them or the new SDK requires an asset change. -- After copying a generated app into `examples/`, remove the generated `.git` directory and `node_modules`, then reinstall from inside the repo workspace. - -## `examples/basic` - -- Generate from the Expo blank TypeScript template: - - `yarn create expo-app /tmp/rntl-basic-fresh --template blank-typescript --yes` -- Restore these repo-specific files on top of the fresh scaffold: - - `App.tsx` - - `components/` - - `__tests__/` - - `theme.ts` - - `jest.config.js` - - `jest-setup.ts` - - `babel.config.js` - - `eslint.config.mjs` - - `README.md` - - `AGENTS.md` - - existing tracked image assets in `assets/` - - `.expo-shared/assets.json` if it existed before -- Keep the fresh Expo entrypoint `index.ts`. -- Update `package.json` and `app.json` to match repo naming and scripts. Keep `app.json` pointing at the restored existing image assets unless changing them is intentional. - -## `examples/cookbook` - -- Generate from a router-enabled Expo scaffold: - - `yarn create expo-app /tmp/rntl-cookbook-fresh --example with-router --yes` -- Restore these repo-specific files on top of the fresh scaffold: - - `app/` - - tutorial test directories such as `basics-tutorial/` and `basics-tutorial-react-strict-dom/` - - `theme.ts` - - `jest.config.js` - - `jest-setup.ts` - - `babel.config.js` - - `.eslintrc` - - `.eslintignore` - - `README.md` - - `AGENTS.md` - - custom assets not present in the scaffold, such as `assets/gradientRNBanner.png` - - existing tracked image assets in `assets/` - - `.expo-shared/assets.json` if it existed before -- Keep the fresh Expo Router entry setup. -- Reapply the cookbook-specific dependency set in `package.json` and `app.json`. Keep `app.json` pointing at the restored existing image assets unless changing them is intentional. - -## Validation after regeneration - -- Run these commands from inside the regenerated app directory: - - `yarn expo install --check` - - `yarn lint` - - `yarn typecheck` - - `yarn test --watchman=false` - -## Lockfile guidance - -- If the fresh scaffold causes dependency-resolution churn, restore the previous `yarn.lock` first and then run `yarn install` instead of re-resolving the full tree. diff --git a/agents/git-workflow.md b/agents/git-workflow.md deleted file mode 100644 index daef86153..000000000 --- a/agents/git-workflow.md +++ /dev/null @@ -1,20 +0,0 @@ -# Git, Releases, And PR Workflow - -## Agent git restrictions - -- Never run git commands that create commits, push, or modify the index/history. The human owns these actions. -- Forbidden commands include (non-exhaustive): `git commit`, `git push`, `git add`, `git rm`, `git restore --staged`, `git reset`, `git rebase`, `git merge`, `git cherry-pick`, `git stash`, `git commit --amend`, and `git tag`. -- Read-only inspection is fine: `git status`, `git log`, `git diff`, `git show`, `git blame`. -- When conflicts or staging are involved, resolve file contents in the working tree only, then hand off to the human to stage and commit. Describe the exact commands you would run instead of running them. - -## Commits and releases - -- Use Conventional Commits such as `fix:`, `feat:`, and `chore:`. -- Releases are managed with `release-it`. - -## PR draft workflow - -- Maintain `PR.txt` at the repository root using the structure from `.github/pull_request_template.md`. -- Keep `PR.txt` aligned with the current branch diff relative to `origin/main`. -- Include tests actually run and any known validation gaps in `PR.txt`. -- Do not commit `PR.txt`. diff --git a/agents/native-events.md b/agents/native-events.md deleted file mode 100644 index 9c4a1fa89..000000000 --- a/agents/native-events.md +++ /dev/null @@ -1,57 +0,0 @@ -# Native Event Propagation - -React Native declares, for each native (host) component, which events **bubble** up the tree and which are **direct**, meaning they are delivered only to the element that emitted them. Use this reference when deciding whether an event helper should look for handlers on ancestor elements. - -## How to read this - -- Native event names use a `top` prefix that maps to the `on*` prop: `topLayout` → `onLayout`. The tables below use the short name (`layout`). -- Every host component inherits the **base view config** events and adds its own component-specific events on top of them. -- `fireEvent` walks up the tree to find a handler, which matches bubbling events. Events for which `isDirectEvent()` in `src/fire-event.ts` returns `true` skip that walk and invoke only the target element's handler. Today only `layout` is treated as direct. -- Snapshot taken from `react-native@0.88.0-rc.1`. See [Sources](#sources) to re-check after RN upgrades. - -## Base view config (all host components) - -| Kind | iOS | Android | -| -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Bubbling | `press`, `click`, `change`, `focus`, `blur`, `submitEditing`, `endEditing`, `keyPress`, `touchStart`, `touchMove`, `touchEnd`, `touchCancel`, `pointer*`\* | `click`, `change`, `select`, `focus`, `blur`, `keyDown`, `keyUp`, `touchStart`, `touchMove`, `touchEnd`, `touchCancel`, `pointer*`\* | -| Direct | `layout`, `accessibilityAction`, `accessibilityTap`, `magicTap`, `accessibilityEscape` | `layout`, `accessibilityAction`, `scroll`, `scrollBeginDrag`, `scrollEndDrag`, `momentumScrollBegin`, `momentumScrollEnd`, `contentSizeChange`, `selectionChange`, `message`, `loadingStart`, `loadingFinish`, `loadingError` | - -\* `pointer*` = `pointerDown`, `pointerMove`, `pointerUp`, `pointerCancel`, `pointerEnter`, `pointerLeave`, `pointerOver`, `pointerOut`, `gotPointerCapture`, `lostPointerCapture`. - -Both platforms also register `onGestureHandlerEvent` and `onGestureHandlerStateChange` as direct events for React Native Gesture Handler. - -## Component-specific events - -Events listed here are added on top of the base view config. "Host name" is the native `uiViewClassName` (or codegen component name). - -| Component | Host name | Bubbling | Direct | -| ---------------------------------- | --------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------- | -| `View`, `Pressable`, etc. | `RCTView` | — | — | -| `Text` | `RCTText` (nested: `RCTVirtualText`, no extra events) | — | `textLayout` | -| `TextInput` (iOS) | `RCTSinglelineTextInputView`, `RCTMultilineTextInputView` | `blur`, `change`, `endEditing`, `focus`, `keyPress`, `submitEditing`, `touchMove`, `touchEnd`, `touchCancel` | `scroll`, `selectionChange`, `contentSizeChange`, `changeSync`, `keyPressSync` | -| `TextInput` (Android) | `AndroidTextInput` | `endEditing`, `keyPress`, `submitEditing` | `scroll` | -| `ScrollView` | `RCTScrollView` | — | `scroll`, `scrollBeginDrag`, `scrollEndDrag`, `momentumScrollBegin`, `momentumScrollEnd`; iOS also `scrollToTop` | -| `ScrollView` (horizontal, Android) | `AndroidHorizontalScrollView` | — | — (scroll events come from the Android base config) | -| `Image` | `RCTImageView` | — | `loadStart`, `progress`, `error`, `load`, `loadEnd`; iOS also `partialLoad` | -| `Switch` (iOS) | `Switch` | `change` | — | -| `Switch` (Android) | `AndroidSwitch` | `change` | — | -| `Modal` | `ModalHostView` | — | `requestClose`, `show`, `dismiss`, `orientationChange` | -| `RefreshControl` (iOS) | `PullToRefreshView` | — | `refresh` | -| `RefreshControl` (Android) | `AndroidSwipeRefreshLayout` | — | `refresh` | -| `DrawerLayoutAndroid` | `AndroidDrawerLayout` | — | `drawerSlide`, `drawerStateChanged`, `drawerOpen`, `drawerClose` | - -## Known gaps in RNTL - -These events are direct in React Native but still bubble through `fireEvent`. Changing them is a breaking change for users who fire them on a child element: - -- Scroll events: `scroll`, `scrollBeginDrag`, `scrollEndDrag`, `momentumScrollBegin`, `momentumScrollEnd` -- `contentSizeChange`, `selectionChange`, `textLayout` -- `Image` load events, `Modal` events, `refresh` - -## Sources - -All paths are relative to `node_modules/react-native`: - -- Base config: `Libraries/NativeComponent/BaseViewConfig.ios.js`, `Libraries/NativeComponent/BaseViewConfig.android.js` -- Static view configs: `Libraries/Text/TextNativeComponent.js`, `Libraries/Image/ImageViewNativeComponent.js`, `Libraries/Components/ScrollView/*NativeComponent.js`, `Libraries/Components/TextInput/RCTTextInputViewConfig.js`, `Libraries/Components/TextInput/AndroidTextInputNativeComponent.js` -- Codegen specs (`DirectEventHandler` vs `BubblingEventHandler` prop types): `src/private/components/*/specs/*NativeComponent.js` diff --git a/agents/testing.md b/agents/testing.md deleted file mode 100644 index e1a4eace9..000000000 --- a/agents/testing.md +++ /dev/null @@ -1,20 +0,0 @@ -# Testing Conventions - -## Test stack - -- Test framework: Jest with the `react-native` preset -- Test environment setup: `jest-setup.ts` -- Auto-cleanup is configured from `src/index.ts` unless explicitly skipped - -## Test location and coverage - -- Library tests are typically colocated under `src/**/__tests__` -- Coverage is collected from `src` and excludes test files - -## Test organization - -- Use `describe` blocks to group tests by theme. -- Do not put all tests into a single `describe`. -- Avoid nested `describe` blocks. -- If a `describe` would contain only one test, make that test top-level instead. -- Prefer `test` over `it`. diff --git a/contributing/accessibility.md b/contributing/accessibility.md new file mode 100644 index 000000000..12a48f743 --- /dev/null +++ b/contributing/accessibility.md @@ -0,0 +1,18 @@ +# Accessibility Model + +RNTL queries and matchers see the tree the way a screen reader would. The rules live in `src/helpers/accessibility.ts`. `*ByRole`, hiding of inaccessible elements, and the accessibility matchers all build on them. + +## Key concepts + +- **Hidden elements.** Some props and styles hide an element and its subtree from screen readers, for example `aria-hidden`, `display: 'none'`, or a modal sibling. Queries skip hidden elements by default (`includeHiddenElements` changes this). +- **Accessibility elements.** Only some elements are focusable by a screen reader: those with `accessible`, plus a few host components by default. `*ByRole` matches only these. +- **Role.** Comes from `role` or `accessibilityRole`. Host `Text` defaults to `text`. +- **Accessible name.** Comes from a label (`aria-labelledby`, `aria-label`, …) or, if there isn't one, from the element's text content. +- **State and value.** Disabled, checked, selected, busy, expanded, and value. Each has a `computeAria*()` function. + +## Guidelines + +- **Follow React Native.** Rules should match RN's documented behavior on iOS and Android. Comments in the code link to the RN docs or source. Keep those links, and note when platforms differ. +- **`aria-*` props win** over the older `accessibilityState`, `accessibilityValue` and `accessibilityLabel` props. Support both forms and test both. +- **Use the helpers.** Read accessibility info only through the functions in `src/helpers/accessibility.ts`, not from props directly. This keeps queries and matchers in agreement. +- **Changes are broad.** A rule change affects many queries and matchers at once. Run the full test suite. diff --git a/contributing/architecture.md b/contributing/architecture.md new file mode 100644 index 000000000..6f817cb49 --- /dev/null +++ b/contributing/architecture.md @@ -0,0 +1,21 @@ +# Architecture And API Design + +RNTL lets you test React Native components the way users interact with them, not through implementation details. It runs your components on top of `test-renderer`, which simulates the React Native runtime inside Jest. + +## API design principles + +- Keep the public API small. +- Render host elements only (`View`, `Text`, etc.), not composite components. +- Expose React and `react-reconciler` features when other Testing Libraries need them. +- Offer escape hatches to fibers for the rare cases that need them. + +## Entry points + +- `src/pure.ts`: the core API, with no side effects. +- `src/index.ts`: re-exports `pure` and registers auto-cleanup after each test. This is what users import by default. + +## Host components + +Some host components need special handling, like `Text`, `TextInput` or `ScrollView`. RNTL recognizes them by their host `type` name, using the helpers in `src/helpers/host-component-names.ts`. Use these helpers instead of comparing `instance.type` to a string. + +The names come from React Native's Jest mocks, not from the native views on a device. A React Native upgrade can change them. `src/__tests__/host-component-names.test.tsx` catches that. diff --git a/contributing/async-and-timers.md b/contributing/async-and-timers.md new file mode 100644 index 000000000..15bddfffe --- /dev/null +++ b/contributing/async-and-timers.md @@ -0,0 +1,19 @@ +# Async, `act` and Timers + +RNTL's API is async: `render`, `fireEvent`, `userEvent` and `waitFor` all return promises. They need to work with React's `act()` and with both real and fake Jest timers. + +## Key concepts + +- **`act` environment.** React only checks for `act()` when the global `IS_REACT_ACT_ENVIRONMENT` is true. RNTL wraps every React update in its own `act()` (`src/act.ts`), which also turns this flag on. +- **`wrapAsync`.** Code that waits on time instead of React updates, like `waitFor` and `userEvent`, runs with the flag turned off so React doesn't warn. React updates inside it still use `act()`. See `src/helpers/wrap-async.ts`. +- **Fake timer detection.** `src/helpers/timers.ts` checks whether Jest fake timers (legacy or modern) are on each time it's needed, so tests can switch timers. RNTL saves the real timer functions when it loads and uses them for its own waiting. +- **`waitFor`.** Adapted from DOM Testing Library. With fake timers, it moves fake time forward itself between checks. With real timers, it polls. +- **`userEvent` delays.** Waits between steps go through `wait()` in `src/user-event/utils/wait.ts`. It moves fake timers forward when they're on, so actions like `longPress` work with either kind of timer. +- **Cleanup.** `render` and `waitFor` add themselves to a cleanup queue (`src/cleanup.ts`). The default entry point runs `cleanup()` after each test. + +## Guidelines + +- Wrap every new React update in RNTL's `act()`. Run code that waits on time through `wrapAsync()`. +- Use the timer functions from `src/helpers/timers.ts`, not the global `setTimeout` and `setImmediate`, which may be faked. +- Test timing changes with real timers, legacy fake timers, and modern fake timers. +- Much of this code comes from React Testing Library and DOM Testing Library. Check how they handle a problem before solving it differently. diff --git a/contributing/build-and-validation.md b/contributing/build-and-validation.md new file mode 100644 index 000000000..15f8b720e --- /dev/null +++ b/contributing/build-and-validation.md @@ -0,0 +1,31 @@ +# Build, Validation, And Repo Layout + +Run `yarn install` once, then `yarn validate` before you push. It runs the same checks as CI: type check, tests, lint, and formatting. + +## Commands + +| Command | What it does | +| --------------------------------------- | -------------------------------------------------------------- | +| `yarn test` | Run tests | +| `yarn typecheck` | Type check | +| `yarn lint` | Run ESLint on `src/` | +| `yarn format:check` / `yarn format:fix` | Check or fix formatting | +| `yarn validate` | All of the above | +| `yarn validate:all` | Also validate the examples, website, and generated docs | +| `yarn build` | Clean `dist/`, compile with Babel, then emit type declarations | +| `yarn docs:generate` | Regenerate package docs from the website | + +## Documentation + +Edit docs in `website/docs/`. The `docs/` folder is generated from it and ships to npm for coding agents, so never edit it by hand. + +After changing docs, run `yarn docs:generate` and commit the result together with your `website/` changes. CI fails if they are out of sync. + +## Repo layout + +- `src/`: library source and tests +- `docs/`: generated package docs (do not edit) +- `website/`: documentation site +- `examples/`: example Expo apps +- `codemods/`: codemods for upgrading user code +- `contributing/`: these guides diff --git a/contributing/code-style.md b/contributing/code-style.md new file mode 100644 index 000000000..fd0008fa7 --- /dev/null +++ b/contributing/code-style.md @@ -0,0 +1,6 @@ +# TypeScript And Code Style + +Tooling enforces the style. Run `yarn lint` and `yarn format:fix` before you push. + +- **Formatting:** oxfmt with single quotes, trailing commas, and sorted imports. +- **Linting:** ESLint with `@callstack/eslint-config` and `typescript-eslint`. Notable rules: no `console`, and use `import type` for type-only imports. diff --git a/contributing/event-dispatch.md b/contributing/event-dispatch.md new file mode 100644 index 000000000..f340cd549 --- /dev/null +++ b/contributing/event-dispatch.md @@ -0,0 +1,24 @@ +# Event Dispatch: `fireEvent` vs `userEvent` + +RNTL has two ways to trigger events. Neither goes through React Native's native event system. Both find `on*` props in the rendered tree and call them inside `act()`. + +## `fireEvent` + +`src/fire-event.ts` calls a single handler for a single event. The work is in finding the right handler: + +- It starts at the target and moves up the tree until it finds a handler. It also checks props of composite components, not only host elements. +- Direct events (see [Native event propagation](native-events.md)) only check the target. +- It mimics cases where a device would not deliver the event, like `pointerEvents`, a non-editable `TextInput`, or a touch responder that declines. + +## `userEvent` + +`src/user-event/` simulates a whole interaction (press, type, scroll, …) as a realistic sequence of events with delays between them. The sequences are based on how React Native behaves on real devices. + +Each step uses `dispatchEvent()`, which only calls the target's own handler. It doesn't bubble or check whether the element is enabled. Each action does those checks itself, so the rules for an interaction live in one place. + +## Guidelines + +- To change which handler gets a single event, change `fireEvent`. To make an interaction more realistic, change the `userEvent` action. +- Keep `dispatchEvent()` simple. +- Put rules that both need, like `pointerEvents` or `editable`, in shared helpers in `src/helpers/`. +- Event sequences should match a real device. Check on a device before changing one, and keep the code comments explaining the observed behavior. diff --git a/contributing/example-apps.md b/contributing/example-apps.md new file mode 100644 index 000000000..d1d7919ea --- /dev/null +++ b/contributing/example-apps.md @@ -0,0 +1,43 @@ +# Example App Regeneration + +When the Expo SDK is upgraded, the apps in `examples/` are recreated from a fresh Expo template instead of being upgraded in place. You then copy back the files that are specific to this repo. + +## Steps + +1. Move the current app to `/tmp` so you can restore files from it. +2. Generate a fresh app in `/tmp` (commands below). +3. Copy the fresh app into `examples/`, then delete its `.git` and `node_modules`. +4. Restore the repo-specific files listed below. +5. Update `package.json` and `app.json` with the repo's app name, scripts, and dependencies. +6. Run `yarn install`, then validate (see the end of this page). + +Keep the existing image assets unless the new SDK requires different ones. + +## `examples/basic` + +```sh +yarn create expo-app /tmp/rntl-basic-fresh --template blank-typescript --yes +``` + +Restore: `App.tsx`, `components/`, `__tests__/`, `theme.ts`, `jest.config.js`, `jest-setup.ts`, `babel.config.js`, `eslint.config.mjs`, `README.md`, `AGENTS.md`, `.expo-shared/assets.json`, and `assets/`. Keep the new `index.ts`. + +## `examples/cookbook` + +```sh +yarn create expo-app /tmp/rntl-cookbook-fresh --example with-router --yes +``` + +Restore: `app/`, the tutorial folders (`basics-tutorial/`, `basics-tutorial-react-strict-dom/`), `theme.ts`, `jest.config.js`, `jest-setup.ts`, `babel.config.js`, `eslint.config.mjs`, `README.md`, `AGENTS.md`, `.expo-shared/assets.json`, and `assets/`. Keep the new Expo Router setup. + +## Validate + +Run these from inside the app folder: + +```sh +yarn expo install --check +yarn lint +yarn typecheck +yarn test --watchman=false +``` + +If the new template causes a lot of `yarn.lock` churn, restore the old `yarn.lock` and run `yarn install` again. diff --git a/contributing/git-workflow.md b/contributing/git-workflow.md new file mode 100644 index 000000000..5b2783472 --- /dev/null +++ b/contributing/git-workflow.md @@ -0,0 +1,29 @@ +# Git, Releases, And PR Workflow + +Branch from `main`, use Conventional Commit messages, and keep each pull request focused on a single change. + +## Commit messages + +Start each message with the type of change: + +- `feat`: new feature +- `fix`: bug fix +- `refactor`: code change with no behavior change +- `docs`: documentation +- `test`: tests only +- `chore`: tooling, CI, dependencies +- `BREAKING`: change that breaks existing usage + +For example: `fix: handle disabled Pressable in userEvent.press`. + +## Pull requests + +Before opening a PR: + +- Run `yarn validate`. +- If you changed docs, run `yarn docs:generate`. +- Fill in the [PR template](../.github/PULL_REQUEST_TEMPLATE.md) with what the change does and how you tested it. + +## Releases + +Maintainers publish releases with [release-it](https://github.com/release-it/release-it): `yarn release` for stable versions and `yarn release:next` for release candidates. diff --git a/contributing/native-events.md b/contributing/native-events.md new file mode 100644 index 000000000..7165104c4 --- /dev/null +++ b/contributing/native-events.md @@ -0,0 +1,37 @@ +# Native Event Propagation + +In React Native, some events **bubble** up to parent elements and others are **direct**, meaning only the element that emitted them receives them. `fireEvent` should behave the same way. + +Today, `fireEvent` treats every event as bubbling except `layout`. The list of direct events lives in `isDirectEvent()` in `src/fire-event.ts`. + +## Which events are which + +There is no simple rule for which events bubble. Coming from user input doesn't make an event bubble: `scroll` and `refresh` start with a user gesture but are direct. Check the lists below rather than guessing. + +**Bubbling:** `press`, `change`, `focus`, `blur`, `submitEditing`, `endEditing`, `keyPress`, and touch and pointer events (`touchStart`, `pointerDown`, etc.). + +**Direct:** + +| Component | Direct events | +| ---------------- | ---------------------------------------------------------------------------------------- | +| All components | `layout`, accessibility actions | +| `ScrollView` | `scroll`, `scrollBeginDrag`, `scrollEndDrag`, `momentumScrollBegin`, `momentumScrollEnd` | +| `TextInput` | `scroll`, `selectionChange`, `contentSizeChange` | +| `Text` | `textLayout` | +| `Image` | `loadStart`, `progress`, `load`, `error`, `loadEnd` | +| `Modal` | `requestClose`, `show`, `dismiss`, `orientationChange` | +| `RefreshControl` | `refresh` | + +This is simplified. A few events differ between iOS and Android. Check the sources below for the exact details. + +## Known gaps + +All the direct events above except `layout` still bubble in `fireEvent`. Fixing that is a breaking change: tests that fire these events on a child element would stop reaching the parent's handler. + +## Sources + +The table is based on `react-native@0.88.0-rc.1`. Re-check it after React Native upgrades. In `node_modules/react-native`: + +- Events shared by all components: `Libraries/NativeComponent/BaseViewConfig.{ios,android}.js` +- Component-specific events: each component's `*NativeComponent.js` or `*ViewConfig.js` file +- Newer components: `src/private/components/*/specs/`, where `DirectEventHandler` and `BubblingEventHandler` prop types mark each event diff --git a/contributing/native-state.md b/contributing/native-state.md new file mode 100644 index 000000000..cc7be3301 --- /dev/null +++ b/contributing/native-state.md @@ -0,0 +1,17 @@ +# Native State + +On a device, some component state lives in native views, not in React. Jest has no native views, so RNTL keeps this state itself in `src/native-state.ts`. + +## What is stored + +- **`TextInput` value** (`valueForInstance`): the text in an uncontrolled `TextInput`. Without it, the input would show no text after `user.type()`. +- **`ScrollView` content offset** (`contentOffsetForInstance`): the current scroll position. Without it, every `scrollTo()` would start from `0, 0`. +- **Layout size** (`layoutSizeForInstance`): an element's width and height from the last `layout` event. Scroll events use it as the `ScrollView`'s default `layoutMeasurement`. + +## Key points + +- **Writes.** `fireEvent` and `userEvent` update native state when they simulate a change that a native view would make. +- **Reads.** Helpers read native state, like `getTextInputValue()` in `src/helpers/text-input.ts`. Queries and matchers use those helpers instead of reading native state directly. +- **Props win.** A controlled prop (like `value`) always takes precedence over native state. +- **No reset.** State is stored in `WeakMap`s keyed by host instance. It disappears when the instance is unmounted, so `cleanup()` doesn't need to clear it. +- **Internal.** Native state isn't part of the public API. diff --git a/contributing/testing.md b/contributing/testing.md new file mode 100644 index 000000000..7ff84b537 --- /dev/null +++ b/contributing/testing.md @@ -0,0 +1,15 @@ +# Testing Conventions + +Every change in `src/` should come with tests. Tests use Jest and live next to the code in `src/**/__tests__/`. + +## Writing tests + +- Use `test`, not `it`. +- Group related tests with `describe`, but don't nest `describe` blocks. +- Don't wrap a whole file in a single `describe`, and don't create a `describe` for just one test. + +## Setup + +- Shared setup lives in `jest-setup.ts`. +- Auto-cleanup between tests comes from `src/index.ts`. +- Coverage is collected from `src/`, excluding tests and `src/test-utils/`.