From a4f5901d7d74280080dcd5dd35acba9554a61b79 Mon Sep 17 00:00:00 2001 From: Abing <1273621932@qq.com> Date: Wed, 18 Sep 2024 17:34:40 +0800 Subject: [PATCH] =?UTF-8?q?test:=20add=20modal=20=E7=BB=84=E4=BB=B6?= =?UTF-8?q?=E6=B5=8B=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/mask/demo/basic.jsx | 8 +- src/components/mask/index.tsx | 121 ++++++++++++++-------------- src/components/mask/mask.test.tsx | 105 ++++++++++++++++-------- src/components/modal/alert.tsx | 2 +- src/components/modal/demo/basic.jsx | 11 ++- src/components/modal/demo/basic.tsx | 3 +- src/components/modal/modal.test.tsx | 109 +++++++++++++++++++++++++ src/components/modal/modal.tsx | 21 ++--- 8 files changed, 269 insertions(+), 111 deletions(-) create mode 100644 src/components/modal/modal.test.tsx diff --git a/src/components/mask/demo/basic.jsx b/src/components/mask/demo/basic.jsx index b1cd9bc..e0fcba2 100644 --- a/src/components/mask/demo/basic.jsx +++ b/src/components/mask/demo/basic.jsx @@ -1,5 +1,5 @@ import React, { useState } from 'react'; -import { Mask, Button, Image } from 'react-single-ui'; +import { Button, Image, Mask } from 'react-single-ui'; import presentImage from './present.png'; export default () => { const [visible, setVisible] = useState(false); @@ -28,7 +28,11 @@ export default () => { 打开自定义内容的蒙层 - setVisible(false)}> + setVisible(false)} + > = ({ + visible = false, + className, + backgroundColor = '#00000066', + zIndex = 999, + onMaskClick, + children, +}) => { + const [container, setContainer] = useState(null); -class Mask extends React.Component { - container: HTMLDivElement | null = null; - maskDom = () => { - const { backgroundColor = '#00000066', zIndex = 999 } = this.props; - const style = { - backgroundColor, - zIndex, - }; + // 创建 container 并在组件卸载时移除 + useEffect(() => { + // console.log('useEffect visible:', visible); // 将日志放在 useEffect 中 + if (visible) { + const newContainer = document.createElement('div'); + newContainer.style.height = '100%'; + const containerId = `${prefixCls}-container-${new Date().getTime()}`; + newContainer.setAttribute('id', containerId); + document.body.appendChild(newContainer); + setContainer(newContainer); - return ( -
- {this.props.children} -
- ); - }; + // 禁用滚动 + const preventDefault = (e: Event) => e.preventDefault(); + document.body.addEventListener('touchmove', preventDefault, { + passive: false, + }); + document.body.addEventListener('scroll', preventDefault, { + passive: false, + }); - handleClickMask = () => { - // 点击遮罩层 - this.removeContainer(); - if (this.props.onMaskClick) { - this.props.onMaskClick(); + return () => { + document.body.removeChild(newContainer); + document.body.removeEventListener('touchmove', preventDefault); + document.body.removeEventListener('scroll', preventDefault); + }; } - }; + }, [visible]); - getContainer = () => { - if (!this.container) { - const container = document.createElement('div'); - container.style.height = '100%'; - const containerId = `${prefixCls}-container-${new Date().getTime()}`; - container.setAttribute('id', containerId); - document.body.appendChild(container); - this.container = container; + // 处理遮罩点击 + const handleClickMask = () => { + if (onMaskClick) { + onMaskClick(); } - return this.container; }; - removeContainer = () => { - if (this.container) { - document.body.removeChild(this.container); - this.container = null; - } - }; + const classes = `${prefixCls}-mask ${className}`; - preventDefault = (e: Event) => { - e.preventDefault(); - }; + const maskDom = ( +
+ {children} +
+ ); - render() { - const { visible } = this.props; - if (IS_REACT_16 && visible) { - document.body.addEventListener('touchmove', this.preventDefault, { - passive: false, - }); - document.body.addEventListener('scroll', this.preventDefault, { - passive: false, - }); - return ReactDOM.createPortal(this.maskDom(), this.getContainer()); - } - document.body.removeEventListener('touchmove', this.preventDefault, false); - document.body.removeEventListener('scroll', this.preventDefault, false); - return null; + if (visible && container) { + return createPortal(maskDom, container); } -} + + return null; +}; export default Mask; diff --git a/src/components/mask/mask.test.tsx b/src/components/mask/mask.test.tsx index c29ad9f..37f0b14 100644 --- a/src/components/mask/mask.test.tsx +++ b/src/components/mask/mask.test.tsx @@ -1,41 +1,82 @@ -import { render, fireEvent, cleanup } from '@testing-library/react'; -import '@testing-library/jest-dom'; +// mask.test.tsx +import { fireEvent, render, screen, waitFor } from '@testing-library/react'; import React from 'react'; -import Mask from './index'; -import { prefixCls } from '../../utils'; +import Mask from './'; -afterEach(() => { - cleanup(); // 清除所有渲染的组件 -}); +describe('Mask Component', () => { + it('should not render mask when visible is false', () => { + render(); + + // 确保在 visible 为 false 时不渲染遮罩 + expect(screen.queryByTestId('mask')).toBeNull(); + }); -describe('Mask component visibility', () => { - it('should be hidden initially and visible after updating props', async () => { - render(); - const element = document.body.querySelector( - `.${prefixCls}-mask`, - ) as HTMLElement; - // 初始状态,Mask 不可见 - expect(element).toBeNull(); - render(); - expect( - document.body.querySelector(`.${prefixCls}-mask`), - ).toBeInTheDocument(); - fireEvent.click( - document.body.querySelector(`.${prefixCls}-mask`) as HTMLElement, + it('should render mask when visible is true', async () => { + render(); + + // 等待组件渲染完成 + await waitFor(() => { + expect(document.querySelector('.test-02')).toBeInTheDocument(); + }); + }); + + it('should call onMaskClick when mask is clicked', async () => { + const mockOnMaskClick = jest.fn(); + + render( + , ); - expect(document.body.querySelector(`.${prefixCls}-mask`)).toBeNull(); + + // 等待组件渲染完成 + await waitFor(() => { + expect(document.querySelector('.test-03')).toBeInTheDocument(); + }); + + // 触发点击事件 + fireEvent.click(document.querySelector('.test-03')!); + + // 确保点击事件处理程序被调用 + expect(mockOnMaskClick).toHaveBeenCalled(); }); - it('should apply zIndex and background correctly', () => { - render(); - const element = document.body.querySelector( - `.${prefixCls}-mask`, - ) as HTMLElement; - expect(element).toHaveStyle({ - zIndex: 100, - backgroundColor: '#ff0000', + it('should apply correct styles based on props', async () => { + const backgroundColor = 'rgba(0, 0, 0, 0.5)'; + const zIndex = 1000; + + render( + , + ); + + // 等待组件渲染完成 + await waitFor(() => { + const maskElement = document.querySelector('.test-04') as HTMLElement; + expect(maskElement).toBeInTheDocument(); + expect(maskElement).toHaveStyle(`background-color: ${backgroundColor}`); + expect(maskElement).toHaveStyle(`z-index: ${zIndex}`); }); - fireEvent.click(element); - expect(document.body.querySelector(`.${prefixCls}-mask`)).toBeNull(); + }); + + it('should not call onMaskClick when mask is not visible', async () => { + const mockOnMaskClick = jest.fn(); + + render( + , + ); + + // 确保在 visible 为 false 时不渲染遮罩 + expect(document.querySelector('.test-05')).toBeNull(); + + // 触发点击事件,不应调用 onMaskClick + fireEvent.click(document.body); + expect(mockOnMaskClick).not.toHaveBeenCalled(); }); }); diff --git a/src/components/modal/alert.tsx b/src/components/modal/alert.tsx index 1b995fd..d254f75 100644 --- a/src/components/modal/alert.tsx +++ b/src/components/modal/alert.tsx @@ -1,8 +1,8 @@ // alert.tsx +import _ from 'lodash'; import React from 'react'; import { createRoot } from 'react-dom/client'; import Modal, { Action, ModalProps } from './modal'; -import _ from 'lodash'; export default function Alert(props: ModalProps) { const { title, message, footer } = props; diff --git a/src/components/modal/demo/basic.jsx b/src/components/modal/demo/basic.jsx index 3419e74..d822e11 100644 --- a/src/components/modal/demo/basic.jsx +++ b/src/components/modal/demo/basic.jsx @@ -1,5 +1,5 @@ import React, { useState } from 'react'; -import { Modal, Button } from 'react-single-ui'; +import { Button, Modal } from 'react-single-ui'; export default () => { const [visible1, setVisible1] = useState(false); @@ -11,12 +11,17 @@ export default () => { setVisible2(true); }; const show3 = () => { - console.log(Modal.alert); - Modal.alert({ + const { close } = Modal.alert({ title: '标题', message: '这是通过 Modal.alert 的方式调用', footer: footer3, }); + setTimeout(() => { + console.log('111111'); + + close(); + }, 1000); + console.log('xxxx:', close); }; const footer1 = [ diff --git a/src/components/modal/demo/basic.tsx b/src/components/modal/demo/basic.tsx index 3419e74..8ce4124 100644 --- a/src/components/modal/demo/basic.tsx +++ b/src/components/modal/demo/basic.tsx @@ -1,5 +1,5 @@ import React, { useState } from 'react'; -import { Modal, Button } from 'react-single-ui'; +import { Button, Modal } from 'react-single-ui'; export default () => { const [visible1, setVisible1] = useState(false); @@ -11,7 +11,6 @@ export default () => { setVisible2(true); }; const show3 = () => { - console.log(Modal.alert); Modal.alert({ title: '标题', message: '这是通过 Modal.alert 的方式调用', diff --git a/src/components/modal/modal.test.tsx b/src/components/modal/modal.test.tsx new file mode 100644 index 0000000..4ff5f9f --- /dev/null +++ b/src/components/modal/modal.test.tsx @@ -0,0 +1,109 @@ +import { act, render } from '@testing-library/react'; +import React from 'react'; +import Modal from './'; +import { modalPrefixCls } from './modal'; + +describe('Modal', () => { + it('should render correctly', () => { + render( + , + ); + + const modalElement = document.body.querySelector(`.${modalPrefixCls}`); + // NOTE: ts 断言 ! + const messageElement = modalElement!.querySelector( + `.${modalPrefixCls}-body`, + ); + const titleElement = modalElement!.querySelector( + `.${modalPrefixCls}-header`, + ); + expect(titleElement).toBeInTheDocument(); + expect(modalElement).toBeInTheDocument(); + expect(messageElement).toHaveTextContent('test message'); + expect(titleElement).toHaveTextContent('test title'); + }); + + it('should render footer correctly', () => { + const handlePressCancel = jest.fn(); + const handlePressComfirm = jest.fn(); + const testFooter = [ + { + text: '取消', + onPress: () => { + handlePressCancel(); + }, + }, + { + text: '确认', + onPress: () => { + handlePressComfirm(); + }, + }, + ]; + render( + , + ); + const wrapper = document.body.querySelector(`.${modalPrefixCls}`)!; + const btns = wrapper.querySelectorAll('a'); + const cancelBtn = btns[0]; + const confirmBtn = btns[1]; + cancelBtn.click(); + confirmBtn.click(); + expect(cancelBtn).toBeInTheDocument(); + expect(confirmBtn).toBeInTheDocument(); + expect(cancelBtn).toHaveTextContent('取消'); + expect(confirmBtn).toHaveTextContent('确认'); + expect(handlePressCancel).toHaveBeenCalledTimes(1); + expect(handlePressComfirm).toHaveBeenCalledTimes(1); + }); + + it('should render alert correctly', async () => { + let closeFn: () => void; + await act(() => { + const { close } = Modal.alert({ + title: 'alert title', + message: 'alert message', + }); + closeFn = close; + }); + const modal = document.body.querySelector(`.${modalPrefixCls}`)!; + expect(modal).toBeInTheDocument(); + expect(modal.querySelector(`.${modalPrefixCls}-header`)).toHaveTextContent( + 'alert title', + ); + expect(modal.querySelector(`.${modalPrefixCls}-body`)).toHaveTextContent( + 'alert message', + ); + // 可选的:调用 close 并检查模态框是否被卸载 + await act(() => { + closeFn(); + }); + expect(modal).not.toBeInTheDocument(); + }); + + it('should correctly show and close modal', () => { + const { rerender } = render( + , + ); + const modalElement = document.body.querySelector(`.${modalPrefixCls}`); + expect(modalElement).not.toBeInTheDocument(); + rerender( + , + ); + const modalElement2 = document.body.querySelector(`.${modalPrefixCls}`); + expect(modalElement2).toBeInTheDocument(); + }); +}); diff --git a/src/components/modal/modal.tsx b/src/components/modal/modal.tsx index 0c7a4a5..ac7f2ca 100644 --- a/src/components/modal/modal.tsx +++ b/src/components/modal/modal.tsx @@ -1,11 +1,11 @@ // modal.tsx import React from 'react'; +import { createRoot } from 'react-dom/client'; import { prefixCls } from '../../utils'; -import Mask from '../mask/index'; import Button, { ButtonType } from '../button/index'; -import { createRoot } from 'react-dom/client'; +import Mask from '../mask/index'; -const modalPrefixCls = prefixCls + '-modal'; +export const modalPrefixCls = prefixCls + '-modal'; export interface Action { text: string; @@ -52,7 +52,7 @@ export interface ModalProps { // 定义 Alert 类型 export type Alert = (options: { - title: React.ReactNode; + title?: React.ReactNode; message: React.ReactNode; footer?: Action[]; }) => { close: () => void }; @@ -81,6 +81,7 @@ const Modal: React.FC> & { alert: Alert } = ( } } const onClickFn = (e: React.MouseEvent) => { + e.stopPropagation(); e.preventDefault(); if (button.onPress) { button.onPress(); @@ -113,17 +114,19 @@ const Modal: React.FC> & { alert: Alert } = ( return (
-
{title}
+ {title &&
{title}
}
{message}
-
- {footer?.map((button, i) => renderFooterButton(button, i))} -
+ {footer && ( +
+ {footer?.map((button, i) => renderFooterButton(button, i))} +
+ )}
); }; return ( - + );