This commit is contained in:
pa
2026-03-12 16:57:51 +09:00
parent c72209f56d
commit 08e160ff69
39 changed files with 3407 additions and 0 deletions

View File

@@ -0,0 +1,108 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { mount } from '@vue/test-utils';
import { nextTick } from 'vue';
vi.mock('@/components/ui/tooltip', () => ({
Tooltip: { template: '<div><slot /></div>' },
TooltipTrigger: { template: '<div><slot /></div>' },
TooltipContent: { template: '<div><slot /></div>' }
}));
vi.mock('@/components/ui/button', () => ({
Button: {
emits: ['click'],
template: '<button data-testid="back-btn" @click="$emit(\'click\', $event)"><slot /></button>'
}
}));
vi.mock('lucide-vue-next', () => ({
ArrowUp: { template: '<i />' }
}));
import BackToTop from '../BackToTop.vue';
function setScrollY(value) {
Object.defineProperty(window, 'scrollY', {
configurable: true,
value
});
}
describe('BackToTop.vue', () => {
beforeEach(() => {
setScrollY(0);
vi.spyOn(window, 'scrollTo').mockImplementation(() => {});
});
afterEach(() => {
vi.restoreAllMocks();
});
it('shows button after scroll threshold and scrolls window to top', async () => {
const wrapper = mount(BackToTop, {
props: {
visibilityHeight: 100,
teleport: false,
tooltip: false
}
});
expect(wrapper.find('[data-testid="back-btn"]').exists()).toBe(false);
setScrollY(120);
window.dispatchEvent(new Event('scroll'));
await nextTick();
const btn = wrapper.find('[data-testid="back-btn"]');
expect(btn.exists()).toBe(true);
await btn.trigger('click');
expect(window.scrollTo).toHaveBeenCalledWith({ top: 0, behavior: 'smooth' });
});
it('uses virtualizer scrollToIndex when provided', async () => {
const scrollToIndex = vi.fn();
const wrapper = mount(BackToTop, {
props: {
visibilityHeight: 0,
teleport: false,
tooltip: false,
virtualizer: { scrollToIndex }
}
});
window.dispatchEvent(new Event('scroll'));
await nextTick();
const btn = wrapper.get('[data-testid="back-btn"]');
await btn.trigger('click');
expect(scrollToIndex).toHaveBeenCalledWith(0, { align: 'start', behavior: 'auto' });
expect(window.scrollTo).not.toHaveBeenCalled();
});
it('scrolls target element to top with auto behavior', async () => {
const target = document.createElement('div');
target.scrollTop = 200;
target.scrollTo = vi.fn();
const wrapper = mount(BackToTop, {
props: {
target,
behavior: 'auto',
visibilityHeight: 100,
teleport: false,
tooltip: false
}
});
target.dispatchEvent(new Event('scroll'));
await nextTick();
const btn = wrapper.get('[data-testid="back-btn"]');
await btn.trigger('click');
expect(target.scrollTo).toHaveBeenCalledWith({ top: 0, behavior: 'auto' });
});
});

View File

@@ -0,0 +1,76 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { mount } from '@vue/test-utils';
import { nextTick } from 'vue';
const mocks = vi.hoisted(() => ({
setInterval: vi.fn(() => 42),
clearInterval: vi.fn(),
timeToText: vi.fn((ms) => `${Math.floor(ms / 1000)}s`)
}));
vi.mock('worker-timers', () => ({
setInterval: (...args) => mocks.setInterval(...args),
clearInterval: (...args) => mocks.clearInterval(...args)
}));
vi.mock('../../shared/utils', () => ({
timeToText: (...args) => mocks.timeToText(...args)
}));
import CountdownTimer from '../CountdownTimer.vue';
describe('CountdownTimer.vue', () => {
beforeEach(() => {
mocks.setInterval.mockClear();
mocks.clearInterval.mockClear();
mocks.timeToText.mockClear();
vi.spyOn(Date, 'now').mockReturnValue(new Date('2026-01-01T00:00:00.000Z').getTime());
});
afterEach(() => {
vi.restoreAllMocks();
});
it('renders remaining time on mount', async () => {
const wrapper = mount(CountdownTimer, {
props: {
datetime: '2025-12-31T23:30:00.000Z',
hours: 1
}
});
await nextTick();
expect(mocks.timeToText).toHaveBeenCalled();
expect(wrapper.text()).toContain('1800s');
});
it('renders dash when countdown expired', async () => {
const wrapper = mount(CountdownTimer, {
props: {
datetime: '2025-12-31T22:00:00.000Z',
hours: 1
}
});
await nextTick();
expect(wrapper.text()).toBe('-');
await wrapper.setProps({ datetime: '2025-12-31T23:59:30.000Z', hours: 0 });
await nextTick();
expect(wrapper.text()).toBe('-');
});
it('clears interval on unmount', () => {
const wrapper = mount(CountdownTimer, {
props: {
datetime: '2025-12-31T23:30:00.000Z',
hours: 1
}
});
wrapper.unmount();
expect(mocks.setInterval).toHaveBeenCalled();
expect(mocks.clearInterval).toHaveBeenCalledWith(42);
});
});

View File

@@ -0,0 +1,35 @@
import { describe, expect, it, vi } from 'vitest';
import { mount } from '@vue/test-utils';
vi.mock('vue-i18n', () => ({
useI18n: () => ({
t: (key) => key
})
}));
vi.mock('lucide-vue-next', () => ({
MessageSquareWarning: { template: '<i data-testid="warn-icon" />' }
}));
import DeprecationAlert from '../DeprecationAlert.vue';
describe('DeprecationAlert.vue', () => {
it('renders relocated title and feature name', () => {
const wrapper = mount(DeprecationAlert, {
props: {
featureName: 'InstanceActionBar'
},
global: {
stubs: {
i18nT: {
template: '<span data-testid="i18n-t"><slot name="feature" /></span>'
}
}
}
});
expect(wrapper.text()).toContain('common.feature_relocated.title');
expect(wrapper.text()).toContain('InstanceActionBar');
expect(wrapper.find('[data-testid="warn-icon"]').exists()).toBe(true);
});
});

View File

