@seasketch/geoprocessing
Version:
Geoprocessing and reporting framework for SeaSketch 2.0
321 lines • 13.2 kB
JavaScript
import { test, expect, vi, beforeEach } from "vitest";
import React, { useState } from "react";
import { render, act as domAct } from "@testing-library/react";
import "@testing-library/jest-dom/vitest";
import { useFunction } from "./useFunction.js";
import { ReportContext } from "../context/index.js";
import { v4 as uuid } from "uuid";
import { GeoprocessingTaskStatus } from "../aws/tasks.js";
import { renderHook, act } from "@testing-library/react";
// switch to manual fetch mocking or vitest-fetch-mock
// import fetchMock from "fetch-mock-jest";
// import createFetchMock from "vitest-fetch-mock";
// const fetchMock = createFetchMock(vi);
const fetchMock = {};
const makeSketchProperties = (id) => {
id = id || uuid();
return {
id,
name: "sketch name",
updatedAt: new Date().toISOString(),
sketchClassId: "abc123",
};
};
const ContextWrapper = (props) => {
const sketchProperties = makeSketchProperties();
return (React.createElement(ReportContext.Provider, { value: {
geometryUri: `https://localhost/${sketchProperties.id}`,
sketchProperties,
projectUrl: "https://example.com/project",
...props.value,
visibleLayers: [],
language: "en",
} }, props.children));
};
// fetchMock.get("https://example.com/project", {
// geoprocessingServices: [
// {
// title: "calcFoo",
// endpoint: "https://example.com/calcFoo",
// },
// ],
// });
beforeEach(() => {
fetchMock.resetHistory();
});
test.skip("useFunction won't accept unrecognizable responses", async () => {
vi.useFakeTimers();
fetchMock.getOnce("*", {}, {
overwriteRoutes: true,
});
const { result } = renderHook(() => useFunction("calcFoo"), {
wrapper: ContextWrapper,
});
expect(result.current.loading).toBe(true);
await act(async () => {
vi.runAllTimers();
});
expect(fetchMock.calls(/calcFoo/).length).toBe(1);
expect(result.current.loading).toBe(false);
expect(result.current.error).toContain("response");
});
test.skip("useFunction unsets loading prop and sets task upon completion of job (executionMode=sync)", async () => {
vi.useFakeTimers();
const id = uuid();
fetchMock.getOnce("*", JSON.stringify({
startedAt: new Date().toISOString(),
duration: 10,
geometryUri: `https://example.com/calcFoo/${id}/geometry`,
location: `https://example.com/calcFoo/${id}`,
id,
logUriTemplate: `https://example.com/calcFoo/${id}/logs`,
service: "calcFoo",
wss: "",
status: "completed",
data: {
foo: "plenty",
},
}), { overwriteRoutes: true });
const { result } = renderHook(() => useFunction("calcFoo"), {
wrapper: ContextWrapper,
});
expect(result.current.loading).toBe(true);
await act(async () => {
vi.runAllTimers();
});
expect(fetchMock.calls(/calcFoo/).length).toBe(1);
expect(result.current.loading).toBe(false);
const task = result.current.task;
expect(task.data.foo).toBe("plenty");
expect(task.status).toBe(GeoprocessingTaskStatus.Completed);
expect(task.error).toBeUndefined();
});
test.skip("useFunction handles errors thrown within geoprocessing function", async () => {
vi.useFakeTimers();
const id = uuid();
fetchMock.getOnce("*", {
startedAt: new Date().toISOString(),
duration: 10,
geometryUri: `https://example.com/calcFoo/${id}/geometry`,
location: `https://example.com/calcFoo/${id}`,
id,
logUriTemplate: `https://example.com/calcFoo/${id}/logs`,
service: "calcFoo",
wss: "",
status: "failed",
error: "Task error",
}, { overwriteRoutes: true });
const { result } = renderHook(() => useFunction("calcFoo"), {
wrapper: ContextWrapper,
});
expect(result.current.loading).toBe(true);
await act(async () => {
vi.runAllTimers();
});
expect(fetchMock.calls(/calcFoo/).length).toBe(1);
expect(result.current.loading).toBe(false);
const task = result.current.task;
expect(task.error).toBe("Task error");
expect(result.current.error).toBe("Task error");
});
test.skip("throws error if ReportContext is not set", async () => {
const { result } = renderHook(() => useFunction("calcFoo"));
expect(result && result.current.error).toBeTruthy();
if (!result || !result.current.error)
return;
expect(result.current.error).toContain("ReportContext");
});
const TestReport = () => {
const { loading, error, task } = useFunction("calcFoo");
return (React.createElement("div", null,
loading && (React.createElement("span", { role: "alert", "aria-busy": "true" }, "loading...")),
error && React.createElement("span", { role: "alert" },
"Errors: ",
error),
task?.status === GeoprocessingTaskStatus.Completed && (React.createElement("span", { role: "alert", "data-results": task.data.foo }, "Task Complete!"))));
};
const TestContainer = (props) => {
const [sketchId, setSketchId] = useState(1);
return (React.createElement(ReportContext.Provider, { value: {
sketchProperties:
// @ts-expect-error type mismatch
props.sketchProperties || makeSketchProperties(sketchId.toString()),
geometryUri: `https://example.com/geometry/${sketchId}`,
projectUrl: "https://example.com/project",
visibleLayers: [],
language: "en",
} },
React.createElement("button", { onClick: () => setSketchId(sketchId + 1) }, "change sketch id"),
props.children));
};
test.skip("changing ReportContext.geometryUri fetches new results", async () => {
vi.useFakeTimers();
const id = uuid();
fetchMock.getOnce("*", JSON.stringify({
startedAt: new Date().toISOString(),
duration: 10,
geometryUri: `https://example.com/calcFoo/${id}/geometry`,
location: `https://example.com/calcFoo/${id}`,
id,
logUriTemplate: `https://example.com/calcFoo/${id}/logs`,
service: "calcFoo",
wss: "",
status: "completed",
data: {
foo: "plenty",
},
}), { overwriteRoutes: true });
const { getByRole, getByText, getAllByText } = render(React.createElement(TestContainer, null,
React.createElement(TestReport, null)));
expect(getByRole("alert")).toHaveTextContent("loading...");
await domAct(async () => {
vi.runAllTimers();
});
// expect(fetchMock.calls("https://example.com/project").length).toBe(1);
expect(fetchMock.calls(/calcFoo/).length).toBe(1);
expect(getAllByText(/task complete/i).length).toBe(1);
expect(getByText(/Task Complete/)).toHaveAttribute("data-results", "plenty");
expect(fetchMock.calls(/calcFoo/).length).toBe(1);
// now setup another mock because clicking the button will change the geometryUri
const id2 = uuid();
fetchMock.getOnce("*", JSON.stringify({
startedAt: new Date().toISOString(),
duration: 10,
geometryUri: `https://example.com/calcFoo/${id2}/geometry`,
location: `https://example.com/calcFoo/${id2}`,
id,
logUriTemplate: `https://example.com/calcFoo/${id2}/logs`,
service: "calcFoo",
wss: "",
status: "completed",
data: {
foo: "lots!",
},
}), { overwriteRoutes: true });
await domAct(async () => {
getByRole("button").click();
});
expect(getAllByText(/task complete/i).length).toBe(1);
expect(getByText(/Task Complete/)).toHaveAttribute("data-results", "lots!");
expect(fetchMock.calls(/calcFoo/).length).toBe(2);
});
const MultiCardTestReport = () => {
const { loading, error, task } = useFunction("calcFoo");
const task2 = useFunction("calcFoo");
return (React.createElement(React.Fragment, null,
React.createElement("div", null,
loading && (React.createElement("span", { role: "alert", "aria-busy": "true" }, "loading...")),
error && React.createElement("span", { role: "alert" },
"Errors: ",
error),
task?.status === GeoprocessingTaskStatus.Completed && (React.createElement("span", { role: "alert", "data-results": (task?.data).foo }, "Task Complete!"))),
React.createElement("div", { id: "task2" },
task2.loading && (React.createElement("span", { role: "alert", "aria-busy": "true" }, "loading...")),
task2.error && React.createElement("span", { role: "alert" },
"Errors: ",
task2.error),
task2.task?.status === GeoprocessingTaskStatus.Completed && (React.createElement("span", { role: "alert", "data-results": (task2.task?.data).foo }, "Task Complete!")))));
};
test.skip("useFunction called multiple times with the same arguments will only fetch once", async () => {
vi.useFakeTimers();
const id = uuid();
fetchMock.getOnce("*", JSON.stringify({
startedAt: new Date().toISOString(),
duration: 10,
geometryUri: `https://example.com/calcFoo/${id}/geometry`,
location: `https://example.com/calcFoo/${id}`,
id,
logUriTemplate: `https://example.com/calcFoo/${id}/logs`,
service: "calcFoo",
wss: "",
status: "completed",
data: {
foo: "plenty",
},
}), { overwriteRoutes: true });
const { getAllByRole, getAllByText } = render(React.createElement(TestContainer, null,
React.createElement(MultiCardTestReport, null)));
for (const el of getAllByRole("alert")) {
expect(el).toHaveTextContent("loading...");
}
await domAct(async () => {
vi.runAllTimers();
});
expect(fetchMock.calls(/calcFoo/).length).toBe(1);
expect(getAllByText(/task complete/i).length).toBe(2);
const completeEls = getAllByText(/task complete/i);
for (const el of completeEls) {
expect(el).toHaveAttribute("data-results", "plenty");
}
expect(fetchMock.calls(/calcFoo/).length).toBe(1);
});
test.skip("useFunction uses a local cache for repeat requests", async () => {
vi.useFakeTimers();
const id = uuid();
const sketchProperties = makeSketchProperties(id);
fetchMock.getOnce("*", JSON.stringify({
startedAt: new Date().toISOString(),
duration: 10,
geometryUri: `https://example.com/calcFoo/${id}/geometry`,
location: `https://example.com/calcFoo/${id}`,
id,
logUriTemplate: `https://example.com/calcFoo/${id}/logs`,
service: "calcFoo",
wss: "",
status: "completed",
data: {
foo: "plenty",
},
}), { overwriteRoutes: true });
const { getByRole, getByText, getAllByText } = render(
// @ts-expect-error type mismatch
React.createElement(TestContainer, { sketchProperties: sketchProperties },
React.createElement(TestReport, null)));
expect(getByRole("alert")).toHaveTextContent("loading...");
await domAct(async () => {
vi.runAllTimers();
});
expect(getAllByText(/task complete/i).length).toBe(1);
expect(getByText(/Task Complete/)).toHaveAttribute("data-results", "plenty");
expect(fetchMock.calls(/calcFoo/).length).toBe(1);
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const queries = render(
// @ts-expect-error type mismatch
React.createElement(TestContainer, { sketchProperties: sketchProperties },
React.createElement(TestReport, null)));
await domAct(async () => {
vi.runAllTimers();
});
expect(fetchMock.calls(/calcFoo/).length).toBe(1);
});
test.skip("Returns error if ReportContext does not include required values", () => {
const { result } = renderHook(() => useFunction("calcFoo"), {
wrapper: ({ children }) => (React.createElement(ContextWrapper
// eslint-disable-next-line react/no-children-prop
, {
// eslint-disable-next-line react/no-children-prop
children: children, value: { projectUrl: null } })),
});
expect(result.current.error).toContain("Client Error");
});
test.skip("Exposes error to client if project metadata can't be fetched", async () => {
vi.useFakeTimers();
fetchMock.get("https://example.com/project", 500, { overwriteRoutes: true });
useFunction.reset();
const { result } = renderHook(() => useFunction("calcFoo"), {
wrapper: ({ children }) => (React.createElement(ContextWrapper
// eslint-disable-next-line react/no-children-prop
, {
// eslint-disable-next-line react/no-children-prop
children: children, value: {
projectUrl: "https://example.com/project",
geometryUri: "https://example.com/geometry/123",
sketchProperties: {},
} })),
});
await act(async () => {
vi.runAllTimers();
});
expect(result.current.error).toContain("metadata");
});
//# sourceMappingURL=useFunction.test.js.map