Vue

Vueを使い始めるには、Vueのドキュメントをお読みください。

使用例

以下のセクションで説明されていることは、これらの例で見つけることができます:

Vueアプリケーションを追加するタイミング

HAMLページだけで要件を満たせる場合もあります。これは主に静的なページや、ロジックがほとんどないページに対して正しい表現です。ページにVueアプリケーションを追加する価値があるかどうかは、どのように判断すればよいのでしょうか?答えは「アプリケーションの状態をメンテナーし、レンダリングされたページをそれと同期させる必要があるとき」です。

これを説明するために、1つのトグルがあり、それをトグルするとAPIリクエストが送信されるページを想像してみましょう。この場合、メンテナーしたい状態には関係なく、リクエストを送信してトグルを切り替えます。しかし、最初のトグルと常に反対のトグルをもう一つ追加する場合、_ステートが_必要になります:一つのトグルは他のトグルのステートを “認識 “する必要があります。プレーンなJavaScriptで書かれた場合、このロジックは通常、DOMイベントをリスニングし、DOMを変更して反応します。このようなケースは、Vue.jsを使用する方がはるかに簡単に処理できます。

Vueアプリケーションが必要であることを示すフラグにはどのようなものがありますか?

  • 複数の要因に基づく複雑な条件分岐を定義し、ユーザーとのインタラクションに応じてそれらを更新する必要がある場合;
  • 何らかの形でアプリケーションの状態をメンテナーし、タグ/要素間で共有する必要がある場合;
  • 将来的に複雑なロジックが追加されることが予想される場合 - 次のステップでJS/HAMLをVueに書き換える必要があるよりも、基本的なVueアプリケーションから始める方が簡単です。

ページ上に複数のVueアプリケーションを表示しないようにします。

以前は、レンダリングされた HAML ページのさまざまな部分に複数の小さな Vue アプリケーションを追加することで、ページに部分的にインタラクティブ性を追加していました。しかし、この方法は複数の複雑な問題を引き起こしました:

  • ほとんどの場合、これらのアプリケーションは状態を共有せず、独立して API リクエストを実行するため、リクエスト数が増大します;
  • 複数のエンドポイントを使用してRailsからVueにデータを提供する必要があります;
  • ページロード後にVueアプリケーションを動的にレンダリングできないため、ページ構造が硬直的になります;
  • Railsルーティングを置き換えるためにクライアントサイドルーティングを完全に活用することはできません;
  • 複数のアプリケーションを利用すると、ユーザー体験が予測できなくなり、ページが複雑になり、デバッグ作業が難しくなります;
  • アプリ間の通信方法は、ウェブバイタルの数値に影響します。

このような理由から、すでに別のVueアプリケーションが存在するPagesに新しいVueアプリケーションを追加することには慎重になりたいと考えています(これには古いナビゲーションや新しいナビゲーションは含まれません)。新しいアプリを追加する前に、希望する機能を実現するために既存のアプリを拡張することが絶対に不可能であることを確認してください。不明な点がある場合は、#frontend または#frontend-maintainers Slack チャンネルでアーキテクチャに関するアドバイスをお気軽にお尋ねください。

それでも新しいアプリケーションを追加する必要がある場合は、既存のアプリケーションとローカルの状態を共有していることを確認してください(できればApollo Client、またはREST APIを使用する場合はVuexを介して)。

Vueアーキテクチャ

Vueアーキテクチャで達成しようとしている主な目標は、データフローを1つにし、データ入力を1つにすることです。この目標を達成するために、VuexまたはApollo Clientを使用します。

このアーキテクチャについては、Vueのドキュメントで状態管理と一方通行のデータフローについても読むことができます。

コンポーネントとストア

イシューボードや 環境テーブルのように、Vue.jsで実装されたいくつかの機能では、明確な関心事の分離を見つけることができます:

new_feature
├── components
│   └── component.vue
│   └── ...
├── store
│  └── new_feature_store.js
├── index.js

一貫性を保つため、同じ構造に従うことをお勧めします。

それぞれについて見てみましょう:

index.js ファイル

このファイルは、新機能のインデックスファイルです。新機能のルートVueインスタンスはここにあるはずです。

StoreとServiceはこのファイルにインポートして初期化し、メインコンポーネントのpropとして提供します。

ページ固有のJavaScriptについて必ず読んでください。

ブートストラップ

HAMLからJavaScriptへのデータ提供

