diff --git a/.lintstagedrc.json b/.lintstagedrc.json new file mode 100644 index 000000000..8ff13f577 --- /dev/null +++ b/.lintstagedrc.json @@ -0,0 +1,15 @@ +{ + "apps/web/**/*.{ts,tsx}": [ + "bash -c 'cd apps/web && npx eslint --max-warnings=0 --fix'", + "bash -c 'cd apps/web && npx tsc --noEmit -p tsconfig.json'" + ], + "apps/web/**/*.{js,jsx,json,css,md}": ["prettier --write"], + "veza-backend-api/**/*.go": [ + "bash -c 'cd veza-backend-api && gofmt -l -w \"$@\"' --", + "bash -c 'cd veza-backend-api && go vet ./...'" + ], + "veza-stream-server/**/*.rs": [ + "bash -c 'cd veza-stream-server && cargo fmt --'" + ], + "*.{json,md,yml,yaml}": ["prettier --write"] +} diff --git a/tests/e2e/41-chat-deep.spec.ts b/tests/e2e/41-chat-deep.spec.ts new file mode 100644 index 000000000..c09167dfc --- /dev/null +++ b/tests/e2e/41-chat-deep.spec.ts @@ -0,0 +1,723 @@ +import { test, expect, type Page } from '@playwright/test'; +import { loginViaAPI, CONFIG, navigateTo } from './helpers'; + +/** + * CHAT DEEP — Behavioural E2E tests for the chat feature. + * + * These tests are written BECAUSE Chat has historically been broken. + * They make REAL assertions about the state of the app and will FAIL + * when the feature is broken — that is the whole point. + * + * Components tested (source of truth): + * - apps/web/src/features/chat/pages/ChatPage.tsx + * - apps/web/src/features/chat/components/ChatSidebar.tsx + * - apps/web/src/features/chat/components/ChatRoom.tsx + * - apps/web/src/features/chat/components/ChatInput.tsx + * - apps/web/src/features/chat/components/ChatMessage.tsx + * - apps/web/src/features/chat/hooks/useChat.ts + * - apps/web/src/features/chat/store/chatStore.ts + * + * Selectors derived from the above code: + * - Message input: [aria-label="Type a message"] (placeholder "Broadcast message...") + * - Send button: [aria-label="Send message"] + * - Attach file: [aria-label="Attach file"] + * - Emoji button: [aria-label="Add emoji"] | [aria-label="Close emoji picker"] + * - Voice button: [aria-label="Voice message"] + * - Room name input (dialog): #room-name + * - Channels list heading: "Active Channels" + * - Connection dot: w-2 h-2 rounded-full (bg-success when connected, bg-destructive otherwise) + * - Empty state placeholder: "No conversation selected" | "Pick a channel from the sidebar" + * - Empty message state: "No messages yet" / "Send the first message" + * + * Routes: /chat (protected, redirects to /login when unauthenticated). + */ + +const CHAT_URL = `${CONFIG.baseURL}/chat`; + +// --- Small helpers scoped to this file -------------------------------------- + +/** Wait for the ChatPage to mount and either be connected or show a state. */ +async function waitForChatPageReady(page: Page): Promise { + // The page root renders either: + // - "ESTABLISHING UPLINK..." while loading + // - The main chat layout with "Active Channels" heading + // - The "Access Restricted" card + // - The "Connection Terminated" error card + // We accept any of those as "ready" (mount complete), but prefer the main layout. + await expect( + page.locator( + 'h2:has-text("Active Channels"), h2:has-text("Access Restricted"), h2:has-text("Connection Terminated"), p:has-text("ESTABLISHING UPLINK")', + ).first(), + ).toBeVisible({ timeout: CONFIG.timeouts.navigation }); +} + +/** Get the first conversation row from the sidebar (buttons rendered by ConversationItem). */ +function firstConversationRow(page: Page) { + // ConversationItem renders a