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:
@@ -29,6 +29,9 @@ jobs:
|
||||
run: dotnet restore
|
||||
- name: Build project
|
||||
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
|
||||
run: dotnet test -e DOTNET_ENVIRONMENT=CI --no-build --verbosity normal
|
||||
- name: Upload test results
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
|
||||
# appsettings
|
||||
appsettings*.json
|
||||
!appsettings.example.json
|
||||
|
||||
# dotenv files
|
||||
.env*
|
||||
|
||||
Vendored
+2
-1
@@ -3,5 +3,6 @@
|
||||
"Antiforgery",
|
||||
"blazor",
|
||||
"Hsts"
|
||||
]
|
||||
],
|
||||
"dotnet.unitTests.runSettingsPath": "./test/Blog.Tests/.runsettings"
|
||||
}
|
||||
@@ -11,4 +11,9 @@
|
||||
<PackageReference Include="Markdig" Version="0.36.2" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<InternalsVisibleTo Include="$(AssemblyName).Tests" />
|
||||
<InternalsVisibleTo Include="DynamicProxyGenAssembly2" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
<base href="/" />
|
||||
<link rel="stylesheet" href="app.css" />
|
||||
<link rel="stylesheet" href="Blog.styles.css" />
|
||||
<link rel="shortcut icon" href="favicon.ico" type="image/x-icon">
|
||||
<HeadOutlet @rendermode="InteractiveServer" />
|
||||
</head>
|
||||
|
||||
|
||||
@@ -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%;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,10 @@
|
||||
@inherits LayoutComponentBase
|
||||
@rendermode InteractiveServer
|
||||
|
||||
<div class="container">
|
||||
<Header />
|
||||
@Body
|
||||
</div>
|
||||
|
||||
<div id="blazor-error-ui">
|
||||
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 {
|
||||
background: lightyellow;
|
||||
bottom: 0;
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
@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>
|
||||
|
||||
|
||||
@@ -24,3 +24,5 @@ app
|
||||
.AddInteractiveServerRenderMode();
|
||||
|
||||
app.Run();
|
||||
|
||||
public partial class Program {}
|
||||
@@ -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
|
||||
```
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"Urls": "https://localhost:4500;http://localhost:4501"
|
||||
}
|
||||
@@ -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
|
||||
@@ -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 {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 1.1 KiB |
@@ -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>
|
||||
@@ -1,4 +1,4 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Project Sdk="Microsoft.NET.Sdk.Razor">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
@@ -11,6 +11,12 @@
|
||||
|
||||
<ItemGroup>
|
||||
<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.Playwright.NUnit" Version="1.42.0" />
|
||||
<PackageReference Include="NUnit" Version="3.13.3" />
|
||||
@@ -19,6 +25,22 @@
|
||||
<PackageReference Include="coverlet.collector" Version="6.0.0" />
|
||||
</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>
|
||||
<ProjectReference Include="..\..\src\Blog\Blog.csproj" />
|
||||
</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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -1 +1,10 @@
|
||||
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;
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
namespace Blog.Tests;
|
||||
|
||||
public class Tests
|
||||
{
|
||||
[SetUp]
|
||||
public void Setup()
|
||||
{
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Test1()
|
||||
{
|
||||
Assert.Pass();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
@using Bunit
|
||||
@using Blog.Components
|
||||
@using FluentAssertions
|
||||
Reference in New Issue
Block a user