From b7f4fa2deb6432765d0fd724d31a8295d8acad1a Mon Sep 17 00:00:00 2001 From: Umang Khemka Date: Wed, 3 Jun 2026 14:46:10 +0530 Subject: [PATCH 1/8] feat: added the validation test --- .../pages/__tests__/nameValidation.test.tsx | 96 +++++++++++++++++++ 1 file changed, 96 insertions(+) create mode 100644 frontend/src/pages/__tests__/nameValidation.test.tsx diff --git a/frontend/src/pages/__tests__/nameValidation.test.tsx b/frontend/src/pages/__tests__/nameValidation.test.tsx new file mode 100644 index 000000000..dc3e0f5a2 --- /dev/null +++ b/frontend/src/pages/__tests__/nameValidation.test.tsx @@ -0,0 +1,96 @@ +import { render, screen } from '@/test-utils'; +import userEvent from '@testing-library/user-event'; +import { AvatarSelectionStep } from '@/components/OnboardingSteps/AvatarSelectionStep'; +import AccountSettingsCard from '@/pages/SettingsPage/components/AccountSettingsCard'; + +const VALID_30 = 'a'.repeat(30); +const INVALID_31 = 'a'.repeat(31); +const ERROR_MSG = 'A single word in your name cannot exceed 30 characters.'; + +beforeEach(() => localStorage.clear()); + +// AvatarSelectionStep + +describe('Name validation - AvatarSelectionStep', () => { + const setup = () => { + const user = userEvent.setup(); + render( + , + ); + const input = screen.getByPlaceholderText('Enter your name'); + return { user, input }; + }; + + test('30-character word is valid - no error shown', async () => { + const { user, input } = setup(); + await user.type(input, VALID_30); + expect(screen.queryByText(ERROR_MSG)).not.toBeInTheDocument(); + }); + + test('31-character word shows error and disables Next button', async () => { + const { user, input } = setup(); + await user.type(input, INVALID_31); + expect(screen.getByText(ERROR_MSG)).toBeInTheDocument(); + expect(screen.getByRole('button', { name: /next/i })).toBeDisabled(); + }); + + test('multi-space input is handled gracefully - no error', async () => { + const { user, input } = setup(); + await user.type(input, 'John Doe'); + expect(screen.queryByText(ERROR_MSG)).not.toBeInTheDocument(); + }); + + test('recovery - valid input after invalid clears error', async () => { + const { user, input } = setup(); + await user.type(input, INVALID_31); + expect(screen.getByText(ERROR_MSG)).toBeInTheDocument(); + await user.clear(input); + await user.type(input, 'John'); + expect(screen.queryByText(ERROR_MSG)).not.toBeInTheDocument(); + }); +}); + +// AccountSettingsCard + +describe('Name validation - AccountSettingsCard', () => { + const setup = () => { + const user = userEvent.setup(); + render(); + const input = screen.getByPlaceholderText('Enter your name'); + return { user, input }; + }; + + test('30 character word is valid – no error shown', async () => { + const { user, input } = setup(); + await user.type(input, VALID_30); + expect(screen.queryByText(ERROR_MSG)).not.toBeInTheDocument(); + }); + + test('31 character word shows error and disables Save Changes button', async () => { + const { user, input } = setup(); + await user.type(input, INVALID_31); + expect(screen.getByText(ERROR_MSG)).toBeInTheDocument(); + expect( + screen.getByRole('button', { name: /save changes/i }), + ).toBeDisabled(); + }); + + test('multi-space input is handled gracefully - no error', async () => { + const { user, input } = setup(); + await user.type(input, 'John Doe'); + expect(screen.queryByText(ERROR_MSG)).not.toBeInTheDocument(); + }); + + test('recovery - valid input after invalid clears error', async () => { + const { user, input } = setup(); + await user.type(input, INVALID_31); + expect(screen.getByText(ERROR_MSG)).toBeInTheDocument(); + await user.clear(input); + await user.type(input, 'John'); + expect(screen.queryByText(ERROR_MSG)).not.toBeInTheDocument(); + }); +}); \ No newline at end of file From 73f7c22f7342e41c100f7618e57f8a90655e72c6 Mon Sep 17 00:00:00 2001 From: Umang Khemka Date: Wed, 3 Jun 2026 15:11:39 +0530 Subject: [PATCH 2/8] fix: address CodeRabbit review - isolate button state via avatar pre-selection and use fireEvent --- .../pages/__tests__/nameValidation.test.tsx | 79 ++++++++++--------- 1 file changed, 40 insertions(+), 39 deletions(-) diff --git a/frontend/src/pages/__tests__/nameValidation.test.tsx b/frontend/src/pages/__tests__/nameValidation.test.tsx index dc3e0f5a2..c8ed1a952 100644 --- a/frontend/src/pages/__tests__/nameValidation.test.tsx +++ b/frontend/src/pages/__tests__/nameValidation.test.tsx @@ -1,5 +1,4 @@ -import { render, screen } from '@/test-utils'; -import userEvent from '@testing-library/user-event'; +import { render, screen, fireEvent } from '@/test-utils'; import { AvatarSelectionStep } from '@/components/OnboardingSteps/AvatarSelectionStep'; import AccountSettingsCard from '@/pages/SettingsPage/components/AccountSettingsCard'; @@ -9,11 +8,10 @@ const ERROR_MSG = 'A single word in your name cannot exceed 30 characters.'; beforeEach(() => localStorage.clear()); -// AvatarSelectionStep +// ─── AvatarSelectionStep ─────────────────────────────────────────────────── describe('Name validation - AvatarSelectionStep', () => { const setup = () => { - const user = userEvent.setup(); render( { currentStepDisplayIndex={0} />, ); + fireEvent.click(screen.getAllByAltText('Avatar')[0]); const input = screen.getByPlaceholderText('Enter your name'); - return { user, input }; + return { input }; }; - test('30-character word is valid - no error shown', async () => { - const { user, input } = setup(); - await user.type(input, VALID_30); + test('30-character word is valid - no error shown', () => { + const { input } = setup(); + fireEvent.change(input, { target: { value: VALID_30 } }); expect(screen.queryByText(ERROR_MSG)).not.toBeInTheDocument(); + expect(screen.getByRole('button', { name: /next/i })).not.toBeDisabled(); }); - test('31-character word shows error and disables Next button', async () => { - const { user, input } = setup(); - await user.type(input, INVALID_31); + test('31-character word shows error and disables Next button', () => { + const { input } = setup(); + fireEvent.change(input, { target: { value: INVALID_31 } }); expect(screen.getByText(ERROR_MSG)).toBeInTheDocument(); expect(screen.getByRole('button', { name: /next/i })).toBeDisabled(); }); - test('multi-space input is handled gracefully - no error', async () => { - const { user, input } = setup(); - await user.type(input, 'John Doe'); + test('multi-space input is handled gracefully - no error', () => { + const { input } = setup(); + fireEvent.change(input, { target: { value: 'John Doe' } }); expect(screen.queryByText(ERROR_MSG)).not.toBeInTheDocument(); }); - test('recovery - valid input after invalid clears error', async () => { - const { user, input } = setup(); - await user.type(input, INVALID_31); + test('recovery - valid input after invalid clears error and re-enables Next', () => { + const { input } = setup(); + fireEvent.change(input, { target: { value: INVALID_31 } }); expect(screen.getByText(ERROR_MSG)).toBeInTheDocument(); - await user.clear(input); - await user.type(input, 'John'); + expect(screen.getByRole('button', { name: /next/i })).toBeDisabled(); + fireEvent.change(input, { target: { value: 'John' } }); expect(screen.queryByText(ERROR_MSG)).not.toBeInTheDocument(); + expect(screen.getByRole('button', { name: /next/i })).not.toBeDisabled(); }); }); -// AccountSettingsCard +// ─── AccountSettingsCard ─────────────────────────────────────────────────── describe('Name validation - AccountSettingsCard', () => { const setup = () => { - const user = userEvent.setup(); render(); + fireEvent.click(screen.getAllByAltText('Avatar')[0]); const input = screen.getByPlaceholderText('Enter your name'); - return { user, input }; + return { input }; }; - test('30 character word is valid – no error shown', async () => { - const { user, input } = setup(); - await user.type(input, VALID_30); + test('30 character word is valid - no error shown', () => { + const { input } = setup(); + fireEvent.change(input, { target: { value: VALID_30 } }); expect(screen.queryByText(ERROR_MSG)).not.toBeInTheDocument(); + expect(screen.getByRole('button', { name: /save changes/i })).not.toBeDisabled(); }); - test('31 character word shows error and disables Save Changes button', async () => { - const { user, input } = setup(); - await user.type(input, INVALID_31); + test('31 character word shows error and disables Save Changes button', () => { + const { input } = setup(); + fireEvent.change(input, { target: { value: INVALID_31 } }); expect(screen.getByText(ERROR_MSG)).toBeInTheDocument(); - expect( - screen.getByRole('button', { name: /save changes/i }), - ).toBeDisabled(); + expect(screen.getByRole('button', { name: /save changes/i })).toBeDisabled(); }); - test('multi-space input is handled gracefully - no error', async () => { - const { user, input } = setup(); - await user.type(input, 'John Doe'); + test('multi-space input is handled gracefully - no error', () => { + const { input } = setup(); + fireEvent.change(input, { target: { value: 'John Doe' } }); expect(screen.queryByText(ERROR_MSG)).not.toBeInTheDocument(); }); - test('recovery - valid input after invalid clears error', async () => { - const { user, input } = setup(); - await user.type(input, INVALID_31); + test('recovery - valid input after invalid clears error and re-enables Save', () => { + const { input } = setup(); + fireEvent.change(input, { target: { value: INVALID_31 } }); expect(screen.getByText(ERROR_MSG)).toBeInTheDocument(); - await user.clear(input); - await user.type(input, 'John'); + expect(screen.getByRole('button', { name: /save changes/i })).toBeDisabled(); + fireEvent.change(input, { target: { value: 'John' } }); expect(screen.queryByText(ERROR_MSG)).not.toBeInTheDocument(); + expect(screen.getByRole('button', { name: /save changes/i })).not.toBeDisabled(); }); }); \ No newline at end of file From c056096765e984fcb480dc6a33758c6aa6a31400 Mon Sep 17 00:00:00 2001 From: Umang Khemka Date: Wed, 3 Jun 2026 15:18:36 +0530 Subject: [PATCH 3/8] fix: format nameValidation test file with prettier --- .../pages/__tests__/nameValidation.test.tsx | 22 +++++++++++++------ 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/frontend/src/pages/__tests__/nameValidation.test.tsx b/frontend/src/pages/__tests__/nameValidation.test.tsx index c8ed1a952..796264aaa 100644 --- a/frontend/src/pages/__tests__/nameValidation.test.tsx +++ b/frontend/src/pages/__tests__/nameValidation.test.tsx @@ -8,7 +8,7 @@ const ERROR_MSG = 'A single word in your name cannot exceed 30 characters.'; beforeEach(() => localStorage.clear()); -// ─── AvatarSelectionStep ─────────────────────────────────────────────────── +//AvatarSelectionStep describe('Name validation - AvatarSelectionStep', () => { const setup = () => { @@ -55,7 +55,7 @@ describe('Name validation - AvatarSelectionStep', () => { }); }); -// ─── AccountSettingsCard ─────────────────────────────────────────────────── +//AccountSettingsCard describe('Name validation - AccountSettingsCard', () => { const setup = () => { @@ -69,14 +69,18 @@ describe('Name validation - AccountSettingsCard', () => { const { input } = setup(); fireEvent.change(input, { target: { value: VALID_30 } }); expect(screen.queryByText(ERROR_MSG)).not.toBeInTheDocument(); - expect(screen.getByRole('button', { name: /save changes/i })).not.toBeDisabled(); + expect( + screen.getByRole('button', { name: /save changes/i }), + ).not.toBeDisabled(); }); test('31 character word shows error and disables Save Changes button', () => { const { input } = setup(); fireEvent.change(input, { target: { value: INVALID_31 } }); expect(screen.getByText(ERROR_MSG)).toBeInTheDocument(); - expect(screen.getByRole('button', { name: /save changes/i })).toBeDisabled(); + expect( + screen.getByRole('button', { name: /save changes/i }), + ).toBeDisabled(); }); test('multi-space input is handled gracefully - no error', () => { @@ -89,9 +93,13 @@ describe('Name validation - AccountSettingsCard', () => { const { input } = setup(); fireEvent.change(input, { target: { value: INVALID_31 } }); expect(screen.getByText(ERROR_MSG)).toBeInTheDocument(); - expect(screen.getByRole('button', { name: /save changes/i })).toBeDisabled(); + expect( + screen.getByRole('button', { name: /save changes/i }), + ).toBeDisabled(); fireEvent.change(input, { target: { value: 'John' } }); expect(screen.queryByText(ERROR_MSG)).not.toBeInTheDocument(); - expect(screen.getByRole('button', { name: /save changes/i })).not.toBeDisabled(); + expect( + screen.getByRole('button', { name: /save changes/i }), + ).not.toBeDisabled(); }); -}); \ No newline at end of file +}); From 8d043e5d2dcbf830e4bbf4c842c58933a37770e1 Mon Sep 17 00:00:00 2001 From: Umang Khemka Date: Wed, 3 Jun 2026 16:31:12 +0530 Subject: [PATCH 4/8] style: apply black formatting to images.py files --- backend/app/database/images.py | 39 +++++++++++++++------------------- backend/app/routes/images.py | 14 ++++++------ 2 files changed, 24 insertions(+), 29 deletions(-) diff --git a/backend/app/database/images.py b/backend/app/database/images.py index 15c1d374d..02f3a12ef 100644 --- a/backend/app/database/images.py +++ b/backend/app/database/images.py @@ -62,8 +62,7 @@ def db_create_images_table() -> None: cursor = conn.cursor() # Create new images table with merged fields including Memories feature columns - cursor.execute( - """ + cursor.execute(""" CREATE TABLE IF NOT EXISTS images ( id TEXT PRIMARY KEY, path VARCHAR UNIQUE, @@ -77,8 +76,7 @@ def db_create_images_table() -> None: captured_at DATETIME, FOREIGN KEY (folder_id) REFERENCES folders(folder_id) ON DELETE CASCADE ) - """ - ) + """) # Create indexes for Memories feature queries cursor.execute("CREATE INDEX IF NOT EXISTS ix_images_latitude ON images(latitude)") @@ -93,8 +91,7 @@ def db_create_images_table() -> None: ) # Create new image_classes junction table - cursor.execute( - """ + cursor.execute(""" CREATE TABLE IF NOT EXISTS image_classes ( image_id TEXT, class_id INTEGER, @@ -102,8 +99,7 @@ def db_create_images_table() -> None: FOREIGN KEY (image_id) REFERENCES images(id) ON DELETE CASCADE, FOREIGN KEY (class_id) REFERENCES mappings(class_id) ON DELETE CASCADE ) - """ - ) + """) conn.commit() conn.close() @@ -265,15 +261,13 @@ def db_get_untagged_images() -> List[UntaggedImageRecord]: cursor = conn.cursor() try: - cursor.execute( - """ + cursor.execute(""" SELECT i.id, i.path, i.folder_id, i.thumbnailPath, i.metadata FROM images i JOIN folders f ON i.folder_id = f.folder_id WHERE f.AI_Tagging = TRUE AND i.isTagged = FALSE - """ - ) + """) results = cursor.fetchall() @@ -457,6 +451,7 @@ def db_toggle_image_favourite_status(image_id: str) -> bool: finally: conn.close() + def db_get_image_by_id(image_id: str) -> Optional[dict]: """ Get a single image by ID with its favorite status. @@ -464,11 +459,14 @@ def db_get_image_by_id(image_id: str) -> Optional[dict]: conn = _connect() cursor = conn.cursor() try: - cursor.execute(""" + cursor.execute( + """ SELECT id, path, folder_id, thumbnailPath, metadata, isTagged, isFavourite FROM images WHERE id = ? - """, (image_id,)) + """, + (image_id,), + ) row = cursor.fetchone() if not row: return None @@ -488,6 +486,7 @@ def db_get_image_by_id(image_id: str) -> Optional[dict]: finally: conn.close() + # ============================================================================ # MEMORIES FEATURE - Location and Time-based Queries # ============================================================================ @@ -749,8 +748,7 @@ def db_get_images_with_location() -> List[dict]: cursor = conn.cursor() try: - cursor.execute( - """ + cursor.execute(""" SELECT i.id, i.path, @@ -770,8 +768,7 @@ def db_get_images_with_location() -> List[dict]: AND i.longitude IS NOT NULL GROUP BY i.id ORDER BY i.captured_at DESC - """ - ) + """) results = cursor.fetchall() @@ -816,8 +813,7 @@ def db_get_all_images_for_memories() -> List[dict]: cursor = conn.cursor() try: - cursor.execute( - """ + cursor.execute(""" SELECT i.id, i.path, @@ -835,8 +831,7 @@ def db_get_all_images_for_memories() -> List[dict]: LEFT JOIN mappings m ON ic.class_id = m.class_id GROUP BY i.id ORDER BY i.captured_at DESC - """ - ) + """) results = cursor.fetchall() diff --git a/backend/app/routes/images.py b/backend/app/routes/images.py index f00b0a941..a7f9fb332 100644 --- a/backend/app/routes/images.py +++ b/backend/app/routes/images.py @@ -7,7 +7,6 @@ from app.database.images import db_toggle_image_favourite_status, db_get_image_by_id from app.logging.setup_logging import get_logger - # Initialize logger logger = get_logger(__name__) router = APIRouter() @@ -106,15 +105,15 @@ def toggle_favourite(req: ToggleFavouriteRequest): success = db_toggle_image_favourite_status(image_id) if not success: raise HTTPException( - status_code=status.HTTP_404_NOT_FOUND, - detail="Image not found or failed to toggle" + status_code=status.HTTP_404_NOT_FOUND, + detail="Image not found or failed to toggle", ) # Fetch updated status to return image = db_get_image_by_id(image_id) if not image: raise HTTPException( - status_code=status.HTTP_404_NOT_FOUND, - detail="Image not found after toggle" + status_code=status.HTTP_404_NOT_FOUND, + detail="Image not found after toggle", ) return { "success": True, @@ -126,10 +125,11 @@ def toggle_favourite(req: ToggleFavouriteRequest): except Exception as e: logger.error(f"error in /toggle-favourite route: {e}") raise HTTPException( - status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - detail=f"Internal server error: {e}" + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=f"Internal server error: {e}", ) + class ImageInfoResponse(BaseModel): id: str path: str From b5deacb9f4fd49416ceeeb676f48a3ebc6dcbd40 Mon Sep 17 00:00:00 2001 From: Umang Khemka Date: Wed, 3 Jun 2026 21:21:00 +0530 Subject: [PATCH 5/8] style: format images.py with black --- backend/app/database/images.py | 30 ++++++++++++++++++++---------- 1 file changed, 20 insertions(+), 10 deletions(-) diff --git a/backend/app/database/images.py b/backend/app/database/images.py index 02f3a12ef..76149202b 100644 --- a/backend/app/database/images.py +++ b/backend/app/database/images.py @@ -62,7 +62,8 @@ def db_create_images_table() -> None: cursor = conn.cursor() # Create new images table with merged fields including Memories feature columns - cursor.execute(""" + cursor.execute( + """ CREATE TABLE IF NOT EXISTS images ( id TEXT PRIMARY KEY, path VARCHAR UNIQUE, @@ -76,7 +77,8 @@ def db_create_images_table() -> None: captured_at DATETIME, FOREIGN KEY (folder_id) REFERENCES folders(folder_id) ON DELETE CASCADE ) - """) + """ + ) # Create indexes for Memories feature queries cursor.execute("CREATE INDEX IF NOT EXISTS ix_images_latitude ON images(latitude)") @@ -91,7 +93,8 @@ def db_create_images_table() -> None: ) # Create new image_classes junction table - cursor.execute(""" + cursor.execute( + """ CREATE TABLE IF NOT EXISTS image_classes ( image_id TEXT, class_id INTEGER, @@ -99,7 +102,8 @@ def db_create_images_table() -> None: FOREIGN KEY (image_id) REFERENCES images(id) ON DELETE CASCADE, FOREIGN KEY (class_id) REFERENCES mappings(class_id) ON DELETE CASCADE ) - """) + """ + ) conn.commit() conn.close() @@ -261,13 +265,15 @@ def db_get_untagged_images() -> List[UntaggedImageRecord]: cursor = conn.cursor() try: - cursor.execute(""" + cursor.execute( + """ SELECT i.id, i.path, i.folder_id, i.thumbnailPath, i.metadata FROM images i JOIN folders f ON i.folder_id = f.folder_id WHERE f.AI_Tagging = TRUE AND i.isTagged = FALSE - """) + """ + ) results = cursor.fetchall() @@ -748,7 +754,8 @@ def db_get_images_with_location() -> List[dict]: cursor = conn.cursor() try: - cursor.execute(""" + cursor.execute( + """ SELECT i.id, i.path, @@ -768,7 +775,8 @@ def db_get_images_with_location() -> List[dict]: AND i.longitude IS NOT NULL GROUP BY i.id ORDER BY i.captured_at DESC - """) + """ + ) results = cursor.fetchall() @@ -813,7 +821,8 @@ def db_get_all_images_for_memories() -> List[dict]: cursor = conn.cursor() try: - cursor.execute(""" + cursor.execute( + """ SELECT i.id, i.path, @@ -831,7 +840,8 @@ def db_get_all_images_for_memories() -> List[dict]: LEFT JOIN mappings m ON ic.class_id = m.class_id GROUP BY i.id ORDER BY i.captured_at DESC - """) + """ + ) results = cursor.fetchall() From 37b0b70048995c6e2594a5f1d5e77886eafc9f04 Mon Sep 17 00:00:00 2001 From: Umang Khemka Date: Mon, 8 Jun 2026 15:14:46 +0530 Subject: [PATCH 6/8] test: split name validation into per-component test files under components/__tests__ --- .../__tests__/AccountSettingsCard.test.tsx | 48 ++++++++ .../__tests__/AvatarSelectionStep.test.tsx | 52 +++++++++ .../pages/__tests__/nameValidation.test.tsx | 105 ------------------ 3 files changed, 100 insertions(+), 105 deletions(-) create mode 100644 frontend/src/components/__tests__/AccountSettingsCard.test.tsx create mode 100644 frontend/src/components/__tests__/AvatarSelectionStep.test.tsx delete mode 100644 frontend/src/pages/__tests__/nameValidation.test.tsx diff --git a/frontend/src/components/__tests__/AccountSettingsCard.test.tsx b/frontend/src/components/__tests__/AccountSettingsCard.test.tsx new file mode 100644 index 000000000..0ebd94e64 --- /dev/null +++ b/frontend/src/components/__tests__/AccountSettingsCard.test.tsx @@ -0,0 +1,48 @@ +import { render, screen } from '@/test-utils'; +import userEvent from '@testing-library/user-event'; +import AccountSettingsCard from '@/pages/SettingsPage/components/AccountSettingsCard'; + +const VALID_30 = 'a'.repeat(30); +const INVALID_31 = 'a'.repeat(31); +const ERROR_MSG = 'A single word in your name cannot exceed 30 characters.'; + +beforeEach(() => localStorage.clear()); + +describe('Name validation - AccountSettingsCard', () => { + const setup = () => { + const user = userEvent.setup(); + render(); + const input = screen.getByPlaceholderText('Enter your name'); + return { user, input }; + }; + + test('30-character word is valid - no error shown', async () => { + const { user, input } = setup(); + await user.type(input, VALID_30); + expect(screen.queryByText(ERROR_MSG)).not.toBeInTheDocument(); + }); + + test('31-character word shows error and disables Save Changes button', async () => { + const { user, input } = setup(); + await user.type(input, INVALID_31); + expect(screen.getByText(ERROR_MSG)).toBeInTheDocument(); + expect( + screen.getByRole('button', { name: /save changes/i }), + ).toBeDisabled(); + }); + + test('multi-space input is handled gracefully - no error', async () => { + const { user, input } = setup(); + await user.type(input, 'John Doe'); + expect(screen.queryByText(ERROR_MSG)).not.toBeInTheDocument(); + }); + + test('recovery - valid input after invalid clears error', async () => { + const { user, input } = setup(); + await user.type(input, INVALID_31); + expect(screen.getByText(ERROR_MSG)).toBeInTheDocument(); + await user.clear(input); + await user.type(input, 'John'); + expect(screen.queryByText(ERROR_MSG)).not.toBeInTheDocument(); + }); +}); \ No newline at end of file diff --git a/frontend/src/components/__tests__/AvatarSelectionStep.test.tsx b/frontend/src/components/__tests__/AvatarSelectionStep.test.tsx new file mode 100644 index 000000000..bb9a495b6 --- /dev/null +++ b/frontend/src/components/__tests__/AvatarSelectionStep.test.tsx @@ -0,0 +1,52 @@ +import { render, screen } from '@/test-utils'; +import userEvent from '@testing-library/user-event'; +import { AvatarSelectionStep } from '@/components/OnboardingSteps/AvatarSelectionStep'; + +const VALID_30 = 'a'.repeat(30); +const INVALID_31 = 'a'.repeat(31); +const ERROR_MSG = 'A single word in your name cannot exceed 30 characters.'; + +beforeEach(() => localStorage.clear()); + +describe('Name validation - AvatarSelectionStep', () => { + const setup = () => { + const user = userEvent.setup(); + render( + , + ); + const input = screen.getByPlaceholderText('Enter your name'); + return { user, input }; + }; + + test('30-character word is valid - no error shown', async () => { + const { user, input } = setup(); + await user.type(input, VALID_30); + expect(screen.queryByText(ERROR_MSG)).not.toBeInTheDocument(); + }); + + test('31-character word shows error and disables Next button', async () => { + const { user, input } = setup(); + await user.type(input, INVALID_31); + expect(screen.getByText(ERROR_MSG)).toBeInTheDocument(); + expect(screen.getByRole('button', { name: /next/i })).toBeDisabled(); + }); + + test('multi-space input is handled gracefully - no error', async () => { + const { user, input } = setup(); + await user.type(input, 'John Doe'); + expect(screen.queryByText(ERROR_MSG)).not.toBeInTheDocument(); + }); + + test('recovery - valid input after invalid clears error', async () => { + const { user, input } = setup(); + await user.type(input, INVALID_31); + expect(screen.getByText(ERROR_MSG)).toBeInTheDocument(); + await user.clear(input); + await user.type(input, 'John'); + expect(screen.queryByText(ERROR_MSG)).not.toBeInTheDocument(); + }); +}); \ No newline at end of file diff --git a/frontend/src/pages/__tests__/nameValidation.test.tsx b/frontend/src/pages/__tests__/nameValidation.test.tsx deleted file mode 100644 index 796264aaa..000000000 --- a/frontend/src/pages/__tests__/nameValidation.test.tsx +++ /dev/null @@ -1,105 +0,0 @@ -import { render, screen, fireEvent } from '@/test-utils'; -import { AvatarSelectionStep } from '@/components/OnboardingSteps/AvatarSelectionStep'; -import AccountSettingsCard from '@/pages/SettingsPage/components/AccountSettingsCard'; - -const VALID_30 = 'a'.repeat(30); -const INVALID_31 = 'a'.repeat(31); -const ERROR_MSG = 'A single word in your name cannot exceed 30 characters.'; - -beforeEach(() => localStorage.clear()); - -//AvatarSelectionStep - -describe('Name validation - AvatarSelectionStep', () => { - const setup = () => { - render( - , - ); - fireEvent.click(screen.getAllByAltText('Avatar')[0]); - const input = screen.getByPlaceholderText('Enter your name'); - return { input }; - }; - - test('30-character word is valid - no error shown', () => { - const { input } = setup(); - fireEvent.change(input, { target: { value: VALID_30 } }); - expect(screen.queryByText(ERROR_MSG)).not.toBeInTheDocument(); - expect(screen.getByRole('button', { name: /next/i })).not.toBeDisabled(); - }); - - test('31-character word shows error and disables Next button', () => { - const { input } = setup(); - fireEvent.change(input, { target: { value: INVALID_31 } }); - expect(screen.getByText(ERROR_MSG)).toBeInTheDocument(); - expect(screen.getByRole('button', { name: /next/i })).toBeDisabled(); - }); - - test('multi-space input is handled gracefully - no error', () => { - const { input } = setup(); - fireEvent.change(input, { target: { value: 'John Doe' } }); - expect(screen.queryByText(ERROR_MSG)).not.toBeInTheDocument(); - }); - - test('recovery - valid input after invalid clears error and re-enables Next', () => { - const { input } = setup(); - fireEvent.change(input, { target: { value: INVALID_31 } }); - expect(screen.getByText(ERROR_MSG)).toBeInTheDocument(); - expect(screen.getByRole('button', { name: /next/i })).toBeDisabled(); - fireEvent.change(input, { target: { value: 'John' } }); - expect(screen.queryByText(ERROR_MSG)).not.toBeInTheDocument(); - expect(screen.getByRole('button', { name: /next/i })).not.toBeDisabled(); - }); -}); - -//AccountSettingsCard - -describe('Name validation - AccountSettingsCard', () => { - const setup = () => { - render(); - fireEvent.click(screen.getAllByAltText('Avatar')[0]); - const input = screen.getByPlaceholderText('Enter your name'); - return { input }; - }; - - test('30 character word is valid - no error shown', () => { - const { input } = setup(); - fireEvent.change(input, { target: { value: VALID_30 } }); - expect(screen.queryByText(ERROR_MSG)).not.toBeInTheDocument(); - expect( - screen.getByRole('button', { name: /save changes/i }), - ).not.toBeDisabled(); - }); - - test('31 character word shows error and disables Save Changes button', () => { - const { input } = setup(); - fireEvent.change(input, { target: { value: INVALID_31 } }); - expect(screen.getByText(ERROR_MSG)).toBeInTheDocument(); - expect( - screen.getByRole('button', { name: /save changes/i }), - ).toBeDisabled(); - }); - - test('multi-space input is handled gracefully - no error', () => { - const { input } = setup(); - fireEvent.change(input, { target: { value: 'John Doe' } }); - expect(screen.queryByText(ERROR_MSG)).not.toBeInTheDocument(); - }); - - test('recovery - valid input after invalid clears error and re-enables Save', () => { - const { input } = setup(); - fireEvent.change(input, { target: { value: INVALID_31 } }); - expect(screen.getByText(ERROR_MSG)).toBeInTheDocument(); - expect( - screen.getByRole('button', { name: /save changes/i }), - ).toBeDisabled(); - fireEvent.change(input, { target: { value: 'John' } }); - expect(screen.queryByText(ERROR_MSG)).not.toBeInTheDocument(); - expect( - screen.getByRole('button', { name: /save changes/i }), - ).not.toBeDisabled(); - }); -}); From 521cae409621e1c2289e3ece9a96ed4b03898ea9 Mon Sep 17 00:00:00 2001 From: Umang Khemka Date: Mon, 8 Jun 2026 15:17:57 +0530 Subject: [PATCH 7/8] style: fix prettier formatting in component test files --- frontend/src/components/__tests__/AccountSettingsCard.test.tsx | 2 +- frontend/src/components/__tests__/AvatarSelectionStep.test.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/src/components/__tests__/AccountSettingsCard.test.tsx b/frontend/src/components/__tests__/AccountSettingsCard.test.tsx index 0ebd94e64..4813c7681 100644 --- a/frontend/src/components/__tests__/AccountSettingsCard.test.tsx +++ b/frontend/src/components/__tests__/AccountSettingsCard.test.tsx @@ -45,4 +45,4 @@ describe('Name validation - AccountSettingsCard', () => { await user.type(input, 'John'); expect(screen.queryByText(ERROR_MSG)).not.toBeInTheDocument(); }); -}); \ No newline at end of file +}); diff --git a/frontend/src/components/__tests__/AvatarSelectionStep.test.tsx b/frontend/src/components/__tests__/AvatarSelectionStep.test.tsx index bb9a495b6..e93319af7 100644 --- a/frontend/src/components/__tests__/AvatarSelectionStep.test.tsx +++ b/frontend/src/components/__tests__/AvatarSelectionStep.test.tsx @@ -49,4 +49,4 @@ describe('Name validation - AvatarSelectionStep', () => { await user.type(input, 'John'); expect(screen.queryByText(ERROR_MSG)).not.toBeInTheDocument(); }); -}); \ No newline at end of file +}); From 03a6d0a308646d100b84279078640e8ac60d49af Mon Sep 17 00:00:00 2001 From: ROHAN PANDEY <95585299+rohan-pandeyy@users.noreply.github.com> Date: Wed, 10 Jun 2026 22:52:23 +0530 Subject: [PATCH 8/8] undo settings page test file movement --- .../src/{components => pages}/__tests__/SettingsPage.test.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename frontend/src/{components => pages}/__tests__/SettingsPage.test.tsx (98%) diff --git a/frontend/src/components/__tests__/SettingsPage.test.tsx b/frontend/src/pages/__tests__/SettingsPage.test.tsx similarity index 98% rename from frontend/src/components/__tests__/SettingsPage.test.tsx rename to frontend/src/pages/__tests__/SettingsPage.test.tsx index defa4cab5..345edd456 100644 --- a/frontend/src/components/__tests__/SettingsPage.test.tsx +++ b/frontend/src/pages/__tests__/SettingsPage.test.tsx @@ -1,6 +1,6 @@ import { render, screen } from '@/test-utils'; import userEvent from '@testing-library/user-event'; -import Settings from '@/pages/SettingsPage/Settings'; +import Settings from '../SettingsPage/Settings'; describe('Settings Page', () => { // shared setup for all tests