Open27

Vue3 & Composition API

phytenphyten

.value が必要の時、不要の時

Vue 3のComposition APIでは、リアクティブなデータを扱う際に、いつ.valueプロパティを使用するかは、そのデータがどのように作成されたかに依存します。以下に、.valueが必要な場合と不要な場合をまとめます。

.valueが必要な場合

  • Refで作成されたリアクティブな変数: ref() 関数を使って作成されたリアクティブな変数は、その値にアクセスするために .value を使用します。これは、単一の値(プリミティブ値やオブジェクト)をリアクティブにする際に使用されます。

    import { ref } from 'vue';
    
    const count = ref(0);
    
    // 値にアクセスするには .value を使用
    console.log(count.value);  // 0
    

.valueが不要な場合

  • Propsオブジェクト: defineProps() で定義されたプロパティは、Vueのリアクティブシステムによって自動的にリアクティブになります。これらのプロパティにアクセスする際には .value は不要です。

    const props = defineProps({
      title: String
    });
    
    // propsから直接アクセス
    console.log(props.title);
    
  • Reactiveで作成されたリアクティブなオブジェクト: reactive() 関数を使って作成されたリアクティブなオブジェクトは、通常のJavaScriptオブジェクトと同様にアクセスします。.value は使用しません。

    import { reactive } from 'vue';
    
    const state = reactive({ count: 0 });
    
    // 通常のオブジェクトとしてアクセス
    console.log(state.count);
    

補足

  • テンプレート内: Vueのテンプレート内では、ref によって作成されたリアクティブな変数から .value を省略できます。Vueのテンプレートコンパイラが自動的に適切なリアクティブな値を参照します。

    <template>
      {{ count }}
    </template>
    
  • Destructuring: reactive オブジェクトから分解代入を行うと、リアクティブなリファレンスを失います。この場合、.valueを使用してもリアクティブな挙動は得られません。

これらの指針に従うことで、Vue 3のComposition APIを使った開発時に.valueの使用を適切に管理できます。

phytenphyten

Vue 3のComposition APIを使用している場合、computed 関数で作成されたリアクティブな値に対する .value プロパティの使用は、そのコンテキストに依存します。ここでのコンテキストとは、主に computed がテンプレート内で使用されるか、JavaScript(またはTypeScript)のコード内で使用されるかに関係します。

.valueが必要な場合(JavaScript/TypeScriptコード内)

  • JavaScript/TypeScriptコード内での使用computed 関数で作成されたリアクティブな値は、内部的には ref として扱われるため、通常のJavaScript(またはTypeScript)コード内でその値にアクセスする際には .value を使用します。

    import { computed } from 'vue';
    
    const count = ref(0);
    const doubled = computed(() => count.value * 2);
    
    console.log(doubled.value); // 0
    

.valueが不要な場合(Vueテンプレート内)

  • テンプレート内での使用:Vueのテンプレート内では、computed で作成されたリアクティブな値から .value を省略できます。Vueのテンプレートコンパイラが自動的に適切なリアクティブな値を参照します。

    <template>
      {{ doubled }}
    </template>
    

特別な注意点

  • Composition APIのsetup関数内でのDestructuringsetup関数内で computed または ref から分解代入を行うと、リアクティブな参照が失われます。この場合、リアクティブな挙動を保つためには .value を使用しても役に立ちません。

    setup() {
      const state = reactive({ count: 0 });
      const { count } = state; // countはリアクティブではない
    }
    

これらのガイドラインを理解しておくことで、Vue 3のComposition APIを使った開発時に computed プロパティの .value の使用を適切に管理できます。

phytenphyten

Vue 3 と <script setup> 構文を使用している場合に、外部からリアクティブなプロパティにアクセス可能になるシチュエーションは、主に以下のようなケースで発生します:

  1. テンプレート内での公開: <script setup> 内で定義されたリアクティブなプロパティ(例えば computed プロパティや ref)は、Vue コンポーネントのテンプレート内で直接使用可能です。Vue の内部処理がこれらのプロパティをコンポーネントのテンプレートコンテキストに自動的に公開します。

  2. テスト環境でのVue Test Utilsの挙動: Vue Test Utils は、コンポーネントの内部状態へのアクセスをテスト目的で容易にします。これは、wrapper.vm を通じてコンポーネントの Vue インスタンスにアクセスし、内部プロパティやメソッドにアクセスすることを可能にする特別な挙動です。

  3. 開発ツールによるデバッグ: Vue 開発者ツールを使用すると、コンポーネントのリアクティブな状態を視覚的に検証できます。これは、Vue の内部プロキシメカニズムを利用してコンポーネントのリアクティブなデータを外部から観察するためのものです。

具体的なケース

  • テンプレート内で、<script setup> で定義された computed プロパティを直接参照する場合。このプロパティは、コンポーネントのテンプレート内で自動的に利用可能です。

    <script setup>
    const count = ref(0);
    const doubled = computed(() => count.value * 2);
    </script>
    
    <template>
      {{ doubled }}
    </template>
    
  • テストコードで Vue Test Utils を使用して、wrapper.vm.doubled のようにコンポーネントの内部状態にアクセスする場合。

    const wrapper = mount(MyComponent);
    expect(wrapper.vm.doubled).toBe(/* 期待される値 */);
    

重要な注意点

  • <script setup> を使用する場合、コンポーネントの内部ロジックや状態はカプセル化され、通常は直接アクセスできないように設計されています。そのため、直接的な内部状態へのアクセスは、Vue の公式な使い方とは異なり、将来的に動作が変更される可能性があります。

  • テストの際には、コンポーネントの公開インターフェース(props, emits, slotsなど)に基づいて挙動を検証することが推奨されます。これにより、実装の詳細に依存しない堅牢なテストが可能になります。

