feat: add site header (#2)

* tests: setup initial infra for end to end tests

* tests: rework end to end host

* tests: remove need for bang operator

* tests: correct header component tests

* docs: add README.md to blog directory with instructions on setting up local dev environment with and without docker

* docs: update non-developer run command

* feat: finish layout of header

* feat: add favicon

* tests: setup test coverage

* chore: update workflow with missing playwright step

* chore: add install command
This commit is contained in:
Stevan Freeborn
2024-04-08 23:34:32 -05:00
committed by GitHub
parent 5ccb9848ed
commit d25e5d4944
26 changed files with 387 additions and 21 deletions
+3
View File
@@ -29,6 +29,9 @@ jobs:
run: dotnet restore run: dotnet restore
- name: Build project - name: Build project
run: dotnet build --no-restore run: dotnet build --no-restore
- name: Install playwright dependencies
shell: pwsh
run: ./test/Blog.Tests/bin/Debug/net8.0/playwright.ps1 install
- name: Test project - name: Test project
run: dotnet test -e DOTNET_ENVIRONMENT=CI --no-build --verbosity normal run: dotnet test -e DOTNET_ENVIRONMENT=CI --no-build --verbosity normal
- name: Upload test results - name: Upload test results
+1
View File
@@ -5,6 +5,7 @@
# appsettings # appsettings
appsettings*.json appsettings*.json
!appsettings.example.json
# dotenv files # dotenv files
.env* .env*
+2 -1
View File
@@ -3,5 +3,6 @@
"Antiforgery", "Antiforgery",
"blazor", "blazor",
"Hsts" "Hsts"
] ],
"dotnet.unitTests.runSettingsPath": "./test/Blog.Tests/.runsettings"
} }
+5
View File
@@ -11,4 +11,9 @@
<PackageReference Include="Markdig" Version="0.36.2" /> <PackageReference Include="Markdig" Version="0.36.2" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<InternalsVisibleTo Include="$(AssemblyName).Tests" />
<InternalsVisibleTo Include="DynamicProxyGenAssembly2" />
</ItemGroup>
</Project> </Project>
+1
View File
@@ -7,6 +7,7 @@
<base href="/" /> <base href="/" />
<link rel="stylesheet" href="app.css" /> <link rel="stylesheet" href="app.css" />
<link rel="stylesheet" href="Blog.styles.css" /> <link rel="stylesheet" href="Blog.styles.css" />
<link rel="shortcut icon" href="favicon.ico" type="image/x-icon">
<HeadOutlet @rendermode="InteractiveServer" /> <HeadOutlet @rendermode="InteractiveServer" />
</head> </head>
+13
View File
@@ -0,0 +1,13 @@
@namespace Blog.Components
<header class="container">
<div class="title-container">
<a href="/">
<h1>journal</h1>
</a>
</div>
<div class="author-container">
<span>by</span>
<img src="https://github.com/StevanFreeborn.png" alt="Stevan Freeborn" />
</div>
</header>
@@ -0,0 +1,24 @@
.container {
display: flex;
justify-content: space-between;
align-items: center;
width: 100%;
gap: 1rem;
& .title-container h1 {
font-size: 1.5rem;
font-weight: 700;
}
& .author-container {
display: flex;
align-items: center;
gap: 0.5rem;
& img {
width: 2rem;
height: 2rem;
border-radius: 50%;
}
}
}
+5 -1
View File
@@ -1,6 +1,10 @@
@inherits LayoutComponentBase @inherits LayoutComponentBase
@rendermode InteractiveServer
@Body <div class="container">
<Header />
@Body
</div>
<div id="blazor-error-ui"> <div id="blazor-error-ui">
An unhandled error has occurred. An unhandled error has occurred.
@@ -1,3 +1,13 @@
.container {
height: 100%;
display: flex;
flex-direction: column;
align-items: center;
max-width: 42rem;
padding: 1.25rem;
width: 100%;
}
#blazor-error-ui { #blazor-error-ui {
background: lightyellow; background: lightyellow;
bottom: 0; bottom: 0;
+5 -1
View File
@@ -1,6 +1,10 @@
@page "/" @page "/"
<PageTitle>Home</PageTitle> <PageTitle>journal - A blog by Stevan Freeborn</PageTitle>
<HeadContent>
<meta name="description" content="A blog by Stevan Freeborn" />
</HeadContent>
<h1>Hello, world!</h1> <h1>Hello, world!</h1>
+2
View File
@@ -24,3 +24,5 @@ app
.AddInteractiveServerRenderMode(); .AddInteractiveServerRenderMode();
app.Run(); app.Run();
public partial class Program {}
+48
View File
@@ -0,0 +1,48 @@
# Blog
## Setup for development
### Clone the repository
```sh
git clone https://github.com/StevanFreeborn/blog.stevanfreeborn.com.git
```
### Generate development SSL certificates
```sh
dotnet dev-certs https -ep ${HOME}/.aspnet/https/Blog.pfx -p <password>
dotnet dev-certs https --trust
```
```powershell
dotnet dev-certs https -ep $env:USERPROFILE\.aspnet\https\Blog.pfx -p <password> --trust
```
### Developing with Docker
#### Create `.env.development` file
```sh
cp src/Blog/example.env.development src/Blog/.env.development
```
#### Build and run the Docker container
```sh
docker-compose -f docker-compose.dev.yml up --build
```
### Developing without Docker
#### Create `appsettings.Development.json` file
```sh
cp src/Blog/appsettings.example.json src/Blog/appsettings.Development.json
```
#### Run the application
```sh
dotnet run --project src/Blog/Blog.csproj
```
+3
View File
@@ -0,0 +1,3 @@
{
"Urls": "https://localhost:4500;http://localhost:4501"
}
+15
View File
@@ -0,0 +1,15 @@
URLS=https://0.0.0.0:4500;http://0.0.0.0:4501
# Path needs to match the path where the cert file
# is mounted in the container
ASPNETCORE_Kestrel__Certificates__Default__Path=
# Password needs to match the password for the cert file
# you generated
ASPNETCORE_Kestrel__Certificates__Default__Password=
# This is the hostname that the dotnet watch process will use to connect to the browser
# The first url in the list is the port that the browser will attempt to connect to
# The second url in the list is the port that the dotnet watch process will be running
# on in the container. This is necessary for hot reload to work.
DOTNET_WATCH_AUTO_RELOAD_WS_HOSTNAME=localhost:1234;http://0.0.0.0:5555;http://127.0.0.1
+60
View File
@@ -1,3 +1,63 @@
@import url('https://fonts.googleapis.com/css2?family=Merriweather:ital,wght@0,300;0,400;0,700;0,900;1,300;1,400;1,700;1,900&display=swap');
/* Resets */
* {
margin: 0;
padding: 0;
}
*,
*::before,
*::after {
box-sizing: border-box;
}
button {
background: none;
border: none;
outline: none;
cursor: pointer;
}
a {
text-decoration: none;
color: inherit;
}
ul {
list-style-type: none;
}
img {
max-width: 100%;
display: block;
}
table {
width: 100%;
border-collapse: collapse;
}
/* Site */
html,
body {
height: 100%;
}
body {
display: flex;
flex-direction: column;
align-items: center;
font-family: 'Merriweather', serif;
font-size: 16px;
line-height: 1.5;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
h1:focus { h1:focus {
outline: none; outline: none;
} }
Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

+10
View File
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<RunSettings>
<Playwright>
<BrowserName>chromium</BrowserName>
<LaunchOptions>
<Headless>false</Headless>
<Channel>msedge</Channel>
</LaunchOptions>
</Playwright>
</RunSettings>
+23 -1
View File
@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk.Razor">
<PropertyGroup> <PropertyGroup>
<TargetFramework>net8.0</TargetFramework> <TargetFramework>net8.0</TargetFramework>
@@ -11,6 +11,12 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="bunit" Version="1.27.17" /> <PackageReference Include="bunit" Version="1.27.17" />
<PackageReference Include="coverlet.msbuild" Version="6.0.2">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="FluentAssertions" Version="6.12.0" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="8.0.3" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.6.0" /> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.6.0" />
<PackageReference Include="Microsoft.Playwright.NUnit" Version="1.42.0" /> <PackageReference Include="Microsoft.Playwright.NUnit" Version="1.42.0" />
<PackageReference Include="NUnit" Version="3.13.3" /> <PackageReference Include="NUnit" Version="3.13.3" />
@@ -19,6 +25,22 @@
<PackageReference Include="coverlet.collector" Version="6.0.0" /> <PackageReference Include="coverlet.collector" Version="6.0.0" />
</ItemGroup> </ItemGroup>
<PropertyGroup>
<CollectCoverage>true</CollectCoverage>
<CoverletOutput>./TestResults/coverage/</CoverletOutput>
<CoverletOutputFormat>cobertura</CoverletOutputFormat>
<Include>[Blog]*</Include>
<ExcludeByFile>**/Blog/Program.cs</ExcludeByFile>
</PropertyGroup>
<Target Name="GenerateHtmlCoverageReport" AfterTargets="GenerateCoverageResultAfterTest">
<Exec Command="reportgenerator -reports:./TestResults/coverage/*.xml -targetdir:./TestResults/coverage/report/ -reporttypes:Html_Dark" />
</Target>
<Target Name="OpenTestReport" AfterTargets="GenerateHtmlCoverageReport" Condition="'$(DOTNET_ENVIRONMENT)' != 'CI'">
<Exec Command="start ./TestResults/coverage/report/index.html" />
</Target>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\..\src\Blog\Blog.csproj" /> <ProjectReference Include="..\..\src\Blog\Blog.csproj" />
</ItemGroup> </ItemGroup>
@@ -0,0 +1,62 @@
using Microsoft.AspNetCore.Hosting.Server.Features;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.Extensions.Logging;
namespace Blog.Tests.EndToEnd;
public class BlogHostFactory<TProgram> : WebApplicationFactory<TProgram> where TProgram : class
{
private IHost? _host;
private void EnsureServer()
{
if (_host is null)
{
using var _ = CreateDefaultClient();
}
}
protected override void Dispose(bool disposing)
{
_host?.Dispose();
}
public string ServerAddress
{
get
{
EnsureServer();
return ClientOptions.BaseAddress.ToString();
}
}
protected override IHost CreateHost(IHostBuilder builder)
{
var testHost = builder
.ConfigureWebHost(
webHostBuilder => webHostBuilder.ConfigureLogging(
config => config.ClearProviders()
)
)
.Build();
builder.ConfigureWebHost(
webHostBuilder => webHostBuilder.UseKestrel(
o => o.Listen(IPAddress.Loopback, 0)
)
);
_host = builder.Build();
_host.Start();
var server = _host.Services.GetRequiredService<IServer>();
var addresses = server.Features.GetRequiredFeature<IServerAddressesFeature>();
ClientOptions.BaseAddress = addresses.Addresses
.Select(x => new Uri(x))
.Last();
testHost.Start();
return testHost;
}
}
+17
View File
@@ -0,0 +1,17 @@
using Microsoft.AspNetCore.Hosting.Server.Features;
using Microsoft.AspNetCore.Http.Features;
namespace Blog.Tests.EndToEnd;
[TestFixture]
public class BlogTest : PageTest
{
private readonly BlogHostFactory<Program> _factory = new();
public override BrowserNewContextOptions ContextOptions()
{
var options = base.ContextOptions();
options.BaseURL = _factory.ServerAddress;
return options;
}
}
+32
View File
@@ -0,0 +1,32 @@
namespace Blog.Tests.EndToEnd;
[TestFixture]
public class HomeTests : BlogTest
{
[Test]
public async Task Home_WhenNavigatedTo_ItShouldDisplayCorrectPageTitle()
{
await Page.GotoAsync("/");
var title = await Page.TitleAsync();
title.Should().Be("journal - A blog by Stevan Freeborn");
}
[Test]
public async Task Home_WhenNavigatedTo_ItShouldCorrectSiteTitle()
{
await Page.GotoAsync("/");
var message = Page.GetByRole(AriaRole.Heading, new () { Name = "journal" });
await Expect(message).ToBeVisibleAsync();
}
[Test]
public async Task Home_WhenNavigatedTo_ItShouldDisplayWrittenByAttribution()
{
await Page.GotoAsync("/");
var message = Page.GetByText("by");
var image = Page.GetByAltText("Stevan Freeborn");
await Expect(message).ToBeVisibleAsync();
await Expect(image).ToBeVisibleAsync();
}
}
+9
View File
@@ -1 +1,10 @@
global using NUnit.Framework; global using NUnit.Framework;
global using Microsoft.AspNetCore.Mvc.Testing;
global using Microsoft.Extensions.Hosting;
global using Microsoft.AspNetCore.Hosting;
global using Microsoft.Playwright.NUnit;
global using Microsoft.Playwright;
global using FluentAssertions;
global using Microsoft.AspNetCore.Hosting.Server;
global using Microsoft.Extensions.DependencyInjection;
global using System.Net;
+32
View File
@@ -0,0 +1,32 @@
@inherits Bunit.TestContext
@code
{
[Test]
public void Header_WhenRendered_ItShouldContainSiteTitle()
{
var cut = Render(@<Header />);
var heading = cut.Find("h1");
heading.MarkupMatches(@<h1>journal</h1>);
}
[Test]
public void Header_WhenRendered_ItShouldContainAuthorInfo()
{
var cut = Render(@<Header />);
var author = cut.Find(".author-container > span");
author.MarkupMatches(@<span>by</span>);
}
[Test]
public void Header_WhenRendered_ItShouldContainAuthorImage()
{
var cut = Render(@<Header />);
var image = cut.Find("img[alt='Stevan Freeborn']");
image.Should().NotBeNull();
}
}
-15
View File
@@ -1,15 +0,0 @@
namespace Blog.Tests;
public class Tests
{
[SetUp]
public void Setup()
{
}
[Test]
public void Test1()
{
Assert.Pass();
}
}
+3
View File
@@ -0,0 +1,3 @@
@using Bunit
@using Blog.Components
@using FluentAssertions