diff --git a/src/Playwright.TestAdapter/PlaywrightSettingsProvider.cs b/src/Playwright.TestAdapter/PlaywrightSettingsProvider.cs index 0390535d7..90ad170c8 100644 --- a/src/Playwright.TestAdapter/PlaywrightSettingsProvider.cs +++ b/src/Playwright.TestAdapter/PlaywrightSettingsProvider.cs @@ -23,6 +23,7 @@ */ using System; +using System.Text.Json; using System.Xml; using Microsoft.VisualStudio.TestPlatform.ObjectModel; using Microsoft.VisualStudio.TestPlatform.ObjectModel.Adapter; @@ -35,6 +36,22 @@ public class PlaywrightSettingsProvider : ISettingsProvider { private static PlaywrightSettingsXml? _settings = null!; + public static void LoadViaEnvIfNeeded() + { + if (_settings == null) + { + var settings = Environment.GetEnvironmentVariable("PW_INTERNAL_ADAPTER_SETTINGS"); + if (!string.IsNullOrEmpty(settings)) + { + _settings = JsonSerializer.Deserialize(settings); + } + else + { + _settings = new PlaywrightSettingsXml(); + } + } + } + public static string BrowserName { get @@ -103,5 +120,10 @@ private static void ValidateBrowserName(string browserName, string fromText, str } public void Load(XmlReader reader) - => _settings = new PlaywrightSettingsXml(reader); + { + // NOTE: ISettingsProvider::Load is not called when there are no runsettings (either file or passed via command line). + _settings = new PlaywrightSettingsXml(reader); + Environment.SetEnvironmentVariable("PW_INTERNAL_ADAPTER_SETTINGS", JsonSerializer.Serialize(_settings)); + } } + diff --git a/src/Playwright.TestAdapter/PlaywrightSettingsXml.cs b/src/Playwright.TestAdapter/PlaywrightSettingsXml.cs index dea4d983d..6e9371d98 100644 --- a/src/Playwright.TestAdapter/PlaywrightSettingsXml.cs +++ b/src/Playwright.TestAdapter/PlaywrightSettingsXml.cs @@ -35,6 +35,10 @@ namespace Microsoft.Playwright.TestAdapter; public class PlaywrightSettingsXml { + public PlaywrightSettingsXml() + { + } + public PlaywrightSettingsXml(XmlReader reader) { // Skip Playwright root Element @@ -159,10 +163,10 @@ private static object ParseAsJson(string value, Type type) return JsonSerializer.Deserialize(value.Replace('\'', '"'), type)!; } - public BrowserTypeLaunchOptions? LaunchOptions { get; private set; } - public string? BrowserName { get; private set; } - public bool? Headless { get; private set; } - public float? ExpectTimeout { get; private set; } - public int? Retries { get; private set; } + public BrowserTypeLaunchOptions? LaunchOptions { get; set; } + public string? BrowserName { get; set; } + public bool? Headless { get; set; } + public float? ExpectTimeout { get; set; } + public int? Retries { get; set; } } diff --git a/src/Playwright.TestingHarnessTest/Playwright.TestingHarnessTest.csproj b/src/Playwright.TestingHarnessTest/Playwright.TestingHarnessTest.csproj index a553b2905..10f2b57d0 100644 --- a/src/Playwright.TestingHarnessTest/Playwright.TestingHarnessTest.csproj +++ b/src/Playwright.TestingHarnessTest/Playwright.TestingHarnessTest.csproj @@ -11,7 +11,7 @@ - + @@ -26,6 +26,11 @@ + + + + + diff --git a/src/Playwright.TestingHarnessTest/tests/baseTest.ts b/src/Playwright.TestingHarnessTest/tests/baseTest.ts index 968eba1dc..2fc316194 100644 --- a/src/Playwright.TestingHarnessTest/tests/baseTest.ts +++ b/src/Playwright.TestingHarnessTest/tests/baseTest.ts @@ -19,7 +19,7 @@ type RunResult = { export const test = base.extend<{ proxyServer: ProxyServer; - testMode: 'nunit' | 'mstest' | 'xunit'; + testMode: 'nunit' | 'mstest' | 'xunit' | 'xunit.v3'; runTest: (files: Record, command: string, env?: NodeJS.ProcessEnv) => Promise; launchServer: ({ port: number }) => Promise; }>({ diff --git a/src/Playwright.TestingHarnessTest/tests/xunit.v3/basic.spec.ts b/src/Playwright.TestingHarnessTest/tests/xunit.v3/basic.spec.ts new file mode 100644 index 000000000..10988308d --- /dev/null +++ b/src/Playwright.TestingHarnessTest/tests/xunit.v3/basic.spec.ts @@ -0,0 +1,574 @@ +/* + * MIT License + * + * Copyright (c) Microsoft Corporation. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +import { test, expect } from '../baseTest'; + +test.use({ testMode: 'xunit.v3' }); + +test('should be able to forward DEBUG=pw:api env var', async ({ runTest }) => { + const result = await runTest({ + 'ExampleTests.cs': ` + using System; + using System.Threading.Tasks; + using Microsoft.Playwright.Xunit.v3; + using Xunit; + + namespace Playwright.TestingHarnessTest.Xunit; + + public class : PageTest + { + [Fact] + public async Task Test() + { + await Page.GotoAsync("about:blank"); + await Page.SetContentAsync(""); + try + { + await Page.Locator("button").ClickAsync(new() { Timeout = 1_000 }); + } + catch + { + } + } + }`, + '.runsettings': ` + + + + + pw:api + + + `, + }, 'dotnet test --settings=.runsettings'); + expect(result.passed).toBe(1); + expect(result.failed).toBe(0); + expect(result.total).toBe(1); + // TODO: change back to .stdout so the stdout is correlated with the test + expect(result.rawStdout).toContain("pw:api") + expect(result.rawStdout).toContain("element is not enabled") + expect(result.rawStdout).toContain("retrying click action") +}); + +test('should be able to set the browser via the runsettings file', async ({ runTest }) => { + const result = await runTest({ + 'ExampleTests.cs': ` + using System; + using System.Threading.Tasks; + using Microsoft.Playwright.Xunit.v3; + using Xunit; + + namespace Playwright.TestingHarnessTest.Xunit; + + public class : PageTest + { + private readonly ITestOutputHelper output; + + public (ITestOutputHelper output) + { + this.output = output; + } + + [Fact] + public async Task Test() + { + await Page.GotoAsync("about:blank"); + output.WriteLine("BrowserName: " + BrowserName); + output.WriteLine("BrowserType: " + BrowserType.Name); + output.WriteLine("User-Agent: " + await Page.EvaluateAsync("() => navigator.userAgent")); + } + }`, + '.runsettings': ` + + + + webkit + + + `, + }, 'dotnet test --settings=.runsettings'); + expect(result.passed).toBe(1); + expect(result.failed).toBe(0); + expect(result.total).toBe(1); + expect(result.stdout).toContain("BrowserName: webkit") + expect(result.stdout).toContain("BrowserType: webkit") + expect(/User-Agent: .*WebKit.*/.test(result.stdout)).toBeTruthy() +}); + +test('should prioritize browser from env over the runsettings file', async ({ runTest }) => { + const result = await runTest({ + 'ExampleTests.cs': ` + using System; + using System.Threading.Tasks; + using Microsoft.Playwright.Xunit.v3; + using Xunit; + + namespace Playwright.TestingHarnessTest.Xunit; + + public class : PageTest + { + private readonly ITestOutputHelper output; + + public (ITestOutputHelper output) + { + this.output = output; + } + + [Fact] + public async Task Test() + { + await Page.GotoAsync("about:blank"); + output.WriteLine("BrowserName: " + BrowserName); + output.WriteLine("BrowserType: " + BrowserType.Name); + output.WriteLine("User-Agent: " + await Page.EvaluateAsync("() => navigator.userAgent")); + } + }`, + '.runsettings': ` + + + + webkit + + + `, + }, 'dotnet test --settings=.runsettings', { + BROWSER: 'firefox' + }); + expect(result.passed).toBe(1); + expect(result.failed).toBe(0); + expect(result.total).toBe(1); + expect(result.stdout).toContain("BrowserName: firefox") + expect(result.stdout).toContain("BrowserType: firefox") + expect(/User-Agent: .*Firefox.*/.test(result.stdout)).toBeTruthy() +}); + +test('should be able to make the browser headed via the env', async ({ runTest }) => { + const result = await runTest({ + 'ExampleTests.cs': ` + using System; + using System.Threading.Tasks; + using Microsoft.Playwright.Xunit.v3; + using Xunit; + + namespace Playwright.TestingHarnessTest.Xunit; + + public class : PageTest + { + private readonly ITestOutputHelper output; + + public (ITestOutputHelper output) + { + this.output = output; + } + + [Fact] + public async Task Test() + { + await Page.GotoAsync("about:blank"); + output.WriteLine("BrowserName: " + BrowserName); + output.WriteLine("User-Agent: " + await Page.EvaluateAsync("() => navigator.userAgent")); + } + }`, + }, 'dotnet test', { + HEADED: '1' + }); + expect(result.passed).toBe(1); + expect(result.failed).toBe(0); + expect(result.total).toBe(1); + expect(result.stdout).toContain("BrowserName: chromium") + expect(result.stdout).not.toContain("Headless") +}); + +test('should be able to parse BrowserName and LaunchOptions.Headless from runsettings', async ({ runTest }) => { + const result = await runTest({ + 'ExampleTests.cs': ` + using System; + using System.Threading.Tasks; + using Microsoft.Playwright.Xunit.v3; + using Xunit; + + namespace Playwright.TestingHarnessTest.Xunit; + + public class : PageTest + { + private readonly ITestOutputHelper output; + + public (ITestOutputHelper output) + { + this.output = output; + } + + [Fact] + public async Task Test() + { + await Page.GotoAsync("about:blank"); + output.WriteLine("BrowserName: " + BrowserName); + output.WriteLine("User-Agent: " + await Page.EvaluateAsync("() => navigator.userAgent")); + } + }`, + '.runsettings': ` + + + + + false + + + firefox + + + `, + }, 'dotnet test --settings=.runsettings'); + expect(result.passed).toBe(1); + expect(result.failed).toBe(0); + expect(result.total).toBe(1); + expect(result.stdout).toContain("BrowserName: firefox") + expect(result.stdout).not.toContain("Headless") +}); + +test('should be able to parse LaunchOptions.Proxy from runsettings', async ({ runTest, proxyServer }) => { + const result = await runTest({ + 'ExampleTests.cs': ` + using System; + using System.Threading.Tasks; + using Microsoft.Playwright.Xunit.v3; + using Xunit; + + namespace Playwright.TestingHarnessTest.Xunit; + + public class : PageTest + { + private readonly ITestOutputHelper output; + + public (ITestOutputHelper output) + { + this.output = output; + } + + [Fact] + public async Task Test() + { + output.WriteLine("User-Agent: " + await Page.EvaluateAsync("() => navigator.userAgent")); + await Page.GotoAsync("http://example.com"); + } + }`, + '.runsettings': ` + + + + chromium + + false + + ${proxyServer.listenAddr()} + user + pwd + + + + + `, + }, 'dotnet test --settings=.runsettings'); + expect(result.passed).toBe(1); + expect(result.failed).toBe(0); + expect(result.total).toBe(1); + + expect(result.stdout).not.toContain("Headless"); + + const { url, auth } = proxyServer.requests.find(r => r.url === 'http://example.com/')!;; + expect(url).toBe('http://example.com/'); + expect(auth).toBe('user:pwd'); +}); + +test('should be able to parse LaunchOptions.Args from runsettings', async ({ runTest }) => { + const result = await runTest({ + 'ExampleTests.cs': ` + using System; + using System.Threading.Tasks; + using Microsoft.Playwright.Xunit.v3; + using Xunit; + + namespace Playwright.TestingHarnessTest.Xunit; + + public class : PageTest + { + private readonly ITestOutputHelper output; + + public (ITestOutputHelper output) + { + this.output = output; + } + + [Fact] + public async Task Test() + { + output.WriteLine("User-Agent: " + await Page.EvaluateAsync("() => navigator.userAgent")); + } + }`, + '.runsettings': ` + + + + + ['--user-agent=hello'] + + + + `, + }, 'dotnet test --settings=.runsettings'); + expect(result.passed).toBe(1); + expect(result.failed).toBe(0); + expect(result.total).toBe(1); + expect(result.stdout).toContain("User-Agent: hello") +}); + +test('should be able to override context options', async ({ runTest }) => { + const result = await runTest({ + 'ExampleTests.cs': ` + using System; + using System.Collections.Generic; + using System.Threading.Tasks; + using Microsoft.Playwright; + using Microsoft.Playwright.Xunit.v3; + using Xunit; + + namespace Playwright.TestingHarnessTest.Xunit; + + public class : PageTest + { + [Fact] + public async Task Test() + { + await Page.GotoAsync("about:blank"); + + Assert.False(await Page.EvaluateAsync("() => matchMedia('(prefers-color-scheme: light)').matches")); + Assert.True(await Page.EvaluateAsync("() => matchMedia('(prefers-color-scheme: dark)').matches")); + + Assert.Equal(1920, await Page.EvaluateAsync("() => window.innerWidth")); + Assert.Equal(1080, await Page.EvaluateAsync("() => window.innerHeight")); + + Assert.Equal("Foobar", await Page.EvaluateAsync("() => navigator.userAgent")); + + var response = await Page.GotoAsync("https://example.com/"); + Assert.Equal("KekStarValue", await response.Request.HeaderValueAsync("Kekstar")); + } + + public override BrowserNewContextOptions ContextOptions() + { + return new BrowserNewContextOptions() + { + ColorScheme = ColorScheme.Dark, + UserAgent = "Foobar", + ViewportSize = new() + { + Width = 1920, + Height = 1080 + }, + ExtraHTTPHeaders = new Dictionary { + { "Kekstar", "KekStarValue" } + } + }; + } + }`}, 'dotnet test'); + expect(result.passed).toBe(1); + expect(result.failed).toBe(0); + expect(result.total).toBe(1); +}); + +test('should be able to override launch options', async ({ runTest }) => { + const result = await runTest({ + 'ExampleTests.cs': ` + using System; + using System.Threading.Tasks; + using Microsoft.Playwright.Xunit.v3; + using Xunit; + + namespace Playwright.TestingHarnessTest.Xunit; + + public class : PageTest + { + [Fact] + public async Task Test() + { + await Page.GotoAsync("about:blank"); + Console.WriteLine("User-Agent: " + await Page.EvaluateAsync("() => navigator.userAgent")); + } + }`, + '.runsettings': ` + + + + + false + + + + `, + }, 'dotnet test --settings=.runsettings'); + expect(result.passed).toBe(1); + expect(result.failed).toBe(0); + expect(result.total).toBe(1); + expect(result.stdout).not.toContain("Headless"); +}); + +test.describe('Expect() timeout', () => { + test('should have 5 seconds by default', async ({ runTest }) => { + const result = await runTest({ + 'ExampleTests.cs': ` + using System; + using System.Collections.Generic; + using System.Threading.Tasks; + using Microsoft.Playwright; + using Microsoft.Playwright.Xunit.v3; + using Xunit; + + namespace Playwright.TestingHarnessTest.Xunit; + + public class : PageTest + { + [Fact] + public async Task Test() + { + await Page.GotoAsync("about:blank"); + await Page.SetContentAsync(""); + await Expect(Page.Locator("button")).ToHaveTextAsync("noooo-wrong-text"); + } + }`, + }, 'dotnet test'); + expect(result.passed).toBe(0); + expect(result.failed).toBe(1); + expect(result.total).toBe(1); + expect(result.rawStdout).toContain("LocatorAssertions.ToHaveTextAsync with timeout 5000ms") + }); + + test('should be able to override it via each Expect() call', async ({ runTest }) => { + const result = await runTest({ + 'ExampleTests.cs': ` + using System; + using System.Collections.Generic; + using System.Threading.Tasks; + using Microsoft.Playwright; + using Microsoft.Playwright.Xunit.v3; + using Xunit; + + namespace Playwright.TestingHarnessTest.Xunit; + + public class : PageTest + { + [Fact] + public async Task Test() + { + await Page.GotoAsync("about:blank"); + await Page.SetContentAsync(""); + await Expect(Page.Locator("button")).ToHaveTextAsync("noooo-wrong-text", new() { Timeout = 100 }); + } + }`, + }, 'dotnet test'); + expect(result.passed).toBe(0); + expect(result.failed).toBe(1); + expect(result.total).toBe(1); + expect(result.rawStdout).toContain("LocatorAssertions.ToHaveTextAsync with timeout 100ms") + }); + + test('should be able to override it via the global config', async ({ runTest }) => { + const result = await runTest({ + 'ExampleTests.cs': ` + using System; + using System.Collections.Generic; + using System.Threading.Tasks; + using Microsoft.Playwright; + using Microsoft.Playwright.Xunit.v3; + using Xunit; + + namespace Playwright.TestingHarnessTest.Xunit; + + public class : PageTest + { + [Fact] + public async Task Test() + { + await Page.GotoAsync("about:blank"); + await Page.SetContentAsync(""); + await Expect(Page.Locator("button")).ToHaveTextAsync("noooo-wrong-text"); + } + }`, + '.runsettings': ` + + + + 123 + + + `, + }, 'dotnet test --settings=.runsettings'); + expect(result.passed).toBe(0); + expect(result.failed).toBe(1); + expect(result.total).toBe(1); + expect(result.rawStdout).toContain("LocatorAssertions.ToHaveTextAsync with timeout 123ms") + }); +}); + +test.describe('ConnectOptions', () => { + const ExampleTestWithConnectOptions = ` + using System; + using System.Threading.Tasks; + using Microsoft.Playwright; + using Microsoft.Playwright.Xunit.v3; + using Xunit; + + namespace Playwright.TestingHarnessTest.Xunit; + + public class : PageTest + { + [Fact] + public async Task Test() + { + await Page.GotoAsync("about:blank"); + } + public override async Task<(string, BrowserTypeConnectOptions)?> ConnectOptionsAsync() + { + return ("http://127.0.0.1:1234", null); + } + }`; + + test('should fail when the server is not reachable', async ({ runTest }) => { + const result = await runTest({ + 'ExampleTests.cs': ExampleTestWithConnectOptions, + }, 'dotnet test'); + expect(result.passed).toBe(0); + expect(result.failed).toBe(1); + expect(result.total).toBe(1); + expect(result.rawStdout).toContain('connect ECONNREFUSED 127.0.0.1:1234') + }); + + test('should pass when the server is reachable', async ({ runTest, launchServer }) => { + await launchServer({ port: 1234 }); + const result = await runTest({ + 'ExampleTests.cs': ExampleTestWithConnectOptions, + }, 'dotnet test'); + expect(result.passed).toBe(1); + expect(result.failed).toBe(0); + expect(result.total).toBe(1); + }); +}); diff --git a/src/Playwright.Xunit.v3/BrowserService.cs b/src/Playwright.Xunit.v3/BrowserService.cs new file mode 100644 index 000000000..53be2802e --- /dev/null +++ b/src/Playwright.Xunit.v3/BrowserService.cs @@ -0,0 +1,100 @@ +/* + * MIT License + * + * Copyright (c) Microsoft Corporation. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Text.Json; +using System.Text.Json.Serialization; +using System.Threading.Tasks; +using Microsoft.Playwright.TestAdapter; + +namespace Microsoft.Playwright.Xunit.v3; + +internal class BrowserService : IWorkerService +{ + public IBrowser Browser { get; private set; } + + private BrowserService(IBrowser browser) + { + Browser = browser; + } + + public static Task Register(WorkerAwareTest test, IBrowserType browserType, (string, BrowserTypeConnectOptions?)? connectOptions) + { + return test.RegisterService("Browser", async () => new BrowserService(await CreateBrowser(browserType, connectOptions).ConfigureAwait(false))); + } + + private static async Task CreateBrowser(IBrowserType browserType, (string WSEndpoint, BrowserTypeConnectOptions? Options)? connectOptions) + { + if (connectOptions.HasValue && connectOptions.Value.WSEndpoint != null) + { + var options = new BrowserTypeConnectOptions(connectOptions?.Options ?? new()); + var headers = options.Headers?.ToDictionary(kvp => kvp.Key, kvp => kvp.Value) ?? []; + headers.Add("x-playwright-launch-options", JsonSerializer.Serialize(PlaywrightSettingsProvider.LaunchOptions, new JsonSerializerOptions() { DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull })); + options.Headers = headers; + return await browserType.ConnectAsync(connectOptions!.Value.WSEndpoint, options).ConfigureAwait(false); + } + + var legacyBrowser = await ConnectBasedOnEnv(browserType); + if (legacyBrowser != null) + { + return legacyBrowser; + } + return await browserType.LaunchAsync(PlaywrightSettingsProvider.LaunchOptions).ConfigureAwait(false); + } + + // TODO: Remove at some point + private static async Task ConnectBasedOnEnv(IBrowserType browserType) + { + var accessToken = Environment.GetEnvironmentVariable("PLAYWRIGHT_SERVICE_ACCESS_TOKEN"); + var serviceUrl = Environment.GetEnvironmentVariable("PLAYWRIGHT_SERVICE_URL"); + + if (string.IsNullOrEmpty(accessToken) || string.IsNullOrEmpty(serviceUrl)) + { + return null; + } + + 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}"; + + return await browserType.ConnectAsync(wsEndpoint, new BrowserTypeConnectOptions + { + Timeout = 3 * 60 * 1000, + ExposeNetwork = exposeNetwork, + Headers = new Dictionary + { + ["Authorization"] = $"Bearer {accessToken}", + ["x-playwright-launch-options"] = JsonSerializer.Serialize(PlaywrightSettingsProvider.LaunchOptions, new JsonSerializerOptions() { DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull }) + } + }).ConfigureAwait(false); + } + + public Task ResetAsync() => Task.CompletedTask; + public Task DisposeAsync() => Browser.CloseAsync(); +} diff --git a/src/Playwright.Xunit.v3/BrowserTest.cs b/src/Playwright.Xunit.v3/BrowserTest.cs new file mode 100644 index 000000000..1529f8ce6 --- /dev/null +++ b/src/Playwright.Xunit.v3/BrowserTest.cs @@ -0,0 +1,64 @@ +/* + * MIT License + * + * Copyright (c) Microsoft Corporation. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Microsoft.Playwright.Xunit.v3; + +public class BrowserTest : PlaywrightTest +{ + public IBrowser Browser { get; internal set; } = null!; + private readonly List _contexts = new(); + + public async Task NewContext(BrowserNewContextOptions? options = null) + { + var context = await Browser.NewContextAsync(options).ConfigureAwait(false); + _contexts.Add(context); + return context; + } + + public override async ValueTask InitializeAsync() + { + await base.InitializeAsync().ConfigureAwait(false); + var service = await BrowserService.Register(this, BrowserType, await ConnectOptionsAsync()).ConfigureAwait(false); + Browser = service.Browser; + } + + public override async ValueTask DisposeAsync() + { + if (TestOk()) + { + foreach (var context in _contexts) + { + await context.CloseAsync().ConfigureAwait(false); + } + } + _contexts.Clear(); + Browser = null!; + await base.DisposeAsync().ConfigureAwait(false); + } + + public virtual Task<(string, BrowserTypeConnectOptions?)?> ConnectOptionsAsync() => Task.FromResult<(string, BrowserTypeConnectOptions?)?>(null); +} diff --git a/src/Playwright.Xunit.v3/ContextTest.cs b/src/Playwright.Xunit.v3/ContextTest.cs new file mode 100644 index 000000000..76622275e --- /dev/null +++ b/src/Playwright.Xunit.v3/ContextTest.cs @@ -0,0 +1,47 @@ +/* + * MIT License + * + * Copyright (c) Microsoft Corporation. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +using System.Threading.Tasks; + +namespace Microsoft.Playwright.Xunit.v3; + +public class ContextTest : BrowserTest +{ + public IBrowserContext Context { get; private set; } = null!; + + public override async ValueTask InitializeAsync() + { + await base.InitializeAsync().ConfigureAwait(false); + Context = await NewContext(ContextOptions()).ConfigureAwait(false); + } + + public virtual BrowserNewContextOptions ContextOptions() + { + return new() + { + Locale = "en-US", + ColorScheme = ColorScheme.Light, + }; + } +} diff --git a/src/Playwright.Xunit.v3/PageTest.cs b/src/Playwright.Xunit.v3/PageTest.cs new file mode 100644 index 000000000..abe0df2c7 --- /dev/null +++ b/src/Playwright.Xunit.v3/PageTest.cs @@ -0,0 +1,38 @@ +/* + * MIT License + * + * Copyright (c) Microsoft Corporation. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +using System.Threading.Tasks; + +namespace Microsoft.Playwright.Xunit.v3; + +public class PageTest : ContextTest +{ + public IPage Page { get; private set; } = null!; + + public override async ValueTask InitializeAsync() + { + await base.InitializeAsync().ConfigureAwait(false); + Page = await Context.NewPageAsync().ConfigureAwait(false); + } +} diff --git a/src/Playwright.Xunit.v3/Playwright.Xunit.v3.csproj b/src/Playwright.Xunit.v3/Playwright.Xunit.v3.csproj new file mode 100644 index 000000000..03d7ddacd --- /dev/null +++ b/src/Playwright.Xunit.v3/Playwright.Xunit.v3.csproj @@ -0,0 +1,43 @@ + + + + Microsoft.Playwright.Xunit.v3 + Microsoft.Playwright.Xunit.v3 + A set of helpers and fixtures to enable using Playwright in xUnit tests. + + Playwright enables reliable end-to-end testing for modern web apps. This package brings in additional helpers + and fixtures to enable using it within xUnit. + + icon.png + netstandard2.0 + true + true + Microsoft.Playwright.Xunit.v3 + 0.0.0 + True + Microsoft.Playwright.Xunit.v3 + ./nupkg + true + enable + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + diff --git a/src/Playwright.Xunit.v3/PlaywrightTest.cs b/src/Playwright.Xunit.v3/PlaywrightTest.cs new file mode 100644 index 000000000..0c99831ca --- /dev/null +++ b/src/Playwright.Xunit.v3/PlaywrightTest.cs @@ -0,0 +1,55 @@ +/* + * MIT License + * + * Copyright (c) Microsoft Corporation. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +using System.Threading.Tasks; +using Microsoft.Playwright.TestAdapter; + +namespace Microsoft.Playwright.Xunit.v3; + +public class PlaywrightTest : WorkerAwareTest +{ + public string BrowserName { get; internal set; } = null!; + + private static readonly Task _playwrightTask = Microsoft.Playwright.Playwright.CreateAsync(); + + public IPlaywright Playwright { get; private set; } = null!; + public IBrowserType BrowserType { get; private set; } = null!; + + public override async ValueTask InitializeAsync() + { + await base.InitializeAsync().ConfigureAwait(false); + Playwright = await _playwrightTask.ConfigureAwait(false); + BrowserName = PlaywrightSettingsProvider.BrowserName; + BrowserType = Playwright[BrowserName]; + Playwright.Selectors.SetTestIdAttribute("data-testid"); + } + + public static void SetDefaultExpectTimeout(float timeout) => Assertions.SetDefaultExpectTimeout(timeout); + + public ILocatorAssertions Expect(ILocator locator) => Assertions.Expect(locator); + + public IPageAssertions Expect(IPage page) => Assertions.Expect(page); + + public IAPIResponseAssertions Expect(IAPIResponse response) => Assertions.Expect(response); +} diff --git a/src/Playwright.Xunit.v3/WorkerAwareTest.cs b/src/Playwright.Xunit.v3/WorkerAwareTest.cs new file mode 100644 index 000000000..e07b62539 --- /dev/null +++ b/src/Playwright.Xunit.v3/WorkerAwareTest.cs @@ -0,0 +1,116 @@ +/* + * MIT License + * + * Copyright (c) Microsoft Corporation. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Playwright.Core; +using Microsoft.Playwright.TestAdapter; +using Xunit; + +namespace Microsoft.Playwright.Xunit.v3; + +public class WorkerAwareTest : IAsyncLifetime +{ + private static readonly ConcurrentStack _allWorkers = new(); + private Worker _currentWorker = null!; + + public WorkerAwareTest() + { + PlaywrightSettingsProvider.LoadViaEnvIfNeeded(); + } + + internal class Worker + { + private static int _lastWorkedIndex = 0; + public int WorkerIndex = Interlocked.Increment(ref _lastWorkedIndex); + public Dictionary Services = []; + } + + public int WorkerIndex { get; internal set; } + + public async Task RegisterService(string name, Func> factory) where T : class, IWorkerService + { + if (!_currentWorker.Services.ContainsKey(name)) + { + _currentWorker.Services[name] = await factory().ConfigureAwait(false); + } + + return (_currentWorker.Services[name] as T)!; + } + + public virtual ValueTask InitializeAsync() + { + if (!_allWorkers.TryPop(out _currentWorker!)) + { + _currentWorker = new(); + } + WorkerIndex = _currentWorker.WorkerIndex; + if (PlaywrightSettingsProvider.ExpectTimeout.HasValue) + { + AssertionsBase.SetDefaultTimeout(PlaywrightSettingsProvider.ExpectTimeout.Value); + } + return new ValueTask(); + } + + public async virtual ValueTask DisposeAsync() + { + if (TestOk()) + { + foreach (var kv in _currentWorker.Services) + { + await kv.Value.ResetAsync().ConfigureAwait(false); + } + _allWorkers.Push(_currentWorker); + } + else + { + foreach (var kv in _currentWorker.Services) + { + await kv.Value.DisposeAsync().ConfigureAwait(false); + } + _currentWorker.Services.Clear(); + } + } + + protected bool TestOk() + { + // Test is still running. + if (TestContext.Current.TestState == null) + { + return false; + } + return + TestContext.Current.TestState.Result == TestResult.Passed || + TestContext.Current.TestState.Result == TestResult.Skipped; + } +} + +public interface IWorkerService +{ + public Task ResetAsync(); + public Task DisposeAsync(); +} diff --git a/src/Playwright.sln b/src/Playwright.sln index a8da083c6..ea7bf3258 100644 --- a/src/Playwright.sln +++ b/src/Playwright.sln @@ -34,6 +34,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Playwright.TestingHarnessTe EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Playwright.Xunit", "Playwright.Xunit\Playwright.Xunit.csproj", "{7E427229-793C-44E5-B90E-FB8E322066FA}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Playwright.Xunit.v3", "Playwright.Xunit.v3\Playwright.Xunit.v3.csproj", "{6CFF37A8-7C6B-4203-9BF7-3A83752653E8}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -80,6 +82,10 @@ Global {7E427229-793C-44E5-B90E-FB8E322066FA}.Debug|Any CPU.Build.0 = Debug|Any CPU {7E427229-793C-44E5-B90E-FB8E322066FA}.Release|Any CPU.ActiveCfg = Release|Any CPU {7E427229-793C-44E5-B90E-FB8E322066FA}.Release|Any CPU.Build.0 = Release|Any CPU + {6CFF37A8-7C6B-4203-9BF7-3A83752653E8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6CFF37A8-7C6B-4203-9BF7-3A83752653E8}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6CFF37A8-7C6B-4203-9BF7-3A83752653E8}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6CFF37A8-7C6B-4203-9BF7-3A83752653E8}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/src/Playwright/Core/AssertionsBase.cs b/src/Playwright/Core/AssertionsBase.cs index f870f9103..928156de1 100644 --- a/src/Playwright/Core/AssertionsBase.cs +++ b/src/Playwright/Core/AssertionsBase.cs @@ -35,6 +35,7 @@ [assembly: InternalsVisibleTo("Microsoft.Playwright.MSTest, PublicKey=0024000004800000940000000602000000240000525341310004000001000100059a04ca5ca77c9b4eb2addd1afe3f8464b20ee6aefe73b8c23c0e6ca278d1a378b33382e7e18d4aa8300dd22d81f146e528d88368f73a288e5b8157da9710fe6f9fa9911fb786193f983408c5ebae0b1ba5d1d00111af2816f5db55871db03d7536f4a7a6c5152d630c1e1886b1a0fb68ba5e7f64a7f24ac372090889be2ffb")] [assembly: InternalsVisibleTo("Microsoft.Playwright.NUnit, PublicKey=0024000004800000940000000602000000240000525341310004000001000100059a04ca5ca77c9b4eb2addd1afe3f8464b20ee6aefe73b8c23c0e6ca278d1a378b33382e7e18d4aa8300dd22d81f146e528d88368f73a288e5b8157da9710fe6f9fa9911fb786193f983408c5ebae0b1ba5d1d00111af2816f5db55871db03d7536f4a7a6c5152d630c1e1886b1a0fb68ba5e7f64a7f24ac372090889be2ffb")] [assembly: InternalsVisibleTo("Microsoft.Playwright.Xunit, PublicKey=0024000004800000940000000602000000240000525341310004000001000100059a04ca5ca77c9b4eb2addd1afe3f8464b20ee6aefe73b8c23c0e6ca278d1a378b33382e7e18d4aa8300dd22d81f146e528d88368f73a288e5b8157da9710fe6f9fa9911fb786193f983408c5ebae0b1ba5d1d00111af2816f5db55871db03d7536f4a7a6c5152d630c1e1886b1a0fb68ba5e7f64a7f24ac372090889be2ffb")] +[assembly: InternalsVisibleTo("Microsoft.Playwright.Xunit.v3, PublicKey=0024000004800000940000000602000000240000525341310004000001000100059a04ca5ca77c9b4eb2addd1afe3f8464b20ee6aefe73b8c23c0e6ca278d1a378b33382e7e18d4aa8300dd22d81f146e528d88368f73a288e5b8157da9710fe6f9fa9911fb786193f983408c5ebae0b1ba5d1d00111af2816f5db55871db03d7536f4a7a6c5152d630c1e1886b1a0fb68ba5e7f64a7f24ac372090889be2ffb")] namespace Microsoft.Playwright.Core;