From 66c36b6423cbd9e2df7a1be9f2e9a60232cfab43 Mon Sep 17 00:00:00 2001
From: Clayton
Date: Mon, 13 Jul 2026 06:43:52 -0500
Subject: [PATCH 1/2] feat(ui): add network-vs-API error distinction and
loading skeletons to StateBoundary
Several routes had minimal loading/error handling: no retry, no way to tell a
connectivity failure apart from a server-side error, and a loading state that
rendered identically to a genuinely-empty one. useApiResource now surfaces the
apiFetch failure kind and status; StateBoundary/ErrorState use it to show
distinct copy and iconography for "can't reach the server" vs "the server
returned an error", and accept an optional content-shaped loadingSkeleton in
place of the generic spinner. Both are additive and backward-compatible --
every existing StateBoundary/ErrorState/useApiResource consumer keeps its
current default copy unless it opts in.
Wires the new primitives into app.runs.tsx, the most minimal of the routes
named in the issue: a proper StateBoundary (retry, network-aware error,
loading skeleton) replaces the old ad-hoc warning banner and the misleading
loading state that showed "no runs match filters" before the first fetch
even completed, plus a RefreshMeta "last refresh" label in the header.
---
.../src/components/site/state-views.test.tsx | 138 +++++++++
.../src/components/site/state-views.tsx | 73 ++++-
.../src/lib/api/use-api-resource.test.ts | 40 +++
.../src/lib/api/use-api-resource.ts | 21 +-
apps/gittensory-ui/src/routes/app.runs.tsx | 275 ++++++++++--------
5 files changed, 406 insertions(+), 141 deletions(-)
create mode 100644 apps/gittensory-ui/src/components/site/state-views.test.tsx
diff --git a/apps/gittensory-ui/src/components/site/state-views.test.tsx b/apps/gittensory-ui/src/components/site/state-views.test.tsx
new file mode 100644
index 0000000000..9e8fe3c561
--- /dev/null
+++ b/apps/gittensory-ui/src/components/site/state-views.test.tsx
@@ -0,0 +1,138 @@
+import { fireEvent, render, screen } from "@testing-library/react";
+import { describe, expect, it, vi } from "vitest";
+
+const { notifyApiFailure } = vi.hoisted(() => ({ notifyApiFailure: vi.fn() }));
+vi.mock("@/lib/api/request", () => ({
+ notifyApiFailure: (...args: unknown[]) => notifyApiFailure(...args),
+}));
+vi.mock("sonner", () => ({ toast: Object.assign(vi.fn(), { success: vi.fn(), error: vi.fn() }) }));
+
+import { ErrorState, StateBoundary } from "@/components/site/state-views";
+
+describe("ErrorState network-vs-API distinction (#793)", () => {
+ it("uses the generic 'couldn't load' copy when no errorKind is given (unchanged default)", () => {
+ render();
+ expect(screen.getByText("Couldn't load this")).toBeTruthy();
+ });
+
+ it("uses connectivity-specific copy for a network errorKind", () => {
+ render();
+ expect(screen.getByText("Can't reach the server")).toBeTruthy();
+ });
+
+ it("uses connectivity-specific copy for a timeout errorKind too", () => {
+ render();
+ expect(screen.getByText("Can't reach the server")).toBeTruthy();
+ });
+
+ it("falls back to the generic copy for an http errorKind", () => {
+ render();
+ expect(screen.getByText("Couldn't load this")).toBeTruthy();
+ });
+
+ it("lets an explicit title/description override the errorKind-derived copy", () => {
+ render();
+ expect(screen.getByText("Custom title")).toBeTruthy();
+ expect(screen.getByText("Custom description")).toBeTruthy();
+ expect(screen.queryByText("Can't reach the server")).toBeNull();
+ });
+});
+
+describe("StateBoundary loadingSkeleton (#793)", () => {
+ it("renders the default spinner LoadingState when no skeleton is given", () => {
+ render(
+
+
content
+ ,
+ );
+ expect(screen.getByRole("status")).toBeTruthy();
+ expect(screen.queryByText("content")).toBeNull();
+ });
+
+ it("renders the provided skeleton instead of the spinner when loading", () => {
+ render(
+ placeholder}>
+
content
+ ,
+ );
+ expect(screen.getByTestId("skeleton")).toBeTruthy();
+ expect(screen.queryByRole("status")).toBeNull();
+ expect(screen.queryByText("content")).toBeNull();
+ });
+});
+
+describe("StateBoundary errorKind passthrough (#793)", () => {
+ it("keeps the pre-#793 default error copy when no errorKind is given", () => {
+ render(
+
+
content
+ ,
+ );
+ expect(screen.getByText("Couldn't load data")).toBeTruthy();
+ });
+
+ it("falls through to ErrorState's network-aware copy when errorKind is network and no override is given", () => {
+ render(
+
+
content
+ ,
+ );
+ expect(screen.getByText("Can't reach the server")).toBeTruthy();
+ expect(screen.queryByText("Couldn't load data")).toBeNull();
+ });
+
+ it("lets an explicit errorTitle/errorDescription win even with a network errorKind", () => {
+ render(
+
+
content
+ ,
+ );
+ expect(screen.getByText("Custom title")).toBeTruthy();
+ expect(screen.getByText("Custom description")).toBeTruthy();
+ });
+
+ it("passes the real errorKind (not a hardcoded 'network') to the error-failure notifier", () => {
+ render(
+
+
content
+ ,
+ );
+ expect(notifyApiFailure).toHaveBeenCalledWith(expect.objectContaining({ kind: "http" }));
+ });
+
+ it("defaults the notifier kind to 'network' when no errorKind is given, matching pre-#793 behavior", () => {
+ render(
+
+