test(10-05): bring every SPA module, composable and component under Vitest
- 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
This commit is contained in:
137
admin/tests/app/i18n.test.ts
Normal file
137
admin/tests/app/i18n.test.ts
Normal file
@@ -0,0 +1,137 @@
|
||||
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')
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user