mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-08-07 09:24:09 +00:00
370b67d7ec
* chore: ignore .superpowers/ brainstorm scratch dir
* feat(sidebar): add useStackMenuItems hook with grouped menu model
Pure transform hook that converts StackMenuCtx into four ordered MenuGroup
arrays (inspect, organize, lifecycle, destructive). Shared type contract in
sidebar-types.ts gives both the ContextMenu and DropdownMenu a single source
of truth so they cannot drift. Covered by 8 unit tests.
* refactor(sidebar): stabilize useStackMenuItems memoization deps
Destructure menuVisibility flags into primitive deps so inline object
literals from callers do not defeat memoization. Add a test confirming
isBusy disables all lifecycle items.
* feat(sidebar): add usePinnedStacks hook with per-node localStorage
* refactor(sidebar): stabilize usePinnedStacks eviction signal and isPinned dep
Change evictedOldest shape to { file, seq } so consumer effects re-fire on
repeated evictions. Narrow isPinned's useCallback dep to the current node's
pinned list so it only rebinds on local changes. Add test for eviction
side-effect and a second test for the seq counter.
* feat(sidebar): add useSidebarGroupCollapse hook with per-node keys
* refactor(sidebar): tighten useSidebarGroupCollapse effect ordering
Collapse the two write/read effects into a single skip-next-write ref
pattern so switching nodes no longer writes the previous node's map under
the new key before hydration. Also skip the no-op mount write. Add test
for setCollapsed.
* feat(sidebar): add row + group-header style helpers
* feat(sidebar): add SidebarBrand with mono kicker + serif hero
* feat(sidebar): add SidebarActions wrapper for create + scan
* feat(sidebar): extract SidebarSearch with kbd pill
* feat(sidebar): add StackRow with cyan-rail active state
* refactor(sidebar): dedupe tooltip markup in StackRow, widen test coverage
Extract a local RowTooltip helper so the update and git-pending branches
share the CursorProvider scaffolding. Add four behavioral tests covering
click, keyboard activation, kebab stop-propagation, and the busy loader
branch.
* feat(sidebar): unify context + kebab menus via useStackMenuItems
* feat(sidebar): add StackGroup with collapse and pinned variant
* feat(sidebar): add StackList with pinned + label groups
* feat(sidebar): add SidebarActivityTicker with idle fallback
* feat(sidebar): add StackSidebar container composing the regions
* feat(sidebar): replace sidebar block with StackSidebar composition
* docs(sidebar): add stack sidebar feature page with screenshots
* fix(sidebar): satisfy react-hooks purity and memoization rules
* fix(sidebar): restore "Sencho Logo" alt text for E2E selector
77 lines
2.5 KiB
TypeScript
77 lines
2.5 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,
|
|
isPaid: true,
|
|
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 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();
|
|
});
|
|
});
|