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