Skip to content

Commit df67660

Browse files
committed
test: add unit coverage for policy workbench navigation
Refactor useNavigation scroll helpers to reduce duplication and add unit tests for back-to-top behavior in window and scroll-container modes, including focus behavior for the search input. Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com>
1 parent 32c6c4b commit df67660

2 files changed

Lines changed: 157 additions & 16 deletions

File tree

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
/*
2+
* SPDX-FileCopyrightText: 2026 LibreSign contributors
3+
* SPDX-License-Identifier: AGPL-3.0-or-later
4+
*/
5+
6+
import { mount } from '@vue/test-utils'
7+
import { defineComponent, h } from 'vue'
8+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
9+
import { useNavigation } from '../../../../views/Settings/PolicyWorkbench/Catalog/composables/useNavigation'
10+
11+
function createRect(top: number): DOMRect {
12+
return {
13+
top,
14+
left: 0,
15+
right: 0,
16+
bottom: top + 40,
17+
width: 100,
18+
height: 40,
19+
x: 0,
20+
y: top,
21+
toJSON: () => ({}),
22+
} as DOMRect
23+
}
24+
25+
describe('useNavigation', () => {
26+
function createHarness() {
27+
const Harness = defineComponent({
28+
setup(_, { expose }) {
29+
const navigation = useNavigation({ value: [] })
30+
expose({ navigation })
31+
return () => h('div')
32+
},
33+
})
34+
35+
return mount(Harness)
36+
}
37+
38+
function getNavigation(wrapper: ReturnType<typeof createHarness>): ReturnType<typeof useNavigation> {
39+
return (wrapper.vm as unknown as { navigation: ReturnType<typeof useNavigation> }).navigation
40+
}
41+
42+
beforeEach(() => {
43+
document.body.innerHTML = ''
44+
document.documentElement.style.setProperty('--header-height', '50px')
45+
Object.defineProperty(window, 'scrollTo', {
46+
value: vi.fn(),
47+
configurable: true,
48+
})
49+
})
50+
51+
afterEach(() => {
52+
document.body.innerHTML = ''
53+
vi.restoreAllMocks()
54+
})
55+
56+
it('scrollToTop falls back to page top when toolbar is unavailable', () => {
57+
const wrapper = createHarness()
58+
const navigation = getNavigation(wrapper)
59+
60+
navigation.catalogToolbarRef.value = null
61+
navigation.scrollToTop()
62+
63+
expect(window.scrollTo).toHaveBeenCalledWith({
64+
top: 0,
65+
behavior: 'smooth',
66+
})
67+
68+
wrapper.unmount()
69+
})
70+
71+
it('scrollToTop targets search toolbar in window scroll mode and focuses input', () => {
72+
const wrapper = createHarness()
73+
const navigation = getNavigation(wrapper)
74+
const nativeQuerySelector = document.querySelector.bind(document)
75+
vi.spyOn(document, 'querySelector').mockImplementation((selector: string) => {
76+
if (selector === '#app-content') {
77+
return null
78+
}
79+
80+
return nativeQuerySelector(selector)
81+
})
82+
83+
const toolbar = document.createElement('div')
84+
const input = document.createElement('input')
85+
toolbar.appendChild(input)
86+
document.body.appendChild(toolbar)
87+
toolbar.getBoundingClientRect = vi.fn(() => createRect(300))
88+
89+
document.documentElement.style.setProperty('--header-height', '60px')
90+
Object.defineProperty(window, 'scrollY', {
91+
get: () => 700,
92+
configurable: true,
93+
})
94+
95+
navigation.catalogToolbarRef.value = toolbar
96+
navigation.scrollToTop()
97+
98+
expect(window.scrollTo).toHaveBeenCalledTimes(1)
99+
expect(window.scrollTo).toHaveBeenCalledWith(expect.objectContaining({
100+
behavior: 'smooth',
101+
}))
102+
expect(document.activeElement).toBe(input)
103+
104+
wrapper.unmount()
105+
})
106+
107+
it('scrollToTop targets toolbar inside a scrollable app-content container', () => {
108+
const wrapper = createHarness()
109+
const navigation = getNavigation(wrapper)
110+
111+
const appContent = document.createElement('div')
112+
appContent.id = 'app-content'
113+
Object.defineProperty(appContent, 'scrollHeight', { value: 1200, configurable: true })
114+
Object.defineProperty(appContent, 'clientHeight', { value: 500, configurable: true })
115+
Object.defineProperty(appContent, 'scrollTop', { value: 200, writable: true, configurable: true })
116+
Object.defineProperty(appContent, 'scrollTo', { value: vi.fn(), configurable: true })
117+
document.body.appendChild(appContent)
118+
119+
const toolbar = document.createElement('div')
120+
const input = document.createElement('input')
121+
toolbar.appendChild(input)
122+
appContent.appendChild(toolbar)
123+
124+
appContent.getBoundingClientRect = vi.fn(() => createRect(100))
125+
toolbar.getBoundingClientRect = vi.fn(() => createRect(250))
126+
127+
navigation.catalogToolbarRef.value = toolbar
128+
navigation.scrollToTop()
129+
130+
expect(appContent.scrollTo).toHaveBeenCalledWith({
131+
top: 338,
132+
behavior: 'smooth',
133+
})
134+
expect(window.scrollTo).not.toHaveBeenCalled()
135+
expect(document.activeElement).toBe(input)
136+
137+
wrapper.unmount()
138+
})
139+
})

src/views/Settings/PolicyWorkbench/Catalog/composables/useNavigation.ts

Lines changed: 18 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -259,6 +259,22 @@ export function useNavigation(
259259
})
260260
}
261261

262+
function scrollCurrentContainerToTop() {
263+
scrollContainer.value = getPrimaryScrollContainer()
264+
if (scrollContainer.value instanceof Window) {
265+
window.scrollTo({
266+
top: 0,
267+
behavior: 'smooth',
268+
})
269+
return
270+
}
271+
272+
scrollContainer.value.scrollTo({
273+
top: 0,
274+
behavior: 'smooth',
275+
})
276+
}
277+
262278
function scrollToCategory(category: RealPolicySettingCategory, event?: MouseEvent) {
263279
;(event?.currentTarget as HTMLElement | null)?.blur()
264280
const target = categorySectionElements.get(category) ?? document.getElementById(`policy-category-${category}`)
@@ -286,18 +302,7 @@ export function useNavigation(
286302
function scrollToCatalogToolbar() {
287303
const toolbar = catalogToolbarRef.value
288304
if (!toolbar) {
289-
scrollContainer.value = getPrimaryScrollContainer()
290-
if (scrollContainer.value instanceof Window) {
291-
window.scrollTo({
292-
top: 0,
293-
behavior: 'smooth',
294-
})
295-
} else {
296-
scrollContainer.value.scrollTo({
297-
top: 0,
298-
behavior: 'smooth',
299-
})
300-
}
305+
scrollCurrentContainerToTop()
301306
return
302307
}
303308

@@ -320,10 +325,7 @@ export function useNavigation(
320325
}
321326

322327
if (typeof scrollContainer.value.getBoundingClientRect !== 'function') {
323-
scrollContainer.value.scrollTo({
324-
top: 0,
325-
behavior: 'smooth',
326-
})
328+
scrollCurrentContainerToTop()
327329
focusCatalogSearchInput()
328330
return
329331
}

0 commit comments

Comments
 (0)