committed by
GitHub
18 changed files with 530 additions and 42 deletions
@ -0,0 +1,114 @@ |
|||||
|
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html |
||||
|
|
||||
|
exports[`defaultPreferences immutability test > should not modify the config object 1`] = ` |
||||
|
{ |
||||
|
"app": { |
||||
|
"accessMode": "frontend", |
||||
|
"authPageLayout": "panel-right", |
||||
|
"checkUpdatesInterval": 1, |
||||
|
"colorGrayMode": false, |
||||
|
"colorWeakMode": false, |
||||
|
"compact": false, |
||||
|
"contentCompact": "wide", |
||||
|
"defaultAvatar": "https://unpkg.com/@vbenjs/static-source@0.1.6/source/avatar-v1.webp", |
||||
|
"dynamicTitle": true, |
||||
|
"enableCheckUpdates": true, |
||||
|
"enablePreferences": true, |
||||
|
"enableRefreshToken": false, |
||||
|
"isMobile": false, |
||||
|
"layout": "sidebar-nav", |
||||
|
"locale": "zh-CN", |
||||
|
"loginExpiredMode": "page", |
||||
|
"name": "Vben Admin", |
||||
|
"preferencesButtonPosition": "auto", |
||||
|
"watermark": false, |
||||
|
}, |
||||
|
"breadcrumb": { |
||||
|
"enable": true, |
||||
|
"hideOnlyOne": false, |
||||
|
"showHome": false, |
||||
|
"showIcon": true, |
||||
|
"styleType": "normal", |
||||
|
}, |
||||
|
"copyright": { |
||||
|
"companyName": "Vben", |
||||
|
"companySiteLink": "https://www.vben.pro", |
||||
|
"date": "2024", |
||||
|
"enable": true, |
||||
|
"icp": "", |
||||
|
"icpLink": "", |
||||
|
}, |
||||
|
"footer": { |
||||
|
"enable": true, |
||||
|
"fixed": false, |
||||
|
}, |
||||
|
"header": { |
||||
|
"enable": true, |
||||
|
"hidden": false, |
||||
|
"mode": "fixed", |
||||
|
}, |
||||
|
"logo": { |
||||
|
"enable": true, |
||||
|
"source": "https://unpkg.com/@vbenjs/static-source@0.1.6/source/logo-v1.webp", |
||||
|
}, |
||||
|
"navigation": { |
||||
|
"accordion": true, |
||||
|
"split": true, |
||||
|
"styleType": "rounded", |
||||
|
}, |
||||
|
"shortcutKeys": { |
||||
|
"enable": true, |
||||
|
"globalLockScreen": true, |
||||
|
"globalLogout": true, |
||||
|
"globalPreferences": true, |
||||
|
"globalSearch": true, |
||||
|
}, |
||||
|
"sidebar": { |
||||
|
"collapsed": false, |
||||
|
"collapsedShowTitle": false, |
||||
|
"enable": true, |
||||
|
"expandOnHover": true, |
||||
|
"extraCollapse": true, |
||||
|
"hidden": false, |
||||
|
"width": 224, |
||||
|
}, |
||||
|
"tabbar": { |
||||
|
"dragable": true, |
||||
|
"enable": true, |
||||
|
"height": 38, |
||||
|
"keepAlive": true, |
||||
|
"persist": true, |
||||
|
"showIcon": true, |
||||
|
"showMaximize": true, |
||||
|
"showMore": true, |
||||
|
"showRefresh": true, |
||||
|
"styleType": "chrome", |
||||
|
}, |
||||
|
"theme": { |
||||
|
"builtinType": "default", |
||||
|
"colorDestructive": "hsl(348 100% 61%)", |
||||
|
"colorPrimary": "hsl(212 100% 45%)", |
||||
|
"colorSuccess": "hsl(144 57% 58%)", |
||||
|
"colorWarning": "hsl(42 84% 61%)", |
||||
|
"mode": "dark", |
||||
|
"radius": "0.5", |
||||
|
"semiDarkHeader": false, |
||||
|
"semiDarkSidebar": true, |
||||
|
}, |
||||
|
"transition": { |
||||
|
"enable": true, |
||||
|
"loading": true, |
||||
|
"name": "fade-slide", |
||||
|
"progress": true, |
||||
|
}, |
||||
|
"widget": { |
||||
|
"fullscreen": true, |
||||
|
"globalSearch": true, |
||||
|
"languageToggle": true, |
||||
|
"lockScreen": true, |
||||
|
"notification": true, |
||||
|
"sidebarToggle": true, |
||||
|
"themeToggle": true, |
||||
|
}, |
||||
|
} |
||||
|
`; |
||||
@ -0,0 +1,10 @@ |
|||||
|
import { describe, expect, it } from 'vitest'; |
||||
|
|
||||
|
import { defaultPreferences } from '../src/config'; |
||||
|
|
||||
|
describe('defaultPreferences immutability test', () => { |
||||
|
// 创建快照,确保默认配置对象不被修改
|
||||
|
it('should not modify the config object', () => { |
||||
|
expect(defaultPreferences).toMatchSnapshot(); |
||||
|
}); |
||||
|
}); |
||||
@ -1,8 +1,8 @@ |
|||||
import { beforeEach, describe, expect, it, vi } from 'vitest'; |
import { beforeEach, describe, expect, it, vi } from 'vitest'; |
||||
|
|
||||
import { defaultPreferences } from './config'; |
import { defaultPreferences } from '../src/config'; |
||||
import { PreferenceManager } from './preferences'; |
import { PreferenceManager } from '../src/preferences'; |
||||
import { isDarkTheme } from './update-css-variables'; |
import { isDarkTheme } from '../src/update-css-variables'; |
||||
|
|
||||
describe('preferences', () => { |
describe('preferences', () => { |
||||
let preferenceManager: PreferenceManager; |
let preferenceManager: PreferenceManager; |
||||
@ -1,6 +1,6 @@ |
|||||
{ |
{ |
||||
"$schema": "https://json.schemastore.org/tsconfig", |
"$schema": "https://json.schemastore.org/tsconfig", |
||||
"extends": "@vben/tsconfig/web.json", |
"extends": "@vben/tsconfig/web.json", |
||||
"include": ["src"], |
"include": ["src", "__tests__"], |
||||
"exclude": ["node_modules"] |
"exclude": ["node_modules"] |
||||
} |
} |
||||
|
|||||
@ -0,0 +1,146 @@ |
|||||
|
// 假设这个文件为 FormApi.ts
|
||||
|
import { beforeEach, describe, expect, it, vi } from 'vitest'; |
||||
|
|
||||
|
import { FormApi } from '../src/form-api'; |
||||
|
|
||||
|
vi.mock('@vben-core/shared/utils', () => ({ |
||||
|
bindMethods: vi.fn(), |
||||
|
createMerge: vi.fn((mergeFn) => { |
||||
|
return (stateOrFn, prev) => { |
||||
|
mergeFn(prev, 'key', stateOrFn); |
||||
|
return { ...prev, ...stateOrFn }; |
||||
|
}; |
||||
|
}), |
||||
|
isFunction: (fn: any) => typeof fn === 'function', |
||||
|
StateHandler: vi.fn().mockImplementation(() => ({ |
||||
|
reset: vi.fn(), |
||||
|
setConditionTrue: vi.fn(), |
||||
|
waitForCondition: vi.fn().mockResolvedValue(true), |
||||
|
})), |
||||
|
})); |
||||
|
|
||||
|
describe('formApi', () => { |
||||
|
let formApi: FormApi; |
||||
|
|
||||
|
beforeEach(() => { |
||||
|
formApi = new FormApi(); |
||||
|
}); |
||||
|
|
||||
|
it('should initialize with default state', () => { |
||||
|
expect(formApi.state).toEqual( |
||||
|
expect.objectContaining({ |
||||
|
actionWrapperClass: '', |
||||
|
collapsed: false, |
||||
|
collapsedRows: 1, |
||||
|
commonConfig: {}, |
||||
|
handleReset: undefined, |
||||
|
handleSubmit: undefined, |
||||
|
layout: 'horizontal', |
||||
|
resetButtonOptions: {}, |
||||
|
schema: [], |
||||
|
showCollapseButton: false, |
||||
|
showDefaultActions: true, |
||||
|
submitButtonOptions: {}, |
||||
|
wrapperClass: 'grid-cols-1', |
||||
|
}), |
||||
|
); |
||||
|
expect(formApi.isMounted).toBe(false); |
||||
|
}); |
||||
|
|
||||
|
it('should mount form actions', async () => { |
||||
|
const formActions: any = { |
||||
|
meta: {}, |
||||
|
resetForm: vi.fn(), |
||||
|
setFieldValue: vi.fn(), |
||||
|
setValues: vi.fn(), |
||||
|
submitForm: vi.fn(), |
||||
|
validate: vi.fn(), |
||||
|
values: { name: 'test' }, |
||||
|
}; |
||||
|
|
||||
|
await formApi.mount(formActions); |
||||
|
expect(formApi.isMounted).toBe(true); |
||||
|
expect(formApi.form).toEqual(formActions); |
||||
|
}); |
||||
|
|
||||
|
it('should get values from form', async () => { |
||||
|
const formActions: any = { |
||||
|
meta: {}, |
||||
|
values: { name: 'test' }, |
||||
|
}; |
||||
|
|
||||
|
await formApi.mount(formActions); |
||||
|
const values = await formApi.getValues(); |
||||
|
expect(values).toEqual({ name: 'test' }); |
||||
|
}); |
||||
|
|
||||
|
it('should set field value', async () => { |
||||
|
const setFieldValueMock = vi.fn(); |
||||
|
const formActions: any = { |
||||
|
meta: {}, |
||||
|
setFieldValue: setFieldValueMock, |
||||
|
values: { name: 'test' }, |
||||
|
}; |
||||
|
|
||||
|
await formApi.mount(formActions); |
||||
|
await formApi.setFieldValue('name', 'new value'); |
||||
|
expect(setFieldValueMock).toHaveBeenCalledWith( |
||||
|
'name', |
||||
|
'new value', |
||||
|
undefined, |
||||
|
); |
||||
|
}); |
||||
|
|
||||
|
it('should reset form', async () => { |
||||
|
const resetFormMock = vi.fn(); |
||||
|
const formActions: any = { |
||||
|
meta: {}, |
||||
|
resetForm: resetFormMock, |
||||
|
values: { name: 'test' }, |
||||
|
}; |
||||
|
|
||||
|
await formApi.mount(formActions); |
||||
|
await formApi.resetForm(); |
||||
|
expect(resetFormMock).toHaveBeenCalled(); |
||||
|
}); |
||||
|
|
||||
|
it('should call handleSubmit on submit', async () => { |
||||
|
const handleSubmitMock = vi.fn(); |
||||
|
const formActions: any = { |
||||
|
meta: {}, |
||||
|
submitForm: vi.fn().mockResolvedValue(true), |
||||
|
values: { name: 'test' }, |
||||
|
}; |
||||
|
|
||||
|
const state = { |
||||
|
handleSubmit: handleSubmitMock, |
||||
|
}; |
||||
|
|
||||
|
formApi.setState(state); |
||||
|
await formApi.mount(formActions); |
||||
|
|
||||
|
const result = await formApi.submitForm(); |
||||
|
expect(formActions.submitForm).toHaveBeenCalled(); |
||||
|
expect(handleSubmitMock).toHaveBeenCalledWith({ name: 'test' }); |
||||
|
expect(result).toEqual({ name: 'test' }); |
||||
|
}); |
||||
|
|
||||
|
it('should unmount form and reset state', () => { |
||||
|
formApi.unmounted(); |
||||
|
expect(formApi.isMounted).toBe(false); |
||||
|
expect(formApi.stateHandler.reset).toHaveBeenCalled(); |
||||
|
}); |
||||
|
|
||||
|
it('should validate form', async () => { |
||||
|
const validateMock = vi.fn().mockResolvedValue(true); |
||||
|
const formActions: any = { |
||||
|
meta: {}, |
||||
|
validate: validateMock, |
||||
|
}; |
||||
|
|
||||
|
await formApi.mount(formActions); |
||||
|
const isValid = await formApi.validate(); |
||||
|
expect(validateMock).toHaveBeenCalled(); |
||||
|
expect(isValid).toBe(true); |
||||
|
}); |
||||
|
}); |
||||
@ -1,6 +1,6 @@ |
|||||
{ |
{ |
||||
"$schema": "https://json.schemastore.org/tsconfig", |
"$schema": "https://json.schemastore.org/tsconfig", |
||||
"extends": "@vben/tsconfig/web.json", |
"extends": "@vben/tsconfig/web.json", |
||||
"include": ["src"], |
"include": ["src", "__tests__"], |
||||
"exclude": ["node_modules"] |
"exclude": ["node_modules"] |
||||
} |
} |
||||
|
|||||
@ -0,0 +1,208 @@ |
|||||
|
<script lang="ts" setup> |
||||
|
import { Page } from '@vben/common-ui'; |
||||
|
|
||||
|
import { Button, Card, message, Space } from 'ant-design-vue'; |
||||
|
|
||||
|
import { useVbenForm } from '#/adapter'; |
||||
|
|
||||
|
const [BaseForm, formApi] = useVbenForm({ |
||||
|
// 所有表单项共用,可单独在表单内覆盖 |
||||
|
commonConfig: { |
||||
|
// 所有表单项 |
||||
|
componentProps: { |
||||
|
class: 'w-full', |
||||
|
}, |
||||
|
}, |
||||
|
// 使用 tailwindcss grid布局 |
||||
|
// 提交函数 |
||||
|
handleSubmit: onSubmit, |
||||
|
// 垂直布局,label和input在不同行,值为vertical |
||||
|
layout: 'horizontal', |
||||
|
// 水平布局,label和input在同一行 |
||||
|
schema: [ |
||||
|
{ |
||||
|
// 组件需要在 #/adapter.ts内注册,并加上类型 |
||||
|
component: 'Input', |
||||
|
// 对应组件的参数 |
||||
|
componentProps: { |
||||
|
placeholder: '请输入用户名', |
||||
|
}, |
||||
|
// 字段名 |
||||
|
fieldName: 'field1', |
||||
|
// 界面显示的label |
||||
|
label: 'field1', |
||||
|
}, |
||||
|
{ |
||||
|
component: 'Input', |
||||
|
componentProps: { |
||||
|
placeholder: '请输入', |
||||
|
}, |
||||
|
fieldName: 'field2', |
||||
|
label: 'field2', |
||||
|
}, |
||||
|
{ |
||||
|
component: 'Input', |
||||
|
componentProps: { |
||||
|
placeholder: '请输入', |
||||
|
}, |
||||
|
fieldName: 'field3', |
||||
|
label: 'field3', |
||||
|
}, |
||||
|
], |
||||
|
// 大屏一行显示3个,中屏一行显示2个,小屏一行显示1个 |
||||
|
wrapperClass: 'grid-cols-1 md:grid-cols-2 lg:grid-cols-3', |
||||
|
}); |
||||
|
|
||||
|
function onSubmit(values: Record<string, any>) { |
||||
|
message.success({ |
||||
|
content: `form values: ${JSON.stringify(values)}`, |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
function handleClick( |
||||
|
action: |
||||
|
| 'batchAddSchema' |
||||
|
| 'batchDeleteSchema' |
||||
|
| 'disabled' |
||||
|
| 'hiddenAction' |
||||
|
| 'hiddenResetButton' |
||||
|
| 'hiddenSubmitButton' |
||||
|
| 'labelWidth' |
||||
|
| 'resetDisabled' |
||||
|
| 'resetLabelWidth' |
||||
|
| 'showAction' |
||||
|
| 'showResetButton' |
||||
|
| 'showSubmitButton' |
||||
|
| 'updateActionAlign' |
||||
|
| 'updateResetButton' |
||||
|
| 'updateSubmitButton', |
||||
|
) { |
||||
|
switch (action) { |
||||
|
case 'labelWidth': { |
||||
|
formApi.setState({ |
||||
|
commonConfig: { |
||||
|
labelWidth: 150, |
||||
|
}, |
||||
|
}); |
||||
|
break; |
||||
|
} |
||||
|
case 'resetLabelWidth': { |
||||
|
formApi.setState({ |
||||
|
commonConfig: { |
||||
|
labelWidth: 100, |
||||
|
}, |
||||
|
}); |
||||
|
break; |
||||
|
} |
||||
|
case 'disabled': { |
||||
|
formApi.setState({ commonConfig: { disabled: true } }); |
||||
|
break; |
||||
|
} |
||||
|
case 'resetDisabled': { |
||||
|
formApi.setState({ commonConfig: { disabled: false } }); |
||||
|
break; |
||||
|
} |
||||
|
case 'hiddenAction': { |
||||
|
formApi.setState({ showDefaultActions: false }); |
||||
|
break; |
||||
|
} |
||||
|
case 'showAction': { |
||||
|
formApi.setState({ showDefaultActions: true }); |
||||
|
break; |
||||
|
} |
||||
|
case 'hiddenResetButton': { |
||||
|
formApi.setState({ resetButtonOptions: { show: false } }); |
||||
|
break; |
||||
|
} |
||||
|
case 'showResetButton': { |
||||
|
formApi.setState({ resetButtonOptions: { show: true } }); |
||||
|
break; |
||||
|
} |
||||
|
case 'hiddenSubmitButton': { |
||||
|
formApi.setState({ submitButtonOptions: { show: false } }); |
||||
|
break; |
||||
|
} |
||||
|
case 'showSubmitButton': { |
||||
|
formApi.setState({ submitButtonOptions: { show: true } }); |
||||
|
break; |
||||
|
} |
||||
|
case 'updateResetButton': { |
||||
|
formApi.setState({ |
||||
|
resetButtonOptions: { disabled: true }, |
||||
|
}); |
||||
|
break; |
||||
|
} |
||||
|
case 'updateSubmitButton': { |
||||
|
formApi.setState({ |
||||
|
submitButtonOptions: { loading: true }, |
||||
|
}); |
||||
|
break; |
||||
|
} |
||||
|
case 'updateActionAlign': { |
||||
|
formApi.setState({ |
||||
|
// 可以自行调整class |
||||
|
actionWrapperClass: 'text-center', |
||||
|
}); |
||||
|
break; |
||||
|
} |
||||
|
case 'batchAddSchema': { |
||||
|
formApi.setState((prev) => { |
||||
|
const currentSchema = prev?.schema ?? []; |
||||
|
const newSchema = []; |
||||
|
for (let i = 0; i < 3; i++) { |
||||
|
newSchema.push({ |
||||
|
component: 'Input', |
||||
|
componentProps: { |
||||
|
placeholder: '请输入', |
||||
|
}, |
||||
|
fieldName: `field${i}${Date.now()}`, |
||||
|
label: `field+`, |
||||
|
}); |
||||
|
} |
||||
|
return { |
||||
|
schema: [...currentSchema, ...newSchema], |
||||
|
}; |
||||
|
}); |
||||
|
break; |
||||
|
} |
||||
|
case 'batchDeleteSchema': { |
||||
|
formApi.setState((prev) => { |
||||
|
const currentSchema = prev?.schema ?? []; |
||||
|
return { |
||||
|
schema: currentSchema.slice(0, -3), |
||||
|
}; |
||||
|
}); |
||||
|
break; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
</script> |
||||
|
|
||||
|
<template> |
||||
|
<Page description="表单组件api操作示例。" title="表单组件"> |
||||
|
<Space class="mb-5 flex-wrap"> |
||||
|
<Button @click="handleClick('labelWidth')">更改labelWidth</Button> |
||||
|
<Button @click="handleClick('resetLabelWidth')">还原labelWidth</Button> |
||||
|
<Button @click="handleClick('disabled')">禁用表单</Button> |
||||
|
<Button @click="handleClick('resetDisabled')">解除禁用</Button> |
||||
|
<Button @click="handleClick('hiddenAction')">隐藏操作按钮</Button> |
||||
|
<Button @click="handleClick('showAction')">显示操作按钮</Button> |
||||
|
<Button @click="handleClick('hiddenResetButton')">隐藏重置按钮</Button> |
||||
|
<Button @click="handleClick('showResetButton')">显示重置按钮</Button> |
||||
|
<Button @click="handleClick('hiddenSubmitButton')">隐藏提交按钮</Button> |
||||
|
<Button @click="handleClick('showSubmitButton')">显示提交按钮</Button> |
||||
|
<Button @click="handleClick('updateResetButton')">修改重置按钮</Button> |
||||
|
<Button @click="handleClick('updateSubmitButton')">修改提交按钮</Button> |
||||
|
<Button @click="handleClick('updateActionAlign')"> |
||||
|
调整操作按钮位置 |
||||
|
</Button> |
||||
|
<Button @click="handleClick('batchAddSchema')"> 批量添加表单项 </Button> |
||||
|
<Button @click="handleClick('batchDeleteSchema')"> |
||||
|
批量删除表单项 |
||||
|
</Button> |
||||
|
</Space> |
||||
|
<Card title="操作示例"> |
||||
|
<BaseForm /> |
||||
|
</Card> |
||||
|
</Page> |
||||
|
</template> |
||||
Loading…
Reference in new issue