getTypeIndex works

main
Jackson Morgan 8 months ago
parent 0f35c7c7a8
commit 16376a4e7f
  1. 82
      package-lock.json
  2. 1
      packages/solid-react/test/Integration.test.tsx
  3. 1
      packages/solid-react/test/setUpServer.ts
  4. 4
      packages/solid-type-index/src/.ldo/profile.context.ts
  5. 4
      packages/solid-type-index/src/.ldo/typeIndex.context.ts
  6. 3
      packages/solid-type-index/src/constants.ts
  7. 93
      packages/solid-type-index/src/getTypeIndex.ts
  8. 45
      packages/solid-type-index/src/react/useInstanceUris.ts
  9. 23
      packages/solid-type-index/src/react/useTypeIndex.ts
  10. 0
      packages/solid-type-index/src/setTypeIndex.ts
  11. 32
      packages/solid-type-index/test/.ldo/post.context.ts
  12. 155
      packages/solid-type-index/test/.ldo/post.schema.ts
  13. 19
      packages/solid-type-index/test/.ldo/post.shapeTypes.ts
  14. 45
      packages/solid-type-index/test/.ldo/post.typings.ts
  15. 41
      packages/solid-type-index/test/General.test.tsx
  16. 431
      packages/solid-type-index/test/Integration.test.tsx
  17. 8
      packages/solid-type-index/test/React.tsx
  18. 131
      packages/solid-type-index/test/setUpServer.ts
  19. 2
      packages/solid-type-index/test/test-server/solidServer.helper.ts

82
package-lock.json generated

