diff --git a/.github/workflows/dotnet.yml b/.github/workflows/dotnet.yml index 26b83ce3..f911baa3 100644 --- a/.github/workflows/dotnet.yml +++ b/.github/workflows/dotnet.yml @@ -32,5 +32,7 @@ jobs: run: dotnet restore - name: Build run: dotnet build --no-restore -c Release + - name: Ensure browsers are installed + run: npx playwright install chromium --with-deps - name: Test run: dotnet test -c Release --no-build -p:VSTestUseMSBuildOutput=false diff --git a/tests/LinkDotNet.Blog.IntegrationTests/LinkDotNet.Blog.IntegrationTests.csproj b/tests/LinkDotNet.Blog.IntegrationTests/LinkDotNet.Blog.IntegrationTests.csproj index a6dd4a0b..be0f8519 100644 --- a/tests/LinkDotNet.Blog.IntegrationTests/LinkDotNet.Blog.IntegrationTests.csproj +++ b/tests/LinkDotNet.Blog.IntegrationTests/LinkDotNet.Blog.IntegrationTests.csproj @@ -8,6 +8,7 @@ + diff --git a/tests/LinkDotNet.Blog.IntegrationTests/PlaywrightSmokeTests.cs b/tests/LinkDotNet.Blog.IntegrationTests/PlaywrightSmokeTests.cs new file mode 100644 index 00000000..00d25f8e --- /dev/null +++ b/tests/LinkDotNet.Blog.IntegrationTests/PlaywrightSmokeTests.cs @@ -0,0 +1,119 @@ +using System; +using System.Threading.Tasks; +using LinkDotNet.Blog.TestUtilities; +using Microsoft.Playwright; + +namespace LinkDotNet.Blog.IntegrationTests; + +[Collection("Sequential")] +public sealed class PlaywrightSmokeTests : IClassFixture, IAsyncDisposable +{ + private readonly PlaywrightWebApplicationFactory factory; + private readonly Lazy> playwrightTask; + private readonly Lazy> browserTask; + + public PlaywrightSmokeTests(PlaywrightWebApplicationFactory factory) + { + this.factory = factory; + _ = factory.CreateClient(); + playwrightTask = new Lazy>(Playwright.CreateAsync); + browserTask = new Lazy>(async () => + { + var playwright = await playwrightTask.Value; + return await playwright.Chromium.LaunchAsync(new BrowserTypeLaunchOptions { Headless = true }); + }); + } + + [Fact] + public async Task ShouldNavigateToHomePageAndShowBlogPosts() + { + var browser = await browserTask.Value; + var page = await browser.NewPageAsync(); + + await page.GotoAsync(factory.ServerAddress, new PageGotoOptions { WaitUntil = WaitUntilState.NetworkIdle }); + + var blogPostElements = await page.QuerySelectorAllAsync("article"); + blogPostElements.Count.ShouldBeGreaterThan(0); + + var title = await page.TitleAsync(); + title.ShouldBe("Integration Tests Blog"); + } + + [Fact] + public async Task ShouldNavigateToBlogPostAndShowContent() + { + var browser = await browserTask.Value; + var page = await browser.NewPageAsync(); + + await page.GotoAsync(factory.ServerAddress, new PageGotoOptions { WaitUntil = WaitUntilState.NetworkIdle }); + + var blogPostLink = await page.QuerySelectorAsync("article a[href*='/blogPost/']"); + blogPostLink.ShouldNotBeNull(); + + var href = await blogPostLink.GetAttributeAsync("href"); + href.ShouldNotBeNull(); + + await page.GotoAsync($"{factory.ServerAddress.TrimEnd('/')}{href}", new PageGotoOptions { WaitUntil = WaitUntilState.NetworkIdle }); + + var heading = await page.QuerySelectorAsync("h1"); + heading.ShouldNotBeNull(); + var headingText = await heading.TextContentAsync(); + headingText.ShouldNotBeNullOrWhiteSpace(); + } + + [Fact] + public async Task ShouldNavigateToSecondPageAndShowBlogPosts() + { + var browser = await browserTask.Value; + var page = await browser.NewPageAsync(); + + await page.GotoAsync(factory.ServerAddress, new PageGotoOptions { WaitUntil = WaitUntilState.NetworkIdle }); + + var page2Link = await page.QuerySelectorAsync("a[href='/2']"); + page2Link.ShouldNotBeNull(); + await page2Link.ClickAsync(); + await page.WaitForLoadStateAsync(LoadState.NetworkIdle); + + var currentUrl = page.Url; + currentUrl.ShouldContain("/2"); + + var blogPostElements = await page.QuerySelectorAllAsync("article"); + blogPostElements.Count.ShouldBeGreaterThan(0); + } + + [Fact] + public async Task ShouldClickOnBlogPostLinkAndNavigate() + { + var browser = await browserTask.Value; + var page = await browser.NewPageAsync(); + + await page.GotoAsync(factory.ServerAddress, new PageGotoOptions { WaitUntil = WaitUntilState.NetworkIdle }); + + var blogPostLink = await page.QuerySelectorAsync("article a[href*='/blogPost/']"); + blogPostLink.ShouldNotBeNull(); + + var expectedHref = await blogPostLink.GetAttributeAsync("href"); + expectedHref.ShouldNotBeNull(); + + await blogPostLink.ClickAsync(); + await page.WaitForLoadStateAsync(LoadState.NetworkIdle); + + var currentUrl = page.Url; + currentUrl.ShouldContain("/blogPost/"); + } + + public async ValueTask DisposeAsync() + { + if (browserTask.IsValueCreated) + { + var browser = await browserTask.Value; + await browser.DisposeAsync(); + } + + if (playwrightTask.IsValueCreated) + { + var playwright = await playwrightTask.Value; + playwright.Dispose(); + } + } +} diff --git a/tests/LinkDotNet.Blog.IntegrationTests/PlaywrightWebApplicationFactory.cs b/tests/LinkDotNet.Blog.IntegrationTests/PlaywrightWebApplicationFactory.cs new file mode 100644 index 00000000..f3701b10 --- /dev/null +++ b/tests/LinkDotNet.Blog.IntegrationTests/PlaywrightWebApplicationFactory.cs @@ -0,0 +1,63 @@ +using System; +using System.Linq; +using LinkDotNet.Blog.Infrastructure.Persistence; +using LinkDotNet.Blog.Web; +using LinkDotNet.Blog.Web.Features.DummyData; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Hosting.Server; +using Microsoft.AspNetCore.Hosting.Server.Features; +using Microsoft.AspNetCore.Mvc.Testing; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; + +namespace LinkDotNet.Blog.IntegrationTests; + +public sealed class PlaywrightWebApplicationFactory : WebApplicationFactory +{ + private IHost? host; + + public string ServerAddress => ClientOptions.BaseAddress.ToString(); + + public override IServiceProvider Services => host?.Services + ?? throw new InvalidOperationException("Create the Client first before retrieving instances from the container"); + + protected override IHost CreateHost(IHostBuilder builder) + { + var testHost = builder.Build(); + + builder = builder.ConfigureWebHost(b => + { + b.UseSetting("Logging:LogLevel:Default", "Error"); + b.UseSetting("BlogName", "Integration Tests Blog"); + b.ConfigureServices(services => + { + services.UseDummyData(new DummyDataOptions { NumberOfBlogPosts = 25 }); + }); + b.UseKestrel(); + }); + + host?.Dispose(); + host = builder.Build(); + host.Start(); + + var server = host!.Services.GetRequiredService(); + var addresses = server.Features.Get(); + + ClientOptions.BaseAddress = addresses!.Addresses + .Select(x => new Uri(x)) + .Last(); + + testHost.Start(); + return testHost; + } + + protected override void Dispose(bool disposing) + { + if (disposing) + { + host?.Dispose(); + } + + base.Dispose(disposing); + } +} diff --git a/tests/LinkDotNet.Blog.IntegrationTests/SequentialCollection.cs b/tests/LinkDotNet.Blog.IntegrationTests/SequentialCollection.cs new file mode 100644 index 00000000..05bec004 --- /dev/null +++ b/tests/LinkDotNet.Blog.IntegrationTests/SequentialCollection.cs @@ -0,0 +1,6 @@ +namespace LinkDotNet.Blog.IntegrationTests; + +[CollectionDefinition("Sequential", DisableParallelization = true)] +public class SequentialCollection +{ +}