373 lines
12 KiB
TypeScript
373 lines
12 KiB
TypeScript
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
import { flushPromises } from '@vue/test-utils'
|
|
import router from '@/router'
|
|
import { MOBILE_WIDTH } from '@/components/__tests__/viewport'
|
|
import type { Article } from '@/composables/useArticles'
|
|
import {
|
|
articleOf,
|
|
bylines,
|
|
callsTo,
|
|
createMetaBackend,
|
|
failWith,
|
|
mountMeta,
|
|
renewSites,
|
|
textOf,
|
|
typeInto,
|
|
unmountMeta,
|
|
type MetaBackend,
|
|
} from './metaTestKit'
|
|
|
|
vi.mock('@/composables/useApi', async (importOriginal) => {
|
|
const actual = await importOriginal<typeof import('@/composables/useApi')>()
|
|
return { ...actual, useApi: vi.fn() }
|
|
})
|
|
|
|
const SEARCH_WAIT_MS = 320
|
|
|
|
beforeEach(() => {
|
|
localStorage.clear()
|
|
})
|
|
|
|
afterEach(unmountMeta)
|
|
|
|
const FIVE = ['Inès Duarte', 'Jonas Pereira', 'Camille Roche', 'Paul Huber', 'Léa Fontaine']
|
|
|
|
function backendWith(sites: 'one' | 'several', articles: Article[]): MetaBackend {
|
|
const backend = createMetaBackend({
|
|
sites: sites === 'one' ? renewSites().slice(0, 1) : renewSites(),
|
|
})
|
|
backend.articles = articles
|
|
return backend
|
|
}
|
|
|
|
function withAuthors(id: number, names: string[], overrides: Partial<Article> = {}) {
|
|
return articleOf(id, `Article ${id}`, {
|
|
authors: bylines(...names),
|
|
...overrides,
|
|
})
|
|
}
|
|
|
|
function bodyRows() {
|
|
return Array.from(document.body.querySelectorAll<HTMLElement>('tbody tr'))
|
|
}
|
|
|
|
function cell(row: HTMLElement, index: number) {
|
|
return row.children[index] as HTMLElement
|
|
}
|
|
|
|
function headers() {
|
|
return Array.from(document.body.querySelectorAll('thead th')).map(textOf)
|
|
}
|
|
|
|
function siteSelect() {
|
|
return document.body.querySelector<HTMLElement>('button[aria-label="Site"]')
|
|
}
|
|
|
|
function listCalls(backend: MetaBackend) {
|
|
return callsTo(backend, 'GET', '/v1/article/').filter((call) => call.query.has('format'))
|
|
}
|
|
|
|
async function chooseSite(label: string) {
|
|
siteSelect()!.click()
|
|
await flushPromises()
|
|
Array.from(document.body.querySelectorAll<HTMLElement>('[role="option"]'))
|
|
.find((item) => textOf(item) === label)!
|
|
.click()
|
|
await flushPromises()
|
|
}
|
|
|
|
function currentQuery() {
|
|
return { ...router.currentRoute.value.query }
|
|
}
|
|
|
|
describe('Articles: the Authors column on every frame', () => {
|
|
it.each([
|
|
{ authors: 'no authors', names: [], avatars: 0, more: '', text: '—' },
|
|
{
|
|
authors: 'one author',
|
|
names: FIVE.slice(0, 1),
|
|
avatars: 1,
|
|
more: '',
|
|
text: '',
|
|
},
|
|
{
|
|
authors: 'three',
|
|
names: FIVE.slice(0, 3),
|
|
avatars: 3,
|
|
more: '',
|
|
text: '',
|
|
},
|
|
{ authors: 'five', names: FIVE, avatars: 3, more: '+2', text: '' },
|
|
])('$authors: $avatars avatar(s) and "$more"', async ({ names, avatars, more, text }) => {
|
|
await mountMeta('articles', '/article', {
|
|
backend: backendWith('one', [withAuthors(1, names)]),
|
|
})
|
|
|
|
const authorsCell = cell(bodyRows()[0]!, 1)
|
|
const stack = authorsCell.querySelector('[data-testid="author-stack"]')
|
|
expect(stack?.querySelectorAll('[aria-hidden="true"]').length ?? 0).toBe(avatars)
|
|
expect(
|
|
authorsCell.querySelector('[data-testid="author-stack-more"]')?.textContent ?? '',
|
|
).toBe(more)
|
|
if (text) {
|
|
expect(textOf(authorsCell)).toBe(text)
|
|
expect(authorsCell.querySelector('[aria-label="No authors"]')).not.toBeNull()
|
|
}
|
|
expect(headers()[1]).toBe('Authors')
|
|
expect(authorsCell.querySelector('[title]')?.getAttribute('title')).toBe(names.join(', '))
|
|
})
|
|
|
|
it('draws avatars with the first name and no site on the mobile cards', async () => {
|
|
await mountMeta('articles', '/article', {
|
|
width: MOBILE_WIDTH,
|
|
backend: backendWith('several', [
|
|
withAuthors(1, FIVE.slice(0, 3), { site_id: 2 }),
|
|
withAuthors(2, []),
|
|
]),
|
|
})
|
|
|
|
const cards = Array.from(
|
|
document.body.querySelectorAll<HTMLElement>('li [data-row-title]'),
|
|
).map((title) => title.closest('li') as HTMLElement)
|
|
expect(textOf(cards[0])).toContain('Inès Duarte +2')
|
|
expect(
|
|
cards[0]!.querySelectorAll('[data-testid="author-stack"] [aria-hidden="true"]'),
|
|
).toHaveLength(3)
|
|
expect(textOf(cards[1])).toContain('No authors')
|
|
expect(textOf(cards[0])).not.toContain('Journal')
|
|
})
|
|
})
|
|
|
|
describe('Articles: the Site column and the Site filter', () => {
|
|
it('has neither with one site', async () => {
|
|
await mountMeta('articles', '/article', {
|
|
backend: backendWith('one', [withAuthors(1, ['Inès Duarte'])]),
|
|
})
|
|
|
|
expect(headers()).toEqual(['Title', 'Authors', 'Status', 'Created', 'Updated', ''])
|
|
expect(siteSelect()).toBeNull()
|
|
expect(textOf(document.body)).not.toContain('All sites')
|
|
})
|
|
|
|
it.each(['owner', 'admin', 'verified'] as const)(
|
|
'shows a read-only Site column and a Site select for a %s with several sites',
|
|
async (role) => {
|
|
const backend = backendWith('several', [
|
|
withAuthors(1, ['Inès Duarte'], { site_id: 2 }),
|
|
withAuthors(2, ['Inès Duarte'], { site_id: 1 }),
|
|
])
|
|
backend.me.role = role === 'verified' ? undefined : role
|
|
await mountMeta('articles', '/article', { backend })
|
|
|
|
expect(headers()).toEqual([
|
|
'Title',
|
|
'Authors',
|
|
'Site',
|
|
'Status',
|
|
'Created',
|
|
'Updated',
|
|
'',
|
|
])
|
|
expect(bodyRows().map((row) => textOf(cell(row, 2)))).toEqual(['Journal', 'Renew'])
|
|
expect(cell(bodyRows()[0]!, 2).querySelector('button, a, input')).toBeNull()
|
|
siteSelect()!.click()
|
|
await flushPromises()
|
|
expect(
|
|
Array.from(document.body.querySelectorAll('[role="option"]')).map(textOf),
|
|
).toEqual(['All sites', 'Renew', 'Journal', 'Projects', 'Archive'])
|
|
},
|
|
)
|
|
|
|
it('drops the Site column and filter when the sites cannot be loaded', async () => {
|
|
const backend = backendWith('several', [withAuthors(1, ['Inès Duarte'])])
|
|
backend.responders['GET /v1/site/'] = failWith(500)
|
|
await mountMeta('articles', '/article', { backend })
|
|
|
|
expect(headers()).toEqual(['Title', 'Authors', 'Status', 'Created', 'Updated', ''])
|
|
expect(siteSelect()).toBeNull()
|
|
expect(document.body.querySelector('[role="alert"]')).toBeNull()
|
|
})
|
|
})
|
|
|
|
describe('Articles: choosing a site', () => {
|
|
function journalAndRenew(count: number) {
|
|
return Array.from({ length: count }, (_, index) =>
|
|
withAuthors(index + 1, ['Inès Duarte'], {
|
|
site_id: index % 2 === 0 ? 2 : 1,
|
|
is_online: index % 3 !== 0,
|
|
}),
|
|
)
|
|
}
|
|
|
|
it('puts site=journal in the URL without the page, and asks for that site with the same q, status and sort', async () => {
|
|
const backend = backendWith('several', journalAndRenew(40))
|
|
await mountMeta('articles', '/article?page=3&size=5&status=online&sort=title:asc', {
|
|
backend,
|
|
})
|
|
const before = textOf(document.body.querySelector('[role="tablist"]'))
|
|
|
|
await chooseSite('Journal')
|
|
|
|
expect(currentQuery()).toEqual({
|
|
site: 'journal',
|
|
size: '5',
|
|
status: 'online',
|
|
sort: 'title:asc',
|
|
})
|
|
const last = listCalls(backend).at(-1)!
|
|
expect(last.query.get('site')).toBe('journal')
|
|
expect(last.query.get('page')).toBe('1')
|
|
expect(last.query.get('status')).toBe('online')
|
|
expect(last.query.get('sort')).toBe('title:asc')
|
|
expect(textOf(document.body.querySelector('[role="tablist"]'))).toBe(before)
|
|
expect(siteSelect()!.textContent).toContain('Journal')
|
|
})
|
|
|
|
it('goes back to the full list and page 1 with All sites', async () => {
|
|
const backend = backendWith('several', journalAndRenew(40))
|
|
await mountMeta('articles', '/article?site=journal&page=2&size=5', {
|
|
backend,
|
|
})
|
|
|
|
await chooseSite('All sites')
|
|
|
|
expect(currentQuery()).toEqual({ size: '5' })
|
|
const last = listCalls(backend).at(-1)!
|
|
expect(last.query.has('site')).toBe(false)
|
|
expect(last.query.get('page')).toBe('1')
|
|
})
|
|
|
|
it.each(['__all', 'all'])('filters by a real site whose key is %s', async (key) => {
|
|
const backend = backendWith('several', journalAndRenew(4))
|
|
backend.sites = backend.sites.map((site) =>
|
|
site.key === 'journal' ? { ...site, key } : site,
|
|
)
|
|
await mountMeta('articles', '/article', { backend })
|
|
|
|
await chooseSite('Journal')
|
|
|
|
expect(currentQuery()).toEqual({ site: key })
|
|
expect(listCalls(backend).at(-1)!.query.get('site')).toBe(key)
|
|
expect(siteSelect()!.textContent).toContain('Journal')
|
|
})
|
|
|
|
it('reads ?site=journal&page=2 on arrival and shows Journal in the filter', async () => {
|
|
const backend = backendWith('several', journalAndRenew(40))
|
|
await mountMeta('articles', '/article?site=journal&page=2&size=5', {
|
|
backend,
|
|
})
|
|
|
|
expect(siteSelect()!.textContent).toContain('Journal')
|
|
const first = listCalls(backend)[0]!
|
|
expect(first.query.get('site')).toBe('journal')
|
|
expect(first.query.get('page')).toBe('2')
|
|
})
|
|
|
|
it.each([
|
|
{ query: '?site=nope', sites: 'several' },
|
|
{ query: '?site=renew', sites: 'one' },
|
|
] as const)('drops $query silently with $sites site(s)', async ({ query, sites }) => {
|
|
const backend = backendWith(sites, journalAndRenew(8))
|
|
await mountMeta('articles', `/article${query}`, { backend })
|
|
|
|
expect(currentQuery()).toEqual({})
|
|
expect(listCalls(backend).every((call) => !call.query.has('site'))).toBe(true)
|
|
expect(document.body.querySelector('[role="alert"], [role="status"]')).toBeNull()
|
|
expect(bodyRows().length).toBeGreaterThan(0)
|
|
})
|
|
|
|
it.each([
|
|
{
|
|
action: 'searches for "plaster"',
|
|
act: async () => {
|
|
await typeInto(
|
|
document.body.querySelector<HTMLInputElement>('input[type="search"]')!,
|
|
'plaster',
|
|
)
|
|
await new Promise((resolve) => setTimeout(resolve, SEARCH_WAIT_MS))
|
|
await flushPromises()
|
|
},
|
|
},
|
|
{
|
|
action: 'changes the tab',
|
|
act: async () => {
|
|
const tab = Array.from(
|
|
document.body.querySelectorAll<HTMLElement>('[role="tab"]'),
|
|
).find((item) => textOf(item).startsWith('Drafts'))!
|
|
tab.dispatchEvent(new MouseEvent('mousedown', { bubbles: true }))
|
|
tab.click()
|
|
await flushPromises()
|
|
},
|
|
},
|
|
{
|
|
action: 'changes the sort',
|
|
act: async () => {
|
|
Array.from(document.body.querySelectorAll<HTMLElement>('thead button'))
|
|
.find((item) => textOf(item).startsWith('Title'))!
|
|
.click()
|
|
await flushPromises()
|
|
},
|
|
},
|
|
{
|
|
action: 'goes to page 2',
|
|
act: async () => {
|
|
document.body.querySelector<HTMLElement>('button[aria-label="Next page"]')!.click()
|
|
await flushPromises()
|
|
},
|
|
},
|
|
{
|
|
action: 'changes the page size',
|
|
act: async () => {
|
|
document.body.querySelector<HTMLElement>('[aria-label="Rows per page"]')!.click()
|
|
await flushPromises()
|
|
Array.from(document.body.querySelectorAll<HTMLElement>('[role="option"]'))
|
|
.find((item) => textOf(item) === '50 per page')!
|
|
.click()
|
|
await flushPromises()
|
|
},
|
|
},
|
|
])('keeps ?site=journal when the user $action', async ({ act }) => {
|
|
const backend = backendWith('several', journalAndRenew(40))
|
|
await mountMeta('articles', '/article?site=journal&size=5', { backend })
|
|
|
|
await act()
|
|
|
|
expect(currentQuery().site).toBe('journal')
|
|
expect(listCalls(backend).at(-1)!.query.get('site')).toBe('journal')
|
|
})
|
|
|
|
it('shows the empty state of a site and returns to the full list with Show all sites', async () => {
|
|
const backend = backendWith('several', journalAndRenew(6))
|
|
await mountMeta('articles', '/article?site=archive', { backend })
|
|
|
|
const text = textOf(document.body)
|
|
expect(text).toContain('No articles on "Archive"')
|
|
expect(text).toContain('Move articles here from the editor, or choose another site.')
|
|
|
|
Array.from(document.body.querySelectorAll<HTMLElement>('button'))
|
|
.find((item) => textOf(item) === 'Show all sites')!
|
|
.click()
|
|
await flushPromises()
|
|
|
|
expect(currentQuery()).toEqual({})
|
|
expect(listCalls(backend).at(-1)!.query.has('site')).toBe(false)
|
|
expect(bodyRows().length).toBeGreaterThan(0)
|
|
})
|
|
|
|
it('keeps the existing No results and No drafts states when a search or a tab is active on a site', async () => {
|
|
const backend = backendWith('several', journalAndRenew(6))
|
|
await mountMeta('articles', '/article?site=archive&q=tiles', {
|
|
backend,
|
|
})
|
|
expect(textOf(document.body)).toContain('No articles match "tiles"')
|
|
expect(textOf(document.body)).not.toContain('No articles on "Archive"')
|
|
await unmountMeta()
|
|
|
|
await mountMeta('articles', '/article?site=archive&status=draft', {
|
|
backend,
|
|
})
|
|
expect(textOf(document.body)).toContain('No drafts')
|
|
expect(textOf(document.body)).not.toContain('No articles on "Archive"')
|
|
})
|
|
})
|