mirror of
https://github.com/MrUnknownDE/VRCX.git
synced 2026-05-01 04:33:46 +02:00
add test
This commit is contained in:
542
src/stores/__tests__/mediaParsers.test.js
Normal file
542
src/stores/__tests__/mediaParsers.test.js
Normal file
@@ -0,0 +1,542 @@
|
||||
import { beforeEach, describe, expect, test, vi } from 'vitest';
|
||||
|
||||
vi.mock('../../shared/utils', () => ({
|
||||
convertYoutubeTime: vi.fn((dur) => {
|
||||
// simplified mock: PT3M30S → 210
|
||||
const m = /(\d+)M/.exec(dur);
|
||||
const s = /(\d+)S/.exec(dur);
|
||||
return (m ? Number(m[1]) * 60 : 0) + (s ? Number(s[1]) : 0);
|
||||
}),
|
||||
findUserByDisplayName: vi.fn(),
|
||||
isRpcWorld: vi.fn(() => false),
|
||||
replaceBioSymbols: vi.fn((s) => s)
|
||||
}));
|
||||
|
||||
import { isRpcWorld, findUserByDisplayName } from '../../shared/utils';
|
||||
import { createMediaParsers } from '../gameLog/mediaParsers';
|
||||
|
||||
/**
|
||||
*
|
||||
* @param overrides
|
||||
*/
|
||||
function makeDeps(overrides = {}) {
|
||||
return {
|
||||
nowPlaying: { value: { url: '' } },
|
||||
setNowPlaying: vi.fn(),
|
||||
clearNowPlaying: vi.fn(),
|
||||
userStore: { cachedUsers: new Map() },
|
||||
advancedSettingsStore: {
|
||||
youTubeApi: false,
|
||||
lookupYouTubeVideo: vi.fn()
|
||||
},
|
||||
...overrides
|
||||
};
|
||||
}
|
||||
|
||||
// ─── addGameLogVideo ─────────────────────────────────────────────────
|
||||
|
||||
describe('addGameLogVideo', () => {
|
||||
let deps, parsers;
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
isRpcWorld.mockReturnValue(false);
|
||||
deps = makeDeps();
|
||||
parsers = createMediaParsers(deps);
|
||||
});
|
||||
|
||||
test('creates VideoPlay entry for a normal URL', async () => {
|
||||
const gameLog = {
|
||||
dt: '2024-01-01',
|
||||
videoUrl: 'https://example.com/video.mp4',
|
||||
displayName: 'Alice'
|
||||
};
|
||||
await parsers.addGameLogVideo(gameLog, 'wrld_123:456', 'usr_a');
|
||||
|
||||
expect(deps.setNowPlaying).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
type: 'VideoPlay',
|
||||
videoUrl: 'https://example.com/video.mp4',
|
||||
displayName: 'Alice',
|
||||
location: 'wrld_123:456',
|
||||
userId: 'usr_a'
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
test('skips video in RPC world (non-YouTube)', async () => {
|
||||
isRpcWorld.mockReturnValue(true);
|
||||
deps = makeDeps();
|
||||
parsers = createMediaParsers(deps);
|
||||
|
||||
const gameLog = {
|
||||
dt: '2024-01-01',
|
||||
videoUrl: 'https://example.com/video.mp4'
|
||||
};
|
||||
await parsers.addGameLogVideo(gameLog, 'wrld_rpc', 'usr_a');
|
||||
|
||||
expect(deps.setNowPlaying).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('processes YouTube video in RPC world when videoId is YouTube', async () => {
|
||||
isRpcWorld.mockReturnValue(true);
|
||||
deps = makeDeps();
|
||||
parsers = createMediaParsers(deps);
|
||||
|
||||
const gameLog = {
|
||||
dt: '2024-01-01',
|
||||
videoUrl: 'https://youtu.be/dQw4w9WgXcQ',
|
||||
videoId: 'YouTube'
|
||||
};
|
||||
await parsers.addGameLogVideo(gameLog, 'wrld_rpc', 'usr_a');
|
||||
|
||||
// YouTube API off, so videoId/videoName/videoLength default
|
||||
expect(deps.setNowPlaying).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
type: 'VideoPlay',
|
||||
videoUrl: 'https://youtu.be/dQw4w9WgXcQ'
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
test('extracts YouTube video ID from youtu.be URL', async () => {
|
||||
deps = makeDeps({
|
||||
advancedSettingsStore: {
|
||||
youTubeApi: true,
|
||||
lookupYouTubeVideo: vi.fn().mockResolvedValue({
|
||||
pageInfo: { totalResults: 1 },
|
||||
items: [
|
||||
{
|
||||
snippet: { title: 'Test Video' },
|
||||
contentDetails: { duration: 'PT3M30S' }
|
||||
}
|
||||
]
|
||||
})
|
||||
}
|
||||
});
|
||||
parsers = createMediaParsers(deps);
|
||||
|
||||
const gameLog = {
|
||||
dt: '2024-01-01',
|
||||
videoUrl: 'https://youtu.be/dQw4w9WgXcQ'
|
||||
};
|
||||
await parsers.addGameLogVideo(gameLog, 'wrld_123:456', 'usr_a');
|
||||
|
||||
expect(
|
||||
deps.advancedSettingsStore.lookupYouTubeVideo
|
||||
).toHaveBeenCalledWith('dQw4w9WgXcQ');
|
||||
expect(deps.setNowPlaying).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
videoId: 'YouTube',
|
||||
videoName: 'Test Video',
|
||||
videoLength: 210
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
test('respects videoPos from gameLog', async () => {
|
||||
const gameLog = {
|
||||
dt: '2024-01-01',
|
||||
videoUrl: 'https://example.com/v.mp4',
|
||||
videoPos: 42
|
||||
};
|
||||
await parsers.addGameLogVideo(gameLog, 'wrld_123:456', 'usr_a');
|
||||
|
||||
expect(deps.setNowPlaying).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ videoPos: 42 })
|
||||
);
|
||||
});
|
||||
|
||||
test('unwraps proxy URLs (t-ne.x0.to)', async () => {
|
||||
const gameLog = {
|
||||
dt: '2024-01-01',
|
||||
videoUrl:
|
||||
'https://t-ne.x0.to/?url=https://www.youtube.com/watch?v=abcdefghijk'
|
||||
};
|
||||
deps = makeDeps({
|
||||
advancedSettingsStore: {
|
||||
youTubeApi: true,
|
||||
lookupYouTubeVideo: vi.fn().mockResolvedValue({
|
||||
pageInfo: { totalResults: 1 },
|
||||
items: [
|
||||
{
|
||||
snippet: { title: 'Proxy Video' },
|
||||
contentDetails: { duration: 'PT1M' }
|
||||
}
|
||||
]
|
||||
})
|
||||
}
|
||||
});
|
||||
parsers = createMediaParsers(deps);
|
||||
|
||||
await parsers.addGameLogVideo(gameLog, 'wrld_123:456', 'usr_a');
|
||||
|
||||
expect(
|
||||
deps.advancedSettingsStore.lookupYouTubeVideo
|
||||
).toHaveBeenCalledWith('abcdefghijk');
|
||||
});
|
||||
});
|
||||
|
||||
// ─── addGameLogPyPyDance ─────────────────────────────────────────────
|
||||
|
||||
describe('addGameLogPyPyDance', () => {
|
||||
let deps, parsers;
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
isRpcWorld.mockReturnValue(true);
|
||||
findUserByDisplayName.mockReturnValue(null);
|
||||
deps = makeDeps();
|
||||
parsers = createMediaParsers(deps);
|
||||
});
|
||||
|
||||
test('parses PyPyDance data and calls setNowPlaying', () => {
|
||||
const gameLog = {
|
||||
dt: '2024-01-01',
|
||||
data: 'VideoPlay(PyPyDance) "https://example.com/v.mp4",10,300,"SomeSource: Song Title(TestUser)"'
|
||||
};
|
||||
parsers.addGameLogPyPyDance(gameLog, 'wrld_rpc');
|
||||
|
||||
expect(deps.setNowPlaying).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
type: 'VideoPlay',
|
||||
videoUrl: 'https://example.com/v.mp4',
|
||||
videoLength: 300,
|
||||
displayName: 'TestUser'
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
test('returns early for unparseable data', () => {
|
||||
const gameLog = { dt: '2024-01-01', data: 'garbage data' };
|
||||
parsers.addGameLogPyPyDance(gameLog, 'wrld_rpc');
|
||||
|
||||
expect(deps.setNowPlaying).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('sets displayName to empty when Random', () => {
|
||||
const gameLog = {
|
||||
dt: '2024-01-01',
|
||||
data: 'VideoPlay(PyPyDance) "https://example.com/v.mp4",5,200,"Source: Title(Random)"'
|
||||
};
|
||||
parsers.addGameLogPyPyDance(gameLog, 'wrld_rpc');
|
||||
|
||||
expect(deps.setNowPlaying).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ displayName: '' })
|
||||
);
|
||||
});
|
||||
|
||||
test('updates nowPlaying when URL matches', () => {
|
||||
deps.nowPlaying.value.url = 'https://example.com/v.mp4';
|
||||
parsers = createMediaParsers(deps);
|
||||
|
||||
const gameLog = {
|
||||
dt: '2024-01-01',
|
||||
data: 'VideoPlay(PyPyDance) "https://example.com/v.mp4",20,300,"Source: Title(User1)"'
|
||||
};
|
||||
parsers.addGameLogPyPyDance(gameLog, 'wrld_rpc');
|
||||
|
||||
expect(deps.setNowPlaying).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
updatedAt: '2024-01-01',
|
||||
videoPos: 20,
|
||||
videoLength: 300
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
// ─── addGameLogVRDancing ─────────────────────────────────────────────
|
||||
|
||||
describe('addGameLogVRDancing', () => {
|
||||
let deps, parsers;
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
isRpcWorld.mockReturnValue(true);
|
||||
findUserByDisplayName.mockReturnValue(null);
|
||||
deps = makeDeps();
|
||||
parsers = createMediaParsers(deps);
|
||||
});
|
||||
|
||||
test('parses VRDancing data and creates entry', () => {
|
||||
const gameLog = {
|
||||
dt: '2024-01-01',
|
||||
data: 'VideoPlay(VRDancing) "https://example.com/v.mp4",10,300,42,"Alice","Cool Song"'
|
||||
};
|
||||
parsers.addGameLogVRDancing(gameLog, 'wrld_rpc');
|
||||
|
||||
expect(deps.setNowPlaying).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
type: 'VideoPlay',
|
||||
videoUrl: 'https://example.com/v.mp4',
|
||||
displayName: 'Alice',
|
||||
videoName: 'Cool Song'
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
test('converts videoId -1 to YouTube', () => {
|
||||
const gameLog = {
|
||||
dt: '2024-01-01',
|
||||
data: 'VideoPlay(VRDancing) "https://youtu.be/dQw4w9WgXcQ",0,300,-1,"Alice","Song"'
|
||||
};
|
||||
// This will call addGameLogVideo internally (YouTube path)
|
||||
parsers.addGameLogVRDancing(gameLog, 'wrld_rpc');
|
||||
|
||||
// setNowPlaying is called via addGameLogVideo for YouTube
|
||||
expect(deps.setNowPlaying).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('strips HTML from videoName', () => {
|
||||
const gameLog = {
|
||||
dt: '2024-01-01',
|
||||
data: 'VideoPlay(VRDancing) "https://example.com/v.mp4",0,200,5,"Bob","[Tag]</b> Actual Title"'
|
||||
};
|
||||
parsers.addGameLogVRDancing(gameLog, 'wrld_rpc');
|
||||
|
||||
expect(deps.setNowPlaying).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ videoName: 'Actual Title' })
|
||||
);
|
||||
});
|
||||
|
||||
test('resets videoPos when it equals videoLength', () => {
|
||||
const gameLog = {
|
||||
dt: '2024-01-01',
|
||||
data: 'VideoPlay(VRDancing) "https://example.com/v.mp4",300,300,5,"Bob","Title"'
|
||||
};
|
||||
parsers.addGameLogVRDancing(gameLog, 'wrld_rpc');
|
||||
|
||||
expect(deps.setNowPlaying).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ videoPos: 0 })
|
||||
);
|
||||
});
|
||||
|
||||
test('returns early for unparseable data', () => {
|
||||
const gameLog = { dt: '2024-01-01', data: 'bad data' };
|
||||
parsers.addGameLogVRDancing(gameLog, 'wrld_rpc');
|
||||
expect(deps.setNowPlaying).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
// ─── addGameLogZuwaZuwaDance ─────────────────────────────────────────
|
||||
|
||||
describe('addGameLogZuwaZuwaDance', () => {
|
||||
let deps, parsers;
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
isRpcWorld.mockReturnValue(true);
|
||||
findUserByDisplayName.mockReturnValue(null);
|
||||
deps = makeDeps();
|
||||
parsers = createMediaParsers(deps);
|
||||
});
|
||||
|
||||
test('parses ZuwaZuwaDance data correctly', () => {
|
||||
const gameLog = {
|
||||
dt: '2024-01-01',
|
||||
data: 'VideoPlay(ZuwaZuwaDance) "https://example.com/v.mp4",5,200,42,"Alice","Dance Song"'
|
||||
};
|
||||
parsers.addGameLogZuwaZuwaDance(gameLog, 'wrld_rpc');
|
||||
|
||||
expect(deps.setNowPlaying).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
type: 'VideoPlay',
|
||||
displayName: 'Alice',
|
||||
videoName: 'Dance Song',
|
||||
videoId: '42'
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
test('converts videoId 9999 to YouTube', () => {
|
||||
const gameLog = {
|
||||
dt: '2024-01-01',
|
||||
data: 'VideoPlay(ZuwaZuwaDance) "https://youtu.be/dQw4w9WgXcQ",0,200,9999,"Alice","Song"'
|
||||
};
|
||||
parsers.addGameLogZuwaZuwaDance(gameLog, 'wrld_rpc');
|
||||
expect(deps.setNowPlaying).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('sets displayName to empty when Random', () => {
|
||||
const gameLog = {
|
||||
dt: '2024-01-01',
|
||||
data: 'VideoPlay(ZuwaZuwaDance) "https://example.com/v.mp4",0,200,1,"Random","Song"'
|
||||
};
|
||||
parsers.addGameLogZuwaZuwaDance(gameLog, 'wrld_rpc');
|
||||
|
||||
expect(deps.setNowPlaying).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ displayName: '' })
|
||||
);
|
||||
});
|
||||
|
||||
test('returns early for unparseable data', () => {
|
||||
const gameLog = { dt: '2024-01-01', data: 'bad' };
|
||||
parsers.addGameLogZuwaZuwaDance(gameLog, 'wrld_rpc');
|
||||
expect(deps.setNowPlaying).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
// ─── addGameLogLSMedia ───────────────────────────────────────────────
|
||||
|
||||
describe('addGameLogLSMedia', () => {
|
||||
let deps, parsers;
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
findUserByDisplayName.mockReturnValue(null);
|
||||
deps = makeDeps();
|
||||
parsers = createMediaParsers(deps);
|
||||
});
|
||||
|
||||
test('parses LSMedia log correctly', () => {
|
||||
const gameLog = {
|
||||
dt: '2024-01-01',
|
||||
data: 'LSMedia 0,6298.292,Natsumi-sama,The Outfit (2022),'
|
||||
};
|
||||
parsers.addGameLogLSMedia(gameLog, 'wrld_123:456');
|
||||
|
||||
expect(deps.setNowPlaying).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
type: 'VideoPlay',
|
||||
videoId: 'LSMedia',
|
||||
displayName: 'Natsumi-sama',
|
||||
videoName: 'The Outfit (2022)'
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
test('returns early for empty video name (regex does not match)', () => {
|
||||
const gameLog = {
|
||||
dt: '2024-01-01',
|
||||
data: 'LSMedia 0,4268.981,Natsumi-sama,,'
|
||||
};
|
||||
parsers.addGameLogLSMedia(gameLog, 'wrld_123:456');
|
||||
|
||||
// The regex requires a non-empty 4th capture group,
|
||||
// so an empty video name causes the parse to fail
|
||||
expect(deps.setNowPlaying).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('returns early for unparseable data', () => {
|
||||
const gameLog = { dt: '2024-01-01', data: 'bad data' };
|
||||
parsers.addGameLogLSMedia(gameLog, 'wrld_123:456');
|
||||
expect(deps.setNowPlaying).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('looks up userId when displayName given', () => {
|
||||
findUserByDisplayName.mockReturnValue({ id: 'usr_found' });
|
||||
|
||||
const gameLog = {
|
||||
dt: '2024-01-01',
|
||||
data: 'LSMedia 0,100,Alice,Movie,'
|
||||
};
|
||||
parsers.addGameLogLSMedia(gameLog, 'wrld_123:456');
|
||||
|
||||
expect(deps.setNowPlaying).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ userId: 'usr_found' })
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
// ─── addGameLogPopcornPalace ─────────────────────────────────────────
|
||||
|
||||
describe('addGameLogPopcornPalace', () => {
|
||||
let deps, parsers;
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
findUserByDisplayName.mockReturnValue(null);
|
||||
deps = makeDeps();
|
||||
parsers = createMediaParsers(deps);
|
||||
});
|
||||
|
||||
test('parses PopcornPalace JSON data', () => {
|
||||
const json = JSON.stringify({
|
||||
videoName: 'How to Train Your Dragon',
|
||||
videoPos: 37.28,
|
||||
videoLength: 11474.05,
|
||||
thumbnailUrl: 'https://example.com/thumb.jpg',
|
||||
displayName: 'miner28_3',
|
||||
isPaused: false,
|
||||
is3D: false,
|
||||
looping: false
|
||||
});
|
||||
const gameLog = {
|
||||
dt: '2024-01-01',
|
||||
data: `VideoPlay(PopcornPalace) ${json}`
|
||||
};
|
||||
parsers.addGameLogPopcornPalace(gameLog, 'wrld_123:456');
|
||||
|
||||
expect(deps.setNowPlaying).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
type: 'VideoPlay',
|
||||
videoId: 'PopcornPalace',
|
||||
videoName: 'How to Train Your Dragon',
|
||||
displayName: 'miner28_3',
|
||||
thumbnailUrl: 'https://example.com/thumb.jpg'
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
test('calls clearNowPlaying when videoName is empty', () => {
|
||||
const json = JSON.stringify({
|
||||
videoName: '',
|
||||
videoPos: 0,
|
||||
videoLength: 0,
|
||||
displayName: 'user'
|
||||
});
|
||||
const gameLog = {
|
||||
dt: '2024-01-01',
|
||||
data: `VideoPlay(PopcornPalace) ${json}`
|
||||
};
|
||||
parsers.addGameLogPopcornPalace(gameLog, 'wrld_123:456');
|
||||
|
||||
expect(deps.clearNowPlaying).toHaveBeenCalled();
|
||||
expect(deps.setNowPlaying).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('returns early for null data', () => {
|
||||
const gameLog = { dt: '2024-01-01', data: null };
|
||||
parsers.addGameLogPopcornPalace(gameLog, 'wrld_123:456');
|
||||
|
||||
expect(deps.setNowPlaying).not.toHaveBeenCalled();
|
||||
expect(deps.clearNowPlaying).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('returns early for invalid JSON', () => {
|
||||
const gameLog = {
|
||||
dt: '2024-01-01',
|
||||
data: 'VideoPlay(PopcornPalace) {bad json'
|
||||
};
|
||||
parsers.addGameLogPopcornPalace(gameLog, 'wrld_123:456');
|
||||
|
||||
expect(deps.setNowPlaying).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('updates existing nowPlaying when URL matches', () => {
|
||||
deps.nowPlaying.value.url = 'Movie Title';
|
||||
parsers = createMediaParsers(deps);
|
||||
|
||||
const json = JSON.stringify({
|
||||
videoName: 'Movie Title',
|
||||
videoPos: 500,
|
||||
videoLength: 7200,
|
||||
thumbnailUrl: '',
|
||||
displayName: 'user'
|
||||
});
|
||||
const gameLog = {
|
||||
dt: '2024-01-01',
|
||||
data: `VideoPlay(PopcornPalace) ${json}`
|
||||
};
|
||||
parsers.addGameLogPopcornPalace(gameLog, 'wrld_123:456');
|
||||
|
||||
expect(deps.setNowPlaying).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
updatedAt: '2024-01-01',
|
||||
videoPos: 500,
|
||||
videoLength: 7200
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
283
src/stores/__tests__/overlayDispatch.test.js
Normal file
283
src/stores/__tests__/overlayDispatch.test.js
Normal file
@@ -0,0 +1,283 @@
|
||||
import { beforeEach, describe, expect, test, vi } from 'vitest';
|
||||
|
||||
vi.mock('../../shared/utils', () => ({
|
||||
extractFileId: vi.fn(),
|
||||
extractFileVersion: vi.fn()
|
||||
}));
|
||||
vi.mock('../../shared/utils/notificationMessage', () => ({
|
||||
getNotificationMessage: vi.fn(),
|
||||
toNotificationText: vi.fn()
|
||||
}));
|
||||
|
||||
import { extractFileId, extractFileVersion } from '../../shared/utils';
|
||||
import {
|
||||
getNotificationMessage,
|
||||
toNotificationText
|
||||
} from '../../shared/utils/notificationMessage';
|
||||
import { createOverlayDispatch } from '../notification/overlayDispatch';
|
||||
|
||||
function makeDeps(overrides = {}) {
|
||||
return {
|
||||
getUserIdFromNoty: vi.fn(() => ''),
|
||||
userRequest: {
|
||||
getCachedUser: vi.fn().mockResolvedValue({ json: null })
|
||||
},
|
||||
notificationsSettingsStore: {
|
||||
notificationTimeout: 5000
|
||||
},
|
||||
advancedSettingsStore: {
|
||||
notificationOpacity: 80
|
||||
},
|
||||
appearanceSettingsStore: {
|
||||
displayVRCPlusIconsAsAvatar: false
|
||||
},
|
||||
...overrides
|
||||
};
|
||||
}
|
||||
|
||||
// ─── notyGetImage ────────────────────────────────────────────────────
|
||||
|
||||
describe('notyGetImage', () => {
|
||||
let deps, dispatch;
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
deps = makeDeps();
|
||||
dispatch = createOverlayDispatch(deps);
|
||||
});
|
||||
|
||||
test('returns thumbnailImageUrl when present', async () => {
|
||||
const noty = { thumbnailImageUrl: 'https://thumb.jpg' };
|
||||
const result = await dispatch.notyGetImage(noty);
|
||||
expect(result).toBe('https://thumb.jpg');
|
||||
});
|
||||
|
||||
test('returns details.imageUrl when thumbnailImageUrl absent', async () => {
|
||||
const noty = { details: { imageUrl: 'https://detail.jpg' } };
|
||||
const result = await dispatch.notyGetImage(noty);
|
||||
expect(result).toBe('https://detail.jpg');
|
||||
});
|
||||
|
||||
test('returns imageUrl when thumbnailImageUrl and details absent', async () => {
|
||||
const noty = { imageUrl: 'https://img.jpg' };
|
||||
const result = await dispatch.notyGetImage(noty);
|
||||
expect(result).toBe('https://img.jpg');
|
||||
});
|
||||
|
||||
test('looks up user currentAvatarThumbnailImageUrl when no image URLs', async () => {
|
||||
deps.getUserIdFromNoty.mockReturnValue('usr_abc');
|
||||
deps.userRequest.getCachedUser.mockResolvedValue({
|
||||
json: {
|
||||
currentAvatarThumbnailImageUrl: 'https://avatar.jpg'
|
||||
}
|
||||
});
|
||||
dispatch = createOverlayDispatch(deps);
|
||||
|
||||
const noty = {};
|
||||
const result = await dispatch.notyGetImage(noty);
|
||||
expect(result).toBe('https://avatar.jpg');
|
||||
});
|
||||
|
||||
test('returns profilePicOverride when available', async () => {
|
||||
deps.getUserIdFromNoty.mockReturnValue('usr_abc');
|
||||
deps.userRequest.getCachedUser.mockResolvedValue({
|
||||
json: {
|
||||
profilePicOverride: 'https://profile.jpg',
|
||||
currentAvatarThumbnailImageUrl: 'https://avatar.jpg'
|
||||
}
|
||||
});
|
||||
dispatch = createOverlayDispatch(deps);
|
||||
|
||||
const result = await dispatch.notyGetImage({});
|
||||
expect(result).toBe('https://profile.jpg');
|
||||
});
|
||||
|
||||
test('returns userIcon when displayVRCPlusIconsAsAvatar is enabled', async () => {
|
||||
deps.getUserIdFromNoty.mockReturnValue('usr_abc');
|
||||
deps.appearanceSettingsStore.displayVRCPlusIconsAsAvatar = true;
|
||||
deps.userRequest.getCachedUser.mockResolvedValue({
|
||||
json: {
|
||||
userIcon: 'https://icon.jpg',
|
||||
profilePicOverride: 'https://profile.jpg',
|
||||
currentAvatarThumbnailImageUrl: 'https://avatar.jpg'
|
||||
}
|
||||
});
|
||||
dispatch = createOverlayDispatch(deps);
|
||||
|
||||
const result = await dispatch.notyGetImage({});
|
||||
expect(result).toBe('https://icon.jpg');
|
||||
});
|
||||
|
||||
test('returns empty string for grp_ userId', async () => {
|
||||
deps.getUserIdFromNoty.mockReturnValue('grp_abc');
|
||||
dispatch = createOverlayDispatch(deps);
|
||||
|
||||
const result = await dispatch.notyGetImage({});
|
||||
expect(result).toBe('');
|
||||
expect(deps.userRequest.getCachedUser).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('returns empty string when user lookup fails', async () => {
|
||||
deps.getUserIdFromNoty.mockReturnValue('usr_abc');
|
||||
deps.userRequest.getCachedUser.mockRejectedValue(
|
||||
new Error('Network error')
|
||||
);
|
||||
dispatch = createOverlayDispatch(deps);
|
||||
|
||||
const result = await dispatch.notyGetImage({});
|
||||
expect(result).toBe('');
|
||||
});
|
||||
|
||||
test('returns empty string when user has no json', async () => {
|
||||
deps.getUserIdFromNoty.mockReturnValue('usr_abc');
|
||||
deps.userRequest.getCachedUser.mockResolvedValue({ json: null });
|
||||
dispatch = createOverlayDispatch(deps);
|
||||
|
||||
const result = await dispatch.notyGetImage({});
|
||||
expect(result).toBe('');
|
||||
});
|
||||
});
|
||||
|
||||
// ─── displayDesktopToast ─────────────────────────────────────────────
|
||||
|
||||
describe('displayDesktopToast', () => {
|
||||
let deps, dispatch;
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
globalThis.WINDOWS = true;
|
||||
globalThis.AppApi = { DesktopNotification: vi.fn() };
|
||||
deps = makeDeps();
|
||||
dispatch = createOverlayDispatch(deps);
|
||||
});
|
||||
|
||||
test('calls desktopNotification with message from getNotificationMessage', () => {
|
||||
getNotificationMessage.mockReturnValue({
|
||||
title: 'Friend Online',
|
||||
body: 'Alice is online'
|
||||
});
|
||||
|
||||
dispatch.displayDesktopToast({}, 'some message', 'img.jpg');
|
||||
|
||||
expect(getNotificationMessage).toHaveBeenCalled();
|
||||
expect(AppApi.DesktopNotification).toHaveBeenCalledWith(
|
||||
'Friend Online',
|
||||
'Alice is online',
|
||||
'img.jpg'
|
||||
);
|
||||
});
|
||||
|
||||
test('does nothing when getNotificationMessage returns null', () => {
|
||||
getNotificationMessage.mockReturnValue(null);
|
||||
|
||||
dispatch.displayDesktopToast({}, 'some message', 'img.jpg');
|
||||
|
||||
expect(AppApi.DesktopNotification).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
// ─── notySaveImage ───────────────────────────────────────────────────
|
||||
|
||||
describe('notySaveImage', () => {
|
||||
let deps, dispatch;
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
globalThis.AppApi = {
|
||||
GetImage: vi.fn().mockResolvedValue('/local/path.jpg')
|
||||
};
|
||||
deps = makeDeps();
|
||||
dispatch = createOverlayDispatch(deps);
|
||||
});
|
||||
|
||||
test('returns saved image path from fileId/fileVersion extraction', async () => {
|
||||
extractFileId.mockReturnValue('file_123');
|
||||
extractFileVersion.mockReturnValue('v1');
|
||||
|
||||
const noty = {
|
||||
thumbnailImageUrl: 'https://api.vrchat.cloud/file_123/v1'
|
||||
};
|
||||
const result = await dispatch.notySaveImage(noty);
|
||||
|
||||
expect(AppApi.GetImage).toHaveBeenCalledWith(
|
||||
'https://api.vrchat.cloud/file_123/v1',
|
||||
'file_123',
|
||||
'v1'
|
||||
);
|
||||
expect(result).toBe('/local/path.jpg');
|
||||
});
|
||||
|
||||
test('falls back to URL-derived fileId for http URLs without fileId', async () => {
|
||||
extractFileId.mockReturnValue('');
|
||||
extractFileVersion.mockReturnValue('');
|
||||
|
||||
const noty = {
|
||||
thumbnailImageUrl:
|
||||
'https://cdn.example.com/1416226261.thumbnail-500.png'
|
||||
};
|
||||
const result = await dispatch.notySaveImage(noty);
|
||||
|
||||
expect(AppApi.GetImage).toHaveBeenCalledWith(
|
||||
'https://cdn.example.com/1416226261.thumbnail-500.png',
|
||||
'1416226261',
|
||||
'1416226261.thumbnail-500.png'
|
||||
);
|
||||
expect(result).toBe('/local/path.jpg');
|
||||
});
|
||||
|
||||
test('returns empty string when no image URL is found', async () => {
|
||||
extractFileId.mockReturnValue('');
|
||||
extractFileVersion.mockReturnValue('');
|
||||
deps.getUserIdFromNoty.mockReturnValue('');
|
||||
dispatch = createOverlayDispatch(deps);
|
||||
|
||||
const noty = {};
|
||||
const result = await dispatch.notySaveImage(noty);
|
||||
|
||||
expect(result).toBe('');
|
||||
});
|
||||
});
|
||||
|
||||
// ─── displayXSNotification ──────────────────────────────────────────
|
||||
|
||||
describe('displayXSNotification', () => {
|
||||
let deps, dispatch;
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
globalThis.AppApi = { XSNotification: vi.fn() };
|
||||
deps = makeDeps();
|
||||
dispatch = createOverlayDispatch(deps);
|
||||
});
|
||||
|
||||
test('calls XSNotification with formatted text', () => {
|
||||
getNotificationMessage.mockReturnValue({
|
||||
title: 'Title',
|
||||
body: 'Body'
|
||||
});
|
||||
toNotificationText.mockReturnValue('Title: Body');
|
||||
|
||||
dispatch.displayXSNotification({ type: 'friendOnline' }, 'msg', 'img');
|
||||
|
||||
expect(toNotificationText).toHaveBeenCalledWith(
|
||||
'Title',
|
||||
'Body',
|
||||
'friendOnline'
|
||||
);
|
||||
expect(AppApi.XSNotification).toHaveBeenCalledWith(
|
||||
'VRCX',
|
||||
'Title: Body',
|
||||
5, // 5000ms / 1000
|
||||
0.8, // 80 / 100
|
||||
'img'
|
||||
);
|
||||
});
|
||||
|
||||
test('does nothing when getNotificationMessage returns null', () => {
|
||||
getNotificationMessage.mockReturnValue(null);
|
||||
|
||||
dispatch.displayXSNotification({}, 'msg', 'img');
|
||||
|
||||
expect(AppApi.XSNotification).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user