ただの備忘録。

react-infinite-scrollerと呼ばれるライブラリを使っている箇所のテストについて。(2023/10/30時点)

技術スタック想定

MockedProviderを使った無限スクロールのモック方法

    import { MockedProvider } from "@apollo/client/testing";
import { render, screen, waitFor } from "@testing-library/react";
import { setupOffsetParent } from "./testUtils";
import { NameList } from ".";
// query名などはsampleなので適当

const firstResponse = {
  cursor: "dummyNextCursor",
  list: [
    {
      id: 1,
      name: "Maru",
    },
    {
      id: 2,
      name: "Bob",
    },
  ],
};

//1回目フェッチ検知用
const mockFirstResponse = jest.fn().mockReturnValue({
  data: {
    getNameList: firstResponse,
  },
});

const secondResponse = {
  cursor: null,
  list: [
    {
      id: 3,
      name: "Jeff",
    },
    {
      id: 4,
      name: "Jun",
    },
  ],
};

//2回目フェッチ検知用
const mockSecondResponse = jest.fn().mockReturnValue({
  data: {
    getNameList: secondResponse,
  },
});

//後で記載するが、react-infinite-scrollerを機能させるためにいくつかモックが必要
setupOffsetParent();
const graphqlMocks = [
  {
    request: {
      query: getNameListDocument,
      variables: {
        data: {
          cursor: null,
        },
      },
    },
    result: mockFirstResponse,
    delay: 500, //ページネーションの回数を正確に検知するために、1回目と2回目のフェッチにdelayをかける
  },
  {
    request: {
      query: getNameListDocument,
      variables: {
        data: {
          cursor: "dummyNextCursor", //2回目のリクエストではcursorが飛ぶ想定
        },
      },
    },
    result: mockSecondResponse,
    delay: 500, //ページネーションの回数を正確に検知するために、1回目と2回目のフェッチにdelayをかける
  },
];

//ここには記載しないが、addTypeNameとfieldPoliciesの設定が別途必要な場合がある
render(
  <MockedProvider mocks={graphqlMocks}>
    <NameList />
  </MockedProvider>,
);

//1回目のフェッチ検知
await waitFor(() => {
  expect(mockFirstResponse).toHaveBeenCalledTimes(1);
});
expect(mockSecondResponse).toHaveBeenCalledTimes(0);

//ページングは続いているので、ローディング表示は続いている感じ
expect(await screen.findByText("LOADING...")).toBeInTheDocument(); //表示確認(ケースによってアサーションは変える)

expect((await screen.findAllByRole("row")).length).toEqual(
  firstResponse.list.length + 1,
);

//loading表示が消えるまで待つ。消えた際に2回目のフェッチが行われたか検知
await waitFor(() => {
  expect(screen.queryByText("LOADING...")).not.toBeInTheDocument();
});
expect(mockSecondResponse).toHaveBeenCalledTimes(1);

//2回目のフェッチ完了後に表示は変わったのかチェック(内容は場合によって変更)
expect((await screen.findAllByRole("row")).length).toEqual(
  firstResponse.list.length + secondResponse.list.length + 1,
);

  

特筆しておきたいこと

react-infinite-scrollerが機能する方法と注意点

参考資料

react-infinite-scrollerの使用上、各要素のoffsetParentが存在していない場合機能してくれないです。

その上、jestでテストする場合はなんとoffsetParentがundefinedとなるのが普通らしい。

なので、react-infinite-scrollerをjest上で機能させる場合、offsetParentをモックさせることが必須となります。

テストするだけなんで、値は適当です。offsetTopだけ0にしちゃえばOK。(そもそもoffsetParent自体が大抵の場合Body要素になるはずなので)

    //infinite scrollを起動させるためにoffsetParentのモックが必要
export const setupOffsetParent = () => {
  Object.defineProperty(HTMLElement.prototype, 'offsetParent', {
    get() {
      return { ...this.parentNode, offsetTop: 0 };
    },
  });
};

  

ここまでやって思ったこと

infinite scrollのテストしたいけど、E2Eにはしたくない
そもそもテストしたいことは

というところを見たい
これだけならQueryのモックとtesting libraryでいけると思ってるが...

でも、領域としてはE2Eにした方が適切なんだろうなぁ。

だから多分Cypressとか使った方がよっぽど早いっす。そもそもjestの性質で無限スクロールを実行するのに必要な要素が消えちゃうのが問題なので。