import { afterEach, beforeAll, describe, expect, it } from 'vitest' import { advancedCount, emptyRobots, parseRobots, serializeRobots, type Robots } from '../../assets/src/elements/robots' import { labelsFor } from '../../assets/src/elements/labels' import { rule } from './css' // E2 golem15-seo-robots (UI-SPEC C5): the value grammar shared with // classes/robots_value.go, the element's emitted values, and the stacking // contract of the two segmented groups at 360px. beforeAll(async () => { await import('../../assets/src/elements/index') }) afterEach(() => { document.body.replaceChildren() }) /** Every combination of the grammar's directives (representative arguments). */ function combinations(): Robots[] { const out: Robots[] = [] for (const index of ['', 'index', 'noindex'] as const) { for (const follow of ['', 'follow', 'nofollow'] as const) { for (let flags = 0; flags < 8; flags++) { for (const maxSnippet of [null, -1, 0, 160]) { for (const maxImagePreview of ['', 'none', 'standard', 'large'] as const) { for (const maxVideoPreview of [null, -1, 30]) { for (const unavailableAfter of ['', '2027-01-31']) { out.push({ index, follow, noarchive: (flags & 1) !== 0, nosnippet: (flags & 2) !== 0, noimageindex: (flags & 4) !== 0, maxSnippet, maxImagePreview, maxVideoPreview, unavailableAfter, }) } } } } } } } return out } describe('E2 value grammar', () => { it('every directive combination round-trips in the fixed order', () => { const all = combinations() expect(all.length).toBe(3 * 3 * 8 * 4 * 4 * 3 * 2) const order = ['index', 'noindex', 'follow', 'nofollow', 'noarchive', 'nosnippet', 'noimageindex', 'max-snippet', 'max-image-preview', 'max-video-preview', 'unavailable_after'] for (const r of all) { const value = serializeRobots(r) expect(parseRobots(value)).toEqual(r) const names = value === '' ? [] : value.split(',').map((d) => d.split(':')[0]) const positions = names.map((n) => order.indexOf(n)) expect(positions).toEqual([...positions].sort((a, b) => a - b)) } }) it('any order, case and spacing reads back in the fixed order', () => { expect(serializeRobots(parseRobots(' MAX-SNIPPET:5 , nofollow,NOINDEX ') as Robots)).toBe('noindex,nofollow,max-snippet:5') expect(serializeRobots(parseRobots('unavailable_after:2027-01-01,noarchive,index') as Robots)).toBe('index,noarchive,unavailable_after:2027-01-01') }) it('all-inherit is the empty string', () => { expect(serializeRobots(emptyRobots())).toBe('') expect(parseRobots('')).toEqual(emptyRobots()) expect(parseRobots(' ')).toEqual(emptyRobots()) expect(advancedCount(emptyRobots())).toBe(0) }) it('malformed values are refused like the Go grammar', () => { for (const bad of [',', 'index,', 'index,noindex', 'follow,nofollow', 'noarchive,noarchive', 'index:1', 'noarchive:yes', 'max-snippet', 'max-snippet:abc', 'max-snippet:-2', 'max-snippet:1.5', 'max-image-preview:huge', 'unavailable_after:2027-13-01', 'unavailable_after:27-01-01', 'unavailable_after:2027-02-30', 'unavailable_after:2027-02-29', 'unavailable_after:2027-04-31', 'unavailable_after:2027-01-00', 'max-snippet:+-1', 'none', 'noodp']) { expect(parseRobots(bad), bad).toBeNull() } }) it('accepts what the Go grammar accepts (WR-24): a leap day and a signed number', () => { expect(serializeRobots(parseRobots('unavailable_after:2028-02-29') as Robots)).toBe('unavailable_after:2028-02-29') expect(serializeRobots(parseRobots('max-snippet:+5') as Robots)).toBe('max-snippet:5') expect(serializeRobots(parseRobots('max-video-preview:-1') as Robots)).toBe('max-video-preview:-1') }) it('a stored value it cannot parse is kept, shown and replaced only by Reset (WR-24)', () => { const node = document.createElement('golem15-seo-robots') as HTMLElement & { value: string } node.setAttribute('ui-locale', 'en') document.body.append(node) node.value = 'noindex,unavailable_after:2027-02-30' const seen: string[] = [] node.addEventListener('summer-change', (ev) => seen.push((ev as CustomEvent).detail.value)) expect(node.querySelector('.seo-robots__unparsed')?.textContent).toContain('noindex,unavailable_after:2027-02-30') const segment = node.querySelector('[data-focus-key="follow-2"]') as HTMLButtonElement expect(segment.disabled).toBe(true) segment.click() expect(seen).toEqual([]) expect(node.value).toBe('noindex,unavailable_after:2027-02-30') ;(node.querySelector('.seo-robots__reset') as HTMLButtonElement).click() expect(seen).toEqual(['']) expect(node.querySelector('.seo-robots__unparsed')).toBeNull() ;(node.querySelector('[data-focus-key="index-2"]') as HTMLButtonElement).click() expect(seen).toEqual(['', 'noindex']) }) it('the element emits the fixed-order value and "" when both groups inherit', () => { const node = document.createElement('golem15-seo-robots') as HTMLElement & { value: string } document.body.append(node) node.value = 'max-snippet:20' const seen: string[] = [] node.addEventListener('summer-change', (ev) => seen.push((ev as CustomEvent).detail.value)) const segment = (key: string, i: number) => node.querySelector(`[data-focus-key="${key}-${i}"]`) as HTMLButtonElement segment('follow', 2).click() segment('index', 2).click() expect(seen).toEqual(['nofollow,max-snippet:20', 'noindex,nofollow,max-snippet:20']) expect(segment('index', 2).getAttribute('aria-checked')).toBe('true') node.value = 'noindex' seen.length = 0 segment('index', 0).click() expect(seen).toEqual(['']) expect(node.value).toBe('') }) it('a disabled element emits nothing', () => { const node = document.createElement('golem15-seo-robots') as HTMLElement & { value: string } node.setAttribute('disabled', '') document.body.append(node) const seen: string[] = [] node.addEventListener('summer-change', (ev) => seen.push((ev as CustomEvent).detail.value)) ;(node.querySelector('[data-focus-key="index-2"]') as HTMLButtonElement).click() expect(seen).toEqual([]) }) }) describe('E2 at 360px', () => { it('the two groups wrap onto their own lines with a gap', () => { const groups = rule('golem15-seo-robots .seo-robots__groups') expect(groups.display).toBe('flex') expect(groups['flex-wrap']).toBe('wrap') expect(Number.parseFloat(groups.gap)).toBeGreaterThan(0) expect(rule('golem15-seo-robots .seo-robots__group')['flex-direction']).toBe('column') }) it('no segment label is clipped in either language', () => { const segment = rule('golem15-seo-robots .seo-robots__segment') for (const clip of ['overflow', 'text-overflow', 'width', 'max-width']) { expect(segment[clip], clip).toBeUndefined() } for (const locale of ['en', 'pl']) { const L = labelsFor(locale) const node = document.createElement('golem15-seo-robots') node.setAttribute('ui-locale', locale) node.style.width = '360px' document.body.append(node) const texts = Array.from(node.querySelectorAll('.seo-robots__segment')).map((s) => s.textContent) expect(texts).toEqual([L.inherit, L.index, L.noindex, L.inherit, L.follow, L.nofollow]) for (const text of texts) { expect(text).not.toMatch(/…$/) } expect(node.querySelectorAll('.seo-robots__groups > .seo-robots__group').length).toBe(2) node.remove() } }) })