From da208287e8c0ac89400168f6b5a4b69ff82cdacb Mon Sep 17 00:00:00 2001 From: GK Bishnoi Date: Sun, 26 Jan 2025 10:57:58 +0530 Subject: [PATCH] Fix: Venue details now update without requiring a name change (#3302) * implement a Visit button in the Joined Organizations filter * Resolved: Potential Issue Identified by CodeRabbit AI * Resolved: Potential Issue Identified by CodeRabbit AI * Resolved: Potential Issue Identified by CodeRabbit AI * Resolved: Potential Issue Identified by CodeRabbit AI * Update src/components/OrganizationCard/OrganizationCard.tsx Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> * Added test cases * Added test cases * added tsdoc * resolve: all issue * Fix: Venue details now update without requiring a name change * Improve Test Cases * Add Codecov upload step * removing changes from pull-request.yml * New Tests for Uncovered Lines * Added additional test cases * Added additional test cases * added comments in OrganizationCard.spec.tsx * added test cases * solved conflict files * Update src/components/OrganizationCard/OrganizationCard.tsx Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> * Added test cases * added tsdoc * resolve: all issue * Fix: Venue details now update without requiring a name change * Improve Test Cases * Add Codecov upload step * removing changes from pull-request.yml * New Tests for Uncovered Lines * Added additional test cases * Added additional test cases * added comments in OrganizationCard.spec.tsx * added test cases * solved conflict files * Resolved Conflict * Resolved Conflict conflicting file * Resolved Conflict conflicting file * Resolved Conflict conflicting file * Resolved Conflict conflicting file * Resolved Conflict conflicting file * Resolved Conflict conflicting file * Resolved issue * Resolved issue * Resolved issue * Solving Conflict files * unchanging Sensitive files --------- Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- DOCUMENTATION.md | 35 +- .../SignOut/SignOut/functions/default.md | 8 +- .../Venues/VenueModal/functions/default.md | 2 +- .../variables/EMPTY_MOCKS.md | 55 + .../variables/ERROR_MOCKS.md | 47 + .../Leaderboard.mocks/variables/MOCKS.md | 55 + src/components/Venues/VenueModal.spec.tsx | 1367 ++++++++++++++--- src/components/Venues/VenueModal.tsx | 127 +- .../Leaderboard/Leaderboard.mocks.ts | 198 +++ 9 files changed, 1607 insertions(+), 287 deletions(-) create mode 100644 docs/docs/auto-docs/screens/OrganizationDashboard/Leaderboard/Leaderboard.mocks/variables/EMPTY_MOCKS.md create mode 100644 docs/docs/auto-docs/screens/OrganizationDashboard/Leaderboard/Leaderboard.mocks/variables/ERROR_MOCKS.md create mode 100644 docs/docs/auto-docs/screens/OrganizationDashboard/Leaderboard/Leaderboard.mocks/variables/MOCKS.md create mode 100644 src/screens/OrganizationDashboard/Leaderboard/Leaderboard.mocks.ts diff --git a/DOCUMENTATION.md b/DOCUMENTATION.md index 1b35f1963d..3b3e5f29e4 100644 --- a/DOCUMENTATION.md +++ b/DOCUMENTATION.md @@ -1,27 +1,32 @@ # Documentation - Welcome to our documentation guide. Here are some useful tips you need to know! # Table of Contents -- [Documentation](#documentation) -- [Table of Contents](#table-of-contents) - - [Where to find our documentation](#where-to-find-our-documentation) +- [Where to find our documentation](#where-to-find-our-documentation) +- [How to use Docusaurus](#how-to-use-docusaurus) +- [Other information](#other-information) ## Where to find our documentation -Our documentation can be found in ONLY THREE PLACES: - -1. **Inline within the repository's code files**: - 1. We have automated processes to extract this information and place it in our Talawa documentation site [docs-admin.talawa.io](https://docs-admin.talawa.io/). - 2. These are placed in the repository's Docusaurus sub-directory `docs/docs/auto-docs` -2. **In the repository's Docusaurus files**: - 1. These include manually edited files from our contributors - 2. These can be found in the `docs/docs/docs` directory -3. **In our `talawa-docs` repository**: - 1. Our [Talawa-Docs](https://github.com/PalisadoesFoundation/talawa-docs) repository contains user edited markdown files that cover information across all the Talawa repositories. - 2. That repository generates web pages for our Talawa documentation site [docs.talawa.io](https://docs.talawa.io/) using the [Docusaurus](https://docusaurus.io/) package. +Our documentation can be found in ONLY TWO PLACES: + +1. ***Inline within the repository's code files***: We have automated processes to extract this information and place it in our Talawa documentation site [docs.talawa.io](https://docs.talawa.io/). +1. ***In our `talawa-docs` repository***: Our [Talawa-Docs](https://github.com/PalisadoesFoundation/talawa-docs) repository contains user edited markdown files that are automatically integrated into our Talawa documentation site [docs.talawa.io](https://docs.talawa.io/) using the [Docusaurus](https://docusaurus.io/) package. + +## How to use Docusaurus +The process in easy: +1. Install `talawa-docs` on your system +1. Launch docusaurus on your system according to the `talawa-docs`documentation. + - A local version of `docs.talawa.io` should automatically launched in your browser at http://localhost:3000/ +1. Add/modify the markdown documents to the `docs/` directory of the `talawa-docs` repository +1. If adding a file, then you will also need to edit the `sidebars.js` which is used to generate the [docs.talawa.io](https://docs.talawa.io/) menus. +1. Always monitor the local website in your brower to make sure the changes are acceptable. + - You'll be able to see errors that you can use for troubleshooting in the CLI window you used to launch the local website. + +## Other information +***PLEASE*** do not add markdown files in this repository. Add them to `talawa-docs`! \ No newline at end of file diff --git a/docs/docs/auto-docs/components/SignOut/SignOut/functions/default.md b/docs/docs/auto-docs/components/SignOut/SignOut/functions/default.md index 208c633a32..d685ea3189 100644 --- a/docs/docs/auto-docs/components/SignOut/SignOut/functions/default.md +++ b/docs/docs/auto-docs/components/SignOut/SignOut/functions/default.md @@ -6,15 +6,15 @@ > **default**(): `Element` -Defined in: [src/components/SignOut/SignOut.tsx:20](https://github.com/PalisadoesFoundation/talawa-admin/blob/main/src/components/SignOut/SignOut.tsx#L20) +Defined in: [src/components/SignOut/SignOut.tsx:19](https://github.com/PalisadoesFoundation/talawa-admin/blob/main/src/components/SignOut/SignOut.tsx#L19) -Renders a sign-out button. +Renders a sign out button. -This component helps to log out. +This component helps to logout. The logout function revokes the refresh token and clears local storage before redirecting to the home page. ## Returns `Element` -JSX.Element - The sign-out button. +JSX.Element - The profile card . diff --git a/docs/docs/auto-docs/components/Venues/VenueModal/functions/default.md b/docs/docs/auto-docs/components/Venues/VenueModal/functions/default.md index 64e42a3b7c..db22491ee0 100644 --- a/docs/docs/auto-docs/components/Venues/VenueModal/functions/default.md +++ b/docs/docs/auto-docs/components/Venues/VenueModal/functions/default.md @@ -11,7 +11,7 @@ Defined in: [src/components/Venues/VenueModal.tsx:56](https://github.com/Palisad A modal component for creating or updating venue information. This component displays a modal window where users can enter details for a venue, such as name, description, capacity, and an image. -It also handles submitting the form data to create or update a venue based on whether the `edit` prop is true or false. +It also handles submitting the form data to create or update a venue based on whether the edit prop is true or false. ## Parameters diff --git a/docs/docs/auto-docs/screens/OrganizationDashboard/Leaderboard/Leaderboard.mocks/variables/EMPTY_MOCKS.md b/docs/docs/auto-docs/screens/OrganizationDashboard/Leaderboard/Leaderboard.mocks/variables/EMPTY_MOCKS.md new file mode 100644 index 0000000000..80f63475bf --- /dev/null +++ b/docs/docs/auto-docs/screens/OrganizationDashboard/Leaderboard/Leaderboard.mocks/variables/EMPTY_MOCKS.md @@ -0,0 +1,55 @@ +[Admin Docs](/) + +*** + +# Variable: EMPTY\_MOCKS + +> `const` **EMPTY\_MOCKS**: `object`[] + +Defined in: [src/screens/OrganizationDashboard/Leaderboard/Leaderboard.mocks.ts:162](https://github.com/PalisadoesFoundation/talawa-admin/blob/main/src/screens/OrganizationDashboard/Leaderboard/Leaderboard.mocks.ts#L162) + +## Type declaration + +### request + +> **request**: `object` + +#### request.query + +> **query**: `DocumentNode` = `VOLUNTEER_RANKING` + +#### request.variables + +> **variables**: `object` + +#### request.variables.orgId + +> **orgId**: `string` = `'orgId'` + +#### request.variables.where + +> **where**: `object` + +#### request.variables.where.nameContains + +> **nameContains**: `string` = `''` + +#### request.variables.where.orderBy + +> **orderBy**: `string` = `'hours_DESC'` + +#### request.variables.where.timeFrame + +> **timeFrame**: `string` = `'allTime'` + +### result + +> **result**: `object` + +#### result.data + +> **data**: `object` + +#### result.data.getVolunteerRanks + +> **getVolunteerRanks**: `any`[] = `[]` diff --git a/docs/docs/auto-docs/screens/OrganizationDashboard/Leaderboard/Leaderboard.mocks/variables/ERROR_MOCKS.md b/docs/docs/auto-docs/screens/OrganizationDashboard/Leaderboard/Leaderboard.mocks/variables/ERROR_MOCKS.md new file mode 100644 index 0000000000..4227de42ab --- /dev/null +++ b/docs/docs/auto-docs/screens/OrganizationDashboard/Leaderboard/Leaderboard.mocks/variables/ERROR_MOCKS.md @@ -0,0 +1,47 @@ +[Admin Docs](/) + +*** + +# Variable: ERROR\_MOCKS + +> `const` **ERROR\_MOCKS**: `object`[] + +Defined in: [src/screens/OrganizationDashboard/Leaderboard/Leaderboard.mocks.ts:183](https://github.com/PalisadoesFoundation/talawa-admin/blob/main/src/screens/OrganizationDashboard/Leaderboard/Leaderboard.mocks.ts#L183) + +## Type declaration + +### error + +> **error**: `Error` + +### request + +> **request**: `object` + +#### request.query + +> **query**: `DocumentNode` = `VOLUNTEER_RANKING` + +#### request.variables + +> **variables**: `object` + +#### request.variables.orgId + +> **orgId**: `string` = `'orgId'` + +#### request.variables.where + +> **where**: `object` + +#### request.variables.where.nameContains + +> **nameContains**: `string` = `''` + +#### request.variables.where.orderBy + +> **orderBy**: `string` = `'hours_DESC'` + +#### request.variables.where.timeFrame + +> **timeFrame**: `string` = `'allTime'` diff --git a/docs/docs/auto-docs/screens/OrganizationDashboard/Leaderboard/Leaderboard.mocks/variables/MOCKS.md b/docs/docs/auto-docs/screens/OrganizationDashboard/Leaderboard/Leaderboard.mocks/variables/MOCKS.md new file mode 100644 index 0000000000..2b1b30dc01 --- /dev/null +++ b/docs/docs/auto-docs/screens/OrganizationDashboard/Leaderboard/Leaderboard.mocks/variables/MOCKS.md @@ -0,0 +1,55 @@ +[Admin Docs](/) + +*** + +# Variable: MOCKS + +> `const` **MOCKS**: `object`[] + +Defined in: [src/screens/OrganizationDashboard/Leaderboard/Leaderboard.mocks.ts:51](https://github.com/PalisadoesFoundation/talawa-admin/blob/main/src/screens/OrganizationDashboard/Leaderboard/Leaderboard.mocks.ts#L51) + +## Type declaration + +### request + +> **request**: `object` + +#### request.query + +> **query**: `DocumentNode` = `VOLUNTEER_RANKING` + +#### request.variables + +> **variables**: `object` + +#### request.variables.orgId + +> **orgId**: `string` = `'orgId'` + +#### request.variables.where + +> **where**: `object` + +#### request.variables.where.nameContains + +> **nameContains**: `string` = `''` + +#### request.variables.where.orderBy + +> **orderBy**: `string` = `'hours_DESC'` + +#### request.variables.where.timeFrame + +> **timeFrame**: `string` = `'allTime'` + +### result + +> **result**: `object` + +#### result.data + +> **data**: `object` + +#### result.data.getVolunteerRanks + +> **getVolunteerRanks**: `object`[] diff --git a/src/components/Venues/VenueModal.spec.tsx b/src/components/Venues/VenueModal.spec.tsx index c840b6de53..e2995d15ab 100644 --- a/src/components/Venues/VenueModal.spec.tsx +++ b/src/components/Venues/VenueModal.spec.tsx @@ -1,27 +1,30 @@ -import React, { act } from 'react'; +import React from 'react'; import { MockedProvider } from '@apollo/react-testing'; import type { RenderResult } from '@testing-library/react'; -import { render, screen, fireEvent } from '@testing-library/react'; +import { render, screen, fireEvent, waitFor } from '@testing-library/react'; import { Provider } from 'react-redux'; import { BrowserRouter } from 'react-router-dom'; import { I18nextProvider } from 'react-i18next'; +import userEvent from '@testing-library/user-event'; +import { toast } from 'react-toastify'; +import { vi } from 'vitest'; +import type * as RouterTypes from 'react-router-dom'; +import { act } from 'react-dom/test-utils'; import type { InterfaceVenueModalProps } from './VenueModal'; import VenueModal from './VenueModal'; import { store } from 'state/store'; import i18nForTest from 'utils/i18nForTest'; -import userEvent from '@testing-library/user-event'; import { StaticMockLink } from 'utils/StaticMockLink'; -import { toast } from 'react-toastify'; import { CREATE_VENUE_MUTATION, UPDATE_VENUE_MUTATION, } from 'GraphQl/Mutations/mutations'; import type { ApolloLink } from '@apollo/client'; -import { vi } from 'vitest'; -import type * as RouterTypes from 'react-router-dom'; +// Mock Setup const MOCKS = [ + // Create venue mock { request: { query: CREATE_VENUE_MUTATION, @@ -41,16 +44,59 @@ const MOCKS = [ }, }, }, + + // Basic update venue mock { request: { query: UPDATE_VENUE_MUTATION, variables: { + id: 'venue1', + name: 'Updated Venue', capacity: 200, description: 'Updated description', file: 'image1', + }, + }, + result: { + data: { + editVenue: { + _id: 'venue1', + }, + }, + }, + }, + + // First sequential update mock + { + request: { + query: UPDATE_VENUE_MUTATION, + variables: { id: 'venue1', - name: 'Updated Venue', - organizationId: 'orgId', + name: 'Updated Venue 1', + capacity: parseInt('100'), + description: 'Updated description for venue 1', + file: 'image1', + }, + }, + result: { + data: { + editVenue: { + _id: 'venue1', + }, + }, + }, + }, + + // Second sequential update mock + { + request: { + query: UPDATE_VENUE_MUTATION, + variables: { + id: 'venue1', + name: 'Updated Venue 2', + capacity: parseInt('100'), + description: 'Updated description for venue 1', + file: 'image1', }, }, result: { @@ -61,10 +107,78 @@ const MOCKS = [ }, }, }, -]; -const link = new StaticMockLink(MOCKS, true); + // Duplicate name error mock + { + request: { + query: CREATE_VENUE_MUTATION, + variables: { + name: 'Existing Venue', + description: 'Test Description', + capacity: 100, + organizationId: 'orgId', + file: '', + }, + }, + error: new Error('alreadyExists'), + }, + + // Network error mock + { + request: { + query: CREATE_VENUE_MUTATION, + variables: { + name: 'Network Test Venue', + description: 'Test Description', + capacity: 100, + organizationId: 'orgId', + file: '', + }, + }, + error: new Error('Network error'), + }, + // Update with unchanged name mock + { + request: { + query: UPDATE_VENUE_MUTATION, + variables: { + id: 'venue1', + capacity: parseInt('150'), + description: 'Changed description', + file: 'image1', + }, + }, + result: { + data: { + editVenue: { + _id: 'venue1', + }, + }, + }, + }, + + // Mock for whitespace trimming test + { + request: { + query: CREATE_VENUE_MUTATION, + variables: { + name: 'Test Venue', // Note: trimmed value + description: 'Test Description', // Note: trimmed value + capacity: 100, + organizationId: 'orgId', + file: '', + }, + }, + result: { + data: { + createVenue: { + _id: 'newVenue', + }, + }, + }, + }, +]; const mockId = 'orgId'; vi.mock('react-router-dom', async () => { @@ -77,14 +191,6 @@ vi.mock('react-router-dom', async () => { }; }); -async function wait(ms = 100): Promise { - await act(() => { - return new Promise((resolve) => { - setTimeout(resolve, ms); - }); - }); -} - vi.mock('react-toastify', () => ({ toast: { success: vi.fn(), @@ -93,30 +199,38 @@ vi.mock('react-toastify', () => ({ }, })); -const props: InterfaceVenueModalProps[] = [ - { - show: true, - onHide: vi.fn(), - edit: false, - venueData: null, - refetchVenues: vi.fn(), - orgId: 'orgId', - }, - { - show: true, - onHide: vi.fn(), - edit: true, - venueData: { - _id: 'venue1', - name: 'Venue 1', - description: 'Updated description for venue 1', - image: 'image1', - capacity: '100', - }, - refetchVenues: vi.fn(), - orgId: 'orgId', +// Helper Functions +async function wait(ms = 100): Promise { + await act(async () => { + await new Promise((resolve) => { + setTimeout(resolve, ms); + }); + }); +} + +const defaultProps: InterfaceVenueModalProps = { + show: true, + onHide: vi.fn(), + edit: false, + venueData: null, + refetchVenues: vi.fn(), + orgId: 'orgId', +}; + +const editProps: InterfaceVenueModalProps = { + show: true, + onHide: vi.fn(), + edit: true, + venueData: { + _id: 'venue1', + name: 'Venue 1', + description: 'Updated description for venue 1', + image: 'image1', + capacity: '100', }, -]; + refetchVenues: vi.fn(), + orgId: 'orgId', +}; const renderVenueModal = ( props: InterfaceVenueModalProps, @@ -136,244 +250,1041 @@ const renderVenueModal = ( }; describe('VenueModal', () => { - global.alert = vi.fn(); - - test('renders correctly when show is true', async () => { - renderVenueModal(props[0], link); - expect(screen.getByText('Venue Details')).toBeInTheDocument(); - }); - - test('does not render when show is false', () => { - const { container } = renderVenueModal({ ...props[0], show: false }, link); - expect(container.firstChild).toBeNull(); + beforeEach(() => { + vi.clearAllMocks(); }); - test('populates form fields correctly in edit mode', () => { - renderVenueModal(props[1], link); - expect(screen.getByDisplayValue('Venue 1')).toBeInTheDocument(); - expect( - screen.getByDisplayValue('Updated description for venue 1'), - ).toBeInTheDocument(); - expect(screen.getByDisplayValue('100')).toBeInTheDocument(); - }); + // Basic Rendering Tests + describe('Rendering', () => { + test('renders correctly when show is true', () => { + renderVenueModal(defaultProps, new StaticMockLink(MOCKS, true)); + expect(screen.getByText('Venue Details')).toBeInTheDocument(); + }); - test('form fields are empty in create mode', () => { - renderVenueModal(props[0], link); - expect(screen.getByPlaceholderText('Enter Venue Name')).toHaveValue(''); - expect(screen.getByPlaceholderText('Enter Venue Description')).toHaveValue( - '', - ); - expect(screen.getByPlaceholderText('Enter Venue Capacity')).toHaveValue(''); - }); + test('does not render when show is false', () => { + const props = { ...defaultProps, show: false }; + const { container } = renderVenueModal( + props, + new StaticMockLink(MOCKS, true), + ); + expect(container.firstChild).toBeNull(); + }); - test('calls onHide when close button is clicked', () => { - renderVenueModal(props[0], link); - fireEvent.click(screen.getByTestId('createVenueModalCloseBtn')); - expect(props[0].onHide).toHaveBeenCalled(); + test('calls onHide when close button is clicked', () => { + renderVenueModal(defaultProps, new StaticMockLink(MOCKS, true)); + fireEvent.click(screen.getByTestId('createVenueModalCloseBtn')); + expect(defaultProps.onHide).toHaveBeenCalled(); + }); }); - test('displays image preview and clear button when an image is selected', async () => { - renderVenueModal(props[0], link); - - const file = new File(['chad'], 'chad.png', { type: 'image/png' }); - const fileInput = screen.getByTestId('venueImgUrl'); - userEvent.upload(fileInput, file); + // Form Field Tests + describe('Form Fields', () => { + test('populates form fields correctly in edit mode', () => { + renderVenueModal(editProps, new StaticMockLink(MOCKS, true)); + expect(screen.getByDisplayValue('Venue 1')).toBeInTheDocument(); + expect( + screen.getByDisplayValue('Updated description for venue 1'), + ).toBeInTheDocument(); + expect(screen.getByDisplayValue('100')).toBeInTheDocument(); + }); - await wait(); + test('form fields are empty in create mode', () => { + renderVenueModal(defaultProps, new StaticMockLink(MOCKS, true)); + expect(screen.getByPlaceholderText('Enter Venue Name')).toHaveValue(''); + expect( + screen.getByPlaceholderText('Enter Venue Description'), + ).toHaveValue(''); + expect(screen.getByPlaceholderText('Enter Venue Capacity')).toHaveValue( + '', + ); + }); - expect(screen.getByAltText('Venue Image Preview')).toBeInTheDocument(); - expect(screen.getByTestId('closeimage')).toBeInTheDocument(); + test('trims whitespace from name and description before submission', async () => { + renderVenueModal(defaultProps, new StaticMockLink(MOCKS, true)); + + fireEvent.change(screen.getByPlaceholderText('Enter Venue Name'), { + target: { value: ' Test Venue ' }, + }); + fireEvent.change(screen.getByPlaceholderText('Enter Venue Description'), { + target: { value: ' Test Description ' }, + }); + fireEvent.change(screen.getByPlaceholderText('Enter Venue Capacity'), { + target: { value: '100' }, + }); + + await act(async () => { + fireEvent.click(screen.getByTestId('createVenueBtn')); + }); + + await waitFor(() => { + expect(toast.success).toHaveBeenCalledWith( + 'organizationVenues.venueCreated', + ); + }); + }); }); - test('removes image preview when clear button is clicked', async () => { - renderVenueModal(props[0], link); - - const file = new File(['chad'], 'chad.png', { type: 'image/png' }); - const fileInput = screen.getByTestId('venueImgUrl'); - userEvent.upload(fileInput, file); + // Image Handling Tests + describe('Image Handling', () => { + test('displays image preview and clear button when an image is selected', async () => { + renderVenueModal(defaultProps, new StaticMockLink(MOCKS, true)); + const file = new File(['test'], 'test.png', { type: 'image/png' }); + const fileInput = screen.getByTestId('venueImgUrl'); + await userEvent.upload(fileInput, file); - await wait(); - - const form = screen.getByTestId('venueForm'); - form.addEventListener('submit', (e) => e.preventDefault()); - fireEvent.click(screen.getByTestId('closeimage')); + expect(screen.getByAltText('Venue Image Preview')).toBeInTheDocument(); + expect(screen.getByTestId('closeimage')).toBeInTheDocument(); + }); - expect( - screen.queryByAltText('Venue Image Preview'), - ).not.toBeInTheDocument(); - expect(screen.queryByTestId('closeimage')).not.toBeInTheDocument(); + test('removes image preview when clear button is clicked', async () => { + renderVenueModal(defaultProps, new StaticMockLink(MOCKS, true)); + const file = new File(['test'], 'test.png', { type: 'image/png' }); + const fileInput = screen.getByTestId('venueImgUrl'); + await userEvent.upload(fileInput, file); + + fireEvent.click(screen.getByTestId('closeimage')); + expect( + screen.queryByAltText('Venue Image Preview'), + ).not.toBeInTheDocument(); + expect(screen.queryByTestId('closeimage')).not.toBeInTheDocument(); + }); }); - test('shows error when venue name is empty', async () => { - renderVenueModal(props[0], link); + // Validation Tests + describe('Validation', () => { + test('shows error when venue name is empty', async () => { + renderVenueModal(defaultProps, new StaticMockLink(MOCKS, true)); - const form = screen.getByTestId('venueForm'); - form.addEventListener('submit', (e) => e.preventDefault()); + fireEvent.change(screen.getByPlaceholderText('Enter Venue Capacity'), { + target: { value: '100' }, + }); - const submitButton = screen.getByTestId('createVenueBtn'); - fireEvent.click(submitButton); + await act(async () => { + fireEvent.click(screen.getByTestId('createVenueBtn')); + }); - await wait(); + await waitFor(() => { + expect(toast.error).toHaveBeenCalledWith( + 'Venue title cannot be empty!', + ); + }); + }); + + test('shows error when venue capacity is not a positive number', async () => { + renderVenueModal(defaultProps, new StaticMockLink(MOCKS, true)); + + fireEvent.change(screen.getByPlaceholderText('Enter Venue Name'), { + target: { value: 'Test Venue' }, + }); + fireEvent.change(screen.getByPlaceholderText('Enter Venue Capacity'), { + target: { value: '-1' }, + }); + + await act(async () => { + fireEvent.click(screen.getByTestId('createVenueBtn')); + }); + + await waitFor(() => { + expect(toast.error).toHaveBeenCalledWith( + 'Capacity must be a positive number!', + ); + }); + }); - expect(toast.error).toHaveBeenCalledWith('Venue title cannot be empty!'); + test('validates capacity edge cases', async () => { + renderVenueModal(defaultProps, new StaticMockLink(MOCKS, true)); + + // Test zero capacity + fireEvent.change(screen.getByPlaceholderText('Enter Venue Name'), { + target: { value: 'Test Venue' }, + }); + fireEvent.change(screen.getByPlaceholderText('Enter Venue Capacity'), { + target: { value: '0' }, + }); + + await act(async () => { + fireEvent.click(screen.getByTestId('createVenueBtn')); + }); + + await waitFor(() => { + expect(toast.error).toHaveBeenCalledWith( + 'Capacity must be a positive number!', + ); + }); + }); }); - test('shows error when venue capacity is not a positive number', async () => { - renderVenueModal(props[0], link); + // Mutation Tests + describe('Mutations', () => { + test('disables submit button during mutation loading state', async () => { + renderVenueModal(defaultProps, new StaticMockLink(MOCKS, true)); - const nameInput = screen.getByPlaceholderText('Enter Venue Name'); - fireEvent.change(nameInput, { target: { value: 'Test venue' } }); + fireEvent.change(screen.getByPlaceholderText('Enter Venue Name'), { + target: { value: 'Test Venue' }, + }); + fireEvent.change(screen.getByPlaceholderText('Enter Venue Capacity'), { + target: { value: '100' }, + }); - const capacityInput = screen.getByPlaceholderText('Enter Venue Capacity'); - fireEvent.change(capacityInput, { target: { value: '-1' } }); + const submitButton = screen.getByTestId('createVenueBtn'); + fireEvent.click(submitButton); - const form = screen.getByTestId('venueForm'); - form.addEventListener('submit', (e) => e.preventDefault()); + expect(submitButton).toBeDisabled(); + }); - const submitButton = screen.getByTestId('createVenueBtn'); - fireEvent.click(submitButton); + test('shows success toast when a new venue is created', async () => { + renderVenueModal(defaultProps, new StaticMockLink(MOCKS, true)); + + fireEvent.change(screen.getByPlaceholderText('Enter Venue Name'), { + target: { value: 'Test Venue' }, + }); + fireEvent.change(screen.getByPlaceholderText('Enter Venue Description'), { + target: { value: 'Test Venue Desc' }, + }); + fireEvent.change(screen.getByPlaceholderText('Enter Venue Capacity'), { + target: { value: '100' }, + }); + + await act(async () => { + fireEvent.click(screen.getByTestId('createVenueBtn')); + }); + + await waitFor(() => { + expect(toast.success).toHaveBeenCalledWith( + 'organizationVenues.venueCreated', + ); + }); + }); - await wait(); + test('handles duplicate venue name error', async () => { + renderVenueModal(defaultProps, new StaticMockLink(MOCKS, true)); + + fireEvent.change(screen.getByPlaceholderText('Enter Venue Name'), { + target: { value: 'Existing Venue' }, + }); + fireEvent.change(screen.getByPlaceholderText('Enter Venue Description'), { + target: { value: 'Test Description' }, + }); + fireEvent.change(screen.getByPlaceholderText('Enter Venue Capacity'), { + target: { value: '100' }, + }); + + await act(async () => { + fireEvent.click(screen.getByTestId('createVenueBtn')); + }); + + await waitFor(() => { + expect(toast.error).toHaveBeenCalledWith( + 'organizationVenues.venueNameExists', + ); + }); + }); - expect(toast.error).toHaveBeenCalledWith( - 'Capacity must be a positive number!', - ); + test('handles network error during venue creation', async () => { + renderVenueModal(defaultProps, new StaticMockLink(MOCKS, true)); + + fireEvent.change(screen.getByPlaceholderText('Enter Venue Name'), { + target: { value: 'Network Test Venue' }, + }); + fireEvent.change(screen.getByPlaceholderText('Enter Venue Description'), { + target: { value: 'Test Description' }, + }); + fireEvent.change(screen.getByPlaceholderText('Enter Venue Capacity'), { + target: { value: '100' }, + }); + + await act(async () => { + fireEvent.click(screen.getByTestId('createVenueBtn')); + }); + + await waitFor(() => { + expect(toast.error).toHaveBeenCalled(); + }); + }); }); - test('shows success toast when a new venue is created', async () => { - renderVenueModal(props[0], link); - - const nameInput = screen.getByPlaceholderText('Enter Venue Name'); - fireEvent.change(nameInput, { target: { value: 'Test Venue' } }); - const descriptionInput = screen.getByPlaceholderText( - 'Enter Venue Description', - ); - fireEvent.change(descriptionInput, { - target: { value: 'Test Venue Desc' }, + // Update Tests + describe('Venue Updates', () => { + test('shows success toast when an existing venue is updated', async () => { + renderVenueModal(editProps, new StaticMockLink(MOCKS, true)); + + fireEvent.change(screen.getByDisplayValue('Venue 1'), { + target: { value: 'Updated Venue' }, + }); + fireEvent.change( + screen.getByDisplayValue('Updated description for venue 1'), + { + target: { value: 'Updated description' }, + }, + ); + fireEvent.change(screen.getByDisplayValue('100'), { + target: { value: '200' }, + }); + + await act(async () => { + fireEvent.click(screen.getByTestId('updateVenueBtn')); + }); + + await waitFor(() => { + expect(toast.success).toHaveBeenCalledWith( + 'Venue details updated successfully', + ); + }); }); - const capacityInput = screen.getByPlaceholderText('Enter Venue Capacity'); - fireEvent.change(capacityInput, { target: { value: 100 } }); - const form = screen.getByTestId('venueForm'); - form.addEventListener('submit', (e) => e.preventDefault()); - - const submitButton = screen.getByTestId('createVenueBtn'); - fireEvent.click(submitButton); - - await wait(); - - expect(toast.success).toHaveBeenCalledWith('Venue added Successfully'); - }); - - test('shows success toast when an existing venue is updated', async () => { - renderVenueModal(props[1], link); + test('handles multiple successive updates correctly', async () => { + const mockLink = new StaticMockLink(MOCKS, true); + const onHide = vi.fn(); + const refetchVenues = vi.fn(); + + const props = { + ...editProps, + onHide, + refetchVenues, + }; + + renderVenueModal(props, mockLink); + + // First update + fireEvent.change(screen.getByDisplayValue('Venue 1'), { + target: { value: 'Updated Venue 1' }, + }); + + await act(async () => { + fireEvent.click(screen.getByTestId('updateVenueBtn')); + await wait(0); + }); + + await waitFor(() => { + expect(refetchVenues).toHaveBeenCalledTimes(1); + }); + + // Second update + fireEvent.change(screen.getByDisplayValue('Updated Venue 1'), { + target: { value: 'Updated Venue 2' }, + }); + + await act(async () => { + fireEvent.click(screen.getByTestId('updateVenueBtn')); + await wait(0); + }); + + await waitFor(() => { + expect(refetchVenues).toHaveBeenCalledTimes(2); + expect(onHide).toHaveBeenCalledTimes(2); + }); + }); - const nameInput = screen.getByDisplayValue('Venue 1'); - fireEvent.change(nameInput, { target: { value: 'Updated Venue' } }); - const descriptionInput = screen.getByDisplayValue( - 'Updated description for venue 1', - ); - fireEvent.change(descriptionInput, { - target: { value: 'Updated description' }, + test('handles unchanged name in edit mode', async () => { + renderVenueModal(editProps, new StaticMockLink(MOCKS, true)); + + fireEvent.change(screen.getByDisplayValue('Venue 1'), { + target: { value: 'Venue 1' }, // Same name + }); + fireEvent.change(screen.getByDisplayValue('100'), { + target: { value: '150' }, + }); + fireEvent.change( + screen.getByDisplayValue('Updated description for venue 1'), + { target: { value: 'Changed description' } }, + ); + + await act(async () => { + fireEvent.click(screen.getByTestId('updateVenueBtn')); + }); + + await waitFor(() => { + expect(toast.success).toHaveBeenCalledWith( + 'Venue details updated successfully', + ); + }); }); + // Error Handling Tests + describe('Error Handling', () => { + test('shows error toast when network error occurs during update', async () => { + const errorMock = [ + { + request: { + query: UPDATE_VENUE_MUTATION, + variables: { + id: 'venue1', + name: 'Updated Venue', + capacity: parseInt('100'), + description: 'Test Description', + file: 'image1', + }, + }, + error: new Error('Network error'), + }, + ]; - const capacityInput = screen.getByDisplayValue('100'); - fireEvent.change(capacityInput, { target: { value: 200 } }); - const form = screen.getByTestId('venueForm'); - form.addEventListener('submit', (e) => e.preventDefault()); + renderVenueModal(editProps, new StaticMockLink(errorMock, true)); - const submitButton = screen.getByTestId('updateVenueBtn'); - fireEvent.click(submitButton); + fireEvent.change(screen.getByDisplayValue('Venue 1'), { + target: { value: 'Updated Venue' }, + }); + fireEvent.change( + screen.getByDisplayValue('Updated description for venue 1'), + { + target: { value: 'Test Description' }, + }, + ); - await wait(); + await act(async () => { + fireEvent.click(screen.getByTestId('updateVenueBtn')); + }); - expect(toast.success).toHaveBeenCalledWith( - 'Venue details updated successfully', - ); - }); -}); + await waitFor(() => { + expect(toast.error).toHaveBeenCalled(); + }); + }); + }); -describe('VenueModal with error scenarios', () => { - test('displays error toast when creating a venue fails', async () => { - const errorMocks = [ - { + test('handles "alreadyExists" error specifically', async () => { + const duplicateNameMock = { request: { query: CREATE_VENUE_MUTATION, variables: { - name: 'Error Venue', - description: 'This should fail', - capacity: 50, + name: 'Duplicate Venue', + description: 'Test Description', + capacity: 100, organizationId: 'orgId', file: '', }, }, - error: new Error('Failed to create venue'), - }, - ]; - - const errorLink = new StaticMockLink(errorMocks, true); - renderVenueModal(props[0], errorLink); - - const nameInput = screen.getByPlaceholderText('Enter Venue Name'); - fireEvent.change(nameInput, { target: { value: 'Error Venue' } }); - - const descriptionInput = screen.getByPlaceholderText( - 'Enter Venue Description', - ); - fireEvent.change(descriptionInput, { - target: { value: 'This should fail' }, + error: new Error('alreadyExists'), + }; + + renderVenueModal( + defaultProps, + new StaticMockLink([duplicateNameMock], true), + ); + + await act(async () => { + await userEvent.type( + screen.getByPlaceholderText('Enter Venue Name'), + 'Duplicate Venue', + ); + await userEvent.type( + screen.getByPlaceholderText('Enter Venue Description'), + 'Test Description', + ); + await userEvent.type( + screen.getByPlaceholderText('Enter Venue Capacity'), + '100', + ); + fireEvent.click(screen.getByTestId('createVenueBtn')); + }); + + await waitFor(() => { + expect(toast.error).toHaveBeenCalledWith( + 'organizationVenues.venueNameExists', + ); + }); }); - const capacityInput = screen.getByPlaceholderText('Enter Venue Capacity'); - fireEvent.change(capacityInput, { target: { value: 50 } }); - - const submitButton = screen.getByTestId('createVenueBtn'); - fireEvent.click(submitButton); - - await wait(); - - expect(toast.error).toHaveBeenCalledWith('Failed to create venue'); - }); - - test('displays error toast when updating a venue fails', async () => { - const errorMocks = [ - { - request: { - query: UPDATE_VENUE_MUTATION, - variables: { - capacity: 150, - description: 'Failed update description', - file: 'image1', - id: 'venue1', - name: 'Failed Update Venue', - organizationId: 'orgId', + // Cleanup Tests + describe('Cleanup', () => { + test('handles mutation errors with custom error messages', async () => { + const errorMock = { + request: { + query: CREATE_VENUE_MUTATION, + variables: { + name: 'Test Venue', + description: 'Test Description', + capacity: 100, + organizationId: 'orgId', + file: '', + }, }, - }, - error: new Error('Failed to update venue'), - }, - ]; + error: new Error('Custom error message'), + }; + + renderVenueModal(defaultProps, new StaticMockLink([errorMock], true)); + + await act(async () => { + await userEvent.type( + screen.getByPlaceholderText('Enter Venue Name'), + 'Test Venue', + ); + await userEvent.type( + screen.getByPlaceholderText('Enter Venue Description'), + 'Test Description', + ); + await userEvent.type( + screen.getByPlaceholderText('Enter Venue Capacity'), + '100', + ); + fireEvent.click(screen.getByTestId('createVenueBtn')); + }); + + await waitFor(() => { + expect(toast.error).toHaveBeenCalled(); + }); + }); + + test('handles unexpected mutation errors', async () => { + const errorMock = { + request: { + query: UPDATE_VENUE_MUTATION, + variables: { + id: 'venue1', + capacity: 100, + description: 'Test Description', + file: 'image1', + }, + }, + error: new Error('Unexpected error'), + }; - const errorLink = new StaticMockLink(errorMocks, true); - renderVenueModal(props[1], errorLink); + renderVenueModal(editProps, new StaticMockLink([errorMock], true)); - const nameInput = screen.getByDisplayValue('Venue 1'); - fireEvent.change(nameInput, { target: { value: 'Failed Update Venue' } }); + await act(async () => { + fireEvent.click(screen.getByTestId('updateVenueBtn')); + }); - const descriptionInput = screen.getByDisplayValue( - 'Updated description for venue 1', - ); - fireEvent.change(descriptionInput, { - target: { value: 'Failed update description' }, + await waitFor(() => { + expect(toast.error).toHaveBeenCalled(); + }); + }); }); - const capacityInput = screen.getByDisplayValue('100'); - fireEvent.change(capacityInput, { target: { value: 150 } }); - - const submitButton = screen.getByTestId('updateVenueBtn'); - fireEvent.click(submitButton); - - await wait(); - - expect(toast.error).toHaveBeenCalledWith('Failed to update venue'); + // Form State Management + test('resets form state when modal is closed and reopened', async () => { + const { rerender } = renderVenueModal( + defaultProps, + new StaticMockLink(MOCKS, true), + ); + + // Fill in form + await act(async () => { + const nameInput = screen.getByPlaceholderText('Enter Venue Name'); + const descInput = screen.getByPlaceholderText( + 'Enter Venue Description', + ); + const capInput = screen.getByPlaceholderText('Enter Venue Capacity'); + + await userEvent.type(nameInput, 'Test Venue'); + await userEvent.type(descInput, 'Test Description'); + await userEvent.type(capInput, '100'); + + expect(nameInput).toHaveValue('Test Venue'); + expect(descInput).toHaveValue('Test Description'); + expect(capInput).toHaveValue('100'); + }); + + // Completely unmount by setting show to false + await act(async () => { + rerender( + + + + + + + + + , + ); + await wait(100); + }); + + // Mount fresh component + renderVenueModal(defaultProps, new StaticMockLink(MOCKS, true)); + + await waitFor(() => { + const newNameInput = screen.getByPlaceholderText('Enter Venue Name'); + const newDescInput = screen.getByPlaceholderText( + 'Enter Venue Description', + ); + const newCapInput = screen.getByPlaceholderText('Enter Venue Capacity'); + + // Check if inputs are empty in new instance + expect(newNameInput).toHaveValue(''); + expect(newDescInput).toHaveValue(''); + expect(newCapInput).toHaveValue(''); + }); + }); + describe('VenueModal Additional Tests', () => { + // Form State Management Tests + describe('Form State Management', () => { + test('updates form state when description is changed', async () => { + renderVenueModal(defaultProps, new StaticMockLink(MOCKS, true)); + const descInput = screen.getByPlaceholderText( + 'Enter Venue Description', + ); + + await act(async () => { + await userEvent.type(descInput, 'New Description'); + }); + + expect(descInput).toHaveValue('New Description'); + }); + + test('enforces maximum length for description', async () => { + renderVenueModal(defaultProps, new StaticMockLink(MOCKS, true)); + const descInput = screen.getByPlaceholderText( + 'Enter Venue Description', + ); + const longText = 'a'.repeat(501); // Exceeds 500 char limit + + await act(async () => { + await userEvent.type(descInput, longText); + }); + + expect(descInput).toHaveValue(longText.slice(0, 500)); + }); + }); + + // Image Handling Edge Cases + describe('Image Handling Edge Cases', () => { + // In VenueModal.spec.tsx + test('handles multiple files selected in image upload', async () => { + renderVenueModal(defaultProps, new StaticMockLink(MOCKS, true)); + const fileInput = screen.getByTestId('venueImgUrl'); + const files = [ + new File(['test1'], 'test1.png', { type: 'image/png' }), + new File(['test2'], 'test2.png', { type: 'image/png' }), + ]; + + await act(async () => { + await userEvent.upload(fileInput, files); + }); + + // Should only use the first file + expect(screen.getAllByAltText('Venue Image Preview')).toHaveLength(1); + }); + + // Validation Edge Cases + describe('Validation Edge Cases', () => { + test('handles empty venue name', async () => { + renderVenueModal(defaultProps, new StaticMockLink(MOCKS, true)); + + fireEvent.change( + screen.getByPlaceholderText('Enter Venue Capacity'), + { + target: { value: '100' }, + }, + ); + + await act(async () => { + fireEvent.click(screen.getByTestId('createVenueBtn')); + }); + + await waitFor(() => { + expect(toast.error).toHaveBeenCalledWith( + 'Venue title cannot be empty!', + ); + }); + }); + + test('handles empty description', async () => { + const createVenueMock = { + request: { + query: CREATE_VENUE_MUTATION, + variables: { + name: 'Test Venue', + description: '', + capacity: 100, + organizationId: 'orgId', + file: '', + }, + }, + result: { + data: { + createVenue: { + _id: 'newVenue', + }, + }, + }, + }; + + const mockLink = new StaticMockLink([createVenueMock], true); + + renderVenueModal(defaultProps, mockLink); + + await act(async () => { + await userEvent.type( + screen.getByPlaceholderText('Enter Venue Name'), + 'Test Venue', + ); + await userEvent.type( + screen.getByPlaceholderText('Enter Venue Capacity'), + '100', + ); + fireEvent.click(screen.getByTestId('createVenueBtn')); + }); + + await waitFor(() => { + expect(toast.success).toHaveBeenCalledWith( + 'organizationVenues.venueCreated', + ); + }); + }); + + test('handles null imageURL', async () => { + const createVenueMock = { + request: { + query: CREATE_VENUE_MUTATION, + variables: { + name: 'Test Venue', + description: 'Test Description', + capacity: 100, + organizationId: 'orgId', + file: '', // Explicitly passing empty string + }, + }, + result: { + data: { + createVenue: { + _id: 'newVenue', + }, + }, + }, + }; + + renderVenueModal( + { + ...defaultProps, + venueData: { + _id: 'testVenue', + name: 'Test Venue', + description: 'Test Description', + capacity: '100', + image: null, // Null image + }, + }, + new StaticMockLink([createVenueMock], true), + ); + + await act(async () => { + fireEvent.click(screen.getByTestId('createVenueBtn')); + }); + + await waitFor(() => { + expect(toast.success).toHaveBeenCalledWith( + 'organizationVenues.venueCreated', + ); + }); + }); + + test('handles empty image URL', async () => { + const createVenueMock = { + request: { + query: CREATE_VENUE_MUTATION, + variables: { + name: 'Test Venue', + description: 'Test Description', + capacity: 100, + organizationId: 'orgId', + file: '', + }, + }, + result: { + data: { + createVenue: { + _id: 'newVenue', + }, + }, + }, + }; + + const mockLink = new StaticMockLink([createVenueMock], true); + + renderVenueModal(defaultProps, mockLink); + + await act(async () => { + await userEvent.type( + screen.getByPlaceholderText('Enter Venue Name'), + 'Test Venue', + ); + await userEvent.type( + screen.getByPlaceholderText('Enter Venue Description'), + 'Test Description', + ); + await userEvent.type( + screen.getByPlaceholderText('Enter Venue Capacity'), + '100', + ); + fireEvent.click(screen.getByTestId('createVenueBtn')); + }); + + await waitFor(() => { + expect(toast.success).toHaveBeenCalledWith( + 'organizationVenues.venueCreated', + ); + }); + }); + + // Test for fallback error message when venue name already exists + test('uses fallback error message when venue name exists', async () => { + const duplicateNameMock = { + request: { + query: CREATE_VENUE_MUTATION, + variables: { + name: 'Duplicate Venue', + description: 'Test Description', + capacity: 100, + organizationId: 'orgId', + file: '', + }, + }, + error: new Error('alreadyExists'), + }; + + const mockLink = new StaticMockLink([duplicateNameMock], true); + + // Mock toast.error to check the exact message + const toastErrorSpy = vi.spyOn(toast, 'error'); + + renderVenueModal(defaultProps, mockLink); + + await act(async () => { + await userEvent.type( + screen.getByPlaceholderText('Enter Venue Name'), + 'Duplicate Venue', + ); + await userEvent.type( + screen.getByPlaceholderText('Enter Venue Description'), + 'Test Description', + ); + await userEvent.type( + screen.getByPlaceholderText('Enter Venue Capacity'), + '100', + ); + fireEvent.click(screen.getByTestId('createVenueBtn')); + }); + + await waitFor(() => { + // Check that the error was called with either the translated message or the fallback + expect(toastErrorSpy).toHaveBeenCalledWith( + expect.stringMatching('organizationVenues.venueNameExists'), + ); + }); + }); + }); + + test('handles special characters in venue name', async () => { + renderVenueModal(defaultProps, new StaticMockLink(MOCKS, true)); + const nameInput = screen.getByPlaceholderText('Enter Venue Name'); + + await act(async () => { + await userEvent.type(nameInput, '!@#$%^&*()'); + }); + + expect(nameInput).toHaveValue('!@#$%^&*()'); + }); + + test('handles non-numeric input for capacity', async () => { + renderVenueModal(defaultProps, new StaticMockLink(MOCKS, true)); + const capacityInput = screen.getByPlaceholderText( + 'Enter Venue Capacity', + ); + + await act(async () => { + await userEvent.type(capacityInput, 'abc'); + }); + + await act(async () => { + fireEvent.click(screen.getByTestId('createVenueBtn')); + }); + + await waitFor(() => { + expect(toast.error).toHaveBeenCalledWith( + 'Venue title cannot be empty!', + ); + }); + }); + }); + + // Error Boundary Tests + describe('Error Handling', () => { + test('handles image upload with no files', async () => { + renderVenueModal(defaultProps, new StaticMockLink(MOCKS, true)); + const fileInput = screen.getByTestId('venueImgUrl'); + + await act(async () => { + // Simulate file input event with no files + fireEvent.change(fileInput, { target: { files: null } }); + }); + + // Verify that the form state remains unchanged + expect(screen.getByPlaceholderText('Enter Venue Name')).toHaveValue( + '', + ); + }); + + test('handles empty description with trim and empty image URL', async () => { + const createVenueMock = { + request: { + query: CREATE_VENUE_MUTATION, + variables: { + name: 'Test Venue', + description: '', + capacity: 100, + organizationId: 'orgId', + file: '', + }, + }, + result: { + data: { + createVenue: { + _id: 'newVenue', + }, + }, + }, + }; + + renderVenueModal( + defaultProps, + new StaticMockLink([createVenueMock], true), + ); + + await act(async () => { + await userEvent.type( + screen.getByPlaceholderText('Enter Venue Name'), + 'Test Venue', + ); + await userEvent.type( + screen.getByPlaceholderText('Enter Venue Description'), + ' ', // Only whitespace + ); + await userEvent.type( + screen.getByPlaceholderText('Enter Venue Capacity'), + '100', + ); + fireEvent.click(screen.getByTestId('createVenueBtn')); + }); + + await waitFor(() => { + expect(toast.success).toHaveBeenCalled(); + }); + }); + + test('handles empty image URL during venue update', async () => { + const updateMock = { + request: { + query: UPDATE_VENUE_MUTATION, + variables: { + id: 'venue1', + name: 'Updated Venue', + capacity: 100, + description: '', + file: '', + }, + }, + result: { + data: { + editVenue: { + _id: 'venue1', + }, + }, + }, + }; + + renderVenueModal( + { + ...editProps, + venueData: { + _id: 'venue1', + name: 'Original Venue', + description: '', + image: '', + capacity: '100', + }, + }, + new StaticMockLink([updateMock], true), + ); + + await act(async () => { + fireEvent.change(screen.getByDisplayValue('Original Venue'), { + target: { value: 'Updated Venue' }, + }); + fireEvent.click(screen.getByTestId('updateVenueBtn')); + }); + + await waitFor(() => { + expect(toast.success).toHaveBeenCalledWith( + 'Venue details updated successfully', + ); + }); + }); + + test('handles form submission with undefined venueData in edit mode', async () => { + const editPropsWithUndefinedVenueData = { + ...editProps, + venueData: undefined, + }; + + const mockLink = new StaticMockLink(MOCKS, true); + renderVenueModal(editPropsWithUndefinedVenueData, mockLink); + + // Attempt to submit form + await act(async () => { + fireEvent.click(screen.getByTestId('updateVenueBtn')); + }); + + // This test ensures no runtime errors occur + expect(screen.getByTestId('updateVenueBtn')).toBeInTheDocument(); + }); + + test('handles description truncation', async () => { + renderVenueModal(defaultProps, new StaticMockLink(MOCKS, true)); + const descInput = screen.getByPlaceholderText( + 'Enter Venue Description', + ); + + const longDescription = 'a'.repeat(600); // More than 500 characters + + await act(async () => { + await userEvent.type(descInput, longDescription); + }); + + // Verify that the description is truncated to 500 characters + expect(descInput).toHaveValue(longDescription.slice(0, 500)); + }); + + test('handles mutation errors with custom error messages', async () => { + const errorMock = { + request: { + query: CREATE_VENUE_MUTATION, + variables: { + name: 'Test Venue', + description: 'Test Description', + capacity: 100, + organizationId: 'orgId', + file: '', + }, + }, + error: new Error('Custom error message'), + }; + + renderVenueModal(defaultProps, new StaticMockLink([errorMock], true)); + + await act(async () => { + await userEvent.type( + screen.getByPlaceholderText('Enter Venue Name'), + 'Test Venue', + ); + await userEvent.type( + screen.getByPlaceholderText('Enter Venue Description'), + 'Test Description', + ); + await userEvent.type( + screen.getByPlaceholderText('Enter Venue Capacity'), + '100', + ); + fireEvent.click(screen.getByTestId('createVenueBtn')); + }); + + await waitFor(() => { + expect(toast.error).toHaveBeenCalled(); + }); + }); + }); + }); }); }); diff --git a/src/components/Venues/VenueModal.tsx b/src/components/Venues/VenueModal.tsx index 7b347defed..635daba2fc 100644 --- a/src/components/Venues/VenueModal.tsx +++ b/src/components/Venues/VenueModal.tsx @@ -25,7 +25,7 @@ export interface InterfaceVenueModalProps { * A modal component for creating or updating venue information. * * This component displays a modal window where users can enter details for a venue, such as name, description, capacity, and an image. - * It also handles submitting the form data to create or update a venue based on whether the `edit` prop is true or false. + * It also handles submitting the form data to create or update a venue based on whether the edit prop is true or false. * * @param show - A flag indicating if the modal should be visible. * @param onHide - A function to call when the modal should be closed. @@ -92,56 +92,105 @@ const VenueModal = ({ * * @returns A promise that resolves when the submission is complete. */ + // Update the handleSubmit function in VenueModal.tsx + const handleSubmit = useCallback(async () => { + // Validate name if (formState.name.trim().length === 0) { toast.error(t('venueTitleError') as string); return; } - const capacityNum = parseInt(formState.capacity); - if (isNaN(capacityNum) || capacityNum <= 0) { + // Only validate name uniqueness if it has changed + if (edit && formState.name.trim() === venueData?.name) { + // If name hasn't changed, only update other fields + const variables = { + id: venueData._id, + capacity: parseInt(formState.capacity, 10), + description: formState.description?.trim() || '', + file: formState.imageURL || '', + // Don't include name if it hasn't changed + }; + + console.log('Sending update mutation without name:', variables); + + try { + const result = await mutate({ + variables, + }); + + if (result?.data?.editVenue) { + toast.success(t('venueUpdated')); + refetchVenues(); + onHide(); + } + } catch (error) { + console.error('Mutation error:', error); + errorHandler(t, error); + } + return; + } + + // Validate capacity + const capacityNum = parseInt(formState.capacity, 10); + if (Number.isNaN(capacityNum) || capacityNum <= 0) { toast.error(t('venueCapacityError') as string); return; } try { - const { data } = await mutate({ - variables: { + if (edit && venueData?._id) { + // If name has changed, include all fields + const variables = { + id: venueData._id, + name: formState.name.trim(), capacity: capacityNum, - file: formState.imageURL, - description: formState.description, - name: formState.name, + description: formState.description?.trim() || '', + file: formState.imageURL || '', + }; + + console.log('Sending update mutation with name:', variables); + + const result = await mutate({ + variables, + }); + + if (result?.data?.editVenue) { + toast.success(t('venueUpdated')); + refetchVenues(); + onHide(); + } + } else { + // Create venue case + const variables = { + name: formState.name.trim(), + capacity: capacityNum, + description: formState.description?.trim() || '', + file: formState.imageURL || '', organizationId: orgId, - ...(edit && { id: venueData?._id }), - }, - }); - if (data) { - toast.success( - edit ? (t('venueUpdated') as string) : (t('venueAdded') as string), - ); - refetchVenues(); - onHide(); - setFormState({ - name: '', - description: '', - capacity: '', - imageURL: '', + }; + + const result = await mutate({ + variables, }); - setVenueImage(false); + + if (result?.data?.createVenue) { + toast.success(t('venueCreated')); + refetchVenues(); + onHide(); + } } } catch (error) { - errorHandler(t, error); + console.error('Mutation error:', error); + if (error instanceof Error && error.message.includes('alreadyExists')) { + toast.error( + t('venueNameExists') || 'A venue with this name already exists', + ); + } else { + errorHandler(t, error); + } } - }, [ - edit, - formState, - mutate, - onHide, - orgId, - refetchVenues, - t, - venueData?._id, - ]); + }, [formState, mutate, onHide, refetchVenues, t, venueData, edit, orgId]); /** * Clears the selected image and resets the image preview. @@ -159,12 +208,12 @@ const VenueModal = ({ // Update form state when venueData changes useEffect(() => { setFormState({ - name: venueData?.name || '', - description: venueData?.description || '', - capacity: venueData?.capacity || '', - imageURL: venueData?.image || '', + name: venueData?.name || '', // Prefill name or set as empty + description: venueData?.description || '', // Prefill description + capacity: venueData?.capacity?.toString() || '', // Prefill capacity as a string + imageURL: venueData?.image || '', // Prefill image }); - setVenueImage(venueData?.image ? true : false); + setVenueImage(!!venueData?.image); // Set preview if image exists }, [venueData]); const { name, description, capacity, imageURL } = formState; diff --git a/src/screens/OrganizationDashboard/Leaderboard/Leaderboard.mocks.ts b/src/screens/OrganizationDashboard/Leaderboard/Leaderboard.mocks.ts new file mode 100644 index 0000000000..b6b22c832a --- /dev/null +++ b/src/screens/OrganizationDashboard/Leaderboard/Leaderboard.mocks.ts @@ -0,0 +1,198 @@ +import { VOLUNTEER_RANKING } from 'GraphQl/Queries/EventVolunteerQueries'; + +const rank1 = { + rank: 1, + hoursVolunteered: 5, + user: { + _id: 'userId1', + lastName: 'Bradley', + firstName: 'Teresa', + image: 'image-url', + email: 'testuser4@example.com', + }, +}; + +const rank2 = { + rank: 2, + hoursVolunteered: 4, + user: { + _id: 'userId2', + lastName: 'Garza', + firstName: 'Bruce', + image: null, + email: 'testuser5@example.com', + }, +}; + +const rank3 = { + rank: 3, + hoursVolunteered: 3, + user: { + _id: 'userId3', + lastName: 'Doe', + firstName: 'John', + image: null, + email: 'testuser6@example.com', + }, +}; + +const rank4 = { + rank: 4, + hoursVolunteered: 2, + user: { + _id: 'userId4', + lastName: 'Doe', + firstName: 'Jane', + image: null, + email: 'testuser7@example.com', + }, +}; + +export const MOCKS = [ + { + request: { + query: VOLUNTEER_RANKING, + variables: { + orgId: 'orgId', + where: { + orderBy: 'hours_DESC', + timeFrame: 'allTime', + nameContains: '', + }, + }, + }, + result: { + data: { + getVolunteerRanks: [rank1, rank2, rank3, rank4], + }, + }, + }, + { + request: { + query: VOLUNTEER_RANKING, + variables: { + orgId: 'orgId', + where: { + orderBy: 'hours_ASC', + timeFrame: 'allTime', + nameContains: '', + }, + }, + }, + result: { + data: { + getVolunteerRanks: [rank4, rank3, rank2, rank1], + }, + }, + }, + { + request: { + query: VOLUNTEER_RANKING, + variables: { + orgId: 'orgId', + where: { + orderBy: 'hours_DESC', + timeFrame: 'weekly', + nameContains: '', + }, + }, + }, + result: { + data: { + getVolunteerRanks: [rank1], + }, + }, + }, + { + request: { + query: VOLUNTEER_RANKING, + variables: { + orgId: 'orgId', + where: { + orderBy: 'hours_DESC', + timeFrame: 'monthly', + nameContains: '', + }, + }, + }, + result: { + data: { + getVolunteerRanks: [rank1, rank2], + }, + }, + }, + { + request: { + query: VOLUNTEER_RANKING, + variables: { + orgId: 'orgId', + where: { + orderBy: 'hours_DESC', + timeFrame: 'yearly', + nameContains: '', + }, + }, + }, + result: { + data: { + getVolunteerRanks: [rank1, rank2, rank3], + }, + }, + }, + { + request: { + query: VOLUNTEER_RANKING, + variables: { + orgId: 'orgId', + where: { + orderBy: 'hours_DESC', + timeFrame: 'allTime', + nameContains: 'T', + }, + }, + }, + result: { + data: { + getVolunteerRanks: [rank1], + }, + }, + }, +]; + +export const EMPTY_MOCKS = [ + { + request: { + query: VOLUNTEER_RANKING, + variables: { + orgId: 'orgId', + where: { + orderBy: 'hours_DESC', + timeFrame: 'allTime', + nameContains: '', + }, + }, + }, + result: { + data: { + getVolunteerRanks: [], + }, + }, + }, +]; + +export const ERROR_MOCKS = [ + { + request: { + query: VOLUNTEER_RANKING, + variables: { + orgId: 'orgId', + where: { + orderBy: 'hours_DESC', + timeFrame: 'allTime', + nameContains: '', + }, + }, + }, + error: new Error('Mock Graphql VOLUNTEER_RANKING Error'), + }, +];