Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 38 additions & 0 deletions TUnit.Playwright/BrowserFixture.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
using Microsoft.Playwright;
using TUnit.Core;
using TUnit.Core.Interfaces;

namespace TUnit.Playwright;

/// <summary>
/// The injected <see cref="PlaywrightFixture"/> is hardcoded to <see cref="SharedType.PerTestSession"/>.
/// Authoring a new fixture class is the way to change that scope — attribute arguments on
/// inherited <c>init</c> properties cannot be overridden.
/// </summary>
public class BrowserFixture : IAsyncInitializer, IAsyncDisposable
{
[ClassDataSource<PlaywrightFixture>(Shared = SharedType.PerTestSession)]
public required PlaywrightFixture PlaywrightFixture { get; init; }

public IBrowser Browser { get; private set; } = null!;

public virtual string BrowserName => Microsoft.Playwright.BrowserType.Chromium;

protected virtual BrowserTypeLaunchOptions GetLaunchOptions() => new();

public virtual async Task InitializeAsync()
{
var browserType = PlaywrightFixture.Playwright[BrowserName]
?? throw new InvalidOperationException($"Unknown BrowserName '{BrowserName}'.");

Browser = await PlaywrightServiceConnector.LaunchAsync(browserType, GetLaunchOptions()).ConfigureAwait(false);
}

public virtual async ValueTask DisposeAsync()
{
if (Browser is not null)
{
await Browser.CloseAsync().ConfigureAwait(false);
}
}
}
34 changes: 2 additions & 32 deletions TUnit.Playwright/BrowserService.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
using System.Globalization;
using Microsoft.Playwright;

namespace TUnit.Playwright;
Expand All @@ -17,37 +16,8 @@ public static Task<BrowserService> Register(
IBrowserType browserType,
BrowserTypeLaunchOptions options)
{
return test.RegisterService("Browser", async () => new BrowserService(await CreateBrowser(browserType, options).ConfigureAwait(false)));
}

private static async Task<IBrowser> CreateBrowser(
IBrowserType browserType,
BrowserTypeLaunchOptions options)
{
var accessToken = Environment.GetEnvironmentVariable("PLAYWRIGHT_SERVICE_ACCESS_TOKEN");
var serviceUrl = Environment.GetEnvironmentVariable("PLAYWRIGHT_SERVICE_URL");

if (string.IsNullOrEmpty(accessToken) || string.IsNullOrEmpty(serviceUrl))
{
return await browserType.LaunchAsync(options).ConfigureAwait(false);
}

var exposeNetwork = Environment.GetEnvironmentVariable("PLAYWRIGHT_SERVICE_EXPOSE_NETWORK") ?? "<loopback>";
var os = Uri.EscapeDataString(Environment.GetEnvironmentVariable("PLAYWRIGHT_SERVICE_OS") ?? "linux");
var runId = Uri.EscapeDataString(Environment.GetEnvironmentVariable("PLAYWRIGHT_SERVICE_RUN_ID") ?? DateTime.Now.ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ss", CultureInfo.InvariantCulture));
var apiVersion = "2023-10-01-preview";
var wsEndpoint = $"{serviceUrl}?os={os}&runId={runId}&api-version={apiVersion}";
var connectOptions = new BrowserTypeConnectOptions
{
Timeout = 3 * 60 * 1000,
ExposeNetwork = exposeNetwork,
Headers = new Dictionary<string, string>
{
["Authorization"] = $"Bearer {accessToken}"
}
};

return await browserType.ConnectAsync(wsEndpoint, connectOptions).ConfigureAwait(false);
return test.RegisterService("Browser", async () =>
new BrowserService(await PlaywrightServiceConnector.LaunchAsync(browserType, options).ConfigureAwait(false)));
}

public Task ResetAsync() => Task.CompletedTask;
Expand Down
32 changes: 1 addition & 31 deletions TUnit.Playwright/BrowserTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ public BrowserTest(BrowserTypeLaunchOptions options)

public async Task<IBrowserContext> NewContext(BrowserNewContextOptions options)
{
options = MergeTelemetryHeaders(options);
options = PlaywrightTelemetryHeaders.Merge(options, PropagateTraceContext);
var context = await Browser.NewContextAsync(options).ConfigureAwait(false);

lock (_contextsLock)
Expand Down Expand Up @@ -91,34 +91,4 @@ public async Task BrowserTearDown(TestContext testContext)
}
}

private BrowserNewContextOptions MergeTelemetryHeaders(BrowserNewContextOptions options)
{
#if NET
if (!PropagateTraceContext || System.Diagnostics.Activity.Current is null)
{
return options;
}

// Seed user headers first so they win when the propagator tries to add the same key.
var merged = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
if (options.ExtraHTTPHeaders is not null)
{
foreach (var kvp in options.ExtraHTTPHeaders)
{
merged[kvp.Key] = kvp.Value;
}
}

var before = merged.Count;
Telemetry.PlaywrightActivityPropagator.InjectInto(merged);
if (merged.Count == before)
{
return options;
}

return new BrowserNewContextOptions(options) { ExtraHTTPHeaders = merged };
#else
return options;
#endif
}
}
41 changes: 41 additions & 0 deletions TUnit.Playwright/ContextFixture.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
using Microsoft.Playwright;
using TUnit.Core;
using TUnit.Core.Interfaces;

