diff --git a/src/__tests__/slack-testing-library.test.ts b/src/__tests__/slack-testing-library.test.ts index 09dfe60..ec7432f 100644 --- a/src/__tests__/slack-testing-library.test.ts +++ b/src/__tests__/slack-testing-library.test.ts @@ -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>).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>).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>).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>).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", + }, + ], + }), + }), + } + ); + }); + }); + }); }); diff --git a/src/slack-testing-library.ts b/src/slack-testing-library.ts index f24f164..d0b04fd 100644 --- a/src/slack-testing-library.ts +++ b/src/slack-testing-library.ts @@ -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); } @@ -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; } /**