@@ -0,0 +1,71 @@
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' } })),
showUserDialog: vi.fn()
}));
vi.mock('../../api', () => ({
queryRequest: {
fetch: (...args) => mocks.fetch(...args)
}
}));
vi.mock('../../coordinators/userCoordinator', () => ({
showUserDialog: (...args) => mocks.showUserDialog(...args)
}));
import DisplayName from '../DisplayName.vue';
async function flush() {
await Promise.resolve();
await Promise.resolve();
}
describe('DisplayName.vue', () => {
beforeEach(() => {
mocks.fetch.mockClear();
mocks.showUserDialog.mockClear();
});
it('uses hint directly and skips user query', async () => {
const wrapper = mount(DisplayName, {
props: {
userid: 'usr_1',
hint: 'Hint Name'
}
});
await flush();
expect(wrapper.text()).toBe('Hint Name');
expect(mocks.fetch).not.toHaveBeenCalled();
});
it('fetches and renders display name when hint is missing', async () => {
const wrapper = mount(DisplayName, {
props: {
userid: 'usr_2'
}
});
await flush();
expect(mocks.fetch).toHaveBeenCalledWith('user.dialog', { userId: 'usr_2' });
expect(wrapper.text()).toBe('Fetched User');
});
it('opens user dialog when clicked', async () => {
const wrapper = mount(DisplayName, {
props: {
userid: 'usr_3',
hint: 'Clickable User'
}
});
await wrapper.trigger('click');
expect(mocks.showUserDialog).toHaveBeenCalledWith('usr_3');
});
});

View File

@@ -0,0 +1,110 @@
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { mount } from '@vue/test-utils';
const mocks = vi.hoisted(() => ({
emojiTable: [],
getCachedEmoji: vi.fn(async () => ({
frames: null,
framesOverTime: null,
loopStyle: null,
versions: []
})),
extractFileId: vi.fn(() => 'file_1'),
generateEmojiStyle: vi.fn(() => 'background: red;')
}));
vi.mock('../../stores', () => ({
useGalleryStore: () => ({
getCachedEmoji: (...args) => mocks.getCachedEmoji(...args),
emojiTable: mocks.emojiTable
})
}));
vi.mock('../../shared/utils', () => ({
extractFileId: (...args) => mocks.extractFileId(...args),
generateEmojiStyle: (...args) => mocks.generateEmojiStyle(...args)
}));
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>' }
}));
vi.mock('lucide-vue-next', () => ({
ImageOff: { template: '<i data-testid="image-off" />' }
}));
import Emoji from '../Emoji.vue';
async function flush() {
await Promise.resolve();
await Promise.resolve();
}
describe('Emoji.vue', () => {
beforeEach(() => {
mocks.emojiTable.length = 0;
mocks.getCachedEmoji.mockClear();
mocks.extractFileId.mockReturnValue('file_1');
mocks.generateEmojiStyle.mockClear();
});
it('renders animated div when emoji has frames in table', async () => {
mocks.emojiTable.push({
id: 'file_1',
frames: 4,
framesOverTime: 1,
loopStyle: 0,
versions: []
});
const wrapper = mount(Emoji, {
props: {
imageUrl: 'https://example.com/file_1.png',
size: 64
}
});
await flush();
const animated = wrapper.find('.avatar');
expect(animated.exists()).toBe(true);
expect(mocks.generateEmojiStyle).toHaveBeenCalled();
expect(animated.attributes('style')).toContain('background: red;');
expect(wrapper.find('[data-testid="avatar"]').exists()).toBe(false);
});
it('falls back to Avatar image when no frames', async () => {
const wrapper = mount(Emoji, {
props: {
imageUrl: 'https://example.com/file_2.png',
size: 48
}
});
await flush();
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);
});
it('updates when imageUrl changes', async () => {
const wrapper = mount(Emoji, {
props: {
imageUrl: 'https://example.com/a.png'
}
});
await flush();
mocks.extractFileId.mockReturnValue('file_2');
await wrapper.setProps({ imageUrl: 'https://example.com/b.png' });
await flush();
expect(mocks.getCachedEmoji).toHaveBeenCalledWith('file_1');
expect(mocks.getCachedEmoji).toHaveBeenCalledWith('file_2');
});
});

View File

@@ -0,0 +1,40 @@
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' } }
}));
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('@/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('lucide-vue-next', () => ({
Copy: { template: '<i />' },
Download: { template: '<i />' },
RefreshCcw: { template: '<i />' },
RotateCcw: { template: '<i />' },
RotateCw: { template: '<i />' },
X: { template: '<i />' },
ZoomIn: { template: '<i />' },
ZoomOut: { template: '<i />' }
}));
import FullscreenImagePreview from '../FullscreenImagePreview.vue';
describe('FullscreenImagePreview.vue', () => {
it('closes dialog when close button clicked', async () => {
const wrapper = mount(FullscreenImagePreview);
await wrapper.get('button[aria-label="Close"]').trigger('click');
expect(mocks.dialog.value.visible).toBe(false);
});
});

View File