namespace TUnit.Playwright;

/// <summary>
/// The injected <see cref="BrowserFixture"/> is hardcoded to <see cref="SharedType.PerTestSession"/>.
/// Authoring a new fixture class is the way to change that scope — attribute arguments on
/// inherited <c>init</c> properties cannot be overridden.
/// </summary>
public class ContextFixture : IAsyncInitializer, IAsyncDisposable
{
[ClassDataSource<BrowserFixture>(Shared = SharedType.PerTestSession)]
public required BrowserFixture BrowserFixture { get; init; }

public IBrowserContext Context { get; private set; } = null!;

protected virtual BrowserNewContextOptions GetContextOptions() =>
new() { Locale = "en-US", ColorScheme = ColorScheme.Light };

/// <summary>
/// When <c>true</c>, seeds the context with W3C trace propagation headers from
/// the current test's <see cref="System.Diagnostics.Activity"/>.
/// </summary>
protected virtual bool PropagateTraceContext => true;

public virtual async Task InitializeAsync()
{
var options = PlaywrightTelemetryHeaders.Merge(GetContextOptions(), PropagateTraceContext);
Context = await BrowserFixture.Browser.NewContextAsync(options).ConfigureAwait(false);
}

public virtual async ValueTask DisposeAsync()
{
if (Context is not null)
{
await Context.CloseAsync().ConfigureAwait(false);
}
}
}
32 changes: 32 additions & 0 deletions TUnit.Playwright/PageFixture.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
using Microsoft.Playwright;
using TUnit.Core;
using TUnit.Core.Interfaces;

namespace TUnit.Playwright;

/// <summary>
/// The injected <see cref="ContextFixture"/> defaults to <see cref="SharedType.None"/> (a fresh
/// context per <see cref="PageFixture"/>). Two <c>[ClassDataSource&lt;PageFixture&gt;]</c>
/// properties on the same test class therefore yield two isolated browser contexts while
/// sharing the underlying <see cref="BrowserFixture"/> at <see cref="SharedType.PerTestSession"/>.
/// </summary>
public class PageFixture : IAsyncInitializer, IAsyncDisposable
{
[ClassDataSource<ContextFixture>]
public required ContextFixture ContextFixture { get; init; }

public IPage Page { get; private set; } = null!;

public virtual async Task InitializeAsync()
{
Page = await ContextFixture.Context.NewPageAsync().ConfigureAwait(false);
}

public virtual async ValueTask DisposeAsync()
{
if (Page is not null)
{
await Page.CloseAsync().ConfigureAwait(false);
}
}
}
27 changes: 27 additions & 0 deletions TUnit.Playwright/PlaywrightFixture.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
using Microsoft.Playwright;
using TUnit.Core.Interfaces;

namespace TUnit.Playwright;

public class PlaywrightFixture : IAsyncInitializer, IAsyncDisposable
{
public IPlaywright Playwright { get; private set; } = null!;

protected virtual string TestIdAttribute => "data-testid";

public virtual async Task InitializeAsync()
{
Playwright = await Microsoft.Playwright.Playwright.CreateAsync().ConfigureAwait(false);
Playwright.Selectors.SetTestIdAttribute(TestIdAttribute);
}

public virtual ValueTask DisposeAsync()
{
Playwright?.Dispose();
return default;
}

public ILocatorAssertions Expect(ILocator locator) => Microsoft.Playwright.Assertions.Expect(locator);
public IPageAssertions Expect(IPage page) => Microsoft.Playwright.Assertions.Expect(page);
public IAPIResponseAssertions Expect(IAPIResponse response) => Microsoft.Playwright.Assertions.Expect(response);
}
37 changes: 37 additions & 0 deletions TUnit.Playwright/PlaywrightServiceConnector.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
using System.Globalization;
using Microsoft.Playwright;

namespace TUnit.Playwright;

