From d6273a402450eb5b9dd82990a9f70d3fa47dc80a Mon Sep 17 00:00:00 2001 From: corvid-agent <0xOpenBytes@gmail.com> Date: Fri, 27 Feb 2026 11:08:25 -0700 Subject: [PATCH 1/2] Improve CI workflows and expand Navigator test coverage - Add pull_request trigger to macOS workflow - Add swift-actions/setup-swift@v2 with Swift 6.1.0 to macOS workflow - Upgrade actions/checkout from v3 to v4 in both workflows - Upgrade upload-pages-artifact from v1 to v3 and deploy-pages from v1 to v4 - Add 16 new tests covering removeLast with count, dismiss priority ordering, popToRoot with active dialogs, model identity (unique IDs), sheet onDismiss callbacks, and alert replacement behavior Closes #9 Co-Authored-By: Claude Opus 4.6 --- .github/workflows/docc.yml | 6 +- .github/workflows/macOS.yml | 8 +- Tests/NavigationTests/NavigationTests.swift | 212 ++++++++++++++++++++ 3 files changed, 222 insertions(+), 4 deletions(-) diff --git a/.github/workflows/docc.yml b/.github/workflows/docc.yml index 78d4467..2ec7944 100644 --- a/.github/workflows/docc.yml +++ b/.github/workflows/docc.yml @@ -20,7 +20,7 @@ jobs: with: xcode-version: 16.0 - name: git checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: docbuild run: > sudo xcode-select -s /Applications/Xcode_16.0.app; @@ -34,9 +34,9 @@ jobs: echo "" > docs/index.html; - name: artifacts - uses: actions/upload-pages-artifact@v1 + uses: actions/upload-pages-artifact@v3 with: path: docs - name: deploy id: deployment - uses: actions/deploy-pages@v1 + uses: actions/deploy-pages@v4 diff --git a/.github/workflows/macOS.yml b/.github/workflows/macOS.yml index a4f2fa8..be84d39 100644 --- a/.github/workflows/macOS.yml +++ b/.github/workflows/macOS.yml @@ -3,6 +3,8 @@ name: macOS on: push: branches: ["**"] + pull_request: + branches: ["**"] jobs: build: @@ -11,7 +13,11 @@ jobs: - uses: maxim-lobanov/setup-xcode@v1 with: xcode-version: 16.0 - - uses: actions/checkout@v3 + - name: Set up Swift + uses: swift-actions/setup-swift@v2 + with: + swift-version: '6.1.0' + - uses: actions/checkout@v4 - name: Build run: swift build -v - name: Run tests diff --git a/Tests/NavigationTests/NavigationTests.swift b/Tests/NavigationTests/NavigationTests.swift index 8b9f719..c12eb47 100644 --- a/Tests/NavigationTests/NavigationTests.swift +++ b/Tests/NavigationTests/NavigationTests.swift @@ -136,3 +136,215 @@ func testDismissConfirmationDialog() { navigator.dismiss() #expect(navigator.confirmDialog == nil, "Navigator confirmation dialog should be nil after dismissal.") } + +// MARK: - Path Management Tests + +@Test("Remove Last with Explicit Count") +@MainActor +func testRemoveLastWithCount() { + let navigator = Navigator() + navigator.append("Path1") + navigator.append("Path2") + navigator.append("Path3") + #expect(navigator.path.count == 3) + navigator.removeLast(2) + #expect(navigator.path.count == 1, "removeLast(2) should remove exactly two elements.") +} + +@Test("Multiple Appends") +@MainActor +func testMultipleAppends() { + let navigator = Navigator() + navigator.append("A") + navigator.append("B") + navigator.append("C") + #expect(navigator.path.count == 3, "Navigator path should contain three elements after three appends.") +} + +@Test("Remove Last Default Count") +@MainActor +func testRemoveLastDefaultCount() { + let navigator = Navigator() + navigator.append("A") + navigator.append("B") + navigator.removeLast() + #expect(navigator.path.count == 1, "removeLast() should remove exactly one element by default.") +} + +// MARK: - Dismiss Priority Tests + +@Test("Dismiss Prioritizes Alert over Confirm Dialog") +@MainActor +func testDismissPrioritizesAlertOverConfirmDialog() { + let navigator = Navigator() + navigator.confirmDialog( + title: "Dialog", + message: { Text("Message") }, + actions: { EmptyView() } + ) + navigator.alert( + title: "Alert", + message: { Text("Alert Message") }, + actions: { EmptyView() } + ) + #expect(navigator.alert != nil) + #expect(navigator.confirmDialog != nil) + navigator.dismiss() + #expect(navigator.alert == nil, "Dismiss should clear alert first.") + #expect(navigator.confirmDialog != nil, "Confirm dialog should remain after dismissing alert.") +} + +@Test("Dismiss Prioritizes Confirm Dialog over Sheet") +@MainActor +func testDismissPrioritizesConfirmDialogOverSheet() { + let navigator = Navigator() + navigator.sheet { Text("Sheet") } + navigator.confirmDialog( + title: "Dialog", + message: { Text("Message") }, + actions: { EmptyView() } + ) + #expect(navigator.sheet != nil) + #expect(navigator.confirmDialog != nil) + navigator.dismiss() + #expect(navigator.confirmDialog == nil, "Dismiss should clear confirm dialog first.") + #expect(navigator.sheet != nil, "Sheet should remain after dismissing confirm dialog.") +} + +@Test("Dismiss Prioritizes Sheet over Path") +@MainActor +func testDismissPrioritizesSheetOverPath() { + let navigator = Navigator() + navigator.append("Path1") + navigator.sheet { Text("Sheet") } + #expect(navigator.sheet != nil) + #expect(navigator.path.count == 1) + navigator.dismiss() + #expect(navigator.sheet == nil, "Dismiss should clear sheet first.") + #expect(navigator.path.count == 1, "Path should remain after dismissing sheet.") +} + +@Test("Dismiss Falls Through to Path") +@MainActor +func testDismissFallsThroughToPath() { + let navigator = Navigator() + navigator.append("Path1") + navigator.append("Path2") + #expect(navigator.path.count == 2) + navigator.dismiss() + #expect(navigator.path.count == 1, "Dismiss should remove last path element when no dialogs are present.") +} + +// MARK: - Pop to Root with Active Dialogs + +@Test("Pop to Root Clears All Active Dialogs") +@MainActor +func testPopToRootClearsAllDialogs() { + let navigator = Navigator() + navigator.append("Path1") + navigator.append("Path2") + navigator.alert( + title: "Alert", + message: { Text("Message") }, + actions: { EmptyView() } + ) + navigator.sheet { Text("Sheet") } + navigator.confirmDialog( + title: "Dialog", + message: { Text("Message") }, + actions: { EmptyView() } + ) + navigator.popToRoot() + #expect(navigator.path.isEmpty, "Path should be empty after popToRoot.") + #expect(navigator.alert == nil, "Alert should be nil after popToRoot.") + #expect(navigator.sheet == nil, "Sheet should be nil after popToRoot.") + #expect(navigator.confirmDialog == nil, "Confirm dialog should be nil after popToRoot.") +} + +// MARK: - Model Identity Tests + +@Test("NavigatorAlert Has Unique ID") +@MainActor +func testNavigatorAlertUniqueID() { + let alert1 = Navigator.NavigatorAlert( + title: "Alert 1", + message: { Text("Message") }, + actions: { EmptyView() } + ) + let alert2 = Navigator.NavigatorAlert( + title: "Alert 2", + message: { Text("Message") }, + actions: { EmptyView() } + ) + #expect(alert1.id != alert2.id, "Each NavigatorAlert should have a unique ID.") +} + +@Test("NavigatorSheet Has Unique ID") +@MainActor +func testNavigatorSheetUniqueID() { + let sheet1 = Navigator.NavigatorSheet(content: { Text("Sheet 1") }) + let sheet2 = Navigator.NavigatorSheet(content: { Text("Sheet 2") }) + #expect(sheet1.id != sheet2.id, "Each NavigatorSheet should have a unique ID.") +} + +@Test("NavigatorConfirmDialog Has Unique ID") +@MainActor +func testNavigatorConfirmDialogUniqueID() { + let dialog1 = Navigator.NavigatorConfirmDialog( + title: "Dialog 1", + message: { Text("Message") }, + actions: { EmptyView() } + ) + let dialog2 = Navigator.NavigatorConfirmDialog( + title: "Dialog 2", + message: { Text("Message") }, + actions: { EmptyView() } + ) + #expect(dialog1.id != dialog2.id, "Each NavigatorConfirmDialog should have a unique ID.") +} + +// MARK: - Sheet onDismiss Callback + +@Test("NavigatorSheet Stores onDismiss Callback") +@MainActor +func testNavigatorSheetOnDismissCallback() { + var dismissed = false + let sheet = Navigator.NavigatorSheet( + content: { Text("Sheet") }, + onDismiss: { dismissed = true } + ) + #expect(sheet.onDismiss != nil, "Sheet should store an onDismiss callback.") + sheet.onDismiss?() + #expect(dismissed, "onDismiss callback should execute when invoked.") +} + +@Test("NavigatorSheet Without onDismiss") +@MainActor +func testNavigatorSheetWithoutOnDismiss() { + let sheet = Navigator.NavigatorSheet(content: { Text("Sheet") }) + #expect(sheet.onDismiss == nil, "Sheet should have nil onDismiss when none is provided.") +} + +@Test("Sheet with onDismiss via Navigator") +@MainActor +func testSheetWithOnDismissViaNavigator() { + var callbackInvoked = false + let navigator = Navigator() + navigator.sheet(content: { Text("Sheet") }, onDismiss: { callbackInvoked = true }) + #expect(navigator.sheet != nil, "Navigator should present a sheet.") + navigator.sheet?.onDismiss?() + #expect(callbackInvoked, "onDismiss callback should be invocable from the stored sheet.") +} + +// MARK: - Alert Replacement + +@Test("Presenting New Alert Replaces Existing") +@MainActor +func testAlertReplacement() { + let navigator = Navigator() + navigator.alert(title: "First", message: { EmptyView() }, actions: { EmptyView() }) + let firstID = navigator.alert?.id + navigator.alert(title: "Second", message: { EmptyView() }, actions: { EmptyView() }) + #expect(navigator.alert?.title == "Second", "New alert should replace the existing one.") + #expect(navigator.alert?.id != firstID, "New alert should have a different ID.") +} From 07d802996cc6ecb85871445f95dd8dc730d7fb41 Mon Sep 17 00:00:00 2001 From: corvid-agent <0xOpenBytes@gmail.com> Date: Fri, 27 Feb 2026 11:12:54 -0700 Subject: [PATCH 2/2] address review feedback: remove pre-condition assertions, use identical params in identity tests Co-Authored-By: Claude Opus 4.6 --- Tests/NavigationTests/NavigationTests.swift | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/Tests/NavigationTests/NavigationTests.swift b/Tests/NavigationTests/NavigationTests.swift index c12eb47..92158f7 100644 --- a/Tests/NavigationTests/NavigationTests.swift +++ b/Tests/NavigationTests/NavigationTests.swift @@ -146,7 +146,6 @@ func testRemoveLastWithCount() { navigator.append("Path1") navigator.append("Path2") navigator.append("Path3") - #expect(navigator.path.count == 3) navigator.removeLast(2) #expect(navigator.path.count == 1, "removeLast(2) should remove exactly two elements.") } @@ -230,7 +229,6 @@ func testDismissFallsThroughToPath() { let navigator = Navigator() navigator.append("Path1") navigator.append("Path2") - #expect(navigator.path.count == 2) navigator.dismiss() #expect(navigator.path.count == 1, "Dismiss should remove last path element when no dialogs are present.") } @@ -267,12 +265,12 @@ func testPopToRootClearsAllDialogs() { @MainActor func testNavigatorAlertUniqueID() { let alert1 = Navigator.NavigatorAlert( - title: "Alert 1", + title: "Alert", message: { Text("Message") }, actions: { EmptyView() } ) let alert2 = Navigator.NavigatorAlert( - title: "Alert 2", + title: "Alert", message: { Text("Message") }, actions: { EmptyView() } ) @@ -282,8 +280,8 @@ func testNavigatorAlertUniqueID() { @Test("NavigatorSheet Has Unique ID") @MainActor func testNavigatorSheetUniqueID() { - let sheet1 = Navigator.NavigatorSheet(content: { Text("Sheet 1") }) - let sheet2 = Navigator.NavigatorSheet(content: { Text("Sheet 2") }) + let sheet1 = Navigator.NavigatorSheet(content: { Text("Sheet") }) + let sheet2 = Navigator.NavigatorSheet(content: { Text("Sheet") }) #expect(sheet1.id != sheet2.id, "Each NavigatorSheet should have a unique ID.") } @@ -291,12 +289,12 @@ func testNavigatorSheetUniqueID() { @MainActor func testNavigatorConfirmDialogUniqueID() { let dialog1 = Navigator.NavigatorConfirmDialog( - title: "Dialog 1", + title: "Dialog", message: { Text("Message") }, actions: { EmptyView() } ) let dialog2 = Navigator.NavigatorConfirmDialog( - title: "Dialog 2", + title: "Dialog", message: { Text("Message") }, actions: { EmptyView() } )