From b8e60dfa4421698ff2e6daa1e4d774ded4c342c7 Mon Sep 17 00:00:00 2001 From: axel7083 <42176370+axel7083@users.noreply.github.com> Date: Wed, 15 Jan 2025 10:48:15 +0100 Subject: [PATCH] feat: adding search controls terminal window (#10612) * feat: adding search controls terminal window Signed-off-by: axel7083 <42176370+axel7083@users.noreply.github.com> * fix: container details tests Signed-off-by: axel7083 <42176370+axel7083@users.noreply.github.com> * fix: pnpm lock Signed-off-by: axel7083 <42176370+axel7083@users.noreply.github.com> * fix: better typing for search addon Signed-off-by: axel7083 <42176370+axel7083@users.noreply.github.com> --------- Signed-off-by: axel7083 <42176370+axel7083@users.noreply.github.com> --- packages/renderer/package.json | 1 + .../lib/container/ContainerDetails.spec.ts | 9 +- .../container/ContainerDetailsLogs.spec.ts | 2 + .../lib/container/ContainerDetailsLogs.svelte | 2 +- .../src/lib/ui/TerminalSearchControls.spec.ts | 159 ++++++++++++++++++ .../src/lib/ui/TerminalSearchControls.svelte | 83 +++++++++ .../renderer/src/lib/ui/TerminalWindow.svelte | 6 + .../src/lib/ui/TerminalWindows.spec.ts | 15 ++ pnpm-lock.yaml | 16 +- 9 files changed, 283 insertions(+), 10 deletions(-) create mode 100644 packages/renderer/src/lib/ui/TerminalSearchControls.spec.ts create mode 100644 packages/renderer/src/lib/ui/TerminalSearchControls.svelte diff --git a/packages/renderer/package.json b/packages/renderer/package.json index 0f70bc33ce7a9..016e994e522e2 100644 --- a/packages/renderer/package.json +++ b/packages/renderer/package.json @@ -49,6 +49,7 @@ "vitest": "^2.1.6", "@xterm/xterm": "^5.5.0", "@xterm/addon-fit": "^0.10.0", + "@xterm/addon-search": "^0.15.0", "vitest-canvas-mock": "^0.3.3", "yaml": "^2.7.0" }, diff --git a/packages/renderer/src/lib/container/ContainerDetails.spec.ts b/packages/renderer/src/lib/container/ContainerDetails.spec.ts index 367cbaa25db18..a75f5fa9121fd 100644 --- a/packages/renderer/src/lib/container/ContainerDetails.spec.ts +++ b/packages/renderer/src/lib/container/ContainerDetails.spec.ts @@ -55,13 +55,8 @@ const myContainer: ContainerInfo = { const deleteContainerMock = vi.fn(); const getContributedMenusMock = vi.fn(); -vi.mock('@xterm/xterm', () => { - return { - Terminal: vi - .fn() - .mockReturnValue({ loadAddon: vi.fn(), open: vi.fn(), write: vi.fn(), clear: vi.fn(), dispose: vi.fn() }), - }; -}); +vi.mock('@xterm/xterm'); +vi.mock('@xterm/addon-search'); const getConfigurationValueMock = vi.fn().mockReturnValue(12); diff --git a/packages/renderer/src/lib/container/ContainerDetailsLogs.spec.ts b/packages/renderer/src/lib/container/ContainerDetailsLogs.spec.ts index 8a4b076d7bbeb..1926675a073e6 100644 --- a/packages/renderer/src/lib/container/ContainerDetailsLogs.spec.ts +++ b/packages/renderer/src/lib/container/ContainerDetailsLogs.spec.ts @@ -25,6 +25,8 @@ import { beforeAll, expect, test, vi } from 'vitest'; import ContainerDetailsLogs from './ContainerDetailsLogs.svelte'; import type { ContainerInfoUI } from './ContainerInfoUI'; +vi.mock('@xterm/addon-search'); + vi.mock('@xterm/xterm', () => { const writeMock = vi.fn(); return { diff --git a/packages/renderer/src/lib/container/ContainerDetailsLogs.svelte b/packages/renderer/src/lib/container/ContainerDetailsLogs.svelte index 21e84e9f1df76..defaa97d4a0c8 100644 --- a/packages/renderer/src/lib/container/ContainerDetailsLogs.svelte +++ b/packages/renderer/src/lib/container/ContainerDetailsLogs.svelte @@ -88,5 +88,5 @@ onDestroy(() => { class:h-0={noLogs === true} class:h-full={noLogs === false} bind:this={terminalParentDiv}> - + diff --git a/packages/renderer/src/lib/ui/TerminalSearchControls.spec.ts b/packages/renderer/src/lib/ui/TerminalSearchControls.spec.ts new file mode 100644 index 0000000000000..70e09d029cef5 --- /dev/null +++ b/packages/renderer/src/lib/ui/TerminalSearchControls.spec.ts @@ -0,0 +1,159 @@ +/********************************************************************** + * Copyright (C) 2025 Red Hat, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + ***********************************************************************/ + +import '@testing-library/jest-dom/vitest'; + +import { fireEvent, render } from '@testing-library/svelte'; +import userEvent from '@testing-library/user-event'; +import { SearchAddon } from '@xterm/addon-search'; +import type { Terminal } from '@xterm/xterm'; +import { beforeEach, expect, test, vi } from 'vitest'; + +import TerminalSearchControls from './TerminalSearchControls.svelte'; + +vi.mock('@xterm/addon-search'); + +const TerminalMock: Terminal = { + onWriteParsed: vi.fn(), + onResize: vi.fn(), + dispose: vi.fn(), +} as unknown as Terminal; + +beforeEach(() => { + vi.resetAllMocks(); +}); + +test('search addon should be loaded to the terminal', () => { + render(TerminalSearchControls, { + terminal: TerminalMock, + }); + + expect(SearchAddon.prototype.activate).toHaveBeenCalledOnce(); + expect(SearchAddon.prototype.activate).toHaveBeenCalledWith(TerminalMock); +}); + +test('search addon should be disposed on component destroy', async () => { + const { unmount } = render(TerminalSearchControls, { + terminal: TerminalMock, + }); + + unmount(); + + await vi.waitFor(() => { + expect(SearchAddon.prototype.dispose).toHaveBeenCalledOnce(); + }); +}); + +test('input should call findNext on search addon', async () => { + const user = userEvent.setup(); + const { getByRole } = render(TerminalSearchControls, { + terminal: TerminalMock, + }); + + const searchTextbox = getByRole('textbox', { + name: 'Find', + }); + + expect(searchTextbox).toBeInTheDocument(); + await user.type(searchTextbox, 'hello'); + + await vi.waitFor(() => { + expect(SearchAddon.prototype.findNext).toHaveBeenCalledWith('hello', { + incremental: false, + }); + }); +}); + +test('key Enter should call findNext with incremental', async () => { + const user = userEvent.setup(); + const { getByRole } = render(TerminalSearchControls, { + terminal: TerminalMock, + }); + + const searchTextbox = getByRole('textbox', { + name: 'Find', + }); + + expect(searchTextbox).toBeInTheDocument(); + await user.type(searchTextbox, 'hello{Enter}'); + + await vi.waitFor(() => { + expect(SearchAddon.prototype.findNext).toHaveBeenCalledWith('hello', { + incremental: true, + }); + }); +}); + +test('arrow down should call findNext', async () => { + const { getByRole } = render(TerminalSearchControls, { + terminal: TerminalMock, + }); + + const upBtn = getByRole('button', { + name: 'Next Match', + }); + + expect(upBtn).toBeInTheDocument(); + await fireEvent.click(upBtn); + + await vi.waitFor(() => { + expect(SearchAddon.prototype.findNext).toHaveBeenCalledWith('', { + incremental: true, + }); + }); +}); + +test('arrow up should call findPrevious', async () => { + const { getByRole } = render(TerminalSearchControls, { + terminal: TerminalMock, + }); + + const upBtn = getByRole('button', { + name: 'Previous Match', + }); + + expect(upBtn).toBeInTheDocument(); + await fireEvent.click(upBtn); + + await vi.waitFor(() => { + expect(SearchAddon.prototype.findPrevious).toHaveBeenCalledWith('', { + incremental: true, + }); + }); +}); + +test('ctrl+F should focus input', async () => { + const { getByRole, container } = render(TerminalSearchControls, { + terminal: TerminalMock, + }); + + const searchTextbox: HTMLInputElement = getByRole('textbox', { + name: 'Find', + }) as HTMLInputElement; + + const focusSpy = vi.spyOn(searchTextbox, 'focus'); + + await fireEvent.keyUp(container, { + ctrlKey: true, + key: 'f', + }); + + await vi.waitFor(() => { + expect(focusSpy).toHaveBeenCalled(); + }); +}); diff --git a/packages/renderer/src/lib/ui/TerminalSearchControls.svelte b/packages/renderer/src/lib/ui/TerminalSearchControls.svelte new file mode 100644 index 0000000000000..f58b5dd838c66 --- /dev/null +++ b/packages/renderer/src/lib/ui/TerminalSearchControls.svelte @@ -0,0 +1,83 @@ + + + +
+
+ +
+
+ + +
+
diff --git a/packages/renderer/src/lib/ui/TerminalWindow.svelte b/packages/renderer/src/lib/ui/TerminalWindow.svelte index b8b66d0d0771c..17b5b34dedb1c 100644 --- a/packages/renderer/src/lib/ui/TerminalWindow.svelte +++ b/packages/renderer/src/lib/ui/TerminalWindow.svelte @@ -5,6 +5,8 @@ import { FitAddon } from '@xterm/addon-fit'; import { Terminal } from '@xterm/xterm'; import { createEventDispatcher, onDestroy, onMount } from 'svelte'; +import TerminalSearchControls from '/@/lib/ui/TerminalSearchControls.svelte'; + import { TerminalSettings } from '../../../../main/src/plugin/terminal-settings'; import { getTerminalTheme } from '../../../../main/src/plugin/terminal-theme'; @@ -13,6 +15,7 @@ export let convertEol: boolean | undefined = undefined; export let disableStdIn: boolean = true; export let screenReaderMode: boolean | undefined = undefined; export let showCursor: boolean = false; +export let search: boolean = false; let logsXtermDiv: HTMLDivElement; let resizeHandler: () => void; @@ -69,4 +72,7 @@ onDestroy(() => { }); +{#if search && terminal} + +{/if}
diff --git a/packages/renderer/src/lib/ui/TerminalWindows.spec.ts b/packages/renderer/src/lib/ui/TerminalWindows.spec.ts index a43382a457fd5..56a0561450f02 100644 --- a/packages/renderer/src/lib/ui/TerminalWindows.spec.ts +++ b/packages/renderer/src/lib/ui/TerminalWindows.spec.ts @@ -32,6 +32,8 @@ vi.mock('@xterm/xterm'); vi.mock('@xterm/addon-fit'); +vi.mock('@xterm/addon-search'); + beforeEach(() => { vi.resetAllMocks(); }); @@ -149,3 +151,16 @@ test('matchMedia resize listener should trigger fit addon', async () => { expect(FitAddon.prototype.fit).toHaveBeenCalled(); }); + +test('search props should add terminal search controls', async () => { + const { getByRole } = render(TerminalWindow, { + terminal: writable() as unknown as Terminal, + search: true, + }); + + const searchTextbox = getByRole('textbox', { + name: 'Find', + }); + + expect(searchTextbox).toBeInTheDocument(); +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9fbcd41a55a30..c4453c22ed5c4 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -615,6 +615,9 @@ importers: '@xterm/addon-fit': specifier: ^0.10.0 version: 0.10.0(@xterm/xterm@5.5.0) + '@xterm/addon-search': + specifier: ^0.15.0 + version: 0.15.0(@xterm/xterm@5.5.0) '@xterm/addon-serialize': specifier: ^0.13.0 version: 0.13.0(@xterm/xterm@5.5.0) @@ -4288,6 +4291,11 @@ packages: peerDependencies: '@xterm/xterm': ^5.0.0 + '@xterm/addon-search@0.15.0': + resolution: {integrity: sha512-ZBZKLQ+EuKE83CqCmSSz5y1tx+aNOCUaA7dm6emgOX+8J9H1FWXZyrKfzjwzV+V14TV3xToz1goIeRhXBS5qjg==} + peerDependencies: + '@xterm/xterm': ^5.0.0 + '@xterm/addon-serialize@0.13.0': resolution: {integrity: sha512-kGs8o6LWAmN1l2NpMp01/YkpxbmO4UrfWybeGu79Khw5K9+Krp7XhXbBTOTc3GJRRhd6EmILjpR8k5+odY39YQ==} peerDependencies: @@ -14024,11 +14032,11 @@ snapshots: '@isaacs/cliui@8.0.2': dependencies: - string-width: 5.1.2 + string-width: 4.2.3 string-width-cjs: string-width@4.2.3 strip-ansi: 7.1.0 strip-ansi-cjs: strip-ansi@6.0.1 - wrap-ansi: 8.1.0 + wrap-ansi: 7.0.0 wrap-ansi-cjs: wrap-ansi@7.0.0 '@isaacs/fs-minipass@4.0.1': @@ -15753,6 +15761,10 @@ snapshots: dependencies: '@xterm/xterm': 5.5.0 + '@xterm/addon-search@0.15.0(@xterm/xterm@5.5.0)': + dependencies: + '@xterm/xterm': 5.5.0 + '@xterm/addon-serialize@0.13.0(@xterm/xterm@5.5.0)': dependencies: '@xterm/xterm': 5.5.0