feat: media picker and image removal
This commit is contained in:
1 parent
008b0dfbb9
commit
b3793d5dbb
54 files changed
+3769
-1412
No files matched your search
@@ -10,6 +10,8 @@ import RootComponent from '@/App.vue'
|
||||
|
||||
const FIXED_NOW = '2026-09-26T11:02:00Z'
|
||||
|
||||
vi.mock('@/lib/fileHash', () => ({ hashFile: vi.fn(async () => 'a'.repeat(64)) }))
|
||||
|
||||
vi.mock('@/lib/imageCrop', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('@/lib/imageCrop')>()
|
||||
return {
|
||||
@@ -118,7 +120,6 @@ function articleFixture(overrides: Partial<Article> = {}): Article {
|
||||
id: 12,
|
||||
title: 'How we price a bathroom renovation',
|
||||
body: '<p>a</p>',
|
||||
images: [],
|
||||
is_online: false,
|
||||
created_at: '2026-03-12T09:14:00Z',
|
||||
updated_at: FIXED_NOW,
|
||||
@@ -186,29 +187,39 @@ afterEach(async () => {
|
||||
await router.push('/login')
|
||||
})
|
||||
|
||||
describe("Opening the uploader from the toolbar's image button shows the idle state", () => {
|
||||
it('opens ImageUploader idle and sends no request', async () => {
|
||||
const wrapper = await mountEditor({
|
||||
'GET /v1/article/12': () => mockResponse(200, articleFixture()),
|
||||
})
|
||||
const EMPTY_LIBRARY = { items: [], next_cursor: null, counts: { all: 0, unused: 0, duplicates: 0 } }
|
||||
|
||||
function articleHandlers(extra: Record<string, () => Response | Promise<Response>> = {}) {
|
||||
return {
|
||||
'GET /v1/article/12': () => mockResponse(200, articleFixture()),
|
||||
'GET /v1/media/?limit=24': () => mockResponse(200, EMPTY_LIBRARY),
|
||||
...extra,
|
||||
}
|
||||
}
|
||||
|
||||
describe("Opening the uploader from the toolbar's image button on an empty library shows Upload", () => {
|
||||
it('opens ImageUploader on the idle Upload tab and sends no POST', async () => {
|
||||
const wrapper = await mountEditor(articleHandlers())
|
||||
|
||||
await toolbarImageButton(wrapper)?.trigger('click')
|
||||
await flushPromises()
|
||||
|
||||
expect(document.body.textContent).toContain('Add an image')
|
||||
expect(document.body.textContent).toContain('Drop an image here')
|
||||
expect(document.body.textContent).toContain('Drop an image here, or choose a file')
|
||||
expect(document.body.textContent).toContain('PNG, JPEG, WEBP, SVG · max 10 MB')
|
||||
expect(findButton('Choose file')).toBeTruthy()
|
||||
expect(findButton('Cancel')).toBeTruthy()
|
||||
expect(findButton('Insert image')).toBeUndefined()
|
||||
expect(FakeXMLHttpRequest.instances).toHaveLength(0)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Dropping a valid file onto the editor opens the uploader already showing the crop step', () => {
|
||||
it('opens the crop step, and confirming it sends the resized file', async () => {
|
||||
const wrapper = await mountEditor({
|
||||
'GET /v1/article/12': () => mockResponse(200, articleFixture()),
|
||||
})
|
||||
describe('Dropping a valid file onto the editor opens the uploader on the crop step', () => {
|
||||
it('looks the hash up, skips the list, and uploads to /v1/media/ from the crop step', async () => {
|
||||
const wrapper = await mountEditor(
|
||||
articleHandlers({
|
||||
[`GET /v1/media/?hash=${'a'.repeat(64)}`]: () => mockResponse(200, EMPTY_LIBRARY),
|
||||
}),
|
||||
)
|
||||
|
||||
const file = fileFixture('before.png', 'image/png', 3.9)
|
||||
const dropEvent = new Event('drop', { bubbles: true, cancelable: true })
|
||||
@@ -219,22 +230,22 @@ describe('Dropping a valid file onto the editor opens the uploader already showi
|
||||
await flushPromises()
|
||||
|
||||
expect(document.body.textContent).toContain('Crop and resize')
|
||||
expect(document.body.textContent).not.toContain('Drop an image here')
|
||||
expect(vi.mocked(global.fetch).mock.calls.map(([url]) => url)).not.toContain(
|
||||
'/v1/media/?limit=24',
|
||||
)
|
||||
|
||||
findButton('Upload image')?.click()
|
||||
await flushPromises()
|
||||
|
||||
expect(FakeXMLHttpRequest.instances).toHaveLength(1)
|
||||
expect(latestXHR().url).toBe('/v1/article/12/image')
|
||||
expect(latestXHR().url).toBe('/v1/media/')
|
||||
expect((latestXHR().sentBody?.get('file') as File).name).toBe('before.png')
|
||||
})
|
||||
})
|
||||
|
||||
describe('A successful upload inserts the image at the cursor and marks the draft dirty', () => {
|
||||
it('inserts the image, marks the draft dirty and toasts', async () => {
|
||||
const wrapper = await mountEditor({
|
||||
'GET /v1/article/12': () => mockResponse(200, articleFixture()),
|
||||
})
|
||||
it('inserts the image with its id, marks the draft dirty and toasts', async () => {
|
||||
const wrapper = await mountEditor(articleHandlers())
|
||||
|
||||
await toolbarImageButton(wrapper)?.trigger('click')
|
||||
await flushPromises()
|
||||
@@ -247,20 +258,25 @@ describe('A successful upload inserts the image at the cursor and marks the draf
|
||||
})
|
||||
fileInput.dispatchEvent(new Event('change'))
|
||||
await flushPromises()
|
||||
|
||||
findButton('Upload image')?.click()
|
||||
await flushPromises()
|
||||
|
||||
expect(FakeXMLHttpRequest.instances).toHaveLength(1)
|
||||
latestXHR().resolve(
|
||||
201,
|
||||
JSON.stringify({ location: '/static/uploadedImages/north-wall.jpg' }),
|
||||
JSON.stringify({
|
||||
id: 13,
|
||||
filename: 'north-wall.jpg',
|
||||
url: '/static/uploadedImages/13.jpg',
|
||||
size: 4089446,
|
||||
}),
|
||||
)
|
||||
await flushPromises()
|
||||
|
||||
const insertedBody = wrapper.findComponent(ArticleEditor).props('modelValue')
|
||||
expect(insertedBody).toContain('/static/uploadedImages/north-wall.jpg')
|
||||
expect(insertedBody).toContain('<img src="/static/uploadedImages/north-wall.jpg" alt="">')
|
||||
expect(insertedBody).toContain(
|
||||
'<img src="/static/uploadedImages/13.jpg" alt="" data-image-id="13">',
|
||||
)
|
||||
expect(wrapper.text()).toContain('Unsaved changes')
|
||||
expect((saveButton(wrapper).element as HTMLButtonElement).disabled).toBe(false)
|
||||
expect(toastAdd).toHaveBeenCalledWith({
|
||||
|
||||
Reference in new issue
Block a user