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('sites', '/settings/sites', { backend }) await openRowMenu(name) await chooseMenuItem('Delete…') return backend } function box() { return dialog()! } function trigger() { return box().querySelector('button[aria-haspopup="listbox"]')! } describe('Sites delete: site with articles', () => { it('explains, defaults the destination to the default site and shows the move summary', async () => { await openDelete('Journal') const text = textOf(box()) expect(box().getAttribute('role')).toBe('alertdialog') expect(text).toContain('Delete this site?') expect(text).toContain( '"Journal" will be deleted. 38 articles belong to it, so choose a site to move them to. This can\'t be undone.', ) expect(text).toContain('Move articles to') expect(textOf(trigger())).toBe('Renew · renew.example') expect(text).toContain( '38 articles will move to Renew. Their canonical host changes to renew.example.', ) expect(button('Cancel', box())).toBeTruthy() expect(button('Move 38 articles and delete', box())).toBeTruthy() }) }) describe('Sites delete: choose destination', () => { it('does not offer the deleted site and shows the name and the host on each option', async () => { await openDelete('Journal') trigger().click() await flushPromises() const options = Array.from(box().querySelectorAll('[role="option"]')).map((option) => textOf(option), ) expect(options).toEqual([ 'Renew renew.example', 'Projects projects.example', 'Archive archive.example', ]) }) }) describe('Sites delete: with articles succeeds', () => { it('sends one DELETE with reassign_to, reads Moving articles… and toasts', async () => { const backend = createBackend() const hold = pending() backend.responders['DELETE /v1/site/2'] = () => hold.promise await openDelete('Journal', backend) await click('Move 38 articles and delete', box()) expect(button('Moving articles…', box())).toBeTruthy() hold.resolve(undefined) await flushPromises() const deletes = callsTo(backend, 'DELETE', '/v1/site/2') expect(deletes).toHaveLength(1) expect(deletes[0]!.query.get('reassign_to')).toBe('1') expect(dialog()).toBeNull() expect(rowTexts().join(' ')).not.toContain('Journal') expect(document.body.textContent).toContain('Site deleted') expect(document.body.textContent).toContain('38 articles moved to Renew.') }) }) describe('Sites delete: no articles', () => { it('asks a plain confirmation with no select and sends no reassign_to', async () => { const backend = await openDelete('Archive') const text = textOf(box()) expect(text).toContain('Delete this site?') expect(text).toContain( '"Archive" will be deleted. No articles belong to it. This can\'t be undone.', ) expect(trigger()).toBeNull() expect(button('Delete site', box())).toBeTruthy() await click('Delete site', box()) const deletes = callsTo(backend, 'DELETE', '/v1/site/4') expect(deletes).toHaveLength(1) expect(deletes[0]!.query.has('reassign_to')).toBe(false) expect(rowTexts().join(' ')).not.toContain('Archive') }) }) describe.each([ ['status 500', failWith(500)], ['a network error', networkFailure], ])('Sites delete: failure 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/site/2'] = responder await openDelete('Journal', backend) await click('Move 38 articles and delete', box()) const alert = textOf(box().querySelector('[role="alert"]')) expect(alert).toContain("Couldn't delete the site") expect(alert).toContain('Nothing was changed. Try again in a moment.') expect(button('Cancel', box())!.hasAttribute('disabled')).toBe(false) expect(button('Move 38 articles and delete', box())!.hasAttribute('disabled')).toBe(false) }) }) describe('Sites delete: server refusals', () => { it('keeps the dialog open and asks for a destination on 409 site_has_articles', async () => { const backend = createBackend() backend.responders['DELETE /v1/site/2'] = failWith(409, 'site_has_articles') await openDelete('Journal', backend) await click('Move 38 articles and delete', box()) expect(dialog()).not.toBeNull() expect(textOf(box())).toContain('Choose where the articles go. This site has 38 articles.') }) it('closes and toasts on 403 default_site', async () => { const backend = createBackend() backend.responders['DELETE /v1/site/2'] = failWith(403, 'default_site') await openDelete('Journal', backend) await click('Move 38 articles and delete', box()) expect(dialog()).toBeNull() expect(document.body.textContent).toContain( "The default site can't be deleted. Make another site the default first.", ) }) it('closes, removes the row and toasts on 404', async () => { const backend = createBackend() backend.responders['DELETE /v1/site/2'] = failWith(404, 'site_not_found') await openDelete('Journal', backend) await click('Move 38 articles and delete', box()) expect(dialog()).toBeNull() expect(rowTexts().join(' ')).not.toContain('Journal') expect(document.body.textContent).toContain('That site no longer exists') }) })