Claude
Skills
Sign in
Back

e2e-chat

Included with Lifetime
$97 forever

# E2E Group Chat Tests

General

What this skill does


# E2E Group Chat Tests

Playwright test suite for Constellation v3 group chat features. Tests message sending and display, threaded replies, emoji reactions, and @mention autocomplete. Uses `page.route()` to mock chat API responses and provides seed helpers for creating test channels and messages.

## When to Use This Skill

Use this skill when the user says:
- "test chat"
- "add chat e2e tests"
- "e2e chat"
- "test group chat"
- "playwright chat tests"
- "test chat messages"
- "test chat threads"

## Prerequisites

- Next.js app with App Router
- `e2e` skill installed (Playwright configured with `playwright.config.ts`)
- `group-chat` skill installed (chat channels, messages, threads, reactions, mentions)
- `auth-dev` skill installed (seed users for authentication)
- Dev server running on `localhost:3000`

## Installation

No additional packages required. The `e2e` skill provides `@playwright/test` and the Playwright configuration.

## What Gets Created

```
e2e/
├── fixtures/
│   └── mock-chat.ts            # Chat API mock and seed helpers
├── helpers/
│   └── chat-auth.ts            # Reusable sign-in helper for chat tests
├── chat-messages.spec.ts       # Message list, send, display, pagination tests
├── chat-threads.spec.ts        # Thread panel, replies, thread count tests
├── chat-reactions.spec.ts      # Emoji picker, reaction toggle, count tests
└── chat-mentions.spec.ts       # @mention autocomplete, insertion, display tests
```

## Setup Steps

### Step 1: Create `e2e/fixtures/mock-chat.ts`

Helpers for creating test data and intercepting chat API routes. Provides both seed functions (for real API calls) and mock functions (for intercepted responses).

