Skip to content

Commit

Permalink
Fix tests and other clean up. (#12)
Browse files Browse the repository at this point in the history
* Fix chromium issue when launching on a 'blocked' port.

This fixes failures when the host decides to run on e.g. 5060 or 5061.
Also exposes Port as a public property, and inits Port and Uri on demand.

* Add EnsureServerStartedAsync and use IHostApplicationLifetime to ensure server is running before returning Playwright browser/context/page etc.
* Make it possible to get both hosts from CompositeHost via Services
* Make it possible to enumerate the IServer instances from the hosts.
* Add a simple test to check the requested url matches playwright and the server's listening addresses.
  • Loading branch information
CZEMacLeod authored Aug 19, 2023
1 parent 6f19802 commit f93ff8c
Show file tree
Hide file tree
Showing 4 changed files with 105 additions and 29 deletions.
9 changes: 8 additions & 1 deletion build/azure-pipelines.yml
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,14 @@ trigger:
pr: none

pool:
vmImage: windows-latest
# vmImage: windows-latest
name: 'C3D Windows'
demands:
- msbuild
- MSBuild_17.0
- visualstudio
- Agent.OS -equals Windows_NT
- VisualStudio.BuildTools.Version -gtVersion 17.0

variables:
- group: Github-Packages
Expand Down
2 changes: 1 addition & 1 deletion samples/Sample.WebApp/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,9 @@ public static void Main(string[] args)
{
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
app.UseHsts();
app.UseHttpsRedirection();
}

app.UseHttpsRedirection();
app.UseStaticFiles();

app.UseRouting();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
using C3D.Extensions.Playwright.AspNetCore.Utilities;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Hosting.Server;
using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Microsoft.Playwright;
using System.Diagnostics.CodeAnalysis;

Expand All @@ -14,12 +15,18 @@ public class PlaywrightWebApplicationFactory<TProgram> : WebApplicationFactory<T
{
private IPlaywright? playwright;
private IBrowser? browser;
private IHostApplicationLifetime? lifetime;
private TaskCompletionSource hostStarted = new();

private string? uri;
private int? port;
// We randomize the server port so we ensure that any hard coded Uri's fail in the tests.
// This also allows multiple servers to run during the tests.
public int Port => port ??= 5000 + Interlocked.Add(ref nextPort, 10 + System.Random.Shared.Next(10));
public string Uri => uri ??= $"http://localhost:{Port}";

private static int nextPort = 0;

public string? Uri => uri;

#region "Overridable Properties"
// Properties in this region can be overridden in a derived type and used as a fixture
// If you create multiple derived fixtures, and derived tests injecting each one into a base test class
Expand All @@ -35,34 +42,32 @@ public class PlaywrightWebApplicationFactory<TProgram> : WebApplicationFactory<T
private BrowserNewPageOptions? pageOptions;
protected virtual BrowserNewPageOptions PageOptions => pageOptions ??= new()
{
BaseURL = uri
BaseURL = Uri
};

private BrowserNewContextOptions? contextOptions;
protected virtual BrowserNewContextOptions ContextOptions => contextOptions ??= new()
{
BaseURL = uri
BaseURL = Uri
};

public virtual LogLevel MinimumLogLevel => LogLevel.Trace;
#endregion

protected virtual IBrowserType GetBrowser(PlaywrightBrowserType? browserType=null) => (browserType ?? BrowserType) switch
protected virtual IBrowserType GetBrowser(PlaywrightBrowserType? browserType = null) => (browserType ?? BrowserType) switch
{
PlaywrightBrowserType.Chromium => playwright?.Chromium,
PlaywrightBrowserType.Firefox => playwright?.Firefox,
PlaywrightBrowserType.Webkit => playwright?.Webkit,
_ => throw new ArgumentOutOfRangeException(nameof(browserType))
} ?? throw new InvalidOperationException("Could not get browser type");


protected virtual ILoggingBuilder ConfigureLogging(ILoggingBuilder builder)
{
builder.SetMinimumLevel(MinimumLogLevel);
return builder;
}

[MemberNotNull(nameof(uri))]
protected override IHost CreateHost(IHostBuilder builder)
{
if (Environment is not null)
Expand All @@ -71,33 +76,38 @@ protected override IHost CreateHost(IHostBuilder builder)
}
builder.ConfigureLogging(logging => ConfigureLogging(logging));

// We randomize the server port so we ensure that any hard coded Uri's fail in the tests.
// This also allows multiple servers to run during the tests.
var port = 5000 + Interlocked.Add(ref nextPort, 10 + System.Random.Shared.Next(10));
uri = $"http://localhost:{port}";

// We the testHost, which can be used with HttpClient with a custom transport
// It is assumed that the return of CreateHost is a host based on the TestHost Server.
var testHost = base.CreateHost(builder);

// Now we reconfigure the builder to use kestrel so we have an http listener that can be used by playwright
builder.ConfigureWebHost(webHostBuilder => webHostBuilder.UseKestrel(options =>
{
options.ListenLocalhost(port);
options.ListenLocalhost(Port);
}));
var host = base.CreateHost(builder);

lifetime = host.Services.GetRequiredService<IHostApplicationLifetime>();
lifetime.ApplicationStarted.Register(() => hostStarted.SetResult());

return new CompositeHost(testHost, host);
}

public async Task<IBrowser> GetDefaultPlaywrightBrowserAsync()
{
_ = Server; // Ensure Server is initialized
await InitializeAsync(); // Ensure Playwright is initialized
await EnsureServerStartedAsync(); // Ensure Server is initialized
await InitializeAsync(); // Ensure Playwright is initialized

return browser;
}

private async Task EnsureServerStartedAsync()
{
if (hostStarted.Task.IsCompleted) return;
_ = Server; // Ensure Server is initialized
await hostStarted.Task.ConfigureAwait(false);
}

/// <summary>
/// Creates a new Browser instance.
/// </summary>
Expand All @@ -108,12 +118,12 @@ public async Task<IBrowser> GetDefaultPlaywrightBrowserAsync()
public async Task<IBrowser> CreateCustomPlaywrightBrowserAsync(PlaywrightBrowserType? browserType = null,
Action<BrowserTypeLaunchOptions>? browserOptions = null)
{
_ = Server; // Ensure Server is initialized
await InitializeAsync(); // Ensure Playwright is initialized
await EnsureServerStartedAsync(); // Ensure Server is initialized
await InitializeAsync(); // Ensure Playwright is initialized
EnsureBrowserInstalled(browserType);
var launchOptions = new BrowserTypeLaunchOptions(LaunchOptions);
browserOptions?.Invoke(launchOptions);
return await GetBrowser(browserType).LaunchAsync(launchOptions);
return await LaunchAsync(GetBrowser(browserType), launchOptions);
}

private void EnsureBrowserInstalled(PlaywrightBrowserType? browserType)
Expand All @@ -129,8 +139,8 @@ private void EnsureBrowserInstalled(PlaywrightBrowserType? browserType)
/// <remarks>The consumer should close the page when they are finished with it.</remarks>
public async Task<IPage> CreatePlaywrightPageAsync(Action<BrowserNewPageOptions>? pageOptions = null)
{
_ = Server; // Ensure Server is initialized
await InitializeAsync(); // Ensure Playwright is initialized
await EnsureServerStartedAsync(); // Ensure Server is initialized
await InitializeAsync(); // Ensure Playwright is initialized

var options = new BrowserNewPageOptions(PageOptions);
pageOptions?.Invoke(options);
Expand Down Expand Up @@ -164,8 +174,8 @@ public async Task<PlaywrightBrowserPage> CreateCustomPlaywrightBrowserPageAsync(
/// <remarks>The consumer is responsible for the correct closure and disposal of the context and any pages creates in it.</remarks>
public async Task<IBrowserContext> CreatePlaywrightContextAsync(Action<BrowserNewContextOptions>? contextOptions = null)
{
_ = Server; // Ensure Server is initialized
await InitializeAsync(); // Ensure Playwright is initialized
await EnsureServerStartedAsync(); // Ensure Server is initialized
await InitializeAsync(); // Ensure Playwright is initialized

var options = new BrowserNewContextOptions(ContextOptions);
contextOptions?.Invoke(options);
Expand Down Expand Up @@ -195,10 +205,27 @@ public virtual async Task InitializeAsync()
PlaywrightUtilities.InstallPlaywright(BrowserType);
#pragma warning disable CS8774 // Member must have a non-null value when exiting.
playwright ??= (await Microsoft.Playwright.Playwright.CreateAsync()) ?? throw new InvalidOperationException();
browser ??= (await GetBrowser().LaunchAsync(LaunchOptions)) ?? throw new InvalidOperationException();
browser ??= (await LaunchAsync(GetBrowser(), LaunchOptions)) ?? throw new InvalidOperationException();
#pragma warning restore CS8774 // Member must have a non-null value when exiting.
}

private async Task<IBrowser> LaunchAsync(IBrowserType browser, BrowserTypeLaunchOptions options)
{
var args = (options.Args ?? Array.Empty<string>()).ToList();
var ports = args.SingleOrDefault(arg => arg.StartsWith("--explicitly-allowed-ports"));
if (ports is null && browser.Name == "chromium")
{
// In chromium some ports are blocked, such as sip ports 5060/5061.
// As these may be picked, we explicitly allow whatever port this host is running on.
args.Add($"--explicitly-allowed-ports={Port}");
options = new(options)
{
Args = args
};
}
return await browser.LaunchAsync(options);
}

protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
Expand Down Expand Up @@ -228,7 +255,7 @@ public async override ValueTask DisposeAsync()

// CompositeHost is based on https://github.com/xaviersolau/DevArticles/blob/e2e_test_blazor_with_playwright/MyBlazorApp/MyAppTests/WebTestingHostFactory.cs
// Relay the call to both test host and kestrel host.
internal sealed class CompositeHost : IHost
internal sealed class CompositeHost : IHost, IServiceProvider
{
private readonly IHost testHost;
private readonly IHost kestrelHost;
Expand All @@ -237,13 +264,24 @@ public CompositeHost(IHost testHost, IHost kestrelHost)
this.testHost = testHost;
this.kestrelHost = kestrelHost;
}
public IServiceProvider Services => testHost.Services;
public IServiceProvider Services => this;
public void Dispose()
{
testHost.Dispose();
kestrelHost.Dispose();
GC.SuppressFinalize(this);
}

public object? GetService(Type serviceType)
{
if (serviceType == typeof(IEnumerable<IHost>)) return new IHost[] { testHost, kestrelHost };
if (serviceType == typeof(IEnumerable<IServer>)) return new IServer[] {
testHost.Services.GetRequiredService<IServer>(),
kestrelHost.Services.GetRequiredService<IServer>()
};
return testHost.Services.GetService(serviceType);
}

public async Task StartAsync(CancellationToken cancellationToken = default)
{
await testHost.StartAsync(cancellationToken);
Expand Down
33 changes: 32 additions & 1 deletion test/Sample.WebApp.Tests/SimpleTests.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
using C3D.Extensions.Playwright.AspNetCore.Xunit;
using Microsoft.AspNetCore.Hosting.Server;
using Microsoft.AspNetCore.Hosting.Server.Features;
using Microsoft.Extensions.DependencyInjection;
using System.Runtime.CompilerServices;
using Xunit.Abstractions;

Expand Down Expand Up @@ -28,4 +31,32 @@ public async Task CheckHomePageTitle()
await page.CloseAsync();
}

}
[Fact]
public async Task CheckServerUrls()
{
WriteFunctionName();

var page = await webApplication.CreatePlaywrightPageAsync();
await page.GotoAsync("/");

var uri = page.Url;
var webAppUri = new Uri(webApplication.Uri).ToString(); // Note, this will add the trailing / to the Uri

var addresses = webApplication.Services.GetServices<IServer>()
.SelectMany(server => server.Features.Get<IServerAddressesFeature>()?.Addresses ?? Enumerable.Empty<string>());

outputHelper.WriteLine("Playwright Url: {0}", uri);
outputHelper.WriteLine("Web Application Url: {0}", webApplication.Uri);
foreach (var address in addresses)
{
outputHelper.WriteLine("Server Address: {0}", address);
}


Assert.Equal(webAppUri, uri); // Check playwright goes to expected page
Assert.Collection(addresses,
address => Assert.Equal(address,webApplication.Uri)
); // Check the server listens only on the expected address

}
}

0 comments on commit f93ff8c

Please sign in to comment.