mirror of
https://github.com/keymanapp/keyman.git
synced 2026-10-25 07:17:41 +00:00
1355 lines
59 KiB
TypeScript
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');
|
|
});
|
|
});
|
|
});
|