- 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
151 lines
6.5 KiB
TypeScript
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' },
|
|
])
|
|
})
|
|
})
|