From 1237625c7c89c4babec2897b2fc4db057a1a257f Mon Sep 17 00:00:00 2001 From: jaxoncreed Date: Sun, 4 Feb 2024 17:20:09 -0500 Subject: [PATCH 1/4] Before risky request refactor --- packages/solid-react/package.json | 2 +- packages/solid-react/src/useResource.ts | 4 ++ .../solid-react/test/Integration.test.tsx | 63 ++++++++++++++++++- packages/solid-react/test/setUpServer.ts | 1 - .../solid/src/requester/BatchedRequester.ts | 1 + packages/solid/src/resource/Resource.ts | 2 + packages/solid/src/util/RequestBatcher.ts | 15 +++++ packages/solid/test/Integration.test.ts | 2 +- 8 files changed, 85 insertions(+), 5 deletions(-) diff --git a/packages/solid-react/package.json b/packages/solid-react/package.json index d20dfd7..dee8e00 100644 --- a/packages/solid-react/package.json +++ b/packages/solid-react/package.json @@ -13,7 +13,7 @@ "lint": "eslint src/** --fix --no-error-on-unmatched-pattern", "test:integration": "start-server-and-test start-test-server http://localhost:3001 start-integration-test", "start-test-server": "ts-node ./test/test-server/runServer.ts", - "start-integration-test": "jest --coverage" + "start-integration-test": "jest --coverage -t \"Reloads the data on mount\"" }, "repository": { "type": "git", diff --git a/packages/solid-react/src/useResource.ts b/packages/solid-react/src/useResource.ts index 38e0bd5..57a0b8c 100644 --- a/packages/solid-react/src/useResource.ts +++ b/packages/solid-react/src/useResource.ts @@ -42,12 +42,15 @@ export function useResource( // Get the resource const resource = useMemo(() => { + console.log(uri); if (uri) { const resource = getResource(uri); // Run read operations if necissary if (!options?.suppressInitialRead) { if (options?.reloadOnMount) { + console.log("Reading again"); resource.read(); + console.log(resource.isLoading()); } else { resource.readIfUnfetched(); } @@ -70,6 +73,7 @@ export function useResource( }, [resource], ); + useEffect(() => { // Remove listeners for the previous resource if (pastResource.current?.resource) { diff --git a/packages/solid-react/test/Integration.test.tsx b/packages/solid-react/test/Integration.test.tsx index 188f0d0..4b0d4fa 100644 --- a/packages/solid-react/test/Integration.test.tsx +++ b/packages/solid-react/test/Integration.test.tsx @@ -1,6 +1,6 @@ -import React from "react"; +import React, { useEffect, useState } from "react"; import type { FunctionComponent } from "react"; -import { render, screen } from "@testing-library/react"; +import { render, screen, fireEvent } from "@testing-library/react"; import { SAMPLE_DATA_URI, setUpServer } from "./setUpServer"; import { UnauthenticatedSolidLdoProvider } from "../src/UnauthenticatedSolidLdoProvider"; import { useResource } from "../src/useResource"; @@ -32,5 +32,64 @@ describe("Integration Tests", () => { const resourceStatus = await screen.findByRole("status"); expect(resourceStatus.innerHTML).toBe("dataReadSuccess"); }); + + it("returns undefined when no uri is provided, then rerenders when one is", async () => { + const UseResourceUndefinedTest: FunctionComponent = () => { + const [uri, setUri] = useState(undefined); + const resource = useResource(uri, { suppressInitialRead: true }); + if (!resource) + return ( +
+

Undefined

+ +
+ ); + return

{resource.status.type}

; + }; + render( + + + , + ); + await screen.findByText("Undefined"); + fireEvent.click(screen.getByText("Next")); + const resourceStatus = await screen.findByRole("status"); + expect(resourceStatus.innerHTML).toBe("unfetched"); + }); + }); + + it("Reloads the data on mount", async () => { + const ReloadTest: FunctionComponent = () => { + const resource = useResource(SAMPLE_DATA_URI, { reloadOnMount: true }); + if (resource?.isLoading()) return

Loading

; + return

{resource.status.type}

; + }; + const ReloadParent: FunctionComponent = () => { + const [showComponent, setShowComponent] = useState(true); + return ( +
+ + {showComponent ? :

Hidden

} +
+ ); + }; + render( + + + , + ); + await screen.findByText("Loading"); + const resourceStatus1 = await screen.findByRole("status"); + expect(resourceStatus1.innerHTML).toBe("dataReadSuccess"); + console.log("=============="); + fireEvent.click(screen.getByText("Show Component")); + await screen.findByText("Hidden"); + console.log("++++++++++++++"); + fireEvent.click(screen.getByText("Show Component")); + await screen.findByText("Loading"); + const resourceStatus2 = await screen.findByRole("status"); + expect(resourceStatus2.innerHTML).toBe("dataReadSuccess"); }); }); diff --git a/packages/solid-react/test/setUpServer.ts b/packages/solid-react/test/setUpServer.ts index a669c01..a3e4f4a 100644 --- a/packages/solid-react/test/setUpServer.ts +++ b/packages/solid-react/test/setUpServer.ts @@ -80,7 +80,6 @@ export function setUpServer(): SetUpServerReturn { slug: TEST_CONTAINER_SLUG, }, }); - console.log("Created container", result.status); await Promise.all([ s.authFetch(TEST_CONTAINER_URI, { method: "POST", diff --git a/packages/solid/src/requester/BatchedRequester.ts b/packages/solid/src/requester/BatchedRequester.ts index 7c0b5a8..9df0077 100644 --- a/packages/solid/src/requester/BatchedRequester.ts +++ b/packages/solid/src/requester/BatchedRequester.ts @@ -55,6 +55,7 @@ export abstract class BatchedRequester { * @returns true if the resource is making any requests */ isLoading(): boolean { + console.log("In isLoading"); return this.requestBatcher.isLoading(ANY_KEY); } diff --git a/packages/solid/src/resource/Resource.ts b/packages/solid/src/resource/Resource.ts index e03f91c..612bbeb 100644 --- a/packages/solid/src/resource/Resource.ts +++ b/packages/solid/src/resource/Resource.ts @@ -325,7 +325,9 @@ export abstract class Resource extends (EventEmitter as new () => TypedEmitter<{ * @returns ReadResult */ protected async handleRead(): Promise { + console.log("Handle read"); const result = await this.requester.read(); + console.log("End Handle Read"); this.status = result; if (result.isError) return result; this.updateWithReadSuccess(result); diff --git a/packages/solid/src/util/RequestBatcher.ts b/packages/solid/src/util/RequestBatcher.ts index cb78d66..75bbf84 100644 --- a/packages/solid/src/util/RequestBatcher.ts +++ b/packages/solid/src/util/RequestBatcher.ts @@ -89,6 +89,9 @@ export class RequestBatcher { * @returns true if the batcher is currently working on the provided process */ public isLoading(key: string): boolean { + console.log("In Is loadin:", key); + console.log("CurrentlyProcessing:", this.currentlyProcessing); + if (key === ANY_KEY) return !!this.currentlyProcessing; return this.currentlyProcessing?.name === key; } @@ -113,22 +116,31 @@ export class RequestBatcher { const timeSinceLastTrigger = Date.now() - lastRequestTimestamp; const triggerProcess = async () => { + console.log("Triggering process"); + // Don't run the process if something is currently processing. + // "triggerProcess" will be called again because this item is still in the + // queue if (this.currentlyProcessing) { return; } this.lastRequestTimestampMap[processName] = Date.now(); this.lastRequestTimestampMap[ANY_KEY] = Date.now(); + // Remove the process from the queue const processToTrigger = this.processQueue.shift(); if (processToTrigger) { this.currentlyProcessing = processToTrigger; try { + console.log("Before process trigger"); + console.log("The current:", this.currentlyProcessing); const returnValue = await processToTrigger.perform( ...processToTrigger.args, ); + console.log("After process trigger"); if (processToTrigger.after) { processToTrigger.after(returnValue); } processToTrigger.awaitingResolutions.forEach((callback) => { + console.log("Resolved"); callback(returnValue); }); } catch (err) { @@ -143,8 +155,10 @@ export class RequestBatcher { }; if (timeSinceLastTrigger < this.batchMillis) { + console.log("Waiting for the future"); setTimeout(triggerProcess, this.batchMillis - timeSinceLastTrigger); } else { + console.log("Doing it now"); triggerProcess(); } } @@ -158,6 +172,7 @@ export class RequestBatcher { options: WaitingProcessOptions, ): Promise { return new Promise((resolve, reject) => { + console.log("Queuing process"); const shouldAwait = options.modifyQueue( this.processQueue, this.currentlyProcessing, diff --git a/packages/solid/test/Integration.test.ts b/packages/solid/test/Integration.test.ts index f06b924..7ee73a1 100644 --- a/packages/solid/test/Integration.test.ts +++ b/packages/solid/test/Integration.test.ts @@ -134,7 +134,7 @@ async function testRequestLoads( return returnVal; } -describe("SolidLdoDataset", () => { +describe("Integration", () => { let app: App; let authFetch: typeof fetch; let fetchMock: jest.Mock< From 787c50e330ae2b849939768282080fb621d1dbe0 Mon Sep 17 00:00:00 2001 From: jaxoncreed Date: Sun, 4 Feb 2024 17:55:12 -0500 Subject: [PATCH 2/4] Completed useResource tests --- packages/solid-react/package.json | 2 +- packages/solid-react/src/useResource.ts | 3 - .../solid-react/test/Integration.test.tsx | 94 ++++++++++++------- .../solid/src/requester/BatchedRequester.ts | 1 - packages/solid/src/resource/Resource.ts | 2 - packages/solid/src/util/RequestBatcher.ts | 24 +---- 6 files changed, 65 insertions(+), 61 deletions(-) diff --git a/packages/solid-react/package.json b/packages/solid-react/package.json index dee8e00..d20dfd7 100644 --- a/packages/solid-react/package.json +++ b/packages/solid-react/package.json @@ -13,7 +13,7 @@ "lint": "eslint src/** --fix --no-error-on-unmatched-pattern", "test:integration": "start-server-and-test start-test-server http://localhost:3001 start-integration-test", "start-test-server": "ts-node ./test/test-server/runServer.ts", - "start-integration-test": "jest --coverage -t \"Reloads the data on mount\"" + "start-integration-test": "jest --coverage" }, "repository": { "type": "git", diff --git a/packages/solid-react/src/useResource.ts b/packages/solid-react/src/useResource.ts index 57a0b8c..0747118 100644 --- a/packages/solid-react/src/useResource.ts +++ b/packages/solid-react/src/useResource.ts @@ -42,15 +42,12 @@ export function useResource( // Get the resource const resource = useMemo(() => { - console.log(uri); if (uri) { const resource = getResource(uri); // Run read operations if necissary if (!options?.suppressInitialRead) { if (options?.reloadOnMount) { - console.log("Reading again"); resource.read(); - console.log(resource.isLoading()); } else { resource.readIfUnfetched(); } diff --git a/packages/solid-react/test/Integration.test.tsx b/packages/solid-react/test/Integration.test.tsx index 4b0d4fa..f842781 100644 --- a/packages/solid-react/test/Integration.test.tsx +++ b/packages/solid-react/test/Integration.test.tsx @@ -1,7 +1,7 @@ import React, { useEffect, useState } from "react"; import type { FunctionComponent } from "react"; import { render, screen, fireEvent } from "@testing-library/react"; -import { SAMPLE_DATA_URI, setUpServer } from "./setUpServer"; +import { SAMPLE_BINARY_URI, SAMPLE_DATA_URI, setUpServer } from "./setUpServer"; import { UnauthenticatedSolidLdoProvider } from "../src/UnauthenticatedSolidLdoProvider"; import { useResource } from "../src/useResource"; @@ -56,40 +56,66 @@ describe("Integration Tests", () => { const resourceStatus = await screen.findByRole("status"); expect(resourceStatus.innerHTML).toBe("unfetched"); }); - }); - it("Reloads the data on mount", async () => { - const ReloadTest: FunctionComponent = () => { - const resource = useResource(SAMPLE_DATA_URI, { reloadOnMount: true }); - if (resource?.isLoading()) return

Loading

; - return

{resource.status.type}

; - }; - const ReloadParent: FunctionComponent = () => { - const [showComponent, setShowComponent] = useState(true); - return ( -
- - {showComponent ? :

Hidden

} -
+ it("Reloads the data on mount", async () => { + const ReloadTest: FunctionComponent = () => { + const resource = useResource(SAMPLE_DATA_URI, { reloadOnMount: true }); + if (resource?.isLoading()) return

Loading

; + return

{resource.status.type}

; + }; + const ReloadParent: FunctionComponent = () => { + const [showComponent, setShowComponent] = useState(true); + return ( +
+ + {showComponent ? :

Hidden

} +
+ ); + }; + render( + + + , ); - }; - render( - - - , - ); - await screen.findByText("Loading"); - const resourceStatus1 = await screen.findByRole("status"); - expect(resourceStatus1.innerHTML).toBe("dataReadSuccess"); - console.log("=============="); - fireEvent.click(screen.getByText("Show Component")); - await screen.findByText("Hidden"); - console.log("++++++++++++++"); - fireEvent.click(screen.getByText("Show Component")); - await screen.findByText("Loading"); - const resourceStatus2 = await screen.findByRole("status"); - expect(resourceStatus2.innerHTML).toBe("dataReadSuccess"); + await screen.findByText("Loading"); + const resourceStatus1 = await screen.findByRole("status"); + expect(resourceStatus1.innerHTML).toBe("dataReadSuccess"); + fireEvent.click(screen.getByText("Show Component")); + await screen.findByText("Hidden"); + fireEvent.click(screen.getByText("Show Component")); + await screen.findByText("Loading"); + const resourceStatus2 = await screen.findByRole("status"); + expect(resourceStatus2.innerHTML).toBe("dataReadSuccess"); + }); + + it("handles swapping to a new resource", async () => { + const SwapResourceTest: FunctionComponent = () => { + const [uri, setUri] = useState(SAMPLE_DATA_URI); + const resource = useResource(uri); + if (resource?.isLoading()) return

Loading

; + return ( +
+

{resource.status.type}

+ +
+ ); + }; + render( + + + , + ); + await screen.findByText("Loading"); + const resourceStatus1 = await screen.findByRole("status"); + expect(resourceStatus1.innerHTML).toBe("dataReadSuccess"); + fireEvent.click(screen.getByText("Update URI")); + await screen.findByText("Loading"); + const resourceStatus2 = await screen.findByRole("status"); + expect(resourceStatus2.innerHTML).toBe("binaryReadSuccess"); + }); }); }); diff --git a/packages/solid/src/requester/BatchedRequester.ts b/packages/solid/src/requester/BatchedRequester.ts index 9df0077..7c0b5a8 100644 --- a/packages/solid/src/requester/BatchedRequester.ts +++ b/packages/solid/src/requester/BatchedRequester.ts @@ -55,7 +55,6 @@ export abstract class BatchedRequester { * @returns true if the resource is making any requests */ isLoading(): boolean { - console.log("In isLoading"); return this.requestBatcher.isLoading(ANY_KEY); } diff --git a/packages/solid/src/resource/Resource.ts b/packages/solid/src/resource/Resource.ts index 612bbeb..e03f91c 100644 --- a/packages/solid/src/resource/Resource.ts +++ b/packages/solid/src/resource/Resource.ts @@ -325,9 +325,7 @@ export abstract class Resource extends (EventEmitter as new () => TypedEmitter<{ * @returns ReadResult */ protected async handleRead(): Promise { - console.log("Handle read"); const result = await this.requester.read(); - console.log("End Handle Read"); this.status = result; if (result.isError) return result; this.updateWithReadSuccess(result); diff --git a/packages/solid/src/util/RequestBatcher.ts b/packages/solid/src/util/RequestBatcher.ts index 75bbf84..4528ec1 100644 --- a/packages/solid/src/util/RequestBatcher.ts +++ b/packages/solid/src/util/RequestBatcher.ts @@ -89,9 +89,6 @@ export class RequestBatcher { * @returns true if the batcher is currently working on the provided process */ public isLoading(key: string): boolean { - console.log("In Is loadin:", key); - console.log("CurrentlyProcessing:", this.currentlyProcessing); - if (key === ANY_KEY) return !!this.currentlyProcessing; return this.currentlyProcessing?.name === key; } @@ -102,10 +99,11 @@ export class RequestBatcher { * the last process was triggered. */ private triggerOrWaitProcess() { - if (!this.processQueue[0]) { + if (!this.processQueue[0] || this.currentlyProcessing) { return; } - const processName = this.processQueue[0].name; + this.currentlyProcessing = this.processQueue.shift(); + const processName = this.currentlyProcessing!.name; // Set last request timestamp if not available if (!this.lastRequestTimestampMap[processName]) { @@ -116,31 +114,20 @@ export class RequestBatcher { const timeSinceLastTrigger = Date.now() - lastRequestTimestamp; const triggerProcess = async () => { - console.log("Triggering process"); - // Don't run the process if something is currently processing. - // "triggerProcess" will be called again because this item is still in the - // queue - if (this.currentlyProcessing) { - return; - } this.lastRequestTimestampMap[processName] = Date.now(); this.lastRequestTimestampMap[ANY_KEY] = Date.now(); // Remove the process from the queue - const processToTrigger = this.processQueue.shift(); + const processToTrigger = this.currentlyProcessing; if (processToTrigger) { this.currentlyProcessing = processToTrigger; try { - console.log("Before process trigger"); - console.log("The current:", this.currentlyProcessing); const returnValue = await processToTrigger.perform( ...processToTrigger.args, ); - console.log("After process trigger"); if (processToTrigger.after) { processToTrigger.after(returnValue); } processToTrigger.awaitingResolutions.forEach((callback) => { - console.log("Resolved"); callback(returnValue); }); } catch (err) { @@ -155,10 +142,8 @@ export class RequestBatcher { }; if (timeSinceLastTrigger < this.batchMillis) { - console.log("Waiting for the future"); setTimeout(triggerProcess, this.batchMillis - timeSinceLastTrigger); } else { - console.log("Doing it now"); triggerProcess(); } } @@ -172,7 +157,6 @@ export class RequestBatcher { options: WaitingProcessOptions, ): Promise { return new Promise((resolve, reject) => { - console.log("Queuing process"); const shouldAwait = options.modifyQueue( this.processQueue, this.currentlyProcessing, From 99c02bdae2199d08d67e967ee621f7cab8f63324 Mon Sep 17 00:00:00 2001 From: jaxoncreed Date: Sun, 4 Feb 2024 21:40:35 -0500 Subject: [PATCH 3/4] All tests except useSubject --- .../src/UnauthenticatedSolidLdoProvider.tsx | 1 + packages/solid-react/src/useLdoMethods.ts | 13 +- packages/solid-react/src/useRootContainer.ts | 11 +- .../solid-react/test/.ldo/post.context.ts | 31 ++++ packages/solid-react/test/.ldo/post.schema.ts | 155 ++++++++++++++++++ .../solid-react/test/.ldo/post.shapeTypes.ts | 19 +++ .../solid-react/test/.ldo/post.typings.ts | 45 +++++ .../solid-react/test/Integration.test.tsx | 101 +++++++++++- packages/solid-react/test/setUpServer.ts | 2 +- .../test/useSubject.integration.test.tsx | 9 + 10 files changed, 379 insertions(+), 8 deletions(-) create mode 100644 packages/solid-react/test/.ldo/post.context.ts create mode 100644 packages/solid-react/test/.ldo/post.schema.ts create mode 100644 packages/solid-react/test/.ldo/post.shapeTypes.ts create mode 100644 packages/solid-react/test/.ldo/post.typings.ts create mode 100644 packages/solid-react/test/useSubject.integration.test.tsx diff --git a/packages/solid-react/src/UnauthenticatedSolidLdoProvider.tsx b/packages/solid-react/src/UnauthenticatedSolidLdoProvider.tsx index 3ff6f99..8f25de1 100644 --- a/packages/solid-react/src/UnauthenticatedSolidLdoProvider.tsx +++ b/packages/solid-react/src/UnauthenticatedSolidLdoProvider.tsx @@ -1,3 +1,4 @@ +/* istanbul ignore file */ import React, { useCallback, useMemo } from "react"; import type { FunctionComponent, PropsWithChildren } from "react"; import type { LoginOptions, SessionInfo } from "./SolidAuthContext"; diff --git a/packages/solid-react/src/useLdoMethods.ts b/packages/solid-react/src/useLdoMethods.ts index ae032ef..d6add4e 100644 --- a/packages/solid-react/src/useLdoMethods.ts +++ b/packages/solid-react/src/useLdoMethods.ts @@ -9,13 +9,18 @@ export interface UseLdoMethods { getSubject( shapeType: ShapeType, subject: string | SubjectNode, - ): Type | Error; + ): Type; createData( shapeType: ShapeType, subject: string | SubjectNode, - ...resources: Resource[] + resource: Resource, + ...additionalResources: Resource[] + ): Type; + changeData( + input: Type, + resource: Resource, + ...additionalResources: Resource[] ): Type; - changeData(input: Type, ...resources: Resource[]): Type; commitData(input: LdoBase): ReturnType; } @@ -35,7 +40,7 @@ export function createUseLdoMethods(dataset: SolidLdoDataset): UseLdoMethods { getSubject( shapeType: ShapeType, subject: string | SubjectNode, - ): Type | Error { + ): Type { return dataset.usingType(shapeType).fromSubject(subject); }, /** diff --git a/packages/solid-react/src/useRootContainer.ts b/packages/solid-react/src/useRootContainer.ts index 92847cb..e32369a 100644 --- a/packages/solid-react/src/useRootContainer.ts +++ b/packages/solid-react/src/useRootContainer.ts @@ -1,8 +1,13 @@ import type { Container, ContainerUri } from "@ldo/solid"; import { useEffect, useState } from "react"; +import type { UseResourceOptions } from "./useResource"; import { useResource } from "./useResource"; import { useLdo } from "./SolidLdoProvider"; -export function useRootContainerFor(uri?: string): Container | undefined { + +export function useRootContainerFor( + uri?: string, + options?: UseResourceOptions, +): Container | undefined { const { getResource } = useLdo(); const [rootContainerUri, setRootContainerUri] = useState< @@ -17,8 +22,10 @@ export function useRootContainerFor(uri?: string): Container | undefined { setRootContainerUri(result.uri); } }); + } else { + setRootContainerUri(undefined); } }, [uri]); - return useResource(rootContainerUri); + return useResource(rootContainerUri, options); } diff --git a/packages/solid-react/test/.ldo/post.context.ts b/packages/solid-react/test/.ldo/post.context.ts new file mode 100644 index 0000000..dafbe33 --- /dev/null +++ b/packages/solid-react/test/.ldo/post.context.ts @@ -0,0 +1,31 @@ +import { ContextDefinition } from "jsonld"; + +/** + * ============================================================================= + * postContext: JSONLD Context for post + * ============================================================================= + */ +export const postContext: ContextDefinition = { + type: { + "@id": "@type", + }, + SocialMediaPosting: "http://schema.org/SocialMediaPosting", + CreativeWork: "http://schema.org/CreativeWork", + Thing: "http://schema.org/Thing", + articleBody: { + "@id": "http://schema.org/articleBody", + "@type": "http://www.w3.org/2001/XMLSchema#string", + }, + uploadDate: { + "@id": "http://schema.org/uploadDate", + "@type": "http://www.w3.org/2001/XMLSchema#date", + }, + image: { + "@id": "http://schema.org/image", + "@type": "@id", + }, + publisher: { + "@id": "http://schema.org/publisher", + "@type": "@id", + }, +}; diff --git a/packages/solid-react/test/.ldo/post.schema.ts b/packages/solid-react/test/.ldo/post.schema.ts new file mode 100644 index 0000000..39e8b63 --- /dev/null +++ b/packages/solid-react/test/.ldo/post.schema.ts @@ -0,0 +1,155 @@ +import { Schema } from "shexj"; + +/** + * ============================================================================= + * postSchema: ShexJ Schema for post + * ============================================================================= + */ +export const postSchema: Schema = { + type: "Schema", + shapes: [ + { + id: "https://example.com/PostSh", + type: "ShapeDecl", + shapeExpr: { + type: "Shape", + expression: { + type: "EachOf", + expressions: [ + { + type: "TripleConstraint", + predicate: "http://www.w3.org/1999/02/22-rdf-syntax-ns#type", + valueExpr: { + type: "NodeConstraint", + values: [ + "http://schema.org/SocialMediaPosting", + "http://schema.org/CreativeWork", + "http://schema.org/Thing", + ], + }, + }, + { + type: "TripleConstraint", + predicate: "http://schema.org/articleBody", + valueExpr: { + type: "NodeConstraint", + datatype: "http://www.w3.org/2001/XMLSchema#string", + }, + min: 0, + max: 1, + annotations: [ + { + type: "Annotation", + predicate: "http://www.w3.org/2000/01/rdf-schema#label", + object: { + value: "articleBody", + }, + }, + { + type: "Annotation", + predicate: "http://www.w3.org/2000/01/rdf-schema#comment", + object: { + value: "The actual body of the article. ", + }, + }, + ], + }, + { + type: "TripleConstraint", + predicate: "http://schema.org/uploadDate", + valueExpr: { + type: "NodeConstraint", + datatype: "http://www.w3.org/2001/XMLSchema#date", + }, + annotations: [ + { + type: "Annotation", + predicate: "http://www.w3.org/2000/01/rdf-schema#label", + object: { + value: "uploadDate", + }, + }, + { + type: "Annotation", + predicate: "http://www.w3.org/2000/01/rdf-schema#comment", + object: { + value: + "Date when this media object was uploaded to this site.", + }, + }, + ], + }, + { + type: "TripleConstraint", + predicate: "http://schema.org/image", + valueExpr: { + type: "NodeConstraint", + nodeKind: "iri", + }, + min: 0, + max: 1, + annotations: [ + { + type: "Annotation", + predicate: "http://www.w3.org/2000/01/rdf-schema#label", + object: { + value: "image", + }, + }, + { + type: "Annotation", + predicate: "http://www.w3.org/2000/01/rdf-schema#comment", + object: { + value: + "A media object that encodes this CreativeWork. This property is a synonym for encoding.", + }, + }, + ], + }, + { + type: "TripleConstraint", + predicate: "http://schema.org/publisher", + valueExpr: { + type: "NodeConstraint", + nodeKind: "iri", + }, + annotations: [ + { + type: "Annotation", + predicate: "http://www.w3.org/2000/01/rdf-schema#label", + object: { + value: "publisher", + }, + }, + { + type: "Annotation", + predicate: "http://www.w3.org/2000/01/rdf-schema#comment", + object: { + value: "The publisher of the creative work.", + }, + }, + ], + }, + ], + }, + annotations: [ + { + type: "Annotation", + predicate: "http://www.w3.org/2000/01/rdf-schema#label", + object: { + value: "SocialMediaPost", + }, + }, + { + type: "Annotation", + predicate: "http://www.w3.org/2000/01/rdf-schema#comment", + object: { + value: + "A post to a social media platform, including blog posts, tweets, Facebook posts, etc.", + }, + }, + ], + }, + }, + ], +}; diff --git a/packages/solid-react/test/.ldo/post.shapeTypes.ts b/packages/solid-react/test/.ldo/post.shapeTypes.ts new file mode 100644 index 0000000..4c50683 --- /dev/null +++ b/packages/solid-react/test/.ldo/post.shapeTypes.ts @@ -0,0 +1,19 @@ +import { ShapeType } from "@ldo/ldo"; +import { postSchema } from "./post.schema"; +import { postContext } from "./post.context"; +import { PostSh } from "./post.typings"; + +/** + * ============================================================================= + * LDO ShapeTypes post + * ============================================================================= + */ + +/** + * PostSh ShapeType + */ +export const PostShShapeType: ShapeType = { + schema: postSchema, + shape: "https://example.com/PostSh", + context: postContext, +}; diff --git a/packages/solid-react/test/.ldo/post.typings.ts b/packages/solid-react/test/.ldo/post.typings.ts new file mode 100644 index 0000000..9ebaf71 --- /dev/null +++ b/packages/solid-react/test/.ldo/post.typings.ts @@ -0,0 +1,45 @@ +import { ContextDefinition } from "jsonld"; + +/** + * ============================================================================= + * Typescript Typings for post + * ============================================================================= + */ + +/** + * PostSh Type + */ +export interface PostSh { + "@id"?: string; + "@context"?: ContextDefinition; + type: + | { + "@id": "SocialMediaPosting"; + } + | { + "@id": "CreativeWork"; + } + | { + "@id": "Thing"; + }; + /** + * The actual body of the article. + */ + articleBody?: string; + /** + * Date when this media object was uploaded to this site. + */ + uploadDate: string; + /** + * A media object that encodes this CreativeWork. This property is a synonym for encoding. + */ + image?: { + "@id": string; + }; + /** + * The publisher of the creative work. + */ + publisher: { + "@id": string; + }; +} diff --git a/packages/solid-react/test/Integration.test.tsx b/packages/solid-react/test/Integration.test.tsx index f842781..e224a35 100644 --- a/packages/solid-react/test/Integration.test.tsx +++ b/packages/solid-react/test/Integration.test.tsx @@ -1,9 +1,18 @@ import React, { useEffect, useState } from "react"; import type { FunctionComponent } from "react"; import { render, screen, fireEvent } from "@testing-library/react"; -import { SAMPLE_BINARY_URI, SAMPLE_DATA_URI, setUpServer } from "./setUpServer"; +import { + SAMPLE_BINARY_URI, + SAMPLE_DATA_URI, + SERVER_DOMAIN, + setUpServer, +} from "./setUpServer"; import { UnauthenticatedSolidLdoProvider } from "../src/UnauthenticatedSolidLdoProvider"; import { useResource } from "../src/useResource"; +import { useRootContainerFor } from "../src/useRootContainer"; +import { useLdo } from "../src/SolidLdoProvider"; +import { PostShShapeType } from "./.ldo/post.shapeTypes"; +import type { PostSh } from "./.ldo/post.typings"; // Use an increased timeout, since the CSS server takes too much setup time. jest.setTimeout(40_000); @@ -118,4 +127,94 @@ describe("Integration Tests", () => { expect(resourceStatus2.innerHTML).toBe("binaryReadSuccess"); }); }); + + describe("useRootContainer", () => { + it("gets the root container for a sub-resource", async () => { + const RootContainerTest: FunctionComponent = () => { + const rootContainer = useRootContainerFor(SAMPLE_DATA_URI, { + suppressInitialRead: true, + }); + return rootContainer ? ( +

{rootContainer?.uri}

+ ) : undefined; + }; + render( + + + , + ); + const container = await screen.findByRole("root"); + expect(container.innerHTML).toBe(SERVER_DOMAIN); + }); + + it("returns undefined when a URI is not provided", async () => { + const RootContainerTest: FunctionComponent = () => { + const rootContainer = useRootContainerFor(undefined, { + suppressInitialRead: true, + }); + return rootContainer ? ( +

{rootContainer?.uri}

+ ) : ( +

Undefined

+ ); + }; + render( + + + , + ); + const container = await screen.findByRole("undefined"); + expect(container.innerHTML).toBe("Undefined"); + }); + }); + + describe("useLdoMethod", () => { + it("uses get subject to get a linked data object", async () => { + const GetSubjectTest: FunctionComponent = () => { + const [subject, setSubject] = useState(); + const { getSubject } = useLdo(); + useEffect(() => { + const someSubject = getSubject( + PostShShapeType, + "https://example.com/subject", + ); + setSubject(someSubject); + }, []); + return subject ?

{subject["@id"]}

: undefined; + }; + render( + + + , + ); + const container = await screen.findByRole("subject"); + expect(container.innerHTML).toBe("https://example.com/subject"); + }); + + it("uses createData to create a new data object", async () => { + const GetSubjectTest: FunctionComponent = () => { + const [subject, setSubject] = useState(); + const { createData, getResource } = useLdo(); + useEffect(() => { + const someSubject = createData( + PostShShapeType, + "https://example.com/subject", + getResource("https://example.com/"), + ); + someSubject.articleBody = "Cool Article"; + setSubject(someSubject); + }, []); + return subject ? ( +

{subject.articleBody}

+ ) : undefined; + }; + render( + + + , + ); + const container = await screen.findByRole("subject"); + expect(container.innerHTML).toBe("Cool Article"); + }); + }); }); diff --git a/packages/solid-react/test/setUpServer.ts b/packages/solid-react/test/setUpServer.ts index a3e4f4a..57fabee 100644 --- a/packages/solid-react/test/setUpServer.ts +++ b/packages/solid-react/test/setUpServer.ts @@ -73,7 +73,7 @@ export function setUpServer(): SetUpServerReturn { beforeEach(async () => { s.fetchMock = jest.fn(s.authFetch); // Create a new document called sample.ttl - const result = await s.authFetch(ROOT_CONTAINER, { + await s.authFetch(ROOT_CONTAINER, { method: "POST", headers: { link: '; rel="type"', diff --git a/packages/solid-react/test/useSubject.integration.test.tsx b/packages/solid-react/test/useSubject.integration.test.tsx new file mode 100644 index 0000000..6aaf84f --- /dev/null +++ b/packages/solid-react/test/useSubject.integration.test.tsx @@ -0,0 +1,9 @@ +import { setUpServer } from "./setUpServer"; + +describe("useSubject", () => { + setUpServer(); + + it("trivial", () => { + expect(true).toBe(true); + }); +}); From 37ac0bd027f6c15f444d232563e4980886569a23 Mon Sep 17 00:00:00 2001 From: jaxoncreed Date: Sun, 4 Feb 2024 23:39:55 -0500 Subject: [PATCH 4/4] Completed tests --- .../solid-react/test/.ldo/post.context.ts | 1 + .../solid-react/test/.ldo/post.typings.ts | 2 +- .../solid-react/test/Integration.test.tsx | 138 ++++++++++++++++++ packages/solid-react/test/setUpServer.ts | 22 +-- .../test/useSubject.integration.test.tsx | 9 -- 5 files changed, 147 insertions(+), 25 deletions(-) delete mode 100644 packages/solid-react/test/useSubject.integration.test.tsx diff --git a/packages/solid-react/test/.ldo/post.context.ts b/packages/solid-react/test/.ldo/post.context.ts index dafbe33..5cb3a91 100644 --- a/packages/solid-react/test/.ldo/post.context.ts +++ b/packages/solid-react/test/.ldo/post.context.ts @@ -27,5 +27,6 @@ export const postContext: ContextDefinition = { publisher: { "@id": "http://schema.org/publisher", "@type": "@id", + "@container": "@set", }, }; diff --git a/packages/solid-react/test/.ldo/post.typings.ts b/packages/solid-react/test/.ldo/post.typings.ts index 9ebaf71..1425a9a 100644 --- a/packages/solid-react/test/.ldo/post.typings.ts +++ b/packages/solid-react/test/.ldo/post.typings.ts @@ -41,5 +41,5 @@ export interface PostSh { */ publisher: { "@id": string; - }; + }[]; } diff --git a/packages/solid-react/test/Integration.test.tsx b/packages/solid-react/test/Integration.test.tsx index e224a35..6120241 100644 --- a/packages/solid-react/test/Integration.test.tsx +++ b/packages/solid-react/test/Integration.test.tsx @@ -13,6 +13,7 @@ import { useRootContainerFor } from "../src/useRootContainer"; import { useLdo } from "../src/SolidLdoProvider"; import { PostShShapeType } from "./.ldo/post.shapeTypes"; import type { PostSh } from "./.ldo/post.typings"; +import { useSubject } from "../src/useSubject"; // Use an increased timeout, since the CSS server takes too much setup time. jest.setTimeout(40_000); @@ -217,4 +218,141 @@ describe("Integration Tests", () => { expect(container.innerHTML).toBe("Cool Article"); }); }); + + describe("useSubject", () => { + it("renders the article body from the useSubject value", async () => { + const UseSubjectTest: FunctionComponent = () => { + useResource(SAMPLE_DATA_URI); + const post = useSubject(PostShShapeType, `${SAMPLE_DATA_URI}#Post1`); + + return

{post.articleBody}

; + }; + render( + + + , + ); + + await screen.findByText("test"); + }); + + it("renders the array value from the useSubject value", async () => { + const UseSubjectTest: FunctionComponent = () => { + const resource = useResource(SAMPLE_DATA_URI); + const post = useSubject(PostShShapeType, `${SAMPLE_DATA_URI}#Post1`); + if (resource.isLoading() || !post) return

loading

; + + return ( +
+

{post.publisher[0]["@id"]}

+
    + {post.publisher.map((publisher) => { + return
  • {publisher["@id"]}
  • ; + })} +
+
+ ); + }; + render( + + + , + ); + + const single = await screen.findByRole("single"); + expect(single.innerHTML).toBe("https://example.com/Publisher1"); + const list = await screen.findByRole("list"); + expect(list.children[0].innerHTML).toBe("https://example.com/Publisher1"); + expect(list.children[1].innerHTML).toBe("https://example.com/Publisher2"); + }); + + it("returns undefined in the subject URI is undefined", async () => { + const UseSubjectTest: FunctionComponent = () => { + useResource(SAMPLE_DATA_URI, { suppressInitialRead: true }); + const post = useSubject(PostShShapeType, undefined); + + return ( +

+ {post === undefined ? "Undefined" : "Not Undefined"} +

+ ); + }; + render( + + + , + ); + + const article = await screen.findByRole("article"); + expect(article.innerHTML).toBe("Undefined"); + }); + + it("returns nothing if a symbol key is provided", async () => { + const UseSubjectTest: FunctionComponent = () => { + const resource = useResource(SAMPLE_DATA_URI); + const post = useSubject(PostShShapeType, `${SAMPLE_DATA_URI}#Post1`); + if (resource.isLoading() || !post) return

loading

; + + return

{typeof post[Symbol.hasInstance]}

; + }; + render( + + + , + ); + + const article = await screen.findByRole("value"); + expect(article.innerHTML).toBe("undefined"); + }); + + it("returns an id if an id key is provided", async () => { + const UseSubjectTest: FunctionComponent = () => { + const resource = useResource(SAMPLE_DATA_URI); + const post = useSubject(PostShShapeType, `${SAMPLE_DATA_URI}#Post1`); + if (resource.isLoading() || !post) return

loading

; + + return

{post["@id"]}

; + }; + render( + + + , + ); + + const article = await screen.findByRole("value"); + expect(article.innerHTML).toBe(`${SAMPLE_DATA_URI}#Post1`); + }); + + it("does not set a value if a value is attempted to be set", async () => { + const warn = jest.spyOn(console, "warn").mockImplementation(() => {}); + const UseSubjectTest: FunctionComponent = () => { + const resource = useResource(SAMPLE_DATA_URI); + const post = useSubject(PostShShapeType, `${SAMPLE_DATA_URI}#Post1`); + if (resource.isLoading() || !post) return

loading

; + + return ( +
+

{post.articleBody}

+ +
+ ); + }; + render( + + + , + ); + + const article = await screen.findByRole("value"); + expect(article.innerHTML).toBe(`test`); + fireEvent.click(screen.getByText("Attempt Change")); + expect(article.innerHTML).not.toBe("bad"); + expect(warn).toHaveBeenCalledWith( + "You've attempted to set a value on a Linked Data Object from the useSubject, useMatchingSubject, or useMatchingObject hooks. These linked data objects should only be used to render data, not modify it. To modify data, use the `changeData` function.", + ); + warn.mockReset(); + }); + }); }); diff --git a/packages/solid-react/test/setUpServer.ts b/packages/solid-react/test/setUpServer.ts index 57fabee..0551ccf 100644 --- a/packages/solid-react/test/setUpServer.ts +++ b/packages/solid-react/test/setUpServer.ts @@ -17,21 +17,13 @@ export const SAMPLE2_BINARY_URI = `${TEST_CONTAINER_URI}${SAMPLE2_BINARY_SLUG}` as LeafUri; export const SAMPLE_CONTAINER_URI = `${TEST_CONTAINER_URI}sample_container/` as ContainerUri; -export const SPIDER_MAN_TTL = `@base . -@prefix rdf: . -@prefix rdfs: . -@prefix foaf: . -@prefix rel: . +export const EXAMPLE_POST_TTL = `@prefix schema: . -<#green-goblin> - rel:enemyOf <#spiderman> ; - a foaf:Person ; # in the context of the Marvel universe - foaf:name "Green Goblin" . - -<#spiderman> - rel:enemyOf <#green-goblin> ; - a foaf:Person ; - foaf:name "Spiderman", "Человек-паук"@ru .`; +<#Post1> + a schema:CreativeWork, schema:Thing, schema:SocialMediaPosting ; + schema:image ; + schema:articleBody "test" ; + schema:publisher , .`; export const TEST_CONTAINER_TTL = `@prefix dc: . @prefix ldp: . @prefix posix: . @@ -84,7 +76,7 @@ export function setUpServer(): SetUpServerReturn { s.authFetch(TEST_CONTAINER_URI, { method: "POST", headers: { "content-type": "text/turtle", slug: "sample.ttl" }, - body: SPIDER_MAN_TTL, + body: EXAMPLE_POST_TTL, }), s.authFetch(TEST_CONTAINER_URI, { method: "POST", diff --git a/packages/solid-react/test/useSubject.integration.test.tsx b/packages/solid-react/test/useSubject.integration.test.tsx deleted file mode 100644 index 6aaf84f..0000000 --- a/packages/solid-react/test/useSubject.integration.test.tsx +++ /dev/null @@ -1,9 +0,0 @@ -import { setUpServer } from "./setUpServer"; - -describe("useSubject", () => { - setUpServer(); - - it("trivial", () => { - expect(true).toBe(true); - }); -});