Some checks failed
Deploy to Test Environment / deploy-to-test (push) Failing after 2m50s
777 lines
22 KiB
TypeScript
777 lines
22 KiB
TypeScript
// src/components/NotificationToastHandler.test.tsx
|
|
import React from 'react';
|
|
import { render, act } from '@testing-library/react';
|
|
import { describe, it, expect, vi, beforeEach, afterEach, Mock } from 'vitest';
|
|
import { NotificationToastHandler } from './NotificationToastHandler';
|
|
import type { DealNotificationData, SystemMessageData } from '../types/websocket';
|
|
|
|
// Use vi.hoisted to properly hoist mock functions
|
|
const { mockToastSuccess, mockToastError, mockToastDefault } = vi.hoisted(() => ({
|
|
mockToastSuccess: vi.fn(),
|
|
mockToastError: vi.fn(),
|
|
mockToastDefault: vi.fn(),
|
|
}));
|
|
|
|
// Mock react-hot-toast
|
|
vi.mock('react-hot-toast', () => {
|
|
const toastFn = (message: string, options?: unknown) => mockToastDefault(message, options);
|
|
toastFn.success = mockToastSuccess;
|
|
toastFn.error = mockToastError;
|
|
return {
|
|
default: toastFn,
|
|
};
|
|
});
|
|
|
|
// Mock useWebSocket hook
|
|
vi.mock('../hooks/useWebSocket', () => ({
|
|
useWebSocket: vi.fn(),
|
|
}));
|
|
|
|
// Mock useEventBus hook
|
|
vi.mock('../hooks/useEventBus', () => ({
|
|
useEventBus: vi.fn(),
|
|
}));
|
|
|
|
// Mock formatCurrency
|
|
vi.mock('../utils/formatUtils', () => ({
|
|
formatCurrency: vi.fn((cents: number) => `$${(cents / 100).toFixed(2)}`),
|
|
}));
|
|
|
|
// Import mocked modules
|
|
import { useWebSocket } from '../hooks/useWebSocket';
|
|
import { useEventBus } from '../hooks/useEventBus';
|
|
|
|
const mockUseWebSocket = useWebSocket as Mock;
|
|
const mockUseEventBus = useEventBus as Mock;
|
|
|
|
describe('NotificationToastHandler', () => {
|
|
let eventBusCallbacks: Map<string, (data?: unknown) => void>;
|
|
let onConnectCallback: (() => void) | undefined;
|
|
let onDisconnectCallback: (() => void) | undefined;
|
|
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
vi.useFakeTimers();
|
|
|
|
// Clear toast mocks
|
|
mockToastSuccess.mockClear();
|
|
mockToastError.mockClear();
|
|
mockToastDefault.mockClear();
|
|
|
|
eventBusCallbacks = new Map();
|
|
onConnectCallback = undefined;
|
|
onDisconnectCallback = undefined;
|
|
|
|
// Default mock implementation for useWebSocket
|
|
mockUseWebSocket.mockImplementation(
|
|
(options: { onConnect?: () => void; onDisconnect?: () => void }) => {
|
|
onConnectCallback = options?.onConnect;
|
|
onDisconnectCallback = options?.onDisconnect;
|
|
return {
|
|
isConnected: true,
|
|
error: null,
|
|
};
|
|
},
|
|
);
|
|
|
|
// Capture callbacks for different event types
|
|
mockUseEventBus.mockImplementation((event: string, callback: (data?: unknown) => void) => {
|
|
eventBusCallbacks.set(event, callback);
|
|
});
|
|
});
|
|
|
|
afterEach(() => {
|
|
vi.useRealTimers();
|
|
vi.restoreAllMocks();
|
|
});
|
|
|
|
describe('rendering', () => {
|
|
it('should render null (no visible output)', () => {
|
|
const { container } = render(<NotificationToastHandler />);
|
|
expect(container.firstChild).toBeNull();
|
|
});
|
|
|
|
it('should subscribe to event bus on mount', () => {
|
|
render(<NotificationToastHandler />);
|
|
|
|
expect(mockUseEventBus).toHaveBeenCalledWith('notification:deal', expect.any(Function));
|
|
expect(mockUseEventBus).toHaveBeenCalledWith('notification:system', expect.any(Function));
|
|
expect(mockUseEventBus).toHaveBeenCalledWith('notification:error', expect.any(Function));
|
|
});
|
|
});
|
|
|
|
describe('connection events', () => {
|
|
it('should show success toast on connect when enabled', () => {
|
|
render(<NotificationToastHandler enabled={true} />);
|
|
|
|
// Trigger onConnect callback
|
|
onConnectCallback?.();
|
|
|
|
expect(mockToastSuccess).toHaveBeenCalledWith(
|
|
'Connected to live notifications',
|
|
expect.objectContaining({
|
|
duration: 2000,
|
|
icon: expect.any(String),
|
|
}),
|
|
);
|
|
});
|
|
|
|
it('should not show success toast on connect when disabled', () => {
|
|
render(<NotificationToastHandler enabled={false} />);
|
|
|
|
onConnectCallback?.();
|
|
|
|
expect(mockToastSuccess).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('should show error toast on disconnect when error exists', () => {
|
|
mockUseWebSocket.mockImplementation(
|
|
(options: { onConnect?: () => void; onDisconnect?: () => void }) => {
|
|
onConnectCallback = options?.onConnect;
|
|
onDisconnectCallback = options?.onDisconnect;
|
|
return {
|
|
isConnected: false,
|
|
error: 'Connection lost',
|
|
};
|
|
},
|
|
);
|
|
|
|
render(<NotificationToastHandler enabled={true} />);
|
|
|
|
onDisconnectCallback?.();
|
|
|
|
expect(mockToastError).toHaveBeenCalledWith(
|
|
'Disconnected from live notifications',
|
|
expect.objectContaining({
|
|
duration: 3000,
|
|
icon: expect.any(String),
|
|
}),
|
|
);
|
|
});
|
|
|
|
it('should not show disconnect toast when disabled', () => {
|
|
mockUseWebSocket.mockImplementation(
|
|
(options: { onConnect?: () => void; onDisconnect?: () => void }) => {
|
|
onConnectCallback = options?.onConnect;
|
|
onDisconnectCallback = options?.onDisconnect;
|
|
return {
|
|
isConnected: false,
|
|
error: 'Connection lost',
|
|
};
|
|
},
|
|
);
|
|
|
|
render(<NotificationToastHandler enabled={false} />);
|
|
|
|
onDisconnectCallback?.();
|
|
|
|
expect(mockToastError).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('should not show disconnect toast when no error', () => {
|
|
mockUseWebSocket.mockImplementation(
|
|
(options: { onConnect?: () => void; onDisconnect?: () => void }) => {
|
|
onConnectCallback = options?.onConnect;
|
|
onDisconnectCallback = options?.onDisconnect;
|
|
return {
|
|
isConnected: false,
|
|
error: null,
|
|
};
|
|
},
|
|
);
|
|
|
|
render(<NotificationToastHandler enabled={true} />);
|
|
|
|
onDisconnectCallback?.();
|
|
|
|
expect(mockToastError).not.toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
describe('deal notifications', () => {
|
|
it('should show toast for single deal notification', () => {
|
|
render(<NotificationToastHandler />);
|
|
|
|
const dealData: DealNotificationData = {
|
|
deals: [
|
|
{
|
|
item_name: 'Milk',
|
|
best_price_in_cents: 399,
|
|
store_name: 'Test Store',
|
|
store_id: 1,
|
|
},
|
|
],
|
|
user_id: 'user-123',
|
|
message: 'New deal found',
|
|
};
|
|
|
|
const callback = eventBusCallbacks.get('notification:deal');
|
|
callback?.(dealData);
|
|
|
|
expect(mockToastSuccess).toHaveBeenCalledWith(
|
|
expect.anything(),
|
|
expect.objectContaining({
|
|
duration: 5000,
|
|
icon: expect.any(String),
|
|
position: 'top-right',
|
|
}),
|
|
);
|
|
});
|
|
|
|
it('should show toast for multiple deals notification', () => {
|
|
render(<NotificationToastHandler />);
|
|
|
|
const dealData: DealNotificationData = {
|
|
deals: [
|
|
{
|
|
item_name: 'Milk',
|
|
best_price_in_cents: 399,
|
|
store_name: 'Store A',
|
|
store_id: 1,
|
|
},
|
|
{
|
|
item_name: 'Bread',
|
|
best_price_in_cents: 299,
|
|
store_name: 'Store B',
|
|
store_id: 2,
|
|
},
|
|
{
|
|
item_name: 'Eggs',
|
|
best_price_in_cents: 499,
|
|
store_name: 'Store C',
|
|
store_id: 3,
|
|
},
|
|
],
|
|
user_id: 'user-123',
|
|
message: 'Multiple deals found',
|
|
};
|
|
|
|
const callback = eventBusCallbacks.get('notification:deal');
|
|
callback?.(dealData);
|
|
|
|
expect(mockToastSuccess).toHaveBeenCalled();
|
|
});
|
|
|
|
it('should not show toast when disabled', () => {
|
|
render(<NotificationToastHandler enabled={false} />);
|
|
|
|
const dealData: DealNotificationData = {
|
|
deals: [
|
|
{
|
|
item_name: 'Milk',
|
|
best_price_in_cents: 399,
|
|
store_name: 'Test Store',
|
|
store_id: 1,
|
|
},
|
|
],
|
|
user_id: 'user-123',
|
|
message: 'New deal found',
|
|
};
|
|
|
|
const callback = eventBusCallbacks.get('notification:deal');
|
|
callback?.(dealData);
|
|
|
|
expect(mockToastSuccess).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('should not show toast when data is undefined', () => {
|
|
render(<NotificationToastHandler />);
|
|
|
|
const callback = eventBusCallbacks.get('notification:deal');
|
|
callback?.(undefined);
|
|
|
|
expect(mockToastSuccess).not.toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
describe('system messages', () => {
|
|
it('should show error toast for error severity', () => {
|
|
render(<NotificationToastHandler />);
|
|
|
|
const systemData: SystemMessageData = {
|
|
message: 'System error occurred',
|
|
severity: 'error',
|
|
};
|
|
|
|
const callback = eventBusCallbacks.get('notification:system');
|
|
callback?.(systemData);
|
|
|
|
expect(mockToastError).toHaveBeenCalledWith(
|
|
'System error occurred',
|
|
expect.objectContaining({
|
|
duration: 6000,
|
|
position: 'top-center',
|
|
icon: expect.any(String),
|
|
}),
|
|
);
|
|
});
|
|
|
|
it('should show warning toast for warning severity', () => {
|
|
render(<NotificationToastHandler />);
|
|
|
|
const systemData: SystemMessageData = {
|
|
message: 'System warning',
|
|
severity: 'warning',
|
|
};
|
|
|
|
// For warning, the default toast() is called
|
|
const callback = eventBusCallbacks.get('notification:system');
|
|
callback?.(systemData);
|
|
|
|
// Warning uses the regular toast function (mockToastDefault)
|
|
expect(mockToastDefault).toHaveBeenCalledWith(
|
|
'System warning',
|
|
expect.objectContaining({
|
|
duration: 4000,
|
|
position: 'top-center',
|
|
icon: expect.any(String),
|
|
}),
|
|
);
|
|
});
|
|
|
|
it('should show info toast for info severity', () => {
|
|
render(<NotificationToastHandler />);
|
|
|
|
const systemData: SystemMessageData = {
|
|
message: 'System info',
|
|
severity: 'info',
|
|
};
|
|
|
|
const callback = eventBusCallbacks.get('notification:system');
|
|
callback?.(systemData);
|
|
|
|
// Info uses the regular toast function (mockToastDefault)
|
|
expect(mockToastDefault).toHaveBeenCalledWith(
|
|
'System info',
|
|
expect.objectContaining({
|
|
duration: 4000,
|
|
position: 'top-center',
|
|
icon: expect.any(String),
|
|
}),
|
|
);
|
|
});
|
|
|
|
it('should not show toast when disabled', () => {
|
|
render(<NotificationToastHandler enabled={false} />);
|
|
|
|
const systemData: SystemMessageData = {
|
|
message: 'System error',
|
|
severity: 'error',
|
|
};
|
|
|
|
const callback = eventBusCallbacks.get('notification:system');
|
|
callback?.(systemData);
|
|
|
|
expect(mockToastError).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('should not show toast when data is undefined', () => {
|
|
render(<NotificationToastHandler />);
|
|
|
|
const callback = eventBusCallbacks.get('notification:system');
|
|
callback?.(undefined);
|
|
|
|
expect(mockToastError).not.toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
describe('error notifications', () => {
|
|
it('should show error toast with message and code', () => {
|
|
render(<NotificationToastHandler />);
|
|
|
|
const errorData = {
|
|
message: 'Something went wrong',
|
|
code: 'ERR_001',
|
|
};
|
|
|
|
const callback = eventBusCallbacks.get('notification:error');
|
|
callback?.(errorData);
|
|
|
|
expect(mockToastError).toHaveBeenCalledWith(
|
|
'Error: Something went wrong',
|
|
expect.objectContaining({
|
|
duration: 5000,
|
|
icon: expect.any(String),
|
|
}),
|
|
);
|
|
});
|
|
|
|
it('should show error toast without code', () => {
|
|
render(<NotificationToastHandler />);
|
|
|
|
const errorData = {
|
|
message: 'Something went wrong',
|
|
};
|
|
|
|
const callback = eventBusCallbacks.get('notification:error');
|
|
callback?.(errorData);
|
|
|
|
expect(mockToastError).toHaveBeenCalledWith(
|
|
'Error: Something went wrong',
|
|
expect.objectContaining({
|
|
duration: 5000,
|
|
}),
|
|
);
|
|
});
|
|
|
|
it('should not show toast when disabled', () => {
|
|
render(<NotificationToastHandler enabled={false} />);
|
|
|
|
const errorData = {
|
|
message: 'Something went wrong',
|
|
};
|
|
|
|
const callback = eventBusCallbacks.get('notification:error');
|
|
callback?.(errorData);
|
|
|
|
expect(mockToastError).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('should not show toast when data is undefined', () => {
|
|
render(<NotificationToastHandler />);
|
|
|
|
const callback = eventBusCallbacks.get('notification:error');
|
|
callback?.(undefined);
|
|
|
|
expect(mockToastError).not.toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
describe('sound playback', () => {
|
|
it('should not play sound by default', () => {
|
|
const audioPlayMock = vi.fn().mockResolvedValue(undefined);
|
|
const AudioMock = vi.fn().mockImplementation(() => ({
|
|
play: audioPlayMock,
|
|
volume: 0,
|
|
}));
|
|
vi.stubGlobal('Audio', AudioMock);
|
|
|
|
render(<NotificationToastHandler playSound={false} />);
|
|
|
|
const dealData: DealNotificationData = {
|
|
deals: [
|
|
{
|
|
item_name: 'Milk',
|
|
best_price_in_cents: 399,
|
|
store_name: 'Test Store',
|
|
store_id: 1,
|
|
},
|
|
],
|
|
user_id: 'user-123',
|
|
message: 'New deal',
|
|
};
|
|
|
|
const callback = eventBusCallbacks.get('notification:deal');
|
|
callback?.(dealData);
|
|
|
|
expect(AudioMock).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('should create Audio instance when playSound is true', () => {
|
|
const audioPlayMock = vi.fn().mockResolvedValue(undefined);
|
|
const AudioMock = vi.fn().mockImplementation(() => ({
|
|
play: audioPlayMock,
|
|
volume: 0,
|
|
}));
|
|
vi.stubGlobal('Audio', AudioMock);
|
|
|
|
render(<NotificationToastHandler playSound={true} />);
|
|
|
|
const dealData: DealNotificationData = {
|
|
deals: [
|
|
{
|
|
item_name: 'Milk',
|
|
best_price_in_cents: 399,
|
|
store_name: 'Test Store',
|
|
store_id: 1,
|
|
},
|
|
],
|
|
user_id: 'user-123',
|
|
message: 'New deal',
|
|
};
|
|
|
|
const callback = eventBusCallbacks.get('notification:deal');
|
|
callback?.(dealData);
|
|
|
|
// Verify Audio constructor was called with correct URL
|
|
expect(AudioMock).toHaveBeenCalledWith('/notification-sound.mp3');
|
|
});
|
|
|
|
it('should use custom sound URL', () => {
|
|
const audioPlayMock = vi.fn().mockResolvedValue(undefined);
|
|
const AudioMock = vi.fn().mockImplementation(() => ({
|
|
play: audioPlayMock,
|
|
volume: 0,
|
|
}));
|
|
vi.stubGlobal('Audio', AudioMock);
|
|
|
|
render(<NotificationToastHandler playSound={true} soundUrl="/custom-sound.mp3" />);
|
|
|
|
const dealData: DealNotificationData = {
|
|
deals: [
|
|
{
|
|
item_name: 'Milk',
|
|
best_price_in_cents: 399,
|
|
store_name: 'Test Store',
|
|
store_id: 1,
|
|
},
|
|
],
|
|
user_id: 'user-123',
|
|
message: 'New deal',
|
|
};
|
|
|
|
const callback = eventBusCallbacks.get('notification:deal');
|
|
callback?.(dealData);
|
|
|
|
expect(AudioMock).toHaveBeenCalledWith('/custom-sound.mp3');
|
|
});
|
|
|
|
it('should handle audio play failure gracefully', () => {
|
|
vi.spyOn(console, 'warn').mockImplementation(() => {});
|
|
const audioPlayMock = vi.fn().mockRejectedValue(new Error('Autoplay blocked'));
|
|
const AudioMock = vi.fn().mockImplementation(() => ({
|
|
play: audioPlayMock,
|
|
volume: 0,
|
|
}));
|
|
vi.stubGlobal('Audio', AudioMock);
|
|
|
|
render(<NotificationToastHandler playSound={true} />);
|
|
|
|
const dealData: DealNotificationData = {
|
|
deals: [
|
|
{
|
|
item_name: 'Milk',
|
|
best_price_in_cents: 399,
|
|
store_name: 'Test Store',
|
|
store_id: 1,
|
|
},
|
|
],
|
|
user_id: 'user-123',
|
|
message: 'New deal',
|
|
};
|
|
|
|
const callback = eventBusCallbacks.get('notification:deal');
|
|
// Should not throw even if play() fails
|
|
expect(() => callback?.(dealData)).not.toThrow();
|
|
// Audio constructor should still be called
|
|
expect(AudioMock).toHaveBeenCalled();
|
|
});
|
|
|
|
it('should handle Audio constructor failure gracefully', () => {
|
|
vi.spyOn(console, 'warn').mockImplementation(() => {});
|
|
const AudioMock = vi.fn().mockImplementation(() => {
|
|
throw new Error('Audio not supported');
|
|
});
|
|
vi.stubGlobal('Audio', AudioMock);
|
|
|
|
render(<NotificationToastHandler playSound={true} />);
|
|
|
|
const dealData: DealNotificationData = {
|
|
deals: [
|
|
{
|
|
item_name: 'Milk',
|
|
best_price_in_cents: 399,
|
|
store_name: 'Test Store',
|
|
store_id: 1,
|
|
},
|
|
],
|
|
user_id: 'user-123',
|
|
message: 'New deal',
|
|
};
|
|
|
|
const callback = eventBusCallbacks.get('notification:deal');
|
|
// Should not throw
|
|
expect(() => callback?.(dealData)).not.toThrow();
|
|
});
|
|
});
|
|
|
|
describe('persistent connection error', () => {
|
|
it('should show error toast after delay when connection error persists', () => {
|
|
mockUseWebSocket.mockImplementation(
|
|
(options: { onConnect?: () => void; onDisconnect?: () => void }) => {
|
|
onConnectCallback = options?.onConnect;
|
|
onDisconnectCallback = options?.onDisconnect;
|
|
return {
|
|
isConnected: false,
|
|
error: 'Connection failed',
|
|
};
|
|
},
|
|
);
|
|
|
|
render(<NotificationToastHandler enabled={true} />);
|
|
|
|
// Fast-forward 5 seconds
|
|
act(() => {
|
|
vi.advanceTimersByTime(5000);
|
|
});
|
|
|
|
expect(mockToastError).toHaveBeenCalledWith(
|
|
'Unable to connect to live notifications. Some features may be limited.',
|
|
expect.objectContaining({
|
|
duration: 5000,
|
|
icon: expect.any(String),
|
|
}),
|
|
);
|
|
});
|
|
|
|
it('should not show error toast before delay', () => {
|
|
mockUseWebSocket.mockImplementation(
|
|
(options: { onConnect?: () => void; onDisconnect?: () => void }) => {
|
|
onConnectCallback = options?.onConnect;
|
|
onDisconnectCallback = options?.onDisconnect;
|
|
return {
|
|
isConnected: false,
|
|
error: 'Connection failed',
|
|
};
|
|
},
|
|
);
|
|
|
|
render(<NotificationToastHandler enabled={true} />);
|
|
|
|
// Advance only 4 seconds
|
|
act(() => {
|
|
vi.advanceTimersByTime(4000);
|
|
});
|
|
|
|
expect(mockToastError).not.toHaveBeenCalledWith(
|
|
expect.stringContaining('Unable to connect'),
|
|
expect.anything(),
|
|
);
|
|
});
|
|
|
|
it('should not show persistent error toast when disabled', () => {
|
|
mockUseWebSocket.mockImplementation(
|
|
(options: { onConnect?: () => void; onDisconnect?: () => void }) => {
|
|
onConnectCallback = options?.onConnect;
|
|
onDisconnectCallback = options?.onDisconnect;
|
|
return {
|
|
isConnected: false,
|
|
error: 'Connection failed',
|
|
};
|
|
},
|
|
);
|
|
|
|
render(<NotificationToastHandler enabled={false} />);
|
|
|
|
act(() => {
|
|
vi.advanceTimersByTime(5000);
|
|
});
|
|
|
|
expect(mockToastError).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('should clear timeout on unmount', () => {
|
|
mockUseWebSocket.mockImplementation(
|
|
(options: { onConnect?: () => void; onDisconnect?: () => void }) => {
|
|
onConnectCallback = options?.onConnect;
|
|
onDisconnectCallback = options?.onDisconnect;
|
|
return {
|
|
isConnected: false,
|
|
error: 'Connection failed',
|
|
};
|
|
},
|
|
);
|
|
|
|
const { unmount } = render(<NotificationToastHandler enabled={true} />);
|
|
|
|
// Unmount before timer fires
|
|
unmount();
|
|
|
|
act(() => {
|
|
vi.advanceTimersByTime(5000);
|
|
});
|
|
|
|
// The toast should not be called because component unmounted
|
|
expect(mockToastError).not.toHaveBeenCalledWith(
|
|
expect.stringContaining('Unable to connect'),
|
|
expect.anything(),
|
|
);
|
|
});
|
|
|
|
it('should not show persistent error toast when there is no error', () => {
|
|
mockUseWebSocket.mockImplementation(
|
|
(options: { onConnect?: () => void; onDisconnect?: () => void }) => {
|
|
onConnectCallback = options?.onConnect;
|
|
onDisconnectCallback = options?.onDisconnect;
|
|
return {
|
|
isConnected: false,
|
|
error: null,
|
|
};
|
|
},
|
|
);
|
|
|
|
render(<NotificationToastHandler enabled={true} />);
|
|
|
|
act(() => {
|
|
vi.advanceTimersByTime(5000);
|
|
});
|
|
|
|
expect(mockToastError).not.toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
describe('default props', () => {
|
|
it('should default enabled to true', () => {
|
|
render(<NotificationToastHandler />);
|
|
|
|
onConnectCallback?.();
|
|
|
|
expect(mockToastSuccess).toHaveBeenCalled();
|
|
});
|
|
|
|
it('should default playSound to false', () => {
|
|
const AudioMock = vi.fn();
|
|
vi.stubGlobal('Audio', AudioMock);
|
|
|
|
render(<NotificationToastHandler />);
|
|
|
|
const dealData: DealNotificationData = {
|
|
deals: [
|
|
{
|
|
item_name: 'Milk',
|
|
best_price_in_cents: 399,
|
|
store_name: 'Test Store',
|
|
store_id: 1,
|
|
},
|
|
],
|
|
user_id: 'user-123',
|
|
message: 'New deal',
|
|
};
|
|
|
|
const callback = eventBusCallbacks.get('notification:deal');
|
|
callback?.(dealData);
|
|
|
|
expect(AudioMock).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('should default soundUrl to /notification-sound.mp3', () => {
|
|
const audioPlayMock = vi.fn().mockResolvedValue(undefined);
|
|
const AudioMock = vi.fn().mockImplementation(() => ({
|
|
play: audioPlayMock,
|
|
volume: 0,
|
|
}));
|
|
vi.stubGlobal('Audio', AudioMock);
|
|
|
|
render(<NotificationToastHandler playSound={true} />);
|
|
|
|
const dealData: DealNotificationData = {
|
|
deals: [
|
|
{
|
|
item_name: 'Milk',
|
|
best_price_in_cents: 399,
|
|
store_name: 'Test Store',
|
|
store_id: 1,
|
|
},
|
|
],
|
|
user_id: 'user-123',
|
|
message: 'New deal',
|
|
};
|
|
|
|
const callback = eventBusCallbacks.get('notification:deal');
|
|
callback?.(dealData);
|
|
|
|
expect(AudioMock).toHaveBeenCalledWith('/notification-sound.mp3');
|
|
});
|
|
});
|
|
});
|