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

Build: Exclude server action stories to run in next 13 #29592

Merged
merged 1 commit into from
Nov 12, 2024
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
Original file line number Diff line number Diff line change
@@ -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 (
<div style={{ display: 'flex', gap: 8 }}>
<form>
<button type="submit" formAction={login}>
Login
</button>
</form>
<form>
<button type="submit" formAction={logout}>
Logout
</button>
</form>
<form>
<button type="submit" formAction={accessRoute}>
Access protected route
</button>
</form>
</div>
);
}

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.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

syntax: typo in 'suspress' - should be 'suppress'

// 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<typeof Component>;

type Story = StoryObj<typeof Component>;

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());
},
};
Original file line number Diff line number Diff line change
@@ -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');
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

style: Potential race condition - cookies() is called twice, once for await and once for get(). Use const cookieStore = await cookies() first


if (!user) {
redirect('/');
}

revalidatePath('/');
redirect(`/protected`);
Comment on lines +14 to +15
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

logic: Calling revalidatePath() right before redirect() is unnecessary since the redirect will trigger a full page load

}

export async function logout() {
(await cookies()).delete('user');
revalidatePath('/');
redirect('/');
}

export async function login() {
(await cookies()).set('user', 'storybookjs');
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

style: No expiration time set for cookie, could lead to indefinite sessions

revalidatePath('/');
redirect('/');
}
Loading