Files
Netcatty/domain/imeControlledInput.test.ts
T
Netcatty-Bot 69df81613b fix(sftp): keep filter input IME-safe during CJK composition (#2746)
* fix(#2745): automated Cursor CLI fix

* fix(sftp): reset IME composing guard when filter bar closes

If the filter input unmounts mid-composition (filter bar toggled closed),
compositionend never fires, leaving filterComposingRef stuck at true and
blocking every later onSetFilter commit. Clear the guard when the bar hides.

Also drop the now-unused startTransition prop from the toolbar (its only
use was the removed deferred filter path).

Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>

* fix(sftp): resync filter draft to committed filter when bar closes

Resetting only the composing guard left filterDraft diverged from pane.filter
when the bar closed mid-composition, so reopening showed stale uncommitted text.
Resync the draft to pane.filter on close.

Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>

* fix(sftp): clear IME composing guard in commitFilterValue

Centralize the guard reset so every commit/clear path (composition end,
Escape, inline clear button, close button) finalizes the IME session. The
inline clear previously left filterComposingRef stuck true, blocking later
onChange commits and letting a delayed compositionend undo the clear.

Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>

* fix(sftp): honor external filter change over stale draft on composition end

If navigation (e.g. follow-terminal-CWD) clears pane.filter mid-composition,
onCompositionEnd previously re-committed the stale composed draft, overwriting
the navigation-clear invariant. Capture pane.filter at composition start and
adopt any external change on end instead of committing the stale draft.

Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>

* fix(sftp): supersede IME composition when navigation clears filter

Navigation can clear pane.filter mid-CJK composition. Adopt that external
clear into the draft while composing, arm a supersede latch so the common
post-compositionend onChange cannot re-commit the stale draft, and reset
composing/supersede state when the filter bar unmounts or a commit path runs.

* fix(sftp): supersede IME filter draft when path navigates

When the committed filter was already empty before composition,
directory navigation sets filter to "" again so pane.filter does not
change. Track path at composition start and supersede the draft when
the path changes so compositionend cannot re-commit stale IME text.

* fix: address Codex review on PR #2746

* fix(sftp): clear IME supersede latch if no post-change arrives

After compositionend arms the supersede latch, schedule a macrotask
fallback clear so ordinary keystrokes work when the browser never
fires the post-composition onChange that would clear it.

* fix: use ASCII punctuation in PR comments and tests

Replace em dashes with ASCII hyphens in comments/tests. Keep the guarded
setTimeout supersede-latch clear from the prior IME fix.

---------

Co-authored-by: netcatty-bot <308658023+netcatty-bot@users.noreply.github.com>
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
Co-authored-by: bincxz <16399091+binaricat@users.noreply.github.com>
2026-08-06 08:53:19 +08:00

185 lines
4.8 KiB
TypeScript

import test from "node:test";
import assert from "node:assert/strict";
import {
resolveSupersededImeInputEvent,
shouldAdoptExternalImeControlledValue,
shouldCommitImeControlledChange,
} from "./imeControlledInput.ts";
test("does not commit controlled changes while an IME composition session is open", () => {
assert.equal(
shouldCommitImeControlledChange({
isComposingSession: true,
nativeEventIsComposing: true,
}),
false,
);
assert.equal(
shouldCommitImeControlledChange({
isComposingSession: true,
nativeEventIsComposing: false,
}),
false,
);
});
test("does not commit when the native event still reports composing", () => {
assert.equal(
shouldCommitImeControlledChange({
isComposingSession: false,
nativeEventIsComposing: true,
}),
false,
);
});
test("commits ordinary keystrokes outside composition", () => {
assert.equal(
shouldCommitImeControlledChange({
isComposingSession: false,
nativeEventIsComposing: false,
}),
true,
);
assert.equal(
shouldCommitImeControlledChange({
isComposingSession: false,
}),
true,
);
});
test("does not commit when composition was externally superseded", () => {
assert.equal(
shouldCommitImeControlledChange({
isComposingSession: false,
nativeEventIsComposing: false,
compositionExternallySuperseded: true,
}),
false,
);
assert.equal(
shouldCommitImeControlledChange({
isComposingSession: true,
nativeEventIsComposing: false,
compositionExternallySuperseded: true,
}),
false,
);
});
test("adopts external value into draft only when not composing and values differ", () => {
assert.equal(
shouldAdoptExternalImeControlledValue({
isComposingSession: false,
draftValue: "sou",
externalValue: "",
}),
true,
);
assert.equal(
shouldAdoptExternalImeControlledValue({
isComposingSession: true,
draftValue: "sou",
externalValue: "",
}),
false,
);
assert.equal(
shouldAdoptExternalImeControlledValue({
isComposingSession: false,
draftValue: "搜",
externalValue: "搜",
}),
false,
);
});
test("adopts external navigation-clear mid-composition when compose-start baseline is provided", () => {
// Parent cleared filter for different-directory navigation while IME was open.
assert.equal(
shouldAdoptExternalImeControlledValue({
isComposingSession: true,
draftValue: "sou",
externalValue: "",
valueAtComposeStart: "old",
}),
true,
);
// External value still matches the compose-start baseline - keep draft for IME.
assert.equal(
shouldAdoptExternalImeControlledValue({
isComposingSession: true,
draftValue: "sou",
externalValue: "old",
valueAtComposeStart: "old",
}),
false,
);
// Draft already matches external after a prior adopt - no-op.
assert.equal(
shouldAdoptExternalImeControlledValue({
isComposingSession: true,
draftValue: "",
externalValue: "",
valueAtComposeStart: "old",
}),
false,
);
});
test("suppresses post-composition onChange after external supersede and clears the latch once ended", () => {
// Mid-composition after navigation clear: ignore event, keep latch armed.
assert.deepEqual(
resolveSupersededImeInputEvent({
compositionExternallySuperseded: true,
isComposingSession: true,
nativeEventIsComposing: true,
}),
{ ignoreEventValue: true, clearSupersedeLatch: false },
);
// Post-compositionend follow-up change (composing=false): ignore once and clear.
assert.deepEqual(
resolveSupersededImeInputEvent({
compositionExternallySuperseded: true,
isComposingSession: false,
nativeEventIsComposing: false,
}),
{ ignoreEventValue: true, clearSupersedeLatch: true },
);
// No supersede: ordinary path.
assert.deepEqual(
resolveSupersededImeInputEvent({
compositionExternallySuperseded: false,
isComposingSession: false,
nativeEventIsComposing: false,
}),
{ ignoreEventValue: false, clearSupersedeLatch: false },
);
});
test("ordinary commits remain blocked only while the supersede latch is armed", () => {
// Documents the stuck-latch failure mode: if compositionend arms the latch and
// no post-composition onChange clears it, shouldCommitImeControlledChange stays
// false for ordinary keystrokes. UI must clear via onChange or a deferred fallback.
assert.equal(
shouldCommitImeControlledChange({
isComposingSession: false,
nativeEventIsComposing: false,
compositionExternallySuperseded: true,
}),
false,
);
assert.equal(
shouldCommitImeControlledChange({
isComposingSession: false,
nativeEventIsComposing: false,
compositionExternallySuperseded: false,
}),
true,
);
});