portainer/app/react/edge/edge-stacks/components/PrivateRegistryFieldset.tes...

389 lines
12 KiB
TypeScript

import { vi } from 'vitest';
import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { Registry } from '@/react/portainer/registries/types/registry';
import selectEvent from '@/react/test-utils/react-select';
import {
REGISTRY_CREDENTIALS_ENABLED,
PrivateRegistryFieldset,
} from './PrivateRegistryFieldset';
describe('Initial rendering', () => {
it('should render with use credentials switch unchecked by default', () => {
renderComponent();
const checkbox = screen.getByRole('checkbox', { name: /use credentials/i });
expect(checkbox).toBeVisible();
expect(checkbox).not.toBeChecked();
});
it('should render with use credentials switch checked when value is defined', () => {
renderComponent({ value: REGISTRY_CREDENTIALS_ENABLED });
const checkbox = screen.getByRole('checkbox', { name: /use credentials/i });
expect(checkbox).toBeChecked();
});
it('should disable switch when formInvalid is true', () => {
renderComponent({ formInvalid: true });
const checkbox = screen.getByRole('checkbox', { name: /use credentials/i });
expect(checkbox).toBeDisabled();
});
});
describe('Switch interaction', () => {
it('should call onChange when switch is toggled on', async () => {
const user = userEvent.setup();
const onChange = vi.fn();
renderComponent({ onChange });
const checkbox = screen.getByRole('checkbox', { name: /use credentials/i });
await user.click(checkbox);
expect(onChange).toHaveBeenCalledWith(REGISTRY_CREDENTIALS_ENABLED);
});
it('should call onChange with undefined when switch is toggled off', async () => {
const user = userEvent.setup();
const onChange = vi.fn();
renderComponent({ value: REGISTRY_CREDENTIALS_ENABLED, onChange });
expect(screen.queryByLabelText('Registry')).toBeInTheDocument();
const checkbox = screen.getByRole('checkbox', { name: /use credentials/i });
await user.click(checkbox);
expect(onChange).toHaveBeenCalledWith(undefined);
});
it('should not call onChange on initial mount', () => {
const onChange = vi.fn();
renderComponent({ onChange });
expect(onChange).not.toHaveBeenCalled();
});
});
describe('Registry selection', () => {
it('should display registry selector with all registries when switch is on', async () => {
const user = userEvent.setup();
renderComponent({ value: REGISTRY_CREDENTIALS_ENABLED });
expect(screen.getByLabelText('Registry')).toBeVisible();
const selector = screen.getByLabelText('Registry');
await user.click(selector);
await waitFor(() => {
expect(screen.getByText('Docker Hub')).toBeVisible();
expect(screen.getByText('Azure Container Registry')).toBeVisible();
expect(screen.getByText('Private Registry')).toBeVisible();
});
});
it('should show selected registry when value is provided', async () => {
renderComponent({ value: 1 });
const selector = screen.getByLabelText('Registry');
expect(selector).toBeVisible();
expect(screen.getByText('Docker Hub')).toBeVisible();
});
it('should call onChange when a registry is selected', async () => {
const user = userEvent.setup();
const onChange = vi.fn();
renderComponent({ value: REGISTRY_CREDENTIALS_ENABLED, onChange });
// Find the react-select input using the combobox role
const input = screen.getByLabelText('Registry');
await selectEvent.select(input, 'Azure Container Registry', { user });
await waitFor(() => {
expect(onChange).toHaveBeenCalledWith(2);
});
});
it('should update selected value when value prop changes', async () => {
const { rerender } = renderComponent({ value: 1 });
expect(screen.getByText('Docker Hub')).toBeVisible();
rerender(
<PrivateRegistryFieldset
value={2}
registries={getMockRegistries()}
onChange={vi.fn()}
/>
);
await waitFor(() => {
expect(screen.getByText('Azure Container Registry')).toBeVisible();
});
});
it('should call onChange when user switches between registries', async () => {
const user = userEvent.setup();
const onChange = vi.fn();
renderComponent({ value: 1, onChange }); // Docker Hub already selected
expect(screen.getByText('Docker Hub')).toBeVisible();
const input = screen.getByLabelText('Registry');
await selectEvent.select(input, 'Azure Container Registry', { user });
await waitFor(() => {
expect(onChange).toHaveBeenCalledWith(2);
});
});
});
describe('Reload functionality', () => {
it('should show reload button when method is not repository and onReload is provided', async () => {
renderComponent({
value: REGISTRY_CREDENTIALS_ENABLED,
method: 'file',
onReload: vi.fn(),
});
expect(screen.getByRole('button', { name: 'Reload' })).toBeVisible();
});
it('should not show reload button when method is repository', () => {
renderComponent({
value: REGISTRY_CREDENTIALS_ENABLED,
method: 'repository',
onReload: vi.fn(),
});
expect(
screen.queryByTestId('private-registry-reload-button')
).not.toBeInTheDocument();
});
it('should call onReload when reload button is clicked', async () => {
const user = userEvent.setup();
const onReload = vi.fn();
renderComponent({ value: 1, method: 'file', onReload });
const reloadButton = screen.getByRole('button', { name: 'Reload' });
await user.click(reloadButton);
expect(onReload).toHaveBeenCalledTimes(1);
});
it('should show blue tip when method is not repository and switch is on', () => {
renderComponent({ value: REGISTRY_CREDENTIALS_ENABLED, method: 'file' });
expect(
screen.getByText(
/If you make any changes to the image urls in your yaml/i
)
).toBeVisible();
});
it('should not show blue tip when method is repository', () => {
renderComponent({
value: REGISTRY_CREDENTIALS_ENABLED,
method: 'repository',
});
expect(
screen.queryByText(
/If you make any changes to the image urls in your yaml/i
)
).not.toBeInTheDocument();
});
it('should not call onReload when switch is toggled off', async () => {
const user = userEvent.setup();
const onReload = vi.fn();
renderComponent({ value: 1, method: 'file', onReload });
const checkbox = screen.getByRole('checkbox', { name: /use credentials/i });
await user.click(checkbox);
expect(onReload).not.toHaveBeenCalled();
});
});
describe('Error handling', () => {
it('should display error message when errorMessage is provided', async () => {
const errorMessage = 'Images need to be from a single registry';
renderComponent({ value: REGISTRY_CREDENTIALS_ENABLED, errorMessage });
expect(screen.getByText(errorMessage)).toBeVisible();
});
it('should not display registry selector when error message is shown', async () => {
const errorMessage = 'Images need to be from a single registry';
renderComponent({ value: REGISTRY_CREDENTIALS_ENABLED, errorMessage });
expect(screen.queryByLabelText('Registry')).not.toBeInTheDocument();
});
});
describe('Edge cases', () => {
it('should handle single registry option', async () => {
const user = userEvent.setup();
const onChange = vi.fn();
const [singleRegistry] = getMockRegistries();
renderComponent({
registries: [singleRegistry],
value: REGISTRY_CREDENTIALS_ENABLED,
onChange,
});
const input = screen.getByLabelText('Registry');
await selectEvent.select(input, 'Docker Hub', { user });
await waitFor(() => {
expect(onChange).toHaveBeenCalledWith(1);
});
});
it('should handle empty registries array', () => {
renderComponent({ registries: [], value: REGISTRY_CREDENTIALS_ENABLED });
expect(screen.getByLabelText('Registry')).toBeVisible();
});
it('should handle undefined value', () => {
renderComponent({ value: undefined });
expect(screen.queryByLabelText('Registry')).not.toBeInTheDocument();
});
it('should handle value changing from undefined to defined', async () => {
const { rerender } = renderComponent({ value: undefined });
const checkbox = screen.getByRole('checkbox', { name: /use credentials/i });
expect(checkbox).not.toBeChecked();
rerender(
<PrivateRegistryFieldset
registries={getMockRegistries()}
onChange={vi.fn()}
value={REGISTRY_CREDENTIALS_ENABLED}
/>
);
expect(checkbox).toBeChecked();
});
it('should preserve registry selection after reload', async () => {
const user = userEvent.setup();
const onChange = vi.fn();
const onReload = vi.fn();
renderComponent({ value: 1, method: 'file', onChange, onReload });
await waitFor(() => {
expect(screen.getByText('Docker Hub')).toBeVisible();
});
const reloadButton = screen.getByRole('button', { name: 'Reload' });
await user.click(reloadButton);
expect(onReload).toHaveBeenCalledTimes(1);
expect(onChange).not.toHaveBeenCalled(); // Registry selection unchanged
expect(screen.getByText('Docker Hub')).toBeVisible();
});
it('should handle value changing from defined to undefined', async () => {
const { rerender } = renderComponent({ value: 1 });
expect(screen.getByText('Docker Hub')).toBeVisible();
rerender(
<PrivateRegistryFieldset
value={undefined}
registries={getMockRegistries()}
onChange={vi.fn()}
/>
);
// Registry selector should be hidden when value is undefined
await waitFor(() => {
expect(screen.queryByText('Docker Hub')).not.toBeInTheDocument();
expect(screen.queryByLabelText('Registry')).not.toBeInTheDocument();
});
});
it('should handle registry ID that does not exist in registries array', () => {
renderComponent({ value: 999 }); // Non-existent ID
const selector = screen.getByLabelText('Registry');
expect(selector).toBeVisible();
// Should not crash, selector should be empty (no selection shown)
expect(screen.queryByText('Docker Hub')).not.toBeInTheDocument();
expect(
screen.queryByText('Azure Container Registry')
).not.toBeInTheDocument();
expect(screen.queryByText('Private Registry')).not.toBeInTheDocument();
});
});
function getMockRegistries(): Registry[] {
return [
{
Id: 1,
Name: 'Docker Hub',
URL: 'docker.io',
Type: 6,
BaseURL: '',
Authentication: true,
Username: 'user1',
RegistryAccesses: null,
Gitlab: { ProjectId: 0, InstanceURL: '', ProjectPath: '' },
Quay: { OrganisationName: '' },
Github: { UseOrganisation: false, OrganisationName: '' },
Ecr: { Region: '' },
},
{
Id: 2,
Name: 'Azure Container Registry',
URL: 'acr.io',
Type: 2,
BaseURL: '',
Authentication: true,
Username: 'user2',
RegistryAccesses: null,
Gitlab: { ProjectId: 0, InstanceURL: '', ProjectPath: '' },
Quay: { OrganisationName: '' },
Github: { UseOrganisation: false, OrganisationName: '' },
Ecr: { Region: '' },
},
{
Id: 3,
Name: 'Private Registry',
URL: 'registry.example.com',
Type: 3,
BaseURL: '',
Authentication: true,
Username: 'user3',
RegistryAccesses: null,
Gitlab: { ProjectId: 0, InstanceURL: '', ProjectPath: '' },
Quay: { OrganisationName: '' },
Github: { UseOrganisation: false, OrganisationName: '' },
Ecr: { Region: '' },
},
];
}
function renderComponent(
props: Partial<Parameters<typeof PrivateRegistryFieldset>[0]> = {}
) {
const defaultProps = {
registries: getMockRegistries(),
onChange: vi.fn(),
};
const mergedProps = { ...defaultProps, ...props };
return render(<PrivateRegistryFieldset {...mergedProps} />);
}