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.0 12 + - - - - - + + + + + + + + + + + + + + + - - - + + + + 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;