diff --git a/.github/workflows/ci-mobile.yml b/.github/workflows/ci-mobile.yml index c3ded279..95d39052 100644 --- a/.github/workflows/ci-mobile.yml +++ b/.github/workflows/ci-mobile.yml @@ -39,11 +39,37 @@ jobs: - name: Build run: dotnet build ${{ matrix.project }} --configuration Release --no-restore + dotnet-client-app-tests: + runs-on: ubuntu-latest + strategy: + matrix: + project: + - apps/app-client-base-net/tests/AppClientBase.UnitTests/AppClientBase.UnitTests.csproj + - apps/web-client-base-net/tests/WebClientBase.SmokeTests/WebClientBase.SmokeTests.csproj + - apps/web-client-tpos-net/tests/WebClientTpos.SmokeTests/WebClientTpos.SmokeTests.csproj + - apps/web-client-eggymon-landipage-net/tests/EggymonLandingPage.SmokeTests/EggymonLandingPage.SmokeTests.csproj + steps: + - uses: actions/checkout@v4 + + - name: Setup .NET + uses: actions/setup-dotnet@v4 + with: + dotnet-version: '10.0.x' + + - name: Restore tests + run: dotnet restore ${{ matrix.project }} + + - name: Run tests + run: dotnet test ${{ matrix.project }} --configuration Release --no-restore + swift-client-app: runs-on: macos-latest steps: - uses: actions/checkout@v4 + - name: Run Swift smoke tests + run: swift test --package-path apps/app-client-base-swift/smoke-tests + - name: Build Swift iOS app run: | xcodebuild \ diff --git a/apps/app-client-base-net/tests/AppClientBase.UnitTests/AppClientBase.UnitTests.csproj b/apps/app-client-base-net/tests/AppClientBase.UnitTests/AppClientBase.UnitTests.csproj new file mode 100644 index 00000000..090bf2e8 --- /dev/null +++ b/apps/app-client-base-net/tests/AppClientBase.UnitTests/AppClientBase.UnitTests.csproj @@ -0,0 +1,41 @@ + + + + AppClientBase.UnitTests + AppClientBase.UnitTests + net10.0 + false + true + enable + enable + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + + + + + + + + diff --git a/apps/app-client-base-net/tests/AppClientBase.UnitTests/ViewModels/MainViewModelTests.cs b/apps/app-client-base-net/tests/AppClientBase.UnitTests/ViewModels/MainViewModelTests.cs new file mode 100644 index 00000000..864c50a3 --- /dev/null +++ b/apps/app-client-base-net/tests/AppClientBase.UnitTests/ViewModels/MainViewModelTests.cs @@ -0,0 +1,81 @@ +using AppClientBase.Services; +using AppClientBase.ViewModels; +using FluentAssertions; +using Xunit; + +namespace AppClientBase.UnitTests.ViewModels; + +/// +/// EN: Unit tests for main view model behavior. +/// VI: Unit tests cho hành vi của main view model. +/// +public class MainViewModelTests +{ + [Fact] + public void Constructor_ShouldSetDefaultTitleAndWelcomeMessage() + { + // Arrange + var navigationService = new FakeNavigationService(); + var settingsService = new FakeSettingsService(); + + // Act + var viewModel = new MainViewModel(navigationService, settingsService); + + // Assert + viewModel.Title.Should().Be("Home"); + viewModel.WelcomeMessage.Should().Be("Welcome to AppClientBase!"); + viewModel.ClickCount.Should().Be(0); + viewModel.ButtonText.Should().Be("Click me"); + } + + [Fact] + public void IncrementCounterCommand_ShouldIncreaseCountAndPersistSetting() + { + // Arrange + var navigationService = new FakeNavigationService(); + var settingsService = new FakeSettingsService(); + var viewModel = new MainViewModel(navigationService, settingsService); + + // Act + viewModel.IncrementCounterCommand.Execute(null); + viewModel.IncrementCounterCommand.Execute(null); + + // Assert + viewModel.ClickCount.Should().Be(2); + viewModel.ButtonText.Should().Be("Clicked 2 times"); + settingsService.Get("ClickCount", 0).Should().Be(2); + } + + private sealed class FakeNavigationService : INavigationService + { + public Task GoToAsync(string route, IDictionary? parameters = null) => Task.CompletedTask; + + public Task GoBackAsync() => Task.CompletedTask; + } + + private sealed class FakeSettingsService : ISettingsService + { + private readonly Dictionary _store = new(StringComparer.Ordinal); + + public T Get(string key, T defaultValue) + { + if (_store.TryGetValue(key, out var value) && value is T typed) + { + return typed; + } + + return defaultValue; + } + + public void Set(string key, T value) + { + _store[key] = value; + } + + public bool Contains(string key) => _store.ContainsKey(key); + + public void Remove(string key) => _store.Remove(key); + + public void Clear() => _store.Clear(); + } +} diff --git a/apps/app-client-base-swift/smoke-tests/Package.swift b/apps/app-client-base-swift/smoke-tests/Package.swift new file mode 100644 index 00000000..01f2264b --- /dev/null +++ b/apps/app-client-base-swift/smoke-tests/Package.swift @@ -0,0 +1,25 @@ +// swift-tools-version: 5.9 + +import PackageDescription + +let package = Package( + name: "AppClientBaseSwiftSmokeTests", + platforms: [ + .macOS(.v14), + ], + products: [ + .library( + name: "AppClientBaseSwiftSmoke", + targets: ["AppClientBaseSwiftSmoke"] + ), + ], + targets: [ + .target( + name: "AppClientBaseSwiftSmoke" + ), + .testTarget( + name: "AppClientBaseSwiftSmokeTests", + dependencies: ["AppClientBaseSwiftSmoke"] + ), + ] +) diff --git a/apps/app-client-base-swift/smoke-tests/Sources/AppClientBaseSwiftSmoke/LocalizationManifest.swift b/apps/app-client-base-swift/smoke-tests/Sources/AppClientBaseSwiftSmoke/LocalizationManifest.swift new file mode 100644 index 00000000..e1a8b2bd --- /dev/null +++ b/apps/app-client-base-swift/smoke-tests/Sources/AppClientBaseSwiftSmoke/LocalizationManifest.swift @@ -0,0 +1,29 @@ +import Foundation + +/// EN: Localization resource smoke-test helper. +/// VI: Helper smoke-test cho tài nguyên localization. +public enum LocalizationManifest { + /// EN: Relative path from package root to app resources. + /// VI: Đường dẫn tương đối từ package root đến tài nguyên ứng dụng. + public static let resourcesRelativePath = "../AppClientBaseSwift/AppClientBaseSwift/Resources" + + /// EN: Build absolute path to Localizable.strings for a locale. + /// VI: Tạo đường dẫn tuyệt đối tới Localizable.strings theo locale. + public static func localizableFilePath( + for locale: String, + currentDirectoryPath: String = FileManager.default.currentDirectoryPath + ) -> String { + let localeFolder = "\(locale).lproj" + return URL(fileURLWithPath: currentDirectoryPath) + .appendingPathComponent(resourcesRelativePath) + .appendingPathComponent(localeFolder) + .appendingPathComponent("Localizable.strings") + .path + } + + /// EN: Check if a locale file exists. + /// VI: Kiểm tra file locale có tồn tại hay không. + public static func hasLocale(_ locale: String, fileManager: FileManager = .default) -> Bool { + fileManager.fileExists(atPath: localizableFilePath(for: locale)) + } +} diff --git a/apps/app-client-base-swift/smoke-tests/Tests/AppClientBaseSwiftSmokeTests/LocalizationManifestTests.swift b/apps/app-client-base-swift/smoke-tests/Tests/AppClientBaseSwiftSmokeTests/LocalizationManifestTests.swift new file mode 100644 index 00000000..171f8b3c --- /dev/null +++ b/apps/app-client-base-swift/smoke-tests/Tests/AppClientBaseSwiftSmokeTests/LocalizationManifestTests.swift @@ -0,0 +1,18 @@ +import XCTest +@testable import AppClientBaseSwiftSmoke + +/// EN: Smoke tests for localization assets. +/// VI: Smoke tests cho tài nguyên bản địa hóa. +final class LocalizationManifestTests: XCTestCase { + func testEnglishLocalizationFileShouldExist() { + XCTAssertTrue(LocalizationManifest.hasLocale("en")) + } + + func testVietnameseLocalizationFileShouldExist() { + XCTAssertTrue(LocalizationManifest.hasLocale("vi")) + } + + func testUnknownLocaleShouldNotExist() { + XCTAssertFalse(LocalizationManifest.hasLocale("zz")) + } +} diff --git a/apps/web-client-base-net/tests/WebClientBase.SmokeTests/ApiResponseTests.cs b/apps/web-client-base-net/tests/WebClientBase.SmokeTests/ApiResponseTests.cs new file mode 100644 index 00000000..4c7483c5 --- /dev/null +++ b/apps/web-client-base-net/tests/WebClientBase.SmokeTests/ApiResponseTests.cs @@ -0,0 +1,36 @@ +using FluentAssertions; +using WebClientBase.Shared; +using Xunit; + +namespace WebClientBase.SmokeTests; + +/// +/// EN: Smoke tests for shared API response contracts. +/// VI: Smoke tests cho contract API response dùng chung. +/// +public class ApiResponseTests +{ + [Fact] + public void Ok_ShouldReturnSuccessWithPayload() + { + // Act + var response = ApiResponse.Ok("healthy"); + + // Assert + response.Success.Should().BeTrue(); + response.Data.Should().Be("healthy"); + response.Error.Should().BeNull(); + } + + [Fact] + public void Fail_ShouldReturnErrorWithoutPayload() + { + // Act + var response = ApiResponse.Fail("bad-request"); + + // Assert + response.Success.Should().BeFalse(); + response.Data.Should().BeNull(); + response.Error.Should().Be("bad-request"); + } +} diff --git a/apps/web-client-base-net/tests/WebClientBase.SmokeTests/WebClientBase.SmokeTests.csproj b/apps/web-client-base-net/tests/WebClientBase.SmokeTests/WebClientBase.SmokeTests.csproj new file mode 100644 index 00000000..82a10674 --- /dev/null +++ b/apps/web-client-base-net/tests/WebClientBase.SmokeTests/WebClientBase.SmokeTests.csproj @@ -0,0 +1,32 @@ + + + + WebClientBase.SmokeTests + WebClientBase.SmokeTests + net10.0 + false + true + enable + enable + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + diff --git a/apps/web-client-eggymon-landipage-net/tests/EggymonLandingPage.SmokeTests/ApiResponseTests.cs b/apps/web-client-eggymon-landipage-net/tests/EggymonLandingPage.SmokeTests/ApiResponseTests.cs new file mode 100644 index 00000000..9e73c2ed --- /dev/null +++ b/apps/web-client-eggymon-landipage-net/tests/EggymonLandingPage.SmokeTests/ApiResponseTests.cs @@ -0,0 +1,44 @@ +using EggymonLandingPage.Shared; +using FluentAssertions; +using Xunit; + +namespace EggymonLandingPage.SmokeTests; + +/// +/// EN: Smoke tests for landing page shared API response model. +/// VI: Smoke tests cho model API response dùng chung của landing page. +/// +public class ApiResponseTests +{ + [Fact] + public void ApiResponse_ShouldStoreSuccessPayload() + { + // Act + var response = new ApiResponse + { + Success = true, + Data = "ok", + }; + + // Assert + response.Success.Should().BeTrue(); + response.Data.Should().Be("ok"); + response.Error.Should().BeNull(); + } + + [Fact] + public void ApiResponse_ShouldStoreErrorMessage() + { + // Act + var response = new ApiResponse + { + Success = false, + Error = "invalid-state", + }; + + // Assert + response.Success.Should().BeFalse(); + response.Data.Should().BeNull(); + response.Error.Should().Be("invalid-state"); + } +} diff --git a/apps/web-client-eggymon-landipage-net/tests/EggymonLandingPage.SmokeTests/EggymonLandingPage.SmokeTests.csproj b/apps/web-client-eggymon-landipage-net/tests/EggymonLandingPage.SmokeTests/EggymonLandingPage.SmokeTests.csproj new file mode 100644 index 00000000..e11fd6a8 --- /dev/null +++ b/apps/web-client-eggymon-landipage-net/tests/EggymonLandingPage.SmokeTests/EggymonLandingPage.SmokeTests.csproj @@ -0,0 +1,32 @@ + + + + EggymonLandingPage.SmokeTests + EggymonLandingPage.SmokeTests + net10.0 + false + true + enable + enable + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + diff --git a/apps/web-client-tpos-net/tests/WebClientTpos.SmokeTests/ApiResponseTests.cs b/apps/web-client-tpos-net/tests/WebClientTpos.SmokeTests/ApiResponseTests.cs new file mode 100644 index 00000000..b7edb028 --- /dev/null +++ b/apps/web-client-tpos-net/tests/WebClientTpos.SmokeTests/ApiResponseTests.cs @@ -0,0 +1,36 @@ +using FluentAssertions; +using WebClientTpos.Shared; +using Xunit; + +namespace WebClientTpos.SmokeTests; + +/// +/// EN: Smoke tests for TPOS shared API response helpers. +/// VI: Smoke tests cho helper API response dùng chung của TPOS. +/// +public class ApiResponseTests +{ + [Fact] + public void Ok_ShouldReturnSuccessfulResponse() + { + // Act + var response = ApiResponse.Ok(200); + + // Assert + response.Success.Should().BeTrue(); + response.Data.Should().Be(200); + response.Error.Should().BeNull(); + } + + [Fact] + public void Fail_ShouldReturnFailedResponse() + { + // Act + var response = ApiResponse.Fail("forbidden"); + + // Assert + response.Success.Should().BeFalse(); + response.Data.Should().BeNull(); + response.Error.Should().Be("forbidden"); + } +} diff --git a/apps/web-client-tpos-net/tests/WebClientTpos.SmokeTests/WebClientTpos.SmokeTests.csproj b/apps/web-client-tpos-net/tests/WebClientTpos.SmokeTests/WebClientTpos.SmokeTests.csproj new file mode 100644 index 00000000..bbdb98ad --- /dev/null +++ b/apps/web-client-tpos-net/tests/WebClientTpos.SmokeTests/WebClientTpos.SmokeTests.csproj @@ -0,0 +1,32 @@ + + + + WebClientTpos.SmokeTests + WebClientTpos.SmokeTests + net10.0 + false + true + enable + enable + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + +