Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 48 additions & 0 deletions .coderabbit.yaml
Original file line number Diff line number Diff line change
@@ -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
12 changes: 10 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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"
}
}
90 changes: 90 additions & 0 deletions src/components/TodoCreate.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
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 (
<Box sx={{ maxWidth: 600, mx: 'auto', p: 3 }}>
<Typography variant="h5" component="h2" sx={{ mb: 3 }}>
新しいTodoを作成
</Typography>

{/* フォーム */}
<Box component="form" onSubmit={handleSubmit} sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
{/* エラーメッセージ表示 */}
{error && (
<Alert severity="error" sx={{ mb: 2 }}>
{error}
</Alert>
)}

{/* タイトル入力フィールド */}
<TextField
label="タイトル"
value={title}
onChange={(e) => {
setTitle(e.target.value);
if (error) setError('');
}}
placeholder="Todoのタイトルを入力"
fullWidth
error={!!error}
helperText={error ? ' ' : ''}
/>

{/* 送信ボタン */}
<Button type="submit" variant="contained" size="large" sx={{ mt: 1 }}>
作成
</Button>
</Box>
</Box>
);
};
107 changes: 107 additions & 0 deletions src/components/__tests__/TodoCreate.test.tsx
Original file line number Diff line number Diff line change
@@ -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(<TodoCreate onTodoCreate={mockOnTodoCreate} />);

// 入力フィールドを取得
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).toEqual(expect.any(String));
expect(createdTodo.createdAt).toBeInstanceOf(Date);
});

/**
* 空のタイトルで送信するとエラーが表示されること
*/
it('should show error when submitting with empty title', () => {
const mockOnTodoCreate = vi.fn();

render(<TodoCreate onTodoCreate={mockOnTodoCreate} />);

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(<TodoCreate onTodoCreate={mockOnTodoCreate} />);

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(<TodoCreate onTodoCreate={mockOnTodoCreate} />);

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('');
});
});
6 changes: 6 additions & 0 deletions src/test/setup.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import '@testing-library/jest-dom';

/**
* Vitestのセットアップファイル
* テストで@testing-library/jest-domのマッチャーを使用できるようにする
*/
21 changes: 21 additions & 0 deletions src/types/todo.ts
Original file line number Diff line number Diff line change
@@ -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;
}
22 changes: 22 additions & 0 deletions vitest.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { defineConfig } from 'vitest/config';
import react from '@vitejs/plugin-react';
import { fileURLToPath } from 'node:url';

/**
* Vitest設定
* Reactコンポーネントのテストを実行するための設定
*/
export default defineConfig({
plugins: [react()],
test: {
globals: true,
environment: 'jsdom',
setupFiles: ['./src/test/setup.ts'],
css: true,
},
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url)),
},
},
});