diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index 09ce7e5..1cc6936 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -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 diff --git a/.gitignore b/.gitignore index e2aa736..65b9f93 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,7 @@ # appsettings appsettings*.json +!appsettings.example.json # dotenv files .env* diff --git a/.vscode/settings.json b/.vscode/settings.json index 7301fa2..f2aafb8 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -3,5 +3,6 @@ "Antiforgery", "blazor", "Hsts" - ] + ], + "dotnet.unitTests.runSettingsPath": "./test/Blog.Tests/.runsettings" } \ No newline at end of file diff --git a/src/Blog/Blog.csproj b/src/Blog/Blog.csproj index 420ef61..50fdd4f 100644 --- a/src/Blog/Blog.csproj +++ b/src/Blog/Blog.csproj @@ -11,4 +11,9 @@ + + + + + diff --git a/src/Blog/Components/App.razor b/src/Blog/Components/App.razor index 1f3f9ab..a4ce524 100644 --- a/src/Blog/Components/App.razor +++ b/src/Blog/Components/App.razor @@ -7,6 +7,7 @@ + diff --git a/src/Blog/Components/Layout/Header.razor b/src/Blog/Components/Layout/Header.razor new file mode 100644 index 0000000..485121b --- /dev/null +++ b/src/Blog/Components/Layout/Header.razor @@ -0,0 +1,13 @@ +@namespace Blog.Components + +
+ +
+ by + Stevan Freeborn +
+
diff --git a/src/Blog/Components/Layout/Header.razor.css b/src/Blog/Components/Layout/Header.razor.css new file mode 100644 index 0000000..bf913a7 --- /dev/null +++ b/src/Blog/Components/Layout/Header.razor.css @@ -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%; + } + } +} \ No newline at end of file diff --git a/src/Blog/Components/Layout/MainLayout.razor b/src/Blog/Components/Layout/MainLayout.razor index 0d3d3af..6af4224 100644 --- a/src/Blog/Components/Layout/MainLayout.razor +++ b/src/Blog/Components/Layout/MainLayout.razor @@ -1,6 +1,10 @@ @inherits LayoutComponentBase +@rendermode InteractiveServer -@Body +
+
+ @Body +
An unhandled error has occurred. diff --git a/src/Blog/Components/Layout/MainLayout.razor.css b/src/Blog/Components/Layout/MainLayout.razor.css index 21a8d24..0431dce 100644 --- a/src/Blog/Components/Layout/MainLayout.razor.css +++ b/src/Blog/Components/Layout/MainLayout.razor.css @@ -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; diff --git a/src/Blog/Components/Pages/Home.razor b/src/Blog/Components/Pages/Home.razor index 9001e0b..1f8286f 100644 --- a/src/Blog/Components/Pages/Home.razor +++ b/src/Blog/Components/Pages/Home.razor @@ -1,6 +1,10 @@ @page "/" -Home +journal - A blog by Stevan Freeborn + + + +

Hello, world!

