@better-scroll/zoom
Version:
a BetterScroll plugin to enlarge or narrow
616 lines (553 loc) • 16.6 kB
text/typescript
import BScroll from '@better-scroll/core'
import Zoom from '../index'
import { ease } from '@better-scroll/shared-utils'
import { createTouchEvent, createZoomElements } from './__utils__/util'
jest.mock('@better-scroll/core')
jest.mock('@better-scroll/core/src/animater/index')
describe('zoom plugin', () => {
let scroll: BScroll
beforeEach(() => {
// create DOM
const { wrapper } = createZoomElements()
scroll = new BScroll(wrapper)
})
afterEach(() => {
jest.clearAllMocks()
})
it('should proxy properties to BScroll instance', () => {
new Zoom(scroll)
expect(scroll.proxy).toBeCalled()
expect(scroll.proxy).toHaveBeenLastCalledWith([
{
key: 'zoomTo',
sourceKey: 'plugins.zoom.zoomTo',
},
])
})
it('should register hooks to BScroll instance', () => {
new Zoom(scroll)
expect(scroll.registerType).toBeCalled()
expect(scroll.registerType).toHaveBeenLastCalledWith([
'beforeZoomStart',
'zoomStart',
'zooming',
'zoomEnd',
])
expect(scroll.eventTypes.beforeZoomStart).toEqual('beforeZoomStart')
expect(scroll.eventTypes.zoomStart).toEqual('zoomStart')
})
it('should handle default options and user options', () => {
// case 1
scroll.options.zoom = true
let zoom = new Zoom(scroll)
expect(zoom.zoomOpt).toMatchObject({
start: 1,
min: 1,
max: 4,
initialOrigin: [0, 0],
minimalZoomDistance: 5,
bounceTime: 800,
})
// case 2
scroll.options.zoom = {
initialOrigin: ['center', 'center'],
bounceTime: 300,
}
zoom = new Zoom(scroll)
expect(zoom.zoomOpt).toMatchObject({
start: 1,
min: 1,
max: 4,
initialOrigin: ['center', 'center'],
minimalZoomDistance: 5,
bounceTime: 300,
})
})
it('should try initialZoomTo when new zoom()', () => {
// start is 1, no zoomTo
new Zoom(scroll)
expect(scroll.scroller.scrollTo).toBeCalledTimes(0)
// start !== 1
scroll.options.zoom = {
start: 1.5,
initialOrigin: [0, 0],
}
new Zoom(scroll)
expect(scroll.scroller.scrollTo).toBeCalledTimes(1)
expect(scroll.scroller.scrollTo).toHaveBeenLastCalledWith(
0,
0,
0,
ease.bounce,
{
start: {
scale: 1,
},
end: {
scale: 1.5,
},
}
)
// start should <= max
scroll.options.zoom = {
start: 3.5,
max: 3,
initialOrigin: [0, 0],
}
new Zoom(scroll)
expect(scroll.scroller.scrollTo).toBeCalledTimes(2)
expect(scroll.scroller.scrollTo).toHaveBeenLastCalledWith(
0,
0,
0,
ease.bounce,
{
start: {
scale: 1,
},
end: {
scale: 3, // equals max
},
}
)
})
it("should set scaled element's transform origin", () => {
new Zoom(scroll)
expect(scroll.scroller.content.style['transform-origin' as any]).toBe('0 0')
})
it('should not response with one finger', () => {
const zoom = new Zoom(scroll)
const hooks = scroll.scroller.actions.hooks
const zoomStartSpy = jest.spyOn(zoom, 'zoomStart')
const zoomSpy = jest.spyOn(zoom, 'zoom')
const zoomEndSpy = jest.spyOn(zoom, 'zoomEnd')
const e = createTouchEvent({ pageX: 0, pageY: 0 })
hooks.trigger(hooks.eventTypes.start, e)
expect(zoomStartSpy).not.toHaveBeenCalled()
hooks.trigger(hooks.eventTypes.beforeMove, e)
expect(zoomSpy).not.toHaveBeenCalled()
hooks.trigger(hooks.eventTypes.beforeEnd, e)
expect(zoomEndSpy).not.toHaveBeenCalled()
})
it('should compute boundary of Behavior when zoom ends', () => {
const zoom = new Zoom(scroll) as any
// simulate two fingers
zoom.numberOfFingers = 2
// allow trigger beforeEnd hooks
zoom.zoomed = true
const e = createTouchEvent({ pageX: 0, pageY: 0 }, { pageX: 20, pageY: 20 })
const actions = scroll.scroller.actions
const behaviorX = scroll.scroller.scrollBehaviorX
const behaviorY = scroll.scroller.scrollBehaviorY
behaviorX.checkInBoundary = jest.fn().mockImplementation(() => {
return { inBoundary: true }
})
behaviorY.checkInBoundary = jest.fn().mockImplementation(() => {
return { inBoundary: true }
})
actions.hooks.trigger(actions.hooks.eventTypes.beforeEnd, e)
expect(behaviorX.computeBoundary).toHaveBeenCalled()
expect(behaviorY.computeBoundary).toHaveBeenCalled()
// we should zoomed before call zoomEnd
zoom.zoomed = false
actions.hooks.trigger(actions.hooks.eventTypes.beforeEnd, e)
expect(behaviorX.computeBoundary).toBeCalledTimes(1)
})
it('should fail when zooming distance < minimalZoomDistance', () => {
scroll.options.zoom = {
minimalZoomDistance: 10,
}
new Zoom(scroll)
const actions = scroll.scroller.actions
const mockZoomingFn = jest.fn()
scroll.on(scroll.eventTypes.zooming, mockZoomingFn)
// zoomStart
const e = createTouchEvent(
{ pageX: 30, pageY: 30 },
{ pageX: 130, pageY: 130 }
)
actions.hooks.trigger(actions.hooks.eventTypes.start, e)
// zoom
const e2 = createTouchEvent(
{ pageX: 30, pageY: 30 },
{ pageX: 135, pageY: 135 }
)
actions.hooks.trigger(actions.hooks.eventTypes.beforeMove, e2)
expect(mockZoomingFn).toHaveBeenCalledTimes(0)
})
it('should have correct behavior when zooming out', () => {
scroll.options.zoom = {
max: 2,
}
const zoom = new Zoom(scroll)
const actions = scroll.scroller.actions
const translater = scroll.scroller.translater
const mockZoomingFn = jest.fn()
scroll.on(scroll.eventTypes.zooming, mockZoomingFn)
// zoomStart
const e = createTouchEvent(
{ pageX: 30, pageY: 30 },
{ pageX: 130, pageY: 130 }
)
actions.hooks.trigger(actions.hooks.eventTypes.start, e)
// zoom
const e2 = createTouchEvent(
{ pageX: 30, pageY: 30 },
{ pageX: 150, pageY: 150 }
)
actions.hooks.trigger(actions.hooks.eventTypes.beforeMove, e2)
// triggered zooming hooks
expect(mockZoomingFn).toHaveBeenCalled()
expect(mockZoomingFn).toHaveBeenCalledTimes(1)
// beforeMove hooks use translater.translate, not scroller.scrollTo
expect(scroll.scroller.translater.translate).toBeCalledWith({
x: -16,
y: -16,
scale: 1.2,
})
expect(zoom.scale).toBe(1.2)
// triggered beforeTranslate hooks
const transformString: string[] = []
const transformPoint = {
scale: 1.2,
}
translater.hooks.trigger(
translater.hooks.eventTypes.beforeTranslate,
transformString,
transformPoint
)
expect(transformString[0]).toBe('scale(1.2)')
// keep zoom
const e3 = createTouchEvent(
{ pageX: 30, pageY: 30 },
{ pageX: 170, pageY: 170 }
)
actions.hooks.trigger('beforeMove', e3)
// triggered zooming hooks
expect(mockZoomingFn).toHaveBeenCalledTimes(2)
expect(scroll.scroller.translater.translate).toHaveBeenLastCalledWith({
x: -32,
y: -32,
scale: 1.4,
})
expect(zoom.scale).toBe(1.4)
// keep zoom, allow zooming exceeds max
const e4 = createTouchEvent(
{ pageX: 30, pageY: 30 },
{ pageX: 240, pageY: 240 }
)
actions.hooks.trigger('beforeMove', e4)
// triggered zooming hooks
expect(mockZoomingFn).toHaveBeenCalledTimes(3)
expect(scroll.scroller.translater.translate).toHaveBeenLastCalledWith({
x: -85,
y: -85,
scale: 2 * 2 * Math.pow(0.5, 2 / 2.1),
})
expect(zoom.scale).toBeCloseTo(2.067)
// zoom end, perform a rebound animation,back to max scale
actions.hooks.trigger('beforeEnd')
expect(zoom.scale).toBe(2)
expect(scroll.scroller.scrollTo).toHaveBeenLastCalledWith(
0,
0,
800,
ease.bounce,
{
start: {
scale: 2.0671155660140554,
},
end: {
scale: 2,
},
}
)
})
it('should have correct behavior when zooming in', () => {
scroll.options.zoom = {
min: 0.5,
}
const zoom = new Zoom(scroll)
const actions = scroll.scroller.actions
const translater = scroll.scroller.translater
const mockZoomingFn = jest.fn()
scroll.on(scroll.eventTypes.zooming, mockZoomingFn)
// zoomStart
const e = createTouchEvent(
{ pageX: 30, pageY: 30 },
{ pageX: 130, pageY: 130 }
)
actions.hooks.trigger(actions.hooks.eventTypes.start, e)
// zoom
const e2 = createTouchEvent(
{ pageX: 30, pageY: 30 },
{ pageX: 110, pageY: 110 }
)
actions.hooks.trigger(actions.hooks.eventTypes.beforeMove, e2)
// triggered zooming hooks
expect(mockZoomingFn).toHaveBeenCalled()
expect(mockZoomingFn).toHaveBeenCalledTimes(1)
// beforeMove hooks use translater.translate, not scroller.scrollTo
expect(scroll.scroller.translater.translate).toBeCalledWith({
x: 16,
y: 16,
scale: 0.8,
})
expect(zoom.scale).toBe(0.8)
// triggered beforeTranslate hooks
const transformString: string[] = []
const transformPoint = {
scale: 0.8,
}
translater.hooks.trigger(
translater.hooks.eventTypes.beforeTranslate,
transformString,
transformPoint
)
expect(transformString[0]).toBe('scale(0.8)')
// keep zoom
const e3 = createTouchEvent(
{ pageX: 30, pageY: 30 },
{ pageX: 90, pageY: 90 }
)
actions.hooks.trigger('beforeMove', e3)
// triggered zooming hooks
expect(mockZoomingFn).toHaveBeenCalledTimes(2)
expect(scroll.scroller.translater.translate).toHaveBeenLastCalledWith({
x: 32,
y: 32,
scale: 0.6,
})
expect(zoom.scale).toBe(0.6)
// keep zoom, allow zooming exceeds max
const e4 = createTouchEvent(
{ pageX: 30, pageY: 30 },
{ pageX: 40, pageY: 40 }
)
actions.hooks.trigger('beforeMove', e4)
// triggered zooming hooks
expect(mockZoomingFn).toHaveBeenCalledTimes(3)
expect(scroll.scroller.translater.translate).toHaveBeenLastCalledWith({
x: 57,
y: 57,
scale: 0.5 * 0.5 * Math.pow(2, 0.1 / 0.5),
})
expect(zoom.scale).toBeCloseTo(0.287)
// zoom end, perform a rebound animation,back to max scale
actions.hooks.trigger('beforeEnd')
expect(zoom.scale).toBe(0.5)
expect(scroll.scroller.scrollTo).toHaveBeenLastCalledWith(
0,
0,
800,
ease.bounce,
{
start: {
scale: 0.2871745887492588,
},
end: {
scale: 0.5,
},
}
)
})
it('should have correct behavior for zoomTo', (done) => {
scroll.options.zoom = {
min: 0.5,
max: 3,
start: 1,
}
const zoom = new Zoom(scroll)
const { scrollBehaviorX, scrollBehaviorY } = scroll.scroller
scrollBehaviorX.contentSize = 100
scrollBehaviorY.contentSize = 100
scrollBehaviorX.wrapperSize = 100
scrollBehaviorY.wrapperSize = 100
// [0, 0] as origin, scale to 2
zoom.zoomTo(2, 0, 0)
expect(scroll.scroller.scrollTo).toHaveBeenCalledWith(
0,
0,
800,
ease.bounce,
{
start: {
scale: 1,
},
end: {
scale: 2,
},
}
)
// ['center', 'center'] as origin, time is 300, scale to 1.5
zoom.zoomTo(1.5, 'center', 'center', 300)
expect(scroll.scroller.scrollTo).toHaveBeenCalledWith(
0,
0,
300,
ease.bounce,
{
start: {
scale: 2,
},
end: {
scale: 1.5,
},
}
)
// ['left', 'top'] as origin, time is 300, scale to 3
zoom.zoomTo(3, 'left', 'top', 300)
expect(scroll.scroller.scrollTo).toHaveBeenCalledWith(
0,
0,
300,
ease.bounce,
{
start: {
scale: 1.5,
},
end: {
scale: 3,
},
}
)
// ['right', 'bottom'] as origin, time is 300, scale to 3
zoom.zoomTo(2, 'right', 'bottom', 300)
expect(scroll.scroller.scrollTo).toHaveBeenCalledWith(
0,
0,
300,
ease.bounce,
{
start: {
scale: 3,
},
end: {
scale: 2,
},
}
)
// The purpose for improving test coverage
setTimeout(() => {
done()
}, 320)
})
it('should support full hooks', () => {
scroll.options.zoom = {
min: 1,
start: 1,
max: 4,
}
new Zoom(scroll)
const actions = scroll.scroller.actions
const behaviorX = scroll.scroller.scrollBehaviorX
const behaviorY = scroll.scroller.scrollBehaviorY
behaviorX.checkInBoundary = jest.fn().mockImplementation(() => {
return { inBoundary: true }
})
behaviorY.checkInBoundary = jest.fn().mockImplementation(() => {
return { inBoundary: true }
})
const mockBeforeZoomStartFn = jest.fn()
const mockZoomStartFn = jest.fn()
const mockZoomingFn = jest.fn()
const mockZoomEndFn = jest.fn()
// tap hooks
scroll.on(scroll.eventTypes.beforeZoomStart, mockBeforeZoomStartFn)
scroll.on(scroll.eventTypes.zoomStart, mockZoomStartFn)
scroll.on(scroll.eventTypes.zooming, mockZoomingFn)
scroll.on(scroll.eventTypes.zoomEnd, mockZoomEndFn)
// zoomStart
const e1 = createTouchEvent(
{ pageX: 30, pageY: 30 },
{ pageX: 130, pageY: 130 }
)
actions.hooks.trigger(actions.hooks.eventTypes.start, e1)
// zooming
const e2 = createTouchEvent(
{ pageX: 30, pageY: 30 },
{ pageX: 150, pageY: 150 }
)
actions.hooks.trigger(actions.hooks.eventTypes.beforeMove, e2)
// zoomEnd
actions.hooks.trigger(actions.hooks.eventTypes.beforeEnd)
expect(mockBeforeZoomStartFn).toBeCalledTimes(1)
expect(mockZoomStartFn).toBeCalledTimes(1)
expect(mockZoomingFn).toBeCalledTimes(1)
expect(mockZoomEndFn).toBeCalledTimes(1)
})
it('should destroy all events', () => {
new Zoom(scroll)
const {
actions,
scrollBehaviorX,
scrollBehaviorY,
translater,
} = scroll.scroller
scroll.hooks.trigger(scroll.hooks.eventTypes.destroy)
expect(scrollBehaviorX.hooks.events['beforeComputeBoundary'].length).toBe(0)
expect(scrollBehaviorY.hooks.events['beforeComputeBoundary'].length).toBe(0)
expect(actions.hooks.events['start'].length).toBe(0)
expect(actions.hooks.events['beforeMove'].length).toBe(0)
expect(actions.hooks.events['beforeEnd'].length).toBe(0)
expect(translater.hooks.events['beforeTranslate'].length).toBe(0)
})
it('should work well when content DOM has changed', () => {
const zoom = new Zoom(scroll)
const newContent = document.createElement('p')
scroll.hooks.trigger(scroll.hooks.eventTypes.contentChanged, newContent)
expect(zoom.scale).toBe(1)
expect(newContent.style['transform-origin' as any]).toBe('0 0')
})
it('should prevent initial scroll when startScale not equals 1', () => {
const { wrapper } = createZoomElements()
scroll = new BScroll(wrapper, {
zoom: {
start: 2,
},
})
new Zoom(scroll)
const ret = scroll.hooks.trigger(
scroll.hooks.eventTypes.beforeInitialScrollTo
)
expect(ret).toBeTruthy()
})
it('should calculate right size when scrollBehavior triggered beforeComputeBoundary hook', () => {
const zoom = new Zoom(scroll)
zoom.scale = 1.2
const scrollBehaviorX = scroll.scroller.scrollBehaviorX
const scrollBehaviorY = scroll.scroller.scrollBehaviorY
scrollBehaviorX.hooks.trigger(
scrollBehaviorX.hooks.eventTypes.beforeComputeBoundary
)
scrollBehaviorY.hooks.trigger(
scrollBehaviorY.hooks.eventTypes.beforeComputeBoundary
)
expect(scrollBehaviorX.contentSize).toBe(360)
expect(scrollBehaviorY.contentSize).toBe(360)
})
it('should dispatch scrollEnd event when two fingers make bs scroll', () => {
new Zoom(scroll)
let endScale
scroll.scroller.actions.hooks.trigger(
scroll.scroller.actions.hooks.eventTypes.start,
{
touches: [
{
pageX: 1,
pageY: 1,
},
{
pageX: 2,
pageY: 2,
},
],
}
)
scroll.on(scroll.eventTypes.zoomEnd, ({ scale }: { scale: number }) => {
endScale = scale
})
scroll.scroller.hooks.trigger(scroll.scroller.hooks.eventTypes.scrollEnd)
expect(endScale).toBe(1)
})
})