Skip to content

Commit

Permalink
Merge branch 'main' into kasper/redirect-boundary-and-fixes
Browse files Browse the repository at this point in the history
  • Loading branch information
yannbf committed May 8, 2024
2 parents 614d575 + f50ac23 commit 56781ea
Show file tree
Hide file tree
Showing 13 changed files with 1,574 additions and 879 deletions.
12 changes: 12 additions & 0 deletions .babelrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"presets": [
[
"next/babel",
{
"preset-env": {
"targets": { "chrome": 117 }
}
}
]
]
}
28 changes: 28 additions & 0 deletions .storybook/test-runner.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { TestRunnerConfig, getStoryContext } from '@storybook/test-runner';
import { MINIMAL_VIEWPORTS } from '@storybook/addon-viewport';

const DEFAULT_VIEWPORT_SIZE = { width: 1280, height: 720 };

const config: TestRunnerConfig = {
async preVisit(page, story) {
const context = await getStoryContext(page, story);
const viewportName = context.parameters?.viewport?.defaultViewport;
const viewportParameter = MINIMAL_VIEWPORTS[viewportName];

if (viewportParameter) {
const viewportSize = Object.entries(viewportParameter.styles).reduce(
(acc, [screen, size]) => ({
...acc,
// make sure your viewport config in Storybook only uses numbers, not percentages
[screen]: parseInt(size),
}),
{}
);

page.setViewportSize(viewportSize);
} else {
page.setViewportSize(DEFAULT_VIEWPORT_SIZE);
}
},
};
export default config;
2 changes: 1 addition & 1 deletion app/note/[id]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React from 'react'
import NoteUI from '#components/note-ui'
import { db } from '#lib/db'
import React from 'react'

export const metadata = {
robots: { index: false },
Expand Down
56 changes: 46 additions & 10 deletions app/note/edit/[id]/page.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import Page from './page'
import { createUserCookie, userCookieKey } from '#lib/session'
import { PageDecorator } from '#.storybook/decorators'
import { db } from '#lib/db'
import { expect, fireEvent, userEvent, within } from '@storybook/test'
import { saveNote } from '#app/actions.mock'

const meta = {
component: Page,
Expand All @@ -25,20 +27,54 @@ const meta = {
},
})
},
parameters: {
layout: 'fullscreen',
nextjs: {
navigation: {
pathname: '/note/edit/[id]',
query: { id: '2' },
},
},
},
args: { params: { id: '2' } },
} satisfies Meta<typeof Page>

export default meta

type Story = StoryObj<typeof meta>

export const Default: Story = {}
export const EditNote: Story = {}

export const UnknownId: Story = {
args: { params: { id: '999' } },
}

export const Save: Story = {
name: 'Save and Delete Flow ▶',
play: async ({ canvasElement, step }) => {
const canvas = within(canvasElement)
const titleInput = await canvas.findByRole('textbox', {
name: /Enter a title for your note/i,
})
const bodyInput = canvas.getByRole('textbox', { name: /body/i })

await step('Clear inputs', async () => {
await userEvent.clear(titleInput)
await userEvent.clear(bodyInput)
})

await step('Edit inputs', async () => {
await fireEvent.change(titleInput, { target: { value: 'Edited Title' } })
await fireEvent.change(bodyInput, { target: { value: 'Edited Body' } })
})

await step('Save', async () => {
const saveButton = canvas.getByRole('menuitem', { name: /done/i })
await userEvent.click(saveButton)
await expect(saveNote).toHaveBeenCalledOnce()
await expect(saveNote).toHaveBeenCalledWith(
'2',
'Edited Title',
'Edited Body',
)
})

await step('Delete', async () => {
const deleteButton = canvas.getByRole('menuitem', { name: /delete/i })
await userEvent.click(deleteButton)
await expect(deleteNote).toHaveBeenCalledOnce()
await expect(deleteNote).toHaveBeenCalledWith('2')
})
},
}
36 changes: 35 additions & 1 deletion app/note/edit/page.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import Page from './page'
import { createUserCookie, userCookieKey } from '#lib/session'
import { PageDecorator } from '#.storybook/decorators'
import { db } from '#lib/db.mock'
import { expect, fireEvent, userEvent, within } from '@storybook/test'
import { saveNote } from '#app/actions.mock'

const meta = {
component: Page,
Expand Down Expand Up @@ -39,4 +41,36 @@ export default meta

type Story = StoryObj<typeof meta>

export const Default: Story = {}
export const EditNewNote: Story = {}

export const Save: Story = {
name: 'Save New Flow ▶',
play: async ({ canvasElement, step }) => {
const canvas = within(canvasElement)
const titleInput = await canvas.findByRole('textbox', {
name: /Enter a title for your note/i,
})
const bodyInput = canvas.getByRole('textbox', { name: /body/i })

await step('Clear inputs', async () => {
await userEvent.clear(titleInput)
await userEvent.clear(bodyInput)
})

await step('Edit inputs', async () => {
await fireEvent.change(titleInput, { target: { value: 'New Note Title' } })
await fireEvent.change(bodyInput, { target: { value: 'New Note Body' } })
})

await step('Save', async () => {
const saveButton = canvas.getByRole('menuitem', { name: /done/i })
await userEvent.click(saveButton)
await expect(saveNote).toHaveBeenCalledOnce()
await expect(saveNote).toHaveBeenCalledWith(
undefined,
'New Note Title',
'New Note Body',
)
})
},
}
6 changes: 4 additions & 2 deletions components/note-ui.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -51,13 +51,15 @@ export const EditModeFlow: Story = {
await step('Save flow', async () => {
const saveButton = await canvas.findByRole('menuitem', { name: /done/i })
await userEvent.click(saveButton)
await expect(saveNote).toHaveBeenCalled()
await expect(saveNote).toHaveBeenCalledOnce()
await expect(saveNote).toHaveBeenCalledWith('1', 'Edited Title', 'Edited Body')
})

await step('Delete flow', async () => {
const deleteButton = await canvas.findByRole('menuitem', { name: /delete/i })
await userEvent.click(deleteButton)
await expect(deleteNote).toHaveBeenCalled()
await expect(deleteNote).toHaveBeenCalledOnce()
await expect(deleteNote).toHaveBeenCalledWith('1')
})
},
}
43 changes: 40 additions & 3 deletions components/search.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,48 @@
import { getRouter } from '@storybook/nextjs/navigation.mock'
import { Meta, StoryObj } from '@storybook/react'
import Search from "./search";
import Search from './search'
import { expect, fireEvent, userEvent, within } from '@storybook/test'

