diff --git a/docs/docs/auto-docs/screens/OrgPost/OrgPost/functions/default.md b/docs/docs/auto-docs/screens/OrgPost/OrgPost/functions/default.md index 62c0eb7190..9b90cc010b 100644 --- a/docs/docs/auto-docs/screens/OrgPost/OrgPost/functions/default.md +++ b/docs/docs/auto-docs/screens/OrgPost/OrgPost/functions/default.md @@ -6,7 +6,7 @@ > **default**(): `JSX.Element` -Defined in: [src/screens/OrgPost/OrgPost.tsx:70](https://github.com/PalisadoesFoundation/talawa-admin/blob/main/src/screens/OrgPost/OrgPost.tsx#L70) +Defined in: [src/screens/OrgPost/OrgPost.tsx:71](https://github.com/PalisadoesFoundation/talawa-admin/blob/main/src/screens/OrgPost/OrgPost.tsx#L71) This function is used to display the posts of the organization. It displays the posts in a card format. It also provides the functionality to create a new post. The user can also sort the posts based on the date of creation. diff --git a/src/components/CollapsibleDropdown/CollapsibleDropdown.module.css b/src/components/CollapsibleDropdown/CollapsibleDropdown.module.css deleted file mode 100644 index 4337742ecc..0000000000 --- a/src/components/CollapsibleDropdown/CollapsibleDropdown.module.css +++ /dev/null @@ -1,14 +0,0 @@ -.iconWrapper { - width: 36px; -} - -.collapseBtn { - height: 48px; -} - -.iconWrapperSm { - width: 36px; - display: flex; - justify-content: center; - align-items: center; -} diff --git a/src/components/CollapsibleDropdown/CollapsibleDropdown.tsx b/src/components/CollapsibleDropdown/CollapsibleDropdown.tsx index 2991c2ade5..91024e026e 100644 --- a/src/components/CollapsibleDropdown/CollapsibleDropdown.tsx +++ b/src/components/CollapsibleDropdown/CollapsibleDropdown.tsx @@ -1,7 +1,7 @@ import React, { useEffect } from 'react'; import { Button, Collapse } from 'react-bootstrap'; import type { TargetsType } from 'state/reducers/routesReducer'; -import styles from './CollapsibleDropdown.module.css'; +import styles from '../../style/app.module.css'; import IconComponent from 'components/IconComponent/IconComponent'; import { NavLink, useLocation, useNavigate } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; @@ -48,7 +48,7 @@ const collapsibleDropdown = ({ aria-expanded={showDropdown} data-testid="collapsible-dropdown" > -
+
{ @@ -81,7 +81,7 @@ const collapsibleDropdown = ({ }} data-testid={`collapsible-dropdown-btn-${index}`} > -
+
{tCommon(name || '')} diff --git a/src/screens/OrgPost/OrgPost.spec.tsx b/src/screens/OrgPost/OrgPost.spec.tsx index cfe2d8c9bf..60a2734ac3 100644 --- a/src/screens/OrgPost/OrgPost.spec.tsx +++ b/src/screens/OrgPost/OrgPost.spec.tsx @@ -1,5 +1,5 @@ import { MockedProvider } from '@apollo/react-testing'; -import { fireEvent, render, screen } from '@testing-library/react'; +import { fireEvent, render, screen, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { I18nextProvider } from 'react-i18next'; import { Provider } from 'react-redux'; @@ -12,7 +12,7 @@ import { store } from 'state/store'; import { StaticMockLink } from 'utils/StaticMockLink'; import i18nForTest from 'utils/i18nForTest'; import OrgPost from './OrgPost'; -import { vi } from 'vitest'; +import { describe, it, expect, beforeEach, vi } from 'vitest'; const MOCKS = [ { request: { @@ -47,9 +47,9 @@ const MOCKS = [ }, likeCount: 0, commentCount: 0, - comments: [], pinned: true, likedBy: [], + comments: [], }, cursor: '6411e53835d7ba2344a78e21', }, @@ -178,7 +178,125 @@ async function wait(ms = 500): Promise { }); } +const initialMock = { + request: { + query: ORGANIZATION_POST_LIST, + variables: { + id: undefined, + after: null, + before: null, + first: 6, + last: null, + }, + }, + result: { + data: { + organizations: [ + { + posts: { + edges: [], + pageInfo: { + startCursor: 'startCursor1', + endCursor: 'endCursor1', + hasNextPage: true, + hasPreviousPage: false, + }, + totalCount: 10, + }, + }, + ], + }, + }, +}; + +const nextPageMock = { + request: { + query: ORGANIZATION_POST_LIST, + variables: { + id: undefined, + after: 'endCursor1', + before: null, + first: 6, + last: null, + }, + }, + result: { + data: { + organizations: [ + { + posts: { + edges: [], + pageInfo: { + startCursor: 'startCursor2', + endCursor: 'endCursor2', + hasNextPage: false, + hasPreviousPage: true, + }, + totalCount: 10, + }, + }, + ], + }, + }, +}; + +const prevPageMock = { + request: { + query: ORGANIZATION_POST_LIST, + variables: { + id: undefined, + after: null, + before: 'startCursor2', + first: null, + last: 6, + }, + }, + result: { + data: { + organizations: [ + { + posts: { + edges: [], + pageInfo: { + startCursor: 'startCursor1', + endCursor: 'endCursor1', + hasNextPage: true, + hasPreviousPage: false, + }, + totalCount: 10, + }, + }, + ], + }, + }, +}; + +const successMock = { + request: { + query: CREATE_POST_MUTATION, + variables: { + title: 'Test Post', + text: 'Test Content', + file: '', + pinned: false, + }, + }, + result: { + data: { + createPost: { + _id: '123', + title: 'Test Post', + text: 'Test Content', + }, + }, + }, +}; + describe('Organisation Post Page', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + const formData = { posttitle: 'dummy post', postinfo: 'This is a dummy post', @@ -186,7 +304,121 @@ describe('Organisation Post Page', () => { postVideo: new File(['hello'], 'hello.mp4', { type: 'video/mp4' }), }; - test('correct mock data should be queried', async () => { + it('handleAddMediaChange: uploading a file and verifying the preview', async () => { + render( + + + + + + + + + , + ); + + await wait(); + + userEvent.click(screen.getByTestId('createPostModalBtn')); + + const fileInput = screen.getByTestId('addMediaField'); + userEvent.upload(fileInput, formData.postImage); + + const imagePreview = await screen.findByAltText('Post Image Preview'); + expect(imagePreview).toBeInTheDocument(); + + const closeButton = screen.getByTestId('mediaCloseButton'); + fireEvent.click(closeButton); + expect(imagePreview).not.toBeInTheDocument(); + }); + + it('handleSearch: searching by title and text', async () => { + render( + + + + + + + + + , + ); + + await wait(); + + const searchInput = screen.getByPlaceholderText(/Search By/i); + userEvent.type(searchInput, 'postone{enter}'); + expect(searchInput).toHaveValue('postone'); + + const sortDropdown = screen.getByTestId('sort'); + userEvent.click(sortDropdown); + }); + + it('createPost: creating a post with and without media, and error handling', async () => { + render( + + + + + + + + + , + ); + + await wait(); + + userEvent.click(screen.getByTestId('createPostModalBtn')); + + const postTitleInput = screen.getByTestId('modalTitle'); + fireEvent.change(postTitleInput, { target: { value: 'Test Post' } }); + + const postInfoTextarea = screen.getByTestId('modalinfo'); + fireEvent.change(postInfoTextarea, { + target: { value: 'Test post information' }, + }); + + const createPostBtn = screen.getByTestId('createPostBtn'); + fireEvent.click(createPostBtn); + + await wait(); + + userEvent.click(screen.getByTestId('closeOrganizationModal')); + }); + + it('Modal interactions: opening and closing the modal', async () => { + render( + + + + + + + + + , + ); + + await wait(); + + const createPostModalBtn = screen.getByTestId('createPostModalBtn'); + userEvent.click(createPostModalBtn); + + const modalTitle = screen.getByTestId('modalOrganizationHeader'); + expect(modalTitle).toBeInTheDocument(); + + const closeButton = screen.getByTestId(/modalOrganizationHeader/i); + userEvent.click(closeButton); + + await wait(); + + const closedModalTitle = screen.queryByText(/postDetail/i); + expect(closedModalTitle).not.toBeInTheDocument(); + }); + + it('correct mock data should be queried', async () => { const dataQuery1 = MOCKS[0]?.result?.data?.organizations[0].posts.edges[0]; expect(dataQuery1).toEqual({ @@ -213,7 +445,7 @@ describe('Organisation Post Page', () => { }); }); - test('Testing create post functionality', async () => { + it('Testing create post functionality', async () => { render( @@ -247,7 +479,7 @@ describe('Organisation Post Page', () => { userEvent.click(screen.getByTestId('closeOrganizationModal')); }, 15000); - test('Testing search functionality', async () => { + it('Testing search functionality', async () => { render( @@ -272,9 +504,9 @@ describe('Organisation Post Page', () => { const sortDropdown = screen.getByTestId('sort'); userEvent.click(sortDropdown); }); - test('Testing search text and title toggle', async () => { + + it('Testing search text and title toggle', async () => { await act(async () => { - // Wrap the test code in act render( @@ -315,9 +547,9 @@ describe('Organisation Post Page', () => { expect(searchInput).toHaveAttribute('placeholder', 'Search By Title'); }); - test('Testing search latest and oldest toggle', async () => { + + it('Testing search latest and oldest toggle', async () => { await act(async () => { - // Wrap the test code in act render( @@ -358,11 +590,12 @@ describe('Organisation Post Page', () => { }); expect(searchInput).toBeInTheDocument(); }); - test('After creating a post, the data should be refetched', async () => { + + it('After creating a post, the data should be refetched', async () => { const refetchMock = vi.fn(); render( - + @@ -378,8 +611,6 @@ describe('Organisation Post Page', () => { userEvent.click(screen.getByTestId('createPostModalBtn')); - // Fill in post form fields... - userEvent.click(screen.getByTestId('createPostBtn')); await wait(); @@ -387,7 +618,7 @@ describe('Organisation Post Page', () => { expect(refetchMock).toHaveBeenCalledTimes(0); }); - test('Create post without media', async () => { + it('Create post without media', async () => { render( @@ -416,80 +647,6 @@ describe('Organisation Post Page', () => { fireEvent.click(createPostBtn); }, 15000); - test('Create post and preview', async () => { - render( - - - - - - - - - - , - ); - - await wait(); - userEvent.click(screen.getByTestId('createPostModalBtn')); - - const postTitleInput = screen.getByTestId('modalTitle'); - fireEvent.change(postTitleInput, { target: { value: 'Test Post' } }); - - const postInfoTextarea = screen.getByTestId('modalinfo'); - fireEvent.change(postInfoTextarea, { - target: { value: 'Test post information' }, - }); - - // Simulate uploading an image - const imageFile = new File(['image content'], 'image.png', { - type: 'image/png', - }); - const imageInput = screen.getByTestId('addMediaField'); - userEvent.upload(imageInput, imageFile); - - // Check if the image is displayed - const imagePreview = await screen.findByAltText('Post Image Preview'); - expect(imagePreview).toBeInTheDocument(); - - // Check if the close button for the image works - const closeButton = screen.getByTestId('mediaCloseButton'); - fireEvent.click(closeButton); - - // Check if the image is removed from the preview - expect(imagePreview).not.toBeInTheDocument(); - }, 15000); - - test('Modal opens and closes', async () => { - render( - - - - - - - - - , - ); - - await wait(); - - const createPostModalBtn = screen.getByTestId('createPostModalBtn'); - - userEvent.click(createPostModalBtn); - - const modalTitle = screen.getByTestId('modalOrganizationHeader'); - expect(modalTitle).toBeInTheDocument(); - - const closeButton = screen.getByTestId(/modalOrganizationHeader/i); - userEvent.click(closeButton); - - await wait(); - - const closedModalTitle = screen.queryByText(/postDetail/i); - expect(closedModalTitle).not.toBeInTheDocument(); - }); it('renders the form with input fields and buttons', async () => { render( @@ -507,7 +664,6 @@ describe('Organisation Post Page', () => { await wait(); userEvent.click(screen.getByTestId('createPostModalBtn')); - // Check if input fields and buttons are present expect(screen.getByTestId('modalTitle')).toBeInTheDocument(); expect(screen.getByTestId('modalinfo')).toBeInTheDocument(); expect(screen.getByTestId('createPostBtn')).toBeInTheDocument(); @@ -530,7 +686,6 @@ describe('Organisation Post Page', () => { await wait(); userEvent.click(screen.getByTestId('createPostModalBtn')); - // Simulate user input fireEvent.change(screen.getByTestId('modalTitle'), { target: { value: 'Test Title' }, }); @@ -538,12 +693,11 @@ describe('Organisation Post Page', () => { target: { value: 'Test Info' }, }); - // Check if input values are set correctly expect(screen.getByTestId('modalTitle')).toHaveValue('Test Title'); expect(screen.getByTestId('modalinfo')).toHaveValue('Test Info'); }); - test('allows users to upload an image', async () => { + it('allows users to upload an image', async () => { render( @@ -579,7 +733,8 @@ describe('Organisation Post Page', () => { const closeButton = screen.getByTestId('mediaCloseButton'); fireEvent.click(closeButton); }, 15000); - test('Create post, preview image, and close preview', async () => { + + it('Create post, preview image, and close preview', async () => { await act(async () => { render( @@ -620,19 +775,17 @@ describe('Organisation Post Page', () => { userEvent.upload(screen.getByTestId('addMediaField'), videoFile); }); - // Check if the video is displayed const videoPreview = await screen.findByTestId('videoPreview'); expect(videoPreview).toBeInTheDocument(); - // Check if the close button for the video works const closeVideoPreviewButton = screen.getByTestId('mediaCloseButton'); await act(async () => { fireEvent.click(closeVideoPreviewButton); }); expect(videoPreview).not.toBeInTheDocument(); }); - test('Sorting posts by pinned status', async () => { - // Mocked data representing posts with different pinned statuses + + it('Sorting posts by pinned status', async () => { const mockedPosts = [ { _id: '1', @@ -656,7 +809,6 @@ describe('Organisation Post Page', () => { }, ]; - // Render the OrgPost component and pass the mocked data to it render( @@ -674,7 +826,6 @@ describe('Organisation Post Page', () => { const sortedPosts = screen.getAllByTestId('post-item'); - // Assert that the posts are sorted correctly expect(sortedPosts).toHaveLength(mockedPosts.length); expect(sortedPosts[0]).toHaveTextContent( 'postoneThis is the first po... Aditya Shelke', @@ -686,4 +837,331 @@ describe('Organisation Post Page', () => { 'posttwoTis is the post two Aditya Shelke', ); }); + + it('successful post creation should reset form and close modal', async () => { + const customMocks = [...MOCKS, successMock]; + const customLink = new StaticMockLink(customMocks, true); + + render( + + + + + + + + + + , + ); + + await wait(); + + userEvent.click(screen.getByTestId('createPostModalBtn')); + + userEvent.type(screen.getByTestId('modalTitle'), 'Test Post'); + userEvent.type(screen.getByTestId('modalinfo'), 'Test Content'); + + await act(async () => { + fireEvent.click(screen.getByTestId('createPostBtn')); + }); + + await wait(); + + expect( + screen.queryByTestId('modalOrganizationHeader'), + ).not.toBeInTheDocument(); + + userEvent.click(screen.getByTestId('createPostModalBtn')); + expect(screen.getByTestId('modalTitle')).toHaveValue(''); + expect(screen.getByTestId('modalinfo')).toHaveValue(''); + expect(screen.getByTestId('pinPost')).not.toBeChecked(); + + const toastContainer = screen.getByRole('alert'); + expect(toastContainer).toBeInTheDocument(); + }); + + it('pagination controls work correctly', async () => { + const mockPostEdges = + MOCKS[0]?.result?.data?.organizations[0].posts.edges ?? []; + const paginationMock = { + request: { + query: ORGANIZATION_POST_LIST, + variables: { + id: undefined, + after: null, + before: null, + first: 6, + last: null, + }, + }, + result: { + data: { + organizations: [ + { + posts: { + edges: mockPostEdges, + pageInfo: { + startCursor: 'startCursor123', + endCursor: 'endCursor123', + hasNextPage: true, + hasPreviousPage: true, + }, + totalCount: 10, + }, + }, + ], + }, + }, + }; + + const customMocks = [...MOCKS, paginationMock]; + const customLink = new StaticMockLink(customMocks, true); + + render( + + + + + + + + + , + ); + + await wait(); + + const nextButton = screen.getByRole('button', { name: /next/i }); + const previousButton = screen.getByRole('button', { name: /previous/i }); + + expect(nextButton).toBeInTheDocument(); + expect(previousButton).toBeInTheDocument(); + + await act(async () => { + fireEvent.click(nextButton); + }); + await wait(); + + await act(async () => { + fireEvent.click(previousButton); + }); + await wait(); + + expect(nextButton).toBeInTheDocument(); + expect(previousButton).toBeInTheDocument(); + }); + + it('pagination buttons states are correctly set', async () => { + const mockPostEdges = + MOCKS[0]?.result?.data?.organizations[0].posts.edges ?? []; + const noPaginationMock = { + request: { + query: ORGANIZATION_POST_LIST, + variables: { + id: undefined, + after: null, + before: null, + first: 6, + last: null, + }, + }, + result: { + data: { + organizations: [ + { + posts: { + edges: mockPostEdges, + pageInfo: { + startCursor: 'startCursor123', + endCursor: 'endCursor123', + hasNextPage: false, + hasPreviousPage: false, + }, + totalCount: 4, + }, + }, + ], + }, + }, + }; + + const customMocks = [...MOCKS, noPaginationMock]; + const customLink = new StaticMockLink(customMocks, true); + + render( + + + + + + + + + , + ); + + await wait(); + + const nextButton = screen.getByRole('button', { name: /next/i }); + const previousButton = screen.getByRole('button', { name: /previous/i }); + + expect(nextButton).toHaveAttribute('disabled'); + expect(previousButton).toHaveAttribute('disabled'); + }); + + it('handleNextPage updates pagination variables correctly', async () => { + render( + + + + + + + + + , + ); + + await waitFor(() => { + expect(screen.getByTestId('nextButton')).not.toBeDisabled(); + }); + + await waitFor(() => { + fireEvent.click(screen.getByTestId('previousButton')); + }); + + await waitFor(() => { + fireEvent.click(screen.getByTestId('nextButton')); + expect(screen.getByTestId('nextButton')).toBeDisabled(); + }); + }); + + it('handlePreviousPage updates pagination variables correctly', async () => { + render( + + + + + + + + + , + ); + + await waitFor(() => { + expect(screen.getByTestId('nextButton')).not.toBeDisabled(); + }); + + await act(async () => { + fireEvent.click(screen.getByTestId('nextButton')); + }); + + await waitFor(() => { + expect(screen.getByTestId('previousButton')).not.toBeDisabled(); + }); + + await act(async () => { + fireEvent.click(screen.getByTestId('previousButton')); + }); + + await waitFor(() => { + expect(screen.getByTestId('previousButton')).toBeDisabled(); + expect(screen.getByTestId('nextButton')).not.toBeDisabled(); + }); + }); + + it('handles create post error cases', async () => { + const errorMock = { + request: { + query: CREATE_POST_MUTATION, + variables: { + title: '', + text: '', + organizationId: 'undefined', + file: '', + pinned: false, + }, + }, + error: new Error('Text fields cannot be empty strings'), + }; + + render( + + + + + + + + + + , + ); + + await wait(); + + userEvent.click(screen.getByTestId('createPostModalBtn')); + + await act(async () => { + fireEvent.click(screen.getByTestId('createPostBtn')); + }); + + await wait(); + + const toastContainer = screen.getByRole('alert'); + expect(toastContainer).toBeInTheDocument(); + }); + + it('handles empty search input', async () => { + render( + + + + + + + + + , + ); + + await wait(); + + const searchInput = screen.getByTestId('searchByName'); + fireEvent.change(searchInput, { target: { value: '' } }); + + await wait(); + + expect(searchInput).toHaveValue(''); + }); + + it('handles file input change with no file selected', async () => { + render( + + + + + + + + + , + ); + + await wait(); + + userEvent.click(screen.getByTestId('createPostModalBtn')); + + const fileInput = screen.getByTestId('addMediaField'); + fireEvent.change(fileInput, { target: { files: [] } }); + + await wait(); + + expect(screen.queryByTestId('mediaPreview')).not.toBeInTheDocument(); + }); }); diff --git a/src/screens/OrgPost/OrgPost.tsx b/src/screens/OrgPost/OrgPost.tsx index 857071ca25..2371b9e264 100644 --- a/src/screens/OrgPost/OrgPost.tsx +++ b/src/screens/OrgPost/OrgPost.tsx @@ -1,4 +1,5 @@ -import { useMutation, useQuery, type ApolloError } from '@apollo/client'; +import type { ApolloError } from '@apollo/client'; +import { useMutation, useQuery } from '@apollo/client'; import { CREATE_POST_MUTATION } from 'GraphQl/Mutations/mutations'; import { ORGANIZATION_POST_LIST } from 'GraphQl/Queries/Queries'; import Loader from 'components/Loader/Loader'; @@ -383,6 +384,7 @@ function orgPost(): JSX.Element { !orgPostListData?.organizations[0].posts.pageInfo .hasPreviousPage } + data-testid="previousButton" > {t('Previous')} @@ -394,6 +396,7 @@ function orgPost(): JSX.Element { disabled={ !orgPostListData?.organizations[0].posts.pageInfo.hasNextPage } + data-testid="nextButton" > {t('Next')} diff --git a/src/screens/OrganizationActionItems/ItemModal.spec.tsx b/src/screens/OrganizationActionItems/ItemModal.spec.tsx index a3467d4cf6..044fcc26cb 100644 --- a/src/screens/OrganizationActionItems/ItemModal.spec.tsx +++ b/src/screens/OrganizationActionItems/ItemModal.spec.tsx @@ -778,6 +778,142 @@ describe('Testing ItemModal', () => { }); }); + it('handles infinite for allottedHours', async () => { + renderItemModal(link1, itemProps[0]); + const hoursInput = screen.getByLabelText(t.allottedHours); + + // Required field setup for form submission + const categorySelect = screen.getByTestId('categorySelect'); + fireEvent.mouseDown(within(categorySelect).getByRole('combobox')); + fireEvent.click(await screen.findByText('Category 1')); + + const memberSelect = screen.getByTestId('memberSelect'); + fireEvent.mouseDown(within(memberSelect).getByRole('combobox')); + fireEvent.click(await screen.findByText('Harve Lance')); + + // Test Infinity + fireEvent.change(hoursInput, { target: { value: Infinity } }); + expect(hoursInput).toHaveValue(''); + + // Test -Infinity + fireEvent.change(hoursInput, { target: { value: -Infinity } }); + expect(hoursInput).toHaveValue(''); + + // Test Number.POSITIVE_INFINITY and Number.NEGATIVE_INFINITY + fireEvent.change(hoursInput, { + target: { value: Number.POSITIVE_INFINITY }, + }); + expect(hoursInput).toHaveValue(''); + + fireEvent.change(hoursInput, { + target: { value: Number.NEGATIVE_INFINITY }, + }); + expect(hoursInput).toHaveValue(''); + }); + + it('should not allow letters or negative values in allotted hours', async () => { + renderItemModal(link1, itemProps[0]); + const hoursInput = screen.getByLabelText(t.allottedHours); + expect(hoursInput).toBeInTheDocument(); + + // Test letter input + fireEvent.change(hoursInput, { target: { value: 'abc' } }); + await waitFor(() => { + expect(hoursInput).toHaveValue(''); + }); + + // Test negative value + fireEvent.change(hoursInput, { target: { value: '-5' } }); + await waitFor(() => { + expect(hoursInput).toHaveValue(''); + }); + + // Test zero as boundary + fireEvent.change(hoursInput, { target: { value: '0' } }); + await waitFor(() => { + expect(hoursInput).toHaveValue('0'); + }); + + // Test maximum allowed value + fireEvent.change(hoursInput, { target: { value: '999999' } }); + await waitFor(() => { + expect(hoursInput).toHaveValue('999999'); + }); + }); + + it('validates allotted hours maximum values', async () => { + renderItemModal(link1, itemProps[0]); + const hoursInput = screen.getByLabelText(t.allottedHours); + + // Test various large values + const testCases = [ + { input: '9007199254740991', expected: '9007199254740991' }, // MAX_SAFE_INTEGER + { input: '9007199254740992', expected: '9007199254740992' }, // MAX_SAFE_INTEGER + 1 + ]; + + for (const { input, expected } of testCases) { + fireEvent.change(hoursInput, { target: { value: input } }); + await waitFor(() => { + expect(hoursInput).toHaveValue(expected); + }); + } + + // Test that reasonable large values are still accepted + const validLargeValues = ['1000', '9999', '99999']; + for (const value of validLargeValues) { + fireEvent.change(hoursInput, { target: { value } }); + await waitFor(() => { + expect(hoursInput).toHaveValue(value); + }); + } + }); + + it('validates allottedHours edge cases', async () => { + renderItemModal(link1, itemProps[0]); + const allottedHours = screen.getByLabelText(t.allottedHours); + + // Test invalid string + fireEvent.change(allottedHours, { target: { value: 'invalid' } }); + expect(allottedHours).toHaveValue(''); + + // Test NaN + fireEvent.change(allottedHours, { target: { value: NaN } }); + expect(allottedHours).toHaveValue(''); + + // Test negative number + fireEvent.change(allottedHours, { target: { value: -5 } }); + expect(allottedHours).toHaveValue(''); + + // Test boundary values + fireEvent.change(allottedHours, { + target: { value: Number.MAX_SAFE_INTEGER }, + }); + expect(allottedHours).toHaveValue('9007199254740991'); + + // Test decimal values - according to the component's implementation, + // it uses parseInt() so decimals should be truncated + fireEvent.change(allottedHours, { target: { value: 5.7 } }); + expect(allottedHours).toHaveValue('5'); + + // Required fields for form submission + const categorySelect = screen.getByTestId('categorySelect'); + fireEvent.mouseDown(within(categorySelect).getByRole('combobox')); + fireEvent.click(await screen.findByText('Category 1')); + + const memberSelect = screen.getByTestId('memberSelect'); + fireEvent.mouseDown(within(memberSelect).getByRole('combobox')); + fireEvent.click(await screen.findByText('Harve Lance')); + + // Test form submission with valid number + fireEvent.change(allottedHours, { target: { value: 10 } }); + const submitButton = screen.getByTestId('submitBtn'); + fireEvent.click(submitButton); + + await waitFor(() => { + expect(toast.success).toHaveBeenCalledWith(t.successfulCreation); + }); + }); + it('should fail to Create Action Item', async () => { renderItemModal(link2, itemProps[0]); // Click Submit @@ -790,6 +926,249 @@ describe('Testing ItemModal', () => { }); }); + it('handles empty strings in all text fields', async () => { + renderItemModal(link1, itemProps[0]); + + // Fill required fields first since they're needed for form submission + const categorySelect = screen.getByTestId('categorySelect'); + fireEvent.mouseDown(within(categorySelect).getByRole('combobox')); + fireEvent.click(await screen.findByText('Category 1')); + + const memberSelect = screen.getByTestId('memberSelect'); + fireEvent.mouseDown(within(memberSelect).getByRole('combobox')); + fireEvent.click(await screen.findByText('Harve Lance')); + + // Test empty strings in optional fields + const preCompletionNotes = screen.getByLabelText(t.preCompletionNotes); + fireEvent.change(preCompletionNotes, { target: { value: '' } }); + expect(preCompletionNotes).toHaveValue(''); + + const allottedHours = screen.getByLabelText(t.allottedHours); + fireEvent.change(allottedHours, { target: { value: '' } }); + expect(allottedHours).toHaveValue(''); + + // Submit form + const submitButton = screen.getByTestId('submitBtn'); + fireEvent.click(submitButton); + + await waitFor(() => { + expect(toast.success).toHaveBeenCalledWith(t.successfulCreation); + // Verify optional fields remain empty after submission + expect(preCompletionNotes).toHaveValue(''); + expect(allottedHours).toHaveValue(''); + }); + }); + + it('handles whitespace-only strings', async () => { + renderItemModal(link1, itemProps[0]); + + // Select Category 1 + const categorySelect = screen.getByTestId('categorySelect'); + fireEvent.mouseDown(within(categorySelect).getByRole('combobox')); + fireEvent.click(await screen.findByText('Category 1')); + + // Select assignee + const memberSelect = screen.getByTestId('memberSelect'); + fireEvent.mouseDown(within(memberSelect).getByRole('combobox')); + fireEvent.click(await screen.findByText('Harve Lance')); + + const preCompletionNotes = screen.getByLabelText(t.preCompletionNotes); + + // Test various whitespace combinations + fireEvent.change(preCompletionNotes, { target: { value: ' ' } }); + expect(preCompletionNotes).toHaveValue(' '); + + // Test leading/trailing whitespace + fireEvent.change(preCompletionNotes, { target: { value: ' test ' } }); + expect(preCompletionNotes).toHaveValue(' test '); + + // Test multiple consecutive spaces + fireEvent.change(preCompletionNotes, { target: { value: 'test test' } }); + expect(preCompletionNotes).toHaveValue('test test'); + + // Test tab characters + fireEvent.change(preCompletionNotes, { target: { value: '\ttest\t' } }); + expect(preCompletionNotes).toHaveValue('\ttest\t'); + + const submitButton = screen.getByTestId('submitBtn'); + fireEvent.click(submitButton); + await waitFor(() => { + expect(toast.success).toHaveBeenCalledWith(t.successfulCreation); + }); + }); + + it('handles special characters in text fields', async () => { + renderItemModal(link1, itemProps[0]); + + // Select Category 1 + const categorySelect = screen.getByTestId('categorySelect'); + fireEvent.mouseDown(within(categorySelect).getByRole('combobox')); + fireEvent.click(await screen.findByText('Category 1')); + + // Select assignee + const memberSelect = screen.getByTestId('memberSelect'); + fireEvent.mouseDown(within(memberSelect).getByRole('combobox')); + fireEvent.click(await screen.findByText('Harve Lance')); + + const preCompletionNotes = screen.getByLabelText(t.preCompletionNotes); + + // Test basic special characters + fireEvent.change(preCompletionNotes, { + target: { value: '!@#$%^&*()_+-=[]{}|;:,.<>?' }, + }); + expect(preCompletionNotes).toHaveValue('!@#$%^&*()_+-=[]{}|;:,.<>?'); + + // Test Unicode characters including emojis and international characters + fireEvent.change(preCompletionNotes, { + target: { value: '🚀 Unicode Test 你好 ñ é è ü ö 한글 עברית العربية' }, + }); + expect(preCompletionNotes).toHaveValue( + '🚀 Unicode Test 你好 ñ é è ü ö 한글 עברית العربية', + ); + + // Test HTML-like content + fireEvent.change(preCompletionNotes, { + target: { value: '
Test
' }, + }); + expect(preCompletionNotes).toHaveValue( + '
Test
', + ); + + // Test SQL-like characters and quotes + fireEvent.change(preCompletionNotes, { + target: { value: '\'; DROP TABLE users; -- "quoted" text\'s test' }, + }); + expect(preCompletionNotes).toHaveValue( + '\'; DROP TABLE users; -- "quoted" text\'s test', + ); + + // Test mathematical and currency symbols + fireEvent.change(preCompletionNotes, { + target: { value: '∑ π ∆ ∞ € £ ¥ ₹ ± ≠ ≈ ∴ ∵' }, + }); + expect(preCompletionNotes).toHaveValue('∑ π ∆ ∞ € £ ¥ ₹ ± ≠ ≈ ∴ ∵'); + + const submitButton = screen.getByTestId('submitBtn'); + fireEvent.click(submitButton); + await waitFor(() => { + expect(toast.success).toHaveBeenCalledWith(t.successfulCreation); + // Verify the last special character value persists after submission + expect(preCompletionNotes).toHaveValue('∑ π ∆ ∞ € £ ¥ ₹ ± ≠ ≈ ∴ ∵'); + }); + }); + + it('handles extremely long text input', async () => { + renderItemModal(link1, itemProps[0]); + + // Select Category + const categorySelect = screen.getByTestId('categorySelect'); + fireEvent.mouseDown(within(categorySelect).getByRole('combobox')); + fireEvent.click(await screen.findByText('Category 1')); + + // Select assignee + const memberSelect = screen.getByTestId('memberSelect'); + fireEvent.mouseDown(within(memberSelect).getByRole('combobox')); + fireEvent.click(await screen.findByText('Harve Lance')); + + const preCompletionNotes = screen.getByLabelText(t.preCompletionNotes); + + // Test various lengths + const lengths = [100, 500, 1000, 5000, 10000]; + for (const length of lengths) { + fireEvent.change(preCompletionNotes, { + target: { value: 'a'.repeat(length) }, + }); + expect(preCompletionNotes).toHaveValue('a'.repeat(length)); + } + + // Test with long repeated words + const longWords = Array(100) + .fill('supercalifragilisticexpialidocious') + .join(' '); + fireEvent.change(preCompletionNotes, { + target: { value: longWords }, + }); + expect(preCompletionNotes).toHaveValue(longWords); + + // Verify form submission with long text + const submitButton = screen.getByTestId('submitBtn'); + fireEvent.click(submitButton); + await waitFor(() => { + expect(toast.success).toHaveBeenCalledWith(t.successfulCreation); + // Verify the long text persists after submission + expect(preCompletionNotes).toHaveValue(longWords); + }); + }); + + it('handles rapid form field changes', async () => { + renderItemModal(link1, itemProps[0]); + + // Required fields setup + const categorySelect = screen.getByTestId('categorySelect'); + fireEvent.mouseDown(within(categorySelect).getByRole('combobox')); + fireEvent.click(await screen.findByText('Category 1')); + + const memberSelect = screen.getByTestId('memberSelect'); + fireEvent.mouseDown(within(memberSelect).getByRole('combobox')); + fireEvent.click(await screen.findByText('Harve Lance')); + + const hoursInput = screen.getByLabelText(t.allottedHours); + + // Test rapid sequential changes + const values = Array.from({ length: 100 }, (_, i) => i.toString()); + + // Fire all changes in immediate succession + values.forEach((value) => { + fireEvent.change(hoursInput, { target: { value } }); + expect(hoursInput).toHaveValue(value); + }); + + // Test rapid changes with special values interspersed + const mixedValues = [ + '50', + '', // Empty value + '51', + 'abc', // Invalid value + '52', + '-1', // Negative value + '53', + '99999', // Large value + '54', + ]; + + mixedValues.forEach((value) => { + fireEvent.change(hoursInput, { target: { value } }); + // For valid numbers, should have the value + // For invalid/empty/negative values, should be empty + const expectedValue = /^\d+$/.test(value) ? value : ''; + expect(hoursInput).toHaveValue(expectedValue); + }); + + // Verify final value after rapid changes + expect(hoursInput).toHaveValue('54'); + + // Test rapid changes to multiple fields simultaneously + const preCompletionNotes = screen.getByLabelText(t.preCompletionNotes); + + // Rapidly alternate between changing hours and notes + for (let i = 0; i < 10; i++) { + fireEvent.change(hoursInput, { target: { value: i.toString() } }); + fireEvent.change(preCompletionNotes, { target: { value: `Note ${i}` } }); + + expect(hoursInput).toHaveValue(i.toString()); + expect(preCompletionNotes).toHaveValue(`Note ${i}`); + } + + const submitButton = screen.getByTestId('submitBtn'); + fireEvent.click(submitButton); + await waitFor(() => { + expect(toast.success).toHaveBeenCalledWith(t.successfulCreation); + // Verify final values persist after submission + expect(hoursInput).toHaveValue('9'); + expect(preCompletionNotes).toHaveValue('Note 9'); + }); + }); + it('No Fields Updated while Updating', async () => { renderItemModal(link2, itemProps[2]); // Click Submit @@ -802,6 +1181,267 @@ describe('Testing ItemModal', () => { }); }); + //checking for empty and null values + it('handles empty and null form values correctly', async () => { + renderItemModal(link1, itemProps[0]); + + const allottedHours = screen.getByLabelText(t.allottedHours); + const preCompletionNotes = screen.getByLabelText(t.preCompletionNotes); + + // Test empty values + fireEvent.change(allottedHours, { target: { value: '' } }); + expect(allottedHours).toHaveValue(''); + + fireEvent.change(preCompletionNotes, { target: { value: '' } }); + expect(preCompletionNotes).toHaveValue(''); + + // Test null values + fireEvent.change(allottedHours, { target: { value: null } }); + expect(allottedHours).toHaveValue(''); + + fireEvent.change(preCompletionNotes, { target: { value: null } }); + expect(preCompletionNotes).toHaveValue(''); + + // Test undefined values + fireEvent.change(allottedHours, { target: { value: undefined } }); + expect(allottedHours).toHaveValue(''); + + fireEvent.change(preCompletionNotes, { target: { value: undefined } }); + expect(preCompletionNotes).toHaveValue(''); + + // Test form submission with missing required fields + const submitButton = screen.getByTestId('submitBtn'); + fireEvent.click(submitButton); + + // Form should not submit without required fields + await waitFor(() => { + // Verify required fields are marked as required + const categorySelect = screen.getByTestId('categorySelect'); + const categoryInput = within(categorySelect).getByRole('combobox'); + expect(categoryInput).toBeRequired(); + + // Verify form values remain empty + expect(allottedHours).toHaveValue(''); + expect(preCompletionNotes).toHaveValue(''); + }); + + // Fill required fields and test submission + const categorySelect = screen.getByTestId('categorySelect'); + fireEvent.mouseDown(within(categorySelect).getByRole('combobox')); + fireEvent.click(await screen.findByText('Category 1')); + + const memberSelect = screen.getByTestId('memberSelect'); + fireEvent.mouseDown(within(memberSelect).getByRole('combobox')); + fireEvent.click(await screen.findByText('Harve Lance')); + + // Submit form with empty optional fields + fireEvent.click(submitButton); + await waitFor(() => { + expect(toast.success).toHaveBeenCalledWith(t.successfulCreation); + // Verify optional fields remain empty after successful submission + expect(allottedHours).toHaveValue(''); + expect(preCompletionNotes).toHaveValue(''); + }); + }); + + // validation of catergory selection + it('validates category selection', async () => { + renderItemModal(link1, itemProps[0]); + + const categorySelect = await screen.findByTestId('categorySelect'); + const inputField = within(categorySelect).getByRole('combobox'); + + // Test initial state and required validation + const submitButton = screen.getByTestId('submitBtn'); + fireEvent.click(submitButton); + expect(inputField).toBeRequired(); + + // Test valid category selection + fireEvent.mouseDown(inputField); + const categoryOption = await screen.findByText('Category 1'); + fireEvent.click(categoryOption); + expect(inputField).toHaveValue('Category 1'); + + // Test direct input of invalid category + fireEvent.change(inputField, { target: { value: 'Invalid Category' } }); + // Autocomplete should show no options for invalid input + await waitFor(() => { + expect(screen.queryByText('Invalid Category')).not.toBeInTheDocument(); + }); + + // Clear selection + fireEvent.change(inputField, { target: { value: '' } }); + fireEvent.blur(inputField); + expect(inputField).toHaveValue(''); + + // Test cycling through multiple valid categories + const validCategories = ['Category 1', 'Category 2']; + for (const category of validCategories) { + fireEvent.mouseDown(inputField); + const option = await screen.findByText(category); + fireEvent.click(option); + expect(inputField).toHaveValue(category); + } + + // Test form submission with valid category + const memberSelect = screen.getByTestId('memberSelect'); + fireEvent.mouseDown(within(memberSelect).getByRole('combobox')); + fireEvent.click(await screen.findByText('Harve Lance')); + + fireEvent.click(submitButton); + await waitFor(() => { + expect(toast.success).toHaveBeenCalledWith(t.successfulCreation); + }); + }); + + // changing of assignee type handling + it('handles assignee type changes correctly', async () => { + renderItemModal(link1, itemProps[1]); + + // First select required category + const categorySelect = screen.getByTestId('categorySelect'); + fireEvent.mouseDown(within(categorySelect).getByRole('combobox')); + fireEvent.click(await screen.findByText('Category 1')); + + // Test individual volunteer selection + const groupRadio = await screen.findByText(t.groups); + const individualRadio = await screen.findByText(t.individuals); + + fireEvent.click(individualRadio); + const volunteerSelect = await screen.findByTestId('volunteerSelect'); + expect(volunteerSelect).toBeInTheDocument(); + + // Test selecting an individual volunteer + const volunteerInput = within(volunteerSelect).getByRole('combobox'); + fireEvent.mouseDown(volunteerInput); + fireEvent.click(await screen.findByText('Teresa Bradley')); + expect(volunteerInput).toHaveValue('Teresa Bradley'); + + // Test switching to group selection + fireEvent.click(groupRadio); + const groupSelect = await screen.findByTestId('volunteerGroupSelect'); + expect(groupSelect).toBeInTheDocument(); + + // Test selecting a group + const groupInput = within(groupSelect).getByRole('combobox'); + fireEvent.mouseDown(groupInput); + fireEvent.click(await screen.findByText('group1')); + expect(groupInput).toHaveValue('group1'); + + // Test switching back to individual + fireEvent.click(individualRadio); + const newVolunteerSelect = await screen.findByTestId('volunteerSelect'); + expect(newVolunteerSelect).toBeInTheDocument(); + + // Test submission without selection + const newVolunteerInput = within(newVolunteerSelect).getByRole('combobox'); + const submitButton = screen.getByTestId('submitBtn'); + fireEvent.change(newVolunteerInput, { target: { value: '' } }); + fireEvent.blur(newVolunteerInput); + fireEvent.click(submitButton); + await waitFor(() => { + expect(newVolunteerInput).toBeRequired(); + }); + }); + + // for handling null change of date + it('handles date changes correctly', async () => { + renderItemModal(link1, itemProps[0]); + + // First select required fields + const categorySelect = screen.getByTestId('categorySelect'); + fireEvent.mouseDown(within(categorySelect).getByRole('combobox')); + fireEvent.click(await screen.findByText('Category 1')); + + const memberSelect = screen.getByTestId('memberSelect'); + fireEvent.mouseDown(within(memberSelect).getByRole('combobox')); + fireEvent.click(await screen.findByText('Harve Lance')); + + const dateInput = screen.getByLabelText(t.dueDate); + + // Test past date input + const pastDate = '01/01/2020'; + fireEvent.change(dateInput, { target: { value: pastDate } }); + expect(dateInput).toHaveValue(pastDate); + + // Test valid future date + const futureDate = '01/01/2025'; + fireEvent.change(dateInput, { target: { value: futureDate } }); + expect(dateInput).toHaveValue(futureDate); + + // Test form submission with valid date + const submitButton = screen.getByTestId('submitBtn'); + fireEvent.click(submitButton); + await waitFor(() => { + expect(toast.success).toHaveBeenCalledWith(t.successfulCreation); + }); + }); + + // for handling form state changes and validations + it('handles all form state changes and validations', async () => { + renderItemModal(link1, itemProps[1]); + + // Test category selection + const categorySelect = screen.getByTestId('categorySelect'); + fireEvent.mouseDown(within(categorySelect).getByRole('combobox')); + fireEvent.click(await screen.findByText('Category 1')); + + // Test assignee type changes + const groupRadio = screen.getByLabelText(t.groups); + fireEvent.click(groupRadio); + + const groupSelect = await screen.getByTestId('volunteerGroupSelect'); + fireEvent.mouseDown(within(groupSelect).getByRole('combobox')); + fireEvent.click(await screen.findByText('group1')); + + // Test date changes + const dateInput = screen.getByLabelText(t.dueDate); + fireEvent.change(dateInput, { target: { value: '' } }); + fireEvent.change(dateInput, { target: { value: '01/01/2024' } }); + + // Test allotted hours with various inputs + const hoursInput = screen.getByLabelText(t.allottedHours); + ['abc', '-5', '', '0', '10'].forEach((value) => { + fireEvent.change(hoursInput, { target: { value } }); + }); + + // Test notes + const notesInput = screen.getByLabelText(t.preCompletionNotes); + fireEvent.change(notesInput, { target: { value: 'Test notes' } }); + + const submitButton = screen.getByTestId('submitBtn'); + fireEvent.click(submitButton); + + await waitFor(() => { + expect(toast.success).toHaveBeenCalledWith(t.successfulCreation); + }); + }); + + // for handling edge cases in timezone + it('handles timezone edge cases', async () => { + renderItemModal(link1, itemProps[0]); + + await waitFor(async () => { + const dateInput = screen.getByLabelText(t.dueDate); + + // Test dates around DST changes + const dstDates = [ + '03/12/2025', // Spring forward + '11/05/2025', // Fall back + ]; + + for (const date of dstDates) { + fireEvent.change(dateInput, { target: { value: date } }); + expect(dateInput).toHaveValue(date); + } + + // Test midnight boundary dates + fireEvent.change(dateInput, { target: { value: '01/01/2025' } }); + expect(dateInput).toHaveValue('01/01/2025'); + }); + }); + + // For testing failure of updating action item it('should fail to Update Action Item', async () => { renderItemModal(link2, itemProps[2]); expect(screen.getAllByText(t.updateActionItem)).toHaveLength(2); @@ -820,4 +1460,55 @@ describe('Testing ItemModal', () => { expect(toast.error).toHaveBeenCalledWith('Mock Graphql Error'); }); }); + + it('handles potentially malicious input patterns correctly', async () => { + renderItemModal(link1, itemProps[0]); + + // Select required fields + const categorySelect = screen.getByTestId('categorySelect'); + fireEvent.mouseDown(within(categorySelect).getByRole('combobox')); + fireEvent.click(await screen.findByText('Category 1')); + + const memberSelect = screen.getByTestId('memberSelect'); + fireEvent.mouseDown(within(memberSelect).getByRole('combobox')); + fireEvent.click(await screen.findByText('Harve Lance')); + + const preCompletionNotes = screen.getByLabelText(t.preCompletionNotes); + + // Test HTML-like content + fireEvent.change(preCompletionNotes, { + target: { value: '
Test
' }, + }); + expect(preCompletionNotes).toHaveValue( + '
Test
', + ); + + // Test common XSS patterns + const xssPatterns = [ + '', + 'javascript:alert(1)', + '">', + '', + '\'--"', + '"; DROP TABLE users; --', + '${alert(1)}', + "{{constructor.constructor('alert(1)')()}}", + ]; + + for (const pattern of xssPatterns) { + fireEvent.change(preCompletionNotes, { target: { value: pattern } }); + expect(preCompletionNotes).toHaveValue(pattern); + } + + // Test form submission with special characters + const submitButton = screen.getByTestId('submitBtn'); + fireEvent.click(submitButton); + await waitFor(() => { + expect(toast.success).toHaveBeenCalledWith(t.successfulCreation); + // Verify the last input value persists after submission + expect(preCompletionNotes).toHaveValue( + xssPatterns[xssPatterns.length - 1], + ); + }); + }); }); diff --git a/src/screens/OrganizationActionItems/ItemModal.tsx b/src/screens/OrganizationActionItems/ItemModal.tsx index 98a4611bf6..b7ca0accae 100644 --- a/src/screens/OrganizationActionItems/ItemModal.tsx +++ b/src/screens/OrganizationActionItems/ItemModal.tsx @@ -226,6 +226,20 @@ const ItemModal: FC = ({ field: keyof InterfaceFormStateType, value: string | number | boolean | Date | undefined | null, ): void => { + // Special handling for allottedHours + if (field === 'allottedHours') { + // If the value is not a valid number or is negative, set to null + const numValue = typeof value === 'string' ? Number(value) : value; + if ( + typeof numValue !== 'number' || + Number.isNaN(numValue) || + numValue < 0 + ) { + setFormState((prevState) => ({ ...prevState, [field]: null })); + return; + } + } + setFormState((prevState) => ({ ...prevState, [field]: value })); }; @@ -574,8 +588,6 @@ const ItemModal: FC = ({ className={styles.noOutline} value={dayjs(dueDate)} onChange={(date: Dayjs | null): void => { - // Added istanbul ignore else, which will ignore else condition, we are not using else condition here - /* istanbul ignore else -- @preserve */ if (date) handleFormChange('dueDate', date.toDate()); }} /> diff --git a/src/style/app.module.css b/src/style/app.module.css index 1a604d7409..0861af482e 100644 --- a/src/style/app.module.css +++ b/src/style/app.module.css @@ -216,7 +216,7 @@ --input-area-color: #f1f3f6; --date-picker-background: #f2f2f2; --grey-bg-color-dark: #707070; - --gret-bg-color: #4b5563; + --grey-bg-color-agenda-category: #4b5563; --dropdown-border-color: #cccccc; --primary-border-solid: 1px solid var(--dropdown-border-color); --disabled-btn: #e7f0fe; @@ -9128,6 +9128,23 @@ button[data-testid='createPostBtn'] { } } +/* CollapsibleDropdown */ + +.collapsibleDropdownIconWrapper { + width: 36px; +} + +.collapsibleDropdownCollapseBtn { + height: 48px; +} + +.collapsibleDropdownIconWrapperSm { + width: 36px; + display: flex; + justify-content: center; + align-items: center; +} + /* AgendaCategory.tsx */ .createModalAgendaCategory { @@ -9143,7 +9160,7 @@ button[data-testid='createPostBtn'] { } .titlemodalAgendaCategory { - color: var(--grey-bg-color); + color: var( --grey-bg-color-agenda-category); font-weight: 600; font-size: 20px; margin-bottom: 20px;