datajunction-ui
Version:
DataJunction UI
329 lines (269 loc) • 9.26 kB
JSX
import React from 'react';
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import { NotificationSubscriptionsSection } from '../NotificationSubscriptionsSection';
describe('NotificationSubscriptionsSection', () => {
const mockOnUpdate = vi.fn();
const mockOnUnsubscribe = vi.fn();
const mockSubscriptions = [
{
entity_name: 'default.orders_count',
entity_type: 'node',
node_type: 'metric',
activity_types: ['update', 'delete'],
status: 'valid',
},
{
entity_name: 'default.dim_customers',
entity_type: 'node',
node_type: 'dimension',
activity_types: ['create'],
status: 'invalid',
},
];
beforeEach(() => {
vi.clearAllMocks();
});
it('renders empty state when no subscriptions', () => {
render(
<NotificationSubscriptionsSection
subscriptions={[]}
onUpdate={mockOnUpdate}
onUnsubscribe={mockOnUnsubscribe}
/>,
);
expect(screen.getByText(/not watching any nodes/i)).toBeInTheDocument();
});
it('renders subscriptions list', () => {
render(
<NotificationSubscriptionsSection
subscriptions={mockSubscriptions}
onUpdate={mockOnUpdate}
onUnsubscribe={mockOnUnsubscribe}
/>,
);
expect(screen.getByText('default.orders_count')).toBeInTheDocument();
expect(screen.getByText('default.dim_customers')).toBeInTheDocument();
expect(screen.getByText('METRIC')).toBeInTheDocument();
expect(screen.getByText('DIMENSION')).toBeInTheDocument();
});
it('shows invalid badge for invalid status', () => {
render(
<NotificationSubscriptionsSection
subscriptions={mockSubscriptions}
onUpdate={mockOnUpdate}
onUnsubscribe={mockOnUnsubscribe}
/>,
);
expect(screen.getByText('INVALID')).toBeInTheDocument();
});
it('displays activity types as badges', () => {
render(
<NotificationSubscriptionsSection
subscriptions={mockSubscriptions}
onUpdate={mockOnUpdate}
onUnsubscribe={mockOnUnsubscribe}
/>,
);
expect(screen.getByText('update')).toBeInTheDocument();
expect(screen.getByText('delete')).toBeInTheDocument();
expect(screen.getByText('create')).toBeInTheDocument();
});
it('enters edit mode when edit button is clicked', () => {
render(
<NotificationSubscriptionsSection
subscriptions={mockSubscriptions}
onUpdate={mockOnUpdate}
onUnsubscribe={mockOnUnsubscribe}
/>,
);
const editButtons = screen.getAllByTitle('Edit subscription');
fireEvent.click(editButtons[0]);
expect(screen.getByText('Save')).toBeInTheDocument();
expect(screen.getByText('Cancel')).toBeInTheDocument();
expect(screen.getByLabelText('Update')).toBeInTheDocument();
});
it('cancels editing when cancel button is clicked', () => {
render(
<NotificationSubscriptionsSection
subscriptions={mockSubscriptions}
onUpdate={mockOnUpdate}
onUnsubscribe={mockOnUnsubscribe}
/>,
);
const editButtons = screen.getAllByTitle('Edit subscription');
fireEvent.click(editButtons[0]);
expect(screen.getByText('Cancel')).toBeInTheDocument();
fireEvent.click(screen.getByText('Cancel'));
expect(screen.queryByText('Cancel')).not.toBeInTheDocument();
});
it('calls onUnsubscribe when unsubscribe is confirmed', async () => {
window.confirm = vi.fn().mockReturnValue(true);
mockOnUnsubscribe.mockResolvedValue();
render(
<NotificationSubscriptionsSection
subscriptions={mockSubscriptions}
onUpdate={mockOnUpdate}
onUnsubscribe={mockOnUnsubscribe}
/>,
);
const unsubscribeButtons = screen.getAllByTitle('Unsubscribe');
fireEvent.click(unsubscribeButtons[0]);
expect(window.confirm).toHaveBeenCalledWith(
'Unsubscribe from notifications for "default.orders_count"?',
);
expect(mockOnUnsubscribe).toHaveBeenCalledWith(mockSubscriptions[0]);
});
it('does not call onUnsubscribe when unsubscribe is cancelled', () => {
window.confirm = vi.fn().mockReturnValue(false);
render(
<NotificationSubscriptionsSection
subscriptions={mockSubscriptions}
onUpdate={mockOnUpdate}
onUnsubscribe={mockOnUnsubscribe}
/>,
);
const unsubscribeButtons = screen.getAllByTitle('Unsubscribe');
fireEvent.click(unsubscribeButtons[0]);
expect(mockOnUnsubscribe).not.toHaveBeenCalled();
});
it('shows alert when trying to save with no activity types', () => {
window.alert = vi.fn();
render(
<NotificationSubscriptionsSection
subscriptions={mockSubscriptions}
onUpdate={mockOnUpdate}
onUnsubscribe={mockOnUnsubscribe}
/>,
);
const editButtons = screen.getAllByTitle('Edit subscription');
fireEvent.click(editButtons[0]);
// Uncheck all activity types
const updateCheckbox = screen.getByLabelText('Update');
const deleteCheckbox = screen.getByLabelText('Delete');
fireEvent.click(updateCheckbox);
fireEvent.click(deleteCheckbox);
fireEvent.click(screen.getByText('Save'));
expect(window.alert).toHaveBeenCalledWith(
'Please select at least one activity type',
);
expect(mockOnUpdate).not.toHaveBeenCalled();
});
it('calls onUpdate with new activity types when saved', async () => {
mockOnUpdate.mockResolvedValue();
render(
<NotificationSubscriptionsSection
subscriptions={mockSubscriptions}
onUpdate={mockOnUpdate}
onUnsubscribe={mockOnUnsubscribe}
/>,
);
const editButtons = screen.getAllByTitle('Edit subscription');
fireEvent.click(editButtons[0]);
// Add 'create' activity type
const createCheckbox = screen.getByLabelText('Create');
fireEvent.click(createCheckbox);
fireEvent.click(screen.getByText('Save'));
await waitFor(() => {
expect(mockOnUpdate).toHaveBeenCalledWith(mockSubscriptions[0], [
'update',
'delete',
'create',
]);
});
});
it('renders entity type badge when node_type is not available', () => {
const subscriptionsWithoutNodeType = [
{
entity_name: 'some_entity',
entity_type: 'namespace',
activity_types: ['create'],
},
];
render(
<NotificationSubscriptionsSection
subscriptions={subscriptionsWithoutNodeType}
onUpdate={mockOnUpdate}
onUnsubscribe={mockOnUnsubscribe}
/>,
);
expect(screen.getByText('namespace')).toBeInTheDocument();
});
it('handles onUpdate error gracefully', async () => {
const consoleSpy = vi.spyOn(console, 'error').mockImplementation();
window.alert = vi.fn();
mockOnUpdate.mockRejectedValue(new Error('Update failed'));
render(
<NotificationSubscriptionsSection
subscriptions={mockSubscriptions}
onUpdate={mockOnUpdate}
onUnsubscribe={mockOnUnsubscribe}
/>,
);
const editButtons = screen.getAllByTitle('Edit subscription');
fireEvent.click(editButtons[0]);
fireEvent.click(screen.getByText('Save'));
await waitFor(() => {
expect(consoleSpy).toHaveBeenCalled();
expect(window.alert).toHaveBeenCalledWith(
'Failed to update subscription',
);
});
consoleSpy.mockRestore();
});
it('handles onUnsubscribe error gracefully', async () => {
const consoleSpy = vi.spyOn(console, 'error').mockImplementation();
window.confirm = vi.fn().mockReturnValue(true);
mockOnUnsubscribe.mockRejectedValue(new Error('Unsubscribe failed'));
render(
<NotificationSubscriptionsSection
subscriptions={mockSubscriptions}
onUpdate={mockOnUpdate}
onUnsubscribe={mockOnUnsubscribe}
/>,
);
const unsubscribeButtons = screen.getAllByTitle('Unsubscribe');
fireEvent.click(unsubscribeButtons[0]);
await waitFor(() => {
expect(consoleSpy).toHaveBeenCalled();
});
consoleSpy.mockRestore();
});
it('shows "All" when subscription has no activity_types', () => {
const subscriptionsWithoutActivityTypes = [
{
entity_name: 'default.some_node',
entity_type: 'node',
node_type: 'metric',
activity_types: null,
status: 'valid',
},
];
render(
<NotificationSubscriptionsSection
subscriptions={subscriptionsWithoutActivityTypes}
onUpdate={mockOnUpdate}
onUnsubscribe={mockOnUnsubscribe}
/>,
);
expect(screen.getByText('All')).toBeInTheDocument();
});
it('shows "All" when subscription has undefined activity_types', () => {
const subscriptionsWithUndefinedActivityTypes = [
{
entity_name: 'default.other_node',
entity_type: 'node',
node_type: 'dimension',
status: 'valid',
// activity_types not defined
},
];
render(
<NotificationSubscriptionsSection
subscriptions={subscriptionsWithUndefinedActivityTypes}
onUpdate={mockOnUpdate}
onUnsubscribe={mockOnUnsubscribe}
/>,
);
expect(screen.getByText('All')).toBeInTheDocument();
});
});