@@ -0,0 +1,63 @@
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { mount } from '@vue/test-utils';
const mocks = vi.hoisted(() => ({
selectResult: vi.fn(),
userImage: vi.fn(() => 'https://example.com/u.png'),
isOpen: { value: true },
query: { value: '' },
friendResults: { value: [] },
ownAvatarResults: { value: [] },
favoriteAvatarResults: { value: [] },
ownWorldResults: { value: [] },
favoriteWorldResults: { value: [] },
ownGroupResults: { value: [] },
joinedGroupResults: { value: [] },
hasResults: { value: false }
}));
vi.mock('pinia', async (i) => ({ ...(await i()), storeToRefs: (s) => s }));
vi.mock('vue-i18n', () => ({ useI18n: () => ({ t: (k) => k }) }));
vi.mock('../../stores/globalSearch', () => ({
useGlobalSearchStore: () => ({
isOpen: mocks.isOpen,
query: mocks.query,
friendResults: mocks.friendResults,
ownAvatarResults: mocks.ownAvatarResults,
favoriteAvatarResults: mocks.favoriteAvatarResults,
ownWorldResults: mocks.ownWorldResults,
favoriteWorldResults: mocks.favoriteWorldResults,
ownGroupResults: mocks.ownGroupResults,
joinedGroupResults: mocks.joinedGroupResults,
hasResults: mocks.hasResults,
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('@/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>' }
}));
vi.mock('lucide-vue-next', () => ({ Globe: { template: '<i />' }, Image: { template: '<i />' }, Users: { template: '<i />' } }));
import GlobalSearchDialog from '../GlobalSearchDialog.vue';
describe('GlobalSearchDialog.vue', () => {
beforeEach(() => {
mocks.selectResult.mockClear();
mocks.query.value = '';
mocks.hasResults.value = false;
mocks.friendResults.value = [];
});
it('renders search dialog structure', () => {
const wrapper = mount(GlobalSearchDialog);
expect(wrapper.text()).toContain('side_panel.search_placeholder');
expect(wrapper.find('[data-testid="sync"]').exists()).toBe(true);
});
});

View File

@@ -0,0 +1,55 @@
import { describe, expect, it, vi } from 'vitest';
import { mount } from '@vue/test-utils';
const mocks = vi.hoisted(() => ({
setQuery: vi.fn(),
filterStateRaw: {
search: '',
filtered: {
items: new Map(),
count: 0,
groups: new Set()
}
},
filterState: null,
allItemsEntries: [['a', {}], ['b', {}]],
allGroupsEntries: [['g1', {}]]
}));
vi.mock('@/components/ui/command', async () => {
const { reactive, ref } = await import('vue');
const filterState = reactive(mocks.filterStateRaw);
mocks.filterState = filterState;
const allItems = ref(new Map(mocks.allItemsEntries));
const allGroups = ref(new Map(mocks.allGroupsEntries));
return {
useCommand: () => ({
filterState,
allItems,
allGroups
})
};
});
vi.mock('../../stores/globalSearch', () => ({
useGlobalSearchStore: () => ({
setQuery: (...args) => mocks.setQuery(...args)
})
}));
import GlobalSearchSync from '../GlobalSearchSync.vue';
describe('GlobalSearchSync.vue', () => {
it('syncs query and keeps hint groups/items visible when query length < 2', async () => {
mount(GlobalSearchSync);
mocks.filterState.search = 'a';
await Promise.resolve();
await Promise.resolve();
expect(mocks.setQuery).toHaveBeenCalledWith('a');
expect(mocks.filterState.filtered.count).toBe(2);
expect(mocks.filterState.filtered.items.get('a')).toBe(1);
expect(mocks.filterState.filtered.groups.has('g1')).toBe(true);
});
});

View File

@@ -0,0 +1,329 @@
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { mount } from '@vue/test-utils';
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' })),
hasGroupPermission: vi.fn(() => false),
formatDateFilter: vi.fn(() => 'formatted-date'),
selfInvite: vi.fn(() => Promise.resolve({})),
closeInstance: vi.fn(() => Promise.resolve({ json: { id: 'inst_closed' } })),
showUserDialog: vi.fn(),
toastSuccess: vi.fn(),
applyInstance: vi.fn(),
showLaunchDialog: vi.fn(),
tryOpenInstanceInVrc: vi.fn(),
modalConfirm: vi.fn(() => Promise.resolve({ ok: true })),
instanceJoinHistory: { value: new Map() },
canOpenInstanceInGame: false,
isOpeningInstance: false,
lastLocation: { location: 'wrld_here:111', playerList: new Set(['u1', 'u2']) },
currentUser: { id: 'usr_me' },
cachedGroups: new Map()
}));
vi.mock('pinia', async (importOriginal) => {
const actual = await importOriginal();
return {
...actual,
storeToRefs: (store) =>
Object.fromEntries(
Object.entries(store).map(([key, value]) => [
key,
key === 'instanceJoinHistory' ? value : value?.value ?? value
])
)
};
});
vi.mock('vue-i18n', () => ({
useI18n: () => ({
t: (key) => key
})
}));
vi.mock('vue-sonner', () => ({
toast: {
success: (...args) => mocks.toastSuccess(...args)
}
}));
vi.mock('../../stores', () => ({
useLocationStore: () => ({
lastLocation: mocks.lastLocation
}),
useUserStore: () => ({
currentUser: mocks.currentUser
}),
useGroupStore: () => ({
cachedGroups: mocks.cachedGroups
}),
useInstanceStore: () => ({
instanceJoinHistory: mocks.instanceJoinHistory,
applyInstance: (...args) => mocks.applyInstance(...args)
}),
useModalStore: () => ({
confirm: (...args) => mocks.modalConfirm(...args)
}),
useLaunchStore: () => ({
isOpeningInstance: mocks.isOpeningInstance,
showLaunchDialog: (...args) => mocks.showLaunchDialog(...args),
tryOpenInstanceInVrc: (...args) => mocks.tryOpenInstanceInVrc(...args)
}),
useInviteStore: () => ({
canOpenInstanceInGame: mocks.canOpenInstanceInGame
})
}));
vi.mock('../../composables/useInviteChecks', () => ({
useInviteChecks: () => ({
checkCanInviteSelf: (...args) => mocks.checkCanInviteSelf(...args)
})
}));
vi.mock('../../shared/utils', () => ({
parseLocation: (...args) => mocks.parseLocation(...args),
hasGroupPermission: (...args) => mocks.hasGroupPermission(...args),
formatDateFilter: (...args) => mocks.formatDateFilter(...args)
}));
vi.mock('../../api', () => ({
instanceRequest: {
selfInvite: (...args) => mocks.selfInvite(...args)
},
miscRequest: {
closeInstance: (...args) => mocks.closeInstance(...args)
}
}));
vi.mock('../../coordinators/userCoordinator', () => ({
showUserDialog: (...args) => mocks.showUserDialog(...args)
}));
vi.mock('@/components/ui/button', () => ({
Button: {
emits: ['click'],
template: '<button data-testid="btn" @click="$emit(\'click\', $event)"><slot /></button>'
}
}));
vi.mock('lucide-vue-next', () => ({
History: { template: '<i data-testid="icon-history" />' },
Loader2: { template: '<i data-testid="icon-loader" />' },
LogIn: { template: '<i data-testid="icon-login" />' },
Mail: { template: '<i data-testid="icon-mail" />' },
MapPin: { template: '<i data-testid="icon-map" />' },
RefreshCw: { template: '<i data-testid="icon-refresh" />' },
UsersRound: { template: '<i data-testid="icon-users" />' }
}));
import InstanceActionBar from '../InstanceActionBar.vue';
function mountBar(props = {}) {
return mount(InstanceActionBar, {
props: {
location: 'wrld_base:111',
launchLocation: '',
inviteLocation: '',
lastJoinLocation: '',
instanceLocation: '',
shortname: 'sn',
instance: {
ownerId: 'usr_me',
capacity: 16,
userCount: 4,
hasCapacityForYou: true,
platforms: { standalonewindows: 1, android: 2, ios: 0 },
users: [{ id: 'usr_a', displayName: 'Alice' }],
gameServerVersion: 123,
$disabledContentSettings: []
},
friendcount: 2,
currentlocation: '',
showLaunch: true,
showInvite: true,
showRefresh: true,
showHistory: true,
showLastJoin: true,
showInstanceInfo: true,
refreshTooltip: 'refresh',
historyTooltip: 'history',
onRefresh: vi.fn(),
onHistory: vi.fn(),
...props
},
global: {
stubs: {
TooltipWrapper: {
props: ['content'],
template: '<div><slot /><slot name="content" /><span v-if="content">{{ content }}</span></div>'
},
Timer: {
props: ['epoch'],
template: '<span data-testid="timer">{{ epoch }}</span>'
}
}
}
});
}
describe('InstanceActionBar.vue', () => {
beforeEach(() => {
mocks.checkCanInviteSelf.mockReturnValue(true);
mocks.parseLocation.mockReturnValue({
isRealInstance: true,
instanceId: 'inst_1',
worldId: 'wrld_1',
tag: 'wrld_1:inst_1'
});
mocks.hasGroupPermission.mockReturnValue(false);
mocks.selfInvite.mockClear();
mocks.closeInstance.mockClear();
mocks.showUserDialog.mockClear();
mocks.toastSuccess.mockClear();
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.canOpenInstanceInGame = false;
mocks.isOpeningInstance = false;
mocks.lastLocation.location = 'wrld_here:111';
mocks.lastLocation.playerList = new Set(['u1', 'u2']);
mocks.currentUser.id = 'usr_me';
mocks.cachedGroups = new Map();
});
it('renders launch and invite buttons when invite-self is allowed', () => {
const wrapper = mountBar({
showRefresh: false,
showHistory: false,
showInstanceInfo: false
});
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');
});
it('launch button opens launch dialog with resolved launchLocation', async () => {
const wrapper = mountBar({
launchLocation: 'wrld_launch:222',
showRefresh: false,
showHistory: false,
showInstanceInfo: false
});
const launchBtn = wrapper.findAll('[data-testid="btn"]')[0];
expect(launchBtn).toBeTruthy();
await launchBtn.trigger('click');
expect(mocks.showLaunchDialog).toHaveBeenCalledWith('wrld_launch:222');
});
it('invite button sends self-invite when canOpenInstanceInGame is false', async () => {
const wrapper = mountBar({
inviteLocation: 'wrld_invite:333',
showRefresh: false,
showHistory: false,
showInstanceInfo: false
});
const inviteBtn = wrapper.findAll('[data-testid="btn"]')[1];
expect(inviteBtn).toBeTruthy();
await inviteBtn.trigger('click');
await Promise.resolve();
expect(mocks.selfInvite).toHaveBeenCalledWith({
instanceId: 'inst_1',
worldId: 'wrld_1',
shortName: 'sn'
});
expect(mocks.toastSuccess).toHaveBeenCalledWith('message.invite.self_sent');
});
it('invite button opens in VRChat when canOpenInstanceInGame is true', async () => {
mocks.canOpenInstanceInGame = true;
const wrapper = mountBar({
inviteLocation: 'wrld_invite:333',
showRefresh: false,
showHistory: false,
showInstanceInfo: false
});
const inviteBtn = wrapper.findAll('[data-testid="btn"]')[1];
expect(inviteBtn).toBeTruthy();
await inviteBtn.trigger('click');
expect(mocks.tryOpenInstanceInVrc).toHaveBeenCalledWith('wrld_1:inst_1', 'sn');
expect(mocks.selfInvite).not.toHaveBeenCalled();
});
it('refresh/history callbacks run when buttons clicked', async () => {
const onRefresh = vi.fn();
const onHistory = vi.fn();
const wrapper = mountBar({
onRefresh,
onHistory,
showLaunch: false,
showInvite: false,
showInstanceInfo: false
});
const buttons = wrapper.findAll('[data-testid="btn"]');
expect(buttons).toHaveLength(2);
await buttons[0].trigger('click');
await buttons[1].trigger('click');
expect(onRefresh).toHaveBeenCalledTimes(1);
expect(onHistory).toHaveBeenCalledTimes(1);
});
it('shows last-join timer and friend count', () => {
const wrapper = mountBar({ friendcount: 5 });
expect(wrapper.find('[data-testid="timer"]').exists()).toBe(true);
expect(wrapper.text()).toContain('5');
});
it('close instance flow confirms, calls api, applies instance and toasts', async () => {
const wrapper = mountBar({
instanceLocation: 'wrld_close:444',
instance: {
ownerId: 'usr_me',
capacity: 16,
userCount: 4,
hasCapacityForYou: true,
platforms: { standalonewindows: 1, android: 2, ios: 0 },
users: [],
gameServerVersion: 123,
$disabledContentSettings: []
}
});
const closeBtn = wrapper.findAll('button').find((btn) => btn.text().includes('dialog.user.info.close_instance'));
expect(closeBtn).toBeTruthy();
await closeBtn.trigger('click');
await Promise.resolve();
await Promise.resolve();
await nextTick();
expect(mocks.modalConfirm).toHaveBeenCalled();
expect(mocks.closeInstance).toHaveBeenCalledWith({ location: 'wrld_close:444', hardClose: false });
expect(mocks.applyInstance).toHaveBeenCalledWith({ id: 'inst_closed' });
expect(mocks.toastSuccess).toHaveBeenCalledWith('message.instance.closed');
});
it('hides launch and invite buttons when invite-self is not allowed', () => {
mocks.checkCanInviteSelf.mockReturnValue(false);
const wrapper = mountBar({
showRefresh: false,
showHistory: false,
showInstanceInfo: false
});
expect(wrapper.findAll('[data-testid="btn"]')).toHaveLength(0);
});
});

