Skip to content

feat: experimental on-device Interactions panel and web-style canvas/userEvent behind features.ondeviceInteractions - #940

Draft
dannyhw wants to merge 6 commits into
nextfrom
feat/ondevice-interactions
Draft

dannyhw wants to merge 6 commits into
nextfrom
feat/ondevice-interactions

Conversation

@dannyhw

@dannyhw dannyhw commented Oct 24, 2026 •

Copy link
Copy Markdown
Member

Summary

Adds an experimental on-device Interactions panel, built into @storybook/react-native like the backgrounds panel, and web-style canvas and userEvent for play functions. Everything is behind a new features.ondeviceInteractions flag; with the flag off, behavior is unchanged.

// .rnstorybook/main.ts
features: {
  ondeviceInteractions: true,
},

Play functions are written the same way as on Storybook web:

import { expect } from '@storybook/react-native/test';

export const SignUp: Story = {
  play: async ({ canvas, userEvent }) => {
    await userEvent.type(canvas.getByLabelText('Name'), 'Ada Lovelace');
    await userEvent.type(canvas.getByPlaceholderText('you@example.com'), 'ada@example.com');
    await userEvent.click(canvas.getByRole('switch', { name: 'Accept the terms' }));
    await userEvent.click(canvas.getByText('Sign up'));

    await expect(await canvas.findByText(/^Signed up/)).toBeTruthy();
  },
};

and the panel lists each call, as on the web:

✓ userEvent.type(canvas.getByLabelText("Name"), "Ada Lovelace", {"delay":40})
✓ userEvent.type(canvas.getByPlaceholderText("you@example.com"), "ada@example.com", {"delay":40})
✓ userEvent.click(canvas.getByRole("switch", {"name":"Accept the terms"}))
✓ userEvent.click(canvas.getByText("Sign up"))
✓ canvas.findByText(/^Signed up/)
✓ expect(<RN:Paragraph>).toBeTruthy()

userEvent needs React Native's Canary release channel, where elements support EventTarget: React Native routes a dispatched change or click to the component's ordinary onChangeText, onValueChange and onPress props. The queries work on current stable versions.

@storybook/react-native/test

The React Native counterpart of storybook/test (src/test). It exports within, userEvent and waitFor, instrumented like their web versions, and re-exports expect, fn, spyOn and the mocking utilities from storybook/test.

  • Queries: ByRole, ByText, ByLabelText, ByPlaceholderText, ByDisplayValue, ByAltText and ByTestId, each with getBy/getAllBy/queryBy/queryAllBy/findBy/findAllBy. Matchers are strings, regexes or functions, with exact and normalizer; errors use Testing Library's wording. Roles come from role/accessibilityRole (mapped to ARIA names, e.g. header → heading), with implicit roles for TextInput, Switch and Image; the accessible name comes from aria-label/accessibilityLabel, aria-labelledby/accessibilityLabelledBy, then the text inside.
  • userEvent: click (presses the nearest pressable, so clicking a button's text works, and toggles switches), dblClick, type (appends one character at a time, with delay), clear, keyboard and setup(). Special keys, hover, tab and selectOptions aren't supported.
  • waitFor: polls, with Testing Library's defaults.

Uses React Native internals: element props (accessibilityRole, placeholder, value, …) aren't exposed by the element API, so getProps reads them from the fiber (__internalInstanceHandle.stateNode.canonical.currentProps, the path React Native's private getCurrentProps uses). It's the only place that does, and it throws a clear error if the path changes.

What the flag turns on

  • Interactions panel (src/interactions): lists the instrumented calls with their status and errors, shows PASS/FAIL/RUNS, and has a Rerun button that emits FORCE_REMOUNT. It records from when it registers, so the log is complete even if the panel opens after the play function ran.
  • canvas, userEvent and canvasElement: a mount annotation waits for the story wrapper View to attach, sets it as canvasElement, and adds instrumented canvas and userEvent to the context. mount() resolves with canvas, as on the web.
  • Play functions run without reactNative.playFn.
  • Rerun during a run: StoryRender.remount() waits for a running play function and then falls back to window.location.reload(), which never resolves on React Native. ReactNativePreview.onForceRemount cancels the run, waits for its STORY_RENDER_PHASE_CHANGED to aborted/errored/finished, then calls remount(). If several remounts arrive in quick succession, only the last one runs. The story wrapper's key includes a mount counter, so a remount renders the story fresh.
  • Synchronous WebSocket channel: with async: true, the instrumenter gets the playing phase change after the play function has started and drops its first calls.
  • Elements in instrumented calls: Storybook's instrumenter serializes any instanceof HTMLElement argument as a DOM element, reading localName, classList and innerText. React Native defines HTMLElement as its own element class, so userEvent.click(element) or expect(element) threw. The flag adds those three getters to the element prototype (localName is the tagName), so elements show as <RN:TextInput>. The instrumenter also walks the keys of objects returned from instrumented calls; it skips DOM elements, but React Native elements look like plain objects and walking their fiber overflowed the stack, so our instrument() calls return elements as they are.

Both instrumenter workarounds would be better fixed in Storybook core.

Example and docs

  • examples/expo-example enables the flag and adds PlayFunctionExample, which uses canvas queries, step and expect (the example is on React Native 0.86, without userEvent).
  • New docs page docs/intro/configuration/interactions.md, and the flag added to the feature flag tables.

Testing

  • src/test/helpers.test.ts: each query type, matcher options, error messages, findBy waiting, and userEvent typing (controlled and uncontrolled), keyboard, clear, clicking through to the pressable, switches, and the error when elements can't dispatch events.
  • src/interactions/interactions.test.ts: play functions get canvasElement, canvas and userEvent; Rerun during a running play function reruns it without calling window.location.reload (fails without the override); the panel keeps its log when the shown story is selected again; call formatting.
  • pnpm test, pnpm lint, pnpm format:check and pnpm check:types pass.
  • On device, React Native Canary (0.88, iOS 27 simulator): the helpers in this PR, copied into the starter repo, fill and submit a signup form through canvas and userEvent. The panel shows the calls above, the invalid-email story passes, and a missing element fails with Unable to find an element with the text: "Submit".
  • On device, expo example (React Native 0.86, iOS 26.4 simulator): PlayFunctionExample/Assertions passes with its canvas queries listed, Rerun works, and selecting the shown story again over the WebSocket channel keeps the log.

@changeset-bot

changeset-bot Bot commented Oct 24, 2026 •

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: fe57cf9

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 9 packages
Name Type
@storybook/react-native Minor
@storybook/addon-ondevice-actions Minor
@storybook/addon-ondevice-backgrounds Minor
@storybook/addon-ondevice-controls Minor
@storybook/addon-ondevice-notes Minor
@storybook/react-native-theming Minor
@storybook/react-native-ui-common Minor
@storybook/react-native-ui-lite Minor
@storybook/react-native-ui Minor

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

…book/react-native/test

Adds Testing Library-style queries, userEvent and waitFor for React Native elements, instrumented so their calls show in the Interactions panel. With ondeviceInteractions enabled, play functions get canvas and userEvent in their context, like on the web.

Also keeps the Interactions panel's log when the story that's already shown is selected again.
@dannyhw dannyhw changed the title feat: experimental on-device Interactions panel behind features.ondeviceInteractions feat: experimental on-device Interactions panel and web-style canvas/userEvent behind features.ondeviceInteractions Oct 24, 2026

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant