Files
buzz/desktop/src/features/messages/lib/timelineLoadingState.test.mjs
thomaspblock 00e61eafa9 fix(desktop): surface channel history load failures (#7013)
## Summary

A failed initial channel-history request no longer appears as an
authoritative empty channel. The timeline now shows an announced error
with a Retry action, while cached messages remain visible when a later
refresh fails; successful empty channels continue to use their normal
intro state.

### Related issue

None found.

### Testing

- Full desktop unit suite (`pnpm test`)
- Desktop TypeScript check (`pnpm exec tsc --noEmit`)
- Biome checks for changed files
- Repository file-size ratchet
- Full pre-push desktop checks and tests
- Desktop app launched successfully against local Postgres and Redis for
manual testing

No screenshot is included because the new UI is only shown after a
terminal relay-history failure; the regression test pins the
error/empty/list precedence directly.

---------

Signed-off-by: Thomas Petersen <thomasp@squareup.com>
2026-08-29 03:23:25 +00:00

262 lines
7.8 KiB
JavaScript

import assert from "node:assert/strict";
import test from "node:test";
import { QueryClient, QueryObserver } from "@tanstack/react-query";
import {
resolveTimelineLoadingLatch,
resolveTimelineQueryLoadingState,
selectTimelineLoadingState,
} from "./timelineLoadingState.ts";
const settled = {
isPending: false,
isFetching: false,
isPlaceholderData: false,
dataLength: null,
};
function deferred() {
let resolve;
let reject;
const promise = new Promise((resolvePromise, rejectPromise) => {
resolve = resolvePromise;
reject = rejectPromise;
});
return { promise, reject, resolve };
}
function resolveQueryLoading(result, settledChannelId = null) {
return resolveTimelineQueryLoadingState(settledChannelId, "chan-a", {
isEnabled: true,
isPending: result.isPending,
isFetching: result.isFetching,
isPlaceholderData: result.isPlaceholderData,
dataLength: result.data?.length ?? null,
isError: result.isError,
});
}
test("pending first fetch with no cache is loading", () => {
assert.equal(
selectTimelineLoadingState({ ...settled, isPending: true }),
true,
);
});
test("stale placeholder while refetching is loading", () => {
// Revisited within gcTime: placeholderData hands back a cached array while the
// authoritative fetch runs. Must keep the skeleton up, not flash the intro.
assert.equal(
selectTimelineLoadingState({
...settled,
isFetching: true,
isPlaceholderData: true,
dataLength: 0,
}),
true,
);
});
test("subscription-seeded empty cache while fetching is loading", () => {
// The live subscription's setQueryData seeds [] before history settles, so
// data is defined but empty and a fetch is still in flight.
assert.equal(
selectTimelineLoadingState({
...settled,
isFetching: true,
isPlaceholderData: false,
dataLength: 0,
}),
true,
);
});
test("settled with rows is not loading", () => {
assert.equal(
selectTimelineLoadingState({ ...settled, dataLength: 5 }),
false,
);
});
test("settled and genuinely empty is not loading (real empty channel)", () => {
assert.equal(
selectTimelineLoadingState({ ...settled, dataLength: 0 }),
false,
);
});
test("background refetch of a populated channel is not loading", () => {
// staleTime expiry can trigger a background refetch; with rows already present
// we are loaded and must not re-show the skeleton.
assert.equal(
selectTimelineLoadingState({
...settled,
isFetching: true,
dataLength: 12,
}),
false,
);
});
test("initial load holds the skeleton while the cold-load top-up fetches", () => {
// Cold load seeds rows before its row-floor top-up finishes, so dataLength>0
// while isFetching is still true. Before settle, hold the skeleton — dropping
// it here is what exposed the older-fetch spinner on first load.
assert.equal(
selectTimelineLoadingState(
{ ...settled, isFetching: true, dataLength: 8 },
false,
),
true,
);
});
test("pre-settle placeholder rows paint immediately (snapshot revisit)", () => {
// A revisit painting from the React-Query cache or a persisted snapshot:
// placeholder rows are a previously-settled timeline, not a partial cold
// load — show them stale-then-revalidate instead of a skeleton.
assert.equal(
selectTimelineLoadingState(
{ ...settled, isFetching: true, isPlaceholderData: true, dataLength: 8 },
false,
),
false,
);
});
test("pre-settle EMPTY placeholder still holds the skeleton", () => {
assert.equal(
selectTimelineLoadingState(
{ ...settled, isFetching: true, isPlaceholderData: true, dataLength: 0 },
false,
),
true,
);
});
test("settled channel with rows mid-refetch is not loading", () => {
// Same query shape, but after first settle: the latch owns refetch blips, so
// present rows mean loaded.
assert.equal(
selectTimelineLoadingState(
{ ...settled, isFetching: true, dataLength: 8 },
true,
),
false,
);
});
test("latch: loading on first entry to a channel", () => {
const r = resolveTimelineLoadingLatch(null, "chan-a", true);
assert.equal(r.isLoading, true);
assert.equal(r.settledChannelId, null);
});
test("latch: settles when loadingNow turns false, recording the channel", () => {
const r = resolveTimelineLoadingLatch(null, "chan-a", false);
assert.equal(r.isLoading, false);
assert.equal(r.settledChannelId, "chan-a");
});
test("latch: background refetch blip stays loaded once settled", () => {
// settled for chan-a, then loadingNow blips true (isFetching) — must NOT
// re-show the skeleton (the bounce Wes reported).
const r = resolveTimelineLoadingLatch("chan-a", "chan-a", true);
assert.equal(r.isLoading, false);
assert.equal(r.settledChannelId, "chan-a");
});
test("latch: switching channels resets and loads the new one", () => {
const r = resolveTimelineLoadingLatch("chan-a", "chan-b", true);
assert.equal(r.isLoading, true);
assert.equal(r.settledChannelId, "chan-a"); // not yet settled for b
});
test("latch: no active channel passes loadingNow through untouched", () => {
assert.equal(
resolveTimelineLoadingLatch("chan-a", null, true).isLoading,
true,
);
assert.equal(
resolveTimelineLoadingLatch("chan-a", null, false).isLoading,
false,
);
});
test("query wiring: cold error retry stays loading until successful empty result", async () => {
const client = new QueryClient({
defaultOptions: { queries: { retry: false } },
});
const first = deferred();
const retry = deferred();
let attempt = 0;
const observer = new QueryObserver(client, {
queryKey: ["messages", "chan-a"],
queryFn: () => [first.promise, retry.promise][attempt++],
});
const unsubscribe = observer.subscribe(() => {});
try {
first.reject(new Error("history unavailable"));
await new Promise((resolve) => setImmediate(resolve));
let loading = resolveQueryLoading(observer.getCurrentResult());
assert.equal(observer.getCurrentResult().status, "error");
assert.deepEqual(loading, { settledChannelId: null, isLoading: false });
const retryResult = observer.refetch();
loading = resolveQueryLoading(observer.getCurrentResult());
assert.equal(observer.getCurrentResult().status, "pending");
assert.deepEqual(loading, { settledChannelId: null, isLoading: true });
retry.resolve([]);
await retryResult;
loading = resolveQueryLoading(observer.getCurrentResult());
assert.equal(observer.getCurrentResult().status, "success");
assert.deepEqual(loading, {
settledChannelId: "chan-a",
isLoading: false,
});
} finally {
unsubscribe();
}
});
test("query wiring: repeated cold retry failure never settles as empty", async () => {
const client = new QueryClient({
defaultOptions: { queries: { retry: false } },
});
const first = deferred();
const retry = deferred();
let attempt = 0;
const observer = new QueryObserver(client, {
queryKey: ["messages", "chan-a"],
queryFn: () => [first.promise, retry.promise][attempt++],
});
const unsubscribe = observer.subscribe(() => {});
try {
first.reject(new Error("history unavailable"));
await new Promise((resolve) => setImmediate(resolve));
assert.deepEqual(resolveQueryLoading(observer.getCurrentResult()), {
settledChannelId: null,
isLoading: false,
});
const retryResult = observer.refetch();
assert.deepEqual(resolveQueryLoading(observer.getCurrentResult()), {
settledChannelId: null,
isLoading: true,
});
retry.reject(new Error("still unavailable"));
await retryResult;
assert.equal(observer.getCurrentResult().status, "error");
assert.deepEqual(resolveQueryLoading(observer.getCurrentResult()), {
settledChannelId: null,
isLoading: false,
});
} finally {
unsubscribe();
}
});