diff --git a/.github/actions/setup-node/action.yml b/.github/actions/setup-node/action.yml new file mode 100644 index 0000000..664be70 --- /dev/null +++ b/.github/actions/setup-node/action.yml @@ -0,0 +1,19 @@ +name: "Setup Node & pnpm" +description: "Install pnpm and setup Node.js with pnpm cache (fixed versions)" +runs: + using: "composite" + steps: + - 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: Install Dependencies + shell: bash + run: pnpm install --frozen-lockfile --ignore-scripts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2dd16a1..4b36797 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -21,19 +21,8 @@ jobs: - 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: Install dependencies - run: pnpm install --frozen-lockfile + - name: Setup Node & pnpm + uses: ./.github/actions/setup-node - name: Run linting run: pnpm lint @@ -46,20 +35,15 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 15 + permissions: + contents: write + 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 Node & pnpm + uses: ./.github/actions/setup-node - name: Setup Git user shell: bash @@ -67,16 +51,20 @@ jobs: git config --global user.email "41898282+github-actions[bot]@users.noreply.github.com" git config --global user.name "github-actions[bot]" - - name: Install dependencies - run: pnpm install --frozen-lockfile + - name: Install Playwright + run: | + sudo npx playwright install-deps + pnpm exec playwright install - name: Build project run: pnpm build - name: Capture screenshots - run: pnpx tsx ./e2e/captureSnapshot/index.ts + timeout-minutes: 3 + run: pnpm e2e env: MAX_CONCURRENCY: 5 + PORT: 4000 - name: Commit screenshots run: | diff --git a/e2e/captureSnapshot/project-detail.ts b/e2e/captureSnapshot/project-detail.ts index bfeb70b..1c04429 100644 --- a/e2e/captureSnapshot/project-detail.ts +++ b/e2e/captureSnapshot/project-detail.ts @@ -1,7 +1,8 @@ +import { projectIds } from "../config"; import { defineCapture } from "../utils/defineCapture"; export const projectDetailCapture = defineCapture({ - href: "projects/L2hvbWUva2FpdG8vcmVwb3MvY2xhdWRlLWNvZGUtdmlld2VyL2Rpc3Qvc3RhbmRhbG9uZS9tb2NrLWdsb2JhbC1jbGF1ZGUtZGlyL3Byb2plY3RzL3NhbXBsZS1wcm9qZWN0", + href: `projects/${projectIds.sampleProject}`, cases: [ { name: "filters-expanded", diff --git a/e2e/captureSnapshot/session-detail.ts b/e2e/captureSnapshot/session-detail.ts index 2c41be3..a5d316a 100644 --- a/e2e/captureSnapshot/session-detail.ts +++ b/e2e/captureSnapshot/session-detail.ts @@ -1,7 +1,8 @@ +import { projectIds } from "../config"; import { defineCapture } from "../utils/defineCapture"; export const sessionDetailCapture = defineCapture({ - href: "projects/L2hvbWUva2FpdG8vcmVwb3MvY2xhdWRlLWNvZGUtdmlld2VyL2Rpc3Qvc3RhbmRhbG9uZS9tb2NrLWdsb2JhbC1jbGF1ZGUtZGlyL3Byb2plY3RzL3NhbXBsZS1wcm9qZWN0/sessions/fe5e1c67-53e7-4862-81ae-d0e013e3270b", + href: `projects/${projectIds.sampleProject}/sessions/fe5e1c67-53e7-4862-81ae-d0e013e3270b`, cases: [ { name: "sidebar-closed", @@ -32,33 +33,6 @@ export const sessionDetailCapture = defineCapture({ }, }, - { - 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) => { diff --git a/e2e/config.ts b/e2e/config.ts new file mode 100644 index 0000000..17f75f4 --- /dev/null +++ b/e2e/config.ts @@ -0,0 +1,15 @@ +import { homedir } from "node:os"; +import { resolve } from "node:path"; +import { encodeProjectId } from "../src/server/service/project/id"; + +// biome-ignore lint/complexity/useLiteralKeys: env var +export const globalClaudeDirectoryPath = process.env["GLOBAL_CLAUDE_DIR"] + ? // biome-ignore lint/complexity/useLiteralKeys: env var + resolve(process.env["GLOBAL_CLAUDE_DIR"]) + : resolve(homedir(), ".claude"); + +export const projectIds = { + sampleProject: encodeProjectId( + resolve(globalClaudeDirectoryPath, "projects", "sample-project"), + ), +} as const; diff --git a/e2e/example.ts b/e2e/example.ts deleted file mode 100644 index cfb1bf2..0000000 --- a/e2e/example.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { resolve } from "node:path"; -import { testDevices } from "./testDevices"; -import { withPlaywright } from "./utils/withPlaywright"; - -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.screenshot({ - path: resolve("e2e", "snapshots", "projects", `_${name}.png`), - fullPage: true, - }); - await cleanUp(); - }, - { - contextOptions: { - ...device, - }, - }, - ); -} diff --git a/e2e/utils/snapshot-utils.ts b/e2e/utils/snapshot-utils.ts deleted file mode 100644 index dd31cc6..0000000 --- a/e2e/utils/snapshot-utils.ts +++ /dev/null @@ -1,127 +0,0 @@ -import { resolve } from "node:path"; -import { testDevices } from "../testDevices"; -import { withPlaywright } from "./withPlaywright"; - -/** - * Take screenshots for a given URL across all test devices - */ -export const takeScreenshots = async ( - url: string, - snapshotName: string, - options?: { - waitForSelector?: string; - timeout?: number; - }, -) => { - const { waitForSelector, timeout = 5000 } = options || {}; - - for (const { device, name } of testDevices) { - await withPlaywright( - async ({ context, cleanUp }) => { - const page = await context.newPage(); - - try { - await page.goto(url); - - // Wait for the page to load - await page.waitForLoadState("networkidle"); - - // Wait for specific selector if provided - if (waitForSelector) { - await page.waitForSelector(waitForSelector, { timeout }); - } - - // Additional wait for content to stabilize - await page.waitForTimeout(1000); - - // Hide dynamic content that might cause flaky screenshots - await page.addStyleTag({ - content: ` - /* Hide elements with potential timing issues */ - [data-testid="timestamp"], - .timestamp, - time { - opacity: 0 !important; - } - - /* Disable animations for consistent screenshots */ - *, *::before, *::after { - animation-duration: 0s !important; - animation-delay: 0s !important; - transition-duration: 0s !important; - transition-delay: 0s !important; - } - `, - }); - - await page.screenshot({ - path: resolve("e2e", "snapshots", snapshotName, `${name}.png`), - fullPage: true, - }); - } finally { - await cleanUp(); - } - }, - { - contextOptions: { - ...device, - }, - }, - ); - } -}; - -/** - * Take screenshots of a specific element across all test devices - */ -export const takeElementScreenshots = async ( - url: string, - selector: string, - snapshotName: string, - options?: { - timeout?: number; - }, -) => { - const { timeout = 5000 } = options || {}; - - for (const { device, name } of testDevices) { - await withPlaywright( - async ({ context, cleanUp }) => { - const page = await context.newPage(); - - try { - await page.goto(url); - await page.waitForLoadState("networkidle"); - - const element = page.locator(selector); - await element.waitFor({ state: "visible", timeout }); - - await page.addStyleTag({ - content: ` - *, *::before, *::after { - animation-duration: 0s !important; - transition-duration: 0s !important; - } - `, - }); - - await element.screenshot({ - path: resolve( - "e2e", - "snapshots", - snapshotName, - `${name}-element.png`, - ), - }); - } finally { - await cleanUp(); - } - }, - { - contextOptions: { - ...device, - }, - }, - ); - } -}; diff --git a/e2e/utils/test-utils.ts b/e2e/utils/test-utils.ts deleted file mode 100644 index 7eada45..0000000 --- a/e2e/utils/test-utils.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { expect, type Page } from "@playwright/test"; - -/** - * Take a full page screenshot for VRT - */ -export const takeFullPageScreenshot = async (page: Page, name: string) => { - // Wait for animations to complete - await page.waitForTimeout(300); - - // Hide elements that might cause flaky tests - await page.addStyleTag({ - content: ` - /* Hide elements with potential timing issues */ - [data-testid="timestamp"], - .timestamp, - time { - visibility: hidden !important; - } - - /* Disable animations for consistent screenshots */ - *, *::before, *::after { - animation-duration: 0s !important; - animation-delay: 0s !important; - transition-duration: 0s !important; - transition-delay: 0s !important; - } - `, - }); - - // Take full page screenshot - await expect(page).toHaveScreenshot(`${name}.png`, { - fullPage: true, - animations: "disabled", - }); -}; diff --git a/package.json b/package.json index 3812915..157b6c3 100644 --- a/package.json +++ b/package.json @@ -23,9 +23,8 @@ }, "scripts": { "dev": "run-p 'dev:*'", - "dev:next": "PORT=3400 next dev --turbopack", - "start": "PORT=3300 node dist/index.js", - "start:e2e": "PORT=4000 GLOBAL_CLAUDE_DIR=./mock-global-claude-dir node dist/index.js", + "dev:next": "next dev --turbopack", + "start": "node dist/index.js", "build": "./scripts/build.sh", "lint": "run-s 'lint:*'", "lint:biome-format": "biome format .", @@ -36,17 +35,7 @@ "typecheck": "tsc --noEmit", "test": "vitest --run", "test:watch": "vitest", - "test:e2e": "GLOBAL_CLAUDE_DIR=./mock-global-claude-dir playwright test", - "test:e2e:ui": "GLOBAL_CLAUDE_DIR=./mock-global-claude-dir playwright test --ui", - "test:e2e:headed": "GLOBAL_CLAUDE_DIR=./mock-global-claude-dir playwright test --headed", - "test:e2e:update": "GLOBAL_CLAUDE_DIR=./mock-global-claude-dir playwright test --update-snapshots", - "screenshots": "GLOBAL_CLAUDE_DIR=./mock-global-claude-dir node e2e/tests/projects-page-new.spec.ts", - "screenshots:all": "run-s screenshots:*", - "screenshots:projects": "GLOBAL_CLAUDE_DIR=./mock-global-claude-dir node e2e/tests/projects-page-new.spec.ts", - "screenshots:project-detail": "GLOBAL_CLAUDE_DIR=./mock-global-claude-dir node e2e/tests/project-detail-new.spec.ts", - "screenshots:session-detail": "GLOBAL_CLAUDE_DIR=./mock-global-claude-dir node e2e/tests/session-detail-new.spec.ts", - "screenshots:error-states": "GLOBAL_CLAUDE_DIR=./mock-global-claude-dir node e2e/tests/error-states.spec.ts", - "screenshots:modals": "GLOBAL_CLAUDE_DIR=./mock-global-claude-dir node e2e/tests/modal-components.spec.ts" + "e2e": "./scripts/e2e.sh" }, "dependencies": { "@anthropic-ai/claude-code": "^1.0.98", diff --git a/playwright.config.ts b/playwright.config.ts deleted file mode 100644 index 739597e..0000000 --- a/playwright.config.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { defineConfig, devices } from '@playwright/test'; - -const isCI = process.env['CI'] === 'true'; - -/** - * See https://playwright.dev/docs/test-configuration. - */ -export default defineConfig({ - testDir: './e2e', - fullyParallel: true, - forbidOnly: isCI, - retries: isCI ? 2 : 0, - workers: isCI ? 1 : undefined, - reporter: 'html', - use: { - baseURL: 'http://localhost:4000', - trace: 'on-first-retry', - screenshot: 'only-on-failure', - }, - - - projects: [ - { - name: 'chromium', - use: { ...devices['Desktop Chrome'] }, - }, - - { - name: 'Mobile Chrome', - use: { ...devices['Pixel 5'] }, - }, - - { - name: 'Mobile Safari', - use: { ...devices['iPhone 12'] }, - }, - ], - - webServer: { - command: 'pnpm start:e2e', - url: 'http://localhost:4000', - reuseExistingServer: false, - env: { - GLOBAL_CLAUDE_DIR: './mock-global-claude-dir' - } - }, -}); \ No newline at end of file diff --git a/scripts/e2e.sh b/scripts/e2e.sh new file mode 100755 index 0000000..9c87aad --- /dev/null +++ b/scripts/e2e.sh @@ -0,0 +1,39 @@ +#!/usr/bin/env bash + +set -euo pipefail + +export PORT=4000 +export GLOBAL_CLAUDE_DIR=$(git rev-parse --show-toplevel)/mock-global-claude-dir + +echo "Check directory structure in $GLOBAL_CLAUDE_DIR:" +ls -l $GLOBAL_CLAUDE_DIR + +kill_process_group() { + local pid=$1 + local sig=${2:-TERM} + + local children=$(pgrep -P $pid 2>/dev/null || true) + for child in $children; do + kill_process_group $child $sig + done + + kill -$sig $pid 2>/dev/null || true +} + +# cleanup関数を定義してtrapで確実に実行 +cleanup() { + if [ -n "${SERVER_PID:-}" ]; then + echo "Killing server (PID: $SERVER_PID) and its process tree..." + kill_process_group $SERVER_PID KILL + fi +} + +trap cleanup EXIT INT TERM + +pnpm start & SERVER_PID=$! +echo "Server started. pid=$SERVER_PID" + +sleep 5 # 即時起動するが、一応少し待っておく + +pnpx tsx ./e2e/captureSnapshot/index.ts +echo "Completed capturing screenshots. Killing server..." diff --git a/src/server/hono/route.ts b/src/server/hono/route.ts index 9121cba..8b91755 100644 --- a/src/server/hono/route.ts +++ b/src/server/hono/route.ts @@ -39,11 +39,14 @@ export const routes = (app: HonoAppType) => { const fileWatcher = getFileWatcher(); const eventBus = getEventBus(); - fileWatcher.startWatching(); + // biome-ignore lint/complexity/useLiteralKeys: env var + if (process.env["NEXT_PHASE"] !== "phase-production-build") { + fileWatcher.startWatching(); - setInterval(() => { - eventBus.emit("heartbeat", {}); - }, 10 * 1000); + setInterval(() => { + eventBus.emit("heartbeat", {}); + }, 10 * 1000); + } return ( app