View File

@@ -0,0 +1,172 @@
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { mount } from '@vue/test-utils';
const mocks = vi.hoisted(() => ({
cachedInstances: new Map(),
lastInstanceApplied: { value: '' },
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' }))
}));
vi.mock('pinia', async (importOriginal) => {
const actual = await importOriginal();
return {
...actual,
storeToRefs: (store) => store
};
});
vi.mock('vue-i18n', () => ({
useI18n: () => ({
t: (key) => key
})
}));
vi.mock('../../stores', () => ({
useInstanceStore: () => ({
cachedInstances: mocks.cachedInstances,
lastInstanceApplied: mocks.lastInstanceApplied
}),
useLaunchStore: () => ({
showLaunchDialog: (...args) => mocks.showLaunchDialog(...args)
}),
useGroupStore: () => ({})
}));
vi.mock('../../coordinators/groupCoordinator', () => ({
showGroupDialog: (...args) => mocks.showGroupDialog(...args)
}));
vi.mock('../../shared/constants', () => ({
accessTypeLocaleKeyMap: {
friends: 'dialog.world.instance.friends',
groupPublic: 'dialog.world.instance.group_public',
group: 'dialog.world.instance.group'
}
}));
vi.mock('../../shared/utils', () => ({
getGroupName: (...args) => mocks.getGroupName(...args),
parseLocation: (...args) => mocks.parseLocation(...args)
}));
vi.mock('lucide-vue-next', () => ({
AlertTriangle: { template: '<i data-testid="alert" />' },
Lock: { template: '<i data-testid="lock" />' },
Unlock: { template: '<i data-testid="unlock" />' }
}));
import LocationWorld from '../LocationWorld.vue';
async function flush() {
await Promise.resolve();
await Promise.resolve();
}
function mountComponent(props = {}) {
return mount(LocationWorld, {
props: {
locationobject: {
tag: 'wrld_1:inst_1',
accessTypeName: 'friends',
strict: false,
shortName: 'short-1',
userId: 'usr_owner',
region: 'eu',
instanceName: 'Instance Name',
groupId: 'grp_1'
},
currentuserid: 'usr_owner',
worlddialogshortname: '',
grouphint: '',
...props
},
global: {
stubs: {
TooltipWrapper: {
props: ['content'],
template: '<span><slot /></span>'
}
}
}
});
}
describe('LocationWorld.vue', () => {
beforeEach(() => {
mocks.cachedInstances = new Map();
mocks.lastInstanceApplied.value = '';
mocks.showLaunchDialog.mockClear();
mocks.showGroupDialog.mockClear();
mocks.getGroupName.mockClear();
mocks.parseLocation.mockClear();
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.find('.flags.eu').exists()).toBe(true);
});
it('marks unlocked for owner and opens launch dialog on click', async () => {
const wrapper = mountComponent();
expect(wrapper.find('[data-testid="unlock"]').exists()).toBe(true);
await wrapper.findAll('.cursor-pointer')[0].trigger('click');
expect(mocks.showLaunchDialog).toHaveBeenCalledWith('wrld_1:inst_1', 'short-1');
});
it('shows group hint and opens group dialog', async () => {
const wrapper = mountComponent({ grouphint: 'Hint Group' });
expect(wrapper.text()).toContain('(Hint Group)');
await wrapper.findAll('.cursor-pointer')[1].trigger('click');
expect(mocks.showGroupDialog).toHaveBeenCalledWith('grp_1');
});
it('loads group name asynchronously when no hint', async () => {
const wrapper = mountComponent({ grouphint: '' });
await flush();
expect(mocks.getGroupName).toHaveBeenCalledWith('grp_1');
expect(wrapper.text()).toContain('(Fetched Group)');
});
it('shows closed indicator and strict lock from instance cache', () => {
mocks.cachedInstances = new Map([
[
'wrld_1:inst_1',
{
displayName: 'Resolved Name',
closedAt: '2026-01-01T00:00:00.000Z'
}
]
]);
const wrapper = mountComponent({
locationobject: {
tag: 'wrld_1:inst_1',
accessTypeName: 'friends',
strict: true,
shortName: 'short-1',
userId: 'usr_other',
region: 'us',
instanceName: 'Fallback Name',
groupId: 'grp_1'
},
currentuserid: 'usr_me'
});
expect(wrapper.text()).toContain('#Resolved Name');
expect(wrapper.find('[data-testid="alert"]').exists()).toBe(true);
expect(wrapper.find('[data-testid="lock"]').exists()).toBe(true);
});
});