Vueアプリケーションをマウントするときに、RailsからJavaScriptにデータを提供する必要があるかもしれません。そのためには、HTML要素でdata 属性を使用し、アプリケーションのマウント中にクエリを実行します。マウントされた要素はVueが生成したDOMに置き換えられるため、この操作はアプリケーションの初期化中にのみ行う必要があります。

data 属性は文字列値しか受け付けないので、他の変数型をキャストするか、文字列に変換する必要があります。

メインのVueコンポーネント内部でDOMをクエリする代わりに、render 関数のprops またはprovide 、DOMからVueインスタンスにデータを提供する利点は、ユニットテスト内でフィクスチャやHTML要素を作成しなくて済むことです。

initSimpleApp

initSimpleApp は、Vue.jsでコンポーネントをマウントする処理を効率化するヘルパー関数です。HTMLのマウントポイントを表すセレクタ文字列と、Vueコンポーネントの2つの引数を受け取ります。

使用するにはinitSimpleApp

  1. ページ内にIDまたは固有のクラスを持つHTML要素を含めます。
  2. JSONオブジェクトを含むdata-view-model属性を追加します。
  3. 必要なVueコンポーネントをインポートし、initSimpleApp 、HTML要素を選択する有効なCSSセレクタ文字列と一緒に渡します。 この文字列は、指定された場所にコンポーネントをマウントします。

initSimpleApp は、data-view-model属性の内容をJSONオブジェクトとして自動的に取得し、マウントされたVueコンポーネントにpropsとして渡します。これは、コンポーネントにデータを事前に投入するために使用できます。

使用例:

//my_component.vue
<template>
  <div>
    <p>Prop1: {{ prop1 }}</p>
    <p>Prop2: {{ prop2 }}</p>
  </div>
</template>

<script>
export default {
  name: 'MyComponent',
  props: {
    prop1: {
      type: String,
      required: true
    },
    prop2: {
      type: Number,
      required: true
    }
  }
}
</script>
<div id="js-my-element" data-view-model='{"prop1": "my object", "prop2": 42 }'></div>
//index.js
import MyComponent from './my_component.vue'
import { initSimpleApp } from '~/helpers/init_simple_app_helper'

initSimpleApp('#js-my-element', MyComponent)
provideinject

Vue はprovideinject を通して依存性注入をサポートしています。 コンポーネント内部ではinject 設定がprovide から渡された値にアクセスします。この Vue アプリの初期化の例では、provide 設定が HAML からコンポーネントに値を渡す方法を示しています:

#js-vue-app{ data: { endpoint: 'foo' }}

// index.js
const el = document.getElementById('js-vue-app');

if (!el) return false;

const { endpoint } = el.dataset;

return new Vue({
  el,
  name: 'MyComponentRoot',
  render(createElement) {
    return createElement('my-component', {
      provide: {
        endpoint
      },
    });
  },
});

コンポーネントやその子コンポーネントは、inject を通してプロパティにアクセスできます:

<script>
  export default {
    name: 'MyComponent',
    inject: ['endpoint'],
    ...
    ...
  };
</script>
<template>
  ...
  ...
</template>

依存性注入を使用して HAML から値を提供することは、次のような場合に理想的です:

  • 注入された値はそのデータ型や内容に対して明示的な検証を必要としません。
  • 値はリアクティブである必要はありません。
  • この値にアクセスする必要のある複数のコンポーネントが階層に存在し、プロップドリリングが不便になります。同じpropが純粋にそれを使用するコンポーネントまで階層内のすべてのコンポーネントに渡されるとき、prop-drillingが発生します。

依存性注入は、両方の条件が真である場合、子コンポーネント(直属の子コンポーネントまたは複数階層)を壊す可能性があります:

  • inject 設定で宣言された値にデフォルトが定義されていない場合。
  • 親コンポーネントがprovide 設定を使用して値を提供していません。

デフォルト値は、それが意味を持つコンテキストで有用かもしれません。

プロップス

HAML からの値が依存性注入の基準に適合しない場合、props を使います。次の例を見てください。

// haml
#js-vue-app{ data: { endpoint: 'foo' }}

// index.js
const el = document.getElementById('js-vue-app');

if (!el) return false;

const { endpoint } = el.dataset;

return new Vue({
  el,
  name: 'MyComponentRoot',
  render(createElement) {
    return createElement('my-component', {
      props: {
        endpoint
      },
    });
  },
});
note
id Vueアプリケーションをマウントするために属性を id追加する場合、id これが idコードベース全体で一意であることを確認してください。

