mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-08-08 01:43:55 +00:00
9e1d97d39e
Wrapped every TooltipContent child in a font-mono text-xs tabular-nums text-stat-value span so all tooltips across the app share a consistent look. Previously some used plain text, others used <p> elements with different fonts and sizes.
125 lines
4.9 KiB
TypeScript
125 lines
4.9 KiB
TypeScript
import { describe, it, expect, vi } from 'vitest';
|
|
import { render, screen, fireEvent } from '@testing-library/react';
|
|
import type { ComponentProps } from 'react';
|
|
import { StackRow } from '../StackRow';
|
|
import type { Label } from '@/components/label-types';
|
|
|
|
function base(overrides: Partial<ComponentProps<typeof StackRow>> = {}) {
|
|
return {
|
|
file: 'web.yml',
|
|
displayName: 'web',
|
|
status: 'running' as const,
|
|
isBusy: false,
|
|
isActive: false,
|
|
labels: [] as Label[],
|
|
hasUpdate: false,
|
|
hasGitPending: false,
|
|
onSelect: vi.fn(),
|
|
kebabSlot: null,
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
describe('StackRow', () => {
|
|
it('renders UP for running', () => {
|
|
render(<StackRow {...base()} />);
|
|
expect(screen.getByText('UP')).toBeInTheDocument();
|
|
});
|
|
|
|
it('renders DN for exited', () => {
|
|
render(<StackRow {...base({ status: 'exited' })} />);
|
|
expect(screen.getByText('DN')).toBeInTheDocument();
|
|
});
|
|
|
|
it('renders -- for unknown', () => {
|
|
render(<StackRow {...base({ status: 'unknown' })} />);
|
|
expect(screen.getByText('--')).toBeInTheDocument();
|
|
});
|
|
|
|
it('renders PT with the amber class for partial', () => {
|
|
const { container } = render(<StackRow {...base({ status: 'partial', running: 3, total: 5 })} />);
|
|
expect(screen.getByText('PT')).toBeInTheDocument();
|
|
expect(container.querySelector('.text-warning')).not.toBeNull();
|
|
});
|
|
|
|
it('wraps the partial pill in a hover tooltip', () => {
|
|
render(<StackRow {...base({ status: 'partial', running: 3, total: 5 })} />);
|
|
const trigger = screen.getByText('PT');
|
|
// Radix TooltipTrigger adds data-state to the wrapped element.
|
|
expect(trigger.getAttribute('data-state')).toBe('closed');
|
|
});
|
|
|
|
it('does not wrap a non-partial pill in a tooltip', () => {
|
|
render(<StackRow {...base({ status: 'running' })} />);
|
|
expect(screen.getByText('UP').getAttribute('data-state')).toBeNull();
|
|
});
|
|
|
|
it('renders cyan rail only when active', () => {
|
|
const { rerender } = render(<StackRow {...base({ isActive: false })} />);
|
|
expect(screen.getByTestId('stack-row')).not.toHaveClass('bg-accent/[0.07]');
|
|
rerender(<StackRow {...base({ isActive: true })} />);
|
|
expect(screen.getByTestId('stack-row')).toHaveClass('bg-accent/[0.07]');
|
|
});
|
|
|
|
it('fires onSelect on click', () => {
|
|
const onSelect = vi.fn();
|
|
render(<StackRow {...base({ onSelect })} />);
|
|
screen.getByTestId('stack-row').click();
|
|
expect(onSelect).toHaveBeenCalledWith('web.yml');
|
|
});
|
|
|
|
it('fires onSelect on Enter and Space', () => {
|
|
const onSelect = vi.fn();
|
|
render(<StackRow {...base({ onSelect })} />);
|
|
const row = screen.getByTestId('stack-row');
|
|
row.focus();
|
|
fireEvent.keyDown(row, { key: 'Enter' });
|
|
fireEvent.keyDown(row, { key: ' ' });
|
|
expect(onSelect).toHaveBeenCalledTimes(2);
|
|
});
|
|
|
|
it('kebab click does not trigger onSelect', () => {
|
|
const onSelect = vi.fn();
|
|
render(<StackRow {...base({ onSelect, kebabSlot: <button data-testid="kebab">k</button> })} />);
|
|
screen.getByTestId('kebab').click();
|
|
expect(onSelect).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('renders loader when isBusy', () => {
|
|
const { container } = render(<StackRow {...base({ isBusy: true })} />);
|
|
expect(container.querySelector('.animate-spin')).not.toBeNull();
|
|
expect(screen.queryByText('UP')).not.toBeInTheDocument();
|
|
});
|
|
|
|
it('does not render trailing label color dots when labels are assigned', () => {
|
|
const labels: Label[] = [
|
|
{ id: 1, node_id: 0, name: 'prod', color: 'teal' },
|
|
{ id: 2, node_id: 0, name: 'media', color: 'blue' },
|
|
];
|
|
const { container } = render(<StackRow {...base({ labels })} />);
|
|
expect(container.querySelectorAll('[style*="--label-"]')).toHaveLength(0);
|
|
});
|
|
|
|
// ── Image-update check status indicator ────────────────────────────────
|
|
|
|
it('shows a muted check-failed indicator when the last check failed and there is no update', () => {
|
|
const { container } = render(<StackRow {...base({ status: 'running', hasUpdate: false, checkStatus: 'failed', lastError: 'Registry unreachable' })} />);
|
|
// The trailing slot renders a tooltip-wrapped AlertCircle icon (an SVG).
|
|
const trailingSlot = container.querySelector('[data-state="closed"]');
|
|
expect(trailingSlot).not.toBeNull();
|
|
// It is not the update dot.
|
|
expect(container.querySelector('.bg-update')).toBeNull();
|
|
});
|
|
|
|
it('prefers the update dot over the check-failed indicator', () => {
|
|
const { container } = render(<StackRow {...base({ status: 'running', hasUpdate: true, checkStatus: 'failed' })} />);
|
|
expect(container.querySelector('.bg-update')).not.toBeNull();
|
|
});
|
|
|
|
it('shows no trailing indicator for a clean ok check with no update', () => {
|
|
const { container } = render(<StackRow {...base({ status: 'running', hasUpdate: false, checkStatus: 'ok' })} />);
|
|
expect(container.querySelector('.lucide-alert-circle')).toBeNull();
|
|
expect(container.querySelector('.bg-update')).toBeNull();
|
|
});
|
|
});
|