We deliberately test at the edges of the system this gives us:
We write high-level, black-box integration tests that focus only on observable outcomes (behaviour not implementation details).
If a refactor breaks a test without changing observable behaviour, the test was too coupled.
We maintain two test suites:
./Tests → CLI tests
./Tests.E2e → Blazor Web UI tests (Playwright)
All tests must pass before a PR can be merged into main.
If something is worth testing manually, it’s worth automating.
When adding features:
When fixing bugs:
failing testWe use Playwright to test the Blazor Server app.
These tests are:
Avoid:
You must build the Docker image before running:
cd RackPeek
docker build -t rackpeek:ci -f RackPeek.Web/Dockerfile .
cd Tests.E2e
dotnet tool install --global Microsoft.Playwright.CLI
dotnet build
playwright install
dotnet test
Rebuild the image whenever the Web project changes.
Each page/component has a POM (Page Object Model) abstraction.
The POM:
AddDesktopAsync, GotoHardwareAsync)Tests should read like workflows — not like browser scripts.
Example:
[Fact]
public async Task User_Can_Add_And_Delete_Desktop()
{
var (context, page) = await CreatePageAsync();
var resourceName = $"e2e-ap-{Guid.NewGuid():N}"[..16];
try
{
await page.GotoAsync(fixture.BaseUrl);
var layout = new MainLayoutPom(page);
await layout.AssertLoadedAsync();
await layout.GotoHardwareAsync();
var hardwarePage = new HardwareTreePom(page);
await hardwarePage.AssertLoadedAsync();
await hardwarePage.GotoDesktopsListAsync();
var listPage = new DesktopsListPom(page);
await listPage.AssertLoadedAsync();
await listPage.AddDesktopAsync(resourceName);
await listPage.AssertDesktopExists(resourceName);
await listPage.DeleteDesktopAsync(resourceName);
await listPage.AssertDesktopDoesNotExist(resourceName);
}
catch (Exception)
{
_output.WriteLine("TEST FAILED — Capturing diagnostics");
_output.WriteLine($"Current URL: {page.Url}");
var html = await page.ContentAsync();
_output.WriteLine("==== DOM SNAPSHOT START ====");
_output.WriteLine(html);
_output.WriteLine("==== DOM SNAPSHOT END ====");
throw;
}
finally
{
await context.CloseAsync();
}
}
You may temporarily modify:
Tests.E2e/infra/PlaywrightFixture.cs
To debug visually:
Browser = await _playwright.Chromium.LaunchAsync(new BrowserTypeLaunchOptions
{
Headless = false,
SlowMo = 500,
Args = new[]
{
"--disable-dev-shm-usage",
"--no-sandbox"
}
});
⚠ Before committing, revert to CI-safe settings:
Browser = await _playwright.Chromium.LaunchAsync(new BrowserTypeLaunchOptions
{
Headless = true,
Args = new[]
{
"--disable-dev-shm-usage",
"--no-sandbox"
}
});
CI must always run headless.
CLI tests are faster and more precise.
They behave more like unit/integration hybrids (intragrationtests if you like):
Run them with:
cd Tests
dotnet test
Example:
[Fact]
public async Task servers_tree_cli_workflow_test()
{
await File.WriteAllTextAsync(Path.Combine(fs.Root, "config.yaml"), "");
var (output, yaml) = await ExecuteAsync("servers", "add", "node01");
Assert.Equal("Server 'node01' added.\n", output);
Assert.Contains("name: node01", yaml);
(output, yaml) = await ExecuteAsync("systems", "add", "host01");
Assert.Equal("System 'host01' added.\n", output);
(output, yaml) = await ExecuteAsync("systems", "add", "host02");
Assert.Equal("System 'host02' added.\n", output);
(output, yaml) = await ExecuteAsync("systems", "add", "host03");
Assert.Equal("System 'host03' added.\n", output);
(output, yaml) = await ExecuteAsync(
"systems", "set", "host01",
"--runs-on", "node01"
);
Assert.Equal("System 'host01' updated.\n", output);
(output, yaml) = await ExecuteAsync("services", "add", "immich");
Assert.Equal("Service 'immich' added.\n", output);
(output, yaml) = await ExecuteAsync("servers", "tree", "node01");
Assert.Equal("""
node01
├── System: host01
│ ├── Service: immich
│ └── Service: paperless
├── System: host02
└── System: host03
""", output);
}
Test what the user observes — not how we implement it.
We value confidence over isolation purity.
A good test explains:
High-value tests matter more than coverage numbers.
If a test is flaky:
A feature is complete when:
We optimise for:
Tests are a first-class citizen of this project.