diff --git a/package.json b/package.json index cddd7e9f..62e7f17b 100644 --- a/package.json +++ b/package.json @@ -31,11 +31,6 @@ "@google-cloud/logging": "^9.6.0", "@google-cloud/profiler": "^4.1.3", "@google-cloud/storage": "^5.8.5", - "@types/express": "^4.17.11", - "@types/jest": "^27.0.0", - "@types/node": "^15.12.2", - "@types/react": "^18.2.51", - "@types/react-dom": "^18.2.18", "axios": "^1.7.4", "blaise-api-node-client": "git+https://github.com/ONSdigital/blaise-api-node-client#1.1.0", "blaise-design-system-react-components": "git+https://github.com/ONSdigital/blaise-design-system-react-components#0.14.0", @@ -74,9 +69,14 @@ "@testing-library/react": "^14.2.1", "@testing-library/user-event": "^13.2.1", "@types/ejs": "^3.1.0", + "@types/express": "^4.17.11", + "@types/jest": "^27.0.0", "@types/jsonwebtoken": "^8.5.5", "@types/lodash": "^4.14.170", + "@types/node": "^15.12.2", "@types/pino-http": "^5.7.0", + "@types/react": "^18.2.51", + "@types/react-dom": "^18.2.18", "@types/react-router-dom": "^5.3.3", "@types/react-timeago": "^4.1.7", "@types/supertest": "^2.0.11", diff --git a/src/components/questionnaireList.test.tsx b/src/components/questionnaireList.test.tsx index 3d8304a3..1ce64f06 100644 --- a/src/components/questionnaireList.test.tsx +++ b/src/components/questionnaireList.test.tsx @@ -1,51 +1,243 @@ /** * @jest-environment jsdom */ - +import React from "react"; +import "@testing-library/jest-dom"; import flushPromises from "../tests/utils"; -import { render, waitFor, screen } from "@testing-library/react"; +import { render, waitFor, screen, act } from "@testing-library/react"; import { MemoryRouter } from "react-router-dom"; -import { act } from "react-dom/test-utils"; -import React from "react"; import { Authenticate } from "blaise-login-react/blaise-login-react-client"; import axios from "axios"; import MockAdapter from "axios-mock-adapter"; import QuestionnaireList from "./questionnaireList"; +import userEvent from "@testing-library/user-event"; const mock = new MockAdapter(axios); -// mock login +// Mock the blaise-login-react Authenticate component jest.mock("blaise-login-react/blaise-login-react-client"); const { MockAuthenticate } = jest.requireActual("blaise-login-react/blaise-login-react-client"); Authenticate.prototype.render = MockAuthenticate.prototype.render; MockAuthenticate.OverrideReturnValues(null, true); -describe("Questionnaire Details page ", () => { +const MOCK_QUESTIONNAIRE_LIST = [ + { + "name": "IPS2409A", + "id": "ef8980d9-5f5c-416d-9b4b-9570c15c85c0", + "serverParkName": "gusty", + "installDate": "2024-10-16T13:14:01.7557563+01:00", + "status": "Active", + "dataRecordCount": 1, + "hasData": true, + "blaiseVersion": "5.14.4.3668", + "nodes": [ + { + "nodeName": "blaise-gusty-mgmt", + "nodeStatus": "Active" + } + ], + "fieldPeriod": "September 2024" + }, + { + "name": "IPS2409B", + "id": "ef8980d9-5f5c-416d-9b4b-9570c15c85c0", + "serverParkName": "gusty", + "installDate": "2024-10-16T13:14:01.7557563+01:00", + "status": "Active", + "dataRecordCount": 1, + "hasData": true, + "blaiseVersion": "5.14.4.3668", + "nodes": [ + { + "nodeName": "blaise-gusty-mgmt", + "nodeStatus": "Active" + } + ], + "fieldPeriod": "September 2024" + }, + { + "name": "IPS_ContactInfo", + "id": "10effca3-caec-4fc0-a037-20a43cf050c2", + "serverParkName": "gusty", + "installDate": "2024-10-18T12:59:23.4164608+01:00", + "status": "Active", + "dataRecordCount": 0, + "hasData": false, + "blaiseVersion": "5.14.4.3668", + "nodes": [ + { + "nodeName": "blaise-gusty-mgmt", + "nodeStatus": "Active" + } + ], + "fieldPeriod": "Field period unknown" + }, + { + "name": "IPS_Attempts", + "id": "10effca3-caec-4fc0-a037-20a43cf050c4", + "serverParkName": "gusty", + "installDate": "2024-10-18T12:59:23.4164608+01:00", + "status": "Active", + "dataRecordCount": 0, + "hasData": false, + "blaiseVersion": "5.14.4.3668", + "nodes": [ + { + "nodeName": "blaise-gusty-mgmt", + "nodeStatus": "Active" + } + ], + "fieldPeriod": "Field period unknown" + }, + { + "name": "DST2304Z", + "id": "10effca3-caec-4fc0-a037-20a43cf050c5", + "serverParkName": "gusty", + "installDate": "2024-10-18T12:59:23.4164608+01:00", + "status": "Active", + "dataRecordCount": 0, + "hasData": false, + "blaiseVersion": "5.14.4.3668", + "nodes": [ + { + "nodeName": "blaise-gusty-mgmt", + "nodeStatus": "Active" + } + ], + "fieldPeriod": "Field period unknown" + } +]; + +describe("Questionnaire List displays valid user questionnaires", () => { beforeEach(() => { + mock.onGet("/api/questionnaires").reply(200, MOCK_QUESTIONNAIRE_LIST); + }); + + afterEach(() => { + mock.reset(); + }); + + it("should not display any questionnaires if no questionnaires were fetched back from the server", async () => { + // Arrange mock.onGet("/api/questionnaires").reply(200, []); + render( + + + + ); + + // Act + await act(async () => { + await flushPromises(); + }); + + // Assert + await waitFor(() => { + expect(screen.getByText(/Filter by questionnaire name/i)).toBeVisible(); + expect(screen.getByText(/No installed questionnaires found/i)).toBeVisible(); + }); + }); + + it("should display a list of questionnaires containing only questionnaires that match the filter", async () => { + // Arrange + render( + + + + ); + + // Act + await act(async () => { + await flushPromises(); + }); + const filterInput = screen.getByTestId(/filter-by-name/i); + userEvent.type(filterInput, "IPS2409A"); + + // Assert + await waitFor(() => { + expect(screen.getByText(/Filter by questionnaire name/i)).toBeVisible(); + expect(screen.getByText(/1 results of 1/i)).toBeVisible(); + expect(screen.getByText(/IPS2409A/i)).toBeVisible(); + }); + }); + +}); + +describe("Questionnaire List displays hidden questionnaires that match when using the search filter", () => { + beforeEach(() => { + mock.onGet("/api/questionnaires").reply(200, MOCK_QUESTIONNAIRE_LIST); }); afterEach(() => { mock.reset(); }); - it("should redirect to the homepage when no questionnaire has been provided ", async () => { + it("should display the hidden ContactInfo questionnaire", async () => { + // Arrange + render( + + + + ); + + // Act + await act(async () => { + await flushPromises(); + }); + const filterInput = screen.getByTestId(/filter-by-name/i); + userEvent.type(filterInput, "ContactInfo"); - // Go direct to the questionnaire details page not from a link + // Assert + await waitFor(() => { + expect(screen.getByText(/Filter by questionnaire name/i)).toBeVisible(); + expect(screen.getByText(/1 results of 1/i)).toBeVisible(); + expect(screen.getByText(/IPS_ContactInfo/i)).toBeVisible(); + }); + }); + + it("should display the hidden Attempts questionnaire", async () => { + // Arrange + render( + + + + ); + + // Act + await act(async () => { + await flushPromises(); + }); + const filterInput = screen.getByTestId(/filter-by-name/i); + userEvent.type(filterInput, "Attempts"); + + // Assert + await waitFor(() => { + expect(screen.getByText(/Filter by questionnaire name/i)).toBeVisible(); + expect(screen.getByText(/1 results of 1/i)).toBeVisible(); + expect(screen.getByText(/IPS_Attempts/i)).toBeVisible(); + }); + }); + + it("should display the hidden DST2304Z test questionnaire", async () => { + // Arrange render( ); + // Act await act(async () => { await flushPromises(); }); + const filterInput = screen.getByTestId(/filter-by-name/i); + userEvent.type(filterInput, "DST2304Z"); + // Assert await waitFor(() => { - expect(screen.queryByText("Questionnaire settings")).toEqual(null); - expect(screen.getByText(/Filter by questionnaire name/i)).toBeDefined(); - expect(screen.queryByText(/Questionnaire details/i)).toEqual(null); + expect(screen.getByText(/Filter by questionnaire name/i)).toBeVisible(); + expect(screen.getByText(/1 results of 1/i)).toBeVisible(); + expect(screen.getByText(/DST2304Z/i)).toBeVisible(); }); }); }); diff --git a/src/components/questionnaireList.tsx b/src/components/questionnaireList.tsx index 8c26bc7a..7b733662 100644 --- a/src/components/questionnaireList.tsx +++ b/src/components/questionnaireList.tsx @@ -15,18 +15,25 @@ type Props = { setErrored: (errored: boolean) => void } -function questionnaireTableRow(questionnaire: Questionnaire): ReactElement { - function questionnaireName(questionnaire: Questionnaire) { - if (questionnaire.name.toUpperCase().startsWith("DST")) { - return ( - <> - <>{questionnaire.name} - - ); - } - return <>{questionnaire.name}; +function isHiddenQuestionnaire(questionnaireName: string): boolean { + const QUESTIONNAIRE_KEYWORDS = ["DST", "CONTACTINFO", "ATTEMPTS"]; + return QUESTIONNAIRE_KEYWORDS.some(keyword => { + return questionnaireName.toUpperCase().includes(keyword); + }); +} + +function questionnaireName(questionnaire: Questionnaire) { + if (isHiddenQuestionnaire(questionnaire.name)) { + return ( + <> + <>{questionnaire.name} + + ); } + return <>{questionnaire.name}; +} +function questionnaireTableRow(questionnaire: Questionnaire): ReactElement { return ( @@ -56,43 +63,62 @@ function questionnaireTableRow(questionnaire: Questionnaire): ReactElement { ); } +function questionnaireTable(filteredList: Questionnaire[], tableColumns: TableColumns[], message: string): ReactElement { + if (filteredList && filteredList.length > 0) { + return + { + filteredList.map((item: Questionnaire) => { + return questionnaireTableRow(item); + }) + } + ; + } + return {message}; +} + export const QuestionnaireList = ({ setErrored }: Props): ReactElement => { const [questionnaires, setQuestionnaires] = useState([]); const [realQuestionnaireCount, setRealQuestionnaireCount] = useState(0); const [loaded, setLoaded] = useState(false); const [message, setMessage] = useState(""); + const [filterText, setFilterText] = useState(""); const [filteredList, setFilteredList] = useState([]); - function filterTestQuestionnaires(questionnairesToFilter: Questionnaire[], filterValue: string): Questionnaire[] { - if (!filterValue.toUpperCase().startsWith("DST")) { - questionnairesToFilter = filter(questionnairesToFilter, (questionnaire) => { - if (!questionnaire?.name) { - return false; - } - return !questionnaire.name.toUpperCase().startsWith("DST"); - }); - setRealQuestionnaireCount(questionnairesToFilter.length); - } - return questionnairesToFilter; - } + const handleFilterChange = (value: string) => { + setFilterText(value); + filterQuestionnaireList(questionnaires, value); + }; - function filterList(filterValue: string) { - // Filter by the search field - if (filterValue === "") { - setFilteredList(filterTestQuestionnaires(questionnaires, filterValue)); + const filterQuestionnaireList = (questionnaireList: Questionnaire[], filterValue: string) =>{ + if (questionnaireList.length === 0) { + setMessage("No installed questionnaires found."); + return []; } - const newFilteredList = filter(filterTestQuestionnaires(questionnaires, filterValue), (questionnaire) => questionnaire.name.includes(filterValue.toUpperCase())); + + const newFilteredList = filter(questionnaireList, (questionnaire) => { + if (filterValue === "") { + return questionnaire.name.toUpperCase().includes(filterValue.toUpperCase()) && !isHiddenQuestionnaire(questionnaire.name); + } + return questionnaire.name.toUpperCase().includes(filterValue.toUpperCase()); + }); + // Order by date newFilteredList.sort((a: Questionnaire, b: Questionnaire) => Date.parse(b.installDate) - Date.parse(a.installDate)); setFilteredList(newFilteredList); - - if (questionnaires.length > 0 && newFilteredList.length === 0) { + setRealQuestionnaireCount(newFilteredList.length); + + if (questionnaireList.length > 0 && newFilteredList.length === 0) { setMessage(`No questionnaires containing ${filterValue} found`); + return []; } - } + return newFilteredList; + }; - async function getQuestionnairesList() { + const getQuestionnairesList = async () => { let questionnaires: Questionnaire[]; try { questionnaires = await getQuestionnaires(); @@ -109,75 +135,59 @@ export const QuestionnaireList = ({ setErrored }: Props): ReactElement => { } return questionnaires; - } + }; useEffect(() => { getQuestionnairesList().then((questionnaireList: Questionnaire[]) => { setQuestionnaires(questionnaireList); - const nonTestQuestionnaires = filterTestQuestionnaires(questionnaireList, ""); - setFilteredList(nonTestQuestionnaires); - if (nonTestQuestionnaires.length === 0) { - setMessage("No installed questionnaires found."); - } + const filteredQuestionnaireList = filterQuestionnaireList(questionnaireList, ""); + setFilteredList(filteredQuestionnaireList); setLoaded(true); }); }, []); - function QuestionnaireTable(): ReactElement { - if (filteredList && filteredList.length > 0) { - return - { - filteredList.map((item: Questionnaire) => { - return questionnaireTableRow(item); - }) - } - ; - } - return {message}; - } - - const tableColumns: TableColumns[] = - [ - { - title: "Questionnaire" - }, - { - title: "Field period" - }, - { - title: "Status" - }, - { - title: "Install date" - }, - { - title: "Cases" - } - ]; - if (!loaded) { return ; } + return ( <>
-
-
{ questionnaires &&

{filteredList.length} results of {realQuestionnaireCount}

} - - + { + questionnaireTable(filteredList, [ + { + title: "Questionnaire" + }, + { + title: "Field period" + }, + { + title: "Status" + }, + { + title: "Install date" + }, + { + title: "Cases" + } + ], message) + }
diff --git a/yarn.lock b/yarn.lock index 0333d873..0a463904 100644 --- a/yarn.lock +++ b/yarn.lock @@ -13174,16 +13174,7 @@ string-natural-compare@^3.0.1: resolved "https://registry.yarnpkg.com/string-natural-compare/-/string-natural-compare-3.0.1.tgz#7a42d58474454963759e8e8b7ae63d71c1e7fdf4" integrity sha512-n3sPwynL1nwKi3WJ6AIsClwBMa0zTi54fn2oLU6ndfTSIO05xaznjSf15PcBZU6FNWbmN5Q6cxT4V5hGvB4taw== -"string-width-cjs@npm:string-width@^4.2.0": - version "4.2.3" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" - integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== - dependencies: - emoji-regex "^8.0.0" - is-fullwidth-code-point "^3.0.0" - strip-ansi "^6.0.1" - -"string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: +"string-width-cjs@npm:string-width@^4.2.0", "string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -13266,14 +13257,7 @@ stringify-object@^3.3.0: is-obj "^1.0.1" is-regexp "^1.0.0" -"strip-ansi-cjs@npm:strip-ansi@^6.0.1": - version "6.0.1" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" - integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== - dependencies: - ansi-regex "^5.0.1" - -strip-ansi@^6.0.0, strip-ansi@^6.0.1: +"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== @@ -14634,7 +14618,7 @@ workbox-window@6.6.1: "@types/trusted-types" "^2.0.2" workbox-core "6.6.1" -"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== @@ -14652,15 +14636,6 @@ wrap-ansi@^6.2.0: string-width "^4.1.0" strip-ansi "^6.0.0" -wrap-ansi@^7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" - integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== - dependencies: - ansi-styles "^4.0.0" - string-width "^4.1.0" - strip-ansi "^6.0.0" - wrap-ansi@^8.1.0: version "8.1.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214"