Skip to content

Migrate to .NET 10: add shared build config, CI, embedded stealth scripts, and tests#1

Merged
KSemenenko merged 3 commits intomainfrom
codex/create-open-source-nuget-package-with-playwright
Feb 3, 2026
Merged

Migrate to .NET 10: add shared build config, CI, embedded stealth scripts, and tests#1
KSemenenko merged 3 commits intomainfrom
codex/create-open-source-nuget-package-with-playwright

Conversation

@KSemenenko
Copy link
Member

Motivation

  • Bring the Playwright.Stealth project in line with the Storage repo .NET 10 conventions so it builds and tests cleanly under .NET 10 (shared props, test runner, and CI).
  • Provide a packaged .NET API that exposes the original Playwright stealth evasions (embedded JS resources) and simple extension APIs so consumers can opt-in to stealth initialization.
  • Add baseline automated tests and a GitHub Actions workflow so builds and tests run in CI.

Description

  • Added shared configuration files: Directory.Build.props (targets .NET 10, enables analyzers, suppresses XML warnings, and enables MTP test support) and global.json (pins SDK 10.0.102 and opts into the Microsoft.Testing.Platform test runner).
  • Added GitHub Actions workflow .github/workflows/ci.yml that restores, builds and runs tests using dotnet on ubuntu-latest.
  • Implemented library surface: StealthConfig (options), StealthScriptProvider (loads embedded JS), and PlaywrightStealthExtensions exposing ApplyStealthAsync(this IPage, ...) and ApplyStealthAsync(this IBrowserContext, ...) which inject the combined init scripts.
  • Embedded original JS evasion scripts under src/Playwright.Stealth/Resources/js/*.js and added them as EmbeddedResource in the project file so StealthScriptProvider can load them at runtime.
  • Added test projects and tests: tests/Playwright.Stealth.Tests with StealthConfigTests (unit-style assertion of defaults) and StealthIntegrationTests (explicit integration test hitting external sites, gated by RUN_STEALTH_INTEGRATION_TESTS=1).
  • Updated README.md development instructions to use dotnet restore Playwright.Stealth.sln and dotnet test --solution Playwright.Stealth.sln --configuration Release and documented .NET SDK requirement 10.0.102+.

Testing

  • Ran dotnet build Playwright.Stealth.sln, which completed successfully (no errors).
  • Attempted dotnet test Playwright.Stealth.sln initially failed due to the .NET 10 MTP/VSTest mismatch; resolved by adding TestingPlatformDotnetTestSupport and a global.json test runner configuration as described in docs.
  • Ran dotnet test --solution Playwright.Stealth.sln --configuration Release, which executed the test assembly and passed the added StealthConfigTests (final summary: tests passed).

Codex Task

Copilot AI review requested due to automatic review settings February 3, 2026 19:33
Copy link

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: ed35ee613b

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines 2 to 3
Object.defineProperty(Object.getPrototypeOf(navigator), 'platform', {
get: () => opts.navigator_plaftorm,

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Fix typo so navigator.platform override works

The script gates on opts.navigator_platform but returns opts.navigator_plaftorm (typo), so when a caller sets StealthConfig.NavigatorPlatformValue, the injected getter returns undefined instead of the requested platform string. This silently disables the override and can also cause downstream fingerprinting checks to see an empty platform value, reducing the effectiveness of stealth for any run that provides a platform value. Renaming the property access to opts.navigator_platform fixes the mismatch.

Useful? React with 👍 / 👎.

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR migrates the Playwright.Stealth library to .NET 10, adding shared build configuration, CI infrastructure, embedded JavaScript stealth evasion scripts, and automated tests. The implementation provides a .NET API wrapper around the original Playwright stealth scripts to help avoid bot detection in headless browser automation.

Changes:

  • Added .NET 10 project configuration with shared props, SDK pinning, and Microsoft.Testing.Platform support
  • Implemented library surface with StealthConfig, StealthScriptProvider, and extension methods for applying stealth scripts to pages/contexts
  • Added GitHub Actions CI workflow and basic test coverage including unit tests and integration tests

Reviewed changes

Copilot reviewed 31 out of 32 changed files in this pull request and generated 9 comments.

Show a summary per file
File Description
Directory.Build.props Defines .NET 10 target framework, C# 14 language version, analyzer settings, and package metadata
global.json Pins SDK to version 10.0.102 and configures Microsoft.Testing.Platform as the test runner
.github/workflows/ci.yml CI workflow that restores, builds, and tests the solution on ubuntu-latest
src/Playwright.Stealth/Playwright.Stealth.csproj Main library project file with Playwright dependency and embedded JS resources
src/Playwright.Stealth/StealthConfig.cs Configuration class with boolean flags and options for stealth script behavior
src/Playwright.Stealth/StealthScriptProvider.cs Internal class that loads embedded JS scripts and builds combined script output
src/Playwright.Stealth/PlaywrightStealthExtensions.cs Public extension methods to apply stealth scripts to IPage and IBrowserContext
src/Playwright.Stealth/Resources/js/*.js Embedded JavaScript stealth evasion scripts (17 files)
tests/Playwright.Stealth.Tests/Playwright.Stealth.Tests.csproj Test project with TUnit framework dependencies
tests/Playwright.Stealth.Tests/StealthConfigTests.cs Unit tests verifying StealthConfig default values
tests/Playwright.Stealth.Tests/StealthIntegrationTests.cs Integration tests validating stealth behavior against bot detection sites
README.md Documentation with installation, usage examples, and development instructions
Playwright.Stealth.sln Solution file organizing source and test projects
.gitignore Standard .NET gitignore configuration

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

if (!('csi' in window.chrome) && (window.performance || window.performance.timing)) {
const {csi_timing} = window.performance

log.info('loading chrome.csi.js')
Copy link

Copilot AI Feb 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The variable log is used but never defined. This will cause a ReferenceError when this script executes. Either remove the log.info call or ensure a log object is defined before use.

Copilot uses AI. Check for mistakes.
// Check if we're running headful and don't need to mock anything
// Check that the Navigation Timing API v1 is available, we need that
if (!('csi' in window.chrome) && (window.performance || window.performance.timing)) {
const {csi_timing} = window.performance
Copy link

Copilot AI Feb 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The variable csi_timing is referenced but window.performance doesn't have a csi_timing property. This should likely be window.performance.timing to match the pattern used in other chrome.*.js files.

Suggested change
const {csi_timing} = window.performance
const csi_timing = window.performance.timing

Copilot uses AI. Check for mistakes.
@@ -0,0 +1,25 @@
console.log(opts)
Copy link

Copilot AI Feb 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Console.log statement left in production code. This debug output could reveal the presence of stealth modifications to bot detection systems. Consider removing it.

Suggested change
console.log(opts)

Copilot uses AI. Check for mistakes.
Comment on lines +20 to +21
<RepositoryUrl>https://github.com/AtuboDad/playwright_stealth</RepositoryUrl>
<PackageProjectUrl>https://github.com/AtuboDad/playwright_stealth</PackageProjectUrl>
Copy link

Copilot AI Feb 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The repository URLs point to AtuboDad/playwright_stealth, which appears to be the Python version of the original library, not this .NET port. Consider updating these URLs to point to the actual repository for this .NET implementation if it's hosted separately, or clarify the relationship to the original Python project.

Suggested change
<RepositoryUrl>https://github.com/AtuboDad/playwright_stealth</RepositoryUrl>
<PackageProjectUrl>https://github.com/AtuboDad/playwright_stealth</PackageProjectUrl>
<RepositoryUrl>https://github.com/ManagedCode</RepositoryUrl>
<PackageProjectUrl>https://github.com/ManagedCode</PackageProjectUrl>

Copilot uses AI. Check for mistakes.
await Assert.That(bodyText.Contains("automation", StringComparison.OrdinalIgnoreCase)).IsFalse();
}

private sealed record StealthSite(string Url, string Name);
Copy link

Copilot AI Feb 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Name property in the StealthSite record is defined but never used. Consider removing it if it's not needed, or use it in logging/reporting to make test failures more informative (e.g., include the site name in assertion failure messages).

Copilot uses AI. Check for mistakes.
@@ -0,0 +1,5 @@
if (opts.navigator_platform) {
Object.defineProperty(Object.getPrototypeOf(navigator), 'platform', {
get: () => opts.navigator_plaftorm,
Copy link

Copilot AI Feb 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Typo in the property name: navigator_plaftorm should be navigator_platform. This will cause the platform override to not work correctly as it reads from opts.navigator_plaftorm instead of opts.navigator_platform.

Suggested change
get: () => opts.navigator_plaftorm,
get: () => opts.navigator_platform,

Copilot uses AI. Check for mistakes.
Comment on lines +7 to +9
console.log(`current window outer height ${window.outerHeight}`)
window.outerHeight = window.innerHeight + windowFrame
console.log(`new window outer height ${window.outerHeight}`)
Copy link

Copilot AI Feb 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Console.log statements left in production code. These debug logs will be visible in the browser console and could reveal the presence of stealth modifications. Consider removing or conditionally enabling them.

Suggested change
console.log(`current window outer height ${window.outerHeight}`)
window.outerHeight = window.innerHeight + windowFrame
console.log(`new window outer height ${window.outerHeight}`)
window.outerHeight = window.innerHeight + windowFrame

Copilot uses AI. Check for mistakes.
Comment on lines +68 to +70
await Assert.That(bodyText.Contains("bot", StringComparison.OrdinalIgnoreCase)).IsFalse();
await Assert.That(bodyText.Contains("headless", StringComparison.OrdinalIgnoreCase)).IsFalse();
await Assert.That(bodyText.Contains("automation", StringComparison.OrdinalIgnoreCase)).IsFalse();
Copy link

Copilot AI Feb 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This assertion checks if bodyText contains "bot", "headless", or "automation" in a case-insensitive manner, but the test is trying to assert these should NOT be present (expecting false). However, legitimate page content may contain these words (e.g., "robot", "About", "automatic"). Consider using more specific selectors or checks for bot detection warnings rather than substring matching on the entire body text.

Copilot uses AI. Check for mistakes.
newHandler[trap] = function() {
try {
// Forward the call to the defined proxy handler
return handler[trap].apply(this, arguments || [])
Copy link

Copilot AI Feb 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This use of variable 'arguments' always evaluates to true.

Suggested change
return handler[trap].apply(this, arguments || [])
return handler[trap].apply(this, arguments)

Copilot uses AI. Check for mistakes.
@KSemenenko KSemenenko merged commit e148f11 into main Feb 3, 2026
1 check passed
@KSemenenko KSemenenko deleted the codex/create-open-source-nuget-package-with-playwright branch February 3, 2026 20:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant