From d826e554736d024b342be26dee90d7b4265cbdbf Mon Sep 17 00:00:00 2001 From: hylee Date: Wed, 22 May 2024 18:35:34 -0700 Subject: [PATCH 1/4] added school edit pages, utils, tests, stories --- frontend/src/main/pages/SchoolEditPage.js | 70 +++++++ frontend/src/main/utils/schoolsUtils.js | 17 ++ .../stories/pages/SchoolEditPage.stories.js | 37 ++++ .../tests/pages/School/SchoolEditPage.test.js | 180 ++++++++++++++++++ .../{ => School}/SchoolIndexPage.test.js | 0 frontend/src/tests/utils/schoolUtils.test.js | 58 ++++++ 6 files changed, 362 insertions(+) create mode 100644 frontend/src/main/pages/SchoolEditPage.js create mode 100644 frontend/src/main/utils/schoolsUtils.js create mode 100644 frontend/src/stories/pages/SchoolEditPage.stories.js create mode 100644 frontend/src/tests/pages/School/SchoolEditPage.test.js rename frontend/src/tests/pages/{ => School}/SchoolIndexPage.test.js (100%) create mode 100644 frontend/src/tests/utils/schoolUtils.test.js diff --git a/frontend/src/main/pages/SchoolEditPage.js b/frontend/src/main/pages/SchoolEditPage.js new file mode 100644 index 000000000..56fef3ab8 --- /dev/null +++ b/frontend/src/main/pages/SchoolEditPage.js @@ -0,0 +1,70 @@ +import BasicLayout from "main/layouts/BasicLayout/BasicLayout"; +import { useParams } from "react-router-dom"; +import SchoolForm from "main/components/School/SchoolForm"; +import { Navigate } from 'react-router-dom' +import { useBackend, useBackendMutation } from "main/utils/useBackend"; +import { toast } from "react-toastify"; + +export default function SchoolEditPage({storybook=false}) { + let { abbrev } = useParams(); + + const { data: school, _error, _status } = + useBackend( + // Stryker disable next-line all : don't test internal caching of React Query + [`/api/schools?abbrev=${abbrev}`], + { // Stryker disable next-line all : GET is the default, so changing this to "" doesn't introduce a bug + method: "GET", + url: `/api/schools/get`, + params: { + abbrev + } + } + ); + + + const objectToAxiosPutParams = (school) => ({ + url: "/api/schools/update", + method: "PUT", + params: { + abbrev: school.abbrev, + }, + data: { + name: school.name, + termRegex: school.termRegex, + termDescription: school.termDescription, + termError: school.termError + }, + }); + + const onSuccess = (school) => { + toast(`School Updated - abbrev: ${school.abbrev} name: ${school.name}`); + } + + const mutation = useBackendMutation( + objectToAxiosPutParams, + { onSuccess }, + // Stryker disable next-line all : hard to set up test for caching + [`/api/schools/update?abbrev=${abbrev}`] + ); + + const { isSuccess } = mutation + + const onSubmit = async (data) => { + mutation.mutate(data); + } + + if (isSuccess && !storybook) { + return + } + + return ( + +
+

Edit School

+ { + school && + } +
+
+ ) +} diff --git a/frontend/src/main/utils/schoolsUtils.js b/frontend/src/main/utils/schoolsUtils.js new file mode 100644 index 000000000..9653c22d2 --- /dev/null +++ b/frontend/src/main/utils/schoolsUtils.js @@ -0,0 +1,17 @@ +import { toast } from "react-toastify"; + +export function onDeleteSuccess(message) { + console.log(message); + toast(message); +} + +export function cellToAxiosParamsDelete(cell) { + return { + url: "/api/schools", + method: "DELETE", + params: { + abbrev: cell.row.values.abbrev + } + } +} + diff --git a/frontend/src/stories/pages/SchoolEditPage.stories.js b/frontend/src/stories/pages/SchoolEditPage.stories.js new file mode 100644 index 000000000..3e92de111 --- /dev/null +++ b/frontend/src/stories/pages/SchoolEditPage.stories.js @@ -0,0 +1,37 @@ +import React from 'react'; +import { apiCurrentUserFixtures } from "fixtures/currentUserFixtures"; +import { systemInfoFixtures } from "fixtures/systemInfoFixtures"; +import { schoolsFixtures } from 'fixtures/schoolsFixtures'; +import { rest } from "msw"; + +import SchoolEditPage from 'main/pages/SchoolEditPage'; + +export default { + title: 'pages/SchoolEditPage', + component: SchoolEditPage +}; + +const Template = () => ; + +export const Default = Template.bind({}); +Default.parameters = { + msw: [ + rest.get('/api/currentUser', (_req, res, ctx) => { + return res( ctx.json(apiCurrentUserFixtures.adminUser)); + }), + rest.get('/api/systemInfo', (_req, res, ctx) => { + return res(ctx.json(systemInfoFixtures.showingNeither)); + }), + rest.get('/api/schools', (_req, res, ctx) => { + return res(ctx.json(schoolsFixtures.threeSchools[0])); + }), + rest.put('/api/schools', async (req, res, ctx) => { + var reqBody = await req.text(); + window.alert("PUT: " + req.url + " and body: " + reqBody); + return res(ctx.status(200),ctx.json({})); + }), + ], +} + + + diff --git a/frontend/src/tests/pages/School/SchoolEditPage.test.js b/frontend/src/tests/pages/School/SchoolEditPage.test.js new file mode 100644 index 000000000..7bc5d9151 --- /dev/null +++ b/frontend/src/tests/pages/School/SchoolEditPage.test.js @@ -0,0 +1,180 @@ +import { fireEvent, render, waitFor, screen } from "@testing-library/react"; +import { QueryClient, QueryClientProvider } from "react-query"; +import { MemoryRouter } from "react-router-dom"; +import SchoolEditPage from "main/pages/SchoolEditPage"; + +import { apiCurrentUserFixtures } from "fixtures/currentUserFixtures"; +import { systemInfoFixtures } from "fixtures/systemInfoFixtures"; +import axios from "axios"; +import AxiosMockAdapter from "axios-mock-adapter"; +import mockConsole from "jest-mock-console"; + +const mockToast = jest.fn(); +jest.mock('react-toastify', () => { + const originalModule = jest.requireActual('react-toastify'); + return { + __esModule: true, + ...originalModule, + toast: (x) => mockToast(x) + }; +}); + +const mockNavigate = jest.fn(); +jest.mock('react-router-dom', () => { + const originalModule = jest.requireActual('react-router-dom'); + return { + __esModule: true, + ...originalModule, + useParams: () => ({ + abbrev: "ucsb" + }), + Navigate: (x) => { mockNavigate(x); return null; } + }; +}); + + +describe("SchoolEditPage tests", () => { + + describe("when the backend doesn't return data", () => { + + const axiosMock = new AxiosMockAdapter(axios); + + beforeEach(() => { + axiosMock.reset(); + axiosMock.resetHistory(); + axiosMock.onGet("/api/currentUser").reply(200, apiCurrentUserFixtures.userOnly); + axiosMock.onGet("/api/systemInfo").reply(200, systemInfoFixtures.showingNeither); + axiosMock.onGet("/api/schools/get", { params: { abbrev: "ucsb" } }).timeout(); + }); + + const queryClient = new QueryClient(); + test("renders header but table is not present", async () => { + + const restoreConsole = mockConsole(); + + render( + + + + + + ); + await screen.findByText("Edit School"); + expect(screen.queryByTestId("SchoolsForm-name")).not.toBeInTheDocument(); + restoreConsole(); + }); + }); + + describe("tests where backend is working normally", () => { + + const axiosMock = new AxiosMockAdapter(axios); + + beforeEach(() => { + axiosMock.reset(); + axiosMock.resetHistory(); + axiosMock.onGet("/api/currentUser").reply(200, apiCurrentUserFixtures.userOnly); + axiosMock.onGet("/api/systemInfo").reply(200, systemInfoFixtures.showingNeither); + axiosMock.onGet("/api/schools/get", { params: { abbrev: "ucsb" } }).reply(200, { + abbrev: "ucsb", + name: "University of California, Santa Barbara", + termRegex: "regexTest", + termDescription: "descriptionTest", + termError: "errorTest", + }); + axiosMock.onPut('/api/schools/update').reply(200, { + abbrev: "ucsb", + name: "University of California, Sha Bi", + termRegex: "regexTest2", + termDescription: "descText2", + termError: "errTest2", + }); + }); + + const queryClient = new QueryClient(); + test("renders without crashing", () => { + render( + + + + + + ); + }); + + test("Is populated with the data provided", async () => { + + render( + + + + + + ); + + await screen.findByTestId("SchoolForm-abbrev"); + + //const idField = screen.getByTestId("SchoolForm-id"); + const nameField = screen.getByTestId("SchoolForm-name"); + const abbrevField = screen.getByTestId("SchoolForm-abbrev"); + const termRegexField = screen.getByTestId("SchoolForm-termRegex"); + const termDescriptionField = screen.getByTestId("SchoolForm-termDescription"); + const termErrorField = screen.getByTestId("SchoolForm-termError"); + const submitButton = screen.getByTestId("SchoolForm-submit"); + + //expect(idField).toHaveValue("17"); + expect(nameField).toHaveValue("University of California, Santa Barbara"); + expect(abbrevField).toHaveValue("ucsb"); + expect(termRegexField).toHaveValue("regexTest"); + expect(termDescriptionField).toHaveValue("descriptionTest"); + expect(termErrorField).toHaveValue("errorTest"); + expect(submitButton).toBeInTheDocument(); + }); + + test("Changes when you click Update", async () => { + + render( + + + + + + ); + + await screen.findByTestId("SchoolForm-abbrev"); + + //const idField = screen.getByTestId("SchoolForm-id"); + const nameField = screen.getByTestId("SchoolForm-name"); + const abbrevField = screen.getByTestId("SchoolForm-abbrev"); + const termRegexField = screen.getByTestId("SchoolForm-termRegex"); + const termDescriptionField = screen.getByTestId("SchoolForm-termDescription"); + const termErrorField = screen.getByTestId("SchoolForm-termError"); + const submitButton = screen.getByTestId("SchoolForm-submit"); + + //expect(idField).toHaveValue("17"); + expect(nameField).toHaveValue("University of California, Santa Barbara"); + expect(abbrevField).toHaveValue("ucsb"); + expect(termRegexField).toHaveValue("regexTest"); + expect(termDescriptionField).toHaveValue("descriptionTest"); + expect(termErrorField).toHaveValue("errorTest"); + expect(submitButton).toBeInTheDocument(); + + fireEvent.change(nameField, { target: { value: "University of California, Sha Bi" } }) + fireEvent.change(termRegexField, { target: { value: "regexTest2" } }) + fireEvent.change(termDescriptionField, { target: { value: "descTest2" } }) + fireEvent.change(termErrorField, { target: { value: "errTest2" } }) + + fireEvent.click(submitButton); + + await waitFor(() => expect(mockToast).toBeCalled()); + expect(mockToast).toBeCalledWith("School Updated - abbrev: ucsb name: University of California, Sha Bi"); + expect(mockNavigate).toBeCalledWith({ "to": "/schools" }); + + expect(axiosMock.history.put.length).toBe(1); // times called + expect(axiosMock.history.put[0].data).toEqual(JSON.stringify({ name: "University of California, Sha Bi", termRegex: "regexTest2", termDescription: "descTest2", termError: "errTest2"})); + + }); + + + }); +}); + diff --git a/frontend/src/tests/pages/SchoolIndexPage.test.js b/frontend/src/tests/pages/School/SchoolIndexPage.test.js similarity index 100% rename from frontend/src/tests/pages/SchoolIndexPage.test.js rename to frontend/src/tests/pages/School/SchoolIndexPage.test.js diff --git a/frontend/src/tests/utils/schoolUtils.test.js b/frontend/src/tests/utils/schoolUtils.test.js new file mode 100644 index 000000000..84b59f7cc --- /dev/null +++ b/frontend/src/tests/utils/schoolUtils.test.js @@ -0,0 +1,58 @@ +import { onDeleteSuccess, cellToAxiosParamsDelete } from "main/utils/schoolsUtils"; +import mockConsole from "jest-mock-console"; + +const mockToast = jest.fn(); +jest.mock('react-toastify', () => { + const originalModule = jest.requireActual('react-toastify'); + return { + __esModule: true, + ...originalModule, + toast: (x) => mockToast(x) + }; +}); + +describe("SchoolUtils", () => { + + describe("onDeleteSuccess", () => { + + test("It puts the message on console.log and in a toast", () => { + // arrange + const restoreConsole = mockConsole(); + + // act + onDeleteSuccess("abc"); + + // assert + expect(mockToast).toHaveBeenCalledWith("abc"); + expect(console.log).toHaveBeenCalled(); + const message = console.log.mock.calls[0][0]; + expect(message).toMatch("abc"); + + restoreConsole(); + }); + + }); + describe("cellToAxiosParamsDelete", () => { + + test("It returns the correct params", () => { + // arrange + const cell = { row: { values: { abbrev: "ucsb" } } }; + + // act + const result = cellToAxiosParamsDelete(cell); + + // assert + expect(result).toEqual({ + url: "/api/schools", + method: "DELETE", + params: { abbrev: "ucsb" } + }); + }); + + }); +}); + + + + + From 26a96b21bdf738a15ee4c2ed40299556d40bebfe Mon Sep 17 00:00:00 2001 From: hylee Date: Thu, 23 May 2024 17:16:13 -0700 Subject: [PATCH 2/4] added check for abbrev as param to schooleditpage test --- frontend/src/tests/pages/School/SchoolEditPage.test.js | 1 + 1 file changed, 1 insertion(+) diff --git a/frontend/src/tests/pages/School/SchoolEditPage.test.js b/frontend/src/tests/pages/School/SchoolEditPage.test.js index 7bc5d9151..544cf1ed8 100644 --- a/frontend/src/tests/pages/School/SchoolEditPage.test.js +++ b/frontend/src/tests/pages/School/SchoolEditPage.test.js @@ -170,6 +170,7 @@ describe("SchoolEditPage tests", () => { expect(mockNavigate).toBeCalledWith({ "to": "/schools" }); expect(axiosMock.history.put.length).toBe(1); // times called + expect(axiosMock.history.put[0].params).toEqual({ abbrev: "ucsb"}); expect(axiosMock.history.put[0].data).toEqual(JSON.stringify({ name: "University of California, Sha Bi", termRegex: "regexTest2", termDescription: "descTest2", termError: "errTest2"})); }); From 62a4357014f6a8a06653ebc6371b0dcffdcf26b5 Mon Sep 17 00:00:00 2001 From: hylee Date: Thu, 23 May 2024 18:29:47 -0700 Subject: [PATCH 3/4] linked edit button frontend and backend properly --- frontend/src/App.js | 2 ++ frontend/src/main/components/School/SchoolTable.js | 2 +- frontend/src/main/pages/SchoolEditPage.js | 4 ++-- frontend/src/main/pages/SchoolIndexPage.js | 2 +- frontend/src/tests/components/School/SchoolTable.test.js | 2 +- frontend/src/tests/pages/School/SchoolEditPage.test.js | 6 +++--- frontend/src/tests/pages/School/SchoolIndexPage.test.js | 2 +- 7 files changed, 11 insertions(+), 9 deletions(-) diff --git a/frontend/src/App.js b/frontend/src/App.js index 1cfcd516e..0015db3a4 100644 --- a/frontend/src/App.js +++ b/frontend/src/App.js @@ -12,6 +12,7 @@ import CoursesEditPage from "main/pages/CoursesEditPage"; import AdminUsersPage from "main/pages/AdminUsersPage"; import AdminJobsPage from "main/pages/AdminJobsPage"; import SchoolIndexPage from "main/pages/SchoolIndexPage"; +import SchoolEditPage from "main/pages/SchoolEditPage"; import CoursesCreatePage from "main/pages/CoursesCreatePage"; import CourseIndexPage from "main/pages/CourseIndexPage"; @@ -25,6 +26,7 @@ function App() { const adminRoutes = hasRole(currentUser, "ROLE_ADMIN") ? ( <> } /> + } /> } /> } /> diff --git a/frontend/src/main/components/School/SchoolTable.js b/frontend/src/main/components/School/SchoolTable.js index eb9c77909..b000aad31 100644 --- a/frontend/src/main/components/School/SchoolTable.js +++ b/frontend/src/main/components/School/SchoolTable.js @@ -13,7 +13,7 @@ import React from "react"; const navigate = useNavigate(); const editCallback = (cell) => { - navigate(`/schools/edit/${cell.row.values.abbrev}`); + navigate(`/admin/schools/edit/${cell.row.values.abbrev}`); }; // Stryker disable all : hard to test for query caching diff --git a/frontend/src/main/pages/SchoolEditPage.js b/frontend/src/main/pages/SchoolEditPage.js index 56fef3ab8..4282a8f04 100644 --- a/frontend/src/main/pages/SchoolEditPage.js +++ b/frontend/src/main/pages/SchoolEditPage.js @@ -14,7 +14,7 @@ export default function SchoolEditPage({storybook=false}) { [`/api/schools?abbrev=${abbrev}`], { // Stryker disable next-line all : GET is the default, so changing this to "" doesn't introduce a bug method: "GET", - url: `/api/schools/get`, + url: `/api/schools`, params: { abbrev } @@ -54,7 +54,7 @@ export default function SchoolEditPage({storybook=false}) { } if (isSuccess && !storybook) { - return + return } return ( diff --git a/frontend/src/main/pages/SchoolIndexPage.js b/frontend/src/main/pages/SchoolIndexPage.js index f292e729e..12d4291eb 100644 --- a/frontend/src/main/pages/SchoolIndexPage.js +++ b/frontend/src/main/pages/SchoolIndexPage.js @@ -15,7 +15,7 @@ export default function SchoolIndexPage() { return (