diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 0000000..f56f9f2 --- /dev/null +++ b/.eslintignore @@ -0,0 +1,6 @@ +node_modules/ +examples/ +__test__/ + +.github +coverage/ diff --git a/.eslintrc.js b/.eslintrc.js index 187894b..979fdef 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -1,4 +1,25 @@ module.exports = { root: true, - extends: '@react-native', + extends: ['@react-native', 'plugin:testing-library/react'], + ignorePatterns: ['.eslintrc.js', '**/*.config.js', '**/*.setup.ts'], + env: { jest: true }, + parserOptions: { + sourceType: 'module', + useJSXTextNode: true, + tsconfigRootDir: __dirname, + project: './tsconfig.json', + }, + plugins: ['unused-imports'], + overrides: [ + { + files: ['src/**/*.{ts,tsx,js,jsx}'], + rules: { + 'object-curly-spacing': ['error', 'always'], + 'unused-imports/no-unused-imports': 'error', + complexity: ['error', 8], + 'import/extensions': 0, + 'react/jsx-filename-extension': [2, { extensions: ['.tsx'] }], + }, + }, + ], }; diff --git a/.github/actions/install-dependencies/action.yml b/.github/actions/install-dependencies/action.yml index cbf0328..293f0c4 100644 --- a/.github/actions/install-dependencies/action.yml +++ b/.github/actions/install-dependencies/action.yml @@ -6,9 +6,17 @@ runs: - name: Setup Node.js uses: actions/setup-node@v3 with: - node-version: 22.9.0 + node-version: 20 cache: 'yarn' + - name: Cache node modules + uses: actions/cache@v3 + with: + path: node_modules + key: ${{ runner.os }}-node-${{ hashFiles('yarn.lock') }} + restore-keys: | + ${{ runner.os }}-node- + - name: Install dependencies run: yarn install shell: bash \ No newline at end of file diff --git a/.github/workflows/code-check.yaml b/.github/workflows/code-check.yaml index 1ee050a..7891ffa 100644 --- a/.github/workflows/code-check.yaml +++ b/.github/workflows/code-check.yaml @@ -19,6 +19,8 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - name: Run unit tests - run: yarn run test --coverage --forceExit --maxWorkers=2 - continue-on-error: false \ No newline at end of file + - name: SonarCloud Scan + uses: sonarsource/sonarcloud-github-action@v2 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} \ No newline at end of file diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 0000000..b709582 --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,30 @@ +name: "CodeQL Analysis" + +on: + push: + branches: [ main ] + pull_request: + branches: + - master + schedule: + - cron: '0 0 * * 0' + +jobs: + codeql: + name: Analyze + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v3 + + - name: Initialize CodeQL + uses: github/codeql-action/init@v3 + with: + languages: typescript, javascript + + - name: Autobuild + uses: github/codeql-action/autobuild@v3 + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v3 diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml new file mode 100644 index 0000000..0cb2386 --- /dev/null +++ b/.github/workflows/lint.yml @@ -0,0 +1,26 @@ +name: Lint +on: + push: + branches: + - master + pull_request: + types: [opened, synchronize, reopened] + +jobs: + eslint: + name: "Eslint check" + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + with: + fetch-depth: 0 + + - name: Install dependencies + uses: ./.github/actions/install-dependencies + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Run ESLint + run: yarn run lint + continue-on-error: false \ No newline at end of file diff --git a/.github/workflows/test-runner.yaml b/.github/workflows/test-runner.yaml new file mode 100644 index 0000000..5ddafe9 --- /dev/null +++ b/.github/workflows/test-runner.yaml @@ -0,0 +1,24 @@ +name: Test +on: + push: + branches: + - master + pull_request: + types: [opened, synchronize, reopened] +jobs: + code-quality-check: + name: Test Runner + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + with: + fetch-depth: 0 + + - name: Install dependencies + uses: ./.github/actions/install-dependencies + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Run unit tests + run: yarn run test --coverage --forceExit --maxWorkers=2 + continue-on-error: false \ No newline at end of file diff --git a/.github/workflows/type-check.yaml b/.github/workflows/type-check.yaml new file mode 100644 index 0000000..a8acd8b --- /dev/null +++ b/.github/workflows/type-check.yaml @@ -0,0 +1,23 @@ +name: TSC +on: + push: + branches: + - master + pull_request: + types: [opened, synchronize, reopened] +jobs: + code-quality-check: + name: Type Check + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v3 + + - name: Install dependencies + uses: ./.github/actions/install-dependencies + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Run TypeScript type check + run: npx tsc --noEmit + continue-on-error: false \ No newline at end of file diff --git a/.prettierrc.js b/.prettierrc.js index 2b54074..1f01b43 100644 --- a/.prettierrc.js +++ b/.prettierrc.js @@ -1,7 +1,9 @@ module.exports = { arrowParens: 'avoid', bracketSameLine: true, - bracketSpacing: false, + bracketSpacing: true, singleQuote: true, trailingComma: 'all', + tabWidth: 2, + printWidth: 130, }; diff --git a/.sonarlint/Ffi_staticLib.json b/.sonarlint/Ffi_staticLib.json new file mode 100644 index 0000000..45fb956 --- /dev/null +++ b/.sonarlint/Ffi_staticLib.json @@ -0,0 +1,5 @@ +{ + "sonarCloudOrganization": "material-elements", + "projectKey": "material-elements_react-native-material-elements", + "region": "EU" +} diff --git a/.vscode/settings.json b/.vscode/settings.json index b6a51e5..8cb6c44 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,3 +1,7 @@ { - "cSpell.words": ["Cocoapods"] + "cSpell.words": ["Cocoapods"], + "sonarlint.connectedMode.project": { + "connectionId": "material-elements", + "projectKey": "material-elements_react-native-material-elements" + } } diff --git a/App.tsx b/App.tsx index 5e12a59..486e2a3 100644 --- a/App.tsx +++ b/App.tsx @@ -1,9 +1,6 @@ import React from 'react'; -import {SafeAreaView, ScrollView} from 'react-native'; -import { - Container, - ThemeProvider, -} from './src/packages/react-native-material-elements'; +import { SafeAreaView, ScrollView } from 'react-native'; +import { Container, ThemeProvider } from './src/packages/react-native-material-elements'; function App(): React.JSX.Element { return ( diff --git a/Component.tsx b/Component.tsx deleted file mode 100644 index f747502..0000000 --- a/Component.tsx +++ /dev/null @@ -1,10 +0,0 @@ -import React from 'react'; -import {Box, useTheme} from './src/packages/react-native-material-elements/src'; - -export const Component = () => { - const {theme} = useTheme(); - - console.log(theme.colors.scan); - - return ; -}; diff --git a/index.js b/index.js index a850d03..9b73932 100644 --- a/index.js +++ b/index.js @@ -2,8 +2,8 @@ * @format */ -import {AppRegistry} from 'react-native'; +import { AppRegistry } from 'react-native'; import App from './App'; -import {name as appName} from './app.json'; +import { name as appName } from './app.json'; AppRegistry.registerComponent(appName, () => App); diff --git a/jest.config.js b/jest.config.js index 735f6a7..2f2e0f9 100644 --- a/jest.config.js +++ b/jest.config.js @@ -17,31 +17,24 @@ module.exports = { '.enum.ts', '/config/', '/coverage/', + '.eslintrc.js', + '.prettierrc.js', + 'babel.config.js', ], coverageReporters: ['html', 'text', 'lcov', 'text-summary'], - moduleFileExtensions: [ - 'ts', - 'tsx', - 'js', - 'jsx', - 'json', - 'node', - 'mjs', - 'svg', - 'png', - ], + moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node', 'mjs', 'svg', 'png'], testEnvironment: 'node', setupFiles: ['./jest.setup.ts'], coverageThreshold: { global: { - branches: 80, + statements: 80, + branches: 75, functions: 80, lines: 80, - statements: 80, }, }, }; diff --git a/metro.config.js b/metro.config.js index 9d41685..72fcb3c 100644 --- a/metro.config.js +++ b/metro.config.js @@ -1,4 +1,4 @@ -const {getDefaultConfig, mergeConfig} = require('@react-native/metro-config'); +const { getDefaultConfig, mergeConfig } = require('@react-native/metro-config'); /** * Metro configuration diff --git a/package.json b/package.json index d45a51a..bf00d93 100644 --- a/package.json +++ b/package.json @@ -10,16 +10,11 @@ "test": "jest --coverage" }, "dependencies": { - "@hookform/resolvers": "^5.1.1", - "@testing-library/react-native": "^12.9.0", "lodash": "^4.17.21", "react": "18.3.1", - "react-hook-form": "^7.58.0", "react-native": "0.76.3", - "react-native-phone-number-input": "^2.1.0", "react-native-vector-icons": "^10.2.0", - "use-context-selector": "^2.0.0", - "yup": "^1.6.1" + "use-context-selector": "^2.0.0" }, "devDependencies": { "@babel/core": "^7.25.2", @@ -33,6 +28,7 @@ "@react-native/metro-config": "0.76.3", "@react-native/typescript-config": "0.76.3", "@testing-library/react-hooks": "^8.0.1", + "@testing-library/react-native": "^12.9.0", "@types/jest": "^29.5.14", "@types/lodash": "^4.17.13", "@types/react": "^18.2.6", @@ -40,6 +36,8 @@ "@types/react-test-renderer": "^18.0.0", "babel-jest": "^29.6.3", "eslint": "^8.19.0", + "eslint-plugin-testing-library": "^7.8.0", + "eslint-plugin-unused-imports": "^4.2.0", "jest": "^29.6.3", "prettier": "2.8.8", "react-test-renderer": "18.2.0", diff --git a/sonar-project.properties b/sonar-project.properties new file mode 100644 index 0000000..0df549a --- /dev/null +++ b/sonar-project.properties @@ -0,0 +1,11 @@ +sonar.projectKey=material-elements_react-native-material-elements +sonar.organization=material-elements + +sonar.sources=src + +sonar.exclusions=**/node_modules/**, **/*.test.ts, **/*.spec.ts, **/__test__/**, **/examples/**, **/**.styles.ts + +sonar.javascript.file.suffixes=.js,.jsx +sonar.typescript.file.suffixes=.ts,.tsx + +sonar.javascript.lcov.reportPaths=coverage/lcov.info \ No newline at end of file diff --git a/src/packages/react-native-material-elements/.prettierrc.js b/src/packages/react-native-material-elements/.prettierrc.js deleted file mode 100644 index 1f01b43..0000000 --- a/src/packages/react-native-material-elements/.prettierrc.js +++ /dev/null @@ -1,9 +0,0 @@ -module.exports = { - arrowParens: 'avoid', - bracketSameLine: true, - bracketSpacing: true, - singleQuote: true, - trailingComma: 'all', - tabWidth: 2, - printWidth: 130, -}; diff --git a/src/packages/react-native-material-elements/__test__/BaseButton.test.tsx b/src/packages/react-native-material-elements/__test__/BaseButton.test.tsx index 9ec81e6..105d21c 100644 --- a/src/packages/react-native-material-elements/__test__/BaseButton.test.tsx +++ b/src/packages/react-native-material-elements/__test__/BaseButton.test.tsx @@ -69,4 +69,12 @@ describe('Base button component', () => { expect(mockOnPress).toHaveBeenCalled(); expect(mockOnPress).toHaveBeenCalledTimes(1); }); + + it('should called the onLongPress function', () => { + jest.useFakeTimers(); + const { getByTestId } = render(); + const button = getByTestId(mockTestId); + fireEvent(button, 'longPress', { nativeEvent: {} }); + expect(mockOnLongPress).toHaveBeenCalled(); + }); }); diff --git a/src/packages/react-native-material-elements/__test__/DropDown.test.tsx b/src/packages/react-native-material-elements/__test__/DropDown.test.tsx index f5dc74c..db60a29 100644 --- a/src/packages/react-native-material-elements/__test__/DropDown.test.tsx +++ b/src/packages/react-native-material-elements/__test__/DropDown.test.tsx @@ -1,9 +1,11 @@ import React from 'react'; -import { DropDown, Text } from '../src'; +import * as RN from 'react-native'; +import { DropDown, DropDownListContainer, gray, Text } from '../src'; import { fireEvent, render, waitFor } from './test-utils'; describe('DropDown Component', () => { const mockInputWrapperTouchTestId = 'input-wrapper-touch-test-id'; + const mockInputTestId = 'input-test-id'; beforeEach(() => { jest.clearAllMocks(); @@ -42,4 +44,220 @@ describe('DropDown Component', () => { expect(mockOnDropDownClicked).toHaveBeenCalled(); expect(mockOnDropDownClicked).toHaveBeenCalledTimes(1); }); + + it('should render the input component', () => { + const { getByTestId } = render(); + + const input = getByTestId(mockInputTestId); + expect(input).toBeDefined(); + }); + + it('should render icon input component when variation prop passed as icon', () => { + const { getByTestId } = render(); + + const input = getByTestId(mockInputTestId); + expect(input).toBeDefined(); + }); + + it('should show empty value when input component mount', () => { + const { getByTestId } = render(); + + const input = getByTestId(mockInputTestId); + expect(input.props.value).toEqual(''); + }); + + it('should not render any input component when invalid variation prop passed', () => { + const { queryByTestId } = render(); + + const input = queryByTestId(mockInputTestId); + expect(input).toBeNull(); + }); + + it('should show the selected list item title', () => { + const { getByTestId } = render( + , + ); + + const input = getByTestId(mockInputTestId); + + expect(input.props.value).toEqual('first_item'); + }); + + it('should show the multiselect message', () => { + const { getByTestId } = render( + , + ); + + const input = getByTestId(mockInputTestId); + + expect(input.props.value).toEqual('Selected items 1'); + }); +}); + +describe('DropDownListContainer component', () => { + const mockListItemTestId = 'mock-list-item-test-id'; + + const mockOnItemClickedHandler = jest.fn(); + const mockOnCloseHandler = jest.fn(); + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('should render properly with default props', () => { + const { toJSON } = render( + , + ); + + expect(toJSON()).toMatchSnapshot(); + }); + + it('should render the list item', () => { + const { getByTestId } = render( + , + ); + + const listItem = getByTestId(mockListItemTestId); + expect(listItem).toBeDefined(); + }); + + it('should called the onItemClicked when onItemClicked props will passed and user tap on the list item', () => { + const { getByTestId } = render( + , + ); + + const listItem = getByTestId(mockListItemTestId); + + fireEvent(listItem, 'press', { nativeEvent: {} }); + + expect(mockOnItemClickedHandler).toHaveBeenCalledTimes(1); + expect(mockOnItemClickedHandler).toHaveBeenCalledWith({ id: '1', title: 'first_item' }); + }); + + it('should show the gray[900] color for title text', () => { + const { getByText } = render( + , + ); + + const title = getByText('first_item'); + expect(title).toBeDefined(); + expect(title.props.style.color).toEqual(gray[900]); + }); + + it('should show the gray[50] color for title text if item is selected', () => { + const { getByText } = render( + , + ); + + const title = getByText('first_item'); + expect(title).toBeDefined(); + expect(title.props.style.color).toEqual(gray[50]); + }); + + it('should show the light color of the title text when color scheme is dark', () => { + jest.spyOn(RN, 'useColorScheme').mockReturnValue('dark'); + + const { getByText } = render( + , + ); + + const title = getByText('first_item'); + expect(title).toBeDefined(); + expect(title.props.style.color).toEqual(gray[900]); + }); + + it('should show the listItemEndAdornment item when list items is isSelected', () => { + const { getByText } = render( + Hello} + displaySelectedAdornment + showSelectedItem + />, + ); + + const listItemEndAdornment = getByText('Hello'); + expect(listItemEndAdornment).toBeDefined(); + }); + + it('should render the search bar component', () => { + const { toJSON, getByPlaceholderText } = render( + , + ); + + expect(toJSON()).toMatchSnapshot(); + + const search = getByPlaceholderText('Search'); + expect(search).toBeDefined(); + }); + + it('should call the onClose function when press on item', () => { + const { getByText } = render( + , + ); + + const title = getByText('first_item'); + fireEvent(title, 'press', { nativeEvent: {} }); + expect(mockOnCloseHandler).toHaveBeenCalled(); + expect(mockOnCloseHandler).toHaveBeenCalledTimes(1); + }); }); diff --git a/src/packages/react-native-material-elements/__test__/InputLabel.test.tsx b/src/packages/react-native-material-elements/__test__/InputLabel.test.tsx new file mode 100644 index 0000000..7437c34 --- /dev/null +++ b/src/packages/react-native-material-elements/__test__/InputLabel.test.tsx @@ -0,0 +1,25 @@ +import React from 'react'; +import { Animated } from 'react-native'; +import { gray, InputLabel } from '../src'; +import { fireEvent, render } from './test-utils'; + +describe('InputLabel', () => { + it('updates textLayoutRect state when onLayout is called', () => { + const { getByText } = render(); + + const textElement = getByText('Test Label'); + + fireEvent(textElement, 'layout', { + nativeEvent: { + layout: { + x: 0, + y: 0, + width: 100, + height: 20, + }, + }, + }); + + expect(textElement.props.style).toEqual({ color: gray[800], fontSize: 14 }); + }); +}); diff --git a/src/packages/react-native-material-elements/__test__/OtpInput.test.tsx b/src/packages/react-native-material-elements/__test__/OtpInput.test.tsx index 8831992..198e306 100644 --- a/src/packages/react-native-material-elements/__test__/OtpInput.test.tsx +++ b/src/packages/react-native-material-elements/__test__/OtpInput.test.tsx @@ -3,6 +3,10 @@ import { OTPInput } from '../src'; import { fireEvent, render } from './test-utils'; describe('OTP Component', () => { + const mockOtpInputTestId = 'mock-otp-input-test-id'; + + const mockOnChange = jest.fn(); + beforeEach(() => { jest.clearAllMocks(); }); @@ -23,7 +27,6 @@ describe('OTP Component', () => { }); it('calls onChange with the correct OTP value', () => { - const mockOnChange = jest.fn(); const { getByTestId } = render(); const firstInput = getByTestId('otp-input-0'); @@ -93,4 +96,65 @@ describe('OTP Component', () => { expect(input.props.value).toBe(''); }); }); + + it('should set the default values', () => { + const { getByTestId } = render(); + + const firstInput = getByTestId(`${mockOtpInputTestId}-0`); + expect(firstInput).toBeDefined(); + expect(firstInput.props.value).toEqual('1'); + + const secondInput = getByTestId(`${mockOtpInputTestId}-1`); + expect(secondInput).toBeDefined(); + expect(secondInput.props.value).toEqual('2'); + + const thirdInput = getByTestId(`${mockOtpInputTestId}-2`); + expect(thirdInput).toBeDefined(); + expect(thirdInput.props.value).toEqual('3'); + }); + + it('ignores text input when length > 1', () => { + const { getByTestId } = render(); + const firstInput = getByTestId('otp-input-0'); + + fireEvent.changeText(firstInput, '12'); + + expect(mockOnChange).not.toHaveBeenCalled(); + expect(firstInput.props.value).toBe(''); + }); + + it('calls onChange when a single character is entered', () => { + const { getByTestId } = render(); + const firstInput = getByTestId('otp-input-0'); + + fireEvent.changeText(firstInput, '1'); + + expect(mockOnChange).toHaveBeenCalledWith('1'); + expect(firstInput.props.value).toBe('1'); + }); + + it('throws error when defaultValue length > OTP length', () => { + const spy = jest.spyOn(console, 'error').mockImplementation(); + expect(() => render()).toThrow( + 'Default value must be equal or less then otp length', + ); + spy.mockRestore(); + }); + + it('applies defaultValue when valid', () => { + const { getByTestId } = render(); + expect(getByTestId('otp-input-0').props.value).toBe('1'); + expect(getByTestId('otp-input-1').props.value).toBe('2'); + expect(getByTestId('otp-input-2').props.value).toBe(''); + expect(getByTestId('otp-input-3').props.value).toBe(''); + }); + + it('focus handler is called on focus event', () => { + const { getByTestId } = render(); + const secondInput = getByTestId('otp-input-1'); + + fireEvent(secondInput, 'focus'); + + expect(secondInput.props.selectTextOnFocus).toBe(true); + }); }); diff --git a/src/packages/react-native-material-elements/__test__/Ripple.test.tsx b/src/packages/react-native-material-elements/__test__/Ripple.test.tsx new file mode 100644 index 0000000..d4e5e42 --- /dev/null +++ b/src/packages/react-native-material-elements/__test__/Ripple.test.tsx @@ -0,0 +1,20 @@ +import React from 'react'; +import { Ripple } from '../src/components/Ripple'; +import { act, render } from './test-utils'; +import { Animated } from 'react-native'; + +describe('Ripple effect', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('should render correctly with default props', () => { + const { toJSON } = render(); + expect(toJSON()).toMatchSnapshot(); + }); + + it('renders correctly', () => { + const { getByTestId } = render(); + expect(getByTestId('ripple')).toBeTruthy(); + }); +}); diff --git a/src/packages/react-native-material-elements/__test__/SegmentedControl.test.tsx b/src/packages/react-native-material-elements/__test__/SegmentedControl.test.tsx new file mode 100644 index 0000000..b49d539 --- /dev/null +++ b/src/packages/react-native-material-elements/__test__/SegmentedControl.test.tsx @@ -0,0 +1,89 @@ +import { SegmentedControl } from '../src'; +import { SegmentedControlContainer } from '../src/components/SegmentedControl/SegmentedControlContainer'; +import { SegmentedControlItem } from '../src/components/SegmentedControl/SegmentedControlItem'; +import { fireEvent, render } from './test-utils'; + +describe('SegmentedControl component', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('should render correctly with default props', () => { + const { toJSON } = render(); + expect(toJSON()).toMatchSnapshot(); + }); +}); + +describe('SegmentedControlContainer component', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('should render correctly with default props', () => { + const { toJSON } = render(); + expect(toJSON()).toMatchSnapshot(); + }); +}); + +describe('SegmentedControlItem component', () => { + const mockTestId = 'segmented-item-test-id'; + + const mockOnPress = jest.fn(); + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('should render correctly with default props', () => { + const { toJSON } = render(); + expect(toJSON()).toMatchSnapshot(); + }); + + it('should render the data when data passed as string', () => { + const { getByText } = render(); + + const text = getByText('First'); + expect(text).toBeDefined(); + }); + + it('should render the data when data passed as number', () => { + const { getByText } = render(); + + const text = getByText('1000'); + expect(text).toBeDefined(); + }); + + it('should called the onPress function when click on the SegmentedControlItem', () => { + const { getByTestId } = render(); + + const item = getByTestId(mockTestId); + + fireEvent(item, 'press', { nativeEvent: {} }); + + expect(mockOnPress).toHaveBeenCalledTimes(1); + }); + + it('should called the onPress function with string data when click on the SegmentedControlItem', () => { + const { getByTestId } = render(); + + const item = getByTestId(mockTestId); + + fireEvent(item, 'press', { nativeEvent: {} }); + + expect(mockOnPress).toHaveBeenCalledTimes(1); + expect(mockOnPress).toHaveBeenCalledWith('data', 0); + }); + + it('should called the onPress function with object data when click on the SegmentedControlItem', () => { + const { getByTestId } = render( + , + ); + + const item = getByTestId(mockTestId); + + fireEvent(item, 'press', { nativeEvent: {} }); + + expect(mockOnPress).toHaveBeenCalledTimes(1); + expect(mockOnPress).toHaveBeenCalledWith({ title: 'label' }, 0); + }); +}); diff --git a/src/packages/react-native-material-elements/__test__/Skeleton.test.tsx b/src/packages/react-native-material-elements/__test__/Skeleton.test.tsx new file mode 100644 index 0000000..b83c709 --- /dev/null +++ b/src/packages/react-native-material-elements/__test__/Skeleton.test.tsx @@ -0,0 +1,44 @@ +import React from 'react'; +import { Animated } from 'react-native'; +import { gray, Skeleton } from '../src'; +import { render } from './test-utils'; + +describe('Skeleton component', () => { + let colorSchemeSpy: jest.SpyInstance; + + beforeEach(() => { + jest.clearAllMocks(); + colorSchemeSpy = jest.spyOn(require('react-native'), 'useColorScheme'); + }); + + afterEach(() => { + colorSchemeSpy.mockRestore(); + }); + + it('renders with default background in light mode', () => { + colorSchemeSpy.mockReturnValue('light'); + const { getByTestId } = render(); + const skeleton = getByTestId('skeleton'); + expect(skeleton.props.style.backgroundColor).toBe(gray[400]); + }); + + it('renders with default background in dark mode', () => { + colorSchemeSpy.mockReturnValue('dark'); + const { getByTestId } = render(); + const skeleton = getByTestId('skeleton'); + expect(skeleton.props.style.backgroundColor).toBe(gray[800]); + }); + + it('uses the custom backgroundColor when provided', () => { + colorSchemeSpy.mockReturnValue('light'); + const { getByTestId } = render(); + const skeleton = getByTestId('skeleton'); + expect(skeleton.props.style.backgroundColor).toBe('red'); + }); + + it('starts shimmer animation on mount', () => { + const loopSpy = jest.spyOn(Animated, 'loop'); + render(); + expect(loopSpy).toHaveBeenCalled(); + }); +}); diff --git a/src/packages/react-native-material-elements/__test__/Snackbar.test.tsx b/src/packages/react-native-material-elements/__test__/Snackbar.test.tsx index c4b2348..224e10d 100644 --- a/src/packages/react-native-material-elements/__test__/Snackbar.test.tsx +++ b/src/packages/react-native-material-elements/__test__/Snackbar.test.tsx @@ -1,11 +1,17 @@ import { DeviceEventEmitter } from 'react-native'; -import { HIDE_SNACK_BAR_MESSAGE, SHOW_SNACK_BAR_MESSAGE, snackbar, SnackbarProperties } from '../src'; +import { HIDE_SNACK_BAR_MESSAGE, SHOW_SNACK_BAR_MESSAGE, Snackbar, snackbar, SnackbarProperties } from '../src'; +import { render } from './test-utils'; describe('snackbar utils', () => { beforeEach(() => { jest.clearAllMocks(); }); + it('should render with default props', () => { + const { toJSON } = render(); + expect(toJSON()).toMatchSnapshot(); + }); + it('should called the snack bar show method with config', () => { snackbar.show({} as any); expect(DeviceEventEmitter.emit).toHaveBeenCalled(); diff --git a/src/packages/react-native-material-elements/__test__/Switch.test.tsx b/src/packages/react-native-material-elements/__test__/Switch.test.tsx index 94b955b..71add79 100644 --- a/src/packages/react-native-material-elements/__test__/Switch.test.tsx +++ b/src/packages/react-native-material-elements/__test__/Switch.test.tsx @@ -1,15 +1,28 @@ import React from 'react'; +import { View } from 'react-native'; import { + getSwitchSizes, Switch, + SWITCH_CONTAINER_ANDROID_MODE_HEIGHT_LARGE, + SWITCH_CONTAINER_ANDROID_MODE_HEIGHT_MEDIUM, + SWITCH_CONTAINER_ANDROID_MODE_HEIGHT_SMALL, + SWITCH_CONTAINER_ANDROID_MODE_WIDTH_LARGE, + SWITCH_CONTAINER_ANDROID_MODE_WIDTH_MEDIUM, + SWITCH_CONTAINER_ANDROID_MODE_WIDTH_SMALL, SWITCH_CONTAINER_HEIGHT_LARGE, SWITCH_CONTAINER_HEIGHT_MEDIUM, SWITCH_CONTAINER_HEIGHT_SMALL, SWITCH_CONTAINER_WIDTH_LARGE, SWITCH_CONTAINER_WIDTH_MEDIUM, SWITCH_CONTAINER_WIDTH_SMALL, + SWITCH_THUMB_HEIGHT_LARGE, + SWITCH_THUMB_HEIGHT_MEDIUM, + SWITCH_THUMB_HEIGHT_SMALL, + SWITCH_THUMB_WIDTH_LARGE, + SWITCH_THUMB_WIDTH_MEDIUM, + SWITCH_THUMB_WIDTH_SMALL, } from '../src'; import { fireEvent, render, waitFor } from './test-utils'; -import { View } from 'react-native'; describe('Switch Component', () => { const switchMockTestId = 'switch-test-id'; @@ -73,3 +86,56 @@ describe('Switch Component', () => { expect(switchComponent.props.style.height).toBe(SWITCH_CONTAINER_HEIGHT_SMALL); }); }); + +describe('getSwitchSizes', () => { + const sizes = ['small', 'medium', 'large'] as const; + const types = ['ios', 'android'] as const; + + sizes.forEach(size => { + types.forEach(type => { + it(`returns correct styles for size=${size} and type=${type}`, () => { + const { thumbStyles, thumbContainerStyles } = getSwitchSizes({ size, type }); + + // check thumb styles + if (size === 'small') { + expect(thumbStyles.width).toBe(SWITCH_THUMB_WIDTH_SMALL); + expect(thumbStyles.height).toBe(SWITCH_THUMB_HEIGHT_SMALL); + } + if (size === 'medium') { + expect(thumbStyles.width).toBe(SWITCH_THUMB_WIDTH_MEDIUM); + expect(thumbStyles.height).toBe(SWITCH_THUMB_HEIGHT_MEDIUM); + } + if (size === 'large') { + expect(thumbStyles.width).toBe(SWITCH_THUMB_WIDTH_LARGE); + expect(thumbStyles.height).toBe(SWITCH_THUMB_HEIGHT_LARGE); + } + + // check container styles + if (size === 'small') { + expect(thumbContainerStyles.width).toBe( + type === 'android' ? SWITCH_CONTAINER_ANDROID_MODE_WIDTH_SMALL : SWITCH_CONTAINER_WIDTH_SMALL, + ); + expect(thumbContainerStyles.height).toBe( + type === 'android' ? SWITCH_CONTAINER_ANDROID_MODE_HEIGHT_SMALL : SWITCH_CONTAINER_HEIGHT_SMALL, + ); + } + if (size === 'medium') { + expect(thumbContainerStyles.width).toBe( + type === 'android' ? SWITCH_CONTAINER_ANDROID_MODE_WIDTH_MEDIUM : SWITCH_CONTAINER_WIDTH_MEDIUM, + ); + expect(thumbContainerStyles.height).toBe( + type === 'android' ? SWITCH_CONTAINER_ANDROID_MODE_HEIGHT_MEDIUM : SWITCH_CONTAINER_HEIGHT_MEDIUM, + ); + } + if (size === 'large') { + expect(thumbContainerStyles.width).toBe( + type === 'android' ? SWITCH_CONTAINER_ANDROID_MODE_WIDTH_LARGE : SWITCH_CONTAINER_WIDTH_LARGE, + ); + expect(thumbContainerStyles.height).toBe( + type === 'android' ? SWITCH_CONTAINER_ANDROID_MODE_HEIGHT_LARGE : SWITCH_CONTAINER_HEIGHT_LARGE, + ); + } + }); + }); + }); +}); diff --git a/src/packages/react-native-material-elements/__test__/Text.test.tsx b/src/packages/react-native-material-elements/__test__/Text.test.tsx index 2ecb24f..1c0e15b 100644 --- a/src/packages/react-native-material-elements/__test__/Text.test.tsx +++ b/src/packages/react-native-material-elements/__test__/Text.test.tsx @@ -1,7 +1,7 @@ import { render as testingRenderer, waitFor } from '@testing-library/react-native'; import React from 'react'; import { Text as RnText, StyleSheet } from 'react-native'; -import { red, secondary, Text, ThemeProvider } from '../src'; +import { FormHelperText, red, secondary, Text, ThemeProvider } from '../src'; import { TextVariation, TextVariationThemeConfig } from '../src/components/types'; import { render } from './test-utils'; @@ -397,3 +397,10 @@ describe('Text Component', () => { }); }); }); + +describe('FormHelperText component', () => { + it('should render correct', () => { + const { toJSON } = render(Hello); + expect(toJSON()).toMatchSnapshot(); + }); +}); diff --git a/src/packages/react-native-material-elements/__test__/TextFieldEndAdornment.test.tsx b/src/packages/react-native-material-elements/__test__/TextFieldEndAdornment.test.tsx index f199cdf..0962fa9 100644 --- a/src/packages/react-native-material-elements/__test__/TextFieldEndAdornment.test.tsx +++ b/src/packages/react-native-material-elements/__test__/TextFieldEndAdornment.test.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { Text } from '../src'; +import { ActivityIndicator, Text } from '../src'; import TextFieldEndAdornment from '../src/components/TextField/TextFieldEndAdornment'; import { render } from './test-utils'; @@ -31,4 +31,41 @@ describe('TextFieldEndAdornment', () => { const loaderComponent = getByTestId(mockLoadingIndicatorTestId); expect(loaderComponent).toBeDefined(); }); + + it('should render the loading indicator when loading prop passed', () => { + const { getByTestId } = render(); + + const loader = getByTestId('loader'); + expect(loader).toBeDefined(); + }); + + it('renders ActivityIndicator when loading=true and showLoadingIndicatorWhenFocused=false', () => { + const { getByTestId, UNSAFE_getByType } = render( + , + ); + + const container = getByTestId('endAdornment'); + expect(container).toBeTruthy(); + expect(UNSAFE_getByType(ActivityIndicator)).toBeTruthy(); + }); + + it('renders ActivityIndicator when loading=true, showLoadingIndicatorWhenFocused=true, and isFocused=true', () => { + const { getByTestId, UNSAFE_getByType } = render( + , + ); + + const container = getByTestId('endAdornment'); + expect(container).toBeTruthy(); + expect(UNSAFE_getByType(ActivityIndicator)).toBeTruthy(); + }); + + it('returns null when no loading and no endAdornment', () => { + const { toJSON } = render(); + expect(toJSON()).toBeNull(); + }); + + it('returns null when showLoadingIndicatorWhenFocused=true but not focused', () => { + const { toJSON } = render(); + expect(toJSON()).toBeNull(); + }); }); diff --git a/src/packages/react-native-material-elements/__test__/TextInputFormValidation01.test.tsx b/src/packages/react-native-material-elements/__test__/TextInputFormValidation01.test.tsx deleted file mode 100644 index 6dd8728..0000000 --- a/src/packages/react-native-material-elements/__test__/TextInputFormValidation01.test.tsx +++ /dev/null @@ -1,76 +0,0 @@ -import React from 'react'; -import { fireEvent, waitFor } from '@testing-library/react-native'; -import { TextInputFormValidation01 } from '../examples/form-validation/TextInputFormValidation01'; -import { render } from './test-utils'; - -describe('TextInputFormValidation01', () => { - const submitButtonLabel = 'Save'; - - beforeAll(() => { - jest.useFakeTimers(); - }); - - afterAll(() => { - jest.useRealTimers(); - }); - - it('renders input and button', () => { - const { getByPlaceholderText, getByText } = render(); - expect(getByPlaceholderText('Email')).toBeTruthy(); - expect(getByText(submitButtonLabel)).toBeTruthy(); - }); - - it('shows error message when email is empty', async () => { - const { getByText } = render(); - const saveButton = getByText(submitButtonLabel); - - fireEvent.press(saveButton, { nativeEvent: {} }); - - await waitFor(() => { - expect(getByText(/email/i)).toBeTruthy(); - }); - }); - - it('shows error message when email is invalid', async () => { - const { getByPlaceholderText, getByText } = render(); - const emailInput = getByPlaceholderText('Email'); - const saveButton = getByText(submitButtonLabel); - - fireEvent.changeText(emailInput, 'invalid-email'); - fireEvent.press(saveButton, { nativeEvent: {} }); - - await waitFor(() => { - expect(getByText(/email must be a valid/i)).toBeTruthy(); - }); - }); - - it('shows error message when email is invalid 2', async () => { - const { getByPlaceholderText, getByText } = render(); - const emailInput = getByPlaceholderText('Email'); - const saveButton = getByText(submitButtonLabel); - - fireEvent.changeText(emailInput, 123); - fireEvent.press(saveButton, { nativeEvent: {} }); - - await waitFor(() => { - expect(getByText(/email must be a valid/i)).toBeTruthy(); - }); - }); - - it('submits form when email is valid', async () => { - const consoleSpy = jest.spyOn(console, 'log').mockImplementation(() => {}); - const { getByPlaceholderText, getByText } = render(); - - const emailInput = getByPlaceholderText('Email'); - const saveButton = getByText(submitButtonLabel); - - fireEvent.changeText(emailInput, 'test@example.com'); - fireEvent.press(saveButton, { nativeEvent: {} }); - - await waitFor(() => { - expect(consoleSpy).toHaveBeenCalledWith({ email: 'test@example.com' }); - }); - - consoleSpy.mockRestore(); - }); -}); diff --git a/src/packages/react-native-material-elements/__test__/__snapshots__/Accordion.test.tsx.snap b/src/packages/react-native-material-elements/__test__/__snapshots__/Accordion.test.tsx.snap index 57da541..75932e2 100644 --- a/src/packages/react-native-material-elements/__test__/__snapshots__/Accordion.test.tsx.snap +++ b/src/packages/react-native-material-elements/__test__/__snapshots__/Accordion.test.tsx.snap @@ -1,4 +1,4 @@ -// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing +// Jest Snapshot v1, https://goo.gl/fbAQLP exports[`Accordion Component should render correctly 1`] = ` diff --git a/src/packages/react-native-material-elements/__test__/__snapshots__/Backdrop.test.tsx.snap b/src/packages/react-native-material-elements/__test__/__snapshots__/Backdrop.test.tsx.snap index c0cede1..ea33268 100644 --- a/src/packages/react-native-material-elements/__test__/__snapshots__/Backdrop.test.tsx.snap +++ b/src/packages/react-native-material-elements/__test__/__snapshots__/Backdrop.test.tsx.snap @@ -1,4 +1,4 @@ -// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing +// Jest Snapshot v1, https://goo.gl/fbAQLP exports[`Backdrop should render correctly 1`] = ` `; + +exports[`DropDownListContainer component should render properly with default props 1`] = ` + + + + + + + + + +`; + +exports[`DropDownListContainer component should render the search bar component 1`] = ` + + + + + + + + + + + + + + + + +`; diff --git a/src/packages/react-native-material-elements/__test__/__snapshots__/Grid.test.tsx.snap b/src/packages/react-native-material-elements/__test__/__snapshots__/Grid.test.tsx.snap index 798d05e..3165976 100644 --- a/src/packages/react-native-material-elements/__test__/__snapshots__/Grid.test.tsx.snap +++ b/src/packages/react-native-material-elements/__test__/__snapshots__/Grid.test.tsx.snap @@ -1,4 +1,4 @@ -// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing +// Jest Snapshot v1, https://goo.gl/fbAQLP exports[`Grid component should render correctly 1`] = ` diff --git a/src/packages/react-native-material-elements/__test__/__snapshots__/ImageList.test.tsx.snap b/src/packages/react-native-material-elements/__test__/__snapshots__/ImageList.test.tsx.snap index 0e2fb90..f4a9c6e 100644 --- a/src/packages/react-native-material-elements/__test__/__snapshots__/ImageList.test.tsx.snap +++ b/src/packages/react-native-material-elements/__test__/__snapshots__/ImageList.test.tsx.snap @@ -1,4 +1,4 @@ -// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing +// Jest Snapshot v1, https://goo.gl/fbAQLP exports[`ImageList Component should render correctly with default props 1`] = ` +`; diff --git a/src/packages/react-native-material-elements/__test__/__snapshots__/SegmentedControl.test.tsx.snap b/src/packages/react-native-material-elements/__test__/__snapshots__/SegmentedControl.test.tsx.snap new file mode 100644 index 0000000..4b6247c --- /dev/null +++ b/src/packages/react-native-material-elements/__test__/__snapshots__/SegmentedControl.test.tsx.snap @@ -0,0 +1,197 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`SegmentedControl component should render correctly with default props 1`] = ` + + + + + + + + + + +`; + +exports[`SegmentedControlContainer component should render correctly with default props 1`] = ` + +`; + +exports[`SegmentedControlItem component should render correctly with default props 1`] = ` + + + + + +`; diff --git a/src/packages/react-native-material-elements/__test__/__snapshots__/Snackbar.test.tsx.snap b/src/packages/react-native-material-elements/__test__/__snapshots__/Snackbar.test.tsx.snap new file mode 100644 index 0000000..55f3d0f --- /dev/null +++ b/src/packages/react-native-material-elements/__test__/__snapshots__/Snackbar.test.tsx.snap @@ -0,0 +1,3 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`snackbar utils should render with default props 1`] = `null`; diff --git a/src/packages/react-native-material-elements/__test__/__snapshots__/Stack.test.tsx.snap b/src/packages/react-native-material-elements/__test__/__snapshots__/Stack.test.tsx.snap index 0d5cad6..28a29d5 100644 --- a/src/packages/react-native-material-elements/__test__/__snapshots__/Stack.test.tsx.snap +++ b/src/packages/react-native-material-elements/__test__/__snapshots__/Stack.test.tsx.snap @@ -1,4 +1,4 @@ -// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing +// Jest Snapshot v1, https://goo.gl/fbAQLP exports[`Stack Component should render correctly 1`] = ` diff --git a/src/packages/react-native-material-elements/__test__/__snapshots__/Text.test.tsx.snap b/src/packages/react-native-material-elements/__test__/__snapshots__/Text.test.tsx.snap index 3e27089..c4bbf3b 100644 --- a/src/packages/react-native-material-elements/__test__/__snapshots__/Text.test.tsx.snap +++ b/src/packages/react-native-material-elements/__test__/__snapshots__/Text.test.tsx.snap @@ -1,4 +1,13 @@ -// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`FormHelperText component should render correct 1`] = ` + + Hello + +`; exports[`Text Component should match the snapshot with default props 1`] = ` `; diff --git a/src/packages/react-native-material-elements/__test__/createThemeDimensions.test.ts b/src/packages/react-native-material-elements/__test__/createThemeDimensions.test.ts index 44e33e5..3c15644 100644 --- a/src/packages/react-native-material-elements/__test__/createThemeDimensions.test.ts +++ b/src/packages/react-native-material-elements/__test__/createThemeDimensions.test.ts @@ -1,5 +1,5 @@ import { createThemeDimensions, themeDimensions } from '../src'; -import { CreateThemeDimensions } from '../src/libraries/themes/types'; +import { CreateThemeDimensions } from '../src/libraries/types'; describe('createThemeDimensions', () => { it('should merge custom and default theme dimensions', () => { diff --git a/src/packages/react-native-material-elements/__test__/styleGenerator.test.ts b/src/packages/react-native-material-elements/__test__/styleGenerator.test.ts index 6bcb53d..fda0d09 100644 --- a/src/packages/react-native-material-elements/__test__/styleGenerator.test.ts +++ b/src/packages/react-native-material-elements/__test__/styleGenerator.test.ts @@ -76,5 +76,15 @@ describe('Style Utilities', () => { expect(result).toEqual({}); consoleSpy.mockRestore(); }); + + it('should warn in the console if the styles property not valid', () => { + const consoleSpy = jest.spyOn(console, 'warn').mockImplementation(); + const args: StylePalette = { unknown: undefined } as any; + const result = generateElementStyles(args); + + expect(consoleSpy).toHaveBeenCalledWith('Invalid element property name: undefined, and value: undefined'); + expect(result).toEqual({}); + consoleSpy.mockRestore(); + }); }); }); diff --git a/src/packages/react-native-material-elements/__test__/useThemedProps.test.tsx b/src/packages/react-native-material-elements/__test__/useThemedProps.test.tsx index ed8f77a..4ec93a8 100644 --- a/src/packages/react-native-material-elements/__test__/useThemedProps.test.tsx +++ b/src/packages/react-native-material-elements/__test__/useThemedProps.test.tsx @@ -3,8 +3,8 @@ import React, { isValidElement } from 'react'; import { Text, View } from 'react-native'; import { useThemedProps } from '../src/hooks'; import { render, ThemeWrapper } from './test-utils'; -import { Theme } from '../src/libraries/themes/types'; import { green } from '../src'; +import { Theme } from '../src/libraries/types'; describe('useThemedProps', () => { it('should match icon component snapshot correctly', () => { diff --git a/src/packages/react-native-material-elements/__test__/utils.test.ts b/src/packages/react-native-material-elements/__test__/utils.test.ts index 49995c2..980a103 100644 --- a/src/packages/react-native-material-elements/__test__/utils.test.ts +++ b/src/packages/react-native-material-elements/__test__/utils.test.ts @@ -154,6 +154,11 @@ describe('merge utility', () => { const result = merge(componentStyles, themeComponentStyles); expect(result).toEqual({ backgroundColor: 'green', borderWidth: 10, borderRadius: 100, marginTop: 10, top: 200, right: 10 }); }); + + it('merges when both param1 and param2 are truthy non-array, non-object values', () => { + const result = merge('hello' as any, 123 as any); + expect(result).toEqual({ 0: 'h', 1: 'e', 2: 'l', 3: 'l', 4: 'o' }); + }); }); describe('getVariant', () => { diff --git a/src/packages/react-native-material-elements/babel.config.js b/src/packages/react-native-material-elements/babel.config.js deleted file mode 100644 index f7b3da3..0000000 --- a/src/packages/react-native-material-elements/babel.config.js +++ /dev/null @@ -1,3 +0,0 @@ -module.exports = { - presets: ['module:@react-native/babel-preset'], -}; diff --git a/src/packages/react-native-material-elements/examples/Badge.tsx b/src/packages/react-native-material-elements/examples/Badge.tsx index 5a8070f..c3a4531 100644 --- a/src/packages/react-native-material-elements/examples/Badge.tsx +++ b/src/packages/react-native-material-elements/examples/Badge.tsx @@ -125,14 +125,7 @@ export const Ex8: React.FC = () => { const { theme } = useTheme(); return ( - + { const { theme } = useTheme(); return ( - console.log(event), - }}> + setIndex(['One', 'Two', 'Three', 'Four', 'Five'].findIndex(e => e === value) + 1)} + onChange={(_, index) => setIndex(index)} /> diff --git a/src/packages/react-native-material-elements/examples/form-validation/TextInputFormValidation01.tsx b/src/packages/react-native-material-elements/examples/form-validation/TextInputFormValidation01.tsx deleted file mode 100644 index 5995566..0000000 --- a/src/packages/react-native-material-elements/examples/form-validation/TextInputFormValidation01.tsx +++ /dev/null @@ -1,56 +0,0 @@ -import { yupResolver } from '@hookform/resolvers/yup'; -import React from 'react'; -import { Controller, useForm } from 'react-hook-form'; -import * as Yup from 'yup'; -import { Box, Button, FormHelperText, TextField } from '../../src'; - -interface FormState { - readonly email: string; -} - -const validations = Yup.object().shape({ - email: Yup.string().email().required(), -}); - -export const TextInputFormValidation01 = function () { - const { - formState: { errors }, - control, - handleSubmit, - } = useForm({ - defaultValues: { - email: '', - }, - resolver: yupResolver(validations), - }); - - const submitHandler = function (data: FormState) { - console.log(data); - }; - - return ( - - ( - - - {errors?.email?.message ? ( - - {errors.email.message} - - ) : null} - - )} - /> -