mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-11 09:49:41 +02:00
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:
@@ -2,12 +2,15 @@
|
||||
* @fileoverview Real-Chromium coverage for the vertical rail's session search.
|
||||
*
|
||||
* What DOM emulation cannot answer: that the shipped CSS actually hides a
|
||||
* filtered row and an emptied group, that the box shows only on the vertical
|
||||
* rail, that the inline oninput/onkeydown/onclick handlers in index.html reach
|
||||
* the app, and that a match inside a collapsed group can be clicked. The real
|
||||
* #tabRail markup is lifted from index.html, and the shipping constants.js,
|
||||
* tab-layout-browser.js, app.js, webview-tabs.js and styles.css are loaded
|
||||
* into a page.
|
||||
* filtered row and an emptied group (and, in the sidebar layout, an emptied
|
||||
* case box), that the box shows only on the vertical rail, that the inline
|
||||
* oninput/onkeydown/onclick handlers in index.html reach the app, that a match
|
||||
* inside a collapsed group can be clicked, and that Escape in the box clears
|
||||
* the search WITHOUT the global key handler (installed for real, capture
|
||||
* phase, so it runs before the box's own onkeydown) also closing every panel.
|
||||
* The real #tabRail markup is lifted from index.html, and the shipping
|
||||
* constants.js, tab-layout-browser.js, app.js, webview-tabs.js and styles.css
|
||||
* are loaded into a page.
|
||||
*
|
||||
* Port: none (page.route on a fake origin, no server).
|
||||
*/
|
||||
@@ -247,6 +250,44 @@ describe('vertical rail session search in Chromium', () => {
|
||||
expect(await page.evaluate(() => (window as any).__redraws)).toBe(1);
|
||||
});
|
||||
|
||||
it('hides a case box the sidebar filter emptied, in the sidebar layout only', async () => {
|
||||
const painted = await page.evaluate(() => {
|
||||
const root = document.documentElement;
|
||||
const probeHtml =
|
||||
'<aside class="session-sidebar" id="sidebarProbe"><div class="session-tabs tabs-clusters">' +
|
||||
'<div class="tab-cluster tab-filtered-out" data-probe="emptied">' +
|
||||
'<span class="tab-cluster-label">api <span class="tab-cluster-count">0</span></span></div>' +
|
||||
'<div class="tab-cluster" data-probe="kept">' +
|
||||
'<span class="tab-cluster-label">web <span class="tab-cluster-count">1</span></span>' +
|
||||
'<div class="session-tab">w1-web</div></div>' +
|
||||
'</div></aside>';
|
||||
const measure = () => {
|
||||
const probe = document.getElementById('sidebarProbe')!;
|
||||
return {
|
||||
emptied: probe.querySelector('[data-probe="emptied"]')!.getClientRects().length,
|
||||
kept: probe.querySelector('[data-probe="kept"]')!.getClientRects().length,
|
||||
};
|
||||
};
|
||||
root.setAttribute('data-tab-orientation', 'horizontal');
|
||||
document.body.insertAdjacentHTML('beforeend', probeHtml);
|
||||
const probe = document.getElementById('sidebarProbe')!;
|
||||
// Header layout (the box outside any sidebar, which that layout hides):
|
||||
// the rule is scoped to the sidebar, so a leaked class hides nothing.
|
||||
root.setAttribute('data-session-list', 'header');
|
||||
probe.classList.remove('session-sidebar');
|
||||
const header = measure();
|
||||
root.setAttribute('data-session-list', 'sidebar');
|
||||
probe.classList.add('session-sidebar');
|
||||
const sidebar = measure();
|
||||
document.getElementById('sidebarProbe')!.remove();
|
||||
root.removeAttribute('data-session-list');
|
||||
root.setAttribute('data-tab-orientation', 'vertical');
|
||||
return { header, sidebar };
|
||||
});
|
||||
expect(painted.sidebar).toEqual({ emptied: 0, kept: 1 });
|
||||
expect(painted.header.emptied).toBe(1);
|
||||
});
|
||||
|
||||
it('walks only the matches with the arrow keys', async () => {
|
||||
await page.getByRole('searchbox', { name: 'Search sessions' }).fill('a');
|
||||
// 'a' matches Alpha API, Roadmap, API Review and Dashboard, not Notes.
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
Reference in New Issue
Block a user