Files
portainer/app/react/components/ResourceDetailHeader/ResourceStatBlock.test.tsx
2026-05-18 18:01:36 -03:00

201 lines
5.3 KiB
TypeScript

import { ComponentProps, ReactNode } from 'react';
import { render, screen } from '@testing-library/react';
import {
ResourceStatBlock,
ResourceStatBlockStatus,
} from './ResourceStatBlock';
type ValueProps = ComponentProps<typeof ResourceStatBlock.Value>;
type ValueSize = NonNullable<ValueProps['size']>;
type ValueAlign = NonNullable<ValueProps['align']>;
interface RenderOptions {
status?: ResourceStatBlockStatus;
labelIcon?: ReactNode;
value?: ReactNode;
valueSuffix?: ReactNode;
valueDot?: boolean;
valueSize?: ValueSize;
valueAlign?: ValueAlign;
meta?: ReactNode;
}
function renderComponent({
status,
labelIcon,
value = 'Synced',
valueSuffix,
valueDot,
valueSize,
valueAlign,
meta,
}: RenderOptions = {}) {
return render(
<ResourceStatBlock status={status} data-cy="stat-block">
<ResourceStatBlock.Label icon={labelIcon}>
Sync Status
</ResourceStatBlock.Label>
<ResourceStatBlock.Value
dot={valueDot}
suffix={valueSuffix}
size={valueSize}
align={valueAlign}
>
{value}
</ResourceStatBlock.Value>
{meta && <ResourceStatBlock.Meta>{meta}</ResourceStatBlock.Meta>}
</ResourceStatBlock>
);
}
describe('ResourceStatBlock', () => {
it('should render the label and value', () => {
renderComponent();
expect(screen.getByText('Sync Status')).toBeVisible();
expect(screen.getByText('Synced')).toBeVisible();
});
it('should render the icon when provided to Label', () => {
renderComponent({ labelIcon: <span data-cy="stat-icon">i</span> });
expect(screen.getByTestId('stat-icon')).toBeInTheDocument();
});
it('should render the suffix when provided to Value', () => {
renderComponent({ valueSuffix: 'healthy' });
expect(screen.getByText('healthy')).toBeVisible();
});
it('should omit the suffix when not provided', () => {
renderComponent();
expect(screen.queryByText('healthy')).not.toBeInTheDocument();
});
it('should render the meta line when provided', () => {
renderComponent({ meta: 'Last reconciled 2 min ago' });
expect(screen.getByText('Last reconciled 2 min ago')).toBeVisible();
});
it('should omit the meta line when not provided', () => {
renderComponent();
expect(
screen.queryByText('Last reconciled 2 min ago')
).not.toBeInTheDocument();
});
describe('dot', () => {
it('should not render a dot by default', () => {
renderComponent();
expect(screen.queryByTestId('stat-block-dot')).not.toBeInTheDocument();
});
it('should render a dot inheriting the container status when dot is true', () => {
renderComponent({ valueDot: true, status: 'success' });
const wrapper = screen.getByTestId('stat-block-dot');
expect(wrapper.firstElementChild).toHaveClass('bg-success-7');
});
});
describe('status variants', () => {
const containerCases: Array<[ResourceStatBlockStatus, string]> = [
['success', 'border-green-4'],
['danger', 'border-error-4'],
['warning', 'border-warning-4'],
['pending', 'border-blue-4'],
['muted', 'border-gray-4'],
];
it.each(containerCases)(
'should apply the %s container styling',
(status, expectedClass) => {
renderComponent({ status });
expect(screen.getByTestId('stat-block')).toHaveClass(expectedClass);
}
);
const valueCases: Array<[ResourceStatBlockStatus, string]> = [
['success', 'text-green-8'],
['danger', 'text-error-8'],
['warning', 'text-warning-8'],
['pending', 'text-blue-8'],
['muted', 'text-graphite-700'],
];
it.each(valueCases)(
'should apply the %s value text color',
(status, expectedClass) => {
renderComponent({ status });
expect(screen.getByText('Synced')).toHaveClass(expectedClass);
}
);
it('should default to muted styling when status is omitted', () => {
renderComponent();
expect(screen.getByTestId('stat-block')).toHaveClass('border-gray-4');
expect(screen.getByText('Synced')).toHaveClass('text-graphite-700');
});
});
describe('Value align', () => {
const cases: Array<[ValueAlign, string]> = [
['start', 'justify-start'],
['center', 'justify-center'],
['end', 'justify-end'],
];
it.each(cases)(
'should align the value row to %s',
(align, expectedClass) => {
renderComponent({ valueAlign: align });
expect(screen.getByText('Synced').parentElement).toHaveClass(
expectedClass
);
}
);
it('should default to start alignment', () => {
renderComponent();
expect(screen.getByText('Synced').parentElement).toHaveClass(
'justify-start'
);
});
});
describe('Value size', () => {
const cases: Array<[ValueSize, string]> = [
['xs', 'text-xs'],
['sm', 'text-sm'],
['base', 'text-base'],
['lg', 'text-lg'],
];
it.each(cases)(
'should size the value text as %s',
(size, expectedClass) => {
renderComponent({ valueSize: size });
expect(screen.getByText('Synced')).toHaveClass(expectedClass);
}
);
it('should default to xs sizing', () => {
renderComponent();
expect(screen.getByText('Synced')).toHaveClass('text-xs');
});
});
});