test(quick-261022-imo): settings section panel coverage

- settingsSectionOf: null cases, category order, link entry placement
- SettingsPanel: labels, links, current entry, empty category, collapse
- AppShell: hidden cases, settings-link controllers, collapse, offset, narrow, focus hand-off
This commit is contained in:
Jakub Zych
2026-10-22 13:52:20 +02:00
parent 834ef98572
commit 85476fa3fe
3 changed files with 219 additions and 0 deletions

View File

@@ -151,4 +151,85 @@ describe('settings section panel', () => {
expect(links[0]!.attributes('aria-current')).toBe('page')
expect(links[1]!.attributes('aria-current')).toBeUndefined()
})
const link = (code: string, label: string, controller: string) => ({
...settingsFixture.list.data[0]!,
code,
label,
model: '',
controller,
order: 15,
})
it('is hidden for a one-entry category and on the settings index', async () => {
setSettings(settingsFixture.list.data)
const branding = await mountShell('/settings/branding')
expect(branding.find('[data-settings-panel]').exists()).toBe(false)
expect(branding.find('aside').exists()).toBe(false)
branding.unmount()
const index = await mountShell('/settings')
expect(index.find('aside').exists()).toBe(false)
})
it('never lists an entry of another category', async () => {
setSettings(settingsFixture.list.data)
const wrapper = await mountShell('/settings/mail')
expect(wrapper.find('[data-settings-panel-item="branding"]').exists()).toBe(false)
expect(wrapper.text()).not.toContain('Branding')
})
it.each(['/acme/lang/locales', '/acme/lang/locales/5'])(
'shows on a settings-link controller without its own navigation entry (%s)',
async (path) => {
setSettings([...settingsFixture.list.data, link('locales', 'Locales', 'acme.lang.locales')])
const wrapper = await mountShell(path)
const panel = wrapper.find('aside[data-settings-panel]')
expect(panel.exists()).toBe(true)
expect(panel.find('[aria-current="page"]').text()).toBe('Locales')
expect(panel.findAll('[data-settings-panel-item]').map((item) => item.text())).toEqual(['Mail', 'Locales', 'Search'])
},
)
it('keeps the plugin panel on a settings-link controller with its own navigation entry', async () => {
setSettings([...settingsFixture.list.data, link('widgets', 'Widgets', 'acme.demo.widgets')])
const wrapper = await mountShell('/acme/demo/widgets')
expect(wrapper.find('[data-settings-panel]').exists()).toBe(false)
expect(wrapper.find('aside span').text()).toBe('Demo')
})
it('collapses with the sidebar and offsets the shell like the plugin panel', async () => {
setSettings(settingsFixture.list.data)
const wrapper = await mountShell('/settings/mail')
const root = wrapper.element as HTMLElement
expect(root.style.getPropertyValue('--shell-offset')).toBe('calc(var(--spacing-rail) + var(--spacing-panel))')
setCollapsed(true)
await flushPromises()
expect(wrapper.find('[data-settings-panel]').exists()).toBe(false)
expect(root.style.getPropertyValue('--shell-offset')).toBe('var(--spacing-rail)')
setCollapsed(false)
await flushPromises()
expect(wrapper.find('[data-settings-panel]').exists()).toBe(true)
expect(root.style.getPropertyValue('--shell-offset')).toBe('calc(var(--spacing-rail) + var(--spacing-panel))')
})
it('is hidden on a narrow viewport', async () => {
fakeMedia({ [NARROW_QUERY]: true })
setSettings(settingsFixture.list.data)
const wrapper = await mountShell('/settings/mail')
expect(wrapper.find('[data-settings-panel]').exists()).toBe(false)
expect((wrapper.element as HTMLElement).style.getPropertyValue('--shell-offset')).toBe('var(--spacing-rail)')
})
it('hands focus to the rail expand button and back', async () => {
setSettings(settingsFixture.list.data)
const wrapper = await mountShell('/settings/mail')
await wrapper.find('[data-settings-panel] [data-sidebar-collapse]').trigger('click')
await flushPromises()
expect(wrapper.find('[data-settings-panel]').exists()).toBe(false)
expect(document.activeElement?.matches('[data-sidebar-expand]')).toBe(true)
await wrapper.find('[data-sidebar-expand]').trigger('click')
await flushPromises()
expect(wrapper.find('aside[data-settings-panel]').exists()).toBe(true)
expect(document.activeElement?.matches('[data-sidebar-collapse]')).toBe(true)
})
})

View File

