From eec8a2ae484d5d0c31b2da4ad8ef5477b806b960 Mon Sep 17 00:00:00 2001 From: Opulence Chuks Date: Sat, 27 Jun 2026 12:44:27 +0100 Subject: [PATCH 1/3] feat: unsaved-changes guard and dirty indicator for settings --- src/app/settings/__tests__/page.test.tsx | 32 +++++++++ src/app/settings/page.tsx | 6 +- .../__tests__/useUnsavedChangesGuard.test.ts | 24 +++++++ src/hooks/useUnsavedChangesGuard.ts | 66 +++++++++++++++++++ 4 files changed, 127 insertions(+), 1 deletion(-) create mode 100644 src/app/settings/__tests__/page.test.tsx create mode 100644 src/hooks/__tests__/useUnsavedChangesGuard.test.ts create mode 100644 src/hooks/useUnsavedChangesGuard.ts diff --git a/src/app/settings/__tests__/page.test.tsx b/src/app/settings/__tests__/page.test.tsx new file mode 100644 index 00000000..8e1430fc --- /dev/null +++ b/src/app/settings/__tests__/page.test.tsx @@ -0,0 +1,32 @@ +import { render, screen, fireEvent, waitFor } from '@testing-library/react'; +import SettingsPage from '@/app/settings/page'; +import '@testing-library/jest-dom'; + +// Mock the useUnsavedChangesGuard hook to control isDirty state. +jest.mock('@/hooks/useUnsavedChangesGuard', () => ({ + useUnsavedChangesGuard: jest.fn(() => ({ isDirty: false, resetBaseline: jest.fn() })), +})); + +describe('SettingsPage unsaved changes UI', () => { + test('shows unsaved changes badge when dirty', async () => { + // Re-mock to return dirty after toggling. + const mockReset = jest.fn(); + const useUnsavedChangesGuard = require('@/hooks/useUnsavedChangesGuard').useUnsavedChangesGuard; + useUnsavedChangesGuard.mockImplementation(() => ({ isDirty: true, resetBaseline: mockReset })); + + render(); + // The badge should be visible. + expect(screen.getByText('Unsaved changes')).toBeInTheDocument(); + // Save button should be enabled. + const saveBtn = screen.getByRole('button', { name: /Save Preferences/i }); + expect(saveBtn).toBeEnabled(); + }); + + test('disables Save button when no changes', () => { + const useUnsavedChangesGuard = require('@/hooks/useUnsavedChangesGuard').useUnsavedChangesGuard; + useUnsavedChangesGuard.mockImplementation(() => ({ isDirty: false, resetBaseline: jest.fn() })); + render(); + const saveBtn = screen.getByRole('button', { name: /Save Preferences/i }); + expect(saveBtn).toBeDisabled(); + }); +}); diff --git a/src/app/settings/page.tsx b/src/app/settings/page.tsx index 4c313398..55b5cdf2 100644 --- a/src/app/settings/page.tsx +++ b/src/app/settings/page.tsx @@ -3,6 +3,7 @@ import React, { useState } from 'react' import { NotificationSection } from '@/components/settings/NotificationSection' import { NotificationToggle } from '@/components/settings/NotificationToggle' +import { useUnsavedChangesGuard } from '@/hooks/useUnsavedChangesGuard'; import { AppShellLayout } from '@/components/shell/AppShellLayout' import { AccountWalletSection } from '@/components/settings/AccountWalletSection' import { @@ -28,6 +29,7 @@ export default function SettingsPage() { }) const [isSaving, setIsSaving] = useState(false) + const { isDirty, resetBaseline } = useUnsavedChangesGuard(preferences); const [showSuccess, setShowSuccess] = useState(false) const handleToggle = (key: keyof typeof preferences) => { @@ -40,6 +42,7 @@ export default function SettingsPage() { setTimeout(() => { setIsSaving(false) setShowSuccess(true) + resetBaseline(); setTimeout(() => setShowSuccess(false), 3000) }, 1000) } @@ -69,6 +72,7 @@ export default function SettingsPage() {

Settings

+ {isDirty && Unsaved changes}

Manage your account, wallet, and notification preferences.

@@ -199,7 +203,7 @@ export default function SettingsPage() {