mirror of
https://github.com/Dispatcharr/Dispatcharr.git
synced 2026-07-21 09:09:22 +00:00
Updated tests after sync
This commit is contained in:
parent
08d79fb4db
commit
f3304bd976
3 changed files with 1024 additions and 57 deletions
|
|
@ -5,20 +5,21 @@ import { Field } from '../Field';
|
|||
// Mock Mantine components
|
||||
vi.mock('@mantine/core', async () => {
|
||||
return {
|
||||
TextInput: ({ label, description, value, onChange }) => (
|
||||
TextInput: ({ label, description, value, onChange, type, placeholder }) => (
|
||||
<div>
|
||||
<label htmlFor="text-input">{label}</label>
|
||||
<input
|
||||
id="text-input"
|
||||
type="text"
|
||||
type={type || 'text'}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
placeholder={placeholder}
|
||||
aria-describedby={description}
|
||||
/>
|
||||
{description && <div>{description}</div>}
|
||||
</div>
|
||||
),
|
||||
NumberInput: ({ label, description, value, onChange }) => (
|
||||
NumberInput: ({ label, description, value, onChange, placeholder }) => (
|
||||
<div>
|
||||
<label htmlFor="number-input">{label}</label>
|
||||
<input
|
||||
|
|
@ -26,6 +27,20 @@ vi.mock('@mantine/core', async () => {
|
|||
type="number"
|
||||
value={value}
|
||||
onChange={(e) => onChange(Number(e.target.value))}
|
||||
placeholder={placeholder}
|
||||
aria-describedby={description}
|
||||
/>
|
||||
{description && <div>{description}</div>}
|
||||
</div>
|
||||
),
|
||||
Textarea: ({ label, description, value, onChange, placeholder }) => (
|
||||
<div>
|
||||
<label htmlFor="textarea-input">{label}</label>
|
||||
<textarea
|
||||
id="textarea-input"
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
placeholder={placeholder}
|
||||
aria-describedby={description}
|
||||
/>
|
||||
{description && <div>{description}</div>}
|
||||
|
|
@ -44,13 +59,14 @@ vi.mock('@mantine/core', async () => {
|
|||
{description && <div>{description}</div>}
|
||||
</div>
|
||||
),
|
||||
Select: ({ label, description, value, data, onChange }) => (
|
||||
Select: ({ label, description, value, data, onChange, placeholder }) => (
|
||||
<div>
|
||||
<label htmlFor="select-input">{label}</label>
|
||||
<select
|
||||
id="select-input"
|
||||
value={value}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
placeholder={placeholder}
|
||||
aria-describedby={description}
|
||||
>
|
||||
{data.map((option) => (
|
||||
|
|
@ -62,6 +78,11 @@ vi.mock('@mantine/core', async () => {
|
|||
{description && <div>{description}</div>}
|
||||
</div>
|
||||
),
|
||||
Text: ({ children, fw, size, c }) => (
|
||||
<div data-fw={fw} data-size={size} data-color={c}>
|
||||
{children}
|
||||
</div>
|
||||
),
|
||||
};
|
||||
});
|
||||
|
||||
|
|
@ -371,6 +392,399 @@ describe('Field', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('Textarea (text type)', () => {
|
||||
it('should render Textarea for text type', () => {
|
||||
const field = {
|
||||
id: 'bio',
|
||||
type: 'text',
|
||||
label: 'Biography',
|
||||
help_text: 'Enter your biography',
|
||||
default: '',
|
||||
};
|
||||
|
||||
render(<Field field={field} value="" onChange={mockOnChange} />);
|
||||
|
||||
expect(screen.getByLabelText('Biography')).toBeInTheDocument();
|
||||
expect(screen.getByText('Enter your biography')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should use provided value', () => {
|
||||
const field = {
|
||||
id: 'bio',
|
||||
type: 'text',
|
||||
label: 'Biography',
|
||||
default: '',
|
||||
};
|
||||
|
||||
render(<Field field={field} value="My bio" onChange={mockOnChange} />);
|
||||
|
||||
expect(screen.getByLabelText('Biography')).toHaveValue('My bio');
|
||||
});
|
||||
|
||||
it('should use default value when value is null', () => {
|
||||
const field = {
|
||||
id: 'bio',
|
||||
type: 'text',
|
||||
label: 'Biography',
|
||||
default: 'Default bio',
|
||||
};
|
||||
|
||||
render(<Field field={field} value={null} onChange={mockOnChange} />);
|
||||
|
||||
expect(screen.getByLabelText('Biography')).toHaveValue('Default bio');
|
||||
});
|
||||
|
||||
it('should call onChange with field id and value', () => {
|
||||
const field = {
|
||||
id: 'bio',
|
||||
type: 'text',
|
||||
label: 'Biography',
|
||||
default: '',
|
||||
};
|
||||
|
||||
render(<Field field={field} value="" onChange={mockOnChange} />);
|
||||
|
||||
fireEvent.change(screen.getByLabelText('Biography'), {
|
||||
target: { value: 'New bio text' },
|
||||
});
|
||||
|
||||
expect(mockOnChange).toHaveBeenCalledWith('bio', 'New bio text');
|
||||
});
|
||||
|
||||
it('should render with placeholder', () => {
|
||||
const field = {
|
||||
id: 'bio',
|
||||
type: 'text',
|
||||
label: 'Biography',
|
||||
placeholder: 'Enter your bio here...',
|
||||
default: '',
|
||||
};
|
||||
|
||||
render(<Field field={field} value="" onChange={mockOnChange} />);
|
||||
|
||||
expect(screen.getByLabelText('Biography')).toHaveAttribute(
|
||||
'placeholder',
|
||||
'Enter your bio here...'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Info type', () => {
|
||||
it('should render info with label and description', () => {
|
||||
const field = {
|
||||
id: 'info1',
|
||||
type: 'info',
|
||||
label: 'Important Information',
|
||||
description: 'This is important info',
|
||||
};
|
||||
|
||||
render(<Field field={field} value={null} onChange={mockOnChange} />);
|
||||
|
||||
expect(screen.getByText('Important Information')).toBeInTheDocument();
|
||||
expect(screen.getByText('This is important info')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render info with only description', () => {
|
||||
const field = {
|
||||
id: 'info2',
|
||||
type: 'info',
|
||||
description: 'Just a description',
|
||||
};
|
||||
|
||||
render(<Field field={field} value={null} onChange={mockOnChange} />);
|
||||
|
||||
expect(screen.getByText('Just a description')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render info with only label', () => {
|
||||
const field = {
|
||||
id: 'info3',
|
||||
type: 'info',
|
||||
label: 'Just a label',
|
||||
};
|
||||
|
||||
render(<Field field={field} value={null} onChange={mockOnChange} />);
|
||||
|
||||
expect(screen.getByText('Just a label')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should prioritize help_text over description', () => {
|
||||
const field = {
|
||||
id: 'info4',
|
||||
type: 'info',
|
||||
label: 'Title',
|
||||
help_text: 'Help text takes priority',
|
||||
description: 'This should not appear',
|
||||
};
|
||||
|
||||
render(<Field field={field} value={null} onChange={mockOnChange} />);
|
||||
|
||||
expect(screen.getByText('Help text takes priority')).toBeInTheDocument();
|
||||
expect(screen.queryByText('This should not appear')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should use field.value if no help_text or description', () => {
|
||||
const field = {
|
||||
id: 'info5',
|
||||
type: 'info',
|
||||
label: 'Title',
|
||||
value: 'Value text',
|
||||
};
|
||||
|
||||
render(<Field field={field} value={null} onChange={mockOnChange} />);
|
||||
|
||||
expect(screen.getByText('Value text')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should not call onChange for info type', () => {
|
||||
const field = {
|
||||
id: 'info6',
|
||||
type: 'info',
|
||||
label: 'Read-only Info',
|
||||
description: 'Cannot be changed',
|
||||
};
|
||||
|
||||
render(<Field field={field} value={null} onChange={mockOnChange} />);
|
||||
|
||||
expect(mockOnChange).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Password input type', () => {
|
||||
it('should render password input when input_type is password', () => {
|
||||
const field = {
|
||||
id: 'password',
|
||||
type: 'string',
|
||||
label: 'Password',
|
||||
input_type: 'password',
|
||||
default: '',
|
||||
};
|
||||
|
||||
render(<Field field={field} value="" onChange={mockOnChange} />);
|
||||
|
||||
expect(screen.getByLabelText('Password')).toHaveAttribute('type', 'password');
|
||||
});
|
||||
|
||||
it('should render text input when input_type is not password', () => {
|
||||
const field = {
|
||||
id: 'username',
|
||||
type: 'string',
|
||||
label: 'Username',
|
||||
input_type: 'text',
|
||||
default: '',
|
||||
};
|
||||
|
||||
render(<Field field={field} value="" onChange={mockOnChange} />);
|
||||
|
||||
expect(screen.getByLabelText('Username')).toHaveAttribute('type', 'text');
|
||||
});
|
||||
|
||||
it('should default to text input when input_type is undefined', () => {
|
||||
const field = {
|
||||
id: 'email',
|
||||
type: 'string',
|
||||
label: 'Email',
|
||||
default: '',
|
||||
};
|
||||
|
||||
render(<Field field={field} value="" onChange={mockOnChange} />);
|
||||
|
||||
expect(screen.getByLabelText('Email')).toHaveAttribute('type', 'text');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Description priority', () => {
|
||||
it('should prioritize help_text over description', () => {
|
||||
const field = {
|
||||
id: 'test',
|
||||
type: 'string',
|
||||
label: 'Test',
|
||||
help_text: 'Help text',
|
||||
description: 'Description text',
|
||||
default: '',
|
||||
};
|
||||
|
||||
render(<Field field={field} value="" onChange={mockOnChange} />);
|
||||
|
||||
expect(screen.getByText('Help text')).toBeInTheDocument();
|
||||
expect(screen.queryByText('Description text')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should use description when help_text is not provided', () => {
|
||||
const field = {
|
||||
id: 'test',
|
||||
type: 'string',
|
||||
label: 'Test',
|
||||
description: 'Description text',
|
||||
default: '',
|
||||
};
|
||||
|
||||
render(<Field field={field} value="" onChange={mockOnChange} />);
|
||||
|
||||
expect(screen.getByText('Description text')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should use field.value when neither help_text nor description provided', () => {
|
||||
const field = {
|
||||
id: 'test',
|
||||
type: 'string',
|
||||
label: 'Test',
|
||||
value: 'Value text',
|
||||
default: '',
|
||||
};
|
||||
|
||||
render(<Field field={field} value="" onChange={mockOnChange} />);
|
||||
|
||||
expect(screen.getByText('Value text')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should not show description when all are undefined', () => {
|
||||
const field = {
|
||||
id: 'test',
|
||||
type: 'string',
|
||||
label: 'Test',
|
||||
default: '',
|
||||
};
|
||||
|
||||
render(<Field field={field} value="" onChange={mockOnChange} />);
|
||||
|
||||
expect(screen.getByLabelText('Test')).toBeInTheDocument();
|
||||
// No description should be present
|
||||
});
|
||||
});
|
||||
|
||||
describe('Placeholder handling', () => {
|
||||
it('should render placeholder for TextInput', () => {
|
||||
const field = {
|
||||
id: 'name',
|
||||
type: 'string',
|
||||
label: 'Name',
|
||||
placeholder: 'Enter your name',
|
||||
default: '',
|
||||
};
|
||||
|
||||
render(<Field field={field} value="" onChange={mockOnChange} />);
|
||||
|
||||
expect(screen.getByLabelText('Name')).toHaveAttribute(
|
||||
'placeholder',
|
||||
'Enter your name'
|
||||
);
|
||||
});
|
||||
|
||||
it('should render placeholder for NumberInput', () => {
|
||||
const field = {
|
||||
id: 'age',
|
||||
type: 'number',
|
||||
label: 'Age',
|
||||
placeholder: 'Enter your age',
|
||||
default: 0,
|
||||
};
|
||||
|
||||
render(<Field field={field} value={null} onChange={mockOnChange} />);
|
||||
|
||||
expect(screen.getByLabelText('Age')).toHaveAttribute(
|
||||
'placeholder',
|
||||
'Enter your age'
|
||||
);
|
||||
});
|
||||
|
||||
it('should render placeholder for Select', () => {
|
||||
const field = {
|
||||
id: 'country',
|
||||
type: 'select',
|
||||
label: 'Country',
|
||||
placeholder: 'Select a country',
|
||||
default: '',
|
||||
options: [
|
||||
{ value: 'us', label: 'United States' },
|
||||
{ value: 'ca', label: 'Canada' },
|
||||
],
|
||||
};
|
||||
|
||||
render(<Field field={field} value="" onChange={mockOnChange} />);
|
||||
|
||||
expect(screen.getByLabelText('Country')).toHaveAttribute(
|
||||
'placeholder',
|
||||
'Select a country'
|
||||
);
|
||||
});
|
||||
|
||||
it('should render placeholder for Textarea', () => {
|
||||
const field = {
|
||||
id: 'bio',
|
||||
type: 'text',
|
||||
label: 'Biography',
|
||||
placeholder: 'Tell us about yourself',
|
||||
default: '',
|
||||
};
|
||||
|
||||
render(<Field field={field} value="" onChange={mockOnChange} />);
|
||||
|
||||
expect(screen.getByLabelText('Biography')).toHaveAttribute(
|
||||
'placeholder',
|
||||
'Tell us about yourself'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Edge cases', () => {
|
||||
it('should handle empty string default value', () => {
|
||||
const field = {
|
||||
id: 'test',
|
||||
type: 'string',
|
||||
label: 'Test',
|
||||
default: '',
|
||||
};
|
||||
|
||||
render(<Field field={field} value={null} onChange={mockOnChange} />);
|
||||
|
||||
expect(screen.getByLabelText('Test')).toHaveValue('');
|
||||
});
|
||||
|
||||
it('should handle 0 as valid number value', () => {
|
||||
const field = {
|
||||
id: 'count',
|
||||
type: 'number',
|
||||
label: 'Count',
|
||||
default: 10,
|
||||
};
|
||||
|
||||
render(<Field field={field} value={0} onChange={mockOnChange} />);
|
||||
|
||||
expect(screen.getByLabelText('Count')).toHaveValue(0);
|
||||
});
|
||||
|
||||
it('should handle false as valid boolean value', () => {
|
||||
const field = {
|
||||
id: 'enabled',
|
||||
type: 'boolean',
|
||||
label: 'Enabled',
|
||||
default: true,
|
||||
};
|
||||
|
||||
render(<Field field={field} value={false} onChange={mockOnChange} />);
|
||||
|
||||
expect(screen.getByLabelText('Enabled')).not.toBeChecked();
|
||||
});
|
||||
|
||||
it('should handle empty string as valid select value', () => {
|
||||
const field = {
|
||||
id: 'status',
|
||||
type: 'select',
|
||||
label: 'Status',
|
||||
default: 'active',
|
||||
options: [
|
||||
{ value: '', label: 'None' },
|
||||
{ value: 'active', label: 'Active' },
|
||||
],
|
||||
};
|
||||
|
||||
render(<Field field={field} value="" onChange={mockOnChange} />);
|
||||
|
||||
expect(screen.getByLabelText('Status')).toHaveValue('');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Default fallback', () => {
|
||||
it('should render TextInput for unknown type', () => {
|
||||
const field = {
|
||||
|
|
|
|||
|
|
@ -28,6 +28,18 @@ vi.mock('@mantine/core', async () => {
|
|||
};
|
||||
});
|
||||
|
||||
// Helper function to create programs at specific times
|
||||
const createProgramAtTime = (id, startHour, durationMinutes) => {
|
||||
const timelineStart = new Date('2024-01-01T00:00:00Z');
|
||||
const startMs = timelineStart.getTime() + startHour * 60 * 60 * 1000;
|
||||
return {
|
||||
id,
|
||||
title: `Program ${id}`,
|
||||
startMs,
|
||||
endMs: startMs + durationMinutes * 60 * 1000,
|
||||
};
|
||||
};
|
||||
|
||||
describe('GuideRow', () => {
|
||||
const mockChannel = {
|
||||
id: 'channel-1',
|
||||
|
|
@ -64,6 +76,9 @@ describe('GuideRow', () => {
|
|||
)),
|
||||
handleLogoClick: vi.fn(),
|
||||
contentWidth: 1920,
|
||||
guideScrollLeftRef: { current: 0 },
|
||||
viewportWidth: 1920,
|
||||
timelineStartMs: new Date('2024-01-01T00:00:00Z').getTime(),
|
||||
};
|
||||
|
||||
const mockStyle = {
|
||||
|
|
@ -160,24 +175,26 @@ describe('GuideRow', () => {
|
|||
|
||||
describe('Programs Rendering', () => {
|
||||
it('should render programs when channel has programs', () => {
|
||||
render(
|
||||
<GuideRow index={0} style={mockStyle} data={mockData} />
|
||||
);
|
||||
// Create program at hour 0 (definitely within viewport at scrollLeft 0)
|
||||
const visibleProgram = createProgramAtTime('program-1', 0, 60);
|
||||
|
||||
expect(screen.getByTestId(`program-${mockProgram.id}`)).toBeInTheDocument();
|
||||
expect(screen.getByText('Test Program')).toBeInTheDocument();
|
||||
expect(mockData.renderProgram).toHaveBeenCalledWith(
|
||||
mockProgram,
|
||||
undefined,
|
||||
mockChannel
|
||||
);
|
||||
const data = {
|
||||
...mockData,
|
||||
programsByChannelId: new Map([[mockChannel.id, [visibleProgram]]]),
|
||||
};
|
||||
|
||||
render(<GuideRow index={0} style={mockStyle} data={data} />);
|
||||
|
||||
expect(screen.getByTestId('program-program-1')).toBeInTheDocument();
|
||||
expect(mockData.renderProgram).toHaveBeenCalledWith(visibleProgram, undefined, mockChannel);
|
||||
});
|
||||
|
||||
it('should render multiple programs', () => {
|
||||
const programs = [
|
||||
mockProgram,
|
||||
{ ...mockProgram, id: 'program-2', title: 'Another Program' },
|
||||
createProgramAtTime('prog-1', 0, 60),
|
||||
createProgramAtTime('prog-2', 1, 30),
|
||||
];
|
||||
|
||||
const data = {
|
||||
...mockData,
|
||||
programsByChannelId: new Map([[mockChannel.id, programs]]),
|
||||
|
|
@ -185,9 +202,8 @@ describe('GuideRow', () => {
|
|||
|
||||
render(<GuideRow index={0} style={mockStyle} data={data} />);
|
||||
|
||||
expect(screen.getByText('Test Program')).toBeInTheDocument();
|
||||
expect(screen.getByText('Another Program')).toBeInTheDocument();
|
||||
expect(mockData.renderProgram).toHaveBeenCalledTimes(2);
|
||||
expect(screen.getByTestId('program-prog-1')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('program-prog-2')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render placeholder when channel has no programs', () => {
|
||||
|
|
@ -199,7 +215,7 @@ describe('GuideRow', () => {
|
|||
render(<GuideRow index={0} style={mockStyle} data={data} />);
|
||||
|
||||
const placeholders = screen.getAllByText('No program data');
|
||||
expect(placeholders.length).toBe(Math.ceil(24 / 2));
|
||||
expect(placeholders.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('should render placeholder when programsByChannelId does not contain channel', () => {
|
||||
|
|
@ -211,7 +227,7 @@ describe('GuideRow', () => {
|
|||
render(<GuideRow index={0} style={mockStyle} data={data} />);
|
||||
|
||||
const placeholders = screen.getAllByText('No program data');
|
||||
expect(placeholders.length).toBe(Math.ceil(24 / 2));
|
||||
expect(placeholders.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('should position placeholder programs correctly', () => {
|
||||
|
|
@ -252,12 +268,10 @@ describe('GuideRow', () => {
|
|||
});
|
||||
|
||||
it('should show play icon on hover', () => {
|
||||
const data = {
|
||||
...mockData,
|
||||
hoveredChannelId: mockChannel.id,
|
||||
};
|
||||
render(<GuideRow index={0} style={mockStyle} data={mockData} />);
|
||||
|
||||
render(<GuideRow index={0} style={mockStyle} data={data} />);
|
||||
const logo = screen.getByAltText('Test Channel').closest('.channel-logo');
|
||||
fireEvent.mouseEnter(logo);
|
||||
|
||||
expect(screen.getByTestId('play-icon')).toBeInTheDocument();
|
||||
});
|
||||
|
|
@ -269,28 +283,6 @@ describe('GuideRow', () => {
|
|||
|
||||
expect(screen.queryByTestId('play-icon')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should call setHoveredChannelId on mouse enter', () => {
|
||||
render(
|
||||
<GuideRow index={0} style={mockStyle} data={mockData} />
|
||||
);
|
||||
|
||||
const logo = screen.getByAltText('Test Channel').closest('.channel-logo');
|
||||
fireEvent.mouseEnter(logo);
|
||||
|
||||
expect(mockData.setHoveredChannelId).toHaveBeenCalledWith(mockChannel.id);
|
||||
});
|
||||
|
||||
it('should call setHoveredChannelId with null on mouse leave', () => {
|
||||
render(
|
||||
<GuideRow index={0} style={mockStyle} data={mockData} />
|
||||
);
|
||||
|
||||
const logo = screen.getByAltText('Test Channel').closest('.channel-logo');
|
||||
fireEvent.mouseLeave(logo);
|
||||
|
||||
expect(mockData.setHoveredChannelId).toHaveBeenCalledWith(null);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Layout and Styling', () => {
|
||||
|
|
@ -332,4 +324,392 @@ describe('GuideRow', () => {
|
|||
expect(imageContainer).toHaveAttribute('h', `${customHeight - 32}px`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Horizontal Viewport Culling', () => {
|
||||
it('should only render programs visible in viewport', () => {
|
||||
const programs = [
|
||||
createProgramAtTime('prog-1', 0, 60), // Hour 0
|
||||
createProgramAtTime('prog-2', 6, 60), // Hour 6
|
||||
createProgramAtTime('prog-3', 12, 60), // Hour 12
|
||||
createProgramAtTime('prog-4', 18, 60), // Hour 18
|
||||
];
|
||||
|
||||
const data = {
|
||||
...mockData,
|
||||
programsByChannelId: new Map([[mockChannel.id, programs]]),
|
||||
guideScrollLeftRef: { current: HOUR_WIDTH * 6 }, // Scroll to hour 6
|
||||
viewportWidth: HOUR_WIDTH * 4, // Show 4 hours
|
||||
timelineStartMs: new Date('2024-01-01T00:00:00Z').getTime(),
|
||||
};
|
||||
|
||||
render(<GuideRow index={0} style={mockStyle} data={data} />);
|
||||
|
||||
// Program at hour 6 should be visible
|
||||
expect(screen.getByTestId('program-prog-2')).toBeInTheDocument();
|
||||
|
||||
// Programs outside viewport + buffer should not be rendered
|
||||
expect(mockData.renderProgram).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should render programs within buffer zone', () => {
|
||||
const programs = [
|
||||
createProgramAtTime('prog-1', 0, 60),
|
||||
createProgramAtTime('prog-2', 1, 60),
|
||||
];
|
||||
|
||||
const data = {
|
||||
...mockData,
|
||||
programsByChannelId: new Map([[mockChannel.id, programs]]),
|
||||
guideScrollLeftRef: { current: HOUR_WIDTH * 2 },
|
||||
viewportWidth: HOUR_WIDTH * 2,
|
||||
timelineStartMs: new Date('2024-01-01T00:00:00Z').getTime(),
|
||||
};
|
||||
|
||||
render(<GuideRow index={0} style={mockStyle} data={data} />);
|
||||
|
||||
// Programs within H_BUFFER (600px) should be rendered
|
||||
const renderedPrograms = mockData.renderProgram.mock.calls.map(
|
||||
call => call[0]
|
||||
);
|
||||
|
||||
expect(renderedPrograms.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('should not render programs completely outside viewport and buffer', () => {
|
||||
const programs = [
|
||||
createProgramAtTime('prog-far-left', 0, 60),
|
||||
createProgramAtTime('prog-visible', 10, 60),
|
||||
createProgramAtTime('prog-far-right', 22, 60),
|
||||
];
|
||||
|
||||
const data = {
|
||||
...mockData,
|
||||
programsByChannelId: new Map([[mockChannel.id, programs]]),
|
||||
guideScrollLeftRef: { current: HOUR_WIDTH * 10 },
|
||||
viewportWidth: HOUR_WIDTH * 2,
|
||||
timelineStartMs: new Date('2024-01-01T00:00:00Z').getTime(),
|
||||
};
|
||||
|
||||
render(<GuideRow index={0} style={mockStyle} data={data} />);
|
||||
|
||||
const renderedPrograms = mockData.renderProgram.mock.calls.map(
|
||||
call => call[0].id
|
||||
);
|
||||
|
||||
// Only visible program should be rendered
|
||||
expect(renderedPrograms).toContain('prog-visible');
|
||||
expect(renderedPrograms).not.toContain('prog-far-left');
|
||||
expect(renderedPrograms).not.toContain('prog-far-right');
|
||||
});
|
||||
|
||||
it('should handle edge case where program spans viewport boundary', () => {
|
||||
const programs = [
|
||||
createProgramAtTime('prog-spanning', 5, 180), // 3-hour program
|
||||
];
|
||||
|
||||
const data = {
|
||||
...mockData,
|
||||
programsByChannelId: new Map([[mockChannel.id, programs]]),
|
||||
guideScrollLeftRef: { current: HOUR_WIDTH * 6 },
|
||||
viewportWidth: HOUR_WIDTH * 2,
|
||||
timelineStartMs: new Date('2024-01-01T00:00:00Z').getTime(),
|
||||
};
|
||||
|
||||
render(<GuideRow index={0} style={mockStyle} data={data} />);
|
||||
|
||||
// Program spanning viewport should be visible
|
||||
expect(screen.getByTestId('program-prog-spanning')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should update visible programs when scroll position changes', () => {
|
||||
const programs = [
|
||||
createProgramAtTime('prog-1', 0, 60),
|
||||
createProgramAtTime('prog-2', 12, 60),
|
||||
];
|
||||
|
||||
const scrollRef = { current: 0 };
|
||||
const data = {
|
||||
...mockData,
|
||||
programsByChannelId: new Map([[mockChannel.id, programs]]),
|
||||
guideScrollLeftRef: scrollRef,
|
||||
viewportWidth: HOUR_WIDTH * 4,
|
||||
timelineStartMs: new Date('2024-01-01T00:00:00Z').getTime(),
|
||||
};
|
||||
|
||||
const { rerender } = render(
|
||||
<GuideRow index={0} style={mockStyle} data={data} />
|
||||
);
|
||||
|
||||
const initialCalls = mockData.renderProgram.mock.calls.length;
|
||||
|
||||
// Scroll to different position
|
||||
scrollRef.current = HOUR_WIDTH * 12;
|
||||
const newData = { ...data, guideScrollLeftRef: scrollRef };
|
||||
|
||||
rerender(<GuideRow index={0} style={mockStyle} data={newData} />);
|
||||
|
||||
// Different programs should be rendered
|
||||
expect(mockData.renderProgram.mock.calls.length).toBeGreaterThan(initialCalls);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Placeholder Culling', () => {
|
||||
it('should only render placeholders visible in viewport', () => {
|
||||
const data = {
|
||||
...mockData,
|
||||
programsByChannelId: new Map([[mockChannel.id, []]]),
|
||||
guideScrollLeftRef: { current: HOUR_WIDTH * 10 },
|
||||
viewportWidth: HOUR_WIDTH * 4,
|
||||
timelineStartMs: new Date('2024-01-01T00:00:00Z').getTime(),
|
||||
};
|
||||
|
||||
const { container } = render(
|
||||
<GuideRow index={0} style={mockStyle} data={data} />
|
||||
);
|
||||
|
||||
const visiblePlaceholders = screen.getAllByText('No program data');
|
||||
|
||||
// Should render fewer than total placeholders due to culling
|
||||
expect(visiblePlaceholders.length).toBeLessThan(Math.ceil(24 / 2));
|
||||
expect(visiblePlaceholders.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('should not render placeholders outside viewport', () => {
|
||||
const data = {
|
||||
...mockData,
|
||||
programsByChannelId: new Map([[mockChannel.id, []]]),
|
||||
guideScrollLeftRef: { current: HOUR_WIDTH * 20 },
|
||||
viewportWidth: HOUR_WIDTH * 2,
|
||||
timelineStartMs: new Date('2024-01-01T00:00:00Z').getTime(),
|
||||
};
|
||||
|
||||
const { container } = render(
|
||||
<GuideRow index={0} style={mockStyle} data={data} />
|
||||
);
|
||||
|
||||
const visiblePlaceholders = screen.getAllByText('No program data');
|
||||
|
||||
// Near the end of timeline, should show fewer placeholders
|
||||
expect(visiblePlaceholders.length).toBeGreaterThan(0);
|
||||
expect(visiblePlaceholders.length).toBeLessThanOrEqual(3);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Hover State Management', () => {
|
||||
it('should show play icon on mouse enter', () => {
|
||||
render(<GuideRow index={0} style={mockStyle} data={mockData} />);
|
||||
|
||||
const logoContainer = screen.getByAltText('Test Channel').closest('.channel-logo');
|
||||
|
||||
expect(screen.queryByTestId('play-icon')).not.toBeInTheDocument();
|
||||
|
||||
fireEvent.mouseEnter(logoContainer);
|
||||
|
||||
expect(screen.getByTestId('play-icon')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should hide play icon on mouse leave', () => {
|
||||
render(<GuideRow index={0} style={mockStyle} data={mockData} />);
|
||||
|
||||
const logoContainer = screen.getByAltText('Test Channel').closest('.channel-logo');
|
||||
|
||||
fireEvent.mouseEnter(logoContainer);
|
||||
expect(screen.getByTestId('play-icon')).toBeInTheDocument();
|
||||
|
||||
fireEvent.mouseLeave(logoContainer);
|
||||
expect(screen.queryByTestId('play-icon')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should maintain hover state independently per row', () => {
|
||||
const data = {
|
||||
...mockData,
|
||||
filteredChannels: [mockChannel, { ...mockChannel, id: 'channel-2', name: 'Channel 2' }],
|
||||
};
|
||||
|
||||
// Render both rows separately
|
||||
const { container: container1 } = render(
|
||||
<GuideRow index={0} style={mockStyle} data={data} />
|
||||
);
|
||||
const { container: container2 } = render(
|
||||
<GuideRow index={1} style={{ ...mockStyle, top: PROGRAM_HEIGHT }} data={data} />
|
||||
);
|
||||
|
||||
// Hover over first row
|
||||
const logo1 = container1.querySelector('.channel-logo');
|
||||
fireEvent.mouseEnter(logo1);
|
||||
|
||||
// First row should show play icon
|
||||
expect(container1.querySelector('[data-testid="play-icon"]')).toBeInTheDocument();
|
||||
|
||||
// Second row should not show play icon
|
||||
expect(container2.querySelector('[data-testid="play-icon"]')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('Program Time Positioning', () => {
|
||||
const createTimedProgram = (startHour, durationMinutes) => {
|
||||
const timelineStart = new Date('2024-01-01T00:00:00Z');
|
||||
const startMs = timelineStart.getTime() + startHour * 60 * 60 * 1000;
|
||||
return {
|
||||
id: `program-${startHour}`,
|
||||
title: `Program at ${startHour}h`,
|
||||
startMs,
|
||||
endMs: startMs + durationMinutes * 60 * 1000,
|
||||
};
|
||||
};
|
||||
|
||||
it('should calculate correct viewport boundaries', () => {
|
||||
const programs = [
|
||||
createTimedProgram(6, 60),
|
||||
createTimedProgram(12, 60),
|
||||
];
|
||||
|
||||
const data = {
|
||||
...mockData,
|
||||
programsByChannelId: new Map([[mockChannel.id, programs]]),
|
||||
guideScrollLeftRef: { current: HOUR_WIDTH * 8 },
|
||||
viewportWidth: HOUR_WIDTH * 4,
|
||||
timelineStartMs: new Date('2024-01-01T00:00:00Z').getTime(),
|
||||
};
|
||||
|
||||
render(<GuideRow index={0} style={mockStyle} data={data} />);
|
||||
|
||||
// Both programs should be rendered since they're within viewport range
|
||||
expect(mockData.renderProgram).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should handle programs at timeline boundaries', () => {
|
||||
const programs = [
|
||||
createTimedProgram(0, 60), // Start of timeline
|
||||
createTimedProgram(23, 60), // End of timeline
|
||||
];
|
||||
|
||||
const data = {
|
||||
...mockData,
|
||||
programsByChannelId: new Map([[mockChannel.id, programs]]),
|
||||
guideScrollLeftRef: { current: 0 },
|
||||
viewportWidth: HOUR_WIDTH * 24,
|
||||
timelineStartMs: new Date('2024-01-01T00:00:00Z').getTime(),
|
||||
};
|
||||
|
||||
render(<GuideRow index={0} style={mockStyle} data={data} />);
|
||||
|
||||
expect(screen.getByText('Program at 0h')).toBeInTheDocument();
|
||||
expect(screen.getByText('Program at 23h')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should handle very short programs', () => {
|
||||
const programs = [
|
||||
createTimedProgram(12, 5), // 5-minute program
|
||||
];
|
||||
|
||||
const data = {
|
||||
...mockData,
|
||||
programsByChannelId: new Map([[mockChannel.id, programs]]),
|
||||
guideScrollLeftRef: { current: HOUR_WIDTH * 12 },
|
||||
viewportWidth: HOUR_WIDTH * 2,
|
||||
timelineStartMs: new Date('2024-01-01T00:00:00Z').getTime(),
|
||||
};
|
||||
|
||||
render(<GuideRow index={0} style={mockStyle} data={data} />);
|
||||
|
||||
expect(screen.getByTestId('program-program-12')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should handle very long programs', () => {
|
||||
const programs = [
|
||||
createTimedProgram(6, 360), // 6-hour program
|
||||
];
|
||||
|
||||
const data = {
|
||||
...mockData,
|
||||
programsByChannelId: new Map([[mockChannel.id, programs]]),
|
||||
guideScrollLeftRef: { current: HOUR_WIDTH * 8 },
|
||||
viewportWidth: HOUR_WIDTH * 2,
|
||||
timelineStartMs: new Date('2024-01-01T00:00:00Z').getTime(),
|
||||
};
|
||||
|
||||
render(<GuideRow index={0} style={mockStyle} data={data} />);
|
||||
|
||||
// Long program spanning viewport should be visible
|
||||
expect(screen.getByTestId('program-program-6')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Edge Cases', () => {
|
||||
it('should handle empty programsByChannelId gracefully', () => {
|
||||
const data = {
|
||||
...mockData,
|
||||
programsByChannelId: new Map(),
|
||||
guideScrollLeftRef: { current: 0 },
|
||||
viewportWidth: HOUR_WIDTH * 4,
|
||||
timelineStartMs: new Date('2024-01-01T00:00:00Z').getTime(),
|
||||
};
|
||||
|
||||
render(<GuideRow index={0} style={mockStyle} data={data} />);
|
||||
|
||||
expect(screen.getAllByText('No program data').length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('should handle zero viewport width', () => {
|
||||
const programs = [mockProgram];
|
||||
|
||||
const data = {
|
||||
...mockData,
|
||||
programsByChannelId: new Map([[mockChannel.id, programs]]),
|
||||
guideScrollLeftRef: { current: 0 },
|
||||
viewportWidth: 0,
|
||||
timelineStartMs: new Date('2024-01-01T00:00:00Z').getTime(),
|
||||
};
|
||||
|
||||
render(<GuideRow index={0} style={mockStyle} data={data} />);
|
||||
|
||||
// Should still render due to buffer
|
||||
expect(screen.getByTestId('guide-row')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should handle negative scroll position', () => {
|
||||
const programs = [createProgramAtTime('prog-1', 0, 60)];
|
||||
|
||||
const data = {
|
||||
...mockData,
|
||||
programsByChannelId: new Map([[mockChannel.id, programs]]),
|
||||
guideScrollLeftRef: { current: -100 },
|
||||
viewportWidth: HOUR_WIDTH * 4,
|
||||
timelineStartMs: new Date('2024-01-01T00:00:00Z').getTime(),
|
||||
};
|
||||
|
||||
render(<GuideRow index={0} style={mockStyle} data={data} />);
|
||||
|
||||
expect(screen.getByTestId('guide-row')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should handle row index out of bounds', () => {
|
||||
const data = {
|
||||
...mockData,
|
||||
filteredChannels: [mockChannel],
|
||||
};
|
||||
|
||||
const { container } = render(
|
||||
<GuideRow index={999} style={mockStyle} data={data} />
|
||||
);
|
||||
|
||||
expect(container.firstChild).toBeNull();
|
||||
});
|
||||
|
||||
it('should memoize component to prevent unnecessary re-renders', () => {
|
||||
const { rerender } = render(
|
||||
<GuideRow index={0} style={mockStyle} data={mockData} />
|
||||
);
|
||||
|
||||
const renderCount = mockData.renderProgram.mock.calls.length;
|
||||
|
||||
// Re-render with same props
|
||||
rerender(<GuideRow index={0} style={mockStyle} data={mockData} />);
|
||||
|
||||
// Should not cause additional renders due to React.memo
|
||||
expect(mockData.renderProgram.mock.calls.length).toBe(renderCount);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -16,6 +16,10 @@ vi.mock('../../utils', () => ({
|
|||
copyToClipboard: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('../NotificationCenter', () => ({
|
||||
default: () => <div data-testid="notification-center">Notification Center</div>,
|
||||
}));
|
||||
|
||||
// Mock lucide-react icons
|
||||
vi.mock('lucide-react', () => ({
|
||||
ListOrdered: ({ onClick }) => <div data-testid="list-ordered-icon" onClick={onClick} />,
|
||||
|
|
@ -30,6 +34,12 @@ vi.mock('lucide-react', () => ({
|
|||
LogOut: ({ onClick }) => <div data-testid="logout-icon" onClick={onClick} />,
|
||||
User: ({ onClick }) => <div data-testid="user-icon" onClick={onClick} />,
|
||||
FileImage: ({ onClick }) => <div data-testid="file-image-icon" onClick={onClick} />,
|
||||
Webhook: () => <div data-testid="webhook-icon" />,
|
||||
Logs: () => <div data-testid="logs-icon" />,
|
||||
ChevronDown: () => <div data-testid="chevron-down-icon" />,
|
||||
ChevronRight: () => <div data-testid="chevron-right-icon" />,
|
||||
MonitorCog: () => <div data-testid="monitor-cog-icon" />,
|
||||
Blocks: () => <div data-testid="blocks-icon" />,
|
||||
}));
|
||||
|
||||
// Mock UserForm component
|
||||
|
|
@ -85,14 +95,11 @@ vi.mock('@mantine/core', async () => {
|
|||
{children}
|
||||
</nav>
|
||||
),
|
||||
ScrollArea: ({ children }) => <div>{children}</div>,
|
||||
};
|
||||
});
|
||||
|
||||
const mockChannels = {
|
||||
'channel-1': { id: 'channel-1', name: 'Channel 1' },
|
||||
'channel-2': { id: 'channel-2', name: 'Channel 2' },
|
||||
'channel-3': { id: 'channel-3', name: 'Channel 3' },
|
||||
};
|
||||
const mockChannels = [ 'channel-1', 'channel-2', 'channel-3' ];
|
||||
|
||||
const mockEnvironment = {
|
||||
public_ip: '192.168.1.1',
|
||||
|
|
@ -182,7 +189,7 @@ describe('Sidebar', () => {
|
|||
});
|
||||
|
||||
describe('Navigation Links - Admin User', () => {
|
||||
it('should render all admin navigation items', () => {
|
||||
it('should render all admin navigation items', async () => {
|
||||
renderSidebar();
|
||||
|
||||
expect(screen.getByText('Channels')).toBeInTheDocument();
|
||||
|
|
@ -192,9 +199,16 @@ describe('Sidebar', () => {
|
|||
expect(screen.getByText('DVR')).toBeInTheDocument();
|
||||
expect(screen.getByText('Stats')).toBeInTheDocument();
|
||||
expect(screen.getByText('Plugins')).toBeInTheDocument();
|
||||
expect(screen.getByText('Users')).toBeInTheDocument();
|
||||
expect(screen.getByText('Logo Manager')).toBeInTheDocument();
|
||||
expect(screen.getByText('Settings')).toBeInTheDocument();
|
||||
|
||||
// Expand System group to access Users
|
||||
const systemButton = screen.getByText('System');
|
||||
fireEvent.click(systemButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Users')).toBeInTheDocument();
|
||||
expect(screen.getByText('Logo Manager')).toBeInTheDocument();
|
||||
expect(screen.getByText('Settings')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('should display channel count badge', () => {
|
||||
|
|
@ -452,4 +466,163 @@ describe('Sidebar', () => {
|
|||
expect(flag).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('NavGroup Component', () => {
|
||||
it('should render Integrations group with children collapsed by default', () => {
|
||||
renderSidebar();
|
||||
|
||||
expect(screen.getByText('Integrations')).toBeInTheDocument();
|
||||
expect(screen.queryByText('Connections')).not.toBeInTheDocument();
|
||||
expect(screen.queryByText('Logs')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should expand Integrations group when clicked', async () => {
|
||||
renderSidebar();
|
||||
|
||||
const integrationsGroup = screen.getByText('Integrations').closest('button');
|
||||
fireEvent.click(integrationsGroup);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Connections')).toBeInTheDocument();
|
||||
expect(screen.getByText('Logs')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('should collapse Integrations group when clicked again', async () => {
|
||||
renderSidebar();
|
||||
|
||||
const integrationsGroup = screen.getByText('Integrations').closest('button');
|
||||
|
||||
// Expand
|
||||
fireEvent.click(integrationsGroup);
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Connections')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
// Collapse
|
||||
fireEvent.click(integrationsGroup);
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByText('Connections')).not.toBeInTheDocument();
|
||||
expect(screen.queryByText('Logs')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('should render System group with children collapsed by default', () => {
|
||||
renderSidebar();
|
||||
|
||||
expect(screen.getByText('System')).toBeInTheDocument();
|
||||
expect(screen.queryByText('Users')).not.toBeInTheDocument();
|
||||
expect(screen.queryByText('Logo Manager')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should expand System group when clicked', async () => {
|
||||
renderSidebar();
|
||||
|
||||
const systemGroup = screen.getByText('System').closest('button');
|
||||
fireEvent.click(systemGroup);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Users')).toBeInTheDocument();
|
||||
expect(screen.getByText('Logo Manager')).toBeInTheDocument();
|
||||
expect(screen.getByText('Settings')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('should hide group label when collapsed sidebar', () => {
|
||||
renderSidebar({ collapsed: true });
|
||||
|
||||
expect(screen.queryByText('Integrations')).not.toBeInTheDocument();
|
||||
expect(screen.queryByText('System')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should not show multiple groups collapsed when both expanded', async () => {
|
||||
renderSidebar();
|
||||
|
||||
const integrationsGroup = screen.getByText('Integrations').closest('button');
|
||||
const systemGroup = screen.getByText('System').closest('button');
|
||||
|
||||
// Expand Integrations
|
||||
fireEvent.click(integrationsGroup);
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Connections')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
// Expand System (Integrations should remain expanded)
|
||||
fireEvent.click(systemGroup);
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Users')).toBeInTheDocument();
|
||||
expect(screen.getByText('Connections')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('NotificationCenter Integration', () => {
|
||||
it('should render NotificationCenter when authenticated and expanded', () => {
|
||||
renderSidebar();
|
||||
|
||||
expect(screen.getByTestId('notification-center')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render NotificationCenter when authenticated and collapsed', () => {
|
||||
renderSidebar({ collapsed: true });
|
||||
|
||||
expect(screen.getByTestId('notification-center')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should not render NotificationCenter when not authenticated', () => {
|
||||
useAuthStore.mockImplementation((selector) => {
|
||||
const state = {
|
||||
isAuthenticated: false,
|
||||
user: null,
|
||||
logout: vi.fn(),
|
||||
};
|
||||
return selector(state);
|
||||
});
|
||||
|
||||
renderSidebar();
|
||||
|
||||
expect(screen.queryByTestId('notification-center')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should not render NotificationCenter when not authenticated and collapsed', () => {
|
||||
useAuthStore.mockImplementation((selector) => {
|
||||
const state = {
|
||||
isAuthenticated: false,
|
||||
user: null,
|
||||
logout: vi.fn(),
|
||||
};
|
||||
return selector(state);
|
||||
});
|
||||
|
||||
renderSidebar({ collapsed: true });
|
||||
|
||||
expect(screen.queryByTestId('notification-center')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Channel Count Badge', () => {
|
||||
it('should display 0 when no channels exist', () => {
|
||||
useChannelsStore.mockReturnValue({});
|
||||
|
||||
renderSidebar();
|
||||
|
||||
expect(screen.getByText('(0)')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should handle null channelIds gracefully', () => {
|
||||
useChannelsStore.mockReturnValue(null);
|
||||
|
||||
renderSidebar();
|
||||
|
||||
expect(screen.getByText('(0)')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should handle array of channel IDs', () => {
|
||||
useChannelsStore.mockReturnValue(['channel-1', 'channel-2', 'channel-3']);
|
||||
|
||||
renderSidebar();
|
||||
|
||||
expect(screen.getByText('(3)')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue