- 41 unit and component suites under admin/tests/{app,state,shell,list,form,relation,views,ui}
covering states and a11y roles; every src module is imported by a test
- typed fixture helper assigns each JSON fixture to its generated OpenAPI type
- fix: iconFor ignores inherited object members such as "constructor"
- fix: field controls import ./control instead of the registry (import cycle
left a renderer unregistered depending on module load order)
- fix: dropdown shows the placeholder for an unknown stored value next to an emptyOption
- fix: list announces a failed schema load even when the rows arrive after it
- tailwind no longer scans admin/tests; boardwalk/dist rebuilt
138 lines
5.0 KiB
TypeScript
138 lines
5.0 KiB
TypeScript
import { beforeEach, describe, expect, it } from 'vitest'
|
||
import {
|
||
currentLocale,
|
||
interpolate,
|
||
loadStrings,
|
||
message,
|
||
pluralCategory,
|
||
setBundle,
|
||
t,
|
||
tc,
|
||
} from '../../src/app/i18n'
|
||
import { API, mockApi, requestsTo } from '../helpers'
|
||
import { langFixture } from '../fixtures/typed'
|
||
|
||
beforeEach(() => {
|
||
setBundle(langFixture.data, 'pl')
|
||
})
|
||
|
||
describe('interpolate (parity with phrasebook.interpolate)', () => {
|
||
it('replaces :name, :Name and :NAME with the plain, first-upper and upper value', () => {
|
||
expect(interpolate(':name / :Name / :NAME', { name: 'ada' })).toBe('ada / Ada / ADA')
|
||
})
|
||
|
||
it('replaces the longer placeholder first so :names never loses to :name', () => {
|
||
expect(interpolate(':names and :name', { name: 'one', names: 'many' })).toBe('many and one')
|
||
expect(interpolate(':name and :names', { names: 'many', name: 'one' })).toBe('one and many')
|
||
})
|
||
|
||
it('accepts a leading colon on the parameter name and skips an empty name', () => {
|
||
expect(interpolate('Hello :who', { ':who': 'world' })).toBe('Hello world')
|
||
expect(interpolate('Hello :', { ':': 'x', '': 'y' })).toBe('Hello :')
|
||
})
|
||
|
||
it('upper-cases the first code point, not the first UTF-16 unit', () => {
|
||
expect(interpolate(':Name', { name: 'łódź' })).toBe('Łódź')
|
||
expect(interpolate(':Name', { name: '𝒶bc' })).toBe('𝒶bc')
|
||
})
|
||
|
||
it('stringifies numbers and leaves text without placeholders or params alone', () => {
|
||
expect(interpolate(':count items', { count: 3 })).toBe('3 items')
|
||
expect(interpolate('plain', { name: 'x' })).toBe('plain')
|
||
expect(interpolate(':name')).toBe(':name')
|
||
expect(interpolate('', { name: 'x' })).toBe('')
|
||
})
|
||
})
|
||
|
||
describe('plural selection', () => {
|
||
it.each([
|
||
[1, 'one'],
|
||
[2, 'few'],
|
||
[5, 'many'],
|
||
[22, 'few'],
|
||
[25, 'many'],
|
||
])('pl %i is %s', (count, category) => {
|
||
expect(pluralCategory(count, 'pl')).toBe(category)
|
||
})
|
||
|
||
it.each([
|
||
[1, 'one'],
|
||
[2, 'other'],
|
||
])('en %i is %s', (count, category) => {
|
||
expect(pluralCategory(count, 'en')).toBe(category)
|
||
})
|
||
|
||
it('answers other for a locale Intl cannot parse', () => {
|
||
expect(pluralCategory(1, '!!invalid!!')).toBe('other')
|
||
})
|
||
|
||
it('uses the bundle locale by default', () => {
|
||
setBundle(langFixture.data, 'en')
|
||
expect(pluralCategory(2)).toBe('other')
|
||
setBundle(langFixture.data, 'pl')
|
||
expect(pluralCategory(2)).toBe('few')
|
||
})
|
||
|
||
it('picks the form for the count and falls back to other when that form is missing', () => {
|
||
const forms = { one: ':count plik', other: ':count pliku' }
|
||
expect(message(forms, 1)).toBe('1 plik')
|
||
expect(message(forms, 5)).toBe('5 pliku')
|
||
expect(message(forms, 2, { count: 'dwa' })).toBe('dwa pliku')
|
||
})
|
||
|
||
it('renders other without a count and nothing without forms', () => {
|
||
expect(message({ other: 'Hi :name', one: 'x' }, undefined, { name: 'Ada' })).toBe('Hi Ada')
|
||
expect(message(null)).toBe('')
|
||
expect(message(undefined, 3)).toBe('')
|
||
expect(message({ one: 'only one' }, 5)).toBe('')
|
||
})
|
||
})
|
||
|
||
describe('bundle lookups', () => {
|
||
it('t returns the key itself when the key is not in the bundle', () => {
|
||
expect(t('backend::lang.missing.key')).toBe('backend::lang.missing.key')
|
||
expect(tc('backend::lang.missing.key', 3)).toBe('backend::lang.missing.key')
|
||
})
|
||
|
||
it('t interpolates the other form and tc the plural form', () => {
|
||
expect(t('backend::lang.form.remove_item', { name: 'Ada' })).toBe('Usuń: Ada')
|
||
expect(tc('backend::lang.list.results', 1)).toBe('1 wynik')
|
||
expect(tc('backend::lang.list.results', 3)).toBe('3 wyniki')
|
||
expect(tc('backend::lang.list.results', 7)).toBe('7 wyników')
|
||
})
|
||
|
||
it('setBundle replaces the strings and sets the document language', () => {
|
||
setBundle({ 'backend::lang.auth.title': { other: 'Welcome back' } }, 'en')
|
||
expect(t('backend::lang.auth.title')).toBe('Welcome back')
|
||
expect(t('backend::lang.auth.submit')).toBe('backend::lang.auth.submit')
|
||
expect(document.documentElement.lang).toBe('en')
|
||
expect(currentLocale.value).toBe('en')
|
||
})
|
||
|
||
it('setBundle without a locale keeps the current locale', () => {
|
||
setBundle({}, 'pl')
|
||
setBundle({ a: { other: 'b' } })
|
||
expect(currentLocale.value).toBe('pl')
|
||
expect(t('a')).toBe('b')
|
||
})
|
||
})
|
||
|
||
describe('loadStrings', () => {
|
||
it('loads GET /lang and adopts meta.locale', async () => {
|
||
const calls = mockApi({
|
||
[`GET ${API}/lang`]: { body: { data: { 'backend::lang.auth.title': { other: 'Hello' } }, meta: { locale: 'en' } } },
|
||
})
|
||
await loadStrings()
|
||
expect(requestsTo(calls, 'GET', `${API}/lang`)).toHaveLength(1)
|
||
expect(t('backend::lang.auth.title')).toBe('Hello')
|
||
expect(currentLocale.value).toBe('en')
|
||
})
|
||
|
||
it('keeps the current strings when the bundle cannot be loaded', async () => {
|
||
mockApi({ [`GET ${API}/lang`]: { status: 500, body: { error: { code: 'server', message: 'x', details: {} } } } })
|
||
await loadStrings()
|
||
expect(t('backend::lang.auth.title')).toBe('Witaj ponownie')
|
||
expect(currentLocale.value).toBe('pl')
|
||
})
|
||
})
|