parent
0f35c7c7a8
commit
16376a4e7f
@ -0,0 +1,3 @@ |
|||||||
|
export const RDF_TYPE = "http://www.w3.org/1999/02/22-rdf-syntax-ns#type"; |
||||||
|
export const TYPE_REGISTRATION = |
||||||
|
"http://www.w3.org/ns/solid/terms#TypeRegistration"; |
@ -0,0 +1,93 @@ |
|||||||
|
import type { ContainerUri, LeafUri, SolidLdoDataset } from "@ldo/solid"; |
||||||
|
import { createSolidLdoDataset } from "@ldo/solid"; |
||||||
|
import type { TypeRegistration } from "./.ldo/typeIndex.typings"; |
||||||
|
import { guaranteeFetch } from "@ldo/solid/dist/util/guaranteeFetch"; |
||||||
|
import type { TypeIndexProfile } from "./.ldo/profile.typings"; |
||||||
|
import { TypeIndexProfileShapeType } from "./.ldo/profile.shapeTypes"; |
||||||
|
import { TypeRegistrationShapeType } from "./.ldo/typeIndex.shapeTypes"; |
||||||
|
import { RDF_TYPE, TYPE_REGISTRATION } from "./constants"; |
||||||
|
|
||||||
|
interface GetInstanceUrisOptions { |
||||||
|
solidLdoDataset?: SolidLdoDataset; |
||||||
|
fetch?: typeof fetch; |
||||||
|
} |
||||||
|
|
||||||
|
export async function getTypeRegistrations( |
||||||
|
webId: string, |
||||||
|
options?: GetInstanceUrisOptions, |
||||||
|
): Promise<TypeRegistration[]> { |
||||||
|
const fetch = guaranteeFetch(options?.fetch); |
||||||
|
const dataset = options?.solidLdoDataset ?? createSolidLdoDataset({ fetch }); |
||||||
|
|
||||||
|
// Get Profile
|
||||||
|
const profileResource = dataset.getResource(webId); |
||||||
|
const readResult = await profileResource.readIfUnfetched(); |
||||||
|
if (readResult.isError) throw readResult; |
||||||
|
const profile = dataset |
||||||
|
.usingType(TypeIndexProfileShapeType) |
||||||
|
.fromSubject(webId); |
||||||
|
|
||||||
|
// Get Type Indexes
|
||||||
|
const typeIndexUris = getTypeIndexesUrisFromProfile(profile); |
||||||
|
|
||||||
|
// Fetch the type Indexes
|
||||||
|
await Promise.all( |
||||||
|
typeIndexUris.map(async (typeIndexUri) => { |
||||||
|
const typeIndexResource = dataset.getResource(typeIndexUri); |
||||||
|
const readResult = await typeIndexResource.readIfUnfetched(); |
||||||
|
if (readResult.isError) throw readResult; |
||||||
|
}), |
||||||
|
); |
||||||
|
|
||||||
|
// Get Type Registrations
|
||||||
|
return dataset |
||||||
|
.usingType(TypeRegistrationShapeType) |
||||||
|
.matchSubject(RDF_TYPE, TYPE_REGISTRATION); |
||||||
|
} |
||||||
|
|
||||||
|
export function getTypeIndexesUrisFromProfile( |
||||||
|
profile: TypeIndexProfile, |
||||||
|
): LeafUri[] { |
||||||
|
const uris: LeafUri[] = []; |
||||||
|
profile.privateTypeIndex?.forEach((indexNode) => { |
||||||
|
uris.push(indexNode["@id"] as LeafUri); |
||||||
|
}); |
||||||
|
profile.publicTypeIndex?.forEach((indexNode) => { |
||||||
|
uris.push(indexNode["@id"] as LeafUri); |
||||||
|
}); |
||||||
|
return uris; |
||||||
|
} |
||||||
|
|
||||||
|
export async function getInstanceUris( |
||||||
|
classUri: string, |
||||||
|
typeRegistrations: TypeRegistration[], |
||||||
|
options?: GetInstanceUrisOptions, |
||||||
|
): Promise<LeafUri[]> { |
||||||
|
const fetch = guaranteeFetch(options?.fetch); |
||||||
|
const dataset = options?.solidLdoDataset ?? createSolidLdoDataset({ fetch }); |
||||||
|
|
||||||
|
const leafUris = new Set<LeafUri>(); |
||||||
|
await Promise.all( |
||||||
|
typeRegistrations.map(async (registration) => { |
||||||
|
if (registration.forClass["@id"] === classUri) { |
||||||
|
// Individual registrations
|
||||||
|
registration.instance?.forEach((instance) => |
||||||
|
leafUris.add(instance["@id"] as LeafUri), |
||||||
|
); |
||||||
|
// Container registrations
|
||||||
|
await Promise.all( |
||||||
|
registration.instanceContainer?.map(async (instanceContainer) => { |
||||||
|
const containerResource = dataset.getResource( |
||||||
|
instanceContainer["@id"] as ContainerUri, |
||||||
|
); |
||||||
|
await containerResource.readIfUnfetched(); |
||||||
|
containerResource.children().forEach((child) => { |
||||||
|
if (child.type === "leaf") leafUris.add(child.uri); |
||||||
|
}); |
||||||
|
}) ?? [], |
||||||
|
); |
||||||
|
} |
||||||
|
}), |
||||||
|
); |
||||||
|
return Array.from(leafUris); |
||||||
|
} |
@ -0,0 +1,45 @@ |
|||||||
|
import type { LeafUri } from "@ldo/solid"; |
||||||
|
import { useTypeIndexProfile } from "./useTypeIndexProfile"; |
||||||
|
import { useEffect, useMemo, useState } from "react"; |
||||||
|
import { useSubscribeToUris } from "./util/useSubscribeToUris"; |
||||||
|
import { useLdo, useMatchSubject } from "@ldo/solid-react"; |
||||||
|
import { TypeRegistrationShapeType } from "../.ldo/typeIndex.shapeTypes"; |
||||||
|
import { RDF_TYPE, TYPE_REGISTRATION } from "../constants"; |
||||||
|
import { |
||||||
|
getInstanceUris, |
||||||
|
getTypeIndexesUrisFromProfile, |
||||||
|
} from "../getTypeIndex"; |
||||||
|
|
||||||
|
/** |
||||||
|
* Provides the LeafUris of everything in a type node for a specific class uri |
||||||
|
* |
||||||
|
* @param classUri - the class uri |
||||||
|
* @returns - URIs of all resources registered with this node |
||||||
|
*/ |
||||||
|
export function useInstanceUris(classUri: string): LeafUri[] { |
||||||
|
const { dataset } = useLdo(); |
||||||
|
const profile = useTypeIndexProfile(); |
||||||
|
|
||||||
|
const typeIndexUris: string[] = useMemo( |
||||||
|
() => (profile ? getTypeIndexesUrisFromProfile(profile) : []), |
||||||
|
[profile], |
||||||
|
); |
||||||
|
|
||||||
|
useSubscribeToUris(typeIndexUris); |
||||||
|
|
||||||
|
const [leafUris, setLeafUris] = useState<LeafUri[]>([]); |
||||||
|
|
||||||
|
const typeRegistrations = useMatchSubject( |
||||||
|
TypeRegistrationShapeType, |
||||||
|
RDF_TYPE, |
||||||
|
TYPE_REGISTRATION, |
||||||
|
); |
||||||
|
|
||||||
|
useEffect(() => { |
||||||
|
getInstanceUris(classUri, typeRegistrations, { |
||||||
|
solidLdoDataset: dataset, |
||||||
|
}).then(setLeafUris); |
||||||
|
}, [typeRegistrations]); |
||||||
|
|
||||||
|
return leafUris; |
||||||
|
} |
@ -1,23 +0,0 @@ |
|||||||
import type { LeafUri } from "@ldo/solid"; |
|
||||||
import { useTypeIndexProfile } from "./useTypeIndexProfile"; |
|
||||||
import { useMemo } from "react"; |
|
||||||
import { useSubscribeToUris } from "./util/useSubscribeToUris"; |
|
||||||
|
|
||||||
export function useTypeIndex(classUri: string): Promise<LeafUri[]> { |
|
||||||
const profile = useTypeIndexProfile(); |
|
||||||
|
|
||||||
const typeIndexUris: string[] = useMemo(() => { |
|
||||||
const uris: string[] = []; |
|
||||||
profile?.privateTypeIndex?.forEach((indexNode) => { |
|
||||||
uris.push(indexNode["@id"]); |
|
||||||
}); |
|
||||||
profile?.publicTypeIndex?.forEach((indexNode) => { |
|
||||||
uris.push(indexNode["@id"]); |
|
||||||
}); |
|
||||||
return uris; |
|
||||||
}, [profile]); |
|
||||||
|
|
||||||
useSubscribeToUris(typeIndexUris); |
|
||||||
|
|
||||||
|
|
||||||
} |
|
@ -1,32 +0,0 @@ |
|||||||
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", |
|
||||||
"@container": "@set", |
|
||||||
}, |
|
||||||
}; |
|
@ -1,155 +0,0 @@ |
|||||||
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.", |
|
||||||
}, |
|
||||||
}, |
|
||||||
], |
|
||||||
}, |
|
||||||
}, |
|
||||||
], |
|
||||||
}; |
|
@ -1,19 +0,0 @@ |
|||||||
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<PostSh> = { |
|
||||||
schema: postSchema, |
|
||||||
shape: "https://example.com/PostSh", |
|
||||||
context: postContext, |
|
||||||
}; |
|
@ -1,45 +0,0 @@ |
|||||||
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; |
|
||||||
}[]; |
|
||||||
} |
|
@ -0,0 +1,41 @@ |
|||||||
|
import { createSolidLdoDataset } from "@ldo/solid"; |
||||||
|
import { |
||||||
|
MY_BOOKMARKS_1_URI, |
||||||
|
MY_BOOKMARKS_2_URI, |
||||||
|
setUpServer, |
||||||
|
WEB_ID, |
||||||
|
} from "./setUpServer"; |
||||||
|
import { getInstanceUris, getTypeRegistrations } from "../src/getTypeIndex"; |
||||||
|
|
||||||
|
// Use an increased timeout, since the CSS server takes too much setup time.
|
||||||
|
jest.setTimeout(40_000); |
||||||
|
|
||||||
|
describe("General Tests", () => { |
||||||
|
setUpServer(); |
||||||
|
|
||||||
|
it("gets the current typeindex", async () => { |
||||||
|
const solidLdoDataset = createSolidLdoDataset(); |
||||||
|
const typeRegistrations = await getTypeRegistrations(WEB_ID, { |
||||||
|
solidLdoDataset, |
||||||
|
}); |
||||||
|
const addressBookUris = await getInstanceUris( |
||||||
|
"http://www.w3.org/2006/vcard/ns#AddressBook", |
||||||
|
typeRegistrations, |
||||||
|
{ solidLdoDataset }, |
||||||
|
); |
||||||
|
expect(addressBookUris).toEqual( |
||||||
|
expect.arrayContaining([ |
||||||
|
"https://example.com/myPrivateAddressBook.ttl", |
||||||
|
"https://example.com/myPublicAddressBook.ttl", |
||||||
|
]), |
||||||
|
); |
||||||
|
const bookmarkUris = await getInstanceUris( |
||||||
|
"http://www.w3.org/2002/01/bookmark#Bookmark", |
||||||
|
typeRegistrations, |
||||||
|
{ solidLdoDataset }, |
||||||
|
); |
||||||
|
expect(bookmarkUris).toEqual( |
||||||
|
expect.arrayContaining([MY_BOOKMARKS_1_URI, MY_BOOKMARKS_2_URI]), |
||||||
|
); |
||||||
|
}); |
||||||
|
}); |
@ -1,431 +0,0 @@ |
|||||||
import React, { useCallback, useEffect, useState } from "react"; |
|
||||||
import type { FunctionComponent } from "react"; |
|
||||||
import { render, screen, fireEvent, act } from "@testing-library/react"; |
|
||||||
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"; |
|
||||||
import { useSubject } from "../src/useSubject"; |
|
||||||
|
|
||||||
// Use an increased timeout, since the CSS server takes too much setup time.
|
|
||||||
jest.setTimeout(40_000); |
|
||||||
|
|
||||||
describe("Integration Tests", () => { |
|
||||||
setUpServer(); |
|
||||||
|
|
||||||
/** |
|
||||||
* =========================================================================== |
|
||||||
* useResource |
|
||||||
* =========================================================================== |
|
||||||
*/ |
|
||||||
describe("useResource", () => { |
|
||||||
it("Fetches a resource and indicates it is loading while doing so", async () => { |
|
||||||
const UseResourceTest: FunctionComponent = () => { |
|
||||||
const resource = useResource(SAMPLE_DATA_URI); |
|
||||||
if (resource?.isLoading()) return <p>Loading</p>; |
|
||||||
return <p role="status">{resource.status.type}</p>; |
|
||||||
}; |
|
||||||
render( |
|
||||||
<UnauthenticatedSolidLdoProvider> |
|
||||||
<UseResourceTest /> |
|
||||||
</UnauthenticatedSolidLdoProvider>, |
|
||||||
); |
|
||||||
await screen.findByText("Loading"); |
|
||||||
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<string | undefined>(undefined); |
|
||||||
const resource = useResource(uri, { suppressInitialRead: true }); |
|
||||||
if (!resource) |
|
||||||
return ( |
|
||||||
<div> |
|
||||||
<p>Undefined</p> |
|
||||||
<button onClick={() => setUri(SAMPLE_DATA_URI)}>Next</button> |
|
||||||
</div> |
|
||||||
); |
|
||||||
return <p role="status">{resource.status.type}</p>; |
|
||||||
}; |
|
||||||
render( |
|
||||||
<UnauthenticatedSolidLdoProvider> |
|
||||||
<UseResourceUndefinedTest /> |
|
||||||
</UnauthenticatedSolidLdoProvider>, |
|
||||||
); |
|
||||||
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 <p>Loading</p>; |
|
||||||
return <p role="status">{resource.status.type}</p>; |
|
||||||
}; |
|
||||||
const ReloadParent: FunctionComponent = () => { |
|
||||||
const [showComponent, setShowComponent] = useState(true); |
|
||||||
return ( |
|
||||||
<div> |
|
||||||
<button onClick={() => setShowComponent(!showComponent)}> |
|
||||||
Show Component |
|
||||||
</button> |
|
||||||
{showComponent ? <ReloadTest /> : <p>Hidden</p>} |
|
||||||
</div> |
|
||||||
); |
|
||||||
}; |
|
||||||
render( |
|
||||||
<UnauthenticatedSolidLdoProvider> |
|
||||||
<ReloadParent /> |
|
||||||
</UnauthenticatedSolidLdoProvider>, |
|
||||||
); |
|
||||||
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", undefined, { |
|
||||||
timeout: 5000, |
|
||||||
}); |
|
||||||
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 <p>Loading</p>; |
|
||||||
return ( |
|
||||||
<div> |
|
||||||
<p role="status">{resource.status.type}</p> |
|
||||||
<button onClick={() => setUri(SAMPLE_BINARY_URI)}> |
|
||||||
Update URI |
|
||||||
</button> |
|
||||||
</div> |
|
||||||
); |
|
||||||
}; |
|
||||||
render( |
|
||||||
<UnauthenticatedSolidLdoProvider> |
|
||||||
<SwapResourceTest /> |
|
||||||
</UnauthenticatedSolidLdoProvider>, |
|
||||||
); |
|
||||||
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"); |
|
||||||
}); |
|
||||||
}); |
|
||||||
|
|
||||||
/** |
|
||||||
* =========================================================================== |
|
||||||
* useRootContainer |
|
||||||
* =========================================================================== |
|
||||||
*/ |
|
||||||
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 ? <p role="root">{rootContainer?.uri}</p> : <></>; |
|
||||||
}; |
|
||||||
render( |
|
||||||
<UnauthenticatedSolidLdoProvider> |
|
||||||
<RootContainerTest /> |
|
||||||
</UnauthenticatedSolidLdoProvider>, |
|
||||||
); |
|
||||||
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 ? ( |
|
||||||
<p role="root">{rootContainer?.uri}</p> |
|
||||||
) : ( |
|
||||||
<p role="undefined">Undefined</p> |
|
||||||
); |
|
||||||
}; |
|
||||||
render( |
|
||||||
<UnauthenticatedSolidLdoProvider> |
|
||||||
<RootContainerTest /> |
|
||||||
</UnauthenticatedSolidLdoProvider>, |
|
||||||
); |
|
||||||
const container = await screen.findByRole("undefined"); |
|
||||||
expect(container.innerHTML).toBe("Undefined"); |
|
||||||
}); |
|
||||||
}); |
|
||||||
|
|
||||||
/** |
|
||||||
* =========================================================================== |
|
||||||
* useLdoMethods |
|
||||||
* =========================================================================== |
|
||||||
*/ |
|
||||||
describe("useLdoMethods", () => { |
|
||||||
it("uses get subject to get a linked data object", async () => { |
|
||||||
const GetSubjectTest: FunctionComponent = () => { |
|
||||||
const [subject, setSubject] = useState<PostSh | undefined>(); |
|
||||||
const { getSubject } = useLdo(); |
|
||||||
useEffect(() => { |
|
||||||
const someSubject = getSubject( |
|
||||||
PostShShapeType, |
|
||||||
"https://example.com/subject", |
|
||||||
); |
|
||||||
setSubject(someSubject); |
|
||||||
}, []); |
|
||||||
return subject ? <p role="subject">{subject["@id"]}</p> : <></>; |
|
||||||
}; |
|
||||||
render( |
|
||||||
<UnauthenticatedSolidLdoProvider> |
|
||||||
<GetSubjectTest /> |
|
||||||
</UnauthenticatedSolidLdoProvider>, |
|
||||||
); |
|
||||||
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<PostSh | undefined>(); |
|
||||||
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 ? <p role="subject">{subject.articleBody}</p> : <></>; |
|
||||||
}; |
|
||||||
render( |
|
||||||
<UnauthenticatedSolidLdoProvider> |
|
||||||
<GetSubjectTest /> |
|
||||||
</UnauthenticatedSolidLdoProvider>, |
|
||||||
); |
|
||||||
const container = await screen.findByRole("subject"); |
|
||||||
expect(container.innerHTML).toBe("Cool Article"); |
|
||||||
}); |
|
||||||
}); |
|
||||||
|
|
||||||
/** |
|
||||||
* =========================================================================== |
|
||||||
* useSubject |
|
||||||
* =========================================================================== |
|
||||||
*/ |
|
||||||
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 <p role="article">{post.articleBody}</p>; |
|
||||||
}; |
|
||||||
render( |
|
||||||
<UnauthenticatedSolidLdoProvider> |
|
||||||
<UseSubjectTest /> |
|
||||||
</UnauthenticatedSolidLdoProvider>, |
|
||||||
); |
|
||||||
|
|
||||||
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 <p>loading</p>; |
|
||||||
|
|
||||||
return ( |
|
||||||
<div> |
|
||||||
<p role="single">{post.publisher[0]["@id"]}</p> |
|
||||||
<ul role="list"> |
|
||||||
{post.publisher.map((publisher) => { |
|
||||||
return <li key={publisher["@id"]}>{publisher["@id"]}</li>; |
|
||||||
})} |
|
||||||
</ul> |
|
||||||
</div> |
|
||||||
); |
|
||||||
}; |
|
||||||
render( |
|
||||||
<UnauthenticatedSolidLdoProvider> |
|
||||||
<UseSubjectTest /> |
|
||||||
</UnauthenticatedSolidLdoProvider>, |
|
||||||
); |
|
||||||
|
|
||||||
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 ( |
|
||||||
<p role="article"> |
|
||||||
{post === undefined ? "Undefined" : "Not Undefined"} |
|
||||||
</p> |
|
||||||
); |
|
||||||
}; |
|
||||||
render( |
|
||||||
<UnauthenticatedSolidLdoProvider> |
|
||||||
<UseSubjectTest /> |
|
||||||
</UnauthenticatedSolidLdoProvider>, |
|
||||||
); |
|
||||||
|
|
||||||
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 <p>loading</p>; |
|
||||||
|
|
||||||
return <p role="value">{typeof post[Symbol.hasInstance]}</p>; |
|
||||||
}; |
|
||||||
render( |
|
||||||
<UnauthenticatedSolidLdoProvider> |
|
||||||
<UseSubjectTest /> |
|
||||||
</UnauthenticatedSolidLdoProvider>, |
|
||||||
); |
|
||||||
|
|
||||||
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 <p>loading</p>; |
|
||||||
|
|
||||||
return <p role="value">{post["@id"]}</p>; |
|
||||||
}; |
|
||||||
render( |
|
||||||
<UnauthenticatedSolidLdoProvider> |
|
||||||
<UseSubjectTest /> |
|
||||||
</UnauthenticatedSolidLdoProvider>, |
|
||||||
); |
|
||||||
|
|
||||||
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 <p>loading</p>; |
|
||||||
|
|
||||||
return ( |
|
||||||
<div> |
|
||||||
<p role="value">{post.articleBody}</p> |
|
||||||
<button onClick={() => (post.articleBody = "bad")}> |
|
||||||
Attempt Change |
|
||||||
</button> |
|
||||||
</div> |
|
||||||
); |
|
||||||
}; |
|
||||||
render( |
|
||||||
<UnauthenticatedSolidLdoProvider> |
|
||||||
<UseSubjectTest /> |
|
||||||
</UnauthenticatedSolidLdoProvider>, |
|
||||||
); |
|
||||||
|
|
||||||
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(); |
|
||||||
}); |
|
||||||
|
|
||||||
it("rerenders when asked to subscribe to a resource", async () => { |
|
||||||
const NotificationTest: FunctionComponent = () => { |
|
||||||
const resource = useResource(SAMPLE_DATA_URI, { subscribe: true }); |
|
||||||
const post = useSubject(PostShShapeType, `${SAMPLE_DATA_URI}#Post1`); |
|
||||||
|
|
||||||
const addPublisher = useCallback(async () => { |
|
||||||
await fetch(SAMPLE_DATA_URI, { |
|
||||||
method: "PATCH", |
|
||||||
body: `INSERT DATA { <${SAMPLE_DATA_URI}#Post1> <http://schema.org/publisher> <https://example.com/Publisher3> . }`, |
|
||||||
headers: { |
|
||||||
"Content-Type": "application/sparql-update", |
|
||||||
}, |
|
||||||
}); |
|
||||||
}, []); |
|
||||||
|
|
||||||
if (resource.isLoading() || !post) return <p>loading</p>; |
|
||||||
|
|
||||||
return ( |
|
||||||
<div> |
|
||||||
<ul role="list"> |
|
||||||
{post.publisher.map((publisher) => { |
|
||||||
return <li key={publisher["@id"]}>{publisher["@id"]}</li>; |
|
||||||
})} |
|
||||||
</ul> |
|
||||||
<button onClick={addPublisher}>Add Publisher</button> |
|
||||||
</div> |
|
||||||
); |
|
||||||
}; |
|
||||||
const { unmount } = render( |
|
||||||
<UnauthenticatedSolidLdoProvider> |
|
||||||
<NotificationTest /> |
|
||||||
</UnauthenticatedSolidLdoProvider>, |
|
||||||
); |
|
||||||
|
|
||||||
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"); |
|
||||||
|
|
||||||
// Wait for subscription to connect
|
|
||||||
await act(async () => { |
|
||||||
await new Promise((resolve) => setTimeout(resolve, 1000)); |
|
||||||
}); |
|
||||||
|
|
||||||
// Click button to add a publisher
|
|
||||||
await fireEvent.click(screen.getByText("Add Publisher")); |
|
||||||
await screen.findByText("https://example.com/Publisher3"); |
|
||||||
|
|
||||||
// Verify the new publisher is in the list
|
|
||||||
const updatedList = await screen.findByRole("list"); |
|
||||||
expect(updatedList.children[2].innerHTML).toBe( |
|
||||||
"https://example.com/Publisher3", |
|
||||||
); |
|
||||||
|
|
||||||
unmount(); |
|
||||||
|
|
||||||
await act(async () => { |
|
||||||
await new Promise((resolve) => setTimeout(resolve, 1000)); |
|
||||||
}); |
|
||||||
}); |
|
||||||
}); |
|
||||||
}); |
|
@ -0,0 +1,8 @@ |
|||||||
|
import { setUpServer } from "./setUpServer"; |
||||||
|
|
||||||
|
// Use an increased timeout, since the CSS server takes too much setup time.
|
||||||
|
jest.setTimeout(40_000); |
||||||
|
|
||||||
|
describe("React Tests", () => { |
||||||
|
setUpServer(); |
||||||
|
}); |
Loading…
Reference in new issue