diff --git a/code/frameworks/nextjs/template/stories/ServerActions.stories.tsx b/code/frameworks/nextjs/template/stories_nextjs-default-ts/ServerActions.stories.tsx similarity index 100% rename from code/frameworks/nextjs/template/stories/ServerActions.stories.tsx rename to code/frameworks/nextjs/template/stories_nextjs-default-ts/ServerActions.stories.tsx diff --git a/code/frameworks/nextjs/template/stories/server-actions.tsx b/code/frameworks/nextjs/template/stories_nextjs-default-ts/server-actions.tsx similarity index 100% rename from code/frameworks/nextjs/template/stories/server-actions.tsx rename to code/frameworks/nextjs/template/stories_nextjs-default-ts/server-actions.tsx diff --git a/code/frameworks/nextjs/template/stories_nextjs-prerelease/ServerActions.stories.tsx b/code/frameworks/nextjs/template/stories_nextjs-prerelease/ServerActions.stories.tsx new file mode 100644 index 000000000000..f1a9ad762eed --- /dev/null +++ b/code/frameworks/nextjs/template/stories_nextjs-prerelease/ServerActions.stories.tsx @@ -0,0 +1,118 @@ +import React from 'react'; + +import { revalidatePath } from '@storybook/nextjs/cache.mock'; +import { cookies } from '@storybook/nextjs/headers.mock'; +import { getRouter, redirect } from '@storybook/nextjs/navigation.mock'; +import type { Meta, StoryObj } from '@storybook/react'; +import { expect, userEvent, waitFor, within } from '@storybook/test'; + +import { accessRoute, login, logout } from './server-actions'; + +function Component() { + return ( +
+
+ +
+
+ +
+
+ +
+
+ ); +} + +export default { + component: Component, + tags: ['!test'], + parameters: { + nextjs: { + appDirectory: true, + navigation: { + pathname: '/', + }, + }, + test: { + // This is needed until Next will update to the React 19 beta: https://github.com/vercel/next.js/pull/65058 + // In the React 19 beta ErrorBoundary errors (such as redirect) are only logged, and not thrown. + // We will also suspress console.error logs for re the console.error logs for redirect in the next framework. + // Using the onCaughtError react root option: + // react: { + // rootOptions: { + // onCaughtError(error: unknown) { + // if (isNextRouterError(error)) return; + // console.error(error); + // }, + // }, + // See: code/frameworks/nextjs/src/preview.tsx + dangerouslyIgnoreUnhandledErrors: true, + }, + }, +} as Meta; + +type Story = StoryObj; + +export const ProtectedWhileLoggedOut: Story = { + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + await userEvent.click(canvas.getByText('Access protected route')); + + await expect(cookies().get).toHaveBeenCalledWith('user'); + await expect(redirect).toHaveBeenCalledWith('/'); + + await waitFor(() => expect(getRouter().push).toHaveBeenCalled()); + }, +}; + +export const ProtectedWhileLoggedIn: Story = { + beforeEach() { + cookies().set('user', 'storybookjs'); + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + await userEvent.click(canvas.getByText('Access protected route')); + + await expect(cookies().get).toHaveBeenLastCalledWith('user'); + await expect(revalidatePath).toHaveBeenLastCalledWith('/'); + await expect(redirect).toHaveBeenLastCalledWith('/protected'); + + await waitFor(() => expect(getRouter().push).toHaveBeenCalled()); + }, +}; + +export const Logout: Story = { + beforeEach() { + cookies().set('user', 'storybookjs'); + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + + await userEvent.click(canvas.getByText('Logout')); + await expect(cookies().delete).toHaveBeenCalled(); + await expect(revalidatePath).toHaveBeenCalledWith('/'); + await expect(redirect).toHaveBeenCalledWith('/'); + + await waitFor(() => expect(getRouter().push).toHaveBeenCalled()); + }, +}; + +export const Login: Story = { + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + await userEvent.click(canvas.getByText('Login')); + + await expect(cookies().set).toHaveBeenCalledWith('user', 'storybookjs'); + await expect(revalidatePath).toHaveBeenCalledWith('/'); + await expect(redirect).toHaveBeenCalledWith('/'); + + await waitFor(() => expect(getRouter().push).toHaveBeenCalled()); + }, +}; diff --git a/code/frameworks/nextjs/template/stories_nextjs-prerelease/server-actions.tsx b/code/frameworks/nextjs/template/stories_nextjs-prerelease/server-actions.tsx new file mode 100644 index 000000000000..6244f78d2472 --- /dev/null +++ b/code/frameworks/nextjs/template/stories_nextjs-prerelease/server-actions.tsx @@ -0,0 +1,28 @@ +'use server'; + +import { revalidatePath } from 'next/cache'; +import { cookies } from 'next/headers'; +import { redirect } from 'next/navigation'; + +export async function accessRoute() { + const user = (await cookies()).get('user'); + + if (!user) { + redirect('/'); + } + + revalidatePath('/'); + redirect(`/protected`); +} + +export async function logout() { + (await cookies()).delete('user'); + revalidatePath('/'); + redirect('/'); +} + +export async function login() { + (await cookies()).set('user', 'storybookjs'); + revalidatePath('/'); + redirect('/'); +}