mirror of
https://github.com/stablyai/orca.git
synced 2026-10-30 16:47:34 +00:00
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 <sage.0118@gmail.com>
This commit is contained in:
@@ -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(
|
||||
<SshPortForwardDialog
|
||||
state={{ mode: 'add', defaults: { remotePort: 3000, targetId: 'target-1' } }}
|
||||
activeConnectionId="target-1"
|
||||
onClose={vi.fn()}
|
||||
/>
|
||||
)
|
||||
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))
|
||||
})
|
||||
})
|
||||
@@ -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({
|
||||
<span className="text-[11px] text-muted-foreground">
|
||||
{translate('auto.components.right.sidebar.PortsPanel.9e5a4118b0', 'Remote Port')}
|
||||
</span>
|
||||
<input
|
||||
<ImeInput
|
||||
type="text"
|
||||
inputMode="numeric"
|
||||
value={remotePort}
|
||||
@@ -243,7 +244,7 @@ function PortForwardForm({
|
||||
<span className="text-[11px] text-muted-foreground">
|
||||
{translate('auto.components.right.sidebar.PortsPanel.b950b1948b', 'Local Port')}
|
||||
</span>
|
||||
<input
|
||||
<ImeInput
|
||||
type="text"
|
||||
inputMode="numeric"
|
||||
value={localPort}
|
||||
@@ -260,7 +261,7 @@ function PortForwardForm({
|
||||
<span className="text-[11px] text-muted-foreground">
|
||||
{translate('auto.components.right.sidebar.PortsPanel.a3721a50b0', 'Remote Host')}
|
||||
</span>
|
||||
<input
|
||||
<ImeInput
|
||||
type="text"
|
||||
value={remoteHost}
|
||||
onChange={(e) => setRemoteHost(e.target.value)}
|
||||
@@ -276,7 +277,7 @@ function PortForwardForm({
|
||||
<span className="text-[11px] text-muted-foreground">
|
||||
{translate('auto.components.right.sidebar.PortsPanel.8dfed0a15c', 'Label (optional)')}
|
||||
</span>
|
||||
<input
|
||||
<ImeInput
|
||||
type="text"
|
||||
value={label}
|
||||
onChange={(e) => setLabel(e.target.value)}
|
||||
|
||||
Reference in New Issue
Block a user