diff --git a/FiscalOS.slnx b/FiscalOS.slnx
index 29a95f0..a34ee68 100644
--- a/FiscalOS.slnx
+++ b/FiscalOS.slnx
@@ -12,6 +12,7 @@
+
diff --git a/src/FiscalOS.AppHost/AppHost.cs b/src/FiscalOS.AppHost/AppHost.cs
index c2665c9..6c5f73f 100644
--- a/src/FiscalOS.AppHost/AppHost.cs
+++ b/src/FiscalOS.AppHost/AppHost.cs
@@ -1,10 +1,12 @@
+using FiscalOS.AppHost;
+
var builder = DistributedApplication.CreateBuilder(args);
-var api = builder.AddProject("API")
+var api = builder.AddProject(ProjectNames.API)
.WithHttpHealthCheck("/health");
-builder.AddViteApp("web", "../FiscalOS.Web")
+builder.AddViteApp(ProjectNames.Web, "../FiscalOS.Web")
.WithReference(api)
.WithHttpsEndpoint(port: 7061, env: "PORT");
-builder.Build().Run();
\ No newline at end of file
+builder.Build().Run();
diff --git a/src/FiscalOS.AppHost/ProjectNames.cs b/src/FiscalOS.AppHost/ProjectNames.cs
new file mode 100644
index 0000000..dc60308
--- /dev/null
+++ b/src/FiscalOS.AppHost/ProjectNames.cs
@@ -0,0 +1,7 @@
+namespace FiscalOS.AppHost;
+
+public static class ProjectNames
+{
+ public const string API = "api";
+ public const string Web = "web";
+}
\ No newline at end of file
diff --git a/tests/Directory.Packages.props b/tests/Directory.Packages.props
index 8146a0f..0604d02 100644
--- a/tests/Directory.Packages.props
+++ b/tests/Directory.Packages.props
@@ -3,9 +3,12 @@
true
+
+
+
diff --git a/tests/FiscalOS.AppHost.Tests/FiscalOS.AppHost.Tests.csproj b/tests/FiscalOS.AppHost.Tests/FiscalOS.AppHost.Tests.csproj
new file mode 100644
index 0000000..8a25485
--- /dev/null
+++ b/tests/FiscalOS.AppHost.Tests/FiscalOS.AppHost.Tests.csproj
@@ -0,0 +1,25 @@
+
+
+
+ Exe
+ FiscalOS.AppHost.Tests
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tests/FiscalOS.AppHost.Tests/Infra/AspireFixture.cs b/tests/FiscalOS.AppHost.Tests/Infra/AspireFixture.cs
new file mode 100644
index 0000000..723d40d
--- /dev/null
+++ b/tests/FiscalOS.AppHost.Tests/Infra/AspireFixture.cs
@@ -0,0 +1,30 @@
+using Aspire.Hosting;
+using Aspire.Hosting.Testing;
+
+namespace FiscalOS.AppHost.Tests.Infra;
+
+public sealed class AspireFixture : IAsyncLifetime
+{
+ private DistributedApplication _app = null!;
+
+ public async ValueTask InitializeAsync()
+ {
+ var appHost = await DistributedApplicationTestingBuilder
+ .CreateAsync();
+
+ _app = await appHost.BuildAsync();
+
+ await _app.StartAsync();
+ }
+
+ public async ValueTask DisposeAsync()
+ {
+ await _app.StopAsync();
+ await _app.DisposeAsync();
+ }
+
+ public Uri GetBaseWebUri()
+ {
+ return _app.GetEndpoint(ProjectNames.Web, "https");
+ }
+}
\ No newline at end of file
diff --git a/tests/FiscalOS.AppHost.Tests/Infra/BaseTest.cs b/tests/FiscalOS.AppHost.Tests/Infra/BaseTest.cs
new file mode 100644
index 0000000..5abcaa4
--- /dev/null
+++ b/tests/FiscalOS.AppHost.Tests/Infra/BaseTest.cs
@@ -0,0 +1,48 @@
+
+
+namespace FiscalOS.AppHost.Tests.Infra;
+
+public abstract class BaseTest(
+ AspireFixture aspire,
+ PlaywrightFixture playwright
+) : IAsyncLifetime, IClassFixture, IClassFixture
+{
+ protected AspireFixture AspireFixture { get; } = aspire;
+ protected PlaywrightFixture PlaywrightFixture { get; } = playwright;
+
+ protected IBrowserContext Context { get; private set; } = null!;
+ protected IPage Page { get; private set; } = null!;
+
+ public async ValueTask InitializeAsync()
+ {
+ Context = await PlaywrightFixture.Browser.NewContextAsync(new()
+ {
+ BaseURL = AspireFixture.GetBaseWebUri().ToString(),
+ });
+ Page = await Context.NewPageAsync();
+ }
+
+ public async ValueTask DisposeAsync()
+ {
+ await Page.CloseAsync();
+ await Context.CloseAsync();
+ await Context.DisposeAsync();
+
+ GC.SuppressFinalize(this);
+ }
+
+ public static ILocatorAssertions Expect(ILocator locator)
+ {
+ return Assertions.Expect(locator);
+ }
+
+ public static IPageAssertions Expect(IPage page)
+ {
+ return Assertions.Expect(page);
+ }
+
+ public static IAPIResponseAssertions Expect(IAPIResponse response)
+ {
+ return Assertions.Expect(response);
+ }
+}
\ No newline at end of file
diff --git a/tests/FiscalOS.AppHost.Tests/Infra/PlaywrightFixture.cs b/tests/FiscalOS.AppHost.Tests/Infra/PlaywrightFixture.cs
new file mode 100644
index 0000000..598a300
--- /dev/null
+++ b/tests/FiscalOS.AppHost.Tests/Infra/PlaywrightFixture.cs
@@ -0,0 +1,26 @@
+namespace FiscalOS.AppHost.Tests.Infra;
+
+public sealed class PlaywrightFixture : IAsyncLifetime
+{
+ private IPlaywright? _playwright;
+
+ internal IBrowser Browser { get; set; } = null!;
+
+ public async ValueTask InitializeAsync()
+ {
+ _playwright = await Playwright.CreateAsync();
+
+ Browser = await _playwright.Chromium.LaunchAsync(new()
+ {
+ Headless = false,
+ });
+ }
+
+ public async ValueTask DisposeAsync()
+ {
+ await Browser.CloseAsync();
+ await Browser.DisposeAsync();
+
+ _playwright?.Dispose();
+ }
+}
\ No newline at end of file
diff --git a/tests/FiscalOS.AppHost.Tests/LoginPageTests.cs b/tests/FiscalOS.AppHost.Tests/LoginPageTests.cs
new file mode 100644
index 0000000..56566da
--- /dev/null
+++ b/tests/FiscalOS.AppHost.Tests/LoginPageTests.cs
@@ -0,0 +1,14 @@
+namespace FiscalOS.AppHost.Tests;
+
+public class LoginPageTests(
+ AspireFixture aspire,
+ PlaywrightFixture playwright
+) : BaseTest(aspire, playwright)
+{
+ [Fact]
+ public async Task LoginPage_WhenNavigatedTo_ItShouldDisplaysLoginButton()
+ {
+ await Page.GotoAsync("/public/login");
+ await Expect(Page.GetByText("Login")).ToBeVisibleAsync();
+ }
+}
\ No newline at end of file
diff --git a/tests/FiscalOS.AppHost.Tests/Usings.cs b/tests/FiscalOS.AppHost.Tests/Usings.cs
new file mode 100644
index 0000000..80c8ec2
--- /dev/null
+++ b/tests/FiscalOS.AppHost.Tests/Usings.cs
@@ -0,0 +1,4 @@
+global using FiscalOS.AppHost.Tests.Infra;
+
+global using Microsoft.Playwright;
+global using Microsoft.Playwright.Xunit.v3;
\ No newline at end of file
diff --git a/tests/FiscalOS.AppHost.Tests/xunit.runner.json b/tests/FiscalOS.AppHost.Tests/xunit.runner.json
new file mode 100644
index 0000000..86c7ea0
--- /dev/null
+++ b/tests/FiscalOS.AppHost.Tests/xunit.runner.json
@@ -0,0 +1,3 @@
+{
+ "$schema": "https://xunit.net/schema/current/xunit.runner.schema.json"
+}