diff --git a/src/Playwright.TestingHarnessTest/Playwright.TestingHarnessTest.csproj b/src/Playwright.TestingHarnessTest/Playwright.TestingHarnessTest.csproj
index 33a1509d7..6ee357481 100644
--- a/src/Playwright.TestingHarnessTest/Playwright.TestingHarnessTest.csproj
+++ b/src/Playwright.TestingHarnessTest/Playwright.TestingHarnessTest.csproj
@@ -6,21 +6,33 @@
0.0.012
+
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
+
+
+
+
diff --git a/src/Playwright.TestingHarnessTest/tests/baseTest.ts b/src/Playwright.TestingHarnessTest/tests/baseTest.ts
index 6ebcc5b4e..bded66f6f 100644
--- a/src/Playwright.TestingHarnessTest/tests/baseTest.ts
+++ b/src/Playwright.TestingHarnessTest/tests/baseTest.ts
@@ -16,9 +16,11 @@ type RunResult = {
}
export const test = base.extend<{
+ testMode: 'nunit' | 'mstest' | 'xunit' | 'xunit.v3';
runTest: (files: Record, command: string, env?: NodeJS.ProcessEnv) => Promise;
}>({
- runTest: async ({ }, use, testInfo) => {
+ testMode: null,
+ runTest: async ({ testMode }, use, testInfo) => {
const testResults: RunResult[] = [];
await use(async (files, command, env) => {
const testDir = testInfo.outputPath();
@@ -34,7 +36,8 @@ export const test = base.extend<{
env: {
...process.env,
...env,
- NODE_OPTIONS: undefined
+ NODE_OPTIONS: undefined,
+ TEST_MODE: testMode,
},
stdio: 'pipe',
});
diff --git a/src/Playwright.TestingHarnessTest/tests/mstest/basic.spec.ts b/src/Playwright.TestingHarnessTest/tests/mstest/basic.spec.ts
index be7501827..c7ce63bfe 100644
--- a/src/Playwright.TestingHarnessTest/tests/mstest/basic.spec.ts
+++ b/src/Playwright.TestingHarnessTest/tests/mstest/basic.spec.ts
@@ -26,6 +26,8 @@ import http from 'http';
import { test, expect } from '../baseTest';
import httpProxy from 'http-proxy';
+test.use({testMode: 'mstest'});
+
test('should be able to forward DEBUG=pw:api env var', async ({ runTest }) => {
const result = await runTest({
'ExampleTests.cs': `
diff --git a/src/Playwright.TestingHarnessTest/tests/nunit/basic.spec.ts b/src/Playwright.TestingHarnessTest/tests/nunit/basic.spec.ts
index 25823c075..440010c29 100644
--- a/src/Playwright.TestingHarnessTest/tests/nunit/basic.spec.ts
+++ b/src/Playwright.TestingHarnessTest/tests/nunit/basic.spec.ts
@@ -26,6 +26,8 @@ import http from 'http';
import { test, expect } from '../baseTest';
import httpProxy from 'http-proxy';
+test.use({testMode: 'nunit'});
+
test('should be able to forward DEBUG=pw:api env var', async ({ runTest }) => {
const result = await runTest({
'ExampleTests.cs': `
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..441d2fcd1
--- /dev/null
+++ b/src/Playwright.TestingHarnessTest/tests/xunit.v3/basic.spec.ts
@@ -0,0 +1,554 @@
+/*
+ * 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 http from 'http';
+import { test, expect } from '../baseTest';
+import httpProxy from 'http-proxy';
+
+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', {
+ // Workaround until we can enable https://github.com/xunit/xunit/issues/1730#issuecomment-2330825763 assembly wide.
+ PWAPI_TO_STDOUT: '1',
+ });
+ 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 }) => {
+ const httpServer = http.createServer((req, res) => {
+ res.end('hello world!')
+ }).listen(3129);
+ const proxyServer = httpProxy.createProxyServer({
+ auth: 'user:pwd',
+ target: 'http://localhost:3129',
+ }).listen(3128);
+
+ const waitForProxyRequest = new Promise<[string, string]>((resolve) => {
+ proxyServer.once('proxyReq', (proxyReq, req, res, options) => {
+ const authHeader = proxyReq.getHeader('authorization') as string;
+ const auth = Buffer.from(authHeader.split(' ')[1], 'base64').toString();
+ resolve([req.url, auth]);
+ });
+ })
+
+ 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
+
+ http://127.0.0.1:3128
+ 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] = await waitForProxyRequest;
+ expect(url).toBe('http://example.com/');
+ expect(auth).toBe('user:pwd');
+
+ proxyServer.close();
+ httpServer.close();
+});
+
+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")
+ });
+});
diff --git a/src/Playwright.TestingHarnessTest/tests/xunit/basic.spec.ts b/src/Playwright.TestingHarnessTest/tests/xunit/basic.spec.ts
index 395f276ca..48be737a9 100644
--- a/src/Playwright.TestingHarnessTest/tests/xunit/basic.spec.ts
+++ b/src/Playwright.TestingHarnessTest/tests/xunit/basic.spec.ts
@@ -26,6 +26,8 @@ import http from 'http';
import { test, expect } from '../baseTest';
import httpProxy from 'http-proxy';
+test.use({testMode: 'xunit'});
+
test('should be able to forward DEBUG=pw:api env var', async ({ runTest }) => {
const result = await runTest({
'ExampleTests.cs': `
diff --git a/src/Playwright.Xunit.v3/BrowserService.cs b/src/Playwright.Xunit.v3/BrowserService.cs
new file mode 100644
index 000000000..61309c1a7
--- /dev/null
+++ b/src/Playwright.Xunit.v3/BrowserService.cs
@@ -0,0 +1,80 @@
+/*
+ * 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.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)
+ {
+ return test.RegisterService("Browser", async () => new BrowserService(await CreateBrowser(browserType).ConfigureAwait(false)));
+ }
+
+ private static async Task CreateBrowser(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 await browserType.LaunchAsync(PlaywrightSettingsProvider.LaunchOptions).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}",
+ ["x-playwright-launch-options"] = JsonSerializer.Serialize(PlaywrightSettingsProvider.LaunchOptions, new JsonSerializerOptions() { DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull })
+ }
+ };
+
+ return await browserType.ConnectAsync(wsEndpoint, connectOptions).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..fbf03f8ea
--- /dev/null
+++ b/src/Playwright.Xunit.v3/BrowserTest.cs
@@ -0,0 +1,62 @@
+/*
+ * 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).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);
+ }
+}
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..b9c71b2a0
--- /dev/null
+++ b/src/Playwright.Xunit.v3/PlaywrightTest.cs
@@ -0,0 +1,56 @@
+/*
+ * 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);
+ System.Console.Error.WriteLine("Reading browser name from playwrightTest: " + PlaywrightSettingsProvider.BrowserName + " with pid " + System.Diagnostics.Process.GetCurrentProcess().Id);
+ 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..ed7858861
--- /dev/null
+++ b/src/Playwright.Xunit.v3/WorkerAwareTest.cs
@@ -0,0 +1,117 @@
+/*
+ * 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!;
+
+ internal class Worker
+ {
+ private static int _lastWorkedIndex = 0;
+ public int WorkerIndex = Interlocked.Increment(ref _lastWorkedIndex);
+ public Dictionary Services = [];
+ }
+
+ [Class]
+ public WorkerAwareTest()
+ {
+ Console.WriteLine("WorkerAwareTest::WorkerAwareTest: " + PlaywrightSettingsProvider.BrowserName);
+ }
+
+ 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;