Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
8 changes: 5 additions & 3 deletions TUnit.Engine/Services/PropertyInjector.cs
Original file line number Diff line number Diff line change
Expand Up @@ -263,9 +263,11 @@ private async Task InjectSourceGeneratedPropertyAsync(

// Use a composite key to avoid conflicts when nested classes have properties with the same name
var cacheKey = PropertyCacheKeyGenerator.GetCacheKey(metadata);
var useTestClassPropertyCache = testContext?.Metadata.TestDetails.ClassType.IsInstanceOfType(instance) == true;

// Check if property was pre-resolved during registration
if (testContext?.Metadata.TestDetails.TestClassInjectedPropertyArguments.TryGetValue(cacheKey, out resolvedValue) != true)
if (!useTestClassPropertyCache ||
testContext?.Metadata.TestDetails.TestClassInjectedPropertyArguments.TryGetValue(cacheKey, out resolvedValue) != true)
{
// Resolve the property value from the data source
resolvedValue = await ResolvePropertyDataAsync(
Expand All @@ -290,9 +292,9 @@ private async Task InjectSourceGeneratedPropertyAsync(
// Store the converted value for potential reuse (e.g., retries).
// Use indexer to overwrite any pre-resolved unconverted value so that
// SetCachedPropertiesOnInstance can use the value directly without re-converting.
if (testContext != null)
if (useTestClassPropertyCache)
{
testContext.Metadata.TestDetails.GetOrCreateInjectedPropertyArguments()[cacheKey] = resolvedValue;
testContext!.Metadata.TestDetails.GetOrCreateInjectedPropertyArguments()[cacheKey] = resolvedValue;
}
}

Expand Down
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
}
}
48 changes: 48 additions & 0 deletions TUnit.Playwright/ContextFixture.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
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!;

/// <summary>
/// Returns the options used when creating each <see cref="IBrowserContext"/>. Defaults
/// match <see cref="ContextTest.ContextOptions"/> — pinned <c>Locale = "en-US"</c> and
/// <c>ColorScheme = Light</c> for deterministic cross-platform rendering. Override to
/// match your application's locale or to restore browser-default behaviour
/// (<c>new BrowserNewContextOptions()</c>).
/// </summary>
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
}
}
Loading
Loading