use oxfmt instead of prettier

This commit is contained in:
pa
2026-03-13 22:30:12 +09:00
parent 82122a4fab
commit 7b7c1b4568
155 changed files with 3467 additions and 1631 deletions

View File

@@ -43,12 +43,7 @@
resolveRegion,
translateAccessType
} from '../shared/utils';
import {
useAppearanceSettingsStore,
useInstanceStore,
useSearchStore,
useWorldStore
} from '../stores';
import { useAppearanceSettingsStore, useInstanceStore, useSearchStore, useWorldStore } from '../stores';
import { showGroupDialog } from '../coordinators/groupCoordinator';
import { showWorldDialog } from '../coordinators/worldCoordinator';
import { Spinner } from './ui/spinner';
@@ -104,9 +99,7 @@
}
]);
const tooltipContent = computed(() => `${t('dialog.new_instance.instance_id')}: #${instanceName.value}`);
const tooltipDisabled = computed(
() => props.disableTooltip || !instanceName.value || showInstanceIdInLocation.value
);
const tooltipDisabled = computed(() => props.disableTooltip || !instanceName.value || showInstanceIdInLocation.value);
const closedTooltip = computed(() => t('dialog.user.info.instance_closed'));
let isDisposed = false;

View File

@@ -216,7 +216,9 @@ describe('AvatarInfo.vue', () => {
test('does not call showAvatarAuthorDialog when no imageurl', async () => {
const wrapper = mountAvatarInfo({});
await wrapper.trigger('click');
expect(avatarCoordinatorModule.showAvatarAuthorDialog).not.toHaveBeenCalled();
expect(
avatarCoordinatorModule.showAvatarAuthorDialog
).not.toHaveBeenCalled();
});
});
});

View File

