Vue.js + Vue.Draggable + CypressでDrag & Dropするテストを書く

# 前置き

Vue.js + Vue.Draggableでドラッグ＆ドロップ機能を実装しているWebページに対してCypressでテストコードを書いていたところ、ドラッグ＆ドロップがなかなか動作せずハマったので解決方法を記録しておきます。

# 環境

* OS: Mac OS X 10.14.5 Mojave
* Node.js: v12.6.0
* npm: 6.10.0
* yarn: 1.16.0
* Vue CLI: v3.9.3
* Vue: 2.6.10
* Vue.Draggable: 2.23.0
* Cypress: 3.4.0
* Chrome: 75
* Electron: 61
* cypress-drag-drop: 1.1.1

# ドラッグ＆ドロップが動作するテストコード

```javascript:test.js
describe('Drag and Drop test', () => {
  it('Swap ITEM-1 and ITEM-2', () => {
    cy.visit('/')

    cy.get('.items').first().as('sourceItem')
    cy.get('.items').last().as('targetItem')
    cy.get('@sourceItem')
      .trigger('pointerdown', { which: 1, button: 0 })
      .trigger('dragstart')
    cy.get('@targetItem')
      .trigger('dragover')
      .trigger('drop')

    cy.get('.items').first().should('contain', 'ITEM-2')
    cy.get('.items').last().should('contain', 'ITEM-1')
  })
})
```

<details><summary>テスト対象のvueコード</summary><div>

※draggableタグの部分は、class属性を追加した以外は公式の[サンプルコード](https://www.npmjs.com/package/vuedraggable#typical-use)をそのまま流用しています。

```vue:App.vue
<template>
  <div id="app">
    <draggable v-model="myArray" group="people" @start="drag=true" @end="drag=false">
       <div v-for="element in myArray" :key="element.id" class="items">{{element.name}}</div>
    </draggable>
  </div>
</template>

<script>
import draggable from 'vuedraggable'

export default {
  name: 'app',
  components: {
    draggable
  },
  data () {
    return {
      myArray: [
        { id: '1', name: 'ITEM-1' },
        { id: '2', name: 'ITEM-2' }
      ]
    }
  }
}
</script>

<style>
#app {
  font-family: 'Avenir', Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
  margin-top: 60px;
}
</style>
```

</div></details>

## テスト実行結果

![Succeed_drag-and-drop.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/79324/a65aa40f-b509-cb4e-7d4a-ef219002f713.gif)
※確認しやすさのためTRIGGER前後のアニメーションの再生速度が遅くなるように編集しています。実際にはCypressの実行はもっと速いです。

ドラッグ＆ドロップによりITEM-1とITEM-2の上下が入れ替わり、ASSERTに成功します。
Chrome 75、Electron 61のどちらでも上記のテストコードでドラッグ＆ドロップを実行できました。


# ドラッグ＆ドロップが動作しないテストコード

最初に書いていたテストコードです。
Cypress公式サイトの [Plugins](https://docs.cypress.io/plugins/) で紹介されている [cypress-drag-drop](https://github.com/4teamwork/cypress-drag-drop) の `drag()` を使いましたが、ドラッグ＆ドロップは動作しませんでした。

```javascript:test.js
describe('Drag and Drop test', () => {
  it('Swap ITEM-1 and ITEM-2', () => {
    cy.visit('/')
    
    cy.get('.items').first().as('sourceItem')
    cy.get('.items').last().as('targetItem')
    cy.get('@sourceItem').drag('@targetItem', 'center')

    cy.get('.items').first().should('contain', 'ITEM-2')
    cy.get('.items').last().should('contain', 'ITEM-1')
  })
})
```

```javascript:commands.js
import 'cypress-drag-drop'
```

## テスト実行結果

![Failed_drag-and-drop.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/79324/d5de7dc6-b07e-7d5c-c355-7df549e9a9f9.gif)
※動作成功例と同様に、確認しやすさのためTRIGGER前後のアニメーションの再生速度が遅くなるように編集しています。

mousedown、dragstartイベント後、dragoverイベントがループしてしまいドラッグ＆ドロップの処理が完了しません。


# 要因
デバッグしてみたところ、Vue.Draggableの対象となっているHTML要素に対して pointerdown イベントが発生すると該当要素に `draggable="true"` の属性が付与され、そのあとでないとドラッグ＆ドロップが実行できないようでした。
最初に利用しようとしていた cypress-drag-drop のプラグインでは pointerdown イベントが発生せず、それが期待通りに動作しなかった原因と考えられます。
事前に該当のHTML要素に対して `draggable="true"` を書いても動作しませんでした。

2019/3/1に登録された cypress-drag-drop の [issue](https://github.com/4teamwork/cypress-drag-drop/issues/3) では vuedraggable に対しても動作しているとのコメントがありましたので、いずれかのパッケージやブラウザのバージョンアップなどにより動かなくなってしまったのかもしれません。

なお、以下のように pointerdown イベントと組み合わせれば、cypress-drag-drop プラグインの `drag()` も期待通りに動作します。

```javascript:test.js
describe('Drag and Drop test', () => {
  it('Swap ITEM-1 and ITEM-2', () => {
    cy.visit('/')

    cy.get('.items').first().as('sourceItem')
    cy.get('.items').last().as('targetItem')
    cy.get('@sourceItem')
      .trigger('pointerdown', { which: 1, button: 0 })
      .drag('@targetItem', 'center')

    cy.get('.items').first().should('contain', 'ITEM-2')
    cy.get('.items').last().should('contain', 'ITEM-1')
  })
})
```

---
# 参考サイト

* [trigger | Cypress Documentation](https://docs.cypress.io/api/commands/trigger.html#Drag-and-Drop)
* [vuedraggable  -  npm](https://www.npmjs.com/package/vuedraggable)
* [4teamwork/cypress-drag-drop: A cypress child command for drag'n'drop support.](https://github.com/4teamwork/cypress-drag-drop)