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']) }) })