mirror of
https://github.com/MrUnknownDE/VRCX.git
synced 2026-05-07 14:56:06 +02:00
add test
This commit is contained in:
@@ -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' });
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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');
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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');
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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]]);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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');
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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');
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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');
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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');
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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');
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,232 @@
|
|||||||
|
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||||
|
import { mount } from '@vue/test-utils';
|
||||||
|
|
||||||
|
const mocks = vi.hoisted(() => ({
|
||||||
|
showFavoriteDialog: vi.fn(),
|
||||||
|
showAvatarDialog: vi.fn(),
|
||||||
|
selectAvatarWithConfirmation: vi.fn(),
|
||||||
|
deleteFavorite: vi.fn(),
|
||||||
|
removeLocalAvatarFavorite: vi.fn(),
|
||||||
|
currentUser: { currentAvatar: '' }
|
||||||
|
}));
|
||||||
|
|
||||||
|
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', () => ({
|
||||||
|
useFavoriteStore: () => ({
|
||||||
|
showFavoriteDialog: (...args) => mocks.showFavoriteDialog(...args)
|
||||||
|
}),
|
||||||
|
useUserStore: () => ({
|
||||||
|
currentUser: mocks.currentUser
|
||||||
|
})
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('../../../../coordinators/avatarCoordinator', () => ({
|
||||||
|
showAvatarDialog: (...args) => mocks.showAvatarDialog(...args),
|
||||||
|
selectAvatarWithConfirmation: (...args) => mocks.selectAvatarWithConfirmation(...args)
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('../../../../coordinators/favoriteCoordinator', () => ({
|
||||||
|
removeLocalAvatarFavorite: (...args) => mocks.removeLocalAvatarFavorite(...args)
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('../../../../api', () => ({
|
||||||
|
favoriteRequest: {
|
||||||
|
deleteFavorite: (...args) => mocks.deleteFavorite(...args)
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('@/components/ui/item', () => ({
|
||||||
|
Item: {
|
||||||
|
emits: ['click'],
|
||||||
|
template: '<div data-testid="item" @click="$emit(\'click\', $event)"><slot /></div>'
|
||||||
|
},
|
||||||
|
ItemActions: { template: '<div><slot /></div>' },
|
||||||
|
ItemMedia: { template: '<div><slot /></div>' },
|
||||||
|
ItemContent: { template: '<div><slot /></div>' },
|
||||||
|
ItemTitle: { template: '<div><slot /></div>' },
|
||||||
|
ItemDescription: { template: '<div><slot /></div>' }
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('@/components/ui/avatar', () => ({
|
||||||
|
Avatar: { template: '<div data-testid="avatar"><slot /></div>' },
|
||||||
|
AvatarImage: {
|
||||||
|
props: ['src'],
|
||||||
|
template: '<img data-testid="avatar-image" :src="src" :class="$attrs.class" />'
|
||||||
|
},
|
||||||
|
AvatarFallback: { template: '<span data-testid="avatar-fallback" :class="$attrs.class"><slot /></span>' }
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('@/components/ui/button', () => ({
|
||||||
|
Button: {
|
||||||
|
emits: ['click'],
|
||||||
|
template: '<button data-testid="button" @click="$emit(\'click\', $event)"><slot /></button>'
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('@/components/ui/checkbox', () => ({
|
||||||
|
Checkbox: {
|
||||||
|
props: ['modelValue'],
|
||||||
|
emits: ['update:modelValue'],
|
||||||
|
template:
|
||||||
|
'<input data-testid="checkbox" type="checkbox" :checked="modelValue" @change="$emit(\'update:modelValue\', $event.target.checked)" />'
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('@/components/ui/context-menu', () => ({
|
||||||
|
ContextMenu: { template: '<div><slot /></div>' },
|
||||||
|
ContextMenuTrigger: { template: '<div><slot /></div>' },
|
||||||
|
ContextMenuContent: { template: '<div><slot /></div>' },
|
||||||
|
ContextMenuSeparator: { template: '<hr />' },
|
||||||
|
ContextMenuItem: {
|
||||||
|
emits: ['click'],
|
||||||
|
template: '<button data-testid="ctx-item" @click="$emit(\'click\')"><slot /></button>'
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('@/components/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('lucide-vue-next', () => ({
|
||||||
|
AlertTriangle: { template: '<i />' },
|
||||||
|
Lock: { template: '<i />' },
|
||||||
|
MoreHorizontal: { template: '<i />' },
|
||||||
|
Trash2: { template: '<i />' }
|
||||||
|
}));
|
||||||
|
|
||||||
|
import FavoritesAvatarItem from '../FavoritesAvatarItem.vue';
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {Record<string, any>} props
|
||||||
|
*/
|
||||||
|
function mountItem(props = {}) {
|
||||||
|
return mount(FavoritesAvatarItem, {
|
||||||
|
props: {
|
||||||
|
favorite: {
|
||||||
|
id: 'avtr_1',
|
||||||
|
ref: {
|
||||||
|
name: 'Avatar One',
|
||||||
|
authorName: 'Author',
|
||||||
|
thumbnailImageUrl: 'https://example.com/avatar_256.png',
|
||||||
|
releaseStatus: 'public'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
group: 'FavGroup',
|
||||||
|
isLocalFavorite: false,
|
||||||
|
editMode: false,
|
||||||
|
selected: false,
|
||||||
|
...props
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param wrapper
|
||||||
|
* @param text
|
||||||
|
*/
|
||||||
|
async function clickMenuItem(wrapper, text) {
|
||||||
|
const buttons = wrapper.findAll('button');
|
||||||
|
const target = buttons.find((btn) => btn.text().includes(text));
|
||||||
|
expect(target, `menu item not found: ${text}`).toBeTruthy();
|
||||||
|
await target.trigger('click');
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('FavoritesAvatarItem.vue', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
mocks.showFavoriteDialog.mockReset();
|
||||||
|
mocks.showAvatarDialog.mockReset();
|
||||||
|
mocks.selectAvatarWithConfirmation.mockReset();
|
||||||
|
mocks.deleteFavorite.mockReset();
|
||||||
|
mocks.removeLocalAvatarFavorite.mockReset();
|
||||||
|
mocks.currentUser.currentAvatar = '';
|
||||||
|
});
|
||||||
|
|
||||||
|
it('opens avatar details when item is clicked', async () => {
|
||||||
|
const wrapper = mountItem();
|
||||||
|
|
||||||
|
await wrapper.get('[data-testid="item"]').trigger('click');
|
||||||
|
|
||||||
|
expect(mocks.showAvatarDialog).toHaveBeenCalledWith('avtr_1');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('adds the unified hover classes on item', () => {
|
||||||
|
const wrapper = mountItem();
|
||||||
|
|
||||||
|
expect(wrapper.get('[data-testid="item"]').classes()).toEqual(
|
||||||
|
expect.arrayContaining(['favorites-item', 'hover:bg-muted', 'x-hover-list'])
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('uses rounded avatar shell and 128 thumbnail image', () => {
|
||||||
|
const wrapper = mountItem();
|
||||||
|
|
||||||
|
expect(wrapper.get('[data-testid="avatar"]').classes()).toEqual(expect.arrayContaining(['rounded-sm', 'size-full']));
|
||||||
|
expect(wrapper.get('[data-testid="avatar-image"]').attributes('src')).toContain('avatar_128.png');
|
||||||
|
expect(wrapper.get('[data-testid="avatar-fallback"]').classes()).toContain('rounded-sm');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shows fallback text when thumbnail is missing', () => {
|
||||||
|
const wrapper = mountItem({
|
||||||
|
favorite: {
|
||||||
|
id: 'avtr_no_thumb',
|
||||||
|
ref: {
|
||||||
|
name: 'Bravo',
|
||||||
|
authorName: 'Author',
|
||||||
|
thumbnailImageUrl: '',
|
||||||
|
releaseStatus: 'public'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(wrapper.find('[data-testid="avatar-image"]').exists()).toBe(false);
|
||||||
|
expect(wrapper.get('[data-testid="avatar-fallback"]').text()).toContain('B');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('uses local delete flow for local favorites', async () => {
|
||||||
|
const wrapper = mountItem({
|
||||||
|
isLocalFavorite: true,
|
||||||
|
group: { name: 'LocalGroup' },
|
||||||
|
favorite: {
|
||||||
|
id: 'avtr_local',
|
||||||
|
name: 'Local Avatar',
|
||||||
|
thumbnailImageUrl: ''
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
await clickMenuItem(wrapper, 'view.favorite.delete_tooltip');
|
||||||
|
|
||||||
|
expect(mocks.removeLocalAvatarFavorite).toHaveBeenCalledWith('avtr_local', 'LocalGroup');
|
||||||
|
expect(mocks.deleteFavorite).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('uses remote delete flow for remote favorites', async () => {
|
||||||
|
const wrapper = mountItem();
|
||||||
|
|
||||||
|
await clickMenuItem(wrapper, 'view.favorite.unfavorite_tooltip');
|
||||||
|
|
||||||
|
expect(mocks.deleteFavorite).toHaveBeenCalledWith({ objectId: 'avtr_1' });
|
||||||
|
expect(mocks.removeLocalAvatarFavorite).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,177 @@
|
|||||||
|
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||||
|
import { mount } from '@vue/test-utils';
|
||||||
|
|
||||||
|
const mocks = vi.hoisted(() => ({
|
||||||
|
showFavoriteDialog: vi.fn(),
|
||||||
|
showAvatarDialog: vi.fn(),
|
||||||
|
selectAvatarWithConfirmation: vi.fn(),
|
||||||
|
currentUser: { currentAvatar: '' }
|
||||||
|
}));
|
||||||
|
|
||||||
|
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', () => ({
|
||||||
|
useFavoriteStore: () => ({
|
||||||
|
showFavoriteDialog: (...args) => mocks.showFavoriteDialog(...args)
|
||||||
|
}),
|
||||||
|
useUserStore: () => ({
|
||||||
|
currentUser: mocks.currentUser
|
||||||
|
})
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('../../../../coordinators/avatarCoordinator', () => ({
|
||||||
|
showAvatarDialog: (...args) => mocks.showAvatarDialog(...args),
|
||||||
|
selectAvatarWithConfirmation: (...args) => mocks.selectAvatarWithConfirmation(...args)
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('@/components/ui/item', () => ({
|
||||||
|
Item: {
|
||||||
|
emits: ['click'],
|
||||||
|
template: '<div data-testid="item" @click="$emit(\'click\', $event)"><slot /></div>'
|
||||||
|
},
|
||||||
|
ItemActions: { template: '<div><slot /></div>' },
|
||||||
|
ItemMedia: { template: '<div><slot /></div>' },
|
||||||
|
ItemContent: { template: '<div><slot /></div>' },
|
||||||
|
ItemTitle: { template: '<div><slot /></div>' },
|
||||||
|
ItemDescription: { template: '<div><slot /></div>' }
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('@/components/ui/avatar', () => ({
|
||||||
|
Avatar: { template: '<div data-testid="avatar"><slot /></div>' },
|
||||||
|
AvatarImage: {
|
||||||
|
props: ['src'],
|
||||||
|
template: '<img data-testid="avatar-image" :src="src" :class="$attrs.class" />'
|
||||||
|
},
|
||||||
|
AvatarFallback: { template: '<span data-testid="avatar-fallback" :class="$attrs.class"><slot /></span>' }
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('@/components/ui/button', () => ({
|
||||||
|
Button: {
|
||||||
|
emits: ['click'],
|
||||||
|
template: '<button data-testid="button" @click="$emit(\'click\', $event)"><slot /></button>'
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('@/components/ui/context-menu', () => ({
|
||||||
|
ContextMenu: { template: '<div><slot /></div>' },
|
||||||
|
ContextMenuTrigger: { template: '<div><slot /></div>' },
|
||||||
|
ContextMenuContent: { template: '<div><slot /></div>' },
|
||||||
|
ContextMenuSeparator: { template: '<hr />' },
|
||||||
|
ContextMenuItem: {
|
||||||
|
emits: ['click'],
|
||||||
|
template: '<button data-testid="ctx-item" @click="$emit(\'click\')"><slot /></button>'
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('@/components/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('lucide-vue-next', () => ({
|
||||||
|
MoreHorizontal: { template: '<i />' }
|
||||||
|
}));
|
||||||
|
|
||||||
|
import FavoritesAvatarLocalHistoryItem from '../FavoritesAvatarLocalHistoryItem.vue';
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {Record<string, any>} props
|
||||||
|
*/
|
||||||
|
function mountItem(props = {}) {
|
||||||
|
return mount(FavoritesAvatarLocalHistoryItem, {
|
||||||
|
props: {
|
||||||
|
favorite: {
|
||||||
|
id: 'avtr_hist_1',
|
||||||
|
name: 'Local History Avatar',
|
||||||
|
authorName: 'Author',
|
||||||
|
thumbnailImageUrl: 'https://example.com/history_256.png',
|
||||||
|
...props.favorite
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param wrapper
|
||||||
|
* @param text
|
||||||
|
*/
|
||||||
|
async function clickMenuItem(wrapper, text) {
|
||||||
|
const buttons = wrapper.findAll('button');
|
||||||
|
const target = buttons.find((btn) => btn.text().includes(text));
|
||||||
|
expect(target, `menu item not found: ${text}`).toBeTruthy();
|
||||||
|
await target.trigger('click');
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('FavoritesAvatarLocalHistoryItem.vue', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
mocks.showFavoriteDialog.mockReset();
|
||||||
|
mocks.showAvatarDialog.mockReset();
|
||||||
|
mocks.selectAvatarWithConfirmation.mockReset();
|
||||||
|
mocks.currentUser.currentAvatar = '';
|
||||||
|
});
|
||||||
|
|
||||||
|
it('opens avatar details when item is clicked', async () => {
|
||||||
|
const wrapper = mountItem();
|
||||||
|
|
||||||
|
await wrapper.get('[data-testid="item"]').trigger('click');
|
||||||
|
|
||||||
|
expect(mocks.showAvatarDialog).toHaveBeenCalledWith('avtr_hist_1');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('adds the unified hover classes on item', () => {
|
||||||
|
const wrapper = mountItem();
|
||||||
|
|
||||||
|
expect(wrapper.get('[data-testid="item"]').classes()).toEqual(
|
||||||
|
expect.arrayContaining(['favorites-item', 'hover:bg-muted', 'x-hover-list'])
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('uses rounded avatar shell and 128 thumbnail image', () => {
|
||||||
|
const wrapper = mountItem();
|
||||||
|
|
||||||
|
expect(wrapper.get('[data-testid="avatar"]').classes()).toEqual(expect.arrayContaining(['rounded-sm', 'size-full']));
|
||||||
|
expect(wrapper.get('[data-testid="avatar-image"]').attributes('src')).toContain('history_128.png');
|
||||||
|
expect(wrapper.get('[data-testid="avatar-fallback"]').classes()).toContain('rounded-sm');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shows fallback text when thumbnail is missing', () => {
|
||||||
|
const wrapper = mountItem({
|
||||||
|
favorite: {
|
||||||
|
id: 'avtr_hist_no_thumb',
|
||||||
|
name: 'Charlie',
|
||||||
|
thumbnailImageUrl: ''
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(wrapper.find('[data-testid="avatar-image"]').exists()).toBe(false);
|
||||||
|
expect(wrapper.get('[data-testid="avatar-fallback"]').text()).toContain('C');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('runs select-avatar action from menu', async () => {
|
||||||
|
const wrapper = mountItem();
|
||||||
|
|
||||||
|
await clickMenuItem(wrapper, 'view.favorite.select_avatar_tooltip');
|
||||||
|
|
||||||
|
expect(mocks.selectAvatarWithConfirmation).toHaveBeenCalledWith('avtr_hist_1');
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -240,6 +240,14 @@ describe('FavoritesFriendItem.vue', () => {
|
|||||||
expect(mocks.showUserDialog).toHaveBeenCalledWith('usr_1');
|
expect(mocks.showUserDialog).toHaveBeenCalledWith('usr_1');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('adds the unified hover classes on item', () => {
|
||||||
|
const wrapper = mountItem();
|
||||||
|
|
||||||
|
expect(wrapper.get('[data-testid="item"]').classes()).toEqual(
|
||||||
|
expect.arrayContaining(['favorites-item', 'hover:bg-muted', 'x-hover-list'])
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
it('emits toggle-select in edit mode checkbox', async () => {
|
it('emits toggle-select in edit mode checkbox', async () => {
|
||||||
const wrapper = mountItem({ editMode: true });
|
const wrapper = mountItem({ editMode: true });
|
||||||
|
|
||||||
|
|||||||
@@ -76,6 +76,15 @@ vi.mock('@/components/ui/item', () => ({
|
|||||||
ItemDescription: { template: '<div><slot /></div>' }
|
ItemDescription: { template: '<div><slot /></div>' }
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
vi.mock('@/components/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('@/components/ui/button', () => ({
|
vi.mock('@/components/ui/button', () => ({
|
||||||
Button: {
|
Button: {
|
||||||
emits: ['click'],
|
emits: ['click'],
|
||||||
@@ -245,6 +254,32 @@ describe('FavoritesWorldItem.vue', () => {
|
|||||||
expect(wrapper.text()).toContain('wrld_missing_ref');
|
expect(wrapper.text()).toContain('wrld_missing_ref');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('adds the unified hover classes on item', () => {
|
||||||
|
const wrapper = mountItem();
|
||||||
|
|
||||||
|
expect(wrapper.get('[data-testid="item"]').classes()).toEqual(
|
||||||
|
expect.arrayContaining(['favorites-item', 'hover:bg-muted', 'x-hover-list'])
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('uses rounded avatar fallback when thumbnail is missing', () => {
|
||||||
|
const wrapper = mountItem({
|
||||||
|
favorite: {
|
||||||
|
id: 'wrld_no_thumb',
|
||||||
|
ref: {
|
||||||
|
name: 'No Thumb World',
|
||||||
|
authorName: 'Author',
|
||||||
|
thumbnailImageUrl: '',
|
||||||
|
releaseStatus: 'public'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(wrapper.find('[data-testid="avatar-image"]').exists()).toBe(false);
|
||||||
|
expect(wrapper.get('[data-testid="avatar"]').classes()).toEqual(expect.arrayContaining(['rounded-sm', 'size-full']));
|
||||||
|
expect(wrapper.get('[data-testid="avatar-fallback"]').classes()).toContain('rounded-sm');
|
||||||
|
});
|
||||||
|
|
||||||
it('deletes local favorite via coordinator', async () => {
|
it('deletes local favorite via coordinator', async () => {
|
||||||
const wrapper = mountItem({
|
const wrapper = mountItem({
|
||||||
favorite: {
|
favorite: {
|
||||||
|
|||||||
@@ -0,0 +1,53 @@
|
|||||||
|
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||||
|
import { defineComponent, h } from 'vue';
|
||||||
|
import { mount } from '@vue/test-utils';
|
||||||
|
|
||||||
|
const mocks = vi.hoisted(() => ({
|
||||||
|
getString: vi.fn(async (_key, fallback) => fallback),
|
||||||
|
setString: vi.fn()
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('../../../../services/config.js', () => ({
|
||||||
|
default: {
|
||||||
|
getString: (...args) => mocks.getString(...args),
|
||||||
|
setString: (...args) => mocks.setString(...args)
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
import { useFavoritesCardScaling } from '../useFavoritesCardScaling';
|
||||||
|
|
||||||
|
function mountComposable() {
|
||||||
|
let api;
|
||||||
|
const Comp = defineComponent({
|
||||||
|
setup() {
|
||||||
|
api = useFavoritesCardScaling({
|
||||||
|
configKey: 'scale-key',
|
||||||
|
spacingConfigKey: 'spacing-key'
|
||||||
|
});
|
||||||
|
return () => h('div');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
mount(Comp);
|
||||||
|
return api;
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('useFavoritesCardScaling', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
mocks.getString.mockClear();
|
||||||
|
mocks.setString.mockClear();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('builds grid style css vars from scale/spacing', async () => {
|
||||||
|
const api = mountComposable();
|
||||||
|
api.cardScale.value = 0.8;
|
||||||
|
api.cardSpacing.value = 1.2;
|
||||||
|
|
||||||
|
const style = api.gridStyle.value(3, { preferredColumns: 2 });
|
||||||
|
|
||||||
|
expect(style['--favorites-card-scale']).toBe('0.80');
|
||||||
|
expect(style['--favorites-card-spacing-scale']).toBe('1.20');
|
||||||
|
expect(Number(style['--favorites-grid-columns'])).toBeGreaterThanOrEqual(1);
|
||||||
|
expect(mocks.setString).toHaveBeenCalledWith('scale-key', '0.8');
|
||||||
|
expect(mocks.setString).toHaveBeenCalledWith('spacing-key', '1.2');
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
import { describe, expect, it, vi } from 'vitest';
|
||||||
|
|
||||||
|
import { useFavoritesLocalGroups } from '../useFavoritesLocalGroups';
|
||||||
|
|
||||||
|
describe('useFavoritesLocalGroups', () => {
|
||||||
|
it('starts, confirms and selects created local group', async () => {
|
||||||
|
const createGroup = vi.fn();
|
||||||
|
const selectGroup = vi.fn();
|
||||||
|
const api = useFavoritesLocalGroups({ createGroup, selectGroup });
|
||||||
|
|
||||||
|
api.startLocalGroupCreation();
|
||||||
|
expect(api.isCreatingLocalGroup.value).toBe(true);
|
||||||
|
|
||||||
|
api.newLocalGroupName.value = ' Local A ';
|
||||||
|
api.handleLocalGroupCreationConfirm();
|
||||||
|
await Promise.resolve();
|
||||||
|
|
||||||
|
expect(createGroup).toHaveBeenCalledWith('Local A');
|
||||||
|
expect(selectGroup).toHaveBeenCalledWith('local', 'Local A', { userInitiated: true });
|
||||||
|
expect(api.isCreatingLocalGroup.value).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('cancels when name is empty', () => {
|
||||||
|
const createGroup = vi.fn();
|
||||||
|
const selectGroup = vi.fn();
|
||||||
|
const api = useFavoritesLocalGroups({ createGroup, selectGroup });
|
||||||
|
|
||||||
|
api.startLocalGroupCreation();
|
||||||
|
api.newLocalGroupName.value = ' ';
|
||||||
|
api.handleLocalGroupCreationConfirm();
|
||||||
|
|
||||||
|
expect(createGroup).not.toHaveBeenCalled();
|
||||||
|
expect(selectGroup).not.toHaveBeenCalled();
|
||||||
|
expect(api.isCreatingLocalGroup.value).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
import { describe, expect, it, vi } from 'vitest';
|
||||||
|
import { defineComponent, h } from 'vue';
|
||||||
|
import { mount } from '@vue/test-utils';
|
||||||
|
import { beforeEach } from 'vitest';
|
||||||
|
|
||||||
|
const mocks = vi.hoisted(() => ({
|
||||||
|
getString: vi.fn(async (_key, fallback) => fallback),
|
||||||
|
setString: vi.fn()
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('../../../../services/config.js', () => ({
|
||||||
|
default: {
|
||||||
|
getString: (...args) => mocks.getString(...args),
|
||||||
|
setString: (...args) => mocks.setString(...args)
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
import { useFavoritesSplitter } from '../useFavoritesSplitter';
|
||||||
|
|
||||||
|
function mountComposable() {
|
||||||
|
let api;
|
||||||
|
const Comp = defineComponent({
|
||||||
|
setup() {
|
||||||
|
api = useFavoritesSplitter({
|
||||||
|
configKey: 'split-key',
|
||||||
|
defaultSize: 240,
|
||||||
|
minPx: 120,
|
||||||
|
maxPx: 360
|
||||||
|
});
|
||||||
|
return () => h('div');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
mount(Comp);
|
||||||
|
return api;
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('useFavoritesSplitter', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
mocks.setString.mockClear();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('persists layout size while dragging', () => {
|
||||||
|
const api = mountComposable();
|
||||||
|
api.splitterGroupRef.value = { getBoundingClientRect: () => ({ width: 1200 }) };
|
||||||
|
|
||||||
|
api.setDragging(true);
|
||||||
|
api.handleLayout([25, 75]);
|
||||||
|
api.setDragging(false);
|
||||||
|
|
||||||
|
expect(mocks.setString).toHaveBeenCalledWith('split-key', '300');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('ignores layout updates when not dragging', () => {
|
||||||
|
const api = mountComposable();
|
||||||
|
api.splitterGroupRef.value = { getBoundingClientRect: () => ({ width: 1200 }) };
|
||||||
|
|
||||||
|
api.handleLayout([20, 80]);
|
||||||
|
|
||||||
|
expect(mocks.setString).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
import { describe, expect, it, vi } from 'vitest';
|
||||||
|
import { mount } from '@vue/test-utils';
|
||||||
|
import { ref } from 'vue';
|
||||||
|
|
||||||
|
const mocks = vi.hoisted(() => ({ lookup: vi.fn(), table: { value: { vip: false, filter: [], search: '' } } }));
|
||||||
|
|
||||||
|
vi.mock('pinia', async (i) => ({ ...(await i()), storeToRefs: (s) => s }));
|
||||||
|
vi.mock('vue-i18n', () => ({ useI18n: () => ({ t: (k) => k }) }));
|
||||||
|
vi.mock('../../../stores', () => ({
|
||||||
|
useGameLogStore: () => ({ gameLogTableLookup: (...a) => mocks.lookup(...a), gameLogTable: mocks.table, gameLogTableData: ref([]) }),
|
||||||
|
useAppearanceSettingsStore: () => ({ tablePageSizes: [20, 50], tablePageSize: 20 }),
|
||||||
|
useVrcxStore: () => ({ maxTableSize: 500 }),
|
||||||
|
useModalStore: () => ({ confirm: vi.fn() })
|
||||||
|
}));
|
||||||
|
vi.mock('../../../components/ui/data-table', () => ({ DataTableLayout: { template: '<div><slot name="toolbar" /></div>' } }));
|
||||||
|
vi.mock('../../../components/ui/input-group', () => ({ InputGroupField: { template: '<input />' } }));
|
||||||
|
vi.mock('@/components/ui/select', () => ({ Select: { emits: ['update:modelValue'], template: '<button data-testid="sel" @click="$emit(\'update:modelValue\', [\'Event\'])"><slot /></button>' }, SelectTrigger: { template: '<div><slot /></div>' }, SelectValue: { template: '<div><slot /></div>' }, SelectContent: { template: '<div><slot /></div>' }, SelectGroup: { template: '<div><slot /></div>' }, SelectItem: { template: '<div><slot /></div>' } }));
|
||||||
|
vi.mock('@/components/ui/toggle', () => ({ Toggle: { template: '<button><slot /></button>' } }));
|
||||||
|
vi.mock('lucide-vue-next', () => ({ Star: { template: '<i />' } }));
|
||||||
|
vi.mock('../../../services/database', () => ({ database: { deleteGameLogEntry: vi.fn() } }));
|
||||||
|
vi.mock('../../../shared/utils', () => ({ removeFromArray: vi.fn() }));
|
||||||
|
vi.mock('../../../composables/useDataTableScrollHeight', () => ({ useDataTableScrollHeight: () => ({ tableStyle: ref({}) }) }));
|
||||||
|
vi.mock('../../../lib/table/useVrcxVueTable', () => ({ useVrcxVueTable: () => ({ table: { getFilteredRowModel: () => ({ rows: [] }) }, pagination: ref({ pageIndex: 0, pageSize: 20 }) }) }));
|
||||||
|
vi.mock('../columns.jsx', () => ({ createColumns: () => [] }));
|
||||||
|
|
||||||
|
import GameLog from '../GameLog.vue';
|
||||||
|
|
||||||
|
describe('GameLog.vue', () => {
|
||||||
|
it('updates filter and triggers lookup when filter changes', async () => {
|
||||||
|
const wrapper = mount(GameLog);
|
||||||
|
await wrapper.get('[data-testid="sel"]').trigger('click');
|
||||||
|
|
||||||
|
expect(mocks.lookup).toHaveBeenCalled();
|
||||||
|
expect(mocks.table.value.filter).toEqual(['Event']);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
import { describe, expect, it, vi } from 'vitest';
|
||||||
|
import { mount } from '@vue/test-utils';
|
||||||
|
import { ref } from 'vue';
|
||||||
|
|
||||||
|
const mocks = vi.hoisted(() => ({ replace: vi.fn(), setNavCollapsed: vi.fn(), setNavWidth: vi.fn() }));
|
||||||
|
|
||||||
|
vi.mock('pinia', async (i) => ({ ...(await i()), storeToRefs: (s) => s }));
|
||||||
|
vi.mock('vue-router', () => ({ useRouter: () => ({ replace: (...a) => mocks.replace(...a) }) }));
|
||||||
|
vi.mock('../../../services/watchState', () => ({ watchState: { isLoggedIn: false } }));
|
||||||
|
vi.mock('../../../stores', () => ({ useAppearanceSettingsStore: () => ({ navWidth: ref(240), isNavCollapsed: ref(false), showStatusBar: ref(false), setNavCollapsed: (...a) => mocks.setNavCollapsed(...a), setNavWidth: (...a) => mocks.setNavWidth(...a) }) }));
|
||||||
|
vi.mock('../../../composables/useMainLayoutResizable', () => ({ useMainLayoutResizable: () => ({ asideDefaultSize: 30, asideMinSize: 0, asideMaxPx: 480, mainDefaultSize: 70, handleLayout: vi.fn(), isAsideCollapsed: () => false, isAsideCollapsedStatic: false, isSideBarTabShow: ref(true) }) }));
|
||||||
|
vi.mock('../../../components/ui/resizable', () => ({ ResizablePanelGroup: { template: '<div><slot :layout="[]" /></div>' }, ResizablePanel: { template: '<div><slot /></div>' }, ResizableHandle: { template: '<div />' } }));
|
||||||
|
vi.mock('../../../components/ui/sidebar', () => ({ SidebarProvider: { template: '<div><slot /></div>' }, SidebarInset: { template: '<div><slot /></div>' } }));
|
||||||
|
vi.mock('../../../components/NavMenu.vue', () => ({ default: { template: '<div />' } }));
|
||||||
|
vi.mock('../../Sidebar/Sidebar.vue', () => ({ default: { template: '<div />' } }));
|
||||||
|
vi.mock('../../../components/StatusBar.vue', () => ({ default: { template: '<div />' } }));
|
||||||
|
vi.mock('../../../components/dialogs/MainDialogContainer.vue', () => ({ default: { template: '<div />' } }));
|
||||||
|
vi.mock('../../../components/FullscreenImagePreview.vue', () => ({ default: { template: '<div />' } }));
|
||||||
|
vi.mock('../../../components/dialogs/ChooseFavoriteGroupDialog.vue', () => ({ default: { template: '<div />' } }));
|
||||||
|
vi.mock('../../../components/dialogs/LaunchDialog.vue', () => ({ default: { template: '<div />' } }));
|
||||||
|
vi.mock('../../Settings/dialogs/LaunchOptionsDialog.vue', () => ({ default: { template: '<div />' } }));
|
||||||
|
vi.mock('../../Favorites/dialogs/FriendImportDialog.vue', () => ({ default: { template: '<div />' } }));
|
||||||
|
vi.mock('../../Favorites/dialogs/WorldImportDialog.vue', () => ({ default: { template: '<div />' } }));
|
||||||
|
vi.mock('../../Favorites/dialogs/AvatarImportDialog.vue', () => ({ default: { template: '<div />' } }));
|
||||||
|
vi.mock('../../../components/dialogs/GroupDialog/GroupMemberModerationDialog.vue', () => ({ default: { template: '<div />' } }));
|
||||||
|
vi.mock('../../../components/dialogs/InviteGroupDialog.vue', () => ({ default: { template: '<div />' } }));
|
||||||
|
vi.mock('../../Settings/dialogs/VRChatConfigDialog.vue', () => ({ default: { template: '<div />' } }));
|
||||||
|
vi.mock('../../Settings/dialogs/PrimaryPasswordDialog.vue', () => ({ default: { template: '<div />' } }));
|
||||||
|
vi.mock('../../../components/dialogs/SendBoopDialog.vue', () => ({ default: { template: '<div />' } }));
|
||||||
|
vi.mock('../../Settings/dialogs/ChangelogDialog.vue', () => ({ default: { template: '<div />' } }));
|
||||||
|
|
||||||
|
import MainLayout from '../MainLayout.vue';
|
||||||
|
|
||||||
|
describe('MainLayout.vue', () => {
|
||||||
|
it('redirects to login when not logged in', () => {
|
||||||
|
mount(MainLayout, { global: { stubs: { RouterView: { template: '<div />' }, KeepAlive: { template: '<div><slot /></div>' } } } });
|
||||||
|
expect(mocks.replace).toHaveBeenCalledWith({ name: 'login' });
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||||
|
import { mount } from '@vue/test-utils';
|
||||||
|
|
||||||
|
const mocks = vi.hoisted(() => ({
|
||||||
|
discordStore: {
|
||||||
|
setDiscordActive: vi.fn(),
|
||||||
|
setDiscordInstance: vi.fn(),
|
||||||
|
setDiscordHideInvite: vi.fn(),
|
||||||
|
setDiscordJoinButton: vi.fn(),
|
||||||
|
setDiscordHideImage: vi.fn(),
|
||||||
|
setDiscordShowPlatform: vi.fn(),
|
||||||
|
setDiscordWorldIntegration: vi.fn(),
|
||||||
|
setDiscordWorldNameAsDiscordStatus: vi.fn(),
|
||||||
|
saveDiscordOption: vi.fn(),
|
||||||
|
discordActive: { __v_isRef: true, value: true },
|
||||||
|
discordInstance: { __v_isRef: true, value: true },
|
||||||
|
discordHideInvite: { __v_isRef: true, value: false },
|
||||||
|
discordJoinButton: { __v_isRef: true, value: true },
|
||||||
|
discordHideImage: { __v_isRef: true, value: false },
|
||||||
|
discordShowPlatform: { __v_isRef: true, value: true },
|
||||||
|
discordWorldIntegration: { __v_isRef: true, value: true },
|
||||||
|
discordWorldNameAsDiscordStatus: { __v_isRef: true, value: false }
|
||||||
|
},
|
||||||
|
showVRChatConfig: vi.fn()
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('pinia', async (i) => ({ ...(await i()), storeToRefs: (s) => s }));
|
||||||
|
vi.mock('vue-i18n', () => ({ useI18n: () => ({ t: (k) => k }) }));
|
||||||
|
vi.mock('../../../../../stores', () => ({
|
||||||
|
useDiscordPresenceSettingsStore: () => mocks.discordStore,
|
||||||
|
useAdvancedSettingsStore: () => ({ showVRChatConfig: (...a) => mocks.showVRChatConfig(...a) })
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('../../SimpleSwitch.vue', () => ({
|
||||||
|
default: {
|
||||||
|
props: ['label', 'disabled'],
|
||||||
|
emits: ['change'],
|
||||||
|
template:
|
||||||
|
'<div data-testid="simple-switch" :data-label="label" :data-disabled="disabled"><button class="emit-change" @click="$emit(\'change\', true)" /></div>'
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
import DiscordPresenceTab from '../DiscordPresenceTab.vue';
|
||||||
|
|
||||||
|
describe('DiscordPresenceTab.vue', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
mocks.discordStore.discordActive.value = true;
|
||||||
|
mocks.discordStore.discordInstance.value = true;
|
||||||
|
mocks.discordStore.setDiscordActive.mockClear();
|
||||||
|
mocks.discordStore.saveDiscordOption.mockClear();
|
||||||
|
mocks.showVRChatConfig.mockClear();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('opens VRChat config and handles switch changes', async () => {
|
||||||
|
const wrapper = mount(DiscordPresenceTab);
|
||||||
|
|
||||||
|
const tooltipRow = wrapper
|
||||||
|
.findAll('div.options-container-item')
|
||||||
|
.find((node) => node.text().includes('view.settings.discord_presence.discord_presence.enable_tooltip'));
|
||||||
|
await tooltipRow.trigger('click');
|
||||||
|
|
||||||
|
expect(mocks.showVRChatConfig).toHaveBeenCalledTimes(1);
|
||||||
|
|
||||||
|
await wrapper.findAll('.emit-change')[0].trigger('click');
|
||||||
|
expect(mocks.discordStore.setDiscordActive).toHaveBeenCalledTimes(1);
|
||||||
|
expect(mocks.discordStore.saveDiscordOption).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('passes disabled state to dependent switches when discord is disabled', () => {
|
||||||
|
mocks.discordStore.discordActive.value = false;
|
||||||
|
const wrapper = mount(DiscordPresenceTab);
|
||||||
|
|
||||||
|
const worldIntegration = wrapper
|
||||||
|
.findAll('[data-testid="simple-switch"]')
|
||||||
|
.find((node) =>
|
||||||
|
node.attributes('data-label')?.includes('world_integration')
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(worldIntegration?.attributes('data-disabled')).toBe('true');
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
import { describe, expect, it, vi } from 'vitest';
|
||||||
|
import { mount } from '@vue/test-utils';
|
||||||
|
|
||||||
|
vi.mock('../../WristOverlaySettings.vue', () => ({
|
||||||
|
default: {
|
||||||
|
emits: ['open-feed-filters'],
|
||||||
|
template:
|
||||||
|
'<button data-testid="open-filters" @click="$emit(\'open-feed-filters\')">open</button>'
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('../../../dialogs/FeedFiltersDialog.vue', () => ({
|
||||||
|
default: {
|
||||||
|
props: ['feedFiltersDialogMode'],
|
||||||
|
template: '<div data-testid="feed-dialog" :data-mode="feedFiltersDialogMode" />'
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
import WristOverlayTab from '../WristOverlayTab.vue';
|
||||||
|
|
||||||
|
describe('WristOverlayTab.vue', () => {
|
||||||
|
it('sets feed dialog mode to wrist when child emits open-feed-filters', async () => {
|
||||||
|
const wrapper = mount(WristOverlayTab);
|
||||||
|
|
||||||
|
expect(wrapper.get('[data-testid="feed-dialog"]').attributes('data-mode')).toBe('');
|
||||||
|
|
||||||
|
await wrapper.get('[data-testid="open-filters"]').trigger('click');
|
||||||
|
|
||||||
|
expect(wrapper.get('[data-testid="feed-dialog"]').attributes('data-mode')).toBe('wrist');
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
import { describe, expect, it, vi } from 'vitest';
|
||||||
|
import { mount } from '@vue/test-utils';
|
||||||
|
|
||||||
|
vi.mock('../../../../components/ui/switch', () => ({
|
||||||
|
Switch: {
|
||||||
|
props: ['modelValue', 'disabled'],
|
||||||
|
emits: ['update:modelValue'],
|
||||||
|
template:
|
||||||
|
'<button data-testid="switch" :data-disabled="disabled" @click="$emit(\'update:modelValue\', !modelValue)">switch</button>'
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('lucide-vue-next', () => ({
|
||||||
|
Info: { template: '<i data-testid="info" />' }
|
||||||
|
}));
|
||||||
|
|
||||||
|
import SimpleSwitch from '../SimpleSwitch.vue';
|
||||||
|
|
||||||
|
describe('SimpleSwitch.vue', () => {
|
||||||
|
it('emits change when inner switch updates', async () => {
|
||||||
|
const wrapper = mount(SimpleSwitch, {
|
||||||
|
props: {
|
||||||
|
label: 'Label',
|
||||||
|
value: false
|
||||||
|
},
|
||||||
|
global: {
|
||||||
|
stubs: {
|
||||||
|
TooltipWrapper: { template: '<span data-testid="tooltip"><slot /></span>' }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
await wrapper.get('[data-testid="switch"]').trigger('click');
|
||||||
|
expect(wrapper.emitted('change')).toEqual([[true]]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('applies long label style and renders tooltip', () => {
|
||||||
|
const wrapper = mount(SimpleSwitch, {
|
||||||
|
props: {
|
||||||
|
label: 'Long',
|
||||||
|
value: true,
|
||||||
|
longLabel: true,
|
||||||
|
tooltip: 'tip',
|
||||||
|
disabled: true
|
||||||
|
},
|
||||||
|
global: {
|
||||||
|
stubs: {
|
||||||
|
TooltipWrapper: { template: '<span data-testid="tooltip"><slot /></span>' }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(wrapper.get('.name').attributes('style')).toContain('width: 300px');
|
||||||
|
expect(wrapper.find('[data-testid="tooltip"]').exists()).toBe(true);
|
||||||
|
expect(wrapper.get('[data-testid="switch"]').attributes('data-disabled')).toBe('true');
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,127 @@
|
|||||||
|
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||||
|
import { mount } from '@vue/test-utils';
|
||||||
|
|
||||||
|
const mocks = vi.hoisted(() => ({
|
||||||
|
notificationsStore: {
|
||||||
|
openVR: { value: true },
|
||||||
|
setOpenVR: vi.fn()
|
||||||
|
},
|
||||||
|
wristStore: {
|
||||||
|
overlayWrist: { value: true },
|
||||||
|
hidePrivateFromFeed: { value: false },
|
||||||
|
openVRAlways: { value: false },
|
||||||
|
overlaybutton: { value: false },
|
||||||
|
overlayHand: { value: '1' },
|
||||||
|
vrBackgroundEnabled: { value: false },
|
||||||
|
minimalFeed: { value: false },
|
||||||
|
hideDevicesFromFeed: { value: false },
|
||||||
|
vrOverlayCpuUsage: { value: false },
|
||||||
|
hideUptimeFromFeed: { value: false },
|
||||||
|
pcUptimeOnFeed: { value: false },
|
||||||
|
setOverlayWrist: vi.fn(),
|
||||||
|
setHidePrivateFromFeed: vi.fn(),
|
||||||
|
setOpenVRAlways: vi.fn(),
|
||||||
|
setOverlaybutton: vi.fn(),
|
||||||
|
setOverlayHand: vi.fn(),
|
||||||
|
setVrBackgroundEnabled: vi.fn(),
|
||||||
|
setMinimalFeed: vi.fn(),
|
||||||
|
setHideDevicesFromFeed: vi.fn(),
|
||||||
|
setVrOverlayCpuUsage: vi.fn(),
|
||||||
|
setHideUptimeFromFeed: vi.fn(),
|
||||||
|
setPcUptimeOnFeed: vi.fn()
|
||||||
|
},
|
||||||
|
saveOpenVROption: vi.fn()
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('pinia', async (i) => ({ ...(await i()), storeToRefs: (s) => s }));
|
||||||
|
vi.mock('vue-i18n', () => ({ useI18n: () => ({ t: (k) => k }) }));
|
||||||
|
|
||||||
|
vi.mock('../../../../stores', () => ({
|
||||||
|
useNotificationsSettingsStore: () => mocks.notificationsStore,
|
||||||
|
useWristOverlaySettingsStore: () => mocks.wristStore,
|
||||||
|
useVrStore: () => ({ saveOpenVROption: (...a) => mocks.saveOpenVROption(...a) })
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('@/components/ui/button', () => ({
|
||||||
|
Button: {
|
||||||
|
props: ['disabled'],
|
||||||
|
emits: ['click'],
|
||||||
|
template:
|
||||||
|
'<button data-testid="filters-btn" :disabled="disabled" @click="$emit(\'click\')"><slot /></button>'
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('../../../../components/ui/radio-group', () => ({
|
||||||
|
RadioGroup: {
|
||||||
|
props: ['modelValue', 'disabled'],
|
||||||
|
emits: ['update:modelValue'],
|
||||||
|
template:
|
||||||
|
'<div data-testid="radio-group" :data-disabled="disabled"><button data-testid="radio-false" @click="$emit(\'update:modelValue\', \'false\')" /><button data-testid="radio-true" @click="$emit(\'update:modelValue\', \'true\')" /><slot /></div>'
|
||||||
|
},
|
||||||
|
RadioGroupItem: { template: '<div />' }
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('../../../../components/ui/toggle-group', () => ({
|
||||||
|
ToggleGroup: {
|
||||||
|
emits: ['update:model-value'],
|
||||||
|
template:
|
||||||
|
'<div data-testid="toggle-group"><button data-testid="toggle-right" @click="$emit(\'update:model-value\', \'2\')" /><slot /></div>'
|
||||||
|
},
|
||||||
|
ToggleGroupItem: { template: '<div><slot /></div>' }
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('../SimpleSwitch.vue', () => ({
|
||||||
|
default: {
|
||||||
|
props: ['label'],
|
||||||
|
emits: ['change'],
|
||||||
|
template:
|
||||||
|
'<div data-testid="simple-switch" :data-label="label"><button class="emit-change" @click="$emit(\'change\', true)" /></div>'
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
import WristOverlaySettings from '../WristOverlaySettings.vue';
|
||||||
|
|
||||||
|
describe('WristOverlaySettings.vue', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
mocks.notificationsStore.openVR.value = true;
|
||||||
|
mocks.wristStore.overlayWrist.value = true;
|
||||||
|
mocks.wristStore.openVRAlways.value = false;
|
||||||
|
mocks.wristStore.overlaybutton.value = false;
|
||||||
|
mocks.notificationsStore.setOpenVR.mockClear();
|
||||||
|
mocks.wristStore.setOpenVRAlways.mockClear();
|
||||||
|
mocks.wristStore.setOverlaybutton.mockClear();
|
||||||
|
mocks.wristStore.setOverlayHand.mockClear();
|
||||||
|
mocks.saveOpenVROption.mockClear();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('emits open-feed-filters and handles switch/radio/toggle updates', async () => {
|
||||||
|
const wrapper = mount(WristOverlaySettings);
|
||||||
|
|
||||||
|
await wrapper.get('[data-testid="filters-btn"]').trigger('click');
|
||||||
|
expect(wrapper.emitted('open-feed-filters')).toBeTruthy();
|
||||||
|
|
||||||
|
await wrapper.findAll('.emit-change')[0].trigger('click');
|
||||||
|
expect(mocks.notificationsStore.setOpenVR).toHaveBeenCalledTimes(1);
|
||||||
|
expect(mocks.saveOpenVROption).toHaveBeenCalled();
|
||||||
|
|
||||||
|
const radioGroups = wrapper.findAll('[data-testid="radio-group"]');
|
||||||
|
await radioGroups[0].get('[data-testid="radio-true"]').trigger('click');
|
||||||
|
expect(mocks.wristStore.setOpenVRAlways).toHaveBeenCalledTimes(1);
|
||||||
|
|
||||||
|
await radioGroups[1].get('[data-testid="radio-true"]').trigger('click');
|
||||||
|
expect(mocks.wristStore.setOverlaybutton).toHaveBeenCalledTimes(1);
|
||||||
|
|
||||||
|
await wrapper.get('[data-testid="toggle-right"]').trigger('click');
|
||||||
|
expect(mocks.wristStore.setOverlayHand).toHaveBeenCalledWith('2');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not toggle openVRAlways when the value is unchanged', async () => {
|
||||||
|
mocks.wristStore.openVRAlways.value = true;
|
||||||
|
const wrapper = mount(WristOverlaySettings);
|
||||||
|
|
||||||
|
const firstRadio = wrapper.findAll('[data-testid="radio-group"]')[0];
|
||||||
|
await firstRadio.get('[data-testid="radio-true"]').trigger('click');
|
||||||
|
|
||||||
|
expect(mocks.wristStore.setOpenVRAlways).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,141 @@
|
|||||||
|
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||||
|
import { mount } from '@vue/test-utils';
|
||||||
|
|
||||||
|
const mocks = vi.hoisted(() => ({
|
||||||
|
sharedFeedFilters: {
|
||||||
|
__v_isRef: true,
|
||||||
|
value: {
|
||||||
|
noty: { status: 'all' },
|
||||||
|
wrist: { status: 'none' }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
photonLoggingEnabled: { __v_isRef: true, value: false },
|
||||||
|
loadSharedFeed: vi.fn(),
|
||||||
|
setString: vi.fn()
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('pinia', async (i) => ({ ...(await i()), storeToRefs: (s) => s }));
|
||||||
|
vi.mock('vue-i18n', () => ({ useI18n: () => ({ t: (k) => k }) }));
|
||||||
|
|
||||||
|
vi.mock('../../../../stores', () => ({
|
||||||
|
usePhotonStore: () => ({ photonLoggingEnabled: mocks.photonLoggingEnabled }),
|
||||||
|
useNotificationsSettingsStore: () => ({ sharedFeedFilters: mocks.sharedFeedFilters }),
|
||||||
|
useSharedFeedStore: () => ({ loadSharedFeed: (...a) => mocks.loadSharedFeed(...a) })
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('../../../../services/config', () => ({
|
||||||
|
default: {
|
||||||
|
setString: (...a) => mocks.setString(...a)
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('../../../../shared/constants', () => ({
|
||||||
|
feedFiltersOptions: () => ({
|
||||||
|
notyFeedFiltersOptions: [
|
||||||
|
{
|
||||||
|
key: 'status',
|
||||||
|
name: 'Noty Status',
|
||||||
|
options: [{ label: 'all', textKey: 'all' }]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
wristFeedFiltersOptions: [
|
||||||
|
{
|
||||||
|
key: 'status',
|
||||||
|
name: 'Wrist Status',
|
||||||
|
options: [{ label: 'none', textKey: 'none' }]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
photonFeedFiltersOptions: []
|
||||||
|
}),
|
||||||
|
sharedFeedFiltersDefaults: {
|
||||||
|
noty: { status: 'default-noty' },
|
||||||
|
wrist: { status: 'default-wrist' }
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('@/components/ui/dialog', () => ({
|
||||||
|
Dialog: {
|
||||||
|
emits: ['update:open'],
|
||||||
|
template:
|
||||||
|
'<div><button data-testid="dialog-close" @click="$emit(\'update:open\', false)" /><slot /></div>'
|
||||||
|
},
|
||||||
|
DialogContent: { template: '<div><slot /></div>' },
|
||||||
|
DialogHeader: { template: '<div><slot /></div>' },
|
||||||
|
DialogTitle: { template: '<h2 data-testid="dialog-title"><slot /></h2>' },
|
||||||
|
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/toggle-group', () => ({
|
||||||
|
ToggleGroup: {
|
||||||
|
emits: ['update:model-value'],
|
||||||
|
template:
|
||||||
|
'<div><button data-testid="toggle-update" @click="$emit(\'update:model-value\', \'all\')" /><slot /></div>'
|
||||||
|
},
|
||||||
|
ToggleGroupItem: { template: '<button><slot /></button>' }
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('lucide-vue-next', () => ({
|
||||||
|
AlertTriangle: { template: '<i />' },
|
||||||
|
Info: { template: '<i />' }
|
||||||
|
}));
|
||||||
|
|
||||||
|
import FeedFiltersDialog from '../FeedFiltersDialog.vue';
|
||||||
|
|
||||||
|
describe('FeedFiltersDialog.vue', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
mocks.sharedFeedFilters.value = {
|
||||||
|
noty: { status: 'all' },
|
||||||
|
wrist: { status: 'none' }
|
||||||
|
};
|
||||||
|
mocks.loadSharedFeed.mockClear();
|
||||||
|
mocks.setString.mockClear();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders title by mode, saves filter change, and closes dialog', async () => {
|
||||||
|
const wrapper = mount(FeedFiltersDialog, {
|
||||||
|
props: {
|
||||||
|
feedFiltersDialogMode: 'wrist'
|
||||||
|
},
|
||||||
|
global: {
|
||||||
|
stubs: {
|
||||||
|
TooltipWrapper: { template: '<span><slot /></span>' }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(wrapper.get('[data-testid="dialog-title"]').text()).toBe('dialog.shared_feed_filters.wrist');
|
||||||
|
|
||||||
|
await wrapper.get('[data-testid="toggle-update"]').trigger('click');
|
||||||
|
expect(mocks.sharedFeedFilters.value.wrist.status).toBe('all');
|
||||||
|
expect(mocks.setString).toHaveBeenCalledWith(
|
||||||
|
'sharedFeedFilters',
|
||||||
|
JSON.stringify(mocks.sharedFeedFilters.value)
|
||||||
|
);
|
||||||
|
expect(mocks.loadSharedFeed).toHaveBeenCalledTimes(1);
|
||||||
|
|
||||||
|
const closeButton = wrapper.findAll('[data-testid="btn"]')[1];
|
||||||
|
await closeButton.trigger('click');
|
||||||
|
expect(wrapper.emitted('update:feedFiltersDialogMode')).toEqual([['']]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('resets noty filters to defaults', async () => {
|
||||||
|
const wrapper = mount(FeedFiltersDialog, {
|
||||||
|
props: {
|
||||||
|
feedFiltersDialogMode: 'noty'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const resetButton = wrapper.findAll('[data-testid="btn"]')[0];
|
||||||
|
await resetButton.trigger('click');
|
||||||
|
|
||||||
|
expect(mocks.sharedFeedFilters.value.noty).toEqual({ status: 'default-noty' });
|
||||||
|
expect(mocks.setString).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||||
|
import { mount } from '@vue/test-utils';
|
||||||
|
import { ref } from 'vue';
|
||||||
|
|
||||||
|
const mocks = vi.hoisted(() => ({
|
||||||
|
openSearch: vi.fn(),
|
||||||
|
markAllAsSeen: vi.fn(),
|
||||||
|
refreshFriends: vi.fn(),
|
||||||
|
hasUnseen: { value: true },
|
||||||
|
centerOpen: { value: false }
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('pinia', async (i) => ({ ...(await i()), storeToRefs: (s) => s }));
|
||||||
|
vi.mock('vue-i18n', () => ({ useI18n: () => ({ t: (k) => k }) }));
|
||||||
|
vi.mock('@vueuse/core', async (importOriginal) => {
|
||||||
|
const actual = await importOriginal();
|
||||||
|
return {
|
||||||
|
...actual,
|
||||||
|
useMagicKeys: () => ({}),
|
||||||
|
whenever: vi.fn()
|
||||||
|
};
|
||||||
|
});
|
||||||
|
vi.mock('../../../stores', () => ({
|
||||||
|
useFriendStore: () => ({ friends: ref(new Map()), isRefreshFriendsLoading: ref(false), onlineFriendCount: ref(0) }),
|
||||||
|
useGroupStore: () => ({ groupInstances: ref([]) }),
|
||||||
|
useNotificationStore: () => ({ isNotificationCenterOpen: mocks.centerOpen, hasUnseenNotifications: mocks.hasUnseen, markAllAsSeen: (...a) => mocks.markAllAsSeen(...a) }),
|
||||||
|
useAppearanceSettingsStore: () => ({ sidebarSortMethod1: ref(''), sidebarSortMethod2: ref(''), sidebarSortMethod3: ref(''), isSidebarGroupByInstance: ref(false), isHideFriendsInSameInstance: ref(false), isSidebarDivideByFriendGroup: ref(false), sidebarFavoriteGroups: ref([]), setSidebarSortMethod1: vi.fn(), setSidebarSortMethod2: vi.fn(), setSidebarSortMethod3: vi.fn(), setIsSidebarGroupByInstance: vi.fn(), setIsHideFriendsInSameInstance: vi.fn(), setIsSidebarDivideByFriendGroup: vi.fn(), setSidebarFavoriteGroups: vi.fn() }),
|
||||||
|
useFavoriteStore: () => ({ favoriteFriendGroups: ref([]), localFriendFavoriteGroups: ref([]) })
|
||||||
|
}));
|
||||||
|
vi.mock('../../../stores/globalSearch', () => ({ useGlobalSearchStore: () => ({ open: (...a) => mocks.openSearch(...a) }) }));
|
||||||
|
vi.mock('../../../coordinators/friendSyncCoordinator', () => ({ runRefreshFriendsListFlow: (...a) => mocks.refreshFriends(...a) }));
|
||||||
|
vi.mock('../sidebarSettingsUtils', () => ({ normalizeFavoriteGroupsChange: (v) => v, resolveFavoriteGroups: (v) => v }));
|
||||||
|
vi.mock('@/components/ui/button', () => ({ Button: { emits: ['click'], template: '<button data-testid="btn" @click="$emit(\'click\')"><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" @click="$emit(\'click\')"><slot /></button>' } }));
|
||||||
|
vi.mock('@/components/ui/popover', () => ({ Popover: { template: '<div><slot /></div>' }, PopoverTrigger: { template: '<div><slot /></div>' }, PopoverContent: { template: '<div><slot /></div>' } }));
|
||||||
|
vi.mock('@/components/ui/select', () => ({ Select: { template: '<div><slot /></div>' }, SelectTrigger: { template: '<div><slot /></div>' }, SelectValue: { template: '<div><slot /></div>' }, SelectContent: { template: '<div><slot /></div>' }, SelectGroup: { template: '<div><slot /></div>' }, SelectItem: { template: '<div><slot /></div>' }, SelectSeparator: { template: '<hr />' } }));
|
||||||
|
vi.mock('@/components/ui/field', () => ({ Field: { template: '<div><slot /></div>' }, FieldLabel: { template: '<div><slot /></div>' }, FieldContent: { template: '<div><slot /></div>' } }));
|
||||||
|
vi.mock('@/components/ui/tabs', () => ({ TabsUnderline: { template: '<div><slot name="friends" /><slot name="groups" /></div>' } }));
|
||||||
|
vi.mock('@/components/ui/switch', () => ({ Switch: { template: '<div />' } }));
|
||||||
|
vi.mock('@/components/ui/spinner', () => ({ Spinner: { template: '<div />' } }));
|
||||||
|
vi.mock('@/components/ui/tooltip', () => ({ TooltipWrapper: { template: '<div><slot /></div>' } }));
|
||||||
|
vi.mock('@/components/ui/kbd', () => ({ Kbd: { template: '<kbd><slot /></kbd>' } }));
|
||||||
|
vi.mock('@/components/ui/separator', () => ({ Separator: { template: '<hr />' } }));
|
||||||
|
vi.mock('lucide-vue-next', () => ({
|
||||||
|
Bell: { template: '<i />' },
|
||||||
|
RefreshCw: { template: '<i />' },
|
||||||
|
Search: { template: '<i />' },
|
||||||
|
Settings: { template: '<i />' }
|
||||||
|
}));
|
||||||
|
vi.mock('../components/FriendsSidebar.vue', () => ({ default: { template: '<div />' } }));
|
||||||
|
vi.mock('../components/GroupsSidebar.vue', () => ({ default: { template: '<div />' } }));
|
||||||
|
vi.mock('../components/GroupOrderSheet.vue', () => ({ default: { template: '<div />' } }));
|
||||||
|
vi.mock('../components/NotificationCenterSheet.vue', () => ({ default: { template: '<div />' } }));
|
||||||
|
vi.mock('../../../components/GlobalSearchDialog.vue', () => ({ default: { template: '<div />' } }));
|
||||||
|
|
||||||
|
import Sidebar from '../Sidebar.vue';
|
||||||
|
|
||||||
|
describe('Sidebar.vue', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
mocks.openSearch.mockClear();
|
||||||
|
mocks.markAllAsSeen.mockClear();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('opens global search and marks notifications read', async () => {
|
||||||
|
const wrapper = mount(Sidebar);
|
||||||
|
const buttons = wrapper.findAll('button');
|
||||||
|
for (const button of buttons) {
|
||||||
|
await button.trigger('click');
|
||||||
|
if (mocks.openSearch.mock.calls.length > 0) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await wrapper.get('[data-testid="ctx"]').trigger('click');
|
||||||
|
|
||||||
|
expect(mocks.openSearch).toHaveBeenCalled();
|
||||||
|
expect(mocks.markAllAsSeen).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,145 @@
|
|||||||
|
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||||
|
import { mount } from '@vue/test-utils';
|
||||||
|
|
||||||
|
const mocks = vi.hoisted(() => ({
|
||||||
|
isAgeGatedInstancesVisible: { value: true },
|
||||||
|
groupInstances: {
|
||||||
|
value: [
|
||||||
|
{
|
||||||
|
group: {
|
||||||
|
groupId: 'grp_1',
|
||||||
|
name: 'Group One',
|
||||||
|
iconUrl: 'https://example.com/icon.png'
|
||||||
|
},
|
||||||
|
instance: {
|
||||||
|
id: 'inst_1',
|
||||||
|
ownerId: 'usr_owner',
|
||||||
|
userCount: 1,
|
||||||
|
capacity: 16,
|
||||||
|
location: 'wrld_1:123'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
sortGroupInstancesByInGame: (a, b) => a[0].group.name.localeCompare(b[0].group.name),
|
||||||
|
showLaunchDialog: vi.fn(),
|
||||||
|
checkCanInviteSelf: vi.fn(() => true),
|
||||||
|
selfInvite: vi.fn().mockResolvedValue({}),
|
||||||
|
showGroupDialog: vi.fn(),
|
||||||
|
toastSuccess: vi.fn()
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('pinia', async (i) => ({ ...(await i()), storeToRefs: (s) => s }));
|
||||||
|
vi.mock('vue-i18n', () => ({ useI18n: () => ({ t: (k) => k }) }));
|
||||||
|
|
||||||
|
vi.mock('@tanstack/vue-virtual', () => ({
|
||||||
|
useVirtualizer: (optionsRef) => ({
|
||||||
|
value: {
|
||||||
|
getVirtualItems: () =>
|
||||||
|
Array.from({ length: optionsRef.value.count }, (_, index) => ({
|
||||||
|
index,
|
||||||
|
key: optionsRef.value.getItemKey(index),
|
||||||
|
start: index * 52
|
||||||
|
})),
|
||||||
|
getTotalSize: () => optionsRef.value.count * 52,
|
||||||
|
measure: vi.fn(),
|
||||||
|
measureElement: vi.fn()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('../../../../stores', () => ({
|
||||||
|
useAppearanceSettingsStore: () => ({
|
||||||
|
isAgeGatedInstancesVisible: mocks.isAgeGatedInstancesVisible
|
||||||
|
}),
|
||||||
|
useGroupStore: () => ({
|
||||||
|
sortGroupInstancesByInGame: mocks.sortGroupInstancesByInGame,
|
||||||
|
groupInstances: mocks.groupInstances
|
||||||
|
}),
|
||||||
|
useLaunchStore: () => ({ showLaunchDialog: (...a) => mocks.showLaunchDialog(...a) })
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('../../../../composables/useInviteChecks', () => ({
|
||||||
|
useInviteChecks: () => ({ checkCanInviteSelf: (...a) => mocks.checkCanInviteSelf(...a) })
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('../../../../shared/utils', () => ({
|
||||||
|
convertFileUrlToImageUrl: (url) => `${url}?small`,
|
||||||
|
parseLocation: (location) => ({
|
||||||
|
isRealInstance: !!location,
|
||||||
|
worldId: location.split(':')[0],
|
||||||
|
instanceId: location.split(':')[1]
|
||||||
|
})
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('../../../../coordinators/groupCoordinator', () => ({
|
||||||
|
showGroupDialog: (...a) => mocks.showGroupDialog(...a)
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('../../../../api', () => ({
|
||||||
|
instanceRequest: {
|
||||||
|
selfInvite: (...a) => mocks.selfInvite(...a)
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('vue-sonner', () => ({
|
||||||
|
toast: {
|
||||||
|
success: (...a) => mocks.toastSuccess(...a)
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('../../../../components/ui/context-menu', () => ({
|
||||||
|
ContextMenu: { template: '<div><slot /></div>' },
|
||||||
|
ContextMenuTrigger: { template: '<div><slot /></div>' },
|
||||||
|
ContextMenuContent: { template: '<div><slot /></div>' },
|
||||||
|
ContextMenuItem: {
|
||||||
|
emits: ['click'],
|
||||||
|
props: ['disabled'],
|
||||||
|
template:
|
||||||
|
'<button data-testid="ctx-item" :disabled="disabled" @click="$emit(\'click\')"><slot /></button>'
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('../../../../components/BackToTop.vue', () => ({
|
||||||
|
default: { template: '<div data-testid="back-to-top" />' }
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('../../../../components/Location.vue', () => ({
|
||||||
|
default: { props: ['location'], template: '<span data-testid="location">{{ location }}</span>' }
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('lucide-vue-next', () => ({
|
||||||
|
ChevronDown: { template: '<i />' }
|
||||||
|
}));
|
||||||
|
|
||||||
|
import GroupsSidebar from '../GroupsSidebar.vue';
|
||||||
|
|
||||||
|
describe('GroupsSidebar.vue', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
mocks.showLaunchDialog.mockClear();
|
||||||
|
mocks.selfInvite.mockClear();
|
||||||
|
mocks.showGroupDialog.mockClear();
|
||||||
|
mocks.toastSuccess.mockClear();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders group rows and handles launch/self-invite actions', async () => {
|
||||||
|
const wrapper = mount(GroupsSidebar);
|
||||||
|
|
||||||
|
expect(wrapper.text()).toContain('Group One');
|
||||||
|
|
||||||
|
await wrapper.get('[data-testid="location"]').trigger('click');
|
||||||
|
expect(mocks.showGroupDialog).toHaveBeenCalledWith('usr_owner');
|
||||||
|
|
||||||
|
const items = wrapper.findAll('[data-testid="ctx-item"]');
|
||||||
|
await items[0].trigger('click');
|
||||||
|
expect(mocks.showLaunchDialog).toHaveBeenCalledWith('wrld_1:123');
|
||||||
|
|
||||||
|
await items[1].trigger('click');
|
||||||
|
await Promise.resolve();
|
||||||
|
expect(mocks.selfInvite).toHaveBeenCalledWith({
|
||||||
|
worldId: 'wrld_1',
|
||||||
|
instanceId: '123'
|
||||||
|
});
|
||||||
|
expect(mocks.toastSuccess).toHaveBeenCalledWith('message.invite.self_sent');
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||||
|
import { mount } from '@vue/test-utils';
|
||||||
|
|
||||||
|
const mocks = vi.hoisted(() => ({
|
||||||
|
followGroupEvent: vi.fn(async () => ({ json: { ok: true } })),
|
||||||
|
showFullscreenImageDialog: vi.fn(),
|
||||||
|
writeText: vi.fn(),
|
||||||
|
toastSuccess: vi.fn()
|
||||||
|
}));
|
||||||
|
|
||||||
|
Object.assign(globalThis, { navigator: { clipboard: { writeText: (...a) => mocks.writeText(...a) } } });
|
||||||
|
|
||||||
|
vi.mock('vue-i18n', () => ({ useI18n: () => ({ t: (k) => k }) }));
|
||||||
|
vi.mock('../../../../stores', () => ({
|
||||||
|
useGalleryStore: () => ({ showFullscreenImageDialog: (...a) => mocks.showFullscreenImageDialog(...a) }),
|
||||||
|
useGroupStore: () => ({ cachedGroups: new Map([['grp_1', { name: 'Group One', bannerUrl: 'https://example.com/banner.png' }]]) })
|
||||||
|
}));
|
||||||
|
vi.mock('../../../../services/appConfig', () => ({ AppDebug: { endpointDomain: 'https://api.example.com' } }));
|
||||||
|
vi.mock('../../../../shared/utils', () => ({ formatDateFilter: () => '12:00' }));
|
||||||
|
vi.mock('../../../../api', () => ({ groupRequest: { followGroupEvent: (...a) => mocks.followGroupEvent(...a) } }));
|
||||||
|
vi.mock('vue-sonner', () => ({ toast: { success: (...a) => mocks.toastSuccess(...a), error: vi.fn() } }));
|
||||||
|
vi.mock('@/components/ui/popover', () => ({ Popover: { template: '<div><slot /></div>' }, PopoverTrigger: { template: '<div><slot /></div>' }, PopoverContent: { template: '<div><slot /></div>' } }));
|
||||||
|
vi.mock('@/components/ui/card', () => ({ Card: { 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', () => ({
|
||||||
|
Calendar: { template: '<i />' },
|
||||||
|
Download: { template: '<i />' },
|
||||||
|
Share2: { template: '<i />' },
|
||||||
|
Star: { template: '<i />' }
|
||||||
|
}));
|
||||||
|
|
||||||
|
import GroupCalendarEventCard from '../GroupCalendarEventCard.vue';
|
||||||
|
|
||||||
|
function mountCard() {
|
||||||
|
return mount(GroupCalendarEventCard, {
|
||||||
|
props: {
|
||||||
|
event: { id: 'evt_1', ownerId: 'grp_1', title: 'Event One', startsAt: '2026-01-01', endsAt: '2026-01-01', accessType: 'public', category: 'social', interestedUserCount: 2, closeInstanceAfterEndMinutes: 30, createdAt: '2026-01-01', description: 'desc', imageUrl: '' },
|
||||||
|
mode: 'timeline',
|
||||||
|
isFollowing: false
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('GroupCalendarEventCard.vue', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
mocks.followGroupEvent.mockClear();
|
||||||
|
mocks.writeText.mockClear();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('copies event link and toggles follow', async () => {
|
||||||
|
const wrapper = mountCard();
|
||||||
|
const buttons = wrapper.findAll('[data-testid="btn"]');
|
||||||
|
|
||||||
|
await buttons[0].trigger('click');
|
||||||
|
await buttons[1].trigger('click');
|
||||||
|
await Promise.resolve();
|
||||||
|
|
||||||
|
expect(mocks.writeText).toHaveBeenCalledWith('https://vrchat.com/home/group/grp_1/calendar/evt_1');
|
||||||
|
expect(mocks.followGroupEvent).toHaveBeenCalledWith({ groupId: 'grp_1', eventId: 'evt_1', isFollowing: true });
|
||||||
|
expect(wrapper.emitted('update-following-calendar-data')).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,159 @@
|
|||||||
|
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||||
|
import { mount } from '@vue/test-utils';
|
||||||
|
|
||||||
|
const mocks = vi.hoisted(() => ({
|
||||||
|
generalStore: {
|
||||||
|
autoStateChangeEnabled: { __v_isRef: true, value: true },
|
||||||
|
autoStateChangeAloneStatus: { __v_isRef: true, value: 'active' },
|
||||||
|
autoStateChangeCompanyStatus: { __v_isRef: true, value: 'busy' },
|
||||||
|
autoStateChangeInstanceTypes: { __v_isRef: true, value: ['invite'] },
|
||||||
|
autoStateChangeNoFriends: { __v_isRef: true, value: false },
|
||||||
|
autoStateChangeAloneDescEnabled: { __v_isRef: true, value: false },
|
||||||
|
autoStateChangeAloneDesc: { __v_isRef: true, value: '' },
|
||||||
|
autoStateChangeCompanyDescEnabled: { __v_isRef: true, value: false },
|
||||||
|
autoStateChangeCompanyDesc: { __v_isRef: true, value: '' },
|
||||||
|
autoStateChangeGroups: { __v_isRef: true, value: [] },
|
||||||
|
autoAcceptInviteRequests: { __v_isRef: true, value: 'Off' },
|
||||||
|
autoAcceptInviteGroups: { __v_isRef: true, value: [] },
|
||||||
|
setAutoStateChangeEnabled: vi.fn(),
|
||||||
|
setAutoStateChangeAloneStatus: vi.fn(),
|
||||||
|
setAutoStateChangeCompanyStatus: vi.fn(),
|
||||||
|
setAutoStateChangeInstanceTypes: vi.fn(),
|
||||||
|
setAutoStateChangeNoFriends: vi.fn(),
|
||||||
|
setAutoStateChangeAloneDescEnabled: vi.fn(),
|
||||||
|
setAutoStateChangeAloneDesc: vi.fn(),
|
||||||
|
setAutoStateChangeCompanyDescEnabled: vi.fn(),
|
||||||
|
setAutoStateChangeCompanyDesc: vi.fn(),
|
||||||
|
setAutoStateChangeGroups: vi.fn(),
|
||||||
|
setAutoAcceptInviteRequests: vi.fn(),
|
||||||
|
setAutoAcceptInviteGroups: vi.fn()
|
||||||
|
},
|
||||||
|
favoriteStore: {
|
||||||
|
favoriteFriendGroups: {
|
||||||
|
__v_isRef: true,
|
||||||
|
value: [{ key: 'grp_a', displayName: 'Group A' }]
|
||||||
|
},
|
||||||
|
localFriendFavoriteGroups: { __v_isRef: true, value: ['Local A'] }
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('pinia', async (i) => ({ ...(await i()), storeToRefs: (s) => s }));
|
||||||
|
vi.mock('vue-i18n', () => ({ useI18n: () => ({ t: (k) => k }) }));
|
||||||
|
|
||||||
|
vi.mock('../../../../stores', () => ({
|
||||||
|
useGeneralSettingsStore: () => mocks.generalStore,
|
||||||
|
useFavoriteStore: () => mocks.favoriteStore
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('../../../../shared/constants', () => ({
|
||||||
|
accessTypeLocaleKeyMap: {
|
||||||
|
invite: 'access.invite',
|
||||||
|
invitePlus: 'access.invite_plus',
|
||||||
|
friends: 'access.friends',
|
||||||
|
friendsPlus: 'access.friends_plus',
|
||||||
|
public: 'access.public',
|
||||||
|
groupPublic: 'access.group_public',
|
||||||
|
groupPlus: 'access.group_plus',
|
||||||
|
groupMembers: 'access.group_members',
|
||||||
|
group: 'access.group'
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('@/components/ui/dialog', () => ({
|
||||||
|
Dialog: {
|
||||||
|
emits: ['update:open'],
|
||||||
|
template:
|
||||||
|
'<div><button data-testid="dialog-close" @click="$emit(\'update:open\', false)" /><slot /></div>'
|
||||||
|
},
|
||||||
|
DialogContent: { template: '<div><slot /></div>' },
|
||||||
|
DialogHeader: { template: '<div><slot /></div>' },
|
||||||
|
DialogTitle: { template: '<h2><slot /></h2>' }
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('@/components/ui/field', () => ({
|
||||||
|
Field: { template: '<div><slot /></div>' },
|
||||||
|
FieldContent: { template: '<div><slot /></div>' },
|
||||||
|
FieldGroup: { template: '<div><slot /></div>' },
|
||||||
|
FieldLabel: { template: '<div><slot /></div>' },
|
||||||
|
FieldSeparator: { template: '<div><slot /></div>' }
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('@/components/ui/select', () => ({
|
||||||
|
Select: { template: '<div><slot /></div>' },
|
||||||
|
SelectContent: { template: '<div><slot /></div>' },
|
||||||
|
SelectGroup: { template: '<div><slot /></div>' },
|
||||||
|
SelectItem: { template: '<div><slot /></div>' },
|
||||||
|
SelectSeparator: { template: '<div><slot /></div>' },
|
||||||
|
SelectTrigger: { template: '<div><slot /></div>' },
|
||||||
|
SelectValue: { template: '<div><slot /></div>' }
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('@/components/ui/radio-group', () => ({
|
||||||
|
RadioGroup: {
|
||||||
|
emits: ['update:modelValue'],
|
||||||
|
template:
|
||||||
|
'<div data-testid="radio-group"><button class="emit-false" @click="$emit(\'update:modelValue\', \'false\')" /><button class="emit-true" @click="$emit(\'update:modelValue\', \'true\')" /><button class="emit-all" @click="$emit(\'update:modelValue\', \'All Favorites\')" /><slot /></div>'
|
||||||
|
},
|
||||||
|
RadioGroupItem: { template: '<div />' }
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('@/components/ui/input', () => ({
|
||||||
|
Input: { template: '<input />' }
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('../../../Settings/components/SimpleSwitch.vue', () => ({
|
||||||
|
default: {
|
||||||
|
props: ['label'],
|
||||||
|
emits: ['change'],
|
||||||
|
template:
|
||||||
|
'<div data-testid="simple-switch" :data-label="label"><button class="emit-true" @click="$emit(\'change\', true)" /><button class="emit-false" @click="$emit(\'change\', false)" /></div>'
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
import AutoChangeStatusDialog from '../AutoChangeStatusDialog.vue';
|
||||||
|
|
||||||
|
describe('AutoChangeStatusDialog.vue', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
mocks.generalStore.autoStateChangeNoFriends.value = false;
|
||||||
|
mocks.generalStore.autoAcceptInviteRequests.value = 'Off';
|
||||||
|
mocks.generalStore.setAutoStateChangeNoFriends.mockClear();
|
||||||
|
mocks.generalStore.setAutoAcceptInviteRequests.mockClear();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('emits close when dialog is closed', async () => {
|
||||||
|
const wrapper = mount(AutoChangeStatusDialog, {
|
||||||
|
props: { isAutoChangeStatusDialogVisible: true }
|
||||||
|
});
|
||||||
|
|
||||||
|
await wrapper.get('[data-testid="dialog-close"]').trigger('click');
|
||||||
|
|
||||||
|
expect(wrapper.emitted('close')).toEqual([[]]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles auto accept switch and alone-condition radio changes', async () => {
|
||||||
|
const wrapper = mount(AutoChangeStatusDialog, {
|
||||||
|
props: { isAutoChangeStatusDialogVisible: true }
|
||||||
|
});
|
||||||
|
|
||||||
|
const autoAcceptSwitch = wrapper
|
||||||
|
.findAll('[data-testid="simple-switch"]')
|
||||||
|
.find((node) =>
|
||||||
|
node
|
||||||
|
.attributes('data-label')
|
||||||
|
?.includes('view.settings.general.automation.auto_invite_request_accept')
|
||||||
|
);
|
||||||
|
|
||||||
|
await autoAcceptSwitch.find('.emit-false').trigger('click');
|
||||||
|
expect(mocks.generalStore.setAutoAcceptInviteRequests).toHaveBeenCalledWith('Off');
|
||||||
|
|
||||||
|
await autoAcceptSwitch.find('.emit-true').trigger('click');
|
||||||
|
expect(mocks.generalStore.setAutoAcceptInviteRequests).toHaveBeenCalledWith('All Favorites');
|
||||||
|
|
||||||
|
const noFriendsRadio = wrapper.findAll('[data-testid="radio-group"]')[0];
|
||||||
|
await noFriendsRadio.find('.emit-false').trigger('click');
|
||||||
|
expect(mocks.generalStore.setAutoStateChangeNoFriends).not.toHaveBeenCalled();
|
||||||
|
|
||||||
|
await noFriendsRadio.find('.emit-true').trigger('click');
|
||||||
|
expect(mocks.generalStore.setAutoStateChangeNoFriends).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user