無限スクロールと言えばVue-infinite-loadingで、導入記事は多いのですが、
テスト(Jest)の記事は見つからなかったのでメモしておきます。
Vue warnとTypeErrorがなかなか消せなかったり、
読み込み後のloaded/complete/errorを、どうテストすべきかで悩みました。
導入
Vue-infinite-loadingの導入は、公式通りにやればできるので、詳細は割愛します。
Guide | Vue-infinite-loading
spinner(ローディング中のくるくる)はデフォルトのを使う、
no-moreやno-resultsは不要(slotしないとデフォルトのが出ちゃう)、
errorは文言やボタンを変えたかったので、下記を参考にしました。
Configure Load Messages | Vue-infinite-loading
<InfiniteLoading v-if="space != null && space.current_page < space.total_pages" :identifier="page" @infinite="getNextSpaces" > <div slot="no-more" /> <div slot="no-results" /> <div slot="error" slot-scope="{ trigger }"> 取得できませんでした。 <v-btn @click="trigger">再取得</v-btn> </div> </InfiniteLoading>
テストの流れ
ページを表示後、下にスクロールして、APIリクエスト+データ表示を繰り返すのを再現してみます。
実際にはスクロールはせず、下記のようにinfiniteをemitしてあげれば、イベントが発火します。
wrapper.findComponent(InfiniteLoading).vm.$emit('infinite')
呼び出している変数やメソッドは記載していないので、リポジトリを参照してください。
https://dev.azure.com/nightonly/nuxt-app-origin/_git/nuxt-app-origin?path=/test/page/spaces/index.spec.js&version=GBspace_develop
初回表示 → イベント発火 → loaded(続きあり) → イベント発火 → complete(完了)の流れ
it('[無限スクロール]表示される', async () => { axiosGetMock = jest.fn() .mockImplementationOnce(() => Promise.resolve({ data: data1 })) .mockImplementationOnce(() => Promise.resolve({ data: data2 })) .mockImplementationOnce(() => Promise.resolve({ data: data3 })) const wrapper = mountFunction(false) helper.loadingTest(wrapper, Loading) await helper.sleep(1) apiCalledTest(1, params)const infiniteLoading = viewTest(wrapper, params, data1, '5件', true)let infiniteLoading = viewTest(wrapper, params, data1, '5件', true)const spaces = data1.spaces// スクロール(1回目) infiniteLoading.vm.$emit('infinite')spaces.push(...data2.spaces)await helper.sleep(1) apiCalledTest(2, params)viewTest(wrapper, params, { space: data2.space, spaces }, '5件', true, 'loaded')infiniteLoading = viewTest(wrapper, params, { space: data2.space, spaces }, '5件', true, 'loaded')const spaces = data1.spaces.concat(data2.spaces) infiniteLoading = viewTest(wrapper, params, { ...data2, spaces }, '5件', true, 'loaded') // スクロール(2回目) infiniteLoading.vm.$emit('infinite')spaces.push(...data3.spaces)await helper.sleep(1) apiCalledTest(3, params)viewTest(wrapper, params, { space: data3.space, spaces }, '5件', false, 'complete')viewTest(wrapper, params, { ...data3.space, spaces: spaces.concat(data3.spaces) }, '5件', false, 'complete') })
初回表示 → イベント発火 → error(再取得ボタン表示)の流れ
it('[無限スクロール]エラーメッセージが表示される', async () => { axiosGetMock = jest.fn() .mockImplementationOnce(() => Promise.resolve({ data: data1 })) .mockImplementationOnce(() => Promise.resolve({ data: null })) const wrapper = mountFunction(false) helper.loadingTest(wrapper, Loading) await helper.sleep(1) apiCalledTest(1, params) const infiniteLoading = viewTest(wrapper, params, data1, '5件', true) // スクロール(1回目) infiniteLoading.vm.$emit('infinite') await helper.sleep(1) helper.mockCalledTest(toastedErrorMock, 1, locales.system.error) helper.mockCalledTest(toastedInfoMock, 0) helper.mockCalledTest(routerPushMock, 0) viewTest(wrapper, params, data1, '5件', true, 'error') })
テスト用に改修
$stateのerror/loaded/completeをMockが呼ばれるようにしたかったのですが、イベントのパラメータをMock等で受ける方法に辿り着けなっかたので、不本意(テストが通過しないコードが残る)ですが変数に格納して、チェックするようにしました。
ちなみに、$state.loaded()や.complete()は下記に書き換えれば通せます。emitのテストが書けそう。ただ、.error()に対応するのが無かったのと、難しく書くのは嫌なので、元のままにしました。
loaded: () => { this.$emit('$InfiniteLoading:loaded', { target: this }); }, complete: () => { this.$emit('$InfiniteLoading:complete', { target: this }); }, error: () => { this.status = STATUS.ERROR; throttleer.reset(); },
data () { return { + testState: null // Jest用 } },
// 次頁のスペースを取得 async getNextSpaces ($state) { if (this.processing || this.space == null) { return } this.page = this.space.current_page + 1 if (!await this.onSpaces()) { + if ($state == null) { this.testState = 'error'; return } $state.error() } else if (this.space.current_page < this.space.total_pages) { + if ($state == null) { this.testState = 'loaded'; return } $state.loaded() } else { + if ($state == null) { this.testState = 'complete'; return } $state.complete() } },
Vue warnとTypeErrorに対応
reading 'tagName'/'_infiniteScrollHeight'
[Vue warn]: Error in callback for immediate watcher "forceUseInfiniteWrapper": "TypeError: Cannot read properties of null (reading 'tagName')" TypeError: Cannot read properties of null (reading 'tagName')
[Vue warn]: Error in event handler for "$InfiniteLoading:reset": "TypeError: Cannot read properties of null (reading '_infiniteScrollHeight')" TypeError: Cannot read properties of null (reading '_infiniteScrollHeight')
stubsに入れることで解消
const wrapper = mount(Page, { localVue, vuetify, stubs: { Lists: true, + InfiniteLoading: true },
reading 'loaded'/'complete'
[Vue warn]: Error in v-on handler (Promise/async): "TypeError: Cannot read properties of undefined (reading 'loaded')" TypeError: Cannot read properties of undefined (reading 'loaded')
[Vue warn]: Error in v-on handler (Promise/async): "TypeError: Cannot read properties of undefined (reading 'complete')" TypeError: Cannot read properties of undefined (reading 'complete')
テスト用に改修で対応済みですが、Jestからの場合、イベント引数の$stateがundefinedになる為。
ちなみにjsでは、$に特別な意味はないんですね。アルファベットと同じ扱いですが、可読性の為に公式でも$を使っている。
} else if (this.space.current_page < this.space.total_pages) { + if ($state == null) { this.testState = 'loaded'; return } $state.loaded() } else { + if ($state == null) { this.testState = 'complete'; return } $state.complete() }
reading 'push'
[Vue warn]: Error in v-on handler (Promise/async): "TypeError: Cannot read properties of undefined (reading 'push')" TypeError: Cannot read properties of undefined (reading 'push')
これは単なる凡ミスです。mocksの定義忘れ。
const wrapper = mount(Page, { <省略> mocks: { + $router: { + push: routerPushMock + },