Vueアプリに渡すデータを明示的に宣言する理由については、Vueスタイルガイドを参照してください。

VueアプリケーションへのRailsフォームフィールドの提供

RailsでフォームをComposerするとき、フォーム入力のnameidvalue 属性はバックエンドに合わせて生成されます。RailsフォームをVueに変換するときや、コンポーネント(日付ピッカーやプロジェクトセレクタなど)をフォームにインテグレーションするときに、これらの生成された属性にアクセスできると便利です。parseRailsFormFields ユーティリティを使うと、生成されたフォーム入力属性をパースして Vue アプリケーションに渡すことができます。これにより、フォームの送信方法を変更することなく、Vueコンポーネントをインテグレーションできます。

-# form.html.haml
= form_for user do |form|
  .js-user-form
    = form.text_field :name, class: 'form-control gl-form-input', data: { js_name: 'name' }
    = form.text_field :email, class: 'form-control gl-form-input', data: { js_name: 'email' }

js_name データ属性は、結果の JavaScript オブジェクトのキーとして使用されます。例えば、= form.text_field :email, data: { js_name: 'fooBarBaz' } は次のように変換されます。{ fooBarBaz: { name: 'user[email]', id: 'user_email', value: '' } }

// index.js
import Vue from 'vue';
import { parseRailsFormFields } from '~/lib/utils/forms';
import UserForm from './components/user_form.vue';

export const initUserForm = () => {
  const el = document.querySelector('.js-user-form');

  if (!el) {
    return null;
  }

  const fields = parseRailsFormFields(el);

  return new Vue({
    el,
    name: 'UserFormRoot',
    render(h) {
      return h(UserForm, {
        props: {
          fields,
        },
      });
    },
  });
};
<script>
// user_form.vue
import { GlButton, GlFormGroup, GlFormInput } from '@gitlab/ui';

export default {
  name: 'UserForm',
  components: { GlButton, GlFormGroup, GlFormInput },
  props: {
    fields: {
      type: Object,
      required: true,
    },
  },
};
</script>

<template>
  <div>
    <gl-form-group :label-for="fields.name.id" :label="__('Name')">
      <gl-form-input v-bind="fields.name" size="lg" />
    </gl-form-group>

    <gl-form-group :label-for="fields.email.id" :label="__('Email')">
      <gl-form-input v-bind="fields.email" type="email" size="lg" />
    </gl-form-group>

    <gl-button type="submit" category="primary" variant="confirm">{{ __('Update') }}</gl-button>
  </div>
</template>

gl オブジェクトへのアクセス

アプリケーションのライフサイクル中に変更されないデータについては、DOM にクエリを発行するのと同じ場所でgl オブジェクトにクエリを発行します。この方法に従うことで、gl オブジェクトをモックする必要がなくなり、テストが容易になります。これはVueインスタンスを初期化するときに行うべきで、データはメインコンポーネントにprops

return new Vue({
  el: '.js-vue-app',
  name: 'MyComponentRoot',
  render(createElement) {
    return createElement('my-component', {
      props: {
        avatarUrl: gl.avatarUrl,
      },
    });
  },
});

機能フラグへのアクセス

機能フラグをフロントエンドにプッシュした後、Vueのprovideinject メカニズムを使用して、Vueアプリケーション内の任意の子孫コンポーネントで機能フラグを利用できるようにします。glFeatures オブジェクトはすでにcommons/vue.jsで提供されているので、フラグを使用するために必要なのは mixin だけです:

// An arbitrary descendant component

import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';

export default {
  // ...
  mixins: [glFeatureFlagsMixin()],
  // ...
  created() {
    if (this.glFeatures.myFlag) {
      // ...
    }
  },
}

この方法にはいくつかの利点があります:

  • 任意に深くネストされたコンポーネントは、中間コンポーネントに気づかれることなくフラグにアクセスすることができます(propsを介してフラグを渡すなど)。
  • フラグをvue-test-utils からmount/shallowMount に prop として渡すことができるので、テストしやすいです。

     import { shallowMount } from '@vue/test-utils';
       
     shallowMount(component, {
       provide: {
         glFeatures: { myFlag: true },
       },
     });
    
  • アプリケーションのエントリーポイント以外では、グローバル変数へのアクセスは必要ありません。

コンポーネント用フォルダ

