Skip to content

Commit

Permalink
feat: stablize createRemixStub (#7647)
Browse files Browse the repository at this point in the history
Co-authored-by: Matt Brophy <[email protected]>
Co-authored-by: Mehdi Achour <[email protected]>
  • Loading branch information
3 people authored Oct 12, 2023
1 parent a71d5c9 commit 2a3ad8c
Show file tree
Hide file tree
Showing 7 changed files with 285 additions and 63 deletions.
7 changes: 7 additions & 0 deletions .changeset/stabilize-create-remix-stub.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"remix": minor
"@remix-run/testing": minor
---

Remove the `unstable_` prefix from `createRemixStub`. After real-world experience, we're confident in the API and ready to commit to it.
* Note: This involves 1 small breaking change. The `<RemixStub remixConfigFuture>` prop has been renamed to `<RemixStub future>`
78 changes: 78 additions & 0 deletions docs/other-api/testing.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
---
title: "@remix-run/testing"
---

# `@remix-run/testing`

This package contains utilities to assist in unit testing portions of your Remix application. This is accomplished by mocking the Remix route modules/assets manifest output by the compiler and generating an in-memory React Router app via [createMemoryRouter][memory-router].

The general usage of this is to test components/hooks that rely on Remix hooks/components which you do not have the ability to cleanly mock (`useLoaderData`, `useFetcher`, etc.). While it can also be used for more advanced testing such as clicking links and navigating to pages, those are better suited for End to End tests via something like [Cypress][cypress] or [Playwright][playwright].

## Usage

To use `createRemixStub`, define your routes using React Router-like route objects, where you specify the `path`, `Component`, `loader`, etc. These are essentially mocking the nesting and exports of the route files in your Remix app:

```tsx
const RemixStub = createRemixStub([
{
path: "/",
Component: MyComponent,
loader() {
return json({ message: "hello" });
},
},
]);
```

Then you can render the `<RemixStub />` component and assert against it:

```tsx
render(<RemixStub />);
await waitFor(() =>
screen.findByText("Some rendered text")
);
```

## Example

Here's a full working example testing using [`jest`][jest] and [React Testing Library][rtl]:

```tsx
import { json } from "@remix-run/node";
import { useLoaderData } from "@remix-run/react";
import { createRemixStub } from "@remix-run/testing";
import {
render,
screen,
waitFor,
} from "@testing-library/react";
import * as React from "react";

test("renders loader data", async () => {
// ⚠️ This would usually be a component you import from your app code
function MyComponent() {
const data = useLoaderData() as { message: string };
return <p>Message: {data.message}</p>;
}

const RemixStub = createRemixStub([
{
path: "/",
Component: MyComponent,
loader() {
return json({ message: "hello" });
},
},
]);

render(<RemixStub />);

await waitFor(() => screen.findByText("Message: hello"));
});
```

[memory-router]: https://reactrouter.com/en/main/routers/create-memory-router
[cypress]: https://www.cypress.io/
[playwright]: https://playwright.dev/
[rtl]: https://testing-library.com/docs/react-testing-library/intro/
[jest]: https://jestjs.io/
82 changes: 82 additions & 0 deletions docs/utils/create-remix-stub.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
---
title: createRemixStub
---

# `createRemixStub`

This utility allows you to unit-test your own components that rely on Remix hooks/components by setting up a mocked set of routes:

```tsx
test("renders loader data", async () => {
const RemixStub = createRemixStub([
{
path: "/",
meta() {
/* ... */
},
links() {
/* ... */
},
Component: MyComponent,
ErrorBoundary: MyErrorBoundary,
action() {
/* ... */
},
loader() {
/* ... */
},
},
]);

render(<RemixStub />);

// Assert initial render
await waitFor(() => screen.findByText("..."));

// Click a button and assert a UI change
user.click(screen.getByText("button text"));
await waitFor(() => screen.findByText("..."));
});
```

If your loaders rely on the `getLoadContext` method, you can provide a stubbed context via the second parameter to `createRemixStub`:

```tsx
const RemixStub = createRemixStub(
[
{
path: "/",
Component: MyComponent,
loader({ context }) {
return json({ message: context.key });
},
},
],
{ key: "value" }
);
```

The `<RemixStub>` component itself takes properties similar to React Router if you need to control the initial URL, history stack, hydration data, or future flags:

```tsx
// Test the app rendered at "/2" with 2 prior history stack entries
render(
<RemixStub
initialEntries={["/", "/1", "/2"]}
initialIndex={2}
/>
);

// Test the app rendered with initial loader data for the root route. When using
// this, it's best to give your routes their own unique IDs in your route definitions
render(
<RemixStub
hydrationData={{
loaderData: { root: { message: "hello" } },
}}
/>
);

// Test the app rendered with given future flags enabled
render(<RemixStub future={{ v3_coolFeature: true }} />);
```
22 changes: 11 additions & 11 deletions packages/remix-react/__tests__/integration/meta-test.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Meta, Outlet } from "@remix-run/react";
import { unstable_createRemixStub } from "@remix-run/testing";
import { createRemixStub } from "@remix-run/testing";
import { prettyDOM, render, screen } from "@testing-library/react";
import user from "@testing-library/user-event";
import * as React from "react";
Expand All @@ -9,7 +9,7 @@ const getHtml = (c: HTMLElement) =>

