Files
sm-seo-plugin/tests/elements/robots.test.ts
Jakub Zych dcfbdc05ed test: element backstops
vitest + happy-dom tests of the UI-SPEC backstops: E1 status thresholds
(29/30 characters, 580/581 px; 119/120 and 920/921 for descriptions)
with a mocked measureText and the counter's wrap contract; the E2 grammar
over every directive combination, the emitted values and the stacking
contract with unclipped en and pl labels; the E3 effective title,
word-boundary truncation and keyphrase stem matching; each E4 lint rule
and a clean file; E6 apply gating. happy-dom has no layout engine, so the
narrow-width rows check the stylesheet's layout contract.
2026-10-27 13:26:59 +01:00

140 lines
6.0 KiB
TypeScript

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', 'none', 'noodp']) {
expect(parseRobots(bad), bad).toBeNull()
}
})
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()
}
})
})