Skip to content

Commit f06a9f3

Browse files
Merge pull request #61546 from nextcloud/backport/61505/stable34
[stable34] fix(settings): correct heading order in account management sidebar
2 parents 6c7a5a4 + 4158e2c commit f06a9f3

5 files changed

Lines changed: 98 additions & 41 deletions

File tree

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
/**
2+
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
3+
* SPDX-License-Identifier: AGPL-3.0-or-later
4+
*/
5+
6+
import { mount } from '@vue/test-utils'
7+
import { describe, expect, it, vi } from 'vitest'
8+
import { ref } from 'vue'
9+
import NcAppNavigationCaption from '@nextcloud/vue/components/NcAppNavigationCaption'
10+
import AppNavigationGroupList from './AppNavigationGroupList.vue'
11+
12+
// The component builds a real Vuex store via useStore(); mock it so this stays
13+
// a focused component test that controls its own data.
14+
vi.mock('../store/index.js', () => ({
15+
useStore: () => ({
16+
getters: {
17+
getServerData: { isAdmin: false, isDelegatedAdmin: false },
18+
getSortedGroups: [],
19+
getSubAdminGroups: [],
20+
getSearchQuery: '',
21+
},
22+
commit: vi.fn(),
23+
dispatch: vi.fn(),
24+
}),
25+
}))
26+
27+
vi.mock('vue-router/composables', async (importActual) => ({
28+
...(await importActual<object>()),
29+
useRoute: () => ({ params: {} }),
30+
useRouter: () => ({ push: vi.fn() }),
31+
}))
32+
33+
vi.mock('../service/groups.ts', () => ({
34+
searchGroups: () => Promise.resolve([]),
35+
}))
36+
37+
vi.mock('@vueuse/core', async (importActual) => ({
38+
...(await importActual<object>()),
39+
useElementVisibility: () => ref(false),
40+
}))
41+
42+
describe('AppNavigationGroupList', () => {
43+
it('does not expose the group list as a heading (BITV 9.1.3.1a)', () => {
44+
const wrapper = mount(AppNavigationGroupList)
45+
46+
// The sidebar group list is navigation, not document structure. It must
47+
// not emit a heading, which would sit before the page <h1> in the DOM
48+
// and produce an out-of-order outline (h2 before h1).
49+
const caption = wrapper.findComponent(NcAppNavigationCaption)
50+
expect(caption.exists()).toBe(true)
51+
expect(caption.find('h1,h2,h3,h4,h5,h6').exists()).toBe(false)
52+
53+
// The "Groups" label is still rendered, just not as a heading.
54+
expect(caption.text()).toContain('Groups')
55+
})
56+
})

apps/settings/src/components/AppNavigationGroupList.vue

Lines changed: 29 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -5,43 +5,42 @@
55

66
<template>
77
<Fragment>
8-
<NcAppNavigationCaption
9-
:name="t('settings', 'Groups')"
10-
:disabled="loadingAddGroup"
11-
:aria-label="loadingAddGroup ? t('settings', 'Creating group…') : t('settings', 'Create group')"
12-
force-menu
13-
is-heading
14-
:open.sync="isAddGroupOpen">
15-
<template v-if="isAdminOrDelegatedAdmin" #actionsTriggerIcon>
16-
<NcLoadingIcon v-if="loadingAddGroup" />
17-
<NcIconSvgWrapper v-else :path="mdiPlus" />
18-
</template>
19-
<template v-if="isAdminOrDelegatedAdmin" #actions>
20-
<NcActionText>
21-
<template #icon>
22-
<NcIconSvgWrapper :path="mdiAccountGroupOutline" />
23-
</template>
24-
{{ t('settings', 'Create group') }}
25-
</NcActionText>
26-
<NcActionInput
27-
v-model="newGroupName"
28-
:label="t('settings', 'Group name')"
29-
data-cy-users-settings-new-group-name
30-
:label-outside="false"
31-
:disabled="loadingAddGroup"
32-
:error="hasAddGroupError"
33-
:helper-text="hasAddGroupError ? t('settings', 'Please enter a valid group name') : ''"
34-
@submit="createGroup" />
35-
</template>
36-
</NcAppNavigationCaption>
37-
388
<p id="group-list-desc" class="hidden-visually">
399
{{ t('settings', 'List of groups. This list is not fully populated for performance reasons. The groups will be loaded as you navigate or search through the list.') }}
4010
</p>
4111
<NcAppNavigationList
4212
class="account-management__group-list"
13+
:aria-label="t('settings', 'Groups')"
4314
aria-describedby="group-list-desc"
4415
data-cy-users-settings-navigation-groups="custom">
16+
<NcAppNavigationCaption
17+
:name="t('settings', 'Groups')"
18+
:disabled="loadingAddGroup"
19+
:aria-label="loadingAddGroup ? t('settings', 'Creating group…') : t('settings', 'Create group')"
20+
force-menu
21+
:open.sync="isAddGroupOpen">
22+
<template v-if="isAdminOrDelegatedAdmin" #actionsTriggerIcon>
23+
<NcLoadingIcon v-if="loadingAddGroup" />
24+
<NcIconSvgWrapper v-else :path="mdiPlus" />
25+
</template>
26+
<template v-if="isAdminOrDelegatedAdmin" #actions>
27+
<NcActionText>
28+
<template #icon>
29+
<NcIconSvgWrapper :path="mdiAccountGroupOutline" />
30+
</template>
31+
{{ t('settings', 'Create group') }}
32+
</NcActionText>
33+
<NcActionInput
34+
v-model="newGroupName"
35+
:label="t('settings', 'Group name')"
36+
data-cy-users-settings-new-group-name
37+
:label-outside="false"
38+
:disabled="loadingAddGroup"
39+
:error="hasAddGroupError"
40+
:helper-text="hasAddGroupError ? t('settings', 'Please enter a valid group name') : ''"
41+
@submit="createGroup" />
42+
</template>
43+
</NcAppNavigationCaption>
4544
<GroupListItem
4645
v-for="group in filteredGroups"
4746
:id="group.id"

