Files
RenewCMS/web/src/views/__tests__/EditorLanguageMenu.spec.ts
T

236 lines
7.4 KiB
TypeScript

import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import router from '@/router'
import { initializeLocale } from '@/i18n'
import { MOBILE_WIDTH } from '@/components/__tests__/viewport'
import { settle, text, unmountRoute } from './localizationKit'
import {
EDIT_ONLY,
FULL,
READ,
buttonNamed,
languageTrigger,
menuRows,
mountEditor,
openLanguageMenu,
rowFor,
titleInput,
} from './editorKit'
import type { LanguageFixture } from './localizationData'
vi.mock('@/composables/useApi', async (importOriginal) => {
const actual = await importOriginal<typeof import('@/composables/useApi')>()
return { ...actual, useApi: vi.fn() }
})
beforeEach(() => initializeLocale())
afterEach(unmountRoute)
const LANGUAGE_NAMES = ['English', 'Arabic', 'French', 'German']
function rowNames() {
return menuRows().map((row) => row.textContent?.trim() ?? '')
}
function languageOf(row: string) {
return LANGUAGE_NAMES.find((name) => row.startsWith(name)) ?? row
}
function languageRows(tags: string[]): LanguageFixture[] {
return tags.map((lang) => ({ lang, versions: 1, published: 0 }))
}
describe('loc.l5.menu: The menu lists readable versions in API order', () => {
it('opens a menu, not a listbox, with English, Arabic and French and no German', async () => {
await mountEditor({ caps: EDIT_ONLY })
await openLanguageMenu()
expect(document.body.querySelector('[role="menu"]')).not.toBeNull()
expect(document.body.querySelector('[role="listbox"]')).toBeNull()
expect(rowNames().map(languageOf)).toEqual(['English', 'Arabic', 'French'])
})
it('names the control "Language: French. Switch version"', async () => {
await mountEditor()
expect(languageTrigger()?.getAttribute('aria-label')).toBe(
'Language: French. Switch version',
)
})
it('shows each language name, code and publication badge, with the French row current', async () => {
await mountEditor({ caps: EDIT_ONLY })
await openLanguageMenu()
expect(rowNames()).toEqual([
expect.stringMatching(/^EnglishEN.*Published/),
expect.stringMatching(/^ArabicAR.*Published/),
expect.stringMatching(/^FrenchFR.*Draft/),
])
expect(rowFor('French')?.querySelector('[aria-current="true"]')).not.toBeNull()
expect(rowFor('English')?.querySelector('[aria-current="true"]')).toBeNull()
})
})
describe('loc.l5.two and loc.l5.many: Menu size', () => {
it('shows two rows without a filter on a two-language site', async () => {
await mountEditor({
caps: EDIT_ONLY,
translations: ['en', 'fr'],
languages: languageRows(['en', 'fr']),
})
await openLanguageMenu()
expect(menuRows()).toHaveLength(2)
expect(document.body.querySelector('input[placeholder="Find a language"]')).toBeNull()
})
it('shows all four rows without a filter', async () => {
await mountEditor({ caps: EDIT_ONLY, translations: ['en', 'fr', 'de', 'ar'] })
await openLanguageMenu()
expect(menuRows()).toHaveLength(4)
expect(document.body.querySelector('input[placeholder="Find a language"]')).toBeNull()
})
it('adds a filter field that narrows the rows by language name on a many-language site', async () => {
const tags = ['en', 'fr', 'de', 'ar', 'es', 'it', 'ja', 'nl', 'pl', 'pt-BR']
await mountEditor({
caps: FULL,
translations: ['en', 'fr', 'de', 'ar'],
languages: languageRows(tags),
})
await openLanguageMenu()
const filter = document.body.querySelector<HTMLInputElement>(
'input[placeholder="Find a language"]',
)!
expect(menuRows().length).toBeGreaterThan(8)
filter.value = 'port'
filter.dispatchEvent(new Event('input', { bubbles: true }))
await settle()
expect(rowNames().map((name) => name.slice(0, 'Portuguese'.length))).toEqual(['Portuguese'])
})
})
describe('loc.perm.*: Row tags and absent rows follow each sibling capabilities', () => {
it('tags a read-only sibling as Read only and still opens it', async () => {
await mountEditor({ siblingCaps: { ar: READ } })
await openLanguageMenu()
expect(rowFor('Arabic')?.textContent).toContain('Read only')
expect(rowFor('English')?.textContent).not.toContain('Read only')
})
it('shows no German row, count or placeholder when German is not in translations', async () => {
await mountEditor({ caps: EDIT_ONLY })
await openLanguageMenu()
expect(rowFor('German')).toBeUndefined()
expect(text()).not.toContain('German')
})
it('keeps a disabled language listed and marks it as disabled', async () => {
await mountEditor({ languageEnabled: { ar: false } })
await openLanguageMenu()
expect(rowFor('Arabic')?.textContent).toContain('Disabled')
})
})
describe('loc.l5b.withAdd, noAdd and roMenu: Add translation rows', () => {
it('lists missing languages as Add translation rows when the caller can create', async () => {
await mountEditor({ caps: FULL })
await openLanguageMenu()
expect(rowFor('German')?.textContent).toContain('Add translation')
})
it('offers nothing for missing languages without create_translation', async () => {
await mountEditor({ caps: EDIT_ONLY })
await openLanguageMenu()
expect(text()).not.toContain('Add translation')
})
it('offers no Add translation row on a read-only version', async () => {
await mountEditor({ caps: READ })
await openLanguageMenu()
expect(text()).not.toContain('Add translation')
})
it('opens the Add a translation dialog for the row and closes the menu', async () => {
await mountEditor({ caps: FULL })
await openLanguageMenu()
rowFor('German')!.click()
await settle()
expect(router.currentRoute.value.fullPath).toBe(
'/article/12/edit?dialog=add-translation&lang=de',
)
expect(document.body.querySelector('[role="menu"]')).toBeNull()
expect(document.body.querySelector('[role="dialog"]')?.textContent).toContain(
'Add a translation',
)
expect(titleInput().value).toBe('Couts de renovation')
})
})
describe('The menu is keyboard operable', () => {
it('opens on ArrowDown with focus on the current row', async () => {
await mountEditor()
languageTrigger()!.dispatchEvent(
new KeyboardEvent('keydown', { key: 'ArrowDown', bubbles: true }),
)
await settle()
expect(document.body.querySelector('[role="menu"]')).not.toBeNull()
expect(document.activeElement?.textContent).toContain('French')
})
it('focuses the current row when the sheet opens on a phone', async () => {
await mountEditor({ width: MOBILE_WIDTH })
await openLanguageMenu()
expect(document.activeElement?.textContent).toContain('French')
})
it('closes on Escape', async () => {
await mountEditor()
await openLanguageMenu()
document.activeElement?.dispatchEvent(
new KeyboardEvent('keydown', { key: 'Escape', bubbles: true }),
)
await settle()
expect(document.body.querySelector('[role="menu"]')).toBeNull()
})
})
describe('A single-language site shows no language control', () => {
it('hides the control, names no language and reads Publish', async () => {
await mountEditor({
lang: 'en',
translations: ['en'],
languages: languageRows(['en']),
online: { en: false },
})
expect(languageTrigger()).toBeNull()
expect(buttonNamed('Publish')).toBeDefined()
expect(text()).not.toContain('English')
expect(titleInput().getAttribute('id')).not.toBeNull()
})
})
describe('On a phone the menu opens as a sheet', () => {
it('lists the same rows in a bottom sheet', async () => {
await mountEditor({ width: MOBILE_WIDTH, caps: EDIT_ONLY })
languageTrigger()!.click()
await settle()
expect(rowNames().map(languageOf)).toEqual(['English', 'Arabic', 'French'])
})
})