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;