```
- **slide-wrapper**
slide 容器。
- **slide-content**
slide 滚动元素。
- **slide-page**
slide 由多个 Page 组成。
::: tip
在 loop 的场景下,slide-content 前后会多插入两个 Page,以便实现无缝衔接滚动的视觉效果。
:::
:::danger 危险
slide-content 必须至少有一个 slide-page,如果只有一个 page,loop 的配置无效。
:::
## 示例
- **横向轮播**
<<< @/examples/vue/components/slide/banner.vue?template
<<< @/examples/vue/components/slide/banner.vue?script
<<< @/examples/vue/components/slide/banner.vue?style
- **全屏轮播**
<<< @/examples/vue/components/slide/fullpage.vue?template
<<< @/examples/vue/components/slide/fullpage.vue?script
<<< @/examples/vue/components/slide/fullpage.vue?style
- **纵向轮播**
<<< @/examples/vue/components/slide/vertical.vue?template
<<< @/examples/vue/components/slide/vertical.vue?script
<<< @/examples/vue/components/slide/vertical.vue?style
- **动态卡片轮播
**
<<< @/examples/vue/components/slide/dynamic.vue?template
<<< @/examples/vue/components/slide/dynamic.vue?script
<<< @/examples/vue/components/slide/dynamic.vue?style
- **初始化索引轮播
**
<<< @/examples/vue/components/slide/specified-index.vue?template
<<< @/examples/vue/components/slide/specified-index.vue?script
<<< @/examples/vue/components/slide/specified-index.vue?style
::: tip
注意:当设置 `useTransition = true`时,可能在 iphone 某些系统上出现闪烁。你需要像上面 demo 中的代码一样,每个 `slide-page` 额外增加下面两个样式:
```css
transform: translate3d(0,0,0)
backface-visibility: hidden
```
:::
## slide 选项对象
:::tip 提示
当 slide 配置为 true 的时候,插件内部使用的是默认的插件选项对象。
```js
const bs = new BScroll('.wrapper', {
slide: true
})
// 相当于
const bs = new BScroll('.wrapper', {
slide: {
loop: true,
threshold: 0.1,
speed: 400,
easing: ease.bounce,
listenFlick: true,
autoplay: true,
interval: 3000
}
})
```
:::
### loop
- **类型**:`boolean`
- **默认值**:`true`
是否可以循环。但是当只有一个元素的时候,该设置不生效。
### autoplay
- **类型**:`boolean`
- **默认值**:`true`
是否开启自动播放。
### interval
- **类型**:`number`
- **默认值**:`3000`
距离下一次播放的间隔。
### speed
- **类型**:`number`
- **默认值**:`400`
切换 Page 动画的默认时长。
### easing
- **类型**:`EaseItem`
- `{ string } style`:用来设置过度动画的 `transition-timing-function` 值。
- `{ Function } fn`:当设置 `useTransition:false` 时,由 `easing.fn` 来确定动画曲线。
- **默认值**:
```js
{
style: 'cubic-bezier(0.165, 0.84, 0.44, 1)',
fn: function(t: number) {
return 1 - --t * t * t * t
}
}
```
滚动的缓动效果配置。
### listenFlick
- **类型**:`boolean`
- **默认值**:`true`
当快速轻抚过 slide 区域时,会触发切换上一页/下一页。设置 listenFlick 为 false,可关闭该效果。
### threshold
- **类型**:`number`
- **默认值**:`0.1`
切换下一个或上一个 Page 的阈值。
:::tip
当滚动距离小于该阈值时,不会触发切换到下一个或上一个。
可以设置为小数,如 0.1,或者整数,如 100。当该值为小数时,threshold 被当成一个百分比,最终的阈值为 `slideWrapperWidth * threshold` 或者 `slideWrapperHeight * threshold`。当该值为整数时,则阈值就是 threshold。
:::
### startPageXIndex
- **类型**:`number`
- **默认值**:`0`
实例化 slide 的时候,滚动到横向对应索引的 page。
### startPageYIndex
- **类型**:`number`
- **默认值**:`0`
实例化 slide 的时候,滚动到竖向对应索引的 page。
## 实例方法
:::tip 提示
以下方法皆已代理至 BetterScroll 实例,例如:
```js
import BScroll from '@better-scroll/core'
import Slide from '@better-scroll/slide'
BScroll.use(Slide)
const bs = new BScroll('.bs-wrapper', {
slide: true
})
bs.next()
bs.prev()
bs.getCurrentPage()
```
:::
### next([time], [easing])
- **参数**:
- `{ number } time<可选>`:动画时长,默认是 `options.speed`
- `{ EaseItem } easing<可选>`:缓动效果配置,参考 [ease.ts](https://github.com/ustbhuangyi/better-scroll/blob/dev/packages/shared-utils/src/ease.ts),默认是 `bounce` 效果
```typescript
interface EaseItem {
style: string
fn(t: number): number
}
```
滚动到下一张。
### prev([time], [easing])
- **参数**:
- `{ number } time<可选>`:动画时长,默认是 `options.speed`
- `{ EaseItem } easing<可选>`:缓动效果配置,参考 [ease.ts](https://github.com/ustbhuangyi/better-scroll/blob/dev/packages/shared-utils/src/ease.ts),默认是 `bounce` 效果
滚动到上一张。
### goToPage(pageX, pageY, [time], [easing])
- **参数**:
- `{ number } pageX`:横向滚动到对应索引的 Page,下标从 0 开始
- `{ number } pageY`:纵向滚动到对应索引的 Page,下标从 0 开始
- `{ number } time<可选>`:动画时长,默认是 `options.speed`
- `{ EaseItem } easing<可选>`:缓动效果配置,参考 [ease.ts](https://github.com/ustbhuangyi/better-scroll/blob/dev/packages/shared-utils/src/ease.ts),默认是 `bounce` 效果
滚动到指定的 Page 位置。
### getCurrentPage()
- **返回值**: `page`
```typescript
type Page = {
x: number,
y: number,
pageX: number, // 横向对应 Page 的索引,下标从 0 开始
pageY: number // 纵向对应 Page 的索引,下标从 0 开始
}
const page:Page = BScroll.getCurrentPage()
```
获取当前页面的信息。
### startPlay()
如果开启了 loop 的配置,手动开启循环播放。
### pausePlay()
如果开启了 loop 的配置,手动关闭循环播放。
## 事件
### slideWillChange
- **参数**:page 对象
- `{ number } x`:即将展示页面的 x 坐标值
- `{ number } y`:即将展示页面的 y 坐标值
- `{ number } pageX`:即将展示的横向页面的索引值,下标从 0 开始
- `{ number } pageY`:即将展示的纵向页面的索引值,下标从 0 开始
- **触发时机**:slide 的 currentPage 值将要改变时
- **用法**:
在 banner 展示中,常常伴随着一个 dot 图例,来指示当前 banner 是第几页,例如前面“横向轮播图”的示例。当用户拖动 banner 出现下一张时,我们希望下面的 dot 图例会同步变换。如下图
![banner示例图]()
通过监听 `slideWillChange` 事件,可以实现该效果。代码如下:
```js
let currentPageIndex // 控制当前页面
const slide = new BScroll(this.$refs.slide, {
scrollX: true,
scrollY: false,
slide: {
threshold: 100
},
useTransition: true,
momentum: false,
bounce: false,
stopPropagation: true,
probeType: 2
})
slide.on('slideWillChange', (page) => {
currentPageIndex = page.pageX
})
```
### slidePageChanged
- **参数**:page 对象
- `{ number } x`:当前页面的 x 坐标值
- `{ number } y`:当前页面的 y 坐标值
- `{ number } pageX`:当前横向页面的索引值,下标从 0 开始
- `{ number } pageY`:当前纵向页面的索引值,下标从 0 开始
- **触发时机**:当 slide 切换 page 之后触发
```js
const slide = new BScroll(this.$refs.slide, {
scrollX: true,
scrollY: false,
slide: true,
momentum: false,
bounce: false
})
slide.on('slidePageChanged', (page) => {
currentPageIndex = page.pageX
})
```
================================================
FILE: packages/vuepress-docs/docs/zh-CN/plugins/wheel.md
================================================
# wheel
## 介绍
wheel 插件,是实现类似 IOS Picker 组件的基石。
## 安装
```bash
npm install @better-scroll/wheel --save
// or
yarn add @better-scroll/wheel
```
## 使用
首先引入 wheel 插件,并通过静态方法 `BScroll.use()` 注册插件。
```js
import BScroll from '@better-scroll/core'
import Wheel from '@better-scroll/wheel'
BScroll.use(Wheel)
```
接着在 `options` 传入正确的配置
```js
let bs = new BScroll('.bs-wrapper', {
wheel: true // wheel options 为 true
})
```
:::tip
wheel options 是 true 或者对象,否则插件功能失效,具体请参考[ wheel options](./wheel.html#wheel-选项对象)。
:::
::: danger 危险
BetterScroll 结合 wheel 插件只是实现 Picker 效果的 JS 逻辑部分,还有 DOM 模版是需要用户去实现,所幸,对于大多数的 Picker 场景,我们给出了相对应的示例。
:::
- **基本使用**
<<< @/examples/vue/components/picker/one-column.vue?template
<<< @/examples/vue/components/picker/one-column.vue?script
<<< @/examples/vue/components/picker/one-column.vue?style
单列 Picker 是一个比较常见的效果。你可以通过 `selectedIndex` 来配置初始化时选中对应索引的 item,`wheelDisabledItemClass` 配置想要禁用的 item 项来模拟 Web Select 标签 disable 的效果。
- **多项选择器**
<<< @/examples/vue/components/picker/double-column.vue?template
<<< @/examples/vue/components/picker/double-column.vue?script
<<< @/examples/vue/components/picker/double-column.vue?style
示例是一个两列的选择器,JS 逻辑部分与单列选择器没有多大的区别,你会发现这个两列选择器之间是没有任何关联,因为它们是两个不同的 BetterScroll 实例。如果你想要实现省市联动的效果,那么得加上一部分代码,让这两个 BetterScroll 实例能够关联起来。请看下一个例子:
- **城市联动选择器**
<<< @/examples/vue/components/picker/linkage-column.vue?template
<<< @/examples/vue/components/picker/linkage-column.vue?script
<<< @/examples/vue/components/picker/linkage-column.vue?style
城市联动 Picker 的效果,必须通过 JS 部分逻辑将不同 BetterScroll 的实例联系起来,不管是省市,还是省市区的联动,亦是如此。
## wheel 选项对象
:::tip 提示
当 wheel 配置为 true 的时候,插件内部使用的是默认的插件选项对象。
```js
const bs = new BScroll('.wrapper', {
wheel: true
})
// 相当于
const bs = new BScroll('.wrapper', {
wheel: {
wheelWrapperClass: 'wheel-scroll',
wheelItemClass: 'wheel-item',
rotate: 25,
adjustTime: 400,
selectedIndex: 0,
wheelDisabledItemClass: 'wheel-disabled-item'
}
})
```
:::
### selectedIndex
- **类型**:`number`
- **默认值**:`0`
实例化 Wheel,默认选中第 selectedIndex 项,索引从 0 开始。
### rotate
- **类型**:`number`
- **默认值**:`25`
当滚动 wheel 时,wheel item 的弯曲程度。
### adjustTime
- **类型**:`number`
- **默认值**:`400`(ms)
当点击某一项的时候,滚动过去的动画时长。
### wheelWrapperClass
- **类型**:`string`
- **默认值**:`wheel-scroll`
滚动元素的 className,这里的「滚动元素」 指的就是 BetterScroll 的 content 元素。
### wheelItemClass
- **类型**:`string`
- **默认值**:`wheel-item`
滚动元素的子元素的样式。
### wheelDisabledItemClass
- **类型**:`string`
- **默认值**:`wheel-disabled-item`
滚动元素中想要禁用的子元素,类似于 `select` 元素中禁用的 `option` 效果。wheel 插件的内部根据 `wheelDisabledItemClass` 配置来判断是否将该项指定为 disabled 状态。
## 实例方法
:::tip 提示
以下方法皆已代理至 BetterScroll 实例,例如:
```js
import BScroll from '@better-scroll/core'
import Wheel from '@better-scroll/wheel'
BScroll.use(Wheel)
const bs = new BScroll('.bs-wrapper', {
wheel: true
})
bs.getSelectedIndex()
bs.wheelTo(1, 300)
```
:::
### getSelectedIndex()
- **返回值**:当前选中项的 index,下标从 0 开始
获取当前选中项的索引。
### wheelTo(index = 0, time = 0, [ease])
- **参数**:
- `{ number } index`:选项索引
- `{ number } time`:动画时长
- `{ number } ease<可选>`:动画时长。缓动效果配置,参考 [ease.ts](https://github.com/ustbhuangyi/better-scroll/blob/dev/packages/shared-utils/src/ease.ts),默认是 `bounce` 效果
滚动至对应索引的列表项。
### stop()
强制让滚动的 BetterScroll 停止下来,并且吸附至当前距离最近的 wheel-item 的位置。
### restorePosition()
强制让滚动的 BetterScroll 停止下来,并且恢复至滚动开始前的位置。
::: tip 提示
以上两个方法只对处于**滚动中的 BetterScroll** 有效,并且 `restorePosition` 是与原生的 iOS picker 组件的效果一模一样,用户可以根据自己的需求选择对应的方法。
:::
## 事件
### wheelIndexChanged
- **参数**:当前选中的 wheel-item 的索引。
- **触发时机**:当列表项发生改变的时候。
```js
import BScroll from '@better-scroll/core'
import Wheel from '@better-scroll/wheel'
BScroll.use(Wheel)
const bs = new BScroll('.bs-wrapper', {
wheel: true
})
bs.on('wheelIndexChanged', (index) => {
console.log(index)
})
```
================================================
FILE: packages/vuepress-docs/docs/zh-CN/plugins/zoom.md
================================================
# zoom
## 介绍
zoom 插件为 BetterScroll 提供缩放功能。
## 安装
```bash
npm install @better-scroll/zoom --save
// or
yarn add @better-scroll/zoom
```
## 使用
为了开启缩放功能,你需要首先引入 zoom 插件,并通过静态方法 `BScroll.use()` 注册插件
```js
import BScroll from '@better-scroll/core'
import Zoom from '@better-scroll/zoom'
BScroll.use(Zoom)
```
上面步骤完成后,在 BetterScroll 的基础能力上扩展了缩放的功能,但是想要缩放真正生效,还需要在 `options` 中传入正确的配置:
```js
new BScroll('.bs-wrapper', {
freeScroll: true,
scrollX: true,
scrollY: true,
zoom: {
start: 1,
min: 0.5,
max: 2
}
})
```
以下是 zoom 插件专属以及[ BetterScroll 的配置](../guide/base-scroll-options.html):
- **zoom<插件专属>**
开启 zoom 功能。若没有该项,则插件不会生效。该配置同时也是用来设置 zoom 特性的相关配置,具体请参考[ zoom 选项对象](./zoom.html#zoom-选项对象)。
- **freeScroll**
如果希望当放大之后,当前区域在 x 和 y 轴方向都可以滚动时,必须设置为 `true`。同时需要设置 scrollX 和 scrollY 均为 true。
- **scrollX**
如果希望当放大之后,当前区域在 x 轴方向可以滚动时,必须设置为 `true`。
- **scrollY**
如果希望当放大之后,当前区域在 y 轴方向可以滚动时,必须设置为 `true`。
## 示例
:::warning
zoom 暂不支持在 pc 端的交互操作,下方 demo 请扫码体验。
:::
<<< @/examples/vue/components/zoom/default.vue?template
<<< @/examples/vue/components/zoom/default.vue?script
<<< @/examples/vue/components/zoom/default.vue?style
## zoom 选项对象
### start
- **类型**:`number`
- **默认值**:`1`
初始缩放比例。
### min
- **类型**:`number`
- **默认值**:`1`
最小缩放比例。
### max
- **类型**:`number`
- **默认值**:`4`
最大缩放比例。
### initialOrigin
- **类型**:`[OriginX, OriginY]`
- **OriginX**:`number | 'left' | 'right' | 'center'`
- **OriginY**:`number | 'top' | 'bottom' | 'center'`
- **默认值**:`[0, 0]`
初始化 zoom 插件的缩放原点,当 `start` 不为 `1` 的时候有效,缩放原点都是以`缩放元素`为坐标系。
- **示例**
```js
new BScroll('.bs-wrapper', {
// ... 其他配置项
zoom: {
initialOrigin: [50, 50], // 基于缩放元素的左顶点上下偏移量都是 50 px
initialOrigin: [0, 0], // 基于缩放元素的左顶点
initialOrigin: ['left', 'top'], // 与上面效果相同
initialOrigin: ['center', 'center'], // 基于缩放元素的中心
initialOrigin: ['right', 'top'], // 基于缩放元素的右顶点
}
})
```
往往你初始化 zoom 的时候只专注于以端点或者中心进行缩放,可以参考以上示例。
### minimalZoomDistance
- **类型**:`number`
- **默认值**:`5`
当你双指进行缩放操作的时候,只有当缩放的距离超过 `minimalZoomDistance`,zoom 才生效。
### bounceTime
- **类型**:`number`
- **默认值**:`800`(毫秒)
双指不断进行缩放操作并且 scale 超过 `max` 阈值的时候,当双指离开的时候,内部会「回弹」至 `max` 的形态,而 `bounceTime` 就是这次「回弹」行为的动画时长。
:::tip 提示
当 zoom 配置为 true 的时候,插件内部使用的是默认的插件选项对象。
```js
const bs = new BScroll('.wrapper', {
zoom: true
})
// 相当于
const bs = new BScroll('.wrapper', {
zoom: {
start: 1,
min: 1,
max: 4,
initialOrigin: [0, 0],
minimalZoomDistance: 5,
bounceTime: 800, // ms
}
})
```
:::
## 实例方法
### zoomTo(scale, x, y, [bounceTime])
- **参数**
- `{number} scale`: 缩放比例
- `{OriginX} x`: 缩放原点的 x 坐标,相当于**缩放元素**的左顶点
- `OriginX:'number | 'left' | 'right' | 'center'`
- `{OriginY} y`: 缩放原点的 y 坐标,相当于**缩放元素**的左顶点
- `OriginY:'number | 'top' | 'bottom' | 'center'`
- `{number} [bounceTime]<可选>:一次缩放行为的动画时长`
以 `[x, y]` 坐标作为原点对元素进行缩放。x 与 y 不仅可以是数字,也可以是对应的字符串,因为一般的场景都是基于端点或者中心进行缩放。
- **示例**
```js
const bs = new BScroll('.bs-wrapper', {
freeScroll: true,
scrollX: true,
scrollY: true,
zoom: {
start: 1,
min: 0.5,
max: 2
}
})
bs.zoomTo(1.8, 'left', 'bottom') // 基于缩放元素的左底点缩放至 1.8 倍
bs.zoomTo(1.8, 'left', 'bottom', 1000) // 基于缩放元素的左底点缩放,动画时长为 1s
bs.zoomTo(1.8, 100, 100) // 基于缩放元素左顶点的上下偏移量 100 为原点进行缩放
bs.zoomTo(2, 'center', 'center') // 基于缩放元素的中心进行缩放
```
## 事件
### beforeZoomStart
- **参数**:无
- **触发时机**:双指接触缩放元素时,不包括直接调用 zoomTo 方法
### zoomStart
- **参数**:无
- **触发时机**:双指缩放距离超过最小阈值 `minimalZoomDistance`,缩放即将开始。不包括直接调用 zoomTo 方法
### zooming
- **参数**:`{ scale }`
- **类型**:`{ scale: number }`
- **触发时机**:双指缩放行为正在进行时或者直接调用 zoomTo 进行缩放的过程
- **示例**:
```js
const bs = new BScroll('.bs-wrapper', {
freeScroll: true,
scrollX: true,
scrollY: true,
zoom: {
start: 1,
min: 0.5,
max: 2
}
})
bs.on('zooming', ({ scale }) => {
// use scale
console.log(scale) // 当前 scale 的值
})
```
### zoomEnd
- **参数**:`{ scale }`
- **类型**:`{ scale: number }`
- **触发时机**:双指缩放行为结束后(如果有回弹,触发时机在回弹动画结束之后)或者调用 zoomTo 完成缩放之后
:::warning
在 zoom 的场景下,你应该监听 zoomStart、zooming、zoomEnd 等等事件,而不是更底层的 scroll、scrollEnd 事件,要不然可能与你的预期不符。
:::
================================================
FILE: packages/vuepress-docs/docs-release.sh
================================================
#!/usr/bin/env sh
set -e
yarn run docs:build
cd docs/.vuepress/dist
git init
git add -A
git commit -m 'update docs'
git push -f git@github.com:better-scroll/docs.git master:gh-pages
cd -
================================================
FILE: packages/vuepress-docs/package.json
================================================
{
"name": "vuepress-docs",
"version": "2.5.1",
"description": "Docs of BetterScroll",
"author": {
"name": "huangyi",
"email": "ustbhuangyi@gmail.com"
},
"private": true,
"scripts": {
"docs:dev": "vuepress dev docs --open",
"docs:build": "vuepress build docs",
"docs:release": "sh docs-release.sh"
},
"bugs": {
"url": "https://github.com/ustbhuangyi/better-scroll/issues"
},
"homepage": "https://github.com/ustbhuangyi/better-scroll",
"keywords": [
"scroll",
"iscroll",
"javascript",
"typescript",
"ios",
"vuepress-docs"
],
"license": "MIT",
"repository": {
"type": "git",
"url": "git@github.com:ustbhuangyi/better-scroll.git",
"directory": "packages/vuepress-docs"
},
"devDependencies": {
"ts-loader": "^5.4.5",
"vuepress": "^1.8.0"
},
"dependencies": {
"qrcode-js-package": "^1.0.4",
"v-tooltip": "^2.0.2"
}
}
================================================
FILE: packages/wheel/README.md
================================================
# @better-scroll/wheel
[中文文档](https://github.com/ustbhuangyi/better-scroll/blob/master/packages/wheel/README_zh-CN.md)
Implement a plugin similar to the effects of the IOS Picker component.
## Usage
```js
import BScroll from '@better-scroll/core'
import Wheel from '@better-scroll/wheel'
BScroll.use(Wheel)
const bs = new BScroll('.wheel-wrapper', {
wheel: {
selectedIndex: 0,
wheelWrapperClass: 'wheel-scroll',
wheelItemClass: 'wheel-item',
wheelDisabledItemClass: 'wheel-disabled-item'
},
probeType: 3
})
```
================================================
FILE: packages/wheel/README_zh-CN.md
================================================
# @better-scroll/wheel
实现类似于 IOS Picker 组件效果的插件。
## 使用
```js
import BScroll from '@better-scroll/core'
import Wheel from '@better-scroll/wheel'
BScroll.use(Wheel)
const bs = new BScroll('.wheel-wrapper', {
wheel: {
selectedIndex: 0,
wheelWrapperClass: 'wheel-scroll',
wheelItemClass: 'wheel-item',
wheelDisabledItemClass: 'wheel-disabled-item'
},
probeType: 3
})
```
================================================
FILE: packages/wheel/package.json
================================================
{
"name": "@better-scroll/wheel",
"version": "2.5.1",
"description": "a BetterScroll plugin to imitate IOS Picker",
"author": {
"name": "jizhi",
"email": "theniceangel@163.com"
},
"main": "dist/wheel.min.js",
"module": "dist/wheel.esm.js",
"typings": "dist/types/index.d.ts",
"publishConfig": {
"access": "public"
},
"bugs": {
"url": "https://github.com/ustbhuangyi/better-scroll/issues"
},
"homepage": "https://github.com/ustbhuangyi/better-scroll",
"keywords": [
"scroll",
"iscroll",
"javascript",
"typescript",
"ios"
],
"license": "MIT",
"repository": {
"type": "git",
"url": "git@github.com:ustbhuangyi/better-scroll.git"
},
"dependencies": {
"@better-scroll/core": "^2.5.1"
},
"gitHead": "f441227b6137d44ba0b44b97ed4cd49de9386130"
}
================================================
FILE: packages/wheel/src/__tests__/index.spec.ts
================================================
import BScroll, { Boundary } from '@better-scroll/core'
import Wheel from '../index'
jest.mock('@better-scroll/core')
const createWheel = (wheelOptions: Object) => {
const wrapper = document.createElement('div')
const content = document.createElement('div')
wrapper.appendChild(content)
const scroll = new BScroll(wrapper, { wheel: wheelOptions })
const wheel = new Wheel(scroll)
return { scroll, wheel }
}
const addPropertiesToWheel =
(wheel: Wheel, obj: T) => {
for (const key in obj) {
;(wheel as any)[key] = obj[key]
}
return wheel
}
describe('wheel plugin tests', () => {
let scroll: BScroll
let wheel: Wheel
beforeEach(() => {
const created = createWheel({})
// create DOM
wheel = created.wheel
scroll = created.scroll
})
afterEach(() => {
jest.clearAllMocks()
})
it('should proxy properties to BScroll instance', () => {
expect(scroll.proxy).toBeCalled()
expect(scroll.proxy).toHaveBeenLastCalledWith([
{
key: 'wheelTo',
sourceKey: 'plugins.wheel.wheelTo'
},
{
key: 'getSelectedIndex',
sourceKey: 'plugins.wheel.getSelectedIndex'
},
{
key: 'restorePosition',
sourceKey: 'plugins.wheel.restorePosition'
}
])
})
it('should handle options', () => {
expect(wheel.options.rotate).toBe(25)
expect(wheel.options.adjustTime).toBe(400)
expect(wheel.options.selectedIndex).toBe(0)
expect(wheel.options.wheelWrapperClass).toBe('wheel-scroll')
expect(wheel.options.wheelItemClass).toBe('wheel-item')
expect(wheel.options.wheelDisabledItemClass).toBe('wheel-disabled-item')
})
it('should refresh BehaviorX and BehaviorY boundary', () => {
const { scrollBehaviorX, scrollBehaviorY } = scroll.scroller
expect(scrollBehaviorX.refresh).toBeCalled()
expect(scrollBehaviorY.refresh).toBeCalled()
})
it('should handle selectedIndex', () => {
// default
expect(wheel.selectedIndex).toBe(0)
// specified
const { wheel: wheel2 } = createWheel({
selectedIndex: 2
})
expect(wheel2.selectedIndex).toBe(2)
})
it('should trigger scroll.scrollTo when invoking wheelTo method', () => {
addPropertiesToWheel(wheel, {
itemHeight: 40
})
wheel.wheelTo(0)
expect(scroll.scrollTo).toBeCalled()
expect(scroll.scrollTo).toHaveBeenLastCalledWith(0, -0, 0, undefined)
})
it('should return seletedIndex when invoking getSelectedIndex', () => {
const { wheel: wheel2 } = createWheel({
selectedIndex: 2
})
expect(wheel2.getSelectedIndex()).toBe(2)
})
it('should support scrollTo somewhere by selectedIndex when initialized', () => {
addPropertiesToWheel(wheel, {
selectedIndex: 1,
itemHeight: 50
})
const postion = {
x: 100,
y: 100
}
// manually trigger
scroll.hooks.trigger(scroll.hooks.eventTypes.beforeInitialScrollTo, postion)
expect(postion).toMatchObject({
x: 0,
y: -50
})
})
it('should invoke wheelTo when scroll.scroller trigger checkClick hook', () => {
let div = document.createElement('div')
addPropertiesToWheel(wheel, {
items: [div],
target: div,
wheelTo: jest.fn()
})
scroll.scroller.hooks.trigger('checkClick')
expect(wheel.wheelTo).toBeCalled()
expect(wheel.wheelTo).toHaveBeenCalledWith(0, 400, expect.anything())
// if target element is not found
addPropertiesToWheel(wheel, {
items: [div],
target: null,
wheelTo: jest.fn()
})
let ret = scroll.scroller.hooks.trigger('checkClick')
expect(ret).toBe(true)
})
it('should invoke findNearestValidWheel when scroll.scroller trigger scrollTo hook', () => {
let endPoint = { x: 0, y: -20 }
let div = document.createElement('div')
addPropertiesToWheel(wheel, {
items: [div],
target: div,
itemHeight: 40,
wheelTo: jest.fn()
})
scroll.scroller.hooks.trigger('scrollTo', endPoint)
expect(endPoint.y).toBe(-0)
})
it('should change position when scroll.scroller trigger scrollToElement hook', () => {
let div = document.createElement('div')
addPropertiesToWheel(wheel, {
items: [div],
target: div,
itemHeight: 40
})
let pos = {
top: -20,
left: 0
}
div.className = 'wheel-item'
scroll.scroller.hooks.trigger('scrollToElement', div, pos)
expect(pos).toEqual({
top: -0,
left: 0
})
// mismatch target element
let div1 = document.createElement('div')
let pos1 = {
top: -40,
left: 0
}
addPropertiesToWheel(wheel, {
items: [div1],
target: div1,
itemHeight: 40
})
let ret = scroll.scroller.hooks.trigger('scrollToElement', div1, pos1)
expect(ret).toBe(true)
expect(pos1).toMatchObject({
top: -40,
left: 0
})
})
it('should change target when scroll.scroller.actionsHandler trigger beforeStart hook', () => {
let e = {} as any
let div = document.createElement('div')
e.target = div
scroll.scroller.actionsHandler.hooks.trigger('beforeStart', e)
expect(wheel.target).toEqual(div)
})
it('should modify boundary when scrollBehaviorY or scrollBehaviorX computedBoundary', () => {
let div = document.createElement('div')
let cachedXBoundary = {} as Boundary
let cachedYBoundary = {} as Boundary
addPropertiesToWheel(wheel, {
items: [div, div],
itemHight: 50
})
const { scrollBehaviorX, scrollBehaviorY } = scroll.scroller
// append two element
scroll.scroller.content.appendChild(document.createElement('div'))
scroll.scroller.content.appendChild(document.createElement('div'))
scrollBehaviorY.contentSize = 100
// manually trigger
scrollBehaviorX.hooks.trigger(
scrollBehaviorX.hooks.eventTypes.computeBoundary,
cachedXBoundary
)
scrollBehaviorY.hooks.trigger(
scrollBehaviorY.hooks.eventTypes.computeBoundary,
cachedYBoundary
)
expect(cachedXBoundary).toMatchObject({
minScrollPos: 0,
maxScrollPos: 0
})
expect(cachedYBoundary).toMatchObject({
minScrollPos: 0,
maxScrollPos: -50
})
})
it('should change momentumInfo when scroll.scroller.scrollBehaviorY trigger momentum or end hook', () => {
let momentumInfo = {
destination: 0,
rate: 15
}
let div = document.createElement('div')
addPropertiesToWheel(wheel, {
items: [div],
target: div,
itemHeight: 40
})
scroll.scroller.scrollBehaviorY.hooks.trigger('momentum', momentumInfo)
expect(momentumInfo).toEqual({
destination: -0,
rate: 4
})
scroll.scroller.scrollBehaviorY.currentPos = -20
scroll.scroller.scrollBehaviorY.hooks.trigger('end', momentumInfo)
expect(momentumInfo).toEqual({
destination: -0,
rate: 4,
duration: 400
})
scroll.scroller.scrollBehaviorY.hooks.trigger('momentum', momentumInfo, 800)
expect(momentumInfo).toEqual({
destination: -0,
rate: 4,
duration: 400
})
})
it('scroll.hooks.refresh ', () => {
let newContent = document.createElement('p')
let div = document.createElement('div')
addPropertiesToWheel(wheel, {
items: [div],
target: div,
itemHeight: 40
})
wheel.options.selectedIndex = 1
scroll.hooks.trigger(scroll.hooks.eventTypes.refresh, newContent)
expect(scroll.scrollTo).toBeCalledWith(0, -40, 0, undefined)
})
it('scroll.scroller.animater.hooks.time ', () => {
let div = document.createElement('div')
addPropertiesToWheel(wheel, {
items: [div]
})
const animater = scroll.scroller.animater
animater.hooks.trigger(animater.hooks.eventTypes.time, 100)
expect(div.style.transitionDuration).toBe('100ms')
})
it('scroll.scroller.animater.hooks.timeFunction ', () => {
let div = document.createElement('div')
addPropertiesToWheel(wheel, {
items: [div]
})
const animater = scroll.scroller.animater
animater.hooks.trigger(
animater.hooks.eventTypes.timeFunction,
'cubic-bezier(0.23, 1, 0.32, 1)'
)
expect(div.style.transitionTimingFunction).toBe(
'cubic-bezier(0.23, 1, 0.32, 1)'
)
})
it('scroll.scroller.animater.hooks.callStop', () => {
let div1 = document.createElement('div')
let div2 = document.createElement('div')
addPropertiesToWheel(wheel, {
items: [div1, div2],
itemHeight: 40,
wheelItemsAllDisabled: false
})
scroll.y = -41
scroll.maxScrollY = -80
scroll.scroller.animater.hooks.trigger('callStop')
expect(scroll.scrollTo).toBeCalledWith(0, -40, 0, undefined)
})
it('scroll.scroller.animater.translater.hooks.translate', () => {
let div = document.createElement('div')
addPropertiesToWheel(wheel, {
items: [div],
itemHeight: 40,
wheelItemsAllDisabled: false
})
const translater = scroll.scroller.animater.translater
translater.hooks.trigger(translater.hooks.eventTypes.translate, {
x: 0,
y: -20
})
expect(wheel.selectedIndex).toEqual(0)
})
it('scroll.scroller.hooks.minDistanceScroll ', () => {
let div = document.createElement('div')
addPropertiesToWheel(wheel, {
items: [div]
})
const scroller = scroll.scroller
scroller.animater.forceStopped = true
scroller.hooks.trigger(scroller.hooks.eventTypes.minDistanceScroll)
expect(scroller.animater.forceStopped).toBe(false)
})
it('scrollEnd event', () => {
let div1 = document.createElement('div')
let div2 = document.createElement('div')
addPropertiesToWheel(wheel, {
itemHeight: 40,
items: [div1, div2]
})
scroll.maxScrollY = -80
scroll.scroller.animater.forceStopped = true
// stopped from an animation,
// prevent user's scrollEnd callback triggered twice
const ret = scroll.trigger(scroll.eventTypes.scrollEnd, { y: 0 })
expect(ret).toBe(true)
wheel.isAdjustingPosition = true
// update selectedIndex
scroll.trigger(scroll.eventTypes.scrollEnd, { y: -41 })
expect(wheel.getSelectedIndex()).toBe(1)
expect(wheel.isAdjustingPosition).toBe(false)
})
it('wheel.restorePosition()', () => {
addPropertiesToWheel(wheel, {
itemHeight: 40
})
// simulate bs is scrolling
scroll.pending = true
wheel.restorePosition()
expect(scroll.scroller.animater.clearTimer).toBeCalled()
expect(scroll.scrollTo).toBeCalledWith(0, -0, 0, undefined)
})
it('should support disable wheel items', () => {
let div1 = document.createElement('div')
let div2 = document.createElement('div')
const scroller = scroll.scroller
const position = { y: -41 }
addPropertiesToWheel(wheel, {
items: [div1, div2],
itemHeight: 40,
wheelItemsAllDisabled: false
})
scroll.y = -41
scroll.maxScrollY = -80
div2.className = 'wheel-disabled-item'
scroller.hooks.trigger(scroller.hooks.eventTypes.scrollTo, position)
expect(position.y).toBe(-0)
div1.className = 'wheel-disabled-item'
wheel.wheelItemsAllDisabled = true
scroller.hooks.trigger(scroller.hooks.eventTypes.scrollTo, position)
expect(position.y).toBe(-0)
let div3 = document.createElement('div')
let position3 = {
y: -39
}
addPropertiesToWheel(wheel, {
items: [div1, div2, div3],
itemHeight: 40,
wheelItemsAllDisabled: false
})
scroller.hooks.trigger(scroller.hooks.eventTypes.scrollTo, position3)
expect(position3.y).toBe(-80)
})
})
================================================
FILE: packages/wheel/src/index.ts
================================================
import BScroll, { Boundary } from '@better-scroll/core'
import {
style,
hasClass,
ease,
EaseItem,
extend,
Position,
HTMLCollectionToArray
} from '@better-scroll/shared-utils'
import propertiesConfig from './propertiesConfig'
export type WheelOptions = Partial | true
const WHEEL_INDEX_CHANGED_EVENT_NAME = 'wheelIndexChanged'
export interface WheelConfig {
selectedIndex: number
rotate: number
adjustTime: number
wheelWrapperClass: string
wheelItemClass: string
wheelDisabledItemClass: string
}
declare module '@better-scroll/core' {
interface CustomOptions {
wheel?: WheelOptions
}
interface CustomAPI {
wheel: PluginAPI
}
}
interface PluginAPI {
wheelTo(index?: number, time?: number, ease?: EaseItem): void
getSelectedIndex(): number
restorePosition(): void
}
const CONSTANTS = {
rate: 4
}
export default class Wheel implements PluginAPI {
static pluginName = 'wheel'
options: WheelConfig
wheelItemsAllDisabled: boolean
items: HTMLCollection
itemHeight: number
selectedIndex: number
isAdjustingPosition: boolean
target: EventTarget | null
constructor(public scroll: BScroll) {
this.init()
}
init() {
this.handleBScroll()
this.handleOptions()
this.handleHooks()
// init boundary for Wheel
this.refreshBoundary()
this.setSelectedIndex(this.options.selectedIndex)
}
private handleBScroll() {
this.scroll.proxy(propertiesConfig)
this.scroll.registerType([WHEEL_INDEX_CHANGED_EVENT_NAME])
}
private handleOptions() {
const userOptions = (this.scroll.options.wheel === true
? {}
: this.scroll.options.wheel) as Partial
const defaultOptions: WheelConfig = {
wheelWrapperClass: 'wheel-scroll',
wheelItemClass: 'wheel-item',
rotate: 25,
adjustTime: 400,
selectedIndex: 0,
wheelDisabledItemClass: 'wheel-disabled-item'
}
this.options = extend(defaultOptions, userOptions)
}
private handleHooks() {
const scroll = this.scroll
const scroller = this.scroll.scroller
const {
actionsHandler,
scrollBehaviorX,
scrollBehaviorY,
animater
} = scroller
let prevContent = scroller.content
// BScroll
scroll.on(scroll.eventTypes.scrollEnd, (position: Position) => {
const index = this.findNearestValidWheel(position.y).index
if (scroller.animater.forceStopped && !this.isAdjustingPosition) {
this.target = this.items[index]
// since stopped from an animation.
// prevent user's scrollEnd callback triggered twice
return true
} else {
this.setSelectedIndex(index)
if (this.isAdjustingPosition) {
this.isAdjustingPosition = false
}
}
})
// BScroll.hooks
this.scroll.hooks.on(
this.scroll.hooks.eventTypes.refresh,
(content: HTMLElement) => {
if (content !== prevContent) {
prevContent = content
this.setSelectedIndex(this.options.selectedIndex, true)
}
// rotate all wheel-items
// because position may not change
this.rotateX(this.scroll.y)
// check we are stop at a disable item or not
this.wheelTo(this.selectedIndex, 0)
}
)
this.scroll.hooks.on(
this.scroll.hooks.eventTypes.beforeInitialScrollTo,
(position: Position) => {
// selectedIndex has higher priority than bs.options.startY
position.x = 0
position.y = -(this.selectedIndex * this.itemHeight)
}
)
// Scroller
scroller.hooks.on(scroller.hooks.eventTypes.checkClick, () => {
const index = HTMLCollectionToArray(this.items).indexOf(this.target)
if (index === -1) return true
this.wheelTo(index, this.options.adjustTime, ease.swipe)
return true
})
scroller.hooks.on(
scroller.hooks.eventTypes.scrollTo,
(endPoint: Position) => {
endPoint.y = this.findNearestValidWheel(endPoint.y).y
}
)
// when content is scrolling
// click wheel-item DOM repeatedly and crazily will cause scrollEnd not triggered
// so reset forceStopped
scroller.hooks.on(scroller.hooks.eventTypes.minDistanceScroll, () => {
const animater = scroller.animater
if (animater.forceStopped === true) {
animater.forceStopped = false
}
})
scroller.hooks.on(
scroller.hooks.eventTypes.scrollToElement,
(el: HTMLElement, pos: { top: number; left: number }) => {
if (!hasClass(el, this.options.wheelItemClass)) {
return true
} else {
pos.top = this.findNearestValidWheel(pos.top).y
}
}
)
// ActionsHandler
actionsHandler.hooks.on(
actionsHandler.hooks.eventTypes.beforeStart,
(e: TouchEvent) => {
this.target = e.target
}
)
// ScrollBehaviorX
// Wheel has no x direction now
scrollBehaviorX.hooks.on(
scrollBehaviorX.hooks.eventTypes.computeBoundary,
(boundary: Boundary) => {
boundary.maxScrollPos = 0
boundary.minScrollPos = 0
}
)
// ScrollBehaviorY
scrollBehaviorY.hooks.on(
scrollBehaviorY.hooks.eventTypes.computeBoundary,
(boundary: Boundary) => {
this.items = this.scroll.scroller.content.children
this.checkWheelAllDisabled()
this.itemHeight =
this.items.length > 0
? scrollBehaviorY.contentSize / this.items.length
: 0
boundary.maxScrollPos = -this.itemHeight * (this.items.length - 1)
boundary.minScrollPos = 0
}
)
scrollBehaviorY.hooks.on(
scrollBehaviorY.hooks.eventTypes.momentum,
(momentumInfo: {
destination: number
duration: number
rate: number
}) => {
momentumInfo.rate = CONSTANTS.rate
momentumInfo.destination = this.findNearestValidWheel(
momentumInfo.destination
).y
}
)
scrollBehaviorY.hooks.on(
scrollBehaviorY.hooks.eventTypes.end,
(momentumInfo: { destination: number; duration: number }) => {
let validWheel = this.findNearestValidWheel(scrollBehaviorY.currentPos)
momentumInfo.destination = validWheel.y
momentumInfo.duration = this.options.adjustTime
}
)
// Animater
animater.hooks.on(animater.hooks.eventTypes.time, (time: number) => {
this.transitionDuration(time)
})
animater.hooks.on(
animater.hooks.eventTypes.timeFunction,
(easing: string) => {
this.timeFunction(easing)
}
)
// bs.stop() to make wheel stop at a correct position when pending
animater.hooks.on(animater.hooks.eventTypes.callStop, () => {
const { index } = this.findNearestValidWheel(this.scroll.y)
this.isAdjustingPosition = true
this.wheelTo(index, 0)
})
// Translater
animater.translater.hooks.on(
animater.translater.hooks.eventTypes.translate,
(endPoint: Position) => {
this.rotateX(endPoint.y)
}
)
}
private refreshBoundary() {
const { scrollBehaviorX, scrollBehaviorY, content } = this.scroll.scroller
scrollBehaviorX.refresh(content)
scrollBehaviorY.refresh(content)
}
setSelectedIndex(index: number, contentChanged: boolean = false) {
const prevSelectedIndex = this.selectedIndex
this.selectedIndex = index
// if content DOM changed, should not trigger event
if (prevSelectedIndex !== index && !contentChanged) {
this.scroll.trigger(WHEEL_INDEX_CHANGED_EVENT_NAME, index)
}
}
getSelectedIndex() {
return this.selectedIndex
}
wheelTo(index = 0, time = 0, ease?: EaseItem) {
const y = -index * this.itemHeight
this.scroll.scrollTo(0, y, time, ease)
}
restorePosition() {
// bs is scrolling
const isPending = this.scroll.pending
if (isPending) {
const selectedIndex = this.getSelectedIndex()
this.scroll.scroller.animater.clearTimer()
this.wheelTo(selectedIndex, 0)
}
}
private transitionDuration(time: number) {
for (let i = 0; i < this.items.length; i++) {
;(this.items[i] as HTMLElement).style[style.transitionDuration as any] =
time + 'ms'
}
}
private timeFunction(easing: string) {
for (let i = 0; i < this.items.length; i++) {
;(this.items[i] as HTMLElement).style[
style.transitionTimingFunction as any
] = easing
}
}
private rotateX(y: number) {
const { rotate = 25 } = this.options
for (let i = 0; i < this.items.length; i++) {
const deg = rotate * (y / this.itemHeight + i)
// Too small value is invalid in some phones, issue 1026
const SafeDeg = deg.toFixed(3)
;(this.items[i] as HTMLElement).style[
style.transform as any
] = `rotateX(${SafeDeg}deg)`
}
}
private findNearestValidWheel(y: number) {
y = y > 0 ? 0 : y < this.scroll.maxScrollY ? this.scroll.maxScrollY : y
let currentIndex = Math.abs(Math.round(-y / this.itemHeight))
const cacheIndex = currentIndex
const items = this.items
const wheelDisabledItemClassName = this.options
.wheelDisabledItemClass as string
// implement web native select element
// first, check whether there is a enable item whose index is smaller than currentIndex
// then, check whether there is a enable item whose index is bigger than currentIndex
// otherwise, there are all disabled items, just keep currentIndex unchange
while (currentIndex >= 0) {
if (
!hasClass(
items[currentIndex] as HTMLElement,
wheelDisabledItemClassName
)
) {
break
}
currentIndex--
}
if (currentIndex < 0) {
currentIndex = cacheIndex
while (currentIndex <= items.length - 1) {
if (
!hasClass(
items[currentIndex] as HTMLElement,
wheelDisabledItemClassName
)
) {
break
}
currentIndex++
}
}
// keep it unchange when all the items are disabled
if (currentIndex === items.length) {
currentIndex = cacheIndex
}
// when all the items are disabled, selectedIndex should always be -1
return {
index: this.wheelItemsAllDisabled ? -1 : currentIndex,
y: -currentIndex * this.itemHeight
}
}
private checkWheelAllDisabled() {
const wheelDisabledItemClassName = this.options.wheelDisabledItemClass
const items = this.items
this.wheelItemsAllDisabled = true
for (let i = 0; i < items.length; i++) {
if (!hasClass(items[i] as HTMLElement, wheelDisabledItemClassName)) {
this.wheelItemsAllDisabled = false
break
}
}
}
}
================================================
FILE: packages/wheel/src/propertiesConfig.ts
================================================
const sourcePrefix = 'plugins.wheel'
const propertiesMap = [
{
key: 'wheelTo',
name: 'wheelTo',
},
{
key: 'getSelectedIndex',
name: 'getSelectedIndex',
},
{
key: 'restorePosition',
name: 'restorePosition',
},
]
export default propertiesMap.map((item) => {
return {
key: item.key,
sourceKey: `${sourcePrefix}.${item.name}`,
}
})
================================================
FILE: packages/zoom/README.md
================================================
# @better-scroll/pull-up
[中文文档](https://github.com/ustbhuangyi/better-scroll/blob/master/packages/zoom/README_zh-CN.md)
Plugin for zooming in or out.
## Usage
```js
import BScroll from '@better-scroll/core'
import Zoom from '@better-scroll/zoom'
BScroll.use(Zoom)
const bs = new BScroll('.zoom-wrapper', {
freeScroll: true,
scrollX: true,
scrollY: true,
disableMouse: true,
useTransition: true,
zoom: {
start: 1,
min: 0.5,
max: 2
}
})
```
================================================
FILE: packages/zoom/README_zh-CN.md
================================================
# @better-scroll/zoom
为 BetterScroll 提供放大或者缩小的效果的插件。
## 使用
```js
import BScroll from '@better-scroll/core'
import Zoom from '@better-scroll/zoom'
BScroll.use(Zoom)
const bs = new BScroll('.zoom-wrapper', {
freeScroll: true,
scrollX: true,
scrollY: true,
disableMouse: true,
useTransition: true,
zoom: {
start: 1,
min: 0.5,
max: 2
}
})
```
================================================
FILE: packages/zoom/package.json
================================================
{
"name": "@better-scroll/zoom",
"version": "2.5.1",
"description": "a BetterScroll plugin to enlarge or narrow",
"author": {
"name": "jizhi",
"email": "theniceangel@163.com"
},
"main": "dist/zoom.min.js",
"module": "dist/zoom.esm.js",
"typings": "dist/types/index.d.ts",
"publishConfig": {
"access": "public"
},
"bugs": {
"url": "https://github.com/ustbhuangyi/better-scroll/issues"
},
"homepage": "https://github.com/ustbhuangyi/better-scroll",
"keywords": [
"scroll",
"iscroll",
"javascript",
"typescript",
"ios",
"image-preview",
"zoom"
],
"license": "MIT",
"repository": {
"type": "git",
"url": "git@github.com:ustbhuangyi/better-scroll.git",
"directory": "packages/zoom"
},
"dependencies": {
"@better-scroll/core": "^2.5.1"
},
"gitHead": "f441227b6137d44ba0b44b97ed4cd49de9386130"
}
================================================
FILE: packages/zoom/src/__tests__/__utils__/util.ts
================================================
import {
createEvent,
CustomTouchEvent
} from '@better-scroll/core/src/__tests__/__utils__/event'
import { createDiv } from '@better-scroll/core/src/__tests__/__utils__/layout'
export function createZoomElements() {
const wrapper = createDiv(300, 300)
const scaledElement = createDiv(300, 300, 0, 0)
wrapper.appendChild(scaledElement)
return { wrapper, scaledElement }
}
export function createTouchEvent(
firstFingerPoint: { pageX: number; pageY: number },
secondFingerPoint?: { pageX: number; pageY: number }
): CustomTouchEvent {
const e = createEvent('Event', 'touch') as CustomTouchEvent
e.touches = [firstFingerPoint]
if (secondFingerPoint) {
e.touches.push(secondFingerPoint)
}
return e
}
================================================
FILE: packages/zoom/src/__tests__/index.spec.ts
================================================
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)
})
})
================================================
FILE: packages/zoom/src/index.ts
================================================
import BScroll, { Behavior, TranslaterPoint } from '@better-scroll/core'
import propertiesConfig from './propertiesConfig'
import {
getDistance,
ease,
between,
offsetToBody,
getRect,
style,
EventEmitter,
extend,
getNow,
requestAnimationFrame,
cancelAnimationFrame,
} from '@better-scroll/shared-utils'
export type ZoomOptions = Partial | true
export interface ZoomConfig {
start: number
min: number
max: number
initialOrigin: [OriginX, OriginY]
minimalZoomDistance: number
bounceTime: number
}
type OriginX = number | 'left' | 'right' | 'center'
type OriginY = number | 'top' | 'bottom' | 'center'
declare module '@better-scroll/core' {
interface CustomOptions {
zoom?: ZoomOptions
}
interface CustomAPI {
zoom: PluginAPI
}
}
interface PluginAPI {
zoomTo(scale: number, x: OriginX, y: OriginY, bounceTime?: number): void
}
interface Point {
x: number
y: number
baseScale: number
}
interface ResolveFormula {
left(): number
top(): number
right(): number
bottom(): number
center(index: number): number
}
const TWO_FINGERS = 2
const RAW_SCALE = 1
export default class Zoom implements PluginAPI {
static pluginName = 'zoom'
origin: Point
scale: number = RAW_SCALE
zoomOpt: ZoomConfig
numberOfFingers: number
private zoomed: boolean
private startDistance: number
private startScale: number
private wrapper: HTMLElement
private prevScale: number = 1
private hooksFn: Array<[EventEmitter, string, Function]>
constructor(public scroll: BScroll) {
this.init()
}
init() {
this.handleBScroll()
this.handleOptions()
this.handleHooks()
this.tryInitialZoomTo(this.zoomOpt)
}
zoomTo(scale: number, x: OriginX, y: OriginY, bounceTime?: number) {
const { originX, originY } = this.resolveOrigin(x, y)
const origin: Point = {
x: originX,
y: originY,
baseScale: this.scale,
}
this._doZoomTo(scale, origin, bounceTime, true)
}
private handleBScroll() {
this.scroll.proxy(propertiesConfig)
this.scroll.registerType([
'beforeZoomStart',
'zoomStart',
'zooming',
'zoomEnd',
])
}
private handleOptions() {
const userOptions = (this.scroll.options.zoom === true
? {}
: this.scroll.options.zoom) as Partial
const defaultOptions: ZoomConfig = {
start: 1,
min: 1,
max: 4,
initialOrigin: [0, 0],
minimalZoomDistance: 5,
bounceTime: 800, // ms
}
this.zoomOpt = extend(defaultOptions, userOptions)
}
private handleHooks() {
const scroll = this.scroll
const scroller = this.scroll.scroller
this.wrapper = this.scroll.scroller.wrapper
this.setTransformOrigin(this.scroll.scroller.content)
const scrollBehaviorX = scroller.scrollBehaviorX
const scrollBehaviorY = scroller.scrollBehaviorY
this.hooksFn = []
// BScroll
this.registerHooks(
scroll.hooks,
scroll.hooks.eventTypes.contentChanged,
(content: HTMLElement) => {
this.setTransformOrigin(content)
this.scale = RAW_SCALE
this.tryInitialZoomTo(this.zoomOpt)
}
)
this.registerHooks(
scroll.hooks,
scroll.hooks.eventTypes.beforeInitialScrollTo,
() => {
// if perform a zoom action, we should prevent initial scroll when initialised
if (this.zoomOpt.start !== RAW_SCALE) {
return true
}
}
)
// enlarge boundary
this.registerHooks(
scrollBehaviorX.hooks,
scrollBehaviorX.hooks.eventTypes.beforeComputeBoundary,
() => {
// content may change, don't cache it's size
const contentSize = getRect(this.scroll.scroller.content)
scrollBehaviorX.contentSize = Math.floor(contentSize.width * this.scale)
}
)
this.registerHooks(
scrollBehaviorY.hooks,
scrollBehaviorY.hooks.eventTypes.beforeComputeBoundary,
() => {
// content may change, don't cache it's size
const contentSize = getRect(this.scroll.scroller.content)
scrollBehaviorY.contentSize = Math.floor(
contentSize.height * this.scale
)
}
)
// touch event
this.registerHooks(
scroller.actions.hooks,
scroller.actions.hooks.eventTypes.start,
(e: TouchEvent) => {
const numberOfFingers = (e.touches && e.touches.length) || 0
this.fingersOperation(numberOfFingers)
if (numberOfFingers === TWO_FINGERS) {
this.zoomStart(e)
}
}
)
this.registerHooks(
scroller.actions.hooks,
scroller.actions.hooks.eventTypes.beforeMove,
(e: TouchEvent) => {
const numberOfFingers = (e.touches && e.touches.length) || 0
this.fingersOperation(numberOfFingers)
if (numberOfFingers === TWO_FINGERS) {
this.zoom(e)
return true
}
}
)
this.registerHooks(
scroller.actions.hooks,
scroller.actions.hooks.eventTypes.beforeEnd,
(e: TouchEvent) => {
const numberOfFingers = this.fingersOperation()
if (numberOfFingers === TWO_FINGERS) {
this.zoomEnd()
return true
}
}
)
this.registerHooks(
scroller.translater.hooks,
scroller.translater.hooks.eventTypes.beforeTranslate,
(transformStyle: string[], point: TranslaterPoint) => {
const scale = point.scale ? point.scale : this.prevScale
this.prevScale = scale
transformStyle.push(`scale(${scale})`)
}
)
this.registerHooks(
scroller.hooks,
scroller.hooks.eventTypes.scrollEnd,
() => {
if (this.fingersOperation() === TWO_FINGERS) {
this.scroll.trigger(this.scroll.eventTypes.zoomEnd, {
scale: this.scale,
})
}
}
)
this.registerHooks(this.scroll.hooks, 'destroy', this.destroy)
}
private setTransformOrigin(content: HTMLElement) {
content.style[style.transformOrigin as any] = '0 0'
}
private tryInitialZoomTo(options: ZoomConfig) {
const { start, initialOrigin } = options
const { scrollBehaviorX, scrollBehaviorY } = this.scroll.scroller
if (start !== RAW_SCALE) {
// Movable plugin may wanna modify minScrollPos or maxScrollPos
// so we force Movable to caculate them
this.resetBoundaries([scrollBehaviorX, scrollBehaviorY])
this.zoomTo(start, initialOrigin[0], initialOrigin[1], 0)
}
}
// getter or setter operation
private fingersOperation(amounts?: number): number | void {
if (typeof amounts === 'number') {
this.numberOfFingers = amounts
} else {
return this.numberOfFingers
}
}
private _doZoomTo(
scale: number,
origin: Point,
time: number = this.zoomOpt.bounceTime,
useCurrentPos = false
) {
const { min, max } = this.zoomOpt
const fromScale = this.scale
const toScale = between(scale, min, max)
// dispatch zooming hooks
;(() => {
if (time === 0) {
this.scroll.trigger(this.scroll.eventTypes.zooming, {
scale: toScale,
})
return
}
if (time > 0) {
let timer: number
const startTime = getNow()
const endTime = startTime + time
const scheduler = () => {
const now = getNow()
if (now >= endTime) {
this.scroll.trigger(this.scroll.eventTypes.zooming, {
scale: toScale,
})
cancelAnimationFrame(timer)
return
}
const ratio = ease.bounce.fn((now - startTime) / time)
const currentScale = ratio * (toScale - fromScale) + fromScale
this.scroll.trigger(this.scroll.eventTypes.zooming, {
scale: currentScale,
})
timer = requestAnimationFrame(scheduler)
}
// start scheduler job
scheduler()
}
})()
// suppose you are zooming by two fingers
this.fingersOperation(2)
this._zoomTo(toScale, fromScale, origin, time, useCurrentPos)
}
private _zoomTo(
toScale: number,
fromScale: number,
origin: Point,
time: number,
useCurrentPos = false
) {
const ratio = toScale / origin.baseScale
this.setScale(toScale)
const scroller = this.scroll.scroller
const { scrollBehaviorX, scrollBehaviorY } = scroller
this.resetBoundaries([scrollBehaviorX, scrollBehaviorY])
// position is restrained in boundary
const newX = this.getNewPos(
origin.x,
ratio,
scrollBehaviorX,
true,
useCurrentPos
)
const newY = this.getNewPos(
origin.y,
ratio,
scrollBehaviorY,
true,
useCurrentPos
)
if (
scrollBehaviorX.currentPos !== Math.round(newX) ||
scrollBehaviorY.currentPos !== Math.round(newY) ||
toScale !== fromScale
) {
scroller.scrollTo(newX, newY, time, ease.bounce, {
start: {
scale: fromScale,
},
end: {
scale: toScale,
},
})
}
}
private resolveOrigin(x: OriginX, y: OriginY) {
const { scrollBehaviorX, scrollBehaviorY } = this.scroll.scroller
const resolveFormula: ResolveFormula = {
left() {
return 0
},
top() {
return 0
},
right() {
return scrollBehaviorX.contentSize
},
bottom() {
return scrollBehaviorY.contentSize
},
center(index: number) {
const baseSize =
index === 0
? scrollBehaviorX.contentSize
: scrollBehaviorY.contentSize
return baseSize / 2
},
}
return {
originX: typeof x === 'number' ? x : resolveFormula[x](0),
originY: typeof y === 'number' ? y : resolveFormula[y](1),
}
}
zoomStart(e: TouchEvent) {
const firstFinger = e.touches[0]
const secondFinger = e.touches[1]
this.startDistance = this.getFingerDistance(e)
this.startScale = this.scale
let { left, top } = offsetToBody(this.wrapper)
this.origin = {
x:
Math.abs(firstFinger.pageX + secondFinger.pageX) / 2 +
left -
this.scroll.x,
y:
Math.abs(firstFinger.pageY + secondFinger.pageY) / 2 +
top -
this.scroll.y,
baseScale: this.startScale,
}
this.scroll.trigger(this.scroll.eventTypes.beforeZoomStart)
}
zoom(e: TouchEvent) {
const currentDistance = this.getFingerDistance(e)
// at least minimalZoomDistance pixels for the zoom to initiate
if (
!this.zoomed &&
Math.abs(currentDistance - this.startDistance) <
this.zoomOpt.minimalZoomDistance
) {
return
}
// when out of boundary , perform a damping algorithm
const endScale = this.dampingScale(
(currentDistance / this.startDistance) * this.startScale
)
const ratio = endScale / this.startScale
this.setScale(endScale)
if (!this.zoomed) {
this.zoomed = true
this.scroll.trigger(this.scroll.eventTypes.zoomStart)
}
const scroller = this.scroll.scroller
const { scrollBehaviorX, scrollBehaviorY } = scroller
const x = this.getNewPos(
this.origin.x,
ratio,
scrollBehaviorX,
false,
false
)
const y = this.getNewPos(
this.origin.y,
ratio,
scrollBehaviorY,
false,
false
)
this.scroll.trigger(this.scroll.eventTypes.zooming, {
scale: this.scale,
})
scroller.translater.translate({ x, y, scale: endScale })
}
zoomEnd() {
if (!this.zoomed) return
// if out of boundary, do rebound!
if (this.shouldRebound()) {
this._doZoomTo(this.scale, this.origin, this.zoomOpt.bounceTime)
return
}
this.scroll.trigger(this.scroll.eventTypes.zoomEnd, { scale: this.scale })
}
private getFingerDistance(e: TouchEvent): number {
const firstFinger = e.touches[0]
const secondFinger = e.touches[1]
const deltaX = Math.abs(firstFinger.pageX - secondFinger.pageX)
const deltaY = Math.abs(firstFinger.pageY - secondFinger.pageY)
return getDistance(deltaX, deltaY)
}
private shouldRebound(): boolean {
const { min, max } = this.zoomOpt
const currentScale = this.scale
// scale exceeded!
if (currentScale !== between(currentScale, min, max)) {
return true
}
const { scrollBehaviorX, scrollBehaviorY } = this.scroll.scroller
// enlarge boundaries manually when zoom is end
this.resetBoundaries([scrollBehaviorX, scrollBehaviorY])
const { inBoundary: xInBoundary } = scrollBehaviorX.checkInBoundary()
const { inBoundary: yInBoundary } = scrollBehaviorX.checkInBoundary()
return !(xInBoundary && yInBoundary)
}
private dampingScale(scale: number) {
const { min, max } = this.zoomOpt
if (scale < min) {
scale = 0.5 * min * Math.pow(2.0, scale / min)
} else if (scale > max) {
scale = 2.0 * max * Math.pow(0.5, max / scale)
}
return scale
}
private setScale(scale: number) {
this.scale = scale
}
private resetBoundaries(scrollBehaviorPairs: [Behavior, Behavior]) {
scrollBehaviorPairs.forEach((behavior) => behavior.computeBoundary())
}
private getNewPos(
origin: number,
lastScale: number,
scrollBehavior: Behavior,
shouldInBoundary?: boolean,
useCurrentPos = false
) {
let newPos =
origin -
origin * lastScale +
(useCurrentPos ? scrollBehavior.currentPos : scrollBehavior.startPos)
if (shouldInBoundary) {
newPos = between(
newPos,
scrollBehavior.maxScrollPos,
scrollBehavior.minScrollPos
)
}
// maxScrollPos or minScrollPos maybe a negative or positive digital
return newPos > 0 ? Math.floor(newPos) : Math.ceil(newPos)
}
private registerHooks(hooks: EventEmitter, name: string, handler: Function) {
hooks.on(name, handler, this)
this.hooksFn.push([hooks, name, handler])
}
destroy() {
this.hooksFn.forEach((item) => {
const hooks = item[0]
const hooksName = item[1]
const handlerFn = item[2]
hooks.off(hooksName, handlerFn)
})
this.hooksFn.length = 0
}
}
================================================
FILE: packages/zoom/src/propertiesConfig.ts
================================================
const sourcePrefix = 'plugins.zoom'
const propertiesMap = [
{
key: 'zoomTo',
name: 'zoomTo'
}
]
export default propertiesMap.map(item => {
return {
key: item.key,
sourceKey: `${sourcePrefix}.${item.name}`
}
})
================================================
FILE: postcss.config.js
================================================
module.exports = {
plugins: [
require('autoprefixer')({
browsers: require('./package.json').browserslist
})
]
}
================================================
FILE: scripts/build.js
================================================
const fs = require('fs')
const path = require('path')
const inquirer = require('inquirer')
const rollup = require('rollup')
const chalk = require('chalk')
const zlib = require('zlib')
const rimraf = require('rimraf')
const typescript = require('rollup-plugin-typescript2')
const uglify = require('rollup-plugin-uglify').uglify
const execa = require('execa')
const ora = require('ora')
const spinner = ora({
prefixText: `${chalk.green('\n[building tasks]')}`
})
function getPackagesName () {
let ret
let all = fs.readdirSync(resolve('packages'))
// drop hidden file whose name is startWidth '.'
// drop packages which would not be published(eg: examples and docs)
ret = all
.filter(name => {
const isHiddenFile = /^\./g.test(name)
return !isHiddenFile
}).filter(name => {
const isPrivatePackages = require(resolve(`packages/${name}/package.json`)).private
return !isPrivatePackages
})
return ret
}
function cleanPackagesOldDist(packagesName) {
packagesName.forEach(name => {
const distPath = resolve(`packages/${name}/dist`)
const typePath = resolve(`packages/${name}/dist/types`)
if (fs.existsSync(distPath)) {
rimraf.sync(distPath)
}
fs.mkdirSync(distPath)
fs.mkdirSync(typePath)
})
}
function resolve(p) {
return path.resolve(__dirname, '../', p)
}
function PascalCase(str){
const re=/-(\w)/g;
const newStr = str.replace(re, function (match, group1){
return group1.toUpperCase();
})
return newStr.charAt(0).toUpperCase() + newStr.slice(1);
}
const generateBanner = (packageName) => {
let ret =
'/*!\n' +
' * better-scroll / ' + packageName + '\n' +
' * (c) 2016-' + new Date().getFullYear() + ' ustbhuangyi\n' +
' * Released under the MIT License.\n' +
' */'
return ret
}
const buildType = [
{
format: 'umd',
ext: '.js'
},
{
format: 'umd',
ext: '.min.js'
},
{
format: 'es',
ext: '.esm.js'
}
]
function generateBuildConfigs(packagesName) {
const result = []
packagesName.forEach(name => {
buildType.forEach((type) => {
let config = {
input: resolve(`packages/${name}/src/index.ts`),
output: {
file: resolve(`packages/${name}/dist/${name}${type.ext}`),
name: PascalCase(name),
format: type.format,
banner: generateBanner(name)
},
plugins: generateBuildPluginsConfigs(type.ext.indexOf('min')>-1, name)
}
// rename
if (name === 'core' && config.output.format !== 'es') {
config.output.name = 'BScroll'
/** Disable warning for default imports */
config.output.exports = 'named'
// it seems the umd bundle can not satisfies our demand
config.output.footer = 'if(typeof window !== "undefined" && window.BScroll) { \n' +
' window.BScroll = window.BScroll.default;\n}'
}
// rollup will valiate config properties of config own and output a warning.
// put packageName in prototype to ignore warning.
Object.defineProperties(config, {
'packageName': {
value: name
},
'ext': {
value: type.ext
}
})
result.push(config)
})
})
return result
}
function generateBuildPluginsConfigs(isMin) {
const tsConfig = {
verbosity: -1,
tsconfig: path.resolve(__dirname, '../tsconfig.json'),
}
const plugins = []
if (isMin) {
plugins.push(uglify())
}
plugins.push(typescript(tsConfig))
return plugins
}
function build(builds) {
let built = 0
const total = builds.length
const next = () => {
buildEntry(builds[built], built + 1, () => {
builds[built-1] = null
built++
if (built < total) {
next()
}
})
}
next()
}
function buildEntry(config, curIndex, next) {
const isProd = /min\.js$/.test(config.output.file)
spinner.start(`${config.packageName}${config.ext} is buiding now. \n`)
rollup.rollup(config).then((bundle) => {
bundle.write(config.output).then(({ output }) => {
const code = output[0].code
spinner.succeed(`${config.packageName}${config.ext} building has ended.`)
function report(extra) {
console.log(chalk.magenta(path.relative(process.cwd(), config.output.file)) + ' ' + getSize(code) + (extra || ''))
next()
}
if (isProd) {
zlib.gzip(code, (err, zipped) => {
if (err) return reject(err)
let words = `(gzipped: ${chalk.magenta(getSize(zipped))})`
report(words)
})
} else {
report()
}
// since we need bundle code for three types
// just generate .d.ts only once
if (curIndex % 3 === 0) {
copyDTSFiles(config.packageName)
}
})
}).catch((e) => {
spinner.fail('buiding is failed')
console.log(e)
})
}
function copyDTSFiles (packageName) {
console.log(chalk.cyan('> start copying .d.ts file to dist dir of packages own.'))
const sourceDir = resolve(`packages/${packageName}/dist/packages/${packageName}/src/*`)
const targetDir = resolve(`packages/${packageName}/dist/types/`)
execa.commandSync(`mv ${sourceDir} ${targetDir}`, { shell: true })
console.log(chalk.cyan('> copy job is done.'))
rimraf.sync(resolve(`packages/${packageName}/dist/packages`))
rimraf.sync(resolve(`packages/${packageName}/dist/node_modules`))
}
function getSize(code) {
return (code.length / 1024).toFixed(2) + 'kb'
}
const getAnswersFromInquirer = async (packagesName) => {
const question = {
type: 'checkbox',
name: 'packages',
scroll: false,
message: 'Select build repo(Support Multiple selection)',
choices: packagesName.map(name => ({
value: name,
name
}))
}
let { packages } = await inquirer.prompt(question)
// make no choice
if (!packages.length) {
console.log(chalk.yellow(`
It seems that you did't make a choice.
Please try it again.
`))
return
}
// chose 'all' option
if (packages.some(package => package === 'all')) {
packages = getPackagesName()
}
const { yes } = await inquirer.prompt([{
name: 'yes',
message: `Confirm build ${packages.join(' and ')} packages?`,
type: 'list',
choices: ['Y', 'N']
}])
if (yes === 'N') {
console.log(chalk.yellow('[release] cancelled.'))
return
}
return packages
}
const buildBootstrap = async () => {
const packagesName = getPackagesName()
// provide 'all' option
packagesName.unshift('all')
const answers = await getAnswersFromInquirer(packagesName)
if (!answers) return
cleanPackagesOldDist(answers)
const buildConfigs = generateBuildConfigs(answers)
build(buildConfigs)
}
buildBootstrap().catch(err => {
console.error(err)
process.exit(1)
})
================================================
FILE: scripts/checkYarn.js
================================================
if (!/yarn\.js$/.test(process.env.npm_execpath || '')) {
console.warn(
'\u001b[33mThis repository requires Yarn 1.x for scripts to work properly.\u001b[39m\n'
)
process.exit(1)
}
================================================
FILE: scripts/release.js
================================================
const execa = require('execa')
const semver = require('semver')
const inquirer = require('inquirer')
const chalk = require('chalk')
const curVersion = require('../lerna.json').version
const release = async () => {
console.log(chalk.yellow(`Current version: ${curVersion}`))
const bumps = ['patch', 'minor', 'major', 'prerelease-alpha', 'prerelease-beta', 'premajor']
const versions = {}
bumps.forEach(b => {
const args = b.split('-')
versions[b] = semver.inc(curVersion, ...args)
})
const bumpChoices = bumps.map(b => ({ name: `${b} (${versions[b]})`, value: b }))
function getVersion (answers) {
return answers.customVersion || versions[answers.bump]
}
function getNpmTags (version) {
if (isPreRelease(version)) {
return ['next']
}
return ['latest', 'next']
}
function isPreRelease (version) {
return !!semver.prerelease(version)
}
const { bump, customVersion, npmTag } = await inquirer.prompt([
{
name: 'bump',
message: 'Select release type:',
type: 'list',
choices: [
...bumpChoices,
{ name: 'custom', value: 'custom' }
]
},
{
name: 'customVersion',
message: 'Input version:',
type: 'input',
when: answers => answers.bump === 'custom'
},
{
name: 'npmTag',
message: 'Input npm tag:',
type: 'list',
choices: answers => getNpmTags(getVersion(answers))
}
])
const version = customVersion || versions[bump]
const { yes } = await inquirer.prompt([{
name: 'yes',
message: `Confirm releasing ${version} (${npmTag})?`,
type: 'list',
choices: ['Y', 'N']
}])
if (yes === 'N') {
console.log(chalk.red('[release] cancelled.'))
return
}
const releaseArguments = [
'publish',
version,
'--force-publish',
'*',
'--npm-tag',
npmTag
]
console.log(chalk.grey(`lerna ${releaseArguments.join(' ')}`))
await execa(require.resolve('lerna/cli'), releaseArguments, { stdio: 'inherit' })
// it seems that sometimes 'gitHead' property in packages/**/package.json will change
// but sometimes it won't, at this condition. work tree is clean, 'git commit ' will cause en error
// so put it in try/catch, because we want to sync dev from master
try {
await execa('git', ['add', '-A'], { stdio: 'inherit' })
await execa('git', ['commit', '-m', `chore: ${version} published`], { stdio: 'inherit' })
await execa('git', ['push', 'origin', `master`], { stdio: 'inherit' })
} catch (error) {}
// sync dev from master
await execa('git', ['checkout', 'dev'], { stdio: 'inherit' })
await execa('git', ['rebase', 'master'], { stdio: 'inherit' })
await execa('git', ['push', 'origin', 'dev'], { stdio: 'inherit' })
await execa('git', ['checkout', 'master'], { stdio: 'inherit' })
}
release().catch(err => {
console.error(err)
process.exit(1)
})
================================================
FILE: test-dts/core.test-d.ts
================================================
import {
BScroll,
expectFuncArguments,
expectFuncReturnValue,
Options
} from './index'
import { EaseItem } from '@better-scroll/shared-utils/src'
describe('core api parameter type should be correct', () => {
type ExtraTransform = { start: object; end: object }
expectFuncArguments<[], BScroll['refresh']>()
expectFuncArguments<
[number, number, number?, EaseItem?, ExtraTransform?],
BScroll['scrollTo']
>()
expectFuncArguments<
[number, number, number?, EaseItem?],
BScroll['scrollBy']
>()
expectFuncArguments<
[
string | HTMLElement,
number,
number | boolean,
number | boolean,
EaseItem?
],
BScroll['scrollToElement']
>()
expectFuncArguments<[], BScroll['stop']>()
expectFuncArguments<[], BScroll['enable']>()
expectFuncArguments<[], BScroll['disable']>()
expectFuncArguments<[], BScroll['destroy']>()
// Events API
expectFuncArguments<[string, Function, Object?], BScroll['on']>()
expectFuncArguments<[string, Function, Object?], BScroll['once']>()
expectFuncArguments<[string?, Function?], BScroll['off']>()
expectFuncReturnValue, BScroll['on']>()
expectFuncReturnValue, BScroll['once']>()
expectFuncReturnValue | undefined, BScroll['off']>()
})
================================================
FILE: test-dts/index.d.ts
================================================
import BScroll from '@better-scroll/core'
import Zoom from '@better-scroll/zoom'
import Wheel from '@better-scroll/wheel'
import Slide from '@better-scroll/slide'
import ScrollBar from '@better-scroll/scroll-bar'
import PullUp from '@better-scroll/pull-up'
import PullDown from '@better-scroll/pull-down'
import ObserveDom from '@better-scroll/observe-dom'
import NestedScroll from '@better-scroll/nested-scroll'
import MouseWheel from '@better-scroll/mouse-wheel'
import Infinity from '@better-scroll/infinity'
import Movable from '@better-scroll/movable'
import { IfEquals } from './util'
export * from '@better-scroll/core'
export * from '@better-scroll/zoom'
export * from '@better-scroll/wheel'
export * from '@better-scroll/slide'
export * from '@better-scroll/scroll-bar'
export * from '@better-scroll/pull-up'
export * from '@better-scroll/pull-down'
export * from '@better-scroll/observe-dom'
export * from '@better-scroll/nested-scroll'
export * from '@better-scroll/mouse-wheel'
export * from '@better-scroll/infinity'
export * from '@better-scroll/movable'
export type ArgumentsCheck<
T extends any[],
U extends (...args: any[]) => any
> = (
...args: any[]
) => U extends (...args: infer P) => any
? IfEquals, T>
: never
export type ReturnValueCheck any> = (
...args: any[]
) => U extends (...args: any[]) => infer P ? IfEquals : never
export declare function expectType>(): void
export declare function expectError(value: T): void
export declare function expectAssignable(): void
export declare function expectFuncArguments<
T extends any[],
T1 extends ArgumentsCheck
>(): void
export declare function expectFuncReturnValue<
T,
T1 extends ReturnValueCheck
>(): void
export {
BScroll,
Zoom,
Wheel,
Slide,
ScrollBar,
PullUp,
PullDown,
ObserveDom,
NestedScroll,
MouseWheel,
Infinity,
Movable
}
================================================
FILE: test-dts/plugin.test-d.ts
================================================
import {
expectType,
expectError,
expectFuncArguments,
expectFuncReturnValue,
BScroll,
createBScroll,
Zoom,
Wheel,
Slide,
ScrollBar,
PullUp,
PullDown,
ObserveDom,
NestedScroll,
MouseWheel,
Movable,
ZoomConfig,
WheelConfig,
SlideConfig,
MouseWheelOptions,
ScrollbarOptions,
ScrollbarConfig,
PullUpLoadOptions,
PullUpLoadConfig,
PullDownRefreshOptions,
PullDownRefreshConfig,
NestedScrollOptions,
} from './index'
import {
DeepNonNullable,
FilterType,
FilterUndef,
FilterBoolean,
ExcludeTrue,
} from './util'
import { EaseItem } from '@better-scroll/shared-utils/src'
describe('BScroll.use should be used normally', () => {
// @ts-expect-error
expectError(BScroll.use())
// @ts-expect-error
expectError(BScroll.use({}))
// @ts-expect-error
expectError(BScroll.use({ pluginName: 'pluginName' }))
// @ts-expect-error
expectError(BScroll.use(function () {}))
// @ts-expect-error
expectError(BScroll.use(class Plugin {}))
expectError(
BScroll.use(
class Plugin {
static pluginName = 'pluginName'
}
)
)
})
describe('zoom plugin options and api type shoule be inferred correctly', () => {
BScroll.use(Zoom)
const bscroll = createBScroll('', {
zoom: {
max: 1,
min: 1,
start: 1,
initialOrigin: ['left', 'top'],
minimalZoomDistance: 5,
bounceTime: 800,
},
})
// Options
type BSOptions = DeepNonNullable
expectType | true, BSOptions['zoom']>()
expectType['max']>>()
expectType['min']>>()
expectType['start']>>()
expectType<
[OriginX, OriginY],
FilterUndef['initialOrigin']>
>()
expectType<
number,
FilterUndef['minimalZoomDistance']>
>()
expectType<
number,
FilterUndef['bounceTime']>
>()
// API
type ZoomToAPI = typeof bscroll.zoomTo
type OriginX = number | 'left' | 'right' | 'center'
type OriginY = number | 'top' | 'bottom' | 'center'
expectFuncArguments<[number, OriginX, OriginY, number?], ZoomToAPI>()
})
describe('whell plugin options and api type shoule be inferred correctly', () => {
BScroll.use(Wheel)
const bscroll = new BScroll('', {
wheel: {
selectedIndex: 1,
rotate: 1,
adjustTime: 1,
wheelWrapperClass: 'wheelWrapperClass',
wheelItemClass: 'wheelItemClass',
wheelDisabledItemClass: 'wheelDisabledItemClass',
},
})
// Options
type BSOptions = DeepNonNullable
expectType | true, BSOptions['wheel']>()
expectType['rotate']>>()
expectType<
number,
FilterUndef['adjustTime']>
>()
expectType<
string,
FilterUndef['wheelWrapperClass']>
>()
expectType<
string,
FilterUndef['wheelItemClass']>
>()
expectType<
string,
FilterUndef['wheelDisabledItemClass']>
>()
// API
type WhellToAPI = typeof bscroll.wheelTo
type GetSelectedIndexAPI = typeof bscroll.getSelectedIndex
expectFuncArguments<[number?, number?, EaseItem?], WhellToAPI>()
expectFuncReturnValue()
})
describe('slider plugin options and api type shoule be inferred correctly', () => {
BScroll.use(Slide)
const bscroll = createBScroll('', {
slide: {
loop: true,
},
})
// Options
type BSOptions = DeepNonNullable
type EaseType = {
style: string
fn: (t: number) => number
}
expectType, FilterBoolean>()
expectType>>()
expectType['loop']>>()
expectType<
number,
FilterUndef['threshold']>
>()
expectType['speed']>>()
expectType<
EaseType,
FilterUndef['easing']>
>()
expectType<
boolean,
FilterUndef['listenFlick']>
>()
// API
type BS = typeof bscroll
type Page = {
pageX: number
pageY: number
}
expectFuncArguments<[number?, EaseItem?], BS['next']>()
expectFuncArguments<[number?, EaseItem?], BS['prev']>()
expectFuncArguments<[number, number, number?, EaseItem?], BS['goToPage']>()
expectFuncReturnValue()
})
describe('scrollBar plugin options and api type shoule be inferred correctly', () => {
BScroll.use(ScrollBar)
const bscroll = new BScroll('', {
scrollbar: {
fade: true,
interactive: true,
},
})
// Options
type BSOptions = DeepNonNullable
expectType()
expectType<
boolean,
FilterType>
>()
expectType, FilterBoolean>()
expectType<
boolean,
FilterUndef['fade']>
>()
expectType<
boolean,
FilterUndef['interactive']>
>()
// API
})
describe('pullUp plugin options and api type shoule be inferred correctly', () => {
BScroll.use(PullUp)
const bscroll = new BScroll('', {
pullUpLoad: true,
})
// Options
type BSOptions = DeepNonNullable
expectType()
expectType<
boolean,
FilterType>
>()
expectType<
number,
FilterUndef['threshold']>
>()
// API
type BS = typeof bscroll
expectFuncArguments<[], BS['finishPullUp']>()
expectFuncArguments<[(true | Partial)?], BS['openPullUp']>()
expectFuncArguments<[], BS['closePullUp']>()
})
describe('pullDown plugin options and api type shoule be inferred correctly', () => {
BScroll.use(PullDown)
const bscroll = new BScroll('', {
pullDownRefresh: {
threshold: 1,
stop: 1,
},
})
// Options
type BSOptions = DeepNonNullable
expectType()
expectType<
number,
FilterUndef['threshold']>
>()
expectType<
number,
FilterUndef['stop']>
>()
// API
type BS = typeof bscroll
expectFuncArguments<[], BS['finishPullDown']>()
expectFuncArguments<
[(true | Partial)?],
BS['openPullDown']
>()
expectFuncArguments<[], BS['closePullDown']>()
expectFuncArguments<[], BS['autoPullDownRefresh']>()
})
describe('observeDom plugin options and api type shoule be inferred correctly', () => {
BScroll.use(ObserveDom)
const bscroll = new BScroll('', {
observeDOM: true,
})
// Options
type BSOptions = DeepNonNullable
expectType()
})
describe('nestedScroll plugin options and api type shoule be inferred correctly', () => {
BScroll.use(NestedScroll)
const bscroll = new BScroll('', {
nestedScroll: {
groupId: 1,
},
})
// Options
type BSOptions = DeepNonNullable
expectType<
string | number,
ExcludeTrue['groupId']
>()
})
describe('mouseWheel plugin options and api type shoule be inferred correctly', () => {
BScroll.use(MouseWheel)
const bscroll = new BScroll('', {
mouseWheel: {
speed: 1,
invert: true,
easeTime: 1,
discreteTime: 1,
throttleTime: 1,
dampingFactor: 0.1,
},
})
// Options
type BSOptions = DeepNonNullable
expectType | true, BSOptions['mouseWheel']>()
expectType<
number,
FilterUndef['speed']>
>()
expectType<
boolean,
FilterUndef['invert']>
>()
expectType<
number,
FilterUndef['easeTime']>
>()
expectType<
number,
FilterUndef['discreteTime']>
>()
expectType<
number,
FilterUndef['throttleTime']>
>()
expectType<
number,
FilterUndef['dampingFactor']>
>()
})
describe('movable plugin options and api type shoule be inferred correctly', () => {
BScroll.use(Movable)
const bscroll = new BScroll('', {
movable: true,
})
// Options
type BSOptions = DeepNonNullable
expectType>()
})
================================================
FILE: test-dts/tsconfig.json
================================================
{
"extends": "../tsconfig.json",
"compilerOptions": {
"noEmit": true,
"declaration": true
},
"include": ["../test-dts"],
"exclude": [
"../tests",
"../packages/*/src/__tests__",
"../packages/*/src/__mocks__",
"../packages/*/src"
]
}
================================================
FILE: test-dts/util.d.ts
================================================
type NonUndefined = T extends undefined ? never : T
export type DeepNonNullable = {
[P in keyof T]-?: T[P] extends object
? DeepNonNullable>
: NonUndefined
}
export type IfEquals = (() => T extends X
? 1
: 2) extends () => T extends Y ? 1 : 2
? A
: B
export type FilterType = T extends F ? never : T
export type FilterUndef = FilterType
export type FilterBoolean = FilterType
export type FilterNull = FilterType
export type FilterString = FilterType
export type FilterNumber = FilterType
export type FilterSymbol = FilterType
export type FilterArray = FilterType>
export type FilterFunc = FilterType
export type FilterObject = FilterType
export type ExcludeTrue = FilterType
================================================
FILE: tests/dts/index.d.ts
================================================
================================================
FILE: tests/e2e/compose-plugins/compose-plugins.e2e.ts
================================================
import { Page } from 'puppeteer'
import extendsTouch from '../../util/extendTouch'
import getTranslate from '../../util/getTranslate'
jest.setTimeout(1000000)
describe('compose plugins', () => {
let page = (global as any).page as Page
extendsTouch(page)
beforeAll(async () => {
await page.goto('http://0.0.0.0:8932/#/compose/')
})
it('should display 4 items at least', async () => {
const itemsCounts = await page.$$eval(
'.compose .example-item',
(elements) => elements.length
)
const itemsContent = await page.$$eval(
'.compose .example-item',
(elements) => {
return elements.map((el) => el.textContent)
}
)
expect(itemsContent).toEqual([
'pullup-pulldown',
'pullup-pulldown-slide',
'pullup-pulldown-outnested',
'slide-nested',
])
await expect(itemsCounts).toBeGreaterThanOrEqual(4)
})
})
================================================
FILE: tests/e2e/compose-plugins/pullup-pulldown-nested.e2e.ts
================================================
import { Page } from 'puppeteer'
import extendsTouch from '../../util/extendTouch'
import getTranslate from '../../util/getTranslate'
jest.setTimeout(1000000)
describe('Compose/pullup-pulldown-nested', () => {
let page = (global as any).page as Page
extendsTouch(page)
beforeAll(async () => {
await page.goto('http://0.0.0.0:8932/#/compose/pullup-pulldown-outnested')
})
it('should trigger outer scroll pullingdown when BS reached the top', async () => {
await page.waitFor(1000)
await page.dispatchScroll({
x: 100,
y: 150,
xDistance: 0,
yDistance: 300,
gestureSourceType: 'touch',
})
const { isShowPullDownTxt, isShowLoading } = await page.$$eval(
'.pulldown-wrapper',
(elements) => {
const isShowPullDownTxt =
window.getComputedStyle(elements[0].children[0]).display === 'block'
const isShowLoading =
window.getComputedStyle(elements[0].children[1].children[0])
.display === 'block'
return {
isShowPullDownTxt,
isShowLoading,
}
}
)
expect(isShowPullDownTxt).toEqual(false)
expect(isShowLoading).toEqual(true)
})
it('should trigger outer scroll pullingup when BS reached the bottom', async () => {
await page.waitFor(1000)
await page.dispatchScroll({
x: 200,
y: 300,
xDistance: 0,
yDistance: -500,
speed: 1500,
gestureSourceType: 'touch',
})
await page.waitFor(4000)
const itemsCounts = await page.$$eval(
'.outer-list-item2',
(element) => element.length
)
await expect(itemsCounts).toBeGreaterThanOrEqual(16)
})
it('the inner scroll should scroll normally', async () => {
await page.waitFor(1000)
await page.dispatchScroll({
x: 200,
y: 200,
xDistance: 0,
yDistance: -500,
speed: 1500,
gestureSourceType: 'touch',
})
await page.waitFor(1000)
const transformText = await page.$eval('.inner-content', (node) => {
return window.getComputedStyle(node).transform
})
const y = getTranslate(transformText, 'y')
expect(y).toBe(-814)
})
})
================================================
FILE: tests/e2e/compose-plugins/pullup-pulldown-slide.e2e.ts
================================================
import { Page } from 'puppeteer'
import extendsTouch from '../../util/extendTouch'
import getTranslate from '../../util/getTranslate'
jest.setTimeout(1000000)
describe('Compose/pullup-pulldown-slide', () => {
let page = (global as any).page as Page
extendsTouch(page)
beforeAll(async () => {
await page.goto('http://0.0.0.0:8932/#/compose/pullup-pulldown-slide')
})
it('should trigger pullingdown when BS reached the top', async () => {
await page.waitFor(1000)
await page.dispatchScroll({
x: 100,
y: 150,
xDistance: 0,
yDistance: 300,
gestureSourceType: 'touch',
})
const { isShowPullDownTxt, isShowLoading } = await page.$$eval(
'.pulldown-wrapper',
(elements) => {
const isShowPullDownTxt =
window.getComputedStyle(elements[0].children[0]).display === 'block'
const isShowLoading =
window.getComputedStyle(elements[0].children[1].children[0])
.display === 'block'
return {
isShowPullDownTxt,
isShowLoading,
}
}
)
expect(isShowPullDownTxt).toEqual(false)
expect(isShowLoading).toEqual(true)
await page.waitFor(1000)
})
it('should switch next page when BS scroll half page', async () => {
await page.waitFor(1000)
await page.dispatchScroll({
x: 100,
y: 200,
xDistance: 0,
yDistance: -50,
gestureSourceType: 'touch',
})
await page.waitFor(1000)
const transformText = await page.$eval(
'.pullup-pulldown-slide-scroller',
(node) => {
return window.getComputedStyle(node).transform
}
)
const y = getTranslate(transformText, 'y')
expect(y).toBe(-627)
})
it('should trigger pullingup when BS reached the bottom', async () => {
await page.waitFor(1000)
for (let i = 0; i < 9; i++) {
await page.dispatchScroll({
x: 100,
y: 200,
xDistance: 0,
yDistance: -150,
gestureSourceType: 'touch',
})
await page.waitFor(1500)
}
await page.dispatchScroll({
x: 100,
y: 200,
xDistance: 0,
yDistance: -150,
gestureSourceType: 'touch',
})
await page.waitFor(2000)
const itemsCounts = await page.$$eval(
'.pullup-pulldown-slide-item',
(element) => element.length
)
await expect(itemsCounts).toBeGreaterThanOrEqual(10)
})
})
================================================
FILE: tests/e2e/compose-plugins/pullup-pulldown.e2e.ts
================================================
import { Page } from 'puppeteer'
import extendsTouch from '../../util/extendTouch'
jest.setTimeout(1000000)
describe('Compose/pullup-pulldown', () => {
let page = (global as any).page as Page
extendsTouch(page)
beforeAll(async () => {
await page.goto('http://0.0.0.0:8932/#/compose/pullup-pulldown')
})
it('should render DOM correctly', async () => {
await page.waitFor(300)
const itemsCounts = await page.$$eval(
'.pullup-down-list-item',
element => element.length
)
await expect(itemsCounts).toBeGreaterThanOrEqual(30)
})
it('should trigger pullingdown when BS reached the top', async () => {
await page.waitFor(1000)
await page.dispatchScroll({
x: 100,
y: 150,
xDistance: 0,
yDistance: 300,
gestureSourceType: 'touch'
})
const { isShowPullDownTxt, isShowLoading } = await page.$$eval(
'.pulldown-wrapper',
elements => {
const isShowPullDownTxt =
window.getComputedStyle(elements[0].children[0]).display === 'block'
const isShowLoading =
window.getComputedStyle(elements[0].children[1].children[0])
.display === 'block'
return {
isShowPullDownTxt,
isShowLoading
}
}
)
expect(isShowPullDownTxt).toEqual(false)
expect(isShowLoading).toEqual(true)
await page.waitFor(1000)
})
it('should trigger pullingup when BS reached the bottom', async () => {
await page.waitFor(300)
await page.dispatchScroll({
x: 200,
y: 630,
xDistance: 0,
yDistance: -1000,
speed: 1500,
gestureSourceType: 'touch'
})
await page.waitFor(4000)
const itemsCounts = await page.$$eval(
'.pullup-down-list-item',
element => element.length
)
await expect(itemsCounts).toBeGreaterThanOrEqual(60)
})
})
================================================
FILE: tests/e2e/compose-plugins/slide-nested.e2e.ts
================================================
import { Page } from 'puppeteer'
import extendsTouch from '../../util/extendTouch'
import getTranslate from '../../util/getTranslate'
jest.setTimeout(1000000)
describe('Compose/slide-nested', () => {
let page = (global as any).page as Page
extendsTouch(page)
beforeAll(async () => {
await page.goto('http://0.0.0.0:8932/#/compose/slide-nested')
})
it('the outer scroll should scroll normally', async () => {
await page.waitFor(1000)
await page.dispatchScroll({
x: 200,
y: 100,
xDistance: 0,
yDistance: -70,
gestureSourceType: 'touch',
})
await page.waitFor(1000)
const transformText = await page.$eval('.outer-content', (node) => {
return window.getComputedStyle(node).transform
})
const y = getTranslate(transformText, 'y')
await expect(y).toBeLessThan(-30)
})
it('the inner scroll should scroll normally', async () => {
await page.waitFor(1000)
await page.dispatchScroll({
x: 200,
y: 300,
xDistance: -100,
yDistance: 0,
gestureSourceType: 'touch',
})
await page.waitFor(1000)
const transformText = await page.$eval('.slide-banner-content', (node) => {
return window.getComputedStyle(node).transform
})
const x = getTranslate(transformText, 'x')
expect(x).toBe(-666)
})
})
================================================
FILE: tests/e2e/core/corescroll.e2e.ts
================================================
import { Page } from 'puppeteer'
import extendTouch from '../../util/extendTouch'
import getTranslate from '../../util/getTranslate'
// set default timeout
jest.setTimeout(1000000)
describe('CoreScroll', () => {
let page = (global as any).page as Page
extendTouch(page)
beforeAll(async () => {
await page.goto('http://0.0.0.0:8932/#/core/')
})
it('should display 4 items at least', async () => {
const itemsCounts = await page.$$eval(
'.core .example-item',
(element) => element.length
)
const itemsContent = await page.$$eval('.core .example-item', (element) =>
element.map((el) => el.textContent)
)
expect(itemsContent).toEqual([
'vertical',
'horizontal',
'dynamic-content',
'specified-content',
'freescroll',
'vertical rotated(v2.3.0)',
'horizontal rotated(v2.3.0)',
])
expect(itemsCounts).toBeGreaterThanOrEqual(5)
})
it("should display correct items's texts", async () => {
const itemsContent = await page.$$eval('.core .example-item', (element) =>
element.map((el) => el.textContent)
)
expect(itemsContent).toEqual([
'vertical',
'horizontal',
'dynamic-content',
'specified-content',
'freescroll',
'vertical rotated(v2.3.0)',
'horizontal rotated(v2.3.0)',
])
})
describe('CoreScroll/vertical', () => {
beforeAll(async () => {
await page.goto('http://0.0.0.0:8932/#/core/default')
})
it('should render corrent DOM', async () => {
const wrapper = await page.$('.scroll-wrapper')
const content = await page.$('.scroll-content')
expect(wrapper).toBeTruthy()
expect(content).toBeTruthy()
})
it('should trigger eventListener when click wrapper DOM', async () => {
let mockHandler = jest.fn()
page.once('dialog', async (dialog) => {
mockHandler()
await dialog.dismiss()
})
// wait for router transition ends
await page.waitFor(1000)
await page.touchscreen.tap(100, 100)
expect(mockHandler).toHaveBeenCalled()
})
it('should scroll when dispatch touch', async () => {
await page.waitFor(1000)
await page.dispatchScroll({
x: 100,
y: 150,
xDistance: 0,
yDistance: -70,
gestureSourceType: 'touch',
})
await page.waitFor(1000)
const transformText = await page.$eval('.scroll-content', (node) => {
return window.getComputedStyle(node).transform
})
const y = getTranslate(transformText, 'y')
expect(y).toBeLessThan(0)
})
it('should dispatch scroll event', async () => {
let mockHandler = jest.fn()
page.once('console', async (message) => {
mockHandler()
})
await page.waitFor(1000)
await page.dispatchScroll({
x: 100,
y: 150,
xDistance: 0,
yDistance: -70,
gestureSourceType: 'touch',
})
await page.waitFor(1000)
expect(mockHandler).toBeCalled()
})
})
describe('CoreScroll/horizontal', () => {
beforeAll(async () => {
await page.goto('http://0.0.0.0:8932/#/core/horizontal')
})
it('should render corrent DOM', async () => {
const wrapper = await page.$('.scroll-wrapper')
const container = await page.$('.horizontal-container')
expect(wrapper).toBeTruthy()
expect(container).toBeTruthy()
})
it('should scroll to right when finger moves from right to left', async () => {
await page.waitFor(1000)
await page.dispatchScroll({
x: 100,
y: 120,
xDistance: -70,
yDistance: 0,
gestureSourceType: 'touch',
})
await page.waitFor(1000)
const transformText = await page.$eval('.scroll-content', (node) => {
return window.getComputedStyle(node).transform
})
const x = getTranslate(transformText, 'x')
expect(x).toBeLessThan(0)
})
})
describe('CoreScroll/freescroll', () => {
beforeAll(async () => {
await page.goto('http://0.0.0.0:8932/#/core/freescroll')
})
it('should scroll correctly when oblique scrolling occurred', async () => {
await page.waitFor(1000)
await page.dispatchScroll({
x: 100,
y: 100,
xDistance: -70,
yDistance: -70,
gestureSourceType: 'touch',
})
await page.waitFor(1000)
const transformText = await page.$eval('.scroll-content', (node) => {
return window.getComputedStyle(node).transform
})
const y = getTranslate(transformText, 'y')
const x = getTranslate(transformText, 'x')
expect(x).toBeLessThan(0)
expect(y).toBeLessThan(0)
})
})
describe('CoreScroll/dynamicContent', () => {
beforeAll(async () => {
await page.goto('http://0.0.0.0:8932/#/core/dynamic-content')
})
it('should support switching content dynamically', async () => {
await page.waitFor(1000)
await page.dispatchScroll({
x: 100,
y: 100,
xDistance: 0,
yDistance: -70,
gestureSourceType: 'touch',
})
await page.waitFor(50)
await page.click('.btn')
await page.waitFor(100)
const itemsCounts = await page.$$eval(
'.scroll-content .scroll-item',
(element) => element.length
)
expect(itemsCounts).toBe(60)
})
})
describe('CoreScroll/verticalRotated', () => {
beforeAll(async () => {
await page.goto('http://0.0.0.0:8932/#/core/vertical-rotated')
})
it('should support vertical rotated scroll', async () => {
await page.waitFor(1000)
await page.dispatchScroll({
x: 210,
y: 180,
xDistance: 70,
yDistance: 0,
gestureSourceType: 'touch',
})
await page.waitFor(100)
const transformText = await page.$eval('.scroll-content', (node) => {
return window.getComputedStyle(node).transform
})
const y = getTranslate(transformText, 'y')
expect(y).toBeLessThan(-20)
})
})
describe('CoreScroll/horizontalRotated', () => {
beforeAll(async () => {
await page.goto('http://0.0.0.0:8932/#/core/horizontal-rotated')
})
it('should support horizontal rotated scroll', async () => {
await page.waitFor(1000)
await page.dispatchScroll({
x: 180,
y: 100,
xDistance: 70,
yDistance: 0,
gestureSourceType: 'touch',
})
await page.waitFor(100)
const transformText = await page.$eval('.scroll-content', (node) => {
return window.getComputedStyle(node).transform
})
const x = getTranslate(transformText, 'x')
expect(x).toBeLessThan(-20)
})
})
})
================================================
FILE: tests/e2e/form/textarea.e2e.ts
================================================
import { Page } from 'puppeteer'
import extendTouch from '../../util/extendTouch'
jest.setTimeout(10000000)
describe('BetterScroll in Form-Textarea', () => {
let page = (global as any).page as Page
extendTouch(page)
beforeAll(async () => {
await page.goto('http://0.0.0.0:8932/#/form/textarea')
})
beforeEach(async () => {
await page.reload({
waitUntil: 'domcontentloaded'
})
})
it('should scroll when not manipulating texatea tag', async () => {
await page.waitFor(1000)
await page.dispatchScroll({
x: 100,
y: 150,
xDistance: 0,
yDistance: -70,
gestureSourceType: 'touch'
})
const content = await page.$('.textarea-scroller')
await page.waitFor(1000)
const boundingBox = await content!.boundingBox()
await expect(boundingBox!.y).toBeLessThan(0)
})
it('should not scroll when manipulating texatea tag', async () => {
await page.reload()
await page.waitFor(1000)
await page.dispatchScroll({
x: 200,
y: 570,
xDistance: 0,
yDistance: -70,
gestureSourceType: 'touch'
})
const content = await page.$('.textarea-scroller')
await page.waitFor(1000)
const boundingBox = await content!.boundingBox()
await expect(boundingBox!.y).toBeGreaterThan(0)
})
})
================================================
FILE: tests/e2e/homepage.e2e.ts
================================================
import { Page } from 'puppeteer'
jest.setTimeout(10000000)
describe('Homepage', () => {
let page = (global as any).page as Page
beforeAll(async () => {
await page.goto('http://0.0.0.0:8932/')
})
it('should display "BetterScroll" text on homepage', async () => {
await expect(page).toMatch('BetterScroll')
})
it('should display seven items at least', async () => {
const itemsCounts = await page.$$eval(
'.example-item',
element => element.length
)
await expect(itemsCounts).toBeGreaterThanOrEqual(7)
})
})
================================================
FILE: tests/e2e/indicators/minimap.e2e.ts
================================================
import { Page } from 'puppeteer'
import extendTouch from '../../util/extendTouch'
import getTranslate from '../../util/getTranslate'
jest.setTimeout(10000000)
describe('Indicators-minimap', () => {
let page = (global as any).page as Page
extendTouch(page)
beforeAll(async () => {
await page.goto('http://0.0.0.0:8932/#/indicators/minimap')
})
beforeEach(async () => {
await page.reload({
waitUntil: 'domcontentloaded'
})
})
it('should render correctly', async () => {
await page.waitFor(300)
const transformText = await page.$eval('.scroll-content', node => {
return window.getComputedStyle(node).transform
})
const transformBScrollY = getTranslate(transformText, 'y')
const indicatorTransformText = await page.$eval(
'.scroll-indicator-handle',
node => {
return window.getComputedStyle(node).transform
}
)
const indicatorTransformY = getTranslate(indicatorTransformText, 'y')
expect(transformBScrollY).toBe(-50)
expect(indicatorTransformY).toBe(8)
})
it('should trigger BS to move when manipulating indicator', async () => {
await page.waitFor(300)
await page.dispatchScroll({
x: 70,
y: 240,
xDistance: 70,
yDistance: 70,
gestureSourceType: 'touch'
})
const transformText = await page.$eval('.scroll-content', node => {
return window.getComputedStyle(node).transform
})
const transformBScrollY = getTranslate(transformText, 'y')
const indicatorTransformText = await page.$eval(
'.scroll-indicator-handle',
node => {
return window.getComputedStyle(node).transform
}
)
const indicatorTransformY = getTranslate(indicatorTransformText, 'y')
expect(transformBScrollY).toBeLessThan(-50)
expect(indicatorTransformY).toBeGreaterThan(8)
})
it('should make scrollbar scroll in when manipulating BS', async () => {
await page.waitFor(300)
await page.dispatchScroll({
x: 100,
y: 100,
xDistance: -50,
yDistance: -50,
gestureSourceType: 'touch'
})
const transformText = await page.$eval('.scroll-content', node => {
return window.getComputedStyle(node).transform
})
const transformBScrollY = getTranslate(transformText, 'y')
const indicatorTransformText = await page.$eval(
'.scroll-indicator-handle',
node => {
return window.getComputedStyle(node).transform
}
)
const indicatorTransformY = getTranslate(indicatorTransformText, 'y')
expect(transformBScrollY).toBeLessThan(-50)
expect(indicatorTransformY).toBeGreaterThan(8)
})
})
================================================
FILE: tests/e2e/indicators/parallax-scrolling.e2e.ts
================================================
import { Page } from 'puppeteer'
import extendTouch from '../../util/extendTouch'
import getTranslate from '../../util/getTranslate'
jest.setTimeout(10000000)
describe('Indicators-parallax-scroll', () => {
let page = (global as any).page as Page
extendTouch(page)
beforeAll(async () => {
await page.goto('http://0.0.0.0:8932/#/indicators/parallax-scroll')
})
beforeEach(async () => {
await page.reload({
waitUntil: 'domcontentloaded'
})
})
it('should trigger BS to move when manipulating indicator', async () => {
await page.waitFor(300)
await page.dispatchScroll({
x: 70,
y: 240,
xDistance: -70,
yDistance: -70,
gestureSourceType: 'touch'
})
await page.waitFor(800)
const transformText = await page.$eval('.scroll-content', node => {
return window.getComputedStyle(node).transform
})
const transformBScrollY = getTranslate(transformText, 'y')
const indicator1TransformText = await page.$eval('.star1-bg', node => {
return window.getComputedStyle(node).transform
})
const indicator2TransformText = await page.$eval('.star2-bg', node => {
return window.getComputedStyle(node).transform
})
const indicator1TransformY = getTranslate(indicator1TransformText, 'y')
const indicator2TransformY = getTranslate(indicator2TransformText, 'y')
expect(transformBScrollY).toBeLessThan(0)
expect(indicator1TransformY).toBeLessThan(0)
expect(indicator2TransformY).toBeLessThan(0)
})
})
================================================
FILE: tests/e2e/infinity/infinity.e2e.ts
================================================
import { Page } from 'puppeteer'
import extendTouch from '../../util/extendTouch'
jest.setTimeout(10000)
describe('Infinity', () => {
let page = (global as any).page as Page
extendTouch(page)
beforeAll(async () => {
await page.goto('http://0.0.0.0:8932/#/infinity/')
})
beforeEach(async () => {
await page.reload()
})
it('should not render all elements when fetch data too mouch', async () => {
await page.waitForSelector(
'.infinity-timeline .infinity-item:not(.tombstone)'
)
// when
await page.dispatchScroll({
x: 100,
y: 555,
xDistance: 0,
yDistance: -555,
gestureSourceType: 'touch'
})
await page.waitFor(1001) // wait fetch data
// then
const itemNum = await page.$$eval(
'.infinity-timeline .infinity-item:not(.tombstone)',
items => items.length
)
expect(itemNum).toBeLessThan(60)
})
it('should render tombstones when data not loaded', async () => {
await page.waitForSelector('.infinity-timeline .infinity-item')
// when
await page.dispatchScroll({
x: 100,
y: 555,
xDistance: 0,
yDistance: -555,
gestureSourceType: 'touch'
})
// then
const itemNum = await page.$$eval(
'.infinity-timeline .tombstone',
items => items.length
)
expect(itemNum).toBeGreaterThan(30)
})
})
================================================
FILE: tests/e2e/mousewheel/mousewheel.e2e.ts
================================================
import { Page } from 'puppeteer'
import extendMouseWheel from '../../util/extendMouseWheel'
import getTranslate from '../../util/getTranslate'
jest.setTimeout(1000000)
describe('MouseWheel plugin', () => {
let page = (global as any).page as Page
extendMouseWheel(page)
beforeAll(async () => {
// emulate pc scene
await page.emulate({
viewport: {
isMobile: false,
width: 375,
height: 667,
},
// tslint:disable-next-line: max-line-length
userAgent:
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3770.80 Safari/537.36',
})
})
describe('MouseWheel & Core', () => {
it('should scroll correctly in vertical direction when integrating with CoreScroll', async () => {
await page.goto('http://0.0.0.0:8932/#/mouse-wheel/vertical-scroll')
await page.waitFor(300)
await page.dispatchMouseWheel({
type: 'mouseWheel',
x: 100,
y: 100,
deltaX: 0,
deltaY: 50,
})
await page.waitFor(1000)
const transformText = await page.$eval('.mouse-wheel-content', (node) => {
return window.getComputedStyle(node).transform
})
const y = getTranslate(transformText, 'y')
await expect(y).toBeLessThan(0)
})
it('should scroll correctly in horizontal direction when integrating with CoreScroll', async () => {
await page.goto('http://0.0.0.0:8932/#/mouse-wheel/horizontal-scroll')
await page.waitFor(300)
await page.dispatchMouseWheel({
type: 'mouseWheel',
x: 130,
y: 130,
deltaX: 0,
deltaY: 100,
})
await page.waitFor(1000)
const transformText = await page.$eval('.mouse-wheel-content', (node) => {
return window.getComputedStyle(node).transform
})
const x = getTranslate(transformText, 'x')
await expect(x).toBeLessThan(0)
})
})
describe('MouseWheel & Slide', () => {
it('should scroll correctly in vertical direction when integrating with Slide', async () => {
await page.goto('http://0.0.0.0:8932/#/mouse-wheel/vertical-slide')
await page.waitFor(300)
await page.dispatchMouseWheel({
type: 'mouseWheel',
x: 100,
y: 100,
deltaX: 0,
deltaY: 200,
})
await page.waitFor(3000)
const transformText = await page.$eval('.slide-content', (node) => {
return window.getComputedStyle(node).transform
})
const y = getTranslate(transformText, 'y')
await expect(y).toBeLessThan(-667)
})
it('should scroll correctly in horizontal direction when integrating with Slide', async () => {
await page.goto('http://0.0.0.0:8932/#/mouse-wheel/horizontal-slide')
await page.waitFor(300)
await page.dispatchMouseWheel({
type: 'mouseWheel',
x: 130,
y: 130,
deltaX: 0,
deltaY: 200,
})
await page.waitFor(1000)
const transformText = await page.$eval('.slide-content', (node) => {
return window.getComputedStyle(node).transform
})
const x = getTranslate(transformText, 'x')
await expect(x).toBeLessThan(-600)
})
})
describe('MouseWheel & PullUp', () => {
it('should scroll correctly when integrating with PullUp', async () => {
await page.goto('http://0.0.0.0:8932/#/mouse-wheel/pullup')
await page.waitFor(300)
await page.dispatchMouseWheel({
type: 'mouseWheel',
x: 100,
y: 100,
deltaX: 0,
deltaY: 10000,
})
await page.waitFor(2000)
const transformText = await page.$eval('.pullup-content', (node) => {
return window.getComputedStyle(node).transform
})
const y = getTranslate(transformText, 'y')
expect(y).toBeLessThan(0)
// wait for loading data
await page.waitFor(2000)
const itemsCounts = await page.$$eval(
'.pullup-list-item',
(element) => element.length
)
await expect(itemsCounts).toBeGreaterThanOrEqual(30)
})
})
describe('MouseWheel & PullDown', () => {
it('should scroll correctly when integrating with PullDown', async () => {
await page.goto('http://0.0.0.0:8932/#/mouse-wheel/pulldown')
await page.waitFor(300)
await page.dispatchMouseWheel({
type: 'mouseWheel',
x: 100,
y: 100,
deltaX: 0,
deltaY: -1000,
})
// wait for loading data
await page.waitFor(2000)
const itemsCounts = await page.$$eval(
'.pulldown-list-item',
(element) => element.length
)
await expect(itemsCounts).toBeGreaterThanOrEqual(40)
})
})
describe('MouseWheel & Wheel', () => {
it('should scroll correctly when integrating with Wheel', async () => {
await page.goto('http://0.0.0.0:8932/#/mouse-wheel/picker')
await page.waitFor(300)
await page.click('.open')
await page.waitFor(500)
await page.dispatchMouseWheel({
type: 'mouseWheel',
x: 200,
y: 630,
deltaX: 0,
deltaY: 100,
})
await page.waitFor(1000)
const transformText = await page.$eval('.wheel-scroll', (node) => {
return window.getComputedStyle(node).transform
})
const y = getTranslate(transformText, 'y')
expect(y).toBeLessThan(-30)
})
})
})
================================================
FILE: tests/e2e/movable/default.e2e.ts
================================================
import { Page } from 'puppeteer'
import extendTouch from '../../util/extendTouch'
import getTranslate from '../../util/getTranslate'
jest.setTimeout(10000000)
describe('Movable Plugin', () => {
let page = (global as any).page as Page
extendTouch(page)
beforeAll(async () => {
await page.goto('http://0.0.0.0:8932/#/movable/default')
})
beforeEach(async () => {
await page.reload({
waitUntil: 'domcontentloaded',
})
})
it('should work well when specify "startX & startY"', async () => {
await page.waitFor(300)
const scaledElTransformText = await page.$eval(
'.scroll-content',
(node) => {
return window.getComputedStyle(node).transform
}
)
const x = getTranslate(scaledElTransformText, 'x')
const y = getTranslate(scaledElTransformText, 'y')
expect(x).toBe(20)
expect(y).toBe(20)
})
it('should work well when dispatchScroll', async () => {
await page.waitFor(300)
await page.dispatchScroll({
x: 100,
y: 100,
xDistance: -70,
yDistance: -70,
gestureSourceType: 'touch',
})
await page.waitFor(2000)
const scaledElTransformText = await page.$eval(
'.scroll-content',
(node) => {
return window.getComputedStyle(node).transform
}
)
const x = getTranslate(scaledElTransformText, 'x')
const y = getTranslate(scaledElTransformText, 'y')
expect(x).toBe(0)
expect(y).toBe(0)
})
})
================================================
FILE: tests/e2e/movable/multi-content-scale.e2e.ts
================================================
import { Page } from 'puppeteer'
import extendTouch from '../../util/extendTouch'
import getTranslate from '../../util/getTranslate'
import getScale from '../../util/getScale'
jest.setTimeout(10000000)
describe('Movable & Zoom with multi content', () => {
let page = (global as any).page as Page
extendTouch(page)
beforeAll(async () => {
await page.goto('http://0.0.0.0:8932/#/movable/multi-content-scale')
})
beforeEach(async () => {
await page.reload({
waitUntil: 'domcontentloaded'
})
})
it('should work well', async () => {
await page.waitFor(300)
const transformText1 = await page.$eval('.content1', node => {
return window.getComputedStyle(node).transform
})
const x1 = getTranslate(transformText1, 'x')
const y1 = getTranslate(transformText1, 'y')
const scale1 = getScale(transformText1)
expect(x1).toBe(47.5)
expect(y1).toBe(89)
expect(scale1).toBe(1.2)
const transformText2 = await page.$eval('.content2', node => {
return window.getComputedStyle(node).transform
})
const x2 = getTranslate(transformText2, 'x')
const y2 = getTranslate(transformText2, 'y')
expect(x2).toBe(0)
expect(y2).toBe(150)
})
it('should work well when call putAt()', async () => {
await page.waitFor(300)
await page.click('.btn')
await page.waitFor(1000)
const transformText2 = await page.$eval('.content2', node => {
return window.getComputedStyle(node).transform
})
const x2 = getTranslate(transformText2, 'x')
const y2 = getTranslate(transformText2, 'y')
expect(x2).toBe(135)
expect(y2).toBe(215)
})
it('should work well when perform a zoom-out gesture', async () => {
await page.waitFor(300)
// zoom out
await page.dispatchPinch({
x: 265,
y: 165,
scaleFactor: 1.5,
gestureSourceType: 'touch'
})
const transformText1 = await page.$eval('.content1', node => {
return window.getComputedStyle(node).transform
})
const scale1 = getScale(transformText1)
expect(scale1).toBeGreaterThan(1.2)
})
})
================================================
FILE: tests/e2e/movable/multi-content.e2e.ts
================================================
import { Page } from 'puppeteer'
import extendTouch from '../../util/extendTouch'
import getTranslate from '../../util/getTranslate'
jest.setTimeout(10000000)
describe('Movable with multi content', () => {
let page = (global as any).page as Page
extendTouch(page)
beforeAll(async () => {
await page.goto('http://0.0.0.0:8932/#/movable/multi-content')
})
beforeEach(async () => {
await page.reload({
waitUntil: 'domcontentloaded'
})
})
it('should work well', async () => {
await page.waitFor(300)
const transformText1 = await page.$eval('.content1', node => {
return window.getComputedStyle(node).transform
})
const x1 = getTranslate(transformText1, 'x')
const y1 = getTranslate(transformText1, 'y')
expect(x1).toBe(10)
expect(y1).toBe(10)
const transformText2 = await page.$eval('.content2', node => {
return window.getComputedStyle(node).transform
})
const x2 = getTranslate(transformText2, 'x')
const y2 = getTranslate(transformText2, 'y')
expect(x2).toBe(0)
expect(y2).toBe(170)
})
it('should work well when call putAt()', async () => {
await page.waitFor(300)
await page.click('.btn')
await page.waitFor(1000)
const transformText2 = await page.$eval('.content2', node => {
return window.getComputedStyle(node).transform
})
const x2 = getTranslate(transformText2, 'x')
const y2 = getTranslate(transformText2, 'y')
expect(x2).toBe(67.5)
expect(y2).toBe(107.5)
})
})
================================================
FILE: tests/e2e/movable/scaled.e2e.ts
================================================
import { Page } from 'puppeteer'
import extendTouch from '../../util/extendTouch'
import getTranslate from '../../util/getTranslate'
import getScale from '../../util/getScale'
jest.setTimeout(10000000)
describe('Movable & Zoom Plugin', () => {
let page = (global as any).page as Page
extendTouch(page)
beforeAll(async () => {
await page.goto('http://0.0.0.0:8932/#/movable/scale')
})
beforeEach(async () => {
await page.reload({
waitUntil: 'domcontentloaded'
})
})
it('should support zoom in', async () => {
await page.waitFor(1000)
await page.waitFor(1000)
// zoom in
await page.dispatchPinch({
x: 180,
y: 180,
scaleFactor: 0.9,
gestureSourceType: 'touch'
})
const scaledElTransformText = await page.$eval('.scroll-content', node => {
return window.getComputedStyle(node).transform
})
const x = getTranslate(scaledElTransformText, 'x')
const y = getTranslate(scaledElTransformText, 'x')
const scale = getScale(scaledElTransformText)
expect(x).toBeGreaterThan(0)
expect(y).toBeGreaterThan(20)
expect(scale).toBeLessThan(1)
})
it('should support zoom out', async () => {
await page.waitFor(1000)
// zoom out
await page.dispatchPinch({
x: 180,
y: 180,
scaleFactor: 1.5,
gestureSourceType: 'touch'
})
await page.waitFor(1000)
const scaledElTransformText = await page.$eval('.scroll-content', node => {
return window.getComputedStyle(node).transform
})
const x = getTranslate(scaledElTransformText, 'x')
const y = getTranslate(scaledElTransformText, 'x')
const scale = getScale(scaledElTransformText)
expect(x).toBeLessThan(0)
expect(y).toBeLessThan(0)
expect(scale).toBeGreaterThan(1)
})
// it('should work well when dispatchScroll', async () => {
// await page.waitFor(300)
// await page.dispatchScroll({
// x: 100,
// y: 100,
// xDistance: -70,
// yDistance: -70,
// gestureSourceType: 'touch'
// })
// await page.waitFor(2000)
// const scaledElTransformText = await page.$eval('.scroll-content', node => {
// return window.getComputedStyle(node).transform
// })
// const x = getTranslate(scaledElTransformText, 'x')
// const y = getTranslate(scaledElTransformText, 'x')
// expect(x).toBe(0)
// expect(y).toBe(0)
// })
})
================================================
FILE: tests/e2e/nested-scroll/horizontal-in-vertical.e2e.ts
================================================
import { Page } from 'puppeteer'
import extendTouch from '../../util/extendTouch'
import getTranslate from '../../util/getTranslate'
jest.setTimeout(10000000)
describe('Nested horizontal-in-vertical scroll', () => {
let page = (global as any).page as Page
extendTouch(page)
beforeAll(async () => {
await page.goto(
'http://0.0.0.0:8932/#/nested-scroll/horizontal-in-vertical'
)
})
beforeEach(async () => {
await page.reload({
waitUntil: 'domcontentloaded',
})
})
it('should make outer BScroll scroll when manipulating outerBScroll', async () => {
await page.waitFor(300)
await page.dispatchScroll({
x: 100,
y: 60,
xDistance: 0,
yDistance: -70,
gestureSourceType: 'touch',
})
await page.waitFor(1000)
const transformText = await page.$eval('.vertical-content', (node) => {
return window.getComputedStyle(node).transform
})
const translateX = getTranslate(transformText!, 'y')
await expect(translateX).toBeLessThan(-30)
})
it('should only make innerBScroll scroll when manipulating innerBScroll', async () => {
await page.waitFor(300)
await page.dispatchScroll({
x: 100,
y: 200,
xDistance: -300,
yDistance: 0,
gestureSourceType: 'touch',
})
await page.waitFor(1000)
const outerTransformText = await page.$eval('.vertical-content', (node) => {
return window.getComputedStyle(node).transform
})
const outerTranslateX = getTranslate(outerTransformText!, 'x')
await expect(outerTranslateX).toBe(0)
const innerTransformText = await page.$eval(
'.slide-banner-content',
(node) => {
return window.getComputedStyle(node).transform
}
)
const innerTranslateX = getTranslate(innerTransformText!, 'x')
await expect(innerTranslateX).toBeLessThan(-100)
})
})
================================================
FILE: tests/e2e/nested-scroll/horizontal.e2e.ts
================================================
import { Page } from 'puppeteer'
import extendTouch from '../../util/extendTouch'
import getTranslate from '../../util/getTranslate'
jest.setTimeout(10000000)
describe('Nested horizontal scroll', () => {
let page = (global as any).page as Page
extendTouch(page)
beforeAll(async () => {
await page.goto('http://0.0.0.0:8932/#/nested-scroll/horizontal')
})
beforeEach(async () => {
await page.reload({
waitUntil: 'domcontentloaded',
})
})
it('should make outer BScroll scroll when manipulating outerBScroll', async () => {
await page.waitFor(300)
await page.dispatchScroll({
x: 100,
y: 110,
xDistance: -70,
yDistance: 0,
gestureSourceType: 'touch',
})
await page.waitFor(2500)
const transformText = await page.$eval('.outer-content', (node) => {
return window.getComputedStyle(node).transform
})
const translateX = getTranslate(transformText!, 'x')
await expect(translateX).toBeLessThan(-30)
})
it('should only make innerBScroll scroll', async () => {
await page.waitFor(300)
await page.dispatchScroll({
x: 270,
y: 110,
xDistance: -70,
yDistance: 0,
gestureSourceType: 'touch',
})
await page.waitFor(2500)
const outerTransformText = await page.$eval('.outer-content', (node) => {
return window.getComputedStyle(node).transform
})
const outerTranslateX = getTranslate(outerTransformText!, 'x')
await expect(outerTranslateX).toBe(0)
const innerTransformText = await page.$eval('.inner-content', (node) => {
return window.getComputedStyle(node).transform
})
const innerTranslateY = getTranslate(innerTransformText!, 'x')
await expect(innerTranslateY).toBeLessThan(-30)
})
it('should make outer BScroll scroll when innerScroll reached boundary', async () => {
await page.waitFor(300)
await page.dispatchScroll({
x: 270,
y: 110,
xDistance: -600,
yDistance: 0,
speed: 1800,
gestureSourceType: 'touch',
})
await page.waitFor(2500)
const innerTransformText = await page.$eval('.inner-content', (node) => {
return window.getComputedStyle(node).transform
})
const innerTranslateX = getTranslate(innerTransformText!, 'x')
await expect(innerTranslateX).toBeLessThan(-50)
await page.dispatchScroll({
x: 270,
y: 110,
xDistance: -50,
yDistance: 0,
gestureSourceType: 'touch',
})
const outerTransformText = await page.$eval('.outer-content', (node) => {
return window.getComputedStyle(node).transform
})
const outerTranslateX = getTranslate(outerTransformText!, 'x')
await expect(outerTranslateX).toBeLessThan(-20)
})
it('should support click handle when use nestedScroll plugin', async () => {
const mockOuterHandler = jest.fn()
const mockInnerHandler = jest.fn()
page.once('dialog', async (dialog) => {
mockOuterHandler()
await dialog.dismiss()
})
// outer click
await page.touchscreen.tap(150, 110)
expect(mockOuterHandler).toBeCalledTimes(1)
await page.waitFor(500)
page.once('dialog', async (dialog) => {
mockInnerHandler()
await dialog.dismiss()
})
// inner click
await page.touchscreen.tap(300, 110)
expect(mockInnerHandler).toBeCalledTimes(1)
})
})
================================================
FILE: tests/e2e/nested-scroll/triple-vertical.e2e.ts
================================================
import { Page } from 'puppeteer'
import extendTouch from '../../util/extendTouch'
import getTranslate from '../../util/getTranslate'
jest.setTimeout(10000000)
describe('Nested triple-vertical scroll', () => {
let page = (global as any).page as Page
extendTouch(page)
beforeAll(async () => {
await page.goto('http://0.0.0.0:8932/#/nested-scroll/triple-vertical')
})
beforeEach(async () => {
await page.reload({
waitUntil: 'domcontentloaded',
})
})
it('should make outer BScroll scroll and others keep unmoved when manipulating outerBScroll', async () => {
await page.waitFor(300)
await page.dispatchScroll({
x: 160,
y: 150,
xDistance: 0,
yDistance: -70,
gestureSourceType: 'touch',
})
await page.waitFor(2500)
const outerTransformText = await page.$eval('.outer-content', (node) => {
return window.getComputedStyle(node).transform
})
const middleTransformText = await page.$eval('.middle-content', (node) => {
return window.getComputedStyle(node).transform
})
const innerTransformText = await page.$eval('.inner-content', (node) => {
return window.getComputedStyle(node).transform
})
const outerTranslateY = getTranslate(outerTransformText!, 'y')
const middleTranslateY = getTranslate(middleTransformText!, 'y')
const innerTranslateY = getTranslate(innerTransformText!, 'y')
expect(outerTranslateY).toBeLessThan(-30)
expect(middleTranslateY).toBeNaN()
expect(innerTranslateY).toBeNaN()
})
it('should only make middle scroll and others keep unmoved', async () => {
await page.waitFor(300)
await page.dispatchScroll({
x: 160,
y: 250,
xDistance: 0,
yDistance: -70,
gestureSourceType: 'touch',
})
await page.waitFor(1000)
const outerTransformText = await page.$eval('.outer-content', (node) => {
return window.getComputedStyle(node).transform
})
const outerTranslateY = getTranslate(outerTransformText!, 'y')
const middleTransformText = await page.$eval('.middle-content', (node) => {
return window.getComputedStyle(node).transform
})
const middleTranslateY = getTranslate(middleTransformText!, 'y')
const innerTransformText = await page.$eval('.inner-content', (node) => {
return window.getComputedStyle(node).transform
})
const innerTranslateY = getTranslate(innerTransformText!, 'y')
expect(innerTranslateY).toBeNaN()
expect(middleTranslateY).toBeLessThan(-30)
expect(outerTranslateY).toBe(0)
})
it('should only make inner scroll and others keep unmoved', async () => {
await page.waitFor(300)
await page.dispatchScroll({
x: 160,
y: 450,
xDistance: 0,
yDistance: -70,
gestureSourceType: 'touch',
})
await page.waitFor(1000)
const outerTransformText = await page.$eval('.outer-content', (node) => {
return window.getComputedStyle(node).transform
})
const outerTranslateY = getTranslate(outerTransformText!, 'y')
const middleTransformText = await page.$eval('.middle-content', (node) => {
return window.getComputedStyle(node).transform
})
const middleTranslateY = getTranslate(middleTransformText!, 'y')
const innerTransformText = await page.$eval('.inner-content', (node) => {
return window.getComputedStyle(node).transform
})
const innerTranslateY = getTranslate(innerTransformText!, 'y')
expect(innerTranslateY).toBeLessThan(-30)
expect(middleTranslateY).toBe(0)
expect(outerTranslateY).toBe(0)
})
it('should make parent BScroll scroll when innerScroll reached boundary', async () => {
await page.waitFor(300)
await page.dispatchScroll({
x: 160,
y: 450,
xDistance: 0,
yDistance: 50,
speed: 3000,
gestureSourceType: 'touch',
})
const outerTransformText = await page.$eval('.outer-content', (node) => {
return window.getComputedStyle(node).transform
})
const outerTranslateY = getTranslate(outerTransformText!, 'y')
const middleTransformText = await page.$eval('.middle-content', (node) => {
return window.getComputedStyle(node).transform
})
const middleTranslateY = getTranslate(middleTransformText!, 'y')
const innerTransformText = await page.$eval('.inner-content', (node) => {
return window.getComputedStyle(node).transform
})
const innerTranslateY = getTranslate(innerTransformText!, 'y')
expect(outerTranslateY).toBeGreaterThan(0)
expect(middleTranslateY).toBeNaN()
expect(innerTranslateY).toBeNaN()
})
it('click', async () => {
const mockOuterHandler = jest.fn()
const mockMiddleHandler = jest.fn()
const mockInnerHandler = jest.fn()
// outer click
page.once('dialog', async (dialog) => {
mockOuterHandler()
await dialog.dismiss()
})
await page.touchscreen.tap(150, 100)
expect(mockOuterHandler).toBeCalledTimes(1)
await page.waitFor(500)
// middle click
page.once('dialog', async (dialog) => {
mockMiddleHandler()
await dialog.dismiss()
})
await page.touchscreen.tap(150, 250)
expect(mockMiddleHandler).toBeCalledTimes(1)
await page.waitFor(500)
// inner click
page.once('dialog', async (dialog) => {
mockInnerHandler()
await dialog.dismiss()
})
await page.touchscreen.tap(150, 400)
expect(mockInnerHandler).toBeCalledTimes(1)
})
})
================================================
FILE: tests/e2e/nested-scroll/vertical.e2e.ts
================================================
import { Page } from 'puppeteer'
import extendTouch from '../../util/extendTouch'
import getTranslate from '../../util/getTranslate'
jest.setTimeout(10000000)
describe('Nested vertical scroll', () => {
let page = (global as any).page as Page
extendTouch(page)
beforeAll(async () => {
await page.goto('http://0.0.0.0:8932/#/nested-scroll/vertical')
})
beforeEach(async () => {
await page.reload({
waitUntil: 'domcontentloaded',
})
})
it('should make outer BScroll scroll when manipulating outerBScroll', async () => {
await page.waitFor(300)
await page.dispatchScroll({
x: 160,
y: 150,
xDistance: 0,
yDistance: -70,
gestureSourceType: 'touch',
})
await page.waitFor(2500)
const transformText = await page.$eval('.outer-content', (node) => {
return window.getComputedStyle(node).transform
})
const translateY = getTranslate(transformText!, 'y')
await expect(translateY).toBeLessThan(-30)
})
it('should only make innerBScroll scroll and outerBScroll stop', async () => {
await page.waitFor(300)
await page.dispatchScroll({
x: 160,
y: 300,
xDistance: 0,
yDistance: -70,
gestureSourceType: 'touch',
})
await page.waitFor(1000)
const outerTransformText = await page.$eval('.outer-content', (node) => {
return window.getComputedStyle(node).transform
})
const outerTranslateY = getTranslate(outerTransformText!, 'y')
await expect(outerTranslateY).toBe(0)
const innerTransformText = await page.$eval('.inner-content', (node) => {
return window.getComputedStyle(node).transform
})
const innerTranslateY = getTranslate(innerTransformText!, 'y')
await expect(innerTranslateY).toBeLessThan(-30)
})
it('should make outer BScroll scroll when innerScroll reached boundary', async () => {
await page.waitFor(300)
await page.dispatchScroll({
x: 160,
y: 300,
xDistance: 0,
yDistance: -300,
speed: 3000,
gestureSourceType: 'touch',
})
await page.waitFor(1000)
const innerTransformText = await page.$eval('.inner-content', (node) => {
return window.getComputedStyle(node).transform
})
const innerTranslateY = getTranslate(innerTransformText!, 'y')
await expect(innerTranslateY).toBeLessThan(-20)
await page.dispatchScroll({
x: 160,
y: 300,
xDistance: 0,
yDistance: -100,
gestureSourceType: 'touch',
})
const outerTransformText = await page.$eval('.outer-content', (node) => {
return window.getComputedStyle(node).transform
})
const outerTranslateY = getTranslate(outerTransformText!, 'y')
await expect(outerTranslateY).toBeLessThan(-50)
})
it('should support click handle when use nestedScroll plugin', async () => {
const mockOuterHandler = jest.fn()
const mockInnerHandler = jest.fn()
page.once('dialog', async (dialog) => {
mockOuterHandler()
await dialog.dismiss()
})
// outer click
await page.touchscreen.tap(300, 100)
expect(mockOuterHandler).toBeCalledTimes(1)
await page.waitFor(500)
page.once('dialog', async (dialog) => {
mockInnerHandler()
await dialog.dismiss()
})
// inner click
await page.touchscreen.tap(350, 500)
expect(mockInnerHandler).toBeCalledTimes(1)
})
})
================================================
FILE: tests/e2e/observe-dom/observe-dom.e2e.ts
================================================
import { Page } from 'puppeteer'
import extendTouch from '../../util/extendTouch'
import getTranslate from '../../util/getTranslate'
jest.setTimeout(10000000)
describe('ObserveDOM', () => {
let page = (global as any).page as Page
extendTouch(page)
beforeEach(async () => {
await page.goto('http://0.0.0.0:8932/#/observe-dom/')
})
it('should observe DOM change and auto refresh bs', async () => {
await page.waitFor(300)
const preItemsCounts = await page.$$eval(
'.scroll-item',
(element) => element.length
)
expect(preItemsCounts).toBe(10)
await page.click('.btn')
await page.waitFor(100)
const PostItemsCounts = await page.$$eval(
'.scroll-item',
(element) => element.length
)
expect(PostItemsCounts).toBe(12)
await page.dispatchScroll({
x: 100,
y: 120,
xDistance: -150,
yDistance: 0,
gestureSourceType: 'touch',
})
await page.waitFor(2600)
const transformText = await page.$eval('.scroll-content', (node) => {
return window.getComputedStyle(node).transform
})
const x = getTranslate(transformText, 'x')
expect(x).toBeLessThanOrEqual(-361)
})
})
================================================
FILE: tests/e2e/observe-image/observe-image.e2e.ts
================================================
import { Page } from 'puppeteer'
import extendTouch from '../../util/extendTouch'
import getTranslate from '../../util/getTranslate'
jest.setTimeout(10000000)
describe('ObserveImage', () => {
let page = (global as any).page as Page
extendTouch(page)
beforeEach(async () => {
await page.goto('http://0.0.0.0:8932/#/observe-image/')
})
it('should autorefresh when img loaded', async () => {
await page.waitFor(3000)
await page.dispatchScroll({
x: 100,
y: 120,
xDistance: 0,
yDistance: -50,
gestureSourceType: 'touch',
})
const transformText = await page.$eval('.scroll-content', (node) => {
return window.getComputedStyle(node).transform
})
const y = getTranslate(transformText, 'y')
expect(y).toBeLessThan(-30)
})
})
================================================
FILE: tests/e2e/picker/double-column.e2e.ts
================================================
import { Page } from 'puppeteer'
import extendTouch from '../../util/extendTouch'
import getTranslate from '../../util/getTranslate'
jest.setTimeout(10000000)
describe('Double column picker', () => {
let page = (global as any).page as Page
extendTouch(page)
beforeAll(async () => {
await page.goto('http://0.0.0.0:8932/#/picker/double-column')
})
beforeEach(async () => {
await page.reload({
waitUntil: 'domcontentloaded',
})
})
it('should render picker DOM correctly', async () => {
await page.waitFor(300)
await page.click('.open')
await page.waitFor(500)
const displayText = await page.$eval('.picker-panel', (node) => {
return window.getComputedStyle(node).display
})
await expect(displayText).toBe('block')
})
it('should get correct text when click "confirm" button', async () => {
await page.waitFor(300)
await page.click('.open')
await page.waitFor(1000)
const openBtn = await page.$('.open')
await page.click('.confirm')
// wait for transition ends
await page.waitFor(100)
const innerText = await page.$eval('.open', (node) => {
return node.textContent
})
await expect(innerText).toBe('Venomancer-0__Durable-0')
})
it('should scroll correctly when simulate touch event on each column', async () => {
await page.waitFor(300)
await page.click('.open')
await page.waitFor(1000)
// first column
await page.dispatchScroll({
x: 100,
y: 630,
xDistance: 0,
yDistance: -70,
gestureSourceType: 'touch',
})
// second column
await page.dispatchScroll({
x: 270,
y: 630,
xDistance: 0,
yDistance: -70,
gestureSourceType: 'touch',
})
// wait for transition ends
await page.waitFor(1000)
const transformTexts = await page.$$eval('.wheel-scroll', (nodes) => {
return nodes.map((node) => window.getComputedStyle(node).transform)
})
for (const transformText of transformTexts) {
const translateY = getTranslate(transformText, 'y')
await expect(translateY).toBeLessThan(-72)
}
})
})
================================================
FILE: tests/e2e/picker/linkage-column.e2e.ts
================================================
import { Page } from 'puppeteer'
import extendTouch from '../../util/extendTouch'
jest.setTimeout(10000000)
describe('Linkage column picker', () => {
let page = (global as any).page as Page
extendTouch(page)
beforeAll(async () => {
await page.goto('http://0.0.0.0:8932/#/picker/linkage-column')
})
beforeEach(async () => {
await page.reload({
waitUntil: 'domcontentloaded',
})
})
it('should get correct text when click "confirm" button', async () => {
await page.waitFor(300)
await page.click('.open')
await page.waitFor(1000)
const openBtn = await page.$('.open')
await page.click('.confirm')
// wait for transition ends
await page.waitFor(100)
const innerText = await page.$eval('.open', (node) => {
return node.textContent
})
await expect(innerText).toBe('北京市-0__北京市-0')
})
it('should linkage correctly when click TianJin province', async () => {
await page.waitFor(300)
await page.click('.open')
await page.waitFor(1000)
const [firstWheelScroll] = await page.$$('.wheel-scroll')
const [, TianJinProvince] = await firstWheelScroll.$$('.wheel-item')
await TianJinProvince.tap()
// when transition ends
await page.waitFor(500)
const cityBtnText = await page.$$eval('.wheel-scroll', (nodes) => {
return nodes[1].querySelectorAll('.wheel-item')[0].textContent
})
await expect(cityBtnText).toBe('天津市')
})
it('should linkage correctly when dispatch touch event in first column', async () => {
await page.waitFor(300)
await page.click('.open')
await page.waitFor(1000)
// first column
await page.dispatchScroll({
x: 100,
y: 630,
xDistance: 0,
yDistance: -70,
gestureSourceType: 'touch',
})
// when transition ends
await page.waitFor(1000)
const cityBtnText = await page.$$eval('.wheel-scroll', (nodes) => {
return nodes[1].querySelectorAll('.wheel-item')[0].textContent
})
await expect(cityBtnText).not.toBe('北京市')
})
})
================================================
FILE: tests/e2e/picker/one-column.e2e.ts
================================================
import { Page } from 'puppeteer'
import extendTouch from '../../util/extendTouch'
import getTranslate from '../../util/getTranslate'
jest.setTimeout(10000000)
describe('One column picker', () => {
let page = (global as any).page as Page
extendTouch(page)
beforeAll(async () => {
await page.goto('http://0.0.0.0:8932/#/picker/one-column')
})
beforeEach(async () => {
await page.reload({
waitUntil: 'domcontentloaded',
})
})
it('should render picker DOM correctly', async () => {
await page.waitFor(300)
await page.click('.open')
await page.waitFor(500)
const displayText = await page.$eval('.picker-panel', (node) => {
return window.getComputedStyle(node).display
})
await expect(displayText).toBe('block')
})
it('should wheelTo third item by default', async () => {
await page.waitFor(300)
await page.click('.open')
await page.waitFor(500)
const transformText = await page.$eval('.wheel-scroll', (node) => {
return window.getComputedStyle(node).transform
})
const translateY = getTranslate(transformText, 'y')
await expect(translateY).toBe(-72)
})
it('should not select disabled item', async () => {
await page.waitFor(300)
await page.click('.open')
await page.waitFor(1000)
await page.tap('.wheel-disabled-item')
await page.waitFor(500)
const transformText = await page.$eval('.wheel-scroll', (node) => {
return window.getComputedStyle(node).transform
})
const translateY = getTranslate(transformText, 'y')
await expect(translateY).toBe(-36)
})
it('should wheel to second item when click second item', async () => {
await page.waitFor(300)
await page.click('.open')
await page.waitFor(1000)
const items = await page.$$('.wheel-item')
const secondItem = items[1]
await secondItem.tap()
// wait for transition ends
await page.waitFor(1000)
const transformText = await page.$eval('.wheel-scroll', (node) => {
return window.getComputedStyle(node).transform
})
const translateY = getTranslate(transformText, 'y')
await expect(translateY).toBe(-36)
})
it('should scroll correctly when simulate touch event', async () => {
await page.waitFor(300)
await page.click('.open')
await page.waitFor(1000)
await page.dispatchScroll({
x: 200,
y: 630,
xDistance: 0,
yDistance: -70,
gestureSourceType: 'touch',
})
// wait for transition ends
await page.waitFor(1000)
const transformText = await page.$eval('.wheel-scroll', (node) => {
return window.getComputedStyle(node).transform
})
const translateY = getTranslate(transformText, 'y')
await expect(translateY).toBeLessThan(-72)
})
it('should restore position when bs is scrolling and invoke restorePosition()', async () => {
await page.waitFor(300)
await page.click('.open')
await page.waitFor(1000)
await page.dispatchScroll({
x: 200,
y: 630,
xDistance: 0,
yDistance: -100,
gestureSourceType: 'touch',
})
await page.click('.cancel')
await page.waitFor(500)
await page.click('.open')
await page.waitFor(500)
const transformText = await page.$eval('.wheel-scroll', (node) => {
return window.getComputedStyle(node).transform
})
const translateY = getTranslate(transformText, 'y')
expect(translateY).toBe(-72)
})
it('should stop at the nearest wheel item when bs is scrolling and invoke stop()', async () => {
await page.waitFor(300)
await page.click('.open')
await page.waitFor(1000)
await page.dispatchScroll({
x: 200,
y: 630,
xDistance: 0,
yDistance: -100,
gestureSourceType: 'touch',
})
await page.click('.confirm')
await page.waitFor(500)
await page.click('.open')
await page.waitFor(500)
const transformText = await page.$eval('.wheel-scroll', (node) => {
return window.getComputedStyle(node).transform
})
const translateY = getTranslate(transformText, 'y')
expect(translateY).toBeLessThan(-180)
})
})
================================================
FILE: tests/e2e/pulldown/default.e2e.ts
================================================
import { Page } from 'puppeteer'
import extendTouch from '../../util/extendTouch'
jest.setTimeout(10000000)
describe('Pulldown', () => {
let page = (global as any).page as Page
extendTouch(page)
beforeEach(async () => {
// disable cache
await page.setCacheEnabled(false)
await page.goto('http://0.0.0.0:8932/#/pulldown/default')
})
it('should render DOM correctly', async () => {
await page.waitFor(300)
const itemsCounts = await page.$$eval(
'.pulldown-list-item',
(element) => element.length
)
await expect(itemsCounts).toBeGreaterThanOrEqual(20)
})
it('should trigger pullingdown when BS reached the top', async () => {
await page.waitFor(300)
await page.dispatchScroll({
x: 200,
y: 100,
xDistance: 0,
yDistance: 400,
speed: 1500,
gestureSourceType: 'touch',
})
// wait for requesting data
await page.waitFor(3000)
const itemsCounts = await page.$$eval(
'.pulldown-list-item',
(element) => element.length
)
// has loaded
await expect(itemsCounts).toBeGreaterThanOrEqual(40)
})
})
================================================
FILE: tests/e2e/pulldown/sina.e2e.ts
================================================
import { Page } from 'puppeteer'
import extendTouch from '../../util/extendTouch'
jest.setTimeout(10000000)
const chunk = (array: T[], size: number) => {
let index = 0
let resIndex = 0
const length = array.length
const result = new Array(Math.ceil(length / size))
while (index < length) {
result[resIndex++] = array.slice(index, (index += size))
}
return result
}
describe('Pulldown-sina-weibo', () => {
let page = (global as any).page as Page
extendTouch(page)
beforeEach(async () => {
// disable cache
await page.setCacheEnabled(false)
await page.goto('http://0.0.0.0:8932/#/pulldown/sina')
})
it('should render DOM correctly', async () => {
await page.waitFor(300)
const itemsCounts = await page.$$eval(
'.pulldown-list-item',
(element) => element.length
)
await expect(itemsCounts).toBeGreaterThanOrEqual(20)
})
it('should go through correct phase', async () => {
await page.waitFor(300)
await page.dispatchTouch({
type: 'touchStart',
touchPoints: [
{
x: 200,
y: 40,
},
],
})
const touchMovePoints = (() => {
const start = 70
const step = 5
const end = 550
const x = 200
let ret: Array<{ x: number; y: number }> = []
for (let i = start; i <= end; i += step) {
ret.push({
x,
y: i,
})
}
// chrome only allow 16 items in touchPoints array
return chunk(ret, 16)
})()
// touchmove
for (const touchPoint of touchMovePoints) {
await page.dispatchTouch({
type: 'touchMove',
touchPoints: touchPoint,
})
}
const textContent = await page.$$eval(
'.pulldown-wrapper',
(element) => element[0].textContent
)
expect(textContent).toContain('Release')
await page.dispatchTouch({
type: 'touchEnd',
touchPoints: [
{
x: 200,
y: 560,
},
],
})
await page.waitFor(300)
// loading
const textContent2 = await page.$$eval(
'.pulldown-wrapper',
(element) => element[0].textContent
)
expect(textContent2).toContain('Loading...')
await page.waitFor(3000)
// refresh succeed
const textContent3 = await page.$$eval(
'.pulldown-wrapper',
(element) => element[0].textContent
)
expect(textContent3).toContain('Refresh succeed')
const itemsCounts = await page.$$eval(
'.pulldown-list-item',
(element) => element.length
)
// has loaded
expect(itemsCounts).toBeGreaterThanOrEqual(40)
})
})
================================================
FILE: tests/e2e/pullup/default.e2e.ts
================================================
import { Page } from 'puppeteer'
import extendTouch from '../../util/extendTouch'
jest.setTimeout(10000000)
describe('Pullup', () => {
let page = (global as any).page as Page
extendTouch(page)
beforeEach(async () => {
// disable cache
await page.setCacheEnabled(false)
await page.goto('http://0.0.0.0:8932/#/pullup/')
})
it('should render DOM correctly', async () => {
await page.waitFor(300)
const itemsCounts = await page.$$eval(
'.pullup-list-item',
element => element.length
)
await expect(itemsCounts).toBeGreaterThanOrEqual(30)
})
it('should trigger pullingup when BS reached the bottom', async () => {
await page.waitFor(300)
await page.dispatchScroll({
x: 200,
y: 630,
xDistance: 0,
yDistance: -1000,
speed: 1500,
gestureSourceType: 'touch'
})
// wait for requesting data
await page.waitFor(3000)
const itemsCounts = await page.$$eval(
'.pullup-list-item',
element => element.length
)
// has loaded
await expect(itemsCounts).toBeGreaterThan(40)
})
})
================================================
FILE: tests/e2e/scrollbar/custom.e2e.ts
================================================
import { Page } from 'puppeteer'
import extendTouch from '../../util/extendTouch'
import getTranslate from '../../util/getTranslate'
jest.setTimeout(10000000)
describe('Scrollbar-custom', () => {
let page = (global as any).page as Page
extendTouch(page)
beforeAll(async () => {
await page.goto('http://0.0.0.0:8932/#/scrollbar/custom')
})
beforeEach(async () => {
await page.reload({
waitUntil: 'domcontentloaded',
})
})
it('should trigger BS to move when manipulating scrollbar', async () => {
await page.waitFor(300)
// horizontal
await page.dispatchScroll({
x: 123,
y: 288,
xDistance: 70,
yDistance: 0,
gestureSourceType: 'touch',
})
const transformXText = await page.$eval(
'.custom-scrollbar-content',
(node) => {
return window.getComputedStyle(node).transform
}
)
const x = getTranslate(transformXText, 'x')
expect(x).toBeLessThan(0)
// vertical
await page.dispatchScroll({
x: 288,
y: 123,
xDistance: 0,
yDistance: 70,
gestureSourceType: 'touch',
})
const transformYText = await page.$eval(
'.custom-scrollbar-content',
(node) => {
return window.getComputedStyle(node).transform
}
)
const y = getTranslate(transformYText, 'y')
expect(y).toBeLessThan(0)
})
it('should trigger make scrollbar scroll in when manipulating BS', async () => {
await page.waitFor(300)
await page.dispatchScroll({
x: 100,
y: 100,
xDistance: -100,
yDistance: 0,
gestureSourceType: 'touch',
})
const transformXText = await page.$eval(
'.custom-horizontal-indicator',
(node) => {
return window.getComputedStyle(node).transform
}
)
const x = getTranslate(transformXText, 'x')
expect(x).toBeGreaterThan(0)
await page.dispatchScroll({
x: 100,
y: 100,
xDistance: 100,
yDistance: -100,
gestureSourceType: 'touch',
})
const transformYText = await page.$eval(
'.custom-vertical-indicator',
(node) => {
return window.getComputedStyle(node).transform
}
)
const y = getTranslate(transformYText, 'y')
expect(y).toBeGreaterThan(0)
})
})
================================================
FILE: tests/e2e/scrollbar/horizontal.e2e.ts
================================================
import { Page } from 'puppeteer'
import extendTouch from '../../util/extendTouch'
import getTranslate from '../../util/getTranslate'
jest.setTimeout(10000000)
describe('Scrollbar-horizontal', () => {
let page = (global as any).page as Page
extendTouch(page)
beforeAll(async () => {
await page.goto('http://0.0.0.0:8932/#/scrollbar/horizontal')
})
beforeEach(async () => {
await page.reload({
waitUntil: 'domcontentloaded',
})
})
it('should render DOM correctly', async () => {
await page.waitFor(300)
const itemsCounts = await page.$$eval(
'.bscroll-horizontal-scrollbar',
(element) => element.length
)
await expect(itemsCounts).toBeGreaterThanOrEqual(1)
})
it('should trigger BS to move when manipulating scrollbar', async () => {
await page.waitFor(300)
await page.dispatchScroll({
x: 70,
y: 195,
xDistance: 70,
yDistance: 0,
gestureSourceType: 'touch',
})
const transformText = await page.$eval('.scroll-content', (node) => {
return window.getComputedStyle(node).transform
})
const x = getTranslate(transformText, 'x')
expect(x).toBeLessThan(0)
})
it('should make scrollbar fade in when dispatch touch event in BS', async () => {
await page.waitFor(300)
await page.dispatchScroll({
x: 100,
y: 100,
xDistance: -100,
yDistance: 0,
gestureSourceType: 'touch',
})
const opacity = await page.$eval(
'.bscroll-horizontal-scrollbar',
(node) => {
return window.getComputedStyle(node).opacity
}
)
expect(Number(opacity)).toBeGreaterThan(0)
})
it('should make scrollbar scroll in when manipulating BS', async () => {
await page.waitFor(300)
await page.dispatchScroll({
x: 100,
y: 100,
xDistance: -100,
yDistance: 0,
gestureSourceType: 'touch',
})
const transformText = await page.$eval('.bscroll-indicator', (node) => {
return window.getComputedStyle(node).transform
})
const x = getTranslate(transformText, 'x')
expect(x).toBeGreaterThan(0)
})
})
================================================
FILE: tests/e2e/scrollbar/mousewheel.e2e.ts
================================================
import { Page } from 'puppeteer'
import extendMouseWheel from '../../util/extendMouseWheel'
import getTranslate from '../../util/getTranslate'
jest.setTimeout(10000000)
describe('Scrollbar-mousewheel', () => {
let page = (global as any).page as Page
extendMouseWheel(page)
beforeAll(async () => {
await page.goto('http://0.0.0.0:8932/#/scrollbar/mousewheel')
})
it('should trigger BS to move when using mousewheel', async () => {
await page.waitFor(2000)
await page.dispatchMouseWheel({
type: 'mouseWheel',
x: 100,
y: 100,
deltaX: 0,
deltaY: 100,
})
await page.waitFor(1000)
const transformText = await page.$eval(
'.custom-horizontal-indicator',
(node) => {
return window.getComputedStyle(node).transform
}
)
const x = getTranslate(transformText, 'x')
expect(x).toBeGreaterThan(0)
})
})
================================================
FILE: tests/e2e/scrollbar/vertical.e2e.ts
================================================
import { Page } from 'puppeteer'
import extendTouch from '../../util/extendTouch'
import getTranslate from '../../util/getTranslate'
jest.setTimeout(10000000)
describe('Scrollbar-vertical', () => {
let page = (global as any).page as Page
extendTouch(page)
beforeAll(async () => {
await page.goto('http://0.0.0.0:8932/#/scrollbar/vertical')
})
beforeEach(async () => {
await page.reload({
waitUntil: 'domcontentloaded',
})
})
it('should render DOM correctly', async () => {
await page.waitFor(300)
const itemsCounts = await page.$$eval(
'.bscroll-vertical-scrollbar',
(element) => element.length
)
await expect(itemsCounts).toBeGreaterThanOrEqual(1)
})
it('should trigger BS to move when manipulating scrollbar', async () => {
await page.waitFor(300)
await page.dispatchScroll({
x: 325,
y: 560,
xDistance: 0,
yDistance: -70,
gestureSourceType: 'touch',
})
const transformText = await page.$eval('.scrollbar-content', (node) => {
return window.getComputedStyle(node).transform
})
const y = getTranslate(transformText, 'y')
expect(y).toBeLessThan(0)
})
it('should make scrollbar fade in when dispatch touch event in BS', async () => {
await page.waitFor(300)
await page.dispatchScroll({
x: 100,
y: 100,
xDistance: 0,
yDistance: -100,
gestureSourceType: 'touch',
})
const opacity = await page.$eval('.bscroll-vertical-scrollbar', (node) => {
return window.getComputedStyle(node).opacity
})
expect(Number(opacity)).toBeGreaterThan(0)
})
it('should make scrollbar scroll in when manipulating BS', async () => {
await page.waitFor(300)
await page.dispatchScroll({
x: 100,
y: 100,
xDistance: 0,
yDistance: -100,
gestureSourceType: 'touch',
})
const transformText = await page.$eval('.bscroll-indicator', (node) => {
return window.getComputedStyle(node).transform
})
const y = getTranslate(transformText, 'y')
expect(y).toBeGreaterThan(0)
})
})
================================================
FILE: tests/e2e/slide/banner.e2e.ts
================================================
import { Page } from 'puppeteer'
import extendTouch from '../../util/extendTouch'
import getTranslate from '../../util/getTranslate'
jest.setTimeout(10000000)
describe('Slider for banner', () => {
let page = (global as any).page as Page
extendTouch(page)
beforeAll(async () => {
await page.goto('http://0.0.0.0:8932/#/slide/banner')
})
beforeEach(async () => {
await page.reload({
waitUntil: 'domcontentloaded',
})
})
it('should loop by default', async () => {
await page.waitFor(300)
// wait for slide autoplay
await page.waitFor(4000)
const transformText = await page.$eval('.slide-banner-content', (node) => {
return window.getComputedStyle(node).transform
})
const x = getTranslate(transformText, 'x')
expect(x).toBe(-670)
})
it('should go nextPage when click nextPage button', async () => {
await page.waitFor(300)
// simulate click
await page.click('.next')
// wait for bs to do a transition
await page.waitFor(1500)
const transformText = await page.$eval('.slide-banner-content', (node) => {
return window.getComputedStyle(node).transform
})
const x = getTranslate(transformText, 'x')
expect(x).toBe(-670)
})
it('should go prevPage when click prevPage button', async () => {
await page.waitFor(300)
await page.click('.next')
// wairt for bs to do a transition
await page.waitFor(1500)
const transformText1 = await page.$eval('.slide-banner-content', (node) => {
return window.getComputedStyle(node).transform
})
const x1 = getTranslate(transformText1, 'x')
expect(x1).toBe(-670)
// simulate click
await page.click('.prev')
await page.waitFor(1500)
const transformText2 = await page.$eval('.slide-banner-content', (node) => {
return window.getComputedStyle(node).transform
})
const x2 = getTranslate(transformText2, 'x')
expect(x2).toBe(-335)
})
it('should change index when drag slide', async () => {
await page.waitFor(300)
const currentIndex = await page.$eval('.dots-wrapper', (el) => {
const children = el.children
let index = 0
for (let i = 0; i < children.length; i++) {
if (children[i].className.indexOf('active') > -1) {
index = i
break
}
}
return index + 1
})
const nextDotsIndex = currentIndex === 3 ? 0 : currentIndex + 1
await page.dispatchScroll({
x: 200,
y: 120,
xDistance: -150,
yDistance: 0,
gestureSourceType: 'touch',
})
const secondDots = await page.$eval(
`.dots-wrapper .dot:nth-child(${nextDotsIndex})`,
(el) => el.className
)
expect(secondDots).toContain('active')
})
})
================================================
FILE: tests/e2e/slide/dynamic.e2e.ts
================================================
import { Page } from 'puppeteer'
import extendTouch from '../../util/extendTouch'
import getTranslate from '../../util/getTranslate'
jest.setTimeout(10000000)
describe('Slider for fullpage', () => {
let page = (global as any).page as Page
extendTouch(page)
beforeAll(async () => {
await page.goto('http://0.0.0.0:8932/#/slide/dynamic')
})
beforeEach(async () => {
await page.reload({
waitUntil: 'domcontentloaded',
})
})
it('increase', async () => {
await page.waitFor(300)
await page.click('.increase')
await page.waitFor(200)
const slidePageLen = await page.$$eval(
'.slide-page',
(element) => element.length
)
const slidePageTexts = await page.$$eval('.slide-page', (node) =>
node.map((n) => n.textContent)
)
expect(slidePageLen).toBe(4)
expect(slidePageTexts).toMatchObject([
'page 2',
'page 1',
'page 2',
'page 1',
])
})
it('decrease', async () => {
await page.waitFor(300)
await page.click('.increase')
await page.waitFor(200)
await page.click('.decrease')
await page.waitFor(200)
const slidePageLen = await page.$$eval(
'.slide-page',
(element) => element.length
)
const slidePageTexts = await page.$$eval('.slide-page', (node) =>
node.map((n) => n.textContent)
)
expect(slidePageLen).toBe(1)
expect(slidePageTexts).toMatchObject(['page 1'])
})
it('scroll ', async () => {
await page.waitFor(300)
await page.click('.increase')
await page.waitFor(200)
await page.dispatchScroll({
x: 200,
y: 150,
xDistance: -150,
yDistance: 0,
gestureSourceType: 'touch',
})
await page.waitFor(500)
const transformText = await page.$eval('.slide-banner-content', (node) => {
return window.getComputedStyle(node).transform
})
const x = getTranslate(transformText, 'x')
expect(x).toBe(-670)
await page.click('.increase')
await page.waitFor(200)
const transformText2 = await page.$eval('.slide-banner-content', (node) => {
return window.getComputedStyle(node).transform
})
const x2 = getTranslate(transformText2, 'x')
expect(x2).toBe(-670)
await page.dispatchScroll({
x: 200,
y: 150,
xDistance: -150,
yDistance: 0,
gestureSourceType: 'touch',
})
await page.waitFor(500)
const transformText3 = await page.$eval('.slide-banner-content', (node) => {
return window.getComputedStyle(node).transform
})
const x3 = getTranslate(transformText3, 'x')
expect(x3).toBe(-1005)
await page.click('.decrease')
await page.waitFor(200)
const transformText4 = await page.$eval('.slide-banner-content', (node) => {
return window.getComputedStyle(node).transform
})
const x4 = getTranslate(transformText4, 'x')
expect(x4).toBe(-670)
await page.click('.decrease')
await page.waitFor(200)
const transformText5 = await page.$eval('.slide-banner-content', (node) => {
return window.getComputedStyle(node).transform
})
const x5 = getTranslate(transformText5, 'x')
expect(x5).toBe(0)
})
})
================================================
FILE: tests/e2e/slide/fullpage.e2e.ts
================================================
import { Page } from 'puppeteer'
import extendTouch from '../../util/extendTouch'
import getTranslate from '../../util/getTranslate'
jest.setTimeout(10000000)
describe('Slider for fullpage', () => {
let page = (global as any).page as Page
extendTouch(page)
beforeAll(async () => {
await page.goto('http://0.0.0.0:8932/#/slide/fullpage')
})
beforeEach(async () => {
await page.reload({
waitUntil: 'domcontentloaded',
})
})
it('should not allow move to pre page when it is first page and loop is false', async () => {
await page.waitFor(300)
await page.dispatchScroll({
x: 100,
y: 120,
xDistance: 110,
yDistance: 0,
gestureSourceType: 'touch',
})
await page.waitFor(200)
const transformText = await page.$eval('.slide-banner-content', (node) => {
return window.getComputedStyle(node).transform
})
const x = getTranslate(transformText, 'x')
expect(x).toBe(0)
})
it('should not allow move to next page when it is last page and loop is false', async () => {
await page.waitFor(300)
// to second page
await page.dispatchScroll({
x: 100,
y: 120,
xDistance: -110,
yDistance: 0,
gestureSourceType: 'touch',
})
await page.waitFor(1000)
// to third page
await page.dispatchScroll({
x: 100,
y: 120,
xDistance: -110,
yDistance: 0,
gestureSourceType: 'touch',
})
await page.waitFor(1000)
// to last page
await page.dispatchScroll({
x: 100,
y: 120,
xDistance: -110,
yDistance: 0,
gestureSourceType: 'touch',
})
await page.waitFor(1000)
// attempts to go next
await page.dispatchScroll({
x: 100,
y: 120,
xDistance: -110,
yDistance: 0,
gestureSourceType: 'touch',
})
await page.waitFor(1000)
const transformText = await page.$eval('.slide-banner-content', (node) => {
return window.getComputedStyle(node).transform
})
const x = getTranslate(transformText, 'x')
expect(x).toBe(-1125)
})
it('should work by dispatching touch events', async () => {
await page.waitFor(300)
await page.dispatchScroll({
x: 100,
y: 120,
xDistance: -110,
yDistance: 0,
gestureSourceType: 'touch',
})
await page.waitFor(1500)
const transformText = await page.$eval('.slide-banner-content', (node) => {
return window.getComputedStyle(node).transform
})
const x = getTranslate(transformText, 'x')
expect(x).toBe(-375)
})
})
================================================
FILE: tests/e2e/slide/specifiedIndex.e2e.ts
================================================
import { Page } from 'puppeteer'
import extendTouch from '../../util/extendTouch'
import getTranslate from '../../util/getTranslate'
jest.setTimeout(10000000)
describe('Slider for specified index', () => {
let page = (global as any).page as Page
extendTouch(page)
beforeAll(async () => {
await page.goto('http://0.0.0.0:8932/#/slide/specified')
})
beforeEach(async () => {
await page.reload({
waitUntil: 'domcontentloaded',
})
})
it('should work well when initialised', async () => {
await page.waitFor(300)
const textContext = await page.$eval('.description', (node) => {
return node.textContent
})
expect(textContext).toBe('currentPageIndex is 2')
})
it('should go nextPage when click nextPage button', async () => {
await page.waitFor(300)
// simulate click
await page.click('.next')
// wait for bs to do a transition
await page.waitFor(1500)
const textContext = await page.$eval('.description', (node) => {
return node.textContent
})
expect(textContext).toBe('currentPageIndex is 3')
})
it('should go prevPage when click prevPage button', async () => {
await page.waitFor(300)
await page.click('.prev')
// wait for bs to do a transition
await page.waitFor(1500)
const textContext = await page.$eval('.description', (node) => {
return node.textContent
})
expect(textContext).toBe('currentPageIndex is 1')
})
it('should change index when drap slide', async () => {
await page.waitFor(300)
await page.dispatchScroll({
x: 200,
y: 120,
xDistance: -150,
yDistance: 0,
gestureSourceType: 'touch',
})
// wait for bs to do a transition
await page.waitFor(1500)
const textContext = await page.$eval('.description', (node) => {
return node.textContent
})
expect(textContext).toBe('currentPageIndex is 3')
})
})
================================================
FILE: tests/e2e/slide/vertical.e2e.ts
================================================
import { Page } from 'puppeteer'
import extendTouch from '../../util/extendTouch'
import getTranslate from '../../util/getTranslate'
jest.setTimeout(10000000)
describe('Slider for vertical', () => {
let page = (global as any).page as Page
extendTouch(page)
beforeAll(async () => {
await page.goto('http://0.0.0.0:8932/#/slide/vertical')
})
beforeEach(async () => {
await page.reload({
waitUntil: 'domcontentloaded'
})
})
it('should work by dispatching touch events', async () => {
await page.waitFor(1500)
await page.dispatchScroll({
x: 100,
y: 200,
xDistance: 0,
yDistance: -50,
gestureSourceType: 'touch'
})
await page.waitFor(1000)
const transformText = await page.$eval('.slide-vertical-content', node => {
return window.getComputedStyle(node).transform
})
const y = getTranslate(transformText, 'y')
expect(y).toBe(-1334)
})
})
================================================
FILE: tests/e2e/zoom/zoom.e2e.ts
================================================
import { Page } from 'puppeteer'
import extendTouch from '../../util/extendTouch'
import getScale from '../../util/getScale'
import getTranslate from '../../util/getTranslate'
jest.setTimeout(10000000)
describe('Zoom', () => {
let page = (global as any).page as Page
extendTouch(page)
beforeAll(async () => {
await page.goto('http://0.0.0.0:8932/#/zoom')
})
beforeEach(async () => {
await page.reload({
waitUntil: 'domcontentloaded'
})
})
it('should work when initializing to scaled > 1', async () => {
await page.waitFor(300)
const scaledElTransformText = await page.$eval('.zoom-items', node => {
return window.getComputedStyle(node).transform
})
const scale = getScale(scaledElTransformText)
expect(scale).toBe(1.5)
})
it('should work when initialOrigin is set to "center"', async () => {
await page.waitFor(300)
const scaledElTransformText = await page.$eval('.zoom-items', node => {
return window.getComputedStyle(node).transform
})
const scale = getScale(scaledElTransformText)
const x = getTranslate(scaledElTransformText, 'x')
const y = getTranslate(scaledElTransformText, 'x')
expect(scale).toBe(1.5)
expect(x).toBeLessThan(0)
expect(y).toBeLessThan(0)
})
it('should work by dispatching zooming out', async () => {
await page.waitFor(300)
// zoom out
await page.dispatchPinch({
x: 100,
y: 100,
scaleFactor: 1.1,
gestureSourceType: 'touch'
})
const scaledElTransformText = await page.$eval('.zoom-items', node => {
return window.getComputedStyle(node).transform
})
const scale = getScale(scaledElTransformText)
expect(scale).toBeGreaterThan(1.5)
})
it('should work by dispatching zooming in', async () => {
await page.waitFor(300)
// zoom in
await page.dispatchPinch({
x: 200,
y: 200,
scaleFactor: 0.5,
gestureSourceType: 'touch'
})
const scaledElTransformText = await page.$eval('.zoom-items', node => {
return window.getComputedStyle(node).transform
})
const scale = getScale(scaledElTransformText)
expect(scale).toBeLessThan(1.5)
})
it('should do a rebound animation when scale exceed "max", and recover to "max"', async () => {
await page.waitFor(300)
// zoom in
await page.dispatchPinch({
x: 200,
y: 200,
scaleFactor: 4,
gestureSourceType: 'touch'
})
// wait for rebound animation ends
await page.waitFor(1000)
const scaledElTransformText = await page.$eval('.zoom-items', node => {
return window.getComputedStyle(node).transform
})
const scale = getScale(scaledElTransformText)
expect(scale).toBe(3)
})
it('should support zoomTo api', async () => {
await page.waitFor(300)
// zoomTo scale(0.5)
await page.click('.zoom-half')
await page.waitFor(1000)
const scaledElTransformTextHalf = await page.$eval('.zoom-items', node => {
return window.getComputedStyle(node).transform
})
let scaleHalf = getScale(scaledElTransformTextHalf)
expect(scaleHalf).toBe(0.5)
// zoomTo scale(1)
await page.click('.zoom-original')
await page.waitFor(1000)
let scaledElTransformTextOriginal = await page.$eval(
'.zoom-items',
node => {
return window.getComputedStyle(node).transform
}
)
const scaleOriginal = getScale(scaledElTransformTextOriginal)
const xOriginal = getTranslate(scaledElTransformTextOriginal, 'x')
const yOriginal = getTranslate(scaledElTransformTextOriginal, 'y')
expect(scaleOriginal).toBe(1)
expect(xOriginal).toBe(0)
expect(yOriginal).toBe(0)
// zoomTo scale(2)
await page.click('.zoom-double')
await page.waitFor(1000)
let scaledElTransformTextDouble = await page.$eval('.zoom-items', node => {
return window.getComputedStyle(node).transform
})
const scaleDouble = getScale(scaledElTransformTextDouble)
const xDouble = getTranslate(scaledElTransformTextDouble, 'x')
const yDouble = getTranslate(scaledElTransformTextDouble, 'y')
expect(scaleDouble).toBe(2)
expect(xDouble).toBeLessThan(0)
expect(yDouble).toBeLessThan(0)
})
it('should allow moving with one finger when scaled out', async () => {
await page.waitFor(300)
// zoom out
await page.dispatchPinch({
x: 100,
y: 100,
scaleFactor: 1.2,
gestureSourceType: 'touch'
})
// touchmove
await page.dispatchScroll({
x: 100,
y: 120,
xDistance: 200,
yDistance: 0,
gestureSourceType: 'touch'
})
await page.waitFor(2500)
let transformText = await page.$eval('.zoom-items', node => {
return window.getComputedStyle(node).transform
})
const xDouble = getTranslate(transformText, 'x')
expect(xDouble).toEqual(0)
})
})
================================================
FILE: tests/util/extendMouseWheel.ts
================================================
import { Page } from 'puppeteer'
interface EventParams {
type: string
x: number
y: number
deltaX: number
deltaY: number
}
const DEFAULT_CHROMIUM_MOUSE_WHEEL_NAME = 'Input.dispatchMouseEvent'
declare module 'puppeteer' {
interface Mouse {
_client: {
send: (name: string, eventParams: EventParams) => Promise
}
}
interface Page {
dispatchMouseWheel: (eventParams: EventParams) => Promise
mouse: Mouse
}
}
// puppeteer 1.17.0 has no api to implement MouseWheel
// since puppeteer is connected to chromium with chromeDevTools
// https://chromedevtools.github.io/devtools-protocol/tot/Input/#method-dispatchMouseEvent
// so we can do it by ourselves
export default (page: Page) => {
page.dispatchMouseWheel = async (eventParams: EventParams) => {
await page.mouse._client.send(
DEFAULT_CHROMIUM_MOUSE_WHEEL_NAME,
eventParams
)
}
}
================================================
FILE: tests/util/extendTouch.ts
================================================
import { Page, Touchscreen } from 'puppeteer'
// https://chromedevtools.github.io/devtools-protocol/tot/Input#method-synthesizePinchGesture
interface PinchParams {
x: number
y: number
scaleFactor: number
gestureSourceType: 'touch' | 'default' | 'mouse'
}
// https://chromedevtools.github.io/devtools-protocol/tot/Input#method-synthesizeScrollGesture
interface ScrollParams {
x: number // X coordinate of the start of the gesture in CSS pixels.
y: number // Y coordinate of the start of the gesture in CSS pixels.
xDistance: number // positive to scroll left
yDistance: number // positive to scroll up
gestureSourceType: 'touch' | 'default' | 'mouse'
speed?: number // Swipe speed in pixels per second
xOverscroll?: number
yOverscroll?: number
preventFling?: boolean
repeatCount?: number
repeatDelayMs?: number
}
interface TouchPoint {
x: number
y: number
radiusX?: number
radiusY?: number
rotationAngle?: number
force?: number
tangentialPressure?: number
tiltX?: number
tiltY?: number
twist?: number
id?: number
}
interface TouchesParams {
type: 'touchStart' | 'touchEnd' | 'touchMove' | 'touchCancel'
touchPoints: TouchPoint[]
modifiers?: number // Alt=1, Ctrl=2, Meta/Command=4, Shift=8
timestamp?: number
}
const PINCH_NAME = 'Input.synthesizePinchGesture'
const SCROLL_NAME = 'Input.synthesizeScrollGesture'
const TOUCHES_NAME = 'Input.dispatchTouchEvent'
declare module 'puppeteer' {
interface Touchscreen {
_client: {
send: (
name: T,
params: PinchParams | ScrollParams | TouchesParams
) => Promise
}
}
interface Page {
dispatchPinch: (pinchParams: PinchParams) => Promise
dispatchScroll: (scrollParams: ScrollParams) => Promise
dispatchTouch: (touchesParams: TouchesParams) => Promise
touchsceen: Touchscreen
}
}
// puppeteer 1.17.0 has no api to implement touchmove
// since puppeteer is connected to chromium with chromeDevTools
// https://chromedevtools.github.io/devtools-protocol/tot/Input#method-dispatchTouchEvent
export default (page: Page) => {
page.dispatchPinch = async (pinchParams) => {
await page.touchscreen._client.send(PINCH_NAME, pinchParams)
}
page.dispatchScroll = async (scrollParams) => {
await page.touchscreen._client.send(SCROLL_NAME, scrollParams)
}
page.dispatchTouch = async (touchesParams) => {
await page.touchscreen._client.send(TOUCHES_NAME, touchesParams)
}
}
================================================
FILE: tests/util/getScale.ts
================================================
export default function getScale(transformText: string) {
const matrix = transformText.split(')')[0].split(', ')
const prefix = matrix[0]
return +prefix.split('(')[1]
}
================================================
FILE: tests/util/getTranslate.ts
================================================
export default function getTranslate(
transformText: string,
direction: 'x' | 'y'
) {
const matrix = transformText.split(')')[0].split(', ')
let ret =
direction === 'x' ? +(matrix[12] || matrix[4]) : +(matrix[13] || matrix[5])
return ret
}
================================================
FILE: tsconfig.json
================================================
{
"compilerOptions": {
"baseUrl": ".",
"rootDir": ".",
"outDir": "dist",
"moduleResolution": "node",
"paths": {
"@better-scroll/*": ["packages/*/src"]
},
"target": "es5",
"module": "es2015",
"lib": ["es2015", "es2016", "es2017", "dom"],
"esModuleInterop": true,
"strictPropertyInitialization": false,
"strict": true,
"preserveSymlinks": true,
"declaration": true,
"allowSyntheticDefaultImports": true,
"experimentalDecorators": true,
"emitDecoratorMetadata": false,
"typeRoots": ["node_modules/@types"]
},
"include": ["packages/*/src"],
"exclude": ["packages/*/src/**/__tests__", "packages/*/src/**/__mocks__"]
}
================================================
FILE: tslint.json
================================================
{
"extends": [
"tslint-config-standard",
"tslint-config-prettier"
],
"rules": {
"max-line-length": {
"options": [140]
},
"no-empty": false,
"deprecation": false,
"strict-type-predicates": false,
"no-angle-bracket-type-assertion": false,
"no-unnecessary-type-assertion": false,
"no-unused-expression": false,
"no-arg": true,
"no-bitwise": true,
"no-conditional-assignment": true,
"no-consecutive-blank-lines": false,
"no-console": false,
"await-promise": false
}
}