From 4d1a99835f3d533c86d00f573e3bf133912435df Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Fri, 20 Mar 2026 10:21:35 -0300 Subject: [PATCH 01/25] chore: add test infrastructure for component testing - Add @vue/test-utils for component mounting and interaction - Add jsdom for DOM environment in Vitest - Add @vitejs/plugin-vue for Vue SFC compilation in tests Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- package.json | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/package.json b/package.json index ef7fbff..9418520 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,9 @@ "build": "NODE_ENV=production vite --mode production build", "dev": "NODE_ENV=development vite --mode development build", "screenshots:refresh": "node playwright/generate-screenshots.mjs", + "screenshots:refresh:changed": "node playwright/generate-screenshots.mjs --changed", "typescript:generate": "npx openapi-typescript -t", + "ts:check": "npx tsc --noEmit", "watch": "NODE_ENV=development vite --mode development build --watch", "test": "vitest run", "test:watch": "vitest", @@ -33,9 +35,12 @@ "vuedraggable": "^4.1.0" }, "devDependencies": { + "@vue/test-utils": "^2.4.6", + "@vitejs/plugin-vue": "^6.0.4", "@nextcloud/browserslist-config": "^3.1.2", "@nextcloud/vite-config": "^2.5.2", "@playwright/test": "^1.54.2", + "jsdom": "^26.1.0", "openapi-typescript": "^7.13.0", "typescript": "^5.9.3", "vitest": "^4.0.18" From 6b075959c389bf9671caa6d0b372794a4bae3957 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Fri, 20 Mar 2026 10:21:42 -0300 Subject: [PATCH 02/25] chore: update vitest config for component testing - Add jsdom as test environment for browser APIs (localStorage, window) - Configure @vitejs/plugin-vue for Vue SFC compilation - Set component test include pattern with src/tests/ suffix Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- vitest.config.js | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/vitest.config.js b/vitest.config.js index a2af810..a8e5ba6 100644 --- a/vitest.config.js +++ b/vitest.config.js @@ -1,11 +1,14 @@ // SPDX-FileCopyrightText: 2026 LibreCode coop and LibreCode contributors // SPDX-License-Identifier: AGPL-3.0-or-later -import { defineConfig } from 'vitest/config' +import { mergeConfig } from 'vitest/config' +import vue from '@vitejs/plugin-vue' -export default defineConfig({ +export default mergeConfig({ + plugins: [vue()], +}, { test: { include: ['src/tests/**/*.{test,spec}.ts'], - environment: 'node', + environment: 'jsdom', }, }) From 1b4e633b28d022979efcde4b16100b03e2fc9497 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Fri, 20 Mar 2026 10:21:50 -0300 Subject: [PATCH 03/25] feat: create AdminSupportBanner component - Extract LibreCode sponsorship banner into dedicated component - Encapsulate visibility state with localStorage persistence - Handle dismiss action and sponsor page navigation internally - Props: storageKey, sponsorUrl (with sensible defaults) - Fully autonomous - no parent event coordination needed Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- src/components/AdminSupportBanner.vue | 142 ++++++++++++++++++++++++++ 1 file changed, 142 insertions(+) create mode 100644 src/components/AdminSupportBanner.vue diff --git a/src/components/AdminSupportBanner.vue b/src/components/AdminSupportBanner.vue new file mode 100644 index 0000000..acd878a --- /dev/null +++ b/src/components/AdminSupportBanner.vue @@ -0,0 +1,142 @@ + + + + + + + From e1def5c223b5272894b84af479a466431ff016b0 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Fri, 20 Mar 2026 10:21:57 -0300 Subject: [PATCH 04/25] feat: create AdminSelectOptionsSection component - Extract select field options editor into dedicated admin component - Implement drag-and-drop reordering with vuedraggable - Add bulk import dialog for multiple options - Validation: detect duplicate options (case-insensitive) - Keyboard support: Enter to add, Backspace/Delete to remove - Prevent empty option duplicates with smart add logic - Maintains full feature parity with extracted code Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- .../admin/AdminSelectOptionsSection.vue | 424 ++++++++++++++++++ 1 file changed, 424 insertions(+) create mode 100644 src/components/admin/AdminSelectOptionsSection.vue diff --git a/src/components/admin/AdminSelectOptionsSection.vue b/src/components/admin/AdminSelectOptionsSection.vue new file mode 100644 index 0000000..45bd1b3 --- /dev/null +++ b/src/components/admin/AdminSelectOptionsSection.vue @@ -0,0 +1,424 @@ + + + + + + + From aa664ea42f68639ed2642385e84943de4606cfb0 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Fri, 20 Mar 2026 10:22:05 -0300 Subject: [PATCH 05/25] test: add AdminSupportBanner component specs - Test sponsor page opens with correct URL and window features - Test dismiss button hides banner and persists state to localStorage - Test pre-dismissed state: starts hidden when localStorage key exists - Setup: vi.mock @nextcloud/vue components; mount with jsdom - Cleanup: afterEach clears localStorage to prevent test pollution Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- .../admin/AdminSupportBanner.spec.ts | 54 +++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 src/tests/components/admin/AdminSupportBanner.spec.ts diff --git a/src/tests/components/admin/AdminSupportBanner.spec.ts b/src/tests/components/admin/AdminSupportBanner.spec.ts new file mode 100644 index 0000000..00a7996 --- /dev/null +++ b/src/tests/components/admin/AdminSupportBanner.spec.ts @@ -0,0 +1,54 @@ +// SPDX-FileCopyrightText: 2026 LibreCode coop and LibreCode contributors +// SPDX-License-Identifier: AGPL-3.0-or-later + +import { afterEach, describe, expect, it, vi } from 'vitest' +import { mount } from '@vue/test-utils' +import { defineComponent, nextTick } from 'vue' +import AdminSupportBanner from '../../../components/AdminSupportBanner.vue' + +vi.mock('@nextcloud/vue', () => ({ + NcButton: defineComponent({ + name: 'NcButton', + emits: ['click'], + template: '', + }), + NcNoteCard: defineComponent({ + name: 'NcNoteCard', + template: '
', + }), +})) + +afterEach(() => { + window.localStorage.clear() + vi.restoreAllMocks() +}) + +describe('AdminSupportBanner', () => { + it('opens sponsor page when sponsor button is clicked', async() => { + const openSpy = vi.spyOn(window, 'open').mockImplementation(() => null) + const wrapper = mount(AdminSupportBanner) + + await wrapper.get('button').trigger('click') + + expect(openSpy).toHaveBeenCalledWith('https://github.com/sponsors/LibreCodeCoop', '_blank', 'noopener,noreferrer') + }) + + it('hides itself after dismiss and persists state', async() => { + const wrapper = mount(AdminSupportBanner) + + const buttons = wrapper.findAll('button') + await buttons[1].trigger('click') + + expect(wrapper.find('[data-testid="profile-fields-admin-support-banner"]').exists()).toBe(false) + expect(window.localStorage.getItem('profile_fields_support_banner_dismissed')).toBe('1') + }) + + it('starts hidden when dismissal key is already persisted', () => { + window.localStorage.setItem('profile_fields_support_banner_dismissed', '1') + + const wrapper = mount(AdminSupportBanner) + return nextTick().then(() => { + expect(wrapper.find('[data-testid="profile-fields-admin-support-banner"]').exists()).toBe(false) + }) + }) +}) From cd674fd9e3a3eea23f368c008245d9c75b0d95a1 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Fri, 20 Mar 2026 10:22:12 -0300 Subject: [PATCH 06/25] test: add AdminSelectOptionsSection component specs - Test add new option: button click emits update:modelValue with correct data - Test skip add when empty exists: prevents duplicate empty option creation - Setup: mock @nextcloud/vue + Draggable; mount with jsdom - Validation: check emitted events and payload structure Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- .../admin/AdminSelectOptionsSection.spec.ts | 96 +++++++++++++++++++ 1 file changed, 96 insertions(+) create mode 100644 src/tests/components/admin/AdminSelectOptionsSection.spec.ts diff --git a/src/tests/components/admin/AdminSelectOptionsSection.spec.ts b/src/tests/components/admin/AdminSelectOptionsSection.spec.ts new file mode 100644 index 0000000..7eb7036 --- /dev/null +++ b/src/tests/components/admin/AdminSelectOptionsSection.spec.ts @@ -0,0 +1,96 @@ +// SPDX-FileCopyrightText: 2026 LibreCode coop and LibreCode contributors +// SPDX-License-Identifier: AGPL-3.0-or-later + +import { describe, expect, it, vi } from 'vitest' +import { mount } from '@vue/test-utils' +import { defineComponent } from 'vue' +import AdminSelectOptionsSection from '../../../components/admin/AdminSelectOptionsSection.vue' + +vi.mock('@nextcloud/vue', () => ({ + NcActionButton: defineComponent({ template: '
' }), + NcActions: defineComponent({ template: '
' }), + NcButton: defineComponent({ + name: 'NcButton', + emits: ['click'], + template: '', + }), + NcIconSvgWrapper: defineComponent({ template: '
' }), + NcInputField: defineComponent({ template: '
' }), +})) + +vi.mock('@nextcloud/vue/components/NcDialog', () => ({ + default: defineComponent({ template: '
' }), +})) + +vi.mock('@nextcloud/vue/components/NcTextArea', () => ({ + default: defineComponent({ template: '