Skip to content
Permalink
Browse files

adds specs, refactors code

  • Loading branch information...
JanTrotnow committed Apr 17, 2018
1 parent f0e69f0 commit 793c126335273dbbe523547d471b6f482d955d42

Some generated files are not rendered by default. Learn more.

Oops, something went wrong.
@@ -66,9 +66,9 @@
"request": "^2.83.0",
"request-promise": "^4.2.2",
"tslint": "^5.8.0",
"typescript": "^2.7.2",
"tslint-config-airbnb": "^5.8.0",
"tslint-config-prettier": "^1.10.0"
"tslint-config-prettier": "^1.10.0",
"typescript": "^2.8.1"
},
"bin": {
"assistant": "./bin/assistant"
@@ -52,11 +52,7 @@ describe("TranslateHelper", function() {
// Regularly, "5" should have a probabilty of 1/10
// In a bug case, the prob is much higher (around 1024/1033 ~ 99%)
const testSize = 100;
const probabilitySet: string[] = await Promise.all(
[...Array(testSize)].map(async (x): Promise<string> => {
return await this.translateHelper.t("templateProbs");
})
);
const probabilitySet = await Promise.all([...Array(testSize)].map(async (x): Promise<string> => this.translateHelper.t("templateProbs")));
const occuredAmount = probabilitySet.filter(x => x.charAt(0) === "5").length / testSize;

expect(occuredAmount).toBeCloseTo(0.15, 0.15); // Expected amount is 10%, let's check for 0%-30%.
@@ -70,7 +66,6 @@ describe("TranslateHelper", function() {
} catch (e) {
expect(e.message).toContain("I18n key lookup could not be resolved");
}
//expect(async () => await this.translateHelper.t(".notExisting")).toThrow();
});
});

@@ -231,7 +226,7 @@ describe("TranslateHelper", function() {
});
});
});

describe("missingInterpolationHandler", function() {
beforeEach(function(this: CurrentThisContext) {
@injectable()
@@ -260,5 +255,10 @@ describe("TranslateHelper", function() {
const translation = await this.translateHelper.t("templateSyntaxSmall");
expect(translation).toContain("test");
});

it("does not call missingInterpolationExtensions if all interpolations are present", async function(this: CurrentThisContext){
await this.translateHelper.t("mySpecificKeys.keyOne");
expect(this.missingInterpolationExtension.execute).not.toHaveBeenCalled();
});
});
});
@@ -1,8 +1,19 @@
import { SimpleVoiceResponse } from "../../../../src/components/unifier/responses/simple-voice-response";
import { createRequestScope } from "../../../support/util/setup";
import { ResponseHandler } from "../../../support/mocks/unifier/handler";
import { Logger, SpecSetup, OptionalHandlerFeatures } from "../../../../src/assistant-source";
import { Container } from "inversify-components";

interface CurrentThisContext {
handler: ResponseHandler & OptionalHandlerFeatures.Reprompt;
simpleVoiceResponse: SimpleVoiceResponse;
logger: Logger;
specHelper: SpecSetup;
container: Container;
}

