spiegel-keyman/web/src/test/auto/dom/cases/browser/contextManager.tests.ts
Eberhard Beilharz e4830fb637
chore(web): add Keyman header to test files
Add missing headers mainly to test files.

Build-bot: skip
Test-bot: skip
2026-10-19 22:41:04 +02:00

1355 lines
59 KiB
TypeScript

/*
* Keyman is copyright (C) SIL Global. MIT License.
*/
import { ContextManager } from 'keyman/app/browser';
import { textStoreForElement } from 'keyman/engine/attachment';
import { TextAreaElementTextStore } from 'keyman/engine/element-text-stores';
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 { KeyboardMap, loadKeyboardsFromStubs } from '../../kbdLoader.js';
import { DeviceSpec, ManagedPromise, timedPromise } from 'keyman/common/web-utils';
import { DEFAULT_BROWSER_TIMEOUT } from '@keymanapp/common-test-resources/test-timeouts.mjs';
import sinon from 'sinon';
import { assert } from 'chai';
const TEST_PHYSICAL_DEVICE = {
formFactor: 'desktop',
OS: 'windows',
browser: 'native',
touchable: false
} as DeviceSpec;
const host = document.createElement('div');
document.body.appendChild(host);
function assertPromiseResolved(promise: Promise<any>, timeout?: number) {
// Ensure timeout is initialized to a numeric value.
// If undefined or 0, expects instant resolution.
timeout ||= 0;
timeout >= 0 ? timeout : 0;
return new Promise<void>((resolve, reject) => {
let fulfilled = false;
promise.then(() => {
if(!fulfilled) {
// resolve();
resolve();
fulfilled = true;
}
}).catch((err) => {
if(!fulfilled) {
reject(err);
fulfilled = true;
}
});
timedPromise(timeout).then(() => {
if(!fulfilled) {
reject(new Error("The Promise failed to reach fulfillment during the allotted time"));
fulfilled = true;
}
});
});
}
function promiseForIframeLoad(iframe: HTMLIFrameElement) {
// Chrome makes this first case tricky - it initializes all iframes with a 'complete' about:blank
// before loading the actual href. (https://stackoverflow.com/a/36155560)
if(iframe.contentDocument
&& iframe.contentDocument.readyState as string === 'complete'
&& iframe.contentDocument.body.innerHTML) {
return Promise.resolve();
} else {
return new Promise((resolve, reject) => {
iframe.addEventListener('load', resolve);
iframe.addEventListener('error', reject);
});
}
}
function dispatchFocus(eventName: string, elem: HTMLElement) {
const event = new FocusEvent(eventName, {relatedTarget: elem});
elem.dispatchEvent(event);
}
// The replaced methods sometimes fail in unit-testing setups, possibly due to the very short time intervals involved.
// The replacements suffice to trigger the same effects.
function upgradeFocus(elem: HTMLElement) {
elem.blur = () => {
dispatchFocus('blur', elem);
}
elem.focus = () => {
const activeElement = document.activeElement as HTMLElement;
activeElement?.blur();
dispatchFocus('focus', elem);
}
}
async function blockConsoleAndAwait(asyncClosure: any) {
const originalConsole = window.console;
/* @ts-ignore */
window.console = {
log: sinon.fake(),
warn: sinon.fake(),
error: sinon.fake()
}
try {
await asyncClosure();
} finally {
window.console = originalConsole;
}
}
async function withDelayedFetching(keyboardLoader: any, time: number, closure: any) {
time || 0;
if(time < 0) {
time = 0;
}
const originalLoad = keyboardLoader.loadKeyboardInternal;
const fetchIntercept = async (...args: any) => {
const retVal = originalLoad.call(keyboardLoader, ...args);
keyboardLoader.loadKeyboardInternal = originalLoad;
await timedPromise(time);
return retVal;
}
keyboardLoader.loadKeyboardInternal = fetchIntercept;
await closure();
}
async function loadKeyboardsForTests(apiStubs: string[]): Promise<KeyboardMap> {
const kbdStubPromises = apiStubs.map((file: string) => {
return fetch(`common/test/resources/json/${file}.json`).then((response) => {
return response.json();
});
});
const kbdStubs = await Promise.all(kbdStubPromises);
return await loadKeyboardsFromStubs(kbdStubs, 'common/test/');
}
describe('app/browser: ContextManager', function () {
this.timeout(DEFAULT_BROWSER_TIMEOUT);
/**
* Holds a test-specific instance of ContextManager.
*/
let contextManager: ContextManager;
/**
* Holds the test-specific instance of the stub & keyboard cache used by the
* current test's `contextManager`.
*/
let keyboardCache: StubAndKeyboardCache;
/**
* Holds the test-specific instance of the keyboard loader used by the current
* test's `keyboardCache`.
*/
let keyboardLoader: any;
beforeEach(async () => {
// Loads a common fixture and ensures all relevant elements are attached.
const fixture = await fetch('/resources/fixtures/a-bit-of-everything.html');
host.innerHTML = await fixture.text();
// Note: iframes require additional time to resolve.
await promiseForIframeLoad(document.getElementById('iframe') as HTMLIFrameElement);
await promiseForIframeLoad(document.getElementById('design-iframe') as HTMLIFrameElement);
// Give the design-mode iframe a bit of time to set itself up properly.
// Note: it is thus important that whatever sends the `install` command has also
// alloted a brief window of time like this as well.
await timedPromise(20);
// Load the page fully before we init ContextManager.
// Note: we provide an incomplete 'mock' of BrowserConfiguration here.
const preResolved = new ManagedPromise<void>();
preResolved.resolve();
contextManager = new ContextManager({
// Needed during keyboard-loading.
deferForInitialization: preResolved,
hostDevice: {... TEST_PHYSICAL_DEVICE},
attachType: 'auto',
// Used when setting keyboard-specific fonts to attached controls
/* @ts-ignore - we're mocking the setup here. */
paths: {
// We don't care if the font actually exists at the location for these tests;
// a simple placeholder will do.
'fonts': ''
}
// signalUser may be relevant for some tests.
}, () => new LegacyEventEmitter());
// Needed for the keyboard tests later.
keyboardLoader = new DOMKeyboardLoader(new KeyboardHarness(window, MinimalKeymanGlobal));
keyboardCache = new StubAndKeyboardCache(keyboardLoader);
contextManager.configure({
keyboardCache: keyboardCache,
predictionContext: {
// we're dummying these out.
resetContext: (() => {}) as any,
setCurrentTextStore: (() => {}) as any,
} as any,
resetContext: () => {}
});
// Allows us to bypass some funky unit-testing focus/blur issues by synthetically triggering the underlying events.
// Doesn't seem necessary for standard use... just when unit testing. (Not sure why.)
contextManager.page.on('enabled', (elem) => {
// will need better handling for design-mode iframes and content-editables, probably.
upgradeFocus(elem);
});
// Pre-attaches to the text fixture's elements.
contextManager.initialize();
});
afterEach(() => {
// Since certain user tests change this.
(contextManager as any).engineConfig.hostDevice.touchable = false;
// The main reason we set `ContextManager` in `beforeEach` - to make cleanup after
// each test round much simpler to maintain. If not reset, the unit test stuff can
// collapse due to side-effects - certain elements only attach if `touchable == false`.
//
// ... I could probably just use an input element and textarea element fixture for those
// tests and be fine, rather than the full gamut.
contextManager?.shutdown();
contextManager = null;
keyboardCache = null;
host.innerHTML = '';
});
// ---------------------------- Start of suite 1 -------------------------------
describe('focus management', () => {
it('initial state: null', () => {
assert.isNotOk(contextManager.activeTextStore);
});
it('change: null -> input', () => {
const textstorechange = sinon.fake();
contextManager.on('textstorechange', textstorechange);
const input = document.getElementById('input');
dispatchFocus('focus', input);
assert.equal(contextManager.activeTextStore?.getElement(), input, ".activeTextStore not updated when element gained focus");
// Check our expectations re: the `textstorechange` event.
assert.isTrue(textstorechange.calledOnce, 'textstorechange event not raised');
const textStore = textstorechange.firstCall.args[0]; // Should be an `InputElementTextStore` instance.
assert.equal(textStore.getElement(), input, '.activeTextStore does not match the newly-focused element');
});
it('change: null -> textarea', () => {
const textstorechange = sinon.fake();
contextManager.on('textstorechange', textstorechange);
const textarea = document.getElementById('textarea');
dispatchFocus('focus', textarea);
assert.equal(contextManager.activeTextStore?.getElement(), textarea, ".activeTextStore not updated when element gained focus");
// Check our expectations re: the `textstorechange` event.
assert.isTrue(textstorechange.calledOnce, 'textstorechange event not raised');
const textStore = textstorechange.firstCall.args[0]; // Should be an `InputElementTextStore` instance.
assert.equal(textStore.getElement(), textarea, '.activeTextStore does not match the newly-focused element');
});
it('change: null -> designIframe', () => {
const textstorechange = sinon.fake();
contextManager.on('textstorechange', textstorechange);
const iframe = document.getElementById('design-iframe') as HTMLIFrameElement;
// Assumes we're testing with Chrome, not Firefox - the latter needs to be
// against the contentDocument, not its .body, I think.
//
// Either way, note that focus is handled specially for design-iframes, thus
// we need slightly different focus-dispatch here.
//
// Possible future improvement: TextStore.focusElement (property)?
// Though that may be affected by the Chrome vs Firefox bit noted above.
dispatchFocus('focus', iframe.contentDocument.body);
assert.equal(contextManager.activeTextStore?.getElement(), iframe, ".activeTextStore not updated when element gained focus");
// Check our expectations re: the `textstorechange` event.
assert.isTrue(textstorechange.calledOnce, 'textstorechange event not raised');
const textStore = textstorechange.firstCall.args[0]; // Should be an `InputElementTextStore` instance.
assert.equal(textStore.getElement(), iframe, '.activeTextStore does not match the newly-focused element');
});
it('change: null -> contentEditable', () => {
const textstorechange = sinon.fake();
contextManager.on('textstorechange', textstorechange);
const editable = document.getElementById('editable');
dispatchFocus('focus', editable);
assert.equal(contextManager.activeTextStore?.getElement(), editable, ".activeTextStore not updated when element gained focus");
// Check our expectations re: the `textstorechange` event.
assert.isTrue(textstorechange.calledOnce, 'textstorechange event not raised');
const textStore = textstorechange.firstCall.args[0]; // Should be an `InputElementTextStore` instance.
assert.equal(textStore.getElement(), editable, '.activeTextStore does not match the newly-focused element');
});
it('change: input -> null', () => {
// Setup: from prior test
const textstorechange = sinon.fake();
contextManager.on('textstorechange', textstorechange);
const input = document.getElementById('input');
dispatchFocus('focus', input);
assert.equal(contextManager.activeTextStore?.getElement(), input);
// actual test
dispatchFocus('blur', input);
assert.equal(contextManager.activeTextStore, null, '.activeTextStore not updated when element lost focus');
// Check our expectations re: the `textstorechange` event.
assert.isTrue(textstorechange.calledTwice, 'textstorechange event not raised');
const textStore = textstorechange.secondCall.args[0]; // Should be null, since we lost focus.
assert.equal(textStore, null, 'textstorechange event did not indicate clearing of .activeTextStore');
});
it('change: input disabled, -> null', async () => {
// Setup: from prior test
const textstorechange = sinon.fake();
contextManager.on('textstorechange', textstorechange);
const input = document.getElementById('input');
dispatchFocus('focus', input);
assert.equal(contextManager.activeTextStore?.getElement(), input);
// actual test
contextManager.page.disableControl(input);
// Relies on a MutationObserver (for 'kmw-disabled' CSS class name checks) to resolve
await timedPromise(10);
assert.equal(contextManager.activeTextStore, null, '.activeTextStore not updated when element KMW-disabled');
// Check our expectations re: the `textstorechange` event.
assert.isTrue(textstorechange.calledTwice, 'textstorechange event not raised');
const textStore = textstorechange.secondCall.args[0]; // Should be null, since we lost focus.
assert.equal(textStore, null, 'textstorechange event did not indicate clearing of .activeTextStore');
});
it('change: input -> textarea', () => {
// Setup: from prior test
const textstorechange = sinon.fake();
contextManager.on('textstorechange', textstorechange);
const input = document.getElementById('input');
dispatchFocus('focus', input);
dispatchFocus('blur', input);
assert.equal(contextManager.activeTextStore?.getElement(), null);
// And now the new stuff.
const textarea = document.getElementById('textarea');
dispatchFocus('focus', textarea);
assert.equal(contextManager.activeTextStore?.getElement(), textarea, ".activeTextStore not updated when element gained focus");
// Check our expectations re: the `textstorechange` event.
assert.isTrue(textstorechange.calledThrice, 'textstorechange event not raised');
const textStore = textstorechange.thirdCall.args[0]; // Should be an `InputElementTextStore` instance.
assert.equal(textStore.getElement(), textarea, '.activeTextStore does not match the newly-focused element');
});
it('restoration: input (no flags set)', () => {
const textstorechange = sinon.fake();
contextManager.on('textstorechange', textstorechange);
const input = document.getElementById('input');
dispatchFocus('focus', input);
assert.isTrue(textstorechange.calledOnce);
dispatchFocus('blur', input);
assert.isTrue(textstorechange.calledTwice);
assert.equal(contextManager.activeTextStore?.getElement(), null);
contextManager.restoreLastActiveTextStore();
assert.isTrue(textstorechange.calledThrice);
assert.equal(contextManager.activeTextStore?.getElement(), input);
});
it('forget: input', () => {
const textstorechange = sinon.fake();
contextManager.on('textstorechange', textstorechange);
const input = document.getElementById('input');
dispatchFocus('focus', input);
assert.isTrue(textstorechange.calledOnce);
contextManager.focusAssistant.maintainingFocus = true;
contextManager.focusAssistant.restoringFocus = true;
contextManager.forgetActiveTextStore();
// The 'forget' operation is **aggressive**. Perma-forget.
assert.isNotOk(contextManager.lastActiveTextStore);
assert.isTrue(textstorechange.calledTwice);
assert.equal(contextManager.activeTextStore?.getElement(), null);
// Again, the 'forget' operation is **aggressive**. Clears all focus-maintenance states.
// After all, there's no longer any prior textStore to maintain or restore - it's forgotten.
assert.equal(contextManager.focusAssistant.maintainingFocus, false);
assert.equal(contextManager.focusAssistant.restoringFocus, false);
dispatchFocus('blur', input); // Should be 100% ignored
assert.isTrue(textstorechange.calledTwice); // there should be no effect.
assert.equal(contextManager.activeTextStore?.getElement(), null);
// If we aren't careful, we can accidentally 'unforget' the element here!
assert.isNotOk(contextManager.lastActiveTextStore, "post-forget textStore blur restored .lastActiveTextStore");
contextManager.restoreLastActiveTextStore();
assert.isTrue(textstorechange.calledTwice); // there should be no effect.
assert.equal(contextManager.activeTextStore?.getElement(), null);
});
it('restoration: input (`maintaining`)', () => {
const textstorechange = sinon.fake();
contextManager.on('textstorechange', textstorechange);
const input = document.getElementById('input');
dispatchFocus('focus', input);
assert.isTrue(textstorechange.calledOnce);
contextManager.focusAssistant.maintainingFocus = true;
dispatchFocus('blur', input); // ignored
assert.isTrue(textstorechange.calledOnce);
// b/c is 'maintained'
assert.isTrue(textstorechange.calledOnce, 'textstorechange called on blur during maintaining state');
assert.equal(contextManager.activeTextStore?.getElement(), input);
contextManager.restoreLastActiveTextStore();
assert.equal(contextManager.activeTextStore?.getElement(), input);
// Since we never 'lost' focus due to the 'maintaining' state, we should only
// have the initial 'textstorechange' raise.
assert.isTrue(textstorechange.calledOnce, 'textstorechange called during restoration of maintained state');
});
it('loss: input (on clear of `maintaining`)', () => {
const textstorechange = sinon.fake();
contextManager.on('textstorechange', textstorechange);
const input = document.getElementById('input');
dispatchFocus('focus', input);
assert.isTrue(textstorechange.calledOnce);
contextManager.focusAssistant.maintainingFocus = true;
dispatchFocus('blur', input); // ignored
// b/c is 'maintained'
assert.isTrue(textstorechange.calledOnce);
assert.equal(contextManager.activeTextStore?.getElement(), input);
contextManager.focusAssistant.maintainingFocus = false;
assert.isTrue(textstorechange.calledTwice);
assert.equal(contextManager.activeTextStore?.getElement(), null);
});
it('restoration: input (`restoring`)', () => {
const textstorechange = sinon.fake();
contextManager.on('textstorechange', textstorechange);
const input = document.getElementById('input');
dispatchFocus('focus', input); // 1
assert.isTrue(textstorechange.calledOnce);
contextManager.focusAssistant.maintainingFocus = true;
dispatchFocus('blur', input);
assert.isTrue(textstorechange.calledOnce); // 'maintaining' state
assert.equal(contextManager.activeTextStore?.getElement(), input);
contextManager.focusAssistant.maintainingFocus = false;
assert.isTrue(textstorechange.calledTwice);
assert.equal(contextManager.activeTextStore?.getElement(), null);
contextManager.focusAssistant.restoringFocus = true;
contextManager.restoreLastActiveTextStore();
assert.isTrue(textstorechange.calledThrice);
assert.equal(contextManager.activeTextStore?.getElement(), input);
});
});
// ------------------------- Second suite: keyboard-related tests --------------------------
describe('keyboard management', () => {
let apiStubs: any;
/**
* Preloaded versions of the keyboards useful for bypassing loading times / allowing synchronicity
* within individual test definitions.
*/
let KEYBOARDS: any;
// const fixture = await fetch('/resources/fixtures/a-bit-of-everything.html');
// host.innerHTML = await fixture.text();
before(async () => {
// Defined here just in case they move later; it'll trigger a failed test on 'before', rather
// than crashing while setting up the tests.
apiStubs = [
'/keyboards/khmer_angkor',
'/keyboards/lao_2008_basic',
'/keyboards/test_chirality',
'/keyboards/test_deadkeys'
];
KEYBOARDS = await loadKeyboardsForTests(apiStubs);
});
beforeEach(() => {
// Since `contextManager` and `keyboardCache` are replaced `beforeEach` test, we need to prep
// the stubs here. They'll all be available, as stubs, within the cache.
for(const key in KEYBOARDS) {
keyboardCache.addStub(KEYBOARDS[key].metadata);
}
});
// At the start of each test, preloaded versions of the keyboards are available BUT NOT in the cache,
// while their stubs for all are fully preloaded and within the cache. Test assertions against
// test keyboard objects themselves are greatly facilitated by having known, preloaded instances.
it('initializes in global-keyboard mode', () => {
assert.isNotOk((contextManager as any).keyboardTarget);
});
describe('global-keyboard mode only' , () => {
it('activate: without .activeTextStore, null -> null (desktop)', async () => {
const beforekeyboardchange = sinon.fake();
const keyboardchange = sinon.fake();
const keyboardasyncload = sinon.fake();
contextManager.on('beforekeyboardchange', beforekeyboardchange);
contextManager.on('keyboardchange', keyboardchange);
contextManager.on('keyboardasyncload', keyboardasyncload);
await contextManager.activateKeyboard('', '');
// When no keyboard is set, the keyboard-metadata pair object itself should be null.
assert.equal(contextManager.activeKeyboard, null);
// Even though it's to effectively the same keyboard, we reload it (in case its stub
// has been replaced)
assert.isTrue(beforekeyboardchange.calledOnce);
// Changing from null-to-null should be a non change; see keyman/keymanweb#96.
assert.isFalse(keyboardchange.calledOnce);
assert.isTrue(keyboardasyncload.notCalled);
});
it('activate: without .activeTextStore, null -> null (touch)', async () => {
const beforekeyboardchange = sinon.fake();
const keyboardchange = sinon.fake();
const keyboardasyncload = sinon.fake();
contextManager.on('beforekeyboardchange', beforekeyboardchange);
contextManager.on('keyboardchange', keyboardchange);
contextManager.on('keyboardasyncload', keyboardasyncload);
// Hacky override - activate touch mode.
(contextManager as any).engineConfig.hostDevice.touchable = true;
await contextManager.activateKeyboard('', '');
// When no keyboard is set, the keyboard-metadata pair object itself should be null.
assert.equal(contextManager.activeKeyboard?.metadata?.id, 'khmer_angkor');
assert.isTrue(beforekeyboardchange.calledTwice); // khmer_angkor is dynamically loaded.
assert.isTrue(keyboardchange.calledOnce);
assert.isTrue(keyboardasyncload.calledOnce); // Again, dynamically loaded.
assert.equal(keyboardchange.firstCall.args[0].metadata.id, 'khmer_angkor');
});
it('activate: without .activeTextStore, preloaded keyboard', async () => {
const beforekeyboardchange = sinon.fake();
const keyboardchange = sinon.fake();
const keyboardasyncload = sinon.fake();
contextManager.on('beforekeyboardchange', beforekeyboardchange);
contextManager.on('keyboardchange', keyboardchange);
contextManager.on('keyboardasyncload', keyboardasyncload);
keyboardCache.addKeyboard(KEYBOARDS.khmer_angkor.keyboard);
await contextManager.activateKeyboard('khmer_angkor', 'km');
// The instance itself may differ, but the .keyboard and .metadata entries will
// be matching instances thanks to preloading.
assert.deepEqual(contextManager.activeKeyboard, KEYBOARDS.khmer_angkor);
assert.isTrue(beforekeyboardchange.calledOnce);
assert.isTrue(keyboardchange.calledOnce);
assert.isTrue(keyboardasyncload.notCalled);
assert.deepEqual(keyboardchange.firstCall.args[0], KEYBOARDS.khmer_angkor);
assert.strictEqual(keyboardchange.firstCall.args[0], contextManager.activeKeyboard);
});
it('activate: without .activeTextStore, loads keyboard', async () => {
const beforekeyboardchange = sinon.fake();
const keyboardchange = sinon.fake();
const keyboardasyncload = sinon.fake();
contextManager.on('beforekeyboardchange', beforekeyboardchange);
contextManager.on('keyboardasyncload', keyboardasyncload);
contextManager.on('keyboardchange', keyboardchange);
// No preloading - `contextManager` should be able to handle it so long as
// a matching stub already exists.
await contextManager.activateKeyboard('khmer_angkor', 'km');
// The instance itself may differ, but the .keyboard and .metadata entries will
// be matching instances thanks to preloading.
assert.isTrue(beforekeyboardchange.calledTwice); // Matches pre-modularized KMW behavior.
assert.isTrue(keyboardchange.calledOnce);
assert.isTrue(keyboardasyncload.calledOnce);
assert.equal(contextManager.activeKeyboard.metadata.id, 'khmer_angkor');
assert.equal(contextManager.activeKeyboard.keyboard.id, prefixed('khmer_angkor'));
assert.equal(contextManager.activeKeyboard.metadata.langId, 'km');
await assertPromiseResolved(keyboardasyncload.firstCall.args[1]);
});
it('activate: without .activeTextStore, missing definition (desktop)', async () => {
const beforekeyboardchange = sinon.fake();
const keyboardchange = sinon.fake();
const keyboardasyncload = sinon.fake();
contextManager.on('beforekeyboardchange', beforekeyboardchange);
contextManager.on('keyboardasyncload', keyboardasyncload);
contextManager.on('keyboardchange', keyboardchange);
// Setup
keyboardCache.addKeyboard(KEYBOARDS.lao_2008_basic.keyboard);
await contextManager.activateKeyboard('lao_2008_basic', 'lo');
// Actual test
try {
await blockConsoleAndAwait(() => contextManager.activateKeyboard('not_defined', 'n/a'));
assert.fail();
} catch (err) {
// Good, an error surfaced.
// Could make assertions about the error?
}
// Fallback behavior: deactivate the keyboard entirely (if on desktop)
assert.equal(contextManager.activeKeyboard, null);
// The two requests - initial setup, then to the default keyboard. No attempts
// are made for the erroneous keyboard because no matching stub could be found.
assert.isTrue(beforekeyboardchange.calledTwice); // Requested twice.
assert.isTrue(keyboardchange.calledTwice); // Actually does change the keyboard twice
assert.isTrue(keyboardasyncload.notCalled);
});
it('activate: without .activeTextStore, missing definition (touch)', async () => {
const beforekeyboardchange = sinon.fake();
const keyboardchange = sinon.fake();
const keyboardasyncload = sinon.fake();
contextManager.on('beforekeyboardchange', beforekeyboardchange);
contextManager.on('keyboardasyncload', keyboardasyncload);
contextManager.on('keyboardchange', keyboardchange);
// Hacky override - activate touch mode.
(contextManager as any).engineConfig.hostDevice.touchable = true;
// Setup
keyboardCache.addKeyboard(KEYBOARDS.lao_2008_basic.keyboard);
await contextManager.activateKeyboard('lao_2008_basic', 'lo');
// Actual test
try {
await blockConsoleAndAwait(() => contextManager.activateKeyboard('not_defined', 'n/a'));
assert.fail();
} catch (err) {
// Good, an error surfaced.
// Could make assertions about the error?
}
// Fallback behavior: activate the first registered stub (if on touch)
assert.equal(contextManager.activeKeyboard?.metadata.id, 'khmer_angkor');
// The two requests - initial setup, then to the default keyboard. No attempts
// are made for the erroneous keyboard because no matching stub could be found.
assert.isTrue(beforekeyboardchange.calledThrice); // Requested three times - the latter two for `khmer_angkor` b/c async.
assert.isTrue(keyboardchange.calledTwice); // Actually does change the keyboard twice
assert.isTrue(keyboardasyncload.calledOnce);
});
it('reactivate: re-requests the already-active keyboard', async () => {
const beforekeyboardchange = sinon.fake();
const keyboardchange = sinon.fake();
const keyboardasyncload = sinon.fake();
contextManager.on('beforekeyboardchange', beforekeyboardchange);
contextManager.on('keyboardasyncload', keyboardasyncload);
contextManager.on('keyboardchange', keyboardchange);
// Setup
keyboardCache.addKeyboard(KEYBOARDS.lao_2008_basic.keyboard);
await contextManager.activateKeyboard('lao_2008_basic', 'lo');
// Actual test
await contextManager.activateKeyboard('lao_2008_basic', 'lo');
assert.equal(contextManager.activeKeyboard.metadata.id, 'lao_2008_basic');
// Even though it's to effectively the same keyboard, we reload it (in case its stub
// has been replaced)
assert.isTrue(beforekeyboardchange.calledTwice);
assert.isTrue(keyboardchange.calledTwice);
assert.isTrue(keyboardasyncload.notCalled);
assert.deepEqual(keyboardchange.secondCall.args[0], keyboardchange.firstCall.args[0]);
});
it('focus gained during activation', async () => {
const beforekeyboardchange = sinon.fake();
const keyboardchange = sinon.fake();
const keyboardasyncload = sinon.fake();
contextManager.on('beforekeyboardchange', beforekeyboardchange);
contextManager.on('keyboardasyncload', keyboardasyncload);
contextManager.on('keyboardchange', keyboardchange);
const fetchPromise = contextManager.activateKeyboard('khmer_angkor', 'km');
// Before we sync up, we shift the active context target.
// Fortunately, this operation is synchronous... so no race conditions here.
//
// As there was no prior element to consider, this change of focus does not attempt
// to re-apply existing settings in the midst of the prior line's activation.
const input = document.getElementById('input');
dispatchFocus('focus', input);
await fetchPromise;
// The instance itself may differ, but the .keyboard and .metadata entries will
// be matching instances thanks to preloading.
assert.isTrue(beforekeyboardchange.calledTwice); // Matches pre-modularized KMW behavior.
assert.isTrue(keyboardchange.calledOnce);
assert.isTrue(keyboardasyncload.calledOnce);
assert.equal(contextManager.activeKeyboard.metadata.id, 'khmer_angkor');
assert.equal(contextManager.activeKeyboard.keyboard.id, prefixed('khmer_angkor'));
assert.equal(contextManager.activeKeyboard.metadata.langId, 'km');
await assertPromiseResolved(keyboardasyncload.firstCall.args[1]);
});
it('focus changed fully after keyboard activation', async () => {
// We activate a keyboard before proceeding.
keyboardCache.addKeyboard(KEYBOARDS.khmer_angkor.keyboard);
await contextManager.activateKeyboard('khmer_angkor', 'km');
const textarea = document.getElementById('textarea');
dispatchFocus('focus', textarea);
const beforekeyboardchange = sinon.fake();
const keyboardchange = sinon.fake();
const keyboardasyncload = sinon.fake();
contextManager.on('beforekeyboardchange', beforekeyboardchange);
contextManager.on('keyboardasyncload', keyboardasyncload);
contextManager.on('keyboardchange', keyboardchange);
// Before we sync up, we shift the active context target.
// Fortunately, this operation is synchronous... so no race conditions here.
//
// Since a prior context was active, KMW will reapply the "current" (before
// activation) keyboard during the focus change (b/c _FocusKeyboardSettings).
const input = document.getElementById('input');
dispatchFocus('blur', textarea);
dispatchFocus('focus', input);
// Allows the _FocusKeyboardSettings trigger to resolve.
await timedPromise(25);
// No need to 'keyboardchange' when the same keyboard is kept active.
assert.isTrue(beforekeyboardchange.notCalled);
assert.isTrue(keyboardchange.notCalled);
assert.isTrue(keyboardasyncload.notCalled);
});
it('focus changed during activation', async () => {
const textarea = document.getElementById('textarea');
dispatchFocus('focus', textarea);
const beforekeyboardchange = sinon.fake();
const keyboardchange = sinon.fake();
const keyboardasyncload = sinon.fake();
contextManager.on('beforekeyboardchange', beforekeyboardchange);
contextManager.on('keyboardasyncload', keyboardasyncload);
contextManager.on('keyboardchange', keyboardchange);
// Adds a delay to the activate keyboard call, to help prevent race conditions below.
const fetchPromise = withDelayedFetching(keyboardLoader, 25, () => {
return contextManager.activateKeyboard('khmer_angkor', 'km')
});
/*
* There's a resolved-promise .then() before the `keyboardasyncload` event can fire;
* in the main engine, this is used to defer until the engine is initialized
* sufficiently enough to proceed.
*/
await Promise.resolve();
assert.isTrue(beforekeyboardchange.calledOnce); // Matches pre-modularized KMW behavior.
assert.isTrue(keyboardchange.notCalled);
// Is triggered via Promise.then() - it's how the main engine ensures actual load
// attempts are deferred until after engine init.
assert.isTrue(keyboardasyncload.calledOnce);
// Before we sync up, we shift the active context target.
//
// Since a prior context was active, KMW will reapply the "current" (before
// activation) keyboard during the focus change (b/c _FocusKeyboardSettings).
// Which triggers a separate `activateKeyboard` call... which can fortunately
// resolve-near instantly.
//
// Therefore, we must prevent race conditions on resolution order...
// hence the `withDelayedFetching` method.
const input = document.getElementById('input');
dispatchFocus('blur', textarea);
dispatchFocus('focus', input);
await timedPromise(10);
// No need to 'keyboardchange' when the same keyboard is kept active.
assert.isTrue(beforekeyboardchange.calledOnce);
assert.isTrue(keyboardchange.notCalled);
// Is triggered via Promise.then() - it's how the main engine ensures actual load
// attempts are deferred until after engine init.
assert.isTrue(keyboardasyncload.calledOnce);
// And now we const all the async stuff resolve.
await fetchPromise;
// The instance itself may differ, but the .keyboard and .metadata entries will
// be matching instances thanks to preloading.
assert.isTrue(beforekeyboardchange.calledTwice); // +1: after async load completed.
assert.isTrue(keyboardchange.calledOnce); // +1: after async load completed.
assert.isTrue(keyboardasyncload.calledOnce);
assert.equal(contextManager.activeKeyboard.metadata.id, 'khmer_angkor');
assert.equal(contextManager.activeKeyboard.keyboard.id, prefixed('khmer_angkor'));
assert.equal(contextManager.activeKeyboard.metadata.langId, 'km');
await assertPromiseResolved(keyboardasyncload.firstCall.args[1]);
});
});
describe('independent-keyboard mode', () => {
it('mode activation', async () => {
keyboardCache.addKeyboard(KEYBOARDS.khmer_angkor.keyboard);
keyboardCache.addKeyboard(KEYBOARDS.lao_2008_basic.keyboard);
// We activate a keyboard before proceeding.
await contextManager.activateKeyboard('khmer_angkor', 'km');
const beforekeyboardchange = sinon.fake();
const keyboardchange = sinon.fake();
const keyboardasyncload = sinon.fake();
contextManager.on('beforekeyboardchange', beforekeyboardchange);
contextManager.on('keyboardasyncload', keyboardasyncload);
contextManager.on('keyboardchange', keyboardchange);
const textarea = document.getElementById('textarea');
const textStore = textStoreForElement(textarea);
contextManager.setKeyboardForTextStore(textStore, 'lao_2008_basic', 'lo');
// As we haven't yet focused the affected target, no keyboard-change events should have triggered yet.
assert.equal((contextManager as any).currentKeyboardSrcTextStore(), null);
assert.isTrue(beforekeyboardchange.notCalled);
assert.isTrue(keyboardchange.notCalled);
assert.isTrue(keyboardasyncload.notCalled);
assert.strictEqual(contextManager.activeKeyboard.metadata, KEYBOARDS.khmer_angkor.metadata);
dispatchFocus('focus', textarea);
// Allows any _FocusKeyboardSettings stuff trigger to resolve.
await timedPromise(10);
// No need to 'keyboardchange' when the same keyboard is kept active.
assert.equal((contextManager as any).currentKeyboardSrcTextStore(), textStore);
assert.isTrue(beforekeyboardchange.calledOnce);
assert.isTrue(keyboardchange.calledOnce);
assert.isTrue(keyboardasyncload.notCalled);
assert.strictEqual(contextManager.activeKeyboard.metadata, KEYBOARDS.lao_2008_basic.metadata);
// Spin off into separate test!
const input = document.getElementById('input');
dispatchFocus('blur', textarea);
dispatchFocus('focus', input);
// Allows any _FocusKeyboardSettings stuff trigger to resolve.
await timedPromise(10);
assert.equal((contextManager as any).keyboardTarget, null);
assert.isTrue(beforekeyboardchange.calledTwice);
assert.isTrue(keyboardchange.calledTwice);
assert.isTrue(keyboardasyncload.notCalled);
assert.strictEqual(contextManager.activeKeyboard.metadata, KEYBOARDS.khmer_angkor.metadata);
});
it('focus change away, to global-mode target', async () => {
keyboardCache.addKeyboard(KEYBOARDS.khmer_angkor.keyboard);
keyboardCache.addKeyboard(KEYBOARDS.lao_2008_basic.keyboard);
// We activate a keyboard before proceeding.
await contextManager.activateKeyboard('khmer_angkor', 'km');
const textarea = document.getElementById('textarea');
const textStore = textStoreForElement(textarea);
contextManager.setKeyboardForTextStore(textStore, 'lao_2008_basic', 'lo');
dispatchFocus('focus', textarea);
// Allows any _FocusKeyboardSettings stuff trigger to resolve.
await timedPromise(10);
// Actual test: transitioning focus from an independent-mode target
// to a global-mode target.
const beforekeyboardchange = sinon.fake();
const keyboardchange = sinon.fake();
const keyboardasyncload = sinon.fake();
contextManager.on('beforekeyboardchange', beforekeyboardchange);
contextManager.on('keyboardasyncload', keyboardasyncload);
contextManager.on('keyboardchange', keyboardchange);
const input = document.getElementById('input');
dispatchFocus('blur', textarea);
dispatchFocus('focus', input);
// Allows any _FocusKeyboardSettings stuff trigger to resolve.
await timedPromise(10);
assert.equal((contextManager as any).keyboardTarget, null);
assert.isTrue(beforekeyboardchange.calledOnce);
assert.isTrue(keyboardchange.calledOnce);
assert.isTrue(keyboardasyncload.notCalled);
assert.strictEqual(contextManager.activeKeyboard.metadata, KEYBOARDS.khmer_angkor.metadata);
});
it('mode deactivation (target inactive)', async () => {
// Written under the assumption that prior tests in the set pass.
keyboardCache.addKeyboard(KEYBOARDS.khmer_angkor.keyboard);
keyboardCache.addKeyboard(KEYBOARDS.lao_2008_basic.keyboard);
// We activate a keyboard before proceeding.
await contextManager.activateKeyboard('khmer_angkor', 'km');
const textarea = document.getElementById('textarea');
const textStore = textStoreForElement(textarea);
contextManager.setKeyboardForTextStore(textStore, 'lao_2008_basic', 'lo');
dispatchFocus('focus', textarea);
// Allows any _FocusKeyboardSettings stuff trigger to resolve.
await timedPromise(10);
// Transition away to a different element.
const input = document.getElementById('input');
dispatchFocus('blur', textarea);
dispatchFocus('focus', input);
// Allows any _FocusKeyboardSettings stuff trigger to resolve.
await timedPromise(10);
// Actual test: transitioning focus from an independent-mode target
// to a global-mode target.
contextManager.setKeyboardForTextStore(textStore, null, null);
const beforekeyboardchange = sinon.fake();
const keyboardchange = sinon.fake();
const keyboardasyncload = sinon.fake();
contextManager.on('beforekeyboardchange', beforekeyboardchange);
contextManager.on('keyboardasyncload', keyboardasyncload);
contextManager.on('keyboardchange', keyboardchange);
dispatchFocus('blur', input);
dispatchFocus('focus', textarea);
// Allows any _FocusKeyboardSettings stuff trigger to resolve.
await timedPromise(10);
assert.equal((contextManager as any).keyboardTarget, null);
assert.isTrue(beforekeyboardchange.notCalled);
assert.isTrue(keyboardchange.notCalled);
assert.isTrue(keyboardasyncload.notCalled);
assert.strictEqual(contextManager.activeKeyboard.metadata, KEYBOARDS.khmer_angkor.metadata);
});
it('mode deactivation (target active)', async () => {
// Written under the assumption that prior tests in the set pass.
keyboardCache.addKeyboard(KEYBOARDS.khmer_angkor.keyboard);
keyboardCache.addKeyboard(KEYBOARDS.lao_2008_basic.keyboard);
// We activate a keyboard before proceeding.
await contextManager.activateKeyboard('khmer_angkor', 'km');
const textarea = document.getElementById('textarea');
const textStore = textStoreForElement(textarea);
contextManager.setKeyboardForTextStore(textStore, 'lao_2008_basic', 'lo');
dispatchFocus('focus', textarea);
// Allows any _FocusKeyboardSettings stuff trigger to resolve.
await timedPromise(10);
const beforekeyboardchange = sinon.fake();
const keyboardchange = sinon.fake();
const keyboardasyncload = sinon.fake();
contextManager.on('beforekeyboardchange', beforekeyboardchange);
contextManager.on('keyboardasyncload', keyboardasyncload);
contextManager.on('keyboardchange', keyboardchange);
// Actual test: transitioning focus from an independent-mode target
// to a global-mode target.
contextManager.setKeyboardForTextStore(textStore, null, null);
// Allow the indirect keyboard-change operation to resolve.
await timedPromise(10);
assert.equal((contextManager as any).keyboardTarget, null);
assert.isTrue(beforekeyboardchange.calledOnce);
assert.isTrue(keyboardchange.calledOnce);
assert.isTrue(keyboardasyncload.notCalled);
assert.strictEqual(contextManager.activeKeyboard?.metadata, KEYBOARDS.khmer_angkor.metadata);
});
it('change of target\'s set keyboard', async () => {
keyboardCache.addKeyboard(KEYBOARDS.khmer_angkor.keyboard);
keyboardCache.addKeyboard(KEYBOARDS.lao_2008_basic.keyboard);
keyboardCache.addKeyboard(KEYBOARDS.test_chirality.keyboard);
// We activate a keyboard before proceeding.
await contextManager.activateKeyboard('khmer_angkor', 'km');
const textarea = document.getElementById('textarea');
const textStore = textStoreForElement(textarea);
contextManager.setKeyboardForTextStore(textStore, 'lao_2008_basic', 'lo');
dispatchFocus('focus', textarea);
// Allows any _FocusKeyboardSettings stuff trigger to resolve.
await timedPromise(10);
// Actual test: transitioning focus from an independent-mode target
// to a global-mode target.
const beforekeyboardchange = sinon.fake();
const keyboardchange = sinon.fake();
const keyboardasyncload = sinon.fake();
contextManager.on('beforekeyboardchange', beforekeyboardchange);
contextManager.on('keyboardasyncload', keyboardasyncload);
contextManager.on('keyboardchange', keyboardchange);
await contextManager.activateKeyboard('test_chirality', 'en');
// Aspect 1: the current keyboard has changed
assert.equal((contextManager as any).currentKeyboardSrcTextStore(), textStore);
assert.isTrue(beforekeyboardchange.calledOnce);
assert.isTrue(keyboardchange.calledOnce);
assert.isTrue(keyboardasyncload.notCalled);
assert.strictEqual(contextManager.activeKeyboard.metadata, KEYBOARDS.test_chirality.metadata);
const input = document.getElementById('input');
dispatchFocus('blur', textarea);
dispatchFocus('focus', input);
// Allows any _FocusKeyboardSettings stuff trigger to resolve.
await timedPromise(10);
// Aspect 2: ... without affecting the global keyboard's setting.
assert.equal((contextManager as any).currentKeyboardSrcTextStore(), null);
assert.isTrue(beforekeyboardchange.calledTwice);
assert.isTrue(keyboardchange.calledTwice);
assert.isTrue(keyboardasyncload.notCalled);
assert.strictEqual(contextManager.activeKeyboard.metadata, KEYBOARDS.khmer_angkor.metadata);
});
it('focus changed during activation on independent-mode target', async () => {
// Only pre-load the 'base' global keyboard.
keyboardCache.addKeyboard(KEYBOARDS.khmer_angkor.keyboard);
keyboardCache.addKeyboard(KEYBOARDS.lao_2008_basic.keyboard);
// We activate the global keyboard before proceeding.
await contextManager.activateKeyboard('khmer_angkor', 'km');
const textarea = document.getElementById('textarea');
const textStore = textStoreForElement(textarea);
contextManager.setKeyboardForTextStore(textStore, 'lao_2008_basic', 'lo');
dispatchFocus('focus', textarea);
// Allows any _FocusKeyboardSettings stuff trigger to resolve.
await timedPromise(10);
// Actual test: transitioning focus from an independent-mode target
// to a global-mode target.
const beforekeyboardchange = sinon.fake();
const keyboardchange = sinon.fake();
const keyboardasyncload = sinon.fake();
contextManager.on('beforekeyboardchange', beforekeyboardchange);
contextManager.on('keyboardasyncload', keyboardasyncload);
contextManager.on('keyboardchange', keyboardchange);
// Core of the test: We're changing the active keyboard for an independent-mode target.
const asyncLoad = withDelayedFetching(keyboardLoader, 50, () => {
return contextManager.activateKeyboard('test_chirality', 'en');
}); // We're simulating a 50 ms delay on the loading of the keyboard script itself.
await Promise.resolve();
// Aspect 1: the current keyboard has not yet changed
assert.equal((contextManager as any).currentKeyboardSrcTextStore(), textStore);
assert.isTrue(beforekeyboardchange.calledOnce); // +1
assert.isTrue(keyboardchange.notCalled); // is delayed 50 ms, so not yet.
assert.isTrue(keyboardasyncload.calledOnce); // The async load has already started.
// There's just artificial loading delay, is all.
assert.strictEqual(contextManager.activeKeyboard.metadata, KEYBOARDS.lao_2008_basic.metadata);
// Aspect 2: swap to a global-mode target, verify expectations.
const input = document.getElementById('input');
dispatchFocus('blur', textarea);
dispatchFocus('focus', input);
// Allows any _FocusKeyboardSettings stuff trigger to resolve.
await timedPromise(10);
assert.equal((contextManager as any).currentKeyboardSrcTextStore(), null);
assert.isTrue(beforekeyboardchange.calledTwice); // +1: re-activating the global keyboard
assert.isTrue(keyboardchange.calledOnce); // +1: same
assert.isTrue(keyboardasyncload.calledOnce);
assert.strictEqual(contextManager.activeKeyboard.metadata, KEYBOARDS.khmer_angkor.metadata);
// Aspect 3: Sync up - without the original, independent-mode target selected.
await asyncLoad;
// ...and verify that the active keyboard has not changed, since the target for
// activation is not itself active.
assert.equal((contextManager as any).currentKeyboardSrcTextStore(), null);
assert.isTrue(beforekeyboardchange.calledTwice); // +1: re-activating the global keyboard
assert.isTrue(keyboardchange.calledOnce); // +1: same
assert.isTrue(keyboardasyncload.calledOnce);
assert.strictEqual(contextManager.activeKeyboard.metadata, KEYBOARDS.khmer_angkor.metadata);
// BUT the async load component should be resolved.
await assertPromiseResolved(keyboardasyncload.firstCall.args[1], 0);
// Aspect 4: swap BACK to the async-loading keyboard's TextStore, which should
// now be fully set to the keyboard that had been requested for activation upon it.
dispatchFocus('blur', input);
dispatchFocus('focus', textarea);
// Allows any _FocusKeyboardSettings stuff trigger to resolve.
await timedPromise(10);
// And, final expectations:
assert.equal((contextManager as any).currentKeyboardSrcTextStore(), textStore);
assert.isTrue(beforekeyboardchange.calledThrice); // +1: activating the independent-mode kbd
assert.isTrue(keyboardchange.calledTwice); // +1: same
assert.isTrue(keyboardasyncload.calledOnce);
// Is the new keyboard, rather than the original one.
assert.strictEqual(contextManager.activeKeyboard.metadata, KEYBOARDS.test_chirality.metadata);
});
it('cancels pending activations when replaced', async () => {
/*
* This delay serves two purposes:
* 1. Ensures that the keyboards have delayed aspects to their operation
* 2. Is long enough that even OS-triggered context-switching effects should
* not prevent said 'delayed aspects'.
* - Race-condition prevention re: the two separate fetch requests.
*/
const FETCH_DELAY = 200; // ms
// Only pre-load the 'base' global + initial-set keyboard.
keyboardCache.addKeyboard(KEYBOARDS.khmer_angkor.keyboard);
// We activate the global keyboard before proceeding.
await contextManager.activateKeyboard('khmer_angkor', 'km');
const textarea = document.getElementById('textarea');
const textStore = textStoreForElement(textarea);
// Matches the current global keyboard, but still sets it to independent-mode.
contextManager.setKeyboardForTextStore(textStore, 'khmer_angkor', 'km');
dispatchFocus('focus', textarea);
// Allows any _FocusKeyboardSettings stuff trigger to resolve.
await timedPromise(10);
// Actual test: transitioning focus from an independent-mode target
// to a global-mode target.
const beforekeyboardchange = sinon.fake();
const keyboardchange = sinon.fake();
const keyboardasyncload = sinon.fake();
contextManager.on('beforekeyboardchange', beforekeyboardchange);
contextManager.on('keyboardasyncload', keyboardasyncload);
contextManager.on('keyboardchange', keyboardchange);
// Time to start the first load request:
const firstActivation = withDelayedFetching(keyboardLoader, FETCH_DELAY, () => {
return contextManager.activateKeyboard('lao_2008_basic', 'lo');
//return contextManager.activateKeyboard('test_chirality', 'en');
}); // We're simulating the delay on the loading of the keyboard script itself.
await Promise.resolve();
// Aspect 1: the current keyboard has not yet changed
assert.equal((contextManager as any).currentKeyboardSrcTextStore(), textStore);
assert.isTrue(beforekeyboardchange.calledOnce); // +1
assert.isTrue(keyboardchange.notCalled); // is delayed 50 ms, so not yet.
assert.isTrue(keyboardasyncload.calledOnce); // The async load has already started.
// There's just artificial loading delay, is all.
assert.strictEqual(contextManager.activeKeyboard.metadata, KEYBOARDS.khmer_angkor.metadata);
// So, how are things handled if we fire off a SECOND load request before the first finishes?
const secondActivation = withDelayedFetching(keyboardLoader, FETCH_DELAY, () => {
return contextManager.activateKeyboard('test_chirality', 'en');
}); // We're simulating the delay on the loading of the keyboard script itself.
// Note that the two activation calls are only separated by a single await Promise.resolve();
// there should be no notable time interval between the two attempts, thus no opportunity for
// the first to resolve before the second has started.
//
// Ideally, we could go with a much shorter fetch delay, but we should play it safe here in case
// of OS context switching.
await Promise.resolve();
// Aspect 2: the current keyboard STILL has not yet changed - still delayed.
assert.equal((contextManager as any).currentKeyboardSrcTextStore(), textStore);
assert.isTrue(beforekeyboardchange.calledTwice); // +1
assert.isTrue(keyboardchange.notCalled); // both should still be delayed.
assert.isTrue(keyboardasyncload.calledTwice); // The async load has already started.
// There's just artificial loading delay, is all.
assert.strictEqual(contextManager.activeKeyboard.metadata, KEYBOARDS.khmer_angkor.metadata);
// Since we don't want any assertions subject to race conditions.
await Promise.all([firstActivation, secondActivation]);
// Critical bit: the `lao` activation should appear to have auto-canceled; this is because
// when its keyboard loaded, we'd already requested the `test_chirality` keyboard.
assert.equal((contextManager as any).currentKeyboardSrcTextStore(), textStore);
assert.isTrue(beforekeyboardchange.calledThrice); // +1
assert.isTrue(keyboardchange.calledOnce); // There should be no attempt to swap to the lao kbd.
assert.isTrue(keyboardasyncload.calledTwice);
assert.strictEqual(contextManager.activeKeyboard.metadata, KEYBOARDS.test_chirality.metadata);
});
it('cancels pending activation when leaving independent mode', async () => {
const FETCH_DELAY = 50;
keyboardCache.addKeyboard(KEYBOARDS.khmer_angkor.keyboard);
await contextManager.activateKeyboard('khmer_angkor', 'km');
const textarea = document.getElementById('textarea');
const textStore = textStoreForElement(textarea);
withDelayedFetching(keyboardLoader, FETCH_DELAY, () => {
contextManager.setKeyboardForTextStore(textStore, 'lao_2008_basic', 'lo');
});
await Promise.resolve();
contextManager.setKeyboardForTextStore(textStore, null, null);
await timedPromise(FETCH_DELAY + 10);
const attachment = textStore.getElement()._kmwAttachment;
assert.isNull(attachment.keyboard);
assert.equal((contextManager as any).currentKeyboardSrcTextStore(), null);
assert.strictEqual(contextManager.activeKeyboard.metadata, KEYBOARDS.khmer_angkor.metadata);
});
});
});
describe('ContextManager.insertText', () =>{
let KEYBOARDS: KeyboardMap;
let kbdInterface: KeyboardInterfaceBase<ContextManager>;
beforeEach(async () => {
KEYBOARDS = await loadKeyboardsForTests(['/keyboards/khmer_angkor']);
const keyboard = KEYBOARDS.khmer_angkor;
keyboardCache.addKeyboard(keyboard.keyboard);
keyboardCache.addStub(keyboard.metadata);
await contextManager.activateKeyboard('khmer_angkor', 'km');
kbdInterface = new KeyboardInterfaceBase<ContextManager>(new Object(), null, null);
});
it('returns false if no text store set', async () => {
// Execute
const ret = contextManager.insertText(kbdInterface, 'a', 0);
// Verify
assert.isFalse(ret);
});
it('returns true if current text store set', async () => {
// Setup
const element = document.getElementById('textarea') as HTMLTextAreaElement;
const textStore = new TextAreaElementTextStore(element);
(contextManager as any).currentTextStore = textStore;
// Execute
const ret = contextManager.insertText(kbdInterface, 'a', 0);
// Verify
assert.isTrue(ret);
const ts = contextManager.activeTextStore;
assert.equal(ts.getText(), 'a');
});
it('returns true if most recent text store set and maintaining focus', async () => {
// Setup
const element = document.getElementById('textarea') as HTMLTextAreaElement;
const textStore = new TextAreaElementTextStore(element);
(contextManager as any).currentTextStore = null;
(contextManager as any).mostRecentTextStore = textStore;
(contextManager as any).focusAssistant = { maintainingFocus: true, isTargetForcingScroll: () => false };
// Execute
const ret = contextManager.insertText(kbdInterface, 'a', 0);
// Verify
assert.isTrue(ret);
const ts = contextManager.activeTextStore;
assert.equal(ts.getText(), 'a');
});
it('returns true if most recent text store and not maintaing focus', async () => {
// Setup
const element = document.getElementById('textarea') as HTMLTextAreaElement;
const textStore = new TextAreaElementTextStore(element);
(contextManager as any).currentTextStore = null;
(contextManager as any).mostRecentTextStore = textStore;
(contextManager as any).focusAssistant = { maintainingFocus: false, isTargetForcingScroll: () => false };
// Execute
const ret = contextManager.insertText(kbdInterface, 'a', 0);
// Verify
assert.isTrue(ret);
const ts = contextManager.activeTextStore;
assert.equal(ts.getText(), 'a');
});
});
});