phytenphyten

wrapper.getComponent メソッドを使用する際にセレクタとして {ref: 'some-element'} を使用したことでうまくいったとのこと、これはセレクタの指定方法に関連する問題でした。Vue Test Utils では、コンポーネントを取得するために様々なセレクタを使用できますが、その選択はコンポーネントの定義やテスト環境の設定に依存します。

wrapper.getComponent('[data-testid="some-element"]') だと、VueWrapperの限定的な機能しか使えない可能性あり。なので、getComponentでは ref を用いて特定すると良さそう。

セレクタの種類

Vue Test Utils では以下のようなセレクタを使用できます:

  • CSS セレクタ: 例えば 'div.some-class''[data-testid="some-testid"]' のような、通常の CSS セレクタです。
  • オブジェクトセレクタ: { name: 'MyComponent' }{ ref: 'myRef' } のような、コンポーネントの名前や ref を指定するオブジェクトです。

修正前後の違い

  • 修正前 ('[data-testid="some-element"]'): こちらは CSS セレクタを使用しており、DOM要素に設定された data-testid 属性を基にコンポーネントを選択していました。これは、通常のHTML要素に対してはうまく機能しますが、Vue コンポーネントに対しては常に適切とは限りません。

  • 修正後 ({ref: 'some-element'}): こちらは Vue コンポーネントの ref 属性を使用しています。Vue Test Utils では、ref を使用して直接コンポーネントのインスタンスにアクセスできるため、より信頼性が高くなります。

結論

セレクタの選択は、テスト対象のコンポーネントの構造やテスト環境に依存します。特に Vue コンポーネントのインスタンスに直接アクセスしたい場合は、ref 属性を使用することが一般的に推奨されます。また、コンポーネントの内部構造に依存するテストは、コンポーネントの変更に対して脆弱になりがちですので、テストの信頼性と保守性を高めるために、公開インターフェースを通じたテストを心がけることが重要です。

phytenphyten

メモ
getComponent(<クエリセレクタ>) だと、不完全なVueWrapperが返ってくる。
(propsやvmなどが取得できない)
なので、 ref で取得したり、コンポーネントオブジェクトで取得したりする必要あり。
ref を使いたくない場合には、findAllComponentsで1度全件取得し、それをdata-testidなどで絞り込んで取得する。
ちょっとバグっぽい挙動な気がする。

phytenphyten

TypeScriptで特定の条件下でのみ発生する値の型を明示的に指定する場合、次のような一般的な形式を使用できます。ここでは、条件に基づいて true または null のみを返すリアクティブな値を作成する例を示します。

import { computed, ComputedRef } from 'vue';

// 条件を示すリアクティブな値の型を ComputedRef<boolean> と仮定
const condition: ComputedRef<boolean> = /* ... 条件の定義 ... */;

// 条件に基づいて true または null のみを返すリアクティブな値
const specificValue: ComputedRef<true | null> = computed(() => {
  return condition.value ? null : true;
});

この例では、conditiontrue の場合に null を、そうでない場合に true を返すリアクティブな値 specificValue を作成しています。ComputedRef<true | null> 型は、この値が true または null のみを取ることを明示的に指定しており、TypeScriptによる誤った型推論を防ぎます。

このパターンは、Vue 3 の Composition API で条件に応じた特定の値をリアクティブに管理する場合に有効です。また、この方法は、任意の条件と値の組み合わせに応じて柔軟に適用できます。

phytenphyten

Jest と Vue Test Utils を使用して、コンポーネントが cancel イベントを 1 回 emit したことを検証するには、wrapper.emitted() メソッドを使用します。このメソッドは、コンポーネントによって発行されたすべてのイベントとその引数のリストを返します。

以下のコードスニペットは、cancel イベントが正確に 1 回 emit されたことを確認する方法を示しています:

// コンポーネントのマウント
const wrapper = mount(YourComponent);

// コンポーネントがイベントをemitするアクションをトリガー
// 例: ボタンクリック、メソッド呼び出しなど

// 'cancel' イベントが1回emitされたことを検証
expect(wrapper.emitted('cancel')).toHaveLength(1);

このテストでは、まずコンポーネントをマウントし、次にイベントを発行するアクション(例えばボタンのクリックやメソッドの呼び出し)をトリガーします。その後、wrapper.emitted('cancel') を使用して cancel イベントが発行された回数を検証します。toHaveLength(1) は、cancel イベントが正確に 1 回だけ発行されたことを検証するために使用されます。

この方法で、コンポーネントが特定のイベントを期待された回数だけ発行したことを検証できます。

phytenphyten

Vue Test Utilsでのテスト中に見かける component-stub のような名前の要素は、スタブ(stub)されたコンポーネントを表します。これは、テスト実行時に特定の子コンポーネントを実際の実装ではなく、簡略化された仮の実装(スタブ)に置き換えることを意味します。この技法は、特に単体テスト(ユニットテスト)でよく使用されます。

スタブとは

スタブは、コンポーネントやメソッドの代わりを務める簡単な実装です。これにより、テスト中に外部のコンポーネントや複雑なメソッドの詳細を気にせずに、特定のコンポーネントの挙動に集中できます。

component-stub の場合

component-stub のように、名前に -stub が付いたコンポーネントは、Vue Test Utilsが自動的にスタブ化したコンポーネントです。例えば、component というコンポーネントがある場合、テスト実行時に Vue Test Utilsはこのコンポーネントを自動的にスタブ化し、component-stub としてレンダリングします。

