feat(SPA): screens for Sites and Authors
This commit is contained in:
1 parent
b34a173d83
commit
3d81d2c717
82 files changed
+10676
-37
No files matched your search
@@ -0,0 +1,209 @@
|
||||
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<typeof import('@/composables/useApi')>()
|
||||
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<HTMLElement>('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<unknown>()
|
||||
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<HTMLElement>('[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')
|
||||
})
|
||||
})
|
||||
Reference in new issue
Block a user