From e081ca8b3b574cc0defac2be7713f12fc4b1eae2 Mon Sep 17 00:00:00 2001 From: Stefan Lange-Hegermann Date: Tue, 16 Sep 2025 22:00:04 +0200 Subject: [PATCH] navigation fixed --- AGENTS.md | 16 ++ Cable/ContentView.swift | 523 +++++++++++++++++++++-------------- Cable/ItemEditorView.swift | 64 ++++- Cable/SystemEditorView.swift | 8 +- CableTests/CableTests.swift | 37 ++- 5 files changed, 433 insertions(+), 215 deletions(-) create mode 100644 AGENTS.md diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..36b6625 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,16 @@ +# Repository Guidelines + +## Project Structure & Module Organization +The SwiftUI application lives in `Cable/`, with `CableApp.swift` bootstrapping shared state and the scene hierarchy. Feature views such as `CalculatorView`, `SystemEditorView`, and `LoadEditorView` sit beside their supporting models (`CableCalculator`, `UnitSystem`, `Item`). Shared assets are stored in `Assets.xcassets`, while entitlement and configuration files remain at the top level. Unit tests reside in `CableTests/` and UI automation in `CableUITests/`; mirror new feature code with corresponding test folders. + +## Build, Test, and Development Commands +Work in Xcode 15 or newer targeting the iOS 17 simulator. Use `open Cable.xcodeproj` to launch the project with the correct scheme configuration. From the command line, `xcodebuild -scheme Cable -destination 'platform=iOS Simulator,name=iPhone 15' build` performs a debug build, and `xcodebuild -scheme Cable -destination 'platform=iOS Simulator,name=iPhone 15' test` runs both unit and UI test bundles. Prefer running through Xcode when iterating on UI so previews and Instruments remain available. + +## Coding Style & Naming Conventions +Follow idiomatic Swift style: four-space indentation, trailing commas for multiline collections, and 120-character soft line limits. Types use `UpperCamelCase`, methods and properties use `lowerCamelCase`, and tests follow `testScenario_expectedResult`. Keep SwiftUI view files focused on a single view struct, extracting supporting types or extensions into adjacent files when logic grows. Rely on Xcode's "Re-Indent" and "Add Missing Conformance" tools for quick formatting passes before reviews. + +## Testing Guidelines +`CableTests.swift` demonstrates standard XCTest usage; add focused test cases that cover each calculator branch and unit conversion rule you touch. Place UI flows that require navigation assertions in `CableUITests/`, reusing launch helpers from `CableUITestsLaunchTests`. Run `xcodebuild ... test` (above) or the Test action in Xcode before submitting changes. Aim to keep or raise coverage for core calculation logic when introducing new units or system behaviors. + +## Commit & Pull Request Guidelines +Existing history favors short, descriptive subjects (e.g., `systems first`); continue with concise, imperative summaries under 50 characters, adding optional bodies when context is needed. Group unrelated work into separate commits to keep diffs reviewable. Pull requests should link related issues, summarize functional changes, call out impacted screens, and attach simulator screenshots for major UI updates. Mention any follow-up tasks or technical debt explicitly in the PR description. diff --git a/Cable/ContentView.swift b/Cable/ContentView.swift index a83396b..cfa1557 100644 --- a/Cable/ContentView.swift +++ b/Cable/ContentView.swift @@ -30,8 +30,22 @@ struct SystemsView: View { @Environment(\.modelContext) private var modelContext @EnvironmentObject var unitSettings: UnitSystemSettings @Query(sort: \ElectricalSystem.timestamp, order: .reverse) private var systems: [ElectricalSystem] - @State private var newSystemToEdit: ElectricalSystem? - @State private var systemToEdit: ElectricalSystem? + @State private var systemNavigationTarget: SystemNavigationTarget? + + private struct SystemNavigationTarget: Identifiable, Hashable { + let id = UUID() + let system: ElectricalSystem + let presentSystemEditor: Bool + let loadToOpenOnAppear: SavedLoad? + + static func == (lhs: SystemNavigationTarget, rhs: SystemNavigationTarget) -> Bool { + lhs.id == rhs.id + } + + func hash(into hasher: inout Hasher) { + hasher.combine(id) + } + } var body: some View { NavigationStack { @@ -52,9 +66,6 @@ struct SystemsView: View { .font(.title3) .foregroundColor(.white) } - .onTapGesture { - systemToEdit = system - } VStack(alignment: .leading, spacing: 4) { Text(system.name) @@ -93,27 +104,11 @@ struct SystemsView: View { } } } - .navigationDestination(item: $newSystemToEdit) { system in - LoadsView(system: system) - } - .sheet(item: $systemToEdit) { system in - SystemEditorView( - systemName: Binding( - get: { system.name }, - set: { system.name = $0 } - ), - location: Binding( - get: { system.location }, - set: { system.location = $0 } - ), - iconName: Binding( - get: { system.iconName }, - set: { system.iconName = $0 } - ), - colorName: Binding( - get: { system.colorName }, - set: { system.colorName = $0 } - ) + .navigationDestination(item: $systemNavigationTarget) { target in + LoadsView( + system: target.system, + presentSystemEditorOnAppear: target.presentSystemEditor, + loadToOpenOnAppear: target.loadToOpenOnAppear ) } } @@ -129,49 +124,113 @@ struct SystemsView: View { .fill(Color.blue.opacity(0.1)) .frame(width: 80, height: 80) - Image(systemName: "building.2") + Image(systemName: "bolt.circle") .font(.system(size: 40)) .foregroundColor(.blue) } VStack(spacing: 8) { - Text("No Systems Yet") + Text("Welcome to Cable by VoltPlan") .font(.title2) .fontWeight(.semibold) .foregroundColor(.primary) - Text("Create your first electrical system to start managing loads and calculations.") + Text("We'll create your first system and component so you can jump straight into the calculator.") .font(.body) .foregroundColor(.secondary) .multilineTextAlignment(.center) + .fixedSize(horizontal: false, vertical: true) .padding(.horizontal, 32) } - Button(action: { - createNewSystem() - }) { - HStack(spacing: 8) { - Image(systemName: "plus.circle.fill") - .font(.system(size: 16)) - Text("Create System") - .fontWeight(.medium) + VStack(spacing: 12) { + Button(action: { + startComponentOnboarding() + }) { + HStack(spacing: 8) { + Image(systemName: "plus.circle.fill") + .font(.system(size: 16)) + Text("Create Component") + .fontWeight(.medium) + } + .foregroundColor(.white) + .frame(maxWidth: .infinity) + .frame(height: 50) + .background(Color.blue) + .cornerRadius(12) } - .foregroundColor(.white) - .frame(maxWidth: .infinity) - .frame(height: 50) - .background(Color.blue) - .cornerRadius(12) + .buttonStyle(.plain) + + Button(action: { + // TODO: Open VoltPlan component library + print("Opening VoltPlan component library...") + }) { + HStack(spacing: 8) { + Image(systemName: "square.grid.3x3") + .font(.system(size: 16)) + Text("Browse VoltPlan Library") + .fontWeight(.medium) + Image(systemName: "arrow.up.right") + .font(.system(size: 12)) + } + .foregroundColor(.blue) + .frame(maxWidth: .infinity) + .frame(height: 50) + .background(Color.blue.opacity(0.1)) + .cornerRadius(12) + } + .buttonStyle(.plain) } - .buttonStyle(.plain) .padding(.horizontal, 32) } Spacer() - Spacer() + + VStack(spacing: 8) { + HStack(spacing: 6) { + Image(systemName: "exclamationmark.triangle.fill") + .foregroundColor(.orange) + .font(.system(size: 16)) + Text("Important Safety Notice") + .font(.headline) + .fontWeight(.semibold) + } + + Text("This app provides estimates for educational purposes only. Always consult qualified electricians and follow local electrical codes for actual installations. Electrical work can be dangerous and should only be performed by licensed professionals.") + .font(.caption) + .foregroundColor(.secondary) + .multilineTextAlignment(.center) + .padding(.horizontal, 24) + } + .padding(.bottom, 32) } } private func createNewSystem() { + let system = makeSystem() + navigateToSystem(system, presentSystemEditor: true, loadToOpen: nil) + } + + private func navigateToSystem(_ system: ElectricalSystem, presentSystemEditor: Bool, loadToOpen: SavedLoad?, animated: Bool = true) { + let target = SystemNavigationTarget( + system: system, + presentSystemEditor: presentSystemEditor, + loadToOpenOnAppear: loadToOpen + ) + + if animated { + systemNavigationTarget = target + } else { + var transaction = Transaction() + transaction.disablesAnimations = true + withTransaction(transaction) { + systemNavigationTarget = target + } + } + } + + @discardableResult + private func makeSystem() -> ElectricalSystem { let existingNames = Set(systems.map { $0.name }) var systemName = "New System" var counter = 1 @@ -188,14 +247,47 @@ struct SystemsView: View { colorName: "blue" ) modelContext.insert(newSystem) - - newSystemToEdit = newSystem + return newSystem + } + + private func startComponentOnboarding() { + let system = makeSystem() + let load = createNewLoad(in: system) + navigateToSystem(system, presentSystemEditor: false, loadToOpen: load, animated: false) + } + + private func createNewLoad(in system: ElectricalSystem) -> SavedLoad { + let newLoad = SavedLoad( + name: "New Load", + voltage: 12.0, + current: 5.0, + power: 60.0, + length: 10.0, + crossSection: 1.0, + iconName: "lightbulb", + colorName: "blue", + isWattMode: false, + system: system + ) + modelContext.insert(newLoad) + return newLoad } private func deleteSystems(offsets: IndexSet) { withAnimation { for index in offsets { - modelContext.delete(systems[index]) + let system = systems[index] + deleteLoads(for: system) + modelContext.delete(system) + } + } + } + + private func deleteLoads(for system: ElectricalSystem) { + let descriptor = FetchDescriptor() + if let loads = try? modelContext.fetch(descriptor) { + for load in loads where load.system == system { + modelContext.delete(load) } } } @@ -225,11 +317,18 @@ struct LoadsView: View { @EnvironmentObject var unitSettings: UnitSystemSettings @Query(sort: \SavedLoad.timestamp, order: .reverse) private var allLoads: [SavedLoad] @State private var newLoadToEdit: SavedLoad? + @State private var showingSystemEditor = false + @State private var hasPresentedSystemEditorOnAppear = false + @State private var hasOpenedLoadOnAppear = false let system: ElectricalSystem + private let presentSystemEditorOnAppear: Bool + private let loadToOpenOnAppear: SavedLoad? - init(system: ElectricalSystem) { + init(system: ElectricalSystem, presentSystemEditorOnAppear: Bool = false, loadToOpenOnAppear: SavedLoad? = nil) { self.system = system + self.presentSystemEditorOnAppear = presentSystemEditorOnAppear + self.loadToOpenOnAppear = loadToOpenOnAppear } private var savedLoads: [SavedLoad] { @@ -237,117 +336,173 @@ struct LoadsView: View { } var body: some View { - NavigationStack { - VStack(spacing: 0) { - librarySection - - if savedLoads.isEmpty { - emptyStateView - } else { - List { - ForEach(savedLoads) { load in - NavigationLink(destination: CalculatorView(savedLoad: load)) { - HStack(spacing: 12) { - ZStack { - RoundedRectangle(cornerRadius: 10) - .fill(colorForName(load.colorName)) - .frame(width: 44, height: 44) - - Image(systemName: load.iconName) - .font(.title3) - .foregroundColor(.white) + VStack(spacing: 0) { + librarySection + + if savedLoads.isEmpty { + emptyStateView + } else { + List { + ForEach(savedLoads) { load in + NavigationLink(destination: CalculatorView(savedLoad: load)) { + HStack(spacing: 12) { + ZStack { + RoundedRectangle(cornerRadius: 10) + .fill(colorForName(load.colorName)) + .frame(width: 44, height: 44) + + Image(systemName: load.iconName) + .font(.title3) + .foregroundColor(.white) + } + + VStack(alignment: .leading, spacing: 6) { + HStack { + Text(load.name) + .fontWeight(.medium) + Spacer() + Text(load.timestamp, format: .dateTime.month().day().hour().minute()) + .foregroundColor(.secondary) + .font(.caption) } - VStack(alignment: .leading, spacing: 6) { - HStack { - Text(load.name) + + // Secondary info + HStack { + Group { + Text(String(format: "%.1fV", load.voltage)) + Text("•") + if load.isWattMode { + Text(String(format: "%.0fW", load.power)) + } else { + Text(String(format: "%.1fA", load.current)) + } + Text("•") + Text(String(format: "%.1f%@", + unitSettings.unitSystem == .metric ? load.length : load.length * 3.28084, + unitSettings.unitSystem.lengthUnit)) + } + .font(.caption) + .foregroundColor(.secondary) + Spacer() + } + + // Prominent fuse and wire gauge display + HStack(spacing: 12) { + HStack(spacing: 4) { + Text("FUSE") + .font(.caption2) .fontWeight(.medium) - Spacer() - Text(load.timestamp, format: .dateTime.month().day().hour().minute()) .foregroundColor(.secondary) - .font(.caption) + Text("\(recommendedFuse(for: load))A") + .font(.subheadline) + .fontWeight(.bold) + .foregroundColor(.orange) } + .padding(.horizontal, 8) + .padding(.vertical, 4) + .background(Color.orange.opacity(0.1)) + .cornerRadius(6) - - // Secondary info - HStack { - Group { - Text(String(format: "%.1fV", load.voltage)) - Text("•") - if load.isWattMode { - Text(String(format: "%.0fW", load.power)) - } else { - Text(String(format: "%.1fA", load.current)) - } - Text("•") - Text(String(format: "%.1f%@", - unitSettings.unitSystem == .metric ? load.length : load.length * 3.28084, - unitSettings.unitSystem.lengthUnit)) - } - .font(.caption) - .foregroundColor(.secondary) - Spacer() + HStack(spacing: 4) { + Text("WIRE") + .font(.caption2) + .fontWeight(.medium) + .foregroundColor(.secondary) + Text(String(format: unitSettings.unitSystem == .imperial ? "%.0f AWG" : "%.1fmm²", + unitSettings.unitSystem == .imperial ? awgFromCrossSection(load.crossSection) : load.crossSection)) + .font(.subheadline) + .fontWeight(.bold) + .foregroundColor(.blue) } + .padding(.horizontal, 8) + .padding(.vertical, 4) + .background(Color.blue.opacity(0.1)) + .cornerRadius(6) - // Prominent fuse and wire gauge display - HStack(spacing: 12) { - HStack(spacing: 4) { - Text("FUSE") - .font(.caption2) - .fontWeight(.medium) - .foregroundColor(.secondary) - Text("\(recommendedFuse(for: load))A") - .font(.subheadline) - .fontWeight(.bold) - .foregroundColor(.orange) - } - .padding(.horizontal, 8) - .padding(.vertical, 4) - .background(Color.orange.opacity(0.1)) - .cornerRadius(6) - - HStack(spacing: 4) { - Text("WIRE") - .font(.caption2) - .fontWeight(.medium) - .foregroundColor(.secondary) - Text(String(format: unitSettings.unitSystem == .imperial ? "%.0f AWG" : "%.1fmm²", - unitSettings.unitSystem == .imperial ? awgFromCrossSection(load.crossSection) : load.crossSection)) - .font(.subheadline) - .fontWeight(.bold) - .foregroundColor(.blue) - } - .padding(.horizontal, 8) - .padding(.vertical, 4) - .background(Color.blue.opacity(0.1)) - .cornerRadius(6) - - Spacer() - } + Spacer() } } - .padding(.vertical, 4) } + .padding(.vertical, 4) } - .onDelete(perform: deleteLoads) + } + .onDelete(perform: deleteLoads) + } + } + } + .navigationBarTitleDisplayMode(.inline) + .toolbar { + ToolbarItem(placement: .principal) { + Button(action: { + showingSystemEditor = true + }) { + HStack(spacing: 8) { + ZStack { + RoundedRectangle(cornerRadius: 6) + .fill(colorForName(system.colorName)) + .frame(width: 24, height: 24) + + Image(systemName: system.iconName) + .font(.system(size: 12)) + .foregroundColor(.white) + } + + Text(system.name) + .font(.headline) + .fontWeight(.semibold) + .foregroundColor(.primary) } } } - .navigationTitle(system.name) - .toolbar { - ToolbarItem(placement: .navigationBarTrailing) { - HStack { - Button(action: { - createNewLoad() - }) { - Image(systemName: "plus") - } - EditButton() + + ToolbarItem(placement: .navigationBarTrailing) { + HStack { + Button(action: { + createNewLoad() + }) { + Image(systemName: "plus") } + EditButton() } } - .navigationDestination(item: $newLoadToEdit) { load in - CalculatorView(savedLoad: load) + } + .navigationDestination(item: $newLoadToEdit) { load in + CalculatorView(savedLoad: load) + } + .sheet(isPresented: $showingSystemEditor) { + SystemEditorView( + systemName: Binding( + get: { system.name }, + set: { system.name = $0 } + ), + location: Binding( + get: { system.location }, + set: { system.location = $0 } + ), + iconName: Binding( + get: { system.iconName }, + set: { system.iconName = $0 } + ), + colorName: Binding( + get: { system.colorName }, + set: { system.colorName = $0 } + ) + ) + } + .onAppear { + if presentSystemEditorOnAppear && !hasPresentedSystemEditorOnAppear { + hasPresentedSystemEditorOnAppear = true + DispatchQueue.main.async { + showingSystemEditor = true + } + } + + if let loadToOpen = loadToOpenOnAppear, !hasOpenedLoadOnAppear { + hasOpenedLoadOnAppear = true + DispatchQueue.main.async { + newLoadToEdit = loadToOpen + } } } } @@ -393,94 +548,50 @@ struct LoadsView: View { VStack(spacing: 0) { Spacer() - VStack(spacing: 24) { - // Icon + VStack(spacing: 20) { ZStack { Circle() .fill(Color.blue.opacity(0.1)) - .frame(width: 80, height: 80) + .frame(width: 72, height: 72) Image(systemName: "bolt.circle") - .font(.system(size: 40)) + .font(.system(size: 34)) .foregroundColor(.blue) } - // Title and subtitle - VStack(spacing: 8) { - Text("No Loads Yet") - .font(.title2) + VStack(spacing: 6) { + Text("No Components Yet") + .font(.title3) .fontWeight(.semibold) .foregroundColor(.primary) - Text("Get started by creating a new electrical load calculation or browse components from the VoltPlan library.") + Text("Add a component to this system to see cable and fuse recommendations.") .font(.body) .foregroundColor(.secondary) .multilineTextAlignment(.center) .padding(.horizontal, 32) } - // Action buttons - VStack(spacing: 12) { - Button(action: { - createNewLoad() - }) { - HStack(spacing: 8) { - Image(systemName: "plus.circle.fill") - .font(.system(size: 16)) - Text("Create New Load") - .fontWeight(.medium) - } - .foregroundColor(.white) - .frame(maxWidth: .infinity) - .frame(height: 50) - .background(Color.blue) - .cornerRadius(12) + Button(action: { + createNewLoad() + }) { + HStack(spacing: 8) { + Image(systemName: "plus.circle.fill") + .font(.system(size: 16)) + Text("Create Component") + .fontWeight(.medium) } - .buttonStyle(.plain) - - Button(action: { - // TODO: Open VoltPlan component library - print("Opening VoltPlan component library...") - }) { - HStack(spacing: 8) { - Image(systemName: "square.grid.3x3") - .font(.system(size: 16)) - Text("Browse VoltPlan Library") - .fontWeight(.medium) - Image(systemName: "arrow.up.right") - .font(.system(size: 12)) - } - .foregroundColor(.blue) - .frame(maxWidth: .infinity) - .frame(height: 50) - .background(Color.blue.opacity(0.1)) - .cornerRadius(12) - } - .buttonStyle(.plain) + .foregroundColor(.white) + .frame(maxWidth: .infinity) + .frame(height: 48) + .background(Color.blue) + .cornerRadius(12) } + .buttonStyle(.plain) .padding(.horizontal, 32) } Spacer() - - // Safety disclaimer - VStack(spacing: 8) { - HStack(spacing: 6) { - Image(systemName: "exclamationmark.triangle.fill") - .foregroundColor(.orange) - .font(.system(size: 16)) - Text("Important Safety Notice") - .font(.headline) - .fontWeight(.semibold) - } - - Text("This app provides estimates for educational purposes only. Always consult qualified electricians and follow local electrical codes for actual installations. Electrical work can be dangerous and should only be performed by licensed professionals.") - .font(.caption) - .foregroundColor(.secondary) - .multilineTextAlignment(.center) - .padding(.horizontal, 24) - } - .padding(.bottom, 32) } } diff --git a/Cable/ItemEditorView.swift b/Cable/ItemEditorView.swift index a04aa67..d8da622 100644 --- a/Cable/ItemEditorView.swift +++ b/Cable/ItemEditorView.swift @@ -6,6 +6,7 @@ // import SwiftUI +import UIKit struct ItemEditorView: View { @Environment(\.dismiss) private var dismiss @@ -93,8 +94,11 @@ struct ItemEditorView: View { } Section("Details") { - TextField(nameFieldLabel, text: $tempName) - .autocapitalization(.words) + AutoSelectTextField( + text: $tempName, + placeholder: nameFieldLabel, + autoFocus: true + ) additionalFields() } @@ -174,4 +178,58 @@ struct ItemEditorView: View { iconName = tempIconName colorName = tempColorName } -} \ No newline at end of file +} + +private struct AutoSelectTextField: UIViewRepresentable { + @Binding var text: String + let placeholder: String + let autoFocus: Bool + + func makeUIView(context: Context) -> UITextField { + let textField = UITextField(frame: .zero) + textField.delegate = context.coordinator + textField.placeholder = placeholder + textField.text = text + textField.autocapitalizationType = .words + textField.clearButtonMode = .whileEditing + textField.returnKeyType = .done + textField.addTarget(context.coordinator, action: #selector(Coordinator.textChanged(_:)), for: .editingChanged) + return textField + } + + func updateUIView(_ uiView: UITextField, context: Context) { + uiView.text = text + uiView.placeholder = placeholder + uiView.autocapitalizationType = .words + + if autoFocus, !context.coordinator.didFocus { + context.coordinator.didFocus = true + DispatchQueue.main.async { + uiView.becomeFirstResponder() + uiView.selectAll(nil) + } + } + } + + func makeCoordinator() -> Coordinator { + Coordinator(text: $text) + } + + final class Coordinator: NSObject, UITextFieldDelegate { + @Binding var text: String + var didFocus = false + + init(text: Binding) { + self._text = text + } + + @objc func textChanged(_ sender: UITextField) { + text = sender.text ?? "" + } + + func textFieldShouldReturn(_ textField: UITextField) -> Bool { + textField.resignFirstResponder() + return true + } + } +} diff --git a/Cable/SystemEditorView.swift b/Cable/SystemEditorView.swift index 8f29608..442b43a 100644 --- a/Cable/SystemEditorView.swift +++ b/Cable/SystemEditorView.swift @@ -16,10 +16,10 @@ struct SystemEditorView: View { @State private var tempLocation: String private let systemIcons = [ - "building.2", "house", "building", "factory", "office.building", "tent", "car.garage", "sailboat", - "airplane", "ferry", "bus", "truck.box", "van.side", "rv", + "building.2", "house", "building", "tent", "sailboat", + "airplane", "ferry", "bus", "truck.box", "server.rack", "externaldrive", "cpu", "gear", "wrench.adjustable", "hammer", - "lightbulb", "bolt", "powerplug", "battery.100", "solar.panel", "windmill", + "lightbulb", "bolt", "powerplug", "battery.100","sun.max", "engine.combustion", "fuelpump", "drop", "flame", "snowflake", "thermometer" ] @@ -63,4 +63,4 @@ struct SystemEditorView: View { @State var color = "blue" return SystemEditorView(systemName: $name, location: $location, iconName: $icon, colorName: $color) -} \ No newline at end of file +} diff --git a/CableTests/CableTests.swift b/CableTests/CableTests.swift index fd98ff9..127dcb5 100644 --- a/CableTests/CableTests.swift +++ b/CableTests/CableTests.swift @@ -10,8 +10,41 @@ import Testing struct CableTests { - @Test func example() async throws { - // Write your test here and use APIs like `#expect(...)` to check expected conditions. + @Test func metricWireSizingUsesNearestStandardSize() async throws { + let calculator = CableCalculator() + calculator.voltage = 12 + calculator.current = 5 + calculator.length = 10 // meters + + let crossSection = calculator.recommendedCrossSection(for: .metric) + #expect(crossSection == 4.0) + + let voltageDrop = calculator.voltageDrop(for: .metric) + #expect(abs(voltageDrop - 0.425) < 0.001) + + let dropPercentage = calculator.voltageDropPercentage(for: .metric) + #expect(abs(dropPercentage - 3.5417) < 0.001) + + let powerLoss = calculator.powerLoss(for: .metric) + #expect(abs(powerLoss - 2.125) < 0.001) } + @Test func imperialWireSizingMatchesExpectedGauge() async throws { + let calculator = CableCalculator() + calculator.voltage = 120 + calculator.current = 15 + calculator.length = 25 // feet + + let awg = calculator.recommendedCrossSection(for: .imperial) + #expect(awg == 18.0) + + let voltageDrop = calculator.voltageDrop(for: .imperial) + #expect(abs(voltageDrop - 4.722) < 0.01) + + let dropPercentage = calculator.voltageDropPercentage(for: .imperial) + #expect(abs(dropPercentage - 3.935) < 0.01) + + let powerLoss = calculator.powerLoss(for: .imperial) + #expect(abs(powerLoss - 70.83) < 0.05) + } }