internal static class PlaywrightServiceConnector
{
private const string ApiVersion = "2023-10-01-preview";
private const int ConnectTimeoutMs = 3 * 60 * 1000;

public static async Task<IBrowser> LaunchAsync(IBrowserType browserType, BrowserTypeLaunchOptions options)
{
var accessToken = Environment.GetEnvironmentVariable("PLAYWRIGHT_SERVICE_ACCESS_TOKEN");
var serviceUrl = Environment.GetEnvironmentVariable("PLAYWRIGHT_SERVICE_URL");

if (string.IsNullOrEmpty(accessToken) || string.IsNullOrEmpty(serviceUrl))
{
return await browserType.LaunchAsync(options).ConfigureAwait(false);
}

var exposeNetwork = Environment.GetEnvironmentVariable("PLAYWRIGHT_SERVICE_EXPOSE_NETWORK") ?? "<loopback>";
var os = Uri.EscapeDataString(Environment.GetEnvironmentVariable("PLAYWRIGHT_SERVICE_OS") ?? "linux");
var runId = Uri.EscapeDataString(Environment.GetEnvironmentVariable("PLAYWRIGHT_SERVICE_RUN_ID")
?? DateTime.UtcNow.ToString("yyyy-MM-ddTHH:mm:ss", CultureInfo.InvariantCulture));
var wsEndpoint = $"{serviceUrl}?os={os}&runId={runId}&api-version={ApiVersion}";

var connectOptions = new BrowserTypeConnectOptions
{
Timeout = ConnectTimeoutMs,
ExposeNetwork = exposeNetwork,
Headers = new Dictionary<string, string> { ["Authorization"] = $"Bearer {accessToken}" }
};

// BrowserTypeLaunchOptions are local-process only; remote connect uses BrowserTypeConnectOptions.
return await browserType.ConnectAsync(wsEndpoint, connectOptions).ConfigureAwait(false);
}
}
37 changes: 37 additions & 0 deletions TUnit.Playwright/PlaywrightTelemetryHeaders.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
using Microsoft.Playwright;

namespace TUnit.Playwright;

internal static class PlaywrightTelemetryHeaders
{
public static BrowserNewContextOptions Merge(BrowserNewContextOptions options, bool propagate)
{
#if NET
if (!propagate || System.Diagnostics.Activity.Current is null)
{
return options;
}

// Seed user headers first so they win when the propagator tries to add the same key.
var merged = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
if (options.ExtraHTTPHeaders is not null)
{
foreach (var kvp in options.ExtraHTTPHeaders)
{
merged[kvp.Key] = kvp.Value;
}
}

var before = merged.Count;
Telemetry.PlaywrightActivityPropagator.InjectInto(merged);
if (merged.Count == before)
{
return options;
}

return new BrowserNewContextOptions(options) { ExtraHTTPHeaders = merged };
#else
return options;
#endif
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,17 @@
[assembly: .(".NETCoreApp,Version=v10.0", FrameworkDisplayName=".NET 10.0")]
namespace
{
public class BrowserFixture : , .
{
public BrowserFixture() { }
public .IBrowser Browser { get; }
public virtual string BrowserName { get; }
[.ClassDataSourceAttribute<.PlaywrightFixture>(Shared=.)]
public required .PlaywrightFixture PlaywrightFixture { get; init; }
public virtual . DisposeAsync() { }
protected virtual .BrowserTypeLaunchOptions GetLaunchOptions() { }
public virtual . InitializeAsync() { }
}
public class BrowserTest : .PlaywrightTest
{
public BrowserTest() { }
Expand All @@ -13,6 +24,17 @@ namespace
public . BrowserTearDown(.TestContext testContext) { }
public .<.IBrowserContext> NewContext(.BrowserNewContextOptions options) { }
}
public class ContextFixture : , .
{
public ContextFixture() { }
[.ClassDataSourceAttribute<.BrowserFixture>(Shared=.)]
public required .BrowserFixture BrowserFixture { get; init; }
public .IBrowserContext Context { get; }
protected virtual bool PropagateTraceContext { get; }
public virtual . DisposeAsync() { }
protected virtual .BrowserNewContextOptions GetContextOptions() { }
public virtual . InitializeAsync() { }
}
public class ContextTest : .BrowserTest
{
public ContextTest() { }
Expand All @@ -32,6 +54,15 @@ namespace
. DisposeAsync();
. ResetAsync();
}
public class PageFixture : , .
{
public PageFixture() { }
[.ClassDataSourceAttribute<.ContextFixture>]
public required .ContextFixture ContextFixture { get; init; }
public .IPage Page { get; }
public virtual . DisposeAsync() { }
public virtual . InitializeAsync() { }
}
public class PageTest : .ContextTest
{
public PageTest() { }
Expand All @@ -40,6 +71,17 @@ namespace
[.Before(., "", 0)]
public . PageSetup() { }
}
public class PlaywrightFixture : , .
{
public PlaywrightFixture() { }
public .IPlaywright Playwright { get; }
protected virtual string TestIdAttribute { get; }
public virtual . DisposeAsync() { }
public .IAPIResponseAssertions Expect(.IAPIResponse response) { }
public .ILocatorAssertions Expect(.ILocator locator) { }
public .IPageAssertions Expect(.IPage page) { }
public virtual . InitializeAsync() { }
}
public class PlaywrightSkipAttribute : .SkipAttribute
{
public PlaywrightSkipAttribute(params .[] combinations) { }
Expand Down
Loading
Loading