diff --git a/src/renderer/src/components/ime-enter-guarded-form.test-events.ts b/src/renderer/src/components/ime-enter-guarded-form.test-events.ts new file mode 100644 index 00000000000..b48d46c860d --- /dev/null +++ b/src/renderer/src/components/ime-enter-guarded-form.test-events.ts @@ -0,0 +1,53 @@ +import { act, fireEvent } from '@testing-library/react' + +function dispatchKey( + input: HTMLInputElement, + type: 'keydown' | 'keyup', + init: KeyboardEventInit +): boolean { + const event = new KeyboardEvent(type, { bubbles: true, cancelable: true, ...init }) + Object.defineProperty(event, 'keyCode', { value: init.keyCode }) + act(() => input.dispatchEvent(event)) + return event.defaultPrevented +} + +function dispatchImplicitSubmit(input: HTMLInputElement, init: KeyboardEventInit): boolean { + const prevented = dispatchKey(input, 'keydown', init) + if (!prevented) { + const form = input.closest('form') + if (!form) { + throw new Error('missing implicit-submit form') + } + act(() => form.dispatchEvent(new Event('submit', { bubbles: true, cancelable: true }))) + } + return prevented +} + +export function dispatchRecordedImeImplicitSubmit(input: HTMLInputElement): boolean { + fireEvent.compositionStart(input) + dispatchKey(input, 'keydown', { + key: 'Process', + code: 'Enter', + keyCode: 229, + isComposing: true + }) + fireEvent.compositionEnd(input, { data: '가' }) + const prevented = dispatchImplicitSubmit(input, { + key: 'Enter', + code: 'Enter', + keyCode: 13, + isComposing: false + }) + dispatchKey(input, 'keyup', { key: 'Process', code: 'Enter', keyCode: 229 }) + dispatchKey(input, 'keyup', { key: 'Enter', code: 'Enter', keyCode: 13 }) + return prevented +} + +export function dispatchOrdinaryImplicitSubmit(input: HTMLInputElement): boolean { + return dispatchImplicitSubmit(input, { + key: 'Enter', + code: 'Enter', + keyCode: 13, + isComposing: false + }) +} diff --git a/src/renderer/src/components/ime-enter-guarded-form.test.tsx b/src/renderer/src/components/ime-enter-guarded-form.test.tsx new file mode 100644 index 00000000000..9dda703fc9b --- /dev/null +++ b/src/renderer/src/components/ime-enter-guarded-form.test.tsx @@ -0,0 +1,86 @@ +// @vitest-environment happy-dom + +import { act, cleanup, fireEvent, render, screen } from '@testing-library/react' +import userEvent from '@testing-library/user-event' +import { afterEach, describe, expect, it, vi } from 'vitest' +import { dispatchOrdinaryImplicitSubmit } from './ime-enter-guarded-form.test-events' +import { ImeEnterGuardedForm } from './ime-enter-guarded-form' + +function dispatchKey(input: HTMLInputElement, init: KeyboardEventInit): boolean { + const event = new KeyboardEvent('keydown', { bubbles: true, cancelable: true, ...init }) + Object.defineProperty(event, 'keyCode', { value: init.keyCode }) + act(() => input.dispatchEvent(event)) + return event.defaultPrevented +} + +afterEach(cleanup) + +describe('form-level Enter default prevention', () => { + it('allows browser implicit submission without a bubbled veto', async () => { + const onSubmit = vi.fn((event: React.FormEvent) => event.preventDefault()) + const user = userEvent.setup() + render( +
+ ) + + await user.click(screen.getByLabelText('unguarded')) + await user.keyboard('{Enter}') + + expect(onSubmit).toHaveBeenCalledOnce() + }) + + it('vetoes browser implicit submission from the bubbled form keydown', async () => { + const onSubmit = vi.fn((event: React.FormEvent) => event.preventDefault()) + let bubbledCurrentTarget: EventTarget | null = null + const onKeyDown = vi.fn((event: React.KeyboardEvent) => { + bubbledCurrentTarget = event.currentTarget + event.preventDefault() + }) + const user = userEvent.setup() + render( + + ) + + await user.click(screen.getByLabelText('guarded')) + await user.keyboard('{Enter}') + + expect(onKeyDown).toHaveBeenCalledOnce() + expect(bubbledCurrentTarget).toBeInstanceOf(HTMLFormElement) + expect(onSubmit).not.toHaveBeenCalled() + }) +}) + +describe('ImeEnterGuardedForm field ownership', () => { + it('resets the carry when focus moves between fields', () => { + const onSubmit = vi.fn((event: React.FormEvent) => event.preventDefault()) + render( +