diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index bffb451..32cf5f5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -17,3 +17,6 @@ jobs: cache: pnpm - run: pnpm install --frozen-lockfile - run: pnpm test + - run: pnpm build + - run: pnpm exec playwright install --with-deps chromium + - run: pnpm test:smoke diff --git a/.gitignore b/.gitignore index 832c017..c280f51 100644 --- a/.gitignore +++ b/.gitignore @@ -13,6 +13,9 @@ # testing /coverage /src/lib/__tests__/fixtures/ +/playwright-report/ +/test-results/ +/blob-report/ # next.js /.next/ diff --git a/package.json b/package.json index 7bd4595..3b518f6 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,7 @@ "start": "next start", "lint": "eslint", "test": "tsx --test 'src/lib/__tests__/*.test.ts'", + "test:smoke": "playwright test", "benchmark": "tsx scripts/benchmark-rendering.ts", "fetch-fixtures": "tsx scripts/fetch-fixtures.ts" }, @@ -22,6 +23,7 @@ "react-dom": "19.2.4" }, "devDependencies": { + "@playwright/test": "^1.61.1", "@tailwindcss/postcss": "^4", "@types/jsdom": "^28.0.1", "@types/node": "^20", diff --git a/playwright.config.ts b/playwright.config.ts new file mode 100644 index 0000000..8a2ff29 --- /dev/null +++ b/playwright.config.ts @@ -0,0 +1,33 @@ +import { defineConfig, devices } from "@playwright/test"; + +const port = 3210; +const baseURL = `http://127.0.0.1:${port}`; + +export default defineConfig({ + testDir: "./tests/smoke", + fullyParallel: false, + forbidOnly: Boolean(process.env.CI), + retries: process.env.CI ? 1 : 0, + workers: 1, + reporter: process.env.CI ? "github" : "list", + use: { + baseURL, + screenshot: "only-on-failure", + trace: "retain-on-failure", + }, + webServer: { + command: `pnpm dev --hostname 127.0.0.1 --port ${port}`, + env: { + MAIL_BROWSER_SMOKE_TESTS: "1", + }, + reuseExistingServer: false, + timeout: 120_000, + url: `${baseURL}/smoke-tests`, + }, + projects: [ + { + name: "chromium", + use: { ...devices["Desktop Chrome"] }, + }, + ], +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 19cf10b..dc7267a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -16,10 +16,10 @@ importers: version: 17.0.5 next: specifier: 16.2.0 - version: 16.2.0(@babel/core@7.29.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + version: 16.2.0(@babel/core@7.29.0)(@playwright/test@1.61.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) next-auth: specifier: ^5.0.0-beta.30 - version: 5.0.0-beta.30(next@16.2.0(@babel/core@7.29.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react@19.2.4) + version: 5.0.0-beta.30(next@16.2.0(@babel/core@7.29.0)(@playwright/test@1.61.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react@19.2.4) pino: specifier: ^10.3.1 version: 10.3.1 @@ -30,6 +30,9 @@ importers: specifier: 19.2.4 version: 19.2.4(react@19.2.4) devDependencies: + '@playwright/test': + specifier: ^1.61.1 + version: 1.61.1 '@tailwindcss/postcss': specifier: ^4 version: 4.2.2 @@ -663,6 +666,11 @@ packages: '@pinojs/redact@0.4.0': resolution: {integrity: sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==} + '@playwright/test@1.61.1': + resolution: {integrity: sha512-8nKv6+0RJSL9FE4jYOEGXnPeM/Hg12qZpmqzZjRh3qM0Y7c3z1mrOTfFLids72RDQYVh9WpLEfR5WdpNX4fkig==} + engines: {node: '>=18'} + hasBin: true + '@rtsao/scc@1.1.0': resolution: {integrity: sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==} @@ -1393,6 +1401,11 @@ packages: resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==} engines: {node: '>= 0.4'} + fsevents@2.3.2: + resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + fsevents@2.3.3: resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} @@ -1973,6 +1986,16 @@ packages: resolution: {integrity: sha512-r34yH/GlQpKZbU1BvFFqOjhISRo1MNx1tWYsYvmj6KIRHSPMT2+yHOEb1SG6NMvRoHRF0a07kCOox/9yakl1vg==} hasBin: true + playwright-core@1.61.1: + resolution: {integrity: sha512-h7Qlt6m4REp25qvIdvbDtVmD4LqVXfpRxhORv9L0jzETM05p4fuPJ3dKyuSXQxDSbXnmS79HAgi9589lGSpLkg==} + engines: {node: '>=18'} + hasBin: true + + playwright@1.61.1: + resolution: {integrity: sha512-DWnY5o3YbLWK4GovuAVwpqL+1VwGNdUGrRr++8j8PtQQzvAVZUIMjKQ90fY689sEJZJBbZVw1rXaOKSTitkzPQ==} + engines: {node: '>=18'} + hasBin: true + possible-typed-array-names@1.1.0: resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==} engines: {node: '>= 0.4'} @@ -2867,6 +2890,10 @@ snapshots: '@pinojs/redact@0.4.0': {} + '@playwright/test@1.61.1': + dependencies: + playwright: 1.61.1 + '@rtsao/scc@1.1.0': {} '@swc/helpers@0.5.15': @@ -3770,6 +3797,9 @@ snapshots: dependencies: is-callable: 1.2.7 + fsevents@2.3.2: + optional: true + fsevents@2.3.3: optional: true @@ -4189,13 +4219,13 @@ snapshots: natural-compare@1.4.0: {} - next-auth@5.0.0-beta.30(next@16.2.0(@babel/core@7.29.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react@19.2.4): + next-auth@5.0.0-beta.30(next@16.2.0(@babel/core@7.29.0)(@playwright/test@1.61.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react@19.2.4): dependencies: '@auth/core': 0.41.0 - next: 16.2.0(@babel/core@7.29.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + next: 16.2.0(@babel/core@7.29.0)(@playwright/test@1.61.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) react: 19.2.4 - next@16.2.0(@babel/core@7.29.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4): + next@16.2.0(@babel/core@7.29.0)(@playwright/test@1.61.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4): dependencies: '@next/env': 16.2.0 '@swc/helpers': 0.5.15 @@ -4214,6 +4244,7 @@ snapshots: '@next/swc-linux-x64-musl': 16.2.0 '@next/swc-win32-arm64-msvc': 16.2.0 '@next/swc-win32-x64-msvc': 16.2.0 + '@playwright/test': 1.61.1 sharp: 0.34.5 transitivePeerDependencies: - '@babel/core' @@ -4341,6 +4372,14 @@ snapshots: sonic-boom: 4.2.1 thread-stream: 4.0.0 + playwright-core@1.61.1: {} + + playwright@1.61.1: + dependencies: + playwright-core: 1.61.1 + optionalDependencies: + fsevents: 2.3.2 + possible-typed-array-names@1.1.0: {} postcss@8.4.31: diff --git a/src/app/smoke-tests/SmokeHarness.tsx b/src/app/smoke-tests/SmokeHarness.tsx new file mode 100644 index 0000000..ff948b2 --- /dev/null +++ b/src/app/smoke-tests/SmokeHarness.tsx @@ -0,0 +1,165 @@ +"use client"; + +import Link from "next/link"; +import AttachmentList from "@/components/AttachmentList"; +import Composer from "@/components/Composer"; +import EmailListPanel from "@/components/EmailListPanel"; +import type { Email, EmailBodyPart } from "@/lib/types"; + +export type SmokePanel = "inbox" | "reply" | "attachments" | "target"; + +const fixtureEmails: Email[] = [ + { + id: "email-maya", + messageId: ["message-maya@example.test"], + threadId: "thread-maya", + mailboxIds: { "mailbox-inbox": true }, + subject: "Quarterly plan", + from: [{ name: "Maya Chen", email: "maya@example.test" }], + to: [{ name: "Phillip Carter", email: "phillip@example.test" }], + cc: null, + replyTo: null, + inReplyTo: null, + receivedAt: "2026-07-24T16:30:00.000Z", + preview: "The revised plan is ready for review.", + bodyValues: {}, + htmlBody: [], + textBody: [], + attachments: [], + hasAttachment: false, + keywords: {}, + size: 1840, + }, + { + id: "email-release", + messageId: ["message-release@example.test"], + threadId: "thread-release", + mailboxIds: { "mailbox-inbox": true }, + subject: "Release notes", + from: [{ name: "Noah Williams", email: "noah@example.test" }], + to: [{ name: "Phillip Carter", email: "phillip@example.test" }], + cc: null, + replyTo: null, + inReplyTo: null, + receivedAt: "2026-07-24T15:00:00.000Z", + preview: "A short summary of what shipped today.", + bodyValues: {}, + htmlBody: [], + textBody: [], + attachments: [], + hasAttachment: false, + keywords: { "$seen": true }, + size: 1220, + }, +]; + +const fixtureAttachments: EmailBodyPart[] = [ + { + blobId: "blob-sheet", + name: "March water bills.xlsx", + size: 24_576, + type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", + disposition: "attachment", + cid: "historical-file-id", + }, + { + blobId: "blob-inline-logo", + name: "tracking-logo.png", + size: 512, + type: "image/png", + disposition: "inline", + cid: "inline-logo", + }, +]; + +const navItems: Array<{ panel: SmokePanel; label: string }> = [ + { panel: "inbox", label: "Inbox" }, + { panel: "reply", label: "Reply" }, + { panel: "attachments", label: "Attachments" }, +]; + +export default function SmokeHarness({ panel }: { panel: SmokePanel }) { + return ( +
+ The explicit spreadsheet attachment should remain visible. +
++ Navigation completed. +
+