Remix の Await コンポーネントをテストする

https://remix.run/docs/en/main/guides/streaming

上記の <Product /> コンポーネントを例に考えてみる。Promise の状態に応じて、次の 3 つのテストを書く。

  1. pending → Suspense の fallback が表示される
  2. fulfilled → Await の children が表示される
  3. rejected → Await の errorElement が表示される

まず 1 から書いてみる。ポイントは、reviews に渡す Promise を resolve も reject もしないところ。

function loader() {
	return defer({
		product: mockProduct,
		// arrange: 解決しない Promise を返す
		reviews: new Promise<Review[]>(function noop() {})
	})
}

const RemixStub = createRemixStub([
	{ path: "/", loader, Component: Product }
])

test("pending の間、 <PreviewSkeleton /> を表示する", () => {
	render(<RemixStub />)

	// assert: <ReviewsSkeleton /> が存在する
	expect(await screen.findByTestId('reviews-skeleton')).toBeInTheDocument()
})

続いて 2。

function loader() {
	return defer({
		product: mockProduct,
		
		// arrange: Promise<Review[]> を返す
		reviews: Promise.resolve(mockReviews)
	})
} 

const RemixStub = createRemixStub([
	{ path: "/", loader, Component: Product }
])

test("リクエスト完了後 <ProductReviews /> を表示する", () => {
	render(<RemixStub />)

	expect(await screen.findByTestId('product-reviews')).toBeInTheDocument()
})

最後に 3。これは Await コンポーネントの props の errorElement に <ReviewsError /> を指定した場合のテストをイメージしている(例: <Await resolve={reviews} errorElement={<ReviewsError />}>...</Await>)。

function loader() {
	return defer({
		product: mockProduct,

		// arrange: サーバーエラーをシミュレート
		reviews: Promise.reject(
			Response.json({ message: "something happened" }, { status: 500 }),
		)
	})
} 

const RemixStub = createRemixStub([
	{ path: "/", loader, Component: Product }
])

test("例外発生時に <ReviewsError /> を表示する", () => {
	render(<RemixStub />)

	// assert: <ReviewsError /> が表示されている
	expect(await screen.findByTestId('reviews-error')).toBeInTheDocument()
})

補足。先ほどのドキュメントのコード例のように errorElement の指定を省略すると、エラー発生時には ErrorBoundary が表示される。これをテストするときは、createRemixStub に ErrorBoundary も渡すようにする。

// 前略

const RemixStub = createRemixStub([
	{ path: "/", loader, Component: Product, ErrorBoundary }
])

// 後略

No comments yet