spiegel-keyman/developer/js/tests/helpers/index.ts
2020-07-22 11:35:20 -06:00

186 lines
4.9 KiB
TypeScript

/// <reference path="../../node_modules/@keymanapp/models-types/index.d.ts" />
/**
* Helpers and utilities for the Mocha tests.
*/
import * as path from 'path';
import {assert} from 'chai';
import {LogMessage, KeymanCompilerError, redirectLogMessagesTo, resetLogMessageHandler, LogLevel} from '../../dist/errors';
export interface CompilationResult {
hasSyntaxError: boolean;
modelConstructorName: string;
error: Error;
exportedModel: object;
}
/**
* Builds a path to the fixture with the given path components.
*
* e.g., makePathToFixture('example.qaa.trivial')
* e.g., makePathToFixture('example.qaa.trivial', 'model.ts')
*
* @param components One or more path components.
*/
export function makePathToFixture(...components: string[]): string {
return path.join(__dirname, '..', 'fixtures', ...components);
}
/**
* Given source code of a model (as produced by the compiler), this evaluates
* it!
*
* That way, you can determine if the source code was syntactically-valid,
* whether it raise an exception during construction, and you can even inspect
* the resultant LexicalModel for yourself!
*
* @param code
*/
export function compileModelSourceCode(code: string) {
assert.typeOf(code, 'string');
let error: Error | null = null;
let exportedModel: object | null = null;
let hasSyntaxError = false;
let modelConstructorName: string | null = null;
let module: ModuleType | null = null;
try {
module = new Function('LMLayerWorker', 'models', 'wordBreakers', code) as ModuleType;
} catch (err) {
if (err instanceof SyntaxError) {
hasSyntaxError = true;
} else {
throw err;
}
}
// Module had some sort of syntax error:
if (module === null) {
return {
error, exportedModel, hasSyntaxError, modelConstructorName
};
}
let fakeLMLayerWorker = {
loadModel(model: object) {
exportedModel = model;
}
};
// We expect the compiled model to reach into the `models` namespace,
// and get some sort of model constructor. Problem is, we don't know
// what model constructors are defined!
// This proxy allows us to intercept
// models.*
// meaning that when the compiled code tries to do this:
//
// new models.WhateverModel('foo', 'bar')
//
// ...we can intercept the name "WhateverModel" and assign it to
// `modelConstructorName`.
let modelsNamespace = new Proxy({}, {
get(_target, property) {
if (typeof property !== 'string')
throw new Error(`Don't know how to handle non-string property: ${String(property)}`);
return class DummyModel {
constructor() {
modelConstructorName = property as string;
}
}
}
});
// We expect the compiled model to reach into the `wordBreaker` namespace,
// and maybe get a function. Problem is, we don't know
// what functions are available!
// This proxy allows us to intercept
// wordBreakers.*
// meaning that when the compiled code tries to do this:
//
// wordBreaker["someWordBreaker"]
//
// ...we can intercept the name "someWordBreaker".
let wordBreakerNamespace = new Proxy({}, {
get(_target, property) {
if (typeof property !== 'string')
throw new Error(`Don't know how to handle non-string property: ${String(property)}`);
return function dummyWordBreaker() {
}
}
});
try {
module(fakeLMLayerWorker, modelsNamespace, wordBreakerNamespace);
} catch (err) {
error = err;
}
return {
error, exportedModel, hasSyntaxError, modelConstructorName
};
}
/**
* Enables one to query log messages after they have been logged.
*/
export class LogHoarder {
private messages: LogMessage[] = [];
/**
* Get rid of all log messages seen.
*/
clear() {
this.messages = [];
}
/**
* Hoards a log message for later perusal.
*/
handleLog(log: LogMessage) {
this.messages.push(log);
}
/**
* Has an error message with this code been witnessed?
*/
hasSeenCode(code: KeymanCompilerError): boolean {
return Boolean(this.messages.find(log => log.code === code));
}
/**
* Have any warnings been logged?
*/
hasSeenWarnings(): boolean {
return this.messages
.filter(log => log.level === LogLevel.CERR_WARNING)
.length > 0;
}
/**
* Overrides the global log handler, allowing one to browse log messages
* later.
*
* Remember to uninstall the log handler afterwards!
*/
install(): this {
redirectLogMessagesTo(this.handleLog.bind(this));
return this;
}
/**
* Return the log message handler to its default.
*
* Note: You MUST uninstall the hoarder after use!
* It's recommended you put this in an afterEach() callback.
*/
uninstall() {
resetLogMessageHandler()
}
}
type ModuleType = (a: LMLayerWorker, b: ModelsNamespace, c: WordBreakersNamespace) => any;
interface LMLayerWorker {
loadModel(model: object): void;
}
type ModelsNamespace = object;
type WordBreakersNamespace = object;