View File

@@ -0,0 +1,309 @@
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { mount } from '@vue/test-utils';
const mocks = vi.hoisted(() => ({
routerPush: vi.fn(() => Promise.resolve()),
directAccessPaste: vi.fn(),
logout: vi.fn(),
clearAllNotifications: vi.fn(),
toggleThemeMode: vi.fn(),
toggleNavCollapsed: vi.fn(),
initThemeColor: vi.fn(() => Promise.resolve()),
applyThemeColor: vi.fn(() => Promise.resolve()),
openExternalLink: vi.fn(),
getString: vi.fn(() => Promise.resolve(null)),
setString: vi.fn(() => Promise.resolve()),
showVRCXUpdateDialog: vi.fn(),
showChangeLogDialog: vi.fn(),
notifiedMenus: { value: [] },
pendingVRCXUpdate: { value: false },
pendingVRCXInstall: { value: false },
appVersion: { value: 'VRCX 2026.01.01' },
themeMode: { value: 'system' },
tableDensity: { value: 'standard' },
isDarkMode: { value: false },
isNavCollapsed: { value: false },
currentRoute: { value: { name: 'unknown', meta: {} } }
}));
vi.mock('pinia', async (importOriginal) => {
const actual = await importOriginal();
return {
...actual,
storeToRefs: (store) => store
};
});
vi.mock('vue-i18n', () => ({
useI18n: () => ({
t: (key) => key,
locale: { value: 'en' }
})
}));
vi.mock('../../views/Feed/Feed.vue', () => ({
default: { template: '<div />' }
}));
vi.mock('../../views/Feed/columns.jsx', () => ({
columns: []
}));
vi.mock('../../plugins/router', () => ({
router: {
beforeEach: vi.fn(),
push: vi.fn(),
replace: vi.fn(),
currentRoute: mocks.currentRoute,
isReady: vi.fn().mockResolvedValue(true)
},
initRouter: vi.fn()
}));
vi.mock('../../plugins/interopApi', () => ({
initInteropApi: vi.fn()
}));
vi.mock('../../services/database', () => ({
database: new Proxy(
{},
{
get: (_target, prop) => {
if (prop === '__esModule') return false;
return vi.fn().mockResolvedValue(null);
}
}
)
}));
vi.mock('../../services/jsonStorage', () => ({
default: vi.fn()
}));
vi.mock('../../services/watchState', () => ({
watchState: { isLoggedIn: false }
}));
vi.mock('vue-router', () => ({
useRouter: () => ({
push: (...args) => mocks.routerPush(...args),
currentRoute: mocks.currentRoute
})
}));
vi.mock('../../stores', () => ({
useVRCXUpdaterStore: () => ({
pendingVRCXUpdate: mocks.pendingVRCXUpdate,
pendingVRCXInstall: mocks.pendingVRCXInstall,
appVersion: mocks.appVersion,
showVRCXUpdateDialog: (...args) => mocks.showVRCXUpdateDialog(...args),
showChangeLogDialog: (...args) => mocks.showChangeLogDialog(...args)
}),
useUiStore: () => ({
notifiedMenus: mocks.notifiedMenus,
clearAllNotifications: (...args) => mocks.clearAllNotifications(...args)
}),
useSearchStore: () => ({
directAccessPaste: (...args) => mocks.directAccessPaste(...args)
}),
useAuthStore: () => ({
logout: (...args) => mocks.logout(...args)
}),
useAppearanceSettingsStore: () => ({
themeMode: mocks.themeMode,
tableDensity: mocks.tableDensity,
isDarkMode: mocks.isDarkMode,
isNavCollapsed: mocks.isNavCollapsed,
setThemeMode: vi.fn(),
toggleThemeMode: (...args) => mocks.toggleThemeMode(...args),
setTableDensity: vi.fn(),
toggleNavCollapsed: (...args) => mocks.toggleNavCollapsed(...args)
})
}));
vi.mock('../../services/config', () => ({
default: {
getString: (...args) => mocks.getString(...args),
setString: (...args) => mocks.setString(...args)
}
}));
vi.mock('../../shared/constants', () => ({
THEME_CONFIG: {
system: { name: 'System' },
light: { name: 'Light' },
dark: { name: 'Dark' }
},
links: {
github: 'https://github.com/vrcx-team/VRCX'
},
navDefinitions: [
{
key: 'feed',
routeName: 'feed',
labelKey: 'nav_tooltip.feed',
tooltip: 'nav_tooltip.feed',
icon: 'ri-feed-line'
},
{
key: 'direct-access',
action: 'direct-access',
labelKey: 'nav_tooltip.direct_access',
tooltip: 'nav_tooltip.direct_access',
icon: 'ri-door-open-line'
}
]
}));
vi.mock('./navMenuUtils', () => ({
getFirstNavRoute: () => 'feed',
isEntryNotified: () => false,
normalizeHiddenKeys: (keys) => keys || [],
sanitizeLayout: (layout) => layout
}));
vi.mock('../../shared/utils', () => ({
openExternalLink: (...args) => mocks.openExternalLink(...args)
}));
vi.mock('@/shared/utils/base/ui', () => ({
useThemeColor: () => ({
themeColors: { value: [{ key: 'blue', label: 'Blue', swatch: '#00f' }] },
currentThemeColor: { value: 'blue' },
isApplyingThemeColor: { value: false },
applyThemeColor: (...args) => mocks.applyThemeColor(...args),
initThemeColor: (...args) => mocks.initThemeColor(...args)
})
}));
vi.mock('@/components/ui/sidebar', () => ({
Sidebar: { template: '<div><slot /></div>' },
SidebarContent: { template: '<div><slot /></div>' },
SidebarFooter: { template: '<div><slot /></div>' },
SidebarGroup: { template: '<div><slot /></div>' },
SidebarGroupContent: { template: '<div><slot /></div>' },
SidebarMenu: { template: '<div><slot /></div>' },
SidebarMenuItem: { template: '<div><slot /></div>' },
SidebarMenuSub: { template: '<div><slot /></div>' },
SidebarMenuSubItem: { template: '<div><slot /></div>' },
SidebarMenuButton: {
emits: ['click'],
template: '<button data-testid="menu-btn" @click="$emit(\'click\', $event)"><slot /></button>'
},
SidebarMenuSubButton: {
emits: ['click'],
template: '<button data-testid="submenu-btn" @click="$emit(\'click\', $event)"><slot /></button>'
}
}));
vi.mock('@/components/ui/dropdown-menu', () => ({
DropdownMenu: { template: '<div><slot /></div>' },
DropdownMenuTrigger: { template: '<div><slot /></div>' },
DropdownMenuContent: { template: '<div><slot /></div>' },
DropdownMenuItem: { emits: ['click', 'select'], template: '<button data-testid="dd-item" @click="$emit(\'click\')" @mousedown="$emit(\'select\', $event)"><slot /></button>' },
DropdownMenuSeparator: { template: '<hr />' },
DropdownMenuLabel: { template: '<div><slot /></div>' },
DropdownMenuSub: { template: '<div><slot /></div>' },
DropdownMenuSubTrigger: { template: '<div><slot /></div>' },
DropdownMenuSubContent: { template: '<div><slot /></div>' },
DropdownMenuCheckboxItem: { emits: ['select'], template: '<button data-testid="dd-check" @click="$emit(\'select\')"><slot /></button>' }
}));
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 data-testid="ctx-item" @click="$emit(\'click\')"><slot /></button>' },
ContextMenuSeparator: { template: '<hr />' }
}));
vi.mock('@/components/ui/collapsible', () => ({
Collapsible: { template: '<div><slot :open="true" /></div>' },
CollapsibleTrigger: { template: '<div><slot /></div>' },
CollapsibleContent: { template: '<div><slot /></div>' }
}));
vi.mock('@/components/ui/kbd', () => ({
Kbd: { template: '<kbd><slot /></kbd>' }
}));
vi.mock('@/components/ui/tooltip', () => ({
TooltipWrapper: { template: '<span><slot /></span>' }
}));
vi.mock('lucide-vue-next', () => ({
ChevronRight: { template: '<i />' },
Heart: { template: '<i />' }
}));
import NavMenu from '../NavMenu.vue';
function mountComponent() {
return mount(NavMenu, {
global: {
stubs: {
CustomNavDialog: { template: '<div data-testid="custom-nav-dialog" />' }
}
}
});
}
describe('NavMenu.vue', () => {
beforeEach(() => {
mocks.routerPush.mockClear();
mocks.directAccessPaste.mockClear();
mocks.logout.mockClear();
mocks.clearAllNotifications.mockClear();
mocks.toggleThemeMode.mockClear();
mocks.toggleNavCollapsed.mockClear();
mocks.initThemeColor.mockClear();
mocks.applyThemeColor.mockClear();
mocks.openExternalLink.mockClear();
mocks.getString.mockClear();
mocks.setString.mockClear();
mocks.currentRoute.value = { name: 'unknown', meta: {} };
});
it('initializes theme and navigates to first route on mount', async () => {
mountComponent();
await Promise.resolve();
await Promise.resolve();
expect(mocks.initThemeColor).toHaveBeenCalled();
expect(mocks.getString).toHaveBeenCalledWith('VRCX_customNavMenuLayoutList');
expect(mocks.routerPush).toHaveBeenCalledWith({ name: 'feed' });
});
it('runs direct access action when direct-access menu is clicked', async () => {
const wrapper = mountComponent();
await vi.waitFor(() => {
const target = wrapper
.findAll('[data-testid="menu-btn"]')
.find((node) => node.text().includes('nav_tooltip.direct_access'));
expect(target).toBeTruthy();
});
const target = wrapper
.findAll('[data-testid="menu-btn"]')
.find((node) => node.text().includes('nav_tooltip.direct_access'));
await target.trigger('click');
expect(mocks.directAccessPaste).toHaveBeenCalledTimes(1);
});
it('toggles theme when toggle-theme button is clicked', async () => {
const wrapper = mountComponent();
await Promise.resolve();
await Promise.resolve();
const target = wrapper
.findAll('[data-testid="menu-btn"]')
.find((node) => node.text().includes('nav_tooltip.toggle_theme'));
expect(target).toBeTruthy();
await target.trigger('click');
expect(mocks.toggleThemeMode).toHaveBeenCalledTimes(1);
});
});

