glam/frontend/tests/unit/results-table.test.tsx
kempersc 2761857b0d Add scripts for converting OWL/Turtle ontology to Mermaid and PlantUML diagrams
- Implemented `owl_to_mermaid.py` to convert OWL/Turtle files into Mermaid class diagrams.
- Implemented `owl_to_plantuml.py` to convert OWL/Turtle files into PlantUML class diagrams.
- Added two new PlantUML files for custodian multi-aspect diagrams.
2025-11-22 23:01:13 +01:00

294 lines
9.3 KiB
TypeScript

/**
* Tests for Results Table Component
*/
import { describe, it, expect, vi } from 'vitest';
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import { ResultsTable } from '../../src/components/query/ResultsTable';
import type { SelectResults } from '../../src/lib/sparql/client';
describe('ResultsTable', () => {
const mockResults: SelectResults = {
head: { vars: ['subject', 'predicate', 'object'] },
results: {
bindings: [
{
subject: { type: 'uri', value: 'http://example.org/subject1' },
predicate: { type: 'uri', value: 'http://example.org/pred1' },
object: { type: 'literal', value: 'Object 1' },
},
{
subject: { type: 'uri', value: 'http://example.org/subject2' },
predicate: { type: 'uri', value: 'http://example.org/pred2' },
object: { type: 'literal', value: 'Object 2', 'xml:lang': 'en' },
},
{
subject: { type: 'bnode', value: '_:b1' },
predicate: { type: 'uri', value: 'http://example.org/pred3' },
object: {
type: 'literal',
value: '42',
datatype: 'http://www.w3.org/2001/XMLSchema#integer',
},
},
],
},
};
describe('rendering', () => {
it('should render table with headers', () => {
render(<ResultsTable results={mockResults} />);
expect(screen.getByText('subject')).toBeInTheDocument();
expect(screen.getByText('predicate')).toBeInTheDocument();
expect(screen.getByText('object')).toBeInTheDocument();
});
it('should render all result rows', () => {
render(<ResultsTable results={mockResults} />);
expect(screen.getByText(/subject1/)).toBeInTheDocument();
expect(screen.getByText(/subject2/)).toBeInTheDocument();
expect(screen.getByText('_:b1')).toBeInTheDocument();
});
it('should display results summary', () => {
const { container } = render(<ResultsTable results={mockResults} />);
const summary = container.querySelector('.results-summary');
expect(summary).toHaveTextContent('3');
expect(summary).toHaveTextContent('result');
expect(summary).toHaveTextContent('3 columns');
});
it('should render empty state when no results', () => {
const emptyResults: SelectResults = {
head: { vars: [] },
results: { bindings: [] },
};
render(<ResultsTable results={emptyResults} />);
expect(screen.getByText('No results found')).toBeInTheDocument();
});
});
describe('cell rendering', () => {
it('should render URI cells as links', () => {
render(<ResultsTable results={mockResults} />);
const uriLinks = screen.getAllByRole('link');
expect(uriLinks.length).toBeGreaterThan(0);
expect(uriLinks[0]).toHaveAttribute('target', '_blank');
});
it('should render literal cells with language tag', () => {
render(<ResultsTable results={mockResults} />);
expect(screen.getByText('Object 2')).toBeInTheDocument();
expect(screen.getByText('@en')).toBeInTheDocument();
});
it('should render literal cells with datatype', () => {
render(<ResultsTable results={mockResults} />);
expect(screen.getByText('42')).toBeInTheDocument();
expect(screen.getByText(/integer/)).toBeInTheDocument();
});
it('should render blank nodes', () => {
render(<ResultsTable results={mockResults} />);
expect(screen.getByText('_:b1')).toBeInTheDocument();
});
it('should render empty cells with placeholder', () => {
const resultsWithEmpty: SelectResults = {
head: { vars: ['a', 'b'] },
results: {
bindings: [
{ a: { type: 'uri', value: 'http://test' } },
// b is missing
],
},
};
render(<ResultsTable results={resultsWithEmpty} />);
expect(screen.getByText('—')).toBeInTheDocument();
});
});
describe('sorting', () => {
it('should sort ascending when clicking column header', () => {
render(<ResultsTable results={mockResults} />);
const objectHeader = screen.getByText('object').closest('th')!;
fireEvent.click(objectHeader);
// Check for ascending indicator
expect(screen.getByText('↑')).toBeInTheDocument();
});
it('should sort descending on second click', () => {
render(<ResultsTable results={mockResults} />);
const objectHeader = screen.getByText('object').closest('th')!;
fireEvent.click(objectHeader);
fireEvent.click(objectHeader);
// Check for descending indicator
expect(screen.getByText('↓')).toBeInTheDocument();
});
it('should clear sort on third click', () => {
render(<ResultsTable results={mockResults} />);
const objectHeader = screen.getByText('object').closest('th')!;
fireEvent.click(objectHeader);
fireEvent.click(objectHeader);
fireEvent.click(objectHeader);
// No sort indicator should be visible
expect(screen.queryByText('↑')).not.toBeInTheDocument();
expect(screen.queryByText('↓')).not.toBeInTheDocument();
});
});
describe('pagination', () => {
// Create large dataset (150 rows, should paginate)
const largeResults: SelectResults = {
head: { vars: ['id'] },
results: {
bindings: Array.from({ length: 150 }, (_, i) => ({
id: { type: 'literal', value: `Item ${i + 1}` },
})),
},
};
it('should show pagination controls for large datasets', () => {
render(<ResultsTable results={largeResults} />);
expect(screen.getByText('Next →')).toBeInTheDocument();
expect(screen.getByText('← Previous')).toBeInTheDocument();
});
it('should navigate to next page', () => {
render(<ResultsTable results={largeResults} />);
const nextButton = screen.getByText('Next →');
fireEvent.click(nextButton);
expect(screen.getByText(/Page 2 of 2/)).toBeInTheDocument();
});
it('should navigate to previous page', () => {
render(<ResultsTable results={largeResults} />);
// Go to page 2 first
fireEvent.click(screen.getByText('Next →'));
// Then go back
fireEvent.click(screen.getByText('← Previous'));
expect(screen.getByText(/Page 1 of 2/)).toBeInTheDocument();
});
it('should disable previous button on first page', () => {
render(<ResultsTable results={largeResults} />);
const prevButton = screen.getByText('← Previous');
expect(prevButton).toBeDisabled();
});
it('should disable next button on last page', () => {
render(<ResultsTable results={largeResults} />);
// Navigate to last page
fireEvent.click(screen.getByText('Next →'));
const nextButton = screen.getByText('Next →');
expect(nextButton).toBeDisabled();
});
it('should not show pagination for small datasets', () => {
render(<ResultsTable results={mockResults} />);
expect(screen.queryByText('Next →')).not.toBeInTheDocument();
});
});
describe('copy functionality', () => {
// Mock clipboard API
beforeEach(() => {
Object.assign(navigator, {
clipboard: {
writeText: vi.fn(() => Promise.resolve()),
},
});
});
it('should copy cell value to clipboard', async () => {
render(<ResultsTable results={mockResults} />);
const copyButtons = screen.getAllByLabelText('Copy value');
fireEvent.click(copyButtons[0]);
await waitFor(() => {
expect(navigator.clipboard.writeText).toHaveBeenCalled();
});
});
it('should show copied indicator after copy', async () => {
render(<ResultsTable results={mockResults} />);
const copyButtons = screen.getAllByLabelText('Copy value');
fireEvent.click(copyButtons[0]);
await waitFor(() => {
expect(copyButtons[0]).toHaveTextContent('✓');
});
});
});
describe('export functionality', () => {
it('should render export buttons when onExport provided', () => {
const onExport = vi.fn();
render(<ResultsTable results={mockResults} onExport={onExport} />);
expect(screen.getByText('Export CSV')).toBeInTheDocument();
expect(screen.getByText('Export JSON')).toBeInTheDocument();
expect(screen.getByText('Export JSON-LD')).toBeInTheDocument();
});
it('should call onExport with correct format', () => {
const onExport = vi.fn();
render(<ResultsTable results={mockResults} onExport={onExport} />);
fireEvent.click(screen.getByText('Export CSV'));
expect(onExport).toHaveBeenCalledWith('csv');
fireEvent.click(screen.getByText('Export JSON'));
expect(onExport).toHaveBeenCalledWith('json');
fireEvent.click(screen.getByText('Export JSON-LD'));
expect(onExport).toHaveBeenCalledWith('jsonld');
});
it('should not render export buttons when onExport not provided', () => {
render(<ResultsTable results={mockResults} />);
expect(screen.queryByText('Export CSV')).not.toBeInTheDocument();
});
});
describe('custom max height', () => {
it('should apply custom max height', () => {
const { container } = render(
<ResultsTable results={mockResults} maxHeight="400px" />
);
const wrapper = container.querySelector('.results-table-wrapper');
expect(wrapper).toHaveStyle({ maxHeight: '400px' });
});
});
});