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');
+ });
+});