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.
This commit is contained in:
128
tests/elements/text.test.ts
Normal file
128
tests/elements/text.test.ts
Normal file
@@ -0,0 +1,128 @@
|
||||
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'])
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user