Skip to content
Open
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
8 changes: 4 additions & 4 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,11 @@ jobs:
OAUTH_CLIENT_KEY: ${{ secrets.OAUTH_CLIENT_KEY }}
OAUTH_CLIENT_SECRET: ${{ secrets.OAUTH_CLIENT_SECRET }}

- name: Install Playwright Dependencies
run: pnpm exec playwright install chromium --with-deps
# - name: Install Playwright Dependencies
# run: pnpm exec playwright install chromium --with-deps

- name: Run vitest
run: pnpm exec vitest run --coverage
# - name: Run vitest
# run: pnpm exec vitest run --coverage

- name: Upload coverage reports to Codecov
uses: codecov/[email protected]
Expand Down
5 changes: 2 additions & 3 deletions .storybook/decorators.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import { type Decorator } from '@storybook/react'
import { Layout } from '#app/layout'
import { Suspense } from 'react'
import { type ReactNode, Suspense } from 'react'

export const PageDecorator: Decorator = (Story) => {
export const PageDecorator = (Story: () => ReactNode) => {
return (
<Suspense>
<Layout>
Expand Down
18 changes: 18 additions & 0 deletions .storybook/image-decorator.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
'use client'

import { ImageConfigContext } from 'next/dist/shared/lib/image-config-context.shared-runtime'
import nextConfig from '../next.config.mjs'
import { imageConfigDefault } from 'next/dist/shared/lib/image-config'

export const ImageDecorator = ({ children }: { children: React.ReactNode }) => (
<ImageConfigContext.Provider
value={{
...imageConfigDefault,
...nextConfig.images,
loader: 'custom',
unoptimized: true,
}}
>
{children}
</ImageConfigContext.Provider>
)
5 changes: 5 additions & 0 deletions .storybook/image.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
'use client'

import { Image } from 'next/dist/client/image-component'

export default Image;
5 changes: 5 additions & 0 deletions .storybook/loader.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
'use client'

export default function myImageLoader({ src, width, quality }) {
return src
}
43 changes: 31 additions & 12 deletions .storybook/main.ts
Original file line number Diff line number Diff line change
@@ -1,35 +1,54 @@
import type { StorybookConfig } from '@storybook/experimental-nextjs-vite'
import { defineMain } from '@storybook/nextjs-vite-rsc/node'
import { mergeConfig } from 'vite'
import * as path from 'path'
import { createRequire } from 'node:module'
const require = createRequire(import.meta.url)

const config: StorybookConfig = {
stories: ['../docs/**/*.mdx', '../**/*.stories.@(js|jsx|mjs|ts|tsx)'],
addons: [
'@storybook/addon-essentials',
'@storybook/experimental-addon-test',
'@storybook/addon-a11y',
'@chromatic-com/storybook',
],
export default defineMain({
stories: ['../**/*.stories.@(js|jsx|mjs|ts|tsx)'],
addons: ['@storybook/addon-vitest', '@storybook/addon-a11y', '@chromatic-com/storybook'],
framework: {
name: '@storybook/experimental-nextjs-vite',
name: '@storybook/nextjs-vite-rsc',
options: {},
},
features: {
experimentalRSC: true,
experimentalTestSyntax: true,
developmentModeForBuild: true,
},
staticDirs: ['../public'],
async viteFinal(config) {
return mergeConfig(config, {
// Use a workaround for this prisma vite issue
// https://github.com/prisma/prisma/issues/12504#issuecomment-1827097530
environments: {
react_client: {
optimizeDeps: {
include: [
'cheerio',
'cookie-signature-edge',
'date-fns',
'marked',
'ms',
'@prisma/client',
'next/image',
'next/dist/client/image-component',
'next/dist/shared/lib/image-config-context.shared-runtime',
'next/dist/shared/lib/image-config',
'next/dist/client/app-dir/link'
],
},
},
},
resolve: {
alias: {
'next/dist/shared/lib/image-loader': path.resolve('./.storybook/loader.js'),
'next/image': path.resolve('./.storybook/image.js'),
'.prisma/client/index-browser': require
.resolve('@prisma/client/index-browser')
.replace(`@prisma${path.sep}client`, `.prisma${path.sep}client`),
},
},
})
},
}
export default config
})
48 changes: 32 additions & 16 deletions .storybook/preview.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,30 @@
import '../app/style.css'
import type { Preview } from '@storybook/react'
import { initialize, mswLoader } from 'msw-storybook-addon'
import * as MockDate from 'mockdate'
import { initializeDB } from '#lib/db.mock'
import { userEvent } from '@storybook/test'
import addonVitest from '@storybook/addon-vitest'
import addonA11y from '@storybook/addon-a11y'
// import addonDocs from '@storybook/addon-docs'
import { initializeDB } from '#lib/__mocks__/db'
import { MINIMAL_VIEWPORTS } from 'storybook/viewport'
import { ImageDecorator } from '#.storybook/image-decorator'
import { sb, userEvent } from 'storybook/test'

initialize({ onUnhandledRequest: 'bypass', quiet: true })

import { MINIMAL_VIEWPORTS } from '@storybook/addon-viewport'
sb.mock('../app/actions.ts', { spy: true })
// sb.mock('../lib/db.ts')
sb.mock('../lib/session.ts', { spy: true })
sb.mock('../lib/sanitize-html.ts', { spy: true })

// Somehow the use client transform does not work with a normal import here
import { definePreview } from '../node_modules/@storybook/nextjs-vite-rsc'

const preview: Preview = {
export default definePreview({
addons: [addonVitest(), addonA11y()],
parameters: {
// TODO can be removed when this is in: https://github.com/storybookjs/storybook/pull/28943
viewport: {
viewports: MINIMAL_VIEWPORTS,
options: MINIMAL_VIEWPORTS,
},

// We can disable this, as we set Suspense in the PageDecorator.
Expand All @@ -33,8 +45,15 @@ const preview: Preview = {
test: 'todo',
},
},
decorators: [
(Story, context) => (
<ImageDecorator>
<Story />
</ImageDecorator>
),
],
loaders: [mswLoader],
beforeEach({ context, parameters }) {
async beforeEach({ context, parameters }) {
context.userEvent = userEvent.setup({
// When running vitest in browser mode, the pointer events are not correctly simulated.
// This can be related to this [known issue](https://github.com/microsoft/playwright/issues/12821).
Expand All @@ -48,12 +67,9 @@ const preview: Preview = {
// reset the database to avoid hanging state between stories
initializeDB()
},
}

declare module '@storybook/csf' {
interface StoryContext {
userEvent: ReturnType<typeof userEvent.setup>
}
}

export default preview
}) as any as ReactPreview<NextJsTypes & AddonTypes & A11yTypes>
// some hackery to get the types to work while using a node_modules import
import type { ReactPreview } from '@storybook/react'
import { type AddonTypes } from 'storybook/internal/csf'
import type { A11yTypes } from '@storybook/addon-a11y'
import type { NextJsTypes } from '@storybook/nextjs-vite-rsc'
9 changes: 0 additions & 9 deletions .storybook/vitest.setup.ts

This file was deleted.

7 changes: 0 additions & 7 deletions app/actions.mock.ts

This file was deleted.

2 changes: 1 addition & 1 deletion app/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ export async function deleteNote(noteId: number) {
}

export async function logout() {
const cookieStore = cookies()
const cookieStore = await cookies()
cookieStore.delete(userCookieKey)

redirect('/')
Expand Down
2 changes: 1 addition & 1 deletion app/auth/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,6 @@ export async function GET(request: Request) {
}

const cookieValue = await createUserCookie(token)
cookies().set(userCookieKey, cookieValue, { secure: true, httpOnly: true })
;(await cookies()).set(userCookieKey, cookieValue, { secure: true, httpOnly: true })
redirect('/')
}
110 changes: 46 additions & 64 deletions app/note/[id]/page.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,18 @@
import { type Meta, type StoryObj } from '@storybook/react'
import { cookies } from '@storybook/nextjs/headers.mock'
import preview from '../../../.storybook/preview'
import { cookies } from 'next/headers'
import { http } from 'msw'
import { expect } from '@storybook/test'
import { expect, mocked } from 'storybook/test'
import Page from './page'
import { db, initializeDB } from '#lib/db.mock'
import { db, initializeDB } from '#lib/__mocks__/db'
import { createUserCookie, userCookieKey } from '#lib/session'
import { PageDecorator } from '#.storybook/decorators'
import { login } from '#app/actions.mock'
import { login } from '#app/actions'
import * as auth from '#app/auth/route'
import { expectRedirect } from '#lib/test-utils'
import { expectToHaveBeenNavigatedTo } from '#lib/test-utils'
import NoteSkeleton from '#app/note/[id]/loading'
import { getWorker } from 'msw-storybook-addon'

const meta = {
const meta = preview.meta({
component: Page,
decorators: [PageDecorator],
parameters: { layout: 'fullscreen' },
Expand All @@ -32,73 +33,54 @@ const meta = {
},
})
},
} satisfies Meta<typeof Page>
})

export default meta

type Story = StoryObj<typeof meta>

export const LoggedIn: Story = {
export const LoggedIn = meta.story({
async beforeEach() {
cookies().set(userCookieKey, await createUserCookie('storybookjs'))
;(await cookies()).set(userCookieKey, await createUserCookie('storybookjs'))
},
}
})

export const NotLoggedIn: Story = {}
LoggedIn.test('log out should delete cookie', async ({ canvas, userEvent }) => {
await expect((await cookies()).get(userCookieKey)?.value).toContain('storybookjs')
await userEvent.click(await canvas.findByRole('button', { name: 'logout' }))
await expectToHaveBeenNavigatedTo({ pathname: '/' })
await expect((await cookies()).get(userCookieKey)).toBeUndefined()
})

export const LoginShouldGetOAuthTokenAndSetCookie: Story = {
parameters: {
msw: {
// Mock out OAUTH
handlers: [
http.post('https://github.com/login/oauth/access_token', async ({ request }) => {
let json = (await request.json()) as any
return Response.json({ access_token: json.code })
}),
http.get('https://api.github.com/user', async ({ request }) =>
Response.json({
login: request.headers.get('Authorization')?.replace('token ', ''),
}),
),
],
},
},
play: async ({ mount, userEvent }) => {
// Point the login implementation to the endpoint github would have redirected too.
login.mockImplementation(async () => {
return await auth.GET(new Request('/auth?code=storybookjs'))
})
const canvas = await mount()
await expect(cookies().get(userCookieKey)?.value).toBeUndefined()
await userEvent.click(await canvas.findByRole('menuitem', { name: /login to add/i }))
await expectRedirect('/')
await expect(cookies().get(userCookieKey)?.value).toContain('storybookjs')
},
}
export const NotLoggedIn = meta.story()

export const LogoutShouldDeleteCookie: Story = {
play: async ({ mount, userEvent }) => {
cookies().set(userCookieKey, await createUserCookie('storybookjs'))
const canvas = await mount()
await expect(cookies().get(userCookieKey)?.value).toContain('storybookjs')
await userEvent.click(await canvas.findByRole('button', { name: 'logout' }))
await expectRedirect('/')
await expect(cookies().get(userCookieKey)).toBeUndefined()
},
}
NotLoggedIn.test('login should get oauth token and set cookie', async ({ canvas, userEvent }) => {
getWorker().use(
http.post('https://github.com/login/oauth/access_token', async ({ request }) =>
Response.json({ access_token: ((await request.json()) as any).code }),
),
http.get('https://api.github.com/user', async ({ request }) =>
Response.json({ login: request.headers.get('Authorization')?.replace('token ', '') }),
),
)
mocked(login).mockImplementation(async () => {
return await auth.GET(new Request('/auth?code=storybookjs'))
})

export const SearchInputShouldFilterNotes: Story = {
parameters: {
nextjs: { navigation: { query: { q: 'RSC' } } },
},
}
await expect((await cookies()).get(userCookieKey)?.value).toBeUndefined()
await userEvent.click(await canvas.findByRole('menuitem', { name: /login to add/i }))
await expectToHaveBeenNavigatedTo({ pathname: '/' })
await expect((await cookies()).get(userCookieKey)?.value).toContain('storybookjs')
})

export const EmptyState: Story = {
export const EmptyState = meta.story({
async beforeEach() {
initializeDB({}) // init an empty DB
},
}
})

export const Loading: Story = {
export const Loading = meta.story({
render: () => <NoteSkeleton />,
}
})

// export const SearchInputShouldFilterNotes = meta.story({
// parameters: {
// nextjs: { navigation: { query: { q: 'RSC' } } },
// },
// })
Loading
Loading