test: switch to vitest (#5991)

* test: use vitest

* test: add script and ci

* chore: improve tsconfig

* refactor: use-form-item

* fix: remove unused

* chore: improve scripts

* test: improve mock

* refactor: change coverage
This commit is contained in:
三咲智子 2022-02-21 14:28:22 +08:00 committed by GitHub
parent 0b4acfbabb
commit aaf90d99d0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
30 changed files with 510 additions and 351 deletions

View File

@ -32,7 +32,7 @@ jobs:
- name: Lint
run: pnpm lint
- name: Test
run: pnpm test -- --coverage
run: pnpm test:coverage
- name: Upload to codecov.io
uses: codecov/codecov-action@v2
with:

View File

@ -40,8 +40,8 @@ pnpm install
Then you can following command to verify your installation.
```shell
pnpm test
pnpm run format
pnpm t
pnpm format
```
## Getting Started developing

View File

@ -3,6 +3,11 @@
*/
module.exports = {
setupFiles: ['./jest.setup.js'],
testMatch: [
'**/__tests__/**/*.[jt]s?(x)',
'**/?(*.)+(spec|test).[jt]s?(x)',
'!**/**vitest**',
],
testPathIgnorePatterns: ['/node_modules/', 'dist'],
modulePathIgnorePatterns: ['/node_modules/', 'dist', 'cypress'],
testEnvironment: 'jsdom',

View File

@ -11,7 +11,10 @@
},
"scripts": {
"cz": "git-cz",
"test": "jest",
"test": "pnpm test:jest && pnpm test:vitest",
"test:jest": "jest",
"test:vitest": "vitest",
"test:coverage": "vitest --coverage",
"prepare:e2e": "if [ ! -d \"docs/.vitepress/dist\" ]; then pnpm run docs:build; fi;",
"e2e": "cypress open",
"e2e:ci": "cypress run",
@ -96,10 +99,12 @@
"@typescript-eslint/parser": "5.12.0",
"@vitejs/plugin-vue": "2.2.2",
"@vitejs/plugin-vue-jsx": "1.3.7",
"@vitest/ui": "^0.3.2",
"@vue/babel-plugin-jsx": "1.1.1",
"@vue/test-utils": "2.0.0-rc.16",
"@vue/tsconfig": "0.1.3",
"babel-jest": "26.6.3",
"c8": "^7.11.0",
"chalk": "4.1.2",
"components-helper": "2.0.0",
"csstype": "2.6.19",
@ -134,6 +139,7 @@
"type-fest": "2.12.0",
"typescript": "4.5.5",
"unplugin-vue-define-options": "0.3.1",
"vitest": "^0.3.2",
"vue": "3.2.30",
"vue-jest": "5.0.0-alpha.10",
"vue-router": "4.0.12",

View File

@ -1,5 +1,6 @@
{
"name": "@element-plus/constants",
"private": true,
"license": "MIT"
"license": "MIT",
"main": "index.ts"
}

View File

@ -1,6 +1,7 @@
import { defineComponent } from 'vue'
import { mount } from '@vue/test-utils'
import { describe, it, expect, fn, afterEach, vi } from 'vitest'
import { useAttrs } from '../use-attrs'
import type { ComponentOptions } from 'vue'
const CLASS = 'a'
const WIDTH = '50px'
@ -8,53 +9,50 @@ const TEST_KEY = 'test'
const TEST_VALUE = 'fake'
const ANOTHER_TEST_VALUE = 'fake1'
const handleClick = jest.fn()
const handleClick = fn()
const genComp = (
inheritAttrs = true,
excludeListeners = false,
excludeKeys: string[] = []
) => {
return {
template: `
<div>
<span v-bind="attrs"></span>
</div>
`,
return defineComponent({
inheritAttrs,
props: {},
setup() {
const attrs = useAttrs({ excludeListeners, excludeKeys })
return {
attrs,
}
},
}
}
const _mount = (Comp: ComponentOptions) => {
return mount({
components: { Comp },
template: `
<comp
class="${CLASS}"
style="width: ${WIDTH}"
${TEST_KEY}="${TEST_VALUE}"
@click="handleClick"
/>`,
methods: {
handleClick,
return () => (
<div>
<span {...attrs.value} />
</div>
)
},
})
}
afterEach(() => {
handleClick.mockClear()
})
const _mount = (Comp: ReturnType<typeof genComp>) => {
return mount({
setup() {
return () => (
<Comp
class={CLASS}
style={{ width: WIDTH }}
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
onClick={handleClick}
{...{ [TEST_KEY]: TEST_VALUE }}
/>
)
},
})
}
describe('useAttrs', () => {
test('class and style should not bind to child node', async () => {
afterEach(() => {
vi.restoreAllMocks()
})
it('class and style should not bind to child node', async () => {
const wrapper = _mount(genComp())
const container = wrapper.element as HTMLDivElement
const span = wrapper.find('span')
@ -70,7 +68,7 @@ describe('useAttrs', () => {
expect(handleClick).toBeCalledTimes(2)
})
test("child node's attributes should update when prop change", async () => {
it("child node's attributes should update when prop change", async () => {
const wrapper = _mount(genComp())
const span = wrapper.find('span')
@ -78,7 +76,7 @@ describe('useAttrs', () => {
expect(span.attributes(TEST_KEY)).toBe(ANOTHER_TEST_VALUE)
})
test('excluded listeners should not bind to child node', async () => {
it('excluded listeners should not bind to child node', async () => {
const wrapper = _mount(genComp(true, true))
const span = wrapper.find('span')
@ -87,7 +85,7 @@ describe('useAttrs', () => {
expect(handleClick).toBeCalledTimes(1)
})
test('excluded attributes should not bind to child node', async () => {
it('excluded attributes should not bind to child node', async () => {
const wrapper = _mount(genComp(true, false, [TEST_KEY]))
const span = wrapper.find('span')

View File

@ -1,11 +1,15 @@
import { computed, defineComponent, nextTick } from 'vue'
import { mount } from '@vue/test-utils'
import { describe, it, fn, vi, expect, afterEach } from 'vitest'
import { debugWarn } from '@element-plus/utils'
import { useDeprecated } from '../use-deprecated'
jest.mock('@element-plus/utils/error', () => {
const AXIOM = 'Rem is the best girl'
vi.mock('@element-plus/utils/error', async () => {
return {
debugWarn: jest.fn(),
...(await vi.importActual<any>('@element-plus/utils/error')),
debugWarn: fn(),
}
})
@ -24,14 +28,15 @@ const DummyComponent = defineComponent({
},
computed(() => props.shouldWarn)
)
return () => AXIOM
},
template: `<div>Rem is the best girl</div>`,
})
describe('useDeprecated', () => {
beforeEach(() => {
;(debugWarn as jest.Mock).mockClear()
afterEach(() => {
vi.restoreAllMocks()
})
it('should warn when condition is true', async () => {
mount(DummyComponent, {
props: {

View File

@ -1,4 +1,5 @@
import { ref } from 'vue'
import { describe, it, expect } from 'vitest'
import { useFocus } from '../use-focus'
describe('useFocus', () => {

View File

@ -1,6 +1,7 @@
import { h, provide } from 'vue'
import { defineComponent, provide } from 'vue'
import { NOOP } from '@vue/shared'
import { mount } from '@vue/test-utils'
import { describe, it, expect } from 'vitest'
import { ElButton } from '@element-plus/components'
import {
elFormKey,
@ -16,23 +17,16 @@ import type {
const AXIOM = 'Rem is the best girl'
const Component = {
render() {
return h(ElButton, this.$attrs, {
default: () => [AXIOM],
})
},
}
const mountComponent = (setup = NOOP, options = {}) => {
return mount(
{
...Component,
const mountComponent = (setup = NOOP, options = {}) =>
mount(
defineComponent({
setup,
},
render() {
return <ElButton {...this.$attrs}>{AXIOM}</ElButton>
},
}),
options
)
}
describe('use-form-item', () => {
it('should return local value', () => {
@ -44,14 +38,10 @@ describe('use-form-item', () => {
const propSize = 'small'
const wrapper = mountComponent(
() => {
provide(elFormItemKey, {
size: 'large',
} as ElFormItemContext)
provide(elFormItemKey, { size: 'large' } as ElFormItemContext)
},
{
props: {
size: propSize,
},
props: { size: propSize },
}
)

View File

@ -1,46 +1,43 @@
import { h, nextTick, computed } from 'vue'
import { nextTick, computed, defineComponent } from 'vue'
import { mount } from '@vue/test-utils'
import { describe, it, expect, beforeEach, afterEach } from 'vitest'
import Chinese from '@element-plus/locale/lang/zh-cn'
import English from '@element-plus/locale/lang/en'
import { useLocale, buildTranslator } from '../use-locale'
import { provideGlobalConfig } from '..'
import type { Language } from '@element-plus/locale'
import type { PropType, ComponentPublicInstance } from 'vue'
import type { VueWrapper } from '@vue/test-utils'
const TestComp = {
const TestComp = defineComponent({
setup() {
const { t } = useLocale()
return () => {
return h(
'div',
{ class: 'locale-manifest' },
t('el.popconfirm.confirmButtonText')
)
}
return () => (
<div class="locale-manifest">{t('el.popconfirm.confirmButtonText')}</div>
)
},
}
})
describe('use-locale', () => {
let wrapper
let wrapper: VueWrapper<ComponentPublicInstance>
beforeEach(() => {
wrapper = mount(
{
props: {
locale: Object,
locale: Object as PropType<Language>,
},
components: {
'el-test': TestComp,
},
setup(props, { slots }) {
provideGlobalConfig(computed(() => ({ locale: props.locale })))
return () => slots.default()
return () => slots.default?.()
},
},
{
props: {
locale: Chinese,
},
slots: {
default: () => h(TestComp),
},
props: { locale: Chinese },
slots: { default: () => <TestComp /> },
}
)
})
@ -70,7 +67,7 @@ describe('use-locale', () => {
)
})
test('return key name if not defined', () => {
it('return key name if not defined', () => {
const t = buildTranslator(English)
expect(t('el.popconfirm.someThing')).toBe('el.popconfirm.someThing')
})

View File

@ -1,5 +1,6 @@
import { ref, nextTick, defineComponent, onMounted } from 'vue'
import { mount } from '@vue/test-utils'
import { describe, it, expect } from 'vitest'
import { hasClass } from '@element-plus/utils'
import { useLockscreen } from '../use-lockscreen'
@ -13,19 +14,15 @@ const Comp = defineComponent({
onMounted(() => {
flag.value = true
})
return () => undefined
},
template: `<div></div>`,
})
describe('useLockscreen', () => {
test('should lock screen when trigger is true', async () => {
it('should lock screen when trigger is true', async () => {
const wrapper = mount({
template: `
<test-comp />
`,
components: {
'test-comp': Comp,
},
setup: () => () => <Comp />,
})
await nextTick()
expect(hasClass(document.body, kls)).toBe(true)
@ -35,26 +32,17 @@ describe('useLockscreen', () => {
expect(hasClass(document.body, kls)).toBe(false)
})
test('should cleanup when unmounted', async () => {
const wrapper = mount({
template: `
<test-comp v-if="shouldRender" />
`,
data() {
return {
shouldRender: true,
}
},
components: {
'test-comp': Comp,
},
it('should cleanup when unmounted', async () => {
const shouldRender = ref(true)
mount({
setup: () => () => shouldRender.value ? <Comp /> : undefined,
})
await nextTick()
expect(hasClass(document.body, kls)).toBe(true)
wrapper.vm.shouldRender = false
shouldRender.value = false
await nextTick()
expect(hasClass(document.body, kls)).toBe(false)

View File

@ -1,12 +1,12 @@
import { ref, nextTick } from 'vue'
import { describe, it, expect, fn } from 'vitest'
import { EVENT_CODE } from '@element-plus/constants'
import { useModal } from '../use-modal'
describe('useModal', () => {
test('should work when ref value changed', async () => {
it('should work when ref value changed', async () => {
const visible = ref(false)
const handleClose = jest.fn()
const handleClose = fn()
useModal(
{

View File

@ -1,21 +1,24 @@
import { nextTick, ref, h, reactive } from 'vue'
import { nextTick, ref, reactive, defineComponent } from 'vue'
import { mount } from '@vue/test-utils'
import { describe, it, expect, fn, beforeEach, afterEach } from 'vitest'
import { useModelToggle, useModelToggleProps } from '../use-model-toggle'
import type { ExtractPropTypes } from 'vue'
import type { VueWrapper } from '@vue/test-utils'
const AXIOM = 'Rem is the best girl'
const onShow = jest.fn()
const onHide = jest.fn()
const onShow = fn()
const onHide = fn()
let flag = true
const shouldProceed = () => flag
const Comp = {
name: 'comp',
props: { ...useModelToggleProps, disabled: Boolean },
setup(props: ExtractPropTypes<typeof useModelToggleProps>) {
const Comp = defineComponent({
props: {
...useModelToggleProps,
disabled: Boolean,
},
setup(props) {
const indicator = ref(false)
const { show, hide, toggle } = useModelToggle({
indicator,
@ -26,36 +29,26 @@ const Comp = {
})
return () => {
return [
h(
'button',
{
class: 'show',
onClick: show,
},
'show'
),
h(
'button',
{
class: 'hide',
onClick: hide,
},
'hide'
),
h('button', {
class: 'toggle',
onClick: toggle,
}),
indicator.value || props.modelValue ? h('div', AXIOM) : null,
]
return (
<>
<button class="show" onClick={show}>
show
</button>
<button class="hide" onClick={hide}>
hide
</button>
<button class="toggle" onClick={toggle}>
toggle
</button>
{indicator.value || props.modelValue ? <div>{AXIOM}</div> : undefined}
</>
)
}
},
}
})
describe('use-model-toggle', () => {
let wrapper: ReturnType<typeof mount>
let wrapper: VueWrapper<any>
beforeEach(() => {
flag = true
wrapper = mount(Comp)
@ -129,33 +122,28 @@ describe('use-model-toggle', () => {
it('should bind with modelValue', async () => {
wrapper.unmount()
const model = ref(false)
const disabled = ref(false)
wrapper = mount({
components: {
Comp,
},
template: `<comp v-model="model" :disabled="disabled" />`,
data() {
return {
model: false,
disabled: false,
}
},
setup: () => () =>
<Comp v-model={model.value} disabled={disabled.value} />,
})
expect(wrapper.findComponent(Comp).text()).not.toContain(AXIOM)
await wrapper.find('.show').trigger('click')
expect(wrapper.vm.model).toBe(true)
expect(model.value).toBe(true)
expect(wrapper.findComponent(Comp).text()).toContain(AXIOM)
await wrapper.find('.hide').trigger('click')
expect(onHide).toHaveBeenCalledTimes(1)
expect(wrapper.vm.model).toBe(false)
expect(model.value).toBe(false)
expect(wrapper.findComponent(Comp).text()).not.toContain(AXIOM)
;(wrapper.vm.model as any) = true
;(wrapper.vm.disabled as any) = true
model.value = true
disabled.value = true
await nextTick()
// when disabled emits false that modifies the model
expect(wrapper.vm.model).toBe(false)
expect(model.value).toBe(false)
// should not hide when disabled
await wrapper.find('.hide').trigger('click')

View File

@ -1,77 +0,0 @@
import { h, nextTick } from 'vue'
import { mount } from '@vue/test-utils'
import { useNamespace, provideGlobalConfig } from '..'
const TestComp = {
setup() {
const ns = useNamespace('table')
return () => {
return h(
'div',
{
id: 'testId',
class: [
ns.b(), // return ns + block
ns.b('body'),
ns.e('content'),
ns.m('active'),
ns.be('content', 'active'),
ns.em('content', 'active'),
ns.bem('body', 'content', 'active'),
ns.is('focus'),
ns.e(), // return empty string
ns.m(), // return empty string
ns.be(), // return empty string
ns.em(), // return empty string
ns.bem(), // return empty string
ns.is('hover', undefined), // return empty string
ns.is('clicked', false), // return empty string
],
},
'text'
)
}
},
}
describe('use-locale', () => {
let wrapper
beforeEach(() => {
wrapper = mount(
{
components: {
'el-test': TestComp,
},
setup(props, { slots }) {
provideGlobalConfig({ namespace: 'ep' })
return () => slots.default()
},
},
{
slots: {
default: () => h(TestComp),
},
}
)
})
afterEach(() => {
wrapper.unmount()
})
it('should provide bem correctly', async () => {
await nextTick()
expect(wrapper.find('#testId').classes().join('~')).toBe(
[
'ep-table', // b()
'ep-table-body', // b('body')
'ep-table__content', // e('content')
'ep-table--active', // m('active')
'ep-table-content__active', // be('content', 'active')
'ep-table__content--active', // em('content', 'active')
'ep-table-body__content--active', // bem('body', 'content', 'active')
'is-focus', // is('focus')
].join('~')
)
})
})

View File

@ -0,0 +1,68 @@
import { defineComponent, nextTick } from 'vue'
import { mount } from '@vue/test-utils'
import { describe, it, expect, beforeEach, afterEach } from 'vitest'
import { useNamespace, provideGlobalConfig } from '..'
import type { VueWrapper } from '@vue/test-utils'
const TestComp = defineComponent({
setup() {
const ns = useNamespace('table')
return () => (
<div
id="testId"
class={[
ns.b(), // return ns + block
ns.b('body'),
ns.e('content'),
ns.m('active'),
ns.be('content', 'active'),
ns.em('content', 'active'),
ns.bem('body', 'content', 'active'),
ns.is('focus'),
ns.e(), // return empty string
ns.m(), // return empty string
ns.be(), // return empty string
ns.em(), // return empty string
ns.bem(), // return empty string
ns.is('hover', undefined), // return empty string
ns.is('clicked', false), // return empty string
]}
>
text
</div>
)
},
})
describe('use-locale', () => {
const Comp = defineComponent({
setup(_props, { slots }) {
provideGlobalConfig({ namespace: 'ep' })
return () => slots.default?.()
},
})
let wrapper: VueWrapper<InstanceType<typeof Comp>>
beforeEach(() => {
wrapper = mount(Comp, {
slots: { default: () => <TestComp /> },
})
})
afterEach(() => {
wrapper.unmount()
})
it('should provide bem correctly', async () => {
await nextTick()
expect(wrapper.find('#testId').classes()).toEqual([
'ep-table', // b()
'ep-table-body', // b('body')
'ep-table__content', // e('content')
'ep-table--active', // m('active')
'ep-table-content__active', // be('content', 'active')
'ep-table__content--active', // em('content', 'active')
'ep-table-body__content--active', // bem('body', 'content', 'active')
'is-focus', // is('focus')
])
})
})

View File

@ -1,44 +1,51 @@
import { ref } from 'vue'
import { on, off } from '@element-plus/utils'
import {
describe,
it,
expect,
beforeAll,
beforeEach,
afterAll,
fn,
} from 'vitest'
import triggerEvent from '@element-plus/test-utils/trigger-event'
import { usePreventGlobal } from '../use-prevent-global'
describe('usePreventGlobal', () => {
const evtName = 'keydown'
const evt = jest.fn()
const evtHandler = fn()
beforeAll(() => {
on(document.body, evtName, evt)
document.body.addEventListener(evtName, evtHandler)
})
beforeEach(() => {
evt.mockClear()
evtHandler.mockClear()
})
afterAll(() => {
off(document.body, evtName, evt)
document.body.removeEventListener(evtName, evtHandler)
})
it('should prevent global event from happening', () => {
const visible = ref(true)
const evt2Trigger = jest.fn().mockReturnValue(true)
const evt2Trigger = fn().mockReturnValue(true)
usePreventGlobal(visible, evtName, evt2Trigger)
triggerEvent(document.body, evtName)
expect(evt).not.toBeCalled()
expect(evtHandler).not.toBeCalled()
expect(evt2Trigger).toHaveBeenCalled()
visible.value = false
// clean up
})
it('should not prevent global event from happening', () => {
const visible = ref(true)
const evt2Trigger = jest.fn().mockReturnValue(false)
const evt2Trigger = fn().mockReturnValue(false)
usePreventGlobal(visible, evtName, evt2Trigger)
triggerEvent(document.body, evtName)
expect(evt).toHaveBeenCalled()
expect(evtHandler).toHaveBeenCalled()
expect(evt2Trigger).toHaveBeenCalled()
visible.value = false

View File

@ -1,4 +1,5 @@
import { ref, nextTick } from 'vue'
import { describe, it, expect } from 'vitest'
import { useRestoreActive } from '../use-restore-active'
describe('useRestoreActive', () => {

View File

@ -1,11 +1,12 @@
import { ref, nextTick, h } from 'vue'
import { ref, nextTick, h, defineComponent } from 'vue'
import { mount } from '@vue/test-utils'
import { describe, it, expect, beforeEach, afterEach } from 'vitest'
import { useTeleport } from '../use-teleport'
import type { VueWrapper } from '@vue/test-utils'
const AXIOM = 'Rem is the best girl'
const Comp = {
const Comp = defineComponent({
setup() {
const appendToBody = ref(true)
const { showTeleport, hideTeleport, renderTeleport } = useTeleport(
@ -13,39 +14,28 @@ const Comp = {
appendToBody
)
return () => {
return [
h(
'button',
{
class: 'show',
onClick: showTeleport,
},
'show'
),
h(
'button',
{
class: 'hide',
onClick: hideTeleport,
},
'hide'
),
h('button', {
class: 'toggle',
onClick: () => {
// toggle append to body.
appendToBody.value = !appendToBody.value
},
}),
renderTeleport(),
]
}
return () => (
<>
<button class="show" onClick={showTeleport}>
show
</button>
<button class="hide" onClick={hideTeleport}>
hide
</button>
<button
class="toggle"
onClick={() => (appendToBody.value = !appendToBody.value)}
>
toggle
</button>
{renderTeleport()}
</>
)
},
}
})
describe('useModal', () => {
let wrapper: ReturnType<typeof mount>
let wrapper: VueWrapper<InstanceType<typeof Comp>>
beforeEach(() => {
wrapper = mount(Comp)

View File

@ -1,4 +1,5 @@
import { mount } from '@vue/test-utils'
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'
import { useTimeout } from '../use-timeout'
const _mount = (cb: () => void) => {
@ -7,29 +8,27 @@ const _mount = (cb: () => void) => {
const { cancelTimeout, registerTimeout } = useTimeout()
registerTimeout(cb, 0)
return {
cancelTimeout,
}
},
render() {
return null
return { cancelTimeout }
},
render: () => undefined,
})
}
jest.useFakeTimers()
describe('use-timeout', () => {
let wrapper
const cb = jest.fn()
beforeEach(() => {
cb.mockClear()
vi.useFakeTimers()
wrapper = _mount(cb)
})
afterEach(() => {
vi.restoreAllMocks()
})
let wrapper: ReturnType<typeof _mount>
const cb = vi.fn()
it('should register timeout correctly', async () => {
expect(cb).not.toHaveBeenCalled()
jest.runOnlyPendingTimers()
vi.runOnlyPendingTimers()
expect(cb).toHaveBeenCalled()
wrapper.unmount()
})
@ -37,7 +36,7 @@ describe('use-timeout', () => {
it('should cancel the timeout correctly', async () => {
wrapper.vm.cancelTimeout()
jest.runOnlyPendingTimers()
vi.runOnlyPendingTimers()
expect(cb).not.toHaveBeenCalled()
wrapper.unmount()
@ -47,7 +46,7 @@ describe('use-timeout', () => {
expect(cb).not.toHaveBeenCalled()
wrapper.unmount()
jest.runOnlyPendingTimers()
vi.runOnlyPendingTimers()
expect(cb).not.toHaveBeenCalled()
})

View File

@ -1,21 +1,30 @@
import { computed, getCurrentInstance, watch, onMounted } from 'vue'
import { isFunction } from '@vue/shared'
import { isClient } from '@vueuse/core'
import { isBoolean, buildProp, definePropType } from '@element-plus/utils'
import { isBoolean, definePropType, buildProp } from '@element-plus/utils'
import type { RouteLocationNormalizedLoaded } from 'vue-router'
import type { Ref, ComponentPublicInstance, ExtractPropTypes } from 'vue'
export const createModelToggleComposable = (name: string) => {
const _prop = buildProp({
type: definePropType<boolean | null>(Boolean),
default: null,
} as const)
const _event = buildProp({
type: definePropType<(val: boolean) => void>(Function),
} as const)
type _UseModelToggleProps<T extends string> = {
[K in T]: typeof _prop
} & {
[K in `onUpdate:${T}`]: typeof _event
}
export const createModelToggleComposable = <T extends string>(name: T) => {
const useModelToggleProps = {
[name]: buildProp({
type: definePropType<boolean | null>(Boolean),
default: null,
} as const),
[`onUpdate:${name}`]: buildProp({
type: definePropType<(val: boolean) => void>(Function),
} as const),
}
[name]: _prop,
[`onUpdate:${name}`]: _event,
} as _UseModelToggleProps<T>
const useModelToggleEmits = [`update:${name}`]
@ -27,7 +36,9 @@ export const createModelToggleComposable = (name: string) => {
onHide,
}: ModelToggleParams) => {
const instance = getCurrentInstance()!
const props = instance.props as UseModelToggleProps & { disabled: boolean }
const props = instance.props as _UseModelToggleProps<T> & {
disabled: boolean
}
const { emit } = instance
const updateEventKey = `update:${name}`

View File

@ -1,3 +1,4 @@
import { describe, it, expect, spyOn } from 'vitest'
import { triggerEvent, isFocusable } from '../..'
const CE = (tag: string) => document.createElement(tag)
@ -6,7 +7,7 @@ describe('Aria Utils', () => {
describe('Trigger Event', () => {
it('Util trigger event to trigger event correctly', () => {
const div = document.createElement('div')
jest.spyOn(div, 'dispatchEvent')
spyOn(div, 'dispatchEvent')
const eventName = 'click'
triggerEvent(div, eventName)
expect(div.dispatchEvent).toHaveBeenCalled()

View File

@ -1,13 +1,12 @@
import { describe, it, expect } from 'vitest'
import { hasClass, addClass, removeClass } from '../..'
const getClass = (el: Element) => {
if (!el) {
return ''
}
if (!el) return ''
return el.getAttribute('class')
}
describe('Dom Utils', () => {
describe('dom style', () => {
describe('hasClass', () => {
it('Judge whether a Element has a class', () => {
const div = document.createElement('div')

View File

@ -1,3 +1,4 @@
import { describe, it, expect, afterEach } from 'vitest'
import {
createGlobalNode,
removeGlobalNode,

View File

@ -2,6 +2,7 @@
import { defineComponent } from 'vue'
import { mount } from '@vue/test-utils'
import { describe, it, expect, vi } from 'vitest'
import { expectTypeOf } from 'expect-type'
import { buildProp, definePropType, mutable, keysOf, buildProps } from '../..'
import type { propKey } from '../..'
@ -449,7 +450,7 @@ describe('buildProps', () => {
describe('runtime', () => {
it('default value', () => {
const warnHandler = jest.fn()
const warnHandler = vi.fn()
const Foo = defineComponent({
props: buildProps({

View File

@ -2,6 +2,7 @@
"name": "@element-plus/utils",
"private": true,
"license": "MIT",
"main": "index.ts",
"peerDependencies": {
"vue": "^3.2.0"
}

View File

@ -38,12 +38,14 @@ importers:
'@typescript-eslint/parser': 5.12.0
'@vitejs/plugin-vue': 2.2.2
'@vitejs/plugin-vue-jsx': 1.3.7
'@vitest/ui': ^0.3.2
'@vue/babel-plugin-jsx': 1.1.1
'@vue/test-utils': 2.0.0-rc.16
'@vue/tsconfig': 0.1.3
'@vueuse/core': ^7.6.0
async-validator: ^4.0.7
babel-jest: 26.6.3
c8: ^7.11.0
chalk: 4.1.2
components-helper: 2.0.0
csstype: 2.6.19
@ -84,6 +86,7 @@ importers:
type-fest: 2.12.0
typescript: 4.5.5
unplugin-vue-define-options: 0.3.1
vitest: ^0.3.2
vue: 3.2.30
vue-jest: 5.0.0-alpha.10
vue-router: 4.0.12
@ -132,10 +135,12 @@ importers:
'@typescript-eslint/parser': 5.12.0_eslint@8.9.0+typescript@4.5.5
'@vitejs/plugin-vue': 2.2.2_vue@3.2.30
'@vitejs/plugin-vue-jsx': 1.3.7
'@vitest/ui': 0.3.2
'@vue/babel-plugin-jsx': 1.1.1_@babel+core@7.17.5
'@vue/test-utils': 2.0.0-rc.16_vue@3.2.30
'@vue/tsconfig': 0.1.3_@types+node@17.0.16
babel-jest: 26.6.3_@babel+core@7.17.5
c8: 7.11.0
chalk: 4.1.2
components-helper: 2.0.0
csstype: 2.6.19
@ -170,6 +175,7 @@ importers:
type-fest: 2.12.0
typescript: 4.5.5
unplugin-vue-define-options: 0.3.1_a2d0c1fa84d0bf84d5748cef41c57115
vitest: 0.3.2_7eedc1c4db875d438e18b4f15a9092b1
vue: 3.2.30
vue-jest: 5.0.0-alpha.10_9f18a56dc640bc0fc3dd15c5d03f3517
vue-router: 4.0.12_vue@3.2.30
@ -480,15 +486,6 @@ packages:
- supports-color
dev: true
/@babel/generator/7.17.0:
resolution: {integrity: sha512-I3Omiv6FGOC29dtlZhkfXO6pgkmukJSlT26QjVvS1DGZe/NzSVCPG41X0tS21oZkJYlovfj9qDWgKP+Cn4bXxw==}
engines: {node: '>=6.9.0'}
dependencies:
'@babel/types': 7.17.0
jsesc: 2.5.2
source-map: 0.5.7
dev: true
/@babel/generator/7.17.3:
resolution: {integrity: sha512-+R6Dctil/MgUsZsZAkYgK+ADNSZzJRRy0TvY65T71z/CR854xHQ1EweBYXdfT+HNeN7w0cSJJEzgxZMv40pxsg==}
engines: {node: '>=6.9.0'}
@ -670,7 +667,7 @@ packages:
'@babel/helper-environment-visitor': 7.16.7
'@babel/helper-member-expression-to-functions': 7.16.7
'@babel/helper-optimise-call-expression': 7.16.7
'@babel/traverse': 7.17.0
'@babel/traverse': 7.17.3
'@babel/types': 7.17.0
transitivePeerDependencies:
- supports-color
@ -1640,24 +1637,6 @@ packages:
'@babel/types': 7.17.0
dev: true
/@babel/traverse/7.17.0:
resolution: {integrity: sha512-fpFIXvqD6kC7c7PUNnZ0Z8cQXlarCLtCUpt2S1Dx7PjoRtCFffvOkHHSom+m5HIxMZn5bIBVb71lhabcmjEsqg==}
engines: {node: '>=6.9.0'}
dependencies:
'@babel/code-frame': 7.16.7
'@babel/generator': 7.17.0
'@babel/helper-environment-visitor': 7.16.7
'@babel/helper-function-name': 7.16.7
'@babel/helper-hoist-variables': 7.16.7
'@babel/helper-split-export-declaration': 7.16.7
'@babel/parser': 7.17.0
'@babel/types': 7.17.0
debug: 4.3.3
globals: 11.12.0
transitivePeerDependencies:
- supports-color
dev: true
/@babel/traverse/7.17.3:
resolution: {integrity: sha512-5irClVky7TxRWIRtxlh2WPUUOLhcPN06AGgaQSB8AEwuyEBgJVuJ5imdHm5zxk8w0QS5T+tDfnDxAlhWjpb7cw==}
engines: {node: '>=6.9.0'}
@ -2798,6 +2777,16 @@ packages:
'@babel/types': 7.17.0
dev: true
/@types/chai-subset/1.3.3:
resolution: {integrity: sha512-frBecisrNGz+F4T6bcc+NLeolfiojh5FxW2klu669+8BARtyQv2C/GkNW6FUodVe4BroGMP/wER/YDGc7rEllw==}
dependencies:
'@types/chai': 4.3.0
dev: true
/@types/chai/4.3.0:
resolution: {integrity: sha512-/ceqdqeRraGolFTcfoXNiqjyQhZzbINDngeoAq9GoHa8PPK1yNzTaxWjA6BFWp5Ua9JpXEMSS4s5i9tS0hOJtw==}
dev: true
/@types/clean-css/4.2.5:
resolution: {integrity: sha512-NEzjkGGpbs9S9fgC4abuBvTpVwE3i+Acu9BBod3PUyjDVZcNsGx61b8r2PphR61QGPnn0JHVs5ey6/I4eTrkxw==}
dependencies:
@ -3232,6 +3221,12 @@ packages:
vue: 3.2.30
dev: true
/@vitest/ui/0.3.2:
resolution: {integrity: sha512-bW2AWmfBxUlz4Xc0lpY1tfMf1MzK3ZnkzN9ACtDTrrWOQ4yEALgKxJm8qDW71x8qllWlrJTbO0R4xUVI96oc/A==}
dependencies:
sirv: 2.0.2
dev: true
/@volar/code-gen/0.31.4:
resolution: {integrity: sha512-ngivMEbBNd19v+EHdLyCJoIGRaoD9J4P20ZgdCEGf2voztja59u3Tilpf9r9ENy/731nG7XncToYm4+c1t/LhA==}
dependencies:
@ -3304,7 +3299,7 @@ packages:
'@babel/helper-module-imports': 7.16.7
'@babel/plugin-syntax-jsx': 7.16.7_@babel+core@7.17.5
'@babel/template': 7.16.7
'@babel/traverse': 7.17.0
'@babel/traverse': 7.17.3
'@babel/types': 7.17.0
'@vue/babel-helper-vue-transform-on': 1.0.2
camelcase: 6.2.0
@ -4007,6 +4002,10 @@ packages:
engines: {node: '>=0.8'}
dev: true
/assertion-error/1.1.0:
resolution: {integrity: sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==}
dev: true
/assign-symbols/1.0.0:
resolution: {integrity: sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c=}
engines: {node: '>=0.10.0'}
@ -4419,6 +4418,25 @@ packages:
resolution: {integrity: sha1-y5T662HIaWRR2zZTThQi+U8K7og=}
dev: true
/c8/7.11.0:
resolution: {integrity: sha512-XqPyj1uvlHMr+Y1IeRndC2X5P7iJzJlEJwBpCdBbq2JocXOgJfr+JVfJkyNMGROke5LfKrhSFXGFXnwnRJAUJw==}
engines: {node: '>=10.12.0'}
hasBin: true
dependencies:
'@bcoe/v8-coverage': 0.2.3
'@istanbuljs/schema': 0.1.2
find-up: 5.0.0
foreground-child: 2.0.0
istanbul-lib-coverage: 3.2.0
istanbul-lib-report: 3.0.0
istanbul-reports: 3.0.2
rimraf: 3.0.2
test-exclude: 6.0.0
v8-to-istanbul: 8.1.1
yargs: 16.2.0
yargs-parser: 20.2.9
dev: true
/cacache/15.2.0:
resolution: {integrity: sha512-uKoJSHmnrqXgthDFx/IU6ED/5xd+NNGe+Bb+kLZy7Ku4P+BaiWEUflAKPZ7eAzsYGcsAGASJZsybXp+quEcHTw==}
engines: {node: '>= 10'}
@ -4529,6 +4547,19 @@ packages:
resolution: {integrity: sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=}
dev: true
/chai/4.3.6:
resolution: {integrity: sha512-bbcp3YfHCUzMOvKqsztczerVgBKSsEijCySNlHHbX3VG1nskvqjz5Rfso1gGwD6w6oOV3eI60pKuMOV5MV7p3Q==}
engines: {node: '>=4'}
dependencies:
assertion-error: 1.1.0
check-error: 1.0.2
deep-eql: 3.0.1
get-func-name: 2.0.0
loupe: 2.3.4
pathval: 1.1.1
type-detect: 4.0.8
dev: true
/chalk/2.4.2:
resolution: {integrity: sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==}
engines: {node: '>=4'}
@ -4569,6 +4600,10 @@ packages:
resolution: {integrity: sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==}
dev: true
/check-error/1.0.2:
resolution: {integrity: sha1-V00xLt2Iu13YkS6Sht1sCu1KrII=}
dev: true
/check-more-types/2.24.0:
resolution: {integrity: sha1-FCD/sQ/URNz8ebQ4kbv//TKoRgA=}
engines: {node: '>= 0.8.0'}
@ -5297,6 +5332,13 @@ packages:
resolution: {integrity: sha1-JJXduvbrh0q7Dhvp3yLS5aVEMmw=}
dev: true
/deep-eql/3.0.1:
resolution: {integrity: sha512-+QeIQyN5ZuO+3Uk5DYh6/1eKO0m0YmJFGNmFHGACpf1ClL1nmlV/p4gNgbl2pJGxgXb4faqo6UE+M5ACEMyVcw==}
engines: {node: '>=0.12'}
dependencies:
type-detect: 4.0.8
dev: true
/deep-is/0.1.4:
resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==}
dev: true
@ -6773,6 +6815,14 @@ packages:
for-in: 1.0.2
dev: true
/foreground-child/2.0.0:
resolution: {integrity: sha512-dCIq9FpEcyQyXKCkyzmlPTFNgrCzPudOe+mhvJU5zAtlBnGVy2yKxtfsxK2tQBThwq225jcvBjpw1Gr40uzZCA==}
engines: {node: '>=8.0.0'}
dependencies:
cross-spawn: 7.0.3
signal-exit: 3.0.5
dev: true
/forever-agent/0.6.1:
resolution: {integrity: sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=}
dev: true
@ -6905,6 +6955,10 @@ packages:
engines: {node: 6.* || 8.* || >= 10.*}
dev: true
/get-func-name/2.0.0:
resolution: {integrity: sha1-6td0q+5y4gQJQzoGY2YCPdaIekE=}
dev: true
/get-intrinsic/1.1.1:
resolution: {integrity: sha512-kWZrnVM42QCiEA2Ig1bG8zjoIMOgxWwYCEeNdwY6Tv/cOSeGpcoX4pXHfKUxNKVoArnrEr2e9srnAxxGIraS9Q==}
dependencies:
@ -7999,6 +8053,11 @@ packages:
engines: {node: '>=8'}
dev: true
/istanbul-lib-coverage/3.2.0:
resolution: {integrity: sha512-eOeJ5BHCmHYvQK7xt9GkdHuzuCGS1Y6g9Gvnx3Ym33fz/HpLRYxiS0wHNr+m/MBC8B647Xt608vCDEvhl9c6Mw==}
engines: {node: '>=8'}
dev: true
/istanbul-lib-instrument/4.0.3:
resolution: {integrity: sha512-BXgQl9kf4WTCPCCpmFGoJkz/+uhvm7h7PFKUYxh7qarQd3ER33vHG//qaE8eN25l07YqZPpHXU9I09l/RD5aGQ==}
engines: {node: '>=8'}
@ -8015,7 +8074,7 @@ packages:
resolution: {integrity: sha512-wcdi+uAKzfiGT2abPpKZ0hSU1rGQjUQnLvtY5MpQ7QCTahD3VODhcu4wcfY1YtkGaDD5yuydOLINXsfbus9ROw==}
engines: {node: '>=8'}
dependencies:
istanbul-lib-coverage: 3.0.0
istanbul-lib-coverage: 3.2.0
make-dir: 3.1.0
supports-color: 7.2.0
dev: true
@ -8025,7 +8084,7 @@ packages:
engines: {node: '>=8'}
dependencies:
debug: 4.3.3
istanbul-lib-coverage: 3.0.0
istanbul-lib-coverage: 3.2.0
source-map: 0.6.1
transitivePeerDependencies:
- supports-color
@ -8943,6 +9002,12 @@ packages:
engines: {node: '>=0.10.0'}
dev: true
/loupe/2.3.4:
resolution: {integrity: sha512-OvKfgCC2Ndby6aSTREl5aCCPTNIzlDfQZvZxNUrBrihDhL3xcrYegTblhmEiCrg2kKQz4XsFIaemE5BF4ybSaQ==}
dependencies:
get-func-name: 2.0.0
dev: true
/lru-cache/6.0.0:
resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==}
engines: {node: '>=10'}
@ -10034,6 +10099,10 @@ packages:
engines: {node: '>=8'}
dev: true
/pathval/1.1.1:
resolution: {integrity: sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==}
dev: true
/pend/1.2.0:
resolution: {integrity: sha1-elfrVQpng/kRUzH89GY9XI4AelA=}
dev: true
@ -11052,6 +11121,15 @@ packages:
totalist: 2.0.0
dev: true
/sirv/2.0.2:
resolution: {integrity: sha512-4Qog6aE29nIjAOKe/wowFTxOdmbEZKb+3tsLljaBRzJwtqto0BChD2zzH0LhgCSXiI+V7X+Y45v14wBZQ1TK3w==}
engines: {node: '>= 10'}
dependencies:
'@polka/url': 1.0.0-next.20
mrmime: 1.0.0
totalist: 3.0.0
dev: true
/sisteransi/1.0.5:
resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==}
dev: true
@ -11651,6 +11729,16 @@ packages:
engines: {node: '>=0.10.0'}
dev: true
/tinypool/0.1.2:
resolution: {integrity: sha512-fvtYGXoui2RpeMILfkvGIgOVkzJEGediv8UJt7TxdAOY8pnvUkFg/fkvqTfXG9Acc9S17Cnn1S4osDc2164guA==}
engines: {node: '>=14.0.0'}
dev: true
/tinyspy/0.2.10:
resolution: {integrity: sha512-Qij6rGWCDjWIejxCXXVi6bNgvrYBp3PbqC4cBP/0fD6WHDOHCw09Zd13CsxrDqSR5PFq01WeqDws8t5lz5sH0A==}
engines: {node: '>=14.0.0'}
dev: true
/tmp/0.0.33:
resolution: {integrity: sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==}
engines: {node: '>=0.6.0'}
@ -11730,6 +11818,11 @@ packages:
engines: {node: '>=6'}
dev: true
/totalist/3.0.0:
resolution: {integrity: sha512-eM+pCBxXO/njtF7vdFsHuqb+ElbxqtI4r5EAvk6grfAFyJ6IvWlSkfZ5T9ozC6xWw3Fj1fGoSmrl0gUs46JVIw==}
engines: {node: '>=6'}
dev: true
/tough-cookie/2.5.0:
resolution: {integrity: sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==}
engines: {node: '>=0.8'}
@ -12288,6 +12381,15 @@ packages:
source-map: 0.7.3
dev: true
/v8-to-istanbul/8.1.1:
resolution: {integrity: sha512-FGtKtv3xIpR6BYhvgH8MI/y78oT7d8Au3ww4QIxymrCtZEh5b8gCw2siywE+puhEmuWKDtmfrvF5UlB298ut3w==}
engines: {node: '>=10.12.0'}
dependencies:
'@types/istanbul-lib-coverage': 2.0.3
convert-source-map: 1.8.0
source-map: 0.7.3
dev: true
/v8flags/3.2.0:
resolution: {integrity: sha512-mH8etigqMfiGWdeXpaaqGfs6BndypxusHHcv2qSHyZkGEznCd/qAXCWWRzeowtL54147cktFOC4P5y+kl8d8Jg==}
engines: {node: '>= 0.10'}
@ -12464,6 +12566,40 @@ packages:
- stylus
dev: true
/vitest/0.3.2_7eedc1c4db875d438e18b4f15a9092b1:
resolution: {integrity: sha512-Xc0u8BVPBdD029uDcLSYDvy1MFenC6V8WvTJOGdld6NNWgz/swgsMvwZNzftsDohmHLgDyck8A+TaQdDd1tNwA==}
engines: {node: '>=14.14.0'}
hasBin: true
peerDependencies:
'@vitest/ui': '*'
c8: '*'
happy-dom: '*'
jsdom: '*'
peerDependenciesMeta:
'@vitest/ui':
optional: true
c8:
optional: true
happy-dom:
optional: true
jsdom:
optional: true
dependencies:
'@types/chai': 4.3.0
'@types/chai-subset': 1.3.3
'@vitest/ui': 0.3.2
c8: 7.11.0
chai: 4.3.6
local-pkg: 0.4.1
tinypool: 0.1.2
tinyspy: 0.2.10
vite: 2.8.3_sass@1.49.7
transitivePeerDependencies:
- less
- sass
- stylus
dev: true
/void-elements/3.1.0:
resolution: {integrity: sha1-YU9/v42AHwu18GYfWy9XhXUOTwk=}
engines: {node: '>=0.10.0'}
@ -12949,6 +13085,19 @@ packages:
yargs-parser: 18.1.3
dev: true
/yargs/16.2.0:
resolution: {integrity: sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==}
engines: {node: '>=10'}
dependencies:
cliui: 7.0.4
escalade: 3.1.1
get-caller-file: 2.0.5
require-directory: 2.1.1
string-width: 4.2.3
y18n: 5.0.8
yargs-parser: 20.2.9
dev: true
/yargs/17.2.1:
resolution: {integrity: sha512-XfR8du6ua4K6uLGm5S6fA+FIJom/MdJcFNVY8geLlp2v8GYbOXD4EB1tPNZsRn4vBzKGMgb5DRZMeWuFc2GO8Q==}
engines: {node: '>=12'}

View File

@ -11,13 +11,11 @@
"allowSyntheticDefaultImports": true,
"types": ["unplugin-vue-define-options"]
},
"references": [{ "path": "./tsconfig.jest.json" }],
"references": [
{ "path": "./tsconfig.vite-config.json" },
{ "path": "./tsconfig.jest.json" },
{ "path": "./tsconfig.vitest.json" }
],
"include": ["packages", "typings"],
"exclude": [
"node_modules",
"**/dist",
"**/__tests__",
"**/*.test.*",
"**/*.spec.*"
]
"exclude": ["node_modules", "**/dist", "**/__tests__/**/*"]
}

View File

@ -0,0 +1,8 @@
{
"extends": "@vue/tsconfig/tsconfig.node.json",
"include": ["vite.config.*", "vitest.config.*"],
"compilerOptions": {
"composite": true,
"types": ["node", "vitest"]
}
}

10
tsconfig.vitest.json Normal file
View File

@ -0,0 +1,10 @@
{
"extends": "@vue/tsconfig/tsconfig.node.json",
"include": ["packages/**/*.vitest.*"],
"compilerOptions": {
"composite": true,
"lib": ["DOM"],
"types": ["node"],
"jsx": "preserve"
}
}

13
vitest.config.ts Normal file
View File

@ -0,0 +1,13 @@
import { defineConfig } from 'vite'
import Vue from '@vitejs/plugin-vue'
import VueJsx from '@vitejs/plugin-vue-jsx'
import DefineOptions from 'unplugin-vue-define-options/vite'
export default defineConfig({
plugins: [Vue(), VueJsx(), DefineOptions()],
test: {
include: ['**/*.vitest.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'],
exclude: ['**/*.test.*', '**/*.spec.*'],
environment: 'jsdom',
},
})