From 9bc792b0798acde92dd622a58e03a45b5610f651 Mon Sep 17 00:00:00 2001 From: jonathan Date: Wed, 7 Jan 2026 12:55:38 -0300 Subject: [PATCH] feat(organisms): ajustes en comportamiento de flashNotification - componente alert recibe string con elementos html inmersos, validando estos antes de montarlos - componentes reciben sx como props para modificarlos: label, tinyAlert, btn y sus variantes, alerts, newTooltip - optimizan los test, dejando mock globales --- .gitignore | 3 +- .vscode/settings.json | 10 +- __mocks__/dompurify.ts | 5 + __mocks__/react-ripples.tsx | 7 ++ jest.config.js | 10 +- package-lock.json | 116 ++++++++++++++++++ package.json | 4 +- src/atoms/Icons/Base.tsx | 2 +- src/atoms/Label/Label.tsx | 7 +- src/atoms/TinyAlert/TinyAlert.tsx | 6 +- src/documentation/Documentation.tsx | 2 + .../components/FlashNotificationDemo.tsx | 10 +- src/documentation/pages/Organisms/Alerts.tsx | 2 +- .../pages/Organisms/FlashNotification.tsx | 6 +- src/molecules/Buttons/Btn.tsx | 5 +- src/molecules/Buttons/BtnLink.tsx | 5 +- src/molecules/Buttons/BtnPrimary.tsx | 2 + src/molecules/Buttons/BtnSecondary.tsx | 2 + src/molecules/Buttons/BtnTertiary.tsx | 5 +- src/molecules/Tooltip/NewTooltip.tsx | 2 +- src/organisms/Alerts/Alert.test.tsx | 1 - src/organisms/Alerts/Alert.tsx | 22 ++-- src/organisms/Alerts/FlashNotification.tsx | 57 +++++---- src/organisms/Alerts/types.d.ts | 7 +- .../Alerts/utils/useFlashNotification.ts | 7 +- src/organisms/CourseList/Boxes/BoxImage.tsx | 2 +- .../CourseList/Boxes/BoxTraditional.tsx | 2 +- src/test/jest-setup.ts | 94 ++++++++++++++ 28 files changed, 346 insertions(+), 57 deletions(-) create mode 100644 __mocks__/dompurify.ts create mode 100644 __mocks__/react-ripples.tsx diff --git a/.gitignore b/.gitignore index 64064e320..95c8f85bc 100644 --- a/.gitignore +++ b/.gitignore @@ -8,4 +8,5 @@ yarn-error.log docs .eslintcache tsconfig.tsbuildinfo -*.env* \ No newline at end of file +*.env* +.jest-cache diff --git a/.vscode/settings.json b/.vscode/settings.json index d8646fc1b..f0273bac2 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -16,5 +16,13 @@ "titleBar.inactiveBackground": "#61dafb99", "titleBar.inactiveForeground": "#15202b99" }, - "peacock.color": "#61dafb" + "peacock.color": "#61dafb", + "editor.formatOnSave": true, + "eslint.validate": [ + "javascript", + "javascriptreact", + "typescript", + "typescriptreact", + "mdx" + ] } \ No newline at end of file diff --git a/__mocks__/dompurify.ts b/__mocks__/dompurify.ts new file mode 100644 index 000000000..e404f4e21 --- /dev/null +++ b/__mocks__/dompurify.ts @@ -0,0 +1,5 @@ +const DOMPurify = { + sanitize: (html: string) => html, +} + +export default DOMPurify diff --git a/__mocks__/react-ripples.tsx b/__mocks__/react-ripples.tsx new file mode 100644 index 000000000..d0d8d0000 --- /dev/null +++ b/__mocks__/react-ripples.tsx @@ -0,0 +1,7 @@ +import React from 'react' + +const Ripples = ({ children }: { children: React.ReactNode }) => { + return <>{children} +} + +export default Ripples diff --git a/jest.config.js b/jest.config.js index eccb379e6..726bc1447 100644 --- a/jest.config.js +++ b/jest.config.js @@ -10,6 +10,14 @@ module.exports = { setupFilesAfterEnv: ['/src/test/jest-setup.ts'], watchPlugins: ['jest-watch-typeahead/filename', 'jest-watch-typeahead/testname'], roots: [''], + maxWorkers: '50%', + cacheDirectory: '/.jest-cache', + globals: { + 'ts-jest': { + diagnostics: false, + isolatedModules: true, + }, + }, modulePaths: tsConfig.compilerOptions ? [tsConfig.compilerOptions.baseUrl] : [], moduleNameMapper: { ...(tsConfig.compilerOptions && tsConfig.compilerOptions.paths @@ -18,4 +26,4 @@ module.exports = { '\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$': '/__mocks__/fileMock.js', '\\.(css)$': 'identity-obj-proxy' } -}; \ No newline at end of file +}; diff --git a/package-lock.json b/package-lock.json index e6a3b2077..a1d3bf353 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2853,6 +2853,12 @@ "@types/jest": "*" } }, + "@types/trusted-types": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", + "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", + "optional": true + }, "@types/unist": { "version": "2.0.11", "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz", @@ -4429,6 +4435,28 @@ "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", "dev": true }, + "dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "requires": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + }, + "dependencies": { + "entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==" + } + } + }, + "domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==" + }, "domexception": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/domexception/-/domexception-2.0.1.tgz", @@ -4446,6 +4474,32 @@ } } }, + "domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "requires": { + "domelementtype": "^2.3.0" + } + }, + "dompurify": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.3.1.tgz", + "integrity": "sha512-qkdCKzLNtrgPFP1Vo+98FRzJnBRGe4ffyCea9IwHB1fyxPOeNTHpLKYGd4Uk9xvNoH0ZoOjwZxNptyMwqrId1Q==", + "requires": { + "@types/trusted-types": "^2.0.7" + } + }, + "domutils": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz", + "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==", + "requires": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + } + }, "dot-prop": { "version": "5.3.0", "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-5.3.0.tgz", @@ -4543,6 +4597,11 @@ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", "dev": true }, + "entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==" + }, "env-ci": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/env-ci/-/env-ci-5.5.0.tgz", @@ -6491,6 +6550,15 @@ "lru-cache": "^6.0.0" } }, + "html-dom-parser": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/html-dom-parser/-/html-dom-parser-5.1.2.tgz", + "integrity": "sha512-9nD3Rj3/FuQt83AgIa1Y3ruzspwFFA54AJbQnohXN+K6fL1/bhcDQJJY5Ne4L4A163ADQFVESd/0TLyNoV0mfg==", + "requires": { + "domhandler": "5.0.3", + "htmlparser2": "10.0.0" + } + }, "html-encoding-sniffer": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-2.0.1.tgz", @@ -6506,6 +6574,28 @@ "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", "dev": true }, + "html-react-parser": { + "version": "5.2.11", + "resolved": "https://registry.npmjs.org/html-react-parser/-/html-react-parser-5.2.11.tgz", + "integrity": "sha512-WnSQVn/D1UTj64nSz5y8MriL+MrbsZH80Ytr1oqKqs8DGZnphWY1R1pl3t7TY3rpqTSu+FHA21P80lrsmrdNBA==", + "requires": { + "domhandler": "5.0.3", + "html-dom-parser": "5.1.2", + "react-property": "2.0.2", + "style-to-js": "1.1.21" + } + }, + "htmlparser2": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-10.0.0.tgz", + "integrity": "sha512-TwAZM+zE5Tq3lrEHvOlvwgj1XLWQCtaaibSN11Q+gGBAS7Y1uZSWwXXRe4iF6OXnaq1riyQAPFOBtYc77Mxq0g==", + "requires": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.2.1", + "entities": "^6.0.0" + } + }, "http-proxy-agent": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-4.0.1.tgz", @@ -6628,6 +6718,11 @@ "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", "dev": true }, + "inline-style-parser": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.2.7.tgz", + "integrity": "sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA==" + }, "internal-slot": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", @@ -11329,6 +11424,11 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" }, + "react-property": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/react-property/-/react-property-2.0.2.tgz", + "integrity": "sha512-+PbtI3VuDV0l6CleQMsx2gtK0JZbZKbpdu5ynr+lbsuvtmgbNcS3VM0tuY2QjFNOcWxvXeHjDpy42RO+4U2rug==" + }, "react-refresh": { "version": "0.13.0", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.13.0.tgz", @@ -12616,6 +12716,22 @@ } } }, + "style-to-js": { + "version": "1.1.21", + "resolved": "https://registry.npmjs.org/style-to-js/-/style-to-js-1.1.21.tgz", + "integrity": "sha512-RjQetxJrrUJLQPHbLku6U/ocGtzyjbJMP9lCNK7Ag0CNh690nSH8woqWH9u16nMjYBAok+i7JO1NP2pOy8IsPQ==", + "requires": { + "style-to-object": "1.0.14" + } + }, + "style-to-object": { + "version": "1.0.14", + "resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-1.0.14.tgz", + "integrity": "sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw==", + "requires": { + "inline-style-parser": "0.2.7" + } + }, "style-value-types": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/style-value-types/-/style-value-types-5.0.0.tgz", diff --git a/package.json b/package.json index 12caf4696..1c19a90b8 100644 --- a/package.json +++ b/package.json @@ -128,7 +128,9 @@ "@emotion/react": "11.8.2", "@emotion/styled": "11.8.1", "date-fns": "^4.1.0", + "dompurify": "^3.3.1", "framer-motion": "6.2.8", + "html-react-parser": "^5.2.11", "react-hot-toast": "2.4.1", "react-ripples": "2.2.1" }, @@ -137,4 +139,4 @@ "@types/react-dom": "17.0.2" }, "sideEffects": false -} +} \ No newline at end of file diff --git a/src/atoms/Icons/Base.tsx b/src/atoms/Icons/Base.tsx index 261c01639..acde91f29 100644 --- a/src/atoms/Icons/Base.tsx +++ b/src/atoms/Icons/Base.tsx @@ -11,7 +11,7 @@ export interface BaseProps { } interface IconProps extends BaseProps { viewBox?: string - children: React.ReactChild | React.ReactChild[] + children: React.ReactNode title?: string } diff --git a/src/atoms/Label/Label.tsx b/src/atoms/Label/Label.tsx index 55ec237c9..76dafb7af 100644 --- a/src/atoms/Label/Label.tsx +++ b/src/atoms/Label/Label.tsx @@ -1,13 +1,14 @@ -import { Box } from '@chakra-ui/react' +import { Box, CSSObject } from '@chakra-ui/react' import { vars } from '@theme' export interface LabelProps { - children: React.ReactChild + children: React.ReactNode bg?: string color?: string size?: 'md' | 'sm' m?: string + sx?: CSSObject } /** @@ -20,6 +21,7 @@ export function Label({ color = vars('colors-neutral-darkCharcoal'), size = 'md', m = '0', + sx, }: LabelProps): JSX.Element { const config = { md: { @@ -45,6 +47,7 @@ export function Label({ lineHeight=".875rem" m={m} p={config[size].p} + sx={sx} > {children} diff --git a/src/atoms/TinyAlert/TinyAlert.tsx b/src/atoms/TinyAlert/TinyAlert.tsx index 359c4c560..6da10db65 100644 --- a/src/atoms/TinyAlert/TinyAlert.tsx +++ b/src/atoms/TinyAlert/TinyAlert.tsx @@ -1,4 +1,4 @@ -import { Box } from '@chakra-ui/react' +import { Box, CSSObject } from '@chakra-ui/react' import { vars } from '@theme' import { TinyAlertInfo, TinyAlertError, TinyAlertWarning, TinyAlertSuccess } from '../Icons' @@ -7,6 +7,7 @@ export interface props { status: 'success' | 'error' | 'info' | 'warning' | 'answered' | 'pending' | 'omitted' | 'new' text?: string margin?: string + sx?: CSSObject } /** @@ -16,7 +17,7 @@ export interface props { * */ -export function TinyAlert({ status, text, margin = '0' }: props): JSX.Element | null { +export function TinyAlert({ status, text, margin = '0', sx }: props): JSX.Element | null { const alerts = { success: { icon: , @@ -84,6 +85,7 @@ export function TinyAlert({ status, text, margin = '0' }: props): JSX.Element | width="fit-content" borderRadius=".25rem" className="TinyAlert-Box" + sx={sx} > {alerts[status].icon && ( { React.useEffect(() => { @@ -16,6 +17,7 @@ export const Documentation = (): JSX.Element => { return ( + }> diff --git a/src/documentation/components/FlashNotificationDemo.tsx b/src/documentation/components/FlashNotificationDemo.tsx index 9d8de5448..cf82b4626 100644 --- a/src/documentation/components/FlashNotificationDemo.tsx +++ b/src/documentation/components/FlashNotificationDemo.tsx @@ -5,20 +5,16 @@ import { Box, Button } from '@chakra-ui/react' export default function FlashNotificationDemo({ state, message, + maxContent, }: IFlashNotificationProps): JSX.Element { const { show, active, config } = useFlashNotification({ state: state, message: message, + maxContent: maxContent, }) return ( - + ) diff --git a/src/documentation/pages/Organisms/Alerts.tsx b/src/documentation/pages/Organisms/Alerts.tsx index 6f9c9e7e7..7a719f7c9 100644 --- a/src/documentation/pages/Organisms/Alerts.tsx +++ b/src/documentation/pages/Organisms/Alerts.tsx @@ -200,7 +200,7 @@ export const ViewAlert = (): JSX.Element => { Tipos implementados en el Alert { Estados Existen 4 posibles estados que definen el ícono y color de la notificación. - + diff --git a/src/molecules/Buttons/Btn.tsx b/src/molecules/Buttons/Btn.tsx index 4718d2c3d..e094fb999 100644 --- a/src/molecules/Buttons/Btn.tsx +++ b/src/molecules/Buttons/Btn.tsx @@ -1,4 +1,4 @@ -import { Box, Button } from '@chakra-ui/react' +import { Box, Button, CSSObject } from '@chakra-ui/react' import Ripples from 'react-ripples' import { vars } from '@theme' @@ -24,6 +24,7 @@ export interface propsBaseBtns { type?: 'button' | 'submit' | 'reset' tabIndex?: number id?: string + sx?: CSSObject } interface props extends propsBaseBtns { bg?: colorScheme @@ -64,6 +65,7 @@ export function Btn({ touchDark = false, type = 'button', tabIndex = 0, + sx, }: props): JSX.Element { let showChildren = children ?? null if (!children && !rightIcon && !leftIcon) { @@ -153,6 +155,7 @@ export function Btn({ span: { h: '1rem', }, + ...sx, }} > {showChildren} diff --git a/src/molecules/Buttons/BtnLink.tsx b/src/molecules/Buttons/BtnLink.tsx index be7aa8c6e..77bbaef70 100644 --- a/src/molecules/Buttons/BtnLink.tsx +++ b/src/molecules/Buttons/BtnLink.tsx @@ -1,5 +1,5 @@ import { vars } from '@theme' -import { Box } from '@chakra-ui/react' +import { Box, CSSObject } from '@chakra-ui/react' export interface props { as?: 'button' | 'a' @@ -14,6 +14,7 @@ export interface props { tabIndex?: number target?: '_blank' | '_self' textDecorationLine?: boolean + sx?: CSSObject } export function BtnLink({ @@ -29,6 +30,7 @@ export function BtnLink({ tabIndex, target = '_blank', textDecorationLine = true, + sx, }: props): JSX.Element { const typeButton = { button: { @@ -68,6 +70,7 @@ export function BtnLink({ cursor: 'pointer', }} {...typeButton[as]} + sx={sx} > {children} diff --git a/src/molecules/Buttons/BtnPrimary.tsx b/src/molecules/Buttons/BtnPrimary.tsx index e2a80a7c5..80cabf5bd 100644 --- a/src/molecules/Buttons/BtnPrimary.tsx +++ b/src/molecules/Buttons/BtnPrimary.tsx @@ -37,6 +37,7 @@ export function BtnPrimary({ type = 'button', tabIndex, id, + sx, }: PrimaryButtonProps): JSX.Element { return ( {children} diff --git a/src/molecules/Buttons/BtnSecondary.tsx b/src/molecules/Buttons/BtnSecondary.tsx index 84edb8de5..9648f4a12 100644 --- a/src/molecules/Buttons/BtnSecondary.tsx +++ b/src/molecules/Buttons/BtnSecondary.tsx @@ -38,6 +38,7 @@ export function BtnSecondary({ type = 'button', tabIndex, id, + sx, }: SecondaryButtonProps): JSX.Element { return ( {children} diff --git a/src/molecules/Buttons/BtnTertiary.tsx b/src/molecules/Buttons/BtnTertiary.tsx index ec8187432..c32099130 100644 --- a/src/molecules/Buttons/BtnTertiary.tsx +++ b/src/molecules/Buttons/BtnTertiary.tsx @@ -1,5 +1,5 @@ import { vars } from '@theme' -import { Button } from '@chakra-ui/react' +import { Button, CSSObject } from '@chakra-ui/react' import { GoAhead, GoBack, @@ -41,6 +41,7 @@ export interface propsTertiaryBtn { type?: 'button' | 'submit' | 'reset' tabIndex?: number withoutColor?: boolean + sx?: CSSObject } interface ButtonWithTextProps extends propsTertiaryBtn { @@ -71,6 +72,7 @@ export function BtnTertiary({ type = 'button', tabIndex, withoutColor = false, + sx, }: ButtonProps): JSX.Element { const gray = vars('colors-neutral-gray') const blue = vars('colors-main-deepSkyBlue') @@ -147,6 +149,7 @@ export function BtnTertiary({ '>span': { mr: 0, }, + ...sx, }} > {children} diff --git a/src/molecules/Tooltip/NewTooltip.tsx b/src/molecules/Tooltip/NewTooltip.tsx index db8337634..db424d179 100644 --- a/src/molecules/Tooltip/NewTooltip.tsx +++ b/src/molecules/Tooltip/NewTooltip.tsx @@ -10,7 +10,7 @@ interface TooltipProps { maxWidth?: string placement?: PlacementWithLogical isOpen?: boolean | undefined - sx?: CSSObject | undefined + sx?: CSSObject transform?: string } diff --git a/src/organisms/Alerts/Alert.test.tsx b/src/organisms/Alerts/Alert.test.tsx index c7aea3bb6..8dec6ab5d 100644 --- a/src/organisms/Alerts/Alert.test.tsx +++ b/src/organisms/Alerts/Alert.test.tsx @@ -1,5 +1,4 @@ import { render, screen, fireEvent } from '@testing-library/react' -// TODO: utilizar userEvent import { Alert } from './Alert' diff --git a/src/organisms/Alerts/Alert.tsx b/src/organisms/Alerts/Alert.tsx index d39577275..8c97edc76 100644 --- a/src/organisms/Alerts/Alert.tsx +++ b/src/organisms/Alerts/Alert.tsx @@ -1,4 +1,6 @@ import { Box, useMediaQuery } from '@chakra-ui/react' +import DOMPurifyLib from 'dompurify' +import ReactParser from 'html-react-parser' import { BtnLink, BtnPrimary } from '@/molecules' import { vars } from '@/theme' @@ -28,19 +30,19 @@ export function Alert({ buttonIcon, buttonLink = false, fullWidth = false, + maxContent = false, isFlash = false, onClick, state, m, endTextLink, onClickLink, + sx, }: IAlertProps): JSX.Element { const [isMobile] = useMediaQuery('(max-width: 425px)') - const handleClick = (): any => { - if (onClick) { - onClick() - } + const handleClick = (): void => { + onClick?.() } let buttonType: undefined | 'link' | 'normal' @@ -48,6 +50,9 @@ export function Alert({ buttonType = buttonLink ? 'link' : 'normal' } + const content = + typeof children === 'string' ? ReactParser(DOMPurifyLib.sanitize(children)) : children + return ( - {children} + {content} {endTextLink && onClickLink && {endTextLink}} {buttonType === 'link' && {buttonText}} diff --git a/src/organisms/Alerts/FlashNotification.tsx b/src/organisms/Alerts/FlashNotification.tsx index d6f9ae342..40f3efcab 100644 --- a/src/organisms/Alerts/FlashNotification.tsx +++ b/src/organisms/Alerts/FlashNotification.tsx @@ -1,5 +1,4 @@ -import { Box } from '@chakra-ui/react' -import { useCallback, useEffect } from 'react' +import { useCallback, useEffect, useRef } from 'react' import { toast, Toaster } from 'react-hot-toast' import { IFlashNotificationProps } from './types.d' @@ -20,44 +19,58 @@ import { Alert } from './Alert' * @example Componente FlashNotification recibiendo argumentos * */ - export function FlashNotification({ message, state, show, - m, -}: IFlashNotificationProps): JSX.Element { + maxContent, +}: IFlashNotificationProps): null { + const hasShownRef = useRef(false) + const showToast = useCallback(() => { toast( (t) => ( - toast.dismiss(t.id)}> + toast.dismiss(t.id)} + maxContent={maxContent} + > {message} ), { - duration: handleTime(message), id: alertStates[state].id, + duration: handleTime(message), } ) - }, [message, state]) + }, [message, state, maxContent]) useEffect(() => { - if (show) { + if (show && !hasShownRef.current) { showToast() + hasShownRef.current = true + } + + if (!show) { + hasShownRef.current = false } }, [show, showToast]) - return ( - - - - ) + return null } + +export const FlashNotificationGlobal = (): JSX.Element => ( + +) diff --git a/src/organisms/Alerts/types.d.ts b/src/organisms/Alerts/types.d.ts index 90447d6fb..a351555d6 100644 --- a/src/organisms/Alerts/types.d.ts +++ b/src/organisms/Alerts/types.d.ts @@ -1,9 +1,11 @@ +import { CSSObject } from '@chakra-ui/react' + type TState = 'error' | 'info' | 'success' | 'warning' export interface IAlertProps { /** * Mensaje de alerta */ - children?: React.ReactChild | React.ReactChild[] + children?: React.ReactNode /** * Muestra el botón para cerrar */ @@ -49,6 +51,8 @@ export interface IAlertProps { // agrega el link al final del texto endTextLink?: string onClickLink?: () => void + maxContent?: boolean + sx?: CSSObject } export interface IFlashNotificationProps { @@ -71,4 +75,5 @@ export interface IFlashNotificationProps { */ state: TState show?: boolean + maxContent?: boolean } diff --git a/src/organisms/Alerts/utils/useFlashNotification.ts b/src/organisms/Alerts/utils/useFlashNotification.ts index 8709fc20f..b42ab6ac1 100644 --- a/src/organisms/Alerts/utils/useFlashNotification.ts +++ b/src/organisms/Alerts/utils/useFlashNotification.ts @@ -19,7 +19,11 @@ import { handleTime } from './handleTime' * */ -export const useFlashNotification = ({ state, message }: IFlashNotificationProps): any => { +export const useFlashNotification = ({ + state, + message, + maxContent, +}: IFlashNotificationProps): any => { // Estado que maneja si la notificación debe mostrarse. const [show, setShow] = useState(false) @@ -48,6 +52,7 @@ export const useFlashNotification = ({ state, message }: IFlashNotificationProps config: { state, message, + maxContent, }, } } diff --git a/src/organisms/CourseList/Boxes/BoxImage.tsx b/src/organisms/CourseList/Boxes/BoxImage.tsx index d0ea0994a..984a76cba 100644 --- a/src/organisms/CourseList/Boxes/BoxImage.tsx +++ b/src/organisms/CourseList/Boxes/BoxImage.tsx @@ -16,7 +16,7 @@ interface ImageBoxProps { // Componente que agrega efecto al hacer click interface WithRipplesProps { enabled: boolean - children: React.ReactChild + children: React.ReactNode } function WithRipples({ enabled, children }: WithRipplesProps): JSX.Element { return enabled ? {children} : <>{children} diff --git a/src/organisms/CourseList/Boxes/BoxTraditional.tsx b/src/organisms/CourseList/Boxes/BoxTraditional.tsx index 9457b5704..1fc37a6b4 100644 --- a/src/organisms/CourseList/Boxes/BoxTraditional.tsx +++ b/src/organisms/CourseList/Boxes/BoxTraditional.tsx @@ -12,7 +12,7 @@ export const CourseBoxContext = React.createContext{children} : <>{children} diff --git a/src/test/jest-setup.ts b/src/test/jest-setup.ts index c44951a68..61015c1e3 100644 --- a/src/test/jest-setup.ts +++ b/src/test/jest-setup.ts @@ -1 +1,95 @@ +/** + * Jest global setup + * ================= + * + * Este archivo se ejecuta automáticamente antes de cada test. + * Su objetivo es: + * - Reducir tiempos de ejecución + * - Evitar dependencias del DOM real + * - Eliminar efectos secundarios visuales (animaciones, estilos, media queries) + * - Mantener los tests deterministas y rápidos + * + * ⚠️ IMPORTANTE: + * - Todo mock definido aquí afecta a TODOS los tests. + * - No agregar lógica específica de un test puntual. + * - Si un test necesita el comportamiento real de una librería, + * se debe hacer `jest.unmock()` dentro del test. + */ + +/** + * Extiende expect() con matchers de Testing Library + * Ej: + * - toBeInTheDocument + * - toHaveTextContent + * - toBeVisible + */ import '@testing-library/jest-dom' + +/** + * react-ripples + * ------------- + * Esta librería usa animaciones y cálculos de layout que: + * - No son relevantes para los tests + * - Son lentos en jsdom + * + * Se mockea completamente para: + * - Evitar errores de render + * - Acelerar la ejecución + */ +jest.mock('react-ripples') + +/** + * DOMPurify + * --------- + * DOMPurify sanitiza HTML y depende de APIs del DOM. + * En tests: + * - No validamos la sanitización + * - Solo necesitamos que no falle el render + * + * Se mockea para evitar costos innecesarios y side-effects. + */ +jest.mock('dompurify') + +/** + * Chakra UI + * --------- + * Chakra usa media queries, observers y cálculos de layout. + * + * Se mantiene la implementación real EXCEPTO: + * - useMediaQuery → siempre retorna false + * + * Beneficios: + * - Tests deterministas + * - No depende del tamaño de pantalla + * - Menor costo de ejecución + */ +jest.mock('@chakra-ui/react', () => { + const actual = jest.requireActual('@chakra-ui/react') + return { + ...actual, + useMediaQuery: () => [false], + } +}) + +/** + * Emotion + * ------- + * Emotion genera estilos dinámicos y keyframes. + * En tests: + * - No validamos CSS + * - Solo necesitamos que el render no falle + * + * Se mockean: + * - css + * - keyframes + * + * Esto reduce significativamente el tiempo de ejecución. + */ +jest.mock('@emotion/react', () => { + const actual = jest.requireActual('@emotion/react') + return { + ...actual, + css: () => '', + keyframes: () => '', + } +})