スタブの利点

  • 依存関係の分離: スタブを使用することで、テスト対象のコンポーネントが他の外部コンポーネントに依存しないようにします。これにより、テストがより予測可能で信頼性の高いものになります。
  • パフォーマンスの向上: 実際の実装の代わりに簡単なスタブを使用することで、テストの実行時間を短縮できます。
  • 単純化: 複雑な子コンポーネントや外部ライブラリのコンポーネントをスタブ化することで、テストを単純化し、管理しやすくすることができます。

スタブのカスタマイズ

Vue Test Utilsでは、shallowMount 関数を使用すると、デフォルトで全ての子コンポーネントがスタブ化されます。また、mount 関数と共に stubs オプションを使用して、特定のコンポーネントのみをスタブ化することもできます。

phytenphyten

shallowMount を使用すると、Vue Test Utils は全ての子コンポーネントをスタブ化します。この挙動により、子コンポーネントからの emit イベントを直接テストすることはできません。ただし、子コンポーネントが emit するイベントに反応して親コンポーネントが何らかのアクションを取る場合、その親コンポーネントの挙動をテストすることは可能です。

子コンポーネントのイベントのテスト

shallowMount でマウントされたコンポーネントで子コンポーネントからのイベントをテストする方法は、以下の通りです。

  1. 子コンポーネントのスタブ化: 子コンポーネントはスタブ化され、その実際の実装はレンダリングされません。これにより、子コンポーネントの内部ロジックや発行するイベントは無視されます。

  2. イベントのマニュアルトリガー: テスト中に子コンポーネントが emit するイベントは手動でトリガーする必要があります。これは、スタブ化された子コンポーネントに対して wrapper.findwrapper.getComponent を使用し、vm.$emit を呼び出すことで行えます。

    const wrapper = shallowMount(ParentComponent);
    wrapper.find(ChildComponentStub).vm.$emit('some-event', eventData);
    
  3. 親コンポーネントの反応のテスト: 子コンポーネントからのイベントに対する親コンポーネントの反応をテストします。これは通常、親コンポーネント内のデータの変更やメソッドの呼び出しとして観察できます。

注意点

  • shallowMount は、主に子コンポーネントの内部実装に依存しない単体テストに適しています。
  • 子コンポーネントとの相互作用が重要な場合や、子コンポーネント自体の挙動を詳細にテストしたい場合は、mount を使用して全ての子コンポーネントを実際にマウントすることを検討してください。

子コンポーネントからのイベント発行を含むより複雑な相互作用をテストする場合は、shallowMount よりも mount の使用が適切な場合が多いです。

phytenphyten

Vue 3 と Jest で shallowMount を使用している場合、名前付きスロット内の特定の要素をテストするためには、いくつかのアプローチが考えられます。ただし、shallowMount は子コンポーネントをスタブ化しますので、スロット内の内容が直接的に親コンポーネントのテンプレートに描画される場合に限り、これらの要素にアクセスすることができます。

スロット内容のテスト

名前付きスロットに挿入される内容をテストする基本的な方法は以下の通りです:

  1. スロットの内容をマウント時に提供する:
    テスト中に shallowMount または mount を使ってコンポーネントをマウントする際、スロットの内容を提供します。

    const wrapper = shallowMount(YourComponent, {
      slots: {
        trigger: `<a data-testid="slot">…</a>`
      }
    });
    
  2. テスト中にスロット内容を検索:
    wrapper.find または wrapper.findAll を使用して、スロット内の特定の要素(この場合は data-testid 属性を持つ要素)を検索します。

    const slotContent = wrapper.find('[data-testid="slot"]');
    expect(slotContent.exists()).toBe(true);
    

注意点

  • shallowMount を使用する場合、名前付きスロット内の内容が子コンポーネントによって提供される場合、その内容はスタブ化された子コンポーネントによって描画されないため、テスト中にはアクセスできません。
  • スロットの内容が親コンポーネントによって直接提供される場合のみ、上記の方法でスロットの内容にアクセスできます。
  • 親コンポーネントが子コンポーネントのスロットに内容を提供する場合は、その内容もテストの一環として考慮する必要があります。

スロットの内容がテストの主要な焦点である場合、または子コンポーネントの実際の描画挙動をテストしたい場合は、mount を使用することを検討してください。これにより、子コンポーネントが提供するスロットの内容も含めて、コンポーネントの全体的な挙動をより正確にテストすることができます。

phytenphyten

