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.
129 lines
5.3 KiB
TypeScript
129 lines
5.3 KiB
TypeScript
import { afterEach, beforeAll, describe, expect, it } from 'vitest'
|
|
import { rule } from './css'
|
|
|
|
// E1 golem15-seo-text (UI-SPEC C4): the status thresholds with a mocked
|
|
// canvas measureText, and the counter line's layout contract at 320px.
|
|
|
|
// The mocked canvas: width(text) answers the pixel width of a string.
|
|
let width: (text: string) => number = (text) => text.length * 10
|
|
HTMLCanvasElement.prototype.getContext = function () {
|
|
return { font: '', measureText: (text: string) => ({ width: width(text) }) } as unknown as CanvasRenderingContext2D
|
|
} as unknown as typeof HTMLCanvasElement.prototype.getContext
|
|
|
|
type Measure = typeof import('../../assets/src/elements/measure')
|
|
let m: Measure
|
|
let labels: typeof import('../../assets/src/elements/labels')
|
|
|
|
beforeAll(async () => {
|
|
m = await import('../../assets/src/elements/measure')
|
|
labels = await import('../../assets/src/elements/labels')
|
|
await import('../../assets/src/elements/index')
|
|
})
|
|
|
|
afterEach(() => {
|
|
width = (text) => text.length * 10
|
|
document.body.replaceChildren()
|
|
})
|
|
|
|
/** A string of n characters (with spaces, so it reads like text). */
|
|
function chars(n: number): string {
|
|
return Array.from({ length: n }, (_, i) => (i % 6 === 5 ? ' ' : 'a')).join('').replace(/ $/, 'a')
|
|
}
|
|
|
|
describe('E1 status thresholds', () => {
|
|
it('title: 29 characters are short, 30 are good', () => {
|
|
width = () => 300
|
|
expect(m.status(chars(29), 'title').status).toBe('short')
|
|
expect(m.status(chars(30), 'title').status).toBe('good')
|
|
})
|
|
|
|
it('title: 580 px is good, 581 px is long', () => {
|
|
width = () => 580
|
|
expect(m.status(chars(40), 'title')).toMatchObject({ status: 'good', px: 580, maxPx: 580, chars: 40 })
|
|
width = () => 581
|
|
expect(m.status(chars(40), 'title')).toMatchObject({ status: 'long', px: 581 })
|
|
})
|
|
|
|
it('title: pixels decide long before characters decide short', () => {
|
|
width = () => 581
|
|
expect(m.status(chars(10), 'title').status).toBe('long')
|
|
})
|
|
|
|
it('description: 119 characters are short, 120 are good', () => {
|
|
width = () => 700
|
|
expect(m.status(chars(119), 'description').status).toBe('short')
|
|
expect(m.status(chars(120), 'description').status).toBe('good')
|
|
})
|
|
|
|
it('description: 920 px is good, 921 px is long', () => {
|
|
width = () => 920
|
|
expect(m.status(chars(130), 'description')).toMatchObject({ status: 'good', maxPx: 920 })
|
|
width = () => 921
|
|
expect(m.status(chars(130), 'description').status).toBe('long')
|
|
})
|
|
|
|
it('an empty or blank value is empty for both measures', () => {
|
|
for (const measure of ['title', 'description'] as const) {
|
|
expect(m.status('', measure).status).toBe('empty')
|
|
expect(m.status(' ', measure)).toMatchObject({ status: 'empty', chars: 0 })
|
|
}
|
|
})
|
|
|
|
it('characters are counted as code points after trimming', () => {
|
|
width = () => 100
|
|
expect(m.status(' Zażółć gęślą ', 'title').chars).toBe(12)
|
|
})
|
|
|
|
it('the element shows the status and the counter in both languages', () => {
|
|
for (const locale of ['en', 'pl']) {
|
|
const L = labels.labelsFor(locale)
|
|
const node = document.createElement('golem15-seo-text') as HTMLElement & { value: string }
|
|
node.setAttribute('ui-locale', locale)
|
|
node.setAttribute('options', JSON.stringify({ measure: 'title' }))
|
|
document.body.append(node)
|
|
width = () => 400
|
|
node.value = chars(29)
|
|
expect(node.dataset.status).toBe('short')
|
|
expect(node.querySelector('.seo-text__status-text')?.textContent).toBe(L.shortTitle)
|
|
width = () => 581
|
|
node.value = chars(35)
|
|
expect(node.dataset.status).toBe('long')
|
|
expect(node.querySelector('.seo-text__status-text')?.textContent).toBe(L.longTitle)
|
|
expect(node.querySelector('.seo-text__count')?.textContent).toContain('581')
|
|
}
|
|
})
|
|
|
|
it('typing emits summer-change with the right-trimmed value', () => {
|
|
const node = document.createElement('golem15-seo-text') as HTMLElement & { value: string }
|
|
document.body.append(node)
|
|
const seen: string[] = []
|
|
node.addEventListener('summer-change', (ev) => seen.push((ev as CustomEvent).detail.value))
|
|
const input = node.querySelector('input') as HTMLInputElement
|
|
input.value = 'Grzyby leśne '
|
|
input.dispatchEvent(new Event('input'))
|
|
expect(seen).toEqual(['Grzyby leśne'])
|
|
expect(node.value).toBe('Grzyby leśne')
|
|
})
|
|
})
|
|
|
|
describe('E1 counter line at 320px', () => {
|
|
// The layout contract that makes the two parts wrap without overlapping.
|
|
it('the status and the count are separate items of a wrapping flex line with a gap', () => {
|
|
const counter = rule('golem15-seo-text .seo-text__counter')
|
|
expect(counter.display).toBe('flex')
|
|
expect(counter['flex-wrap']).toBe('wrap')
|
|
expect(Number.parseFloat(counter.gap)).toBeGreaterThan(0)
|
|
for (const part of ['golem15-seo-text .seo-text__status', 'golem15-seo-text .seo-text__count']) {
|
|
const decls = rule(part)
|
|
expect(decls.position ?? 'static').toBe('static')
|
|
expect(decls.width).toBeUndefined()
|
|
expect(decls['white-space']).toBeUndefined()
|
|
}
|
|
const node = document.createElement('golem15-seo-text')
|
|
node.style.width = '320px'
|
|
document.body.append(node)
|
|
const line = node.querySelector('.seo-text__counter') as HTMLElement
|
|
expect(Array.from(line.children).map((c) => c.className)).toEqual(['seo-text__status', 'seo-text__count'])
|
|
})
|
|
})
|