change(web): convert engine/main, engine/interfaces tests to TS

Build-bot: skip build:web
Test-bot: skip
This commit is contained in:
Joshua Horton 2025-09-08 13:47:17 -05:00
parent d629149913
commit 29d0cb1eb1
7 changed files with 103 additions and 89 deletions

View file

@ -44,4 +44,4 @@ do_build () {
builder_run_action configure node_select_version_and_npm_ci
builder_run_action clean rm -rf "${KEYMAN_ROOT}/web/build/${SUBPROJECT_NAME}"
builder_run_action build do_build
builder_run_action test test-headless "${SUBPROJECT_NAME}"
builder_run_action test test-headless-typescript "${SUBPROJECT_NAME}"

View file

@ -18,10 +18,10 @@ export interface ModelSpec {
* The path/URL to the file that defines the model. If both `path` and `code` are specified,
* `path` takes precedence.
*/
path: string;
path?: string;
/**
* The raw JS script defining the model. Only used if `path` is not specified.
*/
code: string;
code?: string;
}

View file

@ -49,4 +49,4 @@ do_build () {
builder_run_action configure node_select_version_and_npm_ci
builder_run_action clean rm -rf "$KEYMAN_ROOT/web/build/$SUBPROJECT_NAME"
builder_run_action build do_build
builder_run_action test test-headless "${SUBPROJECT_NAME}"
builder_run_action test test-headless-typescript "${SUBPROJECT_NAME}"

View file

@ -1,5 +1,4 @@
import { assert } from 'chai';
import sinon from 'sinon';
import { PathOptionDefaults, PathConfiguration } from 'keyman/engine/interfaces';

View file

@ -1,13 +1,16 @@
import { assert } from 'chai';
import sinon from 'sinon';
import { LexicalModelTypes } from '@keymanapp/common-types';
import { Worker as LMWorker } from "@keymanapp/lexical-model-layer/node";
import { LanguageProcessor, TranscriptionCache } from 'keyman/engine/main';
import { PredictionContext } from 'keyman/engine/interfaces';
import { Worker as LMWorker } from "@keymanapp/lexical-model-layer/node";
import { DeviceSpec } from 'keyman/engine/keyboard';
import { Mock } from 'keyman/engine/js-processor';
function compileDummyModel(suggestionSets) {
import Suggestion = LexicalModelTypes.Suggestion;
function compileDummyModel(suggestionSets: Suggestion[][]) {
return `
LMLayerWorker.loadModel(new models.DummyModel({
futureSuggestions: ${JSON.stringify(suggestionSets, null, 2)},
@ -15,7 +18,7 @@ LMLayerWorker.loadModel(new models.DummyModel({
`;
}
const appleDummySuggestionSets = [[
const appleDummySuggestionSets: Suggestion[][] = [[
// Set 1:
{
transform: { insert: 'e', deleteLeft: 0},
@ -64,9 +67,10 @@ function dummiedGetLayer() {
}
describe("PredictionContext", () => {
let langProcessor;
let langProcessor: LanguageProcessor;
beforeEach(function() {
// @ts-ignore
langProcessor = new LanguageProcessor(LMWorker, new TranscriptionCache());
});
@ -91,7 +95,7 @@ describe("PredictionContext", () => {
assert.isEmpty(updateFake.firstCall.args[0]); // should have no suggestions. (if convenient for testing)
await promise;
let suggestions;
let suggestions: Suggestion[];
// Initialization results: our first set of dummy suggestions.
assert.equal(updateFake.callCount, 2);
@ -130,7 +134,7 @@ describe("PredictionContext", () => {
assert.isEmpty(updateFake.firstCall.args[0]); // should have no suggestions. (if convenient for testing)
await promise;
let suggestions;
let suggestions: Suggestion[];
// Initialization results: our first set of dummy suggestions.
assert.equal(updateFake.callCount, 2);
@ -205,7 +209,7 @@ describe("PredictionContext", () => {
let updateFake = sinon.fake();
predictiveContext.on('update', updateFake);
let suggestions;
let suggestions: Suggestion[];
let previousTextState = Mock.from(textState);
textState.insertTextBeforeCaret('e'); // appl| + e = apple
@ -281,7 +285,7 @@ describe("PredictionContext", () => {
// We need to capture the suggestion we wish to apply. We could hardcode a forced
// value, but that might become brittle in the long-term.
const originalSuggestionSet = suggestionCaptureFake.firstCall.args[0];
const originalSuggestionSet: Suggestion[] = suggestionCaptureFake.firstCall.args[0];
const suggestionApply = originalSuggestionSet.find((obj) => obj.displayAs == 'apply');
assert.isOk(suggestionApply);
@ -346,7 +350,7 @@ describe("PredictionContext", () => {
await postRevertSuggestions;
assert.equal(updateFake.callCount, 1);
const suggestionsPostReversion = updateFake.firstCall.args[0];
const suggestionsPostReversion: Suggestion[] = updateFake.firstCall.args[0];
assert.deepEqual(suggestionsPostReversion.map((obj) => obj.displayAs), ['reverted']);
});
});

View file

@ -1,27 +1,38 @@
import fs from 'node:fs';
import { createRequire } from 'node:module';
import { assert } from 'chai';
import fs from 'fs';
import { createRequire } from 'module';
const require = createRequire(import.meta.url);
import { InputProcessor } from 'keyman/engine/main';
import { KeyboardInterface, Mock } from 'keyman/engine/js-processor';
import { MinimalKeymanGlobal } from 'keyman/engine/keyboard';
import { NodeKeyboardLoader } from 'keyman/engine/keyboard/node-keyboard-loader';
import { KeyboardTest } from '@keymanapp/recorder-core';
import { KeyboardTest, RecordedPhysicalKeystroke, RecordedSequenceTestSet } from '@keymanapp/recorder-core';
import { Worker } from '@keymanapp/lexical-model-layer/node';
import * as utils from '@keymanapp/web-utils';
import { KeyboardInterface, Mock } from 'keyman/engine/js-processor';
import { KeyEvent, KeyEventSpec, MinimalKeymanGlobal } from 'keyman/engine/keyboard';
import { NodeKeyboardLoader } from 'keyman/engine/keyboard/node-keyboard-loader';
import { InputProcessor } from 'keyman/engine/main';
import DeviceSpec = utils.DeviceSpec;
const KMWString = utils.KMWString;
// Required initialization setup.
global.keyman = {}; // So that keyboard-based checks against the global `keyman` succeed.
// 10.0+ dependent keyboards, like khmer_angkor, will otherwise fail to load.
const require = createRequire(import.meta.url);
let device = {
formFactor: 'phone',
OS: 'ios',
browser: 'safari'
declare global {
var keyman: typeof MinimalKeymanGlobal;
var KeymanWeb: any;
}
// Required initialization setup.
//
// So that keyboard-based checks against the global `keyman` succeed.
// 10.0+ dependent keyboards, like khmer_angkor, will otherwise fail to load.
global['keyman'] = MinimalKeymanGlobal;
let device: DeviceSpec = {
formFactor: DeviceSpec.FormFactor.Phone,
OS: DeviceSpec.OperatingSystem.iOS,
browser: DeviceSpec.Browser.Safari,
touchable: true
};
// Initialize supplementary plane string extensions
@ -31,7 +42,7 @@ KMWString.enableSupplementaryPlane(false);
describe('InputProcessor', function() {
describe('[[constructor]]', function () {
it('should initialize without errors', function () {
let core = new InputProcessor(device);
let core = new InputProcessor(device, null);
assert.isNotNull(core);
});
@ -40,6 +51,7 @@ describe('InputProcessor', function() {
try {
// Can construct without the second parameter; if so, the final assertion - .mayPredict
// will be invalidated. (No worker, no ability to predict.)
// @ts-ignore
core = new InputProcessor(device, Worker);
assert.isOk(core.keyboardProcessor);
@ -60,6 +72,7 @@ describe('InputProcessor', function() {
assert.isUndefined(core.languageProcessor.activeModel);
assert.isFalse(core.languageProcessor.isActive);
assert.isTrue(core.languageProcessor.mayPredict);
assert.isTrue(core.languageProcessor.canEnable);
} finally {
core?.languageProcessor?.shutdown();
}
@ -67,8 +80,7 @@ describe('InputProcessor', function() {
});
describe('efficiency tests', function() {
let testDistribution = [];
let keyboardWithHarness;
let keyboardWithHarness: KeyboardInterface;
let mainWebScriptURL = require.resolve('@keymanapp/lm-worker/worker-main.wrapped.js');
@ -82,28 +94,17 @@ describe('InputProcessor', function() {
}
this.beforeAll(async function() {
testDistribution = [];
for(let c = 'A'.charCodeAt(0); c <= 'Z'.charCodeAt(0); c++) {
let char = String.fromCharCode(c);
testDistribution.push({
keyId: "K_" + char,
p: 1 / 26
});
}
// Load the keyboard.
let keyboardLoader = new NodeKeyboardLoader(new KeyboardInterface({}, MinimalKeymanGlobal));
const keyboard = await keyboardLoader.loadKeyboardFromPath(require.resolve('@keymanapp/common-test-resources/keyboards/test_chirality.js'));
keyboardWithHarness = keyboardLoader.harness;
keyboardWithHarness = keyboardLoader.harness as KeyboardInterface;
keyboardWithHarness.activeKeyboard = keyboard;
});
describe('without fat-fingering', function() {
it('with minimal context (no fat-fingers)', function() {
this.timeout(32); // ms
let core = new InputProcessor(device);
let core = new InputProcessor(device, null);
let context = new Mock("", 0);
core.keyboardProcessor.keyboardInterface = keyboardWithHarness;
@ -112,7 +113,7 @@ describe('InputProcessor', function() {
let key = layout.getLayer('default').getKey('K_A');
let event = keyboard.constructKeyEvent(key, device, core.keyboardProcessor.stateKeys);
let behavior = core.processKeyEvent(event, context);
let behavior = core.processKeyEvent(event, context, null);
assert.isNotNull(behavior);
});
@ -123,7 +124,7 @@ describe('InputProcessor', function() {
this.timeout(500); // 500 ms, excluding text import.
// These often run on VMs, so we'll be a bit generous.
let core = new InputProcessor(device); // I mean, it IS long context, and time
let core = new InputProcessor(device, null); // I mean, it IS long context, and time
// thresholding is disabled within Node.
core.keyboardProcessor.keyboardInterface = keyboardWithHarness;
@ -132,7 +133,7 @@ describe('InputProcessor', function() {
let key = layout.getLayer('default').getKey('K_A');
let event = keyboard.constructKeyEvent(key, device, core.keyboardProcessor.stateKeys);
let behavior = core.processKeyEvent(event, context);
let behavior = core.processKeyEvent(event, context, null);
assert.isNotNull(behavior);
});
});
@ -140,17 +141,16 @@ describe('InputProcessor', function() {
describe('with fat-fingering', function() {
it('with minimal context (with fat-fingers)', function() {
this.timeout(32); // ms
let core = new InputProcessor(device);
let core = new InputProcessor(device, null);
let context = new Mock("", 0);
core.keyboardProcessor.keyboardInterface = keyboardWithHarness;
let keyboard = keyboardWithHarness.activeKeyboard;
let layout = keyboard.layout(utils.DeviceSpec.FormFactor.Phone);
let key = layout.getLayer('default').getKey('K_A');
key.keyDistribution = testDistribution;
let event = keyboard.constructKeyEvent(key, device, core.keyboardProcessor.stateKeys);
let behavior = core.processKeyEvent(event, context);
let behavior = core.processKeyEvent(event, context, null);
assert.isNotNull(behavior);
});
@ -164,51 +164,52 @@ describe('InputProcessor', function() {
// Keep at the same 'order of magnitude' as the
// 'without fat-fingers' test.
let core = new InputProcessor(device); // It IS long context, and time
let core = new InputProcessor(device, null); // It IS long context, and time
// thresholding is disabled within Node.
core.keyboardProcessor.keyboardInterface = keyboardWithHarness;
core.keyboardProcessor.keyboardInterface = keyboardWithHarness as KeyboardInterface;
let keyboard = keyboardWithHarness.activeKeyboard;
let layout = keyboard.layout(utils.DeviceSpec.FormFactor.Phone);
let key = layout.getLayer('default').getKey('K_A');
key.keyDistribution = testDistribution;
let event = keyboard.constructKeyEvent(key, device, core.keyboardProcessor.stateKeys);
let behavior = core.processKeyEvent(event, context);
let behavior = core.processKeyEvent(event, context, null);
assert.isNotNull(behavior);
});
});
});
describe('Deadkeys bug #8568 - backspace should not reset all deadkeys', function () {
let keyboardWithHarness;
let keyboardWithHarness: KeyboardInterface;
let testJSONtext = fs.readFileSync(require.resolve('@keymanapp/common-test-resources/json/engine_tests/8568_deadkeys.json'));
// For convenience we define the key sequence in a test file although we don't use the
// rest of the recorder stuff since it uses only KeyboardProcessor, not InputProcessor.
let testDefinitions = new KeyboardTest(JSON.parse(testJSONtext));
let testDefinitions = new KeyboardTest(JSON.parse(testJSONtext.toString()));
before(async function () {
// Load the keyboard.
let keyboardLoader = new NodeKeyboardLoader(new KeyboardInterface({}, MinimalKeymanGlobal));
const keyboard = await keyboardLoader.loadKeyboardFromPath(require.resolve('@keymanapp/common-test-resources/keyboards/test_8568_deadkeys.js'));
keyboardWithHarness = keyboardLoader.harness;
keyboardWithHarness = keyboardLoader.harness as KeyboardInterface;
keyboardWithHarness.activeKeyboard = keyboard;
// This part provides extra assurance that the keyboard properly loaded.
assert.equal(keyboard.id, "Keyboard_test_8568_deadkeys");
});
for (let testSet of testDefinitions.inputTestSets[0]['testSet']) {
const testsToRun: RecordedSequenceTestSet = testDefinitions.inputTestSets[0] as RecordedSequenceTestSet;
for (let testSet of testsToRun.testSet) {
it(testSet.msg ?? 'test', function() {
this.timeout(32); // ms
let core = new InputProcessor(device);
let core = new InputProcessor(device, null);
let context = new Mock("", 0);
core.keyboardProcessor.keyboardInterface = keyboardWithHarness;
let keyboard = keyboardWithHarness.activeKeyboard;
for (let keystroke of testSet.inputs) {
let keyEvent = {
for (let key of testSet.inputs) {
const keystroke = key as RecordedPhysicalKeystroke;
let keySpec: KeyEventSpec = {
Lcode: keystroke.keyCode,
Lmodifiers: keystroke.modifiers,
LmodifierChange: keystroke.modifierChanged,
@ -217,10 +218,10 @@ describe('InputProcessor', function() {
kName: '',
device: device,
isSynthetic: false,
LisVirtualKey: keyboard.definesPositionalOrMnemonic // Only false for 1.0 keyboards.
LisVirtualKey: keyboard.definesPositionalOrMnemonic // Only false for 1.0 keyboards.,
};
let behavior = core.processKeyEvent(keyEvent, context);
let behavior = core.processKeyEvent(new KeyEvent(keySpec), context, null);
assert.isNotNull(behavior);
}
assert.equal(context.getText(), testSet.output);

View file

@ -1,27 +1,36 @@
import { env } from 'node:process';
import path from 'node:path';
import { assert } from 'chai';
import { LanguageProcessor, TranscriptionCache } from 'keyman/engine/main';
import { SourcemappedWorker as LMWorker } from "@keymanapp/lexical-model-layer/node";
import { LexicalModelCompiler } from '@keymanapp/kmc-model';
import { TestCompilerCallbacks } from '@keymanapp/developer-test-helpers';
import { ModelSpec } from 'keyman/engine/interfaces';
import { LanguageProcessor, TranscriptionCache } from 'keyman/engine/main';
import { MinimalKeymanGlobal } from 'keyman/engine/keyboard';
import { Mock } from 'keyman/engine/js-processor';
/*
* Unit tests for the Dummy prediction model.
*/
import { LexicalModelCompiler } from '@keymanapp/kmc-model';
import path from 'path';
import { TestCompilerCallbacks } from '@keymanapp/developer-test-helpers';
import { env } from 'node:process';
const KEYMAN_ROOT = env.KEYMAN_ROOT;
declare global {
var keyman: typeof MinimalKeymanGlobal;
}
// Required initialization setup.
global.keyman = {}; // So that keyboard-based checks against the global `keyman` succeed.
// 10.0+ dependent keyboards, like khmer_angkor, will otherwise fail to load.
//
// So that keyboard-based checks against the global `keyman` succeed. 10.0+
// dependent keyboards, like khmer_angkor, will otherwise fail to load.
global.keyman = MinimalKeymanGlobal;
// Test the KeyboardProcessor interface.
describe('LanguageProcessor', function() {
let languageProcessor;
let languageProcessor: LanguageProcessor;
const callbacks = new TestCompilerCallbacks();
beforeEach(function() {
@ -57,19 +66,18 @@ describe('LanguageProcessor', function() {
it('has expected default values after initialization', function () {
// These checks are lifted from the keyboard init checks found in
// web/src/test/auto/headless/engine/js-processor/basic-init.js.
assert.isDefined(languageProcessor.lmEngine);
assert.isUndefined(languageProcessor.activeModel);
assert.isFalse(languageProcessor.isActive);
assert.isTrue(languageProcessor.mayPredict);
// Some aspects of initialization must wait until after construction and overall
// load of the core. See /web/source/kmwbase.ts, in the final IIFE.
assert.isOk(languageProcessor.lmEngine);
assert.isTrue(languageProcessor.canEnable);
});
});
describe('.predict', function() {
let compiler = null;
let compiler: LexicalModelCompiler = null;
this.beforeAll(async function() {
compiler = new LexicalModelCompiler();
@ -82,7 +90,8 @@ describe('LanguageProcessor', function() {
const PATH = path.join(`${KEYMAN_ROOT}/developer/src/kmc-model/test/fixtures`, MODEL_ID);
describe('using angle brackets for quotes', function() {
let modelCode = null, modelSpec = null;
let modelCode: string = null;
let modelSpec: ModelSpec = null;
this.beforeAll(function() {
modelCode = compiler.generateLexicalModelCode(MODEL_ID, {
format: 'trie-1.0',
@ -114,7 +123,7 @@ describe('LanguageProcessor', function() {
let transcription = contextSource.buildTranscriptionFrom(contextSource, null, null);
languageProcessor.loadModel(modelSpec).then(function() {
languageProcessor.predict(transcription).then(function(suggestions) {
languageProcessor.predict(transcription, 'default').then(function(suggestions) {
assert.isOk(suggestions);
assert.equal(suggestions[0].displayAs, '«li»');
assert.equal(suggestions[0].transform.insert, 'li');
@ -131,7 +140,8 @@ describe('LanguageProcessor', function() {
});
describe('properly cases generated suggestions', function() {
let modelCode = null, modelSpec = null;
let modelCode: string = null;
let modelSpec: ModelSpec = null;
this.beforeAll(function () {
modelCode = compiler.generateLexicalModelCode(MODEL_ID, {
format: 'trie-1.0',
@ -153,7 +163,7 @@ describe('LanguageProcessor', function() {
let transcription = contextSource.buildTranscriptionFrom(contextSource, null, null);
languageProcessor.loadModel(modelSpec).then(function() {
languageProcessor.predict(transcription).then(function(suggestions) {
languageProcessor.predict(transcription, 'default').then(function(suggestions) {
assert.isOk(suggestions);
assert.equal(suggestions[1].displayAs, 'like');
assert.equal(suggestions[1].transform.insert, 'like');
@ -171,7 +181,7 @@ describe('LanguageProcessor', function() {
let transcription = contextSource.buildTranscriptionFrom(contextSource, null, null);
languageProcessor.loadModel(modelSpec).then(function() {
languageProcessor.predict(transcription).then(function(suggestions) {
languageProcessor.predict(transcription, 'default').then(function(suggestions) {
// The source suggestion is simply 'like'.
assert.isOk(suggestions);
assert.equal(suggestions[1].displayAs, 'like');
@ -190,7 +200,7 @@ describe('LanguageProcessor', function() {
let transcription = contextSource.buildTranscriptionFrom(contextSource, null, null);
languageProcessor.loadModel(modelSpec).then(function() {
languageProcessor.predict(transcription).then(function(suggestions) {
languageProcessor.predict(transcription, 'default').then(function(suggestions) {
assert.isOk(suggestions);
assert.equal(suggestions[1].displayAs, 'I');
assert.equal(suggestions[1].transform.insert, 'I');
@ -210,7 +220,7 @@ describe('LanguageProcessor', function() {
let transcription = contextSource.buildTranscriptionFrom(contextSource, null, null);
languageProcessor.loadModel(modelSpec).then(function() {
languageProcessor.predict(transcription).then(function(suggestions) {
languageProcessor.predict(transcription, 'default').then(function(suggestions) {
// The source suggestion is simply 'like'.
assert.isOk(suggestions);
assert.equal(suggestions[1].displayAs, 'LIKE');
@ -229,7 +239,7 @@ describe('LanguageProcessor', function() {
let transcription = contextSource.buildTranscriptionFrom(contextSource, null, null);
languageProcessor.loadModel(modelSpec).then(function() {
languageProcessor.predict(transcription).then(function(suggestions) {
languageProcessor.predict(transcription, 'default').then(function(suggestions) {
assert.isOk(suggestions);
assert.equal(suggestions[0].displayAs, 'I');
assert.equal(suggestions[0].transform.insert, 'I');
@ -250,7 +260,7 @@ describe('LanguageProcessor', function() {
let transcription = contextSource.buildTranscriptionFrom(contextSource, null, null);
languageProcessor.loadModel(modelSpec).then(function() {
languageProcessor.predict(transcription).then(function(suggestions) {
languageProcessor.predict(transcription, 'default').then(function(suggestions) {
// The source suggestion is simply 'like'.
assert.isOk(suggestions);
assert.equal(suggestions[1].displayAs, 'Like');
@ -271,7 +281,7 @@ describe('LanguageProcessor', function() {
let transcription = contextSource.buildTranscriptionFrom(contextSource, null, null);
languageProcessor.loadModel(modelSpec).then(function() {
languageProcessor.predict(transcription).then(function(suggestions) {
languageProcessor.predict(transcription, 'default').then(function(suggestions) {
// The source suggestion is simply 'like'.
assert.isOk(suggestions);
assert.equal(suggestions[1].displayAs, 'Like');