View File

@@ -0,0 +1,80 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { mount } from '@vue/test-utils';
import { nextTick } from 'vue';
const mocks = vi.hoisted(() => ({
timeToText: vi.fn((ms) => `${ms}ms`)
}));
vi.mock('../../shared/utils', () => ({
timeToText: (...args) => mocks.timeToText(...args)
}));
import Timer from '../Timer.vue';
describe('Timer.vue', () => {
let intervalCallback;
beforeEach(() => {
intervalCallback = null;
mocks.timeToText.mockClear();
vi.spyOn(globalThis, 'setInterval').mockImplementation((cb) => {
intervalCallback = cb;
return 99;
});
vi.spyOn(globalThis, 'clearInterval').mockImplementation(() => {});
vi.spyOn(Date, 'now').mockReturnValue(10000);
});
afterEach(() => {
vi.restoreAllMocks();
});
it('renders elapsed time text from epoch', () => {
const wrapper = mount(Timer, {
props: {
epoch: 4000
}
});
expect(wrapper.text()).toBe('6000ms');
expect(mocks.timeToText).toHaveBeenCalledWith(6000);
});
it('updates text when interval callback runs', async () => {
const wrapper = mount(Timer, {
props: {
epoch: 4000
}
});
vi.mocked(Date.now).mockReturnValue(13000);
intervalCallback?.();
await nextTick();
expect(wrapper.text()).toBe('9000ms');
});
it('renders dash when epoch is falsy', () => {
const wrapper = mount(Timer, {
props: {
epoch: 0
}
});
expect(wrapper.text()).toBe('-');
});
it('clears interval on unmount', () => {
const wrapper = mount(Timer, {
props: {
epoch: 1
}
});
wrapper.unmount();
expect(clearInterval).toHaveBeenCalledWith(99);
});
});

