From 19611664d3cdbb32efb7f3db25bc2e25e47d49ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9CSamoilenkoVadym=E2=80=9D?= <“samoylenko.vadym@gmail.com”> Date: Sun, 2 Mar 2025 21:53:47 +0000 Subject: [PATCH] Fix color handling in views and fix tests. Update CategoryModel with proper color system --- .github/workflows/tests.yml | 40 +++-- Coinly.xcodeproj/project.pbxproj | 132 +++++++++++++++ .../xcshareddata/xcschemes/Coinly.xcscheme | 19 ++- .../xcschemes/xcschememanagement.plist | 14 ++ .../xcschemes/CoinlyTests.xcscheme | 79 +++++++++ .../xcschemes/xcschememanagement.plist | 10 +- Coinly/Features/Models/CategoryModel.swift | 36 +++-- Coinly/TEST/AccountTests.swift | 151 ------------------ Coinly/TEST/CoinlyUITests.swift | 67 -------- Coinly/UI/Components/PieChartView.swift | 95 +++++------ Coinly/UI/Components/TransactionRowView.swift | 101 ++++-------- CoinlyTests.xctestplan | 25 +++ CoinlyTests/AccountTests.swift | 83 ++++++++++ 13 files changed, 481 insertions(+), 371 deletions(-) rename Coinly.xcodeproj/{ => project.xcworkspace}/xcshareddata/xcschemes/Coinly.xcscheme (80%) create mode 100644 Coinly.xcodeproj/project.xcworkspace/xcuserdata/vadymsamoilenko.xcuserdatad/xcschemes/xcschememanagement.plist create mode 100644 Coinly.xcodeproj/xcshareddata/xcschemes/CoinlyTests.xcscheme delete mode 100644 Coinly/TEST/AccountTests.swift delete mode 100644 Coinly/TEST/CoinlyUITests.swift create mode 100644 CoinlyTests.xctestplan create mode 100644 CoinlyTests/AccountTests.swift diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 790721a..fe1e7e7 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -7,21 +7,29 @@ on: branches: [ main ] jobs: - build: - runs-on: macos-latest - - steps: - - uses: actions/checkout@v3 + ios-build: + name: Build and Test iOS app + runs-on: macos-13 - - name: Select Xcode - run: sudo xcode-select -switch /Applications/Xcode.app + steps: + - uses: actions/checkout@v3 - - name: Build and Test - run: | - xcodebuild test \ - -project Coinly.xcodeproj \ - -scheme Coinly \ - -destination 'platform=iOS Simulator,name=iPhone 14,OS=latest' \ - -enableCodeCoverage YES \ - CODE_SIGN_IDENTITY="" \ - CODE_SIGNING_REQUIRED=NO + - name: Set Xcode version + run: sudo xcode-select -s /Applications/Xcode.app + + - name: List available schemes + run: xcodebuild -list -project Coinly.xcodeproj + + - name: List available simulators + run: xcrun simctl list devices available + + - name: Build and test + run: | + xcodebuild clean build test \ + -project Coinly.xcodeproj \ + -scheme "Coinly" \ + -sdk iphonesimulator \ + -destination "platform=iOS Simulator,name=iPhone 14,OS=16.4" \ + ONLY_ACTIVE_ARCH=YES \ + CODE_SIGNING_REQUIRED=NO \ + CODE_SIGNING_ALLOWED=NO diff --git a/Coinly.xcodeproj/project.pbxproj b/Coinly.xcodeproj/project.pbxproj index f624544..5faa7c3 100644 --- a/Coinly.xcodeproj/project.pbxproj +++ b/Coinly.xcodeproj/project.pbxproj @@ -6,8 +6,25 @@ objectVersion = 77; objects = { +/* Begin PBXBuildFile section */ + F762F5752D7506E700D76FFE /* AccountTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F762F5742D7506E700D76FFE /* AccountTests.swift */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + F70CDCCB2D7502EB00FF9D53 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = F70CDC3D2D74D15500FF9D53 /* Project object */; + proxyType = 1; + remoteGlobalIDString = F70CDC442D74D15500FF9D53; + remoteInfo = Coinly; + }; +/* End PBXContainerItemProxy section */ + /* Begin PBXFileReference section */ F70CDC452D74D15500FF9D53 /* Coinly.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Coinly.app; sourceTree = BUILT_PRODUCTS_DIR; }; + F70CDCC72D7502EB00FF9D53 /* CoinlyTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = CoinlyTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + F762F5742D7506E700D76FFE /* AccountTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountTests.swift; sourceTree = ""; }; + F79B1D252D75096900B5F35C /* CoinlyTests.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = CoinlyTests.xctestplan; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFileSystemSynchronizedRootGroup section */ @@ -26,12 +43,21 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + F70CDCC42D7502EB00FF9D53 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ F70CDC3C2D74D15500FF9D53 = { isa = PBXGroup; children = ( + F79B1D252D75096900B5F35C /* CoinlyTests.xctestplan */, + F762F5732D75067400D76FFE /* CoinlyTests */, F70CDC472D74D15500FF9D53 /* Coinly */, F70CDC462D74D15500FF9D53 /* Products */, ); @@ -41,10 +67,19 @@ isa = PBXGroup; children = ( F70CDC452D74D15500FF9D53 /* Coinly.app */, + F70CDCC72D7502EB00FF9D53 /* CoinlyTests.xctest */, ); name = Products; sourceTree = ""; }; + F762F5732D75067400D76FFE /* CoinlyTests */ = { + isa = PBXGroup; + children = ( + F762F5742D7506E700D76FFE /* AccountTests.swift */, + ); + path = CoinlyTests; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -70,6 +105,26 @@ productReference = F70CDC452D74D15500FF9D53 /* Coinly.app */; productType = "com.apple.product-type.application"; }; + F70CDCC62D7502EB00FF9D53 /* CoinlyTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = F70CDCCD2D7502EC00FF9D53 /* Build configuration list for PBXNativeTarget "CoinlyTests" */; + buildPhases = ( + F70CDCC32D7502EB00FF9D53 /* Sources */, + F70CDCC42D7502EB00FF9D53 /* Frameworks */, + F70CDCC52D7502EB00FF9D53 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + F70CDCCC2D7502EB00FF9D53 /* PBXTargetDependency */, + ); + name = CoinlyTests; + packageProductDependencies = ( + ); + productName = CoinlyTests; + productReference = F70CDCC72D7502EB00FF9D53 /* CoinlyTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ @@ -83,6 +138,11 @@ F70CDC442D74D15500FF9D53 = { CreatedOnToolsVersion = 16.2; }; + F70CDCC62D7502EB00FF9D53 = { + CreatedOnToolsVersion = 16.2; + LastSwiftMigration = 1620; + TestTargetID = F70CDC442D74D15500FF9D53; + }; }; }; buildConfigurationList = F70CDC402D74D15500FF9D53 /* Build configuration list for PBXProject "Coinly" */; @@ -100,6 +160,7 @@ projectRoot = ""; targets = ( F70CDC442D74D15500FF9D53 /* Coinly */, + F70CDCC62D7502EB00FF9D53 /* CoinlyTests */, ); }; /* End PBXProject section */ @@ -112,6 +173,13 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + F70CDCC52D7502EB00FF9D53 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXResourcesBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ @@ -122,8 +190,24 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + F70CDCC32D7502EB00FF9D53 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + F762F5752D7506E700D76FFE /* AccountTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXSourcesBuildPhase section */ +/* Begin PBXTargetDependency section */ + F70CDCCC2D7502EB00FF9D53 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = F70CDC442D74D15500FF9D53 /* Coinly */; + targetProxy = F70CDCCB2D7502EB00FF9D53 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + /* Begin XCBuildConfiguration section */ F70CDC512D74D15600FF9D53 /* Debug */ = { isa = XCBuildConfiguration; @@ -302,6 +386,45 @@ }; name = Release; }; + F70CDCCE2D7502EC00FF9D53 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = DQDRL8F7U2; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.vadymsamoilenko.coinly.CoinlyTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Coinly.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Coinly"; + }; + name = Debug; + }; + F70CDCCF2D7502EC00FF9D53 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = DQDRL8F7U2; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.vadymsamoilenko.coinly.CoinlyTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Coinly.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Coinly"; + }; + name = Release; + }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ @@ -323,6 +446,15 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; + F70CDCCD2D7502EC00FF9D53 /* Build configuration list for PBXNativeTarget "CoinlyTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + F70CDCCE2D7502EC00FF9D53 /* Debug */, + F70CDCCF2D7502EC00FF9D53 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; /* End XCConfigurationList section */ }; rootObject = F70CDC3D2D74D15500FF9D53 /* Project object */; diff --git a/Coinly.xcodeproj/xcshareddata/xcschemes/Coinly.xcscheme b/Coinly.xcodeproj/project.xcworkspace/xcshareddata/xcschemes/Coinly.xcscheme similarity index 80% rename from Coinly.xcodeproj/xcshareddata/xcschemes/Coinly.xcscheme rename to Coinly.xcodeproj/project.xcworkspace/xcshareddata/xcschemes/Coinly.xcscheme index 92b0a85..e0a10e9 100644 --- a/Coinly.xcodeproj/xcshareddata/xcschemes/Coinly.xcscheme +++ b/Coinly.xcodeproj/project.xcworkspace/xcshareddata/xcschemes/Coinly.xcscheme @@ -18,7 +18,7 @@ BlueprintIdentifier = "F70CDC442D74D15500FF9D53" BuildableName = "Coinly.app" BlueprintName = "Coinly" - ReferencedContainer = "container:Coinly.xcodeproj"> + ReferencedContainer = "container:"> @@ -29,6 +29,19 @@ selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" shouldUseLaunchSchemeArgsEnv = "YES" shouldAutocreateTestPlan = "YES"> + + + + + + + ReferencedContainer = "container:"> @@ -64,7 +77,7 @@ BlueprintIdentifier = "F70CDC442D74D15500FF9D53" BuildableName = "Coinly.app" BlueprintName = "Coinly" - ReferencedContainer = "container:Coinly.xcodeproj"> + ReferencedContainer = "container:"> diff --git a/Coinly.xcodeproj/project.xcworkspace/xcuserdata/vadymsamoilenko.xcuserdatad/xcschemes/xcschememanagement.plist b/Coinly.xcodeproj/project.xcworkspace/xcuserdata/vadymsamoilenko.xcuserdatad/xcschemes/xcschememanagement.plist new file mode 100644 index 0000000..f3e1503 --- /dev/null +++ b/Coinly.xcodeproj/project.xcworkspace/xcuserdata/vadymsamoilenko.xcuserdatad/xcschemes/xcschememanagement.plist @@ -0,0 +1,14 @@ + + + + + SchemeUserState + + Coinly.xcscheme_^#shared#^_ + + orderHint + 0 + + + + diff --git a/Coinly.xcodeproj/xcshareddata/xcschemes/CoinlyTests.xcscheme b/Coinly.xcodeproj/xcshareddata/xcschemes/CoinlyTests.xcscheme new file mode 100644 index 0000000..fcfc58d --- /dev/null +++ b/Coinly.xcodeproj/xcshareddata/xcschemes/CoinlyTests.xcscheme @@ -0,0 +1,79 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Coinly.xcodeproj/xcuserdata/vadymsamoilenko.xcuserdatad/xcschemes/xcschememanagement.plist b/Coinly.xcodeproj/xcuserdata/vadymsamoilenko.xcuserdatad/xcschemes/xcschememanagement.plist index f3e1503..b3ca69c 100644 --- a/Coinly.xcodeproj/xcuserdata/vadymsamoilenko.xcuserdatad/xcschemes/xcschememanagement.plist +++ b/Coinly.xcodeproj/xcuserdata/vadymsamoilenko.xcuserdatad/xcschemes/xcschememanagement.plist @@ -4,11 +4,19 @@ SchemeUserState - Coinly.xcscheme_^#shared#^_ + CoinlyTests.xcscheme_^#shared#^_ orderHint 0 + SuppressBuildableAutocreation + + F70CDC442D74D15500FF9D53 + + primary + + + diff --git a/Coinly/Features/Models/CategoryModel.swift b/Coinly/Features/Models/CategoryModel.swift index 2c3d099..46056b5 100644 --- a/Coinly/Features/Models/CategoryModel.swift +++ b/Coinly/Features/Models/CategoryModel.swift @@ -6,26 +6,36 @@ // -import Foundation +import SwiftUI struct CategoryModel: Identifiable { let id = UUID() let name: String - let icon: String // SF Symbols name - let color: String // Будем хранить как строку -} - -extension CategoryModel { + let icon: String + let colorName: String // переименовали свойство с color на colorName + static let categories = [ - CategoryModel(name: "Food", icon: "cart.fill", color: "red"), - CategoryModel(name: "Transport", icon: "car.fill", color: "blue"), - CategoryModel(name: "Entertainment", icon: "tv.fill", color: "purple"), - CategoryModel(name: "Shopping", icon: "bag.fill", color: "orange"), - CategoryModel(name: "Salary", icon: "dollarsign.circle.fill", color: "green"), - CategoryModel(name: "Other", icon: "square.fill", color: "gray") + CategoryModel(name: "Food", icon: "cart.fill", colorName: "systemRed"), + CategoryModel(name: "Transport", icon: "car.fill", colorName: "systemBlue"), + CategoryModel(name: "Entertainment", icon: "tv.fill", colorName: "systemPurple"), + CategoryModel(name: "Shopping", icon: "bag.fill", colorName: "systemOrange"), + CategoryModel(name: "Salary", icon: "dollarsign.circle.fill", colorName: "systemGreen"), + CategoryModel(name: "Other", icon: "square.fill", colorName: "systemGray") ] static func category(for name: String) -> CategoryModel { categories.first { $0.name == name } ?? categories.last! } -} \ No newline at end of file + + var color: Color { + switch self.colorName { + case "systemRed": return Color(uiColor: .systemRed) + case "systemBlue": return Color(uiColor: .systemBlue) + case "systemPurple": return Color(uiColor: .systemPurple) + case "systemOrange": return Color(uiColor: .systemOrange) + case "systemGreen": return Color(uiColor: .systemGreen) + case "systemGray": return Color(uiColor: .systemGray) + default: return Color(uiColor: .systemGray) + } + } +} diff --git a/Coinly/TEST/AccountTests.swift b/Coinly/TEST/AccountTests.swift deleted file mode 100644 index 51d040d..0000000 --- a/Coinly/TEST/AccountTests.swift +++ /dev/null @@ -1,151 +0,0 @@ -import XCTest -@testable import Coinly - -class AccountTests: XCTestCase { - var sut: AccountModel! // system under test - - override func setUp() { - super.setUp() - // Создаем тестовый аккаунт перед каждым тестом - sut = AccountModel( - name: "Test Account", - type: .wallet, - currency: .usd, - balance: 1000 - ) - } - - override func tearDown() { - sut = nil - super.tearDown() - } - - func testAccountCreation() { - XCTAssertNotNil(sut) - XCTAssertEqual(sut.name, "Test Account") - XCTAssertEqual(sut.type, .wallet) - XCTAssertEqual(sut.currency, .usd) - XCTAssertEqual(sut.balance, 1000) - XCTAssertTrue(sut.isActive) - } - - func testCreditCardOperations() { - // Создаем кредитную карту для теста - let creditCard = AccountModel( - name: "Test Credit Card", - type: .creditCard, - currency: .usd, - balance: 0, - creditLimit: 1000, - interestRate: 19.99 - ) - - // Тестируем добавление покупки - var card = creditCard - XCTAssertTrue(card.addPurchase(500)) - XCTAssertEqual(card.balance, 500) - - // Тестируем превышение лимита - XCTAssertFalse(card.addPurchase(600)) - XCTAssertEqual(card.balance, 500) - - // Тестируем оплату - XCTAssertTrue(card.makePayment(200)) - XCTAssertEqual(card.balance, 300) - } - - func testAvailableCredit() { - let creditCard = AccountModel( - name: "Test Credit Card", - type: .creditCard, - currency: .usd, - balance: 500, - creditLimit: 1000 - ) - - XCTAssertEqual(creditCard.availableCredit, 500) - } -} - -class TransactionTests: XCTestCase { - func testTransactionCreation() { - let transaction = TransactionModel( - amount: 100, - date: Date(), - type: .expense, - category: "Food", - note: "Lunch", - originalCurrency: .usd - ) - - XCTAssertEqual(transaction.amount, 100) - XCTAssertEqual(transaction.type, .expense) - XCTAssertEqual(transaction.category, "Food") - XCTAssertEqual(transaction.note, "Lunch") - XCTAssertEqual(transaction.originalCurrency, .usd) - } - - func testCurrencyConversion() { - let transaction = TransactionModel( - amount: 100, - date: Date(), - type: .expense, - category: "Food", - originalCurrency: .usd - ) - - let settings = AppSettings.shared - settings.currency = .eur - - let convertedAmount = transaction.amountInCurrentCurrency() - XCTAssertNotEqual(convertedAmount, 100) - } -} - -class AccountsStoreTests: XCTestCase { - var store: AccountsStore! - - override func setUp() { - super.setUp() - store = AccountsStore() - } - - override func tearDown() { - store = nil - super.tearDown() - } - - func testAddAccount() { - let initialCount = store.accounts.count - - let newAccount = AccountModel( - name: "Test Account", - type: .wallet, - currency: .usd, - balance: 1000 - ) - - store.addAccount(newAccount) - - XCTAssertEqual(store.accounts.count, initialCount + 1) - XCTAssertEqual(store.accounts.last?.name, "Test Account") - } - - func testDeleteAccount() { - let account = AccountModel( - name: "Test Account", - type: .wallet, - currency: .usd, - balance: 1000 - ) - - store.addAccount(account) - let initialCount = store.accounts.count - - if let index = store.accounts.firstIndex(where: { $0.id == account.id }) { - store.deleteAccount(at: IndexSet([index])) - } - - XCTAssertEqual(store.accounts.count, initialCount - 1) - } -} \ No newline at end of file diff --git a/Coinly/TEST/CoinlyUITests.swift b/Coinly/TEST/CoinlyUITests.swift deleted file mode 100644 index 5bb426a..0000000 --- a/Coinly/TEST/CoinlyUITests.swift +++ /dev/null @@ -1,67 +0,0 @@ -import XCTest - -class CoinlyUITests: XCTestCase { - var app: XCUIApplication! - - override func setUp() { - super.setUp() - continueAfterFailure = false - app = XCUIApplication() - app.launch() - } - - func testAddNewTransaction() { - // Нажимаем на вкладку Transactions - app.tabBars.buttons["Transactions"].tap() - - // Нажимаем кнопку добавления - app.navigationBars.buttons["Add"].tap() - - // Вводим сумму - let amountField = app.textFields["Amount"] - amountField.tap() - amountField.typeText("100") - - // Выбираем тип транзакции - app.segmentedControls.buttons["Expense"].tap() - - // Выбираем категорию - app.buttons["Category"].tap() - app.buttons["Food"].tap() - - // Сохраняем транзакцию - app.navigationBars.buttons["Add"].tap() - - // Проверяем, что транзакция появилась в списке - XCTAssertTrue(app.staticTexts["$100.00"].exists) - } - - func testAddNewAccount() { - // Нажимаем на вкладку Dashboard - app.tabBars.buttons["Dashboard"].tap() - - // Открываем список счетов - app.buttons["Show All Accounts"].tap() - - // Нажимаем кнопку добавления - app.navigationBars.buttons["Add"].tap() - - // Выбираем тип счета - app.buttons["Wallet"].tap() - - // Заполняем данные счета - let nameField = app.textFields["Account Name"] - nameField.tap() - nameField.typeText("Test Wallet") - - let balanceField = app.textFields["Balance"] - balanceField.tap() - balanceField.typeText("1000") - - // Сохраняем счет - app.navigationBars.buttons["Add"].tap() - - // Проверяем, что счет появился в списке - XCTAssertTrue(app.staticTexts["Test Wallet"].exists) - } -} \ No newline at end of file diff --git a/Coinly/UI/Components/PieChartView.swift b/Coinly/UI/Components/PieChartView.swift index a4f4614..87f703d 100644 --- a/Coinly/UI/Components/PieChartView.swift +++ b/Coinly/UI/Components/PieChartView.swift @@ -1,11 +1,3 @@ -// -// PieChartView.swift -// Coinly -// -// Created by Vadym Samoilenko on 02/03/2025. -// - - import SwiftUI struct PieChartView: View { @@ -19,16 +11,18 @@ struct PieChartView: View { let slices: [PieSlice] var body: some View { - ZStack { - ForEach(slices) { slice in - PieSliceView( - startAngle: startAngle(for: slice), - endAngle: endAngle(for: slice), - color: colorFor(category: slice.category) - ) + GeometryReader { geometry in + ZStack { + ForEach(slices) { slice in + PieSliceShape( + startAngle: startAngle(for: slice), + endAngle: endAngle(for: slice) + ) + .fill(slice.category.color) + } } + .aspectRatio(1, contentMode: .fit) } - .aspectRatio(1, contentMode: .fit) } private func startAngle(for slice: PieSlice) -> Double { @@ -42,39 +36,46 @@ struct PieChartView: View { private func endAngle(for slice: PieSlice) -> Double { startAngle(for: slice) + (slice.percentage * 360) } +} + +struct PieSliceShape: Shape { + let startAngle: Double + let endAngle: Double - private func colorFor(category: CategoryModel) -> Color { - switch category.color { - case "red": return .red - case "blue": return .blue - case "purple": return .purple - case "orange": return .orange - case "green": return .green - case "gray": return .gray - default: return .gray - } + func path(in rect: CGRect) -> Path { + let center = CGPoint(x: rect.midX, y: rect.midY) + let radius = min(rect.width, rect.height) / 2 + var path = Path() + + path.move(to: center) + path.addArc( + center: center, + radius: radius, + startAngle: .degrees(startAngle), + endAngle: .degrees(endAngle), + clockwise: false + ) + path.closeSubpath() + + return path } } -struct PieSliceView: View { - let startAngle: Double - let endAngle: Double - let color: Color - - var body: some View { - GeometryReader { geometry in - Path { path in - let center = CGPoint(x: geometry.size.width/2, y: geometry.size.height/2) - let radius = min(geometry.size.width, geometry.size.height)/2 - path.move(to: center) - path.addArc(center: center, - radius: radius, - startAngle: .degrees(startAngle), - endAngle: .degrees(endAngle), - clockwise: false) - path.closeSubpath() - } - .fill(color) - } +struct PieChartView_Previews: PreviewProvider { + static var previews: some View { + PieChartView(slices: [ + PieChartView.PieSlice( + category: CategoryModel.categories[0], + amount: 100, + percentage: 0.4 + ), + PieChartView.PieSlice( + category: CategoryModel.categories[1], + amount: 150, + percentage: 0.6 + ) + ]) + .frame(height: 200) + .padding() } -} \ No newline at end of file +} diff --git a/Coinly/UI/Components/TransactionRowView.swift b/Coinly/UI/Components/TransactionRowView.swift index 52be251..4bb3841 100644 --- a/Coinly/UI/Components/TransactionRowView.swift +++ b/Coinly/UI/Components/TransactionRowView.swift @@ -8,118 +8,73 @@ struct TransactionRowView: View { CategoryModel.category(for: transaction.category) } - private var categoryColor: Color { - switch category.color { - case "red": return Color(uiColor: .systemRed) - case "blue": return Color(uiColor: .systemBlue) - case "purple": return Color(uiColor: .systemPurple) - case "orange": return Color(uiColor: .systemOrange) - case "green": return Color(uiColor: .systemGreen) - case "gray": return Color(uiColor: .systemGray) - default: return Color(uiColor: .systemGray) - } - } - - let dateFormatter: DateFormatter = { - let formatter = DateFormatter() - formatter.dateFormat = "d MMM yyyy" - return formatter - }() - var body: some View { - HStack(spacing: AppStyle.paddingMedium) { + HStack(spacing: 12) { // Category Icon ZStack { Circle() - .fill(categoryColor) - .frame(width: 36, height: 36) + .fill(category.color.opacity(0.1)) + .frame(width: 44, height: 44) Image(systemName: category.icon) - .font(.system(size: 16, weight: .semibold)) - .foregroundColor(.white) + .font(.system(size: 20)) + .foregroundColor(category.color) } // Transaction Details VStack(alignment: .leading, spacing: 4) { - HStack { - Text(transaction.category) - .font(.system(size: 17, weight: .semibold)) - .foregroundColor(AppStyle.labelPrimary) - - if transaction.isExpense { - Text("Expense") - .font(.system(size: 13)) - .foregroundColor(Color(uiColor: .systemRed)) - } + Text(transaction.category) + .font(.headline) + if let note = transaction.note { + Text(note) + .font(.subheadline) + .foregroundColor(Color(uiColor: .secondaryLabel)) } - - HStack(spacing: 4) { - Text(dateFormatter.string(from: transaction.date)) - - if let note = transaction.note { - Text("•") - Text(note) - } - } - .font(.system(size: 15)) - .foregroundColor(Color(uiColor: .systemGray)) } Spacer() - // Amount + // Amount and Date VStack(alignment: .trailing, spacing: 4) { Text(transaction.amountInCurrentCurrency().formatAsCurrency()) - .font(.system(size: 17, weight: .semibold)) + .font(.headline) .foregroundColor(transaction.isExpense ? Color(uiColor: .systemRed) : Color(uiColor: .systemGreen)) - - Text(transaction.originalCurrency.rawValue) - .font(.system(size: 13)) - .foregroundColor(Color(uiColor: .systemGray)) + Text(transaction.date, style: .date) + .font(.caption) + .foregroundColor(Color(uiColor: .secondaryLabel)) } } - .padding(.horizontal, AppStyle.paddingMedium) - .padding(.vertical, 12) - .background(Color(uiColor: .secondarySystemBackground)) - .cornerRadius(12) + .padding(.vertical, 8) } } struct TransactionRowView_Previews: PreviewProvider { static var previews: some View { - VStack(spacing: 8) { + Group { TransactionRowView(transaction: TransactionModel( - amount: 100.00, - date: Date(), - type: .income, - category: "Salary", - note: "Monthly salary", - originalCurrency: .usd - )) - - TransactionRowView(transaction: TransactionModel( - amount: 25.99, + amount: 42.50, date: Date(), type: .expense, category: "Food", - note: "Lunch", + note: "Lunch at work", originalCurrency: .usd )) + .preferredColorScheme(.light) TransactionRowView(transaction: TransactionModel( - amount: 50.00, + amount: 1200, date: Date(), - type: .expense, - category: "Transport", - note: "Fuel", - originalCurrency: .usd + type: .income, + category: "Salary", + note: "Monthly payment", + originalCurrency: .eur )) + .preferredColorScheme(.dark) } + .previewLayout(.sizeThatFits) .padding() - .background(Color(uiColor: .systemBackground)) .environmentObject(AppSettings.shared) - .preferredColorScheme(.dark) } } diff --git a/CoinlyTests.xctestplan b/CoinlyTests.xctestplan new file mode 100644 index 0000000..a7c9586 --- /dev/null +++ b/CoinlyTests.xctestplan @@ -0,0 +1,25 @@ +{ + "configurations" : [ + { + "id" : "06F004A5-B20A-4C00-A918-8D468952AE86", + "name" : "Test Scheme Action", + "options" : { + + } + } + ], + "defaultOptions" : { + + }, + "testTargets" : [ + { + "parallelizable" : true, + "target" : { + "containerPath" : "container:Coinly.xcodeproj", + "identifier" : "F70CDCC62D7502EB00FF9D53", + "name" : "CoinlyTests" + } + } + ], + "version" : 1 +} diff --git a/CoinlyTests/AccountTests.swift b/CoinlyTests/AccountTests.swift new file mode 100644 index 0000000..8191629 --- /dev/null +++ b/CoinlyTests/AccountTests.swift @@ -0,0 +1,83 @@ +import XCTest +@testable import Coinly + +final class AccountTests: XCTestCase { + var sut: AccountModel! + + override func setUp() { + super.setUp() + sut = AccountModel( + name: "Test Account", + type: .wallet, + currency: .usd, + balance: 1000 + ) + } + + override func tearDown() { + sut = nil + super.tearDown() + } + + func testAccountCreation() { + XCTAssertNotNil(sut) + XCTAssertEqual(sut.name, "Test Account") + XCTAssertEqual(sut.type, .wallet) + XCTAssertEqual(sut.currency, .usd) + XCTAssertEqual(sut.balance, 1000) + XCTAssertTrue(sut.isActive) + } + + func testCreditCardOperations() { + // Создаем кредитную карту для теста + let creditCard = AccountModel( + name: "Test Credit Card", + type: .creditCard, + currency: .usd, + balance: 0, + creditLimit: 1000, + interestRate: 19.99 + ) + + // Тестируем добавление покупки + var card = creditCard + XCTAssertTrue(card.addPurchase(500)) + XCTAssertEqual(card.balance, 500) + + // Тестируем превышение лимита + XCTAssertFalse(card.addPurchase(600)) + XCTAssertEqual(card.balance, 500) + + // Тестируем оплату + XCTAssertTrue(card.makePayment(200)) + XCTAssertEqual(card.balance, 300) + } + + func testAvailableCredit() { + let creditCard = AccountModel( + name: "Test Credit Card", + type: .creditCard, + currency: .usd, + balance: 500, + creditLimit: 1000 + ) + + XCTAssertEqual(creditCard.availableCredit, 500) + } + + func testCurrencyConversion() { + let account = AccountModel( + name: "EUR Account", + type: .wallet, + currency: .eur, + balance: 100 + ) + + let settings = AppSettings.shared + settings.currency = .usd + + // Используем форматированную строку для сравнения + let expectedAmount = account.balance.formatAsCurrency() + XCTAssertEqual(account.formattedBalance, expectedAmount) + } +}