diff --git a/web/fizzy.js b/web/fizzy.js index 709c96ca0..a7bff936f 100644 --- a/web/fizzy.js +++ b/web/fizzy.js @@ -1037,6 +1037,16 @@ export class Fizzy { focusQuietly(hidden); } + // Forward keyboard edits to the fragment before the caret as typing to the worker. + // Declare before worker.onmessage, which may deliver textInput immediately. + const inputFilter = new TextInput(hidden, { + text: (text) => post({ type: "text", text }), + backspace: () => { + post({ type: "key", kind: 5, key: "Backspace", repeat: 0, mods: 0 }); + post({ type: "key", kind: 6, key: "Backspace", repeat: 0, mods: 0 }); + }, + }); + let boundKeys = new Set(); const offscreen = canvas.transferControlToOffscreen(); // The init waits for the early compile (`wasmModule`), which by now has usually finished. @@ -1066,15 +1076,6 @@ export class Fizzy { window.matchMedia(q).addEventListener("change", () => post({ type: "prefs", prefs: prefs() })); } - // What the keyboard does to the fragment before the caret reaches the worker as typing. - const inputFilter = new TextInput(hidden, { - text: (text) => post({ type: "text", text }), - backspace: () => { - post({ type: "key", kind: 5, key: "Backspace", repeat: 0, mods: 0 }); - post({ type: "key", kind: 6, key: "Backspace", repeat: 0, mods: 0 }); - }, - }); - worker.onerror = (err) => { console.error(err); onError(`Failed to load the editor (${err.message || "worker error"}).`); diff --git a/web/tests/embed.test.mjs b/web/tests/embed.test.mjs index b49501e2d..117776e37 100644 --- a/web/tests/embed.test.mjs +++ b/web/tests/embed.test.mjs @@ -143,18 +143,25 @@ console.log("embed.test: ok"); // Deliver text input as soon as the handler is installed, before setup completes. let earlyTextInputs = 0; let filterClears = 0; +const filteredKeys = []; +let workerInstance; const { TextInput } = await import(pathToFileURL(path.join(dir, "web.js")).href); TextInput.prototype.clear = () => { filterClears++; }; +// This spy checks routing; text-input.test.js exercises the real filter and Backspace. +TextInput.prototype.keydown = (ev) => { filteredKeys.push(ev.key); }; Canvas.prototype.transferControlToOffscreen = function () { return {}; }; globalThis.OffscreenCanvas = class {}; globalThis.ResizeObserver = class { observe() {} }; +const workerMessages = []; globalThis.Worker = class { + constructor() { workerInstance = this; } set onmessage(handler) { this.handler = handler; handler({ data: { type: "textInput", rect: [10, 20, 80, 30] } }); earlyTextInputs++; } postMessage(message) { + workerMessages.push(message); if (message.type === "init") this.handler({ data: { type: "ready" } }); } }; @@ -168,4 +175,18 @@ assert.equal(filterClears, 1, "entering text input clears the initialized filter assert.equal(document.body.children[0].style.left, "10px", "early text input positions the hidden input"); workerCanvas.dispatch("pointerdown", { pointerId: 1, pointerType: "mouse", buttons: 1, clientX: 0, clientY: 0, timeStamp: 0 }); assert.equal(filterClears, 2, "pointer input still clears the filter"); +const hidden = document.body.children[0]; +assert.equal(key(hidden).defaultPrevented, false, "the worker hidden input is recognized without asking main-thread wasm"); +for (const pressedKey of ["a", "Backspace"]) { + hidden.dispatch("keydown", { key: pressedKey, preventDefault() {} }); + hidden.dispatch("keyup", { key: pressedKey }); +} +assert.deepEqual(filteredKeys, ["a", "Backspace"], "typing and backspace are routed to the filter spy after early text input"); +assert.deepEqual(workerMessages.filter((m) => m.type === "key").map((m) => [m.kind, m.key]), [ + [5, "a"], [6, "a"], [5, "Backspace"], [6, "Backspace"], +], "typing and backspace still reach the worker"); +workerInstance.handler({ data: { type: "textInput", rect: [20, 30, 80, 30] } }); +assert.equal(filterClears, 2, "moving the text input preserves its fragment"); +workerInstance.handler({ data: { type: "textInput", rect: [] } }); +assert.equal(filterClears, 3, "leaving text input clears the filter"); console.log("embed.test: worker startup and early text input ok");