From 85476fa3fed47e572d530a607c600801caa727df Mon Sep 17 00:00:00 2001 From: Jakub Zych Date: Thu, 22 Oct 2026 13:52:20 +0200 Subject: [PATCH] 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 --- admin/tests/shell/AppShell.test.ts | 81 ++++++++++++++++++++ admin/tests/shell/SettingsPanel.test.ts | 99 +++++++++++++++++++++++++ admin/tests/state/useSettings.test.ts | 39 ++++++++++ 3 files changed, 219 insertions(+) create mode 100644 admin/tests/shell/SettingsPanel.test.ts diff --git a/admin/tests/shell/AppShell.test.ts b/admin/tests/shell/AppShell.test.ts index fcd13730..b9f106b6 100644 --- a/admin/tests/shell/AppShell.test.ts +++ b/admin/tests/shell/AppShell.test.ts @@ -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) + }) }) diff --git a/admin/tests/shell/SettingsPanel.test.ts b/admin/tests/shell/SettingsPanel.test.ts new file mode 100644 index 00000000..6ee333c5 --- /dev/null +++ b/admin/tests/shell/SettingsPanel.test.ts @@ -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') + }) +}) diff --git a/admin/tests/state/useSettings.test.ts b/admin/tests/state/useSettings.test.ts index 539b2158..db3482c9 100644 --- a/admin/tests/state/useSettings.test.ts +++ b/admin/tests/state/useSettings.test.ts @@ -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) + }) +})