datajunction-ui
Version:
DataJunction UI
329 lines (271 loc) • 9.77 kB
JSX
import React from 'react';
import { render, screen, waitFor } from '@testing-library/react';
import { NotificationsPage } from '../index';
import DJClientContext from '../../../providers/djclient';
describe('<NotificationsPage />', () => {
const mockNotifications = [
{
id: 1,
entity_type: 'node',
entity_name: 'default.metrics.revenue',
node: 'default.metrics.revenue',
activity_type: 'update',
user: 'alice',
created_at: new Date().toISOString(),
details: { version: 'v2' },
},
{
id: 2,
entity_type: 'node',
entity_name: 'default.dimensions.country',
node: 'default.dimensions.country',
activity_type: 'create',
user: 'bob',
created_at: new Date().toISOString(),
details: { version: 'v1' },
},
];
const mockNodes = [
{
name: 'default.metrics.revenue',
type: 'metric',
current: { displayName: 'Revenue Metric' },
},
{
name: 'default.dimensions.country',
type: 'dimension',
current: { displayName: 'Country' },
},
];
const createMockDjClient = (overrides = {}) => ({
getSubscribedHistory: vi.fn().mockResolvedValue(mockNotifications),
getNodesByNames: vi.fn().mockResolvedValue(mockNodes),
...overrides,
});
const renderWithContext = mockDjClient => {
return render(
<DJClientContext.Provider value={{ DataJunctionAPI: mockDjClient }}>
<NotificationsPage />
</DJClientContext.Provider>,
);
};
beforeEach(() => {
vi.clearAllMocks();
});
it('renders the page title', async () => {
const mockDjClient = createMockDjClient();
renderWithContext(mockDjClient);
// Wait for async effects to complete
await waitFor(() => {
expect(mockDjClient.getSubscribedHistory).toHaveBeenCalled();
});
expect(screen.getByText('Notifications')).toBeInTheDocument();
});
it('shows loading state initially', async () => {
// Use a controlled promise that we can resolve after the test
let resolvePromise;
const pendingPromise = new Promise(resolve => {
resolvePromise = resolve;
});
const mockDjClient = createMockDjClient({
getSubscribedHistory: vi.fn().mockImplementation(() => pendingPromise),
});
renderWithContext(mockDjClient);
// LoadingIcon should be present (check for the container)
const loadingContainer = document.querySelector(
'[style*="text-align: center"]',
);
expect(loadingContainer).toBeInTheDocument();
// Resolve the promise to allow cleanup without act() warnings
resolvePromise([]);
await waitFor(() => {
expect(mockDjClient.getSubscribedHistory).toHaveBeenCalled();
});
});
it('shows empty state when no notifications', async () => {
const mockDjClient = createMockDjClient({
getSubscribedHistory: vi.fn().mockResolvedValue([]),
});
renderWithContext(mockDjClient);
await waitFor(() => {
expect(mockDjClient.getSubscribedHistory).toHaveBeenCalled();
});
expect(screen.getByText(/No notifications yet/i)).toBeInTheDocument();
expect(
screen.getByText(/Watch nodes to receive updates/i),
).toBeInTheDocument();
});
it('displays notifications with display names', async () => {
const mockDjClient = createMockDjClient();
renderWithContext(mockDjClient);
await waitFor(() => {
expect(mockDjClient.getSubscribedHistory).toHaveBeenCalled();
});
// Display names should be shown
expect(await screen.findByText('Revenue Metric')).toBeInTheDocument();
expect(await screen.findByText('Country')).toBeInTheDocument();
});
it('displays entity names below display names', async () => {
const mockDjClient = createMockDjClient();
renderWithContext(mockDjClient);
await waitFor(() => {
expect(mockDjClient.getSubscribedHistory).toHaveBeenCalled();
});
// Entity names should be shown
expect(
await screen.findByText('default.metrics.revenue'),
).toBeInTheDocument();
expect(
await screen.findByText('default.dimensions.country'),
).toBeInTheDocument();
});
it('falls back to entity_name when no display_name', async () => {
const mockDjClient = createMockDjClient({
getNodesByNames: vi.fn().mockResolvedValue([]), // No node info
});
renderWithContext(mockDjClient);
await waitFor(() => {
expect(mockDjClient.getSubscribedHistory).toHaveBeenCalled();
});
// Entity names should be shown as the title (no display names)
const revenueElements = await screen.findAllByText(
'default.metrics.revenue',
);
expect(revenueElements.length).toBeGreaterThan(0);
});
it('shows version badge when version is available', async () => {
const mockDjClient = createMockDjClient();
renderWithContext(mockDjClient);
await waitFor(() => {
expect(mockDjClient.getSubscribedHistory).toHaveBeenCalled();
});
expect(await screen.findByText('v2')).toBeInTheDocument();
expect(await screen.findByText('v1')).toBeInTheDocument();
});
it('links to revision page when version is available', async () => {
const mockDjClient = createMockDjClient();
renderWithContext(mockDjClient);
await waitFor(() => {
expect(mockDjClient.getSubscribedHistory).toHaveBeenCalled();
});
await waitFor(() => {
const links = document.querySelectorAll('a.notification-item');
expect(links.length).toBe(2);
const revenueLink = Array.from(links).find(l =>
l.textContent.includes('Revenue Metric'),
);
expect(revenueLink).toHaveAttribute(
'href',
'/nodes/default.metrics.revenue/revisions/v2',
);
});
});
it('links to history page when no version', async () => {
const mockDjClient = createMockDjClient({
getSubscribedHistory: vi.fn().mockResolvedValue([
{
id: 1,
entity_type: 'node',
entity_name: 'default.source.orders',
node: 'default.source.orders',
activity_type: 'update',
user: 'alice',
created_at: new Date().toISOString(),
details: {}, // No version
},
]),
getNodesByNames: vi.fn().mockResolvedValue([]),
});
renderWithContext(mockDjClient);
await waitFor(() => {
expect(mockDjClient.getSubscribedHistory).toHaveBeenCalled();
});
await waitFor(() => {
const link = document.querySelector('a.notification-item');
expect(link).toHaveAttribute(
'href',
'/nodes/default.source.orders/history',
);
});
});
it('shows node type badge', async () => {
const mockDjClient = createMockDjClient();
renderWithContext(mockDjClient);
await waitFor(() => {
expect(mockDjClient.getSubscribedHistory).toHaveBeenCalled();
});
expect(await screen.findByText('METRIC')).toBeInTheDocument();
expect(await screen.findByText('DIMENSION')).toBeInTheDocument();
});
it('shows activity type and user', async () => {
const mockDjClient = createMockDjClient();
renderWithContext(mockDjClient);
await waitFor(() => {
expect(mockDjClient.getSubscribedHistory).toHaveBeenCalled();
});
expect(await screen.findByText('alice')).toBeInTheDocument();
expect(await screen.findByText('bob')).toBeInTheDocument();
});
it('groups notifications by date', async () => {
const mockDjClient = createMockDjClient();
renderWithContext(mockDjClient);
await waitFor(() => {
expect(mockDjClient.getSubscribedHistory).toHaveBeenCalled();
});
// Both notifications are from today
expect(await screen.findByText('Today')).toBeInTheDocument();
});
it('fetches node info via GraphQL', async () => {
const mockDjClient = createMockDjClient();
renderWithContext(mockDjClient);
await waitFor(() => {
expect(mockDjClient.getSubscribedHistory).toHaveBeenCalledWith(50);
});
await waitFor(() => {
expect(mockDjClient.getNodesByNames).toHaveBeenCalledWith([
'default.metrics.revenue',
'default.dimensions.country',
]);
});
});
it('handles errors gracefully', async () => {
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
const mockDjClient = createMockDjClient({
getSubscribedHistory: vi
.fn()
.mockRejectedValue(new Error('Network error')),
});
renderWithContext(mockDjClient);
await waitFor(() => {
expect(consoleSpy).toHaveBeenCalledWith(
'Error fetching notifications:',
expect.any(Error),
);
});
// Should show empty state after error
expect(screen.getByText(/No notifications yet/i)).toBeInTheDocument();
consoleSpy.mockRestore();
});
it('handles null response from getSubscribedHistory', async () => {
const mockDjClient = createMockDjClient({
getSubscribedHistory: vi.fn().mockResolvedValue(null),
});
renderWithContext(mockDjClient);
await waitFor(() => {
expect(mockDjClient.getSubscribedHistory).toHaveBeenCalled();
});
// Should show empty state when API returns null
expect(screen.getByText(/No notifications yet/i)).toBeInTheDocument();
});
it('handles undefined response from getSubscribedHistory', async () => {
const mockDjClient = createMockDjClient({
getSubscribedHistory: vi.fn().mockResolvedValue(undefined),
});
renderWithContext(mockDjClient);
await waitFor(() => {
expect(mockDjClient.getSubscribedHistory).toHaveBeenCalled();
});
// Should show empty state when API returns undefined
expect(screen.getByText(/No notifications yet/i)).toBeInTheDocument();
});
});