From 0fd21c0d8248e8f85c56185a9af6c97a6fd6e20f Mon Sep 17 00:00:00 2001 From: Eberhard Beilharz Date: Tue, 23 Jun 2026 18:16:18 +0200 Subject: [PATCH 1/4] fix(web): allow to use file URLs on Android and iOS This change fixes the blank keyboard problem on Android and iOS which was caused by using the Fetch API fetch() function which doesn't support file:// URLs. This change now uses the `XMLHttpRequest` class instead and sets the necessary flags on the Android side. For Keyman for Web nothing changes compared to earlier alpha versions, which means that file URLs shouldn't be used. This change also deletes `loadKeyboardHelper.ts` which wasn't used anywhere. Co-authored-by: Marc Durdin Fixes: #16096 Build-bot: release --- .../java/com/keyman/engine/KMKeyboard.java | 2 ++ .../keyboards/loaders/domKeyboardLoader.ts | 35 +++++++++++++++++-- .../headless/engine/loadKeyboardHelper.ts | 14 -------- 3 files changed, 35 insertions(+), 16 deletions(-) delete mode 100644 web/src/test/auto/headless/engine/loadKeyboardHelper.ts diff --git a/android/KMEA/app/src/main/java/com/keyman/engine/KMKeyboard.java b/android/KMEA/app/src/main/java/com/keyman/engine/KMKeyboard.java index 9152f843604..418d23cb8e7 100644 --- a/android/KMEA/app/src/main/java/com/keyman/engine/KMKeyboard.java +++ b/android/KMEA/app/src/main/java/com/keyman/engine/KMKeyboard.java @@ -265,6 +265,8 @@ public void initKMKeyboard(final Context context) { clearCache(true); getSettings().setJavaScriptEnabled(true); getSettings().setAllowFileAccess(true); + // allow loading keyboards with a file:// url + getSettings().setAllowFileAccessFromFileURLs(true); // Normally, this would be true to prevent the WebView from accessing the network. // But this needs to false for sending embedded KMW crash reports to Sentry (keymanapp/keyman#3825) diff --git a/web/src/engine/src/keyboard/keyboards/loaders/domKeyboardLoader.ts b/web/src/engine/src/keyboard/keyboards/loaders/domKeyboardLoader.ts index 6b961ac831a..552c3ea027e 100644 --- a/web/src/engine/src/keyboard/keyboards/loaders/domKeyboardLoader.ts +++ b/web/src/engine/src/keyboard/keyboards/loaders/domKeyboardLoader.ts @@ -29,6 +29,37 @@ export class DOMKeyboardLoader extends KeyboardLoaderBase { this.performCacheBusting = cacheBust || false; } + /** + * Fetches a resource from the specified URL. This replaces the Fetch API function + * fetch() which doesn't work with file:// URLs. + * + * @param uri + * @returns A response promise + * + * @see https://stackoverflow.com/a/63582110 + * + * Note: Even with this function some browsers like Chrome will still block + * file:// because of CORS, so where possible we shouldn't use file:// but + * serve the files through a HTTP server. In the Keyman for Android and iOS + * apps however we don't want to do this and this function allows us to work + * around the limitation of the Fetch API fetch(). On Android we still have to + * explicitly allow file URLs. + */ + private fetch(uri: string): Promise { + return new Promise(function (resolve, reject) { + const httpRequest = new XMLHttpRequest(); + httpRequest.onload = function () { + resolve(new Response(httpRequest.response, { status: httpRequest.status })); + }; + httpRequest.onerror = (e) => { + reject(e); + }; + httpRequest.open('GET', uri); + httpRequest.responseType = "arraybuffer"; + httpRequest.send(null); + }); + }; + protected async loadKeyboardBlob(uri: string, errorBuilder: KeyboardLoadErrorBuilder): Promise { if (this.performCacheBusting) { uri = this.cacheBust(uri); @@ -36,7 +67,7 @@ export class DOMKeyboardLoader extends KeyboardLoaderBase { let response: Response; try { - response = await fetch(uri); + response = await this.fetch(uri); } catch (e) { throw errorBuilder.keyboardDownloadError(e); } @@ -85,4 +116,4 @@ export class DOMKeyboardLoader extends KeyboardLoaderBase { f.call(context, script); } -} \ No newline at end of file +} diff --git a/web/src/test/auto/headless/engine/loadKeyboardHelper.ts b/web/src/test/auto/headless/engine/loadKeyboardHelper.ts deleted file mode 100644 index 5ffaed719e7..00000000000 --- a/web/src/test/auto/headless/engine/loadKeyboardHelper.ts +++ /dev/null @@ -1,14 +0,0 @@ -/* - * Keyman is copyright (C) SIL Global. MIT License. - */ - -import fs from 'node:fs'; - -import { getKeymanRoot } from 'keyman/test/resources'; - -const KEYMAN_ROOT = getKeymanRoot(); - -export function loadKeyboardBlob(filename: string) { - const data = fs.readFileSync(`${KEYMAN_ROOT}${filename}`, null); - return new Uint8Array(data); -} From 79fd7c8a06a67a59a78093fab7e8384c55cbb612 Mon Sep 17 00:00:00 2001 From: Eberhard Beilharz Date: Wed, 24 Jun 2026 18:48:56 +0200 Subject: [PATCH 2/4] fix(web): add a fetch keyboard function This change adds a `fetchKeyboardFunc` to `DOMKeyboardLoader` that allows mobile platforms to use a different method to fetch the keyboard data. WebView's `KeymanEngine` implements fetching the URL by using `XMLHttpRequest`. --- web/src/app/webview/src/keymanEngine.ts | 34 ++++++++++++++- web/src/engine/src/keyboard/index.ts | 2 +- .../keyboards/loaders/domKeyboardLoader.ts | 42 ++++--------------- web/src/engine/src/main/keymanEngineBase.ts | 8 +++- 4 files changed, 48 insertions(+), 38 deletions(-) diff --git a/web/src/app/webview/src/keymanEngine.ts b/web/src/app/webview/src/keymanEngine.ts index 87896956a9c..43b7597375d 100644 --- a/web/src/app/webview/src/keymanEngine.ts +++ b/web/src/app/webview/src/keymanEngine.ts @@ -1,5 +1,5 @@ import { DeviceSpec } from 'keyman/common/web-utils'; -import { DefaultOutputRules, ProcessorAction } from 'keyman/engine/keyboard'; +import { DefaultOutputRules, DOMKeyboardLoader, ProcessorAction } from 'keyman/engine/keyboard'; import { KeymanEngineBase, KeyboardInterfaceBase } from 'keyman/engine/main'; import { AnchoredOSKView, ViewConfiguration, StaticActivator } from 'keyman/engine/osk'; import { getAbsoluteX, getAbsoluteY } from 'keyman/engine/dom-utils'; @@ -42,6 +42,10 @@ export class KeymanEngine extends KeymanEngineBase) { const device = new DeviceSpec( 'native', @@ -159,4 +163,32 @@ export class KeymanEngine extends KeymanEngineBase { + return new Promise(function (resolve, reject) { + const httpRequest = new XMLHttpRequest(); + httpRequest.onload = function () { + resolve(new Response(httpRequest.response, { status: httpRequest.status })); + }; + httpRequest.onerror = (e) => { + reject(e); + }; + httpRequest.open('GET', uri); + httpRequest.responseType = "arraybuffer"; + httpRequest.send(null); + }); + }; + } diff --git a/web/src/engine/src/keyboard/index.ts b/web/src/engine/src/keyboard/index.ts index 40fd493a059..c0992df881b 100644 --- a/web/src/engine/src/keyboard/index.ts +++ b/web/src/engine/src/keyboard/index.ts @@ -32,7 +32,7 @@ export { KeyMapping } from "./keyMapping.js"; export { type SystemStoreMutationHandler, MutableSystemStore, SystemStore, SystemStoreIDs, type SystemStoreDictionary } from "./systemStore.js"; export { type VariableStores, VariableStoreSerializer } from "./variableStore.js"; -export { DOMKeyboardLoader } from './keyboards/loaders/domKeyboardLoader.js'; +export { type FetchFunction, DOMKeyboardLoader } from './keyboards/loaders/domKeyboardLoader.js'; export { SyntheticTextStore } from "./syntheticTextStore.js"; export { TextStore } from "./textStore.js"; export { TextStoreLanguageProcessorInterface } from "./textStoreLanguageProcessorInterface.js"; diff --git a/web/src/engine/src/keyboard/keyboards/loaders/domKeyboardLoader.ts b/web/src/engine/src/keyboard/keyboards/loaders/domKeyboardLoader.ts index 552c3ea027e..118d989c89a 100644 --- a/web/src/engine/src/keyboard/keyboards/loaders/domKeyboardLoader.ts +++ b/web/src/engine/src/keyboard/keyboards/loaders/domKeyboardLoader.ts @@ -7,14 +7,17 @@ import { KeyboardHarness, MinimalKeymanGlobal } from '../keyboardHarness.js'; import { KeyboardLoaderBase } from '../keyboardLoaderBase.js'; import { KeyboardLoadErrorBuilder } from '../keyboardLoadError.js'; +export type FetchFunction = (uri: string) => Promise; + export class DOMKeyboardLoader extends KeyboardLoaderBase { public readonly element: HTMLIFrameElement; private readonly performCacheBusting: boolean; + private readonly fetchKeyboardFunc: FetchFunction; constructor() constructor(harness: KeyboardHarness); - constructor(harness: KeyboardHarness, cacheBust?: boolean) - constructor(harness?: KeyboardHarness, cacheBust?: boolean) { + constructor(harness: KeyboardHarness, cacheBust?: boolean, fetchKeyboardFunc?: FetchFunction) + constructor(harness?: KeyboardHarness, cacheBust?: boolean, fetchKeyboardFunc?: FetchFunction) { if(harness && harness._jsGlobal != window) { // Copy the String typing over; preserve string extensions! harness._jsGlobal['String'] = window['String']; @@ -27,39 +30,10 @@ export class DOMKeyboardLoader extends KeyboardLoaderBase { } this.performCacheBusting = cacheBust || false; + const defaultFetchFunc: FetchFunction = (uri) => fetch(uri); + this.fetchKeyboardFunc = fetchKeyboardFunc ?? defaultFetchFunc; } - /** - * Fetches a resource from the specified URL. This replaces the Fetch API function - * fetch() which doesn't work with file:// URLs. - * - * @param uri - * @returns A response promise - * - * @see https://stackoverflow.com/a/63582110 - * - * Note: Even with this function some browsers like Chrome will still block - * file:// because of CORS, so where possible we shouldn't use file:// but - * serve the files through a HTTP server. In the Keyman for Android and iOS - * apps however we don't want to do this and this function allows us to work - * around the limitation of the Fetch API fetch(). On Android we still have to - * explicitly allow file URLs. - */ - private fetch(uri: string): Promise { - return new Promise(function (resolve, reject) { - const httpRequest = new XMLHttpRequest(); - httpRequest.onload = function () { - resolve(new Response(httpRequest.response, { status: httpRequest.status })); - }; - httpRequest.onerror = (e) => { - reject(e); - }; - httpRequest.open('GET', uri); - httpRequest.responseType = "arraybuffer"; - httpRequest.send(null); - }); - }; - protected async loadKeyboardBlob(uri: string, errorBuilder: KeyboardLoadErrorBuilder): Promise { if (this.performCacheBusting) { uri = this.cacheBust(uri); @@ -67,7 +41,7 @@ export class DOMKeyboardLoader extends KeyboardLoaderBase { let response: Response; try { - response = await this.fetch(uri); + response = await this.fetchKeyboardFunc(uri); } catch (e) { throw errorBuilder.keyboardDownloadError(e); } diff --git a/web/src/engine/src/main/keymanEngineBase.ts b/web/src/engine/src/main/keymanEngineBase.ts index bdb43e05ead..b5c588f09de 100644 --- a/web/src/engine/src/main/keymanEngineBase.ts +++ b/web/src/engine/src/main/keymanEngineBase.ts @@ -225,6 +225,10 @@ export class KeymanEngineBase< }); } + protected createKeyboardLoader(): DOMKeyboardLoader { + return new DOMKeyboardLoader(this.interface, this.config.applyCacheBusting); + } + public async init(optionSpec: Required){ // There may be some valid mutations possible even on repeated calls? // The original seems to allow it. @@ -242,7 +246,7 @@ export class KeymanEngineBase< // Since we're not sandboxing keyboard loads yet, we just use `window` as the jsGlobal object. // All components initialized below require a properly-configured `config.paths` or similar. - const keyboardLoader = new DOMKeyboardLoader(this.interface, config.applyCacheBusting); + const keyboardLoader = this.createKeyboardLoader(); this.keyboardRequisitioner = new KeyboardRequisitioner(keyboardLoader, new DOMCloudRequester(), this.config.paths); this.modelCache = new ModelCache(); const kbdCache = this.keyboardRequisitioner.cache; @@ -598,4 +602,4 @@ export class KeymanEngineBase< }; } -// Intent: define common behaviors for both primary app types; each then subclasses & extends where needed. \ No newline at end of file +// Intent: define common behaviors for both primary app types; each then subclasses & extends where needed. From 236695e2bef322d98bba64d5447e076379887df6 Mon Sep 17 00:00:00 2001 From: Eberhard Beilharz Date: Thu, 25 Jun 2026 10:27:31 +0200 Subject: [PATCH 3/4] fix(web): address code review comments --- .../app/browser/src/browserKeyboardLoader.ts | 15 +++++++ web/src/app/browser/src/keymanEngine.ts | 7 +++- web/src/app/webview/src/keymanEngine.ts | 35 ++--------------- .../app/webview/src/webviewKeyboardLoader.ts | 39 +++++++++++++++++++ web/src/engine/src/keyboard/index.ts | 2 +- .../keyboards/loaders/domKeyboardLoader.ts | 14 +++---- 6 files changed, 71 insertions(+), 41 deletions(-) create mode 100644 web/src/app/browser/src/browserKeyboardLoader.ts create mode 100644 web/src/app/webview/src/webviewKeyboardLoader.ts diff --git a/web/src/app/browser/src/browserKeyboardLoader.ts b/web/src/app/browser/src/browserKeyboardLoader.ts new file mode 100644 index 00000000000..f5f8cc57e5d --- /dev/null +++ b/web/src/app/browser/src/browserKeyboardLoader.ts @@ -0,0 +1,15 @@ +/* + * Keyman is copyright (C) SIL Global. MIT License. + */ + +import { DOMKeyboardLoader, KeyboardHarness } from 'keyman/engine/keyboard'; + +export class BrowserKeyboardLoader extends DOMKeyboardLoader { + constructor(harness: KeyboardHarness, cacheBust: boolean) { + super(harness, cacheBust); + } + + protected fetch(uri: string): Promise { + return window.fetch(uri); + } +} diff --git a/web/src/app/browser/src/keymanEngine.ts b/web/src/app/browser/src/keymanEngine.ts index af3bae2d207..12b29ac6b62 100644 --- a/web/src/app/browser/src/keymanEngine.ts +++ b/web/src/app/browser/src/keymanEngine.ts @@ -8,7 +8,7 @@ import { } from 'keyman/engine/osk'; import { ErrorStub, KeyboardStub, CloudQueryResult, toPrefixedKeyboardId } from 'keyman/engine/keyboard-storage'; import { DeviceSpec } from 'keyman/common/web-utils'; -import { JSKeyboard, Keyboard } from "keyman/engine/keyboard"; +import { DOMKeyboardLoader, JSKeyboard, Keyboard } from "keyman/engine/keyboard"; import KeyboardObject = KeymanWebKeyboard.KeyboardObject; import * as views from './viewsAnchorpoint.js'; @@ -30,6 +30,7 @@ import { BeepHandler } from './beepHandler.js'; import { KeyboardInterface } from './keyboardInterface.js'; import { WorkerFactory } from '@keymanapp/lexical-model-layer/web'; import { KeyboardDetails } from './keyboardDetails.js'; +import { BrowserKeyboardLoader } from './browserKeyboardLoader.js'; export class KeymanEngine extends KeymanEngineBase { touchLanguageMenu?: LanguageMenu; @@ -720,4 +721,8 @@ export class KeymanEngine extends KeymanEngineBase { // Ideally, we would be able to auto-detect `sourceUri`: https://stackoverflow.com/a/60244278. @@ -42,10 +43,6 @@ export class KeymanEngine extends KeymanEngineBase) { const device = new DeviceSpec( 'native', @@ -164,31 +161,7 @@ export class KeymanEngine extends KeymanEngineBase { - return new Promise(function (resolve, reject) { - const httpRequest = new XMLHttpRequest(); - httpRequest.onload = function () { - resolve(new Response(httpRequest.response, { status: httpRequest.status })); - }; - httpRequest.onerror = (e) => { - reject(e); - }; - httpRequest.open('GET', uri); - httpRequest.responseType = "arraybuffer"; - httpRequest.send(null); - }); - }; - + protected createKeyboardLoader(): DOMKeyboardLoader { + return new WebviewKeyboardLoader(this.interface, this.config.applyCacheBusting); + } } diff --git a/web/src/app/webview/src/webviewKeyboardLoader.ts b/web/src/app/webview/src/webviewKeyboardLoader.ts new file mode 100644 index 00000000000..6593c122d7e --- /dev/null +++ b/web/src/app/webview/src/webviewKeyboardLoader.ts @@ -0,0 +1,39 @@ +/* + * Keyman is copyright (C) SIL Global. MIT License. + */ + +import { DOMKeyboardLoader, KeyboardHarness } from 'keyman/engine/keyboard'; + +export class WebviewKeyboardLoader extends DOMKeyboardLoader { + constructor(harness: KeyboardHarness, cacheBust: boolean) { + super(harness, cacheBust); + } + + /** + * Fetches a resource from the specified URL. + * + * @param uri + * @returns A response promise + * + * @see https://stackoverflow.com/a/63582110 + * + * Note: Using XMLHttpRequest allows us to work around the limitations of + * Fetch API's fetch() which doesn't support file:// URLs. At least in + * Keyman for Android we still have to explicitly allow file URLs. + */ + protected fetch(uri: string): Promise { + return new Promise(function (resolve, reject) { + const httpRequest = new XMLHttpRequest(); + httpRequest.onload = function () { + resolve(new Response(httpRequest.response, { status: httpRequest.status })); + }; + httpRequest.onerror = (e) => { + reject(e); + }; + httpRequest.open('GET', uri); + httpRequest.responseType = "arraybuffer"; + httpRequest.send(null); + }); + }; + +} diff --git a/web/src/engine/src/keyboard/index.ts b/web/src/engine/src/keyboard/index.ts index c0992df881b..40fd493a059 100644 --- a/web/src/engine/src/keyboard/index.ts +++ b/web/src/engine/src/keyboard/index.ts @@ -32,7 +32,7 @@ export { KeyMapping } from "./keyMapping.js"; export { type SystemStoreMutationHandler, MutableSystemStore, SystemStore, SystemStoreIDs, type SystemStoreDictionary } from "./systemStore.js"; export { type VariableStores, VariableStoreSerializer } from "./variableStore.js"; -export { type FetchFunction, DOMKeyboardLoader } from './keyboards/loaders/domKeyboardLoader.js'; +export { DOMKeyboardLoader } from './keyboards/loaders/domKeyboardLoader.js'; export { SyntheticTextStore } from "./syntheticTextStore.js"; export { TextStore } from "./textStore.js"; export { TextStoreLanguageProcessorInterface } from "./textStoreLanguageProcessorInterface.js"; diff --git a/web/src/engine/src/keyboard/keyboards/loaders/domKeyboardLoader.ts b/web/src/engine/src/keyboard/keyboards/loaders/domKeyboardLoader.ts index 118d989c89a..d9bc4976d61 100644 --- a/web/src/engine/src/keyboard/keyboards/loaders/domKeyboardLoader.ts +++ b/web/src/engine/src/keyboard/keyboards/loaders/domKeyboardLoader.ts @@ -7,17 +7,14 @@ import { KeyboardHarness, MinimalKeymanGlobal } from '../keyboardHarness.js'; import { KeyboardLoaderBase } from '../keyboardLoaderBase.js'; import { KeyboardLoadErrorBuilder } from '../keyboardLoadError.js'; -export type FetchFunction = (uri: string) => Promise; - export class DOMKeyboardLoader extends KeyboardLoaderBase { public readonly element: HTMLIFrameElement; private readonly performCacheBusting: boolean; - private readonly fetchKeyboardFunc: FetchFunction; constructor() constructor(harness: KeyboardHarness); - constructor(harness: KeyboardHarness, cacheBust?: boolean, fetchKeyboardFunc?: FetchFunction) - constructor(harness?: KeyboardHarness, cacheBust?: boolean, fetchKeyboardFunc?: FetchFunction) { + constructor(harness: KeyboardHarness, cacheBust?: boolean) + constructor(harness?: KeyboardHarness, cacheBust?: boolean) { if(harness && harness._jsGlobal != window) { // Copy the String typing over; preserve string extensions! harness._jsGlobal['String'] = window['String']; @@ -30,8 +27,6 @@ export class DOMKeyboardLoader extends KeyboardLoaderBase { } this.performCacheBusting = cacheBust || false; - const defaultFetchFunc: FetchFunction = (uri) => fetch(uri); - this.fetchKeyboardFunc = fetchKeyboardFunc ?? defaultFetchFunc; } protected async loadKeyboardBlob(uri: string, errorBuilder: KeyboardLoadErrorBuilder): Promise { @@ -41,7 +36,7 @@ export class DOMKeyboardLoader extends KeyboardLoaderBase { let response: Response; try { - response = await this.fetchKeyboardFunc(uri); + response = await this.fetch(uri); } catch (e) { throw errorBuilder.keyboardDownloadError(e); } @@ -90,4 +85,7 @@ export class DOMKeyboardLoader extends KeyboardLoaderBase { f.call(context, script); } + protected fetch(uri: string): Promise { + throw new Error('not implemented'); + } } From c0c1847e23ad56e4266da64637a7a2052177e8fe Mon Sep 17 00:00:00 2001 From: Eberhard Beilharz Date: Fri, 26 Jun 2026 09:16:53 +0200 Subject: [PATCH 4/4] fix(web): cleanup and fix PR - make `DOMKeyboardLoader` and `KeymanEngineBase` abstract since they don't work on their own - create derived `TestingDOMKeyboardLoader` class for testing which uses `fetch()` to load URLs --- .../keyboard/keyboards/loaders/domKeyboardLoader.ts | 6 ++---- web/src/engine/src/main/keymanEngineBase.ts | 6 ++---- .../auto/dom/cases/browser/contextManager.tests.ts | 5 +++-- .../keyboard-storage/keyboardRequisitioner.tests.ts | 8 ++++---- .../dom/cases/keyboard/domKeyboardLoader.tests.ts | 12 ++++++------ web/src/test/auto/dom/kbdLoader.ts | 6 +++--- web/src/test/auto/dom/test_utils.ts | 12 ++++++++++++ 7 files changed, 32 insertions(+), 23 deletions(-) diff --git a/web/src/engine/src/keyboard/keyboards/loaders/domKeyboardLoader.ts b/web/src/engine/src/keyboard/keyboards/loaders/domKeyboardLoader.ts index d9bc4976d61..d4d8e126f8a 100644 --- a/web/src/engine/src/keyboard/keyboards/loaders/domKeyboardLoader.ts +++ b/web/src/engine/src/keyboard/keyboards/loaders/domKeyboardLoader.ts @@ -7,7 +7,7 @@ import { KeyboardHarness, MinimalKeymanGlobal } from '../keyboardHarness.js'; import { KeyboardLoaderBase } from '../keyboardLoaderBase.js'; import { KeyboardLoadErrorBuilder } from '../keyboardLoadError.js'; -export class DOMKeyboardLoader extends KeyboardLoaderBase { +export abstract class DOMKeyboardLoader extends KeyboardLoaderBase { public readonly element: HTMLIFrameElement; private readonly performCacheBusting: boolean; @@ -85,7 +85,5 @@ export class DOMKeyboardLoader extends KeyboardLoaderBase { f.call(context, script); } - protected fetch(uri: string): Promise { - throw new Error('not implemented'); - } + protected abstract fetch(uri: string): Promise; } diff --git a/web/src/engine/src/main/keymanEngineBase.ts b/web/src/engine/src/main/keymanEngineBase.ts index b5c588f09de..d12b91c27f9 100644 --- a/web/src/engine/src/main/keymanEngineBase.ts +++ b/web/src/engine/src/main/keymanEngineBase.ts @@ -35,7 +35,7 @@ function determineBaseLayout(): string { export type KeyEventFullResultCallback = (result: ProcessorAction, error?: Error) => void; export type KeyEventFullHandler = (event: KeyEvent, callback?: KeyEventFullResultCallback) => void; -export class KeymanEngineBase< +export abstract class KeymanEngineBase< ConfigurationT extends EngineConfiguration, ContextManagerT extends ContextManagerBase, HardKeyboardT extends HardKeyboardBase @@ -225,9 +225,7 @@ export class KeymanEngineBase< }); } - protected createKeyboardLoader(): DOMKeyboardLoader { - return new DOMKeyboardLoader(this.interface, this.config.applyCacheBusting); - } + protected abstract createKeyboardLoader(): DOMKeyboardLoader; public async init(optionSpec: Required){ // There may be some valid mutations possible even on repeated calls? diff --git a/web/src/test/auto/dom/cases/browser/contextManager.tests.ts b/web/src/test/auto/dom/cases/browser/contextManager.tests.ts index ce96c2a3561..231be62ab6b 100644 --- a/web/src/test/auto/dom/cases/browser/contextManager.tests.ts +++ b/web/src/test/auto/dom/cases/browser/contextManager.tests.ts @@ -5,7 +5,7 @@ import { LegacyEventEmitter } from 'keyman/engine/events'; import { StubAndKeyboardCache, toPrefixedKeyboardId as prefixed } from 'keyman/engine/keyboard-storage'; import { KeyboardInterfaceBase } from 'keyman/engine/main'; -import { KeyboardHarness, MinimalKeymanGlobal, DOMKeyboardLoader } from 'keyman/engine/keyboard'; +import { KeyboardHarness, MinimalKeymanGlobal } from 'keyman/engine/keyboard'; import { KeyboardMap, loadKeyboardsFromStubs } from '../../kbdLoader.js'; import { DeviceSpec, ManagedPromise, timedPromise } from 'keyman/common/web-utils'; @@ -13,6 +13,7 @@ import { DEFAULT_BROWSER_TIMEOUT } from '@keymanapp/common-test-resources/test-t import sinon from 'sinon'; import { assert } from 'chai'; +import { TestingDOMKeyboardLoader } from '../../test_utils.js'; const TEST_PHYSICAL_DEVICE = { formFactor: 'desktop', @@ -192,7 +193,7 @@ describe('app/browser: ContextManager', function () { }, () => new LegacyEventEmitter()); // Needed for the keyboard tests later. - keyboardLoader = new DOMKeyboardLoader(new KeyboardHarness(window, MinimalKeymanGlobal)); + keyboardLoader = new TestingDOMKeyboardLoader(new KeyboardHarness(window, MinimalKeymanGlobal)); keyboardCache = new StubAndKeyboardCache(keyboardLoader); contextManager.configure({ diff --git a/web/src/test/auto/dom/cases/keyboard-storage/keyboardRequisitioner.tests.ts b/web/src/test/auto/dom/cases/keyboard-storage/keyboardRequisitioner.tests.ts index 5ac6f496e1b..8f9e64ae54e 100644 --- a/web/src/test/auto/dom/cases/keyboard-storage/keyboardRequisitioner.tests.ts +++ b/web/src/test/auto/dom/cases/keyboard-storage/keyboardRequisitioner.tests.ts @@ -2,10 +2,10 @@ import { assert } from 'chai'; import sinon from 'sinon'; import { KeyboardHarness, MinimalKeymanGlobal } from 'keyman/engine/keyboard'; -import { DOMKeyboardLoader } from 'keyman/engine/keyboard'; import { PathConfiguration } from 'keyman/engine/interfaces'; import { CloudQueryEngine, KeyboardRequisitioner, type KeyboardStub } from 'keyman/engine/keyboard-storage'; import { DOMCloudRequester } from 'keyman/engine/keyboard-storage'; +import { TestingDOMKeyboardLoader } from '../../test_utils.js'; const pathConfig = new PathConfiguration({ root: '', @@ -61,7 +61,7 @@ function mockQuery(querier: CloudQueryEngine, queryResultsFile: string) { describe("KeyboardRequisitioner", function () { it('queries for remote stubs and loads their keyboards', async () => { - const keyboardLoader = new DOMKeyboardLoader(new KeyboardHarness(window, MinimalKeymanGlobal)); + const keyboardLoader = new TestingDOMKeyboardLoader(new KeyboardHarness(window, MinimalKeymanGlobal)); const keyboardRequisitioner = new KeyboardRequisitioner(keyboardLoader, new DOMCloudRequester(true), pathConfig); const cache = keyboardRequisitioner.cache; @@ -83,7 +83,7 @@ describe("KeyboardRequisitioner", function () { }); it('loads keyboards for page-local, API-added stubs', async () => { - const keyboardLoader = new DOMKeyboardLoader(new KeyboardHarness(window, MinimalKeymanGlobal)); + const keyboardLoader = new TestingDOMKeyboardLoader(new KeyboardHarness(window, MinimalKeymanGlobal)); const keyboardRequisitioner = new KeyboardRequisitioner(keyboardLoader, new DOMCloudRequester(true), pathConfig); const cache = keyboardRequisitioner.cache; @@ -108,4 +108,4 @@ describe("KeyboardRequisitioner", function () { assert.strictEqual(cache.getKeyboardForStub(stub), khmer_angkor); assert.isOk(khmer_angkor); }); -}); \ No newline at end of file +}); diff --git a/web/src/test/auto/dom/cases/keyboard/domKeyboardLoader.tests.ts b/web/src/test/auto/dom/cases/keyboard/domKeyboardLoader.tests.ts index e9ea8fe8c0e..55d18fb04c6 100644 --- a/web/src/test/auto/dom/cases/keyboard/domKeyboardLoader.tests.ts +++ b/web/src/test/auto/dom/cases/keyboard/domKeyboardLoader.tests.ts @@ -1,11 +1,11 @@ import { assert } from 'chai'; -import { DOMKeyboardLoader } from 'keyman/engine/keyboard'; import { DeviceSpec } from 'keyman/common/web-utils'; import { KeyboardHarness, JSKeyboard, MinimalKeymanGlobal, KeyboardKeymanGlobal, KeyboardDownloadError, KeyboardScriptError, Keyboard, SyntheticTextStore } from 'keyman/engine/keyboard'; import { JSKeyboardInterface } from 'keyman/engine/js-processor'; import { assertThrowsAsync } from 'keyman/tools/testing/test-utils'; import { VariableStoreTestSerializer } from 'keyman/test/headless-resources'; +import { TestingDOMKeyboardLoader } from '../../test_utils.js'; declare let window: typeof globalThis; // KeymanEngine from the web/ folder... when available. @@ -29,7 +29,7 @@ describe('Keyboard loading in DOM', function() { it('throws error when keyboard does not exist', async () => { const harness = new JSKeyboardInterface(window, MinimalKeymanGlobal, new VariableStoreTestSerializer()); - const keyboardLoader = new DOMKeyboardLoader(harness); + const keyboardLoader = new TestingDOMKeyboardLoader(harness); const nonExisting = '/does/not/exist.js'; await assertThrowsAsync(async () => await keyboardLoader.loadKeyboardFromPath(nonExisting), @@ -38,7 +38,7 @@ describe('Keyboard loading in DOM', function() { it('throws error when keyboard is invalid', async () => { const harness = new JSKeyboardInterface(window, MinimalKeymanGlobal, new VariableStoreTestSerializer()); - const keyboardLoader = new DOMKeyboardLoader(harness); + const keyboardLoader = new TestingDOMKeyboardLoader(harness); const nonKeyboardPath = '/common/test/resources/index.mjs'; await assertThrowsAsync(async () => await keyboardLoader.loadKeyboardFromPath(nonKeyboardPath), @@ -47,7 +47,7 @@ describe('Keyboard loading in DOM', function() { it('`window`, disabled rule processing', async () => { const harness = new KeyboardHarness(window, MinimalKeymanGlobal); - let keyboardLoader = new DOMKeyboardLoader(harness); + let keyboardLoader = new TestingDOMKeyboardLoader(harness); let keyboard: Keyboard = await keyboardLoader.loadKeyboardFromPath('/common/test/resources/keyboards/khmer_angkor.js'); assert.isOk(keyboard); @@ -67,7 +67,7 @@ describe('Keyboard loading in DOM', function() { it('`window`, enabled rule processing', async () => { const jsHarness = new JSKeyboardInterface(window, MinimalKeymanGlobal, new VariableStoreTestSerializer()); - const keyboardLoader = new DOMKeyboardLoader(jsHarness); + const keyboardLoader = new TestingDOMKeyboardLoader(jsHarness); const keyboard: Keyboard = await keyboardLoader.loadKeyboardFromPath('/common/test/resources/keyboards/khmer_angkor.js'); const jsKeyboard = keyboard as JSKeyboard; jsHarness.activeKeyboard = jsKeyboard; @@ -99,7 +99,7 @@ describe('Keyboard loading in DOM', function() { it('load keyboards successfully in parallel without side effects', async () => { let jsHarness = new JSKeyboardInterface(window, MinimalKeymanGlobal, new VariableStoreTestSerializer()); - let keyboardLoader = new DOMKeyboardLoader(jsHarness); + let keyboardLoader = new TestingDOMKeyboardLoader(jsHarness); // Preload a keyboard and make it active. const test_kbd: Keyboard = await keyboardLoader.loadKeyboardFromPath('/common/test/resources/keyboards/test_917.js'); diff --git a/web/src/test/auto/dom/kbdLoader.ts b/web/src/test/auto/dom/kbdLoader.ts index 3c1dd21eef7..fedaac372df 100644 --- a/web/src/test/auto/dom/kbdLoader.ts +++ b/web/src/test/auto/dom/kbdLoader.ts @@ -1,5 +1,4 @@ import { - DOMKeyboardLoader, JSKeyboard, Keyboard, KeyboardProperties, @@ -9,8 +8,9 @@ import { import { JSKeyboardInterface } from 'keyman/engine/js-processor'; import { KeyboardInfoPair } from 'keyman/engine/main'; import { VariableStoreTestSerializer } from 'keyman/test/headless-resources'; +import { TestingDOMKeyboardLoader } from './test_utils.js'; -const loader = new DOMKeyboardLoader(new JSKeyboardInterface(window, MinimalKeymanGlobal, new VariableStoreTestSerializer())); +const loader = new TestingDOMKeyboardLoader(new JSKeyboardInterface(window, MinimalKeymanGlobal, new VariableStoreTestSerializer())); export function loadKeyboardFromPath(path: string) { return loader.loadKeyboardFromPath(path); @@ -59,4 +59,4 @@ export function loadKeyboardsFromStubs(apiStubs: any, baseDir: string) { } return priorPromise.then(() => keyboards); -} \ No newline at end of file +} diff --git a/web/src/test/auto/dom/test_utils.ts b/web/src/test/auto/dom/test_utils.ts index c015988707c..44ebe8b8576 100644 --- a/web/src/test/auto/dom/test_utils.ts +++ b/web/src/test/auto/dom/test_utils.ts @@ -1,6 +1,8 @@ // Defines an object for dynamically adding elements for testing purposes. // Designed for use with the robustAttachment.html fixture. +import { DOMKeyboardLoader, KeyboardHarness } from "keyman/engine/keyboard"; + export class DynamicElements { static inputCounter = 0; @@ -82,3 +84,13 @@ export class DynamicElements { return editable.id; } } + +export class TestingDOMKeyboardLoader extends DOMKeyboardLoader { + constructor(harness: KeyboardHarness, cacheBust?: boolean) { + super(harness, cacheBust); + } + + protected fetch(uri: string): Promise { + return window.fetch(uri); + } +}