Files
portainer/app/react/docker/containers/queries/useContainerImageStatus.test.tsx
agent_coder 6aecdfbe46 feat(containers): interactive image-status badge (click to update / re-check)
Make the container image-status badge actionable, matching native Portainer:
- Clicking "Update available" opens the update confirm dialog and runs the
  existing update flow (standalone recreate-with-pull / stack redeploy), gated
  and disabled while in flight to avoid a double submit. The confirm+apply logic
  is extracted from UpdateNowButton into a shared useApplyContainerImageUpdate
  hook so the details button and the list badge share one implementation.
- Clicking "Up to date" re-queries the registry. Because the server caches image
  status (statusCache 5m + remoteDigestCache 5s), a plain refetch was a no-op, so
  the endpoint gains an optional ?force=true that bypasses BOTH caches for a
  manual re-check while still repopulating them; the default (auto badges + the
  auto-update daemon) keeps using the caches unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 19:04:49 +03:00

108 lines
3.3 KiB
TypeScript

import { renderHook } from '@testing-library/react-hooks';
import { http, HttpResponse } from 'msw';
import { server } from '@/setup-tests/server';
import { withTestQueryProvider } from '@/react/test-utils/withTestQuery';
import {
ContainerImageStatus,
useContainerImageStatus,
useRecheckContainerImageStatus,
} from './useContainerImageStatus';
const environmentId = 3;
const containerId = 'abc123';
function renderImageStatusHook(nodeName?: string) {
return renderHook(
() => useContainerImageStatus(environmentId, containerId, nodeName),
{ wrapper: withTestQueryProvider(({ children }) => <>{children}</>) }
);
}
describe('useContainerImageStatus', () => {
it('calls the non-proxied image_status endpoint and returns the status', async () => {
const payload: ContainerImageStatus = { Status: 'outdated' };
server.use(
http.get(
`/api/docker/${environmentId}/containers/${containerId}/image_status`,
() => HttpResponse.json(payload)
)
);
const { result, waitFor } = renderImageStatusHook();
await waitFor(() => result.current.isSuccess);
expect(result.current.data).toEqual(payload);
});
it('forwards nodeName as a query parameter', async () => {
let receivedNodeName: string | null = null;
server.use(
http.get(
`/api/docker/${environmentId}/containers/${containerId}/image_status`,
({ request }) => {
receivedNodeName = new URL(request.url).searchParams.get('nodeName');
return HttpResponse.json<ContainerImageStatus>({ Status: 'updated' });
}
)
);
const { result, waitFor } = renderImageStatusHook('node-2');
await waitFor(() => result.current.isSuccess);
expect(receivedNodeName).toBe('node-2');
});
it('does not send the force parameter on the background query', async () => {
let receivedForce: string | null = 'unset';
server.use(
http.get(
`/api/docker/${environmentId}/containers/${containerId}/image_status`,
({ request }) => {
receivedForce = new URL(request.url).searchParams.get('force');
return HttpResponse.json<ContainerImageStatus>({ Status: 'updated' });
}
)
);
const { result, waitFor } = renderImageStatusHook();
await waitFor(() => result.current.isSuccess);
expect(receivedForce).toBeNull();
});
});
describe('useRecheckContainerImageStatus', () => {
it('sends force=true and updates the shared query cache with the fresh status', async () => {
let receivedForce: string | null = null;
server.use(
http.get(
`/api/docker/${environmentId}/containers/${containerId}/image_status`,
({ request }) => {
receivedForce = new URL(request.url).searchParams.get('force');
return HttpResponse.json<ContainerImageStatus>({
Status: 'outdated',
});
}
)
);
const wrapper = withTestQueryProvider(({ children }) => <>{children}</>);
const { result, waitFor } = renderHook(
() => useRecheckContainerImageStatus(environmentId, containerId),
{ wrapper }
);
result.current.mutate();
await waitFor(() => result.current.isSuccess);
expect(receivedForce).toBe('true');
expect(result.current.data).toEqual({ Status: 'outdated' });
});
});