diff --git a/src/Playwright.MSTest.v4/Playwright.MSTest.v4.csproj b/src/Playwright.MSTest.v4/Playwright.MSTest.v4.csproj new file mode 100644 index 000000000..21573913a --- /dev/null +++ b/src/Playwright.MSTest.v4/Playwright.MSTest.v4.csproj @@ -0,0 +1,50 @@ + + + + Microsoft.Playwright.MSTest.v4 + Microsoft.Playwright.MSTest.v4 + A set of helpers and fixtures to enable using Playwright in MSTest 4 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 MSTest. + + icon.png + netstandard2.0 + true + true + Microsoft.Playwright.MSTest + 0.0.0 + True + Microsoft.Playwright.MSTest.v4 + ./nupkg + true + enable + + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + %(RecursiveDir)%(FileName)%(Extension) + + + diff --git a/src/Playwright.TestingHarnessTest/Playwright.TestingHarnessTest.csproj b/src/Playwright.TestingHarnessTest/Playwright.TestingHarnessTest.csproj index b6670ab1c..2bc474ac1 100644 --- a/src/Playwright.TestingHarnessTest/Playwright.TestingHarnessTest.csproj +++ b/src/Playwright.TestingHarnessTest/Playwright.TestingHarnessTest.csproj @@ -18,6 +18,11 @@ + + + + + diff --git a/src/Playwright.TestingHarnessTest/tests/baseTest.ts b/src/Playwright.TestingHarnessTest/tests/baseTest.ts index c162b3411..55e850795 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' | 'xunit.v3'; + testMode: 'nunit' | 'mstest' | 'mstest.v4' | 'xunit' | 'xunit.v3'; runTest: (files: Record, command: string, env?: NodeJS.ProcessEnv) => Promise; launchServer: (options: { port: number }) => Promise; server: SimpleServer; diff --git a/src/Playwright.TestingHarnessTest/tests/mstest.v4/basic.spec.ts b/src/Playwright.TestingHarnessTest/tests/mstest.v4/basic.spec.ts new file mode 100644 index 000000000..4819349f2 --- /dev/null +++ b/src/Playwright.TestingHarnessTest/tests/mstest.v4/basic.spec.ts @@ -0,0 +1,537 @@ +/* + * 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: 'mstest.v4' }); + +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.MSTest; + using Microsoft.VisualStudio.TestTools.UnitTesting; + + namespace Playwright.TestingHarnessTest.MSTest; + + [TestClass] + public class : PageTest + { + [TestMethod] + 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); + expect(result.stderr).toContain("pw:api") + expect(result.stderr).toContain("element is not enabled") + expect(result.stderr).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.MSTest; + using Microsoft.VisualStudio.TestTools.UnitTesting; + + namespace Playwright.TestingHarnessTest.MSTest; + + [TestClass] + public class : PageTest + { + [TestMethod] + public async Task Test() + { + await Page.GotoAsync("about:blank"); + Console.WriteLine("BrowserName: " + BrowserName); + Console.WriteLine("BrowserType: " + BrowserType.Name); + Console.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.MSTest; + using Microsoft.VisualStudio.TestTools.UnitTesting; + + namespace Playwright.TestingHarnessTest.MSTest; + + [TestClass] + public class : PageTest + { + [TestMethod] + public async Task Test() + { + await Page.GotoAsync("about:blank"); + Console.WriteLine("BrowserName: " + BrowserName); + Console.WriteLine("BrowserType: " + BrowserType.Name); + Console.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.MSTest; + using Microsoft.VisualStudio.TestTools.UnitTesting; + + namespace Playwright.TestingHarnessTest.MSTest; + + [TestClass] + public class : PageTest + { + [TestMethod] + public async Task Test() + { + await Page.GotoAsync("about:blank"); + Console.WriteLine("BrowserName: " + BrowserName); + Console.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.MSTest; + using Microsoft.VisualStudio.TestTools.UnitTesting; + + namespace Playwright.TestingHarnessTest.MSTest; + + [TestClass] + public class : PageTest + { + [TestMethod] + public async Task Test() + { + await Page.GotoAsync("about:blank"); + Console.WriteLine("BrowserName: " + BrowserName); + Console.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, server }) => { + const result = await runTest({ + 'ExampleTests.cs': ` + using System; + using System.Threading.Tasks; + using Microsoft.Playwright.MSTest; + using Microsoft.VisualStudio.TestTools.UnitTesting; + + namespace Playwright.TestingHarnessTest.MSTest; + + [TestClass] + public class : PageTest + { + [TestMethod] + public async Task Test() + { + Console.WriteLine("User-Agent: " + await Page.EvaluateAsync("() => navigator.userAgent")); + await Page.GotoAsync("${server.EMPTY_PAGE}"); + } + }`, + '.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 === server.EMPTY_PAGE)!; + expect(url).toBe(server.EMPTY_PAGE); + 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.MSTest; + using Microsoft.VisualStudio.TestTools.UnitTesting; + + namespace Playwright.TestingHarnessTest.MSTest; + + [TestClass] + public class : PageTest + { + [TestMethod] + public async Task Test() + { + Console.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, server }) => { + const result = await runTest({ + 'ExampleTests.cs': ` + using System; + using System.Collections.Generic; + using System.Threading.Tasks; + using Microsoft.Playwright; + using Microsoft.Playwright.MSTest; + using Microsoft.VisualStudio.TestTools.UnitTesting; + + namespace Playwright.TestingHarnessTest.MSTest; + + [TestClass] + public class : PageTest + { + [TestMethod] + public async Task Test() + { + await Page.GotoAsync("about:blank"); + + Assert.IsFalse(await Page.EvaluateAsync("() => matchMedia('(prefers-color-scheme: light)').matches")); + Assert.IsTrue(await Page.EvaluateAsync("() => matchMedia('(prefers-color-scheme: dark)').matches")); + + Assert.AreEqual(1920, await Page.EvaluateAsync("() => window.innerWidth")); + Assert.AreEqual(1080, await Page.EvaluateAsync("() => window.innerHeight")); + + Assert.AreEqual("Foobar", await Page.EvaluateAsync("() => navigator.userAgent")); + + var response = await Page.GotoAsync("${server.EMPTY_PAGE}"); + Assert.AreEqual(await response.Request.HeaderValueAsync("Kekstar"), "KekStarValue"); + } + + 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.MSTest; + using Microsoft.VisualStudio.TestTools.UnitTesting; + + namespace Playwright.TestingHarnessTest.MSTest; + + [TestClass] + public class : PageTest + { + [TestMethod] + 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.Threading.Tasks; + using Microsoft.Playwright.MSTest; + using Microsoft.VisualStudio.TestTools.UnitTesting; + + namespace Playwright.TestingHarnessTest.MSTest; + + [TestClass] + public class : PageTest + { + [TestMethod] + 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("Expect \"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.Threading.Tasks; + using Microsoft.Playwright.MSTest; + using Microsoft.VisualStudio.TestTools.UnitTesting; + + namespace Playwright.TestingHarnessTest.MSTest; + + [TestClass] + public class : PageTest + { + [TestMethod] + 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("Expect \"ToHaveTextAsync\" with timeout 100ms") + }); + test('should be able to override it via the global settings', async ({ runTest }) => { + const result = await runTest({ + 'ExampleTests.cs': ` + using System; + using System.Threading.Tasks; + using Microsoft.Playwright.MSTest; + using Microsoft.VisualStudio.TestTools.UnitTesting; + + namespace Playwright.TestingHarnessTest.MSTest; + + [TestClass] + public class : PageTest + { + [TestMethod] + 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("Expect \"ToHaveTextAsync\" with timeout 123ms") + }); +}); + +test.describe('ConnectOptions', () => { + const ExampleTestWithConnectOptions = ` + using System; + using System.Threading.Tasks; + using Microsoft.Playwright; + using Microsoft.Playwright.MSTest; + using Microsoft.VisualStudio.TestTools.UnitTesting; + + namespace Playwright.TestingHarnessTest.MSTest; + + [TestClass] + public class : PageTest + { + [TestMethod] + 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.sln b/src/Playwright.sln index ea7bf3258..2c6dabd1c 100644 --- a/src/Playwright.sln +++ b/src/Playwright.sln @@ -1,4 +1,5 @@ -Microsoft Visual Studio Solution File, Format Version 12.00 + +Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 17 VisualStudioVersion = 17.0.31516.353 MinimumVisualStudioVersion = 10.0.40219.1 @@ -36,6 +37,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Playwright.Xunit", "Playwri EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Playwright.Xunit.v3", "Playwright.Xunit.v3\Playwright.Xunit.v3.csproj", "{6CFF37A8-7C6B-4203-9BF7-3A83752653E8}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Playwright.MSTest.v4", "Playwright.MSTest.v4\Playwright.MSTest.v4.csproj", "{391F8C08-8C76-4D26-BBCF-8A3E00252075}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -86,6 +89,10 @@ Global {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 + {391F8C08-8C76-4D26-BBCF-8A3E00252075}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {391F8C08-8C76-4D26-BBCF-8A3E00252075}.Debug|Any CPU.Build.0 = Debug|Any CPU + {391F8C08-8C76-4D26-BBCF-8A3E00252075}.Release|Any CPU.ActiveCfg = Release|Any CPU + {391F8C08-8C76-4D26-BBCF-8A3E00252075}.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 e57e1a191..22ed2b791 100644 --- a/src/Playwright/Core/AssertionsBase.cs +++ b/src/Playwright/Core/AssertionsBase.cs @@ -33,6 +33,7 @@ using Microsoft.Playwright.Transport.Protocol; [assembly: InternalsVisibleTo("Microsoft.Playwright.MSTest, PublicKey=0024000004800000940000000602000000240000525341310004000001000100059a04ca5ca77c9b4eb2addd1afe3f8464b20ee6aefe73b8c23c0e6ca278d1a378b33382e7e18d4aa8300dd22d81f146e528d88368f73a288e5b8157da9710fe6f9fa9911fb786193f983408c5ebae0b1ba5d1d00111af2816f5db55871db03d7536f4a7a6c5152d630c1e1886b1a0fb68ba5e7f64a7f24ac372090889be2ffb")] +[assembly: InternalsVisibleTo("Microsoft.Playwright.MSTest.v4, PublicKey=0024000004800000940000000602000000240000525341310004000001000100059a04ca5ca77c9b4eb2addd1afe3f8464b20ee6aefe73b8c23c0e6ca278d1a378b33382e7e18d4aa8300dd22d81f146e528d88368f73a288e5b8157da9710fe6f9fa9911fb786193f983408c5ebae0b1ba5d1d00111af2816f5db55871db03d7536f4a7a6c5152d630c1e1886b1a0fb68ba5e7f64a7f24ac372090889be2ffb")] [assembly: InternalsVisibleTo("Microsoft.Playwright.NUnit, PublicKey=0024000004800000940000000602000000240000525341310004000001000100059a04ca5ca77c9b4eb2addd1afe3f8464b20ee6aefe73b8c23c0e6ca278d1a378b33382e7e18d4aa8300dd22d81f146e528d88368f73a288e5b8157da9710fe6f9fa9911fb786193f983408c5ebae0b1ba5d1d00111af2816f5db55871db03d7536f4a7a6c5152d630c1e1886b1a0fb68ba5e7f64a7f24ac372090889be2ffb")] [assembly: InternalsVisibleTo("Microsoft.Playwright.Xunit, PublicKey=0024000004800000940000000602000000240000525341310004000001000100059a04ca5ca77c9b4eb2addd1afe3f8464b20ee6aefe73b8c23c0e6ca278d1a378b33382e7e18d4aa8300dd22d81f146e528d88368f73a288e5b8157da9710fe6f9fa9911fb786193f983408c5ebae0b1ba5d1d00111af2816f5db55871db03d7536f4a7a6c5152d630c1e1886b1a0fb68ba5e7f64a7f24ac372090889be2ffb")] [assembly: InternalsVisibleTo("Microsoft.Playwright.Xunit.v3, PublicKey=0024000004800000940000000602000000240000525341310004000001000100059a04ca5ca77c9b4eb2addd1afe3f8464b20ee6aefe73b8c23c0e6ca278d1a378b33382e7e18d4aa8300dd22d81f146e528d88368f73a288e5b8157da9710fe6f9fa9911fb786193f983408c5ebae0b1ba5d1d00111af2816f5db55871db03d7536f4a7a6c5152d630c1e1886b1a0fb68ba5e7f64a7f24ac372090889be2ffb")]