mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-12 10:19:42 +02:00
fix(tabs): Escape in the rail search box clears only the search (#580)
The global key handler in setupEventListeners() sits on document in the capture phase, so it ran before the box's inline onkeydown: the Escape that cleared the search also ran closeAllPanels() (collapsing the Monitor and Subagents panels), closeHelp() and closeSessionManager(). Calling stopPropagation() from the inline handler came too late. The global Escape branch now claims an Escape whose target is #tabRailSearch while the box holds text, the same way it already claims one for the group menu, the grouped-rail drag and the Tiles count menu, and routes it to handleTabRailSearchKeydown(). An empty box still leaves Escape to the global handler, and an Escape that cancels an IME composition is left to the IME. Tests: the unit test installs the real global listener and asserts that Escape with text in the box closes nothing, that an empty box closes as before, and that an Escape outside the box is not claimed. The browser test installs setupEventListeners() for real in Chromium, so the capture-before-inline ordering is the shipped one. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -117,6 +117,14 @@ describe('vertical rail session search in Chromium', () => {
|
||||
};
|
||||
w.__setApp(app);
|
||||
w.__app = app;
|
||||
// The real global key handler (document, capture phase). The close
|
||||
// methods it calls live in settings-ui.js / panels-ui.js, which are not
|
||||
// loaded here, so they record instead.
|
||||
app.setupColorPicker = () => undefined;
|
||||
app.closeAllPanels = () => w.__closed.push('panels');
|
||||
app.closeHelp = () => w.__closed.push('help');
|
||||
app.closeSessionManager = () => w.__closed.push('session-manager');
|
||||
app.setupEventListeners();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -144,6 +152,7 @@ describe('vertical rail session search in Chromium', () => {
|
||||
app._resetTabRailSearch();
|
||||
app._fullRenderSessionTabs();
|
||||
w.__activation = null;
|
||||
w.__closed = [];
|
||||
}, LAYOUT);
|
||||
});
|
||||
|
||||
@@ -201,6 +210,21 @@ describe('vertical rail session search in Chromium', () => {
|
||||
expect(await paintedRows()).toEqual(['alpha', 'web', 'notes']);
|
||||
});
|
||||
|
||||
it('Escape with text in the box clears it and closes nothing else', async () => {
|
||||
const search = page.getByRole('searchbox', { name: 'Search sessions' });
|
||||
await search.fill('review');
|
||||
expect(await paintedRows()).toEqual(['review']);
|
||||
|
||||
await search.press('Escape');
|
||||
expect(await search.inputValue()).toBe('');
|
||||
expect(await paintedRows()).toEqual(['alpha', 'web', 'notes']);
|
||||
expect(await page.evaluate(() => (window as any).__closed)).toEqual([]);
|
||||
|
||||
// An empty box leaves Escape to the global handler, which closes as always.
|
||||
await search.press('Escape');
|
||||
expect(await page.evaluate(() => (window as any).__closed)).toEqual(['panels', 'help', 'session-manager']);
|
||||
});
|
||||
|
||||
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.
|
||||
|
||||
@@ -532,6 +532,80 @@ describe('the sidebar filter keeps its own matching', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('Escape in the search box', () => {
|
||||
/**
|
||||
* The real global key handler from setupEventListeners(), on document in the
|
||||
* capture phase, with the close methods it calls (settings-ui.js and
|
||||
* panels-ui.js are not loaded here) as recorders. The box in this harness
|
||||
* has no inline onkeydown, so only the global handler can clear it.
|
||||
*/
|
||||
function installGlobalKeys(app: Record<string, any>) {
|
||||
const closed: string[] = [];
|
||||
app.setupColorPicker = () => {};
|
||||
app.closeAllPanels = () => closed.push('panels');
|
||||
app.closeHelp = () => closed.push('help');
|
||||
app.closeSessionManager = () => closed.push('session-manager');
|
||||
app.closeCommandPalette = () => {};
|
||||
app.closeShortcutOverlay = () => {};
|
||||
const add = vi.spyOn(document, 'addEventListener');
|
||||
app.setupEventListeners();
|
||||
const listener = add.mock.calls.find(([type]) => type === 'keydown')![1] as EventListener;
|
||||
add.mockRestore();
|
||||
return { closed, remove: () => document.removeEventListener('keydown', listener, true) };
|
||||
}
|
||||
const escape = () => new window.KeyboardEvent('keydown', { key: 'Escape', bubbles: true, cancelable: true });
|
||||
|
||||
it('clears a search and closes nothing else, though the global handler runs first', () => {
|
||||
const app = makeApp();
|
||||
app._fullRenderSessionTabs();
|
||||
const keys = installGlobalKeys(app);
|
||||
try {
|
||||
input().value = 'review';
|
||||
app.setTabRailSearch(input().value);
|
||||
input().focus();
|
||||
|
||||
const event = escape();
|
||||
input().dispatchEvent(event);
|
||||
expect(event.defaultPrevented).toBe(true);
|
||||
expect(input().value).toBe('');
|
||||
expect(app._tabRailSearch).toBe('');
|
||||
expect(visibleRows()).toEqual(['alpha', 'web', 'roadmap', 'review', 'notes']);
|
||||
expect(keys.closed).toEqual([]);
|
||||
|
||||
// The box is empty now: the next Escape is the global handler's again.
|
||||
input().dispatchEvent(escape());
|
||||
expect(keys.closed).toEqual(['panels', 'help', 'session-manager']);
|
||||
} finally {
|
||||
keys.remove();
|
||||
}
|
||||
});
|
||||
|
||||
it('only claims an Escape that lands in the box', () => {
|
||||
const app = makeApp();
|
||||
app._fullRenderSessionTabs();
|
||||
const keys = installGlobalKeys(app);
|
||||
try {
|
||||
app.setTabRailSearch('review');
|
||||
document.body.dispatchEvent(escape());
|
||||
expect(keys.closed).toEqual(['panels', 'help', 'session-manager']);
|
||||
expect(app._tabRailSearch).toBe('review');
|
||||
expect(visibleRows()).toEqual(['review']);
|
||||
} finally {
|
||||
keys.remove();
|
||||
}
|
||||
});
|
||||
|
||||
it('leaves an Escape that cancels an IME composition to the IME', () => {
|
||||
const app = makeApp();
|
||||
app._fullRenderSessionTabs();
|
||||
app.setTabRailSearch('review');
|
||||
const composing = new window.KeyboardEvent('keydown', { key: 'Escape', isComposing: true, cancelable: true });
|
||||
app.handleTabRailSearchKeydown(composing);
|
||||
expect(composing.defaultPrevented).toBe(false);
|
||||
expect(app._tabRailSearch).toBe('review');
|
||||
});
|
||||
});
|
||||
|
||||
// ─── Markup + zh-CN ───────────────────────────────────────────────────────────
|
||||
|
||||
describe('markup and zh-CN', () => {
|
||||
|
||||
Reference in New Issue
Block a user