From ab50728f07989813130bbb96ea4061a659bdf019 Mon Sep 17 00:00:00 2001 From: Stefan Lange-Hegermann Date: Wed, 12 Aug 2026 11:26:03 +0200 Subject: [PATCH] Disable export until a system has components The Overview share menu offered a wiring diagram and a full report even for an empty system. Gate it on loads, batteries or chargers being present (iOS + Android) and add UI tests for both states. --- Cable/Loads/LoadsView.swift | 5 ++ .../SystemExportButtonUITests.swift | 71 +++++++++++++++++++ .../cable/ui/system/SystemDetailScreen.kt | 2 +- .../cable/ui/system/SystemDetailViewModel.kt | 3 + 4 files changed, 80 insertions(+), 1 deletion(-) create mode 100644 CableUITestsScreenshot/SystemExportButtonUITests.swift diff --git a/Cable/Loads/LoadsView.swift b/Cable/Loads/LoadsView.swift index d0ae6b3..b90c61e 100644 --- a/Cable/Loads/LoadsView.swift +++ b/Cable/Loads/LoadsView.swift @@ -56,6 +56,10 @@ struct LoadsView: View { allChargers.filter { $0.system == system } } + private var hasComponents: Bool { + !savedLoads.isEmpty || !savedBatteries.isEmpty || !savedChargers.isEmpty + } + var body: some View { VStack(spacing: 0) { TabView(selection: $selectedComponentTab) { @@ -174,6 +178,7 @@ struct LoadsView: View { } label: { Image(systemName: "square.and.arrow.up") } + .disabled(!hasComponents) .accessibilityIdentifier("system-overview-share-button") } } else if showPrimary || showEditLoads || showEditBatteries || showEditChargers { diff --git a/CableUITestsScreenshot/SystemExportButtonUITests.swift b/CableUITestsScreenshot/SystemExportButtonUITests.swift new file mode 100644 index 0000000..3db2fcd --- /dev/null +++ b/CableUITestsScreenshot/SystemExportButtonUITests.swift @@ -0,0 +1,71 @@ +import XCTest + +/// Verifies that the Overview export/share menu is only available once the +/// system actually contains something worth exporting. +final class SystemExportButtonUITests: XCTestCase { + + override func setUpWithError() throws { + try super.setUpWithError() + continueAfterFailure = false + XCUIDevice.shared.orientation = .portrait + } + + @MainActor + func testExportIsDisabledForSystemWithoutComponents() throws { + let app = launch(arguments: ["--uitest-reset-data"]) + + let createSystemButton = app.buttons["create-system-button"] + XCTAssertTrue(createSystemButton.waitForExistence(timeout: 15)) + createSystemButton.tap() + + let shareButton = app.buttons["system-overview-share-button"] + XCTAssertTrue(shareButton.waitForExistence(timeout: 15)) + XCTAssertFalse( + shareButton.isEnabled, + "Export must stay disabled while the system has no loads, batteries or chargers" + ) + } + + @MainActor + func testExportIsEnabledOnceComponentsExist() throws { + let app = launch(arguments: ["--uitest-reset-data", "--uitest-sample-data"]) + + openFirstSystem(in: app) + + let shareButton = app.buttons["system-overview-share-button"] + XCTAssertTrue(shareButton.waitForExistence(timeout: 15)) + XCTAssertTrue( + shareButton.isEnabled, + "Export must be available for a system that has components" + ) + } + + // MARK: - Helpers + + private func launch(arguments: [String]) -> XCUIApplication { + let app = XCUIApplication() + app.launchArguments = arguments + app.launch() + return app + } + + private func openFirstSystem(in app: XCUIApplication) { + let list: XCUIElement + if app.collectionViews["systems-list"].waitForExistence(timeout: 15) { + list = app.collectionViews["systems-list"] + } else { + list = app.collectionViews.firstMatch + } + XCTAssertTrue(list.waitForExistence(timeout: 15)) + + let firstCell = list.cells.element(boundBy: 0) + XCTAssertTrue(firstCell.waitForExistence(timeout: 10)) + + let cellButton = firstCell.buttons.firstMatch + if cellButton.exists { + cellButton.tap() + } else { + firstCell.coordinate(withNormalizedOffset: CGVector(dx: 0.5, dy: 0.5)).tap() + } + } +} diff --git a/android/app/src/main/java/app/voltplan/cable/ui/system/SystemDetailScreen.kt b/android/app/src/main/java/app/voltplan/cable/ui/system/SystemDetailScreen.kt index 7540cc5..6b880e6 100644 --- a/android/app/src/main/java/app/voltplan/cable/ui/system/SystemDetailScreen.kt +++ b/android/app/src/main/java/app/voltplan/cable/ui/system/SystemDetailScreen.kt @@ -157,7 +157,7 @@ fun SystemDetailScreen( strokeWidth = 2.dp, ) } else { - IconButton(onClick = { showOverviewMenu = true }) { + IconButton(onClick = { showOverviewMenu = true }, enabled = state.hasComponents) { Icon(Icons.Outlined.IosShare, contentDescription = stringResource(R.string.overview_share_pdf)) } } diff --git a/android/app/src/main/java/app/voltplan/cable/ui/system/SystemDetailViewModel.kt b/android/app/src/main/java/app/voltplan/cable/ui/system/SystemDetailViewModel.kt index f1da298..3055c9c 100644 --- a/android/app/src/main/java/app/voltplan/cable/ui/system/SystemDetailViewModel.kt +++ b/android/app/src/main/java/app/voltplan/cable/ui/system/SystemDetailViewModel.kt @@ -22,6 +22,9 @@ data class DetailState( val chargers: List = emptyList(), ) { val metrics: SystemMetrics get() = SystemMetrics(loads, batteries, chargers) + + val hasComponents: Boolean + get() = loads.isNotEmpty() || batteries.isNotEmpty() || chargers.isNotEmpty() } class SystemDetailViewModel(