.');
+}
+
+ReactDOM.createRoot(rootElement).render(
+
+
+ ,
+);
diff --git a/src/pages/TodoPage.tsx b/src/pages/TodoPage.tsx
new file mode 100644
index 0000000..62963c2
--- /dev/null
+++ b/src/pages/TodoPage.tsx
@@ -0,0 +1,233 @@
+/**
+ * TodoPage – main page component for the Todo application.
+ *
+ * Manages the full todo lifecycle: adding, toggling, deleting, and
+ * filtering todo items. State is persisted to localStorage when
+ * available, with a graceful fallback to in-memory state.
+ *
+ * This is a self-contained page component. Presentational sub-components
+ * (TodoInput, TodoList, TodoItem, TodoFilter) will be extracted in a
+ * subsequent task; for now all rendering lives here to ensure the app
+ * compiles and runs end-to-end.
+ */
+import React, { useState, useEffect, useCallback } from 'react';
+
+// ---------------------------------------------------------------------------
+// Types
+// ---------------------------------------------------------------------------
+
+/** Represents a single todo item. */
+interface Todo {
+ /** Unique identifier (crypto.randomUUID or fallback). */
+ id: string;
+ /** User-supplied text for the todo. */
+ text: string;
+ /** Whether the todo has been completed. */
+ completed: boolean;
+ /** Unix-epoch millisecond timestamp of creation. */
+ createdAt: number;
+}
+
+/** The three possible filter states. */
+type FilterType = 'all' | 'active' | 'completed';
+
+// ---------------------------------------------------------------------------
+// localStorage helpers
+// ---------------------------------------------------------------------------
+
+const STORAGE_KEY = 'todo-app-todos';
+
+/**
+ * Attempt to read todos from localStorage.
+ *
+ * Returns the parsed array on success, or `null` when localStorage is
+ * unavailable or the stored value is unparseable.
+ */
+function loadTodos(): Todo[] | null {
+ try {
+ const raw = localStorage.getItem(STORAGE_KEY);
+ if (raw === null) return null;
+ const parsed: unknown = JSON.parse(raw);
+ if (Array.isArray(parsed)) return parsed as Todo[];
+ return null;
+ } catch {
+ return null;
+ }
+}
+
+/**
+ * Persist the given todos array to localStorage.
+ *
+ * Silently ignores errors (e.g. quota exceeded in private browsing).
+ */
+function saveTodos(todos: Todo[]): void {
+ try {
+ localStorage.setItem(STORAGE_KEY, JSON.stringify(todos));
+ } catch {
+ // Graceful fallback – state remains in memory only.
+ }
+}
+
+// ---------------------------------------------------------------------------
+// ID generation helper
+// ---------------------------------------------------------------------------
+
+/**
+ * Generate a unique id string.
+ *
+ * Uses `crypto.randomUUID()` when available (modern browsers), falling
+ * back to a simple timestamp + random suffix.
+ */
+function generateId(): string {
+ if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') {
+ return crypto.randomUUID();
+ }
+ return `${Date.now()}-${Math.random().toString(36).slice(2, 11)}`;
+}
+
+// ---------------------------------------------------------------------------
+// TodoPage component
+// ---------------------------------------------------------------------------
+
+/**
+ * TodoPage is the top-level page that assembles the entire Todo UI.
+ *
+ * It owns the `todos` and `filter` state and will eventually delegate
+ * rendering to extracted child components.
+ */
+const TodoPage: React.FC = () => {
+ const [todos, setTodos] = useState
(() => loadTodos() ?? []);
+ const [filter, setFilter] = useState('all');
+ const [inputValue, setInputValue] = useState('');
+
+ // Persist todos to localStorage whenever they change.
+ useEffect(() => {
+ saveTodos(todos);
+ }, [todos]);
+
+ // -- Handlers -------------------------------------------------------------
+
+ /** Add a new todo. Trims whitespace and rejects empty strings. */
+ const handleAdd = useCallback(() => {
+ const trimmed = inputValue.trim();
+ if (trimmed.length === 0) return;
+
+ const newTodo: Todo = {
+ id: generateId(),
+ text: trimmed,
+ completed: false,
+ createdAt: Date.now(),
+ };
+
+ // Prepend – most recent first.
+ setTodos((prev) => [newTodo, ...prev]);
+ setInputValue('');
+ }, [inputValue]);
+
+ /** Toggle the completed flag of a todo by id. */
+ const handleToggle = useCallback((id: string) => {
+ setTodos((prev) =>
+ prev.map((t) => (t.id === id ? { ...t, completed: !t.completed } : t)),
+ );
+ }, []);
+
+ /** Delete a todo by id. */
+ const handleDelete = useCallback((id: string) => {
+ setTodos((prev) => prev.filter((t) => t.id !== id));
+ }, []);
+
+ /** Handle Enter key in the input field. */
+ const handleKeyDown = useCallback(
+ (e: React.KeyboardEvent) => {
+ if (e.key === 'Enter') handleAdd();
+ },
+ [handleAdd],
+ );
+
+ // -- Derived data ---------------------------------------------------------
+
+ const filteredTodos: Todo[] = todos.filter((t) => {
+ if (filter === 'active') return !t.completed;
+ if (filter === 'completed') return t.completed;
+ return true;
+ });
+
+ const activeTodoCount: number = todos.filter((t) => !t.completed).length;
+ const completedTodoCount: number = todos.filter((t) => t.completed).length;
+
+ // -- Render ---------------------------------------------------------------
+
+ const filters: FilterType[] = ['all', 'active', 'completed'];
+
+ return (
+
+
Todo App
+
+ {/* Input */}
+
+ setInputValue(e.target.value)}
+ onKeyDown={handleKeyDown}
+ aria-label="New todo text"
+ />
+
+
+
+ {/* Filters */}
+
+ {filters.map((f) => (
+
+ ))}
+
+ {activeTodoCount} active / {completedTodoCount} completed
+
+
+
+ {/* List */}
+
+
+ );
+};
+
+export default TodoPage;
diff --git a/src/setupTests.ts b/src/setupTests.ts
new file mode 100644
index 0000000..90132e9
--- /dev/null
+++ b/src/setupTests.ts
@@ -0,0 +1,7 @@
+/**
+ * Test setup file for Vitest.
+ *
+ * Imports jest-dom matchers so they are available in all test files
+ * without explicit imports.
+ */
+import '@testing-library/jest-dom';
diff --git a/src/todo_app.egg-info/PKG-INFO b/src/todo_app.egg-info/PKG-INFO
new file mode 100644
index 0000000..f2cdcd7
--- /dev/null
+++ b/src/todo_app.egg-info/PKG-INFO
@@ -0,0 +1,5 @@
+Metadata-Version: 2.4
+Name: todo-app
+Version: 1.0.0
+Summary: A simple Todo application with React frontend and FastAPI backend
+Requires-Python: >=3.11
diff --git a/src/todo_app.egg-info/SOURCES.txt b/src/todo_app.egg-info/SOURCES.txt
new file mode 100644
index 0000000..becdbad
--- /dev/null
+++ b/src/todo_app.egg-info/SOURCES.txt
@@ -0,0 +1,9 @@
+README.md
+pyproject.toml
+src/todo_app.egg-info/PKG-INFO
+src/todo_app.egg-info/SOURCES.txt
+src/todo_app.egg-info/dependency_links.txt
+src/todo_app.egg-info/top_level.txt
+tests/test_frontend_setup.py
+tests/test_todo_helpers.py
+tests/test_todo_types.py
\ No newline at end of file
diff --git a/src/todo_app.egg-info/dependency_links.txt b/src/todo_app.egg-info/dependency_links.txt
new file mode 100644
index 0000000..8b13789
--- /dev/null
+++ b/src/todo_app.egg-info/dependency_links.txt
@@ -0,0 +1 @@
+
diff --git a/src/todo_app.egg-info/top_level.txt b/src/todo_app.egg-info/top_level.txt
new file mode 100644
index 0000000..5a304b0
--- /dev/null
+++ b/src/todo_app.egg-info/top_level.txt
@@ -0,0 +1,4 @@
+components
+pages
+types
+utils
diff --git a/src/types/todo.ts b/src/types/todo.ts
new file mode 100644
index 0000000..91d367b
--- /dev/null
+++ b/src/types/todo.ts
@@ -0,0 +1,8 @@
+export interface Todo {
+ id: string;
+ text: string;
+ completed: boolean;
+ createdAt: number;
+}
+
+export type FilterType = 'all' | 'active' | 'completed';
diff --git a/src/utils/filterTodos.ts b/src/utils/filterTodos.ts
new file mode 100644
index 0000000..54e0a90
--- /dev/null
+++ b/src/utils/filterTodos.ts
@@ -0,0 +1,13 @@
+import { Todo, FilterType } from '../types/todo';
+
+export function filterTodos(todos: Todo[], filter: FilterType): Todo[] {
+ switch (filter) {
+ case 'active':
+ return todos.filter((todo) => !todo.completed);
+ case 'completed':
+ return todos.filter((todo) => todo.completed);
+ case 'all':
+ default:
+ return todos;
+ }
+}
diff --git a/src/utils/todoHelpers.ts b/src/utils/todoHelpers.ts
new file mode 100644
index 0000000..fe98be0
--- /dev/null
+++ b/src/utils/todoHelpers.ts
@@ -0,0 +1,82 @@
+import type { Todo, FilterType } from '../types/todo';
+
+/**
+ * Generate a unique identifier string.
+ *
+ * Uses `crypto.randomUUID()` when available (modern browsers / Node 19+),
+ * falling back to a timestamp + random-number approach.
+ *
+ * @returns A unique string suitable for use as a Todo id.
+ */
+export function generateId(): string {
+ if (
+ typeof crypto !== 'undefined' &&
+ typeof crypto.randomUUID === 'function'
+ ) {
+ return crypto.randomUUID();
+ }
+ // Fallback for environments without crypto.randomUUID
+ return `${Date.now().toString(36)}-${Math.random().toString(36).substring(2, 9)}`;
+}
+
+/**
+ * Create a new Todo object from the given text.
+ *
+ * The new todo is incomplete by default and stamped with the current time.
+ *
+ * @param text - The display text for the todo. Will be trimmed.
+ * @returns A fully-formed Todo object.
+ * @throws {Error} If text is empty or whitespace-only after trimming.
+ */
+export function createTodo(text: string): Todo {
+ const trimmed = text.trim();
+ if (trimmed.length === 0) {
+ throw new Error('Todo text must not be empty');
+ }
+ return {
+ id: generateId(),
+ text: trimmed,
+ completed: false,
+ createdAt: Date.now(),
+ };
+}
+
+/**
+ * Return a new Todo with the `completed` field toggled.
+ *
+ * This is a pure function — the original todo is not mutated.
+ *
+ * @param todo - The todo to toggle.
+ * @returns A new Todo object with `completed` flipped.
+ */
+export function toggleTodo(todo: Todo): Todo {
+ return { ...todo, completed: !todo.completed };
+}
+
+/**
+ * Filter an array of todos by the given filter type.
+ *
+ * - `'all'` → returns every todo.
+ * - `'active'` → returns only todos where `completed` is `false`.
+ * - `'completed'` → returns only todos where `completed` is `true`.
+ *
+ * This is a pure function — the original array is not mutated.
+ *
+ * @param todos - The full list of todos. Defaults to `[]` if not provided.
+ * @param filter - The filter mode to apply. Defaults to `'all'`.
+ * @returns A new array containing only the matching todos.
+ */
+export function filterTodos(
+ todos: Todo[] = [],
+ filter: FilterType = 'all',
+): Todo[] {
+ switch (filter) {
+ case 'active':
+ return todos.filter((todo) => !todo.completed);
+ case 'completed':
+ return todos.filter((todo) => todo.completed);
+ case 'all':
+ default:
+ return [...todos];
+ }
+}
diff --git a/src/vite-env.d.ts b/src/vite-env.d.ts
new file mode 100644
index 0000000..11f02fe
--- /dev/null
+++ b/src/vite-env.d.ts
@@ -0,0 +1 @@
+///
diff --git a/test-results.xml b/test-results.xml
new file mode 100644
index 0000000..efa65cc
--- /dev/null
+++ b/test-results.xml
@@ -0,0 +1,13 @@
+tests/test_frontend_setup.py:61: in test_tsconfig_has_jsx
+ data = json.loads((ROOT / "tsconfig.json").read_text())
+ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+/usr/local/lib/python3.12/json/__init__.py:346: in loads
+ return _default_decoder.decode(s)
+ ^^^^^^^^^^^^^^^^^^^^^^^^^^
+/usr/local/lib/python3.12/json/decoder.py:338: in decode
+ obj, end = self.raw_decode(s, idx=_w(s, 0).end())
+ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+/usr/local/lib/python3.12/json/decoder.py:354: in raw_decode
+ obj, end = self.scan_once(s, idx)
+ ^^^^^^^^^^^^^^^^^^^^^^
+E json.decoder.JSONDecodeError: Expecting property name enclosed in double quotes: line 9 column 5 (char 187)
\ No newline at end of file
diff --git a/tests/__init__.py b/tests/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/tests/test_app_renders.tsx b/tests/test_app_renders.tsx
new file mode 100644
index 0000000..9738ff0
--- /dev/null
+++ b/tests/test_app_renders.tsx
@@ -0,0 +1,21 @@
+/**
+ * Smoke test for the root App component.
+ *
+ * Verifies that App renders without crashing and displays the
+ * expected heading from the TodoPage placeholder.
+ */
+import { render, screen } from '@testing-library/react';
+import { describe, it, expect } from 'vitest';
+import App from '../src/App';
+
+describe('App', () => {
+ it('renders the todo app heading', () => {
+ render();
+ expect(screen.getByText('Todo App')).toBeInTheDocument();
+ });
+
+ it('renders the placeholder message', () => {
+ render();
+ expect(screen.getByText('Start adding your todos!')).toBeInTheDocument();
+ });
+});
diff --git a/tests/test_app_structure.py b/tests/test_app_structure.py
new file mode 100644
index 0000000..7cfb68c
--- /dev/null
+++ b/tests/test_app_structure.py
@@ -0,0 +1,159 @@
+"""Structural tests for the React Todo application.
+
+Verify that the critical source files exist and contain the expected
+import/export signatures so the app compiles end-to-end.
+"""
+
+import os
+from pathlib import Path
+from typing import List
+
+import pytest
+
+# Repository root is two levels up from tests/
+ROOT: Path = Path(__file__).resolve().parent.parent
+
+
+def _read(rel_path: str) -> str:
+ """Read a file relative to the repository root and return its contents."""
+ full = ROOT / rel_path
+ assert full.exists(), f"Expected file not found: {rel_path}"
+ return full.read_text(encoding="utf-8")
+
+
+class TestCriticalFilesExist:
+ """Ensure the minimum set of files required for compilation are present."""
+
+ @pytest.mark.parametrize(
+ "rel_path",
+ [
+ "src/App.tsx",
+ "src/pages/TodoPage.tsx",
+ "RUNNING.md",
+ "ARCHITECTURE.md",
+ ],
+ )
+ def test_file_exists(self, rel_path: str) -> None:
+ """Verify that *rel_path* exists relative to the repo root."""
+ full = ROOT / rel_path
+ assert full.exists(), f"Missing file: {rel_path}"
+
+
+class TestAppTsx:
+ """Verify src/App.tsx imports and renders TodoPage correctly."""
+
+ def test_imports_todo_page(self) -> None:
+ """App.tsx must import TodoPage from the pages directory."""
+ content = _read("src/App.tsx")
+ assert "import TodoPage from './pages/TodoPage'" in content
+
+ def test_renders_todo_page(self) -> None:
+ """App.tsx must render the component."""
+ content = _read("src/App.tsx")
+ assert "" in content or "" in content
+
+ def test_exports_default(self) -> None:
+ """App.tsx must have a default export."""
+ content = _read("src/App.tsx")
+ assert "export default App" in content
+
+
+class TestTodoPage:
+ """Verify src/pages/TodoPage.tsx is a self-contained, compilable component."""
+
+ def test_exports_default(self) -> None:
+ """TodoPage.tsx must have a default export."""
+ content = _read("src/pages/TodoPage.tsx")
+ assert "export default TodoPage" in content
+
+ def test_does_not_import_nonexistent_components(self) -> None:
+ """TodoPage must not import from ../components/* until those files exist."""
+ content = _read("src/pages/TodoPage.tsx")
+ # If TodoPage imports from ../components, verify each imported file exists.
+ import re
+
+ matches: List[str] = re.findall(
+ r"from\s+['\"](\.\./components/\w+)['\"]", content
+ )
+ for rel_import in matches:
+ # Convert relative import to file path from src/pages/
+ resolved = (ROOT / "src" / "pages" / rel_import).with_suffix(".tsx")
+ assert resolved.exists(), (
+ f"TodoPage.tsx imports '{rel_import}' but {resolved} does not exist"
+ )
+
+ def test_contains_todo_interface(self) -> None:
+ """TodoPage.tsx must define or import a Todo type."""
+ content = _read("src/pages/TodoPage.tsx")
+ assert "Todo" in content
+
+ def test_uses_usestate(self) -> None:
+ """TodoPage.tsx must use useState for state management."""
+ content = _read("src/pages/TodoPage.tsx")
+ assert "useState" in content
+
+
+class TestRunningMd:
+ """Verify RUNNING.md contains the required setup instructions."""
+
+ def test_contains_npm_install(self) -> None:
+ """RUNNING.md must document 'npm install'."""
+ content = _read("RUNNING.md")
+ assert "npm install" in content
+
+ def test_contains_npm_run_dev(self) -> None:
+ """RUNNING.md must document 'npm run dev'."""
+ content = _read("RUNNING.md")
+ assert "npm run dev" in content
+
+ def test_contains_npm_test(self) -> None:
+ """RUNNING.md must document 'npm test'."""
+ content = _read("RUNNING.md")
+ assert "npm test" in content
+
+ def test_contains_app_description(self) -> None:
+ """RUNNING.md must contain a description of the application."""
+ content = _read("RUNNING.md")
+ assert "Todo" in content
+ assert "application" in content.lower() or "app" in content.lower()
+
+ def test_mentions_localhost_5173(self) -> None:
+ """RUNNING.md must mention the default dev server URL."""
+ content = _read("RUNNING.md")
+ assert "5173" in content
+
+
+class TestArchitectureMd:
+ """Verify ARCHITECTURE.md contains key architectural decisions."""
+
+ def test_contains_file_structure_section(self) -> None:
+ """ARCHITECTURE.md must have a File Structure section."""
+ content = _read("ARCHITECTURE.md")
+ assert "File Structure" in content or "file structure" in content.lower()
+
+ def test_defines_todo_interface(self) -> None:
+ """ARCHITECTURE.md must define the Todo interface with required fields."""
+ content = _read("ARCHITECTURE.md")
+ for field in ["id: string", "text: string", "completed: boolean", "createdAt: number"]:
+ assert field in content, f"ARCHITECTURE.md missing Todo field: {field}"
+
+ def test_defines_filter_type(self) -> None:
+ """ARCHITECTURE.md must define the FilterType union."""
+ content = _read("ARCHITECTURE.md")
+ assert "FilterType" in content
+ assert "'all'" in content
+ assert "'active'" in content
+ assert "'completed'" in content
+
+ def test_lists_source_files(self) -> None:
+ """ARCHITECTURE.md must reference at least 14 source file paths."""
+ content = _read("ARCHITECTURE.md")
+ # Count .tsx and .ts file references (excluding markdown code fence language hints)
+ import re
+
+ file_refs = re.findall(r"\S+\.tsx?\b", content)
+ # Deduplicate
+ unique = set(file_refs)
+ assert len(unique) >= 14, (
+ f"Expected at least 14 unique .ts/.tsx file references, found {len(unique)}: {unique}"
+ )
diff --git a/tests/test_frontend_setup.py b/tests/test_frontend_setup.py
new file mode 100644
index 0000000..7feec1b
--- /dev/null
+++ b/tests/test_frontend_setup.py
@@ -0,0 +1,158 @@
+"""Tests to verify the frontend project setup files exist and are well-formed."""
+
+import json
+from pathlib import Path
+
+import pytest
+
+ROOT = Path(__file__).resolve().parent.parent
+
+
+def test_package_json_exists() -> None:
+ """package.json must exist at the repository root."""
+ assert (ROOT / "package.json").is_file()
+
+
+def test_package_json_has_required_dependencies() -> None:
+ """package.json must list all required dependencies."""
+ data = json.loads((ROOT / "package.json").read_text())
+ deps = set(data.get("dependencies", {}).keys())
+ dev_deps = set(data.get("devDependencies", {}).keys())
+ all_deps = deps | dev_deps
+
+ required = {
+ "react",
+ "react-dom",
+ "typescript",
+ "vite",
+ "@vitejs/plugin-react",
+ "vitest",
+ "@testing-library/react",
+ "@testing-library/jest-dom",
+ }
+ missing = required - all_deps
+ assert not missing, f"Missing dependencies in package.json: {missing}"
+
+
+def test_vite_config_exists() -> None:
+ """vite.config.ts must exist at the repository root."""
+ assert (ROOT / "vite.config.ts").is_file()
+
+
+def test_vite_config_contains_react_plugin() -> None:
+ """vite.config.ts must import and use the React plugin."""
+ content = (ROOT / "vite.config.ts").read_text()
+ assert "@vitejs/plugin-react" in content
+ assert "react()" in content
+
+
+def test_tsconfig_exists() -> None:
+ """tsconfig.json must exist at the repository root."""
+ assert (ROOT / "tsconfig.json").is_file()
+
+
+def test_tsconfig_node_exists() -> None:
+ """tsconfig.node.json must exist at the repository root."""
+ assert (ROOT / "tsconfig.node.json").is_file()
+
+
+def test_tsconfig_has_jsx() -> None:
+ """tsconfig.json must enable JSX for React."""
+ data = json.loads((ROOT / "tsconfig.json").read_text())
+ jsx = data.get("compilerOptions", {}).get("jsx", "")
+ assert "react" in jsx.lower(), f"Expected react-jsx, got: {jsx}"
+
+
+def test_index_html_exists() -> None:
+ """index.html must exist at the repository root."""
+ assert (ROOT / "index.html").is_file()
+
+
+def test_index_html_has_root_div() -> None:
+ """index.html must contain a div with id='root'."""
+ content = (ROOT / "index.html").read_text()
+ assert 'id="root"' in content
+
+
+def test_index_html_references_main_tsx() -> None:
+ """index.html must reference the main.tsx entry point."""
+ content = (ROOT / "index.html").read_text()
+ assert "src/main.tsx" in content
+
+
+def test_main_tsx_exists() -> None:
+ """src/main.tsx must exist."""
+ assert (ROOT / "src" / "main.tsx").is_file()
+
+
+def test_main_tsx_imports_app() -> None:
+ """src/main.tsx must import the App component."""
+ content = (ROOT / "src" / "main.tsx").read_text()
+ assert "import" in content and "App" in content
+
+
+def test_main_tsx_uses_createroot() -> None:
+ """src/main.tsx must use createRoot for React 18 rendering."""
+ content = (ROOT / "src" / "main.tsx").read_text()
+ assert "createRoot" in content
+
+
+def test_app_tsx_exists() -> None:
+ """src/App.tsx must exist."""
+ assert (ROOT / "src" / "App.tsx").is_file()
+
+
+def test_app_tsx_imports_todo_page() -> None:
+ """src/App.tsx must import TodoPage."""
+ content = (ROOT / "src" / "App.tsx").read_text()
+ assert "TodoPage" in content
+
+
+def test_todo_page_exists() -> None:
+ """src/pages/TodoPage.tsx must exist."""
+ assert (ROOT / "src" / "pages" / "TodoPage.tsx").is_file()
+
+
+def test_index_css_exists() -> None:
+ """src/index.css must exist."""
+ assert (ROOT / "src" / "index.css").is_file()
+
+
+def test_index_css_has_reset() -> None:
+ """src/index.css must contain a box-sizing reset."""
+ content = (ROOT / "src" / "index.css").read_text()
+ assert "box-sizing" in content
+
+
+def test_setup_tests_exists() -> None:
+ """src/setupTests.ts must exist for vitest setup."""
+ assert (ROOT / "src" / "setupTests.ts").is_file()
+
+
+def test_architecture_md_exists() -> None:
+ """ARCHITECTURE.md must exist at the repository root."""
+ assert (ROOT / "ARCHITECTURE.md").is_file()
+
+
+def test_architecture_md_has_todo_interface() -> None:
+ """ARCHITECTURE.md must define the Todo interface with required fields."""
+ content = (ROOT / "ARCHITECTURE.md").read_text()
+ assert "id: string" in content or "id:" in content
+ assert "text: string" in content or "text:" in content
+ assert "completed: boolean" in content or "completed:" in content
+ assert "createdAt: number" in content or "createdAt:" in content
+
+
+def test_architecture_md_has_filter_type() -> None:
+ """ARCHITECTURE.md must define the FilterType union."""
+ content = (ROOT / "ARCHITECTURE.md").read_text()
+ assert "FilterType" in content
+ assert "all" in content
+ assert "active" in content
+ assert "completed" in content
+
+
+def test_architecture_md_has_file_structure_section() -> None:
+ """ARCHITECTURE.md must contain a File Structure section."""
+ content = (ROOT / "ARCHITECTURE.md").read_text()
+ assert "File Structure" in content
diff --git a/tests/test_todo_helpers.py b/tests/test_todo_helpers.py
new file mode 100644
index 0000000..c1089ff
--- /dev/null
+++ b/tests/test_todo_helpers.py
@@ -0,0 +1,142 @@
+"""Tests that validate the todoHelpers TypeScript utility file.
+
+These tests read the source file and assert that the required
+function signatures and logic patterns are present.
+"""
+
+from __future__ import annotations
+
+from pathlib import Path
+
+import pytest
+
+ROOT_DIR: Path = Path(__file__).resolve().parent.parent
+HELPERS_PATH: Path = ROOT_DIR / "src" / "utils" / "todoHelpers.ts"
+
+
+@pytest.fixture()
+def helpers_content() -> str:
+ """Read and return the contents of src/utils/todoHelpers.ts."""
+ assert HELPERS_PATH.exists(), f"{HELPERS_PATH} does not exist"
+ return HELPERS_PATH.read_text(encoding="utf-8")
+
+
+class TestTodoHelpersFile:
+ """Verify the todoHelpers module exists and is well-structured."""
+
+ def test_file_exists(self) -> None:
+ """src/utils/todoHelpers.ts must exist."""
+ assert HELPERS_PATH.exists()
+
+ def test_imports_todo_type(self, helpers_content: str) -> None:
+ """The file must import from the types module."""
+ assert "from '../types/todo'" in helpers_content or 'from "../types/todo"' in helpers_content
+
+
+class TestGenerateId:
+ """Verify the generateId function."""
+
+ def test_export_exists(self, helpers_content: str) -> None:
+ """generateId must be an exported function."""
+ assert "export function generateId" in helpers_content
+
+ def test_returns_string(self, helpers_content: str) -> None:
+ """generateId must have a string return type."""
+ # Look for the return type annotation
+ assert "generateId(): string" in helpers_content
+
+ def test_uses_crypto_random_uuid(self, helpers_content: str) -> None:
+ """generateId should use crypto.randomUUID when available."""
+ assert "crypto.randomUUID" in helpers_content
+
+
+class TestCreateTodo:
+ """Verify the createTodo function."""
+
+ def test_export_exists(self, helpers_content: str) -> None:
+ """createTodo must be an exported function."""
+ assert "export function createTodo" in helpers_content
+
+ def test_accepts_text_param(self, helpers_content: str) -> None:
+ """createTodo must accept a text parameter of type string."""
+ assert "createTodo(text: string)" in helpers_content
+
+ def test_returns_todo(self, helpers_content: str) -> None:
+ """createTodo must return a Todo."""
+ assert "createTodo(text: string): Todo" in helpers_content
+
+ def test_trims_text(self, helpers_content: str) -> None:
+ """createTodo must trim the input text."""
+ assert ".trim()" in helpers_content
+
+ def test_rejects_empty_text(self, helpers_content: str) -> None:
+ """createTodo must throw on empty/whitespace-only text."""
+ assert "throw" in helpers_content.lower() or "Error" in helpers_content
+
+ def test_sets_completed_false(self, helpers_content: str) -> None:
+ """New todos must default to completed: false."""
+ assert "completed: false" in helpers_content
+
+
+class TestToggleTodo:
+ """Verify the toggleTodo function."""
+
+ def test_export_exists(self, helpers_content: str) -> None:
+ """toggleTodo must be an exported function."""
+ assert "export function toggleTodo" in helpers_content
+
+ def test_accepts_todo_param(self, helpers_content: str) -> None:
+ """toggleTodo must accept a Todo parameter."""
+ assert "toggleTodo(todo: Todo)" in helpers_content
+
+ def test_returns_todo(self, helpers_content: str) -> None:
+ """toggleTodo must return a Todo."""
+ assert "toggleTodo(todo: Todo): Todo" in helpers_content
+
+ def test_flips_completed(self, helpers_content: str) -> None:
+ """toggleTodo must negate the completed field."""
+ assert "!todo.completed" in helpers_content
+
+ def test_uses_spread(self, helpers_content: str) -> None:
+ """toggleTodo must use spread to avoid mutation."""
+ assert "...todo" in helpers_content
+
+
+class TestFilterTodos:
+ """Verify the filterTodos function."""
+
+ def test_export_exists(self, helpers_content: str) -> None:
+ """filterTodos must be an exported function."""
+ assert "export function filterTodos" in helpers_content
+
+ def test_accepts_todos_array(self, helpers_content: str) -> None:
+ """filterTodos must accept a Todo[] parameter."""
+ assert "todos: Todo[]" in helpers_content or "todos:Todo[]" in helpers_content
+
+ def test_accepts_filter_param(self, helpers_content: str) -> None:
+ """filterTodos must accept a FilterType parameter."""
+ assert "filter: FilterType" in helpers_content or "filter:FilterType" in helpers_content
+
+ def test_returns_todo_array(self, helpers_content: str) -> None:
+ """filterTodos must return Todo[]."""
+ assert "Todo[]" in helpers_content
+
+ def test_handles_active_filter(self, helpers_content: str) -> None:
+ """filterTodos must handle the 'active' case."""
+ assert "'active'" in helpers_content
+
+ def test_handles_completed_filter(self, helpers_content: str) -> None:
+ """filterTodos must handle the 'completed' case."""
+ assert "'completed'" in helpers_content
+
+ def test_handles_all_filter(self, helpers_content: str) -> None:
+ """filterTodos must handle the 'all' case."""
+ assert "'all'" in helpers_content
+
+ def test_todos_default_empty_array(self, helpers_content: str) -> None:
+ """todos parameter must default to an empty array."""
+ assert "todos: Todo[] = []" in helpers_content
+
+ def test_filter_default_all(self, helpers_content: str) -> None:
+ """filter parameter must default to 'all'."""
+ assert "filter: FilterType = 'all'" in helpers_content
diff --git a/tests/test_todo_page.tsx b/tests/test_todo_page.tsx
new file mode 100644
index 0000000..109aa43
--- /dev/null
+++ b/tests/test_todo_page.tsx
@@ -0,0 +1,29 @@
+/**
+ * Tests for the TodoPage placeholder component.
+ *
+ * Verifies that the placeholder renders its heading and message.
+ * These tests will be expanded when TodoPage gains real functionality.
+ */
+import { render, screen } from '@testing-library/react';
+import { describe, it, expect } from 'vitest';
+import TodoPage from '../src/pages/TodoPage';
+
+describe('TodoPage', () => {
+ it('renders the page heading', () => {
+ render();
+ const heading = screen.getByRole('heading', { level: 1 });
+ expect(heading).toHaveTextContent('Todo App');
+ });
+
+ it('renders the placeholder text', () => {
+ render();
+ expect(screen.getByText('Start adding your todos!')).toBeInTheDocument();
+ });
+
+ it('wraps content in a main element', () => {
+ render();
+ const main = screen.getByRole('main');
+ expect(main).toBeInTheDocument();
+ expect(main).toHaveClass('todo-page');
+ });
+});
diff --git a/tests/test_todo_types.py b/tests/test_todo_types.py
new file mode 100644
index 0000000..e0a1daf
--- /dev/null
+++ b/tests/test_todo_types.py
@@ -0,0 +1,66 @@
+"""Tests that validate the Todo TypeScript type definition file.
+
+These tests read the source file and assert that required interface
+fields and type exports are present.
+"""
+
+from __future__ import annotations
+
+import os
+from pathlib import Path
+
+import pytest
+
+ROOT_DIR: Path = Path(__file__).resolve().parent.parent
+TODO_TYPES_PATH: Path = ROOT_DIR / "src" / "types" / "todo.ts"
+
+
+@pytest.fixture()
+def todo_types_content() -> str:
+ """Read and return the contents of src/types/todo.ts."""
+ assert TODO_TYPES_PATH.exists(), f"{TODO_TYPES_PATH} does not exist"
+ return TODO_TYPES_PATH.read_text(encoding="utf-8")
+
+
+class TestTodoInterface:
+ """Verify the Todo interface is correctly defined."""
+
+ def test_file_exists(self) -> None:
+ """src/types/todo.ts must exist."""
+ assert TODO_TYPES_PATH.exists()
+
+ def test_exports_todo_interface(self, todo_types_content: str) -> None:
+ """The file must export a Todo interface."""
+ assert "export interface Todo" in todo_types_content
+
+ def test_id_field_is_string(self, todo_types_content: str) -> None:
+ """Todo.id must be typed as string."""
+ assert "id: string" in todo_types_content or "id:string" in todo_types_content
+
+ def test_text_field_is_string(self, todo_types_content: str) -> None:
+ """Todo.text must be typed as string."""
+ assert "text: string" in todo_types_content or "text:string" in todo_types_content
+
+ def test_completed_field_is_boolean(self, todo_types_content: str) -> None:
+ """Todo.completed must be typed as boolean."""
+ assert "completed: boolean" in todo_types_content or "completed:boolean" in todo_types_content
+
+ def test_created_at_field_is_number(self, todo_types_content: str) -> None:
+ """Todo.createdAt must be typed as number."""
+ assert "createdAt: number" in todo_types_content or "createdAt:number" in todo_types_content
+
+ def test_exports_filter_type(self, todo_types_content: str) -> None:
+ """The file must export a FilterType union type."""
+ assert "export type FilterType" in todo_types_content
+
+ def test_filter_type_includes_all(self, todo_types_content: str) -> None:
+ """FilterType must include 'all'."""
+ assert "'all'" in todo_types_content
+
+ def test_filter_type_includes_active(self, todo_types_content: str) -> None:
+ """FilterType must include 'active'."""
+ assert "'active'" in todo_types_content
+
+ def test_filter_type_includes_completed(self, todo_types_content: str) -> None:
+ """FilterType must include 'completed'."""
+ assert "'completed'" in todo_types_content
diff --git a/tests/types/todo.test.ts b/tests/types/todo.test.ts
new file mode 100644
index 0000000..5df6ada
--- /dev/null
+++ b/tests/types/todo.test.ts
@@ -0,0 +1,47 @@
+/**
+ * Tests for the Todo type definitions.
+ *
+ * These are structural/compile-time sanity checks that verify objects
+ * conforming to the Todo interface and TodoFilter type are accepted.
+ */
+
+import { describe, it, expect } from 'vitest';
+import { Todo, TodoFilter } from '../../src/types/todo';
+
+describe('Todo interface', () => {
+ it('should allow creating an object that satisfies the Todo shape', () => {
+ const todo: Todo = {
+ id: 'abc123',
+ text: 'Write tests',
+ completed: false,
+ createdAt: 1700000000000,
+ };
+
+ expect(todo.id).toBe('abc123');
+ expect(todo.text).toBe('Write tests');
+ expect(todo.completed).toBe(false);
+ expect(todo.createdAt).toBe(1700000000000);
+ });
+
+ it('should allow completed to be true', () => {
+ const todo: Todo = {
+ id: 'xyz789',
+ text: 'Done task',
+ completed: true,
+ createdAt: 1700000000000,
+ };
+
+ expect(todo.completed).toBe(true);
+ });
+});
+
+describe('TodoFilter type', () => {
+ it('should accept all valid filter values', () => {
+ const filters: TodoFilter[] = ['all', 'active', 'completed'];
+
+ expect(filters).toHaveLength(3);
+ expect(filters).toContain('all');
+ expect(filters).toContain('active');
+ expect(filters).toContain('completed');
+ });
+});
diff --git a/tests/utils/todoHelpers.test.ts b/tests/utils/todoHelpers.test.ts
new file mode 100644
index 0000000..6eb62d6
--- /dev/null
+++ b/tests/utils/todoHelpers.test.ts
@@ -0,0 +1,173 @@
+/**
+ * Tests for the pure todo helper utilities.
+ */
+
+import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
+import {
+ generateId,
+ createTodo,
+ toggleTodo,
+ filterTodos,
+} from '../../src/utils/todoHelpers';
+import { Todo } from '../../src/types/todo';
+
+// ---------------------------------------------------------------------------
+// generateId
+// ---------------------------------------------------------------------------
+
+describe('generateId', () => {
+ it('should return a non-empty string', () => {
+ const id = generateId();
+ expect(typeof id).toBe('string');
+ expect(id.length).toBeGreaterThan(0);
+ });
+
+ it('should return unique values on successive calls', () => {
+ const ids = new Set(Array.from({ length: 100 }, () => generateId()));
+ expect(ids.size).toBe(100);
+ });
+});
+
+// ---------------------------------------------------------------------------
+// createTodo
+// ---------------------------------------------------------------------------
+
+describe('createTodo', () => {
+ beforeEach(() => {
+ vi.useFakeTimers();
+ vi.setSystemTime(new Date('2024-01-15T12:00:00.000Z'));
+ });
+
+ afterEach(() => {
+ vi.useRealTimers();
+ });
+
+ it('should create a todo with the given text', () => {
+ const todo = createTodo('Buy groceries');
+ expect(todo.text).toBe('Buy groceries');
+ });
+
+ it('should create a todo that is not completed', () => {
+ const todo = createTodo('Buy groceries');
+ expect(todo.completed).toBe(false);
+ });
+
+ it('should set createdAt to the current timestamp', () => {
+ const now = Date.now();
+ const todo = createTodo('Buy groceries');
+ expect(todo.createdAt).toBe(now);
+ });
+
+ it('should assign a string id', () => {
+ const todo = createTodo('Buy groceries');
+ expect(typeof todo.id).toBe('string');
+ expect(todo.id.length).toBeGreaterThan(0);
+ });
+
+ it('should produce unique ids for different todos', () => {
+ const a = createTodo('Task A');
+ const b = createTodo('Task B');
+ expect(a.id).not.toBe(b.id);
+ });
+});
+
+// ---------------------------------------------------------------------------
+// toggleTodo
+// ---------------------------------------------------------------------------
+
+describe('toggleTodo', () => {
+ const baseTodo: Todo = {
+ id: 'test-1',
+ text: 'Test todo',
+ completed: false,
+ createdAt: 1700000000000,
+ };
+
+ it('should flip completed from false to true', () => {
+ const toggled = toggleTodo(baseTodo);
+ expect(toggled.completed).toBe(true);
+ });
+
+ it('should flip completed from true to false', () => {
+ const completedTodo: Todo = { ...baseTodo, completed: true };
+ const toggled = toggleTodo(completedTodo);
+ expect(toggled.completed).toBe(false);
+ });
+
+ it('should not mutate the original todo', () => {
+ const original: Todo = { ...baseTodo };
+ toggleTodo(original);
+ expect(original.completed).toBe(false);
+ });
+
+ it('should preserve all other fields', () => {
+ const toggled = toggleTodo(baseTodo);
+ expect(toggled.id).toBe(baseTodo.id);
+ expect(toggled.text).toBe(baseTodo.text);
+ expect(toggled.createdAt).toBe(baseTodo.createdAt);
+ });
+});
+
+// ---------------------------------------------------------------------------
+// filterTodos
+// ---------------------------------------------------------------------------
+
+describe('filterTodos', () => {
+ const todos: Todo[] = [
+ { id: '1', text: 'Active 1', completed: false, createdAt: 1 },
+ { id: '2', text: 'Completed 1', completed: true, createdAt: 2 },
+ { id: '3', text: 'Active 2', completed: false, createdAt: 3 },
+ { id: '4', text: 'Completed 2', completed: true, createdAt: 4 },
+ ];
+
+ it('should return all todos when filter is "all"', () => {
+ const result = filterTodos(todos, 'all');
+ expect(result).toHaveLength(4);
+ expect(result).toEqual(todos);
+ });
+
+ it('should return only active todos when filter is "active"', () => {
+ const result = filterTodos(todos, 'active');
+ expect(result).toHaveLength(2);
+ expect(result.every((t) => !t.completed)).toBe(true);
+ });
+
+ it('should return only completed todos when filter is "completed"', () => {
+ const result = filterTodos(todos, 'completed');
+ expect(result).toHaveLength(2);
+ expect(result.every((t) => t.completed)).toBe(true);
+ });
+
+ it('should return an empty array when given an empty list', () => {
+ expect(filterTodos([], 'all')).toEqual([]);
+ expect(filterTodos([], 'active')).toEqual([]);
+ expect(filterTodos([], 'completed')).toEqual([]);
+ });
+
+ it('should default to an empty array when todos is undefined', () => {
+ // Explicit test for the default parameter
+ expect(filterTodos(undefined as unknown as Todo[], 'all')).toEqual([]);
+ });
+
+ it('should not mutate the input array', () => {
+ const copy = [...todos];
+ filterTodos(todos, 'active');
+ expect(todos).toEqual(copy);
+ });
+
+ it('should return an empty array for active filter when all are completed', () => {
+ const allCompleted: Todo[] = [
+ { id: '1', text: 'Done 1', completed: true, createdAt: 1 },
+ { id: '2', text: 'Done 2', completed: true, createdAt: 2 },
+ ];
+ expect(filterTodos(allCompleted, 'active')).toEqual([]);
+ });
+
+ it('should return an empty array for completed filter when none are completed', () => {
+ const noneCompleted: Todo[] = [
+ { id: '1', text: 'Todo 1', completed: false, createdAt: 1 },
+ { id: '2', text: 'Todo 2', completed: false, createdAt: 2 },
+ ];
+ expect(filterTodos(noneCompleted, 'completed')).toEqual([]);
+ });
+});
diff --git a/tsconfig.json b/tsconfig.json
new file mode 100644
index 0000000..a7fc6fb
--- /dev/null
+++ b/tsconfig.json
@@ -0,0 +1,25 @@
+{
+ "compilerOptions": {
+ "target": "ES2020",
+ "useDefineForClassFields": true,
+ "lib": ["ES2020", "DOM", "DOM.Iterable"],
+ "module": "ESNext",
+ "skipLibCheck": true,
+
+ /* Bundler mode */
+ "moduleResolution": "bundler",
+ "allowImportingTsExtensions": true,
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "noEmit": true,
+ "jsx": "react-jsx",
+
+ /* Linting */
+ "strict": true,
+ "noUnusedLocals": true,
+ "noUnusedParameters": true,
+ "noFallthroughCasesInSwitch": true
+ },
+ "include": ["src"],
+ "references": [{ "path": "./tsconfig.node.json" }]
+}
diff --git a/tsconfig.node.json b/tsconfig.node.json
new file mode 100644
index 0000000..42872c5
--- /dev/null
+++ b/tsconfig.node.json
@@ -0,0 +1,10 @@
+{
+ "compilerOptions": {
+ "composite": true,
+ "skipLibCheck": true,
+ "module": "ESNext",
+ "moduleResolution": "bundler",
+ "allowSyntheticDefaultImports": true
+ },
+ "include": ["vite.config.ts"]
+}
diff --git a/vite.config.ts b/vite.config.ts
new file mode 100644
index 0000000..4ed43ca
--- /dev/null
+++ b/vite.config.ts
@@ -0,0 +1,24 @@
+import { defineConfig } from 'vite';
+import react from '@vitejs/plugin-react';
+
+///
+
+/**
+ * Vite configuration for the Todo frontend application.
+ *
+ * - Uses the React plugin for JSX transform and fast-refresh.
+ * - Configures Vitest with jsdom for component testing.
+ */
+export default defineConfig({
+ plugins: [react()],
+ server: {
+ port: 5173,
+ host: true,
+ },
+ test: {
+ globals: true,
+ environment: 'jsdom',
+ setupFiles: './src/setupTests.ts',
+ css: true,
+ },
+});