Files
sm-seo-plugin/tests/elements/links.test.ts
Jakub Zych 075cf17007 test: element tests for E8 types and currency, E9 states and queue, E10 list caps, the parent select
- E8: the page and profile type options, an invalid currency
- E9: the checking line with aria-busy, noRules per type, one queued
  request, the danger line and its recovery
- E10: both lists capped at 10
- the parent select's value parsing, empty default and disabled state
2026-11-04 19:54:06 +01:00

151 lines
6.5 KiB
TypeScript

import { afterEach, beforeAll, describe, expect, it, vi } from 'vitest'
import { labelsFor } from '../../assets/src/elements/labels'
import type { LinksData } from '../../assets/src/elements/links'
import { HostFailure, mountWidget, settle } from './host'
// E10 golem15-seo-links (UI-SPEC C19, D-82).
beforeAll(async () => {
await import('../../assets/src/elements/index')
})
afterEach(() => {
vi.unstubAllGlobals()
vi.useRealTimers()
document.body.replaceChildren()
})
const en = labelsFor('en')
const crawled: LinksData = {
crawledAt: '2026-11-04T10:00:00Z',
inbound: 0,
outbound: 3,
orphan: true,
out: [{ phrase: 'shiitake', title: 'Shiitake', url: 'https://example.com/pages/shiitake' }],
in: [{ phrase: 'lwia grzywa', title: 'Oyster', adminUrl: '/backend/acme/pages/pages/update/3' }],
}
function stubClipboard(): { writes: string[] } {
const writes: string[] = []
vi.stubGlobal('navigator', {
...navigator,
clipboard: {
writeText: (text: string) => {
writes.push(text)
return Promise.resolve()
},
},
})
return { writes }
}
describe('E10 golem15-seo-links', () => {
it('asks for the links of the shown locale on connect and renders counters, the orphan callout and both lists', async () => {
stubClipboard()
const host = mountWidget('golem15-seo-links', { attrs: { 'ui-locale': 'en' }, answer: () => crawled })
await settle()
expect(host.posted).toEqual([{ op: 'links', locale: '' }])
const node = host.node
const values = [...node.querySelectorAll('.summer-stat__value')].map((n) => n.textContent)
expect(values).toEqual(['0', '3'])
expect([...node.querySelectorAll('.summer-stat__label')].map((n) => n.textContent)).toEqual([en.linksInbound, en.linksOutbound])
expect(node.textContent).toContain('From the crawl of ')
expect(node.querySelector('.summer-callout--warning')!.textContent).toContain(en.linksOrphanTitle)
const headings = [...node.querySelectorAll('.seo-links__heading')].map((n) => n.textContent)
expect(headings).toEqual([en.linksOut, en.linksIn])
const out = node.querySelector('.seo-links__list--out .seo-links__row')!
expect(out.textContent).toContain('“shiitake” → Shiitake')
const link = out.querySelector('a')!
expect(link.getAttribute('href')).toBe('https://example.com/pages/shiitake')
expect(link.target).toBe('_blank')
expect(link.rel).toBe('noopener')
const inbound = node.querySelector('.seo-links__list--in .seo-links__row a')!
expect(inbound.getAttribute('href')).toBe('/backend/acme/pages/pages/update/3')
expect(node.querySelector('.seo-links__none')).toBeNull()
expect(node.getAttribute('aria-busy')).toBe('false')
})
it('shows notCrawled instead of counters before a crawl and none under empty lists', async () => {
const host = mountWidget('golem15-seo-links', {
attrs: { 'ui-locale': 'en' },
answer: () => ({ crawledAt: null, inbound: null, outbound: null, orphan: false, out: [], in: [] }),
})
await settle()
const node = host.node
expect(node.querySelector('.summer-stats')).toBeNull()
expect(node.textContent).toContain(en.linksNotCrawled)
expect(node.querySelector('.summer-callout')).toBeNull()
expect(node.querySelectorAll('.seo-links__none')).toHaveLength(2)
expect(node.querySelector('.seo-links__none')!.textContent).toBe(en.linksNone)
})
it('shows the checking line with aria-busy while loading and the danger line on failure', async () => {
const host = mountWidget('golem15-seo-links', {
attrs: { 'ui-locale': 'pl' },
answer: () => {
throw new HostFailure('down')
},
})
await Promise.resolve()
expect(host.node.getAttribute('aria-busy')).toBe('true')
expect(host.node.querySelector('.seo-links__checking')!.getAttribute('role')).toBe('status')
await settle()
const error = host.node.querySelector('.seo-links__error')!
expect(error.textContent).toBe(labelsFor('pl').linksFailed)
expect(error.getAttribute('role')).toBe('status')
expect(host.node.getAttribute('aria-busy')).toBe('false')
})
it('copies the address and says so for two seconds', async () => {
const clip = stubClipboard()
const host = mountWidget('golem15-seo-links', { attrs: { 'ui-locale': 'en' }, answer: () => crawled })
await settle()
vi.useFakeTimers()
const button = host.node.querySelector<HTMLButtonElement>('.seo-links__copy')!
expect(button.textContent).toBe(en.linksCopy)
button.click()
await Promise.resolve()
await Promise.resolve()
expect(clip.writes).toEqual(['https://example.com/pages/shiitake'])
expect(host.node.querySelector('.seo-links__status')!.textContent).toBe(en.linksCopied)
vi.advanceTimersByTime(2000)
expect(host.node.querySelector('.seo-links__status')!.textContent).toBe('')
})
it('hides Copy address without a clipboard and keeps the address as text', async () => {
vi.stubGlobal('navigator', { ...navigator, clipboard: undefined })
const host = mountWidget('golem15-seo-links', { attrs: { 'ui-locale': 'en' }, answer: () => crawled })
await settle()
expect(host.node.querySelector('.seo-links__copy')).toBeNull()
expect(host.node.querySelector('.seo-links__url')!.textContent).toBe('https://example.com/pages/shiitake')
})
it('lists at most 10 pages in each list', async () => {
const many: LinksData = {
...crawled,
orphan: false,
out: Array.from({ length: 14 }, (_, i) => ({ phrase: `p${i}`, title: `Out ${i}`, url: `https://example.com/o/${i}` })),
in: Array.from({ length: 12 }, (_, i) => ({ phrase: `q${i}`, title: `In ${i}`, adminUrl: `/backend/x/${i}` })),
}
const host = mountWidget('golem15-seo-links', { attrs: { 'ui-locale': 'en' }, answer: () => many })
await settle()
expect(host.node.querySelectorAll('.seo-links__list--out .seo-links__row')).toHaveLength(10)
expect(host.node.querySelectorAll('.seo-links__list--in .seo-links__row')).toHaveLength(10)
expect(host.node.textContent).toContain('Out 9')
expect(host.node.textContent).not.toContain('Out 10')
expect(host.node.querySelector('.seo-links__orphan')).toBeNull()
})
it('asks again on the ML locale switch', async () => {
const host = mountWidget('golem15-seo-links', { attrs: { 'ui-locale': 'en' }, answer: () => crawled })
await settle()
window.dispatchEvent(new CustomEvent('summer:ml-locale', { detail: 'pl' }))
await settle()
expect(host.posted).toEqual([
{ op: 'links', locale: '' },
{ op: 'links', locale: 'pl' },
])
})
})