feat(e2e): enhance end-to-end testing setup with new scripts and capture cases

- Updated package.json to include new E2E scripts for execution and snapshot capturing.
- Added new capture cases for the "new-project-modal" and "start-new-chat" functionalities in the E2E tests.
- Increased wait times in session detail captures to ensure elements are fully loaded before interactions.
- Introduced new shell scripts for starting the server and capturing snapshots, improving the E2E testing workflow.
- Updated NewChatModal and SessionsTab components to include data-testid attributes for better test targeting.
This commit is contained in:
d-kimsuon
2025-10-18 16:56:08 +09:00
parent 3e598eadbb
commit 1e62eeb856
13 changed files with 117 additions and 61 deletions

View File

@@ -2,4 +2,16 @@ import { defineCapture } from "../utils/defineCapture";
export const projectsCapture = defineCapture({
href: "/projects",
cases: [
{
name: "new-project-modal",
setup: async (page) => {
const newProjectButton = page.locator(
'[data-testid="new-project-button"]',
);
await newProjectButton.click();
await page.waitForTimeout(1000);
},
},
],
});

View File

@@ -12,14 +12,14 @@ export const sessionDetailCapture = defineCapture({
);
if (await menuButton.isVisible()) {
await menuButton.click();
await page.waitForTimeout(300);
await page.waitForTimeout(1000);
const sessionsTabButton = page.locator(
'[data-testid="sessions-tab-button-mobile"]',
);
if (await sessionsTabButton.isVisible()) {
await sessionsTabButton.click();
await page.waitForTimeout(300);
await page.waitForTimeout(1000);
}
} else {
const sessionsTabButton = page.locator(
@@ -27,7 +27,7 @@ export const sessionDetailCapture = defineCapture({
);
if (await sessionsTabButton.isVisible()) {
await sessionsTabButton.click();
await page.waitForTimeout(300);
await page.waitForTimeout(1000);
}
}
},
@@ -41,14 +41,14 @@ export const sessionDetailCapture = defineCapture({
);
if (await menuButton.isVisible()) {
await menuButton.click();
await page.waitForTimeout(300);
await page.waitForTimeout(1000);
const settingsTabButton = page.locator(
'[data-testid="settings-tab-button-mobile"]',
);
if (await settingsTabButton.isVisible()) {
await settingsTabButton.click();
await page.waitForTimeout(300);
await page.waitForTimeout(1000);
}
} else {
const settingsTabButton = page.locator(
@@ -56,10 +56,35 @@ export const sessionDetailCapture = defineCapture({
);
if (await settingsTabButton.isVisible()) {
await settingsTabButton.click();
await page.waitForTimeout(300);
await page.waitForTimeout(1000);
}
}
},
},
{
name: "start-new-chat",
setup: async (page) => {
const menuButton = page.locator(
'[data-testid="mobile-sidebar-toggle-button"]',
);
if (await menuButton.isVisible()) {
await menuButton.click();
await page.waitForTimeout(1000);
const startNewChatButton = page.locator(
'[data-testid="start-new-chat-button-mobile"]',
);
await startNewChatButton.click();
await page.waitForTimeout(1000);
} else {
const startNewChatButton = page.locator(
'[data-testid="start-new-chat-button"]',
);
await startNewChatButton.click();
await page.waitForTimeout(1000);
}
},
},
],
});

View File