このフォルダには、この新機能に特化したすべてのコンポーネントが格納されます。他の場所でも使用される可能性のあるコンポーネントを使用または作成するには、vue_shared/components を参照してください。

コンポーネントを作成するタイミングを知るための良いガイドラインは、他の場所で再利用できるかどうかを考えることです。

たとえば、GitLab ではテーブルがさまざまな場所で使われています。逆に、テーブルのセルをひとつのテーブルでしか使わないような場合は、このパターンを使うべきではありません。

コンポーネントについては Vue.js のサイトComponent System を参照ください。

ストア用のフォルダ

Vuex

詳しくはこちらのページをご覧ください。

ビュー・ルーター

ページにVue Routerを追加します:

  1. *vueroute というワイルドカードを使用して、Railsルートファイルにキャッチオールルートを追加します:

    # example from ee/config/routes/project.rb
       
    resources :iteration_cadences, path: 'cadences(/*vueroute)', action: :index
    

    上記の例では、iteration_cadences コントローラからpath の先頭にマッチする任意のルート、たとえばgroupname/projectname/-/cadences/123/456/index ページを送信します。

  2. ベースルート(*vueroute より前のすべて)をフロントエンドに渡し、base パラメータとして使用して Vue Router を初期化します:

    .js-my-app{ data: { base_path: project_iteration_cadences_path(project) } }
    
  3. ルータを初期化します:

    Vue.use(VueRouter);
       
    export function createRouter(basePath) {
      return new VueRouter({
        routes: createRoutes(),
        mode: 'history',
        base: basePath,
      });
    }
    
  4. path: '*' で認識できないルートのフォールバックを追加します。のどちらかです:
    • routes配列の最後にリダイレクトを追加します:

       const routes = [
         {
           path: '/',
           name: 'list-page',
           component: ListPage,
         },
         {
           path: '*',
           redirect: '/',
         },
       ];
      
    • fallbackコンポーネントをroutes配列の最後に追加します:

       const routes = [
         {
           path: '/',
           name: 'list-page',
           component: ListPage,
         },
         {
           path: '*',
           component: NotFound,
         },
       ];
      
  5. オプション。子ルートでもパスヘルパーを使用できるようにするには、controlleraction パラメータを追加して親コントローラを使用するようにします。

    resources :iteration_cadences, path: 'cadences(/*vueroute)', action: :index do
      resources :iterations, only: [:index, :new, :edit, :show], constraints: { id: /\d+/ }, controller: :iteration_cadences, action: :index
    end
    

    これは/cadences/123/iterations/456/edit のようなルートをバックエンドでバリデートできることを意味し、たとえばグループやプロジェクトのメンバーシップをチェックできます。また、_path ヘルパーを使用できることを意味します。つまり、パスの*vueroute 部分を手動で構築することなく、feature specs でページをロードできます。

VueとjQueryの混合

  • VueとjQueryの混在は推奨されません。
  • Vueで特定のjQueryプラグインを使用するには、その周りのラッパーを作成します。
  • Vueは、jQueryイベントリスナーを使用して、既存のjQueryイベントをリッスンすることができます。
  • VueがjQueryとやりとりするために、新しいjQueryイベントを追加することは推奨されません。

VueクラスとJavaScriptクラスの混在(データ関数内)

Vueのドキュメントでは、Data関数/オブジェクトは以下のように定義されています:

Vueインスタンスのデータオブジェクト。Vue は、そのプロパティをゲッター/セッターに再帰的に変換して「リアクティブ」にします。オブジェクトはプレーンでなければなりません。ブラウザAPIオブジェクトやプロトタイププロパティなどのネイティブオブジェクトは無視されます。独自のステートフルな振る舞いを持つオブジェクトを観察することは推奨されません。

Vueのガイダンスに基づきます:

  • データ関数内でJavaScriptクラスを使用したり作成したりしないでください。
  • 新しい JavaScript クラスの実装を追加しないでください。
  • プリミティブやオブジェクトを使用できない場合は GraphQLVuex、またはコンポーネントのセットを使用してください。
  • そのようなアプローチを使用した既存の実装をメンテナーしてください。
  • コンポーネントに大幅な変更があった場合はコンポーネントを純粋なオブジェクトモデルにマイグレーションしてください。
  • ヘルパーやユーティリティにビジネスロジックを追加し、コンポーネントとは別にテストできるようにします

なぜ

