diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 0000000..6c3c2eb
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,42 @@
+name: CI
+
+on:
+ push:
+ branches: [ main ]
+ pull_request:
+ branches: [ main ]
+
+jobs:
+ backend-test:
+ runs-on: ubuntu-latest
+ defaults:
+ run:
+ working-directory: ./backend
+ steps:
+ - uses: actions/checkout@v4
+ - name: Set up Python
+ uses: actions/setup-python@v5
+ with:
+ python-version: '3.10'
+ - name: Install dependencies
+ run: pip install -r requirements.txt
+ - name: Run tests
+ env:
+ GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }}
+ run: pytest
+
+ frontend-test:
+ runs-on: ubuntu-latest
+ defaults:
+ run:
+ working-directory: ./frontend
+ steps:
+ - uses: actions/checkout@v4
+ - name: Set up Node.js
+ uses: actions/setup-node@v4
+ with:
+ node-version: '20'
+ - name: Install dependencies
+ run: npm install
+ - name: Run tests
+ run: npm test
diff --git a/.gitignore b/.gitignore
index b76a78b..1d1dd5b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,6 +1,7 @@
# Python
backend/venv/
-backend/__pycache__/
+__pycache__/
+.pytest_cache/
*.pyc
# Environment variables
diff --git a/README.md b/README.md
index c346ec9..5fbb4d7 100644
--- a/README.md
+++ b/README.md
@@ -37,7 +37,7 @@
- 目的フェーズ間のコンテキスト引継ぎの補助となるjsonサマリー出力
- チャット履歴の管理
- Markdownおよびコードブロックの整形表示
-
+---
## 技術スタック
### バックエンド
@@ -54,7 +54,7 @@
- **UIライブラリ**: shadcn/ui
- **スタイリング**: Tailwind CSS
- **Markdownレンダリング**: react-markdown, remark-gfm, rehype-highlight
-
+---
## セットアップ手順
### 前提条件
@@ -72,7 +72,7 @@ cd castor
- プロジェクトルート (`castor/`) に `.env` ファイルを作成し、Google AI Studioから取得したAPIキーを設定します。
- APIを無料枠で使用する場合、2.5-flashモデルの仕様を推奨します。
```
-GOOGLE_API_KEY="YOUR_GOOGLE_API_KEY_HERE"
+GEMINI_API_KEY="YOUR_API_KEY_HERE"
```
### 3. バックエンドのセットアップと起動
@@ -119,17 +119,28 @@ npm run dev
このスクリプトは、新しいウィンドウでバックエンドとフロントエンドをそれぞれ起動します。
+---
+## License
+
+This project is licensed under the Apache License, Version 2.0. See the [LICENSE](LICENSE) file for details.
+
+---
+
## 現在の課題
- 要件定義は機能・非機能で行ったが、実装中の気付きに合わせた変更を言語化すべきだった。
-- 業務的なチケット化・スコープ管理を意識したが、個人開発では難しい部分が多かった。
-- PoC的なポートフォリオのため更新が多く、バージョニング/リリース管理の必要性は検討中。
+- PoC的なポートフォリオのため細かい更新が多く、バージョニング管理の必要性は検討中。
+- コードアシストを受けた部分と手作業・目視でのフロー確認についての言及。
- READMEへのアーキテクチャ図の追加。
-- コードアシストを受けた部分と手作業・目視でのフロー確認についての言及。コード内への記載。
-- 現在テストは最小限で、開発ではホットリロード+ログデバッグを使用。
-- テストコードでの実施が課題。自動化も含め勉強の必要性を感じている。
+- ~~業務的なチケット化・スコープ管理を意識したが、個人開発では難しい部分が多かった。~~
+→セルフでIssueを立てるようにした。
+- ~~テストコードでの実施が課題。自動化も含め勉強の必要性を感じている。~~
+→Pytest,Vitestの導入とGithub Actionsを試している
-## 履歴
+---
+## 開発履歴
+
+(クリックしてください)
**2025-11-06**:
- プロジェクトの初期セットアップとディレクトリ構成の整理。
@@ -151,7 +162,7 @@ npm run dev
- フロントエンドにMarkdownレンダリングを追加。
- READMEを更新し、プロジェクトの目的、設計原則、セットアップ手順、技術スタックを明確化。
- READMEに動作イメージのスクリーンショットを追加。
--
+
**2025-11-20**:
- 履歴削除機能とUI改善、ダークテーマ/トグルを追加。
- 軽微なバグ修正を実施。
@@ -162,6 +173,11 @@ npm run dev
- `README.md`内の画像パスをローカルの`images`ディレクトリを参照するように更新。
- ウェルカム画面にフェーズ説明を追加。
-## License
+**2025-11-26**:
+- ライセンスの追加。
+- サイドバーのコンポーネントの最適化、メモパッド機能を追加。
-This project is licensed under the Apache License, Version 2.0. See the [LICENSE](LICENSE) file for details.
+**2025-11-27**:
+- テストコードの導入
+- GitHub ActionsのCIワークフローを追加
+
diff --git a/backend/.env.example b/backend/.env.example
new file mode 100644
index 0000000..d472d19
--- /dev/null
+++ b/backend/.env.example
@@ -0,0 +1,5 @@
+# Google AI Studio (https://aistudio.google.com/app/apikey) から取得したAPIキーを
+# 以下の"YOUR_API_KEY_HERE"を実際のAPIキーに置き換えてください。
+
+# GEMINI_API_KEY="YOUR_API_KEY_HERE"
+# ファイル名を.envに変更して使用してください。
diff --git a/backend/__init__.py b/backend/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/backend/app/main.py b/backend/app/main.py
index 8f2e276..aa93601 100644
--- a/backend/app/main.py
+++ b/backend/app/main.py
@@ -3,6 +3,7 @@
import traceback
from fastapi import FastAPI, HTTPException
from fastapi.middleware.cors import CORSMiddleware
+from contextlib import asynccontextmanager
from .schemas import ChatRequest, HistoryRequest, CreateProjectRequest, NewSessionRequest
from .chat_logic import (
init_chat_on_startup,
@@ -17,10 +18,16 @@
delete_project, delete_history
)
+# --- Lifespan pytestに推奨された ---
+@asynccontextmanager
+async def lifespan(app: FastAPI):
+ await init_chat_on_startup()
+ yield
# --- FastAPI ---
app = FastAPI()
+
# --- CORS(React/Vite) ---
origins = [
"http://localhost:5173", # Vite default port
@@ -35,10 +42,6 @@
)
-# --- Init and Session Persistence ---
-init_chat_on_startup()
-
-
# --- Endpoints ---
@app.get("/")
def root():
diff --git a/backend/requirements.txt b/backend/requirements.txt
index 3cf6302..2457b2c 100644
Binary files a/backend/requirements.txt and b/backend/requirements.txt differ
diff --git a/backend/tests/test_chat.py b/backend/tests/test_chat.py
new file mode 100644
index 0000000..a3f2896
--- /dev/null
+++ b/backend/tests/test_chat.py
@@ -0,0 +1,90 @@
+import pytest
+from fastapi.testclient import TestClient
+from app.main import app
+
+# TestClientインスタンスを作成
+client = TestClient(app)
+
+# 正常系はGeminiが生成し、異常系は自分で書いてみた
+
+# `send_chat_message` SDKでAPI呼び出しを行う
+# `save_message` ユーザー入力とAI応答を保存する
+CHAT_LOGIC_PATH = "app.main.send_chat_message"
+STORAGE_LOGIC_PATH = "app.main.save_message"
+
+@pytest.mark.asyncio
+async def test_chat_endpoint_success(mocker):
+ """
+ /chat エンドポイントの正常系テスト
+ - 外部関数をモック化する
+ - 正常なリクエストを送信し、200 OKが返ることを確認する
+ - モック化した関数が正しく呼び出されることを確認する
+ """
+ # --- モックの設定 ---
+ # send_chat_message をモック化し、固定の応答を返すように設定
+ mock_send_chat = mocker.patch(CHAT_LOGIC_PATH, return_value="AIの応答メッセージ")
+
+ # save_message をモック化
+ mock_save_message = mocker.patch(STORAGE_LOGIC_PATH)
+
+ # --- テストデータ ---
+ test_request_body = {
+ "message": "テストメッセージ",
+ "project": "test_project",
+ "phase": "test_phase",
+ "session_id": "test_session_123"
+ }
+
+ # --- リクエストの実行 ---
+ # /chat エンドポイントにPOSTリクエストを送信
+ # TestClientは非同期エンドポイントも同期的に呼び出せる
+ response = client.post("/chat", json=test_request_body)
+
+ # --- 検証 ---
+ # ステータスコードが200 OKであることを確認
+ assert response.status_code == 200
+
+ # レスポンスボディが期待通りであることを確認
+ assert response.json() == {"response": "AIの応答メッセージ"}
+
+ # --- モックの呼び出し検証 ---
+ # send_chat_message が1回、指定されたメッセージで呼び出されたことを確認
+ mock_send_chat.assert_called_once_with(message="テストメッセージ")
+
+ # save_message が2回呼び出されたことを確認(ユーザーメッセージとAI応答)
+ assert mock_save_message.call_count == 2
+
+ # 1回目の呼び出し(ユーザーメッセージ)の引数を確認
+ mock_save_message.call_args_list[0].assert_called_with(
+ "test_project", "test_phase", "test_session_123", "user", "テストメッセージ"
+ )
+ # 2回目の呼び出し(AI応答)の引数を確認
+ mock_save_message.call_args_list[1].assert_called_with(
+ "test_project", "test_phase", "test_session_123", "model", "AIの応答メッセージ"
+ )
+
+
+@pytest.mark.asyncio
+async def test_chat_endpoint_invalid_body(mocker):
+ """
+ /chat エンドポイントの異常系テスト
+ - 不正なリクエストボディ
+ - messageフィールドが欠落している場合、400 Bad Requestが返ることを確認する
+ """
+ # --- テストデータ ---
+ invalid_request_body = {
+ "project": "test_project",
+ "phase": "test_phase",
+ "session_id": "test_session_123"
+ # "message" フィールドが欠落
+ }
+
+ # --- リクエストの実行 ---
+ response = client.post("/chat", json=invalid_request_body)
+
+ # --- 検証 ---
+ # ステータスコードが400 Bad Requestであることを確認
+ assert response.status_code == 422 # FastAPIはバリデーションエラーで422を返す
+
+ # エラーメッセージに 'message' フィールドの欠落が含まれていることを確認
+ assert "message" in response.json()["detail"][0]["loc"]
\ No newline at end of file
diff --git a/backend/tests/test_history_crud.py b/backend/tests/test_history_crud.py
new file mode 100644
index 0000000..d870300
--- /dev/null
+++ b/backend/tests/test_history_crud.py
@@ -0,0 +1,129 @@
+# Generated by Gemini CLI / テストコードを生成させてみた
+import pytest
+from fastapi.testclient import TestClient
+from app.main import app
+
+# TestClientインスタンスを作成
+client = TestClient(app)
+
+# `get_histories_list` 関数のパス
+STORAGE_LOGIC_GET_HISTORIES_PATH = "app.main.get_histories_list"
+
+def test_get_histories_list_success(mocker):
+ """
+ GET /projects/{project}/histories エンドポイントの正常系テスト
+ - storage_logic.get_histories_list をモック化する
+ - 指定したプロジェクトの履歴一覧が正しく返されることを確認する
+ """
+ # --- モックの設定 ---
+ # get_histories_list をモック化し、ダミーの履歴リストを返すように設定
+ dummy_histories = [
+ {"id": "session1", "phase": "recon"},
+ {"id": "session2", "phase": "exploitation"}
+ ]
+ mock_get_histories = mocker.patch(
+ STORAGE_LOGIC_GET_HISTORIES_PATH, return_value=dummy_histories
+ )
+
+ # --- テストデータ ---
+ project_name = "test_project_for_histories"
+
+ # --- リクエストの実行 ---
+ response = client.get(f"/projects/{project_name}/histories")
+
+ # --- 検証 ---
+ # ステータスコードが200 OKであることを確認
+ assert response.status_code == 200
+
+ # レスポンスボディが期待通りであることを確認
+ assert response.json() == {"histories": dummy_histories}
+
+ # --- モックの呼び出し検証 ---
+ # get_histories_list が正しいプロジェクト名で1回呼び出されたことを確認
+ mock_get_histories.assert_called_once_with(project_name)
+
+# `load_history` 関数のパス
+STORAGE_LOGIC_LOAD_HISTORY_PATH = "app.main.load_history"
+
+
+def test_load_history_success(mocker):
+ """
+ POST /load_history エンドポイントの正常系テスト
+ - storage_logic.load_history をモック化する
+ - 指定した履歴データが正しく返されることを確認する
+ """
+ # --- モックの設定 ---
+ # load_history をモック化し、ダミーの履歴データを返すように設定
+ dummy_history_data = {
+ "messages": [
+ {"role": "user", "content": "最初のメッセージ"},
+ {"role": "model", "content": "最初の応答"}
+ ],
+ "project": "test_project_load",
+ "phase": "test_phase_load",
+ "session_id": "test_session_load_123"
+ }
+ mock_load_history = mocker.patch(
+ STORAGE_LOGIC_LOAD_HISTORY_PATH, return_value=dummy_history_data
+ )
+
+ # --- テストデータ ---
+ request_body = {
+ "project": "test_project_load",
+ "phase": "test_phase_load",
+ "session_id": "test_session_load_123"
+ }
+
+ # --- リクエストの実行 ---
+ response = client.post("/load_history", json=request_body)
+
+ # --- 検証 ---
+ # ステータスコードが200 OKであることを確認
+ assert response.status_code == 200
+
+ # レスポンスボディが期待通りであることを確認
+ assert response.json() == dummy_history_data
+
+ # --- モックの呼び出し検証 ---
+ # load_history が正しい引数で1回呼び出されたことを確認
+ mock_load_history.assert_called_once_with(
+ request_body["project"], request_body["phase"], request_body["session_id"]
+ )
+
+# `delete_history` 関数のパス
+STORAGE_LOGIC_DELETE_HISTORY_PATH = "app.main.delete_history"
+
+
+def test_delete_history_success(mocker):
+ """
+ DELETE /projects/{project}/histories/{phase}/{session_id} エンドポイントの正常系テスト
+ - storage_logic.delete_history をモック化する
+ - 指定した履歴が正常に削除されることを確認する
+ """
+ # --- モックの設定 ---
+ # delete_history をモック化し、Trueを返すように設定
+ mock_delete_history = mocker.patch(
+ STORAGE_LOGIC_DELETE_HISTORY_PATH, return_value=True
+ )
+
+ # --- テストデータ ---
+ project_name = "test_project_delete"
+ phase_name = "test_phase_delete"
+ session_id = "test_session_delete_456"
+
+ # --- リクエストの実行 ---
+ # f-stringを使ってURLにパスパラメータを埋め込む
+ response = client.delete(f"/projects/{project_name}/histories/{phase_name}/{session_id}")
+
+ # --- 検証 ---
+ # ステータスコードが200 OKであることを確認
+ assert response.status_code == 200
+
+ # レスポンスボディが期待通りであることを確認
+ assert response.json() == {"deleted": True, "file": f"{phase_name}_{session_id}.json"}
+
+ # --- モックの呼び出し検証 ---
+ # delete_history が正しい引数で1回呼び出されたことを確認
+ mock_delete_history.assert_called_once_with(
+ project_name, phase_name, session_id
+ )
diff --git a/backend/tests/test_project_crud.py b/backend/tests/test_project_crud.py
new file mode 100644
index 0000000..a5cd0d4
--- /dev/null
+++ b/backend/tests/test_project_crud.py
@@ -0,0 +1,75 @@
+# 自分で書いたテストコード
+import pytest
+from fastapi.testclient import TestClient
+from app.main import app
+
+# TestClientインスタンスを作成
+client = TestClient(app)
+
+STORAGE_LOGIC_CREATE_PROJECT_PATH = "app.main.create_project"
+STORAGE_LOGIC_GET_PROJECT_PATH = "app.main.get_projects_list"
+STORAGE_LOGIC_DELETE_PROJECT_PATH = "app.main.delete_project"
+
+def test_create_project_success(mocker):
+ """
+ /create_project エンドポイントの正常系テスト
+ - storage_logic.create_project をモック化する
+ """
+ # --- モックの設定 ---
+ # create_project をモック化し、Trueを返すように設定
+ mock_create_project = mocker.patch(
+ STORAGE_LOGIC_CREATE_PROJECT_PATH, return_value=True
+ )
+
+ # --- テストデータ ---
+ test_request_body = {"project": "new_test_project"}
+
+ # --- リクエストの実行 ---
+ response = client.post("/create_project", json=test_request_body)
+
+ # --- 検証 ---
+ # ステータスコードが200 OKであることを確認
+ assert response.status_code == 200
+
+ # レスポンスボディが期待通りであることを確認
+ assert response.json() == {"created": True, "project": "new_test_project"}
+
+ # --- モックの呼び出し検証 ---
+ # create_project が1回、指定されたプロジェクト名で呼び出されたことを確認
+ mock_create_project.assert_called_once_with("new_test_project")
+
+
+def test_get_projects_list_success(mocker):
+ """
+ /projects エンドポイントの正常系テスト
+ - storage_logic.get_projects_list をモック化する
+ """
+
+ mock_get_projects_list = mocker.patch(
+ STORAGE_LOGIC_GET_PROJECT_PATH, return_value=["project1", "project2"]
+ )
+
+ response = client.get("/projects")
+ assert response.status_code == 200
+ assert response.json() == {"projects": ["project1", "project2"]}
+
+ mock_get_projects_list.assert_called_once()
+
+
+def test_delete_project_success(mocker):
+ """
+ /projects/{project} エンドポイントの正常系テスト (DELETE)
+ - storage_logic.delete_project をモック化する
+ """
+
+ mock_delete_project = mocker.patch(
+ STORAGE_LOGIC_DELETE_PROJECT_PATH, return_value=True
+ )
+
+ project_to_delete = "delete_test_project"
+
+ response = client.delete(f"/projects/{project_to_delete}")
+ assert response.status_code == 200
+ assert response.json() == {"deleted": True, "project": project_to_delete}
+
+ mock_delete_project.assert_called_once_with(project_to_delete)
\ No newline at end of file
diff --git a/backend/tests/test_root.py b/backend/tests/test_root.py
new file mode 100644
index 0000000..8c8f9fe
--- /dev/null
+++ b/backend/tests/test_root.py
@@ -0,0 +1,21 @@
+# 様々を参考にしてテストコードを自分で書いてみた。
+from fastapi.testclient import TestClient
+from app.main import app
+
+client = TestClient(app)
+
+def test_read_root():
+ """
+ ルートエンドポイント ("/") のテスト
+ - ステータスコードが200であること
+ - レスポンスボディが期待通りであること
+ を確認する
+ """
+ # ルートエンドポイントにGETリクエストを送信
+ response = client.get("/")
+
+ # ステータスコードが200 OKであることを確認
+ assert response.status_code == 200
+
+ # レスポンスのJSONボディが期待通りであることを確認
+ assert response.json() == {"message": "Castor Backend is running."}
diff --git a/backend/tests/test_session.py b/backend/tests/test_session.py
new file mode 100644
index 0000000..b29e83a
--- /dev/null
+++ b/backend/tests/test_session.py
@@ -0,0 +1,138 @@
+# Generated by Gemini CLI / テストコードを生成させてみた
+import pytest
+from fastapi.testclient import TestClient
+from app.main import app
+
+# TestClientインスタンスを作成
+client = TestClient(app)
+
+# `create_or_resume_session` 関数のパス
+CHAT_LOGIC_SESSION_PATH = "app.main.create_or_resume_session"
+
+@pytest.mark.asyncio
+async def test_new_session_success(mocker):
+ """
+ POST /new_session エンドポイントの正常系テスト
+ - chat_logic.create_or_resume_session をモック化する
+ - 新しいセッションが正常に初期化されることを確認する
+ """
+ # --- モックの設定 ---
+ # create_or_resume_session をモック化
+ mock_create_session = mocker.patch(CHAT_LOGIC_SESSION_PATH)
+
+ # --- テストデータ ---
+ request_body = {
+ "project": "new_session_project",
+ "phase": "new_session_phase"
+ }
+
+ # --- リクエストの実行 ---
+ # TestClientは非同期エンドポイントも同期的に呼び出せる
+ response = client.post("/new_session", json=request_body)
+
+ # --- 検証 ---
+ # ステータスコードが200 OKであることを確認
+ assert response.status_code == 200
+
+ # レスポンスボディが期待通りであることを確認
+ assert response.json() == {
+ "message": "New chat session initialized successfully.",
+ "project": "new_session_project",
+ "phase": "new_session_phase"
+ }
+
+ # --- モックの呼び出し検証 ---
+ # create_or_resume_session が正しい引数で1回呼び出されたことを確認
+ # 非同期関数なので assert_awaited_once_with を使う
+ mock_create_session.assert_awaited_once_with(phase="new_session_phase", history=None)
+
+# `load_history` 関数のパス (test_history_crud.py にも定義済みだが、重複しないようここに定義)
+STORAGE_LOGIC_LOAD_HISTORY_PATH = "app.main.load_history"
+
+@pytest.mark.asyncio
+async def test_resume_session_success(mocker):
+ """
+ POST /resume_session エンドポイントの正常系テスト
+ - load_history と create_or_resume_session をモック化する
+ - セッションが正常に再開されることを確認する
+ """
+ # --- モックの設定 ---
+ # load_history がダミーの履歴データを返すように設定
+ dummy_history_data = {
+ "messages": [
+ {"role": "user", "content": "履歴からのメッセージ"},
+ {"role": "model", "content": "履歴からの応答"}
+ ],
+ "phase": "resumed_phase",
+ "project": "resumed_project",
+ "session_id": "resumed_session_123"
+ }
+ mock_load_history = mocker.patch(
+ STORAGE_LOGIC_LOAD_HISTORY_PATH, return_value=dummy_history_data
+ )
+
+ # create_or_resume_session をモック化
+ mock_create_session = mocker.patch(CHAT_LOGIC_SESSION_PATH)
+
+ # --- テストデータ ---
+ request_body = {
+ "project": "resumed_project",
+ "phase": "resumed_phase",
+ "session_id": "resumed_session_123"
+ }
+
+ # --- リクエストの実行 ---
+ response = client.post("/resume_session", json=request_body)
+
+ # --- 検証 ---
+ assert response.status_code == 200
+ assert response.json() == {"status": "resumed", "message_count": 2}
+
+ mock_load_history.assert_called_once_with(
+ request_body["project"], request_body["phase"], request_body["session_id"]
+ )
+ mock_create_session.assert_awaited_once_with(
+ phase=dummy_history_data["phase"], history=dummy_history_data["messages"]
+ )
+
+
+@pytest.mark.asyncio
+async def test_resume_session_no_history(mocker):
+ """
+ POST /resume_session エンドポイントの異常系テスト(履歴なし)
+ - load_history が空の履歴を返す場合に、セッション再構築が呼ばれないことを確認する
+ """
+ # --- モックの設定 ---
+ # load_history が空の履歴データを返すように設定
+ dummy_empty_history_data = {
+ "messages": [], # メッセージが空
+ "phase": "empty_phase",
+ "project": "empty_project",
+ "session_id": "empty_session_123"
+ }
+ mock_load_history = mocker.patch(
+ STORAGE_LOGIC_LOAD_HISTORY_PATH, return_value=dummy_empty_history_data
+ )
+
+ # create_or_resume_session をモック化
+ mock_create_session = mocker.patch(CHAT_LOGIC_SESSION_PATH)
+
+ # --- テストデータ ---
+ request_body = {
+ "project": "empty_project",
+ "phase": "empty_phase",
+ "session_id": "empty_session_123"
+ }
+
+ # --- リクエストの実行 ---
+ response = client.post("/resume_session", json=request_body)
+
+ # --- 検証 ---
+ assert response.status_code == 200
+ assert response.json() == {"status": "no_history", "message": "履歴が存在しません"}
+
+ mock_load_history.assert_called_once_with(
+ request_body["project"], request_body["phase"], request_body["session_id"]
+ )
+ # 履歴がないので create_or_resume_session は呼ばれないことを確認
+ mock_create_session.assert_not_awaited()
\ No newline at end of file
diff --git a/frontend/package-lock.json b/frontend/package-lock.json
index 94218c7..178bbd3 100644
--- a/frontend/package-lock.json
+++ b/frontend/package-lock.json
@@ -35,6 +35,8 @@
},
"devDependencies": {
"@eslint/js": "^9.36.0",
+ "@testing-library/jest-dom": "^6.9.1",
+ "@testing-library/react": "^16.3.0",
"@types/node": "^24.6.0",
"@types/react": "^19.1.16",
"@types/react-dom": "^19.1.9",
@@ -44,12 +46,83 @@
"eslint-plugin-react-hooks": "^5.2.0",
"eslint-plugin-react-refresh": "^0.4.22",
"globals": "^16.4.0",
+ "jsdom": "^27.2.0",
"tw-animate-css": "^1.4.0",
"typescript": "~5.9.3",
"typescript-eslint": "^8.45.0",
- "vite": "^7.1.7"
+ "vite": "^7.1.7",
+ "vitest": "^4.0.14"
}
},
+ "node_modules/@acemir/cssom": {
+ "version": "0.9.24",
+ "resolved": "https://registry.npmjs.org/@acemir/cssom/-/cssom-0.9.24.tgz",
+ "integrity": "sha512-5YjgMmAiT2rjJZU7XK1SNI7iqTy92DpaYVgG6x63FxkJ11UpYfLndHJATtinWJClAXiOlW9XWaUyAQf8pMrQPg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@adobe/css-tools": {
+ "version": "4.4.4",
+ "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.4.4.tgz",
+ "integrity": "sha512-Elp+iwUx5rN5+Y8xLt5/GRoG20WGoDCQ/1Fb+1LiGtvwbDavuSk0jhD/eZdckHAuzcDzccnkv+rEjyWfRx18gg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@asamuzakjp/css-color": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-4.1.0.tgz",
+ "integrity": "sha512-9xiBAtLn4aNsa4mDnpovJvBn72tNEIACyvlqaNJ+ADemR+yeMJWnBudOi2qGDviJa7SwcDOU/TRh5dnET7qk0w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@csstools/css-calc": "^2.1.4",
+ "@csstools/css-color-parser": "^3.1.0",
+ "@csstools/css-parser-algorithms": "^3.0.5",
+ "@csstools/css-tokenizer": "^3.0.4",
+ "lru-cache": "^11.2.2"
+ }
+ },
+ "node_modules/@asamuzakjp/css-color/node_modules/lru-cache": {
+ "version": "11.2.2",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.2.tgz",
+ "integrity": "sha512-F9ODfyqML2coTIsQpSkRHnLSZMtkU8Q+mSfcaIyKwy58u+8k5nvAYeiNhsyMARvzNcXJ9QfWVrcPsC9e9rAxtg==",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": "20 || >=22"
+ }
+ },
+ "node_modules/@asamuzakjp/dom-selector": {
+ "version": "6.7.4",
+ "resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-6.7.4.tgz",
+ "integrity": "sha512-buQDjkm+wDPXd6c13534URWZqbz0RP5PAhXZ+LIoa5LgwInT9HVJvGIJivg75vi8I13CxDGdTnz+aY5YUJlIAA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@asamuzakjp/nwsapi": "^2.3.9",
+ "bidi-js": "^1.0.3",
+ "css-tree": "^3.1.0",
+ "is-potential-custom-element-name": "^1.0.1",
+ "lru-cache": "^11.2.2"
+ }
+ },
+ "node_modules/@asamuzakjp/dom-selector/node_modules/lru-cache": {
+ "version": "11.2.2",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.2.tgz",
+ "integrity": "sha512-F9ODfyqML2coTIsQpSkRHnLSZMtkU8Q+mSfcaIyKwy58u+8k5nvAYeiNhsyMARvzNcXJ9QfWVrcPsC9e9rAxtg==",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": "20 || >=22"
+ }
+ },
+ "node_modules/@asamuzakjp/nwsapi": {
+ "version": "2.3.9",
+ "resolved": "https://registry.npmjs.org/@asamuzakjp/nwsapi/-/nwsapi-2.3.9.tgz",
+ "integrity": "sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/@babel/code-frame": {
"version": "7.27.1",
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz",
@@ -284,6 +357,16 @@
"@babel/core": "^7.0.0-0"
}
},
+ "node_modules/@babel/runtime": {
+ "version": "7.28.4",
+ "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz",
+ "integrity": "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
"node_modules/@babel/template": {
"version": "7.27.2",
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz",
@@ -332,6 +415,141 @@
"node": ">=6.9.0"
}
},
+ "node_modules/@csstools/color-helpers": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.1.0.tgz",
+ "integrity": "sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/csstools"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/csstools"
+ }
+ ],
+ "license": "MIT-0",
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@csstools/css-calc": {
+ "version": "2.1.4",
+ "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-2.1.4.tgz",
+ "integrity": "sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/csstools"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/csstools"
+ }
+ ],
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "peerDependencies": {
+ "@csstools/css-parser-algorithms": "^3.0.5",
+ "@csstools/css-tokenizer": "^3.0.4"
+ }
+ },
+ "node_modules/@csstools/css-color-parser": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-3.1.0.tgz",
+ "integrity": "sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/csstools"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/csstools"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "@csstools/color-helpers": "^5.1.0",
+ "@csstools/css-calc": "^2.1.4"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "peerDependencies": {
+ "@csstools/css-parser-algorithms": "^3.0.5",
+ "@csstools/css-tokenizer": "^3.0.4"
+ }
+ },
+ "node_modules/@csstools/css-parser-algorithms": {
+ "version": "3.0.5",
+ "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.5.tgz",
+ "integrity": "sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/csstools"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/csstools"
+ }
+ ],
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "peerDependencies": {
+ "@csstools/css-tokenizer": "^3.0.4"
+ }
+ },
+ "node_modules/@csstools/css-syntax-patches-for-csstree": {
+ "version": "1.0.19",
+ "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.0.19.tgz",
+ "integrity": "sha512-QW5/SM2ARltEhoKcmRI1LoLf3/C7dHGswwCnfLcoMgqurBT4f8GvwXMgAbK/FwcxthmJRK5MGTtddj0yQn0J9g==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/csstools"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/csstools"
+ }
+ ],
+ "license": "MIT-0",
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@csstools/css-tokenizer": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-3.0.4.tgz",
+ "integrity": "sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/csstools"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/csstools"
+ }
+ ],
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ }
+ },
"node_modules/@esbuild/aix-ppc64": {
"version": "0.25.11",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.11.tgz",
@@ -2353,6 +2571,13 @@
"win32"
]
},
+ "node_modules/@standard-schema/spec": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.0.0.tgz",
+ "integrity": "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/@tailwindcss/node": {
"version": "4.1.17",
"resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.17.tgz",
@@ -2610,6 +2835,90 @@
"vite": "^5.2.0 || ^6 || ^7"
}
},
+ "node_modules/@testing-library/dom": {
+ "version": "10.4.1",
+ "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz",
+ "integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "@babel/code-frame": "^7.10.4",
+ "@babel/runtime": "^7.12.5",
+ "@types/aria-query": "^5.0.1",
+ "aria-query": "5.3.0",
+ "dom-accessibility-api": "^0.5.9",
+ "lz-string": "^1.5.0",
+ "picocolors": "1.1.1",
+ "pretty-format": "^27.0.2"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@testing-library/jest-dom": {
+ "version": "6.9.1",
+ "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.9.1.tgz",
+ "integrity": "sha512-zIcONa+hVtVSSep9UT3jZ5rizo2BsxgyDYU7WFD5eICBE7no3881HGeb/QkGfsJs6JTkY1aQhT7rIPC7e+0nnA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@adobe/css-tools": "^4.4.0",
+ "aria-query": "^5.0.0",
+ "css.escape": "^1.5.1",
+ "dom-accessibility-api": "^0.6.3",
+ "picocolors": "^1.1.1",
+ "redent": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=14",
+ "npm": ">=6",
+ "yarn": ">=1"
+ }
+ },
+ "node_modules/@testing-library/jest-dom/node_modules/dom-accessibility-api": {
+ "version": "0.6.3",
+ "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz",
+ "integrity": "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@testing-library/react": {
+ "version": "16.3.0",
+ "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.3.0.tgz",
+ "integrity": "sha512-kFSyxiEDwv1WLl2fgsq6pPBbw5aWKrsY2/noi1Id0TK0UParSF62oFQFGHXIyaG4pp2tEub/Zlel+fjjZILDsw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.12.5"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "peerDependencies": {
+ "@testing-library/dom": "^10.0.0",
+ "@types/react": "^18.0.0 || ^19.0.0",
+ "@types/react-dom": "^18.0.0 || ^19.0.0",
+ "react": "^18.0.0 || ^19.0.0",
+ "react-dom": "^18.0.0 || ^19.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@types/aria-query": {
+ "version": "5.0.4",
+ "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz",
+ "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true
+ },
"node_modules/@types/babel__core": {
"version": "7.20.5",
"resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
@@ -2655,6 +2964,17 @@
"@babel/types": "^7.28.2"
}
},
+ "node_modules/@types/chai": {
+ "version": "5.2.3",
+ "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz",
+ "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/deep-eql": "*",
+ "assertion-error": "^2.0.1"
+ }
+ },
"node_modules/@types/debug": {
"version": "4.1.12",
"resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz",
@@ -2664,6 +2984,13 @@
"@types/ms": "*"
}
},
+ "node_modules/@types/deep-eql": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz",
+ "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/@types/estree": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
@@ -3053,6 +3380,117 @@
"vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0"
}
},
+ "node_modules/@vitest/expect": {
+ "version": "4.0.14",
+ "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.0.14.tgz",
+ "integrity": "sha512-RHk63V3zvRiYOWAV0rGEBRO820ce17hz7cI2kDmEdfQsBjT2luEKB5tCOc91u1oSQoUOZkSv3ZyzkdkSLD7lKw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@standard-schema/spec": "^1.0.0",
+ "@types/chai": "^5.2.2",
+ "@vitest/spy": "4.0.14",
+ "@vitest/utils": "4.0.14",
+ "chai": "^6.2.1",
+ "tinyrainbow": "^3.0.3"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/@vitest/mocker": {
+ "version": "4.0.14",
+ "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.0.14.tgz",
+ "integrity": "sha512-RzS5NujlCzeRPF1MK7MXLiEFpkIXeMdQ+rN3Kk3tDI9j0mtbr7Nmuq67tpkOJQpgyClbOltCXMjLZicJHsH5Cg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@vitest/spy": "4.0.14",
+ "estree-walker": "^3.0.3",
+ "magic-string": "^0.30.21"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ },
+ "peerDependencies": {
+ "msw": "^2.4.9",
+ "vite": "^6.0.0 || ^7.0.0-0"
+ },
+ "peerDependenciesMeta": {
+ "msw": {
+ "optional": true
+ },
+ "vite": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@vitest/pretty-format": {
+ "version": "4.0.14",
+ "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.0.14.tgz",
+ "integrity": "sha512-SOYPgujB6TITcJxgd3wmsLl+wZv+fy3av2PpiPpsWPZ6J1ySUYfScfpIt2Yv56ShJXR2MOA6q2KjKHN4EpdyRQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "tinyrainbow": "^3.0.3"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/@vitest/runner": {
+ "version": "4.0.14",
+ "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.0.14.tgz",
+ "integrity": "sha512-BsAIk3FAqxICqREbX8SetIteT8PiaUL/tgJjmhxJhCsigmzzH8xeadtp7LRnTpCVzvf0ib9BgAfKJHuhNllKLw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@vitest/utils": "4.0.14",
+ "pathe": "^2.0.3"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/@vitest/snapshot": {
+ "version": "4.0.14",
+ "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.0.14.tgz",
+ "integrity": "sha512-aQVBfT1PMzDSA16Y3Fp45a0q8nKexx6N5Amw3MX55BeTeZpoC08fGqEZqVmPcqN0ueZsuUQ9rriPMhZ3Mu19Ag==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@vitest/pretty-format": "4.0.14",
+ "magic-string": "^0.30.21",
+ "pathe": "^2.0.3"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/@vitest/spy": {
+ "version": "4.0.14",
+ "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.0.14.tgz",
+ "integrity": "sha512-JmAZT1UtZooO0tpY3GRyiC/8W7dCs05UOq9rfsUUgEZEdq+DuHLmWhPsrTt0TiW7WYeL/hXpaE07AZ2RCk44hg==",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/@vitest/utils": {
+ "version": "4.0.14",
+ "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.0.14.tgz",
+ "integrity": "sha512-hLqXZKAWNg8pI+SQXyXxWCTOpA3MvsqcbVeNgSi8x/CSN2wi26dSzn1wrOhmCmFjEvN9p8/kLFRHa6PI8jHazw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@vitest/pretty-format": "4.0.14",
+ "tinyrainbow": "^3.0.3"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
"node_modules/acorn": {
"version": "8.15.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
@@ -3076,6 +3514,16 @@
"acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
}
},
+ "node_modules/agent-base": {
+ "version": "7.1.4",
+ "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz",
+ "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 14"
+ }
+ },
"node_modules/ajv": {
"version": "6.12.6",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
@@ -3093,6 +3541,17 @@
"url": "https://github.com/sponsors/epoberezkin"
}
},
+ "node_modules/ansi-regex": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
"node_modules/ansi-styles": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
@@ -3128,11 +3587,31 @@
"node": ">=10"
}
},
- "node_modules/bail": {
- "version": "2.0.2",
- "resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz",
- "integrity": "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==",
- "license": "MIT",
+ "node_modules/aria-query": {
+ "version": "5.3.0",
+ "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz",
+ "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "dequal": "^2.0.3"
+ }
+ },
+ "node_modules/assertion-error": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz",
+ "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/bail": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz",
+ "integrity": "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==",
+ "license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/wooorm"
@@ -3155,6 +3634,16 @@
"baseline-browser-mapping": "dist/cli.js"
}
},
+ "node_modules/bidi-js": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.0.3.tgz",
+ "integrity": "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "require-from-string": "^2.0.2"
+ }
+ },
"node_modules/brace-expansion": {
"version": "1.1.12",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
@@ -3254,6 +3743,16 @@
"url": "https://github.com/sponsors/wooorm"
}
},
+ "node_modules/chai": {
+ "version": "6.2.1",
+ "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.1.tgz",
+ "integrity": "sha512-p4Z49OGG5W/WBCPSS/dH3jQ73kD6tiMmUM+bckNK6Jr5JHMG3k9bg/BvKR8lKmtVBKmOiuVaV2ws8s9oSbwysg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ }
+ },
"node_modules/chalk": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
@@ -3400,12 +3899,62 @@
"node": ">= 8"
}
},
+ "node_modules/css-tree": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.1.0.tgz",
+ "integrity": "sha512-0eW44TGN5SQXU1mWSkKwFstI/22X2bG1nYzZTYMAWjylYURhse752YgbE4Cx46AC+bAvI+/dYTPRk1LqSUnu6w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "mdn-data": "2.12.2",
+ "source-map-js": "^1.0.1"
+ },
+ "engines": {
+ "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0"
+ }
+ },
+ "node_modules/css.escape": {
+ "version": "1.5.1",
+ "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz",
+ "integrity": "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/cssstyle": {
+ "version": "5.3.3",
+ "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-5.3.3.tgz",
+ "integrity": "sha512-OytmFH+13/QXONJcC75QNdMtKpceNk3u8ThBjyyYjkEcy/ekBwR1mMAuNvi3gdBPW3N5TlCzQ0WZw8H0lN/bDw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@asamuzakjp/css-color": "^4.0.3",
+ "@csstools/css-syntax-patches-for-csstree": "^1.0.14",
+ "css-tree": "^3.1.0"
+ },
+ "engines": {
+ "node": ">=20"
+ }
+ },
"node_modules/csstype": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
"license": "MIT"
},
+ "node_modules/data-urls": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-6.0.0.tgz",
+ "integrity": "sha512-BnBS08aLUM+DKamupXs3w2tJJoqU+AkaE/+6vQxi/G/DPmIZFJJp9Dkb1kM03AZx8ADehDUZgsNxju3mPXZYIA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "whatwg-mimetype": "^4.0.0",
+ "whatwg-url": "^15.0.0"
+ },
+ "engines": {
+ "node": ">=20"
+ }
+ },
"node_modules/debug": {
"version": "4.4.3",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
@@ -3423,6 +3972,13 @@
}
}
},
+ "node_modules/decimal.js": {
+ "version": "10.6.0",
+ "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz",
+ "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/decode-named-character-reference": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.2.0.tgz",
@@ -3480,6 +4036,14 @@
"url": "https://github.com/sponsors/wooorm"
}
},
+ "node_modules/dom-accessibility-api": {
+ "version": "0.5.16",
+ "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz",
+ "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true
+ },
"node_modules/electron-to-chromium": {
"version": "1.5.240",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.240.tgz",
@@ -3500,6 +4064,26 @@
"node": ">=10.13.0"
}
},
+ "node_modules/entities": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz",
+ "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=0.12"
+ },
+ "funding": {
+ "url": "https://github.com/fb55/entities?sponsor=1"
+ }
+ },
+ "node_modules/es-module-lexer": {
+ "version": "1.7.0",
+ "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz",
+ "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/esbuild": {
"version": "0.25.11",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.11.tgz",
@@ -3741,6 +4325,16 @@
"url": "https://opencollective.com/unified"
}
},
+ "node_modules/estree-walker": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz",
+ "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree": "^1.0.0"
+ }
+ },
"node_modules/esutils": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
@@ -3751,6 +4345,16 @@
"node": ">=0.10.0"
}
},
+ "node_modules/expect-type": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.2.2.tgz",
+ "integrity": "sha512-JhFGDVJ7tmDJItKhYgJCGLOWjuK9vPxiXoUFLwLDc99NlmklilbiQJwoctZtt13+xMw91MCk/REan6MWHqDjyA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=12.0.0"
+ }
+ },
"node_modules/extend": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
@@ -4042,6 +4646,19 @@
"node": ">=12.0.0"
}
},
+ "node_modules/html-encoding-sniffer": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz",
+ "integrity": "sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "whatwg-encoding": "^3.1.1"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
"node_modules/html-url-attributes": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/html-url-attributes/-/html-url-attributes-3.0.1.tgz",
@@ -4052,6 +4669,47 @@
"url": "https://opencollective.com/unified"
}
},
+ "node_modules/http-proxy-agent": {
+ "version": "7.0.2",
+ "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz",
+ "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "agent-base": "^7.1.0",
+ "debug": "^4.3.4"
+ },
+ "engines": {
+ "node": ">= 14"
+ }
+ },
+ "node_modules/https-proxy-agent": {
+ "version": "7.0.6",
+ "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz",
+ "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "agent-base": "^7.1.2",
+ "debug": "4"
+ },
+ "engines": {
+ "node": ">= 14"
+ }
+ },
+ "node_modules/iconv-lite": {
+ "version": "0.6.3",
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
+ "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "safer-buffer": ">= 2.1.2 < 3.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
"node_modules/ignore": {
"version": "5.3.2",
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
@@ -4089,6 +4747,16 @@
"node": ">=0.8.19"
}
},
+ "node_modules/indent-string": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz",
+ "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
"node_modules/inline-style-parser": {
"version": "0.2.6",
"resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.2.6.tgz",
@@ -4184,6 +4852,13 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/is-potential-custom-element-name": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz",
+ "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/isexe": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
@@ -4220,6 +4895,46 @@
"js-yaml": "bin/js-yaml.js"
}
},
+ "node_modules/jsdom": {
+ "version": "27.2.0",
+ "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-27.2.0.tgz",
+ "integrity": "sha512-454TI39PeRDW1LgpyLPyURtB4Zx1tklSr6+OFOipsxGUH1WMTvk6C65JQdrj455+DP2uJ1+veBEHTGFKWVLFoA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@acemir/cssom": "^0.9.23",
+ "@asamuzakjp/dom-selector": "^6.7.4",
+ "cssstyle": "^5.3.3",
+ "data-urls": "^6.0.0",
+ "decimal.js": "^10.6.0",
+ "html-encoding-sniffer": "^4.0.0",
+ "http-proxy-agent": "^7.0.2",
+ "https-proxy-agent": "^7.0.6",
+ "is-potential-custom-element-name": "^1.0.1",
+ "parse5": "^8.0.0",
+ "saxes": "^6.0.0",
+ "symbol-tree": "^3.2.4",
+ "tough-cookie": "^6.0.0",
+ "w3c-xmlserializer": "^5.0.0",
+ "webidl-conversions": "^8.0.0",
+ "whatwg-encoding": "^3.1.1",
+ "whatwg-mimetype": "^4.0.0",
+ "whatwg-url": "^15.1.0",
+ "ws": "^8.18.3",
+ "xml-name-validator": "^5.0.0"
+ },
+ "engines": {
+ "node": "^20.19.0 || ^22.12.0 || >=24.0.0"
+ },
+ "peerDependencies": {
+ "canvas": "^3.0.0"
+ },
+ "peerDependenciesMeta": {
+ "canvas": {
+ "optional": true
+ }
+ }
+ },
"node_modules/jsesc": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz",
@@ -4592,6 +5307,17 @@
"react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0"
}
},
+ "node_modules/lz-string": {
+ "version": "1.5.0",
+ "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz",
+ "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "bin": {
+ "lz-string": "bin/bin.js"
+ }
+ },
"node_modules/magic-string": {
"version": "0.30.21",
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
@@ -4893,6 +5619,13 @@
"url": "https://opencollective.com/unified"
}
},
+ "node_modules/mdn-data": {
+ "version": "2.12.2",
+ "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.12.2.tgz",
+ "integrity": "sha512-IEn+pegP1aManZuckezWCO+XZQDplx1366JoVhTpMpBB1sPey/SbveZQUosKiKiGYjg1wH4pMlNgXbCiYgihQA==",
+ "dev": true,
+ "license": "CC0-1.0"
+ },
"node_modules/merge2": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
@@ -5480,6 +6213,16 @@
"node": ">=8.6"
}
},
+ "node_modules/min-indent": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz",
+ "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
"node_modules/minimatch": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
@@ -5531,6 +6274,17 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/obug": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.1.tgz",
+ "integrity": "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==",
+ "dev": true,
+ "funding": [
+ "https://github.com/sponsors/sxzz",
+ "https://opencollective.com/debug"
+ ],
+ "license": "MIT"
+ },
"node_modules/optionator": {
"version": "0.9.4",
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
@@ -5619,6 +6373,19 @@
"integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==",
"license": "MIT"
},
+ "node_modules/parse5": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.0.tgz",
+ "integrity": "sha512-9m4m5GSgXjL4AjumKzq1Fgfp3Z8rsvjRNbnkVwfu2ImRqE5D0LnY2QfDen18FSY9C573YU5XxSapdHZTZ2WolA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "entities": "^6.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/inikulin/parse5?sponsor=1"
+ }
+ },
"node_modules/path-exists": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
@@ -5639,6 +6406,13 @@
"node": ">=8"
}
},
+ "node_modules/pathe": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz",
+ "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/picocolors": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
@@ -5696,6 +6470,36 @@
"node": ">= 0.8.0"
}
},
+ "node_modules/pretty-format": {
+ "version": "27.5.1",
+ "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz",
+ "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "ansi-regex": "^5.0.1",
+ "ansi-styles": "^5.0.0",
+ "react-is": "^17.0.1"
+ },
+ "engines": {
+ "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0"
+ }
+ },
+ "node_modules/pretty-format/node_modules/ansi-styles": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz",
+ "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
"node_modules/property-information": {
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/property-information/-/property-information-7.1.0.tgz",
@@ -5758,6 +6562,14 @@
"react": "^19.2.0"
}
},
+ "node_modules/react-is": {
+ "version": "17.0.2",
+ "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
+ "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true
+ },
"node_modules/react-markdown": {
"version": "10.1.0",
"resolved": "https://registry.npmjs.org/react-markdown/-/react-markdown-10.1.0.tgz",
@@ -5902,6 +6714,20 @@
}
}
},
+ "node_modules/redent": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz",
+ "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "indent-string": "^4.0.0",
+ "strip-indent": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
"node_modules/rehype-highlight": {
"version": "7.0.2",
"resolved": "https://registry.npmjs.org/rehype-highlight/-/rehype-highlight-7.0.2.tgz",
@@ -6000,6 +6826,16 @@
"url": "https://opencollective.com/unified"
}
},
+ "node_modules/require-from-string": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
+ "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
"node_modules/resolve-from": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
@@ -6086,6 +6922,26 @@
"queue-microtask": "^1.2.2"
}
},
+ "node_modules/safer-buffer": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
+ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/saxes": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz",
+ "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "xmlchars": "^2.2.0"
+ },
+ "engines": {
+ "node": ">=v12.22.7"
+ }
+ },
"node_modules/scheduler": {
"version": "0.27.0",
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz",
@@ -6131,6 +6987,13 @@
"node": ">=8"
}
},
+ "node_modules/siginfo": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz",
+ "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==",
+ "dev": true,
+ "license": "ISC"
+ },
"node_modules/source-map-js": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
@@ -6150,6 +7013,20 @@
"url": "https://github.com/sponsors/wooorm"
}
},
+ "node_modules/stackback": {
+ "version": "0.0.2",
+ "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz",
+ "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/std-env": {
+ "version": "3.10.0",
+ "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz",
+ "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/stringify-entities": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz",
@@ -6164,6 +7041,19 @@
"url": "https://github.com/sponsors/wooorm"
}
},
+ "node_modules/strip-indent": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz",
+ "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "min-indent": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
"node_modules/strip-json-comments": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
@@ -6208,6 +7098,13 @@
"node": ">=8"
}
},
+ "node_modules/symbol-tree": {
+ "version": "3.2.4",
+ "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz",
+ "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/tailwind-merge": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.3.1.tgz",
@@ -6237,6 +7134,20 @@
"url": "https://opencollective.com/webpack"
}
},
+ "node_modules/tinybench": {
+ "version": "2.9.0",
+ "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz",
+ "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/tinyexec": {
+ "version": "0.3.2",
+ "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz",
+ "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/tinyglobby": {
"version": "0.2.15",
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
@@ -6282,6 +7193,36 @@
"url": "https://github.com/sponsors/jonschlinkert"
}
},
+ "node_modules/tinyrainbow": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.0.3.tgz",
+ "integrity": "sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/tldts": {
+ "version": "7.0.19",
+ "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.0.19.tgz",
+ "integrity": "sha512-8PWx8tvC4jDB39BQw1m4x8y5MH1BcQ5xHeL2n7UVFulMPH/3Q0uiamahFJ3lXA0zO2SUyRXuVVbWSDmstlt9YA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "tldts-core": "^7.0.19"
+ },
+ "bin": {
+ "tldts": "bin/cli.js"
+ }
+ },
+ "node_modules/tldts-core": {
+ "version": "7.0.19",
+ "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.0.19.tgz",
+ "integrity": "sha512-lJX2dEWx0SGH4O6p+7FPwYmJ/bu1JbcGJ8RLaG9b7liIgZ85itUVEPbMtWRVrde/0fnDPEPHW10ZsKW3kVsE9A==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/to-regex-range": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
@@ -6295,6 +7236,32 @@
"node": ">=8.0"
}
},
+ "node_modules/tough-cookie": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.0.tgz",
+ "integrity": "sha512-kXuRi1mtaKMrsLUxz3sQYvVl37B0Ns6MzfrtV5DvJceE9bPyspOqk9xxv7XbZWcfLWbFmm997vl83qUWVJA64w==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "tldts": "^7.0.5"
+ },
+ "engines": {
+ "node": ">=16"
+ }
+ },
+ "node_modules/tr46": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/tr46/-/tr46-6.0.0.tgz",
+ "integrity": "sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "punycode": "^2.3.1"
+ },
+ "engines": {
+ "node": ">=20"
+ }
+ },
"node_modules/trim-lines": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz",
@@ -6718,6 +7685,157 @@
"url": "https://github.com/sponsors/jonschlinkert"
}
},
+ "node_modules/vitest": {
+ "version": "4.0.14",
+ "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.0.14.tgz",
+ "integrity": "sha512-d9B2J9Cm9dN9+6nxMnnNJKJCtcyKfnHj15N6YNJfaFHRLua/d3sRKU9RuKmO9mB0XdFtUizlxfz/VPbd3OxGhw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@vitest/expect": "4.0.14",
+ "@vitest/mocker": "4.0.14",
+ "@vitest/pretty-format": "4.0.14",
+ "@vitest/runner": "4.0.14",
+ "@vitest/snapshot": "4.0.14",
+ "@vitest/spy": "4.0.14",
+ "@vitest/utils": "4.0.14",
+ "es-module-lexer": "^1.7.0",
+ "expect-type": "^1.2.2",
+ "magic-string": "^0.30.21",
+ "obug": "^2.1.1",
+ "pathe": "^2.0.3",
+ "picomatch": "^4.0.3",
+ "std-env": "^3.10.0",
+ "tinybench": "^2.9.0",
+ "tinyexec": "^0.3.2",
+ "tinyglobby": "^0.2.15",
+ "tinyrainbow": "^3.0.3",
+ "vite": "^6.0.0 || ^7.0.0",
+ "why-is-node-running": "^2.3.0"
+ },
+ "bin": {
+ "vitest": "vitest.mjs"
+ },
+ "engines": {
+ "node": "^20.0.0 || ^22.0.0 || >=24.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ },
+ "peerDependencies": {
+ "@edge-runtime/vm": "*",
+ "@opentelemetry/api": "^1.9.0",
+ "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0",
+ "@vitest/browser-playwright": "4.0.14",
+ "@vitest/browser-preview": "4.0.14",
+ "@vitest/browser-webdriverio": "4.0.14",
+ "@vitest/ui": "4.0.14",
+ "happy-dom": "*",
+ "jsdom": "*"
+ },
+ "peerDependenciesMeta": {
+ "@edge-runtime/vm": {
+ "optional": true
+ },
+ "@opentelemetry/api": {
+ "optional": true
+ },
+ "@types/node": {
+ "optional": true
+ },
+ "@vitest/browser-playwright": {
+ "optional": true
+ },
+ "@vitest/browser-preview": {
+ "optional": true
+ },
+ "@vitest/browser-webdriverio": {
+ "optional": true
+ },
+ "@vitest/ui": {
+ "optional": true
+ },
+ "happy-dom": {
+ "optional": true
+ },
+ "jsdom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/vitest/node_modules/picomatch": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
+ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/w3c-xmlserializer": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz",
+ "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "xml-name-validator": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/webidl-conversions": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-8.0.0.tgz",
+ "integrity": "sha512-n4W4YFyz5JzOfQeA8oN7dUYpR+MBP3PIUsn2jLjWXwK5ASUzt0Jc/A5sAUZoCYFJRGF0FBKJ+1JjN43rNdsQzA==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=20"
+ }
+ },
+ "node_modules/whatwg-encoding": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz",
+ "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "iconv-lite": "0.6.3"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/whatwg-mimetype": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz",
+ "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/whatwg-url": {
+ "version": "15.1.0",
+ "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-15.1.0.tgz",
+ "integrity": "sha512-2ytDk0kiEj/yu90JOAp44PVPUkO9+jVhyf+SybKlRHSDlvOOZhdPIrr7xTH64l4WixO2cP+wQIcgujkGBPPz6g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "tr46": "^6.0.0",
+ "webidl-conversions": "^8.0.0"
+ },
+ "engines": {
+ "node": ">=20"
+ }
+ },
"node_modules/which": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
@@ -6734,6 +7852,23 @@
"node": ">= 8"
}
},
+ "node_modules/why-is-node-running": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz",
+ "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "siginfo": "^2.0.0",
+ "stackback": "0.0.2"
+ },
+ "bin": {
+ "why-is-node-running": "cli.js"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
"node_modules/word-wrap": {
"version": "1.2.5",
"resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz",
@@ -6744,6 +7879,45 @@
"node": ">=0.10.0"
}
},
+ "node_modules/ws": {
+ "version": "8.18.3",
+ "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz",
+ "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10.0.0"
+ },
+ "peerDependencies": {
+ "bufferutil": "^4.0.1",
+ "utf-8-validate": ">=5.0.2"
+ },
+ "peerDependenciesMeta": {
+ "bufferutil": {
+ "optional": true
+ },
+ "utf-8-validate": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/xml-name-validator": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz",
+ "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/xmlchars": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz",
+ "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/yallist": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
diff --git a/frontend/package.json b/frontend/package.json
index 2cf1ea0..3cff4da 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -7,7 +7,9 @@
"dev": "vite",
"build": "tsc -b && vite build",
"lint": "eslint .",
- "preview": "vite preview"
+ "preview": "vite preview",
+ "test": "vitest",
+ "test:watch": "vitest --watch"
},
"dependencies": {
"@radix-ui/react-accordion": "^1.2.12",
@@ -37,6 +39,8 @@
},
"devDependencies": {
"@eslint/js": "^9.36.0",
+ "@testing-library/jest-dom": "^6.9.1",
+ "@testing-library/react": "^16.3.0",
"@types/node": "^24.6.0",
"@types/react": "^19.1.16",
"@types/react-dom": "^19.1.9",
@@ -46,9 +50,11 @@
"eslint-plugin-react-hooks": "^5.2.0",
"eslint-plugin-react-refresh": "^0.4.22",
"globals": "^16.4.0",
+ "jsdom": "^27.2.0",
"tw-animate-css": "^1.4.0",
"typescript": "~5.9.3",
"typescript-eslint": "^8.45.0",
- "vite": "^7.1.7"
+ "vite": "^7.1.7",
+ "vitest": "^4.0.14"
}
}
diff --git a/frontend/src/components/chat-history.tsx b/frontend/src/components/chat-history.tsx
index d8b66ff..a88f1fd 100644
--- a/frontend/src/components/chat-history.tsx
+++ b/frontend/src/components/chat-history.tsx
@@ -28,7 +28,7 @@ const WelcomeScreen: React.FC = () => (
);
// チャットメッセージバブル
-const ChatMessageBubble: React.FC<{ message: Message }> = ({ message }) => {
+export const ChatMessageBubble: React.FC<{ message: Message }> = ({ message }) => {
const isUser = message.role === MessageRole.USER;
// ユーザーメッセージ(右寄せ、最大幅設定、その他設定もここで)
@@ -64,10 +64,13 @@ const ChatMessageBubble: React.FC<{ message: Message }> = ({ message }) => {
}
// ローディングインジケーター
+import { Spinner } from "@/components/ui/spinner";
+
const LoadingIndicator = () => (
-
- AIが考え中です...
+
+
+ AIが考え中です...
);
diff --git a/frontend/src/setupTests.ts b/frontend/src/setupTests.ts
new file mode 100644
index 0000000..02e45a9
--- /dev/null
+++ b/frontend/src/setupTests.ts
@@ -0,0 +1,17 @@
+// @testing-library/jest-dom の拡張アサーションを有効化
+import '@testing-library/jest-dom';
+
+// Mock for window.matchMedia used in use-mobile hook
+Object.defineProperty(window, 'matchMedia', {
+ writable: true,
+ value: (query: string) => ({
+ matches: false,
+ media: query,
+ onchange: null,
+ addListener: () => {},
+ removeListener: () => {},
+ addEventListener: () => {},
+ removeEventListener: () => {},
+ dispatchEvent: () => false,
+ }),
+});
diff --git a/frontend/src/tests/ChatMessageBubble.test.tsx b/frontend/src/tests/ChatMessageBubble.test.tsx
new file mode 100644
index 0000000..598b708
--- /dev/null
+++ b/frontend/src/tests/ChatMessageBubble.test.tsx
@@ -0,0 +1,89 @@
+// frontend/src/tests/ChatMessageBubble.test.tsx
+
+/**
+ * @fileoverview ChatMessageBubble コンポーネントのテストスイート。
+ * ChatMessageBubble は、ユーザーからのメッセージとAIからのレスポンスを個別に表示し、
+ * それぞれのロールに応じたスタイルが正しく適用されていることを検証します。
+ *
+ * @remarks
+ * @testing-library/react を使用してコンポーネントをレンダリングし、
+ * vitest のアサーションAPIを使用して挙動を検証します。
+ * スタイルの検証には、DOM要素に特定のクラスが存在するかを確認しています。
+ */
+
+// 必要なテストユーティリティとアサーションAPIをインポートします。
+import { render, screen } from '@testing-library/react'; // コンポーネントのレンダリングとDOMへのアクセスを提供
+import { expect, describe, it } from 'vitest'; // テストのグループ化とアサーションのためのVitest API
+
+// テスト対象のコンポーネントと、メッセージの役割を定義する型をインポートします。
+import { ChatMessageBubble } from '@/components/chat-history'; // テスト対象のChatMessageBubbleコンポーネント
+import { MessageRole } from '@/types/types'; // メッセージの役割 (USER, MODELなど) を定義したEnum
+
+/**
+ * ChatMessageBubble コンポーネントのテストスイートを定義します。
+ * 'describe' ブロック内で関連するテストをグループ化します。
+ */
+describe('ChatMessageBubble Component', () => {
+
+ /**
+ * ユーザーのメッセージが正しく表示され、右寄せスタイルが適用されることを検証するテストケース。
+ *
+ * 1. ダミーのユーザーメッセージデータを作成します。
+ * - `userMessageContent`: 表示されるテキスト内容。
+ * - `userMessage`: `Message` 型に準拠したオブジェクトで、`id`, `role` (USER), `parts` を含みます。
+ * 2. `ChatMessageBubble` コンポーネントを `render` 関数でレンダリングします。
+ * 3. `screen.getByText` を使用して、画面上にユーザーメッセージの内容が表示されていることを確認します。
+ * 4. メッセージテキスト要素から親要素を辿り、Flexboxの右寄せ(`justify-end`)クラスが適用されているか検証します。
+ * - `closest('div')` で `ReactMarkdown` を囲む `div` を取得します。
+ * - `.parentElement` でそのさらに親の `div` (Flexコンテナ) を取得し、スタイルが適用されていることを確認します。
+ */
+ it('ユーザーのメッセージが正しく表示され、右寄せスタイルが適用されること', () => {
+ const userMessageContent = 'nmapの結果を解析して。';
+ const userMessage = {
+ id: '1',
+ role: MessageRole.USER, // ユーザーの役割を設定
+ parts: [{ text: userMessageContent }], // メッセージの内容
+ };
+
+ // ChatMessageBubbleコンポーネントを、作成したユーザーメッセージをプロップとして渡してレンダリングします。
+ render(
);
+
+ // 画面上にユーザーメッセージのテキストが表示されていることを確認します。
+ expect(screen.getByText(userMessageContent)).toBeInTheDocument();
+
+ // メッセージテキストを含む要素の親要素を取得し、それが右寄せクラスを持っているか検証します。
+ // closest('div')でテキストを囲む最も近いdivを取得し、その親(parentElement)にスタイルが付与されているため、それを検証します。
+ const containerElement = screen.getByText(userMessageContent).closest('div')?.parentElement;
+ expect(containerElement).toHaveClass('justify-end'); // Flexboxの右寄せクラス
+ });
+
+ /**
+ * AIからのレスポンスが正しく表示され、左寄せスタイルが適用されることを検証するテストケース。
+ *
+ * 1. ダミーのAIメッセージデータを作成します。
+ * - `aiMessageContent`: 表示されるテキスト内容。
+ * - `aiMessage`: `Message` 型に準拠したオブジェクトで、`id`, `role` (MODEL), `parts` を含みます。
+ * 2. `ChatMessageBubble` コンポーネントを `render` 関数でレンダリングします。
+ * 3. `screen.getByText` を使用して、画面上にAIメッセージの内容が表示されていることを確認します。
+ * 4. メッセージテキスト要素から親要素を辿り、Flexboxの左寄せ(`justify-start`)クラスが適用されているか検証します。
+ * - ユーザーメッセージのテストと同様に、`.closest('div')?.parentElement` を使用します。
+ */
+ it('AIからのレスポンスが正しく表示され、左寄せスタイルが適用されること', () => {
+ const aiMessageContent = 'nmapの結果を解析しました。開いているポートは...';
+ const aiMessage = {
+ id: '2',
+ role: MessageRole.MODEL, // AI (モデル) の役割を設定
+ parts: [{ text: aiMessageContent }], // メッセージの内容
+ };
+
+ // ChatMessageBubbleコンポーネントを、作成したAIメッセージをプロップとして渡してレンダリングします。
+ render(
);
+
+ // 画面上にAIメッセージのテキストが表示されていることを確認します。
+ expect(screen.getByText(aiMessageContent)).toBeInTheDocument();
+
+ // メッセージテキストを含む要素の親要素を取得し、それが左寄せクラスを持っているか検証します。
+ const containerElement = screen.getByText(aiMessageContent).closest('div')?.parentElement;
+ expect(containerElement).toHaveClass('justify-start'); // Flexboxの左寄せクラス
+ });
+});
\ No newline at end of file
diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts
index afb00f9..5edb40a 100644
--- a/frontend/vite.config.ts
+++ b/frontend/vite.config.ts
@@ -1,7 +1,7 @@
-import path from "path"
-import react from '@vitejs/plugin-react'
-import tailwindcss from "@tailwindcss/vite"
-import { defineConfig } from 'vite'
+import path from "path";
+import react from '@vitejs/plugin-react';
+import tailwindcss from "@tailwindcss/vite";
+import { defineConfig } from 'vitest/config';
// https://vite.dev/config/
export default defineConfig({
@@ -11,4 +11,11 @@ export default defineConfig({
"@": path.resolve(__dirname, "./src"),
},
},
+ test: {
+ globals: true, // describe, it, expect などをグローバルで使えるようにする
+ environment: 'jsdom', // JSDOM環境でテストを実行(Reactコンポーネントの描画に必要)
+ setupFiles: './src/setupTests.ts', // テスト前の共通処理ファイル
+ // テストファイルを src/**.test.(ts|tsx) のパターンで認識させる
+ include: ['**/*.test.?(c|m)[jt]s?(x)'],
+ },
})
diff --git a/pytest.ini b/pytest.ini
new file mode 100644
index 0000000..bd1c81c
--- /dev/null
+++ b/pytest.ini
@@ -0,0 +1,5 @@
+[pytest]
+pythonpath =
+ .
+ backend
+testpaths = backend/tests
\ No newline at end of file