describe("VoiceResponse", function() {
beforeEach(function() {
beforeEach(function(this: CurrentThisContext) {
createRequestScope(this.specHelper);
this.handler = this.container.inversifyInstance.get("core:unifier:current-response-handler");
this.logger = this.container.inversifyInstance.get("core:root:current-logger");
@@ -12,54 +23,57 @@ describe("VoiceResponse", function() {
});

describe("endSessionWith", function() {
beforeEach(function() {
beforeEach(function(this: CurrentThisContext) {
this.simpleVoiceResponse.endSessionWith("test string");
});

it("sets text", function() {
it("sets text", function(this: CurrentThisContext) {
expect(this.handler.voiceMessage).toEqual("test string");
});

it("ends session", function() {
it("ends session", function(this: CurrentThisContext) {
expect(this.handler.endSession).toBeTruthy();
});

it("sends response", function() {
it("sends response", function(this: CurrentThisContext) {
expect(this.handler.sendResponse).toHaveBeenCalled();
});
});

describe("prompt", function() {
it("sets text", function() {
it("sets text", function(this: CurrentThisContext) {
this.simpleVoiceResponse.prompt("test string");
expect(this.handler.voiceMessage).toEqual("test string");
});

it("does not end session", function() {
it("does not end session", function(this: CurrentThisContext) {
this.simpleVoiceResponse.prompt("test string");
expect(this.handler.endSession).toBeFalsy();
});

it("sends response", function() {
it("sends response", function(this: CurrentThisContext) {
this.simpleVoiceResponse.prompt("test string");
expect(this.handler.sendResponse).toHaveBeenCalled();
});

describe("with reprompts given", function() {
describe("with a handler not supporting reprompts", function() {
it("throws exception", function() {
expect(() => {
this.simpleVoiceResponse.prompt("test string", "my reprompt", "my reprompt 2");
}).toThrow();
it("throws exception", async function(this: CurrentThisContext) {
try {
await this.simpleVoiceResponse.prompt("test string", "my reprompt", "my reprompt 2");
fail();
} catch (e) {
expect(e.message).toContain("The currently used platform does not support reprompting.");
}
});
});

describe("with a handler supporting reprompts", function() {
beforeEach(function() {
beforeEach(function(this: CurrentThisContext) {
Object.assign(this.handler, { reprompts: null });
});

it("sets reprompts", function() {
it("sets reprompts", function(this: CurrentThisContext) {
this.simpleVoiceResponse.prompt("test string", "my reprompt", "my reprompt 2");
expect(this.handler.reprompts).toEqual(["my reprompt", "my reprompt 2"]);
});
@@ -26,7 +26,7 @@ export class MainState extends BaseState implements State.Required {
this.responseFactory = responseFactory;
}

async unhandledGenericIntent(...args: any[]) {
public async unhandledGenericIntent(...args: any[]) {
this.spyIfExistent("unhandled", ...args);
}

@@ -13,7 +13,7 @@ export class SecondState implements State.Required {
this.spy = spy;
}

async unhandledGenericIntent(...args: any[]) {
public async unhandledGenericIntent(...args: any[]) {
this.spyIfExistent("unhandled", ...args);
}

@@ -20,7 +20,7 @@ export class UnhandledErrorState implements State.Required {
this.responseFactory = responseFactory;
}

async unhandledGenericIntent(...args: any[]) {
public async unhandledGenericIntent(...args: any[]) {
this.spyIfExistent("unhandled", ...args);
throw new Error("Error");
}
@@ -68,7 +68,9 @@ export function cli(argv, resolvedIndex) {
];

// Merge root files into array
copyInstructions = copyInstructions.concat(['.gitignore', 'index.ts', 'tsconfig.json', 'tslint.json', 'README.md', 'package.json'].map(rootFile => [rootFile, rootFile]));
copyInstructions = copyInstructions.concat(
[".gitignore", "index.ts", "tsconfig.json", "tslint.json", "README.md", "package.json"].map(rootFile => [rootFile, rootFile])
);

// Copy templates!
copyInstructions.forEach(copyInstruction => {
@@ -42,51 +42,47 @@ export const descriptor: ComponentDescriptor<Configuration.Defaults> = {
})
.inSingletonScope();

bindService
.bindGlobalService<InterpolationResolver>("interpolation-resolver")
.to(InterpolationResolverImpl)
.inSingletonScope();
bindService.bindGlobalService<InterpolationResolver>("interpolation-resolver").to(InterpolationResolverImpl);


bindService.bindGlobalService<i18next.I18n>("instance").toDynamicValue(context => {
return context.container.get<I18nextWrapper>("core:i18n:wrapper").instance;
});
},
request: (bindService, lookupService) => {
bindService.bindGlobalService<TranslateHelper>("current-translate-helper").to(TranslateHelperImpl);

bindService
bindService.bindGlobalService<i18next.I18n>("instance").toDynamicValue(context => {
return context.container.get<I18nextWrapper>("core:i18n:wrapper").instance;
});
},
request: (bindService, lookupService) => {
bindService.bindGlobalService<TranslateHelper>("current-translate-helper").to(TranslateHelperImpl);

bindService
.bindGlobalService<I18nContext>("current-context")
.to(I18nContext)
.inSingletonScope();

// Hook into beforeIntent and save current state and current intent into I18nContext (see above)
// Since I18nContext is a singleton in request scope, it will be the same context instance for this request.
bindService.bindExtension<Hooks.Hook>(lookupService.lookup("core:state-machine").getInterface("beforeIntent")).toDynamicValue(context => {
return (mode, state, stateName, intent) => {
const currentI18nContext = context.container.get<I18nContext>("core:i18n:current-context");
currentI18nContext.intent = intent;
currentI18nContext.state = stateName.charAt(0).toLowerCase() + stateName.slice(1);
return { success: true, result: currentI18nContext };
};
});

// Registers a spec helper function which returns all possible values instead of a sample one
bindService.bindGlobalService<TranslateValuesFor>("current-translate-values-for").toDynamicValue(context => {
return async (key: string, options = {}): Promise<string[]> => {
const translations: string[] = (context.container.get<I18nextWrapper>("core:i18n:spec-wrapper").instance.t(key, options)).split(arraySplitter);
const translateHelper = context.container.get<TranslateHelper>("core:i18n:current-translate-helper");
// Hook into beforeIntent and save current state and current intent into I18nContext (see above)
// Since I18nContext is a singleton in request scope, it will be the same context instance for this request.
bindService.bindExtension<Hooks.Hook>(lookupService.lookup("core:state-machine").getInterface("beforeIntent")).toDynamicValue(context => {
return (mode, state, stateName, intent) => {
const currentI18nContext = context.container.get<I18nContext>("core:i18n:current-context");
currentI18nContext.intent = intent;
currentI18nContext.state = stateName.charAt(0).toLowerCase() + stateName.slice(1);
return { success: true, result: currentI18nContext };
};
});

return Promise.all(
translations.map(async translation => {
return (translation = await context.container
.get<InterpolationResolver>("core:i18n:interpolation-resolver")
.resolveMissingInterpolations(translation, translateHelper));
})
);
};
});
},
// Registers a spec helper function which returns all possible values instead of a sample one
bindService.bindGlobalService<TranslateValuesFor>("current-translate-values-for").toDynamicValue(context => {
return async (key: string, options = {}): Promise<string[]> => {
const translations: string[] = context.container
.get<I18nextWrapper>("core:i18n:spec-wrapper")
.instance.t(key, options)
.split(arraySplitter);
const translateHelper = context.container.get<TranslateHelper>("core:i18n:current-translate-helper");

return Promise.all(
translations.map(async translation =>
context.container.get<InterpolationResolver>("core:i18n:interpolation-resolver").resolveMissingInterpolations(translation, translateHelper)
)
);
};
});
},
};
},
};
@@ -28,11 +28,12 @@ export class InterpolationResolver implements InterpolationResolverInterface {
const interpolation = translatedValue.split("*~~")[1].split("~~*")[0];
let interpolationValue: string | undefined;

for (let missingInterpolationExtension of this.missingInterpolationExtensions) {
interpolationValue = await missingInterpolationExtension.execute(interpolation, translateHelper);

if (typeof interpolationValue !== "undefined") {
translatedValue = translatedValue.replace("*~~" + interpolation + "~~*", interpolationValue);
const missingInterpolationExtensionsPromises = this.missingInterpolationExtensions.map((missingInterpolationExtension) => missingInterpolationExtension.execute(interpolation, translateHelper));
const interpolationValues = await Promise.all(missingInterpolationExtensionsPromises);

for (let value of interpolationValues) {
if (typeof value !== "undefined") {
translatedValue = translatedValue.replace("*~~" + interpolation + "~~*", value);
break;
}
}
@@ -70,7 +70,7 @@ export abstract class BaseState implements State.Required, Voiceable, TranslateH
}

/** Prompts with current unhandled message */
async unhandledGenericIntent(machine: Transitionable, originalIntentMethod: string, ...args: any[]): Promise<any> {
public async unhandledGenericIntent(machine: Transitionable, originalIntentMethod: string, ...args: any[]): Promise<any> {
this.responseFactory.createVoiceResponse().prompt(await this.translateHelper.t());
}

@@ -84,7 +84,8 @@ export abstract class BaseState implements State.Required, Voiceable, TranslateH
* First try is `currentState.currentIntent.platform.device`.
* @param locals If given: variables to use in response
*/
t(locals?: {[name: string]: string | number | object}): Promise<string>;
// tslint:disable-next-line:function-name
public async t(locals?: {[name: string]: string | number | object}): Promise<string>;

/**
* Translates the given key using your json translations.
@@ -93,26 +94,28 @@ export abstract class BaseState implements State.Required, Voiceable, TranslateH
* If you pass an absolute key (without "." at beginning), this method will look at given absolute key.
* @param locals Variables to use in reponse
*/
t(key?: string, locals?: {[name: string]: string | number | object}): Promise<string>;
// tslint:disable-next-line:function-name
public async t(key?: string, locals?: {[name: string]: string | number | object}): Promise<string>;

async t(...args: any[]) {
return await (this.translateHelper as any).t(...args);
// tslint:disable-next-line:function-name
public async t(...args: any[]) {
return (this.translateHelper as any).t(...args);
}

/**
* Sends voice message but does not end session, so the user is able to respond
* @param {string} text Text to say to user
* @param {string[]} [reprompts] If the user does not answer in a given time, these reprompt messages will be used.
*/
prompt(text: string | Promise<string>, ...reprompts: Array<string | Promise<string>>): void | Promise<void> {
public async prompt(text: string | Promise<string>, ...reprompts: Array<string | Promise<string>>): Promise<void> {
return this.responseFactory.createVoiceResponse().prompt(text, ...reprompts);
}

/**
* Sends voice message and ends session
* @param {string} text Text to say to user
*/
public endSessionWith(text: string) {
public async endSessionWith(text: string): Promise<void> {
return this.responseFactory.createVoiceResponse().endSessionWith(text);
}

@@ -48,14 +48,14 @@ export interface Voiceable {
* Sends voice message and ends session
* @param {string} text Text to say to user
*/
endSessionWith(text: string): void | Promise<void>;
endSessionWith(text: string | Promise<string>): Promise<void> | void;

/**
* Sends voice message but does not end session, so the user is able to respond
* @param {string} text Text to say to user
* @param {string[]} [reprompts] If the user does not answer in a given time, these reprompt messages will be used.
*/
prompt(text: string | Promise<string>, ...reprompts: Array<string | Promise<string>>): void | Promise<void>;
prompt(text: string | Promise<string>, ...reprompts: Array<string | Promise<string>>): Promise<void> | void;
}

// Currently, we are not allowed to use camelCase here! So try to just use a single word!
Oops, something went wrong.

0 comments on commit 793c126

Please sign in to comment.
You can’t perform that action at this time.