mirror of
https://github.com/aljazceru/claude-code-viewer.git
synced 2026-01-14 11:04:21 +01:00
ci(e2e): start:e2e server before capturing
This commit is contained in:
19
.github/actions/setup-node/action.yml
vendored
Normal file
19
.github/actions/setup-node/action.yml
vendored
Normal file
@@ -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
|
||||
40
.github/workflows/ci.yml
vendored
40
.github/workflows/ci.yml
vendored
@@ -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: |
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
15
e2e/config.ts
Normal file
15
e2e/config.ts
Normal file
@@ -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;
|
||||
@@ -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,
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -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,
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
};
|
||||
@@ -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",
|
||||
});
|
||||
};
|
||||
17
package.json
17
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",
|
||||
|
||||
@@ -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'
|
||||
}
|
||||
},
|
||||
});
|
||||
39
scripts/e2e.sh
Executable file
39
scripts/e2e.sh
Executable file
@@ -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..."
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user