無限スクロールと言えば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
+ },
