From 658bc81b19ded8b9c262c03ebeb4bda658857c3a Mon Sep 17 00:00:00 2001 From: Yogendra Shelke <25844542+YogendraShelke@users.noreply.github.com> Date: Sun, 18 Jan 2026 12:45:57 +0530 Subject: [PATCH] feat: add harness tests for turbo module --- .github/CI_DOCUMENTATION.md | 223 ++++++ .github/actions/setup-node-yarn/action.yml | 29 +- .github/scripts/launch-ios-simulator.sh | 208 ++++++ .github/workflows/android.yml | 114 +++ .github/workflows/build-android.yml | 32 - .github/workflows/build-ios.yml | 57 -- .github/workflows/ci.yml | 12 +- .github/workflows/ios.yml | 89 +++ README.md | 9 + example/__tests__/cookie.harness.ts | 65 ++ .../__tests__/encrypted-storage.harness.ts | 183 +++++ example/__tests__/file-system.harness.ts | 277 ++++++++ example/__tests__/mx-configuration.harness.ts | 148 ++++ example/__tests__/navigation-mode.harness.ts | 122 ++++ example/__tests__/ota.harness.ts | 169 +++++ example/__tests__/splash-screen.harness.ts | 114 +++ .../ios/MendixNativeExample/AppDelegate.swift | 9 +- example/ios/Podfile.lock | 4 +- example/jest.config.js | 10 +- example/metro.config.js | 13 +- example/package.json | 17 +- example/rn-harness.config.mjs | 34 + ios/Modules/Encryption/EncryptedStorage.swift | 2 +- .../NativeFsModule/NativeFsModule.swift | 2 +- yarn.lock | 662 +++++++++++++++++- 25 files changed, 2482 insertions(+), 122 deletions(-) create mode 100644 .github/CI_DOCUMENTATION.md create mode 100755 .github/scripts/launch-ios-simulator.sh create mode 100644 .github/workflows/android.yml delete mode 100644 .github/workflows/build-android.yml delete mode 100644 .github/workflows/build-ios.yml create mode 100644 .github/workflows/ios.yml create mode 100644 example/__tests__/cookie.harness.ts create mode 100644 example/__tests__/encrypted-storage.harness.ts create mode 100644 example/__tests__/file-system.harness.ts create mode 100644 example/__tests__/mx-configuration.harness.ts create mode 100644 example/__tests__/navigation-mode.harness.ts create mode 100644 example/__tests__/ota.harness.ts create mode 100644 example/__tests__/splash-screen.harness.ts create mode 100644 example/rn-harness.config.mjs diff --git a/.github/CI_DOCUMENTATION.md b/.github/CI_DOCUMENTATION.md new file mode 100644 index 0000000..9a1f464 --- /dev/null +++ b/.github/CI_DOCUMENTATION.md @@ -0,0 +1,223 @@ +# GitHub Actions CI/CD Documentation + +This document describes the Continuous Integration setup for the mendix-native project. + +## Overview + +The CI pipeline runs on every pull request and includes: + +1. **Linting** - Code quality checks +2. **Building** - iOS and Android example app builds +3. **Testing** - Automated harness tests on both platforms + +## Tooling Versions + +All workflows use standardized tooling versions to ensure consistency: + +### Core Tools + +| Tool | Version | Defined In | +| ---------------- | -------- | ----------------------------- | +| **Node.js** | `24` | `.nvmrc` | +| **Yarn** | `4.12.0` | `package.json#packageManager` | +| **React Native** | `0.78.2` | `package.json` | +| **React** | `19.0.0` | `package.json` | +| **TypeScript** | `5.9.2` | `package.json` | + +### Android Tooling + +| Tool | Version | Defined In | +| ----------------------- | ------------------------ | ------------------------------ | +| **Java** | `17` (Zulu distribution) | All Android workflows | +| **Gradle** | `8.12` | `gradle-wrapper.properties` | +| **Android Build Tools** | `35.0.0` | `example/android/build.gradle` | +| **NDK** | `27.3.13750724` | `example/android/build.gradle` | +| **Kotlin** | `2.0.21` | `example/android/build.gradle` | +| **compileSdkVersion** | `35` | `example/android/build.gradle` | +| **targetSdkVersion** | `35` | `example/android/build.gradle` | +| **minSdkVersion** | `24` | `example/android/build.gradle` | + +### iOS Tooling + +| Tool | Version | Defined In | +| ------------- | ----------------------------------- | -------------------------------- | +| **Xcode** | `26.2` | All iOS workflows | +| **iOS SDK** | `26.2` | Xcode 26.2 includes iOS 26.2 SDK | +| **Ruby** | `3.2` | `ios.yml` | +| **CocoaPods** | `>= 1.13` (excludes 1.15.0, 1.15.1) | `example/Gemfile` | + +> **Note for Local Development:** GitHub Actions `macos-latest` runners use Xcode 26.2 as the default. +> Your local machine may have a different Xcode version. The workflows are configured to match GitHub Actions' +> environment. Local development can use any compatible Xcode version, but ensure simulator devices specified +> in `rn-harness.config.mjs` are available on your system. + +### Test Configuration + +| Tool | Version | Defined In | +| ------------------------ | --------------------- | ----------------------- | +| **React Native Harness** | `1.0.0-alpha.21` | `package.json` | +| **Android Emulator** | Pixel_API_34 (API 34) | `rn-harness.config.mjs` | +| **iOS Simulator** | iPhone 17 (iOS 26.2) | `rn-harness.config.mjs` | + +### GitHub Actions + +All actions are pinned to specific commit SHAs for security and reproducibility: + +| Action | Version | SHA | +| ---------------------------------------- | -------- | ------------------------------------------ | +| `actions/checkout` | v4.2.2 | `93cb6efe18208431cddfb8368fd83d5badbf9bfd` | +| `actions/setup-node` | v4.2.2 | `49933ea5288caeca8642d1e84afbd3f7d6820020` | +| `actions/setup-java` | v4.7.1 | `f2beeb24e141e01a676f977032f5a29d81c9e27e` | +| `actions/cache` | v4.2.0 | `0057852bfaa89a56745cba8c7296529d2fc39830` | +| `ruby/setup-ruby` | v1.204.0 | `d697be2f83c6234b20877c3b5eac7a7f342f0d0c` | +| `android-actions/setup-android` | v3.2.1 | `9fc6c4e9069bf8d3d10b2204b1fb8f6ef7065407` | +| `reactivecircus/android-emulator-runner` | v2.33.0 | `b530d96654c385303d652368551fb075bc2f0b6b` | + +> **Note on iOS Simulator:** We use a custom shell script (`.github/scripts/launch-ios-simulator.sh`) instead of `futureware-tech/simulator-action` for better reliability and faster boot times. The action was sometimes unstable and could hang for hours without response. + +### Version Consistency Rules + +**IMPORTANT**: When updating versions, maintain consistency: + +1. **iOS Simulator Device**: Must match between: + + - `rn-harness.config.mjs` (test configuration) + - `ios.yml` (xcodebuild destination) + - `example/package.json` (ios:simulator:build script) + - Must be available in the specified Xcode version + +2. **Xcode Version**: Must match in: + + - `ios.yml` (xcode-select command and DEVELOPER_DIR env var) + - iOS simulator version in harness config must be compatible + +3. **Android Emulator Device**: Must match between: + + - `rn-harness.config.mjs` (test configuration) + - `android.yml` (AVD_NAME environment variable) + +4. **NDK Version**: Must match between: + + - `example/android/build.gradle` (ndkVersion) + - `android.yml` (sdkmanager install command and cache path) + +5. **Java Version**: Must be specified in: + + - `android.yml` (Setup Java step) + +6. **Ruby Version**: Must satisfy: + + - `ios.yml` specification (3.2) + - `example/Gemfile` requirement (>= 2.6.10) + - `ios.yml` working-directory must point to `example` (where Gemfile lives) + +7. **React Native Harness**: Must match between: + - `example/package.json` + - All harness platform packages (`@react-native-harness/*`) + +## Workflows + +#### 1. Lint (`.github/workflows/lint.yml`) + +- Runs ESLint and TypeScript checks + +#### 2. Android Build & Test (`.github/workflows/android.yml`) + +**Single integrated job that:** + +- Builds the Android example app (debug APK) +- Sets up Android emulator +- Runs harness tests +- Runs on: `ubuntu-latest` +- Uses: Java 17, Android SDK, NDK 27.3.13750724 + +#### 3. iOS Build & Test (`.github/workflows/ios.yml`) + +**Single integrated job that:** + +- Builds the iOS example app for simulator +- Sets up iOS simulator +- Runs harness tests +- Runs on: `macos-latest` +- Uses: Xcode 26.2, Ruby 3.2, CocoaPods + +## Test Configuration + +The harness tests are configured in `example/rn-harness.config.mjs`: + +```javascript +runners: [ + androidPlatform({ + name: 'android', + device: androidEmulator('Pixel_API_34'), + bundleId: 'mendixnative.example', + }), + applePlatform({ + name: 'ios', + device: appleSimulator('iPhone 17', '26.2'), + bundleId: 'mendixnative.example', + }), +]; +``` + +The workflows handle device setup automatically: + +- **Android**: Uses `reactivecircus/android-emulator-runner` to launch the emulator +- **iOS**: Uses a custom script (`.github/scripts/launch-ios-simulator.sh`) that launches the simulator with specific OS version pinning + +## Running Tests Locally + +### iOS + +Make sure `iPhone 17 (iOS 26.2)` simulator is up and running. You can use the launch script: + +```bash +# Launch simulator automatically +./.github/scripts/launch-ios-simulator.sh "iPhone 17" "26.2" + +# Then run tests +corepack enable +yarn install +yarn prepare +cd example +yarn pod +yarn harness:ios:with:build +``` + +### Android + +Make sure emulator with name `Pixel_API_34` is up and running + +```bash +corepack enable +yarn install +yarn prepare +cd example +yarn harness:android:with:build +``` + +## Troubleshooting + +### Device/Emulator Issues + +**Android emulator fails to boot or tests time out:** + +- Check AVD cache - clear `avd-*` entries if corrupted +- Verify KVM is enabled (workflows handle this automatically) +- Check emulator logs in workflow output +- Ensure AVD_NAME matches harness config (`Pixel_API_34`) + +**iOS simulator not found or fails to boot:** + +- Verify simulator model exists in Xcode version (`iPhone 17`) +- Check iOS version compatibility (`26.2`) +- Review simulator launch script logs for errors +- Ensure device name matches harness config exactly +- Check if `jq` is installed (script requires it for JSON parsing) +- Simulator script automatically handles stuck processes and timeouts + +## References + +- [React Native Harness Documentation](https://www.react-native-harness.dev/) +- [React Native Harness CI/CD Guide](https://www.react-native-harness.dev/docs/guides/ci-cd) +- [Android Emulator Runner Action](https://github.com/ReactiveCircus/android-emulator-runner) diff --git a/.github/actions/setup-node-yarn/action.yml b/.github/actions/setup-node-yarn/action.yml index 6e550cc..3e85382 100644 --- a/.github/actions/setup-node-yarn/action.yml +++ b/.github/actions/setup-node-yarn/action.yml @@ -8,16 +8,41 @@ runs: uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 with: node-version-file: '.nvmrc' - cache: yarn - name: Enable corepack shell: bash run: corepack enable + - name: Get yarn cache directory path + id: yarn-cache-dir-path + shell: bash + run: echo "dir=$(yarn config get cacheFolder)" >> $GITHUB_OUTPUT + + - name: Cache Yarn dependencies + uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 + with: + path: | + ${{ steps.yarn-cache-dir-path.outputs.dir }} + .yarn/cache + .yarn/install-state.gz + key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} + restore-keys: | + ${{ runner.os }}-yarn- + - name: Install dependencies shell: bash run: yarn install --immutable - - name: Prepare package + - name: Cache built library + id: cache-lib + uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 + with: + path: lib + key: ${{ runner.os }}-lib-${{ hashFiles('src/**/*.{ts,tsx,js,jsx}', 'package.json', 'tsconfig.json') }} + restore-keys: | + ${{ runner.os }}-lib- + + - name: Build library (bob build) + if: steps.cache-lib.outputs.cache-hit != 'true' shell: bash run: yarn prepare diff --git a/.github/scripts/launch-ios-simulator.sh b/.github/scripts/launch-ios-simulator.sh new file mode 100755 index 0000000..0752b82 --- /dev/null +++ b/.github/scripts/launch-ios-simulator.sh @@ -0,0 +1,208 @@ +#!/bin/bash + +# Script to reliably launch iOS Simulator +# Usage: ./launch-ios-simulator.sh [timeout_seconds] +# +# Examples: +# ./launch-ios-simulator.sh "iPhone 17" "26.2" +# ./launch-ios-simulator.sh "iPhone 17" "26.2" 120 +# +# Exit codes: +# 0 - Success +# 1 - Device not found +# 2 - Boot timeout +# 3 - Invalid arguments + +set -euo pipefail + +# Configuration +DEVICE_NAME="${1:-}" +OS_VERSION="${2:-}" +TIMEOUT="${3:-200}" +CHECK_INTERVAL=2 + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +# Helper functions +log_info() { + printf "${BLUE}ℹ${NC} %s\n" "$1" +} + +log_success() { + printf "${GREEN}✓${NC} %s\n" "$1" +} + +log_warning() { + printf "${YELLOW}⚠${NC} %s\n" "$1" +} + +log_error() { + printf "${RED}✗${NC} %s\n" "$1" >&2 +} + +# Validate arguments +if [ -z "$DEVICE_NAME" ] || [ -z "$OS_VERSION" ]; then + log_error "Usage: $0 [timeout_seconds]" + log_error "Example: $0 \"iPhone 17\" \"26.2\" 120" + exit 3 +fi + +log_info "Starting iOS Simulator setup..." +log_info "Device: $DEVICE_NAME" +log_info "OS Version: $OS_VERSION" +log_info "Timeout: ${TIMEOUT}s" + +# Kill any stuck simulators +log_info "Checking for existing Simulator processes..." +if pgrep -x "Simulator" > /dev/null; then + log_warning "Found existing Simulator process, shutting down..." + killall Simulator 2>/dev/null || true + sleep 2 +fi + +# Find the device UDID +log_info "Finding device UDID for '$DEVICE_NAME' with iOS $OS_VERSION..." + +# Convert version like "26.2" to runtime key format "iOS-26-2" +IOS_RUNTIME_KEY=$(echo "$OS_VERSION" | sed 's/\./-/g') + +# Always show available devices +log_info "" +log_info "Available devices:" +xcrun simctl list devices available --json | \ + jq -r '.devices | to_entries[] | select(.key | contains("iOS")) | .key as $runtime | .value[] | + ($runtime | capture("iOS-(?[0-9]+)-(?[0-9]+)") | "iOS \(.major).\(.minor)") as $version | + " \(.name) - \($version) - \(.udid)"' +log_info "" + +# Search for device in the specific iOS version runtime +DEVICE_UDID=$(xcrun simctl list devices available --json | \ + jq -r --arg name "$DEVICE_NAME" --arg runtime_suffix "iOS-${IOS_RUNTIME_KEY}" \ + '.devices | to_entries[] | select(.key | endswith($runtime_suffix)) | .value[] | select(.name == $name and .isAvailable == true) | .udid' | \ + head -n 1) + +if [ -z "$DEVICE_UDID" ]; then + log_error "Device '$DEVICE_NAME' with iOS $OS_VERSION not found or not available" + log_info "" + log_info "Hint: Use exact device name from the list above (e.g., 'iPhone Air' not 'iPhone 17 Air')" + exit 1 +fi + +log_success "Found device: $DEVICE_NAME (UDID: $DEVICE_UDID)" + +# Check current state +CURRENT_STATE=$(xcrun simctl list devices --json | \ + jq -r --arg udid "$DEVICE_UDID" \ + '.devices[] | .[] | select(.udid == $udid) | .state') + +log_info "Current state: $CURRENT_STATE" + +# Shutdown if already booted to ensure clean state +if [ "$CURRENT_STATE" = "Booted" ]; then + log_warning "Device already booted, shutting down for clean start..." + xcrun simctl shutdown "$DEVICE_UDID" 2>/dev/null || true + sleep 2 +fi + +# Boot the simulator +log_info "Booting simulator..." +xcrun simctl boot "$DEVICE_UDID" 2>/dev/null || { + # Check if it's already booting or booted + CURRENT_STATE=$(xcrun simctl list devices --json | \ + jq -r --arg udid "$DEVICE_UDID" \ + '.devices[] | .[] | select(.udid == $udid) | .state') + + if [ "$CURRENT_STATE" != "Booted" ] && [ "$CURRENT_STATE" != "Booting" ]; then + log_error "Failed to boot simulator" + exit 2 + fi +} + +# Wait for simulator to boot +log_info "Waiting for simulator to boot (timeout: ${TIMEOUT}s)..." +ELAPSED=0 +while [ $ELAPSED -lt $TIMEOUT ]; do + STATE=$(xcrun simctl list devices --json | \ + jq -r --arg udid "$DEVICE_UDID" \ + '.devices[] | .[] | select(.udid == $udid) | .state') + + if [ "$STATE" = "Booted" ]; then + log_success "Simulator booted successfully!" + break + fi + + if [ $((ELAPSED % 10)) -eq 0 ] && [ $ELAPSED -gt 0 ]; then + log_info "Still waiting... (${ELAPSED}s elapsed, timeout in $((TIMEOUT - ELAPSED))s)" + fi + + sleep $CHECK_INTERVAL + ELAPSED=$((ELAPSED + CHECK_INTERVAL)) +done + +# Verify boot completed +FINAL_STATE=$(xcrun simctl list devices --json | \ + jq -r --arg udid "$DEVICE_UDID" \ + '.devices[] | .[] | select(.udid == $udid) | .state') + +if [ "$FINAL_STATE" != "Booted" ]; then + log_error "Simulator boot timeout after ${TIMEOUT}s (state: $FINAL_STATE)" + exit 2 +fi + +# Open Simulator.app (optional, for visibility) +log_info "Opening Simulator.app..." +open -a Simulator --args -CurrentDeviceUDID "$DEVICE_UDID" 2>/dev/null || { + log_warning "Failed to open Simulator.app (non-critical)" +} + +# Final verification +# `simctl bootstatus -b` occasionally hangs on CI even when the device is already Booted. +# Guard it with a timeout so this script can never run forever. +log_info "Verifying simulator status..." +if command -v gtimeout >/dev/null 2>&1; then + gtimeout 60 xcrun simctl bootstatus "$DEVICE_UDID" -b 2>/dev/null || { + log_warning "bootstatus check failed or timed out (non-critical)" + } +elif command -v timeout >/dev/null 2>&1; then + timeout 60 xcrun simctl bootstatus "$DEVICE_UDID" -b 2>/dev/null || { + log_warning "bootstatus check failed or timed out (non-critical)" + } +else + # Best effort: run without a guard, but don't block the pipeline forever. + xcrun simctl bootstatus "$DEVICE_UDID" -b 2>/dev/null & + BOOTSTATUS_PID=$! + for _ in $(seq 1 60); do + if ! kill -0 "$BOOTSTATUS_PID" 2>/dev/null; then + wait "$BOOTSTATUS_PID" || log_warning "bootstatus check failed (non-critical)" + BOOTSTATUS_PID="" + break + fi + sleep 1 + done + if [ -n "${BOOTSTATUS_PID:-}" ]; then + kill "$BOOTSTATUS_PID" 2>/dev/null || true + log_warning "bootstatus check timed out (non-critical)" + fi +fi + +# Print simulator info +log_success "Simulator is ready!" +echo "" +echo "Device Information:" +echo " Name: $DEVICE_NAME" +echo " OS Version: iOS $OS_VERSION" +echo " UDID: $DEVICE_UDID" +echo " State: $(xcrun simctl list devices --json | jq -r --arg udid "$DEVICE_UDID" '.devices[] | .[] | select(.udid == $udid) | .state')" +echo "" + +# Export UDID for use in subsequent steps +echo "SIMULATOR_UDID=$DEVICE_UDID" >> "${GITHUB_ENV:-/dev/null}" +log_success "Simulator UDID exported to GITHUB_ENV" + +log_success "Setup complete! Simulator is ready for testing." +exit 0 diff --git a/.github/workflows/android.yml b/.github/workflows/android.yml new file mode 100644 index 0000000..c568fd5 --- /dev/null +++ b/.github/workflows/android.yml @@ -0,0 +1,114 @@ +name: Android Build & Test + +on: + workflow_call: + +env: + DEVICE_API_LEVEL: '34' + DEVICE_ARCH: 'x86_64' + DEVICE_TARGET: 'google_apis' + AVD_NAME: 'Pixel_API_34' + +jobs: + android: + name: Android Build & Test + runs-on: ubuntu-latest + timeout-minutes: 60 + + steps: + - name: Checkout + uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd + + - name: Setup Project + uses: ./.github/actions/setup-node-yarn + + - name: Setup Java + uses: actions/setup-java@f2beeb24e141e01a676f977032f5a29d81c9e27e + with: + distribution: 'zulu' + java-version: '17' + + - name: Setup Android SDK + uses: android-actions/setup-android@9fc6c4e9069bf8d3d10b2204b1fb8f6ef7065407 + + - name: Enable KVM + run: | + echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules + sudo udevadm control --reload-rules + sudo udevadm trigger --name-match=kvm + ls /dev/kvm + + # Build dependencies (only if APK not cached) + - name: Cache NDK + uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 + with: + path: /usr/local/lib/android/sdk/ndk/27.3.13750724 + key: ${{ runner.os }}-ndk-27.3.13750724 + + - name: Install NDK + run: echo "y" | ${ANDROID_HOME}/cmdline-tools/latest/bin/sdkmanager --install "ndk;27.3.13750724" + + - name: Cache Gradle + uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 + with: + path: | + ~/.gradle/caches + ~/.gradle/wrapper + example/android/.gradle + example/android/app/.cxx + example/android/app/build/intermediates + key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties', 'android/build.gradle', 'example/android/app/build.gradle') }} + restore-keys: | + ${{ runner.os }}-gradle- + + # Build APK (only if not cached) + - name: Build APK with Gradle + working-directory: example + run: yarn app:package:android + + - name: AVD cache + uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 + with: + path: | + ~/.android/avd/* + ~/.android/adb* + key: avd-${{ env.DEVICE_API_LEVEL }}-${{ env.DEVICE_ARCH }} + + # Create AVD (only if not cached, runs after build) + - name: Create AVD and generate snapshot for caching + uses: reactivecircus/android-emulator-runner@b530d96654c385303d652368551fb075bc2f0b6b + with: + api-level: ${{ env.DEVICE_API_LEVEL }} + target: ${{ env.DEVICE_TARGET }} + arch: ${{ env.DEVICE_ARCH }} + ram-size: 4096M + disk-size: 6G + force-avd-creation: false + avd-name: ${{ env.AVD_NAME }} + disable-animations: true + emulator-options: -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none -memory 4096 + script: echo "Generated AVD snapshot for caching." + + # Launch AVD, install app, and run tests + - name: Run Harness E2E tests + uses: reactivecircus/android-emulator-runner@b530d96654c385303d652368551fb075bc2f0b6b + with: + working-directory: example + api-level: ${{ env.DEVICE_API_LEVEL }} + target: ${{ env.DEVICE_TARGET }} + arch: ${{ env.DEVICE_ARCH }} + ram-size: 4096M + force-avd-creation: false + avd-name: ${{ env.AVD_NAME }} + disable-animations: true + emulator-boot-timeout: 900 + emulator-options: -no-snapshot-save -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none -memory 4096 + script: | + yarn app:install:android + yarn harness:android + + # Cleanup + - name: Stop Gradle Daemon + if: always() + working-directory: example/android + run: ./gradlew --stop diff --git a/.github/workflows/build-android.yml b/.github/workflows/build-android.yml deleted file mode 100644 index 666f58a..0000000 --- a/.github/workflows/build-android.yml +++ /dev/null @@ -1,32 +0,0 @@ -name: Build Android Example - -on: - workflow_call: - -jobs: - build-android-example: - name: Build Android Example - runs-on: ubuntu-latest - - steps: - - name: Checkout - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd - - - name: Setup Project - uses: ./.github/actions/setup-node-yarn - - - name: Setup Java - uses: actions/setup-java@f2beeb24e141e01a676f977032f5a29d81c9e27e - with: - distribution: 'zulu' - java-version: '17' - - - name: Setup Android SDK - uses: android-actions/setup-android@9fc6c4e9069bf8d3d10b2204b1fb8f6ef7065407 - - - name: Install NDK - run: echo "y" | ${ANDROID_HOME}/cmdline-tools/latest/bin/sdkmanager --install "ndk;27.3.13750724" - - - name: Build with Gradle - run: ./gradlew assembleDebug - working-directory: example/android \ No newline at end of file diff --git a/.github/workflows/build-ios.yml b/.github/workflows/build-ios.yml deleted file mode 100644 index 52aa156..0000000 --- a/.github/workflows/build-ios.yml +++ /dev/null @@ -1,57 +0,0 @@ -name: Build iOS Example - -on: - workflow_call: - -jobs: - build-ios-example: - name: Build iOS Example - runs-on: macos-latest - - steps: - - name: Checkout - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd - - - name: Setup Project - uses: ./.github/actions/setup-node-yarn - - - name: Setup Xcode - run: sudo xcode-select -s /Applications/Xcode_26.1.app - - - name: Setup Ruby - uses: ruby/setup-ruby@d697be2f83c6234b20877c3b5eac7a7f342f0d0c - with: - ruby-version: '3.2' - bundler-cache: true - working-directory: example/ios - - - name: Cache CocoaPods - uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 - with: - path: | - example/ios/Pods - ~/Library/Caches/CocoaPods - ~/.cocoapods - key: ${{ runner.os }}-pods-${{ hashFiles('example/ios/Podfile.lock') }} - restore-keys: | - ${{ runner.os }}-pods- - - - name: Install CocoaPods - working-directory: example/ios - run: | - bundle install - bundle exec pod install - - - name: Build iOS example for simulator - working-directory: example/ios - run: | - xcodebuild \ - -workspace MendixNativeExample.xcworkspace \ - -scheme MendixNativeExample \ - -configuration Debug \ - -sdk iphonesimulator \ - -destination 'platform=iOS Simulator,name=iPhone 17,OS=latest' \ - -derivedDataPath build \ - CODE_SIGNING_ALLOWED=NO \ - CODE_SIGNING_REQUIRED=NO \ - build diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 065c669..64d205f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -8,12 +8,12 @@ jobs: name: Lint uses: ./.github/workflows/lint.yml - build-android: - name: Build Android + android: + name: Android Build & Test needs: lint - uses: ./.github/workflows/build-android.yml + uses: ./.github/workflows/android.yml - build-ios: - name: Build iOS + ios: + name: iOS Build & Test needs: lint - uses: ./.github/workflows/build-ios.yml + uses: ./.github/workflows/ios.yml diff --git a/.github/workflows/ios.yml b/.github/workflows/ios.yml new file mode 100644 index 0000000..b94d3f5 --- /dev/null +++ b/.github/workflows/ios.yml @@ -0,0 +1,89 @@ +name: iOS Build & Test + +on: + workflow_call: + +env: + DEVICE_MODEL: 'iPhone 17' + IOS_VERSION: '26.2' + USE_CCACHE: 1 + +jobs: + ios: + name: iOS Build & Test + runs-on: macos-26 + timeout-minutes: 60 + env: + DEVELOPER_DIR: /Applications/Xcode_26.2.app/Contents/Developer + + steps: + - name: Checkout + uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd + + - name: Setup Project + uses: ./.github/actions/setup-node-yarn + + - name: Setup Xcode + run: sudo xcode-select -s /Applications/Xcode_26.2.app + + - name: Setup Ruby + uses: ruby/setup-ruby@d697be2f83c6234b20877c3b5eac7a7f342f0d0c + with: + ruby-version: '3.2' + bundler-cache: true + working-directory: example + + # Build dependencies (only if app not cached) + - name: Cache CocoaPods + uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 + with: + path: | + example/ios/Pods + ~/Library/Caches/CocoaPods + ~/.cocoapods + key: ${{ runner.os }}-pods-${{ hashFiles('example/ios/Podfile.lock') }} + restore-keys: | + ${{ runner.os }}-pods- + + - name: Install CocoaPods + working-directory: example + run: yarn pod + + # Build iOS app (only if not cached) + - name: Build iOS app for simulator + if: steps.cache-ios-app.outputs.cache-hit != 'true' + working-directory: example + run: yarn app:package:ios + + # Setup simulator (after build) + - name: Launch iOS Simulator + run: | + # Install jq if not available + if ! command -v jq &> /dev/null; then + brew install jq + fi + + # Ensure `gtimeout` is available (coreutils) for robust timeouts on macOS. + if ! command -v gtimeout &> /dev/null; then + brew install coreutils + fi + + # Launch simulator with our custom script + bash .github/scripts/launch-ios-simulator.sh "${{ env.DEVICE_MODEL }}" "${{ env.IOS_VERSION }}" + + # Install app on simulator + - name: Install app on simulator + working-directory: example + run: yarn app:install:ios + + # Wait for app and bridge to be fully ready + - name: Wait for React Native bridge initialization + run: | + echo "Waiting for app and bridge to be fully initialized..." + sleep 10 + echo "Ready to run tests" + + # Run tests + - name: Run Harness E2E tests + working-directory: example + run: yarn harness:ios diff --git a/README.md b/README.md index b82271a..e5036a0 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,10 @@ # Mendix Native +[![CI](https://github.com/mendix/mendix-native/actions/workflows/ci.yml/badge.svg)](https://github.com/mendix/mendix-native/actions/workflows/ci.yml) +[![Lint](https://github.com/mendix/mendix-native/actions/workflows/lint.yml/badge.svg)](https://github.com/mendix/mendix-native/actions/workflows/lint.yml) +[![Android](https://github.com/mendix/mendix-native/actions/workflows/android.yml/badge.svg)](https://github.com/mendix/mendix-native/actions/workflows/android.yml) +[![iOS](https://github.com/mendix/mendix-native/actions/workflows/ios.yml/badge.svg)](https://github.com/mendix/mendix-native/actions/workflows/ios.yml) + Mendix native mobile package for React Native applications. ## Prerequisites @@ -15,6 +20,7 @@ Before you begin, ensure you have the following installed: ## Local Development Setup This project is a monorepo managed using Yarn workspaces with: + - The library package in the root directory - An example app in the `example/` directory @@ -80,10 +86,12 @@ yarn example android #### Edit Native Code **iOS (Objective-C/Swift):** + - Open `example/ios/MendixNativeExample.xcworkspace` in Xcode - Find source files at: `Pods > Development Pods > mendix-native` **Android (Java/Kotlin):** + - Open `example/android` in Android Studio - Find source files under: `mendix-native` in the Android view @@ -231,6 +239,7 @@ Example: ## [Unreleased] ## [1.2.0] - 2025-01-15 + ``` diff --git a/example/__tests__/cookie.harness.ts b/example/__tests__/cookie.harness.ts new file mode 100644 index 0000000..1ced281 --- /dev/null +++ b/example/__tests__/cookie.harness.ts @@ -0,0 +1,65 @@ +import { describe, test, expect, beforeEach } from 'react-native-harness'; +import { NativeCookie } from 'mendix-native'; + +describe('NativeCookie', () => { + describe('API surface', () => { + test('should expose clearAll method', () => { + expect(typeof NativeCookie.clearAll).toBe('function'); + }); + }); + + describe('clearAll', () => { + test('should call clearAll without throwing', async () => { + await expect(NativeCookie.clearAll()).resolves.not.toThrow(); + }); + + test('should return a Promise', () => { + const result = NativeCookie.clearAll(); + expect(result).toBeInstanceOf(Promise); + }); + + test('should resolve to undefined', async () => { + const result = await NativeCookie.clearAll(); + expect(result).toBeOneOf([undefined, null]); + }); + + test('should be callable multiple times', async () => { + await NativeCookie.clearAll(); + await NativeCookie.clearAll(); + await NativeCookie.clearAll(); + + // Should not throw + expect(true).toBe(true); + }); + + test('should handle concurrent clearAll calls', async () => { + const promises = [ + NativeCookie.clearAll(), + NativeCookie.clearAll(), + NativeCookie.clearAll(), + ]; + + await expect(Promise.all(promises)).resolves.not.toThrow(); + }); + }); + + describe('integration scenarios', () => { + beforeEach(async () => { + // Clear cookies before each test + await NativeCookie.clearAll(); + }); + + test('should clear all cookies when called', async () => { + // This test verifies that clearAll can be called successfully + // The actual cookie clearing behavior would need to be tested + // in an integration test with a real web view + await expect(NativeCookie.clearAll()).resolves.not.toThrow(); + }); + + test('should work when no cookies exist', async () => { + // Calling clearAll when there are no cookies should succeed + await NativeCookie.clearAll(); + await expect(NativeCookie.clearAll()).resolves.not.toThrow(); + }); + }); +}); diff --git a/example/__tests__/encrypted-storage.harness.ts b/example/__tests__/encrypted-storage.harness.ts new file mode 100644 index 0000000..fb81841 --- /dev/null +++ b/example/__tests__/encrypted-storage.harness.ts @@ -0,0 +1,183 @@ +import { describe, test, expect, beforeEach } from 'react-native-harness'; +import { RNMendixEncryptedStorage } from 'mendix-native'; + +describe('RNMendixEncryptedStorage', () => { + beforeEach(async () => { + // Clear storage before each test + await RNMendixEncryptedStorage.clear(); + }); + + describe('setItem and getItem', () => { + test('should store and retrieve a string value', async () => { + const key = 'testKey'; + const value = 'testValue'; + + await RNMendixEncryptedStorage.setItem(key, value); + const retrievedValue = await RNMendixEncryptedStorage.getItem(key); + + expect(retrievedValue).toBe(value); + }); + + test('should store and retrieve multiple key-value pairs', async () => { + await RNMendixEncryptedStorage.setItem('key1', 'value1'); + await RNMendixEncryptedStorage.setItem('key2', 'value2'); + await RNMendixEncryptedStorage.setItem('key3', 'value3'); + + const value1 = await RNMendixEncryptedStorage.getItem('key1'); + const value2 = await RNMendixEncryptedStorage.getItem('key2'); + const value3 = await RNMendixEncryptedStorage.getItem('key3'); + + expect(value1).toBe('value1'); + expect(value2).toBe('value2'); + expect(value3).toBe('value3'); + }); + + test('should return null for non-existent key', async () => { + const value = await RNMendixEncryptedStorage.getItem('nonExistentKey'); + expect(value).toBe(null); + }); + + test('should update existing value when setting same key', async () => { + const key = 'updateKey'; + + await RNMendixEncryptedStorage.setItem(key, 'initialValue'); + await RNMendixEncryptedStorage.setItem(key, 'updatedValue'); + + const value = await RNMendixEncryptedStorage.getItem(key); + expect(value).toBe('updatedValue'); + }); + + test('should handle empty string values', async () => { + await RNMendixEncryptedStorage.setItem('emptyKey', ''); + const value = await RNMendixEncryptedStorage.getItem('emptyKey'); + expect(value).toBe(''); + }); + + test('should handle special characters in keys and values', async () => { + const specialKey = 'key-with-special!@#$%^&*()chars'; + const specialValue = 'value with emoji 🔐 and symbols !@#$%'; + + await RNMendixEncryptedStorage.setItem(specialKey, specialValue); + const value = await RNMendixEncryptedStorage.getItem(specialKey); + + expect(value).toBe(specialValue); + }); + + test('should handle JSON stringified objects', async () => { + const obj = { name: 'John', age: 30, active: true }; + const jsonString = JSON.stringify(obj); + + await RNMendixEncryptedStorage.setItem('jsonKey', jsonString); + const retrieved = await RNMendixEncryptedStorage.getItem('jsonKey'); + + expect(retrieved).toBe(jsonString); + expect(JSON.parse(retrieved!)).toEqual(obj); + }); + }); + + describe('removeItem', () => { + test('should remove an existing item', async () => { + await RNMendixEncryptedStorage.setItem('removeKey', 'removeValue'); + await RNMendixEncryptedStorage.removeItem('removeKey'); + + const value = await RNMendixEncryptedStorage.getItem('removeKey'); + expect(value).toBe(null); + }); + + test('should not throw error when removing non-existent item', async () => { + await expect( + RNMendixEncryptedStorage.removeItem('nonExistentKey') + ).resolves.not.toThrow(); + }); + + test('should only remove specified item', async () => { + await RNMendixEncryptedStorage.setItem('key1', 'value1'); + await RNMendixEncryptedStorage.setItem('key2', 'value2'); + + await RNMendixEncryptedStorage.removeItem('key1'); + + const value1 = await RNMendixEncryptedStorage.getItem('key1'); + const value2 = await RNMendixEncryptedStorage.getItem('key2'); + + expect(value1).toBe(null); + expect(value2).toBe('value2'); + }); + }); + + describe('clear', () => { + test('should clear all stored items', async () => { + await RNMendixEncryptedStorage.setItem('key1', 'value1'); + await RNMendixEncryptedStorage.setItem('key2', 'value2'); + await RNMendixEncryptedStorage.setItem('key3', 'value3'); + + await RNMendixEncryptedStorage.clear(); + + const value1 = await RNMendixEncryptedStorage.getItem('key1'); + const value2 = await RNMendixEncryptedStorage.getItem('key2'); + const value3 = await RNMendixEncryptedStorage.getItem('key3'); + + expect(value1).toBe(null); + expect(value2).toBe(null); + expect(value3).toBe(null); + }); + + test('should work on empty storage', async () => { + await expect(RNMendixEncryptedStorage.clear()).resolves.not.toThrow(); + }); + + test('should allow new items after clear', async () => { + await RNMendixEncryptedStorage.setItem('beforeClear', 'value'); + await RNMendixEncryptedStorage.clear(); + await RNMendixEncryptedStorage.setItem('afterClear', 'newValue'); + + const value = await RNMendixEncryptedStorage.getItem('afterClear'); + expect(value).toBe('newValue'); + }); + }); + + describe('IS_ENCRYPTED property', () => { + test('should have IS_ENCRYPTED as a boolean', () => { + expect(typeof RNMendixEncryptedStorage.IS_ENCRYPTED).toBe('boolean'); + }); + + test('should be a constant value (not a function)', () => { + const firstValue = RNMendixEncryptedStorage.IS_ENCRYPTED; + const secondValue = RNMendixEncryptedStorage.IS_ENCRYPTED; + expect(firstValue).toBe(secondValue); + }); + }); + + describe('concurrent operations', () => { + test('should handle concurrent setItem operations', async () => { + await Promise.all([ + RNMendixEncryptedStorage.setItem('concurrent1', 'value1'), + RNMendixEncryptedStorage.setItem('concurrent2', 'value2'), + RNMendixEncryptedStorage.setItem('concurrent3', 'value3'), + ]); + + const [value1, value2, value3] = await Promise.all([ + RNMendixEncryptedStorage.getItem('concurrent1'), + RNMendixEncryptedStorage.getItem('concurrent2'), + RNMendixEncryptedStorage.getItem('concurrent3'), + ]); + + expect(value1).toBe('value1'); + expect(value2).toBe('value2'); + expect(value3).toBe('value3'); + }); + + test('should handle concurrent getItem operations', async () => { + await RNMendixEncryptedStorage.setItem('shared', 'sharedValue'); + + const results = await Promise.all([ + RNMendixEncryptedStorage.getItem('shared'), + RNMendixEncryptedStorage.getItem('shared'), + RNMendixEncryptedStorage.getItem('shared'), + ]); + + results.forEach((result) => { + expect(result).toBe('sharedValue'); + }); + }); + }); +}); diff --git a/example/__tests__/file-system.harness.ts b/example/__tests__/file-system.harness.ts new file mode 100644 index 0000000..ca8b769 --- /dev/null +++ b/example/__tests__/file-system.harness.ts @@ -0,0 +1,277 @@ +import { describe, test, expect } from 'react-native-harness'; +import { NativeFileSystem } from 'mendix-native'; + +describe('NativeFileSystem', () => { + const testFile = 'test-file.txt'; + + describe('Constants', () => { + test('should have constants defined', () => { + const documentDirectory = NativeFileSystem.DocumentDirectoryPath; + const relativePath = + NativeFileSystem.relativeToDocumentsAbsolutePath(testFile); + expect(typeof documentDirectory).toBe('string'); + expect(documentDirectory.length).toBeGreaterThan(0); + expect(relativePath).toBe(`${documentDirectory}/${testFile}`); + expect(NativeFileSystem.SUPPORTS_DIRECTORY_MOVE).toBe(true); + expect(NativeFileSystem.SUPPORTS_ENCRYPTION).toBe(true); + }); + }); + + describe('fileExists', () => { + test('should return false for non-existent file', async () => { + const exists = await NativeFileSystem.fileExists( + NativeFileSystem.relativeToDocumentsAbsolutePath( + 'non-existent-file.txt' + ) + ); + expect(exists).toBe(false); + }); + + test('should throw for non white listed path', async () => { + try { + await NativeFileSystem.fileExists(testFile); + expect(true).toBe(false); // This should not be reached + } catch (error: any) { + const errorMessage = + 'Path needs to be an absolute path to the apps accessible space.'; + expect(error.message).contains(errorMessage); + } + }); + + test('should return true for created file', async () => { + // Create a test file first using writeJson + const testPath = + NativeFileSystem.relativeToDocumentsAbsolutePath('exists-test.json'); + await NativeFileSystem.writeJson({ test: 'data' }, testPath); + + const exists = await NativeFileSystem.fileExists(testPath); + expect(exists).toBe(true); + + // Cleanup + await NativeFileSystem.remove(testPath); + }); + }); + + describe('writeJson and readJson', () => { + test('should write and read JSON object', async () => { + const testData = { name: 'John', age: 30, active: true }; + const filePath = + NativeFileSystem.relativeToDocumentsAbsolutePath('test-object.json'); + + await NativeFileSystem.writeJson(testData, filePath); + const readData = + await NativeFileSystem.readJson(filePath); + + expect(readData).toEqual(testData); + + // Cleanup + await NativeFileSystem.remove(filePath); + }); + + test('should handle nested JSON structures', async () => { + const testData = { + user: { + name: 'Alice', + address: { + street: '123 Main St', + city: 'Springfield', + }, + }, + items: [1, 2, 3], + }; + const filePath = + NativeFileSystem.relativeToDocumentsAbsolutePath('test-nested.json'); + + await NativeFileSystem.writeJson(testData, filePath); + const readData = + await NativeFileSystem.readJson(filePath); + + expect(readData).toEqual(testData); + + // Cleanup + await NativeFileSystem.remove(filePath); + }); + + test('should handle empty object', async () => { + const testData = {}; + const filePath = + NativeFileSystem.relativeToDocumentsAbsolutePath('test-empty.json'); + + await NativeFileSystem.writeJson(testData, filePath); + const readData = await NativeFileSystem.readJson(filePath); + + expect(readData).toEqual(testData); + + // Cleanup + await NativeFileSystem.remove(filePath); + }); + + test('should overwrite existing file', async () => { + const filePath = NativeFileSystem.relativeToDocumentsAbsolutePath( + 'test-overwrite.json' + ); + + await NativeFileSystem.writeJson({ version: 1 }, filePath); + await NativeFileSystem.writeJson({ version: 2 }, filePath); + + const readData = await NativeFileSystem.readJson<{ version: number }>( + filePath + ); + expect(readData).toEqual({ version: 2 }); + + // Cleanup + await NativeFileSystem.remove(filePath); + }); + }); + + describe('remove', () => { + test('should remove existing file', async () => { + const filePath = + NativeFileSystem.relativeToDocumentsAbsolutePath('test-remove.json'); + + await NativeFileSystem.writeJson({ test: 'data' }, filePath); + await NativeFileSystem.remove(filePath); + + const exists = await NativeFileSystem.fileExists(filePath); + expect(exists).toBe(false); + }); + + test('should handle removing non-existent file', async () => { + const filePath = + NativeFileSystem.relativeToDocumentsAbsolutePath('non-existent.json'); + + // Should not throw error + await expect(NativeFileSystem.remove(filePath)).resolves.not.toThrow(); + }); + }); + + describe('move', () => { + test('should move file to new location', async () => { + const sourcePath = + NativeFileSystem.relativeToDocumentsAbsolutePath('source-file.json'); + const destPath = + NativeFileSystem.relativeToDocumentsAbsolutePath('dest-file.json'); + + const testData = { moved: true }; + await NativeFileSystem.writeJson(testData, sourcePath); + await NativeFileSystem.move(sourcePath, destPath); + + const sourceExists = await NativeFileSystem.fileExists(sourcePath); + const destExists = await NativeFileSystem.fileExists(destPath); + + expect(sourceExists).toBe(false); + expect(destExists).toBe(true); + + const readData = + await NativeFileSystem.readJson(destPath); + expect(readData).toEqual(testData); + + // Cleanup + await NativeFileSystem.remove(destPath); + }); + + test('should preserve file content after move', async () => { + const sourcePath = + NativeFileSystem.relativeToDocumentsAbsolutePath('original.json'); + const destPath = + NativeFileSystem.relativeToDocumentsAbsolutePath('moved.json'); + + const testData = { name: 'Test', values: [1, 2, 3] }; + await NativeFileSystem.writeJson(testData, sourcePath); + await NativeFileSystem.move(sourcePath, destPath); + + const readData = + await NativeFileSystem.readJson(destPath); + expect(readData).toEqual(testData); + + // Cleanup + await NativeFileSystem.remove(destPath); + }); + }); + + describe('list', () => { + test('should list files in directory', async () => { + const dirPath = NativeFileSystem.DocumentDirectoryPath; + const files = await NativeFileSystem.list(dirPath); + + expect(Array.isArray(files)).toBe(true); + }); + + test('should return array of strings', async () => { + const dirPath = NativeFileSystem.DocumentDirectoryPath; + const files = await NativeFileSystem.list(dirPath); + + files.forEach((file) => { + expect(typeof file).toBe('string'); + }); + }); + }); + + describe('relativeToDocumentsAbsolutePath', () => { + test('should convert relative path to absolute', () => { + const relativePath = 'test-file.txt'; + const absolutePath = + NativeFileSystem.relativeToDocumentsAbsolutePath(relativePath); + + expect(absolutePath).toContain(NativeFileSystem.DocumentDirectoryPath); + expect(absolutePath).toContain(relativePath); + }); + + test('should return same path if already absolute', () => { + const absolutePath = `${NativeFileSystem.DocumentDirectoryPath}/test.txt`; + const result = + NativeFileSystem.relativeToDocumentsAbsolutePath(absolutePath); + + expect(result).toBe(absolutePath); + }); + + test('should handle nested paths', () => { + const relativePath = 'folder/subfolder/file.txt'; + const absolutePath = + NativeFileSystem.relativeToDocumentsAbsolutePath(relativePath); + + expect(absolutePath).toContain(NativeFileSystem.DocumentDirectoryPath); + expect(absolutePath).toContain('folder/subfolder/file.txt'); + }); + }); + + describe('setEncryptionEnabled', () => { + test('should call setEncryptionEnabled with true', () => { + expect(() => { + NativeFileSystem.setEncryptionEnabled(true); + }).not.toThrow(); + }); + + test('should call setEncryptionEnabled with false', () => { + expect(() => { + NativeFileSystem.setEncryptionEnabled(false); + }).not.toThrow(); + }); + + test('should be callable multiple times', () => { + expect(() => { + NativeFileSystem.setEncryptionEnabled(true); + NativeFileSystem.setEncryptionEnabled(false); + NativeFileSystem.setEncryptionEnabled(true); + }).not.toThrow(); + }); + }); + + describe('API methods exist', () => { + test('should have read method', () => { + expect(typeof NativeFileSystem.read).toBe('function'); + }); + + test('should have readAsDataURL method', () => { + expect(typeof NativeFileSystem.readAsDataURL).toBe('function'); + }); + + test('should have readAsText method', () => { + expect(typeof NativeFileSystem.readAsText).toBe('function'); + }); + + test('should have save method', () => { + expect(typeof NativeFileSystem.save).toBe('function'); + }); + }); +}); diff --git a/example/__tests__/mx-configuration.harness.ts b/example/__tests__/mx-configuration.harness.ts new file mode 100644 index 0000000..ae0b7fa --- /dev/null +++ b/example/__tests__/mx-configuration.harness.ts @@ -0,0 +1,148 @@ +import { describe, test, expect } from 'react-native-harness'; +import { MxConfiguration } from 'mendix-native'; + +describe('MxConfiguration', () => { + describe('Configuration object', () => { + test('should be defined', () => { + expect(MxConfiguration).toBeDefined(); + }); + + test('should be an object', () => { + expect(typeof MxConfiguration).toBe('object'); + }); + + test('should not be null', () => { + expect(MxConfiguration).not.toBe(null); + }); + }); + + describe('Required properties', () => { + test('should have RUNTIME_URL', () => { + expect(MxConfiguration).toHaveProperty('RUNTIME_URL'); + expect(typeof MxConfiguration.RUNTIME_URL).toBe('string'); + }); + + test('should have APP_NAME', () => { + expect(MxConfiguration).toHaveProperty('APP_NAME'); + // APP_NAME can be string or null + const appName = MxConfiguration.APP_NAME; + expect(appName === null || typeof appName === 'string').toBe(true); + }); + + test('should have DATABASE_NAME', () => { + expect(MxConfiguration).toHaveProperty('DATABASE_NAME'); + expect(typeof MxConfiguration.DATABASE_NAME).toBe('string'); + }); + + test('should have WARNINGS_FILTER_LEVEL', () => { + expect(MxConfiguration).toHaveProperty('WARNINGS_FILTER_LEVEL'); + expect(typeof MxConfiguration.WARNINGS_FILTER_LEVEL).toBe('string'); + }); + + test('should have OTA_MANIFEST_PATH', () => { + expect(MxConfiguration).toHaveProperty('OTA_MANIFEST_PATH'); + expect(typeof MxConfiguration.OTA_MANIFEST_PATH).toBe('string'); + }); + }); + + describe('Optional properties', () => { + test('may have NATIVE_DEPENDENCIES', () => { + if ('NATIVE_DEPENDENCIES' in MxConfiguration) { + const deps = MxConfiguration.NATIVE_DEPENDENCIES; + if (deps !== undefined) { + expect(typeof deps).toBe('object'); + } + } + }); + + test('may have IS_DEVELOPER_APP', () => { + if ('IS_DEVELOPER_APP' in MxConfiguration) { + const isDev = MxConfiguration.IS_DEVELOPER_APP; + if (isDev !== undefined) { + expect(typeof isDev).toBe('boolean'); + } + } + }); + + test('may have NATIVE_BINARY_VERSION', () => { + if ('NATIVE_BINARY_VERSION' in MxConfiguration) { + const version = MxConfiguration.NATIVE_BINARY_VERSION; + if (version !== undefined) { + expect(typeof version).toBe('number'); + } + } + }); + + test('may have APP_SESSION_ID', () => { + if ('APP_SESSION_ID' in MxConfiguration) { + const sessionId = MxConfiguration.APP_SESSION_ID; + if (sessionId != null) { + expect(typeof sessionId).toBe('string'); + } + } + }); + }); + + describe('Property values', () => { + test('RUNTIME_URL should not be empty', () => { + expect(MxConfiguration.RUNTIME_URL.length).toBeGreaterThan(0); + }); + + test('DATABASE_NAME should not be empty', () => { + expect(MxConfiguration.DATABASE_NAME.length).toBeGreaterThan(0); + }); + + test('WARNINGS_FILTER_LEVEL should be valid', () => { + const level = MxConfiguration.WARNINGS_FILTER_LEVEL.toLowerCase(); + // We don't enforce specific values but ensure it's not empty + expect(level.length).toBeGreaterThan(0); + }); + + test('OTA_MANIFEST_PATH should not be empty', () => { + expect(MxConfiguration.OTA_MANIFEST_PATH.length).toBeGreaterThan(0); + }); + }); + + describe('Immutability', () => { + test('should be the same object on multiple accesses', () => { + const config1 = MxConfiguration; + const config2 = MxConfiguration; + expect(config1).toBe(config2); + }); + + test('should have consistent property values', () => { + const runtimeUrl1 = MxConfiguration.RUNTIME_URL; + const runtimeUrl2 = MxConfiguration.RUNTIME_URL; + expect(runtimeUrl1).toBe(runtimeUrl2); + }); + }); + + describe('Type structure', () => { + test('should match expected configuration structure', () => { + const config = MxConfiguration; + + // Required fields + expect(config).toHaveProperty('RUNTIME_URL'); + expect(config).toHaveProperty('APP_NAME'); + expect(config).toHaveProperty('DATABASE_NAME'); + expect(config).toHaveProperty('WARNINGS_FILTER_LEVEL'); + expect(config).toHaveProperty('OTA_MANIFEST_PATH'); + }); + }); + + describe('Native dependencies', () => { + test('NATIVE_DEPENDENCIES should be object if present', () => { + if (MxConfiguration.NATIVE_DEPENDENCIES) { + expect(typeof MxConfiguration.NATIVE_DEPENDENCIES).toBe('object'); + + // If present, should be key-value pairs of strings + Object.entries(MxConfiguration.NATIVE_DEPENDENCIES).forEach( + ([key, value]) => { + expect(typeof key).toBe('string'); + expect(typeof value).toBe('string'); + } + ); + } + }); + }); +}); diff --git a/example/__tests__/navigation-mode.harness.ts b/example/__tests__/navigation-mode.harness.ts new file mode 100644 index 0000000..5504c53 --- /dev/null +++ b/example/__tests__/navigation-mode.harness.ts @@ -0,0 +1,122 @@ +import { describe, test, expect } from 'react-native-harness'; +import { AndroidNavigationBar } from 'mendix-native'; + +describe('AndroidNavigationBar', () => { + describe('API surface', () => { + test('should be defined', () => { + expect(AndroidNavigationBar).toBeDefined(); + }); + + test('should be an object', () => { + expect(typeof AndroidNavigationBar).toBe('object'); + }); + + test('should have height property', () => { + expect(AndroidNavigationBar).toHaveProperty('height'); + }); + + test('should have isActive property', () => { + expect(AndroidNavigationBar).toHaveProperty('isActive'); + }); + }); + + describe('height property', () => { + test('should be a number', () => { + expect(typeof AndroidNavigationBar.height).toBe('number'); + }); + + test('should be non-negative', () => { + expect(AndroidNavigationBar.height).toBeGreaterThanOrEqual(0); + }); + + test('should be finite', () => { + expect(Number.isFinite(AndroidNavigationBar.height)).toBe(true); + }); + + test('should be consistent on multiple accesses', () => { + const height1 = AndroidNavigationBar.height; + const height2 = AndroidNavigationBar.height; + expect(height1).toBe(height2); + }); + }); + + describe('isActive property', () => { + test('should be a boolean', () => { + expect(typeof AndroidNavigationBar.isActive).toBe('boolean'); + }); + + test('should be consistent on multiple accesses', () => { + const isActive1 = AndroidNavigationBar.isActive; + const isActive2 = AndroidNavigationBar.isActive; + expect(isActive1).toBe(isActive2); + }); + + test('should be either true or false', () => { + const isActive = AndroidNavigationBar.isActive; + expect(isActive === true || isActive === false).toBe(true); + }); + }); + + describe('Logic consistency', () => { + test('when isActive is false, height could be 0', () => { + if (!AndroidNavigationBar.isActive) { + // Height could be 0 when navigation bar is not active + // But this is not required, just documenting behavior + expect(typeof AndroidNavigationBar.height).toBe('number'); + } + }); + + test('when isActive is true, height should typically be positive', () => { + if (AndroidNavigationBar.isActive) { + // When active, we generally expect a positive height + // However, this may vary by device + expect(typeof AndroidNavigationBar.height).toBe('number'); + } + }); + }); + + describe('Immutability', () => { + test('should return the same object reference', () => { + const nav1 = AndroidNavigationBar; + const nav2 = AndroidNavigationBar; + expect(nav1).toBe(nav2); + }); + + test('properties should remain constant', () => { + const initialHeight = AndroidNavigationBar.height; + const initialIsActive = AndroidNavigationBar.isActive; + + // Access multiple times + for (let i = 0; i < 5; i++) { + expect(AndroidNavigationBar.height).toBe(initialHeight); + expect(AndroidNavigationBar.isActive).toBe(initialIsActive); + } + }); + }); + + describe('Platform-specific behavior', () => { + test('should work on Android platform', () => { + // These properties are Android-specific + // They should still be accessible regardless of platform + expect(AndroidNavigationBar).toBeDefined(); + expect(typeof AndroidNavigationBar.height).toBe('number'); + expect(typeof AndroidNavigationBar.isActive).toBe('boolean'); + }); + }); + + describe('Type safety', () => { + test('should not have additional unexpected properties', () => { + const keys = Object.keys(AndroidNavigationBar); + expect(keys).toContain('height'); + expect(keys).toContain('isActive'); + }); + + test('should match expected structure', () => { + const nav = AndroidNavigationBar; + expect(nav).toHaveProperty('height'); + expect(nav).toHaveProperty('isActive'); + expect(typeof nav.height).toBe('number'); + expect(typeof nav.isActive).toBe('boolean'); + }); + }); +}); diff --git a/example/__tests__/ota.harness.ts b/example/__tests__/ota.harness.ts new file mode 100644 index 0000000..e4bcf78 --- /dev/null +++ b/example/__tests__/ota.harness.ts @@ -0,0 +1,169 @@ +import { describe, test, expect } from 'react-native-harness'; +import { NativeOta } from 'mendix-native'; + +describe('NativeOta', () => { + describe('API surface', () => { + test('should expose download method', () => { + expect(typeof NativeOta.download).toBe('function'); + }); + + test('should expose deploy method', () => { + expect(typeof NativeOta.deploy).toBe('function'); + }); + }); + + describe('download', () => { + test('should accept OtaDownloadConfig parameter', () => { + const config = { + url: 'https://example.com/ota-package.zip', + }; + + // Should not throw when called with valid config + const result = NativeOta.download(config); + expect(result).toBeInstanceOf(Promise); + }); + + test('should return a Promise', () => { + const config = { + url: 'https://example.com/ota-package.zip', + }; + + const result = NativeOta.download(config); + expect(result).toBeInstanceOf(Promise); + }); + + test('should handle different URL formats', () => { + const configs = [ + { url: 'https://example.com/package.zip' }, + { url: 'http://localhost:8080/bundle.zip' }, + { url: 'https://cdn.example.com/v1.0.0/update.zip' }, + ]; + + configs.forEach((config) => { + const result = NativeOta.download(config); + expect(result).toBeInstanceOf(Promise); + }); + }); + }); + + describe('deploy', () => { + test('should accept OtaDeployConfig parameter', () => { + const config = { + otaDeploymentID: 'deployment-123', + otaPackage: '/path/to/package.zip', + extractionDir: '/path/to/extraction/dir', + }; + + const result = NativeOta.deploy(config); + expect(result).toBeInstanceOf(Promise); + }); + + test('should return a Promise', () => { + const config = { + otaDeploymentID: 'deployment-123', + otaPackage: '/path/to/package.zip', + extractionDir: '/path/to/extraction/dir', + }; + + const result = NativeOta.deploy(config); + expect(result).toBeInstanceOf(Promise); + }); + + test('should handle different deployment IDs', () => { + const configs = [ + { + otaDeploymentID: 'deployment-1', + otaPackage: '/path/to/package1.zip', + extractionDir: '/path/to/dir1', + }, + { + otaDeploymentID: 'deployment-2', + otaPackage: '/path/to/package2.zip', + extractionDir: '/path/to/dir2', + }, + { + otaDeploymentID: 'prod-deployment-v1.0.0', + otaPackage: '/path/to/prod.zip', + extractionDir: '/path/to/prod-dir', + }, + ]; + + configs.forEach((config) => { + const result = NativeOta.deploy(config); + expect(result).toBeInstanceOf(Promise); + }); + }); + + test('should handle various path formats', () => { + const configs = [ + { + otaDeploymentID: 'test', + otaPackage: 'package.zip', + extractionDir: 'extraction', + }, + { + otaDeploymentID: 'test', + otaPackage: '/absolute/path/package.zip', + extractionDir: '/absolute/path/extraction', + }, + { + otaDeploymentID: 'test', + otaPackage: './relative/path/package.zip', + extractionDir: './relative/path/extraction', + }, + ]; + + configs.forEach((config) => { + const result = NativeOta.deploy(config); + expect(result).toBeInstanceOf(Promise); + }); + }); + }); + + describe('type safety', () => { + test('download config should require url property', () => { + const config = { + url: 'https://example.com/package.zip', + }; + + // TypeScript should ensure url is present + expect(config.url).toBeDefined(); + expect(typeof config.url).toBe('string'); + }); + + test('deploy config should require all properties', () => { + const config = { + otaDeploymentID: 'deployment-123', + otaPackage: '/path/to/package.zip', + extractionDir: '/path/to/extraction/dir', + }; + + // TypeScript should ensure all required properties are present + expect(config.otaDeploymentID).toBeDefined(); + expect(config.otaPackage).toBeDefined(); + expect(config.extractionDir).toBeDefined(); + }); + }); + + describe('workflow scenarios', () => { + test('should support download then deploy workflow', async () => { + const downloadConfig = { + url: 'https://example.com/ota-package.zip', + }; + + // In a real scenario, download would complete and return package path + // For testing, we just verify the methods can be called in sequence + const downloadPromise = NativeOta.download(downloadConfig); + expect(downloadPromise).toBeInstanceOf(Promise); + + const deployConfig = { + otaDeploymentID: 'deployment-123', + otaPackage: '/path/to/downloaded-package.zip', + extractionDir: '/path/to/extraction', + }; + + const deployPromise = NativeOta.deploy(deployConfig); + expect(deployPromise).toBeInstanceOf(Promise); + }); + }); +}); diff --git a/example/__tests__/splash-screen.harness.ts b/example/__tests__/splash-screen.harness.ts new file mode 100644 index 0000000..85dc188 --- /dev/null +++ b/example/__tests__/splash-screen.harness.ts @@ -0,0 +1,114 @@ +import { describe, test, expect } from 'react-native-harness'; +import { MendixSplashScreen } from 'mendix-native'; + +describe('MendixSplashScreen', () => { + describe('API surface', () => { + test('should expose show method', () => { + expect(typeof MendixSplashScreen.show).toBe('function'); + }); + + test('should expose hide method', () => { + expect(typeof MendixSplashScreen.hide).toBe('function'); + }); + }); + + describe('show', () => { + test('should call show without throwing', () => { + expect(() => { + MendixSplashScreen.show(); + }).not.toThrow(); + }); + + test('should be callable multiple times', () => { + expect(() => { + MendixSplashScreen.show(); + MendixSplashScreen.show(); + MendixSplashScreen.show(); + }).not.toThrow(); + }); + + test('should return undefined', () => { + const result = MendixSplashScreen.show(); + expect(result).toBe(undefined); + }); + }); + + describe('hide', () => { + test('should call hide without throwing', () => { + expect(() => { + MendixSplashScreen.hide(); + }).not.toThrow(); + }); + + test('should be callable multiple times', () => { + expect(() => { + MendixSplashScreen.hide(); + MendixSplashScreen.hide(); + MendixSplashScreen.hide(); + }).not.toThrow(); + }); + + test('should return undefined', () => { + const result = MendixSplashScreen.hide(); + expect(result).toBe(undefined); + }); + }); + + describe('show and hide sequence', () => { + test('should handle show then hide', () => { + expect(() => { + MendixSplashScreen.show(); + MendixSplashScreen.hide(); + }).not.toThrow(); + }); + + test('should handle hide without prior show', () => { + expect(() => { + MendixSplashScreen.hide(); + }).not.toThrow(); + }); + + test('should handle rapid show/hide cycles', () => { + expect(() => { + for (let i = 0; i < 10; i++) { + MendixSplashScreen.show(); + MendixSplashScreen.hide(); + } + }).not.toThrow(); + }); + + test('should handle alternating show/hide calls', () => { + expect(() => { + MendixSplashScreen.show(); + MendixSplashScreen.hide(); + MendixSplashScreen.show(); + MendixSplashScreen.hide(); + }).not.toThrow(); + }); + }); + + describe('edge cases', () => { + test('should handle hide before show', () => { + expect(() => { + MendixSplashScreen.hide(); + MendixSplashScreen.show(); + }).not.toThrow(); + }); + + test('should handle multiple consecutive shows', () => { + expect(() => { + MendixSplashScreen.show(); + MendixSplashScreen.show(); + MendixSplashScreen.show(); + }).not.toThrow(); + }); + + test('should handle multiple consecutive hides', () => { + expect(() => { + MendixSplashScreen.hide(); + MendixSplashScreen.hide(); + MendixSplashScreen.hide(); + }).not.toThrow(); + }); + }); +}); diff --git a/example/ios/MendixNativeExample/AppDelegate.swift b/example/ios/MendixNativeExample/AppDelegate.swift index 52f3677..c6b7531 100644 --- a/example/ios/MendixNativeExample/AppDelegate.swift +++ b/example/ios/MendixNativeExample/AppDelegate.swift @@ -16,10 +16,17 @@ class AppDelegate: RCTAppDelegate { //Start - For MendixApplication compatibility only, not part of React Native template SessionCookieStore.restore() + + guard let bundleUrl = bundleURL() else { + let message = "No script URL provided. Make sure the metro packager is running or you have embedded a JS bundle in your application bundle." + NativeErrorHandler().handle(message: message, stackTrace: []) + return false + } + MxConfiguration.update(from: MendixApp.init( identifier: nil, - bundleUrl: bundleURL()!, + bundleUrl: bundleUrl, runtimeUrl: URL(string: "http://localhost:8081")!, warningsFilter: .none, isDeveloperApp: false, diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock index 0b949f8..676dcb3 100644 --- a/example/ios/Podfile.lock +++ b/example/ios/Podfile.lock @@ -8,7 +8,7 @@ PODS: - hermes-engine (0.78.2): - hermes-engine/Pre-built (= 0.78.2) - hermes-engine/Pre-built (0.78.2) - - MendixNative (0.3.1): + - MendixNative (0.3.2): - DoubleConversion - glog - hermes-engine @@ -1851,7 +1851,7 @@ SPEC CHECKSUMS: fmt: a40bb5bd0294ea969aaaba240a927bd33d878cdd glog: eb93e2f488219332457c3c4eafd2738ddc7e80b8 hermes-engine: 2771b98fb813fdc6f92edd7c9c0035ecabf9fee7 - MendixNative: 358ef00fc883a39da69680c6c2a09ecf85a0b887 + MendixNative: 5d4f642347f8d28bdf18e0af822bdb0ff830a430 op-sqlite: e8590c98ed3f69c072eea7949bfa214088d3f5c1 OpenSSL-Universal: 6082b0bf950e5636fe0d78def171184e2b3899c2 RCT-Folly: e78785aa9ba2ed998ea4151e314036f6c49e6d82 diff --git a/example/jest.config.js b/example/jest.config.js index 8eb675e..af66ed2 100644 --- a/example/jest.config.js +++ b/example/jest.config.js @@ -1,3 +1,11 @@ module.exports = { - preset: 'react-native', + projects: [ + { + displayName: 'react-native-harness', + preset: 'react-native-harness', + testMatch: [ + '/__tests__/**/*.(test|spec|harness).(js|jsx|ts|tsx)', + ], + }, + ], }; diff --git a/example/metro.config.js b/example/metro.config.js index 2da198e..3b52c28 100644 --- a/example/metro.config.js +++ b/example/metro.config.js @@ -1,16 +1,25 @@ const path = require('path'); -const { getDefaultConfig } = require('@react-native/metro-config'); +const { getDefaultConfig, mergeConfig } = require('@react-native/metro-config'); const { withMetroConfig } = require('react-native-monorepo-config'); const root = path.resolve(__dirname, '..'); +const customConfig = { + resolver: { + unstable_enablePackageExports: true, + }, +}; // Custom config can be removed once react native to update to higher version supporting package exports. https://github.com/callstackincubator/react-native-harness/issues/46#issuecomment-3718445067 + +const config = mergeConfig(getDefaultConfig(__dirname), customConfig); + /** * Metro configuration * https://facebook.github.io/metro/docs/configuration * * @type {import('metro-config').MetroConfig} */ -module.exports = withMetroConfig(getDefaultConfig(__dirname), { +module.exports = withMetroConfig(config, { root, dirname: __dirname, + watchFolders: [root], }); diff --git a/example/package.json b/example/package.json index 97316b8..e8fcc96 100644 --- a/example/package.json +++ b/example/package.json @@ -8,7 +8,18 @@ "start": "react-native start", "build:android": "react-native build-android --extra-params \"--no-daemon --console=plain -PreactNativeArchitectures=arm64-v8a\"", "build:ios": "react-native build-ios --mode Debug", - "pod": "(cd ios && bundle install && bundle exec pod install)" + "pod": "bundle install && bundle exec pod install --project-directory=ios", + "app:package:ios": "xcodebuild -workspace ios/MendixNativeExample.xcworkspace -scheme MendixNativeExample -configuration Debug -sdk iphonesimulator -destination 'generic/platform=iOS Simulator' -derivedDataPath ios/build build", + "app:install:ios": "xcrun simctl install booted ios/build/Build/Products/Debug-iphonesimulator/MendixNativeExample.app", + "app:package:android": "(cd android && ./gradlew assembleDebug --no-daemon)", + "app:install:android": "adb install -r android/app/build/outputs/apk/debug/app-debug.apk", + "harness": "react-native-harness --verbose", + "harness:ios": "yarn harness --harnessRunner ios", + "harness:android": "yarn harness --harnessRunner android", + "harness:all": "yarn harness:ios && yarn harness:android", + "harness:all:with:build": "yarn harness:ios:with:build && yarn harness:android:with:build", + "harness:android:with:build": "yarn app:package:android && yarn app:install:android && yarn harness:android", + "harness:ios:with:build": "yarn app:package:ios && yarn app:install:ios && yarn harness:ios" }, "dependencies": { "@op-engineering/op-sqlite": "15.1.5", @@ -24,11 +35,15 @@ "@react-native-community/cli": "18.0.1", "@react-native-community/cli-platform-android": "18.0.1", "@react-native-community/cli-platform-ios": "18.0.1", + "@react-native-harness/jest": "1.0.0-alpha.21", + "@react-native-harness/platform-android": "1.0.0-alpha.21", + "@react-native-harness/platform-apple": "1.0.0-alpha.21", "@react-native/babel-preset": "0.78.2", "@react-native/metro-config": "0.78.2", "@react-native/typescript-config": "0.78.2", "@types/react": "19.0.0", "react-native-builder-bob": "0.40.16", + "react-native-harness": "1.0.0-alpha.21", "react-native-monorepo-config": "0.1.9" }, "engines": { diff --git a/example/rn-harness.config.mjs b/example/rn-harness.config.mjs new file mode 100644 index 0000000..222c0da --- /dev/null +++ b/example/rn-harness.config.mjs @@ -0,0 +1,34 @@ +import { + androidPlatform, + androidEmulator, +} from '@react-native-harness/platform-android'; +import { + applePlatform, + appleSimulator, +} from '@react-native-harness/platform-apple'; + +const config = { + entryPoint: './index.js', + appRegistryComponentName: 'App', + bridgeTimeout: 300000, + bundleStartTimeout: 300000, + maxAppRestarts: 3, + resetEnvironmentBetweenTestFiles: true, + defaultRunner: 'android', + unstable__skipAlreadyIncludedModules: false, + + runners: [ + androidPlatform({ + name: 'android', + device: androidEmulator('Pixel_API_34'), + bundleId: 'mendixnative.example', + }), + applePlatform({ + name: 'ios', + device: appleSimulator('iPhone 17', '26.2'), + bundleId: 'mendixnative.example', + }), + ], +}; + +export default config; diff --git a/ios/Modules/Encryption/EncryptedStorage.swift b/ios/Modules/Encryption/EncryptedStorage.swift index 589c75f..4ec5e89 100644 --- a/ios/Modules/Encryption/EncryptedStorage.swift +++ b/ios/Modules/Encryption/EncryptedStorage.swift @@ -44,7 +44,7 @@ import Foundation } promise.resolve(value) } else if status == errSecItemNotFound { - promise.resolve(nil) + promise.resolve(NSNull()) } else { promise.reject("An error occured while retrieving value", errorCode: Int(status)) } diff --git a/ios/Modules/NativeFsModule/NativeFsModule.swift b/ios/Modules/NativeFsModule/NativeFsModule.swift index 0042045..93daa07 100644 --- a/ios/Modules/NativeFsModule/NativeFsModule.swift +++ b/ios/Modules/NativeFsModule/NativeFsModule.swift @@ -279,7 +279,7 @@ public class NativeFsModule: NSObject { try NativeFsModule.ensureWhiteListedPath(paths) return true } catch let error { - reject(NativeFsModule.INVALID_PATH, NativeFsModule.formatError("Path not accessible"), error) + reject(NativeFsModule.INVALID_PATH, NativeFsModule.formatError("Path needs to be an absolute path to the apps accessible space."), error) return false } } diff --git a/yarn.lock b/yarn.lock index 0dbcb71..fbdbeeb 100644 --- a/yarn.lock +++ b/yarn.lock @@ -42,6 +42,17 @@ __metadata: languageName: node linkType: hard +"@babel/code-frame@npm:^7.28.6": + version: 7.28.6 + resolution: "@babel/code-frame@npm:7.28.6" + dependencies: + "@babel/helper-validator-identifier": "npm:^7.28.5" + js-tokens: "npm:^4.0.0" + picocolors: "npm:^1.1.1" + checksum: 10/93e7ed9e039e3cb661bdb97c26feebafacc6ec13d745881dae5c7e2708f579475daebe7a3b5d23b183bb940b30744f52f4a5bcb65b4df03b79d82fcb38495784 + languageName: node + linkType: hard + "@babel/compat-data@npm:^7.25.2, @babel/compat-data@npm:^7.27.2, @babel/compat-data@npm:^7.27.7, @babel/compat-data@npm:^7.28.5": version: 7.28.5 resolution: "@babel/compat-data@npm:7.28.5" @@ -122,6 +133,19 @@ __metadata: languageName: node linkType: hard +"@babel/generator@npm:^7.28.6": + version: 7.28.6 + resolution: "@babel/generator@npm:7.28.6" + dependencies: + "@babel/parser": "npm:^7.28.6" + "@babel/types": "npm:^7.28.6" + "@jridgewell/gen-mapping": "npm:^0.3.12" + "@jridgewell/trace-mapping": "npm:^0.3.28" + jsesc: "npm:^3.0.2" + checksum: 10/ef2af927e8e0985d02ec4321a242da761a934e927539147c59fdd544034dc7f0e9846f6bf86209aca7a28aee2243ed0fad668adccd48f96d7d6866215173f9af + languageName: node + linkType: hard + "@babel/helper-annotate-as-pure@npm:^7.27.1, @babel/helper-annotate-as-pure@npm:^7.27.3": version: 7.27.3 resolution: "@babel/helper-annotate-as-pure@npm:7.27.3" @@ -161,6 +185,23 @@ __metadata: languageName: node linkType: hard +"@babel/helper-create-class-features-plugin@npm:^7.28.6": + version: 7.28.6 + resolution: "@babel/helper-create-class-features-plugin@npm:7.28.6" + dependencies: + "@babel/helper-annotate-as-pure": "npm:^7.27.3" + "@babel/helper-member-expression-to-functions": "npm:^7.28.5" + "@babel/helper-optimise-call-expression": "npm:^7.27.1" + "@babel/helper-replace-supers": "npm:^7.28.6" + "@babel/helper-skip-transparent-expression-wrappers": "npm:^7.27.1" + "@babel/traverse": "npm:^7.28.6" + semver: "npm:^6.3.1" + peerDependencies: + "@babel/core": ^7.0.0 + checksum: 10/11f55607fcf66827ade745c0616aa3c6086aa655c0fab665dd3c4961829752e4c94c942262db30c4831ef9bce37ad444722e85ef1b7136587e28c6b1ef8ad43c + languageName: node + linkType: hard + "@babel/helper-create-regexp-features-plugin@npm:^7.18.6, @babel/helper-create-regexp-features-plugin@npm:^7.27.1": version: 7.28.5 resolution: "@babel/helper-create-regexp-features-plugin@npm:7.28.5" @@ -245,6 +286,13 @@ __metadata: languageName: node linkType: hard +"@babel/helper-plugin-utils@npm:^7.28.6": + version: 7.28.6 + resolution: "@babel/helper-plugin-utils@npm:7.28.6" + checksum: 10/21c853bbc13dbdddf03309c9a0477270124ad48989e1ad6524b83e83a77524b333f92edd2caae645c5a7ecf264ec6d04a9ebe15aeb54c7f33c037b71ec521e4a + languageName: node + linkType: hard + "@babel/helper-remap-async-to-generator@npm:^7.27.1": version: 7.27.1 resolution: "@babel/helper-remap-async-to-generator@npm:7.27.1" @@ -271,6 +319,19 @@ __metadata: languageName: node linkType: hard +"@babel/helper-replace-supers@npm:^7.28.6": + version: 7.28.6 + resolution: "@babel/helper-replace-supers@npm:7.28.6" + dependencies: + "@babel/helper-member-expression-to-functions": "npm:^7.28.5" + "@babel/helper-optimise-call-expression": "npm:^7.27.1" + "@babel/traverse": "npm:^7.28.6" + peerDependencies: + "@babel/core": ^7.0.0 + checksum: 10/ad2724713a4d983208f509e9607e8f950855f11bd97518a700057eb8bec69d687a8f90dc2da0c3c47281d2e3b79cf1d14ecf1fe3e1ee0a8e90b61aee6759c9a7 + languageName: node + linkType: hard + "@babel/helper-skip-transparent-expression-wrappers@npm:^7.27.1": version: 7.27.1 resolution: "@babel/helper-skip-transparent-expression-wrappers@npm:7.27.1" @@ -334,6 +395,17 @@ __metadata: languageName: node linkType: hard +"@babel/parser@npm:^7.28.6": + version: 7.28.6 + resolution: "@babel/parser@npm:7.28.6" + dependencies: + "@babel/types": "npm:^7.28.6" + bin: + parser: ./bin/babel-parser.js + checksum: 10/483a6fb5f9876ec9cbbb98816f2c94f39ae4d1158d35f87e1c4bf19a1f56027c96a1a3962ff0c8c46e8322a6d9e1c80d26b7f9668410df13d5b5769d9447b010 + languageName: node + linkType: hard + "@babel/plugin-bugfix-firefox-class-in-computed-class-key@npm:^7.25.3, @babel/plugin-bugfix-firefox-class-in-computed-class-key@npm:^7.28.5": version: 7.28.5 resolution: "@babel/plugin-bugfix-firefox-class-in-computed-class-key@npm:7.28.5" @@ -750,6 +822,18 @@ __metadata: languageName: node linkType: hard +"@babel/plugin-transform-class-static-block@npm:^7.27.1": + version: 7.28.6 + resolution: "@babel/plugin-transform-class-static-block@npm:7.28.6" + dependencies: + "@babel/helper-create-class-features-plugin": "npm:^7.28.6" + "@babel/helper-plugin-utils": "npm:^7.28.6" + peerDependencies: + "@babel/core": ^7.12.0 + checksum: 10/bea7836846deefd02d9976ad1b30b5ade0d6329ecd92866db789dcf6aacfaf900b7a77031e25680f8de5ad636a771a5bdca8961361e6218d45d538ec5d9b71cc + languageName: node + linkType: hard + "@babel/plugin-transform-classes@npm:^7.25.0, @babel/plugin-transform-classes@npm:^7.25.4, @babel/plugin-transform-classes@npm:^7.28.4": version: 7.28.4 resolution: "@babel/plugin-transform-classes@npm:7.28.4" @@ -1665,6 +1749,17 @@ __metadata: languageName: node linkType: hard +"@babel/template@npm:^7.28.6": + version: 7.28.6 + resolution: "@babel/template@npm:7.28.6" + dependencies: + "@babel/code-frame": "npm:^7.28.6" + "@babel/parser": "npm:^7.28.6" + "@babel/types": "npm:^7.28.6" + checksum: 10/0ad6e32bf1e7e31bf6b52c20d15391f541ddd645cbd488a77fe537a15b280ee91acd3a777062c52e03eedbc2e1f41548791f6a3697c02476ec5daf49faa38533 + languageName: node + linkType: hard + "@babel/traverse--for-generate-function-map@npm:@babel/traverse@^7.25.3, @babel/traverse@npm:^7.25.2, @babel/traverse@npm:^7.25.3, @babel/traverse@npm:^7.27.1, @babel/traverse@npm:^7.28.0, @babel/traverse@npm:^7.28.3, @babel/traverse@npm:^7.28.4, @babel/traverse@npm:^7.28.5": version: 7.28.5 resolution: "@babel/traverse@npm:7.28.5" @@ -1680,6 +1775,21 @@ __metadata: languageName: node linkType: hard +"@babel/traverse@npm:^7.28.6": + version: 7.28.6 + resolution: "@babel/traverse@npm:7.28.6" + dependencies: + "@babel/code-frame": "npm:^7.28.6" + "@babel/generator": "npm:^7.28.6" + "@babel/helper-globals": "npm:^7.28.0" + "@babel/parser": "npm:^7.28.6" + "@babel/template": "npm:^7.28.6" + "@babel/types": "npm:^7.28.6" + debug: "npm:^4.3.1" + checksum: 10/dd71efe9412433169b805d5c346a6473e539ce30f605752a0d40a0733feba37259bd72bb4ad2ab591e2eaff1ee56633de160c1e98efdc8f373cf33a4a8660275 + languageName: node + linkType: hard + "@babel/types@npm:^7.0.0, @babel/types@npm:^7.20.7, @babel/types@npm:^7.25.2, @babel/types@npm:^7.27.1, @babel/types@npm:^7.27.3, @babel/types@npm:^7.28.2, @babel/types@npm:^7.28.4, @babel/types@npm:^7.28.5, @babel/types@npm:^7.3.3, @babel/types@npm:^7.4.4": version: 7.28.5 resolution: "@babel/types@npm:7.28.5" @@ -1690,6 +1800,16 @@ __metadata: languageName: node linkType: hard +"@babel/types@npm:^7.28.6": + version: 7.28.6 + resolution: "@babel/types@npm:7.28.6" + dependencies: + "@babel/helper-string-parser": "npm:^7.27.1" + "@babel/helper-validator-identifier": "npm:^7.28.5" + checksum: 10/f9c6e52b451065aae5654686ecfc7de2d27dd0fbbc204ee2bd912a71daa359521a32f378981b1cf333ace6c8f86928814452cb9f388a7da59ad468038deb6b5f + languageName: node + linkType: hard + "@bcoe/v8-coverage@npm:^0.2.3": version: 0.2.3 resolution: "@bcoe/v8-coverage@npm:0.2.3" @@ -1697,6 +1817,27 @@ __metadata: languageName: node linkType: hard +"@clack/core@npm:1.0.0-alpha.5": + version: 1.0.0-alpha.5 + resolution: "@clack/core@npm:1.0.0-alpha.5" + dependencies: + picocolors: "npm:^1.0.0" + sisteransi: "npm:^1.0.5" + checksum: 10/b3907cffe8addfb3d6cb6f8abf70e1b27a92af356b63827d7d43e5273859665678ef4ee582a358f9893d4b925c885abcc8baf378aec6a7231dae7bc256199e94 + languageName: node + linkType: hard + +"@clack/prompts@npm:1.0.0-alpha.5": + version: 1.0.0-alpha.5 + resolution: "@clack/prompts@npm:1.0.0-alpha.5" + dependencies: + "@clack/core": "npm:1.0.0-alpha.5" + picocolors: "npm:^1.0.0" + sisteransi: "npm:^1.0.5" + checksum: 10/53388c862d6026bb02a0758caab8421749a48ba28d95815ba974dbfb1cdde3fe209b51338aca2237de0f0d77ae9bee3aa42b33d8a84f62b7b77ba25f7b8c5b06 + languageName: node + linkType: hard + "@commitlint/cli@npm:^19.8.1": version: 19.8.1 resolution: "@commitlint/cli@npm:19.8.1" @@ -2397,6 +2538,20 @@ __metadata: languageName: node linkType: hard +"@jest/console@npm:30.2.0": + version: 30.2.0 + resolution: "@jest/console@npm:30.2.0" + dependencies: + "@jest/types": "npm:30.2.0" + "@types/node": "npm:*" + chalk: "npm:^4.1.2" + jest-message-util: "npm:30.2.0" + jest-util: "npm:30.2.0" + slash: "npm:^3.0.0" + checksum: 10/7cda9793962afa5c7fcfdde0ff5012694683b17941ee3c6a55ea9fd9a02f1c51ec4b4c767b867e1226f85a26af1d0f0d72c6a344e34c5bc4300312ebffd6e50b + languageName: node + linkType: hard + "@jest/console@npm:^29.7.0": version: 29.7.0 resolution: "@jest/console@npm:29.7.0" @@ -2518,6 +2673,16 @@ __metadata: languageName: node linkType: hard +"@jest/pattern@npm:30.0.1": + version: 30.0.1 + resolution: "@jest/pattern@npm:30.0.1" + dependencies: + "@types/node": "npm:*" + jest-regex-util: "npm:30.0.1" + checksum: 10/afd03b4d3eadc9c9970cf924955dee47984a7e767901fe6fa463b17b246f0ddeec07b3e82c09715c54bde3c8abb92074160c0d79967bd23778724f184e7f5b7b + languageName: node + linkType: hard + "@jest/reporters@npm:^29.7.0": version: 29.7.0 resolution: "@jest/reporters@npm:29.7.0" @@ -2555,6 +2720,15 @@ __metadata: languageName: node linkType: hard +"@jest/schemas@npm:30.0.5": + version: 30.0.5 + resolution: "@jest/schemas@npm:30.0.5" + dependencies: + "@sinclair/typebox": "npm:^0.34.0" + checksum: 10/40df4db55d4aeed09d1c7e19caf23788309cea34490a1c5d584c913494195e698b9967e996afc27226cac6d76e7512fe73ae6b9584480695c60dd18a5459cdba + languageName: node + linkType: hard + "@jest/schemas@npm:^29.6.3": version: 29.6.3 resolution: "@jest/schemas@npm:29.6.3" @@ -2587,6 +2761,18 @@ __metadata: languageName: node linkType: hard +"@jest/test-result@npm:^30.2.0": + version: 30.2.0 + resolution: "@jest/test-result@npm:30.2.0" + dependencies: + "@jest/console": "npm:30.2.0" + "@jest/types": "npm:30.2.0" + "@types/istanbul-lib-coverage": "npm:^2.0.6" + collect-v8-coverage: "npm:^1.0.2" + checksum: 10/f58f79c3c3ba6dd15325e05b0b5a300777cd8cc38327f622608b6fe849b1073ee9633e33d1e5d7ef5b97a1ce71543d0ad92674b7a279f53033143e8dd7c22959 + languageName: node + linkType: hard + "@jest/test-sequencer@npm:^29.7.0": version: 29.7.0 resolution: "@jest/test-sequencer@npm:29.7.0" @@ -2622,6 +2808,21 @@ __metadata: languageName: node linkType: hard +"@jest/types@npm:30.2.0": + version: 30.2.0 + resolution: "@jest/types@npm:30.2.0" + dependencies: + "@jest/pattern": "npm:30.0.1" + "@jest/schemas": "npm:30.0.5" + "@types/istanbul-lib-coverage": "npm:^2.0.6" + "@types/istanbul-reports": "npm:^3.0.4" + "@types/node": "npm:*" + "@types/yargs": "npm:^17.0.33" + chalk: "npm:^4.1.2" + checksum: 10/f50fcaea56f873a51d19254ab16762f2ea8ca88e3e08da2e496af5da2b67c322915a4fcd0153803cc05063ffe87ebef2ab4330e0a1b06ab984a26c916cbfc26b + languageName: node + linkType: hard + "@jest/types@npm:^26.6.2": version: 26.6.2 resolution: "@jest/types@npm:26.6.2" @@ -3140,6 +3341,172 @@ __metadata: languageName: node linkType: hard +"@react-native-harness/babel-preset@npm:1.0.0-alpha.21": + version: 1.0.0-alpha.21 + resolution: "@react-native-harness/babel-preset@npm:1.0.0-alpha.21" + dependencies: + "@babel/plugin-transform-class-static-block": "npm:^7.27.1" + babel-plugin-istanbul: "npm:^7.0.1" + peerDependencies: + "@babel/core": ^7.22.0 + checksum: 10/3fa225a2306f4f58e277db39279e07d91199c9876a1ad9fc2d1920b4069b15d7a5340d8ecab907cba6a648a703e0e0fc78b44448dd778b3d6d1bbeb2c7c022a3 + languageName: node + linkType: hard + +"@react-native-harness/bridge@npm:1.0.0-alpha.21": + version: 1.0.0-alpha.21 + resolution: "@react-native-harness/bridge@npm:1.0.0-alpha.21" + dependencies: + "@react-native-harness/tools": "npm:1.0.0-alpha.21" + birpc: "npm:^2.4.0" + tslib: "npm:^2.3.0" + ws: "npm:^8.18.2" + checksum: 10/8b8efde703d1ff8ef9bcac598cfe0ae55ab41cd86d36b25b388163b5272fb7d9997c886651b604ab1b8911b43e27ec749b70d4a70cbbd3b60c447562164f0053 + languageName: node + linkType: hard + +"@react-native-harness/bundler-metro@npm:1.0.0-alpha.21": + version: 1.0.0-alpha.21 + resolution: "@react-native-harness/bundler-metro@npm:1.0.0-alpha.21" + dependencies: + "@react-native-harness/config": "npm:1.0.0-alpha.21" + "@react-native-harness/metro": "npm:1.0.0-alpha.21" + "@react-native-harness/tools": "npm:1.0.0-alpha.21" + connect: "npm:^3.7.0" + nocache: "npm:^4.0.0" + tslib: "npm:^2.3.0" + peerDependencies: + metro: "*" + metro-config: "*" + checksum: 10/0d5a684f2454fd2cebcb404fcd4fe7f0458e9ead7472ccd5a980e96671e0427aba9dfcb99954a5fa1cbb984ada732820d9506b34e1a29aaaa026225012a45e9c + languageName: node + linkType: hard + +"@react-native-harness/cli@npm:1.0.0-alpha.21": + version: 1.0.0-alpha.21 + resolution: "@react-native-harness/cli@npm:1.0.0-alpha.21" + dependencies: + "@react-native-harness/bridge": "npm:1.0.0-alpha.21" + "@react-native-harness/config": "npm:1.0.0-alpha.21" + tslib: "npm:^2.3.0" + peerDependencies: + jest-cli: "*" + checksum: 10/8c2345f9eef8555d61442ee7434c8ddc2fe9e881186001da6d63d105b9f092f4520abd1d1d3f697261c06d1081757a5cca06a804e8f5df5e8fb1a0355d6e287f + languageName: node + linkType: hard + +"@react-native-harness/config@npm:1.0.0-alpha.21": + version: 1.0.0-alpha.21 + resolution: "@react-native-harness/config@npm:1.0.0-alpha.21" + dependencies: + "@react-native-harness/tools": "npm:1.0.0-alpha.21" + tslib: "npm:^2.3.0" + zod: "npm:^3.25.67" + checksum: 10/e27712d87dbcf800da199a6c569f8a8d068c6a6bc015f3ff01235e0d2b9d01bb76d49d6e07753295fc7600bd574726a9b127f4840b31d0df2bc4edde2961c408 + languageName: node + linkType: hard + +"@react-native-harness/jest@npm:1.0.0-alpha.21": + version: 1.0.0-alpha.21 + resolution: "@react-native-harness/jest@npm:1.0.0-alpha.21" + dependencies: + "@jest/test-result": "npm:^30.2.0" + "@react-native-harness/bridge": "npm:1.0.0-alpha.21" + "@react-native-harness/bundler-metro": "npm:1.0.0-alpha.21" + "@react-native-harness/config": "npm:1.0.0-alpha.21" + "@react-native-harness/platforms": "npm:1.0.0-alpha.21" + "@react-native-harness/tools": "npm:1.0.0-alpha.21" + chalk: "npm:^4.1.2" + jest-message-util: "npm:^30.2.0" + jest-util: "npm:^30.2.0" + p-limit: "npm:^7.1.1" + tslib: "npm:^2.3.0" + yargs: "npm:^17.7.2" + checksum: 10/50ac7947a6fc49bcfa4ccdbea4bdb203a189cfa4c425c2b8c717b73c37f8be53799d972b3cb7bddc884bcddc5d5e6dd966b04ee1bc7738ce54121c0ea6434767 + languageName: node + linkType: hard + +"@react-native-harness/metro@npm:1.0.0-alpha.21": + version: 1.0.0-alpha.21 + resolution: "@react-native-harness/metro@npm:1.0.0-alpha.21" + dependencies: + "@react-native-harness/babel-preset": "npm:1.0.0-alpha.21" + "@react-native-harness/config": "npm:1.0.0-alpha.21" + tslib: "npm:^2.3.0" + peerDependencies: + "@react-native-harness/runtime": 1.0.0-alpha.21 + metro: "*" + checksum: 10/1af775503790adcbf23132efeb5eab431f295a160d79fa46d56123012b36839d003bb991952128e31b81c8a537dc65cb7bc25ec5faaaefb912bd44279bb00d4c + languageName: node + linkType: hard + +"@react-native-harness/platform-android@npm:1.0.0-alpha.21": + version: 1.0.0-alpha.21 + resolution: "@react-native-harness/platform-android@npm:1.0.0-alpha.21" + dependencies: + "@react-native-harness/config": "npm:1.0.0-alpha.21" + "@react-native-harness/platforms": "npm:1.0.0-alpha.21" + "@react-native-harness/tools": "npm:1.0.0-alpha.21" + tslib: "npm:^2.3.0" + zod: "npm:^3.25.67" + checksum: 10/ea99d5eddc4da9e82af21b1e75f5c6745cdada2d8ee0c98cc62418b73ab2cc7e0c01818d5e5edcb906958769281a4a5bb0dd9c778ad0bb1696a4325019744c69 + languageName: node + linkType: hard + +"@react-native-harness/platform-apple@npm:1.0.0-alpha.21": + version: 1.0.0-alpha.21 + resolution: "@react-native-harness/platform-apple@npm:1.0.0-alpha.21" + dependencies: + "@react-native-harness/platforms": "npm:1.0.0-alpha.21" + "@react-native-harness/tools": "npm:1.0.0-alpha.21" + tslib: "npm:^2.3.0" + zod: "npm:^3.25.67" + checksum: 10/1a92cb718000445f2d0ff51ef40067290a86b18dd965f18a485dd587af9f66e716060405ba66ed2d486420580c9c20a8d9ecf6478ce564c0f6491d53057bc749 + languageName: node + linkType: hard + +"@react-native-harness/platforms@npm:1.0.0-alpha.21": + version: 1.0.0-alpha.21 + resolution: "@react-native-harness/platforms@npm:1.0.0-alpha.21" + dependencies: + tslib: "npm:^2.3.0" + checksum: 10/82b24477d82be4253e593ae5b7a91b511d5002eedab9c66f946e7f11754878d89855e6297fa8d4e82b92ef278ce97fd01975e3793263731da46605b3319243fe + languageName: node + linkType: hard + +"@react-native-harness/runtime@npm:1.0.0-alpha.21": + version: 1.0.0-alpha.21 + resolution: "@react-native-harness/runtime@npm:1.0.0-alpha.21" + dependencies: + "@react-native-harness/bridge": "npm:1.0.0-alpha.21" + "@vitest/expect": "npm:4.0.16" + "@vitest/spy": "npm:4.0.16" + chai: "npm:^6.2.2" + event-target-shim: "npm:^6.0.2" + use-sync-external-store: "npm:^1.6.0" + zustand: "npm:^5.0.5" + peerDependencies: + react: "*" + react-native: "*" + checksum: 10/410dad9aeda57007259246fa9eeb9408740245bc606971821711f94f3139d613da7954280cbe9ac97bcc1cdfa6f228a29153e2b97434f45283c081b4ec59e752 + languageName: node + linkType: hard + +"@react-native-harness/tools@npm:1.0.0-alpha.21": + version: 1.0.0-alpha.21 + resolution: "@react-native-harness/tools@npm:1.0.0-alpha.21" + dependencies: + "@clack/prompts": "npm:1.0.0-alpha.5" + is-unicode-supported: "npm:^0.1.0" + nano-spawn: "npm:^1.0.2" + picocolors: "npm:^1.1.1" + tslib: "npm:^2.3.0" + peerDependencies: + react-native: "*" + checksum: 10/cbdb54c3334b782a3fb7724be5447922b633186b852d156df359a72fed997c1ede1c36853c7cf1a23c942e6410a7a557dac5750491b38baf1a13dda18b045ff8 + languageName: node + linkType: hard + "@react-native/assets-registry@npm:0.78.2": version: 0.78.2 resolution: "@react-native/assets-registry@npm:0.78.2" @@ -3425,6 +3792,13 @@ __metadata: languageName: node linkType: hard +"@sinclair/typebox@npm:^0.34.0": + version: 0.34.47 + resolution: "@sinclair/typebox@npm:0.34.47" + checksum: 10/a7f279020ae45ce21636424ec6439f0e302c28196ed7d2cddf2f80899580cb454f07ce3a033a1aea7ecbcfe47cefd9801c7514bf3adaaecfd1b7d9c83ca3eb9f + languageName: node + linkType: hard + "@sindresorhus/merge-streams@npm:^2.1.0": version: 2.3.0 resolution: "@sindresorhus/merge-streams@npm:2.3.0" @@ -3450,6 +3824,13 @@ __metadata: languageName: node linkType: hard +"@standard-schema/spec@npm:^1.0.0": + version: 1.1.0 + resolution: "@standard-schema/spec@npm:1.1.0" + checksum: 10/a209615c9e8b2ea535d7db0a5f6aa0f962fd4ab73ee86a46c100fb78116964af1f55a27c1794d4801e534a196794223daa25ff5135021e03c7828aa3d95e1763 + languageName: node + linkType: hard + "@tootallnate/quickjs-emscripten@npm:^0.23.0": version: 0.23.0 resolution: "@tootallnate/quickjs-emscripten@npm:0.23.0" @@ -3498,6 +3879,16 @@ __metadata: languageName: node linkType: hard +"@types/chai@npm:^5.2.2": + version: 5.2.3 + resolution: "@types/chai@npm:5.2.3" + dependencies: + "@types/deep-eql": "npm:*" + assertion-error: "npm:^2.0.1" + checksum: 10/e79947307dc235953622e65f83d2683835212357ca261389116ab90bed369ac862ba28b146b4fed08b503ae1e1a12cb93ce783f24bb8d562950469f4320e1c7c + languageName: node + linkType: hard + "@types/conventional-commits-parser@npm:^5.0.0": version: 5.0.2 resolution: "@types/conventional-commits-parser@npm:5.0.2" @@ -3507,6 +3898,13 @@ __metadata: languageName: node linkType: hard +"@types/deep-eql@npm:*": + version: 4.0.2 + resolution: "@types/deep-eql@npm:4.0.2" + checksum: 10/249a27b0bb22f6aa28461db56afa21ec044fa0e303221a62dff81831b20c8530502175f1a49060f7099e7be06181078548ac47c668de79ff9880241968d43d0c + languageName: node + linkType: hard + "@types/estree@npm:^1.0.6": version: 1.0.8 resolution: "@types/estree@npm:1.0.8" @@ -3530,7 +3928,7 @@ __metadata: languageName: node linkType: hard -"@types/istanbul-lib-coverage@npm:*, @types/istanbul-lib-coverage@npm:^2.0.0, @types/istanbul-lib-coverage@npm:^2.0.1": +"@types/istanbul-lib-coverage@npm:*, @types/istanbul-lib-coverage@npm:^2.0.0, @types/istanbul-lib-coverage@npm:^2.0.1, @types/istanbul-lib-coverage@npm:^2.0.6": version: 2.0.6 resolution: "@types/istanbul-lib-coverage@npm:2.0.6" checksum: 10/3feac423fd3e5449485afac999dcfcb3d44a37c830af898b689fadc65d26526460bedb889db278e0d4d815a670331796494d073a10ee6e3a6526301fe7415778 @@ -3546,7 +3944,7 @@ __metadata: languageName: node linkType: hard -"@types/istanbul-reports@npm:^3.0.0": +"@types/istanbul-reports@npm:^3.0.0, @types/istanbul-reports@npm:^3.0.4": version: 3.0.4 resolution: "@types/istanbul-reports@npm:3.0.4" dependencies: @@ -3620,7 +4018,7 @@ __metadata: languageName: node linkType: hard -"@types/stack-utils@npm:^2.0.0": +"@types/stack-utils@npm:^2.0.0, @types/stack-utils@npm:^2.0.3": version: 2.0.3 resolution: "@types/stack-utils@npm:2.0.3" checksum: 10/72576cc1522090fe497337c2b99d9838e320659ac57fa5560fcbdcbafcf5d0216c6b3a0a8a4ee4fdb3b1f5e3420aa4f6223ab57b82fef3578bec3206425c6cf5 @@ -3643,7 +4041,7 @@ __metadata: languageName: node linkType: hard -"@types/yargs@npm:^17.0.8": +"@types/yargs@npm:^17.0.33, @types/yargs@npm:^17.0.8": version: 17.0.35 resolution: "@types/yargs@npm:17.0.35" dependencies: @@ -3833,6 +4231,46 @@ __metadata: languageName: node linkType: hard +"@vitest/expect@npm:4.0.16": + version: 4.0.16 + resolution: "@vitest/expect@npm:4.0.16" + dependencies: + "@standard-schema/spec": "npm:^1.0.0" + "@types/chai": "npm:^5.2.2" + "@vitest/spy": "npm:4.0.16" + "@vitest/utils": "npm:4.0.16" + chai: "npm:^6.2.1" + tinyrainbow: "npm:^3.0.3" + checksum: 10/1da98c86d394a4955bef381ac2c63a52d2eec0086f55e18858083da928cfdf51e7a30bfd88b1814e861906dae44d089aeab0fcc67b2597a4a8073c70cd14bdf7 + languageName: node + linkType: hard + +"@vitest/pretty-format@npm:4.0.16": + version: 4.0.16 + resolution: "@vitest/pretty-format@npm:4.0.16" + dependencies: + tinyrainbow: "npm:^3.0.3" + checksum: 10/914d5d35fb3b0aa67f8e6065ac3d1f1798b7774e1ad9d1e873e7c6efdc7925c98e0f8188bb13c4f3feb4d80b756c337f7a55cd4f78c50fe786330d0aaede7cfd + languageName: node + linkType: hard + +"@vitest/spy@npm:4.0.16": + version: 4.0.16 + resolution: "@vitest/spy@npm:4.0.16" + checksum: 10/76cbabfdd77adf16904d5c128de67abca650bbc2ed36acc68fca548dc51844c7fc1ac516e384d07341b25ae39318c7c2feb499ffa7283a1a838f762cb0cda6ab + languageName: node + linkType: hard + +"@vitest/utils@npm:4.0.16": + version: 4.0.16 + resolution: "@vitest/utils@npm:4.0.16" + dependencies: + "@vitest/pretty-format": "npm:4.0.16" + tinyrainbow: "npm:^3.0.3" + checksum: 10/07fb3c96867656ff080df7ae6056a8dc23931d0f8bc16e15994c576c580dc6e2dcf71af0964fee197ea7eea4f4ad72c256f56cd3b81599f9e0ba63a228968d50 + languageName: node + linkType: hard + "@vscode/sudo-prompt@npm:^9.0.0": version: 9.3.1 resolution: "@vscode/sudo-prompt@npm:9.3.1" @@ -4010,7 +4448,7 @@ __metadata: languageName: node linkType: hard -"ansi-styles@npm:^5.0.0": +"ansi-styles@npm:^5.0.0, ansi-styles@npm:^5.2.0": version: 5.2.0 resolution: "ansi-styles@npm:5.2.0" checksum: 10/d7f4e97ce0623aea6bc0d90dcd28881ee04cba06c570b97fd3391bd7a268eedfd9d5e2dd4fdcbdd82b8105df5faf6f24aaedc08eaf3da898e702db5948f63469 @@ -4190,6 +4628,13 @@ __metadata: languageName: node linkType: hard +"assertion-error@npm:^2.0.1": + version: 2.0.1 + resolution: "assertion-error@npm:2.0.1" + checksum: 10/a0789dd882211b87116e81e2648ccb7f60340b34f19877dd020b39ebb4714e475eb943e14ba3e22201c221ef6645b7bfe10297e76b6ac95b48a9898c1211ce66 + languageName: node + linkType: hard + "ast-types@npm:^0.13.4": version: 0.13.4 resolution: "ast-types@npm:0.13.4" @@ -4284,6 +4729,19 @@ __metadata: languageName: node linkType: hard +"babel-plugin-istanbul@npm:^7.0.1": + version: 7.0.1 + resolution: "babel-plugin-istanbul@npm:7.0.1" + dependencies: + "@babel/helper-plugin-utils": "npm:^7.0.0" + "@istanbuljs/load-nyc-config": "npm:^1.0.0" + "@istanbuljs/schema": "npm:^0.1.3" + istanbul-lib-instrument: "npm:^6.0.2" + test-exclude: "npm:^6.0.0" + checksum: 10/fe9f865f975aaa7a033de9ccb2b63fdcca7817266c5e98d3e02ac7ffd774c695093d215302796cb3770a71ef4574e7a9b298504c3c0c104cf4b48c8eda67b2a6 + languageName: node + linkType: hard + "babel-plugin-jest-hoist@npm:^29.6.3": version: 29.6.3 resolution: "babel-plugin-jest-hoist@npm:29.6.3" @@ -4445,6 +4903,13 @@ __metadata: languageName: node linkType: hard +"birpc@npm:^2.4.0": + version: 2.9.0 + resolution: "birpc@npm:2.9.0" + checksum: 10/2e620815cb01ac51257ce23d052bc03a6c42003faf0c48f2ae8beca37af44583b7865528752a9d2ce403b7a8dededfff6fef2ceb627399caf9656ab7155e12b6 + languageName: node + linkType: hard + "bl@npm:^4.1.0": version: 4.1.0 resolution: "bl@npm:4.1.0" @@ -4690,6 +5155,13 @@ __metadata: languageName: node linkType: hard +"chai@npm:^6.2.1, chai@npm:^6.2.2": + version: 6.2.2 + resolution: "chai@npm:6.2.2" + checksum: 10/13cda42cc40aa46da04a41cf7e5c61df6b6ae0b4e8a8c8b40e04d6947e4d7951377ea8c14f9fa7fe5aaa9e8bd9ba414f11288dc958d4cee6f5221b9436f2778f + languageName: node + linkType: hard + "chalk@npm:^4.0.0, chalk@npm:^4.1.0, chalk@npm:^4.1.2": version: 4.1.2 resolution: "chalk@npm:4.1.2" @@ -4779,7 +5251,7 @@ __metadata: languageName: node linkType: hard -"ci-info@npm:^4.3.0": +"ci-info@npm:^4.2.0, ci-info@npm:^4.3.0": version: 4.3.1 resolution: "ci-info@npm:4.3.1" checksum: 10/9dc952bef67e665ccde2e7a552d42d5d095529d21829ece060a00925ede2dfa136160c70ef2471ea6ed6c9b133218b47c007f56955c0f1734a2e57f240aa7445 @@ -4888,7 +5360,7 @@ __metadata: languageName: node linkType: hard -"collect-v8-coverage@npm:^1.0.0": +"collect-v8-coverage@npm:^1.0.0, collect-v8-coverage@npm:^1.0.2": version: 1.0.3 resolution: "collect-v8-coverage@npm:1.0.3" checksum: 10/656443261fb7b79cf79e89cba4b55622b07c1d4976c630829d7c5c585c73cda1c2ff101f316bfb19bb9e2c58d724c7db1f70a21e213dcd14099227c5e6019860 @@ -5041,7 +5513,7 @@ __metadata: languageName: node linkType: hard -"connect@npm:^3.6.5": +"connect@npm:^3.6.5, connect@npm:^3.7.0": version: 3.7.0 resolution: "connect@npm:3.7.0" dependencies: @@ -6309,6 +6781,13 @@ __metadata: languageName: node linkType: hard +"event-target-shim@npm:^6.0.2": + version: 6.0.2 + resolution: "event-target-shim@npm:6.0.2" + checksum: 10/aa69fc4193cad3f1e4dc0c2d3f2689ea2d477f5ff2fbee8b65f866035b15658e1985932b06ba2190c3d2cc9cc6802c26facd6c60487590c1a05f44545ec24f42 + languageName: node + linkType: hard + "execa@npm:^4.0.3": version: 4.1.0 resolution: "execa@npm:4.1.0" @@ -7026,7 +7505,7 @@ __metadata: languageName: node linkType: hard -"graceful-fs@npm:^4.1.3, graceful-fs@npm:^4.1.6, graceful-fs@npm:^4.2.0, graceful-fs@npm:^4.2.4, graceful-fs@npm:^4.2.6, graceful-fs@npm:^4.2.9": +"graceful-fs@npm:^4.1.3, graceful-fs@npm:^4.1.6, graceful-fs@npm:^4.2.0, graceful-fs@npm:^4.2.11, graceful-fs@npm:^4.2.4, graceful-fs@npm:^4.2.6, graceful-fs@npm:^4.2.9": version: 4.2.11 resolution: "graceful-fs@npm:4.2.11" checksum: 10/bf152d0ed1dc159239db1ba1f74fdbc40cb02f626770dcd5815c427ce0688c2635a06ed69af364396da4636d0408fcf7d4afdf7881724c3307e46aff30ca49e2 @@ -7992,7 +8471,7 @@ __metadata: languageName: node linkType: hard -"istanbul-lib-instrument@npm:^6.0.0": +"istanbul-lib-instrument@npm:^6.0.0, istanbul-lib-instrument@npm:^6.0.2": version: 6.0.3 resolution: "istanbul-lib-instrument@npm:6.0.3" dependencies: @@ -8267,6 +8746,23 @@ __metadata: languageName: node linkType: hard +"jest-message-util@npm:30.2.0, jest-message-util@npm:^30.2.0": + version: 30.2.0 + resolution: "jest-message-util@npm:30.2.0" + dependencies: + "@babel/code-frame": "npm:^7.27.1" + "@jest/types": "npm:30.2.0" + "@types/stack-utils": "npm:^2.0.3" + chalk: "npm:^4.1.2" + graceful-fs: "npm:^4.2.11" + micromatch: "npm:^4.0.8" + pretty-format: "npm:30.2.0" + slash: "npm:^3.0.0" + stack-utils: "npm:^2.0.6" + checksum: 10/e29ec76e8c8e4da5f5b25198be247535626ccf3a940e93fdd51fc6a6bcf70feaa2921baae3806182a090431d90b08c939eb13fb64249b171d2e9ae3a452a8fd2 + languageName: node + linkType: hard + "jest-message-util@npm:^29.7.0": version: 29.7.0 resolution: "jest-message-util@npm:29.7.0" @@ -8307,6 +8803,13 @@ __metadata: languageName: node linkType: hard +"jest-regex-util@npm:30.0.1": + version: 30.0.1 + resolution: "jest-regex-util@npm:30.0.1" + checksum: 10/fa8dac80c3e94db20d5e1e51d1bdf101cf5ede8f4e0b8f395ba8b8ea81e71804ffd747452a6bb6413032865de98ac656ef8ae43eddd18d980b6442a2764ed562 + languageName: node + linkType: hard + "jest-regex-util@npm:^29.6.3": version: 29.6.3 resolution: "jest-regex-util@npm:29.6.3" @@ -8428,6 +8931,20 @@ __metadata: languageName: node linkType: hard +"jest-util@npm:30.2.0, jest-util@npm:^30.2.0": + version: 30.2.0 + resolution: "jest-util@npm:30.2.0" + dependencies: + "@jest/types": "npm:30.2.0" + "@types/node": "npm:*" + chalk: "npm:^4.1.2" + ci-info: "npm:^4.2.0" + graceful-fs: "npm:^4.2.11" + picomatch: "npm:^4.0.2" + checksum: 10/cf2f2fb83417ea69f9992121561c95cf4e9aad7946819b771b8b52addf78811101b33b51d0a39fa0c305f2751dab262feed7699de052659ff03d51827c8862f5 + languageName: node + linkType: hard + "jest-util@npm:^29.7.0": version: 29.7.0 resolution: "jest-util@npm:29.7.0" @@ -9201,6 +9718,9 @@ __metadata: "@react-native-community/cli": "npm:18.0.1" "@react-native-community/cli-platform-android": "npm:18.0.1" "@react-native-community/cli-platform-ios": "npm:18.0.1" + "@react-native-harness/jest": "npm:1.0.0-alpha.21" + "@react-native-harness/platform-android": "npm:1.0.0-alpha.21" + "@react-native-harness/platform-apple": "npm:1.0.0-alpha.21" "@react-native/babel-preset": "npm:0.78.2" "@react-native/metro-config": "npm:0.78.2" "@react-native/typescript-config": "npm:0.78.2" @@ -9209,6 +9729,7 @@ __metadata: react-native: "npm:0.78.2" react-native-builder-bob: "npm:0.40.16" react-native-gesture-handler: "npm:2.25.0" + react-native-harness: "npm:1.0.0-alpha.21" react-native-monorepo-config: "npm:0.1.9" languageName: unknown linkType: soft @@ -9735,6 +10256,13 @@ __metadata: languageName: node linkType: hard +"nano-spawn@npm:^1.0.2": + version: 1.0.3 + resolution: "nano-spawn@npm:1.0.3" + checksum: 10/72c56e68ae733c81c459a338fd51e2aa3be06b1cca746c2abe83df7acfac7eee008b01833f5a8781f4ac9fc1eafd23036a44755257a669dfcc2ff2453850822a + languageName: node + linkType: hard + "natural-compare@npm:^1.4.0": version: 1.4.0 resolution: "natural-compare@npm:1.4.0" @@ -9793,6 +10321,13 @@ __metadata: languageName: node linkType: hard +"nocache@npm:^4.0.0": + version: 4.0.0 + resolution: "nocache@npm:4.0.0" + checksum: 10/5b018b7900fbc580c327b1a87decb20e8d9d233face8b8dfa286a54304f2cc11bfaea9828d0d4b635ca55a73539269a6b64a85185c4a363cc288fd706f6e3825 + languageName: node + linkType: hard + "node-fetch-native@npm:^1.6.6": version: 1.6.7 resolution: "node-fetch-native@npm:1.6.7" @@ -10192,6 +10727,15 @@ __metadata: languageName: node linkType: hard +"p-limit@npm:^7.1.1": + version: 7.2.0 + resolution: "p-limit@npm:7.2.0" + dependencies: + yocto-queue: "npm:^1.2.1" + checksum: 10/ea1cae6e8bbf84a65cc832e4d6a802ec34902bfe0da2fba0c6fadb72f0c51d48db67b7fb2df8433dde5f4a970cf482b9c2cb4edbfa8cfadd7f4341c6fdb38a5d + languageName: node + linkType: hard + "p-locate@npm:^3.0.0": version: 3.0.0 resolution: "p-locate@npm:3.0.0" @@ -10449,7 +10993,7 @@ __metadata: languageName: node linkType: hard -"picocolors@npm:^1.0.1, picocolors@npm:^1.1.1": +"picocolors@npm:^1.0.0, picocolors@npm:^1.0.1, picocolors@npm:^1.1.1": version: 1.1.1 resolution: "picocolors@npm:1.1.1" checksum: 10/e1cf46bf84886c79055fdfa9dcb3e4711ad259949e3565154b004b260cd356c5d54b31a1437ce9782624bf766272fe6b0154f5f0c744fb7af5d454d2b60db045 @@ -10552,6 +11096,17 @@ __metadata: languageName: node linkType: hard +"pretty-format@npm:30.2.0": + version: 30.2.0 + resolution: "pretty-format@npm:30.2.0" + dependencies: + "@jest/schemas": "npm:30.0.5" + ansi-styles: "npm:^5.2.0" + react-is: "npm:^18.3.1" + checksum: 10/725890d648e3400575eebc99a334a4cd1498e0d36746313913706bbeea20ada27e17c184a3cd45c50f705c16111afa829f3450233fc0fda5eed293c69757e926 + languageName: node + linkType: hard + "pretty-format@npm:^26.6.2": version: 26.6.2 resolution: "pretty-format@npm:26.6.2" @@ -10754,7 +11309,7 @@ __metadata: languageName: node linkType: hard -"react-is@npm:^18.0.0": +"react-is@npm:^18.0.0, react-is@npm:^18.3.1": version: 18.3.1 resolution: "react-is@npm:18.3.1" checksum: 10/d5f60c87d285af24b1e1e7eaeb123ec256c3c8bdea7061ab3932e3e14685708221bf234ec50b21e10dd07f008f1b966a2730a0ce4ff67905b3872ff2042aec22 @@ -10807,6 +11362,22 @@ __metadata: languageName: node linkType: hard +"react-native-harness@npm:1.0.0-alpha.21": + version: 1.0.0-alpha.21 + resolution: "react-native-harness@npm:1.0.0-alpha.21" + dependencies: + "@react-native-harness/babel-preset": "npm:1.0.0-alpha.21" + "@react-native-harness/cli": "npm:1.0.0-alpha.21" + "@react-native-harness/jest": "npm:1.0.0-alpha.21" + "@react-native-harness/metro": "npm:1.0.0-alpha.21" + "@react-native-harness/runtime": "npm:1.0.0-alpha.21" + tslib: "npm:^2.3.0" + bin: + react-native-harness: bin.js + checksum: 10/32f6db33de03987959198eece3614f06f0597b51c61e41c02747e1e2a9fefd5b25710e2e9ba37793e58113d165619cdfe51bf990ad16366b9b134e004b01f38b + languageName: node + linkType: hard + "react-native-monorepo-config@npm:0.1.9": version: 0.1.9 resolution: "react-native-monorepo-config@npm:0.1.9" @@ -11711,7 +12282,7 @@ __metadata: languageName: node linkType: hard -"stack-utils@npm:^2.0.3": +"stack-utils@npm:^2.0.3, stack-utils@npm:^2.0.6": version: 2.0.6 resolution: "stack-utils@npm:2.0.6" dependencies: @@ -12091,6 +12662,13 @@ __metadata: languageName: node linkType: hard +"tinyrainbow@npm:^3.0.3": + version: 3.0.3 + resolution: "tinyrainbow@npm:3.0.3" + checksum: 10/169cc63c15e1378674180f3207c82c05bfa58fc79992e48792e8d97b4b759012f48e95297900ede24a81f0087cf329a0d85bb81109739eacf03c650127b3f6c1 + languageName: node + linkType: hard + "tmp@npm:^0.2.3": version: 0.2.5 resolution: "tmp@npm:0.2.5" @@ -12137,7 +12715,7 @@ __metadata: languageName: node linkType: hard -"tslib@npm:^2.0.1, tslib@npm:^2.1.0": +"tslib@npm:^2.0.1, tslib@npm:^2.1.0, tslib@npm:^2.3.0": version: 2.8.1 resolution: "tslib@npm:2.8.1" checksum: 10/3e2e043d5c2316461cb54e5c7fe02c30ef6dccb3384717ca22ae5c6b5bc95232a6241df19c622d9c73b809bea33b187f6dbc73030963e29950c2141bc32a79f7 @@ -12523,6 +13101,15 @@ __metadata: languageName: node linkType: hard +"use-sync-external-store@npm:^1.6.0": + version: 1.6.0 + resolution: "use-sync-external-store@npm:1.6.0" + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + checksum: 10/b40ad2847ba220695bff2d4ba4f4d60391c0fb4fb012faa7a4c18eb38b69181936f5edc55a522c4d20a788d1a879b73c3810952c9d0fd128d01cb3f22042c09e + languageName: node + linkType: hard + "util-deprecate@npm:^1.0.1": version: 1.0.2 resolution: "util-deprecate@npm:1.0.2" @@ -12801,6 +13388,21 @@ __metadata: languageName: node linkType: hard +"ws@npm:^8.18.2": + version: 8.19.0 + resolution: "ws@npm:8.19.0" + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: ">=5.0.2" + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + checksum: 10/26e4901e93abaf73af9f26a93707c95b4845e91a7a347ec8c569e6e9be7f9df066f6c2b817b2d685544e208207898a750b78461e6e8d810c11a370771450c31b + languageName: node + linkType: hard + "wsl-utils@npm:^0.1.0": version: 0.1.0 resolution: "wsl-utils@npm:0.1.0" @@ -12890,7 +13492,7 @@ __metadata: languageName: node linkType: hard -"yargs@npm:^17.0.0, yargs@npm:^17.3.1, yargs@npm:^17.5.1, yargs@npm:^17.6.2": +"yargs@npm:^17.0.0, yargs@npm:^17.3.1, yargs@npm:^17.5.1, yargs@npm:^17.6.2, yargs@npm:^17.7.2": version: 17.7.2 resolution: "yargs@npm:17.7.2" dependencies: @@ -12912,7 +13514,7 @@ __metadata: languageName: node linkType: hard -"yocto-queue@npm:^1.0.0": +"yocto-queue@npm:^1.0.0, yocto-queue@npm:^1.2.1": version: 1.2.2 resolution: "yocto-queue@npm:1.2.2" checksum: 10/92dd9880c324dbc94ff4b677b7d350ba8d835619062b7102f577add7a59ab4d87f40edc5a03d77d369dfa9d11175b1b2ec4a06a6f8a5d8ce5d1306713f66ee41 @@ -12925,3 +13527,31 @@ __metadata: checksum: 10/b2144b38807673a4254dae06fe1a212729550609e606289c305e45c585b36fab1dbba44fe6cde90db9b28be465ec63f4c2a50867aeec6672f6bc36b6c9a361a0 languageName: node linkType: hard + +"zod@npm:^3.25.67": + version: 3.25.76 + resolution: "zod@npm:3.25.76" + checksum: 10/f0c963ec40cd96858451d1690404d603d36507c1fc9682f2dae59ab38b578687d542708a7fdbf645f77926f78c9ed558f57c3d3aa226c285f798df0c4da16995 + languageName: node + linkType: hard + +"zustand@npm:^5.0.5": + version: 5.0.10 + resolution: "zustand@npm:5.0.10" + peerDependencies: + "@types/react": ">=18.0.0" + immer: ">=9.0.6" + react: ">=18.0.0" + use-sync-external-store: ">=1.2.0" + peerDependenciesMeta: + "@types/react": + optional: true + immer: + optional: true + react: + optional: true + use-sync-external-store: + optional: true + checksum: 10/1a48812ef1f5dc8ead8eb05c9861d1627d4ec79302f3b329cb3029d07e0325c44beb1829d2d2bbc511cf42e4f26be496723813f1117effa1ee10c2e83834c9ff + languageName: node + linkType: hard