@ -6251,6 +6251,10 @@
"resolved": "packages/solid-react",
"link": true
},
"node_modules/@ldo/solid-type-index": {
"resolved": "packages/solid-type-index",
"link": true
},
"node_modules/@ldo/subscribable-dataset": {
"resolved": "packages/subscribable-dataset",
"link": true
@ -29728,6 +29732,84 @@
"node": ">=4.2.0"
}
},
"packages/solid-type-index": {
"name": "@ldo/solid-type-index",
"version": "0.0.1-alpha.28",
"license": "MIT",
"dependencies": {
"@ldo/solid": "^0.0.1-alpha.28",
"@ldo/solid-react": "^0.0.1-alpha.28"
},
"devDependencies": {
"@ldo/rdf-utils": "^0.0.1-alpha.24",
"@rdfjs/types": "^1.0.1",
"@testing-library/react": "^14.1.2",
"@types/jest": "^27.0.3",
"jest-environment-jsdom": "^27.0.0",
"start-server-and-test": "^2.0.3",
"ts-jest": "^27.1.2",
"ts-node": "^10.9.2"
}
},
"packages/solid-type-index/node_modules/ts-jest": {
"version": "27.1.5",
"resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-27.1.5.tgz",
"integrity": "sha512-Xv6jBQPoBEvBq/5i2TeSG9tt/nqkbpcurrEG1b+2yfBrcJelOZF9Ml6dmyMh7bcW9JyFbRYpR5rxROSlBLTZHA==",
"dev": true,
"license": "MIT",
"dependencies": {
"bs-logger": "0.x",
"fast-json-stable-stringify": "2.x",
"jest-util": "^27.0.0",
"json5": "2.x",
"lodash.memoize": "4.x",
"make-error": "1.x",
"semver": "7.x",
"yargs-parser": "20.x"
},
"bin": {
"ts-jest": "cli.js"
},
"engines": {
"node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0"
},
"peerDependencies": {
"@babel/core": ">=7.0.0-beta.0 <8",
"@types/jest": "^27.0.0",
"babel-jest": ">=27.0.0 <28",
"jest": "^27.0.0",
"typescript": ">=3.8 <5.0"
},
"peerDependenciesMeta": {
"@babel/core": {
"optional": true
},
"@types/jest": {
"optional": true
},
"babel-jest": {
"optional": true
},
"esbuild": {
"optional": true
}
}
},
"packages/solid-type-index/node_modules/typescript": {
"version": "4.9.5",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz",
"integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==",
"dev": true,
"license": "Apache-2.0",
"peer": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
},
"engines": {
"node": ">=4.2.0"
}
},
"packages/solid/node_modules/ts-jest": {
"version": "27.1.5",
"dev": true,

@ -533,7 +533,6 @@ describe("Integration Tests", () => {
SAMPLE_DATA_URI,
]);
useSubscribeToResource(...subscribedUris);
const resource1 = useResource(SAMPLE_DATA_URI);
const resource2 = useResource(SAMPLE_BINARY_URI);
const post = useSubject(PostShShapeType, `${SAMPLE_DATA_URI}#Post1`);

@ -4,6 +4,7 @@ import fetch from "cross-fetch";
export const SERVER_DOMAIN = process.env.SERVER || "http://localhost:3001/";
export const ROOT_ROUTE = process.env.ROOT_CONTAINER || "example/";
export const ROOT_CONTAINER = `${SERVER_DOMAIN}${ROOT_ROUTE}`;
export const WEB_ID = `${SERVER_DOMAIN}${ROOT_ROUTE}profile/card#me`;
export const TEST_CONTAINER_SLUG = "test_ldo/";
export const TEST_CONTAINER_URI =
`${ROOT_CONTAINER}${TEST_CONTAINER_SLUG}` as ContainerUri;

@ -1,11 +1,11 @@
import { ContextDefinition } from "jsonld";
import { LdoJsonldContext } from "@ldo/jsonld-dataset-proxy";
/**
* =============================================================================
* profileContext: JSONLD Context for profile
* =============================================================================
*/
export const profileContext: ContextDefinition = {
export const profileContext: LdoJsonldContext = {
privateTypeIndex: {
"@id": "http://www.w3.org/ns/solid/terms#privateTypeIndex",
"@type": "@id",

@ -1,11 +1,11 @@
import { ContextDefinition } from "jsonld";
import { LdoJsonldContext } from "@ldo/jsonld-dataset-proxy";
/**
* =============================================================================
* typeIndexContext: JSONLD Context for typeIndex
* =============================================================================
*/
export const typeIndexContext: ContextDefinition = {
export const typeIndexContext: LdoJsonldContext = {
TypeIndex: {
"@id": "http://www.w3.org/ns/solid/terms#TypeIndex",
"@context": {

@ -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();
});

@ -1,47 +1,49 @@
import type { ContainerUri, LeafUri } from "@ldo/solid";
import fetch from "cross-fetch";
export const SERVER_DOMAIN = process.env.SERVER || "http://localhost:3001/";
export const SERVER_DOMAIN = process.env.SERVER || "http://localhost:3003/";
export const ROOT_ROUTE = process.env.ROOT_CONTAINER || "example/";
export const ROOT_CONTAINER = `${SERVER_DOMAIN}${ROOT_ROUTE}`;
export const TEST_CONTAINER_SLUG = "test_ldo/";
export const TEST_CONTAINER_URI =
`${ROOT_CONTAINER}${TEST_CONTAINER_SLUG}` as ContainerUri;
export const SAMPLE_DATA_URI = `${TEST_CONTAINER_URI}sample.ttl` as LeafUri;
export const SAMPLE2_DATA_SLUG = "sample2.ttl";
export const SAMPLE2_DATA_URI =
`${TEST_CONTAINER_URI}${SAMPLE2_DATA_SLUG}` as LeafUri;
export const SAMPLE_BINARY_URI = `${TEST_CONTAINER_URI}sample.txt` as LeafUri;
export const SAMPLE2_BINARY_SLUG = `sample2.txt`;
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 EXAMPLE_POST_TTL = `@prefix schema: <http://schema.org/> .
export const PROFILE_CONTAINER = `${ROOT_CONTAINER}profile/`;
export const WEB_ID = `${PROFILE_CONTAINER}card.ttl#me`;
export const PUBLIC_TYPE_INDEX_URI = `${PROFILE_CONTAINER}publicTypeIndex.ttl`;
export const PRIVATE_TYPE_INDEX_URI = `${PROFILE_CONTAINER}privateTypeIndex.ttl`;
export const MY_BOOKMARKS_CONTAINER = `${ROOT_CONTAINER}myBookmarks/`;
export const MY_BOOKMARKS_1_URI = `${ROOT_CONTAINER}myBookmarks/bookmark1.ttl`;
export const MY_BOOKMARKS_2_URI = `${ROOT_CONTAINER}myBookmarks/bookmark2.ttl`;
<#Post1>
a schema:CreativeWork, schema:Thing, schema:SocialMediaPosting ;
schema:image <https://example.com/postImage.jpg> ;
schema:articleBody "test" ;
schema:publisher <https://example.com/Publisher1>, <https://example.com/Publisher2> .`;
export const TEST_CONTAINER_TTL = `@prefix dc: <http://purl.org/dc/terms/>.
@prefix ldp: <http://www.w3.org/ns/ldp#>.
@prefix posix: <http://www.w3.org/ns/posix/stat#>.
@prefix xsd: <http://www.w3.org/2001/XMLSchema#>.
export const PROFILE_TTL = `
<#me> <http://www.w3.org/ns/solid/terms#publicTypeIndex> <${PROFILE_CONTAINER}publicTypeIndex.ttl> ;
<http://www.w3.org/ns/solid/terms#privateTypeIndex> <${PROFILE_CONTAINER}privateTypeIndex.ttl> .`;
export const PUBLIC_TYPE_INDEX_TTL = `@prefix solid: <http://www.w3.org/ns/solid/terms#>.
@prefix vcard: <http://www.w3.org/2006/vcard/ns#>.
@prefix bk: <http://www.w3.org/2002/01/bookmark#>.
<> <urn:npm:solid:community-server:http:slug> "sample.txt";
a ldp:Container, ldp:BasicContainer, ldp:Resource;
dc:modified "2023-10-20T13:57:14.000Z"^^xsd:dateTime.
<sample.ttl> a ldp:Resource, <http://www.w3.org/ns/iana/media-types/text/turtle#Resource>;
dc:modified "2023-10-20T13:57:14.000Z"^^xsd:dateTime.
<sample.txt> a ldp:Resource, <http://www.w3.org/ns/iana/media-types/text/plain#Resource>;
dc:modified "2023-10-20T13:57:14.000Z"^^xsd:dateTime.
<> posix:mtime 1697810234;
ldp:contains <sample.ttl>, <sample.txt>.
<sample.ttl> posix:mtime 1697810234;
posix:size 522.
<sample.txt> posix:mtime 1697810234;
posix:size 10.`;
<>
a solid:TypeIndex ;
a solid:ListedDocument.
<#ab09fd> a solid:TypeRegistration;
solid:forClass vcard:AddressBook;
solid:instance <https://example.com/myPublicAddressBook.ttl>.
<#bq1r5e> a solid:TypeRegistration;
solid:forClass bk:Bookmark;
solid:instanceContainer <${ROOT_CONTAINER}myBookmarks/>.`;
export const PRIVATE_TYPE_INDEX_TTL = `@prefix solid: <http://www.w3.org/ns/solid/terms#>.
@prefix vcard: <http://www.w3.org/2006/vcard/ns#>.
@prefix bk: <http://www.w3.org/2002/01/bookmark#>.
<>
a solid:TypeIndex ;
a solid:UnlistedDocument.
<#ab09fd> a solid:TypeRegistration;
solid:forClass vcard:AddressBook;
solid:instance <https://example.com/myPrivateAddressBook.ttl>.
<#bq1r5e> a solid:TypeRegistration;
solid:forClass bk:Bookmark;
solid:instanceContainer <${ROOT_CONTAINER}myBookmarks/>.`;
export interface SetUpServerReturn {
authFetch: typeof fetch;
@ -65,44 +67,53 @@ export function setUpServer(): SetUpServerReturn {
beforeEach(async () => {
s.fetchMock = jest.fn(s.authFetch);
// Create a new document called sample.ttl
await s.authFetch(WEB_ID, { method: "DELETE" });
await s.authFetch(ROOT_CONTAINER, {
method: "POST",
headers: {
link: '<http://www.w3.org/ns/ldp#Container>; rel="type"',
slug: TEST_CONTAINER_SLUG,
slug: "myBookmarks/",
},
});
await Promise.all([
s.authFetch(TEST_CONTAINER_URI, {
s.authFetch(PROFILE_CONTAINER, {
method: "POST",
headers: { "content-type": "text/turtle", slug: "card.ttl" },
body: PROFILE_TTL,
}),
s.authFetch(PROFILE_CONTAINER, {
method: "POST",
headers: { "content-type": "text/turtle", slug: "publicTypeIndex.ttl" },
body: PUBLIC_TYPE_INDEX_TTL,
}),
s.authFetch(PROFILE_CONTAINER, {
method: "POST",
headers: {
"content-type": "text/turtle",
slug: "privateTypeIndex.ttl",
},
body: PRIVATE_TYPE_INDEX_TTL,
}),
s.authFetch(MY_BOOKMARKS_CONTAINER, {
method: "POST",
headers: { "content-type": "text/turtle", slug: "sample.ttl" },
body: EXAMPLE_POST_TTL,
headers: { "content-type": "text/turtle", slug: "bookmark1.ttl" },
body: "",
}),
s.authFetch(TEST_CONTAINER_URI, {
s.authFetch(MY_BOOKMARKS_CONTAINER, {
method: "POST",
headers: { "content-type": "text/plain", slug: "sample.txt" },
body: "some text.",
headers: { "content-type": "text/turtle", slug: "bookmark2.ttl" },
body: "",
}),
]);
});
afterEach(async () => {
await Promise.all([
s.authFetch(SAMPLE_DATA_URI, {
method: "DELETE",
}),
s.authFetch(SAMPLE2_DATA_URI, {
method: "DELETE",
}),
s.authFetch(SAMPLE_BINARY_URI, {
method: "DELETE",
}),
s.authFetch(SAMPLE2_BINARY_URI, {
method: "DELETE",
}),
s.authFetch(SAMPLE_CONTAINER_URI, {
method: "DELETE",
}),
await s.authFetch(WEB_ID, { method: "DELETE" }),
await s.authFetch(PUBLIC_TYPE_INDEX_URI, { method: "DELETE" }),
await s.authFetch(PRIVATE_TYPE_INDEX_URI, { method: "DELETE" }),
await s.authFetch(MY_BOOKMARKS_1_URI, { method: "DELETE" }),
await s.authFetch(MY_BOOKMARKS_2_URI, { method: "DELETE" }),
]);
});

@ -26,7 +26,7 @@ export async function createApp(): Promise<App> {
),
variableBindings: {},
shorthand: {
port: 3_001,
port: 3_003,
loggingLevel: "off",
seedConfig: path.join(__dirname, "configs", "solid-css-seed.json"),
},

Loading…
Cancel
Save