From 9124ef29651350220f9aee728bddedf3aa3dabbe Mon Sep 17 00:00:00 2001 From: Peter Ringelmann Date: Mon, 22 Jun 2026 16:25:17 +0200 Subject: [PATCH 1/2] fix(settings): correct heading order in account management sidebar Signed-off-by: Peter Ringelmann --- .../components/AppNavigationGroupList.spec.ts | 57 +++++++++++++++++++ .../src/components/AppNavigationGroupList.vue | 28 +++++++++ 2 files changed, 85 insertions(+) create mode 100644 apps/settings/src/components/AppNavigationGroupList.spec.ts diff --git a/apps/settings/src/components/AppNavigationGroupList.spec.ts b/apps/settings/src/components/AppNavigationGroupList.spec.ts new file mode 100644 index 0000000000000..01b53caf27bb3 --- /dev/null +++ b/apps/settings/src/components/AppNavigationGroupList.spec.ts @@ -0,0 +1,57 @@ +/** + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +import { mount } from '@vue/test-utils' +import { ref } from 'vue' +import { describe, expect, it, vi } from 'vitest' +import NcAppNavigationCaption from '@nextcloud/vue/components/NcAppNavigationCaption' + +// The component builds a real Vuex store via useStore(); mock it so this stays +// a focused component test that controls its own data. +vi.mock('../store/index.js', () => ({ + useStore: () => ({ + getters: { + getServerData: { isAdmin: false, isDelegatedAdmin: false }, + getSortedGroups: [], + getSubAdminGroups: [], + getSearchQuery: '', + }, + commit: vi.fn(), + dispatch: vi.fn(), + }), +})) + +vi.mock('vue-router/composables', async (importActual) => ({ + ...(await importActual()), + useRoute: () => ({ params: {} }), + useRouter: () => ({ push: vi.fn() }), +})) + +vi.mock('../service/groups.ts', () => ({ + searchGroups: () => Promise.resolve([]), +})) + +vi.mock('@vueuse/core', async (importActual) => ({ + ...(await importActual()), + useElementVisibility: () => ref(false), +})) + +import AppNavigationGroupList from './AppNavigationGroupList.vue' + +describe('AppNavigationGroupList', () => { + it('does not expose the group list as a heading (BITV 9.1.3.1a)', () => { + const wrapper = mount(AppNavigationGroupList) + + // The sidebar group list is navigation, not document structure. It must + // not emit a heading, which would sit before the page

in the DOM + // and produce an out-of-order outline (h2 before h1). + const caption = wrapper.findComponent(NcAppNavigationCaption) + expect(caption.exists()).toBe(true) + expect(caption.find('h1,h2,h3,h4,h5,h6').exists()).toBe(false) + + // The "Groups" label is still rendered, just not as a heading. + expect(caption.text()).toContain('Groups') + }) +}) diff --git a/apps/settings/src/components/AppNavigationGroupList.vue b/apps/settings/src/components/AppNavigationGroupList.vue index 86abd6e8ba08b..85ba84fa7fc98 100644 --- a/apps/settings/src/components/AppNavigationGroupList.vue +++ b/apps/settings/src/components/AppNavigationGroupList.vue @@ -46,6 +46,34 @@ class="account-management__group-list" aria-describedby="group-list-desc" data-cy-users-settings-navigation-groups="custom"> + + + + Date: Tue, 23 Jun 2026 13:42:43 +0200 Subject: [PATCH 2/2] test(settings): exclude the group caption from group-row selectors test(settings): exclude the group caption from group-row selectors Signed-off-by: Peter Ringelmann [skip ci] --- cypress/e2e/settings/users_groups.cy.ts | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/cypress/e2e/settings/users_groups.cy.ts b/cypress/e2e/settings/users_groups.cy.ts index d91b49110d3aa..76a18ca9d4f23 100644 --- a/cypress/e2e/settings/users_groups.cy.ts +++ b/cypress/e2e/settings/users_groups.cy.ts @@ -169,6 +169,7 @@ describe('Settings: Delete an empty group', { testIsolation: false }, () => { // see that the list of groups does not contain the group cy.get('ul[data-cy-users-settings-navigation-groups="custom"]') .find('li') + .not('.app-navigation-caption') .should('not.exist') // and also not in database cy.runOccCommand('group:list --output=json').then(($response) => { @@ -225,6 +226,7 @@ describe('Settings: Delete a non empty group', () => { // see that the list of groups does not contain the group foo cy.get('ul[data-cy-users-settings-navigation-groups="custom"]') .find('li') + .not('.app-navigation-caption') .should('not.exist') // and also not in database cy.runOccCommand('group:list --output=json').then(($response) => { @@ -266,16 +268,16 @@ describe('Settings: Sort groups in the UI', () => { it('See that the groups are sorted by the member count', () => { cy.get('ul[data-cy-users-settings-navigation-groups="custom"]').within(() => { - cy.get('li').eq(0).should('contain', 'B') // 1 member - cy.get('li').eq(1).should('contain', 'A') // 0 members + cy.get('li').not('.app-navigation-caption').eq(0).should('contain', 'B') // 1 member + cy.get('li').not('.app-navigation-caption').eq(1).should('contain', 'A') // 0 members }) }) it('See that the order is preserved after a reload', () => { cy.reload() cy.get('ul[data-cy-users-settings-navigation-groups="custom"]').within(() => { - cy.get('li').eq(0).should('contain', 'B') // 1 member - cy.get('li').eq(1).should('contain', 'A') // 0 members + cy.get('li').not('.app-navigation-caption').eq(0).should('contain', 'B') // 1 member + cy.get('li').not('.app-navigation-caption').eq(1).should('contain', 'A') // 0 members }) }) @@ -294,16 +296,16 @@ describe('Settings: Sort groups in the UI', () => { it('See that the groups are sorted by the user count', () => { cy.get('ul[data-cy-users-settings-navigation-groups="custom"]').within(() => { - cy.get('li').eq(0).should('contain', 'A') - cy.get('li').eq(1).should('contain', 'B') + cy.get('li').not('.app-navigation-caption').eq(0).should('contain', 'A') + cy.get('li').not('.app-navigation-caption').eq(1).should('contain', 'B') }) }) it('See that the order is preserved after a reload', () => { cy.reload() cy.get('ul[data-cy-users-settings-navigation-groups="custom"]').within(() => { - cy.get('li').eq(0).should('contain', 'A') - cy.get('li').eq(1).should('contain', 'B') + cy.get('li').not('.app-navigation-caption').eq(0).should('contain', 'A') + cy.get('li').not('.app-navigation-caption').eq(1).should('contain', 'B') }) }) })