diff --git a/.eslintrc b/.eslintrc index e9539865dd..137d893825 100644 --- a/.eslintrc +++ b/.eslintrc @@ -136,6 +136,9 @@ "no-unused-vars": "off", "import/no-default-export": "warn", "no-underscore-dangle": "warn", + "react/require-default-props": "off", + "no-shadow": "off", + "@typescript-eslint/no-shadow": "error" } }, { diff --git a/client/common/Button.stories.jsx b/client/common/Button.stories.jsx index d11634ae28..0a0150a5b6 100644 --- a/client/common/Button.stories.jsx +++ b/client/common/Button.stories.jsx @@ -1,7 +1,7 @@ import React from 'react'; import { action } from '@storybook/addon-actions'; -import Button from './Button'; +import { Button, ButtonDisplays, ButtonKinds, ButtonTypes } from './Button'; import { GithubIcon, DropdownArrowIcon, PlusIcon } from './icons'; export default { @@ -15,13 +15,13 @@ export default { }; export const AllFeatures = (args) => ( - ); export const SubmitButton = () => ( - ); @@ -59,7 +59,7 @@ export const ButtonWithIconAfter = () => ( ); export const InlineButtonWithIconAfter = () => ( - ); @@ -68,6 +68,6 @@ export const InlineIconOnlyButton = () => ( ); + const anchor = screen.getByRole('link'); + expect(anchor.tagName.toLowerCase()).toBe('a'); + expect(anchor).toHaveAttribute('href', 'https://example.com'); + }); + + it('renders as a React Router when `to` is provided', () => { + render(); + const link = screen.getByRole('link'); + expect(link.tagName.toLowerCase()).toBe('a'); // Link renders as + expect(link).toHaveAttribute('href', '/dashboard'); + }); + + it('renders as a ); + const el = screen.getByRole('button'); + expect(el.tagName.toLowerCase()).toBe('button'); + expect(el).toHaveAttribute('type', 'button'); + }); + + // Children & Icons + it('renders children', () => { + render(); + expect(screen.getByText('Click Me')).toBeInTheDocument(); + }); + + it('renders an iconBefore and button text', () => { + render( + + ); + expect(screen.getByLabelText('iconbefore')).toBeInTheDocument(); + expect(screen.getByRole('button')).toHaveTextContent( + 'This has a before icon' + ); + }); + + it('renders with iconAfter', () => { + render( + + ); + expect(screen.getByLabelText('iconafter')).toBeInTheDocument(); + expect(screen.getByRole('button')).toHaveTextContent( + 'This has an after icon' + ); + }); + + it('renders only the icon if iconOnly', () => { + render( + + ); + expect(screen.getByLabelText('iconafter')).toBeInTheDocument(); + expect(screen.getByRole('button')).not.toHaveTextContent( + 'This has an after icon' + ); + }); + + // HTML attributes + it('calls onClick handler when clicked', () => { + const handleClick = jest.fn(); + render(); + fireEvent.click(screen.getByText('Click')); + expect(handleClick).toHaveBeenCalledTimes(1); + }); + + it('renders disabled state', () => { + render(); + expect(screen.getByRole('button')).toBeDisabled(); + }); + + it('uses aria-label when provided', () => { + render( )} diff --git a/client/modules/IDE/components/NewFolderForm.jsx b/client/modules/IDE/components/NewFolderForm.jsx index b0935397ff..381520c47e 100644 --- a/client/modules/IDE/components/NewFolderForm.jsx +++ b/client/modules/IDE/components/NewFolderForm.jsx @@ -2,7 +2,7 @@ import React, { useRef, useEffect } from 'react'; import { useTranslation } from 'react-i18next'; import { Form, Field } from 'react-final-form'; import { useDispatch } from 'react-redux'; -import Button from '../../../common/Button'; +import { Button, ButtonTypes } from '../../../common/Button'; import { handleCreateFolder } from '../actions/files'; function NewFolderForm() { @@ -54,7 +54,7 @@ function NewFolderForm() { {() => ( - )} diff --git a/client/modules/Legal/pages/Legal.jsx b/client/modules/Legal/pages/Legal.jsx index 0e629f789a..24f8920fe3 100644 --- a/client/modules/Legal/pages/Legal.jsx +++ b/client/modules/Legal/pages/Legal.jsx @@ -4,7 +4,7 @@ import React, { useEffect, useState } from 'react'; import Helmet from 'react-helmet'; import { useTranslation } from 'react-i18next'; import styled from 'styled-components'; -import RouterTab from '../../../common/RouterTab'; +import { RouterTab } from '../../../common/RouterTab'; import RootPage from '../../../components/RootPage'; import { remSize } from '../../../theme'; import Loader from '../../App/components/loader'; diff --git a/client/modules/User/components/APIKeyForm.jsx b/client/modules/User/components/APIKeyForm.jsx index eff0ae2d7b..fda5945f87 100644 --- a/client/modules/User/components/APIKeyForm.jsx +++ b/client/modules/User/components/APIKeyForm.jsx @@ -2,7 +2,7 @@ import PropTypes from 'prop-types'; import React, { useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useDispatch, useSelector } from 'react-redux'; -import Button from '../../../common/Button'; +import { Button, ButtonTypes } from '../../../common/Button'; import { PlusIcon } from '../../../common/icons'; import CopyableInput from '../../IDE/components/CopyableInput'; import { createApiKey, removeApiKey } from '../actions'; @@ -78,7 +78,7 @@ const APIKeyForm = () => { disabled={keyLabel === ''} iconBefore={} label="Create new key" - type="submit" + type={ButtonTypes.SUBMIT} > {t('APIKeyForm.CreateTokenSubmit')} diff --git a/client/modules/User/components/AccountForm.jsx b/client/modules/User/components/AccountForm.jsx index c738e79db2..4ef40e4298 100644 --- a/client/modules/User/components/AccountForm.jsx +++ b/client/modules/User/components/AccountForm.jsx @@ -2,7 +2,7 @@ import React from 'react'; import { Form, Field } from 'react-final-form'; import { useSelector, useDispatch } from 'react-redux'; import { useTranslation } from 'react-i18next'; -import Button from '../../../common/Button'; +import { Button, ButtonTypes } from '../../../common/Button'; import { validateSettings } from '../../../utils/reduxFormUtils'; import { updateSettings, initiateVerification } from '../actions'; import { apiClient } from '../../../utils/apiClient'; @@ -175,7 +175,7 @@ function AccountForm() { )} )} - diff --git a/client/modules/User/components/CollectionCreate.jsx b/client/modules/User/components/CollectionCreate.jsx index 02d3fb59f0..3b2ccdefed 100644 --- a/client/modules/User/components/CollectionCreate.jsx +++ b/client/modules/User/components/CollectionCreate.jsx @@ -4,7 +4,7 @@ import { useTranslation } from 'react-i18next'; import { useDispatch } from 'react-redux'; import { generateCollectionName } from '../../../utils/generateRandomName'; -import Button from '../../../common/Button'; +import { Button, ButtonTypes } from '../../../common/Button'; import { createCollection } from '../../IDE/actions/collections'; const CollectionCreate = () => { @@ -74,7 +74,7 @@ const CollectionCreate = () => { rows="6" />

- diff --git a/client/modules/User/components/CollectionMetadata.jsx b/client/modules/User/components/CollectionMetadata.jsx index 88dc6d0312..dcb47e0929 100644 --- a/client/modules/User/components/CollectionMetadata.jsx +++ b/client/modules/User/components/CollectionMetadata.jsx @@ -4,7 +4,7 @@ import React, { useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useDispatch, useSelector } from 'react-redux'; import { Link } from 'react-router-dom'; -import Button from '../../../common/Button'; +import { Button } from '../../../common/Button'; import Overlay from '../../App/components/Overlay'; import { editCollection } from '../../IDE/actions/collections'; import AddToCollectionSketchList from '../../IDE/components/AddToCollectionSketchList'; diff --git a/client/modules/User/components/CollectionShareButton.jsx b/client/modules/User/components/CollectionShareButton.jsx index c4fd06bcb6..a5d8705dcd 100644 --- a/client/modules/User/components/CollectionShareButton.jsx +++ b/client/modules/User/components/CollectionShareButton.jsx @@ -2,9 +2,9 @@ import PropTypes from 'prop-types'; import React, { useCallback, useState } from 'react'; import { useTranslation } from 'react-i18next'; -import Button from '../../../common/Button'; +import { Button } from '../../../common/Button'; import { DropdownArrowIcon } from '../../../common/icons'; -import useModalClose from '../../../common/useModalClose'; +import { useModalClose } from '../../../common/useModalClose'; import CopyableInput from '../../IDE/components/CopyableInput'; const ShareURL = ({ value }) => { diff --git a/client/modules/User/components/CookieConsent.jsx b/client/modules/User/components/CookieConsent.jsx index 0817e10c70..9cfff74ac6 100644 --- a/client/modules/User/components/CookieConsent.jsx +++ b/client/modules/User/components/CookieConsent.jsx @@ -10,7 +10,7 @@ import PropTypes from 'prop-types'; import { getConfig } from '../../../utils/getConfig'; import { setUserCookieConsent } from '../actions'; import { remSize, prop, device } from '../../../theme'; -import Button from '../../../common/Button'; +import { Button, ButtonKinds } from '../../../common/Button'; const CookieConsentContainer = styled.div` position: fixed; @@ -177,10 +177,7 @@ function CookieConsent({ hide }) { /> - diff --git a/client/modules/User/components/LoginForm.unit.test.jsx b/client/modules/User/components/LoginForm.unit.test.jsx index 7ab207fb01..2f4262c16b 100644 --- a/client/modules/User/components/LoginForm.unit.test.jsx +++ b/client/modules/User/components/LoginForm.unit.test.jsx @@ -23,7 +23,9 @@ jest.mock('../actions', () => ({ ) })); -jest.mock('../../../common/useSyncFormTranslations', () => jest.fn()); +jest.mock('../../../common/useSyncFormTranslations', () => ({ + useSyncFormTranslations: jest.fn() +})); const subject = () => { reduxRender(, { diff --git a/client/modules/User/components/NewPasswordForm.jsx b/client/modules/User/components/NewPasswordForm.jsx index 95e96ec4a3..feca326c77 100644 --- a/client/modules/User/components/NewPasswordForm.jsx +++ b/client/modules/User/components/NewPasswordForm.jsx @@ -5,7 +5,7 @@ import { useDispatch } from 'react-redux'; import { useTranslation } from 'react-i18next'; import { validateNewPassword } from '../../../utils/reduxFormUtils'; import { updatePassword } from '../actions'; -import Button from '../../../common/Button'; +import { Button, ButtonTypes } from '../../../common/Button'; function NewPasswordForm(props) { const { resetPasswordToken } = props; @@ -64,7 +64,10 @@ function NewPasswordForm(props) {

)} - diff --git a/client/modules/User/components/ResetPasswordForm.jsx b/client/modules/User/components/ResetPasswordForm.jsx index f44e7649bd..fe3752fdf4 100644 --- a/client/modules/User/components/ResetPasswordForm.jsx +++ b/client/modules/User/components/ResetPasswordForm.jsx @@ -4,7 +4,7 @@ import { Form, Field } from 'react-final-form'; import { useDispatch, useSelector } from 'react-redux'; import { validateResetPassword } from '../../../utils/reduxFormUtils'; import { initiateResetPassword } from '../actions'; -import Button from '../../../common/Button'; +import { Button, ButtonTypes } from '../../../common/Button'; function ResetPasswordForm(props) { const { t } = useTranslation(); @@ -45,7 +45,7 @@ function ResetPasswordForm(props) { )} diff --git a/client/modules/User/components/SocialAuthButton.jsx b/client/modules/User/components/SocialAuthButton.jsx index ac4a8b3b11..e48c660b67 100644 --- a/client/modules/User/components/SocialAuthButton.jsx +++ b/client/modules/User/components/SocialAuthButton.jsx @@ -6,7 +6,7 @@ import { useDispatch } from 'react-redux'; import { remSize } from '../../../theme'; import { GithubIcon, GoogleIcon } from '../../../common/icons'; -import Button from '../../../common/Button'; +import { Button } from '../../../common/Button'; import { unlinkService } from '../actions'; import { persistState } from '../../IDE/actions/project'; diff --git a/client/modules/User/pages/DashboardView.jsx b/client/modules/User/pages/DashboardView.jsx index fcde949cd7..611fef9f5a 100644 --- a/client/modules/User/pages/DashboardView.jsx +++ b/client/modules/User/pages/DashboardView.jsx @@ -3,7 +3,7 @@ import { useDispatch, useSelector } from 'react-redux'; import { useLocation, useParams } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; -import Button from '../../../common/Button'; +import { Button } from '../../../common/Button'; import Nav from '../../IDE/components/Header/Nav'; import Overlay from '../../App/components/Overlay'; import AssetList from '../../IDE/components/AssetList'; diff --git a/package-lock.json b/package-lock.json index f968228de1..31ceb6c724 100644 --- a/package-lock.json +++ b/package-lock.json @@ -158,6 +158,8 @@ "@types/node": "^16.18.126", "@types/react": "^16.14.0", "@types/react-dom": "^16.9.25", + "@types/react-router-dom": "^5.3.3", + "@types/styled-components": "^5.1.34", "@typescript-eslint/eslint-plugin": "^5.62.0", "@typescript-eslint/parser": "^5.62.0", "babel-core": "^7.0.0-bridge.0", @@ -14131,6 +14133,13 @@ "@types/unist": "*" } }, + "node_modules/@types/history": { + "version": "4.7.11", + "resolved": "https://registry.npmjs.org/@types/history/-/history-4.7.11.tgz", + "integrity": "sha512-qjDJRrmvBMiTx+jyLxvLfJU7UznFuokDv4f3WRuriHKERccVpFU+8XMQUAbDzoiJCsmexxRExQeMwwCdamSKDA==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/hoist-non-react-statics": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz", @@ -14368,6 +14377,29 @@ "redux": "^4.0.0" } }, + "node_modules/@types/react-router": { + "version": "5.1.20", + "resolved": "https://registry.npmjs.org/@types/react-router/-/react-router-5.1.20.tgz", + "integrity": "sha512-jGjmu/ZqS7FjSH6owMcD5qpq19+1RS9DeVRqfl1FeBMxTDQAGwlMWOcs52NDoXaNKyG3d1cYQFMs9rCrb88o9Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/history": "^4.7.11", + "@types/react": "*" + } + }, + "node_modules/@types/react-router-dom": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/@types/react-router-dom/-/react-router-dom-5.3.3.tgz", + "integrity": "sha512-kpqnYK4wcdm5UaWI3fLcELopqLrHgLqNsdpHauzlQktfkHL3npOSwtj1Uz9oKBAzs7lFtVkV8j83voAz2D8fhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/history": "^4.7.11", + "@types/react": "*", + "@types/react-router": "*" + } + }, "node_modules/@types/react/node_modules/csstype": { "version": "3.0.8", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.0.8.tgz", @@ -14434,6 +14466,25 @@ "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", "dev": true }, + "node_modules/@types/styled-components": { + "version": "5.1.34", + "resolved": "https://registry.npmjs.org/@types/styled-components/-/styled-components-5.1.34.tgz", + "integrity": "sha512-mmiVvwpYklFIv9E8qfxuPyIt/OuyIrn6gMOAMOFUO3WJfSrSE+sGUoa4PiZj77Ut7bKZpaa6o1fBKS/4TOEvnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/hoist-non-react-statics": "*", + "@types/react": "*", + "csstype": "^3.0.2" + } + }, + "node_modules/@types/styled-components/node_modules/csstype": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/testing-library__jest-dom": { "version": "5.14.0", "resolved": "https://registry.npmjs.org/@types/testing-library__jest-dom/-/testing-library__jest-dom-5.14.0.tgz", @@ -50505,6 +50556,12 @@ "@types/unist": "*" } }, + "@types/history": { + "version": "4.7.11", + "resolved": "https://registry.npmjs.org/@types/history/-/history-4.7.11.tgz", + "integrity": "sha512-qjDJRrmvBMiTx+jyLxvLfJU7UznFuokDv4f3WRuriHKERccVpFU+8XMQUAbDzoiJCsmexxRExQeMwwCdamSKDA==", + "dev": true + }, "@types/hoist-non-react-statics": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz", @@ -50746,6 +50803,27 @@ "redux": "^4.0.0" } }, + "@types/react-router": { + "version": "5.1.20", + "resolved": "https://registry.npmjs.org/@types/react-router/-/react-router-5.1.20.tgz", + "integrity": "sha512-jGjmu/ZqS7FjSH6owMcD5qpq19+1RS9DeVRqfl1FeBMxTDQAGwlMWOcs52NDoXaNKyG3d1cYQFMs9rCrb88o9Q==", + "dev": true, + "requires": { + "@types/history": "^4.7.11", + "@types/react": "*" + } + }, + "@types/react-router-dom": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/@types/react-router-dom/-/react-router-dom-5.3.3.tgz", + "integrity": "sha512-kpqnYK4wcdm5UaWI3fLcELopqLrHgLqNsdpHauzlQktfkHL3npOSwtj1Uz9oKBAzs7lFtVkV8j83voAz2D8fhw==", + "dev": true, + "requires": { + "@types/history": "^4.7.11", + "@types/react": "*", + "@types/react-router": "*" + } + }, "@types/redux-devtools-themes": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/@types/redux-devtools-themes/-/redux-devtools-themes-1.0.0.tgz", @@ -50807,6 +50885,25 @@ "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", "dev": true }, + "@types/styled-components": { + "version": "5.1.34", + "resolved": "https://registry.npmjs.org/@types/styled-components/-/styled-components-5.1.34.tgz", + "integrity": "sha512-mmiVvwpYklFIv9E8qfxuPyIt/OuyIrn6gMOAMOFUO3WJfSrSE+sGUoa4PiZj77Ut7bKZpaa6o1fBKS/4TOEvnA==", + "dev": true, + "requires": { + "@types/hoist-non-react-statics": "*", + "@types/react": "*", + "csstype": "^3.0.2" + }, + "dependencies": { + "csstype": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", + "dev": true + } + } + }, "@types/testing-library__jest-dom": { "version": "5.14.0", "resolved": "https://registry.npmjs.org/@types/testing-library__jest-dom/-/testing-library__jest-dom-5.14.0.tgz", diff --git a/package.json b/package.json index a6117f966d..431913dd97 100644 --- a/package.json +++ b/package.json @@ -130,6 +130,8 @@ "@types/node": "^16.18.126", "@types/react": "^16.14.0", "@types/react-dom": "^16.9.25", + "@types/react-router-dom": "^5.3.3", + "@types/styled-components": "^5.1.34", "@typescript-eslint/eslint-plugin": "^5.62.0", "@typescript-eslint/parser": "^5.62.0", "babel-core": "^7.0.0-bridge.0",