- theme: light and dark ignore prefers-color-scheme, choices and guard - i18n: ?locale= only with a preference, availableLocales from meta - usePreferences: cache validation, cycle order and PUT bodies, revert with a danger toast, profile apply, setLocale save, cache and reload - rail theme button, user menu language group, --shell-offset and the form save bar offset; fixture strings and resetState theme reset
203 lines
8.0 KiB
TypeScript
203 lines
8.0 KiB
TypeScript
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
import { currentLocale } from '../../src/app/i18n'
|
|
import {
|
|
applyProfilePreferences,
|
|
applyTheme,
|
|
cachedLocale,
|
|
cachedTheme,
|
|
cycleTheme,
|
|
LOCALE_KEY,
|
|
localePreference,
|
|
nextTheme,
|
|
page,
|
|
setLocale,
|
|
setTheme,
|
|
THEME_KEY,
|
|
themeChoice,
|
|
usePreferences,
|
|
} from '../../src/state/usePreferences'
|
|
import { toasts } from '../../src/state/useToasts'
|
|
import { API, mockApi, profile, requestsTo, resetState } from '../helpers'
|
|
|
|
const saved = (theme: string, locale = '') => ({ body: { data: { theme, locale }, meta: {} } })
|
|
const failed = { status: 500, body: { error: { code: 'error', message: 'x', details: {} } } }
|
|
|
|
async function bodyOf(request: Request): Promise<unknown> {
|
|
return JSON.parse(await request.clone().text())
|
|
}
|
|
|
|
function withPreferences(theme: string, locale: string) {
|
|
return { ...profile.data, preferences: { theme, locale } }
|
|
}
|
|
|
|
beforeEach(() => {
|
|
resetState()
|
|
})
|
|
|
|
afterEach(() => {
|
|
vi.restoreAllMocks()
|
|
applyTheme('system')
|
|
document.documentElement.classList.remove('dark')
|
|
})
|
|
|
|
describe('usePreferences cache', () => {
|
|
it('uses the documented storage keys', () => {
|
|
expect(THEME_KEY).toBe('summer-admin.theme')
|
|
expect(LOCALE_KEY).toBe('summer-admin.locale')
|
|
})
|
|
|
|
it('cachedTheme accepts only a valid choice', () => {
|
|
expect(cachedTheme()).toBe('system')
|
|
window.localStorage.setItem(THEME_KEY, 'dark')
|
|
expect(cachedTheme()).toBe('dark')
|
|
window.localStorage.setItem(THEME_KEY, 'blue')
|
|
expect(cachedTheme()).toBe('system')
|
|
window.localStorage.setItem(THEME_KEY, '')
|
|
expect(cachedTheme()).toBe('system')
|
|
})
|
|
|
|
it('cachedLocale accepts only a short locale-shaped code', () => {
|
|
expect(cachedLocale()).toBe('')
|
|
for (const code of ['pl', 'en', 'pt-BR', 'zh_Hant']) {
|
|
window.localStorage.setItem(LOCALE_KEY, code)
|
|
expect(cachedLocale()).toBe(code)
|
|
}
|
|
for (const code of ['p', 'pl;drop', '../en', 'a'.repeat(17), 'en-', '<script>', 'en-US-extra']) {
|
|
window.localStorage.setItem(LOCALE_KEY, code)
|
|
expect(cachedLocale()).toBe('')
|
|
}
|
|
})
|
|
})
|
|
|
|
describe('theme', () => {
|
|
it('cycles system, light, dark, system', () => {
|
|
expect(nextTheme('system')).toBe('light')
|
|
expect(nextTheme('light')).toBe('dark')
|
|
expect(nextTheme('dark')).toBe('system')
|
|
})
|
|
|
|
it('applies at once, caches and PUTs each choice', async () => {
|
|
const calls = mockApi({ [`PUT ${API}/account/preferences`]: saved('light') })
|
|
await cycleTheme()
|
|
expect(themeChoice.value).toBe('light')
|
|
expect(document.documentElement.classList.contains('dark')).toBe(false)
|
|
expect(window.localStorage.getItem(THEME_KEY)).toBe('light')
|
|
await cycleTheme()
|
|
expect(themeChoice.value).toBe('dark')
|
|
expect(document.documentElement.classList.contains('dark')).toBe(true)
|
|
expect(window.localStorage.getItem(THEME_KEY)).toBe('dark')
|
|
await cycleTheme()
|
|
expect(themeChoice.value).toBe('system')
|
|
// system is the default and is not stored.
|
|
expect(window.localStorage.getItem(THEME_KEY)).toBeNull()
|
|
const puts = requestsTo(calls, 'PUT', `${API}/account/preferences`)
|
|
expect(await Promise.all(puts.map(bodyOf))).toEqual([{ theme: 'light' }, { theme: 'dark' }, { theme: 'system' }])
|
|
expect(toasts.value).toHaveLength(0)
|
|
})
|
|
|
|
it('reverts the theme and the cache and shows a danger toast when the save fails', async () => {
|
|
mockApi({ [`PUT ${API}/account/preferences`]: failed })
|
|
await setTheme('dark')
|
|
expect(themeChoice.value).toBe('system')
|
|
expect(document.documentElement.classList.contains('dark')).toBe(false)
|
|
expect(window.localStorage.getItem(THEME_KEY)).toBeNull()
|
|
expect(toasts.value).toHaveLength(1)
|
|
expect(toasts.value[0]).toMatchObject({ tone: 'danger', text: 'Nie udało się zapisać ustawienia.' })
|
|
})
|
|
|
|
it('reverts on a network failure too', async () => {
|
|
applyTheme('light')
|
|
window.localStorage.setItem(THEME_KEY, 'light')
|
|
vi.spyOn(globalThis, 'fetch').mockRejectedValue(new TypeError('network'))
|
|
await setTheme('dark')
|
|
expect(themeChoice.value).toBe('light')
|
|
expect(window.localStorage.getItem(THEME_KEY)).toBe('light')
|
|
expect(toasts.value[0]?.tone).toBe('danger')
|
|
})
|
|
})
|
|
|
|
describe('applyProfilePreferences', () => {
|
|
it('applies and caches the stored theme', async () => {
|
|
const calls = mockApi({})
|
|
await applyProfilePreferences(withPreferences('dark', ''))
|
|
expect(themeChoice.value).toBe('dark')
|
|
expect(document.documentElement.classList.contains('dark')).toBe(true)
|
|
expect(window.localStorage.getItem(THEME_KEY)).toBe('dark')
|
|
expect(requestsTo(calls, 'GET', `${API}/lang`)).toHaveLength(0)
|
|
})
|
|
|
|
it('ignores an invalid theme and a missing preferences object', async () => {
|
|
applyTheme('light')
|
|
await applyProfilePreferences(withPreferences('blue', ''))
|
|
expect(themeChoice.value).toBe('light')
|
|
const { preferences: _omit, ...bare } = profile.data
|
|
await applyProfilePreferences(bare as never)
|
|
await applyProfilePreferences(null)
|
|
expect(themeChoice.value).toBe('light')
|
|
})
|
|
|
|
it('reloads the strings only when the stored locale differs', async () => {
|
|
const calls = mockApi({
|
|
[`GET ${API}/lang`]: { body: { data: { 'backend::lang.auth.title': { other: 'Welcome back' } }, meta: { locale: 'en', locales: ['en', 'pl'] } } },
|
|
})
|
|
expect(currentLocale.value).toBe('pl')
|
|
await applyProfilePreferences(withPreferences('system', 'pl'))
|
|
expect(requestsTo(calls, 'GET', `${API}/lang`)).toHaveLength(0)
|
|
expect(window.localStorage.getItem(LOCALE_KEY)).toBe('pl')
|
|
expect(localePreference.value).toBe('pl')
|
|
|
|
await applyProfilePreferences(withPreferences('system', 'en'))
|
|
const loads = requestsTo(calls, 'GET', `${API}/lang`)
|
|
expect(loads).toHaveLength(1)
|
|
expect(new URL(loads[0]!.url).searchParams.get('locale')).toBe('en')
|
|
expect(currentLocale.value).toBe('en')
|
|
expect(window.localStorage.getItem(LOCALE_KEY)).toBe('en')
|
|
|
|
await applyProfilePreferences(withPreferences('system', ''))
|
|
expect(requestsTo(calls, 'GET', `${API}/lang`)).toHaveLength(1)
|
|
expect(window.localStorage.getItem(LOCALE_KEY)).toBeNull()
|
|
expect(localePreference.value).toBe('')
|
|
})
|
|
})
|
|
|
|
describe('setLocale', () => {
|
|
it('PUTs the locale, caches it and reloads the page', async () => {
|
|
const reload = vi.spyOn(page, 'reload').mockImplementation(() => undefined)
|
|
const calls = mockApi({ [`PUT ${API}/account/preferences`]: saved('system', 'en') })
|
|
await setLocale('en')
|
|
const puts = requestsTo(calls, 'PUT', `${API}/account/preferences`)
|
|
expect(await Promise.all(puts.map(bodyOf))).toEqual([{ locale: 'en' }])
|
|
expect(window.localStorage.getItem(LOCALE_KEY)).toBe('en')
|
|
expect(localePreference.value).toBe('en')
|
|
expect(reload).toHaveBeenCalledTimes(1)
|
|
})
|
|
|
|
it('does nothing for the current preference', async () => {
|
|
const reload = vi.spyOn(page, 'reload').mockImplementation(() => undefined)
|
|
await applyProfilePreferences(withPreferences('system', 'pl'))
|
|
const calls = mockApi({ [`PUT ${API}/account/preferences`]: saved('system', 'pl') })
|
|
await setLocale('pl')
|
|
expect(requestsTo(calls, 'PUT', `${API}/account/preferences`)).toHaveLength(0)
|
|
expect(reload).not.toHaveBeenCalled()
|
|
})
|
|
|
|
it('shows a danger toast and neither caches nor reloads when the save fails', async () => {
|
|
const reload = vi.spyOn(page, 'reload').mockImplementation(() => undefined)
|
|
mockApi({ [`PUT ${API}/account/preferences`]: failed })
|
|
await setLocale('en')
|
|
expect(reload).not.toHaveBeenCalled()
|
|
expect(window.localStorage.getItem(LOCALE_KEY)).toBeNull()
|
|
expect(localePreference.value).toBe('')
|
|
expect(toasts.value[0]).toMatchObject({ tone: 'danger', text: 'Nie udało się zapisać ustawienia.' })
|
|
})
|
|
|
|
it('exposes the API through usePreferences', () => {
|
|
const prefs = usePreferences()
|
|
expect(prefs.theme).toBe(themeChoice)
|
|
expect(prefs.locale).toBe(localePreference)
|
|
expect(prefs.setTheme).toBe(setTheme)
|
|
expect(prefs.cycleTheme).toBe(cycleTheme)
|
|
expect(prefs.setLocale).toBe(setLocale)
|
|
})
|
|
})
|