const meta = {
component: Search,
} satisfies Meta<typeof Search>

export default meta;
export default meta

type Story = StoryObj<typeof meta>
export const Default: Story = {}
export const Default: Story = {}

export const WithInput: Story = {
name: 'With input ▶️',
play: async ({ canvasElement, step }) => {
const canvas = within(canvasElement)
const input = canvas.getByRole('textbox')

await step('Search', async () => {
await fireEvent.change(input, { target: { value: 'Some search query' } })
expect(getRouter().replace).toHaveBeenCalledWith(
expect.stringContaining('q=Some+search+query'),
)
})
},
}

export const InputCleared: Story = {
name: 'Input cleared ▶️',
play: async (playContext) => {
await WithInput.play!(playContext)

const { canvasElement, step } = playContext
const canvas = within(canvasElement)
const input = canvas.getByRole('textbox')

getRouter().replace.mockClear()

await step('Clear', async () => {
await fireEvent.change(input, { target: { value: '' } })
expect(getRouter().replace).toHaveBeenCalledWith(
expect.not.stringContaining('q='),
)
})
},
}
106 changes: 95 additions & 11 deletions components/sidebar.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,23 +1,107 @@
import { useState, useEffect } from 'react'
import { Meta, StoryObj } from '@storybook/react'
import Sidebar from "./sidebar";
import { createNotes } from '#mocks/notes';
import Sidebar from './sidebar'
import { createNotes } from '#mocks/notes'
import { expect, fireEvent, userEvent, within, waitFor } from '@storybook/test'

const meta = {
component: Sidebar,
args: {
notes: createNotes(),
children: null,
},
} satisfies Meta<typeof Sidebar>

export default meta;
export default meta

type Story = StoryObj<typeof meta>
export const Default: Story = {
args: {
notes: createNotes(),
children: null
}
}
export const Default: Story = {}
export const Empty: Story = {
args: {
notes: [],
children: null
},
}

export const NotesExpanded: Story = {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement)
const expanders = canvas.getAllByAltText(/expand/i)

expanders.forEach(async (expander) => {
await userEvent.click(expander)
})
},
}


//@ts-expect-error -- doesn't know about the Promise.withResolvers function
const changeNoteGate = Promise.withResolvers();

export const NoteChangedAnimation: Story = {
render: () => {
const [notes, setNotes] = useState(createNotes())
useEffect(() => {
setTimeout(() => {
setNotes((prevNotes) => {
return [
{
...prevNotes[0],
title: 'New title',
},
...prevNotes.slice(1),
]
})
changeNoteGate.resolve();
}, 1000)
}, [])
return <Sidebar notes={notes} />
},
play: async () => {
await changeNoteGate.promise;
}
}
}

export const ToggleSidebarOnMobile: Story = {
parameters: {
viewport: {
defaultViewport: 'mobile1',
},
chromatic: { viewports: [320] },
},
play: async ({ canvasElement, step }) => {
const canvas = within(canvasElement)
const searchInput = canvas.getByRole('menubar')

await step('Sidebar is initially visible', async () => {
expect(searchInput).toBeVisible()
expect(isElementInView(searchInput)).toBe(true)
})

await step('Select note', async () => {
const note = canvas.getAllByRole('button', {
name: /Open note for preview/i,
})[0]
note.click()
})

await waitFor(function sidebarIsNotVisible() {
expect(isElementInView(searchInput)).toBe(false)
})
},
}

/**
* assertion to check if an element is in or out of the viewport,
* regardless of being in the DOM or not
*/
function isElementInView(element: Element) {
var rect = element.getBoundingClientRect()
var html = document.documentElement

return (
rect.bottom >= 0 &&
rect.top <= (window.innerHeight || html.clientHeight) &&
rect.right >= 0 &&
rect.left <= (window.innerWidth || html.clientWidth)
)
}
2 changes: 1 addition & 1 deletion lib/session.mock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@ import { fn } from '@storybook/test'
import * as actual from './session'

export * from './session'
export const getUserFromSession = fn(actual.getUserFromSession)
export const getUserFromSession = fn(actual.getUserFromSession).mockName('getUserFromSession')
4 changes: 2 additions & 2 deletions mocks/notes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,15 @@ export const createNotes: () => Note[] = () => {
const otherDate = new Date('2024-04-19T15:22:04Z')
return [
{
id: '1',
id: 1,
title: 'Module mocking in Storybook?',
body: "Yup, that's a thing now! 🎉",
createdBy: 'storybookjs',
createdAt: date,
updatedAt: date,
},
{
id: '2',
id: 2,
title: 'RSC support as well??',
body: 'RSC is pretty cool, even cooler that Storybook supports it!',
createdBy: 'storybookjs',
Expand Down

0 comments on commit 56781ea

Please sign in to comment.