You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Commit 0b69f6b
Browse filesBrowse the repository at this point in the historyBrowse files
-[Git, releases, and PR workflow](contributing/git-workflow.md)
29
29
30
+
## Reference sources
31
+
32
+
Upstream sources are checked out as shallow git submodules under `refs/` for code research. Read and search them to see how upstream implements something (event dispatch, renderer internals, query semantics) instead of guessing or fetching from the web.
33
+
34
+
-`refs/react-native/`: [facebook/react-native](https://github.com/facebook/react-native) (core components in `packages/react-native/Libraries/`)
35
+
-`refs/react/`: [facebook/react](https://github.com/facebook/react) (reconciler, test renderer, and RN renderer in `packages/`)
-`refs/expensify-app/`: [Expensify/App](https://github.com/Expensify/App), a large production React Native app with about 1,000 test files that use this library (in `tests/ui/`, `tests/unit/`, `tests/perf-test/`). Use it to see how real-world tests call the API and to judge the impact of behavior or API changes. Check its `package.json` for the version it uses.
39
+
40
+
Notes:
41
+
42
+
- Treat `refs/` as read-only. Never edit files there or import from it in `src/`.
43
+
- Submodules track upstream `main`, which can differ from the versions installed in `node_modules/`. For behavior that must match what this library runs against, check the installed package in `node_modules/` too.
44
+
- If `refs/` is empty, ask the human to run `git submodule update --init --depth 1`.
45
+
- Tooling ignores `refs/` (Jest, ESLint, oxfmt, `tsc`). Keep it that way when changing configs.
Copy file name to clipboardExpand all lines: contributing/build-and-validation.md
+1Lines changed: 1 addition & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -29,3 +29,4 @@ After changing docs, run `yarn docs:generate` and commit the result together wit
29
29
-`examples/`: example Expo apps
30
30
-`codemods/`: codemods for upgrading user code
31
31
-`contributing/`: these guides
32
+
-`refs/`: upstream sources (React, React Native, Testing Library) and the Expensify app as a real-world test suite, as shallow git submodules, for reading only. Fetch them with `git submodule update --init --depth 1`.
Copy file name to clipboardExpand all lines: contributing/event-dispatch.md
+18-16Lines changed: 18 additions & 16 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -2,24 +2,26 @@
2
2
3
3
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()`.
4
4
5
-
Both are built on the shared event subsystem in `src/events/`, which also holds `fireEvent` itself:
5
+
Both are built on the shared event subsystem in `src/events/legacy/`, which also holds `fireEvent` itself. This is the `'legacy'` event system, the default for the `unstable_eventSystem` config option. A `'modern'` event system that follows React Native's event dispatch is in progress.
|`propagation.ts`| Bubbling vs direct events, walking up host and composite elements |
13
+
|`is-enabled.ts`| Whether a device would deliver the event: `pointerEvents`, `editable`, touch responders |
14
+
|`dispatch.ts`|`dispatchEvent()`: calls the target's own handler in `act()`, used by `userEvent`|
15
+
|`warnings.ts`|`eventDiagnostics` warnings for `fireEvent`, and helpers shared with `userEvent`|
16
+
|`builders/`| Legacy event objects: `wrapNativeEvent()` (stubs from `baseSyntheticEvent()`), touch and responder events |
17
+
18
+
Code used by both event systems lives in `src/events/shared/`: `handler.ts` (finding the `on*` handler for an event name in props), `native-state.ts` and `update-native-state.ts` ([native state](native-state.md) and how `fireEvent` updates it), `payloads.ts` (`nativeEvent` payloads matching what React Native sends on a device), `merge.ts` (deep merging custom props into them), and `types.ts`.
19
+
20
+
`src/user-event/` is a separate module on top of the event subsystem. It creates and dispatches native events through the facades `src/events/create-event.ts` and `src/events/dispatch-event.ts`, and imports the rest only through `src/events/legacy/index.ts`, which also re-exports `src/events/shared/handler.ts` and `src/events/shared/native-state.ts`.
19
21
20
22
## `fireEvent`
21
23
22
-
`src/events/fire-event.ts` is the public API. It calls a single handler for a single event, found with `findEventHandler()` from `src/events/propagation.ts`. The work is in finding the right handler:
24
+
`src/events/legacy/fire-event.ts` is the public API. It calls a single handler for a single event, found with `findEventHandler()` from `src/events/legacy/propagation.ts`. The work is in finding the right handler:
23
25
24
26
- 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.
25
27
- Direct events (see [Native event propagation](native-events.md)) still bubble, with a warning when they reach an ancestor that emits them. `fireEvent.layout()` only checks the target.
@@ -29,11 +31,11 @@ Both are built on the shared event subsystem in `src/events/`, which also holds
29
31
30
32
`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.
31
33
32
-
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.
34
+
Each step dispatches a native event, created for the configured event system by `createEvent()` (`src/events/create-event.ts`: a legacy event object or a `SyntheticEvent`) and dispatched by `dispatchEvent()` (`src/events/dispatch-event.ts`), or calls a JavaScript callback (`changeText`, `pressIn`, ...) with `invokeEventHandler()`, which only calls the target's own handler. Modern `fireEvent.changeText()` instead calls `onChangeText` from the `change` dispatch, through `dispatchEvent()`'s `afterTargetHandler` option, right after the input's own `onChange`, where `TextInput` calls it. Neither checks whether the element is enabled. Each action does those checks itself, so the rules for an interaction live in one place.
33
35
34
36
For the `eventDiagnostics` warning, each action tracks itself with an `Interaction` from `src/user-event/utils/interaction.ts`:
35
37
36
-
- Dispatch events with `interaction.dispatchEvent()`, so it records whether any handler ran. Events go to `interaction.target`, which is the element the action was called with, unless the action moves it (as `press()` does when an ancestor handles the press). If the action has to call a handler itself, record it with `interaction.recordEvent()` (as `pullToRefresh()` does for `onRefresh` on the `refreshControl` prop).
38
+
- Dispatch native events with `interaction.dispatchEvent(eventType, buildFocusEvent())`, using the `build*Event()` helpers from `src/events/create-event.ts`, and call JavaScript callbacks with `interaction.invokeEventHandler(eventType, ...params)` (or `interaction.dispatchTouchEvent()` for `pressIn`, `pressOut` and `longPress`, which passes a touch event), so it records whether any handler ran. Events go to `interaction.target`, which is the element the action was called with, unless the action moves it (as `press()` does when an ancestor handles the press). If the action has to call a handler itself, record it with `interaction.recordEvent()` (as `pullToRefresh()` does for `onRefresh` on the `refreshControl` prop).
37
39
- Set `hasUpdatedNativeState` when the action writes to `nativeState`.
38
40
- Add elements that could handle the action but don't accept it to `skippedTargets`: disabled, non-editable `TextInput`, blocked by `pointerEvents`, or with a responder that declines the touch. The warning first reports the ones blocked by `pointerEvents`, with the element that blocks them (`getPointerEventsBlocker()`). Otherwise it reports the disabled ones (`computeAriaDisabled()`, which includes non-editable `TextInput`; when all of them are non-editable `TextInput`, the message calls them non-editable, see `formatDisabledTargets()`), and skips the warning if every skipped element has a responder that declines the touch. Text actions (`type()`, `clear()`, `paste()`) add the `TextInput` when it is non-editable or blocked by `pointerEvents`.
39
41
- Call `warnAboutUnhandledInteraction()` from `src/user-event/utils/warnings.ts` at the end. It warns only if no handler ran and native state didn't change.
@@ -42,5 +44,5 @@ For the `eventDiagnostics` warning, each action tracks itself with an `Interacti
42
44
43
45
- To change which handler gets a single event, change `fireEvent`. To make an interaction more realistic, change the `userEvent` action.
44
46
- Keep `dispatchEvent()` simple.
45
-
- Put event rules that both need, like the `pointerEvents` and `editable` checks, in `src/events/`. They may build on general helpers from `src/helpers/` (for example `isEditableTextInput`). Code used only by `userEvent`, like delays and scroll steps, stays in `src/user-event/`.
47
+
- Put event rules that both need, like the `pointerEvents` and `editable` checks, in `src/events/legacy/`. They may build on general helpers from `src/helpers/` (for example `isEditableTextInput`). Code used only by `userEvent`, like delays and scroll steps, stays in `src/user-event/`.
46
48
- Event sequences should match a real device. Check on a device before changing one, and keep the code comments explaining the observed behavior.
Copy file name to clipboardExpand all lines: contributing/native-events.md
+2-2Lines changed: 2 additions & 2 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -2,7 +2,7 @@
2
2
3
3
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.
4
4
5
-
Today, `fireEvent` still bubbles direct events, with a warning (see [Known gaps](#known-gaps)). Only `fireEvent.layout()` does not bubble. The rules live in `isDirectEvent()` in `src/events/propagation.ts`.
5
+
Today, `fireEvent` still bubbles direct events, with a warning (see [Known gaps](#known-gaps)). Only `fireEvent.layout()` does not bubble. The rules live in `isDirectEvent()` in `src/events/legacy/propagation.ts`.
6
6
7
7
## Which events are which
8
8
@@ -34,7 +34,7 @@ Until then, `fireEvent` logs a warning when a direct event bubbles from a nested
34
34
35
35
`contentSizeChange` is not a native `ScrollView` event, so the table above doesn't list it. The `ScrollView` component calls `onContentSizeChange` from the `onLayout` of its content view and passes `onContentSizeChange: null` to the host element. The Jest `ScrollView` mock passes the prop to the host element instead, so the rule uses `ScrollView` as the emitting element. `FlatList` and `SectionList` always set this handler, and tests fire the event on list items, so making it direct will break more tests than other events.
36
36
37
-
Both rules depend on the Jest mock. The `FlatList` cases in `src/events/__tests__/fire-event.test.tsx` cover both, so a mock change that moves these handlers fails them.
37
+
Both rules depend on the Jest mock. The `FlatList` cases in `src/events/legacy/__tests__/fire-event.test.tsx` cover both, so a mock change that moves these handlers fails them.
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/events/native-state.ts`.
3
+
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/events/shared/native-state.ts`.
4
4
5
5
## What is stored
6
6
@@ -10,8 +10,8 @@ On a device, some component state lives in native views, not in React. Jest has
10
10
11
11
## Key points
12
12
13
-
-**Writes.**`fireEvent` and `userEvent` update native state when they simulate a change that a native view would make. `fireEvent`does it through `updateNativeStateFromEvent()` in `src/events/update-native-state.ts`. Each `userEvent` action writes it directly.
14
-
-**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.
13
+
-**Writes.**`fireEvent` and `userEvent` update native state when they simulate a change that a native view would make. Both `fireEvent`implementations do it through `updateNativeStateFromEvent()` in `src/events/shared/update-native-state.ts`. It saves the `TextInput` value from `changeText` and `change` events (`nativeEvent.text`). Each `userEvent` action writes it directly.
14
+
-**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. Event payloads read it through `getNativeStateEventProps()`, next to `updateNativeStateFromEvent()`. Modern `fireEvent` applies it centrally, in `fireEventInternal()`, to the default payload of `fireEvent.scroll()` and `fireEvent.layout()`.
15
15
-**Props win.** A controlled prop (like `value`) always takes precedence over native state.
16
16
-**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.
17
17
-**Internal.** Native state isn't part of the public API.
Copy file name to clipboardExpand all lines: contributing/testing.md
+13Lines changed: 13 additions & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -13,3 +13,16 @@ Every change in `src/` should come with tests. Tests use Jest and live next to t
13
13
- Shared setup lives in `jest-setup.ts`.
14
14
- Auto-cleanup between tests comes from `src/index.ts`.
15
15
- Coverage is collected from `src/`, excluding tests and `src/test-utils/`.
16
+
17
+
## Event systems
18
+
19
+
`fireEvent` and `userEvent` work with both event systems (`configure({ unstable_eventSystem })`), so `jest.config.js` has two projects:
20
+
21
+
-`legacy` runs all tests with the default `'legacy'` event system.
22
+
-`modern` runs all tests again with `'modern'` (`jest-setup-modern.ts`).
23
+
24
+
Both projects share the same snapshots. `createEventLogger()` entries print only the `nativeEvent` of event payloads (`src/test-utils/event-serializer.ts`), so a snapshot is the same for a legacy event object and a modern `SyntheticEvent`, and a difference between the two systems fails the snapshot. Use `--selectProjects legacy` or `--selectProjects modern` to run one of them.
25
+
26
+
When a test expects different behavior in the two systems, e.g. events bubbling to a parent, branch on `getConfig().unstable_eventSystem` inside the test instead of skipping it.
27
+
28
+
Tests of legacy behavior the modern event system doesn't have (several handler arguments, bubbling to composite props, direct events bubbling with a warning, ...) call `runInLegacyEventSystem()` from `src/test-utils/event-system.ts` at the top of the file (or in a `describe()`). They run in the legacy event system in both projects, e.g. `src/events/legacy/__tests__/fire-event.test.tsx`.
0 commit comments