Closed4

[React] View Transitions API を使ったスライドショー

sprout2000sprout2000

カスタムフック

import { useEffect, useState } from "react";
import { flushSync } from "react-dom";

export const useSlideshow = (query: string, interval: number) => {
  const [url, setUrl] = useState(query);

  useEffect(() => {
    const timerId = setInterval(() => {
      fetch(query)
        .then((res) => {
          if (document.startViewTransition) {
            document.startViewTransition(() => {
              flushSync(() => {
                setUrl(res.url);
              });
            });
          } else {
            flushSync(() => {
              setUrl(res.url);
            });
          }
        })
        .catch((err) => console.error(`${err}`));
    }, interval);

    return () => {
      clearInterval(timerId);
    };
  }, [query, interval]);

  return [url, setUrl] as const;
};
sprout2000sprout2000

型宣言

export interface ViewTransition {
  ready: Promise<void>;
  finished: Promise<void>;
  updateCallbackDone: Promise<void>;
  skipTransition: () => undefined;
}

declare global {
  interface Document {
    startViewTransition?: (callback: () => void) => ViewTransition;
  }
}
sprout2000sprout2000

テストファイル

import "@testing-library/jest-dom";
import { act, render, screen } from "@testing-library/react";
import { useSlideshow } from "../useSlideshow";

vi.useFakeTimers();

const query = "https://unsplash.com/photos/bIhpiQA009k";

describe("useSlideshow", () => {
  beforeEach(() => {
    vi.clearAllTimers();
    vi.clearAllMocks();
    delete document.startViewTransition;
  });

  afterEach(() => {
    delete document.startViewTransition;
  });

  it("should update the url after specified interval without transition", async () => {
    const mockFetch = Promise.resolve({
      url: query,
    });

    global.fetch = vi.fn().mockResolvedValueOnce(mockFetch);

    function MockComponent() {
      const [url] = useSlideshow(query, 1000);
      return <div>{url}</div>;
    }

    render(<MockComponent />);
    await act(async () => {
      vi.advanceTimersByTime(1000);
    });

    expect(screen.getByText(query)).toBeInTheDocument();
    expect(document.startViewTransition).toBeUndefined();
  });

  it("should update the url after specified interval with transition", async () => {
    document.startViewTransition = vi.fn().mockImplementationOnce((cb) => {
      cb();
    });

    const mockFetch = Promise.resolve({
      url: query,
    });

    global.fetch = vi.fn().mockResolvedValueOnce(mockFetch);

    function MockComponent() {
      const [url] = useSlideshow(query, 1000);
      return <div>{url}</div>;
    }

    render(<MockComponent />);
    await act(async () => {
      vi.advanceTimersByTime(1000);
    });

    expect(screen.getByText(query)).toBeInTheDocument();
    expect(document.startViewTransition).toHaveBeenCalledTimes(1);
  });

  it("should catch error and log it to the console", async () => {
    const consoleSpy = vi.spyOn(console, "error");

    global.fetch = vi.fn().mockRejectedValueOnce(new Error("Fetch error"));

    function MockComponent() {
      const [url] = useSlideshow(query, 1000);
      return <div>{url}</div>;
    }

    render(<MockComponent />);
    await act(async () => {
      vi.advanceTimersByTime(1000);
    });

    expect(consoleSpy).toHaveBeenCalledWith("Error: Fetch error");
    consoleSpy.mockRestore();
  });
});
このスクラップは2023/04/07にクローズされました