[Init] Initial commit - NetMesh terminal manager
Some checks failed
build-packages / resolve bundled mosh-client (push) Has been cancelled
build-packages / resolve bundled et-client (push) Has been cancelled
build-packages / build-macos (push) Has been cancelled
build-packages / build-windows (push) Has been cancelled
build-packages / build-linux-x64 (push) Has been cancelled
build-packages / build-linux-arm64 (push) Has been cancelled
build-packages / release (push) Has been cancelled
build-packages / update Nix release metadata (push) Has been cancelled
build-packages / bump homebrew tap (push) Has been cancelled
test / lint-and-test (push) Has been cancelled
AI automation / Route event (push) Has been cancelled
AI automation / Hand reopened issue to maintainers (push) Has been cancelled
AI automation / Clean source issue state (push) Has been cancelled
AI automation / Reconcile handoffs (push) Has been cancelled
AI automation / Classify issue (push) Has been cancelled
AI automation / Claude Code smoke (push) Has been cancelled
AI automation / Review issue follow-up (push) Has been cancelled
AI automation / Publish issue follow-up (push) Has been cancelled
AI automation / Implement with Claude Code (push) Has been cancelled
AI automation / Publish implement PR (push) Has been cancelled
AI automation / Continue queued issue comments (push) Has been cancelled
AI automation / Codex review loop (push) Has been cancelled
AI automation / Publish Codex fix (push) Has been cancelled
AI automation / Clear Codex dispatch marker (push) Has been cancelled
AI automation / Own PR re-request Codex (push) Has been cancelled
AI automation / External PR re-request Codex (push) Has been cancelled
AI automation / Poll Codex reaction / retry (push) Has been cancelled
build-et-binaries / build-linux-x64 (push) Has been cancelled
build-et-binaries / build-linux-arm64 (push) Has been cancelled
build-et-binaries / build-macos-universal (push) Has been cancelled
build-et-binaries / build-windows-x64 (push) Has been cancelled
build-et-binaries / release (push) Has been cancelled
Some checks failed
build-packages / resolve bundled mosh-client (push) Has been cancelled
build-packages / resolve bundled et-client (push) Has been cancelled
build-packages / build-macos (push) Has been cancelled
build-packages / build-windows (push) Has been cancelled
build-packages / build-linux-x64 (push) Has been cancelled
build-packages / build-linux-arm64 (push) Has been cancelled
build-packages / release (push) Has been cancelled
build-packages / update Nix release metadata (push) Has been cancelled
build-packages / bump homebrew tap (push) Has been cancelled
test / lint-and-test (push) Has been cancelled
AI automation / Route event (push) Has been cancelled
AI automation / Hand reopened issue to maintainers (push) Has been cancelled
AI automation / Clean source issue state (push) Has been cancelled
AI automation / Reconcile handoffs (push) Has been cancelled
AI automation / Classify issue (push) Has been cancelled
AI automation / Claude Code smoke (push) Has been cancelled
AI automation / Review issue follow-up (push) Has been cancelled
AI automation / Publish issue follow-up (push) Has been cancelled
AI automation / Implement with Claude Code (push) Has been cancelled
AI automation / Publish implement PR (push) Has been cancelled
AI automation / Continue queued issue comments (push) Has been cancelled
AI automation / Codex review loop (push) Has been cancelled
AI automation / Publish Codex fix (push) Has been cancelled
AI automation / Clear Codex dispatch marker (push) Has been cancelled
AI automation / Own PR re-request Codex (push) Has been cancelled
AI automation / External PR re-request Codex (push) Has been cancelled
AI automation / Poll Codex reaction / retry (push) Has been cancelled
build-et-binaries / build-linux-x64 (push) Has been cancelled
build-et-binaries / build-linux-arm64 (push) Has been cancelled
build-et-binaries / build-macos-universal (push) Has been cancelled
build-et-binaries / build-windows-x64 (push) Has been cancelled
build-et-binaries / release (push) Has been cancelled
This commit is contained in:
526
components/terminal/runtime/terminalImeTextInput.test.ts
Normal file
526
components/terminal/runtime/terminalImeTextInput.test.ts
Normal file
@@ -0,0 +1,526 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { dirname, join } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import {
|
||||
isAsciiPunctuationKey,
|
||||
isUnchangedDeferredImeTextInput,
|
||||
shouldBlockKeyPressForImeTextInput,
|
||||
shouldCommitDeferredImeTextInput,
|
||||
shouldDeferKeyDownForImeTextInput,
|
||||
resolveDeferredKeyupRelease,
|
||||
shouldDiscardStaleDeferredImeTextInput,
|
||||
shouldFlushDeferredImeTextInputOnKeyUp,
|
||||
shouldFlushStaleDeferredImeTextInput,
|
||||
} from "./terminalImeTextInput";
|
||||
|
||||
const runtimeSource = readFileSync(
|
||||
join(dirname(fileURLToPath(import.meta.url)), "createXTermRuntime.ts"),
|
||||
"utf8",
|
||||
);
|
||||
|
||||
test("isAsciiPunctuationKey accepts common remappable punctuation", () => {
|
||||
for (const key of [",", ".", "/", ";", "'", "[", "]", "\\", "-", "=", "`", "?", "!", ":", '"', "<", ">", "{", "}", "|", "_", "+", "~", "@", "#", "$", "%", "^", "&", "*", "(", ")"]) {
|
||||
assert.equal(isAsciiPunctuationKey(key), true, key);
|
||||
}
|
||||
});
|
||||
|
||||
test("isAsciiPunctuationKey rejects letters, digits, space, and CJK", () => {
|
||||
for (const key of ["a", "Z", "0", " ", ",", "、", "?", "Enter", "ArrowLeft"]) {
|
||||
assert.equal(isAsciiPunctuationKey(key), false, key);
|
||||
}
|
||||
});
|
||||
|
||||
test("shouldDeferKeyDownForImeTextInput defers bare ASCII punctuation keydowns", () => {
|
||||
assert.equal(
|
||||
shouldDeferKeyDownForImeTextInput({ type: "keydown", key: ",", keyCode: 188 }),
|
||||
true,
|
||||
);
|
||||
assert.equal(
|
||||
shouldDeferKeyDownForImeTextInput({ type: "keydown", key: "?", keyCode: 191 }),
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
test("shouldDeferKeyDownForImeTextInput leaves composition and modified keys alone", () => {
|
||||
assert.equal(
|
||||
shouldDeferKeyDownForImeTextInput({ type: "keydown", key: ",", keyCode: 229 }),
|
||||
false,
|
||||
);
|
||||
assert.equal(
|
||||
shouldDeferKeyDownForImeTextInput({ type: "keydown", key: ",", isComposing: true }),
|
||||
false,
|
||||
);
|
||||
assert.equal(
|
||||
shouldDeferKeyDownForImeTextInput({ type: "keydown", key: ",", ctrlKey: true }),
|
||||
false,
|
||||
);
|
||||
assert.equal(
|
||||
shouldDeferKeyDownForImeTextInput({ type: "keydown", key: "a", keyCode: 65 }),
|
||||
false,
|
||||
);
|
||||
assert.equal(
|
||||
shouldDeferKeyDownForImeTextInput({ type: "keypress", key: "," }),
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
test("shouldBlockKeyPressForImeTextInput only while a deferral is armed", () => {
|
||||
assert.equal(
|
||||
shouldBlockKeyPressForImeTextInput(",", { type: "keypress", key: "," }),
|
||||
true,
|
||||
);
|
||||
assert.equal(
|
||||
shouldBlockKeyPressForImeTextInput(null, { type: "keypress", key: "," }),
|
||||
false,
|
||||
);
|
||||
assert.equal(
|
||||
shouldBlockKeyPressForImeTextInput(",", { type: "keydown", key: "," }),
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
test("shouldBlockKeyPressForImeTextInput still blocks the deferred keystroke itself", () => {
|
||||
assert.equal(
|
||||
shouldBlockKeyPressForImeTextInput(
|
||||
"/",
|
||||
{ type: "keypress", key: "/", keyCode: 191 },
|
||||
191,
|
||||
),
|
||||
true,
|
||||
);
|
||||
// Shift+/ reports "?" on keydown, so the deferral arms with "?".
|
||||
assert.equal(
|
||||
shouldBlockKeyPressForImeTextInput(
|
||||
"?",
|
||||
{ type: "keypress", key: "?", keyCode: 191 },
|
||||
191,
|
||||
),
|
||||
true,
|
||||
);
|
||||
assert.equal(
|
||||
shouldBlockKeyPressForImeTextInput(
|
||||
"/",
|
||||
{ type: "keypress", key: "/", isComposing: true },
|
||||
191,
|
||||
),
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
test("a stale deferral must not swallow unrelated keypresses (#3103)", () => {
|
||||
// Uppercase letters are routed through keypress by xterm, so a blanket
|
||||
// keypress block turned one stale deferral into a terminal that ignored
|
||||
// everything typed afterwards.
|
||||
assert.equal(
|
||||
shouldBlockKeyPressForImeTextInput(
|
||||
"/",
|
||||
{ type: "keypress", key: "X", keyCode: 88 },
|
||||
191,
|
||||
),
|
||||
false,
|
||||
);
|
||||
assert.equal(
|
||||
shouldBlockKeyPressForImeTextInput(
|
||||
"/",
|
||||
{ type: "keypress", key: "a", keyCode: 65 },
|
||||
191,
|
||||
),
|
||||
false,
|
||||
);
|
||||
// A matching keyCode keeps the block when an IME rewrites the key label.
|
||||
assert.equal(
|
||||
shouldBlockKeyPressForImeTextInput(
|
||||
"/",
|
||||
{ type: "keypress", key: "1", keyCode: 191 },
|
||||
191,
|
||||
),
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
test("a Windows IME release reporting Process must flush the deferred slash (#3103)", () => {
|
||||
// Windows + CJK IME in vi: keydown still reports the physical key while the
|
||||
// IME consumes it, then reports Process/229 on the release and never sends
|
||||
// an insertText the runtime would treat as a remap.
|
||||
let deferredKey: string | null = null;
|
||||
const keydown = { type: "keydown", key: "/", keyCode: 191 };
|
||||
if (shouldDeferKeyDownForImeTextInput(keydown)) deferredKey = keydown.key;
|
||||
assert.equal(deferredKey, "/");
|
||||
|
||||
const release = { type: "keyup", key: "Process", keyCode: 229 };
|
||||
if (shouldFlushDeferredImeTextInputOnKeyUp(deferredKey, release)) deferredKey = null;
|
||||
assert.equal(deferredKey, null, "the deferral must not outlive its keystroke");
|
||||
|
||||
// Input keeps flowing afterwards.
|
||||
assert.equal(
|
||||
shouldBlockKeyPressForImeTextInput(deferredKey, { type: "keypress", key: "x", keyCode: 88 }),
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
test("a held key released while the deferral is armed keeps its own keyup identity", () => {
|
||||
// "/" is deferred (its own release was swallowed) and the user still holds
|
||||
// "a" with the other hand, so the "a" keyup lands first. The stale deferral
|
||||
// still ends, but that release is a real keyup, not an IME sentinel: it
|
||||
// keeps its own identity, and the deferred press is released separately.
|
||||
const deferredKey = "/";
|
||||
const heldKeyUp = { type: "keyup", key: "a", keyCode: 65, code: "KeyA" };
|
||||
assert.equal(
|
||||
shouldFlushDeferredImeTextInputOnKeyUp(deferredKey, heldKeyUp),
|
||||
true,
|
||||
"the stale deferral still ends on a real release",
|
||||
);
|
||||
assert.equal(
|
||||
resolveDeferredKeyupRelease(deferredKey, "Slash", heldKeyUp),
|
||||
"unrelated",
|
||||
"an unrelated release must not be rewritten to the deferred key",
|
||||
);
|
||||
});
|
||||
|
||||
test("only IME sentinel releases take over the deferred key's release identity", () => {
|
||||
// Windows IMEs report Process/229 (occasionally Unidentified) as the release
|
||||
// of a key they consumed — those are the only releases that stand in for the
|
||||
// deferred punctuation key.
|
||||
assert.equal(
|
||||
resolveDeferredKeyupRelease("/", "Slash", { type: "keyup", key: "Process", keyCode: 229 }),
|
||||
"deferred",
|
||||
);
|
||||
assert.equal(
|
||||
resolveDeferredKeyupRelease("/", "Slash", { type: "keyup", key: "Unidentified" }),
|
||||
"deferred",
|
||||
);
|
||||
assert.equal(
|
||||
resolveDeferredKeyupRelease("/", "Slash", { type: "keyup", key: "/", keyCode: 191 }),
|
||||
"own",
|
||||
"a matched release already encodes from the real event",
|
||||
);
|
||||
assert.equal(
|
||||
resolveDeferredKeyupRelease("/", "Slash", {
|
||||
type: "keyup",
|
||||
key: "1",
|
||||
keyCode: 191,
|
||||
code: "Slash",
|
||||
}),
|
||||
"own",
|
||||
"a release of the same physical key already pairs the flushed press",
|
||||
);
|
||||
assert.equal(
|
||||
resolveDeferredKeyupRelease("/", "Slash", { type: "keyup", key: "Shift", keyCode: 16 }),
|
||||
"own",
|
||||
);
|
||||
assert.equal(
|
||||
resolveDeferredKeyupRelease("/", "Slash", {
|
||||
type: "keyup",
|
||||
key: "a",
|
||||
keyCode: 65,
|
||||
code: "KeyA",
|
||||
ctrlKey: true,
|
||||
}),
|
||||
"own",
|
||||
);
|
||||
assert.equal(
|
||||
resolveDeferredKeyupRelease(null, null, { type: "keyup", key: "Process", keyCode: 229 }),
|
||||
"own",
|
||||
);
|
||||
});
|
||||
|
||||
test("a deferral left without release or insertText recovers on the next keystroke (#3103)", () => {
|
||||
let deferredKey: string | null = null;
|
||||
const keydown = { type: "keydown", key: "/", keyCode: 191 };
|
||||
if (shouldDeferKeyDownForImeTextInput(keydown)) deferredKey = keydown.key;
|
||||
assert.equal(deferredKey, "/");
|
||||
|
||||
// No insertText and no keyup at all; the user then types a plain letter.
|
||||
const nextKeyDown = { type: "keydown", key: "a", keyCode: 65 };
|
||||
if (shouldFlushStaleDeferredImeTextInput(deferredKey, nextKeyDown)) deferredKey = null;
|
||||
assert.equal(deferredKey, null, "the stale deferral must flush before the new keystroke");
|
||||
assert.equal(
|
||||
shouldBlockKeyPressForImeTextInput(deferredKey, { type: "keypress", key: "a", keyCode: 65 }),
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
test("a modified keydown discards the stale deferral instead of flushing it", () => {
|
||||
// The IME swallowed the "/" release and the user then interrupts with
|
||||
// Ctrl+C. Flushing there would inject "/" in front of the interrupt, and
|
||||
// keeping the deferral armed would inject it before the next character, so
|
||||
// the lost keystroke is dropped instead.
|
||||
assert.equal(
|
||||
shouldFlushStaleDeferredImeTextInput("/", {
|
||||
type: "keydown",
|
||||
key: "c",
|
||||
keyCode: 67,
|
||||
ctrlKey: true,
|
||||
}),
|
||||
false,
|
||||
);
|
||||
assert.equal(
|
||||
shouldDiscardStaleDeferredImeTextInput("/", {
|
||||
type: "keydown",
|
||||
key: "c",
|
||||
keyCode: 67,
|
||||
ctrlKey: true,
|
||||
}),
|
||||
true,
|
||||
);
|
||||
assert.equal(
|
||||
shouldDiscardStaleDeferredImeTextInput("/", {
|
||||
type: "keydown",
|
||||
key: "Tab",
|
||||
keyCode: 9,
|
||||
altKey: true,
|
||||
}),
|
||||
true,
|
||||
);
|
||||
assert.equal(
|
||||
shouldDiscardStaleDeferredImeTextInput("/", {
|
||||
type: "keydown",
|
||||
key: "d",
|
||||
keyCode: 229,
|
||||
ctrlKey: true,
|
||||
isComposing: true,
|
||||
}),
|
||||
false,
|
||||
"a composition still owns the keystroke and resolves via insertText",
|
||||
);
|
||||
assert.equal(
|
||||
shouldDiscardStaleDeferredImeTextInput("/", { type: "keydown", key: "a", keyCode: 65 }),
|
||||
false,
|
||||
);
|
||||
assert.equal(
|
||||
shouldDiscardStaleDeferredImeTextInput(null, {
|
||||
type: "keydown",
|
||||
key: "c",
|
||||
keyCode: 67,
|
||||
ctrlKey: true,
|
||||
}),
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
test("auto-repeat and modifier keystrokes keep the deferral armed", () => {
|
||||
// Same-key keydown is auto-repeat: re-arm instead of flushing, so a held key
|
||||
// does not emit an extra character per repeat.
|
||||
assert.equal(
|
||||
shouldFlushStaleDeferredImeTextInput("/", { type: "keydown", key: "/", keyCode: 191 }),
|
||||
false,
|
||||
);
|
||||
assert.equal(
|
||||
shouldFlushStaleDeferredImeTextInput("/", { type: "keydown", key: "Shift", keyCode: 16 }),
|
||||
false,
|
||||
);
|
||||
assert.equal(
|
||||
shouldFlushStaleDeferredImeTextInput("/", {
|
||||
type: "keydown",
|
||||
key: "d",
|
||||
keyCode: 229,
|
||||
isComposing: true,
|
||||
}),
|
||||
false,
|
||||
);
|
||||
assert.equal(
|
||||
shouldFlushDeferredImeTextInputOnKeyUp("/", { type: "keyup", key: "Shift", keyCode: 16 }),
|
||||
false,
|
||||
);
|
||||
assert.equal(
|
||||
shouldFlushDeferredImeTextInputOnKeyUp("/", {
|
||||
type: "keyup",
|
||||
key: "c",
|
||||
keyCode: 67,
|
||||
ctrlKey: true,
|
||||
}),
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
test("an active composition still resolves the deferred keystroke via insertText", () => {
|
||||
// The IME absorbed the punctuation into a composition: the composing release
|
||||
// must not flush the ASCII key ahead of the committed glyph.
|
||||
assert.equal(
|
||||
shouldFlushDeferredImeTextInputOnKeyUp("/", {
|
||||
type: "keyup",
|
||||
key: "/",
|
||||
keyCode: 191,
|
||||
isComposing: true,
|
||||
}),
|
||||
false,
|
||||
);
|
||||
assert.equal(
|
||||
shouldCommitDeferredImeTextInput("/", { inputType: "insertText", data: "、" }),
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
test("a matched keyup still flushes the English punctuation fallback (#2833)", () => {
|
||||
assert.equal(
|
||||
shouldFlushDeferredImeTextInputOnKeyUp("/", { type: "keyup", key: "/", keyCode: 191 }),
|
||||
true,
|
||||
);
|
||||
assert.equal(
|
||||
shouldFlushDeferredImeTextInputOnKeyUp(null, { type: "keyup", key: "/", keyCode: 191 }),
|
||||
false,
|
||||
);
|
||||
assert.equal(
|
||||
shouldFlushDeferredImeTextInputOnKeyUp("/", { type: "keydown", key: "/", keyCode: 191 }),
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
test("shouldCommitDeferredImeTextInput accepts insertText payloads while deferred", () => {
|
||||
assert.equal(
|
||||
shouldCommitDeferredImeTextInput(",", { inputType: "insertText", data: "," }),
|
||||
true,
|
||||
);
|
||||
assert.equal(
|
||||
shouldCommitDeferredImeTextInput(",", { inputType: "insertText", data: "," }),
|
||||
true,
|
||||
);
|
||||
assert.equal(
|
||||
shouldCommitDeferredImeTextInput(null, { inputType: "insertText", data: "," }),
|
||||
false,
|
||||
);
|
||||
assert.equal(
|
||||
shouldCommitDeferredImeTextInput(",", { inputType: "insertFromPaste", data: "," }),
|
||||
false,
|
||||
);
|
||||
assert.equal(
|
||||
shouldCommitDeferredImeTextInput(",", { inputType: "insertText", data: null }),
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
test("isUnchangedDeferredImeTextInput detects English punctuation flush", () => {
|
||||
assert.equal(isUnchangedDeferredImeTextInput(",", ","), true);
|
||||
assert.equal(isUnchangedDeferredImeTextInput(",", ","), false);
|
||||
assert.equal(isUnchangedDeferredImeTextInput(null, ","), false);
|
||||
});
|
||||
|
||||
test("createXTermRuntime defers ASCII punctuation keydowns to insertText", () => {
|
||||
assert.match(runtimeSource, /shouldDeferKeyDownForImeTextInput\(e\)/);
|
||||
assert.match(runtimeSource, /armImeTextInputDeferral\(e\)/);
|
||||
assert.match(
|
||||
runtimeSource,
|
||||
/shouldBlockKeyPressForImeTextInput\(\s*imeTextInputDeferredKey,\s*e,\s*imeTextInputDeferredKittyEvent\?\.keyCode \?\? null,\s*\)/,
|
||||
);
|
||||
assert.match(
|
||||
runtimeSource,
|
||||
/shouldCommitDeferredImeTextInput\(imeTextInputDeferredKey, event\)/,
|
||||
);
|
||||
assert.match(runtimeSource, /commitImeTextInput\(event\.data\)/);
|
||||
assert.match(runtimeSource, /isUnchangedDeferredImeTextInput\(deferredKey, text\)/);
|
||||
assert.match(runtimeSource, /imeTextInputDeferredKittyEvent/);
|
||||
// Manual commit bypasses xterm onUserInput; clear selection unless preserved.
|
||||
assert.match(
|
||||
runtimeSource,
|
||||
/!ctx\.terminalSettingsRef\.current\?\.preserveSelectionOnInput/,
|
||||
);
|
||||
assert.match(runtimeSource, /term\.clearSelection\(\)/);
|
||||
const clearSelIdx = runtimeSource.indexOf("term.clearSelection()");
|
||||
const commitIdx = runtimeSource.indexOf("const commitImeTextInput = (text: string)");
|
||||
const firstHandleIdx = runtimeSource.indexOf(
|
||||
"handleTerminalInputData",
|
||||
commitIdx,
|
||||
);
|
||||
assert.ok(
|
||||
commitIdx >= 0 &&
|
||||
clearSelIdx > commitIdx &&
|
||||
firstHandleIdx > clearSelIdx,
|
||||
);
|
||||
// Must run before Kitty/xterm send the half-width key from keydown.
|
||||
const deferIdx = runtimeSource.indexOf("shouldDeferKeyDownForImeTextInput(e)");
|
||||
const kittySendIdx = runtimeSource.indexOf("if (kittySequenceForKeyDown)");
|
||||
assert.ok(deferIdx >= 0 && kittySendIdx > deferIdx);
|
||||
// Unchanged ASCII must encode via Kitty key events, not composition text.
|
||||
const unchangedIdx = runtimeSource.indexOf("isUnchangedDeferredImeTextInput(deferredKey, text)");
|
||||
const compositionIdx = runtimeSource.indexOf(
|
||||
"encodeKittyCompositionText(kittyKeyboardMode, sanitizedText)",
|
||||
);
|
||||
assert.ok(unchangedIdx >= 0 && compositionIdx > unchangedIdx);
|
||||
// Even when the source writes the literal glyph, broadcast peers still get
|
||||
// the deferred physical key (with legacy fallback), not composition text.
|
||||
const armIdx = runtimeSource.indexOf("const armImeTextInputDeferral");
|
||||
assert.ok(armIdx >= 0);
|
||||
const armSlice = runtimeSource.slice(armIdx, armIdx + 700);
|
||||
assert.match(
|
||||
armSlice,
|
||||
/imeTextInputDeferredKittyEvent = toKittyKeyboardEvent\(event\)/,
|
||||
);
|
||||
assert.doesNotMatch(
|
||||
armSlice,
|
||||
/imeTextInputDeferredKittyEvent = kittyKeyboardProtocolEnabled/,
|
||||
);
|
||||
const unchangedFallbackIdx = runtimeSource.indexOf(
|
||||
"isUnchangedDeferredImeTextInput(deferredKey, text))",
|
||||
unchangedIdx + 1,
|
||||
);
|
||||
assert.ok(unchangedFallbackIdx > unchangedIdx);
|
||||
assert.match(
|
||||
runtimeSource.slice(unchangedFallbackIdx, unchangedFallbackIdx + 1800),
|
||||
/handleTerminalInputData\(text, \{ perCharacterWrites: shouldSplitImeTextInputForWire\(text\) \}\);[\s\S]*shouldTrackKittyKeyRelease\(kittyKeyboardMode, pressEvent\)[\s\S]*upsertKittyKeyboardForwardedPress\(\s*kittyForwardedKeys,[\s\S]*broadcastKittyInput\(\{[\s\S]*kind: "key",[\s\S]*fallbackToLegacy: true,/,
|
||||
);
|
||||
// Remap path must fall back to literal text when composition encoding is null
|
||||
// (report-all without associated text).
|
||||
assert.match(
|
||||
runtimeSource.slice(compositionIdx, compositionIdx + 560),
|
||||
/if \(encoded\) \{[\s\S]*handleTerminalInputData\(encoded, \{ source: "kitty" \}\);[\s\S]*\} else \{[\s\S]*handleTerminalInputData\(sanitizedText, \{\s*perCharacterWrites: shouldSplitImeTextInputForWire\(sanitizedText\),?\s*\}\);/,
|
||||
);
|
||||
});
|
||||
|
||||
test("createXTermRuntime recovers a stuck IME punctuation deferral (#3103)", () => {
|
||||
// Any real key release ends the deferral, not just an exact key match.
|
||||
const keyupIdx = runtimeSource.indexOf('if (e.type === "keyup")');
|
||||
assert.ok(keyupIdx >= 0);
|
||||
const keyupSlice = runtimeSource.slice(keyupIdx, keyupIdx + 2800);
|
||||
assert.match(
|
||||
keyupSlice,
|
||||
/shouldFlushDeferredImeTextInputOnKeyUp\(imeTextInputDeferredKey, e\)/,
|
||||
);
|
||||
assert.match(keyupSlice, /flushImeTextInputDeferral\(\);/);
|
||||
// Only an IME sentinel release (Process/229/Unidentified) takes over the
|
||||
// deferred key's release identity; an unrelated keyup keeps its own identity
|
||||
// and the flushed press is released separately.
|
||||
assert.match(
|
||||
keyupSlice,
|
||||
/resolveDeferredKeyupRelease\(\s*imeTextInputDeferredKey,\s*deferredKittyEvent\?\.code \?\? null,\s*e,?\s*\)/,
|
||||
);
|
||||
assert.match(keyupSlice, /releaseMode === "deferred"/);
|
||||
assert.match(keyupSlice, /releaseMode === "unrelated"/);
|
||||
assert.match(
|
||||
keyupSlice,
|
||||
/\.\.\.deferredKittyEvent,\s*type: "keyup",/,
|
||||
);
|
||||
const kittyReleaseIdx = keyupSlice.indexOf("toKittyKeyboardEvent(releaseEvent)");
|
||||
assert.ok(kittyReleaseIdx > keyupSlice.indexOf("flushImeTextInputDeferral();"));
|
||||
|
||||
// The stale flush runs on keydown, after the broadcast guard and before the
|
||||
// composition handling, so a wedged deferral cannot survive a new keystroke.
|
||||
const staleIdx = runtimeSource.indexOf(
|
||||
"shouldFlushStaleDeferredImeTextInput(imeTextInputDeferredKey, e)",
|
||||
);
|
||||
assert.ok(staleIdx > keyupIdx);
|
||||
const keydownGuardIdx = runtimeSource.indexOf("if (handlingKittyBroadcast) return true;", keyupIdx);
|
||||
const keyCode229Idx = runtimeSource.indexOf("if (e.keyCode === 229) {", keyupIdx);
|
||||
assert.ok(
|
||||
keydownGuardIdx > keyupIdx &&
|
||||
staleIdx > keydownGuardIdx &&
|
||||
keyCode229Idx > staleIdx,
|
||||
);
|
||||
assert.match(
|
||||
runtimeSource.slice(staleIdx - 400, staleIdx + 200),
|
||||
/flushImeTextInputDeferral\(\);/,
|
||||
);
|
||||
// The recovery runs because the deferred key's release will never arrive, so
|
||||
// the press emitted by the flush must be paired with a synthesized release
|
||||
// instead of staying forwarded until focus loss.
|
||||
const recoverySlice = runtimeSource.slice(staleIdx, staleIdx + 500);
|
||||
assert.match(recoverySlice, /deferredKittyEvent = imeTextInputDeferredKittyEvent/);
|
||||
assert.match(recoverySlice, /releaseForwardedKittyPress\(\s*\{\s*\.\.\.deferredKittyEvent,\s*type: "keyup",?\s*\}\s*\)/);
|
||||
// A modified keydown (Ctrl+C, Alt+…) drops the lost keystroke instead of
|
||||
// injecting it in front of the interrupt or shortcut.
|
||||
assert.match(
|
||||
runtimeSource.slice(staleIdx, staleIdx + 700),
|
||||
/shouldDiscardStaleDeferredImeTextInput\(imeTextInputDeferredKey, e\)/,
|
||||
);
|
||||
});
|
||||
Reference in New Issue
Block a user