diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d588c83..4c32e77 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -41,5 +41,53 @@ jobs: - name: Run type checking run: pnpm typecheck + - name: Run tests + run: pnpm test + + e2e: + name: E2E Visual Regression Tests + runs-on: ubuntu-latest + timeout-minutes: 15 + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Install pnpm + uses: pnpm/action-setup@v4 + with: + version: 10.8.1 + + - name: Setup Node.js 20.12.0 + uses: actions/setup-node@v4 + with: + node-version: 20.12.0 + cache: 'pnpm' + + - name: Setup Git user + shell: bash + run: | + git config --global user.email "xxx@example.com" + git config --global user.name "user" + + - name: Install dependencies + run: pnpm install --frozen-lockfile + - name: Build project run: pnpm build + + - name: Capture screenshots + run: pnpx tsx ./e2e/captureSnapshot/index.ts + env: + MAX_CONCURRENCY: 5 + + - name: Setup Git user + shell: bash + run: | + git config --global user.email "41898282+github-actions[bot]@users.noreply.github.com" + git config --global user.name "github-actions[bot]" + + - name: Commit screenshots + run: | + git add e2e/snapshots + git diff --staged --exit-code || (git commit -m 'ci(snapshots): update screenshots' && git push) diff --git a/e2e/captureSnapshot/error-pages.ts b/e2e/captureSnapshot/error-pages.ts index 113ba29..5bb6307 100644 --- a/e2e/captureSnapshot/error-pages.ts +++ b/e2e/captureSnapshot/error-pages.ts @@ -1,45 +1,5 @@ -import { resolve } from "node:path"; -import { testDevices } from "../testDevices"; -import { withPlaywright } from "../utils/withPlaywright"; +import { defineCapture } from "../utils/defineCapture"; -// Different error scenarios to capture -const errorScenarios = [ - { - name: "404", - url: "http://localhost:4000/non-existent-page", - }, - { - name: "invalid-project", - url: "http://localhost:4000/projects/non-existent-project", - }, - { - name: "invalid-session", - url: "http://localhost:4000/projects/sample-project/sessions/non-existent-session", - }, -]; - -for (const scenario of errorScenarios) { - for (const { device, name } of testDevices) { - await withPlaywright( - async ({ context, cleanUp }) => { - const page = await context.newPage(); - await page.goto(scenario.url); - await page.screenshot({ - path: resolve( - "e2e", - "snapshots", - "errors", - `${scenario.name}_${name}.png`, - ), - fullPage: true, - }); - await cleanUp(); - }, - { - contextOptions: { - ...device, - }, - }, - ); - } -} +export const errorPagesCapture = defineCapture({ + href: "/non-existent-page", +}); diff --git a/e2e/captureSnapshot/home.ts b/e2e/captureSnapshot/home.ts index 25f51ca..9a89dc7 100644 --- a/e2e/captureSnapshot/home.ts +++ b/e2e/captureSnapshot/home.ts @@ -1,23 +1,5 @@ -import { resolve } from "node:path"; -import { testDevices } from "../testDevices"; -import { withPlaywright } from "../utils/withPlaywright"; +import { defineCapture } from "../utils/defineCapture"; -for (const { device, name } of testDevices) { - await withPlaywright( - async ({ context, cleanUp }) => { - const page = await context.newPage(); - await page.goto("http://localhost:4000/"); - await page.waitForLoadState("networkidle"); - await page.screenshot({ - path: resolve("e2e", "snapshots", "root", `${name}.png`), - fullPage: true, - }); - await cleanUp(); - }, - { - contextOptions: { - ...device, - }, - }, - ); -} +export const homeCapture = defineCapture({ + href: "/", +}); diff --git a/e2e/captureSnapshot/index.ts b/e2e/captureSnapshot/index.ts index 0cca057..1dc1682 100644 --- a/e2e/captureSnapshot/index.ts +++ b/e2e/captureSnapshot/index.ts @@ -1,40 +1,26 @@ -import { execSync } from "node:child_process"; -import { dirname, resolve } from "node:path"; -import { fileURLToPath } from "node:url"; +import { homeCapture } from "./home"; +import { errorPagesCapture } from "./error-pages"; +import { projectsCapture } from "./projects"; +import { projectDetailCapture } from "./project-detail"; +import { sessionDetailCapture } from "./session-detail"; +import { TaskExecutor } from "../utils/TaskExecutor"; -const __filename = fileURLToPath(import.meta.url); -const __dirname = dirname(__filename); +const executor = new TaskExecutor({ + maxConcurrency: process.env['MAX_CONCURRENCY'] ? parseInt(process.env['MAX_CONCURRENCY']) : 10, +}); -const scripts = [ - "home.ts", - "projects.ts", - "project-detail.ts", - "session-detail.ts", - "error-pages.ts", +const tasks = [ + ...homeCapture.tasks, + ...errorPagesCapture.tasks, + ...projectsCapture.tasks, + ...projectDetailCapture.tasks, + ...sessionDetailCapture.tasks, ]; -async function captureAllSnapshots() { - console.log("🚀 Starting screenshot capture for all pages...\n"); +executor.setTasks(tasks); - for (const script of scripts) { - const scriptPath = resolve(__dirname, script); - console.log(`📸 Capturing: ${script.replace(".ts", "")}...`); - - try { - // Execute each script using tsx - execSync(`npx tsx "${scriptPath}"`, { - stdio: "inherit", - cwd: resolve(__dirname, "..", ".."), - }); - console.log(`✅ Completed: ${script.replace(".ts", "")}\n`); - } catch (error) { - console.error( - `❌ Failed: ${script.replace(".ts", "")} - ${error.message}\n`, - ); - } - } - - console.log("🎉 All screenshot captures completed!"); +try { + await executor.execute(); +} catch (error) { + console.error(error); } - -captureAllSnapshots().catch(console.error); diff --git a/e2e/captureSnapshot/project-detail.ts b/e2e/captureSnapshot/project-detail.ts index a76aff7..bfeb70b 100644 --- a/e2e/captureSnapshot/project-detail.ts +++ b/e2e/captureSnapshot/project-detail.ts @@ -1,65 +1,31 @@ -import { resolve } from "node:path"; -import { testDevices } from "../testDevices"; -import { withPlaywright } from "../utils/withPlaywright"; +import { defineCapture } from "../utils/defineCapture"; -// Test different UI states on project detail page -const testStates = [ - { name: "default", action: null }, - { - name: "filters-expanded", - action: async (page) => { - const filterToggle = page.locator( - '[data-testid="filter-toggle"], button:has-text("Filters"), .filter-toggle', - ); - if (await filterToggle.isVisible()) { - await filterToggle.click(); - await page.waitForTimeout(300); - } - }, - }, - { - name: "new-chat-modal", - action: async (page) => { - const newChatButton = page.locator( - 'button:has-text("New Chat"), [data-testid="new-chat"], .new-chat-button', - ); - if (await newChatButton.isVisible()) { - await newChatButton.click(); - await page.waitForTimeout(300); - } - }, - }, -]; - -for (const state of testStates) { - for (const { device, name } of testDevices) { - await withPlaywright( - async ({ context, cleanUp }) => { - const page = await context.newPage(); - await page.goto("http://localhost:4000/projects/sample-project"); - await page.waitForLoadState("networkidle"); - - if (state.action) { - await state.action(page); +export const projectDetailCapture = defineCapture({ + href: "projects/L2hvbWUva2FpdG8vcmVwb3MvY2xhdWRlLWNvZGUtdmlld2VyL2Rpc3Qvc3RhbmRhbG9uZS9tb2NrLWdsb2JhbC1jbGF1ZGUtZGlyL3Byb2plY3RzL3NhbXBsZS1wcm9qZWN0", + cases: [ + { + name: "filters-expanded", + setup: async (page) => { + const filterToggle = page.locator( + '[data-testid="expand-filter-settings-button"]', + ); + if (await filterToggle.isVisible()) { + await filterToggle.click(); + await page.waitForTimeout(300); + } else { + throw new Error("Filter settings button not found"); } - - await page.screenshot({ - path: resolve( - "e2e", - "snapshots", - "project-detail", - state.name, - `${name}.png`, - ), - fullPage: true, - }); - await cleanUp(); }, - { - contextOptions: { - ...device, - }, + }, + { + name: "new-chat-modal", + setup: async (page) => { + const newChatButton = page.locator('[data-testid="new-chat"]'); + if (await newChatButton.isVisible()) { + await newChatButton.click(); + await page.waitForTimeout(300); + } }, - ); - } -} + }, + ], +}); diff --git a/e2e/captureSnapshot/projects.ts b/e2e/captureSnapshot/projects.ts index 8d2a936..5818280 100644 --- a/e2e/captureSnapshot/projects.ts +++ b/e2e/captureSnapshot/projects.ts @@ -1,48 +1,5 @@ -import { resolve } from "node:path"; -import { testDevices } from "../testDevices"; -import { withPlaywright } from "../utils/withPlaywright"; +import { defineCapture } from "../utils/defineCapture"; -// Test different states on projects page -const testStates = [ - { name: "default", action: null }, - { - name: "empty", - action: async (page) => { - // Check for empty state (this will capture whatever state exists) - await page.waitForTimeout(500); - }, - }, -]; - -for (const state of testStates) { - for (const { device, name } of testDevices) { - await withPlaywright( - async ({ context, cleanUp }) => { - const page = await context.newPage(); - await page.goto("http://localhost:4000/projects"); - await page.waitForLoadState("networkidle"); - - if (state.action) { - await state.action(page); - } - - await page.screenshot({ - path: resolve( - "e2e", - "snapshots", - "projects", - state.name, - `${name}.png`, - ), - fullPage: true, - }); - await cleanUp(); - }, - { - contextOptions: { - ...device, - }, - }, - ); - } -} +export const projectsCapture = defineCapture({ + href: "/projects", +}); diff --git a/e2e/captureSnapshot/session-detail.ts b/e2e/captureSnapshot/session-detail.ts index 5888449..2c41be3 100644 --- a/e2e/captureSnapshot/session-detail.ts +++ b/e2e/captureSnapshot/session-detail.ts @@ -1,65 +1,91 @@ -import { resolve } from "node:path"; -import { testDevices } from "../testDevices"; -import { withPlaywright } from "../utils/withPlaywright"; +import { defineCapture } from "../utils/defineCapture"; -// Multiple session IDs to capture different session detail pages -const sessionIds = [ - "1af7fc5e-8455-4414-9ccd-011d40f70b2a", - "5c0375b4-57a5-4f26-b12d-d022ee4e51b7", - "fe5e1c67-53e7-4862-81ae-d0e013e3270b", -]; +export const sessionDetailCapture = defineCapture({ + href: "projects/L2hvbWUva2FpdG8vcmVwb3MvY2xhdWRlLWNvZGUtdmlld2VyL2Rpc3Qvc3RhbmRhbG9uZS9tb2NrLWdsb2JhbC1jbGF1ZGUtZGlyL3Byb2plY3RzL3NhbXBsZS1wcm9qZWN0/sessions/fe5e1c67-53e7-4862-81ae-d0e013e3270b", + cases: [ + { + name: "sidebar-closed", + setup: async (page) => { + const menuButton = page.locator( + '[data-testid="mobile-sidebar-toggle-button"]', + ); + if (await menuButton.isVisible()) { + await menuButton.click(); + await page.waitForTimeout(300); -// Test different sidebar states -const testStates = [ - { name: "default", action: null }, - { - name: "sidebar-open", - action: async (page) => { - const menuButton = page.locator( - '[data-testid="menu-button"], button:has-text("Menu"), .menu-toggle, .hamburger', - ); - if (await menuButton.isVisible()) { - await menuButton.click(); - await page.waitForTimeout(300); - } - }, - }, -]; - -for (const sessionId of sessionIds) { - for (const state of testStates) { - for (const { device, name } of testDevices) { - await withPlaywright( - async ({ context, cleanUp }) => { - const page = await context.newPage(); - await page.goto( - `http://localhost:4000/projects/sample-project/sessions/${sessionId}`, + const sessionsTabButton = page.locator( + '[data-testid="sessions-tab-button-mobile"]', ); - await page.waitForLoadState("networkidle"); - - if (state.action) { - await state.action(page); + if (await sessionsTabButton.isVisible()) { + await sessionsTabButton.click(); + await page.waitForTimeout(300); } + } else { + const sessionsTabButton = page.locator( + '[data-testid="sessions-tab-button"]', + ); + if (await sessionsTabButton.isVisible()) { + await sessionsTabButton.click(); + await page.waitForTimeout(300); + } + } + }, + }, - // Create separate directories for each session - await page.screenshot({ - path: resolve( - "e2e", - "snapshots", - "session-detail", - state.name, - `${sessionId}_${name}.png`, - ), - fullPage: true, - }); - await cleanUp(); - }, - { - contextOptions: { - ...device, - }, - }, - ); - } - } -} + { + name: "mcp-servers-tab", + setup: async (page) => { + const menuButton = page.locator( + '[data-testid="mobile-sidebar-toggle-button"]', + ); + if (await menuButton.isVisible()) { + await menuButton.click(); + await page.waitForTimeout(300); + + const mcpTabButton = page.locator( + '[data-testid="mcp-tab-button-mobile"]', + ); + if (await mcpTabButton.isVisible()) { + await mcpTabButton.click(); + await page.waitForTimeout(300); + } + } else { + const mcpTabButton = page.locator('[data-testid="mcp-tab-button"]'); + if (await mcpTabButton.isVisible()) { + await mcpTabButton.click(); + await page.waitForTimeout(300); + } + } + }, + }, + + { + name: "settings-tab", + setup: async (page) => { + const menuButton = page.locator( + '[data-testid="mobile-sidebar-toggle-button"]', + ); + if (await menuButton.isVisible()) { + await menuButton.click(); + await page.waitForTimeout(300); + + const settingsTabButton = page.locator( + '[data-testid="settings-tab-button-mobile"]', + ); + if (await settingsTabButton.isVisible()) { + await settingsTabButton.click(); + await page.waitForTimeout(300); + } + } else { + const settingsTabButton = page.locator( + '[data-testid="settings-tab-button"]', + ); + if (await settingsTabButton.isVisible()) { + await settingsTabButton.click(); + await page.waitForTimeout(300); + } + } + }, + }, + ], +}); diff --git a/e2e/snapshots/errors/404_desktop.png b/e2e/snapshots/errors/404_desktop.png deleted file mode 100644 index 03f3ad9..0000000 Binary files a/e2e/snapshots/errors/404_desktop.png and /dev/null differ diff --git a/e2e/snapshots/errors/404_mobile.png b/e2e/snapshots/errors/404_mobile.png deleted file mode 100644 index 6cfc9c9..0000000 Binary files a/e2e/snapshots/errors/404_mobile.png and /dev/null differ diff --git a/e2e/snapshots/errors/404_table.png b/e2e/snapshots/errors/404_table.png deleted file mode 100644 index 692c6fd..0000000 Binary files a/e2e/snapshots/errors/404_table.png and /dev/null differ diff --git a/e2e/snapshots/errors/invalid-project_desktop.png b/e2e/snapshots/errors/invalid-project_desktop.png deleted file mode 100644 index 03f3ad9..0000000 Binary files a/e2e/snapshots/errors/invalid-project_desktop.png and /dev/null differ diff --git a/e2e/snapshots/errors/invalid-project_mobile.png b/e2e/snapshots/errors/invalid-project_mobile.png deleted file mode 100644 index 6cfc9c9..0000000 Binary files a/e2e/snapshots/errors/invalid-project_mobile.png and /dev/null differ diff --git a/e2e/snapshots/errors/invalid-project_table.png b/e2e/snapshots/errors/invalid-project_table.png deleted file mode 100644 index 692c6fd..0000000 Binary files a/e2e/snapshots/errors/invalid-project_table.png and /dev/null differ diff --git a/e2e/snapshots/errors/invalid-session_desktop.png b/e2e/snapshots/errors/invalid-session_desktop.png deleted file mode 100644 index 03f3ad9..0000000 Binary files a/e2e/snapshots/errors/invalid-session_desktop.png and /dev/null differ diff --git a/e2e/snapshots/errors/invalid-session_mobile.png b/e2e/snapshots/errors/invalid-session_mobile.png deleted file mode 100644 index 6cfc9c9..0000000 Binary files a/e2e/snapshots/errors/invalid-session_mobile.png and /dev/null differ diff --git a/e2e/snapshots/errors/invalid-session_table.png b/e2e/snapshots/errors/invalid-session_table.png deleted file mode 100644 index 692c6fd..0000000 Binary files a/e2e/snapshots/errors/invalid-session_table.png and /dev/null differ diff --git a/e2e/snapshots/project-detail/default/desktop.png b/e2e/snapshots/project-detail/default/desktop.png deleted file mode 100644 index 03f3ad9..0000000 Binary files a/e2e/snapshots/project-detail/default/desktop.png and /dev/null differ diff --git a/e2e/snapshots/project-detail/default/mobile.png b/e2e/snapshots/project-detail/default/mobile.png deleted file mode 100644 index 6cfc9c9..0000000 Binary files a/e2e/snapshots/project-detail/default/mobile.png and /dev/null differ diff --git a/e2e/snapshots/project-detail/default/table.png b/e2e/snapshots/project-detail/default/table.png deleted file mode 100644 index 692c6fd..0000000 Binary files a/e2e/snapshots/project-detail/default/table.png and /dev/null differ diff --git a/e2e/snapshots/project-detail/filters-expanded/desktop.png b/e2e/snapshots/project-detail/filters-expanded/desktop.png deleted file mode 100644 index 03f3ad9..0000000 Binary files a/e2e/snapshots/project-detail/filters-expanded/desktop.png and /dev/null differ diff --git a/e2e/snapshots/project-detail/filters-expanded/mobile.png b/e2e/snapshots/project-detail/filters-expanded/mobile.png deleted file mode 100644 index 6cfc9c9..0000000 Binary files a/e2e/snapshots/project-detail/filters-expanded/mobile.png and /dev/null differ diff --git a/e2e/snapshots/project-detail/filters-expanded/table.png b/e2e/snapshots/project-detail/filters-expanded/table.png deleted file mode 100644 index 692c6fd..0000000 Binary files a/e2e/snapshots/project-detail/filters-expanded/table.png and /dev/null differ diff --git a/e2e/snapshots/project-detail/new-chat-modal/desktop.png b/e2e/snapshots/project-detail/new-chat-modal/desktop.png deleted file mode 100644 index 03f3ad9..0000000 Binary files a/e2e/snapshots/project-detail/new-chat-modal/desktop.png and /dev/null differ diff --git a/e2e/snapshots/project-detail/new-chat-modal/mobile.png b/e2e/snapshots/project-detail/new-chat-modal/mobile.png deleted file mode 100644 index 6cfc9c9..0000000 Binary files a/e2e/snapshots/project-detail/new-chat-modal/mobile.png and /dev/null differ diff --git a/e2e/snapshots/project-detail/new-chat-modal/table.png b/e2e/snapshots/project-detail/new-chat-modal/table.png deleted file mode 100644 index 692c6fd..0000000 Binary files a/e2e/snapshots/project-detail/new-chat-modal/table.png and /dev/null differ diff --git a/e2e/snapshots/projects/_desktop.png b/e2e/snapshots/projects/_desktop.png deleted file mode 100644 index 03f3ad9..0000000 Binary files a/e2e/snapshots/projects/_desktop.png and /dev/null differ diff --git a/e2e/snapshots/projects/_mobile.png b/e2e/snapshots/projects/_mobile.png deleted file mode 100644 index 6cfc9c9..0000000 Binary files a/e2e/snapshots/projects/_mobile.png and /dev/null differ diff --git a/e2e/snapshots/projects/_table.png b/e2e/snapshots/projects/_table.png deleted file mode 100644 index 692c6fd..0000000 Binary files a/e2e/snapshots/projects/_table.png and /dev/null differ diff --git a/e2e/snapshots/projects/default/desktop.png b/e2e/snapshots/projects/default/desktop.png deleted file mode 100644 index 03f3ad9..0000000 Binary files a/e2e/snapshots/projects/default/desktop.png and /dev/null differ diff --git a/e2e/snapshots/projects/default/mobile.png b/e2e/snapshots/projects/default/mobile.png deleted file mode 100644 index 6cfc9c9..0000000 Binary files a/e2e/snapshots/projects/default/mobile.png and /dev/null differ diff --git a/e2e/snapshots/projects/default/table.png b/e2e/snapshots/projects/default/table.png deleted file mode 100644 index 692c6fd..0000000 Binary files a/e2e/snapshots/projects/default/table.png and /dev/null differ diff --git a/e2e/snapshots/projects/empty/desktop.png b/e2e/snapshots/projects/empty/desktop.png deleted file mode 100644 index 03f3ad9..0000000 Binary files a/e2e/snapshots/projects/empty/desktop.png and /dev/null differ diff --git a/e2e/snapshots/projects/empty/mobile.png b/e2e/snapshots/projects/empty/mobile.png deleted file mode 100644 index 6cfc9c9..0000000 Binary files a/e2e/snapshots/projects/empty/mobile.png and /dev/null differ diff --git a/e2e/snapshots/projects/empty/table.png b/e2e/snapshots/projects/empty/table.png deleted file mode 100644 index 692c6fd..0000000 Binary files a/e2e/snapshots/projects/empty/table.png and /dev/null differ diff --git a/e2e/snapshots/root/desktop.png b/e2e/snapshots/root/desktop.png deleted file mode 100644 index 03f3ad9..0000000 Binary files a/e2e/snapshots/root/desktop.png and /dev/null differ diff --git a/e2e/snapshots/root/mobile.png b/e2e/snapshots/root/mobile.png deleted file mode 100644 index 6cfc9c9..0000000 Binary files a/e2e/snapshots/root/mobile.png and /dev/null differ diff --git a/e2e/snapshots/root/table.png b/e2e/snapshots/root/table.png deleted file mode 100644 index 692c6fd..0000000 Binary files a/e2e/snapshots/root/table.png and /dev/null differ diff --git a/e2e/snapshots/session-detail/default/1af7fc5e-8455-4414-9ccd-011d40f70b2a_desktop.png b/e2e/snapshots/session-detail/default/1af7fc5e-8455-4414-9ccd-011d40f70b2a_desktop.png deleted file mode 100644 index 03f3ad9..0000000 Binary files a/e2e/snapshots/session-detail/default/1af7fc5e-8455-4414-9ccd-011d40f70b2a_desktop.png and /dev/null differ diff --git a/e2e/snapshots/session-detail/default/1af7fc5e-8455-4414-9ccd-011d40f70b2a_mobile.png b/e2e/snapshots/session-detail/default/1af7fc5e-8455-4414-9ccd-011d40f70b2a_mobile.png deleted file mode 100644 index 6cfc9c9..0000000 Binary files a/e2e/snapshots/session-detail/default/1af7fc5e-8455-4414-9ccd-011d40f70b2a_mobile.png and /dev/null differ diff --git a/e2e/snapshots/session-detail/default/1af7fc5e-8455-4414-9ccd-011d40f70b2a_table.png b/e2e/snapshots/session-detail/default/1af7fc5e-8455-4414-9ccd-011d40f70b2a_table.png deleted file mode 100644 index 692c6fd..0000000 Binary files a/e2e/snapshots/session-detail/default/1af7fc5e-8455-4414-9ccd-011d40f70b2a_table.png and /dev/null differ diff --git a/e2e/snapshots/session-detail/default/5c0375b4-57a5-4f26-b12d-d022ee4e51b7_desktop.png b/e2e/snapshots/session-detail/default/5c0375b4-57a5-4f26-b12d-d022ee4e51b7_desktop.png deleted file mode 100644 index 03f3ad9..0000000 Binary files a/e2e/snapshots/session-detail/default/5c0375b4-57a5-4f26-b12d-d022ee4e51b7_desktop.png and /dev/null differ diff --git a/e2e/snapshots/session-detail/default/5c0375b4-57a5-4f26-b12d-d022ee4e51b7_mobile.png b/e2e/snapshots/session-detail/default/5c0375b4-57a5-4f26-b12d-d022ee4e51b7_mobile.png deleted file mode 100644 index 6cfc9c9..0000000 Binary files a/e2e/snapshots/session-detail/default/5c0375b4-57a5-4f26-b12d-d022ee4e51b7_mobile.png and /dev/null differ diff --git a/e2e/snapshots/session-detail/default/5c0375b4-57a5-4f26-b12d-d022ee4e51b7_table.png b/e2e/snapshots/session-detail/default/5c0375b4-57a5-4f26-b12d-d022ee4e51b7_table.png deleted file mode 100644 index 692c6fd..0000000 Binary files a/e2e/snapshots/session-detail/default/5c0375b4-57a5-4f26-b12d-d022ee4e51b7_table.png and /dev/null differ diff --git a/e2e/snapshots/session-detail/default/fe5e1c67-53e7-4862-81ae-d0e013e3270b_desktop.png b/e2e/snapshots/session-detail/default/fe5e1c67-53e7-4862-81ae-d0e013e3270b_desktop.png deleted file mode 100644 index 03f3ad9..0000000 Binary files a/e2e/snapshots/session-detail/default/fe5e1c67-53e7-4862-81ae-d0e013e3270b_desktop.png and /dev/null differ diff --git a/e2e/snapshots/session-detail/default/fe5e1c67-53e7-4862-81ae-d0e013e3270b_mobile.png b/e2e/snapshots/session-detail/default/fe5e1c67-53e7-4862-81ae-d0e013e3270b_mobile.png deleted file mode 100644 index 6cfc9c9..0000000 Binary files a/e2e/snapshots/session-detail/default/fe5e1c67-53e7-4862-81ae-d0e013e3270b_mobile.png and /dev/null differ diff --git a/e2e/snapshots/session-detail/default/fe5e1c67-53e7-4862-81ae-d0e013e3270b_table.png b/e2e/snapshots/session-detail/default/fe5e1c67-53e7-4862-81ae-d0e013e3270b_table.png deleted file mode 100644 index 692c6fd..0000000 Binary files a/e2e/snapshots/session-detail/default/fe5e1c67-53e7-4862-81ae-d0e013e3270b_table.png and /dev/null differ diff --git a/e2e/snapshots/session-detail/sidebar-open/1af7fc5e-8455-4414-9ccd-011d40f70b2a_desktop.png b/e2e/snapshots/session-detail/sidebar-open/1af7fc5e-8455-4414-9ccd-011d40f70b2a_desktop.png deleted file mode 100644 index 03f3ad9..0000000 Binary files a/e2e/snapshots/session-detail/sidebar-open/1af7fc5e-8455-4414-9ccd-011d40f70b2a_desktop.png and /dev/null differ diff --git a/e2e/snapshots/session-detail/sidebar-open/1af7fc5e-8455-4414-9ccd-011d40f70b2a_mobile.png b/e2e/snapshots/session-detail/sidebar-open/1af7fc5e-8455-4414-9ccd-011d40f70b2a_mobile.png deleted file mode 100644 index 6cfc9c9..0000000 Binary files a/e2e/snapshots/session-detail/sidebar-open/1af7fc5e-8455-4414-9ccd-011d40f70b2a_mobile.png and /dev/null differ diff --git a/e2e/snapshots/session-detail/sidebar-open/1af7fc5e-8455-4414-9ccd-011d40f70b2a_table.png b/e2e/snapshots/session-detail/sidebar-open/1af7fc5e-8455-4414-9ccd-011d40f70b2a_table.png deleted file mode 100644 index 692c6fd..0000000 Binary files a/e2e/snapshots/session-detail/sidebar-open/1af7fc5e-8455-4414-9ccd-011d40f70b2a_table.png and /dev/null differ diff --git a/e2e/snapshots/session-detail/sidebar-open/5c0375b4-57a5-4f26-b12d-d022ee4e51b7_desktop.png b/e2e/snapshots/session-detail/sidebar-open/5c0375b4-57a5-4f26-b12d-d022ee4e51b7_desktop.png deleted file mode 100644 index 03f3ad9..0000000 Binary files a/e2e/snapshots/session-detail/sidebar-open/5c0375b4-57a5-4f26-b12d-d022ee4e51b7_desktop.png and /dev/null differ diff --git a/e2e/snapshots/session-detail/sidebar-open/5c0375b4-57a5-4f26-b12d-d022ee4e51b7_mobile.png b/e2e/snapshots/session-detail/sidebar-open/5c0375b4-57a5-4f26-b12d-d022ee4e51b7_mobile.png deleted file mode 100644 index 6cfc9c9..0000000 Binary files a/e2e/snapshots/session-detail/sidebar-open/5c0375b4-57a5-4f26-b12d-d022ee4e51b7_mobile.png and /dev/null differ diff --git a/e2e/snapshots/session-detail/sidebar-open/5c0375b4-57a5-4f26-b12d-d022ee4e51b7_table.png b/e2e/snapshots/session-detail/sidebar-open/5c0375b4-57a5-4f26-b12d-d022ee4e51b7_table.png deleted file mode 100644 index 692c6fd..0000000 Binary files a/e2e/snapshots/session-detail/sidebar-open/5c0375b4-57a5-4f26-b12d-d022ee4e51b7_table.png and /dev/null differ diff --git a/e2e/snapshots/session-detail/sidebar-open/fe5e1c67-53e7-4862-81ae-d0e013e3270b_desktop.png b/e2e/snapshots/session-detail/sidebar-open/fe5e1c67-53e7-4862-81ae-d0e013e3270b_desktop.png deleted file mode 100644 index 03f3ad9..0000000 Binary files a/e2e/snapshots/session-detail/sidebar-open/fe5e1c67-53e7-4862-81ae-d0e013e3270b_desktop.png and /dev/null differ diff --git a/e2e/snapshots/session-detail/sidebar-open/fe5e1c67-53e7-4862-81ae-d0e013e3270b_mobile.png b/e2e/snapshots/session-detail/sidebar-open/fe5e1c67-53e7-4862-81ae-d0e013e3270b_mobile.png deleted file mode 100644 index 6cfc9c9..0000000 Binary files a/e2e/snapshots/session-detail/sidebar-open/fe5e1c67-53e7-4862-81ae-d0e013e3270b_mobile.png and /dev/null differ diff --git a/e2e/snapshots/session-detail/sidebar-open/fe5e1c67-53e7-4862-81ae-d0e013e3270b_table.png b/e2e/snapshots/session-detail/sidebar-open/fe5e1c67-53e7-4862-81ae-d0e013e3270b_table.png deleted file mode 100644 index 692c6fd..0000000 Binary files a/e2e/snapshots/session-detail/sidebar-open/fe5e1c67-53e7-4862-81ae-d0e013e3270b_table.png and /dev/null differ diff --git a/e2e/testDevices.ts b/e2e/testDevices.ts index 8a3ac2a..9d588d8 100644 --- a/e2e/testDevices.ts +++ b/e2e/testDevices.ts @@ -10,7 +10,7 @@ export const testDevices = [ device: devices["iPhone 15"], }, { - name: "table", + name: "tablet", device: devices["iPad Pro 11"], }, ]; diff --git a/e2e/utils/TaskExecutor.ts b/e2e/utils/TaskExecutor.ts new file mode 100644 index 0000000..3f626b6 --- /dev/null +++ b/e2e/utils/TaskExecutor.ts @@ -0,0 +1,136 @@ +import { ulid } from "ulid"; + +export type Task = { + key: string; + execute: () => Promise; +} + +type TaskStatus = { + id: string + task: Task +} & ( + { + status: 'pending' | 'completed' | 'failed' + } | { + status: 'running', + promise: Promise + } +) + +type Options = { + maxConcurrency: number +} + +export class TaskExecutor { + private taskStatuses: TaskStatus[] = [] + private executionPromise?: { + resolve: () => void + reject: (reason?: unknown) => void + promise: Promise + } + private options: Options + + constructor(options?: Partial) { + this.options = { + maxConcurrency: 10, + ...options, + } + } + + private setExecutionPromise() { + let resolveExecution: (() => void) | undefined + let rejectExecution: ((reason?: unknown) => void) | undefined + + const promise = new Promise((resolve, reject) => { + resolveExecution = resolve + rejectExecution = reject + }) + + if (resolveExecution === undefined || rejectExecution === undefined) { + throw new Error('Illegal state: Promise not created') + } + + this.executionPromise = { + resolve: resolveExecution, + reject: rejectExecution, + promise, + } + } + + public setTasks(tasks: Task[]) { + const newTaskStatuses: TaskStatus[] = tasks.map((task) => ({ + id: `${task.key}-${ulid()}`, + status: 'pending', + task, + })) + + this.taskStatuses.push(...newTaskStatuses) + } + + private get pendingTasks() { + return this.taskStatuses.filter((task) => task.status === 'pending') + } + + private get runningTasks() { + return this.taskStatuses.filter((task) => task.status === 'running') + } + + private updateStatus(id: string, status: TaskStatus) { + const found = this.taskStatuses.find((task) => task.id === id) + + if (!found) { + throw new Error(`Task not found: ${id}`) + } + + Object.assign(found, status) + } + + public async execute() { + this.setExecutionPromise() + this.refresh() + await this.executionPromise?.promise + } + + private refresh() { + if (this.runningTasks.length === 0 && this.pendingTasks.length === 0) { + this.executionPromise?.resolve() + console.log('execution completed.') + return; + } + + const remainingTaskCount = this.options.maxConcurrency - this.runningTasks.length + + if (remainingTaskCount <= 0) { + return; + } + + for (const task of this.pendingTasks.slice(0, remainingTaskCount)) { + this.updateStatus(task.id, { + id: task.id, + status: 'running', + task: task.task, + promise: (async () => { + try { + await task.task.execute() + + this.updateStatus(task.id, { + id: task.id, + status: 'completed', + task: task.task, + }) + } catch (error) { + console.error(error) + + this.updateStatus(task.id, { + id: task.id, + status: 'failed', + task: task.task, + }) + } finally { + this.refresh() + } + })(), + }) + } + } +} diff --git a/e2e/utils/defineCapture.ts b/e2e/utils/defineCapture.ts new file mode 100644 index 0000000..2313921 --- /dev/null +++ b/e2e/utils/defineCapture.ts @@ -0,0 +1,83 @@ +import { resolve } from "node:path"; +import type { Page } from "playwright"; +import { testDevices } from "../testDevices"; +import { withPlaywright } from "../utils/withPlaywright"; +import { Task } from "./TaskExecutor"; + +type CaptureCase = { + name: string; + setup: (page: Page) => Promise; +}; + +export const defineCapture = (options: { + href: string; + cases?: readonly CaptureCase[]; +}) => { + const { href, cases = [] } = options; + + const paths = href + .split("/") + .map((path) => path.trim()) + .filter((path) => path !== ""); + + const captureWithCase = async ( + device: (typeof testDevices)[number], + testCase?: CaptureCase, + ) => { + await withPlaywright( + async ({ context, cleanUp }) => { + const page = await context.newPage(); + await page.goto(href); + + await page.waitForLoadState("domcontentloaded"); + await page.waitForTimeout(1000); + + if (testCase) { + await testCase.setup(page); + } + + const picturePath = testCase + ? resolve( + "e2e", + "snapshots", + ...paths, + testCase.name, + `${device.name}.png`, + ) + : resolve("e2e", "snapshots", ...paths, `${device.name}.png`); + + await page.screenshot({ + path: picturePath, + fullPage: true, + }); + + console.log(`[captured] ${picturePath}`); + + await cleanUp(); + }, + { + contextOptions: { + ...device.device, + baseURL: "http://localhost:4000", + }, + }, + ); + }; + + const tasks = testDevices.flatMap((device): Task[] => { + return [ + { + key: `${device.name}-default`, + execute: () => captureWithCase(device), + }, + ...cases.map((testCase) => ({ + key: `${device.name}-${testCase.name}`, + execute: () => captureWithCase(device, testCase), + })), + ]; + }); + + return { + tasks, + } as const; +}; diff --git a/e2e/utils/snapshot-utils.ts b/e2e/utils/snapshot-utils.ts index ababe02..dd31cc6 100644 --- a/e2e/utils/snapshot-utils.ts +++ b/e2e/utils/snapshot-utils.ts @@ -5,14 +5,14 @@ import { withPlaywright } from "./withPlaywright"; /** * Take screenshots for a given URL across all test devices */ -export async function takeScreenshots( +export const takeScreenshots = async ( url: string, snapshotName: string, options?: { waitForSelector?: string; timeout?: number; }, -) { +) => { const { waitForSelector, timeout = 5000 } = options || {}; for (const { device, name } of testDevices) { @@ -69,19 +69,19 @@ export async function takeScreenshots( }, ); } -} +}; /** * Take screenshots of a specific element across all test devices */ -export async function takeElementScreenshots( +export const takeElementScreenshots = async ( url: string, selector: string, snapshotName: string, options?: { timeout?: number; }, -) { +) => { const { timeout = 5000 } = options || {}; for (const { device, name } of testDevices) { @@ -124,4 +124,4 @@ export async function takeElementScreenshots( }, ); } -} +}; diff --git a/e2e/utils/test-utils.ts b/e2e/utils/test-utils.ts index 72ee989..7eada45 100644 --- a/e2e/utils/test-utils.ts +++ b/e2e/utils/test-utils.ts @@ -1,23 +1,9 @@ import { expect, type Page } from "@playwright/test"; -/** - * Utility functions for e2e tests - */ - -/** - * Wait for the application to fully load - */ -export async function waitForAppLoad(page: Page) { - // Wait for React to hydrate and any initial data loading - await page.waitForLoadState("networkidle"); - // Additional wait to ensure all components are rendered - await page.waitForTimeout(1000); -} - /** * Take a full page screenshot for VRT */ -export async function takeFullPageScreenshot(page: Page, name: string) { +export const takeFullPageScreenshot = async (page: Page, name: string) => { // Wait for animations to complete await page.waitForTimeout(300); @@ -46,12 +32,4 @@ export async function takeFullPageScreenshot(page: Page, name: string) { fullPage: true, animations: "disabled", }); -} - -/** - * Navigate to a page and wait for it to load - */ -export async function navigateAndWait(page: Page, path: string) { - await page.goto(path); - await waitForAppLoad(page); -} +}; diff --git a/next.config.ts b/next.config.ts index 68a6c64..c837de7 100644 --- a/next.config.ts +++ b/next.config.ts @@ -2,6 +2,9 @@ import type { NextConfig } from "next"; const nextConfig: NextConfig = { output: "standalone", + typescript: { + ignoreBuildErrors: true, + }, }; export default nextConfig; diff --git a/src/app/projects/[projectId]/components/ProjectPage.tsx b/src/app/projects/[projectId]/components/ProjectPage.tsx index 24d37be..53a90de 100644 --- a/src/app/projects/[projectId]/components/ProjectPage.tsx +++ b/src/app/projects/[projectId]/components/ProjectPage.tsx @@ -68,7 +68,11 @@ export const ProjectPageContent = ({ projectId }: { projectId: string }) => { + diff --git a/src/app/projects/[projectId]/sessions/[sessionId]/components/sessionSidebar/MobileSidebar.tsx b/src/app/projects/[projectId]/sessions/[sessionId]/components/sessionSidebar/MobileSidebar.tsx index 803b0e8..2900418 100644 --- a/src/app/projects/[projectId]/sessions/[sessionId]/components/sessionSidebar/MobileSidebar.tsx +++ b/src/app/projects/[projectId]/sessions/[sessionId]/components/sessionSidebar/MobileSidebar.tsx @@ -129,6 +129,7 @@ export const MobileSidebar: FC = ({ ? "bg-sidebar-accent text-sidebar-accent-foreground shadow-sm" : "text-sidebar-foreground/70", )} + data-testid="sessions-tab-button-mobile" > @@ -143,6 +144,7 @@ export const MobileSidebar: FC = ({ ? "bg-sidebar-accent text-sidebar-accent-foreground shadow-sm" : "text-sidebar-foreground/70", )} + data-testid="mcp-tab-button-mobile" > @@ -157,6 +159,7 @@ export const MobileSidebar: FC = ({ ? "bg-sidebar-accent text-sidebar-accent-foreground shadow-sm" : "text-sidebar-foreground/70", )} + data-testid="settings-tab-button-mobile" > diff --git a/src/app/projects/[projectId]/sessions/[sessionId]/components/sessionSidebar/SessionSidebar.tsx b/src/app/projects/[projectId]/sessions/[sessionId]/components/sessionSidebar/SessionSidebar.tsx index 5518c42..cb926a5 100644 --- a/src/app/projects/[projectId]/sessions/[sessionId]/components/sessionSidebar/SessionSidebar.tsx +++ b/src/app/projects/[projectId]/sessions/[sessionId]/components/sessionSidebar/SessionSidebar.tsx @@ -103,6 +103,7 @@ export const SessionSidebar: FC<{ : "text-sidebar-foreground/70", )} title="Sessions" + data-testid="sessions-tab-button" > @@ -118,6 +119,7 @@ export const SessionSidebar: FC<{ : "text-sidebar-foreground/70", )} title="MCP Servers" + data-testid="mcp-tab-button" > @@ -133,6 +135,7 @@ export const SessionSidebar: FC<{ : "text-sidebar-foreground/70", )} title="Settings" + data-testid="settings-tab-button" >