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('.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('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' }, ]) }) })