diff --git a/backend/src/api/routes/index.ts b/backend/src/api/routes/index.ts index 61a3ff2f5b98..fe53800a4b9b 100644 --- a/backend/src/api/routes/index.ts +++ b/backend/src/api/routes/index.ts @@ -140,7 +140,12 @@ function applyDevApiRoutes(app: Application): void { app.use("/configure", expressStatic(join(__dirname, "../../../private"))); app.use(async (req, res, next) => { - const slowdown = (await getLiveConfiguration()).dev.responseSlowdownMs; + let slowdown = (await getLiveConfiguration()).dev.responseSlowdownMs; + if (req.path.includes("connection")) slowdown *= 2.2; + if (req.path.includes("presets")) slowdown *= 1.6; + if (req.path.includes("config")) slowdown *= 1.2; + if (req.path.includes("configuration")) slowdown *= 0.5; + if (slowdown > 0) { Logger.info( `Simulating ${slowdown}ms delay for ${req.method} ${req.path}`, diff --git a/frontend/__tests__/components/AsyncContent.spec.tsx b/frontend/__tests__/components/AsyncContent.spec.tsx index 613dd48d2851..37ff6d0e72ce 100644 --- a/frontend/__tests__/components/AsyncContent.spec.tsx +++ b/frontend/__tests__/components/AsyncContent.spec.tsx @@ -2,25 +2,12 @@ import { describe, it, expect } from "vitest"; import { render, screen, waitFor } from "@solidjs/testing-library"; import { createResource, Resource } from "solid-js"; import AsyncContent from "../../src/ts/components/common/AsyncContent"; +import { + createLoadingStore, + LoadingStore, +} from "../../src/ts/signals/util/loadingStore"; describe("AsyncContent", () => { - function renderWithResource( - resource: Resource, - errorMessage?: string, - ): { - container: HTMLElement; - } { - const { container } = render(() => ( - - {(data) =>
{String(data)}
} -
- )); - - return { - container, - }; - } - it("renders loading state while resource is pending", () => { const [resource] = createResource(async () => { await new Promise((resolve) => setTimeout(resolve, 100)); @@ -75,4 +62,94 @@ describe("AsyncContent", () => { expect(screen.getByText(/An error occurred/)).toBeInTheDocument(); }); }); + + it("renders loading state while loadingStore is pending", () => { + const loadingStore = createLoadingStore( + "test", + async () => { + await new Promise((resolve) => setTimeout(resolve, 100)); + return { data: "data" }; + }, + () => ({}), + ); + + const { container } = renderWithLoadingStore(loadingStore); + + const preloader = container.querySelector(".preloader"); + expect(preloader).toBeInTheDocument(); + expect(preloader).toHaveClass("preloader"); + expect(preloader?.querySelector("i")).toHaveClass( + "fas", + "fa-fw", + "fa-spin", + "fa-circle-notch", + ); + }); + + it("renders data when loadingStore resolves", async () => { + const loadingStore = createLoadingStore<{ data?: string }>( + "test", + async () => { + return { data: "Test Data" }; + }, + () => ({}), + ); + + renderWithLoadingStore(loadingStore); + + await waitFor(() => { + expect(screen.getByTestId("content")).toHaveTextContent("Test Data"); + }); + }); + + it("renders error message when loadingStore fails", async () => { + const loadingStore = createLoadingStore( + "test", + async () => { + throw new Error("Test error"); + }, + () => ({}), + ); + + renderWithLoadingStore(loadingStore, "Custom error message"); + + await waitFor(() => { + expect(screen.getByText(/Custom error message/)).toBeInTheDocument(); + }); + }); + + function renderWithResource( + resource: Resource, + errorMessage?: string, + ): { + container: HTMLElement; + } { + const { container } = render(() => ( + + {(data) =>
{String(data)}
} +
+ )); + + return { + container, + }; + } + + function renderWithLoadingStore( + loadingStore: LoadingStore<{ data?: string }>, + errorMessage?: string, + ): { + container: HTMLElement; + } { + loadingStore.load(); + const { container } = render(() => ( + + {(data) =>
{data.data}
} +
+ )); + + return { + container, + }; + } }); diff --git a/frontend/__tests__/signals/util/loadingStore.spec.ts b/frontend/__tests__/signals/util/loadingStore.spec.ts new file mode 100644 index 000000000000..7040ced8850e --- /dev/null +++ b/frontend/__tests__/signals/util/loadingStore.spec.ts @@ -0,0 +1,134 @@ +import { createLoadingStore } from "../../../src/ts/signals/util/loadingStore"; +import { vi, describe, it, expect, beforeEach } from "vitest"; + +const mockFetcher = vi.fn(); +const initialValue = vi.fn(() => ({ data: null })); + +describe("createLoadingStore", () => { + beforeEach(() => { + mockFetcher.mockClear(); + initialValue.mockClear(); + }); + + it("should initialize with the correct state", () => { + const store = createLoadingStore("test", mockFetcher, initialValue); + + expect(store.state().state).toBe("unresolved"); + expect(store.state().loading).toBe(false); + expect(store.state().ready).toBe(false); + expect(store.state().refreshing).toBe(false); + expect(store.state().error).toBeUndefined(); + expect(store.store).toEqual({ data: null }); + }); + + it("should transition to loading when load is called", async () => { + const store = createLoadingStore("test", mockFetcher, initialValue); + store.load(); + + expect(store.state().state).toBe("pending"); + expect(store.state().loading).toBe(true); + }); + + it("should enable loading if ready is called", async () => { + const store = createLoadingStore("test", mockFetcher, initialValue); + mockFetcher.mockResolvedValueOnce({ data: "test" }); + + await store.ready(); + }); + + it("should call the fetcher when load is called", async () => { + const store = createLoadingStore("test", mockFetcher, initialValue); + mockFetcher.mockResolvedValueOnce({ data: "test" }); + store.load(); + + await store.ready(); + + expect(mockFetcher).toHaveBeenCalledTimes(1); + expect(store.state().state).toBe("ready"); + expect(store.store).toEqual({ data: "test" }); + }); + + it("should handle error when fetcher fails", async () => { + mockFetcher.mockRejectedValueOnce(new Error("Failed to load")); + const store = createLoadingStore("test", mockFetcher, initialValue); + + store.load(); + + await expect(store.ready()).rejects.toThrow("Failed to load"); + + expect(store.state().state).toBe("errored"); + expect(store.state().error).toEqual(new Error("Failed to load")); + }); + + it("should transition to refreshing state on refresh", async () => { + const store = createLoadingStore("test", mockFetcher, initialValue); + mockFetcher.mockResolvedValueOnce({ data: "test" }); + store.load(); + + store.refresh(); // trigger refresh + expect(store.state().state).toBe("refreshing"); + expect(store.state().refreshing).toBe(true); + }); + + it("should trigger load when refresh is called and shouldLoad is false", async () => { + const store = createLoadingStore("test", mockFetcher, initialValue); + mockFetcher.mockResolvedValueOnce({ data: "test" }); + expect(store.state().state).toBe("unresolved"); + + store.refresh(); + expect(store.state().state).toBe("refreshing"); + expect(store.state().refreshing).toBe(true); + + // Wait for the store to be ready after fetching + await store.ready(); + + // Ensure the store's state is 'ready' after the refresh + expect(store.state().state).toBe("ready"); + expect(store.store).toEqual({ data: "test" }); + }); + + it("should reset the store to its initial value on reset", async () => { + const store = createLoadingStore("test", mockFetcher, initialValue); + mockFetcher.mockResolvedValueOnce({ data: "test" }); + store.load(); + + await store.ready(); + + expect(store.store).toEqual({ data: "test" }); + + store.reset(); + expect(store.state().state).toBe("unresolved"); + expect(store.state().loading).toBe(false); + expect(store.store).toEqual({ data: null }); + }); + + it("should handle a promise rejection during reset", async () => { + const store = createLoadingStore("test", mockFetcher, initialValue); + + // Mock the fetcher to resolve with data + mockFetcher.mockResolvedValueOnce({ data: "test" }); + + // Trigger loading the store + store.load(); + + // Wait for the store to be ready + await store.ready(); + + // Ensure the store state after loading + expect(store.state().state).toBe("ready"); + expect(store.store).toEqual({ data: "test" }); + + // Now call reset, which should reject the ready promise + const readyPromise = store.ready(); // Grab the current ready promise + + store.reset(); // Call reset, which should reject the promise + + // Ensure the promise rejects as expected + await expect(readyPromise).rejects.toThrow("Reset"); + + // Ensure the state is reset + expect(store.state().state).toBe("unresolved"); + expect(store.state().loading).toBe(false); + expect(store.store).toEqual({ data: null }); + }); +}); diff --git a/frontend/src/html/pages/account.html b/frontend/src/html/pages/account.html index b1d7a856c0f7..7ffd78d2a27a 100644 --- a/frontend/src/html/pages/account.html +++ b/frontend/src/html/pages/account.html @@ -1,4 +1,5 @@