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.
140 lines
6.0 KiB
TypeScript
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()
|
|
}
|
|
})
|
|
})
|