diff --git a/src/Blog/Program.cs b/src/Blog/Program.cs index a2d5151..bc42976 100644 --- a/src/Blog/Program.cs +++ b/src/Blog/Program.cs @@ -23,4 +23,6 @@ app .MapRazorComponents() .AddInteractiveServerRenderMode(); -app.Run(); \ No newline at end of file +app.Run(); + +public partial class Program {} \ No newline at end of file diff --git a/src/Blog/README.md b/src/Blog/README.md new file mode 100644 index 0000000..63851e3 --- /dev/null +++ b/src/Blog/README.md @@ -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 +dotnet dev-certs https --trust +``` + +```powershell +dotnet dev-certs https -ep $env:USERPROFILE\.aspnet\https\Blog.pfx -p --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 +``` diff --git a/src/Blog/appsettings.example.json b/src/Blog/appsettings.example.json new file mode 100644 index 0000000..acae0aa --- /dev/null +++ b/src/Blog/appsettings.example.json @@ -0,0 +1,3 @@ +{ + "Urls": "https://localhost:4500;http://localhost:4501" +} \ No newline at end of file diff --git a/src/Blog/example.env.development b/src/Blog/example.env.development new file mode 100644 index 0000000..fb80ac5 --- /dev/null +++ b/src/Blog/example.env.development @@ -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 \ No newline at end of file diff --git a/src/Blog/wwwroot/app.css b/src/Blog/wwwroot/app.css index 795e36b..16a6d49 100644 --- a/src/Blog/wwwroot/app.css +++ b/src/Blog/wwwroot/app.css @@ -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; } diff --git a/src/Blog/wwwroot/favicon.ico b/src/Blog/wwwroot/favicon.ico new file mode 100644 index 0000000..ec4c0e3 Binary files /dev/null and b/src/Blog/wwwroot/favicon.ico differ diff --git a/src/Blog/wwwroot/favicon.png b/src/Blog/wwwroot/favicon.png deleted file mode 100644 index 8422b59..0000000 Binary files a/src/Blog/wwwroot/favicon.png and /dev/null differ diff --git a/test/Blog.Tests/.runsettings b/test/Blog.Tests/.runsettings new file mode 100644 index 0000000..d2d9961 --- /dev/null +++ b/test/Blog.Tests/.runsettings @@ -0,0 +1,10 @@ + + + + chromium + + false + msedge + + + \ No newline at end of file diff --git a/test/Blog.Tests/Blog.Tests.csproj b/test/Blog.Tests/Blog.Tests.csproj index 636746e..61db240 100644 --- a/test/Blog.Tests/Blog.Tests.csproj +++ b/test/Blog.Tests/Blog.Tests.csproj @@ -1,4 +1,4 @@ - + net8.0 @@ -11,6 +11,12 @@ + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + @@ -19,6 +25,22 @@ + + true + ./TestResults/coverage/ + cobertura + [Blog]* + **/Blog/Program.cs + + + + + + + + + + diff --git a/test/Blog.Tests/EndToEnd/BlogHostFactory.cs b/test/Blog.Tests/EndToEnd/BlogHostFactory.cs new file mode 100644 index 0000000..c138951 --- /dev/null +++ b/test/Blog.Tests/EndToEnd/BlogHostFactory.cs @@ -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 : WebApplicationFactory 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(); + var addresses = server.Features.GetRequiredFeature(); + + ClientOptions.BaseAddress = addresses.Addresses + .Select(x => new Uri(x)) + .Last(); + + testHost.Start(); + return testHost; + } +} \ No newline at end of file diff --git a/test/Blog.Tests/EndToEnd/BlogTest.cs b/test/Blog.Tests/EndToEnd/BlogTest.cs new file mode 100644 index 0000000..27ec44a --- /dev/null +++ b/test/Blog.Tests/EndToEnd/BlogTest.cs @@ -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 _factory = new(); + + public override BrowserNewContextOptions ContextOptions() + { + var options = base.ContextOptions(); + options.BaseURL = _factory.ServerAddress; + return options; + } +} \ No newline at end of file diff --git a/test/Blog.Tests/EndToEnd/HomeTests.cs b/test/Blog.Tests/EndToEnd/HomeTests.cs new file mode 100644 index 0000000..e0ba4e5 --- /dev/null +++ b/test/Blog.Tests/EndToEnd/HomeTests.cs @@ -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(); + } +} \ No newline at end of file diff --git a/test/Blog.Tests/GlobalUsings.cs b/test/Blog.Tests/GlobalUsings.cs index cefced4..7892f58 100644 --- a/test/Blog.Tests/GlobalUsings.cs +++ b/test/Blog.Tests/GlobalUsings.cs @@ -1 +1,10 @@ -global using NUnit.Framework; \ No newline at end of file +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; \ No newline at end of file diff --git a/test/Blog.Tests/Unit/Header.spec.razor b/test/Blog.Tests/Unit/Header.spec.razor new file mode 100644 index 0000000..7963c28 --- /dev/null +++ b/test/Blog.Tests/Unit/Header.spec.razor @@ -0,0 +1,32 @@ +@inherits Bunit.TestContext + +@code +{ + [Test] + public void Header_WhenRendered_ItShouldContainSiteTitle() + { + var cut = Render(@
); + + var heading = cut.Find("h1"); + + heading.MarkupMatches(@

journal

); + } + + [Test] + public void Header_WhenRendered_ItShouldContainAuthorInfo() + { + var cut = Render(@
); + + var author = cut.Find(".author-container > span"); + + author.MarkupMatches(@by); + } + + [Test] + public void Header_WhenRendered_ItShouldContainAuthorImage() + { + var cut = Render(@
); + var image = cut.Find("img[alt='Stevan Freeborn']"); + image.Should().NotBeNull(); + } +} diff --git a/test/Blog.Tests/UnitTest1.cs b/test/Blog.Tests/UnitTest1.cs deleted file mode 100644 index cd61a57..0000000 --- a/test/Blog.Tests/UnitTest1.cs +++ /dev/null @@ -1,15 +0,0 @@ -namespace Blog.Tests; - -public class Tests -{ - [SetUp] - public void Setup() - { - } - - [Test] - public void Test1() - { - Assert.Pass(); - } -} \ No newline at end of file diff --git a/test/Blog.Tests/_Imports.razor b/test/Blog.Tests/_Imports.razor new file mode 100644 index 0000000..99e4f3d --- /dev/null +++ b/test/Blog.Tests/_Imports.razor @@ -0,0 +1,3 @@ +@using Bunit +@using Blog.Components +@using FluentAssertions \ No newline at end of file