Skip to content

Testing

In jsdom and happy-dom there is no real page on the other side of an iframe, so nothing connects. react-iframe-kit/testing plays the missing side over the same protocol as the real one:

  • mockChild is the page inside the iframe, for testing the parent: useIframeRPC, useIframeEvent, useIframeResize, useIframeTitle.
  • mockParent is the page around it, for testing the child: connectToParent, useParent, useParentEvent.
import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { mockChild } from 'react-iframe-kit/testing';
import type { ChildSide, ParentSide } from './contract';
test('pays through the widget', async () => {
render(<Checkout />);
const child = mockChild<ParentSide, ChildSide>(screen.getByTitle('Payment'), {
methods: { pay: async (amount) => ({ ok: amount > 0 }) },
});
await userEvent.click(screen.getByRole('button', { name: 'Pay' }));
await waitFor(() => expect(screen.getByText('Paid')).toBeInTheDocument());
child.dispose();
});

Pass the parent’s side first, like connectToParent. The iframe must be in the document; mockChild can be created before or after your component connects.

remote The parent’s methods, e.g. await child.remote.getUser().
emit(name, payload?) Sends one of the page’s events to the parent.
on(name, handler) Listens to the parent’s events; returns the unsubscribe function.
resize({ width, height }) Reports a content size, as autoResize would.
setTitle(title) Reports a page title, as syncTitle would.
inert Whether the parent currently asks the page to be inert (useIframeInert).
whenConnected() Resolves once the handshake completes.
dispose() Unloads the page: calls still pending on the parent reject with RIK_CONNECTION_LOST.
status 'connecting', 'connected' or 'disposed'.

Options: methods, origin (default: the origin of the iframe’s src), timeout, connectTimeout.

import { render, screen, waitFor } from '@testing-library/react';
import { mockParent } from 'react-iframe-kit/testing';
import type { ChildSide, ParentSide } from './contract';
test('loads the user from the host page', async () => {
const parent = mockParent<ChildSide, ParentSide>({
origin: 'https://app.example.com', // must be in the page's allowedOrigins
methods: { getUser: () => ({ name: 'Ann' }) },
});
render(<Widget />);
await waitFor(() => expect(screen.getByText('Hello, Ann')).toBeInTheDocument());
await expect(parent.remote.reset()).resolves.toBe(true);
parent.dispose();
});

Pass the page’s side first, like useIframeRPC. Create it before the code under test connects: it makes the test page look framed, and a connection started before that stays 'idle'.

remote The page’s methods.
emit(name, payload?) Sends one of the parent’s events to the page.
on(name, handler) Listens to the page’s events; returns the unsubscribe function.
size The last size the page reported with autoResize, or null.
title The last title the page reported with syncTitle, or null.
whenConnected() Resolves once the handshake completes.
dispose() Goes away like an unmounting parent: the page’s pending calls reject with RIK_CONNECTION_LOST, and the page is no longer framed.
status 'connecting', 'connected' or 'disposed'.

Options: methods, origin (default: the test page’s origin), timeout, connectTimeout.

The page’s connection lives for the whole test file. After dispose(), the next mockParent reconnects it, so each test can create its own.

Both need a global MessageChannel, which Node and Vitest’s jsdom and happy-dom environments provide. mockParent adds a hidden <iframe> to the document while it exists.

In happy-dom, set navigation: { disableChildFrameNavigation: true } so an iframe with a remote src isn’t fetched. Don’t use disableIframePageLoading: it leaves iframes without a window, and both mocks need one.