テストする
定義は純粋なので、parseUrlやhrefはそのまま確かめられます。一方でuseAppStateはブラウザのAPIの上で動き、ストアはテストをまたいで残ります。このページでは、テストの側で用意しておくことを説明します。
このページの内容
純粋な関数を確かめる
parseUrlとhref、searchのほか、CookieのparseCookiesとcookieValueも、ブラウザ無しで呼べます。Nodeで動くふつうのテストで確かめられます。
state.test.tsimport { 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()で登録表を空にしないと、前のテストの状態が次のテストに持ち越されます。
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イベントを受け止めます。
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を自分で出します。
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()を呼べば、それがほかのタブからの書き込みの代わりになります。値は、ブラウザのストアと同じくパーセントエンコードして書きます。
await cookieStore.set(
density.cookieName,
encodeURIComponent(density.cookieValue({ density: 'compact' })),
);