spiegel-keyman/developer/src/common/web/utils/test/ldml/ldml-keyboard-xml-reader.tests.ts
Marc Durdin 7d5c98ddba fix(developer): bump fast-xml-parser to 5.0.9
This is a major dependency version bump (4.5.0 to 5.0.9) which we would
normally avoid during beta. However, we need the fix
NaturalIntelligence/fast-xml-parser#725 for hex escapes in XML, which is
incorporated in 5.0.9.

I have assessed the other changes to fast-xml-parser and found no
breaking changes for us, but I tightened the types in xml-utils
declarations and found some minor inconsistencies which appear to have
no impact, and which I have corrected:
* wrong type in unused parameter to `tagValueProcessor`
* reference to unused property `options.emptyTag`

While doing this, I consolidated the common options for the parser in
order to verify consistency, but made no changes to the resolved
parsing/building options.

Added a test to ldml keyboard reading, to verify that numeric strings
are treated as strings, given the divergence in the `numberParseOptions`
option, and it shows that numeric strings are treated as strings.

Fixes: #13348
2025-03-17 06:22:29 +07:00

262 lines
9.5 KiB
TypeScript

import 'mocha';
import {assert} from 'chai';
import { Constants } from '@keymanapp/common-types';
import { CommonTypesMessages } from '../../src/common-messages.js';
import { LKKey, ImportStatus } from '../../src/types/ldml-keyboard/ldml-keyboard-xml.js';
import { testReaderCases } from '../helpers/reader-callback-test.js';
import CLDRScanToVkey = Constants.CLDRScanToVkey;
import CLDRScanToKeyMap = Constants.CLDRScanToKeyMap;
import USVirtualKeyCodes = Constants.USVirtualKeyCodes;
function pluckKeysFromKeybag(keys: LKKey[], ids: string[]) {
return keys.filter(({id}) => ids.indexOf(id) !== -1);
}
describe('ldml keyboard xml reader tests', function () {
this.slow(500); // 0.5 sec -- json schema validation takes a while
testReaderCases([
{
subpath: 'invalid-structure-per-dtd.xml',
errors: [CommonTypesMessages.Error_SchemaValidationError({
instancePath: '/keyboard3',
keyword: 'required',
message: `must have required property 'info'`,
params: 'missingProperty="info"',
})],
},
{
subpath: 'invalid-conforms-to.xml',
errors: [CommonTypesMessages.Error_SchemaValidationError({
instancePath: '/keyboard3/conformsTo',
keyword: 'enum',
message: `must be equal to one of the allowed values`,
params: 'allowedValues="45,46"', // this has to be kept in sync with the DTD
})],
},
{
subpath: 'import-minimal.xml',
callback: (data, source, subpath, callbacks) => {
assert.ok(source?.keyboard3?.keys);
const k = pluckKeysFromKeybag(source?.keyboard3?.keys.key, ['a', 'b', 'c']);
assert.sameDeepOrderedMembers(k.map((entry) => {
// Drop the Symbol members from the returned keys; assertions may expect their presence.
return {
id: entry.id,
output: entry.output
};
}), [
{id: 'a', output: 'a'},
{id: 'b', output: 'b'},
{id: 'c', output: 'c'},
]);
// all of the keys are implied imports here
assert.isTrue(ImportStatus.isImpliedImport(source?.keyboard3?.keys.key.find(({id}) => id === 'a')));
assert.isTrue(ImportStatus.isImport(source?.keyboard3?.keys.key.find(({id}) => id === 'a')));
},
},
{
subpath: 'import-minimal1.xml',
callback: (data, source, subpath, callbacks) => {
assert.ok(source?.keyboard3?.keys);
const k = pluckKeysFromKeybag(source?.keyboard3?.keys.key, ['a', 'b', 'c']);
assert.sameDeepOrderedMembers(k.map((entry) => {
// Drop the Symbol members from the returned keys; assertions may expect their presence.
return {
id: entry.id,
output: entry.output
};
}), [
{id: 'a', output: 'a'},
{id: 'b', output: 'b'},
{id: 'c', output: 'c'},
]);
},
},
{
subpath: 'import-minimal2.xml',
callback: (data, source, subpath, callbacks) => {
assert.ok(source?.keyboard3?.keys);
const k = pluckKeysFromKeybag(source?.keyboard3?.keys.key, ['a', 'b', 'c']);
assert.sameDeepOrderedMembers(k.map((entry) => {
// Drop the Symbol members from the returned keys; assertions may expect their presence.
return {
id: entry.id,
output: entry.output
};
}), [
{id: 'a', output: 'a'},
{id: 'b', output: 'b'},
{id: 'c', output: 'c'},
{id: 'a', output: 'å'}, // overridden
]);
},
},
{
subpath: 'import-symbols.xml',
callback: (data, source, subpath, callbacks) => {
assert.ok(source?.keyboard3?.keys);
const k = pluckKeysFromKeybag(source?.keyboard3?.keys.key, ['a', 'b', 'c', 'zz', 'hash', 'hyphen']);
assert.sameDeepOrderedMembers(k.map((entry) => {
// Drop the Symbol members from the returned keys; assertions may expect their presence.
return {
id: entry.id,
output: entry.output
};
}), [
{ id: 'a', output: 'a' }, // implied
{ id: 'b', output: 'b' },
{ id: 'c', output: 'c' },
{ id: 'hash', output: '#' }, // imported symbols
{ id: 'hyphen', output: '-' },
{ id: 'zz', output: 'zz' }, // new key
{ id: 'hash', output: '##' }, // override
]);
// 'a' is an implied import
assert.isTrue(ImportStatus.isImpliedImport(k.find(({id}) => id === 'a')));
assert.isTrue(ImportStatus.isImport(k.find(({id}) => id === 'a')));
// 'hash' is an import but not implied
assert.isFalse(ImportStatus.isImpliedImport(k.find(({id}) => id === 'hash')));
assert.isTrue(ImportStatus.isImport(k.find(({id}) => id === 'hash')));
assert.isFalse(ImportStatus.isLocalImport(k.find(({id}) => id === 'hash')));
// 'zz' is not imported
assert.isFalse(ImportStatus.isImpliedImport(k.find(({id}) => id === 'zz')));
assert.isFalse(ImportStatus.isImport(k.find(({id}) => id === 'zz')));
},
},
{
subpath: 'import-local.xml',
callback: (data, source, subpath, callbacks) => {
assert.ok(source?.keyboard3?.keys);
const k = pluckKeysFromKeybag(source?.keyboard3?.keys.key, ['interrobang','snail']);
assert.sameDeepOrderedMembers(k.map((entry) => {
// Drop the Symbol members from the returned keys; assertions may expect their presence.
return {
id: entry.id,
output: entry.output
};
}), [
{ id: 'interrobang', output: '‽' },
{ id: 'snail', output: '@' },
]);
// all of the keys are implied imports here
assert.isFalse(ImportStatus.isImpliedImport(source?.keyboard3?.keys.key.find(({id}) => id === 'snail')));
assert.isTrue(ImportStatus.isImport(source?.keyboard3?.keys.key.find(({id}) => id === 'snail')));
assert.isTrue(ImportStatus.isLocalImport(source?.keyboard3?.keys.key.find(({id}) => id === 'snail')));
},
},
{
subpath: 'invalid-import-base.xml',
loadfail: true,
errors: [
CommonTypesMessages.Error_ImportInvalidBase({
base: 'SOME_INVALID_BASE',
path: 'B',
subtag: 'C'
}),
],
},
{
subpath: 'invalid-import-local.xml',
loadfail: true,
errors: [
CommonTypesMessages.Error_ImportReadFail({
base: undefined,
path: 'keys-Zyyy-DOESNOTEXIST.xml',
subtag: 'keys'
}),
],
},
{
subpath: 'invalid-import-path.xml',
loadfail: true,
errors: [
CommonTypesMessages.Error_ImportInvalidPath({
base: 'cldr',
path: '45/too/many/slashes/leading/to/nothing-Zxxx-does-not-exist.xml',
subtag: null,
}),
],
},
{
subpath: 'invalid-import-readfail.xml',
loadfail: true,
errors: [
CommonTypesMessages.Error_ImportReadFail({
base: 'cldr',
path: '45/none-Zxxx-does-not-exist.xml',
subtag: null,
}),
],
},
{
subpath: 'invalid-import-wrongroot.xml',
loadfail: true,
errors: [
CommonTypesMessages.Error_ImportWrongRoot({
base: null,
path: '45/keys-Zyyy-punctuation.xml',
subtag: 'flicks',
}),
],
},
{
subpath: 'numeric-id.xml',
callback: (data, source, subpath, callbacks) => {
assert.ok(source?.keyboard3?.keys);
const k = pluckKeysFromKeybag(source?.keyboard3?.keys.key, ['1', '2']);
assert.sameDeepOrderedMembers(k.map((entry) => {
// Drop the Symbol members from the returned keys; assertions may expect their presence.
return {
id: entry.id,
output: entry.output
};
}), [
{id: '1', output: '1'}, //default import
{id: '2', output: '2'}, //default import
{id: '1', output: 'ħ'}, //override
{id: '2', output: 'ថា'}, //override
]);
},
},
]);
});
describe('check scan code routines', () => {
it('should be able to detect bad single scancodes', () => {
const badScans = new Set<number>();
const ckey = CLDRScanToVkey(0xFF, badScans);
assert.isUndefined(ckey, `expected undefined 0xFF`);
assert.equal(badScans.size, 1);
assert.sameDeepMembers(Array.from(badScans.values()), [0xFF]);
});
it('should be able to detect bad list scancodes', () => {
const badScans = new Set<number>();
const ckeys = CLDRScanToKeyMap([[0x02, 0xFF]], badScans);
assert.sameDeepMembers(ckeys, [[49, undefined]]);
assert.equal(badScans.size, 1);
assert.sameDeepMembers(Array.from(badScans.values()), [0xFF]);
});
it('CLDRScanToUSVirtualKeyCodes should be 1:1', () => {
const vkeyToScan = new Map<number, number>();
// check all scancodes
for (let scan = 0; scan <= 0xFF; scan++) {
const vkey = CLDRScanToVkey(scan);
if (vkey === undefined) {
// not mapped, which is OK
continue;
}
if (scan === 0x56 || scan === 0x7D) {
// These both can map to this scancode
assert.equal(vkey, USVirtualKeyCodes.K_oE2);
} else {
// don't check those exceptions
assert.isFalse(vkeyToScan.has(vkey),
`vkey ${vkey} mapped from more than one scancode: ${Number(vkeyToScan.get(vkey)).toString(16)} and ${Number(scan).toString(16)}`);
}
// do make sure nothing else maps to that vkey
vkeyToScan.set(vkey, scan);
}
});
});