mirror of
https://github.com/MrUnknownDE/VRCX.git
synced 2026-05-01 20:53:45 +02:00
add context menu to friends locations card
This commit is contained in:
@@ -0,0 +1,464 @@
|
||||
import { beforeEach, describe, expect, test, vi } from 'vitest';
|
||||
import { createI18n } from 'vue-i18n';
|
||||
import { createTestingPinia } from '@pinia/testing';
|
||||
import { mount } from '@vue/test-utils';
|
||||
import { ref } from 'vue';
|
||||
|
||||
import FriendsLocationsCard from '../FriendsLocationsCard.vue';
|
||||
import en from '../../../../localization/en.json';
|
||||
|
||||
vi.mock('../../../../views/Feed/Feed.vue', () => ({
|
||||
default: { template: '<div />' }
|
||||
}));
|
||||
vi.mock('../../../../views/Feed/columns.jsx', () => ({
|
||||
columns: []
|
||||
}));
|
||||
vi.mock('../../../../plugin/router', () => ({
|
||||
router: {
|
||||
beforeEach: vi.fn(),
|
||||
push: vi.fn(),
|
||||
replace: vi.fn(),
|
||||
currentRoute: ref({ path: '/', name: '', meta: {} }),
|
||||
isReady: vi.fn().mockResolvedValue(true)
|
||||
},
|
||||
initRouter: vi.fn()
|
||||
}));
|
||||
vi.mock('vue-router', async (importOriginal) => {
|
||||
const actual = await importOriginal();
|
||||
return {
|
||||
...actual,
|
||||
useRouter: vi.fn(() => ({
|
||||
push: vi.fn(),
|
||||
replace: vi.fn(),
|
||||
currentRoute: ref({ path: '/', name: '', meta: {} })
|
||||
}))
|
||||
};
|
||||
});
|
||||
vi.mock('../../../../plugin/interopApi', () => ({
|
||||
initInteropApi: vi.fn()
|
||||
}));
|
||||
vi.mock('../../../../service/database', () => ({
|
||||
database: new Proxy(
|
||||
{},
|
||||
{
|
||||
get: (_target, prop) => {
|
||||
if (prop === '__esModule') return false;
|
||||
return vi.fn().mockResolvedValue(null);
|
||||
}
|
||||
}
|
||||
)
|
||||
}));
|
||||
vi.mock('../../../../service/config', () => ({
|
||||
default: {
|
||||
init: vi.fn(),
|
||||
getString: vi
|
||||
.fn()
|
||||
.mockImplementation((_key, defaultValue) => defaultValue ?? '{}'),
|
||||
setString: vi.fn(),
|
||||
getBool: vi
|
||||
.fn()
|
||||
.mockImplementation((_key, defaultValue) => defaultValue ?? false),
|
||||
setBool: vi.fn(),
|
||||
getInt: vi
|
||||
.fn()
|
||||
.mockImplementation((_key, defaultValue) => defaultValue ?? 0),
|
||||
setInt: vi.fn(),
|
||||
getFloat: vi
|
||||
.fn()
|
||||
.mockImplementation((_key, defaultValue) => defaultValue ?? 0),
|
||||
setFloat: vi.fn(),
|
||||
getObject: vi.fn().mockReturnValue(null),
|
||||
setObject: vi.fn(),
|
||||
getArray: vi.fn().mockReturnValue([]),
|
||||
setArray: vi.fn(),
|
||||
remove: vi.fn()
|
||||
}
|
||||
}));
|
||||
vi.mock('../../../../service/jsonStorage', () => ({
|
||||
default: vi.fn()
|
||||
}));
|
||||
vi.mock('../../../../service/watchState', () => ({
|
||||
watchState: { isLoggedIn: false }
|
||||
}));
|
||||
vi.mock('../../../../shared/utils/world', () => ({
|
||||
getWorldName: vi.fn().mockResolvedValue(''),
|
||||
isRpcWorld: vi.fn().mockReturnValue(false)
|
||||
}));
|
||||
vi.mock('../../../../shared/utils/group', () => ({
|
||||
getGroupName: vi.fn().mockResolvedValue(''),
|
||||
hasGroupPermission: vi.fn().mockReturnValue(false),
|
||||
hasGroupModerationPermission: vi.fn().mockReturnValue(false)
|
||||
}));
|
||||
|
||||
const {
|
||||
mockSendRequestInvite,
|
||||
mockSendInvite,
|
||||
mockSelfInvite,
|
||||
mockGetCachedWorld
|
||||
} = vi.hoisted(() => ({
|
||||
mockSendRequestInvite: vi.fn().mockResolvedValue({}),
|
||||
mockSendInvite: vi.fn().mockResolvedValue({}),
|
||||
mockSelfInvite: vi.fn().mockResolvedValue({}),
|
||||
mockGetCachedWorld: vi
|
||||
.fn()
|
||||
.mockResolvedValue({ ref: { name: 'Test World' } })
|
||||
}));
|
||||
|
||||
vi.mock('../../../../api', () => {
|
||||
const p = (overrides = {}) =>
|
||||
new Proxy(overrides, {
|
||||
get: (target, prop) => {
|
||||
if (prop in target) return target[prop];
|
||||
if (prop === '__esModule') return false;
|
||||
return vi.fn().mockResolvedValue({});
|
||||
}
|
||||
});
|
||||
return {
|
||||
request: p(),
|
||||
userRequest: p(),
|
||||
worldRequest: p({
|
||||
getCachedWorld: (...args) => mockGetCachedWorld(...args)
|
||||
}),
|
||||
instanceRequest: p({
|
||||
selfInvite: (...args) => mockSelfInvite(...args)
|
||||
}),
|
||||
friendRequest: p(),
|
||||
avatarRequest: p(),
|
||||
notificationRequest: p({
|
||||
sendRequestInvite: (...args) => mockSendRequestInvite(...args),
|
||||
sendInvite: (...args) => mockSendInvite(...args)
|
||||
}),
|
||||
playerModerationRequest: p(),
|
||||
avatarModerationRequest: p(),
|
||||
favoriteRequest: p(),
|
||||
vrcPlusIconRequest: p(),
|
||||
vrcPlusImageRequest: p(),
|
||||
inviteMessagesRequest: p(),
|
||||
miscRequest: p(),
|
||||
authRequest: p(),
|
||||
groupRequest: p(),
|
||||
inventoryRequest: p(),
|
||||
propRequest: p(),
|
||||
imageRequest: p()
|
||||
};
|
||||
});
|
||||
|
||||
const i18n = createI18n({
|
||||
locale: 'en',
|
||||
fallbackLocale: 'en',
|
||||
legacy: false,
|
||||
globalInjection: false,
|
||||
missingWarn: false,
|
||||
fallbackWarn: false,
|
||||
messages: { en }
|
||||
});
|
||||
|
||||
// Stub all complex UI components — render slots transparently
|
||||
const stubs = {
|
||||
ContextMenu: { template: '<div data-testid="context-menu"><slot /></div>' },
|
||||
ContextMenuTrigger: {
|
||||
template: '<div data-testid="context-menu-trigger"><slot /></div>',
|
||||
props: ['as-child']
|
||||
},
|
||||
ContextMenuContent: {
|
||||
template: '<div data-testid="context-menu-content"><slot /></div>'
|
||||
},
|
||||
ContextMenuItem: {
|
||||
template:
|
||||
'<button data-testid="context-menu-item" :data-disabled="disabled" @click="$emit(\'click\')"><slot /></button>',
|
||||
props: ['disabled'],
|
||||
emits: ['click']
|
||||
},
|
||||
ContextMenuSeparator: {
|
||||
template: '<hr data-testid="context-menu-separator" />'
|
||||
},
|
||||
Card: {
|
||||
template: '<div data-testid="card"><slot /></div>',
|
||||
props: ['class', 'style']
|
||||
},
|
||||
Avatar: { template: '<div><slot /></div>', props: ['class', 'style'] },
|
||||
AvatarImage: { template: '<img />', props: ['src'] },
|
||||
AvatarFallback: { template: '<span><slot /></span>' },
|
||||
Location: {
|
||||
template: '<span class="location-stub" />',
|
||||
props: ['location', 'traveling', 'link', 'class']
|
||||
},
|
||||
Pencil: { template: '<span class="pencil-icon" />', props: ['class'] },
|
||||
TooltipWrapper: {
|
||||
template: '<span><slot /></span>',
|
||||
props: ['content', 'disabled', 'delayDuration', 'side']
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* @param overrides
|
||||
*/
|
||||
function makeFriend(overrides = {}) {
|
||||
return {
|
||||
id: 'usr_test123',
|
||||
name: 'TestUser',
|
||||
state: 'online',
|
||||
status: 'active',
|
||||
ref: {
|
||||
location: 'wrld_12345:67890~region(us)',
|
||||
travelingToLocation: '',
|
||||
statusDescription: 'Hello World',
|
||||
status: 'active'
|
||||
},
|
||||
pendingOffline: false,
|
||||
worldName: 'Test World',
|
||||
...overrides
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param props
|
||||
* @param storeState
|
||||
*/
|
||||
function mountCard(props = {}, storeState = {}) {
|
||||
const friend = props.friend ?? makeFriend();
|
||||
return mount(FriendsLocationsCard, {
|
||||
props: { friend, ...props },
|
||||
global: {
|
||||
plugins: [
|
||||
i18n,
|
||||
createTestingPinia({
|
||||
stubActions: true,
|
||||
initialState: {
|
||||
Game: {
|
||||
isGameRunning: storeState.isGameRunning ?? false
|
||||
},
|
||||
Location: {
|
||||
lastLocation: storeState.lastLocation ?? {
|
||||
location: 'wrld_abc:123~region(us)'
|
||||
},
|
||||
lastLocationDestination:
|
||||
storeState.lastLocationDestination ?? ''
|
||||
},
|
||||
User: {
|
||||
currentUser: storeState.currentUser ?? {
|
||||
isBoopingEnabled: true
|
||||
}
|
||||
},
|
||||
Launch: {},
|
||||
Instance: {},
|
||||
World: {},
|
||||
Search: {},
|
||||
AppearanceSettings: { showInstanceIdInLocation: false },
|
||||
Group: {}
|
||||
}
|
||||
})
|
||||
],
|
||||
stubs
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param wrapper
|
||||
*/
|
||||
function getMenuItems(wrapper) {
|
||||
return wrapper.findAll('[data-testid="context-menu-item"]');
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param wrapper
|
||||
*/
|
||||
function getMenuItemTexts(wrapper) {
|
||||
return getMenuItems(wrapper).map((item) => item.text().trim());
|
||||
}
|
||||
|
||||
describe('FriendsLocationsCard.vue', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('basic rendering', () => {
|
||||
test('renders friend name', () => {
|
||||
const wrapper = mountCard();
|
||||
expect(wrapper.text()).toContain('TestUser');
|
||||
});
|
||||
|
||||
test('renders status description', () => {
|
||||
const wrapper = mountCard();
|
||||
expect(wrapper.text()).toContain('Hello World');
|
||||
});
|
||||
|
||||
test('renders avatar fallback from first letter of name', () => {
|
||||
const wrapper = mountCard({
|
||||
friend: makeFriend({ name: 'Alice' })
|
||||
});
|
||||
expect(wrapper.text()).toContain('A');
|
||||
});
|
||||
|
||||
test('hides location when displayInstanceInfo is false', () => {
|
||||
const wrapper = mountCard({ displayInstanceInfo: false });
|
||||
expect(wrapper.find('.location-stub').exists()).toBe(false);
|
||||
});
|
||||
|
||||
test('shows location when displayInstanceInfo is true', () => {
|
||||
const wrapper = mountCard({ displayInstanceInfo: true });
|
||||
expect(wrapper.find('.location-stub').exists()).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('context menu visibility', () => {
|
||||
test('shows Request Invite for online friends', () => {
|
||||
const wrapper = mountCard({
|
||||
friend: makeFriend({ state: 'online' })
|
||||
});
|
||||
const texts = getMenuItemTexts(wrapper);
|
||||
expect(texts).toContain('Request Invite');
|
||||
});
|
||||
|
||||
test('hides Request Invite for non-online friends', () => {
|
||||
const wrapper = mountCard({
|
||||
friend: makeFriend({ state: 'active' })
|
||||
});
|
||||
const texts = getMenuItemTexts(wrapper);
|
||||
expect(texts).not.toContain('Request Invite');
|
||||
});
|
||||
|
||||
test('shows Invite when game is running', () => {
|
||||
const wrapper = mountCard({}, { isGameRunning: true });
|
||||
const texts = getMenuItemTexts(wrapper);
|
||||
expect(texts).toContain('Invite');
|
||||
});
|
||||
|
||||
test('hides Invite when game is not running', () => {
|
||||
const wrapper = mountCard({}, { isGameRunning: false });
|
||||
const texts = getMenuItemTexts(wrapper);
|
||||
expect(texts).not.toContain('Invite');
|
||||
});
|
||||
|
||||
test('always shows Send Boop', () => {
|
||||
const wrapper = mountCard(
|
||||
{ friend: makeFriend({ state: 'active' }) },
|
||||
{ isGameRunning: false }
|
||||
);
|
||||
const texts = getMenuItemTexts(wrapper);
|
||||
expect(texts).toContain('Send Boop');
|
||||
});
|
||||
|
||||
test('shows Launch/Invite and Invite Yourself for online friends with real location', () => {
|
||||
const wrapper = mountCard({
|
||||
friend: makeFriend({
|
||||
state: 'online',
|
||||
ref: { location: 'wrld_12345:67890~region(us)' }
|
||||
})
|
||||
});
|
||||
const texts = getMenuItemTexts(wrapper);
|
||||
expect(texts).toContain('Launch/Invite');
|
||||
expect(texts).toContain('Invite Yourself');
|
||||
});
|
||||
|
||||
test('hides Launch/Invite and Invite Yourself for friends without real location', () => {
|
||||
const wrapper = mountCard({
|
||||
friend: makeFriend({
|
||||
state: 'online',
|
||||
ref: { location: 'private' }
|
||||
})
|
||||
});
|
||||
const texts = getMenuItemTexts(wrapper);
|
||||
expect(texts).not.toContain('Launch/Invite');
|
||||
expect(texts).not.toContain('Invite Yourself');
|
||||
});
|
||||
|
||||
test('hides Launch/Invite and Invite Yourself for non-online friends', () => {
|
||||
const wrapper = mountCard({
|
||||
friend: makeFriend({
|
||||
state: 'active',
|
||||
ref: { location: 'wrld_12345:67890~region(us)' }
|
||||
})
|
||||
});
|
||||
const texts = getMenuItemTexts(wrapper);
|
||||
expect(texts).not.toContain('Launch/Invite');
|
||||
expect(texts).not.toContain('Invite Yourself');
|
||||
});
|
||||
|
||||
test('shows separator when friend is online with real location', () => {
|
||||
const wrapper = mountCard({
|
||||
friend: makeFriend({
|
||||
state: 'online',
|
||||
ref: { location: 'wrld_12345:67890~region(us)' }
|
||||
})
|
||||
});
|
||||
expect(
|
||||
wrapper.find('[data-testid="context-menu-separator"]').exists()
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
test('hides separator when friend has no real location', () => {
|
||||
const wrapper = mountCard({
|
||||
friend: makeFriend({
|
||||
state: 'online',
|
||||
ref: { location: 'private' }
|
||||
})
|
||||
});
|
||||
expect(
|
||||
wrapper.find('[data-testid="context-menu-separator"]').exists()
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('context menu disabled states', () => {
|
||||
test('Send Boop is disabled when booping is not enabled', () => {
|
||||
const wrapper = mountCard(
|
||||
{},
|
||||
{ currentUser: { isBoopingEnabled: false } }
|
||||
);
|
||||
const boopItem = getMenuItems(wrapper).find(
|
||||
(item) => item.text().trim() === 'Send Boop'
|
||||
);
|
||||
expect(boopItem?.attributes('data-disabled')).toBe('true');
|
||||
});
|
||||
|
||||
test('Send Boop is enabled when booping is enabled', () => {
|
||||
const wrapper = mountCard(
|
||||
{},
|
||||
{ currentUser: { isBoopingEnabled: true } }
|
||||
);
|
||||
const boopItem = getMenuItems(wrapper).find(
|
||||
(item) => item.text().trim() === 'Send Boop'
|
||||
);
|
||||
expect(boopItem?.attributes('data-disabled')).toBe('false');
|
||||
});
|
||||
});
|
||||
|
||||
describe('context menu actions', () => {
|
||||
test('friendRequestInvite calls sendRequestInvite API', async () => {
|
||||
const wrapper = mountCard({
|
||||
friend: makeFriend({ state: 'online' })
|
||||
});
|
||||
const requestInviteItem = getMenuItems(wrapper).find(
|
||||
(item) => item.text().trim() === 'Request Invite'
|
||||
);
|
||||
await requestInviteItem.trigger('click');
|
||||
expect(mockSendRequestInvite).toHaveBeenCalledWith(
|
||||
{ platform: 'standalonewindows' },
|
||||
'usr_test123'
|
||||
);
|
||||
});
|
||||
|
||||
test('friendInviteSelf calls selfInvite API', async () => {
|
||||
const wrapper = mountCard({
|
||||
friend: makeFriend({
|
||||
state: 'online',
|
||||
ref: { location: 'wrld_12345:67890~region(us)' }
|
||||
})
|
||||
});
|
||||
const selfInviteItem = getMenuItems(wrapper).find(
|
||||
(item) => item.text().trim() === 'Invite Yourself'
|
||||
);
|
||||
await selfInviteItem.trigger('click');
|
||||
expect(mockSelfInvite).toHaveBeenCalledWith({
|
||||
instanceId: '67890~region(us)',
|
||||
worldId: 'wrld_12345'
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user