Skip to content

Commit dbce19a

Browse files
committed
fix: persist policy workbench user preferences
Signed-off-by: Vitor Mattos <[email protected]>
1 parent 3dc5078 commit dbce19a

4 files changed

Lines changed: 583 additions & 0 deletions

File tree

lib/Service/AccountService.php

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,8 @@ public function getConfig(?IUser $user = null): array {
212212
$info['files_list_sorting_mode'] = $this->getUserConfigByKey('files_list_sorting_mode', $user) ?: 'name';
213213
$info['files_list_sorting_direction'] = $this->getUserConfigByKey('files_list_sorting_direction', $user) ?: 'asc';
214214
$info['policy_workbench_catalog_compact_view'] = $this->getUserConfigByKey('policy_workbench_catalog_compact_view', $user) === '1';
215+
$info['policy_workbench_catalog_collapsed'] = $this->getUserConfigByKey('policy_workbench_catalog_collapsed', $user) === '1';
216+
$info['policy_workbench_category_collapsed_state'] = $this->getUserConfigJsonByKey('policy_workbench_category_collapsed_state', $user);
215217
$info['can_manage_group_policies'] = $this->policyAuthorizationService->canUserManageGroupPolicies($user);
216218
$info['manageable_policy_group_ids'] = $this->policyAuthorizationService->getManageablePolicyGroupIds($user);
217219

@@ -275,6 +277,23 @@ private function getUserConfigByKey(string $key, ?IUser $user = null): string {
275277
return $this->userConfig->getValueString($user->getUID(), Application::APP_ID, $key);
276278
}
277279

280+
/**
281+
* @return array<string, mixed>|null
282+
*/
283+
private function getUserConfigJsonByKey(string $key, ?IUser $user = null): ?array {
284+
if (!$user) {
285+
return null;
286+
}
287+
288+
$value = $this->userConfig->getValueString($user->getUID(), Application::APP_ID, $key, '');
289+
if (empty($value)) {
290+
return null;
291+
}
292+
293+
$decoded = json_decode($value, true);
294+
return is_array($decoded) ? $decoded : null;
295+
}
296+
278297
private function getUserConfigIdDocsFilters(?IUser $user = null): array {
279298
if (!$user) {
280299
return [];
Lines changed: 254 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,254 @@
1+
/**
2+
* SPDX-FileCopyrightText: 2026 LibreCode coop and contributors
3+
* SPDX-License-Identifier: AGPL-3.0-or-later
4+
*/
5+
6+
import { expect, test, type Page } from '@playwright/test'
7+
8+
import { login } from '../support/nc-login'
9+
import { setUserLanguage } from '../support/nc-provisioning'
10+
11+
test.describe.configure({ mode: 'serial', retries: 0, timeout: 90000 })
12+
13+
function collectJavascriptErrors(page: Page) {
14+
const issues: string[] = []
15+
16+
page.on('console', (message) => {
17+
if (message.type() !== 'error') {
18+
return
19+
}
20+
21+
const text = message.text().trim()
22+
if (!text) {
23+
return
24+
}
25+
26+
if (text.includes('/img/app-dark.svg') && text.includes('Content Security Policy directive')) {
27+
return
28+
}
29+
30+
if (text.startsWith('Failed to load resource:')) {
31+
return
32+
}
33+
34+
issues.push(`console.error: ${text}`)
35+
})
36+
37+
page.on('pageerror', (error) => {
38+
const message = error.message.trim()
39+
issues.push(`pageerror: ${message}`)
40+
})
41+
42+
return {
43+
clear() {
44+
issues.length = 0
45+
},
46+
all() {
47+
return [...issues]
48+
},
49+
}
50+
}
51+
52+
async function getCatalogCollapseButton(page: Page) {
53+
return page.getByRole('button', {
54+
name: /Collapse settings categories|Expand settings categories/i,
55+
}).first()
56+
}
57+
58+
async function getCatalogViewButton(page: Page) {
59+
return page.getByRole('button', {
60+
name: /Switch to compact view|Switch to card view/i,
61+
}).first()
62+
}
63+
64+
async function waitForUserConfigSave(page: Page, key: string) {
65+
return page.waitForResponse((response) => {
66+
return response.request().method() === 'PUT'
67+
&& response.url().includes(`/apps/libresign/api/v1/account/config/${key}`)
68+
&& response.status() === 200
69+
})
70+
}
71+
72+
test('catalog controls keep behavior, layout, and JS health', async ({ page }) => {
73+
const adminUser = process.env.NEXTCLOUD_ADMIN_USER ?? 'admin'
74+
const adminPassword = process.env.NEXTCLOUD_ADMIN_PASSWORD ?? 'admin'
75+
76+
await login(page.request, adminUser, adminPassword)
77+
await setUserLanguage(page.request, adminUser, 'en')
78+
79+
await page.setViewportSize({ width: 1365, height: 950 })
80+
81+
const errorCollector = collectJavascriptErrors(page)
82+
83+
await page.goto('./settings/admin/libresign')
84+
85+
const searchField = page.getByRole('textbox', { name: /Search settings/i }).first()
86+
await expect(searchField).toBeVisible({ timeout: 20000 })
87+
88+
const categoryToggles = page.locator('.policy-workbench__category-toggle')
89+
await expect(categoryToggles.first()).toBeVisible({ timeout: 20000 })
90+
await expect(categoryToggles).toHaveCount(7)
91+
92+
const workbenchSection = page.locator('.policy-workbench__section').first()
93+
await expect(workbenchSection).toBeVisible({ timeout: 20000 })
94+
95+
// Ignore potential startup noise and only validate errors introduced by user interactions.
96+
errorCollector.clear()
97+
98+
const collapseButton = await getCatalogCollapseButton(page)
99+
const initialCollapseLabel = await collapseButton.getAttribute('aria-label')
100+
if (initialCollapseLabel && /Expand settings categories/i.test(initialCollapseLabel)) {
101+
await Promise.all([
102+
waitForUserConfigSave(page, 'policy_workbench_catalog_collapsed'),
103+
collapseButton.click(),
104+
])
105+
await expect(collapseButton).toHaveAttribute('aria-label', /Collapse settings categories/i)
106+
}
107+
108+
await Promise.all([
109+
waitForUserConfigSave(page, 'policy_workbench_catalog_collapsed'),
110+
collapseButton.click(),
111+
])
112+
await expect(collapseButton).toHaveAttribute('aria-label', /Expand settings categories/i)
113+
114+
await Promise.all([
115+
waitForUserConfigSave(page, 'policy_workbench_catalog_collapsed'),
116+
collapseButton.click(),
117+
])
118+
await expect(collapseButton).toHaveAttribute('aria-label', /Collapse settings categories/i)
119+
120+
const viewButton = await getCatalogViewButton(page)
121+
const initialViewLabel = await viewButton.getAttribute('aria-label')
122+
if (initialViewLabel && /Switch to card view/i.test(initialViewLabel)) {
123+
await Promise.all([
124+
waitForUserConfigSave(page, 'policy_workbench_catalog_compact_view'),
125+
viewButton.click(),
126+
])
127+
await expect(viewButton).toHaveAttribute('aria-label', /Switch to compact view/i)
128+
}
129+
await expect(viewButton).toHaveAttribute('aria-label', /Switch to compact view/i)
130+
await expect(workbenchSection).toBeVisible()
131+
132+
await Promise.all([
133+
waitForUserConfigSave(page, 'policy_workbench_catalog_compact_view'),
134+
viewButton.click(),
135+
])
136+
await expect(viewButton).toHaveAttribute('aria-label', /Switch to card view/i)
137+
await expect(workbenchSection).toBeVisible()
138+
139+
await Promise.all([
140+
waitForUserConfigSave(page, 'policy_workbench_catalog_compact_view'),
141+
viewButton.click(),
142+
])
143+
await expect(viewButton).toHaveAttribute('aria-label', /Switch to compact view/i)
144+
145+
expect(errorCollector.all(), 'No JavaScript errors should happen during collapse/expand and view switching').toEqual([])
146+
})
147+
148+
test('chip navigation expands target category when catalog is collapsed', async ({ page }) => {
149+
const adminUser = process.env.NEXTCLOUD_ADMIN_USER ?? 'admin'
150+
const adminPassword = process.env.NEXTCLOUD_ADMIN_PASSWORD ?? 'admin'
151+
152+
await login(page.request, adminUser, adminPassword)
153+
await setUserLanguage(page.request, adminUser, 'en')
154+
155+
await page.setViewportSize({ width: 1365, height: 950 })
156+
157+
const errorCollector = collectJavascriptErrors(page)
158+
159+
await page.goto('./settings/admin/libresign')
160+
161+
const searchField = page.getByRole('textbox', { name: /Search settings/i }).first()
162+
await expect(searchField).toBeVisible({ timeout: 20000 })
163+
164+
const collapseButton = await getCatalogCollapseButton(page)
165+
const initialCollapseLabel = await collapseButton.getAttribute('aria-label')
166+
if (initialCollapseLabel && /Expand settings categories/i.test(initialCollapseLabel)) {
167+
await Promise.all([
168+
waitForUserConfigSave(page, 'policy_workbench_catalog_collapsed'),
169+
collapseButton.click(),
170+
])
171+
await expect(collapseButton).toHaveAttribute('aria-label', /Collapse settings categories/i)
172+
}
173+
await Promise.all([
174+
waitForUserConfigSave(page, 'policy_workbench_catalog_collapsed'),
175+
collapseButton.click(),
176+
])
177+
await expect(collapseButton).toHaveAttribute('aria-label', /Expand settings categories/i)
178+
179+
const targetSectionToggle = page.locator('#policy-category-how-signing-works .policy-workbench__category-toggle').first()
180+
const targetSection = page.locator('#policy-category-how-signing-works')
181+
const initialSectionY = await targetSection.boundingBox().then((box) => box?.y ?? Number.POSITIVE_INFINITY)
182+
await expect(targetSectionToggle).toHaveAttribute('aria-expanded', 'false')
183+
184+
const targetChip = page.getByRole('button', { name: /Go to How signing works/i }).first()
185+
await expect(targetChip).toBeVisible({ timeout: 20000 })
186+
await targetChip.click()
187+
188+
await expect(targetSectionToggle).toHaveAttribute('aria-expanded', 'true')
189+
190+
await expect.poll(async () => {
191+
const box = await targetSection.boundingBox()
192+
return box?.y ?? Number.POSITIVE_INFINITY
193+
}, { timeout: 10000 }).toBeLessThan(initialSectionY)
194+
195+
expect(errorCollector.all(), 'No JavaScript errors should happen during chip navigation').toEqual([])
196+
})
197+
198+
test('catalog collapse and per-category expanded state persist after reload', async ({ page }) => {
199+
const adminUser = process.env.NEXTCLOUD_ADMIN_USER ?? 'admin'
200+
const adminPassword = process.env.NEXTCLOUD_ADMIN_PASSWORD ?? 'admin'
201+
202+
await login(page.request, adminUser, adminPassword)
203+
await setUserLanguage(page.request, adminUser, 'en')
204+
205+
await page.setViewportSize({ width: 1365, height: 950 })
206+
207+
const errorCollector = collectJavascriptErrors(page)
208+
209+
await page.goto('./settings/admin/libresign')
210+
211+
const searchField = page.getByRole('textbox', { name: /Search settings/i }).first()
212+
await expect(searchField).toBeVisible({ timeout: 20000 })
213+
214+
const collapseButton = await getCatalogCollapseButton(page)
215+
const initialCollapseLabel = await collapseButton.getAttribute('aria-label')
216+
if (initialCollapseLabel && /Expand settings categories/i.test(initialCollapseLabel)) {
217+
await Promise.all([
218+
waitForUserConfigSave(page, 'policy_workbench_catalog_collapsed'),
219+
collapseButton.click(),
220+
])
221+
await expect(collapseButton).toHaveAttribute('aria-label', /Collapse settings categories/i)
222+
}
223+
224+
await Promise.all([
225+
waitForUserConfigSave(page, 'policy_workbench_catalog_collapsed'),
226+
collapseButton.click(),
227+
])
228+
await expect(collapseButton).toHaveAttribute('aria-label', /Expand settings categories/i)
229+
230+
await page.reload()
231+
await expect(searchField).toBeVisible({ timeout: 20000 })
232+
await expect(collapseButton).toHaveAttribute('aria-label', /Expand settings categories/i)
233+
234+
const signingWorksToggle = page.locator('#policy-category-how-signing-works .policy-workbench__category-toggle').first()
235+
await expect(signingWorksToggle).toHaveAttribute('aria-expanded', 'false')
236+
237+
const signerSeesToggle = page.locator('#policy-category-signer-experience .policy-workbench__category-toggle').first()
238+
await expect(signerSeesToggle).toHaveAttribute('aria-expanded', 'false')
239+
240+
await Promise.all([
241+
waitForUserConfigSave(page, 'policy_workbench_category_collapsed_state'),
242+
signingWorksToggle.click(),
243+
])
244+
await expect(signingWorksToggle).toHaveAttribute('aria-expanded', 'true')
245+
await expect(collapseButton).toHaveAttribute('aria-label', /Collapse settings categories/i)
246+
247+
await page.reload()
248+
await expect(searchField).toBeVisible({ timeout: 20000 })
249+
await expect(collapseButton).toHaveAttribute('aria-label', /Collapse settings categories/i)
250+
await expect(signingWorksToggle).toHaveAttribute('aria-expanded', 'true')
251+
await expect(signerSeesToggle).toHaveAttribute('aria-expanded', 'false')
252+
253+
expect(errorCollector.all(), 'No JavaScript errors should happen while persisting catalog state').toEqual([])
254+
})

0 commit comments

Comments
 (0)