mirror of
https://github.com/keymanapp/keyman.git
synced 2026-08-19 23:07:42 +00:00
feat(common): ldml read keyboard testdata 🙀
- early scaffolding - with some basic tests #7535
This commit is contained in:
parent
251ed6fb8c
commit
9a31e866df
7 changed files with 340 additions and 3 deletions
|
|
@ -0,0 +1,74 @@
|
|||
//
|
||||
// Conforms to techpreview
|
||||
//
|
||||
// The interfaces in this file are designed with reference to the mapped
|
||||
// structures produced by xml2js when passed a LDML keyboard test data .xml file.
|
||||
//
|
||||
// Using prefix LKT for LDML Keyboard Test
|
||||
//
|
||||
|
||||
export interface LDMLKeyboardTestDataXMLSourceFile {
|
||||
/**
|
||||
* <keyboardTest> -- the root element.
|
||||
*/
|
||||
keyboardTest: LKTKeyboardTest;
|
||||
}
|
||||
|
||||
export interface LKTKeyboardTest {
|
||||
conformsTo?: string;
|
||||
info?: LKTInfo;
|
||||
repertoire?: LKTRepertoire[];
|
||||
tests?: LKTTests[];
|
||||
};
|
||||
|
||||
export interface LKTInfo {
|
||||
author?: string;
|
||||
keyboard?: string;
|
||||
name?: string;
|
||||
};
|
||||
|
||||
export interface LKTRepertoire {
|
||||
name?: string;
|
||||
chars?: string;
|
||||
type?: string;
|
||||
};
|
||||
|
||||
export interface LKTTests {
|
||||
name?: string;
|
||||
test?: LKTTest[];
|
||||
};
|
||||
|
||||
export interface LKTTest {
|
||||
name?: string;
|
||||
startContext?: LKTStartContext;
|
||||
actions?: LKTAction[]; // differs from XML, to represent order of actions
|
||||
};
|
||||
|
||||
export interface LKTStartContext {
|
||||
to?: string;
|
||||
};
|
||||
|
||||
export interface LKTCheck {
|
||||
result?: string;
|
||||
};
|
||||
|
||||
export interface LKTEmit {
|
||||
to?: string;
|
||||
};
|
||||
|
||||
export interface LKTKeystroke {
|
||||
key?: string;
|
||||
flick?: string;
|
||||
longPress?: string;
|
||||
tapCount?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Test Actions.
|
||||
* The expectation is that each LKTAction object will have exactly one non-falsy field.
|
||||
*/
|
||||
export interface LKTAction {
|
||||
check?: LKTCheck;
|
||||
emit?: LKTEmit;
|
||||
keystroke?: LKTKeystroke;
|
||||
};
|
||||
|
|
@ -5,6 +5,13 @@ import { boxXmlArray } from '../util/util.js';
|
|||
import { CompilerCallbacks } from '../util/compiler-interfaces.js';
|
||||
import { constants } from '@keymanapp/ldml-keyboard-constants';
|
||||
import { CommonTypesMessages } from '../util/common-events.js';
|
||||
import { LDMLKeyboardTestDataXMLSourceFile } from './ldml-keyboard-testdata-xml.js';
|
||||
|
||||
interface NameAndProps {
|
||||
'$'?: any; // content
|
||||
'#name'?: string; // element name
|
||||
'$$'?: any; // children
|
||||
};
|
||||
|
||||
export default class LDMLKeyboardXMLSourceFileReader {
|
||||
callbacks: CompilerCallbacks;
|
||||
|
|
@ -186,7 +193,7 @@ export default class LDMLKeyboardXMLSourceFileReader {
|
|||
/**
|
||||
* @returns true if valid, false if invalid
|
||||
*/
|
||||
public validate(source: LDMLKeyboardXMLSourceFile, schemaSource: Buffer): boolean {
|
||||
public validate(source: LDMLKeyboardXMLSourceFile | LDMLKeyboardTestDataXMLSourceFile, schemaSource: Buffer): boolean {
|
||||
const schema = JSON.parse(schemaSource.toString('utf8'));
|
||||
const ajv = new Ajv();
|
||||
if(!ajv.validate(schema, source)) {
|
||||
|
|
@ -220,7 +227,7 @@ export default class LDMLKeyboardXMLSourceFileReader {
|
|||
// An alternative fix would be to pull xml2js directly from github
|
||||
// rather than using the version tagged on npmjs.com.
|
||||
});
|
||||
parser.parseString(file, (e: unknown, r: unknown) => { a = r as LDMLKeyboardXMLSourceFile });
|
||||
parser.parseString(file, (e: unknown, r: unknown) => { a = r as LDMLKeyboardXMLSourceFile }); // TODO-LDML: isn't 'e' the error?
|
||||
return a;
|
||||
})();
|
||||
return source;
|
||||
|
|
@ -241,4 +248,111 @@ export default class LDMLKeyboardXMLSourceFileReader {
|
|||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
loadTestDataUnboxed(file: Uint8Array): any {
|
||||
let source = (() => {
|
||||
let a: any;
|
||||
let parser = new xml2js.Parser({
|
||||
// explicitArray: false,
|
||||
preserveChildrenOrder:true, // needed for test data
|
||||
explicitChildren: true, // needed for test data
|
||||
// mergeAttrs: true,
|
||||
// includeWhiteChars: false,
|
||||
// emptyTag: {} as any
|
||||
// Why "as any"? xml2js is broken:
|
||||
// https://github.com/Leonidas-from-XIV/node-xml2js/issues/648 means
|
||||
// that an old version of `emptyTag` is used which doesn't support
|
||||
// functions, but DefinitelyTyped is requiring use of function or a
|
||||
// string. See also notes at
|
||||
// https://github.com/DefinitelyTyped/DefinitelyTyped/pull/59259#issuecomment-1254405470
|
||||
// An alternative fix would be to pull xml2js directly from github
|
||||
// rather than using the version tagged on npmjs.com.
|
||||
});
|
||||
parser.parseString(file, (e: unknown, r: unknown) => { a = r as any }); // TODO-LDML: isn't 'e' the error?
|
||||
return a; // Why 'any'? Because we need to box up the $'s into proper properties.
|
||||
})();
|
||||
return source;
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter the obj array for a subtag
|
||||
* @param source array of source objs
|
||||
* @param subtag subtag to filter on
|
||||
* @returns
|
||||
*/
|
||||
findSubtagArray(source: NameAndProps[], subtag: string): NameAndProps[] {
|
||||
return source?.filter(o => o['#name'] === subtag);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get exactly one element
|
||||
* @param source
|
||||
* @param subtag
|
||||
* @returns
|
||||
*/
|
||||
findSubtag(source: NameAndProps[], subtag: string): NameAndProps | null {
|
||||
const r = this.findSubtagArray(source, subtag);
|
||||
if (!r || r.length === 0) {
|
||||
return null;
|
||||
} else if (r.length === 1) {
|
||||
return r[0];
|
||||
} else {
|
||||
this.callbacks.reportMessage(CommonTypesMessages.Error_TestDataUnexpectedArray({subtag}));
|
||||
return null; // ERROR
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The default test data stuffer.
|
||||
* Just gets $ (the attrs) as the body.
|
||||
* Override to use something more complex, such as including child nodes.
|
||||
*/
|
||||
static readonly defaultMapper = ((o : NameAndProps) => o?.$);
|
||||
|
||||
/**
|
||||
*
|
||||
* @param obj target object
|
||||
* @param source array of $/#name strings
|
||||
* @param subtag name to extract
|
||||
* @param mapper custom mapper function
|
||||
*/
|
||||
stuffBoxes(obj: any, source: NameAndProps[], subtag: string, asArray?: boolean, mapper?: (v: NameAndProps) => any) {
|
||||
if (!mapper) {
|
||||
mapper = LDMLKeyboardXMLSourceFileReader.defaultMapper;
|
||||
}
|
||||
if (asArray) {
|
||||
obj[subtag] = this.findSubtagArray(source, subtag)?.map(mapper); // extract contents only
|
||||
} else {
|
||||
obj[subtag] = mapper(this.findSubtag(source, subtag)); // run the mapper once
|
||||
}
|
||||
}
|
||||
|
||||
boxTestDataArrays(raw: any) : LDMLKeyboardTestDataXMLSourceFile | null {
|
||||
if (!raw) return null;
|
||||
const a : LDMLKeyboardTestDataXMLSourceFile = {
|
||||
keyboardTest: {
|
||||
conformsTo: raw?.keyboardTest?.$?.conformsTo,
|
||||
}
|
||||
};
|
||||
|
||||
const $$ = raw?.keyboardTest?.$$;
|
||||
this.stuffBoxes(a.keyboardTest, $$, 'info');
|
||||
this.stuffBoxes(a.keyboardTest, $$, 'repertoire', true);
|
||||
|
||||
// TODO-LDML: all the things
|
||||
|
||||
return a;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param file test file
|
||||
* @returns source on success, otherwise null
|
||||
*/
|
||||
public loadTestData(file: Uint8Array): LDMLKeyboardTestDataXMLSourceFile | null {
|
||||
if (!file) {
|
||||
return null;
|
||||
}
|
||||
const source = this.loadTestDataUnboxed(file);
|
||||
return this.boxTestDataArrays(source);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -38,4 +38,9 @@ export class CommonTypesMessages {
|
|||
m(this.ERROR_ImportMergeFail,
|
||||
`Problem importing ${o.path}: not sure how to handle non-array ${o.subtag}.${o.subsubtag}`);
|
||||
static ERROR_ImportMergeFail = SevError | 0x0006;
|
||||
|
||||
static Error_TestDataUnexpectedArray = (o: {subtag: string}) =>
|
||||
m(this.ERROR_TestDataUnexpectedArray,
|
||||
`Problem reading test data: expected single ${o.subtag} element, found multiple`);
|
||||
static ERROR_TestDataUnexpectedArray = SevError | 0x0007;
|
||||
};
|
||||
|
|
|
|||
22
common/web/types/test/fixtures/test-fr.xml
vendored
Normal file
22
common/web/types/test/fixtures/test-fr.xml
vendored
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE keyboardTest SYSTEM "../../../../../resources/standards-data/ldml-keyboards/techpreview/dtd/ldmlKeyboardTest.dtd">
|
||||
<keyboardTest conformsTo="techpreview">
|
||||
<info keyboard="fr-t-k0-azerty.xml" author="Team Keyboard" name="fr-test" />
|
||||
<repertoire name="simple-repertoire" chars="[a b c d e \u{22}]" type="simple" /> <!-- verify that these outputs are all available from simple keys on any layer, for all form factors -->
|
||||
<repertoire name="chars-repertoire" chars="[á é ó]" type="gesture" /> <!-- verify that these outputs are all available from simple or gesture keys on any layer, for touch -->
|
||||
<tests name="key-tests">
|
||||
<test name="key-test">
|
||||
<startContext to="abc\u0022..."/>
|
||||
<!-- tests by pressing key ids -->
|
||||
<keystroke key="s"/>
|
||||
<check result="abc\u0022...s" />
|
||||
<keystroke key="t"/>
|
||||
<check result="abc\u0022...st" />
|
||||
<keystroke key="u"/>
|
||||
<check result="abc\u0022...stu" />
|
||||
<!-- tests by specifying 'to' output char -->
|
||||
<emit to="v"/>
|
||||
<check result="abc\u0022...stuv" />
|
||||
</test>
|
||||
</tests>
|
||||
</keyboardTest>
|
||||
|
|
@ -29,3 +29,7 @@ export function loadFile(baseFilename: string, filename: string | URL): Buffer {
|
|||
export function loadLdmlKeyboardSchema(): Buffer {
|
||||
return fs.readFileSync(new URL(path.join('..', '..', 'src', 'ldml-keyboard.schema.json'), import.meta.url));
|
||||
}
|
||||
|
||||
export function loadLdmlKeyboardTestDataSchema(): Buffer {
|
||||
return fs.readFileSync(new URL(path.join('..', '..', 'src', 'ldml-keyboardtest.schema.json'), import.meta.url));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,10 @@
|
|||
import 'mocha';
|
||||
import {assert} from 'chai';
|
||||
import {loadLdmlKeyboardSchema, loadFile, makePathToFixture} from '../helpers/index.js';
|
||||
import {loadLdmlKeyboardSchema, loadFile, makePathToFixture, loadLdmlKeyboardTestDataSchema} from '../helpers/index.js';
|
||||
import LDMLKeyboardXMLSourceFileReader from '../../src/ldml-keyboard/ldml-keyboard-xml-reader.js';
|
||||
import { CompilerCallbacks, CompilerEvent } from '../../src/util/compiler-interfaces.js';
|
||||
import { LDMLKeyboardXMLSourceFile } from '../../src/ldml-keyboard/ldml-keyboard-xml.js';
|
||||
import { LDMLKeyboardTestDataXMLSourceFile } from '../ldml-keyboard/ldml-keyboard-testdata-xml.js';
|
||||
|
||||
// TODO-LDML: this is largely a port from developer/src/kmc-keyboard/test/helpers/index.ts
|
||||
|
||||
|
|
@ -66,6 +67,34 @@ export interface CompilationCase {
|
|||
throws?: RegExp;
|
||||
}
|
||||
|
||||
|
||||
export interface TestDataCase {
|
||||
/**
|
||||
* If true, loading (validation) should fail
|
||||
*/
|
||||
loadfail?: boolean;
|
||||
/**
|
||||
* path to xml, such as 'sections/layr/invalid-case.xml'
|
||||
*/
|
||||
subpath: string;
|
||||
/**
|
||||
* expected error messages. If falsy, expected to succeed. All must be present to pass.
|
||||
*/
|
||||
errors?: CompilerEvent[];
|
||||
/**
|
||||
* expected warning messages. All must be present to pass.
|
||||
*/
|
||||
warnings?: CompilerEvent[];
|
||||
/**
|
||||
* optional callback with the section
|
||||
*/
|
||||
callback?: (data: Buffer, source: LDMLKeyboardTestDataXMLSourceFile, subpath: string, callbacks: TestCompilerCallbacks ) => void;
|
||||
/**
|
||||
* if present, expect compiler to throw (use .* to match all)
|
||||
*/
|
||||
throws?: RegExp;
|
||||
}
|
||||
|
||||
/**
|
||||
* Run a bunch of cases
|
||||
* @param cases cases to run
|
||||
|
|
@ -115,3 +144,54 @@ export function testReaderCases(cases : CompilationCase[]) {
|
|||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Run a bunch of cases
|
||||
* @param cases cases to run
|
||||
* @param compiler argument to loadSectionFixture()
|
||||
* @param callbacks argument to loadSectionFixture()
|
||||
*/
|
||||
export function testTestdataReaderCases(cases : TestDataCase[]) {
|
||||
// we need our own callbacks rather than using the global so messages don't get mixed
|
||||
const callbacks = new TestCompilerCallbacks();
|
||||
const reader = new LDMLKeyboardXMLSourceFileReader(callbacks);
|
||||
for (let testcase of cases) {
|
||||
const expectFailure = testcase.throws || !!(testcase.errors); // if true, we expect this to fail
|
||||
const testHeading = expectFailure ? `should fail to load: ${testcase.subpath}`:
|
||||
`should load: ${testcase.subpath}`;
|
||||
it(testHeading, function () {
|
||||
callbacks.clear();
|
||||
|
||||
const data = loadFile(testcase.subpath, makePathToFixture(testcase.subpath));
|
||||
assert.ok(data, `reading ${testcase.subpath}`);
|
||||
const source = reader.loadTestData(data);
|
||||
if (!testcase.loadfail) {
|
||||
assert.ok(source, `loading ${testcase.subpath}`);
|
||||
} else {
|
||||
assert.notOk(source, `loading ${testcase.subpath} (expected failure)`);
|
||||
}
|
||||
// special case for an expected exception
|
||||
if (testcase.throws) {
|
||||
assert.throws(() => reader.validate(source, loadLdmlKeyboardTestDataSchema()), testcase.throws);
|
||||
} else {
|
||||
assert.doesNotThrow(() => reader.validate(source, loadLdmlKeyboardTestDataSchema()), `validating ${testcase.subpath}`);
|
||||
// if we expected errors or warnings, show them
|
||||
if (testcase.errors) {
|
||||
assert.includeDeepMembers(callbacks.messages, testcase.errors, 'expected errors to be included');
|
||||
}
|
||||
if (testcase.warnings) {
|
||||
assert.includeDeepMembers(callbacks.messages, testcase.warnings, 'expected warnings to be included');
|
||||
} else if (!expectFailure) {
|
||||
// no warnings, so expect zero messages
|
||||
assert.strictEqual(callbacks.messages.length, 0, 'expected zero messages');
|
||||
}
|
||||
|
||||
// run the user-supplied callback if any
|
||||
if (testcase.callback) {
|
||||
testcase.callback(data, source, testcase.subpath, callbacks);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,38 @@
|
|||
import { constants } from '@keymanapp/ldml-keyboard-constants';
|
||||
import { assert } from 'chai';
|
||||
import 'mocha';
|
||||
import { testTestdataReaderCases } from '../helpers/reader-callback-test.js';
|
||||
|
||||
describe('ldml keyboard xml reader tests', function () {
|
||||
this.slow(500); // 0.5 sec -- json schema validation takes a while
|
||||
|
||||
testTestdataReaderCases([
|
||||
{
|
||||
subpath: 'test-fr.xml',
|
||||
callback: (data, source) => {
|
||||
// TODO-LDML: for dev, dump it out
|
||||
console.dir({source}, {depth: Infinity});
|
||||
assert.ok(source);
|
||||
assert.ok(source.keyboardTest);
|
||||
assert.equal(source.keyboardTest.conformsTo, constants.cldr_version_latest);
|
||||
|
||||
assert.deepEqual(source.keyboardTest.info, {
|
||||
keyboard: 'fr-t-k0-azerty.xml',
|
||||
author: 'Team Keyboard',
|
||||
name: 'fr-test'
|
||||
});
|
||||
|
||||
assert.sameDeepMembers(source.keyboardTest.repertoire, [
|
||||
{
|
||||
name: 'simple-repertoire',
|
||||
chars: '[a b c d e \\u{22}]',
|
||||
type: 'simple'
|
||||
},
|
||||
{ name: 'chars-repertoire', chars: '[á é ó]', type: 'gesture' }
|
||||
]);
|
||||
|
||||
// TODO-LDML: all the things
|
||||
},
|
||||
},
|
||||
]);
|
||||
});
|
||||
Loading…
Add table
Reference in a new issue