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.
This commit is contained in:
2026-08-12 11:26:03 +02:00
parent 345c8b3ac7
commit ab50728f07
4 changed files with 80 additions and 1 deletions

View File

@@ -56,6 +56,10 @@ struct LoadsView: View {
allChargers.filter { $0.system == system } allChargers.filter { $0.system == system }
} }
private var hasComponents: Bool {
!savedLoads.isEmpty || !savedBatteries.isEmpty || !savedChargers.isEmpty
}
var body: some View { var body: some View {
VStack(spacing: 0) { VStack(spacing: 0) {
TabView(selection: $selectedComponentTab) { TabView(selection: $selectedComponentTab) {
@@ -174,6 +178,7 @@ struct LoadsView: View {
} label: { } label: {
Image(systemName: "square.and.arrow.up") Image(systemName: "square.and.arrow.up")
} }
.disabled(!hasComponents)
.accessibilityIdentifier("system-overview-share-button") .accessibilityIdentifier("system-overview-share-button")
} }
} else if showPrimary || showEditLoads || showEditBatteries || showEditChargers { } else if showPrimary || showEditLoads || showEditBatteries || showEditChargers {

View File

@@ -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()
}
}
}

View File

@@ -157,7 +157,7 @@ fun SystemDetailScreen(
strokeWidth = 2.dp, strokeWidth = 2.dp,
) )
} else { } else {
IconButton(onClick = { showOverviewMenu = true }) { IconButton(onClick = { showOverviewMenu = true }, enabled = state.hasComponents) {
Icon(Icons.Outlined.IosShare, contentDescription = stringResource(R.string.overview_share_pdf)) Icon(Icons.Outlined.IosShare, contentDescription = stringResource(R.string.overview_share_pdf))
} }
} }

View File

@@ -22,6 +22,9 @@ data class DetailState(
val chargers: List<SavedCharger> = emptyList(), val chargers: List<SavedCharger> = emptyList(),
) { ) {
val metrics: SystemMetrics get() = SystemMetrics(loads, batteries, chargers) val metrics: SystemMetrics get() = SystemMetrics(loads, batteries, chargers)
val hasComponents: Boolean
get() = loads.isNotEmpty() || batteries.isNotEmpty() || chargers.isNotEmpty()
} }
class SystemDetailViewModel( class SystemDetailViewModel(