@k8ordo/color-scheme

テストする

@k8ordo/color-schemeは、localStorageと<html>のクラス、matchMediaを使います。どれもブラウザにしか無いので、テストもVitestのブラウザモードのような本物のブラウザで動かします。このページでは、テストの間で状態を戻す方法と、プロバイダの下でフックを描いて確かめる方法を説明します。

このページの内容

テストの間で状態を戻す

プロバイダは、localStorageの行と<html>のクラス、@k8ordo/stateのストアを読み書きします。どれもテストをまたいで残るので、各テストの前に3つとも戻します。

color-scheme.test.tsx
import { resetStateRegistry } from '@k8ordo/state';
import { beforeEach } from 'vitest';

beforeEach(() => {
  localStorage.clear();
  document.documentElement.classList.remove('dark');
  resetStateRegistry();
});
  • localStorage.clear():前のテストで保存した選択を消します。残っていると、次のテストは何も選んでいない状態から始まりません。
  • classList.remove('dark'):前のテストでプロバイダのeffectが付けたクラスを外します。
  • resetStateRegistry():@k8ordo/stateのストアを捨てます。ストアはキーごとに1つ作られてテストをまたいで残るので、捨てないと前のテストの値を持ち続けます。
情報

メモ

resetStateRegistry()を呼ぶときは、フックを使うコンポーネントがアンマウントされている必要があります。マウントされたままのフックは、捨てたはずの古いストアを持ち続けるからです。vitest-browser-reactを使っていれば、前のテストの描画は各テストの前に片付けられます。

プロバイダの下でフックを描く

useColorScheme()はプロバイダの外では例外を投げるので、プロバイダをrenderHookのwrapperに渡して描きます。

color-scheme.test.tsx
import {
  ColorSchemeProvider,
  colorSchemeState,
  useColorScheme,
} from '@k8ordo/color-scheme';
import type { ReactNode } from 'react';
import { expect, it, vi } from 'vitest';
import { renderHook } from 'vitest-browser-react';

const wrapper = ({ children }: { children: ReactNode }) => (
  <ColorSchemeProvider>{children}</ColorSchemeProvider>
);

it('starts from a stored preference', async () => {
  localStorage.setItem(
    colorSchemeState.storageKey,
    JSON.stringify({ preference: 'dark' }),
  );
  const { result } = await renderHook(() => useColorScheme(), {
    wrapper,
  });

  expect(result.current.preference).toBe('dark');
  expect(result.current.scheme).toBe('dark');
  expect(
    document.documentElement.classList.contains('dark'),
  ).toBe(true);
});

localStorageのキーは、文字列で書かずにcolorSchemeState.storageKeyから読みます。キーの組み立て方を決めているのは@k8ordo/stateだからです。

選択を変えて確かめる

setPreferenceで選択を変えたら、vi.waitForで描き直しを待ってから確かめます。

color-scheme.test.tsx
it('stores a choice, and stores none for system', async () => {
  const { result } = await renderHook(() => useColorScheme(), {
    wrapper,
  });

  result.current.setPreference('dark');
  await vi.waitFor(() => {
    expect(result.current.scheme).toBe('dark');
  });
  expect(
    document.documentElement.classList.contains('dark'),
  ).toBe(true);
  expect(localStorage.getItem(colorSchemeState.storageKey)).toBe(
    '{"preference":"dark"}',
  );

  result.current.setPreference('system');
  await vi.waitFor(() => {
    expect(result.current.preference).toBe('system');
  });
  expect(
    localStorage.getItem(colorSchemeState.storageKey),
  ).toBe('{}');
});

setPreferenceを呼んでも、その場では描き直されません。localStorageへの書き込みも、マイクロタスクにまとめてから行われます。描き直しが済めば書き込みも済んでいるので、schemeやpreferenceが変わるのを待ってから、行とクラスを確かめます。

'system'に戻したときは行が消えるのではなく、preferenceの無い{}が残ります。

OSがダークのブラウザで確かめる

'system'がどちらに決まるかは、テストを動かすブラウザのprefers-color-schemeしだいです。Playwrightで起動したブラウザは、指定しなければライトを好みます。

既定値から始まることを確かめるだけなら、defaultPreference="dark"を渡したプロバイダをwrapperにすれば足ります。OSの設定に従うことそのものを確かめたいときは、ブラウザのほうをダークにします。

vite.config.ts
import { playwright } from '@vitest/browser-playwright';
import { defineConfig } from 'vitest/config';

export default defineConfig({
  test: {
    browser: {
      enabled: true,
      provider: playwright({
        contextOptions: { colorScheme: 'dark' },
      }),
      instances: [{ browser: 'chromium' }],
    },
  },
});

この設定は、そのブラウザで動くすべてのテストにかかります。

インラインスクリプトは走らない

テストのようにブラウザの中だけで描くと、プロバイダが描くインラインスクリプトは実行されません。Reactは、ブラウザで自分が作ったインラインの<script>を実行しないからです。

開発ビルドのReactは、そのことをコンソールにエラーとして出します。Encountered a script tag while rendering React componentで始まるメッセージですが、テストが失敗したわけではありません。

そのため、テストで確かめる<html>のクラスは、プロバイダのeffectが付けたものです。スクリプトが最初の描画の前にクラスを付けることは、このパッケージ自身のテストで確かめています。

k8ordo

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

© 2026 k8o — MIT License

組版:Noto Sans JP / M PLUS 2