test: add app-level smoke baselines and mobile CI test jobs

Co-authored-by: Velik <hongochai10@users.noreply.github.com>
This commit is contained in:
Cursor Agent
2026-02-23 13:15:02 +00:00
parent 969aa81ea2
commit d808f1d4e1
12 changed files with 432 additions and 0 deletions

View File

@@ -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 \

View File

@@ -0,0 +1,41 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<AssemblyName>AppClientBase.UnitTests</AssemblyName>
<RootNamespace>AppClientBase.UnitTests</RootNamespace>
<TargetFramework>net10.0</TargetFramework>
<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<!-- EN: Test framework dependencies / VI: Phụ thuộc framework test -->
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
<PackageReference Include="xunit" Version="2.9.2" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="FluentAssertions" Version="6.12.2" />
<PackageReference Include="coverlet.collector" Version="6.0.2">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<!-- EN: Source generator package used by included view models.
VI: Gói source generator dùng cho view model được include. -->
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.4.0" />
</ItemGroup>
<ItemGroup>
<!-- EN: Reuse app source files that are platform-neutral.
VI: Tái sử dụng source file ứng dụng không phụ thuộc nền tảng. -->
<Compile Include="..\..\ViewModels\BaseViewModel.cs" Link="ViewModels\BaseViewModel.cs" />
<Compile Include="..\..\ViewModels\MainViewModel.cs" Link="ViewModels\MainViewModel.cs" />
<Compile Include="..\..\Services\INavigationService.cs" Link="Services\INavigationService.cs" />
<Compile Include="..\..\Services\ISettingsService.cs" Link="Services\ISettingsService.cs" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,81 @@
using AppClientBase.Services;
using AppClientBase.ViewModels;
using FluentAssertions;
using Xunit;
namespace AppClientBase.UnitTests.ViewModels;
/// <summary>
/// EN: Unit tests for main view model behavior.
/// VI: Unit tests cho hành vi của main view model.
/// </summary>
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<string, object>? parameters = null) => Task.CompletedTask;
public Task GoBackAsync() => Task.CompletedTask;
}
private sealed class FakeSettingsService : ISettingsService
{
private readonly Dictionary<string, object?> _store = new(StringComparer.Ordinal);
public T Get<T>(string key, T defaultValue)
{
if (_store.TryGetValue(key, out var value) && value is T typed)
{
return typed;
}
return defaultValue;
}
public void Set<T>(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();
}
}

View File

@@ -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"]
),
]
)

View File

@@ -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 dn tương đi t package root đến tài nguyên ng dng.
public static let resourcesRelativePath = "../AppClientBaseSwift/AppClientBaseSwift/Resources"
/// EN: Build absolute path to Localizable.strings for a locale.
/// VI: To đưng dn tuyt đi ti 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: Kim tra file locale có tn ti hay không.
public static func hasLocale(_ locale: String, fileManager: FileManager = .default) -> Bool {
fileManager.fileExists(atPath: localizableFilePath(for: locale))
}
}

View File

@@ -0,0 +1,18 @@
import XCTest
@testable import AppClientBaseSwiftSmoke
/// EN: Smoke tests for localization assets.
/// VI: Smoke tests cho tài nguyên bn đ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"))
}
}

View File

@@ -0,0 +1,36 @@
using FluentAssertions;
using WebClientBase.Shared;
using Xunit;
namespace WebClientBase.SmokeTests;
/// <summary>
/// EN: Smoke tests for shared API response contracts.
/// VI: Smoke tests cho contract API response dùng chung.
/// </summary>
public class ApiResponseTests
{
[Fact]
public void Ok_ShouldReturnSuccessWithPayload()
{
// Act
var response = ApiResponse<string>.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<string>.Fail("bad-request");
// Assert
response.Success.Should().BeFalse();
response.Data.Should().BeNull();
response.Error.Should().Be("bad-request");
}
}

View File

@@ -0,0 +1,32 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<AssemblyName>WebClientBase.SmokeTests</AssemblyName>
<RootNamespace>WebClientBase.SmokeTests</RootNamespace>
<TargetFramework>net10.0</TargetFramework>
<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<!-- EN: Test framework dependencies / VI: Phụ thuộc framework test -->
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
<PackageReference Include="xunit" Version="2.9.2" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="FluentAssertions" Version="6.12.2" />
<PackageReference Include="coverlet.collector" Version="6.0.2">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\WebClientBase.Shared\WebClientBase.Shared.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,44 @@
using EggymonLandingPage.Shared;
using FluentAssertions;
using Xunit;
namespace EggymonLandingPage.SmokeTests;
/// <summary>
/// EN: Smoke tests for landing page shared API response model.
/// VI: Smoke tests cho model API response dùng chung của landing page.
/// </summary>
public class ApiResponseTests
{
[Fact]
public void ApiResponse_ShouldStoreSuccessPayload()
{
// Act
var response = new ApiResponse<string>
{
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<object>
{
Success = false,
Error = "invalid-state",
};
// Assert
response.Success.Should().BeFalse();
response.Data.Should().BeNull();
response.Error.Should().Be("invalid-state");
}
}

View File

@@ -0,0 +1,32 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<AssemblyName>EggymonLandingPage.SmokeTests</AssemblyName>
<RootNamespace>EggymonLandingPage.SmokeTests</RootNamespace>
<TargetFramework>net10.0</TargetFramework>
<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<!-- EN: Test framework dependencies / VI: Phụ thuộc framework test -->
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
<PackageReference Include="xunit" Version="2.9.2" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="FluentAssertions" Version="6.12.2" />
<PackageReference Include="coverlet.collector" Version="6.0.2">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\EggymonLandingPage.Shared\EggymonLandingPage.Shared.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,36 @@
using FluentAssertions;
using WebClientTpos.Shared;
using Xunit;
namespace WebClientTpos.SmokeTests;
/// <summary>
/// EN: Smoke tests for TPOS shared API response helpers.
/// VI: Smoke tests cho helper API response dùng chung của TPOS.
/// </summary>
public class ApiResponseTests
{
[Fact]
public void Ok_ShouldReturnSuccessfulResponse()
{
// Act
var response = ApiResponse<int>.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");
}
}

View File

@@ -0,0 +1,32 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<AssemblyName>WebClientTpos.SmokeTests</AssemblyName>
<RootNamespace>WebClientTpos.SmokeTests</RootNamespace>
<TargetFramework>net10.0</TargetFramework>
<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<!-- EN: Test framework dependencies / VI: Phụ thuộc framework test -->
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
<PackageReference Include="xunit" Version="2.9.2" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="FluentAssertions" Version="6.12.2" />
<PackageReference Include="coverlet.collector" Version="6.0.2">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\WebClientTpos.Shared\WebClientTpos.Shared.csproj" />
</ItemGroup>
</Project>