cypress/e2e/settings/users_groups.cy.ts

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,7 @@ describe('Settings: Delete an empty group', { testIsolation: false }, () => {
163163
// see that the list of groups does not contain the group
164164
cy.get('ul[data-cy-users-settings-navigation-groups="custom"]')
165165
.find('li')
166+
.not('.app-navigation-caption')
166167
.should('not.exist')
167168
// and also not in database
168169
cy.runOccCommand('group:list --output=json').then(($response) => {
@@ -219,6 +220,7 @@ describe('Settings: Delete a non empty group', () => {
219220
// see that the list of groups does not contain the group foo
220221
cy.get('ul[data-cy-users-settings-navigation-groups="custom"]')
221222
.find('li')
223+
.not('.app-navigation-caption')
222224
.should('not.exist')
223225
// and also not in database
224226
cy.runOccCommand('group:list --output=json').then(($response) => {
@@ -260,16 +262,16 @@ describe('Settings: Sort groups in the UI', () => {
260262

261263
it('See that the groups are sorted by the member count', () => {
262264
cy.get('ul[data-cy-users-settings-navigation-groups="custom"]').within(() => {
263-
cy.get('li').eq(0).should('contain', 'B') // 1 member
264-
cy.get('li').eq(1).should('contain', 'A') // 0 members
265+
cy.get('li').not('.app-navigation-caption').eq(0).should('contain', 'B') // 1 member
266+
cy.get('li').not('.app-navigation-caption').eq(1).should('contain', 'A') // 0 members
265267
})
266268
})
267269

268270
it('See that the order is preserved after a reload', () => {
269271
cy.reload()
270272
cy.get('ul[data-cy-users-settings-navigation-groups="custom"]').within(() => {
271-
cy.get('li').eq(0).should('contain', 'B') // 1 member
272-
cy.get('li').eq(1).should('contain', 'A') // 0 members
273+
cy.get('li').not('.app-navigation-caption').eq(0).should('contain', 'B') // 1 member
274+
cy.get('li').not('.app-navigation-caption').eq(1).should('contain', 'A') // 0 members
273275
})
274276
})
275277

@@ -288,16 +290,16 @@ describe('Settings: Sort groups in the UI', () => {
288290

289291
it('See that the groups are sorted by the user count', () => {
290292
cy.get('ul[data-cy-users-settings-navigation-groups="custom"]').within(() => {
291-
cy.get('li').eq(0).should('contain', 'A')
292-
cy.get('li').eq(1).should('contain', 'B')
293+
cy.get('li').not('.app-navigation-caption').eq(0).should('contain', 'A')
294+
cy.get('li').not('.app-navigation-caption').eq(1).should('contain', 'B')
293295
})
294296
})
295297

296298
it('See that the order is preserved after a reload', () => {
297299
cy.reload()
298300
cy.get('ul[data-cy-users-settings-navigation-groups="custom"]').within(() => {
299-
cy.get('li').eq(0).should('contain', 'A')
300-
cy.get('li').eq(1).should('contain', 'B')
301+
cy.get('li').not('.app-navigation-caption').eq(0).should('contain', 'A')
302+
cy.get('li').not('.app-navigation-caption').eq(1).should('contain', 'B')
301303
})
302304
})
303305
})

dist/settings-vue-settings-users-management.js

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

dist/settings-vue-settings-users-management.js.map

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)