diff --git a/developer/src/kmc-keyboard-info/.gitignore b/developer/src/kmc-keyboard-info/.gitignore index fb2891a0a2..f118f231af 100644 --- a/developer/src/kmc-keyboard-info/.gitignore +++ b/developer/src/kmc-keyboard-info/.gitignore @@ -1 +1,2 @@ -src/imports/ \ No newline at end of file +src/imports/ +test/fixtures/khmer_angkor/build/actual.keyboard_info \ No newline at end of file diff --git a/developer/src/kmc-keyboard-info/src/keyboard-info-compiler.ts b/developer/src/kmc-keyboard-info/src/keyboard-info-compiler.ts index 9fc291bb20..d2e32d0d71 100644 --- a/developer/src/kmc-keyboard-info/src/keyboard-info-compiler.ts +++ b/developer/src/kmc-keyboard-info/src/keyboard-info-compiler.ts @@ -618,3 +618,9 @@ export class KeyboardInfoCompiler implements KeymanCompiler { } +/** + * these are exported only for unit tests, do not use + */ +export const unitTestEndpoints = { + langtagsByTag, +}; \ No newline at end of file diff --git a/developer/src/kmc-keyboard-info/test/fixtures/invalid-js-file/build/invalid_js_file.js b/developer/src/kmc-keyboard-info/test/fixtures/invalid-js-file/build/invalid_js_file.js new file mode 100644 index 0000000000..816f909dfa --- /dev/null +++ b/developer/src/kmc-keyboard-info/test/fixtures/invalid-js-file/build/invalid_js_file.js @@ -0,0 +1 @@ +// This is a file used to test the loadJsFile throws error if .js file is invalid error (see keyboard-info-compiler.ts) \ No newline at end of file diff --git a/developer/src/kmc-keyboard-info/test/fixtures/two-kmx/build/k_001___basic_input_unicodei.kmx b/developer/src/kmc-keyboard-info/test/fixtures/two-kmx/build/k_001___basic_input_unicodei.kmx new file mode 100644 index 0000000000..82f60cd001 Binary files /dev/null and b/developer/src/kmc-keyboard-info/test/fixtures/two-kmx/build/k_001___basic_input_unicodei.kmx differ diff --git a/developer/src/kmc-keyboard-info/test/fixtures/two-kmx/build/k_002___basic_input_unicode.kmx b/developer/src/kmc-keyboard-info/test/fixtures/two-kmx/build/k_002___basic_input_unicode.kmx new file mode 100644 index 0000000000..5f1497a29e Binary files /dev/null and b/developer/src/kmc-keyboard-info/test/fixtures/two-kmx/build/k_002___basic_input_unicode.kmx differ diff --git a/developer/src/kmc-keyboard-info/test/test-keyboard-info-compiler.ts b/developer/src/kmc-keyboard-info/test/test-keyboard-info-compiler.ts index ab47b39871..271e045785 100644 --- a/developer/src/kmc-keyboard-info/test/test-keyboard-info-compiler.ts +++ b/developer/src/kmc-keyboard-info/test/test-keyboard-info-compiler.ts @@ -3,7 +3,11 @@ import { assert } from 'chai'; import 'mocha'; import { TestCompilerCallbacks } from '@keymanapp/developer-test-helpers'; import { makePathToFixture } from './helpers/index.js'; -import { KeyboardInfoCompiler, KeyboardInfoCompilerResult } from '../src/keyboard-info-compiler.js'; +import { KeyboardInfoCompiler, KeyboardInfoCompilerResult, unitTestEndpoints } from '../src/keyboard-info-compiler.js'; +import langtags from "../src/imports/langtags.js"; +import { KmpCompiler, KmpCompilerOptions } from '@keymanapp/kmc-package'; +import { CompilerCallbacks, KMX, KeymanFileTypes, KeymanTargets, KmpJsonFile } from '@keymanapp/common-types'; +import { KeyboardInfoFile, KeyboardInfoFilePlatform } from './keyboard-info-file.js'; const callbacks = new TestCompilerCallbacks(); @@ -11,21 +15,58 @@ beforeEach(function() { callbacks.clear(); }); +const KHMER_ANGKOR_KPJ = makePathToFixture('khmer_angkor', 'khmer_angkor.kpj'); +const KHMER_ANGKOR_JS = makePathToFixture('khmer_angkor', 'build', 'khmer_angkor.js'); +const KHMER_ANGKOR_KPS = makePathToFixture('khmer_angkor', 'source', 'khmer_angkor.kps'); +const KHMER_ANGKOR_KMP = makePathToFixture('khmer_angkor', 'build', 'khmer_angkor.kmp'); + +const KHMER_ANGKOR_SOURCES = { + kmpFilename: KHMER_ANGKOR_KMP, + sourcePath: 'release/k/khmer_angkor', + kpsFilename: KHMER_ANGKOR_KPS, + jsFilename: KHMER_ANGKOR_JS, + forPublishing: true, +}; + +const KHMER_ANGKOR_DISPLAY_FONT = "Mondulkiri-R.ttf"; +const KHMER_ANGKOR_OSK_FONT = "khmer_busra_kbd.ttf"; +const KHMER_ANGKOR_EXAMPLES_NO_ID = { keys: "x j m E r", text: "ខ្មែរ", note: "Name of language" }; + +const KHMER_ANGKOR_KEYBOARD = { + displayFont: KHMER_ANGKOR_DISPLAY_FONT, + oskFont: KHMER_ANGKOR_OSK_FONT, + name: "Khmer Angkor", + id: "khmer_angkor", + version: "1.3", + languages: [ { name: "Central Khmer (Khmer, Cambodia)", id: "km" } ], + examples: [ { id: "km", ...KHMER_ANGKOR_EXAMPLES_NO_ID } ] +}; + +const KHMER_ANGKOR_DISPLAY_FONT_INFO = { family: "Khmer Mondulkiri", source: [ KHMER_ANGKOR_DISPLAY_FONT ] }; +const KHMER_ANGKOR_OSK_FONT_INFO = { family: "Khmer Busra Kbd", source: [ KHMER_ANGKOR_OSK_FONT ] }; + +const SECOND_DISPLAY_FONT = "second.ttf"; +const SECOND_OSK_FONT = "second_osk.ttf"; +const SECOND_EXAMPLES_NO_ID = { keys: "t w o", text: "two", note: "The number 2" }; + +const SECOND_KEYBOARD = { + displayFont: SECOND_DISPLAY_FONT, + oskFont: SECOND_OSK_FONT, + name: "Second Lang", + id: "second_lang", + version: "0.1", + languages: [ { name: "Second Language", id: "en" } ], + examples: [ { id: "en", ...SECOND_EXAMPLES_NO_ID } ] +}; + +const SECOND_DISPLAY_FONT_INFO = { family: "Second", source: [ SECOND_DISPLAY_FONT ] }; +const SECOND_OSK_FONT_INFO = { family: "Second Kbd", source: [ SECOND_OSK_FONT ] }; + describe('keyboard-info-compiler', function () { it('compile a .keyboard_info file correctly', async function() { - const kpjFilename = makePathToFixture('khmer_angkor', 'khmer_angkor.kpj'); - const jsFilename = makePathToFixture('khmer_angkor', 'build', 'khmer_angkor.js'); - const kpsFilename = makePathToFixture('khmer_angkor', 'source', 'khmer_angkor.kps'); - const kmpFilename = makePathToFixture('khmer_angkor', 'build', 'khmer_angkor.kmp'); + const kpjFilename = KHMER_ANGKOR_KPJ; const buildKeyboardInfoFilename = makePathToFixture('khmer_angkor', 'build', 'khmer_angkor.keyboard_info'); - - const sources = { - kmpFilename, - sourcePath: 'release/k/khmer_angkor', - kpsFilename, - jsFilename: jsFilename, - forPublishing: true, - }; + const sources = KHMER_ANGKOR_SOURCES; const compiler = new KeyboardInfoCompiler(); assert.isTrue(await compiler.init(callbacks, {sources})); @@ -50,4 +91,354 @@ describe('keyboard-info-compiler', function () { assert.deepEqual(actual, expected); }); + + it('check preinit creates langtagsByTag correctly', async function() { + const compiler = new KeyboardInfoCompiler(); // indirectly call preinit() + assert.isNotNull(compiler); + const en_langtag = langtags.find(({ tag }) => tag === 'en'); + assert.deepEqual((unitTestEndpoints.langtagsByTag)['en'], en_langtag); + assert.deepEqual((unitTestEndpoints.langtagsByTag)['en-Latn-US'], en_langtag); + assert.deepEqual((unitTestEndpoints.langtagsByTag)['en-Latn'], en_langtag); + assert.deepEqual((unitTestEndpoints.langtagsByTag)['en-US'], en_langtag); + }); + + it('check init initialises KeyboardInfoCompiler correctly', async function() { + const sources = KHMER_ANGKOR_SOURCES; + const compiler = new KeyboardInfoCompiler(); + assert.isTrue(await compiler.init(callbacks, {sources})); + assert.deepEqual(compiler['callbacks'], callbacks); + assert.deepEqual(compiler['options'], {sources}); + }); + + it('check run returns null if KmpCompiler.init fails', async function() { + const kpjFilename = KHMER_ANGKOR_KPJ; + const sources = KHMER_ANGKOR_SOURCES; + const compiler = new KeyboardInfoCompiler(); + assert.isTrue(await compiler.init(callbacks, {sources})); + const origKmpCompilerInit = KmpCompiler.prototype.init; + let result: KeyboardInfoCompilerResult; + try { + KmpCompiler.prototype.init = async (_callbacks: CompilerCallbacks, _options: KmpCompilerOptions): Promise => false; + result = await compiler.run(kpjFilename, null); + } catch(e) { + assert.fail(e); + } finally { + KmpCompiler.prototype.init = origKmpCompilerInit; + } + assert.isNull(result); + }); + + it('check run returns null if KmpCompiler.transformKpsToKmpObject fails', async function() { + const kpjFilename = KHMER_ANGKOR_KPJ; + const sources = KHMER_ANGKOR_SOURCES; + const compiler = new KeyboardInfoCompiler(); + assert.isTrue(await compiler.init(callbacks, {sources})); + const origKmpCompilerTransformKpsToKmpObject = KmpCompiler.prototype.transformKpsToKmpObject; + let result: KeyboardInfoCompilerResult; + try { + KmpCompiler.prototype.transformKpsToKmpObject = (_kpsFilename: string): KmpJsonFile.KmpJsonFile => null; + result = await compiler.run(kpjFilename, null); + } catch(e) { + assert.fail(e); + } finally { + KmpCompiler.prototype.transformKpsToKmpObject = origKmpCompilerTransformKpsToKmpObject; + } + assert.isNull(result); + }); + + it('check run returns null if loadJsFile fails', async function() { + const kpjFilename = KHMER_ANGKOR_KPJ; + const sources = KHMER_ANGKOR_SOURCES + const compiler = new KeyboardInfoCompiler(); + assert.isTrue(await compiler.init(callbacks, {sources})); + compiler['loadJsFile'] = (_filename: string): string => null; + const result = await compiler.run(kpjFilename, null); + assert.isNull(result); + }); + + it('check run returns null if license is not MIT', async function() { + const kpjFilename = KHMER_ANGKOR_KPJ; + const sources = KHMER_ANGKOR_SOURCES; + const compiler = new KeyboardInfoCompiler(); + assert.isTrue(await compiler.init(callbacks, {sources})); + compiler['isLicenseMIT'] = (_filename: string): boolean => false; + const result = await compiler.run(kpjFilename, null); + assert.isNull(result); + }); + + it('check run returns null if fillLanguages fails', async function() { + const kpjFilename = KHMER_ANGKOR_KPJ; + const sources = KHMER_ANGKOR_SOURCES; + const compiler = new KeyboardInfoCompiler(); + assert.isTrue(await compiler.init(callbacks, {sources})); + compiler['fillLanguages'] = async (_kpsFilename: string, _keyboard_info: KeyboardInfoFile, _kmpJsonData: KmpJsonFile.KmpJsonFile): Promise => false; + const result = await compiler.run(kpjFilename, null); + assert.isNull(result); + }); + + it('should write artifacts to disk', async function() { + const kpjFilename = KHMER_ANGKOR_KPJ; + const actualFilename = makePathToFixture('khmer_angkor', 'build', 'actual.keyboard_info'); + const expectedFilename = makePathToFixture('khmer_angkor', 'build', 'khmer_angkor.keyboard_info'); + const sources = KHMER_ANGKOR_SOURCES; + + const compiler = new KeyboardInfoCompiler(); + assert.isTrue(await compiler.init(callbacks, {sources})); + const result = await compiler.run(kpjFilename, null); + assert.isNotNull(result); + + if(fs.existsSync(actualFilename)) { + fs.rmSync(actualFilename); + } + + result.artifacts.keyboard_info.filename = actualFilename; + assert.isTrue(await compiler.write(result.artifacts)); + assert(fs.existsSync(actualFilename)) + + const actual = JSON.parse(fs.readFileSync(actualFilename, 'utf-8')); + const expected = JSON.parse(fs.readFileSync(expectedFilename, 'utf-8')); + + // `lastModifiedDate` is dependent on time of run (not worth mocking) + delete actual['lastModifiedDate']; + delete expected['lastModifiedDate']; + + assert.deepEqual(actual, expected); + + if(fs.existsSync(actualFilename)) { // tidy up + fs.rmSync(actualFilename); + } + }); + + it('check mapKeymanTargetToPlatform returns correct platforms', async function() { + const compiler = new KeyboardInfoCompiler(); + const map: {[index in KeymanTargets.KeymanTarget]: KeyboardInfoFilePlatform[]} = { + any: [], + androidphone: ['android'], + androidtablet: ['android'], + desktop: [], + ipad: ['ios'], + iphone: ['ios'], + linux: ['linux'], + macosx: ['macos'], + mobile: [], + tablet: [], + web: ['desktopWeb'], + windows: ['windows'] + } + for (const [target, platform] of Object.entries(map)) { + assert.deepEqual(compiler['mapKeymanTargetToPlatform'](target), platform); + } + }); + + it('check kmxFileVersionToString returns correct strings', async function() { + const compiler = new KeyboardInfoCompiler(); + const convs = [ + {num: 0x0000, str: '0.0'}, + {num: 0x0001, str: '0.1'}, + {num: 0x0100, str: '1.0'}, + {num: 0x0101, str: '1.1'}, + {num: 0x0A0A, str: '10.10'}, + ]; + convs.forEach((conv) => { + assert.equal(compiler['kmxFileVersionToString'](conv.num), conv.str); + }); + }); + + it('check loadKmxFiles returns empty array if .kmx file is missing from .kmp', async function() { + const kpsFilename = KHMER_ANGKOR_KPS; + const compiler = new KeyboardInfoCompiler(); + const kmpCompiler = new KmpCompiler(); + assert.isTrue(await kmpCompiler.init(callbacks, {})); + const kmpJsonData = kmpCompiler.transformKpsToKmpObject(kpsFilename); + assert.isNotNull(kmpJsonData); + // remove .kmx file + kmpJsonData.files = kmpJsonData.files.filter(file => !KeymanFileTypes.filenameIs(file.name, KeymanFileTypes.Binary.Keyboard)); + const kmxFiles: { + filename: string, + data: KMX.KEYBOARD + }[] = compiler['loadKmxFiles'](kpsFilename, kmpJsonData); + assert.deepEqual(kmxFiles, []); + }); + + it('check loadKmxFiles throws error if .kmx file is missing from disk', async function() { + const kpsFilename = KHMER_ANGKOR_KPS; + const compiler = new KeyboardInfoCompiler(); + const kmpCompiler = new KmpCompiler(); + assert.isTrue(await kmpCompiler.init(callbacks, {})); + const kmpJsonData = kmpCompiler.transformKpsToKmpObject(kpsFilename); + assert.isNotNull(kmpJsonData); + // rename .kmx file in files list so it cannot be loaded from disk + const kmpIndex = kmpJsonData.files.findIndex(file => KeymanFileTypes.filenameIs(file.name, KeymanFileTypes.Binary.Keyboard)); + kmpJsonData.files[kmpIndex].name = '../build/throw_error.kmx'; + assert.throws(() => compiler['loadKmxFiles'](kpsFilename, kmpJsonData)); + }); + + it('check loadKmxFiles can handle two .kmx files', async function() { + const jsFilename = makePathToFixture('two-kmx', 'build', 'two_kmx.js'); + const kpsFilename = makePathToFixture('two-kmx', 'source', 'two_kmx.kps'); + const kmpFilename = makePathToFixture('two-kmx', 'build', 'two_kmx.kmp'); + + const sources = { + kmpFilename, + sourcePath: 'release/k/two-kmx', + kpsFilename, + jsFilename: jsFilename, + forPublishing: true, + }; + + const kmx_filename_001 = 'k_001___basic_input_unicodei.kmx'; + const kmx_filename_002 = 'k_002___basic_input_unicode.kmx'; + + const compiler = new KeyboardInfoCompiler(); + assert.isTrue(await compiler.init(callbacks, {sources})); + const kmpJsonData: KmpJsonFile.KmpJsonFile = { + system: { fileVersion: '', keymanDeveloperVersion: '' }, + options: null, + files: [ + { name: '../build/' + kmx_filename_001, description: 'Keyboard 001' }, + { name: '../build/' + kmx_filename_002, description: 'Keyboard 002' }, + ] + }; + const kmxFiles: { + filename: string, + data: KMX.KEYBOARD + }[] = compiler['loadKmxFiles'](kpsFilename, kmpJsonData); + assert.equal(kmxFiles.length, 2); + assert.deepEqual(kmxFiles[0].filename, kmx_filename_001); + assert.deepEqual(kmxFiles[1].filename, kmx_filename_002); + assert.isNotNull(kmxFiles[0].data); + assert.isNotNull(kmxFiles[1].data); + }); + + it('check loadJsFile throws error if .js file is invalid', async function() { + const jsFilename = makePathToFixture('invalid-js-file', 'build', 'invalid_js_file.js'); + const kpsFilename = makePathToFixture('invalid-js-file', 'source', 'invalid_js_file.kps'); + const kmpFilename = makePathToFixture('invalid-js-file', 'build', 'invalid_js_file.kmp'); + + const sources = { + kmpFilename, + sourcePath: 'release/k/invalid-js-file', + kpsFilename, + jsFilename: jsFilename, + forPublishing: true, + }; + + const compiler = new KeyboardInfoCompiler(); + assert.isTrue(await compiler.init(callbacks, {sources})); + const origTextDecoderDecode = TextDecoder.prototype.decode; + TextDecoder.prototype.decode = () => { throw new TypeError(); } + assert.throws(() => compiler['loadJsFile'](jsFilename)); + TextDecoder.prototype.decode = origTextDecoderDecode; + }); + + it('check fillLanguages constructs keyboard_info.languages correctly', async function() { + const kmpJsonData: KmpJsonFile.KmpJsonFile = { + system: { fileVersion: '', keymanDeveloperVersion: '' }, + options: null, + keyboards: [KHMER_ANGKOR_KEYBOARD], + }; + + const sources = KHMER_ANGKOR_SOURCES; + const compiler = new KeyboardInfoCompiler(); + assert.isTrue(await compiler.init(callbacks, {sources})); + compiler['fontSourceToKeyboardInfoFont'] = async (_kpsFilename: string, _kmpJsonData: KmpJsonFile.KmpJsonFile, _source: string[]) => { + if (_source[0] == KHMER_ANGKOR_DISPLAY_FONT) { + return KHMER_ANGKOR_DISPLAY_FONT_INFO; + } else { // osk font + return KHMER_ANGKOR_OSK_FONT_INFO; + } + } + const keyboard_info: KeyboardInfoFile = {}; + const result = await compiler['fillLanguages'](KHMER_ANGKOR_KPS, keyboard_info, kmpJsonData); + assert.isTrue(result); + assert.deepEqual(keyboard_info.languages, {km: { + examples: [ KHMER_ANGKOR_EXAMPLES_NO_ID ], + font: KHMER_ANGKOR_DISPLAY_FONT_INFO, + oskFont: KHMER_ANGKOR_OSK_FONT_INFO, + languageName: "Khmer", + regionName: undefined, + scriptName: undefined, + displayName: "Khmer", + }}); + }); + + it('check fillLanguages can handle two keyboards correctly', async function() { + const kmpJsonData: KmpJsonFile.KmpJsonFile = { + system: { fileVersion: '', keymanDeveloperVersion: '' }, + options: null, + keyboards: [KHMER_ANGKOR_KEYBOARD, SECOND_KEYBOARD], + }; + + const sources = KHMER_ANGKOR_SOURCES; + const compiler = new KeyboardInfoCompiler(); + assert.isTrue(await compiler.init(callbacks, {sources})); + let callCount = 0; + compiler['fontSourceToKeyboardInfoFont'] = async (_kpsFilename: string, _kmpJsonData: KmpJsonFile.KmpJsonFile, _source: string[]) => { + callCount++; + const info = [KHMER_ANGKOR_DISPLAY_FONT_INFO, KHMER_ANGKOR_OSK_FONT_INFO, SECOND_DISPLAY_FONT_INFO, SECOND_OSK_FONT_INFO]; + return info[callCount-1]; + }; + const keyboard_info: KeyboardInfoFile = {}; + const result = await compiler['fillLanguages'](KHMER_ANGKOR_KPS, keyboard_info, kmpJsonData); + assert.isTrue(result); + assert.deepEqual(keyboard_info.languages, {km: { + examples: [ KHMER_ANGKOR_EXAMPLES_NO_ID ], + font: KHMER_ANGKOR_DISPLAY_FONT_INFO, + oskFont: KHMER_ANGKOR_OSK_FONT_INFO, + languageName: "Khmer", + regionName: undefined, + scriptName: undefined, + displayName: "Khmer", + }, en: { + examples: [ SECOND_EXAMPLES_NO_ID ], + font: SECOND_DISPLAY_FONT_INFO, + oskFont: SECOND_OSK_FONT_INFO, + languageName: "English", + regionName: undefined, + scriptName: undefined, + displayName: "English", + }}); + }); + + it('check fillLanguages returns false if fontSourceToKeyboardInfoFont fails for display font', async function() { + const kmpJsonData: KmpJsonFile.KmpJsonFile = { + system: { fileVersion: '', keymanDeveloperVersion: '' }, + options: null, + keyboards: [KHMER_ANGKOR_KEYBOARD], + }; + + const sources = KHMER_ANGKOR_SOURCES; + const compiler = new KeyboardInfoCompiler(); + assert.isTrue(await compiler.init(callbacks, {sources})); + compiler['fontSourceToKeyboardInfoFont'] = async (_kpsFilename: string, _kmpJsonData: KmpJsonFile.KmpJsonFile, _source: string[]) => { + if (_source[0] == KHMER_ANGKOR_DISPLAY_FONT) { + return null; + } else { // osk font + return KHMER_ANGKOR_OSK_FONT_INFO; + } + } + const result = await compiler['fillLanguages'](KHMER_ANGKOR_KPS, {}, kmpJsonData); + assert.isFalse(result); + }); + + it('check fillLanguages returns false if fontSourceToKeyboardInfoFont fails for osk font', async function() { + const kmpJsonData: KmpJsonFile.KmpJsonFile = { + system: { fileVersion: '', keymanDeveloperVersion: '' }, + options: null, + keyboards: [KHMER_ANGKOR_KEYBOARD], + }; + + const sources = KHMER_ANGKOR_SOURCES; + const compiler = new KeyboardInfoCompiler(); + assert.isTrue(await compiler.init(callbacks, {sources})); + compiler['fontSourceToKeyboardInfoFont'] = async (_kpsFilename: string, _kmpJsonData: KmpJsonFile.KmpJsonFile, _source: string[]) => { + if (_source[0] == KHMER_ANGKOR_DISPLAY_FONT) { + return KHMER_ANGKOR_DISPLAY_FONT_INFO; + } else { // osk font + return null; + } + } + const result = await compiler['fillLanguages'](KHMER_ANGKOR_KPS, {}, kmpJsonData); + assert.isFalse(result); + }); });