```typescript
import type { Page } from "@playwright/test";

/**
 * Shape of a chat channel returned by the API.
 */
type MockChannel = {
  id: string;
  name: string;
  description: string;
  createdAt: string;
  memberCount: number;
};

/**
 * Shape of a chat message returned by the API.
 */
type MockMessage = {
  id: string;
  channelId: string;
  content: string;
  authorId: string;
  authorName: string;
  authorAvatar: string;
  createdAt: string;
  threadCount: number;
  reactions: Array<{
    emoji: string;
    count: number;
    userIds: string[];
  }>;
};

/**
 * Shape of a chat user for mention autocomplete.
 */
type MockUser = {
  id: string;
  name: string;
  email: string;
  avatar: string;
};

const MOCK_CHANNEL: MockChannel = {
  id: "ch-test-001",
  name: "general",
  description: "General discussion channel for testing",
  createdAt: "2026-02-18T00:00:00.000Z",
  memberCount: 2,
};

const MOCK_USERS: MockUser[] = [
  {
    id: "user-admin-1",
    name: "Admin User",
    email: "[email protected]",
    avatar: "/avatars/admin.png",
  },
  {
    id: "user-member-1",
    name: "Member User",
    email: "[email protected]",
    avatar: "/avatars/member.png",
  },
];

/**
 * Generates an array of mock messages for a given channel.
 */
function generateMockMessages(
  channelId: string,
  count: number
): MockMessage[] {
  const messages: MockMessage[] = [];
  for (let i = 0; i < count; i++) {
    const author = MOCK_USERS[i % MOCK_USERS.length];
    messages.push({
      id: `msg-${channelId}-${String(i).padStart(4, "0")}`,
      channelId,
      content: `Test message number ${i + 1} in the channel`,
      authorId: author.id,
      authorName: author.name,
      authorAvatar: author.avatar,
      createdAt: new Date(Date.now() - (count - i) * 60_000).toISOString(),
      threadCount: 0,
      reactions: [],
    });
  }
  return messages;
}

/**
 * Seeds a test chat channel by calling the actual API.
 * Returns the created channel data.
 *
 * Requires the user to be authenticated (call signInAsTestUser first).
 */
export async function seedChatChannel(page: Page): Promise<MockChannel> {
  const response = await page.request.post("/api/chat/channels", {
    data: {
      name: `test-${Date.now()}`,
      description: "E2E test channel",
    },
  });

  if (response.ok()) {
    const body = (await response.json()) as { channel: MockChannel };
    return body.channel;
  }

  // If the API is not available, return mock data
  return { ...MOCK_CHANNEL, id: `ch-test-${Date.now()}` };
}

/**
 * Seeds messages into a channel by calling the actual API.
 *
 * Requires the user to be authenticated.
 */
export async function seedMessages(
  page: Page,
  channelId: string,
  count: number
): Promise<MockMessage[]> {
  const messages: MockMessage[] = [];

  for (let i = 0; i < count; i++) {
    const response = await page.request.post(
      `/api/chat/channels/${channelId}/messages`,
      {
        data: {
          content: `Seeded test message ${i + 1}`,
        },
      }
    );

    if (response.ok()) {
      const body = (await response.json()) as { message: MockMessage };
      messages.push(body.message);
    }
  }

  return messages;
}

/**
 * Intercepts chat API routes with mock responses.
 * Use this when you want fully mocked data without hitting the real API.
 *
 * Intercepts:
 * - GET  /api/chat/channels                         -> channel list
 * - GET  /api/chat/channels/:id                     -> single channel
 * - GET  /api/chat/channels/:id/messages             -> message list (with pagination)
 * - POST /api/chat/channels/:id/messages             -> send message
 * - POST /api/chat/channels/:id/messages/:mid/react  -> add reaction
 * - GET  /api/chat/channels/:id/messages/:mid/thread -> thread messages
 * - POST /api/chat/channels/:id/messages/:mid/thread -> reply in thread
 * - GET  /api/chat/users/search                      -> mention autocomplete
 */
export async function interceptChatApi(page: Page): Promise<void> {
  // Track messages so newly sent ones appear in subsequent fetches
  const messageStore: MockMessage[] = generateMockMessages(
    MOCK_CHANNEL.id,
    5
  );

  // Channel list
  await page.route("**/api/chat/channels", async (route) => {
    if (route.request().method() === "GET") {
      await route.fulfill({
        status: 200,
        contentType: "application/json",
        body: JSON.stringify({ channels: [MOCK_CHANNEL] }),
      });
      return;
    }

    if (route.request().method() === "POST") {
      const data = route.request().postDataJSON() as {
        name: string;
        description?: string;
      };
      const newChannel: MockChannel = {
        ...MOCK_CHANNEL,
        id: `ch-${Date.now()}`,
        name: data.name,
        description: data.description ?? "",
      };
      await route.fulfill({
        status: 201,
        contentType: "application/json",
        body: JSON.stringify({ channel: newChannel }),
      });
      return;
    }

    await route.continue();
  });

  // Messages in a channel (supports pagination via ?cursor param)
  await page.route(
    /\/api\/chat\/channels\/[^/]+\/messages$/,
    async (route) => {
      if (route.request().method() === "GET") {
        const url = new URL(route.request().url());
        const cursor = url.searchParams.get("cursor");
        const limit = Number(url.searchParams.get("limit") ?? "20");

        // Simple pagination: if cursor is provided, return older messages
        let messagesToReturn: MockMessage[];
        if (cursor) {
          // Generate older messages for pagination
          messagesToReturn = generateMockMessages(
            MOCK_CHANNEL.id,
            limit
          ).map((msg, idx) => ({
            ...msg,
            id: `msg-old-${idx}`,
            content: `Older message ${idx + 1}`,
            createdAt: new Date(
              Date.now() - (limit + idx) * 120_000
            ).toISOString(),
          }));
        } else {
          messagesToReturn = messageStore.slice(-limit);
        }

        await route.fulfill({
          status: 200,
          contentType: "application/json",
          body: JSON.stringify({
            messages: messagesToReturn,
            nextCursor:
              messagesToReturn.length >= limit
          
Files: 1
Size: 46.0 KB
Complexity: 36/100
Category: General

Related in General