Skip to content

Commit

Permalink
feat: interactWith sends the association action to your base API wh…
Browse files Browse the repository at this point in the history
…en triggered (#6)

* feat: interactWith now sends any associated action back to your app

* interactWith tests
chrishutchinson authored Sep 1, 2021

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
1 parent 1765817 commit 4d0a0a2
Showing 2 changed files with 235 additions and 25 deletions.
177 changes: 177 additions & 0 deletions src/__tests__/slack-testing-library.test.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import { View } from "@slack/types";
import { Message } from "@slack/web-api/dist/response/ChatScheduleMessageResponse";
import { Server } from "http";
import fetch from "cross-fetch";
import { SlackTestingLibrary } from "../slack-testing-library";
import { startServer } from "../util/server";

jest.mock("../util/server");
jest.mock("http");
jest.mock("cross-fetch");

export const createMockServer = ({
listen,
@@ -260,4 +262,179 @@ describe("SlackTestingLibrary", () => {
await sl.getByText("Match: 1234");
});
});

describe("#interactWith()", () => {
it("should throw an error if the server hasn't been initialised", async () => {
const sl = new SlackTestingLibrary({
baseUrl: "https://www.github.com/chrishutchinson/slack-testing-library",
});

await expect(sl.interactWith("button", "Label")).rejects.toThrow(
"Start the Slack listening server first by awaiting `sl.init()`"
);
});

it("should throw an error if an active screen hasn't been set", async () => {
const sl = new SlackTestingLibrary({
baseUrl: "https://www.github.com/chrishutchinson/slack-testing-library",
});

(startServer as jest.Mock<Promise<Server>>).mockImplementation(async () =>
createMockServer()
);

await sl.init();

await expect(sl.interactWith("button", "Label")).rejects.toThrow(
"No active screen"
);
});

describe("active screen: view", () => {
it("should throw if an element with the type and label can't be found in the view", async () => {
const sl = new SlackTestingLibrary({
baseUrl:
"https://www.github.com/chrishutchinson/slack-testing-library",
});

(startServer as jest.Mock<Promise<Server>>).mockImplementation(
async ({ onViewChange }) => {
// Set the active screen
onViewChange({
blocks: [
{
type: "section",
text: {
text: "Match: 1234",
type: "plain_text",
},
accessory: {
type: "button",
action_id: "sample_button_action_id",
text: {
text: "Match: 5678",
type: "plain_text",
},
},
},
],
} as View);

return createMockServer();
}
);

await sl.init();

await expect(sl.interactWith("button", "Match: 1234")).rejects.toThrow(
"Unable to find button with the label 'Match: 1234'."
);
});

it("should throw if a matching element is found in the view but it doesn't have an action ID", async () => {
const sl = new SlackTestingLibrary({
baseUrl:
"https://www.github.com/chrishutchinson/slack-testing-library",
});

(startServer as jest.Mock<Promise<Server>>).mockImplementation(
async ({ onViewChange }) => {
// Set the active screen
onViewChange({
blocks: [
{
type: "section",
text: {
text: "Match: 1234",
type: "plain_text",
},
accessory: {
type: "button",
text: {
text: "Match: 1234",
type: "plain_text",
},
},
},
],
} as View);

return createMockServer();
}
);

await sl.init();

await expect(sl.interactWith("button", "Match: 1234")).rejects.toThrow(
"Unable to interact with the matching button element. It does not have an associated action ID."
);
});

it("should call the URL provided in the constructor with a block action payload", async () => {
const sl = new SlackTestingLibrary({
baseUrl:
"https://www.github.com/chrishutchinson/slack-testing-library",
actor: {
teamId: "T1234567",
userId: "U1234567",
},
});

(startServer as jest.Mock<Promise<Server>>).mockImplementation(
async ({ onViewChange }) => {
// Set the active screen
onViewChange({
blocks: [
{
type: "section",
text: {
text: "Match: 1234",
type: "plain_text",
},
accessory: {
type: "button",
action_id: "sample_button_action_id",
text: {
text: "Match: 1234",
type: "plain_text",
},
},
},
],
} as View);

return createMockServer();
}
);

await sl.init();

await sl.interactWith("button", "Match: 1234");

expect(fetch).toHaveBeenCalledWith(
"https://www.github.com/chrishutchinson/slack-testing-library",
{
method: "post",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
payload: JSON.stringify({
user: {
id: "U1234567",
team_id: "T1234567",
},
type: "block_actions",
actions: [
{
action_id: "sample_button_action_id",
},
],
}),
}),
}
);
});
});
});
});
83 changes: 58 additions & 25 deletions src/slack-testing-library.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Server } from "http";
import fetch from "cross-fetch";
import { Button, KnownBlock, View } from "@slack/types";
import { Button, KnownBlock, SectionBlock, View } from "@slack/types";
import { Message } from "@slack/web-api/dist/response/ChatPostMessageResponse";
import { WebAPICallResult } from "@slack/web-api";

