diff --git a/src/components/dialogs/CustomNavDialog.vue b/src/components/dialogs/CustomNavDialog.vue index 6c964ca3..66b6e947 100644 --- a/src/components/dialogs/CustomNavDialog.vue +++ b/src/components/dialogs/CustomNavDialog.vue @@ -192,6 +192,7 @@ type: 'folder', id: entry.id, name: entry.name, + nameKey: entry.nameKey || null, icon: entry.icon, items: Array.isArray(entry.items) ? [...entry.items] : [] }; @@ -667,6 +668,7 @@ const entry = localLayout.value.find((e) => e.type === 'folder' && e.id === folderEditor.editingId); if (entry) { entry.name = folderEditor.data.name.trim(); + entry.nameKey = null; entry.icon = folderEditor.data.icon?.trim() || DEFAULT_FOLDER_ICON; localLayout.value = [...localLayout.value]; } @@ -675,6 +677,7 @@ type: 'folder', id: folderEditor.data.id, name: folderEditor.data.name.trim(), + nameKey: null, icon: folderEditor.data.icon?.trim() || DEFAULT_FOLDER_ICON, items: [] }); diff --git a/src/components/dialogs/__tests__/CustomNavDialog.test.js b/src/components/dialogs/__tests__/CustomNavDialog.test.js new file mode 100644 index 00000000..97829c7d --- /dev/null +++ b/src/components/dialogs/__tests__/CustomNavDialog.test.js @@ -0,0 +1,76 @@ +import { describe, expect, it, vi } from 'vitest'; +import { mount } from '@vue/test-utils'; + +vi.mock('vue-i18n', () => ({ useI18n: () => ({ t: (k) => k }) })); +vi.mock('@/shared/utils/common', () => ({ openExternalLink: vi.fn() })); +vi.mock('@/components/ui/dialog', () => ({ + Dialog: { template: '
' }, + DialogContent: { template: '
' }, + DialogFooter: { template: '
' }, + DialogHeader: { template: '
' }, + DialogTitle: { template: '
' } +})); +vi.mock('@/components/ui/button', () => ({ + Button: { + emits: ['click'], + template: '' + } +})); +vi.mock('@/components/ui/hover-card', () => ({ + HoverCard: { template: '
' }, + HoverCardContent: { template: '
' }, + HoverCardTrigger: { template: '
' } +})); +vi.mock('@/components/ui/input-group', () => ({ + InputGroupButton: { template: '' }, + InputGroupField: { template: '' } +})); +vi.mock('@/components/ui/separator', () => ({ Separator: { template: '
' } })); +vi.mock('@/components/ui/tree', () => ({ + Tree: { + props: ['items'], + template: '
' + } +})); +vi.mock('@dnd-kit/vue', () => ({ DragDropProvider: { template: '
' } })); +vi.mock('@dnd-kit/vue/sortable', () => ({ isSortable: () => false })); +vi.mock('lucide-vue-next', () => new Proxy({}, { get: () => ({ template: '' }) })); +vi.mock('../SortableTreeNode.vue', () => ({ default: { template: '
' } })); + +import CustomNavDialog from '../CustomNavDialog.vue'; + +describe('CustomNavDialog.vue', () => { + it('keeps folder nameKey when restoring defaults and saving', async () => { + const defaultLayout = [ + { + type: 'folder', + id: 'default-folder-social', + nameKey: 'nav_tooltip.social', + name: 'Social', + icon: 'ri-group-line', + items: ['friend-log'] + } + ]; + + const wrapper = mount(CustomNavDialog, { + props: { + visible: true, + layout: [], + hiddenKeys: [], + defaultLayout + } + }); + + const buttons = wrapper.findAll('[data-testid="btn"]'); + const resetButton = buttons.find((button) => button.text().includes('nav_menu.custom_nav.restore_default')); + const saveButton = buttons.find((button) => button.text().includes('common.actions.confirm')); + + await resetButton.trigger('click'); + await saveButton.trigger('click'); + + const emitted = wrapper.emitted('save'); + expect(emitted).toBeTruthy(); + expect(emitted[0][0]).toEqual(defaultLayout); + expect(emitted[0][0][0].nameKey).toBe('nav_tooltip.social'); + }); +}); diff --git a/src/views/Settings/dialogs/__tests__/AvatarProviderDialog.test.js b/src/views/Settings/dialogs/__tests__/AvatarProviderDialog.test.js new file mode 100644 index 00000000..523bad61 --- /dev/null +++ b/src/views/Settings/dialogs/__tests__/AvatarProviderDialog.test.js @@ -0,0 +1,136 @@ +import { beforeEach, describe, expect, test, vi } from 'vitest'; +import { mount } from '@vue/test-utils'; + +const mocks = vi.hoisted(() => ({ + avatarRemoteDatabaseProviderList: require('vue').ref([]), + saveAvatarProviderList: vi.fn(), + removeAvatarProvider: vi.fn() +})); + +vi.mock('pinia', async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + storeToRefs: (store) => store + }; +}); + +vi.mock('vue-i18n', () => ({ + useI18n: () => ({ + t: (key) => key, + locale: require('vue').ref('en') + }) +})); + +vi.mock('../../../../stores', () => ({ + useAvatarProviderStore: () => ({ + avatarRemoteDatabaseProviderList: mocks.avatarRemoteDatabaseProviderList, + saveAvatarProviderList: (...args) => mocks.saveAvatarProviderList(...args), + removeAvatarProvider: (...args) => mocks.removeAvatarProvider(...args) + }) +})); + +vi.mock('@/components/ui/dialog', () => ({ + Dialog: { + props: ['open'], + emits: ['update:open'], + template: + '
' + + '' + + '' + + '
' + }, + DialogContent: { template: '
' }, + DialogHeader: { template: '
' }, + DialogTitle: { template: '

' } +})); + +vi.mock('@/components/ui/button', () => ({ + Button: { + emits: ['click'], + template: '' + } +})); + +vi.mock('@/components/ui/input-group', () => ({ + InputGroupAction: { + props: ['modelValue', 'size'], + emits: ['update:modelValue', 'change'], + template: + '
' + + '' + + '' + + '
' + } +})); + +vi.mock('lucide-vue-next', () => ({ + Trash2: { + emits: ['click'], + template: '' + } +})); + +import AvatarProviderDialog from '../AvatarProviderDialog.vue'; + +function mountComponent(props = {}) { + return mount(AvatarProviderDialog, { + props: { + isAvatarProviderDialogVisible: true, + ...props + } + }); +} + +describe('AvatarProviderDialog.vue', () => { + beforeEach(() => { + mocks.avatarRemoteDatabaseProviderList.value = ['https://a.example', 'https://b.example']; + mocks.saveAvatarProviderList.mockReset(); + mocks.removeAvatarProvider.mockReset(); + }); + + test('renders provider rows when dialog is visible', () => { + const wrapper = mountComponent(); + + expect(wrapper.find('[data-testid="dialog"]').exists()).toBe(true); + expect(wrapper.findAll('.provider-row')).toHaveLength(2); + }); + + test('emits close when dialog open updates to false', async () => { + const wrapper = mountComponent(); + + await wrapper.get('[data-testid="close-dialog"]').trigger('click'); + + expect(wrapper.emitted('update:isAvatarProviderDialogVisible')).toEqual([[false]]); + }); + + test('adds empty provider entry when add button clicked', async () => { + const wrapper = mountComponent(); + + await wrapper.get('[data-testid="add-provider"]').trigger('click'); + + expect(mocks.avatarRemoteDatabaseProviderList.value).toEqual([ + 'https://a.example', + 'https://b.example', + '' + ]); + }); + + test('calls saveAvatarProviderList on provider input change', async () => { + const wrapper = mountComponent(); + + const input = wrapper.findAll('[data-testid="provider-input"]')[0]; + await input.setValue('https://updated.example'); + + expect(mocks.avatarRemoteDatabaseProviderList.value[0]).toBe('https://updated.example'); + expect(mocks.saveAvatarProviderList).toHaveBeenCalledTimes(1); + }); + + test('calls removeAvatarProvider with row provider when trash clicked', async () => { + const wrapper = mountComponent(); + + await wrapper.findAll('[data-testid="trash"]')[1].trigger('click'); + + expect(mocks.removeAvatarProvider).toHaveBeenCalledWith('https://b.example'); + }); +});