Skip to content

Commit

Permalink
add AudioPlayer tests (fixes #3)
Browse files Browse the repository at this point in the history
  • Loading branch information
hoegertn committed Aug 31, 2017
1 parent 24a3cce commit 0bd5e9c
Show file tree
Hide file tree
Showing 4 changed files with 265 additions and 13 deletions.
40 changes: 29 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,13 @@ For some simple examples, see the 'examples' directory.

## Test Framework Documentation

### alexaTest.initialize(index, appId, userId)
### alexaTest.initialize(index, appId, userId, deviceId)
Initializes the test framework. Must be called before generating requests or running any tests.
* `index`: The object containing your skill's 'handler' method. Must define a method called `handler(event, context, callback)`, which runs the skill.
* The test framework passes 'true' as a fourth parameter to the handler. Obviously this should be used sparingly, if at all.
* `appId`: The Skill's app ID. Looks like "amzn1.ask.skill.00000000-0000-0000-0000-000000000000".
* `userId`: The Amazon User ID to test with. Looks like "amzn1.ask.account.LONG_STRING".
* `deviceId`: Optional The Amazon Device ID to test with. Looks like "amzn1.ask.device.LONG_STRING"

### alexaTest.initializeI18N(resources)
Initializes i18n. You only need this if you use i18n in your skill, and you want to use i18n to fetch result strings to test against. You must have installed the optional dependencies `i18n` and `i18next-sprintf-postprocessor`.
Expand Down Expand Up @@ -66,20 +67,27 @@ Returns a [SessionEndedRequest][sessionendedrequest docs]. The request can be pa
* `reason`: The reason. See the [SessionEndedRequest][sessionendedrequest docs] documentation.
* `locale`: Optionally, an override locale for the request.

### alexaTest.addEntityResolutionToRequest(request, slotName, slotType, value, id)
### addAudioPlayerContextToRequest(request, [token], [offset], [activity])
Adds an AudioPlayer context to the given request. Returns the given request to allow call chaining.
* `request`: The intent request to modify.
* `token` An opaque token that represents the audio stream described by this AudioPlayer object. You provide this token when sending the Play directive.
* `offset` Identifies a track’s offset in milliseconds at the time the request was sent. This is 0 if the track is at the beginning.
* `activity` Indicates the last known state of audio playback.

### alexaTest.addEntityResolutionToRequest(request, slotName, slotType, value, [id])
Adds an entity resolution to the given request. Returns the given request to allow call chaining.
* `request` The intent request to modify.
* `slotName` The name of the slot to add the resolution to. If the slot does not exist it is added.
* `slotType` The type of the slot.
* `value` The value of the slot.
* `id` Optionally, the id of the resolved entity.
* `request`: The intent request to modify.
* `slotName`: The name of the slot to add the resolution to. If the slot does not exist it is added.
* `slotType`: The type of the slot.
* `value`: The value of the slot.
* `id`: Optionally, the id of the resolved entity.

### alexaTest.addEntityResolutionNoMatchToRequest(request, slotName, slotType, value)
Adds an entity resolution with code ER_SUCCESS_NO_MATCH to the given request. Returns the given request to allow call chaining.
* `request` The intent request to modify.
* `slotName` The name of the slot to add the resolution to. If the slot does not exist it is added.
* `slotType` The type of the slot.
* `value` The value of the slot.
* `request`: The intent request to modify.
* `slotName`: The name of the slot to add the resolution to. If the slot does not exist it is added.
* `slotType`: The type of the slot.
* `value`: The value of the slot.

### alexaTest.test(sequence, [description])
Tests the skill with a sequence of requests and expected responses. This method should be called from inside a Mocha `describe` block.
Expand All @@ -102,8 +110,18 @@ Tests the skill with a sequence of requests and expected responses. This method
* `hasCardContent`: Optional String. Tests that the card sent by the response has the title specified.
* `withStoredAttributes`: Optional Object. The attributes to initialize the handler with. Used with DynamoDB mock
* `storesAttributes`: Optional Object. Tests that the given attributes were stored in the DynamoDB.
* `playsStream`: Optional Object. Tests that the AudioPlayer is used to play a stream.
* `stopsStream`: Optional Boolean. Tests that the AudioPlayer is stopped.
* `clearsQueue`: Optional String. Tests that the AudioPlayer clears the queue with the given clear behavior.
* `description`: An optional description for the mocha test

The `playsStream` Object has the following properties:
* `behavior`: String. The expected playBehavior of the AudioPlayer.
* `url`: String. The expected URL of the stream.
* `token`: Optional String. The expected token for the stream.
* `previousToken`: Optional String. The expected previousToken for the stream.
* `offset`: Optional Integer. The expected offset of the stream.

### alexaTest.t(arguments)
Forwards the request to `alexaTest.i18n.t` and returns the result. You must have called `alexaTest.initializeI18N` previously.

Expand Down
81 changes: 81 additions & 0 deletions examples/skill-sample-nodejs-audioplayer/audioplayer-tests.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
// include the testing framework
//const alexaTest = require('alexa-skill-test-framework');
const alexaTest = require('../../index');

// initialize the testing framework
alexaTest.initialize(
require('./audioplayer.js'),
"amzn1.ask.skill.00000000-0000-0000-0000-000000000000",
"amzn1.ask.account.VOID");

describe("Audio Player Skill", () => {
'use strict';

describe("LaunchRequest", () => {
alexaTest.test([
{
request: alexaTest.getLaunchRequest(),
says: "Hello World!", repromptsNothing: true, shouldEndSession: true
}
]);
});

describe("PlayStreamIntent", () => {
alexaTest.test([
{
request: alexaTest.getIntentRequest("PlayStreamIntent"),
playsStream: {
behavior: 'REPLACE_ALL',
url: 'https://superAudio.stream',
token: 'superToken'
}
}
]);
});

describe("ClearQueueIntent", () => {
alexaTest.test([
{
request: alexaTest.getIntentRequest("ClearQueueIntent"),
clearsQueue: 'CLEAR_ALL'
}
]);
});

describe("AMAZON.ResumeIntent", () => {
alexaTest.test([
{
request: alexaTest.getIntentRequest("AMAZON.ResumeIntent"),
playsStream: {
behavior: 'REPLACE_ALL',
url: 'https://superAudio.stream',
token: 'superToken',
offset: 0
}
}
]);
});

describe("AMAZON.ResumeIntent at position", () => {
alexaTest.test([
{
request: alexaTest.addAudioPlayerContextToRequest(alexaTest.getIntentRequest("AMAZON.ResumeIntent"), 'superToken', 123),
playsStream: {
behavior: 'REPLACE_ALL',
url: 'https://superAudio.stream',
token: 'superToken',
offset: 123
}
}
]);
});

describe("AMAZON.PauseIntent", () => {
alexaTest.test([
{
request: alexaTest.getIntentRequest("AMAZON.PauseIntent"),
stopsStream: true
}
]);
});
});
47 changes: 47 additions & 0 deletions examples/skill-sample-nodejs-audioplayer/audioplayer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
const Alexa = require("alexa-sdk");

exports.handler = (event, context, callback) => {
'use strict';

var alexa = Alexa.handler(event, context, callback);
alexa.registerHandlers(handlers);
alexa.execute();
};

const handlers = {
'LaunchRequest': function () {
'use strict';
this.emit(':tell', 'Hello World!');
},
'PlayStreamIntent': function () {
'use strict';
this.response.audioPlayerPlay('REPLACE_ALL', 'https://superAudio.stream', 'superToken', null, 0);
this.emit(':responseReady');
},
'ClearQueueIntent': function () {
'use strict';
this.response.audioPlayerClearQueue('CLEAR_ALL');
this.emit(':responseReady');
},
'AMAZON.PauseIntent': function () {
'use strict';
this.response.audioPlayerStop();
this.emit(':responseReady');
},
'AMAZON.ResumeIntent': function () {
'use strict';
let offset = this.event.context.AudioPlayer.offsetInMilliseconds || 0;
this.response.audioPlayerPlay('REPLACE_ALL', 'https://superAudio.stream', 'superToken', 'superToken', offset);
this.emit(':responseReady');
},
'AMAZON.StopIntent': function () {
'use strict';
this.response.audioPlayerStop();
this.emit(':responseReady');
},
'AMAZON.CancelIntent': function () {
'use strict';
this.response.audioPlayerStop();
this.emit(':responseReady');
},
};
110 changes: 108 additions & 2 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -92,12 +92,14 @@ module.exports = {
* @param {object} index The object containing your skill's 'handler' method.
* @param {string} appId The Skill's app ID. Looks like "amzn1.ask.skill.00000000-0000-0000-0000-000000000000".
* @param {string} userId The Amazon User ID to test with. Looks like "amzn1.ask.account.LONG_STRING"
* @param {string} deviceId Optional The Amazon Device ID to test with. Looks like "amzn1.ask.device.LONG_STRING"
*/
initialize: function (index, appId, userId) {
initialize: function (index, appId, userId, deviceId) {
'use strict';
this.index = index;
this.appId = appId;
this.userId = userId;
this.deviceId = deviceId || 'amzn1.ask.device.VOID';
},

/**
Expand Down Expand Up @@ -183,6 +185,7 @@ module.exports = {
return {
"version": this.version,
"session": this._getSessionData(),
"context": this._getContextData(),
"request": {
"type": "LaunchRequest",
"requestId": "EdwRequestId." + uuid.v4(),
Expand Down Expand Up @@ -211,6 +214,7 @@ module.exports = {
return {
"version": this.version,
"session": this._getSessionData(),
"context": this._getContextData(),
"request": {
"type": "IntentRequest",
"requestId": "EdwRequestId." + uuid.v4(),
Expand All @@ -232,6 +236,7 @@ module.exports = {
return {
"version": this.version,
"session": this._getSessionData(),
"context": this._getContextData(),
"request": {
"type": "SessionEndedRequest",
"requestId": "EdwRequestId." + uuid.v4(),
Expand All @@ -243,6 +248,32 @@ module.exports = {
};
},

/**
* Adds an AudioPlayer context to the given request.
* @param {object} request The intent request to modify.
* @param {string} token An opaque token that represents the audio stream described by this AudioPlayer object. You provide this token when sending the Play directive.
* @param {string} offset Identifies a track’s offset in milliseconds at the time the request was sent. This is 0 if the track is at the beginning.
* @param {string} activity Indicates the last known state of audio playback.
* @return {object} the given intent request to allow chaining.
*/
addAudioPlayerContextToRequest: function (request, token, offset, activity) {
'use strict';
if (!request) {
throw 'request must be specified to add entity resolution';
}

if (token) {
request.context.AudioPlayer.token = token;
}
if (offset) {
request.context.AudioPlayer.offsetInMilliseconds = offset;
}
if (activity) {
request.context.AudioPlayer.playerActivity = activity;
}
return request;
},

/**
* Adds an entity resolution to the given request.
* @param {object} request The intent request to modify.
Expand Down Expand Up @@ -349,8 +380,11 @@ module.exports = {
* `hasAttributes`: Optional Object. Tests that the response contains the given attributes and values.
* `hasCardTitle`: Optional String. Tests that the card sent by the response has the title specified.
* `hasCardContent`: Optional String. Tests that the card sent by the response has the title specified.
* `withStoredAttributes`: Optional Object. The attributes to initialize the handler with. Used with DynamoDB mock
* `withStoredAttributes`: Optional Object. The attributes to initialize the handler with. Used with DynamoDB mock.
* `storesAttributes`: Optional Object. Tests that the given attributes were stored in the DynamoDB.
* `playsStream`: Optional Object. Tests that the AudioPlayer is used to play a stream.
* `stopsStream`: Optional Boolean. Tests that the AudioPlayer is stopped.
* `clearsQueue`: Optional String. Tests that the AudioPlayer clears the queue with the given clear behavior.
* @param {string} testDescription An optional description for the mocha test
*/
test: function (sequence, testDescription) {
Expand Down Expand Up @@ -517,6 +551,8 @@ module.exports = {
});
}

checkAudioPlayer(self, context, response, currentItem);

// custom checks
if (currentItem.saysCallback) {
currentItem.saysCallback(context, actualSay);
Expand Down Expand Up @@ -628,6 +664,29 @@ module.exports = {
};
},

/**
* Internal method.
*/
_getContextData: function () {
'use strict';
return {
"System": {
"application": {"applicationId": this.appId},
"user": {"userId": this.userId},
"device": {
"deviceId": this.deviceId,
"supportedInterfaces": {
"AudioPlayer": {}
}
},
"apiEndpoint": "https://api.amazonalexa.com/"
},
"AudioPlayer": {
"playerActivity": "IDLE"
}
};
},

/**
* Internal method.
*/
Expand All @@ -644,3 +703,50 @@ module.exports = {
return undefined;
}
};

const checkAudioPlayer = (self, context, response, currentItem) => {
'use strict';

if (currentItem.playsStream) {
let playDirective = self._getDirectiveFromResponse(response, 'AudioPlayer.Play');
if (!playDirective) {
context.assert({message: "the response did not play a stream"});
return;
}

let playConfig = currentItem.playsStream;
self._assertStringEqual(context, "playBehavior", playDirective.playBehavior, playConfig.behavior);

let stream = playDirective.audioItem.stream;
if (!stream.url.startsWith('https://')) {
context.assert({message: "the stream URL is not https"});
}
self._assertStringEqual(context, "url", stream.url, playConfig.url);

if (playConfig.token) {
self._assertStringEqual(context, "token", stream.token, playConfig.token);
}
if (playConfig.previousToken) {
self._assertStringEqual(context, "expectedPreviousToken", stream.expectedPreviousToken, playConfig.previousToken);
}
if (playConfig.offset) {
self._assertStringEqual(context, "offsetInMilliseconds", stream.offsetInMilliseconds.toString(), playConfig.offset.toString());
}
}

if (currentItem.stopsStream) {
if (!self._getDirectiveFromResponse(response, 'AudioPlayer.Stop')) {
context.assert({message: "the response did not stop the stream"});
return;
}
}

if (currentItem.clearsQueue) {
let clearDirective = self._getDirectiveFromResponse(response, 'AudioPlayer.ClearQueue');
if (!clearDirective) {
context.assert({message: "the response did not clear the audio queue"});
return;
}
self._assertStringEqual(context, "clearBehavior", clearDirective.clearBehavior, currentItem.clearsQueue);
}
};

0 comments on commit 0bd5e9c

Please sign in to comment.