test: cover work-status empty panel recovery
This commit is contained in:
@@ -203,6 +203,11 @@ section decides for itself that it has nothing to say, so they report through
|
|||||||
`presenceContext.ts` and the panel collapses when none rendered. Deriving that
|
`presenceContext.ts` and the panel collapses when none rendered. Deriving that
|
||||||
at the panel level would mean duplicating every data source the sections read.
|
at the panel level would mean duplicating every data source the sections read.
|
||||||
|
|
||||||
|
There is one deliberate exception: when the user hides every section, the card
|
||||||
|
stays visible with a localized empty state and section controls. Collapsing that
|
||||||
|
state would also hide the only recovery path. A panel with enabled sections but
|
||||||
|
no data still follows the presence reports and collapses as before.
|
||||||
|
|
||||||
The scroll offset resets on session change: restoring one session's offset into
|
The scroll offset resets on session change: restoring one session's offset into
|
||||||
another's shorter panel lands somewhere arbitrary.
|
another's shorter panel lands somewhere arbitrary.
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import React from 'react';
|
|||||||
import { useI18n } from '@/lib/i18n';
|
import { useI18n } from '@/lib/i18n';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
import { ScrollShadow } from '@/components/ui/ScrollShadow';
|
import { ScrollShadow } from '@/components/ui/ScrollShadow';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
import { useUIStore } from '@/stores/useUIStore';
|
import { useUIStore } from '@/stores/useUIStore';
|
||||||
import { WORK_STATUS_PANEL_WIDTH } from './useWorkStatusVisibility';
|
import { WORK_STATUS_PANEL_WIDTH } from './useWorkStatusVisibility';
|
||||||
import { WorkStatusGoalRow } from './WorkStatusGoalRow';
|
import { WorkStatusGoalRow } from './WorkStatusGoalRow';
|
||||||
@@ -13,7 +14,11 @@ import { WorkStatusMcpSection } from './WorkStatusMcpSection';
|
|||||||
import { WorkStatusPinnedSection } from './WorkStatusPinnedSection';
|
import { WorkStatusPinnedSection } from './WorkStatusPinnedSection';
|
||||||
import { WorkStatusContextSection } from './WorkStatusContextSection';
|
import { WorkStatusContextSection } from './WorkStatusContextSection';
|
||||||
import { WorkStatusSectionsDialog } from './WorkStatusSectionsDialog';
|
import { WorkStatusSectionsDialog } from './WorkStatusSectionsDialog';
|
||||||
import { areAllWorkStatusSectionsHidden, isWorkStatusSectionVisible } from './sections';
|
import {
|
||||||
|
areAllWorkStatusSectionsHidden,
|
||||||
|
getWorkStatusPanelPresentation,
|
||||||
|
isWorkStatusSectionVisible,
|
||||||
|
} from './sections';
|
||||||
import { WorkStatusPresenceProvider } from './presence';
|
import { WorkStatusPresenceProvider } from './presence';
|
||||||
import { Icon } from '@/components/icon/Icon';
|
import { Icon } from '@/components/icon/Icon';
|
||||||
|
|
||||||
@@ -89,7 +94,12 @@ export const WorkStatusPanel: React.FC<Props> = ({ sessionId, directory, visible
|
|||||||
// for the transient "no data yet" state so the panel doesn't flash a bare
|
// for the transient "no data yet" state so the panel doesn't flash a bare
|
||||||
// bordered card on first mount.
|
// bordered card on first mount.
|
||||||
const allSectionsHidden = areAllWorkStatusSectionsHidden(hiddenSections);
|
const allSectionsHidden = areAllWorkStatusSectionsHidden(hiddenSections);
|
||||||
const interactive = visible && (renderedSections > 0 || allSectionsHidden);
|
const { interactive, showEmptyState } = getWorkStatusPanelPresentation({
|
||||||
|
visible,
|
||||||
|
contentMounted,
|
||||||
|
renderedSections,
|
||||||
|
allSectionsHidden,
|
||||||
|
});
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (visible) {
|
if (visible) {
|
||||||
setContentMounted(true);
|
setContentMounted(true);
|
||||||
@@ -251,16 +261,17 @@ export const WorkStatusPanel: React.FC<Props> = ({ sessionId, directory, visible
|
|||||||
</WorkStatusPresenceProvider>
|
</WorkStatusPresenceProvider>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
{contentMounted && allSectionsHidden ? (
|
{showEmptyState ? (
|
||||||
<div className="flex flex-col items-center justify-center px-4 py-8 text-center">
|
<div className="flex flex-col items-center justify-center px-4 py-8 text-center">
|
||||||
<span className="text-sm text-muted-foreground">{t('chat.workStatus.sections.allHidden')}</span>
|
<span className="text-sm text-muted-foreground">{t('chat.workStatus.sections.allHidden')}</span>
|
||||||
<button
|
<Button
|
||||||
type="button"
|
variant="link"
|
||||||
|
size="xs"
|
||||||
onClick={() => setSectionsDialogOpen(true)}
|
onClick={() => setSectionsDialogOpen(true)}
|
||||||
className="mt-2 text-xs text-muted-foreground underline underline-offset-2 transition-colors hover:text-foreground"
|
className="mt-2 normal-case text-muted-foreground hover:text-foreground"
|
||||||
>
|
>
|
||||||
{t('chat.workStatus.sections.open')}
|
{t('chat.workStatus.sections.open')}
|
||||||
</button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import React from 'react';
|
|||||||
import { useI18n } from '@/lib/i18n';
|
import { useI18n } from '@/lib/i18n';
|
||||||
import { useUIStore } from '@/stores/useUIStore';
|
import { useUIStore } from '@/stores/useUIStore';
|
||||||
import { SettingsCheckboxRow } from '@/components/sections/shared/SettingsSection';
|
import { SettingsCheckboxRow } from '@/components/sections/shared/SettingsSection';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
@@ -63,13 +64,14 @@ export const WorkStatusSectionsDialog: React.FC<{
|
|||||||
{noneVisible ? (
|
{noneVisible ? (
|
||||||
<span className="text-xs text-destructive">{t('chat.workStatus.sections.noneWarning')}</span>
|
<span className="text-xs text-destructive">{t('chat.workStatus.sections.noneWarning')}</span>
|
||||||
) : <span />}
|
) : <span />}
|
||||||
<button
|
<Button
|
||||||
type="button"
|
variant="link"
|
||||||
|
size="xs"
|
||||||
onClick={handleShowAll}
|
onClick={handleShowAll}
|
||||||
className="text-xs text-muted-foreground underline underline-offset-2 transition-colors hover:text-foreground"
|
className="normal-case text-muted-foreground hover:text-foreground"
|
||||||
>
|
>
|
||||||
{t('chat.workStatus.sections.showAll')}
|
{t('chat.workStatus.sections.showAll')}
|
||||||
</button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import {
|
|||||||
WORK_STATUS_SECTION_IDS,
|
WORK_STATUS_SECTION_IDS,
|
||||||
WORK_STATUS_SECTION_LABEL_KEYS,
|
WORK_STATUS_SECTION_LABEL_KEYS,
|
||||||
areAllWorkStatusSectionsHidden,
|
areAllWorkStatusSectionsHidden,
|
||||||
|
getWorkStatusPanelPresentation,
|
||||||
isWorkStatusSectionVisible,
|
isWorkStatusSectionVisible,
|
||||||
sanitizeWorkStatusHiddenSections,
|
sanitizeWorkStatusHiddenSections,
|
||||||
} from './sections';
|
} from './sections';
|
||||||
@@ -62,6 +63,44 @@ describe('areAllWorkStatusSectionsHidden', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('getWorkStatusPanelPresentation', () => {
|
||||||
|
test('keeps a visible all-hidden panel interactive and renders its recovery state', () => {
|
||||||
|
expect(getWorkStatusPanelPresentation({
|
||||||
|
visible: true,
|
||||||
|
contentMounted: true,
|
||||||
|
renderedSections: 0,
|
||||||
|
allSectionsHidden: true,
|
||||||
|
})).toEqual({ interactive: true, showEmptyState: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
test('covers the optimistic fresh-mount count when all sections are hidden', () => {
|
||||||
|
expect(getWorkStatusPanelPresentation({
|
||||||
|
visible: true,
|
||||||
|
contentMounted: true,
|
||||||
|
renderedSections: 1,
|
||||||
|
allSectionsHidden: true,
|
||||||
|
})).toEqual({ interactive: true, showEmptyState: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
test('preserves collapse when no section has data but sections remain enabled', () => {
|
||||||
|
expect(getWorkStatusPanelPresentation({
|
||||||
|
visible: true,
|
||||||
|
contentMounted: true,
|
||||||
|
renderedSections: 0,
|
||||||
|
allSectionsHidden: false,
|
||||||
|
})).toEqual({ interactive: false, showEmptyState: false });
|
||||||
|
});
|
||||||
|
|
||||||
|
test('does not expose controls or the empty state during a hidden collapse', () => {
|
||||||
|
expect(getWorkStatusPanelPresentation({
|
||||||
|
visible: false,
|
||||||
|
contentMounted: false,
|
||||||
|
renderedSections: 0,
|
||||||
|
allSectionsHidden: true,
|
||||||
|
})).toEqual({ interactive: false, showEmptyState: false });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('sanitizeWorkStatusHiddenSections', () => {
|
describe('sanitizeWorkStatusHiddenSections', () => {
|
||||||
test('keeps known ids and drops everything else', () => {
|
test('keeps known ids and drops everything else', () => {
|
||||||
expect(sanitizeWorkStatusHiddenSections(['usage', 'nope', 42, null, 'tasks']))
|
expect(sanitizeWorkStatusHiddenSections(['usage', 'nope', 42, null, 'tasks']))
|
||||||
|
|||||||
@@ -60,6 +60,21 @@ export const areAllWorkStatusSectionsHidden = (
|
|||||||
): boolean =>
|
): boolean =>
|
||||||
hidden != null && WORK_STATUS_SECTION_IDS.every((id) => hidden.includes(id));
|
hidden != null && WORK_STATUS_SECTION_IDS.every((id) => hidden.includes(id));
|
||||||
|
|
||||||
|
export const getWorkStatusPanelPresentation = ({
|
||||||
|
visible,
|
||||||
|
contentMounted,
|
||||||
|
renderedSections,
|
||||||
|
allSectionsHidden,
|
||||||
|
}: {
|
||||||
|
visible: boolean;
|
||||||
|
contentMounted: boolean;
|
||||||
|
renderedSections: number;
|
||||||
|
allSectionsHidden: boolean;
|
||||||
|
}): { interactive: boolean; showEmptyState: boolean } => ({
|
||||||
|
interactive: visible && (renderedSections > 0 || allSectionsHidden),
|
||||||
|
showEmptyState: contentMounted && allSectionsHidden,
|
||||||
|
});
|
||||||
|
|
||||||
export const sanitizeWorkStatusHiddenSections = (value: unknown): WorkStatusSectionId[] => {
|
export const sanitizeWorkStatusHiddenSections = (value: unknown): WorkStatusSectionId[] => {
|
||||||
if (!Array.isArray(value)) return [];
|
if (!Array.isArray(value)) return [];
|
||||||
const seen = new Set<WorkStatusSectionId>();
|
const seen = new Set<WorkStatusSectionId>();
|
||||||
|
|||||||
Reference in New Issue
Block a user