diff --git a/__tests__/Index-test.tsx b/__tests__/Index-test.tsx
deleted file mode 100644
index c402fe3..0000000
--- a/__tests__/Index-test.tsx
+++ /dev/null
@@ -1,11 +0,0 @@
-import { render } from "@testing-library/react-native";
-
-import Index from "@/src/app/index";
-
-describe("", () => {
- test("Text renders correctly on Index", () => {
- const { getByText } = render();
-
- getByText("Scriptian Browser");
- });
-});
diff --git a/babel.config.js b/babel.config.js
new file mode 100644
index 0000000..5ee9fc9
--- /dev/null
+++ b/babel.config.js
@@ -0,0 +1,6 @@
+module.exports = function (api) {
+ api.cache(true);
+ return {
+ presets: ['babel-preset-expo'],
+ };
+};
diff --git a/eslint.config.js b/eslint.config.js
index 5025da6..d869024 100644
--- a/eslint.config.js
+++ b/eslint.config.js
@@ -7,4 +7,19 @@ module.exports = defineConfig([
{
ignores: ['dist/*'],
},
+ {
+ files: ['**/__tests__/**/*', '**/*.test.*', '**/*.spec.*', '**/jest-setup.js'],
+ languageOptions: {
+ globals: {
+ jest: 'readonly',
+ describe: 'readonly',
+ it: 'readonly',
+ expect: 'readonly',
+ beforeEach: 'readonly',
+ afterEach: 'readonly',
+ beforeAll: 'readonly',
+ afterAll: 'readonly',
+ },
+ },
+ },
]);
diff --git a/jest-setup.js b/jest-setup.js
new file mode 100644
index 0000000..1570c4d
--- /dev/null
+++ b/jest-setup.js
@@ -0,0 +1,24 @@
+import 'react-native-gesture-handler/jestSetup';
+
+// Mock react-native-reanimated
+jest.mock('react-native-reanimated', () => {
+ const Reanimated = require('react-native-reanimated/mock');
+ Reanimated.default.call = () => { };
+ return Reanimated;
+});
+
+// Mock AsyncStorage
+jest.mock('@react-native-async-storage/async-storage', () =>
+ require('@react-native-async-storage/async-storage/jest/async-storage-mock')
+);
+
+// Mock Animated to silence warnings
+jest.mock('react-native/Libraries/Animated/AnimatedImplementation', () => 'Animated');
+
+// Additional React Native mocks for Expo
+jest.mock('expo-constants', () => ({
+ default: {
+ deviceId: 'test-device-id',
+ experienceUrl: 'exp://test.com',
+ },
+}));
\ No newline at end of file
diff --git a/jest.config.js b/jest.config.js
new file mode 100644
index 0000000..0c0af25
--- /dev/null
+++ b/jest.config.js
@@ -0,0 +1,15 @@
+module.exports = {
+ preset: 'jest-expo',
+ setupFilesAfterEnv: ['/jest-setup.js'],
+ transformIgnorePatterns: [
+ "node_modules/(?!((jest-)?react-native|@react-native(-community)?)|expo(nent)?|@expo(nent)?/.*|@expo-google-fonts/.*|react-navigation|@react-navigation/.*|@sentry/react-native|native-base|react-native-svg)"
+ ],
+ testEnvironment: 'jsdom',
+ moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json'],
+ transform: {
+ '^.+\\.(js|jsx|ts|tsx)$': 'babel-jest',
+ },
+ testPathIgnorePatterns: [
+ '/__tests__/utils/', // ignores any __tests__/utils folder
+ ],
+};
\ No newline at end of file
diff --git a/package-lock.json b/package-lock.json
index 4100082..072727b 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -9,7 +9,6 @@
"version": "0.0.1",
"dependencies": {
"@expo/vector-icons": "^15.0.2",
- "@react-native-async-storage/async-storage": "^2.2.0",
"@react-navigation/bottom-tabs": "^7.4.0",
"@react-navigation/elements": "^2.6.3",
"@react-navigation/native": "^7.1.8",
@@ -41,14 +40,18 @@
"redux-persist": "^6.0.0"
},
"devDependencies": {
+ "@babel/preset-typescript": "^7.27.1",
+ "@react-native-async-storage/async-storage": "^2.2.0",
"@testing-library/react-native": "^13.3.3",
- "@types/jest": "29.5.14",
+ "@types/jest": "^29.5.14",
"@types/react": "~19.1.0",
"@types/react-native": "^0.72.8",
+ "babel-jest": "^30.2.0",
"eslint": "^9.25.0",
"eslint-config-expo": "~10.0.0",
"jest": "~29.7.0",
"jest-expo": "~54.0.12",
+ "ts-jest": "^29.4.5",
"typescript": "~5.9.2"
}
},
@@ -2679,6 +2682,30 @@
"node": "^14.15.0 || ^16.10.0 || >=18.0.0"
}
},
+ "node_modules/@jest/pattern": {
+ "version": "30.0.1",
+ "resolved": "https://registry.npmjs.org/@jest/pattern/-/pattern-30.0.1.tgz",
+ "integrity": "sha512-gWp7NfQW27LaBQz3TITS8L7ZCQ0TLvtmI//4OwlQRx4rnWxcPNIYjxZpDcN4+UlGxgm3jS5QPz8IPTCkb59wZA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "*",
+ "jest-regex-util": "30.0.1"
+ },
+ "engines": {
+ "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
+ }
+ },
+ "node_modules/@jest/pattern/node_modules/jest-regex-util": {
+ "version": "30.0.1",
+ "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-30.0.1.tgz",
+ "integrity": "sha512-jHEQgBXAgc+Gh4g0p3bCevgRCVRkB4VB70zhoAE48gxeSr1hfUOsM/C2WoJgVL7Eyg//hudYENbm3Ne+/dRVVA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
+ }
+ },
"node_modules/@jest/reporters": {
"version": "29.7.0",
"resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-29.7.0.tgz",
@@ -3207,6 +3234,7 @@
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/@react-native-async-storage/async-storage/-/async-storage-2.2.0.tgz",
"integrity": "sha512-gvRvjR5JAaUZF8tv2Kcq/Gbt3JHwbKFYfmb445rhOj6NUMx3qPLixmDx5pZAyb9at1bYvJ4/eTUipU5aki45xw==",
+ "dev": true,
"license": "MIT",
"dependencies": {
"merge-options": "^3.0.4"
@@ -5228,24 +5256,270 @@
}
},
"node_modules/babel-jest": {
- "version": "29.7.0",
- "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz",
- "integrity": "sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==",
+ "version": "30.2.0",
+ "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-30.2.0.tgz",
+ "integrity": "sha512-0YiBEOxWqKkSQWL9nNGGEgndoeL0ZpWrbLMNL5u/Kaxrli3Eaxlt3ZtIDktEvXt4L/R9r3ODr2zKwGM/2BjxVw==",
+ "dev": true,
"license": "MIT",
"dependencies": {
- "@jest/transform": "^29.7.0",
- "@types/babel__core": "^7.1.14",
- "babel-plugin-istanbul": "^6.1.1",
- "babel-preset-jest": "^29.6.3",
- "chalk": "^4.0.0",
- "graceful-fs": "^4.2.9",
+ "@jest/transform": "30.2.0",
+ "@types/babel__core": "^7.20.5",
+ "babel-plugin-istanbul": "^7.0.1",
+ "babel-preset-jest": "30.2.0",
+ "chalk": "^4.1.2",
+ "graceful-fs": "^4.2.11",
"slash": "^3.0.0"
},
"engines": {
- "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
},
"peerDependencies": {
- "@babel/core": "^7.8.0"
+ "@babel/core": "^7.11.0 || ^8.0.0-0"
+ }
+ },
+ "node_modules/babel-jest/node_modules/@jest/schemas": {
+ "version": "30.0.5",
+ "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.0.5.tgz",
+ "integrity": "sha512-DmdYgtezMkh3cpU8/1uyXakv3tJRcmcXxBOcO0tbaozPwpmh4YMsnWrQm9ZmZMfa5ocbxzbFk6O4bDPEc/iAnA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@sinclair/typebox": "^0.34.0"
+ },
+ "engines": {
+ "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
+ }
+ },
+ "node_modules/babel-jest/node_modules/@jest/transform": {
+ "version": "30.2.0",
+ "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-30.2.0.tgz",
+ "integrity": "sha512-XsauDV82o5qXbhalKxD7p4TZYYdwcaEXC77PPD2HixEFF+6YGppjrAAQurTl2ECWcEomHBMMNS9AH3kcCFx8jA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/core": "^7.27.4",
+ "@jest/types": "30.2.0",
+ "@jridgewell/trace-mapping": "^0.3.25",
+ "babel-plugin-istanbul": "^7.0.1",
+ "chalk": "^4.1.2",
+ "convert-source-map": "^2.0.0",
+ "fast-json-stable-stringify": "^2.1.0",
+ "graceful-fs": "^4.2.11",
+ "jest-haste-map": "30.2.0",
+ "jest-regex-util": "30.0.1",
+ "jest-util": "30.2.0",
+ "micromatch": "^4.0.8",
+ "pirates": "^4.0.7",
+ "slash": "^3.0.0",
+ "write-file-atomic": "^5.0.1"
+ },
+ "engines": {
+ "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
+ }
+ },
+ "node_modules/babel-jest/node_modules/@jest/types": {
+ "version": "30.2.0",
+ "resolved": "https://registry.npmjs.org/@jest/types/-/types-30.2.0.tgz",
+ "integrity": "sha512-H9xg1/sfVvyfU7o3zMfBEjQ1gcsdeTMgqHoYdN79tuLqfTtuu7WckRA1R5whDwOzxaZAeMKTYWqP+WCAi0CHsg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jest/pattern": "30.0.1",
+ "@jest/schemas": "30.0.5",
+ "@types/istanbul-lib-coverage": "^2.0.6",
+ "@types/istanbul-reports": "^3.0.4",
+ "@types/node": "*",
+ "@types/yargs": "^17.0.33",
+ "chalk": "^4.1.2"
+ },
+ "engines": {
+ "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
+ }
+ },
+ "node_modules/babel-jest/node_modules/@sinclair/typebox": {
+ "version": "0.34.41",
+ "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.41.tgz",
+ "integrity": "sha512-6gS8pZzSXdyRHTIqoqSVknxolr1kzfy4/CeDnrzsVz8TTIWUbOBr6gnzOmTYJ3eXQNh4IYHIGi5aIL7sOZ2G/g==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/babel-jest/node_modules/babel-plugin-istanbul": {
+ "version": "7.0.1",
+ "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-7.0.1.tgz",
+ "integrity": "sha512-D8Z6Qm8jCvVXtIRkBnqNHX0zJ37rQcFJ9u8WOS6tkYOsRdHBzypCstaxWiu5ZIlqQtviRYbgnRLSoCEvjqcqbA==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "workspaces": [
+ "test/babel-8"
+ ],
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.0.0",
+ "@istanbuljs/load-nyc-config": "^1.0.0",
+ "@istanbuljs/schema": "^0.1.3",
+ "istanbul-lib-instrument": "^6.0.2",
+ "test-exclude": "^6.0.0"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/babel-jest/node_modules/ci-info": {
+ "version": "4.3.1",
+ "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.3.1.tgz",
+ "integrity": "sha512-Wdy2Igu8OcBpI2pZePZ5oWjPC38tmDVx5WKUXKwlLYkA0ozo85sLsLvkBbBn/sZaSCMFOGZJ14fvW9t5/d7kdA==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/sibiraj-s"
+ }
+ ],
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/babel-jest/node_modules/istanbul-lib-instrument": {
+ "version": "6.0.3",
+ "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz",
+ "integrity": "sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "@babel/core": "^7.23.9",
+ "@babel/parser": "^7.23.9",
+ "@istanbuljs/schema": "^0.1.3",
+ "istanbul-lib-coverage": "^3.2.0",
+ "semver": "^7.5.4"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/babel-jest/node_modules/jest-haste-map": {
+ "version": "30.2.0",
+ "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-30.2.0.tgz",
+ "integrity": "sha512-sQA/jCb9kNt+neM0anSj6eZhLZUIhQgwDt7cPGjumgLM4rXsfb9kpnlacmvZz3Q5tb80nS+oG/if+NBKrHC+Xw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jest/types": "30.2.0",
+ "@types/node": "*",
+ "anymatch": "^3.1.3",
+ "fb-watchman": "^2.0.2",
+ "graceful-fs": "^4.2.11",
+ "jest-regex-util": "30.0.1",
+ "jest-util": "30.2.0",
+ "jest-worker": "30.2.0",
+ "micromatch": "^4.0.8",
+ "walker": "^1.0.8"
+ },
+ "engines": {
+ "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
+ },
+ "optionalDependencies": {
+ "fsevents": "^2.3.3"
+ }
+ },
+ "node_modules/babel-jest/node_modules/jest-regex-util": {
+ "version": "30.0.1",
+ "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-30.0.1.tgz",
+ "integrity": "sha512-jHEQgBXAgc+Gh4g0p3bCevgRCVRkB4VB70zhoAE48gxeSr1hfUOsM/C2WoJgVL7Eyg//hudYENbm3Ne+/dRVVA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
+ }
+ },
+ "node_modules/babel-jest/node_modules/jest-util": {
+ "version": "30.2.0",
+ "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-30.2.0.tgz",
+ "integrity": "sha512-QKNsM0o3Xe6ISQU869e+DhG+4CK/48aHYdJZGlFQVTjnbvgpcKyxpzk29fGiO7i/J8VENZ+d2iGnSsvmuHywlA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jest/types": "30.2.0",
+ "@types/node": "*",
+ "chalk": "^4.1.2",
+ "ci-info": "^4.2.0",
+ "graceful-fs": "^4.2.11",
+ "picomatch": "^4.0.2"
+ },
+ "engines": {
+ "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
+ }
+ },
+ "node_modules/babel-jest/node_modules/jest-worker": {
+ "version": "30.2.0",
+ "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-30.2.0.tgz",
+ "integrity": "sha512-0Q4Uk8WF7BUwqXHuAjc23vmopWJw5WH7w2tqBoUOZpOjW/ZnR44GXXd1r82RvnmI2GZge3ivrYXk/BE2+VtW2g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "*",
+ "@ungap/structured-clone": "^1.3.0",
+ "jest-util": "30.2.0",
+ "merge-stream": "^2.0.0",
+ "supports-color": "^8.1.1"
+ },
+ "engines": {
+ "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
+ }
+ },
+ "node_modules/babel-jest/node_modules/picomatch": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
+ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/babel-jest/node_modules/semver": {
+ "version": "7.7.3",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz",
+ "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/babel-jest/node_modules/supports-color": {
+ "version": "8.1.1",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz",
+ "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "has-flag": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/supports-color?sponsor=1"
+ }
+ },
+ "node_modules/babel-jest/node_modules/write-file-atomic": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-5.0.1.tgz",
+ "integrity": "sha512-+QU2zd6OTD8XWIJCbffaiQeH9U73qIqafo1x6V1snCWYGJf6cVE0cDR4D8xRzcEnfI21IFrUPzPGtcPf8AC+Rw==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "imurmurhash": "^0.1.4",
+ "signal-exit": "^4.0.1"
+ },
+ "engines": {
+ "node": "^14.17.0 || ^16.13.0 || >=18.0.0"
}
},
"node_modules/babel-plugin-istanbul": {
@@ -5265,18 +5539,16 @@
}
},
"node_modules/babel-plugin-jest-hoist": {
- "version": "29.6.3",
- "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.6.3.tgz",
- "integrity": "sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg==",
+ "version": "30.2.0",
+ "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-30.2.0.tgz",
+ "integrity": "sha512-ftzhzSGMUnOzcCXd6WHdBGMyuwy15Wnn0iyyWGKgBDLxf9/s5ABuraCSpBX2uG0jUg4rqJnxsLc5+oYBqoxVaA==",
+ "dev": true,
"license": "MIT",
"dependencies": {
- "@babel/template": "^7.3.3",
- "@babel/types": "^7.3.3",
- "@types/babel__core": "^7.1.14",
- "@types/babel__traverse": "^7.0.6"
+ "@types/babel__core": "^7.20.5"
},
"engines": {
- "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
}
},
"node_modules/babel-plugin-polyfill-corejs2": {
@@ -5421,19 +5693,20 @@
}
},
"node_modules/babel-preset-jest": {
- "version": "29.6.3",
- "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-29.6.3.tgz",
- "integrity": "sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA==",
+ "version": "30.2.0",
+ "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-30.2.0.tgz",
+ "integrity": "sha512-US4Z3NOieAQumwFnYdUWKvUKh8+YSnS/gB3t6YBiz0bskpu7Pine8pPCheNxlPEW4wnUkma2a94YuW2q3guvCQ==",
+ "dev": true,
"license": "MIT",
"dependencies": {
- "babel-plugin-jest-hoist": "^29.6.3",
- "babel-preset-current-node-syntax": "^1.0.0"
+ "babel-plugin-jest-hoist": "30.2.0",
+ "babel-preset-current-node-syntax": "^1.2.0"
},
"engines": {
- "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
},
"peerDependencies": {
- "@babel/core": "^7.0.0"
+ "@babel/core": "^7.11.0 || ^8.0.0-beta.1"
}
},
"node_modules/balanced-match": {
@@ -5585,6 +5858,19 @@
"node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
}
},
+ "node_modules/bs-logger": {
+ "version": "0.2.6",
+ "resolved": "https://registry.npmjs.org/bs-logger/-/bs-logger-0.2.6.tgz",
+ "integrity": "sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fast-json-stable-stringify": "2.x"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
"node_modules/bser": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz",
@@ -9002,6 +9288,38 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/handlebars": {
+ "version": "4.7.8",
+ "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.8.tgz",
+ "integrity": "sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "minimist": "^1.2.5",
+ "neo-async": "^2.6.2",
+ "source-map": "^0.6.1",
+ "wordwrap": "^1.0.0"
+ },
+ "bin": {
+ "handlebars": "bin/handlebars"
+ },
+ "engines": {
+ "node": ">=0.4.7"
+ },
+ "optionalDependencies": {
+ "uglify-js": "^3.1.4"
+ }
+ },
+ "node_modules/handlebars/node_modules/source-map": {
+ "version": "0.6.1",
+ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
+ "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
"node_modules/has-bigints": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz",
@@ -9754,6 +10072,7 @@
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz",
"integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==",
+ "dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
@@ -10215,6 +10534,61 @@
}
}
},
+ "node_modules/jest-config/node_modules/babel-jest": {
+ "version": "29.7.0",
+ "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz",
+ "integrity": "sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==",
+ "devOptional": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jest/transform": "^29.7.0",
+ "@types/babel__core": "^7.1.14",
+ "babel-plugin-istanbul": "^6.1.1",
+ "babel-preset-jest": "^29.6.3",
+ "chalk": "^4.0.0",
+ "graceful-fs": "^4.2.9",
+ "slash": "^3.0.0"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.8.0"
+ }
+ },
+ "node_modules/jest-config/node_modules/babel-plugin-jest-hoist": {
+ "version": "29.6.3",
+ "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.6.3.tgz",
+ "integrity": "sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg==",
+ "devOptional": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/template": "^7.3.3",
+ "@babel/types": "^7.3.3",
+ "@types/babel__core": "^7.1.14",
+ "@types/babel__traverse": "^7.0.6"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/jest-config/node_modules/babel-preset-jest": {
+ "version": "29.6.3",
+ "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-29.6.3.tgz",
+ "integrity": "sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA==",
+ "devOptional": true,
+ "license": "MIT",
+ "dependencies": {
+ "babel-plugin-jest-hoist": "^29.6.3",
+ "babel-preset-current-node-syntax": "^1.0.0"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0"
+ }
+ },
"node_modules/jest-config/node_modules/ci-info": {
"version": "3.9.0",
"resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz",
@@ -10394,6 +10768,61 @@
"react-native": "*"
}
},
+ "node_modules/jest-expo/node_modules/babel-jest": {
+ "version": "29.7.0",
+ "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz",
+ "integrity": "sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jest/transform": "^29.7.0",
+ "@types/babel__core": "^7.1.14",
+ "babel-plugin-istanbul": "^6.1.1",
+ "babel-preset-jest": "^29.6.3",
+ "chalk": "^4.0.0",
+ "graceful-fs": "^4.2.9",
+ "slash": "^3.0.0"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.8.0"
+ }
+ },
+ "node_modules/jest-expo/node_modules/babel-plugin-jest-hoist": {
+ "version": "29.6.3",
+ "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.6.3.tgz",
+ "integrity": "sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/template": "^7.3.3",
+ "@babel/types": "^7.3.3",
+ "@types/babel__core": "^7.1.14",
+ "@types/babel__traverse": "^7.0.6"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/jest-expo/node_modules/babel-preset-jest": {
+ "version": "29.6.3",
+ "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-29.6.3.tgz",
+ "integrity": "sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "babel-plugin-jest-hoist": "^29.6.3",
+ "babel-preset-current-node-syntax": "^1.0.0"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0"
+ }
+ },
"node_modules/jest-get-type": {
"version": "29.6.3",
"resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz",
@@ -11529,6 +11958,13 @@
"integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==",
"license": "MIT"
},
+ "node_modules/lodash.memoize": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz",
+ "integrity": "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/lodash.merge": {
"version": "4.6.2",
"resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
@@ -11675,6 +12111,13 @@
"node": ">=10"
}
},
+ "node_modules/make-error": {
+ "version": "1.3.6",
+ "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz",
+ "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==",
+ "dev": true,
+ "license": "ISC"
+ },
"node_modules/makeerror": {
"version": "1.0.12",
"resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz",
@@ -11710,6 +12153,7 @@
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/merge-options/-/merge-options-3.0.4.tgz",
"integrity": "sha512-2Sug1+knBjkaMsMgf1ctR1Ujx+Ayku4EdJN4Z+C2+JzoeF7A3OZ9KM2GY0CpQS51NR61LTurMJrRKPhSs3ZRTQ==",
+ "dev": true,
"license": "MIT",
"dependencies": {
"is-plain-obj": "^2.1.0"
@@ -13519,6 +13963,58 @@
}
}
},
+ "node_modules/react-native/node_modules/babel-jest": {
+ "version": "29.7.0",
+ "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz",
+ "integrity": "sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==",
+ "license": "MIT",
+ "dependencies": {
+ "@jest/transform": "^29.7.0",
+ "@types/babel__core": "^7.1.14",
+ "babel-plugin-istanbul": "^6.1.1",
+ "babel-preset-jest": "^29.6.3",
+ "chalk": "^4.0.0",
+ "graceful-fs": "^4.2.9",
+ "slash": "^3.0.0"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.8.0"
+ }
+ },
+ "node_modules/react-native/node_modules/babel-plugin-jest-hoist": {
+ "version": "29.6.3",
+ "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.6.3.tgz",
+ "integrity": "sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/template": "^7.3.3",
+ "@babel/types": "^7.3.3",
+ "@types/babel__core": "^7.1.14",
+ "@types/babel__traverse": "^7.0.6"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/react-native/node_modules/babel-preset-jest": {
+ "version": "29.6.3",
+ "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-29.6.3.tgz",
+ "integrity": "sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA==",
+ "license": "MIT",
+ "dependencies": {
+ "babel-plugin-jest-hoist": "^29.6.3",
+ "babel-preset-current-node-syntax": "^1.0.0"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0"
+ }
+ },
"node_modules/react-native/node_modules/commander": {
"version": "12.1.0",
"resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz",
@@ -15469,6 +15965,85 @@
"integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==",
"license": "Apache-2.0"
},
+ "node_modules/ts-jest": {
+ "version": "29.4.5",
+ "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.4.5.tgz",
+ "integrity": "sha512-HO3GyiWn2qvTQA4kTgjDcXiMwYQt68a1Y8+JuLRVpdIzm+UOLSHgl/XqR4c6nzJkq5rOkjc02O2I7P7l/Yof0Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "bs-logger": "^0.2.6",
+ "fast-json-stable-stringify": "^2.1.0",
+ "handlebars": "^4.7.8",
+ "json5": "^2.2.3",
+ "lodash.memoize": "^4.1.2",
+ "make-error": "^1.3.6",
+ "semver": "^7.7.3",
+ "type-fest": "^4.41.0",
+ "yargs-parser": "^21.1.1"
+ },
+ "bin": {
+ "ts-jest": "cli.js"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || ^18.0.0 || >=20.0.0"
+ },
+ "peerDependencies": {
+ "@babel/core": ">=7.0.0-beta.0 <8",
+ "@jest/transform": "^29.0.0 || ^30.0.0",
+ "@jest/types": "^29.0.0 || ^30.0.0",
+ "babel-jest": "^29.0.0 || ^30.0.0",
+ "jest": "^29.0.0 || ^30.0.0",
+ "jest-util": "^29.0.0 || ^30.0.0",
+ "typescript": ">=4.3 <6"
+ },
+ "peerDependenciesMeta": {
+ "@babel/core": {
+ "optional": true
+ },
+ "@jest/transform": {
+ "optional": true
+ },
+ "@jest/types": {
+ "optional": true
+ },
+ "babel-jest": {
+ "optional": true
+ },
+ "esbuild": {
+ "optional": true
+ },
+ "jest-util": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/ts-jest/node_modules/semver": {
+ "version": "7.7.3",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz",
+ "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/ts-jest/node_modules/type-fest": {
+ "version": "4.41.0",
+ "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz",
+ "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==",
+ "dev": true,
+ "license": "(MIT OR CC0-1.0)",
+ "engines": {
+ "node": ">=16"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/tsconfig-paths": {
"version": "3.15.0",
"resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz",
@@ -15650,6 +16225,20 @@
"node": "*"
}
},
+ "node_modules/uglify-js": {
+ "version": "3.19.3",
+ "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.3.tgz",
+ "integrity": "sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "optional": true,
+ "bin": {
+ "uglifyjs": "bin/uglifyjs"
+ },
+ "engines": {
+ "node": ">=0.8.0"
+ }
+ },
"node_modules/unbox-primitive": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz",
@@ -16475,6 +17064,13 @@
"node": ">=0.10.0"
}
},
+ "node_modules/wordwrap": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz",
+ "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/wrap-ansi": {
"version": "8.1.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz",
diff --git a/package.json b/package.json
index 3fb4daf..f9a52f0 100644
--- a/package.json
+++ b/package.json
@@ -13,7 +13,6 @@
},
"dependencies": {
"@expo/vector-icons": "^15.0.2",
- "@react-native-async-storage/async-storage": "^2.2.0",
"@react-navigation/bottom-tabs": "^7.4.0",
"@react-navigation/elements": "^2.6.3",
"@react-navigation/native": "^7.1.8",
@@ -45,18 +44,19 @@
"redux-persist": "^6.0.0"
},
"devDependencies": {
+ "@babel/preset-typescript": "^7.27.1",
+ "@react-native-async-storage/async-storage": "^2.2.0",
"@testing-library/react-native": "^13.3.3",
- "@types/jest": "29.5.14",
+ "@types/jest": "^29.5.14",
"@types/react": "~19.1.0",
"@types/react-native": "^0.72.8",
+ "babel-jest": "^30.2.0",
"eslint": "^9.25.0",
"eslint-config-expo": "~10.0.0",
"jest": "~29.7.0",
"jest-expo": "~54.0.12",
+ "ts-jest": "^29.4.5",
"typescript": "~5.9.2"
},
- "private": true,
- "jest": {
- "preset": "jest-expo"
- }
+ "private": true
}
diff --git a/src/__tests__/utils/testUtils.tsx b/src/__tests__/utils/testUtils.tsx
new file mode 100644
index 0000000..70c34b0
--- /dev/null
+++ b/src/__tests__/utils/testUtils.tsx
@@ -0,0 +1,135 @@
+import { configureStore } from "@reduxjs/toolkit";
+import {
+ RenderHookOptions,
+ renderHook as rtlRenderHook,
+} from "@testing-library/react-native";
+import { ReactNode } from "react";
+import { Provider } from "react-redux";
+
+import browserReducer from "../../features/browser/browserSlice";
+import { BrowserState, BrowserTab } from "../../features/browser/types";
+import scriptsReducer from "../../features/scripts/scriptsSlice";
+import {
+ ScriptExecution,
+ ScriptsState,
+ UserScript,
+} from "../../features/scripts/types";
+import settingsReducer from "../../features/settings/settingsSlice";
+import { SettingsState } from "../../features/settings/types";
+import { RootState } from "../../store";
+
+// Default states for each slice
+export const defaultBrowserState: BrowserState = {
+ tabs: [
+ {
+ id: "tab-1",
+ url: "https://example.com",
+ title: "Example",
+ isLoading: false,
+ canGoBack: false,
+ canGoForward: false,
+ activeScripts: [],
+ },
+ ],
+ activeTabId: "tab-1",
+ showTabs: false,
+ bookmarks: ["https://bookmark.com"],
+};
+
+export const defaultScriptsState: ScriptsState = {
+ userScripts: {},
+ executions: {},
+ isLoading: false,
+ error: null,
+};
+
+export const defaultSettingsState: SettingsState = {
+ scriptsEnabled: true,
+ maxExecutionTime: 5000,
+ logExecutions: true,
+ theme: "light",
+ autoUpdateScripts: false,
+};
+
+// Store factory function
+export const createMockStore = (preloadedState?: Partial) => {
+ return configureStore({
+ reducer: {
+ browser: browserReducer,
+ scripts: scriptsReducer,
+ settings: settingsReducer,
+ },
+ preloadedState: {
+ browser: { ...defaultBrowserState, ...preloadedState?.browser },
+ scripts: { ...defaultScriptsState, ...preloadedState?.scripts },
+ settings: { ...defaultSettingsState, ...preloadedState?.settings },
+ },
+ });
+};
+
+// Wrapper component factory
+export const createTestWrapper = (
+ store: ReturnType
+) => {
+ const TestWrapper = ({ children }: { children: ReactNode }) => (
+ {children}
+ );
+ TestWrapper.displayName = "TestWrapper";
+ return TestWrapper;
+};
+
+// Custom render hook with store
+export const renderHookWithStore = (
+ hook: (props: TProps) => TResult,
+ options?: {
+ initialState?: Partial;
+ hookOptions?: RenderHookOptions;
+ }
+) => {
+ const store = createMockStore(options?.initialState);
+ const wrapper = createTestWrapper(store);
+
+ const result = rtlRenderHook(hook, {
+ wrapper,
+ ...options?.hookOptions,
+ });
+
+ return {
+ ...result,
+ store, // Return store for assertions
+ };
+};
+
+// Test data factories
+export const createMockTab = (overrides?: Partial) => ({
+ id: "mock-tab-id",
+ url: "https://mock.com",
+ title: "Mock Tab",
+ isLoading: false,
+ canGoBack: false,
+ canGoForward: false,
+ activeScripts: [],
+ ...overrides,
+});
+
+export const createMockScript = (overrides?: Partial) => ({
+ id: "mock-script-id",
+ name: "Mock Script",
+ description: "A mock script for testing",
+ code: 'console.log("Hello World");',
+ urlPatterns: ["*"],
+ enabled: true,
+ runAt: "document-ready" as const,
+ createdAt: "2025-01-01T00:00:00Z",
+ updatedAt: "2025-01-01T00:00:00Z",
+ ...overrides,
+});
+
+export const createMockExecution = (overrides?: Partial) => ({
+ id: "mock-execution-id",
+ scriptId: "mock-script-id",
+ url: "https://example.com",
+ timestamp: "2025-01-01T00:00:00Z",
+ success: true,
+ ...overrides,
+});
diff --git a/src/features/browser/browserSlice.ts b/src/features/browser/browserSlice.ts
index 76df1c5..0b03a45 100644
--- a/src/features/browser/browserSlice.ts
+++ b/src/features/browser/browserSlice.ts
@@ -2,14 +2,14 @@ import { createSlice, nanoid, PayloadAction } from "@reduxjs/toolkit";
import { BrowserState, BrowserTab } from "./types";
const homePageUrl =
- process.env.EXPO_PUBLIC_HOME_PAGE_URL || "https://afnprojects.com";
+ process.env.EXPO_PUBLIC_HOME_PAGE_URL || "https://www.google.com";
const initialState: BrowserState = {
tabs: [
{
id: "1",
url: homePageUrl,
- title: "Ali Fuat Numanoglu's Projects",
+ title: "Homepage",
isLoading: false,
canGoBack: false,
canGoForward: false,
@@ -25,21 +25,30 @@ const browserSlice = createSlice({
name: "browser",
initialState,
reducers: {
- addTab: (state, action: PayloadAction<{ url?: string }>) => {
- const newTab: BrowserTab = {
- id: nanoid(),
- url: action.payload.url || homePageUrl,
- title: "New Tab",
- isLoading: false,
- canGoBack: false,
- canGoForward: false,
- activeScripts: [],
- };
- state.tabs.push(newTab);
- state.activeTabId = newTab.id;
- state.showTabs = false;
+ addTab: {
+ reducer: (state, action: PayloadAction<{ id: string; url?: string }>) => {
+ const newTab: BrowserTab = {
+ id: action.payload.id,
+ url: action.payload.url || homePageUrl,
+ title: "New Tab",
+ isLoading: false,
+ canGoBack: false,
+ canGoForward: false,
+ activeScripts: [],
+ };
+ state.tabs.push(newTab);
+ state.activeTabId = newTab.id;
+ state.showTabs = false;
+ },
+ prepare: (url?: string) => {
+ return {
+ payload: {
+ id: nanoid(),
+ url,
+ },
+ };
+ },
},
-
closeTab: (state, action: PayloadAction) => {
const tabId = action.payload;
state.tabs = state.tabs.filter((tab) => tab.id !== tabId);
diff --git a/src/features/browser/hooks/__tests__/userBrowser-test.ts b/src/features/browser/hooks/__tests__/userBrowser-test.ts
new file mode 100644
index 0000000..293e5d0
--- /dev/null
+++ b/src/features/browser/hooks/__tests__/userBrowser-test.ts
@@ -0,0 +1,335 @@
+import { act } from "@testing-library/react-native";
+import {
+ createMockTab,
+ defaultBrowserState,
+ renderHookWithStore,
+} from "../../../../__tests__/utils/testUtils";
+import { useBrowser } from "../useBrowser";
+
+describe("useBrowser Hook", () => {
+ describe("Initial State", () => {
+ it("should return initial browser state correctly", () => {
+ const { result } = renderHookWithStore(() => useBrowser());
+
+ expect(result.current.tabs).toHaveLength(1);
+ expect(result.current.activeTabId).toBe("tab-1");
+ expect(result.current.showTabs).toBe(false);
+ expect(result.current.bookmarks).toEqual(["https://bookmark.com"]);
+ expect(result.current.activeTab).toEqual({
+ id: "tab-1",
+ url: "https://example.com",
+ title: "Example",
+ isLoading: false,
+ canGoBack: false,
+ canGoForward: false,
+ activeScripts: [],
+ });
+ });
+
+ it("should return computed values correctly", () => {
+ const { result } = renderHookWithStore(() => useBrowser(), {
+ initialState: {
+ browser: {
+ ...defaultBrowserState,
+ tabs: [
+ createMockTab({
+ id: "tab-1",
+ url: "https://example.com",
+ title: "Example",
+ }),
+ createMockTab({
+ id: "tab-2",
+ url: "https://google.com",
+ title: "Google",
+ }),
+ ],
+ },
+ },
+ });
+
+ expect(result.current.tabCount).toBe(2);
+ expect(result.current.hasMultipleTabs).toBe(true);
+ });
+
+ it("should handle no active tab correctly", () => {
+ const { result } = renderHookWithStore(() => useBrowser(), {
+ initialState: {
+ browser: {
+ ...defaultBrowserState,
+ tabs: [],
+ activeTabId: "",
+ },
+ },
+ });
+
+ expect(result.current.activeTab).toBeUndefined();
+ expect(result.current.tabCount).toBe(0);
+ expect(result.current.hasMultipleTabs).toBe(false);
+ });
+ });
+
+ describe("Tab Management", () => {
+ it("should create new tab with default URL", () => {
+ const { result, store } = renderHookWithStore(() => useBrowser());
+
+ act(() => {
+ result.current.createNewTab();
+ });
+
+ const state = store.getState().browser;
+ expect(state.tabs).toHaveLength(2);
+ expect(state.tabs[1].url).toBe(
+ process.env.EXPO_PUBLIC_HOME_PAGE_URL || "https://www.google.com"
+ );
+ });
+
+ it("should create new tab with custom URL", () => {
+ const { result, store } = renderHookWithStore(() => useBrowser());
+
+ act(() => {
+ result.current.createNewTab("https://custom.com");
+ });
+
+ const state = store.getState().browser;
+ expect(state.tabs).toHaveLength(2);
+ expect(state.tabs[1].url).toBe("https://custom.com");
+ });
+
+ it("should close tab by id", () => {
+ const { result, store } = renderHookWithStore(() => useBrowser(), {
+ initialState: {
+ browser: {
+ ...defaultBrowserState,
+ tabs: [
+ createMockTab({
+ id: "tab-1",
+ url: "https://example.com",
+ title: "Example",
+ }),
+ createMockTab({
+ id: "tab-2",
+ url: "https://google.com",
+ title: "Google",
+ }),
+ ],
+ },
+ },
+ });
+
+ act(() => {
+ result.current.closeTabById("tab-1");
+ });
+
+ const state = store.getState().browser;
+ expect(state.tabs).toHaveLength(1);
+ expect(state.tabs[0].id).toBe("tab-2");
+ });
+
+ it("should update tab by id", () => {
+ const { result, store } = renderHookWithStore(() => useBrowser());
+
+ act(() => {
+ result.current.updateTabById("tab-1", {
+ title: "Updated Title",
+ isLoading: true,
+ });
+ });
+
+ const state = store.getState().browser;
+ const updatedTab = state.tabs.find((tab) => tab.id === "tab-1");
+ expect(updatedTab?.title).toBe("Updated Title");
+ expect(updatedTab?.isLoading).toBe(true);
+ });
+
+ it("should switch to tab", () => {
+ const { result, store } = renderHookWithStore(() => useBrowser(), {
+ initialState: {
+ browser: {
+ ...defaultBrowserState,
+ tabs: [
+ createMockTab({
+ id: "tab-1",
+ url: "https://example.com",
+ title: "Example",
+ }),
+ createMockTab({
+ id: "tab-2",
+ url: "https://google.com",
+ title: "Google",
+ }),
+ ],
+ activeTabId: "tab-1",
+ },
+ },
+ });
+
+ act(() => {
+ result.current.switchToTab("tab-2");
+ });
+
+ const state = store.getState().browser;
+ expect(state.activeTabId).toBe("tab-2");
+ expect(state.showTabs).toBe(false);
+ });
+
+ it("should toggle tab view", () => {
+ const { result, store } = renderHookWithStore(() => useBrowser(), {
+ initialState: {
+ browser: {
+ ...defaultBrowserState,
+ showTabs: false,
+ },
+ },
+ });
+
+ act(() => {
+ result.current.toggleTabs();
+ });
+
+ const state = store.getState().browser;
+ expect(state.showTabs).toBe(true);
+ });
+ });
+
+ describe("Navigation", () => {
+ it("should navigate tab to URL", () => {
+ const { result, store } = renderHookWithStore(() => useBrowser());
+
+ act(() => {
+ result.current.navigateTab("tab-1", "https://newurl.com");
+ });
+
+ const state = store.getState().browser;
+ const tab = state.tabs.find((t) => t.id === "tab-1");
+ expect(tab?.url).toBe("https://newurl.com");
+ expect(tab?.isLoading).toBe(true);
+ });
+ });
+
+ describe("Bookmarks", () => {
+ it("should add bookmark when URL is not bookmarked", () => {
+ const { result, store } = renderHookWithStore(() => useBrowser(), {
+ initialState: {
+ browser: {
+ ...defaultBrowserState,
+ bookmarks: ["https://existing.com"],
+ },
+ },
+ });
+
+ act(() => {
+ result.current.toggleBookmark("https://newbookmark.com");
+ });
+
+ const state = store.getState().browser;
+ expect(state.bookmarks).toContain("https://newbookmark.com");
+ expect(state.bookmarks).toHaveLength(2);
+ });
+
+ it("should remove bookmark when URL is already bookmarked", () => {
+ const { result, store } = renderHookWithStore(() => useBrowser(), {
+ initialState: {
+ browser: {
+ ...defaultBrowserState,
+ bookmarks: ["https://existing.com", "https://tobedeleted.com"],
+ },
+ },
+ });
+
+ act(() => {
+ result.current.toggleBookmark("https://tobedeleted.com");
+ });
+
+ const state = store.getState().browser;
+ expect(state.bookmarks).not.toContain("https://tobedeleted.com");
+ expect(state.bookmarks).toHaveLength(1);
+ });
+
+ it("should correctly identify bookmarked URLs", () => {
+ const { result } = renderHookWithStore(() => useBrowser(), {
+ initialState: {
+ browser: {
+ ...defaultBrowserState,
+ bookmarks: ["https://bookmarked.com"],
+ },
+ },
+ });
+
+ expect(result.current.isBookmarked("https://bookmarked.com")).toBe(true);
+ expect(result.current.isBookmarked("https://notbookmarked.com")).toBe(
+ false
+ );
+ });
+ });
+
+ describe("Function Stability", () => {
+ it("should maintain function reference stability for memoized functions", () => {
+ const { result, rerender } = renderHookWithStore(() => useBrowser());
+
+ const firstRender = {
+ toggleBookmark: result.current.toggleBookmark,
+ };
+
+ rerender({});
+
+ const secondRender = {
+ toggleBookmark: result.current.toggleBookmark,
+ };
+
+ // toggleBookmark should maintain reference stability due to useCallback
+ expect(firstRender.toggleBookmark).toBe(secondRender.toggleBookmark);
+ });
+
+ it("should create new function references for non-memoized functions", () => {
+ const { result, rerender } = renderHookWithStore(() => useBrowser());
+
+ const firstRender = {
+ createNewTab: result.current.createNewTab,
+ closeTabById: result.current.closeTabById,
+ };
+
+ rerender({});
+
+ const secondRender = {
+ createNewTab: result.current.createNewTab,
+ closeTabById: result.current.closeTabById,
+ };
+
+ // These functions are not memoized, so they should be different references
+ expect(firstRender.createNewTab).not.toBe(secondRender.createNewTab);
+ expect(firstRender.closeTabById).not.toBe(secondRender.closeTabById);
+ });
+ });
+
+ describe("Edge Cases", () => {
+ it("should handle empty tab array", () => {
+ const { result } = renderHookWithStore(() => useBrowser(), {
+ initialState: {
+ browser: {
+ ...defaultBrowserState,
+ tabs: [],
+ activeTabId: "",
+ },
+ },
+ });
+
+ expect(result.current.tabs).toHaveLength(0);
+ expect(result.current.activeTab).toBeUndefined();
+ expect(result.current.tabCount).toBe(0);
+ expect(result.current.hasMultipleTabs).toBe(false);
+ });
+
+ it("should handle invalid active tab id", () => {
+ const { result } = renderHookWithStore(() => useBrowser(), {
+ initialState: {
+ browser: {
+ ...defaultBrowserState,
+ activeTabId: "non-existent-tab",
+ },
+ },
+ });
+
+ expect(result.current.activeTab).toBeUndefined();
+ });
+ });
+});
diff --git a/src/features/browser/hooks/useBrowser.ts b/src/features/browser/hooks/useBrowser.ts
new file mode 100644
index 0000000..f5c3545
--- /dev/null
+++ b/src/features/browser/hooks/useBrowser.ts
@@ -0,0 +1,88 @@
+import { useCallback } from "react";
+import { useAppDispatch, useAppSelector } from "../../../store";
+import {
+ addBookmark,
+ addTab,
+ closeTab,
+ navigateToUrl,
+ removeBookmark,
+ switchTab,
+ toggleTabView,
+ updateTab,
+} from "../browserSlice";
+import { BrowserTab } from "../types";
+
+export const useBrowser = () => {
+ const dispatch = useAppDispatch();
+ const browserState = useAppSelector((state) => state.browser);
+
+ // Tab management operations
+ const createNewTab = (url?: string) => {
+ dispatch(addTab(url));
+ };
+
+ const closeTabById = (tabId: string) => {
+ dispatch(closeTab(tabId));
+ };
+
+ const updateTabById = (tabId: string, updates: Partial) => {
+ dispatch(updateTab({ tabId, updates }));
+ };
+
+ const switchToTab = (tabId: string) => {
+ dispatch(switchTab(tabId));
+ };
+
+ const toggleTabs = () => {
+ dispatch(toggleTabView());
+ };
+
+ // Navigation operations
+ const navigateTab = (tabId: string, url: string) => {
+ dispatch(navigateToUrl({ tabId, url }));
+ };
+
+ // Bookmark operations
+ const toggleBookmark = useCallback(
+ (url: string) => {
+ if (browserState.bookmarks.includes(url)) {
+ dispatch(removeBookmark(url));
+ } else {
+ dispatch(addBookmark(url));
+ }
+ },
+ [dispatch, browserState.bookmarks]
+ );
+
+ const isBookmarked = (url: string) => {
+ return browserState.bookmarks.includes(url);
+ };
+
+ // Computed values
+ const activeTab = browserState.tabs.find(
+ (tab) => tab.id === browserState.activeTabId
+ );
+ const tabCount = browserState.tabs.length;
+ const hasMultipleTabs = tabCount > 1;
+
+ return {
+ // State
+ tabs: browserState.tabs,
+ activeTabId: browserState.activeTabId,
+ showTabs: browserState.showTabs,
+ bookmarks: browserState.bookmarks,
+ activeTab,
+ tabCount,
+ hasMultipleTabs,
+
+ // Actions
+ createNewTab,
+ closeTabById,
+ updateTabById,
+ switchToTab,
+ toggleTabs,
+ navigateTab,
+ toggleBookmark,
+ isBookmarked,
+ };
+};
diff --git a/src/features/browser/types.ts b/src/features/browser/types.ts
index 74bac0e..b8981a6 100644
--- a/src/features/browser/types.ts
+++ b/src/features/browser/types.ts
@@ -1,6 +1,6 @@
export interface BrowserTab {
id: string;
- url: string;
+ url?: string;
title: string;
favicon?: string;
isLoading: boolean;
diff --git a/src/hooks/useAppDispatch.ts b/src/hooks/useAppDispatch.ts
index 48c0f38..15b0a50 100644
--- a/src/hooks/useAppDispatch.ts
+++ b/src/hooks/useAppDispatch.ts
@@ -1,4 +1,4 @@
import { useDispatch } from "react-redux";
import type { AppDispatch } from "../store";
-export const useAppDispatch = () => useDispatch();
+export const useAppDispatch = useDispatch.withTypes();
diff --git a/src/store/index.ts b/src/store/index.ts
index de57f88..8f341c6 100644
--- a/src/store/index.ts
+++ b/src/store/index.ts
@@ -1,5 +1,5 @@
import AsyncStorage from "@react-native-async-storage/async-storage";
-import { combineReducers, configureStore } from "@reduxjs/toolkit";
+import { configureStore } from "@reduxjs/toolkit";
import {
FLUSH,
PAUSE,
@@ -11,9 +11,7 @@ import {
REHYDRATE,
} from "redux-persist";
-import browserReducer from "../features/browser/browserSlice";
-import scriptsReducer from "../features/scripts/scriptsSlice";
-import settingsReducer from "../features/settings/settingsSlice";
+import { rootReducer } from "./rootReducer";
const persistConfig = {
key: "root",
@@ -21,12 +19,6 @@ const persistConfig = {
whitelist: ["scripts", "settings"],
};
-const rootReducer = combineReducers({
- browser: browserReducer,
- scripts: scriptsReducer,
- settings: settingsReducer,
-});
-
const persistedReducer = persistReducer(persistConfig, rootReducer);
export const store = configureStore({
@@ -44,3 +36,6 @@ export const persistor = persistStore(store);
export type RootState = ReturnType;
export type AppDispatch = typeof store.dispatch;
+
+export { useAppDispatch } from "../hooks/useAppDispatch";
+export { useAppSelector } from "../hooks/useAppSelector";
diff --git a/src/store/rootReducer.ts b/src/store/rootReducer.ts
new file mode 100644
index 0000000..fd87148
--- /dev/null
+++ b/src/store/rootReducer.ts
@@ -0,0 +1,11 @@
+import { combineReducers } from "@reduxjs/toolkit";
+
+import browserReducer from "../features/browser/browserSlice";
+import scriptsReducer from "../features/scripts/scriptsSlice";
+import settingsReducer from "../features/settings/settingsSlice";
+
+export const rootReducer = combineReducers({
+ browser: browserReducer,
+ scripts: scriptsReducer,
+ settings: settingsReducer,
+});