feat: editor language menu, title field, header actions, publish dialogs, read-only rendering and draft recovery
This commit is contained in:
1 parent
a671b81f80
commit
339e646eb8
85 files changed
+6369
-624
No files matched your search
@@ -0,0 +1,227 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
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('renders the Add translation row without doing anything when chosen', async () => {
|
||||
await mountEditor({ caps: FULL })
|
||||
await openLanguageMenu()
|
||||
rowFor('German')!.click()
|
||||
await settle()
|
||||
|
||||
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'])
|
||||
})
|
||||
})
|
||||
Reference in new issue
Block a user