mirror of
https://github.com/permissionlesstech/bitchat.git
synced 2026-08-22 07:16:03 +00:00
* Add a persistent connectivity banner and stop the radar lying Two "the app looks fine while it isn't" problems from the UX audit: - Bluetooth-off had exactly one signal: a dismissible alert. Once dismissed, the app looked completely normal — empty timeline, radar sweeping "searching for people" — while the radio was off. The radar animation now requires a radio that can actually scan, and a persistent red banner under the header carries the state instead: off (tap → settings), denied (tap → settings), or unsupported (mesh unavailable; location channels still work). - A Tor bootstrap stall was only ever announced inside geohash timelines (addGeohashOnlySystemMessage), so someone sitting in #mesh or a DM whose messages route over Nostr got silence and hanging "sent" states. The stall now also drives a chrome-level torBlocked flag and the same banner: "tor can't connect — internet features are paused. mesh still works." The banner renders nothing when everything is healthy, and .unknown/ .resetting radio states deliberately stay quiet — a false "bluetooth is off" flash at launch would be the same kind of lie. Priority and quiet-start behavior are pinned by ConnectivityIssueTests. 4 new strings, all 30 locales. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Review fixes: banner in the sheet, real fix targets, tor-clear tests - The people/DM sheet covers the root header, so the connectivity banner vanished exactly where people sit longest. It now mirrors into the sheet via a top safe-area inset over both the people list and the DM view. - "tap to fix" for powered-off Bluetooth opened the macOS privacy permission pane, where an already-authorized person can't fix anything. New SystemSettings.bluetoothPower routes to the Bluetooth pane; denied still goes to the privacy anchor. The one-shot alert's settings button routes by state the same way. - Tests pin that torBlocked clears on tor-ready and on preference change (no stale "tor can't connect" after toggling tor off). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: jack <jackjackbits@users.noreply.github.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
198 lines
6.1 KiB
Swift
198 lines
6.1 KiB
Swift
//
|
|
// ChatViewModelTorTests.swift
|
|
// bitchatTests
|
|
//
|
|
// Tests for ChatViewModel+Tor.swift Tor lifecycle notification handlers.
|
|
//
|
|
|
|
import Testing
|
|
import Foundation
|
|
@testable import bitchat
|
|
|
|
// MARK: - Test Helpers
|
|
|
|
@MainActor
|
|
private func makeTestableViewModel() -> (viewModel: ChatViewModel, transport: MockTransport) {
|
|
let keychain = MockKeychain()
|
|
let keychainHelper = MockKeychainHelper()
|
|
let idBridge = NostrIdentityBridge(keychain: keychainHelper)
|
|
let identityManager = MockIdentityManager(keychain)
|
|
let transport = MockTransport()
|
|
|
|
let viewModel = ChatViewModel(
|
|
keychain: keychain,
|
|
idBridge: idBridge,
|
|
identityManager: identityManager,
|
|
transport: transport
|
|
)
|
|
|
|
return (viewModel, transport)
|
|
}
|
|
|
|
// MARK: - Tor Notification Handler Tests
|
|
|
|
struct ChatViewModelTorTests {
|
|
|
|
// MARK: - handleTorWillStart Tests
|
|
|
|
@Test @MainActor
|
|
func handleTorWillStart_whenEnforced_setsAnnouncedFlag() async {
|
|
let (viewModel, _) = makeTestableViewModel()
|
|
|
|
// Precondition: flag should start false
|
|
#expect(!viewModel.torStatusAnnounced)
|
|
|
|
// Action: simulate Tor starting notification
|
|
viewModel.handleTorWillStart()
|
|
|
|
// Wait for Task to complete
|
|
try? await Task.sleep(nanoseconds: 100_000_000)
|
|
|
|
// Assert: flag should be set (torEnforced is true in tests)
|
|
#expect(viewModel.torStatusAnnounced)
|
|
}
|
|
|
|
@Test @MainActor
|
|
func handleTorWillStart_whenAlreadyAnnounced_doesNotDuplicate() async {
|
|
let (viewModel, _) = makeTestableViewModel()
|
|
|
|
// Setup: pre-set the flag
|
|
viewModel.torStatusAnnounced = true
|
|
|
|
// Switch to a geohash channel so messages would be visible
|
|
viewModel.switchLocationChannel(to: .location(GeohashChannel(level: .city, geohash: "u4pruydq")))
|
|
try? await Task.sleep(nanoseconds: 100_000_000)
|
|
|
|
let initialMessageCount = viewModel.messages.count
|
|
|
|
// Action: call handler again
|
|
viewModel.handleTorWillStart()
|
|
try? await Task.sleep(nanoseconds: 100_000_000)
|
|
|
|
// Assert: no new message added (flag was already true)
|
|
#expect(viewModel.messages.count == initialMessageCount)
|
|
}
|
|
|
|
// MARK: - handleTorWillRestart Tests
|
|
|
|
@Test @MainActor
|
|
func handleTorWillRestart_setsPendingFlag() async {
|
|
let (viewModel, _) = makeTestableViewModel()
|
|
|
|
// Precondition
|
|
#expect(!viewModel.torRestartPending)
|
|
|
|
// Action
|
|
viewModel.handleTorWillRestart()
|
|
try? await Task.sleep(nanoseconds: 100_000_000)
|
|
|
|
// Assert
|
|
#expect(viewModel.torRestartPending)
|
|
}
|
|
|
|
@Test @MainActor
|
|
func handleTorWillRestart_setsFlag_regardlessOfChannel() async {
|
|
let (viewModel, _) = makeTestableViewModel()
|
|
|
|
// Action: call handler (works regardless of channel)
|
|
viewModel.handleTorWillRestart()
|
|
try? await Task.sleep(nanoseconds: 100_000_000)
|
|
|
|
// Assert: flag should be set
|
|
#expect(viewModel.torRestartPending)
|
|
}
|
|
|
|
// MARK: - handleTorDidBecomeReady Tests
|
|
|
|
@Test @MainActor
|
|
func handleTorDidBecomeReady_afterRestart_clearsPendingFlag() async {
|
|
let (viewModel, _) = makeTestableViewModel()
|
|
|
|
// Setup: simulate restart pending state
|
|
viewModel.torRestartPending = true
|
|
|
|
// Action
|
|
viewModel.handleTorDidBecomeReady()
|
|
try? await Task.sleep(nanoseconds: 100_000_000)
|
|
|
|
// Assert: should clear pending flag
|
|
#expect(!viewModel.torRestartPending)
|
|
}
|
|
|
|
@Test @MainActor
|
|
func handleTorDidBecomeReady_initialStart_setsAnnouncedFlag() async {
|
|
let (viewModel, _) = makeTestableViewModel()
|
|
|
|
// Setup: not restarting, but initial ready not announced yet
|
|
viewModel.torRestartPending = false
|
|
viewModel.torInitialReadyAnnounced = false
|
|
|
|
// Action
|
|
viewModel.handleTorDidBecomeReady()
|
|
try? await Task.sleep(nanoseconds: 100_000_000)
|
|
|
|
// Assert: should set flag (torEnforced is true in tests)
|
|
#expect(viewModel.torInitialReadyAnnounced)
|
|
}
|
|
|
|
@Test @MainActor
|
|
func handleTorDidBecomeReady_alreadyAnnounced_noDuplicate() async {
|
|
let (viewModel, _) = makeTestableViewModel()
|
|
|
|
// Setup: already announced initial ready
|
|
viewModel.torRestartPending = false
|
|
viewModel.torInitialReadyAnnounced = true
|
|
viewModel.switchLocationChannel(to: .location(GeohashChannel(level: .city, geohash: "u4pruydq")))
|
|
try? await Task.sleep(nanoseconds: 100_000_000)
|
|
|
|
let initialMessageCount = viewModel.messages.count
|
|
|
|
// Action
|
|
viewModel.handleTorDidBecomeReady()
|
|
try? await Task.sleep(nanoseconds: 100_000_000)
|
|
|
|
// Assert: no new message
|
|
#expect(viewModel.messages.count == initialMessageCount)
|
|
}
|
|
|
|
// MARK: - handleTorPreferenceChanged Tests
|
|
|
|
@Test @MainActor
|
|
func handleTorPreferenceChanged_resetsAllFlags() async {
|
|
let (viewModel, _) = makeTestableViewModel()
|
|
|
|
// Setup: set all flags
|
|
viewModel.torStatusAnnounced = true
|
|
viewModel.torInitialReadyAnnounced = true
|
|
viewModel.torRestartPending = true
|
|
viewModel.torBlocked = true
|
|
|
|
// Action
|
|
viewModel.handleTorPreferenceChanged(Notification(name: .init("test")))
|
|
try? await Task.sleep(nanoseconds: 100_000_000)
|
|
|
|
// Assert: all flags reset — torBlocked drives the connectivity
|
|
// banner; toggling tor off must not leave a stale "tor can't
|
|
// connect" line for a network nobody is waiting on.
|
|
#expect(!viewModel.torStatusAnnounced)
|
|
#expect(!viewModel.torInitialReadyAnnounced)
|
|
#expect(!viewModel.torRestartPending)
|
|
#expect(!viewModel.torBlocked)
|
|
}
|
|
|
|
@Test @MainActor
|
|
func handleTorDidBecomeReady_clearsBlockedBanner() async {
|
|
let (viewModel, _) = makeTestableViewModel()
|
|
|
|
// Setup: a stalled bootstrap raised the banner, then tor got through.
|
|
viewModel.torBlocked = true
|
|
|
|
// Action
|
|
viewModel.handleTorDidBecomeReady()
|
|
try? await Task.sleep(nanoseconds: 100_000_000)
|
|
|
|
// Assert
|
|
#expect(!viewModel.torBlocked)
|
|
}
|
|
}
|