Files
element-plus/packages/components/message/__tests__/message-manager.test.tsx
知晓同丶 98041055b0 feat(components): [message] add placement option & method (#21747)
* feat(components): [message] add `placement` option & method

* fix: resolve test hanging issue caused by reactive circular dependency

* test: add test

* feat: add placement `top-left/top-right/bottom-left/bottom-right`

* refactor: split large normalizeOptions function

* docs: adjust description text

* chore: remove unused height expose

* refactor: simpify code

* style: opt-in center style & simplified animations

* style: replace :not(.center) with :is(.left,.right) for more explicit

* feat: add `placement` to `config-provider`

* fix: fix test warning when placement is undefined

* refactor: remove useless style & simpify types

* fix: avoid circular dependency

* chore: types related

* Update docs/examples/config-provider/message.vue

Co-authored-by: Noblet Ouways <91417411+Dsaquel@users.noreply.github.com>

* Update packages/components/config-provider/__tests__/config-provider.test.tsx

Co-authored-by: Noblet Ouways <91417411+Dsaquel@users.noreply.github.com>

* chore: make typecheck happy & format

* style: add top/bottom transition

* chore: format

---------

Co-authored-by: zhixiaotong <947803089@qq.com>
Co-authored-by: Noblet Ouways <91417411+Dsaquel@users.noreply.github.com>
Co-authored-by: Dsaquel <291874700n@gmail.com>
2025-08-19 15:16:49 +00:00

280 lines
7.4 KiB
TypeScript

import { h, nextTick, ref } from 'vue'
import { describe, expect, it, test, vi } from 'vitest'
import { getStyle } from '@element-plus/utils'
import { rAF } from '@element-plus/test-utils/tick'
import { ElMessage } from '..'
import Message from '../src/method'
import { messageTypes } from '../src/message'
const selector = '.el-message'
// TODO: testing the original transition with `nextTick`'
describe('Message on command', () => {
test('it should get component handle', async () => {
const handle = Message()
await rAF()
expect(document.querySelector(selector)).toBeTruthy()
handle.close()
await rAF()
await nextTick()
expect(document.querySelector(selector)).toBeFalsy()
})
test('it should be able to manually close a message', async () => {
const { close } = Message()
await rAF()
const element = document.querySelector(selector)
expect(element).toBeTruthy()
close()
await rAF()
await nextTick()
expect(document.querySelector(selector)).toBeNull()
})
test('it should close all messages', async () => {
const onClose = vi.fn()
const instances = []
for (let i = 0; i < 4; i++) {
const instance = Message({
duration: 0,
onClose,
})
instances.push(instance)
}
await rAF()
const elements = document.querySelectorAll(selector)
expect(elements.length).toBe(4)
Message.closeAll()
await rAF()
expect(onClose).toHaveBeenCalledTimes(4)
expect(document.querySelectorAll(selector).length).toBe(0)
})
test('it should close all messages with mixed placement', async () => {
const onClose = vi.fn()
const instances = []
for (let i = 0; i < 2; i++) {
const instance = Message({
duration: 0,
placement: 'top',
onClose,
})
instances.push(instance)
}
for (let i = 0; i < 2; i++) {
const instance = Message({
duration: 0,
placement: 'bottom',
onClose,
})
instances.push(instance)
}
await rAF()
const elements = document.querySelectorAll(selector)
expect(elements.length).toBe(4)
Message.closeAll()
await rAF()
expect(onClose).toHaveBeenCalledTimes(4)
expect(document.querySelectorAll(selector).length).toBe(0)
})
test('it should close all messages of the specified type', async () => {
const onClose = vi.fn()
const instances = []
const success = 'success'
for (let i = 0; i < 4; i++) {
const instance = Message({
type: success,
duration: 0,
onClose,
})
instances.push(instance)
}
for (let i = 0; i < 2; i++) {
const instance = Message({
duration: 0,
onClose,
})
instances.push(instance)
}
await rAF()
const elements = document.querySelectorAll(selector)
const successElements = document.querySelectorAll(`${selector}--${success}`)
expect(elements.length).toBe(6)
expect(successElements.length).toBe(4)
Message.closeAll(success)
await rAF()
expect(onClose).toHaveBeenCalledTimes(4)
expect(document.querySelectorAll(selector).length).toBe(2)
Message.closeAll()
})
test('it should close all messages by specified placement', async () => {
const onClose = vi.fn()
const instances = []
for (let i = 0; i < 3; i++) {
const instance = Message({
duration: 0,
placement: 'top',
onClose,
})
instances.push(instance)
}
for (let i = 0; i < 2; i++) {
const instance = Message({
duration: 0,
placement: 'bottom',
onClose,
})
instances.push(instance)
}
await rAF()
const elements = document.querySelectorAll(selector)
expect(elements.length).toBe(5)
Message.closeAllByPlacement('top')
await rAF()
expect(onClose).toHaveBeenCalledTimes(3)
expect(document.querySelectorAll(selector).length).toBe(2)
Message.closeAllByPlacement('bottom')
await rAF()
expect(onClose).toHaveBeenCalledTimes(5)
expect(document.querySelectorAll(selector).length).toBe(0)
})
test('it should stack messages', async () => {
const messages = [Message(), Message(), Message()]
await rAF()
const elements = document.querySelectorAll(selector)
expect(elements.length).toBe(3)
const getTopValue = (elm: Element): number =>
Number.parseInt(getStyle(elm as HTMLElement, 'top'), 10)
const topValues: number[] = []
elements.forEach((e) => {
topValues.push(getTopValue(e))
})
for (let i = 1; i < topValues.length; i++) {
expect(topValues[i - 1]).toBeLessThan(topValues[i])
}
messages.forEach((m) => m.close())
})
test('correct space when set offset', async () => {
const offset = 100
const space = 16
const messages = [Message({ offset }), Message({ offset })]
await rAF()
const elements = document.querySelectorAll(selector)
expect(elements.length).toBe(2)
const getTopValue = (elm: Element): number =>
Number.parseFloat(getStyle(elm as HTMLElement, 'top'))
const firstElementTop = getTopValue(elements[0])
const secondElementTop = getTopValue(elements[1])
expect(firstElementTop).toBe(offset)
expect(secondElementTop).toBe(offset + space)
messages.forEach((m) => m.close())
})
test('it should have 4 other types of message', () => {
messageTypes.forEach((type) => {
expect(Message[type]).toBeInstanceOf(Function)
})
})
test('it should appendTo specified HTMLElement', async () => {
const htmlElement = document.createElement('div')
const handle = Message({
appendTo: htmlElement,
})
await rAF()
expect(htmlElement.querySelector(selector)).toBeTruthy()
handle.close()
await rAF()
await nextTick()
expect(htmlElement.querySelector(selector)).toBeFalsy()
})
test('it should appendTo specified selector', async () => {
const htmlElement = document.createElement('div')
htmlElement.classList.add('message-manager')
document.body.appendChild(htmlElement)
const handle = Message({
appendTo: '.message-manager',
})
await rAF()
expect(htmlElement.querySelector(selector)).toBeTruthy()
handle.close()
await rAF()
await nextTick()
expect(htmlElement.querySelector(selector)).toBeFalsy()
})
test('it should render vnode function', async () => {
const i = ref(0)
Message({
message: () => h('div', i.value),
})
await rAF()
expect(document.querySelector(selector)?.textContent).toMatchInlineSnapshot(
`"0"`
)
i.value++
await rAF()
expect(document.querySelector(selector)?.textContent).toMatchInlineSnapshot(
`"1"`
)
})
describe('context inheritance', () => {
it('should globally inherit context correctly', () => {
expect(ElMessage._context).toBe(null)
const testContext = {
config: {
globalProperties: {},
},
_context: {},
}
ElMessage.install?.(testContext as any)
expect(ElMessage._context).not.toBe(null)
expect(ElMessage._context).toBe(testContext._context)
// clean up
ElMessage._context = null
})
})
// #20333
test('it should destroy self immediately', async () => {
const el = document.createElement('div')
document.body.appendChild(el)
const { close } = Message({ appendTo: el })
close()
await rAF()
expect(el.querySelector(selector)).toBeFalsy()
})
})