WIP: pre-granular-permissions commit
feat: Add category and support contact fields to Agent schema and UI components
Revert "feat: Add category and support contact fields to Agent schema and UI components"
This reverts commit c43a52b4c9.
Fix: Update import for renderHook in useAgentCategories.spec.tsx
fix: Update icon rendering in AgentCategoryDisplay tests to use empty spans
refactor: Improve category synchronization logic and clean up AgentConfig component
refactor: Remove unused UI flow translations from translation.json
feat: agent marketplace features
🔐 feat: Granular Role-based Permissions + Entra ID Group Discovery (#7804)
371 lines
11 KiB
TypeScript
371 lines
11 KiB
TypeScript
import React from 'react';
|
|
import { render, screen, waitFor, act } from '@testing-library/react';
|
|
import { SmartLoader, useHasData } from '../SmartLoader';
|
|
|
|
// Mock setTimeout and clearTimeout for testing
|
|
jest.useFakeTimers();
|
|
|
|
describe('SmartLoader', () => {
|
|
const LoadingComponent = () => <div data-testid="loading">Loading...</div>;
|
|
const ContentComponent = () => (
|
|
<div data-testid="content">
|
|
{/* eslint-disable-line i18next/no-literal-string */}Content loaded
|
|
</div>
|
|
);
|
|
|
|
beforeEach(() => {
|
|
jest.clearAllTimers();
|
|
});
|
|
|
|
afterEach(() => {
|
|
jest.useRealTimers();
|
|
jest.useFakeTimers();
|
|
});
|
|
|
|
describe('Basic functionality', () => {
|
|
it('shows content immediately when not loading', () => {
|
|
render(
|
|
<SmartLoader isLoading={false} hasData={true} loadingComponent={<LoadingComponent />}>
|
|
<ContentComponent />
|
|
</SmartLoader>,
|
|
);
|
|
|
|
expect(screen.getByTestId('content')).toBeInTheDocument();
|
|
expect(screen.queryByTestId('loading')).not.toBeInTheDocument();
|
|
});
|
|
|
|
it('shows content immediately when loading but has existing data', () => {
|
|
render(
|
|
<SmartLoader isLoading={true} hasData={true} loadingComponent={<LoadingComponent />}>
|
|
<ContentComponent />
|
|
</SmartLoader>,
|
|
);
|
|
|
|
expect(screen.getByTestId('content')).toBeInTheDocument();
|
|
expect(screen.queryByTestId('loading')).not.toBeInTheDocument();
|
|
});
|
|
|
|
it('shows content initially, then loading after delay when loading with no data', async () => {
|
|
render(
|
|
<SmartLoader
|
|
isLoading={true}
|
|
hasData={false}
|
|
delay={150}
|
|
loadingComponent={<LoadingComponent />}
|
|
>
|
|
<ContentComponent />
|
|
</SmartLoader>,
|
|
);
|
|
|
|
// Initially shows content
|
|
expect(screen.getByTestId('content')).toBeInTheDocument();
|
|
expect(screen.queryByTestId('loading')).not.toBeInTheDocument();
|
|
|
|
// After delay, shows loading
|
|
act(() => {
|
|
jest.advanceTimersByTime(150);
|
|
});
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByTestId('loading')).toBeInTheDocument();
|
|
expect(screen.queryByTestId('content')).not.toBeInTheDocument();
|
|
});
|
|
});
|
|
|
|
it('prevents loading flash for quick responses', async () => {
|
|
const { rerender } = render(
|
|
<SmartLoader
|
|
isLoading={true}
|
|
hasData={false}
|
|
delay={150}
|
|
loadingComponent={<LoadingComponent />}
|
|
>
|
|
<ContentComponent />
|
|
</SmartLoader>,
|
|
);
|
|
|
|
// Initially shows content
|
|
expect(screen.getByTestId('content')).toBeInTheDocument();
|
|
|
|
// Advance time but not past delay
|
|
act(() => {
|
|
jest.advanceTimersByTime(100);
|
|
});
|
|
|
|
// Loading finishes before delay
|
|
rerender(
|
|
<SmartLoader
|
|
isLoading={false}
|
|
hasData={true}
|
|
delay={150}
|
|
loadingComponent={<LoadingComponent />}
|
|
>
|
|
<ContentComponent />
|
|
</SmartLoader>,
|
|
);
|
|
|
|
// Should still show content, never showed loading
|
|
expect(screen.getByTestId('content')).toBeInTheDocument();
|
|
expect(screen.queryByTestId('loading')).not.toBeInTheDocument();
|
|
|
|
// Advance past original delay to ensure loading doesn't appear
|
|
act(() => {
|
|
jest.advanceTimersByTime(100);
|
|
});
|
|
|
|
expect(screen.getByTestId('content')).toBeInTheDocument();
|
|
expect(screen.queryByTestId('loading')).not.toBeInTheDocument();
|
|
});
|
|
});
|
|
|
|
describe('Delay behavior', () => {
|
|
it('respects custom delay times', async () => {
|
|
render(
|
|
<SmartLoader
|
|
isLoading={true}
|
|
hasData={false}
|
|
delay={300}
|
|
loadingComponent={<LoadingComponent />}
|
|
>
|
|
<ContentComponent />
|
|
</SmartLoader>,
|
|
);
|
|
|
|
// Should show content initially
|
|
expect(screen.getByTestId('content')).toBeInTheDocument();
|
|
|
|
// Should not show loading before delay
|
|
act(() => {
|
|
jest.advanceTimersByTime(250);
|
|
});
|
|
expect(screen.getByTestId('content')).toBeInTheDocument();
|
|
expect(screen.queryByTestId('loading')).not.toBeInTheDocument();
|
|
|
|
// Should show loading after delay
|
|
act(() => {
|
|
jest.advanceTimersByTime(60);
|
|
});
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByTestId('loading')).toBeInTheDocument();
|
|
});
|
|
});
|
|
|
|
it('uses default delay when not specified', async () => {
|
|
render(
|
|
<SmartLoader isLoading={true} hasData={false} loadingComponent={<LoadingComponent />}>
|
|
<ContentComponent />
|
|
</SmartLoader>,
|
|
);
|
|
|
|
// Should show content initially
|
|
expect(screen.getByTestId('content')).toBeInTheDocument();
|
|
|
|
// Should show loading after default delay (150ms)
|
|
act(() => {
|
|
jest.advanceTimersByTime(150);
|
|
});
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByTestId('loading')).toBeInTheDocument();
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('State transitions', () => {
|
|
it('immediately hides loading when loading completes', async () => {
|
|
const { rerender } = render(
|
|
<SmartLoader
|
|
isLoading={true}
|
|
hasData={false}
|
|
delay={100}
|
|
loadingComponent={<LoadingComponent />}
|
|
>
|
|
<ContentComponent />
|
|
</SmartLoader>,
|
|
);
|
|
|
|
// Advance past delay to show loading
|
|
act(() => {
|
|
jest.advanceTimersByTime(100);
|
|
});
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByTestId('loading')).toBeInTheDocument();
|
|
});
|
|
|
|
// Loading completes
|
|
rerender(
|
|
<SmartLoader
|
|
isLoading={false}
|
|
hasData={true}
|
|
delay={100}
|
|
loadingComponent={<LoadingComponent />}
|
|
>
|
|
<ContentComponent />
|
|
</SmartLoader>,
|
|
);
|
|
|
|
// Should immediately show content
|
|
expect(screen.getByTestId('content')).toBeInTheDocument();
|
|
expect(screen.queryByTestId('loading')).not.toBeInTheDocument();
|
|
});
|
|
|
|
it('handles rapid loading state changes correctly', async () => {
|
|
const { rerender } = render(
|
|
<SmartLoader
|
|
isLoading={true}
|
|
hasData={false}
|
|
delay={100}
|
|
loadingComponent={<LoadingComponent />}
|
|
>
|
|
<ContentComponent />
|
|
</SmartLoader>,
|
|
);
|
|
|
|
// Rapid state changes
|
|
rerender(
|
|
<SmartLoader
|
|
isLoading={false}
|
|
hasData={true}
|
|
delay={100}
|
|
loadingComponent={<LoadingComponent />}
|
|
>
|
|
<ContentComponent />
|
|
</SmartLoader>,
|
|
);
|
|
|
|
rerender(
|
|
<SmartLoader
|
|
isLoading={true}
|
|
hasData={false}
|
|
delay={100}
|
|
loadingComponent={<LoadingComponent />}
|
|
>
|
|
<ContentComponent />
|
|
</SmartLoader>,
|
|
);
|
|
|
|
// Should show content throughout rapid changes
|
|
expect(screen.getByTestId('content')).toBeInTheDocument();
|
|
expect(screen.queryByTestId('loading')).not.toBeInTheDocument();
|
|
});
|
|
});
|
|
|
|
describe('CSS classes', () => {
|
|
it('applies custom className', () => {
|
|
const { container } = render(
|
|
<SmartLoader
|
|
isLoading={false}
|
|
hasData={true}
|
|
loadingComponent={<LoadingComponent />}
|
|
className="custom-class"
|
|
>
|
|
<ContentComponent />
|
|
</SmartLoader>,
|
|
);
|
|
|
|
const wrapper = container.firstChild as HTMLElement;
|
|
expect(wrapper).toHaveClass('custom-class');
|
|
});
|
|
|
|
it('applies className to both loading and content states', async () => {
|
|
const { container } = render(
|
|
<SmartLoader
|
|
isLoading={true}
|
|
hasData={false}
|
|
delay={50}
|
|
loadingComponent={<LoadingComponent />}
|
|
className="custom-class"
|
|
>
|
|
<ContentComponent />
|
|
</SmartLoader>,
|
|
);
|
|
|
|
// Content state
|
|
expect(container.firstChild).toHaveClass('custom-class');
|
|
|
|
// Loading state
|
|
act(() => {
|
|
jest.advanceTimersByTime(50);
|
|
});
|
|
|
|
await waitFor(() => {
|
|
expect(container.firstChild).toHaveClass('custom-class');
|
|
});
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('useHasData', () => {
|
|
const TestComponent: React.FC<{ data: any }> = ({ data }) => {
|
|
const hasData = useHasData(data);
|
|
return <div data-testid="result">{hasData ? 'has-data' : 'no-data'}</div>;
|
|
};
|
|
|
|
it('returns false for null data', () => {
|
|
render(<TestComponent data={null} />);
|
|
expect(screen.getByTestId('result')).toHaveTextContent('no-data');
|
|
});
|
|
|
|
it('returns false for undefined data', () => {
|
|
render(<TestComponent data={undefined} />);
|
|
expect(screen.getByTestId('result')).toHaveTextContent('no-data');
|
|
});
|
|
|
|
it('detects empty agents array as no data', () => {
|
|
render(<TestComponent data={{ agents: [] }} />);
|
|
expect(screen.getByTestId('result')).toHaveTextContent('no-data');
|
|
});
|
|
|
|
it('detects non-empty agents array as has data', () => {
|
|
render(<TestComponent data={{ agents: [{ id: '1', name: 'Test' }] }} />);
|
|
expect(screen.getByTestId('result')).toHaveTextContent('has-data');
|
|
});
|
|
|
|
it('detects invalid agents property as no data', () => {
|
|
render(<TestComponent data={{ agents: 'not-array' }} />);
|
|
expect(screen.getByTestId('result')).toHaveTextContent('no-data');
|
|
});
|
|
|
|
it('detects empty array as no data', () => {
|
|
render(<TestComponent data={[]} />);
|
|
expect(screen.getByTestId('result')).toHaveTextContent('no-data');
|
|
});
|
|
|
|
it('detects non-empty array as has data', () => {
|
|
render(<TestComponent data={[{ name: 'category1' }]} />);
|
|
expect(screen.getByTestId('result')).toHaveTextContent('has-data');
|
|
});
|
|
|
|
it('detects agent with id as has data', () => {
|
|
render(<TestComponent data={{ id: '123', name: 'Test Agent' }} />);
|
|
expect(screen.getByTestId('result')).toHaveTextContent('has-data');
|
|
});
|
|
|
|
it('detects agent with name only as has data', () => {
|
|
render(<TestComponent data={{ name: 'Test Agent' }} />);
|
|
expect(screen.getByTestId('result')).toHaveTextContent('has-data');
|
|
});
|
|
|
|
it('detects object without id or name as no data', () => {
|
|
render(<TestComponent data={{ description: 'Some description' }} />);
|
|
expect(screen.getByTestId('result')).toHaveTextContent('no-data');
|
|
});
|
|
|
|
it('handles string data as no data', () => {
|
|
render(<TestComponent data="some string" />);
|
|
expect(screen.getByTestId('result')).toHaveTextContent('no-data');
|
|
});
|
|
|
|
it('handles number data as no data', () => {
|
|
render(<TestComponent data={42} />);
|
|
expect(screen.getByTestId('result')).toHaveTextContent('no-data');
|
|
});
|
|
|
|
it('handles boolean data as no data', () => {
|
|
render(<TestComponent data={true} />);
|
|
expect(screen.getByTestId('result')).toHaveTextContent('no-data');
|
|
});
|
|
});
|