@@ -11,7 +11,8 @@ vi.mock('@/components/ui/tooltip', () => ({
vi.mock('@/components/ui/button', () => ({
Button: {
emits: ['click'],
template: '<button data-testid="back-btn" @click="$emit(\'click\', $event)"><slot /></button>'
template:
'<button data-testid="back-btn" @click="$emit(\'click\', $event)"><slot /></button>'
}
}));
@@ -58,7 +59,10 @@ describe('BackToTop.vue', () => {
await btn.trigger('click');
expect(window.scrollTo).toHaveBeenCalledWith({ top: 0, behavior: 'smooth' });
expect(window.scrollTo).toHaveBeenCalledWith({
top: 0,
behavior: 'smooth'
});
});
it('uses virtualizer scrollToIndex when provided', async () => {
@@ -78,7 +82,10 @@ describe('BackToTop.vue', () => {
const btn = wrapper.get('[data-testid="back-btn"]');
await btn.trigger('click');
expect(scrollToIndex).toHaveBeenCalledWith(0, { align: 'start', behavior: 'auto' });
expect(scrollToIndex).toHaveBeenCalledWith(0, {
align: 'start',
behavior: 'auto'
});
expect(window.scrollTo).not.toHaveBeenCalled();
});
@@ -103,6 +110,9 @@ describe('BackToTop.vue', () => {
const btn = wrapper.get('[data-testid="back-btn"]');
await btn.trigger('click');
expect(target.scrollTo).toHaveBeenCalledWith({ top: 0, behavior: 'auto' });
expect(target.scrollTo).toHaveBeenCalledWith({
top: 0,
behavior: 'auto'
});
});
});

View File

@@ -24,7 +24,9 @@ describe('CountdownTimer.vue', () => {
mocks.setInterval.mockClear();
mocks.clearInterval.mockClear();
mocks.timeToText.mockClear();
vi.spyOn(Date, 'now').mockReturnValue(new Date('2026-01-01T00:00:00.000Z').getTime());
vi.spyOn(Date, 'now').mockReturnValue(
new Date('2026-01-01T00:00:00.000Z').getTime()
);
});
afterEach(() => {
@@ -55,7 +57,10 @@ describe('CountdownTimer.vue', () => {
expect(wrapper.text()).toBe('-');
await wrapper.setProps({ datetime: '2025-12-31T23:59:30.000Z', hours: 0 });
await wrapper.setProps({
datetime: '2025-12-31T23:59:30.000Z',
hours: 0
});
await nextTick();
expect(wrapper.text()).toBe('-');
});

View File

@@ -22,7 +22,8 @@ describe('DeprecationAlert.vue', () => {
global: {
stubs: {
i18nT: {
template: '<span data-testid="i18n-t"><slot name="feature" /></span>'
template:
'<span data-testid="i18n-t"><slot name="feature" /></span>'
}
}
}

View File

@@ -2,7 +2,9 @@ import { beforeEach, describe, expect, it, vi } from 'vitest';
import { mount } from '@vue/test-utils';
const mocks = vi.hoisted(() => ({
fetch: vi.fn(() => Promise.resolve({ json: { displayName: 'Fetched User' } })),
fetch: vi.fn(() =>
Promise.resolve({ json: { displayName: 'Fetched User' } })
),
showUserDialog: vi.fn()
}));
@@ -52,7 +54,9 @@ describe('DisplayName.vue', () => {
await flush();
expect(mocks.fetch).toHaveBeenCalledWith('user.dialog', { userId: 'usr_2' });
expect(mocks.fetch).toHaveBeenCalledWith('user.dialog', {
userId: 'usr_2'
});
expect(wrapper.text()).toBe('Fetched User');
});

View File

@@ -27,8 +27,13 @@ vi.mock('../../shared/utils', () => ({
vi.mock('../ui/avatar', () => ({
Avatar: { template: '<div data-testid="avatar"><slot /></div>' },
AvatarImage: { props: ['src'], template: '<img data-testid="avatar-image" :src="src" />' },
AvatarFallback: { template: '<span data-testid="avatar-fallback"><slot /></span>' }
AvatarImage: {
props: ['src'],
template: '<img data-testid="avatar-image" :src="src" />'
},
AvatarFallback: {
template: '<span data-testid="avatar-fallback"><slot /></span>'
}
}));
vi.mock('lucide-vue-next', () => ({
@@ -87,8 +92,12 @@ describe('Emoji.vue', () => {
expect(mocks.getCachedEmoji).toHaveBeenCalledWith('file_1');
expect(wrapper.find('[data-testid="avatar"]').exists()).toBe(true);
expect(wrapper.find('[data-testid="avatar-image"]').attributes('src')).toBe('https://example.com/file_2.png');
expect(wrapper.find('[data-testid="avatar-fallback"]').exists()).toBe(true);
expect(
wrapper.find('[data-testid="avatar-image"]').attributes('src')
).toBe('https://example.com/file_2.png');
expect(wrapper.find('[data-testid="avatar-fallback"]').exists()).toBe(
true
);
});
it('updates when imageUrl changes', async () => {

View File

@@ -2,20 +2,66 @@ import { describe, expect, it, vi } from 'vitest';
import { mount } from '@vue/test-utils';
const mocks = vi.hoisted(() => ({
dialog: { value: { visible: true, imageUrl: 'https://example.com/a.png', fileName: 'a.png' } }
dialog: {
value: {
visible: true,
imageUrl: 'https://example.com/a.png',
fileName: 'a.png'
}
}
}));
vi.mock('pinia', async (i) => ({ ...(await i()), storeToRefs: (s) => s }));
vi.mock('vue-i18n', () => ({ useI18n: () => ({ t: (k) => k }) }));
vi.mock('@/stores/settings/general', () => ({ useGeneralSettingsStore: () => ({ disableGpuAcceleration: { value: false } }) }));
vi.mock('../../stores', () => ({ useGalleryStore: () => ({ fullscreenImageDialog: mocks.dialog, showFullscreenImageDialog: vi.fn() }) }));
vi.mock('@/lib/modalPortalLayers', () => ({ acquireModalPortalLayer: () => ({ element: 'body', bringToFront: vi.fn(), release: vi.fn() }) }));
vi.mock('@/stores/settings/general', () => ({
useGeneralSettingsStore: () => ({
disableGpuAcceleration: { value: false }
})
}));
vi.mock('../../stores', () => ({
useGalleryStore: () => ({
fullscreenImageDialog: mocks.dialog,
showFullscreenImageDialog: vi.fn()
})
}));
vi.mock('@/lib/modalPortalLayers', () => ({
acquireModalPortalLayer: () => ({
element: 'body',
bringToFront: vi.fn(),
release: vi.fn()
})
}));
vi.mock('@/lib/utils', () => ({ cn: (...a) => a.filter(Boolean).join(' ') }));
vi.mock('../../shared/utils', () => ({ escapeTag: (s) => s, extractFileId: () => 'f1' }));
vi.mock('vue-sonner', () => ({ toast: { info: vi.fn(() => 'id'), success: vi.fn(), error: vi.fn(), dismiss: vi.fn() } }));
vi.mock('@/components/ui/dialog', () => ({ Dialog: { template: '<div><slot /></div>' } }));
vi.mock('reka-ui', () => ({ DialogPortal: { template: '<div><slot /></div>' }, DialogOverlay: { template: '<div><slot /></div>' }, DialogContent: { emits: ['click'], template: '<div @click="$emit(\'click\')"><slot /></div>' } }));
vi.mock('@/components/ui/button', () => ({ Button: { emits: ['click'], template: '<button :aria-label="$attrs[\'aria-label\']" @click="$emit(\'click\')"><slot /></button>' } }));
vi.mock('../../shared/utils', () => ({
escapeTag: (s) => s,
extractFileId: () => 'f1'
}));
vi.mock('vue-sonner', () => ({
toast: {
info: vi.fn(() => 'id'),
success: vi.fn(),
error: vi.fn(),
dismiss: vi.fn()
}
}));
vi.mock('@/components/ui/dialog', () => ({
Dialog: { template: '<div><slot /></div>' }
}));
vi.mock('reka-ui', () => ({
DialogPortal: { template: '<div><slot /></div>' },
DialogOverlay: { template: '<div><slot /></div>' },
DialogContent: {
emits: ['click'],
template: '<div @click="$emit(\'click\')"><slot /></div>'
}
}));
vi.mock('@/components/ui/button', () => ({
Button: {
emits: ['click'],
template:
'<button :aria-label="$attrs[\'aria-label\']" @click="$emit(\'click\')"><slot /></button>'
}
}));
vi.mock('lucide-vue-next', () => ({
Copy: { template: '<i />' },
Download: { template: '<i />' },

View File

@@ -33,17 +33,35 @@ vi.mock('../../stores/globalSearch', () => ({
selectResult: (...args) => mocks.selectResult(...args)
})
}));
vi.mock('../../composables/useUserDisplay', () => ({ useUserDisplay: () => ({ userImage: (...a) => mocks.userImage(...a) }) }));
vi.mock('../GlobalSearchSync.vue', () => ({ default: { template: '<div data-testid="sync" />' } }));
vi.mock('@/components/ui/dialog', () => ({ Dialog: { template: '<div><slot /></div>' }, DialogContent: { template: '<div><slot /></div>' }, DialogHeader: { template: '<div><slot /></div>' }, DialogTitle: { template: '<div><slot /></div>' }, DialogDescription: { template: '<div><slot /></div>' } }));
vi.mock('../../composables/useUserDisplay', () => ({
useUserDisplay: () => ({ userImage: (...a) => mocks.userImage(...a) })
}));
vi.mock('../GlobalSearchSync.vue', () => ({
default: { template: '<div data-testid="sync" />' }
}));
vi.mock('@/components/ui/dialog', () => ({
Dialog: { template: '<div><slot /></div>' },
DialogContent: { template: '<div><slot /></div>' },
DialogHeader: { template: '<div><slot /></div>' },
DialogTitle: { template: '<div><slot /></div>' },
DialogDescription: { template: '<div><slot /></div>' }
}));
vi.mock('@/components/ui/command', () => ({
Command: { template: '<div><slot /></div>' },
CommandInput: { template: '<input />' },
CommandList: { template: '<div><slot /></div>' },
CommandGroup: { template: '<div><slot /></div>' },
CommandItem: { emits: ['select'], template: '<button data-testid="cmd-item" @click="$emit(\'select\')"><slot /></button>' }
CommandItem: {
emits: ['select'],
template:
'<button data-testid="cmd-item" @click="$emit(\'select\')"><slot /></button>'
}
}));
vi.mock('lucide-vue-next', () => ({
Globe: { template: '<i />' },
Image: { template: '<i />' },
Users: { template: '<i />' }
}));
vi.mock('lucide-vue-next', () => ({ Globe: { template: '<i />' }, Image: { template: '<i />' }, Users: { template: '<i />' } }));
import GlobalSearchDialog from '../GlobalSearchDialog.vue';

View File

@@ -12,7 +12,10 @@ const mocks = vi.hoisted(() => ({
}
},
filterState: null,
allItemsEntries: [['a', {}], ['b', {}]],
allItemsEntries: [
['a', {}],
['b', {}]
],
allGroupsEntries: [['g1', {}]]
}));

View File

@@ -4,11 +4,18 @@ import { nextTick } from 'vue';
const mocks = vi.hoisted(() => ({
checkCanInviteSelf: vi.fn(() => true),
parseLocation: vi.fn(() => ({ isRealInstance: true, instanceId: 'inst_1', worldId: 'wrld_1', tag: 'wrld_1:inst_1' })),
parseLocation: vi.fn(() => ({
isRealInstance: true,
instanceId: 'inst_1',
worldId: 'wrld_1',
tag: 'wrld_1:inst_1'
})),
hasGroupPermission: vi.fn(() => false),
formatDateFilter: vi.fn(() => 'formatted-date'),
selfInvite: vi.fn(() => Promise.resolve({})),
closeInstance: vi.fn(() => Promise.resolve({ json: { id: 'inst_closed' } })),
closeInstance: vi.fn(() =>
Promise.resolve({ json: { id: 'inst_closed' } })
),
showUserDialog: vi.fn(),
toastSuccess: vi.fn(),
applyInstance: vi.fn(),
@@ -18,7 +25,10 @@ const mocks = vi.hoisted(() => ({
instanceJoinHistory: { value: new Map() },
canOpenInstanceInGame: false,
isOpeningInstance: false,
lastLocation: { location: 'wrld_here:111', playerList: new Set(['u1', 'u2']) },
lastLocation: {
location: 'wrld_here:111',
playerList: new Set(['u1', 'u2'])
},
currentUser: { id: 'usr_me' },
cachedGroups: new Map()
}));
@@ -31,7 +41,9 @@ vi.mock('pinia', async (importOriginal) => {
Object.fromEntries(
Object.entries(store).map(([key, value]) => [
key,
key === 'instanceJoinHistory' ? value : value?.value ?? value
key === 'instanceJoinHistory'
? value
: (value?.value ?? value)
])
)
};
@@ -104,7 +116,8 @@ vi.mock('../../coordinators/userCoordinator', () => ({
vi.mock('@/components/ui/button', () => ({
Button: {
emits: ['click'],
template: '<button data-testid="btn" @click="$emit(\'click\', $event)"><slot /></button>'
template:
'<button data-testid="btn" @click="$emit(\'click\', $event)"><slot /></button>'
}
}));
@@ -157,7 +170,8 @@ function mountBar(props = {}) {
stubs: {
TooltipWrapper: {
props: ['content'],
template: '<div><slot /><slot name="content" /><span v-if="content">{{ content }}</span></div>'
template:
'<div><slot /><slot name="content" /><span v-if="content">{{ content }}</span></div>'
},
Timer: {
props: ['epoch'],
@@ -185,8 +199,12 @@ describe('InstanceActionBar.vue', () => {
mocks.applyInstance.mockClear();
mocks.showLaunchDialog.mockClear();
mocks.tryOpenInstanceInVrc.mockClear();
mocks.modalConfirm.mockImplementation(() => Promise.resolve({ ok: true }));
mocks.instanceJoinHistory.value = new Map([['wrld_base:111', 1700000000]]);
mocks.modalConfirm.mockImplementation(() =>
Promise.resolve({ ok: true })
);
mocks.instanceJoinHistory.value = new Map([
['wrld_base:111', 1700000000]
]);
mocks.canOpenInstanceInGame = false;
mocks.isOpeningInstance = false;
mocks.lastLocation.location = 'wrld_here:111';
@@ -203,8 +221,12 @@ describe('InstanceActionBar.vue', () => {
});
expect(wrapper.findAll('[data-testid="btn"]')).toHaveLength(2);
expect(wrapper.text()).toContain('dialog.user.info.launch_invite_tooltip');
expect(wrapper.text()).toContain('dialog.user.info.self_invite_tooltip');
expect(wrapper.text()).toContain(
'dialog.user.info.launch_invite_tooltip'
);
expect(wrapper.text()).toContain(
'dialog.user.info.self_invite_tooltip'
);
});
it('launch button opens launch dialog with resolved launchLocation', async () => {
@@ -240,7 +262,9 @@ describe('InstanceActionBar.vue', () => {
worldId: 'wrld_1',
shortName: 'sn'
});
expect(mocks.toastSuccess).toHaveBeenCalledWith('message.invite.self_sent');
expect(mocks.toastSuccess).toHaveBeenCalledWith(
'message.invite.self_sent'
);
});
it('invite button opens in VRChat when canOpenInstanceInGame is true', async () => {
@@ -256,7 +280,10 @@ describe('InstanceActionBar.vue', () => {
await inviteBtn.trigger('click');
expect(mocks.tryOpenInstanceInVrc).toHaveBeenCalledWith('wrld_1:inst_1', 'sn');
expect(mocks.tryOpenInstanceInVrc).toHaveBeenCalledWith(
'wrld_1:inst_1',
'sn'
);
expect(mocks.selfInvite).not.toHaveBeenCalled();
});
@@ -302,7 +329,11 @@ describe('InstanceActionBar.vue', () => {
}
});
const closeBtn = wrapper.findAll('button').find((btn) => btn.text().includes('dialog.user.info.close_instance'));
const closeBtn = wrapper
.findAll('button')
.find((btn) =>
btn.text().includes('dialog.user.info.close_instance')
);
expect(closeBtn).toBeTruthy();
await closeBtn.trigger('click');
@@ -311,9 +342,14 @@ describe('InstanceActionBar.vue', () => {
await nextTick();
expect(mocks.modalConfirm).toHaveBeenCalled();
expect(mocks.closeInstance).toHaveBeenCalledWith({ location: 'wrld_close:444', hardClose: false });
expect(mocks.closeInstance).toHaveBeenCalledWith({
location: 'wrld_close:444',
hardClose: false
});
expect(mocks.applyInstance).toHaveBeenCalledWith({ id: 'inst_closed' });
expect(mocks.toastSuccess).toHaveBeenCalledWith('message.instance.closed');
expect(mocks.toastSuccess).toHaveBeenCalledWith(
'message.instance.closed'
);
});
it('hides launch and invite buttons when invite-self is not allowed', () => {

View File

@@ -7,7 +7,11 @@ const mocks = vi.hoisted(() => ({
showLaunchDialog: vi.fn(),
showGroupDialog: vi.fn(),
getGroupName: vi.fn(() => Promise.resolve('Fetched Group')),
parseLocation: vi.fn(() => ({ isRealInstance: true, tag: 'wrld_1:inst_1', groupId: 'grp_1' }))
parseLocation: vi.fn(() => ({
isRealInstance: true,
tag: 'wrld_1:inst_1',
groupId: 'grp_1'
}))
}));
vi.mock('pinia', async (importOriginal) => {
@@ -102,13 +106,19 @@ describe('LocationWorld.vue', () => {
mocks.showGroupDialog.mockClear();
mocks.getGroupName.mockClear();
mocks.parseLocation.mockClear();
mocks.parseLocation.mockImplementation(() => ({ isRealInstance: true, tag: 'wrld_1:inst_1', groupId: 'grp_1' }));
mocks.parseLocation.mockImplementation(() => ({
isRealInstance: true,
tag: 'wrld_1:inst_1',
groupId: 'grp_1'
}));
});
it('renders translated access type and instance name', () => {
const wrapper = mountComponent();
expect(wrapper.text()).toContain('dialog.world.instance.friends #Instance Name');
expect(wrapper.text()).toContain(
'dialog.world.instance.friends #Instance Name'
);
expect(wrapper.find('.flags.eu').exists()).toBe(true);
});
@@ -119,7 +129,10 @@ describe('LocationWorld.vue', () => {
await wrapper.findAll('.cursor-pointer')[0].trigger('click');
expect(mocks.showLaunchDialog).toHaveBeenCalledWith('wrld_1:inst_1', 'short-1');
expect(mocks.showLaunchDialog).toHaveBeenCalledWith(
'wrld_1:inst_1',
'short-1'
);
});
it('shows group hint and opens group dialog', async () => {

View File

@@ -695,11 +695,7 @@
const platforms = [];
if (ref.unityPackages) {
for (const unityPackage of ref.unityPackages) {
if (
unityPackage.variant &&
unityPackage.variant !== 'standard' &&
unityPackage.variant !== 'security'
) {
if (unityPackage.variant && unityPackage.variant !== 'standard' && unityPackage.variant !== 'security') {
// skip imposters
continue;
}

View File

@@ -426,12 +426,7 @@
if (byId) return byId;
}
if (
allowIndexFallback &&
typeof entity.index === 'number' &&
entity.index >= 0 &&
entity.index < nodes.length
) {
if (allowIndexFallback && typeof entity.index === 'number' && entity.index >= 0 && entity.index < nodes.length) {
return nodes[entity.index] || null;
}
@@ -620,8 +615,7 @@
const sourceNode =
(sourceIdSnapshot
? visibleNodes.find(
(node) =>
node.id === sourceIdSnapshot && node.type === (sourceIsFolderSnapshot ? 'folder' : 'item')
(node) => node.id === sourceIdSnapshot && node.type === (sourceIsFolderSnapshot ? 'folder' : 'item')
)
: null) || resolveNodeFromDnDEntity(source, visibleNodes);
if (!sourceNode) return;

View File

@@ -381,8 +381,7 @@
const { showFullscreenImageDialog } = useGalleryStore();
const instanceStore = useInstanceStore();
const { pastCalenderEvents, upcomingCalenderEvents, updateFollowingCalendarData } =
useGroupCalendarEvents(groupDialog);
const { pastCalenderEvents, upcomingCalenderEvents, updateFollowingCalendarData } = useGroupCalendarEvents(groupDialog);
/**
*

View File

@@ -122,11 +122,7 @@
import { useI18n } from 'vue-i18n';
import { useAppearanceSettingsStore, useGalleryStore, useGroupStore, useUserStore } from '../../../stores';
import {
applyGroupMember,
handleGroupMember,
handleGroupMemberProps
} from '../../../coordinators/groupCoordinator';
import { applyGroupMember, handleGroupMember, handleGroupMemberProps } from '../../../coordinators/groupCoordinator';
import { hasGroupPermission } from '../../../shared/utils';
import { useUserDisplay } from '../../../composables/useUserDisplay';
import { groupDialogFilterOptions, groupDialogSortingOptions } from '../../../shared/constants';

View File

@@ -74,13 +74,7 @@
{ label: 'description', text: 'dialog.group_member_moderation.description' },
{ label: 'data', text: 'dialog.group_member_moderation.data' }
];
const checkedGroupLogsExportLogsOptions = ref([
'created_at',
'eventType',
'actorDisplayName',
'description',
'data'
]);
const checkedGroupLogsExportLogsOptions = ref(['created_at', 'eventType', 'actorDisplayName', 'description', 'data']);
/**
*

View File

@@ -10,7 +10,7 @@ vi.mock('vue-i18n', () => ({
locale: require('vue').ref('en')
}),
createI18n: () => ({
global: { t: (key) => key , locale: require('vue').ref('en') },
global: { t: (key) => key, locale: require('vue').ref('en') },
install: vi.fn()
})
}));

View File

@@ -3,12 +3,11 @@ import { mount } from '@vue/test-utils';
vi.mock('vue-i18n', () => ({
useI18n: () => ({
t: (key) => key
,
locale: require('vue').ref('en')
}),
t: (key) => key,
locale: require('vue').ref('en')
}),
createI18n: () => ({
global: { t: (key) => key , locale: require('vue').ref('en') },
global: { t: (key) => key, locale: require('vue').ref('en') },
install: vi.fn()
})
}));
@@ -53,7 +52,8 @@ function mountComponent(props = {}) {
Trash2: { template: '<svg class="trash-icon" />' },
X: { template: '<svg class="x-icon" />' },
TooltipWrapper: {
template: '<div class="tooltip-stub"><slot /><slot name="content" /></div>'
template:
'<div class="tooltip-stub"><slot /><slot name="content" /></div>'
}
}
}
@@ -68,24 +68,32 @@ describe('GroupModerationBulkActions.vue', () => {
describe('rendering', () => {
test('renders user ID input field', () => {
const wrapper = mountComponent();
expect(wrapper.text()).toContain('dialog.group_member_moderation.user_id');
expect(wrapper.text()).toContain(
'dialog.group_member_moderation.user_id'
);
});
test('renders selected users section', () => {
const wrapper = mountComponent();
expect(wrapper.text()).toContain('dialog.group_member_moderation.selected_users');
expect(wrapper.text()).toContain(
'dialog.group_member_moderation.selected_users'
);
});
test('renders roles dropdown with available roles', () => {
const wrapper = mountComponent();
expect(wrapper.text()).toContain('dialog.group_member_moderation.selected_roles');
expect(wrapper.text()).toContain(
'dialog.group_member_moderation.selected_roles'
);
});
test('renders action buttons', () => {
const wrapper = mountComponent();
const text = wrapper.text();
expect(text).toContain('dialog.group_member_moderation.add_roles');
expect(text).toContain('dialog.group_member_moderation.remove_roles');
expect(text).toContain(
'dialog.group_member_moderation.remove_roles'
);
expect(text).toContain('dialog.group_member_moderation.save_note');
expect(text).toContain('dialog.group_member_moderation.kick');
expect(text).toContain('dialog.group_member_moderation.ban');
@@ -95,8 +103,18 @@ describe('GroupModerationBulkActions.vue', () => {
test('renders selected user badges', () => {
const wrapper = mountComponent({
selectedUsersArray: [
{ id: 'usr_1', userId: 'usr_1', membershipStatus: 'member', user: { displayName: 'Alice' } },
{ id: 'usr_2', userId: 'usr_2', membershipStatus: 'member', user: { displayName: 'Bob' } }
{
id: 'usr_1',
userId: 'usr_1',
membershipStatus: 'member',
user: { displayName: 'Alice' }
},
{
id: 'usr_2',
userId: 'usr_2',
membershipStatus: 'member',
user: { displayName: 'Bob' }
}
]
});
expect(wrapper.text()).toContain('Alice');
@@ -106,54 +124,88 @@ describe('GroupModerationBulkActions.vue', () => {
test('shows warning tooltip for non-member users', () => {
const wrapper = mountComponent({
selectedUsersArray: [
{ id: 'usr_1', userId: 'usr_1', membershipStatus: 'banned', user: { displayName: 'Charlie' } }
{
id: 'usr_1',
userId: 'usr_1',
membershipStatus: 'banned',
user: { displayName: 'Charlie' }
}
]
});
expect(wrapper.text()).toContain('dialog.group_member_moderation.user_isnt_in_group');
expect(wrapper.text()).toContain(
'dialog.group_member_moderation.user_isnt_in_group'
);
});
test('does not show warning for member users', () => {
const wrapper = mountComponent({
selectedUsersArray: [
{ id: 'usr_1', userId: 'usr_1', membershipStatus: 'member', user: { displayName: 'Alice' } }
{
id: 'usr_1',
userId: 'usr_1',
membershipStatus: 'member',
user: { displayName: 'Alice' }
}
]
});
expect(wrapper.text()).not.toContain('dialog.group_member_moderation.user_isnt_in_group');
expect(wrapper.text()).not.toContain(
'dialog.group_member_moderation.user_isnt_in_group'
);
});
});
describe('progress indicator', () => {
test('shows progress when progressCurrent > 0', () => {
const wrapper = mountComponent({ progressCurrent: 3, progressTotal: 10 });
expect(wrapper.text()).toContain('dialog.group_member_moderation.progress');
const wrapper = mountComponent({
progressCurrent: 3,
progressTotal: 10
});
expect(wrapper.text()).toContain(
'dialog.group_member_moderation.progress'
);
expect(wrapper.text()).toContain('3/10');
});
test('shows cancel button during progress', () => {
const wrapper = mountComponent({ progressCurrent: 3, progressTotal: 10 });
expect(wrapper.text()).toContain('dialog.group_member_moderation.cancel');
const wrapper = mountComponent({
progressCurrent: 3,
progressTotal: 10
});
expect(wrapper.text()).toContain(
'dialog.group_member_moderation.cancel'
);
});
test('hides progress when not in progress', () => {
const wrapper = mountComponent({ progressCurrent: 0 });
expect(wrapper.text()).not.toContain('dialog.group_member_moderation.progress');
expect(wrapper.text()).not.toContain(
'dialog.group_member_moderation.progress'
);
});
});
describe('button disabled states', () => {
test('add/remove roles disabled when no roles selected', () => {
const wrapper = mountComponent({ selectedRoles: [] });
const addBtn = wrapper.findAll('button').find((b) =>
b.text().includes('dialog.group_member_moderation.add_roles')
);
const addBtn = wrapper
.findAll('button')
.find((b) =>
b
.text()
.includes('dialog.group_member_moderation.add_roles')
);
expect(addBtn.attributes('disabled')).toBeDefined();
});
test('add/remove roles enabled when roles are selected', () => {
const wrapper = mountComponent({ selectedRoles: ['role_1'] });
const addBtn = wrapper.findAll('button').find((b) =>
b.text().includes('dialog.group_member_moderation.add_roles')
);
const addBtn = wrapper
.findAll('button')
.find((b) =>
b
.text()
.includes('dialog.group_member_moderation.add_roles')
);
expect(addBtn.attributes('disabled')).toBeUndefined();
});
@@ -163,25 +215,35 @@ describe('GroupModerationBulkActions.vue', () => {
progressCurrent: 5,
progressTotal: 10
});
const kickBtn = wrapper.findAll('button').find((b) =>
b.text().includes('dialog.group_member_moderation.kick')
);
const kickBtn = wrapper
.findAll('button')
.find((b) =>
b.text().includes('dialog.group_member_moderation.kick')
);
expect(kickBtn.attributes('disabled')).toBeDefined();
});
test('select user button disabled when no user ID entered', () => {
const wrapper = mountComponent({ selectUserId: '' });
const selectBtn = wrapper.findAll('button').find((b) =>
b.text().includes('dialog.group_member_moderation.select_user')
);
const selectBtn = wrapper
.findAll('button')
.find((b) =>
b
.text()
.includes('dialog.group_member_moderation.select_user')
);
expect(selectBtn.attributes('disabled')).toBeDefined();
});
test('select user button enabled when user ID is entered', () => {
const wrapper = mountComponent({ selectUserId: 'usr_test' });
const selectBtn = wrapper.findAll('button').find((b) =>
b.text().includes('dialog.group_member_moderation.select_user')
);
const selectBtn = wrapper
.findAll('button')
.find((b) =>
b
.text()
.includes('dialog.group_member_moderation.select_user')
);
expect(selectBtn.attributes('disabled')).toBeUndefined();
});
});
@@ -194,9 +256,11 @@ describe('GroupModerationBulkActions.vue', () => {
_mockPermissions: ['group-bans-manage']
}
});
const kickBtn = wrapper.findAll('button').find((b) =>
b.text().includes('dialog.group_member_moderation.kick')
);
const kickBtn = wrapper
.findAll('button')
.find((b) =>
b.text().includes('dialog.group_member_moderation.kick')
);
expect(kickBtn.attributes('disabled')).toBeDefined();
});
@@ -207,12 +271,16 @@ describe('GroupModerationBulkActions.vue', () => {
_mockPermissions: ['group-members-remove']
}
});
const banBtn = wrapper.findAll('button').find((b) =>
b.text().includes('dialog.group_member_moderation.ban')
);
const unbanBtn = wrapper.findAll('button').find((b) =>
b.text().includes('dialog.group_member_moderation.unban')
);
const banBtn = wrapper
.findAll('button')
.find((b) =>
b.text().includes('dialog.group_member_moderation.ban')
);
const unbanBtn = wrapper
.findAll('button')
.find((b) =>
b.text().includes('dialog.group_member_moderation.unban')
);
expect(banBtn.attributes('disabled')).toBeDefined();
expect(unbanBtn.attributes('disabled')).toBeDefined();
});
@@ -221,9 +289,13 @@ describe('GroupModerationBulkActions.vue', () => {
describe('events', () => {
test('emits select-user on select button click', async () => {
const wrapper = mountComponent({ selectUserId: 'usr_test' });
const selectBtn = wrapper.findAll('button').find((b) =>
b.text().includes('dialog.group_member_moderation.select_user')
);
const selectBtn = wrapper
.findAll('button')
.find((b) =>
b
.text()
.includes('dialog.group_member_moderation.select_user')
);
await selectBtn.trigger('click');
expect(wrapper.emitted('select-user')).toBeTruthy();
});
@@ -241,7 +313,12 @@ describe('GroupModerationBulkActions.vue', () => {
});
test('emits delete-user when removing a selected user', async () => {
const user = { id: 'usr_1', userId: 'usr_1', membershipStatus: 'member', user: { displayName: 'Alice' } };
const user = {
id: 'usr_1',
userId: 'usr_1',
membershipStatus: 'member',
user: { displayName: 'Alice' }
};
const wrapper = mountComponent({ selectedUsersArray: [user] });
// The X button is a native <button type="button"> inside each Badge
const deleteBtn = wrapper.find('button[type="button"]');
@@ -251,18 +328,25 @@ describe('GroupModerationBulkActions.vue', () => {
test('emits ban on ban button click', async () => {
const wrapper = mountComponent();
const banBtn = wrapper.findAll('button').find((b) =>
b.text().includes('dialog.group_member_moderation.ban')
);
const banBtn = wrapper
.findAll('button')
.find((b) =>
b.text().includes('dialog.group_member_moderation.ban')
);
await banBtn.trigger('click');
expect(wrapper.emitted('ban')).toBeTruthy();
});
test('emits cancel-progress on cancel click', async () => {
const wrapper = mountComponent({ progressCurrent: 3, progressTotal: 10 });
const cancelBtn = wrapper.findAll('button').find((b) =>
b.text().includes('dialog.group_member_moderation.cancel')
);
const wrapper = mountComponent({
progressCurrent: 3,
progressTotal: 10
});
const cancelBtn = wrapper
.findAll('button')
.find((b) =>
b.text().includes('dialog.group_member_moderation.cancel')
);
await cancelBtn.trigger('click');
expect(wrapper.emitted('cancel-progress')).toBeTruthy();
});

View File

@@ -54,7 +54,10 @@ describe('useGroupDialogCommands', () => {
it('returns early when dialog is not visible', () => {
const groupDialog = createGroupDialog({ visible: false });
const deps = createDeps();
const { groupDialogCommand } = useGroupDialogCommands(groupDialog, deps);
const { groupDialogCommand } = useGroupDialogCommands(
groupDialog,
deps
);
groupDialogCommand('Refresh');
expect(deps.showGroupDialog).not.toHaveBeenCalled();
@@ -63,7 +66,10 @@ describe('useGroupDialogCommands', () => {
it('Share copies group URL', () => {
const groupDialog = createGroupDialog();
const deps = createDeps();
const { groupDialogCommand } = useGroupDialogCommands(groupDialog, deps);
const { groupDialogCommand } = useGroupDialogCommands(
groupDialog,
deps
);
groupDialogCommand('Share');
expect(copyToClipboard).toHaveBeenCalledWith(
@@ -74,7 +80,10 @@ describe('useGroupDialogCommands', () => {
it('Invite To Group dispatches invite callback', () => {
const groupDialog = createGroupDialog();
const deps = createDeps();
const { groupDialogCommand } = useGroupDialogCommands(groupDialog, deps);
const { groupDialogCommand } = useGroupDialogCommands(
groupDialog,
deps
);
groupDialogCommand('Invite To Group');
expect(deps.showInviteGroupDialog).toHaveBeenCalledWith('grp_123', '');
@@ -83,7 +92,10 @@ describe('useGroupDialogCommands', () => {
it('Refresh calls showGroupDialog with forceRefresh', () => {
const groupDialog = createGroupDialog();
const deps = createDeps();
const { groupDialogCommand } = useGroupDialogCommands(groupDialog, deps);
const { groupDialogCommand } = useGroupDialogCommands(
groupDialog,
deps
);
groupDialogCommand('Refresh');
expect(deps.showGroupDialog).toHaveBeenCalledWith('grp_123', {
@@ -94,7 +106,10 @@ describe('useGroupDialogCommands', () => {
it('Block Group confirms and calls blockGroup', async () => {
const groupDialog = createGroupDialog();
const deps = createDeps();
const { groupDialogCommand } = useGroupDialogCommands(groupDialog, deps);
const { groupDialogCommand } = useGroupDialogCommands(
groupDialog,
deps
);
groupDialogCommand('Block Group');
await vi.waitFor(() => {
@@ -109,7 +124,10 @@ describe('useGroupDialogCommands', () => {
it('Unblock Group confirms and calls unblockGroup', async () => {
const groupDialog = createGroupDialog();
const deps = createDeps();
const { groupDialogCommand } = useGroupDialogCommands(groupDialog, deps);
const { groupDialogCommand } = useGroupDialogCommands(
groupDialog,
deps
);
groupDialogCommand('Unblock Group');
await vi.waitFor(() => {
@@ -129,7 +147,10 @@ describe('useGroupDialogCommands', () => {
confirm: vi.fn().mockResolvedValue({ ok: false })
}
});
const { groupDialogCommand } = useGroupDialogCommands(groupDialog, deps);
const { groupDialogCommand } = useGroupDialogCommands(
groupDialog,
deps
);
groupDialogCommand('Block Group');
await vi.waitFor(() => {

View File

@@ -233,9 +233,7 @@ describe('useGroupGalleries', () => {
.spyOn(console, 'error')
.mockImplementation(() => {});
queryRequest.fetch.mockRejectedValueOnce(
new Error('API Error')
);
queryRequest.fetch.mockRejectedValueOnce(new Error('API Error'));
await expect(
getGroupGallery('grp_1', 'g1')

View File

@@ -4,12 +4,11 @@ import { describe, expect, test, vi, beforeEach } from 'vitest';
vi.mock('vue-sonner', () => ({ toast: { success: vi.fn(), error: vi.fn() } }));
vi.mock('vue-i18n', () => ({
useI18n: () => ({
t: (key) => key
,
locale: require('vue').ref('en')
}),
t: (key) => key,
locale: require('vue').ref('en')
}),
createI18n: () => ({
global: { t: (key) => key , locale: require('vue').ref('en') },
global: { t: (key) => key, locale: require('vue').ref('en') },
install: vi.fn()
})
}));
@@ -95,11 +94,19 @@ import { queryRequest } from '../../../../api';
function createTables() {
return {
members: reactive({ data: [], pageSize: 15 }),
bans: reactive({ data: [], filters: [{ prop: ['$displayName'], value: '' }], pageSize: 15 }),
bans: reactive({
data: [],
filters: [{ prop: ['$displayName'], value: '' }],
pageSize: 15
}),
invites: reactive({ data: [], pageSize: 15 }),
joinRequests: reactive({ data: [], pageSize: 15 }),
blocked: reactive({ data: [], pageSize: 15 }),
logs: reactive({ data: [], filters: [{ prop: ['description'], value: '' }], pageSize: 15 })
logs: reactive({
data: [],
filters: [{ prop: ['description'], value: '' }],
pageSize: 15
})
};
}
@@ -167,11 +174,19 @@ describe('useGroupModerationData', () => {
userId: `usr_${i}`,
user: { displayName: `User${i}` }
}));
const page2 = [{ userId: 'usr_100', user: { displayName: 'User100' } }];
const page2 = [
{ userId: 'usr_100', user: { displayName: 'User100' } }
];
deps.groupRequest.getGroupBans
.mockResolvedValueOnce({ json: page1, params: { groupId: 'grp_test' } })
.mockResolvedValueOnce({ json: page2, params: { groupId: 'grp_test' } });
.mockResolvedValueOnce({
json: page1,
params: { groupId: 'grp_test' }
})
.mockResolvedValueOnce({
json: page2,
params: { groupId: 'grp_test' }
});
const { getAllGroupBans } = useGroupModerationData(deps);
await getAllGroupBans('grp_test');
@@ -198,12 +213,17 @@ describe('useGroupModerationData', () => {
test('handles API error gracefully', async () => {
const { toast } = await import('vue-sonner');
const deps = createDeps();
deps.groupRequest.getGroupBans.mockRejectedValue(new Error('Network error'));
deps.groupRequest.getGroupBans.mockRejectedValue(
new Error('Network error')
);
const { getAllGroupBans, isGroupMembersLoading } = useGroupModerationData(deps);
const { getAllGroupBans, isGroupMembersLoading } =
useGroupModerationData(deps);
await getAllGroupBans('grp_test');
expect(toast.error).toHaveBeenCalledWith('Failed to get group bans');
expect(toast.error).toHaveBeenCalledWith(
'Failed to get group bans'
);
expect(isGroupMembersLoading.value).toBe(false);
});
@@ -213,10 +233,16 @@ describe('useGroupModerationData', () => {
userId: `usr_${i}`
}));
deps.groupRequest.getGroupBans
.mockResolvedValueOnce({ json: page1, params: { groupId: 'grp_test' } })
.mockResolvedValueOnce({
json: page1,
params: { groupId: 'grp_test' }
})
.mockImplementation(() => {
deps.groupMemberModeration.value.visible = false;
return Promise.resolve({ json: [{ userId: 'usr_extra' }], params: { groupId: 'grp_test' } });
return Promise.resolve({
json: [{ userId: 'usr_extra' }],
params: { groupId: 'grp_test' }
});
});
const { getAllGroupBans } = useGroupModerationData(deps);
@@ -254,7 +280,10 @@ describe('useGroupModerationData', () => {
});
const { getAllGroupLogs } = useGroupModerationData(deps);
await getAllGroupLogs('grp_test', ['group.member.ban', 'group.member.kick']);
await getAllGroupLogs('grp_test', [
'group.member.ban',
'group.member.kick'
]);
expect(deps.groupRequest.getGroupLogs).toHaveBeenCalledWith(
expect.objectContaining({
@@ -281,7 +310,8 @@ describe('useGroupModerationData', () => {
params: { groupId: 'grp_test' }
});
const { getAllGroupInvitesAndJoinRequests } = useGroupModerationData(deps);
const { getAllGroupInvitesAndJoinRequests } =
useGroupModerationData(deps);
await getAllGroupInvitesAndJoinRequests('grp_test');
expect(deps.tables.invites.data).toHaveLength(1);
@@ -294,7 +324,10 @@ describe('useGroupModerationData', () => {
test('parses multiple user IDs from input', async () => {
const deps = createDeps();
deps.groupRequest.getGroupMember.mockResolvedValue({
json: { userId: 'usr_aaaa1111-2222-3333-4444-555566667777', user: { displayName: 'A' } },
json: {
userId: 'usr_aaaa1111-2222-3333-4444-555566667777',
user: { displayName: 'A' }
},
params: {}
});
@@ -344,7 +377,10 @@ describe('useGroupModerationData', () => {
const { addGroupMemberToSelection } = useGroupModerationData(deps);
await addGroupMemberToSelection('usr_1');
expect(deps.selection.setSelectedUsers).toHaveBeenCalledWith('usr_1', member);
expect(deps.selection.setSelectedUsers).toHaveBeenCalledWith(
'usr_1',
member
);
expect(queryRequest.fetch).not.toHaveBeenCalled();
});
@@ -362,11 +398,16 @@ describe('useGroupModerationData', () => {
const { addGroupMemberToSelection } = useGroupModerationData(deps);
await addGroupMemberToSelection('usr_1');
expect(queryRequest.fetch).toHaveBeenCalledWith('user.dialog', { userId: 'usr_1' });
expect(deps.selection.setSelectedUsers).toHaveBeenCalledWith('usr_1', expect.objectContaining({
userId: 'usr_1',
displayName: 'Alice'
}));
expect(queryRequest.fetch).toHaveBeenCalledWith('user.dialog', {
userId: 'usr_1'
});
expect(deps.selection.setSelectedUsers).toHaveBeenCalledWith(
'usr_1',
expect.objectContaining({
userId: 'usr_1',
displayName: 'Alice'
})
);
});
});
@@ -395,7 +436,8 @@ describe('useGroupModerationData', () => {
const deps = createDeps();
deps.tables.members.data = [{ userId: 'usr_1' }];
const { groupMembersSearch, memberSearch, isGroupMembersLoading } = useGroupModerationData(deps);
const { groupMembersSearch, memberSearch, isGroupMembersLoading } =
useGroupModerationData(deps);
memberSearch.value = 'ab';
groupMembersSearch();
@@ -405,9 +447,13 @@ describe('useGroupModerationData', () => {
test('setGroupMemberSortOrder does nothing when sort is the same', async () => {
const deps = createDeps();
deps.groupRequest.getGroupMember.mockResolvedValue({ json: null, params: {} });
deps.groupRequest.getGroupMember.mockResolvedValue({
json: null,
params: {}
});
const { setGroupMemberSortOrder, memberSortOrder } = useGroupModerationData(deps);
const { setGroupMemberSortOrder, memberSortOrder } =
useGroupModerationData(deps);
const currentSort = memberSortOrder.value;
await setGroupMemberSortOrder(currentSort);
@@ -418,7 +464,8 @@ describe('useGroupModerationData', () => {
test('setGroupMemberFilter does nothing when filter is the same', async () => {
const deps = createDeps();
const { setGroupMemberFilter, memberFilter } = useGroupModerationData(deps);
const { setGroupMemberFilter, memberFilter } =
useGroupModerationData(deps);
const currentFilter = memberFilter.value;
await setGroupMemberFilter(currentFilter);
@@ -429,7 +476,8 @@ describe('useGroupModerationData', () => {
describe('loadAllGroupMembers', () => {
test('does nothing when already loading', async () => {
const deps = createDeps();
const { loadAllGroupMembers, isGroupMembersLoading } = useGroupModerationData(deps);
const { loadAllGroupMembers, isGroupMembersLoading } =
useGroupModerationData(deps);
isGroupMembersLoading.value = true;
await loadAllGroupMembers();

View File

@@ -174,11 +174,7 @@
});
};
addFriendGroup(
'friendsInInstance',
t('dialog.invite.friends_in_instance'),
props.inviteDialog?.friendsInInstance
);
addFriendGroup('friendsInInstance', t('dialog.invite.friends_in_instance'), props.inviteDialog?.friendsInInstance);
addFriendGroup('vip', t('side_panel.favorite'), vipFriends.value);
addFriendGroup('online', t('side_panel.online'), onlineFriends.value);
addFriendGroup('active', t('side_panel.active'), activeFriends.value);

View File

@@ -47,13 +47,7 @@
useUserStore,
useVrcxStore
} from '../../../stores';
import {
compareByCreatedAt,
localeIncludes,
parseLocation,
removeFromArray,
timeToText
} from '../../../shared/utils';
import { compareByCreatedAt, localeIncludes, parseLocation, removeFromArray, timeToText } from '../../../shared/utils';
import { DataTableLayout } from '../../ui/data-table';
import { createPreviousInstancesColumns } from './previousInstancesColumns.jsx';
import { database } from '../../../services/database';
@@ -176,9 +170,7 @@
function deleteGameLogInstancePrompt(row) {
const description =
props.variant === 'user'
? 'Continue? Delete User From GameLog Instance'
: 'Continue? Delete GameLog Instance';
props.variant === 'user' ? 'Continue? Delete User From GameLog Instance' : 'Continue? Delete GameLog Instance';
modalStore
.confirm({
description,

View File

@@ -497,7 +497,7 @@
useInstanceStore,
useLocationStore,
useModalStore,
useUserStore,
useUserStore
} from '../../../stores';
import { showWorldDialog } from '../../../coordinators/worldCoordinator';
import { queryRequest, userRequest } from '../../../api';
@@ -506,7 +506,6 @@
import { showUserDialog } from '../../../coordinators/userCoordinator';
import { showGroupDialog } from '../../../coordinators/groupCoordinator';
const EditNoteAndMemoDialog = defineAsyncComponent(() => import('./EditNoteAndMemoDialog.vue'));
defineEmits(['showBioDialog']);

View File

@@ -61,12 +61,7 @@
import { storeToRefs } from 'pinia';
import { useI18n } from 'vue-i18n';
import {
compareByDisplayName,
compareByFriendOrder,
compareByLastActiveRef,
userImage
} from '../../../shared/utils';
import { compareByDisplayName, compareByFriendOrder, compareByLastActiveRef, userImage } from '../../../shared/utils';
import { database } from '../../../services/database';
import { processBulk } from '../../../services/request';
import { useOptionKeySelect } from '../../../composables/useOptionKeySelect';

View File

@@ -249,13 +249,7 @@
import { storeToRefs } from 'pinia';
import { useI18n } from 'vue-i18n';
import {
formatDateFilter,
languageClass,
openDiscordProfile,
userImage,
userStatusClass
} from '../../../shared/utils';
import { formatDateFilter, languageClass, openDiscordProfile, userImage, userStatusClass } from '../../../shared/utils';
import { Popover, PopoverContent, PopoverTrigger } from '../../ui/popover';
import { useGalleryStore, useUserStore } from '../../../stores';
import { Badge } from '../../ui/badge';

View File

@@ -2,20 +2,61 @@ import { beforeEach, describe, expect, it, vi } from 'vitest';
import { mount } from '@vue/test-utils';
import { ref } from 'vue';
const mocks = vi.hoisted(() => ({ saveUserMemo: vi.fn(), saveNote: vi.fn(async () => ({ json: { note: 'n1' }, params: { targetUserId: 'usr_1', note: 'n1' } })), getUser: vi.fn() }));
const mocks = vi.hoisted(() => ({
saveUserMemo: vi.fn(),
saveNote: vi.fn(async () => ({
json: { note: 'n1' },
params: { targetUserId: 'usr_1', note: 'n1' }
})),
getUser: vi.fn()
}));
vi.mock('pinia', async (i) => ({ ...(await i()), storeToRefs: (s) => s }));
vi.mock('vue-i18n', () => ({ useI18n: () => ({ t: (k) => k }) }));
vi.mock('../../../../stores', () => ({
useUserStore: () => ({ userDialog: ref({ id: 'usr_1', note: 'n1', memo: 'm1', ref: { id: 'usr_1', note: 'n1' } }), cachedUsers: new Map([['usr_1', { note: 'n1' }]]) }),
useAppearanceSettingsStore: () => ({ hideUserNotes: ref(false), hideUserMemos: ref(false) })
useUserStore: () => ({
userDialog: ref({
id: 'usr_1',
note: 'n1',
memo: 'm1',
ref: { id: 'usr_1', note: 'n1' }
}),
cachedUsers: new Map([['usr_1', { note: 'n1' }]])
}),
useAppearanceSettingsStore: () => ({
hideUserNotes: ref(false),
hideUserMemos: ref(false)
})
}));
vi.mock('../../../../api', () => ({
miscRequest: { saveNote: (...a) => mocks.saveNote(...a) },
userRequest: { getUser: (...a) => mocks.getUser(...a) }
}));
vi.mock('../../../../coordinators/memoCoordinator', () => ({
saveUserMemo: (...a) => mocks.saveUserMemo(...a)
}));
vi.mock('../../../../api', () => ({ miscRequest: { saveNote: (...a) => mocks.saveNote(...a) }, userRequest: { getUser: (...a) => mocks.getUser(...a) } }));
vi.mock('../../../../coordinators/memoCoordinator', () => ({ saveUserMemo: (...a) => mocks.saveUserMemo(...a) }));
vi.mock('../../../../shared/utils', () => ({ replaceBioSymbols: (s) => s }));
vi.mock('@/components/ui/dialog', () => ({ Dialog: { template: '<div><slot /></div>' }, DialogContent: { template: '<div><slot /></div>' }, DialogHeader: { template: '<div><slot /></div>' }, DialogTitle: { template: '<div><slot /></div>' }, DialogFooter: { template: '<div><slot /></div>' } }));
vi.mock('@/components/ui/button', () => ({ Button: { emits: ['click'], template: '<button data-testid="btn" @click="$emit(\'click\')"><slot /></button>' } }));
vi.mock('@/components/ui/input-group', () => ({ InputGroupTextareaField: { props: ['modelValue'], emits: ['update:modelValue'], template: '<textarea />' } }));
vi.mock('@/components/ui/dialog', () => ({
Dialog: { template: '<div><slot /></div>' },
DialogContent: { template: '<div><slot /></div>' },
DialogHeader: { template: '<div><slot /></div>' },
DialogTitle: { template: '<div><slot /></div>' },
DialogFooter: { template: '<div><slot /></div>' }
}));
vi.mock('@/components/ui/button', () => ({
Button: {
emits: ['click'],
template:
'<button data-testid="btn" @click="$emit(\'click\')"><slot /></button>'
}
}));
vi.mock('@/components/ui/input-group', () => ({
InputGroupTextareaField: {
props: ['modelValue'],
emits: ['update:modelValue'],
template: '<textarea />'
}
}));
import EditNoteAndMemoDialog from '../EditNoteAndMemoDialog.vue';
@@ -25,7 +66,9 @@ describe('EditNoteAndMemoDialog.vue', () => {
});
it('emits close and saves memo on confirm', async () => {
const wrapper = mount(EditNoteAndMemoDialog, { props: { visible: false } });
const wrapper = mount(EditNoteAndMemoDialog, {
props: { visible: false }
});
await wrapper.setProps({ visible: true });
const buttons = wrapper.findAll('[data-testid="btn"]');
await buttons[1].trigger('click');

View File

@@ -5,14 +5,49 @@ import { ref } from 'vue';
vi.mock('pinia', async (i) => ({ ...(await i()), storeToRefs: (s) => s }));
vi.mock('vue-i18n', () => ({ useI18n: () => ({ t: (k) => k }) }));
vi.mock('../../../../stores', () => ({
useUserStore: () => ({ userDialog: ref({ ref: { id: 'usr_2', $isModerator: false }, isFriend: false, isFavorite: false, incomingRequest: false, outgoingRequest: false, isBlock: false, isMute: false, isMuteChat: false, isInteractOff: false, isHideAvatar: false, isShowAvatar: false }), currentUser: ref({ id: 'usr_1', isBoopingEnabled: true }) }),
useUserStore: () => ({
userDialog: ref({
ref: { id: 'usr_2', $isModerator: false },
isFriend: false,
isFavorite: false,
incomingRequest: false,
outgoingRequest: false,
isBlock: false,
isMute: false,
isMuteChat: false,
isInteractOff: false,
isHideAvatar: false,
isShowAvatar: false
}),
currentUser: ref({ id: 'usr_1', isBoopingEnabled: true })
}),
useGameStore: () => ({ isGameRunning: ref(false) }),
useLocationStore: () => ({ lastLocation: ref({ location: 'wrld_1:1' }) })
}));
vi.mock('../../../../composables/useInviteChecks', () => ({ useInviteChecks: () => ({ checkCanInvite: () => true }) }));
vi.mock('../../../ui/dropdown-menu', () => ({ DropdownMenu: { template: '<div><slot /></div>' }, DropdownMenuTrigger: { template: '<div><slot /></div>' }, DropdownMenuContent: { template: '<div><slot /></div>' }, DropdownMenuSeparator: { template: '<hr />' }, DropdownMenuItem: { emits: ['click'], template: '<button data-testid="dd-item" @click="$emit(\'click\')"><slot /></button>' } }));
vi.mock('@/components/ui/button', () => ({ Button: { emits: ['click'], template: '<button data-testid="btn" @click="$emit(\'click\')"><slot /></button>' } }));
vi.mock('../../../ui/tooltip', () => ({ TooltipWrapper: { template: '<div><slot /></div>' } }));
vi.mock('../../../../composables/useInviteChecks', () => ({
useInviteChecks: () => ({ checkCanInvite: () => true })
}));
vi.mock('../../../ui/dropdown-menu', () => ({
DropdownMenu: { template: '<div><slot /></div>' },
DropdownMenuTrigger: { template: '<div><slot /></div>' },
DropdownMenuContent: { template: '<div><slot /></div>' },
DropdownMenuSeparator: { template: '<hr />' },
DropdownMenuItem: {
emits: ['click'],
template:
'<button data-testid="dd-item" @click="$emit(\'click\')"><slot /></button>'
}
}));
vi.mock('@/components/ui/button', () => ({
Button: {
emits: ['click'],
template:
'<button data-testid="btn" @click="$emit(\'click\')"><slot /></button>'
}
}));
vi.mock('../../../ui/tooltip', () => ({
TooltipWrapper: { template: '<div><slot /></div>' }
}));
vi.mock('lucide-vue-next', () => ({
Check: { template: '<i />' },
CheckCircle: { template: '<i />' },
@@ -42,7 +77,9 @@ import UserActionDropdown from '../UserActionDropdown.vue';
describe('UserActionDropdown.vue', () => {
it('forwards command callback from dropdown item', async () => {
const userDialogCommand = vi.fn();
const wrapper = mount(UserActionDropdown, { props: { userDialogCommand } });
const wrapper = mount(UserActionDropdown, {
props: { userDialogCommand }
});
await wrapper.findAll('[data-testid="dd-item"]')[0].trigger('click');

View File

@@ -6,7 +6,8 @@ vi.mock('vue-i18n', () => ({
useI18n: () => {
const { ref } = require('vue');
return {
t: (key, params) => (params ? `${key}:${JSON.stringify(params)}` : key),
t: (key, params) =>
params ? `${key}:${JSON.stringify(params)}` : key,
locale: ref('en')
};
},
@@ -242,7 +243,9 @@ describe('UserDialogInfoTab.vue', () => {
test('renders imported InstanceActionBar and Spinner components when conditions are met', () => {
const wrapper = mountComponent();
expect(wrapper.find('instance-action-bar-stub').exists()).toBe(true);
expect(wrapper.find('instance-action-bar-stub').exists()).toBe(
true
);
expect(wrapper.find('spinner-stub').exists()).toBe(true);
});
});

View File

@@ -10,7 +10,7 @@ vi.mock('vue-i18n', () => ({
locale: require('vue').ref('en')
}),
createI18n: () => ({
global: { t: (key) => key , locale: require('vue').ref('en') },
global: { t: (key) => key, locale: require('vue').ref('en') },
install: vi.fn()
})
}));

View File

@@ -6,15 +6,34 @@ const mocks = vi.hoisted(() => ({
addFavorite: vi.fn(() => Promise.resolve()),
deleteFavoriteNoConfirm: vi.fn(),
toastSuccess: vi.fn(),
favoriteDialog: { __v_isRef: true, value: { visible: true, type: 'friend', objectId: 'usr_1', currentGroup: null } }
favoriteDialog: {
__v_isRef: true,
value: {
visible: true,
type: 'friend',
objectId: 'usr_1',
currentGroup: null
}
}
}));
vi.mock('pinia', async (i) => ({ ...(await i()), storeToRefs: (s) => s }));
vi.mock('vue-i18n', () => ({ useI18n: () => ({ t: (k) => k }) }));
vi.mock('vue-sonner', () => ({ toast: { success: (...a) => mocks.toastSuccess(...a) } }));
vi.mock('vue-sonner', () => ({
toast: { success: (...a) => mocks.toastSuccess(...a) }
}));
vi.mock('../../../stores', () => ({
useFavoriteStore: () => ({
favoriteFriendGroups: ref([{ key: 'group_1', type: 'friend', name: 'group_1', displayName: 'G1', count: 0, capacity: 100 }]),
favoriteFriendGroups: ref([
{
key: 'group_1',
type: 'friend',
name: 'group_1',
displayName: 'G1',
count: 0,
capacity: 100
}
]),
favoriteAvatarGroups: ref([]),
favoriteWorldGroups: ref([]),
favoriteDialog: mocks.favoriteDialog,
@@ -31,9 +50,22 @@ vi.mock('../../../stores', () => ({
}),
useUserStore: () => ({ isLocalUserVrcPlusSupporter: ref(true) })
}));
vi.mock('../../../api', () => ({ favoriteRequest: { addFavorite: (...a) => mocks.addFavorite(...a) } }));
vi.mock('@/components/ui/dialog', () => ({ Dialog: { template: '<div><slot /></div>' }, DialogContent: { template: '<div><slot /></div>' }, DialogHeader: { template: '<div><slot /></div>' }, DialogTitle: { template: '<div><slot /></div>' } }));
vi.mock('@/components/ui/button', () => ({ Button: { emits: ['click'], template: '<button data-testid="btn" @click="$emit(\'click\')"><slot /></button>' } }));
vi.mock('../../../api', () => ({
favoriteRequest: { addFavorite: (...a) => mocks.addFavorite(...a) }
}));
vi.mock('@/components/ui/dialog', () => ({
Dialog: { template: '<div><slot /></div>' },
DialogContent: { template: '<div><slot /></div>' },
DialogHeader: { template: '<div><slot /></div>' },
DialogTitle: { template: '<div><slot /></div>' }
}));
vi.mock('@/components/ui/button', () => ({
Button: {
emits: ['click'],
template:
'<button data-testid="btn" @click="$emit(\'click\')"><slot /></button>'
}
}));
vi.mock('lucide-vue-next', () => ({ Check: { template: '<i />' } }));
import ChooseFavoriteGroupDialog from '../ChooseFavoriteGroupDialog.vue';
@@ -42,7 +74,12 @@ describe('ChooseFavoriteGroupDialog.vue', () => {
beforeEach(() => {
mocks.addFavorite.mockClear();
mocks.toastSuccess.mockClear();
mocks.favoriteDialog.value = { visible: true, type: 'friend', objectId: 'usr_1', currentGroup: null };
mocks.favoriteDialog.value = {
visible: true,
type: 'friend',
objectId: 'usr_1',
currentGroup: null
};
});
it('runs delete action for current group', async () => {
@@ -50,7 +87,12 @@ describe('ChooseFavoriteGroupDialog.vue', () => {
visible: true,
type: 'friend',
objectId: 'usr_1',
currentGroup: { key: 'group_1', displayName: 'G1', count: 0, capacity: 100 }
currentGroup: {
key: 'group_1',
displayName: 'G1',
count: 0,
capacity: 100
}
};
const wrapper = mount(ChooseFavoriteGroupDialog);
await wrapper.get('[data-testid="btn"]').trigger('click');

View File

@@ -5,8 +5,16 @@ vi.mock('vue-i18n', () => ({ useI18n: () => ({ t: (k) => k }) }));
vi.mock('@/shared/utils/common', () => ({ openExternalLink: vi.fn() }));
vi.mock('../../../stores', () => ({
useDashboardStore: () => ({
createDashboard: vi.fn(async () => ({ id: 'dashboard-1', name: 'Dashboard', icon: 'ri-dashboard-line' })),
getDashboard: vi.fn(() => ({ id: 'dashboard-1', name: 'Dashboard', icon: 'ri-dashboard-line' })),
createDashboard: vi.fn(async () => ({
id: 'dashboard-1',
name: 'Dashboard',
icon: 'ri-dashboard-line'
})),
getDashboard: vi.fn(() => ({
id: 'dashboard-1',
name: 'Dashboard',
icon: 'ri-dashboard-line'
})),
updateDashboard: vi.fn(async () => {}),
deleteDashboard: vi.fn(async () => {}),
setEditingDashboardId: vi.fn()
@@ -25,7 +33,8 @@ vi.mock('@/components/ui/dialog', () => ({
vi.mock('@/components/ui/button', () => ({
Button: {
emits: ['click'],
template: '<button data-testid="btn" @click="$emit(\'click\')"><slot /></button>'
template:
'<button data-testid="btn" @click="$emit(\'click\')"><slot /></button>'
}
}));
vi.mock('@/components/ui/hover-card', () => ({
@@ -37,17 +46,26 @@ vi.mock('@/components/ui/input-group', () => ({
InputGroupButton: { template: '<button><slot /></button>' },
InputGroupField: { template: '<input />' }
}));
vi.mock('@/components/ui/separator', () => ({ Separator: { template: '<hr />' } }));
vi.mock('@/components/ui/separator', () => ({
Separator: { template: '<hr />' }
}));
vi.mock('@/components/ui/tree', () => ({
Tree: {
props: ['items'],
template: '<div><slot :flatten-items="[]" /></div>'
}
}));
vi.mock('@dnd-kit/vue', () => ({ DragDropProvider: { template: '<div><slot /></div>' } }));
vi.mock('@dnd-kit/vue', () => ({
DragDropProvider: { template: '<div><slot /></div>' }
}));
vi.mock('@dnd-kit/vue/sortable', () => ({ isSortable: () => false }));
vi.mock('lucide-vue-next', () => new Proxy({}, { get: () => ({ template: '<i />' }) }));
vi.mock('../SortableTreeNode.vue', () => ({ default: { template: '<div />' } }));
vi.mock(
'lucide-vue-next',
() => new Proxy({}, { get: () => ({ template: '<i />' }) })
);
vi.mock('../SortableTreeNode.vue', () => ({
default: { template: '<div />' }
}));
import CustomNavDialog from '../CustomNavDialog.vue';
@@ -74,8 +92,12 @@ describe('CustomNavDialog.vue', () => {
});
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'));
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');

View File

@@ -19,13 +19,36 @@ vi.mock('../../composables/useImageCropper', () => ({
getCroppedBlob: (...a) => mocks.getCroppedBlob(...a)
})
}));
vi.mock('@/components/ui/dialog', () => ({ Dialog: { template: '<div><slot /></div>' }, DialogContent: { template: '<div><slot /></div>' }, DialogHeader: { template: '<div><slot /></div>' }, DialogTitle: { template: '<div><slot /></div>' }, DialogFooter: { template: '<div><slot /></div>' } }));
vi.mock('@/components/ui/button', () => ({ Button: { emits: ['click'], template: '<button data-testid="btn" @click="$emit(\'click\')"><slot /></button>' } }));
vi.mock('@/components/ui/slider', () => ({ Slider: { emits: ['value-commit'], template: '<div />' } }));
vi.mock('@/components/ui/spinner', () => ({ Spinner: { template: '<div />' } }));
vi.mock('@/components/ui/tooltip/TooltipWrapper.vue', () => ({ default: { template: '<div><slot /></div>' } }));
vi.mock('vue-advanced-cropper', () => ({ Cropper: { emits: ['change'], template: '<div />' } }));
vi.mock('lucide-vue-next', () => new Proxy({}, { get: () => ({ template: '<i />' }) }));
vi.mock('@/components/ui/dialog', () => ({
Dialog: { template: '<div><slot /></div>' },
DialogContent: { template: '<div><slot /></div>' },
DialogHeader: { template: '<div><slot /></div>' },
DialogTitle: { template: '<div><slot /></div>' },
DialogFooter: { template: '<div><slot /></div>' }
}));
vi.mock('@/components/ui/button', () => ({
Button: {
emits: ['click'],
template:
'<button data-testid="btn" @click="$emit(\'click\')"><slot /></button>'
}
}));
vi.mock('@/components/ui/slider', () => ({
Slider: { emits: ['value-commit'], template: '<div />' }
}));
vi.mock('@/components/ui/spinner', () => ({
Spinner: { template: '<div />' }
}));
vi.mock('@/components/ui/tooltip/TooltipWrapper.vue', () => ({
default: { template: '<div><slot /></div>' }
}));
vi.mock('vue-advanced-cropper', () => ({
Cropper: { emits: ['change'], template: '<div />' }
}));
vi.mock(
'lucide-vue-next',
() => new Proxy({}, { get: () => ({ template: '<i />' }) })
);
import ImageCropDialog from '../ImageCropDialog.vue';

View File

@@ -10,24 +10,71 @@ const mocks = vi.hoisted(() => ({
setString: vi.fn(),
getString: vi.fn(async () => ''),
applyGroup: vi.fn((g) => g),
inviteDialog: { __v_isRef: true, value: { visible: true, loading: false, groupId: 'grp_1', userId: '', userIds: ['usr_1'], groupName: '', userObject: null } }
inviteDialog: {
__v_isRef: true,
value: {
visible: true,
loading: false,
groupId: 'grp_1',
userId: '',
userIds: ['usr_1'],
groupName: '',
userObject: null
}
}
}));
vi.mock('pinia', async (i) => ({ ...(await i()), storeToRefs: (s) => s }));
vi.mock('vue-i18n', () => ({ useI18n: () => ({ t: (k) => k }) }));
vi.mock('vue-sonner', () => ({ toast: { error: vi.fn() } }));
vi.mock('../../../shared/utils', () => ({ hasGroupPermission: () => true }));
vi.mock('../../../composables/useUserDisplay', () => ({ useUserDisplay: () => ({ userImage: () => '', userStatusClass: () => '' }) }));
vi.mock('../../../composables/useUserDisplay', () => ({
useUserDisplay: () => ({ userImage: () => '', userStatusClass: () => '' })
}));
vi.mock('../../../stores', () => ({
useFriendStore: () => ({ vipFriends: ref([]), onlineFriends: ref([]), activeFriends: ref([]), offlineFriends: ref([]) }),
useGroupStore: () => ({ currentUserGroups: ref(new Map()), inviteGroupDialog: mocks.inviteDialog, applyGroup: (...a) => mocks.applyGroup(...a) }),
useFriendStore: () => ({
vipFriends: ref([]),
onlineFriends: ref([]),
activeFriends: ref([]),
offlineFriends: ref([])
}),
useGroupStore: () => ({
currentUserGroups: ref(new Map()),
inviteGroupDialog: mocks.inviteDialog,
applyGroup: (...a) => mocks.applyGroup(...a)
}),
useModalStore: () => ({ confirm: (...a) => mocks.confirm(...a) })
}));
vi.mock('../../../api', () => ({ groupRequest: { sendGroupInvite: (...a) => mocks.sendGroupInvite(...a), getGroup: (...a) => mocks.getGroup(...a) }, queryRequest: { fetch: (...a) => mocks.fetch(...a) } }));
vi.mock('../../../services/config', () => ({ default: { getString: (...a) => mocks.getString(...a), setString: (...a) => mocks.setString(...a) } }));
vi.mock('@/components/ui/dialog', () => ({ Dialog: { template: '<div><slot /></div>' }, DialogContent: { template: '<div><slot /></div>' }, DialogHeader: { template: '<div><slot /></div>' }, DialogTitle: { template: '<div><slot /></div>' }, DialogFooter: { template: '<div><slot /></div>' } }));
vi.mock('@/components/ui/button', () => ({ Button: { emits: ['click'], template: '<button data-testid="btn" @click="$emit(\'click\')"><slot /></button>' } }));
vi.mock('../../ui/virtual-combobox', () => ({ VirtualCombobox: { template: '<div />' } }));
vi.mock('../../../api', () => ({
groupRequest: {
sendGroupInvite: (...a) => mocks.sendGroupInvite(...a),
getGroup: (...a) => mocks.getGroup(...a)
},
queryRequest: { fetch: (...a) => mocks.fetch(...a) }
}));
vi.mock('../../../services/config', () => ({
default: {
getString: (...a) => mocks.getString(...a),
setString: (...a) => mocks.setString(...a)
}
}));
vi.mock('@/components/ui/dialog', () => ({
Dialog: { template: '<div><slot /></div>' },
DialogContent: { template: '<div><slot /></div>' },
DialogHeader: { template: '<div><slot /></div>' },
DialogTitle: { template: '<div><slot /></div>' },
DialogFooter: { template: '<div><slot /></div>' }
}));
vi.mock('@/components/ui/button', () => ({
Button: {
emits: ['click'],
template:
'<button data-testid="btn" @click="$emit(\'click\')"><slot /></button>'
}
}));
vi.mock('../../ui/virtual-combobox', () => ({
VirtualCombobox: { template: '<div />' }
}));
vi.mock('lucide-vue-next', () => ({ Check: { template: '<i />' } }));
import InviteGroupDialog from '../InviteGroupDialog.vue';

View File

@@ -6,10 +6,19 @@ const mocks = vi.hoisted(() => ({
selfInvite: vi.fn(async () => ({})),
writeText: vi.fn(),
getBool: vi.fn(async () => false),
launchDialogData: { value: { visible: true, loading: true, tag: 'wrld_1:123', shortName: 'abc' } }
launchDialogData: {
value: {
visible: true,
loading: true,
tag: 'wrld_1:123',
shortName: 'abc'
}
}
}));
Object.assign(globalThis, { navigator: { clipboard: { writeText: (...a) => mocks.writeText(...a) } } });
Object.assign(globalThis, {
navigator: { clipboard: { writeText: (...a) => mocks.writeText(...a) } }
});
vi.mock('pinia', async (i) => ({ ...(await i()), storeToRefs: (s) => s }));
vi.mock('vue-i18n', () => ({ useI18n: () => ({ t: (k) => k }) }));
@@ -18,27 +27,81 @@ vi.mock('../../../stores', () => ({
useFriendStore: () => ({ friends: ref(new Map()) }),
useGameStore: () => ({ isGameRunning: ref(false) }),
useInviteStore: () => ({ canOpenInstanceInGame: ref(false) }),
useLaunchStore: () => ({ launchDialogData: mocks.launchDialogData, launchGame: vi.fn(), tryOpenInstanceInVrc: vi.fn() }),
useLaunchStore: () => ({
launchDialogData: mocks.launchDialogData,
launchGame: vi.fn(),
tryOpenInstanceInVrc: vi.fn()
}),
useLocationStore: () => ({ lastLocation: ref({ friendList: new Map() }) }),
useModalStore: () => ({ confirm: vi.fn() })
}));
vi.mock('../../../shared/utils', () => ({
getLaunchURL: () => 'vrchat://launch',
isRealInstance: () => true,
parseLocation: () => ({ isRealInstance: true, worldId: 'wrld_1', instanceId: '123', tag: 'wrld_1:123' })
parseLocation: () => ({
isRealInstance: true,
worldId: 'wrld_1',
instanceId: '123',
tag: 'wrld_1:123'
})
}));
vi.mock('../../../composables/useInviteChecks', () => ({
useInviteChecks: () => ({ checkCanInvite: () => true })
}));
vi.mock('../../../api', () => ({
instanceRequest: {
selfInvite: (...a) => mocks.selfInvite(...a),
getInstanceShortName: vi.fn()
},
queryRequest: { fetch: vi.fn() }
}));
vi.mock('../../../services/config', () => ({
default: { getBool: (...a) => mocks.getBool(...a), setBool: vi.fn() }
}));
vi.mock('@/components/ui/dialog', () => ({
Dialog: { template: '<div><slot /></div>' },
DialogContent: { template: '<div><slot /></div>' },
DialogHeader: { template: '<div><slot /></div>' },
DialogTitle: { template: '<div><slot /></div>' },
DialogDescription: { template: '<div><slot /></div>' },
DialogFooter: { template: '<div><slot /></div>' }
}));
vi.mock('@/components/ui/dropdown-menu', () => ({
DropdownMenu: { template: '<div><slot /></div>' },
DropdownMenuTrigger: { template: '<div><slot /></div>' },
DropdownMenuContent: { template: '<div><slot /></div>' },
DropdownMenuItem: { template: '<div><slot /></div>' }
}));
vi.mock('@/components/ui/field', () => ({
Field: { template: '<div><slot /></div>' },
FieldGroup: { template: '<div><slot /></div>' },
FieldLabel: { template: '<div><slot /></div>' },
FieldContent: { template: '<div><slot /></div>' }
}));
vi.mock('@/components/ui/button', () => ({
Button: {
emits: ['click'],
template:
'<button data-testid="btn" @click="$emit(\'click\')"><slot /></button>'
}
}));
vi.mock('@/components/ui/button-group', () => ({
ButtonGroup: { template: '<div><slot /></div>' }
}));
vi.mock('@/components/ui/input-group', () => ({
InputGroupField: { template: '<input />' }
}));
vi.mock('@/components/ui/tooltip', () => ({
TooltipWrapper: { template: '<div><slot /></div>' }
}));
vi.mock('../InviteDialog/InviteDialog.vue', () => ({
default: { template: '<div />' }
}));
vi.mock('lucide-vue-next', () => ({
Copy: { template: '<i />' },
Info: { template: '<i />' },
MoreHorizontal: { template: '<i />' }
}));
vi.mock('../../../composables/useInviteChecks', () => ({ useInviteChecks: () => ({ checkCanInvite: () => true }) }));
vi.mock('../../../api', () => ({ instanceRequest: { selfInvite: (...a) => mocks.selfInvite(...a), getInstanceShortName: vi.fn() }, queryRequest: { fetch: vi.fn() } }));
vi.mock('../../../services/config', () => ({ default: { getBool: (...a) => mocks.getBool(...a), setBool: vi.fn() } }));
vi.mock('@/components/ui/dialog', () => ({ Dialog: { template: '<div><slot /></div>' }, DialogContent: { template: '<div><slot /></div>' }, DialogHeader: { template: '<div><slot /></div>' }, DialogTitle: { template: '<div><slot /></div>' }, DialogDescription: { template: '<div><slot /></div>' }, DialogFooter: { template: '<div><slot /></div>' } }));
vi.mock('@/components/ui/dropdown-menu', () => ({ DropdownMenu: { template: '<div><slot /></div>' }, DropdownMenuTrigger: { template: '<div><slot /></div>' }, DropdownMenuContent: { template: '<div><slot /></div>' }, DropdownMenuItem: { template: '<div><slot /></div>' } }));
vi.mock('@/components/ui/field', () => ({ Field: { template: '<div><slot /></div>' }, FieldGroup: { template: '<div><slot /></div>' }, FieldLabel: { template: '<div><slot /></div>' }, FieldContent: { template: '<div><slot /></div>' } }));
vi.mock('@/components/ui/button', () => ({ Button: { emits: ['click'], template: '<button data-testid="btn" @click="$emit(\'click\')"><slot /></button>' } }));
vi.mock('@/components/ui/button-group', () => ({ ButtonGroup: { template: '<div><slot /></div>' } }));
vi.mock('@/components/ui/input-group', () => ({ InputGroupField: { template: '<input />' } }));
vi.mock('@/components/ui/tooltip', () => ({ TooltipWrapper: { template: '<div><slot /></div>' } }));
vi.mock('../InviteDialog/InviteDialog.vue', () => ({ default: { template: '<div />' } }));
vi.mock('lucide-vue-next', () => ({ Copy: { template: '<i />' }, Info: { template: '<i />' }, MoreHorizontal: { template: '<i />' } }));
import LaunchDialog from '../LaunchDialog.vue';

View File

@@ -5,31 +5,83 @@ import { ref } from 'vue';
const mocks = vi.hoisted(() => ({
closeMainDialog: vi.fn(),
handleBreadcrumbClick: vi.fn(),
dialogCrumbs: { value: [{ type: 'user', id: 'u1', label: 'User' }, { type: 'world', id: 'w1', label: 'World' }] },
dialogCrumbs: {
value: [
{ type: 'user', id: 'u1', label: 'User' },
{ type: 'world', id: 'w1', label: 'World' }
]
},
userVisible: { value: true }
}));
vi.mock('pinia', async (i) => ({ ...(await i()), storeToRefs: (s) => s }));
vi.mock('@/stores', () => ({
useUiStore: () => ({ dialogCrumbs: mocks.dialogCrumbs.value, closeMainDialog: (...a) => mocks.closeMainDialog(...a), handleBreadcrumbClick: (...a) => mocks.handleBreadcrumbClick(...a) }),
useUiStore: () => ({
dialogCrumbs: mocks.dialogCrumbs.value,
closeMainDialog: (...a) => mocks.closeMainDialog(...a),
handleBreadcrumbClick: (...a) => mocks.handleBreadcrumbClick(...a)
}),
useUserStore: () => ({ userDialog: { visible: mocks.userVisible.value } }),
useWorldStore: () => ({ worldDialog: { visible: false } }),
useAvatarStore: () => ({ avatarDialog: { visible: false } }),
useGroupStore: () => ({ groupDialog: { visible: false } }),
useInstanceStore: () => ({ previousInstancesInfoDialog: ref({ visible: false }), previousInstancesListDialog: ref({ visible: false, variant: 'user' }) })
useInstanceStore: () => ({
previousInstancesInfoDialog: ref({ visible: false }),
previousInstancesListDialog: ref({ visible: false, variant: 'user' })
})
}));
vi.mock('@/components/ui/dialog', () => ({
Dialog: { template: '<div><slot /></div>' },
DialogContent: { template: '<div><slot /></div>' }
}));
vi.mock('@/components/ui/breadcrumb', () => ({
Breadcrumb: { template: '<div><slot /></div>' },
BreadcrumbList: { template: '<div><slot /></div>' },
BreadcrumbItem: { template: '<div><slot /></div>' },
BreadcrumbLink: { template: '<div><slot /></div>' },
BreadcrumbSeparator: { template: '<span>/</span>' },
BreadcrumbPage: { template: '<span><slot /></span>' },
BreadcrumbEllipsis: { template: '<span>...</span>' }
}));
vi.mock('@/components/ui/dropdown-menu', () => ({
DropdownMenu: { template: '<div><slot /></div>' },
DropdownMenuTrigger: { template: '<div><slot /></div>' },
DropdownMenuContent: { template: '<div><slot /></div>' },
DropdownMenuItem: {
emits: ['click'],
template:
'<button data-testid="crumb-dd" @click="$emit(\'click\')"><slot /></button>'
}
}));
vi.mock('@/components/ui/button', () => ({
Button: {
emits: ['click'],
template:
'<button data-testid="btn" @click="$emit(\'click\')"><slot /></button>'
}
}));
vi.mock('@/components/ui/tooltip', () => ({
TooltipWrapper: { template: '<div><slot /></div>' }
}));
vi.mock('@/components/ui/dialog', () => ({ Dialog: { template: '<div><slot /></div>' }, DialogContent: { template: '<div><slot /></div>' } }));
vi.mock('@/components/ui/breadcrumb', () => ({ Breadcrumb: { template: '<div><slot /></div>' }, BreadcrumbList: { template: '<div><slot /></div>' }, BreadcrumbItem: { template: '<div><slot /></div>' }, BreadcrumbLink: { template: '<div><slot /></div>' }, BreadcrumbSeparator: { template: '<span>/</span>' }, BreadcrumbPage: { template: '<span><slot /></span>' }, BreadcrumbEllipsis: { template: '<span>...</span>' } }));
vi.mock('@/components/ui/dropdown-menu', () => ({ DropdownMenu: { template: '<div><slot /></div>' }, DropdownMenuTrigger: { template: '<div><slot /></div>' }, DropdownMenuContent: { template: '<div><slot /></div>' }, DropdownMenuItem: { emits: ['click'], template: '<button data-testid="crumb-dd" @click="$emit(\'click\')"><slot /></button>' } }));
vi.mock('@/components/ui/button', () => ({ Button: { emits: ['click'], template: '<button data-testid="btn" @click="$emit(\'click\')"><slot /></button>' } }));
vi.mock('@/components/ui/tooltip', () => ({ TooltipWrapper: { template: '<div><slot /></div>' } }));
vi.mock('lucide-vue-next', () => ({ ArrowLeft: { template: '<i />' } }));
vi.mock('../AvatarDialog/AvatarDialog.vue', () => ({ default: { template: '<div />' } }));
vi.mock('../GroupDialog/GroupDialog.vue', () => ({ default: { template: '<div />' } }));
vi.mock('../PreviousInstancesDialog/PreviousInstancesInfoDialog.vue', () => ({ default: { template: '<div />' } }));
vi.mock('../PreviousInstancesDialog/PreviousInstancesListDialog.vue', () => ({ default: { template: '<div />' } }));
vi.mock('../UserDialog/UserDialog.vue', () => ({ default: { template: '<div data-testid="user-dialog" />' } }));
vi.mock('../WorldDialog/WorldDialog.vue', () => ({ default: { template: '<div />' } }));
vi.mock('../AvatarDialog/AvatarDialog.vue', () => ({
default: { template: '<div />' }
}));
vi.mock('../GroupDialog/GroupDialog.vue', () => ({
default: { template: '<div />' }
}));
vi.mock('../PreviousInstancesDialog/PreviousInstancesInfoDialog.vue', () => ({
default: { template: '<div />' }
}));
vi.mock('../PreviousInstancesDialog/PreviousInstancesListDialog.vue', () => ({
default: { template: '<div />' }
}));
vi.mock('../UserDialog/UserDialog.vue', () => ({
default: { template: '<div data-testid="user-dialog" />' }
}));
vi.mock('../WorldDialog/WorldDialog.vue', () => ({
default: { template: '<div />' }
}));
import MainDialogContainer from '../MainDialogContainer.vue';

View File

@@ -9,16 +9,47 @@ const mocks = vi.hoisted(() => ({
vi.mock('pinia', async (i) => ({ ...(await i()), storeToRefs: (s) => s }));
vi.mock('vue-i18n', () => ({ useI18n: () => ({ t: (k) => k }) }));
vi.mock('../../../api', () => ({ miscRequest: { sendBoop: (...a) => mocks.sendBoop(...a) }, notificationRequest: { hideNotificationV2: vi.fn() }, queryRequest: { fetch: (...a) => mocks.fetch(...a) } }));
vi.mock('../../../stores', () => ({
useUserStore: () => ({ sendBoopDialog: mocks.boopDialog, isLocalUserVrcPlusSupporter: { value: false } }),
useNotificationStore: () => ({ notificationTable: { value: { data: [] } }, isNotificationExpired: () => false, handleNotificationV2Hide: vi.fn() }),
useGalleryStore: () => ({ showGalleryPage: vi.fn(), refreshEmojiTable: vi.fn(), emojiTable: { value: [] } })
vi.mock('../../../api', () => ({
miscRequest: { sendBoop: (...a) => mocks.sendBoop(...a) },
notificationRequest: { hideNotificationV2: vi.fn() },
queryRequest: { fetch: (...a) => mocks.fetch(...a) }
}));
vi.mock('../../../stores', () => ({
useUserStore: () => ({
sendBoopDialog: mocks.boopDialog,
isLocalUserVrcPlusSupporter: { value: false }
}),
useNotificationStore: () => ({
notificationTable: { value: { data: [] } },
isNotificationExpired: () => false,
handleNotificationV2Hide: vi.fn()
}),
useGalleryStore: () => ({
showGalleryPage: vi.fn(),
refreshEmojiTable: vi.fn(),
emojiTable: { value: [] }
})
}));
vi.mock('../../../shared/constants/photon.js', () => ({
photonEmojis: ['Wave']
}));
vi.mock('@/components/ui/dialog', () => ({
Dialog: { template: '<div><slot /></div>' },
DialogContent: { template: '<div><slot /></div>' },
DialogHeader: { template: '<div><slot /></div>' },
DialogTitle: { template: '<div><slot /></div>' },
DialogFooter: { template: '<div><slot /></div>' }
}));
vi.mock('@/components/ui/button', () => ({
Button: {
emits: ['click'],
template:
'<button data-testid="btn" @click="$emit(\'click\')"><slot /></button>'
}
}));
vi.mock('../../ui/virtual-combobox', () => ({
VirtualCombobox: { template: '<div />' }
}));
vi.mock('../../../shared/constants/photon.js', () => ({ photonEmojis: ['Wave'] }));
vi.mock('@/components/ui/dialog', () => ({ Dialog: { template: '<div><slot /></div>' }, DialogContent: { template: '<div><slot /></div>' }, DialogHeader: { template: '<div><slot /></div>' }, DialogTitle: { template: '<div><slot /></div>' }, DialogFooter: { template: '<div><slot /></div>' } }));
vi.mock('@/components/ui/button', () => ({ Button: { emits: ['click'], template: '<button data-testid="btn" @click="$emit(\'click\')"><slot /></button>' } }));
vi.mock('../../ui/virtual-combobox', () => ({ VirtualCombobox: { template: '<div />' } }));
vi.mock('../../Emoji.vue', () => ({ default: { template: '<div />' } }));
vi.mock('lucide-vue-next', () => ({ Check: { template: '<i />' } }));

View File

@@ -4,7 +4,9 @@ import { mount } from '@vue/test-utils';
const mocks = vi.hoisted(() => ({
close: vi.fn(),
save: vi.fn(),
dialog: { value: { visible: true, maxTableSize: '1000', searchLimit: '100' } }
dialog: {
value: { visible: true, maxTableSize: '1000', searchLimit: '100' }
}
}));
vi.mock('pinia', async (i) => ({ ...(await i()), storeToRefs: (s) => s }));
@@ -20,10 +22,30 @@ vi.mock('../../../stores', () => ({
SEARCH_LIMIT_MAX: 1000
})
}));
vi.mock('@/components/ui/dialog', () => ({ Dialog: { template: '<div><slot /></div>' }, DialogContent: { template: '<div><slot /></div>' }, DialogHeader: { template: '<div><slot /></div>' }, DialogTitle: { template: '<div><slot /></div>' }, DialogDescription: { template: '<div><slot /></div>' }, DialogFooter: { template: '<div><slot /></div>' } }));
vi.mock('@/components/ui/field', () => ({ Field: { template: '<div><slot /></div>' }, FieldGroup: { template: '<div><slot /></div>' }, FieldLabel: { template: '<div><slot /></div>' }, FieldContent: { template: '<div><slot /></div>' } }));
vi.mock('@/components/ui/button', () => ({ Button: { emits: ['click'], template: '<button data-testid="btn" :disabled="$attrs.disabled" @click="$emit(\'click\')"><slot /></button>' } }));
vi.mock('@/components/ui/input-group', () => ({ InputGroupField: { template: '<input />' } }));
vi.mock('@/components/ui/dialog', () => ({
Dialog: { template: '<div><slot /></div>' },
DialogContent: { template: '<div><slot /></div>' },
DialogHeader: { template: '<div><slot /></div>' },
DialogTitle: { template: '<div><slot /></div>' },
DialogDescription: { template: '<div><slot /></div>' },
DialogFooter: { template: '<div><slot /></div>' }
}));
vi.mock('@/components/ui/field', () => ({
Field: { template: '<div><slot /></div>' },
FieldGroup: { template: '<div><slot /></div>' },
FieldLabel: { template: '<div><slot /></div>' },
FieldContent: { template: '<div><slot /></div>' }
}));
vi.mock('@/components/ui/button', () => ({
Button: {
emits: ['click'],
template:
'<button data-testid="btn" :disabled="$attrs.disabled" @click="$emit(\'click\')"><slot /></button>'
}
}));
vi.mock('@/components/ui/input-group', () => ({
InputGroupField: { template: '<input />' }
}));
import TableLimitsDialog from '../TableLimitsDialog.vue';

View File

@@ -326,9 +326,7 @@
const dashboard = await dashboardStore.createDashboard(t('dashboard.default_name'));
const dashboardKey = `${DASHBOARD_NAV_KEY_PREFIX}${dashboard.id}`;
const currentLayout = [...navLayout.value];
const directAccessIdx = currentLayout.findIndex(
(entry) => entry.type === 'item' && entry.key === 'direct-access'
);
const directAccessIdx = currentLayout.findIndex((entry) => entry.type === 'item' && entry.key === 'direct-access');
const newEntry = { type: 'item', key: dashboardKey };
if (directAccessIdx !== -1) {
currentLayout.splice(directAccessIdx, 0, newEntry);

View File

@@ -13,12 +13,15 @@ vi.mock('lucide-vue-next', () => ({
vi.mock('@/components/ui/sidebar', () => ({
SidebarMenuItem: { template: '<div><slot /></div>' },
SidebarMenuButton: { template: '<button data-testid="folder-btn"><slot /></button>' },
SidebarMenuButton: {
template: '<button data-testid="folder-btn"><slot /></button>'
},
SidebarMenuSub: { template: '<div><slot /></div>' },
SidebarMenuSubItem: { template: '<div><slot /></div>' },
SidebarMenuSubButton: {
emits: ['click'],
template: '<button data-testid="submenu-btn" @click="$emit(\'click\')"><slot /></button>'
template:
'<button data-testid="submenu-btn" @click="$emit(\'click\')"><slot /></button>'
}
}));
@@ -32,18 +35,25 @@ vi.mock('@/components/ui/context-menu', () => ({
ContextMenu: { template: '<div><slot /></div>' },
ContextMenuTrigger: { template: '<div><slot /></div>' },
ContextMenuContent: { template: '<div><slot /></div>' },
ContextMenuItem: { emits: ['click'], template: '<button @click="$emit(\'click\')"><slot /></button>' },
ContextMenuItem: {
emits: ['click'],
template: '<button @click="$emit(\'click\')"><slot /></button>'
},
ContextMenuSeparator: { template: '<div />' }
}));
vi.mock('@/components/ui/dropdown-menu', () => ({
DropdownMenu: {
emits: ['update:open'],
template: '<div><button data-testid="dropdown-open" @click="$emit(\'update:open\', true)" /><slot /></div>'
template:
'<div><button data-testid="dropdown-open" @click="$emit(\'update:open\', true)" /><slot /></div>'
},
DropdownMenuTrigger: { template: '<div><slot /></div>' },
DropdownMenuContent: { template: '<div><slot /></div>' },
DropdownMenuItem: { emits: ['select'], template: '<button @click="$emit(\'select\', $event)"><slot /></button>' }
DropdownMenuItem: {
emits: ['select'],
template: '<button @click="$emit(\'select\', $event)"><slot /></button>'
}
}));
import NavMenuFolderItem from '../NavMenuFolderItem.vue';
@@ -53,7 +63,14 @@ const folderItem = {
icon: 'ri-folder-line',
title: 'Folder',
titleIsCustom: true,
children: [{ index: 'feed', label: 'nav_tooltip.feed', icon: 'ri-rss-line', titleIsCustom: false }]
children: [
{
index: 'feed',
label: 'nav_tooltip.feed',
icon: 'ri-rss-line',
titleIsCustom: false
}
]
};
describe('NavMenuFolderItem', () => {

View File

@@ -21,7 +21,8 @@ vi.mock('@/components/ui/sidebar', () => ({
SidebarMenuItem: { template: '<div><slot /></div>' },
SidebarMenuButton: {
emits: ['click'],
template: '<button data-testid="sidebar-menu-btn" @click="$emit(\'click\')"><slot /></button>'
template:
'<button data-testid="sidebar-menu-btn" @click="$emit(\'click\')"><slot /></button>'
}
}));
@@ -31,7 +32,8 @@ vi.mock('@/components/ui/dropdown-menu', () => ({
DropdownMenuContent: { template: '<div><slot /></div>' },
DropdownMenuItem: {
emits: ['click', 'select'],
template: '<button data-testid="dd-item" @click="$emit(\'click\')" @mousedown="$emit(\'select\')"><slot /></button>'
template:
'<button data-testid="dd-item" @click="$emit(\'click\')" @mousedown="$emit(\'select\')"><slot /></button>'
},
DropdownMenuLabel: { template: '<div><slot /></div>' },
DropdownMenuSeparator: { template: '<div />' },
@@ -40,7 +42,8 @@ vi.mock('@/components/ui/dropdown-menu', () => ({
DropdownMenuSubContent: { template: '<div><slot /></div>' },
DropdownMenuCheckboxItem: {
emits: ['select'],
template: '<button data-testid="dd-check" @click="$emit(\'select\')"><slot /></button>'
template:
'<button data-testid="dd-check" @click="$emit(\'select\')"><slot /></button>'
}
}));

View File

@@ -71,7 +71,10 @@ describe('useNavLayout', () => {
triggerNavAction({ routeName: 'dashboard', routeParams: { id: '1' } });
expect(deps.push).toHaveBeenCalledWith({ name: 'dashboard', params: { id: '1' } });
expect(deps.push).toHaveBeenCalledWith({
name: 'dashboard',
params: { id: '1' }
});
});
it('applies custom layout and persists', async () => {

View File

@@ -22,7 +22,11 @@ describe('useNavTheme', () => {
const setTableDensity = vi.fn();
const toggleThemeMode = vi.fn();
const { handleThemeSelect, handleTableDensitySelect, handleThemeToggle } = useNavTheme({
const {
handleThemeSelect,
handleTableDensitySelect,
handleThemeToggle
} = useNavTheme({
t: (key) => key,
appearanceSettingsStore: {
setThemeMode,

View File

@@ -69,17 +69,11 @@
createYearRange({
start:
props?.minValue ??
(toRaw(props.placeholder) ?? props.defaultPlaceholder ?? today(getLocalTimeZone())).cycle(
'year',
-100
),
(toRaw(props.placeholder) ?? props.defaultPlaceholder ?? today(getLocalTimeZone())).cycle('year', -100),
end:
props?.maxValue ??
(toRaw(props.placeholder) ?? props.defaultPlaceholder ?? today(getLocalTimeZone())).cycle(
'year',
10
)
(toRaw(props.placeholder) ?? props.defaultPlaceholder ?? today(getLocalTimeZone())).cycle('year', 10)
})
);
});

View File

@@ -27,13 +27,7 @@
as: { type: null, required: false },
class: { type: null, required: false }
});
const emits = defineEmits([
'escapeKeyDown',
'pointerDownOutside',
'focusOutside',
'interactOutside',
'closeAutoFocus'
]);
const emits = defineEmits(['escapeKeyDown', 'pointerDownOutside', 'focusOutside', 'interactOutside', 'closeAutoFocus']);
const delegatedProps = reactiveOmit(props, 'class');

View File

@@ -31,13 +31,7 @@
as: { type: null, required: false },
class: { type: null, required: false }
});
const emits = defineEmits([
'escapeKeyDown',
'pointerDownOutside',
'focusOutside',
'interactOutside',
'closeAutoFocus'
]);
const emits = defineEmits(['escapeKeyDown', 'pointerDownOutside', 'focusOutside', 'interactOutside', 'closeAutoFocus']);
const delegatedProps = reactiveOmit(props, 'class');

View File

@@ -50,12 +50,7 @@
class: { type: null, required: false }
});
const emits = defineEmits([
'update:modelValue',
'update:validModelValue',
'update:placeholder',
'update:startValue'
]);
const emits = defineEmits(['update:modelValue', 'update:validModelValue', 'update:placeholder', 'update:startValue']);
const delegatedProps = reactiveOmit(props, 'class');

View File

@@ -126,9 +126,7 @@
const groupItems = Array.isArray(group?.items) ? group.items : [];
const filteredItems = normalizedSearch
? groupItems.filter((item) =>
(item.search ?? item.label ?? '').toLowerCase().includes(normalizedSearch)
)
? groupItems.filter((item) => (item.search ?? item.label ?? '').toLowerCase().includes(normalizedSearch))
: groupItems;
if (!filteredItems.length) continue;