Skip to content

Commit ea4e0df

Browse files
committed
fix: preserve native clock precision by default
1 parent 5da3706 commit ea4e0df

5 files changed

Lines changed: 20 additions & 10 deletions

File tree

‎README.md‎

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,7 @@ On Chromium, `context.ApplyStealthAsync` also initializes dedicated, module, sha
139139
|-------|-------------|--------|
140140
| CDP detection | Masks Chrome DevTools Protocol traces | `CdpDetection` |
141141
| Automation properties | Removes `cdc_*`, `$cdc_*`, `domAutomationController` | `AutomationProperties` |
142-
| Performance jitter | Monotonic millisecond clock precision; enabled by default | `PerformanceJitter` |
142+
| Performance jitter | Optional monotonic millisecond clock precision; disabled by default to preserve native timing | `PerformanceJitter` |
143143

144144
### DOM & Internals
145145
| Patch | Description | Config |
@@ -174,6 +174,12 @@ string[] args = PlaywrightStealthExtensions.StealthArgs;
174174

175175
On Linux these arguments select ANGLE's native OpenGL backend through EGL and allow the installed driver, including Mesa on CI runners, instead of falling back to SwiftShader. This also supports headless rendering without a display. WebGL rendering and shader reporting remain native in pages and workers. `LaunchStealthAsync` preserves an explicitly supplied `--use-angle` or `--use-gl` backend. Linux hosts need a working EGL/OpenGL driver; headful runs also need a display.
176176

177+
For Ubuntu runners, install the native graphics libraries alongside Playwright's browser dependencies:
178+
179+
```bash
180+
sudo apt-get install -y --no-install-recommends libegl1 libegl-mesa0 libgles2 libgl1-mesa-dri
181+
```
182+
177183
## Configuration Reference
178184

179185
### Toggle Options (bool)

‎src/Playwright.Stealth/StealthConfig.cs‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ public sealed class StealthConfig
3131
public bool AutomationProperties { get; init; } = true;
3232
public int NavigatorMaxTouchPoints { get; init; } = -1;
3333
public bool CanvasFingerprint { get; init; }
34-
public bool PerformanceJitter { get; init; } = true;
34+
public bool PerformanceJitter { get; init; }
3535
public bool NavigatorPdfViewer { get; init; } = true;
3636
public bool AudioContext { get; init; }
3737

‎tests/Playwright.Stealth.Tests/StealthCompatibilityTests.cs‎

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ await WithBrowserAsync(headless, async browser =>
3939
[HTMLImageElement.prototype, 'naturalHeight'],
4040
[HTMLIFrameElement.prototype, 'contentWindow'],
4141
[SpeechSynthesis.prototype, 'getVoices'],
42+
[Performance.prototype, 'now'],
4243
[Permissions.prototype, 'query'],
4344
[Navigator.prototype, 'webdriver'],
4445
[WebGLRenderingContext.prototype, 'getParameter'],
@@ -121,18 +122,20 @@ public async Task LaunchStealth_DefaultOptions_RendersWebGlInPageAndWorker(bool
121122
}
122123

123124
[Test]
124-
[Arguments(true)]
125-
[Arguments(false)]
126-
public async Task ApplyStealth_PerformanceClock_RemainsMonotonicAndPreservesNativeErrors(bool headless)
125+
[Arguments(true, false)]
126+
[Arguments(false, false)]
127+
[Arguments(true, true)]
128+
[Arguments(false, true)]
129+
public async Task ApplyStealth_PerformanceClock_RemainsMonotonicAndPreservesNativeErrors(bool headless, bool reducePrecision)
127130
{
128131
await WithBrowserAsync(headless, async browser =>
129132
{
130133
await using IBrowserContext context = await browser.NewContextAsync();
131-
await context.ApplyStealthAsync();
134+
await context.ApplyStealthAsync(new() { PerformanceJitter = reducePrecision });
132135
IPage page = await context.NewPageAsync();
133136
await page.GotoAsync("about:blank");
134137
string[] failures = await page.EvaluateAsync<string[]>("""
135-
() => {
138+
reducePrecision => {
136139
const failures = [];
137140
const now = Performance.prototype.now;
138141
try { now.call({}); failures.push('accepted invalid receiver'); }
@@ -142,6 +145,7 @@ await WithBrowserAsync(headless, async browser =>
142145
let previous = performance.now();
143146
for (let i = 0; i < 10000; i++) {
144147
const current = performance.now();
148+
if (reducePrecision && !Number.isInteger(current)) { failures.push('clock precision opt-in ignored'); break; }
145149
if (current < previous) { failures.push('clock moved backwards'); break; }
146150
previous = current;
147151
}
@@ -150,7 +154,7 @@ await WithBrowserAsync(headless, async browser =>
150154
if (Math.abs(performance.timeOrigin + performance.now() - Date.now()) > 1000) failures.push('clock time origin');
151155
return failures;
152156
}
153-
""");
157+
""", reducePrecision);
154158
await Assert.That(failures).IsEmpty();
155159
});
156160
}

‎tests/Playwright.Stealth.Tests/StealthConfigTests.cs‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ public async Task Defaults_Should_Enable_Core_Stealth_Scripts()
2121
await Assert.That(config.Languages.Count).IsEqualTo(0);
2222
await Assert.That(config.CanvasFingerprint).IsFalse();
2323
await Assert.That(config.AudioContext).IsFalse();
24-
await Assert.That(config.PerformanceJitter).IsTrue();
24+
await Assert.That(config.PerformanceJitter).IsFalse();
2525
await Assert.That(config.Vendor).IsEmpty();
2626
await Assert.That(config.Renderer).IsEmpty();
2727
}

‎tests/Playwright.Stealth.Tests/StealthScriptProviderTests.cs‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ public async Task BuildScripts_Should_Include_Default_Scripts()
3030
await Assert.That(ContainsScript(scripts, "automationPatterns")).IsTrue();
3131
await Assert.That(ContainsScript(scripts, "Object.getPrototypeOf(navigator), 'maxTouchPoints'")).IsFalse();
3232
await Assert.That(ContainsScript(scripts, "noiseSeed")).IsFalse();
33-
await Assert.That(ContainsScript(scripts, "Math.floor(utils.cache.Reflect.apply")).IsTrue();
33+
await Assert.That(ContainsScript(scripts, "Math.floor(utils.cache.Reflect.apply")).IsFalse();
3434
await Assert.That(ContainsScript(scripts, "pdfViewerEnabled")).IsTrue();
3535
await Assert.That(ContainsScript(scripts, "const audioNoiseSeed")).IsFalse();
3636
}

0 commit comments

Comments
 (0)