import React, { useCallback, useEffect, useState } from "react";
import type { FunctionComponent } from "react";
import {
render,
screen,
fireEvent,
act,
cleanup,
} from "@testing-library/react";
import {
fileData,
MAIN_PROFILE_SUBJECT,
MAIN_PROFILE_URI,
SAMPLE_BINARY_URI,
SAMPLE_DATA_URI,
SERVER_DOMAIN,
setUpServerFiles,
THIRD_PROFILE_SUBJECT,
} from "./fileData.js";
import { UnauthenticatedSolidLdoProvider } from "../src/UnauthenticatedSolidLdoProvider.js";
import {
dataset,
useLdo,
useMatchObject,
useMatchSubject,
useResource,
useRootContainerFor,
useSubject,
useSubscribeToResource,
useLinkQuery,
} from "../src/index.js";
import { PostShShapeType } from "./.ldo/post.shapeTypes.js";
import type { PostSh } from "./.ldo/post.typings.js";
import { SolidProfileShapeShapeType } from "./.ldo/solidProfile.shapeTypes.js";
import { changeData, commitData } from "@ldo/connected";
import type { SolidProfileShape } from "./.ldo/solidProfile.typings.js";
import { describe, vi, afterEach, expect, it } from "vitest";
import { setupServer } from "@ldo/test-solid-server";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
const __dirname = dirname(fileURLToPath(import.meta.url));
describe("Integration Tests", () => {
setupServer(
3002,
fileData,
join(
__dirname,
"configs",
"components-config",
"unauthenticatedServer.json",
),
true,
);
setUpServerFiles();
afterEach(() => {
dataset.forgetAllResources();
dataset.deleteMatches(undefined, undefined, undefined, undefined);
cleanup();
});
/**
* ===========================================================================
* 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
Loading
;
return {resource.status.type}
;
};
render(
,
);
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(undefined);
const resource = useResource(uri, { suppressInitialRead: true });
if (!resource)
return (
Undefined
setUri(SAMPLE_DATA_URI)}>Next
);
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 (
setShowComponent(!showComponent)}>
Show Component
{showComponent ?
:
Hidden
}
);
};
render(
,
);
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 Loading
;
return (
{resource.status.type}
setUri(SAMPLE_BINARY_URI)}>
Update URI
);
};
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");
});
});
/**
* ===========================================================================
* 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 ? {rootContainer?.uri}
: <>>;
};
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");
});
});
/**
* ===========================================================================
* useLdoMethods
* ===========================================================================
*/
describe("useLdoMethods", () => {
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"]}
: <>>;
};
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}
: <>>;
};
render(
,
);
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 {post.articleBody}
;
};
render(
,
);
await screen.findByText("test");
});
it("renders the set 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.toArray()[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 = vi.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}
{
post.articleBody = "bad";
post.publisher.add({ "@id": "example" });
}}
>
Attempt Change
);
};
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.",
);
expect(warn).toHaveBeenCalledWith(
"You've attempted to modify 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 [isSubscribed, setIsSubscribed] = useState(true);
const resource = useResource(SAMPLE_DATA_URI, {
subscribe: isSubscribed,
});
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> . }`,
headers: {
"Content-Type": "application/sparql-update",
},
});
}, []);
if (resource.isLoading() || !post) return loading
;
return (
{resource.isSubscribedToNotifications().toString()}
{post.publisher.map((publisher) => {
return {publisher["@id"]} ;
})}
Add Publisher
setIsSubscribed(false)}>Unsubscribe
);
};
const { unmount } = render(
,
);
// Wait for subscription to connect
await act(async () => {
await new Promise((resolve) => setTimeout(resolve, 1000));
});
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");
const resourceP = await screen.findByRole("resource");
expect(resourceP.innerHTML).toBe("true");
// 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",
);
await fireEvent.click(screen.getByText("Unsubscribe"));
const resourcePUpdated = await screen.findByRole("resource");
expect(resourcePUpdated.innerHTML).toBe("false");
unmount();
});
});
/**
* ===========================================================================
* useMatchSubject
* ===========================================================================
*/
describe("useMatchSubject", () => {
it("returns an array of matched subjects", async () => {
const UseMatchSubjectTest: FunctionComponent = () => {
const resource = useResource(SAMPLE_DATA_URI);
const posts = useMatchSubject(
PostShShapeType,
"http://www.w3.org/1999/02/22-rdf-syntax-ns#type",
"http://schema.org/CreativeWork",
);
if (resource.isLoading()) return loading
;
return (
{posts.map((post) => {
return {post["@id"]} ;
})}
);
};
render(
,
);
const list = await screen.findByRole("list");
expect(list.children[0].innerHTML).toBe(
"http://localhost:3002/example/test_ldo/sample.ttl#Post1",
);
expect(list.children[1].innerHTML).toBe(
"http://localhost:3002/example/test_ldo/sample.ttl#Post2",
);
});
});
/**
* ===========================================================================
* useMatchObject
* ===========================================================================
*/
describe("useMatchObject", () => {
it("returns an array of matched objects", async () => {
const UseMatchObjectTest: FunctionComponent = () => {
const resource = useResource(SAMPLE_DATA_URI);
const publishers = useMatchObject(
PostShShapeType,
"http://localhost:3002/example/test_ldo/sample.ttl#Post1",
"http://schema.org/publisher",
);
if (resource.isLoading()) return loading
;
return (
{publishers.map((publisher) => {
return {publisher["@id"]} ;
})}
);
};
render(
,
);
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");
});
});
/**
* ===========================================================================
* useSubscribeToResource
* ===========================================================================
*/
describe("useSubscribeToResource", () => {
it("handles useSubscribeToResource", async () => {
const NotificationTest: FunctionComponent = () => {
const [subscribedUris, setSubScribedUris] = useState([
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`);
const addPublisher = useCallback(async () => {
await fetch(SAMPLE_DATA_URI, {
method: "PATCH",
body: `INSERT DATA { <${SAMPLE_DATA_URI}#Post1> . }`,
headers: {
"Content-Type": "application/sparql-update",
},
});
}, []);
if (resource1.isLoading() || resource2.isLoading())
return Loading
;
return (
{resource1.isSubscribedToNotifications().toString()}
{resource2.isSubscribedToNotifications().toString()}
{post.publisher.map((publisher) => {
return {publisher["@id"]} ;
})}
Add Publisher
setSubScribedUris([SAMPLE_DATA_URI, SAMPLE_BINARY_URI])
}
>
Subscribe More
setSubScribedUris([SAMPLE_BINARY_URI])}>
Subscribe Less
);
};
const { unmount } = render(
,
);
const preResource1P = await screen.findByRole("resource1");
expect(preResource1P.innerHTML).toBe("false");
// Wait for subscription to connect
await act(async () => {
await new Promise((resolve) => setTimeout(resolve, 1000));
});
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");
const resource1P = await screen.findByRole("resource1");
expect(resource1P.innerHTML).toBe("true");
const resource2P = await screen.findByRole("resource2");
expect(resource2P.innerHTML).toBe("false");
// 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",
);
await fireEvent.click(screen.getByText("Subscribe More"));
// Wait for subscription to connect
await act(async () => {
await new Promise((resolve) => setTimeout(resolve, 1000));
});
const resource1PUpdated = await screen.findByRole("resource1");
expect(resource1PUpdated.innerHTML).toBe("true");
const resource2PUpdated = await screen.findByRole("resource2");
expect(resource2PUpdated.innerHTML).toBe("true");
await fireEvent.click(screen.getByText("Subscribe Less"));
// Wait for subscription to connect
await act(async () => {
await new Promise((resolve) => setTimeout(resolve, 1000));
});
const resource1PUpdatedAgain = await screen.findByRole("resource1");
expect(resource1PUpdatedAgain.innerHTML).toBe("false");
const resource2PUpdatedAgain = await screen.findByRole("resource2");
expect(resource2PUpdatedAgain.innerHTML).toBe("true");
unmount();
});
});
/**
* ===========================================================================
* useLinkQuery
* ===========================================================================
*/
describe("useLinkQuery", () => {
const linkQuery = {
name: true,
knows: {
name: true,
},
} as const;
it("Fetches a resource using useLinkQuery", async () => {
const UseLinkQueryTest: FunctionComponent = () => {
const profile = useLinkQuery(
SolidProfileShapeShapeType,
MAIN_PROFILE_URI,
MAIN_PROFILE_SUBJECT,
linkQuery,
);
const addProfile = useCallback(async () => {
const cProfile = changeData(
profile as SolidProfileShape,
dataset.getResource(MAIN_PROFILE_URI),
);
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
cProfile.knows?.add({ "@id": THIRD_PROFILE_SUBJECT });
await commitData(cProfile);
}, [profile]);
if (!profile) return Loading
;
return (
{profile.name}
{profile.knows?.map((nestedProfile) => (
{nestedProfile.name}
))}
Add Profile
);
};
const { unmount } = render(
,
);
await screen.findByText("Loading");
let profileNameElement = await screen.findByRole("profile-name");
expect(profileNameElement.textContent).toBe("Main User");
let list = await screen.findByRole("list");
expect(list.children[0].innerHTML).toBe("Other User");
expect(list.children.length).toBe(1);
// Click button to add a publisher
await fireEvent.click(screen.getByText("Add Profile"));
// Give some time for notifications to propogate
await act(async () => {
await new Promise((resolve) => setTimeout(resolve, 2000));
});
profileNameElement = await screen.findByRole("profile-name");
expect(profileNameElement.textContent).toBe("Main User");
list = await screen.findByRole("list");
expect(list.children[0].innerHTML).toBe("Other User");
expect(list.children[1].innerHTML).toBe("Third User");
expect(list.children.length).toBe(2);
unmount();
});
});
});