mirror of
https://github.com/bitwarden/ios.git
synced 2025-12-10 17:46:07 -06:00
[PM-26063] Add reusable component for the Flight Recorder settings view (#2141)
This commit is contained in:
parent
0e2991802d
commit
b8161ee72c
@ -0,0 +1,11 @@
|
||||
// MARK: - FlightRecorderSettingsSectionAction
|
||||
|
||||
/// Actions handled by the Flight Recorder settings section component.
|
||||
///
|
||||
/// This is a reusable component that can be integrated into any processor that displays Flight
|
||||
/// Recorder settings.
|
||||
///
|
||||
public enum FlightRecorderSettingsSectionAction: Equatable {
|
||||
/// The view Flight Recorder logs button was tapped.
|
||||
case viewLogsTapped
|
||||
}
|
||||
@ -0,0 +1,11 @@
|
||||
// MARK: - FlightRecorderSettingsSectionEffect
|
||||
|
||||
/// Effects handled by the Flight Recorder settings section component.
|
||||
///
|
||||
/// This is a reusable component that can be integrated into any processor that displays Flight
|
||||
/// Recorder settings.
|
||||
///
|
||||
public enum FlightRecorderSettingsSectionEffect: Equatable {
|
||||
/// The Flight Recorder toggle value changed.
|
||||
case toggleFlightRecorder(Bool)
|
||||
}
|
||||
@ -0,0 +1,44 @@
|
||||
import BitwardenResources
|
||||
import Foundation
|
||||
|
||||
// MARK: - FlightRecorderSettingsSectionState
|
||||
|
||||
/// The state for the Flight Recorder settings section component.
|
||||
///
|
||||
/// This is a reusable component that can be integrated into any processor that displays Flight
|
||||
/// Recorder settings.
|
||||
///
|
||||
public struct FlightRecorderSettingsSectionState: Equatable {
|
||||
// MARK: Properties
|
||||
|
||||
/// The Flight Recorder's active log metadata, if logging is enabled.
|
||||
public var activeLog: FlightRecorderData.LogMetadata?
|
||||
|
||||
// MARK: Computed Properties
|
||||
|
||||
/// The accessibility label for the Flight Recorder toggle.
|
||||
var flightRecorderToggleAccessibilityLabel: String {
|
||||
var accessibilityLabelComponents = [Localizations.flightRecorder]
|
||||
if let log = activeLog {
|
||||
// VoiceOver doesn't read the short date style correctly so use the medium style instead.
|
||||
let dateFormatter = DateFormatter()
|
||||
dateFormatter.dateStyle = .medium
|
||||
|
||||
accessibilityLabelComponents.append(Localizations.loggingEndsOnDateAtTime(
|
||||
dateFormatter.string(from: log.endDate),
|
||||
log.formattedEndTime,
|
||||
))
|
||||
}
|
||||
return accessibilityLabelComponents.joined(separator: ", ")
|
||||
}
|
||||
|
||||
// MARK: Initialization
|
||||
|
||||
/// Creates a new `FlightRecorderState`.
|
||||
///
|
||||
/// - Parameter activeLog: The Flight Recorder's active log metadata, if logging is enabled.
|
||||
///
|
||||
public init(activeLog: FlightRecorderData.LogMetadata? = nil) {
|
||||
self.activeLog = activeLog
|
||||
}
|
||||
}
|
||||
@ -1,22 +1,21 @@
|
||||
import BitwardenKit
|
||||
import BitwardenResources
|
||||
import XCTest
|
||||
|
||||
@testable import BitwardenShared
|
||||
@testable import BitwardenKit
|
||||
|
||||
class AboutStateTests: BitwardenTestCase {
|
||||
class FlightRecorderSettingsSectionStateTests: BitwardenTestCase {
|
||||
/// `flightRecorderToggleAccessibilityLabel` returns the flight recorder toggle's accessibility
|
||||
/// label when the flight recorder is off.
|
||||
func test_flightRecorderToggleAccessibilityLabel_flightRecorderOff() {
|
||||
let subject = AboutState(flightRecorderActiveLog: nil)
|
||||
let subject = FlightRecorderSettingsSectionState()
|
||||
XCTAssertEqual(subject.flightRecorderToggleAccessibilityLabel, Localizations.flightRecorder)
|
||||
}
|
||||
|
||||
/// `flightRecorderToggleAccessibilityLabel` returns the flight recorder toggle's accessibility
|
||||
/// label when the flight recorder is on.
|
||||
func test_flightRecorderToggleAccessibilityLabel_flightRecorderOn() {
|
||||
let subject = AboutState(
|
||||
flightRecorderActiveLog: FlightRecorderData.LogMetadata(
|
||||
let subject = FlightRecorderSettingsSectionState(
|
||||
activeLog: FlightRecorderData.LogMetadata(
|
||||
duration: .eightHours,
|
||||
startDate: Date(year: 2025, month: 5, day: 1),
|
||||
),
|
||||
@ -0,0 +1,73 @@
|
||||
// swiftlint:disable:this file_name
|
||||
import BitwardenKitMocks
|
||||
import BitwardenResources
|
||||
import SnapshotTesting
|
||||
import SwiftUI
|
||||
import XCTest
|
||||
|
||||
@testable import BitwardenKit
|
||||
|
||||
class FlightRecorderSettingsSectionViewSnapshotTests: BitwardenTestCase {
|
||||
// MARK: Properties
|
||||
|
||||
var processor: MockProcessor<
|
||||
FlightRecorderSettingsSectionState,
|
||||
FlightRecorderSettingsSectionAction,
|
||||
FlightRecorderSettingsSectionEffect,
|
||||
>!
|
||||
var subject: FlightRecorderSettingsSectionView!
|
||||
|
||||
// MARK: Computed Properties
|
||||
|
||||
/// Returns the subject view wrapped with padding and background for snapshot testing.
|
||||
var snapshotView: some View {
|
||||
ZStack(alignment: .top) {
|
||||
SharedAsset.Colors.backgroundPrimary.swiftUIColor.ignoresSafeArea()
|
||||
|
||||
subject
|
||||
.padding()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Setup & Teardown
|
||||
|
||||
override func setUp() {
|
||||
super.setUp()
|
||||
|
||||
processor = MockProcessor(state: FlightRecorderSettingsSectionState())
|
||||
let store = Store(processor: processor)
|
||||
|
||||
subject = FlightRecorderSettingsSectionView(store: store)
|
||||
}
|
||||
|
||||
override func tearDown() {
|
||||
super.tearDown()
|
||||
|
||||
processor = nil
|
||||
subject = nil
|
||||
}
|
||||
|
||||
// MARK: Snapshots
|
||||
|
||||
/// The flight recorder settings section view renders correctly when disabled.
|
||||
@MainActor
|
||||
func disabletest_snapshot_flightRecorderSettingsSection_disabled() {
|
||||
assertSnapshots(
|
||||
of: snapshotView,
|
||||
as: [.defaultPortrait, .defaultPortraitDark, .defaultPortraitAX5],
|
||||
)
|
||||
}
|
||||
|
||||
/// The flight recorder settings section view renders correctly when enabled.
|
||||
@MainActor
|
||||
func disabletest_snapshot_flightRecorderSettingsSection_enabled() {
|
||||
processor.state.activeLog = FlightRecorderData.LogMetadata(
|
||||
duration: .eightHours,
|
||||
startDate: Date(year: 2025, month: 5, day: 1, hour: 8),
|
||||
)
|
||||
assertSnapshots(
|
||||
of: snapshotView,
|
||||
as: [.defaultPortrait, .defaultPortraitDark, .defaultPortraitAX5],
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,69 @@
|
||||
// swiftlint:disable:this file_name
|
||||
import BitwardenKitMocks
|
||||
import BitwardenResources
|
||||
import TestHelpers
|
||||
import ViewInspector
|
||||
import XCTest
|
||||
|
||||
@testable import BitwardenKit
|
||||
|
||||
class FlightRecorderSettingsSectionViewTests: BitwardenTestCase {
|
||||
// MARK: Properties
|
||||
|
||||
var processor: MockProcessor<
|
||||
FlightRecorderSettingsSectionState,
|
||||
FlightRecorderSettingsSectionAction,
|
||||
FlightRecorderSettingsSectionEffect,
|
||||
>!
|
||||
var subject: FlightRecorderSettingsSectionView!
|
||||
|
||||
// MARK: Setup & Teardown
|
||||
|
||||
override func setUp() {
|
||||
super.setUp()
|
||||
|
||||
processor = MockProcessor(state: FlightRecorderSettingsSectionState())
|
||||
let store = Store(processor: processor)
|
||||
|
||||
subject = FlightRecorderSettingsSectionView(store: store)
|
||||
}
|
||||
|
||||
override func tearDown() {
|
||||
super.tearDown()
|
||||
|
||||
processor = nil
|
||||
subject = nil
|
||||
}
|
||||
|
||||
// MARK: Tests
|
||||
|
||||
/// Toggling the Flight Recorder toggle on dispatches the `.toggleFlightRecorder(true)` effect.
|
||||
@MainActor
|
||||
func test_toggle_on() async throws {
|
||||
let toggle = try subject.inspect().find(toggleWithAccessibilityLabel: Localizations.flightRecorder)
|
||||
try toggle.tap()
|
||||
try await waitForAsync { !self.processor.effects.isEmpty }
|
||||
XCTAssertEqual(processor.effects.last, .toggleFlightRecorder(true))
|
||||
}
|
||||
|
||||
/// Toggling the Flight Recorder toggle off dispatches the `.toggleFlightRecorder(false)` effect.
|
||||
@MainActor
|
||||
func test_toggle_off() async throws {
|
||||
let toggle = try subject.inspect().find(toggleWithAccessibilityLabel: Localizations.flightRecorder)
|
||||
processor.state.activeLog = FlightRecorderData.LogMetadata(
|
||||
duration: .eightHours,
|
||||
startDate: Date(year: 2025, month: 5, day: 1),
|
||||
)
|
||||
try toggle.tap()
|
||||
try await waitForAsync { !self.processor.effects.isEmpty }
|
||||
XCTAssertEqual(processor.effects.last, .toggleFlightRecorder(false))
|
||||
}
|
||||
|
||||
/// Tapping the view recorded logs button dispatches the `.viewLogsTapped` action.
|
||||
@MainActor
|
||||
func test_viewLogsButton_tap() throws {
|
||||
let button = try subject.inspect().find(button: Localizations.viewRecordedLogs)
|
||||
try button.tap()
|
||||
XCTAssertEqual(processor.dispatchedActions.last, .viewLogsTapped)
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,107 @@
|
||||
import BitwardenResources
|
||||
import SwiftUI
|
||||
|
||||
// MARK: - FlightRecorderSettingsSectionView
|
||||
|
||||
/// A reusable view component that displays Flight Recorder settings.
|
||||
///
|
||||
/// This view provides a toggle to enable/disable flight recording and displays the active log's
|
||||
/// end date and time when logging is enabled. It also includes a button to view recorded logs
|
||||
/// and a help link for more information about the Flight Recorder feature.
|
||||
///
|
||||
/// This component can be integrated into any settings view that needs to display Flight Recorder
|
||||
/// settings.
|
||||
///
|
||||
public struct FlightRecorderSettingsSectionView: View {
|
||||
// MARK: Types
|
||||
|
||||
public typealias FlightRecorderSettingsSectionStore = Store<
|
||||
FlightRecorderSettingsSectionState,
|
||||
FlightRecorderSettingsSectionAction,
|
||||
FlightRecorderSettingsSectionEffect,
|
||||
>
|
||||
|
||||
// MARK: Properties
|
||||
|
||||
/// An object used to open urls from this view.
|
||||
@Environment(\.openURL) private var openURL
|
||||
|
||||
/// The `Store` for this view.
|
||||
@ObservedObject var store: FlightRecorderSettingsSectionStore
|
||||
|
||||
// MARK: View
|
||||
|
||||
public var body: some View {
|
||||
ContentBlock(dividerLeadingPadding: 16) {
|
||||
BitwardenToggle(
|
||||
isOn: store.bindingAsync(
|
||||
get: { $0.activeLog != nil },
|
||||
perform: FlightRecorderSettingsSectionEffect.toggleFlightRecorder,
|
||||
),
|
||||
accessibilityIdentifier: "FlightRecorderSwitch",
|
||||
accessibilityLabel: store.state.flightRecorderToggleAccessibilityLabel,
|
||||
) {
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
HStack(spacing: 8) {
|
||||
Text(Localizations.flightRecorder)
|
||||
|
||||
Button {
|
||||
openURL(ExternalLinksConstants.flightRecorderHelp)
|
||||
} label: {
|
||||
SharedAsset.Icons.questionCircle16.swiftUIImage
|
||||
.scaledFrame(width: 16, height: 16)
|
||||
.accessibilityLabel(Localizations.learnMore)
|
||||
}
|
||||
.buttonStyle(.fieldLabelIcon)
|
||||
}
|
||||
|
||||
if let log = store.state.activeLog {
|
||||
Text(Localizations.loggingEndsOnDateAtTime(log.formattedEndDate, log.formattedEndTime))
|
||||
.foregroundStyle(SharedAsset.Colors.textSecondary.swiftUIColor)
|
||||
.styleGuide(.subheadline)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SettingsListItem(Localizations.viewRecordedLogs) {
|
||||
store.send(.viewLogsTapped)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Initialization
|
||||
|
||||
/// Creates a new `FlightRecorderSettingsSectionView`.
|
||||
///
|
||||
/// - Parameter store: The `Store` for managing the Flight Recorder settings section state,
|
||||
/// actions, and effects.
|
||||
///
|
||||
public init(store: FlightRecorderSettingsSectionStore) {
|
||||
self.store = store
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Previews
|
||||
|
||||
#if DEBUG
|
||||
#Preview("Disabled") {
|
||||
FlightRecorderSettingsSectionView(
|
||||
store: Store(processor: StateProcessor(state: FlightRecorderSettingsSectionState())),
|
||||
)
|
||||
.padding()
|
||||
.background(SharedAsset.Colors.backgroundPrimary.swiftUIColor)
|
||||
}
|
||||
|
||||
#Preview("Enabled") {
|
||||
FlightRecorderSettingsSectionView(
|
||||
store: Store(processor: StateProcessor(state: FlightRecorderSettingsSectionState(
|
||||
activeLog: FlightRecorderData.LogMetadata(
|
||||
duration: .eightHours,
|
||||
startDate: Date(timeIntervalSinceNow: 60 * 60 * -4),
|
||||
),
|
||||
))),
|
||||
)
|
||||
.padding()
|
||||
.background(SharedAsset.Colors.backgroundPrimary.swiftUIColor)
|
||||
}
|
||||
#endif
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 83 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 84 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 112 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 92 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 92 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 141 KiB |
@ -11,6 +11,9 @@ enum AboutAction: Equatable {
|
||||
/// The url has been opened so clear the value in the state.
|
||||
case clearURL
|
||||
|
||||
/// An action for the Flight Recorder feature.
|
||||
case flightRecorder(FlightRecorderSettingsSectionAction)
|
||||
|
||||
/// The help center button was tapped.
|
||||
case helpCenterTapped
|
||||
|
||||
@ -32,9 +35,6 @@ enum AboutAction: Equatable {
|
||||
/// The version was tapped.
|
||||
case versionTapped
|
||||
|
||||
/// The view flight recorder logs button was tapped.
|
||||
case viewFlightRecorderLogsTapped
|
||||
|
||||
/// The web vault button was tapped.
|
||||
case webVaultTapped
|
||||
}
|
||||
|
||||
@ -1,11 +1,13 @@
|
||||
import BitwardenKit
|
||||
|
||||
// MARK: - AboutEffect
|
||||
|
||||
/// Effects that can be processed by the `AboutProcessor`.
|
||||
///
|
||||
enum AboutEffect: Equatable {
|
||||
/// An effect for the Flight Recorder feature.
|
||||
case flightRecorder(FlightRecorderSettingsSectionEffect)
|
||||
|
||||
/// Stream the active flight recorder log.
|
||||
case streamFlightRecorderLog
|
||||
|
||||
/// The flight recorder toggle value changed.
|
||||
case toggleFlightRecorder(Bool)
|
||||
}
|
||||
|
||||
@ -53,14 +53,17 @@ final class AboutProcessor: StateProcessor<AboutState, AboutAction, AboutEffect>
|
||||
|
||||
override func perform(_ effect: AboutEffect) async {
|
||||
switch effect {
|
||||
case let .flightRecorder(flightRecorderEffect):
|
||||
switch flightRecorderEffect {
|
||||
case let .toggleFlightRecorder(isOn):
|
||||
if isOn {
|
||||
coordinator.navigate(to: .flightRecorder(.enableFlightRecorder))
|
||||
} else {
|
||||
await services.flightRecorder.disableFlightRecorder()
|
||||
}
|
||||
}
|
||||
case .streamFlightRecorderLog:
|
||||
await streamFlightRecorderLog()
|
||||
case let .toggleFlightRecorder(isOn):
|
||||
if isOn {
|
||||
coordinator.navigate(to: .flightRecorder(.enableFlightRecorder))
|
||||
} else {
|
||||
await services.flightRecorder.disableFlightRecorder()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -70,6 +73,11 @@ final class AboutProcessor: StateProcessor<AboutState, AboutAction, AboutEffect>
|
||||
state.appReviewUrl = nil
|
||||
case .clearURL:
|
||||
state.url = nil
|
||||
case let .flightRecorder(flightRecorderAction):
|
||||
switch flightRecorderAction {
|
||||
case .viewLogsTapped:
|
||||
coordinator.navigate(to: .flightRecorder(.flightRecorderLogs))
|
||||
}
|
||||
case .helpCenterTapped:
|
||||
state.url = ExternalLinksConstants.helpAndFeedback
|
||||
case .learnAboutOrganizationsTapped:
|
||||
@ -91,8 +99,6 @@ final class AboutProcessor: StateProcessor<AboutState, AboutAction, AboutEffect>
|
||||
services.errorReporter.isEnabled = isOn
|
||||
case .versionTapped:
|
||||
handleVersionTapped()
|
||||
case .viewFlightRecorderLogsTapped:
|
||||
coordinator.navigate(to: .flightRecorder(.flightRecorderLogs))
|
||||
case .webVaultTapped:
|
||||
coordinator.showAlert(.webVaultAlert {
|
||||
self.state.url = self.services.environmentService.webVaultURL
|
||||
@ -111,7 +117,7 @@ final class AboutProcessor: StateProcessor<AboutState, AboutAction, AboutEffect>
|
||||
/// Streams the flight recorder's active log metadata.
|
||||
private func streamFlightRecorderLog() async {
|
||||
for await activeLog in await services.flightRecorder.activeLogPublisher().values {
|
||||
state.flightRecorderActiveLog = activeLog
|
||||
state.flightRecorderState.activeLog = activeLog
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -81,10 +81,35 @@ class AboutProcessorTests: BitwardenTestCase {
|
||||
XCTAssertEqual(subject.state.version, "1.0 (1)")
|
||||
}
|
||||
|
||||
/// `perform(_:)` with `.flightRecorder(.toggleFlightRecorder(true))` navigates to the enable
|
||||
/// flight recorder screen when toggled on.
|
||||
@MainActor
|
||||
func test_perform_flightRecorder_toggleFlightRecorder_on() async {
|
||||
XCTAssertNil(subject.state.flightRecorderState.activeLog)
|
||||
|
||||
await subject.perform(.flightRecorder(.toggleFlightRecorder(true)))
|
||||
|
||||
XCTAssertEqual(coordinator.routes, [.flightRecorder(.enableFlightRecorder)])
|
||||
}
|
||||
|
||||
/// `perform(_:)` with `.flightRecorder(.toggleFlightRecorder(false))` disables the flight
|
||||
/// recorder when toggled off.
|
||||
@MainActor
|
||||
func test_perform_flightRecorder_toggleFlightRecorder_off() async throws {
|
||||
subject.state.flightRecorderState.activeLog = FlightRecorderData.LogMetadata(
|
||||
duration: .eightHours,
|
||||
startDate: .now,
|
||||
)
|
||||
|
||||
await subject.perform(.flightRecorder(.toggleFlightRecorder(false)))
|
||||
|
||||
XCTAssertTrue(flightRecorder.disableFlightRecorderCalled)
|
||||
}
|
||||
|
||||
/// `perform(_:)` with `.streamFlightRecorderLog` subscribes to the active flight recorder log.
|
||||
@MainActor
|
||||
func test_perform_streamFlightRecorderLog() async throws {
|
||||
XCTAssertNil(subject.state.flightRecorderActiveLog)
|
||||
XCTAssertNil(subject.state.flightRecorderState.activeLog)
|
||||
|
||||
let task = Task {
|
||||
await subject.perform(.streamFlightRecorderLog)
|
||||
@ -93,36 +118,12 @@ class AboutProcessorTests: BitwardenTestCase {
|
||||
|
||||
let log = FlightRecorderData.LogMetadata(duration: .eightHours, startDate: .now)
|
||||
flightRecorder.activeLogSubject.send(log)
|
||||
try await waitForAsync { self.subject.state.flightRecorderActiveLog != nil }
|
||||
XCTAssertEqual(subject.state.flightRecorderActiveLog, log)
|
||||
try await waitForAsync { self.subject.state.flightRecorderState.activeLog != nil }
|
||||
XCTAssertEqual(subject.state.flightRecorderState.activeLog, log)
|
||||
|
||||
flightRecorder.activeLogSubject.send(nil)
|
||||
try await waitForAsync { self.subject.state.flightRecorderActiveLog == nil }
|
||||
XCTAssertNil(subject.state.flightRecorderActiveLog)
|
||||
}
|
||||
|
||||
/// `perform(_:)` with `.toggleFlightRecorder(false)` disables the flight recorder when toggled off.
|
||||
@MainActor
|
||||
func test_perform_toggleFlightRecorder_off() async throws {
|
||||
subject.state.flightRecorderActiveLog = FlightRecorderData.LogMetadata(
|
||||
duration: .eightHours,
|
||||
startDate: .now,
|
||||
)
|
||||
|
||||
await subject.perform(.toggleFlightRecorder(false))
|
||||
|
||||
XCTAssertTrue(flightRecorder.disableFlightRecorderCalled)
|
||||
}
|
||||
|
||||
/// `perform(_:)` with `.toggleFlightRecorder(true)` navigates to the enable flight
|
||||
/// recorder screen when toggled on.
|
||||
@MainActor
|
||||
func test_perform_toggleFlightRecorder_on() async {
|
||||
XCTAssertNil(subject.state.flightRecorderActiveLog)
|
||||
|
||||
await subject.perform(.toggleFlightRecorder(true))
|
||||
|
||||
XCTAssertEqual(coordinator.routes, [.flightRecorder(.enableFlightRecorder)])
|
||||
try await waitForAsync { self.subject.state.flightRecorderState.activeLog == nil }
|
||||
XCTAssertNil(subject.state.flightRecorderState.activeLog)
|
||||
}
|
||||
|
||||
/// `receive(_:)` with `.clearAppReviewURL` clears the app review URL in the state.
|
||||
@ -141,6 +142,15 @@ class AboutProcessorTests: BitwardenTestCase {
|
||||
XCTAssertNil(subject.state.url)
|
||||
}
|
||||
|
||||
/// `receive(_:)` with action `.flightRecorder(.viewLogsTapped)` navigates to the view flight
|
||||
/// recorder logs screen.
|
||||
@MainActor
|
||||
func test_receive_flightRecorder_viewFlightRecorderLogsTapped() {
|
||||
subject.receive(.flightRecorder(.viewLogsTapped))
|
||||
|
||||
XCTAssertEqual(coordinator.routes, [.flightRecorder(.flightRecorderLogs)])
|
||||
}
|
||||
|
||||
/// `receive(_:)` with `.helpCenterTapped` set the URL to open in the state.
|
||||
@MainActor
|
||||
func test_receive_helpCenterTapped() {
|
||||
@ -226,15 +236,6 @@ class AboutProcessorTests: BitwardenTestCase {
|
||||
XCTAssertEqual(subject.state.toast, Toast(title: Localizations.valueHasBeenCopied(Localizations.appInfo)))
|
||||
}
|
||||
|
||||
/// `receive(_:)` with action `.isFlightRecorderToggleOn` navigates to the view flight recorder
|
||||
/// logs screen.
|
||||
@MainActor
|
||||
func test_receive_viewFlightRecorderLogsTapped() {
|
||||
subject.receive(.viewFlightRecorderLogsTapped)
|
||||
|
||||
XCTAssertEqual(coordinator.routes, [.flightRecorder(.flightRecorderLogs)])
|
||||
}
|
||||
|
||||
/// `receive(_:)` with `.webVaultTapped` shows an alert for navigating to the web vault
|
||||
/// When `Continue` is tapped on the alert, sets the URL to open in the state.
|
||||
@MainActor
|
||||
|
||||
@ -15,8 +15,8 @@ struct AboutState {
|
||||
/// The copyright text.
|
||||
var copyrightText = ""
|
||||
|
||||
/// The flight recorder's active log metadata, if logging is enabled.
|
||||
var flightRecorderActiveLog: FlightRecorderData.LogMetadata?
|
||||
/// The state for the Flight Recorder feature.
|
||||
var flightRecorderState = FlightRecorderSettingsSectionState()
|
||||
|
||||
/// Whether the submit crash logs toggle is on.
|
||||
var isSubmitCrashLogsToggleOn: Bool = false
|
||||
@ -29,22 +29,4 @@ struct AboutState {
|
||||
|
||||
/// The version of the app.
|
||||
var version = ""
|
||||
|
||||
// MARK: Computed Properties
|
||||
|
||||
/// The accessibility label for the flight recorder toggle.
|
||||
var flightRecorderToggleAccessibilityLabel: String {
|
||||
var accessibilityLabelComponents = [Localizations.flightRecorder]
|
||||
if let log = flightRecorderActiveLog {
|
||||
// VoiceOver doesn't read the short date style correctly so use the medium style instead.
|
||||
let dateFormatter = DateFormatter()
|
||||
dateFormatter.dateStyle = .medium
|
||||
|
||||
accessibilityLabelComponents.append(Localizations.loggingEndsOnDateAtTime(
|
||||
dateFormatter.string(from: log.endDate),
|
||||
log.formattedEndTime,
|
||||
))
|
||||
}
|
||||
return accessibilityLabelComponents.joined(separator: ", ")
|
||||
}
|
||||
}
|
||||
|
||||
@ -45,18 +45,30 @@ class AboutViewTests: BitwardenTestCase {
|
||||
|
||||
/// The flight recorder toggle turns logging on and off.
|
||||
@MainActor
|
||||
func test_flightRecorderToggle_tap() async throws {
|
||||
func test_flightRecorder_toggle_tap() async throws {
|
||||
let toggle = try subject.inspect().find(toggleWithAccessibilityLabel: Localizations.flightRecorder)
|
||||
|
||||
try toggle.tap()
|
||||
try await waitForAsync { !self.processor.effects.isEmpty }
|
||||
XCTAssertEqual(processor.effects, [.toggleFlightRecorder(true)])
|
||||
XCTAssertEqual(processor.effects, [.flightRecorder(.toggleFlightRecorder(true))])
|
||||
processor.effects.removeAll()
|
||||
|
||||
processor.state.flightRecorderActiveLog = FlightRecorderData.LogMetadata(duration: .eightHours, startDate: .now)
|
||||
processor.state.flightRecorderState.activeLog = FlightRecorderData.LogMetadata(
|
||||
duration: .eightHours,
|
||||
startDate: .now,
|
||||
)
|
||||
try toggle.tap()
|
||||
try await waitForAsync { !self.processor.effects.isEmpty }
|
||||
XCTAssertEqual(processor.effects, [.toggleFlightRecorder(false)])
|
||||
XCTAssertEqual(processor.effects, [.flightRecorder(.toggleFlightRecorder(false))])
|
||||
}
|
||||
|
||||
/// Tapping the flight recorder view recorded logs button dispatches the
|
||||
/// `.viewFlightRecorderLogsTapped` action.
|
||||
@MainActor
|
||||
func test_flightRecorder_viewRecordedLogsButton_tap() throws {
|
||||
let button = try subject.inspect().find(button: Localizations.viewRecordedLogs)
|
||||
try button.tap()
|
||||
XCTAssertEqual(processor.dispatchedActions.last, .flightRecorder(.viewLogsTapped))
|
||||
}
|
||||
|
||||
/// Tapping the privacy policy button dispatches the `.privacyPolicyTapped` action.
|
||||
@ -83,14 +95,6 @@ class AboutViewTests: BitwardenTestCase {
|
||||
XCTAssertEqual(processor.dispatchedActions.last, .versionTapped)
|
||||
}
|
||||
|
||||
/// Tapping the view recorded logs button dispatches the `.viewFlightRecorderLogsTapped` action.
|
||||
@MainActor
|
||||
func test_viewRecordedLogsButton_tap() throws {
|
||||
let button = try subject.inspect().find(button: Localizations.viewRecordedLogs)
|
||||
try button.tap()
|
||||
XCTAssertEqual(processor.dispatchedActions.last, .viewFlightRecorderLogsTapped)
|
||||
}
|
||||
|
||||
/// Tapping the web vault button dispatches the `.webVaultTapped` action.
|
||||
@MainActor
|
||||
func test_webVaultButton_tap() throws {
|
||||
|
||||
@ -27,7 +27,7 @@ struct AboutView: View {
|
||||
|
||||
copyrightNotice
|
||||
}
|
||||
.animation(.default, value: store.state.flightRecorderActiveLog)
|
||||
.animation(.default, value: store.state.flightRecorderState.activeLog)
|
||||
.scrollView()
|
||||
.navigationBar(title: Localizations.about, titleDisplayMode: .inline)
|
||||
.task {
|
||||
@ -62,41 +62,13 @@ struct AboutView: View {
|
||||
|
||||
/// The section for the flight recorder.
|
||||
@ViewBuilder private var flightRecorderSection: some View {
|
||||
ContentBlock(dividerLeadingPadding: 16) {
|
||||
BitwardenToggle(
|
||||
isOn: store.bindingAsync(
|
||||
get: { $0.flightRecorderActiveLog != nil },
|
||||
perform: AboutEffect.toggleFlightRecorder,
|
||||
),
|
||||
accessibilityIdentifier: "FlightRecorderSwitch",
|
||||
accessibilityLabel: store.state.flightRecorderToggleAccessibilityLabel,
|
||||
) {
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
HStack(spacing: 8) {
|
||||
Text(Localizations.flightRecorder)
|
||||
|
||||
Button {
|
||||
openURL(ExternalLinksConstants.flightRecorderHelp)
|
||||
} label: {
|
||||
SharedAsset.Icons.questionCircle16.swiftUIImage
|
||||
.scaledFrame(width: 16, height: 16)
|
||||
.accessibilityLabel(Localizations.learnMore)
|
||||
}
|
||||
.buttonStyle(.fieldLabelIcon)
|
||||
}
|
||||
|
||||
if let log = store.state.flightRecorderActiveLog {
|
||||
Text(Localizations.loggingEndsOnDateAtTime(log.formattedEndDate, log.formattedEndTime))
|
||||
.foregroundStyle(SharedAsset.Colors.textSecondary.swiftUIColor)
|
||||
.styleGuide(.subheadline)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SettingsListItem(Localizations.viewRecordedLogs) {
|
||||
store.send(.viewFlightRecorderLogsTapped)
|
||||
}
|
||||
}
|
||||
FlightRecorderSettingsSectionView(
|
||||
store: store.child(
|
||||
state: \.flightRecorderState,
|
||||
mapAction: { .flightRecorder($0) },
|
||||
mapEffect: { .flightRecorder($0) },
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
/// The section of miscellaneous about items.
|
||||
@ -154,9 +126,9 @@ struct AboutView: View {
|
||||
|
||||
#Preview {
|
||||
AboutView(store: Store(processor: StateProcessor(state: AboutState(
|
||||
flightRecorderActiveLog: FlightRecorderData.LogMetadata(
|
||||
flightRecorderState: FlightRecorderSettingsSectionState(activeLog: FlightRecorderData.LogMetadata(
|
||||
duration: .eightHours,
|
||||
startDate: Date(timeIntervalSinceNow: 60 * 60 * -4),
|
||||
),
|
||||
)),
|
||||
))))
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user