From 418ea38a2455a35afb693172eed6e34ff8dbb631 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sat, 31 Jan 2026 04:07:27 +0000
Subject: [PATCH 1/3] Initial plan
From 85c11937998576f6253876397458c93293620689 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sat, 31 Jan 2026 04:10:27 +0000
Subject: [PATCH 2/3] feat: add Playwright web accessor project
Co-authored-by: kerryjiang <456060+kerryjiang@users.noreply.github.com>
---
Directory.Packages.props | 3 +-
NetInteractor.sln | 15 ++
.../NetInteractor.Playwright.csproj | 22 +++
.../PlaywrightWebAccessor.cs | 181 ++++++++++++++++++
4 files changed, 220 insertions(+), 1 deletion(-)
create mode 100644 src/NetInteractor.Playwright/NetInteractor.Playwright.csproj
create mode 100644 src/NetInteractor.Playwright/PlaywrightWebAccessor.cs
diff --git a/Directory.Packages.props b/Directory.Packages.props
index b0bb0f4..7238e77 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -4,6 +4,7 @@
+
@@ -13,4 +14,4 @@
-
\ No newline at end of file
+
diff --git a/NetInteractor.sln b/NetInteractor.sln
index 093d77f..8a05bf0 100644
--- a/NetInteractor.sln
+++ b/NetInteractor.sln
@@ -11,6 +11,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NetInteractor.Test", "test\
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NetInteractor.PuppeteerSharp", "src\NetInteractor.PuppeteerSharp\NetInteractor.PuppeteerSharp.csproj", "{96C0F6DB-7F80-4E41-B9E8-75C5F750B4BF}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NetInteractor.Playwright", "src\NetInteractor.Playwright\NetInteractor.Playwright.csproj", "{1451EBF6-EF0F-4F7E-8F3E-7529AF5F9E62}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -57,6 +59,18 @@ Global
{96C0F6DB-7F80-4E41-B9E8-75C5F750B4BF}.Release|x64.Build.0 = Release|Any CPU
{96C0F6DB-7F80-4E41-B9E8-75C5F750B4BF}.Release|x86.ActiveCfg = Release|Any CPU
{96C0F6DB-7F80-4E41-B9E8-75C5F750B4BF}.Release|x86.Build.0 = Release|Any CPU
+ {1451EBF6-EF0F-4F7E-8F3E-7529AF5F9E62}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {1451EBF6-EF0F-4F7E-8F3E-7529AF5F9E62}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {1451EBF6-EF0F-4F7E-8F3E-7529AF5F9E62}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {1451EBF6-EF0F-4F7E-8F3E-7529AF5F9E62}.Debug|x64.Build.0 = Debug|Any CPU
+ {1451EBF6-EF0F-4F7E-8F3E-7529AF5F9E62}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {1451EBF6-EF0F-4F7E-8F3E-7529AF5F9E62}.Debug|x86.Build.0 = Debug|Any CPU
+ {1451EBF6-EF0F-4F7E-8F3E-7529AF5F9E62}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {1451EBF6-EF0F-4F7E-8F3E-7529AF5F9E62}.Release|Any CPU.Build.0 = Release|Any CPU
+ {1451EBF6-EF0F-4F7E-8F3E-7529AF5F9E62}.Release|x64.ActiveCfg = Release|Any CPU
+ {1451EBF6-EF0F-4F7E-8F3E-7529AF5F9E62}.Release|x64.Build.0 = Release|Any CPU
+ {1451EBF6-EF0F-4F7E-8F3E-7529AF5F9E62}.Release|x86.ActiveCfg = Release|Any CPU
+ {1451EBF6-EF0F-4F7E-8F3E-7529AF5F9E62}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -65,5 +79,6 @@ Global
{75781078-1761-4EDD-A872-5F71D1FF9721} = {F5108642-5386-4F95-ACF8-8DA0AD621820}
{A9186BD7-EFFC-4B4D-B828-C4F4AA924C50} = {F5108642-5386-4F95-ACF8-8DA0AD621820}
{96C0F6DB-7F80-4E41-B9E8-75C5F750B4BF} = {F5108642-5386-4F95-ACF8-8DA0AD621820}
+ {1451EBF6-EF0F-4F7E-8F3E-7529AF5F9E62} = {F5108642-5386-4F95-ACF8-8DA0AD621820}
EndGlobalSection
EndGlobal
diff --git a/src/NetInteractor.Playwright/NetInteractor.Playwright.csproj b/src/NetInteractor.Playwright/NetInteractor.Playwright.csproj
new file mode 100644
index 0000000..aa7c56d
--- /dev/null
+++ b/src/NetInteractor.Playwright/NetInteractor.Playwright.csproj
@@ -0,0 +1,22 @@
+
+
+ netstandard2.0;net8.0;net9.0
+ Kerry Jiang
+ Playwright implementation of IWebAccessor for NetInteractor - enables browser automation with Chromium/Firefox/WebKit.
+ Apache-2.0
+ Web;Automation;Html;Scraping;Testing;Playwright;Browser;WebAutomation;Headless;Chromium;Firefox;WebKit
+ https://github.com/kerryjiang/NetInteractor.git
+
+
+ v$(PackageVersion).md
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/NetInteractor.Playwright/PlaywrightWebAccessor.cs b/src/NetInteractor.Playwright/PlaywrightWebAccessor.cs
new file mode 100644
index 0000000..bf211a4
--- /dev/null
+++ b/src/NetInteractor.Playwright/PlaywrightWebAccessor.cs
@@ -0,0 +1,181 @@
+using System;
+using System.Collections.Specialized;
+using System.Collections.Generic;
+using System.Linq;
+using System.Net.Http;
+using System.Net.Http.Headers;
+using System.Threading;
+using System.Threading.Tasks;
+using System.Text;
+using Microsoft.Playwright;
+using NetInteractor.Config;
+
+namespace NetInteractor.WebAccessors
+{
+ public class PlaywrightWebAccessor : IWebAccessor, IAsyncDisposable
+ {
+ private IPlaywright _playwright;
+ private IBrowser _browser;
+ private readonly SemaphoreSlim _browserLock = new SemaphoreSlim(1, 1);
+ private readonly BrowserTypeLaunchOptions _launchOptions;
+ private bool _disposed;
+
+ public PlaywrightWebAccessor(BrowserTypeLaunchOptions launchOptions = null)
+ {
+ _launchOptions = launchOptions ?? new BrowserTypeLaunchOptions
+ {
+ Headless = true,
+ Args = new[] { "--no-sandbox", "--disable-setuid-sandbox" }
+ };
+ }
+
+ private async Task GetBrowserAsync()
+ {
+ if (_browser == null)
+ {
+ await _browserLock.WaitAsync();
+ try
+ {
+ if (_browser == null)
+ {
+ _playwright = await Microsoft.Playwright.Playwright.CreateAsync();
+ _browser = await _playwright.Chromium.LaunchAsync(_launchOptions);
+ }
+ }
+ finally
+ {
+ _browserLock.Release();
+ }
+ }
+ return _browser;
+ }
+
+ public async Task GetAsync(string url, InteractActionConfig config = null)
+ {
+ var browser = await GetBrowserAsync();
+ var page = await browser.NewPageAsync();
+
+ try
+ {
+ var response = await page.GotoAsync(url, new PageGotoOptions
+ {
+ WaitUntil = WaitUntilState.NetworkIdle
+ });
+
+ var loadDelayStr = config?.Options?.FirstOrDefault(attr => attr.Name == "loadDelay")?.Value;
+ if (!string.IsNullOrEmpty(loadDelayStr) && int.TryParse(loadDelayStr, out var loadDelay))
+ {
+ var delayTask = page.WaitForTimeoutAsync(loadDelay);
+ var navigationTask = page.WaitForNavigationAsync(new PageWaitForNavigationOptions
+ {
+ WaitUntil = WaitUntilState.NetworkIdle
+ });
+
+ var completedTask = await Task.WhenAny(delayTask, navigationTask);
+ if (completedTask == navigationTask)
+ {
+ try
+ {
+ response = await navigationTask;
+ }
+ catch (PlaywrightException)
+ {
+ }
+ }
+ }
+
+ return await GetResultFromResponse(page, response);
+ }
+ finally
+ {
+ await page.CloseAsync();
+ }
+ }
+
+ public async Task PostAsync(string url, NameValueCollection formValues, InteractActionConfig config = null)
+ {
+ var browser = await GetBrowserAsync();
+ var page = await browser.NewPageAsync();
+
+ try
+ {
+ var formData = string.Join("&", formValues.Keys.OfType()
+ .Select(k => k + "=" + Uri.EscapeDataString(formValues[k] ?? string.Empty)));
+
+ await page.RouteAsync("**/*", async route =>
+ {
+ var request = route.Request;
+ if (request.Url == url && request.Method == "GET")
+ {
+ await route.ContinueAsync(new RouteContinueOptions
+ {
+ Method = "POST",
+ PostData = Encoding.UTF8.GetBytes(formData),
+ Headers = request.Headers.Concat(new[]
+ {
+ new KeyValuePair("Content-Type", "application/x-www-form-urlencoded")
+ }).ToDictionary(x => x.Key, x => x.Value)
+ });
+ }
+ else
+ {
+ await route.ContinueAsync();
+ }
+ });
+
+ var response = await page.GotoAsync(url, new PageGotoOptions
+ {
+ WaitUntil = WaitUntilState.NetworkIdle
+ });
+
+ return await GetResultFromResponse(page, response);
+ }
+ finally
+ {
+ await page.CloseAsync();
+ }
+ }
+
+ private async Task GetResultFromResponse(IPage page, IResponse response)
+ {
+ var html = await page.ContentAsync();
+
+ var mockResponse = new HttpResponseMessage();
+ IEnumerable> headerPairs = response?.Headers ?? Enumerable.Empty>();
+
+ foreach (var header in headerPairs)
+ {
+ try
+ {
+ mockResponse.Headers.TryAddWithoutValidation(header.Key, header.Value);
+ }
+ catch (Exception)
+ {
+ }
+ }
+
+ return new ResponseInfo
+ {
+ StatusCode = (int)(response?.Status ?? 0),
+ StatusDescription = response?.StatusText,
+ Html = html,
+ Url = response?.Url,
+ Headers = mockResponse.Headers
+ };
+ }
+
+ public async ValueTask DisposeAsync()
+ {
+ if (!_disposed)
+ {
+ _disposed = true;
+ if (_browser != null)
+ {
+ await _browser.DisposeAsync();
+ }
+ _playwright?.Dispose();
+ _browserLock.Dispose();
+ }
+ }
+ }
+}
From 1411f2a45ec89a0b4c952d9ed0eddfc21bea47b7 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sat, 31 Jan 2026 04:17:23 +0000
Subject: [PATCH 3/3] test: add Playwright accessor to integration tests
Co-authored-by: kerryjiang <456060+kerryjiang@users.noreply.github.com>
---
test/NetInteractor.Test/IntegrationTests.cs | 7 +++++++
test/NetInteractor.Test/NetInteractor.Test.csproj | 1 +
2 files changed, 8 insertions(+)
diff --git a/test/NetInteractor.Test/IntegrationTests.cs b/test/NetInteractor.Test/IntegrationTests.cs
index f558c1a..3397020 100644
--- a/test/NetInteractor.Test/IntegrationTests.cs
+++ b/test/NetInteractor.Test/IntegrationTests.cs
@@ -42,6 +42,13 @@ public IEnumerator