bespoken / virtual-alexa

:robot: Easily test and debug Alexa skills programmatically
https://bespoken.io
Apache License 2.0
112 stars 35 forks source link

Working with typescript (using ts-jest) #46

Closed CoreyCole closed 6 years ago

CoreyCole commented 6 years ago

I'm setting up a Typescript testing pipeline using jest (ts-jest) for snapshot testing. The way ts-jest works is that it preprocesses your ts files into js for testing, i.e. in my package.json

  "jest": {
    "transform": {
      "^.+\\.tsx?$": "ts-jest"
    },
    "testMatch": [
      "**/__tests__/*.+(ts|tsx|js)"
    ],
    "moduleFileExtensions": [
      "ts",
      "tsx",
      "js",
      "jsx",
      "json",
      "node"
    ]
  }

This was working with BSTAlexa with some tweaks to how it was interpreting the intent schema, but switching to va.VirtualAlexa I'm now getting a problem where SkillInteractor.js cannot find the module src/index.js, i.e.

    Cannot find module '/my-skill-folder/src/index.js' from 'ModuleInvoker.js'

      at Resolver.resolveModule (node_modules/jest-resolve/build/index.js:169:17)
      at Function.invokeHandler (node_modules/virtual-alexa/lib/src/ModuleInvoker.js:10:31)
      at LocalSkillInteractor.invoke (node_modules/virtual-alexa/lib/src/LocalSkillInteractor.js:13:50)
      at LocalSkillInteractor.<anonymous> (node_modules/virtual-alexa/lib/src/SkillInteractor.js:77:39)

This is because index.js does not exist? Not exactly sure

CoreyCole commented 6 years ago

I was able to work around this by wrapping my handlers in a js file, i.e. in my init.js (not init.ts) file

const handler = require('./src/index').handler;
exports.handler = handler;

and in my integration test file

        alexa = new va.VirtualAlexaBuilder()
            .handler('./init.handler')
            .intentSchemaFile('./speechAssets/IntentSchema.json')
            .sampleUtterancesFile('./speechAssets/SampleUtterances.txt')
            .create();

I guess this is fine. You can close this issue if this is the behavior you want.

Also, weird little edge case I found while messing around with this. I thnk when you are getting the module in ModuleInvoked.js, the way you are getting the file name is splitting on periods (.)

    static invokeHandler(handler, event) {
        const handlerParts = handler.split(".");
        const functionName = handlerParts[handlerParts.length - 1];
        const fileName = handlerParts.slice(0, handlerParts.length - 1).join("/") + ".js";
        const fullPath = path.join(process.cwd(), fileName);
        const handlerModule = require(fullPath);
        return ModuleInvoker.invokeFunction(handlerModule[functionName], event);
    }

so I was unable to name my init file something like init.test.js as I get error:

    Cannot find module '/my-skill-folder/init/test.js' from 'ModuleInvoker.js'

      at Resolver.resolveModule (node_modules/jest-resolve/build/index.js:169:17)
      at Function.invokeHandler (node_modules/virtual-alexa/lib/src/ModuleInvoker.js:10:31)
      at LocalSkillInteractor.invoke (node_modules/virtual-alexa/lib/src/LocalSkillInteractor.js:13:50)
      at LocalSkillInteractor.<anonymous> (node_modules/virtual-alexa/lib/src/SkillInteractor.js:77:39)

not sure if you'd want to open up another issue about fixing that one as it's probably not too important

jkelvie commented 6 years ago

I don't see here how you were initially calling it that led to the issue, so I'm not sure what issue you are working around. Please provide a sample of the code that you are having trouble with when submitting issues.

And I do see lots of other code, just not the code that caused the issue (which makes it difficult to help).

CoreyCole commented 6 years ago

Sorry I was all over the place. In typescript jest test:

    beforeEach(async (done: jest.DoneCallback) => {
        // the below file paths are relative to where package.json is for some reason
        // maybe because it's where the test command is called...
        server = new bst.LambdaServer('./init', 10000, true);
        alexa = new va.VirtualAlexaBuilder()
            .handler('./init.handler')
            .interactionModelFile('./speechAssets/InteractionModel.json')
            .sampleUtterancesFile('./speechAssets/SampleUtterances.txt')
            .applicationID(constants.appId)
            .create();
        await server.start();
        done();
    });

Only works with a javascript init file. If I rename my init.js file toinit.ts and run my ts-jest suite I get the error I pasted above:

    Cannot find module '/my-skill-folder/init.js' from 'ModuleInvoker.js'

      at Resolver.resolveModule (node_modules/jest-resolve/build/index.js:169:17)
      at Function.invokeHandler (node_modules/virtual-alexa/lib/src/ModuleInvoker.js:10:31)
      at LocalSkillInteractor.invoke (node_modules/virtual-alexa/lib/src/LocalSkillInteractor.js:13:50)
      at LocalSkillInteractor.<anonymous> (node_modules/virtual-alexa/lib/src/SkillInteractor.js:77:39)

The contents of my init file (typescript or javascript) are the same in both cases

const handler = require('./src/index').handler;
exports.handler = handler;

Honestly, this is fine for me. I don't mind having the javascript file as long as the rest of my tests are in typescript. It's up to you if you'd like to keep this open.

The other issue I was talking about is naming the init file init.test.js is not currently possible, but let's not worry about that.

jkelvie commented 6 years ago

Hi @CoreyCole - thanks for this info. With regard to this code:

alexa = new va.VirtualAlexaBuilder()
            .handler('./init.handler')
            .interactionModelFile('./speechAssets/InteractionModel.json')
            .sampleUtterancesFile('./speechAssets/SampleUtterances.txt')
            .applicationID(constants.appId)
            .create();

The name of the handler should index.handler, where index is the name of the file (such as index.js) and handler is the function being called.

Now, what you may have run into - we have a bug with handling "." in the filenames. This has been fixed in the latest beta version (you can give it a try by installing virtual-alexa@beta). But if you stay with the current version, do not use a period in the name, so it would just be:

.handler("index.handler")

I hope that helps!

CoreyCole commented 6 years ago

Right, the name of my file is init.js and the above beforeEach code works. The problem is that when I change the init file name to typescript and call it init.ts is when it does not work.

Thanks for all the help by the way!

jkelvie commented 6 years ago

Okay, that does not surprise me - we don't handle typescript files at the moment - I guess we would need to auto-translate them to the js name?

Another alternative, which you may have already discovered, is that you can pass the lambda function itself to handler. Like this:

const myFunction = function(event: any, context: any) {
  context.done(null, { custom: true });
};

const virtualAlexa = VirtualAlexa.Builder()
  .handler(myFunction)
  .interactionModelFile("models/en-US.json")
  .create();

Perhaps that is a more elegant way to use it with TS.

CoreyCole commented 6 years ago

Yeah, it's definitely not a big deal. What was interesting is that the ts setup worked with BSTAlexa. The ts-jest preprocessing converts everything to js files before running, so I was assuming something weird was going on.

I had not discovered that, thanks for the help!