describe("meta", () => {
it("no meta export renders meta from nearest route meta in the tree", () => {
let RemixStub = unstable_createRemixStub([
let RemixStub = createRemixStub([
{
id: "root",
path: "/",
Expand Down Expand Up @@ -66,7 +66,7 @@ describe("meta", () => {
});

it("empty meta array does not render a tag", () => {
let RemixStub = unstable_createRemixStub([
let RemixStub = createRemixStub([
{
path: "/",
meta: () => [],
Expand All @@ -93,7 +93,7 @@ describe("meta", () => {
});

it("meta from `matches` renders meta tags", () => {
let RemixStub = unstable_createRemixStub([
let RemixStub = createRemixStub([
{
id: "root",
path: "/",
Expand Down Expand Up @@ -141,7 +141,7 @@ describe("meta", () => {
});

it("{ charSet } adds a <meta charset='utf-8' />", () => {
let RemixStub = unstable_createRemixStub([
let RemixStub = createRemixStub([
{
path: "/",
meta: () => [{ charSet: "utf-8" }],
Expand All @@ -161,7 +161,7 @@ describe("meta", () => {
});

it("{ title } adds a <title />", () => {
let RemixStub = unstable_createRemixStub([
let RemixStub = createRemixStub([
{
path: "/",
meta: () => [{ title: "Document Title" }],
Expand All @@ -181,7 +181,7 @@ describe("meta", () => {
});

it("{ property: 'og:*', content: '*' } adds a <meta property='og:*' />", () => {
let RemixStub = unstable_createRemixStub([
let RemixStub = createRemixStub([
{
path: "/",
meta: () => [
Expand Down Expand Up @@ -221,7 +221,7 @@ describe("meta", () => {
email: ["[email protected]", "[email protected]"],
};

let RemixStub = unstable_createRemixStub([
let RemixStub = createRemixStub([
{
path: "/",
meta: () => [
Expand All @@ -244,7 +244,7 @@ describe("meta", () => {
});

it("{ tagName: 'link' } adds a <link />", () => {
let RemixStub = unstable_createRemixStub([
let RemixStub = createRemixStub([
{
path: "/",
meta: () => [
Expand All @@ -270,7 +270,7 @@ describe("meta", () => {
});

it("does not mutate meta when using tagName", async () => {
let RemixStub = unstable_createRemixStub([
let RemixStub = createRemixStub([
{
path: "/",
meta: ({ data }) => data?.meta,
Expand Down Expand Up @@ -329,7 +329,7 @@ describe("meta", () => {
});

it("loader errors are passed to meta", () => {
let RemixStub = unstable_createRemixStub([
let RemixStub = createRemixStub([
{
path: "/",
Component() {
Expand Down
Loading

0 comments on commit 2a3ad8c

Please sign in to comment.