From 1d578763bbdd1bc927d4b5d955fab4ed26b36e71 Mon Sep 17 00:00:00 2001 From: gekko555 Date: Sun, 3 May 2026 19:01:19 +0900 Subject: [PATCH 1/2] Add Todo creation component with MUI - Create TypeScript type definitions for Todo - Implement TodoCreate component using MUI - Add tests for TodoCreate component - Configure Vitest for testing - Add test script to package.json --- package.json | 12 ++- src/components/TodoCreate.tsx | 87 +++++++++++++++ src/components/__tests__/TodoCreate.test.tsx | 107 +++++++++++++++++++ src/test/setup.ts | 6 ++ src/types/todo.ts | 21 ++++ vitest.config.ts | 22 ++++ 6 files changed, 253 insertions(+), 2 deletions(-) create mode 100644 src/components/TodoCreate.tsx create mode 100644 src/components/__tests__/TodoCreate.test.tsx create mode 100644 src/test/setup.ts create mode 100644 src/types/todo.ts create mode 100644 vitest.config.ts diff --git a/package.json b/package.json index 6b021b9..954ce75 100644 --- a/package.json +++ b/package.json @@ -7,14 +7,20 @@ "dev": "vite", "build": "tsc -b && vite build", "lint": "eslint .", - "preview": "vite preview" + "preview": "vite preview", + "test": "vitest" }, "dependencies": { + "@emotion/react": "^11.14.0", + "@emotion/styled": "^11.14.1", + "@mui/material": "^9.0.0", "react": "^19.2.5", "react-dom": "^19.2.5" }, "devDependencies": { "@eslint/js": "^10.0.1", + "@testing-library/jest-dom": "^6.9.1", + "@testing-library/react": "^16.3.2", "@types/node": "^24.12.2", "@types/react": "^19.2.14", "@types/react-dom": "^19.2.3", @@ -23,8 +29,10 @@ "eslint-plugin-react-hooks": "^7.1.1", "eslint-plugin-react-refresh": "^0.5.2", "globals": "^17.5.0", + "jsdom": "^29.1.1", "typescript": "~6.0.2", "typescript-eslint": "^8.58.2", - "vite": "^8.0.10" + "vite": "^8.0.10", + "vitest": "^4.1.5" } } diff --git a/src/components/TodoCreate.tsx b/src/components/TodoCreate.tsx new file mode 100644 index 0000000..0e5f9c0 --- /dev/null +++ b/src/components/TodoCreate.tsx @@ -0,0 +1,87 @@ +import { useState } from 'react'; +import { Box, TextField, Button, Typography, Alert } from '@mui/material'; +import type { Todo } from '../types/todo'; + +/** + * Todo作成コンポーネントのProps + */ +interface TodoCreateProps { + /** Todo作成時のコールバック関数 */ + onTodoCreate: (todo: Todo) => void; +} + +/** + * Todo作成コンポーネント + * + * ユーザーが新しいTodoを作成するためのフォームを提供する + */ +export const TodoCreate = ({ onTodoCreate }: TodoCreateProps) => { + // 入力フィールドの状態管理 + const [title, setTitle] = useState(''); + // エラーメッセージの状態管理 + const [error, setError] = useState(''); + + /** + * フォーム送信ハンドラー + * + * @param e - フォーム送信イベント + */ + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + + // バリデーション: タイトルが空の場合はエラー + if (!title.trim()) { + setError('タイトルを入力してください'); + return; + } + + // 新しいTodoオブジェクトを作成 + const newTodo: Todo = { + id: crypto.randomUUID(), // 一意なIDを生成 + title: title.trim(), + completed: false, + createdAt: new Date(), + }; + + // 親コンポーネントにTodo作成を通知 + onTodoCreate(newTodo); + + // フォームをリセット + setTitle(''); + setError(''); + }; + + return ( + + + 新しいTodoを作成 + + + {/* フォーム */} + + {/* エラーメッセージ表示 */} + {error && ( + + {error} + + )} + + {/* タイトル入力フィールド */} + setTitle(e.target.value)} + placeholder="Todoのタイトルを入力" + fullWidth + error={!!error} + helperText={error ? ' ' : ''} + /> + + {/* 送信ボタン */} + + + + ); +}; diff --git a/src/components/__tests__/TodoCreate.test.tsx b/src/components/__tests__/TodoCreate.test.tsx new file mode 100644 index 0000000..8f2888e --- /dev/null +++ b/src/components/__tests__/TodoCreate.test.tsx @@ -0,0 +1,107 @@ +import { describe, it, expect, vi } from 'vitest'; +import { render, screen, fireEvent } from '@testing-library/react'; +import { TodoCreate } from '../TodoCreate'; +import type { Todo } from '../../types/todo'; + +/** + * TodoCreateコンポーネントのテスト + */ +describe('TodoCreate', () => { + /** + * 正常にTodoを作成できること + */ + it('should create a todo when form is submitted with valid title', () => { + // モック関数を作成 + const mockOnTodoCreate = vi.fn(); + + // コンポーネントをレンダリング + render(); + + // 入力フィールドを取得 + const input = screen.getByLabelText(/タイトル/i); + // ボタンを取得 + const button = screen.getByRole('button', { name: /作成/i }); + + // タイトルを入力 + fireEvent.change(input, { target: { value: 'テストTodo' } }); + + // フォームを送信 + fireEvent.click(button); + + // onTodoCreateが呼ばれたことを確認 + expect(mockOnTodoCreate).toHaveBeenCalledTimes(1); + + // 作成されたTodoの内容を確認 + const createdTodo = mockOnTodoCreate.mock.calls[0][0] as Todo; + expect(createdTodo.title).toBe('テストTodo'); + expect(createdTodo.completed).toBe(false); + expect(createdTodo.id).toBeDefined(); + expect(createdTodo.createdAt).toBeDefined(); + }); + + /** + * 空のタイトルで送信するとエラーが表示されること + */ + it('should show error when submitting with empty title', () => { + const mockOnTodoCreate = vi.fn(); + + render(); + + const button = screen.getByRole('button', { name: /作成/i }); + + // 空のタイトルで送信 + fireEvent.click(button); + + // onTodoCreateが呼ばれないことを確認 + expect(mockOnTodoCreate).not.toHaveBeenCalled(); + + // エラーメッセージが表示されることを確認 + expect(screen.getByText(/タイトルを入力してください/i)).toBeInTheDocument(); + }); + + /** + * 空白のみのタイトルで送信するとエラーが表示されること + */ + it('should show error when submitting with whitespace-only title', () => { + const mockOnTodoCreate = vi.fn(); + + render(); + + const input = screen.getByLabelText(/タイトル/i); + const button = screen.getByRole('button', { name: /作成/i }); + + // 空白のみを入力 + fireEvent.change(input, { target: { value: ' ' } }); + + // 送信 + fireEvent.click(button); + + // onTodoCreateが呼ばれないことを確認 + expect(mockOnTodoCreate).not.toHaveBeenCalled(); + + // エラーメッセージが表示されることを確認 + expect(screen.getByText(/タイトルを入力してください/i)).toBeInTheDocument(); + }); + + /** + * 送信後に入力フィールドがクリアされること + */ + it('should clear input field after successful submission', () => { + const mockOnTodoCreate = vi.fn(); + + render(); + + const input = screen.getByLabelText(/タイトル/i) as HTMLInputElement; + const button = screen.getByRole('button', { name: /作成/i }); + + // タイトルを入力 + fireEvent.change(input, { target: { value: 'テストTodo' } }); + expect(input.value).toBe('テストTodo'); + + // 送信 + fireEvent.click(button); + + // 入力フィールドがクリアされたことを確認 + expect(input.value).toBe(''); + }); +}); diff --git a/src/test/setup.ts b/src/test/setup.ts new file mode 100644 index 0000000..21b5f01 --- /dev/null +++ b/src/test/setup.ts @@ -0,0 +1,6 @@ +import '@testing-library/jest-dom'; + +/** + * Vitestのセットアップファイル + * テストで@testing-library/jest-domのマッチャーを使用できるようにする + */ diff --git a/src/types/todo.ts b/src/types/todo.ts new file mode 100644 index 0000000..699a472 --- /dev/null +++ b/src/types/todo.ts @@ -0,0 +1,21 @@ +/** + * Todoアイテムの型定義 + */ +export interface Todo { + /** Todoの一意識別子 */ + id: string; + /** Todoのタイトル */ + title: string; + /** Todoの完了状態 */ + completed: boolean; + /** 作成日時 */ + createdAt: Date; +} + +/** + * 新規Todo作成時の入力データ型 + */ +export interface CreateTodoInput { + /** Todoのタイトル */ + title: string; +} diff --git a/vitest.config.ts b/vitest.config.ts new file mode 100644 index 0000000..f1d2bf2 --- /dev/null +++ b/vitest.config.ts @@ -0,0 +1,22 @@ +import { defineConfig } from 'vitest/config'; +import react from '@vitejs/plugin-react'; +import path from 'path'; + +/** + * Vitest設定 + * Reactコンポーネントのテストを実行するための設定 + */ +export default defineConfig({ + plugins: [react()], + test: { + globals: true, + environment: 'jsdom', + setupFiles: ['./src/test/setup.ts'], + css: true, + }, + resolve: { + alias: { + '@': path.resolve(__dirname, './src'), + }, + }, +}); From 95e0c05be8d710a5f1ee861caedee3b23507269d Mon Sep 17 00:00:00 2001 From: gekko555 Date: Tue, 5 May 2026 01:17:50 +0900 Subject: [PATCH 2/2] Address CodeRabbit feedback --- .coderabbit.yaml | 48 ++++++++++++++++++++ src/components/TodoCreate.tsx | 5 +- src/components/__tests__/TodoCreate.test.tsx | 4 +- vitest.config.ts | 4 +- 4 files changed, 56 insertions(+), 5 deletions(-) create mode 100644 .coderabbit.yaml diff --git a/.coderabbit.yaml b/.coderabbit.yaml new file mode 100644 index 0000000..b91aa23 --- /dev/null +++ b/.coderabbit.yaml @@ -0,0 +1,48 @@ +# yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json +language: "ja-JP" +tone_instructions: "日本語で、初学者にも伝わるやさしい口調。指摘は一般人向けの説明とエンジニア向けの説明を分けて、理由と修正方針を簡潔に示してください。" +early_access: false + +reviews: + profile: "chill" + request_changes_workflow: false + high_level_summary: true + review_status: true + review_details: true + collapse_walkthrough: false + auto_review: + enabled: true + auto_incremental_review: true + auto_pause_after_reviewed_commits: 5 + drafts: true + base_branches: + - ".*" + ignore_title_keywords: + - "WIP" + - "[skip review]" + ignore_usernames: + - "dependabot[bot]" + - "renovate[bot]" + - "github-actions[bot]" + path_instructions: + - path: "src/**/*.{ts,tsx}" + instructions: | + React / TypeScript 初学者の学習用リポジトリとしてレビューしてください。 + + コメントを書くときは、可能な限り次の2段構成にしてください。 + - 一般人向けの説明: 何が困るのか、何のために直すのかを専門用語少なめで説明する。 + - エンジニア向けの説明: React、TypeScript、状態管理、イベント、props、テスト観点から具体的に説明する。 + + 重大な不具合、型安全性、Reactの基本パターン、アクセシビリティ、テスト不足を優先して指摘してください。 + 学習目的の小さなPRでは、過度な抽象化や大規模リファクタは強く求めず、次の一歩がわかる提案にしてください。 + - path: "src/components/__tests__/**/*.{ts,tsx}" + instructions: | + テストコードは、ユーザー操作に近い観点で読めるかを重視してください。 + 実装詳細に依存しすぎるテストより、画面上の入力、クリック、表示結果、親コンポーネントへの通知を検証する方針を優先してください。 + - path: "*.{config.ts,config.js}" + instructions: | + Vite / Vitest 設定では、このリポジトリが package.json の "type": "module" を使う前提で確認してください。 + Node.js の ESM 環境で壊れやすい __dirname や CommonJS 前提の書き方があれば、import.meta.url を使った安全な書き方を提案してください。 + +chat: + auto_reply: true diff --git a/src/components/TodoCreate.tsx b/src/components/TodoCreate.tsx index 0e5f9c0..e6ac65f 100644 --- a/src/components/TodoCreate.tsx +++ b/src/components/TodoCreate.tsx @@ -70,7 +70,10 @@ export const TodoCreate = ({ onTodoCreate }: TodoCreateProps) => { setTitle(e.target.value)} + onChange={(e) => { + setTitle(e.target.value); + if (error) setError(''); + }} placeholder="Todoのタイトルを入力" fullWidth error={!!error} diff --git a/src/components/__tests__/TodoCreate.test.tsx b/src/components/__tests__/TodoCreate.test.tsx index 8f2888e..55d77bc 100644 --- a/src/components/__tests__/TodoCreate.test.tsx +++ b/src/components/__tests__/TodoCreate.test.tsx @@ -35,8 +35,8 @@ describe('TodoCreate', () => { const createdTodo = mockOnTodoCreate.mock.calls[0][0] as Todo; expect(createdTodo.title).toBe('テストTodo'); expect(createdTodo.completed).toBe(false); - expect(createdTodo.id).toBeDefined(); - expect(createdTodo.createdAt).toBeDefined(); + expect(createdTodo.id).toEqual(expect.any(String)); + expect(createdTodo.createdAt).toBeInstanceOf(Date); }); /** diff --git a/vitest.config.ts b/vitest.config.ts index f1d2bf2..69ce8ac 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -1,6 +1,6 @@ import { defineConfig } from 'vitest/config'; import react from '@vitejs/plugin-react'; -import path from 'path'; +import { fileURLToPath } from 'node:url'; /** * Vitest設定 @@ -16,7 +16,7 @@ export default defineConfig({ }, resolve: { alias: { - '@': path.resolve(__dirname, './src'), + '@': fileURLToPath(new URL('./src', import.meta.url)), }, }, });