import { afterEach, describe, expect, it, vi } from 'vitest' import { flushPromises } from '@vue/test-utils' import { button, callsTo, chooseMenuItem, click, createBackend, dialog, failWith, mountDirectory, networkFailure, openRowMenu, pending, rowTexts, textOf, unmountDirectory, } from './directoryTestKit' vi.mock('@/composables/useApi', async (importOriginal) => { const actual = await importOriginal() return { ...actual, useApi: vi.fn() } }) afterEach(unmountDirectory) async function openDelete(name: string, backend = createBackend()) { await mountDirectory('authors', '/authors', { backend }) await openRowMenu(name) await chooseMenuItem('Delete…') return backend } function box() { return dialog()! } function trigger() { return box().querySelector('button[aria-haspopup="listbox"]')! } async function openSelect() { trigger().click() await flushPromises() } function optionLabels() { return Array.from(box().querySelectorAll('[role="option"]')).map((option) => textOf(option)) } describe('Authors delete: reassign (recommended)', () => { it('explains, preselects the first other author and lists the buttons', async () => { const backend = await openDelete('Camille Roche') const text = textOf(box()) expect(box().getAttribute('role')).toBe('alertdialog') expect(text).toContain('Delete this author?') expect(text).toContain( '"Camille Roche" will be deleted and is credited on 3 articles. This can\'t be undone.', ) expect(text).toContain('Credit these articles to') expect(textOf(trigger())).toBe('Inès Duarte') expect(text).toContain( 'Where an article has several authors, Camille Roche is removed and the others stay.', ) expect(button('Cancel', box())).toBeTruthy() expect(button('Delete and reassign 3 articles', box())).toBeTruthy() expect( backend.calls.filter((call) => call.raw === '/v1/author/?page=1&size=100'), ).toHaveLength(1) }) it('adds the linked-account line for a linked author', async () => { await openDelete('Inès Duarte') expect(textOf(box())).toContain( "Linked to ines.duarte@renew.example. The user account isn't affected.", ) }) }) describe('Authors delete: reassign succeeds', () => { it('sends one DELETE with reassign_to, reads Reassigning… and toasts', async () => { const backend = createBackend() const hold = pending() backend.responders['DELETE /v1/author/7'] = () => hold.promise await openDelete('Camille Roche', backend) await click('Delete and reassign 3 articles', box()) expect(button('Reassigning…', box())).toBeTruthy() hold.resolve(undefined) await flushPromises() const deletes = callsTo(backend, 'DELETE', '/v1/author/7') expect(deletes).toHaveLength(1) expect(deletes[0]!.query.get('reassign_to')).toBe('2') expect(rowTexts().join(' ')).not.toContain('Camille Roche') expect(document.body.textContent).toContain('Author deleted') expect(document.body.textContent).toContain('3 articles now credit Inès Duarte.') }) }) describe('Authors delete: choose who', () => { it('does not list the deleted author and ends with Nobody', async () => { await openDelete('Camille Roche') await openSelect() const labels = optionLabels() expect(labels.some((label) => label.includes('Camille Roche'))).toBe(false) expect(labels.at(-1)).toContain('Nobody') expect(labels.at(-1)).toContain('Remove the byline') expect(labels[0]).toContain('Inès Duarte') }) }) describe('Authors delete: nobody', () => { it('explains the byline and sends reassign_to=none', async () => { const backend = await openDelete('Camille Roche') await openSelect() Array.from(box().querySelectorAll('[role="option"]')) .find((option) => textOf(option).includes('Nobody'))! .click() await flushPromises() expect(textOf(box())).toContain( 'The articles keep any other authors. Articles with only this author will have no byline.', ) await click('Delete and reassign 3 articles', box()) expect(callsTo(backend, 'DELETE', '/v1/author/7')[0]!.query.get('reassign_to')).toBe('none') }) }) describe.each([ ['status 500', failWith(500)], ['a network error', networkFailure], ])('Authors delete: failed with %s', (_name, responder) => { it('keeps the dialog open with the notice and enables the buttons again', async () => { const backend = createBackend() backend.responders['DELETE /v1/author/7'] = responder await openDelete('Camille Roche', backend) await click('Delete and reassign 3 articles', box()) const alert = textOf(box().querySelector('[role="alert"]')) expect(alert).toContain("Couldn't delete the author") expect(alert).toContain('Nothing was changed. Try again in a moment.') expect(button('Cancel', box())!.hasAttribute('disabled')).toBe(false) expect(button('Delete and reassign 3 articles', box())!.hasAttribute('disabled')).toBe( false, ) }) }) describe('Authors delete: server fallback (409)', () => { it('opens the fallback dialog and Reassign articles reopens the reassign dialog', async () => { const backend = createBackend() backend.responders['DELETE /v1/author/7'] = failWith(409, 'author_has_articles') await openDelete('Camille Roche', backend) await click('Delete and reassign 3 articles', box()) await flushPromises() const text = textOf(box()) expect(text).toContain("Can't delete this author") expect(text).toContain( '"Camille Roche" is credited on 3 articles. Choose another author to credit instead, or remove Camille Roche from those articles first.', ) expect(button('Close', box())).toBeTruthy() expect(button('Reassign articles', box())).toBeTruthy() await click('Reassign articles', box()) await flushPromises() expect(textOf(box())).toContain('Delete this author?') }) }) describe('Authors delete: no articles', () => { it('asks a plain confirmation with no select and sends no reassign_to', async () => { const backend = await openDelete('Léa Fontaine') const text = textOf(box()) expect(text).toContain('Delete this author?') expect(text).toContain( '"Léa Fontaine" will be deleted. No articles credit this author. This can\'t be undone.', ) expect(trigger()).toBeNull() expect(button('Delete author', box())).toBeTruthy() await click('Delete author', box()) const deletes = callsTo(backend, 'DELETE', '/v1/author/10') expect(deletes).toHaveLength(1) expect(deletes[0]!.query.has('reassign_to')).toBe(false) expect(rowTexts().join(' ')).not.toContain('Léa Fontaine') }) }) describe('Authors delete: author already gone', () => { it('closes, removes the row and shows the info toast on 404', async () => { const backend = createBackend() backend.responders['DELETE /v1/author/7'] = failWith(404, 'author_not_found') await openDelete('Camille Roche', backend) await click('Delete and reassign 3 articles', box()) expect(dialog()).toBeNull() expect(rowTexts().join(' ')).not.toContain('Camille Roche') expect(document.body.textContent).toContain('That author no longer exists') }) })