View File

@@ -0,0 +1,36 @@
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() }));
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) })
}));
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 />' } }));
import EditNoteAndMemoDialog from '../EditNoteAndMemoDialog.vue';
describe('EditNoteAndMemoDialog.vue', () => {
beforeEach(() => {
mocks.saveUserMemo.mockClear();
});
it('emits close and saves memo on confirm', async () => {
const wrapper = mount(EditNoteAndMemoDialog, { props: { visible: false } });
await wrapper.setProps({ visible: true });
const buttons = wrapper.findAll('[data-testid="btn"]');
await buttons[1].trigger('click');
expect(mocks.saveUserMemo).toHaveBeenCalledWith('usr_1', 'm1');
expect(wrapper.emitted('update:visible')).toEqual([[false]]);
});
});

View File

@@ -0,0 +1,51 @@
import { describe, expect, it, vi } from 'vitest';
import { mount } from '@vue/test-utils';
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 }) }),
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('lucide-vue-next', () => ({
Check: { template: '<i />' },
CheckCircle: { template: '<i />' },
Flag: { template: '<i />' },
LineChart: { template: '<i />' },
Mail: { template: '<i />' },
MessageCircle: { template: '<i />' },
MessageSquare: { template: '<i />' },
Mic: { template: '<i />' },
MoreHorizontal: { template: '<i />' },
MousePointer: { template: '<i />' },
Pencil: { template: '<i />' },
Plus: { template: '<i />' },
RefreshCw: { template: '<i />' },
Settings: { template: '<i />' },
Share2: { template: '<i />' },
Star: { template: '<i />' },
Trash2: { template: '<i />' },
User: { template: '<i />' },
VolumeX: { template: '<i />' },
X: { template: '<i />' },
XCircle: { template: '<i />' }
}));
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 } });
await wrapper.findAll('[data-testid="dd-item"]')[0].trigger('click');
expect(userDialogCommand).toHaveBeenCalled();
});
});

View File

