285 lines
7.7 KiB
TypeScript
285 lines
7.7 KiB
TypeScript
import { render, screen, waitFor } from '@testing-library/react';
|
|
import userEvent from '@testing-library/user-event';
|
|
import { vi } from 'vitest';
|
|
import { http, HttpResponse } from 'msw';
|
|
|
|
import { withTestQueryProvider } from '@/react/test-utils/withTestQuery';
|
|
import { withTestRouter } from '@/react/test-utils/withRouter';
|
|
import { withUserProvider } from '@/react/test-utils/withUserProvider';
|
|
import { server } from '@/setup-tests/server';
|
|
import { createMockUser } from '@/react-tools/test-mocks';
|
|
|
|
import { CreateImageSection } from './CreateImageSection';
|
|
|
|
vi.mock('@uirouter/react', async (importOriginal: () => Promise<object>) => ({
|
|
...(await importOriginal()),
|
|
useCurrentStateAndParams: vi.fn(() => ({
|
|
params: { endpointId: 1 },
|
|
})),
|
|
}));
|
|
|
|
vi.mock('@/portainer/services/notifications', () => ({
|
|
notifySuccess: vi.fn(),
|
|
}));
|
|
|
|
describe('CreateImageSection', () => {
|
|
beforeEach(() => {
|
|
// Setup MSW handlers for registries API
|
|
server.use(
|
|
http.get('/api/endpoints/:endpointId/registries', () =>
|
|
HttpResponse.json([
|
|
{
|
|
Id: 1,
|
|
Name: 'DockerHub',
|
|
Type: 1,
|
|
URL: 'docker.io',
|
|
Username: 'testuser',
|
|
},
|
|
{
|
|
Id: 2,
|
|
Name: 'Private Registry',
|
|
Type: 3,
|
|
URL: 'registry.example.com',
|
|
Username: '',
|
|
},
|
|
])
|
|
),
|
|
// Mock images endpoint for autocomplete functionality
|
|
http.get('/api/endpoints/:endpointId/docker/images/json', () =>
|
|
HttpResponse.json([
|
|
{
|
|
Id: 'sha256:abc123',
|
|
RepoTags: ['docker.io/testuser/my-app:latest', 'nginx:latest'],
|
|
},
|
|
{
|
|
Id: 'sha256:def456',
|
|
RepoTags: ['registry.example.com/another-app:v1.0'],
|
|
},
|
|
])
|
|
)
|
|
);
|
|
});
|
|
|
|
afterEach(() => {
|
|
vi.clearAllMocks();
|
|
});
|
|
|
|
describe('Authorization', () => {
|
|
it('should render widget when user has DockerImageCreate authorization', async () => {
|
|
renderComponent({
|
|
userAuthorizations: {
|
|
DockerImageCreate: true,
|
|
},
|
|
});
|
|
|
|
await waitFor(() => {
|
|
expect(
|
|
screen.getByRole('heading', { name: /create image/i })
|
|
).toBeVisible();
|
|
});
|
|
});
|
|
|
|
it('should not render when user lacks authorization', () => {
|
|
renderComponent({
|
|
userAuthorizations: {
|
|
DockerImageCreate: false,
|
|
},
|
|
});
|
|
|
|
expect(
|
|
screen.queryByRole('heading', { name: /create image/i })
|
|
).not.toBeInTheDocument();
|
|
});
|
|
});
|
|
|
|
describe('Basic Rendering', () => {
|
|
it('should render widget with title', async () => {
|
|
renderComponent();
|
|
|
|
await waitFor(() => {
|
|
expect(
|
|
screen.getByRole('heading', { name: /create image/i })
|
|
).toBeVisible();
|
|
});
|
|
});
|
|
|
|
it('should render description text', async () => {
|
|
renderComponent();
|
|
|
|
await waitFor(() => {
|
|
expect(
|
|
screen.getByText(/you can create an image from this container/i)
|
|
).toBeVisible();
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('Image Creation', () => {
|
|
it('should successfully create image and call onSuccess', async () => {
|
|
const onSuccess = vi.fn();
|
|
let capturedRequest: {
|
|
container?: string;
|
|
repo?: string;
|
|
tag?: string;
|
|
} = {};
|
|
|
|
// Setup MSW handler for commit
|
|
server.use(
|
|
http.post(
|
|
'/api/endpoints/:endpointId/docker/commit',
|
|
async ({ request }) => {
|
|
const url = new URL(request.url);
|
|
capturedRequest = {
|
|
container: url.searchParams.get('container') || undefined,
|
|
repo: url.searchParams.get('repo') || undefined,
|
|
tag: url.searchParams.get('tag') || undefined,
|
|
};
|
|
return HttpResponse.json({ Id: 'sha256:abc123' });
|
|
}
|
|
)
|
|
);
|
|
|
|
renderComponent({ onSuccess });
|
|
|
|
// Wait for form to load
|
|
await waitFor(() => {
|
|
expect(screen.getByRole('combobox', { name: 'Image' })).toBeVisible();
|
|
});
|
|
|
|
// Fill in image name
|
|
const imageInput = screen.getByRole('combobox', { name: 'Image' });
|
|
await userEvent.type(imageInput, 'my-app:v1.0');
|
|
|
|
// Submit form
|
|
const createButton = screen.getByRole('button', { name: /^create$/i });
|
|
|
|
await waitFor(() => {
|
|
expect(createButton).not.toBeDisabled();
|
|
});
|
|
|
|
await userEvent.click(createButton);
|
|
|
|
// Verify API call
|
|
await waitFor(() => {
|
|
expect(capturedRequest.container).toBe('container123');
|
|
expect(capturedRequest.repo).toBe('my-app');
|
|
expect(capturedRequest.tag).toBe('v1.0');
|
|
});
|
|
|
|
// Verify onSuccess callback
|
|
await waitFor(() => {
|
|
expect(onSuccess).toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
it('should handle API error', async () => {
|
|
const onMutationError = vi.fn();
|
|
const consoleErrorSpy = vi
|
|
.spyOn(console, 'error')
|
|
.mockImplementation(() => {});
|
|
|
|
server.use(
|
|
http.post('/api/endpoints/:endpointId/docker/commit', () =>
|
|
HttpResponse.json({ message: 'Container not found' }, { status: 404 })
|
|
)
|
|
);
|
|
|
|
renderComponent({ onMutationError });
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByRole('combobox', { name: 'Image' })).toBeVisible();
|
|
});
|
|
|
|
const imageInput = screen.getByRole('combobox', { name: 'Image' });
|
|
await userEvent.type(imageInput, 'my-app');
|
|
|
|
const createButton = screen.getByRole('button', { name: /^create$/i });
|
|
|
|
await waitFor(() => {
|
|
expect(createButton).not.toBeDisabled();
|
|
});
|
|
|
|
await userEvent.click(createButton);
|
|
|
|
// Wait for the mutation error to be handled
|
|
await waitFor(
|
|
() => {
|
|
expect(onMutationError).toHaveBeenCalled();
|
|
},
|
|
{ timeout: 3000 }
|
|
);
|
|
|
|
consoleErrorSpy.mockRestore();
|
|
});
|
|
|
|
it('should show loading state during creation', async () => {
|
|
server.use(
|
|
http.post('/api/endpoints/:endpointId/docker/commit', async () => {
|
|
await new Promise((resolve) => {
|
|
setTimeout(resolve, 100);
|
|
});
|
|
return HttpResponse.json({ Id: 'sha256:abc123' });
|
|
})
|
|
);
|
|
|
|
renderComponent();
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByRole('combobox', { name: 'Image' })).toBeVisible();
|
|
});
|
|
|
|
const imageInput = screen.getByRole('combobox', { name: 'Image' });
|
|
await userEvent.type(imageInput, 'my-app');
|
|
|
|
const createButton = screen.getByRole('button', { name: /^create$/i });
|
|
|
|
await waitFor(() => {
|
|
expect(createButton).not.toBeDisabled();
|
|
});
|
|
|
|
await userEvent.click(createButton);
|
|
|
|
// Check loading state
|
|
await waitFor(() => {
|
|
expect(
|
|
screen.getByRole('button', { name: /creating image/i })
|
|
).toBeDisabled();
|
|
});
|
|
});
|
|
});
|
|
});
|
|
|
|
function renderComponent({
|
|
userAuthorizations,
|
|
onMutationError,
|
|
...componentProps
|
|
}: Partial<React.ComponentProps<typeof CreateImageSection>> & {
|
|
userAuthorizations?: Record<string, boolean>;
|
|
onMutationError?: (error: unknown) => void;
|
|
} = {}) {
|
|
const defaultProps: React.ComponentProps<typeof CreateImageSection> = {
|
|
environmentId: 1,
|
|
containerId: 'container123',
|
|
...componentProps,
|
|
};
|
|
|
|
const defaultAuthorizations = {
|
|
DockerImageCreate: true,
|
|
};
|
|
|
|
const mockUser = createMockUser({
|
|
EndpointAuthorizations: {
|
|
1: userAuthorizations || defaultAuthorizations,
|
|
},
|
|
Id: 1,
|
|
Role: 1,
|
|
});
|
|
|
|
const Wrapper = withTestQueryProvider(
|
|
withUserProvider(withTestRouter(CreateImageSection), mockUser),
|
|
onMutationError ? { onMutationError } : undefined
|
|
);
|
|
|
|
return render(<Wrapper {...defaultProps} />);
|
|
}
|