@@ -145,6 +145,31 @@ export class SlackTestingLibrary {
});
}

private async fireInteraction(actionId: string) {
this.checkActorStatus();

await fetch(this.options.baseUrl, {
method: "post",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
payload: JSON.stringify({
user: {
id: this.options.actor!.userId,
team_id: this.options.actor!.teamId,
},
type: "block_actions",
actions: [
{
action_id: actionId,
},
],
}),
}),
});
}

private async checkRequestLog(
matcher: Partial<{
url: string;
@@ -242,25 +267,28 @@ export class SlackTestingLibrary {
}
}

/**
* Opens the "App Home" view
*/
async openHome() {
this.checkServerStatus();

private checkActorStatus() {
if (!this.options.actor) {
throw new Error(
"Please provide an actor team ID and user ID when you initialise SlackTester"
);
}
}

/**
* Opens the "App Home" view
*/
async openHome() {
this.checkServerStatus();
this.checkActorStatus();

await this.fireEvent({
type: "event",
team_id: this.options.actor.teamId,
team_id: this.options.actor!.teamId,
event: {
type: "app_home_opened",
},
user: this.options.actor.userId,
user: this.options.actor!.userId,
} as SlackEvent<any>);
}

@@ -275,22 +303,17 @@ export class SlackTestingLibrary {

async mentionApp({ channelId }: { channelId: string }) {
this.checkServerStatus();

if (!this.options.actor) {
throw new Error(
"Please provide an actor team ID and user ID when you initialise SlackTester"
);
}
this.checkActorStatus();

const timestamp = Date.now();

await this.fireEvent({
type: "event",
team_id: this.options.actor.teamId,
team_id: this.options.actor!.teamId,
event: {
type: "app_mention",
user: this.options.actor.userId,
team: this.options.actor.teamId,
user: this.options.actor!.userId,
team: this.options.actor!.teamId,
text: `<@${this.options.app.botId}>`,
ts: (timestamp / 1000).toFixed(6),
channel: channelId,
@@ -333,9 +356,21 @@ export class SlackTestingLibrary {

if (!matchingElement) {
throw new Error(
`Unable to find ${elementType} with the label ${label}`
`Unable to find ${elementType} with the label '${label}'.`
);
}

const { action_id } = (matchingElement as SectionBlock)
.accessory as Button;

if (!action_id) {
throw new Error(
`Unable to interact with the matching ${elementType} element. It does not have an associated action ID.`
);
}

await this.fireInteraction(action_id);

return;
}

@@ -391,24 +426,22 @@ export class SlackTestingLibrary {
*
* @returns boolean Whether or not a view has been published
*/
async hasViewPublish() {
async hasViewPublish(count = 1) {
this.checkServerStatus();

const requestLog = await this.checkRequestLog({
url: "/slack/api/views.publish",
});

if (requestLog.length === 0) {
if (requestLog.length === 0 && count !== 0) {
throw new Error("Did not find any matching view publishes");
}

if (requestLog.length > 1) {
if (requestLog.length !== count) {
throw new Error(
"Found more than one matching view publishes. Use `hasManyViewPublish` or `hasViewPublish(count)`."
`Did not find ${count} matching view publishes (got ${requestLog.length}).`
);
}

return true;
}

/**

0 comments on commit 4d0a0a2

Please sign in to comment.