From 79ad53f9cf8960449a90512e0b986536c069194c Mon Sep 17 00:00:00 2001 From: Eberhard Beilharz Date: Mon, 8 Dec 2025 19:40:24 +0100 Subject: [PATCH 01/13] refactor(web): document and cleanup parameters of `Keyboard.notify()` Document what values are possible for the `eventCode` parameter and what these values mean. Also change `data` parameter to a `boolean`. Test-bot: skip --- web/src/app/browser/src/contextManager.ts | 4 ++-- .../engine/src/js-processor/jsKeyboardProcessor.ts | 4 ++-- web/src/engine/src/keyboard/keyboards/jsKeyboard.ts | 6 +++--- web/src/engine/src/keyboard/keyboards/kmxKeyboard.ts | 11 +++++++---- 4 files changed, 14 insertions(+), 11 deletions(-) diff --git a/web/src/app/browser/src/contextManager.ts b/web/src/app/browser/src/contextManager.ts index 30729b34df..8004e743d8 100644 --- a/web/src/app/browser/src/contextManager.ts +++ b/web/src/app/browser/src/contextManager.ts @@ -613,7 +613,7 @@ export class ContextManager extends ContextManagerBase { const activeKeyboard = this.activeKeyboard?.keyboard; if(!focusAssistant.restoringFocus) { textStore?.deadkeys().clear(); - activeKeyboard?.notify(0, textStore, 1); // I2187 + activeKeyboard?.notify(0, textStore, true); // I2187 } if(!focusAssistant.restoringFocus && this.mostRecentTextStore != textStore) { @@ -711,7 +711,7 @@ export class ContextManager extends ContextManagerBase { const {activeKeyboard} = this; const {maintainingFocus} = this.focusAssistant; if(!maintainingFocus && activeKeyboard) { - activeKeyboard.keyboard.notify(0, textStore, 0); // I2187 + activeKeyboard.keyboard.notify(0, textStore, false); // I2187 } if(previousTextStore && !this.activeTextStore) { this.emit('textstorechange', null); diff --git a/web/src/engine/src/js-processor/jsKeyboardProcessor.ts b/web/src/engine/src/js-processor/jsKeyboardProcessor.ts index a42ecc7067..5b2bd705ac 100644 --- a/web/src/engine/src/js-processor/jsKeyboardProcessor.ts +++ b/web/src/engine/src/js-processor/jsKeyboardProcessor.ts @@ -539,7 +539,7 @@ export class JSKeyboardProcessor extends EventEmitter implements Keybo } if(Levent.isModifier) { - this.activeKeyboard.notify(Levent.Lcode, textStore, isKeyDown ? 1 : 0); + this.activeKeyboard.notify(Levent.Lcode, textStore, isKeyDown); // For eventual integration - we bypass an OSK update for physical keystrokes when in touch mode. if(!Levent.device.touchable) { return this._UpdateVKShift(Levent); // I2187 @@ -549,7 +549,7 @@ export class JSKeyboardProcessor extends EventEmitter implements Keybo } if(Levent.LmodifierChange) { - this.activeKeyboard.notify(0, textStore, 1); + this.activeKeyboard.notify(0, textStore, true); if(!Levent.device.touchable) { this._UpdateVKShift(Levent); } diff --git a/web/src/engine/src/keyboard/keyboards/jsKeyboard.ts b/web/src/engine/src/keyboard/keyboards/jsKeyboard.ts index 90b45f3ba5..4c6631a1a8 100644 --- a/web/src/engine/src/keyboard/keyboards/jsKeyboard.ts +++ b/web/src/engine/src/keyboard/keyboards/jsKeyboard.ts @@ -358,12 +358,12 @@ export class JSKeyboard { * * @param {number} command event code (16,17,18) or 0 * @param {TextStore} textStore textStore - * @param {number} data 1 or 0 + * @param {boolean} data 1 or 0 */ - public notify(command: number, textStore: TextStore, data: number): void { // I2187 + public notify(command: number, textStore: TextStore, data: boolean): void { // I2187 // Good example use case - the Japanese CJK-picker keyboard if(typeof(this.scriptObject['KNS']) == 'function') { - this.scriptObject['KNS'](command, textStore, data); + this.scriptObject['KNS'](command, textStore, data ? 1 : 0); } } diff --git a/web/src/engine/src/keyboard/keyboards/kmxKeyboard.ts b/web/src/engine/src/keyboard/keyboards/kmxKeyboard.ts index 73b62558dc..ba78dc4e01 100644 --- a/web/src/engine/src/keyboard/keyboards/kmxKeyboard.ts +++ b/web/src/engine/src/keyboard/keyboards/kmxKeyboard.ts @@ -99,12 +99,15 @@ export class KMXKeyboard { } /** - * @param {number} eventCode event code (16,17,18) or 0 // TODO-web-core: document meaning of these! (#15290) - * @param {TextStore} textStore textStore - * @param {number} data 1 or 0 * Notifies keyboard of keystroke or other event + * + * @param {number} eventCode key code (16-18: Shift, Control or Alt), + * or 0 for focus + * @param {TextStore} textStore textStore + * @param {number} boolean true for KeyDown or FocusReceived, + * false for KeyUp or FocusLost */ - public notify(eventCode: 16|17|18|0, textStore: TextStore, data: number) { // I2187 + public notify(eventCode: number, textStore: TextStore, data: boolean): void { // I2187 // TODO-web-core: do we need to support this? (#15290) } From d1ddff8b567b80c10abda7c7f6473669e50c1501 Mon Sep 17 00:00:00 2001 From: Eberhard Beilharz Date: Mon, 8 Dec 2025 19:43:35 +0100 Subject: [PATCH 02/13] refactor(web): cleanup and clarifications Build-bot: skip Test-bot: skip --- web/common.inc.sh | 3 ++- web/src/test/auto/headless/README.md | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/web/common.inc.sh b/web/common.inc.sh index b587bf99ab..b4a3c47d7e 100644 --- a/web/common.inc.sh +++ b/web/common.inc.sh @@ -91,7 +91,7 @@ function test-headless() { TEST_FOLDER=$1 TEST_BASE="${KEYMAN_ROOT}/web/src/test/auto/headless/" TEST_EXTENSIONS=${2:-} - if [ ! -z "${2:-}" ]; then + if [[ ! -z "${2:-}" ]]; then TEST_BASE="${KEYMAN_ROOT}/web/build/test/headless/" # Ensure the compiled tests are available. @@ -104,6 +104,7 @@ function test-headless() { echo "##teamcity[flowStarted flowId='unit_tests']" fi if [[ -n "${TEST_EXTENSIONS}" ]]; then + # file extension of test files TEST_OPTS+=(--extension "${TEST_EXTENSIONS}") fi diff --git a/web/src/test/auto/headless/README.md b/web/src/test/auto/headless/README.md index e496cf8a2a..58e1a4107f 100644 --- a/web/src/test/auto/headless/README.md +++ b/web/src/test/auto/headless/README.md @@ -5,4 +5,4 @@ under `src`. For example, `src/test/auto/headless/engine/js-processor` are the tests for `src/engine/js-processor` and will be run from -`src/engine/js-processor/build.sh`. +`src/engine/build.sh`. From dbb05e3fcc0b56ed332cb2c67dd9852610c9341d Mon Sep 17 00:00:00 2001 From: Eberhard Beilharz Date: Thu, 11 Dec 2025 17:52:14 +0100 Subject: [PATCH 03/13] refactor(web): use constants instead of hard coded values And some other refactorings. Test-bot: skip --- .../app/browser/src/hardwareEventKeyboard.ts | 40 ++++++++++++------- .../src/main/headless/inputProcessor.ts | 19 ++++----- .../coreKeyboardProcessor.tests.ts | 8 ++-- 3 files changed, 36 insertions(+), 31 deletions(-) diff --git a/web/src/app/browser/src/hardwareEventKeyboard.ts b/web/src/app/browser/src/hardwareEventKeyboard.ts index 7057bd9df9..8564c6a799 100644 --- a/web/src/app/browser/src/hardwareEventKeyboard.ts +++ b/web/src/app/browser/src/hardwareEventKeyboard.ts @@ -14,6 +14,12 @@ type KeyboardState = { baseLayout: string } +const DOM_KEY_LOCATION = { + STANDARD: 0, + LEFT: 1, + RIGHT: 2, +}; + // Important: the following two lines should not cause a compile error if left uncommented. // let dummy1: KeyboardProcessor; // let dummy2: KeyboardState = dummy1; @@ -51,9 +57,9 @@ export function _GetEventKeyCode(e: KeyboardEvent) { * @param {KeyboardEvent} e Event object * @param {KeyboardState} keyboardState Keyboard state object * @param {DeviceSpec} device Device object - * @return {KeyEvent} KeymanWeb KeyEvent object, or null - * for duplicate/spurious events or if - * there is no key code. + * + * @return {KeyEvent} KeymanWeb KeyEvent object, or null for duplicate/spurious + * events or if there is no key code. */ export function preprocessKeyboardEvent(e: KeyboardEvent, keyboardState: KeyboardState, device: DeviceSpec): KeyEvent { if(e.cancelBubble === true) { @@ -104,17 +110,21 @@ export function preprocessKeyboardEvent(e: KeyboardEvent, keyboardState: Keyboar */ let curModState = 0x0000; - curModState |= (e.getModifierState("Shift") ? 0x10 : 0); + curModState |= (e.getModifierState("Shift") ? ModifierKeyConstants.K_SHIFTFLAG : 0); if(e.getModifierState("Control")) { - curModState |= ((e.location != 0 && ctrlEvent) ? - (e.location == 1 ? ModifierKeyConstants.LCTRLFLAG : ModifierKeyConstants.RCTRLFLAG) : // Condition 1 - prevModState & 0x0003 /* LCTRLFLAG | RCTRLFLAG */); // Condition 2 + curModState |= ((e.location != DOM_KEY_LOCATION.STANDARD && ctrlEvent) + ? (e.location == DOM_KEY_LOCATION.LEFT + ? ModifierKeyConstants.LCTRLFLAG + : ModifierKeyConstants.RCTRLFLAG) // Condition 1 + : prevModState & (ModifierKeyConstants.LCTRLFLAG | ModifierKeyConstants.RCTRLFLAG)); // Condition 2 } if(e.getModifierState("Alt")) { - curModState |= ((e.location != 0 && altEvent) ? - (e.location == 1 ? ModifierKeyConstants.LALTFLAG : ModifierKeyConstants.RALTFLAG) : // Condition 1 - prevModState & 0x000C /* LALTFLAG | RALTFLAG */); // Condition 2 + curModState |= ((e.location != DOM_KEY_LOCATION.STANDARD && altEvent) + ? (e.location == DOM_KEY_LOCATION.LEFT + ? ModifierKeyConstants.LALTFLAG + : ModifierKeyConstants.RALTFLAG) // Condition 1 + : prevModState & (ModifierKeyConstants.LALTFLAG | ModifierKeyConstants.RALTFLAG)); // Condition 2 } // Stage 2 - detect state key information. It can be looked up per keypress with no issue. @@ -143,7 +153,7 @@ export function preprocessKeyboardEvent(e: KeyboardEvent, keyboardState: Keyboar curModState &= ~ altGrMask; } // Perform basic filtering for Windows-based ALT_GR emulation on European keyboards. - if(curModState & ModifierKeyConstants.RALTFLAG) { + if((curModState & ModifierKeyConstants.RALTFLAG) != 0) { curModState &= ~ModifierKeyConstants.LCTRLFLAG; } @@ -151,7 +161,7 @@ export function preprocessKeyboardEvent(e: KeyboardEvent, keyboardState: Keyboar const modifierBitmasks = Codes.modifierBitmasks; const activeKeyboard = keyboardState.activeKeyboard; let Lmodifiers: number; - if(activeKeyboard && activeKeyboard.isChiral) { + if(activeKeyboard?.isChiral) { Lmodifiers = curModState & modifierBitmasks.CHIRAL; // Note for future - embedding a kill switch here would facilitate disabling AltGr / Right-alt simulation. @@ -162,9 +172,9 @@ export function preprocessKeyboardEvent(e: KeyboardEvent, keyboardState: Keyboar } else { // No need to sim AltGr here; we don't need chiral ALTs. Lmodifiers = - (curModState & 0x10) | // SHIFT - ((curModState & (ModifierKeyConstants.LCTRLFLAG | ModifierKeyConstants.RCTRLFLAG)) ? 0x20 : 0) | - ((curModState & (ModifierKeyConstants.LALTFLAG | ModifierKeyConstants.RALTFLAG)) ? 0x40 : 0); + (curModState & ModifierKeyConstants.K_SHIFTFLAG) | + ((curModState & (ModifierKeyConstants.LCTRLFLAG | ModifierKeyConstants.RCTRLFLAG)) != 0 ? ModifierKeyConstants.K_CTRLFLAG : 0) | + ((curModState & (ModifierKeyConstants.LALTFLAG | ModifierKeyConstants.RALTFLAG)) != 0 ? ModifierKeyConstants.K_ALTFLAG : 0); } diff --git a/web/src/engine/src/main/headless/inputProcessor.ts b/web/src/engine/src/main/headless/inputProcessor.ts index ffcb282a69..426bd186ed 100644 --- a/web/src/engine/src/main/headless/inputProcessor.ts +++ b/web/src/engine/src/main/headless/inputProcessor.ts @@ -170,24 +170,21 @@ export class InputProcessor { const formFactor = keyEvent.device.formFactor; const fromOSK = keyEvent.isSynthetic; - // The default OSK layout for desktop devices does not include nextlayer info, relying on modifier detection here. + // The default OSK layout for desktop devices does not include nextlayer info, relying on + // modifier detection here. // It's the OSK equivalent to doModifierPress on 'desktop' form factors. - if((formFactor == DeviceSpec.FormFactor.Desktop || !this.activeKeyboard || (this.activeKeyboard instanceof JSKeyboard && this.activeKeyboard.usesDesktopLayoutOnDevice(keyEvent.device))) && fromOSK) { + if((formFactor == DeviceSpec.FormFactor.Desktop || !this.activeKeyboard || (this.activeKeyboard instanceof JSKeyboard && this.activeKeyboard.usesDesktopLayoutOnDevice(keyEvent.device))) && fromOSK && this.keyboardProcessor.selectLayer(keyEvent)) { // If it's a desktop OSK style and this triggers a layer change, // a modifier key was clicked. No output expected, so it's safe to instantly exit. - if(this.keyboardProcessor.selectLayer(keyEvent)) { - return new ProcessorAction(); - } + return new ProcessorAction(); } - // Will handle keystroke-based non-layer change modifier & state keys, mapping them through the physical keyboard's version - // of state management. `doModifierPress` must always run. - if (this.keyboardProcessor.doModifierPress(keyEvent, textStore, !fromOSK)) { + // Will handle keystroke-based non-layer change modifier & state keys, mapping them through + // the physical keyboard's version of state management. `doModifierPress` must always run. + if (this.keyboardProcessor.doModifierPress(keyEvent, textStore, !fromOSK) && !fromOSK) { // If run on a desktop platform, we know that modifier & state key presses may not // produce output, so we may make an immediate return safely. - if(!fromOSK) { - return new ProcessorAction(); - } + return new ProcessorAction(); } // If suggestions exist AND space is pressed, accept the suggestion and do not process the keystroke. diff --git a/web/src/test/auto/headless/engine/core-processor/coreKeyboardProcessor.tests.ts b/web/src/test/auto/headless/engine/core-processor/coreKeyboardProcessor.tests.ts index 42c20db423..f01cba232e 100644 --- a/web/src/test/auto/headless/engine/core-processor/coreKeyboardProcessor.tests.ts +++ b/web/src/test/auto/headless/engine/core-processor/coreKeyboardProcessor.tests.ts @@ -30,12 +30,10 @@ describe('CoreKeyboardProcessor', function () { const item = new KM_Core.instance.km_core_context_item(); if (isMarker) { item.marker = c as number; + } else if (typeof c == 'number') { + item.character = c; } else { - if (typeof (c) == 'number') { - item.character = c; - } else { - item.character = c.codePointAt(0); - } + item.character = c.codePointAt(0); } contextItems.push_back(item); }; From 878b8ab2f024fda00df65e96a65f33f20c3f92d3 Mon Sep 17 00:00:00 2001 From: Eberhard Beilharz Date: Thu, 11 Dec 2025 17:52:39 +0100 Subject: [PATCH 04/13] chore(web): remove `default` from hardware keyboard Test-bot: skip --- web/src/app/browser/src/hardwareEventKeyboard.ts | 2 +- web/src/app/browser/src/keymanEngine.ts | 2 +- web/src/app/browser/src/test-index.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/web/src/app/browser/src/hardwareEventKeyboard.ts b/web/src/app/browser/src/hardwareEventKeyboard.ts index 8564c6a799..22f47544d7 100644 --- a/web/src/app/browser/src/hardwareEventKeyboard.ts +++ b/web/src/app/browser/src/hardwareEventKeyboard.ts @@ -220,7 +220,7 @@ export function preprocessKeyboardEvent(e: KeyboardEvent, keyboardState: Keyboar return processedEvent; } -export default class HardwareEventKeyboard extends HardKeyboardBase { +export class HardwareEventKeyboard extends HardKeyboardBase { private readonly hardDevice: DeviceSpec; // Needed properties & methods: diff --git a/web/src/app/browser/src/keymanEngine.ts b/web/src/app/browser/src/keymanEngine.ts index c5e44512c5..2695cee82c 100644 --- a/web/src/app/browser/src/keymanEngine.ts +++ b/web/src/app/browser/src/keymanEngine.ts @@ -14,7 +14,7 @@ import * as views from './viewsAnchorpoint.js'; import { BrowserConfiguration, BrowserInitOptionDefaults, BrowserInitOptionSpec } from './configuration.js'; import { ContextManager } from './contextManager.js'; import DefaultBrowserRules from './defaultBrowserRules.js'; -import HardwareEventKeyboard from './hardwareEventKeyboard.js'; +import { HardwareEventKeyboard } from './hardwareEventKeyboard.js'; import { FocusStateAPIObject } from './context/focusAssistant.js'; import { PageIntegrationHandlers } from './context/pageIntegrationHandlers.js'; import { LanguageMenu } from './languageMenu.js'; diff --git a/web/src/app/browser/src/test-index.ts b/web/src/app/browser/src/test-index.ts index d094fe7c25..6f010f494e 100644 --- a/web/src/app/browser/src/test-index.ts +++ b/web/src/app/browser/src/test-index.ts @@ -1,6 +1,6 @@ export { BrowserConfiguration, BrowserInitOptionSpec } from './configuration.js'; export { ContextManager, KeyboardCookie } from "./contextManager.js"; -export { preprocessKeyboardEvent, default as HardwareEventKeyboard } from './hardwareEventKeyboard.js'; +export { preprocessKeyboardEvent, HardwareEventKeyboard } from './hardwareEventKeyboard.js'; export { KeymanEngine } from './keymanEngine.js'; export { KeyboardInterface } from './keyboardInterface.js'; From 1b04772bb99869db87c4e736ebbd0e52a68857c6 Mon Sep 17 00:00:00 2001 From: Eberhard Beilharz Date: Fri, 12 Dec 2025 11:31:55 +0100 Subject: [PATCH 05/13] refactor(web): address code review comments --- web/src/engine/src/main/headless/inputProcessor.ts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/web/src/engine/src/main/headless/inputProcessor.ts b/web/src/engine/src/main/headless/inputProcessor.ts index 426bd186ed..f8bc1725ba 100644 --- a/web/src/engine/src/main/headless/inputProcessor.ts +++ b/web/src/engine/src/main/headless/inputProcessor.ts @@ -168,20 +168,21 @@ export class InputProcessor { */ private _processKeyEvent(keyEvent: KeyEvent, textStore: TextStore): ProcessorAction { const formFactor = keyEvent.device.formFactor; - const fromOSK = keyEvent.isSynthetic; // The default OSK layout for desktop devices does not include nextlayer info, relying on // modifier detection here. // It's the OSK equivalent to doModifierPress on 'desktop' form factors. - if((formFactor == DeviceSpec.FormFactor.Desktop || !this.activeKeyboard || (this.activeKeyboard instanceof JSKeyboard && this.activeKeyboard.usesDesktopLayoutOnDevice(keyEvent.device))) && fromOSK && this.keyboardProcessor.selectLayer(keyEvent)) { + if ((formFactor == DeviceSpec.FormFactor.Desktop || !this.activeKeyboard || (this.activeKeyboard instanceof JSKeyboard && this.activeKeyboard.usesDesktopLayoutOnDevice(keyEvent.device))) && keyEvent.isSynthetic) { // If it's a desktop OSK style and this triggers a layer change, // a modifier key was clicked. No output expected, so it's safe to instantly exit. - return new ProcessorAction(); + if (this.keyboardProcessor.selectLayer(keyEvent)) { + return new ProcessorAction(); + } } // Will handle keystroke-based non-layer change modifier & state keys, mapping them through // the physical keyboard's version of state management. `doModifierPress` must always run. - if (this.keyboardProcessor.doModifierPress(keyEvent, textStore, !fromOSK) && !fromOSK) { + if (this.keyboardProcessor.doModifierPress(keyEvent, textStore, !keyEvent.isSynthetic) && !keyEvent.isSynthetic) { // If run on a desktop platform, we know that modifier & state key presses may not // produce output, so we may make an immediate return safely. return new ProcessorAction(); From 4d644cd00e2c28f84f34dde90f8f9c72355d0ef6 Mon Sep 17 00:00:00 2001 From: Eberhard Beilharz Date: Mon, 15 Dec 2025 15:44:42 +0100 Subject: [PATCH 06/13] Apply suggestions from code review Co-authored-by: Marc Durdin --- web/src/engine/src/main/headless/inputProcessor.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/web/src/engine/src/main/headless/inputProcessor.ts b/web/src/engine/src/main/headless/inputProcessor.ts index f8bc1725ba..88168a54f4 100644 --- a/web/src/engine/src/main/headless/inputProcessor.ts +++ b/web/src/engine/src/main/headless/inputProcessor.ts @@ -172,7 +172,11 @@ export class InputProcessor { // The default OSK layout for desktop devices does not include nextlayer info, relying on // modifier detection here. // It's the OSK equivalent to doModifierPress on 'desktop' form factors. - if ((formFactor == DeviceSpec.FormFactor.Desktop || !this.activeKeyboard || (this.activeKeyboard instanceof JSKeyboard && this.activeKeyboard.usesDesktopLayoutOnDevice(keyEvent.device))) && keyEvent.isSynthetic) { + if ((formFactor == DeviceSpec.FormFactor.Desktop || !this.activeKeyboard || + (this.activeKeyboard instanceof JSKeyboard && + this.activeKeyboard.usesDesktopLayoutOnDevice(keyEvent.device))) && + keyEvent.isSynthetic + ) { // If it's a desktop OSK style and this triggers a layer change, // a modifier key was clicked. No output expected, so it's safe to instantly exit. if (this.keyboardProcessor.selectLayer(keyEvent)) { @@ -182,7 +186,8 @@ export class InputProcessor { // Will handle keystroke-based non-layer change modifier & state keys, mapping them through // the physical keyboard's version of state management. `doModifierPress` must always run. - if (this.keyboardProcessor.doModifierPress(keyEvent, textStore, !keyEvent.isSynthetic) && !keyEvent.isSynthetic) { + const wasModifierPress = this.keyboardProcessor.doModifierPress(keyEvent, textStore, !keyEvent.isSynthetic); + if (wasModifierPress && !keyEvent.isSynthetic) { // If run on a desktop platform, we know that modifier & state key presses may not // produce output, so we may make an immediate return safely. return new ProcessorAction(); From e4e76caea76006d853d18711c97805eaa5b0395b Mon Sep 17 00:00:00 2001 From: Eberhard Beilharz Date: Wed, 17 Dec 2025 15:55:52 +0100 Subject: [PATCH 07/13] refactor(web): address code review comment Build-bot: skip Test-bot: skip --- web/src/engine/src/keyboard/keyboards/jsKeyboard.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/src/engine/src/keyboard/keyboards/jsKeyboard.ts b/web/src/engine/src/keyboard/keyboards/jsKeyboard.ts index 4c6631a1a8..50cc7846b9 100644 --- a/web/src/engine/src/keyboard/keyboards/jsKeyboard.ts +++ b/web/src/engine/src/keyboard/keyboards/jsKeyboard.ts @@ -358,7 +358,7 @@ export class JSKeyboard { * * @param {number} command event code (16,17,18) or 0 * @param {TextStore} textStore textStore - * @param {boolean} data 1 or 0 + * @param {boolean} data true o false */ public notify(command: number, textStore: TextStore, data: boolean): void { // I2187 // Good example use case - the Japanese CJK-picker keyboard From 1895a944a2b27747ce14e8879d3b1885c215b072 Mon Sep 17 00:00:00 2001 From: Eberhard Beilharz Date: Thu, 18 Dec 2025 12:48:45 +0100 Subject: [PATCH 08/13] Apply suggestions from code review Co-authored-by: Marc Durdin --- web/src/engine/src/keyboard/keyboards/kmxKeyboard.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/src/engine/src/keyboard/keyboards/kmxKeyboard.ts b/web/src/engine/src/keyboard/keyboards/kmxKeyboard.ts index ba78dc4e01..ff69acfa51 100644 --- a/web/src/engine/src/keyboard/keyboards/kmxKeyboard.ts +++ b/web/src/engine/src/keyboard/keyboards/kmxKeyboard.ts @@ -104,7 +104,7 @@ export class KMXKeyboard { * @param {number} eventCode key code (16-18: Shift, Control or Alt), * or 0 for focus * @param {TextStore} textStore textStore - * @param {number} boolean true for KeyDown or FocusReceived, + * @param {boolean} data true for KeyDown or FocusReceived, * false for KeyUp or FocusLost */ public notify(eventCode: number, textStore: TextStore, data: boolean): void { // I2187 From 72d12622fff76308ec9124396440b9dd1547dee0 Mon Sep 17 00:00:00 2001 From: Eberhard Beilharz Date: Fri, 19 Dec 2025 11:59:32 +0100 Subject: [PATCH 09/13] refactor(web): address code review comment --- web/src/app/browser/src/contextManager.ts | 4 ++-- .../engine/src/js-processor/jsKeyboardProcessor.ts | 4 ++-- web/src/engine/src/keyboard/index.ts | 2 +- web/src/engine/src/keyboard/keyboards/jsKeyboard.ts | 12 +++++++----- .../src/keyboard/keyboards/keyboardLoaderBase.ts | 8 ++++++++ .../engine/src/keyboard/keyboards/kmxKeyboard.ts | 13 +++++++------ 6 files changed, 27 insertions(+), 16 deletions(-) diff --git a/web/src/app/browser/src/contextManager.ts b/web/src/app/browser/src/contextManager.ts index 8004e743d8..30729b34df 100644 --- a/web/src/app/browser/src/contextManager.ts +++ b/web/src/app/browser/src/contextManager.ts @@ -613,7 +613,7 @@ export class ContextManager extends ContextManagerBase { const activeKeyboard = this.activeKeyboard?.keyboard; if(!focusAssistant.restoringFocus) { textStore?.deadkeys().clear(); - activeKeyboard?.notify(0, textStore, true); // I2187 + activeKeyboard?.notify(0, textStore, 1); // I2187 } if(!focusAssistant.restoringFocus && this.mostRecentTextStore != textStore) { @@ -711,7 +711,7 @@ export class ContextManager extends ContextManagerBase { const {activeKeyboard} = this; const {maintainingFocus} = this.focusAssistant; if(!maintainingFocus && activeKeyboard) { - activeKeyboard.keyboard.notify(0, textStore, false); // I2187 + activeKeyboard.keyboard.notify(0, textStore, 0); // I2187 } if(previousTextStore && !this.activeTextStore) { this.emit('textstorechange', null); diff --git a/web/src/engine/src/js-processor/jsKeyboardProcessor.ts b/web/src/engine/src/js-processor/jsKeyboardProcessor.ts index 5b2bd705ac..a42ecc7067 100644 --- a/web/src/engine/src/js-processor/jsKeyboardProcessor.ts +++ b/web/src/engine/src/js-processor/jsKeyboardProcessor.ts @@ -539,7 +539,7 @@ export class JSKeyboardProcessor extends EventEmitter implements Keybo } if(Levent.isModifier) { - this.activeKeyboard.notify(Levent.Lcode, textStore, isKeyDown); + this.activeKeyboard.notify(Levent.Lcode, textStore, isKeyDown ? 1 : 0); // For eventual integration - we bypass an OSK update for physical keystrokes when in touch mode. if(!Levent.device.touchable) { return this._UpdateVKShift(Levent); // I2187 @@ -549,7 +549,7 @@ export class JSKeyboardProcessor extends EventEmitter implements Keybo } if(Levent.LmodifierChange) { - this.activeKeyboard.notify(0, textStore, true); + this.activeKeyboard.notify(0, textStore, 1); if(!Levent.device.touchable) { this._UpdateVKShift(Levent); } diff --git a/web/src/engine/src/keyboard/index.ts b/web/src/engine/src/keyboard/index.ts index c8ea67593f..0e8fc53d3e 100644 --- a/web/src/engine/src/keyboard/index.ts +++ b/web/src/engine/src/keyboard/index.ts @@ -4,7 +4,7 @@ export { JSKeyboard, LayoutState } from "./keyboards/jsKeyboard.js"; export { KeyboardMinimalInterface } from './keyboards/keyboardMinimalInterface.js'; export { KMXKeyboard } from './keyboards/kmxKeyboard.js'; export { KeyboardHarness, KeyboardKeymanGlobal, MinimalCodesInterface, MinimalKeymanGlobal } from "./keyboards/keyboardHarness.js"; -export { Keyboard, KeyboardLoaderBase } from "./keyboards/keyboardLoaderBase.js"; +export { NotifyEventCode, Keyboard, KeyboardLoaderBase } from "./keyboards/keyboardLoaderBase.js"; export { KeyboardLoadErrorBuilder, KeyboardMissingError, KeyboardScriptError, KeyboardDownloadError, InvalidKeyboardError } from './keyboards/keyboardLoadError.js' export { BeepHandler, EventMap, KeyboardProcessor } from "./keyboards/keyboardProcessor.js"; export { diff --git a/web/src/engine/src/keyboard/keyboards/jsKeyboard.ts b/web/src/engine/src/keyboard/keyboards/jsKeyboard.ts index 50cc7846b9..5fd6e06530 100644 --- a/web/src/engine/src/keyboard/keyboards/jsKeyboard.ts +++ b/web/src/engine/src/keyboard/keyboards/jsKeyboard.ts @@ -14,6 +14,7 @@ type TouchLayoutSpec = TouchLayout.TouchLayoutPlatform & { isDefault?: boolean}; import { Version, DeviceSpec } from "keyman/common/web-utils"; import { StateKeyMap } from "./stateKeyMap.js"; +import { NotifyEventCode } from './keyboardLoaderBase.js'; /** * Stores preprocessed properties of a keyboard for quick retrieval later. @@ -356,14 +357,15 @@ export class JSKeyboard { /** * Notifies keyboard of keystroke or other event * - * @param {number} command event code (16,17,18) or 0 - * @param {TextStore} textStore textStore - * @param {boolean} data true o false + * @param {NotifyEventCode} command event code (16,17,18) or 0 + * @param {TextStore} textStore textStore + * @param {number} data 1 for KeyDown or FocusReceived, + * 0 for KeyUp or FocusLost */ - public notify(command: number, textStore: TextStore, data: boolean): void { // I2187 + public notify(command: NotifyEventCode, textStore: TextStore, data: number): void { // I2187 // Good example use case - the Japanese CJK-picker keyboard if(typeof(this.scriptObject['KNS']) == 'function') { - this.scriptObject['KNS'](command, textStore, data ? 1 : 0); + this.scriptObject['KNS'](command, textStore, data); } } diff --git a/web/src/engine/src/keyboard/keyboards/keyboardLoaderBase.ts b/web/src/engine/src/keyboard/keyboards/keyboardLoaderBase.ts index b7fe16dd1d..2d938e159e 100644 --- a/web/src/engine/src/keyboard/keyboards/keyboardLoaderBase.ts +++ b/web/src/engine/src/keyboard/keyboards/keyboardLoaderBase.ts @@ -4,6 +4,14 @@ import { KMXKeyboard } from './kmxKeyboard.js'; import { KeyboardHarness } from "./keyboardHarness.js"; import KeyboardProperties from "./keyboardProperties.js"; import { KeyboardLoadErrorBuilder, StubBasedErrorBuilder, UriBasedErrorBuilder } from './keyboardLoadError.js'; +import { Codes } from '../codes.js'; + +export enum NotifyEventCode { + FocusEvent = 0, + ShiftKey = Codes.keyCodes.K_SHIFT, + ControlKey = Codes.keyCodes.K_CONTROL, + AltKey = Codes.keyCodes.K_ALT, +}; export type KeyboardStub = KeyboardProperties & { filename: string }; export type Keyboard = JSKeyboard | KMXKeyboard; diff --git a/web/src/engine/src/keyboard/keyboards/kmxKeyboard.ts b/web/src/engine/src/keyboard/keyboards/kmxKeyboard.ts index ff69acfa51..1c8123192f 100644 --- a/web/src/engine/src/keyboard/keyboards/kmxKeyboard.ts +++ b/web/src/engine/src/keyboard/keyboards/kmxKeyboard.ts @@ -7,6 +7,7 @@ import { ActiveKey, ActiveSubKey } from './activeLayout.js'; import { StateKeyMap } from './stateKeyMap.js'; import { KeyEvent } from '../keyEvent.js'; import { TextStore } from '../textStore.js'; +import { NotifyEventCode } from './keyboardLoaderBase.js'; /** * Acts as a wrapper class for KMX(+) Keyman keyboards @@ -101,13 +102,13 @@ export class KMXKeyboard { /** * Notifies keyboard of keystroke or other event * - * @param {number} eventCode key code (16-18: Shift, Control or Alt), - * or 0 for focus - * @param {TextStore} textStore textStore - * @param {boolean} data true for KeyDown or FocusReceived, - * false for KeyUp or FocusLost + * @param {NotifyEventCode} eventCode key code (16-18: Shift, Control or Alt), + * or 0 for focus + * @param {TextStore} textStore textStore + * @param {number} data 1 for KeyDown or FocusReceived, + * 0 for KeyUp or FocusLost */ - public notify(eventCode: number, textStore: TextStore, data: boolean): void { // I2187 + public notify(eventCode: NotifyEventCode, textStore: TextStore, data: number): void { // I2187 // TODO-web-core: do we need to support this? (#15290) } From 5b385572270c96f2883848f099a9b7016f919772 Mon Sep 17 00:00:00 2001 From: Eberhard Beilharz Date: Fri, 19 Dec 2025 18:29:39 +0100 Subject: [PATCH 10/13] refactor(web): remove defaults in keyboard-storage - Also renamed `prefixed` and `withoutPrefix` functions to use the name that was aliased on most cases: `toPrefixedKeyboardId` and `toUnprefixedKeyboardId`. - Renamed `ModelManager` class to `ModelCache` which was the name used everywhere except in comments. Part-of: #15292 Test-bot: skip --- .../KMEA/app/src/main/assets/android-host.js | 2 +- .../Contents/Resources/ios-host.js | 2 +- .../src/keyboard-storage/cloud/index.ts | 4 +-- .../src/keyboard-storage/cloud/queryEngine.ts | 6 ++-- .../cloud/requesterInterface.ts | 2 +- .../src/keyboard-storage/domCloudRequester.ts | 4 +-- web/src/engine/src/keyboard-storage/index.ts | 14 ++++----- .../keyboard-storage/keyboardRequisitioner.ts | 4 +-- .../src/keyboard-storage/keyboardStub.ts | 2 +- .../engine/src/keyboard-storage/modelCache.ts | 2 +- .../keyboard-storage/stubAndKeyboardCache.ts | 30 ++++++++----------- .../src/main/headless/languageProcessor.ts | 2 +- .../resources/loader/nodeCloudRequester.ts | 4 +-- 13 files changed, 37 insertions(+), 41 deletions(-) diff --git a/android/KMEA/app/src/main/assets/android-host.js b/android/KMEA/app/src/main/assets/android-host.js index ad440d493a..48672c0572 100644 --- a/android/KMEA/app/src/main/assets/android-host.js +++ b/android/KMEA/app/src/main/assets/android-host.js @@ -247,7 +247,7 @@ function deregisterModel(modelID) { } function enableSuggestions(model, suggestionType) { - // Set the options first so that KMW's ModelManager can properly handle model enablement states + // Set the options first so that KMW's ModelCache can properly handle model enablement states // the moment we actually register the new model. // Use console_debug console_debug('enableSuggestions(model, maySuggest='+suggestionType+')'); diff --git a/ios/engine/KMEI/KeymanEngine/resources/Keyman.bundle/Contents/Resources/ios-host.js b/ios/engine/KMEI/KeymanEngine/resources/Keyman.bundle/Contents/Resources/ios-host.js index b390b438de..25af94b5bd 100644 --- a/ios/engine/KMEI/KeymanEngine/resources/Keyman.bundle/Contents/Resources/ios-host.js +++ b/ios/engine/KMEI/KeymanEngine/resources/Keyman.bundle/Contents/Resources/ios-host.js @@ -327,7 +327,7 @@ function toHex(theString) { } function enableSuggestions(model, mayPredict, mayCorrect) { - // Set the options first so that KMW's ModelManager can properly handle model enablement states + // Set the options first so that KMW's ModelCache can properly handle model enablement states // the moment we actually register the new model. keyman.core.languageProcessor.mayPredict = mayPredict; keyman.core.languageProcessor.mayCorrect = mayCorrect; diff --git a/web/src/engine/src/keyboard-storage/cloud/index.ts b/web/src/engine/src/keyboard-storage/cloud/index.ts index eb477cacec..2ab752bcde 100644 --- a/web/src/engine/src/keyboard-storage/cloud/index.ts +++ b/web/src/engine/src/keyboard-storage/cloud/index.ts @@ -1,2 +1,2 @@ -export { CloudQueryResult, default as QueryEngine } from './queryEngine.js'; -export { default as RequesterInterface } from './requesterInterface.js'; +export { CloudQueryResult, CloudQueryEngine as QueryEngine } from './queryEngine.js'; +export { CloudRequesterInterface as RequesterInterface } from './requesterInterface.js'; diff --git a/web/src/engine/src/keyboard-storage/cloud/queryEngine.ts b/web/src/engine/src/keyboard-storage/cloud/queryEngine.ts index e94aaca1bd..fe76a3fcb6 100644 --- a/web/src/engine/src/keyboard-storage/cloud/queryEngine.ts +++ b/web/src/engine/src/keyboard-storage/cloud/queryEngine.ts @@ -2,9 +2,9 @@ import { EventEmitter } from 'eventemitter3'; import { PathConfiguration } from 'keyman/engine/interfaces'; -import { default as KeyboardStub, ErrorStub, KeyboardAPISpec } from '../keyboardStub.js'; +import { KeyboardStub, ErrorStub, KeyboardAPISpec } from '../keyboardStub.js'; import { LanguageAPIPropertySpec, ManagedPromise, Version } from 'keyman/engine/keyboard'; -import CloudRequesterInterface from './requesterInterface.js'; +import { CloudRequesterInterface } from './requesterInterface.js'; // For when the API call straight-up times out. export const CLOUD_TIMEOUT_ERR = "The Cloud API request timed out."; @@ -56,7 +56,7 @@ interface EventMap { 'unboundregister': (registration: ReturnType) => void } -export default class CloudQueryEngine extends EventEmitter { +export class CloudQueryEngine extends EventEmitter { private cloudResolutionPromises: Map> = new Map(); private _languageListPromise: ManagedPromise; diff --git a/web/src/engine/src/keyboard-storage/cloud/requesterInterface.ts b/web/src/engine/src/keyboard-storage/cloud/requesterInterface.ts index 6c612d9cb2..b1c84b6c05 100644 --- a/web/src/engine/src/keyboard-storage/cloud/requesterInterface.ts +++ b/web/src/engine/src/keyboard-storage/cloud/requesterInterface.ts @@ -1,6 +1,6 @@ import { ManagedPromise } from 'keyman/engine/keyboard'; -export default interface CloudRequesterInterface { +export interface CloudRequesterInterface { request(query: string): { promise: ManagedPromise, queryId: number diff --git a/web/src/engine/src/keyboard-storage/domCloudRequester.ts b/web/src/engine/src/keyboard-storage/domCloudRequester.ts index a39310ad8a..4901b4be15 100644 --- a/web/src/engine/src/keyboard-storage/domCloudRequester.ts +++ b/web/src/engine/src/keyboard-storage/domCloudRequester.ts @@ -1,8 +1,8 @@ import { ManagedPromise } from 'keyman/engine/keyboard'; -import CloudRequesterInterface from './cloud/requesterInterface.js'; +import { CloudRequesterInterface } from './cloud/requesterInterface.js'; import { CLOUD_MALFORMED_OBJECT_ERR, CLOUD_TIMEOUT_ERR, CLOUD_STUB_REGISTRATION_ERR } from './cloud/queryEngine.js'; -export default class DOMCloudRequester implements CloudRequesterInterface { +export class DOMCloudRequester implements CloudRequesterInterface { private readonly fileLocal: boolean; constructor(fileLocal: boolean = false) { diff --git a/web/src/engine/src/keyboard-storage/index.ts b/web/src/engine/src/keyboard-storage/index.ts index 80073844db..08f44191c0 100644 --- a/web/src/engine/src/keyboard-storage/index.ts +++ b/web/src/engine/src/keyboard-storage/index.ts @@ -2,15 +2,15 @@ export { ErrorStub, type KeyboardAPISpec, - default as KeyboardStub, + KeyboardStub, mergeAndResolveStubPromises, RawKeyboardStub, REGIONS, REGION_CODES } from './keyboardStub.js'; -export { default as StubAndKeyboardCache, toPrefixedKeyboardId, toUnprefixedKeyboardId } from './stubAndKeyboardCache.js'; -export { CloudQueryResult, default as CloudQueryEngine } from './cloud/queryEngine.js'; -export { default as CloudRequesterInterface } from './cloud/requesterInterface.js'; -export { default as KeyboardRequisitioner } from './keyboardRequisitioner.js'; -export { default as ModelCache } from './modelCache.js'; -export { default as DOMCloudRequester } from './domCloudRequester.js'; +export { StubAndKeyboardCache, toPrefixedKeyboardId, toUnprefixedKeyboardId } from './stubAndKeyboardCache.js'; +export { CloudQueryResult, CloudQueryEngine } from './cloud/queryEngine.js'; +export { CloudRequesterInterface } from './cloud/requesterInterface.js'; +export { KeyboardRequisitioner } from './keyboardRequisitioner.js'; +export { ModelCache } from './modelCache.js'; +export { DOMCloudRequester } from './domCloudRequester.js'; \ No newline at end of file diff --git a/web/src/engine/src/keyboard-storage/keyboardRequisitioner.ts b/web/src/engine/src/keyboard-storage/keyboardRequisitioner.ts index b629d2abbb..d1da9ade89 100644 --- a/web/src/engine/src/keyboard-storage/keyboardRequisitioner.ts +++ b/web/src/engine/src/keyboard-storage/keyboardRequisitioner.ts @@ -17,7 +17,7 @@ import { mergeAndResolveStubPromises, toUnprefixedKeyboardId as unprefixed } from "./index.js"; -import { default as CloudRequesterInterface } from "./cloud/requesterInterface.js"; +import { CloudRequesterInterface } from "./cloud/requesterInterface.js"; import { rejectErrorStubs } from "./keyboardStub.js"; class CloudRequestEntry { @@ -89,7 +89,7 @@ function isUniqueRequest(cache: StubAndKeyboardCache, cloudList: {id: string, la }; // TODO: Move to the keyboard-cache child project - we can test it headlessly there! -export default class KeyboardRequisitioner { +export class KeyboardRequisitioner { readonly cache: StubAndKeyboardCache; readonly cloudQueryEngine: CloudQueryEngine; readonly pathConfig: PathConfiguration; diff --git a/web/src/engine/src/keyboard-storage/keyboardStub.ts b/web/src/engine/src/keyboard-storage/keyboardStub.ts index 5f19d1d783..17877dcfe8 100644 --- a/web/src/engine/src/keyboard-storage/keyboardStub.ts +++ b/web/src/engine/src/keyboard-storage/keyboardStub.ts @@ -48,7 +48,7 @@ function configureFilePathing(path: string, configurationBasePath: string) { } } -export default class KeyboardStub extends KeyboardProperties { +export class KeyboardStub extends KeyboardProperties { KR: string; KRC: string; KF: string; diff --git a/web/src/engine/src/keyboard-storage/modelCache.ts b/web/src/engine/src/keyboard-storage/modelCache.ts index 6f4b39999a..1b01b45d57 100644 --- a/web/src/engine/src/keyboard-storage/modelCache.ts +++ b/web/src/engine/src/keyboard-storage/modelCache.ts @@ -1,6 +1,6 @@ import { ModelSpec } from 'keyman/engine/interfaces'; -export default class ModelManager { +export class ModelCache { // Tracks registered models by ID. private registeredModels: {[id: string]: ModelSpec} = {}; diff --git a/web/src/engine/src/keyboard-storage/stubAndKeyboardCache.ts b/web/src/engine/src/keyboard-storage/stubAndKeyboardCache.ts index 1f26759416..1537a8cc52 100644 --- a/web/src/engine/src/keyboard-storage/stubAndKeyboardCache.ts +++ b/web/src/engine/src/keyboard-storage/stubAndKeyboardCache.ts @@ -1,11 +1,11 @@ import { type Keyboard, JSKeyboard, KeyboardLoaderBase as KeyboardLoader, KMXKeyboard } from "keyman/engine/keyboard"; import { EventEmitter } from "eventemitter3"; -import KeyboardStub from "./keyboardStub.js"; +import { KeyboardStub } from "./keyboardStub.js"; const KEYBOARD_PREFIX = "Keyboard_"; -function prefixed(text: string) { +export function toPrefixedKeyboardId(text: string) { if(!text.startsWith(KEYBOARD_PREFIX)) { return KEYBOARD_PREFIX + text; } else { @@ -13,9 +13,7 @@ function prefixed(text: string) { } } -export {prefixed as toPrefixedKeyboardId}; - -function withoutPrefix(text: string) { +export function toUnprefixedKeyboardId(text: string) { if(text.startsWith(KEYBOARD_PREFIX)) { return text.substring(KEYBOARD_PREFIX.length); } else { @@ -23,8 +21,6 @@ function withoutPrefix(text: string) { } } -export {withoutPrefix as toUnprefixedKeyboardId}; - interface EventMap { /** * Indicates that the specified stub has just been registered within the cache. @@ -41,7 +37,7 @@ interface EventMap { keyboardadded: (keyboard: Keyboard) => void; } -export default class StubAndKeyboardCache extends EventEmitter { +export class StubAndKeyboardCache extends EventEmitter { private stubSetTable: Record> = {}; private keyboardTable: Record> = {}; @@ -70,7 +66,7 @@ export default class StubAndKeyboardCache extends EventEmitter { if(!keyboardID) { return null; } - const entry = this.keyboardTable[prefixed(keyboardID)]; + const entry = this.keyboardTable[toPrefixedKeyboardId(keyboardID)]; // Unit testing may 'trip up' in the DOM, as bundled versions of a class from one bundled // module will fail against an `instanceof` expecting the version bundled in a second. @@ -123,7 +119,7 @@ export default class StubAndKeyboardCache extends EventEmitter { } addKeyboard(keyboard: Keyboard) { - const keyboardID = prefixed(keyboard.id); + const keyboardID = toPrefixedKeyboardId(keyboard.id); this.keyboardTable[keyboardID] = keyboard; this.emit('keyboardadded', keyboard); @@ -138,7 +134,7 @@ export default class StubAndKeyboardCache extends EventEmitter { throw new Error("Keyboard ID must be specified"); } - keyboardID = prefixed(keyboardID); + keyboardID = toPrefixedKeyboardId(keyboardID); const cachedEntry = this.keyboardTable[keyboardID]; return cachedEntry instanceof Promise; @@ -153,7 +149,7 @@ export default class StubAndKeyboardCache extends EventEmitter { throw new Error("Cannot load keyboards; this cache was configured without a loader"); } - keyboardID = prefixed(keyboardID); + keyboardID = toPrefixedKeyboardId(keyboardID); const cachedEntry = this.keyboardTable[keyboardID]; if(cachedEntry instanceof JSKeyboard) { @@ -164,11 +160,11 @@ export default class StubAndKeyboardCache extends EventEmitter { const stub = this.getStub(keyboardID, null); if(!stub) { - throw new Error(`No stub for ${withoutPrefix(keyboardID)} has been registered`); + throw new Error(`No stub for ${toUnprefixedKeyboardId(keyboardID)} has been registered`); } if(!stub.filename) { - throw new Error(`The registered stub for ${withoutPrefix(keyboardID)} lacks a path to the main keyboard file`); + throw new Error(`The registered stub for ${toUnprefixedKeyboardId(keyboardID)} lacks a path to the main keyboard file`); } const promise = this.keyboardLoader.loadKeyboardFromStub(stub); @@ -189,7 +185,7 @@ export default class StubAndKeyboardCache extends EventEmitter { } addStub(stub: KeyboardStub) { - const keyboardID = prefixed(stub.KI); + const keyboardID = toPrefixedKeyboardId(stub.KI); const stubTable = this.stubSetTable[keyboardID] = this.stubSetTable[keyboardID] ?? {}; stubTable[stub.KLC] = stub; @@ -213,7 +209,7 @@ export default class StubAndKeyboardCache extends EventEmitter { } if(keyboardID) { - keyboardID = prefixed(keyboardID); + keyboardID = toPrefixedKeyboardId(keyboardID); } const stubTable = this.stubSetTable[keyboardID] ?? {}; @@ -238,7 +234,7 @@ export default class StubAndKeyboardCache extends EventEmitter { * If `false`, only forgets the metadata (stubs). */ forgetKeyboard(keyboard: string | JSKeyboard, purge: boolean = false) { - const id: string = (keyboard instanceof JSKeyboard) ? keyboard.id : prefixed(keyboard); + const id: string = (keyboard instanceof JSKeyboard) ? keyboard.id : toPrefixedKeyboardId(keyboard); if(this.stubSetTable[id]) { delete this.stubSetTable[id]; diff --git a/web/src/engine/src/main/headless/languageProcessor.ts b/web/src/engine/src/main/headless/languageProcessor.ts index bad68b5d2b..589bed6b2e 100644 --- a/web/src/engine/src/main/headless/languageProcessor.ts +++ b/web/src/engine/src/main/headless/languageProcessor.ts @@ -360,7 +360,7 @@ export class LanguageProcessor extends EventEmitter { /** * Retrieves the context and output state of KMW immediately before the prediction with * token `id` was generated. Must correspond to a 'recent' one, as only so many are stored - * in `ModelManager`'s history buffer. + * in `ModelCache`'s history buffer. * @param id A unique identifier corresponding to a recent `Transcription`. * @returns The matching `Transcription`, or `null` none is found. */ diff --git a/web/src/test/auto/resources/loader/nodeCloudRequester.ts b/web/src/test/auto/resources/loader/nodeCloudRequester.ts index ae1b57e14d..3b4e923b5c 100644 --- a/web/src/test/auto/resources/loader/nodeCloudRequester.ts +++ b/web/src/test/auto/resources/loader/nodeCloudRequester.ts @@ -1,10 +1,10 @@ import { ManagedPromise } from 'keyman/engine/keyboard'; -import CloudRequesterInterface from '../../../../engine/src/keyboard-storage/cloud/requesterInterface.js'; +import { CloudRequesterInterface } from '../../../../engine/src/keyboard-storage/cloud/requesterInterface.js'; import { CLOUD_TIMEOUT_ERR, CLOUD_STUB_REGISTRATION_ERR, CloudQueryResult, - default as CloudQueryEngine + CloudQueryEngine } from '../../../../engine/src/keyboard-storage/cloud/queryEngine.js'; import fs from 'node:fs'; From 0dbbcc452ea3827772ef20d34d5bb07ac29731fd Mon Sep 17 00:00:00 2001 From: Eberhard Beilharz Date: Fri, 19 Dec 2025 10:07:29 +0100 Subject: [PATCH 11/13] maint(web): escape single quotes in TC service messages This fixes a problem where TC shows an error because it can't find the end of the service message for a failed test. Single quotes (and some other characters) have to be escaped with `|'` inside of a message. Build-bot: skip:all build:web Test-bot: skip --- common/test/resources/playwright-TC-reporter.ts | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/common/test/resources/playwright-TC-reporter.ts b/common/test/resources/playwright-TC-reporter.ts index f550a48d2c..8cbc0967e7 100644 --- a/common/test/resources/playwright-TC-reporter.ts +++ b/common/test/resources/playwright-TC-reporter.ts @@ -62,6 +62,15 @@ class TestNode { TestNode.OpenNodes.push(this.id); } + private escape(message: string): string { + // TeamCity escaping rules for: ' | [ ] \n \r \uNNNN + // See: https://www.jetbrains.com/help/teamcity/service-messages.html#Escaped+Values + return message?.replace(/['|\[\]]/g, (matched) => `|${matched}`) + .replace(/\n/g, '|n') + .replace(/\r/g, '|r') + .replace(/[\u0080-\uFFFF]/g, c => `|0x${c.charCodeAt(0).toString(16).padStart(4, '0')}`) ?? ''; + } + private getTestResult(result: TestResult): { msgTitle: string, details: string } { if (!result) { return null; @@ -72,9 +81,9 @@ class TestNode { case 'failed': case 'interrupted': case 'timedOut': - return { msgTitle: 'testFailed', details: `message='${result.error?.message}' details='${result.error?.value ?? result.error?.cause}'` }; + return { msgTitle: 'testFailed', details: `message='${this.escape(result.error?.message)}' details='${this.escape(result.error?.value ?? result.error?.cause)}'` }; case 'skipped': - return { msgTitle: 'testIgnored', details: `message='${result.annotations?.toString() ?? ''}'` }; + return { msgTitle: 'testIgnored', details: `message='${this.escape(result.annotations?.toString()) ?? ''}'` }; } } From 5f2e72749a13f0a370efcd2af3931502013899ab Mon Sep 17 00:00:00 2001 From: Eberhard Beilharz Date: Tue, 6 Jan 2026 17:43:27 +0100 Subject: [PATCH 12/13] test(web): address code review comments --- web/src/test/auto/e2e/baseline/baseline.tests.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/web/src/test/auto/e2e/baseline/baseline.tests.ts b/web/src/test/auto/e2e/baseline/baseline.tests.ts index 8a9994fb04..489927762b 100644 --- a/web/src/test/auto/e2e/baseline/baseline.tests.ts +++ b/web/src/test/auto/e2e/baseline/baseline.tests.ts @@ -58,7 +58,7 @@ const testsToFix = { 'k_037___options___double_reset.kmn', 'k_039___generic_ctrlalt.kmn', 'k_049___enter_invalidates_context.kmn', - 'k_055___deadkey_cancelled_by_arrow.kmn', + 'k_055___deadkey_cancelled_by_arrow.kmn', // Keyman Engine for Web does not interpret arrow keys - #15397 ], // TODO: fix these tests (#15342) '.js': [ @@ -85,7 +85,7 @@ const testsToFix = { 'k_049___enter_invalidates_context.kmn', 'k_050___nul_and_context.kmn', // js only 'k_052___nul_and_index.kmn', // js only - 'k_055___deadkey_cancelled_by_arrow.kmn', + 'k_055___deadkey_cancelled_by_arrow.kmn', // Keyman Engine for Web does not interpret arrow keys - #15397 ] }; From f65a752b4784c99c18ea77d7b268bf7c2f41bc56 Mon Sep 17 00:00:00 2001 From: Eberhard Beilharz Date: Tue, 6 Jan 2026 18:19:07 +0100 Subject: [PATCH 13/13] test(web): address code review comments --- common/test/keyboards/baseline/README.md | 18 ++++++++++++++++++ core/tests/unit/kmx/meson.build | 2 +- .../test/auto/e2e/baseline/baseline.tests.ts | 4 ---- 3 files changed, 19 insertions(+), 5 deletions(-) diff --git a/common/test/keyboards/baseline/README.md b/common/test/keyboards/baseline/README.md index 7894490d9b..090fdcbd5b 100644 --- a/common/test/keyboards/baseline/README.md +++ b/common/test/keyboards/baseline/README.md @@ -26,3 +26,21 @@ The keyboards can be built with: This builds the keyboards with debug information and no compiler version embedded. + +## Grouping of the test fixtures + +Fixtures that test similar functionality are roughly grouped together. +There is some overlap between different groups, so this was done +mainly by test name. + +| Name | Test group | +|----------|------------------------------------------| +| k_00xx_* | Tests that didn't fit in any other group | +| k_01xx_* | Basic rules | +| k_02xx_* | RALT | +| k_03xx_* | deadkeys | +| k_04xx_* | Using multiple groups | +| k_05xx_* | Options | +| k_06xx_* | System stores | +| k_07xx_* | Caps related tests | +| k_08xx_* | Context related | diff --git a/core/tests/unit/kmx/meson.build b/core/tests/unit/kmx/meson.build index a3af0a741e..8193fe5046 100644 --- a/core/tests/unit/kmx/meson.build +++ b/core/tests/unit/kmx/meson.build @@ -86,7 +86,7 @@ tests = [ 'k_0810___nul_and_index', 'k_0811___if_and_index', 'k_0812___nul_and_contextex', - # Skipped: 'k_0813___deadkey_cancelled_by_arrow', + # TODO-web-core: Skipped: 'k_0813___deadkey_cancelled_by_arrow', ] diff --git a/web/src/test/auto/e2e/baseline/baseline.tests.ts b/web/src/test/auto/e2e/baseline/baseline.tests.ts index 38e45bb8b1..630330d156 100644 --- a/web/src/test/auto/e2e/baseline/baseline.tests.ts +++ b/web/src/test/auto/e2e/baseline/baseline.tests.ts @@ -63,12 +63,8 @@ const testsToFix = { // TODO: fix these tests (#15342) '.js': [ 'k_0000___null_keyboard.kmn', - // 'k_0103___vkey_input__shift_ctrl_.kmn', 'k_0104___vkey_input__ctrl_alt_.kmn', - // 'k_0105___vkey_input__ctrl_alt_2_.kmn', - // 'k_0200___ralt.kmn', 'k_0203___generic_ctrlalt.kmn', - // 'k_0400___groups_and_virtual_keys.kmn', 'k_0501___options_with_preset.kmn', 'k_0503___options_with_save_and_preset.kmn', 'k_0504___options_with_reset.kmn',