{"version":3,"file":"testing.cjs","names":[],"sources":["../src/testing/createMockStreamChatClient.ts","../src/testing/createMockMessagingClient.ts"],"sourcesContent":["import { StreamChat } from 'stream-chat'\n\n/* eslint-disable @typescript-eslint/no-explicit-any */\n\n/** A participant in the mock conversation. */\nexport interface MockMessagingUser {\n  id: string\n  name?: string\n  image?: string\n}\n\nexport interface CreateMockStreamChatClientOptions {\n  /**\n   * Also replace the connection-dependent client methods (`connectUser`,\n   * `disconnectUser`, `userMuteStatus`) with offline no-ops. Needed when the\n   * client drives a flow that would otherwise open a WebSocket or throw\n   * \"Make sure to await connectUser() first.\" offline. Direct `<Chat client>`\n   * rendering (e.g. component stories) does not need this.\n   */\n  stubConnection?: boolean\n}\n\n/**\n * Builds an offline `StreamChat` for mocks and stories: a real client on a\n * throwaway api key with `userID`/`user` set so `<Chat>` treats it as\n * connected without a backend. The single place that knows how to construct a\n * connectionless Stream client, so a stream-chat version bump only needs\n * updating here rather than in each mock surface.\n */\nexport function createMockStreamChatClient(\n  user: MockMessagingUser,\n  { stubConnection = false }: CreateMockStreamChatClientOptions = {}\n): StreamChat {\n  const client = new StreamChat('mock-api-key', {\n    allowServerSideConnect: true,\n  })\n  client.userID = user.id\n  client.user = user as any\n\n  if (stubConnection) {\n    client.connectUser = async () => ({ me: user }) as any\n    client.disconnectUser = async () => undefined\n    client.userMuteStatus = (() => false) as any\n  }\n\n  return client\n}\n\n/* eslint-enable @typescript-eslint/no-explicit-any */\n","import type {\n  Channel,\n  MessageResponse,\n  QueryChannelAPIResponse,\n  SendMessageAPIResponse,\n} from 'stream-chat'\nimport { StreamChat } from 'stream-chat'\n\nimport {\n  createMockStreamChatClient,\n  type MockMessagingUser,\n} from './createMockStreamChatClient'\n\n/**\n * A seed message, authored from the viewer's perspective. `from: 'me'` is the\n * connected user (the viewer); `from: 'them'` is the other participant.\n */\nexport interface MockMessage {\n  id?: string\n  text?: string\n  from: 'me' | 'them'\n  /** Stream attachment payloads, rendered by the toolkit's attachment gate. */\n  // eslint-disable-next-line @typescript-eslint/no-explicit-any\n  attachments?: any[]\n  /** e.g. `{ custom_type: 'MESSAGE_WELCOME' }`. */\n  metadata?: Record<string, unknown>\n}\n\nexport interface CreateMockMessagingClientOptions {\n  /** The connected/viewing user. */\n  currentUser: MockMessagingUser\n  /** The other party in the direct conversation (e.g. the creator/linker). */\n  participant: MockMessagingUser\n  /** Seed conversation, oldest-first. */\n  messages?: MockMessage[]\n  /** Channel id; defaults to `mock-dm`. */\n  channelId?: string\n  /**\n   * Optional canned reply. Called after the viewer sends a message; return the\n   * reply text (or a partial message) to have `participant` echo a response, or\n   * a falsy value for no reply. Drives the \"send echo\" in `dev:mock-messaging`.\n   */\n  onSend?: (\n    text: string\n  ) => string | { text?: string; attachments?: unknown[] } | null | undefined\n}\n\nexport interface MockMessagingClient {\n  /** Pass to `<MessagingProvider client={...}>`. */\n  client: StreamChat\n  /** The seeded direct-conversation channel. */\n  channel: Channel\n  /** Pass to `<MessagingShell initialParticipantFilter={...}>`. */\n  participantFilterId: string\n}\n\nconst REPLY_DELAY_MS = 600\n\n/**\n * Builds an offline Stream client seeded with a single direct conversation, so\n * consumers can render the **real** messaging UI (MessagingProvider →\n * MessagingShell → ChannelView) without a Stream backend — for `dev:mock-*`\n * surfaces and Storybook. It is a real `StreamChat` whose network calls\n * (`connectUser`, `queryChannels`, `channel.watch`, `channel.sendMessage`) are\n * replaced with in-memory behaviour driven by Stream's own channel-state\n * machinery, so rendering fidelity matches production.\n *\n * Ships from `@linktr.ee/messaging-react/testing` and must never be imported by\n * production code.\n */\nexport function createMockMessagingClient({\n  currentUser,\n  participant,\n  messages = [],\n  channelId = 'mock-dm',\n  onSend,\n}: CreateMockMessagingClientOptions): MockMessagingClient {\n  // Offline client marked connected without a WebSocket. `stubConnection` also\n  // no-ops connectUser/disconnectUser/userMuteStatus: `MessagingProvider`\n  // renders `<Chat client={client}>` directly so it never calls connectUser,\n  // and handling the `message.new` events dispatched on send + echo below runs\n  // Channel._countMessageAsUnread → client.userMuteStatus, which throws offline.\n  const client = createMockStreamChatClient(currentUser, {\n    stubConnection: true,\n  })\n\n  const cid = `messaging:${channelId}`\n\n  const toStreamMessage = (\n    message: MockMessage,\n    index: number\n  ): MessageResponse => {\n    const author = message.from === 'me' ? currentUser : participant\n    // Stagger timestamps oldest-first so the list orders naturally. Must be a\n    // Date, not an ISO string: we seed `channel.state.messages` directly\n    // (bypassing Stream's wire→Date parsing), and stream-chat-react calls\n    // `.getTime()` on `created_at` (e.g. useCooldownTimer / addToMessageList).\n    const createdAt = new Date(Date.now() - (messages.length - index) * 60_000)\n    return {\n      id: message.id ?? `mock-msg-${index}`,\n      text: message.text ?? '',\n      type: 'regular',\n      html: message.text ? `<p>${message.text}</p>` : '',\n      user: author,\n      attachments: message.attachments ?? [],\n      latest_reactions: [],\n      own_reactions: [],\n      reaction_counts: {},\n      reaction_scores: {},\n      reply_count: 0,\n      status: 'received',\n      cid,\n      created_at: createdAt,\n      updated_at: createdAt,\n      mentioned_users: [],\n      ...(message.metadata ? { metadata: message.metadata } : {}),\n      // eslint-disable-next-line @typescript-eslint/no-explicit-any\n    } as any\n  }\n\n  const seededMessages = messages.map(toStreamMessage)\n\n  const memberState = {\n    [currentUser.id]: {\n      user: { ...currentUser, is_account: false },\n      user_id: currentUser.id,\n      role: 'owner',\n      is_account: false,\n    },\n    [participant.id]: {\n      user: { ...participant, is_account: true },\n      user_id: participant.id,\n      role: 'member',\n      is_account: true,\n    },\n  }\n\n  const channel = client.channel('messaging', channelId, {\n    // eslint-disable-next-line @typescript-eslint/no-explicit-any\n    members: [currentUser.id, participant.id] as any,\n  })\n  // Read/unread bookkeeping consults channel mute status; without a live\n  // connection the real method throws, so report \"not muted\".\n  channel.muteStatus = () => ({\n    muted: false,\n    createdAt: null,\n    expiresAt: null,\n  })\n\n  // Neutralise the rest of the connection-dependent channel API that\n  // stream-chat-react calls during the chat lifecycle. Each of these POSTs to\n  // Stream or checks for a live WebSocket and would otherwise throw\n  // \"Make sure to await connectUser() first.\" offline. They are fire-and-forget\n  // (read receipts, typing) or read-only (pagination, unread) so no-ops are safe.\n  /* eslint-disable @typescript-eslint/no-explicit-any */\n  channel.markRead = (async () => ({}) as any) as any\n  channel.keystroke = (async () => undefined) as any\n  channel.stopTyping = (async () => undefined) as any\n  channel.sendReaction = (async () => ({}) as any) as any\n  channel.deleteReaction = (async () => ({}) as any) as any\n  channel.countUnread = (() => 0) as any\n  // Pagination: report no older pages so \"load more\" resolves to a no-op.\n  channel.query = (async () => ({ messages: [] }) as any) as any\n  // Leave/Delete Conversation calls channel.hide(); the real method POSTs to\n  // Stream, so no-op it to keep the action working offline.\n  channel.hide = (async () => ({}) as any) as any\n  /* eslint-enable @typescript-eslint/no-explicit-any */\n\n  // Seed state locally instead of fetching. Mirrors the proven ChannelView\n  // story mock: override `watch` to populate `state` and return a canned\n  // QueryChannelAPIResponse. `stream-chat-react` may call `watch()` again on\n  // remount/navigation, so seed only once — otherwise a re-watch would wipe\n  // messages added via `sendMessage` + the send-echo reply, resetting the chat.\n  let hasSeeded = false\n  channel.watch = async () => {\n    if (!hasSeeded) {\n      // eslint-disable-next-line @typescript-eslint/no-explicit-any\n      channel.state.messages = seededMessages as unknown as any[]\n      hasSeeded = true\n    }\n    // eslint-disable-next-line @typescript-eslint/no-explicit-any\n    channel.state.members = memberState as unknown as any\n    return {\n      channel: { members: [currentUser.id, participant.id] },\n      members: [],\n      messages: channel.state.messages,\n      watchers: [],\n      pinned_messages: [],\n      duration: '0ms',\n    } as unknown as QueryChannelAPIResponse\n  }\n\n  const appendMessage = (message: MessageResponse) => {\n    // addMessageSorted is Stream's own state mutation (dedupes by id), so the\n    // toolkit re-renders through the real channel-state path.\n    // eslint-disable-next-line @typescript-eslint/no-explicit-any\n    channel.state.addMessageSorted(message as any)\n    client.dispatchEvent({\n      type: 'message.new',\n      cid,\n      message,\n      user: message.user ?? undefined,\n      // eslint-disable-next-line @typescript-eslint/no-explicit-any\n    } as any)\n  }\n\n  let sentCount = 0\n  let replyCount = 0\n  channel.sendMessage = async (message) => {\n    const text =\n      typeof message === 'string'\n        ? message\n        : ((message as { text?: string })?.text ?? '')\n    const sent = {\n      ...toStreamMessage(\n        { text, from: 'me', id: `mock-sent-${sentCount++}` },\n        seededMessages.length\n      ),\n      // eslint-disable-next-line @typescript-eslint/no-explicit-any\n      ...(typeof message === 'object' ? (message as any) : {}),\n      user: currentUser,\n      cid,\n    } as MessageResponse\n\n    // stream-chat-react adds the outgoing message optimistically; addMessageSorted\n    // dedupes by id, so confirming here is safe and idempotent.\n    appendMessage(sent)\n\n    const reply = onSend?.(text)\n    if (reply) {\n      const replyText = typeof reply === 'string' ? reply : reply.text\n      const replyAttachments =\n        typeof reply === 'string' ? undefined : reply.attachments\n      // Capture a unique id before scheduling: two sends within REPLY_DELAY_MS\n      // would otherwise both read the same later `sentCount` at fire time and\n      // emit colliding reply ids, so Stream's id-based dedupe drops one echo.\n      const replyId = `mock-reply-${replyCount++}`\n      setTimeout(() => {\n        appendMessage(\n          toStreamMessage(\n            {\n              text: replyText,\n              from: 'them',\n              // eslint-disable-next-line @typescript-eslint/no-explicit-any\n              attachments: replyAttachments as any,\n              id: replyId,\n            },\n            seededMessages.length\n          )\n        )\n      }, REPLY_DELAY_MS)\n    }\n\n    return { message: sent } as unknown as SendMessageAPIResponse\n  }\n\n  // MessagingShell finds the direct conversation via queryChannels; always\n  // return the one seeded channel regardless of the filter.\n  // eslint-disable-next-line @typescript-eslint/no-explicit-any\n  client.queryChannels = async () => [channel] as any\n\n  // Initialise state up-front so first render already has the conversation.\n  void channel.watch()\n\n  return { client, channel, participantFilterId: participant.id }\n}\n"],"mappings":"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"}