JavaScriptのクラスが巨大なコードベースにおいてメンテナーの問題を引き起こすその他の理由:

  • クラスが作成された後、Vueの反応性やベストプラクティスを侵害するような方法で拡張される可能性があります。
  • クラスは抽象化のレイヤーを追加するため、コンポーネントの API とその内部構造がわかりにくくなります。
  • テストが難しくなります。コンポーネント・データ関数によってクラスがインスタンス化されるため、コンポーネントとクラスを別々に「管理」することが難しくなります。
  • オブジェクト指向の原則(OOP) を関数型コードベースに追加すると、コードの書き方がもうひとつ増え、一貫性と明快さが失われます。

スタイルガイド

Vue コンポーネントやテンプレートを作成、テストする際のベストプラクティスについては、スタイルガイドのVue セクションを参照してください。

コンポジションAPI

Vue 2.7では、Composer APIをVueコンポーネントやスタンドアロンのComposerableとして使用できます。

<script><script setup>

Composition API を使用すると、コンポーネントの<script> セクションにロジックを配置したり、専用の<script setup> セクションを設けたりすることができます。<script> を使用し、setup() プロパティを使用してコンポーネントに Composition API を追加する必要があります:

<script>
  import { computed } from 'vue';

  export default {
    name: 'MyComponent',
    setup(props) {
      const doubleCount = computed(() => props.count*2)
    }
  }
</script>

v-bind 制限

どうしても必要な場合を除き、v-bind="$attrs" の使用は避けてください。ネイティブ・コントロールのラッパーを開発するときに必要になるかもしれません。 (これはgitlab-ui コンポーネントの良い候補です。) それ以外の場合は、常にprops と明示的なデータフローを使用することを優先してください。

v-bind="$attrs"

  1. コンポーネントの契約の損失。props は、この問題にアドレスするために特別に設計されました。
  2. v-bind="$attrs" 、データの流れを理解するためにコンポーネントの階層全体をスキャンする必要があるため、特にデバッグが困難。
  3. Vue 3へのマイグレーション時の問題。Vue 3の$attrs 、イベントリスナーが含まれているため、Vue 3のマイグレーション完了後に予期せぬ副作用が発生する可能性があります。

コンポーネントごとに1つのAPIスタイルを目指す

Vue コンポーネントにsetup() プロパティを追加する場合は、完全に Composition API にリファクタリングすることを検討してください。特に大規模なコンポーネントの場合、常に実現可能というわけではありませんが、可読性とメンテナーのために、コンポーネントごとにAPIスタイルを1つにすることを目指すべきです。

Composer

Composer APIでは、リアクティブな状態を含むロジックを_Composerableに_抽象化する新しい方法があります。Composerは、パラメータを受け取り、Vueコンポーネントで使用するリアクティブなプロパティやメソッドを返すことができる関数です。

// useCount.js
import { ref } from 'vue';

export function useCount(initialValue) {
  const count = ref(initialValue)

  function incrementCount() {
    count.value += 1
  }

  function decrementCount() {
    count.value -= 1
  }

  return { count, incrementCount, decrementCount }
}
// MyComponent.vue
import { useCount } from 'useCount'

export default {
  name: 'MyComponent',
  setup() {
    const { count, incrementCount, decrementCount } = useCount(5)

    return { count, incrementCount, decrementCount }
  }
}

関数名やファイル名の前にuse

Vue におけるコンポーザブルの一般的な命名規則は、use を先頭に付け、コンポーザブルな機能を簡潔に参照することです (useBreakpoints,useGeolocation など)。同じルールが、コンポーザブルを含む.js ファイルにも適用されます。ファイルが複数のコンポーザブルを含む場合でも、use_ で始める必要があります。

ライフサイクルの落とし穴を避ける

Composerを作るときは、できるだけシンプルにすることを目指しましょう。ライフサイクルフックはComposerに複雑さを与え、予期せぬ副作用を引き起こすかもしれません。それを避けるために、以下の原則に従いましょう:

  • 可能な限りライフサイクルフックの使用を最小限にし、代わりにコールバックを受け取ったり返したりします。
  • Composerにライフサイクルフックが必要な場合は、クリーンアップも行うようにしてください。onMounted でリスナーを追加した場合、同じ Composer 内のonUnmounted でリスナーを削除する必要があります。
  • ライフサイクルフックは常にすぐにセットアップしてください:
