fix(tabs): hide a case box the sidebar filter emptied (#580)

Since the sidebar filter and the rail search share _applyTabListFilter(),
the sidebar's filter box also marks a .tab-cluster it emptied in the
by-case tab layout and rewrites its count. But the only rule hiding
.tab-cluster.tab-filtered-out was the rail-scoped one, so in the sidebar
the emptied box stayed painted with its label and a count of 0 while its
rows were hidden.

The sidebar rule now hides the marked case box as well. It stays scoped
to html[data-session-list="sidebar"], so a leaked class still cannot hide
anything on the header strip. An alerted row still counts toward its box
(the owner's call on #580), so a box holding a row that needs the user
never hides.

Tests: a unit test renders the sidebar in the by-case layout and checks
the emptied box is marked with a 0 count, that an alerted row keeps its
box on screen and counted, and that clearing restores the totals; a
stylesheet check pins the scoped selector; the browser test measures the
shipped styles.css in Chromium: the marked box is not painted in the
sidebar layout, an unmarked one is, and the header layout hides nothing.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Codeman maintainer committed 2026-10-10 04:08:07 +02:00
1 parent 5f373a9324
commit e589004926
3 files changed
+103 -8

No files matched your search

+50
View File
@@ -530,6 +530,56 @@ describe('the sidebar filter keeps its own matching', () => {
expect(visibleRows()).toEqual(['roadmap']);
expect(emptyNote().hidden).toBe(true);
});
it('marks a case box the sidebar filter emptied, unless an alerted row keeps it', () => {
const app = makeApp({ tabLayout: null });
document.documentElement.setAttribute('data-tab-orientation', 'horizontal');
document.documentElement.dataset.tabArrangement = 'case';
app.isSessionSidebarActive = () => true;
// alpha and notes share a folder, so they share a box.
app.sessions.get('notes').workingDir = '/srv/alpha';
const box = (key: string) => document.querySelector<HTMLElement>(`.tab-cluster[data-cluster-key="${key}"]`)!;
try {
app._fullRenderSessionTabs();
expect(box('/srv/alpha').querySelector('.tab-cluster-count')?.textContent).toBe('2');
app.applySidebarFilter('notes');
expect(visibleRows()).toEqual(['notes']);
expect(box('/srv/alpha').classList.contains('tab-filtered-out')).toBe(false);
expect(box('/srv/alpha').querySelector('.tab-cluster-count')?.textContent).toBe('1');
// The emptied box is marked (styles.css hides it in the sidebar too).
expect(box('/srv/api-plans').classList.contains('tab-filtered-out')).toBe(true);
expect(box('/srv/api-plans').querySelector('.tab-cluster-count')?.textContent).toBe('0');
// A row that needs the user keeps its box on screen, counted.
app.tabAlerts.set('roadmap', 'action');
app._applyTabListFilter();
// DOM order is box order: the /srv/alpha box (alpha, notes) comes first.
expect(visibleRows()).toEqual(['notes', 'roadmap']);
expect(box('/srv/api-plans').classList.contains('tab-filtered-out')).toBe(false);
expect(box('/srv/api-plans').querySelector('.tab-cluster-count')?.textContent).toBe('1');
app.applySidebarFilter('');
expect(document.querySelectorAll('.tab-cluster.tab-filtered-out')).toHaveLength(0);
expect(box('/srv/alpha').querySelector('.tab-cluster-count')?.textContent).toBe('2');
} finally {
delete document.documentElement.dataset.tabArrangement;
}
});
it('hides an emptied case box in the sidebar stylesheet, scoped to the sidebar layout', () => {
const css = read('styles.css');
const rule = /([^{}]+)\{\s*display:\s*none\s*!important;\s*\}/g;
const selectorsHiding = [...css.matchAll(rule)].flatMap((match) =>
match[1]
.replace(/\/\*[\s\S]*?\*\//g, '')
.split(',')
.map((selector) => selector.trim())
);
expect(selectorsHiding).toContain('html[data-session-list="sidebar"] .tab-cluster.tab-filtered-out');
// Never unscoped: a leaked class must not hide a box on the header strip.
expect(selectorsHiding).not.toContain('.tab-cluster.tab-filtered-out');
});
});
describe('Escape in the search box', () => {