From 642118cf3b634bd193cdc288565e80b84725c146 Mon Sep 17 00:00:00 2001 From: Andy Davison Date: Fri, 4 Sep 2020 12:29:18 +0100 Subject: [PATCH 01/12] Add SWR, initial useDataset commit --- package-lock.json | 11 +++++++++-- package.json | 3 ++- src/hooks/useDataset/index.tsx | 30 ++++++++++++++++++++++++++++++ 3 files changed, 41 insertions(+), 3 deletions(-) create mode 100644 src/hooks/useDataset/index.tsx diff --git a/package-lock.json b/package-lock.json index 2ec59491..81c87e89 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10868,8 +10868,7 @@ "fast-deep-equal": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz", - "integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk=", - "dev": true + "integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk=" }, "fast-diff": { "version": "1.2.0", @@ -22775,6 +22774,14 @@ } } }, + "swr": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/swr/-/swr-0.3.2.tgz", + "integrity": "sha512-Bs5Bihq1hQ66O5bdKaL47iZ2nlAaBsd8tTLRLkw9stZeuBEfH7zSuQI95S2TpchL0ybsMq3isWwuso2uPvCfHA==", + "requires": { + "fast-deep-equal": "2.0.1" + } + }, "symbol-tree": { "version": "3.2.4", "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", diff --git a/package.json b/package.json index 0aa9f2ff..fc09cc82 100644 --- a/package.json +++ b/package.json @@ -88,6 +88,7 @@ "@inrupt/solid-client": "^0.2.0", "@inrupt/solid-client-authn-browser": "^0.1.1", "@types/react-table": "^7.0.22", - "react-table": "^7.5.0" + "react-table": "^7.5.0", + "swr": "^0.3.2" } } diff --git a/src/hooks/useDataset/index.tsx b/src/hooks/useDataset/index.tsx new file mode 100644 index 00000000..1ca4ee8b --- /dev/null +++ b/src/hooks/useDataset/index.tsx @@ -0,0 +1,30 @@ +/** + * Copyright 2020 Inrupt Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, + * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A + * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE + * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +import { useContext } from "react"; +import useSWR from "swr"; +import { fetchLitDataset } from "@inrupt/solid-client"; +import SessionContext from "../../context/sessionContext"; + +export default function useDataset(datasetIri: string) { + const { session } = useContext(SessionContext); + return useSWR(datasetIri, () => fetchLitDataset(datasetIri, session.fetch)); +} From e2959840dee2f596f4e7d7918b2bcfc325e67876 Mon Sep 17 00:00:00 2001 From: Andy Davison Date: Tue, 8 Sep 2020 17:17:45 +0100 Subject: [PATCH 02/12] Initial useDataset/useThing implementation --- src/hooks/useDataset/index.tsx | 20 +++++++++++++++--- src/hooks/useThing/index.tsx | 37 ++++++++++++++++++++++++++++++++++ 2 files changed, 54 insertions(+), 3 deletions(-) create mode 100644 src/hooks/useThing/index.tsx diff --git a/src/hooks/useDataset/index.tsx b/src/hooks/useDataset/index.tsx index 1ca4ee8b..a151df47 100644 --- a/src/hooks/useDataset/index.tsx +++ b/src/hooks/useDataset/index.tsx @@ -21,10 +21,24 @@ import { useContext } from "react"; import useSWR from "swr"; -import { fetchLitDataset } from "@inrupt/solid-client"; +import { + getSolidDataset, + SolidDataset, + WithResourceInfo, +} from "@inrupt/solid-client"; import SessionContext from "../../context/sessionContext"; -export default function useDataset(datasetIri: string) { +export default function useDataset( + datasetIri: string, + options?: any +): { dataset: (SolidDataset & WithResourceInfo) | undefined; error: any } { const { session } = useContext(SessionContext); - return useSWR(datasetIri, () => fetchLitDataset(datasetIri, session.fetch)); + const { data, error } = useSWR(datasetIri, () => { + const getSolidDatasetOptions = { + ...(session && { fetch: session.fetch }), + ...options, + }; + return getSolidDataset(datasetIri, getSolidDatasetOptions); + }); + return { dataset: data, error }; } diff --git a/src/hooks/useThing/index.tsx b/src/hooks/useThing/index.tsx new file mode 100644 index 00000000..54b73158 --- /dev/null +++ b/src/hooks/useThing/index.tsx @@ -0,0 +1,37 @@ +/** + * Copyright 2020 Inrupt Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, + * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A + * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE + * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +import { getThing, Thing } from "@inrupt/solid-client"; +import useDataset from "../useDataset"; + +export default function useThing( + datasetIri: string, + thingIri: string, + options?: any +): { thing: Thing | undefined; error: any } { + const { dataset, error } = useDataset(datasetIri, options); + if (!dataset) { + return { thing: undefined, error }; + } + + const thing = getThing(dataset, thingIri); + return { thing, error }; +} From 49acecfbd5a696eadea29c46893802d560106c61 Mon Sep 17 00:00:00 2001 From: Andy Davison Date: Wed, 9 Sep 2020 11:19:40 +0100 Subject: [PATCH 03/12] rename getSolidDatasetOptions to requestOptions --- src/hooks/useDataset/index.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/hooks/useDataset/index.tsx b/src/hooks/useDataset/index.tsx index a151df47..f19d1c59 100644 --- a/src/hooks/useDataset/index.tsx +++ b/src/hooks/useDataset/index.tsx @@ -34,11 +34,11 @@ export default function useDataset( ): { dataset: (SolidDataset & WithResourceInfo) | undefined; error: any } { const { session } = useContext(SessionContext); const { data, error } = useSWR(datasetIri, () => { - const getSolidDatasetOptions = { + const requestOptions = { ...(session && { fetch: session.fetch }), ...options, }; - return getSolidDataset(datasetIri, getSolidDatasetOptions); + return getSolidDataset(datasetIri, requestOptions); }); return { dataset: data, error }; } From 80e27aa1a3a3d5e4be6f8fc7b99bd555a4b78df7 Mon Sep 17 00:00:00 2001 From: Andy Davison Date: Wed, 9 Sep 2020 14:20:39 +0100 Subject: [PATCH 04/12] Switch fetch used in useDataset; remove check on fetch as always exists --- src/hooks/useDataset/index.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/hooks/useDataset/index.tsx b/src/hooks/useDataset/index.tsx index f19d1c59..91a4129f 100644 --- a/src/hooks/useDataset/index.tsx +++ b/src/hooks/useDataset/index.tsx @@ -32,10 +32,10 @@ export default function useDataset( datasetIri: string, options?: any ): { dataset: (SolidDataset & WithResourceInfo) | undefined; error: any } { - const { session } = useContext(SessionContext); + const { fetch } = useContext(SessionContext); const { data, error } = useSWR(datasetIri, () => { const requestOptions = { - ...(session && { fetch: session.fetch }), + fetch, ...options, }; return getSolidDataset(datasetIri, requestOptions); From a639f629efa5acfcc728e6acd30a8887adee776a Mon Sep 17 00:00:00 2001 From: Andy Davison Date: Wed, 9 Sep 2020 14:37:59 +0100 Subject: [PATCH 05/12] add useDataset tests --- src/hooks/useDataset/index.test.tsx | 117 ++++++++++++++++++++++++++++ 1 file changed, 117 insertions(+) create mode 100644 src/hooks/useDataset/index.test.tsx diff --git a/src/hooks/useDataset/index.test.tsx b/src/hooks/useDataset/index.test.tsx new file mode 100644 index 00000000..0f40ee41 --- /dev/null +++ b/src/hooks/useDataset/index.test.tsx @@ -0,0 +1,117 @@ +/** + * Copyright 2020 Inrupt Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, + * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A + * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE + * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +import * as React from "react"; +import { renderHook } from "@testing-library/react-hooks"; +import { waitFor } from "@testing-library/react"; +import * as SolidFns from "@inrupt/solid-client"; +import { Session } from "@inrupt/solid-client-authn-browser"; +import SessionContext from "../../context/sessionContext"; +import useDataset from "./index"; + +describe("useDataset() hook", () => { + const mockDatasetIri = "https://mock.url"; + const mockDataset = SolidFns.mockSolidDatasetFrom(mockDatasetIri); + const mockGetSolidDataset = jest + .spyOn(SolidFns, "getSolidDataset") + .mockResolvedValue(mockDataset); + const mockFetch = jest.fn(); + const wrapper = ({ children }: { children: React.ReactNode }) => ( + + {children} + + ); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it("should call getSolidDataset with given Iri", async () => { + const { result } = renderHook(() => useDataset(mockDatasetIri), { + wrapper, + }); + + expect(mockGetSolidDataset).toHaveBeenCalledTimes(1); + expect(mockGetSolidDataset).toHaveBeenCalledWith(mockDatasetIri, { + fetch: mockFetch, + }); + await waitFor(() => expect(result.current.dataset).toBe(mockDataset)); + }); + + it("should call getSolidDataset with given options", async () => { + const newMockFetch = jest.fn(); + const mockAdditionalOption = "additional option"; + const mockOptions = { + fetch: newMockFetch, + additionalOption: mockAdditionalOption, + }; + + const { result } = renderHook( + () => useDataset(mockDatasetIri, mockOptions), + { + wrapper, + } + ); + + expect(mockGetSolidDataset).toHaveBeenCalledTimes(1); + expect(mockGetSolidDataset).toHaveBeenCalledWith(mockDatasetIri, { + fetch: newMockFetch, + additionalOption: mockAdditionalOption, + }); + await waitFor(() => expect(result.current.dataset).toBe(mockDataset)); + }); + + // it("The hook should return values set in the SessionContext", async () => { + // interface IProps { + // children: React.ReactNode; + // } + + // const wrapper = ({ children }: IProps) => ( + // + // {children} + // + // ); + + // const { result } = renderHook(() => useDataset(), { wrapper }); + // expect(result.current.session.info.webId).toEqual( + // "https://solid.community/" + // ); + // expect(result.current.sessionRequestInProgress).toEqual(true); + // }); +}); From 7f4b55c11aee0fa0e45ac27f7d189aead04e063c Mon Sep 17 00:00:00 2001 From: Andy Davison Date: Mon, 14 Sep 2020 12:07:54 +0100 Subject: [PATCH 06/12] Fix caching in useDataset; make fresh request if supplied different options/fetch --- src/hooks/useDataset/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/hooks/useDataset/index.tsx b/src/hooks/useDataset/index.tsx index 91a4129f..e4601d76 100644 --- a/src/hooks/useDataset/index.tsx +++ b/src/hooks/useDataset/index.tsx @@ -33,7 +33,7 @@ export default function useDataset( options?: any ): { dataset: (SolidDataset & WithResourceInfo) | undefined; error: any } { const { fetch } = useContext(SessionContext); - const { data, error } = useSWR(datasetIri, () => { + const { data, error } = useSWR([datasetIri, options, fetch], () => { const requestOptions = { fetch, ...options, From 3d8c9f3841b4117f63d1b58294a5405355c57932 Mon Sep 17 00:00:00 2001 From: Andy Davison Date: Mon, 14 Sep 2020 12:08:37 +0100 Subject: [PATCH 07/12] Add remaining useDataset/useThing tests --- src/hooks/useDataset/index.test.tsx | 51 ++++++---------- src/hooks/useThing/index.test.tsx | 95 +++++++++++++++++++++++++++++ 2 files changed, 114 insertions(+), 32 deletions(-) create mode 100644 src/hooks/useThing/index.test.tsx diff --git a/src/hooks/useDataset/index.test.tsx b/src/hooks/useDataset/index.test.tsx index 0f40ee41..53adb74c 100644 --- a/src/hooks/useDataset/index.test.tsx +++ b/src/hooks/useDataset/index.test.tsx @@ -21,11 +21,11 @@ import * as React from "react"; import { renderHook } from "@testing-library/react-hooks"; -import { waitFor } from "@testing-library/react"; +import { SWRConfig, cache } from "swr"; import * as SolidFns from "@inrupt/solid-client"; import { Session } from "@inrupt/solid-client-authn-browser"; import SessionContext from "../../context/sessionContext"; -import useDataset from "./index"; +import useDataset from "."; describe("useDataset() hook", () => { const mockDatasetIri = "https://mock.url"; @@ -42,16 +42,17 @@ describe("useDataset() hook", () => { session: {} as Session, }} > - {children} + {children} ); afterEach(() => { jest.clearAllMocks(); + cache.clear(); }); it("should call getSolidDataset with given Iri", async () => { - const { result } = renderHook(() => useDataset(mockDatasetIri), { + const { result, waitFor } = renderHook(() => useDataset(mockDatasetIri), { wrapper, }); @@ -70,7 +71,7 @@ describe("useDataset() hook", () => { additionalOption: mockAdditionalOption, }; - const { result } = renderHook( + const { result, waitFor } = renderHook( () => useDataset(mockDatasetIri, mockOptions), { wrapper, @@ -85,33 +86,19 @@ describe("useDataset() hook", () => { await waitFor(() => expect(result.current.dataset).toBe(mockDataset)); }); - // it("The hook should return values set in the SessionContext", async () => { - // interface IProps { - // children: React.ReactNode; - // } + it("should return error if getSolidDataset call fails", async () => { + mockGetSolidDataset.mockRejectedValue(new Error("async error")); - // const wrapper = ({ children }: IProps) => ( - // - // {children} - // - // ); + const { result, waitFor } = renderHook(() => useDataset(mockDatasetIri), { + wrapper, + }); - // const { result } = renderHook(() => useDataset(), { wrapper }); - // expect(result.current.session.info.webId).toEqual( - // "https://solid.community/" - // ); - // expect(result.current.sessionRequestInProgress).toEqual(true); - // }); + expect(mockGetSolidDataset).toHaveBeenCalledTimes(1); + expect(mockGetSolidDataset).toHaveBeenCalledWith(mockDatasetIri, { + fetch: mockFetch, + }); + await waitFor(() => + expect(result.current.error.message).toBe("async error") + ); + }); }); diff --git a/src/hooks/useThing/index.test.tsx b/src/hooks/useThing/index.test.tsx new file mode 100644 index 00000000..0a28c015 --- /dev/null +++ b/src/hooks/useThing/index.test.tsx @@ -0,0 +1,95 @@ +/** + * Copyright 2020 Inrupt Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, + * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A + * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE + * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +import { renderHook } from "@testing-library/react-hooks"; +import * as SolidFns from "@inrupt/solid-client"; +import useDataset from "../useDataset"; +import useThing from "."; + +const mockDatasetIri = "https://mock.url"; +const mockThingIri = "https://mock.url#thing"; +const mockDataset = SolidFns.mockSolidDatasetFrom(mockDatasetIri); +const mockThing = SolidFns.mockThingFrom(mockThingIri); +const mockGetThing = jest + .spyOn(SolidFns, "getThing") + .mockReturnValue(mockThing); + +jest.mock("../useDataset"); + +describe("useThing() hook", () => { + afterEach(() => { + jest.clearAllMocks(); + }); + + it("should call useDataset with given dataset iri and options", async () => { + (useDataset as jest.Mock).mockReturnValue({ + dataset: mockDataset, + error: undefined, + }); + const mockOptions = { + additionalOption: "additional option", + }; + renderHook(() => useThing(mockDatasetIri, mockThingIri, mockOptions)); + + expect(useDataset).toHaveBeenCalledTimes(1); + expect(useDataset).toHaveBeenCalledWith(mockDatasetIri, mockOptions); + }); + + it("should call getThing with given thing iri, and return retrieved Thing", async () => { + (useDataset as jest.Mock).mockReturnValue({ + dataset: mockDataset, + error: undefined, + }); + const { result, waitFor } = renderHook(() => + useThing(mockDatasetIri, mockThingIri) + ); + + expect(mockGetThing).toHaveBeenCalledTimes(1); + await waitFor(() => expect(result.current.thing).toBe(mockThing)); + }); + + it("when dataset is undefined, should not call getThing, and return thing: undefined", async () => { + (useDataset as jest.Mock).mockReturnValue({ + dataset: undefined, + error: undefined, + }); + const { result, waitFor } = renderHook(() => + useThing(mockDatasetIri, mockThingIri) + ); + + expect(mockGetThing).toHaveBeenCalledTimes(0); + await waitFor(() => expect(result.current.thing).toBeUndefined()); + }); + + it("should return any error returned by useDataset", async () => { + (useDataset as jest.Mock).mockReturnValue({ + dataset: mockDataset, + error: new Error("useDataset error"), + }); + const { result, waitFor } = renderHook(() => + useThing(mockDatasetIri, mockThingIri) + ); + + await waitFor(() => + expect(result.current.error.message).toBe("useDataset error") + ); + }); +}); From 55d9186c815b6876f00b47e68e9a2ca8562bbd37 Mon Sep 17 00:00:00 2001 From: Andy Davison Date: Mon, 14 Sep 2020 13:22:11 +0100 Subject: [PATCH 08/12] Allow undefined datasetUri for conditional fetching --- src/hooks/useDataset/index.test.tsx | 10 ++++++++++ src/hooks/useDataset/index.tsx | 22 ++++++++++++++-------- src/hooks/useThing/index.tsx | 4 +++- 3 files changed, 27 insertions(+), 9 deletions(-) diff --git a/src/hooks/useDataset/index.test.tsx b/src/hooks/useDataset/index.test.tsx index 53adb74c..b93c5dfd 100644 --- a/src/hooks/useDataset/index.test.tsx +++ b/src/hooks/useDataset/index.test.tsx @@ -101,4 +101,14 @@ describe("useDataset() hook", () => { expect(result.current.error.message).toBe("async error") ); }); + + it("should return dataset undefined if uri is not defined", async () => { + const { result, waitFor } = renderHook(() => useDataset(null), { + wrapper, + }); + + expect(mockGetSolidDataset).toHaveBeenCalledTimes(0); + + await waitFor(() => expect(result.current.dataset).toBeUndefined()); + }); }); diff --git a/src/hooks/useDataset/index.tsx b/src/hooks/useDataset/index.tsx index e4601d76..a78a6c23 100644 --- a/src/hooks/useDataset/index.tsx +++ b/src/hooks/useDataset/index.tsx @@ -29,16 +29,22 @@ import { import SessionContext from "../../context/sessionContext"; export default function useDataset( - datasetIri: string, + datasetIri: string | null | undefined, + // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any options?: any + // eslint-disable-next-line @typescript-eslint/no-explicit-any ): { dataset: (SolidDataset & WithResourceInfo) | undefined; error: any } { const { fetch } = useContext(SessionContext); - const { data, error } = useSWR([datasetIri, options, fetch], () => { - const requestOptions = { - fetch, - ...options, - }; - return getSolidDataset(datasetIri, requestOptions); - }); + const { data, error } = useSWR( + datasetIri ? [datasetIri, options, fetch] : null, + () => { + const requestOptions = { + fetch, + ...options, + }; + // useSWR will only call this fetcher if datasetUri is defined + return getSolidDataset(datasetIri as string, requestOptions); + } + ); return { dataset: data, error }; } diff --git a/src/hooks/useThing/index.tsx b/src/hooks/useThing/index.tsx index 54b73158..d621edc2 100644 --- a/src/hooks/useThing/index.tsx +++ b/src/hooks/useThing/index.tsx @@ -23,9 +23,11 @@ import { getThing, Thing } from "@inrupt/solid-client"; import useDataset from "../useDataset"; export default function useThing( - datasetIri: string, + datasetIri: string | null | undefined, thingIri: string, + // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any options?: any + // eslint-disable-next-line @typescript-eslint/no-explicit-any ): { thing: Thing | undefined; error: any } { const { dataset, error } = useDataset(datasetIri, options); if (!dataset) { From f7f8a292464ade16ffba38bb886825886059ab1a Mon Sep 17 00:00:00 2001 From: Andy Davison Date: Mon, 14 Sep 2020 13:23:15 +0100 Subject: [PATCH 09/12] convert datasetContext to use useDataset --- src/context/datasetContext/index.test.tsx | 36 ++++++++++++---- src/context/datasetContext/index.tsx | 51 +++++------------------ 2 files changed, 37 insertions(+), 50 deletions(-) diff --git a/src/context/datasetContext/index.test.tsx b/src/context/datasetContext/index.test.tsx index 9918266e..ea0a7094 100644 --- a/src/context/datasetContext/index.test.tsx +++ b/src/context/datasetContext/index.test.tsx @@ -23,8 +23,11 @@ import * as React from "react"; import { RenderResult, render, waitFor } from "@testing-library/react"; import * as SolidFns from "@inrupt/solid-client"; +import useDataset from "../../hooks/useDataset"; import DatasetContext, { DatasetProvider } from "./index"; +jest.mock("../../hooks/useDataset"); + let documentBody: RenderResult; const mockUrl = "https://some-interesting-value.com"; @@ -119,6 +122,10 @@ function ExampleComponentWithDatasetUrl(): React.ReactElement { describe("Testing DatasetContext", () => { it("matches snapshot with Dataset provided", () => { + (useDataset as jest.Mock).mockReturnValue({ + dataset: undefined, + error: undefined, + }); documentBody = render( @@ -129,7 +136,10 @@ describe("Testing DatasetContext", () => { }); it("matches snapshot when fetching fails", async () => { - jest.spyOn(SolidFns, "fetchLitDataset").mockRejectedValue(null); + (useDataset as jest.Mock).mockReturnValue({ + dataset: undefined, + error: "Error", + }); documentBody = render( @@ -141,6 +151,10 @@ describe("Testing DatasetContext", () => { }); it("matches snapshot when fetching", async () => { + (useDataset as jest.Mock).mockReturnValue({ + dataset: undefined, + error: undefined, + }); documentBody = render( { }); describe("Functional testing", () => { - it("Should call fetchLitDataset", async () => { - jest - .spyOn(SolidFns, "fetchLitDataset") - .mockResolvedValue(mockDataSetWithResourceInfo); + it("Should call useDataset", async () => { + (useDataset as jest.Mock).mockReturnValue({ + dataset: mockDataSetWithResourceInfo, + error: undefined, + }); render( ); - expect(SolidFns.fetchLitDataset).toHaveBeenCalled(); + expect(useDataset).toHaveBeenCalled(); }); - it("When fetchLitDataset fails, should call onError if passed", async () => { + it("When useDataset return an error, should call onError if passed", async () => { + (useDataset as jest.Mock).mockReturnValue({ + dataset: undefined, + error: "Error", + }); const onError = jest.fn(); - (SolidFns.fetchLitDataset as jest.Mock).mockRejectedValue(null); render( { ); - await waitFor(() => expect(onError).toHaveBeenCalled()); + await waitFor(() => expect(onError).toHaveBeenCalledWith("Error")); }); }); diff --git a/src/context/datasetContext/index.tsx b/src/context/datasetContext/index.tsx index 848cc66d..9d52a913 100644 --- a/src/context/datasetContext/index.tsx +++ b/src/context/datasetContext/index.tsx @@ -19,22 +19,10 @@ * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -import React, { - createContext, - ReactElement, - useState, - useEffect, - useCallback, - useContext, -} from "react"; -import { - fetchLitDataset, - LitDataset, - WithResourceInfo, - UrlString, -} from "@inrupt/solid-client"; +import React, { createContext, ReactElement } from "react"; +import { LitDataset, WithResourceInfo, UrlString } from "@inrupt/solid-client"; -import SessionContext from "../sessionContext"; +import useDataset from "../../hooks/useDataset"; export interface IDatasetContext { dataset: LitDataset | (LitDataset & WithResourceInfo) | undefined; @@ -68,35 +56,16 @@ export const DatasetProvider = ({ datasetUrl, loading, }: RequireDatasetOrDatasetUrl): ReactElement => { - const { fetch } = useContext(SessionContext); - const [dataset, setDataset] = useState< - LitDataset | (LitDataset & WithResourceInfo) | undefined - >(propDataset); + const { dataset, error } = useDataset(datasetUrl); - const fetchDataset = useCallback( - async (url: string) => { - try { - const resource = await fetchLitDataset(url, { fetch }); - setDataset(resource); - } catch (error) { - if (onError) { - onError(error); - } - } - }, - [onError, fetch] - ); - - useEffect(() => { - if (!dataset && datasetUrl) { - // eslint-disable-next-line no-void - void fetchDataset(datasetUrl); - } - }, [dataset, datasetUrl, fetchDataset]); + if (error && onError) { + onError(error); + } + const datasetToUse = propDataset ?? dataset; return ( - - {dataset ? children : loading || Fetching data...} + + {datasetToUse ? children : loading || Fetching data...} ); }; From a0d28a666e0fe311497bd16441dd85741910024e Mon Sep 17 00:00:00 2001 From: Andy Davison Date: Mon, 14 Sep 2020 13:49:13 +0100 Subject: [PATCH 10/12] Bump version, add exports for useThing/useDataset --- CHANGELOG.md | 6 ++++++ package-lock.json | 2 +- package.json | 2 +- src/index.ts | 2 ++ 4 files changed, 10 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a98a4af2..7756aa60 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +## 0.11.0 ( September 14, 2020 ) + +### Added + +- Add useThing and useDataset hooks + ## 0.9.8 ( September 9, 2020 ) ### Added diff --git a/package-lock.json b/package-lock.json index 81c87e89..03b783b9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "@inrupt/solid-ui-react", - "version": "0.10.1", + "version": "0.11.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index fc09cc82..7a65df31 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@inrupt/solid-ui-react", - "version": "0.10.1", + "version": "0.11.0", "description": "Set of UI libraries using @solid/core", "main": "dist/index.js", "types": "dist/src/index.d.ts", diff --git a/src/index.ts b/src/index.ts index e5518463..e8ab7842 100644 --- a/src/index.ts +++ b/src/index.ts @@ -38,3 +38,5 @@ export { DatasetProvider, } from "./context/datasetContext"; export { default as useSession } from "./hooks/useSession"; +export { default as useDataset } from "./hooks/useDataset"; +export { default as useThing } from "./hooks/useThing"; From 6cb0128f74b07c8f6d34ade18bc1eb4313d37b0a Mon Sep 17 00:00:00 2001 From: Andy Davison Date: Mon, 14 Sep 2020 16:31:45 +0100 Subject: [PATCH 11/12] get dataset from context if datasetIri is not defined --- src/hooks/useDataset/index.test.tsx | 14 ++++++++++---- src/hooks/useDataset/index.tsx | 13 ++++++++++--- 2 files changed, 20 insertions(+), 7 deletions(-) diff --git a/src/hooks/useDataset/index.test.tsx b/src/hooks/useDataset/index.test.tsx index b93c5dfd..6e991cb1 100644 --- a/src/hooks/useDataset/index.test.tsx +++ b/src/hooks/useDataset/index.test.tsx @@ -24,12 +24,14 @@ import { renderHook } from "@testing-library/react-hooks"; import { SWRConfig, cache } from "swr"; import * as SolidFns from "@inrupt/solid-client"; import { Session } from "@inrupt/solid-client-authn-browser"; +import DatasetContext from "../../context/datasetContext"; import SessionContext from "../../context/sessionContext"; import useDataset from "."; describe("useDataset() hook", () => { const mockDatasetIri = "https://mock.url"; const mockDataset = SolidFns.mockSolidDatasetFrom(mockDatasetIri); + const mockContextDataset = SolidFns.mockSolidDatasetFrom(mockDatasetIri); const mockGetSolidDataset = jest .spyOn(SolidFns, "getSolidDataset") .mockResolvedValue(mockDataset); @@ -42,7 +44,9 @@ describe("useDataset() hook", () => { session: {} as Session, }} > - {children} + + {children} + ); @@ -102,13 +106,15 @@ describe("useDataset() hook", () => { ); }); - it("should return dataset undefined if uri is not defined", async () => { - const { result, waitFor } = renderHook(() => useDataset(null), { + it("should attempt to return dataset from context if uri is not defined", async () => { + const { result, waitFor } = renderHook(() => useDataset(), { wrapper, }); expect(mockGetSolidDataset).toHaveBeenCalledTimes(0); - await waitFor(() => expect(result.current.dataset).toBeUndefined()); + await waitFor(() => + expect(result.current.dataset).toBe(mockContextDataset) + ); }); }); diff --git a/src/hooks/useDataset/index.tsx b/src/hooks/useDataset/index.tsx index a78a6c23..bb159a1c 100644 --- a/src/hooks/useDataset/index.tsx +++ b/src/hooks/useDataset/index.tsx @@ -27,14 +27,19 @@ import { WithResourceInfo, } from "@inrupt/solid-client"; import SessionContext from "../../context/sessionContext"; +import DatasetContext from "../../context/datasetContext"; export default function useDataset( - datasetIri: string | null | undefined, + datasetIri?: string | null | undefined, // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any options?: any +): { + dataset: SolidDataset | (SolidDataset & WithResourceInfo) | undefined; // eslint-disable-next-line @typescript-eslint/no-explicit-any -): { dataset: (SolidDataset & WithResourceInfo) | undefined; error: any } { + error: any; +} { const { fetch } = useContext(SessionContext); + const { dataset: datasetFromContext } = useContext(DatasetContext); const { data, error } = useSWR( datasetIri ? [datasetIri, options, fetch] : null, () => { @@ -46,5 +51,7 @@ export default function useDataset( return getSolidDataset(datasetIri as string, requestOptions); } ); - return { dataset: data, error }; + + const dataset = datasetIri ? data : datasetFromContext; + return { dataset, error }; } From 3f0f97ab8b0ef1129870cf34ee11028f86a2a55b Mon Sep 17 00:00:00 2001 From: Andy Davison Date: Mon, 14 Sep 2020 16:33:31 +0100 Subject: [PATCH 12/12] get Thing from context if thingIri is not defined --- src/hooks/useThing/index.test.tsx | 22 ++++++++++++++++++++++ src/hooks/useThing/index.tsx | 10 ++++++++-- 2 files changed, 30 insertions(+), 2 deletions(-) diff --git a/src/hooks/useThing/index.test.tsx b/src/hooks/useThing/index.test.tsx index 0a28c015..ee24c208 100644 --- a/src/hooks/useThing/index.test.tsx +++ b/src/hooks/useThing/index.test.tsx @@ -19,15 +19,18 @@ * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +import React from "react"; import { renderHook } from "@testing-library/react-hooks"; import * as SolidFns from "@inrupt/solid-client"; import useDataset from "../useDataset"; +import ThingContext from "../../context/thingContext"; import useThing from "."; const mockDatasetIri = "https://mock.url"; const mockThingIri = "https://mock.url#thing"; const mockDataset = SolidFns.mockSolidDatasetFrom(mockDatasetIri); const mockThing = SolidFns.mockThingFrom(mockThingIri); +const mockContextThing = SolidFns.mockThingFrom(mockThingIri); const mockGetThing = jest .spyOn(SolidFns, "getThing") .mockReturnValue(mockThing); @@ -92,4 +95,23 @@ describe("useThing() hook", () => { expect(result.current.error.message).toBe("useDataset error") ); }); + + it("should attempt to return thing from context if thing uri is not defined", async () => { + const wrapper = ({ children }: { children: React.ReactNode }) => ( + + {children} + + ); + (useDataset as jest.Mock).mockReturnValue({ + dataset: mockDataset, + error: undefined, + }); + const { result, waitFor } = renderHook( + () => useThing(mockDatasetIri, undefined), + { wrapper } + ); + + expect(mockGetThing).not.toHaveBeenCalled(); + await waitFor(() => expect(result.current.thing).toBe(mockContextThing)); + }); }); diff --git a/src/hooks/useThing/index.tsx b/src/hooks/useThing/index.tsx index d621edc2..60770e2b 100644 --- a/src/hooks/useThing/index.tsx +++ b/src/hooks/useThing/index.tsx @@ -20,16 +20,22 @@ */ import { getThing, Thing } from "@inrupt/solid-client"; +import { useContext } from "react"; +import ThingContext from "../../context/thingContext"; import useDataset from "../useDataset"; export default function useThing( - datasetIri: string | null | undefined, - thingIri: string, + datasetIri?: string | null | undefined, + thingIri?: string | null | undefined, // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any options?: any // eslint-disable-next-line @typescript-eslint/no-explicit-any ): { thing: Thing | undefined; error: any } { const { dataset, error } = useDataset(datasetIri, options); + const { thing: thingFromContext } = useContext(ThingContext); + if (!thingIri) { + return { thing: thingFromContext, error }; + } if (!dataset) { return { thing: undefined, error }; }