// bad
const useAsyncLogic = () => {
  const action = async () => {
    await doSomething();
    onMounted(doSomethingElse);
  };
  return { action };
};

// OK
const useAsyncLogic = () => {
  const done = ref(false);
  onMounted(() => {
    watch(
      done,
      () => done.value && doSomethingElse(),
      { immediate: true },
    );
  });
  const action = async () => {
    await doSomething();
    done.value = true;
  };
  return { action };
};

脱出ハッチを避ける

Vueが提供するエスケープハッチを使って、すべてをブラックボックスとして行うComposerを書きたくなるかもしれません。しかし、ほとんどの場合、それでは複雑すぎてメンテナーが大変です。一つの逃げ道はgetCurrentInstance メソッドです。このメソッドは、現在のレンダリングコンポーネントのインスタンスを返します。このメソッドを使用する代わりに、引数を介して Composer にデータやメソッドを渡すことをお勧めします。

const useSomeLogic = () => {
  doSomeLogic();
  getCurrentInstance().emit('done'); // bad
};
const done = () => emit('done');

const useSomeLogic = (done) => {
  doSomeLogic();
  done(); // good, composable doesn't try to be too smart
}

ComposerとVuex

ComposerでVuexの状態を使用するのは避けたいものです。それが不可能な場合は、propsを使用してその状態を受け取り、setup からイベントを発行してVuexの状態を更新します。親コンポーネントは、Vuexからその状態を取得し、子コンポーネントから発行されるイベントでその状態を変更する責任を負う必要があります。プロップから降りてくる状態を決して変異させるべきではありません。ComposerがVuexの状態を変更する必要がある場合は、コールバックを使用してイベントを発行する必要があります。

const useAsyncComposable = ({ state, update }) => {
  const start = async () => {
    const newState = await doSomething(state);
    update(newState);
  };
  return { start };
};

const ComponentWithComposable = {
  setup(props, { emit }) {
    const update = (data) => emit('update', data);
    const state = computed(() => props.state); // state from Vuex
    const { start } = useAsyncComposable({ state, update });
    start();
  },
};

Composer のテスト

Vueコンポーネントのテスト

Vue コンポーネントをテストするためのガイドラインとベストプラクティスについては、Vue テストスタイルガイドを参照してください。

各Vueコンポーネントには固有の出力があります。この出力は、レンダリング関数の中に常に存在します。

Vueコンポーネントの各メソッドは個別にテストできますが、私たちの目標は、常に状態を表すrender関数の出力をテストすることです。

Vueのテストガイドをご覧ください。

以下は、このVueコンポーネントのよく構造化されたユニットテストの例です:

import { GlLoadingIcon } from '@gitlab/ui';
import MockAdapter from 'axios-mock-adapter';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import axios from '~/lib/utils/axios_utils';
import App from '~/todos/app.vue';

const TEST_TODOS = [{ text: 'Lorem ipsum test text' }, { text: 'Lorem ipsum 2' }];
const TEST_NEW_TODO = 'New todo title';
const TEST_TODO_PATH = '/todos';

describe('~/todos/app.vue', () => {
  let wrapper;
  let mock;

  beforeEach(() => {
    // IMPORTANT: Use axios-mock-adapter for stubbing axios API requests
    mock = new MockAdapter(axios);
    mock.onGet(TEST_TODO_PATH).reply(200, TEST_TODOS);
    mock.onPost(TEST_TODO_PATH).reply(200);
  });

  afterEach(() => {
    // IMPORTANT: Clean up the axios mock adapter
    mock.restore();
  });

  // It is very helpful to separate setting up the component from
  // its collaborators (for example, Vuex and axios).
  const createWrapper = (props = {}) => {
    wrapper = shallowMountExtended(App, {
      propsData: {
        path: TEST_TODO_PATH,
        ...props,
      },
    });
  };
  // Helper methods greatly help test maintainability and readability.
  const findLoader = () => wrapper.findComponent(GlLoadingIcon);
  const findAddButton = () => wrapper.findByTestId('add-button');
  const findTextInput = () => wrapper.findByTestId('text-input');
  const findTodoData = () =>
    wrapper
      .findAllByTestId('todo-item')
      .wrappers.map((item) => ({ text: item.text() }));

  describe('when mounted and loading', () => {
    beforeEach(() => {
      // Create request which will never resolve
      mock.onGet(TEST_TODO_PATH).reply(() => new Promise(() => {}));
      createWrapper();
    });

    it('should render the loading state', () => {
      expect(findLoader().exists()).toBe(true);
    });
  });

  describe('when todos are loaded', () => {
    beforeEach(() => {
      createWrapper();
      // IMPORTANT: This component fetches data asynchronously on mount, so let's wait for the Vue template to update
      return wrapper.vm.$nextTick();
    });

    it('should not show loading', () => {
      expect(findLoader().exists()).toBe(false);
    });

    it('should render todos', () => {
      expect(findTodoData()).toEqual(TEST_TODOS);
    });

    it('when todo is added, should post new todo', async () => {
      findTextInput().vm.$emit('update', TEST_NEW_TODO);
      findAddButton().vm.$emit('click');

      await wrapper.vm.$nextTick();

      expect(mock.history.post.map((x) => JSON.parse(x.data))).toEqual([{ text: TEST_NEW_TODO }]);
    });
  });
});

