From c0f9df5beaa7e7282c40d72fea5bc8e1a0e0ab2c Mon Sep 17 00:00:00 2001 From: Neil <4138956+nwparker@users.noreply.github.com> Date: Tue, 27 Oct 2026 01:03:19 -0700 Subject: [PATCH] fix(ssh): keep the IME confirmation Enter from submitting the port-forward dialog (#34315) The SSH port-forward dialog renders its four fields as raw inputs inside a form with a submit button. IMEs that redispatch an unmarked Enter after compositionend therefore trigger implicit submission with the half-typed value. The pre-revert ImeEnterGuardedForm covered this form; #25480 moved the guard into shared fields but did not reach these raw inputs, and the #28609 lint rule only inspects Enter handlers, which these fields lack. Render the fields with ImeInput so the shared gesture tracker swallows the confirmation and its redispatch while ordinary Enter still submits. Co-authored-by: Seiji Kohara --- ...ssh-port-forward-dialog.ime-enter.test.tsx | 65 +++++++++++++++++++ .../right-sidebar/ssh-port-forward-dialog.tsx | 9 +-- 2 files changed, 70 insertions(+), 4 deletions(-) create mode 100644 src/renderer/src/components/right-sidebar/ssh-port-forward-dialog.ime-enter.test.tsx diff --git a/src/renderer/src/components/right-sidebar/ssh-port-forward-dialog.ime-enter.test.tsx b/src/renderer/src/components/right-sidebar/ssh-port-forward-dialog.ime-enter.test.tsx new file mode 100644 index 00000000000..c03751ba3e3 --- /dev/null +++ b/src/renderer/src/components/right-sidebar/ssh-port-forward-dialog.ime-enter.test.tsx @@ -0,0 +1,65 @@ +// @vitest-environment happy-dom +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import { act, cleanup, fireEvent, render, screen } from '@testing-library/react' +import { endComposition } from '@/lib/ime-composition-event.test-support' +import { SshPortForwardDialog } from './ssh-port-forward-dialog' + +const addPortForward = vi.fn(() => Promise.resolve()) + +beforeEach(() => { + vi.useFakeTimers() + addPortForward.mockClear() + Object.defineProperty(window, 'api', { + configurable: true, + value: { ssh: { addPortForward } } + }) +}) + +afterEach(() => { + cleanup() + vi.useRealTimers() +}) + +// happy-dom has no implicit submission, so a keydown left unprevented submits the form here. +function pressEnterWithImplicitSubmit(field: HTMLElement): boolean { + const notPrevented = fireEvent.keyDown(field, { key: 'Enter', keyCode: 13 }) + if (notPrevented) { + fireEvent.submit(field.closest('form')!) + } + return notPrevented +} + +describe('SshPortForwardDialog IME confirmation', () => { + it.each([ + { field: 'Remote Port', value: '8080', sent: { remotePort: 8080 } }, + { field: 'Local Port', value: '18080', sent: { localPort: 18080 } }, + { field: 'Remote Host', value: '開発機', sent: { remoteHost: '開発機' } }, + { field: 'Label (optional)', value: '開発', sent: { label: '開発' } } + ])('keeps the confirm redispatch in $field from submitting', async ({ field, value, sent }) => { + render( + + ) + const input = screen.getByLabelText(field) + + fireEvent.compositionStart(input) + fireEvent.keyDown(input, { key: 'Process', keyCode: 229, isComposing: true }) + fireEvent.change(input, { target: { value } }) + endComposition(input, value) + fireEvent.keyUp(input, { key: 'Process', keyCode: 229 }) + + await act(async () => { + expect(pressEnterWithImplicitSubmit(input)).toBe(false) + }) + expect(addPortForward).not.toHaveBeenCalled() + + await act(async () => { + expect(pressEnterWithImplicitSubmit(input)).toBe(true) + }) + expect(addPortForward).toHaveBeenCalledOnce() + expect(addPortForward).toHaveBeenCalledWith(expect.objectContaining(sent)) + }) +}) diff --git a/src/renderer/src/components/right-sidebar/ssh-port-forward-dialog.tsx b/src/renderer/src/components/right-sidebar/ssh-port-forward-dialog.tsx index bbd7108cc2f..24a44768cf4 100644 --- a/src/renderer/src/components/right-sidebar/ssh-port-forward-dialog.tsx +++ b/src/renderer/src/components/right-sidebar/ssh-port-forward-dialog.tsx @@ -9,6 +9,7 @@ import { import { Button } from '@/components/ui/button' import type { PortForwardEntry } from '../../../../shared/ssh-types' import { translate } from '@/i18n/i18n' +import { ImeInput } from '@/lib/ime-text-field' // Remap privileged ports to avoid EACCES when binding locally without root. function safeLocalPort(remotePort: number): number { @@ -218,7 +219,7 @@ function PortForwardForm({ {translate('auto.components.right.sidebar.PortsPanel.9e5a4118b0', 'Remote Port')} - {translate('auto.components.right.sidebar.PortsPanel.b950b1948b', 'Local Port')} - {translate('auto.components.right.sidebar.PortsPanel.a3721a50b0', 'Remote Host')} - setRemoteHost(e.target.value)} @@ -276,7 +277,7 @@ function PortForwardForm({ {translate('auto.components.right.sidebar.PortsPanel.8dfed0a15c', 'Label (optional)')} - setLabel(e.target.value)}