@k8ordo/state

テストする

定義は純粋なので、parseUrlやhrefはそのまま確かめられます。一方でuseAppStateはブラウザのAPIの上で動き、ストアはテストをまたいで残ります。このページでは、テストの側で用意しておくことを説明します。

このページの内容

純粋な関数を確かめる

parseUrlとhref、searchのほか、CookieのparseCookiesとcookieValueも、ブラウザ無しで呼べます。Nodeで動くふつうのテストで確かめられます。

state.test.ts
import { expect, test } from 'vitest';

import { density, listState } from './state';

test('a page below 1 reads as the first page', () => {
  expect(listState.parseUrl({ page: '0' }).page).toBe(1);
});

test('a link leaves the defaults out', () => {
  expect(listState.href('/products', { page: 1 })).toBe('/products');
});

test('a compact cookie reads back as compact', () => {
  const cookies = new Map([
    [density.cookieName, density.cookieValue({ density: 'compact' })],
  ]);
  expect(density.parseCookies(cookies).density).toBe('compact');
});

parseUrlには、URLSearchParamsのほかに、フレームワークがページに渡すのと同じ形のオブジェクトも渡せます。parseCookiesには、Mapで作ったCookieをそのまま渡せます。

コンポーネントはブラウザで動かす

useAppStateを使うコンポーネントは、本物のNavigation APIとWeb Storage、Cookie Store APIの上で動きます。そのため、ブラウザの中でテストします。このパッケージ自身は、Vitestのブラウザモードでテストしています。

Cookieの状態を確かめるテストは、HTTPSで配信します。Cookie Store APIはSecureを必ず付け、WebKitはhttp://localhostでもそのCookieを捨てるからです。

テストのたびに状態を消す

ストアはProviderの中ではなく、モジュールの中の登録表に残ります。そのため、resetStateRegistry()で登録表を空にしないと、前のテストの状態が次のテストに持ち越されます。

ts
import { resetStateRegistry } from '@k8ordo/state';
import { afterEach } from 'vitest';

import { density, prefs } from './state';

afterEach(async () => {
  resetStateRegistry();
  localStorage.removeItem(prefs.storageKey);
  await cookieStore.delete(density.cookieName);
});

登録表を空にしても、ブラウザに保存した行は残ります。localStorageとsessionStorageの行は定義のstorageKeyで、CookieはcookieNameで消します。

警告

落とし穴

resetStateRegistry()の前に、コンポーネントをアンマウントしてください。マウントされたままのフックは、クロージャを通して古いストアを持ち続けます。

テストがルーターの代わりをする

URLを変えるupdate()は、navigation.navigate()を呼びます。誰も受け止めなければ、それは別のドキュメントの読み込みになり、テストを動かしているページごと移動してしまいます。そこで、ルーターの代わりにテスト自身がnavigateイベントを受け止めます。

ts
const interceptAsRouter = (event: NavigateEvent) => {
  if (event.canIntercept) event.intercept();
};

let home: string;

beforeEach(() => {
  home = location.href;
  navigation.addEventListener('navigate', interceptAsRouter);
});

afterEach(async () => {
  await navigation.navigate(home, { history: 'replace' }).finished;
  navigation.removeEventListener('navigate', interceptAsRouter);
});

テストが書き換えたURLは、次のテストの前に元に戻しておきます。戻すときも受け止める必要があるので、イベントの登録はURLを戻したあとに外します。

ほかのタブの書き込みを再現する

タブをまたぐ同期も、1つのページの中で確かめられます。

storageイベントは、書き込んだのとは別のタブにしか届きません。そのため、setItemで行を書いてから、StorageEventを自分で出します。

ts
localStorage.setItem(
  prefs.storageKey,
  JSON.stringify({ view: 'table', pageSize: 20 }),
);
window.dispatchEvent(
  new StorageEvent('storage', {
    key: prefs.storageKey,
    storageArea: localStorage,
  }),
);

一方でCookie Store APIのchangeイベントは、同じタブにも届きます。テストが自分でcookieStore.set()を呼べば、それがほかのタブからの書き込みの代わりになります。値は、ブラウザのストアと同じくパーセントエンコードして書きます。

ts
await cookieStore.set(
  density.cookieName,
  encodeURIComponent(density.cookieValue({ density: 'compact' })),
);
k8ordo

Baselineに入った機能を、制限なく使うReactのライブラリ群。

© 2026 k8o — MIT License

組版:Noto Sans JP / M PLUS 2