From efe0ef23cc502da6bdd9ad6f02fb5baa159c4024 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 18 Nov 2024 13:47:31 +0000 Subject: [PATCH 01/27] fix(deps): update dependency com.nimbusds:nimbus-jose-jwt to v9.46 (#470) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> (cherry picked from commit c065264e87901fa7ba08eeb6ad491aa5036656b4) --- backend/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/pom.xml b/backend/pom.xml index bf211e04..03fc389f 100644 --- a/backend/pom.xml +++ b/backend/pom.xml @@ -545,7 +545,7 @@ com.nimbusds nimbus-jose-jwt - 9.45 + 9.46 org.testcontainers From c4ccfe8bfef266fa165347299926ef415b6e0bd9 Mon Sep 17 00:00:00 2001 From: Paulo Gomes da Cruz Junior Date: Mon, 18 Nov 2024 06:10:43 -0800 Subject: [PATCH 02/27] chore: removing old and unused class reference --- .../Openings/SearchScreenDataTable/index.tsx | 44 +++++++++++++++---- 1 file changed, 35 insertions(+), 9 deletions(-) diff --git a/frontend/src/components/SilvicultureSearch/Openings/SearchScreenDataTable/index.tsx b/frontend/src/components/SilvicultureSearch/Openings/SearchScreenDataTable/index.tsx index 1b62d519..def31764 100644 --- a/frontend/src/components/SilvicultureSearch/Openings/SearchScreenDataTable/index.tsx +++ b/frontend/src/components/SilvicultureSearch/Openings/SearchScreenDataTable/index.tsx @@ -41,16 +41,18 @@ import { } from "../../../../utils/fileConversions"; import { useNavigate } from "react-router-dom"; import { setOpeningFavorite } from '../../../../services/OpeningFavouriteService'; +import { usePostViewedOpening } from "../../../../services/queries/dashboard/dashboardQueries"; import { useNotification } from "../../../../contexts/NotificationProvider"; import TruncatedText from "../../../TruncatedText"; import FriendlyDate from "../../../FriendlyDate"; +import ComingSoonModal from "../../../ComingSoonModal"; interface ISearchScreenDataTable { rows: OpeningsSearch[]; headers: ITableHeader[]; defaultColumns: ITableHeader[]; - handleCheckboxChange: (columnKey: string) => void; + handleCheckboxChange: (columnKey: string) => void; toggleSpatial: () => void; showSpatial: boolean; totalItems: number; @@ -80,12 +82,15 @@ const SearchScreenDataTable: React.FC = ({ const [openEdit, setOpenEdit] = useState(false); const [openDownload, setOpenDownload] = useState(false); const [selectedRows, setSelectedRows] = useState([]); // State to store selected rows + const [openingDetails, setOpeningDetails] = useState(''); + const { mutate: markAsViewedOpening, isError, error } = usePostViewedOpening(); const navigate = useNavigate(); // This ref is used to calculate the width of the container for each cell const cellRefs = useRef([]); // Holds the with of each cell in the table const [cellWidths, setCellWidths] = useState([]); + const { displayNotification } = useNotification(); useEffect(() => { const widths = cellRefs.current.map((cell: ICellRefs) => cell.offsetWidth || 0); @@ -119,6 +124,18 @@ const SearchScreenDataTable: React.FC = ({ const { displayNotification } = useNotification(); + const handleRowClick = (openingId: string) => { + // Call the mutation to mark as viewed + markAsViewedOpening(openingId, { + onSuccess: () => { + setOpeningDetails(openingId.toString()); + }, + onError: (err: any) => { + // Display error notification (UI needs to be designed for this) + } + }); + }; + //Function to handle the favourite feature of the opening for a user const handleFavouriteOpening = (openingId: string) => { try{ @@ -134,7 +151,7 @@ const SearchScreenDataTable: React.FC = ({ }) } catch (error) { console.error(`Failed to update favorite status for ${openingId}`); - } + } } return ( @@ -307,16 +324,23 @@ const SearchScreenDataTable: React.FC = ({ {rows && rows.map((row: any, i: number) => ( - + { + if(header.key !== "actions"){ + handleRowClick(row.openingId); + } + }} + > {headers.map((header) => header.selected ? ( (cellRefs.current[i] = el)} key={header.key} className={ - header.key === "actions" && showSpatial ? "p-0" : - header.elipsis ? "ellipsis" : - null + header.key === "actions" && showSpatial + ? "p-0" + : null } > {header.key === "statusDescription" ? ( @@ -373,8 +397,8 @@ const SearchScreenDataTable: React.FC = ({ ) : header.header === "Category" ? ( - ) : header.key === 'disturbanceStartDate' ? ( @@ -419,7 +443,9 @@ const SearchScreenDataTable: React.FC = ({ handleItemsPerPageChange(page, pageSize); }} /> - )} + )} + + ); }; From 83cd8e2be0d0b2e16a3e13a6ab2362996c53a9c6 Mon Sep 17 00:00:00 2001 From: Paulo Gomes da Cruz Junior Date: Tue, 19 Nov 2024 06:16:19 -0800 Subject: [PATCH 03/27] fix(SILVA-570): fixing recent openings --- .../db/migration/V1__create_schema.sql | 16 + .../components/ActionButtons.test.tsx | 39 +++ .../components/BarChartGrouped.test.tsx | 52 +-- .../components/ComingSoonModal.test.tsx | 27 ++ .../Opening/RecentOpeningsDataTable.test.tsx | 69 ++++ .../__test__/components/OpeningsTab.test.tsx | 73 +++-- .../Openings/SearchScreenDataTable.test.tsx | 306 ++++++++++++++++-- .../components/SpatialCheckbox.test.tsx | 61 ++++ .../components/TableCellContent.test.tsx | 98 ++++++ .../components/TableRowComponent.test.tsx | 88 +++++ .../src/__test__/screens/Opening.test.tsx | 62 ++-- .../__test__/services/OpeningService.test.ts | 52 --- .../dashboard/dashboardQueries.test.tsx | 112 +++++++ .../services/search/openings.test.tsx | 38 ++- .../src/components/ActionButtons/index.tsx | 32 ++ .../src/components/ComingSoonModal/index.tsx | 26 ++ .../components/ComingSoonModal/styles.scss | 7 + .../RecentOpeningsDataTable/headerData.ts | 21 ++ .../Opening/RecentOpeningsDataTable/index.tsx | 110 +++++++ .../RecentOpeningsDataTable/styles.scss | 165 ++++++++++ .../{testData.ts => headerData.ts} | 0 frontend/src/components/OpeningsTab/index.tsx | 61 +--- .../Openings/OpeningsSearchTab/index.tsx | 2 +- .../SearchScreenDataTable/headerData.ts | 55 ++++ .../Openings/SearchScreenDataTable/index.tsx | 2 - .../SearchScreenDataTable/testData.ts | 284 ---------------- .../src/components/SpatialCheckbox/index.tsx | 28 ++ .../src/components/TableCellContent/index.tsx | 50 +++ .../components/TableRowComponent/index.tsx | 43 +++ frontend/src/services/OpeningService.ts | 44 --- .../queries/dashboard/dashboardQueries.ts | 32 ++ .../services/queries/search/openingQueries.ts | 11 +- frontend/src/services/search/openings.ts | 33 +- 33 files changed, 1574 insertions(+), 525 deletions(-) create mode 100644 frontend/src/__test__/components/ActionButtons.test.tsx create mode 100644 frontend/src/__test__/components/ComingSoonModal.test.tsx create mode 100644 frontend/src/__test__/components/Dashboard/Opening/RecentOpeningsDataTable.test.tsx create mode 100644 frontend/src/__test__/components/SpatialCheckbox.test.tsx create mode 100644 frontend/src/__test__/components/TableCellContent.test.tsx create mode 100644 frontend/src/__test__/components/TableRowComponent.test.tsx create mode 100644 frontend/src/__test__/services/queries/dashboard/dashboardQueries.test.tsx create mode 100644 frontend/src/components/ActionButtons/index.tsx create mode 100644 frontend/src/components/ComingSoonModal/index.tsx create mode 100644 frontend/src/components/ComingSoonModal/styles.scss create mode 100644 frontend/src/components/Dashboard/Opening/RecentOpeningsDataTable/headerData.ts create mode 100644 frontend/src/components/Dashboard/Opening/RecentOpeningsDataTable/index.tsx create mode 100644 frontend/src/components/Dashboard/Opening/RecentOpeningsDataTable/styles.scss rename frontend/src/components/OpeningScreenDataTable/{testData.ts => headerData.ts} (100%) create mode 100644 frontend/src/components/SilvicultureSearch/Openings/SearchScreenDataTable/headerData.ts delete mode 100644 frontend/src/components/SilvicultureSearch/Openings/SearchScreenDataTable/testData.ts create mode 100644 frontend/src/components/SpatialCheckbox/index.tsx create mode 100644 frontend/src/components/TableCellContent/index.tsx create mode 100644 frontend/src/components/TableRowComponent/index.tsx create mode 100644 frontend/src/services/queries/dashboard/dashboardQueries.ts diff --git a/backend/src/main/resources/db/migration/V1__create_schema.sql b/backend/src/main/resources/db/migration/V1__create_schema.sql index d40f9e76..660adddf 100644 --- a/backend/src/main/resources/db/migration/V1__create_schema.sql +++ b/backend/src/main/resources/db/migration/V1__create_schema.sql @@ -44,3 +44,19 @@ CREATE TABLE IF NOT EXISTS silva.oracle_extraction_logs ( CONSTRAINT oracle_extraction_logs_pk PRIMARY KEY(id) ); + +-- Create sequence if it doesn't exist for User Recent Openings +CREATE SEQUENCE IF NOT EXISTS silva.user_recent_openings_seq +START WITH 1 +INCREMENT BY 1 +MINVALUE 1 +NO MAXVALUE +CACHE 30; + +-- Use the sequence in your table creation or insert statements +CREATE TABLE IF NOT EXISTS silva.user_recent_openings ( + id BIGINT PRIMARY KEY DEFAULT nextval('silva.user_recent_openings_seq'), + opening_id VARCHAR(255) NOT NULL, + user_id VARCHAR(255) NOT NULL, + last_viewed TIMESTAMP DEFAULT NOW() +); diff --git a/frontend/src/__test__/components/ActionButtons.test.tsx b/frontend/src/__test__/components/ActionButtons.test.tsx new file mode 100644 index 00000000..6f554d9b --- /dev/null +++ b/frontend/src/__test__/components/ActionButtons.test.tsx @@ -0,0 +1,39 @@ +// ActionButtons.test.tsx +import React from "react"; +import { vi } from "vitest"; +import { render, screen, fireEvent } from "@testing-library/react"; +import ActionButtons from "../../components/ActionButtons"; + +// Mock console.log +const consoleLogMock = vi.spyOn(console, "log").mockImplementationOnce(() =>vi.fn()) ; + +afterEach(() => { + consoleLogMock.mockClear(); +}); + +afterAll(() => { + consoleLogMock.mockRestore(); +}); + +describe("ActionButtons", () => { + const rowId = "test-row-id"; + + it("renders the 'View' and 'Document Download' buttons", () => { + render(); + + // Check that both buttons are in the document + expect(screen.getByRole("button", { name: /View/i })).toBeInTheDocument(); + expect(screen.getByRole("button", { name: /Document Download/i })).toBeInTheDocument(); + }); + + it("calls console.log with rowId when the 'View' button is clicked", () => { + render(); + + // Find the "View" button and click it + const viewButton = screen.getByRole("button", { name: /View/i }); + fireEvent.click(viewButton); + + // Check if console.log was called with the correct rowId + expect(consoleLogMock).toHaveBeenCalledWith(rowId); + }); +}); diff --git a/frontend/src/__test__/components/BarChartGrouped.test.tsx b/frontend/src/__test__/components/BarChartGrouped.test.tsx index 16255636..f1d12e98 100644 --- a/frontend/src/__test__/components/BarChartGrouped.test.tsx +++ b/frontend/src/__test__/components/BarChartGrouped.test.tsx @@ -1,26 +1,40 @@ import React from 'react'; -import { render, screen, waitFor } from '@testing-library/react'; -import { describe, expect, it, vi } from 'vitest'; +import { render, screen } from '@testing-library/react'; +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import BarChartGrouped from '../../components/BarChartGrouped'; -import { fetchOpeningsPerYear } from '../../services/OpeningService'; - -vi.mock('../../services/OpeningService', () => ({ - fetchOpeningsPerYear: vi.fn(() => Promise.resolve([ - { group: '2022', key: 'Openings', value: 10 }, - { group: '2023', key: 'Openings', value: 15 }, - ])), +import { useDistrictListQuery, useFetchOpeningsPerYear } from '../../services/queries/dashboard/dashboardQueries'; +import { describe, expect, it } from 'vitest'; +import { vi } from 'vitest'; +import '@testing-library/jest-dom'; +// Mock the hook +vi.mock('../../services/queries/dashboard/dashboardQueries', () => ({ + useFetchOpeningsPerYear: vi.fn(), + useDistrictListQuery: vi.fn(), })); -describe('BarChartGrouped component tests', () => { - it('should render loading state while fetching data and clean it after', async () => { - render(); +const queryClient = new QueryClient(); - const element = await waitFor(() => screen.getByText('Loading...')); +describe('BarChartGrouped component', () => { + it('should display loading state when data is fetching', () => { + // Mock loading state for openings data + (useFetchOpeningsPerYear as any).mockReturnValue({ + data: [], + isLoading: true, + }); - expect(element).toBeDefined(); - - expect(fetchOpeningsPerYear).toHaveBeenCalled(); - expect(screen.queryByTestId('bar-chart')).toBeDefined(); - }); + // If you're using useDistrictListQuery, mock it too + (useDistrictListQuery as any).mockReturnValue({ + data: [], + isLoading: false, + }); -}); + render( + + + + ); + + // Check if loading text is displayed + expect(screen.getByText('Loading...')).toBeInTheDocument(); + }); +}); \ No newline at end of file diff --git a/frontend/src/__test__/components/ComingSoonModal.test.tsx b/frontend/src/__test__/components/ComingSoonModal.test.tsx new file mode 100644 index 00000000..037e6f3c --- /dev/null +++ b/frontend/src/__test__/components/ComingSoonModal.test.tsx @@ -0,0 +1,27 @@ +//vite test for the ComingSoonModal component +import React from 'react'; +import { render, screen } from '@testing-library/react'; +import { describe, expect, it, vi } from 'vitest'; +import ComingSoonModal from '../../components/ComingSoonModal'; + +describe('ComingSoonModal', () => { + it('renders the modal', () => { + render(); + expect(screen.getByText('Coming Soon')).toBeInTheDocument(); + expect(screen.getByText('An opening details page is in development.')).toBeInTheDocument(); + }); + + it('renders the modal with the correct opening ID', () => { + render(); + expect(screen.getByText('Opening ID: 1234')).toBeInTheDocument(); + }); + + it('calls the setOpeningDetails function when the modal is closed', async() => { + const setOpeningDetails = vi.fn(); + render(); + const closeButton = screen.getByRole('button', { name: 'Close' }); + await closeButton.click(); + expect(setOpeningDetails).toHaveBeenCalled(); + }); + +}); \ No newline at end of file diff --git a/frontend/src/__test__/components/Dashboard/Opening/RecentOpeningsDataTable.test.tsx b/frontend/src/__test__/components/Dashboard/Opening/RecentOpeningsDataTable.test.tsx new file mode 100644 index 00000000..cb8c535f --- /dev/null +++ b/frontend/src/__test__/components/Dashboard/Opening/RecentOpeningsDataTable.test.tsx @@ -0,0 +1,69 @@ +// src/__test__/components/SilvicultureSearch/Openings/OpeningsSearchBar.test.tsx + +import React from "react"; +import { render, screen } from "@testing-library/react"; +import "@testing-library/jest-dom"; +import { vi } from "vitest"; +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import PaginationProvider from "../../../../contexts/PaginationProvider"; +import RecentOpeningsDataTable from "../../../../components/Dashboard/Opening/RecentOpeningsDataTable"; +import { MemoryRouter } from "react-router-dom"; +import exp from "constants"; + +describe("OpeningsSearchBar", () => { + // Create a new QueryClient instance for each test + const queryClient = new QueryClient(); + const handleCheckboxChange = vi.fn() + const setLoadId = vi.fn() + const toggleSpatial = vi.fn() + const showSpatial = false + const data = { data: [], perPage: 0, totalPages: 0 } + const headers = [] + + it("shows appropriate message when no data is in the table", () => { + render( + + + + + + + + ); + expect(screen.getByText(/There are no openings to show yet/i)).toBeInTheDocument(); + expect(screen.queryByText(/Your recent openings will appear here once you generate one/i)).toBeInTheDocument(); + }); + + it("renders a blank table when rows is empty array", () => { + render( + + + + + + + + ); + // Check if the table is present + const table = screen.getByRole('table'); + expect(table).toBeInTheDocument(); + }); +}); \ No newline at end of file diff --git a/frontend/src/__test__/components/OpeningsTab.test.tsx b/frontend/src/__test__/components/OpeningsTab.test.tsx index 10bd1b32..d96aa034 100644 --- a/frontend/src/__test__/components/OpeningsTab.test.tsx +++ b/frontend/src/__test__/components/OpeningsTab.test.tsx @@ -4,8 +4,7 @@ import { render, act, waitFor, screen } from '@testing-library/react'; import OpeningsTab from '../../components/OpeningsTab'; import { AuthProvider } from '../../contexts/AuthProvider'; import { getWmsLayersWhitelistUsers } from '../../services/SecretsService'; -import { fetchRecentOpenings } from '../../services/OpeningService'; -import { RecentOpening } from '../../types/RecentOpening'; +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import PaginationProvider from '../../contexts/PaginationProvider'; @@ -20,32 +19,66 @@ vi.mock('../../services/OpeningService', async () => { fetchRecentOpenings: vi.fn(), }; }); - - -const rows: RecentOpening[] = [{ - id: '123', - openingId: '123', - fileId: '1', - cuttingPermit: '1', - timberMark: '1', - cutBlock: '1', - grossAreaHa: 1, - statusDesc: 'Approved', - categoryDesc: 'Another:Another', - disturbanceStart: '1', - entryTimestamp: '1', - updateTimestamp: '1', -}]; +const queryClient = new QueryClient(); describe('Openings Tab test',() => { it('should render properly',async () =>{ (getWmsLayersWhitelistUsers as vi.Mock).mockResolvedValue([{userName: 'TEST'}]); - (fetchRecentOpenings as vi.Mock).mockResolvedValue(rows); + await act(async () => { - render(); + render( + + + + + + + + ); }); expect(screen.getByText('Recent openings')).toBeInTheDocument(); }); + it('should have Hide map when the showSpatial is true',async () =>{ + (getWmsLayersWhitelistUsers as vi.Mock).mockResolvedValue([{userName: 'TEST'}]); + + await act(async () => { + render( + + + + + + + + ); + }); + expect(screen.getByText('Hide map')).toBeInTheDocument(); + }); + + it('should render the table', async () => { + // Mocking state values + vi.spyOn(React, 'useState') + .mockImplementationOnce(() => [null, vi.fn()]) // for loadId + .mockImplementationOnce(() => [true, vi.fn()]) // for openingPolygonNotFound + .mockImplementationOnce(() => [{ userName: 'TEST' }, vi.fn()]) // for wmsUsersWhitelist + .mockImplementationOnce(() => [[], vi.fn()]); // for headers + + (getWmsLayersWhitelistUsers as vi.Mock).mockResolvedValue([{ userName: 'TEST' }]); + + await act(async () => { + render( + + + + + + + + ); + }); + expect(screen.getByRole('table')).toBeInTheDocument(); + }); + }); \ No newline at end of file diff --git a/frontend/src/__test__/components/SilvicultureSearch/Openings/SearchScreenDataTable.test.tsx b/frontend/src/__test__/components/SilvicultureSearch/Openings/SearchScreenDataTable.test.tsx index b2d2e752..a739ceac 100644 --- a/frontend/src/__test__/components/SilvicultureSearch/Openings/SearchScreenDataTable.test.tsx +++ b/frontend/src/__test__/components/SilvicultureSearch/Openings/SearchScreenDataTable.test.tsx @@ -1,15 +1,245 @@ import React from 'react'; import { render } from '@testing-library/react'; import { describe, expect, it, vi } from 'vitest'; -import SearchScreenDataTable from '../../../../components/SilvicultureSearch/Openings/SearchScreenDataTable/index'; -import { columns, rows } from '../../../../components/SilvicultureSearch/Openings/SearchScreenDataTable/testData'; +import SearchScreenDataTable from '../../../../components/SilvicultureSearch/Openings/SearchScreenDataTable'; +import { columns } from '../../../../components/SilvicultureSearch/Openings/SearchScreenDataTable/headerData'; import PaginationProvider from '../../../../contexts/PaginationProvider'; import { NotificationProvider } from '../../../../contexts/NotificationProvider'; import { BrowserRouter } from 'react-router-dom'; import { OpeningsSearchProvider } from '../../../../contexts/search/OpeningsSearch'; +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; const handleCheckboxChange = vi.fn(); const toggleSpatial = vi.fn(); +const queryClient = new QueryClient(); + +const rows:any = [ + { + id: '114207', + openingId: '114207', + fileId: 'TFL47', + cuttingPermit: '12S', + timberMark: '47/12S', + cutBlock: '12-69', + grossAreaHa: '12.9', + status: 'Free growing', + category: 'FTML', + disturbanceStart: '-', + createdAt: '2022-10-27', + orgUnit: 'DCC - Cariboo chilcotin natural resources', + lastViewed: '2022-10-27' + }, + { + id: '114206', + openingId: '114206', + fileId: 'TFL47', + cuttingPermit: '12T', + timberMark: '47/12S', + cutBlock: '12-69', + grossAreaHa: '12.9', + status: 'Free growing', + category: 'FTML', + disturbanceStart: '-', + createdAt: '2022-09-04', + orgUnit: 'DCC - Cariboo chilcotin natural resources', + lastViewed: '2022-10-27' + }, + { + id: '114205', + openingId: '114205', + fileId: 'TFL47', + cuttingPermit: '12T', + timberMark: '47/12S', + cutBlock: '12-44A', + grossAreaHa: '12.9', + status: 'Free growing', + category: 'FTML', + disturbanceStart: '-', + createdAt: '2022-09-04', + orgUnit: 'DCC - Cariboo chilcotin natural resources', + lastViewed: '2022-10-27' + }, + { + id: '114204', + openingId: '114204', + fileId: 'TFL47', + cuttingPermit: '12T', + timberMark: '47/12S', + cutBlock: '12-44A', + grossAreaHa: '12.9', + status: 'Active', + category: 'FTML', + disturbanceStart: '-', + createdAt: '2022-01-16', + orgUnit: 'DCC - Cariboo chilcotin natural resources', + lastViewed: '2022-10-26' + }, + { + id: '114203', + openingId: '114203', + fileId: 'TFL47', + cuttingPermit: '12T', + timberMark: '47/12S', + cutBlock: '12-44A', + grossAreaHa: '12.9', + status: 'Active', + category: 'FTML', + disturbanceStart: '-', + createdAt: '2021-12-08', + orgUnit: 'DCC - Cariboo chilcotin natural resources', + lastViewed: '2022-10-26' + }, + { + id: '114202', + openingId: '114202', + fileId: 'TFL47', + cuttingPermit: '12T', + timberMark: '47/12S', + cutBlock: '12-44A', + grossAreaHa: '12.9', + status: 'Free growing', + category: 'FTML', + disturbanceStart: '-', + createdAt: '2021-11-15', + orgUnit: 'DCC - Cariboo chilcotin natural resources', + lastViewed: '2022-10-25' + }, + { + id: '114201', + openingId: '114201', + fileId: 'TFL47', + cuttingPermit: '12T', + timberMark: '47/12S', + cutBlock: '12-44A', + grossAreaHa: '12.9', + status: 'Free growing', + category: 'FTML', + disturbanceStart: '-', + createdAt: '2021-11-15', + orgUnit: 'DCC - Cariboo chilcotin natural resources', + lastViewed: '2022-10-25' + }, + { + id: '114200', + openingId: '114200', + fileId: 'TFL47', + cuttingPermit: '12T', + timberMark: '47/12S', + cutBlock: '12-44A', + grossAreaHa: '12.9', + status: 'Active', + category: 'FTML', + disturbanceStart: '-', + createdAt: '2021-10-20', + orgUnit: 'DCC - Cariboo chilcotin natural resources', + lastViewed: '2022-10-24' + }, + { + id: '114199', + openingId: '114199', + fileId: 'TFL47', + cuttingPermit: '12T', + timberMark: '47/12S', + cutBlock: '12-44A', + grossAreaHa: '12.9', + status: 'Active', + category: 'FTML', + disturbanceStart: '-', + createdAt: '2021-10-20', + orgUnit: 'DCC - Cariboo chilcotin natural resources', + lastViewed: '2022-10-24' + }, + { + id: '114198', + openingId: '114198', + fileId: 'TFL47', + cuttingPermit: '12T', + timberMark: '47/12S', + cutBlock: '12-44A', + grossAreaHa: '12.9', + status: 'Free growing', + category: 'FTML', + disturbanceStart: '-', + createdAt: '2021-09-12', + orgUnit: 'DCC - Cariboo chilcotin natural resources', + lastViewed: '2022-10-23' + }, + { + id: '114197', + openingId: '114197', + fileId: 'TFL47', + cuttingPermit: '12T', + timberMark: '47/12S', + cutBlock: '12-44A', + grossAreaHa: '12.9', + status: 'Free growing', + category: 'FTML', + disturbanceStart: '-', + createdAt: '2021-09-12', + orgUnit: 'DCC - Cariboo chilcotin natural resources', + lastViewed: '2022-10-23' + }, + { + id: '114196', + openingId: '114196', + fileId: 'TFL47', + cuttingPermit: '12T', + timberMark: '47/12S', + cutBlock: '12-44A', + grossAreaHa: '12.9', + status: 'Free growing', + category: 'FTML', + disturbanceStart: '-', + createdAt: '2021-08-05', + orgUnit: 'DCC - Cariboo chilcotin natural resources', + lastViewed: '2022-10-22' + }, + { + id: '114195', + openingId: '114195', + fileId: 'TFL47', + cuttingPermit: '12T', + timberMark: '47/12S', + cutBlock: '12-44A', + grossAreaHa: '12.9', + status: 'Free growing', + category: 'FTML', + disturbanceStart: '-', + createdAt: '2021-08-05', + orgUnit: 'DCC - Cariboo chilcotin natural resources', + lastViewed: '2022-10-22' + }, + { + id: '114194', + openingId: '114194', + fileId: 'TFL47', + cuttingPermit: '12T', + timberMark: '47/12S', + cutBlock: '12-44A', + grossAreaHa: '12.9', + status: 'Active', + category: 'FTML', + disturbanceStart: '-', + createdAt: '2021-07-10', + orgUnit: 'DCC - Cariboo chilcotin natural resources', + lastViewed: '2022-10-21' + }, + { + id: '114193', + openingId: '114193', + fileId: 'TFL47', + cuttingPermit: '12T', + timberMark: '47/12S', + cutBlock: '12-44A', + grossAreaHa: '12.9', + status: 'Active', + category: 'FTML', + disturbanceStart: '-', + createdAt: '2021-07-10', + orgUnit: 'DCC - Cariboo chilcotin natural resources', + lastViewed: '2022-10-21' + } +]; describe('Search Screen Data table test', () => { @@ -17,21 +247,23 @@ describe('Search Screen Data table test', () => { const { getByText, container } = render( - - - - - - - + + + + + + + + + ); @@ -45,11 +277,42 @@ describe('Search Screen Data table test', () => { const { getByText, container } = render( + + + + + + + + + + + ); + + expect(container).toBeInTheDocument(); + expect(container.querySelector('.total-search-results')).toBeInTheDocument(); + expect(container.querySelector('.total-search-results')).toContainHTML('Total Search Results'); + expect(container.querySelector('.total-search-results')).toContainHTML('0'); + }); + + + it('should render the checkbox for showSPatial being true', () => { + render( + + { + ); + const checkbox = document.querySelector('.cds--checkbox-group'); + expect(checkbox).toBeInTheDocument(); - expect(container).toBeInTheDocument(); - expect(container.querySelector('.total-search-results')).toBeInTheDocument(); - expect(container.querySelector('.total-search-results')).toContainHTML('Total Search Results'); - expect(container.querySelector('.total-search-results')).toContainHTML('0'); }); }); \ No newline at end of file diff --git a/frontend/src/__test__/components/SpatialCheckbox.test.tsx b/frontend/src/__test__/components/SpatialCheckbox.test.tsx new file mode 100644 index 00000000..267bbdf8 --- /dev/null +++ b/frontend/src/__test__/components/SpatialCheckbox.test.tsx @@ -0,0 +1,61 @@ +// SpatialCheckbox.test.tsx +import React from "react"; +import { vi } from "vitest"; +import { render, screen, fireEvent } from "@testing-library/react"; +import SpatialCheckbox from "../../components/SpatialCheckbox"; + +describe("SpatialCheckbox", () => { + const rowId = "test-row-id"; + const handleRowSelectionChanged = vi.fn(); + + afterEach(() => { + handleRowSelectionChanged.mockClear(); + }); + + it("renders the checkbox with the tooltip", () => { + render( + + ); + + // Check if the checkbox is in the document + const checkbox = screen.getByRole("checkbox", { + name: /click to view this opening's map activity/i, + }); + expect(checkbox).toBeInTheDocument(); + }); + + it("sets the checkbox to 'checked' if rowId is in selectedRows", () => { + render( + + ); + + const checkbox = screen.getByRole("checkbox"); + expect(checkbox).toBeChecked(); + }); + + it("calls handleRowSelectionChanged with rowId when checkbox is toggled", () => { + render( + + ); + + const checkbox = screen.getByRole("checkbox"); + + // Simulate a click on the checkbox + fireEvent.click(checkbox); + + // Check if handleRowSelectionChanged was called with the correct rowId + expect(handleRowSelectionChanged).toHaveBeenCalledWith(rowId); + }); +}); diff --git a/frontend/src/__test__/components/TableCellContent.test.tsx b/frontend/src/__test__/components/TableCellContent.test.tsx new file mode 100644 index 00000000..3cebab09 --- /dev/null +++ b/frontend/src/__test__/components/TableCellContent.test.tsx @@ -0,0 +1,98 @@ +// TableCellContent.test.tsx +import React from "react"; +import { vi } from "vitest"; +import { render, screen } from "@testing-library/react"; +import TableCellContent from "../../components/TableCellContent"; +import { OpeningsSearch } from "../../types/OpeningsSearch"; + +// Mock components +vi.mock("../StatusTag", () => ({ + default: ({ code }: { code: string }) =>
{`Status: ${code}`}
, +})); +vi.mock("../ActionButtons", () => ({ + default: ({ rowId }: { rowId: string }) =>
{`Actions for ${rowId}`}
, +})); +vi.mock("../SpatialCheckbox", () => ({ + default: ({ rowId }: { rowId: string }) =>
{`Spatial Checkbox for ${rowId}`}
, +})); + +describe("TableCellContent", () => { + const row = { + openingId: 1, + statusDescription: "Active", + categoryCode: "A", + categoryDescription: "Category A", + } as OpeningsSearch; + const selectedRows: string[] = []; + const handleRowSelectionChanged = vi.fn(); + + it("renders StatusTag when headerKey is 'statusDescription'", () => { + render( + + ); + + expect(screen.getByText(/Active/i)).toBeInTheDocument(); + }); + + it("renders ActionButtons and optionally SpatialCheckbox when headerKey is 'actions'", () => { + const { rerender } = render( + + ); + console.log(screen.debug()); + expect(screen.queryByText(/View/i)).toBeInTheDocument(); + }); + + it("renders category code and description when headerKey is 'Category'", () => { + render( + + ); + expect(screen.getAllByText("A - Category A")[0]).toBeInTheDocument(); + }); + + it("renders default content for other headerKey values", () => { + render( + + ); + + expect(screen.getByText(/Unknown Value/i)).toBeInTheDocument(); + }); + + it("renders SpatialCheckbox when headerKey is 'actions' and showSpatial is true", () => { + render( + + ); + //check if the Checkbox text is present + expect(screen.getByText(/Click to view this opening's map activity./i)).toBeInTheDocument(); + }); + +}); diff --git a/frontend/src/__test__/components/TableRowComponent.test.tsx b/frontend/src/__test__/components/TableRowComponent.test.tsx new file mode 100644 index 00000000..8e0a4ba2 --- /dev/null +++ b/frontend/src/__test__/components/TableRowComponent.test.tsx @@ -0,0 +1,88 @@ +// TableRowComponent.test.tsx +import React from "react"; +import { vi } from "vitest"; +import { render, screen, fireEvent } from "@testing-library/react"; +import TableRowComponent from "../../components/TableRowComponent"; +import { OpeningsSearch } from "../../types/OpeningsSearch"; +import { ITableHeader } from "../../types/TableHeader"; + +describe("TableRowComponent", () => { + const row: OpeningsSearch = { + openingId: 1, + statusDescription: "Active", + categoryCode: "A", + categoryDescription: "Category A", + }; + + const headers: ITableHeader[] = [ + { key: "statusDescription", label: "Status", selected: true }, + { key: "categoryCode", label: "Category Code", selected: true }, + { key: "actions", label: "Actions", selected: false }, + ]; + + const showSpatial = false; + const selectedRows: string[] = []; + const handleRowSelectionChanged = vi.fn(); + const setOpeningDetails = vi.fn(); + + afterEach(() => { + handleRowSelectionChanged.mockClear(); + setOpeningDetails.mockClear(); + }); + + it("renders TableCellContent for each selected header", () => { + render( + + ); + + // Check that the expected content for each selected header is rendered + expect(screen.getByText("Active")).toBeInTheDocument(); + expect(screen.getByText("A")).toBeInTheDocument(); + expect(screen.queryByText("Category A")).not.toBeInTheDocument(); // CategoryDescription isn't selected + }); + + it("calls setOpeningDetails when the row is clicked", () => { + render( + + ); + + // Simulate click on the row + const tableRow = screen.getByRole("row"); + fireEvent.click(tableRow); + + // Verify that setOpeningDetails was called with true + expect(setOpeningDetails).toHaveBeenCalledWith(true); + }); + + it("renders TableCell components only for selected headers", () => { + render( + + ); + + // Verify content specific to selected headers + expect(screen.getByText("Active")).toBeInTheDocument(); + expect(screen.getByText("A")).toBeInTheDocument(); + expect(screen.queryByText("Actions")).not.toBeInTheDocument(); + }); +}); diff --git a/frontend/src/__test__/screens/Opening.test.tsx b/frontend/src/__test__/screens/Opening.test.tsx index c5c623e2..177bed49 100644 --- a/frontend/src/__test__/screens/Opening.test.tsx +++ b/frontend/src/__test__/screens/Opening.test.tsx @@ -7,9 +7,10 @@ import { NotificationProvider } from '../../contexts/NotificationProvider'; import { BrowserRouter } from 'react-router-dom'; import { RecentOpening } from '../../types/RecentOpening'; import { getWmsLayersWhitelistUsers } from '../../services/SecretsService'; -import { fetchFreeGrowingMilestones, fetchOpeningsPerYear, fetchRecentOpenings, fetchRecentActions } from '../../services/OpeningService'; +import { fetchFreeGrowingMilestones, fetchOpeningsPerYear, fetchRecentActions } from '../../services/OpeningService'; import { fetchOpeningFavourites } from '../../services/OpeningFavouriteService'; import { AuthProvider } from '../../contexts/AuthProvider'; +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; const data = { "activityType": "Update", @@ -32,7 +33,6 @@ vi.mock('../../services/OpeningService', async () => { const actual = await vi.importActual('../../services/OpeningService'); return { ...actual, - fetchRecentOpenings: vi.fn(), fetchOpeningsPerYear: vi.fn(), fetchFreeGrowingMilestones: vi.fn(), fetchRecentActions: vi.fn(), @@ -73,13 +73,14 @@ const paginationValueMock = { setInitialItemsPerPage: vi.fn(), }; +const queryClient = new QueryClient(); + describe('Opening screen test cases', () => { beforeEach(() => { vi.clearAllMocks(); - (getWmsLayersWhitelistUsers as vi.Mock).mockResolvedValue([{userName: 'TEST'}]); - (fetchRecentOpenings as vi.Mock).mockResolvedValue(rows); + (getWmsLayersWhitelistUsers as vi.Mock).mockResolvedValue([{userName: 'TEST'}]); (fetchOpeningsPerYear as vi.Mock).mockResolvedValue([ { group: '2022', key: 'Openings', value: 10 }, { group: '2023', key: 'Openings', value: 15 }, @@ -88,14 +89,12 @@ describe('Opening screen test cases', () => { (fetchOpeningFavourites as vi.Mock).mockResolvedValue([1,2,3]); (fetchRecentActions as vi.Mock).mockResolvedValue([data]); - - - }); it('should renders Opening Page Title component', async () => { const { getByTestId } = render( + @@ -103,6 +102,7 @@ describe('Opening screen test cases', () => { + ); @@ -121,13 +121,15 @@ describe('Opening screen test cases', () => { await act(async () => { ({ container } = render( - - - - - - - + + + + + + + + + )); }); @@ -160,13 +162,15 @@ describe('Opening screen test cases', () => { await act(async () => { ({ container, getByText } = render( - - - - - - - + + + + + + + + + )); }); @@ -183,13 +187,15 @@ describe('Opening screen test cases', () => { await act(async () => { ({ container, getByText } = render( - - - - - - - + + + + + + + + + )); }); diff --git a/frontend/src/__test__/services/OpeningService.test.ts b/frontend/src/__test__/services/OpeningService.test.ts index 665fc031..e3d050b2 100644 --- a/frontend/src/__test__/services/OpeningService.test.ts +++ b/frontend/src/__test__/services/OpeningService.test.ts @@ -1,7 +1,6 @@ import { describe, it, expect, vi } from 'vitest'; import axios from 'axios'; import { - fetchRecentOpenings, fetchOpeningsPerYear, fetchFreeGrowingMilestones, fetchRecentActions @@ -21,57 +20,6 @@ describe('OpeningService', () => { (getAuthIdToken as vi.Mock).mockReturnValue(authToken); }); - describe('fetchRecentOpenings', () => { - it('should fetch recent openings successfully', async () => { - const mockData = { - data: [ - { - openingId: 1, - forestFileId: '123', - cuttingPermit: '456', - timberMark: '789', - cutBlock: 'A', - grossAreaHa: 10, - status: { description: 'Active' }, - category: { description: 'Category1' }, - disturbanceStart: '2023-01-01', - entryTimestamp: '2023-01-01T00:00:00Z', - updateTimestamp: '2023-01-02T00:00:00Z' - } - ] - }; - (axios.get as vi.Mock).mockResolvedValue({ status: 200, data: mockData }); - - const result = await fetchRecentOpenings(); - - expect(axios.get).toHaveBeenCalledWith(`${backendUrl}/api/openings/recent-openings?page=0&perPage=100`, { - headers: { Authorization: `Bearer ${authToken}` } - }); - expect(result).toEqual([ - { - id: '1', - openingId: '1', - forestFileId: '123', - cuttingPermit: '456', - timberMark: '789', - cutBlock: 'A', - grossAreaHa: '10', - status: 'Active', - category: 'Category1', - disturbanceStart: '2023-01-01', - entryTimestamp: '2023-01-01', - updateTimestamp: '2023-01-02' - } - ]); - }); - - it('should handle error while fetching recent openings', async () => { - (axios.get as vi.Mock).mockRejectedValue(new Error('Network Error')); - - await expect(fetchRecentOpenings()).rejects.toThrow('Network Error'); - }); - }); - describe('fetchOpeningsPerYear', () => { it('should fetch openings per year successfully', async () => { const mockData = [ diff --git a/frontend/src/__test__/services/queries/dashboard/dashboardQueries.test.tsx b/frontend/src/__test__/services/queries/dashboard/dashboardQueries.test.tsx new file mode 100644 index 00000000..82d05cca --- /dev/null +++ b/frontend/src/__test__/services/queries/dashboard/dashboardQueries.test.tsx @@ -0,0 +1,112 @@ +import { render, screen, waitFor } from "@testing-library/react"; +import axios from "axios"; +import { usePostViewedOpening, postViewedOpening } from "../../../../services/queries/dashboard/dashboardQueries"; +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; +import { vi, describe, it, expect } from "vitest"; +import { getAuthIdToken } from "../../../../services/AuthService"; + +// Mock axios and the AuthService function +vi.mock("axios"); +vi.mock("../../../../services/AuthService", () => ({ + getAuthIdToken: vi.fn(), +})); + +describe("postViewedOpening", () => { + const backendUrl = import.meta.env.VITE_BACKEND_URL; + const openingId = "123"; + + it("should send a PUT request and return data on success", async () => { + const mockResponse = { data: { message: "Success" } }; + (axios.put as vi.Mock).mockResolvedValue(mockResponse); + (getAuthIdToken as vi.Mock).mockReturnValue("testAuthToken"); + + const result = await postViewedOpening(openingId); + + expect(axios.put).toHaveBeenCalledWith(`${backendUrl}/api/openings/recent/${openingId}`, null, { + headers: { Authorization: `Bearer testAuthToken` }, + }); + expect(result).toEqual(mockResponse.data); + }); + + it("should throw a 403 error if the user is unauthorized", async () => { + (axios.put as vi.Mock).mockRejectedValue({ response: { status: 403 } }); + + await expect(postViewedOpening(openingId)).rejects.toThrow( + "Forbidden: You don't have permission to view this opening." + ); + }); + + it("should throw a specific error message for other server errors", async () => { + const errorMessage = "Server error occurred"; + (axios.put as vi.Mock).mockRejectedValue({ + response: { data: { message: errorMessage } }, + }); + + await expect(postViewedOpening(openingId)).rejects.toThrow(errorMessage); + }); +}); + +describe("usePostViewedOpening", () => { + const queryClient = new QueryClient(); + + const wrapper = ({ children }: { children: React.ReactNode }) => ( + {children} + ); + + // Mock Component to test the hook + const MockComponent = ({ openingId }: { openingId: string }) => { + const mutation = usePostViewedOpening(); + + return ( + + ); + }; + + it("should call postViewedOpening when mutate is invoked", async () => { + const mockResponse = { message: "Success" }; + const backendUrl = import.meta.env.VITE_BACKEND_URL; + (axios.put as vi.Mock).mockResolvedValue({ data: mockResponse }); + (getAuthIdToken as vi.Mock).mockReturnValue("testAuthToken"); + + render( + + + + ); + + // Trigger the mutation + screen.getByTestId("mutate-button").click(); + + // Wait for axios call + await waitFor(() => + expect(axios.put).toHaveBeenCalledWith(`${backendUrl}/api/openings/recent/123`, null, { + headers: { Authorization: `Bearer testAuthToken` }, + }) + ); + }); + + it("should handle errors when mutation fails", async () => { + const errorMessage = "Server error occurred"; + (axios.put as vi.Mock).mockRejectedValue({ + response: { data: { message: errorMessage } }, + }); + + render( + + + + ); + + // Trigger the mutation + screen.getByTestId("mutate-button").click(); + + await waitFor(() => { + expect(axios.put).toHaveBeenCalled(); + }); + }); +}); diff --git a/frontend/src/__test__/services/search/openings.test.tsx b/frontend/src/__test__/services/search/openings.test.tsx index 6ddbf33e..5dec0d22 100644 --- a/frontend/src/__test__/services/search/openings.test.tsx +++ b/frontend/src/__test__/services/search/openings.test.tsx @@ -5,6 +5,7 @@ import { fetchOpenings, OpeningFilters } from "../../../services/search/openings import { getAuthIdToken } from "../../../services/AuthService"; import { createDateParams } from "../../../utils/searchUtils"; import { describe, it, beforeEach, afterEach, vi, expect } from "vitest"; +import exp from "constants"; // Mock dependencies vi.mock("axios"); @@ -53,10 +54,14 @@ const mockApiResponse = { orgUnitCode: "DPG", orgUnitName: "Prince George Natural Resource District", entryUserId: "Datafix107808", - statusCode: "APP", - statusDescription: "Approved", - categoryCode: "FTML", - categoryDescription: "Forest Tenure - Major Licensee", + category: { + code: "CONT", + description: "SP as a part of contractual agreement", + }, + status: { + code: "APP", + description: "Approved", + }, }, ], }, @@ -106,4 +111,29 @@ describe("fetchOpenings", () => { await expect(fetchOpenings(sampleFilters)).rejects.toThrow("Network error"); }); + + + it("should return flattened data structure with specific fields", async () => { + // Arrange: setting up the mock response for axios + mockedAxios.get.mockResolvedValue(mockApiResponse); + + // Act: call the fetchOpenings function + const result = await fetchOpenings(sampleFilters); + + // Assert: check that the response data is correctly flattened + const firstOpening = result.data[0]; + expect(firstOpening.openingId).toEqual(9100129); + expect(firstOpening.categoryCode).toEqual("CONT"); + expect(firstOpening.categoryDescription).toEqual("SP as a part of contractual agreement"); + expect(firstOpening.statusCode).toEqual("APP"); + expect(firstOpening.statusDescription).toEqual("Approved"); + expect(firstOpening.timberMark).toEqual("W1729S"); + expect(firstOpening.cutBlockId).toEqual("06-03"); + expect(firstOpening.entryUserId).toEqual("Datafix107808"); + + + // Confirm that original nested properties were removed + expect(firstOpening.status).toBeUndefined(); + expect(firstOpening.category).toBeUndefined(); + }); }); diff --git a/frontend/src/components/ActionButtons/index.tsx b/frontend/src/components/ActionButtons/index.tsx new file mode 100644 index 00000000..877cfcc5 --- /dev/null +++ b/frontend/src/components/ActionButtons/index.tsx @@ -0,0 +1,32 @@ +// ActionButtons.tsx + +import React from "react"; +import { Button } from "@carbon/react"; +import * as Icons from "@carbon/icons-react"; + +interface ActionButtonsProps { + rowId: string; +} + +const ActionButtons: React.FC = ({ rowId }) => ( + <> +