chore: 🚀 添加 jest 用于组件库工具方法和hooks的测试 (#971)

This commit is contained in:
不如摸鱼去 2025-03-25 16:49:01 +08:00 committed by GitHub
parent 4319fa9bad
commit 58157d8014
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 1556 additions and 272 deletions

1
.gitignore vendored
View File

@ -1,4 +1,5 @@
.DS_Store
coverage/
node_modules/
unpackage/
dist/

20
babel.config.js Normal file
View File

@ -0,0 +1,20 @@
module.exports = {
presets: [
['@babel/preset-env', {
targets: { node: 'current' },
modules: 'commonjs'
}],
'@babel/preset-typescript'
],
plugins: [
function () {
return {
visitor: {
MetaProperty(path) {
path.replaceWithSourceString('{ url: "file://test" }')
}
}
}
}
]
}

48
jest.config.js Normal file
View File

@ -0,0 +1,48 @@
/*
* @Author: weisheng
* @Date: 2025-03-24 12:33:15
* @LastEditTime: 2025-03-24 22:02:51
* @LastEditors: weisheng
* @Description:
* @FilePath: /wot-design-uni/jest.config.js
* 记得注释
*/
module.exports = {
testEnvironment: 'jsdom',
moduleFileExtensions: ['ts', 'js', 'json', 'vue'],
moduleNameMapper: {
'^@/(.*)$': '<rootDir>/src/$1',
'\\.(css|less|scss|sass)$': 'identity-obj-proxy'
},
transform: {
'^.+\\.ts$': 'ts-jest',
'^.+\\.js$': 'babel-jest',
'^.+\\.vue$': '@vue/vue3-jest'
},
testRegex: '(/tests/.*\\.spec)\\.(jsx?|tsx?)$', // 只匹配 .spec 结尾的文件
testPathIgnorePatterns: ['/node_modules/', '/tests/setup.js', '/tests/__mocks__/'],
globals: {
'vue-jest': {
compilerOptions: {
isCustomElement: (tag) => tag.startsWith('uni-'),
whitespace: 'condense',
delimiters: ['{{', '}}']
},
experimentalCoverage: true, // 添加实验性覆盖率支持
templateCompiler: {
compiler: require('@vue/compiler-dom'),
compilerOptions: {
hoistStatic: false,
prefixIdentifiers: true
}
}
},
'ts-jest': {
diagnostics: false,
isolatedModules: true,
babelConfig: true
}
},
transformIgnorePatterns: ['/node_modules/(?!(uni-app|@dcloudio)/)'],
setupFiles: ['<rootDir>/tests/setup.js']
}

View File

@ -58,7 +58,14 @@
"upload:mp-dingtalk": "uni build -p mp-dingtalk && minici --platform dd",
"build:qrcode": "esno ./scripts/qrcode.ts",
"build:changelog": "esno ./scripts/changelog.ts",
"build:theme-vars": "esno ./scripts/buildThemeVars.ts"
"build:theme-vars": "esno ./scripts/buildThemeVars.ts",
"test": "jest",
"test:watch": "jest --watch",
"test:h5": "cross-env UNI_PLATFORM=h5 jest -i",
"test:android": "cross-env UNI_PLATFORM=app UNI_OS_NAME=android jest -i",
"test:ios": "cross-env UNI_PLATFORM=app UNI_OS_NAME=ios jest -i",
"test:mp-weixin": "cross-env UNI_PLATFORM=mp-weixin jest -i",
"test:mp-baidu": "cross-env UNI_PLATFORM=mp-baidu jest -i"
},
"dependencies": {
"@dcloudio/uni-app": "3.0.0-4020420240722002",
@ -130,7 +137,19 @@
"vitepress": "^1.5.0",
"vitest": "^0.30.1",
"vue-eslint-parser": "^9.1.0",
"vue-tsc": "^2.0.29"
"vue-tsc": "^2.0.29",
"@babel/core": "^7.15.0",
"@babel/preset-env": "^7.15.0",
"@babel/preset-typescript": "^7.15.0",
"@types/jest": "^27.0.0",
"@vue/test-utils": "^2.0.0",
"babel-jest": "^27.0.4",
"jest": "27.0.4",
"jest-environment-node": "27.5.1",
"ts-jest": "27.0.4",
"cross-env": "^7.0.3",
"puppeteer": "14.0.0",
"@vue/vue3-jest": "27.0.0-alpha.1"
},
"config": {
"commitizen": {

1233
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,46 @@
import { useTouch } from '@/uni_modules/wot-design-uni/components/composables/useTouch'
describe('useTouch', () => {
const touch = useTouch()
it('should initialize with default values', () => {
expect(touch.direction.value).toBe('')
expect(touch.deltaX.value).toBe(0)
expect(touch.deltaY.value).toBe(0)
})
it('should handle touch start', () => {
touch.touchStart({
touches: [{ clientX: 100, clientY: 100 }]
})
expect(touch.startX.value).toBe(100)
expect(touch.startY.value).toBe(100)
})
it('should handle touch move horizontal', () => {
touch.touchStart({
touches: [{ clientX: 100, clientY: 100 }]
})
touch.touchMove({
touches: [{ clientX: 200, clientY: 120 }]
})
expect(touch.direction.value).toBe('horizontal')
expect(touch.deltaX.value).toBe(100)
})
it('should handle touch move vertical', () => {
touch.touchStart({
touches: [{ clientX: 100, clientY: 100 }]
})
touch.touchMove({
touches: [{ clientX: 120, clientY: 200 }]
})
expect(touch.direction.value).toBe('vertical')
expect(touch.deltaY.value).toBe(100)
})
})

View File

@ -0,0 +1,105 @@
import { useTranslate } from '@/uni_modules/wot-design-uni/components/composables/useTranslate'
import Locale from '@/uni_modules/wot-design-uni/locale'
import enUS from '@/uni_modules/wot-design-uni/locale/lang/en-US'
describe('useTranslate', () => {
beforeEach(() => {
Locale.use('zh-CN')
})
it('should translate basic messages in Chinese', () => {
const { translate: calendarTranslate } = useTranslate('calendar')
const { translate: pickerTranslate } = useTranslate('picker')
expect(calendarTranslate('placeholder')).toBe('请选择')
expect(pickerTranslate('cancel')).toBe('取消')
})
it('should switch to English translations', () => {
const { translate } = useTranslate('calendar')
expect(translate('confirm')).toBe('确定')
Locale.use('en-US', enUS)
expect(translate('confirm')).toBe('OK')
})
it('should handle function messages with parameters', () => {
const { translate } = useTranslate('calendarView')
expect(translate('rangePrompt', 7)).toBe('选择天数不能超过7天')
expect(translate('hour', 10)).toBe('10时')
expect(translate('minute', 30)).toBe('30分')
expect(translate('second', 45)).toBe('45秒')
Locale.use('en-US', enUS)
expect(translate('rangePrompt', 7)).toBe('The number of selected days cannot exceed 7 days')
expect(translate('hour', 10)).toBe('10')
expect(translate('minute', 30)).toBe('30')
expect(translate('second', 45)).toBe('45')
})
it('should handle nested object translations', () => {
const { translate } = useTranslate('calendarView')
expect(translate('weeks.mon')).toBe('一')
expect(translate('weeks.sun')).toBe('日')
Locale.use('en-US', enUS)
expect(translate('weeks.mon')).toBe('Mon')
expect(translate('weeks.sun')).toBe('Sun')
})
it('should handle pagination translations', () => {
const { translate } = useTranslate('pagination')
expect(translate('prev')).toBe('上一页')
expect(translate('next')).toBe('下一页')
expect(translate('total', 100)).toBe('当前数据100条')
Locale.use('en-US', enUS)
expect(translate('prev')).toBe('Previous')
expect(translate('next')).toBe('Next')
expect(translate('total', 100)).toBe('Total: 100')
})
it('should handle date formats', () => {
const { translate } = useTranslate('calendar')
expect(translate('timeFormat')).toBe('YY年MM月DD日 HH:mm:ss')
expect(translate('dateFormat')).toBe('YYYY年MM月DD日')
expect(translate('weekFormat', 2023, 1)).toBe('2023 第 1 周')
Locale.use('en-US', enUS)
expect(translate('timeFormat')).toBe('YY-MM-DD HH:mm:ss')
expect(translate('dateFormat')).toBe('YYYY-MM-DD')
expect(translate('weekFormat', 2023, 1)).toBe('2023 W1')
})
it('should handle form related translations', () => {
const { translate } = useTranslate('messageBox')
expect(translate('confirm')).toBe('确定')
expect(translate('cancel')).toBe('取消')
expect(translate('inputPlaceholder')).toBe('请输入')
Locale.use('en-US', enUS)
expect(translate('confirm')).toBe('OK')
expect(translate('cancel')).toBe('Cancel')
expect(translate('inputPlaceholder')).toBe('Please input information')
})
it('should fallback to key path when translation missing', () => {
const { translate } = useTranslate('nonexistent')
expect(translate('key')).toBe('nonexistent.key')
})
it('should handle component specific messages', () => {
const { translate: uploadTranslate } = useTranslate('upload')
const { translate: searchTranslate } = useTranslate('search')
const { translate: signatureTranslate } = useTranslate('signature')
expect(uploadTranslate('error')).toBe('上传失败')
expect(searchTranslate('search')).toBe('搜索')
expect(signatureTranslate('confirmText')).toBe('确认')
Locale.use('en-US', enUS)
expect(uploadTranslate('error')).toBe('Failed to upload')
expect(searchTranslate('search')).toBe('Search')
expect(signatureTranslate('confirmText')).toBe('OK')
})
})

View File

@ -0,0 +1,287 @@
import { useUpload } from '@/uni_modules/wot-design-uni/components/composables/useUpload'
import type { UploadFileItem } from '@/uni_modules/wot-design-uni/components/wd-upload/types'
// Mock uni API
const mockUploadTask = {
abort: jest.fn(),
onProgressUpdate: jest.fn((callback) => {
callback({ progress: 50 })
})
}
;(global as any).uni = {
uploadFile: jest.fn((options) => {
const { success, fail } = options
if (options.url.includes('success')) {
success?.({ statusCode: 200, data: 'success' })
} else {
fail?.({ errMsg: 'upload failed' })
}
return mockUploadTask
})
} as any
describe('useUpload', () => {
const { startUpload, abort, UPLOAD_STATUS } = useUpload()
beforeEach(() => {
jest.clearAllMocks()
})
// 测试基本上传功能
it('should upload file successfully', async () => {
const file: UploadFileItem = {
url: 'file://temp/image.png',
status: UPLOAD_STATUS.PENDING,
percent: 0,
uid: 1
}
const onSuccess = jest.fn()
const onProgress = jest.fn()
await startUpload(file, {
action: 'https://api.example.com/success',
name: 'file',
onSuccess,
onProgress
})
expect(file.status).toBe(UPLOAD_STATUS.SUCCESS)
expect(onSuccess).toHaveBeenCalled()
expect(onProgress).toHaveBeenCalledWith({ progress: 50 }, file)
})
// 测试上传失败场景
it('should handle upload failure', async () => {
const file: UploadFileItem = {
url: 'file://temp/image.png',
status: UPLOAD_STATUS.PENDING,
percent: 0,
uid: 1
}
const onError = jest.fn()
await startUpload(file, {
action: 'https://api.example.com/fail',
name: 'file',
onError
})
expect(file.status).toBe(UPLOAD_STATUS.FAIL)
expect(file.error).toBe('upload failed')
expect(onError).toHaveBeenCalled()
})
// 测试自定义上传方法
it('should use custom upload method', async () => {
const file: UploadFileItem = {
url: 'file://temp/image.png',
status: UPLOAD_STATUS.PENDING,
percent: 0,
uid: 1
}
const customUpload = jest.fn()
await startUpload(file, {
action: 'https://api.example.com',
uploadMethod: customUpload
})
expect(customUpload).toHaveBeenCalledWith(
file,
{},
expect.objectContaining({
action: 'https://api.example.com'
})
)
})
// 测试中断上传
it('should abort upload task', () => {
const file: UploadFileItem = {
url: 'file://temp/image.png',
status: UPLOAD_STATUS.PENDING,
percent: 0,
uid: 1
}
startUpload(file, {
action: 'https://api.example.com/success',
abortPrevious: true
})
abort()
expect(mockUploadTask.abort).toHaveBeenCalled()
})
// 测试自动中断之前的上传任务
it('should abort previous upload when abortPrevious is true', async () => {
const file1: UploadFileItem = {
url: 'file://temp/image1.png',
status: UPLOAD_STATUS.PENDING,
percent: 0,
uid: 1
}
const file2: UploadFileItem = {
url: 'file://temp/image2.png',
status: UPLOAD_STATUS.PENDING,
percent: 0,
uid: 1
}
await startUpload(file1, {
action: 'https://api.example.com/success',
abortPrevious: true
})
await startUpload(file2, {
action: 'https://api.example.com/success',
abortPrevious: true
})
expect(mockUploadTask.abort).toHaveBeenCalledTimes(1)
})
// 测试请求头和表单数据的传递
it('should pass custom headers and formData', () => {
const file: UploadFileItem = {
url: 'file://temp/image.png',
status: UPLOAD_STATUS.PENDING,
percent: 0,
uid: 1
}
const customHeaders = { 'X-Custom-Header': 'test' }
const customFormData = { field: 'value' }
startUpload(file, {
action: 'https://api.example.com/success',
header: customHeaders,
formData: customFormData
})
expect(uni.uploadFile).toHaveBeenCalledWith(
expect.objectContaining({
header: customHeaders,
formData: customFormData
})
)
})
// 测试特定文件类型上传
it('should handle specific file types', () => {
const file: UploadFileItem = {
url: 'file://temp/video.mp4',
status: UPLOAD_STATUS.PENDING,
percent: 0,
uid: 1
}
startUpload(file, {
action: 'https://api.example.com/success',
fileType: 'video'
})
expect(uni.uploadFile).toHaveBeenCalledWith(
expect.objectContaining({
fileType: 'video'
})
)
})
// 测试自定义状态码
it('should handle custom status code', async () => {
const file: UploadFileItem = {
url: 'file://temp/image.png',
status: UPLOAD_STATUS.PENDING,
percent: 0,
uid: 1
}
const onSuccess = jest.fn()
const onError = jest.fn()
await startUpload(file, {
action: 'https://api.example.com/success',
statusCode: 201,
onSuccess,
onError
})
// 由于mock返回200使用201作为成功状态码应该触发错误回调
expect(onError).toHaveBeenCalled()
expect(onSuccess).not.toHaveBeenCalled()
})
// 测试自定义状态字段
it('should use custom status key', async () => {
const file: UploadFileItem = {
url: 'file://temp/image.png',
customStatus: UPLOAD_STATUS.PENDING,
percent: 0,
uid: 1
}
await startUpload(file, {
action: 'https://api.example.com/success',
statusKey: 'customStatus'
})
expect(file.customStatus).toBe(UPLOAD_STATUS.SUCCESS)
})
// 测试具体任务的中断
it('should abort specific upload task', () => {
const file: UploadFileItem = {
url: 'file://temp/image.png',
status: UPLOAD_STATUS.PENDING,
percent: 0,
uid: 1
}
const task = startUpload(file, {
action: 'https://api.example.com/success'
})
abort(task as UniApp.UploadTask)
expect(mockUploadTask.abort).toHaveBeenCalled()
})
// 测试进度回调的详细信息
it('should provide detailed progress information', () => {
const file: UploadFileItem = {
url: 'file://temp/image.png',
status: UPLOAD_STATUS.PENDING,
percent: 0,
uid: 1
}
const onProgress = jest.fn()
mockUploadTask.onProgressUpdate = jest.fn((callback) => {
callback({
progress: 50,
totalBytesSent: 5000,
totalBytesExpectedToSend: 10000
})
})
startUpload(file, {
action: 'https://api.example.com/success',
onProgress
})
expect(onProgress).toHaveBeenCalledWith(
expect.objectContaining({
progress: 50,
totalBytesSent: 5000,
totalBytesExpectedToSend: 10000
}),
file
)
})
})

57
tests/setup.js Normal file
View File

@ -0,0 +1,57 @@
/*
* @Author: weisheng
* @Date: 2025-03-24 13:52:28
* @LastEditTime: 2025-03-24 14:05:48
* @LastEditors: weisheng
* @Description:
* @FilePath: /wot-design-uni/tests/setup.js
* 记得注释
*/
import { config } from '@vue/test-utils'
import { defineComponent } from 'vue'
// 创建通用的 uni 组件 mock
const createUniBehavior = () => {
return defineComponent({
template: '<div class="uni-mock"><slot /></div>',
props: ['modelValue'],
emits: ['update:modelValue', 'input', 'change']
})
}
// 配置全局组件
config.global.components = {
'uni-table': createUniBehavior(),
'uni-tr': createUniBehavior(),
'uni-td': createUniBehavior()
}
// 配置 Vue Test Utils
config.global.mocks = {
$t: (key) => key
// 添加其他全局属性
}
// Mock uni-app global API
global.uni = {
uploadFile: jest.fn(),
showToast: jest.fn(),
showModal: jest.fn()
// 添加其他需要的 uni API
}
// 添加 Vue 编译器配置
global.__VUE_OPTIONS_API__ = true
global.__VUE_PROD_DEVTOOLS__ = false
// Mock import.meta
global.import = {
meta: {
glob: () => ({})
}
}
// Mock window
global.window = {
// Add any window properties needed
}

View File

@ -1,7 +1,7 @@
{
"extends": "@vue/tsconfig/tsconfig.json",
"compilerOptions": {
"verbatimModuleSyntax":true,
"verbatimModuleSyntax": false,
"ignoreDeprecations": "5.0",
"lib": [
"ESNext",
@ -19,9 +19,11 @@
"@dcloudio/types",
"mini-types",
"@uni-helper/uni-types",
"miniprogram-api-typings"
"miniprogram-api-typings",
"jest"
],
"sourceMap": true
"sourceMap": true,
"module": "ESNext"
},
"vueCompilerOptions": {
"plugins": [