test(stacks): cover orphaned branch; name the stack-container state; type link params (F1,F2,F4)
Maintainer pre-merge review follow-up:
F1: test the orphaned-stack breadcrumb branch (orphaned=true, no regular) —
href carries stackId/orphaned, not external.
F2: extract STACK_CONTAINER_STATE_NAME so code + test share one literal.
F4: type buildStackLinkParams' return as StackLinkParams (documents the real
shape; external stays boolean, serialized by ui-router — no runtime change).
F3 (legacy ?id= deep links) answered wontfix in the PR thread.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -9,7 +9,7 @@ import { server } from '@/setup-tests/server';
|
||||
import { User } from '@/portainer/users/types';
|
||||
import { ContainerDetailsViewModel } from '@/docker/models/containerDetails';
|
||||
|
||||
import { ItemView } from './ItemView';
|
||||
import { ItemView, STACK_CONTAINER_STATE_NAME } from './ItemView';
|
||||
|
||||
const { useCurrentStateAndParamsMock } = vi.hoisted(() => ({
|
||||
useCurrentStateAndParamsMock: vi.fn(),
|
||||
@@ -52,7 +52,7 @@ describe('ItemView', () => {
|
||||
|
||||
it('keeps the stack trail when the container is opened from a stack', async () => {
|
||||
useCurrentStateAndParamsMock.mockReturnValue({
|
||||
state: { name: 'docker.stacks.stack.container' },
|
||||
state: { name: STACK_CONTAINER_STATE_NAME },
|
||||
params: {
|
||||
id: 'container-id-123',
|
||||
endpointId: '1',
|
||||
@@ -82,7 +82,7 @@ describe('ItemView', () => {
|
||||
|
||||
it('keeps the stack trail without a stack id for an external stack', async () => {
|
||||
useCurrentStateAndParamsMock.mockReturnValue({
|
||||
state: { name: 'docker.stacks.stack.container' },
|
||||
state: { name: STACK_CONTAINER_STATE_NAME },
|
||||
params: {
|
||||
id: 'container-id-123',
|
||||
endpointId: '1',
|
||||
@@ -121,6 +121,45 @@ describe('ItemView', () => {
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('keeps the stack trail with the stack id for an orphaned stack', async () => {
|
||||
useCurrentStateAndParamsMock.mockReturnValue({
|
||||
state: { name: STACK_CONTAINER_STATE_NAME },
|
||||
params: {
|
||||
id: 'container-id-123',
|
||||
endpointId: '1',
|
||||
nodeName: undefined,
|
||||
name: 'orphan-stack',
|
||||
stackId: '7',
|
||||
type: '2',
|
||||
// Orphaned stacks are identified by orphaned=true and, unlike regular
|
||||
// stacks, carry no `regular` flag. They still have a DB id (stackId),
|
||||
// so the back-link must keep stackId and must NOT take the external
|
||||
// (id-less) branch.
|
||||
orphaned: 'true',
|
||||
tab: 'logs',
|
||||
},
|
||||
});
|
||||
|
||||
renderComponent();
|
||||
|
||||
// Stack trail is shown instead of the global Containers crumb.
|
||||
const stacksCrumb = await screen.findByRole('link', { name: 'Stacks' });
|
||||
expect(stacksCrumb).toBeVisible();
|
||||
|
||||
const stackCrumb = screen.getByRole('link', { name: 'orphan-stack' });
|
||||
expect(stackCrumb).toBeVisible();
|
||||
const href = stackCrumb.getAttribute('href');
|
||||
// Orphaned stacks keep their numeric stack id so the stack can reload,
|
||||
// flag the orphaned state, and must not be treated as external.
|
||||
expect(href).toContain('stackId=7');
|
||||
expect(href).toContain('orphaned=true');
|
||||
expect(href).not.toContain('external=');
|
||||
|
||||
expect(
|
||||
screen.queryByRole('link', { name: 'Containers' })
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders health status section when container has health data', async () => {
|
||||
renderComponent({
|
||||
container: {
|
||||
|
||||
@@ -22,6 +22,27 @@ import { VolumesSection } from './VolumesSection/VolumesSection';
|
||||
import { ContainerNetworksDatatable } from './ContainerNetworksDatatable';
|
||||
import { HealthStatus } from './HealthStatus';
|
||||
|
||||
// ui-router state name for a container opened from within a stack. Kept as a
|
||||
// single source of truth so a future state rename stays in sync with the
|
||||
// breadcrumb logic (and its test) instead of silently falling back to the
|
||||
// default crumb.
|
||||
export const STACK_CONTAINER_STATE_NAME = 'docker.stacks.stack.container';
|
||||
|
||||
// Shape of the params handed to the docker.stacks.stack route when building the
|
||||
// back-to-stack breadcrumb link. Values are strings (inherited route params)
|
||||
// except `external`, which is emitted as a boolean and serialized to
|
||||
// `external=true` by ui-router.
|
||||
type StackLinkParams = {
|
||||
name?: string;
|
||||
stackId?: string;
|
||||
type?: string;
|
||||
regular?: string;
|
||||
orphaned?: string;
|
||||
orphanedRunning?: string;
|
||||
external?: boolean;
|
||||
tab?: string;
|
||||
};
|
||||
|
||||
export function ItemView() {
|
||||
const environmentId = useEnvironmentId();
|
||||
const { state, params } = useCurrentStateAndParams();
|
||||
@@ -116,7 +137,7 @@ export function ItemView() {
|
||||
);
|
||||
}
|
||||
|
||||
// When a container is opened from a stack (state docker.stacks.stack.container),
|
||||
// When a container is opened from a stack (state STACK_CONTAINER_STATE_NAME),
|
||||
// keep the stack trail in the breadcrumbs so the user can navigate back to the
|
||||
// stack. Otherwise fall back to the global containers list.
|
||||
function getContainerBreadcrumbs(
|
||||
@@ -124,7 +145,7 @@ function getContainerBreadcrumbs(
|
||||
params: Record<string, string | undefined>,
|
||||
containerName: string
|
||||
): Array<Crumb | string> {
|
||||
if (stateName === 'docker.stacks.stack.container') {
|
||||
if (stateName === STACK_CONTAINER_STATE_NAME) {
|
||||
return [
|
||||
{ label: 'Stacks', link: 'docker.stacks' },
|
||||
{
|
||||
@@ -143,7 +164,7 @@ function getContainerBreadcrumbs(
|
||||
// (container) route params, which are inherited from the parent stack state.
|
||||
function buildStackLinkParams(
|
||||
params: Record<string, string | undefined>
|
||||
): Record<string, unknown> {
|
||||
): StackLinkParams {
|
||||
// External stacks have no DB id; they are identified by name/type only.
|
||||
if (params.external === 'true') {
|
||||
return {
|
||||
|
||||
Reference in New Issue
Block a user