From c0d83359cc68ff274555304fcc01ab59112c9302 Mon Sep 17 00:00:00 2001 From: Ryan Westfall Date: Tue, 14 Jul 2026 18:03:24 -0700 Subject: [PATCH] Add unit tests (#11) (#14) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary - Add Jest/RTL unit tests for data models, Card, Footer, CustomTextField/PasswordField, CustomToastMessage, ConversationCard, AuthContext, and SignIn - Remove the broken CRA `App.test.tsx` placeholder that expected "learn react" - Add `npm run test:ci` for non-interactive CI runs Closes #11 ## Test plan - [ ] `cd llm-fe && npm install` - [ ] `npm run test:ci` — expect 9 suites / 32 tests passing Reviewed-on: https://git.aimloperations.com/ai_ml_operations/chat_web_app/pulls/14 --- llm-fe/package.json | 1 + llm-fe/src/App.test.tsx | 9 - .../src/llm-fe/components/Card/Card.test.tsx | 22 ++ .../ConversationCard.test.tsx | 56 +++++ .../CustomPasswordField.test.tsx | 33 +++ .../CustomTextField/CustomTextField.test.tsx | 40 ++++ .../CustomToastMessage.test.tsx | 10 + .../llm-fe/components/Footer/Footer.test.tsx | 20 ++ .../src/llm-fe/contexts/AuthContext.test.tsx | 71 +++++++ llm-fe/src/llm-fe/data.test.ts | 195 ++++++++++++++++++ .../src/llm-fe/pages/SignIn/SignIn.test.tsx | 112 ++++++++++ 11 files changed, 560 insertions(+), 9 deletions(-) delete mode 100644 llm-fe/src/App.test.tsx create mode 100644 llm-fe/src/llm-fe/components/Card/Card.test.tsx create mode 100644 llm-fe/src/llm-fe/components/ConversationCard/ConversationCard.test.tsx create mode 100644 llm-fe/src/llm-fe/components/CustomPasswordField/CustomPasswordField.test.tsx create mode 100644 llm-fe/src/llm-fe/components/CustomTextField/CustomTextField.test.tsx create mode 100644 llm-fe/src/llm-fe/components/CustomToastMessage/CustomToastMessage.test.tsx create mode 100644 llm-fe/src/llm-fe/components/Footer/Footer.test.tsx create mode 100644 llm-fe/src/llm-fe/contexts/AuthContext.test.tsx create mode 100644 llm-fe/src/llm-fe/data.test.ts create mode 100644 llm-fe/src/llm-fe/pages/SignIn/SignIn.test.tsx diff --git a/llm-fe/package.json b/llm-fe/package.json index 46b4939..4073984 100644 --- a/llm-fe/package.json +++ b/llm-fe/package.json @@ -41,6 +41,7 @@ "start": "NODE_ENV=development react-scripts start", "build": "NODE_ENV=production react-scripts build", "test": "NODE_ENV=development react-scripts test", + "test:ci": "CI=true NODE_ENV=development react-scripts test --watchAll=false --coverage=false", "eject": "react-scripts eject" }, "eslintConfig": { diff --git a/llm-fe/src/App.test.tsx b/llm-fe/src/App.test.tsx deleted file mode 100644 index 2a68616..0000000 --- a/llm-fe/src/App.test.tsx +++ /dev/null @@ -1,9 +0,0 @@ -import React from 'react'; -import { render, screen } from '@testing-library/react'; -import App from './App'; - -test('renders learn react link', () => { - render(); - const linkElement = screen.getByText(/learn react/i); - expect(linkElement).toBeInTheDocument(); -}); diff --git a/llm-fe/src/llm-fe/components/Card/Card.test.tsx b/llm-fe/src/llm-fe/components/Card/Card.test.tsx new file mode 100644 index 0000000..84cc59c --- /dev/null +++ b/llm-fe/src/llm-fe/components/Card/Card.test.tsx @@ -0,0 +1,22 @@ +import React from 'react'; +import { render, screen } from '@testing-library/react'; +import Card from './Card'; + +describe('Card', () => { + it('renders children inside card body', () => { + render( + + Card content + + ); + + expect(screen.getByText('Card content')).toBeInTheDocument(); + expect(screen.getByText('Card content').closest('.card')).toHaveClass('card-body'); + }); + + it('renders empty card when no children given', () => { + const { container } = render(); + expect(container.querySelector('.card.card-body')).toBeInTheDocument(); + expect(container.querySelector('.card.card-body')?.childElementCount).toBe(0); + }); +}); diff --git a/llm-fe/src/llm-fe/components/ConversationCard/ConversationCard.test.tsx b/llm-fe/src/llm-fe/components/ConversationCard/ConversationCard.test.tsx new file mode 100644 index 0000000..b3144fc --- /dev/null +++ b/llm-fe/src/llm-fe/components/ConversationCard/ConversationCard.test.tsx @@ -0,0 +1,56 @@ +import React from 'react'; +import { render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { ThemeProvider, createTheme } from '@mui/material/styles'; +import ConversationCard from './ConversationCard'; + +jest.mock('../../ui-kit/components/MDTypography', () => ({ + __esModule: true, + default: ({ children, onClick }: { children: React.ReactNode; onClick?: () => void }) => ( + + ), +})); + +const renderCard = (props: Partial> = {}) => { + const setSelectConversation = props.setSelectConversation ?? jest.fn(); + const deleteConversation = props.deleteConversation ?? jest.fn(); + + render( + + + + ); + + return { setSelectConversation, deleteConversation }; +}; + +describe('ConversationCard', () => { + it('renders conversation title', () => { + renderCard({ title: 'Project brainstorm' }); + expect(screen.getByText('Project brainstorm')).toBeInTheDocument(); + }); + + it('selects conversation when title clicked', async () => { + const user = userEvent.setup(); + const { setSelectConversation } = renderCard({ conversation_id: 12 }); + + await user.click(screen.getByText('My chat')); + expect(setSelectConversation).toHaveBeenCalledWith(12); + }); + + it('deletes conversation when delete button clicked', async () => { + const user = userEvent.setup(); + const { deleteConversation } = renderCard({ conversation_id: 8 }); + + await user.click(screen.getByLabelText(/delete/i)); + expect(deleteConversation).toHaveBeenCalledWith(8); + }); +}); diff --git a/llm-fe/src/llm-fe/components/CustomPasswordField/CustomPasswordField.test.tsx b/llm-fe/src/llm-fe/components/CustomPasswordField/CustomPasswordField.test.tsx new file mode 100644 index 0000000..b83c642 --- /dev/null +++ b/llm-fe/src/llm-fe/components/CustomPasswordField/CustomPasswordField.test.tsx @@ -0,0 +1,33 @@ +import React from 'react'; +import { render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { ThemeProvider, createTheme } from '@mui/material/styles'; +import CustomPasswordField from './CustomPasswordField'; + +describe('CustomPasswordField', () => { + it('renders a password input with label', () => { + render( + + + + ); + + const input = screen.getByLabelText('Password'); + expect(input).toBeInTheDocument(); + expect(input).toHaveAttribute('type', 'password'); + }); + + it('calls changeHandler when user types', async () => { + const user = userEvent.setup(); + const changeHandler = jest.fn(); + + render( + + + + ); + + await user.type(screen.getByLabelText('Password'), 'secret'); + expect(changeHandler).toHaveBeenCalled(); + }); +}); diff --git a/llm-fe/src/llm-fe/components/CustomTextField/CustomTextField.test.tsx b/llm-fe/src/llm-fe/components/CustomTextField/CustomTextField.test.tsx new file mode 100644 index 0000000..dcc85a4 --- /dev/null +++ b/llm-fe/src/llm-fe/components/CustomTextField/CustomTextField.test.tsx @@ -0,0 +1,40 @@ +import React from 'react'; +import { render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { ThemeProvider, createTheme } from '@mui/material/styles'; +import CustomTextField from './CustomTextField'; + +const renderField = (props: Partial> = {}) => { + const changeHandler = props.changeHandler ?? jest.fn(); + render( + + + + ); + return { changeHandler }; +}; + +describe('CustomTextField', () => { + it('renders labeled text input', () => { + renderField({ label: 'Email', name: 'email' }); + expect(screen.getByLabelText('Email')).toBeInTheDocument(); + }); + + it('calls changeHandler when user types', async () => { + const user = userEvent.setup(); + const { changeHandler } = renderField(); + + await user.type(screen.getByLabelText('Username'), 'abc'); + expect(changeHandler).toHaveBeenCalled(); + }); + + it('supports multiline mode', () => { + renderField({ label: 'Notes', name: 'notes', isMultline: true }); + expect(screen.getByLabelText('Notes').tagName).toBe('TEXTAREA'); + }); +}); diff --git a/llm-fe/src/llm-fe/components/CustomToastMessage/CustomToastMessage.test.tsx b/llm-fe/src/llm-fe/components/CustomToastMessage/CustomToastMessage.test.tsx new file mode 100644 index 0000000..94106ab --- /dev/null +++ b/llm-fe/src/llm-fe/components/CustomToastMessage/CustomToastMessage.test.tsx @@ -0,0 +1,10 @@ +import React from 'react'; +import { render, screen } from '@testing-library/react'; +import CustomToastMessage from './CustomeToastMessage'; + +describe('CustomToastMessage', () => { + it('renders the provided message in toast body', () => { + render(); + expect(screen.getByText('Saved successfully')).toBeInTheDocument(); + }); +}); diff --git a/llm-fe/src/llm-fe/components/Footer/Footer.test.tsx b/llm-fe/src/llm-fe/components/Footer/Footer.test.tsx new file mode 100644 index 0000000..f510cf9 --- /dev/null +++ b/llm-fe/src/llm-fe/components/Footer/Footer.test.tsx @@ -0,0 +1,20 @@ +import React from 'react'; +import { render, screen } from '@testing-library/react'; +import Footer from './Footer'; + +jest.mock('../../ui-kit/components/MDTypography', () => ({ + __esModule: true, + default: ({ children }: { children: React.ReactNode }) =>
{children}
, +})); + +describe('Footer', () => { + it('renders copyright and developer credit', () => { + render(