UNPKG

chat

Version:

Unified chat abstraction for Slack, Teams, Google Chat, and Discord

143 lines (110 loc) 5.74 kB
--- title: Testing description: Test your bot handlers and custom adapters with @chat-adapter/testsVitest factories, custom matchers, and a setup file. type: guide prerequisites: - /docs/getting-started related: - /docs/state - /docs/handling-events - /docs/contributing/testing --- The [`@chat-adapter/tests`](https://www.npmjs.com/package/@chat-adapter/tests) package gives you Vitest factories, custom matchers, and a setup file for testing bots and custom adapters built on Chat SDK. ## Install ```bash pnpm add -D @chat-adapter/tests ``` `chat` and `vitest` are peer dependencies — they should already be in your project. ## Setup file (recommended) Auto-register all matchers by adding the package's setup file to your Vitest config: ```typescript title="vitest.config.ts" lineNumbers import { defineConfig } from "vitest/config"; export default defineConfig({ test: { setupFiles: ["@chat-adapter/tests/setup"], }, }); ``` Without the setup file, register matchers manually: ```typescript import { matchers } from "@chat-adapter/tests/matchers"; expect.extend(matchers); ``` ## Mock factories ```typescript import { createMockAdapter, createMockChatInstance, createMockState, createTestMessage, mockLogger, } from "@chat-adapter/tests"; ``` | Factory | Returns | Notes | |---|---|---| | `createMockAdapter(name?, overrides?)` | `Adapter` | Every method is `vi.fn()` with sensible defaults | | `createMockChatInstance(options?)` | `ChatInstance` | Every `process*` handler is `vi.fn()`; `getState`/`getUserName`/`getLogger` wired up | | `createMockState()` | `MockStateAdapter` | In-memory `Map`s for subscriptions, locks, KV, lists, queues; `cache` exposes the underlying map | | `createTestMessage(id, text, overrides?)` | `Message` | Markdown text is parsed into the formatted AST | | `mockLogger` / `createMockLogger()` | `Logger` | Shared default vs fresh-per-call | ## Matchers | Matcher | Asserts | |---|---| | `expect(adapter).toHavePosted(threadId, textPattern?)` | `adapter.postMessage` was called for this thread | | `expect(adapter).toHaveEdited(threadId, messageId, textPattern?)` | `adapter.editMessage` was called for this message | | `expect(adapter).toHaveDeleted(threadId, messageId)` | `adapter.deleteMessage` was called for this message | | `expect(adapter).toHaveReactedWith(threadId, messageId, emoji)` | `adapter.addReaction` was called with the emoji (string or `EmojiValue.name`) | | `expect(adapter).toHaveStartedTyping(threadId)` | `adapter.startTyping` was called for this thread | | `expect(adapter).toHavePostedToChannel(channelId, textPattern?)` | `adapter.postChannelMessage` was called for this channel | | `expect(chat).toHaveDispatched(handler)` | The named `process*` handler on the mock `ChatInstance` was called | | `expect(state).toBeSubscribedTo(threadId)` | `state.isSubscribed(threadId)` resolves to `true` (async — `await expect(...)`) | Text-pattern matchers extract a comparable string from `AdapterPostableMessage` — strings directly, `PostableMarkdown.markdown`, `PostableRaw.raw`, and `PostableCard.fallbackText`. AST-shaped messages and cards without `fallbackText` aren't text-matchable; assert without `textPattern` and inspect `mock.calls` directly. ## Bot authors: test your handlers When you're building a bot on top of Chat SDK, the kit lets you exercise your handlers without a real Slack/Teams/etc. webhook on the wire: ```typescript title="bot.test.ts" import { describe, expect, it } from "vitest"; import { Chat } from "chat"; import { createMockAdapter, createMockState } from "@chat-adapter/tests"; describe("bot handlers", () => { it("replies with a greeting on mention", async () => { const slack = createMockAdapter("slack"); const state = createMockState(); const bot = new Chat({ userName: "mybot", adapters: { slack }, state, }); bot.onNewMention(async (thread) => { await thread.post("hello there"); }); // Drive a synthesized mention through the bot… // (use your adapter's webhook path or a thread-level call) expect(slack).toHavePosted("slack:C1:t1", /hello there/); }); }); ``` ## Adapter authors: test webhook → dispatch When you're building a custom `Adapter`, the kit gives you a `ChatInstance` mock you can hand to your adapter and assert that webhooks route through the right `process*` hook with the right normalized payload: ```typescript title="adapter.test.ts" import { describe, expect, it } from "vitest"; import { createMockChatInstance } from "@chat-adapter/tests"; import { MyAdapter } from "./adapter"; describe("MyAdapter.handleWebhook", () => { it("dispatches incoming messages through processMessage", async () => { const chat = createMockChatInstance(); const adapter = new MyAdapter({ /* config */ }); await adapter.initialize(chat); const request = new Request("https://example.com/webhook", { method: "POST", body: JSON.stringify({ /* platform-specific payload */ }), headers: { "content-type": "application/json" }, }); const response = await adapter.handleWebhook(request); expect(response.status).toBe(200); expect(chat).toHaveDispatched("processMessage"); }); }); ``` ## Adapter-specific helpers Helpers that depend on a specific platform's wire format (signed Slack webhooks, Teams claim builders, etc.) live in each adapter's own `/testing` subpath rather than in this kit, so adopting `@chat-adapter/tests` doesn't pull in adapter dependencies you don't use. If you're contributing adapters or core to this repo, see the [Testing adapters contributing guide](/docs/contributing/testing) for hand-rolled patterns used inside `packages/`.