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, ReportContextValue } from "../context/index.js";
import { v4 as uuid } from "uuid";
import { SketchProperties } from "../types/index.js";
import { GeoprocessingTaskStatus, GeoprocessingTask } 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: any = {};
const makeSketchProperties = (id?: string): SketchProperties => {
id = id || uuid();
return {
id,
name: "sketch name",
updatedAt: new Date().toISOString(),
sketchClassId: "abc123",
} as SketchProperties;
};
const ContextWrapper: React.FunctionComponent<{
children?: any;
value?: ReportContextValue;
}> = (props) => {
const sketchProperties = makeSketchProperties();
return (
{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",
},
} as GeoprocessingTask),
{ 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: GeoprocessingTask = 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",
} as GeoprocessingTask,
{ 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: GeoprocessingTask = 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 (
{loading && (
loading...
)}
{error && Errors: {error}}
{task?.status === GeoprocessingTaskStatus.Completed && (
Task Complete!
)}
);
};
interface TestContainerProps {
children: React.ReactNode;
}
const TestContainer: React.FC = (props) => {
const [sketchId, setSketchId] = useState(1);
return (
{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",
},
} as GeoprocessingTask),
{ overwriteRoutes: true },
);
const { getByRole, getByText, getAllByText } = render(
,
);
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!",
},
} as GeoprocessingTask),
{ 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 (
<>
{loading && (
loading...
)}
{error && Errors: {error}}
{task?.status === GeoprocessingTaskStatus.Completed && (
Task Complete!
)}
{task2.loading && (
loading...
)}
{task2.error && Errors: {task2.error}}
{task2.task?.status === GeoprocessingTaskStatus.Completed && (
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",
},
} as GeoprocessingTask),
{ overwriteRoutes: true },
);
const { getAllByRole, getAllByText } = render(
,
);
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",
},
} as GeoprocessingTask),
{ overwriteRoutes: true },
);
const { getByRole, getByText, getAllByText } = render(
// @ts-expect-error type mismatch
,
);
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
,
);
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 }) => (
),
});
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 }) => (
),
});
await act(async () => {
vi.runAllTimers();
});
expect(result.current.error).toContain("metadata");
});