spiegel-keyman/web/src/test/auto/headless/engine/js-processor/specialized-backspace.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

382 lines
14 KiB
TypeScript

/*
* Keyman is copyright (C) SIL Global. MIT License.
*/
import { assert } from 'chai';
import { createRequire } from 'module';
const require = createRequire(import.meta.url);
import { DeviceSpec, KMWString } from 'keyman/common/web-utils';
import { Codes, DefaultOutputRules, KeyEvent, MinimalKeymanGlobal, SyntheticTextStore } from 'keyman/engine/keyboard';
import { JSKeyboardInterface, JSKeyboardProcessor } from 'keyman/engine/js-processor';
import { NodeKeyboardLoader } from 'keyman/test/resources';
import { VariableStoreTestSerializer } from 'keyman/test/headless-resources';
import { ModifierKeyConstants } from '@keymanapp/common-types';
const TEST_DEVICE = {
formFactor: 'desktop',
OS: 'windows',
browser: 'native',
touchable: false
} as DeviceSpec;
// Basic scaffolding necessary to use special, locally-defined test keyboards.
class CommonKbdScriptProps {
KMINVER = "10.0";
// this.KV left empty - we aren't doing layout stuff for this test, so it's "fine".
// - also this.KV.KLS
// - also this.KV.BK
KH = '';
KM = 0;
KBVER = "0.0.1";
KMBM = ModifierKeyConstants.K_SHIFTFLAG /* 0x0010 */;
gs(t: any, e: any) {
return (this as any).g_main(t, e);
}
}
const COMMON_KBD_SCRIPT_PROPS = new CommonKbdScriptProps();
const DUMMIED_KEYS_KEYBOARD_SCRIPT = function keyboard_core (this: any) {
Object.assign(this, COMMON_KBD_SCRIPT_PROPS);
this.KI="Keyboard_dummied_keys";
this.KN="Dummied Keys";
this.g_main=function(t:any, e:any) {
// All keys match but do nothing.
return 1;
}
this.gs=function(t:any, e:any) {
return this.g_main(t, e);
}
}
const DOUBLED_BKSP_KEYBOARD_SCRIPT = function keyboard_core (this: any) {
Object.assign(this, COMMON_KBD_SCRIPT_PROPS);
this.KI="Keyboard_doubled_backspace";
this.KN="Doubled Backspace";
this.KO=function(count: number, t: any, output: string) {
// Delete 'count' characters from the text store and output the replacement
t.deleteCharsBeforeCaret(count);
if(output) {
t.insertTextBeforeCaret(output);
}
};
this.g_main=function(t: { getTextBeforeCaret: () => string; }, e: { Lcode: number; }) {
// Standard compiled keyboards do not directly use any methods on `t`.
if(e.Lcode == Codes.keyCodes.K_BKSP && KMWString.length(t.getTextBeforeCaret()) >= 2){
// If the context has at least two characters, delete two.
this.KO(2, t, '')
// Our rule matched, so signal that.
return 1;
} else {
// All other keystrokes should result in default behavior.
return 0;
}
}
this.gs=function(t:any, e:any) {
return this.g_main(t, e);
}
}
describe('Engine - specialized backspace handling', function() {
const ipaPath = require.resolve('@keymanapp/common-test-resources/keyboards/sil_ipa.js');
const angkorPath = require.resolve('@keymanapp/common-test-resources/keyboards/khmer_angkor.js');
const device = {
formFactor: 'desktop',
OS: 'windows',
browser: 'native',
touchable: false
} as DeviceSpec;
let ipaWithHarness: JSKeyboardInterface;
let angkorWithHarness: JSKeyboardInterface;
let dummiedWithHarness: JSKeyboardInterface;
let bksp2xWithHarness: JSKeyboardInterface;
before(async () => {
// -- START: Standard keyboard unit test loading boilerplate --
let harness = new JSKeyboardInterface({}, MinimalKeymanGlobal, new VariableStoreTestSerializer());
let keyboardLoader = new NodeKeyboardLoader(harness);
let keyboard = await keyboardLoader.loadKeyboardFromPath(ipaPath);
// -- END: Standard keyboard unit test loading boilerplate --
// This part provides extra assurance that the keyboard properly loaded.
assert.equal(keyboard.id, "Keyboard_sil_ipa");
harness.activeKeyboard = keyboard;
ipaWithHarness = harness;
// --------------
// -- START: Standard keyboard unit test loading boilerplate --
harness = new JSKeyboardInterface({}, MinimalKeymanGlobal, new VariableStoreTestSerializer());
keyboardLoader = new NodeKeyboardLoader(harness);
keyboard = await keyboardLoader.loadKeyboardFromPath(angkorPath);
// -- END: Standard keyboard unit test loading boilerplate --
// This part provides extra assurance that the keyboard properly loaded.
assert.equal(keyboard.id, "Keyboard_khmer_angkor");
harness.activeKeyboard = keyboard;
angkorWithHarness = harness;
// --------------
harness = new JSKeyboardInterface(globalThis, MinimalKeymanGlobal, new VariableStoreTestSerializer());
harness.install();
// Sets the keyboard as the harness's "loaded" keyboard, but not "active".
harness.KR(new (DUMMIED_KEYS_KEYBOARD_SCRIPT as any)());
harness.activeKeyboard = harness.loadedKeyboard;
assert.isOk(harness.activeKeyboard);
dummiedWithHarness = harness;
// --------------
harness = new JSKeyboardInterface(globalThis, MinimalKeymanGlobal, new VariableStoreTestSerializer());
harness.install();
// Sets the keyboard as the harness's "loaded" keyboard, but not "active".
harness.KR(new (DOUBLED_BKSP_KEYBOARD_SCRIPT as any)());
harness.activeKeyboard = harness.loadedKeyboard;
assert.isOk(harness.activeKeyboard);
bksp2xWithHarness = harness;
});
it('empty context, positional keyboard', () => {
const contextSource = new SyntheticTextStore('');
const event = new KeyEvent({
Lcode: Codes.keyCodes.K_BKSP,
Lmodifiers: 0,
Lstates: ModifierKeyConstants.NOTCAPITALFLAG | ModifierKeyConstants.NOTNUMLOCKFLAG | ModifierKeyConstants.NOTSCROLLFLAG,
LisVirtualKey: true,
kName: '',
vkCode: Codes.keyCodes.K_BKSP,
device
});
const processor = new JSKeyboardProcessor(TEST_DEVICE, {
baseLayout: 'us',
keyboardInterface: angkorWithHarness,
defaultOutputRules: new DefaultOutputRules()
});
const result = processor.processKeystroke(event, contextSource);
assert.isTrue(result.triggerKeyDefault);
assert.equal(contextSource.getTextBeforeCaret(), '');
assert.equal(contextSource.getTextAfterCaret(), '');
assert.isOk(result?.transcription?.transform);
const transform = result.transcription.transform;
assert.equal(transform.insert, '');
assert.equal(transform.deleteLeft, 1);
assert.isNotOk(transform.deleteRight);
});
it("empty context, positional keyboard, outputless-key that's not BKSP", () => {
const contextSource = new SyntheticTextStore('');
const event = new KeyEvent({
Lcode: Codes.keyCodes.K_A,
Lmodifiers: 0,
Lstates: ModifierKeyConstants.NOTCAPITALFLAG | ModifierKeyConstants.NOTNUMLOCKFLAG | ModifierKeyConstants.NOTSCROLLFLAG,
LisVirtualKey: true,
kName: '',
vkCode: Codes.keyCodes.K_A,
device: device
});
// A specialized test keyboard that handles keys without emitting any content.
// We want to ensure error cases without output, on null context, don't act
// like backspaces.
const processor = new JSKeyboardProcessor(TEST_DEVICE, {
baseLayout: 'us',
keyboardInterface: dummiedWithHarness,
defaultOutputRules: new DefaultOutputRules()
});
const result = processor.processKeystroke(event, contextSource);
// Did match a keyboard rule.
assert.isFalse(result.triggerKeyDefault);
assert.equal(contextSource.getTextBeforeCaret(), '');
assert.equal(contextSource.getTextAfterCaret(), '');
assert.isOk(result?.transcription?.transform);
const transform = result.transcription.transform;
assert.equal(transform.insert, '');
// Does not trigger backspace-like behavior.
assert.equal(transform.deleteLeft, 0);
assert.isNotOk(transform.deleteRight);
});
it('empty context, positional keyboard, but text is selected', () => {
const contextSource = new SyntheticTextStore('selected text', 0);
contextSource.setSelection(0, KMWString.length(contextSource.getText()));
const event = new KeyEvent({
Lcode: Codes.keyCodes.K_BKSP,
Lmodifiers: 0,
Lstates: ModifierKeyConstants.NOTCAPITALFLAG | ModifierKeyConstants.NOTNUMLOCKFLAG | ModifierKeyConstants.NOTSCROLLFLAG,
LisVirtualKey: true,
kName: '',
vkCode: Codes.keyCodes.K_BKSP,
device: device
});
const processor = new JSKeyboardProcessor(TEST_DEVICE, {
baseLayout: 'us',
keyboardInterface: angkorWithHarness,
defaultOutputRules: new DefaultOutputRules()
});
const result = processor.processKeystroke(event, contextSource);
assert.isFalse(result.triggerKeyDefault);
assert.equal(contextSource.getTextBeforeCaret(), '');
assert.equal(contextSource.getTextAfterCaret(), '');
assert.isOk(result?.transcription?.transform);
const transform = result.transcription.transform;
assert.equal(transform.insert, '');
// This BKSP was to delete selected text, NOT to delete text before the caret.
assert.equal(transform.deleteLeft, 0);
// Is currently not what I'd expect - it reports the length of the selected string instead!
// Fixing that may have knock-on effects, though... and it seems to pass through
// the apps just fine as-is.
//
// I do have concerns for certain scripts on iOS, though... see #4538.
// assert.isNotOk(transform.deleteRight);
});
it('empty left-context, positional keyboard', () => {
const contextSource = new SyntheticTextStore('post-caret text', 0);
const event = new KeyEvent({
Lcode: Codes.keyCodes.K_BKSP,
Lmodifiers: 0,
Lstates: ModifierKeyConstants.NOTCAPITALFLAG | ModifierKeyConstants.NOTNUMLOCKFLAG | ModifierKeyConstants.NOTSCROLLFLAG,
LisVirtualKey: true,
kName: '',
vkCode: Codes.keyCodes.K_BKSP,
device: device
});
const processor = new JSKeyboardProcessor(TEST_DEVICE, {
baseLayout: 'us',
keyboardInterface: angkorWithHarness,
defaultOutputRules: new DefaultOutputRules()
});
const result = processor.processKeystroke(event, contextSource);
assert.isTrue(result.triggerKeyDefault);
assert.equal(contextSource.getTextBeforeCaret(), '');
assert.equal(contextSource.getTextAfterCaret(), 'post-caret text');
assert.isOk(result?.transcription?.transform);
const transform = result.transcription.transform;
assert.equal(transform.insert, '');
assert.equal(transform.deleteLeft, 1);
assert.isNotOk(transform.deleteRight);
});
it('empty context, mnemonic keyboard', () => {
const contextSource = new SyntheticTextStore('');
const event = new KeyEvent({
Lcode: Codes.keyCodes.K_BKSP,
Lmodifiers: 0,
Lstates: ModifierKeyConstants.NOTCAPITALFLAG | ModifierKeyConstants.NOTNUMLOCKFLAG | ModifierKeyConstants.NOTSCROLLFLAG,
LisVirtualKey: true,
kName: '',
vkCode: Codes.keyCodes.K_BKSP,
device: device
});
const processor = new JSKeyboardProcessor(TEST_DEVICE, {
baseLayout: 'us',
keyboardInterface: ipaWithHarness,
defaultOutputRules: new DefaultOutputRules()
});
const result = processor.processKeystroke(event, contextSource);
assert.isTrue(result.triggerKeyDefault);
assert.equal(contextSource.getTextBeforeCaret(), '');
assert.equal(contextSource.getTextAfterCaret(), '');
assert.isOk(result?.transcription?.transform);
const transform = result.transcription.transform;
assert.equal(transform.insert, '');
assert.equal(transform.deleteLeft, 1);
assert.isNotOk(transform.deleteRight);
});
it('final empty context, positional keyboard, rule-handled BKSP', () => {
const contextSource = new SyntheticTextStore('abc', 2);
const event = new KeyEvent({
Lcode: Codes.keyCodes.K_BKSP,
Lmodifiers: 0,
Lstates: ModifierKeyConstants.NOTCAPITALFLAG | ModifierKeyConstants.NOTNUMLOCKFLAG | ModifierKeyConstants.NOTSCROLLFLAG,
LisVirtualKey: true,
kName: '',
vkCode: Codes.keyCodes.K_BKSP,
device: device
});
// A specialized test keyboard that duplicates backspaces when sufficient
// context exists.
const processor = new JSKeyboardProcessor(TEST_DEVICE, {
baseLayout: 'us',
keyboardInterface: bksp2xWithHarness,
defaultOutputRules: new DefaultOutputRules()
});
const result = processor.processKeystroke(event, contextSource);
// Did match a keyboard rule.
assert.isFalse(result.triggerKeyDefault);
assert.equal(contextSource.getTextBeforeCaret(), '');
assert.equal(contextSource.getTextAfterCaret(), 'c');
assert.isOk(result?.transcription?.transform);
const transform = result.transcription.transform;
assert.equal(transform.insert, '');
// Triggered keyboard's backspace behavior, not special started-as-null behavior.
// (Ensure that _ending_ with empty context doesn't overwrite a rule that gets us there.)
assert.equal(transform.deleteLeft, 2);
assert.isNotOk(transform.deleteRight);
});
// Special case: BKSP rule-matches with empty left-context.
it("empty context, positional keyboard, outputless BKSP rule", () => {
const contextSource = new SyntheticTextStore('');
const event = new KeyEvent({
Lcode: Codes.keyCodes.K_BKSP,
Lmodifiers: 0,
Lstates: ModifierKeyConstants.NOTCAPITALFLAG | ModifierKeyConstants.NOTNUMLOCKFLAG | ModifierKeyConstants.NOTSCROLLFLAG,
LisVirtualKey: true,
kName: '',
vkCode: Codes.keyCodes.K_BKSP,
device: device
});
// A specialized test keyboard that handles keys without emitting any content.
// We want to ensure error cases without output, on null context, don't act
// like backspaces.
const processor = new JSKeyboardProcessor(TEST_DEVICE, {
baseLayout: 'us',
keyboardInterface: dummiedWithHarness,
defaultOutputRules: new DefaultOutputRules()
});
const result = processor.processKeystroke(event, contextSource);
assert.isFalse(result.triggerKeyDefault);
assert.equal(contextSource.getTextBeforeCaret(), '');
assert.equal(contextSource.getTextAfterCaret(), '');
assert.isOk(result?.transcription?.transform);
const transform = result.transcription.transform;
assert.equal(transform.insert, '');
// Does not trigger backspace-like behavior because of the keyboard's rule.
assert.equal(transform.deleteLeft, 0);
assert.isNotOk(transform.deleteRight);
});
});