Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(remix-testing): stablize createRemixStub #7647

Merged
merged 8 commits into from
Oct 12, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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: ["sonnyday@fancymail.com", "surfergal@veryprofessional.org"],
};

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