From 35ea65cde132c63eca4b1fba1fcef719ba98486d Mon Sep 17 00:00:00 2001 From: Eberhard Beilharz Date: Mon, 20 Apr 2026 10:59:55 +0200 Subject: [PATCH 1/2] feat(web): get list of store names from stores defined in core This implements `CoreKeyboardInterface.loadSerializedOptions` by getting the list of all variable store names from Core and then trying to load these. Also change `VariableStoreSerializer.loadStore` signature so that it can return `undefined`. This allows to return only those options from `loadSerializedOptions` that have a non-default value, i.e. were serialized before. Build-bot: skip build:web Test-bot: skip --- web/docs/internal/gestures/index.md | 5 +- .../core-processor/coreKeyboardInterface.ts | 41 ++++--- web/src/engine/src/core-processor/index.ts | 9 ++ web/src/engine/src/keyboard/variableStore.ts | 2 +- .../src/main/variableStoreCookieSerializer.ts | 4 +- .../test/auto/e2e/baseline/baseline.tests.ts | 7 -- .../variableStoreTestSerializer.ts | 4 +- .../coreKeyboardInterface.tests.ts | 113 ++++++++++++++++++ 8 files changed, 154 insertions(+), 31 deletions(-) create mode 100644 web/src/test/auto/headless/engine/core-processor/coreKeyboardInterface.tests.ts diff --git a/web/docs/internal/gestures/index.md b/web/docs/internal/gestures/index.md index 69e8bacd4d..4a39261a44 100644 --- a/web/docs/internal/gestures/index.md +++ b/web/docs/internal/gestures/index.md @@ -18,7 +18,8 @@ Keyman configures two separate instances of this gesture engine - one for main k ---- -For the main keyboard implementation of Keyman Engine for Web, the full set of Keyman gesture components is defined in `web/src/engine/osk/src/input/gestures/specsForLayout.ts`. +For the main keyboard implementation of Keyman Engine for Web, the full set of Keyman gesture +components is defined in `web/src/engine/src/osk/input/gestures/specsForLayout.ts`. ### Longpresses @@ -44,4 +45,4 @@ Components: 1. The user holds down on a modipress-friendly key 2. Engine alt-state: a different set of initial gesture components is activated and used for newly-starting concurrent gestures -3. Release of the modipressed key \ No newline at end of file +3. Release of the modipressed key diff --git a/web/src/engine/src/core-processor/coreKeyboardInterface.ts b/web/src/engine/src/core-processor/coreKeyboardInterface.ts index fbee9956e5..e7e8c2c975 100644 --- a/web/src/engine/src/core-processor/coreKeyboardInterface.ts +++ b/web/src/engine/src/core-processor/coreKeyboardInterface.ts @@ -1,13 +1,17 @@ /* * Keyman is copyright (C) SIL Global. MIT License. */ -import { KM_Core, km_core_option_item } from 'keyman/engine/core-adapter'; +import { KM_Core, km_core_option_item, KM_CORE_OPTION_SCOPE } from 'keyman/engine/core-adapter'; import { KeyboardMinimalInterface, Keyboard, VariableStoreSerializer, KMXKeyboard } from 'keyman/engine/keyboard'; +import { toPrefixedKeyboardId } from 'keyman/engine/keyboard-storage'; export class CoreKeyboardInterface implements KeyboardMinimalInterface { private _activeKeyboard: Keyboard; - public constructor(public variableStoreSerializer: VariableStoreSerializer) { + public constructor(public readonly variableStoreSerializer: VariableStoreSerializer) { + if (!variableStoreSerializer) { + throw new Error('variableStoreSerializer is required'); + } } public get activeKeyboard(): Keyboard { @@ -15,33 +19,36 @@ export class CoreKeyboardInterface implements KeyboardMinimalInterface { } public set activeKeyboard(keyboard: Keyboard) { this._activeKeyboard = keyboard; - const options = this.loadSerializedOptions(keyboard); + const options = this.loadSerializedOptions(); if (options.length > 0) { - const kmxKeyboard = this._activeKeyboard as KMXKeyboard; - KM_Core.instance.state_options_update(kmxKeyboard.state, options); + KM_Core.instance.state_options_update(this.KmxKeyboard.state, options); } } + private get KmxKeyboard(): KMXKeyboard { + return this._activeKeyboard as KMXKeyboard; + } - private loadSerializedOptions(keyboard: Keyboard): km_core_option_item[] { - // TODO-WEB-CORE: use km_core_keyboard_get_attrs to get list of all variable - // store names and then iterate through those rather than reading from - // cookie props + private loadSerializedOptions(): km_core_option_item[] { const options: km_core_option_item[] = []; - /*const stores = this.variableStoreSerializer.findStores(toPrefixedKeyboardId(keyboard.id)); - for (const store of stores) { - for (const key in store) { - if (store.hasOwnProperty(key)) { + const attrs = KM_Core.instance.keyboard_get_attrs(this.KmxKeyboard.keyboard); + const prefixedKeyboardId = toPrefixedKeyboardId(this.KmxKeyboard.id); + const defaultOptions = attrs.object.default_options as km_core_option_item[]; + for (const defaultOption of defaultOptions) { + if (defaultOption.scope == KM_CORE_OPTION_SCOPE.OPT_KEYBOARD) { + const savedValue = this.variableStoreSerializer.loadStore(prefixedKeyboardId, defaultOption.key); + if (savedValue !== undefined) { const item: km_core_option_item = { - key: key, - value: store[key], - scope: KM_CORE_OPTION_SCOPE.OPT_KEYBOARD + key: defaultOption.key, + value: savedValue, + scope: defaultOption.scope }; options.push(item); } } - }*/ + } + attrs.delete(); return options; } } diff --git a/web/src/engine/src/core-processor/index.ts b/web/src/engine/src/core-processor/index.ts index 3376653ad8..9f746199a7 100644 --- a/web/src/engine/src/core-processor/index.ts +++ b/web/src/engine/src/core-processor/index.ts @@ -1 +1,10 @@ +import { CoreKeyboardInterface } from './coreKeyboardInterface.js'; + export { CoreKeyboardProcessor } from './coreKeyboardProcessor.js'; + +/** + * these are exported only for unit tests, do not use + */ +export const unitTestEndPoints = { + CoreKeyboardInterface, +}; diff --git a/web/src/engine/src/keyboard/variableStore.ts b/web/src/engine/src/keyboard/variableStore.ts index 8d78c19da8..a576135a9b 100644 --- a/web/src/engine/src/keyboard/variableStore.ts +++ b/web/src/engine/src/keyboard/variableStore.ts @@ -7,6 +7,6 @@ export type VariableStores = { [name: string]: string; }; export interface VariableStoreSerializer { - loadStore(keyboardID: string, storeName: string): string; + loadStore(keyboardID: string, storeName: string): string | undefined; saveStore(keyboardID: string, storeName: string, storeValue: string): void; } diff --git a/web/src/engine/src/main/variableStoreCookieSerializer.ts b/web/src/engine/src/main/variableStoreCookieSerializer.ts index c17e29140c..045cbba9bd 100644 --- a/web/src/engine/src/main/variableStoreCookieSerializer.ts +++ b/web/src/engine/src/main/variableStoreCookieSerializer.ts @@ -6,11 +6,11 @@ export class VariableStoreCookieSerializer implements VariableStoreSerializer { return `KeymanWeb_${keyboardID}_Option_${storeName}`; } - public loadStore(keyboardID: string, storeName: string): string { + public loadStore(keyboardID: string, storeName: string): string | undefined { // Note, for historical reasons, we pass a VariableStores object into the // cookie serializer but expect only a single value to be loaded const storeCookieSerializer = new CookieSerializer(this.getStoreCookieName(keyboardID, storeName)); - return storeCookieSerializer.load(decodeURIComponent)[storeName]; + return storeCookieSerializer.load(decodeURIComponent)[storeName] ?? undefined; } public saveStore(keyboardID: string, storeName: string, storeValue: string) { diff --git a/web/src/test/auto/e2e/baseline/baseline.tests.ts b/web/src/test/auto/e2e/baseline/baseline.tests.ts index 72e5862946..527099e6c3 100644 --- a/web/src/test/auto/e2e/baseline/baseline.tests.ts +++ b/web/src/test/auto/e2e/baseline/baseline.tests.ts @@ -47,13 +47,6 @@ const testsToFix = { 'k_0202___alt.kmn', 'k_0203___generic_ctrlalt.kmn', 'k_0400___groups_and_virtual_keys.kmn', // kmx only - 'k_0501___options_with_preset.kmn', // variable stores initialization work to do with keyboard_attrs - 'k_0503___options_with_save_and_preset.kmn', // variable stores initialization work to do with keyboard_attrs - 'k_0504___options_with_reset.kmn', // variable stores initialization work to do with keyboard_attrs - 'k_0505___options_double_set_reset.kmn', // variable stores initialization work to do with keyboard_attrs - 'k_0506___options_double_set_staged.kmn', // variable stores initialization work to do with keyboard_attrs - 'k_0507___options___double_reset_staged.kmn', // variable stores initialization work to do with keyboard_attrs - 'k_0508___options___double_reset.kmn', // variable stores initialization work to do with keyboard_attrs 'k_0600___system_stores.kmn', 'k_0601___system_stores_2.kmn', 'k_0700___caps_lock.kmn', diff --git a/web/src/test/auto/headless-resources/variableStoreTestSerializer.ts b/web/src/test/auto/headless-resources/variableStoreTestSerializer.ts index 1a7c362213..db36035138 100644 --- a/web/src/test/auto/headless-resources/variableStoreTestSerializer.ts +++ b/web/src/test/auto/headless-resources/variableStoreTestSerializer.ts @@ -10,8 +10,8 @@ export class VariableStoreTestSerializer implements VariableStoreSerializer { return `KeymanWeb_${keyboardID}_Option_${storeName}`; } - loadStore(keyboardID: string, storeName: string): string { - return this.stores[this.getStoreName(keyboardID, storeName)] ?? ''; + loadStore(keyboardID: string, storeName: string): string | undefined { + return this.stores[this.getStoreName(keyboardID, storeName)] ?? undefined; } saveStore(keyboardID: string, storeName: string, storeValue: string): void { diff --git a/web/src/test/auto/headless/engine/core-processor/coreKeyboardInterface.tests.ts b/web/src/test/auto/headless/engine/core-processor/coreKeyboardInterface.tests.ts new file mode 100644 index 0000000000..2b95f06360 --- /dev/null +++ b/web/src/test/auto/headless/engine/core-processor/coreKeyboardInterface.tests.ts @@ -0,0 +1,113 @@ +/* + * Keyman is copyright (C) SIL Global. MIT License. + */ + +import { assert } from 'chai'; +import sinon from 'sinon'; +import { unitTestEndPoints } from 'keyman/engine/core-processor'; +import { VariableStoreTestSerializer } from 'keyman/test/headless-resources'; +import { KM_Core, KM_CORE_STATUS, KM_CORE_OPTION_SCOPE, type km_core_option_item } from 'keyman/engine/core-adapter'; +import { coreurl } from '../loadKeyboardHelper.js'; + +describe('CoreKeyboardInterface tests', function () { + let sandbox: sinon.SinonSandbox; + const mockKeyboard = { + keyboard: {} as any, + id: 'test_keyboard', + state: {} as any + }; + + beforeEach(async function () { + sandbox = sinon.createSandbox(); + await KM_Core.createCoreProcessor(coreurl); + }); + + afterEach(function () { + sandbox.restore(); + }); + + describe('loadSerializedOptions', function () { + it('returns empty array if there are no default options', function () { + // Setup + const mockAttrs = { + object: { + id: 'test_keyboard', + version_string: '1.234', + default_options: [] as km_core_option_item[], + }, + status: KM_CORE_STATUS.OK, + delete: sandbox.stub() + }; + sandbox.stub(KM_Core.instance, 'keyboard_get_attrs').returns(mockAttrs as any); + const keyboardInterface = new unitTestEndPoints.CoreKeyboardInterface(new VariableStoreTestSerializer()); + keyboardInterface['_activeKeyboard'] = mockKeyboard as any; + + // Execute + const options = keyboardInterface['loadSerializedOptions'](); + + // Verify + assert.isArray(options); + assert.lengthOf(options, 0); + }); + + it('returns no options if no options serialized', function () { + // Setup + const mockAttrs = { + object: { + id: 'test_keyboard', + version_string: '1.234', + default_options: [ + { key: 'opt1', value: 'val1', scope: KM_CORE_OPTION_SCOPE.OPT_KEYBOARD } + ], + }, + status: KM_CORE_STATUS.OK, + delete: sandbox.stub(), + }; + sandbox.stub(KM_Core.instance, 'keyboard_get_attrs').returns(mockAttrs as any); + const keyboardInterface = new unitTestEndPoints.CoreKeyboardInterface(new VariableStoreTestSerializer()); + keyboardInterface['_activeKeyboard'] = mockKeyboard as any; + + // Execute + const options = keyboardInterface['loadSerializedOptions'](); + + // Verify + assert.isArray(options); + assert.lengthOf(options, 0); + }); + + it('returns serialized options from keyboard attrs', function () { + // Setup + const mockAttrs = { + object: { + id: 'test_keyboard', + version_string: '1.234', + default_options: [ + { key: 'opt1', value: 'default1', scope: KM_CORE_OPTION_SCOPE.OPT_KEYBOARD }, + { key: 'opt2', value: 'default2', scope: KM_CORE_OPTION_SCOPE.OPT_KEYBOARD }, // ignored because not serialized + { key: 'opt3', value: 'default3', scope: KM_CORE_OPTION_SCOPE.OPT_KEYBOARD }, + { key: 'opt4', value: 'default4', scope: KM_CORE_OPTION_SCOPE.OPT_ENVIRONMENT }, + ], + }, + status: KM_CORE_STATUS.OK, + delete: sandbox.stub(), + }; + sandbox.stub(KM_Core.instance, 'keyboard_get_attrs').returns(mockAttrs as any); + const serializer = new VariableStoreTestSerializer(); + serializer.saveStore('Keyboard_test_keyboard', 'opt1', 'val1'); + serializer.saveStore('Keyboard_test_keyboard', 'opt3', ''); + serializer.saveStore('Keyboard_test_keyboard', 'opt4', 'val4'); // ignored since not keyboard-scoped + serializer.saveStore('Keyboard_test_keyboard', 'opt5', 'val5'); // ignored since not in default options + const keyboardInterface = new unitTestEndPoints.CoreKeyboardInterface(serializer); + keyboardInterface['_activeKeyboard'] = mockKeyboard as any; + + // Execute + const options = keyboardInterface['loadSerializedOptions'](); + + // Verify + assert.isArray(options); + assert.lengthOf(options,2); + assert.deepEqual(options[0], { key: 'opt1', value: 'val1', scope: KM_CORE_OPTION_SCOPE.OPT_KEYBOARD }); + assert.deepEqual(options[1], { key: 'opt3', value: '', scope: KM_CORE_OPTION_SCOPE.OPT_KEYBOARD }); + }); + }); +}); \ No newline at end of file From 09d2816085dd8b99257b89b4260210390c32c705 Mon Sep 17 00:00:00 2001 From: Eberhard Beilharz Date: Tue, 28 Apr 2026 16:52:22 +0200 Subject: [PATCH 2/2] refactor(web): address code review comment Co-authored-by: Marc Durdin --- web/src/app/browser/src/keymanEngine.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/web/src/app/browser/src/keymanEngine.ts b/web/src/app/browser/src/keymanEngine.ts index 70033b6dc3..072742e02c 100644 --- a/web/src/app/browser/src/keymanEngine.ts +++ b/web/src/app/browser/src/keymanEngine.ts @@ -481,8 +481,7 @@ export class KeymanEngine extends KeymanEngineBase