子コンポーネント

  1. 子コンポーネントがレンダリングされるかどうかを定義するディレクティブをテストしてください (例えば、v-ifv-for)。
  2. 子コンポーネントに渡すpropsをテストしてください(特に、propsがテスト対象のコンポーネントで計算される場合、たとえば、computed プロパティで)。.vm.somePropではなく.props() を使用することを忘れないでください。
  3. 子コンポーネントから発せられるイベントに正しく反応することをテストします:
const checkbox = wrapper.findByTestId('checkboxTestId');

expect(checkbox.attributes('disabled')).not.toBeDefined();

findChildComponent().vm.$emit('primary');
await nextTick();

expect(checkbox.attributes('disabled')).toBeDefined();
  1. 子コンポーネントの内部実装はテストしないでください:
// bad
expect(findChildComponent().find('.error-alert').exists()).toBe(false);

// good
expect(findChildComponent().props('withAlertContainer')).toBe(false);

イベント

コンポーネントのアクションに応じて発生するイベントをテストする必要があります。このテストでは、正しいイベントが正しい引数で発生することを確認します。

DOM イベントの場合は、trigger を使ってイベントを発生させます。

// Assuming SomeButton renders: <button>Some button</button>
wrapper = mount(SomeButton);

...
it('should fire the click event', () => {
  const btn = wrapper.find('button')

  btn.trigger('click');
  ...
})

Vue のイベントを発生させる場合は、emit を使用します。

wrapper = shallowMount(DropdownItem);

...

it('should fire the itemClicked event', () => {
  DropdownItem.vm.$emit('itemClicked');
  ...
})

emitted() メソッドの結果に対してアサートすることで、イベントが発生したことを確認する必要があります。

Vue.jsエキスパートのロール

Vue.jsのエキスパートになるには、あなた自身のマージリクエストとあなたのレビューで明らかになったときのみ申請してください:

  • VueとVuexの反応性に対する深い理解
  • VueとVuexのコードは、公式と私たちのガイドラインの両方に従って構造化されています。
  • VueおよびVuexアプリケーションのテストに関する完全な理解
  • Vuexコードは文書化されたパターンに従っています
  • 既存のVueおよびVuexアプリケーションと既存の再利用可能なコンポーネントに関する知識

Vue 2 -> Vue 3へのマイグレーション

このセクションは、コードベースをVue 2.xからVue 3.xにマイグレーションする作業をサポートするために一時的に追加されます。

最終的なマイグレーションのための技術的負債を増やさないために、コードベースへの特定の機能の追加は最小限に抑えることをお勧めします:

  • フィルタです;
  • イベントバス;
  • 機能テンプレート
  • slot 属性

詳細はVue 3へのマイグレーションをご覧ください。

付録 - テスト対象のVueコンポーネント

これは、Vueコンポーネントのテストセクションでテストされるサンプルコンポーネントのテンプレートです:

<template>
  <div class="content">
    <gl-loading-icon v-if="isLoading" />
    <template v-else>
      <div
        v-for="todo in todos"
        :key="todo.id"
        :class="{ 'gl-strike': todo.isDone }"
        data-testid="todo-item"
      >{{ toddo.text }}</div>
      <footer class="gl-border-t-1 gl-mt-3 gl-pt-3">
        <gl-form-input
          type="text"
          v-model="todoText"
          data-testid="text-input"
        >
        <gl-button
          variant="confirm"
          data-testid="add-button"
          @click="addTodo"
        >Add</gl-button>
      </footer>
    </template>
  </div>
</template>