diff --git a/web/src/engine/src/core-processor/coreKeyboardProcessor.ts b/web/src/engine/src/core-processor/coreKeyboardProcessor.ts index 625a7255a7..f6c0d767bf 100644 --- a/web/src/engine/src/core-processor/coreKeyboardProcessor.ts +++ b/web/src/engine/src/core-processor/coreKeyboardProcessor.ts @@ -290,7 +290,8 @@ export class CoreKeyboardProcessor extends EventEmitter implements Key /** * Processes post-keystroke actions for the given device and text store. - * Handles any actions that should occur after a keystroke is processed. + * Handles any actions that should occur after a keystroke is processed + * or after applying suggestions. * * @param {DeviceSpec} device The device specification. * @param {TextStore} textStore The current text store context. @@ -298,7 +299,10 @@ export class CoreKeyboardProcessor extends EventEmitter implements Key * @returns {ProcessorAction} The resulting processor action, or null if not applicable. */ public processPostKeystroke(device: DeviceSpec, textStore: TextStore): ProcessorAction { - // TODO-web-core: Implement this method (#15286) + // TODO-embed-osk-in-kmx: Implement this method (#15286) + // This gets called after processing a keystroke to process the PostKeystroke group + // (irrelevant for web-core since that is handled in Core), but also after + // applying a suggestion, which we do need to handle. return null; } @@ -343,7 +347,7 @@ export class CoreKeyboardProcessor extends EventEmitter implements Key * @returns {boolean} True if the keyboard layer changed, false otherwise. */ public selectLayer(keyEvent: KeyEvent): boolean { - // TODO-web-core: Implement this method (#15284) + // TODO-embed-osk-in-kmx: Implement this method (#15284) return false; } diff --git a/web/src/engine/src/keyboard/keyboards/kmxKeyboard.ts b/web/src/engine/src/keyboard/keyboards/kmxKeyboard.ts index 1c8123192f..3a7b8a43e7 100644 --- a/web/src/engine/src/keyboard/keyboards/kmxKeyboard.ts +++ b/web/src/engine/src/keyboard/keyboards/kmxKeyboard.ts @@ -34,7 +34,7 @@ export class KMXKeyboard { },{ scope: KM_CORE_OPTION_SCOPE.OPT_ENVIRONMENT, key: KM_CORE_KMX_ENV.SIMULATEALTGR, - value: "0" // TODO: We won't support simulating AltGr option in v19 + value: "0" // TODO: We won't support simulating AltGr option in v19 - see also emulatesAltGr } ] const result = KM_Core.instance.state_create(_keyboard, environment_opts); @@ -55,7 +55,7 @@ export class KMXKeyboard { } public constructKeyEvent(key: ActiveKey | ActiveSubKey, device: DeviceSpec, stateKeys: StateKeyMap): KeyEvent { - // TODO-web-core: Implement this method (#15290) + // TODO-embed-osk-in-kmx: Implement this method (#15290) return null; } @@ -86,7 +86,8 @@ export class KMXKeyboard { } public get isChiral(): boolean { - // TODO-web-core: Implement this method (#15290) + // TODO-embed-osk-in-kmx: Implement this method (#15290) + // Only relevant for OSK return false; } @@ -95,7 +96,7 @@ export class KMXKeyboard { * @return {boolean} */ public get emulatesAltGr(): boolean { - // TODO-web-core: Implement this method (#15290) + // TODO: We won't support simulating AltGr option in v19 - see also c'tor return false; } @@ -108,8 +109,8 @@ export class KMXKeyboard { * @param {number} data 1 for KeyDown or FocusReceived, * 0 for KeyUp or FocusLost */ - public notify(eventCode: NotifyEventCode, textStore: TextStore, data: number): void { // I2187 - // TODO-web-core: do we need to support this? (#15290) + public notify(eventCode: NotifyEventCode, textStore: TextStore, data: number): void { + // TODO: implement for IMX (cf #2239 and #7928) } } diff --git a/web/src/engine/src/keyboard/variableStore.ts b/web/src/engine/src/keyboard/variableStore.ts index 29de6ececa..2b7687245a 100644 --- a/web/src/engine/src/keyboard/variableStore.ts +++ b/web/src/engine/src/keyboard/variableStore.ts @@ -13,4 +13,5 @@ export type VariableStore = { [name: string]: string; }; export interface VariableStoreSerializer { loadStore(keyboardID: string, storeName: string): VariableStore; saveStore(keyboardID: string, storeName: string, storeMap: VariableStore): void; + findStores(keyboardID: string): VariableStore[]; } diff --git a/web/src/engine/src/main/variableStoreCookieSerializer.ts b/web/src/engine/src/main/variableStoreCookieSerializer.ts index de7da7e702..58425d61ee 100644 --- a/web/src/engine/src/main/variableStoreCookieSerializer.ts +++ b/web/src/engine/src/main/variableStoreCookieSerializer.ts @@ -9,27 +9,51 @@ import { CookieSerializer } from "keyman/engine/dom-utils"; // dynamic property names; they'd have to be known at compile time to facilitate // strict type checking. class VarStoreSerializer extends CookieSerializer { - constructor(keyboardID: string, storeName: string) { + public constructor(keyboardID: string, storeName: string) { super(`KeymanWeb_${keyboardID}_Option_${storeName}`); } - load() { + public load(): VariableStore { return super.load(decodeURIComponent); } - save(storeMap: VariableStore) { + public save(storeMap: VariableStore) { super.save(storeMap, encodeURIComponent); } + + /** + * Find all variable stores associated with a given keyboard. + * + * @param {string} keyboardID The keyboard ID whose variable stores are to be found. + * + * @returns An array of VariableStore objects found for the keyboard. + */ + public static findStores(keyboardID: string): VariableStore[] { + const pattern = new RegExp(`^KeymanWeb_${keyboardID}_Option_`); + const matching = CookieSerializer.loadAllMatching(pattern, decodeURIComponent); + return matching.map(m => m.value); + } } export class VariableStoreCookieSerializer implements VariableStoreSerializer { - loadStore(keyboardID: string, storeName: string): VariableStore { + public loadStore(keyboardID: string, storeName: string): VariableStore { const storeCookieSerializer = new VarStoreSerializer(keyboardID, storeName); return storeCookieSerializer.load(); } - saveStore(keyboardID: string, storeName: string, storeMap: VariableStore) { + public saveStore(keyboardID: string, storeName: string, storeMap: VariableStore) { const storeCookieSerializer = new VarStoreSerializer(keyboardID, storeName); storeCookieSerializer.save(storeMap); } + + /** + * Find all variable stores associated with a given keyboard. + * + * @param {string} keyboardID The keyboard ID whose variable stores are to be found. + * + * @returns An array of VariableStore objects found for the keyboard. + */ + public findStores(keyboardID: string): VariableStore[] { + return VarStoreSerializer.findStores(keyboardID); + } } \ No newline at end of file diff --git a/web/src/test/auto/dom/cases/main/variableStoreCookieSerializer.tests.ts b/web/src/test/auto/dom/cases/main/variableStoreCookieSerializer.tests.ts new file mode 100644 index 0000000000..1730e7c101 --- /dev/null +++ b/web/src/test/auto/dom/cases/main/variableStoreCookieSerializer.tests.ts @@ -0,0 +1,144 @@ +/* + * Keyman is copyright (C) SIL Global. MIT License. + */ +import { assert } from 'chai'; +import { VariableStore } from 'keyman/engine/keyboard'; +import { VariableStoreCookieSerializer } from 'keyman/engine/main'; + +describe('VariableStoreCookieSerializer', () => { + describe('saveStore and loadStore', () => { + const keyboardID = 'test-keyboard'; + const storeName = 'testStore'; + const cookieName = `KeymanWeb_${keyboardID}_Option_${storeName}`; + + afterEach(async () => { + document.cookie = `${cookieName}=; max-age=0`; + }); + + it('should save and load a simple store with string values', () => { + // Arrange + const serializer = new VariableStoreCookieSerializer(); + const storeData: VariableStore = { + option1: 'value1', + option2: 'value2', + option3: 'value3' + }; + + // Act + serializer.saveStore(keyboardID, storeName, storeData); + const loaded = serializer.loadStore(keyboardID, storeName); + + // Assert + assert.deepEqual(loaded, storeData, 'loaded store should match saved store'); + }); + + it('should save and load a store with empty values', () => { + // Arrange + const serializer = new VariableStoreCookieSerializer(); + const storeData: VariableStore = { + option1: '', + option2: 'value', + option3: '' + }; + + // Act + serializer.saveStore(keyboardID, storeName, storeData); + const loaded = serializer.loadStore(keyboardID, storeName); + + // Assert + assert.deepEqual(loaded, storeData, 'empty string values should be preserved'); + }); + + it('should save and load a store with special characters', () => { + // Arrange + const serializer = new VariableStoreCookieSerializer(); + const storeData: VariableStore = { + encoded: 'value:with;special&chars=', + emoji: '⭐' + }; + + // Act + serializer.saveStore(keyboardID, storeName, storeData); + const loaded = serializer.loadStore(keyboardID, storeName); + + // Assert + assert.deepEqual(loaded, storeData, 'special characters should be properly encoded/decoded'); + }); + + it('should handle multiple stores for the same keyboard independently', () => { + // Arrange + const serializer = new VariableStoreCookieSerializer(); + const store1Data: VariableStore = { option: 'store1value' }; + const store2Data: VariableStore = { option: 'store2value' }; + + // Act + serializer.saveStore(keyboardID, 'store1', store1Data); + serializer.saveStore(keyboardID, 'store2', store2Data); + const loaded1 = serializer.loadStore(keyboardID, 'store1'); + const loaded2 = serializer.loadStore(keyboardID, 'store2'); + + // Assert + assert.deepEqual(loaded1, store1Data, 'first store should be independent'); + assert.deepEqual(loaded2, store2Data, 'second store should be independent'); + + // Cleanup + document.cookie = `KeymanWeb_${keyboardID}_Option_store1=; max-age=0`; + document.cookie = `KeymanWeb_${keyboardID}_Option_store2=; max-age=0`; + }); + + it('should return empty object for non-existent store', () => { + // Arrange + const serializer = new VariableStoreCookieSerializer(); + + // Act + const loaded = serializer.loadStore(keyboardID, 'nonexistent'); + + // Assert + assert.deepEqual(loaded, {}, 'non-existent store should return empty object'); + }); + }); + + describe('findStores', () => { + it('should return an empty array when no stores exist for a keyboardID', () => { + + // Arrange + const serializer = new VariableStoreCookieSerializer(); + const keyboardID = 'test-keyboard'; + const expected: VariableStore[] = []; + + // Act + const result = serializer.findStores(keyboardID); + + // Assert + assert.deepEqual(result, expected, 'result should be an empty array'); + assert.isTrue(Array.isArray(result), 'result should be an array'); + assert.strictEqual(result.length, 0, 'result array length should be 0'); + }); + + it('should return store for keyboard', () => { + // Arrange + const serializer = new VariableStoreCookieSerializer(); + const storeData1: VariableStore = { + option1: 'value1', + option2: 'value2', + option3: 'value3' + }; + serializer.saveStore('test-keyboard', 'storeName', storeData1); + const storeData2: VariableStore = { + settingA: 'A', + settingB: 'B' + }; + serializer.saveStore('another-keyboard', 'anotherStore', storeData2); + + // Act + const stores = serializer.findStores('test-keyboard'); + + // Assert + assert.deepEqual(stores, [storeData1], 'stores should match saved store'); + + // Cleanup + document.cookie = `KeymanWeb_test-keyboard_Option_storeName=; max-age=0`; + }); + + }); +}); diff --git a/web/src/test/auto/dom/web-test-runner.config.mjs b/web/src/test/auto/dom/web-test-runner.config.mjs index 881d428f4c..25eab56492 100644 --- a/web/src/test/auto/dom/web-test-runner.config.mjs +++ b/web/src/test/auto/dom/web-test-runner.config.mjs @@ -84,11 +84,16 @@ export default { // Relative, from the containing package.json files: ['web/build/test/dom/cases/keyboard-storage/**/*.tests.mjs'] }, + { + name: 'engine/main', + // Relative, from the containing package.json + files: ['web/build/test/dom/cases/main/**/*.tests.mjs'] + }, { name: 'engine/osk', // Relative, from the containing package.json files: ['web/build/test/dom/cases/osk/**/*.tests.mjs'] - } + }, ], middleware: [ // Rewrites short-hand paths for test resources, making them fully relative to the repo root.