This commit is contained in:
pa
2026-03-06 04:22:16 +09:00
parent 761ef5ad6b
commit 787f25705e
55 changed files with 6437 additions and 506 deletions

View 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
})
);
});
});

View 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();
});
});