Migrate to .NET 10: add shared build config, CI, embedded stealth scripts, and tests#1
Conversation
There was a problem hiding this comment.
💡 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".
| Object.defineProperty(Object.getPrototypeOf(navigator), 'platform', { | ||
| get: () => opts.navigator_plaftorm, |
There was a problem hiding this comment.
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 👍 / 👎.
There was a problem hiding this comment.
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') |
There was a problem hiding this comment.
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.
| // 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 |
There was a problem hiding this comment.
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.
| const {csi_timing} = window.performance | |
| const csi_timing = window.performance.timing |
| @@ -0,0 +1,25 @@ | |||
| console.log(opts) | |||
There was a problem hiding this comment.
Console.log statement left in production code. This debug output could reveal the presence of stealth modifications to bot detection systems. Consider removing it.
| console.log(opts) |
| <RepositoryUrl>https://github.com/AtuboDad/playwright_stealth</RepositoryUrl> | ||
| <PackageProjectUrl>https://github.com/AtuboDad/playwright_stealth</PackageProjectUrl> |
There was a problem hiding this comment.
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.
| <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> |
| await Assert.That(bodyText.Contains("automation", StringComparison.OrdinalIgnoreCase)).IsFalse(); | ||
| } | ||
|
|
||
| private sealed record StealthSite(string Url, string Name); |
There was a problem hiding this comment.
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).
| @@ -0,0 +1,5 @@ | |||
| if (opts.navigator_platform) { | |||
| Object.defineProperty(Object.getPrototypeOf(navigator), 'platform', { | |||
| get: () => opts.navigator_plaftorm, | |||
There was a problem hiding this comment.
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.
| get: () => opts.navigator_plaftorm, | |
| get: () => opts.navigator_platform, |
| console.log(`current window outer height ${window.outerHeight}`) | ||
| window.outerHeight = window.innerHeight + windowFrame | ||
| console.log(`new window outer height ${window.outerHeight}`) |
There was a problem hiding this comment.
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.
| 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 |
| 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(); |
There was a problem hiding this comment.
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.
| newHandler[trap] = function() { | ||
| try { | ||
| // Forward the call to the defined proxy handler | ||
| return handler[trap].apply(this, arguments || []) |
There was a problem hiding this comment.
This use of variable 'arguments' always evaluates to true.
| return handler[trap].apply(this, arguments || []) | |
| return handler[trap].apply(this, arguments) |
Motivation
Description
Directory.Build.props(targets .NET 10, enables analyzers, suppresses XML warnings, and enables MTP test support) andglobal.json(pins SDK10.0.102and opts into theMicrosoft.Testing.Platformtest runner)..github/workflows/ci.ymlthat restores, builds and runs tests usingdotnetonubuntu-latest.StealthConfig(options),StealthScriptProvider(loads embedded JS), andPlaywrightStealthExtensionsexposingApplyStealthAsync(this IPage, ...)andApplyStealthAsync(this IBrowserContext, ...)which inject the combined init scripts.src/Playwright.Stealth/Resources/js/*.jsand added them asEmbeddedResourcein the project file soStealthScriptProvidercan load them at runtime.tests/Playwright.Stealth.TestswithStealthConfigTests(unit-style assertion of defaults) andStealthIntegrationTests(explicit integration test hitting external sites, gated byRUN_STEALTH_INTEGRATION_TESTS=1).README.mddevelopment instructions to usedotnet restore Playwright.Stealth.slnanddotnet test --solution Playwright.Stealth.sln --configuration Releaseand documented .NET SDK requirement10.0.102+.Testing
dotnet build Playwright.Stealth.sln, which completed successfully (no errors).dotnet test Playwright.Stealth.slninitially failed due to the .NET 10 MTP/VSTest mismatch; resolved by addingTestingPlatformDotnetTestSupportand aglobal.jsontest runner configuration as described in docs.dotnet test --solution Playwright.Stealth.sln --configuration Release, which executed the test assembly and passed the addedStealthConfigTests(final summary: tests passed).Codex Task