From 026a150be0979bb4199db849d27b91cd293a50ec Mon Sep 17 00:00:00 2001 From: afnx Date: Thu, 16 Oct 2025 18:34:53 -0700 Subject: [PATCH 1/7] feat: add test packages --- package-lock.json | 652 ++++++++++++++++++++++++++++++++++++++++++++-- package.json | 12 +- 2 files changed, 630 insertions(+), 34 deletions(-) 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 } From 558086712ee2bf89f74558f833a40ad8b0443b34 Mon Sep 17 00:00:00 2001 From: afnx Date: Thu, 16 Oct 2025 18:35:34 -0700 Subject: [PATCH 2/7] feat: add Jest and ESLint configuration files with necessary mocks and presets --- babel.config.js | 6 ++++++ eslint.config.js | 15 +++++++++++++++ jest-setup.js | 24 ++++++++++++++++++++++++ jest.config.js | 15 +++++++++++++++ 4 files changed, 60 insertions(+) create mode 100644 babel.config.js create mode 100644 jest-setup.js create mode 100644 jest.config.js 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 From b23596046170557b046d1982bc2a47a6c81a2ed9 Mon Sep 17 00:00:00 2001 From: afnx Date: Thu, 16 Oct 2025 18:36:09 -0700 Subject: [PATCH 3/7] feat: add test utility functions and mock data for Redux store integration --- __tests__/Index-test.tsx | 11 --- src/__tests__/utils/testUtils.tsx | 135 ++++++++++++++++++++++++++++++ 2 files changed, 135 insertions(+), 11 deletions(-) delete mode 100644 __tests__/Index-test.tsx create mode 100644 src/__tests__/utils/testUtils.tsx 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/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, +}); From 6a4a4719193c37b86b1e2e7cc7b506a403303fe9 Mon Sep 17 00:00:00 2001 From: afnx Date: Sun, 19 Oct 2025 16:20:02 -0700 Subject: [PATCH 4/7] refactor: move rootReducer to a separate file for better organization --- src/store/index.ts | 15 +++++---------- src/store/rootReducer.ts | 11 +++++++++++ 2 files changed, 16 insertions(+), 10 deletions(-) create mode 100644 src/store/rootReducer.ts 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, +}); From 0c67899fc150091364f0458c1afeb7ebf3b9ef9e Mon Sep 17 00:00:00 2001 From: afnx Date: Sun, 19 Oct 2025 16:20:14 -0700 Subject: [PATCH 5/7] fix: update useAppDispatch to use withTypes for improved type safety --- src/hooks/useAppDispatch.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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(); From f12ca01a9eb246fb79206341c35f6b98d6963b6c Mon Sep 17 00:00:00 2001 From: afnx Date: Sun, 19 Oct 2025 16:20:43 -0700 Subject: [PATCH 6/7] fix: update homepage URL and make tab URL optional in types --- src/features/browser/browserSlice.ts | 41 +++++++++++++++++----------- src/features/browser/types.ts | 2 +- 2 files changed, 26 insertions(+), 17 deletions(-) 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/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; From 0dcd92eb2f4a8b7cac96c668a471efbc2c6055cf Mon Sep 17 00:00:00 2001 From: afnx Date: Sun, 19 Oct 2025 16:21:19 -0700 Subject: [PATCH 7/7] feat: implement useBrowser hook with tab management, navigation, and bookmarking functionalities --- .../hooks/__tests__/userBrowser-test.ts | 335 ++++++++++++++++++ src/features/browser/hooks/useBrowser.ts | 88 +++++ 2 files changed, 423 insertions(+) create mode 100644 src/features/browser/hooks/__tests__/userBrowser-test.ts create mode 100644 src/features/browser/hooks/useBrowser.ts 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, + }; +};