diff --git a/.github/workflows/test-ios.yml b/.github/workflows/test-ios.yml new file mode 100644 index 000000000..a202a224f --- /dev/null +++ b/.github/workflows/test-ios.yml @@ -0,0 +1,54 @@ +name: iOS Unit Tests + +on: + push: + branches: + - main + pull_request: + branches: + - main + +jobs: + test-ios: + runs-on: macos-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + cache: 'npm' + + - name: Install JS dependencies + run: npm ci + + - name: Setup Ruby + uses: ruby/setup-ruby@v1 + with: + ruby-version: '3.2' + bundler-cache: true + + - name: Install CocoaPods + run: | + gem install cocoapods + cd ios && pod install + + - name: Run iOS unit tests + run: | + xcodebuild test \ + -workspace ios/OffgridMobile.xcworkspace \ + -scheme OffgridMobile \ + -destination 'platform=iOS Simulator,name=iPhone 16e' \ + -only-testing:OffgridMobileTests \ + | xcpretty --color && exit "${PIPESTATUS[0]}" + + - name: Upload test results + if: always() + uses: actions/upload-artifact@v4 + with: + name: ios-test-results + path: ~/Library/Developer/Xcode/DerivedData/**/Logs/Test/*.xcresult + if-no-files-found: ignore diff --git a/ios/OffgridMobile.xcodeproj/project.pbxproj b/ios/OffgridMobile.xcodeproj/project.pbxproj index aa6f6d29a..ada318e49 100644 --- a/ios/OffgridMobile.xcodeproj/project.pbxproj +++ b/ios/OffgridMobile.xcodeproj/project.pbxproj @@ -16,12 +16,24 @@ 0A7B3D032F3A0B1200CC5FA1 /* PDFExtractorModule.m in Sources */ = {isa = PBXBuildFile; fileRef = 0A7B3D012F3A0B1200CC5FA1 /* PDFExtractorModule.m */; }; 0A7B3D042F3A0B1200CC5FA1 /* PDFExtractorModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A7B3D022F3A0B1200CC5FA1 /* PDFExtractorModule.swift */; }; 13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB51A68108700A75B9A /* Images.xcassets */; }; + 553E18B7CCC207C0885499E4 /* libPods-OffgridMobileTests.a in Frameworks */ = {isa = PBXBuildFile; fileRef = D1B1541769AADA563D6CC44E /* libPods-OffgridMobileTests.a */; }; 761780ED2CA45674006654EE /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 761780EC2CA45674006654EE /* AppDelegate.swift */; }; 80EE15520A374D84DFA0E523 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB81A68108700A75B9A /* PrivacyInfo.xcprivacy */; }; 81AB9BB82411601600AC10FF /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 81AB9BB72411601600AC10FF /* LaunchScreen.storyboard */; }; A084C602C3B4A415DC74D43F /* libPods-OffgridMobile.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 3A3BA1A946E10FA48AA4C0EB /* libPods-OffgridMobile.a */; }; + AABB000100000000000001AA /* OffgridMobileTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AABB000200000000000002AA /* OffgridMobileTests.swift */; }; /* End PBXBuildFile section */ +/* Begin PBXContainerItemProxy section */ + AABB00010000000000001004 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 83CBB9F71A601CBA00E9B192 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 13B07F861A680F5B00A75B9A; + remoteInfo = OffgridMobile; + }; +/* End PBXContainerItemProxy section */ + /* Begin PBXFileReference section */ 049521632F390D4500AA4EB4 /* CoreMLDiffusionModule.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = CoreMLDiffusionModule.m; sourceTree = ""; }; 049521642F390D4500AA4EB4 /* CoreMLDiffusionModule.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoreMLDiffusionModule.swift; sourceTree = ""; }; @@ -39,6 +51,11 @@ 3A3BA1A946E10FA48AA4C0EB /* libPods-OffgridMobile.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-OffgridMobile.a"; sourceTree = BUILT_PRODUCTS_DIR; }; 761780EC2CA45674006654EE /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = AppDelegate.swift; path = OffgridMobile/AppDelegate.swift; sourceTree = ""; }; 81AB9BB72411601600AC10FF /* LaunchScreen.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; name = LaunchScreen.storyboard; path = OffgridMobile/LaunchScreen.storyboard; sourceTree = ""; }; + AABB000200000000000002AA /* OffgridMobileTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OffgridMobileTests.swift; sourceTree = ""; }; + AABB000400000000000004AA /* OffgridMobileTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = OffgridMobileTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + B9DE36A1FFE10AF8CD81DBD2 /* Pods-OffgridMobileTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-OffgridMobileTests.release.xcconfig"; path = "Target Support Files/Pods-OffgridMobileTests/Pods-OffgridMobileTests.release.xcconfig"; sourceTree = ""; }; + D0917E571600B3FFEDA59EF7 /* Pods-OffgridMobileTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-OffgridMobileTests.debug.xcconfig"; path = "Target Support Files/Pods-OffgridMobileTests/Pods-OffgridMobileTests.debug.xcconfig"; sourceTree = ""; }; + D1B1541769AADA563D6CC44E /* libPods-OffgridMobileTests.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-OffgridMobileTests.a"; sourceTree = BUILT_PRODUCTS_DIR; }; ED297162215061F000B7C4FE /* JavaScriptCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = JavaScriptCore.framework; path = System/Library/Frameworks/JavaScriptCore.framework; sourceTree = SDKROOT; }; /* End PBXFileReference section */ @@ -52,6 +69,14 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + AABB000800000000000008AA /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 553E18B7CCC207C0885499E4 /* libPods-OffgridMobileTests.a in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ @@ -79,6 +104,7 @@ children = ( ED297162215061F000B7C4FE /* JavaScriptCore.framework */, 3A3BA1A946E10FA48AA4C0EB /* libPods-OffgridMobile.a */, + D1B1541769AADA563D6CC44E /* libPods-OffgridMobileTests.a */, ); name = Frameworks; sourceTree = ""; @@ -94,6 +120,7 @@ isa = PBXGroup; children = ( 13B07FAE1A68108700A75B9A /* OffgridMobile */, + AABB000500000000000005AA /* OffgridMobileTests */, 832341AE1AAA6A7D00B99B32 /* Libraries */, 83CBBA001A601CBA00E9B192 /* Products */, 2D16E6871FA4F8E400B85C8A /* Frameworks */, @@ -108,15 +135,26 @@ isa = PBXGroup; children = ( 13B07F961A680F5B00A75B9A /* OffgridMobile.app */, + AABB000400000000000004AA /* OffgridMobileTests.xctest */, ); name = Products; sourceTree = ""; }; + AABB000500000000000005AA /* OffgridMobileTests */ = { + isa = PBXGroup; + children = ( + AABB000200000000000002AA /* OffgridMobileTests.swift */, + ); + path = OffgridMobileTests; + sourceTree = ""; + }; BBD78D7AC51CEA395F1C20DB /* Pods */ = { isa = PBXGroup; children = ( 2BD3167161334CCC189096E3 /* Pods-OffgridMobile.debug.xcconfig */, 37BD4C6C3858A907C678B5B4 /* Pods-OffgridMobile.release.xcconfig */, + D0917E571600B3FFEDA59EF7 /* Pods-OffgridMobileTests.debug.xcconfig */, + B9DE36A1FFE10AF8CD81DBD2 /* Pods-OffgridMobileTests.release.xcconfig */, ); path = Pods; sourceTree = ""; @@ -124,6 +162,25 @@ /* End PBXGroup section */ /* Begin PBXNativeTarget section */ + 00E356ED1AD99517003FC87E /* OffgridMobileTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = AABB00010000000000001003 /* Build configuration list for PBXNativeTarget "OffgridMobileTests" */; + buildPhases = ( + 61A28276E96052FBB39B62C5 /* [CP] Check Pods Manifest.lock */, + AABB000700000000000007AA /* Sources */, + AABB000800000000000008AA /* Frameworks */, + AABB000900000000000009AA /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + AABB00010000000000001005 /* PBXTargetDependency */, + ); + name = OffgridMobileTests; + productName = OffgridMobileTests; + productReference = AABB000400000000000004AA /* OffgridMobileTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; 13B07F861A680F5B00A75B9A /* OffgridMobile */ = { isa = PBXNativeTarget; buildConfigurationList = 13B07F931A680F5B00A75B9A /* Build configuration list for PBXNativeTarget "OffgridMobile" */; @@ -153,6 +210,9 @@ attributes = { LastUpgradeCheck = 1210; TargetAttributes = { + 00E356ED1AD99517003FC87E = { + TestTargetID = 13B07F861A680F5B00A75B9A; + }; 13B07F861A680F5B00A75B9A = { LastSwiftMigration = 1120; }; @@ -175,6 +235,7 @@ projectRoot = ""; targets = ( 13B07F861A680F5B00A75B9A /* OffgridMobile */, + 00E356ED1AD99517003FC87E /* OffgridMobileTests */, ); }; /* End PBXProject section */ @@ -191,6 +252,13 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + AABB000900000000000009AA /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ @@ -240,17 +308,35 @@ inputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-OffgridMobile/Pods-OffgridMobile-frameworks-${CONFIGURATION}-input-files.xcfilelist", ); - inputPaths = ( - ); name = "[CP] Embed Pods Frameworks"; outputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-OffgridMobile/Pods-OffgridMobile-frameworks-${CONFIGURATION}-output-files.xcfilelist", ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-OffgridMobile/Pods-OffgridMobile-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; + 61A28276E96052FBB39B62C5 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-OffgridMobileTests-checkManifestLockResult.txt", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-OffgridMobile/Pods-OffgridMobile-frameworks.sh\"\n"; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; showEnvVarsInLog = 0; }; DADC570D62064073AFEE927B /* [CP] Copy Pods Resources */ = { @@ -261,14 +347,10 @@ inputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-OffgridMobile/Pods-OffgridMobile-resources-${CONFIGURATION}-input-files.xcfilelist", ); - inputPaths = ( - ); name = "[CP] Copy Pods Resources"; outputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-OffgridMobile/Pods-OffgridMobile-resources-${CONFIGURATION}-output-files.xcfilelist", ); - outputPaths = ( - ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-OffgridMobile/Pods-OffgridMobile-resources.sh\"\n"; @@ -291,8 +373,24 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + AABB000700000000000007AA /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + AABB000100000000000001AA /* OffgridMobileTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXSourcesBuildPhase section */ +/* Begin PBXTargetDependency section */ + AABB00010000000000001005 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 13B07F861A680F5B00A75B9A /* OffgridMobile */; + targetProxy = AABB00010000000000001004 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + /* Begin XCBuildConfiguration section */ 13B07F941A680F5B00A75B9A /* Debug */ = { isa = XCBuildConfiguration; @@ -502,6 +600,34 @@ }; name = Release; }; + AABB00010000000000001001 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = D0917E571600B3FFEDA59EF7 /* Pods-OffgridMobileTests.debug.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(BUILT_PRODUCTS_DIR)/OffgridMobile.app/OffgridMobile"; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; + PRODUCT_BUNDLE_IDENTIFIER = ai.offgridmobile.tests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUNDLE_LOADER)"; + }; + name = Debug; + }; + AABB00010000000000001002 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = B9DE36A1FFE10AF8CD81DBD2 /* Pods-OffgridMobileTests.release.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(BUILT_PRODUCTS_DIR)/OffgridMobile.app/OffgridMobile"; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; + PRODUCT_BUNDLE_IDENTIFIER = ai.offgridmobile.tests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUNDLE_LOADER)"; + }; + name = Release; + }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ @@ -523,6 +649,15 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; + AABB00010000000000001003 /* Build configuration list for PBXNativeTarget "OffgridMobileTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + AABB00010000000000001001 /* Debug */, + AABB00010000000000001002 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; /* End XCConfigurationList section */ /* Begin XCRemoteSwiftPackageReference section */ diff --git a/ios/OffgridMobileTests/OffgridMobileTests.swift b/ios/OffgridMobileTests/OffgridMobileTests.swift new file mode 100644 index 000000000..e0be7fd41 --- /dev/null +++ b/ios/OffgridMobileTests/OffgridMobileTests.swift @@ -0,0 +1,183 @@ +import XCTest +import PDFKit + +@testable import OffgridMobile + +// MARK: - PDFExtractorModule Tests + +final class PDFExtractorModuleTests: XCTestCase { + + private var module: PDFExtractorModule! + + override func setUp() { + super.setUp() + module = PDFExtractorModule() + } + + /// Creates a single-page PDF containing `text` and returns a file URL in the temp directory. + private func makeTempPDF(text: String) -> URL { + let url = FileManager.default.temporaryDirectory + .appendingPathComponent(UUID().uuidString + ".pdf") + let renderer = UIGraphicsPDFRenderer(bounds: CGRect(x: 0, y: 0, width: 612, height: 792)) + let data = renderer.pdfData { ctx in + ctx.beginPage() + let attrs: [NSAttributedString.Key: Any] = [.font: UIFont.systemFont(ofSize: 12)] + text.draw(in: CGRect(x: 72, y: 72, width: 468, height: 648), withAttributes: attrs) + } + try! data.write(to: url) + return url + } + + func testExtractTextResolvesWithContent() { + let url = makeTempPDF(text: "Hello, PDF World!") + let exp = expectation(description: "resolve") + + module.extractText( + url.absoluteString, + maxChars: 10_000, + resolver: { result in + XCTAssertNotNil(result) + exp.fulfill() + }, + rejecter: { _, _, _ in + XCTFail("extractText should not reject a valid PDF") + exp.fulfill() + } + ) + + waitForExpectations(timeout: 5) + try? FileManager.default.removeItem(at: url) + } + + func testExtractTextTruncatesAtMaxChars() { + let longText = String(repeating: "A", count: 300) + let url = makeTempPDF(text: longText) + let exp = expectation(description: "truncate") + + module.extractText( + url.absoluteString, + maxChars: 50, + resolver: { result in + let text = (result as? String) ?? "" + XCTAssertTrue( + text.contains("... [Extracted"), + "Truncated text should contain page marker, got: \(text.prefix(120))" + ) + exp.fulfill() + }, + rejecter: { _, _, _ in + XCTFail("extractText should not reject") + exp.fulfill() + } + ) + + waitForExpectations(timeout: 5) + try? FileManager.default.removeItem(at: url) + } + + func testExtractTextRejectsInvalidPath() { + let exp = expectation(description: "reject invalid path") + + module.extractText( + "/nonexistent/path/file.pdf", + maxChars: 10_000, + resolver: { _ in + XCTFail("extractText should reject a non-existent file") + exp.fulfill() + }, + rejecter: { code, _, _ in + XCTAssertEqual(code, "PDF_ERROR") + exp.fulfill() + } + ) + + waitForExpectations(timeout: 5) + } +} + +// MARK: - CoreMLDiffusionModule Tests + +final class CoreMLDiffusionModuleTests: XCTestCase { + + private var module: CoreMLDiffusionModule! + + override func setUp() { + super.setUp() + module = CoreMLDiffusionModule() + } + + func testSupportedEvents() { + let events = module.supportedEvents()! + XCTAssertTrue(events.contains("LocalDreamProgress")) + XCTAssertTrue(events.contains("LocalDreamError")) + XCTAssertEqual(events.count, 2) + } + + func testIsNpuSupportedReturnsTrue() { + let exp = expectation(description: "isNpuSupported") + module.isNpuSupported( + { value in + XCTAssertEqual(value as? Bool, true) + exp.fulfill() + }, + rejecter: { _, _, _ in XCTFail("unexpected reject"); exp.fulfill() } + ) + waitForExpectations(timeout: 2) + } + + func testIsGeneratingReturnsFalseInitially() { + let exp = expectation(description: "isGenerating") + module.isGenerating( + { value in + XCTAssertEqual(value as? Bool, false) + exp.fulfill() + }, + rejecter: { _, _, _ in XCTFail("unexpected reject"); exp.fulfill() } + ) + waitForExpectations(timeout: 2) + } + + func testIsModelLoadedReturnsFalseInitially() { + let exp = expectation(description: "isModelLoaded") + module.isModelLoaded( + { value in + XCTAssertEqual(value as? Bool, false) + exp.fulfill() + }, + rejecter: { _, _, _ in XCTFail("unexpected reject"); exp.fulfill() } + ) + waitForExpectations(timeout: 2) + } + + func testCancelGenerationSucceeds() { + let exp = expectation(description: "cancelGeneration") + module.cancelGeneration( + { value in + XCTAssertEqual(value as? Bool, true) + exp.fulfill() + }, + rejecter: { _, _, _ in XCTFail("unexpected reject"); exp.fulfill() } + ) + waitForExpectations(timeout: 2) + } +} + +// MARK: - DownloadManagerModule Tests + +final class DownloadManagerModuleTests: XCTestCase { + + private var module: DownloadManagerModule! + + override func setUp() { + super.setUp() + module = DownloadManagerModule() + } + + func testSupportedEventsContainsAllExpectedEvents() { + let events = module.supportedEvents()! + XCTAssertTrue(events.contains("DownloadProgress")) + XCTAssertTrue(events.contains("DownloadComplete")) + XCTAssertTrue(events.contains("DownloadError")) + XCTAssertEqual(events.count, 3) + } +} diff --git a/ios/Podfile b/ios/Podfile index 2a9ca2dfe..9faab368f 100644 --- a/ios/Podfile +++ b/ios/Podfile @@ -23,6 +23,10 @@ target 'OffgridMobile' do :app_path => "#{Pod::Config.instance.installation_root}/.." ) + target 'OffgridMobileTests' do + inherit! :search_paths + end + post_install do |installer| react_native_post_install( installer, diff --git a/ios/Podfile.lock b/ios/Podfile.lock index faf8f1b93..2e4e142d8 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -3608,6 +3608,6 @@ SPEC CHECKSUMS: whisper-rn: 7566faf9b7d78e39ab9fc634cb90fdee81177793 Yoga: 5456bb010373068fc92221140921b09d126b116e -PODFILE CHECKSUM: 2d58f6c10e8da008e32da26f04d8b4cac1768a91 +PODFILE CHECKSUM: 31818a1f7d1207c486dba2e42df373cf65ace073 COCOAPODS: 1.16.2 diff --git a/package.json b/package.json index 2749563e8..e65c34551 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ "test:e2e": "./scripts/run-tests.sh", "test:e2e:all": "./scripts/run-tests.sh", "test:e2e:single": "maestro test", + "test:ios": "cd ios && xcodebuild test -workspace OffgridMobile.xcworkspace -scheme OffgridMobile -destination 'platform=iOS Simulator,name=iPhone 16e' -only-testing:OffgridMobileTests | xcpretty", "postinstall": "patch-package" }, "dependencies": {