@@ -0,0 +1,99 @@
import { afterEach, beforeEach, describe, expect, it } from 'vitest'
import { enableAutoUnmount, flushPromises, mount } from '@vue/test-utils'
import SettingsPanel from '../../src/components/shell/SettingsPanel.vue'
import { SIDEBAR_KEY, setCollapsed, sidebarCollapsed } from '../../src/state/useSidebar'
import {
setSettings,
settingsEntry,
settingsLinkFor,
settingsSectionOf,
type SettingsSection,
} from '../../src/state/useSettings'
import { settingsFixture } from '../fixtures/typed'
import { resetState, routerAt, signIn } from '../helpers'
beforeEach(() => {
resetState()
setCollapsed(false)
})
afterEach(() => {
setCollapsed(false)
document.body.innerHTML = ''
})
enableAutoUnmount(afterEach)
const locales = {
...settingsFixture.list.data[0]!,
code: 'locales',
label: 'Locales',
model: '',
controller: 'acme.lang.locales',
order: 15,
}
async function mountPanel(path: string, section: SettingsSection) {
await signIn()
const router = await routerAt(path)
const wrapper = mount(SettingsPanel, { props: { section }, global: { plugins: [router] }, attachTo: document.body })
await flushPromises()
return wrapper
}
describe('settings panel', () => {
it('is a labelled region with the category and its pages in section order', async () => {
setSettings(settingsFixture.list.data)
const wrapper = await mountPanel('/settings/search', settingsSectionOf(settingsEntry('search'))!)
const aside = wrapper.find('aside')
expect(aside.attributes('aria-label')).toBe('Sekcje')
expect(aside.attributes('data-settings-panel')).toBeDefined()
expect(aside.find('span').text()).toBe('System')
const links = wrapper.findAll('a')
expect(links.map((link) => [link.text(), link.attributes('href'), link.attributes('data-settings-panel-item')])).toEqual([
['Mail', '/settings/mail', 'mail'],
['Search', '/settings/search', 'search'],
])
expect(links[0]!.attributes('aria-current')).toBeUndefined()
expect(links[1]!.attributes('aria-current')).toBe('page')
})
it('links a settings link entry to its controller and highlights only it', async () => {
setSettings([...settingsFixture.list.data, locales])
const wrapper = await mountPanel('/acme/lang/locales', settingsSectionOf(settingsLinkFor('acme.lang.locales'))!)
const links = wrapper.findAll('a')
expect(links.map((link) => [link.text(), link.attributes('href')])).toEqual([
['Mail', '/settings/mail'],
['Locales', '/acme/lang/locales'],
['Search', '/settings/search'],
])
expect(links.filter((link) => link.attributes('aria-current') === 'page').map((link) => link.text())).toEqual([
'Locales',
])
})
it('falls back to "Ustawienia" for an empty category', async () => {
const [search, mail] = settingsFixture.list.data
const section: SettingsSection = {
category: '',
current: { ...mail!, category: '' },
entries: [
{ ...mail!, category: '' },
{ ...search!, category: '' },
],
}
const wrapper = await mountPanel('/settings/mail', section)
expect(wrapper.find('aside span').text()).toBe('Ustawienia')
})
it('collapses on "Zwiń menu" and persists the choice', async () => {
setSettings(settingsFixture.list.data)
const wrapper = await mountPanel('/settings/mail', settingsSectionOf(settingsEntry('mail'))!)
const button = wrapper.find('[data-sidebar-collapse]')
expect(button.attributes('aria-label')).toBe('Zwiń menu')
expect(button.attributes('type')).toBe('button')
await button.trigger('click')
expect(sidebarCollapsed.value).toBe(true)
expect(window.localStorage.getItem(SIDEBAR_KEY)).toBe('true')
})
})

View File

@@ -8,6 +8,7 @@ import {
settingsGroups,
settingsLinkFor,
settingsPath,
settingsSectionOf,
useSettings,
} from '../../src/state/useSettings'
import { settingsFixture } from '../fixtures/typed'
@@ -65,3 +66,41 @@ describe('useSettings (D-21)', () => {
expect(useSettings().settingsLinkFor).toBe(settingsLinkFor)
})
})
describe('settingsSectionOf (quick-261022-imo)', () => {
const link = (code: string, order: number, controller: string) => ({
...settingsFixture.list.data[0]!,
code,
label: code,
model: '',
controller,
order,
})
it('is null without a current entry or for a one-entry category', () => {
setSettings(settingsFixture.list.data)
expect(settingsSectionOf(null)).toBeNull()
expect(settingsSectionOf(settingsEntry('branding'))).toBeNull()
})
it('returns the current category in settingsGroups order', () => {
setSettings(settingsFixture.list.data)
const section = settingsSectionOf(settingsEntry('mail'))
expect(section?.category).toBe('System')
expect(section?.current.code).toBe('mail')
expect(section?.entries.map((entry) => entry.code)).toEqual(['mail', 'search'])
})
it('places a link entry by its order and never lists another category', () => {
setSettings([...settingsFixture.list.data, link('locales', 15, 'acme.lang.locales')])
const section = settingsSectionOf(settingsLinkFor('acme.lang.locales'))
expect(section?.current.code).toBe('locales')
expect(section?.entries.map((entry) => entry.code)).toEqual(['mail', 'locales', 'search'])
expect(section?.entries.some((entry) => entry.category === 'Appearance')).toBe(false)
expect(settingsSectionOf(settingsEntry('search'))?.entries.map((entry) => entry.code)).not.toContain('branding')
})
it('is exposed through useSettings', () => {
expect(useSettings().settingsSectionOf).toBe(settingsSectionOf)
})
})