156 lines
4.9 KiB
TypeScript
156 lines
4.9 KiB
TypeScript
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<ReturnType<typeof mount>> | null = null
|
|
|
|
async function mountArticleEditor(body = '<p>Hello</p>') {
|
|
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<HTMLButtonElement>('.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(
|
|
'<img src="/static/uploadedImages/photo.png" alt="alt text" data-image-id="10">',
|
|
)
|
|
})
|
|
})
|
|
|
|
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('<p>Hello</p>')
|
|
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(
|
|
'<p>Hello</p><img src="/static/uploadedImages/a.png" alt="" data-image-id="10">',
|
|
)
|
|
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()
|
|
})
|
|
})
|