@@ -26,34 +26,38 @@ export const defineCapture = (options: {
) => {
await withPlaywright(
async ({ context, cleanUp }) => {
const page = await context.newPage();
await page.goto(href);
try {
const page = await context.newPage();
await page.goto(href);
await page.waitForLoadState("domcontentloaded");
await page.waitForTimeout(1000);
await page.waitForLoadState("domcontentloaded");
await page.waitForTimeout(1000);
if (testCase) {
await testCase.setup(page);
if (testCase) {
await testCase.setup(page);
}
await page.waitForTimeout(1000);
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}`);
} finally {
await cleanUp();
}
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: {

View File

@@ -1,5 +1,3 @@
import { existsSync } from "node:fs";
import path from "node:path";
import {
type Browser,
type BrowserContext,
@@ -10,8 +8,6 @@ import {
} from "playwright";
import prexit from "prexit";
const STORAGE_PATH = path.join(process.cwd(), ".user-data", "session.json");
type PlaywrightContext = {
context: BrowserContext;
cleanUp: () => Promise<void>;
@@ -35,7 +31,6 @@ const useBrowser = (options: BrowserOptions) => {
...launchOptions,
});
context ??= await browser.newContext({
storageState: existsSync(STORAGE_PATH) ? STORAGE_PATH : undefined,
...contextOptions,
});
@@ -63,9 +58,6 @@ export const withPlaywright = async <T>(
})();
let isClosed = false;
const cleanUp = async () => {
await context.storageState({
path: STORAGE_PATH,
});
await Promise.all(context.pages().map((page) => page.close()));
await context.close();
await browser.close();

View File

@@ -35,7 +35,9 @@
"typecheck": "tsc --noEmit",
"test": "vitest --run",
"test:watch": "vitest",
"e2e": "./scripts/e2e.sh"
"e2e": "./scripts/e2e/exec_e2e.sh",
"e2e:start-server": "./scripts/e2e/start_server.sh",
"e2e:capture-snapshots": "./scripts/e2e/capture_snapshots.sh"
},
"dependencies": {
"@anthropic-ai/claude-code": "^1.0.98",

View File

@@ -0,0 +1,7 @@
#!/usr/bin/env bash
set -euo pipefail
export GLOBAL_CLAUDE_DIR=$(git rev-parse --show-toplevel)/mock-global-claude-dir
pnpx tsx ./e2e/captureSnapshot/index.ts

View File

@@ -2,12 +2,6 @@
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}
@@ -30,10 +24,10 @@ cleanup() {
trap cleanup EXIT INT TERM
pnpm start & SERVER_PID=$!
./scripts/e2e/start_server.sh & SERVER_PID=$!
echo "Server started. pid=$SERVER_PID"
sleep 5 # 即時起動するが、一応少し待っておく
pnpx tsx ./e2e/captureSnapshot/index.ts
./scripts/e2e/capture_snapshots.sh
echo "Completed capturing screenshots. Killing server..."

11
scripts/e2e/start_server.sh Executable file
View File

@@ -0,0 +1,11 @@
#!/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
node ./dist/index.js

View File

@@ -12,7 +12,7 @@ import { NewChat } from "./NewChat";
export const NewChatModal: FC<{
projectId: string;
trigger?: ReactNode;
trigger: ReactNode;
}> = ({ projectId, trigger }) => {
const [open, setOpen] = useState(false);
@@ -22,15 +22,11 @@ export const NewChatModal: FC<{
return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>
{trigger ?? (
<Button className="gap-2">
<PlusIcon className="w-4 h-4" />
New Chat
</Button>
)}
</DialogTrigger>
<DialogContent className="w-[95vw] md:w-[80vw]">
<DialogTrigger asChild>{trigger}</DialogTrigger>
<DialogContent
className="w-[95vw] md:w-[80vw]"
data-testid="start-new-chat-modal"
>
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<MessageSquareIcon className="w-5 h-5" />

View File

@@ -204,6 +204,7 @@ export const SessionPageContent: FC<{
<div
ref={scrollContainerRef}
className="flex-1 overflow-y-auto min-h-0 min-w-0"
data-testid="scrollable-content"
>
<main className="w-full px-4 sm:px-6 md:px-8 lg:px-12 xl:px-16 relative z-5 min-w-0">
<ConversationList

View File

@@ -98,6 +98,7 @@ export const MobileSidebar: FC<MobileSidebarProps> = ({
hasNextPage={hasNextPage}
isFetchingNextPage={isFetchingNextPage}
onLoadMore={() => fetchNextPage()}
isMobile={true}
/>
);
case "mcp":

View File

@@ -19,6 +19,7 @@ export const SessionsTab: FC<{
hasNextPage?: boolean;
isFetchingNextPage?: boolean;
onLoadMore?: () => void;
isMobile?: boolean;
}> = ({
sessions,
currentSessionId,
@@ -26,6 +27,7 @@ export const SessionsTab: FC<{
hasNextPage,
isFetchingNextPage,
onLoadMore,
isMobile = false,
}) => {
const sessionProcesses = useAtomValue(sessionProcessesAtom);
@@ -70,7 +72,16 @@ export const SessionsTab: FC<{
<NewChatModal
projectId={projectId}
trigger={
<Button size="sm" variant="outline" className="gap-1.5">
<Button
size="sm"
variant="outline"
className="gap-1.5"
data-testid={
isMobile === true
? "start-new-chat-button-mobile"
: "start-new-chat-button"
}
>
<PlusIcon className="w-3.5 h-3.5" />
New
</Button>

View File

@@ -52,12 +52,12 @@ export const CreateProjectDialog: FC = () => {
return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>
<Button>
<Button data-testid="new-project-button">
<Plus className="w-4 h-4 mr-2" />
New Project
</Button>
</DialogTrigger>
<DialogContent className="max-w-2xl">
<DialogContent className="max-w-2xl" data-testid="new-project-modal">
<DialogHeader>
<DialogTitle>Create New Project</DialogTitle>
<DialogDescription>