import { afterEach, describe, expect, it, vi } from 'vitest' import { mount, flushPromises } from '@vue/test-utils' import { createPinia, setActivePinia } from 'pinia' import uiPlugin from '@nuxt/ui/vue-plugin' import router from '@/router' import { useSessionStore } from '@/stores/session' import RootComponent from '@/App.vue' import ArticleEditor from '@/components/ArticleEditor.vue' vi.mock('@nuxt/ui/composables', () => ({ useOverlay: () => ({ create: () => ({ open: () => Promise.resolve(false) }) }), useToast: () => ({ add: vi.fn() }), })) const emptyRectList = () => Object.assign([], { item: () => null }) as unknown as DOMRectList Range.prototype.getClientRects = emptyRectList Range.prototype.getBoundingClientRect = () => new DOMRect() function mockResponse(status: number, body: unknown = null) { return { ok: status >= 200 && status < 300, status, headers: { get: () => null }, json: async () => body, } as unknown as Response } let currentWrapper: Awaited> | null = null async function mountArticleEditor(body = '

Hello

') { setActivePinia(createPinia()) const session = useSessionStore() session.session = { verified: true, username: 'marta' } session.status = 'ready' global.fetch = vi.fn(() => Promise.resolve( mockResponse(200, { id: 12, title: 'T', body, is_online: false, created_at: '2026-01-01T00:00:00Z', updated_at: '2026-01-01T00:00:00Z', }), ), ) as unknown as typeof fetch await router.push('/article/12/edit') const wrapper = mount(RootComponent, { attachTo: document.body, global: { plugins: [router, uiPlugin] }, }) currentWrapper = wrapper await flushPromises() return wrapper } afterEach(async () => { currentWrapper?.unmount() currentWrapper = null await router.push('/login') }) describe('the alignment toolbar buttons align the current block', () => { const ALIGNMENTS = ['left', 'center', 'right', 'justify'] const FIRST_ALIGN_BUTTON = 7 function alignButton(name: string) { const buttons = document.body.querySelectorAll('.article-editor button') return buttons[FIRST_ALIGN_BUTTON + ALIGNMENTS.indexOf(name)] ?? null } it.each(ALIGNMENTS)('keeps the %s button enabled', async (name) => { await mountArticleEditor() const button = alignButton(name) expect(button).not.toBeNull() expect(button?.disabled).toBe(false) }) it('writes the alignment into the body when a button is clicked', async () => { const wrapper = await mountArticleEditor() const editor = wrapper.findComponent(ArticleEditor) alignButton('center')?.click() await flushPromises() expect(editor.props('modelValue')).toContain('text-align: center') }) }) describe('insertImage stamps the inserted image with its numeric id', () => { it('renders data-image-id on the img and emits the updated body', async () => { const wrapper = await mountArticleEditor() const editor = wrapper.findComponent(ArticleEditor) ;( editor.vm as unknown as { insertImage: (src: string, alt: string, id: number) => void } ).insertImage('/static/uploadedImages/photo.png', 'alt text', 10) await flushPromises() expect(editor.props('modelValue')).toContain( 'alt text', ) }) }) describe('removeImage removes only the node whose data-image-id matches', () => { it('deletes the matching image node and leaves the rest of the content', async () => { const wrapper = await mountArticleEditor() const editor = wrapper.findComponent(ArticleEditor) const vm = editor.vm as unknown as { insertImage: (src: string, alt: string, id: number) => void removeImage: (id: number) => void } vm.insertImage('/static/uploadedImages/a.png', '', 10) await flushPromises() vm.insertImage('/static/uploadedImages/b.png', '', 11) await flushPromises() vm.removeImage(10) await flushPromises() const body = editor.props('modelValue') as string expect(body).not.toContain('data-image-id="10"') expect(body).toContain('data-image-id="11"') }) it('does nothing when no image carries the given id', async () => { const wrapper = await mountArticleEditor('

Hello

') const editor = wrapper.findComponent(ArticleEditor) const vm = editor.vm as unknown as { removeImage: (id: number) => void } vm.removeImage(999) await flushPromises() expect(editor.emitted('update:modelValue')).toBeFalsy() }) }) describe('imageSrc reads the src of the node whose data-image-id matches', () => { it('returns the src of the matching image and null for an unknown id', async () => { const wrapper = await mountArticleEditor( '

Hello

', ) const vm = wrapper.findComponent(ArticleEditor).vm as unknown as { imageSrc: (id: number) => string | null } expect(vm.imageSrc(10)).toBe('/static/uploadedImages/a.png') expect(vm.imageSrc(999)).toBeNull() }) })