Skip to content

Commit b777b8b

Browse files
committed
fix: add framework for less frictional unit testing in ACDL language server, backfill hover tests
1 parent c885d8c commit b777b8b

File tree

20 files changed

+555
-37
lines changed

20 files changed

+555
-37
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -309,7 +309,7 @@
309309
"webpack-dev": "webpack --mode development --watch",
310310
"lint": "eslint src",
311311
"test-compile": "tsc -p ./",
312-
"copyTestData": "cp -R ./test/mockSkill ./out/test",
312+
"copyTestData": "cp -R ./test/mockSkill ./out/test && cp -R ./test/acdlServer/mockACDL ./out/test/acdlServer",
313313
"compile": "shx rm -rf ./out/ && shx rm -rf ./dist/ && tsc -p ./",
314314
"format": "prettier --write .",
315315
"watch": "tsc -watch -p ./",

src/acdlServer/acdlServer.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ const getProjectUri = async (uri: string): Promise<string | void> => {
7272
* @param projectURI
7373
* @returns {Project} retrieve a Project that's already loaded from the "uriToProjects" dictionary
7474
*/
75-
const getProject = (projectURI: string): Project => uriToProjects[projectURI];
75+
export const getProject = (projectURI: string): Project => uriToProjects[projectURI];
7676

7777
/**
7878
* Get all Projects loaded/cached
@@ -145,10 +145,10 @@ export const addProject = async (uri: string): Promise<Project> => {
145145
return uriToProjects[projectUri];
146146
};
147147

148-
export const enum UpdateProjectResult{
148+
export const enum UpdateProjectResult {
149149
UPDATED,
150150
ADDED,
151-
NONE
151+
NONE,
152152
}
153153

154154
/**
@@ -173,7 +173,7 @@ export const updateProject = async (textDocument: TextDocument): Promise<UpdateP
173173
if (project) {
174174
project.update(file);
175175
return UpdateProjectResult.UPDATED;
176-
}
176+
}
177177

178178
await addProject(uri);
179179
return UpdateProjectResult.ADDED;

test/acdlServer/acdlTestUtils.ts

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import {Project, loadProject, loadProjectConfig} from "@alexa/acdl";
2+
import * as path from "path";
3+
import {workspace} from "vscode";
4+
import {MarkupContent, Position} from "vscode-languageclient";
5+
import {pathToFileURL} from "url";
6+
import {getFormalizedURI} from "../../src/acdlServer/utils";
7+
import {getHoverContent} from "../../src/acdlServer/acdlServer";
8+
9+
const mockACDLPath = path.resolve(__dirname, "mockACDL");
10+
const mockACDLConversationsPath = path.resolve(mockACDLPath, "skill-package", "conversations");
11+
12+
/**
13+
* Loads the mockACDL project for testing.
14+
* @returns the file URI location of the project if loaded successfully, `undefined` if not loaded successfully
15+
*/
16+
export async function initializeTestProject(): Promise<Project> {
17+
const projectConfig = await loadProjectConfig(mockACDLPath);
18+
const project = await loadProject(projectConfig);
19+
return project;
20+
}
21+
22+
/**
23+
* Retrieves a Position representing the location of `text` in the file indicated by `testFileName`.
24+
* @param text Text snippet to locate
25+
* @param testFileName Name of the file to locate `text` in. This file is assumed to be in `./mockACDL/skill-package/conversations`.
26+
* @returns Position representing the first found instance of `text` in `testFileName`.
27+
*/
28+
export async function getTextPosition(text: RegExp, testFileName: string): Promise<Position | undefined> {
29+
const testFilePath = path.resolve(mockACDLPath, "skill-package", "conversations", testFileName);
30+
31+
const textDoc = await workspace.openTextDocument(testFilePath);
32+
const match = textDoc.getText().match(text);
33+
34+
if (!match?.index) return undefined;
35+
36+
return textDoc.positionAt(match.index);
37+
}
38+
39+
/**
40+
* Retrieves hover text as if the user's cursor was at the first character of the first found instance of `text` in `testFileName`.
41+
* @param text Text snippet representing where the hover cursor is
42+
* @param testFileName ACDL file in which we should search for `text` and retrieve hover information
43+
* @returns The resulting hover text when `text` is hovered
44+
*/
45+
export async function getHoverText(text: RegExp, testFileName: string): Promise<string> {
46+
const testFilePath = path.resolve(mockACDLConversationsPath, testFileName);
47+
48+
let pos = await getTextPosition(text, testFilePath);
49+
if (!pos) throw new Error(`Position of test text '${text.source}' not found in '${testFileName}'`);
50+
51+
// Increment character by one to better reflect desired location. E.g., without this, getting hover text for `weather` in `response(weather`
52+
// gets the hover text for the ( token, not weather.
53+
pos = {
54+
line: pos.line,
55+
character: pos.character + 1,
56+
};
57+
58+
const hoverResult = await getHoverContent({
59+
textDocument: {
60+
uri: getFormalizedURI(pathToFileURL(testFilePath).toString()),
61+
},
62+
position: pos,
63+
});
64+
65+
const hoverText = hoverResult ? (hoverResult?.contents as MarkupContent).value : "";
66+
67+
// Trim ```acdl\n markup from beginning and \n``` markup from end
68+
return hoverText.substring(8, hoverText.length - 4);
69+
}

test/acdlServer/hover.test.ts

Lines changed: 90 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,41 +1,99 @@
11
import assert from "assert";
2+
import * as sinon from "sinon";
3+
import {Project} from "@alexa/acdl";
4+
import * as server from "../../src/acdlServer/acdlServer";
5+
import {getHoverText, initializeTestProject} from "./acdlTestUtils";
26
import {formatComment, getCommentContent} from "../../src/acdlServer/hover";
37

48
describe("Hover", () => {
5-
it("should get empty string from jsdoc", () => {
6-
const jsdoc = `/**
7-
*/`;
8-
const result = getCommentContent(jsdoc);
9-
const expected = ``;
10-
assert.strictEqual(result, expected);
11-
});
12-
it("should get comment content from jsdoc", () => {
13-
const jsdoc = `/**
14-
* random text
15-
*/`;
16-
const result = getCommentContent(jsdoc);
17-
const expected = `random text`;
18-
assert.strictEqual(result, expected);
19-
});
9+
describe("jsdoc", () => {
10+
it("should get empty string from jsdoc", () => {
11+
const jsdoc = `/**
12+
*/`;
13+
const result = getCommentContent(jsdoc);
14+
const expected = ``;
15+
assert.strictEqual(result, expected);
16+
});
17+
it("should get comment content from jsdoc", () => {
18+
const jsdoc = `/**
19+
* random text
20+
*/`;
21+
const result = getCommentContent(jsdoc);
22+
const expected = `random text`;
23+
assert.strictEqual(result, expected);
24+
});
25+
26+
it("should get comment content from jsdoc - multiline", () => {
27+
const jsdoc = `/**
28+
* random text
29+
* multiline comment
30+
*/`;
31+
const result = getCommentContent(jsdoc);
32+
const expected = `random text\nmultiline comment`;
33+
assert.strictEqual(result, expected);
34+
});
2035

21-
it("should get comment content from jsdoc - multiline", () => {
22-
const jsdoc = `/**
23-
* random text
24-
* multiline comment
25-
*/`;
26-
const result = getCommentContent(jsdoc);
27-
const expected = `random text\nmultiline comment`;
28-
assert.strictEqual(result, expected);
36+
it("should highlight word after @param in jsdoc", () => {
37+
const decl = {
38+
comment: `/**
39+
* @param arg arg description
40+
*/`,
41+
};
42+
const result = formatComment(decl);
43+
const expected = ["@param - `arg` arg description "];
44+
assert.deepStrictEqual(result, expected);
45+
});
2946
});
3047

31-
it("should highlight word after @param in jsdoc", () => {
32-
const decl = {
33-
comment: `/**
34-
* @param arg arg description
35-
*/`,
36-
};
37-
const result = formatComment(decl);
38-
const expected = ["@param - `arg` arg description "];
39-
assert.deepStrictEqual(result, expected);
48+
describe("definitions and declarations", () => {
49+
const sbox = sinon.createSandbox();
50+
const testFileName = "Weather.acdl";
51+
let project: Project | undefined;
52+
53+
before(async () => {
54+
try {
55+
project = await initializeTestProject();
56+
sinon.stub(server, "getProject").returns(project);
57+
} catch (err) {
58+
throw new Error(`Failed to initialize ACDL test project: ${err.message}`);
59+
}
60+
});
61+
62+
after(() => sbox.restore());
63+
64+
it("should display the action declaration when hovering a user-defined action name", async () => {
65+
const hoverText = await getHoverText(/getWeather\(/, testFileName);
66+
67+
const expectedText = "(action) getWeather(City cityName, DATE date) : WeatherResult";
68+
69+
assert.strictEqual(hoverText, expectedText);
70+
});
71+
72+
it("should display the type definition when hovering a user-defined complex type", async () => {
73+
const hoverText = await getHoverText(/WeatherResult getWea/, testFileName);
74+
75+
const expectedText = "type WeatherResult {\n City cityName\n NUMBER highTemp\n NUMBER lowTemp\n}";
76+
77+
assert.strictEqual(hoverText, expectedText);
78+
});
79+
80+
it("should display the declaration for an `expect` call when hovering the expect keyword", async () => {
81+
const hoverText = await getHoverText(/expect\(/, testFileName);
82+
83+
const expectedText = "(action) expect(Type<RequestAct> act, Event<CityAndDate> event) : CityAndDate";
84+
85+
assert.strictEqual(hoverText, expectedText);
86+
});
87+
88+
it("should display the string contents of an APLA document when hovering an APLA template", async () => {
89+
const hoverText = await getHoverText(/weather_apla, N/, testFileName);
90+
91+
const expectedText =
92+
"(ResponseTemplate) weather_apla \n" +
93+
'"In {weatherResult.cityName}, it\'s a high of {weatherResult.highTemp} degrees and a low of {weatherResult.lowTemp} degrees."' +
94+
"\nwhen {weatherResult != null && payload.weatherResult.cityName != null && payload.weatherResult.highTemp != null && payload.weatherResult.lowTemp != null}";
95+
96+
assert.strictEqual(hoverText, expectedText);
97+
});
4098
});
4199
});
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"askcliResourcesVersion": "2020-03-31",
3+
"profiles": {
4+
"default": {
5+
"skillMetadata": {
6+
"src": "./skill-package"
7+
}
8+
}
9+
}
10+
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
namespace com.weatherbot
2+
3+
import com.amazon.alexa.ask.conversations.*
4+
import com.amazon.ask.types.builtins.AMAZON.*
5+
import com.amazon.alexa.schema.Nothing
6+
import prompts.weather_apla
7+
import prompts.request_city_apla
8+
import prompts.request_date_apla
9+
import prompts.request_city_date_apla
10+
11+
type CityAndDate {
12+
optional City cityName
13+
optional DATE date
14+
}
15+
16+
getWeatherEvent = utterances<CityAndDate>(
17+
[
18+
"What's the weather {date} in {cityName}",
19+
"what is the weather {date}",
20+
"How is the weather {date}",
21+
"How is weather in {cityName} {date}",
22+
"how is weather",
23+
"can you please give me weather report for {date}"
24+
]
25+
)
26+
27+
type WeatherResult {
28+
City cityName
29+
NUMBER highTemp
30+
NUMBER lowTemp
31+
}
32+
33+
type ResponsePayload {
34+
WeatherResult weatherResult
35+
}
36+
37+
action WeatherResult getWeather(City cityName, DATE date)
38+
39+
dialog Nothing Weather {
40+
sample {
41+
weatherRequest = expect(Invoke, getWeatherEvent)
42+
43+
ensure(
44+
RequestArguments {arguments = [getWeather.arguments.cityName], response = request_city_apla},
45+
RequestArguments {arguments = [getWeather.arguments.date], response = request_date_apla},
46+
RequestArguments {arguments = [getWeather.arguments.cityName, getWeather.arguments.date], response = request_city_date_apla}
47+
)
48+
49+
weatherResult = getWeather(weatherRequest.cityName, weatherRequest.date)
50+
51+
response(weather_apla, Notify {actionName = getWeather}, payload = ResponsePayload {weatherResult = weatherResult})
52+
}
53+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
{
2+
"interactionModel": {
3+
"languageModel": {
4+
"invocationName": "my weather bot",
5+
"intents": [
6+
{
7+
"name": "AMAZON.CancelIntent",
8+
"samples": []
9+
},
10+
{
11+
"name": "AMAZON.HelpIntent",
12+
"samples": []
13+
},
14+
{
15+
"name": "AMAZON.StopIntent",
16+
"samples": []
17+
},
18+
{
19+
"name": "GetWeatherIntent",
20+
"samples": [
21+
"what is the weather",
22+
"how is the weather",
23+
"tell me the weather"
24+
]
25+
},
26+
{
27+
"name": "AMAZON.NavigateHomeIntent",
28+
"samples": []
29+
},
30+
{
31+
"name": "AMAZON.FallbackIntent",
32+
"samples": []
33+
}
34+
],
35+
"types": []
36+
}
37+
}
38+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
{
2+
"type": "APL-A",
3+
"version": "0.1",
4+
"mainTemplate": {
5+
"parameters": [
6+
"payload"
7+
],
8+
"item": {
9+
"type": "RandomSelector",
10+
"description": "Change 'type' above to try different Selector Component Types like Sequential",
11+
"items": [
12+
{
13+
"type": "Speech",
14+
"contentType": "text",
15+
"content": "Bye.",
16+
"description": "Expand on 'items' array to add multiple prompts, use response template arguments by adding it to 'content' like this ${payload.input_argument_name} and add SSML by changing 'contentType' to 'SSML' and adding SSML to 'content' <amazon:effect name=\"whispered\">like that</amazon:effect>"
17+
}
18+
]
19+
}
20+
}
21+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
{
2+
"type": "APL-A",
3+
"version": "0.1",
4+
"mainTemplate": {
5+
"parameters": [
6+
"payload"
7+
],
8+
"item": {
9+
"type": "RandomSelector",
10+
"description": "Change 'type' above to try different Selector Component Types like Sequential",
11+
"items": [
12+
{
13+
"type": "Speech",
14+
"contentType": "text",
15+
"content": "Sorry, there was an error in the request.",
16+
"description": "Expand on 'items' array to add multiple prompts, use response template arguments by adding it to 'content' like this ${payload.input_argument_name} and add SSML by changing 'contentType' to 'SSML' and adding SSML to 'content' <amazon:effect name=\"whispered\">like that</amazon:effect>"
17+
}
18+
]
19+
}
20+
}
21+
}

0 commit comments

Comments
 (0)