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/*"]
}