647 lines
22 KiB
TypeScript
647 lines
22 KiB
TypeScript
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||
import { flushPromises, mount, type VueWrapper } from '@vue/test-utils'
|
||
import { createPinia, setActivePinia } from 'pinia'
|
||
import { defineComponent, h } from 'vue'
|
||
import uiPlugin from '@nuxt/ui/vue-plugin'
|
||
import UApp from '@nuxt/ui/components/App.vue'
|
||
import router from '@/router'
|
||
import { ApiError, NetworkError, useApi } from '@/composables/useApi'
|
||
import type { ImageVariant } from '@/composables/useImageVariants'
|
||
import { useSessionStore } from '@/stores/session'
|
||
import { deferred, setViewport, stubBrowser, variant } from './variantsTestKit'
|
||
import IntegrationView from '../IntegrationView.vue'
|
||
import { accessFor, ownerAccess } from '@/test/access'
|
||
|
||
vi.mock('@/composables/useApi', async (importOriginal) => {
|
||
const actual = await importOriginal<typeof import('@/composables/useApi')>()
|
||
return { ...actual, useApi: vi.fn() }
|
||
})
|
||
|
||
const ORIGIN = 'https://cms.renew.example'
|
||
const LIST_PATH = '/v1/image-variants'
|
||
const BASE = `${ORIGIN}/media/north-wall.jpg`
|
||
const SECTION_ORDER = ['images', 'settings', 'session', 'errors']
|
||
const BODY_UNKNOWN =
|
||
'{ "status": 400, "error": "unknown_variant", "message": "Unknown variant", "detail": "unknown" }'
|
||
const BODY_UNSUPPORTED =
|
||
'{ "status": 400, "error": "unsupported_parameter", "message": "Unsupported parameter", "detail": "width" }'
|
||
const BODY_HERO =
|
||
'{ "status": 400, "error": "unknown_variant", "message": "Unknown variant", "detail": "hero" }'
|
||
|
||
const CURL = `curl -O "${BASE}?v=thumb"`
|
||
const HTML = [
|
||
'<img',
|
||
` src="${BASE}?v=card"`,
|
||
` srcset="${BASE}?v=thumb 400w,`,
|
||
` ${BASE}?v=card 800w,`,
|
||
` ${BASE}?v=hero 1200w"`,
|
||
' sizes="(min-width: 800px) 800px, 100vw"',
|
||
' alt="North wall">',
|
||
].join('\n')
|
||
const JAVASCRIPT = [
|
||
`const res = await fetch("${BASE}?v=thumb")`,
|
||
'if (!res.ok) throw new Error(await res.text())',
|
||
'const blob = await res.blob()',
|
||
].join('\n')
|
||
|
||
type Row = [string, number, number | null, ImageVariant['format']]
|
||
|
||
const BOARD_ROWS: Row[] = [
|
||
['thumb', 400, 300, 'webp'],
|
||
['card', 800, null, 'webp'],
|
||
['hero', 1200, 630, 'webp'],
|
||
['square', 600, 600, 'jpeg'],
|
||
['og', 1200, 630, 'jpeg'],
|
||
['banner', 1600, 400, 'webp'],
|
||
['teaser', 640, 360, 'webp'],
|
||
['sidebar', 320, null, 'webp'],
|
||
['gallery', 1000, 750, 'webp'],
|
||
['avatar', 96, 96, 'webp'],
|
||
['wide', 1920, 800, 'webp'],
|
||
['mini', 120, 90, 'webp'],
|
||
]
|
||
|
||
function boardVariants() {
|
||
return BOARD_ROWS.map(([name, width, height, format]) =>
|
||
variant({ name, width, height, format, quality: 82 }),
|
||
)
|
||
}
|
||
|
||
function withPrint() {
|
||
return [...boardVariants(), variant({ name: 'print', enabled: false })]
|
||
}
|
||
|
||
type ListHandler = () => Promise<unknown>
|
||
|
||
interface MountOptions {
|
||
role?: string | null
|
||
list?: ListHandler
|
||
desktop?: boolean
|
||
}
|
||
|
||
let wrapper: VueWrapper | null = null
|
||
let requestMock: ReturnType<typeof vi.fn>
|
||
let listHandler: ListHandler
|
||
|
||
function listing(items: ImageVariant[]): ListHandler {
|
||
return () => Promise.resolve({ items })
|
||
}
|
||
|
||
function stubClipboard() {
|
||
const writeText = vi.fn().mockResolvedValue(undefined)
|
||
Object.defineProperty(navigator, 'clipboard', {
|
||
configurable: true,
|
||
value: { writeText },
|
||
})
|
||
return writeText
|
||
}
|
||
|
||
async function mountPage(options: MountOptions = {}) {
|
||
const { role = 'owner', list = listing(withPrint()), desktop = true } = options
|
||
setViewport(desktop)
|
||
stubBrowser()
|
||
Element.prototype.scrollIntoView = vi.fn()
|
||
vi.stubGlobal('location', { ...window.location, origin: ORIGIN })
|
||
listHandler = list
|
||
setActivePinia(createPinia())
|
||
const session = useSessionStore()
|
||
const sessionRole = (role ?? undefined) as 'owner' | 'admin' | undefined
|
||
session.session = {
|
||
verified: true,
|
||
username: 'marta',
|
||
id: 1,
|
||
displayName: 'Marta Alves',
|
||
access: accessFor(sessionRole),
|
||
}
|
||
session.status = 'ready'
|
||
requestMock = vi.fn((path: string, init?: RequestInit) => {
|
||
if (path === LIST_PATH && !init?.method) {
|
||
return listHandler()
|
||
}
|
||
throw new Error(`Unexpected request: ${init?.method ?? 'GET'} ${path}`)
|
||
})
|
||
vi.mocked(useApi).mockReturnValue({
|
||
getMe: vi.fn().mockResolvedValue({
|
||
verified: true,
|
||
access: ownerAccess(),
|
||
username: 'marta',
|
||
id: 1,
|
||
role,
|
||
}),
|
||
request: requestMock,
|
||
})
|
||
await router.push('/integration')
|
||
const Host = defineComponent({
|
||
render: () => h(UApp, null, () => h(IntegrationView)),
|
||
})
|
||
wrapper = mount(Host, {
|
||
attachTo: document.body,
|
||
global: { plugins: [router, uiPlugin] },
|
||
})
|
||
await flushPromises()
|
||
}
|
||
|
||
function listCalls() {
|
||
return requestMock.mock.calls.filter(([path]) => path === LIST_PATH)
|
||
}
|
||
|
||
function requestedPaths() {
|
||
return requestMock.mock.calls
|
||
.map(([path]) => path)
|
||
.filter((path) => path !== '/v1/content-type')
|
||
}
|
||
|
||
function images() {
|
||
return document.body.querySelector<HTMLElement>('[data-anchor="images"]')!
|
||
}
|
||
|
||
function headingBlock(title: string) {
|
||
const heading = Array.from(images().querySelectorAll('h3')).find(
|
||
(item) => item.textContent?.trim() === title,
|
||
)
|
||
return heading?.closest('div.flex.flex-col') as HTMLElement | undefined
|
||
}
|
||
|
||
function normalized(element: Element | null | undefined) {
|
||
return element?.textContent?.replace(/\s+/g, ' ').trim() ?? ''
|
||
}
|
||
|
||
function tabs(root: ParentNode) {
|
||
return Array.from(root.querySelectorAll<HTMLElement>('[role="tab"]'))
|
||
}
|
||
|
||
function tabNamed(root: ParentNode, label: string) {
|
||
return tabs(root).find((tab) => tab.textContent?.trim() === label)!
|
||
}
|
||
|
||
function copyButton(root: ParentNode) {
|
||
return root.querySelector<HTMLElement>('button[aria-label="Copy code"]')!
|
||
}
|
||
|
||
function requestSnippet() {
|
||
return headingBlock('Request an image')!
|
||
}
|
||
|
||
function builder() {
|
||
return headingBlock('URL builder')!
|
||
}
|
||
|
||
function urlSnippet() {
|
||
return builder().querySelector<HTMLElement>('pre')!.parentElement!
|
||
}
|
||
|
||
function variantTable() {
|
||
return images().querySelector<HTMLElement>('[role="table"][aria-label="Variants"]')
|
||
}
|
||
|
||
function variantRows() {
|
||
const body = variantTable()?.querySelectorAll('[role="rowgroup"]')[1]
|
||
return Array.from(body?.querySelectorAll<HTMLElement>('[role="row"]') ?? [])
|
||
}
|
||
|
||
function cells(row: HTMLElement) {
|
||
return Array.from(row.querySelectorAll('[role="cell"]')).map((cell) => cell.textContent?.trim())
|
||
}
|
||
|
||
function responseRows() {
|
||
const table = images().querySelector('[role="table"][aria-label="Responses"]')
|
||
return Array.from(table?.querySelectorAll<HTMLElement>('[role="rowgroup"]')[1]!.children ?? [])
|
||
}
|
||
|
||
function alerts() {
|
||
return Array.from(images().querySelectorAll<HTMLElement>('[role="alert"]'))
|
||
}
|
||
|
||
function button(label: string) {
|
||
return Array.from(images().querySelectorAll('button, a')).find(
|
||
(item) => item.textContent?.trim() === label,
|
||
) as HTMLElement | undefined
|
||
}
|
||
|
||
function preview() {
|
||
return images().querySelector<HTMLElement>('div[role="img"]')!
|
||
}
|
||
|
||
function variantTrigger() {
|
||
return builder().querySelector<HTMLElement>('button[aria-label="Variant"]')!
|
||
}
|
||
|
||
async function pickVariant(name: string) {
|
||
variantTrigger().click()
|
||
await flushPromises()
|
||
const option = Array.from(document.body.querySelectorAll<HTMLElement>('[role="option"]')).find(
|
||
(item) => item.textContent?.trim() === name,
|
||
)!
|
||
option.click()
|
||
await flushPromises()
|
||
}
|
||
|
||
async function focusWindow() {
|
||
window.dispatchEvent(new Event('focus'))
|
||
await flushPromises()
|
||
}
|
||
|
||
afterEach(async () => {
|
||
wrapper?.unmount()
|
||
wrapper = null
|
||
vi.unstubAllGlobals()
|
||
vi.useRealTimers()
|
||
document.body.innerHTML = ''
|
||
await router.push('/login')
|
||
})
|
||
|
||
beforeEach(() => {
|
||
vi.mocked(useApi).mockReset()
|
||
})
|
||
|
||
describe('I1. Many variants', () => {
|
||
it('draws the section in order with the snippets, the table, the builder and the responses', async () => {
|
||
await mountPage()
|
||
|
||
const anchors = Array.from(document.body.querySelectorAll('[data-anchor]')).map((item) =>
|
||
item.getAttribute('data-anchor'),
|
||
)
|
||
expect(anchors.filter((id) => SECTION_ORDER.includes(id!))).toEqual(SECTION_ORDER)
|
||
expect(images().querySelector('h2')?.textContent?.trim()).toBe('Images and variants')
|
||
expect(
|
||
Array.from(document.body.querySelectorAll('nav[aria-label="On this page"] button')).map(
|
||
(item) => item.textContent?.trim(),
|
||
),
|
||
).toEqual(expect.arrayContaining(['Images and variants']))
|
||
const labels = Array.from(
|
||
document.body.querySelectorAll('nav[aria-label="On this page"] button'),
|
||
).map((item) => item.textContent?.trim())
|
||
expect(labels.indexOf('Images and variants')).toBe(labels.indexOf('Sites and authors') + 1)
|
||
|
||
const request = requestSnippet()
|
||
expect(tabs(request).map((tab) => tab.textContent?.trim())).toEqual([
|
||
'curl',
|
||
'HTML',
|
||
'JavaScript',
|
||
])
|
||
expect(
|
||
tabs(request).find((tab) => tab.getAttribute('aria-selected') === 'true')?.textContent,
|
||
).toBe('HTML')
|
||
expect(request.querySelector('pre')?.textContent).toBe(HTML)
|
||
tabNamed(request, 'curl').click()
|
||
await flushPromises()
|
||
expect(request.querySelector('pre')?.textContent).toBe(CURL)
|
||
tabNamed(request, 'JavaScript').click()
|
||
await flushPromises()
|
||
expect(request.querySelector('pre')?.textContent).toBe(JAVASCRIPT)
|
||
|
||
expect(normalized(headingBlock('Variants'))).toContain('12 enabled variants')
|
||
const headers = Array.from(variantTable()!.querySelectorAll('[role="columnheader"]')).map(
|
||
(item) => item.textContent?.trim(),
|
||
)
|
||
expect(headers).toEqual(['Name', 'Dimensions', 'Format', 'Example URL'])
|
||
const rows = variantRows()
|
||
expect(rows).toHaveLength(12)
|
||
expect(rows.map((row) => cells(row)[0])).toEqual(BOARD_ROWS.map(([name]) => name))
|
||
expect(cells(rows[0]!)).toEqual([
|
||
'thumb',
|
||
'400×300',
|
||
'WebP',
|
||
'/media/north-wall.jpg?v=thumb',
|
||
])
|
||
expect(cells(rows[1]!)[1]).toBe('800 × auto')
|
||
expect(normalized(variantTable())).not.toContain('print')
|
||
expect(variantTable()!.querySelectorAll('[role="rowgroup"]')[1]!.className).toContain(
|
||
'max-h-66',
|
||
)
|
||
expect(normalized(headingBlock('Variants'))).toContain(
|
||
'Disabled variants are not served. A request for one returns 400 "Unknown variant".',
|
||
)
|
||
|
||
const fields = normalized(builder())
|
||
expect(fields).toContain('Image')
|
||
expect(fields).toContain('north-wall.jpg')
|
||
expect(fields).toContain('Variant')
|
||
expect(variantTrigger().textContent?.trim()).toBe('thumb')
|
||
expect(urlSnippet().querySelector('pre')?.textContent).toBe(`${BASE}?v=thumb`)
|
||
expect(tabs(urlSnippet())).toHaveLength(0)
|
||
expect(urlSnippet().querySelector('[role="tablist"]')?.textContent?.trim()).toBe('URL')
|
||
expect(normalized(headingBlock('Responses'))).toContain('/media/photo.png?v=thumb')
|
||
})
|
||
})
|
||
|
||
describe('I2. One variant', () => {
|
||
it('uses the single variant everywhere and draws no scroll limit and no disabled note', async () => {
|
||
await mountPage({
|
||
list: listing([variant({ name: 'thumb', width: 400, height: 300 })]),
|
||
})
|
||
|
||
expect(normalized(headingBlock('Variants'))).toContain('1 enabled variant')
|
||
expect(normalized(headingBlock('Variants'))).not.toContain('1 enabled variants')
|
||
expect(variantRows()).toHaveLength(1)
|
||
expect(variantTable()!.querySelectorAll('[role="rowgroup"]')[1]!.className).not.toContain(
|
||
'max-h-66',
|
||
)
|
||
const request = requestSnippet()
|
||
expect(request.querySelector('pre')?.textContent).toBe(
|
||
HTML.replaceAll('?v=card', '?v=thumb').replaceAll('?v=hero', '?v=thumb'),
|
||
)
|
||
expect(normalized(images())).not.toContain('Disabled variants are not served')
|
||
})
|
||
})
|
||
|
||
describe('I3. No variants, admin', () => {
|
||
it.each([
|
||
['an empty list', listing([])],
|
||
['only disabled variants', listing([variant({ name: 'print', enabled: false })])],
|
||
])('shows the empty block with the link for %s', async (_name, list) => {
|
||
await mountPage({ role: 'admin', list })
|
||
|
||
const text = normalized(images())
|
||
expect(text).toContain('No image variants yet')
|
||
expect(text).toContain(
|
||
'Until you add a variant, any request with the ?v= parameter returns status 400 unknown_variant. Requests without it return the original image.',
|
||
)
|
||
const link = button('Go to image variants') as HTMLAnchorElement
|
||
expect(link.getAttribute('href')).toBe('/settings/media/variants')
|
||
const request = requestSnippet()
|
||
tabNamed(request, 'curl').click()
|
||
await flushPromises()
|
||
expect(request.querySelector('pre')?.textContent).toBe(`curl -O "${BASE}?v=<variant-name>"`)
|
||
expect(variantTable()).toBeNull()
|
||
expect(headingBlock('URL builder')).toBeUndefined()
|
||
expect(normalized(headingBlock('Responses'))).toContain('/media/photo.png?v=thumb')
|
||
})
|
||
})
|
||
|
||
describe('I4. No variants, not an admin', () => {
|
||
it.each([['member'], [null]])(
|
||
'sends no request and shows plain information for role %s',
|
||
async (role) => {
|
||
await mountPage({ role })
|
||
await focusWindow()
|
||
|
||
expect(requestedPaths()).toEqual([])
|
||
const text = normalized(images())
|
||
expect(text).toContain('No image variants yet')
|
||
expect(text).toContain(
|
||
'Ask an owner or admin to add one. Until you do, any request with the ?v= parameter returns status 400 unknown_variant. Requests without it return the original image.',
|
||
)
|
||
expect(button('Go to image variants')).toBeUndefined()
|
||
expect(images().querySelectorAll('a')).toHaveLength(0)
|
||
expect(headingBlock('URL builder')).toBeUndefined()
|
||
expect(requestSnippet().querySelector('pre')?.textContent).toContain('<variant-name>')
|
||
},
|
||
)
|
||
|
||
it('shows the same state when an owner gets a 403', async () => {
|
||
await mountPage({ list: () => Promise.reject(new ApiError(403)) })
|
||
|
||
expect(normalized(images())).toContain(
|
||
'Ask an owner or admin to add one. Until you do, any request with the ?v= parameter returns status 400 unknown_variant. Requests without it return the original image.',
|
||
)
|
||
expect(images().querySelectorAll('a')).toHaveLength(0)
|
||
expect(headingBlock('URL builder')).toBeUndefined()
|
||
})
|
||
})
|
||
|
||
describe('I5. Variant removed', () => {
|
||
function assertRemoved() {
|
||
expect(variantTrigger().textContent?.trim()).toBe('hero (removed)')
|
||
expect(urlSnippet().querySelector('pre')?.textContent).toBe(`${BASE}?v=hero`)
|
||
const warning = alerts().find((item) => item.textContent?.includes('no longer exists'))!
|
||
expect(warning.textContent).toContain('The variant "hero" no longer exists')
|
||
expect(normalized(warning)).toContain(
|
||
`There is no fallback to the original. Requests with ?v=hero return status 400 with the body ${BODY_HERO}. Remove the parameter to get the original image, or pick another variant.`,
|
||
)
|
||
const box = preview()
|
||
expect(box.getAttribute('aria-label')).toBe('No preview: unknown variant')
|
||
expect(box.textContent?.trim()).toBe('No preview: unknown variant')
|
||
expect(box.getAttribute('style')).toContain('4 / 3')
|
||
expect(box.className).toContain('opacity-50')
|
||
}
|
||
|
||
it('shows the warning after a window focus reload drops the selected variant', async () => {
|
||
await mountPage()
|
||
await pickVariant('hero')
|
||
expect(listCalls()).toHaveLength(1)
|
||
listHandler = listing(boardVariants().filter((item) => item.name !== 'hero'))
|
||
|
||
await focusWindow()
|
||
|
||
expect(listCalls()).toHaveLength(2)
|
||
assertRemoved()
|
||
})
|
||
|
||
it('shows the warning after Try again when the first load failed and the list lacks the selection', async () => {
|
||
await mountPage({ list: () => Promise.reject(new ApiError(500)) })
|
||
listHandler = listing(boardVariants())
|
||
button('Try again')!.click()
|
||
await flushPromises()
|
||
await pickVariant('hero')
|
||
listHandler = listing(boardVariants().filter((item) => item.name !== 'hero'))
|
||
|
||
await focusWindow()
|
||
|
||
assertRemoved()
|
||
})
|
||
})
|
||
|
||
describe('I6. Loading', () => {
|
||
it('draws three skeletons in a busy region and nothing else of the section', async () => {
|
||
const pending = deferred<unknown>()
|
||
await mountPage({ list: () => pending.promise })
|
||
|
||
const busy = images().querySelector('[aria-busy="true"]')!
|
||
expect(busy.children).toHaveLength(3)
|
||
expect(images().querySelector('h2')?.textContent?.trim()).toBe('Images and variants')
|
||
expect(images().querySelector('h3')).toBeNull()
|
||
expect(variantTable()).toBeNull()
|
||
expect(images().querySelector('pre')).toBeNull()
|
||
pending.resolve({ items: [] })
|
||
})
|
||
})
|
||
|
||
describe('I7. Error loading', () => {
|
||
it.each([
|
||
['status 500', () => Promise.reject(new ApiError(500))],
|
||
['a network failure', () => Promise.reject(new NetworkError())],
|
||
])('shows the alert, the placeholder snippet and retries for %s', async (_name, failing) => {
|
||
await mountPage({ list: failing })
|
||
|
||
const [alert] = alerts()
|
||
expect(alert!.textContent).toContain("Couldn't load image variants")
|
||
expect(alert!.textContent).toContain(
|
||
'The snippets below still work. The variant list and the URL builder need a connection.',
|
||
)
|
||
expect(requestSnippet().querySelector('pre')?.textContent).toContain('<variant-name>')
|
||
expect(variantTable()).toBeNull()
|
||
expect(headingBlock('URL builder')).toBeUndefined()
|
||
expect(headingBlock('Responses')).toBeUndefined()
|
||
|
||
listHandler = listing(boardVariants())
|
||
button('Try again')!.click()
|
||
await flushPromises()
|
||
|
||
expect(listCalls()).toHaveLength(2)
|
||
expect(alerts()).toHaveLength(0)
|
||
expect(variantRows()).toHaveLength(12)
|
||
})
|
||
})
|
||
|
||
describe('I8. Snippet copied', () => {
|
||
it('copies the active tab, reads Copied politely for 2 s and leaves the builder alone', async () => {
|
||
const writeText = stubClipboard()
|
||
await mountPage()
|
||
vi.useFakeTimers()
|
||
|
||
copyButton(requestSnippet()).click()
|
||
await flushPromises()
|
||
|
||
expect(writeText).toHaveBeenCalledWith(HTML)
|
||
const live = requestSnippet().querySelector('[aria-live="polite"]')!
|
||
expect(live.textContent).toBe('Copied')
|
||
expect(copyButton(urlSnippet()).textContent?.trim()).toBe('Copy')
|
||
|
||
await vi.advanceTimersByTimeAsync(1999)
|
||
expect(live.textContent).toBe('Copied')
|
||
await vi.advanceTimersByTimeAsync(1)
|
||
expect(live.textContent).toBe('Copy')
|
||
})
|
||
})
|
||
|
||
describe('The URL builder', () => {
|
||
it('builds the URL and the placeholder preview for the picked variant', async () => {
|
||
await mountPage()
|
||
|
||
await pickVariant('square')
|
||
|
||
expect(urlSnippet().querySelector('pre')?.textContent).toBe(`${BASE}?v=square`)
|
||
const box = preview()
|
||
expect(box.getAttribute('role')).toBe('img')
|
||
expect(box.getAttribute('aria-label')).toBe('square · 600×600 · JPEG')
|
||
expect(box.textContent?.trim()).toBe('square · 600×600 · JPEG')
|
||
expect(box.getAttribute('style')).toContain('600 / 600')
|
||
expect(box.className).toContain('max-h-60')
|
||
expect(images().querySelectorAll('img')).toHaveLength(0)
|
||
expect(requestedPaths()).toEqual([LIST_PATH])
|
||
})
|
||
})
|
||
|
||
describe('The preview ratio', () => {
|
||
it('falls back to 4 / 3 when a dimension is null', async () => {
|
||
await mountPage()
|
||
|
||
await pickVariant('card')
|
||
|
||
expect(preview().getAttribute('style')).toContain('4 / 3')
|
||
expect(preview().getAttribute('aria-label')).toBe('card · 800 × auto · WebP')
|
||
})
|
||
})
|
||
|
||
describe('I9. URL builder copied', () => {
|
||
it('copies the exact URL and leaves the request snippet alone', async () => {
|
||
const writeText = stubClipboard()
|
||
await mountPage()
|
||
vi.useFakeTimers()
|
||
|
||
copyButton(urlSnippet()).click()
|
||
await flushPromises()
|
||
|
||
expect(writeText).toHaveBeenCalledWith(`${BASE}?v=thumb`)
|
||
expect(urlSnippet().querySelector('[aria-live="polite"]')?.textContent).toBe('Copied')
|
||
expect(copyButton(requestSnippet()).textContent?.trim()).toBe('Copy')
|
||
await vi.advanceTimersByTimeAsync(2000)
|
||
expect(urlSnippet().querySelector('[aria-live="polite"]')?.textContent).toBe('Copy')
|
||
})
|
||
})
|
||
|
||
describe('Reload triggers', () => {
|
||
it('sends one call on mount, one per focus and one per Try again', async () => {
|
||
await mountPage()
|
||
expect(listCalls()).toHaveLength(1)
|
||
|
||
await focusWindow()
|
||
await focusWindow()
|
||
expect(listCalls()).toHaveLength(3)
|
||
expect(requestedPaths()).toEqual([LIST_PATH, LIST_PATH, LIST_PATH])
|
||
})
|
||
|
||
it('keeps the active tab and the selection when the variant is still enabled', async () => {
|
||
await mountPage()
|
||
tabNamed(requestSnippet(), 'JavaScript').click()
|
||
await flushPromises()
|
||
await pickVariant('square')
|
||
|
||
await focusWindow()
|
||
|
||
const request = requestSnippet()
|
||
expect(
|
||
tabs(request)
|
||
.find((tab) => tab.getAttribute('aria-selected') === 'true')
|
||
?.textContent?.trim(),
|
||
).toBe('JavaScript')
|
||
expect(variantTrigger().textContent?.trim()).toBe('square')
|
||
expect(urlSnippet().querySelector('pre')?.textContent).toBe(`${BASE}?v=square`)
|
||
})
|
||
|
||
it('stops listening to focus once the page is gone', async () => {
|
||
await mountPage()
|
||
wrapper!.unmount()
|
||
wrapper = null
|
||
|
||
await focusWindow()
|
||
|
||
expect(listCalls()).toHaveLength(1)
|
||
})
|
||
})
|
||
|
||
describe('The Responses table', () => {
|
||
it('lists the six rows with the exact bodies and no 409 or 422', async () => {
|
||
await mountPage()
|
||
|
||
const headers = Array.from(
|
||
images().querySelectorAll('[aria-label="Responses"] [role="columnheader"]'),
|
||
).map((item) => item.textContent?.trim())
|
||
expect(headers).toEqual(['Request', 'Status', 'Response'])
|
||
const rows = responseRows().map((row) =>
|
||
Array.from(row.children)
|
||
.map((cell) => normalized(cell))
|
||
.join(' '),
|
||
)
|
||
expect(rows).toEqual([
|
||
'/media/photo.png?v=thumb 200 The variant image. Generated on first request.',
|
||
`/media/photo.png?v=unknown 400 Unknown variant ${BODY_UNKNOWN}`,
|
||
`/media/photo.png?width=400 400 Unsupported parameter ${BODY_UNSUPPORTED}`,
|
||
'/media/missing.png?v=thumb 404 Not found 404 page not found',
|
||
'/media/huge.png?v=thumb 413 Source image too large The source image is too large to generate this variant.',
|
||
'/media/photo.png?v=thumb 500 Unable to read the file Unable to read the file',
|
||
])
|
||
const statuses = responseRows().map((row) => row.querySelectorAll('span')[1]!.textContent)
|
||
for (const absent of ['409', '422']) {
|
||
expect(statuses).not.toContain(absent)
|
||
}
|
||
expect(images().querySelector('[aria-label="Responses"]')!.className).toContain('min-w-140')
|
||
expect(variantTable()!.className).toContain('min-w-130')
|
||
})
|
||
})
|
||
|
||
describe('The error sample label', () => {
|
||
it('keeps "Error response · text/plain" and labels nothing in the images section text/plain', async () => {
|
||
await mountPage()
|
||
|
||
const errors = document.body.querySelector('[data-anchor="errors"] [role="tablist"]')!
|
||
expect(errors.textContent?.trim()).toBe('Error response · text/plain')
|
||
expect(normalized(images())).not.toContain('text/plain')
|
||
})
|
||
})
|
||
|
||
describe('Calls made by the page', () => {
|
||
it('sends only the list call for an owner and for an admin', async () => {
|
||
await mountPage({ role: 'admin' })
|
||
|
||
expect(requestedPaths()).toEqual([LIST_PATH])
|
||
})
|
||
|
||
it('sends nothing for any other role', async () => {
|
||
await mountPage({ role: 'member' })
|
||
|
||
expect(requestedPaths()).toEqual([])
|
||
})
|
||
})
|