diff --git a/TUnit.Engine/Services/PropertyInjector.cs b/TUnit.Engine/Services/PropertyInjector.cs
index a16767e55d..fd1efb5da1 100644
--- a/TUnit.Engine/Services/PropertyInjector.cs
+++ b/TUnit.Engine/Services/PropertyInjector.cs
@@ -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(
@@ -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;
}
}
diff --git a/TUnit.Playwright/BrowserFixture.cs b/TUnit.Playwright/BrowserFixture.cs
new file mode 100644
index 0000000000..50bc7d321e
--- /dev/null
+++ b/TUnit.Playwright/BrowserFixture.cs
@@ -0,0 +1,38 @@
+using Microsoft.Playwright;
+using TUnit.Core;
+using TUnit.Core.Interfaces;
+
+namespace TUnit.Playwright;
+
+///
+/// The injected is hardcoded to .
+/// Authoring a new fixture class is the way to change that scope — attribute arguments on
+/// inherited init properties cannot be overridden.
+///
+public class BrowserFixture : IAsyncInitializer, IAsyncDisposable
+{
+ [ClassDataSource(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);
+ }
+ }
+}
diff --git a/TUnit.Playwright/BrowserService.cs b/TUnit.Playwright/BrowserService.cs
index 22f9b66860..441f8ba725 100644
--- a/TUnit.Playwright/BrowserService.cs
+++ b/TUnit.Playwright/BrowserService.cs
@@ -1,4 +1,3 @@
-using System.Globalization;
using Microsoft.Playwright;
namespace TUnit.Playwright;
@@ -17,37 +16,8 @@ public static Task Register(
IBrowserType browserType,
BrowserTypeLaunchOptions options)
{
- return test.RegisterService("Browser", async () => new BrowserService(await CreateBrowser(browserType, options).ConfigureAwait(false)));
- }
-
- private static async Task 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") ?? "";
- 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
- {
- ["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;
diff --git a/TUnit.Playwright/BrowserTest.cs b/TUnit.Playwright/BrowserTest.cs
index e29e08d6bc..b93769d498 100644
--- a/TUnit.Playwright/BrowserTest.cs
+++ b/TUnit.Playwright/BrowserTest.cs
@@ -34,7 +34,7 @@ public BrowserTest(BrowserTypeLaunchOptions options)
public async Task NewContext(BrowserNewContextOptions options)
{
- options = MergeTelemetryHeaders(options);
+ options = PlaywrightTelemetryHeaders.Merge(options, PropagateTraceContext);
var context = await Browser.NewContextAsync(options).ConfigureAwait(false);
lock (_contextsLock)
@@ -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(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
- }
}
diff --git a/TUnit.Playwright/ContextFixture.cs b/TUnit.Playwright/ContextFixture.cs
new file mode 100644
index 0000000000..545eae9449
--- /dev/null
+++ b/TUnit.Playwright/ContextFixture.cs
@@ -0,0 +1,48 @@
+using Microsoft.Playwright;
+using TUnit.Core;
+using TUnit.Core.Interfaces;
+
+namespace TUnit.Playwright;
+
+///
+/// The injected is hardcoded to .
+/// Authoring a new fixture class is the way to change that scope — attribute arguments on
+/// inherited init properties cannot be overridden.
+///
+public class ContextFixture : IAsyncInitializer, IAsyncDisposable
+{
+ [ClassDataSource(Shared = SharedType.PerTestSession)]
+ public required BrowserFixture BrowserFixture { get; init; }
+
+ public IBrowserContext Context { get; private set; } = null!;
+
+ ///
+ /// Returns the options used when creating each . Defaults
+ /// match — pinned Locale = "en-US" and
+ /// ColorScheme = Light for deterministic cross-platform rendering. Override to
+ /// match your application's locale or to restore browser-default behaviour
+ /// (new BrowserNewContextOptions()).
+ ///
+ protected virtual BrowserNewContextOptions GetContextOptions() =>
+ new() { Locale = "en-US", ColorScheme = ColorScheme.Light };
+
+ ///
+ /// When true, seeds the context with W3C trace propagation headers from
+ /// the current test's .
+ ///
+ 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);
+ }
+ }
+}
diff --git a/TUnit.Playwright/PageFixture.cs b/TUnit.Playwright/PageFixture.cs
new file mode 100644
index 0000000000..6b804fcd5d
--- /dev/null
+++ b/TUnit.Playwright/PageFixture.cs
@@ -0,0 +1,32 @@
+using Microsoft.Playwright;
+using TUnit.Core;
+using TUnit.Core.Interfaces;
+
+namespace TUnit.Playwright;
+
+///
+/// The injected defaults to (a fresh
+/// context per ). Two [ClassDataSource<PageFixture>]
+/// properties on the same test class therefore yield two isolated browser contexts while
+/// sharing the underlying at .
+///
+public class PageFixture : IAsyncInitializer, IAsyncDisposable
+{
+ [ClassDataSource]
+ 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);
+ }
+ }
+}
diff --git a/TUnit.Playwright/PlaywrightFixture.cs b/TUnit.Playwright/PlaywrightFixture.cs
new file mode 100644
index 0000000000..652e70f4e9
--- /dev/null
+++ b/TUnit.Playwright/PlaywrightFixture.cs
@@ -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);
+}
diff --git a/TUnit.Playwright/PlaywrightServiceConnector.cs b/TUnit.Playwright/PlaywrightServiceConnector.cs
new file mode 100644
index 0000000000..9e76b21733
--- /dev/null
+++ b/TUnit.Playwright/PlaywrightServiceConnector.cs
@@ -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 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") ?? "";
+ 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 { ["Authorization"] = $"Bearer {accessToken}" }
+ };
+
+ // BrowserTypeLaunchOptions are local-process only; remote connect uses BrowserTypeConnectOptions.
+ return await browserType.ConnectAsync(wsEndpoint, connectOptions).ConfigureAwait(false);
+ }
+}
diff --git a/TUnit.Playwright/PlaywrightTelemetryHeaders.cs b/TUnit.Playwright/PlaywrightTelemetryHeaders.cs
new file mode 100644
index 0000000000..67d3b2c62c
--- /dev/null
+++ b/TUnit.Playwright/PlaywrightTelemetryHeaders.cs
@@ -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(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
+ }
+}
diff --git a/TUnit.PublicAPI/Tests.Playwright_Library_Has_No_API_Changes.DotNet10_0.verified.txt b/TUnit.PublicAPI/Tests.Playwright_Library_Has_No_API_Changes.DotNet10_0.verified.txt
index 8112d0e13a..9736bef78e 100644
--- a/TUnit.PublicAPI/Tests.Playwright_Library_Has_No_API_Changes.DotNet10_0.verified.txt
+++ b/TUnit.PublicAPI/Tests.Playwright_Library_Has_No_API_Changes.DotNet10_0.verified.txt
@@ -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() { }
@@ -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() { }
@@ -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() { }
@@ -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) { }
diff --git a/TUnit.PublicAPI/Tests.Playwright_Library_Has_No_API_Changes.DotNet8_0.verified.txt b/TUnit.PublicAPI/Tests.Playwright_Library_Has_No_API_Changes.DotNet8_0.verified.txt
index 215a68c1c3..091f3bb52f 100644
--- a/TUnit.PublicAPI/Tests.Playwright_Library_Has_No_API_Changes.DotNet8_0.verified.txt
+++ b/TUnit.PublicAPI/Tests.Playwright_Library_Has_No_API_Changes.DotNet8_0.verified.txt
@@ -1,6 +1,17 @@
[assembly: .(".NETCoreApp,Version=v8.0", FrameworkDisplayName=".NET 8.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() { }
@@ -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() { }
@@ -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() { }
@@ -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) { }
diff --git a/TUnit.PublicAPI/Tests.Playwright_Library_Has_No_API_Changes.DotNet9_0.verified.txt b/TUnit.PublicAPI/Tests.Playwright_Library_Has_No_API_Changes.DotNet9_0.verified.txt
index ce58540a67..a8e94774c0 100644
--- a/TUnit.PublicAPI/Tests.Playwright_Library_Has_No_API_Changes.DotNet9_0.verified.txt
+++ b/TUnit.PublicAPI/Tests.Playwright_Library_Has_No_API_Changes.DotNet9_0.verified.txt
@@ -1,6 +1,17 @@
[assembly: .(".NETCoreApp,Version=v9.0", FrameworkDisplayName=".NET 9.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() { }
@@ -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() { }
@@ -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() { }
@@ -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) { }
diff --git a/TUnit.PublicAPI/Tests.Playwright_Library_Has_No_API_Changes.Net4_7.verified.txt b/TUnit.PublicAPI/Tests.Playwright_Library_Has_No_API_Changes.Net4_7.verified.txt
index 38b7c68060..12880eb451 100644
--- a/TUnit.PublicAPI/Tests.Playwright_Library_Has_No_API_Changes.Net4_7.verified.txt
+++ b/TUnit.PublicAPI/Tests.Playwright_Library_Has_No_API_Changes.Net4_7.verified.txt
@@ -1,6 +1,17 @@
[assembly: .(".NETStandard,Version=v2.0", FrameworkDisplayName=".NET Standard 2.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() { }
@@ -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() { }
@@ -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() { }
@@ -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) { }
diff --git a/TUnit.Templates.Tests/Snapshots/InstantiationTest.TUnit.Playwright._.verified/TUnit.Playwright/TwoContextFixtureTests.cs b/TUnit.Templates.Tests/Snapshots/InstantiationTest.TUnit.Playwright._.verified/TUnit.Playwright/TwoContextFixtureTests.cs
new file mode 100644
index 0000000000..101d8dbc95
--- /dev/null
+++ b/TUnit.Templates.Tests/Snapshots/InstantiationTest.TUnit.Playwright._.verified/TUnit.Playwright/TwoContextFixtureTests.cs
@@ -0,0 +1,47 @@
+using TUnit.Assertions;
+using TUnit.Assertions.Extensions;
+using TUnit.Core;
+using TUnit.Playwright;
+
+namespace TUnit.Playwright;
+
+public class TwoContextFixtureTests
+{
+ [ClassDataSource]
+ public required PageFixture Alice { get; init; }
+
+ [ClassDataSource]
+ public required PageFixture Bob { get; init; }
+
+ [Test]
+ public async Task Two_Pages_Have_Isolated_Storage_But_Share_Browser()
+ {
+ await NavigateToStorageOrigin(Alice);
+ await NavigateToStorageOrigin(Bob);
+
+ await Alice.Page.EvaluateAsync("() => localStorage.setItem('user', 'alice')");
+
+ var aliceUser = await Alice.Page.EvaluateAsync("() => localStorage.getItem('user')");
+ var bobUser = await Bob.Page.EvaluateAsync("() => localStorage.getItem('user')");
+
+ await Assert.That(aliceUser).IsEqualTo("alice");
+ await Assert.That(bobUser).IsNull();
+
+ await Assert.That(Alice.Page).IsNotSameReferenceAs(Bob.Page);
+ await Assert.That(Alice.ContextFixture.Context).IsNotSameReferenceAs(Bob.ContextFixture.Context);
+ await Assert.That(Alice.ContextFixture.BrowserFixture.Browser)
+ .IsSameReferenceAs(Bob.ContextFixture.BrowserFixture.Browser);
+ }
+
+ private static async Task NavigateToStorageOrigin(PageFixture fixture)
+ {
+ await fixture.Page.RouteAsync("https://tunit-playwright.test/**", route =>
+ route.FulfillAsync(new()
+ {
+ Body = "TUnit Playwright",
+ ContentType = "text/html",
+ }));
+
+ await fixture.Page.GotoAsync("https://tunit-playwright.test/");
+ }
+}
diff --git a/TUnit.Templates/content/Directory.Build.props b/TUnit.Templates/content/Directory.Build.props
index 21dd24a0cb..ba13f9ea34 100644
--- a/TUnit.Templates/content/Directory.Build.props
+++ b/TUnit.Templates/content/Directory.Build.props
@@ -36,4 +36,18 @@
-
\ No newline at end of file
+
+
+
+
+
+
+
+
+
diff --git a/TUnit.Templates/content/TUnit.Playwright/TwoContextFixtureTests.cs b/TUnit.Templates/content/TUnit.Playwright/TwoContextFixtureTests.cs
new file mode 100644
index 0000000000..f25e6c3055
--- /dev/null
+++ b/TUnit.Templates/content/TUnit.Playwright/TwoContextFixtureTests.cs
@@ -0,0 +1,47 @@
+using TUnit.Assertions;
+using TUnit.Assertions.Extensions;
+using TUnit.Core;
+using TUnit.Playwright;
+
+namespace TestProject;
+
+public class TwoContextFixtureTests
+{
+ [ClassDataSource]
+ public required PageFixture Alice { get; init; }
+
+ [ClassDataSource]
+ public required PageFixture Bob { get; init; }
+
+ [Test]
+ public async Task Two_Pages_Have_Isolated_Storage_But_Share_Browser()
+ {
+ await NavigateToStorageOrigin(Alice);
+ await NavigateToStorageOrigin(Bob);
+
+ await Alice.Page.EvaluateAsync("() => localStorage.setItem('user', 'alice')");
+
+ var aliceUser = await Alice.Page.EvaluateAsync("() => localStorage.getItem('user')");
+ var bobUser = await Bob.Page.EvaluateAsync("() => localStorage.getItem('user')");
+
+ await Assert.That(aliceUser).IsEqualTo("alice");
+ await Assert.That(bobUser).IsNull();
+
+ await Assert.That(Alice.Page).IsNotSameReferenceAs(Bob.Page);
+ await Assert.That(Alice.ContextFixture.Context).IsNotSameReferenceAs(Bob.ContextFixture.Context);
+ await Assert.That(Alice.ContextFixture.BrowserFixture.Browser)
+ .IsSameReferenceAs(Bob.ContextFixture.BrowserFixture.Browser);
+ }
+
+ private static async Task NavigateToStorageOrigin(PageFixture fixture)
+ {
+ await fixture.Page.RouteAsync("https://tunit-playwright.test/**", route =>
+ route.FulfillAsync(new()
+ {
+ Body = "TUnit Playwright",
+ ContentType = "text/html",
+ }));
+
+ await fixture.Page.GotoAsync("https://tunit-playwright.test/");
+ }
+}
diff --git a/TUnit.TestProject/Bugs/5840/Tests.cs b/TUnit.TestProject/Bugs/5840/Tests.cs
new file mode 100644
index 0000000000..05a316c2d6
--- /dev/null
+++ b/TUnit.TestProject/Bugs/5840/Tests.cs
@@ -0,0 +1,28 @@
+using TUnit.TestProject.Attributes;
+
+namespace TUnit.TestProject.Bugs._5840;
+
+[EngineTest(ExpectedResult.Pass)]
+public class Issue5840NestedPropertyCacheTests
+{
+ [ClassDataSource(Shared = SharedType.None)]
+ public required Issue5840OuterFixture First { get; init; }
+
+ [ClassDataSource(Shared = SharedType.None)]
+ public required Issue5840OuterFixture Second { get; init; }
+
+ [Test]
+ public async Task Nested_ClassDataSource_Properties_Are_Isolated_Per_Parent_Instance()
+ {
+ await Assert.That(First).IsNotSameReferenceAs(Second);
+ await Assert.That(First.Inner).IsNotSameReferenceAs(Second.Inner);
+ }
+}
+
+public class Issue5840OuterFixture
+{
+ [ClassDataSource(Shared = SharedType.None)]
+ public required Issue5840InnerFixture Inner { get; init; }
+}
+
+public class Issue5840InnerFixture;