Testing
Running your plugin end to end — Metro, a simulator, the app, and React Native
DevTools — is the right way to verify how a panel looks and feels. It is a slow
way to answer the question you ask far more often: do the panel and the
react-native.ts side still agree on the messages they exchange?
@rozenite/testing answers that one in milliseconds. It gives you both ends of
an in-memory channel, so your real panel code and your real React Native code
run against each other in Node — no Metro, no simulator, no DevTools.
Installation
Install it as a dev dependency alongside @rozenite/plugin-bridge:
The package works with any test runner — Vitest, Jest, or node --test. It
brings no runner globals of its own, so you keep using your runner's
assertions.
To render panel components you also need React and a renderer such as
React Testing Library,
plus a DOM environment (jsdom or happy-dom) configured in your runner. The
message-level and RPC tests below need neither.
Test the messages between both sides
connectFakePair() returns two ends of one channel: device and panel.
Whatever one end sends arrives at the other. Hand each end to a client with the
channel option, and both sides run their real communication code.
Messages are always delivered asynchronously, exactly as they are on a device.
A send() never reaches the other side's listener before the current tick
finishes, so assert on what arrived with await, never straight after
send().
Pass the same pluginId to both clients. Clients only receive messages
addressed to their own plugin, so a mismatched id looks exactly like a message
that never arrived.
Test an RPC method
RPC methods work over the same pair — register a handler on one side and call it from the other:
Errors thrown by a handler travel back to the caller, so expect(...).rejects
works on a failing call the same way it does in production.
Test your panel component
Panel components call useRozeniteDevToolsClient({ pluginId }) themselves and
take no channel prop — so wrap the component in RozeniteChannelProvider and
give it one end of the pair. Every useRozeniteDevToolsClient() inside the
provider uses that channel instead of connecting to DevTools, and the component
under test stays exactly as it ships.
role tells the provider which side of the protocol the subtree stands in for.
Only the React Native side announces itself with a plugin-mounted message, so
role="panel" keeps that message off the wire and role="device" puts it
there. Set it whenever a test asserts on the exact messages exchanged.
Test your React Native side
If your React Native integration is a hook, render it the same way with the other end of the pair:
If your integration is a plain function that takes a client, skip the provider
and pass it a client built with the channel option, as in the first example.
Wait for a message
Three helpers wait for something to arrive. Each takes a required timeoutMs
and rejects with a WaitForTimeoutError when nothing matching shows up, so a
broken protocol fails your test instead of hanging your suite.
Start waiting before you trigger the exchange when the reply can be immediate:
Simulate a slow or missing peer
A pair can drop or delay messages in either direction, which is how you cover the cases that are painful to reproduce on a device — a panel that was never opened, or a device that answers slowly.
Both take effect immediately and stay in force until you change them, so you
can drop messages part-way through a test and turn delivery back on with
dropDeviceToPanel(false).
Delays use timers. If your test uses fake timers, advance them to let a delayed message through.
When a test times out
A WaitForTimeoutError means nothing matching arrived in time. The usual
causes, in the order worth checking:
- The two clients use different plugin ids. Messages are addressed by plugin id, and one that doesn't match is silently ignored.
- The message type or payload doesn't match your predicate. Drop the predicate first to confirm the message arrives at all.
- The other side was never wired up. Register handlers on the device client before the panel sends anything.
- A client was closed too early. Closing a client also stops delivery for any other client sharing that same end of the pair. Give each side its own pair if they need independent lifetimes.
- A drop or delay is still in force from an earlier step in the test.
Raise timeoutMs last. It is rarely the answer: everything here runs
in-process, so a message that hasn't arrived in a second is not on its way.
What this doesn't cover
These tests prove the two sides agree on the messages they exchange. They say nothing about how your panel looks, whether DevTools loads your plugin, or how your code behaves against a real device — for that, run the plugin in the development workflow and check it by hand before you release.
