fix(frontend): complete i18n coverage and responsive layouts

This commit is contained in:
elky
2026-09-07 08:54:19 +08:00
parent 14f96c9fa0
commit b599fb7354
78 changed files with 4809 additions and 1485 deletions
@@ -0,0 +1,68 @@
import { afterEach, describe, expect, it, vi } from 'vitest'
import { createApp, h, nextTick, ref, type App } from 'vue'
import Pagination from '../pagination.vue'
import { setI18nLocale } from '@/i18n'
const mountedApps: Array<{ app: App, root: HTMLElement }> = []
afterEach(() => {
for (const { app, root } of mountedApps.splice(0)) {
app.unmount()
root.remove()
}
})
describe('Pagination', () => {
it('updates the summary and accessible page controls when the locale changes', async () => {
const root = document.createElement('div')
document.body.appendChild(root)
const updateCurrent = vi.fn()
const app = createApp({
render: () => h(Pagination, {
current: 1,
total: 1250,
pageSize: 20,
showPageSizeSelector: false,
'onUpdate:current': updateCurrent,
}),
})
app.mount(root)
mountedApps.push({ app, root })
expect(root.querySelector('[aria-live]')?.textContent).toContain('共 1,250 条')
expect(root.querySelector('[aria-current="page"]')?.getAttribute('aria-label')).toBe('第 1 页')
setI18nLocale('en-US')
await nextTick()
expect(root.querySelector('[aria-live]')?.textContent).toContain('Showing 1-20 of 1,250 items')
expect(root.querySelector('[aria-current="page"]')?.getAttribute('aria-label')).toBe('Page 1')
expect(root.querySelector('input')?.getAttribute('aria-label')).toBe('Go to page')
root.querySelector<HTMLButtonElement>('[aria-label="Page 2"]')?.click()
expect(updateCurrent).toHaveBeenCalledWith(2)
})
it('shows a zero-based empty range after the final record is removed', async () => {
const total = ref(1)
const root = document.createElement('div')
document.body.appendChild(root)
const app = createApp({
render: () => h(Pagination, {
current: 1,
total: total.value,
showPageSizeSelector: false,
}),
})
app.mount(root)
mountedApps.push({ app, root })
total.value = 0
setI18nLocale('en-US')
await nextTick()
expect(root.querySelector('[aria-live]')?.textContent).toContain('Showing 0-0 of 0 items')
expect(root.querySelectorAll('button')).toHaveLength(0)
})
})
@@ -0,0 +1,55 @@
import { afterEach, describe, expect, it, vi } from 'vitest'
import { createApp, h, nextTick, type App } from 'vue'
import Tabs from '../tabs.vue'
import TabsList from '../tabs-list.vue'
import TabsTrigger from '../tabs-trigger.vue'
import { setI18nLocale, useI18n } from '@/i18n'
const mountedApps: Array<{ app: App, root: HTMLElement }> = []
afterEach(() => {
for (const { app, root } of mountedApps.splice(0)) {
app.unmount()
root.remove()
}
vi.restoreAllMocks()
vi.useRealTimers()
})
describe('TabsList', () => {
it('repositions the indicator after translated labels change width', async () => {
vi.useFakeTimers()
vi.spyOn(HTMLElement.prototype, 'getBoundingClientRect').mockImplementation(function (this: HTMLElement) {
return { width: this.textContent === '个人设置' ? 80 : 140 } as DOMRect
})
vi.spyOn(HTMLElement.prototype, 'offsetLeft', 'get').mockReturnValue(4)
const root = document.createElement('div')
document.body.appendChild(root)
const app = createApp({
setup() {
const { t } = useI18n()
return () => h(Tabs, { modelValue: 'settings' }, {
default: () => h(TabsList, {}, {
default: () => h(TabsTrigger, { value: 'settings' }, () => t('common.settings')),
}),
})
},
})
app.mount(root)
mountedApps.push({ app, root })
await nextTick()
await vi.runAllTimersAsync()
const indicator = root.querySelector<HTMLElement>('.tabs-indicator')
expect(indicator?.style.width).toBe('80px')
expect(indicator?.style.transform).toBe('translateX(4px)')
setI18nLocale('en-US')
await nextTick()
await vi.runAllTimersAsync()
expect(indicator?.style.width).toBe('140px')
})
})