datajunction-ui
Version:
DataJunction UI
432 lines (376 loc) • 12.3 kB
JSX
import React from 'react';
import { render, screen, fireEvent } from '@testing-library/react';
import { MemoryRouter } from 'react-router-dom';
import { MyNodesSection } from '../MyNodesSection';
vi.mock('../MyWorkspacePage.css', () => ({}));
vi.mock('../NodeList', () => ({
NodeList: ({ nodes, showUpdatedAt }) => (
<div data-testid="node-list">
{nodes.map(node => (
<div key={node.name}>{node.name}</div>
))}
</div>
),
}));
vi.mock('../TypeGroupGrid', () => ({
TypeGroupGrid: ({ groupedData }) => (
<div data-testid="type-group-grid">
{groupedData.map(group => (
<div key={group.type}>
{group.type}: {group.nodes.length} nodes
</div>
))}
</div>
),
}));
describe('<MyNodesSection />', () => {
beforeEach(() => {
// Default to list view for most tests; Group by Type tests manage localStorage themselves
localStorage.setItem('workspace_groupByType', 'false');
});
const mockOwnedNodes = [
{
name: 'default.owned_metric',
type: 'METRIC',
current: { displayName: 'Owned Metric', updatedAt: '2024-01-01' },
},
];
const mockWatchedNodes = [
{
name: 'default.watched_metric',
type: 'METRIC',
current: { displayName: 'Watched Metric', updatedAt: '2024-01-02' },
},
{
name: 'default.owned_metric', // Also owned, should be filtered out
type: 'METRIC',
current: { displayName: 'Owned Metric', updatedAt: '2024-01-01' },
},
];
const mockRecentlyEdited = [
{
name: 'default.edited_metric',
type: 'METRIC',
current: { displayName: 'Edited Metric', updatedAt: '2024-01-03' },
},
];
it('should render loading state', () => {
render(
<MemoryRouter>
<MyNodesSection
ownedNodes={[]}
watchedNodes={[]}
recentlyEdited={[]}
username="test.user@example.com"
loading={true}
/>
</MemoryRouter>,
);
expect(screen.queryByText('Owned (0)')).not.toBeInTheDocument();
});
it('should render empty state when no nodes', () => {
render(
<MemoryRouter>
<MyNodesSection
ownedNodes={[]}
watchedNodes={[]}
recentlyEdited={[]}
username="test.user@example.com"
loading={false}
/>
</MemoryRouter>,
);
expect(screen.getByText('No nodes yet.')).toBeInTheDocument();
expect(screen.getByText('Create a node')).toBeInTheDocument();
expect(screen.getByText('Claim ownership')).toBeInTheDocument();
});
it('should render tabs with node counts', () => {
render(
<MemoryRouter>
<MyNodesSection
ownedNodes={mockOwnedNodes}
watchedNodes={mockWatchedNodes}
recentlyEdited={mockRecentlyEdited}
username="test.user@example.com"
loading={false}
/>
</MemoryRouter>,
);
expect(screen.getByText('Owned')).toBeInTheDocument();
// Should only count watched nodes that aren't owned (1 watched-only)
expect(screen.getByText('Watched (1)')).toBeInTheDocument();
expect(screen.getByText('Recent Edits')).toBeInTheDocument();
});
it('should show owned nodes by default', () => {
render(
<MemoryRouter>
<MyNodesSection
ownedNodes={mockOwnedNodes}
watchedNodes={mockWatchedNodes}
recentlyEdited={mockRecentlyEdited}
username="test.user@example.com"
loading={false}
/>
</MemoryRouter>,
);
expect(screen.getByText('default.owned_metric')).toBeInTheDocument();
expect(
screen.queryByText('default.watched_metric'),
).not.toBeInTheDocument();
});
it('should switch to watched tab', () => {
render(
<MemoryRouter>
<MyNodesSection
ownedNodes={mockOwnedNodes}
watchedNodes={mockWatchedNodes}
recentlyEdited={mockRecentlyEdited}
username="test.user@example.com"
loading={false}
/>
</MemoryRouter>,
);
fireEvent.click(screen.getByText('Watched (1)'));
// Should show watched-only nodes (not owned)
expect(screen.getByText('default.watched_metric')).toBeInTheDocument();
expect(screen.queryByText('default.owned_metric')).not.toBeInTheDocument();
});
it('should switch to recent edits tab', () => {
render(
<MemoryRouter>
<MyNodesSection
ownedNodes={mockOwnedNodes}
watchedNodes={mockWatchedNodes}
recentlyEdited={mockRecentlyEdited}
username="test.user@example.com"
loading={false}
/>
</MemoryRouter>,
);
fireEvent.click(screen.getByText('Recent Edits'));
expect(screen.getByText('default.edited_metric')).toBeInTheDocument();
});
it('should show empty state for active tab with no nodes', () => {
render(
<MemoryRouter>
<MyNodesSection
ownedNodes={mockOwnedNodes}
watchedNodes={[]}
recentlyEdited={[]}
username="test.user@example.com"
loading={false}
/>
</MemoryRouter>,
);
fireEvent.click(screen.getByText('Watched (0)'));
expect(screen.getByText('No watched nodes')).toBeInTheDocument();
});
it('should limit displayed nodes to 8 in list view', () => {
const manyNodes = Array.from({ length: 15 }, (_, i) => ({
name: `default.metric_${i}`,
type: 'METRIC',
current: { displayName: `Metric ${i}`, updatedAt: '2024-01-01' },
}));
render(
<MemoryRouter>
<MyNodesSection
ownedNodes={manyNodes}
watchedNodes={[]}
recentlyEdited={[]}
username="test.user@example.com"
loading={false}
/>
</MemoryRouter>,
);
// Should show "+7 more" text
expect(screen.getByText('+7 more')).toBeInTheDocument();
});
it('should link to browse page with username filter', () => {
render(
<MemoryRouter>
<MyNodesSection
ownedNodes={mockOwnedNodes}
watchedNodes={[]}
recentlyEdited={[]}
username="test.user@example.com"
loading={false}
/>
</MemoryRouter>,
);
// The "View All →" link should have the username filter
const viewAllLink = screen.getByText('View All →').closest('a');
expect(viewAllLink).toHaveAttribute(
'href',
'/?ownedBy=test.user@example.com',
);
});
it('should auto-switch to edited tab when no owned nodes but has recently edited', async () => {
const { waitFor } = await import('@testing-library/react');
render(
<MemoryRouter>
<MyNodesSection
ownedNodes={[]}
watchedNodes={[]}
recentlyEdited={mockRecentlyEdited}
username="test.user@example.com"
loading={false}
/>
</MemoryRouter>,
);
// After effect fires, should auto-switch to edited tab
await waitFor(() => {
expect(screen.getByText('default.edited_metric')).toBeInTheDocument();
});
});
it('should show "No recent edits" when on edited tab with no recent edits', () => {
render(
<MemoryRouter>
<MyNodesSection
ownedNodes={mockOwnedNodes}
watchedNodes={[]}
recentlyEdited={[]}
username="test.user@example.com"
loading={false}
/>
</MemoryRouter>,
);
fireEvent.click(screen.getByText('Recent Edits'));
expect(screen.getByText('No recent edits')).toBeInTheDocument();
});
it('should filter watched nodes to exclude owned nodes', () => {
const ownedNodes = [
{ name: 'node1', type: 'METRIC', current: {} },
{ name: 'node2', type: 'METRIC', current: {} },
];
const watchedNodes = [
{ name: 'node1', type: 'METRIC', current: {} }, // Also owned
{ name: 'node3', type: 'METRIC', current: {} }, // Watched only
];
render(
<MemoryRouter>
<MyNodesSection
ownedNodes={ownedNodes}
watchedNodes={watchedNodes}
recentlyEdited={[]}
username="test.user@example.com"
loading={false}
/>
</MemoryRouter>,
);
// Watched tab should only show 1 (watched-only nodes)
expect(screen.getByText('Watched (1)')).toBeInTheDocument();
});
describe('Group by Type feature', () => {
beforeEach(() => {
localStorage.clear();
});
it('should show group by type toggle when nodes exist', () => {
render(
<MemoryRouter>
<MyNodesSection
ownedNodes={mockOwnedNodes}
watchedNodes={[]}
recentlyEdited={[]}
username="test.user@example.com"
loading={false}
/>
</MemoryRouter>,
);
expect(screen.getByLabelText('Group by Type')).toBeInTheDocument();
});
it('should not show toggle when no nodes', () => {
render(
<MemoryRouter>
<MyNodesSection
ownedNodes={[]}
watchedNodes={[]}
recentlyEdited={[]}
username="test.user@example.com"
loading={false}
/>
</MemoryRouter>,
);
expect(screen.queryByLabelText('Group by Type')).not.toBeInTheDocument();
});
it('should toggle between grouped and list view', () => {
render(
<MemoryRouter>
<MyNodesSection
ownedNodes={mockOwnedNodes}
watchedNodes={[]}
recentlyEdited={[]}
username="test.user@example.com"
loading={false}
/>
</MemoryRouter>,
);
// Initially should show grouped view (default is true)
expect(screen.getByTestId('type-group-grid')).toBeInTheDocument();
expect(screen.queryByTestId('node-list')).not.toBeInTheDocument();
// Click toggle to switch to list view
const toggle = screen.getByLabelText('Group by Type');
fireEvent.click(toggle);
// Should now show list view
expect(screen.queryByTestId('type-group-grid')).not.toBeInTheDocument();
expect(screen.getByTestId('node-list')).toBeInTheDocument();
});
it('should persist toggle state to localStorage', () => {
render(
<MemoryRouter>
<MyNodesSection
ownedNodes={mockOwnedNodes}
watchedNodes={[]}
recentlyEdited={[]}
username="test.user@example.com"
loading={false}
/>
</MemoryRouter>,
);
const toggle = screen.getByLabelText('Group by Type');
fireEvent.click(toggle);
expect(localStorage.getItem('workspace_groupByType')).toBe('false');
fireEvent.click(toggle);
expect(localStorage.getItem('workspace_groupByType')).toBe('true');
});
it('should load toggle state from localStorage', () => {
localStorage.setItem('workspace_groupByType', 'true');
render(
<MemoryRouter>
<MyNodesSection
ownedNodes={mockOwnedNodes}
watchedNodes={[]}
recentlyEdited={[]}
username="test.user@example.com"
loading={false}
/>
</MemoryRouter>,
);
// Should show grouped view on mount
expect(screen.getByTestId('type-group-grid')).toBeInTheDocument();
expect(screen.getByLabelText('Group by Type')).toBeChecked();
});
it('should group nodes by type correctly', () => {
const mixedNodes = [
{ name: 'metric1', type: 'metric', current: {} },
{ name: 'metric2', type: 'metric', current: {} },
{ name: 'dim1', type: 'dimension', current: {} },
{ name: 'source1', type: 'source', current: {} },
];
render(
<MemoryRouter>
<MyNodesSection
ownedNodes={mixedNodes}
watchedNodes={[]}
recentlyEdited={[]}
username="test.user@example.com"
loading={false}
/>
</MemoryRouter>,
);
// Default is grouped view (localStorage cleared by beforeEach → null → true)
expect(screen.getByText('metric: 2 nodes')).toBeInTheDocument();
expect(screen.getByText('dimension: 1 nodes')).toBeInTheDocument();
expect(screen.getByText('source: 1 nodes')).toBeInTheDocument();
});
});
});