feat: article creation modal
This commit is contained in:
1 parent
a4734b2c91
commit
c47d33eaa9
8 files changed
+1030
-116
No files matched your search
@@ -7,11 +7,17 @@ import { useSessionStore } from '@/stores/session'
|
||||
import type { Article } from '@/composables/useArticles'
|
||||
import HomeView from '../HomeView.vue'
|
||||
|
||||
function mockResponse(
|
||||
status: number,
|
||||
body: unknown = null,
|
||||
headers: Record<string, string> = {},
|
||||
) {
|
||||
let toastAdd: ReturnType<typeof vi.fn>
|
||||
|
||||
vi.mock('@nuxt/ui/composables', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('@nuxt/ui/composables')>()
|
||||
return {
|
||||
...actual,
|
||||
useToast: () => ({ add: toastAdd }),
|
||||
}
|
||||
})
|
||||
|
||||
function mockResponse(status: number, body: unknown = null, headers: Record<string, string> = {}) {
|
||||
return {
|
||||
ok: status >= 200 && status < 300,
|
||||
status,
|
||||
@@ -25,9 +31,7 @@ function routeFor(url: string, init?: RequestInit) {
|
||||
return `${method} ${url}`
|
||||
}
|
||||
|
||||
function fetchRouter(
|
||||
handlers: Record<string, () => Response | Promise<Response>>,
|
||||
) {
|
||||
function fetchRouter(handlers: Record<string, () => Response | Promise<Response>>) {
|
||||
return vi.fn((url: string, init?: RequestInit) => {
|
||||
const key = routeFor(url, init)
|
||||
const handler = handlers[key]
|
||||
@@ -97,10 +101,9 @@ const sixArticles: Article[] = [
|
||||
|
||||
let currentWrapper: Awaited<ReturnType<typeof mount>> | null = null
|
||||
|
||||
async function mountHome(
|
||||
handlers: Record<string, () => Response | Promise<Response>>,
|
||||
) {
|
||||
async function mountHome(handlers: Record<string, () => Response | Promise<Response>>) {
|
||||
setActivePinia(createPinia())
|
||||
toastAdd = vi.fn()
|
||||
const session = useSessionStore()
|
||||
session.session = { verified: true, username: 'marta' }
|
||||
session.status = 'ready'
|
||||
@@ -157,8 +160,7 @@ describe('Skeletons appear only after 150 ms, never for a fast response', () =>
|
||||
it('never shows a skeleton when the response resolves in under 150 ms', async () => {
|
||||
vi.useFakeTimers()
|
||||
const wrapper = await mountHome({
|
||||
'GET /v1/article/': () =>
|
||||
Promise.resolve(mockResponse(200, sixArticles)),
|
||||
'GET /v1/article/': () => Promise.resolve(mockResponse(200, sixArticles)),
|
||||
})
|
||||
|
||||
await vi.advanceTimersByTimeAsync(0)
|
||||
@@ -175,9 +177,7 @@ describe('Default — the list loaded with a mix of online and draft articles',
|
||||
'GET /v1/article/': () => mockResponse(200, sixArticles),
|
||||
})
|
||||
|
||||
expect(wrapper.text()).toContain(
|
||||
"Here's where things stand on your site.",
|
||||
)
|
||||
expect(wrapper.text()).toContain("Here's where things stand on your site.")
|
||||
|
||||
const rows = wrapper.find('[data-testid="recent-list"]').findAll('li')
|
||||
expect(rows).toHaveLength(5)
|
||||
@@ -196,9 +196,7 @@ describe('Default — the list loaded with a mix of online and draft articles',
|
||||
expect(wrapper.text()).toContain('2')
|
||||
expect(wrapper.text()).toContain('drafts')
|
||||
|
||||
const guideLink = wrapper
|
||||
.findAll('a')
|
||||
.find((a) => a.text() === 'Open the guide')
|
||||
const guideLink = wrapper.findAll('a').find((a) => a.text() === 'Open the guide')
|
||||
expect(guideLink?.attributes('href')).toBe('/integration')
|
||||
|
||||
await findButton(wrapper, 'New article')?.trigger('click')
|
||||
@@ -207,6 +205,56 @@ describe('Default — the list loaded with a mix of online and draft articles',
|
||||
})
|
||||
})
|
||||
|
||||
describe('A successful create hands off to the editor with a toast', () => {
|
||||
it('navigates to /article/{id}/edit and raises the Draft created toast', async () => {
|
||||
const created = {
|
||||
id: 42,
|
||||
title: 'Choosing tiles for a small bathroom',
|
||||
body: 'Small rooms show every grout line, so we start with the floor.',
|
||||
images: [],
|
||||
is_online: false,
|
||||
created_at: '2026-01-01T00:00:00Z',
|
||||
updated_at: '2026-01-01T00:00:00Z',
|
||||
}
|
||||
const wrapper = await mountHome({
|
||||
'GET /v1/article/': () => mockResponse(200, sixArticles),
|
||||
'POST /v1/article/': () => mockResponse(201, created),
|
||||
})
|
||||
|
||||
await findButton(wrapper, 'New article')?.trigger('click')
|
||||
await flushPromises()
|
||||
|
||||
const titleInput = document.body.querySelector<HTMLInputElement>(
|
||||
'input[placeholder="e.g. Choosing tiles for a small bathroom"]',
|
||||
)!
|
||||
titleInput.value = created.title
|
||||
titleInput.dispatchEvent(new Event('input', { bubbles: true }))
|
||||
|
||||
const bodyTextarea = document.body.querySelector<HTMLTextAreaElement>(
|
||||
'textarea[placeholder="Start writing. You\'ll continue in the editor."]',
|
||||
)!
|
||||
bodyTextarea.value = created.body
|
||||
bodyTextarea.dispatchEvent(new Event('input', { bubbles: true }))
|
||||
await flushPromises()
|
||||
|
||||
const confirmButton = Array.from(document.body.querySelectorAll('button')).find(
|
||||
(button) => button.textContent?.trim() === 'Create draft',
|
||||
)
|
||||
confirmButton?.click()
|
||||
await flushPromises()
|
||||
|
||||
await vi.waitFor(() => expect(router.currentRoute.value.path).toBe('/article/42/edit'))
|
||||
|
||||
expect(toastAdd).toHaveBeenCalledWith({
|
||||
id: 'article-created-42',
|
||||
color: 'success',
|
||||
icon: 'i-lucide-circle-check',
|
||||
title: 'Draft created',
|
||||
description: '"Choosing tiles for a small bathroom" is saved as a draft.',
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('Empty, first run — zero articles', () => {
|
||||
it('shows the empty state and opens NewArticleModal', async () => {
|
||||
const wrapper = await mountHome({
|
||||
@@ -239,9 +287,7 @@ describe('Error — a 500 or a network failure shows the same inline alert', ()
|
||||
'GET /v1/article/': handler,
|
||||
})
|
||||
|
||||
expect(wrapper.text()).toContain(
|
||||
"Here's where things stand on your site.",
|
||||
)
|
||||
expect(wrapper.text()).toContain("Here's where things stand on your site.")
|
||||
expect(wrapper.text()).toContain('Good')
|
||||
const alert = wrapper.find('[role="alert"]')
|
||||
expect(alert.exists()).toBe(true)
|
||||
@@ -282,9 +328,7 @@ describe('The list is fetched exactly once per navigation', () => {
|
||||
|
||||
expect(listCall).toHaveBeenCalledTimes(1)
|
||||
expect(wrapper.text()).toContain('6')
|
||||
expect(
|
||||
wrapper.find('[data-testid="recent-list"]').findAll('li'),
|
||||
).toHaveLength(5)
|
||||
expect(wrapper.find('[data-testid="recent-list"]').findAll('li')).toHaveLength(5)
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
Reference in new issue
Block a user