mirror of
https://github.com/MrUnknownDE/VRCX.git
synced 2026-05-06 22:46:06 +02:00
add context menu to friends locations card
This commit is contained in:
@@ -1,41 +1,90 @@
|
|||||||
<template>
|
<template>
|
||||||
<Card class="friend-card p-0 gap-0" :style="cardStyle" @click="showUserDialog(friend.id)">
|
<ContextMenu>
|
||||||
<div class="friend-card__header">
|
<ContextMenuTrigger as-child>
|
||||||
<div>
|
<Card class="friend-card p-0 gap-0" :style="cardStyle" @click="showUserDialog(friend.id)">
|
||||||
<Avatar class="friend-card__avatar" :style="{ width: `${avatarSize}px`, height: `${avatarSize}px` }">
|
<div class="friend-card__header">
|
||||||
<AvatarImage :src="userImage(friend.ref, true)" />
|
<div>
|
||||||
<AvatarFallback>{{ avatarFallback }}</AvatarFallback>
|
<Avatar
|
||||||
</Avatar>
|
class="friend-card__avatar"
|
||||||
</div>
|
:style="{ width: `${avatarSize}px`, height: `${avatarSize}px` }">
|
||||||
<span class="friend-card__status-dot" :class="statusDotClass"></span>
|
<AvatarImage :src="userImage(friend.ref, true)" />
|
||||||
<div class="friend-card__name ml-0.5" :title="friend.name">{{ friend.name }}</div>
|
<AvatarFallback>{{ avatarFallback }}</AvatarFallback>
|
||||||
</div>
|
</Avatar>
|
||||||
<div class="friend-card__body">
|
</div>
|
||||||
<div class="friend-card__signature ml-1" :title="friend.ref?.statusDescription">
|
<span class="friend-card__status-dot" :class="statusDotClass"></span>
|
||||||
<Pencil v-if="friend.ref?.statusDescription" class="h-3.5 w-3.5 mr-1" style="opacity: 0.7" />
|
<div class="friend-card__name ml-0.5" :title="friend.name">{{ friend.name }}</div>
|
||||||
{{ friend.ref?.statusDescription || ' ' }}
|
</div>
|
||||||
</div>
|
<div class="friend-card__body">
|
||||||
<div v-if="displayInstanceInfo" @click.stop class="friend-card__world" :title="friend.worldName">
|
<div class="friend-card__signature ml-1" :title="friend.ref?.statusDescription">
|
||||||
<Location
|
<Pencil v-if="friend.ref?.statusDescription" class="h-3.5 w-3.5 mr-1" style="opacity: 0.7" />
|
||||||
class="friend-card__location"
|
{{ friend.ref?.statusDescription || ' ' }}
|
||||||
:location="friend.ref?.location"
|
</div>
|
||||||
:traveling="friend.ref?.travelingToLocation"
|
<div v-if="displayInstanceInfo" @click.stop class="friend-card__world" :title="friend.worldName">
|
||||||
link />
|
<Location
|
||||||
</div>
|
class="friend-card__location"
|
||||||
</div>
|
:location="friend.ref?.location"
|
||||||
</Card>
|
:traveling="friend.ref?.travelingToLocation"
|
||||||
|
link />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</ContextMenuTrigger>
|
||||||
|
<ContextMenuContent>
|
||||||
|
<ContextMenuItem v-if="friend.state === 'online'" @click="friendRequestInvite">
|
||||||
|
{{ t('dialog.user.actions.request_invite') }}
|
||||||
|
</ContextMenuItem>
|
||||||
|
<ContextMenuItem v-if="isGameRunning" :disabled="!canInviteToMyLocation" @click="friendInvite">
|
||||||
|
{{ t('dialog.user.actions.invite') }}
|
||||||
|
</ContextMenuItem>
|
||||||
|
<ContextMenuItem :disabled="!currentUser?.isBoopingEnabled" @click="friendSendBoop">
|
||||||
|
{{ t('dialog.user.actions.send_boop') }}
|
||||||
|
</ContextMenuItem>
|
||||||
|
<ContextMenuSeparator v-if="friend.state === 'online' && hasFriendLocation" />
|
||||||
|
<ContextMenuItem
|
||||||
|
v-if="friend.state === 'online' && hasFriendLocation"
|
||||||
|
:disabled="!canJoinFriend"
|
||||||
|
@click="friendJoin">
|
||||||
|
{{ t('dialog.user.info.launch_invite_tooltip') }}
|
||||||
|
</ContextMenuItem>
|
||||||
|
<ContextMenuItem
|
||||||
|
v-if="friend.state === 'online' && hasFriendLocation"
|
||||||
|
:disabled="!canJoinFriend"
|
||||||
|
@click="friendInviteSelf">
|
||||||
|
{{ t('dialog.user.info.self_invite_tooltip') }}
|
||||||
|
</ContextMenuItem>
|
||||||
|
</ContextMenuContent>
|
||||||
|
</ContextMenu>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
|
import {
|
||||||
|
ContextMenu,
|
||||||
|
ContextMenuContent,
|
||||||
|
ContextMenuItem,
|
||||||
|
ContextMenuSeparator,
|
||||||
|
ContextMenuTrigger
|
||||||
|
} from '@/components/ui/context-menu';
|
||||||
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
|
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
|
||||||
import { Card } from '@/components/ui/card';
|
import { Card } from '@/components/ui/card';
|
||||||
import { Pencil } from 'lucide-vue-next';
|
import { Pencil } from 'lucide-vue-next';
|
||||||
import { computed } from 'vue';
|
import { computed } from 'vue';
|
||||||
|
import { storeToRefs } from 'pinia';
|
||||||
|
import { toast } from 'vue-sonner';
|
||||||
|
import { useI18n } from 'vue-i18n';
|
||||||
|
|
||||||
import { userImage, userStatusClass } from '../../../shared/utils';
|
import { isRealInstance, parseLocation, userImage, userStatusClass } from '../../../shared/utils';
|
||||||
import { useUserStore } from '../../../stores';
|
import { useGameStore, useLaunchStore, useLocationStore, useUserStore } from '../../../stores';
|
||||||
|
import { instanceRequest, notificationRequest, worldRequest } from '../../../api';
|
||||||
|
import { checkCanInvite, checkCanInviteSelf } from '../../../shared/utils/invite.js';
|
||||||
|
|
||||||
const { showUserDialog } = useUserStore();
|
import Location from '../../../components/Location.vue';
|
||||||
|
|
||||||
|
const { t } = useI18n();
|
||||||
|
const { showUserDialog, showSendBoopDialog } = useUserStore();
|
||||||
|
const launchStore = useLaunchStore();
|
||||||
|
const { lastLocation, lastLocationDestination } = storeToRefs(useLocationStore());
|
||||||
|
const { isGameRunning } = storeToRefs(useGameStore());
|
||||||
|
const { currentUser } = storeToRefs(useUserStore());
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
friend: {
|
friend: {
|
||||||
@@ -102,6 +151,86 @@
|
|||||||
|
|
||||||
return 'friend-card__status-dot--hidden';
|
return 'friend-card__status-dot--hidden';
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const canInviteToMyLocation = computed(() => checkCanInvite(lastLocation.value.location));
|
||||||
|
|
||||||
|
const hasFriendLocation = computed(() => {
|
||||||
|
const loc = props.friend.ref?.location;
|
||||||
|
return !!loc && isRealInstance(loc);
|
||||||
|
});
|
||||||
|
|
||||||
|
const canJoinFriend = computed(() => {
|
||||||
|
const loc = props.friend.ref?.location;
|
||||||
|
if (!loc || !isRealInstance(loc)) return false;
|
||||||
|
return checkCanInviteSelf(loc);
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
function friendRequestInvite() {
|
||||||
|
notificationRequest.sendRequestInvite({ platform: 'standalonewindows' }, props.friend.id).then(() => {
|
||||||
|
toast.success('Request invite sent');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
function friendInvite() {
|
||||||
|
let currentLocation = lastLocation.value.location;
|
||||||
|
if (currentLocation === 'traveling') {
|
||||||
|
currentLocation = lastLocationDestination.value;
|
||||||
|
}
|
||||||
|
const L = parseLocation(currentLocation);
|
||||||
|
worldRequest.getCachedWorld({ worldId: L.worldId }).then((args) => {
|
||||||
|
notificationRequest
|
||||||
|
.sendInvite(
|
||||||
|
{
|
||||||
|
instanceId: L.tag,
|
||||||
|
worldId: L.tag,
|
||||||
|
worldName: args.ref.name
|
||||||
|
},
|
||||||
|
props.friend.id
|
||||||
|
)
|
||||||
|
.then(() => {
|
||||||
|
toast.success(t('message.invite.sent'));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
function friendSendBoop() {
|
||||||
|
showSendBoopDialog(props.friend.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
function friendJoin() {
|
||||||
|
const loc = props.friend.ref?.location;
|
||||||
|
if (!loc) return;
|
||||||
|
launchStore.showLaunchDialog(loc);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
function friendInviteSelf() {
|
||||||
|
const loc = props.friend.ref?.location;
|
||||||
|
if (!loc) return;
|
||||||
|
const L = parseLocation(loc);
|
||||||
|
instanceRequest
|
||||||
|
.selfInvite({
|
||||||
|
instanceId: L.instanceId,
|
||||||
|
worldId: L.worldId
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
toast.success(t('message.invite.self_sent'));
|
||||||
|
});
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|||||||
@@ -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