useInfiniteScroll 官方 Demo:https://vueuse.org/core/useInfiniteScroll/#useinfinitescroll
import { flushPromises } from '@vue/test-utils'
import { describe, expect, it, vi } from 'vitest'
import { ref } from 'vue'
import { useInfiniteScroll } from '@/compositions/useInfiniteScroll'
import { useElementVisibility } from '@/compositions/useElementVisibility'
vi.mock('@/compositions/useElementVisibility')
describe('useInfiniteScroll', () => {
// 測試案例放這邊
function givenMockElement({
scrollHeight = 0,
} = {}) {
const mockElement = document.createElement('div')
Object.defineProperty(mockElement, 'scrollHeight', {
value: scrollHeight,
})
return mockElement
}
function givenElementVisibilityRefMock(defaultValue) {
const mockVisibilityRef = ref(defaultValue)
// TS
// vi.mocked(useElementVisibility).mockReturnValue(mockVisibilityRef)
// JS
useElementVisibility.mockReturnValue(mockVisibilityRef)
return mockVisibilityRef
}
})
先來看看 givenMockElement
這個 helper function,主要在測試案例中用來取得可以客製 scrollHeight
的 mockElement。這邊用到 Object.defineProperty
主要是因為 scrollHeight
是一個 read-only 的屬性。
接著來看 givenElementVisibilityRefMock
這個 helper function,主要在測試案例中 mock useElementVisibility 的回傳值,來測試 scroll container 出現或沒出現在可視範圍的情境。
因為要使用 mockReturnValue
所以開頭有 mock @/compositions/useElementVisibility
這個模組。
接下來的測試案例都會放在 // 測試案例放這邊
這個註解的層級。
it.each([
[ref(givenMockElement())],
[givenMockElement()],
[document],
[window],
])('should calls the loadMore handler, when element is visible', (target) => {
const mockHandler = vi.fn()
givenElementVisibilityRefMock(true)
useInfiniteScroll(target, mockHandler)
expect(mockHandler).toHaveBeenCalledTimes(1)
})
這個案例用到 it.each
來處理多種參數類型的測試,target 分別會拿到 ref(givenMockElement())
、givenMockElement()
、document
、window
。
useInfiniteScroll 接受的第二個參數是 loadMore
function,這邊傳入一個 mock function mockHandler
。
另外有透過 givenElementVisibilityRefMock(true)
來 mock useElementVisibility 的回傳值 isElementVisible
為 true,根據昨天看到的 useInfiniteScroll 原始碼,isElementVisible
為 true 是 loadMore
function 被執行的其中一個條件。
it('should calls the loadMore handler, when element visibility state form hidden to visible', async () => {
const mockHandler = vi.fn()
const mockElement = givenMockElement()
const visibilityRefMock = givenElementVisibilityRefMock(false)
useInfiniteScroll(mockElement, mockHandler)
expect(mockHandler).not.toHaveBeenCalled()
visibilityRefMock.value = true
await flushPromises()
expect(mockHandler).toHaveBeenCalledTimes(1)
})
這個案例主要也是透過 mock useElementVisibility 的回傳值 isElementVisible
來做測試,一開始 isElementVisible
為 false 時,要驗證 mockHandler 不能被觸發。後來透過 visibilityRefMock.value = true
,把 isElementVisible
設定為 true,useInfiniteScroll 內部的 watcher 會偵測到這個變動,進而執行 mockHandler
。
那 await flushPromises()
的用途是什麼呢?記得昨天提到 useInfiniteScroll 內部有使用到 Promise.all 來處理核心邏輯,使用 await flushPromises()
會讓當前還在 pending 的 Promise 立即完成,也會等 DOM 更新成最新狀態,才繼續後續的驗證。
vue-test-utils flushPromises
文件:https://test-utils.vuejs.org/api/#flushPromises
it('should call the loadMore handler, when user scrolls', async () => {
const mockElementScrollHeight = 100
const mockHandler = vi.fn()
const mockElement = givenMockElement({
scrollHeight: mockElementScrollHeight,
})
givenElementVisibilityRefMock(true)
useInfiniteScroll(mockElement, mockHandler)
mockElement.scrollTop = mockElementScrollHeight
mockElement.dispatchEvent(new Event('scroll'))
await flushPromises()
expect(mockHandler).toHaveBeenCalledTimes(1)
})
這個案例重點在 scroll 到目標位置,有沒有成功執行到 mockHandler
function。
透過一開始提到的 givenMockElement
helper function 把 scrollHeight 設定成 100, mock useElementVisibility 的回傳值 isElementVisible
為 true,但這個 isElementVisible
為 true 並不會觸發 mockHandler
,因為除了 isElementVisible
為 true 這個條件以外;還有另外一個條件是必須滾動到 scroll container 底部,目前還沒 scroll 所以這個條件不會成立。
接著我們把 mockElement.scrollTop
設定成 100,並使用 dispatchEvent
觸發 scroll,剛好 scroll 到最底部,最後驗證 mockHandler 有執行過一次。
GitHub:https://github.com/RhinoLee/30days_vue/pull/26/files
useInfiniteScroll API 就到今天告一段落啦~
剩餘的天數應該會來看 vueuse 是怎麼透過 VitePress 來生成官方文件的。