Skip to content

Commit 009cf99

Browse files
committed
test: add coverage for tab sidebar entrypoint
Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com>
1 parent 8d483fd commit 009cf99

2 files changed

Lines changed: 171 additions & 8 deletions

File tree

src/tab.ts

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ window.addEventListener('DOMContentLoaded', () => {
5252
const tabPinia = createPinia()
5353
let currentApp: ReturnType<typeof createApp> | null = null
5454
let currentInstance: TabComponentInstance | null = null
55+
let mountVersion = 0
5556

5657
sidebarService.registerTab(new sidebarService.Tab({
5758
id: 'libresign',
@@ -74,15 +75,23 @@ window.addEventListener('DOMContentLoaded', () => {
7475
},
7576
mount(el: HTMLElement, rawFileInfo: unknown) {
7677
const fileInfo = rawFileInfo as FileInfo
77-
currentApp = createApp(AppFilesTab)
78-
currentApp.config.globalProperties.t = t
79-
currentApp.config.globalProperties.n = n
80-
currentApp.use(tabPinia)
81-
currentInstance = currentApp.mount(el) as TabComponentInstance
82-
if (typeof currentInstance?.update === 'function') {
83-
currentInstance.update(fileInfo)
84-
}
8578
window.OCA.Libresign.fileInfo = fileInfo
79+
80+
const currentMountVersion = ++mountVersion
81+
void import('./components/RightSidebar/AppFilesTab.vue').then(({ default: AppFilesTab }) => {
82+
if (!el.isConnected || currentMountVersion !== mountVersion) {
83+
return
84+
}
85+
86+
currentApp = createApp(AppFilesTab)
87+
currentApp.config.globalProperties.t = t
88+
currentApp.config.globalProperties.n = n
89+
currentApp.use(tabPinia)
90+
currentInstance = currentApp.mount(el) as TabComponentInstance
91+
if (typeof currentInstance?.update === 'function') {
92+
currentInstance.update(fileInfo)
93+
}
94+
})
8695
},
8796
update(rawFileInfo: unknown) {
8897
const fileInfo = rawFileInfo as FileInfo
@@ -92,6 +101,7 @@ window.addEventListener('DOMContentLoaded', () => {
92101
window.OCA.Libresign.fileInfo = fileInfo
93102
},
94103
destroy() {
104+
mountVersion += 1
95105
if (currentApp) {
96106
currentApp.unmount()
97107
currentApp = null

src/tests/tab.spec.ts

Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
/*
2+
* SPDX-FileCopyrightText: 2026 LibreSign contributors
3+
* SPDX-License-Identifier: AGPL-3.0-or-later
4+
*/
5+
6+
import { beforeAll, beforeEach, describe, expect, it, vi } from 'vitest'
7+
8+
const mockLoadState = vi.fn(() => true)
9+
const mockRegisterTab = vi.fn()
10+
const mockCreatePinia = vi.fn(() => ({ _id: 'pinia' }))
11+
12+
const mockMountedInstance = {
13+
update: vi.fn(),
14+
}
15+
16+
const mockVueApp = {
17+
config: { globalProperties: {} as Record<string, unknown> },
18+
use: vi.fn().mockReturnThis(),
19+
mount: vi.fn(() => mockMountedInstance),
20+
unmount: vi.fn(),
21+
}
22+
23+
const mockCreateApp = vi.fn(() => mockVueApp)
24+
const appFilesTabModuleLoaded = vi.fn(() => ({
25+
default: { name: 'AppFilesTabStub', template: '<div />' },
26+
}))
27+
28+
vi.mock('@nextcloud/initial-state', () => ({
29+
loadState: mockLoadState,
30+
}))
31+
32+
vi.mock('@nextcloud/l10n', () => ({
33+
t: (_app: string, text: string) => text,
34+
n: (_app: string, singular: string, _plural: string, _count: number) => singular,
35+
}))
36+
37+
vi.mock('@nextcloud/files', () => ({
38+
FileType: { Folder: 'dir' },
39+
}))
40+
41+
vi.mock('pinia', () => ({
42+
createPinia: mockCreatePinia,
43+
}))
44+
45+
vi.mock('vue', () => ({
46+
createApp: mockCreateApp,
47+
}))
48+
49+
vi.mock('../components/RightSidebar/AppFilesTab.vue', () => appFilesTabModuleLoaded())
50+
vi.mock('../../img/app-dark.svg?raw', () => ({ default: '<svg />' }))
51+
vi.mock('../style/icons.scss', () => ({}))
52+
53+
beforeAll(async () => {
54+
await import('../tab')
55+
})
56+
57+
beforeEach(() => {
58+
vi.clearAllMocks()
59+
window.OCA = window.OCA ?? {}
60+
window.OCA.Libresign = {}
61+
;(window.OCA as any).Files = {
62+
Sidebar: {
63+
registerTab: mockRegisterTab,
64+
open: vi.fn(),
65+
setActiveTab: vi.fn(),
66+
Tab: class MockSidebarTab {
67+
constructor(config: Record<string, unknown>) {
68+
return config
69+
}
70+
},
71+
},
72+
}
73+
})
74+
75+
describe('tab.ts', () => {
76+
it('registers LibreSign sidebar tab on DOMContentLoaded', () => {
77+
window.dispatchEvent(new Event('DOMContentLoaded'))
78+
79+
expect(mockRegisterTab).toHaveBeenCalledOnce()
80+
const tabConfig = mockRegisterTab.mock.calls[0][0] as { id: string; name: string }
81+
expect(tabConfig.id).toBe('libresign')
82+
expect(tabConfig.name).toBe('LibreSign')
83+
})
84+
85+
it('enabled() returns false when certificate is not configured', () => {
86+
mockLoadState.mockReturnValue(false)
87+
window.dispatchEvent(new Event('DOMContentLoaded'))
88+
const tabConfig = mockRegisterTab.mock.calls[0][0] as {
89+
enabled: (context: Record<string, unknown>) => boolean
90+
}
91+
92+
expect(tabConfig.enabled({ type: 'file', mimetype: 'application/pdf' })).toBe(false)
93+
})
94+
95+
it('enabled() accepts signed folders and maps file info into OCA.Libresign', () => {
96+
mockLoadState.mockReturnValue(true)
97+
window.dispatchEvent(new Event('DOMContentLoaded'))
98+
const tabConfig = mockRegisterTab.mock.calls[0][0] as {
99+
enabled: (context: Record<string, unknown>) => boolean
100+
update: (context: Record<string, unknown>) => void
101+
}
102+
103+
const fileInfo = {
104+
fileid: 101,
105+
basename: 'Signed',
106+
dirname: '/Documents',
107+
type: 'dir',
108+
attributes: {
109+
'libresign-signature-status': 'completed',
110+
},
111+
}
112+
113+
const enabled = tabConfig.enabled(fileInfo)
114+
tabConfig.update(fileInfo)
115+
116+
expect(enabled).toBe(true)
117+
expect(window.OCA.Libresign.fileInfo).toMatchObject({
118+
fileid: 101,
119+
basename: 'Signed',
120+
dirname: '/Documents',
121+
})
122+
})
123+
124+
it('lazy mounts Vue only when custom element is connected and unmounts on disconnect', async () => {
125+
window.dispatchEvent(new Event('DOMContentLoaded'))
126+
const tabConfig = mockRegisterTab.mock.calls[0][0] as {
127+
mount: (el: HTMLElement, rawFileInfo: Record<string, unknown>) => void
128+
destroy: () => void
129+
}
130+
expect(mockCreateApp).not.toHaveBeenCalled()
131+
expect(appFilesTabModuleLoaded).not.toHaveBeenCalled()
132+
133+
const element = document.createElement('div')
134+
document.body.appendChild(element)
135+
tabConfig.mount(element, {
136+
fileid: 101,
137+
basename: 'Signed',
138+
dirname: '/Documents',
139+
type: 'dir',
140+
attributes: {
141+
'libresign-signature-status': 'completed',
142+
},
143+
})
144+
145+
await vi.waitFor(() => expect(appFilesTabModuleLoaded).toHaveBeenCalledOnce())
146+
expect(mockCreateApp).toHaveBeenCalledOnce()
147+
expect(mockVueApp.mount).toHaveBeenCalledOnce()
148+
149+
tabConfig.destroy()
150+
expect(mockVueApp.unmount).toHaveBeenCalledOnce()
151+
element.remove()
152+
})
153+
})

0 commit comments

Comments
 (0)