diff --git a/frontend/src/components/BillsTracker.test.tsx b/frontend/src/components/BillsTracker.test.tsx
new file mode 100644
index 0000000..3fc2a94
--- /dev/null
+++ b/frontend/src/components/BillsTracker.test.tsx
@@ -0,0 +1,54 @@
+import { render, screen } from '@testing-library/react'
+import { describe, it, expect, vi } from 'vitest'
+import { BillsTracker } from './BillsTracker'
+import type { BudgetDetail } from '@/lib/api'
+
+vi.mock('react-i18next', () => ({
+ useTranslation: () => ({ t: (key: string) => key, i18n: { language: 'en' } }),
+}))
+
+const emptyBudgetFixture: BudgetDetail = {
+ id: 'b1',
+ name: 'Test Budget',
+ start_date: '2025-01-01',
+ end_date: '2025-01-31',
+ currency: 'EUR',
+ carryover_amount: 0,
+ items: [],
+ totals: {
+ income_budget: 0,
+ income_actual: 0,
+ bills_budget: 0,
+ bills_actual: 0,
+ expenses_budget: 0,
+ expenses_actual: 0,
+ debts_budget: 0,
+ debts_actual: 0,
+ savings_budget: 0,
+ savings_actual: 0,
+ investments_budget: 0,
+ investments_actual: 0,
+ available: 0,
+ },
+}
+
+describe('BillsTracker', () => {
+ it('renders without crashing', () => {
+ render()
+ expect(screen.getByText('dashboard.billsTracker')).toBeInTheDocument()
+ })
+
+ it.skip('shows tinted skeleton when no bill items', () => {
+ // STATE-03: when budget.items contains no bill-type items, BillsTracker renders
+ // skeleton rows with a bill-category tint (palette bill light shade) instead of an empty table body
+ })
+
+ it.skip('flashes row green on successful inline save', () => {
+ // IXTN-03: after onUpdate resolves successfully, the updated row briefly gets a green
+ // flash class before returning to its normal appearance
+ })
+
+ it.skip('flashes row red on failed inline save', () => {
+ // IXTN-03: after onUpdate rejects, the row briefly gets a red flash class to signal failure
+ })
+})
diff --git a/frontend/src/components/BudgetSetup.test.tsx b/frontend/src/components/BudgetSetup.test.tsx
new file mode 100644
index 0000000..93973c5
--- /dev/null
+++ b/frontend/src/components/BudgetSetup.test.tsx
@@ -0,0 +1,37 @@
+import { render, screen } from '@testing-library/react'
+import { describe, it, expect, vi } from 'vitest'
+import { BudgetSetup } from './BudgetSetup'
+
+vi.mock('react-i18next', () => ({
+ useTranslation: () => ({ t: (key: string) => key, i18n: { language: 'en' } }),
+}))
+
+vi.mock('@/lib/api', () => ({
+ budgets: {
+ create: vi.fn().mockResolvedValue({ id: 'b1', name: 'Test Budget' }),
+ copyFrom: vi.fn().mockResolvedValue({}),
+ },
+}))
+
+describe('BudgetSetup', () => {
+ const defaultProps = {
+ existingBudgets: [],
+ onCreated: vi.fn(),
+ onCancel: vi.fn(),
+ }
+
+ it('renders without crashing', () => {
+ render()
+ expect(screen.getByText('budget.setup')).toBeInTheDocument()
+ })
+
+ it.skip('shows spinner in create button when saving', () => {
+ // IXTN-01: while saving is true, the create button renders a spinner (Loader2 icon)
+ // Steps: fill required fields, click create, assert spinner is visible before resolve
+ })
+
+ it.skip('disables create button when saving', () => {
+ // IXTN-01: while saving is true, the create button is disabled to prevent double-submit
+ // Steps: fill required fields, click create, assert button is disabled before resolve
+ })
+})
diff --git a/frontend/src/pages/CategoriesPage.test.tsx b/frontend/src/pages/CategoriesPage.test.tsx
new file mode 100644
index 0000000..8a4bf55
--- /dev/null
+++ b/frontend/src/pages/CategoriesPage.test.tsx
@@ -0,0 +1,48 @@
+import { render, screen } from '@testing-library/react'
+import { describe, it, expect, vi } from 'vitest'
+import { MemoryRouter } from 'react-router-dom'
+import { CategoriesPage } from './CategoriesPage'
+
+vi.mock('react-i18next', () => ({
+ useTranslation: () => ({ t: (key: string) => key, i18n: { language: 'en' } }),
+}))
+
+vi.mock('@/lib/api', () => ({
+ categories: {
+ list: vi.fn().mockResolvedValue([]),
+ create: vi.fn().mockResolvedValue({}),
+ update: vi.fn().mockResolvedValue({}),
+ delete: vi.fn().mockResolvedValue(undefined),
+ },
+}))
+
+describe('CategoriesPage', () => {
+ it('renders without crashing', () => {
+ render(
+
+
+
+ )
+ expect(screen.getByText('nav.categories')).toBeInTheDocument()
+ })
+
+ it.skip('opens confirmation dialog when delete button clicked', () => {
+ // IXTN-05: clicking the delete button on a category row should open a confirmation dialog
+ // before any deletion is performed, not immediately call categories.delete
+ })
+
+ it.skip('executes delete on confirm and shows spinner', () => {
+ // IXTN-05: after confirming the dialog, categories.delete is called and a spinner is shown
+ // on the confirm button while the async call is in flight
+ })
+
+ it.skip('shows error inline when delete fails', () => {
+ // IXTN-05: when categories.delete rejects, an error message is shown inline (role=alert)
+ // without closing the confirmation dialog
+ })
+
+ it.skip('shows empty state when no categories exist', () => {
+ // STATE-02: when categories.list returns [], the page renders an empty state element
+ // (e.g. a message or illustration) rather than just an empty table
+ })
+})
diff --git a/frontend/src/pages/DashboardPage.test.tsx b/frontend/src/pages/DashboardPage.test.tsx
new file mode 100644
index 0000000..4f23816
--- /dev/null
+++ b/frontend/src/pages/DashboardPage.test.tsx
@@ -0,0 +1,54 @@
+import { render, screen } from '@testing-library/react'
+import { describe, it, expect, vi } from 'vitest'
+import { MemoryRouter } from 'react-router-dom'
+import { DashboardPage } from './DashboardPage'
+
+vi.mock('react-i18next', () => ({
+ useTranslation: () => ({ t: (key: string) => key, i18n: { language: 'en' } }),
+}))
+
+vi.mock('@/lib/api', () => ({
+ budgets: {
+ list: vi.fn().mockResolvedValue([]),
+ get: vi.fn().mockResolvedValue(null),
+ create: vi.fn().mockResolvedValue({}),
+ copyFrom: vi.fn().mockResolvedValue({}),
+ },
+ budgetItems: {
+ update: vi.fn().mockResolvedValue({}),
+ },
+}))
+
+// Mock useBudgets so loading starts as false (skips the loading skeleton branch)
+vi.mock('@/hooks/useBudgets', () => ({
+ useBudgets: () => ({
+ list: [],
+ current: null,
+ loading: false,
+ fetchList: vi.fn().mockResolvedValue(undefined),
+ selectBudget: vi.fn().mockResolvedValue(undefined),
+ setCurrent: vi.fn(),
+ }),
+}))
+
+describe('DashboardPage', () => {
+ it('renders without crashing', () => {
+ render(
+
+
+
+ )
+ // When list is empty and not loading, the create budget button should be present
+ expect(screen.getByText('budget.create')).toBeInTheDocument()
+ })
+
+ it.skip('shows empty state with CTA when no budgets', () => {
+ // STATE-01: when budgets.list returns [] and loading is false, the dashboard renders
+ // a dedicated empty state component with a call-to-action to create the first budget
+ })
+
+ it.skip('shows loading skeleton while fetching', () => {
+ // STATE-03 (dashboard): while the initial budget list fetch is in flight,
+ // skeleton placeholders are displayed instead of the empty state or budget content
+ })
+})