jiko21’s techblog

Svelteで色々試す

TD;DR

  • 現在、Svelteの開発環境周りはまだまだ整っているとは言えない
  • Storybook、testing-library等は一応対応している
  • testing-library自体はまだまだReactなどに比べると対応が弱い

はじめに

Svelteで以下を試してみたので備忘録がてら書きました。

  • フツーにアプリを作ってみる
  • Storybookを試してみる
  • テストコードを書いてみる

フツーにアプリを作ってみる

今回は一通りテストとかを書きたかったのでシンプルなTodoアプリを実装してます。

こちらで確認できます。

コードを見たい!という方はこちらをご確認ください。

Storybookを試してみる

上記のようにシンプルで小規模な場合は問題ありませんが、 ボタンやフォームのコンポーネントのデザインを実装するためにわざわざページ上で表示して確認、 とかは効率が悪いです。

あと、コンポーネントのカタログ的なものがないと、 コンポーネントを探すのに時間がかかって、同じようなコンポーネントを量産されたりするかもですね。

こちらは特に他のライブラリ・フレームワークと何ら変わりなく、次のコマンドで導入可能です。

npx sb init

このことは公式でも書かれてます。

自分で試してみたものはGitHub Pagesでこちらにあげてます。

書き心地に関しては、そこまで書きにくい、ということはなかったです。


<script>
  import { Meta, Template, Story } from '@storybook/addon-svelte-csf';
  import Button from './component.svelte';
</script>

<Meta
  title="components/Button"
  component={Button}
  argTypes={{
    label: { control: 'text' },
    style: {
      type: 'select',
      options: ['primary', 'danger'],
    },
    disabled: { control: 'bool' },
    onClick: { action: 'onClick' },
  }}
/>

<Template let:args>
  <Button {...args} on:click={args.onClick} />
</Template>

<Story
  name="primary"
  args={{
    label: 'button',
    style: 'primary',
    disabled: false,
  }}
/>

<Story
  name="danger"
  args={{
    label: 'button',
    style: 'danger',
    disabled: false,
  }}
/>

<Story
  name="Disabled"
  args={{
    label: 'button',
    disabled: true,
  }}
/>

テストを書いてみる

実際にお金を稼ぐプロダクトとなるとある程度テストは書いておきたいところです。

Svelteも他のライブラリと同じく、testing-libraryが存在します。(こちら)

シンプルなcomponentをテストする

buttonの例 上の画像のようなシンプルなbuttonコンポーネントを例に取ります。 コンポーネントの実装自体はとてもシンプルでイベントハンドラを持っていて、色の指定等をstyleでできるようにしています。

<script lang="ts">
  type ButtonStyle = 'primary' | 'danger';
  export let label = '';
  export let style: ButtonStyle = 'primary';
  export let disabled = false;
  export let testId = '';
  let className: ButtonStyle | 'disabled' = style;
  $: computeStyle(disabled)
  const computeStyle = (disabled) => {
    className = disabled ? 'disabled' : style;
  }
</script>

<button class={className} data-testid={testId} disabled={disabled} on:click>
  {label}
</button>

<style>
  button {
    border: none;
    border-radius: 4px;
    font-weight: 700;
    height: 32px;
    line-height: 16px;
    min-width: 60px;
    padding: 8px 4px;
    position: relative;
  }
  button:active {
      top: 1px;
  }
  button.primary {
    background-color: #00c1cf;
    color: white;
  }
  button.primary:hover {
    background-color: #5ec9d1;
    color: white;
  }
  button.danger {
      background-color: #ff002b;
      color: white;
  }
  button.danger:hover {
      background-color: #ff3b3b;
      color: white;
  }
  button.disabled {
    background-color: #dfdfdf;
    color: #3c3c3c;
  }
  button.disabled:active {
      top: 0;
  }
</style>

実際に今回、以下を試しました。

  • snapshot testing
  • イベントの発火が確認できるか

snapshot testing

snapshot testingに関して、特に詳しい説明は省略します。 他ライブラリでsnaspshot testingをしたことがあれば、ここに関しては特に難しいことなくかけると思います。

describe('components/Button', () => {
  describe('primary', () => {
    it('correctly render', () => {
      const component = render(Button, {
        label: 'button',
      });
      expect(component.container).toMatchSnapshot();
      expect(screen.getByText('button')).toBeTruthy();
    });
  });
});

イベントの発火が確認できるか

こちらに関してはちょっとsvelteだと特殊かもしれません。 Reactのようにpropsでイベントを渡すわけではないので、イベントとmock関数を別で紐付ける必要があります。

describe('components/Button', () => {
  const onClickMock = jest.fn();
  beforeEach(() => {
    jest.clearAllMocks();
  });
  describe('primary', () => {
    it('call function correctly', () => {
      const { component } = render(Button, {
        label: 'button',
      });
      // clickイベント時に関数を呼び出す
      component.$on('click', onClickMock);
      fireEvent.click(screen.getByText('button'));

      expect(onClickMock).toBeCalled();
    });
  });
});

あとはボタンの押下などはtesting-library/reactと同様です。

コンポーネントを組み合わせたUIテストの場合

こちらに関しては現在まだまだサポートがしっかりできていない状況です。 というのも、bindingのforwardingに関して、どうやらまだ対応できていないようです。

テストケース 上記のような挙動をテストしてみましょう。

  1. 文字を入力
  2. ボタンを押す
  3. 文字が消える(ボタンに応じた処理が呼ばれる)
describe('components/TodoForm', () => {
  it('文字入力後にボタンを押すと文字が消える', async () => {
    const addMock = jest.fn();
    const { component } = render(TodoForm);
    const input: any = screen.getByTestId('todo-input');
    fireEvent.change(input, {
      target: {
        value: 'test',
      },
    });
    component.$on('add', addMock);
    await fireEvent.click(screen.getByTestId('submit'));
    // 文字が消える
    expect(input.value).toBe('');
    // 処理が呼ばれる
    expect(addMock).toHaveBeenCalled();
  });
});

testing-libraryに慣れ親しんだ方であれば上記のように書くかと思います。 Reactとかだとこのように書いてもちゃんと動いてくれますがSvelteだとうまく動いてくれないです。 実行結果1

ちゃんとフォームにテキストを入れてボタンを押したのにテキストが消えないですね... どうやらbindingをforwardingした場合の挙動はまだ安定しないようです。

今回、ボタンを押下した際に呼ばれるaddイベントは、文字長が1以上のときに呼ばれるように実装しているので、 もちろん、呼び出されないです。

ここらへんの挙動に関しては、このようなUI test/Unit testで保証するよりはe2eなどで担保するほうがいいかもしれません。

最後に

ここ最近Svelteは注目を浴びており、自分の周りでも触り始めている人をちょくちょく見ます。

基本的に実装に関しては公式Tutorialを一通りみたり、忘れたときに見ればなんとかなりますが、 まだまだエコシステムに関してはReactなどにくらべるとまだまだ整備が完璧には整ってはいないようです。

とはいえ、SveltekitなどでSSGまでサポートしていたりと、かなり広範囲に渡って公式でサポートしているので、今後が楽しみですね。

about author...

Frontend engineer.
loves: TypeScript, React, Node.js

more detail...