fix: language buttons real design / conformance
This commit is contained in:
1 parent
2c8f39273c
commit
0490a4c494
23 files changed
+1408
-412
No files matched your search
+1
-218
@@ -3,16 +3,13 @@ import { flushPromises, mount, type VueWrapper } from '@vue/test-utils'
|
||||
import { createPinia, setActivePinia } from 'pinia'
|
||||
import ui from '@nuxt/ui/vue-plugin'
|
||||
import App from '@/App.vue'
|
||||
import LanguagePicker from '@/components/LanguagePicker.vue'
|
||||
import ConfirmDialog from '@/components/ConfirmDialog.vue'
|
||||
import UserMenu from '@/components/UserMenu.vue'
|
||||
import AccountMenuSheet from '@/components/AccountMenuSheet.vue'
|
||||
import { useApi, ApiError } from '@/composables/useApi'
|
||||
import { useSessionStore } from '@/stores/session'
|
||||
import { useColorMode } from '@/composables/useColorMode'
|
||||
import router from '@/router'
|
||||
import { DESKTOP_WIDTH, setViewportWidth } from '@/components/__tests__/viewport'
|
||||
import { initializeLocale, locale, setLocale, t } from './index'
|
||||
import { initializeLocale, setLocale, t } from './index'
|
||||
import { messages } from './messages'
|
||||
import { formatAbsoluteDateFromIso } from '@/lib/relativeDate'
|
||||
import { formatSize, pluralArticles } from '@/lib/mediaFormat'
|
||||
@@ -84,22 +81,6 @@ async function mountRoute(path: string) {
|
||||
return wrapper
|
||||
}
|
||||
|
||||
function trigger() {
|
||||
return document.body.querySelector<HTMLButtonElement>(
|
||||
'[role="combobox"][aria-label="Language"], [role="combobox"][aria-label="Langue"]',
|
||||
)!
|
||||
}
|
||||
|
||||
function key(element: Element, value: string) {
|
||||
element.dispatchEvent(
|
||||
new KeyboardEvent('keydown', {
|
||||
key: value,
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
describe('Translate the entire interface', () => {
|
||||
it('provides nonempty equivalent interpolation parameters for every English and French key', () => {
|
||||
const placeholders = (text: string) =>
|
||||
@@ -321,152 +302,6 @@ describe('Switch language without losing work', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('Operate the picker accessibly', () => {
|
||||
it.each([false, true])(
|
||||
'supports keyboard selection and focus return with mobile=%s',
|
||||
async (mobile) => {
|
||||
wrapper = mount(LanguagePicker, {
|
||||
attachTo: document.body,
|
||||
props: { mobile },
|
||||
global: { plugins: [ui] },
|
||||
})
|
||||
trigger().focus()
|
||||
key(trigger(), 'ArrowDown')
|
||||
await flushPromises()
|
||||
const options = Array.from(
|
||||
document.body.querySelectorAll<HTMLElement>('[role="option"]'),
|
||||
)
|
||||
const selected = options
|
||||
.find((option) => option.textContent!.includes('English'))
|
||||
?.getAttribute('aria-selected')
|
||||
key(document.activeElement!, 'End')
|
||||
await flushPromises()
|
||||
await new Promise((resolve) => setTimeout(resolve, 0))
|
||||
key(document.activeElement!, 'Enter')
|
||||
await flushPromises()
|
||||
expect({
|
||||
options: options.map((option) => option.textContent!.trim()),
|
||||
selected,
|
||||
locale: locale.value,
|
||||
closed: !document.body.querySelector('[role="listbox"]'),
|
||||
focus: document.activeElement === trigger(),
|
||||
}).toEqual({
|
||||
options: ['English', 'Français'],
|
||||
selected: 'true',
|
||||
locale: 'fr',
|
||||
closed: true,
|
||||
focus: true,
|
||||
})
|
||||
},
|
||||
)
|
||||
it('dismisses with Escape and preserves the language', async () => {
|
||||
wrapper = mount(LanguagePicker, {
|
||||
attachTo: document.body,
|
||||
global: { plugins: [ui] },
|
||||
})
|
||||
trigger().focus()
|
||||
key(trigger(), 'ArrowDown')
|
||||
await flushPromises()
|
||||
key(document.activeElement!, 'Escape')
|
||||
await flushPromises()
|
||||
expect([
|
||||
locale.value,
|
||||
!document.body.querySelector('[role="listbox"]'),
|
||||
document.activeElement === trigger(),
|
||||
]).toEqual(['en', true, true])
|
||||
})
|
||||
it.each(['desktop', 'mobile'])('offers the picker in the %s account menu', async (menu) => {
|
||||
useSessionStore().session = {
|
||||
verified: true,
|
||||
username: 'marta',
|
||||
displayName: '',
|
||||
}
|
||||
useSessionStore().status = 'ready'
|
||||
await router.push('/profile')
|
||||
wrapper =
|
||||
menu === 'desktop'
|
||||
? mount(UserMenu, {
|
||||
attachTo: document.body,
|
||||
props: { username: 'marta', collapsed: false },
|
||||
global: { plugins: [ui, router] },
|
||||
})
|
||||
: mount(AccountMenuSheet, {
|
||||
attachTo: document.body,
|
||||
props: { username: 'marta', open: true },
|
||||
global: { plugins: [ui, router] },
|
||||
})
|
||||
if (menu === 'desktop')
|
||||
document.body.querySelector<HTMLButtonElement>('button[title="Account"]')!.click()
|
||||
await flushPromises()
|
||||
const profileEntry = Array.from(
|
||||
document.body.querySelectorAll<HTMLElement>('[role="menuitem"]'),
|
||||
).find((item) => item.textContent?.trim() === 'Profile')!
|
||||
profileEntry.focus()
|
||||
key(profileEntry, menu === 'desktop' ? 'End' : 'ArrowDown')
|
||||
await new Promise((resolve) => setTimeout(resolve, 0))
|
||||
const reachedControl = document.activeElement === trigger()
|
||||
key(trigger(), 'ArrowDown')
|
||||
await flushPromises()
|
||||
key(document.activeElement!, 'End')
|
||||
await new Promise((resolve) => setTimeout(resolve, 0))
|
||||
key(document.activeElement!, 'Enter')
|
||||
await flushPromises()
|
||||
expect([
|
||||
reachedControl,
|
||||
locale.value,
|
||||
document.activeElement === trigger(),
|
||||
trigger().textContent?.includes('Langue'),
|
||||
]).toEqual([true, 'fr', true, true])
|
||||
})
|
||||
it('retains French after logout and renders the public authentication screen in French', async () => {
|
||||
setLocale('fr')
|
||||
await mountRoute('/profile')
|
||||
document.body.querySelector<HTMLButtonElement>('button[title="Compte"]')!.click()
|
||||
await flushPromises()
|
||||
Array.from(document.body.querySelectorAll<HTMLElement>('[role="menuitem"]'))
|
||||
.find((item) => item.textContent?.trim() === 'Se déconnecter')!
|
||||
.click()
|
||||
await flushPromises()
|
||||
expect([
|
||||
router.currentRoute.value.path,
|
||||
locale.value,
|
||||
localStorage.getItem('renewcms-language'),
|
||||
document.body.textContent!.includes('Se connecter'),
|
||||
]).toEqual(['/login', 'fr', 'fr', true])
|
||||
})
|
||||
it('updates an existing success notification without translating the saved account name', async () => {
|
||||
await mountRoute('/profile')
|
||||
await wrapper!.find('input[autocomplete="off"]').setValue('Log in')
|
||||
request.mockResolvedValueOnce({ display_name: 'Log in' })
|
||||
await wrapper!.find('form').trigger('submit')
|
||||
await flushPromises()
|
||||
setLocale('fr')
|
||||
await flushPromises()
|
||||
expect([
|
||||
document.body.textContent!.includes('Nom d’affichage enregistré'),
|
||||
document.body.textContent!.includes(
|
||||
'Vous apparaissez désormais sous le nom Log in dans RenewCMS et les e-mails.',
|
||||
),
|
||||
]).toEqual([true, true])
|
||||
})
|
||||
it('updates an already-open confirmation dialog', async () => {
|
||||
await mountRoute('/profile')
|
||||
await wrapper!.find('input[autocomplete="off"]').setValue('Unsaved')
|
||||
const navigation = router.push('/home')
|
||||
await flushPromises()
|
||||
setLocale('fr')
|
||||
await flushPromises()
|
||||
const translated = document.body
|
||||
.querySelector('[role="alertdialog"]')
|
||||
?.textContent?.includes('Quitter sans enregistrer ?')
|
||||
Array.from(document.body.querySelectorAll<HTMLButtonElement>('[role="alertdialog"] button'))
|
||||
.find((button) => button.textContent?.trim() === 'Abandonner les modifications')!
|
||||
.click()
|
||||
await navigation
|
||||
expect(translated).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Hardener localization outcomes', () => {
|
||||
it.each([false, true])(
|
||||
'updates not-found breadcrumbs round-trip with authenticated=%s',
|
||||
@@ -563,58 +398,6 @@ describe('Hardener localization outcomes', () => {
|
||||
)
|
||||
vi.unstubAllGlobals()
|
||||
})
|
||||
it.each(['desktop', 'mobile'] as const)(
|
||||
'dismisses nested picker with Escape and outside press in %s menu',
|
||||
async (menu) => {
|
||||
useSessionStore().session = { verified: true, username: 'marta', displayName: '' }
|
||||
useSessionStore().status = 'ready'
|
||||
await router.push('/profile')
|
||||
wrapper =
|
||||
menu === 'desktop'
|
||||
? mount(UserMenu, {
|
||||
attachTo: document.body,
|
||||
props: { username: 'marta', collapsed: false },
|
||||
global: { plugins: [ui, router] },
|
||||
})
|
||||
: mount(AccountMenuSheet, {
|
||||
attachTo: document.body,
|
||||
props: { username: 'marta', open: true },
|
||||
global: { plugins: [ui, router] },
|
||||
})
|
||||
if (menu === 'desktop')
|
||||
document.body.querySelector<HTMLButtonElement>('button[title="Account"]')!.click()
|
||||
await flushPromises()
|
||||
trigger().focus()
|
||||
key(trigger(), 'ArrowDown')
|
||||
await flushPromises()
|
||||
key(document.activeElement!, 'Escape')
|
||||
await flushPromises()
|
||||
const escape = [
|
||||
locale.value,
|
||||
!document.body.querySelector('[role="listbox"]'),
|
||||
document.activeElement === trigger(),
|
||||
]
|
||||
trigger().focus()
|
||||
key(trigger(), 'ArrowDown')
|
||||
await flushPromises()
|
||||
const outside = document.createElement('button')
|
||||
outside.textContent = 'Outside'
|
||||
document.body.append(outside)
|
||||
outside.dispatchEvent(
|
||||
Object.assign(new MouseEvent('pointerdown', { bubbles: true, button: 0 }), {
|
||||
pointerType: 'mouse',
|
||||
}),
|
||||
)
|
||||
outside.click()
|
||||
await flushPromises()
|
||||
expect([
|
||||
escape,
|
||||
locale.value,
|
||||
!document.body.querySelector('[role="listbox"]'),
|
||||
document.activeElement === trigger(),
|
||||
]).toEqual([['en', true, true], 'en', true, true])
|
||||
},
|
||||
)
|
||||
it.each(['/article', '/article/12/edit'])(
|
||||
'updates already-open delete descriptions round-trip from %s preserving title and image sentence',
|
||||
async (path) => {
|
||||
|
||||
Reference in new issue
Block a user