Vue 3 で Jest と Vue Test Utils を使用してテストする際に、名前付きスロット(#named)がレンダリングされない問題に対処するためには、いくつかの可能な原因と解決策を検討する必要があります。

スロットのレンダリングに影響する要因

  1. スロットの内容が条件付きでレンダリングされる: 例えば、v-ifv-show ディレクティブによってスロットの内容が条件付きで表示される場合、その条件が満たされていないとスロットはレンダリングされません。テストセットアップ時に、これらの条件を満たすように状態を設定する必要があります。

  2. スロットを提供する親コンポーネントの挙動: 名前付きスロット #namedYourComponent コンポーネントによって使用されている可能性があります。shallowMount を使用している場合、このコンポーネントはスタブ化され、その結果、スロットの内容がレンダリングされない可能性があります。

解決策

  1. フルレンダリングの使用: スロットの内容が子コンポーネントによって制御されている場合、shallowMount の代わりに mount を使用してフルレンダリングを行い、子コンポーネントの実際の挙動を含めることを検討してください。

  2. 条件の確認: スロットのレンダリングが特定の条件に依存している場合は、その条件がテストセットアップ時に満たされていることを確認してください。

  3. スタブのカスタマイズ: 特定の子コンポーネントをスタブ化する必要がある場合は、mount 関数と stubs オプションを使用して、特定のコンポーネントのみをスタブ化し、他のコンポーネントは実際の実装でレンダリングするように設定します。

    mount(YourComponent, {
      global: {
        stubs: {
          'ChildComponent': true,
        },
      },
      propsData: {
        ...props,
      },
    })
    

デバッグ

問題の原因を特定するために、テスト中のコンポーネントの状態やレンダリングされたHTMLを確認することも有用です。wrapper.html()wrapper.vm を使用して、コンポーネントの状態やレンダリングされた内容を確認することができます。

phytenphyten

Vue2だとshallowMountしても子コンポーネントのdefaultのslotも、名前付きのslotも読み込んでくれていたが、Vue3だとdefaultのslotしか読み込んでくれなくなった。

Vue3だとmountして個別にstub化すると良さそう。

phytenphyten
Object.assign(wrapper.vm, { $parent: {
  close: () => undefined,
} });

このコードは、Vue Test Utils を使用して Vue コンポーネントのテストを行う際の一般的なテクニックです。ここでの Object.assign(wrapper.vm, { $parent: { close: () => undefined } }); は、テスト中の Vue コンポーネントのインスタンス(wrapper.vm)に $parent プロパティを動的に追加または変更しています。

コードの解説

  1. wrapper.vm: wrapper は Vue Test Utils によってマウントされた Vue コンポーネントのラッパーです。vm はその Vue インスタンスを指します。

  2. Object.assign: JavaScript の Object.assign メソッドは、一つ以上のソースオブジェクトからターゲットオブジェクトにプロパティをコピーするために使用されます。このメソッドは、ターゲットオブジェクトを変更し、変更されたオブジェクトを返します。

  3. $parent プロパティの追加: このコードでは、Vue コンポーネントの $parent プロパティを追加または上書きしています。$parent は、親コンポーネントのインスタンスを参照する Vue の内部プロパティです。

  4. close メソッドの定義: ここで close: () => undefined と定義されているのは、親コンポーネントに close というメソッドを追加しています。このメソッドは何もせず undefined を返すダミーの実装です。

使用目的

このようなコードが使用される状況は、通常、以下のような場合です:

  • テスト対象のコンポーネントが親コンポーネントのメソッド(この場合は close)を呼び出す場合。
  • 実際の親コンポーネントがテスト中に存在しないか、またはテストに不要な複雑さを追加する場合。
  • 親コンポーネントの特定の挙動を模倣(モック)するために、簡単な実装やダミーの実装を提供する必要がある場合。

このテクニックを使うことで、テストをより制御しやすくし、テスト対象のコンポーネントが期待通りに親コンポーネントとのインタラクションを行えるかを検証できます。ただし、このようなアプローチはコンポーネントの内部実装に依存するため、使用する際には注意が必要です。

phytenphyten

Vue3だと以下のエラーが発生

TypeError: 'set' on proxy: trap returned falsish for property '$parent'
    at Function.assign (<anonymous>)

Vue 3 では、コンポーネントのインスタンスはリアクティブなプロキシとして実装されています。この変更により、Vue 2 で可能だったいくつかの操作(例えば、直接的なインスタンスのプロパティの変更)は、Vue 3 ではエラーを引き起こすことがあります。

エラーメッセージ TypeError: 'set' on proxy: trap returned falsish for property '$parent' は、Vue 3 のリアクティブシステムにおいて $parent プロパティを直接変更しようとした際に発生しています。Vue 3 のリアクティブプロキシは、内部プロパティの不適切な変更を防ぐために設計されているため、このような操作は許可されていません。

解決策

Vue 3 でテスト中に親コンポーネントのメソッドやプロパティを模倣するには、より適切なアプローチを取る必要があります。

  1. プロパティのモック化: コンポーネントをマウントする際に、親コンポーネントのメソッドやプロパティをモック化することが推奨されます。これは、mountshallowMount のオプションで行うことができます。

  2. プロバイド/インジェクトの使用: 親子関係を持つコンポーネント間でメソッドやプロパティを共有するには、Vue のプロバイド/インジェクトシステムを使用することも一つの方法です。

  3. グローバルモック: global.mocks オプションを使用して、グローバルなプロパティやメソッドをモック化することもできます。

import { mount } from '@vue/test-utils';
import YourComponent from '@/components/YourComponent';

const wrapper = mount(YourComponent, {
  global: {
    mocks: {
      $parent: {
        close: () => undefined
      }
    }
  }
});

このコードでは、$parent とその close メソッドがモック化されています。これにより、テスト中のコンポーネントが $parent.close() を呼び出す際に、カスタムのダミー実装が使用されます。

注意点

  • Vue 3 のリアクティブシステムは非常に強力ですが、内部プロパティへの直接的な変更はエラーを引き起こす可能性があるため、注意が必要です。
  • テスト中にコンポーネントの挙動を変更する場合は、Vue Test Utils が提供する正式な方法を使用することが望ましいです。
phytenphyten

Vue 3 のテスト環境で Object.assign(wrapper.vm, { $parent: { close: () => undefined } }); のようなコードを使用しようとすると、エラーが発生します。このエラーは Vue 3 のリアクティブシステムと関連しています。まず、$parent とは何か、そしてなぜこのコードが Vue 3 で問題を引き起こすのかを説明します。

$parent とは

  • $parent は、Vue インスタンス内で親コンポーネントを参照するためのプロパティです。Vue では、コンポーネントは階層的に配置され、各コンポーネントは自分の子コンポーネントと親コンポーネントを知ることができます。$parent は、子コンポーネントからその直接の親コンポーネントにアクセスするために使われます。

問題のコード

Object.assign(wrapper.vm, { $parent: { close: () => undefined } });
  • このコードは、Vue Test Utils でラップされたコンポーネントの Vue インスタンス(wrapper.vm)に $parent プロパティを追加または変更しようとしています。ここでの目的は、テストのために $parentclose メソッドをカスタムの実装に置き換えることです。

Vue 3 での問題

  • Vue 3 では、コンポーネントのインスタンスは ES6 の Proxy を使ってリアクティブになっています。Proxy はオブジェクトのプロパティへのアクセスを制御し、Vue のリアクティブシステムはこれを使ってデータの変更を検知します。
  • Vue 3 のリアクティブプロキシは、内部的な特定のプロパティ(例えば $parent)の直接的な変更を許可しません。そのため、Object.assign を使ってこれらのプロパティを変更しようとすると、リアクティブシステムがエラーを発生させます。

解決策

  • テスト中に $parent のメソッドをカスタマイズする必要がある場合、代わりにイベント発行やプロバイド/インジェクトのメカニズム、あるいは Vuex などのグローバル状態管理を利用することを検討してください。
  • Vue 3 では、コンポーネント間の通信や依存関係の管理には、より宣言的で予測可能な方法が推奨されます。これにより、コードのテスト容易性と保守性が向上します。

以上の点を踏まえて、Vue 3 のリアクティブシステムとの互換性を保ちつつ、テストを実装することが重要です。

phytenphyten

Reactive Proxy について

Vue 3 で導入されたリアクティブプロキシは、Vue.js のリアクティブシステムの核心的な部分です。これは、JavaScript の Proxy オブジェクトを活用して、データの変更を追跡し、アプリケーションのUIをリアクティブに更新します。ここでいう「リアクティブ」とは、データが変更されると自動的にそれに応じてビューが更新されるという性質を指します。

JavaScript の Proxy

Proxy は、ES6(ECMAScript 2015)で導入された JavaScript の機能で、オブジェクトにカスタムの挙動を追加するために使用されます。Proxy を使うと、オブジェクトのプロパティへのアクセスや変更、関数の呼び出しなどに対して、カスタムの処理を挟むことができます。

Vue 3 のリアクティブプロキシ

Vue 3 では、データのリアクティブ性を実現するために、Proxy を使用しています。これにより、オブジェクトのプロパティが読み取られたり変更されたりしたときに、Vue がそれを検知して適切なアップデートを行うことができます。

reactive 関数

Vue 3 では、reactive 関数を使用してオブジェクトをリアクティブなオブジェクトに変換します。この関数は、通常の JavaScript オブジェクトを取り、そのオブジェクトのリアクティブなプロキシを返します。

import { reactive } from 'vue';

const state = reactive({ count: 0 });

この例では、state はリアクティブなプロキシです。state.count の値が変更されると、Vue はこれを検知し、依存する UI コンポーネントを自動的に更新します。

リアクティブプロキシの利点

  • パフォーマンス: Vue 3 のリアクティブプロキシは、Vue 2 のオブジェクト汚染(Object.defineProperty)に基づくリアクティブシステムよりも効率的です。
  • 柔軟性: Proxy は、オブジェクトのより微細な変更を検出でき、より洗練されたリアクティブ性を提供します。
  • 透明性: データのリアクティブ化が透明であり、開発者は通常の JavaScript オブジェクトのように扱うことができます。

Vue 3 のリアクティブプロキシの導入は、フレームワークのパフォーマンスと柔軟性の向上に大きく貢献しています。これにより、よりリッチでリアクティブなユーザーインターフェースの構築が可能になります。

phytenphyten

Vue 2 と Vue 3 では、リアクティブシステムの実装方法が大きく異なります。これらの違いを理解することは、Vueの動作原理とその進化を把握する上で重要です。

Vue 2 のリアクティブシステム

Vue 2 では、オブジェクトのプロパティに対するリアクティブ性は Object.defineProperty メソッドを使用して実装されていました。この方法では、各リアクティブなデータプロパティにゲッター(getter)とセッター(setter)を定義し、これらを通じて依存関係の追跡と変更の通知を行っていました。

特徴

  • オブジェクトの変更の検知: Vue 2 は、コンポーネントの data オブジェクトに定義されたプロパティに対して、ゲッターとセッターを定義します。
  • 依存関係の追跡: データプロパティにアクセスする際、Vue はゲッターを介して依存関係を収集し、コンポーネントの再レンダリングが必要なタイミングを把握します。
  • 変更の通知: データが変更されると、セッターが呼ばれ、Vue は関連するコンポーネントを再レンダリングします。

制限

  • 初期化時のプロパティのみ: Vue 2 では、コンポーネントの data に初期化時に存在しないプロパティはリアクティブではありませんでした。
  • 配列の制限: 配列のインデックスによる変更や、配列の長さの変更はリアクティブに検知できませんでした。

Vue 3 のリアクティブシステム

Vue 3 では、ES6 の Proxy オブジェクトを使用してリアクティブシステムが実装されています。Proxy は、オブジェクトや配列へのすべての操作をインターセプトし、より洗練されたリアクティブな挙動を提供します。

特徴

  • より良いパフォーマンス: Proxy は、オブジェクト全体に対して一つのハンドラを設定します。これにより、Vue 2 の Object.defineProperty を使用した方法よりも効率的です。
  • 全てのプロパティがリアクティブ: Proxy は、オブジェクトの新しいプロパティの追加や削除も検知できます。
  • 配列操作の完全なサポート: Proxy は、配列のインデックス操作や length プロパティの変更も検知できるため、Vue 2 の制限を克服しています。

結論

Vue 3 のリアクティブシステムは、Vue 2 のそれと比較してより強力で効率的です。Proxy の使用により、Vue 3 はより多様なデータ操作をリアクティブに検知し、パフォーマンスの向上を実現しています。この進化により、Vue のリアクティブ性はより堅牢で、多様なアプリケーション要件に対応できるようになっています

phytenphyten

global.stubsglobal.mocks との違い

Vue Test Utils での global.stubsglobal.mocks は、テスト中の Vue コンポーネントの挙動をカスタマイズするために使用されますが、その目的と使用方法には違いがあります。

global.stubs

global.stubs は、特定の子コンポーネントをスタブ(簡略化された代替品)に置き換えるために使用されます。これは、テスト対象のコンポーネントの特定の子コンポーネントの実際の実装をスキップし、代わりに簡単な実装や完全にカスタマイズされた挙動を提供するために役立ちます。

使用方法

const wrapper = mount(YourComponent, {
  global: {
    stubs: {
      'ChildComponent': true, // 子コンポーネントを自動スタブ化
      'AnotherComponent': MyStub // 子コンポーネントをカスタムスタブに置き換え
    }
  }
});

ここでは、ChildComponent は自動的にスタブ化され、AnotherComponentMyStub に置き換えられます。

global.mocks

global.mocks は、テスト中のコンポーネントインスタンスにグローバルプロパティやメソッドを追加するために使用されます。これは、グローバルなオブジェクトや Vue インスタンスメソッドをモック化(模擬実装を提供)するために役立ちます。

使用方法

const wrapper = mount(YourComponent, {
  global: {
    mocks: {
      $route: { path: '/some/path' }, // Vue Router のルートオブジェクトをモック化
      $store: { state: {}, dispatch: jest.fn() } // Vuex ストアをモック化
    }
  }
});

この例では、$route$store がモック化されており、テスト中にこれらのオブジェクトにアクセスすると、指定したモックオブジェクトが使用されます。

結論

  • global.stubs: 子コンポーネントを簡略化またはカスタムスタブに置き換えるために使用。
  • global.mocks: Vue インスタンスのグローバルプロパティやメソッドをモック化するために使用。

これらのオプションを適切に使用することで、Vue コンポーネントのテストをより柔軟に制御し、依存関係を簡素化し、特定のテストケースに特化した環境を構築することができます。

phytenphyten

フォーカスのテストについて

input.trigger('focus') ではなく、 input.element.focus() を使うこと。

Jest で Vue コンポーネントのテストを行う際に、要素のフォーカス状態をテストするためには、テスト環境で DOM API が利用可能であることを確認する必要があります。Jest はデフォルトで JSDOM を使用しており、これによりブラウザの DOM API をエミュレートします。これを利用して、要素のフォーカス状態をテストすることができます。

以下に、具体的なテストの実装例を示します。

テストのセットアップ

  1. Jest の設定: Jest が JSDOM を使用していることを確認します。ほとんどの標準的な Jest 環境では、これはデフォルトで設定されています。

  2. Vue コンポーネントのインポート: テスト対象の Vue コンポーネントをインポートします。

  3. Vue Test Utils の使用: Vue Test Utils を使ってコンポーネントをマウントします。

フォーカステストの実装

import { mount } from '@vue/test-utils';
import YourComponent from '@/components/YourComponent';

describe('YourComponent', () => {
  it('focuses the input element', async () => {
    // コンポーネントをマウント
    const wrapper = mount(YourComponent);

    // フォーカスする要素を取得
    const input = wrapper.find('input'); // 例: input 要素を探す

    // 要素にフォーカスを当てる
    input.element.focus();

    // Vue の nextTick を待つ
    await wrapper.vm.$nextTick();

    // フォーカスが当たっていることを確認
    expect(document.activeElement).toBe(input.element);
  });
});

このテストでは、input 要素にフォーカスを当て、document.activeElement がその要素であることを確認しています。

注意点

  • テストが正しく動作するためには、要素がフォーカス可能であることを確認してください(例えば、input, button, textarea など)。
  • テスト実行環境によっては、DOM API の一部が制限される場合があります。そのため、Jest の設定や JSDOM のバージョンを確認することが重要です。
  • wrapper.vm.$nextTick() を使用して、Vue のリアクティブな更新が完了するのを待ちます。これにより、DOM の更新が完了してからアサーションを実行することができます。
phytenphyten

移行ビルドについて

Vue 3 では、Vue 2 からの移行を支援するために「移行ビルド(Migration Build)」という機能が提供されています。この移行ビルドは、Vue 3 のリリースの一環として、既存の Vue 2 アプリケーションを Vue 3 にスムーズに移行するために特別に設計されたビルドです。

configureCompat 関数は、移行ビルドで利用可能な機能の一つで、Vue 2 と Vue 3 の間の互換性の違いを細かく制御するために使用されます。この関数を使うことで、特定の互換性の動作を有効または無効にすることができます。

COMPONENT_V_MODEL: false の意味

COMPONENT_V_MODEL: false という設定は、Vue 2 と Vue 3 の間で異なる v-model の挙動に関する互換性の設定を無効にします。具体的には、この設定により、Vue 3 における新しい v-model の実装が使用され、Vue 2 の v-model の挙動は無効になります。

Vue 2 では、v-model はデフォルトで value プロパティと input イベントにバインドされていました。しかし、Vue 3 では、v-modelmodelValue プロパティと update:modelValue イベントにバインドされるように変更されています。これにより、同じコンポーネント内で複数の v-model を使用することが可能になり、より柔軟なデータバインディングが実現されています。

この設定を false にすることで、Vue 3 の新しい v-model の挙動を強制し、Vue 2 の古い挙動を無効にすることができます。これは、Vue 2 から Vue 3 への移行過程において、新しいパターンに徐々に適応していく際に役立ちます。

使用方法

configureCompat は、アプリケーションの初期化段階で使用されます。例えば、アプリケーションのエントリポイント(通常は main.js または main.ts)で設定することができます。

import { createApp, configureCompat } from 'vue';
import App from './App.vue';

configureCompat({
  COMPONENT_V_MODEL: false,
});

const app = createApp(App);
app.mount('#app');

この設定は、アプリケーションの移行期間中に特に役立ち、開発者が新しい Vue 3 のパターンに慣れるための時間を提供します。最終的には、すべての互換性の設定を無効にし、Vue 3 の機能を完全に活用することが推奨されます。

phytenphyten

configureCompat 関数を使用して MODE: 3COMPONENT_V_MODEL: false を設定する場合、以下のような挙動が期待されます:

  1. MODE: 3: これは Vue 3 の完全なモードを意味します。すなわち、Vue 2 の互換性を持たせるための特定の機能や挙動は無効になり、純粋な Vue 3 の挙動のみが有効化されます。これにより、Vue 2 の挙動をエミュレートするための特別な処理は行われなくなり、Vue 3 の新しい機能やパフォーマンス最適化がフルに活用されます。

  2. COMPONENT_V_MODEL: false: これは Vue 3 の v-model の新しい実装を使用することを意味します。Vue 2 での v-model は、value プロパティと input イベントにバインドされていましたが、Vue 3 では modelValue プロパティと update:modelValue イベントにバインドされるようになっています。この設定を false にすることで、Vue 2 の v-model の挙動は無効化され、Vue 3 の新しい v-model の挙動が採用されます。

結論

この設定を行うことで、Vue 2 の互換性に関する特別な挙動やエミュレーションを無効化し、Vue 3 の新しい機能やパフォーマンス最適化を完全に活用することになります。これは、アプリケーションを Vue 2 から Vue 3 へ移行する際の最終段階、または Vue 3 を使用して新しいプロジェクトを始める際に適した設定です。

移行期間中には、アプリケーションのコードを Vue 3 の新しいパターンやベストプラクティスに合わせてリファクタリングする必要があるでしょう。これにより、Vue 3 の全機能を最大限に活用することができます。

phytenphyten

Vue 3 と Composition API を用いたコンポーネントネスティングとデータ伝達の実装例

指定された要件に従って、MyComponent 内に ParentComponent, ChildComponent, GrandChildComponent をネストし、Composition API と <script setup> 構文を使って実装する例を示します。まずは propsemit を使った例から始め、その後 provideinject を使った例を説明します。

Props と Emit を使った例

MyComponent

MyComponent は、他のコンポーネントをネストしています。

<template>
  <ParentComponent>
    <ChildComponent label="マニュアル">
      <GrandChildComponent label="孫マニュアル" />
    </ChildComponent>
    <ChildComponent label="テスト">
      <GrandChildComponent label="孫テスト" />
    </ChildComponent>
  </ParentComponent>
</template>

<script setup lang="ts">
import ParentComponent from './ParentComponent.vue';
import ChildComponent from './ChildComponent.vue';
import GrandChildComponent from './GrandChildComponent.vue';
</script>

ParentComponent

ParentComponent は単純なコンテナとして機能します。

<template>
  <div class="parent-component">
    <slot></slot>
  </div>
</template>

<script setup lang="ts">
// 特にロジックはありません。
</script>

ChildComponent

ChildComponentlabel prop を受け取り、スロットを提供します。

<template>
  <div class="child-component">
    <h2>{{ label }}</h2>
    <slot></slot>
  </div>
</template>

<script setup lang="ts">
import { defineProps } from 'vue';

const props = defineProps({
  label: String,
});
</script>

GrandChildComponent

GrandChildComponentlabel prop を受け取ります。

<template>
  <div class="grandchild-component">
    <p>{{ label }}</p>
  </div>
</template>

<script setup lang="ts">
import { defineProps } from 'vue';

const props = defineProps({
  label: String,
});
</script>

Provide と Inject を使った例

MyComponent

MyComponent の構造は同じですが、provideinject を使用してデータを渡します。

<template>
  <!-- 同じ構造 -->
</template>

<script setup lang="ts">
// 同じインポート
</script>

ParentComponent

ParentComponent では、provide 関数を使ってデータを提供します。

<template>
  <div class="parent-component">
    <slot></slot>
  </div>
</template>

<script setup lang="ts">
import { provide } from 'vue';

provide('sharedData', '共有データ');
</script>

ChildComponent

ChildComponentprovide されたデータを扱いませんが、スロットを提供します。

<template>
  <div class="child-component">
    <h2>{{ label }}</h2>
    <slot></slot>
  </div>
</template>

<script setup lang="ts">
import { defineProps } from 'vue';

const props = defineProps({
  label: String,
});
</script>

GrandChildComponent

GrandChildComponentinject を使ってデータを受け取ります。

<template>
  <div class="grandchild-component">
    <p>{{ label }}</p>
    <p>共有データ: {{ sharedData }}</p>
  </div>
</template>

<script setup lang="ts">
import { defineProps, inject } from 'vue';

const props = defineProps({
  label: String,
});
const sharedData = inject('sharedData');
</script>

解説

  • Props と Emit: この方法では、データは親コンポーネントから子コンポーネントへ props を通じて渡されます。子コンポーネントは必要に応じて emit を使用して親にイベントを通知します。
  • Provide と Inject: この方法では、ParentComponent から provide

使用してデータを提供し、GrandChildComponentinject を使ってそのデータを受け取ります。これにより、中間の ChildComponent を通さずにデータを渡すことができます。

<script setup> 構文を使用すると、コンポーネントの定義がより簡潔になり、Composition API の利用が容易になります。また、TypeScript との統合により、より型安全なコードを書くことができます。

phytenphyten

ライブラリの部分的なmock化

usePermission のモックが mockReturnValue メソッドを持たないというエラーが発生している場合、jest.mock の使用方法に問題がある可能性があります。jest.mock でモジュール全体をモックにし、その中で特定の関数(この場合は usePermission)を Jest のモック関数(jest.fn())として設定する必要があります。

jest.mock の中で直接 jest.fn() を使用してモックを設定し、その後 mockReturnValue または mockImplementation を使って、テストの前にその挙動を指定します。以下にその例を示します。

正しいモックの設定方法

// @vueuse/core モジュールをモック化します
jest.mock('@vueuse/core', () => {
  // 必要な依存関係をインポート
  const originalModule = jest.requireActual('@vueuse/core');

  return {
    ...originalModule,
    // usePermission を Jest のモック関数として設定
    usePermission: jest.fn()
  };
});

// テストの前に usePermission のモックの挙動を設定
beforeEach(() => {
  const { usePermission } = require('@vueuse/core');
  usePermission.mockReturnValue({ value: 'denied' }); // リアクティブな値を模倣
});

// テストケース
describe('Your Test Suite', () => {
  it('Your test case', () => {
    // テストの実装...
  });

  // 他のテストケース...
});

// テストの後にモックをクリーンアップ
afterEach(() => {
  jest.clearAllMocks();
});

このコードでは、jest.mock@vueuse/core モジュールをモック化し、その中で usePermission を Jest のモック関数に設定しています。beforeEachusePermission の挙動を設定し、afterEach でモックをクリーンアップしています。

注意点

  • モックの設定はファイルの最上部で行うことが一般的です。
  • jest.requireActual を使用して、モジュールのオリジナルの実装を保持し、必要な部分だけをオーバーライドします。
  • テストの後にモックをクリーンアップすることを忘れないでください(jest.clearAllMocksjest.resetAllMocks)。

これらの手順を実行しても問題が解決しない場合は、テストの設定や環境に問題がある可能性があります。その場合は、テストランナー(Jest)の設定や、プロジェクトの依存関係の管理を再検討することをお勧めします。また、使用している Jest や Vue のバージョンに互換性の問題がないかも確認してください。

phytenphyten

require を使わない場合(こっちのほうが良い)

エラー「Require statement not part of import statement」は、TypeScriptのプロジェクトでCommonJSのrequire文を使用しようとしたときに発生します。TypeScriptは主にES6のimport文を使用することを推奨しているため、require文を使用するとこの種のエラーが発生することがあります。

この問題を解決するためには、jest.mockでモック化した関数をimport文を使ってインポートする方法を採用する必要があります。以下はその修正方法です。

修正されたテストコード

import useSupported from '../useSupported';
import { usePermission } from '@vueuse/core';
import flushPromises from 'flush-promises';

jest.mock('@vueuse/core', () => ({
  ...jest.requireActual('@vueuse/core'),
  usePermission: jest.fn(),
}));

// モックの挙動を設定
beforeEach(() => {
  (usePermission as jest.Mock).mockReturnValue({ value: 'denied' });
});

describe('useSupported', () => {
  // 既存のテストコード...

  describe('test suite', () => {
    it('test', async () => {
      (usePermission as jest.Mock).mockReturnValue({ value: 'granted' });
    });

    // 他のテストケース...
  });

  // 他のdescribeブロック...
});

// テストの後にモックをクリーンアップ
afterEach(() => {
  jest.resetAllMocks();
});

このコードでは、jest.mockを使って@vueuse/coreをモック化し、usePermissionをJestのモック関数に設定しています。テストケース内では、usePermissionのモックの挙動をbeforeEachや個々のテストケース内で設定しています。

注意点

  • usePermissionをモック関数として使用するためには、jest.Mock型アサーションを使用しています。
  • jest.mockの設定はファイルの最上部で行うことが一般的です。
  • テストの後にはjest.resetAllMocksを使用して、モックをクリーンアップすることが推奨されます。
  • もしjest.mockを使用してモジュールの特定の関数のみをモックにする場合、jest.requireActualを使用して元のモジュールの他の関数を保持することができます。
phytenphyten

リセット系のjestメソッド

jest.restoreAllMocks() は、jest.spyOn で作成されたモック関数を元の実装に戻すことに特化しています。これに対して、jest.clearAllMocks() はモック関数の呼び出し履歴や返り値のみをクリアし、jest.resetAllMocks() はモック関数の履歴や返り値をクリアし、さらにモック関数自体を新しい jest.fn() にリセットします。restoreAllMocks は、spyOn でモック化された関数のオリジナルの実装を復元することで、他のモック関数の動作には影響しません。これは、元の関数の挙動を保証しつつ、テスト間での独立性を維持するために有用です。

phytenphyten

名前付きv-modelの記法

Vue 3 で Composition API、TypeScript、および <script setup> 構文を使用して v-model を利用する例を以下に示します。

親コンポーネント (ParentComponent.vue)

<template>
  <ChildComponent v-model:title="pageTitle" />
</template>

<script setup lang="ts">
import { ref } from 'vue';
import ChildComponent from './ChildComponent.vue';

const pageTitle = ref('初期ページタイトル');
</script>

子コンポーネント (ChildComponent.vue)

<template>
  <input :value="title" @input="$emit('update:title', $event.target.value)" />
</template>

<script setup lang="ts">
import { defineProps, defineEmits } from 'vue';

const props = defineProps<{ title: string }>();
const emit = defineEmits(['update:title']);
</script>

この例では、<script setup> 構文を使用して、より宣言的で簡潔なコンポーネント定義を行っています。TypeScript と組み合わせることで、型安全性を高めることができます。子コンポーネントでは definePropsdefineEmits を使ってプロパティとイベントを定義し、親コンポーネントは ref を使って反応的なデータを管理します。v-model:title は親コンポーネントから子コンポーネントへの双方向バインディングを行います。