mirror of
https://github.com/MrUnknownDE/VRCX.git
synced 2026-04-18 22:33:50 +02:00
add test
This commit is contained in:
558
src/shared/utils/__tests__/user.test.js
Normal file
558
src/shared/utils/__tests__/user.test.js
Normal file
@@ -0,0 +1,558 @@
|
||||
import { beforeEach, describe, expect, test, vi } from 'vitest';
|
||||
|
||||
// Mock stores
|
||||
vi.mock('../../../stores', () => ({
|
||||
useUserStore: vi.fn(),
|
||||
useAppearanceSettingsStore: vi.fn()
|
||||
}));
|
||||
|
||||
// Mock common.js
|
||||
vi.mock('../common', () => ({
|
||||
convertFileUrlToImageUrl: vi.fn((url) => `converted:${url}`)
|
||||
}));
|
||||
|
||||
// Mock base/format.js
|
||||
vi.mock('../base/format', () => ({
|
||||
timeToText: vi.fn((ms) => `${Math.round(ms / 1000)}s`)
|
||||
}));
|
||||
|
||||
// Mock base/ui.js
|
||||
vi.mock('../base/ui', () => ({
|
||||
HueToHex: vi.fn((h) => `#hue${h}`)
|
||||
}));
|
||||
|
||||
// Mock transitive deps that get pulled in via stores
|
||||
vi.mock('../../../views/Feed/Feed.vue', () => ({
|
||||
default: { template: '<div />' }
|
||||
}));
|
||||
vi.mock('../../../views/Feed/columns.jsx', () => ({ columns: [] }));
|
||||
vi.mock('../../../plugin/router', () => ({
|
||||
default: { push: vi.fn(), currentRoute: { value: {} } }
|
||||
}));
|
||||
|
||||
import { useAppearanceSettingsStore, useUserStore } from '../../../stores';
|
||||
import {
|
||||
languageClass,
|
||||
parseUserUrl,
|
||||
removeEmojis,
|
||||
statusClass,
|
||||
userImage,
|
||||
userImageFull,
|
||||
userOnlineFor,
|
||||
userOnlineForTimestamp,
|
||||
userStatusClass
|
||||
} from '../user';
|
||||
|
||||
describe('User Utils', () => {
|
||||
describe('removeEmojis', () => {
|
||||
test('removes emoji characters from text', () => {
|
||||
expect(removeEmojis('Hello 🌍 World')).toBe('Hello World');
|
||||
});
|
||||
|
||||
test('collapses multiple spaces after removal', () => {
|
||||
expect(removeEmojis('A 🎮 B')).toBe('A B');
|
||||
});
|
||||
|
||||
test('returns empty string for falsy input', () => {
|
||||
expect(removeEmojis('')).toBe('');
|
||||
expect(removeEmojis(null)).toBe('');
|
||||
expect(removeEmojis(undefined)).toBe('');
|
||||
});
|
||||
|
||||
test('returns original text when no emojis', () => {
|
||||
expect(removeEmojis('Hello World')).toBe('Hello World');
|
||||
});
|
||||
|
||||
test('trims whitespace', () => {
|
||||
expect(removeEmojis(' Hello ')).toBe('Hello');
|
||||
});
|
||||
});
|
||||
|
||||
describe('statusClass', () => {
|
||||
test('returns online style for active status', () => {
|
||||
expect(statusClass('active')).toEqual({
|
||||
'status-icon': true,
|
||||
online: true
|
||||
});
|
||||
});
|
||||
|
||||
test('returns joinme style for join me status', () => {
|
||||
expect(statusClass('join me')).toEqual({
|
||||
'status-icon': true,
|
||||
joinme: true
|
||||
});
|
||||
});
|
||||
|
||||
test('returns askme style for ask me status', () => {
|
||||
expect(statusClass('ask me')).toEqual({
|
||||
'status-icon': true,
|
||||
askme: true
|
||||
});
|
||||
});
|
||||
|
||||
test('returns busy style for busy status', () => {
|
||||
expect(statusClass('busy')).toEqual({
|
||||
'status-icon': true,
|
||||
busy: true
|
||||
});
|
||||
});
|
||||
|
||||
test('returns null for undefined status', () => {
|
||||
expect(statusClass(undefined)).toBeNull();
|
||||
});
|
||||
|
||||
test('returns null for unknown status strings', () => {
|
||||
expect(statusClass('offline')).toBeNull();
|
||||
expect(statusClass('unknown')).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('languageClass', () => {
|
||||
test('returns mapped flag for known languages', () => {
|
||||
expect(languageClass('eng')).toEqual({ us: true });
|
||||
expect(languageClass('jpn')).toEqual({ jp: true });
|
||||
expect(languageClass('kor')).toEqual({ kr: true });
|
||||
});
|
||||
|
||||
test('returns unknown flag for unmapped languages', () => {
|
||||
expect(languageClass('xyz')).toEqual({ unknown: true });
|
||||
expect(languageClass('')).toEqual({ unknown: true });
|
||||
});
|
||||
});
|
||||
|
||||
describe('parseUserUrl', () => {
|
||||
test('extracts user ID from VRChat URL', () => {
|
||||
expect(
|
||||
parseUserUrl('https://vrchat.com/home/user/usr_abc123-def456')
|
||||
).toBe('usr_abc123-def456');
|
||||
});
|
||||
|
||||
test('returns undefined for non-user URLs', () => {
|
||||
expect(
|
||||
parseUserUrl('https://vrchat.com/home/world/wrld_abc')
|
||||
).toBeUndefined();
|
||||
});
|
||||
|
||||
test('throws for invalid URLs', () => {
|
||||
expect(() => parseUserUrl('not-a-url')).toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('userOnlineForTimestamp', () => {
|
||||
test('returns ISO date for online user with $online_for', () => {
|
||||
const ts = Date.now() - 60000;
|
||||
const ctx = { ref: { state: 'online', $online_for: ts } };
|
||||
const result = userOnlineForTimestamp(ctx);
|
||||
expect(result).toBe(new Date(ts).toJSON());
|
||||
});
|
||||
|
||||
test('returns ISO date for active user with $active_for', () => {
|
||||
const ts = Date.now() - 30000;
|
||||
const ctx = { ref: { state: 'active', $active_for: ts } };
|
||||
expect(userOnlineForTimestamp(ctx)).toBe(new Date(ts).toJSON());
|
||||
});
|
||||
|
||||
test('returns ISO date for offline user with $offline_for', () => {
|
||||
const ts = Date.now() - 120000;
|
||||
const ctx = {
|
||||
ref: { state: 'offline', $offline_for: ts }
|
||||
};
|
||||
expect(userOnlineForTimestamp(ctx)).toBe(new Date(ts).toJSON());
|
||||
});
|
||||
|
||||
test('returns null when no timestamp available', () => {
|
||||
const ctx = { ref: { state: 'offline' } };
|
||||
expect(userOnlineForTimestamp(ctx)).toBeNull();
|
||||
});
|
||||
|
||||
test('prefers $online_for for online state', () => {
|
||||
const ts1 = Date.now() - 10000;
|
||||
const ts2 = Date.now() - 50000;
|
||||
const ctx = {
|
||||
ref: {
|
||||
state: 'online',
|
||||
$online_for: ts1,
|
||||
$offline_for: ts2
|
||||
}
|
||||
};
|
||||
expect(userOnlineForTimestamp(ctx)).toBe(new Date(ts1).toJSON());
|
||||
});
|
||||
});
|
||||
|
||||
describe('userOnlineFor', () => {
|
||||
test('returns formatted time for online user', () => {
|
||||
const now = Date.now();
|
||||
vi.spyOn(Date, 'now').mockReturnValue(now);
|
||||
const ref = { state: 'online', $online_for: now - 5000 };
|
||||
expect(userOnlineFor(ref)).toBe('5s');
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
test('returns formatted time for active user', () => {
|
||||
const now = Date.now();
|
||||
vi.spyOn(Date, 'now').mockReturnValue(now);
|
||||
const ref = { state: 'active', $active_for: now - 10000 };
|
||||
expect(userOnlineFor(ref)).toBe('10s');
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
test('returns formatted time for offline user with $offline_for', () => {
|
||||
const now = Date.now();
|
||||
vi.spyOn(Date, 'now').mockReturnValue(now);
|
||||
const ref = { state: 'offline', $offline_for: now - 3000 };
|
||||
expect(userOnlineFor(ref)).toBe('3s');
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
test('returns dash when no timestamp available', () => {
|
||||
expect(userOnlineFor({ state: 'offline' })).toBe('-');
|
||||
});
|
||||
});
|
||||
|
||||
describe('userStatusClass (with store mock)', () => {
|
||||
beforeEach(() => {
|
||||
useUserStore.mockReturnValue({
|
||||
currentUser: {
|
||||
id: 'usr_me',
|
||||
presence: { platform: 'standalonewindows' },
|
||||
onlineFriends: [],
|
||||
activeFriends: []
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
test('returns null for undefined user', () => {
|
||||
expect(userStatusClass(undefined)).toBeNull();
|
||||
});
|
||||
|
||||
test('returns current user style with status', () => {
|
||||
const result = userStatusClass({
|
||||
id: 'usr_me',
|
||||
status: 'active',
|
||||
isFriend: true
|
||||
});
|
||||
expect(result).toMatchObject({
|
||||
'status-icon': true,
|
||||
online: true,
|
||||
mobile: false
|
||||
});
|
||||
});
|
||||
|
||||
test('returns mobile true for non-PC platform on current user', () => {
|
||||
useUserStore.mockReturnValue({
|
||||
currentUser: {
|
||||
id: 'usr_me',
|
||||
presence: { platform: 'android' },
|
||||
onlineFriends: [],
|
||||
activeFriends: []
|
||||
}
|
||||
});
|
||||
const result = userStatusClass({
|
||||
id: 'usr_me',
|
||||
status: 'active'
|
||||
});
|
||||
expect(result.mobile).toBe(true);
|
||||
});
|
||||
|
||||
test('returns null for non-friend users', () => {
|
||||
expect(
|
||||
userStatusClass({
|
||||
id: 'usr_other',
|
||||
status: 'active',
|
||||
isFriend: false
|
||||
})
|
||||
).toBeNull();
|
||||
});
|
||||
|
||||
test('returns offline style for pending offline friend', () => {
|
||||
const result = userStatusClass(
|
||||
{ id: 'usr_other', isFriend: true, status: 'active' },
|
||||
true
|
||||
);
|
||||
expect(result).toMatchObject({
|
||||
'status-icon': true,
|
||||
offline: true
|
||||
});
|
||||
});
|
||||
|
||||
test('returns correct style for each friend status', () => {
|
||||
const cases = [
|
||||
{
|
||||
status: 'active',
|
||||
location: 'wrld_1',
|
||||
state: 'online',
|
||||
expected: 'online'
|
||||
},
|
||||
{
|
||||
status: 'join me',
|
||||
location: 'wrld_1',
|
||||
state: 'online',
|
||||
expected: 'joinme'
|
||||
},
|
||||
{
|
||||
status: 'ask me',
|
||||
location: 'wrld_1',
|
||||
state: 'online',
|
||||
expected: 'askme'
|
||||
},
|
||||
{
|
||||
status: 'busy',
|
||||
location: 'wrld_1',
|
||||
state: 'online',
|
||||
expected: 'busy'
|
||||
}
|
||||
];
|
||||
for (const { status, location, state, expected } of cases) {
|
||||
const result = userStatusClass({
|
||||
id: 'usr_friend',
|
||||
isFriend: true,
|
||||
status,
|
||||
location,
|
||||
state
|
||||
});
|
||||
expect(result[expected]).toBe(true);
|
||||
}
|
||||
});
|
||||
|
||||
test('returns offline style for location offline', () => {
|
||||
const result = userStatusClass({
|
||||
id: 'usr_f',
|
||||
isFriend: true,
|
||||
status: 'active',
|
||||
location: 'offline',
|
||||
state: ''
|
||||
});
|
||||
expect(result.offline).toBe(true);
|
||||
});
|
||||
|
||||
test('returns active style for state active', () => {
|
||||
const result = userStatusClass({
|
||||
id: 'usr_f',
|
||||
isFriend: true,
|
||||
status: 'busy',
|
||||
location: 'private',
|
||||
state: 'active'
|
||||
});
|
||||
expect(result.active).toBe(true);
|
||||
});
|
||||
|
||||
test('sets mobile flag for non-PC platform friend', () => {
|
||||
const result = userStatusClass({
|
||||
id: 'usr_f',
|
||||
isFriend: true,
|
||||
status: 'active',
|
||||
location: 'wrld_1',
|
||||
state: 'online',
|
||||
$platform: 'android'
|
||||
});
|
||||
expect(result.mobile).toBe(true);
|
||||
});
|
||||
|
||||
test('no mobile flag for standalonewindows platform', () => {
|
||||
const result = userStatusClass({
|
||||
id: 'usr_f',
|
||||
isFriend: true,
|
||||
status: 'active',
|
||||
location: 'wrld_1',
|
||||
state: 'online',
|
||||
$platform: 'standalonewindows'
|
||||
});
|
||||
expect(result.mobile).toBeUndefined();
|
||||
});
|
||||
|
||||
test('uses userId as fallback when id is not present', () => {
|
||||
const result = userStatusClass({
|
||||
userId: 'usr_me',
|
||||
status: 'busy'
|
||||
});
|
||||
expect(result).toMatchObject({
|
||||
'status-icon': true,
|
||||
busy: true,
|
||||
mobile: false
|
||||
});
|
||||
});
|
||||
|
||||
test('handles private location with empty state (temp fix branch)', () => {
|
||||
useUserStore.mockReturnValue({
|
||||
currentUser: {
|
||||
id: 'usr_me',
|
||||
onlineFriends: [],
|
||||
activeFriends: ['usr_f']
|
||||
}
|
||||
});
|
||||
const result = userStatusClass({
|
||||
id: 'usr_f',
|
||||
isFriend: true,
|
||||
status: 'busy',
|
||||
location: 'private',
|
||||
state: ''
|
||||
});
|
||||
// activeFriends includes usr_f → active
|
||||
expect(result.active).toBe(true);
|
||||
});
|
||||
|
||||
test('handles private location temp fix → offline branch', () => {
|
||||
useUserStore.mockReturnValue({
|
||||
currentUser: {
|
||||
id: 'usr_me',
|
||||
onlineFriends: [],
|
||||
activeFriends: []
|
||||
}
|
||||
});
|
||||
const result = userStatusClass({
|
||||
id: 'usr_f',
|
||||
isFriend: true,
|
||||
status: 'busy',
|
||||
location: 'private',
|
||||
state: ''
|
||||
});
|
||||
expect(result.offline).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('userImage (with store mock)', () => {
|
||||
beforeEach(() => {
|
||||
useAppearanceSettingsStore.mockReturnValue({
|
||||
displayVRCPlusIconsAsAvatar: false
|
||||
});
|
||||
});
|
||||
|
||||
test('returns empty string for falsy user', () => {
|
||||
expect(userImage(null)).toBe('');
|
||||
expect(userImage(undefined)).toBe('');
|
||||
});
|
||||
|
||||
test('returns profilePicOverrideThumbnail when available', () => {
|
||||
const user = {
|
||||
profilePicOverrideThumbnail: 'https://img.com/pic/256/thumb'
|
||||
};
|
||||
expect(userImage(user)).toBe('https://img.com/pic/256/thumb');
|
||||
});
|
||||
|
||||
test('replaces resolution for icon mode with profilePicOverrideThumbnail', () => {
|
||||
const user = {
|
||||
profilePicOverrideThumbnail: 'https://img.com/pic/256/thumb'
|
||||
};
|
||||
expect(userImage(user, true, '64')).toBe(
|
||||
'https://img.com/pic/64/thumb'
|
||||
);
|
||||
});
|
||||
|
||||
test('returns profilePicOverride when no thumbnail', () => {
|
||||
const user = { profilePicOverride: 'https://img.com/full' };
|
||||
expect(userImage(user)).toBe('https://img.com/full');
|
||||
});
|
||||
|
||||
test('returns thumbnailUrl as fallback', () => {
|
||||
const user = { thumbnailUrl: 'https://img.com/thumb' };
|
||||
expect(userImage(user)).toBe('https://img.com/thumb');
|
||||
});
|
||||
|
||||
test('returns currentAvatarThumbnailImageUrl as fallback', () => {
|
||||
const user = {
|
||||
currentAvatarThumbnailImageUrl:
|
||||
'https://img.com/avatar/256/thumb'
|
||||
};
|
||||
expect(userImage(user)).toBe('https://img.com/avatar/256/thumb');
|
||||
});
|
||||
|
||||
test('replaces resolution for icon mode with currentAvatarThumbnailImageUrl', () => {
|
||||
const user = {
|
||||
currentAvatarThumbnailImageUrl:
|
||||
'https://img.com/avatar/256/thumb'
|
||||
};
|
||||
expect(userImage(user, true, '64')).toBe(
|
||||
'https://img.com/avatar/64/thumb'
|
||||
);
|
||||
});
|
||||
|
||||
test('returns currentAvatarImageUrl as last resort', () => {
|
||||
const user = {
|
||||
currentAvatarImageUrl: 'https://img.com/avatar/full'
|
||||
};
|
||||
expect(userImage(user)).toBe('https://img.com/avatar/full');
|
||||
});
|
||||
|
||||
test('converts currentAvatarImageUrl for icon mode', () => {
|
||||
const user = {
|
||||
currentAvatarImageUrl: 'https://img.com/avatar/full'
|
||||
};
|
||||
expect(userImage(user, true)).toBe(
|
||||
'converted:https://img.com/avatar/full'
|
||||
);
|
||||
});
|
||||
|
||||
test('returns empty string when user has no image fields', () => {
|
||||
expect(userImage({})).toBe('');
|
||||
});
|
||||
|
||||
test('returns userIcon when displayVRCPlusIconsAsAvatar is true', () => {
|
||||
useAppearanceSettingsStore.mockReturnValue({
|
||||
displayVRCPlusIconsAsAvatar: true
|
||||
});
|
||||
const user = {
|
||||
userIcon: 'https://img.com/icon',
|
||||
thumbnailUrl: 'https://img.com/thumb'
|
||||
};
|
||||
expect(userImage(user)).toBe('https://img.com/icon');
|
||||
});
|
||||
|
||||
test('converts userIcon for icon mode when VRCPlus setting enabled', () => {
|
||||
useAppearanceSettingsStore.mockReturnValue({
|
||||
displayVRCPlusIconsAsAvatar: true
|
||||
});
|
||||
const user = { userIcon: 'https://img.com/icon' };
|
||||
expect(userImage(user, true)).toBe(
|
||||
'converted:https://img.com/icon'
|
||||
);
|
||||
});
|
||||
|
||||
test('returns userIcon for isUserDialogIcon even if VRCPlus setting off', () => {
|
||||
const user = {
|
||||
userIcon: 'https://img.com/icon',
|
||||
thumbnailUrl: 'https://img.com/thumb'
|
||||
};
|
||||
expect(userImage(user, false, '128', true)).toBe(
|
||||
'https://img.com/icon'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('userImageFull (with store mock)', () => {
|
||||
beforeEach(() => {
|
||||
useAppearanceSettingsStore.mockReturnValue({
|
||||
displayVRCPlusIconsAsAvatar: false
|
||||
});
|
||||
});
|
||||
|
||||
test('returns empty string for falsy user', () => {
|
||||
expect(userImageFull(null)).toBe('');
|
||||
});
|
||||
|
||||
test('returns profilePicOverride when available', () => {
|
||||
const user = {
|
||||
profilePicOverride: 'https://img.com/full',
|
||||
currentAvatarImageUrl: 'https://img.com/avatar'
|
||||
};
|
||||
expect(userImageFull(user)).toBe('https://img.com/full');
|
||||
});
|
||||
|
||||
test('returns currentAvatarImageUrl as fallback', () => {
|
||||
const user = {
|
||||
currentAvatarImageUrl: 'https://img.com/avatar'
|
||||
};
|
||||
expect(userImageFull(user)).toBe('https://img.com/avatar');
|
||||
});
|
||||
|
||||
test('returns userIcon when VRCPlus setting enabled', () => {
|
||||
useAppearanceSettingsStore.mockReturnValue({
|
||||
displayVRCPlusIconsAsAvatar: true
|
||||
});
|
||||
const user = {
|
||||
userIcon: 'https://img.com/icon',
|
||||
profilePicOverride: 'https://img.com/full'
|
||||
};
|
||||
expect(userImageFull(user)).toBe('https://img.com/icon');
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user