diff --git a/.eslintrc.js b/.eslintrc.js index 187894b..94873ec 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -1,4 +1,14 @@ module.exports = { root: true, - extends: '@react-native', + extends: ['@react-native', 'plugin:@tanstack/query/recommended'], + overrides: [ + { + // Test files only + files: ['**/__tests__/**/*.[jt]s?(x)', '**/?(*.)+(spec|test).[jt]s?(x)'], + extends: ['plugin:testing-library/react'], + env: { + jest: true, + }, + }, + ], }; diff --git a/__tests__/App.test.tsx b/__tests__/App.test.tsx deleted file mode 100644 index d5d3244..0000000 --- a/__tests__/App.test.tsx +++ /dev/null @@ -1,17 +0,0 @@ -/** - * @format - */ - -import 'react-native'; -import React from 'react'; -import App from '../src/App.tsx'; - -// Note: import explicitly to use the types shipped with jest. -import {it} from '@jest/globals'; - -// Note: test renderer must be required after react-native. -import renderer from 'react-test-renderer'; - -it('renders correctly', () => { - renderer.create(); -}); diff --git a/jest.config.js b/jest.config.js index 8eb675e..65b2481 100644 --- a/jest.config.js +++ b/jest.config.js @@ -1,3 +1,12 @@ module.exports = { preset: 'react-native', + setupFilesAfterEnv: ['/jest.setup.ts'], + transform: { + '^.+\\.(js|jsx|ts|tsx)$': 'babel-jest', + }, + transformIgnorePatterns: [ + 'node_modules/(?!(react-native|@react-native|@react-navigation|@react-navigation/native-stack|query-string|decode-uri-component|filter-obj|split-on-first|react-native-reanimated|react-native-vector-icons)/)', + ], + testPathIgnorePatterns: ['/node_modules/'], + moduleFileExtensions: ['js', 'jsx', 'ts', 'tsx', 'json', 'node'], }; diff --git a/jest.setup.ts b/jest.setup.ts new file mode 100644 index 0000000..aa3bdb4 --- /dev/null +++ b/jest.setup.ts @@ -0,0 +1,16 @@ +import '@testing-library/react-native'; +import {setUpTests} from 'react-native-reanimated'; + +setUpTests(); + +jest.mock('@react-native-vector-icons/ionicons', () => 'MockedIonIcons'); +jest.mock('react-native-safe-area-context', () => { + const inset = {top: 0, right: 0, bottom: 0, left: 0}; + return { + SafeAreaProvider: jest.fn().mockImplementation(({children}) => children), + SafeAreaConsumer: jest + .fn() + .mockImplementation(({children}) => children(inset)), + useSafeAreaInsets: jest.fn().mockImplementation(() => inset), + }; +}); diff --git a/package-lock.json b/package-lock.json index faada5b..7d5c540 100644 --- a/package-lock.json +++ b/package-lock.json @@ -36,12 +36,17 @@ "@react-native/eslint-config": "0.76.5", "@react-native/metro-config": "0.76.5", "@react-native/typescript-config": "0.76.5", + "@testing-library/react-native": "^13.0.0", + "@types/jest": "^29.5.14", "@types/react": "^18.2.6", "@types/react-test-renderer": "^18.0.0", "babel-jest": "^29.6.3", "babel-plugin-module-resolver": "^5.0.2", "eslint": "^8.19.0", + "eslint-plugin-testing-library": "^7.1.1", + "identity-obj-proxy": "^3.0.0", "jest": "^29.6.3", + "mockdate": "^3.0.5", "prettier": "3.4.2", "react-test-renderer": "18.3.1", "typescript": "5.0.4" @@ -3738,6 +3743,67 @@ "react": "^18 || ^19" } }, + "node_modules/@testing-library/react-native": { + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/@testing-library/react-native/-/react-native-13.0.0.tgz", + "integrity": "sha512-NwwhIyMS+VD80KdDz9MS7iatCCgTGp+ZQd7EW+AKbJuWdMdDdopM042NLsp58haREzeFNAhVIPkm+1oSNw3Z5g==", + "dev": true, + "license": "MIT", + "dependencies": { + "jest-matcher-utils": "^29.7.0", + "pretty-format": "^29.7.0", + "redent": "^3.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "jest": ">=29.0.0", + "react": ">=18.2.0", + "react-native": ">=0.71", + "react-test-renderer": ">=18.2.0" + }, + "peerDependenciesMeta": { + "jest": { + "optional": true + } + } + }, + "node_modules/@testing-library/react-native/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@testing-library/react-native/node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@testing-library/react-native/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/babel__core": { "version": "7.20.5", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", @@ -3812,6 +3878,52 @@ "@types/istanbul-lib-report": "*" } }, + "node_modules/@types/jest": { + "version": "29.5.14", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.14.tgz", + "integrity": "sha512-ZN+4sdnLUbo8EVvVc2ao0GFW6oVrQRPn4K2lglySj7APvSrgzxHiNNK99us4WDMi57xxA2yggblIAMNhXOotLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "expect": "^29.0.0", + "pretty-format": "^29.0.0" + } + }, + "node_modules/@types/jest/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@types/jest/node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@types/jest/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/json-schema": { "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", @@ -6408,6 +6520,164 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/eslint-plugin-testing-library": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-testing-library/-/eslint-plugin-testing-library-7.1.1.tgz", + "integrity": "sha512-nszC833aZPwB6tik1nMkbFqmtgIXTT0sfJEYs0zMBKMlkQ4to2079yUV96SvmLh00ovSBJI4pgcBC1TiIP8mXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "^8.15.0", + "@typescript-eslint/utils": "^8.15.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0", + "pnpm": "^9.14.0" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0" + } + }, + "node_modules/eslint-plugin-testing-library/node_modules/@typescript-eslint/scope-manager": { + "version": "8.19.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.19.1.tgz", + "integrity": "sha512-60L9KIuN/xgmsINzonOcMDSB8p82h95hoBfSBtXuO4jlR1R9L1xSkmVZKgCPVfavDlXihh4ARNjXhh1gGnLC7Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.19.1", + "@typescript-eslint/visitor-keys": "8.19.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/eslint-plugin-testing-library/node_modules/@typescript-eslint/types": { + "version": "8.19.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.19.1.tgz", + "integrity": "sha512-JBVHMLj7B1K1v1051ZaMMgLW4Q/jre5qGK0Ew6UgXz1Rqh+/xPzV1aW581OM00X6iOfyr1be+QyW8LOUf19BbA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/eslint-plugin-testing-library/node_modules/@typescript-eslint/typescript-estree": { + "version": "8.19.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.19.1.tgz", + "integrity": "sha512-jk/TZwSMJlxlNnqhy0Eod1PNEvCkpY6MXOXE/WLlblZ6ibb32i2We4uByoKPv1d0OD2xebDv4hbs3fm11SMw8Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.19.1", + "@typescript-eslint/visitor-keys": "8.19.1", + "debug": "^4.3.4", + "fast-glob": "^3.3.2", + "is-glob": "^4.0.3", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^2.0.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <5.8.0" + } + }, + "node_modules/eslint-plugin-testing-library/node_modules/@typescript-eslint/utils": { + "version": "8.19.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.19.1.tgz", + "integrity": "sha512-IxG5gLO0Ne+KaUc8iW1A+XuKLd63o4wlbI1Zp692n1xojCl/THvgIKXJXBZixTh5dd5+yTJ/VXH7GJaaw21qXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.4.0", + "@typescript-eslint/scope-manager": "8.19.1", + "@typescript-eslint/types": "8.19.1", + "@typescript-eslint/typescript-estree": "8.19.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.8.0" + } + }, + "node_modules/eslint-plugin-testing-library/node_modules/@typescript-eslint/visitor-keys": { + "version": "8.19.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.19.1.tgz", + "integrity": "sha512-fzmjU8CHK853V/avYZAvuVut3ZTfwN5YtMaoi+X9Y9MA9keaWNHC3zEQ9zvyX/7Hj+5JkNyK1l7TOR2hevHB6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.19.1", + "eslint-visitor-keys": "^4.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/eslint-plugin-testing-library/node_modules/eslint-visitor-keys": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", + "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-plugin-testing-library/node_modules/semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/eslint-plugin-testing-library/node_modules/ts-api-utils": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.0.0.tgz", + "integrity": "sha512-xCt/TOAc+EOHS1XPnijD3/yzpH6qg2xppZO1YDqGoVsNXfQfzHpOdNuXwrwOU8u4ITXJyDCTyt8w5g1sZv9ynQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, "node_modules/eslint-scope": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", @@ -7372,6 +7642,13 @@ "dev": true, "license": "MIT" }, + "node_modules/harmony-reflect": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/harmony-reflect/-/harmony-reflect-1.6.2.tgz", + "integrity": "sha512-HIp/n38R9kQjDEziXyDTuW3vvoxxyxjxFzXLrBr18uB47GnSt+G9D29fqrpM5ZkspMcPICud3XsBJQ4Y2URg8g==", + "dev": true, + "license": "(Apache-2.0 OR MPL-1.1)" + }, "node_modules/has-bigints": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", @@ -7520,6 +7797,19 @@ "node": ">=10.17.0" } }, + "node_modules/identity-obj-proxy": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/identity-obj-proxy/-/identity-obj-proxy-3.0.0.tgz", + "integrity": "sha512-00n6YnVHKrinT9t0d9+5yZC6UBNJANpYEQvL2LlX6Ab9lnmxzIRcEmTPuyGScvl1+jKuCICX1Z0Ab1pPKKdikA==", + "dev": true, + "license": "MIT", + "dependencies": { + "harmony-reflect": "^1.4.6" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/ieee754": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", @@ -7621,6 +7911,16 @@ "node": ">=0.8.19" } }, + "node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/inflight": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", @@ -10236,6 +10536,16 @@ "node": ">=6" } }, + "node_modules/min-indent": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", + "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/minimatch": { "version": "9.0.5", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", @@ -10283,6 +10593,13 @@ "mkdirp": "bin/cmd.js" } }, + "node_modules/mockdate": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/mockdate/-/mockdate-3.0.5.tgz", + "integrity": "sha512-iniQP4rj1FhBdBYS/+eQv7j1tadJ9lJtdzgOpvsOHng/GbcDh2Fhdeq+ZRldrPYdXvCyfFUmFeEwEGXZB5I/AQ==", + "dev": true, + "license": "MIT" + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -11625,6 +11942,20 @@ "node": ">= 4" } }, + "node_modules/redent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", + "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "indent-string": "^4.0.0", + "strip-indent": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/reflect.getprototypeof": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", @@ -12685,6 +13016,19 @@ "node": ">=6" } }, + "node_modules/strip-indent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", + "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "min-indent": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/strip-json-comments": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", diff --git a/package.json b/package.json index 0764c0b..4116f00 100644 --- a/package.json +++ b/package.json @@ -38,12 +38,17 @@ "@react-native/eslint-config": "0.76.5", "@react-native/metro-config": "0.76.5", "@react-native/typescript-config": "0.76.5", + "@testing-library/react-native": "^13.0.0", + "@types/jest": "^29.5.14", "@types/react": "^18.2.6", "@types/react-test-renderer": "^18.0.0", "babel-jest": "^29.6.3", "babel-plugin-module-resolver": "^5.0.2", "eslint": "^8.19.0", + "eslint-plugin-testing-library": "^7.1.1", + "identity-obj-proxy": "^3.0.0", "jest": "^29.6.3", + "mockdate": "^3.0.5", "prettier": "3.4.2", "react-test-renderer": "18.3.1", "typescript": "5.0.4" diff --git a/src/components/Box/Box.spec.tsx b/src/components/Box/Box.spec.tsx new file mode 100644 index 0000000..65d7e61 --- /dev/null +++ b/src/components/Box/Box.spec.tsx @@ -0,0 +1,42 @@ +import {render} from '@testing-library/react-native'; +import {Box} from './Box'; +import {StyleSheet, Text} from 'react-native'; + +describe('Box', () => { + it('renders children correctly', () => { + const {getByText} = render( + + Test children + , + ); + expect(getByText('Test children')).toBeTruthy(); + }); + + it('renders title correctly', () => { + const {getByText} = render(); + expect(getByText('Test title')).toBeTruthy(); + }); + + it('renders footer correctly', () => { + const {getByText} = render(Test footer} />); + expect(getByText('Test footer')).toBeTruthy(); + }); + + it('pass style correctly', () => { + const style = { + backgroundColor: 'blue', + }; + + const {getByTestId} = render(); + expect(getByTestId('Box')).toHaveStyle(style); + }); + + it('pass contentStyle correctly', () => { + const contentStyle = { + backgroundColor: 'red', + }; + + const {getByTestId} = render(); + expect(getByTestId('Box.Content')).toHaveStyle(contentStyle); + }); +}); diff --git a/src/components/Box/Box.tsx b/src/components/Box/Box.tsx index fad4759..e91340b 100644 --- a/src/components/Box/Box.tsx +++ b/src/components/Box/Box.tsx @@ -18,11 +18,13 @@ export const Box: FC = ({ title, }) => { return ( - + {title} - {children} + + {children} + {Footer} ); diff --git a/src/components/ConditionsBox/ConditionsBox.spec.tsx b/src/components/ConditionsBox/ConditionsBox.spec.tsx new file mode 100644 index 0000000..43a0361 --- /dev/null +++ b/src/components/ConditionsBox/ConditionsBox.spec.tsx @@ -0,0 +1,41 @@ +import {render} from '@testing-library/react-native'; +import {ConditionsBox} from './ConditionsBox'; + +describe('ConditionsBox', () => { + it('renders footer text correctly', () => { + const {getByText} = render(); + expect(getByText('Test footer text')).toBeTruthy(); + }); + + it('renders title correctly', () => { + const {getByText} = render(); + expect(getByText('Test title')).toBeTruthy(); + }); + + it('pass style correctly', () => { + const style = { + backgroundColor: 'blue', + }; + + const {getByTestId} = render(); + expect(getByTestId('Box')).toHaveStyle(style); + }); + + describe('when iconName is set', () => { + it('renders Icon correctly', () => { + const {getByTestId} = render(); + expect(getByTestId('ConditionsBox.Icon')).toHaveProp( + 'name', + 'sunny-outline', + ); + expect(getByTestId('ConditionsBox.Icon')).toHaveProp('size', 48); + }); + }); + + describe('when iconName is not set', () => { + it('does not render Icon', () => { + const {queryByTestId} = render(); + expect(queryByTestId('ConditionsBox.Icon')).toBeFalsy(); + }); + }); +}); diff --git a/src/components/ConditionsBox/ConditionsBox.tsx b/src/components/ConditionsBox/ConditionsBox.tsx index ff648e9..60ef54e 100644 --- a/src/components/ConditionsBox/ConditionsBox.tsx +++ b/src/components/ConditionsBox/ConditionsBox.tsx @@ -1,5 +1,5 @@ import {Box, BoxProps} from '~/components/Box'; -import {WeatherConditionIconName} from '~/features/weather/constants.ts'; +import {WeatherConditionIconName} from '~/features/weather/constants'; import {FC} from 'react'; import Icon from '@react-native-vector-icons/ionicons'; import {theme} from '~/theme'; @@ -19,7 +19,13 @@ export const ConditionsBox: FC = ({ {footerText}} {...props}> - {iconName && } + {iconName && ( + + )} ); }; diff --git a/src/components/DaylightBox/DaylightBox.spec.tsx b/src/components/DaylightBox/DaylightBox.spec.tsx new file mode 100644 index 0000000..83cc071 --- /dev/null +++ b/src/components/DaylightBox/DaylightBox.spec.tsx @@ -0,0 +1,37 @@ +import {render} from '@testing-library/react-native'; +import {DaylightBox, DaylightBoxProps} from './DaylightBox'; +import MockDate from 'mockdate'; + +MockDate.set(1736424071); + +describe('DaylightBox', () => { + afterAll(() => { + MockDate.reset(); + }); + + const defaultProps: Partial = { + sunrise: new Date().valueOf(), + sunset: new Date().valueOf() + 3600, + }; + + it('renders sunrise correctly', () => { + const {getByText} = render(); + expect(getByText('Sunrise')).toBeTruthy(); + expect(getByText('13:01')).toBeTruthy(); + }); + + it('renders sunset correctly', () => { + const {getByText} = render(); + expect(getByText('Sunset')).toBeTruthy(); + expect(getByText('14:01')).toBeTruthy(); + }); + + it('pass style correctly', () => { + const style = { + backgroundColor: 'blue', + }; + + const {getByTestId} = render(); + expect(getByTestId('Box')).toHaveStyle(style); + }); +}); diff --git a/src/components/DaylightBox/DaylightBox.tsx b/src/components/DaylightBox/DaylightBox.tsx index 602de57..2cf4fcc 100644 --- a/src/components/DaylightBox/DaylightBox.tsx +++ b/src/components/DaylightBox/DaylightBox.tsx @@ -2,7 +2,7 @@ import {FC} from 'react'; import {Box, BoxProps} from '~/components/Box'; import {StyleSheet, Text, View} from 'react-native'; import {theme} from '~/theme'; -import {formatTime} from '~/utils/dateTime.ts'; +import {formatTime} from '~/utils/dateTime'; export interface DaylightBoxProps extends Pick { sunrise?: number; diff --git a/src/components/RefetchAnimatedIcon/index.ts b/src/components/RefetchAnimatedIcon/index.ts index 022f80a..3dc157a 100644 --- a/src/components/RefetchAnimatedIcon/index.ts +++ b/src/components/RefetchAnimatedIcon/index.ts @@ -1 +1 @@ -export * from './RefetchAnimatedIcon.tsx'; +export * from './RefetchAnimatedIcon'; diff --git a/src/components/ScreenHeader/ScreenHeader.spec.tsx b/src/components/ScreenHeader/ScreenHeader.spec.tsx new file mode 100644 index 0000000..8bb8a4f --- /dev/null +++ b/src/components/ScreenHeader/ScreenHeader.spec.tsx @@ -0,0 +1,91 @@ +import {ScreenHeader, ScreenHeaderProps} from './ScreenHeader'; +import {fireEvent, render} from '@testing-library/react-native'; +import {NativeStackNavigationProp} from '@react-navigation/native-stack'; +import {Route} from '@react-navigation/native'; +import {TestsProviders} from '~/utils/tests'; +import {Text} from 'react-native'; + +describe('ScreenHeader', () => { + const defaultProps: ScreenHeaderProps = { + navigation: { + goBack: () => {}, + } as NativeStackNavigationProp<{}>, + options: { + headerTitle: 'Test header title', + }, + route: {} as Route, + }; + + const renderComponent = (props?: Partial) => + render( + + + , + ); + + it('renders title correctly', () => { + const {getByText} = renderComponent(); + expect(getByText(defaultProps.options.headerTitle as string)).toBeTruthy(); + }); + + describe('when CenterContent is defined', () => { + const props: Partial = { + CenterContent: Test center content, + }; + + it('renders center content correctly', () => { + const {getByText} = renderComponent(props); + expect(getByText('Test center content')).toBeTruthy(); + }); + + it('does not render title', () => { + const {queryByText} = renderComponent(props); + expect( + queryByText(defaultProps.options.headerTitle as string), + ).toBeFalsy(); + }); + }); + + describe('when RightContent is defined', () => { + const props: Partial = { + RightContent: Test right content, + }; + + it('renders center content correctly', () => { + const {getByText} = renderComponent(props); + expect(getByText('Test right content')).toBeTruthy(); + }); + }); + + describe('when back is not defined', () => { + it('does not render back button', () => { + const {queryByTestId} = renderComponent(); + + expect(queryByTestId('ScreenHeader.BackButton')).toBeFalsy(); + }); + }); + + describe('when back is defined', () => { + const props: Partial = { + back: { + title: '', + href: '', + }, + }; + + it('renders back button', () => { + const {getByTestId} = renderComponent(props); + + expect(getByTestId('ScreenHeader.BackButton')).toBeTruthy(); + }); + + it('should call goBack on back button press', () => { + const goBackSpy = jest.spyOn(defaultProps.navigation, 'goBack'); + const {getByTestId} = renderComponent(props); + + fireEvent.press(getByTestId('ScreenHeader.BackButton')); + + expect(goBackSpy).toHaveBeenCalledTimes(1); + }); + }); +}); diff --git a/src/components/ScreenHeader/ScreenHeader.tsx b/src/components/ScreenHeader/ScreenHeader.tsx index ed2f767..875c619 100644 --- a/src/components/ScreenHeader/ScreenHeader.tsx +++ b/src/components/ScreenHeader/ScreenHeader.tsx @@ -35,7 +35,9 @@ export const ScreenHeader: FC = ({ ]}> {!!back && ( - navigation.goBack()}> + navigation.goBack()}> { + it('renders correctly', () => { + const {getByText} = render( + + Test children + , + ); + expect(getByText('Test children')).toBeTruthy(); + }); + + it('should call onPress on press', () => { + const onPressMock = jest.fn(); + const container = render(); + + fireEvent.press(container.root); + + expect(onPressMock).toHaveBeenCalledTimes(1); + }); +}); diff --git a/src/components/TemperatureBadge/TemperatureBadge.spec.tsx b/src/components/TemperatureBadge/TemperatureBadge.spec.tsx new file mode 100644 index 0000000..28ecc52 --- /dev/null +++ b/src/components/TemperatureBadge/TemperatureBadge.spec.tsx @@ -0,0 +1,32 @@ +import {render} from '@testing-library/react-native'; +import {TemperatureBadge, TemperatureBadgeProps} from './TemperatureBadge'; + +describe('TemperatureBadge', () => { + const defaultProps: TemperatureBadgeProps = { + value: 0, + unit: 'celsius', + }; + + it('renders temperatures correctly', () => { + const {getByText} = render(); + + expect(getByText('0°C')).toBeTruthy(); + }); + + describe('when unit equals fahrenheit', () => { + it('renders temperatures correctly', () => { + const {getByText} = render( + , + ); + + expect(getByText('0°F')).toBeTruthy(); + }); + }); + + describe('when temperature is undefined ', () => { + it('renders temperature placeholder', () => { + const {getByText} = render(); + expect(getByText('-°C')).toBeTruthy(); + }); + }); +}); diff --git a/src/components/TemperatureBadge/TemperatureBadge.tsx b/src/components/TemperatureBadge/TemperatureBadge.tsx index 993ec9d..8065538 100644 --- a/src/components/TemperatureBadge/TemperatureBadge.tsx +++ b/src/components/TemperatureBadge/TemperatureBadge.tsx @@ -1,6 +1,7 @@ import {FC} from 'react'; import {Text} from 'react-native'; -import {TemperatureUnit} from '~/types/units.ts'; +import {TemperatureUnit} from '~/types/units'; +import {TemperatureUnitCharMap} from '~/constants/units'; export interface TemperatureBadgeProps { value?: number; @@ -11,7 +12,7 @@ export const TemperatureBadge: FC = ({value, unit}) => { return ( {typeof value !== 'undefined' ? Math.round(value) : '-'} - {unit === 'celsius' ? '°C' : '°F'} + {TemperatureUnitCharMap[unit]} ); }; diff --git a/src/components/TemperatureBox/TemperatureBox.spec.tsx b/src/components/TemperatureBox/TemperatureBox.spec.tsx new file mode 100644 index 0000000..501ce2b --- /dev/null +++ b/src/components/TemperatureBox/TemperatureBox.spec.tsx @@ -0,0 +1,43 @@ +import {render} from '@testing-library/react-native'; +import {TemperatureBox, TemperatureBoxProps} from './TemperatureBox'; + +describe('TemperatureBox', () => { + const defaultProps: TemperatureBoxProps = { + min: 10, + max: 15, + value: 12, + feelsLike: 11, + unit: 'celsius', + }; + + it('renders temperatures correctly', () => { + const {getByText} = render(); + + expect(getByText('10°C')).toBeTruthy(); + expect(getByText('15°C')).toBeTruthy(); + expect(getByText('12°C')).toBeTruthy(); + expect(getByText('11°C')).toBeTruthy(); + }); + + describe('when unit equals fahrenheit', () => { + it('renders temperatures correctly', () => { + const {getByText} = render( + , + ); + + expect(getByText('10°F')).toBeTruthy(); + expect(getByText('15°F')).toBeTruthy(); + expect(getByText('12°F')).toBeTruthy(); + expect(getByText('11°F')).toBeTruthy(); + }); + }); + + describe('when temperature is undefined ', () => { + it('renders temperature placeholder', () => { + const {getAllByText} = render( + , + ); + expect(getAllByText('-°C')).toHaveLength(4); + }); + }); +}); diff --git a/src/components/TemperatureBox/TemperatureBox.tsx b/src/components/TemperatureBox/TemperatureBox.tsx index ea6f00e..67e4c56 100644 --- a/src/components/TemperatureBox/TemperatureBox.tsx +++ b/src/components/TemperatureBox/TemperatureBox.tsx @@ -1,9 +1,10 @@ import {StyleProp, StyleSheet, Text, View, ViewStyle} from 'react-native'; import {FC, useMemo} from 'react'; import {theme} from '~/theme'; -import {TemperatureUnit} from '~/types/units.ts'; +import {TemperatureUnit} from '~/types/units'; import Icon from '@react-native-vector-icons/ionicons'; import {Box} from '~/components/Box'; +import {TemperatureUnitCharMap} from '~/constants/units'; export interface TemperatureBoxProps { min?: number; @@ -19,7 +20,7 @@ export const TemperatureBox: FC = ({ unit, ...props }) => { - const unitChar = unit === 'celsius' ? '°C' : '°F'; + const unitChar = TemperatureUnitCharMap[unit]; const [value, min, max, feelsLike] = useMemo( () => [props.value, props.min, props.max, props.feelsLike] diff --git a/src/components/WindBox/WindBox.spec.tsx b/src/components/WindBox/WindBox.spec.tsx new file mode 100644 index 0000000..9b92291 --- /dev/null +++ b/src/components/WindBox/WindBox.spec.tsx @@ -0,0 +1,26 @@ +import {render} from '@testing-library/react-native'; +import {WindBox, WindBoxProps} from './WindBox'; + +describe('WindBox', () => { + const defaultProps: WindBoxProps = { + speed: 120, + deg: 230, + unit: 'metric', + }; + + it('renders data correctly', () => { + const {getByText} = render(); + + expect(getByText('120m/s')).toBeTruthy(); + expect(getByText('230°')).toBeTruthy(); + }); + + describe('when unit equals fahrenheit', () => { + it('renders data correctly', () => { + const {getByText} = render(); + + expect(getByText('120mph')).toBeTruthy(); + expect(getByText('230°')).toBeTruthy(); + }); + }); +}); diff --git a/src/components/WindBox/WindBox.tsx b/src/components/WindBox/WindBox.tsx index 336f4bc..b234551 100644 --- a/src/components/WindBox/WindBox.tsx +++ b/src/components/WindBox/WindBox.tsx @@ -4,7 +4,7 @@ import {FC} from 'react'; import Icon from '@react-native-vector-icons/ionicons'; import {theme} from '~/theme'; import {StyleSheet, Text, View} from 'react-native'; -import {MeasurementSystemWindSpeedUnitMap} from '~/constants/units.ts'; +import {MeasurementSystemWindSpeedUnitMap} from '~/constants/units'; export interface WindBoxProps extends Pick { speed?: number; @@ -27,7 +27,7 @@ export const WindBox: FC = ({speed, deg, unit, style}) => { color={theme.token.icon.secondary} size={theme.getSize(5)} /> - + {speed} {speedUnit} diff --git a/src/constants/units.ts b/src/constants/units.ts index 4a6585f..7b8b242 100644 --- a/src/constants/units.ts +++ b/src/constants/units.ts @@ -1,8 +1,4 @@ -import { - MeasurementSystem, - TemperatureUnit, - WindSpeedUnit, -} from '~/types/units.ts'; +import {MeasurementSystem, TemperatureUnit, WindSpeedUnit} from '~/types/units'; export const MeasurementSystemTemperatureUnitMap: Record< MeasurementSystem, @@ -19,3 +15,8 @@ export const MeasurementSystemWindSpeedUnitMap: Record< metric: 'm/s', imperial: 'mph', }; + +export const TemperatureUnitCharMap: Record = { + celsius: '°C', + fahrenheit: '°F', +}; diff --git a/src/features/cities/components/CitiesLike/CitiesLike.spec.tsx b/src/features/cities/components/CitiesLike/CitiesLike.spec.tsx new file mode 100644 index 0000000..53aa2d9 --- /dev/null +++ b/src/features/cities/components/CitiesLike/CitiesLike.spec.tsx @@ -0,0 +1,25 @@ +import {fireEvent, render} from '@testing-library/react-native'; +import {CitiesLike} from '~/features/cities/components/CitiesLike/CitiesLike'; +import {useCitiesLikes} from '~/features/cities/hooks/useCitiesLikes'; + +describe('CitiesLike', () => { + const cityId = 123; + + it('renders correctly when city is not liked', () => { + const {getByTestId} = render(); + expect(getByTestId('CitiesLike.Icon')).toHaveProp('name', 'heart-outline'); + }); + + it('toggle city like on press', () => { + const {getByTestId, root} = render(); + + expect(getByTestId('CitiesLike.Icon')).toHaveProp('name', 'heart-outline'); + fireEvent.press(root); + + expect(useCitiesLikes.getState().liked).toStrictEqual({[cityId]: true}); + expect(getByTestId('CitiesLike.Icon')).toHaveProp( + 'name', + 'heart-dislike-outline', + ); + }); +}); diff --git a/src/features/cities/components/CitiesLike/CitiesLike.tsx b/src/features/cities/components/CitiesLike/CitiesLike.tsx index dc543d8..29c29e7 100644 --- a/src/features/cities/components/CitiesLike/CitiesLike.tsx +++ b/src/features/cities/components/CitiesLike/CitiesLike.tsx @@ -16,6 +16,7 @@ export const CitiesLike: FC = ({cityId}) => { onPress={toggle} hitSlop={theme.getSize(4)}> diff --git a/src/features/cities/hooks/useCitiesLike.ts b/src/features/cities/hooks/useCitiesLike.ts index 8a224cd..288b526 100644 --- a/src/features/cities/hooks/useCitiesLike.ts +++ b/src/features/cities/hooks/useCitiesLike.ts @@ -5,7 +5,7 @@ export const useCitiesLike = (cityId: number) => { const toggle = useCitiesLikes(state => state.toggleCityLike); return { - liked: liked, + liked, toggle: () => toggle(cityId), }; }; diff --git a/src/features/cities/hooks/useCitiesLikes.spec.ts b/src/features/cities/hooks/useCitiesLikes.spec.ts new file mode 100644 index 0000000..e60be4b --- /dev/null +++ b/src/features/cities/hooks/useCitiesLikes.spec.ts @@ -0,0 +1,28 @@ +import {useCitiesLikes} from '~/features/cities/hooks/useCitiesLikes'; +import {renderHook, act} from '@testing-library/react-native'; + +describe('useCitiesLikes', () => { + it('has correct initial state', () => { + const {result} = renderHook(() => useCitiesLikes()); + expect(result.current).toStrictEqual({ + liked: {}, + toggleCityLike: expect.any(Function), + }); + }); + + it('toggles city like', () => { + const {result} = renderHook(() => useCitiesLikes()); + + expect(result.current.liked).toStrictEqual({}); + + act(() => result.current.toggleCityLike(123)); + expect(result.current.liked).toStrictEqual({ + 123: true, + }); + + act(() => result.current.toggleCityLike(123)); + expect(result.current.liked).toStrictEqual({ + 123: false, + }); + }); +}); diff --git a/src/features/cities/hooks/useCitiesLikesArray.spec.ts b/src/features/cities/hooks/useCitiesLikesArray.spec.ts new file mode 100644 index 0000000..96dc66b --- /dev/null +++ b/src/features/cities/hooks/useCitiesLikesArray.spec.ts @@ -0,0 +1,25 @@ +import {renderHook, act} from '@testing-library/react-native'; +import {useCitiesLikesArray} from './useCitiesLikesArray'; +import {useCitiesLikes} from './useCitiesLikes'; + +describe('useCitiesLikes', () => { + it('returns liked cities array', () => { + const {result} = renderHook(() => useCitiesLikesArray()); + + expect(result.current).toStrictEqual([]); + + act(() => + useCitiesLikes.setState({ + liked: { + 1: true, + 2: true, + 3: true, + 4: false, + 5: false, + }, + }), + ); + + expect(result.current).toStrictEqual([1, 2, 3]); + }); +}); diff --git a/src/features/cities/hooks/useCititesLike.spec.ts b/src/features/cities/hooks/useCititesLike.spec.ts new file mode 100644 index 0000000..741ed21 --- /dev/null +++ b/src/features/cities/hooks/useCititesLike.spec.ts @@ -0,0 +1,22 @@ +import {renderHook, act} from '@testing-library/react-native'; +import {useCitiesLike} from '~/features/cities/hooks/useCitiesLike'; + +describe('useCitiesLike', () => { + const cityId = 123; + it('returns correct values', () => { + const {result} = renderHook(() => useCitiesLike(cityId)); + expect(result.current.liked).toBeFalsy(); + }); + + it('returns correct values when toggle', () => { + const {result} = renderHook(() => useCitiesLike(cityId)); + + expect(result.current.liked).toBeFalsy(); + + act(() => result.current.toggle()); + expect(result.current.liked).toBeTruthy(); + + act(() => result.current.toggle()); + expect(result.current.liked).toBeFalsy(); + }); +}); diff --git a/src/features/weather/components/WeatherCityDaylightBox/index.ts b/src/features/weather/components/WeatherCityDaylightBox/index.ts index 40d3e26..aadf0a2 100644 --- a/src/features/weather/components/WeatherCityDaylightBox/index.ts +++ b/src/features/weather/components/WeatherCityDaylightBox/index.ts @@ -1 +1 @@ -export * from './WeatherCityDaylightBox.tsx'; +export * from './WeatherCityDaylightBox'; diff --git a/src/features/weather/components/WeatherCityListItem/WeatherCityListItem.tsx b/src/features/weather/components/WeatherCityListItem/WeatherCityListItem.tsx index 3cb709e..b840eba 100644 --- a/src/features/weather/components/WeatherCityListItem/WeatherCityListItem.tsx +++ b/src/features/weather/components/WeatherCityListItem/WeatherCityListItem.tsx @@ -4,7 +4,7 @@ import {FC, useMemo} from 'react'; import {WeatherCityTemperatureBadge} from '../WeatherCityTemperatureBadge'; import {StyleSheet, View} from 'react-native'; import {theme} from '~/theme'; -import {getWeatherConditionsIconName} from '~/features/weather/utils.ts'; +import {getWeatherConditionsIconName} from '~/features/weather/utils'; export interface WeatherCityListItemProps { cityId: number; diff --git a/src/features/weather/components/WeatherCityTemperatureBadge/WeatherCityTemperatureBadge.tsx b/src/features/weather/components/WeatherCityTemperatureBadge/WeatherCityTemperatureBadge.tsx index ee23cc8..dd82813 100644 --- a/src/features/weather/components/WeatherCityTemperatureBadge/WeatherCityTemperatureBadge.tsx +++ b/src/features/weather/components/WeatherCityTemperatureBadge/WeatherCityTemperatureBadge.tsx @@ -2,7 +2,7 @@ import {useWeatherUnit} from '../../hooks/useWeatherUnit'; import {TemperatureBadge} from '~/components/TemperatureBadge'; import {FC} from 'react'; import {MeasurementSystemTemperatureUnitMap} from '~/constants/units'; -import {useWeatherCity} from '~/features/weather/hooks/useWeatherCity.ts'; +import {useWeatherCity} from '~/features/weather/hooks/useWeatherCity'; export interface WeatherTemperatureBadgeProps { cityId: number; diff --git a/src/features/weather/components/WeatherCityTemperatureBadge/index.ts b/src/features/weather/components/WeatherCityTemperatureBadge/index.ts index 8738b79..9d620c1 100644 --- a/src/features/weather/components/WeatherCityTemperatureBadge/index.ts +++ b/src/features/weather/components/WeatherCityTemperatureBadge/index.ts @@ -1 +1 @@ -export * from './WeatherCityTemperatureBadge.tsx'; +export * from './WeatherCityTemperatureBadge'; diff --git a/src/features/weather/components/WeatherCityTemperatureBox/WeatherCityTemperatureBox.tsx b/src/features/weather/components/WeatherCityTemperatureBox/WeatherCityTemperatureBox.tsx index ceecf0b..b893e50 100644 --- a/src/features/weather/components/WeatherCityTemperatureBox/WeatherCityTemperatureBox.tsx +++ b/src/features/weather/components/WeatherCityTemperatureBox/WeatherCityTemperatureBox.tsx @@ -2,7 +2,7 @@ import {useWeatherUnit} from '~/features/weather/hooks/useWeatherUnit'; import {FC} from 'react'; import {TemperatureBox, TemperatureBoxProps} from '~/components/TemperatureBox'; import {MeasurementSystemTemperatureUnitMap} from '~/constants/units'; -import {useWeatherCity} from '~/features/weather/hooks/useWeatherCity.ts'; +import {useWeatherCity} from '~/features/weather/hooks/useWeatherCity'; export interface WeatherTemperatureBoxProps extends Pick { diff --git a/src/features/weather/components/WeatherCityWindBox/WeatherCityWindBox.tsx b/src/features/weather/components/WeatherCityWindBox/WeatherCityWindBox.tsx index 2069b50..6cc7044 100644 --- a/src/features/weather/components/WeatherCityWindBox/WeatherCityWindBox.tsx +++ b/src/features/weather/components/WeatherCityWindBox/WeatherCityWindBox.tsx @@ -1,7 +1,7 @@ import {WindBox, WindBoxProps} from '~/components/WindBox'; -import {useWeatherUnit} from '~/features/weather/hooks/useWeatherUnit.ts'; +import {useWeatherUnit} from '~/features/weather/hooks/useWeatherUnit'; import {FC} from 'react'; -import {useWeatherCity} from '~/features/weather/hooks/useWeatherCity.ts'; +import {useWeatherCity} from '~/features/weather/hooks/useWeatherCity'; export interface WeatherCityWindBoxProps extends Pick { cityId: number; diff --git a/src/features/weather/hooks/useWeatherCity.ts b/src/features/weather/hooks/useWeatherCity.ts index 45c414e..2535993 100644 --- a/src/features/weather/hooks/useWeatherCity.ts +++ b/src/features/weather/hooks/useWeatherCity.ts @@ -1,7 +1,7 @@ import {useWeatherUnit} from './useWeatherUnit'; import {useQuery} from '@tanstack/react-query'; import {OpenWeatherApi} from '~/services/openWeatherApi'; -import {MeasurementSystem} from '~/types/units.ts'; +import {MeasurementSystem} from '~/types/units'; type UseWeatherCityQueryKey = [string, number, MeasurementSystem]; diff --git a/src/features/weather/hooks/useWeatherUnit.spec.ts b/src/features/weather/hooks/useWeatherUnit.spec.ts new file mode 100644 index 0000000..8218165 --- /dev/null +++ b/src/features/weather/hooks/useWeatherUnit.spec.ts @@ -0,0 +1,28 @@ +import {renderHook, act} from '@testing-library/react-native'; +import {useWeatherUnit} from './useWeatherUnit'; + +describe('useWeatherUnit', () => { + it('returns correct default values', () => { + const {result} = renderHook(() => useWeatherUnit()); + expect(result.current).toEqual({ + unit: 'metric', + setImperial: expect.any(Function), + setMetric: expect.any(Function), + }); + }); + + it('sets unit to imperial', () => { + const {result} = renderHook(() => useWeatherUnit()); + act(() => result.current.setImperial()); + expect(result.current.unit).toBe('imperial'); + }); + + it('sets unit to metric', () => { + const {result} = renderHook(() => useWeatherUnit()); + + act(() => result.current.setImperial()); + expect(result.current.unit).toBe('imperial'); + act(() => result.current.setMetric()); + expect(result.current.unit).toBe('metric'); + }); +}); diff --git a/src/features/weather/hooks/useWeatherUnit.ts b/src/features/weather/hooks/useWeatherUnit.ts index e6107ce..26eb034 100644 --- a/src/features/weather/hooks/useWeatherUnit.ts +++ b/src/features/weather/hooks/useWeatherUnit.ts @@ -1,5 +1,5 @@ import {create} from 'zustand'; -import {MeasurementSystem} from '~/types/units.ts'; +import {MeasurementSystem} from '~/types/units'; export interface WeatherUnitStore { unit: MeasurementSystem; diff --git a/src/features/weather/utils.ts b/src/features/weather/utils.ts index a8b3e35..0e59599 100644 --- a/src/features/weather/utils.ts +++ b/src/features/weather/utils.ts @@ -1,7 +1,7 @@ import { WeatherConditionIdPrefixes, WeatherConditionsIconMap, -} from '~/features/weather/constants.ts'; +} from '~/features/weather/constants'; export const getWeatherConditionsIconName = (conditionsId?: number) => { const matchingId = WeatherConditionIdPrefixes.find(i => diff --git a/src/screens/DetailsScreen/DetailsScreen.tsx b/src/screens/DetailsScreen/DetailsScreen.tsx index 0d2c987..5a3c005 100644 --- a/src/screens/DetailsScreen/DetailsScreen.tsx +++ b/src/screens/DetailsScreen/DetailsScreen.tsx @@ -1,11 +1,11 @@ -import {useWeatherCity} from '~/features/weather/hooks/useWeatherCity.ts'; -import {DetailScreenProps} from '~/screens/DetailsScreen/DetailsScreen.types.ts'; +import {useWeatherCity} from '~/features/weather/hooks/useWeatherCity'; +import {DetailScreenProps} from '~/screens/DetailsScreen/DetailsScreen.types'; import {FC} from 'react'; import {Animated, StyleSheet, View} from 'react-native'; import ScrollView = Animated.ScrollView; import {theme} from '~/theme'; import {useSafeAreaInsets} from 'react-native-safe-area-context'; -import {WeatherCityTemperatureBox} from '~/features/weather/components/WeatherCityTemperatureBox/WeatherCityTemperatureBox.tsx'; +import {WeatherCityTemperatureBox} from '~/features/weather/components/WeatherCityTemperatureBox/WeatherCityTemperatureBox'; import {WeatherCityConditionsBox} from '~/features/weather/components/WeatherCityConditionsBox'; import {WeatherCityWindBox} from '~/features/weather/components/WeatherCityWindBox'; import {WeatherCityAirBox} from '~/features/weather/components/WeatherCityAirBox'; diff --git a/src/screens/DetailsScreen/DetailsScreenHeader.tsx b/src/screens/DetailsScreen/DetailsScreenHeader.tsx index 5bf3ddf..68f1902 100644 --- a/src/screens/DetailsScreen/DetailsScreenHeader.tsx +++ b/src/screens/DetailsScreen/DetailsScreenHeader.tsx @@ -1,7 +1,7 @@ import {FC} from 'react'; import {ScreenHeader, ScreenHeaderProps} from '~/components/ScreenHeader'; import {RootStackParamsList} from '~/navigation/RootStack'; -import {useWeatherCity} from '~/features/weather/hooks/useWeatherCity.ts'; +import {useWeatherCity} from '~/features/weather/hooks/useWeatherCity'; import {StyleSheet, Text, View} from 'react-native'; import {theme} from '~/theme'; import {ScreenHeaderButton} from '~/components/ScreenHeaderButton'; diff --git a/src/services/openWeatherApi.ts b/src/services/openWeatherApi.ts index 03503c3..956f3c2 100644 --- a/src/services/openWeatherApi.ts +++ b/src/services/openWeatherApi.ts @@ -1,6 +1,6 @@ import {OPEN_WEATHER_API_KEY} from '@env'; import qs from 'query-string'; -import {MeasurementSystem} from '../types/units.ts'; +import {MeasurementSystem} from '../types/units'; export interface OpenWeatherCityWeather { id: number; diff --git a/src/utils/tests.tsx b/src/utils/tests.tsx new file mode 100644 index 0000000..df114ef --- /dev/null +++ b/src/utils/tests.tsx @@ -0,0 +1,6 @@ +import {FC, ReactNode} from 'react'; +import {SafeAreaProvider} from 'react-native-safe-area-context'; + +export const TestsProviders: FC<{children: ReactNode}> = ({children}) => ( + {children} +); diff --git a/tsconfig.json b/tsconfig.json index 07ff694..a58ff62 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,6 +1,7 @@ { "extends": "@react-native/typescript-config/tsconfig.json", "compilerOptions": { + "types": ["jest", "node"], "paths": { "~/*": ["./src/*"] }