Newer
Older
smart-home-server / webclient-vue / src / composables / __tests__ / useAsyncRequest.spec.js
import { describe, it, expect, vi, beforeEach } from "vitest";
import { useAsyncRequest } from "../useAsyncRequest";

describe("useAsyncRequest", () => {
  beforeEach(() => {
    vi.useFakeTimers({ shouldAdvanceTime: true });
  });

  it("sets isLoading to true during execution and false after", async () => {
    const { isLoading, execute } = useAsyncRequest();

    const promise = execute(async () => {
      expect(isLoading.value).toBe(true);
      return { ok: true, data: [] };
    });

    expect(isLoading.value).toBe(true);
    await promise;
    expect(isLoading.value).toBe(false);
  });

  it("clears previous error before new execution", async () => {
    const { error, execute } = useAsyncRequest();

    await execute(async () => ({ ok: false, error: { message: "fail" } }));
    expect(error.value).toEqual({ message: "fail" });

    await execute(async () => ({ ok: true, data: [] }));
    expect(error.value).toBeNull();
  });

  it("sets error on failed result", async () => {
    const { error, execute } = useAsyncRequest();

    const result = await execute(async () => ({
      ok: false,
      error: { type: "api_error", message: "Bad request" },
    }));

    expect(result.ok).toBe(false);
    expect(error.value).toEqual({ type: "api_error", message: "Bad request" });
  });

  it("ignores timeout error by default", async () => {
    const { error, execute } = useAsyncRequest();

    const result = await execute(async () => ({
      ok: false,
      error: { type: "timeout", message: "Aborted" },
    }));

    expect(result.ok).toBe(false);
    expect(error.value).toBeNull();
  });

  it("records timeout error when ignoreTimeout is disabled", async () => {
    const { error, execute } = useAsyncRequest({ ignoreTimeout: false });

    const result = await execute(async () => ({
      ok: false,
      error: { type: "timeout", message: "Aborted" },
    }));

    expect(result.ok).toBe(false);
    expect(error.value).toEqual({ type: "timeout", message: "Aborted" });
  });

  it("sets network_error on thrown exception", async () => {
    const { error, execute } = useAsyncRequest();

    const result = await execute(async () => {
      throw new Error("Connection refused");
    });

    expect(result.ok).toBe(false);
    expect(result.error.type).toBe("network_error");
    expect(error.value.message).toBe("Connection refused");
  });

  it("aborts previous request before starting a new one", async () => {
    const { abortController, execute } = useAsyncRequest();

    let firstSignal;
    execute(async (signal) => {
      firstSignal = signal;
      await new Promise((resolve) => setTimeout(resolve, 100));
      return { ok: true, data: [] };
    });

    expect(abortController.value).not.toBeNull();
    expect(firstSignal.aborted).toBe(false);

    const secondPromise = execute(async () => ({ ok: true, data: [] }));
    expect(firstSignal.aborted).toBe(true);

    await secondPromise;
  });

  it("abort() cancels the current request", async () => {
    const { abortController, execute, abort } = useAsyncRequest();

    let signal;
    execute(async (s) => {
      signal = s;
      await new Promise((resolve) => setTimeout(resolve, 100));
      return { ok: true, data: [] };
    });

    abort();
    expect(signal.aborted).toBe(true);
    expect(abortController.value).toBeNull();
  });

  it("clear() resets error and aborts current request", async () => {
    const { error, abortController, execute, clear } = useAsyncRequest();

    await execute(async () => ({
      ok: false,
      error: { message: "fail" },
    }));
    expect(error.value).not.toBeNull();

    let signal;
    execute(async (s) => {
      signal = s;
      await new Promise((resolve) => setTimeout(resolve, 100));
      return { ok: true, data: [] };
    });

    clear();
    expect(error.value).toBeNull();
    expect(signal.aborted).toBe(true);
    expect(abortController.value).toBeNull();
  });
});