feat: add quiet reading with rare confirmations
Part of the quiet-user-feedback change; docs and specs land in their own repositories.
This commit is contained in:
@@ -8,4 +8,8 @@ export {
|
||||
formatSnapshotTime,
|
||||
instrumentLogoUrl,
|
||||
} from "./display";
|
||||
export {
|
||||
type InitialAvailability,
|
||||
useInitialAvailability,
|
||||
} from "./initial-availability";
|
||||
export { useUrlPeriod } from "./useUrlPeriod";
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
import { act, renderHook } from "@testing-library/react";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
|
||||
import { useInitialAvailability } from "./initial-availability";
|
||||
|
||||
describe("useInitialAvailability", () => {
|
||||
it("keeps loading until content, error, or the initial wait budget", async () => {
|
||||
vi.useFakeTimers();
|
||||
|
||||
try {
|
||||
const { rerender, result } = renderHook(
|
||||
props => useInitialAvailability(props),
|
||||
{
|
||||
initialProps: {
|
||||
hasContent: false,
|
||||
identity: "account-a",
|
||||
isError: false,
|
||||
isFetching: true,
|
||||
timeoutMs: 100,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
expect(result.current).toBe("loading");
|
||||
await act(() => vi.advanceTimersByTimeAsync(99));
|
||||
expect(result.current).toBe("loading");
|
||||
await act(() => vi.advanceTimersByTimeAsync(1));
|
||||
expect(result.current).toBe("unavailable");
|
||||
|
||||
rerender({
|
||||
hasContent: true,
|
||||
identity: "account-a",
|
||||
isError: false,
|
||||
isFetching: false,
|
||||
timeoutMs: 100,
|
||||
});
|
||||
expect(result.current).toBe("ready");
|
||||
}
|
||||
finally {
|
||||
vi.useRealTimers();
|
||||
}
|
||||
});
|
||||
|
||||
it("does not flash loading during retry for the same identity", async () => {
|
||||
vi.useFakeTimers();
|
||||
|
||||
try {
|
||||
const { rerender, result } = renderHook(
|
||||
props => useInitialAvailability(props),
|
||||
{
|
||||
initialProps: {
|
||||
hasContent: false,
|
||||
identity: "account-a",
|
||||
isError: true,
|
||||
isFetching: false,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
await act(() => vi.runOnlyPendingTimersAsync());
|
||||
expect(result.current).toBe("unavailable");
|
||||
rerender({
|
||||
hasContent: false,
|
||||
identity: "account-a",
|
||||
isError: false,
|
||||
isFetching: true,
|
||||
});
|
||||
|
||||
expect(result.current).toBe("unavailable");
|
||||
}
|
||||
finally {
|
||||
vi.useRealTimers();
|
||||
}
|
||||
});
|
||||
|
||||
it("resets loading only when identity changes", async () => {
|
||||
vi.useFakeTimers();
|
||||
|
||||
try {
|
||||
const { rerender, result } = renderHook(
|
||||
props => useInitialAvailability(props),
|
||||
{
|
||||
initialProps: {
|
||||
hasContent: false,
|
||||
identity: "account-a",
|
||||
isError: true,
|
||||
isFetching: false,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
await act(() => vi.runOnlyPendingTimersAsync());
|
||||
expect(result.current).toBe("unavailable");
|
||||
rerender({
|
||||
hasContent: false,
|
||||
identity: "account-b",
|
||||
isError: false,
|
||||
isFetching: true,
|
||||
});
|
||||
|
||||
expect(result.current).toBe("loading");
|
||||
}
|
||||
finally {
|
||||
vi.useRealTimers();
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,49 @@
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
const INITIAL_WAIT_BUDGET_MS = 10_000;
|
||||
|
||||
type InitialAvailability
|
||||
= | "loading"
|
||||
| "ready"
|
||||
| "unavailable";
|
||||
|
||||
type InitialAvailabilityInput = {
|
||||
hasContent: boolean;
|
||||
identity: string;
|
||||
isError: boolean;
|
||||
isFetching: boolean;
|
||||
timeoutMs?: number;
|
||||
};
|
||||
|
||||
function useInitialAvailability(
|
||||
input: InitialAvailabilityInput,
|
||||
): InitialAvailability {
|
||||
const timeoutMs = input.timeoutMs ?? INITIAL_WAIT_BUDGET_MS;
|
||||
|
||||
const [unavailableIdentity, setUnavailableIdentity] = useState<null | string>(
|
||||
null,
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (input.hasContent) return;
|
||||
|
||||
if (input.isError) {
|
||||
const timer = setTimeout(() => setUnavailableIdentity(input.identity), 0);
|
||||
|
||||
return () => clearTimeout(timer);
|
||||
}
|
||||
|
||||
const timer = setTimeout(
|
||||
() => setUnavailableIdentity(input.identity),
|
||||
timeoutMs,
|
||||
);
|
||||
|
||||
return () => clearTimeout(timer);
|
||||
}, [input.hasContent, input.identity, input.isError, timeoutMs]);
|
||||
|
||||
if (input.hasContent) return "ready";
|
||||
|
||||
return unavailableIdentity === input.identity ? "unavailable" : "loading";
|
||||
}
|
||||
|
||||
export { type InitialAvailability, useInitialAvailability };
|
||||
Reference in New Issue
Block a user