Files
TREK/client/tests/unit/studio/flags.test.ts
T
jubnl 77dddd5e8a v4.0.0 (#1719)
Release branch for 4.0.0.
2026-08-27 13:22:39 +02:00

172 lines
6.1 KiB
TypeScript

import { describe, it, expect } from 'vitest'
import { COUNTRY_SHAPES } from '../../../src/components/Studio/countryShapes'
import {
FLAG_H, FLAG_SPECS, FLAG_W, flagBands, flagDisc, flagSpec,
} from '../../../src/components/Studio/flags'
/**
* The drawn flags (#1973).
*
* The rule that matters is the one about restraint: a flag is listed only when
* its construction is exact. A wrong flag in a printed book is worse than a
* country outline, so the guard here is as much about what is *absent* as
* about what is drawn.
*/
describe('the flag table', () => {
it('uses two-letter uppercase codes throughout', () => {
for (const code of Object.keys(FLAG_SPECS)) {
expect(code, code).toMatch(/^[A-Z]{2}$/)
}
})
it('gives every colour as #rrggbb, so it can be handed straight to SVG', () => {
for (const [code, spec] of Object.entries(FLAG_SPECS)) {
const colours = spec.t === 'h' || spec.t === 'v'
? spec.c
: spec.t === 'nordic'
? [spec.field, spec.cross, ...(spec.inner ? [spec.inner] : [])]
: spec.t === 'cross'
? [spec.field, spec.cross]
: [spec.field, spec.disc]
for (const c of colours) expect(c, `${code}: ${c}`).toMatch(/^#[0-9a-f]{6}$/)
}
})
it('has at least two bands on any striped flag', () => {
for (const [code, spec] of Object.entries(FLAG_SPECS)) {
if (spec.t === 'h' || spec.t === 'v') {
expect(spec.c.length, code).toBeGreaterThanOrEqual(2)
}
}
})
/*
* The point of the whole approach: anything that cannot be built from bands,
* a cross or a disc is left out rather than approximated. These are the flags
* most likely to be reached for and wrongly added — each carries an emblem or
* unequal bands that this construction cannot express.
*/
it('leaves out the flags it cannot draw exactly', () => {
for (const code of ['GB', 'US', 'ES', 'PT', 'MX', 'BR', 'IN', 'EG', 'CA', 'AU', 'CO', 'LV', 'GR', 'TR']) {
expect(FLAG_SPECS[code], code).toBeUndefined()
}
})
it('has a country silhouette for every flag it does not draw', () => {
// The fallback has to exist, or a missing flag becomes bare letters.
for (const code of ['GB', 'US', 'ES', 'PT', 'MX', 'BR', 'IN', 'CA', 'AU', 'TR', 'GR']) {
expect(COUNTRY_SHAPES[code], code).toBeTruthy()
}
})
})
describe('flagBands', () => {
it('splits horizontal bands evenly across the height', () => {
const bands = flagBands({ t: 'h', c: ['#000000', '#dd0000', '#ffce00'] })
expect(bands).toHaveLength(3)
for (const b of bands) {
expect(b.w).toBe(FLAG_W)
expect(b.h).toBeCloseTo(FLAG_H / 3, 6)
}
expect(bands[0].y).toBe(0)
expect(bands[2].y + bands[2].h).toBeCloseTo(FLAG_H, 6)
})
it('splits vertical bands evenly across the width', () => {
const bands = flagBands({ t: 'v', c: ['#002395', '#ffffff', '#ed2939'] })
for (const b of bands) {
expect(b.h).toBe(FLAG_H)
expect(b.w).toBeCloseTo(FLAG_W / 3, 6)
}
expect(bands[2].x + bands[2].w).toBeCloseTo(FLAG_W, 6)
})
it('keeps the band order, which is the whole identity of a tricolour', () => {
const bands = flagBands(FLAG_SPECS.DE)
expect(bands.map(b => b.fill)).toEqual(['#000000', '#dd0000', '#ffce00'])
})
/*
* Left of centre is what separates a Nordic cross from a plain one — the
* upright at the halfway mark would be the Swiss flag's geometry, not
* Denmark's.
*/
it('puts the Nordic upright left of centre', () => {
const bands = flagBands(FLAG_SPECS.DK)
const upright = bands[1]
expect(upright.x + upright.w / 2).toBeLessThan(FLAG_W / 2)
expect(upright.h).toBe(FLAG_H)
})
it('draws the inner cross on top of the outer one, and thinner', () => {
const bands = flagBands(FLAG_SPECS.IS)
// field, upright, crossbar, then the two inner bars.
expect(bands).toHaveLength(5)
expect(bands[3].fill).toBe('#dc1e35')
expect(bands[3].w).toBeLessThan(bands[1].w)
})
it('omits the inner cross where the flag has none', () => {
expect(flagBands(FLAG_SPECS.DK)).toHaveLength(3)
})
it('centres the Swiss cross both ways and keeps it inside the field', () => {
const [field, upright, bar] = flagBands(FLAG_SPECS.CH)
expect(field.w).toBe(FLAG_W)
expect(upright.x + upright.w / 2).toBeCloseTo(FLAG_W / 2, 6)
expect(bar.y + bar.h / 2).toBeCloseTo(FLAG_H / 2, 6)
expect(upright.y).toBeGreaterThan(0)
expect(upright.y + upright.h).toBeLessThan(FLAG_H)
})
it('keeps every band inside the flag box', () => {
for (const [code, spec] of Object.entries(FLAG_SPECS)) {
for (const b of flagBands(spec)) {
expect(b.x, code).toBeGreaterThanOrEqual(0)
expect(b.y, code).toBeGreaterThanOrEqual(0)
expect(b.x + b.w, code).toBeLessThanOrEqual(FLAG_W + 0.001)
expect(b.y + b.h, code).toBeLessThanOrEqual(FLAG_H + 0.001)
}
}
})
it('covers the whole box with its first band, so no page shows through', () => {
for (const [code, spec] of Object.entries(FLAG_SPECS)) {
const bands = flagBands(spec)
const area = bands.reduce((sum, b) => sum + b.w * b.h, 0)
expect(area, code).toBeGreaterThanOrEqual(FLAG_W * FLAG_H - 0.001)
}
})
})
describe('flagDisc', () => {
it('is a centred circle on a disc flag', () => {
const disc = flagDisc(FLAG_SPECS.JP)!
expect(disc.cx).toBe(FLAG_W / 2)
expect(disc.cy).toBe(FLAG_H / 2)
expect(disc.r).toBeLessThan(FLAG_H / 2)
expect(disc.fill).toBe('#bc002d')
})
it('is null for every other construction', () => {
expect(flagDisc(FLAG_SPECS.DE)).toBeNull()
expect(flagDisc(FLAG_SPECS.IS)).toBeNull()
expect(flagDisc(FLAG_SPECS.CH)).toBeNull()
})
})
describe('flagSpec', () => {
it('is case-insensitive, because codes arrive from several places', () => {
expect(flagSpec('is')).toBe(FLAG_SPECS.IS)
expect(flagSpec('IS')).toBe(FLAG_SPECS.IS)
})
it('is null for nothing and for a country with no construction', () => {
expect(flagSpec(null)).toBeNull()
expect(flagSpec(undefined)).toBeNull()
expect(flagSpec('')).toBeNull()
expect(flagSpec('GB')).toBeNull()
})
})