@@ -0,0 +1,60 @@
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { mount } from '@vue/test-utils';
import { ref } from 'vue';
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 } }
}));
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('../../../stores', () => ({
useFavoriteStore: () => ({
favoriteFriendGroups: ref([{ key: 'group_1', type: 'friend', name: 'group_1', displayName: 'G1', count: 0, capacity: 100 }]),
favoriteAvatarGroups: ref([]),
favoriteWorldGroups: ref([]),
favoriteDialog: mocks.favoriteDialog,
localWorldFavoriteGroups: ref([]),
localAvatarFavoriteGroups: ref([]),
localFriendFavoriteGroups: ref([]),
localWorldFavGroupLength: vi.fn(() => 0),
hasLocalWorldFavorite: vi.fn(() => false),
hasLocalAvatarFavorite: vi.fn(() => false),
localAvatarFavGroupLength: vi.fn(() => 0),
deleteFavoriteNoConfirm: (...a) => mocks.deleteFavoriteNoConfirm(...a),
localFriendFavGroupLength: vi.fn(() => 0),
hasLocalFriendFavorite: vi.fn(() => false)
}),
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('lucide-vue-next', () => ({ Check: { template: '<i />' } }));
import ChooseFavoriteGroupDialog from '../ChooseFavoriteGroupDialog.vue';
describe('ChooseFavoriteGroupDialog.vue', () => {
beforeEach(() => {
mocks.addFavorite.mockClear();
mocks.toastSuccess.mockClear();
mocks.favoriteDialog.value = { visible: true, type: 'friend', objectId: 'usr_1', currentGroup: null };
});
it('runs delete action for current group', async () => {
mocks.favoriteDialog.value = {
visible: true,
type: 'friend',
objectId: 'usr_1',
currentGroup: { key: 'group_1', displayName: 'G1', count: 0, capacity: 100 }
};
const wrapper = mount(ChooseFavoriteGroupDialog);
await wrapper.get('[data-testid="btn"]').trigger('click');
expect(mocks.deleteFavoriteNoConfirm).toHaveBeenCalledWith('usr_1');
});
});

View File

@@ -0,0 +1,39 @@
import { describe, expect, it, vi } from 'vitest';
import { mount } from '@vue/test-utils';
const mocks = vi.hoisted(() => ({
resetCropState: vi.fn(),
loadImageForCrop: vi.fn(),
getCroppedBlob: vi.fn(async () => new Blob(['x'], { type: 'image/png' })),
cropperRef: { value: null },
cropperImageSrc: { value: 'blob://img' }
}));
vi.mock('vue-i18n', () => ({ useI18n: () => ({ t: (k) => k }) }));
vi.mock('../../composables/useImageCropper', () => ({
useImageCropper: () => ({
cropperRef: mocks.cropperRef,
cropperImageSrc: mocks.cropperImageSrc,
resetCropState: (...a) => mocks.resetCropState(...a),
loadImageForCrop: (...a) => mocks.loadImageForCrop(...a),
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 />' }) }));
import ImageCropDialog from '../ImageCropDialog.vue';
describe('ImageCropDialog.vue', () => {
it('renders crop dialog title', () => {
const wrapper = mount(ImageCropDialog, {
props: { open: true, title: 'Crop', aspectRatio: 1, file: null }
});
expect(wrapper.text()).toContain('Crop');
});
});

View File

@@ -0,0 +1,40 @@
import { describe, expect, it, vi } from 'vitest';
import { mount } from '@vue/test-utils';
import { ref } from 'vue';
const mocks = vi.hoisted(() => ({
confirm: vi.fn(async () => ({ ok: true })),
sendGroupInvite: vi.fn(async () => ({})),
getGroup: vi.fn(async () => ({ json: { id: 'grp_1' } })),
fetch: vi.fn(async () => ({ ref: { name: 'Group One' } })),
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 } }
}));
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('../../../stores', () => ({
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('lucide-vue-next', () => ({ Check: { template: '<i />' } }));
import InviteGroupDialog from '../InviteGroupDialog.vue';
describe('InviteGroupDialog.vue', () => {
it('renders invite dialog', async () => {
const wrapper = mount(InviteGroupDialog);
expect(wrapper.text()).toContain('dialog.invite_to_group.header');
});
});

View File

@@ -0,0 +1,55 @@
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { mount } from '@vue/test-utils';
import { ref } from 'vue';
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' } }
}));
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 }) }));
vi.mock('vue-sonner', () => ({ toast: { success: vi.fn(), error: vi.fn() } }));
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() }),
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' })
}));
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';
describe('LaunchDialog.vue', () => {
beforeEach(() => {
mocks.selfInvite.mockClear();
});
it('renders launch dialog header', async () => {
const wrapper = mount(LaunchDialog);
await Promise.resolve();
expect(wrapper.text()).toContain('dialog.launch.header');
});
});

View File

@@ -0,0 +1,48 @@
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { mount } from '@vue/test-utils';
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' }] },
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) }),
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' }) })
}));
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 />' } }));
import MainDialogContainer from '../MainDialogContainer.vue';
describe('MainDialogContainer.vue', () => {
beforeEach(() => {
mocks.handleBreadcrumbClick.mockClear();
});
it('renders active dialog and handles breadcrumb back click', async () => {
const wrapper = mount(MainDialogContainer);
expect(wrapper.find('[data-testid="user-dialog"]').exists()).toBe(true);
await wrapper.get('[data-testid="btn"]').trigger('click');
expect(mocks.handleBreadcrumbClick).toHaveBeenCalled();
});
});

View File

@@ -0,0 +1,32 @@
import { describe, expect, it, vi } from 'vitest';
import { mount } from '@vue/test-utils';
const mocks = vi.hoisted(() => ({
sendBoop: vi.fn(),
fetch: vi.fn(async () => ({ ref: { displayName: 'User A' } })),
boopDialog: { value: { visible: true, userId: 'usr_1' } }
}));
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('../../../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 />' } }));
import SendBoopDialog from '../SendBoopDialog.vue';
describe('SendBoopDialog.vue', () => {
it('renders boop dialog content', async () => {
const wrapper = mount(SendBoopDialog);
expect(wrapper.text()).toContain('dialog.boop_dialog.header');
});
});

View File

@@ -0,0 +1,40 @@
import { describe, expect, it, vi } from 'vitest';
import { mount } from '@vue/test-utils';
const mocks = vi.hoisted(() => ({
close: vi.fn(),
save: vi.fn(),
dialog: { value: { visible: true, maxTableSize: '1000', searchLimit: '100' } }
}));
vi.mock('pinia', async (i) => ({ ...(await i()), storeToRefs: (s) => s }));
vi.mock('vue-i18n', () => ({ useI18n: () => ({ t: (k) => k }) }));
vi.mock('../../../stores', () => ({
useAppearanceSettingsStore: () => ({
tableLimitsDialog: mocks.dialog,
closeTableLimitsDialog: (...a) => mocks.close(...a),
saveTableLimitsDialog: (...a) => mocks.save(...a),
TABLE_MAX_SIZE_MIN: 100,
TABLE_MAX_SIZE_MAX: 5000,
SEARCH_LIMIT_MIN: 10,
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 />' } }));
import TableLimitsDialog from '../TableLimitsDialog.vue';
describe('TableLimitsDialog.vue', () => {
it('disables save when limits are invalid and calls close', async () => {
mocks.dialog.value.maxTableSize = '1';
const wrapper = mount(TableLimitsDialog);
const buttons = wrapper.findAll('[data-testid="btn"]');
expect(buttons[1].attributes('disabled')).toBeDefined();
await buttons[0].trigger('click');
expect(mocks.close).toHaveBeenCalled();
});
});