From 1b95975eb28bbc8d96d182d644d30fc9b2464f66 Mon Sep 17 00:00:00 2001 From: "microsoft-playwright-automation[bot]" <203992400+microsoft-playwright-automation[bot]@users.noreply.github.com> Date: Wed, 2 Sep 2026 10:23:20 +0000 Subject: [PATCH] feat(roll): roll to ToT Playwright (02-09-26) --- dotnet/docs/api-testing.mdx | 1271 ++++++++++++++++++- dotnet/docs/api/class-apirequestcontext.mdx | 3 + dotnet/docs/api/class-browser.mdx | 6 - dotnet/docs/api/class-browsercontext.mdx | 13 +- dotnet/docs/api/class-browsertype.mdx | 3 - dotnet/docs/api/class-frame.mdx | 41 +- dotnet/docs/api/class-framelocator.mdx | 16 + dotnet/docs/api/class-locator.mdx | 32 +- dotnet/docs/api/class-page.mdx | 41 +- dotnet/docs/auth.mdx | 3 +- dotnet/docs/locators.mdx | 4 +- dotnet/docs/other-locators.mdx | 5 + dotnet/docs/webview2.mdx | 278 +++- java/docs/api-testing.mdx | 32 +- java/docs/api/class-apirequestcontext.mdx | 3 + java/docs/api/class-browser.mdx | 6 - java/docs/api/class-browsercontext.mdx | 13 +- java/docs/api/class-browsertype.mdx | 3 - java/docs/api/class-frame.mdx | 41 +- java/docs/api/class-framelocator.mdx | 16 + java/docs/api/class-locator.mdx | 32 +- java/docs/api/class-page.mdx | 41 +- java/docs/auth.mdx | 3 +- java/docs/locators.mdx | 4 +- java/docs/other-locators.mdx | 5 + nodejs/docs/api/class-androiddevice.mdx | 3 - nodejs/docs/api/class-apirequestcontext.mdx | 3 + nodejs/docs/api/class-browser.mdx | 6 - nodejs/docs/api/class-browsercontext.mdx | 85 +- nodejs/docs/api/class-browsertype.mdx | 3 - nodejs/docs/api/class-frame.mdx | 41 +- nodejs/docs/api/class-framelocator.mdx | 16 + nodejs/docs/api/class-fullconfig.mdx | 2 +- nodejs/docs/api/class-locator.mdx | 32 +- nodejs/docs/api/class-page.mdx | 41 +- nodejs/docs/api/class-testconfig.mdx | 2 +- nodejs/docs/api/class-testoptions.mdx | 23 - nodejs/docs/api/class-teststep.mdx | 27 +- nodejs/docs/api/class-teststepinfo.mdx | 26 + nodejs/docs/locators.mdx | 4 +- nodejs/docs/other-locators.mdx | 5 + nodejs/docs/test-reporters.mdx | 18 +- python/docs/api/class-apirequestcontext.mdx | 3 + python/docs/api/class-browser.mdx | 6 - python/docs/api/class-browsercontext.mdx | 13 +- python/docs/api/class-browsertype.mdx | 3 - python/docs/api/class-frame.mdx | 82 +- python/docs/api/class-framelocator.mdx | 40 + python/docs/api/class-locator.mdx | 52 +- python/docs/api/class-page.mdx | 82 +- python/docs/auth.mdx | 6 +- python/docs/locators.mdx | 6 +- python/docs/other-locators.mdx | 5 + 53 files changed, 2060 insertions(+), 490 deletions(-) diff --git a/dotnet/docs/api-testing.mdx b/dotnet/docs/api-testing.mdx index 86601ec14b..fb338eba26 100644 --- a/dotnet/docs/api-testing.mdx +++ b/dotnet/docs/api-testing.mdx @@ -17,7 +17,7 @@ Sometimes you may want to send requests to the server directly from .NET without All of that could be achieved via [APIRequestContext] methods. -The following examples rely on the [`Microsoft.Playwright.MSTest`](./test-runners.mdx) package which creates a Playwright and Page instance for each test. +The following examples rely on the MSTest, NUnit, xUnit or xUnit v3 [base classes](./test-runners.mdx) which create a Playwright and Page instance for each test. ## Writing API Test @@ -32,7 +32,760 @@ The following example demonstrates how to use Playwright to test issues creation GitHub API requires authorization, so we'll configure the token once for all tests. While at it, we'll also set the `baseURL` to simplify the tests. + + + + +```csharp +using Microsoft.Playwright; +using Microsoft.Playwright.MSTest; + +namespace PlaywrightTests; + +[TestClass] +public class TestGitHubAPI : PlaywrightTest +{ + static string? API_TOKEN = Environment.GetEnvironmentVariable("GITHUB_API_TOKEN"); + + private IAPIRequestContext Request = null!; + + [TestInitialize] + public async Task SetUpAPITesting() + { + await CreateAPIRequestContext(); + } + + private async Task CreateAPIRequestContext() + { + var headers = new Dictionary(); + // We set this header per GitHub guidelines. + headers.Add("Accept", "application/vnd.github.v3+json"); + // Add authorization token to all requests. + // Assuming personal access token available in the environment. + headers.Add("Authorization", "token " + API_TOKEN); + + Request = await this.Playwright.APIRequest.NewContextAsync(new() { + // All requests we send go to this API endpoint. + BaseURL = "https://api.github.com", + ExtraHTTPHeaders = headers, + }); + } + + [TestCleanup] + public async Task TearDownAPITesting() + { + await Request.DisposeAsync(); + } +} +``` + + + + + +```csharp +using Microsoft.Playwright; +using Microsoft.Playwright.NUnit; +using NUnit.Framework; + +namespace PlaywrightTests; + +[Parallelizable(ParallelScope.Self)] +[TestFixture] +public class TestGitHubAPI : PlaywrightTest +{ + static string? API_TOKEN = Environment.GetEnvironmentVariable("GITHUB_API_TOKEN"); + + private IAPIRequestContext Request = null!; + + [SetUp] + public async Task SetUpAPITesting() + { + await CreateAPIRequestContext(); + } + + private async Task CreateAPIRequestContext() + { + var headers = new Dictionary(); + // We set this header per GitHub guidelines. + headers.Add("Accept", "application/vnd.github.v3+json"); + // Add authorization token to all requests. + // Assuming personal access token available in the environment. + headers.Add("Authorization", "token " + API_TOKEN); + + Request = await this.Playwright.APIRequest.NewContextAsync(new() { + // All requests we send go to this API endpoint. + BaseURL = "https://api.github.com", + ExtraHTTPHeaders = headers, + }); + } + + [TearDown] + public async Task TearDownAPITesting() + { + await Request.DisposeAsync(); + } +} +``` + + + + + +```csharp +using Microsoft.Playwright; +using Microsoft.Playwright.Xunit; + +namespace PlaywrightTests; + +public class TestGitHubAPI : PlaywrightTest +{ + static string? API_TOKEN = Environment.GetEnvironmentVariable("GITHUB_API_TOKEN"); + + private IAPIRequestContext Request = null!; + + public override async Task InitializeAsync() + { + await base.InitializeAsync(); + await CreateAPIRequestContext(); + } + + private async Task CreateAPIRequestContext() + { + var headers = new Dictionary(); + // We set this header per GitHub guidelines. + headers.Add("Accept", "application/vnd.github.v3+json"); + // Add authorization token to all requests. + // Assuming personal access token available in the environment. + headers.Add("Authorization", "token " + API_TOKEN); + + Request = await this.Playwright.APIRequest.NewContextAsync(new() { + // All requests we send go to this API endpoint. + BaseURL = "https://api.github.com", + ExtraHTTPHeaders = headers, + }); + } + + public override async Task DisposeAsync() + { + await Request.DisposeAsync(); + await base.DisposeAsync(); + } +} +``` + + + + + +```csharp +using Microsoft.Playwright; +using Microsoft.Playwright.Xunit.v3; + +namespace PlaywrightTests; + +public class TestGitHubAPI : PlaywrightTest +{ + static string? API_TOKEN = Environment.GetEnvironmentVariable("GITHUB_API_TOKEN"); + + private IAPIRequestContext Request = null!; + + public override async Task InitializeAsync() + { + await base.InitializeAsync(); + await CreateAPIRequestContext(); + } + + private async Task CreateAPIRequestContext() + { + var headers = new Dictionary(); + // We set this header per GitHub guidelines. + headers.Add("Accept", "application/vnd.github.v3+json"); + // Add authorization token to all requests. + // Assuming personal access token available in the environment. + headers.Add("Authorization", "token " + API_TOKEN); + + Request = await this.Playwright.APIRequest.NewContextAsync(new() { + // All requests we send go to this API endpoint. + BaseURL = "https://api.github.com", + ExtraHTTPHeaders = headers, + }); + } + + public override async Task DisposeAsync() + { + await Request.DisposeAsync(); + await base.DisposeAsync(); + } +} +``` + + + + + +### Write tests + +Now that we initialized request object we can add a few tests that will create new issues in the repository. + + + + + +```csharp +using System.Text.Json; +using Microsoft.Playwright; +using Microsoft.Playwright.MSTest; + +namespace PlaywrightTests; + +[TestClass] +public class TestGitHubAPI : PlaywrightTest +{ + static string REPO = "test"; + static string USER = Environment.GetEnvironmentVariable("GITHUB_USER"); + static string? API_TOKEN = Environment.GetEnvironmentVariable("GITHUB_API_TOKEN"); + + private IAPIRequestContext Request = null!; + + [TestMethod] + public async Task ShouldCreateBugReport() + { + var data = new Dictionary + { + { "title", "[Bug] report 1" }, + { "body", "Bug description" } + }; + var newIssue = await Request.PostAsync("/repos/" + USER + "/" + REPO + "/issues", new() { DataObject = data }); + await Expect(newIssue).ToBeOKAsync(); + + var issues = await Request.GetAsync("/repos/" + USER + "/" + REPO + "/issues"); + await Expect(newIssue).ToBeOKAsync(); + var issuesJsonResponse = await issues.JsonAsync(); + JsonElement? issue = null; + foreach (JsonElement issueObj in issuesJsonResponse?.EnumerateArray()) + { + if (issueObj.TryGetProperty("title", out var title) == true) + { + if (title.GetString() == "[Bug] report 1") + { + issue = issueObj; + } + } + } + Assert.IsNotNull(issue); + Assert.AreEqual("Bug description", issue?.GetProperty("body").GetString()); + } + + [TestMethod] + public async Task ShouldCreateFeatureRequests() + { + var data = new Dictionary + { + { "title", "[Feature] request 1" }, + { "body", "Feature description" } + }; + var newIssue = await Request.PostAsync("/repos/" + USER + "/" + REPO + "/issues", new() { DataObject = data }); + await Expect(newIssue).ToBeOKAsync(); + + var issues = await Request.GetAsync("/repos/" + USER + "/" + REPO + "/issues"); + await Expect(newIssue).ToBeOKAsync(); + var issuesJsonResponse = await issues.JsonAsync(); + + JsonElement? issue = null; + foreach (JsonElement issueObj in issuesJsonResponse?.EnumerateArray()) + { + if (issueObj.TryGetProperty("title", out var title) == true) + { + if (title.GetString() == "[Feature] request 1") + { + issue = issueObj; + } + } + } + Assert.IsNotNull(issue); + Assert.AreEqual("Feature description", issue?.GetProperty("body").GetString()); + } + + // ... +} +``` + + + + + +```csharp +using System.Text.Json; +using Microsoft.Playwright; +using Microsoft.Playwright.NUnit; +using NUnit.Framework; + +namespace PlaywrightTests; + +[Parallelizable(ParallelScope.Self)] +[TestFixture] +public class TestGitHubAPI : PlaywrightTest +{ + static string REPO = "test"; + static string USER = Environment.GetEnvironmentVariable("GITHUB_USER"); + static string? API_TOKEN = Environment.GetEnvironmentVariable("GITHUB_API_TOKEN"); + + private IAPIRequestContext Request = null!; + + [Test] + public async Task ShouldCreateBugReport() + { + var data = new Dictionary + { + { "title", "[Bug] report 1" }, + { "body", "Bug description" } + }; + var newIssue = await Request.PostAsync("/repos/" + USER + "/" + REPO + "/issues", new() { DataObject = data }); + await Expect(newIssue).ToBeOKAsync(); + + var issues = await Request.GetAsync("/repos/" + USER + "/" + REPO + "/issues"); + await Expect(newIssue).ToBeOKAsync(); + var issuesJsonResponse = await issues.JsonAsync(); + JsonElement? issue = null; + foreach (JsonElement issueObj in issuesJsonResponse?.EnumerateArray()) + { + if (issueObj.TryGetProperty("title", out var title) == true) + { + if (title.GetString() == "[Bug] report 1") + { + issue = issueObj; + } + } + } + Assert.That(issue, Is.Not.Null); + Assert.That(issue?.GetProperty("body").GetString(), Is.EqualTo("Bug description")); + } + + [Test] + public async Task ShouldCreateFeatureRequests() + { + var data = new Dictionary + { + { "title", "[Feature] request 1" }, + { "body", "Feature description" } + }; + var newIssue = await Request.PostAsync("/repos/" + USER + "/" + REPO + "/issues", new() { DataObject = data }); + await Expect(newIssue).ToBeOKAsync(); + + var issues = await Request.GetAsync("/repos/" + USER + "/" + REPO + "/issues"); + await Expect(newIssue).ToBeOKAsync(); + var issuesJsonResponse = await issues.JsonAsync(); + + JsonElement? issue = null; + foreach (JsonElement issueObj in issuesJsonResponse?.EnumerateArray()) + { + if (issueObj.TryGetProperty("title", out var title) == true) + { + if (title.GetString() == "[Feature] request 1") + { + issue = issueObj; + } + } + } + Assert.That(issue, Is.Not.Null); + Assert.That(issue?.GetProperty("body").GetString(), Is.EqualTo("Feature description")); + } + + // ... +} +``` + + + + + +```csharp +using System.Text.Json; +using Microsoft.Playwright; +using Microsoft.Playwright.Xunit; + +namespace PlaywrightTests; + +public class TestGitHubAPI : PlaywrightTest +{ + static string REPO = "test"; + static string USER = Environment.GetEnvironmentVariable("GITHUB_USER"); + static string? API_TOKEN = Environment.GetEnvironmentVariable("GITHUB_API_TOKEN"); + + private IAPIRequestContext Request = null!; + + [Fact] + public async Task ShouldCreateBugReport() + { + var data = new Dictionary + { + { "title", "[Bug] report 1" }, + { "body", "Bug description" } + }; + var newIssue = await Request.PostAsync("/repos/" + USER + "/" + REPO + "/issues", new() { DataObject = data }); + await Expect(newIssue).ToBeOKAsync(); + + var issues = await Request.GetAsync("/repos/" + USER + "/" + REPO + "/issues"); + await Expect(newIssue).ToBeOKAsync(); + var issuesJsonResponse = await issues.JsonAsync(); + JsonElement? issue = null; + foreach (JsonElement issueObj in issuesJsonResponse?.EnumerateArray()) + { + if (issueObj.TryGetProperty("title", out var title) == true) + { + if (title.GetString() == "[Bug] report 1") + { + issue = issueObj; + } + } + } + Assert.NotNull(issue); + Assert.Equal("Bug description", issue?.GetProperty("body").GetString()); + } + + [Fact] + public async Task ShouldCreateFeatureRequests() + { + var data = new Dictionary + { + { "title", "[Feature] request 1" }, + { "body", "Feature description" } + }; + var newIssue = await Request.PostAsync("/repos/" + USER + "/" + REPO + "/issues", new() { DataObject = data }); + await Expect(newIssue).ToBeOKAsync(); + + var issues = await Request.GetAsync("/repos/" + USER + "/" + REPO + "/issues"); + await Expect(newIssue).ToBeOKAsync(); + var issuesJsonResponse = await issues.JsonAsync(); + + JsonElement? issue = null; + foreach (JsonElement issueObj in issuesJsonResponse?.EnumerateArray()) + { + if (issueObj.TryGetProperty("title", out var title) == true) + { + if (title.GetString() == "[Feature] request 1") + { + issue = issueObj; + } + } + } + Assert.NotNull(issue); + Assert.Equal("Feature description", issue?.GetProperty("body").GetString()); + } + + // ... +} +``` + + + + + +```csharp +using System.Text.Json; +using Microsoft.Playwright; +using Microsoft.Playwright.Xunit.v3; + +namespace PlaywrightTests; + +public class TestGitHubAPI : PlaywrightTest +{ + static string REPO = "test"; + static string USER = Environment.GetEnvironmentVariable("GITHUB_USER"); + static string? API_TOKEN = Environment.GetEnvironmentVariable("GITHUB_API_TOKEN"); + + private IAPIRequestContext Request = null!; + + [Fact] + public async Task ShouldCreateBugReport() + { + var data = new Dictionary + { + { "title", "[Bug] report 1" }, + { "body", "Bug description" } + }; + var newIssue = await Request.PostAsync("/repos/" + USER + "/" + REPO + "/issues", new() { DataObject = data }); + await Expect(newIssue).ToBeOKAsync(); + + var issues = await Request.GetAsync("/repos/" + USER + "/" + REPO + "/issues"); + await Expect(newIssue).ToBeOKAsync(); + var issuesJsonResponse = await issues.JsonAsync(); + JsonElement? issue = null; + foreach (JsonElement issueObj in issuesJsonResponse?.EnumerateArray()) + { + if (issueObj.TryGetProperty("title", out var title) == true) + { + if (title.GetString() == "[Bug] report 1") + { + issue = issueObj; + } + } + } + Assert.NotNull(issue); + Assert.Equal("Bug description", issue?.GetProperty("body").GetString()); + } + + [Fact] + public async Task ShouldCreateFeatureRequests() + { + var data = new Dictionary + { + { "title", "[Feature] request 1" }, + { "body", "Feature description" } + }; + var newIssue = await Request.PostAsync("/repos/" + USER + "/" + REPO + "/issues", new() { DataObject = data }); + await Expect(newIssue).ToBeOKAsync(); + + var issues = await Request.GetAsync("/repos/" + USER + "/" + REPO + "/issues"); + await Expect(newIssue).ToBeOKAsync(); + var issuesJsonResponse = await issues.JsonAsync(); + + JsonElement? issue = null; + foreach (JsonElement issueObj in issuesJsonResponse?.EnumerateArray()) + { + if (issueObj.TryGetProperty("title", out var title) == true) + { + if (title.GetString() == "[Feature] request 1") + { + issue = issueObj; + } + } + } + Assert.NotNull(issue); + Assert.Equal("Feature description", issue?.GetProperty("body").GetString()); + } + + // ... +} +``` + + + + + +### Setup and teardown + +These tests assume that repository exists. You probably want to create a new one before running tests and delete it afterwards. + + + + + +Use `[TestInitialize]` and `[TestCleanup]` hooks for that. + +```csharp +using System.Text.Json; +using Microsoft.Playwright; +using Microsoft.Playwright.MSTest; + +namespace PlaywrightTests; + +[TestClass] +public class TestGitHubAPI : PlaywrightTest +{ + // ... + [TestInitialize] + public async Task SetUpAPITesting() + { + await CreateAPIRequestContext(); + await CreateTestRepository(); + } + + private async Task CreateTestRepository() + { + var resp = await Request.PostAsync("/user/repos", new() + { + DataObject = new Dictionary() + { + ["name"] = REPO, + }, + }); + await Expect(resp).ToBeOKAsync(); + } + + [TestCleanup] + public async Task TearDownAPITesting() + { + await DeleteTestRepository(); + await Request.DisposeAsync(); + } + + private async Task DeleteTestRepository() + { + var resp = await Request.DeleteAsync("/repos/" + USER + "/" + REPO); + await Expect(resp).ToBeOKAsync(); + } +} +``` + + + + + +Use `[SetUp]` and `[TearDown]` hooks for that. + +```csharp +using System.Text.Json; +using Microsoft.Playwright; +using Microsoft.Playwright.NUnit; +using NUnit.Framework; + +namespace PlaywrightTests; + +[Parallelizable(ParallelScope.Self)] +[TestFixture] +public class TestGitHubAPI : PlaywrightTest +{ + // ... + [SetUp] + public async Task SetUpAPITesting() + { + await CreateAPIRequestContext(); + await CreateTestRepository(); + } + + private async Task CreateTestRepository() + { + var resp = await Request.PostAsync("/user/repos", new() + { + DataObject = new Dictionary() + { + ["name"] = REPO, + }, + }); + await Expect(resp).ToBeOKAsync(); + } + + [TearDown] + public async Task TearDownAPITesting() + { + await DeleteTestRepository(); + await Request.DisposeAsync(); + } + + private async Task DeleteTestRepository() + { + var resp = await Request.DeleteAsync("/repos/" + USER + "/" + REPO); + await Expect(resp).ToBeOKAsync(); + } +} +``` + + + + + +Override the `InitializeAsync` and `DisposeAsync` methods for that. + +```csharp +using System.Text.Json; +using Microsoft.Playwright; +using Microsoft.Playwright.Xunit; + +namespace PlaywrightTests; + +public class TestGitHubAPI : PlaywrightTest +{ + // ... + public override async Task InitializeAsync() + { + await base.InitializeAsync(); + await CreateAPIRequestContext(); + await CreateTestRepository(); + } + + private async Task CreateTestRepository() + { + var resp = await Request.PostAsync("/user/repos", new() + { + DataObject = new Dictionary() + { + ["name"] = REPO, + }, + }); + await Expect(resp).ToBeOKAsync(); + } + + public override async Task DisposeAsync() + { + await DeleteTestRepository(); + await Request.DisposeAsync(); + await base.DisposeAsync(); + } + + private async Task DeleteTestRepository() + { + var resp = await Request.DeleteAsync("/repos/" + USER + "/" + REPO); + await Expect(resp).ToBeOKAsync(); + } +} +``` + + + + + +Override the `InitializeAsync` and `DisposeAsync` methods for that. + +```csharp +using System.Text.Json; +using Microsoft.Playwright; +using Microsoft.Playwright.Xunit.v3; + +namespace PlaywrightTests; + +public class TestGitHubAPI : PlaywrightTest +{ + // ... + public override async Task InitializeAsync() + { + await base.InitializeAsync(); + await CreateAPIRequestContext(); + await CreateTestRepository(); + } + + private async Task CreateTestRepository() + { + var resp = await Request.PostAsync("/user/repos", new() + { + DataObject = new Dictionary() + { + ["name"] = REPO, + }, + }); + await Expect(resp).ToBeOKAsync(); + } + + public override async Task DisposeAsync() + { + await DeleteTestRepository(); + await Request.DisposeAsync(); + await base.DisposeAsync(); + } + + private async Task DeleteTestRepository() + { + var resp = await Request.DeleteAsync("/repos/" + USER + "/" + REPO); + await Expect(resp).ToBeOKAsync(); + } +} +``` + + + + + +### Complete test example + +Here is the complete example of an API test: + + + + + ```csharp +using System.Text.Json; using Microsoft.Playwright; using Microsoft.Playwright.MSTest; @@ -41,61 +794,278 @@ namespace PlaywrightTests; [TestClass] public class TestGitHubAPI : PlaywrightTest { + static string REPO = "test-repo-2"; + static string USER = Environment.GetEnvironmentVariable("GITHUB_USER"); + static string? API_TOKEN = Environment.GetEnvironmentVariable("GITHUB_API_TOKEN"); + + private IAPIRequestContext Request = null!; + + [TestMethod] + public async Task ShouldCreateBugReport() + { + var data = new Dictionary + { + { "title", "[Bug] report 1" }, + { "body", "Bug description" } + }; + var newIssue = await Request.PostAsync("/repos/" + USER + "/" + REPO + "/issues", new() { DataObject = data }); + await Expect(newIssue).ToBeOKAsync(); + + var issues = await Request.GetAsync("/repos/" + USER + "/" + REPO + "/issues"); + await Expect(newIssue).ToBeOKAsync(); + var issuesJsonResponse = await issues.JsonAsync(); + JsonElement? issue = null; + foreach (JsonElement issueObj in issuesJsonResponse?.EnumerateArray()) + { + if (issueObj.TryGetProperty("title", out var title) == true) + { + if (title.GetString() == "[Bug] report 1") + { + issue = issueObj; + } + } + } + Assert.IsNotNull(issue); + Assert.AreEqual("Bug description", issue?.GetProperty("body").GetString()); + } + + [TestMethod] + public async Task ShouldCreateFeatureRequests() + { + var data = new Dictionary + { + { "title", "[Feature] request 1" }, + { "body", "Feature description" } + }; + var newIssue = await Request.PostAsync("/repos/" + USER + "/" + REPO + "/issues", new() { DataObject = data }); + await Expect(newIssue).ToBeOKAsync(); + + var issues = await Request.GetAsync("/repos/" + USER + "/" + REPO + "/issues"); + await Expect(newIssue).ToBeOKAsync(); + var issuesJsonResponse = await issues.JsonAsync(); + + JsonElement? issue = null; + foreach (JsonElement issueObj in issuesJsonResponse?.EnumerateArray()) + { + if (issueObj.TryGetProperty("title", out var title) == true) + { + if (title.GetString() == "[Feature] request 1") + { + issue = issueObj; + } + } + } + Assert.IsNotNull(issue); + Assert.AreEqual("Feature description", issue?.GetProperty("body").GetString()); + } + + [TestInitialize] + public async Task SetUpAPITesting() + { + await CreateAPIRequestContext(); + await CreateTestRepository(); + } + + private async Task CreateAPIRequestContext() + { + var headers = new Dictionary + { + // We set this header per GitHub guidelines. + { "Accept", "application/vnd.github.v3+json" }, + // Add authorization token to all requests. + // Assuming personal access token available in the environment. + { "Authorization", "token " + API_TOKEN } + }; + + Request = await Playwright.APIRequest.NewContextAsync(new() + { + // All requests we send go to this API endpoint. + BaseURL = "https://api.github.com", + ExtraHTTPHeaders = headers, + }); + } + + private async Task CreateTestRepository() + { + var resp = await Request.PostAsync("/user/repos", new() + { + DataObject = new Dictionary() + { + ["name"] = REPO, + }, + }); + await Expect(resp).ToBeOKAsync(); + } + + [TestCleanup] + public async Task TearDownAPITesting() + { + await DeleteTestRepository(); + await Request.DisposeAsync(); + } + + private async Task DeleteTestRepository() + { + var resp = await Request.DeleteAsync("/repos/" + USER + "/" + REPO); + await Expect(resp).ToBeOKAsync(); + } +} +``` + + + + + +```csharp +using System.Text.Json; +using Microsoft.Playwright; +using Microsoft.Playwright.NUnit; +using NUnit.Framework; + +namespace PlaywrightTests; + +[Parallelizable(ParallelScope.Self)] +[TestFixture] +public class TestGitHubAPI : PlaywrightTest +{ + static string REPO = "test-repo-2"; + static string USER = Environment.GetEnvironmentVariable("GITHUB_USER"); static string? API_TOKEN = Environment.GetEnvironmentVariable("GITHUB_API_TOKEN"); private IAPIRequestContext Request = null!; - [TestInitialize] + [Test] + public async Task ShouldCreateBugReport() + { + var data = new Dictionary + { + { "title", "[Bug] report 1" }, + { "body", "Bug description" } + }; + var newIssue = await Request.PostAsync("/repos/" + USER + "/" + REPO + "/issues", new() { DataObject = data }); + await Expect(newIssue).ToBeOKAsync(); + + var issues = await Request.GetAsync("/repos/" + USER + "/" + REPO + "/issues"); + await Expect(newIssue).ToBeOKAsync(); + var issuesJsonResponse = await issues.JsonAsync(); + JsonElement? issue = null; + foreach (JsonElement issueObj in issuesJsonResponse?.EnumerateArray()) + { + if (issueObj.TryGetProperty("title", out var title) == true) + { + if (title.GetString() == "[Bug] report 1") + { + issue = issueObj; + } + } + } + Assert.That(issue, Is.Not.Null); + Assert.That(issue?.GetProperty("body").GetString(), Is.EqualTo("Bug description")); + } + + [Test] + public async Task ShouldCreateFeatureRequests() + { + var data = new Dictionary + { + { "title", "[Feature] request 1" }, + { "body", "Feature description" } + }; + var newIssue = await Request.PostAsync("/repos/" + USER + "/" + REPO + "/issues", new() { DataObject = data }); + await Expect(newIssue).ToBeOKAsync(); + + var issues = await Request.GetAsync("/repos/" + USER + "/" + REPO + "/issues"); + await Expect(newIssue).ToBeOKAsync(); + var issuesJsonResponse = await issues.JsonAsync(); + + JsonElement? issue = null; + foreach (JsonElement issueObj in issuesJsonResponse?.EnumerateArray()) + { + if (issueObj.TryGetProperty("title", out var title) == true) + { + if (title.GetString() == "[Feature] request 1") + { + issue = issueObj; + } + } + } + Assert.That(issue, Is.Not.Null); + Assert.That(issue?.GetProperty("body").GetString(), Is.EqualTo("Feature description")); + } + + [SetUp] public async Task SetUpAPITesting() { await CreateAPIRequestContext(); + await CreateTestRepository(); } private async Task CreateAPIRequestContext() { - var headers = new Dictionary(); - // We set this header per GitHub guidelines. - headers.Add("Accept", "application/vnd.github.v3+json"); - // Add authorization token to all requests. - // Assuming personal access token available in the environment. - headers.Add("Authorization", "token " + API_TOKEN); + var headers = new Dictionary + { + // We set this header per GitHub guidelines. + { "Accept", "application/vnd.github.v3+json" }, + // Add authorization token to all requests. + // Assuming personal access token available in the environment. + { "Authorization", "token " + API_TOKEN } + }; - Request = await this.Playwright.APIRequest.NewContextAsync(new() { + Request = await Playwright.APIRequest.NewContextAsync(new() + { // All requests we send go to this API endpoint. BaseURL = "https://api.github.com", ExtraHTTPHeaders = headers, }); } - [TestCleanup] + private async Task CreateTestRepository() + { + var resp = await Request.PostAsync("/user/repos", new() + { + DataObject = new Dictionary() + { + ["name"] = REPO, + }, + }); + await Expect(resp).ToBeOKAsync(); + } + + [TearDown] public async Task TearDownAPITesting() { + await DeleteTestRepository(); await Request.DisposeAsync(); } + + private async Task DeleteTestRepository() + { + var resp = await Request.DeleteAsync("/repos/" + USER + "/" + REPO); + await Expect(resp).ToBeOKAsync(); + } } ``` -### Write tests + -Now that we initialized request object we can add a few tests that will create new issues in the repository. + ```csharp using System.Text.Json; using Microsoft.Playwright; -using Microsoft.Playwright.MSTest; +using Microsoft.Playwright.Xunit; namespace PlaywrightTests; -[TestClass] public class TestGitHubAPI : PlaywrightTest { - static string REPO = "test"; + static string REPO = "test-repo-2"; static string USER = Environment.GetEnvironmentVariable("GITHUB_USER"); static string? API_TOKEN = Environment.GetEnvironmentVariable("GITHUB_API_TOKEN"); private IAPIRequestContext Request = null!; - [TestMethod] + [Fact] public async Task ShouldCreateBugReport() { var data = new Dictionary @@ -120,11 +1090,11 @@ public class TestGitHubAPI : PlaywrightTest } } } - Assert.IsNotNull(issue); - Assert.AreEqual("Bug description", issue?.GetProperty("body").GetString()); + Assert.NotNull(issue); + Assert.Equal("Bug description", issue?.GetProperty("body").GetString()); } - [TestMethod] + [Fact] public async Task ShouldCreateFeatureRequests() { var data = new Dictionary @@ -150,36 +1120,36 @@ public class TestGitHubAPI : PlaywrightTest } } } - Assert.IsNotNull(issue); - Assert.AreEqual("Feature description", issue?.GetProperty("body").GetString()); + Assert.NotNull(issue); + Assert.Equal("Feature description", issue?.GetProperty("body").GetString()); } - // ... -} -``` - -### Setup and teardown - -These tests assume that repository exists. You probably want to create a new one before running tests and delete it afterwards. Use `[SetUp]` and `[TearDown]` hooks for that. - -```csharp -using System.Text.Json; -using Microsoft.Playwright; -using Microsoft.Playwright.MSTest; - -namespace PlaywrightTests; - -[TestClass] -public class TestGitHubAPI : PlaywrightTest -{ - // ... - [TestInitialize] - public async Task SetUpAPITesting() + public override async Task InitializeAsync() { + await base.InitializeAsync(); await CreateAPIRequestContext(); await CreateTestRepository(); } + private async Task CreateAPIRequestContext() + { + var headers = new Dictionary + { + // We set this header per GitHub guidelines. + { "Accept", "application/vnd.github.v3+json" }, + // Add authorization token to all requests. + // Assuming personal access token available in the environment. + { "Authorization", "token " + API_TOKEN } + }; + + Request = await Playwright.APIRequest.NewContextAsync(new() + { + // All requests we send go to this API endpoint. + BaseURL = "https://api.github.com", + ExtraHTTPHeaders = headers, + }); + } + private async Task CreateTestRepository() { var resp = await Request.PostAsync("/user/repos", new() @@ -192,11 +1162,11 @@ public class TestGitHubAPI : PlaywrightTest await Expect(resp).ToBeOKAsync(); } - [TestCleanup] - public async Task TearDownAPITesting() + public override async Task DisposeAsync() { await DeleteTestRepository(); await Request.DisposeAsync(); + await base.DisposeAsync(); } private async Task DeleteTestRepository() @@ -207,18 +1177,17 @@ public class TestGitHubAPI : PlaywrightTest } ``` -### Complete test example + -Here is the complete example of an API test: + ```csharp using System.Text.Json; using Microsoft.Playwright; -using Microsoft.Playwright.MSTest; +using Microsoft.Playwright.Xunit.v3; namespace PlaywrightTests; -[TestClass] public class TestGitHubAPI : PlaywrightTest { static string REPO = "test-repo-2"; @@ -227,7 +1196,7 @@ public class TestGitHubAPI : PlaywrightTest private IAPIRequestContext Request = null!; - [TestMethod] + [Fact] public async Task ShouldCreateBugReport() { var data = new Dictionary @@ -252,11 +1221,11 @@ public class TestGitHubAPI : PlaywrightTest } } } - Assert.IsNotNull(issue); - Assert.AreEqual("Bug description", issue?.GetProperty("body").GetString()); + Assert.NotNull(issue); + Assert.Equal("Bug description", issue?.GetProperty("body").GetString()); } - [TestMethod] + [Fact] public async Task ShouldCreateFeatureRequests() { var data = new Dictionary @@ -282,13 +1251,13 @@ public class TestGitHubAPI : PlaywrightTest } } } - Assert.IsNotNull(issue); - Assert.AreEqual("Feature description", issue?.GetProperty("body").GetString()); + Assert.NotNull(issue); + Assert.Equal("Feature description", issue?.GetProperty("body").GetString()); } - [TestInitialize] - public async Task SetUpAPITesting() + public override async Task InitializeAsync() { + await base.InitializeAsync(); await CreateAPIRequestContext(); await CreateTestRepository(); } @@ -324,11 +1293,11 @@ public class TestGitHubAPI : PlaywrightTest await Expect(resp).ToBeOKAsync(); } - [TestCleanup] - public async Task TearDownAPITesting() + public override async Task DisposeAsync() { await DeleteTestRepository(); await Request.DisposeAsync(); + await base.DisposeAsync(); } private async Task DeleteTestRepository() @@ -339,12 +1308,21 @@ public class TestGitHubAPI : PlaywrightTest } ``` + + + + ## Prepare server state via API calls The following test creates a new issue via API and then navigates to the list of all issues in the project to check that it appears at the top of the list. The check is performed using [LocatorAssertions]. + + + + ```csharp -class TestGitHubAPI : PageTest +[TestClass] +public class TestGitHubAPI : PageTest { [TestMethod] public async Task LastCreatedIssueShouldBeFirstInTheList() @@ -366,13 +1344,105 @@ class TestGitHubAPI : PageTest } ``` + + + + +```csharp +[Parallelizable(ParallelScope.Self)] +[TestFixture] +public class TestGitHubAPI : PageTest +{ + [Test] + public async Task LastCreatedIssueShouldBeFirstInTheList() + { + var data = new Dictionary + { + { "title", "[Feature] request 1" }, + { "body", "Feature description" } + }; + var newIssue = await Request.PostAsync("/repos/" + USER + "/" + REPO + "/issues", new() { DataObject = data }); + await Expect(newIssue).ToBeOKAsync(); + + // When inheriting from 'PlaywrightTest' it only gives you a Playwright instance. To get a Page instance, either start + // a browser, context, and page manually or inherit from 'PageTest' which will launch it for you. + await Page.GotoAsync("https://github.com/" + USER + "/" + REPO + "/issues"); + var firstIssue = Page.Locator("a[data-hovercard-type='issue']").First; + await Expect(firstIssue).ToHaveTextAsync("[Feature] request 1"); + } +} +``` + + + + + +```csharp +public class TestGitHubAPI : PageTest +{ + [Fact] + public async Task LastCreatedIssueShouldBeFirstInTheList() + { + var data = new Dictionary + { + { "title", "[Feature] request 1" }, + { "body", "Feature description" } + }; + var newIssue = await Request.PostAsync("/repos/" + USER + "/" + REPO + "/issues", new() { DataObject = data }); + await Expect(newIssue).ToBeOKAsync(); + + // When inheriting from 'PlaywrightTest' it only gives you a Playwright instance. To get a Page instance, either start + // a browser, context, and page manually or inherit from 'PageTest' which will launch it for you. + await Page.GotoAsync("https://github.com/" + USER + "/" + REPO + "/issues"); + var firstIssue = Page.Locator("a[data-hovercard-type='issue']").First; + await Expect(firstIssue).ToHaveTextAsync("[Feature] request 1"); + } +} +``` + + + + + +```csharp +public class TestGitHubAPI : PageTest +{ + [Fact] + public async Task LastCreatedIssueShouldBeFirstInTheList() + { + var data = new Dictionary + { + { "title", "[Feature] request 1" }, + { "body", "Feature description" } + }; + var newIssue = await Request.PostAsync("/repos/" + USER + "/" + REPO + "/issues", new() { DataObject = data }); + await Expect(newIssue).ToBeOKAsync(); + + // When inheriting from 'PlaywrightTest' it only gives you a Playwright instance. To get a Page instance, either start + // a browser, context, and page manually or inherit from 'PageTest' which will launch it for you. + await Page.GotoAsync("https://github.com/" + USER + "/" + REPO + "/issues"); + var firstIssue = Page.Locator("a[data-hovercard-type='issue']").First; + await Expect(firstIssue).ToHaveTextAsync("[Feature] request 1"); + } +} +``` + + + + + ## Check the server state after running user actions The following test creates a new issue via user interface in the browser and then checks via API if it was created: + + + + ```csharp // Make sure to extend from PageTest if you want to use the Page class. -class GitHubTests : PageTest +[TestClass] +public class GitHubTests : PageTest { [TestMethod] public async Task LastCreatedIssueShouldBeOnTheServer() @@ -391,6 +1461,87 @@ class GitHubTests : PageTest } ``` + + + + +```csharp +// Make sure to extend from PageTest if you want to use the Page class. +[Parallelizable(ParallelScope.Self)] +[TestFixture] +public class GitHubTests : PageTest +{ + [Test] + public async Task LastCreatedIssueShouldBeOnTheServer() + { + await Page.GotoAsync("https://github.com/" + USER + "/" + REPO + "/issues"); + await Page.Locator("text=New Issue").ClickAsync(); + await Page.Locator("[aria-label='Title']").FillAsync("Bug report 1"); + await Page.Locator("[aria-label='Comment body']").FillAsync("Bug description"); + await Page.Locator("text=Submit new issue").ClickAsync(); + var issueId = Page.Url.Substring(Page.Url.LastIndexOf('/')); + + var newIssue = await Request.GetAsync("https://github.com/" + USER + "/" + REPO + "/issues/" + issueId); + await Expect(newIssue).ToBeOKAsync(); + Assert.That(await newIssue.TextAsync(), Does.Contain("Bug report 1")); + } +} +``` + + + + + +```csharp +// Make sure to extend from PageTest if you want to use the Page class. +public class GitHubTests : PageTest +{ + [Fact] + public async Task LastCreatedIssueShouldBeOnTheServer() + { + await Page.GotoAsync("https://github.com/" + USER + "/" + REPO + "/issues"); + await Page.Locator("text=New Issue").ClickAsync(); + await Page.Locator("[aria-label='Title']").FillAsync("Bug report 1"); + await Page.Locator("[aria-label='Comment body']").FillAsync("Bug description"); + await Page.Locator("text=Submit new issue").ClickAsync(); + var issueId = Page.Url.Substring(Page.Url.LastIndexOf('/')); + + var newIssue = await Request.GetAsync("https://github.com/" + USER + "/" + REPO + "/issues/" + issueId); + await Expect(newIssue).ToBeOKAsync(); + Assert.Contains("Bug report 1", await newIssue.TextAsync()); + } +} +``` + + + + + +```csharp +// Make sure to extend from PageTest if you want to use the Page class. +public class GitHubTests : PageTest +{ + [Fact] + public async Task LastCreatedIssueShouldBeOnTheServer() + { + await Page.GotoAsync("https://github.com/" + USER + "/" + REPO + "/issues"); + await Page.Locator("text=New Issue").ClickAsync(); + await Page.Locator("[aria-label='Title']").FillAsync("Bug report 1"); + await Page.Locator("[aria-label='Comment body']").FillAsync("Bug description"); + await Page.Locator("text=Submit new issue").ClickAsync(); + var issueId = Page.Url.Substring(Page.Url.LastIndexOf('/')); + + var newIssue = await Request.GetAsync("https://github.com/" + USER + "/" + REPO + "/issues/" + issueId); + await Expect(newIssue).ToBeOKAsync(); + Assert.Contains("Bug report 1", await newIssue.TextAsync()); + } +} +``` + + + + + ## Reuse authentication state Web apps use cookie-based or token-based authentication, where authenticated state is stored as [cookies](https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies). Playwright provides [ApiRequestContext.StorageStateAsync()](/api/class-apirequestcontext.mdx#api-request-context-storage-state) method that can be used to retrieve storage state from an authenticated context and then create new contexts with that state. diff --git a/dotnet/docs/api/class-apirequestcontext.mdx b/dotnet/docs/api/class-apirequestcontext.mdx index 68b708efdc..96ab12610e 100644 --- a/dotnet/docs/api/class-apirequestcontext.mdx +++ b/dotnet/docs/api/class-apirequestcontext.mdx @@ -562,6 +562,9 @@ await ApiRequestContext.StorageStateAsync(options); - `IndexedDB` [bool]? *(optional)* Added in: v1.51# Set to `true` to include IndexedDB in the storage state snapshot. + - `Opfs` [bool]? *(optional)* Added in: v1.63# + + Set to `true` to include the origin private file system in the storage state snapshot. - `Path` [string]? *(optional)*# The file path to save the storage state to. If [Path](/api/class-apirequestcontext.mdx#api-request-context-storage-state-option-path) is a relative path, then it is resolved relative to current working directory. If no path is provided, storage state is still returned, but won't be saved to the disk. diff --git a/dotnet/docs/api/class-browser.mdx b/dotnet/docs/api/class-browser.mdx index 9104f28fa0..0ca1d4a6ff 100644 --- a/dotnet/docs/api/class-browser.mdx +++ b/dotnet/docs/api/class-browser.mdx @@ -311,9 +311,6 @@ await browser.CloseAsync(); - `Permissions` [IEnumerable]?<[string]> *(optional)*# A list of permissions to grant to all pages in this context. See [BrowserContext.GrantPermissionsAsync()](/api/class-browsercontext.mdx#browser-context-grant-permissions) for more details. Defaults to none. - - `PierceFrames` [bool]? *(optional)*# - - If set to true, all selectors in this context will pierce frames by default, as if every locator was created through [Page.PierceFrames()](/api/class-page.mdx#page-pierce-frames). Defaults to `false`. - `Proxy` Proxy? *(optional)*# - `Server` [string] @@ -536,9 +533,6 @@ await Browser.NewPageAsync(options); - `Permissions` [IEnumerable]?<[string]> *(optional)*# A list of permissions to grant to all pages in this context. See [BrowserContext.GrantPermissionsAsync()](/api/class-browsercontext.mdx#browser-context-grant-permissions) for more details. Defaults to none. - - `PierceFrames` [bool]? *(optional)*# - - If set to true, all selectors in this context will pierce frames by default, as if every locator was created through [Page.PierceFrames()](/api/class-page.mdx#page-pierce-frames). Defaults to `false`. - `Proxy` Proxy? *(optional)*# - `Server` [string] diff --git a/dotnet/docs/api/class-browsercontext.mdx b/dotnet/docs/api/class-browsercontext.mdx index 418b52421c..394af4cdd4 100644 --- a/dotnet/docs/api/class-browsercontext.mdx +++ b/dotnet/docs/api/class-browsercontext.mdx @@ -903,7 +903,7 @@ await BrowserContext.SetOfflineAsync(offline); Added in: v1.59browserContext.SetStorageStateAsync -Clears the existing cookies, local storage, IndexedDB entries and virtual WebAuthn credentials, and sets the new storage state. When the storage state contains credentials, the virtual WebAuthn authenticator is installed (equivalent to [Credentials.InstallAsync()](/api/class-credentials.mdx#credentials-install)), preventing all real authenticators from working in this context. +Clears the existing cookies, local storage, IndexedDB entries, origin private file system entries and virtual WebAuthn credentials, and sets the new storage state. When the storage state contains credentials, the virtual WebAuthn authenticator is installed (equivalent to [Credentials.InstallAsync()](/api/class-credentials.mdx#credentials-install)), preventing all real authenticators from working in this context. **Usage** @@ -926,7 +926,7 @@ await context.SetStorageStateAsync("state.json"); Added before v1.9browserContext.StorageStateAsync -Returns storage state for this browser context, contains current cookies, local storage snapshot, IndexedDB snapshot and virtual WebAuthn credentials. +Returns storage state for this browser context, contains current cookies, local storage snapshot, IndexedDB snapshot, origin private file system snapshot and virtual WebAuthn credentials. **Usage** @@ -942,6 +942,15 @@ await BrowserContext.StorageStateAsync(options); - `IndexedDB` [bool]? *(optional)* Added in: v1.51# Set to `true` to include [IndexedDB](https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API) in the storage state snapshot. If your application uses IndexedDB to store authentication tokens, like Firebase Authentication, enable this. + - `Opfs` [bool]? *(optional)* Added in: v1.63# + + Set to `true` to include the [origin private file system](https://developer.mozilla.org/en-US/docs/Web/API/File_System_API/Origin_private_file_system) in the storage state snapshot. + + :::note + + OPFS is currently not supported in ephemeral WebKit contexts. + ::: + - `Path` [string]? *(optional)*# The file path to save the storage state to. If [Path](/api/class-browsercontext.mdx#browser-context-storage-state-option-path) is a relative path, then it is resolved relative to current working directory. If no path is provided, storage state is still returned, but won't be saved to the disk. diff --git a/dotnet/docs/api/class-browsertype.mdx b/dotnet/docs/api/class-browsertype.mdx index 352cd2c242..fd6bbb2de7 100644 --- a/dotnet/docs/api/class-browsertype.mdx +++ b/dotnet/docs/api/class-browsertype.mdx @@ -461,9 +461,6 @@ await BrowserType.LaunchPersistentContextAsync(userDataDir, options); - `Permissions` [IEnumerable]?<[string]> *(optional)*# A list of permissions to grant to all pages in this context. See [BrowserContext.GrantPermissionsAsync()](/api/class-browsercontext.mdx#browser-context-grant-permissions) for more details. Defaults to none. - - `PierceFrames` [bool]? *(optional)*# - - If set to true, all selectors in this context will pierce frames by default, as if every locator was created through [Page.PierceFrames()](/api/class-page.mdx#page-pierce-frames). Defaults to `false`. - `Proxy` Proxy? *(optional)*# - `Server` [string] diff --git a/dotnet/docs/api/class-frame.mdx b/dotnet/docs/api/class-frame.mdx index 11c29790cb..f5c30fa9b9 100644 --- a/dotnet/docs/api/class-frame.mdx +++ b/dotnet/docs/api/class-frame.mdx @@ -328,6 +328,8 @@ Console.WriteLine(frame == contentFrame); // -> True When working with iframes, you can create a frame locator that will enter the iframe and allow selecting elements in that iframe. +When called without [selector](/api/class-frame.mdx#frame-frame-locator-option-selector), the search starts in this frame or in any of the iframes inside it, so that you don't need to locate each iframe first. Note that the rest of the locator is resolved inside a single frame, just like any other locator. If it matches elements inside multiple frames, an error is thrown. + **Usage** Following snippet locates element with text "Submit" in the iframe with id `my-frame`, like `