diff --git a/next-ui/src/mocks/api/handlers.ts b/next-ui/src/mocks/api/handlers.ts
index 567965c3..6292f56e 100644
--- a/next-ui/src/mocks/api/handlers.ts
+++ b/next-ui/src/mocks/api/handlers.ts
@@ -19,3 +19,6 @@ export const handlers = [
export const response401Unauthorized = () =>
HttpResponse.json({ error: 'Unauthorized' }, { status: 401 })
+
+export const response502BadGateway = () =>
+ HttpResponse.json({ error: 'Bad gateway' }, { status: 502 })
diff --git a/next-ui/src/pages/login.stories.ts b/next-ui/src/pages/login.stories.ts
new file mode 100644
index 00000000..39ab81da
--- /dev/null
+++ b/next-ui/src/pages/login.stories.ts
@@ -0,0 +1,98 @@
+import type { Meta, StoryObj } from '@storybook/vue3-vite'
+
+import login from './login.vue'
+import { http, delay } from 'msw'
+
+import { response401Unauthorized, response502BadGateway } from '@/mocks/api/handlers'
+import { expect, waitFor } from 'storybook/test'
+import { useMessagesStore } from '@/stores/messages'
+
+const meta = {
+ component: login,
+ render: (args: object) => ({
+ components: { login },
+ setup() {
+ return { args }
+ },
+ template: '',
+ }),
+ parameters: {
+ // More on how to position stories at: https://storybook.js.org/docs/configure/story-layout
+ },
+ args: {},
+} satisfies Meta
+
+export default meta
+type Story = StoryObj
+
+export const Default: Story = {
+ args: {},
+}
+
+export const Invalid: Story = {
+ parameters: {
+ msw: {
+ handlers: [http.get('*/api/v2/users/me', response401Unauthorized)],
+ },
+ },
+ play: async ({ canvas, userEvent }) => {
+ const login = canvas.getByLabelText(/email/i, {
+ selector: 'input',
+ })
+ await userEvent.type(login, 'test@example.org')
+
+ const password = canvas.getByLabelText(/password/i, {
+ selector: 'input',
+ })
+ await userEvent.type(password, 'abc')
+
+ await userEvent.click(canvas.getByRole('button', { name: /sign in/i }))
+
+ await waitFor(() => expect(canvas.getByText(/invalid login/i)).toBeVisible())
+ },
+}
+
+export const Loading: Story = {
+ parameters: {
+ msw: {
+ handlers: [http.all('*', async () => await delay(5_000))],
+ },
+ },
+ play: async ({ canvas, userEvent }) => {
+ const login = canvas.getByLabelText(/email/i, {
+ selector: 'input',
+ })
+ await userEvent.type(login, 'test@example.org')
+
+ const password = canvas.getByLabelText(/password/i, {
+ selector: 'input',
+ })
+ await userEvent.type(password, 'abc')
+
+ await userEvent.click(canvas.getByRole('button', { name: /sign in/i }))
+ },
+}
+
+export const Error: Story = {
+ parameters: {
+ msw: {
+ handlers: [http.all('*', response502BadGateway)],
+ },
+ },
+ play: async ({ canvas, userEvent }) => {
+ const login = canvas.getByLabelText(/email/i, {
+ selector: 'input',
+ })
+ await userEvent.type(login, 'test@example.org')
+
+ const password = canvas.getByLabelText(/password/i, {
+ selector: 'input',
+ })
+ await userEvent.type(password, 'abc')
+
+ await userEvent.click(canvas.getByRole('button', { name: /sign in/i }))
+
+ const messagesStore = useMessagesStore()
+ await waitFor(() => expect(messagesStore.messages.length).toBe(1))
+ },
+}