diff --git a/.yarn/patches/vitest-npm-3.2.3-d0d609a9f8.patch b/.yarn/patches/vitest-npm-3.2.3-d0d609a9f8.patch new file mode 100644 index 000000000..2872a2a0b --- /dev/null +++ b/.yarn/patches/vitest-npm-3.2.3-d0d609a9f8.patch @@ -0,0 +1,54 @@ +diff --git a/dist/chunks/index.CmSc2RE5.js b/dist/chunks/index.CmSc2RE5.js +index 90004b3064c731a22f584000525e0bff3f423867..7fa0351ff36a5d21640520685420046298b71959 100644 +--- a/dist/chunks/index.CmSc2RE5.js ++++ b/dist/chunks/index.CmSc2RE5.js +@@ -3,8 +3,6 @@ import { Console } from 'node:console'; + // SEE https://github.com/jsdom/jsdom/blob/master/lib/jsdom/living/interfaces.js + const LIVING_KEYS = [ + "DOMException", +- "URL", +- "URLSearchParams", + "EventTarget", + "NamedNodeMap", + "Node", +@@ -161,9 +159,6 @@ const LIVING_KEYS = [ + "ShadowRoot", + "MutationObserver", + "MutationRecord", +- "Headers", +- "AbortController", +- "AbortSignal", + "Uint8Array", + "Uint16Array", + "Uint32Array", +@@ -441,9 +436,6 @@ var jsdom = { + // https://nodejs.org/dist/latest/docs/api/globals.html + const globalNames = [ + "structuredClone", +- "fetch", +- "Request", +- "Response", + "BroadcastChannel", + "MessageChannel", + "MessagePort", +@@ -454,6 +446,20 @@ var jsdom = { + const value = globalThis[name]; + if (typeof value !== "undefined" && typeof dom.window[name] === "undefined") dom.window[name] = value; + } ++ const overrideGlobals = [ ++ "fetch", ++ "Request", ++ "Response", ++ "Headers", ++ "AbortController", ++ "AbortSignal", ++ "URL", ++ "URLSearchParams", ++ ]; ++ for (const name of overrideGlobals) { ++ const value = globalThis[name]; ++ if (typeof value !== "undefined") dom.window[name] = value; ++ } + return { + getVmContext() { + return dom.getInternalVMContext(); diff --git a/package.json b/package.json index 96a88559a..a317dcfcb 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,8 @@ }, "resolutions": { "@types/react": "18.3.28", - "@types/react-dom": "18.3.7" + "@types/react-dom": "18.3.7", + "vitest@3.2.3": "patch:vitest@npm%3A3.2.3#./.yarn/patches/vitest-npm-3.2.3-d0d609a9f8.patch" }, "scripts": { "build": "yarn workspaces foreach --interlaced --verbose --parallel --jobs 3 --exclude datagateway-common run build", diff --git a/packages/datagateway-common/package.json b/packages/datagateway-common/package.json index e8ea645a4..750697bd9 100644 --- a/packages/datagateway-common/package.json +++ b/packages/datagateway-common/package.json @@ -22,7 +22,6 @@ "browserslist-to-esbuild": "2.1.1", "date-fns": "2.30.0", "hex-to-rgba": "2.0.1", - "history": "4.10.1", "i18next": "22.0.3", "lodash.debounce": "4.0.8", "loglevel": "1.9.1", @@ -43,7 +42,7 @@ "@mui/material": ">= 5.5.0 < 6", "react": "^18.2.0", "react-dom": "^18.2.0", - "react-router-dom": ">= 5.2.0 < 6" + "react-router": ">= 7 < 8" }, "devDependencies": { "@mui/icons-material": "5.18.0", @@ -55,7 +54,6 @@ "@testing-library/user-event": "14.6.1", "@types/node": "24.12.0", "@types/react": "18.3.28", - "@types/react-router-dom": "5.3.3", "@types/react-virtualized": "9.22.2", "@vitest/coverage-v8": "3.2.3", "eslint": "9.39.3", @@ -70,7 +68,7 @@ "lint-staged": "16.4.0", "react": "18.3.1", "react-dom": "18.3.1", - "react-router-dom": "5.3.4", + "react-router": "7.13.2", "react-test-renderer": "17.0.2", "typescript-eslint": "8.56.1", "vitest": "3.2.3", diff --git a/packages/datagateway-common/src/api/dataPublications.test.tsx b/packages/datagateway-common/src/api/dataPublications.test.tsx index 5c6069580..3754c9963 100644 --- a/packages/datagateway-common/src/api/dataPublications.test.tsx +++ b/packages/datagateway-common/src/api/dataPublications.test.tsx @@ -1,6 +1,7 @@ -import { act, renderHook, waitFor } from '@testing-library/react'; +import { renderHook, screen, waitFor } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; import axios from 'axios'; -import { History, createMemoryHistory } from 'history'; +import { Link } from 'react-router'; import { DataPublication } from '../app.types'; import * as handleICATError from '../handleICATError'; import { createReactQueryWrapper } from '../setupTests'; @@ -16,13 +17,14 @@ import { describe('data publications api functions', () => { let mockData: DataPublication[] = []; - let history: History; let params: URLSearchParams; + let user: ReturnType; const handleICATErrorSpy = vi .spyOn(handleICATError, 'default') .mockImplementation(vi.fn()); beforeEach(() => { + user = userEvent.setup(); mockData = [ { id: 1, @@ -35,11 +37,7 @@ describe('data publications api functions', () => { title: 'Test 2', }, ]; - history = createMemoryHistory({ - initialEntries: [ - '/?sort={"name":"asc","title":"desc"}&filters={"name":{"value":"test","type":"include"}}&page=2&results=20', - ], - }); + window.history.replaceState({}, '', '/'); params = new URLSearchParams(); }); @@ -53,6 +51,11 @@ describe('data publications api functions', () => { vi.mocked(axios.get).mockResolvedValue({ data: mockData, }); + window.history.replaceState( + {}, + '', + '/?sort={"name":"asc","title":"desc"}&filters={"name":{"value":"test","type":"include"}}&page=2&results=20' + ); const { result } = renderHook( () => @@ -68,7 +71,19 @@ describe('data publications api functions', () => { }, ]), { - wrapper: createReactQueryWrapper(history), + wrapper: ({ children }) => { + const Wrapper = createReactQueryWrapper(); + return ( + + <> + {children} + + Test link + + + + ); + }, } ); @@ -106,12 +121,8 @@ describe('data publications api functions', () => { ); expect(result.current.data).toEqual(mockData); - act(() => { - // test that order of sort object triggers new query - history.push( - '/?sort={"title":"desc", "name":"asc"}&filters={"name":{"value":"test","type":"include"}}&page=2&results=20' - ); - }); + // test that order of sort object triggers new query + await user.click(screen.getByRole('link')); await waitFor(() => expect(result.current.isSuccess).toBe(true)); @@ -157,6 +168,11 @@ describe('data publications api functions', () => { ? Promise.resolve({ data: mockData[0] }) : Promise.resolve({ data: mockData[1] }) ); + window.history.replaceState( + {}, + '', + '/?sort={"name":"asc","title":"desc"}&filters={"name":{"value":"test","type":"include"}}&page=2&results=20' + ); const { result } = renderHook( () => @@ -172,7 +188,19 @@ describe('data publications api functions', () => { }, ]), { - wrapper: createReactQueryWrapper(history), + wrapper: ({ children }) => { + const Wrapper = createReactQueryWrapper(); + return ( + + <> + {children} + + Test link + + + + ); + }, } ); @@ -232,12 +260,8 @@ describe('data publications api functions', () => { mockData[1], ]); - act(() => { - // test that order of sort object triggers new query - history.push( - '/?sort={"title":"desc", "name":"asc"}&filters={"name":{"value":"test","type":"include"}}' - ); - }); + // test that order of sort object triggers new query + await user.click(screen.getByRole('link')); await waitFor(() => expect(result.current.isSuccess).toBe(true)); @@ -281,9 +305,14 @@ describe('data publications api functions', () => { vi.mocked(axios.get).mockResolvedValue({ data: mockData, }); + window.history.replaceState( + {}, + '', + '/?sort={"name":"asc","title":"desc"}&filters={"name":{"value":"test","type":"include"}}&page=2&results=20' + ); const { result } = renderHook(() => useDataPublication(1), { - wrapper: createReactQueryWrapper(history), + wrapper: createReactQueryWrapper(), }); await waitFor(() => expect(result.current.isSuccess).toBe(true)); @@ -409,6 +438,11 @@ describe('data publications api functions', () => { name: { eq: 'test' }, }) ); + window.history.replaceState( + {}, + '', + '/?sort={"name":"asc","title":"desc"}&filters={"name":{"value":"test","type":"include"}}&page=2&results=20' + ); const { result } = renderHook( () => @@ -419,7 +453,7 @@ describe('data publications api functions', () => { }, ]), { - wrapper: createReactQueryWrapper(history), + wrapper: createReactQueryWrapper(), } ); @@ -445,6 +479,12 @@ describe('data publications api functions', () => { params.append('order', JSON.stringify('id asc')); params.append('include', '"type"'); + window.history.replaceState( + {}, + '', + '/?sort={"name":"asc","title":"desc"}&filters={"name":{"value":"test","type":"include"}}&page=2&results=20' + ); + const { result } = renderHook( () => useDataPublicationsByFilters([ @@ -454,7 +494,7 @@ describe('data publications api functions', () => { }, ]), { - wrapper: createReactQueryWrapper(history), + wrapper: createReactQueryWrapper(), } ); @@ -483,6 +523,11 @@ describe('data publications api functions', () => { vi.mocked(axios.get).mockResolvedValue({ data: mockData.length, }); + window.history.replaceState( + {}, + '', + '/?sort={"name":"asc","title":"desc"}&filters={"name":{"value":"test","type":"include"}}&page=2&results=20' + ); const { result } = renderHook( () => @@ -498,7 +543,7 @@ describe('data publications api functions', () => { }, ]), { - wrapper: createReactQueryWrapper(history), + wrapper: createReactQueryWrapper(), } ); @@ -567,11 +612,16 @@ describe('data publications api functions', () => { ? Promise.resolve({ data: mockData[0] }) : Promise.resolve({ data: mockData[1] }) ); + window.history.replaceState( + {}, + '', + '/?sort={"name":"asc","title":"desc"}&filters={"name":{"value":"test","type":"include"}}&page=2&results=20' + ); const { result } = renderHook( () => useDataPublicationContent('1', 'investigation'), { - wrapper: createReactQueryWrapper(history), + wrapper: createReactQueryWrapper(), } ); @@ -643,11 +693,16 @@ describe('data publications api functions', () => { vi.mocked(axios.get).mockImplementation((_url, _options) => Promise.resolve({ data: mockData[0] }) ); + window.history.replaceState( + {}, + '', + '/?sort={"name":"asc","title":"desc"}&filters={"name":{"value":"test","type":"include"}}&page=2&results=20' + ); const { result } = renderHook( () => useDataPublicationContent('1', 'dataset'), { - wrapper: createReactQueryWrapper(history), + wrapper: createReactQueryWrapper(), } ); @@ -689,11 +744,16 @@ describe('data publications api functions', () => { vi.mocked(axios.get).mockImplementation((_url, _options) => Promise.resolve({ data: mockData[0] }) ); + window.history.replaceState( + {}, + '', + '/?sort={"name":"asc","title":"desc"}&filters={"name":{"value":"test","type":"include"}}&page=2&results=20' + ); const { result } = renderHook( () => useDataPublicationContent('1', 'datafile'), { - wrapper: createReactQueryWrapper(history), + wrapper: createReactQueryWrapper(), } ); @@ -779,11 +839,16 @@ describe('data publications api functions', () => { vi.mocked(axios.get).mockResolvedValue({ data: mockData.length, }); + window.history.replaceState( + {}, + '', + '/?sort={"name":"asc","title":"desc"}&filters={"name":{"value":"test","type":"include"}}&page=2&results=20' + ); const { result } = renderHook( () => useDataPublicationContentCount('1', 'investigation'), { - wrapper: createReactQueryWrapper(history), + wrapper: createReactQueryWrapper(), } ); @@ -820,11 +885,16 @@ describe('data publications api functions', () => { vi.mocked(axios.get).mockResolvedValue({ data: mockData.length, }); + window.history.replaceState( + {}, + '', + '/?sort={"name":"asc","title":"desc"}&filters={"name":{"value":"test","type":"include"}}&page=2&results=20' + ); const { result } = renderHook( () => useDataPublicationContentCount('1', 'dataset'), { - wrapper: createReactQueryWrapper(history), + wrapper: createReactQueryWrapper(), } ); @@ -861,11 +931,16 @@ describe('data publications api functions', () => { vi.mocked(axios.get).mockResolvedValue({ data: mockData.length, }); + window.history.replaceState( + {}, + '', + '/?sort={"name":"asc","title":"desc"}&filters={"name":{"value":"test","type":"include"}}&page=2&results=20' + ); const { result } = renderHook( () => useDataPublicationContentCount('1', 'datafile'), { - wrapper: createReactQueryWrapper(history), + wrapper: createReactQueryWrapper(), } ); diff --git a/packages/datagateway-common/src/api/dataPublications.tsx b/packages/datagateway-common/src/api/dataPublications.tsx index 26916a3c2..66b3a16f8 100644 --- a/packages/datagateway-common/src/api/dataPublications.tsx +++ b/packages/datagateway-common/src/api/dataPublications.tsx @@ -1,7 +1,7 @@ import { useInfiniteQuery, useQuery } from '@tanstack/react-query'; import axios from 'axios'; import { useSelector } from 'react-redux'; -import { useLocation } from 'react-router-dom'; +import { useLocation } from 'react-router'; import { AdditionalFilters, DataPublication, diff --git a/packages/datagateway-common/src/api/datafiles.test.tsx b/packages/datagateway-common/src/api/datafiles.test.tsx index dd4b57791..7fe29d466 100644 --- a/packages/datagateway-common/src/api/datafiles.test.tsx +++ b/packages/datagateway-common/src/api/datafiles.test.tsx @@ -1,6 +1,7 @@ -import { act, renderHook, waitFor } from '@testing-library/react'; +import { renderHook, screen, waitFor } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; import axios from 'axios'; -import { History, createMemoryHistory } from 'history'; +import { Link } from 'react-router'; import { Datafile } from '../app.types'; import * as handleICATError from '../handleICATError'; import { createReactQueryWrapper } from '../setupTests'; @@ -15,11 +16,12 @@ import { describe('datafile api functions', () => { let mockData: Datafile[] = []; - let history: History; let params: URLSearchParams; let handleICATErrorSpy: ReturnType; + let user: ReturnType; beforeEach(() => { + user = userEvent.setup(); mockData = [ { id: 1, @@ -38,11 +40,7 @@ describe('datafile api functions', () => { createTime: '2019-06-10', }, ]; - history = createMemoryHistory({ - initialEntries: [ - '/?sort={"name":"asc","title":"desc"}&filters={"name":{"value":"test","type":"include"}}&page=2&results=20', - ], - }); + window.history.replaceState({}, '', '/'); params = new URLSearchParams(); handleICATErrorSpy = vi .spyOn(handleICATError, 'default') @@ -59,6 +57,11 @@ describe('datafile api functions', () => { vi.mocked(axios.get).mockResolvedValue({ data: mockData, }); + window.history.replaceState( + {}, + '', + '/?sort={"name":"asc","title":"desc"}&filters={"name":{"value":"test","type":"include"}}&page=2&results=20' + ); const { result } = renderHook( () => @@ -71,7 +74,19 @@ describe('datafile api functions', () => { }, ]), { - wrapper: createReactQueryWrapper(history), + wrapper: ({ children }) => { + const Wrapper = createReactQueryWrapper(); + return ( + + <> + {children} + + Test link + + + + ); + }, } ); @@ -106,12 +121,8 @@ describe('datafile api functions', () => { ); expect(result.current.data).toEqual(mockData); - act(() => { - // test that order of sort object triggers new query - history.push( - '/?sort={"title":"desc", "name":"asc"}&filters={"name":{"value":"test","type":"include"}}&page=2&results=20' - ); - }); + // test that order of sort object triggers new query + await user.click(screen.getByRole('link')); await waitFor(() => expect(result.current.isSuccess).toBe(true)); @@ -157,6 +168,11 @@ describe('datafile api functions', () => { ? Promise.resolve({ data: mockData[0] }) : Promise.resolve({ data: mockData[1] }) ); + window.history.replaceState( + {}, + '', + '/?sort={"name":"asc","title":"desc"}&filters={"name":{"value":"test","type":"include"}}&page=2&results=20' + ); const { result } = renderHook( () => @@ -169,7 +185,19 @@ describe('datafile api functions', () => { }, ]), { - wrapper: createReactQueryWrapper(history), + wrapper: ({ children }) => { + const Wrapper = createReactQueryWrapper(); + return ( + + <> + {children} + + Test link + + + + ); + }, } ); @@ -226,12 +254,7 @@ describe('datafile api functions', () => { mockData[1], ]); - act(() => { - // test that order of sort object triggers new query - history.push( - '/?sort={"title":"desc", "name":"asc"}&filters={"name":{"value":"test","type":"include"}}' - ); - }); + await user.click(screen.getByRole('link')); await waitFor(() => expect(result.current.isSuccess).toBe(true)); @@ -275,6 +298,11 @@ describe('datafile api functions', () => { vi.mocked(axios.get).mockResolvedValue({ data: mockData.length, }); + window.history.replaceState( + {}, + '', + '/?sort={"name":"asc","title":"desc"}&filters={"name":{"value":"test","type":"include"}}&page=2&results=20' + ); const { result } = renderHook( () => @@ -285,7 +313,7 @@ describe('datafile api functions', () => { }, ]), { - wrapper: createReactQueryWrapper(history), + wrapper: createReactQueryWrapper(), } ); @@ -479,6 +507,9 @@ describe('datafile api functions', () => { it('should create a download for the datafile with a server URL', async () => { vi.spyOn(document, 'createElement'); vi.spyOn(document.body, 'appendChild'); + vi.spyOn(window.URL, 'createObjectURL').mockImplementation( + () => 'downloadDatafileTestObjectUrl' + ); downloadDatafile('https://www.example.com/ids', 1, 'test'); @@ -493,6 +524,9 @@ describe('datafile api functions', () => { it('should create a download for the datafile with the given Blob content', async () => { vi.spyOn(document, 'createElement'); vi.spyOn(document.body, 'appendChild'); + vi.spyOn(window.URL, 'createObjectURL').mockImplementation( + () => 'downloadDatafileTestObjectUrl2' + ); downloadDatafile( 'https://www.example.com/ids', @@ -503,7 +537,7 @@ describe('datafile api functions', () => { expect(document.createElement).toHaveBeenCalledWith('a'); const link = document.createElement('a'); - link.href = 'testObjectUrl'; + link.href = 'downloadDatafileTestObjectUrl2'; link.download = 'test'; link.target = '_blank'; link.style.display = 'none'; diff --git a/packages/datagateway-common/src/api/datafiles.tsx b/packages/datagateway-common/src/api/datafiles.tsx index ac4a56e43..debe6818a 100644 --- a/packages/datagateway-common/src/api/datafiles.tsx +++ b/packages/datagateway-common/src/api/datafiles.tsx @@ -2,7 +2,7 @@ import type { UseQueryResult } from '@tanstack/react-query'; import { useInfiniteQuery, useQuery } from '@tanstack/react-query'; import axios, { AxiosError, AxiosProgressEvent } from 'axios'; import { useSelector } from 'react-redux'; -import { useLocation } from 'react-router-dom'; +import { useLocation } from 'react-router'; import { getApiParams, parseSearchToQuery, useEntity } from '.'; import { AdditionalFilters, diff --git a/packages/datagateway-common/src/api/datasets.test.tsx b/packages/datagateway-common/src/api/datasets.test.tsx index bd32aab87..59206a367 100644 --- a/packages/datagateway-common/src/api/datasets.test.tsx +++ b/packages/datagateway-common/src/api/datasets.test.tsx @@ -1,6 +1,7 @@ -import { act, renderHook, waitFor } from '@testing-library/react'; +import { renderHook, screen, waitFor } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; import axios from 'axios'; -import { History, createMemoryHistory } from 'history'; +import { Link } from 'react-router'; import { Dataset } from '../app.types'; import * as handleICATError from '../handleICATError'; import { createReactQueryWrapper } from '../setupTests'; @@ -14,13 +15,14 @@ import { describe('dataset api functions', () => { let mockData: Dataset[] = []; - let history: History; let params: URLSearchParams; + let user: ReturnType; const handleICATErrorSpy = vi .spyOn(handleICATError, 'default') .mockImplementation(vi.fn()); beforeEach(() => { + user = userEvent.setup(); mockData = [ { id: 1, @@ -41,11 +43,8 @@ describe('dataset api functions', () => { createTime: '2019-06-12', }, ]; - history = createMemoryHistory({ - initialEntries: [ - '/?sort={"name":"asc","title":"desc"}&filters={"name":{"value":"test","type":"include"}}&page=2&results=20', - ], - }); + window.history.replaceState({}, '', '/'); + params = new URLSearchParams(); }); @@ -60,6 +59,11 @@ describe('dataset api functions', () => { vi.mocked(axios.get).mockResolvedValue({ data: mockData, }); + window.history.replaceState( + {}, + '', + '/?sort={"name":"asc","title":"desc"}&filters={"name":{"value":"test","type":"include"}}&page=2&results=20' + ); const { result } = renderHook( () => @@ -72,7 +76,19 @@ describe('dataset api functions', () => { }, ]), { - wrapper: createReactQueryWrapper(history), + wrapper: ({ children }) => { + const Wrapper = createReactQueryWrapper(); + return ( + + <> + {children} + + Test link + + + + ); + }, } ); @@ -107,12 +123,8 @@ describe('dataset api functions', () => { ); expect(result.current.data).toEqual(mockData); - act(() => { - // test that order of sort object triggers new query - history.push( - '/?sort={"title":"desc", "name":"asc"}&filters={"name":{"value":"test","type":"include"}}&page=2&results=20' - ); - }); + // test that order of sort object triggers new query + await user.click(screen.getByRole('link')); await waitFor(() => expect(result.current.isSuccess).toBe(true)); @@ -158,6 +170,11 @@ describe('dataset api functions', () => { ? Promise.resolve({ data: mockData[0] }) : Promise.resolve({ data: mockData[1] }) ); + window.history.replaceState( + {}, + '', + '/?sort={"name":"asc","title":"desc"}&filters={"name":{"value":"test","type":"include"}}&page=2&results=20' + ); const { result } = renderHook( () => @@ -170,7 +187,19 @@ describe('dataset api functions', () => { }, ]), { - wrapper: createReactQueryWrapper(history), + wrapper: ({ children }) => { + const Wrapper = createReactQueryWrapper(); + return ( + + <> + {children} + + Test link + + + + ); + }, } ); @@ -227,12 +256,8 @@ describe('dataset api functions', () => { mockData[1], ]); - act(() => { - // test that order of sort object triggers new query - history.push( - '/?sort={"title":"desc", "name":"asc"}&filters={"name":{"value":"test","type":"include"}}' - ); - }); + // test that order of sort object triggers new query + await user.click(screen.getByRole('link')); await waitFor(() => expect(result.current.isSuccess).toBe(true)); @@ -276,6 +301,11 @@ describe('dataset api functions', () => { vi.mocked(axios.get).mockResolvedValue({ data: mockData.length, }); + window.history.replaceState( + {}, + '', + '/?sort={"name":"asc","title":"desc"}&filters={"name":{"value":"test","type":"include"}}&page=2&results=20' + ); const { result } = renderHook( () => @@ -286,7 +316,7 @@ describe('dataset api functions', () => { }, ]), { - wrapper: createReactQueryWrapper(history), + wrapper: createReactQueryWrapper(), } ); diff --git a/packages/datagateway-common/src/api/datasets.tsx b/packages/datagateway-common/src/api/datasets.tsx index ae658ac4a..c65ca1c70 100644 --- a/packages/datagateway-common/src/api/datasets.tsx +++ b/packages/datagateway-common/src/api/datasets.tsx @@ -5,7 +5,7 @@ import { } from '@tanstack/react-query'; import axios, { AxiosError } from 'axios'; import { useSelector } from 'react-redux'; -import { useLocation } from 'react-router-dom'; +import { useLocation } from 'react-router'; import { getApiParams, parseSearchToQuery, useEntity } from '.'; import { AdditionalFilters, diff --git a/packages/datagateway-common/src/api/dois.test.tsx b/packages/datagateway-common/src/api/dois.test.tsx index 1c07a39b5..09628b189 100644 --- a/packages/datagateway-common/src/api/dois.test.tsx +++ b/packages/datagateway-common/src/api/dois.test.tsx @@ -451,7 +451,7 @@ describe('doi api functions', () => { const resetQueriesSpy = vi.spyOn(queryClient, 'resetQueries'); const { result } = renderHook(() => usePublishDraftVersion(), { - wrapper: createReactQueryWrapper(undefined, queryClient), + wrapper: createReactQueryWrapper(queryClient), }); act(() => { diff --git a/packages/datagateway-common/src/api/facilityCycles.test.tsx b/packages/datagateway-common/src/api/facilityCycles.test.tsx index fb52f99be..c7fd0b9e0 100644 --- a/packages/datagateway-common/src/api/facilityCycles.test.tsx +++ b/packages/datagateway-common/src/api/facilityCycles.test.tsx @@ -1,6 +1,7 @@ -import { act, renderHook, waitFor } from '@testing-library/react'; +import { renderHook, screen, waitFor } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; import axios from 'axios'; -import { History, createMemoryHistory } from 'history'; +import { Link } from 'react-router'; import { FacilityCycle } from '../app.types'; import * as handleICATError from '../handleICATError'; import { createReactQueryWrapper } from '../setupTests'; @@ -13,13 +14,14 @@ import { describe('facility cycle api functions', () => { let mockData: FacilityCycle[] = []; - let history: History; let params: URLSearchParams; + let user: ReturnType; const handleICATErrorSpy = vi .spyOn(handleICATError, 'default') .mockImplementation(vi.fn()); beforeEach(() => { + user = userEvent.setup(); mockData = [ { id: 1, @@ -36,11 +38,7 @@ describe('facility cycle api functions', () => { endDate: '2019-07-04', }, ]; - history = createMemoryHistory({ - initialEntries: [ - '/?sort={"name":"asc","title":"desc"}&filters={"name":{"value":"test","type":"include"}}&page=2&results=20', - ], - }); + window.history.replaceState({}, '', '/'); params = new URLSearchParams(); }); @@ -92,9 +90,26 @@ describe('facility cycle api functions', () => { vi.mocked(axios.get).mockResolvedValue({ data: mockData, }); + window.history.replaceState( + {}, + '', + '/?sort={"name":"asc","title":"desc"}&filters={"name":{"value":"test","type":"include"}}&page=2&results=20' + ); const { result } = renderHook(() => useFacilityCyclesPaginated(1), { - wrapper: createReactQueryWrapper(history), + wrapper: ({ children }) => { + const Wrapper = createReactQueryWrapper(); + return ( + + <> + {children} + + Test link + + + + ); + }, }); await waitFor(() => expect(result.current.isSuccess).toBe(true)); @@ -135,12 +150,8 @@ describe('facility cycle api functions', () => { ); expect(result.current.data).toEqual(mockData); - act(() => { - // test that order of sort object triggers new query - history.push( - '/?sort={"title":"desc", "name":"asc"}&filters={"name":{"value":"test","type":"include"}}&page=2&results=20' - ); - }); + // test that order of sort object triggers new query + await user.click(screen.getByRole('link')); await waitFor(() => expect(result.current.isSuccess).toBe(true)); @@ -197,9 +208,26 @@ describe('facility cycle api functions', () => { ? Promise.resolve({ data: mockData[0] }) : Promise.resolve({ data: mockData[1] }) ); + window.history.replaceState( + {}, + '', + '/?sort={"name":"asc","title":"desc"}&filters={"name":{"value":"test","type":"include"}}&page=2&results=20' + ); const { result } = renderHook(() => useFacilityCyclesInfinite(1), { - wrapper: createReactQueryWrapper(history), + wrapper: ({ children }) => { + const Wrapper = createReactQueryWrapper(); + return ( + + <> + {children} + + Test link + + + + ); + }, }); await waitFor(() => expect(result.current.isSuccess).toBe(true)); @@ -262,12 +290,8 @@ describe('facility cycle api functions', () => { mockData[1], ]); - act(() => { - // test that order of sort object triggers new query - history.push( - '/?sort={"title":"desc", "name":"asc"}&filters={"name":{"value":"test","type":"include"}}' - ); - }); + // test that order of sort object triggers new query + await user.click(screen.getByRole('link')); await waitFor(() => expect(result.current.isSuccess).toBe(true)); @@ -322,9 +346,14 @@ describe('facility cycle api functions', () => { vi.mocked(axios.get).mockResolvedValue({ data: mockData.length, }); + window.history.replaceState( + {}, + '', + '/?sort={"name":"asc","title":"desc"}&filters={"name":{"value":"test","type":"include"}}&page=2&results=20' + ); const { result } = renderHook(() => useFacilityCycleCount(1), { - wrapper: createReactQueryWrapper(history), + wrapper: createReactQueryWrapper(), }); await waitFor(() => expect(result.current.isSuccess).toBe(true)); diff --git a/packages/datagateway-common/src/api/facilityCycles.tsx b/packages/datagateway-common/src/api/facilityCycles.tsx index 948d8c3c3..4713e3429 100644 --- a/packages/datagateway-common/src/api/facilityCycles.tsx +++ b/packages/datagateway-common/src/api/facilityCycles.tsx @@ -1,7 +1,7 @@ import { useInfiniteQuery, useQuery } from '@tanstack/react-query'; import axios from 'axios'; import { useSelector } from 'react-redux'; -import { useLocation } from 'react-router-dom'; +import { useLocation } from 'react-router'; import { getApiParams, parseSearchToQuery } from '.'; import { FacilityCycle, diff --git a/packages/datagateway-common/src/api/index.test.tsx b/packages/datagateway-common/src/api/index.test.tsx index 3017e2dc4..31282bb0a 100644 --- a/packages/datagateway-common/src/api/index.test.tsx +++ b/packages/datagateway-common/src/api/index.test.tsx @@ -1,9 +1,8 @@ import { act, renderHook, waitFor } from '@testing-library/react'; import axios from 'axios'; -import { History, createMemoryHistory } from 'history'; import React from 'react'; -import { Router } from 'react-router-dom'; -import type { MockInstance } from 'vitest'; +import { RouterProvider, createBrowserRouter } from 'react-router'; +import { MockInstance } from 'vitest'; import { FiltersType, Investigation, @@ -340,20 +339,17 @@ describe('generic api functions', () => { }); describe('push functions', () => { - let history: History; let wrapper: React.JSXElementConstructor<{ - children: React.ReactElement; + children: React.ReactNode; }>; - let pushSpy: MockInstance; - let replaceSpy: MockInstance; + let router: ReturnType; + let navigateSpy: MockInstance; beforeEach(() => { - history = createMemoryHistory(); - pushSpy = vi.spyOn(history, 'push'); - replaceSpy = vi.spyOn(history, 'replace'); - const newWrapper: React.JSXElementConstructor<{ - children: React.ReactElement; - }> = ({ children }) => {children}; - wrapper = newWrapper; + wrapper = ({ children }) => { + router = createBrowserRouter([{ path: '*', element: children }]); + navigateSpy = vi.spyOn(router, 'navigate'); + return ; + }; }); afterEach(() => { @@ -361,6 +357,7 @@ describe('generic api functions', () => { vi.resetModules(); window.history.pushState({}, 'Test', '/'); vi.doUnmock('./index.tsx'); + navigateSpy.mockClear(); }); describe('useSort', () => { @@ -373,9 +370,12 @@ describe('generic api functions', () => { result.current('name', 'asc', 'push'); }); - expect(pushSpy).toHaveBeenCalledWith({ - search: `?sort=${encodeURIComponent('{"name":"asc"}')}`, - }); + expect(navigateSpy).toHaveBeenCalledWith( + { + search: `?sort=${encodeURIComponent('{"name":"asc"}')}`, + }, + expect.objectContaining({ replace: false }) + ); }); it('returns callback that can replace the sort with a new one in the url query', () => { @@ -387,9 +387,12 @@ describe('generic api functions', () => { result.current('name', 'asc', 'replace'); }); - expect(replaceSpy).toHaveBeenCalledWith({ - search: `?sort=${encodeURIComponent('{"name":"asc"}')}`, - }); + expect(navigateSpy).toHaveBeenCalledWith( + { + search: `?sort=${encodeURIComponent('{"name":"asc"}')}`, + }, + expect.objectContaining({ replace: true }) + ); }); it('returns callback that when called removes a null sort from the url query', () => { @@ -406,9 +409,12 @@ describe('generic api functions', () => { result.current('name', null, 'push'); }); - expect(pushSpy).toHaveBeenCalledWith({ - search: '?', - }); + expect(navigateSpy).toHaveBeenCalledWith( + { + search: '?', + }, + expect.objectContaining({ replace: false }) + ); }); it('returns callback that, when called without shift modifier, replaces sort with the new one', () => { @@ -425,9 +431,12 @@ describe('generic api functions', () => { result.current('title', 'asc', 'push', false); }); - expect(pushSpy).toHaveBeenCalledWith({ - search: `?sort=${encodeURIComponent('{"title":"asc"}')}`, - }); + expect(navigateSpy).toHaveBeenCalledWith( + { + search: `?sort=${encodeURIComponent('{"title":"asc"}')}`, + }, + expect.objectContaining({ replace: false }) + ); }); it('returns callback that, when called with shift modifier, appends new sort to the existing one', () => { @@ -445,9 +454,12 @@ describe('generic api functions', () => { result.current('title', 'asc', 'push', true); }); - expect(pushSpy).toHaveBeenCalledWith({ - search: `?sort=${encodeURIComponent('{"name":"asc","title":"asc"}')}`, - }); + expect(navigateSpy).toHaveBeenCalledWith( + { + search: `?sort=${encodeURIComponent('{"name":"asc","title":"asc"}')}`, + }, + expect.objectContaining({ replace: false }) + ); }); }); @@ -461,11 +473,14 @@ describe('generic api functions', () => { result.current('name', { value: 'test', type: 'include' }); }); - expect(pushSpy).toHaveBeenCalledWith({ - search: `?filters=${encodeURIComponent( - '{"name":{"value":"test","type":"include"}}' - )}`, - }); + expect(navigateSpy).toHaveBeenCalledWith( + { + search: `?filters=${encodeURIComponent( + '{"name":{"value":"test","type":"include"}}' + )}`, + }, + expect.objectContaining({ replace: false }) + ); }); it('returns callback that when called removes a null sort from the url query', () => { @@ -485,9 +500,12 @@ describe('generic api functions', () => { result.current('name', null); }); - expect(pushSpy).toHaveBeenCalledWith({ - search: '?', - }); + expect(navigateSpy).toHaveBeenCalledWith( + { + search: '?', + }, + expect.objectContaining({ replace: false }) + ); }); it('can pass a filter prefix to the callback', () => { @@ -499,11 +517,14 @@ describe('generic api functions', () => { result.current('name', { value: 'test', type: 'include' }); }); - expect(pushSpy).toHaveBeenCalledWith({ - search: `?filters=${encodeURIComponent( - '{"prefix.name":{"value":"test","type":"include"}}' - )}`, - }); + expect(navigateSpy).toHaveBeenCalledWith( + { + search: `?filters=${encodeURIComponent( + '{"prefix.name":{"value":"test","type":"include"}}' + )}`, + }, + expect.objectContaining({ replace: false }) + ); vi.doMock('./index.tsx', async () => ({ ...(await vi.importActual('./index.tsx')), @@ -517,9 +538,12 @@ describe('generic api functions', () => { result.current('name', null); }); - expect(pushSpy).toHaveBeenCalledWith({ - search: '?', - }); + expect(navigateSpy).toHaveBeenCalledWith( + { + search: '?', + }, + expect.objectContaining({ replace: false }) + ); }); }); @@ -536,11 +560,14 @@ describe('generic api functions', () => { ]); }); - expect(pushSpy).toHaveBeenCalledWith({ - search: `?filters=${encodeURIComponent( - '{"name":{"value":"test","type":"include"},"title":{"value":"test2","type":"include"}}' - )}`, - }); + expect(navigateSpy).toHaveBeenCalledWith( + { + search: `?filters=${encodeURIComponent( + '{"name":{"value":"test","type":"include"},"title":{"value":"test2","type":"include"}}' + )}`, + }, + expect.any(Object) + ); }); it('returns callback that when called removes multiple null filters from the url query', () => { @@ -563,9 +590,12 @@ describe('generic api functions', () => { ]); }); - expect(pushSpy).toHaveBeenCalledWith({ - search: '?', - }); + expect(navigateSpy).toHaveBeenCalledWith( + { + search: '?', + }, + expect.any(Object) + ); }); }); @@ -579,7 +609,7 @@ describe('generic api functions', () => { result.current(1); }); - expect(pushSpy).toHaveBeenCalledWith('?page=1'); + expect(navigateSpy).toHaveBeenCalledWith('?page=1', expect.any(Object)); }); }); @@ -593,7 +623,10 @@ describe('generic api functions', () => { result.current(10); }); - expect(pushSpy).toHaveBeenCalledWith('?results=10'); + expect(navigateSpy).toHaveBeenCalledWith( + '?results=10', + expect.any(Object) + ); }); }); @@ -618,10 +651,13 @@ describe('generic api functions', () => { }); }); - expect(pushSpy).toHaveBeenCalledWith({ - search: - '?filters=%7B%22name%22%3A%7B%22value%22%3A%22test%22%2C%22type%22%3A%22include%22%7D%2C%22title%22%3A%7B%22value%22%3A%22test2%22%2C%22type%22%3A%22include%22%7D%7D', - }); + expect(navigateSpy).toHaveBeenCalledWith( + { + search: + '?filters=%7B%22name%22%3A%7B%22value%22%3A%22test%22%2C%22type%22%3A%22include%22%7D%2C%22title%22%3A%7B%22value%22%3A%22test2%22%2C%22type%22%3A%22include%22%7D%7D', + }, + expect.objectContaining({ replace: false }) + ); }); it('returns callback that when called removes all sorts from the url query (push)', () => { @@ -641,9 +677,12 @@ describe('generic api functions', () => { result.current({ name: 'asc' }); }); - expect(pushSpy).toHaveBeenCalledWith({ - search: '?sort=%7B%22name%22%3A%22asc%22%7D', - }); + expect(navigateSpy).toHaveBeenCalledWith( + { + search: '?sort=%7B%22name%22%3A%22asc%22%7D', + }, + expect.objectContaining({ replace: false }) + ); }); it('returns callback that when called removes page number from the url query (push)', () => { @@ -663,9 +702,12 @@ describe('generic api functions', () => { result.current(2); }); - expect(pushSpy).toHaveBeenCalledWith({ - search: '?page=2', - }); + expect(navigateSpy).toHaveBeenCalledWith( + { + search: '?page=2', + }, + expect.objectContaining({ replace: false }) + ); }); it('returns callback that when called removes results number from the url query (push)', () => { @@ -685,9 +727,12 @@ describe('generic api functions', () => { result.current(10); }); - expect(pushSpy).toHaveBeenCalledWith({ - search: '?results=10', - }); + expect(navigateSpy).toHaveBeenCalledWith( + { + search: '?results=10', + }, + expect.objectContaining({ replace: false }) + ); }); it('returns callback that when called removes all filters from the url query (replace)', () => { @@ -710,10 +755,13 @@ describe('generic api functions', () => { }); }); - expect(replaceSpy).toHaveBeenCalledWith({ - search: - '?filters=%7B%22name%22%3A%7B%22value%22%3A%22test%22%2C%22type%22%3A%22include%22%7D%2C%22title%22%3A%7B%22value%22%3A%22test2%22%2C%22type%22%3A%22include%22%7D%7D', - }); + expect(navigateSpy).toHaveBeenCalledWith( + { + search: + '?filters=%7B%22name%22%3A%7B%22value%22%3A%22test%22%2C%22type%22%3A%22include%22%7D%2C%22title%22%3A%7B%22value%22%3A%22test2%22%2C%22type%22%3A%22include%22%7D%7D', + }, + expect.objectContaining({ replace: true }) + ); }); it('returns callback that when called removes all sorts from the url query (replace)', () => { @@ -733,9 +781,12 @@ describe('generic api functions', () => { result.current({ name: 'asc' }); }); - expect(replaceSpy).toHaveBeenCalledWith({ - search: '?sort=%7B%22name%22%3A%22asc%22%7D', - }); + expect(navigateSpy).toHaveBeenCalledWith( + { + search: '?sort=%7B%22name%22%3A%22asc%22%7D', + }, + expect.objectContaining({ replace: true }) + ); }); it('returns callback that when called removes page number from the url query (replace)', () => { @@ -755,9 +806,12 @@ describe('generic api functions', () => { result.current(2); }); - expect(replaceSpy).toHaveBeenCalledWith({ - search: '?page=2', - }); + expect(navigateSpy).toHaveBeenCalledWith( + { + search: '?page=2', + }, + expect.objectContaining({ replace: true }) + ); }); it('returns callback that when called removes results number from the url query (replace)', () => { @@ -777,9 +831,12 @@ describe('generic api functions', () => { result.current(10); }); - expect(replaceSpy).toHaveBeenCalledWith({ - search: '?results=10', - }); + expect(navigateSpy).toHaveBeenCalledWith( + { + search: '?results=10', + }, + expect.objectContaining({ replace: true }) + ); }); }); @@ -793,7 +850,10 @@ describe('generic api functions', () => { result.current('table'); }); - expect(pushSpy).toHaveBeenCalledWith('?view=table'); + expect(navigateSpy).toHaveBeenCalledWith( + '?view=table', + expect.objectContaining({ replace: false }) + ); }); it('returns callback that when called replaces the current view with a new one in the url query', () => { @@ -805,7 +865,10 @@ describe('generic api functions', () => { result.current('table'); }); - expect(replaceSpy).toHaveBeenCalledWith('?view=table'); + expect(navigateSpy).toHaveBeenCalledWith( + '?view=table', + expect.objectContaining({ replace: true }) + ); }); }); @@ -819,9 +882,12 @@ describe('generic api functions', () => { result.current('test'); }); - expect(pushSpy).toHaveBeenCalledWith({ - search: '?searchText=test', - }); + expect(navigateSpy).toHaveBeenCalledWith( + { + search: '?searchText=test', + }, + expect.any(Object) + ); }); }); @@ -835,8 +901,9 @@ describe('generic api functions', () => { result.current(false, false, false); }); - expect(pushSpy).toHaveBeenCalledWith( - '?dataset=false&datafile=false&investigation=false' + expect(navigateSpy).toHaveBeenCalledWith( + '?dataset=false&datafile=false&investigation=false', + expect.any(Object) ); }); }); @@ -851,8 +918,9 @@ describe('generic api functions', () => { result.current(new Date('2021-10-17T00:00:00Z')); }); - expect(pushSpy).toHaveBeenCalledWith( - expect.stringContaining('?startDate=2021-10-17') + expect(navigateSpy).toHaveBeenCalledWith( + expect.stringContaining('?startDate=2021-10-17'), + expect.any(Object) ); }); it('returns callback that when called with null can remove startDate from the url query', () => { @@ -865,7 +933,7 @@ describe('generic api functions', () => { result.current(null); }); - expect(pushSpy).toHaveBeenLastCalledWith('?'); + expect(navigateSpy).toHaveBeenCalledWith('?', expect.any(Object)); }); }); @@ -879,8 +947,9 @@ describe('generic api functions', () => { result.current(new Date('2021-10-25T00:00:00Z')); }); - expect(pushSpy).toHaveBeenCalledWith( - expect.stringContaining('?endDate=2021-10-25') + expect(navigateSpy).toHaveBeenCalledWith( + expect.stringContaining('?endDate=2021-10-25'), + expect.any(Object) ); }); it('returns callback that when called with null can remove endDate from the url query', () => { @@ -893,17 +962,17 @@ describe('generic api functions', () => { result.current(null); }); - expect(pushSpy).toHaveBeenLastCalledWith('?'); + expect(navigateSpy).toHaveBeenCalledWith('?', expect.any(Object)); }); }); describe('usePushQueryParams', () => { it('returns callback that when called pushes query params to the url query', () => { - history.replace({ - search: - '?view=table&searchText=testText&datafile=false&startDate=2021-10-17&endDate=2021-10-25', - }); - replaceSpy.mockClear(); + window.history.replaceState( + {}, + '', + '/?view=table&searchText=testText&datafile=false&startDate=2021-10-17&endDate=2021-10-25' + ); const { result } = renderHook(() => usePushQueryParams(), { wrapper, @@ -922,10 +991,13 @@ describe('generic api functions', () => { }); }); - expect(pushSpy).toHaveBeenCalledWith({ - search: - '?view=card&searchText=newText&dataset=false&investigation=false&endDate=2021-10-25¤tTab=dataset&restrict=true', - }); + expect(navigateSpy).toHaveBeenCalledWith( + { + search: + '?view=card&searchText=newText&dataset=false&investigation=false&endDate=2021-10-25¤tTab=dataset&restrict=true', + }, + expect.any(Object) + ); }); }); @@ -939,9 +1011,12 @@ describe('generic api functions', () => { result.current('name', 'asc', 'push'); }); - expect(pushSpy).toHaveBeenCalledWith({ - search: `?sort=${encodeURIComponent('{"name":"asc"}')}`, - }); + expect(navigateSpy).toHaveBeenCalledWith( + { + search: `?sort=${encodeURIComponent('{"name":"asc"}')}`, + }, + expect.objectContaining({ replace: false }) + ); }); it('returns callback that can replace the sort with a new one in the url query', () => { @@ -953,9 +1028,12 @@ describe('generic api functions', () => { result.current('name', 'asc', 'replace'); }); - expect(replaceSpy).toHaveBeenCalledWith({ - search: `?sort=${encodeURIComponent('{"name":"asc"}')}`, - }); + expect(navigateSpy).toHaveBeenCalledWith( + { + search: `?sort=${encodeURIComponent('{"name":"asc"}')}`, + }, + expect.objectContaining({ replace: true }) + ); }); it('returns callback that when called removes a null sort from the url query', () => { @@ -972,9 +1050,12 @@ describe('generic api functions', () => { result.current('name', null, 'push'); }); - expect(pushSpy).toHaveBeenCalledWith({ - search: '?', - }); + expect(navigateSpy).toHaveBeenCalledWith( + { + search: '?', + }, + expect.objectContaining({ replace: false }) + ); }); }); @@ -988,7 +1069,10 @@ describe('generic api functions', () => { result.current(true); }); - expect(pushSpy).toHaveBeenCalledWith('?restrict=true'); + expect(navigateSpy).toHaveBeenCalledWith( + '?restrict=true', + expect.any(Object) + ); }); }); }); diff --git a/packages/datagateway-common/src/api/index.tsx b/packages/datagateway-common/src/api/index.tsx index 9bc7ccf5b..588026ff2 100644 --- a/packages/datagateway-common/src/api/index.tsx +++ b/packages/datagateway-common/src/api/index.tsx @@ -9,7 +9,7 @@ import { isValid } from 'date-fns'; import format from 'date-fns/format'; import React from 'react'; import { useSelector } from 'react-redux'; -import { useHistory, useLocation } from 'react-router-dom'; +import { useLocation, useNavigate } from 'react-router'; import { AdditionalFilters, DOIViewType, @@ -198,19 +198,19 @@ export const usePushQueryParams = (): (( newQueryParams: Partial ) => void) => { const location = useLocation(); - const { push } = useHistory(); + const navigate = useNavigate(); return React.useCallback( (newQueryParams: Partial) => { const currentQueryParams = parseSearchToQuery(location.search); - push({ + navigate({ search: `?${parseQueryToSearch({ ...currentQueryParams, ...newQueryParams, }).toString()}`, }); }, - [location.search, push] + [location.search, navigate] ); }; @@ -321,7 +321,7 @@ export const useSort = (): (( updateMethod: UpdateMethod, shiftDown?: boolean ) => void) => { - const { push, replace } = useHistory(); + const navigate = useNavigate(); return React.useCallback( ( @@ -356,11 +356,14 @@ export const useSort = (): (( }, }; } - (updateMethod === 'push' ? push : replace)({ - search: `?${parseQueryToSearch(query).toString()}`, - }); + navigate( + { + search: `?${parseQueryToSearch(query).toString()}`, + }, + { replace: updateMethod === 'replace' } + ); }, - [push, replace] + [navigate] ); }; @@ -369,7 +372,7 @@ export const useSingleSort = (): (( order: Order | null, updateMethod: UpdateMethod ) => void) => { - const { push, replace } = useHistory(); + const navigate = useNavigate(); return React.useCallback( ( @@ -391,11 +394,14 @@ export const useSingleSort = (): (( }, }; } - (updateMethod === 'push' ? push : replace)({ - search: `?${parseQueryToSearch(query).toString()}`, - }); + navigate( + { + search: `?${parseQueryToSearch(query).toString()}`, + }, + { replace: updateMethod === 'replace' } + ); }, - [push, replace] + [navigate] ); }; @@ -403,7 +409,7 @@ const useFilter = ( updateMethod: UpdateMethod, filterPrefix?: string ): ((filterKey: string, filter: Filter | null) => void) => { - const { push, replace } = useHistory(); + const navigate = useNavigate(); return React.useCallback( (filterKey: string, filter: Filter | null) => { @@ -430,11 +436,14 @@ const useFilter = ( }, }; } - (updateMethod === 'push' ? push : replace)({ - search: `?${parseQueryToSearch(query).toString()}`, - }); + navigate( + { + search: `?${parseQueryToSearch(query).toString()}`, + }, + { replace: updateMethod === 'replace' } + ); }, - [filterPrefix, push, replace, updateMethod] + [filterPrefix, navigate, updateMethod] ); }; @@ -468,7 +477,7 @@ export const usePushDatafileFilter = (): (( export const usePushFilters = (): (( filters: { filterKey: string; filter: Filter | null }[] ) => void) => { - const { push } = useHistory(); + const navigate = useNavigate(); return React.useCallback( (filters: { filterKey: string; filter: Filter | null }[]) => { let query = parseSearchToQuery(window.location.search); @@ -494,9 +503,9 @@ export const usePushFilters = (): (( }; } }); - push({ search: `?${parseQueryToSearch(query).toString()}` }); + navigate({ search: `?${parseQueryToSearch(query).toString()}` }); }, - [push] + [navigate] ); }; @@ -504,8 +513,7 @@ export const useUpdateQueryParam = ( type: 'filters' | 'sort' | 'page' | 'results', updateMethod: 'push' | 'replace' ): ((param: FiltersType | SortType | number | null) => void) => { - const { push, replace } = useHistory(); - const functionToUse = updateMethod === 'push' ? push : replace; + const navigate = useNavigate(); return React.useCallback( (param: FiltersType | SortType | number | null) => { // need to use window.location.search and not useLocation to ensure we have the most @@ -522,14 +530,17 @@ export const useUpdateQueryParam = ( query.results = param as number | null; } - functionToUse({ search: `?${parseQueryToSearch(query).toString()}` }); + navigate( + { search: `?${parseQueryToSearch(query).toString()}` }, + { replace: updateMethod === 'replace' } + ); }, - [type, functionToUse] + [type, navigate, updateMethod] ); }; export const usePushPage = (): ((page: number) => void) => { - const { push } = useHistory(); + const navigate = useNavigate(); return React.useCallback( (page: number) => { @@ -537,14 +548,14 @@ export const usePushPage = (): ((page: number) => void) => { ...parseSearchToQuery(window.location.search), page, }; - push(`?${parseQueryToSearch(query).toString()}`); + navigate(`?${parseQueryToSearch(query).toString()}`); }, - [push] + [navigate] ); }; export const usePushResults = (): ((results: number) => void) => { - const { push } = useHistory(); + const navigate = useNavigate(); return React.useCallback( (results: number) => { @@ -552,17 +563,16 @@ export const usePushResults = (): ((results: number) => void) => { ...parseSearchToQuery(window.location.search), results, }; - push(`?${parseQueryToSearch(query).toString()}`); + navigate(`?${parseQueryToSearch(query).toString()}`); }, - [push] + [navigate] ); }; export const useUpdateView = ( updateMethod: UpdateMethod ): ((view: ViewsType) => void) => { - const { push, replace } = useHistory(); - const functionToUse = updateMethod === 'push' ? push : replace; + const navigate = useNavigate(); return React.useCallback( (view: ViewsType) => { @@ -570,15 +580,17 @@ export const useUpdateView = ( ...parseSearchToQuery(window.location.search), view, }; - functionToUse(`?${parseQueryToSearch(query).toString()}`); + navigate(`?${parseQueryToSearch(query).toString()}`, { + replace: updateMethod === 'replace', + }); }, - [functionToUse] + [navigate, updateMethod] ); }; export const usePushSearchText = (): ((searchText: string) => void) => { const location = useLocation(); - const { push } = useHistory(); + const navigate = useNavigate(); return React.useCallback( (searchText: string) => { @@ -586,11 +598,11 @@ export const usePushSearchText = (): ((searchText: string) => void) => { ...parseSearchToQuery(location.search), searchText, }; - push({ + navigate({ search: `?${parseQueryToSearch(query).toString()}`, }); }, - [location.search, push] + [location.search, navigate] ); }; @@ -600,7 +612,7 @@ export const usePushSearchToggles = (): (( investigation: boolean ) => void) => { const location = useLocation(); - const { push } = useHistory(); + const navigate = useNavigate(); return React.useCallback( (dataset: boolean, datafile: boolean, investigation: boolean) => { @@ -610,16 +622,16 @@ export const usePushSearchToggles = (): (( datafile, investigation, }; - push(`?${parseQueryToSearch(query).toString()}`); + navigate(`?${parseQueryToSearch(query).toString()}`); }, - [location.search, push] + [location.search, navigate] ); }; export const usePushSearchStartDate = (): (( startDate: Date | null ) => void) => { - const { push } = useHistory(); + const navigate = useNavigate(); return React.useCallback( (startDate: Date | null) => { @@ -629,21 +641,21 @@ export const usePushSearchStartDate = (): (( ...parseSearchToQuery(window.location.search), startDate, }; - push(`?${parseQueryToSearch(query).toString()}`); + navigate(`?${parseQueryToSearch(query).toString()}`); } else { const searchParams = parseQueryToSearch( parseSearchToQuery(window.location.search) ); searchParams.delete('startDate'); - push(`?${searchParams.toString()}`); + navigate(`?${searchParams.toString()}`); } }, - [push] + [navigate] ); }; export const usePushSearchEndDate = (): ((endDate: Date | null) => void) => { - const { push } = useHistory(); + const navigate = useNavigate(); return React.useCallback( (endDate: Date | null) => { @@ -653,22 +665,22 @@ export const usePushSearchEndDate = (): ((endDate: Date | null) => void) => { ...parseSearchToQuery(window.location.search), endDate, }; - push(`?${parseQueryToSearch(query).toString()}`); + navigate(`?${parseQueryToSearch(query).toString()}`); } else { const searchParams = parseQueryToSearch( parseSearchToQuery(window.location.search) ); searchParams.delete('endDate'); - push(`?${searchParams.toString()}`); + navigate(`?${searchParams.toString()}`); } }, - [push] + [navigate] ); }; export const usePushSearchRestrict = (): ((restrict: boolean) => void) => { const location = useLocation(); - const { push } = useHistory(); + const navigate = useNavigate(); return React.useCallback( (restrict: boolean) => { @@ -676,9 +688,9 @@ export const usePushSearchRestrict = (): ((restrict: boolean) => void) => { ...parseSearchToQuery(location.search), restrict, }; - push(`?${parseQueryToSearch(query).toString()}`); + navigate(`?${parseQueryToSearch(query).toString()}`); }, - [location.search, push] + [location.search, navigate] ); }; diff --git a/packages/datagateway-common/src/api/instruments.test.tsx b/packages/datagateway-common/src/api/instruments.test.tsx index 49becfaaf..8f0cca393 100644 --- a/packages/datagateway-common/src/api/instruments.test.tsx +++ b/packages/datagateway-common/src/api/instruments.test.tsx @@ -1,6 +1,7 @@ -import { act, renderHook, waitFor } from '@testing-library/react'; +import { renderHook, screen, waitFor } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; import axios from 'axios'; -import { History, createMemoryHistory } from 'history'; +import { Link } from 'react-router'; import { Instrument } from '../app.types'; import * as handleICATError from '../handleICATError'; import { createReactQueryWrapper } from '../setupTests'; @@ -13,13 +14,14 @@ import { describe('instrument api functions', () => { let mockData: Instrument[] = []; - let history: History; let params: URLSearchParams; + let user: ReturnType; const handleICATErrorSpy = vi .spyOn(handleICATError, 'default') .mockImplementation(vi.fn()); beforeEach(() => { + user = userEvent.setup(); mockData = [ { id: 1, @@ -30,11 +32,7 @@ describe('instrument api functions', () => { name: 'Test 2', }, ]; - history = createMemoryHistory({ - initialEntries: [ - '/?sort={"name":"asc","title":"desc"}&filters={"name":{"value":"test","type":"include"}}&page=2&results=20', - ], - }); + window.history.replaceState({}, '', '/'); params = new URLSearchParams(); }); @@ -48,9 +46,26 @@ describe('instrument api functions', () => { vi.mocked(axios.get).mockResolvedValue({ data: mockData, }); + window.history.replaceState( + {}, + '', + '/?sort={"name":"asc","title":"desc"}&filters={"name":{"value":"test","type":"include"}}&page=2&results=20' + ); const { result } = renderHook(() => useInstrumentsPaginated(), { - wrapper: createReactQueryWrapper(history), + wrapper: ({ children }) => { + const Wrapper = createReactQueryWrapper(); + return ( + + <> + {children} + + Test link + + + + ); + }, }); await waitFor(() => expect(result.current.isSuccess).toBe(true)); @@ -78,12 +93,8 @@ describe('instrument api functions', () => { ); expect(result.current.data).toEqual(mockData); - act(() => { - // test that order of sort object triggers new query - history.push( - '/?sort={"title":"desc", "name":"asc"}&filters={"name":{"value":"test","type":"include"}}&page=2&results=20' - ); - }); + // test that order of sort object triggers new query + await user.click(screen.getByRole('link')); await waitFor(() => expect(result.current.isSuccess).toBe(true)); @@ -139,9 +150,26 @@ describe('instrument api functions', () => { ? Promise.resolve({ data: mockData[0] }) : Promise.resolve({ data: mockData[1] }) ); + window.history.replaceState( + {}, + '', + '/?sort={"name":"asc","title":"desc"}&filters={"name":{"value":"test","type":"include"}}&page=2&results=20' + ); const { result } = renderHook(() => useInstrumentsInfinite(), { - wrapper: createReactQueryWrapper(history), + wrapper: ({ children }) => { + const Wrapper = createReactQueryWrapper(); + return ( + + <> + {children} + + Test link + + + + ); + }, }); await waitFor(() => expect(result.current.isSuccess).toBe(true)); @@ -191,12 +219,8 @@ describe('instrument api functions', () => { mockData[1], ]); - act(() => { - // test that order of sort object triggers new query - history.push( - '/?sort={"title":"desc", "name":"asc"}&filters={"name":{"value":"test","type":"include"}}' - ); - }); + // test that order of sort object triggers new query + await user.click(screen.getByRole('link')); await waitFor(() => expect(result.current.isSuccess).toBe(true)); @@ -250,9 +274,14 @@ describe('instrument api functions', () => { vi.mocked(axios.get).mockResolvedValue({ data: mockData.length, }); + window.history.replaceState( + {}, + '', + '/?sort={"name":"asc","title":"desc"}&filters={"name":{"value":"test","type":"include"}}&page=2&results=20' + ); const { result } = renderHook(() => useInstrumentCount(), { - wrapper: createReactQueryWrapper(history), + wrapper: createReactQueryWrapper(), }); await waitFor(() => expect(result.current.isSuccess).toBe(true)); diff --git a/packages/datagateway-common/src/api/instruments.tsx b/packages/datagateway-common/src/api/instruments.tsx index c8e1695ed..beb27b7df 100644 --- a/packages/datagateway-common/src/api/instruments.tsx +++ b/packages/datagateway-common/src/api/instruments.tsx @@ -1,7 +1,7 @@ import { useInfiniteQuery, useQuery } from '@tanstack/react-query'; import axios from 'axios'; import { useSelector } from 'react-redux'; -import { useLocation } from 'react-router-dom'; +import { useLocation } from 'react-router'; import { getApiParams, parseSearchToQuery } from '.'; import { AdditionalFilters, diff --git a/packages/datagateway-common/src/api/investigations.test.tsx b/packages/datagateway-common/src/api/investigations.test.tsx index 9fc57ee7c..20d2e84d6 100644 --- a/packages/datagateway-common/src/api/investigations.test.tsx +++ b/packages/datagateway-common/src/api/investigations.test.tsx @@ -1,6 +1,7 @@ -import { act, renderHook, waitFor } from '@testing-library/react'; +import { renderHook, screen, waitFor } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; import axios from 'axios'; -import { History, createMemoryHistory } from 'history'; +import { Link } from 'react-router'; import { Investigation } from '../app.types'; import * as handleICATError from '../handleICATError'; import { createReactQueryWrapper } from '../setupTests'; @@ -14,13 +15,14 @@ import { describe('investigation api functions', () => { let mockData: Investigation[] = []; - let history: History; let params: URLSearchParams; + let user: ReturnType; const handleICATErrorSpy = vi .spyOn(handleICATError, 'default') .mockImplementation(vi.fn()); beforeEach(() => { + user = userEvent.setup(); mockData = [ { id: 1, @@ -47,11 +49,7 @@ describe('investigation api functions', () => { endDate: '2021-08-13', }, ]; - history = createMemoryHistory({ - initialEntries: [ - '/?sort={"name":"asc","title":"desc"}&filters={"name":{"value":"test","type":"include"}}&page=2&results=20', - ], - }); + window.history.replaceState({}, '', '/'); params = new URLSearchParams(); }); @@ -66,6 +64,11 @@ describe('investigation api functions', () => { vi.mocked(axios.get).mockResolvedValue({ data: mockData, }); + window.history.replaceState( + {}, + '', + '/?sort={"name":"asc","title":"desc"}&filters={"name":{"value":"test","type":"include"}}&page=2&results=20' + ); const { result } = renderHook( () => @@ -78,7 +81,19 @@ describe('investigation api functions', () => { }, ]), { - wrapper: createReactQueryWrapper(history), + wrapper: ({ children }) => { + const Wrapper = createReactQueryWrapper(); + return ( + + <> + {children} + + Test link + + + + ); + }, } ); @@ -113,12 +128,8 @@ describe('investigation api functions', () => { ); expect(result.current.data).toEqual(mockData); - act(() => { - // test that order of sort object triggers new query - history.push( - '/?sort={"title":"desc", "name":"asc"}&filters={"name":{"value":"test","type":"include"}}&page=2&results=20' - ); - }); + // test that order of sort object triggers new query + await user.click(screen.getByRole('link')); await waitFor(() => expect(result.current.isSuccess).toBe(true)); @@ -129,6 +140,11 @@ describe('investigation api functions', () => { vi.mocked(axios.get).mockResolvedValue({ data: mockData, }); + window.history.replaceState( + {}, + '', + '/?sort={"name":"asc","title":"desc"}&filters={"name":{"value":"test","type":"include"}}&page=2&results=20' + ); const { result } = renderHook( () => @@ -144,7 +160,7 @@ describe('investigation api functions', () => { true ), { - wrapper: createReactQueryWrapper(history), + wrapper: createReactQueryWrapper(), } ); @@ -218,6 +234,11 @@ describe('investigation api functions', () => { ? Promise.resolve({ data: mockData[0] }) : Promise.resolve({ data: mockData[1] }) ); + window.history.replaceState( + {}, + '', + '/?sort={"name":"asc","title":"desc"}&filters={"name":{"value":"test","type":"include"}}&page=2&results=20' + ); const { result } = renderHook( () => @@ -230,7 +251,19 @@ describe('investigation api functions', () => { }, ]), { - wrapper: createReactQueryWrapper(history), + wrapper: ({ children }) => { + const Wrapper = createReactQueryWrapper(); + return ( + + <> + {children} + + Test link + + + + ); + }, } ); @@ -287,12 +320,8 @@ describe('investigation api functions', () => { mockData[1], ]); - act(() => { - // test that order of sort object triggers new query - history.push( - '/?sort={"title":"desc", "name":"asc"}&filters={"name":{"value":"test","type":"include"}}' - ); - }); + // test that order of sort object triggers new query + await user.click(screen.getByRole('link')); await waitFor(() => expect(result.current.isSuccess).toBe(true)); @@ -305,6 +334,11 @@ describe('investigation api functions', () => { ? Promise.resolve({ data: mockData[0] }) : Promise.resolve({ data: mockData[1] }) ); + window.history.replaceState( + {}, + '', + '/?sort={"name":"asc","title":"desc"}&filters={"name":{"value":"test","type":"include"}}&page=2&results=20' + ); const { result } = renderHook( () => @@ -320,7 +354,7 @@ describe('investigation api functions', () => { true ), { - wrapper: createReactQueryWrapper(history), + wrapper: createReactQueryWrapper(), } ); @@ -414,6 +448,11 @@ describe('investigation api functions', () => { vi.mocked(axios.get).mockResolvedValue({ data: mockData.length, }); + window.history.replaceState( + {}, + '', + '/?sort={"name":"asc","title":"desc"}&filters={"name":{"value":"test","type":"include"}}&page=2&results=20' + ); const { result } = renderHook( () => @@ -424,7 +463,7 @@ describe('investigation api functions', () => { }, ]), { - wrapper: createReactQueryWrapper(history), + wrapper: createReactQueryWrapper(), } ); diff --git a/packages/datagateway-common/src/api/investigations.tsx b/packages/datagateway-common/src/api/investigations.tsx index a7481680c..19ebdeebd 100644 --- a/packages/datagateway-common/src/api/investigations.tsx +++ b/packages/datagateway-common/src/api/investigations.tsx @@ -5,7 +5,7 @@ import { } from '@tanstack/react-query'; import axios, { AxiosError } from 'axios'; import { useSelector } from 'react-redux'; -import { useLocation } from 'react-router-dom'; +import { useLocation } from 'react-router'; import { getApiParams, parseSearchToQuery } from '.'; import type { AdditionalFilters, diff --git a/packages/datagateway-common/src/api/retryICATErrors.test.ts b/packages/datagateway-common/src/api/retryICATErrors.test.ts index f430de580..abc3adb4d 100644 --- a/packages/datagateway-common/src/api/retryICATErrors.test.ts +++ b/packages/datagateway-common/src/api/retryICATErrors.test.ts @@ -88,7 +88,7 @@ describe('retryICATErrors', () => { const { result: { current: retryICATErrors }, } = renderHook(() => useRetryICATErrors(), { - wrapper: createReactQueryWrapper(undefined, testQueryClient), + wrapper: createReactQueryWrapper(testQueryClient), }); let result = retryICATErrors(0, error); @@ -107,7 +107,7 @@ describe('retryICATErrors', () => { const { result: { current: retryICATErrors }, } = renderHook(() => useRetryICATErrors(), { - wrapper: createReactQueryWrapper(undefined, testQueryClient), + wrapper: createReactQueryWrapper(testQueryClient), }); let result = retryICATErrors(2, error); diff --git a/packages/datagateway-common/src/dois/DOIConfirmDialog.component.test.tsx b/packages/datagateway-common/src/dois/DOIConfirmDialog.component.test.tsx index 26f9f7987..b67b97424 100644 --- a/packages/datagateway-common/src/dois/DOIConfirmDialog.component.test.tsx +++ b/packages/datagateway-common/src/dois/DOIConfirmDialog.component.test.tsx @@ -1,25 +1,19 @@ import { render, RenderResult, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; -import { createMemoryHistory, MemoryHistory } from 'history'; import * as React from 'react'; -import { Router } from 'react-router-dom'; +import { BrowserRouter } from 'react-router'; import DOIConfirmDialog from './DOIConfirmDialog.component'; describe('DOI Confirm Dialog component', () => { let user: ReturnType; let props: React.ComponentProps; - const renderComponent = (): RenderResult & { history: MemoryHistory } => { - const history = createMemoryHistory(); - return { - history, - ...render( - - - - ), - }; - }; + const renderComponent = (): RenderResult => + render( + + + + ); beforeEach(() => { user = userEvent.setup(); @@ -66,7 +60,7 @@ describe('DOI Confirm Dialog component', () => { attributes: { doi: 'test_doiv1' }, }, }; - const { history } = renderComponent(); + renderComponent(); expect( screen.getByText('DOIConfirmDialog.mint_success') @@ -79,8 +73,8 @@ describe('DOI Confirm Dialog component', () => { name: 'DOIConfirmDialog.view_data_publication', }) ); - expect(history.location).toMatchObject({ - pathname: `/browse/dataPublication/${props.data.version.data_publication_id}`, + expect(window.location).toMatchObject({ + pathname: `/browse/dataPublication/${props.data?.version.data_publication_id}`, }); }); diff --git a/packages/datagateway-common/src/dois/DOIConfirmDialog.component.tsx b/packages/datagateway-common/src/dois/DOIConfirmDialog.component.tsx index 137cc6f66..57bb322aa 100644 --- a/packages/datagateway-common/src/dois/DOIConfirmDialog.component.tsx +++ b/packages/datagateway-common/src/dois/DOIConfirmDialog.component.tsx @@ -9,7 +9,7 @@ import { MutationStatus } from '@tanstack/react-query'; import { AxiosError } from 'axios'; import React from 'react'; import { useTranslation } from 'react-i18next'; -import { Link } from 'react-router-dom'; +import { Link } from 'react-router'; import { DOIResponse } from '../app.types'; import DialogContent from '../dialogContent.component'; diff --git a/packages/datagateway-common/src/homePage/__snapshots__/homePage.component.test.tsx.snap b/packages/datagateway-common/src/homePage/__snapshots__/homePage.component.test.tsx.snap index 054ec2d83..8f69f7254 100644 --- a/packages/datagateway-common/src/homePage/__snapshots__/homePage.component.test.tsx.snap +++ b/packages/datagateway-common/src/homePage/__snapshots__/homePage.component.test.tsx.snap @@ -78,6 +78,7 @@ exports[`Home page component > homepage renders correctly 1`] = ` > homepage renders correctly 1`] = ` > homepage renders correctly 1`] = ` > { diff --git a/packages/datagateway-common/src/homePage/homePage.component.tsx b/packages/datagateway-common/src/homePage/homePage.component.tsx index a529c7a1e..35733507c 100644 --- a/packages/datagateway-common/src/homePage/homePage.component.tsx +++ b/packages/datagateway-common/src/homePage/homePage.component.tsx @@ -4,7 +4,7 @@ import { Avatar, Box, Button, Grid, Paper, alpha, styled } from '@mui/material'; import Typography from '@mui/material/Typography'; import React from 'react'; import { Trans, useTranslation } from 'react-i18next'; -import { Link } from 'react-router-dom'; +import { Link } from 'react-router'; export interface HomePageProps { logo: string; @@ -99,9 +99,9 @@ const BrowseDecal = styled('div', { theme.palette.mode === 'light' ? `url(${decal2Image})` : // eslint-disable-next-line @typescript-eslint/no-explicit-any - (theme as any).colours?.type === 'default' - ? `url(${decal2DarkImage})` - : `url(${decal2DarkHCImage})`, + (theme as any).colours?.type === 'default' + ? `url(${decal2DarkImage})` + : `url(${decal2DarkHCImage})`, backgroundRepeat: 'no-repeat', backgroundPosition: 'top left', backgroundSize: 'auto 100%', diff --git a/packages/datagateway-common/src/main.tsx b/packages/datagateway-common/src/main.tsx index afebb42a2..7fe8a5b5c 100644 --- a/packages/datagateway-common/src/main.tsx +++ b/packages/datagateway-common/src/main.tsx @@ -33,10 +33,10 @@ export { export { default as AdvancedFilter } from './card/advancedFilter.component'; export { default as CardView } from './card/cardView.component'; export type { - CardViewDetails, CVCustomFilters, CVFilterInfo, CVSelectedFilter, + CardViewDetails, } from './card/cardView.component'; export * from './api/index'; @@ -91,15 +91,16 @@ export { default as ISISDatasetDetailsPanel } from './detailsPanels/isis/dataset export { default as ISISInstrumentDetailsPanel } from './detailsPanels/isis/instrumentDetailsPanel.component'; export { default as ISISInvestigationDetailsPanel } from './detailsPanels/isis/investigationDetailsPanel.component'; -export type { ContributorUser } from './dois/creatorsAndContributors.component'; export { default as DOIConfirmDialog } from './dois/DOIConfirmDialog.component'; export { default as DOIMetadataConfirmation } from './dois/DOIMetadataConfirmation.component'; export { default as DOIMetadataForm } from './dois/DOIMetadataForm.component'; +export type { ContributorUser } from './dois/creatorsAndContributors.component'; export { default as DialogContent } from './dialogContent.component'; export { default as DialogTitle } from './dialogTitle.component'; export * from './urlBuilders'; +export * from './utils'; // const root = ReactDOM.createRoot(document.getElementById('root')); // root.render(); diff --git a/packages/datagateway-common/src/setupTests.tsx b/packages/datagateway-common/src/setupTests.tsx index 97805a97e..f5dc3e33b 100644 --- a/packages/datagateway-common/src/setupTests.tsx +++ b/packages/datagateway-common/src/setupTests.tsx @@ -4,10 +4,9 @@ import { QueryClientProvider, } from '@tanstack/react-query'; import '@testing-library/jest-dom'; -import { History, createMemoryHistory } from 'history'; import React from 'react'; import { Provider } from 'react-redux'; -import { Router } from 'react-router-dom'; +import { BrowserRouter } from 'react-router'; import type { Action } from 'redux'; import configureStore from 'redux-mock-store'; import thunk from 'redux-thunk'; @@ -25,20 +24,6 @@ vi.setConfig({ testTimeout: 20_000 }); // and https://github.com/testing-library/user-event/issues/1115 vi.stubGlobal('jest', { advanceTimersByTime: vi.advanceTimersByTime.bind(vi) }); -if (typeof window.URL.createObjectURL === 'undefined') { - // required as work-around for jsdom environment not implementing window.URL.createObjectURL method - Object.defineProperty(window.URL, 'createObjectURL', { - value: () => 'testObjectUrl', - }); -} - -if (typeof window.URL.revokeObjectURL === 'undefined') { - // required as work-around for jsdom environment not implementing window.URL.createObjectURL method - Object.defineProperty(window.URL, 'revokeObjectURL', { - value: () => {}, - }); -} - // Add in ResizeObserver as it's not in jsdom's environment vi.stubGlobal( 'ResizeObserver', @@ -99,7 +84,6 @@ export const createTestQueryClient = (): QueryClient => }); export const createReactQueryWrapper = ( - history: History = createMemoryHistory(), queryClient: QueryClient = createTestQueryClient() ): React.JSXElementConstructor<{ children: React.ReactNode }> => { const state = { @@ -123,11 +107,11 @@ export const createReactQueryWrapper = ( children: React.ReactNode; }> = ({ children }) => ( - + {children} - + ); return wrapper; diff --git a/packages/datagateway-common/src/table/cellRenderers/cellContentRenderers.test.tsx b/packages/datagateway-common/src/table/cellRenderers/cellContentRenderers.test.tsx index 82d73aa03..846a8ad13 100644 --- a/packages/datagateway-common/src/table/cellRenderers/cellContentRenderers.test.tsx +++ b/packages/datagateway-common/src/table/cellRenderers/cellContentRenderers.test.tsx @@ -1,5 +1,6 @@ import { render, screen } from '@testing-library/react'; -import { MemoryRouter } from 'react-router-dom'; +import userEvent from '@testing-library/user-event'; +import { MemoryRouter, useLocation } from 'react-router'; import { datasetLink, formatBytes, @@ -92,5 +93,26 @@ describe('Cell content renderers', () => { '/test/url?test=true&view=card' ); }); + + it('renders correctly with location state', async () => { + const StateTestComponent = () => { + const { state } = useLocation(); + return
{JSON.stringify(state)}
; + }; + const user = userEvent.setup(); + + render( + + {tableLink('/test/url', 'test text', undefined, undefined, { + test: true, + })} + + + ); + await user.click(screen.getByRole('link', { name: 'test text' })); + expect(screen.getByTestId('state-test')).toHaveTextContent( + '{"test":true}' + ); + }); }); }); diff --git a/packages/datagateway-common/src/table/cellRenderers/cellContentRenderers.tsx b/packages/datagateway-common/src/table/cellRenderers/cellContentRenderers.tsx index 632d2fb77..638766791 100644 --- a/packages/datagateway-common/src/table/cellRenderers/cellContentRenderers.tsx +++ b/packages/datagateway-common/src/table/cellRenderers/cellContentRenderers.tsx @@ -1,6 +1,6 @@ import { Link } from '@mui/material'; import React from 'react'; -import { Link as RouterLink } from 'react-router-dom'; +import { Link as RouterLink } from 'react-router'; import type { ViewsType } from '../../app.types'; export function formatBytes(bytes: number | undefined): string { @@ -69,12 +69,14 @@ export function tableLink( linkLocation: React.ComponentProps['to'], linkText: string, view?: ViewsType, - testid?: string + testid?: string, + linkState?: React.ComponentProps['state'] ): React.ReactElement { return ( {linkText} diff --git a/packages/datagateway-common/src/utils.ts b/packages/datagateway-common/src/utils.ts new file mode 100644 index 000000000..e86089a7c --- /dev/null +++ b/packages/datagateway-common/src/utils.ts @@ -0,0 +1,7 @@ +/** + * Appends an asterisk to the provided route to make it so react-router matches the route and any sub-matches + * @param route The route to convert + * @returns The route with an asterisk appended to use with react-router as a non-exact route + */ +export const makeRouteNonExact = (route: string): string => + route.endsWith('/') ? `${route}*` : `${route}/*`; diff --git a/packages/datagateway-common/src/views/addToCartButton.component.test.tsx b/packages/datagateway-common/src/views/addToCartButton.component.test.tsx index 2a9498891..ce3992dd7 100644 --- a/packages/datagateway-common/src/views/addToCartButton.component.test.tsx +++ b/packages/datagateway-common/src/views/addToCartButton.component.test.tsx @@ -8,7 +8,7 @@ import { import userEvent from '@testing-library/user-event'; import axios, { AxiosResponse } from 'axios'; import { Provider } from 'react-redux'; -import { MemoryRouter } from 'react-router-dom'; +import { MemoryRouter } from 'react-router'; import configureStore from 'redux-mock-store'; import thunk from 'redux-thunk'; import { DownloadCartItem } from '../app.types'; diff --git a/packages/datagateway-common/src/views/clearFiltersButton.component.test.tsx b/packages/datagateway-common/src/views/clearFiltersButton.component.test.tsx index 5eb7168c0..1500b72b0 100644 --- a/packages/datagateway-common/src/views/clearFiltersButton.component.test.tsx +++ b/packages/datagateway-common/src/views/clearFiltersButton.component.test.tsx @@ -2,7 +2,7 @@ import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { render, screen, type RenderResult } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { Provider } from 'react-redux'; -import { MemoryRouter } from 'react-router-dom'; +import { MemoryRouter } from 'react-router'; import configureStore from 'redux-mock-store'; import thunk from 'redux-thunk'; import { StateType } from '../state/app.types'; diff --git a/packages/datagateway-common/src/views/downloadButton.component.test.tsx b/packages/datagateway-common/src/views/downloadButton.component.test.tsx index 780983612..77f3efd76 100644 --- a/packages/datagateway-common/src/views/downloadButton.component.test.tsx +++ b/packages/datagateway-common/src/views/downloadButton.component.test.tsx @@ -16,7 +16,7 @@ import { } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { Provider } from 'react-redux'; -import { MemoryRouter } from 'react-router-dom'; +import { MemoryRouter } from 'react-router'; import thunk from 'redux-thunk'; import * as parseTokens from '../parseTokens'; import { StateType } from '../state/app.types'; diff --git a/packages/datagateway-common/src/views/viewButton.component.test.tsx b/packages/datagateway-common/src/views/viewButton.component.test.tsx index b342b33d3..cf3e3fda3 100644 --- a/packages/datagateway-common/src/views/viewButton.component.test.tsx +++ b/packages/datagateway-common/src/views/viewButton.component.test.tsx @@ -3,7 +3,7 @@ import { render, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import * as React from 'react'; import { Provider } from 'react-redux'; -import { MemoryRouter } from 'react-router-dom'; +import { MemoryRouter } from 'react-router'; import configureStore from 'redux-mock-store'; import thunk from 'redux-thunk'; import type { StateType } from '../state/app.types'; @@ -13,11 +13,7 @@ import ViewButton from './viewButton.component'; describe('Generic view button', () => { let state: StateType; - function Wrapper({ - children, - }: { - children: React.ReactElement; - }): JSX.Element { + function Wrapper({ children }: { children: React.ReactNode }): JSX.Element { return ( { fixture: 'investigation.json', }); cy.login(); - cy.visit( - '/browse/instrument/1/facilityCycle/19/investigation/19/dataset/79/datafile/3484', - { - onBeforeLoad(win: Cypress.AUTWindow) { - cy.spy(win.navigator.clipboard, 'writeText').as('copy'); - }, - } - ); }); - it('should show test file content and details', () => { - // should be able to see the content of the text file - cy.get('[aria-label="Text content of Datafile 3484.txt"').should('exist'); - cy.contains('First line').should('exist'); - cy.contains('Second line').should('exist'); - cy.contains('Third line').should('exist'); - - // show details switch should be on - // HTML structure of the switch is something like this: - // - // - cy.get('label') - .contains('Show details') - .prev() - .find('input[type="checkbox"]') - .should('be.checked'); - - // should be able to see details of the datafile - cy.contains('Datafile 3484.txt').should('exist'); - cy.contains( - 'Effort plan social history carry this summer stuff. Fear source yard small. ' + - 'Together discover new account parent. Learn wall industry red suffer. Black concern building behavior able long.' - ).should('exist'); - cy.contains('159.93 MB').should('exist'); - cy.contains('/wish/although/eat.txt').should('exist'); - cy.contains('2019-09-05 14:03:35+01:00').should('exist'); - cy.contains('2011-03-25 13:11:50+00:00').should('exist'); - - // should be at 100% zoom - cy.contains('100%').should('exist'); - cy.get('[aria-label="Text content of Datafile 3484.txt"').should( - 'have.css', - 'font-size', - '12px' - ); - }); + describe('Facility cycle hierarchy', () => { + beforeEach(() => { + cy.visit( + '/browse/instrument/1/facilityCycle/19/investigation/19/dataset/79/datafile/3484', + { + onBeforeLoad(win: Cypress.AUTWindow) { + cy.spy(win.navigator.clipboard, 'writeText').as('copy'); + }, + } + ); + }); - it('should have a download button that downloads the current datafile', () => { - // downloads performed in Cypress are saved in cypress/downloads - // this retrieves the path to that folder - const downloadFolder = Cypress.config('downloadsFolder'); + it('should show test file content and details', () => { + // should be able to see the content of the text file + cy.get('[aria-label="Text content of Datafile 3484.txt"').should('exist'); + cy.contains('First line').should('exist'); + cy.contains('Second line').should('exist'); + cy.contains('Third line').should('exist'); + + // show details switch should be on + // HTML structure of the switch is something like this: + // + // + cy.get('label') + .contains('Show details') + .prev() + .find('input[type="checkbox"]') + .should('be.checked'); + + // should be able to see details of the datafile + cy.contains('Datafile 3484.txt').should('exist'); + cy.contains( + 'Effort plan social history carry this summer stuff. Fear source yard small. ' + + 'Together discover new account parent. Learn wall industry red suffer. Black concern building behavior able long.' + ).should('exist'); + cy.contains('159.93 MB').should('exist'); + cy.contains('/wish/although/eat.txt').should('exist'); + cy.contains('2019-09-05 14:03:35+01:00').should('exist'); + cy.contains('2011-03-25 13:11:50+00:00').should('exist'); + + // should be at 100% zoom + cy.contains('100%').should('exist'); + cy.get('[aria-label="Text content of Datafile 3484.txt"').should( + 'have.css', + 'font-size', + '12px' + ); + }); - cy.contains('Download').click(); - cy.readFile(join(downloadFolder, '_wish_although_eat.txt')); - }); + it('should have a download button that downloads the current datafile', () => { + // downloads performed in Cypress are saved in cypress/downloads + // this retrieves the path to that folder + const downloadFolder = Cypress.config('downloadsFolder'); - it('should have a copy link button that copies the link to the current datafile to the clipboard', () => { - cy.contains('Copy link').click(); - cy.get('@copy').should( - 'be.calledOnceWithExactly', - 'http://127.0.0.1:3000/browse/instrument/1/facilityCycle/19/investigation/19/dataset/79/datafile/3484' - ); - // should show a successful after copy is successful - cy.contains('Link copied to clipboard').should('exist'); - // the message should disappear after a set duration - cy.contains('Link copied to clipboard').should('not.exist'); - }); + cy.contains('Download').click(); + cy.readFile(join(downloadFolder, '_wish_although_eat.txt')); + }); + + it('should have a copy link button that copies the link to the current datafile to the clipboard', () => { + cy.contains('Copy link').click(); + cy.get('@copy').should( + 'be.calledOnceWithExactly', + 'http://127.0.0.1:3000/browse/instrument/1/facilityCycle/19/investigation/19/dataset/79/datafile/3484' + ); + // should show a successful after copy is successful + cy.contains('Link copied to clipboard').should('exist'); + // the message should disappear after a set duration + cy.contains('Link copied to clipboard').should('not.exist'); + }); + + it('should have zoom controls that control the size of the preview content', () => { + // reset zoom button should not appear initially + cy.contains('Reset zoom').should('not.exist'); + + cy.contains('Zoom in').click(); + cy.get('[aria-label="Text content of Datafile 3484.txt"').should( + 'have.css', + 'font-size', + '13px' + ); + cy.contains('110%').should('exist'); + cy.contains('Zoom in').click(); + cy.get('[aria-label="Text content of Datafile 3484.txt"').should( + 'have.css', + 'font-size', + '14px' + ); + cy.contains('120%').should('exist'); + + cy.contains('Reset zoom').click(); + cy.get('[aria-label="Text content of Datafile 3484.txt"').should( + 'have.css', + 'font-size', + '12px' + ); + cy.contains('100%').should('exist'); + // reset zoom should not appear anymore because zoom is reset + cy.contains('Reset zoom').should('not.exist'); + + cy.contains('Zoom out').click(); + cy.get('[aria-label="Text content of Datafile 3484.txt"').should( + 'have.css', + 'font-size', + '11px' + ); + cy.contains('90%').should('exist'); + cy.contains('Zoom out').click(); + cy.get('[aria-label="Text content of Datafile 3484.txt"').should( + 'have.css', + 'font-size', + '10px' + ); + cy.contains('80%').should('exist'); + + cy.contains('Reset zoom').click(); + cy.get('[aria-label="Text content of Datafile 3484.txt"').should( + 'have.css', + 'font-size', + '12px' + ); + // reset zoom should not appear anymore because zoom is reset + cy.contains('Reset zoom').should('not.exist'); + cy.contains('100%').should('exist'); + }); - it('should have zoom controls that control the size of the preview content', () => { - // reset zoom button should not appear initially - cy.contains('Reset zoom').should('not.exist'); - - cy.contains('Zoom in').click(); - cy.get('[aria-label="Text content of Datafile 3484.txt"').should( - 'have.css', - 'font-size', - '13px' - ); - cy.contains('110%').should('exist'); - cy.contains('Zoom in').click(); - cy.get('[aria-label="Text content of Datafile 3484.txt"').should( - 'have.css', - 'font-size', - '14px' - ); - cy.contains('120%').should('exist'); - - cy.contains('Reset zoom').click(); - cy.get('[aria-label="Text content of Datafile 3484.txt"').should( - 'have.css', - 'font-size', - '12px' - ); - cy.contains('100%').should('exist'); - // reset zoom should not appear anymore because zoom is reset - cy.contains('Reset zoom').should('not.exist'); - - cy.contains('Zoom out').click(); - cy.get('[aria-label="Text content of Datafile 3484.txt"').should( - 'have.css', - 'font-size', - '11px' - ); - cy.contains('90%').should('exist'); - cy.contains('Zoom out').click(); - cy.get('[aria-label="Text content of Datafile 3484.txt"').should( - 'have.css', - 'font-size', - '10px' - ); - cy.contains('80%').should('exist'); - - cy.contains('Reset zoom').click(); - cy.get('[aria-label="Text content of Datafile 3484.txt"').should( - 'have.css', - 'font-size', - '12px' - ); - // reset zoom should not appear anymore because zoom is reset - cy.contains('Reset zoom').should('not.exist'); - cy.contains('100%').should('exist'); + it('should have a details pane toggle that toggles details pane', () => { + cy.get('label').contains('Show details').click(); + + cy.contains('Datafile 3484.txt').should('not.exist'); + cy.contains( + 'Effort plan social history carry this summer stuff. Fear source yard small. ' + + 'Together discover new account parent. Learn wall industry red suffer. Black concern building behavior able long.' + ).should('not.exist'); + cy.contains('159.93 MB').should('not.exist'); + cy.contains('/wish/although/eat.txt').should('not.exist'); + cy.contains('2019-09-05 14:03:35+01:00').should('not.exist'); + cy.contains('2011-03-25 13:11:50+00:00').should('not.exist'); + + // see above for the HTML structure of the checkbox + cy.get('label') + .contains('Show details') + .prev() + .find('input[type="checkbox"]') + .should('not.be.checked'); + + cy.get('label').contains('Show details').click(); + + cy.contains('Datafile 3484.txt').should('exist'); + cy.contains( + 'Effort plan social history carry this summer stuff. Fear source yard small. ' + + 'Together discover new account parent. Learn wall industry red suffer. Black concern building behavior able long.' + ).should('exist'); + cy.contains('159.93 MB').should('exist'); + cy.contains('/wish/although/eat.txt').should('exist'); + cy.contains('2019-09-05 14:03:35+01:00').should('exist'); + cy.contains('2011-03-25 13:11:50+00:00').should('exist'); + + // see above for the HTML structure of the checkbox + cy.get('label') + .contains('Show details') + .prev() + .find('input[type="checkbox"]') + .should('be.checked'); + }); }); - it('should have a details pane toggle that toggles details pane', () => { - cy.get('label').contains('Show details').click(); - - cy.contains('Datafile 3484.txt').should('not.exist'); - cy.contains( - 'Effort plan social history carry this summer stuff. Fear source yard small. ' + - 'Together discover new account parent. Learn wall industry red suffer. Black concern building behavior able long.' - ).should('not.exist'); - cy.contains('159.93 MB').should('not.exist'); - cy.contains('/wish/although/eat.txt').should('not.exist'); - cy.contains('2019-09-05 14:03:35+01:00').should('not.exist'); - cy.contains('2011-03-25 13:11:50+00:00').should('not.exist'); - - // see above for the HTML structure of the checkbox - cy.get('label') - .contains('Show details') - .prev() - .find('input[type="checkbox"]') - .should('not.be.checked'); - - cy.get('label').contains('Show details').click(); - - cy.contains('Datafile 3484.txt').should('exist'); - cy.contains( - 'Effort plan social history carry this summer stuff. Fear source yard small. ' + - 'Together discover new account parent. Learn wall industry red suffer. Black concern building behavior able long.' - ).should('exist'); - cy.contains('159.93 MB').should('exist'); - cy.contains('/wish/although/eat.txt').should('exist'); - cy.contains('2019-09-05 14:03:35+01:00').should('exist'); - cy.contains('2011-03-25 13:11:50+00:00').should('exist'); - - // see above for the HTML structure of the checkbox - cy.get('label') - .contains('Show details') - .prev() - .find('input[type="checkbox"]') - .should('be.checked'); + describe('Data publication hierarchy', () => { + beforeEach(() => { + cy.visit( + '/browseDataPublications/instrument/13/dataPublication/57/investigation/46/dataset/15/datafile/1085' + ); + }); + + it('should load datafile previewer in data publication hierarchy', () => { + // should be able to see the content of the text file + cy.contains('First line').should('exist'); + cy.contains('Second line').should('exist'); + cy.contains('Third line').should('exist'); + }); }); }); diff --git a/packages/datagateway-dataview/cypress/e2e/homePage.cy.ts b/packages/datagateway-dataview/cypress/e2e/homePage.cy.ts index f917a87ef..3f01116c7 100644 --- a/packages/datagateway-dataview/cypress/e2e/homePage.cy.ts +++ b/packages/datagateway-dataview/cypress/e2e/homePage.cy.ts @@ -7,16 +7,16 @@ describe('DataGateway HomePage', () => { //Headings cy.get('[data-testid="browse-button"]').click(); cy.url().should('include', '/browse/investigation'); - cy.go('back'); + cy.visit('/datagateway'); cy.get('[data-testid="search-button"]').click(); cy.url().should('include', '/search'); - cy.go('back'); + cy.visit('/datagateway'); cy.get('[data-testid="download-button"]').click(); cy.url().should('include', '/download'); - cy.go('back'); + cy.visit('/datagateway'); cy.origin('https://www.isis.stfc.ac.uk/about/', () => { cy.on('uncaught:exception', (_e) => { return false; @@ -24,6 +24,5 @@ describe('DataGateway HomePage', () => { }); cy.get('[data-testid="facility-button"]').click(); cy.url().should('equal', 'https://www.isis.stfc.ac.uk/about/'); - cy.go('back'); }); }); diff --git a/packages/datagateway-dataview/cypress/e2e/table/dls/allDois.cy.ts b/packages/datagateway-dataview/cypress/e2e/table/dls/allDois.cy.ts index d32d4a7a5..eaae1662c 100644 --- a/packages/datagateway-dataview/cypress/e2e/table/dls/allDois.cy.ts +++ b/packages/datagateway-dataview/cypress/e2e/table/dls/allDois.cy.ts @@ -79,17 +79,13 @@ describe('DLS - All DOIs Table', () => { }); it('should be able to sort by all sort directions on single and multiple columns', () => { + cy.get('[aria-sort="descending"]').should('exist'); cy.contains('User-created DOI').click(); - //Revert the default sort - cy.contains('[role="button"]', 'Publication Date') - .as('dateSortButton') - .click(); - // ascending order cy.contains('[role="button"]', 'Title').as('titleSortButton').click(); - cy.get('[aria-sort="ascending"]').should('exist'); + cy.contains('[aria-sort="ascending"]', 'Title').should('exist'); cy.get('.MuiTableSortLabel-iconDirectionAsc').should('be.visible'); cy.get('[aria-rowindex="1"] [aria-colindex="1"]').contains( 'Test DOI Title 1' @@ -122,6 +118,7 @@ describe('DLS - All DOIs Table', () => { }); it('should be able to filter with text & date filters on multiple columns', () => { + cy.get('[aria-sort="descending"]').should('exist'); cy.contains('User-created DOI').click(); // test text filter diff --git a/packages/datagateway-dataview/cypress/e2e/table/dls/datafiles.cy.ts b/packages/datagateway-dataview/cypress/e2e/table/dls/datafiles.cy.ts index 62da2bed7..abb472dbf 100644 --- a/packages/datagateway-dataview/cypress/e2e/table/dls/datafiles.cy.ts +++ b/packages/datagateway-dataview/cypress/e2e/table/dls/datafiles.cy.ts @@ -38,16 +38,11 @@ describe('DLS - Datafiles Table', () => { }); it('should be able to sort by all sort directions on single and multiple columns', () => { - // Revert the default sort - cy.contains('[role="button"]', 'Name').as('nameSortButton').click(); - cy.get('@nameSortButton').click(); - cy.wait('@datafilesOrder', { timeout: 10000 }); - // ascending order cy.contains('[role="button"]', 'Location').as('locationSortButton').click(); cy.wait('@datafilesOrder', { timeout: 10000 }); - cy.get('[aria-sort="ascending"]').should('exist'); + cy.contains('[aria-sort="ascending"]', 'Location').should('exist'); cy.get('.MuiTableSortLabel-iconDirectionAsc').should('be.visible'); cy.get('[aria-rowindex="1"] [aria-colindex="4"]').contains( '/analysis/unit/bank.tiff' @@ -84,7 +79,9 @@ describe('DLS - Datafiles Table', () => { // multiple columns (shift click) cy.contains('[role="button"]', 'Create Time').click(); cy.wait('@datafilesOrder', { timeout: 10000 }); - cy.get('@nameSortButton').click({ shiftKey: true }); + cy.contains('[role="button"]', 'Name') + .as('nameSortButton') + .click({ shiftKey: true }); cy.wait('@datafilesOrder', { timeout: 10000 }); cy.get('@nameSortButton').click({ shiftKey: true }); cy.wait('@datafilesOrder', { timeout: 10000 }); @@ -104,6 +101,8 @@ describe('DLS - Datafiles Table', () => { // check icon when clicking on a column cy.contains('[role="button"]', 'Location').click(); + + cy.contains('[aria-sort="ascending"]', 'Location').should('exist'); cy.get('[data-testid="ArrowDownwardIcon"]').should('have.length', 1); cy.get('.MuiTableSortLabel-iconDirectionAsc').should('exist'); diff --git a/packages/datagateway-dataview/cypress/e2e/table/dls/datasets.cy.ts b/packages/datagateway-dataview/cypress/e2e/table/dls/datasets.cy.ts index 86541eff8..5037d7fef 100644 --- a/packages/datagateway-dataview/cypress/e2e/table/dls/datasets.cy.ts +++ b/packages/datagateway-dataview/cypress/e2e/table/dls/datasets.cy.ts @@ -50,8 +50,11 @@ describe('DLS - Datasets Table', () => { it('should be able to sort by all sort directions on single and multiple columns', () => { // Revert the default sort + cy.contains('[aria-sort="ascending"]', 'Name').should('exist'); cy.contains('[role="button"]', 'Name').as('nameSortButton').click(); + cy.contains('[aria-sort="descending"]', 'Name').should('exist'); cy.get('@nameSortButton').click(); + cy.contains('[aria-sort]', 'Name').should('not.exist'); // ascending order cy.get('@nameSortButton').click(); @@ -98,20 +101,19 @@ describe('DLS - Datasets Table', () => { // should replace previous sort when clicked without shift cy.contains('[role="button"]', 'Modified Time').click(); + cy.contains('[aria-sort="ascending"]', 'Modified Time').should('exist'); cy.contains('[role="button"]', 'Modified Time').click(); - cy.get('[aria-sort="descending"]').should('have.length', 1); + cy.contains('[aria-sort="descending"]', 'Modified Time').should('exist'); cy.get('[aria-rowindex="1"] [aria-colindex="3"]').contains('DATASET 61'); }); it('should change icons when sorting on a column', () => { - // clear default sort - cy.contains('[role="button"]', 'Name').click(); - cy.contains('[role="button"]', 'Name').click(); - - cy.get('[data-testid="SortIcon"]').should('have.length', 5); + cy.get('[data-testid="SortIcon"]').should('have.length', 4); // check icon when clicking on a column cy.contains('[role="button"]', 'Create Time').click(); + + cy.contains('[aria-sort="ascending"]', 'Create Time').should('exist'); cy.get('[data-testid="ArrowDownwardIcon"]').should('have.length', 1); cy.get('.MuiTableSortLabel-iconDirectionAsc').should('exist'); diff --git a/packages/datagateway-dataview/cypress/e2e/table/dls/myData.cy.ts b/packages/datagateway-dataview/cypress/e2e/table/dls/myData.cy.ts index aed3e37f3..40a21ab69 100644 --- a/packages/datagateway-dataview/cypress/e2e/table/dls/myData.cy.ts +++ b/packages/datagateway-dataview/cypress/e2e/table/dls/myData.cy.ts @@ -45,13 +45,10 @@ describe('DLS - MyData Table', () => { }); it('should be able to sort by all sort directions on single and multiple columns', () => { - //Revert the default sort - cy.contains('[role="button"]', 'Start Date').click(); - // ascending order cy.contains('[role="button"]', 'Title').as('titleSortButton').click(); - cy.get('[aria-sort="ascending"]').should('exist'); + cy.contains('[aria-sort="ascending"]', 'Title').should('exist'); cy.get('.MuiTableSortLabel-iconDirectionAsc').should('be.visible'); cy.get('[aria-rowindex="1"] [aria-colindex="2"]').contains( 'Across prepare why go.' @@ -98,13 +95,12 @@ describe('DLS - MyData Table', () => { }); it('should change icons when sorting on a column', () => { - // clear default sort - cy.contains('[role="button"]', 'Start Date').click(); - - cy.get('[data-testid="SortIcon"]').should('have.length', 6); + cy.get('[data-testid="SortIcon"]').should('have.length', 5); // check icon when clicking on a column cy.contains('[role="button"]', 'Instrument').click(); + + cy.contains('[aria-sort="ascending"]', 'Instrument').should('exist'); cy.get('[data-testid="ArrowDownwardIcon"]').should('have.length', 1); cy.get('.MuiTableSortLabel-iconDirectionAsc').should('exist'); diff --git a/packages/datagateway-dataview/cypress/e2e/table/dls/myDois.cy.ts b/packages/datagateway-dataview/cypress/e2e/table/dls/myDois.cy.ts index 7ddb886ce..3eb2af757 100644 --- a/packages/datagateway-dataview/cypress/e2e/table/dls/myDois.cy.ts +++ b/packages/datagateway-dataview/cypress/e2e/table/dls/myDois.cy.ts @@ -103,15 +103,10 @@ describe('DLS - MyDOIs Table', () => { }); it('should be able to sort by all sort directions on single and multiple columns', () => { - //Revert the default sort - cy.contains('[role="button"]', 'Publication Date') - .as('dateSortButton') - .click(); - // ascending order cy.contains('[role="button"]', 'Title').as('titleSortButton').click(); - cy.get('[aria-sort="ascending"]').should('exist'); + cy.contains('[aria-sort="ascending"]', 'Title').should('exist'); cy.get('.MuiTableSortLabel-iconDirectionAsc').should('be.visible'); cy.get('[aria-rowindex="1"] [aria-colindex="1"]').contains( 'Test DOI Title 1' @@ -221,10 +216,18 @@ describe('DLS - MyDOIs Table', () => { cy.title().should('equal', 'DataGateway DataView'); cy.get('#datagateway-dataview').should('be.visible'); + // wait for default sort to get applied before changing the DOI type + // which also updates the URL and can cause a race condition + cy.contains('[aria-sort="descending"]', 'Publication Date').should( + 'exist' + ); + cy.contains('Am listed on the session DOI').click(); //Default sort - cy.get('[aria-sort="descending"]').should('exist'); + cy.contains('[aria-sort="descending"]', 'Publication Date').should( + 'exist' + ); cy.get('.MuiTableSortLabel-iconDirectionDesc').should('be.visible'); cy.contains('72: Star enter wide nearly off.').click({ force: true }); diff --git a/packages/datagateway-dataview/cypress/e2e/table/isis/datafiles.cy.ts b/packages/datagateway-dataview/cypress/e2e/table/isis/datafiles.cy.ts index b072d69ae..78f80bcd1 100644 --- a/packages/datagateway-dataview/cypress/e2e/table/isis/datafiles.cy.ts +++ b/packages/datagateway-dataview/cypress/e2e/table/isis/datafiles.cy.ts @@ -53,17 +53,11 @@ describe('ISIS - Datafiles Table', () => { }); it('should be able to sort by all sort directions on single and multiple columns', () => { - //Revert the default sort - cy.contains('[role="button"]', 'Modified Time') - .as('timeSortButton') - .click(); - cy.wait('@datafilesOrder', { timeout: 10000 }); - // ascending order cy.contains('[role="button"]', 'Location').as('locationSortButton').click(); cy.wait('@datafilesOrder', { timeout: 10000 }); - cy.get('[aria-sort="ascending"]').should('exist'); + cy.contains('[aria-sort="ascending"]', 'Location').should('exist'); cy.get('.MuiTableSortLabel-iconDirectionAsc').should('be.visible'); cy.get('[aria-rowindex="1"] [aria-colindex="4"]').contains( '/add/go/interview.png' @@ -97,7 +91,7 @@ describe('ISIS - Datafiles Table', () => { ); // multiple columns (shift click) - cy.get('@timeSortButton').click(); + cy.contains('[role="button"]', 'Modified Time').click(); cy.wait('@datafilesOrder', { timeout: 10000 }); cy.contains('[role="button"]', 'Name') .as('nameSortButton') @@ -110,7 +104,6 @@ describe('ISIS - Datafiles Table', () => { // should replace previous sort when clicked without shift cy.contains('[role="button"]', 'Location').click(); - cy.wait('@datafilesOrder', { timeout: 10000 }); cy.get('[aria-sort="ascending"]').should('have.length', 1); cy.get('[aria-rowindex="1"] [aria-colindex="4"]').contains( '/add/go/interview.png' @@ -118,14 +111,12 @@ describe('ISIS - Datafiles Table', () => { }); it('should change icons when sorting on a column', () => { - // clear default sort - cy.contains('[role="button"]', 'Name').click(); - cy.contains('[role="button"]', 'Name').click(); - - cy.get('[data-testid="SortIcon"]').should('have.length', 4); + cy.get('[data-testid="SortIcon"]').should('have.length', 3); // check icon when clicking on a column cy.contains('[role="button"]', 'Location').click(); + + cy.contains('[aria-sort="ascending"]', 'Location').should('exist'); cy.get('[data-testid="ArrowDownwardIcon"]').should('have.length', 1); cy.get('.MuiTableSortLabel-iconDirectionAsc').should('exist'); diff --git a/packages/datagateway-dataview/cypress/e2e/table/isis/datasets.cy.ts b/packages/datagateway-dataview/cypress/e2e/table/isis/datasets.cy.ts index f012b55b0..56023cfbf 100644 --- a/packages/datagateway-dataview/cypress/e2e/table/isis/datasets.cy.ts +++ b/packages/datagateway-dataview/cypress/e2e/table/isis/datasets.cy.ts @@ -48,21 +48,22 @@ describe('ISIS - Datasets Table', () => { }); it('should be able to sort by all sort directions on single and multiple columns', () => { + cy.contains('[aria-sort="ascending"]', 'Name').should('exist'); //Revert the default sort - cy.contains('[role="button"]', 'Create Time').as('timeSortButton').click(); - cy.wait('@datasetsOrder', { timeout: 10000 }); + cy.contains('[role="button"]', 'Name').as('nameSortButton').click(); + cy.contains('[aria-sort="descending"]', 'Name').should('exist'); + cy.get('@nameSortButton').click(); + cy.get('[aria-sort]').should('not.exist'); // ascending order cy.contains('[role="button"]', 'Name').as('nameSortButton').click(); - cy.wait('@datasetsOrder', { timeout: 10000 }); - cy.get('[aria-sort="ascending"]').should('exist'); + cy.contains('[aria-sort="ascending"]', 'Name').should('exist'); cy.get('.MuiTableSortLabel-iconDirectionAsc').should('be.visible'); cy.get('[aria-rowindex="1"] [aria-colindex="3"]').contains('DATASET 19'); // descending order cy.get('@nameSortButton').click(); - cy.wait('@datasetsOrder', { timeout: 10000 }); cy.get('[aria-sort="descending"]').should('exist'); cy.get('.MuiTableSortLabel-iconDirectionDesc').should( @@ -85,29 +86,26 @@ describe('ISIS - Datasets Table', () => { cy.get('[aria-rowindex="1"] [aria-colindex="3"]').contains('DATASET 19'); // multiple columns (shift click) - cy.get('@timeSortButton').click(); - cy.wait('@datasetsOrder', { timeout: 10000 }); + cy.contains('[role="button"]', 'Create Time').click(); cy.get('@nameSortButton').click({ shiftKey: true }); - cy.wait('@datasetsOrder', { timeout: 10000 }); cy.get('[aria-rowindex="1"] [aria-colindex="3"]').contains('DATASET 19'); // should replace previous sort when clicked without shift cy.contains('[role="button"]', 'Modified Time').click(); + cy.contains('[aria-sort="ascending"]', 'Modified Time').should('exist'); cy.contains('[role="button"]', 'Modified Time').click(); cy.get('[aria-sort="descending"]').should('have.length', 1); cy.get('[aria-rowindex="1"] [aria-colindex="3"]').contains('DATASET 79'); }); it('should change icons when sorting on a column', () => { - // clear default sort - cy.contains('[role="button"]', 'Name').click(); - cy.contains('[role="button"]', 'Name').click(); - - cy.get('[data-testid="SortIcon"]').should('have.length', 4); + cy.get('[data-testid="SortIcon"]').should('have.length', 3); // check icon when clicking on a column cy.contains('[role="button"]', 'Create Time').click(); + + cy.contains('[aria-sort="ascending"]', 'Create Time').should('exist'); cy.get('[data-testid="ArrowDownwardIcon"]').should('have.length', 1); cy.get('.MuiTableSortLabel-iconDirectionAsc').should('exist'); diff --git a/packages/datagateway-dataview/cypress/e2e/table/isis/facilityCycles.cy.ts b/packages/datagateway-dataview/cypress/e2e/table/isis/facilityCycles.cy.ts index da858de13..79226ef0f 100644 --- a/packages/datagateway-dataview/cypress/e2e/table/isis/facilityCycles.cy.ts +++ b/packages/datagateway-dataview/cypress/e2e/table/isis/facilityCycles.cy.ts @@ -29,13 +29,10 @@ describe('ISIS - FacilityCycles Table', () => { }); it('should be able to sort by all sort directions on single and multiple columns', () => { - //Revert the default sort - cy.contains('[role="button"]', 'Start Date').click(); - // ascending order cy.contains('[role="button"]', 'Name').as('nameSortButton').click(); - cy.get('[aria-sort="ascending"]').should('exist'); + cy.contains('[aria-sort="ascending"]', 'Name').should('exist'); cy.get('.MuiTableSortLabel-iconDirectionAsc').should('be.visible'); cy.get('[aria-rowindex="1"] [aria-colindex="2"]').contains( '2001-04-02 00:00:00' @@ -75,29 +72,28 @@ describe('ISIS - FacilityCycles Table', () => { // should replace previous sort when clicked without shift cy.contains('[role="button"]', 'End Date').click(); + cy.contains('[aria-sort="ascending"]', 'End Date').should('exist'); cy.contains('[role="button"]', 'End Date').click(); cy.get('[aria-sort="descending"]').should('have.length', 1); cy.get('[aria-rowindex="1"] [aria-colindex="1"]').contains('2004 cycle 3'); }); it('should change icons when sorting on a column', () => { - // clear default sort - cy.contains('[role="button"]', 'Start Date').click(); - - cy.get('[data-testid="SortIcon"]').should('have.length', 3); + cy.get('[data-testid="SortIcon"]').should('have.length', 2); // check icon when clicking on a column - cy.contains('[role="button"]', 'Start Date').click(); + cy.contains('[role="button"]', 'End Date').click(); + cy.contains('[aria-sort="ascending"]', 'End Date').should('exist'); cy.get('[data-testid="ArrowDownwardIcon"]').should('have.length', 1); cy.get('.MuiTableSortLabel-iconDirectionAsc').should('exist'); // check icon when clicking on a column again - cy.contains('[role="button"]', 'Start Date').click(); + cy.contains('[role="button"]', 'End Date').click(); cy.get('[data-testid="ArrowDownwardIcon"]').should('have.length', 1); cy.get('.MuiTableSortLabel-iconDirectionAsc').should('not.exist'); // check icon when hovering over a column - cy.contains('[role="button"]', 'End Date').trigger('mouseover'); + cy.contains('[role="button"]', 'Start Date').trigger('mouseover'); cy.get('[data-testid="ArrowUpwardIcon"]').should('have.length', 1); cy.get('[data-testid="ArrowDownwardIcon"]').should('have.length', 1); diff --git a/packages/datagateway-dataview/cypress/e2e/table/isis/instruments.cy.ts b/packages/datagateway-dataview/cypress/e2e/table/isis/instruments.cy.ts index fea99b6b8..8623a8f16 100644 --- a/packages/datagateway-dataview/cypress/e2e/table/isis/instruments.cy.ts +++ b/packages/datagateway-dataview/cypress/e2e/table/isis/instruments.cy.ts @@ -31,14 +31,17 @@ describe('ISIS - Instruments Table', () => { }); it('should be able to sort by all sort directions on single and multiple columns', () => { + cy.contains('[aria-sort="ascending"]', 'Name').should('exist'); //Revert the default sort cy.contains('[role="button"]', 'Name').as('nameSortButton').click(); + cy.contains('[aria-sort="descending"]', 'Name').should('exist'); cy.get('@nameSortButton').click(); + cy.get('[aria-sort]').should('not.exist'); // ascending order cy.get('@nameSortButton').click(); - cy.get('[aria-sort="ascending"]').should('exist'); + cy.contains('[aria-sort="ascending"]', 'Name').should('exist'); cy.get('.MuiTableSortLabel-iconDirectionAsc').should('be.visible'); cy.get('[aria-rowindex="1"] [aria-colindex="2"]').contains( 'Eight imagine picture tough.' @@ -83,14 +86,17 @@ describe('ISIS - Instruments Table', () => { }); it('should change icons when sorting on a column', () => { - // clear default sort - cy.contains('[role="button"]', 'Name').click(); - cy.contains('[role="button"]', 'Name').click(); - + cy.contains('[aria-sort="ascending"]', 'Name').should('exist'); + //Revert the default sort + cy.contains('[role="button"]', 'Name').as('nameSortButton').click(); + cy.contains('[aria-sort="descending"]', 'Name').should('exist'); + cy.get('@nameSortButton').click(); + cy.get('[aria-sort]').should('not.exist'); cy.get('[data-testid="SortIcon"]').should('have.length', 2); // check icon when clicking on a column cy.contains('[role="button"]', 'Name').click(); + cy.contains('[aria-sort="ascending"]', 'Name').should('exist'); cy.get('[data-testid="ArrowDownwardIcon"]').should('have.length', 1); cy.get('.MuiTableSortLabel-iconDirectionAsc').should('exist'); diff --git a/packages/datagateway-dataview/cypress/e2e/table/isis/investigationDataPublications.cy.ts b/packages/datagateway-dataview/cypress/e2e/table/isis/investigationDataPublications.cy.ts index 41b4cce8b..5ca88f4eb 100644 --- a/packages/datagateway-dataview/cypress/e2e/table/isis/investigationDataPublications.cy.ts +++ b/packages/datagateway-dataview/cypress/e2e/table/isis/investigationDataPublications.cy.ts @@ -82,15 +82,10 @@ describe('ISIS - Investigation Data Publication Table', () => { }); it('should be able to sort by all sort directions on single and multiple columns', () => { - //Revert the default sort - cy.contains('[role="button"]', 'Publication Date') - .as('dateSortButton') - .click(); - // ascending order cy.contains('[role="button"]', 'Title').as('titleSortButton').click(); - cy.get('[aria-sort="ascending"]').should('exist'); + cy.contains('[aria-sort="ascending"]', 'Title').should('exist'); cy.get('.MuiTableSortLabel-iconDirectionAsc').should('be.visible'); cy.get('[aria-rowindex="1"] [aria-colindex="1"]').contains( 'Because fine have business' @@ -124,7 +119,9 @@ describe('ISIS - Investigation Data Publication Table', () => { ); // multiple columns (shift click) - cy.get('@dateSortButton').click(); + cy.contains('[role="button"]', 'Publication Date') + .as('dateSortButton') + .click(); cy.get('@dateSortButton').click({ shiftKey: true }); cy.get('@titleSortButton').click({ shiftKey: true }); @@ -139,13 +136,11 @@ describe('ISIS - Investigation Data Publication Table', () => { }); it('should change icons when sorting on a column', () => { - // clear default sort - cy.contains('[role="button"]', 'Publication Date').click(); - - cy.get('[data-testid="SortIcon"]').should('have.length', 3); + cy.get('[data-testid="SortIcon"]').should('have.length', 2); // check icon when clicking on a column cy.contains('[role="button"]', 'DOI').click(); + cy.contains('[aria-sort="ascending"]', 'DOI').should('exist'); cy.get('[data-testid="ArrowDownwardIcon"]').should('have.length', 1); cy.get('.MuiTableSortLabel-iconDirectionAsc').should('exist'); diff --git a/packages/datagateway-dataview/cypress/e2e/table/isis/studyDataPublications.cy.ts b/packages/datagateway-dataview/cypress/e2e/table/isis/studyDataPublications.cy.ts index 7062c7ceb..b4711db47 100644 --- a/packages/datagateway-dataview/cypress/e2e/table/isis/studyDataPublications.cy.ts +++ b/packages/datagateway-dataview/cypress/e2e/table/isis/studyDataPublications.cy.ts @@ -78,13 +78,10 @@ describe('ISIS - Study Data Publication Table', () => { }); it('should be able to sort by all sort directions on single and multiple columns', () => { - //Revert the default sort - cy.contains('[role="button"]', 'Title').as('titleSortButton').click(); - // ascending order cy.contains('[role="button"]', 'DOI').as('doiSortButton').click(); - cy.get('[aria-sort="ascending"]').should('exist'); + cy.contains('[aria-sort="ascending"]', 'DOI').should('exist'); cy.get('.MuiTableSortLabel-iconDirectionAsc').should('be.visible'); cy.get('[aria-rowindex="1"] [aria-colindex="1"]').contains( 'Article subject amount' @@ -118,7 +115,7 @@ describe('ISIS - Study Data Publication Table', () => { ); // multiple columns (shift click) - cy.get('@titleSortButton').click(); + cy.contains('[role="button"]', 'Title').as('titleSortButton').click(); cy.get('@titleSortButton').click({ shiftKey: true }); cy.get('@doiSortButton').click({ shiftKey: true }); @@ -133,13 +130,11 @@ describe('ISIS - Study Data Publication Table', () => { }); it('should change icons when sorting on a column', () => { - // clear default sort - cy.contains('[role="button"]', 'Title').click(); - - cy.get('[data-testid="SortIcon"]').should('have.length', 2); + cy.get('[data-testid="SortIcon"]').should('have.length', 1); // check icon when clicking on a column cy.contains('[role="button"]', 'DOI').click(); + cy.contains('[aria-sort="ascending"]', 'DOI').should('exist'); cy.get('[data-testid="ArrowDownwardIcon"]').should('have.length', 1); cy.get('.MuiTableSortLabel-iconDirectionAsc').should('exist'); diff --git a/packages/datagateway-dataview/package.json b/packages/datagateway-dataview/package.json index b0c16ac07..f3fd798e3 100644 --- a/packages/datagateway-dataview/package.json +++ b/packages/datagateway-dataview/package.json @@ -10,14 +10,12 @@ "@mui/material": "5.18.0", "@tanstack/react-query": "5.90.21", "@tanstack/react-query-devtools": "5.91.3", - "@types/history": "4.7.11", "@types/jsrsasign": "10.5.2", "@types/lodash.debounce": "4.0.6", "@types/lodash.memoize": "4.1.6", "@types/node": "24.12.0", "@types/react": "18.3.28", "@types/react-dom": "18.3.7", - "@types/react-router-dom": "5.3.3", "@types/react-virtualized": "9.22.2", "@types/redux-logger": "3.0.8", "@vitejs/plugin-react": "5.2.0", @@ -26,7 +24,6 @@ "browserslist-to-esbuild": "2.1.1", "datagateway-common": "^3.0.0", "date-fns": "2.30.0", - "history": "4.10.1", "i18next": "22.0.3", "i18next-browser-languagedetector": "8.2.0", "i18next-http-backend": "3.0.2", @@ -39,7 +36,7 @@ "react-dom": "18.3.1", "react-i18next": "12.3.1", "react-redux": "8.1.3", - "react-router-dom": "5.3.4", + "react-router": "7.13.2", "react-virtualized": "9.22.6", "redux": "4.2.0", "redux-logger": "3.0.6", diff --git a/packages/datagateway-dataview/src/App.tsx b/packages/datagateway-dataview/src/App.tsx index 1063a7f9d..99b12ddc4 100644 --- a/packages/datagateway-dataview/src/App.tsx +++ b/packages/datagateway-dataview/src/App.tsx @@ -19,13 +19,13 @@ import log from 'loglevel'; import React from 'react'; import { Translation } from 'react-i18next'; import { Provider, connect } from 'react-redux'; -import { BrowserRouter } from 'react-router-dom'; +import { BrowserRouter } from 'react-router'; import { AnyAction, Store, applyMiddleware, compose, createStore } from 'redux'; import { createLogger } from 'redux-logger'; import thunk, { ThunkDispatch } from 'redux-thunk'; import './App.css'; import { saveApiUrlMiddleware } from './page/idCheckFunctions'; -import PageContainer from './page/pageContainer.component'; +import ConnectedPageRouting from './page/pageRouting.component'; import { configureApp } from './state/actions'; import { StateType } from './state/app.types'; import AppReducer from './state/reducers/app.reducer'; @@ -148,7 +148,7 @@ class App extends React.Component { Finished loading } > - + diff --git a/packages/datagateway-dataview/src/page/__snapshots__/withIdCheck.test.tsx.snap b/packages/datagateway-dataview/src/page/__snapshots__/withIdCheck.test.tsx.snap index d95addd66..5cf46e007 100644 --- a/packages/datagateway-dataview/src/page/__snapshots__/withIdCheck.test.tsx.snap +++ b/packages/datagateway-dataview/src/page/__snapshots__/withIdCheck.test.tsx.snap @@ -42,6 +42,7 @@ exports[`WithIdCheck > renders error when checkingPromise does not resolve to be We're sorry, it seems as though the URL you requested is attempting to fetch incorrect data. Please double check your URL, navigate back via the breadcrumbs or
go back to the top level @@ -87,6 +88,7 @@ exports[`WithIdCheck > renders error when checkingPromise is rejected 1`] = ` We're sorry, it seems as though the URL you requested is attempting to fetch incorrect data. Please double check your URL, navigate back via the breadcrumbs or go back to the top level diff --git a/packages/datagateway-dataview/src/page/breadcrumbs.component.test.tsx b/packages/datagateway-dataview/src/page/breadcrumbs.component.test.tsx index 9f93b62df..9454d0786 100644 --- a/packages/datagateway-dataview/src/page/breadcrumbs.component.test.tsx +++ b/packages/datagateway-dataview/src/page/breadcrumbs.component.test.tsx @@ -1,20 +1,19 @@ -import { Provider } from 'react-redux'; -import configureStore from 'redux-mock-store'; -import thunk from 'redux-thunk'; -import { Router } from 'react-router-dom'; -import { dGCommonInitialState } from 'datagateway-common'; -import { initialState as dgDataViewInitialState } from '../state/reducers/dgdataview.reducer'; -import type { StateType } from '../state/app.types'; -import { createLocation, createMemoryHistory, type History } from 'history'; -import PageBreadcrumbs from './breadcrumbs.component'; -import axios from 'axios'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { render, - type RenderResult, screen, within, + type RenderResult, } from '@testing-library/react'; +import axios from 'axios'; +import { dGCommonInitialState } from 'datagateway-common'; +import { Provider } from 'react-redux'; +import { BrowserRouter } from 'react-router'; +import configureStore from 'redux-mock-store'; +import thunk from 'redux-thunk'; +import type { StateType } from '../state/app.types'; +import { initialState as dgDataViewInitialState } from '../state/reducers/dgdataview.reducer'; +import PageBreadcrumbs from './breadcrumbs.component'; vi.mock('loglevel'); @@ -58,7 +57,6 @@ const DLSRoutes = { describe('PageBreadcrumbs tests (Generic, DLS, ISIS)', () => { let state: StateType; - let history: History; const renderComponent = ( state: StateType, @@ -68,17 +66,15 @@ describe('PageBreadcrumbs tests (Generic, DLS, ISIS)', () => { return render( - + - + ); }; beforeEach(() => { - history = createMemoryHistory(); - state = JSON.parse( JSON.stringify({ dgdataview: { @@ -126,6 +122,8 @@ describe('PageBreadcrumbs tests (Generic, DLS, ISIS)', () => { }, }); }); + + window.history.replaceState({}, '', '/'); }); afterEach(() => { @@ -135,7 +133,7 @@ describe('PageBreadcrumbs tests (Generic, DLS, ISIS)', () => { describe('Generic', () => { it('generic route renders correctly at the base route and does not request', async () => { // Set up test state pathname. - history.replace(createLocation(genericRoutes['investigations'])); + window.history.replaceState({}, '', genericRoutes['investigations']); // Set up store with test state and mount the breadcrumb. renderComponent(state); @@ -151,11 +149,10 @@ describe('PageBreadcrumbs tests (Generic, DLS, ISIS)', () => { it('generic route renders correctly at the dataset level and requests the investigation entity', async () => { // Set up test state pathname. - history.replace( - createLocation({ - pathname: genericRoutes['datasets'], - search: '?view=card', - }) + window.history.replaceState( + {}, + '', + `${genericRoutes['datasets']}?view=card` ); // Set up store with test state and mount the breadcrumb. @@ -191,7 +188,7 @@ describe('PageBreadcrumbs tests (Generic, DLS, ISIS)', () => { it('generic route renders correctly at the datafile level and requests the investigation & dataset entities', async () => { // Set up test state pathname. - history.replace(createLocation(genericRoutes['datafiles'])); + window.history.replaceState({}, '', genericRoutes['datafiles']); // Set up store with test state and mount the breadcrumb. renderComponent(state); @@ -232,7 +229,7 @@ describe('PageBreadcrumbs tests (Generic, DLS, ISIS)', () => { describe('DLS', () => { it('DLS route renders correctly at the base level and does not request', async () => { // Set up test state pathname. - history.replace(createLocation(DLSRoutes['proposals'])); + window.history.replaceState({}, '', DLSRoutes['proposals']); // Set up store with test state and mount the breadcrumb. renderComponent(state); @@ -247,7 +244,7 @@ describe('PageBreadcrumbs tests (Generic, DLS, ISIS)', () => { it('DLS route renders correctly at the investigation level and requests the proposal entity', async () => { // Set up test state pathname. - history.replace(createLocation(DLSRoutes['investigations'])); + window.history.replaceState({}, '', DLSRoutes['investigations']); // Set up store with test state and mount the breadcrumb. renderComponent(state); @@ -256,7 +253,7 @@ describe('PageBreadcrumbs tests (Generic, DLS, ISIS)', () => { expect(axios.get).toHaveBeenCalledTimes(1); expect(axios.get).toHaveBeenCalledWith( '/investigations/findone?where=' + - JSON.stringify({ name: { eq: 'INVESTIGATION 1' } }), + JSON.stringify({ name: { eq: 'INVESTIGATION%201' } }), { headers: { Authorization: 'Bearer null', @@ -272,7 +269,7 @@ describe('PageBreadcrumbs tests (Generic, DLS, ISIS)', () => { it('DLS route renders correctly at the dataset level and requests the proposal & investigation entities', async () => { // Set up test state pathname. - history.replace(createLocation(DLSRoutes['datasets'])); + window.history.replaceState({}, '', DLSRoutes['datasets']); // Set up store with test state and mount the breadcrumb. renderComponent(state); @@ -282,7 +279,7 @@ describe('PageBreadcrumbs tests (Generic, DLS, ISIS)', () => { expect(axios.get).toHaveBeenNthCalledWith( 1, '/investigations/findone?where=' + - JSON.stringify({ name: { eq: 'INVESTIGATION 1' } }), + JSON.stringify({ name: { eq: 'INVESTIGATION%201' } }), { headers: { Authorization: 'Bearer null', @@ -305,7 +302,7 @@ describe('PageBreadcrumbs tests (Generic, DLS, ISIS)', () => { ); expect(breadcrumbs[0]).toHaveAttribute( 'href', - '/browse/proposal/INVESTIGATION 1/investigation' + '/browse/proposal/INVESTIGATION%201/investigation' ); expect(within(breadcrumbs[0]).getByText('Title 1')).toBeInTheDocument(); expect(within(breadcrumbs[1]).getByText('1')).toBeInTheDocument(); @@ -319,7 +316,7 @@ describe('PageBreadcrumbs tests (Generic, DLS, ISIS)', () => { it('DLS route renders correctly at the datafile level and requests the proposal, investigation and dataset entities', async () => { // Set up test state pathname. - history.replace(createLocation(DLSRoutes['datafiles'])); + window.history.replaceState({}, '', DLSRoutes['datafiles']); // Set up store with test state and mount the breadcrumb. renderComponent(state); @@ -329,7 +326,7 @@ describe('PageBreadcrumbs tests (Generic, DLS, ISIS)', () => { expect(axios.get).toHaveBeenNthCalledWith( 1, '/investigations/findone?where=' + - JSON.stringify({ name: { eq: 'INVESTIGATION 1' } }), + JSON.stringify({ name: { eq: 'INVESTIGATION%201' } }), { headers: { Authorization: 'Bearer null', @@ -357,12 +354,12 @@ describe('PageBreadcrumbs tests (Generic, DLS, ISIS)', () => { ); expect(breadcrumbs[0]).toHaveAttribute( 'href', - '/browse/proposal/INVESTIGATION 1/investigation' + '/browse/proposal/INVESTIGATION%201/investigation' ); expect(within(breadcrumbs[0]).getByText('Title 1')).toBeInTheDocument(); expect(breadcrumbs[1]).toHaveAttribute( 'href', - '/browse/proposal/INVESTIGATION 1/investigation/1/dataset' + '/browse/proposal/INVESTIGATION%201/investigation/1/dataset' ); expect(within(breadcrumbs[1]).getByText('1')).toBeInTheDocument(); expect(within(breadcrumbs[2]).getByText('Name 2')).toBeInTheDocument(); @@ -378,7 +375,7 @@ describe('PageBreadcrumbs tests (Generic, DLS, ISIS)', () => { describe('ISIS', () => { it('ISIS route renders correctly at the base level and does not request', async () => { // Set up test state pathname. - history.replace(createLocation(ISISRoutes['instruments'])); + window.history.replaceState({}, '', ISISRoutes['instruments']); // Set up store with test state and mount the breadcrumb. renderComponent(state, ['investigation', 'dataset']); @@ -394,7 +391,7 @@ describe('PageBreadcrumbs tests (Generic, DLS, ISIS)', () => { it('ISIS route renders correctly at the facility cycle level and requests the instrument entity', async () => { // Set up test state pathname. - history.replace(createLocation(ISISRoutes['facilityCycles'])); + window.history.replaceState({}, '', ISISRoutes['facilityCycles']); // Set up store with test state and mount the breadcrumb. renderComponent(state, ['investigation', 'dataset']); @@ -426,7 +423,7 @@ describe('PageBreadcrumbs tests (Generic, DLS, ISIS)', () => { it('ISIS route renders correctly at the investigation level and requests the instrument and facility cycle entities', async () => { // Set up test state pathname. - history.replace(createLocation(ISISRoutes['investigations'])); + window.history.replaceState({}, '', ISISRoutes['investigations']); // Set up store with test state and mount the breadcrumb. renderComponent(state, ['investigation', 'dataset']); @@ -468,7 +465,7 @@ describe('PageBreadcrumbs tests (Generic, DLS, ISIS)', () => { it('ISIS route renders correctly at the dataset level and requests the instrument, facility cycle and investigation entities', async () => { // Set up test state pathname. - history.replace(createLocation(ISISRoutes['datasets'])); + window.history.replaceState({}, '', ISISRoutes['datasets']); // Set up store with test state and mount the breadcrumb. renderComponent(state, ['investigation', 'dataset']); @@ -524,7 +521,7 @@ describe('PageBreadcrumbs tests (Generic, DLS, ISIS)', () => { it('ISIS route renders correctly at the datafile level and requests the instrument, facility cycle, investigation and dataset entities', async () => { // Set up test state pathname. - history.replace(createLocation(ISISRoutes['datafiles'])); + window.history.replaceState({}, '', ISISRoutes['datafiles']); // Set up store with test state and mount the breadcrumb. renderComponent(state, ['investigation', 'dataset']); @@ -585,7 +582,7 @@ describe('PageBreadcrumbs tests (Generic, DLS, ISIS)', () => { it('ISIS experiments route renders correctly at the base level and does not request', async () => { // Set up test state pathname. - history.replace(createLocation(ISISExperimentsRoutes['instruments'])); + window.history.replaceState({}, '', ISISExperimentsRoutes['instruments']); // Set up store with test state and mount the breadcrumb. renderComponent(state, ['investigation', 'dataset']); @@ -601,8 +598,10 @@ describe('PageBreadcrumbs tests (Generic, DLS, ISIS)', () => { it('ISIS experiments route renders correctly at the study datapublications level and requests the instrument entity', async () => { // Set up test state pathname. - history.replace( - createLocation(ISISExperimentsRoutes['studyDataPublications']) + window.history.replaceState( + {}, + '', + ISISExperimentsRoutes['studyDataPublications'] ); // Set up store with test state and mount the breadcrumb. @@ -638,8 +637,10 @@ describe('PageBreadcrumbs tests (Generic, DLS, ISIS)', () => { it('ISIS experiments route renders correctly at the data publication investigation level and requests the instrument and datapublication entities', async () => { // Set up test state pathname. - history.replace( - createLocation(ISISExperimentsRoutes['investigationDataPublications']) + window.history.replaceState( + {}, + '', + ISISExperimentsRoutes['investigationDataPublications'] ); // Set up store with test state and mount the breadcrumb. @@ -685,7 +686,7 @@ describe('PageBreadcrumbs tests (Generic, DLS, ISIS)', () => { it('ISIS experiments route renders correctly at the dataset level and requests the instrument, data publication and investigation entities', async () => { // Set up test state pathname. - history.replace(createLocation(ISISExperimentsRoutes['datasets'])); + window.history.replaceState({}, '', ISISExperimentsRoutes['datasets']); // Set up store with test state and mount the breadcrumb. renderComponent(state, ['dataPublication', 'investigation', 'dataset']); @@ -744,7 +745,7 @@ describe('PageBreadcrumbs tests (Generic, DLS, ISIS)', () => { it('ISIS experiments route renders correctly at the datafile level and requests the instrument, data publication, investigation and dataset entities', async () => { // Set up test state pathname. - history.replace(createLocation(ISISExperimentsRoutes['datafiles'])); + window.history.replaceState({}, '', ISISExperimentsRoutes['datafiles']); // Set up store with test state and mount the breadcrumb. renderComponent(state, ['dataPublication', 'investigation', 'dataset']); diff --git a/packages/datagateway-dataview/src/page/breadcrumbs.component.tsx b/packages/datagateway-dataview/src/page/breadcrumbs.component.tsx index e9300ba8d..9686e7402 100644 --- a/packages/datagateway-dataview/src/page/breadcrumbs.component.tsx +++ b/packages/datagateway-dataview/src/page/breadcrumbs.component.tsx @@ -22,7 +22,7 @@ import { import React from 'react'; import { useTranslation } from 'react-i18next'; import { useSelector } from 'react-redux'; -import { Link, useLocation } from 'react-router-dom'; +import { Link, useLocation } from 'react-router'; import { StateType } from '../state/app.types'; interface BreadcrumbProps { diff --git a/packages/datagateway-dataview/src/page/pageContainer.component.test.tsx b/packages/datagateway-dataview/src/page/pageContainer.component.test.tsx index a9e5ac4ac..d21bbf314 100644 --- a/packages/datagateway-dataview/src/page/pageContainer.component.test.tsx +++ b/packages/datagateway-dataview/src/page/pageContainer.component.test.tsx @@ -1,10 +1,5 @@ -import { - dGCommonInitialState, - DownloadCartItem, - readSciGatewayToken, -} from 'datagateway-common'; -import { createMemoryHistory, createPath, History } from 'history'; -import { generatePath, Router } from 'react-router-dom'; +import { dGCommonInitialState, DownloadCartItem } from 'datagateway-common'; +import { BrowserRouter, generatePath } from 'react-router'; import configureStore from 'redux-mock-store'; import thunk from 'redux-thunk'; import { StateType } from '../state/app.types'; @@ -17,7 +12,6 @@ import { useQueryClient, } from '@tanstack/react-query'; import { - act, render, screen, waitFor, @@ -26,32 +20,11 @@ import { } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import axios, { AxiosResponse } from 'axios'; +import React from 'react'; import { Provider } from 'react-redux'; -import { - checkInstrumentId as unmockedCheckInstrumentId, - checkInvestigationId as unmockedCheckInvestigationId, -} from './idCheckFunctions'; import PageContainer, { paths } from './pageContainer.component'; vi.mock('loglevel'); -vi.mock('./idCheckFunctions'); -const checkInstrumentId = vi.mocked(unmockedCheckInstrumentId); -const checkInvestigationId = vi.mocked(unmockedCheckInvestigationId); - -vi.mock('datagateway-common', async () => { - const originalModule = await vi.importActual('datagateway-common'); - - return { - __esModule: true, - ...originalModule, - // mock table and cardview to opt out of rendering them in these tests as there's no need - Table: vi.fn(() => 'MockedTable'), - CardView: vi.fn(() => 'MockedCardView'), - readSciGatewayToken: vi.fn(() => - (originalModule.readSciGatewayToken as typeof readSciGatewayToken)() - ), - }; -}); vi.mock('@tanstack/react-query', async () => ({ __esModule: true, @@ -64,15 +37,12 @@ vi.mock('@tanstack/react-query', async () => ({ describe('PageContainer - Tests', () => { let queryClient: QueryClient; - let history: History; let user: ReturnType; let cartItems: DownloadCartItem[]; let holder: HTMLElement; + let props: React.ComponentProps; - const renderComponent = ( - h: History = history, - client: QueryClient = queryClient - ): RenderResult => { + const renderComponent = (client: QueryClient = queryClient): RenderResult => { const state: StateType = { dgcommon: dGCommonInitialState, dgdataview: dgDataViewInitialState, @@ -81,54 +51,24 @@ describe('PageContainer - Tests', () => { const testStore = mockStore(state); return render( - + - + - + ); }; beforeEach(() => { queryClient = new QueryClient(); - history = createMemoryHistory({ - initialEntries: ['/'], - }); user = userEvent.setup(); cartItems = []; + props = { + loggedInAnonymously: false, + }; - // @ts-expect-error we need it this way - delete window.location; - // @ts-expect-error we need it this way - window.location = new URL(`http://localhost/`); - - // below code keeps window.location in sync with history changes - // (needed because useUpdateQueryParam uses window.location not history) - const historyReplace = history.replace; - const historyReplaceSpy = vi.spyOn(history, 'replace'); - historyReplaceSpy.mockImplementation((args) => { - historyReplace(args); - if (typeof args === 'string') { - // @ts-expect-error we need it this way - window.location = new URL(`http://localhost${args}`); - } else { - // @ts-expect-error we need it this way - window.location = new URL(`http://localhost${createPath(args)}`); - } - }); - const historyPush = history.push; - const historyPushSpy = vi.spyOn(history, 'push'); - historyPushSpy.mockImplementation((args) => { - historyPush(args); - if (typeof args === 'string') { - // @ts-expect-error we need it this way - window.location = new URL(`http://localhost${args}`); - } else { - // @ts-expect-error we need it this way - window.location = new URL(`http://localhost${createPath(args)}`); - } - }); + window.history.replaceState({}, '', '/'); holder = document.createElement('div'); holder.setAttribute('id', 'datagateway-search'); @@ -172,7 +112,7 @@ describe('PageContainer - Tests', () => { }); it('displays the correct entity count', async () => { - history.replace(paths.toggle.investigation); + window.history.replaceState({}, '', paths.toggle.investigation); vi.mocked(useQueryClient, { partial: true }).mockReturnValue({ getQueriesData: vi.fn(() => [[[], 101]] as [[], never][]), }); @@ -191,27 +131,30 @@ describe('PageContainer - Tests', () => { await screen.findByRole('button', { name: 'view-search' }) ); - expect(history.location.pathname).toBe('/search/data'); + expect(window.location.pathname).toBe('/search/data'); + }); - act(() => { - history.push('/browse/instrument'); - }); + it('opens search plugin when icon clicked (ISIS)', async () => { + window.history.replaceState({}, '', '/browse/instrument'); + renderComponent(); await user.click( await screen.findByRole('button', { name: 'view-search' }) ); - expect(history.location.pathname).toBe('/search/isis'); + expect(window.location.pathname).toBe('/search/isis'); + }); - act(() => { - history.push('/browse/proposal'); - }); + it('opens search plugin when icon clicked', async () => { + window.history.replaceState({}, '', '/browse/proposal'); + + renderComponent(); await user.click( await screen.findByRole('button', { name: 'view-search' }) ); - expect(history.location.pathname).toBe('/search/dls'); + expect(window.location.pathname).toBe('/search/dls'); }); it('opens download plugin when Download Cart clicked', async () => { @@ -221,8 +164,7 @@ describe('PageContainer - Tests', () => { await screen.findByRole('button', { name: 'app.cart_arialabel' }) ); - expect(history.length).toBe(2); - expect(history.location.pathname).toBe('/download'); + expect(window.location.pathname).toBe('/download'); }); it('do not display loading bar loading false', async () => { @@ -240,7 +182,9 @@ describe('PageContainer - Tests', () => { }); it('display clear filters button and clear for filters onClick', async () => { - history.replace( + window.history.replaceState( + {}, + '', '/browse/investigation?filters=%7B"title"%3A%7B"value"%3A"spend"%2C"type"%3A"include"%7D%7D' ); renderComponent(); @@ -252,18 +196,18 @@ describe('PageContainer - Tests', () => { expect( await screen.findByRole('button', { name: 'app.clear_filters' }) ).toBeDisabled(); - expect(history.location.search).toEqual('?'); + expect(window.location.search).toEqual(''); }); it('display clear filters button and clear for filters onClick (/my-data/DLS)', async () => { const dateNow = `${new Date(Date.now()).toISOString().split('T')[0]}`; - history.replace( + window.history.replaceState( + {}, + '', '/my-data/DLS?filters=%7B"startDate"%3A%7B"endDate"%3A" ' + dateNow + '"%7D%2C"title"%3A%7B"value"%3A"test"%2C"type"%3A"include"%7D%7D&sort=%7B%22startDate%22%3A%22desc%22%7D' ); - const response = { username: 'SomePerson' }; - vi.mocked(readSciGatewayToken, { partial: true }).mockReturnValue(response); renderComponent(); await user.click( @@ -272,17 +216,15 @@ describe('PageContainer - Tests', () => { expect( await screen.findByRole('button', { name: 'app.clear_filters' }) ).toBeDisabled(); - expect(history.location.search).toEqual( + expect(window.location.search).toEqual( '?filters=%7B%22startDate%22%3A%7B%22endDate%22%3A%22' + dateNow + '%22%7D%7D&sort=%7B%22startDate%22%3A%22desc%22%7D' ); - - vi.mocked(readSciGatewayToken).mockClear(); }); it('display disabled clear filters button', async () => { - history.replace(paths.toggle.investigation); + window.history.replaceState({}, '', paths.toggle.investigation); renderComponent(); expect( @@ -291,8 +233,11 @@ describe('PageContainer - Tests', () => { }); it('display filter warning on datafile table', async () => { - history.replace('/browse/investigation/1/dataset/25/datafile'); - vi.mocked(checkInvestigationId).mockResolvedValueOnce(true); + window.history.replaceState( + {}, + '', + '/browse/investigation/1/dataset/25/datafile' + ); renderComponent(); @@ -302,7 +247,7 @@ describe('PageContainer - Tests', () => { }); it('switches view button display name when clicked', async () => { - history.replace(paths.toggle.investigation); + window.history.replaceState({}, '', paths.toggle.investigation); renderComponent(); @@ -317,9 +262,7 @@ describe('PageContainer - Tests', () => { }); it('displays role selector when on My Data route', async () => { - const response = { username: 'SomePerson' }; - vi.mocked(readSciGatewayToken, { partial: true }).mockReturnValue(response); - history.replace(paths.myData.root); + window.history.replaceState({}, '', paths.myData.root); renderComponent(); @@ -331,9 +274,7 @@ describe('PageContainer - Tests', () => { }); it('displays doi type selector when on My DOIs route', async () => { - const response = { username: 'SomePerson' }; - vi.mocked(readSciGatewayToken, { partial: true }).mockReturnValue(response); - history.replace(paths.dataPublications.dls.myDOIs); + window.history.replaceState({}, '', paths.dataPublications.dls.myDOIs); renderComponent(); @@ -345,9 +286,7 @@ describe('PageContainer - Tests', () => { }); it('displays doi type selector when on All DOIs route', async () => { - const response = { username: 'SomePerson' }; - vi.mocked(readSciGatewayToken, { partial: true }).mockReturnValue(response); - history.replace(paths.dataPublications.dls.allDOIs); + window.history.replaceState({}, '', paths.dataPublications.dls.allDOIs); renderComponent(); @@ -359,7 +298,11 @@ describe('PageContainer - Tests', () => { }); it('display filter warning on toggle table', async () => { - history.replace(`${paths.toggle.investigation}?view=table`); + window.history.replaceState( + {}, + '', + `${paths.toggle.investigation}?view=table` + ); renderComponent(); @@ -369,7 +312,11 @@ describe('PageContainer - Tests', () => { }); it('do not display filter warning on toggle card', async () => { - history.replace(`${paths.toggle.investigation}?view=card`); + window.history.replaceState( + {}, + '', + `${paths.toggle.investigation}?view=card` + ); renderComponent(); @@ -379,11 +326,12 @@ describe('PageContainer - Tests', () => { }); it('do not use StyledRouting component on landing pages', async () => { - vi.mocked(checkInstrumentId).mockResolvedValueOnce(true); vi.mocked(useQueryClient, { partial: true }).mockReturnValue({ getQueriesData: vi.fn(), }); - history.replace( + window.history.replaceState( + {}, + '', generatePath(paths.dataPublications.landing.isisDataPublicationLanding, { instrumentId: 1, dataPublicationId: 2, @@ -392,15 +340,12 @@ describe('PageContainer - Tests', () => { renderComponent(); - expect( - await screen.findByTestId('isis-dataPublication-landing') - ).toBeInTheDocument(); expect(screen.queryByTestId('styled-routing')).toBeNull(); }); it('set view to card if cardview stored in localstorage', async () => { localStorage.setItem('dataView', 'card'); - history.replace(paths.toggle.investigation); + window.history.replaceState({}, '', paths.toggle.investigation); renderComponent(); @@ -408,14 +353,13 @@ describe('PageContainer - Tests', () => { await screen.findByRole('button', { name: 'page view app.view_table' }) ).toBeInTheDocument(); - expect(history.location.search).toBe('?view=card'); + expect(window.location.search).toBe('?view=card'); localStorage.removeItem('dataView'); }); it('displays warning label when browsing data anonymously', async () => { - const response = { username: 'anon/anon' }; - vi.mocked(readSciGatewayToken, { partial: true }).mockReturnValue(response); + props.loggedInAnonymously = true; renderComponent(); @@ -425,15 +369,13 @@ describe('PageContainer - Tests', () => { }); it('displays warning label when browsing study hierarchy', async () => { - history.replace( + window.history.replaceState( + {}, + '', generatePath(paths.dataPublications.toggle.isisDataPublication, { instrumentId: 1, }) ); - const response = { username: 'SomePerson' }; - vi.mocked(readSciGatewayToken, { partial: true }).mockReturnValueOnce( - response - ); renderComponent(); @@ -443,11 +385,6 @@ describe('PageContainer - Tests', () => { }); it('does not display warning label when logged in', async () => { - const response = { username: 'SomePerson' }; - vi.mocked(readSciGatewayToken, { partial: true }).mockReturnValueOnce( - response - ); - renderComponent(); await waitFor(() => { @@ -496,11 +433,13 @@ describe('PageContainer - Tests', () => { await screen.findByRole('button', { name: 'selection-alert-link' }) ); - expect(history.location.pathname).toBe('/download'); + expect(window.location.pathname).toBe('/download'); }); it('shows breadcrumb according to the current path', async () => { - history.replace( + window.history.replaceState( + {}, + '', generatePath(paths.toggle.isisInvestigation, { instrumentId: 1, facilityCycleId: 1, @@ -531,7 +470,7 @@ describe('PageContainer - Tests', () => { }); it('does not fetch cart when on homepage (cart request errors when user is viewing homepage unauthenticated)', () => { - history.replace(paths.homepage); + window.history.replaceState({}, '', paths.homepage); renderComponent(); expect(axios.get).not.toHaveBeenCalledWith('/user/cart'); diff --git a/packages/datagateway-dataview/src/page/pageContainer.component.tsx b/packages/datagateway-dataview/src/page/pageContainer.component.tsx index 748007f4a..a956005ff 100644 --- a/packages/datagateway-dataview/src/page/pageContainer.component.tsx +++ b/packages/datagateway-dataview/src/page/pageContainer.component.tsx @@ -20,30 +20,16 @@ import { ViewCartButton, ViewsType, parseSearchToQuery, - readSciGatewayToken, useCart, useUpdateQueryParam, useUpdateView, } from 'datagateway-common'; -import { Location as LocationType } from 'history'; import React from 'react'; import { useTranslation } from 'react-i18next'; -import { useSelector } from 'react-redux'; -import { - Route, - Switch as SwitchRouting, - matchPath, - useHistory, - useLocation, - useRouteMatch, -} from 'react-router-dom'; -import { StateType } from '../state/app.types'; +import { Outlet, matchPath, useLocation, useNavigate } from 'react-router'; import DOITypeSelector from '../views/doiTypeSelector.component'; import RoleSelector from '../views/roleSelector.component'; import PageBreadcrumbs from './breadcrumbs.component'; -import PageRouting from './pageRouting.component'; -import { DoiRedirect, GenericRedirect } from './redirect.component'; -import TranslatedHomePage from './translatedHomePage.component'; const getTablePaperStyle = ( displayFilterMessage: boolean, @@ -195,12 +181,14 @@ const NavBar = React.memo( } & CartProps ): React.ReactElement => { const [t] = useTranslation(); - const isDataPublication = - useRouteMatch([ - ...Object.values(paths.dataPublications.toggle), - ...Object.values(paths.dataPublications.standard), - ]) !== null; - const isISISRoute = useRouteMatch(isisPaths) !== null; + const { pathname } = useLocation(); + const isDataPublication = [ + ...Object.values(paths.dataPublications.toggle), + ...Object.values(paths.dataPublications.standard), + ].some((pathPattern) => matchPath(pathPattern, pathname) !== null); + const isISISRoute = isisPaths.some( + (pathPattern) => matchPath(pathPattern, pathname) !== null + ); const landingPages = isDataPublication ? paths.dataPublications.landing : isISISRoute @@ -220,18 +208,11 @@ const NavBar = React.memo( xs aria-label="page-breadcrumbs" > - {/* don't show breadcrumbs on /my-data or dls landing pages - only on browse */} - - - - - - + {/* show breadcrumbs on browse routes */} + {[paths.root, paths.dataPublications.root].some( + (pathPattern) => + matchPath({ path: pathPattern, end: false }, pathname) !== null + ) && } {props.loggedInAnonymously || isDataPublication ? ( @@ -293,9 +274,8 @@ const NavBar = React.memo( {/* The table entity count has a size of 2 (or 3 for xs screens); the breadcrumbs will take the remainder of the space. */} - { - return ( - - - - {t('app.results')}: {props.entityCount} - - - - ); - }} - /> + ) + .some( + (pathPattern) => matchPath(pathPattern, pathname) !== null + ) && ( + + + + {t('app.results')}: {props.entityCount} + + + + )} { - const { - view, - location, - viewStyle, - displayFilterMessage, - loggedInAnonymously, - linearProgressHeight, - } = props; + const { viewStyle, displayFilterMessage, linearProgressHeight } = props; const breadcrumbDiv = document.getElementById('breadcrumbs'); @@ -431,11 +400,7 @@ const StyledRouting = (props: { sx={viewStyle === 'card' ? cardPaperStyle : tableClassStyle} className="tour-dataview-data" > - + ); @@ -446,72 +411,54 @@ const ViewRouting = React.memo( view: ViewsType; loadedCount: boolean; totalDataCount: number; - location: LocationType; - loggedInAnonymously: boolean; linearProgressHeight: string; }): React.ReactElement => { - const { - view, - loadedCount, - totalDataCount, - location, - loggedInAnonymously, - linearProgressHeight, - } = props; + const { view, loadedCount, totalDataCount, linearProgressHeight } = props; const displayFilterMessage = loadedCount && totalDataCount === 0 && - !matchPath(location.pathname, { - path: Object.values(paths.preview), - exact: true, - }) && - !matchPath(location.pathname, { - path: paths.landing.dlsDataPublicationLanding + '/edit', - exact: true, - }); - - return ( - - {/* For "landing" paths, don't use a containing Paper */} - ( - - )} + Object.values(paths.preview).every( + (pathPattern) => matchPath(pathPattern, location.pathname) === null + ) && + !matchPath( + paths.landing.dlsDataPublicationLanding + '/edit', + location.pathname + ); + + /* For "landing" paths, don't use a containing Paper */ + if ( + [ + ...Object.values(paths.landing), + ...Object.values(paths.dataPublications.landing), + ...Object.values(paths.preview), + ].some( + (pathPattern) => matchPath(pathPattern, location.pathname) !== null + ) + ) + return ; + + /* For "toggle" paths, check state for the current view to determine styling */ + if ( + togglePaths.some( + (pathPattern) => matchPath(pathPattern, location.pathname) !== null + ) + ) + return ( + - {/* For "toggle" paths, check state for the current view to determine styling */} - - - + ); - {/* Otherwise, use the paper styling for tables*/} - - - - + /* Otherwise, use the paper styling for tables*/ + + return ( + ); } ); @@ -554,17 +501,17 @@ const getToggle = (pathname: string, view: ViewsType): boolean => { : false; }; -const DataviewPageContainer: React.FC = () => { +const PageContainer = (props: { loggedInAnonymously: boolean }) => { + const { loggedInAnonymously } = props; const location = useLocation(); - const { push } = useHistory(); - const anonUserName = useSelector( - (state: StateType) => state.dgcommon.anonUserName - ); - const prevLocationRef = React.useRef(location); + const navigate = useNavigate(); + const { view } = React.useMemo( () => parseSearchToQuery(location.search), [location.search] ); + + const prevLocationRef = React.useRef(location); const [totalDataCount, setTotalDataCount] = React.useState(0); // exclude size and count queries from showing the linear progress bar for performance @@ -622,22 +569,27 @@ const DataviewPageContainer: React.FC = () => { pushView(nextView); }, [pushView, view]); - const navigateToDownload = React.useCallback(() => push('/download'), [push]); + const navigateToDownload = React.useCallback( + () => navigate('/download'), + [navigate] + ); - const isisRouteMatch = useRouteMatch(isisPaths); - const dlsRouteMatch = useRouteMatch(dlsPaths); - const isISISRoute = isisRouteMatch !== null; - const isDLSRoute = dlsRouteMatch !== null; + const isISISRoute = isisPaths.some( + (pathPattern) => matchPath(pathPattern, location.pathname) !== null + ); + const isDLSRoute = dlsPaths.some( + (pathPattern) => matchPath(pathPattern, location.pathname) !== null + ); const navigateToSearch = React.useCallback(() => { if (isISISRoute) { - return push('/search/isis'); + return navigate('/search/isis'); } else if (isDLSRoute) { - return push('/search/dls'); + return navigate('/search/dls'); } else { - return push('/search/data'); + return navigate('/search/data'); } - }, [push, isISISRoute, isDLSRoute]); + }, [navigate, isISISRoute, isDLSRoute]); React.useEffect(() => { prevLocationRef.current = location; @@ -658,11 +610,6 @@ const DataviewPageContainer: React.FC = () => { } }, [location.pathname, view, prevView, prevLocation.pathname, replaceView]); - // Determine whether logged in anonymously (assume this if username is null) - const username = readSciGatewayToken().username; - const loggedInAnonymously = - username === null || username === (anonUserName ?? 'anon/anon'); - const { filters } = React.useMemo( () => parseSearchToQuery(location.search), [location.search] @@ -703,48 +650,35 @@ const DataviewPageContainer: React.FC = () => { - {/* Toggle between the table and card view */} - ( - - - - )} - /> - ( - // doesn't need a grid item wrapper as it's already got a grid - - )} - /> - ( - // doesn't need a grid item wrapper as it's already got a grid - - )} - /> - ( - - - - )} - /> - + matchPath(pathPattern, location.pathname) !== null + ) && ( + + + + )} + {matchPath( + paths.dataPublications.dls.myDOIs, + location.pathname + ) !== null && } + {matchPath( + paths.dataPublications.dls.allDOIs, + location.pathname + ) !== null && } + {/* Toggle between the table and card view */} + {Object.values(togglePaths).some( + (pathPattern) => + matchPath(pathPattern, location.pathname) !== null + ) && ( + + )} + {Object.values(paths.myData) + .concat( [ paths.dataPublications.dls.allDOIs, paths.dataPublications.dls.myDOIs, @@ -753,16 +687,18 @@ const DataviewPageContainer: React.FC = () => { Object.values(paths.standard), Object.values(paths.dataPublications.toggle), Object.values(paths.dataPublications.standard) - )} - render={() => ( - - - - )} - /> + ) + .some( + (pathPattern) => + matchPath(pathPattern, location.pathname) !== null + ) && ( + + + + )} { @@ -801,25 +735,4 @@ const DataviewPageContainer: React.FC = () => { ); }; -const PageContainer: React.FC = () => { - const location = useLocation(); - - return ( - - {/* Load the homepage */} - - - - - - - - {/* Load the standard dataview pageContainer */} - - - - - ); -}; - export default PageContainer; diff --git a/packages/datagateway-dataview/src/page/pageRouting.component.test.tsx b/packages/datagateway-dataview/src/page/pageRouting.component.test.tsx index ed076930c..a68292b2a 100644 --- a/packages/datagateway-dataview/src/page/pageRouting.component.test.tsx +++ b/packages/datagateway-dataview/src/page/pageRouting.component.test.tsx @@ -3,16 +3,19 @@ import configureStore from 'redux-mock-store'; import thunk from 'redux-thunk'; import { StateType } from '../state/app.types'; -import { DataPublication, dGCommonInitialState } from 'datagateway-common'; +import { + DataPublication, + dGCommonInitialState, + readSciGatewayToken, +} from 'datagateway-common'; import { Provider } from 'react-redux'; -import { Router } from 'react-router-dom'; +import { BrowserRouter } from 'react-router'; import { initialState as dgDataViewInitialState } from '../state/reducers/dgdataview.reducer'; import PageRouting from './pageRouting.component'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { act, render, screen } from '@testing-library/react'; import axios from 'axios'; -import { History, createMemoryHistory } from 'history'; import { findColumnHeaderByName, flushPromises } from '../setupTests'; import { checkDatasetId as unmockedCheckDatasetId, @@ -23,6 +26,17 @@ import { checkStudyDataPublicationId as unmockedCheckStudyDataPublicationId, } from './idCheckFunctions'; +vi.mock('datagateway-common', async () => { + const originalModule = await vi.importActual('datagateway-common'); + + return { + __esModule: true, + ...originalModule, + readSciGatewayToken: vi.fn(() => + (originalModule.readSciGatewayToken as typeof readSciGatewayToken)() + ), + }; +}); vi.mock('loglevel'); vi.mock('./idCheckFunctions'); const checkDatasetId = vi.mocked(unmockedCheckDatasetId); @@ -96,7 +110,6 @@ const DLSRoutes = { describe('PageTable', () => { let state: StateType; - let history: History; function Wrapper({ children }: { children: React.ReactNode }): JSX.Element { const mockStore = configureStore([thunk]); @@ -107,16 +120,14 @@ describe('PageTable', () => { }); return ( - + {children} - + ); } beforeEach(() => { - history = createMemoryHistory(); - state = JSON.parse( JSON.stringify({ dgdataview: dgDataViewInitialState, @@ -127,6 +138,9 @@ describe('PageTable', () => { vi.mocked(axios.get).mockImplementation((url: string) => { if (url.includes('count')) { return Promise.resolve({ data: 0 }); + } else if (url.match(/^\/[a-zA-Z]+\/\d+/) || url.includes('findone')) { + // request for single entity e.g. breadcrumb request + return Promise.resolve({ data: { id: 1, name: '1', title: '1' } }); } else if (url.includes('datapublications')) { // this is so that routes can convert from data pub id -> investigation id return Promise.resolve({ @@ -152,8 +166,10 @@ describe('PageTable', () => { } satisfies DataPublication, ], }); + } else if (url.includes('investigationusers')) { + return Promise.resolve({ data: [{ id: 1, role: 'role' }] }); } else { - return Promise.resolve({ data: [{ id: 1, name: '1' }] }); + return Promise.resolve({ data: [{ id: 1, name: '1', title: '1' }] }); } }); checkInstrumentAndFacilityCycleId.mockImplementation(() => @@ -172,18 +188,11 @@ describe('PageTable', () => { describe('Generic', () => { it('renders PageTable correctly', () => { - history.push('/'); - - render( - , - { - wrapper: Wrapper, - } - ); + window.history.replaceState({}, '', '/'); + + render(, { + wrapper: Wrapper, + }); expect( screen.getByRole('link', { name: 'Browse investigations' }) @@ -191,16 +200,9 @@ describe('PageTable', () => { }); it('renders PageCard correctly', () => { - history.push('/'); - - render( - , - { wrapper: Wrapper } - ); + window.history.replaceState({}, '', '/?view=card'); + + render(, { wrapper: Wrapper }); expect( screen.getByRole('link', { name: 'Browse investigations' }) @@ -208,18 +210,11 @@ describe('PageTable', () => { }); it('renders InvestigationTable for generic investigations route', async () => { - history.push(genericRoutes['investigations']); - - render( - , - { - wrapper: Wrapper, - } - ); + window.history.replaceState({}, '', genericRoutes['investigations']); + + render(, { + wrapper: Wrapper, + }); expect( await findColumnHeaderByName('investigations.title') @@ -248,35 +243,25 @@ describe('PageTable', () => { }); it('renders InvestigationCardView for generic investigations route', async () => { - history.push(genericRoutes.investigations); - - render( - , - { wrapper: Wrapper } + window.history.replaceState( + {}, + '', + `${genericRoutes.investigations}?view=card` ); + render(, { wrapper: Wrapper }); + expect( await screen.findByTestId('investigation-card-view') ).toBeInTheDocument(); }); it('renders DatasetTable for generic datasets route', async () => { - history.push(genericRoutes['datasets']); - - render( - , - { - wrapper: Wrapper, - } - ); + window.history.replaceState({}, '', genericRoutes['datasets']); + + render(, { + wrapper: Wrapper, + }); expect(await findColumnHeaderByName('datasets.name')).toBeInTheDocument(); expect( @@ -291,35 +276,25 @@ describe('PageTable', () => { }); it('renders DatasetCardView for generic datasets route', async () => { - history.push(genericRoutes.datasets); - - render( - , - { wrapper: Wrapper } + window.history.replaceState( + {}, + '', + `${genericRoutes.datasets}?view=card` ); + render(, { wrapper: Wrapper }); + expect( await screen.findByTestId('dataset-card-view') ).toBeInTheDocument(); }); it('renders DatafileTable for generic datafiles route', async () => { - history.push(genericRoutes['datafiles']); - - render( - , - { - wrapper: Wrapper, - } - ); + window.history.replaceState({}, '', genericRoutes['datafiles']); + + render(, { + wrapper: Wrapper, + }); expect( await findColumnHeaderByName('datafiles.name') @@ -337,18 +312,11 @@ describe('PageTable', () => { it('does not render DatafileTable for incorrect generic datafiles route', async () => { checkInvestigationId.mockImplementation(() => Promise.resolve(false)); - history.push(genericRoutes['datafiles']); - - render( - , - { - wrapper: Wrapper, - } - ); + window.history.replaceState({}, '', genericRoutes['datafiles']); + + render(, { + wrapper: Wrapper, + }); expect(await screen.findByText('loading.oops')).toBeInTheDocument(); }); @@ -356,18 +324,15 @@ describe('PageTable', () => { describe('ISIS', () => { it('renders ISISMyDataTable for ISIS my data route', async () => { - history.push(ISISRoutes['mydata']); - - render( - , - { - wrapper: Wrapper, - } - ); + vi.mocked(readSciGatewayToken).mockReturnValue({ + username: 'SomePerson', + sessionId: '', + }); + window.history.replaceState({}, '', ISISRoutes['mydata']); + + render(, { + wrapper: Wrapper, + }); expect( await findColumnHeaderByName('investigations.title') @@ -396,35 +361,27 @@ describe('PageTable', () => { }); it('redirects to login page when not signed in (ISISMyDataTable) ', () => { - history.push(ISISRoutes['mydata']); - - render( - , - { - wrapper: Wrapper, - } - ); + // react-router will warn that /login doesn't match a route + vi.spyOn(console, 'warn').mockImplementation(() => {}); + vi.mocked(readSciGatewayToken).mockReturnValue({ + username: 'anon/anon', + sessionId: '', + }); + window.history.replaceState({}, '', ISISRoutes['mydata']); + + render(, { + wrapper: Wrapper, + }); - expect(history.location.pathname).toBe('/login'); + expect(window.location.pathname).toBe('/login'); }); it('renders ISISInstrumentsTable for ISIS instruments route', async () => { - history.push(ISISRoutes['instruments']); - - render( - , - { - wrapper: Wrapper, - } - ); + window.history.replaceState({}, '', ISISRoutes['instruments']); + + render(, { + wrapper: Wrapper, + }); expect( await findColumnHeaderByName('instruments.name') @@ -435,35 +392,25 @@ describe('PageTable', () => { }); it('renders ISISInstrumentsCardView for ISIS instruments route', async () => { - history.push(ISISRoutes.instruments); - - render( - , - { wrapper: Wrapper } + window.history.replaceState( + {}, + '', + `${ISISRoutes.instruments}?view=card` ); + render(, { wrapper: Wrapper }); + expect( await screen.findByTestId('isis-instruments-card-view') ).toBeInTheDocument(); }); it('renders ISISFacilityCyclesTable for ISIS facilityCycles route', async () => { - history.push(ISISRoutes['facilityCycles']); - - render( - , - { - wrapper: Wrapper, - } - ); + window.history.replaceState({}, '', ISISRoutes['facilityCycles']); + + render(, { + wrapper: Wrapper, + }); expect( await findColumnHeaderByName('facilitycycles.name') @@ -477,35 +424,25 @@ describe('PageTable', () => { }); it('renders ISISFacilityCyclesCardView for ISIS facilityCycles route', async () => { - history.push(ISISRoutes.facilityCycles); - - render( - , - { wrapper: Wrapper } + window.history.replaceState( + {}, + '', + `${ISISRoutes.facilityCycles}?view=card` ); + render(, { wrapper: Wrapper }); + expect( await screen.findByTestId('isis-facility-card-view') ).toBeInTheDocument(); }); it('renders ISISInvestigationsTable for ISIS investigations route', async () => { - history.push(ISISRoutes['investigations']); - - render( - , - { - wrapper: Wrapper, - } - ); + window.history.replaceState({}, '', ISISRoutes['investigations']); + + render(, { + wrapper: Wrapper, + }); expect( await findColumnHeaderByName('investigations.title') @@ -531,36 +468,30 @@ describe('PageTable', () => { }); it('renders ISISInvestigationsCardView for ISIS investigations route', async () => { - history.push(ISISRoutes.investigations); - - render( - , - { wrapper: Wrapper } + window.history.replaceState( + {}, + '', + `${ISISRoutes.investigations}?view=card` ); + render(, { wrapper: Wrapper }); + expect( await screen.findByTestId('isis-investigations-card-view') ).toBeInTheDocument(); }); it('renders ISISInvestigationLanding for ISIS investigation route', async () => { - history.push(ISISRoutes['landing']['investigation']); - - render( - , - { - wrapper: Wrapper, - } + window.history.replaceState( + {}, + '', + ISISRoutes['landing']['investigation'] ); + render(, { + wrapper: Wrapper, + }); + expect( await screen.findByLabelText('branding-title') ).toBeInTheDocument(); @@ -571,16 +502,9 @@ describe('PageTable', () => { Promise.resolve(false) ); - history.push(ISISRoutes.landing.investigation); + window.history.replaceState({}, '', ISISRoutes.landing.investigation); - render( - , - { wrapper: Wrapper } - ); + render(, { wrapper: Wrapper }); await act(async () => { await flushPromises(); @@ -590,16 +514,9 @@ describe('PageTable', () => { }); it('renders ISISDatasetsTable for ISIS datasets route', async () => { - history.push(ISISRoutes.datasets); - - render( - , - { wrapper: Wrapper } - ); + window.history.replaceState({}, '', ISISRoutes.datasets); + + render(, { wrapper: Wrapper }); await act(async () => { await flushPromises(); @@ -620,16 +537,9 @@ describe('PageTable', () => { Promise.resolve(false) ); - history.push(ISISRoutes.datasets); + window.history.replaceState({}, '', ISISRoutes.datasets); - render( - , - { wrapper: Wrapper } - ); + render(, { wrapper: Wrapper }); await act(async () => { await flushPromises(); @@ -639,16 +549,9 @@ describe('PageTable', () => { }); it('renders ISISDatasetsCardview for ISIS datasets route', async () => { - history.push(ISISRoutes.datasets); - - render( - , - { wrapper: Wrapper } - ); + window.history.replaceState({}, '', `${ISISRoutes.datasets}?view=card`); + + render(, { wrapper: Wrapper }); await act(async () => { await flushPromises(); @@ -662,31 +565,17 @@ describe('PageTable', () => { Promise.resolve(false) ); - history.push(ISISRoutes.datasets); + window.history.replaceState({}, '', `${ISISRoutes.datasets}?view=card`); - render( - , - { wrapper: Wrapper } - ); + render(, { wrapper: Wrapper }); expect(await screen.findByText('loading.oops')).toBeInTheDocument(); }); it('renders ISISDatasetLanding for ISIS dataset route', async () => { - history.push(ISISRoutes.landing.dataset); - - render( - , - { wrapper: Wrapper } - ); + window.history.replaceState({}, '', ISISRoutes.landing.dataset); + + render(, { wrapper: Wrapper }); expect( await screen.findByTestId('isis-dataset-landing') @@ -698,31 +587,17 @@ describe('PageTable', () => { Promise.resolve(false) ); - history.push(ISISRoutes.landing.dataset); + window.history.replaceState({}, '', ISISRoutes.landing.dataset); - render( - , - { wrapper: Wrapper } - ); + render(, { wrapper: Wrapper }); expect(await screen.findByText('loading.oops')).toBeInTheDocument(); }); it('renders ISISDatafilesTable for ISIS datafiles route', async () => { - history.push(ISISRoutes.datafiles); - - render( - , - { wrapper: Wrapper } - ); + window.history.replaceState({}, '', ISISRoutes.datafiles); + + render(, { wrapper: Wrapper }); expect( await findColumnHeaderByName('datafiles.name') @@ -744,31 +619,17 @@ describe('PageTable', () => { ); checkInvestigationId.mockImplementation(() => Promise.resolve(false)); - history.push(ISISRoutes.datafiles); + window.history.replaceState({}, '', ISISRoutes.datafiles); - render( - , - { wrapper: Wrapper } - ); + render(, { wrapper: Wrapper }); expect(await screen.findByText('loading.oops')).toBeInTheDocument(); }); it('renders DatafilePreviewer for ISIS datafiles previewer route', async () => { - history.push(ISISRoutes.datafilePreview); - - render( - , - { wrapper: Wrapper } - ); + window.history.replaceState({}, '', ISISRoutes.datafilePreview); + + render(, { wrapper: Wrapper }); expect( await screen.findByText('datafiles.preview.cannot_preview') @@ -782,16 +643,9 @@ describe('PageTable', () => { checkInvestigationId.mockImplementation(() => Promise.resolve(false)); checkDatasetId.mockImplementation(() => Promise.resolve(false)); - history.push(ISISRoutes.datafilePreview); + window.history.replaceState({}, '', ISISRoutes.datafilePreview); - render( - , - { wrapper: Wrapper } - ); + render(, { wrapper: Wrapper }); expect(await screen.findByText('loading.oops')).toBeInTheDocument(); }); @@ -799,17 +653,14 @@ describe('PageTable', () => { describe('ISIS Data Publication Hierarchy', () => { it('renders ISISInstrumentsTable for ISIS instruments route in Data Publication Hierarchy', async () => { - history.push(ISISDataPublicationsRoutes.instruments); - - render( - , - { wrapper: Wrapper } + window.history.replaceState( + {}, + '', + ISISDataPublicationsRoutes.instruments ); + render(, { wrapper: Wrapper }); + expect( await findColumnHeaderByName('instruments.name') ).toBeInTheDocument(); @@ -819,34 +670,28 @@ describe('PageTable', () => { }); it('renders ISISInstrumentsCardView for ISIS instruments route in Data Publication Hierarchy', async () => { - history.push(ISISDataPublicationsRoutes.instruments); - - render( - , - { wrapper: Wrapper } + window.history.replaceState( + {}, + '', + `${ISISDataPublicationsRoutes.instruments}?view=card` ); + render(, { wrapper: Wrapper }); + expect( await screen.findByTestId('isis-instruments-card-view') ).toBeInTheDocument(); }); it('renders ISISDataPublicationsTable for ISIS dataPublications route in Data Publication Hierarchy', async () => { - history.push(ISISDataPublicationsRoutes['dataPublications']); - - render( - , - { wrapper: Wrapper } + window.history.replaceState( + {}, + '', + ISISDataPublicationsRoutes['dataPublications'] ); + render(, { wrapper: Wrapper }); + expect( await findColumnHeaderByName('datapublications.title') ).toBeInTheDocument(); @@ -856,34 +701,28 @@ describe('PageTable', () => { }); it('renders ISISDataPublicationsCardView for ISIS dataPublications route in Data Publication Hierarchy', async () => { - history.push(ISISDataPublicationsRoutes.dataPublications); - - render( - , - { wrapper: Wrapper } + window.history.replaceState( + {}, + '', + `${ISISDataPublicationsRoutes.dataPublications}?view=card` ); + render(, { wrapper: Wrapper }); + expect( await screen.findByTestId('isis-dataPublications-card-view') ).toBeInTheDocument(); }); it('renders ISISDataPublicationLanding for ISIS dataPublications route for Data Publication Hierarchy', async () => { - history.push(ISISDataPublicationsRoutes.landing.dataPublication); - - render( - , - { wrapper: Wrapper } + window.history.replaceState( + {}, + '', + ISISDataPublicationsRoutes.landing.dataPublication ); + render(, { wrapper: Wrapper }); + expect( await screen.findByTestId('isis-dataPublication-landing') ).toBeInTheDocument(); @@ -892,32 +731,26 @@ describe('PageTable', () => { it('does not render ISISDataPublicationLanding for incorrect ISIS dataPublications route for Data Publication Hierarchy', async () => { checkInstrumentId.mockImplementation(() => Promise.resolve(false)); - history.push(ISISDataPublicationsRoutes.landing.dataPublication); - - render( - , - { wrapper: Wrapper } + window.history.replaceState( + {}, + '', + ISISDataPublicationsRoutes.landing.dataPublication ); + render(, { wrapper: Wrapper }); + expect(await screen.findByText('loading.oops')).toBeInTheDocument(); }); it('renders ISISInvestigationsTable for ISIS investigations route in Data Publication Hierarchy', async () => { - history.push(ISISDataPublicationsRoutes.investigations); - - render( - , - { wrapper: Wrapper } + window.history.replaceState( + {}, + '', + ISISDataPublicationsRoutes.investigations ); + render(, { wrapper: Wrapper }); + expect( await findColumnHeaderByName('datapublications.title') ).toBeInTheDocument(); @@ -930,33 +763,23 @@ describe('PageTable', () => { }); it('renders ISISInvestigationsCardView for ISIS investigations route in Data Publication Hierarchy', async () => { - history.push(ISISDataPublicationsRoutes.investigations); - - render( - , - { wrapper: Wrapper } + window.history.replaceState( + {}, + '', + `${ISISDataPublicationsRoutes.investigations}?view=card` ); + render(, { wrapper: Wrapper }); + expect( await screen.findByTestId('isis-dataPublications-card-view') ).toBeInTheDocument(); }); it('renders ISISDatasetsTable for ISIS datasets route in Data Publication Hierarchy', async () => { - history.push(ISISDataPublicationsRoutes.datasets); - - render( - , - { wrapper: Wrapper } - ); + window.history.replaceState({}, '', ISISDataPublicationsRoutes.datasets); + + render(, { wrapper: Wrapper }); expect(await findColumnHeaderByName('datasets.name')).toBeInTheDocument(); expect(await findColumnHeaderByName('datasets.size')).toBeInTheDocument(); @@ -974,32 +797,22 @@ describe('PageTable', () => { Promise.resolve(false) ); - history.push(ISISDataPublicationsRoutes.datasets); + window.history.replaceState({}, '', ISISDataPublicationsRoutes.datasets); - render( - , - { wrapper: Wrapper } - ); + render(, { wrapper: Wrapper }); expect(await screen.findByText('loading.oops')).toBeInTheDocument(); }); it('renders ISISInvestigationLanding for ISIS investigation route for Data Publication Hierarchy', async () => { - history.push(ISISDataPublicationsRoutes.landing.investigation); - - render( - , - { wrapper: Wrapper } + window.history.replaceState( + {}, + '', + ISISDataPublicationsRoutes.landing.investigation ); + render(, { wrapper: Wrapper }); + expect( await screen.findByTestId('isis-investigation-landing') ).toBeInTheDocument(); @@ -1011,32 +824,26 @@ describe('PageTable', () => { Promise.resolve(false) ); - history.push(ISISDataPublicationsRoutes.landing.investigation); - - render( - , - { wrapper: Wrapper } + window.history.replaceState( + {}, + '', + ISISDataPublicationsRoutes.landing.investigation ); + render(, { wrapper: Wrapper }); + expect(await screen.findByText('loading.oops')).toBeInTheDocument(); }); it('renders ISISDatasetsCardView for ISIS datasets route in Data Publication Hierarchy', async () => { - history.push(ISISDataPublicationsRoutes.datasets); - - render( - , - { wrapper: Wrapper } + window.history.replaceState( + {}, + '', + `${ISISDataPublicationsRoutes.datasets}?view=card` ); + render(, { wrapper: Wrapper }); + expect( await screen.findByTestId('isis-datasets-card-view') ).toBeInTheDocument(); @@ -1048,32 +855,26 @@ describe('PageTable', () => { Promise.resolve(false) ); - history.push(ISISDataPublicationsRoutes.datasets); - - render( - , - { wrapper: Wrapper } + window.history.replaceState( + {}, + '', + `${ISISDataPublicationsRoutes.datasets}?view=card` ); + render(, { wrapper: Wrapper }); + expect(await screen.findByText('loading.oops')).toBeInTheDocument(); }); it('renders ISISDatasetLanding for ISIS dataset route for Data Publication Hierarchy', async () => { - history.push(ISISDataPublicationsRoutes.landing.dataset); - - render( - , - { wrapper: Wrapper } + window.history.replaceState( + {}, + '', + ISISDataPublicationsRoutes.landing.dataset ); + render(, { wrapper: Wrapper }); + expect( await screen.findByTestId('isis-dataset-landing') ).toBeInTheDocument(); @@ -1085,31 +886,21 @@ describe('PageTable', () => { Promise.resolve(false) ); - history.push(ISISDataPublicationsRoutes.landing.dataset); - - render( - , - { wrapper: Wrapper } + window.history.replaceState( + {}, + '', + ISISDataPublicationsRoutes.landing.dataset ); + render(, { wrapper: Wrapper }); + expect(await screen.findByText('loading.oops')).toBeInTheDocument(); }); it('renders ISISDatafilesTable for ISIS datafiles route in Data Publication Hierarchy', async () => { - history.push(ISISDataPublicationsRoutes.datafiles); - - render( - , - { wrapper: Wrapper } - ); + window.history.replaceState({}, '', ISISDataPublicationsRoutes.datafiles); + + render(, { wrapper: Wrapper }); expect( await findColumnHeaderByName('datafiles.name') @@ -1132,32 +923,22 @@ describe('PageTable', () => { ); checkInvestigationId.mockImplementation(() => Promise.resolve(false)); - history.push(ISISDataPublicationsRoutes.datafiles); + window.history.replaceState({}, '', ISISDataPublicationsRoutes.datafiles); - render( - , - { wrapper: Wrapper } - ); + render(, { wrapper: Wrapper }); expect(await screen.findByText('loading.oops')).toBeInTheDocument(); }); it('renders DatafilePreviewer for ISIS datafile preview route in Data Publication Hierarchy', async () => { - history.push(ISISDataPublicationsRoutes.datafilePreview); - - render( - , - { wrapper: Wrapper } + window.history.replaceState( + {}, + '', + ISISDataPublicationsRoutes.datafilePreview ); + render(, { wrapper: Wrapper }); + expect( await screen.findByText('datafiles.preview.cannot_preview') ).toBeInTheDocument(); @@ -1171,33 +952,28 @@ describe('PageTable', () => { checkInvestigationId.mockImplementation(() => Promise.resolve(false)); checkDatasetId.mockImplementation(() => Promise.resolve(false)); - history.push(ISISDataPublicationsRoutes.datafilePreview); - - render( - , - { wrapper: Wrapper } + window.history.replaceState( + {}, + '', + ISISDataPublicationsRoutes.datafilePreview ); + render(, { wrapper: Wrapper }); + expect(await screen.findByText('loading.oops')).toBeInTheDocument(); }); }); describe('DLS', () => { it('renders DLSMyDataTable for DLS my data route', async () => { - history.push(DLSRoutes.mydata); - - render( - , - { wrapper: Wrapper } - ); + vi.mocked(readSciGatewayToken).mockReturnValue({ + username: 'SomePerson', + sessionId: '', + }); + + window.history.replaceState({}, '', DLSRoutes.mydata); + + render(, { wrapper: Wrapper }); expect( await findColumnHeaderByName('investigations.title') @@ -1220,31 +996,28 @@ describe('PageTable', () => { }); it('redirects to login page when not signed in (DLSMyDataTable) ', () => { - history.push(DLSRoutes.mydata); - - render( - , - { wrapper: Wrapper } - ); + // react-router will warn that /login doesn't match a route + vi.spyOn(console, 'warn').mockImplementation(() => {}); + vi.mocked(readSciGatewayToken).mockReturnValue({ + username: 'anon/anon', + sessionId: '', + }); + window.history.replaceState({}, '', DLSRoutes.mydata); + + render(, { wrapper: Wrapper }); - expect(history.location.pathname).toBe('/login'); + expect(window.location.pathname).toBe('/login'); }); it('renders DLSMyDOIsTable for DLS my dois route', async () => { - history.push(DLSRoutes.mydois); - - render( - , - { wrapper: Wrapper } - ); + vi.mocked(readSciGatewayToken).mockReturnValue({ + username: 'SomePerson', + sessionId: '', + }); + + window.history.replaceState({}, '', DLSRoutes.mydois); + + render(, { wrapper: Wrapper }); expect( await findColumnHeaderByName('datapublications.title') @@ -1257,17 +1030,10 @@ describe('PageTable', () => { ).toBeInTheDocument(); }); - it('renders DLSMyDOIsTable for DLS all dois route', async () => { - history.push(DLSRoutes.alldois); + it('renders DLSAllDOIsTable for DLS all dois route', async () => { + window.history.replaceState({}, '', DLSRoutes.alldois); - render( - , - { wrapper: Wrapper } - ); + render(, { wrapper: Wrapper }); expect( await findColumnHeaderByName('datapublications.title') @@ -1281,31 +1047,23 @@ describe('PageTable', () => { }); it('redirects to login page when not signed in (DLSMyDOIsTable) ', () => { - history.push(DLSRoutes.mydois); - - render( - , - { wrapper: Wrapper } - ); + // react-router will warn that /login doesn't match a route + vi.spyOn(console, 'warn').mockImplementation(() => {}); + vi.mocked(readSciGatewayToken).mockReturnValue({ + username: 'anon/anon', + sessionId: '', + }); + window.history.replaceState({}, '', DLSRoutes.mydois); + + render(, { wrapper: Wrapper }); - expect(history.location.pathname).toBe('/login'); + expect(window.location.pathname).toBe('/login'); }); it('renders DLSProposalTable for DLS proposal route', async () => { - history.push(DLSRoutes.proposals); - - render( - , - { wrapper: Wrapper } - ); + window.history.replaceState({}, '', DLSRoutes.proposals); + + render(, { wrapper: Wrapper }); expect( await findColumnHeaderByName('investigations.title') @@ -1316,16 +1074,9 @@ describe('PageTable', () => { }); it('renders DLSProposalCardView for DLS proposal route', async () => { - history.push(DLSRoutes.proposals); - - render( - , - { wrapper: Wrapper } - ); + window.history.replaceState({}, '', `${DLSRoutes.proposals}?view=card`); + + render(, { wrapper: Wrapper }); expect( await screen.findByTestId('dls-proposals-card-view') @@ -1333,16 +1084,9 @@ describe('PageTable', () => { }); it('renders DLSVisitsTable for DLS investigations route', async () => { - history.push(DLSRoutes.investigations); - - render( - , - { wrapper: Wrapper } - ); + window.history.replaceState({}, '', DLSRoutes.investigations); + + render(, { wrapper: Wrapper }); expect( await findColumnHeaderByName('investigations.visit_id') @@ -1362,33 +1106,23 @@ describe('PageTable', () => { }); it('renders DLSVisitsCardView for DLS investigations route', async () => { - history.push(DLSRoutes.investigations); - - render( - , - { wrapper: Wrapper } + window.history.replaceState( + {}, + '', + `${DLSRoutes.investigations}?view=card` ); + render(, { wrapper: Wrapper }); + expect( await screen.findByTestId('dls-visits-card-view') ).toBeInTheDocument(); }); it('renders DLSDatasetsTable for DLS datasets route', async () => { - history.push(DLSRoutes.datasets); - - render( - , - { wrapper: Wrapper } - ); + window.history.replaceState({}, '', DLSRoutes.datasets); + + render(, { wrapper: Wrapper }); expect(await findColumnHeaderByName('datasets.name')).toBeInTheDocument(); expect( @@ -1406,31 +1140,17 @@ describe('PageTable', () => { it('does not render DLSDatasetsTable for incorrect DLS datasets route', async () => { checkProposalName.mockImplementation(() => Promise.resolve(false)); - history.push(DLSRoutes.datasets); + window.history.replaceState({}, '', DLSRoutes.datasets); - render( - , - { wrapper: Wrapper } - ); + render(, { wrapper: Wrapper }); expect(await screen.findByText('loading.oops')).toBeInTheDocument(); }); it('renders DLSDatasetsCardView for DLS datasets route', async () => { - history.push(DLSRoutes.datasets); - - render( - , - { wrapper: Wrapper } - ); + window.history.replaceState({}, '', `${DLSRoutes.datasets}?view=card`); + + render(, { wrapper: Wrapper }); expect( await screen.findByTestId('dls-datasets-card-view') @@ -1440,31 +1160,17 @@ describe('PageTable', () => { it('does not render DLSDatasetsCardView for incorrect DLS datasets route', async () => { checkProposalName.mockImplementation(() => Promise.resolve(false)); - history.push(DLSRoutes.datasets); + window.history.replaceState({}, '', `${DLSRoutes.datasets}?view=card`); - render( - , - { wrapper: Wrapper } - ); + render(, { wrapper: Wrapper }); expect(await screen.findByText('loading.oops')).toBeInTheDocument(); }); it('renders DLSDatafilesTable for DLS datafiles route', async () => { - history.push(DLSRoutes.datafiles); - - render( - , - { wrapper: Wrapper } - ); + window.history.replaceState({}, '', DLSRoutes.datafiles); + + render(, { wrapper: Wrapper }); expect( await findColumnHeaderByName('datafiles.name') @@ -1484,32 +1190,18 @@ describe('PageTable', () => { checkProposalName.mockImplementation(() => Promise.resolve(false)); checkInvestigationId.mockImplementation(() => Promise.resolve(false)); - history.push(DLSRoutes.datafiles); + window.history.replaceState({}, '', `${DLSRoutes.datafiles}?view=card`); - render( - , - { wrapper: Wrapper } - ); + render(, { wrapper: Wrapper }); expect(await screen.findByText('loading.oops')).toBeInTheDocument(); }); }); it('renders DLSDataPublicationLanding for DLS dataPublications route', async () => { - history.push(DLSRoutes.dataPublicationLanding); - - render( - , - { wrapper: Wrapper } - ); + window.history.replaceState({}, '', DLSRoutes.dataPublicationLanding); + + render(, { wrapper: Wrapper }); expect( await screen.findByTestId('dls-dataPublication-landing') diff --git a/packages/datagateway-dataview/src/page/pageRouting.component.tsx b/packages/datagateway-dataview/src/page/pageRouting.component.tsx index 74620f626..f773a13f4 100644 --- a/packages/datagateway-dataview/src/page/pageRouting.component.tsx +++ b/packages/datagateway-dataview/src/page/pageRouting.component.tsx @@ -1,13 +1,5 @@ -import type { Location as LocationType } from 'history'; import React from 'react'; -import { - Link, - Redirect, - Route, - Switch, - type RouteComponentProps, -} from 'react-router-dom'; -import DatafilePreviewer from '../views/datafilePreview/datafilePreviewer.component'; +import { Link, Navigate, Route, Routes, useLocation } from 'react-router'; import DatafileTable from '../views/table/datafileTable.component'; import DatasetTable from '../views/table/datasetTable.component'; @@ -16,7 +8,6 @@ import InvestigationTable from '../views/table/investigationTable.component'; import DLSDatafilesTable from '../views/table/dls/dlsDatafilesTable.component'; import DLSDatasetsTable from '../views/table/dls/dlsDatasetsTable.component'; import DLSProposalsTable from '../views/table/dls/dlsProposalsTable.component'; -import DLSVisitsTable from '../views/table/dls/dlsVisitsTable.component'; import ISISDatafilesTable from '../views/table/isis/isisDatafilesTable.component'; import ISISDataPublicationsTable from '../views/table/isis/isisDataPublicationsTable.component'; @@ -41,329 +32,46 @@ import ISISFacilityCyclesCardView from '../views/card/isis/isisFacilityCyclesCar import ISISInstrumentsCardView from '../views/card/isis/isisInstrumentsCardView.component'; import ISISInvestigationsCardView from '../views/card/isis/isisInvestigationsCardView.component'; import ISISDataPublicationLanding from '../views/landing/isis/isisDataPublicationLanding.component'; -import ISISDatasetLanding from '../views/landing/isis/isisDatasetLanding.component'; import ISISInvestigationLanding from '../views/landing/isis/isisInvestigationLanding.component'; import DLSDatasetsCardView from '../views/card/dls/dlsDatasetsCardView.component'; import DLSProposalsCardView from '../views/card/dls/dlsProposalsCardView.component'; -import DLSVisitsCardView from '../views/card/dls/dlsVisitsCardView.component'; import DLSDataPublicationLanding from '../views/landing/dls/dlsDataPublicationLanding.component'; import { - checkDatasetId, - checkInstrumentAndFacilityCycleId, - checkInstrumentId, - checkInvestigationId, - checkProposalName, - checkStudyDataPublicationId, -} from './idCheckFunctions'; - -import { useDataPublication, ViewsType } from 'datagateway-common'; + parseSearchToQuery, + readSciGatewayToken, + StateType, + ViewsType, +} from 'datagateway-common'; +import { useSelector } from 'react-redux'; +import DLSVisitsCardView from '../views/card/dls/dlsVisitsCardView.component'; +import { ISISDatafilePreviewer } from '../views/datafilePreview/isisDatafilePreviewer.component'; import DLSDataPublicationEditForm from '../views/landing/dls/dlsDataPublicationEditForm.component'; -import { paths } from './pageContainer.component'; -import WithIdCheck from './withIdCheck'; - -const SafeISISDatafilesTable = (props: { - instrumentId: string; - instrumentChildId: string; - investigationId: string; - datasetId: string; - dataPublication: boolean; -}): React.ReactElement => { - const { data, isPending } = useDataPublication( - parseInt(props.investigationId), - props.dataPublication - ); - const dataPublicationInvestigationId = - data?.content?.dataCollectionInvestigations?.[0]?.investigation?.id; - - const checkingPromise = props.dataPublication - ? Promise.all([ - checkInstrumentId( - parseInt(props.instrumentId), - parseInt(props.instrumentChildId) - ), - checkStudyDataPublicationId( - parseInt(props.instrumentChildId), - parseInt(props.investigationId) - ), - checkInvestigationId( - dataPublicationInvestigationId ?? -1, - parseInt(props.datasetId) - ), - ...(isPending ? [new Promise(() => undefined)] : []), - ]).then((values) => !values.includes(false)) - : Promise.all([ - checkInstrumentAndFacilityCycleId( - parseInt(props.instrumentId), - parseInt(props.instrumentChildId), - parseInt(props.investigationId) - ), - checkInvestigationId( - parseInt(props.investigationId), - parseInt(props.datasetId) - ), - ]).then((values) => !values.includes(false)); - - return ( - - - - ); -}; - -const SafeISISDatasetLanding = (props: { - instrumentId: string; - instrumentChildId: string; - investigationId: string; - datasetId: string; - dataPublication: boolean; -}): React.ReactElement => { - const { data, isPending } = useDataPublication( - parseInt(props.investigationId), - props.dataPublication - ); - const dataPublicationInvestigationId = - data?.content?.dataCollectionInvestigations?.[0]?.investigation?.id; - - const checkingPromise = props.dataPublication - ? Promise.all([ - checkInstrumentId( - parseInt(props.instrumentId), - parseInt(props.instrumentChildId) - ), - checkStudyDataPublicationId( - parseInt(props.instrumentChildId), - parseInt(props.investigationId) - ), - checkInvestigationId( - dataPublicationInvestigationId ?? -1, - parseInt(props.datasetId) - ), - ...(isPending ? [new Promise(() => undefined)] : []), - ]).then((values) => !values.includes(false)) - : Promise.all([ - checkInstrumentAndFacilityCycleId( - parseInt(props.instrumentId), - parseInt(props.instrumentChildId), - parseInt(props.investigationId) - ), - checkInvestigationId( - parseInt(props.investigationId), - parseInt(props.datasetId) - ), - ]).then((values) => !values.includes(false)); - - return ( - - - - ); -}; - -const SafeISISDatasetsTable = (props: { - instrumentId: string; - instrumentChildId: string; - investigationId: string; - dataPublication: boolean; -}): React.ReactElement => { - const { data, isPending } = useDataPublication( - parseInt(props.investigationId), - - props.dataPublication - ); - - const dataPublicationInvestigationId = - data?.content?.dataCollectionInvestigations?.[0]?.investigation?.id; - - const checkingPromise = props.dataPublication - ? Promise.all([ - checkInstrumentId( - parseInt(props.instrumentId), - parseInt(props.instrumentChildId) - ), - checkStudyDataPublicationId( - parseInt(props.instrumentChildId), - parseInt(props.investigationId) - ), - ...(isPending ? [new Promise(() => undefined)] : []), - ]).then((values) => !values.includes(false)) - : checkInstrumentAndFacilityCycleId( - parseInt(props.instrumentId), - parseInt(props.instrumentChildId), - parseInt(props.investigationId) - ); - - return ( - - - - ); -}; - -const SafeISISDatasetsCardView = (props: { - instrumentId: string; - instrumentChildId: string; - investigationId: string; - dataPublication: boolean; -}): React.ReactElement => { - const { data, isPending } = useDataPublication( - parseInt(props.investigationId), - props.dataPublication - ); - - const dataPublicationInvestigationId = - data?.content?.dataCollectionInvestigations?.[0]?.investigation?.id; - - const checkingPromise = props.dataPublication - ? Promise.all([ - checkInstrumentId( - parseInt(props.instrumentId), - parseInt(props.instrumentChildId) - ), - checkStudyDataPublicationId( - parseInt(props.instrumentChildId), - parseInt(props.investigationId) - ), - ...(isPending ? [new Promise(() => undefined)] : []), - ]).then((values) => !values.includes(false)) - : checkInstrumentAndFacilityCycleId( - parseInt(props.instrumentId), - parseInt(props.instrumentChildId), - parseInt(props.investigationId) - ); - - return ( - - - - ); -}; - -const SafeISISInvestigationLanding = React.memo( - (props: { - instrumentId: string; - instrumentChildId: string; - investigationId: string; - dataPublication: boolean; - }): React.ReactElement => { - const checkingPromise = props.dataPublication - ? Promise.all([ - checkInstrumentId( - parseInt(props.instrumentId), - parseInt(props.instrumentChildId) - ), - checkStudyDataPublicationId( - parseInt(props.instrumentChildId), - parseInt(props.investigationId) - ), - ]).then((values) => !values.includes(false)) - : checkInstrumentAndFacilityCycleId( - parseInt(props.instrumentId), - parseInt(props.instrumentChildId), - parseInt(props.investigationId) - ); - - return ( - - - - ); - } -); -SafeISISInvestigationLanding.displayName = 'SafeISISInvestigationLanding'; - -const SafeDatafilePreviewer = (props: { - dataPublication: boolean; - instrumentId: string; - instrumentChildId: string; - investigationId: string; - datasetId: string; - datafileId: string; -}): React.ReactElement => { - const { data, isPending } = useDataPublication( - parseInt(props.investigationId), - props.dataPublication - ); - - const dataPublicationInvestigationId = - data?.content?.dataCollectionInvestigations?.[0]?.investigation?.id; - - const checkingPromise = props.dataPublication - ? Promise.all([ - checkInstrumentId( - parseInt(props.instrumentId), - parseInt(props.instrumentChildId) - ), - checkStudyDataPublicationId( - parseInt(props.instrumentChildId), - parseInt(props.investigationId) - ), - checkInvestigationId( - dataPublicationInvestigationId ?? -1, - parseInt(props.datasetId) - ), - checkDatasetId(parseInt(props.datasetId), parseInt(props.datafileId)), - ...(isPending ? [new Promise(() => undefined)] : []), - ]).then((values) => !values.includes(false)) - : Promise.all([ - checkInstrumentAndFacilityCycleId( - parseInt(props.instrumentId), - parseInt(props.instrumentChildId), - parseInt(props.investigationId) - ), - checkInvestigationId( - parseInt(props.investigationId), - parseInt(props.datasetId) - ), - checkDatasetId(parseInt(props.datasetId), parseInt(props.datafileId)), - ]).then((checks) => checks.every((passes) => passes)); - - return ( - - - - ); -}; +import ISISDatasetLandingPage from '../views/landing/isis/isisDatasetLanding.component'; +import DLSVisitsTable from '../views/table/dls/dlsVisitsTable.component'; +import PageContainer, { paths } from './pageContainer.component'; +import { DoiRedirect, GenericRedirect } from './redirect.component'; +import { TranslatedHomePage } from './translatedHomePage.component'; interface PageRoutingProps { view: ViewsType; - location: LocationType; loggedInAnonymously: boolean; } -class PageRouting extends React.PureComponent { - public render(): React.ReactNode { - return ( - +const PageRouting = ({ view, loggedInAnonymously }: PageRoutingProps) => { + return ( + + } /> + } /> + } /> + } + > - this.props.view === 'card' ? ( + element={ + view === 'card' ? ( Browse investigations @@ -375,431 +83,228 @@ class PageRouting extends React.PureComponent { {/* My Data routes */} - - {this.props.loggedInAnonymously === true ? ( - - ) : ( - - )} - + + ) : ( + + ) + } + /> - - {this.props.loggedInAnonymously === true ? ( - - ) : ( - - )} - + + ) : ( + + ) + } + /> - - {this.props.loggedInAnonymously === true ? ( - - ) : ( - - )} - + + ) : ( + + ) + } + /> - - - + } + /> {/* DLS routes */} : } /> ) => - this.props.view === 'card' ? ( - - ) : ( - - ) - } + element={view === 'card' ? : } /> ( - - {this.props.view === 'card' ? ( - - ) : ( - - )} - - )} + element={ + view === 'card' ? : + } /> ( - !values.includes(false))} - > - - - )} + element={} /> ( - - )} + element={} /> ( - - )} + element={} /> {/* ISIS dataPublications routes */} - this.props.view === 'card' ? ( - + element={ + view === 'card' ? ( + ) : ( - + ) } /> - this.props.view === 'card' ? ( - + element={ + view === 'card' ? ( + ) : ( - + ) } /> ( - - - - )} + element={} /> - this.props.view === 'card' ? ( - + element={ + view === 'card' ? ( + ) : ( - + ) } /> ( - - )} + element={} /> - this.props.view === 'card' ? ( - + element={ + view === 'card' ? ( + ) : ( - + ) } /> ( - - )} + element={} /> ( - - )} + element={} /> ( - - )} + element={} /> {/* ISIS routes */} - this.props.view === 'card' ? ( - + element={ + view === 'card' ? ( + ) : ( - + ) } /> - this.props.view === 'card' ? ( - + element={ + view === 'card' ? ( + ) : ( - + ) } /> - this.props.view === 'card' ? ( - + element={ + view === 'card' ? ( + ) : ( - + ) } /> ( - - )} + element={} /> - this.props.view === 'card' ? ( - + element={ + view === 'card' ? ( + ) : ( - + ) } /> ( - - )} + element={} /> ( - - )} + element={} /> ( - - )} + element={} /> {/* Generic routes */} : } /> - this.props.view === 'card' ? ( - - ) : ( - - ) - } + element={view === 'card' ? : } /> - ( - - - - )} - /> - - ); - } -} + } /> + + + ); +}; + +const ConnectedPageRouting = () => { + const anonUserName = useSelector( + (state: StateType) => state.dgcommon.anonUserName + ); + // Determine whether logged in anonymously (assume this if username is null) + const username = readSciGatewayToken().username; + const loggedInAnonymously = + username === null || username === (anonUserName ?? 'anon/anon'); + + const location = useLocation(); + const { view } = React.useMemo( + () => parseSearchToQuery(location.search), + [location.search] + ); + + return ; +}; -export default PageRouting; +export default ConnectedPageRouting; diff --git a/packages/datagateway-dataview/src/page/redirect.component.test.tsx b/packages/datagateway-dataview/src/page/redirect.component.test.tsx index 3dc393df9..3f2c61d7e 100644 --- a/packages/datagateway-dataview/src/page/redirect.component.test.tsx +++ b/packages/datagateway-dataview/src/page/redirect.component.test.tsx @@ -1,5 +1,6 @@ import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { render, screen, type RenderResult } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; import { Datafile, Dataset, @@ -7,9 +8,8 @@ import { NotificationType, useEntity, } from 'datagateway-common'; -import { History, createLocation, createMemoryHistory } from 'history'; import log from 'loglevel'; -import { Route, Router } from 'react-router-dom'; +import { BrowserRouter, Link, Route, Routes } from 'react-router'; import { AnyAction } from 'redux'; import { paths } from './pageContainer.component'; import { DoiRedirect, GenericRedirect } from './redirect.component'; @@ -25,7 +25,6 @@ vi.mock('datagateway-common', async () => { }); describe('Redirect component', () => { - let history: History; let mockInvestigationData: Investigation; let mockDatasetData: Dataset; let mockDatafileData: Datafile; @@ -102,25 +101,24 @@ describe('Redirect component', () => { describe('DOI Redirect component', () => { function renderComponent(): RenderResult { return render( - + - - - + + } /> + + - + ); } beforeEach(() => { - history = createMemoryHistory({ - initialEntries: [createLocation('/doi-redirect/LILS/investigation/1')], - }); + window.history.replaceState({}, '', '/doi-redirect/LILS/investigation/1'); }); it('redirects to correct link when everything loads correctly', async () => { renderComponent(); - expect(history.location.pathname).toBe( + expect(window.location.pathname).toBe( '/browse/instrument/2/facilityCycle/3/investigation/1/dataset' ); }); @@ -149,7 +147,7 @@ describe('Redirect component', () => { }); renderComponent(); - expect(history.location.pathname).toBe('/datagateway'); + expect(window.location.pathname).toBe('/datagateway'); expect(log.error).toHaveBeenCalledWith('Invalid redirect'); expect(events.length).toBe(1); expect(events[0].detail).toEqual({ @@ -164,32 +162,51 @@ describe('Redirect component', () => { }); describe('Generic Redirect component', () => { + let stateTestLinkLocation = ''; function renderComponent(): RenderResult { return render( - + - - - + + } + /> + + Test link + + } + /> + + - + ); } beforeEach(() => { - history = createMemoryHistory({ - initialEntries: [createLocation('/redirect/LILS/investigation/name/1')], - }); + stateTestLinkLocation = ''; + window.history.replaceState( + {}, + '', + '/redirect/LILS/investigation/name/1' + ); }); it('redirects to correct link when everything loads correctly', async () => { - history.replace('/redirect/LILS/datafile/name/3'); + window.history.replaceState({}, '', '/redirect/LILS/datafile/name/3'); renderComponent(); - expect(history.location.pathname).toBe( + expect(window.location.pathname).toBe( '/browse/investigation/1/dataset/2/datafile' ); - expect(history.location.search).toBe( + expect(window.location.search).toBe( `?filters=${encodeURIComponent( '{"name":{"value":"datafile3","type":"exact"}}' )}` @@ -201,14 +218,15 @@ describe('Redirect component', () => { { filterType: 'include', filterValue: JSON.stringify(['dataset.investigation', 'dataset']), - } + }, + true ); }); it('redirects to correct link when everything loads correctly (ISIS hierarchy)', async () => { - history.replace('/redirect/ISIS/dataset/name/2'); + window.history.replaceState({}, '', '/redirect/ISIS/dataset/name/2'); renderComponent(); - expect(history.location.pathname).toBe( + expect(window.location.pathname).toBe( '/browse/instrument/2/facilityCycle/3/investigation/1/dataset/2/datafile' ); expect(vi.mocked(useEntity, { partial: true })).toHaveBeenCalledWith( @@ -222,28 +240,34 @@ describe('Redirect component', () => { 'investigation.investigationInstruments.instrument', 'investigation.investigationFacilityCycles.facilityCycle', ]), - } + }, + true ); }); it('redirects to correct link when everything loads correctly (DLS hierarchy)', async () => { - history.replace('/redirect/DLS/investigation/visitId/1'); + window.history.replaceState( + {}, + '', + '/redirect/DLS/investigation/visitId/1' + ); renderComponent(); - expect(history.location.pathname).toBe( + expect(window.location.pathname).toBe( '/browse/proposal/investigation1/investigation/1/dataset' ); expect(vi.mocked(useEntity, { partial: true })).toHaveBeenCalledWith( 'investigation', 'visitId', '1', - undefined + undefined, + true ); }); it('redirects to correct link when everything loads correctly (DLS hierarchy at dataset level)', async () => { - history.replace('/redirect/DLS/dataset/name/2'); + window.history.replaceState({}, '', '/redirect/DLS/dataset/name/2'); renderComponent(); - expect(history.location.pathname).toBe( + expect(window.location.pathname).toBe( '/browse/proposal/investigation1/investigation/1/dataset/2/datafile' ); expect(vi.mocked(useEntity, { partial: true })).toHaveBeenCalledWith( @@ -253,12 +277,13 @@ describe('Redirect component', () => { { filterType: 'include', filterValue: JSON.stringify(['investigation']), - } + }, + true ); }); it('displays loading spinner when things are loading', async () => { - history.replace('/redirect/ISIS/datafile/name/3'); + window.history.replaceState({}, '', '/redirect/ISIS/datafile/name/3'); vi.mocked(useEntity, { partial: true }).mockReturnValue({ data: undefined, isPending: true, @@ -279,12 +304,17 @@ describe('Redirect component', () => { 'dataset.investigation.investigationInstruments.instrument', 'dataset.investigation.investigationFacilityCycles.facilityCycle', ]), - } + }, + true ); }); it('throws error and redirects to homepage if no investigation is returned', async () => { - history.replace('/redirect/ISIS/investigation/name/1'); + window.history.replaceState( + {}, + '', + '/redirect/ISIS/investigation/name/1' + ); const events: CustomEvent[] = []; document.dispatchEvent = (e: Event) => { @@ -307,9 +337,10 @@ describe('Redirect component', () => { investigationInstruments: 'instrument', investigationFacilityCycles: 'facilityCycle', }), - } + }, + true ); - expect(history.location.pathname).toBe('/datagateway'); + expect(window.location.pathname).toBe('/datagateway'); expect(log.error).toHaveBeenCalledWith('Invalid redirect'); expect(events.length).toBe(1); expect(events[0].detail).toEqual({ @@ -323,10 +354,8 @@ describe('Redirect component', () => { }); it('throws error and redirects to homepage if no investigation is returned with fromDataPublication true', async () => { - history.replace({ - pathname: '/redirect/DLS/investigation/id/1', - state: { fromDataPublication: true }, - }); + stateTestLinkLocation = '/redirect/DLS/investigation/id/1'; + window.history.replaceState({}, '', '/state-test'); const events: CustomEvent[] = []; document.dispatchEvent = (e: Event) => { @@ -337,15 +366,18 @@ describe('Redirect component', () => { data: undefined, isPending: false, }); + const user = userEvent.setup(); renderComponent(); + await user.click(screen.getByRole('link')); expect(vi.mocked(useEntity, { partial: true })).toHaveBeenCalledWith( 'investigation', 'id', '1', - undefined + undefined, + true ); - expect(history.location.pathname).toBe('/datagateway'); + expect(window.location.pathname).toBe('/datagateway'); expect(log.error).toHaveBeenCalledWith('Invalid redirect'); expect(events.length).toBe(1); expect(events[0].detail).toEqual({ diff --git a/packages/datagateway-dataview/src/page/redirect.component.tsx b/packages/datagateway-dataview/src/page/redirect.component.tsx index 07be03a41..1578e0a87 100644 --- a/packages/datagateway-dataview/src/page/redirect.component.tsx +++ b/packages/datagateway-dataview/src/page/redirect.component.tsx @@ -12,7 +12,7 @@ import { } from 'datagateway-common'; import log from 'loglevel'; import React from 'react'; -import { Redirect, useLocation, useParams } from 'react-router-dom'; +import { Navigate, useLocation, useParams } from 'react-router'; import { paths } from './pageContainer.component'; export const RedirectComponent: React.FC<{ @@ -39,7 +39,7 @@ export const RedirectComponent: React.FC<{ return ( - + ); }; @@ -51,7 +51,8 @@ type DoiRedirectRouteParams = { }; export const DoiRedirect: React.FC = () => { - const { entityName, entityId } = useParams(); + const { entityName = '', entityId = '' } = + useParams(); const { data: investigation, isPending: isInvestigationLoading } = useEntity( 'investigation', @@ -94,16 +95,21 @@ type GenericRedirectRouteParams = { }; export const GenericRedirect: React.FC = () => { - const { facilityName, entityName, entityField, fieldValue } = - useParams(); + const { + facilityName = '', + entityName, + entityField = '', + fieldValue = '', + } = useParams(); - const { state } = useLocation<{ fromDataPublication?: boolean }>(); + const location = useLocation(); + const state = location.state as { fromDataPublication?: boolean } | undefined; const isISIS = facilityName.toLowerCase() === FACILITY_NAME.isis.toLowerCase(); const { data: entity, isPending: isEntityLoading } = useEntity( - entityName, + entityName as GenericRedirectRouteParams['entityName'], entityField, decodeURIComponent(fieldValue), // call decodeURIComponent here to e.g. allow URL encoding of slashes to search for datafile locations etc. entityName === 'investigation' @@ -143,7 +149,8 @@ export const GenericRedirect: React.FC = () => { : []), ]), } - : undefined + : undefined, + typeof entityName !== 'undefined' ); const redirectUrl = diff --git a/packages/datagateway-dataview/src/page/translatedHomePage.component.test.tsx b/packages/datagateway-dataview/src/page/translatedHomePage.component.test.tsx index 0f54d775d..5de2ba71c 100644 --- a/packages/datagateway-dataview/src/page/translatedHomePage.component.test.tsx +++ b/packages/datagateway-dataview/src/page/translatedHomePage.component.test.tsx @@ -1,5 +1,5 @@ import { render, screen } from '@testing-library/react'; -import { MemoryRouter } from 'react-router-dom'; +import { MemoryRouter } from 'react-router'; import { TranslatedHomePage as HomePage, TranslatedHomePageStateProps, diff --git a/packages/datagateway-dataview/src/page/translatedHomePage.component.tsx b/packages/datagateway-dataview/src/page/translatedHomePage.component.tsx index b80ac26ac..a9a19ebcb 100644 --- a/packages/datagateway-dataview/src/page/translatedHomePage.component.tsx +++ b/packages/datagateway-dataview/src/page/translatedHomePage.component.tsx @@ -14,8 +14,8 @@ import { connect } from 'react-redux'; import { StateType } from '../state/app.types'; export interface TranslatedHomePageStateProps { - facilityImageURL?: string | undefined; - pluginHost: string | undefined; + facilityImageURL?: string; + pluginHost?: string; } export const TranslatedHomePage = React.memo( diff --git a/packages/datagateway-dataview/src/page/withIdCheck.test.tsx b/packages/datagateway-dataview/src/page/withIdCheck.test.tsx index c1739a1f1..9a9160813 100644 --- a/packages/datagateway-dataview/src/page/withIdCheck.test.tsx +++ b/packages/datagateway-dataview/src/page/withIdCheck.test.tsx @@ -1,6 +1,6 @@ import { act, render } from '@testing-library/react'; import React from 'react'; -import { MemoryRouter } from 'react-router-dom'; +import { MemoryRouter } from 'react-router'; import type { Mock, MockInstance } from 'vitest'; import { flushPromises } from '../setupTests'; import WithIdCheck from './withIdCheck'; diff --git a/packages/datagateway-dataview/src/page/withIdCheck.tsx b/packages/datagateway-dataview/src/page/withIdCheck.tsx index efa90f70e..b714f48ab 100644 --- a/packages/datagateway-dataview/src/page/withIdCheck.tsx +++ b/packages/datagateway-dataview/src/page/withIdCheck.tsx @@ -2,7 +2,7 @@ import BugReport from '@mui/icons-material/BugReport'; import { CircularProgress, Grid, Link, Typography } from '@mui/material'; import React from 'react'; import { Trans, useTranslation } from 'react-i18next'; -import { Link as RouterLink, useLocation } from 'react-router-dom'; +import { Link as RouterLink, useLocation } from 'react-router'; const containerStyle = { height: '100%' }; @@ -16,17 +16,19 @@ const WithIdCheck: React.FC<{ const [t] = useTranslation(); React.useEffect(() => { - checkingPromise - .then((valid) => { - setValid(valid); - }) - .catch(() => { - setValid(false); - }) - .finally(() => { - setLoading(false); - }); - }, [checkingPromise]); + // only want to run then/catch handlers on promise once, so only run when promise finally has not run + if (loading) + checkingPromise + .then((valid) => { + setValid(valid); + }) + .catch(() => { + setValid(false); + }) + .finally(() => { + setLoading(false); + }); + }, [checkingPromise, loading]); const location = useLocation(); diff --git a/packages/datagateway-dataview/src/setupTests.ts b/packages/datagateway-dataview/src/setupTests.ts index 53d27b555..03e1289a0 100644 --- a/packages/datagateway-dataview/src/setupTests.ts +++ b/packages/datagateway-dataview/src/setupTests.ts @@ -20,10 +20,6 @@ vi.setConfig({ testTimeout: 20_000 }); // and https://github.com/testing-library/user-event/issues/1115 vi.stubGlobal('jest', { advanceTimersByTime: vi.advanceTimersByTime.bind(vi) }); -function noOp(): void { - // required as work-around for jsdom environment not implementing window.URL.createObjectURL method -} - // Add in ResizeObserver as it's not in jsdom's environment vi.stubGlobal( 'ResizeObserver', @@ -34,10 +30,6 @@ vi.stubGlobal( })) ); -if (typeof window.URL.createObjectURL === 'undefined') { - Object.defineProperty(window.URL, 'createObjectURL', { value: noOp }); -} - // these are used for testing async actions export let actions: Action[] = []; export const resetActions = (): void => { diff --git a/packages/datagateway-dataview/src/views/card/datasetCardView.component.test.tsx b/packages/datagateway-dataview/src/views/card/datasetCardView.component.test.tsx index 5dba66b76..d091c214b 100644 --- a/packages/datagateway-dataview/src/views/card/datasetCardView.component.test.tsx +++ b/packages/datagateway-dataview/src/views/card/datasetCardView.component.test.tsx @@ -12,11 +12,11 @@ import { useDatasetsPaginated, type Dataset, } from 'datagateway-common'; -import { createMemoryHistory, type History } from 'history'; import { Provider } from 'react-redux'; -import { Router } from 'react-router-dom'; +import { BrowserRouter, Route, Routes, generatePath } from 'react-router'; import configureStore from 'redux-mock-store'; import thunk from 'redux-thunk'; +import { paths } from '../../page/pageContainer.component'; import type { StateType } from '../../state/app.types'; import { initialState as dgDataViewInitialState } from '../../state/reducers/dgdataview.reducer'; import DatasetCardView from './datasetCardView.component'; @@ -32,21 +32,31 @@ vi.mock('datagateway-common', async () => { }; }); +vi.mock('../../../page/idCheckFunctions', () => ({ + checkInstrumentId: vi.fn().mockResolvedValue(true), + checkStudyDataPublicationId: vi.fn().mockResolvedValue(true), + checkInstrumentAndFacilityCycleId: vi.fn().mockResolvedValue(true), +})); + describe('Dataset - Card View', () => { const mockStore = configureStore([thunk]); let state: StateType; let cardData: Dataset[]; - let history: History; let user: ReturnType; const renderComponent = (): RenderResult => render( - + - + + } + /> + - + ); @@ -62,7 +72,11 @@ describe('Dataset - Card View', () => { fileCount: 1, }, ]; - history = createMemoryHistory(); + window.history.replaceState( + {}, + '', + generatePath(paths.toggle.dataset, { investigationId: '1' }) + ); user = userEvent.setup(); state = JSON.parse( @@ -183,7 +197,7 @@ describe('Dataset - Card View', () => { await user.type(filter, 'test'); - expect(history.location.search).toBe( + expect(window.location.search).toBe( `?filters=${encodeURIComponent( '{"name":{"value":"test","type":"include"}}' )}` @@ -191,7 +205,7 @@ describe('Dataset - Card View', () => { await user.clear(filter); - expect(history.location.search).toBe('?'); + expect(window.location.search).toBe(''); }); it('updates filter query params on date filter', async () => { @@ -208,7 +222,7 @@ describe('Dataset - Card View', () => { await user.type(filter, '2019-08-06'); - expect(history.location.search).toBe( + expect(window.location.search).toBe( `?filters=${encodeURIComponent('{"modTime":{"endDate":"2019-08-06"}}')}` ); @@ -217,7 +231,7 @@ describe('Dataset - Card View', () => { await user.keyboard('{Control}a{/Control}'); await user.keyboard('{Delete}'); - expect(history.location.search).toBe('?'); + expect(window.location.search).toBe(''); }); it('updates sort query params on sort', async () => { @@ -227,7 +241,7 @@ describe('Dataset - Card View', () => { await screen.findByRole('button', { name: 'Sort by DATASETS.NAME' }) ); - expect(history.location.search).toBe( + expect(window.location.search).toBe( `?sort=${encodeURIComponent('{"name":"asc"}')}` ); }); diff --git a/packages/datagateway-dataview/src/views/card/datasetCardView.component.tsx b/packages/datagateway-dataview/src/views/card/datasetCardView.component.tsx index 9750a381c..0fd4481d3 100644 --- a/packages/datagateway-dataview/src/views/card/datasetCardView.component.tsx +++ b/packages/datagateway-dataview/src/views/card/datasetCardView.component.tsx @@ -17,13 +17,15 @@ import { } from 'datagateway-common'; import React from 'react'; import { useTranslation } from 'react-i18next'; -import { useLocation } from 'react-router-dom'; +import { useLocation, useParams } from 'react-router'; -interface DatasetCardViewProps { +interface BaseDatasetCardViewProps { investigationId: string; } -const DatasetCardView = (props: DatasetCardViewProps): React.ReactElement => { +const BaseDatasetCardView = ( + props: BaseDatasetCardViewProps +): React.ReactElement => { const { investigationId } = props; const [t] = useTranslation(); @@ -141,4 +143,9 @@ const DatasetCardView = (props: DatasetCardViewProps): React.ReactElement => { ); }; +const DatasetCardView = () => { + const { investigationId = '' } = useParams(); + return ; +}; + export default DatasetCardView; diff --git a/packages/datagateway-dataview/src/views/card/dls/dlsDatasetsCardView.component.test.tsx b/packages/datagateway-dataview/src/views/card/dls/dlsDatasetsCardView.component.test.tsx index 9f3770dca..1b64a9b6a 100644 --- a/packages/datagateway-dataview/src/views/card/dls/dlsDatasetsCardView.component.test.tsx +++ b/packages/datagateway-dataview/src/views/card/dls/dlsDatasetsCardView.component.test.tsx @@ -1,5 +1,5 @@ import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; -import { RenderResult, render, screen } from '@testing-library/react'; +import { RenderResult, act, render, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import axios, { AxiosResponse } from 'axios'; import { @@ -8,11 +8,12 @@ import { useDatasetCount, useDatasetsPaginated, } from 'datagateway-common'; -import { History, createMemoryHistory } from 'history'; import { Provider } from 'react-redux'; -import { Router } from 'react-router-dom'; +import { BrowserRouter, Route, Routes, generatePath } from 'react-router'; import configureStore from 'redux-mock-store'; import thunk from 'redux-thunk'; +import { paths } from '../../../page/pageContainer.component'; +import { flushPromises } from '../../../setupTests'; import { StateType } from '../../../state/app.types'; import { initialState as dgDataViewInitialState } from '../../../state/reducers/dgdataview.reducer'; import DLSDatasetsCardView from './dlsDatasetsCardView.component'; @@ -28,21 +29,29 @@ vi.mock('datagateway-common', async () => { }; }); +vi.mock('../../../page/idCheckFunctions', () => ({ + checkProposalName: vi.fn().mockResolvedValue(true), +})); + describe('DLS Datasets - Card View', () => { const mockStore = configureStore([thunk]); let state: StateType; let cardData: Dataset[]; - let history: History; let user: ReturnType; const renderComponent = (): RenderResult => render( - + - + + } + /> + - + ); @@ -57,7 +66,14 @@ describe('DLS Datasets - Card View', () => { fileCount: 1, }, ]; - history = createMemoryHistory(); + window.history.replaceState( + {}, + '', + generatePath(paths.toggle.dlsDataset, { + investigationId: '1', + proposalName: 'test', + }) + ); user = userEvent.setup(); state = JSON.parse( @@ -111,7 +127,7 @@ describe('DLS Datasets - Card View', () => { await user.type(filter, 'test'); - expect(history.location.search).toBe( + expect(window.location.search).toContain( `?filters=${encodeURIComponent( '{"name":{"value":"test","type":"include"}}' )}` @@ -119,7 +135,7 @@ describe('DLS Datasets - Card View', () => { await user.clear(filter); - expect(history.location.search).toBe('?'); + expect(window.location.search).not.toContain('filters='); }); it('updates filter query params on date filter', async () => { @@ -136,7 +152,7 @@ describe('DLS Datasets - Card View', () => { await user.type(filter, '2019-08-06'); - expect(history.location.search).toBe( + expect(window.location.search).toContain( `?filters=${encodeURIComponent('{"endDate":{"endDate":"2019-08-06"}}')}` ); @@ -145,7 +161,7 @@ describe('DLS Datasets - Card View', () => { await user.keyboard('{Control}a{/Control}'); await user.keyboard('{Delete}'); - expect(history.location.search).toBe('?'); + expect(window.location.search).not.toContain('filters='); }); it('uses default sort', async () => { @@ -153,18 +169,20 @@ describe('DLS Datasets - Card View', () => { expect(await screen.findByTestId('card')).toBeInTheDocument(); - expect(history.length).toBe(1); - expect(history.location.search).toBe( + await act(async () => { + await flushPromises(); + }); + + expect(window.location.search).toBe( `?sort=${encodeURIComponent('{"name":"asc"}')}` ); - // check that the data request is sent only once after mounting - expect(useDatasetsPaginated).toHaveBeenCalledTimes(2); - expect(useDatasetsPaginated).toHaveBeenCalledWith(expect.anything(), false); - expect(useDatasetsPaginated).toHaveBeenLastCalledWith( - expect.anything(), - true - ); + // check that the data hook is only called once with the query enabled + expect( + vi + .mocked(useDatasetsPaginated) + .mock.calls.filter((call) => call[1] === true) + ).toHaveLength(1); }); it('updates sort query params on sort', async () => { @@ -174,7 +192,7 @@ describe('DLS Datasets - Card View', () => { await screen.findByRole('button', { name: 'Sort by DATASETS.NAME' }) ); - expect(history.location.search).toBe( + expect(window.location.search).toBe( `?sort=${encodeURIComponent('{"name":"desc"}')}` ); }, 10000); @@ -196,10 +214,13 @@ describe('DLS Datasets - Card View', () => { ).toBeInTheDocument(); }); - it('renders fine with incomplete data', () => { + it('renders fine with incomplete data', async () => { vi.mocked(useDatasetCount, { partial: true }).mockReturnValueOnce({}); vi.mocked(useDatasetsPaginated, { partial: true }).mockReturnValueOnce({}); expect(() => renderComponent()).not.toThrowError(); + expect( + await screen.findByTestId('dls-datasets-card-view') + ).toBeInTheDocument(); }); }); diff --git a/packages/datagateway-dataview/src/views/card/dls/dlsDatasetsCardView.component.tsx b/packages/datagateway-dataview/src/views/card/dls/dlsDatasetsCardView.component.tsx index 293e7f22e..531b7a18a 100644 --- a/packages/datagateway-dataview/src/views/card/dls/dlsDatasetsCardView.component.tsx +++ b/packages/datagateway-dataview/src/views/card/dls/dlsDatasetsCardView.component.tsx @@ -22,14 +22,18 @@ import React from 'react'; import ConfirmationNumberIcon from '@mui/icons-material/ConfirmationNumber'; import { useTranslation } from 'react-i18next'; -import { useLocation } from 'react-router-dom'; +import { useLocation, useParams } from 'react-router'; +import { checkProposalName } from '../../../page/idCheckFunctions'; +import WithIdCheck from '../../../page/withIdCheck'; -interface DLSDatasetsCVProps { +interface BaseDLSDatasetsCVProps { proposalName: string; investigationId: string; } -const DLSDatasetsCardView = (props: DLSDatasetsCVProps): React.ReactElement => { +const BaseDLSDatasetsCardView = ( + props: BaseDLSDatasetsCVProps +): React.ReactElement => { const { proposalName, investigationId } = props; const [t] = useTranslation(); @@ -47,13 +51,14 @@ const DLSDatasetsCardView = (props: DLSDatasetsCVProps): React.ReactElement => { const pushPage = usePushPage(); const pushResults = usePushResults(); - // isMounted is used to disable queries when the component isn't fully mounted. + // isInitialised is used to disable queries when the component isn't fully initialised. // It prevents the request being sent twice if default sort is set. // It is not needed for cards/tables that don't have default sort. - const [isMounted, setIsMounted] = React.useState(false); + const [isInitialised, setIsInitialised] = React.useState(false); + React.useEffect(() => { - setIsMounted(true); - }, []); + if (!isInitialised && Object.keys(sort).length > 0) setIsInitialised(true); + }, [isInitialised, sort]); const { data: totalDataCount, isPending: countLoading } = useDatasetCount([ { @@ -72,7 +77,7 @@ const DLSDatasetsCardView = (props: DLSDatasetsCVProps): React.ReactElement => { }), }, ], - isMounted + isInitialised ); const title: CardViewDetails = React.useMemo( @@ -184,4 +189,21 @@ const DLSDatasetsCardView = (props: DLSDatasetsCVProps): React.ReactElement => { ); }; +const DLSDatasetsCardView = () => { + const { proposalName = '', investigationId = '' } = useParams(); + return ( + + + + ); +}; + export default DLSDatasetsCardView; diff --git a/packages/datagateway-dataview/src/views/card/dls/dlsProposalsCardView.component.test.tsx b/packages/datagateway-dataview/src/views/card/dls/dlsProposalsCardView.component.test.tsx index 9b8f2e881..d562fb2df 100644 --- a/packages/datagateway-dataview/src/views/card/dls/dlsProposalsCardView.component.test.tsx +++ b/packages/datagateway-dataview/src/views/card/dls/dlsProposalsCardView.component.test.tsx @@ -7,9 +7,8 @@ import { useInvestigationsPaginated, type Investigation, } from 'datagateway-common'; -import { createMemoryHistory, type History } from 'history'; import { Provider } from 'react-redux'; -import { Router } from 'react-router-dom'; +import { BrowserRouter } from 'react-router'; import configureStore from 'redux-mock-store'; import thunk from 'redux-thunk'; import type { StateType } from '../../../state/app.types'; @@ -31,17 +30,16 @@ describe('DLS Proposals - Card View', () => { const mockStore = configureStore([thunk]); let state: StateType; let cardData: Investigation[]; - let history: History; let user: ReturnType; const renderComponent = (): RenderResult => render( - + - + ); @@ -54,7 +52,6 @@ describe('DLS Proposals - Card View', () => { visitId: '1', }, ]; - history = createMemoryHistory(); user = userEvent.setup(); state = JSON.parse( @@ -96,7 +93,7 @@ describe('DLS Proposals - Card View', () => { await user.type(filter, 'test'); - expect(history.location.search).toBe( + expect(window.location.search).toContain( `?filters=${encodeURIComponent( '{"title":{"value":"test","type":"include"}}' )}` @@ -104,7 +101,7 @@ describe('DLS Proposals - Card View', () => { await user.clear(filter); - expect(history.location.search).toBe('?'); + expect(window.location.search).not.toContain('filters='); }); it('uses default sort', async () => { @@ -112,8 +109,7 @@ describe('DLS Proposals - Card View', () => { expect(await screen.findByTestId('card')).toBeInTheDocument(); - expect(history.length).toBe(1); - expect(history.location.search).toBe( + expect(window.location.search).toBe( `?sort=${encodeURIComponent('{"title":"asc"}')}` ); diff --git a/packages/datagateway-dataview/src/views/card/dls/dlsProposalsCardView.component.tsx b/packages/datagateway-dataview/src/views/card/dls/dlsProposalsCardView.component.tsx index 2c766c51f..41cfc9fc3 100644 --- a/packages/datagateway-dataview/src/views/card/dls/dlsProposalsCardView.component.tsx +++ b/packages/datagateway-dataview/src/views/card/dls/dlsProposalsCardView.component.tsx @@ -14,7 +14,7 @@ import { } from 'datagateway-common'; import React from 'react'; import { useTranslation } from 'react-i18next'; -import { useLocation } from 'react-router-dom'; +import { useLocation } from 'react-router'; const DLSProposalsCardView = (): React.ReactElement => { const [t] = useTranslation(); @@ -31,13 +31,14 @@ const DLSProposalsCardView = (): React.ReactElement => { const pushPage = usePushPage(); const pushResults = usePushResults(); - // isMounted is used to disable queries when the component isn't fully mounted. + // isInitialised is used to disable queries when the component isn't fully initialised. // It prevents the request being sent twice if default sort is set. // It is not needed for cards/tables that don't have default sort. - const [isMounted, setIsMounted] = React.useState(false); + const [isInitialised, setIsInitialised] = React.useState(false); + React.useEffect(() => { - setIsMounted(true); - }, []); + if (!isInitialised && Object.keys(sort).length > 0) setIsInitialised(true); + }, [isInitialised, sort]); const { data: totalDataCount, isPending: countLoading } = useInvestigationCount([ @@ -56,7 +57,7 @@ const DLSProposalsCardView = (): React.ReactElement => { // Do not add order by id as id is not a distinct field above and will otherwise // cause missing results true, - isMounted + isInitialised ); const title: CardViewDetails = React.useMemo( diff --git a/packages/datagateway-dataview/src/views/card/dls/dlsVisitsCardView.component.test.tsx b/packages/datagateway-dataview/src/views/card/dls/dlsVisitsCardView.component.test.tsx index 42f149d8d..4a0de4adb 100644 --- a/packages/datagateway-dataview/src/views/card/dls/dlsVisitsCardView.component.test.tsx +++ b/packages/datagateway-dataview/src/views/card/dls/dlsVisitsCardView.component.test.tsx @@ -1,5 +1,5 @@ import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; -import { render, screen, type RenderResult } from '@testing-library/react'; +import { act, render, screen, type RenderResult } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import axios, { AxiosResponse } from 'axios'; import { @@ -8,11 +8,12 @@ import { useInvestigationsPaginated, type Investigation, } from 'datagateway-common'; -import { createMemoryHistory, type History } from 'history'; import { Provider } from 'react-redux'; -import { Router } from 'react-router-dom'; +import { BrowserRouter, Route, Routes, generatePath } from 'react-router'; import configureStore from 'redux-mock-store'; import thunk from 'redux-thunk'; +import { paths } from '../../../page/pageContainer.component'; +import { flushPromises } from '../../../setupTests'; import type { StateType } from '../../../state/app.types'; import { initialState as dgDataViewInitialState } from '../../../state/reducers/dgdataview.reducer'; import DLSVisitsCardView from './dlsVisitsCardView.component'; @@ -32,17 +33,21 @@ describe('DLS Visits - Card View', () => { const mockStore = configureStore([thunk]); let state: StateType; let cardData: Investigation[]; - let history: History; let user: ReturnType; const renderComponent = (): RenderResult => render( - + - + + } + /> + - + ); @@ -57,7 +62,14 @@ describe('DLS Visits - Card View', () => { fileCount: 1, }, ]; - history = createMemoryHistory(); + window.history.replaceState( + {}, + '', + generatePath(paths.toggle.dlsVisit, { + investigationId: '1', + proposalName: 'test', + }) + ); user = userEvent.setup(); state = JSON.parse( @@ -119,7 +131,7 @@ describe('DLS Visits - Card View', () => { await user.type(filter, 'test'); - expect(history.location.search).toBe( + expect(window.location.search).toContain( `?filters=${encodeURIComponent( '{"visitId":{"value":"test","type":"include"}}' )}` @@ -127,7 +139,7 @@ describe('DLS Visits - Card View', () => { await user.clear(filter); - expect(history.location.search).toBe('?'); + expect(window.location.search).not.toContain('filters='); }); it('updates filter query params on date filter', async () => { @@ -144,7 +156,7 @@ describe('DLS Visits - Card View', () => { await user.type(filter, '2019-08-06'); - expect(history.location.search).toBe( + expect(window.location.search).toContain( `?filters=${encodeURIComponent('{"endDate":{"endDate":"2019-08-06"}}')}` ); @@ -153,7 +165,7 @@ describe('DLS Visits - Card View', () => { await user.keyboard('{Control}a{/Control}'); await user.keyboard('{Delete}'); - expect(history.location.search).toBe('?'); + expect(window.location.search).not.toContain('filters='); }); it('uses default sort', async () => { @@ -161,23 +173,20 @@ describe('DLS Visits - Card View', () => { expect(await screen.findByTestId('card')).toBeInTheDocument(); - expect(history.length).toBe(1); - expect(history.location.search).toBe( + await act(async () => { + await flushPromises(); + }); + + expect(window.location.search).toBe( `?sort=${encodeURIComponent('{"startDate":"desc"}')}` ); - // check that the data request is sent only once after mounting - expect(useInvestigationsPaginated).toHaveBeenCalledTimes(2); - expect(useInvestigationsPaginated).toHaveBeenCalledWith( - expect.anything(), - undefined, - false - ); - expect(useInvestigationsPaginated).toHaveBeenLastCalledWith( - expect.anything(), - undefined, - true - ); + // check that the data hook is only called once with the query enabled + expect( + vi + .mocked(useInvestigationsPaginated) + .mock.calls.filter((call) => call[2] === true) + ).toHaveLength(1); }); it('updates sort query params on sort', async () => { @@ -189,7 +198,7 @@ describe('DLS Visits - Card View', () => { }) ); - expect(history.location.search).toBe( + expect(window.location.search).toBe( `?sort=${encodeURIComponent('{"visitId":"asc"}')}` ); }); diff --git a/packages/datagateway-dataview/src/views/card/dls/dlsVisitsCardView.component.tsx b/packages/datagateway-dataview/src/views/card/dls/dlsVisitsCardView.component.tsx index aa4c3881f..f7426efa8 100644 --- a/packages/datagateway-dataview/src/views/card/dls/dlsVisitsCardView.component.tsx +++ b/packages/datagateway-dataview/src/views/card/dls/dlsVisitsCardView.component.tsx @@ -24,13 +24,15 @@ import { useTextFilter, } from 'datagateway-common'; import { useTranslation } from 'react-i18next'; -import { useLocation } from 'react-router-dom'; +import { useLocation, useParams } from 'react-router'; -interface DLSVisitsCVProps { +interface BaseDLSVisitsCVProps { proposalName: string; } -const DLSVisitsCardView = (props: DLSVisitsCVProps): React.ReactElement => { +const BaseDLSVisitsCardView = ( + props: BaseDLSVisitsCVProps +): React.ReactElement => { const { proposalName } = props; const [t] = useTranslation(); @@ -48,13 +50,14 @@ const DLSVisitsCardView = (props: DLSVisitsCVProps): React.ReactElement => { const pushPage = usePushPage(); const pushResults = usePushResults(); - // isMounted is used to disable queries when the component isn't fully mounted. + // isInitialised is used to disable queries when the component isn't fully initialised. // It prevents the request being sent twice if default sort is set. // It is not needed for cards/tables that don't have default sort. - const [isMounted, setIsMounted] = React.useState(false); + const [isInitialised, setIsInitialised] = React.useState(false); + React.useEffect(() => { - setIsMounted(true); - }, []); + if (!isInitialised && Object.keys(sort).length > 0) setIsInitialised(true); + }, [isInitialised, sort]); const { data: totalDataCount, isPending: countLoading } = useInvestigationCount([ @@ -77,7 +80,7 @@ const DLSVisitsCardView = (props: DLSVisitsCVProps): React.ReactElement => { }, ], undefined, - isMounted + isInitialised ); const title = React.useMemo( @@ -173,4 +176,9 @@ const DLSVisitsCardView = (props: DLSVisitsCVProps): React.ReactElement => { ); }; +const DLSVisitsCardView = () => { + const { proposalName = '' } = useParams(); + return ; +}; + export default DLSVisitsCardView; diff --git a/packages/datagateway-dataview/src/views/card/investigationCardView.component.test.tsx b/packages/datagateway-dataview/src/views/card/investigationCardView.component.test.tsx index 71f617212..9be0352d0 100644 --- a/packages/datagateway-dataview/src/views/card/investigationCardView.component.test.tsx +++ b/packages/datagateway-dataview/src/views/card/investigationCardView.component.test.tsx @@ -1,42 +1,40 @@ +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import { + render, + screen, + within, + type RenderResult, +} from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import axios, { AxiosResponse } from 'axios'; import { - dGCommonInitialState, DownloadCartItem, Investigation, + dGCommonInitialState, } from 'datagateway-common'; import { Provider } from 'react-redux'; -import { Router } from 'react-router-dom'; +import { BrowserRouter } from 'react-router'; import configureStore from 'redux-mock-store'; import thunk from 'redux-thunk'; import type { StateType } from '../../state/app.types'; import { initialState as dgDataViewInitialState } from '../../state/reducers/dgdataview.reducer'; import InvestigationCardView from './investigationCardView.component'; -import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; -import { createMemoryHistory, type History } from 'history'; -import { - render, - type RenderResult, - screen, - within, -} from '@testing-library/react'; -import userEvent from '@testing-library/user-event'; -import axios, { AxiosResponse } from 'axios'; describe('Investigation - Card View', () => { const mockStore = configureStore([thunk]); let state: StateType; let cardData: Investigation[]; - let history: History; let user: ReturnType; let cartItems: DownloadCartItem[]; const renderComponent = (): RenderResult => render( - + - + ); @@ -56,7 +54,6 @@ describe('Investigation - Card View', () => { endDate: '2020-01-02', }, ]; - history = createMemoryHistory(); user = userEvent.setup(); state = JSON.parse( @@ -227,7 +224,7 @@ describe('Investigation - Card View', () => { await user.type(filter, 'test'); - expect(history.location.search).toBe( + expect(window.location.search).toBe( `?filters=${encodeURIComponent( '{"title":{"value":"test","type":"include"}}' )}` @@ -235,7 +232,7 @@ describe('Investigation - Card View', () => { await user.clear(filter); - expect(history.location.search).toBe('?'); + expect(window.location.search).toBe(''); }); it('updates filter query params on date filter', async () => { @@ -251,7 +248,7 @@ describe('Investigation - Card View', () => { await user.type(filter, '2019-08-06'); - expect(history.location.search).toBe( + expect(window.location.search).toBe( `?filters=${encodeURIComponent('{"endDate":{"endDate":"2019-08-06"}}')}` ); @@ -260,7 +257,7 @@ describe('Investigation - Card View', () => { await user.keyboard('{Control}a{/Control}'); await user.keyboard('{Delete}'); - expect(history.location.search).toBe('?'); + expect(window.location.search).toBe(''); }); it('updates sort query params on sort', async () => { @@ -272,7 +269,7 @@ describe('Investigation - Card View', () => { }) ); - expect(history.location.search).toBe( + expect(window.location.search).toBe( `?sort=${encodeURIComponent('{"title":"asc"}')}` ); }); diff --git a/packages/datagateway-dataview/src/views/card/investigationCardView.component.tsx b/packages/datagateway-dataview/src/views/card/investigationCardView.component.tsx index cd3a2dfc5..0f451ad1d 100644 --- a/packages/datagateway-dataview/src/views/card/investigationCardView.component.tsx +++ b/packages/datagateway-dataview/src/views/card/investigationCardView.component.tsx @@ -24,7 +24,7 @@ import { } from 'datagateway-common'; import React from 'react'; import { useTranslation } from 'react-i18next'; -import { useLocation } from 'react-router-dom'; +import { useLocation } from 'react-router'; const InvestigationCardView = (): React.ReactElement => { const [t] = useTranslation(); diff --git a/packages/datagateway-dataview/src/views/card/isis/isisDataPublicationsCardView.component.test.tsx b/packages/datagateway-dataview/src/views/card/isis/isisDataPublicationsCardView.component.test.tsx index 03decc6f8..81e411fa4 100644 --- a/packages/datagateway-dataview/src/views/card/isis/isisDataPublicationsCardView.component.test.tsx +++ b/packages/datagateway-dataview/src/views/card/isis/isisDataPublicationsCardView.component.test.tsx @@ -1,33 +1,40 @@ -import { type DataPublication, dGCommonInitialState } from 'datagateway-common'; -import { Provider } from 'react-redux'; -import { generatePath, Router } from 'react-router-dom'; -import configureStore from 'redux-mock-store'; -import thunk from 'redux-thunk'; -import type { StateType } from '../../../state/app.types'; -import { initialState as dgDataViewInitialState } from '../../../state/reducers/dgdataview.reducer'; -import ISISDataPublicationsCardView from './isisDataPublicationsCardView.component'; -import { createMemoryHistory, type History } from 'history'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { render, - type RenderResult, screen, within, + type RenderResult, } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import axios, { AxiosResponse } from 'axios'; +import { dGCommonInitialState, type DataPublication } from 'datagateway-common'; +import { Provider } from 'react-redux'; +import { BrowserRouter, Route, Routes, generatePath } from 'react-router'; +import configureStore from 'redux-mock-store'; +import thunk from 'redux-thunk'; import { paths } from '../../../page/pageContainer.component'; +import type { StateType } from '../../../state/app.types'; +import { initialState as dgDataViewInitialState } from '../../../state/reducers/dgdataview.reducer'; +import ISISDataPublicationsCardView from './isisDataPublicationsCardView.component'; describe('ISIS Data Publication - Card View', () => { const mockStore = configureStore([thunk]); let state: StateType; let cardData: DataPublication[]; - let history: History; let user: ReturnType; const renderComponent = (studyDataPublicationId?: string): RenderResult => { + window.history.replaceState( + {}, + '', + generatePath(paths.dataPublications.toggle.isisStudyDataPublication, { + instrumentId: 1, + }) + ); if (studyDataPublicationId) - history.replace( + window.history.replaceState( + {}, + '', generatePath( paths.dataPublications.toggle.isisInvestigationDataPublication, { @@ -38,14 +45,22 @@ describe('ISIS Data Publication - Card View', () => { ); return render( - + - + + } + /> + } + /> + - + ); }; @@ -76,13 +91,6 @@ describe('ISIS Data Publication - Card View', () => { }, }, ]; - history = createMemoryHistory({ - initialEntries: [ - generatePath(paths.dataPublications.toggle.isisStudyDataPublication, { - instrumentId: 1, - }), - ], - }); state = JSON.parse( JSON.stringify({ @@ -148,8 +156,7 @@ describe('ISIS Data Publication - Card View', () => { expect(await screen.findByTestId('card')).toBeInTheDocument(); - expect(history.length).toBe(1); - expect(history.location.search).toBe( + expect(window.location.search).toBe( `?sort=${encodeURIComponent('{"title":"desc"}')}` ); @@ -175,7 +182,7 @@ describe('ISIS Data Publication - Card View', () => { await user.type(filter, 'Test'); - expect(history.location.search).toBe( + expect(window.location.search).toContain( `?filters=${encodeURIComponent( '{"title":{"value":"Test","type":"include"}}' )}` @@ -183,7 +190,7 @@ describe('ISIS Data Publication - Card View', () => { await user.clear(filter); - expect(history.location.search).toBe('?'); + expect(window.location.search).not.toContain('filters='); }); it('updates sort query params on sort', async () => { @@ -195,7 +202,7 @@ describe('ISIS Data Publication - Card View', () => { }) ); - expect(history.location.search).toBe( + expect(window.location.search).toBe( `?sort=${encodeURIComponent('{"pid":"asc"}')}` ); }); @@ -231,8 +238,7 @@ describe('ISIS Data Publication - Card View', () => { expect(await screen.findByTestId('card')).toBeInTheDocument(); - expect(history.length).toBe(1); - expect(history.location.search).toBe( + expect(window.location.search).toBe( `?sort=${encodeURIComponent('{"publicationDate":"desc"}')}` ); @@ -256,7 +262,7 @@ describe('ISIS Data Publication - Card View', () => { }); await user.type(filterInput, '2019-08-06'); - expect(history.location.search).toBe( + expect(window.location.search).toContain( `?filters=${encodeURIComponent( '{"publicationDate":{"endDate":"2019-08-06"}}' )}` @@ -267,7 +273,7 @@ describe('ISIS Data Publication - Card View', () => { await user.keyboard('{Control}a{/Control}'); await user.keyboard('{Delete}'); - expect(history.location.search).toBe('?'); + expect(window.location.search).not.toContain('filters='); }); }); }); diff --git a/packages/datagateway-dataview/src/views/card/isis/isisDataPublicationsCardView.component.tsx b/packages/datagateway-dataview/src/views/card/isis/isisDataPublicationsCardView.component.tsx index c01e86adf..c349e5b3d 100644 --- a/packages/datagateway-dataview/src/views/card/isis/isisDataPublicationsCardView.component.tsx +++ b/packages/datagateway-dataview/src/views/card/isis/isisDataPublicationsCardView.component.tsx @@ -18,15 +18,15 @@ import { } from 'datagateway-common'; import React from 'react'; import { useTranslation } from 'react-i18next'; -import { useLocation } from 'react-router-dom'; +import { useLocation, useParams } from 'react-router'; -interface ISISDataPublicationsCVProps { +interface BaseISISDataPublicationsCVProps { instrumentId: string; studyDataPublicationId?: string; } -const ISISDataPublicationsCardView = ( - props: ISISDataPublicationsCVProps +const BaseISISDataPublicationsCardView = ( + props: BaseISISDataPublicationsCVProps ): React.ReactElement => { const { instrumentId, studyDataPublicationId } = props; @@ -45,13 +45,14 @@ const ISISDataPublicationsCardView = ( const pushPage = usePushPage(); const pushResults = usePushResults(); - // isMounted is used to disable queries when the component isn't fully mounted. + // isInitialised is used to disable queries when the component isn't fully initialised. // It prevents the request being sent twice if default sort is set. // It is not needed for cards/tables that don't have default sort. - const [isMounted, setIsMounted] = React.useState(false); + const [isInitialised, setIsInitialised] = React.useState(false); + React.useEffect(() => { - setIsMounted(true); - }, []); + if (!isInitialised && Object.keys(sort).length > 0) setIsInitialised(true); + }, [isInitialised, sort]); const { data: totalDataCount, isPending: countLoading } = useDataPublicationCount([ @@ -138,7 +139,7 @@ const ISISDataPublicationsCardView = ( }, ]), ], - isMounted + isInitialised ); const title: CardViewDetails = React.useMemo(() => { @@ -224,4 +225,14 @@ const ISISDataPublicationsCardView = ( ); }; +const ISISDataPublicationsCardView = () => { + const { instrumentId = '', studyDataPublicationId = '' } = useParams(); + return ( + + ); +}; + export default ISISDataPublicationsCardView; diff --git a/packages/datagateway-dataview/src/views/card/isis/isisDatasetsCardView.component.test.tsx b/packages/datagateway-dataview/src/views/card/isis/isisDatasetsCardView.component.test.tsx index f09e8b61d..0f98d89dd 100644 --- a/packages/datagateway-dataview/src/views/card/isis/isisDatasetsCardView.component.test.tsx +++ b/packages/datagateway-dataview/src/views/card/isis/isisDatasetsCardView.component.test.tsx @@ -1,5 +1,5 @@ import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; -import { render, screen, type RenderResult } from '@testing-library/react'; +import { act, render, screen, type RenderResult } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import axios, { AxiosResponse } from 'axios'; import { @@ -8,12 +8,12 @@ import { useDatasetsPaginated, type Dataset, } from 'datagateway-common'; -import { createMemoryHistory, type History } from 'history'; import { Provider } from 'react-redux'; -import { Router, generatePath } from 'react-router-dom'; +import { BrowserRouter, Route, Routes, generatePath } from 'react-router'; import configureStore from 'redux-mock-store'; import thunk from 'redux-thunk'; import { paths } from '../../../page/pageContainer.component'; +import { flushPromises } from '../../../setupTests'; import type { StateType } from '../../../state/app.types'; import { initialState as dgDataViewInitialState } from '../../../state/reducers/dgdataview.reducer'; import ISISDatasetsCardView from './isisDatasetsCardView.component'; @@ -29,21 +29,36 @@ vi.mock('datagateway-common', async () => { }; }); +vi.mock('../../../page/idCheckFunctions', () => ({ + checkInstrumentId: vi.fn().mockResolvedValue(true), + checkStudyDataPublicationId: vi.fn().mockResolvedValue(true), + checkInstrumentAndFacilityCycleId: vi.fn().mockResolvedValue(true), +})); + describe('ISIS Datasets - Card View', () => { const mockStore = configureStore([thunk]); let state: StateType; let cardData: Dataset[]; - let history: History; let user: ReturnType; const renderComponent = (): RenderResult => render( - + - + + } + /> + } + /> + + - + ); @@ -57,15 +72,15 @@ describe('ISIS Datasets - Card View', () => { createTime: '2019-07-23', }, ]; - history = createMemoryHistory({ - initialEntries: [ - generatePath(paths.toggle.isisDataset, { - instrumentId: '1', - investigationId: '1', - facilityCycleId: '1', - }), - ], - }); + window.history.replaceState( + {}, + '', + generatePath(paths.toggle.isisDataset, { + instrumentId: '1', + facilityCycleId: '1', + investigationId: '1', + }) + ); user = userEvent.setup(); state = JSON.parse( @@ -93,6 +108,17 @@ describe('ISIS Datasets - Card View', () => { }); } + if (/\/datapublications$/.test(url)) { + return Promise.resolve({ + data: { + id: 1, + content: { + dataCollectionInvestigations: [{ investigation: { id: 1 } }], + }, + }, + }); + } + return Promise.reject(`Endpoint not mocked: ${url}`); }); @@ -113,17 +139,21 @@ describe('ISIS Datasets - Card View', () => { }); it('correct link used for dataPublication hierarchy', async () => { - history.replace( + window.history.replaceState( + {}, + '', generatePath(paths.dataPublications.toggle.isisDataset, { instrumentId: '1', - investigationId: '1', dataPublicationId: '1', + investigationId: '1', }) ); renderComponent(); - expect(await screen.findByRole('link', { name: 'Test 1' })).toHaveAttribute( + expect( + await screen.findByRole('link', { name: 'Test 1' }, { timeout: 5_000 }) + ).toHaveAttribute( 'href', '/browseDataPublications/instrument/1/dataPublication/1/investigation/1/dataset/1' ); @@ -144,7 +174,7 @@ describe('ISIS Datasets - Card View', () => { await user.type(filter, 'test'); - expect(history.location.search).toBe( + expect(window.location.search).toContain( `?filters=${encodeURIComponent( '{"name":{"value":"test","type":"include"}}' )}` @@ -152,7 +182,7 @@ describe('ISIS Datasets - Card View', () => { await user.clear(filter); - expect(history.location.search).toBe('?'); + expect(window.location.search).not.toContain('filters='); }); it('updates filter query params on date filter', async () => { @@ -169,7 +199,7 @@ describe('ISIS Datasets - Card View', () => { await user.type(filter, '2019-08-06'); - expect(history.location.search).toBe( + expect(window.location.search).toContain( `?filters=${encodeURIComponent('{"modTime":{"endDate":"2019-08-06"}}')}` ); @@ -178,26 +208,28 @@ describe('ISIS Datasets - Card View', () => { await user.keyboard('{Control}a{/Control}'); await user.keyboard('{Delete}'); - expect(history.location.search).toBe('?'); + expect(window.location.search).not.toContain('filters='); }); it('uses default sort', async () => { renderComponent(); + await act(async () => { + await flushPromises(); + }); + expect(await screen.findByTestId('card')).toBeInTheDocument(); - expect(history.length).toBe(1); - expect(history.location.search).toBe( + expect(window.location.search).toBe( `?sort=${encodeURIComponent('{"name":"asc"}')}` ); - // check that the data request is sent only once after mounting - expect(useDatasetsPaginated).toHaveBeenCalledTimes(2); - expect(useDatasetsPaginated).toHaveBeenCalledWith(expect.anything(), false); - expect(useDatasetsPaginated).toHaveBeenLastCalledWith( - expect.anything(), - true - ); + // check that the data hook is only called once with the query enabled + expect( + vi + .mocked(useDatasetsPaginated) + .mock.calls.filter((call) => call[1] === true) + ).toHaveLength(1); }); it('updates sort query params on sort', async () => { @@ -207,7 +239,7 @@ describe('ISIS Datasets - Card View', () => { await screen.findByRole('button', { name: 'Sort by DATASETS.NAME' }) ); - expect(history.location.search).toBe( + expect(window.location.search).toBe( `?sort=${encodeURIComponent('{"name":"desc"}')}` ); }); @@ -235,15 +267,18 @@ describe('ISIS Datasets - Card View', () => { await screen.findByRole('tab', { name: 'datasets.details.datafiles' }) ); - expect(history.location.pathname).toBe( + expect(window.location.pathname).toBe( '/browse/instrument/1/facilityCycle/1/investigation/1/dataset/1/datafile' ); }); - it('renders fine with incomplete data', () => { + it('renders fine with incomplete data', async () => { vi.mocked(useDatasetCount, { partial: true }).mockReturnValueOnce({}); vi.mocked(useDatasetsPaginated, { partial: true }).mockReturnValueOnce({}); expect(() => renderComponent()).not.toThrowError(); + expect( + await screen.findByTestId('isis-datasets-card-view') + ).toBeInTheDocument(); }); }); diff --git a/packages/datagateway-dataview/src/views/card/isis/isisDatasetsCardView.component.tsx b/packages/datagateway-dataview/src/views/card/isis/isisDatasetsCardView.component.tsx index bf852417a..372696f50 100644 --- a/packages/datagateway-dataview/src/views/card/isis/isisDatasetsCardView.component.tsx +++ b/packages/datagateway-dataview/src/views/card/isis/isisDatasetsCardView.component.tsx @@ -11,6 +11,7 @@ import { formatBytes, parseSearchToQuery, tableLink, + useDataPublication, useDatasetCount, useDatasetsPaginated, useDateFilter, @@ -22,7 +23,13 @@ import { } from 'datagateway-common'; import React from 'react'; import { useTranslation } from 'react-i18next'; -import { useHistory, useLocation } from 'react-router-dom'; +import { useLocation, useNavigate, useParams } from 'react-router'; +import { + checkInstrumentAndFacilityCycleId, + checkInstrumentId, + checkStudyDataPublicationId, +} from '../../../page/idCheckFunctions'; +import WithIdCheck from '../../../page/withIdCheck'; const ActionButtonsContainer = styled('div')(({ theme }) => ({ display: 'flex', @@ -33,18 +40,18 @@ const ActionButtonsContainer = styled('div')(({ theme }) => ({ }, })); -interface ISISDatasetCardViewProps { +interface BaseISISDatasetCardViewProps { investigationId: string; } -const ISISDatasetsCardView = ( - props: ISISDatasetCardViewProps +const BaseISISDatasetsCardView = ( + props: BaseISISDatasetCardViewProps ): React.ReactElement => { const { investigationId } = props; const [t] = useTranslation(); const location = useLocation(); - const { push } = useHistory(); + const navigate = useNavigate(); const { filters, view, sort, page, results } = React.useMemo( () => parseSearchToQuery(location.search), @@ -58,13 +65,14 @@ const ISISDatasetsCardView = ( const pushPage = usePushPage(); const pushResults = usePushResults(); - // isMounted is used to disable queries when the component isn't fully mounted. + // isInitialised is used to disable queries when the component isn't fully initialised. // It prevents the request being sent twice if default sort is set. // It is not needed for cards/tables that don't have default sort. - const [isMounted, setIsMounted] = React.useState(false); + const [isInitialised, setIsInitialised] = React.useState(false); + React.useEffect(() => { - setIsMounted(true); - }, []); + if (!isInitialised && Object.keys(sort).length > 0) setIsInitialised(true); + }, [isInitialised, sort]); const { data: totalDataCount, isPending: countLoading } = useDatasetCount([ { @@ -83,7 +91,7 @@ const ISISDatasetsCardView = ( }), }, ], - isMounted + isInitialised ); const title: CardViewDetails = React.useMemo( @@ -163,11 +171,11 @@ const ISISDatasetsCardView = ( const url = view ? `${location.pathname}/${id}/datafile?view=${view}` : `${location.pathname}/${id}/datafile`; - push(url); + navigate(url); }} /> ), - [push, location.pathname, view] + [navigate, location.pathname, view] ); return ( @@ -194,4 +202,49 @@ const ISISDatasetsCardView = ( ); }; +const ISISDatasetsCardView = (props: { + dataPublication: boolean; +}): React.ReactElement => { + const { + instrumentId = '', + facilityCycleId = '', + dataPublicationId = '', + investigationId = '', + } = useParams(); + const { data, isPending } = useDataPublication( + parseInt(investigationId), + props.dataPublication + ); + + const dataPublicationInvestigationId = + data?.content?.dataCollectionInvestigations?.[0]?.investigation?.id; + + const checkingPromise = props.dataPublication + ? Promise.all([ + checkInstrumentId(parseInt(instrumentId), parseInt(dataPublicationId)), + checkStudyDataPublicationId( + parseInt(dataPublicationId), + parseInt(investigationId) + ), + ...(isPending ? [new Promise(() => undefined)] : []), + ]).then((values) => !values.includes(false)) + : checkInstrumentAndFacilityCycleId( + parseInt(instrumentId), + parseInt(facilityCycleId), + parseInt(investigationId) + ); + + return ( + + + + ); +}; + export default ISISDatasetsCardView; diff --git a/packages/datagateway-dataview/src/views/card/isis/isisFacilityCyclesCardView.component.test.tsx b/packages/datagateway-dataview/src/views/card/isis/isisFacilityCyclesCardView.component.test.tsx index 6df431e38..d8a28be19 100644 --- a/packages/datagateway-dataview/src/views/card/isis/isisFacilityCyclesCardView.component.test.tsx +++ b/packages/datagateway-dataview/src/views/card/isis/isisFacilityCyclesCardView.component.test.tsx @@ -1,18 +1,18 @@ import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; -import { render, RenderResult, screen } from '@testing-library/react'; +import { RenderResult, act, render, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { - dGCommonInitialState, FacilityCycle, + dGCommonInitialState, useFacilityCycleCount, useFacilityCyclesPaginated, } from 'datagateway-common'; -import { createMemoryHistory, History } from 'history'; import { Provider } from 'react-redux'; -import { Router } from 'react-router-dom'; +import { BrowserRouter, Route, Routes, generatePath } from 'react-router'; import configureStore from 'redux-mock-store'; import thunk from 'redux-thunk'; -import type { MockInstance } from 'vitest'; +import { paths } from '../../../page/pageContainer.component'; +import { flushPromises } from '../../../setupTests'; import { StateType } from '../../../state/app.types'; import { initialState as dgDataViewInitialState } from '../../../state/reducers/dgdataview.reducer'; import ISISFacilityCyclesCardView from './isisFacilityCyclesCardView.component'; @@ -32,18 +32,21 @@ describe('ISIS Facility Cycles - Card View', () => { const mockStore = configureStore([thunk]); let state: StateType; let cardData: FacilityCycle[]; - let history: History; - let replaceSpy: MockInstance; let user: ReturnType; const renderComponent = (): RenderResult => render( - + - + + } + /> + - + ); @@ -62,8 +65,13 @@ describe('ISIS Facility Cycles - Card View', () => { name: 'Test 1', }, ]; - history = createMemoryHistory(); - replaceSpy = vi.spyOn(history, 'replace'); + window.history.replaceState( + {}, + '', + generatePath(paths.toggle.isisFacilityCycle, { + instrumentId: '1', + }) + ); vi.mocked(useFacilityCycleCount, { partial: true }).mockReturnValue({ data: 1, @@ -97,7 +105,7 @@ describe('ISIS Facility Cycles - Card View', () => { await user.type(filter, 'test'); - expect(history.location.search).toBe( + expect(window.location.search).toContain( `?filters=${encodeURIComponent( '{"name":{"value":"test","type":"include"}}' )}` @@ -105,7 +113,7 @@ describe('ISIS Facility Cycles - Card View', () => { await user.clear(filter); - expect(history.location.search).toBe('?'); + expect(window.location.search).not.toContain('filters='); }); it('updates filter query params on date filter', async () => { @@ -122,7 +130,7 @@ describe('ISIS Facility Cycles - Card View', () => { await user.type(filter, '2019-08-06'); - expect(history.location.search).toBe( + expect(window.location.search).toContain( `?filters=${encodeURIComponent('{"endDate":{"endDate":"2019-08-06"}}')}` ); @@ -131,29 +139,28 @@ describe('ISIS Facility Cycles - Card View', () => { await user.keyboard('{Control}a{/Control}'); await user.keyboard('{Delete}'); - expect(history.location.search).toBe('?'); + expect(window.location.search).not.toContain('filters='); }); it('uses default sort', async () => { renderComponent(); - expect(await screen.findByTestId('card')).toBeInTheDocument(); - - expect(history.length).toBe(1); - expect(replaceSpy).toHaveBeenCalledWith({ - search: `?sort=${encodeURIComponent('{"startDate":"desc"}')}`, + await act(async () => { + await flushPromises(); }); - // check that the data request is sent only once after mounting - expect(useFacilityCyclesPaginated).toHaveBeenCalledTimes(2); - expect(useFacilityCyclesPaginated).toHaveBeenCalledWith( - expect.anything(), - false - ); - expect(useFacilityCyclesPaginated).toHaveBeenLastCalledWith( - expect.anything(), - true + expect(await screen.findByTestId('card')).toBeInTheDocument(); + + expect(window.location.search).toBe( + `?sort=${encodeURIComponent('{"startDate":"desc"}')}` ); + + // check that the data hook is only called once with the query enabled + expect( + vi + .mocked(useFacilityCyclesPaginated) + .mock.calls.filter((call) => call[1] === true) + ).toHaveLength(1); }); it('updates sort query params on sort', async () => { @@ -163,7 +170,7 @@ describe('ISIS Facility Cycles - Card View', () => { await screen.findByRole('button', { name: 'Sort by FACILITYCYCLES.NAME' }) ); - expect(history.location.search).toBe( + expect(window.location.search).toBe( `?sort=${encodeURIComponent('{"name":"asc"}')}` ); }); diff --git a/packages/datagateway-dataview/src/views/card/isis/isisFacilityCyclesCardView.component.tsx b/packages/datagateway-dataview/src/views/card/isis/isisFacilityCyclesCardView.component.tsx index ed4c72bdb..e4cad0ef6 100644 --- a/packages/datagateway-dataview/src/views/card/isis/isisFacilityCyclesCardView.component.tsx +++ b/packages/datagateway-dataview/src/views/card/isis/isisFacilityCyclesCardView.component.tsx @@ -16,14 +16,14 @@ import { } from 'datagateway-common'; import React from 'react'; import { useTranslation } from 'react-i18next'; -import { useLocation } from 'react-router-dom'; +import { useLocation, useParams } from 'react-router'; -interface ISISFacilityCyclesCVProps { +interface BaseISISFacilityCyclesCVProps { instrumentId: string; } -const ISISFacilityCyclesCardView = ( - props: ISISFacilityCyclesCVProps +const BaseISISFacilityCyclesCardView = ( + props: BaseISISFacilityCyclesCVProps ): React.ReactElement => { const { instrumentId } = props; const [t] = useTranslation(); @@ -41,19 +41,20 @@ const ISISFacilityCyclesCardView = ( const pushPage = usePushPage(); const pushResults = usePushResults(); - // isMounted is used to disable queries when the component isn't fully mounted. + // isInitialised is used to disable queries when the component isn't fully initialised. // It prevents the request being sent twice if default sort is set. // It is not needed for cards/tables that don't have default sort. - const [isMounted, setIsMounted] = React.useState(false); + const [isInitialised, setIsInitialised] = React.useState(false); + React.useEffect(() => { - setIsMounted(true); - }, []); + if (!isInitialised && Object.keys(sort).length > 0) setIsInitialised(true); + }, [isInitialised, sort]); const { data: totalDataCount, isPending: countLoading } = useFacilityCycleCount(parseInt(instrumentId)); const { isPending: dataLoading, data } = useFacilityCyclesPaginated( parseInt(instrumentId), - isMounted + isInitialised ); const title: CardViewDetails = React.useMemo( @@ -121,4 +122,9 @@ const ISISFacilityCyclesCardView = ( ); }; +const ISISFacilityCyclesCardView = () => { + const { instrumentId = '' } = useParams(); + return ; +}; + export default ISISFacilityCyclesCardView; diff --git a/packages/datagateway-dataview/src/views/card/isis/isisInstrumentsCardView.component.test.tsx b/packages/datagateway-dataview/src/views/card/isis/isisInstrumentsCardView.component.test.tsx index 3c1ce3416..9e826f1ec 100644 --- a/packages/datagateway-dataview/src/views/card/isis/isisInstrumentsCardView.component.test.tsx +++ b/packages/datagateway-dataview/src/views/card/isis/isisInstrumentsCardView.component.test.tsx @@ -1,5 +1,5 @@ import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; -import { render, screen, type RenderResult } from '@testing-library/react'; +import { act, render, screen, type RenderResult } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import axios, { AxiosResponse } from 'axios'; import { @@ -8,11 +8,12 @@ import { useInstrumentsPaginated, type Instrument, } from 'datagateway-common'; -import { createMemoryHistory, type History } from 'history'; import { Provider } from 'react-redux'; -import { Router } from 'react-router-dom'; +import { BrowserRouter, Route, Routes } from 'react-router'; import configureStore from 'redux-mock-store'; import thunk from 'redux-thunk'; +import { paths } from '../../../page/pageContainer.component'; +import { flushPromises } from '../../../setupTests'; import type { StateType } from '../../../state/app.types'; import { initialState as dgDataViewInitialState } from '../../../state/reducers/dgdataview.reducer'; import ISISInstrumentsCardView from './isisInstrumentsCardView.component'; @@ -32,17 +33,22 @@ describe('ISIS Instruments - Card View', () => { const mockStore = configureStore([thunk]); let state: StateType; let cardData: Instrument[]; - let history: History; let user: ReturnType; + let props: React.ComponentProps; const renderComponent = (): RenderResult => render( - + - + + } + /> + - + ); @@ -54,7 +60,8 @@ describe('ISIS Instruments - Card View', () => { name: 'Test 1', }, ]; - history = createMemoryHistory(); + window.history.replaceState({}, '', paths.toggle.isisInstrument); + props = { dataPublication: false }; state = JSON.parse( JSON.stringify({ @@ -101,15 +108,8 @@ describe('ISIS Instruments - Card View', () => { }); it('correct link used for studyHierarchy', async () => { - render( - - - - - - - - ); + props.dataPublication = true; + renderComponent(); expect(await screen.findByRole('link', { name: 'Test 1' })).toHaveAttribute( 'href', '/browseDataPublications/instrument/1/dataPublication' @@ -131,7 +131,7 @@ describe('ISIS Instruments - Card View', () => { await user.type(filter, 'test'); - expect(history.location.search).toBe( + expect(window.location.search).toContain( `?filters=${encodeURIComponent( '{"fullName":{"value":"test","type":"include"}}' )}` @@ -139,23 +139,28 @@ describe('ISIS Instruments - Card View', () => { await user.clear(filter); - expect(history.location.search).toBe('?'); + expect(window.location.search).not.toContain('filters='); }); it('uses default sort', async () => { renderComponent(); + await act(async () => { + await flushPromises(); + }); + expect(await screen.findByTestId('card')).toBeInTheDocument(); - expect(history.length).toBe(1); - expect(history.location.search).toBe( + expect(window.location.search).toBe( `?sort=${encodeURIComponent('{"fullName":"asc"}')}` ); - // check that the data request is sent only once after mounting - expect(useInstrumentsPaginated).toHaveBeenCalledTimes(2); - expect(useInstrumentsPaginated).toHaveBeenCalledWith(undefined, false); - expect(useInstrumentsPaginated).toHaveBeenLastCalledWith(undefined, true); + // check that the data hook is only called once with the query enabled + expect( + vi + .mocked(useInstrumentsPaginated) + .mock.calls.filter((call) => call[1] === true) + ).toHaveLength(1); }); it('updates sort query params on sort', async () => { @@ -165,7 +170,7 @@ describe('ISIS Instruments - Card View', () => { await screen.findByRole('button', { name: 'Sort by INSTRUMENTS.NAME' }) ); - expect(history.location.search).toBe( + expect(window.location.search).toBe( `?sort=${encodeURIComponent('{"fullName":"desc"}')}` ); }); diff --git a/packages/datagateway-dataview/src/views/card/isis/isisInstrumentsCardView.component.tsx b/packages/datagateway-dataview/src/views/card/isis/isisInstrumentsCardView.component.tsx index 3b44b2a4f..0fadcbc4f 100644 --- a/packages/datagateway-dataview/src/views/card/isis/isisInstrumentsCardView.component.tsx +++ b/packages/datagateway-dataview/src/views/card/isis/isisInstrumentsCardView.component.tsx @@ -18,7 +18,7 @@ import { } from 'datagateway-common'; import React from 'react'; import { useTranslation } from 'react-i18next'; -import { useLocation } from 'react-router-dom'; +import { useLocation } from 'react-router'; interface ISISInstrumentsCVProps { dataPublication: boolean; @@ -42,19 +42,20 @@ const ISISInstrumentsCardView = ( const pushPage = usePushPage(); const pushResults = usePushResults(); - // isMounted is used to disable queries when the component isn't fully mounted. + // isInitialised is used to disable queries when the component isn't fully initialised. // It prevents the request being sent twice if default sort is set. // It is not needed for cards/tables that don't have default sort. - const [isMounted, setIsMounted] = React.useState(false); + const [isInitialised, setIsInitialised] = React.useState(false); + React.useEffect(() => { - setIsMounted(true); - }, []); + if (!isInitialised && Object.keys(sort).length > 0) setIsInitialised(true); + }, [isInitialised, sort]); const { data: totalDataCount, isPending: countLoading } = useInstrumentCount(); const { isPending: dataLoading, data } = useInstrumentsPaginated( undefined, - isMounted + isInitialised ); const title: CardViewDetails = React.useMemo(() => { diff --git a/packages/datagateway-dataview/src/views/card/isis/isisInvestigationsCardView.component.test.tsx b/packages/datagateway-dataview/src/views/card/isis/isisInvestigationsCardView.component.test.tsx index 253c99e8e..353b84fb8 100644 --- a/packages/datagateway-dataview/src/views/card/isis/isisInvestigationsCardView.component.test.tsx +++ b/packages/datagateway-dataview/src/views/card/isis/isisInvestigationsCardView.component.test.tsx @@ -9,30 +9,30 @@ import { import userEvent from '@testing-library/user-event'; import axios, { type AxiosResponse } from 'axios'; import { dGCommonInitialState, type Investigation } from 'datagateway-common'; -import { createMemoryHistory, type History } from 'history'; import { Provider } from 'react-redux'; -import { Router, generatePath } from 'react-router-dom'; +import { BrowserRouter, Route, Routes, generatePath } from 'react-router'; import configureStore from 'redux-mock-store'; import thunk from 'redux-thunk'; -import type { MockInstance } from 'vitest'; import { paths } from '../../../page/pageContainer.component'; import { flushPromises } from '../../../setupTests'; import type { StateType } from '../../../state/app.types'; import { initialState as dgDataViewInitialState } from '../../../state/reducers/dgdataview.reducer'; import ISISInvestigationsCardView from './isisInvestigationsCardView.component'; +vi.mock('../../../page/idCheckFunctions', () => ({ + checkInstrumentAndFacilityCycleId: vi.fn().mockResolvedValue(true), +})); + describe('ISIS Investigations - Card View', () => { const mockStore = configureStore([thunk]); let state: StateType; let cardData: Investigation[]; - let history: History; - let replaceSpy: MockInstance; let user: ReturnType; const renderComponent = (): RenderResult => render( - + { }) } > - + + } + /> + + - + ); @@ -109,15 +115,14 @@ describe('ISIS Investigations - Card View', () => { ], }, ]; - history = createMemoryHistory({ - initialEntries: [ - generatePath(paths.toggle.isisInvestigation, { - instrumentId: '1', - facilityCycleId: '1', - }), - ], - }); - replaceSpy = vi.spyOn(history, 'replace'); + window.history.replaceState( + {}, + '', + generatePath(paths.toggle.isisInvestigation, { + instrumentId: '1', + facilityCycleId: '1', + }) + ); user = userEvent.setup(); state = JSON.parse( @@ -263,7 +268,7 @@ describe('ISIS Investigations - Card View', () => { await user.type(filter, 'test'); - expect(history.location.search).toBe( + expect(window.location.search).toContain( `?filters=${encodeURIComponent( '{"title":{"value":"test","type":"include"}}' )}` @@ -271,7 +276,7 @@ describe('ISIS Investigations - Card View', () => { await user.clear(filter); - expect(history.location.search).toBe('?'); + expect(window.location.search).not.toContain('filters='); }); it('updates filter query params on date filter', async () => { @@ -288,7 +293,7 @@ describe('ISIS Investigations - Card View', () => { await user.type(filter, '2019-08-06'); - expect(history.location.search).toBe( + expect(window.location.search).toContain( `?filters=${encodeURIComponent('{"endDate":{"endDate":"2019-08-06"}}')}` ); @@ -297,7 +302,7 @@ describe('ISIS Investigations - Card View', () => { await user.keyboard('{Control}a{/Control}'); await user.keyboard('{Delete}'); - expect(history.location.search).toBe('?'); + expect(window.location.search).not.toContain('filters='); }); it('displays the correct user as the PI ', async () => { @@ -308,12 +313,15 @@ describe('ISIS Investigations - Card View', () => { it('uses default sort', async () => { renderComponent(); + await act(async () => { + await flushPromises(); + }); + expect(await screen.findByTestId('card')).toBeInTheDocument(); - expect(history.length).toBe(1); - expect(replaceSpy).toHaveBeenCalledWith({ - search: `?sort=${encodeURIComponent('{"startDate":"desc"}')}`, - }); + expect(window.location.search).toBe( + `?sort=${encodeURIComponent('{"startDate":"desc"}')}` + ); // check that the data request is sent only once after mounting const datafilesCalls = vi @@ -329,7 +337,7 @@ describe('ISIS Investigations - Card View', () => { name: 'Sort by INVESTIGATIONS.TITLE', }) ); - expect(history.location.search).toBe( + expect(window.location.search).toBe( `?sort=${encodeURIComponent('{"title":"asc"}')}` ); }); @@ -355,7 +363,7 @@ describe('ISIS Investigations - Card View', () => { name: 'investigations.details.datasets', }) ); - expect(history.location.pathname).toBe( + expect(window.location.pathname).toBe( '/browse/instrument/1/facilityCycle/1/investigation/1/dataset' ); }); diff --git a/packages/datagateway-dataview/src/views/card/isis/isisInvestigationsCardView.component.tsx b/packages/datagateway-dataview/src/views/card/isis/isisInvestigationsCardView.component.tsx index c898289bb..a0ee645ce 100644 --- a/packages/datagateway-dataview/src/views/card/isis/isisInvestigationsCardView.component.tsx +++ b/packages/datagateway-dataview/src/views/card/isis/isisInvestigationsCardView.component.tsx @@ -29,7 +29,7 @@ import { import React from 'react'; import { useTranslation } from 'react-i18next'; import { useSelector } from 'react-redux'; -import { useHistory, useLocation } from 'react-router-dom'; +import { useLocation, useNavigate, useParams } from 'react-router'; import { StateType } from '../../../state/app.types'; const ActionButtonsContainer = styled('div')(({ theme }) => ({ @@ -41,19 +41,19 @@ const ActionButtonsContainer = styled('div')(({ theme }) => ({ }, })); -interface ISISInvestigationsCardViewProps { +interface BaseISISInvestigationsCardViewProps { instrumentId: string; facilityCycleId: string; } -const ISISInvestigationsCardView = ( - props: ISISInvestigationsCardViewProps +const BaseISISInvestigationsCardView = ( + props: BaseISISInvestigationsCardViewProps ): React.ReactElement => { const { instrumentId, facilityCycleId } = props; const [t] = useTranslation(); const location = useLocation(); - const { push } = useHistory(); + const navigate = useNavigate(); const { filters, view, sort, page, results } = React.useMemo( () => parseSearchToQuery(location.search), @@ -92,13 +92,14 @@ const ISISInvestigationsCardView = ( }, ]; - // isMounted is used to disable queries when the component isn't fully mounted. + // isInitialised is used to disable queries when the component isn't fully initialised. // It prevents the request being sent twice if default sort is set. // It is not needed for cards/tables that don't have default sort. - const [isMounted, setIsMounted] = React.useState(false); + const [isInitialised, setIsInitialised] = React.useState(false); + React.useEffect(() => { - setIsMounted(true); - }, []); + if (!isInitialised && Object.keys(sort).length > 0) setIsInitialised(true); + }, [isInitialised, sort]); const { data: totalDataCount, isPending: countLoading } = useInvestigationCount(investigationQueryFilters); @@ -123,7 +124,7 @@ const ISISInvestigationsCardView = ( }, ], undefined, - isMounted + isInitialised ); const title: CardViewDetails = React.useMemo( @@ -256,11 +257,11 @@ const ISISInvestigationsCardView = ( const url = view ? `${location.pathname}/${id}/dataset?view=${view}` : `${location.pathname}/${id}/dataset`; - push(url); + navigate(url); }} /> ), - [location.pathname, push, view] + [location.pathname, navigate, view] ); return ( @@ -287,4 +288,14 @@ const ISISInvestigationsCardView = ( ); }; +const ISISInvestigationsCardView = () => { + const { instrumentId = '', facilityCycleId = '' } = useParams(); + return ( + + ); +}; + export default ISISInvestigationsCardView; diff --git a/packages/datagateway-dataview/src/views/datafilePreview/datafilePreviewer.component.test.tsx b/packages/datagateway-dataview/src/views/datafilePreview/datafilePreviewer.component.test.tsx index 7385cb354..58c1dc1af 100644 --- a/packages/datagateway-dataview/src/views/datafilePreview/datafilePreviewer.component.test.tsx +++ b/packages/datagateway-dataview/src/views/datafilePreview/datafilePreviewer.component.test.tsx @@ -11,7 +11,7 @@ import axios, { type AxiosRequestConfig } from 'axios'; import { downloadDatafile } from 'datagateway-common'; import type { Datafile } from 'datagateway-common/lib/app.types'; import { Provider } from 'react-redux'; -import { MemoryRouter } from 'react-router-dom'; +import { MemoryRouter } from 'react-router'; import { combineReducers, createStore, type Store } from 'redux'; import DGDataViewReducer from '../../state/reducers/dgdataview.reducer'; import DatafilePreviewer from './datafilePreviewer.component'; diff --git a/packages/datagateway-dataview/src/views/datafilePreview/isisDatafilePreviewer.component.tsx b/packages/datagateway-dataview/src/views/datafilePreview/isisDatafilePreviewer.component.tsx new file mode 100644 index 000000000..f78184b32 --- /dev/null +++ b/packages/datagateway-dataview/src/views/datafilePreview/isisDatafilePreviewer.component.tsx @@ -0,0 +1,59 @@ +import { useDataPublication } from 'datagateway-common'; +import { useParams } from 'react-router'; +import { + checkDatasetId, + checkInstrumentAndFacilityCycleId, + checkInstrumentId, + checkInvestigationId, + checkStudyDataPublicationId, +} from '../../page/idCheckFunctions'; +import WithIdCheck from '../../page/withIdCheck'; +import DatafilePreviewer from './datafilePreviewer.component'; + +export const ISISDatafilePreviewer = (props: { dataPublication: boolean }) => { + const { + instrumentId = '', + facilityCycleId = '', + dataPublicationId = '', + investigationId = '', + datasetId = '', + datafileId = '', + } = useParams(); + const { data, isPending } = useDataPublication( + parseInt(investigationId), + props.dataPublication + ); + + const dataPublicationInvestigationId = + data?.content?.dataCollectionInvestigations?.[0]?.investigation?.id; + + const checkingPromise = props.dataPublication + ? Promise.all([ + checkInstrumentId(parseInt(instrumentId), parseInt(dataPublicationId)), + checkStudyDataPublicationId( + parseInt(dataPublicationId), + parseInt(investigationId) + ), + checkInvestigationId( + dataPublicationInvestigationId ?? -1, + parseInt(datasetId) + ), + checkDatasetId(parseInt(datasetId), parseInt(datafileId)), + ...(isPending ? [new Promise(() => undefined)] : []), + ]).then((values) => !values.includes(false)) + : Promise.all([ + checkInstrumentAndFacilityCycleId( + parseInt(instrumentId), + parseInt(facilityCycleId), + parseInt(investigationId) + ), + checkInvestigationId(parseInt(investigationId), parseInt(datasetId)), + checkDatasetId(parseInt(datasetId), parseInt(datafileId)), + ]).then((checks) => checks.every((passes) => passes)); + + return ( + + + + ); +}; diff --git a/packages/datagateway-dataview/src/views/datafilePreview/previewDatafileButton.component.test.tsx b/packages/datagateway-dataview/src/views/datafilePreview/previewDatafileButton.component.test.tsx index 71ac420c7..6a27dbbaa 100644 --- a/packages/datagateway-dataview/src/views/datafilePreview/previewDatafileButton.component.test.tsx +++ b/packages/datagateway-dataview/src/views/datafilePreview/previewDatafileButton.component.test.tsx @@ -7,7 +7,7 @@ import { type StateType, } from 'datagateway-common'; import { Provider } from 'react-redux'; -import { MemoryRouter } from 'react-router-dom'; +import { MemoryRouter } from 'react-router'; import configureStore from 'redux-mock-store'; import thunk from 'redux-thunk'; import PreviewDatafileButton, { diff --git a/packages/datagateway-dataview/src/views/datafilePreview/previewDatafileButton.component.tsx b/packages/datagateway-dataview/src/views/datafilePreview/previewDatafileButton.component.tsx index 9ab8b401a..416a2e28a 100644 --- a/packages/datagateway-dataview/src/views/datafilePreview/previewDatafileButton.component.tsx +++ b/packages/datagateway-dataview/src/views/datafilePreview/previewDatafileButton.component.tsx @@ -7,7 +7,7 @@ import { } from 'datagateway-common'; import { useTranslation } from 'react-i18next'; import { useSelector } from 'react-redux'; -import { Link } from 'react-router-dom'; +import { Link } from 'react-router'; import { isDatafilePreviewable } from './datafileExtension'; export interface PreviewDatafileButtonProps { diff --git a/packages/datagateway-dataview/src/views/datafilePreview/toolbar/actionButtons.component.test.tsx b/packages/datagateway-dataview/src/views/datafilePreview/toolbar/actionButtons.component.test.tsx index ee535743d..31516c5f3 100644 --- a/packages/datagateway-dataview/src/views/datafilePreview/toolbar/actionButtons.component.test.tsx +++ b/packages/datagateway-dataview/src/views/datafilePreview/toolbar/actionButtons.component.test.tsx @@ -7,9 +7,8 @@ import { } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { downloadDatafile } from 'datagateway-common'; -import { History, createMemoryHistory } from 'history'; import { Provider } from 'react-redux'; -import { MemoryRouter, Router } from 'react-router-dom'; +import { BrowserRouter, MemoryRouter } from 'react-router'; import type { Store } from 'redux'; import { combineReducers, createStore } from 'redux'; import type { StateType } from '../../../state/app.types'; @@ -28,11 +27,9 @@ vi.mock('datagateway-common', async () => ({ function renderComponent({ context, store, - history, }: { context?: DatafilePreviewerContextShape; store: Store; - history: History; }): RenderResult { const mockContext: DatafilePreviewerContextShape = context ?? { datafile: mockDatafile, @@ -40,11 +37,11 @@ function renderComponent({ return render( - + - + ); } @@ -52,7 +49,6 @@ function renderComponent({ describe('ActionButtons', () => { let user: ReturnType; let store: Store; - let history: History; beforeEach(() => { user = userEvent.setup(); @@ -66,7 +62,7 @@ describe('ActionButtons', () => { dgdataview: DGDataViewReducer, }) ); - history = createMemoryHistory(); + window.history.replaceState({}, '', '/'); }); afterEach(() => { @@ -87,11 +83,13 @@ describe('ActionButtons', () => { it('should have a back button that brings the users back to the datafile table when clicked', async () => { // pretend the user visited the datafile previewer directly through the URL. - history.replace( + window.history.replaceState( + {}, + '', '/browse/instrument/33/facilityCycle/89981656/investigation/91429827/dataset/91429833/datafile/91445688' ); - renderComponent({ store, history }); + renderComponent({ store }); await user.click( screen.getByRole('button', { @@ -99,7 +97,7 @@ describe('ActionButtons', () => { }) ); - expect(history.location.pathname).toBe( + expect(window.location.pathname).toBe( '/browse/instrument/33/facilityCycle/89981656/investigation/91429827/dataset/91429833/datafile' ); }); @@ -108,7 +106,6 @@ describe('ActionButtons', () => { it('lets users download the currently previewed datafile', async () => { renderComponent({ store, - history, context: { datafile: mockDatafile, datafileContent: new Blob(['hello']), @@ -127,7 +124,6 @@ describe('ActionButtons', () => { renderComponent({ store, - history, context: { datafile: datafileWithNoLocation, datafileContent: new Blob(['hello']), @@ -162,7 +158,7 @@ describe('ActionButtons', () => { .spyOn(navigator.clipboard, 'writeText') .mockImplementationOnce(() => Promise.resolve()); - renderComponent({ store, history }); + renderComponent({ store }); await user.click( screen.getByRole('button', { @@ -195,7 +191,6 @@ describe('ActionButtons', () => { it('should have a zoom in button that increases the zoom level of the datafile previewer when clicked', async () => { renderComponent({ store, - history, }); await user.click( @@ -212,7 +207,6 @@ describe('ActionButtons', () => { it('should have a zoom out button that decreases the zoom level of the datafile previewer when clicked', async () => { renderComponent({ store, - history, }); await user.click( @@ -228,7 +222,6 @@ describe('ActionButtons', () => { it('that is hidden when the zoom level of the datafile previewer is at the default value', () => { renderComponent({ store, - history, }); expect( @@ -241,7 +234,6 @@ describe('ActionButtons', () => { it('that is shown when the zoom level of the datafile previewer is changed and resets it when clicked', async () => { renderComponent({ store, - history, }); // click the zoom in button to change the zoom level diff --git a/packages/datagateway-dataview/src/views/datafilePreview/toolbar/actionButtons.component.tsx b/packages/datagateway-dataview/src/views/datafilePreview/toolbar/actionButtons.component.tsx index db0a5c99c..06ab8e379 100644 --- a/packages/datagateway-dataview/src/views/datafilePreview/toolbar/actionButtons.component.tsx +++ b/packages/datagateway-dataview/src/views/datafilePreview/toolbar/actionButtons.component.tsx @@ -9,7 +9,7 @@ import { downloadDatafile } from 'datagateway-common'; import React from 'react'; import { useTranslation } from 'react-i18next'; import { useDispatch, useSelector } from 'react-redux'; -import { useHistory, useLocation } from 'react-router-dom'; +import { useLocation, useNavigate } from 'react-router'; import type { StateType } from '../../../state/app.types'; import DatafilePreviewerContext from '../datafilePreviewerContext'; import DATAFILE_PREVIEWER_DEFAULT from '../defaults'; @@ -48,7 +48,7 @@ function ActionButtons(): JSX.Element { ); const [t] = useTranslation(); const { pathname } = useLocation(); - const history = useHistory(); + const navigate = useNavigate(); // this should only occur when DatafilePreviewerContext is not provided if (!previewerContext) return <>; @@ -56,7 +56,7 @@ function ActionButtons(): JSX.Element { const { datafile, datafileContent } = previewerContext; function goBackToDatafileTable(): void { - history.push(pathname.split('/').slice(0, -1).join('/')); + navigate(pathname.split('/').slice(0, -1).join('/')); } function zoomIn(): void { diff --git a/packages/datagateway-dataview/src/views/doiTypeSelector.component.test.tsx b/packages/datagateway-dataview/src/views/doiTypeSelector.component.test.tsx index b8f3e6011..422b8e782 100644 --- a/packages/datagateway-dataview/src/views/doiTypeSelector.component.test.tsx +++ b/packages/datagateway-dataview/src/views/doiTypeSelector.component.test.tsx @@ -2,7 +2,7 @@ import { render, screen, type RenderResult } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { parseSearchToQuery, usePushQueryParams } from 'datagateway-common'; import React from 'react'; -import { MemoryRouter } from 'react-router-dom'; +import { MemoryRouter } from 'react-router'; import DOITypeSelector from './doiTypeSelector.component'; vi.mock('datagateway-common', async () => { diff --git a/packages/datagateway-dataview/src/views/doiTypeSelector.component.tsx b/packages/datagateway-dataview/src/views/doiTypeSelector.component.tsx index b21d50560..4791aa47e 100644 --- a/packages/datagateway-dataview/src/views/doiTypeSelector.component.tsx +++ b/packages/datagateway-dataview/src/views/doiTypeSelector.component.tsx @@ -12,7 +12,7 @@ import { usePushQueryParams, } from 'datagateway-common'; import { useTranslation } from 'react-i18next'; -import { useLocation } from 'react-router-dom'; +import { useLocation } from 'react-router'; interface DOITypeSelectorProps { type: 'myDOIs' | 'allDOIs'; diff --git a/packages/datagateway-dataview/src/views/landing/dls/dlsDataPublicationContentTable.component.test.tsx b/packages/datagateway-dataview/src/views/landing/dls/dlsDataPublicationContentTable.component.test.tsx index 82abf0610..923bfae1e 100644 --- a/packages/datagateway-dataview/src/views/landing/dls/dlsDataPublicationContentTable.component.test.tsx +++ b/packages/datagateway-dataview/src/views/landing/dls/dlsDataPublicationContentTable.component.test.tsx @@ -20,9 +20,8 @@ import { findCellInRow, findColumnIndexByName, } from 'datagateway-search/src/setupTests'; -import { createMemoryHistory, type History } from 'history'; import { Provider } from 'react-redux'; -import { Router } from 'react-router-dom'; +import { BrowserRouter } from 'react-router'; import configureStore from 'redux-mock-store'; import thunk from 'redux-thunk'; import { findAllRows, findColumnHeaderByName } from '../../../setupTests'; @@ -34,7 +33,6 @@ describe('DataPublication content table component', () => { const mockStore = configureStore([thunk]); let state: StateType; let cartItems: DownloadCartItem[]; - let history: History; let user: UserEvent; let holder: HTMLElement; let investigations: Investigation[]; @@ -44,18 +42,17 @@ describe('DataPublication content table component', () => { const renderComponent = (): RenderResult => render( - + - + ); beforeEach(() => { user = userEvent.setup(); cartItems = []; - history = createMemoryHistory(); investigations = [ { @@ -424,8 +421,8 @@ describe('DataPublication content table component', () => { // user.type inputs the given string character by character to simulate user typing // each keystroke of user.type creates a new entry in the history stack // so the initial entry + 1 characters in "1" = 2 entries - expect(history.length).toBe(2); - expect(history.location.search).toBe( + + expect(window.location.search).toBe( `?filters=${encodeURIComponent( '{"visitId":{"value":"1","type":"include"}}' )}` @@ -433,8 +430,7 @@ describe('DataPublication content table component', () => { await user.clear(filterInput); - expect(history.length).toBe(3); - expect(history.location.search).toBe('?'); + expect(window.location.search).toBe(''); }); it('updates filter query params on date filter', async () => { @@ -446,8 +442,7 @@ describe('DataPublication content table component', () => { await user.type(filterInput, '2019-08-06'); - expect(history.length).toBe(2); - expect(history.location.search).toBe( + expect(window.location.search).toBe( `?filters=${encodeURIComponent('{"startDate":{"endDate":"2019-08-06"}}')}` ); @@ -455,8 +450,7 @@ describe('DataPublication content table component', () => { await user.keyboard('{Control}a{/Control}'); await user.keyboard('{Delete}'); - expect(history.length).toBe(3); - expect(history.location.search).toBe('?'); + expect(window.location.search).toBe(''); }); it('updates sort query params on sort', async () => { @@ -466,8 +460,7 @@ describe('DataPublication content table component', () => { await screen.findByRole('button', { name: 'investigations.visit_id' }) ); - expect(history.length).toBe(2); - expect(history.location.search).toBe( + expect(window.location.search).toBe( `?sort=${encodeURIComponent('{"visitId":"asc"}')}` ); }); diff --git a/packages/datagateway-dataview/src/views/landing/dls/dlsDataPublicationContentTable.component.tsx b/packages/datagateway-dataview/src/views/landing/dls/dlsDataPublicationContentTable.component.tsx index 103414f28..3e5ffa443 100644 --- a/packages/datagateway-dataview/src/views/landing/dls/dlsDataPublicationContentTable.component.tsx +++ b/packages/datagateway-dataview/src/views/landing/dls/dlsDataPublicationContentTable.component.tsx @@ -27,7 +27,7 @@ import { } from 'datagateway-common'; import React from 'react'; import { useTranslation } from 'react-i18next'; -import { useHistory, useLocation } from 'react-router-dom'; +import { useLocation, useNavigate } from 'react-router'; import { IndexRange, TableCellProps } from 'react-virtualized'; interface DLSDataPublicationContentTableProps { @@ -49,15 +49,13 @@ const DLSDataPublicationContentTable = ( ): void => { setCurrentTab(newValue); // remove any applied sorts/filters on tab change - history.replace({ - search: '', - }); + navigate({ search: '' }, { replace: true }); }; const [t] = useTranslation(); const location = useLocation(); - const history = useHistory(); + const navigate = useNavigate(); const { filters, sort } = React.useMemo( () => parseSearchToQuery(location.search), @@ -111,11 +109,11 @@ const DLSDataPublicationContentTable = ( filterComponent: textFilter, cellContentRenderer: (cellProps: TableCellProps) => tableLink( - { - pathname: `/redirect/DLS/investigation/id/${cellProps.rowData.id}`, - state: { fromDataPublication: true }, - }, - cellProps.rowData.visitId + `/redirect/DLS/investigation/id/${cellProps.rowData.id}`, + cellProps.rowData.visitId, + undefined, + undefined, + { fromDataPublication: true } ), }, { @@ -162,11 +160,11 @@ const DLSDataPublicationContentTable = ( filterComponent: textFilter, cellContentRenderer: (cellProps: TableCellProps) => tableLink( - { - pathname: `/redirect/DLS/dataset/id/${cellProps.rowData.id}`, - state: { fromDataPublication: true }, - }, - cellProps.rowData.name + `/redirect/DLS/dataset/id/${cellProps.rowData.id}`, + cellProps.rowData.name, + undefined, + undefined, + { fromDataPublication: true } ), }, { @@ -204,11 +202,11 @@ const DLSDataPublicationContentTable = ( filterComponent: textFilter, cellContentRenderer: (cellProps: TableCellProps) => tableLink( - { - pathname: `/redirect/DLS/datafile/id/${cellProps.rowData.id}`, - state: { fromDataPublication: true }, - }, - cellProps.rowData.name + `/redirect/DLS/datafile/id/${cellProps.rowData.id}`, + cellProps.rowData.name, + undefined, + undefined, + { fromDataPublication: true } ), }, { diff --git a/packages/datagateway-dataview/src/views/landing/dls/dlsDataPublicationEditForm.component.test.tsx b/packages/datagateway-dataview/src/views/landing/dls/dlsDataPublicationEditForm.component.test.tsx index bfcf68174..5a9fa03f8 100644 --- a/packages/datagateway-dataview/src/views/landing/dls/dlsDataPublicationEditForm.component.test.tsx +++ b/packages/datagateway-dataview/src/views/landing/dls/dlsDataPublicationEditForm.component.test.tsx @@ -19,9 +19,8 @@ import { DownloadCartItem, dGCommonInitialState, } from 'datagateway-common'; -import { History, createMemoryHistory } from 'history'; import { Provider } from 'react-redux'; -import { Router, generatePath } from 'react-router-dom'; +import { MemoryRouter, Route, Routes, generatePath } from 'react-router'; import configureStore from 'redux-mock-store'; import thunk from 'redux-thunk'; import { paths } from '../../../page/pageContainer.component'; @@ -41,17 +40,28 @@ const createTestQueryClient = (): QueryClient => describe('DOI edit form component', () => { const mockStore = configureStore([thunk]); let state: StateType; - let history: History; let holder: HTMLElement; + let initialEntries: React.ComponentProps< + typeof MemoryRouter + >['initialEntries']; const renderComponent = (): RenderResult => render( - + - + + } + /> + } + /> + - + ); @@ -269,19 +279,17 @@ describe('DOI edit form component', () => { }) ); - history = createMemoryHistory({ - initialEntries: [ - { - pathname: generatePath( - paths.landing.dlsDataPublicationLanding + '/edit', - { - dataPublicationId: '1', - } - ), - state: { fromEdit: true }, - }, - ], - }); + initialEntries = [ + { + pathname: generatePath( + `${paths.landing.dlsDataPublicationLanding}/edit`, + { + dataPublicationId: '1', + } + ), + state: { fromEdit: true }, + }, + ]; user = userEvent.setup(); @@ -419,14 +427,14 @@ describe('DOI edit form component', () => { }); it('should redirect back to landing page if user directly accesses the url', async () => { - history = createMemoryHistory(); - renderComponent(); - - expect(history.location).toMatchObject({ - pathname: generatePath(paths.landing.dlsDataPublicationLanding, { + initialEntries = [ + generatePath(`${paths.landing.dlsDataPublicationLanding}/edit`, { dataPublicationId: '1', }), - }); + ]; + renderComponent(); + + expect(screen.getByTestId('mock-landing-page')); }); it('should default fill the form with existing info and let the user change these and submit a mint request', async () => { diff --git a/packages/datagateway-dataview/src/views/landing/dls/dlsDataPublicationEditForm.component.tsx b/packages/datagateway-dataview/src/views/landing/dls/dlsDataPublicationEditForm.component.tsx index c991e003a..1fad27ae8 100644 --- a/packages/datagateway-dataview/src/views/landing/dls/dlsDataPublicationEditForm.component.tsx +++ b/packages/datagateway-dataview/src/views/landing/dls/dlsDataPublicationEditForm.component.tsx @@ -21,20 +21,20 @@ import { import React from 'react'; import { useTranslation } from 'react-i18next'; import { useSelector } from 'react-redux'; -import { Redirect, useLocation } from 'react-router-dom'; +import { Navigate, generatePath, useLocation, useParams } from 'react-router'; import { paths } from '../../../page/pageContainer.component'; import { StateType } from '../../../state/app.types'; import DLSDataPublicationDataEditor, { TransferListItem, } from './dlsDataPublicationDataEditor.component'; -interface DLSDataPublicationEditFormProps { +interface BaseDLSDataPublicationEditFormProps { dataPublicationId: string; } -const DLSDataPublicationEditForm: React.FC = ( - props -) => { +const BaseDLSDataPublicationEditForm: React.FC< + BaseDLSDataPublicationEditFormProps +> = (props) => { const { dataPublicationId } = props; const [selectedUsers, setSelectedUsers] = React.useState( [] @@ -270,7 +270,8 @@ const DLSDataPublicationEditForm: React.FC = ( unmintableEntityIDs, ]); - const location = useLocation<{ fromEdit: boolean } | undefined>(); + const location = useLocation(); + const locationState: { fromEdit: boolean } | undefined = location.state; const [t] = useTranslation(); @@ -360,12 +361,14 @@ const DLSDataPublicationEditForm: React.FC = ( }, [draftVersionDataPublicationId, deleteVersionDraft, dataPublicationId]); // redirect if the user tries to access the link directly instead of from the edit button - if (!location.state?.fromEdit) { - const landingPageUrl = paths.landing.dlsDataPublicationLanding.replace( - ':dataPublicationId', - dataPublicationId + if (!locationState?.fromEdit) { + const landingPageUrl = generatePath( + paths.landing.dlsDataPublicationLanding, + { + dataPublicationId, + } ); - return ; + return ; } return ( @@ -446,4 +449,12 @@ const DLSDataPublicationEditForm: React.FC = ( ); }; +const DLSDataPublicationEditForm = () => { + const { dataPublicationId = '' } = useParams(); + + return ( + + ); +}; + export default DLSDataPublicationEditForm; diff --git a/packages/datagateway-dataview/src/views/landing/dls/dlsDataPublicationLanding.component.test.tsx b/packages/datagateway-dataview/src/views/landing/dls/dlsDataPublicationLanding.component.test.tsx index 8aac85533..5d96fba58 100644 --- a/packages/datagateway-dataview/src/views/landing/dls/dlsDataPublicationLanding.component.test.tsx +++ b/packages/datagateway-dataview/src/views/landing/dls/dlsDataPublicationLanding.component.test.tsx @@ -20,15 +20,20 @@ import { dGCommonInitialState, readSciGatewayToken, } from 'datagateway-common'; -import { History, createMemoryHistory } from 'history'; import { Provider } from 'react-redux'; -import { Router, generatePath } from 'react-router-dom'; +import { + MemoryRouter, + Route, + Routes, + generatePath, + useLocation, +} from 'react-router'; import configureStore from 'redux-mock-store'; import thunk from 'redux-thunk'; import { paths } from '../../../page/pageContainer.component'; import { StateType } from '../../../state/app.types'; import { initialState as dgDataViewInitialState } from '../../../state/reducers/dgdataview.reducer'; -import DLSDataPublicationLanding from './dlsDataPublicationLanding.component'; +import DLSDataPublicationLandingPage from './dlsDataPublicationLanding.component'; vi.mock('datagateway-common', async () => { const originalModule = await vi.importActual('datagateway-common'); @@ -40,20 +45,37 @@ vi.mock('datagateway-common', async () => { }; }); +// used to verify the location state is correct +const MockEditPage = () => { + const { state } = useLocation(); + return
{JSON.stringify(state)}
; +}; + describe('DLS Data Publication Landing page', () => { const mockStore = configureStore([thunk]); let state: StateType; - let history: History; let user: UserEvent; + let initialEntries: React.ComponentProps< + typeof MemoryRouter + >['initialEntries']; const renderComponent = (): RenderResult => render( - + - + + } + /> + } + /> + - + ); @@ -276,13 +298,11 @@ describe('DLS Data Publication Landing page', () => { }) ); - history = createMemoryHistory({ - initialEntries: [ - generatePath(paths.landing.dlsDataPublicationLanding, { - dataPublicationId: '1', - }), - ], - }); + initialEntries = [ + generatePath(paths.landing.dlsDataPublicationLanding, { + dataPublicationId: '1', + }), + ]; user = userEvent.setup(); axios.get = vi @@ -443,12 +463,9 @@ describe('DLS Data Publication Landing page', () => { }) ); - expect(history.location).toMatchObject({ - pathname: `${generatePath(paths.landing.dlsDataPublicationLanding, { - dataPublicationId: '1', - })}/edit`, - state: { fromEdit: true }, - }); + expect(screen.getByTestId('mock-edit-page')).toHaveTextContent( + '{"fromEdit":true}' + ); }); it('renders download button & clicking it opens download dialogue', async () => { diff --git a/packages/datagateway-dataview/src/views/landing/dls/dlsDataPublicationLanding.component.tsx b/packages/datagateway-dataview/src/views/landing/dls/dlsDataPublicationLanding.component.tsx index a0d15cc57..03939588c 100644 --- a/packages/datagateway-dataview/src/views/landing/dls/dlsDataPublicationLanding.component.tsx +++ b/packages/datagateway-dataview/src/views/landing/dls/dlsDataPublicationLanding.component.tsx @@ -29,7 +29,6 @@ import { } from 'datagateway-common'; import React from 'react'; import { useTranslation } from 'react-i18next'; -import { useHistory } from 'react-router-dom'; import CitationFormatter from '../../citationFormatter.component'; import Branding from './dlsBranding.component'; import DLSDataPublicationContentTable from './dlsDataPublicationContentTable.component'; @@ -40,6 +39,8 @@ import DLSDataPublicationVersionPanel, { // TODO: when vite 6, explore no-inline w/ pluginHost vs inline as we have to inline in vite 5 import ORCIDIdLogo from 'datagateway-common/src/images/ORCID-iD_icon_unauth_vector.svg'; import { useSelector } from 'react-redux'; +import { generatePath, useNavigate, useParams } from 'react-router'; +import { paths } from '../../../page/pageContainer.component'; import { StateType } from '../../../state/app.types'; const Subheading = styled(Typography)(({ theme }) => ({ @@ -138,7 +139,7 @@ const LandingPage = (props: LandingPageProps): React.ReactElement => { const PIRole = useSelector((state: StateType) => state.dgdataview.PIRole); - const history = useHistory(); + const navigate = useNavigate(); const [currentTab, setCurrentTab] = React.useState<'details' | 'content'>( 'details' @@ -557,10 +558,17 @@ const LandingPage = (props: LandingPageProps): React.ReactElement => { - history.push({ - pathname: `${dataPublicationId}/edit`, - state: { fromEdit: true }, - }) + navigate( + `${generatePath( + paths.landing.dlsDataPublicationLanding, + { + dataPublicationId, + } + )}/edit`, + { + state: { fromEdit: true }, + } + ) } aria-label={t('datapublications.edit.edit_label')} > @@ -727,4 +735,10 @@ const LandingPage = (props: LandingPageProps): React.ReactElement => { ); }; -export default LandingPage; +const DLSDataPublicationLandingPage = () => { + const { dataPublicationId = '' } = useParams(); + + return ; +}; + +export default DLSDataPublicationLandingPage; diff --git a/packages/datagateway-dataview/src/views/landing/isis/isisDataPublicationLanding.component.test.tsx b/packages/datagateway-dataview/src/views/landing/isis/isisDataPublicationLanding.component.test.tsx index 5e7ce00db..7c9dbf84c 100644 --- a/packages/datagateway-dataview/src/views/landing/isis/isisDataPublicationLanding.component.test.tsx +++ b/packages/datagateway-dataview/src/views/landing/isis/isisDataPublicationLanding.component.test.tsx @@ -7,9 +7,8 @@ import { useDataPublication, useDataPublicationsByFilters, } from 'datagateway-common'; -import { History, createMemoryHistory } from 'history'; import { Provider } from 'react-redux'; -import { Router, generatePath } from 'react-router-dom'; +import { BrowserRouter, Route, Routes, generatePath } from 'react-router'; import configureStore from 'redux-mock-store'; import thunk from 'redux-thunk'; import { paths } from '../../../page/pageContainer.component'; @@ -28,20 +27,34 @@ vi.mock('datagateway-common', async () => { }; }); +vi.mock('../../../page/idCheckFunctions', () => ({ + checkInstrumentId: vi.fn().mockResolvedValue(true), +})); + describe('ISIS Data Publication Landing page', () => { const mockStore = configureStore([thunk]); let state: StateType; - let history: History; let user: ReturnType; const renderComponent = (): RenderResult => render( - + - + + } + /> + + - + ); @@ -129,17 +142,14 @@ describe('ISIS Data Publication Landing page', () => { ); state.dgdataview.pluginHost = '/test/'; - history = createMemoryHistory({ - initialEntries: [ - generatePath( - paths.dataPublications.landing.isisDataPublicationLanding, - { - instrumentId: '4', - dataPublicationId: '5', - } - ), - ], - }); + window.history.replaceState( + {}, + '', + generatePath(paths.dataPublications.landing.isisDataPublicationLanding, { + instrumentId: '4', + dataPublicationId: '5', + }) + ); user = userEvent.setup(); initialStudyDataPublicationData = { @@ -200,30 +210,34 @@ describe('ISIS Data Publication Landing page', () => { renderComponent(); await user.click( - screen.getByRole('tab', { + await screen.findByRole('tab', { name: 'datapublications.details.investigations', }) ); - expect(history.location.pathname).toBe( + expect(window.location.pathname).toBe( '/browseDataPublications/instrument/4/dataPublication/5/investigation' ); }); it('in cards view', async () => { - history.replace({ search: '?view=card' }); + window.history.replaceState( + {}, + '', + `${window.location.pathname}?view=card` + ); renderComponent(); await user.click( - screen.getByRole('tab', { + await screen.findByRole('tab', { name: 'datapublications.details.investigations', }) ); - expect(history.location.pathname).toBe( + expect(window.location.pathname).toBe( '/browseDataPublications/instrument/4/dataPublication/5/investigation' ); - expect(history.location.search).toBe('?view=card'); + expect(window.location.search).toBe('?view=card'); }); }); diff --git a/packages/datagateway-dataview/src/views/landing/isis/isisDataPublicationLanding.component.tsx b/packages/datagateway-dataview/src/views/landing/isis/isisDataPublicationLanding.component.tsx index 5e81c124a..48e303353 100644 --- a/packages/datagateway-dataview/src/views/landing/isis/isisDataPublicationLanding.component.tsx +++ b/packages/datagateway-dataview/src/views/landing/isis/isisDataPublicationLanding.component.tsx @@ -27,7 +27,9 @@ import { import React from 'react'; import { useTranslation } from 'react-i18next'; import { useSelector } from 'react-redux'; -import { useHistory, useLocation } from 'react-router-dom'; +import { useLocation, useNavigate, useParams } from 'react-router'; +import { checkInstrumentId } from '../../../page/idCheckFunctions'; +import WithIdCheck from '../../../page/withIdCheck'; import { StateType } from '../../../state/app.types'; import CitationFormatter from '../../citationFormatter.component'; import Branding from './isisBranding.component'; @@ -166,7 +168,7 @@ const LinkedInvestigation = ( const LandingPage = (props: LandingPageProps): React.ReactElement => { const [t] = useTranslation(); - const { push } = useHistory(); + const navigate = useNavigate(); const location = useLocation(); const { view } = React.useMemo( () => parseSearchToQuery(location.search), @@ -387,7 +389,7 @@ const LandingPage = (props: LandingPageProps): React.ReactElement => { id="datapublication-investigations-tab" label={t('datapublications.details.investigations')} onClick={() => - push( + navigate( view ? `${location.pathname}/investigation?view=${view}` : `${location.pathname}/investigation` @@ -486,4 +488,19 @@ const LandingPage = (props: LandingPageProps): React.ReactElement => { ); }; -export default LandingPage; +const ISISDataPublicationLandingPage = () => { + const { instrumentId = '', dataPublicationId = '' } = useParams(); + + return ( + + + + ); +}; + +export default ISISDataPublicationLandingPage; diff --git a/packages/datagateway-dataview/src/views/landing/isis/isisDatasetLanding.component.test.tsx b/packages/datagateway-dataview/src/views/landing/isis/isisDatasetLanding.component.test.tsx index 3411c4848..f71c9eeea 100644 --- a/packages/datagateway-dataview/src/views/landing/isis/isisDatasetLanding.component.test.tsx +++ b/packages/datagateway-dataview/src/views/landing/isis/isisDatasetLanding.component.test.tsx @@ -1,14 +1,14 @@ import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { render, screen, type RenderResult } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; +import axios, { AxiosResponse } from 'axios'; import { Dataset, dGCommonInitialState, useDatasetDetails, } from 'datagateway-common'; -import { History, createMemoryHistory } from 'history'; import { Provider } from 'react-redux'; -import { Router, generatePath } from 'react-router-dom'; +import { BrowserRouter, Route, Routes, generatePath } from 'react-router'; import configureStore from 'redux-mock-store'; import thunk from 'redux-thunk'; import { paths } from '../../../page/pageContainer.component'; @@ -27,20 +27,40 @@ vi.mock('datagateway-common', async () => { }; }); +vi.mock('../../../page/idCheckFunctions', () => ({ + checkInstrumentId: vi.fn().mockResolvedValue(true), + checkInvestigationId: vi.fn().mockResolvedValue(true), + checkStudyDataPublicationId: vi.fn().mockResolvedValue(true), + checkInstrumentAndFacilityCycleId: vi.fn().mockResolvedValue(true), +})); + describe('ISIS Dataset Landing page', () => { const mockStore = configureStore([thunk]); let state: StateType; - let history: History; let user: ReturnType; const renderComponent = (): RenderResult => render( - + - + + } + /> + } + /> + + + - + ); @@ -69,21 +89,38 @@ describe('ISIS Dataset Landing page', () => { }) ); state.dgdataview.pluginHost = '/test/'; - history = createMemoryHistory({ - initialEntries: [ - generatePath(paths.landing.isisDatasetLanding, { - instrumentId: '4', - investigationId: '1', - facilityCycleId: '5', - datasetId: '87', - }), - ], - }); + window.history.replaceState( + {}, + '', + generatePath(paths.landing.isisDatasetLanding, { + instrumentId: '4', + investigationId: '1', + facilityCycleId: '5', + datasetId: '87', + }) + ); user = userEvent.setup(); vi.mocked(useDatasetDetails, { partial: true }).mockReturnValue({ data: initialData, }); + + axios.get = vi + .fn() + .mockImplementation((url: string): Promise> => { + if (/\/datapublications$/.test(url)) { + return Promise.resolve({ + data: { + id: 1, + content: { + dataCollectionInvestigations: [{ investigation: { id: 1 } }], + }, + }, + }); + } + + return Promise.reject(`Endpoint not mocked: ${url}`); + }); }); afterEach(() => { @@ -98,27 +135,33 @@ describe('ISIS Dataset Landing page', () => { await screen.findByRole('tab', { name: 'datasets.details.datafiles' }) ); - expect(history.location.pathname).toBe( + expect(window.location.pathname).toBe( '/browse/instrument/4/facilityCycle/5/investigation/1/dataset/87/datafile' ); }); it('for facility cycle hierarchy and cards view', async () => { - history.replace({ search: '?view=card' }); + window.history.replaceState( + {}, + '', + `${window.location.pathname}?view=card` + ); renderComponent(); await user.click( await screen.findByRole('tab', { name: 'datasets.details.datafiles' }) ); - expect(history.location.pathname).toBe( + expect(window.location.pathname).toBe( '/browse/instrument/4/facilityCycle/5/investigation/1/dataset/87/datafile' ); - expect(history.location.search).toBe('?view=card'); + expect(window.location.search).toBe('?view=card'); }); it('for data publication hierarchy and normal view', async () => { - history.replace( + window.history.replaceState( + {}, + '', generatePath(paths.dataPublications.landing.isisDatasetLanding, { instrumentId: '4', investigationId: '1', @@ -132,34 +175,32 @@ describe('ISIS Dataset Landing page', () => { await screen.findByRole('tab', { name: 'datasets.details.datafiles' }) ); - expect(history.location.pathname).toBe( + expect(window.location.pathname).toBe( '/browseDataPublications/instrument/4/dataPublication/5/investigation/1/dataset/87/datafile' ); }); it('for data publication hierarchy and cards view', async () => { - history.replace({ - pathname: generatePath( - paths.dataPublications.landing.isisDatasetLanding, - { - instrumentId: '4', - investigationId: '1', - dataPublicationId: '5', - datasetId: '87', - } - ), - search: '?view=card', - }); + window.history.replaceState( + {}, + '', + `${generatePath(paths.dataPublications.landing.isisDatasetLanding, { + instrumentId: '4', + investigationId: '1', + dataPublicationId: '5', + datasetId: '87', + })}?view=card` + ); renderComponent(); await user.click( await screen.findByRole('tab', { name: 'datasets.details.datafiles' }) ); - expect(history.location.pathname).toBe( + expect(window.location.pathname).toBe( '/browseDataPublications/instrument/4/dataPublication/5/investigation/1/dataset/87/datafile' ); - expect(history.location.search).toBe('?view=card'); + expect(window.location.search).toBe('?view=card'); }); }); diff --git a/packages/datagateway-dataview/src/views/landing/isis/isisDatasetLanding.component.tsx b/packages/datagateway-dataview/src/views/landing/isis/isisDatasetLanding.component.tsx index 46ef53849..c11fd824f 100644 --- a/packages/datagateway-dataview/src/views/landing/isis/isisDatasetLanding.component.tsx +++ b/packages/datagateway-dataview/src/views/landing/isis/isisDatasetLanding.component.tsx @@ -1,3 +1,7 @@ +import CalendarToday from '@mui/icons-material/CalendarToday'; +import CheckCircle from '@mui/icons-material/CheckCircle'; +import Public from '@mui/icons-material/Public'; +import Save from '@mui/icons-material/Save'; import { Divider, Grid, @@ -8,23 +12,27 @@ import { Tabs, Typography, } from '@mui/material'; -import CalendarToday from '@mui/icons-material/CalendarToday'; -import CheckCircle from '@mui/icons-material/CheckCircle'; -import Public from '@mui/icons-material/Public'; -import Save from '@mui/icons-material/Save'; import { + AddToCartButton, + ArrowTooltip, Dataset, + DownloadButton, formatBytes, + getTooltipText, parseSearchToQuery, + useDataPublication, useDatasetDetails, - AddToCartButton, - DownloadButton, - ArrowTooltip, - getTooltipText, } from 'datagateway-common'; import React from 'react'; import { useTranslation } from 'react-i18next'; -import { useHistory, useLocation } from 'react-router-dom'; +import { useLocation, useNavigate, useParams } from 'react-router'; +import { + checkInstrumentAndFacilityCycleId, + checkInstrumentId, + checkInvestigationId, + checkStudyDataPublicationId, +} from '../../../page/idCheckFunctions'; +import WithIdCheck from '../../../page/withIdCheck'; import Branding from './isisBranding.component'; const Subheading = styled(Typography)(({ theme }) => ({ @@ -66,9 +74,11 @@ interface LandingPageProps { datasetId: string; } -const LandingPage = (props: LandingPageProps): React.ReactElement => { +export const BaseISISDatasetLandingPage = ( + props: LandingPageProps +): React.ReactElement => { const [t] = useTranslation(); - const { push } = useHistory(); + const navigate = useNavigate(); const location = useLocation(); const { view } = React.useMemo( () => parseSearchToQuery(location.search), @@ -145,7 +155,7 @@ const LandingPage = (props: LandingPageProps): React.ReactElement => { id="dataset-datafiles-tab" label={t('datasets.details.datafiles')} onClick={() => - push( + navigate( view ? `${location.pathname}/datafile?view=${view}` : `${location.pathname}/datafile` @@ -221,4 +231,50 @@ const LandingPage = (props: LandingPageProps): React.ReactElement => { ); }; -export default LandingPage; +const ISISDatasetLandingPage = (props: { + dataPublication: boolean; +}): React.ReactElement => { + const { + instrumentId = '', + facilityCycleId = '', + dataPublicationId = '', + investigationId = '', + datasetId = '', + } = useParams(); + const { data, isPending } = useDataPublication( + parseInt(investigationId), + props.dataPublication + ); + const dataPublicationInvestigationId = + data?.content?.dataCollectionInvestigations?.[0]?.investigation?.id; + + const checkingPromise = props.dataPublication + ? Promise.all([ + checkInstrumentId(parseInt(instrumentId), parseInt(dataPublicationId)), + checkStudyDataPublicationId( + parseInt(dataPublicationId), + parseInt(investigationId) + ), + checkInvestigationId( + dataPublicationInvestigationId ?? -1, + parseInt(datasetId) + ), + ...(isPending ? [new Promise(() => undefined)] : []), + ]).then((values) => !values.includes(false)) + : Promise.all([ + checkInstrumentAndFacilityCycleId( + parseInt(instrumentId), + parseInt(facilityCycleId), + parseInt(investigationId) + ), + checkInvestigationId(parseInt(investigationId), parseInt(datasetId)), + ]).then((values) => !values.includes(false)); + + return ( + + + + ); +}; + +export default ISISDatasetLandingPage; diff --git a/packages/datagateway-dataview/src/views/landing/isis/isisInvestigationLanding.component.test.tsx b/packages/datagateway-dataview/src/views/landing/isis/isisInvestigationLanding.component.test.tsx index cc3c02984..715e80d54 100644 --- a/packages/datagateway-dataview/src/views/landing/isis/isisInvestigationLanding.component.test.tsx +++ b/packages/datagateway-dataview/src/views/landing/isis/isisInvestigationLanding.component.test.tsx @@ -14,9 +14,8 @@ import { useDataPublicationsByFilters, useEntity, } from 'datagateway-common'; -import { History, createMemoryHistory } from 'history'; import { Provider } from 'react-redux'; -import { Router, generatePath } from 'react-router-dom'; +import { BrowserRouter, Route, Routes, generatePath } from 'react-router'; import configureStore from 'redux-mock-store'; import thunk from 'redux-thunk'; import { paths } from '../../../page/pageContainer.component'; @@ -36,23 +35,39 @@ vi.mock('datagateway-common', async () => { }; }); +vi.mock('../../../page/idCheckFunctions', () => ({ + checkInstrumentId: vi.fn().mockResolvedValue(true), + checkStudyDataPublicationId: vi.fn().mockResolvedValue(true), + checkInstrumentAndFacilityCycleId: vi.fn().mockResolvedValue(true), +})); + describe('ISIS Investigation Landing page', () => { const mockStore = configureStore([thunk]); let state: StateType; - let history: History; let user: ReturnType; - const renderComponent = (dataPublication = false): RenderResult => + const renderComponent = (): RenderResult => render( - + - + + } + /> + } + /> + + + - + ); @@ -157,15 +172,15 @@ describe('ISIS Investigation Landing page', () => { }) ); state.dgdataview.pluginHost = '/test/'; - history = createMemoryHistory({ - initialEntries: [ - generatePath(paths.landing.isisInvestigationLanding, { - instrumentId: '4', - facilityCycleId: '5', - investigationId: '1', - }), - ], - }); + window.history.replaceState( + {}, + '', + generatePath(paths.landing.isisInvestigationLanding, { + instrumentId: '4', + facilityCycleId: '5', + investigationId: '1', + }) + ); user = userEvent.setup(); initialInvestigationData = { @@ -420,14 +435,16 @@ describe('ISIS Investigation Landing page', () => { }); it('renders correctly for data publication hierarchy', async () => { - history.replace( + window.history.replaceState( + {}, + '', generatePath(paths.dataPublications.landing.isisInvestigationLanding, { instrumentId: '4', dataPublicationId: '5', investigationId: '1', }) ); - renderComponent(true); + renderComponent(); // branding should be visible expect( @@ -545,14 +562,16 @@ describe('ISIS Investigation Landing page', () => { initialDataPublicationData.content.dataCollectionInvestigations[0].investigation = undefined; - history.replace( + window.history.replaceState( + {}, + '', generatePath(paths.dataPublications.landing.isisInvestigationLanding, { instrumentId: '4', dataPublicationId: '5', investigationId: '1', }) ); - renderComponent(true); + renderComponent(); expect( await screen.findByText('Description not provided') @@ -588,7 +607,7 @@ describe('ISIS Investigation Landing page', () => { renderComponent(); expect( - screen.getByRole('link', { name: 'datasets.dataset: dataset 1' }) + await screen.findByRole('link', { name: 'datasets.dataset: dataset 1' }) ).toHaveAttribute( 'href', '/browse/instrument/4/facilityCycle/5/investigation/1/dataset/1' @@ -600,17 +619,21 @@ describe('ISIS Investigation Landing page', () => { }) ); - expect(history.location.pathname).toBe( + expect(window.location.pathname).toBe( '/browse/instrument/4/facilityCycle/5/investigation/1/dataset' ); }); it('for facility cycle hierarchy and cards view', async () => { - history.replace({ search: '?view=card' }); + window.history.replaceState( + {}, + '', + `${window.location.pathname}?view=card` + ); renderComponent(); expect( - screen.getByRole('link', { name: 'datasets.dataset: dataset 1' }) + await screen.findByRole('link', { name: 'datasets.dataset: dataset 1' }) ).toHaveAttribute( 'href', '/browse/instrument/4/facilityCycle/5/investigation/1/dataset/1?view=card' @@ -622,24 +645,26 @@ describe('ISIS Investigation Landing page', () => { }) ); - expect(history.location.pathname).toBe( + expect(window.location.pathname).toBe( '/browse/instrument/4/facilityCycle/5/investigation/1/dataset' ); - expect(history.location.search).toBe('?view=card'); + expect(window.location.search).toBe('?view=card'); }); it('for data publication hierarchy and normal view', async () => { - history.replace( + window.history.replaceState( + {}, + '', generatePath(paths.dataPublications.landing.isisInvestigationLanding, { instrumentId: '4', dataPublicationId: '5', investigationId: '1', }) ); - renderComponent(true); + renderComponent(); expect( - screen.getByRole('link', { name: 'datasets.dataset: dataset 1' }) + await screen.findByRole('link', { name: 'datasets.dataset: dataset 1' }) ).toHaveAttribute( 'href', '/browseDataPublications/instrument/4/dataPublication/5/investigation/1/dataset/1' @@ -651,27 +676,28 @@ describe('ISIS Investigation Landing page', () => { }) ); - expect(history.location.pathname).toBe( + expect(window.location.pathname).toBe( '/browseDataPublications/instrument/4/dataPublication/5/investigation/1/dataset' ); }); it('for data publication hierarchy and cards view', async () => { - history.replace({ - pathname: generatePath( + window.history.replaceState( + {}, + '', + `${generatePath( paths.dataPublications.landing.isisInvestigationLanding, { instrumentId: '4', dataPublicationId: '5', investigationId: '1', } - ), - search: '?view=card', - }); - renderComponent(true); + )}?view=card` + ); + renderComponent(); expect( - screen.getByRole('link', { name: 'datasets.dataset: dataset 1' }) + await screen.findByRole('link', { name: 'datasets.dataset: dataset 1' }) ).toHaveAttribute( 'href', '/browseDataPublications/instrument/4/dataPublication/5/investigation/1/dataset/1?view=card' @@ -683,10 +709,10 @@ describe('ISIS Investigation Landing page', () => { }) ); - expect(history.location.pathname).toBe( + expect(window.location.pathname).toBe( '/browseDataPublications/instrument/4/dataPublication/5/investigation/1/dataset' ); - expect(history.location.search).toBe('?view=card'); + expect(window.location.search).toBe('?view=card'); }); }); }); diff --git a/packages/datagateway-dataview/src/views/landing/isis/isisInvestigationLanding.component.tsx b/packages/datagateway-dataview/src/views/landing/isis/isisInvestigationLanding.component.tsx index 4665c9ca8..37de447f5 100644 --- a/packages/datagateway-dataview/src/views/landing/isis/isisInvestigationLanding.component.tsx +++ b/packages/datagateway-dataview/src/views/landing/isis/isisInvestigationLanding.component.tsx @@ -35,7 +35,13 @@ import { import React from 'react'; import { useTranslation } from 'react-i18next'; import { useSelector } from 'react-redux'; -import { useHistory, useLocation } from 'react-router-dom'; +import { useLocation, useNavigate, useParams } from 'react-router'; +import { + checkInstrumentAndFacilityCycleId, + checkInstrumentId, + checkStudyDataPublicationId, +} from '../../../page/idCheckFunctions'; +import WithIdCheck from '../../../page/withIdCheck'; import { StateType } from '../../../state/app.types'; import CitationFormatter from '../../citationFormatter.component'; import Branding from './isisBranding.component'; @@ -159,7 +165,7 @@ const CommonLandingPage = ( props: CommonLandingPageProps ): React.ReactElement => { const [t] = useTranslation(); - const { push } = useHistory(); + const navigate = useNavigate(); const location = useLocation(); const { view } = React.useMemo( () => parseSearchToQuery(location.search), @@ -413,7 +419,7 @@ const CommonLandingPage = ( id="investigation-datasets-tab" label={t('investigations.details.datasets')} onClick={() => - push( + navigate( view ? `${location.pathname}/dataset?view=${view}` : `${location.pathname}/dataset` @@ -438,8 +444,8 @@ const CommonLandingPage = ( ? data.summary : 'Description not provided' : data?.description && data.description !== 'null' - ? data.description - : 'Description not provided'} + ? data.description + : 'Description not provided'} {formattedUsers.length > 0 && (
@@ -650,7 +656,9 @@ const CommonLandingPage = ( ); }; -const LandingPage = (props: LandingPageProps): React.ReactElement => { +export const BaseISISInvestigationLandingPage = ( + props: LandingPageProps +): React.ReactElement => { if (props.dataPublication) { return ; } else { @@ -658,4 +666,36 @@ const LandingPage = (props: LandingPageProps): React.ReactElement => { } }; -export default LandingPage; +const ISISInvestigationLandingPage = (props: { dataPublication: boolean }) => { + const { + instrumentId = '', + dataPublicationId = '', + facilityCycleId = '', + investigationId = '', + } = useParams(); + const checkingPromise = props.dataPublication + ? Promise.all([ + checkInstrumentId(parseInt(instrumentId), parseInt(dataPublicationId)), + checkStudyDataPublicationId( + parseInt(dataPublicationId), + parseInt(investigationId) + ), + ]).then((values) => !values.includes(false)) + : checkInstrumentAndFacilityCycleId( + parseInt(instrumentId), + parseInt(facilityCycleId), + parseInt(investigationId) + ); + + return ( + + + + ); +}; + +export default ISISInvestigationLandingPage; diff --git a/packages/datagateway-dataview/src/views/roleSelector.component.test.tsx b/packages/datagateway-dataview/src/views/roleSelector.component.test.tsx index c32f4a65e..bdecc6fae 100644 --- a/packages/datagateway-dataview/src/views/roleSelector.component.test.tsx +++ b/packages/datagateway-dataview/src/views/roleSelector.component.test.tsx @@ -3,15 +3,15 @@ import { render, screen, type RenderResult } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import axios from 'axios'; import { - dGCommonInitialState, InvestigationUser, + StateType, + dGCommonInitialState, parseSearchToQuery, readSciGatewayToken, - StateType, usePushFilter, } from 'datagateway-common'; import { Provider } from 'react-redux'; -import { MemoryRouter } from 'react-router-dom'; +import { MemoryRouter } from 'react-router'; import configureStore from 'redux-mock-store'; import thunk from 'redux-thunk'; import { initialState as dgDataViewInitialState } from '../state/reducers/dgdataview.reducer'; diff --git a/packages/datagateway-dataview/src/views/roleSelector.component.tsx b/packages/datagateway-dataview/src/views/roleSelector.component.tsx index a17770779..2ca1ec9a9 100644 --- a/packages/datagateway-dataview/src/views/roleSelector.component.tsx +++ b/packages/datagateway-dataview/src/views/roleSelector.component.tsx @@ -18,7 +18,7 @@ import { import React from 'react'; import { useTranslation } from 'react-i18next'; import { useSelector } from 'react-redux'; -import { useLocation } from 'react-router-dom'; +import { useLocation } from 'react-router'; const fetchRoles = (apiUrl: string, username: string): Promise => { const params = new URLSearchParams(); diff --git a/packages/datagateway-dataview/src/views/table/datafileTable.component.test.tsx b/packages/datagateway-dataview/src/views/table/datafileTable.component.test.tsx index f85eeb4db..6f91ef4c6 100644 --- a/packages/datagateway-dataview/src/views/table/datafileTable.component.test.tsx +++ b/packages/datagateway-dataview/src/views/table/datafileTable.component.test.tsx @@ -17,33 +17,41 @@ import { findCellInRow, findColumnIndexByName, } from 'datagateway-search/src/setupTests'; -import { createMemoryHistory, type History } from 'history'; import { Provider } from 'react-redux'; -import { Router } from 'react-router-dom'; +import { BrowserRouter, Route, Routes, generatePath } from 'react-router'; import configureStore from 'redux-mock-store'; import thunk from 'redux-thunk'; +import { paths } from '../../page/pageContainer.component'; import { findAllRows, findColumnHeaderByName } from '../../setupTests'; import type { StateType } from '../../state/app.types'; import { initialState as dgDataViewInitialState } from '../../state/reducers/dgdataview.reducer'; import DatafileTable from './datafileTable.component'; +vi.mock('../../page/idCheckFunctions', () => ({ + checkInvestigationId: vi.fn().mockResolvedValue(true), +})); + describe('Datafile table component', () => { const mockStore = configureStore([thunk]); let state: StateType; let rowData: Datafile[]; let cartItems: DownloadCartItem[]; - let history: History; let user: ReturnType; let holder: HTMLElement; const renderComponent = (): RenderResult => render( - + - + + } + /> + - + ); @@ -62,7 +70,14 @@ describe('Datafile table component', () => { datafileCreateTime: '2019-01-01', }, ]; - history = createMemoryHistory(); + window.history.replaceState( + {}, + '', + generatePath(paths.standard.datafile, { + datasetId: '1', + investigationId: '2', + }) + ); holder = document.createElement('div'); holder.setAttribute('id', 'datagateway-dataview'); @@ -215,8 +230,8 @@ describe('Datafile table component', () => { // user.type inputs the given string character by character to simulate user typing // each keystroke of user.type creates a new entry in the history stack // so the initial entry + 4 characters in "test" = 5 entries - expect(history.length).toBe(5); - expect(history.location.search).toBe( + + expect(window.location.search).toBe( `?filters=${encodeURIComponent( '{"name":{"value":"test","type":"include"}}' )}` @@ -224,8 +239,7 @@ describe('Datafile table component', () => { await user.clear(filterInput); - expect(history.length).toBe(6); - expect(history.location.search).toBe('?'); + expect(window.location.search).toBe(''); }); it('updates filter query params on date filter', async () => { @@ -237,8 +251,7 @@ describe('Datafile table component', () => { await user.type(filterInput, '2019-08-06'); - expect(history.length).toBe(2); - expect(history.location.search).toBe( + expect(window.location.search).toBe( `?filters=${encodeURIComponent( '{"datafileModTime":{"endDate":"2019-08-06"}}' )}` @@ -249,8 +262,7 @@ describe('Datafile table component', () => { await user.keyboard('{Control}a{/Control}'); await user.keyboard('{Delete}'); - expect(history.length).toBe(3); - expect(history.location.search).toBe('?'); + expect(window.location.search).toBe(''); }); it('updates sort query params on sort', async () => { @@ -260,8 +272,7 @@ describe('Datafile table component', () => { await screen.findByRole('button', { name: 'datafiles.name' }) ); - expect(history.length).toBe(2); - expect(history.location.search).toBe( + expect(window.location.search).toBe( `?sort=${encodeURIComponent('{"name":"asc"}')}` ); }); diff --git a/packages/datagateway-dataview/src/views/table/datafileTable.component.tsx b/packages/datagateway-dataview/src/views/table/datafileTable.component.tsx index aafa2dc05..05e987904 100644 --- a/packages/datagateway-dataview/src/views/table/datafileTable.component.tsx +++ b/packages/datagateway-dataview/src/views/table/datafileTable.component.tsx @@ -24,16 +24,20 @@ import { import React from 'react'; import { useTranslation } from 'react-i18next'; import { useSelector } from 'react-redux'; -import { useLocation } from 'react-router-dom'; +import { useLocation, useParams } from 'react-router'; import { IndexRange } from 'react-virtualized'; +import { checkInvestigationId } from '../../page/idCheckFunctions'; +import WithIdCheck from '../../page/withIdCheck'; import type { StateType } from '../../state/app.types'; -interface DatafileTableProps { +interface BaseDatafileTableProps { datasetId: string; investigationId: string; } -const DatafileTable = (props: DatafileTableProps): React.ReactElement => { +export const BaseDatafileTable = ( + props: BaseDatafileTableProps +): React.ReactElement => { const { datasetId, investigationId } = props; const [t] = useTranslation(); @@ -190,4 +194,21 @@ const DatafileTable = (props: DatafileTableProps): React.ReactElement => { ); }; +const DatafileTable = () => { + const { investigationId = '', datasetId = '' } = useParams(); + return ( + + + + ); +}; + export default DatafileTable; diff --git a/packages/datagateway-dataview/src/views/table/datasetTable.component.test.tsx b/packages/datagateway-dataview/src/views/table/datasetTable.component.test.tsx index 243add897..c1b229888 100644 --- a/packages/datagateway-dataview/src/views/table/datasetTable.component.test.tsx +++ b/packages/datagateway-dataview/src/views/table/datasetTable.component.test.tsx @@ -17,11 +17,11 @@ import { findCellInRow, findColumnIndexByName, } from 'datagateway-search/src/setupTests'; -import { createMemoryHistory, type History } from 'history'; import { Provider } from 'react-redux'; -import { Router } from 'react-router-dom'; +import { BrowserRouter, Route, Routes, generatePath } from 'react-router'; import configureStore from 'redux-mock-store'; import thunk from 'redux-thunk'; +import { paths } from '../../page/pageContainer.component'; import { findAllRows, findColumnHeaderByName } from '../../setupTests'; import type { StateType } from '../../state/app.types'; import { initialState } from '../../state/reducers/dgdataview.reducer'; @@ -32,7 +32,6 @@ describe('Dataset table component', () => { let state: StateType; let rowData: Dataset[]; let cartItems: DownloadCartItem[]; - let history: History; let user: ReturnType; let holder: HTMLElement; @@ -40,11 +39,13 @@ describe('Dataset table component', () => { const store = mockStore(state); return render( - + - + + } /> + - + ); }; @@ -62,7 +63,13 @@ describe('Dataset table component', () => { createTime: '2019-07-23', }, ]; - history = createMemoryHistory(); + window.history.replaceState( + {}, + '', + generatePath(paths.toggle.dataset, { + investigationId: '1', + }) + ); holder = document.createElement('div'); holder.setAttribute('id', 'datagateway-dataview'); @@ -218,8 +225,7 @@ describe('Dataset table component', () => { await user.type(filterInput, 'test'); - expect(history.length).toBe(5); - expect(history.location.search).toBe( + expect(window.location.search).toBe( `?filters=${encodeURIComponent( '{"name":{"value":"test","type":"include"}}' )}` @@ -227,8 +233,7 @@ describe('Dataset table component', () => { await user.clear(filterInput); - expect(history.length).toBe(6); - expect(history.location.search).toBe('?'); + expect(window.location.search).toBe(''); }); it('updates filter query params on date filter', async () => { @@ -240,8 +245,7 @@ describe('Dataset table component', () => { await user.type(filterInput, '2019-08-06'); - expect(history.length).toBe(2); - expect(history.location.search).toBe( + expect(window.location.search).toBe( `?filters=${encodeURIComponent('{"modTime":{"endDate":"2019-08-06"}}')}` ); @@ -250,8 +254,7 @@ describe('Dataset table component', () => { await user.keyboard('{Control}a{/Control}'); await user.keyboard('{Delete}'); - expect(history.length).toBe(3); - expect(history.location.search).toBe('?'); + expect(window.location.search).toBe(''); }); it('updates sort query params on sort', async () => { @@ -261,8 +264,7 @@ describe('Dataset table component', () => { await screen.findByRole('button', { name: 'datasets.name' }) ); - expect(history.length).toBe(2); - expect(history.location.search).toBe( + expect(window.location.search).toBe( `?sort=${encodeURIComponent('{"name":"asc"}')}` ); }); diff --git a/packages/datagateway-dataview/src/views/table/datasetTable.component.tsx b/packages/datagateway-dataview/src/views/table/datasetTable.component.tsx index 5633b9ce6..12af18a37 100644 --- a/packages/datagateway-dataview/src/views/table/datasetTable.component.tsx +++ b/packages/datagateway-dataview/src/views/table/datasetTable.component.tsx @@ -21,15 +21,15 @@ import { import React from 'react'; import { useTranslation } from 'react-i18next'; import { useSelector } from 'react-redux'; -import { useLocation } from 'react-router-dom'; +import { useLocation, useParams } from 'react-router'; import { IndexRange } from 'react-virtualized'; import { StateType } from '../../state/app.types'; -interface DatasetTableProps { +interface BaseDatasetTableProps { investigationId: string; } -const DatasetTable = (props: DatasetTableProps): React.ReactElement => { +const BaseDatasetTable = (props: BaseDatasetTableProps): React.ReactElement => { const { investigationId } = props; const [t] = useTranslation(); @@ -186,4 +186,9 @@ const DatasetTable = (props: DatasetTableProps): React.ReactElement => { ); }; +const DatasetTable = () => { + const { investigationId = '' } = useParams(); + return ; +}; + export default DatasetTable; diff --git a/packages/datagateway-dataview/src/views/table/dls/dlsDOITables.component.test.tsx b/packages/datagateway-dataview/src/views/table/dls/dlsDOITables.component.test.tsx index 5280200e8..bf95efebd 100644 --- a/packages/datagateway-dataview/src/views/table/dls/dlsDOITables.component.test.tsx +++ b/packages/datagateway-dataview/src/views/table/dls/dlsDOITables.component.test.tsx @@ -1,5 +1,6 @@ import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { + act, render, screen, within, @@ -16,9 +17,8 @@ import { useDataPublicationsInfinite, type DataPublication, } from 'datagateway-common'; -import { createMemoryHistory, type MemoryHistory } from 'history'; import { Provider } from 'react-redux'; -import { Router } from 'react-router-dom'; +import { BrowserRouter } from 'react-router'; import configureStore from 'redux-mock-store'; import thunk from 'redux-thunk'; import { @@ -27,6 +27,7 @@ import { findColumnHeaderByName, findColumnIndexByName, findRowAt, + flushPromises, } from '../../../setupTests'; import type { StateType } from '../../../state/app.types'; import { initialState as dgDataViewInitialState } from '../../../state/reducers/dgdataview.reducer'; @@ -48,13 +49,13 @@ describe('DLS DOI table components', () => { const mockStore = configureStore([thunk]); let state: StateType; let rowData: DataPublication[]; - let history: MemoryHistory; let user: UserEvent; beforeEach(() => { - history = createMemoryHistory(); user = userEvent.setup(); + window.history.replaceState({}, '', '/'); + state = JSON.parse( JSON.stringify({ dgdataview: dgDataViewInitialState, @@ -137,11 +138,11 @@ describe('DLS DOI table components', () => { const store = mockStore(state); return render( - + - + ); }; @@ -189,7 +190,10 @@ describe('DLS DOI table components', () => { }, ]; expect(useDataPublicationCount).toHaveBeenCalledWith(filterParams); - expect(useDataPublicationsInfinite).toHaveBeenCalledWith(filterParams); + expect(useDataPublicationsInfinite).toHaveBeenCalledWith( + filterParams, + true + ); const rows = await findAllRows(); expect(rows).toHaveLength(2); @@ -243,7 +247,7 @@ describe('DLS DOI table components', () => { }); it('supplies the correct filter params for user doiType', async () => { - history.replace('?doiType=user'); + window.history.replaceState({}, '', '/?doiType=user'); renderComponent(); const filterParams = [ @@ -278,11 +282,14 @@ describe('DLS DOI table components', () => { }, ]; expect(useDataPublicationCount).toHaveBeenCalledWith(filterParams); - expect(useDataPublicationsInfinite).toHaveBeenCalledWith(filterParams); + expect(useDataPublicationsInfinite).toHaveBeenCalledWith( + filterParams, + true + ); }); it('supplies the correct filter params for session doiType', async () => { - history.replace('?doiType=session'); + window.history.replaceState({}, '', '/?doiType=session'); renderComponent(); const filterParams = [ @@ -300,11 +307,14 @@ describe('DLS DOI table components', () => { }, ]; expect(useDataPublicationCount).toHaveBeenCalledWith(filterParams); - expect(useDataPublicationsInfinite).toHaveBeenCalledWith(filterParams); + expect(useDataPublicationsInfinite).toHaveBeenCalledWith( + filterParams, + true + ); }); it('supplies the correct filter params for openSession doiType', async () => { - history.replace('?doiType=openSession'); + window.history.replaceState({}, '', '/?doiType=openSession'); renderComponent(); const filterParams = [ @@ -322,11 +332,14 @@ describe('DLS DOI table components', () => { }, ]; expect(useDataPublicationCount).toHaveBeenCalledWith(filterParams); - expect(useDataPublicationsInfinite).toHaveBeenCalledWith(filterParams); + expect(useDataPublicationsInfinite).toHaveBeenCalledWith( + filterParams, + true + ); }); it('supplies the correct filter params for closedSession doiType', async () => { - history.replace('?doiType=closedSession'); + window.history.replaceState({}, '', '/?doiType=closedSession'); renderComponent(); const filterParams = [ @@ -344,7 +357,10 @@ describe('DLS DOI table components', () => { }, ]; expect(useDataPublicationCount).toHaveBeenCalledWith(filterParams); - expect(useDataPublicationsInfinite).toHaveBeenCalledWith(filterParams); + expect(useDataPublicationsInfinite).toHaveBeenCalledWith( + filterParams, + true + ); }); it('updates filter query params on text filter', async () => { @@ -357,7 +373,7 @@ describe('DLS DOI table components', () => { await user.type(filterInput, 'test'); - expect(history.location.search).toBe( + expect(window.location.search).toContain( `?filters=${encodeURIComponent( '{"title":{"value":"test","type":"include"}}' )}` @@ -365,7 +381,7 @@ describe('DLS DOI table components', () => { await user.clear(filterInput); - expect(history.location.search).toBe('?'); + expect(window.location.search).not.toContain('filters='); }); it('updates filter query params on date filter', async () => { @@ -377,7 +393,7 @@ describe('DLS DOI table components', () => { await user.type(filterInput, '2023-07-21'); - expect(history.location.search).toBe( + expect(window.location.search).toContain( `?filters=${encodeURIComponent( '{"publicationDate":{"endDate":"2023-07-21"}}' )}` @@ -388,7 +404,28 @@ describe('DLS DOI table components', () => { await user.keyboard('{Control}a{/Control}'); await user.keyboard('{Delete}'); - expect(history.location.search).toBe('?'); + expect(window.location.search).not.toContain('filters='); + }); + + it('uses default sort', async () => { + renderComponent(); + + await act(async () => { + await flushPromises(); + }); + + expect(await screen.findAllByRole('gridcell')).toBeTruthy(); + + expect(window.location.search).toBe( + `?sort=${encodeURIComponent('{"publicationDate":"desc"}')}` + ); + + // check that the data hook is only called once with the query enabled + expect( + vi + .mocked(useDataPublicationsInfinite) + .mock.calls.filter((call) => call[1] === true) + ).toHaveLength(1); }); it('updates sort query params on sort', async () => { @@ -398,7 +435,7 @@ describe('DLS DOI table components', () => { await screen.findByRole('button', { name: 'datapublications.title' }) ); - expect(history.location.search).toBe( + expect(window.location.search).toBe( `?sort=${encodeURIComponent('{"title":"asc"}')}` ); }); @@ -422,11 +459,11 @@ describe('DLS DOI table components', () => { const store = mockStore(state); return render( - + - + ); }; @@ -444,7 +481,10 @@ describe('DLS DOI table components', () => { ]; expect(useDataPublicationCount).toHaveBeenCalledWith(filterParams); - expect(useDataPublicationsInfinite).toHaveBeenCalledWith(filterParams); + expect(useDataPublicationsInfinite).toHaveBeenCalledWith( + filterParams, + true + ); const rows = await findAllRows(); expect(rows).toHaveLength(2); @@ -498,7 +538,7 @@ describe('DLS DOI table components', () => { }); it('supplies the correct filter params for user doiType', async () => { - history.replace('?doiType=user'); + window.history.replaceState({}, '', '/?doiType=user'); renderComponent(); const filterParams = [ @@ -527,11 +567,14 @@ describe('DLS DOI table components', () => { }, ]; expect(useDataPublicationCount).toHaveBeenCalledWith(filterParams); - expect(useDataPublicationsInfinite).toHaveBeenCalledWith(filterParams); + expect(useDataPublicationsInfinite).toHaveBeenCalledWith( + filterParams, + true + ); }); it('supplies the correct filter params for openSession doiType', async () => { - history.replace('?doiType=openSession'); + window.history.replaceState({}, '', '/?doiType=openSession'); renderComponent(); const filterParams = [ @@ -543,11 +586,14 @@ describe('DLS DOI table components', () => { }, ]; expect(useDataPublicationCount).toHaveBeenCalledWith(filterParams); - expect(useDataPublicationsInfinite).toHaveBeenCalledWith(filterParams); + expect(useDataPublicationsInfinite).toHaveBeenCalledWith( + filterParams, + true + ); }); it('supplies the correct filter params for closedSession doiType', async () => { - history.replace('?doiType=closedSession'); + window.history.replaceState({}, '', '/?doiType=closedSession'); renderComponent(); const filterParams = [ @@ -559,7 +605,10 @@ describe('DLS DOI table components', () => { }, ]; expect(useDataPublicationCount).toHaveBeenCalledWith(filterParams); - expect(useDataPublicationsInfinite).toHaveBeenCalledWith(filterParams); + expect(useDataPublicationsInfinite).toHaveBeenCalledWith( + filterParams, + true + ); }); }); }); diff --git a/packages/datagateway-dataview/src/views/table/dls/dlsDOITables.component.tsx b/packages/datagateway-dataview/src/views/table/dls/dlsDOITables.component.tsx index 7916a50ef..4e3723b4a 100644 --- a/packages/datagateway-dataview/src/views/table/dls/dlsDOITables.component.tsx +++ b/packages/datagateway-dataview/src/views/table/dls/dlsDOITables.component.tsx @@ -24,7 +24,7 @@ import Fingerprint from '@mui/icons-material/Fingerprint'; import Lock from '@mui/icons-material/Lock'; import Public from '@mui/icons-material/Public'; import { Chip } from '@mui/material'; -import { useLocation } from 'react-router-dom'; +import { useLocation } from 'react-router'; interface DLSBaseDOIsTableProps { filterParams: AdditionalFilters; @@ -43,7 +43,19 @@ const DLSBaseDOIsTable = (props: DLSBaseDOIsTableProps): React.ReactElement => { */ const { data: totalDataCount } = useDataPublicationCount(filterParams); - const { fetchNextPage, data } = useDataPublicationsInfinite(filterParams); + // isInitialised is used to disable queries when the component isn't fully initialised. + // It prevents the request being sent twice if default sort is set. + // It is not needed for cards/tables that don't have default sort. + const [isInitialised, setIsInitialised] = React.useState(false); + + React.useEffect(() => { + if (!isInitialised && Object.keys(sort).length > 0) setIsInitialised(true); + }, [filters, isInitialised, sort]); + + const { fetchNextPage, data } = useDataPublicationsInfinite( + filterParams, + isInitialised + ); /* istanbul ignore next */ const aggregatedData: DataPublication[] = React.useMemo(() => { diff --git a/packages/datagateway-dataview/src/views/table/dls/dlsDatafilesTable.component.test.tsx b/packages/datagateway-dataview/src/views/table/dls/dlsDatafilesTable.component.test.tsx index a1eeb30e0..f50458ec9 100644 --- a/packages/datagateway-dataview/src/views/table/dls/dlsDatafilesTable.component.test.tsx +++ b/packages/datagateway-dataview/src/views/table/dls/dlsDatafilesTable.component.test.tsx @@ -1,5 +1,6 @@ import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { + act, render, screen, waitFor, @@ -18,22 +19,28 @@ import { useIds, useRemoveFromCart, } from 'datagateway-common'; -import { createMemoryHistory, type History } from 'history'; import { Provider } from 'react-redux'; -import { Router } from 'react-router-dom'; +import { BrowserRouter, Route, Routes, generatePath } from 'react-router'; import configureStore from 'redux-mock-store'; import thunk from 'redux-thunk'; +import { paths } from '../../../page/pageContainer.component'; import { findAllRows, findCellInRow, findColumnHeaderByName, findColumnIndexByName, findRowAt, + flushPromises, } from '../../../setupTests'; import { StateType } from '../../../state/app.types'; import { initialState as dgDataViewInitialState } from '../../../state/reducers/dgdataview.reducer'; import DLSDatafilesTable from './dlsDatafilesTable.component'; +vi.mock('../../../page/idCheckFunctions', () => ({ + checkProposalName: vi.fn().mockResolvedValue(true), + checkInvestigationId: vi.fn().mockResolvedValue(true), +})); + vi.mock('datagateway-common', async () => { const originalModule = await vi.importActual('datagateway-common'); @@ -54,17 +61,21 @@ describe('DLS datafiles table component', () => { const mockStore = configureStore([thunk]); let state: StateType; let rowData: Datafile[]; - let history: History; let user: ReturnType; const renderComponent = (): RenderResult => render( - + - + + } + /> + - + ); @@ -81,7 +92,15 @@ describe('DLS datafiles table component', () => { datafileCreateTime: '2019-01-01', }, ]; - history = createMemoryHistory(); + window.history.replaceState( + {}, + '', + generatePath(paths.standard.dlsDatafile, { + datasetId: '1', + investigationId: '2', + proposalName: 'test', + }) + ); user = userEvent.setup(); state = JSON.parse( @@ -199,8 +218,8 @@ describe('DLS datafiles table component', () => { // user.type inputs the given string character by character to simulate user typing // each keystroke of user.type creates a new entry in the history stack // so the initial entry + 4 characters in "test" = 5 entries - expect(history.length).toBe(5); - expect(history.location.search).toBe( + + expect(window.location.search).toContain( `?filters=${encodeURIComponent( '{"name":{"value":"test","type":"include"}}' )}` @@ -208,8 +227,7 @@ describe('DLS datafiles table component', () => { await user.clear(filterInput); - expect(history.length).toBe(6); - expect(history.location.search).toBe('?'); + expect(window.location.search).not.toContain('filters='); }); it('updates filter query params on date filter', async () => { @@ -221,8 +239,7 @@ describe('DLS datafiles table component', () => { await user.type(filterInput, '2019-08-06'); - expect(history.length).toBe(2); - expect(history.location.search).toBe( + expect(window.location.search).toContain( `?filters=${encodeURIComponent( '{"datafileCreateTime":{"endDate":"2019-08-06"}}' )}` @@ -233,27 +250,28 @@ describe('DLS datafiles table component', () => { await user.keyboard('{Control}a{/Control}'); await user.keyboard('{Delete}'); - expect(history.length).toBe(3); - expect(history.location.search).toBe('?'); + expect(window.location.search).not.toContain('filters='); }); it('uses default sort', async () => { renderComponent(); + await act(async () => { + await flushPromises(); + }); + expect(await screen.findAllByRole('gridcell')).toBeTruthy(); - expect(history.length).toBe(1); - expect(history.location.search).toBe( + expect(window.location.search).toBe( `?sort=${encodeURIComponent('{"name":"asc"}')}` ); - // check that the data request is sent only once after mounting - expect(useDatafilesInfinite).toHaveBeenCalledTimes(2); - expect(useDatafilesInfinite).toHaveBeenCalledWith(expect.anything(), false); - expect(useDatafilesInfinite).toHaveBeenLastCalledWith( - expect.anything(), - true - ); + // check that the data hook is only called once with the query enabled + expect( + vi + .mocked(useDatafilesInfinite) + .mock.calls.filter((call) => call[1] === true) + ).toHaveLength(1); }); it('updates sort query params on sort', async () => { @@ -263,8 +281,7 @@ describe('DLS datafiles table component', () => { await screen.findByRole('button', { name: 'datafiles.name' }) ); - expect(history.length).toBe(2); - expect(history.location.search).toBe( + expect(window.location.search).toBe( `?sort=${encodeURIComponent('{"name":"desc"}')}` ); }); @@ -317,9 +334,9 @@ describe('DLS datafiles table component', () => { vi.mocked(useCart, { partial: true }).mockReturnValueOnce({ data: [ { - entityId: 1, + entityId: 3, entityType: 'dataset', - id: 1, + id: 3, name: 'test', parentEntities: [], }, diff --git a/packages/datagateway-dataview/src/views/table/dls/dlsDatafilesTable.component.tsx b/packages/datagateway-dataview/src/views/table/dls/dlsDatafilesTable.component.tsx index 0922a31ff..348ed0393 100644 --- a/packages/datagateway-dataview/src/views/table/dls/dlsDatafilesTable.component.tsx +++ b/packages/datagateway-dataview/src/views/table/dls/dlsDatafilesTable.component.tsx @@ -22,17 +22,22 @@ import { import React from 'react'; import { useTranslation } from 'react-i18next'; import { useSelector } from 'react-redux'; -import { useLocation } from 'react-router-dom'; +import { useLocation, useParams } from 'react-router'; import { IndexRange } from 'react-virtualized'; +import { + checkInvestigationId, + checkProposalName, +} from '../../../page/idCheckFunctions'; +import WithIdCheck from '../../../page/withIdCheck'; import { StateType } from '../../../state/app.types'; -interface DLSDatafilesTableProps { +interface BaseDLSDatafilesTableProps { datasetId: string; investigationId: string; } -const DLSDatafilesTable = ( - props: DLSDatafilesTableProps +const BaseDLSDatafilesTable = ( + props: BaseDLSDatafilesTableProps ): React.ReactElement => { const { datasetId, investigationId } = props; @@ -76,13 +81,14 @@ const DLSDatafilesTable = ( }, ]); - // isMounted is used to disable queries when the component isn't fully mounted. + // isInitialised is used to disable queries when the component isn't fully initialised. // It prevents the request being sent twice if default sort is set. // It is not needed for cards/tables that don't have default sort. - const [isMounted, setIsMounted] = React.useState(false); + const [isInitialised, setIsInitialised] = React.useState(false); + React.useEffect(() => { - setIsMounted(true); - }, []); + if (!isInitialised && Object.keys(sort).length > 0) setIsInitialised(true); + }, [isInitialised, sort]); const { fetchNextPage, data } = useDatafilesInfinite( [ @@ -91,7 +97,7 @@ const DLSDatafilesTable = ( filterValue: JSON.stringify({ 'dataset.id': { eq: datasetId } }), }, ], - isMounted + isInitialised ); const loadMoreRows = React.useCallback( @@ -192,4 +198,25 @@ const DLSDatafilesTable = ( ); }; +const DLSDatafilesTable = () => { + const { + proposalName = '', + investigationId = '', + datasetId = '', + } = useParams(); + return ( + !values.includes(false))} + > + + + ); +}; + export default DLSDatafilesTable; diff --git a/packages/datagateway-dataview/src/views/table/dls/dlsDatasetsTable.component.test.tsx b/packages/datagateway-dataview/src/views/table/dls/dlsDatasetsTable.component.test.tsx index 60b999f2c..39b1bbbee 100644 --- a/packages/datagateway-dataview/src/views/table/dls/dlsDatasetsTable.component.test.tsx +++ b/packages/datagateway-dataview/src/views/table/dls/dlsDatasetsTable.component.test.tsx @@ -1,5 +1,6 @@ import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { + act, render, screen, waitFor, @@ -18,22 +19,27 @@ import { useIds, useRemoveFromCart, } from 'datagateway-common'; -import { History, createMemoryHistory } from 'history'; import { Provider } from 'react-redux'; -import { Router } from 'react-router-dom'; +import { BrowserRouter, Route, Routes, generatePath } from 'react-router'; import configureStore from 'redux-mock-store'; import thunk from 'redux-thunk'; +import { paths } from '../../../page/pageContainer.component'; import { findAllRows, findCellInRow, findColumnHeaderByName, findColumnIndexByName, findRowAt, + flushPromises, } from '../../../setupTests'; import { StateType } from '../../../state/app.types'; import { initialState as dgDataViewInitialState } from '../../../state/reducers/dgdataview.reducer'; import DLSDatasetsTable from './dlsDatasetsTable.component'; +vi.mock('../../../page/idCheckFunctions', () => ({ + checkProposalName: vi.fn().mockResolvedValue(true), +})); + vi.mock('datagateway-common', async () => { const originalModule = await vi.importActual('datagateway-common'); @@ -56,18 +62,22 @@ describe('DLS Dataset table component', () => { const mockStore = configureStore([thunk]); let state: StateType; let rowData: Dataset[]; - let history: History; let user: ReturnType; const renderComponent = (): RenderResult => { const store = mockStore(state); return render( - + - + + } + /> + - + ); }; @@ -83,7 +93,14 @@ describe('DLS Dataset table component', () => { createTime: '2019-07-23', }, ]; - history = createMemoryHistory(); + window.history.replaceState( + {}, + '', + generatePath(paths.toggle.dlsDataset, { + investigationId: '1', + proposalName: 'Proposal 1', + }) + ); user = userEvent.setup(); state = JSON.parse( @@ -193,8 +210,7 @@ describe('DLS Dataset table component', () => { await user.type(filterInput, 'test'); - expect(history.length).toBe(5); - expect(history.location.search).toBe( + expect(window.location.search).toContain( `?filters=${encodeURIComponent( '{"name":{"value":"test","type":"include"}}' )}` @@ -202,8 +218,7 @@ describe('DLS Dataset table component', () => { await user.clear(filterInput); - expect(history.length).toBe(6); - expect(history.location.search).toBe('?'); + expect(window.location.search).not.toContain('filters='); }); it('updates filter query params on date filter', async () => { @@ -215,8 +230,7 @@ describe('DLS Dataset table component', () => { await user.type(filterInput, '2019-08-06'); - expect(history.length).toBe(2); - expect(history.location.search).toBe( + expect(window.location.search).toContain( `?filters=${encodeURIComponent('{"modTime":{"endDate":"2019-08-06"}}')}` ); @@ -225,27 +239,28 @@ describe('DLS Dataset table component', () => { await user.keyboard('{Control}a{/Control}'); await user.keyboard('{Delete}'); - expect(history.length).toBe(3); - expect(history.location.search).toBe('?'); + expect(window.location.search).not.toContain('filters='); }); it('uses default sort', async () => { renderComponent(); + await act(async () => { + await flushPromises(); + }); + expect(await screen.findAllByRole('gridcell')).toBeTruthy(); - expect(history.length).toBe(1); - expect(history.location.search).toBe( + expect(window.location.search).toBe( `?sort=${encodeURIComponent('{"name":"asc"}')}` ); - // check that the data request is sent only once after mounting - expect(useDatasetsInfinite).toHaveBeenCalledTimes(2); - expect(useDatasetsInfinite).toHaveBeenCalledWith(expect.anything(), false); - expect(useDatasetsInfinite).toHaveBeenLastCalledWith( - expect.anything(), - true - ); + // check that the data hook is only called once with the query enabled + expect( + vi + .mocked(useDatasetsInfinite) + .mock.calls.filter((call) => call[1] === true) + ).toHaveLength(1); }); it('updates sort query params on sort', async () => { @@ -255,8 +270,7 @@ describe('DLS Dataset table component', () => { await screen.findByRole('button', { name: 'datasets.name' }) ); - expect(history.length).toBe(2); - expect(history.location.search).toBe( + expect(window.location.search).toBe( `?sort=${encodeURIComponent('{"name":"desc"}')}` ); }); @@ -309,9 +323,9 @@ describe('DLS Dataset table component', () => { vi.mocked(useCart, { partial: true }).mockReturnValueOnce({ data: [ { - entityId: 1, + entityId: 3, entityType: 'investigation', - id: 1, + id: 3, name: 'test', parentEntities: [], }, diff --git a/packages/datagateway-dataview/src/views/table/dls/dlsDatasetsTable.component.tsx b/packages/datagateway-dataview/src/views/table/dls/dlsDatasetsTable.component.tsx index 6237292b2..40f3e174d 100644 --- a/packages/datagateway-dataview/src/views/table/dls/dlsDatasetsTable.component.tsx +++ b/packages/datagateway-dataview/src/views/table/dls/dlsDatasetsTable.component.tsx @@ -23,16 +23,20 @@ import { import React from 'react'; import { useTranslation } from 'react-i18next'; import { useSelector } from 'react-redux'; -import { useLocation } from 'react-router-dom'; +import { useLocation, useParams } from 'react-router'; import { IndexRange, TableCellProps } from 'react-virtualized'; +import { checkProposalName } from '../../../page/idCheckFunctions'; +import WithIdCheck from '../../../page/withIdCheck'; import { StateType } from '../../../state/app.types'; -interface DLSDatasetsTableProps { +interface BaseDLSDatasetsTableProps { proposalName: string; investigationId: string; } -const DLSDatasetsTable = (props: DLSDatasetsTableProps): React.ReactElement => { +const BaseDLSDatasetsTable = ( + props: BaseDLSDatasetsTableProps +): React.ReactElement => { const { investigationId, proposalName } = props; const [t] = useTranslation(); @@ -79,13 +83,14 @@ const DLSDatasetsTable = (props: DLSDatasetsTableProps): React.ReactElement => { }, ]); - // isMounted is used to disable queries when the component isn't fully mounted. + // isInitialised is used to disable queries when the component isn't fully initialised. // It prevents the request being sent twice if default sort is set. // It is not needed for cards/tables that don't have default sort. - const [isMounted, setIsMounted] = React.useState(false); + const [isInitialised, setIsInitialised] = React.useState(false); + React.useEffect(() => { - setIsMounted(true); - }, []); + if (!isInitialised && Object.keys(sort).length > 0) setIsInitialised(true); + }, [isInitialised, sort]); const { fetchNextPage, data } = useDatasetsInfinite( [ @@ -96,7 +101,7 @@ const DLSDatasetsTable = (props: DLSDatasetsTableProps): React.ReactElement => { }), }, ], - isMounted + isInitialised ); const loadMoreRows = React.useCallback( @@ -207,4 +212,21 @@ const DLSDatasetsTable = (props: DLSDatasetsTableProps): React.ReactElement => { ); }; +const DLSDatasetsTable = () => { + const { proposalName = '', investigationId = '' } = useParams(); + return ( + + + + ); +}; + export default DLSDatasetsTable; diff --git a/packages/datagateway-dataview/src/views/table/dls/dlsMyDataTable.component.test.tsx b/packages/datagateway-dataview/src/views/table/dls/dlsMyDataTable.component.test.tsx index 4bea7026f..d5166622a 100644 --- a/packages/datagateway-dataview/src/views/table/dls/dlsMyDataTable.component.test.tsx +++ b/packages/datagateway-dataview/src/views/table/dls/dlsMyDataTable.component.test.tsx @@ -14,9 +14,8 @@ import { useInvestigationsInfinite, type Investigation, } from 'datagateway-common'; -import { createMemoryHistory, type MemoryHistory } from 'history'; import { Provider } from 'react-redux'; -import { Router } from 'react-router-dom'; +import { BrowserRouter } from 'react-router'; import configureStore from 'redux-mock-store'; import thunk from 'redux-thunk'; import { @@ -46,24 +45,23 @@ describe('DLS MyData table component', () => { const mockStore = configureStore([thunk]); let state: StateType; let rowData: Investigation[]; - let history: MemoryHistory; let user: ReturnType; const renderComponent = (): RenderResult => { const store = mockStore(state); return render( - + - + ); }; beforeEach(() => { - history = createMemoryHistory(); + window.history.replaceState({}, '', '/'); user = userEvent.setup(); state = JSON.parse( @@ -205,7 +203,6 @@ describe('DLS MyData table component', () => { }); it('sorts by startDate desc and filters startDate to be before the current date on load', async () => { - const replaceSpy = vi.spyOn(history, 'replace'); renderComponent(); expect( @@ -214,17 +211,21 @@ describe('DLS MyData table component', () => { }) ).toHaveValue('1970-01-01'); - expect(replaceSpy).toHaveBeenCalledTimes(2); - expect(replaceSpy).toHaveBeenCalledWith({ - search: `?filters=${encodeURIComponent( + expect(window.location.search).toContain( + `filters=${encodeURIComponent( JSON.stringify({ startDate: { endDate: '1970-01-01' } }) - )}`, - }); - expect(replaceSpy).toHaveBeenCalledWith({ - search: `?sort=${encodeURIComponent( - JSON.stringify({ startDate: 'desc' }) - )}`, - }); + )}` + ); + expect(window.location.search).toContain( + `sort=${encodeURIComponent(JSON.stringify({ startDate: 'desc' }))}` + ); + + // check that the data hook is only called once with the query enabled + expect( + vi + .mocked(useInvestigationsInfinite) + .mock.calls.filter((call) => call[2] === true) + ).toHaveLength(1); }); it('updates filter query params on text filter', async () => { @@ -237,15 +238,13 @@ describe('DLS MyData table component', () => { await user.type(filterInput, 'test'); - expect(history.location.search).toBe( - `?filters=${encodeURIComponent( - '{"visitId":{"value":"test","type":"include"}}' - )}` + expect(window.location.search).toContain( + `${encodeURIComponent('"visitId":{"value":"test","type":"include"}')}` ); await user.clear(filterInput); - expect(history.location.search).toBe('?'); + expect(window.location.search).not.toContain('visitId'); }); it('updates filter query params on date filter', async () => { @@ -257,8 +256,8 @@ describe('DLS MyData table component', () => { await user.type(filterInput, '2019-08-06'); - expect(history.location.search).toBe( - `?filters=${encodeURIComponent('{"endDate":{"endDate":"2019-08-06"}}')}` + expect(window.location.search).toContain( + `${encodeURIComponent('"endDate":{"endDate":"2019-08-06"}')}` ); // await user.clear(filterInput); @@ -266,7 +265,7 @@ describe('DLS MyData table component', () => { await user.keyboard('{Control}a{/Control}'); await user.keyboard('{Delete}'); - expect(history.location.search).toBe('?'); + expect(window.location.search).not.toContain('"endDate":{"endDate"'); }); it('updates sort query params on sort', async () => { @@ -276,8 +275,8 @@ describe('DLS MyData table component', () => { await screen.findByRole('button', { name: 'investigations.title' }) ); - expect(history.location.search).toBe( - `?sort=${encodeURIComponent('{"title":"asc"}')}` + expect(window.location.search).toContain( + `sort=${encodeURIComponent('{"title":"asc"}')}` ); }); diff --git a/packages/datagateway-dataview/src/views/table/dls/dlsMyDataTable.component.tsx b/packages/datagateway-dataview/src/views/table/dls/dlsMyDataTable.component.tsx index 7c61380e2..8dd4f54ba 100644 --- a/packages/datagateway-dataview/src/views/table/dls/dlsMyDataTable.component.tsx +++ b/packages/datagateway-dataview/src/views/table/dls/dlsMyDataTable.component.tsx @@ -23,7 +23,7 @@ import CalendarToday from '@mui/icons-material/CalendarToday'; import Fingerprint from '@mui/icons-material/Fingerprint'; import Save from '@mui/icons-material/Save'; import Subject from '@mui/icons-material/Subject'; -import { useLocation } from 'react-router-dom'; +import { useLocation } from 'react-router'; const DLSMyDataTable = (): React.ReactElement => { const [t] = useTranslation(); @@ -44,13 +44,19 @@ const DLSMyDataTable = (): React.ReactElement => { }, ]); - // isMounted is used to disable queries when the component isn't fully mounted. - // It prevents the request being sent twice if default sort/filter is set. - // It is not needed for cards/tables that don't have default sort/filter. - const [isMounted, setIsMounted] = React.useState(false); + // isInitialised is used to disable queries when the component isn't fully initialised. + // It prevents the request being sent twice if default sort is set. + // It is not needed for cards/tables that don't have default sort. + const [isInitialised, setIsInitialised] = React.useState(false); + React.useEffect(() => { - setIsMounted(true); - }, []); + if ( + !isInitialised && + Object.keys(sort).length > 0 && + Object.keys(filters).length > 0 + ) + setIsInitialised(true); + }, [filters, isInitialised, sort]); const { fetchNextPage, data } = useInvestigationsInfinite( [ @@ -70,7 +76,7 @@ const DLSMyDataTable = (): React.ReactElement => { }, ], undefined, - isMounted + isInitialised ); /* istanbul ignore next */ diff --git a/packages/datagateway-dataview/src/views/table/dls/dlsProposalsTable.component.test.tsx b/packages/datagateway-dataview/src/views/table/dls/dlsProposalsTable.component.test.tsx index d01d316e5..cf6e91c99 100644 --- a/packages/datagateway-dataview/src/views/table/dls/dlsProposalsTable.component.test.tsx +++ b/packages/datagateway-dataview/src/views/table/dls/dlsProposalsTable.component.test.tsx @@ -1,5 +1,6 @@ import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { + act, render, screen, within, @@ -12,9 +13,8 @@ import { useInvestigationsInfinite, type Investigation, } from 'datagateway-common'; -import { History, createMemoryHistory } from 'history'; import { Provider } from 'react-redux'; -import { Router } from 'react-router-dom'; +import { BrowserRouter } from 'react-router'; import configureStore from 'redux-mock-store'; import thunk from 'redux-thunk'; import { @@ -23,6 +23,7 @@ import { findColumnHeaderByName, findColumnIndexByName, findRowAt, + flushPromises, } from '../../../setupTests'; import type { StateType } from '../../../state/app.types'; import { initialState as dgDataViewInitialState } from '../../../state/reducers/dgdataview.reducer'; @@ -43,18 +44,17 @@ describe('DLS Proposals table component', () => { const mockStore = configureStore([thunk]); let state: StateType; let rowData: Investigation[]; - let history: History; let user: ReturnType; const renderComponent = (): RenderResult => { const store = mockStore(state); return render( - + - + ); }; @@ -81,7 +81,7 @@ describe('DLS Proposals table component', () => { endDate: '2019-06-11', }, ]; - history = createMemoryHistory(); + window.history.replaceState({}, '', '/'); user = userEvent.setup(); state = JSON.parse( @@ -149,8 +149,8 @@ describe('DLS Proposals table component', () => { // user.type inputs the given string character by character to simulate user typing // each keystroke of user.type creates a new entry in the history stack // so the initial entry + 4 characters in "test" = 5 entries - expect(history.length).toBe(5); - expect(history.location.search).toBe( + + expect(window.location.search).toContain( `?filters=${encodeURIComponent( '{"title":{"value":"test","type":"include"}}' )}` @@ -158,32 +158,28 @@ describe('DLS Proposals table component', () => { await user.clear(filterInput); - expect(history.length).toBe(6); - expect(history.location.search).toBe('?'); + expect(window.location.search).not.toContain('filters='); }); it('uses default sort', async () => { renderComponent(); + await act(async () => { + await flushPromises(); + }); + expect(await screen.findAllByRole('gridcell')).toBeTruthy(); - expect(history.length).toBe(1); - expect(history.location.search).toBe( + expect(window.location.search).toBe( `?sort=${encodeURIComponent('{"title":"asc"}')}` ); - // check that the data request is sent only once after mounting - expect(useInvestigationsInfinite).toHaveBeenCalledTimes(2); - expect(useInvestigationsInfinite).toHaveBeenCalledWith( - expect.anything(), - expect.anything(), - false - ); - expect(useInvestigationsInfinite).toHaveBeenLastCalledWith( - expect.anything(), - expect.anything(), - true - ); + // check that the data hook is only called once with the query enabled + expect( + vi + .mocked(useInvestigationsInfinite) + .mock.calls.filter((call) => call[2] === true) + ).toHaveLength(1); }); it('renders title and name as links', async () => { diff --git a/packages/datagateway-dataview/src/views/table/dls/dlsProposalsTable.component.tsx b/packages/datagateway-dataview/src/views/table/dls/dlsProposalsTable.component.tsx index 4ab585c41..44b70dc8c 100644 --- a/packages/datagateway-dataview/src/views/table/dls/dlsProposalsTable.component.tsx +++ b/packages/datagateway-dataview/src/views/table/dls/dlsProposalsTable.component.tsx @@ -12,7 +12,7 @@ import { } from 'datagateway-common'; import React from 'react'; import { useTranslation } from 'react-i18next'; -import { useLocation } from 'react-router-dom'; +import { useLocation } from 'react-router'; import { IndexRange, TableCellProps } from 'react-virtualized'; const DLSProposalsTable = (): React.ReactElement => { @@ -31,13 +31,14 @@ const DLSProposalsTable = (): React.ReactElement => { }, ]); - // isMounted is used to disable queries when the component isn't fully mounted. + // isInitialised is used to disable queries when the component isn't fully initialised. // It prevents the request being sent twice if default sort is set. // It is not needed for cards/tables that don't have default sort. - const [isMounted, setIsMounted] = React.useState(false); + const [isInitialised, setIsInitialised] = React.useState(false); + React.useEffect(() => { - setIsMounted(true); - }, []); + if (!isInitialised && Object.keys(sort).length > 0) setIsInitialised(true); + }, [isInitialised, sort]); const { fetchNextPage, data } = useInvestigationsInfinite( [ @@ -49,7 +50,7 @@ const DLSProposalsTable = (): React.ReactElement => { // Do not add order by id as id is not a distinct field above and will otherwise // cause missing results true, - isMounted + isInitialised ); /* istanbul ignore next */ diff --git a/packages/datagateway-dataview/src/views/table/dls/dlsVisitsTable.component.test.tsx b/packages/datagateway-dataview/src/views/table/dls/dlsVisitsTable.component.test.tsx index 04dc5caa9..327d57f10 100644 --- a/packages/datagateway-dataview/src/views/table/dls/dlsVisitsTable.component.test.tsx +++ b/packages/datagateway-dataview/src/views/table/dls/dlsVisitsTable.component.test.tsx @@ -1,5 +1,6 @@ import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { + act, render, screen, within, @@ -13,17 +14,18 @@ import { useInvestigationCount, useInvestigationsInfinite, } from 'datagateway-common'; -import { History, createMemoryHistory } from 'history'; import { Provider } from 'react-redux'; -import { Router } from 'react-router-dom'; +import { BrowserRouter, Route, Routes, generatePath } from 'react-router'; import configureStore from 'redux-mock-store'; import thunk from 'redux-thunk'; +import { paths } from '../../../page/pageContainer.component'; import { findAllRows, findCellInRow, findColumnHeaderByName, findColumnIndexByName, findRowAt, + flushPromises, } from '../../../setupTests'; import { StateType } from '../../../state/app.types'; import { initialState as dgDataViewInitialState } from '../../../state/reducers/dgdataview.reducer'; @@ -44,18 +46,22 @@ describe('DLS Visits table component', () => { const mockStore = configureStore([thunk]); let state: StateType; let rowData: Investigation[]; - let history: History; let user: ReturnType; const renderComponent = (): RenderResult => { const store = mockStore(state); return render( - + - + + } + /> + - + ); }; @@ -84,7 +90,13 @@ describe('DLS Visits table component', () => { endDate: '2019-06-11', }, ]; - history = createMemoryHistory(); + window.history.replaceState( + {}, + '', + generatePath(paths.toggle.dlsVisit, { + proposalName: 'Test 1', + }) + ); user = userEvent.setup(); state = JSON.parse( @@ -202,8 +214,8 @@ describe('DLS Visits table component', () => { // user.type inputs the given string character by character to simulate user typing // each keystroke of user.type creates a new entry in the history stack // so the initial entry + 4 characters in "test" = 5 entries - expect(history.length).toBe(5); - expect(history.location.search).toBe( + + expect(window.location.search).toContain( `?filters=${encodeURIComponent( '{"visitId":{"value":"test","type":"include"}}' )}` @@ -211,8 +223,7 @@ describe('DLS Visits table component', () => { await user.clear(filterInput); - expect(history.length).toBe(6); - expect(history.location.search).toBe('?'); + expect(window.location.search).not.toContain('filters='); }); it('updates filter query params on date filter', async () => { @@ -224,8 +235,7 @@ describe('DLS Visits table component', () => { await user.type(filterInput, '2019-08-06'); - expect(history.length).toBe(2); - expect(history.location.search).toBe( + expect(window.location.search).toContain( `?filters=${encodeURIComponent('{"endDate":{"endDate":"2019-08-06"}}')}` ); @@ -234,32 +244,28 @@ describe('DLS Visits table component', () => { await user.keyboard('{Control}a{/Control}'); await user.keyboard('{Delete}'); - expect(history.length).toBe(3); - expect(history.location.search).toBe('?'); + expect(window.location.search).not.toContain('filters='); }); it('uses default sort', async () => { renderComponent(); + await act(async () => { + await flushPromises(); + }); + expect(await screen.findAllByRole('gridcell')).toBeTruthy(); - expect(history.length).toBe(1); - expect(history.location.search).toBe( + expect(window.location.search).toBe( `?sort=${encodeURIComponent('{"startDate":"desc"}')}` ); - // check that the data request is sent only once after mounting - expect(useInvestigationsInfinite).toHaveBeenCalledTimes(2); - expect(useInvestigationsInfinite).toHaveBeenCalledWith( - expect.anything(), - undefined, - false - ); - expect(useInvestigationsInfinite).toHaveBeenLastCalledWith( - expect.anything(), - undefined, - true - ); + // check that the data hook is only called once with the query enabled + expect( + vi + .mocked(useInvestigationsInfinite) + .mock.calls.filter((call) => call[2] === true) + ).toHaveLength(1); }); it('updates sort query params on sort', async () => { @@ -269,8 +275,7 @@ describe('DLS Visits table component', () => { await screen.findByRole('button', { name: 'investigations.visit_id' }) ); - expect(history.length).toBe(2); - expect(history.location.search).toBe( + expect(window.location.search).toBe( `?sort=${encodeURIComponent('{"visitId":"asc"}')}` ); }); diff --git a/packages/datagateway-dataview/src/views/table/dls/dlsVisitsTable.component.tsx b/packages/datagateway-dataview/src/views/table/dls/dlsVisitsTable.component.tsx index 1f00fc4bf..fd49ad48f 100644 --- a/packages/datagateway-dataview/src/views/table/dls/dlsVisitsTable.component.tsx +++ b/packages/datagateway-dataview/src/views/table/dls/dlsVisitsTable.component.tsx @@ -18,14 +18,16 @@ import { } from 'datagateway-common'; import React from 'react'; import { useTranslation } from 'react-i18next'; -import { useLocation } from 'react-router-dom'; +import { useLocation, useParams } from 'react-router'; import { IndexRange, TableCellProps } from 'react-virtualized'; -interface DLSVisitsTableProps { +interface BaseDLSVisitsTableProps { proposalName: string; } -const DLSVisitsTable = (props: DLSVisitsTableProps): React.ReactElement => { +const BaseDLSVisitsTable = ( + props: BaseDLSVisitsTableProps +): React.ReactElement => { const { proposalName } = props; const [t] = useTranslation(); @@ -43,13 +45,14 @@ const DLSVisitsTable = (props: DLSVisitsTableProps): React.ReactElement => { }, ]); - // isMounted is used to disable queries when the component isn't fully mounted. + // isInitialised is used to disable queries when the component isn't fully initialised. // It prevents the request being sent twice if default sort is set. // It is not needed for cards/tables that don't have default sort. - const [isMounted, setIsMounted] = React.useState(false); + const [isInitialised, setIsInitialised] = React.useState(false); + React.useEffect(() => { - setIsMounted(true); - }, []); + if (!isInitialised && Object.keys(sort).length > 0) setIsInitialised(true); + }, [isInitialised, sort]); const { fetchNextPage, data } = useInvestigationsInfinite( [ @@ -65,7 +68,7 @@ const DLSVisitsTable = (props: DLSVisitsTableProps): React.ReactElement => { }, ], undefined, - isMounted + isInitialised ); /* istanbul ignore next */ @@ -159,4 +162,9 @@ const DLSVisitsTable = (props: DLSVisitsTableProps): React.ReactElement => { ); }; +const DLSVisitsTable = () => { + const { proposalName = '' } = useParams(); + return ; +}; + export default DLSVisitsTable; diff --git a/packages/datagateway-dataview/src/views/table/investigationTable.component.test.tsx b/packages/datagateway-dataview/src/views/table/investigationTable.component.test.tsx index c972b1fe2..b0d265b39 100644 --- a/packages/datagateway-dataview/src/views/table/investigationTable.component.test.tsx +++ b/packages/datagateway-dataview/src/views/table/investigationTable.component.test.tsx @@ -17,9 +17,8 @@ import { findCellInRow, findColumnIndexByName, } from 'datagateway-search/src/setupTests'; -import { createMemoryHistory, type History } from 'history'; import { Provider } from 'react-redux'; -import { Router } from 'react-router-dom'; +import { BrowserRouter } from 'react-router'; import configureStore from 'redux-mock-store'; import thunk from 'redux-thunk'; import { findAllRows, findColumnHeaderByName } from '../../setupTests'; @@ -32,7 +31,6 @@ describe('Investigation table component', () => { let state: StateType; let rowData: Investigation[]; let cartItems: DownloadCartItem[]; - let history: History; let user: ReturnType; let holder: HTMLElement; @@ -40,11 +38,11 @@ describe('Investigation table component', () => { const store = mockStore(state); return render( - + - + ); }; @@ -74,8 +72,6 @@ describe('Investigation table component', () => { endDate: '2019-07-24', }, ]; - history = createMemoryHistory(); - holder = document.createElement('div'); holder.setAttribute('id', 'datagateway-dataview'); document.body.appendChild(holder); @@ -275,8 +271,7 @@ describe('Investigation table component', () => { await user.type(filterInput, 'test'); - expect(history.length).toBe(5); - expect(history.location.search).toBe( + expect(window.location.search).toBe( `?filters=${encodeURIComponent( '{"name":{"value":"test","type":"include"}}' )}` @@ -284,8 +279,7 @@ describe('Investigation table component', () => { await user.clear(filterInput); - expect(history.length).toBe(6); - expect(history.location.search).toBe('?'); + expect(window.location.search).toBe(''); }); it('updates filter query params on date filter', async () => { @@ -297,8 +291,7 @@ describe('Investigation table component', () => { await user.type(filterInput, '2019-08-06'); - expect(history.length).toBe(2); - expect(history.location.search).toBe( + expect(window.location.search).toBe( `?filters=${encodeURIComponent( '{"startDate":{"startDate":"2019-08-06"}}' )}` @@ -309,8 +302,7 @@ describe('Investigation table component', () => { await user.keyboard('{Control}a{/Control}'); await user.keyboard('{Delete}'); - expect(history.length).toBe(3); - expect(history.location.search).toBe('?'); + expect(window.location.search).toBe(''); }); it('updates sort query params on sort', async () => { @@ -318,8 +310,7 @@ describe('Investigation table component', () => { await user.click(screen.getByText('investigations.title')); - expect(history.length).toBe(2); - expect(history.location.search).toBe( + expect(window.location.search).toBe( `?sort=${encodeURIComponent('{"title":"asc"}')}` ); }); diff --git a/packages/datagateway-dataview/src/views/table/investigationTable.component.tsx b/packages/datagateway-dataview/src/views/table/investigationTable.component.tsx index 214ceaf1d..8f592d627 100644 --- a/packages/datagateway-dataview/src/views/table/investigationTable.component.tsx +++ b/packages/datagateway-dataview/src/views/table/investigationTable.component.tsx @@ -26,7 +26,7 @@ import { import React from 'react'; import { useTranslation } from 'react-i18next'; import { useSelector } from 'react-redux'; -import { useLocation } from 'react-router-dom'; +import { useLocation } from 'react-router'; import { IndexRange, TableCellProps } from 'react-virtualized'; import { StateType } from '../../state/app.types'; diff --git a/packages/datagateway-dataview/src/views/table/isis/__snapshots__/isisDatasetsTable.component.test.tsx.snap b/packages/datagateway-dataview/src/views/table/isis/__snapshots__/isisDatasetsTable.component.test.tsx.snap index 7afd2b343..55e425069 100644 --- a/packages/datagateway-dataview/src/views/table/isis/__snapshots__/isisDatasetsTable.component.test.tsx.snap +++ b/packages/datagateway-dataview/src/views/table/isis/__snapshots__/isisDatasetsTable.component.test.tsx.snap @@ -3,6 +3,7 @@ exports[`ISIS Dataset table component > renders dataset name as a link 1`] = ` Test 1 @@ -12,6 +13,7 @@ exports[`ISIS Dataset table component > renders dataset name as a link 1`] = ` exports[`ISIS Dataset table component > renders dataset name as a link in data publication hierarchy 1`] = ` Test 1 diff --git a/packages/datagateway-dataview/src/views/table/isis/__snapshots__/isisFacilityCyclesTable.component.test.tsx.snap b/packages/datagateway-dataview/src/views/table/isis/__snapshots__/isisFacilityCyclesTable.component.test.tsx.snap index 1a5fa92c3..ceafba738 100644 --- a/packages/datagateway-dataview/src/views/table/isis/__snapshots__/isisFacilityCyclesTable.component.test.tsx.snap +++ b/packages/datagateway-dataview/src/views/table/isis/__snapshots__/isisFacilityCyclesTable.component.test.tsx.snap @@ -3,6 +3,7 @@ exports[`ISIS FacilityCycles table component > renders facilitycycle name as a link 1`] = ` Test 1 diff --git a/packages/datagateway-dataview/src/views/table/isis/isisDataPublicationsTable.component.test.tsx b/packages/datagateway-dataview/src/views/table/isis/isisDataPublicationsTable.component.test.tsx index 5cf1b64bd..490b5939b 100644 --- a/packages/datagateway-dataview/src/views/table/isis/isisDataPublicationsTable.component.test.tsx +++ b/packages/datagateway-dataview/src/views/table/isis/isisDataPublicationsTable.component.test.tsx @@ -2,6 +2,7 @@ import { initialState as dgDataViewInitialState } from '../../../state/reducers/ import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { + act, render, screen, waitFor, @@ -11,9 +12,8 @@ import { import userEvent from '@testing-library/user-event'; import axios, { AxiosResponse } from 'axios'; import { dGCommonInitialState, type DataPublication } from 'datagateway-common'; -import { createMemoryHistory, type History } from 'history'; import { Provider } from 'react-redux'; -import { Router, generatePath } from 'react-router-dom'; +import { BrowserRouter, Route, Routes, generatePath } from 'react-router'; import configureStore from 'redux-mock-store'; import thunk from 'redux-thunk'; import { paths } from '../../../page/pageContainer.component'; @@ -23,6 +23,7 @@ import { findColumnHeaderByName, findColumnIndexByName, findRowAt, + flushPromises, } from '../../../setupTests'; import type { StateType } from '../../../state/app.types'; import ISISDataPublicationsTable from './isisDataPublicationsTable.component'; @@ -31,12 +32,20 @@ describe('ISIS Data Publication table component', () => { const mockStore = configureStore([thunk]); let state: StateType; let rowData: DataPublication[]; - let history: History; let user: ReturnType; const renderComponent = (studyDataPublicationId?: string): RenderResult => { + window.history.replaceState( + {}, + '', + generatePath(paths.dataPublications.toggle.isisStudyDataPublication, { + instrumentId: 1, + }) + ); if (studyDataPublicationId) - history.replace( + window.history.replaceState( + {}, + '', generatePath( paths.dataPublications.toggle.isisInvestigationDataPublication, { @@ -48,14 +57,22 @@ describe('ISIS Data Publication table component', () => { const store = mockStore(state); return render( - + - + + } + /> + } + /> + - + ); }; @@ -86,13 +103,6 @@ describe('ISIS Data Publication table component', () => { }, }, ]; - history = createMemoryHistory({ - initialEntries: [ - generatePath(paths.dataPublications.toggle.isisStudyDataPublication, { - instrumentId: 1, - }), - ], - }); user = userEvent.setup({ delay: null, }); @@ -181,8 +191,8 @@ describe('ISIS Data Publication table component', () => { // user.type inputs the given string character by character to simulate user typing // each keystroke of user.type creates a new entry in the history stack // so the initial entry + 4 characters in "test" = 5 entries - expect(history.length).toBe(5); - expect(history.location.search).toBe( + + expect(window.location.search).toContain( `?filters=${encodeURIComponent( '{"title":{"value":"test","type":"include"}}' )}` @@ -190,17 +200,19 @@ describe('ISIS Data Publication table component', () => { await user.clear(filterInput); - expect(history.length).toBe(6); - expect(history.location.search).toBe('?'); + expect(window.location.search).not.toContain('filters='); }); it('uses default sort', async () => { renderComponent(); + await act(async () => { + await flushPromises(); + }); + expect(await screen.findAllByRole('gridcell')).toBeTruthy(); - expect(history.length).toBe(1); - expect(history.location.search).toBe( + expect(window.location.search).toBe( `?sort=${encodeURIComponent('{"title":"desc"}')}` ); @@ -218,8 +230,7 @@ describe('ISIS Data Publication table component', () => { await screen.findByRole('button', { name: 'datapublications.pid' }) ); - expect(history.length).toBe(2); - expect(history.location.search).toBe( + expect(window.location.search).toBe( `?sort=${encodeURIComponent('{"pid":"asc"}')}` ); }); @@ -305,8 +316,7 @@ describe('ISIS Data Publication table component', () => { await user.type(filterInput, '2019-08-06'); - expect(history.length).toBe(2); - expect(history.location.search).toBe( + expect(window.location.search).toContain( `?filters=${encodeURIComponent( '{"publicationDate":{"endDate":"2019-08-06"}}' )}` @@ -317,17 +327,19 @@ describe('ISIS Data Publication table component', () => { await user.keyboard('{Control}a{/Control}'); await user.keyboard('{Delete}'); - expect(history.length).toBe(3); - expect(history.location.search).toBe('?'); + expect(window.location.search).not.toContain('filters='); }); it('uses default sort', async () => { renderComponent('2'); + await act(async () => { + await flushPromises(); + }); + expect(await screen.findAllByRole('gridcell')).toBeTruthy(); - expect(history.length).toBe(1); - expect(history.location.search).toBe( + expect(window.location.search).toBe( `?sort=${encodeURIComponent('{"publicationDate":"desc"}')}` ); diff --git a/packages/datagateway-dataview/src/views/table/isis/isisDataPublicationsTable.component.tsx b/packages/datagateway-dataview/src/views/table/isis/isisDataPublicationsTable.component.tsx index c6845ff05..3cdab9309 100644 --- a/packages/datagateway-dataview/src/views/table/isis/isisDataPublicationsTable.component.tsx +++ b/packages/datagateway-dataview/src/views/table/isis/isisDataPublicationsTable.component.tsx @@ -18,15 +18,15 @@ import { IndexRange, TableCellProps } from 'react-virtualized'; import CalendarToday from '@mui/icons-material/CalendarToday'; import Fingerprint from '@mui/icons-material/Fingerprint'; import Public from '@mui/icons-material/Public'; -import { useLocation } from 'react-router-dom'; +import { useLocation, useParams } from 'react-router'; -interface ISISDataPublicationsTableProps { +interface BaseISISDataPublicationsTableProps { instrumentId: string; studyDataPublicationId?: string; } -const ISISDataPublicationsTable = ( - props: ISISDataPublicationsTableProps +const BaseISISDataPublicationsTable = ( + props: BaseISISDataPublicationsTableProps ): React.ReactElement => { const { instrumentId, studyDataPublicationId } = props; @@ -80,13 +80,14 @@ const ISISDataPublicationsTable = ( ]), ]); - // isMounted is used to disable queries when the component isn't fully mounted. + // isInitialised is used to disable queries when the component isn't fully initialised. // It prevents the request being sent twice if default sort is set. // It is not needed for cards/tables that don't have default sort. - const [isMounted, setIsMounted] = React.useState(false); + const [isInitialised, setIsInitialised] = React.useState(false); + React.useEffect(() => { - setIsMounted(true); - }, []); + if (!isInitialised && Object.keys(sort).length > 0) setIsInitialised(true); + }, [isInitialised, sort]); const { fetchNextPage, data } = useDataPublicationsInfinite( [ @@ -130,7 +131,7 @@ const ISISDataPublicationsTable = ( }, ]), ], - isMounted + isInitialised ); /* istanbul ignore next */ @@ -226,4 +227,14 @@ const ISISDataPublicationsTable = ( ); }; +const ISISDataPublicationsTable = () => { + const { instrumentId = '', studyDataPublicationId = '' } = useParams(); + return ( + + ); +}; + export default ISISDataPublicationsTable; diff --git a/packages/datagateway-dataview/src/views/table/isis/isisDatafilesTable.component.test.tsx b/packages/datagateway-dataview/src/views/table/isis/isisDatafilesTable.component.test.tsx index e88f48b83..e72918024 100644 --- a/packages/datagateway-dataview/src/views/table/isis/isisDatafilesTable.component.test.tsx +++ b/packages/datagateway-dataview/src/views/table/isis/isisDatafilesTable.component.test.tsx @@ -1,5 +1,6 @@ import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { + act, render, screen, waitFor, @@ -17,33 +18,52 @@ import { findCellInRow, findColumnIndexByName, } from 'datagateway-search/src/setupTests'; -import { createMemoryHistory, type History } from 'history'; import { Provider } from 'react-redux'; -import { Router } from 'react-router-dom'; +import { BrowserRouter, Route, Routes, generatePath } from 'react-router'; import configureStore from 'redux-mock-store'; import thunk from 'redux-thunk'; -import { findAllRows, findColumnHeaderByName } from '../../../setupTests'; +import { paths } from '../../../page/pageContainer.component'; +import { + findAllRows, + findColumnHeaderByName, + flushPromises, +} from '../../../setupTests'; import type { StateType } from '../../../state/app.types'; import { initialState as dgDataViewInitialState } from '../../../state/reducers/dgdataview.reducer'; import ISISDatafilesTable from './isisDatafilesTable.component'; +vi.mock('../../../page/idCheckFunctions', () => ({ + checkInstrumentId: vi.fn().mockResolvedValue(true), + checkStudyDataPublicationId: vi.fn().mockResolvedValue(true), + checkInstrumentAndFacilityCycleId: vi.fn().mockResolvedValue(true), + checkInvestigationId: vi.fn().mockResolvedValue(true), +})); + describe('ISIS datafiles table component', () => { const mockStore = configureStore([thunk]); let state: StateType; let rowData: Datafile[]; let cartItems: DownloadCartItem[]; - let history: History; let user: ReturnType; let holder: HTMLElement; const renderComponent = (): RenderResult => render( - + - + + } + /> + } + /> + - + ); @@ -61,7 +81,16 @@ describe('ISIS datafiles table component', () => { }, ]; cartItems = []; - history = createMemoryHistory(); + window.history.replaceState( + {}, + '', + generatePath(paths.standard.isisDatafile, { + datasetId: '1', + investigationId: '2', + facilityCycleId: '3', + instrumentId: '4', + }) + ); user = userEvent.setup(); holder = document.createElement('div'); @@ -99,6 +128,17 @@ describe('ISIS datafiles table component', () => { }); } + if (/\/datapublications$/.test(url)) { + return Promise.resolve({ + data: { + id: 1, + content: { + dataCollectionInvestigations: [{ investigation: { id: 5 } }], + }, + }, + }); + } + return Promise.reject(`Endpoint not mocked: ${url}`); }); @@ -203,6 +243,29 @@ describe('ISIS datafiles table component', () => { ).toBeInTheDocument(); }); + it('renders correctly in data publications hierarchy', async () => { + window.history.replaceState( + {}, + '', + generatePath(paths.dataPublications.standard.isisDatafile, { + datasetId: '1', + investigationId: '2', + dataPublicationId: '3', + instrumentId: '4', + }) + ); + + renderComponent(); + + // wait for rows to show up + await waitFor( + async () => { + expect(await findAllRows()).toHaveLength(1); + }, + { timeout: 5_000 } + ); + }); + it('updates filter query params on text filter', async () => { renderComponent(); @@ -216,8 +279,8 @@ describe('ISIS datafiles table component', () => { // user.type inputs the given string character by character to simulate user typing // each keystroke of user.type creates a new entry in the history stack // so the initial entry + 4 characters in "test" = 5 entries - expect(history.length).toBe(5); - expect(history.location.search).toBe( + + expect(window.location.search).toContain( `?filters=${encodeURIComponent( '{"name":{"value":"test","type":"include"}}' )}` @@ -225,8 +288,7 @@ describe('ISIS datafiles table component', () => { await user.clear(filterInput); - expect(history.length).toBe(6); - expect(history.location.search).toBe('?'); + expect(window.location.search).not.toContain('filters='); }); it('updates filter query params on date filter', async () => { @@ -238,8 +300,7 @@ describe('ISIS datafiles table component', () => { await user.type(filterInput, '2019-08-06'); - expect(history.length).toBe(2); - expect(history.location.search).toBe( + expect(window.location.search).toContain( `?filters=${encodeURIComponent( '{"datafileModTime":{"endDate":"2019-08-06"}}' )}` @@ -250,17 +311,19 @@ describe('ISIS datafiles table component', () => { await user.keyboard('{Control}a{/Control}'); await user.keyboard('{Delete}'); - expect(history.length).toBe(3); - expect(history.location.search).toBe('?'); + expect(window.location.search).not.toContain('filters='); }); it('uses default sort', async () => { renderComponent(); + await act(async () => { + await flushPromises(); + }); + expect(await screen.findAllByRole('gridcell')).toBeTruthy(); - expect(history.length).toBe(1); - expect(history.location.search).toBe( + expect(window.location.search).toBe( `?sort=${encodeURIComponent('{"name":"asc"}')}` ); @@ -278,8 +341,7 @@ describe('ISIS datafiles table component', () => { // click on the datafiles.name column header await user.click(await screen.findByText('datafiles.name')); - expect(history.length).toBe(2); - expect(history.location.search).toBe( + expect(window.location.search).toBe( `?sort=${encodeURIComponent('{"name":"desc"}')}` ); }); @@ -358,14 +420,22 @@ describe('ISIS datafiles table component', () => { it('renders actions correctly', async () => { renderComponent(); expect( - await screen.findByRole('button', { name: 'buttons.download' }) - ).toBeTruthy(); + await screen.findByRole( + 'button', + { name: 'buttons.download' }, + { timeout: 5_000 } + ) + ).toBeInTheDocument(); }); it('displays details panel when expanded', async () => { renderComponent(); await user.click( - await screen.findByRole('button', { name: 'Show details' }) + await screen.findByRole( + 'button', + { name: 'Show details' }, + { timeout: 5_000 } + ) ); expect( await screen.findByTestId('isis-datafile-details-panel') diff --git a/packages/datagateway-dataview/src/views/table/isis/isisDatafilesTable.component.tsx b/packages/datagateway-dataview/src/views/table/isis/isisDatafilesTable.component.tsx index 874470074..5a2b63886 100644 --- a/packages/datagateway-dataview/src/views/table/isis/isisDatafilesTable.component.tsx +++ b/packages/datagateway-dataview/src/views/table/isis/isisDatafilesTable.component.tsx @@ -13,6 +13,7 @@ import { parseSearchToQuery, useAddToCart, useCart, + useDataPublication, useDatafileCount, useDatafilesInfinite, useDateFilter, @@ -24,18 +25,25 @@ import { import React from 'react'; import { useTranslation } from 'react-i18next'; import { useSelector } from 'react-redux'; -import { useLocation } from 'react-router-dom'; +import { useLocation, useParams } from 'react-router'; import { IndexRange } from 'react-virtualized'; +import { + checkInstrumentAndFacilityCycleId, + checkInstrumentId, + checkInvestigationId, + checkStudyDataPublicationId, +} from '../../../page/idCheckFunctions'; +import WithIdCheck from '../../../page/withIdCheck'; import { StateType } from '../../../state/app.types'; import PreviewDatafileButton from '../../datafilePreview/previewDatafileButton.component'; -interface ISISDatafilesTableProps { +interface BaseISISDatafilesTableProps { datasetId: string; investigationId: string; } -const ISISDatafilesTable = ( - props: ISISDatafilesTableProps +const BaseISISDatafilesTable = ( + props: BaseISISDatafilesTableProps ): React.ReactElement => { const { datasetId, investigationId } = props; @@ -79,13 +87,14 @@ const ISISDatafilesTable = ( }, ]); - // isMounted is used to disable queries when the component isn't fully mounted. + // isInitialised is used to disable queries when the component isn't fully initialised. // It prevents the request being sent twice if default sort is set. // It is not needed for cards/tables that don't have default sort. - const [isMounted, setIsMounted] = React.useState(false); + const [isInitialised, setIsInitialised] = React.useState(false); + React.useEffect(() => { - setIsMounted(true); - }, []); + if (!isInitialised && Object.keys(sort).length > 0) setIsInitialised(true); + }, [isInitialised, sort]); const { fetchNextPage, data } = useDatafilesInfinite( [ @@ -94,7 +103,7 @@ const ISISDatafilesTable = ( filterValue: JSON.stringify({ 'dataset.id': { eq: datasetId } }), }, ], - isMounted + isInitialised ); const loadMoreRows = React.useCallback( @@ -210,4 +219,55 @@ const ISISDatafilesTable = ( ); }; +const ISISDatafilesTable = (props: { dataPublication: boolean }) => { + const { + instrumentId = '', + facilityCycleId = '', + dataPublicationId = '', + investigationId = '', + datasetId = '', + } = useParams(); + const { data, isPending } = useDataPublication( + parseInt(investigationId), + props.dataPublication + ); + const dataPublicationInvestigationId = + data?.content?.dataCollectionInvestigations?.[0]?.investigation?.id; + + const checkingPromise = props.dataPublication + ? Promise.all([ + checkInstrumentId(parseInt(instrumentId), parseInt(dataPublicationId)), + checkStudyDataPublicationId( + parseInt(dataPublicationId), + parseInt(investigationId) + ), + checkInvestigationId( + dataPublicationInvestigationId ?? -1, + parseInt(datasetId) + ), + ...(isPending ? [new Promise(() => undefined)] : []), + ]).then((values) => !values.includes(false)) + : Promise.all([ + checkInstrumentAndFacilityCycleId( + parseInt(instrumentId), + parseInt(facilityCycleId), + parseInt(investigationId) + ), + checkInvestigationId(parseInt(investigationId), parseInt(datasetId)), + ]).then((values) => !values.includes(false)); + + return ( + + + + ); +}; + export default ISISDatafilesTable; diff --git a/packages/datagateway-dataview/src/views/table/isis/isisDatasetsTable.component.test.tsx b/packages/datagateway-dataview/src/views/table/isis/isisDatasetsTable.component.test.tsx index cb8d136c3..5932df6be 100644 --- a/packages/datagateway-dataview/src/views/table/isis/isisDatasetsTable.component.test.tsx +++ b/packages/datagateway-dataview/src/views/table/isis/isisDatasetsTable.component.test.tsx @@ -1,5 +1,6 @@ import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { + act, render, screen, waitFor, @@ -17,16 +18,22 @@ import { useRemoveFromCart, type Dataset, } from 'datagateway-common'; -import { createMemoryHistory, type History } from 'history'; import { Provider } from 'react-redux'; -import { Router, generatePath } from 'react-router-dom'; +import { BrowserRouter, Route, Routes, generatePath } from 'react-router'; import configureStore from 'redux-mock-store'; import thunk from 'redux-thunk'; import { paths } from '../../../page/pageContainer.component'; +import { flushPromises } from '../../../setupTests'; import type { StateType } from '../../../state/app.types'; import { initialState as dgDataViewInitialState } from '../../../state/reducers/dgdataview.reducer'; import ISISDatasetsTable from './isisDatasetsTable.component'; +vi.mock('../../../page/idCheckFunctions', () => ({ + checkInstrumentId: vi.fn().mockResolvedValue(true), + checkStudyDataPublicationId: vi.fn().mockResolvedValue(true), + checkInstrumentAndFacilityCycleId: vi.fn().mockResolvedValue(true), +})); + vi.mock('datagateway-common', async () => { const originalModule = await vi.importActual('datagateway-common'); @@ -46,18 +53,27 @@ describe('ISIS Dataset table component', () => { const mockStore = configureStore([thunk]); let state: StateType; let rowData: Dataset[]; - let history: History; let user: ReturnType; const renderComponent = (): RenderResult => { const store = mockStore(state); return render( - + - + + } + /> + } + /> + + - + ); }; @@ -71,15 +87,15 @@ describe('ISIS Dataset table component', () => { createTime: '2019-07-23', }, ]; - history = createMemoryHistory({ - initialEntries: [ - generatePath(paths.toggle.isisDataset, { - instrumentId: '1', - investigationId: '3', - facilityCycleId: '2', - }), - ], - }); + window.history.replaceState( + {}, + '', + generatePath(paths.toggle.isisDataset, { + instrumentId: '1', + facilityCycleId: '2', + investigationId: '3', + }) + ); user = userEvent.setup(); state = JSON.parse( @@ -121,6 +137,17 @@ describe('ISIS Dataset table component', () => { }); } + if (/\/datapublications$/.test(url)) { + return Promise.resolve({ + data: { + id: 1, + content: { + dataCollectionInvestigations: [{ investigation: { id: 5 } }], + }, + }, + }); + } + return Promise.reject(`Endpoint not mocked: ${url}`); }); }); @@ -139,8 +166,7 @@ describe('ISIS Dataset table component', () => { await user.type(filterInput, 'test'); - expect(history.length).toBe(5); - expect(history.location.search).toBe( + expect(window.location.search).toContain( `?filters=${encodeURIComponent( '{"name":{"value":"test","type":"include"}}' )}` @@ -148,8 +174,7 @@ describe('ISIS Dataset table component', () => { await user.clear(filterInput); - expect(history.length).toBe(6); - expect(history.location.search).toBe('?'); + expect(window.location.search).not.toContain('filters='); }); it('updates filter query params on date filter', async () => { @@ -161,7 +186,7 @@ describe('ISIS Dataset table component', () => { await user.type(filterInput, '2019-08-06'); - expect(history.location.search).toBe( + expect(window.location.search).toContain( `?filters=${encodeURIComponent('{"modTime":{"endDate":"2019-08-06"}}')}` ); @@ -170,27 +195,28 @@ describe('ISIS Dataset table component', () => { await user.keyboard('{Control}a{/Control}'); await user.keyboard('{Delete}'); - expect(history.length).toBe(3); - expect(history.location.search).toBe('?'); + expect(window.location.search).not.toContain('filters='); }); it('uses default sort', async () => { renderComponent(); + await act(async () => { + await flushPromises(); + }); + expect(await screen.findAllByRole('gridcell')).toBeTruthy(); - expect(history.length).toBe(1); - expect(history.location.search).toBe( + expect(window.location.search).toBe( `?sort=${encodeURIComponent('{"name":"asc"}')}` ); - // check that the data request is sent only once after mounting - expect(useDatasetsInfinite).toHaveBeenCalledTimes(2); - expect(useDatasetsInfinite).toHaveBeenCalledWith(expect.anything(), false); - expect(useDatasetsInfinite).toHaveBeenLastCalledWith( - expect.anything(), - true - ); + // check that the data hook is only called once with the query enabled + expect( + vi + .mocked(useDatasetsInfinite) + .mock.calls.filter((call) => call[1] === true) + ).toHaveLength(1); }); it('updates sort query params on sort', async () => { @@ -198,8 +224,7 @@ describe('ISIS Dataset table component', () => { await user.click(await screen.findByText('datasets.name')); - expect(history.length).toBe(2); - expect(history.location.search).toBe( + expect(window.location.search).toBe( `?sort=${encodeURIComponent('{"name":"desc"}')}` ); }); @@ -313,18 +338,20 @@ describe('ISIS Dataset table component', () => { await screen.findByRole('tab', { name: 'datasets.details.datafiles' }) ); - expect(history.location.pathname).toBe( + expect(window.location.pathname).toBe( '/browse/instrument/1/facilityCycle/2/investigation/3/dataset/1/datafile' ); }); - it('renders dataset name as a link', () => { + it('renders dataset name as a link', async () => { renderComponent(); - expect(screen.getByText('Test 1')).toMatchSnapshot(); + expect(await screen.findByText('Test 1')).toMatchSnapshot(); }); - it('renders dataset name as a link in data publication hierarchy', () => { - history.replace( + it('renders dataset name as a link in data publication hierarchy', async () => { + window.history.replaceState( + {}, + '', generatePath(paths.dataPublications.toggle.isisDataset, { instrumentId: '1', investigationId: '3', @@ -333,7 +360,7 @@ describe('ISIS Dataset table component', () => { ); renderComponent(); - expect(screen.getByText('Test 1')).toMatchSnapshot(); + expect(await screen.findByText('Test 1')).toMatchSnapshot(); }); it('renders actions correctly', async () => { diff --git a/packages/datagateway-dataview/src/views/table/isis/isisDatasetsTable.component.tsx b/packages/datagateway-dataview/src/views/table/isis/isisDatasetsTable.component.tsx index 09b0852fa..27f95f997 100644 --- a/packages/datagateway-dataview/src/views/table/isis/isisDatasetsTable.component.tsx +++ b/packages/datagateway-dataview/src/views/table/isis/isisDatasetsTable.component.tsx @@ -14,6 +14,7 @@ import { tableLink, useAddToCart, useCart, + useDataPublication, useDatasetCount, useDatasetsInfinite, useDateFilter, @@ -25,16 +26,22 @@ import { import React from 'react'; import { useTranslation } from 'react-i18next'; import { useSelector } from 'react-redux'; -import { useHistory, useLocation } from 'react-router-dom'; +import { useLocation, useNavigate, useParams } from 'react-router'; import { IndexRange, TableCellProps } from 'react-virtualized'; +import { + checkInstrumentAndFacilityCycleId, + checkInstrumentId, + checkStudyDataPublicationId, +} from '../../../page/idCheckFunctions'; +import WithIdCheck from '../../../page/withIdCheck'; import { StateType } from '../../../state/app.types'; -interface ISISDatasetsTableProps { +interface BaseISISDatasetsTableProps { investigationId: string; } -const ISISDatasetsTable = ( - props: ISISDatasetsTableProps +const BaseISISDatasetsTable = ( + props: BaseISISDatasetsTableProps ): React.ReactElement => { const { investigationId } = props; @@ -42,7 +49,7 @@ const ISISDatasetsTable = ( const location = useLocation(); - const { push } = useHistory(); + const navigate = useNavigate(); const disableSelectAll = useSelector( (state: StateType) => state.dgcommon.features?.disableSelectAll ?? false @@ -84,13 +91,14 @@ const ISISDatasetsTable = ( }, ]); - // isMounted is used to disable queries when the component isn't fully mounted. + // isInitialised is used to disable queries when the component isn't fully initialised. // It prevents the request being sent twice if default sort is set. // It is not needed for cards/tables that don't have default sort. - const [isMounted, setIsMounted] = React.useState(false); + const [isInitialised, setIsInitialised] = React.useState(false); + React.useEffect(() => { - setIsMounted(true); - }, []); + if (!isInitialised && Object.keys(sort).length > 0) setIsInitialised(true); + }, [isInitialised, sort]); const { fetchNextPage, data } = useDatasetsInfinite( [ @@ -101,7 +109,7 @@ const ISISDatasetsTable = ( }), }, ], - isMounted + isInitialised ); const loadMoreRows = React.useCallback( @@ -188,11 +196,11 @@ const ISISDatasetsTable = ( rowData={rowData} detailsPanelResize={detailsPanelResize} viewDatafiles={(id: number) => - push(`${location.pathname}/${id}/datafile`) + navigate(`${location.pathname}/${id}/datafile`) } /> ), - [location.pathname, push] + [location.pathname, navigate] ); return ( @@ -230,4 +238,47 @@ const ISISDatasetsTable = ( ); }; +const ISISDatasetsTable = (props: { dataPublication: boolean }) => { + const { + instrumentId = '', + dataPublicationId = '', + facilityCycleId = '', + investigationId = '', + } = useParams(); + const { data, isPending } = useDataPublication( + parseInt(investigationId), + props.dataPublication + ); + + const dataPublicationInvestigationId = + data?.content?.dataCollectionInvestigations?.[0]?.investigation?.id; + + const checkingPromise = props.dataPublication + ? Promise.all([ + checkInstrumentId(parseInt(instrumentId), parseInt(dataPublicationId)), + checkStudyDataPublicationId( + parseInt(dataPublicationId), + parseInt(investigationId) + ), + ...(isPending ? [new Promise(() => undefined)] : []), + ]).then((values) => !values.includes(false)) + : checkInstrumentAndFacilityCycleId( + parseInt(instrumentId), + parseInt(facilityCycleId), + parseInt(investigationId) + ); + + return ( + + + + ); +}; + export default ISISDatasetsTable; diff --git a/packages/datagateway-dataview/src/views/table/isis/isisFacilityCyclesTable.component.test.tsx b/packages/datagateway-dataview/src/views/table/isis/isisFacilityCyclesTable.component.test.tsx index 056f98424..05d565f41 100644 --- a/packages/datagateway-dataview/src/views/table/isis/isisFacilityCyclesTable.component.test.tsx +++ b/packages/datagateway-dataview/src/views/table/isis/isisFacilityCyclesTable.component.test.tsx @@ -1,5 +1,6 @@ import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { + act, render, screen, within, @@ -12,17 +13,17 @@ import { useFacilityCyclesInfinite, type FacilityCycle, } from 'datagateway-common'; -import { createMemoryHistory, type History } from 'history'; import { Provider } from 'react-redux'; -import { Router } from 'react-router-dom'; +import { BrowserRouter, Route, Routes, generatePath } from 'react-router'; import configureStore from 'redux-mock-store'; import thunk from 'redux-thunk'; -import type { MockInstance } from 'vitest'; +import { paths } from '../../../page/pageContainer.component'; import { findAllRows, findCellInRow, findColumnHeaderByName, findColumnIndexByName, + flushPromises, } from '../../../setupTests'; import type { StateType } from '../../../state/app.types'; import { initialState as dgDataViewInitialState } from '../../../state/reducers/dgdataview.reducer'; @@ -43,19 +44,22 @@ describe('ISIS FacilityCycles table component', () => { const mockStore = configureStore([thunk]); let state: StateType; let rowData: FacilityCycle[]; - let history: History; - let replaceSpy: MockInstance; let user: ReturnType; const renderComponent = (): RenderResult => { const store = mockStore(state); return render( - + - + + } + /> + - + ); }; @@ -70,8 +74,13 @@ describe('ISIS FacilityCycles table component', () => { endDate: '2019-07-04', }, ]; - history = createMemoryHistory(); - replaceSpy = vi.spyOn(history, 'replace'); + window.history.replaceState( + {}, + '', + generatePath(paths.toggle.isisFacilityCycle, { + instrumentId: '1', + }) + ); user = userEvent.setup(); state = JSON.parse( @@ -152,8 +161,8 @@ describe('ISIS FacilityCycles table component', () => { // user.type inputs the given string character by character to simulate user typing // each keystroke of user.type creates a new entry in the history stack // so the initial entry + 4 characters in "test" = 5 entries - expect(history.length).toBe(5); - expect(history.location.search).toBe( + + expect(window.location.search).toContain( `?filters=${encodeURIComponent( '{"name":{"value":"test","type":"include"}}' )}` @@ -161,8 +170,7 @@ describe('ISIS FacilityCycles table component', () => { await user.clear(filterInput); - expect(history.length).toBe(6); - expect(history.location.search).toBe('?'); + expect(window.location.search).not.toContain('filters='); }); it('updates filter query params on date filter', async () => { @@ -174,8 +182,7 @@ describe('ISIS FacilityCycles table component', () => { await user.type(filterInput, '2019-08-06'); - expect(history.length).toBe(2); - expect(history.location.search).toBe( + expect(window.location.search).toContain( `?filters=${encodeURIComponent('{"endDate":{"endDate":"2019-08-06"}}')}` ); @@ -184,30 +191,28 @@ describe('ISIS FacilityCycles table component', () => { await user.keyboard('{Control}a{/Control}'); await user.keyboard('{Delete}'); - expect(history.length).toBe(3); - expect(history.location.search).toBe('?'); + expect(window.location.search).not.toContain('filters='); }); it('uses default sort', async () => { renderComponent(); - expect(await screen.findAllByRole('gridcell')).toBeTruthy(); - - expect(history.length).toBe(1); - expect(replaceSpy).toHaveBeenCalledWith({ - search: `?sort=${encodeURIComponent('{"startDate":"desc"}')}`, + await act(async () => { + await flushPromises(); }); - // check that the data request is sent only once after mounting - expect(useFacilityCyclesInfinite).toHaveBeenCalledTimes(2); - expect(useFacilityCyclesInfinite).toHaveBeenCalledWith( - expect.anything(), - false - ); - expect(useFacilityCyclesInfinite).toHaveBeenLastCalledWith( - expect.anything(), - true + expect(await screen.findAllByRole('gridcell')).toBeTruthy(); + + expect(window.location.search).toBe( + `?sort=${encodeURIComponent('{"startDate":"desc"}')}` ); + + // check that the data hook is only called once with the query enabled + expect( + vi + .mocked(useFacilityCyclesInfinite) + .mock.calls.filter((call) => call[1] === true) + ).toHaveLength(1); }); it('updates sort query params on sort', async () => { @@ -215,8 +220,7 @@ describe('ISIS FacilityCycles table component', () => { await user.click(await screen.findByText('facilitycycles.name')); - expect(history.length).toBe(2); - expect(history.location.search).toBe( + expect(window.location.search).toBe( `?sort=${encodeURIComponent('{"name":"asc"}')}` ); }); diff --git a/packages/datagateway-dataview/src/views/table/isis/isisFacilityCyclesTable.component.tsx b/packages/datagateway-dataview/src/views/table/isis/isisFacilityCyclesTable.component.tsx index a0066bb26..2345fcc18 100644 --- a/packages/datagateway-dataview/src/views/table/isis/isisFacilityCyclesTable.component.tsx +++ b/packages/datagateway-dataview/src/views/table/isis/isisFacilityCyclesTable.component.tsx @@ -14,15 +14,15 @@ import { } from 'datagateway-common'; import React from 'react'; import { useTranslation } from 'react-i18next'; -import { useLocation } from 'react-router-dom'; +import { useLocation, useParams } from 'react-router'; import { IndexRange, TableCellProps } from 'react-virtualized'; -interface ISISFacilityCyclesTableProps { +interface BaseISISFacilityCyclesTableProps { instrumentId: string; } -const ISISFacilityCyclesTable = ( - props: ISISFacilityCyclesTableProps +const BaseISISFacilityCyclesTable = ( + props: BaseISISFacilityCyclesTableProps ): React.ReactElement => { const { instrumentId } = props; @@ -34,20 +34,21 @@ const ISISFacilityCyclesTable = ( [location.search] ); - // isMounted is used to disable queries when the component isn't fully mounted. + // isInitialised is used to disable queries when the component isn't fully initialised. // It prevents the request being sent twice if default sort is set. // It is not needed for cards/tables that don't have default sort. - const [isMounted, setIsMounted] = React.useState(false); + const [isInitialised, setIsInitialised] = React.useState(false); + React.useEffect(() => { - setIsMounted(true); - }, []); + if (!isInitialised && Object.keys(sort).length > 0) setIsInitialised(true); + }, [isInitialised, sort]); const { data: totalDataCount } = useFacilityCycleCount( parseInt(instrumentId) ); const { fetchNextPage, data } = useFacilityCyclesInfinite( parseInt(instrumentId), - isMounted + isInitialised ); /* istanbul ignore next */ @@ -115,4 +116,9 @@ const ISISFacilityCyclesTable = ( ); }; +const ISISFacilityCyclesTable = () => { + const { instrumentId = '' } = useParams(); + return ; +}; + export default ISISFacilityCyclesTable; diff --git a/packages/datagateway-dataview/src/views/table/isis/isisInstrumentsTable.component.test.tsx b/packages/datagateway-dataview/src/views/table/isis/isisInstrumentsTable.component.test.tsx index 4ed549e09..b52cdbcae 100644 --- a/packages/datagateway-dataview/src/views/table/isis/isisInstrumentsTable.component.test.tsx +++ b/packages/datagateway-dataview/src/views/table/isis/isisInstrumentsTable.component.test.tsx @@ -1,5 +1,6 @@ import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { + act, render, screen, within, @@ -8,14 +9,13 @@ import { import userEvent from '@testing-library/user-event'; import axios, { AxiosResponse } from 'axios'; import { - dGCommonInitialState, Instrument, + dGCommonInitialState, useInstrumentCount, useInstrumentsInfinite, } from 'datagateway-common'; -import { createMemoryHistory, History } from 'history'; import { Provider } from 'react-redux'; -import { Router } from 'react-router-dom'; +import { BrowserRouter } from 'react-router'; import configureStore from 'redux-mock-store'; import thunk from 'redux-thunk'; import { @@ -23,6 +23,7 @@ import { findCellInRow, findColumnHeaderByName, findColumnIndexByName, + flushPromises, } from '../../../setupTests'; import { StateType } from '../../../state/app.types'; import { initialState as dgDataViewInitialState } from '../../../state/reducers/dgdataview.reducer'; @@ -43,18 +44,17 @@ describe('ISIS Instruments table component', () => { const mockStore = configureStore([thunk]); let state: StateType; let rowData: Instrument[]; - let history: History; let user: ReturnType; const renderComponent = (dataPublication = false): RenderResult => { const store = mockStore(state); return render( - + - + ); }; @@ -77,7 +77,7 @@ describe('ISIS Instruments table component', () => { type: 'type2', }, ]; - history = createMemoryHistory(); + window.history.replaceState({}, '', '/'); user = userEvent.setup(); state = JSON.parse( @@ -175,8 +175,8 @@ describe('ISIS Instruments table component', () => { // user.type inputs the given string character by character to simulate user typing // each keystroke of user.type creates a new entry in the history stack // so the initial entry + 4 characters in "test" = 5 entries - expect(history.length).toBe(5); - expect(history.location.search).toBe( + + expect(window.location.search).toContain( `?filters=${encodeURIComponent( '{"fullName":{"value":"test","type":"include"}}' )}` @@ -184,24 +184,28 @@ describe('ISIS Instruments table component', () => { await user.clear(filterInput); - expect(history.length).toBe(6); - expect(history.location.search).toBe('?'); + expect(window.location.search).not.toContain('filters='); }); it('uses default sort', async () => { renderComponent(); + await act(async () => { + await flushPromises(); + }); + expect(await screen.findAllByRole('gridcell')).toBeTruthy(); - expect(history.length).toBe(1); - expect(history.location.search).toBe( + expect(window.location.search).toBe( `?sort=${encodeURIComponent('{"fullName":"asc"}')}` ); - // check that the data request is sent only once after mounting - expect(useInstrumentsInfinite).toHaveBeenCalledTimes(2); - expect(useInstrumentsInfinite).toHaveBeenCalledWith(undefined, false); - expect(useInstrumentsInfinite).toHaveBeenLastCalledWith(undefined, true); + // check that the data hook is only called once with the query enabled + expect( + vi + .mocked(useInstrumentsInfinite) + .mock.calls.filter((call) => call[1] === true) + ).toHaveLength(1); }); it('updates sort query params on sort', async () => { @@ -209,8 +213,7 @@ describe('ISIS Instruments table component', () => { await user.click(await screen.findByText('instruments.name')); - expect(history.length).toBe(2); - expect(history.location.search).toBe( + expect(window.location.search).toBe( `?sort=${encodeURIComponent('{"fullName":"desc"}')}` ); }); diff --git a/packages/datagateway-dataview/src/views/table/isis/isisInstrumentsTable.component.tsx b/packages/datagateway-dataview/src/views/table/isis/isisInstrumentsTable.component.tsx index e26fb51eb..d3575d4bb 100644 --- a/packages/datagateway-dataview/src/views/table/isis/isisInstrumentsTable.component.tsx +++ b/packages/datagateway-dataview/src/views/table/isis/isisInstrumentsTable.component.tsx @@ -13,7 +13,7 @@ import { } from 'datagateway-common'; import React from 'react'; import { useTranslation } from 'react-i18next'; -import { useLocation } from 'react-router-dom'; +import { useLocation } from 'react-router'; import { IndexRange, TableCellProps } from 'react-virtualized'; interface ISISInstrumentsTableProps { @@ -33,16 +33,20 @@ const ISISInstrumentsTable = ( [location.search] ); - // isMounted is used to disable queries when the component isn't fully mounted. + // isInitialised is used to disable queries when the component isn't fully initialised. // It prevents the request being sent twice if default sort is set. // It is not needed for cards/tables that don't have default sort. - const [isMounted, setIsMounted] = React.useState(false); + const [isInitialised, setIsInitialised] = React.useState(false); + React.useEffect(() => { - setIsMounted(true); - }, []); + if (!isInitialised && Object.keys(sort).length > 0) setIsInitialised(true); + }, [isInitialised, sort]); const { data: totalDataCount } = useInstrumentCount(); - const { fetchNextPage, data } = useInstrumentsInfinite(undefined, isMounted); + const { fetchNextPage, data } = useInstrumentsInfinite( + undefined, + isInitialised + ); /* istanbul ignore next */ const aggregatedData: Instrument[] = React.useMemo(() => { diff --git a/packages/datagateway-dataview/src/views/table/isis/isisInvestigationsTable.component.test.tsx b/packages/datagateway-dataview/src/views/table/isis/isisInvestigationsTable.component.test.tsx index 931241fd9..e9ae27415 100644 --- a/packages/datagateway-dataview/src/views/table/isis/isisInvestigationsTable.component.test.tsx +++ b/packages/datagateway-dataview/src/views/table/isis/isisInvestigationsTable.component.test.tsx @@ -1,5 +1,6 @@ import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { + act, render, screen, waitFor, @@ -13,12 +14,10 @@ import { dGCommonInitialState, type Investigation, } from 'datagateway-common'; -import { createMemoryHistory, type History } from 'history'; import { Provider } from 'react-redux'; -import { Router, generatePath } from 'react-router-dom'; +import { BrowserRouter, Route, Routes, generatePath } from 'react-router'; import configureStore from 'redux-mock-store'; import thunk from 'redux-thunk'; -import type { MockInstance } from 'vitest'; import { paths } from '../../../page/pageContainer.component'; import { findAllRows, @@ -26,6 +25,7 @@ import { findColumnHeaderByName, findColumnIndexByName, findRowAt, + flushPromises, } from '../../../setupTests'; import type { StateType } from '../../../state/app.types'; import { initialState as dgDataViewInitialState } from '../../../state/reducers/dgdataview.reducer'; @@ -35,8 +35,6 @@ describe('ISIS Investigations table component', () => { const mockStore = configureStore([thunk]); let state: StateType; let rowData: Investigation[]; - let history: History; - let replaceSpy: MockInstance; let user: ReturnType; let cartItems: DownloadCartItem[]; let holder: HTMLElement; @@ -45,11 +43,16 @@ describe('ISIS Investigations table component', () => { const store = mockStore(state); return render( - + - + + } + /> + - + ); }; @@ -120,15 +123,14 @@ describe('ISIS Investigations table component', () => { endDate: '2019-06-11', }, ]; - history = createMemoryHistory({ - initialEntries: [ - generatePath(paths.toggle.isisInvestigation, { - instrumentId: '4', - facilityCycleId: '5', - }), - ], - }); - replaceSpy = vi.spyOn(history, 'replace'); + window.history.replaceState( + {}, + '', + generatePath(paths.toggle.isisInvestigation, { + instrumentId: '4', + facilityCycleId: '5', + }) + ); user = userEvent.setup(); holder = document.createElement('div'); @@ -331,8 +333,8 @@ describe('ISIS Investigations table component', () => { // user.type inputs the given string character by character to simulate user typing // each keystroke of user.type creates a new entry in the history stack // so the initial entry + 4 characters in "test" = 5 entries - expect(history.length).toBe(5); - expect(history.location.search).toBe( + + expect(window.location.search).toContain( `?filters=${encodeURIComponent( '{"name":{"value":"test","type":"include"}}' )}` @@ -340,8 +342,7 @@ describe('ISIS Investigations table component', () => { await user.clear(filterInput); - expect(history.length).toBe(6); - expect(history.location.search).toBe('?'); + expect(window.location.search).not.toContain('filters='); }); it('updates filter query params on date filter', async () => { @@ -353,8 +354,7 @@ describe('ISIS Investigations table component', () => { await user.type(filterInput, '2019-08-06'); - expect(history.length).toBe(2); - expect(history.location.search).toBe( + expect(window.location.search).toContain( `?filters=${encodeURIComponent( '{"startDate":{"startDate":"2019-08-06"}}' )}` @@ -365,21 +365,23 @@ describe('ISIS Investigations table component', () => { await user.keyboard('{Control}a{/Control}'); await user.keyboard('{Delete}'); - expect(history.length).toBe(3); - expect(history.location.search).toBe('?'); + expect(window.location.search).not.toContain('filters='); }); it('uses default sort', async () => { renderComponent(); + await act(async () => { + await flushPromises(); + }); + expect(await screen.findAllByRole('gridcell')).toBeTruthy(); - expect(history.length).toBe(1); - expect(replaceSpy).toHaveBeenCalledWith({ - search: `?sort=${encodeURIComponent('{"startDate":"desc"}')}`, - }); + expect(window.location.search).toBe( + `?sort=${encodeURIComponent('{"startDate":"desc"}')}` + ); - // check that the data request is sent only once after mounting + // check that the data request is sent only once after mounting & default sort is set const datafilesCalls = vi .mocked(axios.get) .mock.calls.filter((call) => call[0] === '/investigations'); @@ -394,8 +396,7 @@ describe('ISIS Investigations table component', () => { await screen.findByRole('button', { name: 'investigations.title' }) ); - expect(history.length).toBe(2); - expect(history.location.search).toBe( + expect(window.location.search).toBe( `?sort=${encodeURIComponent('{"title":"asc"}')}` ); }); diff --git a/packages/datagateway-dataview/src/views/table/isis/isisInvestigationsTable.component.tsx b/packages/datagateway-dataview/src/views/table/isis/isisInvestigationsTable.component.tsx index 51accfff3..333d90a28 100644 --- a/packages/datagateway-dataview/src/views/table/isis/isisInvestigationsTable.component.tsx +++ b/packages/datagateway-dataview/src/views/table/isis/isisInvestigationsTable.component.tsx @@ -31,17 +31,17 @@ import { import React from 'react'; import { useTranslation } from 'react-i18next'; import { useSelector } from 'react-redux'; -import { useHistory, useLocation } from 'react-router-dom'; +import { useLocation, useNavigate, useParams } from 'react-router'; import { IndexRange, TableCellProps } from 'react-virtualized'; import { StateType } from '../../../state/app.types'; -interface ISISInvestigationsTableProps { +interface BaseISISInvestigationsTableProps { instrumentId: string; facilityCycleId: string; } -const ISISInvestigationsTable = ( - props: ISISInvestigationsTableProps +const BaseISISInvestigationsTable = ( + props: BaseISISInvestigationsTableProps ): React.ReactElement => { const { instrumentId, facilityCycleId } = props; const disableSelectAll = useSelector( @@ -49,7 +49,7 @@ const ISISInvestigationsTable = ( ); const PIRole = useSelector((state: StateType) => state.dgdataview.PIRole); const location = useLocation(); - const { push } = useHistory(); + const navigate = useNavigate(); const [t] = useTranslation(); const { filters, view, sort } = React.useMemo( @@ -76,13 +76,14 @@ const ISISInvestigationsTable = ( }, ]; - // isMounted is used to disable queries when the component isn't fully mounted. + // isInitialised is used to disable queries when the component isn't fully initialised. // It prevents the request being sent twice if default sort is set. // It is not needed for cards/tables that don't have default sort. - const [isMounted, setIsMounted] = React.useState(false); + const [isInitialised, setIsInitialised] = React.useState(false); + React.useEffect(() => { - setIsMounted(true); - }, []); + if (!isInitialised && Object.keys(sort).length > 0) setIsInitialised(true); + }, [isInitialised, sort]); const { data: totalDataCount } = useInvestigationCount( investigationQueryFilters @@ -108,7 +109,7 @@ const ISISInvestigationsTable = ( }, ], undefined, - isMounted + isInitialised ); const { data: allIds, isPending: allIdsLoading } = useIds( 'investigation', @@ -167,11 +168,11 @@ const ISISInvestigationsTable = ( rowData={rowData} detailsPanelResize={detailsPanelResize} viewDatasets={(id: number) => - push(`${location.pathname}/${id}/dataset`) + navigate(`${location.pathname}/${id}/dataset`) } /> ), - [push, location.pathname] + [navigate, location.pathname] ); const columns: ColumnType[] = React.useMemo( @@ -308,4 +309,14 @@ const ISISInvestigationsTable = ( ); }; +const ISISInvestigationsTable = () => { + const { instrumentId = '', facilityCycleId = '' } = useParams(); + return ( + + ); +}; + export default ISISInvestigationsTable; diff --git a/packages/datagateway-dataview/src/views/table/isis/isisMyDataTable.component.test.tsx b/packages/datagateway-dataview/src/views/table/isis/isisMyDataTable.component.test.tsx index 8ecf08d0a..4e27a3a0a 100644 --- a/packages/datagateway-dataview/src/views/table/isis/isisMyDataTable.component.test.tsx +++ b/packages/datagateway-dataview/src/views/table/isis/isisMyDataTable.component.test.tsx @@ -1,5 +1,6 @@ import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { + act, render, screen, waitFor, @@ -20,10 +21,8 @@ import { useRemoveFromCart, type Investigation, } from 'datagateway-common'; -import { createMemoryHistory, type History } from 'history'; -import * as React from 'react'; import { Provider } from 'react-redux'; -import { Router } from 'react-router-dom'; +import { BrowserRouter } from 'react-router'; import configureStore from 'redux-mock-store'; import thunk from 'redux-thunk'; import { @@ -32,6 +31,7 @@ import { findColumnHeaderByName, findColumnIndexByName, findRowAt, + flushPromises, } from '../../../setupTests'; import type { StateType } from '../../../state/app.types'; import { initialState as dgDataViewInitialState } from '../../../state/reducers/dgdataview.reducer'; @@ -59,26 +59,23 @@ describe('ISIS MyData table component', () => { const mockStore = configureStore([thunk]); let state: StateType; let rowData: Investigation[]; - let history: History; let user: ReturnType; - const renderComponent = ( - element: React.ReactElement = - ): RenderResult => { + const renderComponent = (): RenderResult => { const store = mockStore(state); return render( - + - {element} + - + ); }; beforeEach(() => { - history = createMemoryHistory(); + window.history.replaceState({}, '', '/'); user = userEvent.setup(); state = JSON.parse( @@ -283,7 +280,7 @@ describe('ISIS MyData table component', () => { it('sorts by startDate desc on load', () => { renderComponent(); - expect(history.location.search).toBe( + expect(window.location.search).toBe( `?sort=${encodeURIComponent(JSON.stringify({ startDate: 'desc' }))}` ); }); @@ -301,8 +298,8 @@ describe('ISIS MyData table component', () => { // user.type inputs the given string character by character to simulate user typing // each keystroke of user.type creates a new entry in the history stack // so the initial entry + 4 characters in "test" = 5 entries - expect(history.length).toBe(5); - expect(history.location.search).toBe( + + expect(window.location.search).toContain( `?filters=${encodeURIComponent( '{"name":{"value":"test","type":"include"}}' )}` @@ -310,8 +307,7 @@ describe('ISIS MyData table component', () => { await user.clear(filterInput); - expect(history.length).toBe(6); - expect(history.location.search).toBe('?'); + expect(window.location.search).not.toContain('filters='); }); it('updates filter query params on date filter', async () => { @@ -323,8 +319,7 @@ describe('ISIS MyData table component', () => { await user.type(filterInput, '2019-08-06'); - expect(history.length).toBe(2); - expect(history.location.search).toBe( + expect(window.location.search).toContain( `?filters=${encodeURIComponent( '{"startDate":{"startDate":"2019-08-06"}}' )}` @@ -335,32 +330,28 @@ describe('ISIS MyData table component', () => { await user.keyboard('{Control}a{/Control}'); await user.keyboard('{Delete}'); - expect(history.length).toBe(3); - expect(history.location.search).toBe('?'); + expect(window.location.search).not.toContain('filters='); }); it('uses default sort', async () => { renderComponent(); + await act(async () => { + await flushPromises(); + }); + expect(await screen.findAllByRole('gridcell')).toBeTruthy(); - expect(history.length).toBe(1); - expect(history.location.search).toBe( + expect(window.location.search).toBe( `?sort=${encodeURIComponent('{"startDate":"desc"}')}` ); - // check that the data request is sent only once after mounting - expect(useInvestigationsInfinite).toHaveBeenCalledTimes(2); - expect(useInvestigationsInfinite).toHaveBeenCalledWith( - expect.anything(), - undefined, - false - ); - expect(useInvestigationsInfinite).toHaveBeenLastCalledWith( - expect.anything(), - undefined, - true - ); + // check that the data hook is only called once with the query enabled + expect( + vi + .mocked(useInvestigationsInfinite) + .mock.calls.filter((call) => call[2] === true) + ).toHaveLength(1); }); it('updates sort query params on sort', async () => { @@ -370,8 +361,7 @@ describe('ISIS MyData table component', () => { await screen.findByRole('button', { name: 'investigations.title' }) ); - expect(history.length).toBe(2); - expect(history.location.search).toBe( + expect(window.location.search).toBe( `?sort=${encodeURIComponent('{"title":"asc"}')}` ); }); @@ -493,7 +483,7 @@ describe('ISIS MyData table component', () => { }) ); - expect(history.location.pathname).toBe( + expect(window.location.pathname).toBe( '/browse/instrument/3/facilityCycle/8/investigation/1/dataset' ); }); diff --git a/packages/datagateway-dataview/src/views/table/isis/isisMyDataTable.component.tsx b/packages/datagateway-dataview/src/views/table/isis/isisMyDataTable.component.tsx index 14ace809b..fbb424219 100644 --- a/packages/datagateway-dataview/src/views/table/isis/isisMyDataTable.component.tsx +++ b/packages/datagateway-dataview/src/views/table/isis/isisMyDataTable.component.tsx @@ -31,7 +31,7 @@ import { import React from 'react'; import { useTranslation } from 'react-i18next'; import { useSelector } from 'react-redux'; -import { useHistory, useLocation } from 'react-router-dom'; +import { useLocation, useNavigate } from 'react-router'; import { IndexRange, TableCellProps } from 'react-virtualized'; import { StateType } from '../../../state/app.types'; @@ -40,7 +40,7 @@ const ISISMyDataTable = (): React.ReactElement => { (state: StateType) => state.dgcommon.features?.disableSelectAll ?? false ); const location = useLocation(); - const { push } = useHistory(); + const navigate = useNavigate(); const [t] = useTranslation(); const username = readSciGatewayToken().username || ''; @@ -49,13 +49,14 @@ const ISISMyDataTable = (): React.ReactElement => { [location.search] ); - // isMounted is used to disable queries when the component isn't fully mounted. + // isInitialised is used to disable queries when the component isn't fully initialised. // It prevents the request being sent twice if default sort is set. // It is not needed for cards/tables that don't have default sort. - const [isMounted, setIsMounted] = React.useState(false); + const [isInitialised, setIsInitialised] = React.useState(false); + React.useEffect(() => { - setIsMounted(true); - }, []); + if (!isInitialised && Object.keys(sort).length > 0) setIsInitialised(true); + }, [isInitialised, sort]); const { data: totalDataCount } = useInvestigationCount([ { @@ -89,7 +90,7 @@ const ISISMyDataTable = (): React.ReactElement => { }, ], undefined, - isMounted + isInitialised ); const { data: allIds, isPending: allIdsLoading } = useIds( 'investigation', @@ -156,12 +157,12 @@ const ISISMyDataTable = (): React.ReactElement => { rowData={rowData} detailsPanelResize={detailsPanelResize} viewDatasets={() => { - if (datasetTableUrl) push(datasetTableUrl); + if (datasetTableUrl) navigate(datasetTableUrl); }} /> ); }, - [push] + [navigate] ); const columns: ColumnType[] = React.useMemo( diff --git a/packages/datagateway-download/cypress/e2e/adminDownloadStatus.cy.ts b/packages/datagateway-download/cypress/e2e/adminDownloadStatus.cy.ts index a5b08ed6c..388d184a2 100644 --- a/packages/datagateway-download/cypress/e2e/adminDownloadStatus.cy.ts +++ b/packages/datagateway-download/cypress/e2e/adminDownloadStatus.cy.ts @@ -44,15 +44,12 @@ describe('Admin Download Status', () => { }); it('should be able to sort by all sort directions on single and multiple columns', () => { - // remove default sort - cy.contains('[role="button"]', 'Requested Date').click(); - // ascending order cy.contains('[role="button"]', 'Access Method') .as('accessMethodSortButton') .click(); - cy.get('[aria-sort="ascending"]').should('exist'); + cy.contains('[aria-sort="ascending"]', 'Access Method').should('exist'); cy.get('.MuiTableSortLabel-iconDirectionAsc').should('be.visible'); cy.get('[aria-rowindex="1"] [aria-colindex="5"]').should( 'have.text', @@ -108,13 +105,11 @@ describe('Admin Download Status', () => { }); it('should change icons when sorting on a column', () => { - // clear default sort - cy.contains('[role="button"]', 'Requested Date').click(); - - cy.get('[data-testid="SortIcon"]').should('have.length', 9); + cy.get('[data-testid="SortIcon"]').should('have.length', 8); // check icon when clicking on a column cy.contains('[role="button"]', 'ID').click(); + cy.contains('[aria-sort="ascending"]', 'ID').should('exist'); cy.get('[data-testid="ArrowDownwardIcon"]').should('have.length', 1); cy.get('.MuiTableSortLabel-iconDirectionAsc').should('exist'); diff --git a/packages/datagateway-download/package.json b/packages/datagateway-download/package.json index 4b510e135..0ddca6b24 100644 --- a/packages/datagateway-download/package.json +++ b/packages/datagateway-download/package.json @@ -15,7 +15,6 @@ "@types/node": "24.12.0", "@types/react": "18.3.28", "@types/react-dom": "18.3.7", - "@types/react-router-dom": "5.3.3", "@types/react-virtualized": "9.22.2", "@vitejs/plugin-react": "5.2.0", "axios": "1.13.5", @@ -25,7 +24,6 @@ "date-fns": "2.30.0", "date-fns-tz": "2.0.0", "fastq": "^1.19.1", - "history": "4.10.1", "i18next": "22.0.3", "i18next-browser-languagedetector": "8.2.0", "i18next-http-backend": "3.0.2", @@ -39,7 +37,7 @@ "react": "18.3.1", "react-dom": "18.3.1", "react-i18next": "12.3.1", - "react-router-dom": "5.3.4", + "react-router": "7.13.2", "react-virtualized": "9.22.6", "single-spa-react": "5.1.4", "tslib": "2.8.1", diff --git a/packages/datagateway-download/src/App.tsx b/packages/datagateway-download/src/App.tsx index ad80e055f..9314d14f5 100644 --- a/packages/datagateway-download/src/App.tsx +++ b/packages/datagateway-download/src/App.tsx @@ -12,7 +12,7 @@ import { queryCacheConfig, } from 'datagateway-common'; import React, { Component } from 'react'; -import { Link, Route, BrowserRouter as Router, Switch } from 'react-router-dom'; +import { Link, Route, BrowserRouter as Router, Routes } from 'react-router'; import ConfigProvider, { DownloadSettingsContext } from './ConfigProvider'; import DOIGenerationForm from './DOIGenerationForm/DOIGenerationForm.component'; @@ -108,23 +108,22 @@ class App extends Component { } > - + {/* development redirect route so people don't get confused by blank screen */} Downloads} + element={Downloads} /> - - - - - - - - - - + } + /> + } + /> + } /> + diff --git a/packages/datagateway-download/src/DOIGenerationForm/DOIGenerationForm.component.test.tsx b/packages/datagateway-download/src/DOIGenerationForm/DOIGenerationForm.component.test.tsx index d73521fc3..10ea95b95 100644 --- a/packages/datagateway-download/src/DOIGenerationForm/DOIGenerationForm.component.test.tsx +++ b/packages/datagateway-download/src/DOIGenerationForm/DOIGenerationForm.component.test.tsx @@ -1,7 +1,6 @@ import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { RenderResult, - act, render, screen, waitForElementToBeRemoved, @@ -17,8 +16,7 @@ import { User, fetchDownloadCart, } from 'datagateway-common'; -import { MemoryHistory, createMemoryHistory } from 'history'; -import { Router } from 'react-router-dom'; +import { MemoryRouter, Route, Routes } from 'react-router'; import { DownloadSettingsContext } from '../ConfigProvider'; import { deleteDraftDOI, @@ -26,7 +24,6 @@ import { mintDraftCart, publishDraftDOI, } from '../downloadApi'; -import { flushPromises } from '../setupTests'; import { mockCartItems, mockedSettings } from '../testData'; import DOIGenerationForm from './DOIGenerationForm.component'; @@ -62,21 +59,27 @@ const createTestQueryClient = (): QueryClient => }); const renderComponent = ( - history = createMemoryHistory({ - initialEntries: [{ pathname: '/download/mint', state: { fromCart: true } }], - }) -): RenderResult & { history: MemoryHistory } => ({ - history, - ...render( + initialEntries: React.ComponentProps< + typeof MemoryRouter + >['initialEntries'] = [ + { pathname: '/download/mint', state: { fromCart: true } }, + ] +): RenderResult => + render( - - - + + + } /> +
} + /> + + - ), -}); + ); describe('DOI generation form component', () => { let user: ReturnType; @@ -255,13 +258,9 @@ describe('DOI generation form component', () => { }); it('should redirect back to /download if user directly accesses the url', async () => { - const { history } = renderComponent(createMemoryHistory()); - - await act(async () => { - await flushPromises(); - }); + renderComponent(['/download/mint']); - expect(history.location).toMatchObject({ pathname: '/download' }); + expect(await screen.findByTestId('mock-cart-page')); }); it('should render the data policy before loading the form', async () => { diff --git a/packages/datagateway-download/src/DOIGenerationForm/DOIGenerationForm.component.tsx b/packages/datagateway-download/src/DOIGenerationForm/DOIGenerationForm.component.tsx index 31806bb9d..903162bc6 100644 --- a/packages/datagateway-download/src/DOIGenerationForm/DOIGenerationForm.component.tsx +++ b/packages/datagateway-download/src/DOIGenerationForm/DOIGenerationForm.component.tsx @@ -23,7 +23,7 @@ import { } from 'datagateway-common'; import React from 'react'; import { useTranslation } from 'react-i18next'; -import { Redirect, useLocation } from 'react-router-dom'; +import { Navigate, useLocation } from 'react-router'; import { DownloadSettingsContext } from '../ConfigProvider'; import { useCart, @@ -103,7 +103,7 @@ const DOIGenerationForm: React.FC = () => { } }, [cart]); - const location = useLocation<{ fromCart: boolean } | undefined>(); + const location = useLocation(); const [t] = useTranslation(); @@ -181,8 +181,8 @@ const DOIGenerationForm: React.FC = () => { }, [deleteDraft, draftDataPublicationId]); // redirect if the user tries to access the link directly instead of from the cart - if (!location.state?.fromCart) { - return ; + if (!(location.state as { fromCart: boolean } | undefined)?.fromCart) { + return ; } return ( diff --git a/packages/datagateway-download/src/downloadApiHooks.test.tsx b/packages/datagateway-download/src/downloadApiHooks.test.tsx index 8bbcd9298..d55de0d0d 100644 --- a/packages/datagateway-download/src/downloadApiHooks.test.tsx +++ b/packages/datagateway-download/src/downloadApiHooks.test.tsx @@ -11,9 +11,7 @@ import { Download, queryCacheConfig, } from 'datagateway-common'; -import { createMemoryHistory } from 'history'; import * as React from 'react'; -import { Router } from 'react-router-dom'; import { DownloadSettingsContext } from './ConfigProvider'; import { useAdminDownloadDeleted, @@ -52,17 +50,14 @@ const createReactQueryWrapper = ( children: React.ReactNode; }> => { const testQueryClient = createTestQueryClient(); - const history = createMemoryHistory(); const wrapper: React.JSXElementConstructor<{ children: React.ReactNode; }> = ({ children }) => ( - - - {children} - - + + {children} + ); return wrapper; diff --git a/packages/datagateway-download/src/downloadCart/downloadCartItemLink.component.tsx b/packages/datagateway-download/src/downloadCart/downloadCartItemLink.component.tsx index ef63a7a0e..c43d6177f 100644 --- a/packages/datagateway-download/src/downloadCart/downloadCartItemLink.component.tsx +++ b/packages/datagateway-download/src/downloadCart/downloadCartItemLink.component.tsx @@ -2,7 +2,7 @@ import { Link as MuiLink } from '@mui/material'; import { useQuery } from '@tanstack/react-query'; import type { DownloadCartItem } from 'datagateway-common'; import pLimit from 'p-limit'; -import { Link } from 'react-router-dom'; +import { Link } from 'react-router'; type LinkBuilder = () => Promise; diff --git a/packages/datagateway-download/src/downloadCart/downloadCartTable.component.test.tsx b/packages/datagateway-download/src/downloadCart/downloadCartTable.component.test.tsx index 7f61f2c1e..005dc614c 100644 --- a/packages/datagateway-download/src/downloadCart/downloadCartTable.component.test.tsx +++ b/packages/datagateway-download/src/downloadCart/downloadCartTable.component.test.tsx @@ -11,8 +11,7 @@ import { import userEvent from '@testing-library/user-event'; import axios, { AxiosResponse } from 'axios'; import { fetchDownloadCart } from 'datagateway-common'; -import { MemoryHistory, createMemoryHistory } from 'history'; -import { Router } from 'react-router-dom'; +import { MemoryRouter, Route, Routes, useLocation } from 'react-router'; import { DownloadSettingsContext } from '../ConfigProvider'; import { downloadPreparedCart, @@ -55,25 +54,32 @@ const createTestQueryClient = (): QueryClient => }, }); -const renderComponent = (): RenderResult & { history: MemoryHistory } => { - const history = createMemoryHistory(); - return { - history: history, - ...render( - - - - - - - - ), - }; +// used to verify the location state is correct +const MockMintPage = () => { + const { state } = useLocation(); + return
{JSON.stringify(state)}
; +}; + +const renderComponent = (settings = mockedSettings): RenderResult => { + return render( + + + + + } + /> + } /> + + + + + ); }; describe('Download cart table component', () => { let holder: HTMLElement | null; - let queryClient: QueryClient; let user: ReturnType; let mintabilityResponse: Promise>; @@ -86,7 +92,6 @@ describe('Download cart table component', () => { beforeEach(() => { user = userEvent.setup(); - queryClient = new QueryClient(); //https://stackoverflow.com/questions/43694975/jest-enzyme-using-mount-document-getelementbyid-returns-null-on-componen holder = document.createElement('div'); @@ -488,20 +493,10 @@ describe('Download cart table component', () => { ).toBeNull(); resetDOM(); - render( - - - - - - - - ); + renderComponent({ + ...mockedSettings, + totalSizeMax: 1, + }); expect( await screen.findByText( @@ -511,20 +506,10 @@ describe('Download cart table component', () => { ).toBeTruthy(); resetDOM(); - render( - - - - - - - - ); + renderComponent({ + ...mockedSettings, + fileCountMax: 1, + }); expect( await screen.findByText( @@ -535,21 +520,11 @@ describe('Download cart table component', () => { }); it('does not display error alerts if file/size limits are not set', async () => { - render( - - - - - - - - ); + renderComponent({ + ...mockedSettings, + fileCountMax: undefined, + totalSizeMax: undefined, + }); await waitFor(() => { expect( @@ -571,7 +546,7 @@ describe('Download cart table component', () => { }); it('should go to DOI generation form when Generate DOI button is clicked', async () => { - const { history } = renderComponent(); + renderComponent(); const mintButton = await screen.findByRole('link', { name: 'downloadCart.generate_DOI', @@ -580,17 +555,16 @@ describe('Download cart table component', () => { await user.click(mintButton); - expect(history.location).toMatchObject({ - pathname: '/download/mint', - state: { fromCart: true }, - }); + expect(screen.getByTestId('mock-mint-page')).toHaveTextContent( + '{"fromCart":true}' + ); }); it('should disable Generate DOI button when mintability is loading', async () => { mintabilityResponse = new Promise((_) => { // do nothing, simulating pending promise to test loading state }); - const { history } = renderComponent(); + renderComponent(); const generateDOIButton = screen .getByRole('link', { name: 'downloadCart.generate_DOI' }) @@ -605,10 +579,7 @@ describe('Download cart table component', () => { await user.click(generateDOIButton); - expect(history.location).not.toMatchObject({ - pathname: '/download/mint', - state: { fromCart: true }, - }); + expect(screen.queryByTestId('mock-mint-page')).not.toBeInTheDocument(); }); it('should disable Generate DOI button when cart is not mintable', async () => { @@ -621,7 +592,7 @@ describe('Download cart table component', () => { // have to assert here to suppress vitest complaining about the mintabilityResponse promise rejection await expect(mintabilityResponse).rejects.toThrow(); - const { history } = renderComponent(); + renderComponent(); const generateDOIButton = screen .getByRole('link', { name: 'downloadCart.generate_DOI' }) @@ -645,10 +616,7 @@ describe('Download cart table component', () => { await user.click(generateDOIButton); - expect(history.location).not.toMatchObject({ - pathname: '/download/mint', - state: { fromCart: true }, - }); + expect(screen.queryByTestId('mock-mint-page')).not.toBeInTheDocument(); await user.unhover(generateDOIButton); for (const row of tableRows) { diff --git a/packages/datagateway-download/src/downloadCart/downloadCartTable.component.tsx b/packages/datagateway-download/src/downloadCart/downloadCartTable.component.tsx index b753c4cba..980e166ef 100644 --- a/packages/datagateway-download/src/downloadCart/downloadCartTable.component.tsx +++ b/packages/datagateway-download/src/downloadCart/downloadCartTable.component.tsx @@ -31,7 +31,7 @@ import { } from 'datagateway-common'; import React from 'react'; import { Trans, useTranslation } from 'react-i18next'; -import { Link as RouterLink } from 'react-router-dom'; +import { Link as RouterLink } from 'react-router'; import { DownloadSettingsContext } from '../ConfigProvider'; import { useCart, @@ -648,10 +648,8 @@ const DownloadCartTable: React.FC = ( color="primary" disabled={cartMintabilityLoading || !mintable} component={RouterLink} - to={{ - pathname: '/download/mint', - state: { fromCart: true }, - }} + to={'/download/mint'} + state={{ fromCart: true }} > {t('downloadCart.generate_DOI')} diff --git a/packages/datagateway-download/src/downloadTab/downloadTab.component.test.tsx b/packages/datagateway-download/src/downloadTab/downloadTab.component.test.tsx index 36c9ad2c8..d23793068 100644 --- a/packages/datagateway-download/src/downloadTab/downloadTab.component.test.tsx +++ b/packages/datagateway-download/src/downloadTab/downloadTab.component.test.tsx @@ -3,8 +3,7 @@ import { RenderResult, act, render, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import axios, { AxiosResponse } from 'axios'; import { fetchDownloadCart } from 'datagateway-common'; -import { History, createMemoryHistory } from 'history'; -import { Router } from 'react-router-dom'; +import { MemoryRouter } from 'react-router'; import { DownloadSettingsContext } from '../ConfigProvider'; import { downloadDeleted, @@ -29,12 +28,10 @@ vi.mock('datagateway-common', async () => { vi.mock('../downloadApi'); describe('DownloadTab', () => { - let history: History; let holder; let user: ReturnType; beforeEach(() => { - history = createMemoryHistory(); user = userEvent.setup(); holder = document.createElement('div'); @@ -73,13 +70,13 @@ describe('DownloadTab', () => { const renderComponent = (): RenderResult => { const queryClient = new QueryClient(); return render( - + - + ); }; diff --git a/packages/datagateway-download/src/setupTests.ts b/packages/datagateway-download/src/setupTests.ts index f263d2c8d..cc4623aed 100644 --- a/packages/datagateway-download/src/setupTests.ts +++ b/packages/datagateway-download/src/setupTests.ts @@ -1,4 +1,3 @@ - import '@testing-library/jest-dom'; import failOnConsole from 'vitest-fail-on-console'; @@ -20,14 +19,6 @@ vi.stubGlobal( })) ); -function noOp(): void { - // required as work-around for jsdom environment not implementing window.URL.createObjectURL method -} - -if (typeof window.URL.createObjectURL === 'undefined') { - Object.defineProperty(window.URL, 'createObjectURL', { value: noOp }); -} - export const flushPromises = (): Promise => new Promise((resolve) => setTimeout(resolve)); diff --git a/packages/datagateway-search/package.json b/packages/datagateway-search/package.json index 81a094706..74f8d99fc 100644 --- a/packages/datagateway-search/package.json +++ b/packages/datagateway-search/package.json @@ -12,13 +12,11 @@ "@mui/x-date-pickers": "6.20.2", "@tanstack/react-query": "5.90.21", "@tanstack/react-query-devtools": "5.91.3", - "@types/history": "4.7.11", "@types/jsrsasign": "10.5.2", "@types/lodash.isequal": "4.5.8", "@types/node": "24.12.0", "@types/react": "18.3.28", "@types/react-dom": "18.3.7", - "@types/react-router-dom": "5.3.3", "@types/react-virtualized": "9.22.2", "@types/redux-logger": "3.0.8", "@vitejs/plugin-react": "5.2.0", @@ -27,7 +25,6 @@ "browserslist-to-esbuild": "2.1.1", "datagateway-common": "^3.0.0", "date-fns": "2.30.0", - "history": "4.10.1", "i18next": "22.0.3", "i18next-browser-languagedetector": "8.2.0", "i18next-http-backend": "3.0.2", @@ -39,7 +36,7 @@ "react-dom": "18.3.1", "react-i18next": "12.3.1", "react-redux": "8.1.3", - "react-router-dom": "5.3.4", + "react-router": "7.13.2", "react-virtualized": "9.22.6", "redux": "4.2.0", "redux-logger": "3.0.6", diff --git a/packages/datagateway-search/src/App.tsx b/packages/datagateway-search/src/App.tsx index 834600023..eccad292b 100644 --- a/packages/datagateway-search/src/App.tsx +++ b/packages/datagateway-search/src/App.tsx @@ -18,7 +18,7 @@ import log from 'loglevel'; import React from 'react'; import { Translation } from 'react-i18next'; import { Provider, connect } from 'react-redux'; -import { BrowserRouter } from 'react-router-dom'; +import { BrowserRouter } from 'react-router'; import { AnyAction, Store, applyMiddleware, compose, createStore } from 'redux'; import { createLogger } from 'redux-logger'; import thunk, { ThunkDispatch } from 'redux-thunk'; diff --git a/packages/datagateway-search/src/card/datasetSearchCardView.component.test.tsx b/packages/datagateway-search/src/card/datasetSearchCardView.component.test.tsx index 063abcc54..dba554772 100644 --- a/packages/datagateway-search/src/card/datasetSearchCardView.component.test.tsx +++ b/packages/datagateway-search/src/card/datasetSearchCardView.component.test.tsx @@ -10,9 +10,8 @@ import { SearchResultSource, dGCommonInitialState, } from 'datagateway-common'; -import { MemoryHistory, createMemoryHistory } from 'history'; import { Provider } from 'react-redux'; -import { Router } from 'react-router-dom'; +import { BrowserRouter } from 'react-router'; import configureStore from 'redux-mock-store'; import thunk from 'redux-thunk'; import { StateType } from '../state/app.types'; @@ -24,17 +23,16 @@ describe('Dataset - Card View', () => { let cardData: SearchResultSource; let searchResult: SearchResult; let searchResponse: SearchResponse; - let history: MemoryHistory; let queryClient: QueryClient; function renderComponent({ hierarchy = '' } = {}): RenderResult { return render( - + - + ); } @@ -93,14 +91,11 @@ describe('Dataset - Card View', () => { searchResponse = { results: [searchResult], }; - history = createMemoryHistory({ - initialEntries: [ - { - pathname: '/search/data', - search: '?currentTab=dataset', - }, - ], - }); + window.history.replaceState( + {}, + '', + '/search/data?searchText=test search¤tTab=dataset' + ); queryClient = new QueryClient({ defaultOptions: { queries: { retry: false }, @@ -124,9 +119,13 @@ describe('Dataset - Card View', () => { }); it('disables the search query if dataset search is disabled', async () => { - const searchParams = new URLSearchParams(history.location.search); + const searchParams = new URLSearchParams(window.location.search); searchParams.append('dataset', 'false'); - history.replace({ search: `?${searchParams.toString()}` }); + window.history.replaceState( + {}, + '', + `${window.location.pathname}?${searchParams.toString()}` + ); renderComponent(); @@ -353,7 +352,7 @@ describe('Dataset - Card View', () => { within(panel).getByRole('tab', { name: 'datasets.details.datafiles' }) ); - expect(history.location.pathname).toBe( + expect(window.location.pathname).toBe( '/browse/instrument/4/facilityCycle/6/investigation/2/dataset/1/datafile' ); }); diff --git a/packages/datagateway-search/src/card/datasetSearchCardView.component.tsx b/packages/datagateway-search/src/card/datasetSearchCardView.component.tsx index 42090ee3d..fcc4248ea 100644 --- a/packages/datagateway-search/src/card/datasetSearchCardView.component.tsx +++ b/packages/datagateway-search/src/card/datasetSearchCardView.component.tsx @@ -30,7 +30,7 @@ import { import React from 'react'; import { useTranslation } from 'react-i18next'; import { useSelector } from 'react-redux'; -import { useHistory, useLocation } from 'react-router-dom'; +import { useLocation, useNavigate } from 'react-router'; import FacetPanel from '../facet/components/facetPanel/facetPanel.component'; import SelectedFilterChips from '../facet/components/selectedFilterChips.component'; import { facetClassificationFromSearchResponses } from '../facet/facet'; @@ -56,7 +56,7 @@ const DatasetCardView: React.FC = (props) => { const { hierarchy } = props; const location = useLocation(); - const { push } = useHistory(); + const navigate = useNavigate(); const queryParams = React.useMemo( () => parseSearchToQuery(location.search), [location.search] @@ -71,8 +71,8 @@ const DatasetCardView: React.FC = (props) => { restrict, dataset, currentTab, + searchText, } = queryParams; - const searchText = queryParams.searchText ? queryParams.searchText : ''; const minNumResults = useSelector( (state: StateType) => state.dgsearch.minNumResults @@ -86,7 +86,7 @@ const DatasetCardView: React.FC = (props) => { useLuceneSearchInfinite( 'Dataset', { - searchText, + searchText: searchText ?? '', startDate, endDate, sort, @@ -107,7 +107,7 @@ const DatasetCardView: React.FC = (props) => { }, currentTab === 'dataset' ? filters : {}, { - enabled: dataset, + enabled: dataset && searchText !== null, // this select removes the facet count for the InvestigationInstrument.instrument.name // facet since the number is confusing for datafiles select: (data) => ({ @@ -342,7 +342,7 @@ const DatasetCardView: React.FC = (props) => { { - if (datasetsUrl) push(datasetsUrl); + if (datasetsUrl) navigate(datasetsUrl); }} /> ); @@ -354,7 +354,7 @@ const DatasetCardView: React.FC = (props) => { return ; } }, - [hierarchy, push] + [hierarchy, navigate] ); const buttons = React.useMemo( diff --git a/packages/datagateway-search/src/card/investigationSearchCardView.component.test.tsx b/packages/datagateway-search/src/card/investigationSearchCardView.component.test.tsx index 1d7682edd..e29af0593 100644 --- a/packages/datagateway-search/src/card/investigationSearchCardView.component.test.tsx +++ b/packages/datagateway-search/src/card/investigationSearchCardView.component.test.tsx @@ -11,9 +11,8 @@ import { StateType, dGCommonInitialState, } from 'datagateway-common'; -import { MemoryHistory, createMemoryHistory } from 'history'; import { Provider } from 'react-redux'; -import { Router } from 'react-router-dom'; +import { BrowserRouter } from 'react-router'; import configureStore from 'redux-mock-store'; import thunk from 'redux-thunk'; import { initialState as dgSearchInitialState } from '../state/reducers/dgsearch.reducer'; @@ -24,17 +23,16 @@ describe('Investigation - Card View', () => { let cardData: SearchResultSource; let searchResult: SearchResult; let searchResponse: SearchResponse; - let history: MemoryHistory; let queryClient: QueryClient; function renderComponent({ hierarchy = '' } = {}): RenderResult { return render( - + - + ); } @@ -99,11 +97,12 @@ describe('Investigation - Card View', () => { searchResponse = { results: [searchResult], }; - history = createMemoryHistory({ - initialEntries: [ - { search: 'searchText=test search¤tTab=investigation' }, - ], - }); + window.history.replaceState( + {}, + '', + '/search/data?searchText=test search¤tTab=investigation' + ); + queryClient = new QueryClient({ defaultOptions: { queries: { retry: false }, @@ -126,9 +125,13 @@ describe('Investigation - Card View', () => { }); it('disables the search query if investigation search is disabled', async () => { - const searchParams = new URLSearchParams(history.location.search); + const searchParams = new URLSearchParams(window.location.search); searchParams.append('investigation', 'false'); - history.replace({ search: `?${searchParams.toString()}` }); + window.history.replaceState( + {}, + '', + `${window.location.pathname}?${searchParams.toString()}` + ); renderComponent(); @@ -294,7 +297,7 @@ describe('Investigation - Card View', () => { ); await waitFor(() => { - expect(history.location.pathname).toBe( + expect(window.location.pathname).toBe( '/browse/instrument/4/facilityCycle/6/investigation/1/dataset' ); }); diff --git a/packages/datagateway-search/src/card/investigationSearchCardView.component.tsx b/packages/datagateway-search/src/card/investigationSearchCardView.component.tsx index 56d10a7b9..de1fa5f4a 100644 --- a/packages/datagateway-search/src/card/investigationSearchCardView.component.tsx +++ b/packages/datagateway-search/src/card/investigationSearchCardView.component.tsx @@ -36,7 +36,7 @@ import { import React from 'react'; import { useTranslation } from 'react-i18next'; import { useSelector } from 'react-redux'; -import { useHistory, useLocation } from 'react-router-dom'; +import { useLocation, useNavigate } from 'react-router'; import FacetPanel from '../facet/components/facetPanel/facetPanel.component'; import SelectedFilterChips from '../facet/components/selectedFilterChips.component'; import { facetClassificationFromSearchResponses } from '../facet/facet'; @@ -62,7 +62,7 @@ const InvestigationCardView: React.FC = (props) => { const [t] = useTranslation(); const location = useLocation(); - const { push } = useHistory(); + const navigate = useNavigate(); const queryParams = React.useMemo( () => parseSearchToQuery(location.search), @@ -78,8 +78,8 @@ const InvestigationCardView: React.FC = (props) => { restrict, investigation, currentTab, + searchText, } = queryParams; - const searchText = queryParams.searchText ? queryParams.searchText : ''; const handleSort = useSort(); const pushFilter = usePushInvestigationFilter(); @@ -98,7 +98,7 @@ const InvestigationCardView: React.FC = (props) => { useLuceneSearchInfinite( 'Investigation', { - searchText, + searchText: searchText ?? '', startDate, endDate, sort, @@ -122,7 +122,7 @@ const InvestigationCardView: React.FC = (props) => { ], }, currentTab === 'investigation' ? filters : {}, - { enabled: investigation } + { enabled: investigation && searchText !== null } ); function mapSource(response: SearchResponse): SearchResultSource[] { @@ -323,7 +323,7 @@ const InvestigationCardView: React.FC = (props) => { { - if (url) push(url); + if (url) navigate(url); }} /> ); @@ -336,7 +336,7 @@ const InvestigationCardView: React.FC = (props) => { return ; } }, - [hierarchy, push] + [hierarchy, navigate] ); const removeFilterChip = ( diff --git a/packages/datagateway-search/src/facet/useFacetFilters.test.tsx b/packages/datagateway-search/src/facet/useFacetFilters.test.tsx index d785f03ed..43e60ee96 100644 --- a/packages/datagateway-search/src/facet/useFacetFilters.test.tsx +++ b/packages/datagateway-search/src/facet/useFacetFilters.test.tsx @@ -1,26 +1,41 @@ -import { act, renderHook, waitFor } from '@testing-library/react'; -import { MemoryHistory, createMemoryHistory } from 'history'; +import { act, renderHook, screen, waitFor } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; import * as React from 'react'; -import { Router } from 'react-router-dom'; +import { BrowserRouter, useNavigate } from 'react-router'; import useFacetFilters from './useFacetFilters'; describe('useFacetFilters', () => { - let history: MemoryHistory; + let buttonSearchParams: URLSearchParams; + + const ChangeSearchParamsButton = () => { + const navigate = useNavigate(); + return ( + + ); + }; function Wrapper({ children }: { children: React.ReactNode }): JSX.Element { - return {children}; + return ( + + <> + {children} + + + + ); } beforeEach(() => { - history = createMemoryHistory(); + window.history.replaceState({}, '', '/'); + buttonSearchParams = new URLSearchParams(); }); - it('stores the currently selected filters', () => { - const { result, rerender } = renderHook(() => useFacetFilters(), { - wrapper: Wrapper, - }); - // should be empty initially - expect(result.current.selectedFacetFilters).toEqual({}); + it('stores the currently selected filters', async () => { + const user = userEvent.setup(); const searchParam = new URLSearchParams(); searchParam.append( @@ -29,12 +44,15 @@ describe('useFacetFilters', () => { 'investigation.type.name': ['experiment'], }) ); + buttonSearchParams = searchParam; - act(() => { - history.push({ search: `?${searchParam.toString()}` }); + const { result } = renderHook(() => useFacetFilters(), { + wrapper: Wrapper, }); + // should be empty initially + expect(result.current.selectedFacetFilters).toEqual({}); - rerender(); + await user.click(screen.getByRole('button')); expect(result.current.selectedFacetFilters).toEqual({ 'investigation.type.name': ['experiment'], @@ -59,7 +77,7 @@ describe('useFacetFilters', () => { 'investigation.type.name': ['experiment'], }); }); - expect(history.location.search).toEqual(''); + expect(window.location.search).toEqual(''); act(() => { result.current.addFacetFilter({ @@ -74,7 +92,7 @@ describe('useFacetFilters', () => { 'investigation.type.name': ['experiment', 'calibration'], }); }); - expect(history.location.search).toEqual(''); + expect(window.location.search).toEqual(''); act(() => { result.current.addFacetFilter({ @@ -90,7 +108,7 @@ describe('useFacetFilters', () => { 'investigationparameter.type.name': ['run_number_after'], }); }); - expect(history.location.search).toEqual(''); + expect(window.location.search).toEqual(''); }); it('adds filters and apply the changes immediately when applyImmediately set to true', async () => { @@ -115,7 +133,7 @@ describe('useFacetFilters', () => { await waitFor(() => { expect(result.current.selectedFacetFilters).toEqual(selectedFilters); }); - expect(history.location.search).toEqual(`?${searchParams.toString()}`); + expect(window.location.search).toEqual(`?${searchParams.toString()}`); act(() => { result.current.addFacetFilter({ @@ -135,7 +153,7 @@ describe('useFacetFilters', () => { await waitFor(() => { expect(result.current.selectedFacetFilters).toEqual(selectedFilters); }); - expect(history.location.search).toEqual(`?${searchParams.toString()}`); + expect(window.location.search).toEqual(`?${searchParams.toString()}`); }); it('removes filters without applying the changes', async () => { @@ -152,7 +170,11 @@ describe('useFacetFilters', () => { ); const searchParamStr = `?${searchParams.toString()}`; - history.replace({ search: searchParamStr }); + window.history.replaceState( + {}, + '', + `${window.location.pathname}${searchParamStr}` + ); const { result } = renderHook(() => useFacetFilters(), { wrapper: Wrapper, @@ -176,7 +198,7 @@ describe('useFacetFilters', () => { ], }); }); - expect(history.location.search).toEqual(searchParamStr); + expect(window.location.search).toEqual(searchParamStr); act(() => { result.current.removeFacetFilter({ @@ -193,7 +215,7 @@ describe('useFacetFilters', () => { ], }); }); - expect(history.location.search).toEqual(searchParamStr); + expect(window.location.search).toEqual(searchParamStr); act(() => { result.current.removeFacetFilter({ @@ -207,7 +229,7 @@ describe('useFacetFilters', () => { 'investigationparameter.type.name': ['run_number_after'], }); }); - expect(history.location.search).toEqual(searchParamStr); + expect(window.location.search).toEqual(searchParamStr); }); it('removes filters and apply the changes immediately when applyImmediately set to true', async () => { @@ -225,7 +247,11 @@ describe('useFacetFilters', () => { const searchParamStr = `?${searchParams.toString()}`; - history.replace({ search: searchParamStr }); + window.history.replaceState( + {}, + '', + `${window.location.pathname}${searchParamStr}` + ); const { result } = renderHook(() => useFacetFilters(), { wrapper: Wrapper, @@ -249,7 +275,7 @@ describe('useFacetFilters', () => { ], }); }); - expect(history.location.search).toEqual(searchParamStr); + expect(window.location.search).toEqual(searchParamStr); act(() => { result.current.removeFacetFilter({ @@ -269,7 +295,7 @@ describe('useFacetFilters', () => { await waitFor(() => { expect(result.current.selectedFacetFilters).toEqual(selectedFilters); }); - expect(history.location.search).toEqual(`?${searchParams.toString()}`); + expect(window.location.search).toEqual(`?${searchParams.toString()}`); }); it('applies the update filters to the URL when requested', async () => { @@ -284,7 +310,11 @@ describe('useFacetFilters', () => { ], }) ); - history.replace({ search: `?${searchParams.toString()}` }); + window.history.replaceState( + {}, + '', + `${window.location.pathname}?${searchParams.toString()}` + ); const { result } = renderHook(() => useFacetFilters(), { wrapper: Wrapper, @@ -318,7 +348,7 @@ describe('useFacetFilters', () => { ); await waitFor(() => { - expect(history.location.search).toEqual(`?${newSearchParams.toString()}`); + expect(window.location.search).toEqual(`?${newSearchParams.toString()}`); }); expect(result.current.selectedFacetFilters).toEqual({ 'investigation.type.name': ['experiment', 'calibration'], diff --git a/packages/datagateway-search/src/facet/useFacetFilters.ts b/packages/datagateway-search/src/facet/useFacetFilters.ts index 237a3f26e..7aa8b532d 100644 --- a/packages/datagateway-search/src/facet/useFacetFilters.ts +++ b/packages/datagateway-search/src/facet/useFacetFilters.ts @@ -1,11 +1,11 @@ -import React from 'react'; import { FiltersType, parseSearchToQuery, SearchFilter, } from 'datagateway-common'; -import { useHistory, useLocation } from 'react-router-dom'; import isEqual from 'lodash.isequal'; +import React from 'react'; +import { useLocation, useNavigate } from 'react-router'; function useFacetFilters(): { selectedFacetFilters: FiltersType; @@ -23,7 +23,7 @@ function useFacetFilters(): { haveUnappliedFilters: boolean; } { const location = useLocation(); - const { push } = useHistory(); + const navigate = useNavigate(); const { filters } = React.useMemo( () => parseSearchToQuery(location.search), [location.search] @@ -104,8 +104,8 @@ function useFacetFilters(): { const applyFacetFilters = React.useCallback((): void => { const searchParams = new URLSearchParams(location.search); searchParams.set('filters', JSON.stringify(selectedFacetFilters)); - push({ search: `?${searchParams.toString()}` }); - }, [location.search, push, selectedFacetFilters]); + navigate({ search: `?${searchParams.toString()}` }); + }, [location.search, navigate, selectedFacetFilters]); React.useEffect(() => { setSelectedFacetFilters(filters); diff --git a/packages/datagateway-search/src/search/advancedHelpDialog.component.test.tsx b/packages/datagateway-search/src/search/advancedHelpDialog.component.test.tsx index 741ec30a5..d29b73666 100644 --- a/packages/datagateway-search/src/search/advancedHelpDialog.component.test.tsx +++ b/packages/datagateway-search/src/search/advancedHelpDialog.component.test.tsx @@ -6,7 +6,7 @@ import { } from '@testing-library/react'; import { dGCommonInitialState } from 'datagateway-common'; import { Provider } from 'react-redux'; -import { MemoryRouter } from 'react-router-dom'; +import { MemoryRouter } from 'react-router'; import configureStore from 'redux-mock-store'; import thunk from 'redux-thunk'; import AdvancedHelpDialog from './advancedHelpDialog.component'; diff --git a/packages/datagateway-search/src/search/advancedHelpDialog.component.tsx b/packages/datagateway-search/src/search/advancedHelpDialog.component.tsx index 3147eeb17..808f7bcc4 100644 --- a/packages/datagateway-search/src/search/advancedHelpDialog.component.tsx +++ b/packages/datagateway-search/src/search/advancedHelpDialog.component.tsx @@ -17,7 +17,7 @@ import { } from '@mui/material'; import React from 'react'; import { Trans, useTranslation } from 'react-i18next'; -import { Link as RouterLink } from 'react-router-dom'; +import { Link as RouterLink } from 'react-router'; const Section = styled('section')(({ theme }) => ({ marginTop: theme.spacing(4), diff --git a/packages/datagateway-search/src/search/checkBoxes.component.test.tsx b/packages/datagateway-search/src/search/checkBoxes.component.test.tsx index 941861202..ef566a877 100644 --- a/packages/datagateway-search/src/search/checkBoxes.component.test.tsx +++ b/packages/datagateway-search/src/search/checkBoxes.component.test.tsx @@ -5,12 +5,10 @@ import { type RenderResult, } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; -import { createMemoryHistory, type History } from 'history'; import { Provider } from 'react-redux'; -import { Router } from 'react-router-dom'; +import { BrowserRouter } from 'react-router'; import configureStore from 'redux-mock-store'; import thunk from 'redux-thunk'; -import type { MockInstance } from 'vitest'; import type { StateType } from '../state/app.types'; import { initialState } from '../state/reducers/dgsearch.reducer'; import CheckBoxesGroup from './checkBoxes.component'; @@ -22,23 +20,20 @@ describe('Checkbox component tests', () => { let state: StateType; const mockStore = configureStore([thunk]); let testStore: ReturnType; - let history: History; - let pushSpy: MockInstance; function renderComponent(): RenderResult { return render( - + - + ); } beforeEach(() => { user = userEvent.setup(); - history = createMemoryHistory(); - pushSpy = vi.spyOn(history, 'push'); + window.history.replaceState({}, '', '/'); state = JSON.parse(JSON.stringify({ dgsearch: initialState })); @@ -61,7 +56,7 @@ describe('Checkbox component tests', () => { }); it('renders a dropdown button that expands to show search type checkboxes', async () => { - history.replace('/?searchText=&investigation=false'); + window.history.replaceState({}, '', '/?searchText=&investigation=false'); renderComponent(); // open the dropdown @@ -85,7 +80,7 @@ describe('Checkbox component tests', () => { it('renders correctly when datafiles are not searchable', async () => { state.dgsearch.searchableEntities = ['investigation', 'dataset']; - history.replace('/?searchText=&investigation=false'); + window.history.replaceState({}, '', '/?searchText=&investigation=false'); renderComponent(); // open the dropdown @@ -109,7 +104,9 @@ describe('Checkbox component tests', () => { }); it('renders an error message when nothing is selected', async () => { - history.replace( + window.history.replaceState( + {}, + '', '/?searchText=&investigation=false&dataset=false&datafile=false' ); @@ -121,7 +118,7 @@ describe('Checkbox component tests', () => { }); it('pushes URL with new dataset value when user clicks checkbox', async () => { - history.replace('/?searchText=&investigation=false'); + window.history.replaceState({}, '', '/?searchText=&investigation=false'); renderComponent(); await user.click( @@ -135,13 +132,13 @@ describe('Checkbox component tests', () => { }) ); - expect(pushSpy).toHaveBeenCalledWith( + expect(window.location.search).toBe( '?searchText=&dataset=false&investigation=false' ); }); it('pushes URL with new datafile value when user clicks checkbox', async () => { - history.replace('/?searchText=&investigation=false'); + window.history.replaceState({}, '', '/?searchText=&investigation=false'); renderComponent(); await user.click( @@ -155,13 +152,13 @@ describe('Checkbox component tests', () => { }) ); - expect(pushSpy).toHaveBeenCalledWith( + expect(window.location.search).toBe( '?searchText=&datafile=false&investigation=false' ); }); it('pushes URL with new investigation value when user clicks checkbox', async () => { - history.replace('/?searchText=&investigation=false'); + window.history.replaceState({}, '', '/?searchText=&investigation=false'); renderComponent(); await user.click( @@ -175,6 +172,6 @@ describe('Checkbox component tests', () => { }) ); - expect(pushSpy).toHaveBeenCalledWith('?searchText='); + expect(window.location.search).toBe('?searchText='); }); }); diff --git a/packages/datagateway-search/src/search/checkBoxes.component.tsx b/packages/datagateway-search/src/search/checkBoxes.component.tsx index c82b78de0..1dd366eca 100644 --- a/packages/datagateway-search/src/search/checkBoxes.component.tsx +++ b/packages/datagateway-search/src/search/checkBoxes.component.tsx @@ -13,7 +13,7 @@ import { parseSearchToQuery, usePushSearchToggles } from 'datagateway-common'; import React from 'react'; import { useTranslation } from 'react-i18next'; import { connect } from 'react-redux'; -import { useLocation } from 'react-router-dom'; +import { useLocation } from 'react-router'; import { StateType } from '../state/app.types'; const ITEM_HEIGHT = 48; diff --git a/packages/datagateway-search/src/search/datePicker.component.test.tsx b/packages/datagateway-search/src/search/datePicker.component.test.tsx index 7d60a919f..0e59b8d56 100644 --- a/packages/datagateway-search/src/search/datePicker.component.test.tsx +++ b/packages/datagateway-search/src/search/datePicker.component.test.tsx @@ -1,14 +1,12 @@ -import { StateType } from '../state/app.types'; +import { render, screen, type RenderResult } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; import { Provider } from 'react-redux'; +import { BrowserRouter } from 'react-router'; import configureStore from 'redux-mock-store'; -import SelectDates from './datePicker.component'; import thunk from 'redux-thunk'; -import { Router } from 'react-router-dom'; +import { StateType } from '../state/app.types'; import { initialState } from '../state/reducers/dgsearch.reducer'; -import { createMemoryHistory, History } from 'history'; -import { render, type RenderResult, screen } from '@testing-library/react'; -import userEvent from '@testing-library/user-event'; -import type { MockInstance } from 'vitest'; +import SelectDates from './datePicker.component'; vi.mock('loglevel'); @@ -16,23 +14,20 @@ describe('DatePicker component tests', () => { let state: StateType; const mockStore = configureStore([thunk]); let testStore: ReturnType; - let history: History; - let pushSpy: MockInstance; const testInitiateSearch = vi.fn(); - const renderComponent = (h: History = history): RenderResult => + const renderComponent = (): RenderResult => render( - + - + ); beforeEach(() => { - history = createMemoryHistory(); - pushSpy = vi.spyOn(history, 'push'); + window.history.replaceState({}, '', '/'); state = JSON.parse(JSON.stringify({ dgsearch: initialState })); @@ -55,7 +50,9 @@ describe('DatePicker component tests', () => { }); it('renders correctly', async () => { - history.replace( + window.history.replaceState( + {}, + '', '/?searchText=&investigation=false&startDate=2021-10-26&endDate=2021-10-28' ); @@ -81,7 +78,7 @@ describe('DatePicker component tests', () => { }); it('pushes URL with new start date value when user types number into Start Date input', async () => { - history.replace('/?searchText=&investigation=false'); + window.history.replaceState({}, '', '/?searchText=&investigation=false'); renderComponent(); const startDateInput = await screen.findByRole('textbox', { @@ -90,11 +87,13 @@ describe('DatePicker component tests', () => { await user.type(startDateInput, '2012-01-01'); - expect(pushSpy).toHaveBeenCalledWith('?startDate=2012-01-01'); + expect(window.location.search).toContain('startDate=2012-01-01'); }); it('initiates search with valid start and end dates', async () => { - history.replace( + window.history.replaceState( + {}, + '', '/?searchText=&investigation=false&startDate=2012-01-01&endDate=2013-01-01' ); @@ -109,7 +108,11 @@ describe('DatePicker component tests', () => { }); it('initiates search with valid start date and empty end date', async () => { - history.replace('/?searchText=&investigation=false&startDate=2012-01-01'); + window.history.replaceState( + {}, + '', + '/?searchText=&investigation=false&startDate=2012-01-01' + ); renderComponent(); const startDateInput = await screen.findByRole('textbox', { @@ -122,7 +125,11 @@ describe('DatePicker component tests', () => { }); it('initiates search with valid end date and empty start date', async () => { - history.replace('/?searchText=&investigation=false&endDate=2012-01-01'); + window.history.replaceState( + {}, + '', + '/?searchText=&investigation=false&endDate=2012-01-01' + ); renderComponent(); const startDateInput = await screen.findByRole('textbox', { @@ -135,7 +142,7 @@ describe('DatePicker component tests', () => { }); it('initiates search with empty start and end dates', async () => { - history.replace('/?searchText=&investigation=false'); + window.history.replaceState({}, '', '/?searchText=&investigation=false'); renderComponent(); const startDateInput = await screen.findByRole('textbox', { @@ -149,7 +156,7 @@ describe('DatePicker component tests', () => { // In v6, date pickers don't allow invalid dates to be entered it('displays error message when an invalid date is entered', async () => { - history.replace('/?searchText=&investigation=false'); + window.history.replaceState({}, '', '/?searchText=&investigation=false'); renderComponent(); const startDateInput = await screen.findByRole('textbox', { @@ -164,7 +171,7 @@ describe('DatePicker component tests', () => { }); it('displays error message when a date after the maximum date is entered', async () => { - history.replace('/?searchText=&investigation=false'); + window.history.replaceState({}, '', '/?searchText=&investigation=false'); renderComponent(); const startDateInput = await screen.findByRole('textbox', { @@ -179,7 +186,11 @@ describe('DatePicker component tests', () => { }); it('displays error message when a date after the end date is entered', async () => { - history.replace('/?searchText=&investigation=false&endDate=2011-11-21'); + window.history.replaceState( + {}, + '', + '/?searchText=&investigation=false&endDate=2011-11-21' + ); renderComponent(); const startDateInput = await screen.findByRole('textbox', { @@ -199,7 +210,11 @@ describe('DatePicker component tests', () => { }); it('invalid date in URL is ignored', async () => { - history.replace('/?searchText=&investigation=false&startDate=2011-14-21'); + window.history.replaceState( + {}, + '', + '/?searchText=&investigation=false&startDate=2011-14-21' + ); renderComponent(); const startDateInput = await screen.findByRole('textbox', { @@ -218,7 +233,7 @@ describe('DatePicker component tests', () => { }); it('pushes URL with new end date value when user types number into Start Date input', async () => { - history.replace('/?searchText=&investigation=false'); + window.history.replaceState({}, '', '/?searchText=&investigation=false'); renderComponent(); const endDateInput = await screen.findByRole('textbox', { @@ -227,11 +242,13 @@ describe('DatePicker component tests', () => { await user.type(endDateInput, '2000 01 01'); - expect(pushSpy).toHaveBeenCalledWith('?endDate=2000-01-01'); + expect(window.location.search).toContain('endDate=2000-01-01'); }); it('initiates search with valid start and end dates', async () => { - history.replace( + window.history.replaceState( + {}, + '', '/?searchText=&investigation=false&startDate=2012-01-01&endDate=2013-01-01' ); @@ -246,7 +263,11 @@ describe('DatePicker component tests', () => { }); it('initiates search with valid start date and empty end date', async () => { - history.replace('/?searchText=&investigation=false&startDate=2012-01-01'); + window.history.replaceState( + {}, + '', + '/?searchText=&investigation=false&startDate=2012-01-01' + ); renderComponent(); const endDateInput = await screen.findByRole('textbox', { @@ -259,7 +280,11 @@ describe('DatePicker component tests', () => { }); it('initiates search with valid end date and empty start date', async () => { - history.replace('/?searchText=&investigation=false&endDate=2012-01-01'); + window.history.replaceState( + {}, + '', + '/?searchText=&investigation=false&endDate=2012-01-01' + ); renderComponent(); const endDateInput = await screen.findByRole('textbox', { @@ -272,7 +297,7 @@ describe('DatePicker component tests', () => { }); it('initiates search with empty start and end dates', async () => { - history.replace('/?searchText=&investigation=false'); + window.history.replaceState({}, '', '/?searchText=&investigation=false'); renderComponent(); const endDateInput = await screen.findByRole('textbox', { @@ -286,7 +311,7 @@ describe('DatePicker component tests', () => { // In v6, date pickers don't allow invalid dates to be entered it('displays error message when an invalid date is entered', async () => { - history.replace('/?searchText=&investigation=false'); + window.history.replaceState({}, '', '/?searchText=&investigation=false'); renderComponent(); const endDateInput = await screen.findByRole('textbox', { @@ -301,7 +326,7 @@ describe('DatePicker component tests', () => { }); it('displays error message when a date before the minimum date is entered', async () => { - history.replace('/?searchText=&investigation=false'); + window.history.replaceState({}, '', '/?searchText=&investigation=false'); renderComponent(); const endDateInput = await screen.findByRole('textbox', { @@ -316,7 +341,11 @@ describe('DatePicker component tests', () => { }); it('displays error message when a date before the start date is entered', async () => { - history.replace('/?searchText=&investigation=false&startDate=2011-11-21'); + window.history.replaceState( + {}, + '', + '/?searchText=&investigation=false&startDate=2011-11-21' + ); renderComponent(); const endDateInput = await screen.findByRole('textbox', { @@ -336,7 +365,11 @@ describe('DatePicker component tests', () => { }); it('invalid date in URL is ignored', async () => { - history.replace('/?searchText=&investigation=false&endDate=2011-14-21'); + window.history.replaceState( + {}, + '', + '/?searchText=&investigation=false&endDate=2011-14-21' + ); renderComponent(); const endDateInput = await screen.findByRole('textbox', { diff --git a/packages/datagateway-search/src/search/datePicker.component.tsx b/packages/datagateway-search/src/search/datePicker.component.tsx index 64f27267f..18bb36e5e 100644 --- a/packages/datagateway-search/src/search/datePicker.component.tsx +++ b/packages/datagateway-search/src/search/datePicker.component.tsx @@ -1,14 +1,3 @@ -import React, { useState } from 'react'; -import { connect } from 'react-redux'; -import { StateType } from '../state/app.types'; -import { useTranslation } from 'react-i18next'; -import { - parseSearchToQuery, - usePushSearchEndDate, - usePushSearchStartDate, -} from 'datagateway-common'; -import { useLocation } from 'react-router-dom'; -import { isBefore, isValid } from 'date-fns'; import { TextField, TextFieldProps, @@ -16,9 +5,20 @@ import { createTheme, useTheme, } from '@mui/material'; -import { AdapterDateFns } from '@mui/x-date-pickers/AdapterDateFns'; import { DatePicker, LocalizationProvider } from '@mui/x-date-pickers'; +import { AdapterDateFns } from '@mui/x-date-pickers/AdapterDateFns'; +import { + parseSearchToQuery, + usePushSearchEndDate, + usePushSearchStartDate, +} from 'datagateway-common'; +import { isBefore, isValid } from 'date-fns'; import { enGB } from 'date-fns/locale'; +import React, { useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { connect } from 'react-redux'; +import { useLocation } from 'react-router'; +import { StateType } from '../state/app.types'; interface DatePickerProps { initiateSearch: () => void; diff --git a/packages/datagateway-search/src/search/searchTextBox.component.test.tsx b/packages/datagateway-search/src/search/searchTextBox.component.test.tsx index 825fcb826..f4b95994a 100644 --- a/packages/datagateway-search/src/search/searchTextBox.component.test.tsx +++ b/packages/datagateway-search/src/search/searchTextBox.component.test.tsx @@ -4,58 +4,24 @@ import { RenderResult, screen, } from '@testing-library/react'; -import { createMemoryHistory, History } from 'history'; -import { Provider } from 'react-redux'; -import { Router } from 'react-router-dom'; -import configureStore from 'redux-mock-store'; -import thunk from 'redux-thunk'; -import { StateType } from '../state/app.types'; -import { initialState } from '../state/reducers/dgsearch.reducer'; import SearchTextBox from './searchTextBox.component'; vi.mock('loglevel'); describe('Search text box component tests', () => { - let state: StateType; - const mockStore = configureStore([thunk]); - let testStore: ReturnType; - let history: History; - const testInitiateSearch = vi.fn(); const handleChange = vi.fn(); - const createWrapper = (h: History = history): RenderResult => { + const createWrapper = (): RenderResult => { return render( - - - - - + ); }; - beforeEach(() => { - history = createMemoryHistory(); - - state = JSON.parse(JSON.stringify({ dgsearch: initialState })); - - state.dgsearch = { - ...state.dgsearch, - tabs: { - datasetTab: true, - datafileTab: true, - investigationTab: true, - }, - settingsLoaded: true, - }; - - testStore = mockStore(state); - }); - afterEach(() => { vi.clearAllMocks(); }); diff --git a/packages/datagateway-search/src/search/sortSelect.component.test.tsx b/packages/datagateway-search/src/search/sortSelect.component.test.tsx index 8523003d9..5ab8de1b0 100644 --- a/packages/datagateway-search/src/search/sortSelect.component.test.tsx +++ b/packages/datagateway-search/src/search/sortSelect.component.test.tsx @@ -1,19 +1,25 @@ import { render, screen, within } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; -import { createMemoryHistory } from 'history'; -import { MemoryRouter, Router } from 'react-router-dom'; +import { BrowserRouter } from 'react-router'; import SortSelectComponent from './sortSelect.component'; describe('sortSelect', () => { - it('renders correctly', async () => { - const user = userEvent.setup(); - + const renderComponent = () => render( - + - + ); + beforeEach(() => { + window.history.replaceState({}, '', '/'); + }); + + it('renders correctly', async () => { + const user = userEvent.setup(); + + renderComponent(); + // open the dropdown menu await user.click(screen.getByRole('combobox', { name: 'sort.label' })); @@ -36,13 +42,8 @@ describe('sortSelect', () => { it('updates URL correctly accordingly to selected sort', async () => { const user = userEvent.setup(); - const history = createMemoryHistory(); - render( - - - - ); + renderComponent(); // open the dropdown menu await user.click(screen.getByRole('combobox', { name: 'sort.label' })); @@ -50,7 +51,7 @@ describe('sortSelect', () => { screen.getByRole('option', { name: 'sort.date_desc' }), ]); - expect(history.location.search).toBe( + expect(window.location.search).toBe( `?${new URLSearchParams({ sort: JSON.stringify({ date: 'desc' }), }).toString()}` @@ -62,7 +63,7 @@ describe('sortSelect', () => { screen.getByRole('option', { name: 'sort.name_asc' }), ]); - expect(history.location.search).toBe( + expect(window.location.search).toBe( `?${new URLSearchParams({ sort: JSON.stringify({ name: 'asc' }), }).toString()}` @@ -74,16 +75,9 @@ describe('sortSelect', () => { sort: JSON.stringify({ fileSize: 'asc' }), }); - const history = createMemoryHistory(); - history.replace({ - search: `?${initialQuery.toString()}`, - }); + window.history.replaceState({}, '', `/?${initialQuery.toString()}`); - render( - - - - ); + renderComponent(); expect( within(screen.getByRole('combobox', { name: 'sort.label' })).getByText( diff --git a/packages/datagateway-search/src/search/sortSelect.component.tsx b/packages/datagateway-search/src/search/sortSelect.component.tsx index a24482421..b29b3572b 100644 --- a/packages/datagateway-search/src/search/sortSelect.component.tsx +++ b/packages/datagateway-search/src/search/sortSelect.component.tsx @@ -3,7 +3,7 @@ import { FormControl, InputLabel, MenuItem, Select } from '@mui/material'; import { Order, parseSearchToQuery, useSingleSort } from 'datagateway-common'; import React from 'react'; import { useTranslation } from 'react-i18next'; -import { useLocation } from 'react-router-dom'; +import { useLocation } from 'react-router'; const ITEM_HEIGHT = 48; const ITEM_PADDING_TOP = 8; diff --git a/packages/datagateway-search/src/searchBoxContainer.component.test.tsx b/packages/datagateway-search/src/searchBoxContainer.component.test.tsx index 35a0d34dc..ded481183 100644 --- a/packages/datagateway-search/src/searchBoxContainer.component.test.tsx +++ b/packages/datagateway-search/src/searchBoxContainer.component.test.tsx @@ -2,7 +2,7 @@ import type { RenderResult } from '@testing-library/react'; import { render, screen, within } from '@testing-library/react'; import * as React from 'react'; import { Provider } from 'react-redux'; -import { MemoryRouter } from 'react-router-dom'; +import { MemoryRouter } from 'react-router'; import type { DeepPartial } from 'redux'; import configureStore from 'redux-mock-store'; import thunk from 'redux-thunk'; diff --git a/packages/datagateway-search/src/searchBoxContainer.component.tsx b/packages/datagateway-search/src/searchBoxContainer.component.tsx index d7dd09620..059d34c7c 100644 --- a/packages/datagateway-search/src/searchBoxContainer.component.tsx +++ b/packages/datagateway-search/src/searchBoxContainer.component.tsx @@ -1,8 +1,7 @@ import { Box, Grid, Link, styled, Theme, Typography } from '@mui/material'; -import { Location } from 'history'; import React from 'react'; import { Trans, useTranslation } from 'react-i18next'; -import { Link as RouterLink } from 'react-router-dom'; +import { Link as RouterLink, useLocation } from 'react-router'; import AdvancedHelpDialog from './search/advancedHelpDialog.component'; import CheckboxesGroup from './search/checkBoxes.component'; import SelectDates from './search/datePicker.component'; @@ -29,6 +28,18 @@ interface SearchBoxContainerProps { onMyDataCheckboxChange: (checked: boolean) => void; } +function searchTextExampleLink( + exampleSearchText: string, + location: ReturnType +) { + const searchParams = new URLSearchParams(location.search); + searchParams.set('searchText', exampleSearchText); + return { + ...location, + search: searchParams.toString(), + }; +} + const SearchBoxContainer = ( props: SearchBoxContainerProps ): React.ReactElement => { @@ -41,17 +52,7 @@ const SearchBoxContainer = ( onMyDataCheckboxChange, } = props; const [t] = useTranslation(); - - function searchTextExampleLink(exampleSearchText: string) { - return (location: Location): Location => { - const searchParams = new URLSearchParams(location.search); - searchParams.set('searchText', exampleSearchText); - return { - ...location, - search: searchParams.toString(), - }; - }; - } + const location = useLocation(); return ( @@ -120,7 +121,10 @@ const SearchBoxContainer = ( "instrument calibration" @@ -128,7 +132,10 @@ const SearchBoxContainer = ( neutron AND scattering diff --git a/packages/datagateway-search/src/searchPageContainer.component.test.tsx b/packages/datagateway-search/src/searchPageContainer.component.test.tsx index 2fb114ca8..3620bc525 100644 --- a/packages/datagateway-search/src/searchPageContainer.component.test.tsx +++ b/packages/datagateway-search/src/searchPageContainer.component.test.tsx @@ -1,29 +1,31 @@ -import thunk from 'redux-thunk'; -import configureStore from 'redux-mock-store'; -import { StateType } from './state/app.types'; -import { initialState as dgSearchInitialState } from './state/reducers/dgsearch.reducer'; +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import { + act, + render, + renderHook, + screen, + type RenderResult, +} from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import axios from 'axios'; import { dGCommonInitialState, readSciGatewayToken, type DownloadCartItem, - useCart, - parseSearchToQuery, } from 'datagateway-common'; -import { createMemoryHistory, createPath, type History } from 'history'; -import { Router } from 'react-router-dom'; -import SearchPageContainer, { - usePushCurrentTab, -} from './searchPageContainer.component'; import { Provider } from 'react-redux'; -import axios from 'axios'; -import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; -import { render, type RenderResult, screen, act } from '@testing-library/react'; -import userEvent from '@testing-library/user-event'; +import { BrowserRouter, Link } from 'react-router'; import type { DeepPartial } from 'redux'; import { applyMiddleware, compose, createStore } from 'redux'; -import AppReducer from './state/reducers/app.reducer'; -import { renderHook } from '@testing-library/react'; +import configureStore from 'redux-mock-store'; +import thunk from 'redux-thunk'; import type { MockInstance } from 'vitest'; +import SearchPageContainer, { + usePushCurrentTab, +} from './searchPageContainer.component'; +import { StateType } from './state/app.types'; +import AppReducer from './state/reducers/app.reducer'; +import { initialState as dgSearchInitialState } from './state/reducers/dgsearch.reducer'; vi.mock('loglevel'); @@ -33,12 +35,6 @@ vi.mock('datagateway-common', async () => { return { __esModule: true, ...originalModule, - parseSearchToQuery: vi.fn((queryParams: string) => - (originalModule.parseSearchToQuery as typeof parseSearchToQuery)( - queryParams - ) - ), - useCart: vi.fn(() => (originalModule.useCart as typeof useCart)()), readSciGatewayToken: vi.fn(), }; }); @@ -71,6 +67,7 @@ describe('usePushCurrentTab', () => { window.localStorage.__proto__, 'getItem' ); + window.history.replaceState({}, '', '/'); }); afterEach(() => { @@ -79,28 +76,26 @@ describe('usePushCurrentTab', () => { }); it('returns callback that when called pushes a new tab to the url query', () => { - const history = createMemoryHistory(); - const { result } = renderHook(() => usePushCurrentTab(), { - wrapper: ({ children }) => {children}, + wrapper: ({ children }) => {children}, }); act(() => { result.current('dataset'); }); - expect(history.location.search).toEqual('?currentTab=dataset'); + expect(window.location.search).toEqual('?currentTab=dataset'); }); it('returns callback that when called pushes a new tab to the url query, and stores and restores any stored search query params', () => { - const history = createMemoryHistory({ - initialEntries: [ - '/search/data?currentTab=investigation&filters={"title":{"value":"test","type":"include"}}&page=2&results=30', - ], - }); + window.history.replaceState( + {}, + '', + '/search/data?currentTab=investigation&filters={"title":{"value":"test","type":"include"}}&page=2&results=30' + ); const { result } = renderHook(() => usePushCurrentTab(), { - wrapper: ({ children }) => {children}, + wrapper: ({ children }) => {children}, }); localStorageGetItemMock.mockImplementation((name) => { @@ -124,7 +119,7 @@ describe('usePushCurrentTab', () => { '30' ); - expect(history.location.search).toEqual( + expect(window.location.search).toEqual( '?page=3&results=20¤tTab=dataset&filters=%7B%22name%22%3A%7B%22value%22%3A%22test2%22%2C%22type%22%3A%22include%22%7D%7D' ); }); @@ -133,18 +128,17 @@ describe('usePushCurrentTab', () => { describe('SearchPageContainer - Tests', () => { let state: DeepPartial; let queryClient: QueryClient; - let history: History; let holder: HTMLElement; let cartItems: DownloadCartItem[]; function renderComponent(): RenderResult { return render( - + - + ); } @@ -152,40 +146,7 @@ describe('SearchPageContainer - Tests', () => { beforeEach(() => { cartItems = []; queryClient = new QueryClient(); - history = createMemoryHistory({ - initialEntries: ['/search/data'], - }); - // @ts-expect-error we need it this way - delete window.location; - // @ts-expect-error we need it this way - window.location = new URL(`http://localhost/search/data`); - - // below code keeps window.location in sync with history changes - // (needed because useUpdateQueryParam uses window.location not history) - const historyReplace = history.replace; - const historyReplaceSpy = vi.spyOn(history, 'replace'); - historyReplaceSpy.mockImplementation((args) => { - historyReplace(args); - if (typeof args === 'string') { - // @ts-expect-error we need it this way - window.location = new URL(`http://localhost${args}`); - } else { - // @ts-expect-error we need it this way - window.location = new URL(`http://localhost${createPath(args)}`); - } - }); - const historyPush = history.push; - const historyPushSpy = vi.spyOn(history, 'push'); - historyPushSpy.mockImplementation((args) => { - historyPush(args); - if (typeof args === 'string') { - // @ts-expect-error we need it this way - window.location = new URL(`http://localhost${args}`); - } else { - // @ts-expect-error we need it this way - window.location = new URL(`http://localhost${createPath(args)}`); - } - }); + window.history.replaceState({}, '', '/search/data'); window.localStorage.clear(); @@ -243,7 +204,7 @@ describe('SearchPageContainer - Tests', () => { window.localStorage.__proto__, 'removeItem' ); - history.replace({ key: 'testKey', pathname: '/' }); + window.history.replaceState({}, '', '/'); renderComponent(); @@ -370,7 +331,9 @@ describe('SearchPageContainer - Tests', () => { }); it('builds correct parameters for datafile request if date and search text properties are in use', async () => { - history.replace( + window.history.replaceState( + {}, + '', '/search/data?searchText=hello&dataset=false&investigation=false&startDate=2013-11-11&endDate=2016-11-11' ); @@ -407,7 +370,9 @@ describe('SearchPageContainer - Tests', () => { }); it('builds correct parameters for dataset request if date and search text properties are in use', async () => { - history.replace( + window.history.replaceState( + {}, + '', '/search/data?searchText=hello&datafile=false&investigation=false&startDate=2013-11-11&endDate=2016-11-11' ); @@ -446,7 +411,9 @@ describe('SearchPageContainer - Tests', () => { }); it('builds correct parameters for investigation request if date and search text properties are in use', async () => { - history.replace( + window.history.replaceState( + {}, + '', '/search/data?searchText=hello&dataset=false&datafile=false&startDate=2013-11-11&endDate=2016-11-11' ); @@ -488,7 +455,9 @@ describe('SearchPageContainer - Tests', () => { }); it('builds correct parameters for datafile request if only start date is in use', async () => { - history.replace( + window.history.replaceState( + {}, + '', '/search/data?searchText=&dataset=false&investigation=false&startDate=2013-11-11' ); @@ -524,7 +493,9 @@ describe('SearchPageContainer - Tests', () => { }); it('builds correct parameters for dataset request if only start date is in use', async () => { - history.replace( + window.history.replaceState( + {}, + '', '/search/data?searchText=test&datafile=false&investigation=false&startDate=2013-11-11' ); @@ -563,7 +534,9 @@ describe('SearchPageContainer - Tests', () => { }); it('builds correct parameters for investigation request if only start date is in use', async () => { - history.replace( + window.history.replaceState( + {}, + '', '/search/data?searchText=test&dataset=false&datafile=false&startDate=2013-11-11' ); @@ -607,7 +580,9 @@ describe('SearchPageContainer - Tests', () => { it('builds correct parameters for datafile request if only end date is in use', async () => { const user = userEvent.setup(); - history.replace( + window.history.replaceState( + {}, + '', '/search/data?searchText=&dataset=false&investigation=false&endDate=2016-11-11' ); @@ -646,7 +621,9 @@ describe('SearchPageContainer - Tests', () => { }); it('builds correct parameters for dataset request if only end date is in use', async () => { - history.replace( + window.history.replaceState( + {}, + '', '/search/data?searchText=test&datafile=false&investigation=false&endDate=2016-11-11' ); @@ -685,7 +662,9 @@ describe('SearchPageContainer - Tests', () => { }); it('builds correct parameters for investigation request if only end date is in use', async () => { - history.replace( + window.history.replaceState( + {}, + '', '/search/data?searchText=test&dataset=false&datafile=false&endDate=2016-11-11' ); @@ -728,7 +707,11 @@ describe('SearchPageContainer - Tests', () => { it('builds correct parameters for datafile request if date and search text properties are not in use', async () => { const user = userEvent.setup(); - history.replace('/search/data?dataset=false&investigation=false'); + window.history.replaceState( + {}, + '', + '/search/data?dataset=false&investigation=false' + ); renderComponent(); @@ -767,7 +750,11 @@ describe('SearchPageContainer - Tests', () => { it('builds correct parameters for dataset request if date and search text properties are not in use', async () => { const user = userEvent.setup(); - history.replace('/search/data?datafile=false&investigation=false'); + window.history.replaceState( + {}, + '', + '/search/data?datafile=false&investigation=false' + ); renderComponent(); @@ -804,7 +791,11 @@ describe('SearchPageContainer - Tests', () => { it('builds correct parameters for investigation request if date and search text properties are not in use', async () => { const user = userEvent.setup(); - history.replace('/search/data?dataset=false&datafile=false'); + window.history.replaceState( + {}, + '', + '/search/data?dataset=false&datafile=false' + ); renderComponent(); @@ -842,18 +833,13 @@ describe('SearchPageContainer - Tests', () => { it('display clear filters button and clear for filters onClick', async () => { const user = userEvent.setup(); - - renderComponent(); - - await user.click( - screen.getByRole('button', { name: 'searchBox.search_button_arialabel' }) + window.history.replaceState( + {}, + '', + `/search/data?searchText=&filters=%7B"title"%3A%7B"value"%3A"spend"%2C"type"%3A"include"%7D%7D` ); - act(() => { - history.replace( - `/search/data?filters=%7B"title"%3A%7B"value"%3A"spend"%2C"type"%3A"include"%7D%7D` - ); - }); + renderComponent(); expect( await screen.findByRole('button', { name: 'app.clear_filters' }) @@ -864,7 +850,7 @@ describe('SearchPageContainer - Tests', () => { expect( await screen.findByRole('button', { name: 'app.clear_filters' }) ).toBeDisabled(); - expect(history.location.search).toEqual('?'); + expect(window.location.search).toEqual('?searchText='); }); it('display disabled clear filters button', async () => { @@ -889,11 +875,15 @@ describe('SearchPageContainer - Tests', () => { - + + {/* Test link to update search params mid-test */} + + Link + - + ); } @@ -901,11 +891,11 @@ describe('SearchPageContainer - Tests', () => { const user = userEvent.setup(); // test it works with loading from URL params - act(() => { - history.replace( - '/search/data?searchText=test&dataset=false&datafile=false' - ); - }); + window.history.replaceState( + {}, + '', + '/search/data?searchText=test&dataset=false&datafile=false' + ); renderComponentWithRealStore(); @@ -916,9 +906,7 @@ describe('SearchPageContainer - Tests', () => { expect(screen.queryByRole('tab', { name: 'tabs.datafile' })).toBeNull(); // also test it works on initiateSearch - act(() => { - history.replace('/search/data?searchText=test&datafile=false'); - }); + await user.click(screen.getByRole('link', { name: 'Link' })); await user.click( screen.getByRole('button', { name: 'searchBox.search_button_arialabel' }) @@ -936,7 +924,9 @@ describe('SearchPageContainer - Tests', () => { it('search is not initiated when no search types are enabled', async () => { const user = userEvent.setup(); - history.replace( + window.history.replaceState( + {}, + '', '/search/data?searchText=test&investigation=false&dataset=false&datafile=false' ); @@ -974,7 +964,7 @@ describe('SearchPageContainer - Tests', () => { screen.getByRole('button', { name: 'searchBox.search_button_arialabel' }) ); - expect(history.location.search).toEqual('?searchText=test&restrict=true'); + expect(window.location.search).toEqual('?searchText=test&restrict=true'); }); it('shows SelectionAlert banner when item selected', async () => { @@ -999,7 +989,9 @@ describe('SearchPageContainer - Tests', () => { }); it('initiates search when visiting a direct url', async () => { - history.replace( + window.history.replaceState( + {}, + '', '/search/data?searchText=hello&restrict=true&startDate=2013-11-11&endDate=2016-11-11' ); @@ -1039,7 +1031,7 @@ describe('SearchPageContainer - Tests', () => { }); it('initiates search when visiting a direct url with empty search text', async () => { - history.replace('/search/data?searchText='); + window.history.replaceState({}, '', '/search/data?searchText='); renderComponent(); @@ -1076,7 +1068,11 @@ describe('SearchPageContainer - Tests', () => { if (state.dgsearch) state.dgsearch.searchableEntities = ['investigation', 'dataset']; - history.replace('/search/data?searchText=hello&datafiles=true'); + window.history.replaceState( + {}, + '', + '/search/data?searchText=hello&datafile=true' + ); renderComponent(); @@ -1169,8 +1165,9 @@ describe('SearchPageContainer - Tests', () => { screen.getByRole('button', { name: 'searchBox.search_button_arialabel' }) ); - expect(axios.get).toHaveBeenNthCalledWith( - 1, + // expect 4 calls, 1 to the cart and 3 searches + expect(axios.get).toHaveBeenCalledTimes(4); + expect(axios.get).toHaveBeenCalledWith( 'https://example.com/icat/search/documents', { params: generateURLSearchParams({ @@ -1241,7 +1238,7 @@ describe('SearchPageContainer - Tests', () => { await screen.findByTestId('search-box-container') ).toBeInTheDocument(); - expect(history.location.search).toEqual('?currentTab=dataset'); + expect(window.location.search).toEqual('?currentTab=dataset'); }); it('defaults to datafile if when investigation and dataset are false ', async () => { @@ -1260,7 +1257,7 @@ describe('SearchPageContainer - Tests', () => { await screen.findByTestId('search-box-container') ).toBeInTheDocument(); - expect(history.location.search).toEqual('?currentTab=datafile'); + expect(window.location.search).toEqual('?currentTab=datafile'); }); it('defaults to investigation if when investigation ,dataset and datafile are false ', async () => { @@ -1280,7 +1277,7 @@ describe('SearchPageContainer - Tests', () => { ).toBeInTheDocument(); // i.e default value is investigation it set in the searchPageContainer - expect(history.location.search).toEqual(''); + expect(window.location.search).toEqual(''); }); it('handles anonymous users correctly', async () => { diff --git a/packages/datagateway-search/src/searchPageContainer.component.tsx b/packages/datagateway-search/src/searchPageContainer.component.tsx index 4c546a968..c1b18c4f2 100644 --- a/packages/datagateway-search/src/searchPageContainer.component.tsx +++ b/packages/datagateway-search/src/searchPageContainer.component.tsx @@ -1,13 +1,6 @@ import React from 'react'; import { useDispatch, useSelector } from 'react-redux'; -import { - Link, - Route, - RouteComponentProps, - Switch, - useHistory, - useLocation, -} from 'react-router-dom'; +import { Link, Route, Routes, useLocation, useNavigate } from 'react-router'; import { StateType } from './state/app.types'; import { Grid, Paper, styled } from '@mui/material'; @@ -89,7 +82,7 @@ const getResults = (searchableEntities: string): number | null => { }; export const usePushCurrentTab = (): ((newCurrentTab: string) => void) => { - const { push } = useHistory(); + const navigate = useNavigate(); const location = useLocation(); const { filters, page, results, currentTab } = React.useMemo( () => parseSearchToQuery(location.search), @@ -117,9 +110,9 @@ export const usePushCurrentTab = (): ((newCurrentTab: string) => void) => { results: newResults, currentTab: newCurrentTab, }; - push(`?${parseQueryToSearch(query).toString()}`); + navigate(`?${parseQueryToSearch(query).toString()}`); }, - [currentTab, filters, page, push, results] + [currentTab, filters, page, navigate, results] ); }; @@ -244,8 +237,8 @@ const SearchPageContainer: React.FC = () => { (isFirstRender || checkedBoxes.includes(queryParams.currentTab)) ? queryParams.currentTab : checkedBoxes.length !== 0 - ? checkedBoxes[0] - : searchableEntities[0]; + ? checkedBoxes[0] + : searchableEntities[0]; //Do not allow these to be searched if they are not searchable (prevents URL //forcing them to be searched) @@ -394,9 +387,12 @@ const SearchPageContainer: React.FC = () => { const containerHeight = `calc(100vh - 64px - 36px - ${searchBoxHeight}px - 8px - 49px - 2px)`; const { data: cartItems } = useCart(); - const { push } = useHistory(); + const navigate = useNavigate(); - const navigateToDownload = React.useCallback(() => push('/download'), [push]); + const navigateToDownload = React.useCallback( + () => navigate('/download'), + [navigate] + ); const disabled = Object.keys(queryParams.filters).length === 0; @@ -407,15 +403,11 @@ const SearchPageContainer: React.FC = () => { }; return ( - - Search data} - /> + + Search data} /> ) => ( + element={ { { )} - )} + } /> - + ); }; diff --git a/packages/datagateway-search/src/searchTabs/searchTabs.component.test.tsx b/packages/datagateway-search/src/searchTabs/searchTabs.component.test.tsx index b2fd41bfe..5b7027340 100644 --- a/packages/datagateway-search/src/searchTabs/searchTabs.component.test.tsx +++ b/packages/datagateway-search/src/searchTabs/searchTabs.component.test.tsx @@ -1,15 +1,14 @@ import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; -import { act, render, screen, within } from '@testing-library/react'; +import { render, screen, within } from '@testing-library/react'; import axios, { type AxiosRequestConfig, type AxiosResponse } from 'axios'; import { dGCommonInitialState, type DatasearchType, type SearchResponse, } from 'datagateway-common'; -import { createMemoryHistory, type History } from 'history'; import * as React from 'react'; import { Provider } from 'react-redux'; -import { Router } from 'react-router-dom'; +import { BrowserRouter, Route, Routes, useNavigate } from 'react-router'; import configureStore from 'redux-mock-store'; import thunk from 'redux-thunk'; import type { StateType } from '../state/app.types'; @@ -21,7 +20,6 @@ import SearchTabs from './searchTabs.component'; describe('SearchTabs', () => { let state: StateType; - let history: History; let user: ReturnType; const mockStore = configureStore([thunk]); let searchParams: URLSearchParams; @@ -142,31 +140,47 @@ describe('SearchTabs', () => { return Promise.reject(`Endpoint not mocked: ${url}`); }; + const ChangeSearchParamsButton = () => { + const navigate = useNavigate(); + return ( + + ); + }; + const Wrapper = ({ children, }: { children: React.ReactNode; }): JSX.Element => ( - + - {children} + + + {children} + + + } + /> + - + ); beforeEach(() => { searchParams = new URLSearchParams(); searchParams.append('searchText', 'test'); - history = createMemoryHistory({ - initialEntries: [ - { - pathname: '/search/data', - search: searchParams.toString(), - }, - ], - }); + window.history.replaceState( + {}, + '', + `/search/data?${searchParams.toString()}` + ); user = userEvent.setup(); state = JSON.parse( @@ -202,7 +216,6 @@ describe('SearchTabs', () => { { { { ); searchParams.set('currentTab', 'dataset'); - act(() => { - history.replace({ search: searchParams.toString() }); - }); + await user.click( + screen.getByRole('button', { name: 'Change search params' }) + ); expect( screen.queryByTestId('investigation-search-table') @@ -318,7 +329,6 @@ describe('SearchTabs', () => { { ); searchParams.set('currentTab', 'datafile'); - act(() => { - history.replace({ search: searchParams.toString() }); - }); + await user.click( + screen.getByRole('button', { name: 'Change search params' }) + ); expect( screen.queryByTestId('investigation-search-table') @@ -354,7 +364,6 @@ describe('SearchTabs', () => { { { /> ); searchParams.set('currentTab', 'dataset'); - act(() => { - history.replace({ search: searchParams.toString() }); - }); + await user.click( + screen.getByRole('button', { name: 'Change search params' }) + ); expect( screen.queryByTestId('investigation-search-card-view') @@ -420,7 +428,6 @@ describe('SearchTabs', () => { { /> ); searchParams.set('currentTab', 'datafile'); - act(() => { - history.replace({ search: searchParams.toString() }); - }); + await user.click( + screen.getByRole('button', { name: 'Change search params' }) + ); expect( screen.queryByTestId('investigation-search-card-view') @@ -451,16 +458,17 @@ describe('SearchTabs', () => { }, }; - const onTabChange = vi.fn((newTab) => { + const onTabChange = vi.fn(async (newTab) => { searchParams.set('currentTab', newTab); - history.replace({ search: searchParams.toString() }); + await user.click( + screen.getByRole('button', { name: 'Change search params' }) + ); }); const { rerender } = render( { { { { ({ backgroundColor: (theme as any).colours?.tabsGrey, })); -export interface SearchTabsProps { +export interface BaseSearchTabsProps { view: ViewsType; containerHeight: string; hierarchy: string; @@ -86,7 +87,7 @@ export interface SearchTabsProps { currentTab: string; } -const SearchTabs = ({ +const BaseSearchTabs = ({ view, containerHeight, hierarchy, @@ -94,7 +95,7 @@ const SearchTabs = ({ currentTab, cartItems, navigateToDownload, -}: SearchTabsProps & CartProps): React.ReactElement => { +}: BaseSearchTabsProps & CartProps): React.ReactElement => { const isDatasetTabEnabled = useSelector( (state: StateType) => state.dgsearch.tabs.datasetTab ); @@ -272,4 +273,11 @@ const SearchTabs = ({ ); }; +const SearchTabs = ( + props: Omit & CartProps +) => { + const { hierarchy = '' } = useParams(); + return ; +}; + export default SearchTabs; diff --git a/packages/datagateway-search/src/setupTests.ts b/packages/datagateway-search/src/setupTests.ts index 967e4753d..ddb29e103 100644 --- a/packages/datagateway-search/src/setupTests.ts +++ b/packages/datagateway-search/src/setupTests.ts @@ -15,10 +15,6 @@ vi.setConfig({ testTimeout: 20_000 }); // and https://github.com/testing-library/user-event/issues/1115 vi.stubGlobal('jest', { advanceTimersByTime: vi.advanceTimersByTime.bind(vi) }); -function noOp(): void { - // required as work-around for jsdom environment not implementing window.URL.createObjectURL method -} - // Mock Date.toLocaleDateString so that it always uses en-GB as locale and UTC timezone // instead of using the system default, which can be different depending on the environment. // save a reference to the original implementation of Date.toLocaleDateString @@ -35,10 +31,6 @@ vi.spyOn(Date.prototype, 'toLocaleDateString').mockImplementation(function ( return toLocaleDateString.call(this, 'en-GB', { timeZone: 'UTC' }); }); -if (typeof window.URL.createObjectURL === 'undefined') { - Object.defineProperty(window.URL, 'createObjectURL', { value: noOp }); -} - // jsdom doesn't implement ResizeObserver so mock it vi.stubGlobal( 'ResizeObserver', diff --git a/packages/datagateway-search/src/table/datafileSearchTable.component.test.tsx b/packages/datagateway-search/src/table/datafileSearchTable.component.test.tsx index 71de5fa0e..11576d58d 100644 --- a/packages/datagateway-search/src/table/datafileSearchTable.component.test.tsx +++ b/packages/datagateway-search/src/table/datafileSearchTable.component.test.tsx @@ -15,9 +15,8 @@ import { SearchResultSource, dGCommonInitialState, } from 'datagateway-common'; -import { History, createMemoryHistory } from 'history'; import { Provider } from 'react-redux'; -import { Router } from 'react-router-dom'; +import { BrowserRouter } from 'react-router'; import configureStore from 'redux-mock-store'; import thunk from 'redux-thunk'; import { @@ -35,7 +34,6 @@ import DatafileSearchTable from './datafileSearchTable.component'; describe('Datafile search table component', () => { const mockStore = configureStore([thunk]); let state: StateType; - let history: History; let queryClient: QueryClient; let user: ReturnType; let cartItems: DownloadCartItem[]; @@ -46,11 +44,11 @@ describe('Datafile search table component', () => { const renderComponent = (hierarchy?: string): RenderResult => render( - + - + ); @@ -113,11 +111,11 @@ describe('Datafile search table component', () => { } beforeEach(() => { - history = createMemoryHistory({ - initialEntries: [ - { search: '?searchText=test search¤tTab=datafile' }, - ], - }); + window.history.replaceState( + {}, + '', + '/search/data?searchText=test search¤tTab=datafile' + ); user = userEvent.setup(); queryClient = new QueryClient({ defaultOptions: { @@ -215,9 +213,13 @@ describe('Datafile search table component', () => { }); it('disables the search query if datafile search is disabled', async () => { - const searchParams = new URLSearchParams(history.location.search); + const searchParams = new URLSearchParams(window.location.search); searchParams.append('datafile', 'false'); - history.replace({ search: `?${searchParams.toString()}` }); + window.history.replaceState( + {}, + '', + `${window.location.pathname}?${searchParams.toString()}` + ); renderComponent(); @@ -377,15 +379,18 @@ describe('Datafile search table component', () => { }); it('applies filters already present in the URL on first render', async () => { - const searchParams = new URLSearchParams(history.location.search); + const searchParams = new URLSearchParams(window.location.search); searchParams.append( 'filters', JSON.stringify({ 'Datafile.datafileFormat.name': ['txt'], }) ); - history.replace({ search: `?${searchParams.toString()}` }); - + window.history.replaceState( + {}, + '', + `${window.location.pathname}?${searchParams.toString()}` + ); renderComponent(); // when filters are applied @@ -412,15 +417,18 @@ describe('Datafile search table component', () => { }); it('allows filters to be removed through the facet filter panel', async () => { - const searchParams = new URLSearchParams(history.location.search); + const searchParams = new URLSearchParams(window.location.search); searchParams.append( 'filters', JSON.stringify({ 'Datafile.datafileFormat.name': ['txt'], }) ); - history.replace({ search: `?${searchParams.toString()}` }); - + window.history.replaceState( + {}, + '', + `${window.location.pathname}?${searchParams.toString()}` + ); renderComponent(); // when filters are applied @@ -474,15 +482,18 @@ describe('Datafile search table component', () => { }); it('allows filters to be removed by removing filter chips', async () => { - const searchParams = new URLSearchParams(history.location.search); + const searchParams = new URLSearchParams(window.location.search); searchParams.append( 'filters', JSON.stringify({ 'Datafile.datafileFormat.name': ['txt'], }) ); - history.replace({ search: `?${searchParams.toString()}` }); - + window.history.replaceState( + {}, + '', + `${window.location.pathname}?${searchParams.toString()}` + ); renderComponent(); // when filters are applied diff --git a/packages/datagateway-search/src/table/datafileSearchTable.component.tsx b/packages/datagateway-search/src/table/datafileSearchTable.component.tsx index 2ea1199ea..7cf6c0b6e 100644 --- a/packages/datagateway-search/src/table/datafileSearchTable.component.tsx +++ b/packages/datagateway-search/src/table/datafileSearchTable.component.tsx @@ -25,7 +25,7 @@ import { import React from 'react'; import { useTranslation } from 'react-i18next'; import { useSelector } from 'react-redux'; -import { useLocation } from 'react-router-dom'; +import { useLocation } from 'react-router'; import type { TableCellProps } from 'react-virtualized'; import FacetPanel from '../facet/components/facetPanel/facetPanel.component'; import SelectedFilterChips from '../facet/components/selectedFilterChips.component'; @@ -46,9 +46,16 @@ const DatafileSearchTable: React.FC = (props) => { () => parseSearchToQuery(location.search), [location.search] ); - const { startDate, endDate, sort, filters, restrict, datafile, currentTab } = - queryParams; - const searchText = queryParams.searchText ? queryParams.searchText : ''; + const { + startDate, + endDate, + sort, + filters, + restrict, + datafile, + currentTab, + searchText, + } = queryParams; const disableSelectAll = useSelector( (state: StateType) => state.dgcommon.features?.disableSelectAll ?? false @@ -66,7 +73,7 @@ const DatafileSearchTable: React.FC = (props) => { useLuceneSearchInfinite( 'Datafile', { - searchText, + searchText: searchText ?? '', startDate, endDate, sort, @@ -87,7 +94,7 @@ const DatafileSearchTable: React.FC = (props) => { }, currentTab === 'datafile' ? filters : {}, { - enabled: datafile, + enabled: datafile && searchText !== null, // this select removes the facet count for the InvestigationInstrument.instrument.name // facet since the number is confusing for datafiles select: (data) => ({ diff --git a/packages/datagateway-search/src/table/datasetSearchTable.component.test.tsx b/packages/datagateway-search/src/table/datasetSearchTable.component.test.tsx index 9f2b2e1fa..87c251d0c 100644 --- a/packages/datagateway-search/src/table/datasetSearchTable.component.test.tsx +++ b/packages/datagateway-search/src/table/datasetSearchTable.component.test.tsx @@ -16,9 +16,8 @@ import { type SearchResponse, type SearchResult, } from 'datagateway-common'; -import { createMemoryHistory, type History } from 'history'; import { Provider } from 'react-redux'; -import { Router } from 'react-router-dom'; +import { BrowserRouter } from 'react-router'; import configureStore from 'redux-mock-store'; import thunk from 'redux-thunk'; import { @@ -83,7 +82,6 @@ describe('Dataset table component', () => { const mockStore = configureStore([thunk]); let container: HTMLDivElement; let state: StateType; - let history: History; let queryClient: QueryClient; let user: ReturnType; let cartItems: DownloadCartItem[]; @@ -93,11 +91,11 @@ describe('Dataset table component', () => { const renderComponent = (hierarchy?: string): RenderResult => { return render( - + - + , { container: document.body.appendChild(container) } ); @@ -148,9 +146,11 @@ describe('Dataset table component', () => { beforeEach(() => { user = userEvent.setup(); - history = createMemoryHistory({ - initialEntries: [{ search: 'searchText=test search¤tTab=dataset' }], - }); + window.history.replaceState( + {}, + '', + '/search/data?searchText=test search¤tTab=dataset' + ); queryClient = new QueryClient({ defaultOptions: { queries: { retry: false }, @@ -186,10 +186,13 @@ describe('Dataset table component', () => { }); it('disables the search query if dataset search is disabled', async () => { - const searchParams = new URLSearchParams(history.location.search); + const searchParams = new URLSearchParams(window.location.search); searchParams.append('dataset', 'false'); - history.replace({ search: `?${searchParams.toString()}` }); - + window.history.replaceState( + {}, + '', + `${window.location.pathname}?${searchParams.toString()}` + ); renderComponent(); // check that column headers are shown correctly. @@ -424,15 +427,18 @@ describe('Dataset table component', () => { }); it('applies filters already present in the URL on first render', async () => { - const searchParams = new URLSearchParams(history.location.search); + const searchParams = new URLSearchParams(window.location.search); searchParams.append( 'filters', JSON.stringify({ 'Dataset.name': ['asd'], }) ); - history.replace({ search: `?${searchParams.toString()}` }); - + window.history.replaceState( + {}, + '', + `${window.location.pathname}?${searchParams.toString()}` + ); renderComponent(); // when filters are applied @@ -459,15 +465,18 @@ describe('Dataset table component', () => { }); it('allows filters to be removed through the facet filter panel', async () => { - const searchParams = new URLSearchParams(history.location.search); + const searchParams = new URLSearchParams(window.location.search); searchParams.append( 'filters', JSON.stringify({ 'Dataset.name': ['asd'], }) ); - history.replace({ search: `?${searchParams.toString()}` }); - + window.history.replaceState( + {}, + '', + `${window.location.pathname}?${searchParams.toString()}` + ); renderComponent(); const selectedFilterChips = await screen.findByLabelText('selectedFilters'); @@ -527,15 +536,18 @@ describe('Dataset table component', () => { }); it('allows filters to be removed by removing filter chips', async () => { - const searchParams = new URLSearchParams(history.location.search); + const searchParams = new URLSearchParams(window.location.search); searchParams.append( 'filters', JSON.stringify({ 'Dataset.name': ['asd'], }) ); - history.replace({ search: `?${searchParams.toString()}` }); - + window.history.replaceState( + {}, + '', + `${window.location.pathname}?${searchParams.toString()}` + ); renderComponent(); // when filters are applied @@ -710,7 +722,7 @@ describe('Dataset table component', () => { }) ); - expect(history.location.pathname).toBe( + expect(window.location.pathname).toBe( '/browse/instrument/4/facilityCycle/6/investigation/2/dataset/1/datafile' ); }); diff --git a/packages/datagateway-search/src/table/datasetSearchTable.component.tsx b/packages/datagateway-search/src/table/datasetSearchTable.component.tsx index f05d2c780..cd3cb4d09 100644 --- a/packages/datagateway-search/src/table/datasetSearchTable.component.tsx +++ b/packages/datagateway-search/src/table/datasetSearchTable.component.tsx @@ -27,7 +27,7 @@ import { import React from 'react'; import { useTranslation } from 'react-i18next'; import { useSelector } from 'react-redux'; -import { useHistory, useLocation } from 'react-router-dom'; +import { useLocation, useNavigate } from 'react-router'; import type { IndexRange, TableCellProps } from 'react-virtualized'; import FacetPanel from '../facet/components/facetPanel/facetPanel.component'; import SelectedFilterChips from '../facet/components/selectedFilterChips.component'; @@ -42,7 +42,7 @@ interface DatasetTableProps { const DatasetSearchTable: React.FC = ({ hierarchy }) => { const location = useLocation(); - const { push } = useHistory(); + const navigate = useNavigate(); const queryParams = React.useMemo( () => parseSearchToQuery(location.search), [location.search] @@ -95,7 +95,7 @@ const DatasetSearchTable: React.FC = ({ hierarchy }) => { }, currentTab === 'dataset' ? filters : {}, { - enabled: dataset, + enabled: dataset && searchText !== null, // this select removes the facet count for the InvestigationInstrument.instrument.name // facet since the number is confusing for datafiles select: (data) => ({ @@ -352,7 +352,7 @@ const DatasetSearchTable: React.FC = ({ hierarchy }) => { rowData={rowData} detailsPanelResize={detailsPanelResize} viewDatafiles={() => { - if (url) push(url); + if (url) navigate(url); }} /> ); @@ -375,7 +375,7 @@ const DatasetSearchTable: React.FC = ({ hierarchy }) => { ); } }, - [hierarchy, push] + [hierarchy, navigate] ); if (currentTab !== 'dataset') return null; diff --git a/packages/datagateway-search/src/table/investigationSearchTable.component.test.tsx b/packages/datagateway-search/src/table/investigationSearchTable.component.test.tsx index 5b26e5837..96b7ca27b 100644 --- a/packages/datagateway-search/src/table/investigationSearchTable.component.test.tsx +++ b/packages/datagateway-search/src/table/investigationSearchTable.component.test.tsx @@ -15,9 +15,8 @@ import { type SearchResponse, type SearchResult, } from 'datagateway-common'; -import { createMemoryHistory, type History } from 'history'; import { Provider } from 'react-redux'; -import { Router } from 'react-router-dom'; +import { BrowserRouter } from 'react-router'; import configureStore from 'redux-mock-store'; import thunk from 'redux-thunk'; import { @@ -36,7 +35,6 @@ describe('Investigation Search Table component', () => { const mockStore = configureStore([thunk]); let container: HTMLDivElement; let state: StateType; - let history: History; let user: ReturnType; let queryClient: QueryClient; @@ -47,11 +45,11 @@ describe('Investigation Search Table component', () => { const renderComponent = (hierarchy?: string): RenderResult => { return render( - + - + , { container: document.body.appendChild(container), @@ -118,11 +116,11 @@ describe('Investigation Search Table component', () => { beforeEach(() => { user = userEvent.setup(); - history = createMemoryHistory({ - initialEntries: [ - { search: 'searchText=test search¤tTab=investigation' }, - ], - }); + window.history.replaceState( + {}, + '', + '/search/data?searchText=test search¤tTab=investigation' + ); queryClient = new QueryClient({ defaultOptions: { queries: { retry: false }, @@ -193,10 +191,13 @@ describe('Investigation Search Table component', () => { }); it('disables the search query if investigation search is disabled', async () => { - const searchParams = new URLSearchParams(history.location.search); + const searchParams = new URLSearchParams(window.location.search); searchParams.append('investigation', 'false'); - history.replace({ search: `?${searchParams.toString()}` }); - + window.history.replaceState( + {}, + '', + `${window.location.pathname}?${searchParams.toString()}` + ); renderComponent(); // check that column headers are shown correctly. @@ -522,15 +523,18 @@ describe('Investigation Search Table component', () => { }); it('applies filters already present in the URL on first render', async () => { - const searchParams = new URLSearchParams(history.location.search); + const searchParams = new URLSearchParams(window.location.search); searchParams.append( 'filters', JSON.stringify({ 'Investigation.type.name': ['experiment'], }) ); - history.replace({ search: `?${searchParams.toString()}` }); - + window.history.replaceState( + {}, + '', + `${window.location.pathname}?${searchParams.toString()}` + ); renderComponent(); // when filters are applied @@ -565,15 +569,18 @@ describe('Investigation Search Table component', () => { }); it('allows filters to be removed through the facet filter panel', async () => { - const searchParams = new URLSearchParams(history.location.search); + const searchParams = new URLSearchParams(window.location.search); searchParams.append( 'filters', JSON.stringify({ 'Investigation.type.name': ['experiment'], }) ); - history.replace({ search: `?${searchParams.toString()}` }); - + window.history.replaceState( + {}, + '', + `${window.location.pathname}?${searchParams.toString()}` + ); renderComponent(); const selectedFilterChips = await screen.findByLabelText('selectedFilters'); @@ -633,15 +640,18 @@ describe('Investigation Search Table component', () => { }); it('allows filters to be removed by removing filter chips', async () => { - const searchParams = new URLSearchParams(history.location.search); + const searchParams = new URLSearchParams(window.location.search); searchParams.append( 'filters', JSON.stringify({ 'Investigation.type.name': ['calibration'], }) ); - history.replace({ search: `?${searchParams.toString()}` }); - + window.history.replaceState( + {}, + '', + `${window.location.pathname}?${searchParams.toString()}` + ); renderComponent(); // when filters are applied @@ -818,7 +828,7 @@ describe('Investigation Search Table component', () => { }) ); - expect(history.location.pathname).toBe( + expect(window.location.pathname).toBe( '/browse/instrument/3/facilityCycle/5/investigation/1/dataset' ); }); diff --git a/packages/datagateway-search/src/table/investigationSearchTable.component.tsx b/packages/datagateway-search/src/table/investigationSearchTable.component.tsx index f4ad26bb2..9234f7dbb 100644 --- a/packages/datagateway-search/src/table/investigationSearchTable.component.tsx +++ b/packages/datagateway-search/src/table/investigationSearchTable.component.tsx @@ -24,7 +24,7 @@ import { import React from 'react'; import { useTranslation } from 'react-i18next'; import { useSelector } from 'react-redux'; -import { useHistory, useLocation } from 'react-router-dom'; +import { useLocation, useNavigate } from 'react-router'; import { TableCellProps } from 'react-virtualized'; import FacetPanel from '../facet/components/facetPanel/facetPanel.component'; import SelectedFilterChips from '../facet/components/selectedFilterChips.component'; @@ -41,7 +41,7 @@ const InvestigationSearchTable: React.FC = (props) => { const { hierarchy } = props; const location = useLocation(); - const { push } = useHistory(); + const navigate = useNavigate(); const queryParams = React.useMemo( () => parseSearchToQuery(location.search), [location.search] @@ -54,8 +54,8 @@ const InvestigationSearchTable: React.FC = (props) => { restrict, investigation, currentTab, + searchText, } = queryParams; - const searchText = queryParams.searchText ? queryParams.searchText : ''; const disableSelectAll = useSelector( (state: StateType) => state.dgcommon.features?.disableSelectAll ?? false @@ -75,7 +75,7 @@ const InvestigationSearchTable: React.FC = (props) => { useLuceneSearchInfinite( 'Investigation', { - searchText, + searchText: searchText ?? '', startDate, endDate, sort, @@ -100,7 +100,7 @@ const InvestigationSearchTable: React.FC = (props) => { }, currentTab === 'investigation' ? filters : {}, - { enabled: investigation } + { enabled: investigation && searchText !== null } ); const { data: cartItems, isPending: cartLoading } = useCart(); const { mutate: addToCart, isPending: addToCartLoading } = @@ -307,7 +307,7 @@ const InvestigationSearchTable: React.FC = (props) => { rowData={rowData} detailsPanelResize={detailsPanelResize} viewDatasets={() => { - if (url) push(url); + if (url) navigate(url); }} /> ); @@ -330,7 +330,7 @@ const InvestigationSearchTable: React.FC = (props) => { ); } }, - [hierarchy, push] + [hierarchy, navigate] ); if (currentTab !== 'investigation') return null; diff --git a/yarn.lock b/yarn.lock index 6d6fc704f..73e99064d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -210,7 +210,7 @@ __metadata: languageName: node linkType: hard -"@babel/runtime@npm:^7.1.2, @babel/runtime@npm:^7.12.1, @babel/runtime@npm:^7.12.13, @babel/runtime@npm:^7.12.5, @babel/runtime@npm:^7.14.6, @babel/runtime@npm:^7.17.2, @babel/runtime@npm:^7.18.3, @babel/runtime@npm:^7.20.6, @babel/runtime@npm:^7.21.0, @babel/runtime@npm:^7.23.2, @babel/runtime@npm:^7.23.9, @babel/runtime@npm:^7.24.6, @babel/runtime@npm:^7.26.0, @babel/runtime@npm:^7.28.6, @babel/runtime@npm:^7.5.5, @babel/runtime@npm:^7.7.2, @babel/runtime@npm:^7.8.7, @babel/runtime@npm:^7.9.2": +"@babel/runtime@npm:^7.12.1, @babel/runtime@npm:^7.12.5, @babel/runtime@npm:^7.14.6, @babel/runtime@npm:^7.17.2, @babel/runtime@npm:^7.18.3, @babel/runtime@npm:^7.20.6, @babel/runtime@npm:^7.21.0, @babel/runtime@npm:^7.23.2, @babel/runtime@npm:^7.23.9, @babel/runtime@npm:^7.24.6, @babel/runtime@npm:^7.26.0, @babel/runtime@npm:^7.28.6, @babel/runtime@npm:^7.5.5, @babel/runtime@npm:^7.7.2, @babel/runtime@npm:^7.8.7, @babel/runtime@npm:^7.9.2": version: 7.28.6 resolution: "@babel/runtime@npm:7.28.6" checksum: 42d8a868c2fc2e9a77927945a6daa7ec03c7ea49e611e0d15442933cdabb12f20e3a6849c729259076c10a4247adec229331d1f94c2d0073ea0979d7853e29fd @@ -1912,13 +1912,6 @@ __metadata: languageName: node linkType: hard -"@types/history@npm:4.7.11, @types/history@npm:^4.7.11": - version: 4.7.11 - resolution: "@types/history@npm:4.7.11" - checksum: c92e2ba407dcab0581a9afdf98f533aa41b61a71133420a6d92b1ca9839f741ab1f9395b17454ba5b88cb86020b70b22d74a1950ccfbdfd9beeaa5459fdc3464 - languageName: node - linkType: hard - "@types/hoist-non-react-statics@npm:^3.3.0, @types/hoist-non-react-statics@npm:^3.3.1": version: 3.3.1 resolution: "@types/hoist-non-react-statics@npm:3.3.1" @@ -2030,27 +2023,6 @@ __metadata: languageName: node linkType: hard -"@types/react-router-dom@npm:5.3.3": - version: 5.3.3 - resolution: "@types/react-router-dom@npm:5.3.3" - dependencies: - "@types/history": ^4.7.11 - "@types/react": "*" - "@types/react-router": "*" - checksum: 28c4ea48909803c414bf5a08502acbb8ba414669b4b43bb51297c05fe5addc4df0b8fd00e0a9d1e3535ec4073ef38aaafac2c4a2b95b787167d113bc059beff3 - languageName: node - linkType: hard - -"@types/react-router@npm:*": - version: 5.1.20 - resolution: "@types/react-router@npm:5.1.20" - dependencies: - "@types/history": ^4.7.11 - "@types/react": "*" - checksum: 128764143473a5e9457ddc715436b5d49814b1c214dde48939b9bef23f0e77f52ffcdfa97eb8d3cc27e2c229869c0cdd90f637d887b62f2c9f065a87d6425419 - languageName: node - linkType: hard - "@types/react-transition-group@npm:^4.4.10, @types/react-transition-group@npm:^4.4.8": version: 4.4.12 resolution: "@types/react-transition-group@npm:4.4.12" @@ -3349,6 +3321,13 @@ __metadata: languageName: node linkType: hard +"cookie@npm:^1.0.1": + version: 1.1.1 + resolution: "cookie@npm:1.1.1" + checksum: e40c4ff817a73ec0a41413654ca9a71e6207bd6e511151bb1d307aef2903eb59771127a85474afe0e12a8982804f7b03cce0bda798b55c306aca9608b4b9acab + languageName: node + linkType: hard + "cookie@npm:~0.7.1": version: 0.7.2 resolution: "cookie@npm:0.7.2" @@ -3566,7 +3545,6 @@ __metadata: "@types/lodash.debounce": 4.0.6 "@types/node": 24.12.0 "@types/react": 18.3.28 - "@types/react-router-dom": 5.3.3 "@types/react-virtualized": 9.22.2 "@vitejs/plugin-react": 5.2.0 "@vitest/coverage-v8": 3.2.3 @@ -3583,7 +3561,6 @@ __metadata: eslint-plugin-testing-library: 7.16.0 globals: 17.4.0 hex-to-rgba: 2.0.1 - history: 4.10.1 i18next: 22.0.3 jsdom: 26.1.0 lint-staged: 16.4.0 @@ -3595,7 +3572,7 @@ __metadata: react-draggable: 4.5.0 react-i18next: 12.3.1 react-redux: 8.1.3 - react-router-dom: 5.3.4 + react-router: 7.13.2 react-test-renderer: 17.0.2 react-virtualized: 9.22.6 redux: 4.2.0 @@ -3612,7 +3589,7 @@ __metadata: "@mui/material": ">= 5.5.0 < 6" react: ^18.2.0 react-dom: ^18.2.0 - react-router-dom: ">= 5.2.0 < 6" + react-router: ">= 7 < 8" languageName: unknown linkType: soft @@ -3633,7 +3610,6 @@ __metadata: "@testing-library/jest-dom": 6.9.1 "@testing-library/react": 16.3.2 "@testing-library/user-event": 14.6.1 - "@types/history": 4.7.11 "@types/jsrsasign": 10.5.2 "@types/lodash.debounce": 4.0.6 "@types/lodash.memoize": 4.1.6 @@ -3641,7 +3617,6 @@ __metadata: "@types/react": 18.3.28 "@types/react-dom": 18.3.7 "@types/react-redux": 7.1.22 - "@types/react-router-dom": 5.3.3 "@types/react-virtualized": 9.22.2 "@types/redux-logger": 3.0.8 "@types/redux-mock-store": 1.5.0 @@ -3663,7 +3638,6 @@ __metadata: eslint-plugin-testing-library: 7.16.0 express: 4.22.1 globals: 17.4.0 - history: 4.10.1 i18next: 22.0.3 i18next-browser-languagedetector: 8.2.0 i18next-http-backend: 3.0.2 @@ -3678,7 +3652,7 @@ __metadata: react-dom: 18.3.1 react-i18next: 12.3.1 react-redux: 8.1.3 - react-router-dom: 5.3.4 + react-router: 7.13.2 react-virtualized: 9.22.6 redux: 4.2.0 redux-logger: 3.0.6 @@ -3717,7 +3691,6 @@ __metadata: "@types/node": 24.12.0 "@types/react": 18.3.28 "@types/react-dom": 18.3.7 - "@types/react-router-dom": 5.3.3 "@types/react-virtualized": 9.22.2 "@vitejs/plugin-react": 5.2.0 "@vitest/coverage-v8": 3.2.3 @@ -3739,7 +3712,6 @@ __metadata: express: 4.22.1 fastq: ^1.19.1 globals: 17.4.0 - history: 4.10.1 i18next: 22.0.3 i18next-browser-languagedetector: 8.2.0 i18next-http-backend: 3.0.2 @@ -3755,7 +3727,7 @@ __metadata: react: 18.3.1 react-dom: 18.3.1 react-i18next: 12.3.1 - react-router-dom: 5.3.4 + react-router: 7.13.2 react-virtualized: 9.22.6 single-spa-react: 5.1.4 start-server-and-test: ~2.1.0 @@ -3786,14 +3758,12 @@ __metadata: "@testing-library/jest-dom": 6.9.1 "@testing-library/react": 16.3.2 "@testing-library/user-event": 14.6.1 - "@types/history": 4.7.11 "@types/jsrsasign": 10.5.2 "@types/lodash.isequal": 4.5.8 "@types/node": 24.12.0 "@types/react": 18.3.28 "@types/react-dom": 18.3.7 "@types/react-redux": 7.1.22 - "@types/react-router-dom": 5.3.3 "@types/react-virtualized": 9.22.2 "@types/redux-logger": 3.0.8 "@types/redux-mock-store": 1.5.0 @@ -3815,7 +3785,6 @@ __metadata: eslint-plugin-testing-library: 7.16.0 express: 4.22.1 globals: 17.4.0 - history: 4.10.1 i18next: 22.0.3 i18next-browser-languagedetector: 8.2.0 i18next-http-backend: 3.0.2 @@ -3829,7 +3798,7 @@ __metadata: react-dom: 18.3.1 react-i18next: 12.3.1 react-redux: 8.1.3 - react-router-dom: 5.3.4 + react-router: 7.13.2 react-virtualized: 9.22.6 redux: 4.2.0 redux-logger: 3.0.6 @@ -5496,21 +5465,7 @@ __metadata: languageName: node linkType: hard -"history@npm:4.10.1, history@npm:^4.9.0": - version: 4.10.1 - resolution: "history@npm:4.10.1" - dependencies: - "@babel/runtime": ^7.1.2 - loose-envify: ^1.2.0 - resolve-pathname: ^3.0.0 - tiny-invariant: ^1.0.2 - tiny-warning: ^1.0.0 - value-equal: ^1.0.1 - checksum: addd84bc4683929bae4400419b5af132ff4e4e9b311a0d4e224579ea8e184a6b80d7f72c55927e4fa117f69076a9e47ce082d8d0b422f1a9ddac7991490ca1d0 - languageName: node - linkType: hard - -"hoist-non-react-statics@npm:^3.1.0, hoist-non-react-statics@npm:^3.3.0, hoist-non-react-statics@npm:^3.3.1, hoist-non-react-statics@npm:^3.3.2": +"hoist-non-react-statics@npm:^3.3.0, hoist-non-react-statics@npm:^3.3.1, hoist-non-react-statics@npm:^3.3.2": version: 3.3.2 resolution: "hoist-non-react-statics@npm:3.3.2" dependencies: @@ -6095,13 +6050,6 @@ __metadata: languageName: node linkType: hard -"isarray@npm:0.0.1": - version: 0.0.1 - resolution: "isarray@npm:0.0.1" - checksum: 49191f1425681df4a18c2f0f93db3adb85573bcdd6a4482539d98eac9e705d8961317b01175627e860516a2fc45f8f9302db26e5a380a97a520e272e2a40a8d4 - languageName: node - linkType: hard - "isarray@npm:^2.0.5": version: 2.0.5 resolution: "isarray@npm:2.0.5" @@ -6587,7 +6535,7 @@ __metadata: languageName: node linkType: hard -"loose-envify@npm:^1.1.0, loose-envify@npm:^1.2.0, loose-envify@npm:^1.3.1, loose-envify@npm:^1.4.0": +"loose-envify@npm:^1.1.0, loose-envify@npm:^1.4.0": version: 1.4.0 resolution: "loose-envify@npm:1.4.0" dependencies: @@ -7338,15 +7286,6 @@ __metadata: languageName: node linkType: hard -"path-to-regexp@npm:^1.7.0": - version: 1.8.0 - resolution: "path-to-regexp@npm:1.8.0" - dependencies: - isarray: 0.0.1 - checksum: 709f6f083c0552514ef4780cb2e7e4cf49b0cc89a97439f2b7cc69a608982b7690fb5d1720a7473a59806508fc2dae0be751ba49f495ecf89fd8fbc62abccbcd - languageName: node - linkType: hard - "path-to-regexp@npm:~0.1.12": version: 0.1.12 resolution: "path-to-regexp@npm:0.1.12" @@ -7636,7 +7575,7 @@ __metadata: languageName: node linkType: hard -"react-is@npm:^16.13.1, react-is@npm:^16.6.0, react-is@npm:^16.7.0": +"react-is@npm:^16.13.1, react-is@npm:^16.7.0": version: 16.13.1 resolution: "react-is@npm:16.13.1" checksum: f7a19ac3496de32ca9ae12aa030f00f14a3d45374f1ceca0af707c831b2a6098ef0d6bdae51bd437b0a306d7f01d4677fcc8de7c0d331eb47ad0f46130e53c5f @@ -7703,39 +7642,19 @@ __metadata: languageName: node linkType: hard -"react-router-dom@npm:5.3.4": - version: 5.3.4 - resolution: "react-router-dom@npm:5.3.4" - dependencies: - "@babel/runtime": ^7.12.13 - history: ^4.9.0 - loose-envify: ^1.3.1 - prop-types: ^15.6.2 - react-router: 5.3.4 - tiny-invariant: ^1.0.2 - tiny-warning: ^1.0.0 - peerDependencies: - react: ">=15" - checksum: b86a6f2f5222f041e38adf4e4b32c7643d6735a1a915ef25855b2db285fd059d72ba8d62e5bcd5d822b8ef9520a80453209e55077f5a90d0f72e908979b8f535 - languageName: node - linkType: hard - -"react-router@npm:5.3.4": - version: 5.3.4 - resolution: "react-router@npm:5.3.4" +"react-router@npm:7.13.2": + version: 7.13.2 + resolution: "react-router@npm:7.13.2" dependencies: - "@babel/runtime": ^7.12.13 - history: ^4.9.0 - hoist-non-react-statics: ^3.1.0 - loose-envify: ^1.3.1 - path-to-regexp: ^1.7.0 - prop-types: ^15.6.2 - react-is: ^16.6.0 - tiny-invariant: ^1.0.2 - tiny-warning: ^1.0.0 + cookie: ^1.0.1 + set-cookie-parser: ^2.6.0 peerDependencies: - react: ">=15" - checksum: 892d4e274a23bf4f39abc2efca54472fb646d3aed4b584020cf49654d2f50d09a2bacebe7c92b4ec7cb8925077376dfcd0664bad6442a73604397cefec9f01f9 + react: ">=18" + react-dom: ">=18" + peerDependenciesMeta: + react-dom: + optional: true + checksum: 84e704693900afca4933641cd86110a72305b18a2eb922b2d367c7f6f733c177af2ef1c9e4b291f235c34462250ea20da045f75146dd28d0abe6675d5fac3e3b languageName: node linkType: hard @@ -7918,13 +7837,6 @@ __metadata: languageName: node linkType: hard -"resolve-pathname@npm:^3.0.0": - version: 3.0.0 - resolution: "resolve-pathname@npm:3.0.0" - checksum: 6147241ba42c423dbe83cb067a2b4af4f60908c3af57e1ea567729cc71416c089737fe2a73e9e79e7a60f00f66c91e4b45ad0d37cd4be2d43fec44963ef14368 - languageName: node - linkType: hard - "resolve@npm:^1.19.0": version: 1.22.1 resolution: "resolve@npm:1.22.1" @@ -8275,6 +8187,13 @@ __metadata: languageName: node linkType: hard +"set-cookie-parser@npm:^2.6.0": + version: 2.7.2 + resolution: "set-cookie-parser@npm:2.7.2" + checksum: 9e1b09e7184079c81f9ba4d2db3222854adf4e6e4fe73982388367649386a93e7f9b979333ddeba22610706def93c2478f34c3324fe223528cb2c4c879a2c2d3 + languageName: node + linkType: hard + "set-function-length@npm:^1.2.2": version: 1.2.2 resolution: "set-function-length@npm:1.2.2" @@ -8872,20 +8791,6 @@ __metadata: languageName: node linkType: hard -"tiny-invariant@npm:^1.0.2": - version: 1.3.1 - resolution: "tiny-invariant@npm:1.3.1" - checksum: 872dbd1ff20a21303a2fd20ce3a15602cfa7fcf9b228bd694a52e2938224313b5385a1078cb667ed7375d1612194feaca81c4ecbe93121ca1baebe344de4f84c - languageName: node - linkType: hard - -"tiny-warning@npm:^1.0.0": - version: 1.0.3 - resolution: "tiny-warning@npm:1.0.3" - checksum: da62c4acac565902f0624b123eed6dd3509bc9a8d30c06e017104bedcf5d35810da8ff72864400ad19c5c7806fc0a8323c68baf3e326af7cb7d969f846100d71 - languageName: node - linkType: hard - "tinybench@npm:^2.9.0": version: 2.9.0 resolution: "tinybench@npm:2.9.0" @@ -9277,13 +9182,6 @@ __metadata: languageName: node linkType: hard -"value-equal@npm:^1.0.1": - version: 1.0.1 - resolution: "value-equal@npm:1.0.1" - checksum: bb7ae1facc76b5cf8071aeb6c13d284d023fdb370478d10a5d64508e0e6e53bb459c4bbe34258df29d82e6f561f874f0105eba38de0e61fe9edd0bdce07a77a2 - languageName: node - linkType: hard - "vary@npm:~1.1.2": version: 1.1.2 resolution: "vary@npm:1.1.2" @@ -9484,6 +9382,62 @@ __metadata: languageName: node linkType: hard +"vitest@patch:vitest@npm%3A3.2.3#./.yarn/patches/vitest-npm-3.2.3-d0d609a9f8.patch::locator=datagateway%40workspace%3A.": + version: 3.2.3 + resolution: "vitest@patch:vitest@npm%3A3.2.3#./.yarn/patches/vitest-npm-3.2.3-d0d609a9f8.patch::version=3.2.3&hash=48a75e&locator=datagateway%40workspace%3A." + dependencies: + "@types/chai": ^5.2.2 + "@vitest/expect": 3.2.3 + "@vitest/mocker": 3.2.3 + "@vitest/pretty-format": ^3.2.3 + "@vitest/runner": 3.2.3 + "@vitest/snapshot": 3.2.3 + "@vitest/spy": 3.2.3 + "@vitest/utils": 3.2.3 + chai: ^5.2.0 + debug: ^4.4.1 + expect-type: ^1.2.1 + magic-string: ^0.30.17 + pathe: ^2.0.3 + picomatch: ^4.0.2 + std-env: ^3.9.0 + tinybench: ^2.9.0 + tinyexec: ^0.3.2 + tinyglobby: ^0.2.14 + tinypool: ^1.1.0 + tinyrainbow: ^2.0.0 + vite: ^5.0.0 || ^6.0.0 || ^7.0.0-0 + vite-node: 3.2.3 + why-is-node-running: ^2.3.0 + peerDependencies: + "@edge-runtime/vm": "*" + "@types/debug": ^4.1.12 + "@types/node": ^18.0.0 || ^20.0.0 || >=22.0.0 + "@vitest/browser": 3.2.3 + "@vitest/ui": 3.2.3 + happy-dom: "*" + jsdom: "*" + peerDependenciesMeta: + "@edge-runtime/vm": + optional: true + "@types/debug": + optional: true + "@types/node": + optional: true + "@vitest/browser": + optional: true + "@vitest/ui": + optional: true + happy-dom: + optional: true + jsdom: + optional: true + bin: + vitest: vitest.mjs + checksum: 36950504338b84764335e26f48a8239cf482f99c6b3dc95596c4f21bc64c4209175f3d60da9c8f08c91adcb05ba66feaac779a0f3727742f9219c4b6b985842b + languageName: node + linkType: hard + "void-elements@npm:3.1.0": version: 3.1.0 resolution: "void-elements@npm:3.1.0"