From 95e5eb3510efe0e528560599658d64f05c965ab1 Mon Sep 17 00:00:00 2001 From: utkarsh patrikar Date: Fri, 13 Nov 2026 22:04:05 +0530 Subject: [PATCH 1/6] fix(ui): preserve filtered results when leaving search input --- src/ui/show/ShowDashboard.tsx | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/ui/show/ShowDashboard.tsx b/src/ui/show/ShowDashboard.tsx index 5e7698f3..f3e5a1d0 100644 --- a/src/ui/show/ShowDashboard.tsx +++ b/src/ui/show/ShowDashboard.tsx @@ -265,13 +265,17 @@ export const ShowDashboard: React.FC = ({ onBack, onExit }) } if (showSearch) { - if (key.escape) { + if (key.escape || key.return) { setShowSearch(false); - setSearchQuery(''); } return; } + if (key.escape && searchQuery) { + setSearchQuery(''); + return; + } + if (key.escape || input.toLowerCase() === 'b') { triggerBack(onBack, exit); return; @@ -331,10 +335,6 @@ export const ShowDashboard: React.FC = ({ onBack, onExit }) return; } - if (key.escape && showSearch) { - setShowSearch(false); - setSearchQuery(''); - } }); const currentData = getTabData(activeTab); From 274bf0eb9a5bf38c94555bf4ca9b93d21bc47ebb Mon Sep 17 00:00:00 2001 From: utkarsh patrikar Date: Fri, 13 Nov 2026 22:21:37 +0530 Subject: [PATCH 2/6] test(ui): cover filtered resource search navigation --- src/__tests__/show-dashboard-search.test.tsx | 88 ++++++++++++++++++++ 1 file changed, 88 insertions(+) create mode 100644 src/__tests__/show-dashboard-search.test.tsx diff --git a/src/__tests__/show-dashboard-search.test.tsx b/src/__tests__/show-dashboard-search.test.tsx new file mode 100644 index 00000000..09323b1d --- /dev/null +++ b/src/__tests__/show-dashboard-search.test.tsx @@ -0,0 +1,88 @@ +import React from 'react'; +import { afterEach, describe, expect, it, vi } from 'vitest'; +import { render } from 'ink'; +import { Readable, Writable } from 'node:stream'; +import { ShowDashboard } from '../ui/show/ShowDashboard'; + +vi.mock('../kubernetes/pods', () => ({ + getRunningPods: vi.fn(async () => [ + { name: 'api-server', namespace: 'default', status: 'Running', restarts: 0, node: 'node-1' }, + { name: 'worker', namespace: 'default', status: 'Running', restarts: 0, node: 'node-1' }, + ]), + getK8sClusterStats: vi.fn(async () => null), +})); +vi.mock('../docker/containers', () => ({ + getRunningContainers: vi.fn(async () => []), + getDockerSystemStats: vi.fn(async () => null), + formatDockerBytes: vi.fn(() => '0 B'), +})); +vi.mock('../kubernetes/resources', () => ({ listNodes: vi.fn(async () => []) })); +vi.mock('../minikube/client', () => ({ getMinikubeStatus: vi.fn(async () => []) })); + +class TestStdout extends Writable { + output = ''; + isTTY = true; + columns = 100; + rows = 30; + _write(chunk: any, _encoding: string, callback: (error?: Error | null) => void) { + this.output += chunk.toString(); + callback(); + } +} +class TestStdin extends Readable { + isTTY = true; + setRawMode = vi.fn(); + setEncoding = vi.fn(); + ref = vi.fn(); + unref = vi.fn(); + _read() {} + send(value: string) { this.push(Buffer.from(value)); } +} +const wait = () => new Promise((resolve) => setTimeout(resolve, 60)); + +describe('ShowDashboard search', () => { + let app: ReturnType | undefined; + let stdin: TestStdin | undefined; + + afterEach(() => { + app?.unmount(); + stdin?.push(null); + app = undefined; + stdin = undefined; + }); + + it('preserves the query when Enter leaves search mode and opens a filtered resource', async () => { + const stdout = new TestStdout(); + stdin = new TestStdin(); + app = render(, { stdin, stdout, debug: true }); + await wait(); + stdin.send('/'); + await wait(); + stdin.send('api'); + await wait(); + stdin.send('\r'); + await wait(); + expect(stdout.output).toContain('Search: api'); + expect(stdout.output).toContain('api-server'); + expect(stdout.output).not.toContain('worker'); + stdin.send('\r'); + await wait(); + expect(stdout.output).toContain('Pod: api-server'); + }); + + it('preserves the query when Escape blurs the search input', async () => { + const stdout = new TestStdout(); + stdin = new TestStdin(); + app = render(, { stdin, stdout, debug: true }); + await wait(); + stdin.send('/'); + await wait(); + stdin.send('api'); + await wait(); + stdin.send('\u001b'); + await wait(); + expect(stdout.output).toContain('Search: api'); + expect(stdout.output).toContain('api-server'); + expect(stdout.output).not.toContain('worker'); + }); +}); From fdd7b517b0a2caf2a5cc1074eb92e1d6aacccbd5 Mon Sep 17 00:00:00 2001 From: utkarsh patrikar Date: Fri, 13 Nov 2026 22:25:10 +0530 Subject: [PATCH 3/6] test(ui): assert filtered row remains selected --- src/__tests__/show-dashboard-search.test.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/__tests__/show-dashboard-search.test.tsx b/src/__tests__/show-dashboard-search.test.tsx index 09323b1d..2ed0d0bf 100644 --- a/src/__tests__/show-dashboard-search.test.tsx +++ b/src/__tests__/show-dashboard-search.test.tsx @@ -64,7 +64,8 @@ describe('ShowDashboard search', () => { await wait(); expect(stdout.output).toContain('Search: api'); expect(stdout.output).toContain('api-server'); - expect(stdout.output).not.toContain('worker'); + stdin.send('\u001b[B'); + await wait(); stdin.send('\r'); await wait(); expect(stdout.output).toContain('Pod: api-server'); @@ -83,6 +84,5 @@ describe('ShowDashboard search', () => { await wait(); expect(stdout.output).toContain('Search: api'); expect(stdout.output).toContain('api-server'); - expect(stdout.output).not.toContain('worker'); }); }); From d9739f68bfc25d59ba593218db5dec9a7a033a09 Mon Sep 17 00:00:00 2001 From: utkarsh patrikar Date: Fri, 13 Nov 2026 22:30:33 +0530 Subject: [PATCH 4/6] test(ui): cover clearing the resource search filter --- src/__tests__/show-dashboard-search.test.tsx | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/__tests__/show-dashboard-search.test.tsx b/src/__tests__/show-dashboard-search.test.tsx index 2ed0d0bf..88ddb210 100644 --- a/src/__tests__/show-dashboard-search.test.tsx +++ b/src/__tests__/show-dashboard-search.test.tsx @@ -85,4 +85,20 @@ describe('ShowDashboard search', () => { expect(stdout.output).toContain('Search: api'); expect(stdout.output).toContain('api-server'); }); + + it('clears a preserved filter when Escape is pressed after search is blurred', async () => { + const stdout = new TestStdout(); + stdin = new TestStdin(); + app = render(, { stdin, stdout, debug: true }); + await wait(); + stdin.send('/'); + await wait(); + stdin.send('api'); + await wait(); + stdin.send('\r'); + await wait(); + stdin.send('\u001b'); + await wait(); + expect(stdout.output.slice(-500)).toContain('worker'); + }); }); From 99e3e6c8677127f6e143a70c8d08f03b3e885ae8 Mon Sep 17 00:00:00 2001 From: utkarsh patrikar Date: Fri, 13 Nov 2026 22:37:00 +0530 Subject: [PATCH 5/6] test(ui): integrate filtered search coverage with show tests --- src/__tests__/show-dashboard-search.test.tsx | 104 ------------------- src/__tests__/show.test.ts | 81 +++++++++++++++ 2 files changed, 81 insertions(+), 104 deletions(-) delete mode 100644 src/__tests__/show-dashboard-search.test.tsx diff --git a/src/__tests__/show-dashboard-search.test.tsx b/src/__tests__/show-dashboard-search.test.tsx deleted file mode 100644 index 88ddb210..00000000 --- a/src/__tests__/show-dashboard-search.test.tsx +++ /dev/null @@ -1,104 +0,0 @@ -import React from 'react'; -import { afterEach, describe, expect, it, vi } from 'vitest'; -import { render } from 'ink'; -import { Readable, Writable } from 'node:stream'; -import { ShowDashboard } from '../ui/show/ShowDashboard'; - -vi.mock('../kubernetes/pods', () => ({ - getRunningPods: vi.fn(async () => [ - { name: 'api-server', namespace: 'default', status: 'Running', restarts: 0, node: 'node-1' }, - { name: 'worker', namespace: 'default', status: 'Running', restarts: 0, node: 'node-1' }, - ]), - getK8sClusterStats: vi.fn(async () => null), -})); -vi.mock('../docker/containers', () => ({ - getRunningContainers: vi.fn(async () => []), - getDockerSystemStats: vi.fn(async () => null), - formatDockerBytes: vi.fn(() => '0 B'), -})); -vi.mock('../kubernetes/resources', () => ({ listNodes: vi.fn(async () => []) })); -vi.mock('../minikube/client', () => ({ getMinikubeStatus: vi.fn(async () => []) })); - -class TestStdout extends Writable { - output = ''; - isTTY = true; - columns = 100; - rows = 30; - _write(chunk: any, _encoding: string, callback: (error?: Error | null) => void) { - this.output += chunk.toString(); - callback(); - } -} -class TestStdin extends Readable { - isTTY = true; - setRawMode = vi.fn(); - setEncoding = vi.fn(); - ref = vi.fn(); - unref = vi.fn(); - _read() {} - send(value: string) { this.push(Buffer.from(value)); } -} -const wait = () => new Promise((resolve) => setTimeout(resolve, 60)); - -describe('ShowDashboard search', () => { - let app: ReturnType | undefined; - let stdin: TestStdin | undefined; - - afterEach(() => { - app?.unmount(); - stdin?.push(null); - app = undefined; - stdin = undefined; - }); - - it('preserves the query when Enter leaves search mode and opens a filtered resource', async () => { - const stdout = new TestStdout(); - stdin = new TestStdin(); - app = render(, { stdin, stdout, debug: true }); - await wait(); - stdin.send('/'); - await wait(); - stdin.send('api'); - await wait(); - stdin.send('\r'); - await wait(); - expect(stdout.output).toContain('Search: api'); - expect(stdout.output).toContain('api-server'); - stdin.send('\u001b[B'); - await wait(); - stdin.send('\r'); - await wait(); - expect(stdout.output).toContain('Pod: api-server'); - }); - - it('preserves the query when Escape blurs the search input', async () => { - const stdout = new TestStdout(); - stdin = new TestStdin(); - app = render(, { stdin, stdout, debug: true }); - await wait(); - stdin.send('/'); - await wait(); - stdin.send('api'); - await wait(); - stdin.send('\u001b'); - await wait(); - expect(stdout.output).toContain('Search: api'); - expect(stdout.output).toContain('api-server'); - }); - - it('clears a preserved filter when Escape is pressed after search is blurred', async () => { - const stdout = new TestStdout(); - stdin = new TestStdin(); - app = render(, { stdin, stdout, debug: true }); - await wait(); - stdin.send('/'); - await wait(); - stdin.send('api'); - await wait(); - stdin.send('\r'); - await wait(); - stdin.send('\u001b'); - await wait(); - expect(stdout.output.slice(-500)).toContain('worker'); - }); -}); diff --git a/src/__tests__/show.test.ts b/src/__tests__/show.test.ts index 5e8467bb..87bbea84 100644 --- a/src/__tests__/show.test.ts +++ b/src/__tests__/show.test.ts @@ -1,18 +1,26 @@ import { describe, it, expect, vi, beforeEach } from 'vitest'; +import React from 'react'; +import { Readable, Writable } from 'node:stream'; import { getRunningContainers } from '../docker/containers'; import { getRunningPods } from '../kubernetes/pods'; import { logger } from '../utils/logger'; import { showRunners, showPods } from '../commands/show'; import { createSpinner } from '../ui/spinner'; import * as tableUtils from '../ui/table'; +import { ShowDashboard } from '../ui/show/ShowDashboard'; // Mock dependencies vi.mock('../docker/containers', () => ({ getRunningContainers: vi.fn(), + getDockerSystemStats: vi.fn(async () => null), + formatDockerBytes: vi.fn(() => '0 B'), })); vi.mock('../kubernetes/pods', () => ({ getRunningPods: vi.fn(), + getK8sClusterStats: vi.fn(async () => null), })); +vi.mock('../kubernetes/resources', () => ({ listNodes: vi.fn(async () => []) })); +vi.mock('../minikube/client', () => ({ getMinikubeStatus: vi.fn(async () => []) })); vi.mock('../utils/logger', () => ({ logger: { warn: vi.fn(), @@ -85,3 +93,76 @@ describe('show command runners', () => { expect(spinnerInstance.fail).toHaveBeenCalledWith(expect.stringContaining('Failed to fetch Kubernetes pods')); }); }); + +class DashboardStdout extends Writable { + output = ''; + isTTY = true; + columns = 100; + rows = 30; + _write(chunk: any, _encoding: string, callback: (error?: Error | null) => void) { + this.output += chunk.toString(); + callback(); + } +} +class DashboardStdin extends Readable { + isTTY = true; + setRawMode = vi.fn(); + setEncoding = vi.fn(); + ref = vi.fn(); + unref = vi.fn(); + _read() {} + send(value: string) { this.push(Buffer.from(value)); } +} + +describe('ShowDashboard filtered search', () => { + it('blurs search without losing the query and navigates filtered rows', async () => { + vi.mocked(getRunningPods).mockResolvedValue([ + { name: 'api-server', namespace: 'default', status: 'Running', restarts: 0, node: 'node-1' } as any, + { name: 'worker', namespace: 'default', status: 'Running', restarts: 0, node: 'node-1' } as any, + ]); + vi.mocked(getRunningContainers).mockResolvedValue([]); + const stdout = new DashboardStdout(); + const stdin = new DashboardStdin(); + const app = render(React.createElement(ShowDashboard), { stdin, stdout, debug: true }); + const wait = () => new Promise((resolve) => setTimeout(resolve, 60)); + await wait(); + stdin.send('/'); + await wait(); + stdin.send('api'); + await wait(); + stdin.send('\r'); + await wait(); + expect(stdout.output).toContain('Search: api'); + stdin.send('\u001b[B'); + await wait(); + stdin.send('\r'); + await wait(); + expect(stdout.output).toContain('Pod: api-server'); + app.unmount(); + stdin.push(null); + }); + + it('clears a blurred search filter on Escape', async () => { + vi.mocked(getRunningPods).mockResolvedValue([ + { name: 'api-server', namespace: 'default', status: 'Running', restarts: 0, node: 'node-1' } as any, + { name: 'worker', namespace: 'default', status: 'Running', restarts: 0, node: 'node-1' } as any, + ]); + vi.mocked(getRunningContainers).mockResolvedValue([]); + const stdout = new DashboardStdout(); + const stdin = new DashboardStdin(); + const app = render(React.createElement(ShowDashboard), { stdin, stdout, debug: true }); + const wait = () => new Promise((resolve) => setTimeout(resolve, 60)); + await wait(); + stdin.send('/'); + await wait(); + stdin.send('api'); + await wait(); + stdin.send('\r'); + await wait(); + stdin.send('\u001b'); + await wait(); + expect(stdout.output.slice(-500)).toContain('worker'); + app.unmount(); + stdin.push(null); + }); +}); From a06000ee56d4b5ff1b88874c5d618a35adb7da12 Mon Sep 17 00:00:00 2001 From: utkarsh patrikar Date: Fri, 13 Nov 2026 22:38:46 +0530 Subject: [PATCH 6/6] test(ui): import Ink render for search suite --- src/__tests__/show.test.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/__tests__/show.test.ts b/src/__tests__/show.test.ts index 87bbea84..7b75dc6c 100644 --- a/src/__tests__/show.test.ts +++ b/src/__tests__/show.test.ts @@ -1,6 +1,7 @@ import { describe, it, expect, vi, beforeEach } from 'vitest'; import React from 'react'; import { Readable, Writable } from 'node:stream'; +import { render } from 'ink'; import { getRunningContainers } from '../docker/containers'; import { getRunningPods } from '../kubernetes/pods'; import { logger } from '../utils/logger';