diff --git a/src/PlaywrightNative.Tests/Page/PageTransition/PageTransitionTests.cs b/src/PlaywrightNative.Tests/Page/PageTransition/PageTransitionTests.cs
new file mode 100644
index 00000000..3154b88d
--- /dev/null
+++ b/src/PlaywrightNative.Tests/Page/PageTransition/PageTransitionTests.cs
@@ -0,0 +1,77 @@
+/*
+ * Copyright (c) 2020 Dario Kondratiuk
+ * Modifications copyright (c) Microsoft Corporation.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+using System.Linq;
+using System.Threading.Tasks;
+using Microsoft.Playwright;
+using NUnit.Framework;
+using PlaywrightNative.NUnit;
+
+namespace PlaywrightNative.Tests
+{
+ ///
+ /// Official page-transition.spec.ts .
+ ///
+ [TestFixture]
+ public class PageTransitionTests : PageTestEx
+ {
+ [PlaywrightTest("page-transition.spec.ts", "should not crash when filter transition completes")]
+ [Test]
+ [Timeout(TestConstants.DefaultTestTimeout)]
+ public async Task ShouldNotCrashWhenFilterTransitionCompletes()
+ {
+ if (TestConstants.IsWebKit)
+ {
+ Assert.Ignore("official fixme(browserName === 'webkit'): Web process crashes on the compositor thread when a filter transitions to none");
+ return;
+ }
+
+ await using IBrowser browser = await BrowserLauncher.LaunchAsync().ConfigureAwait(false);
+ await using IBrowserContext context = await browser.NewContextAsync().ConfigureAwait(false);
+ IPage page = await context.NewPageAsync().ConfigureAwait(false);
+ await page.SetContentAsync(@"
+
+ " + string.Concat(Enumerable.Repeat("
hello
", 10)) + @"
+ ").ConfigureAwait(false);
+ bool crashed = false;
+ page.Crash += (_, _) => crashed = true;
+ for (int i = 0; i < 10 && !crashed; i++)
+ {
+ try
+ {
+ await page.EvaluateAsync(@"async () => {
+ const divs = [...document.querySelectorAll('div')];
+ const settled = () => Promise.all(divs.map(div => new Promise(f => div.addEventListener('transitionend', f, { once: true }))));
+ let promise = settled();
+ divs.forEach(div => div.classList.add('dim'));
+ await promise;
+ promise = settled();
+ divs.forEach(div => div.classList.remove('dim'));
+ await promise;
+ }").ConfigureAwait(false);
+ }
+ catch (PlaywrightException)
+ {
+ }
+ }
+
+ Assert.That(crashed, Is.False);
+ }
+ }
+}
diff --git a/src/PlaywrightNative.Tests/PageNetworkResponseParityTests.cs b/src/PlaywrightNative.Tests/PageNetworkResponseParityTests.cs
index ceed21cb..238dc842 100644
--- a/src/PlaywrightNative.Tests/PageNetworkResponseParityTests.cs
+++ b/src/PlaywrightNative.Tests/PageNetworkResponseParityTests.cs
@@ -330,6 +330,29 @@ public async Task ShouldReturnUncompressedTextForBrotliEncoding()
Assert.That(await response.TextAsync().ConfigureAwait(false), Is.EqualTo(SimpleJsonBody));
}
+ [PlaywrightTest("page-network-response.spec.ts", "should return text for identity encoding")]
+ [Test]
+ [Timeout(30_000)]
+ public async Task ShouldReturnTextForIdentityEncoding()
+ {
+ EnsureServer();
+ const string text = "hello
";
+ Server.SetRoute("/identity.html", async http =>
+ {
+ http.Response.StatusCode = 200;
+ http.Response.Headers["Content-Type"] = "text/html; charset=utf-8";
+ http.Response.Headers["Content-Encoding"] = "Identity";
+ await http.Response.WriteAsync(text).ConfigureAwait(false);
+ });
+
+ await using IBrowser browser = await BrowserLauncher.LaunchAsync().ConfigureAwait(false);
+ await using IBrowserContext context = await browser.NewContextAsync().ConfigureAwait(false);
+ IPage page = await context.NewPageAsync().ConfigureAwait(false);
+ IResponse response = await page.GoToAsync(Prefix + "/identity.html").ConfigureAwait(false);
+ Assert.That(HeaderMap.All(response.Headers)["content-encoding"], Is.EqualTo("Identity"));
+ Assert.That(await response.TextAsync().ConfigureAwait(false), Is.EqualTo(text));
+ }
+
[PlaywrightTest("page-network-response.spec.ts", "should throw when requesting body of redirected response")]
[Test]
[Timeout(30_000)]
diff --git a/src/PlaywrightNative.Tests/PageScreenshotParityTests.cs b/src/PlaywrightNative.Tests/PageScreenshotParityTests.cs
index dc6235bb..58cb0dfa 100644
--- a/src/PlaywrightNative.Tests/PageScreenshotParityTests.cs
+++ b/src/PlaywrightNative.Tests/PageScreenshotParityTests.cs
@@ -20,6 +20,7 @@
using System.Text.Json;
using System.Threading.Tasks;
using NUnit.Framework;
+using PlaywrightNative.Helpers;
using PlaywrightNative.NUnit;
using PlaywrightNative.TestServer;
@@ -27,9 +28,6 @@ namespace PlaywrightNative.Tests
{
///
/// Official page-screenshot.spec.ts titles.
- /// Skipped (Node-only): __testHookBeforeScreenshot ,
- /// should capture screenshots after layoutchanges in transitionend event
- /// (uses Node window.builtins.Date ).
/// Do not edit leftover PageScreenshot* /
/// ExpectScreenshot* .
///
@@ -863,6 +861,36 @@ public async Task ShouldNotCaptureCssAnimationsInShadowDom()
}
}
+ [PlaywrightTest("page-screenshot.spec.ts", "should stop animations that happen right before screenshot")]
+ [Test]
+ [Timeout(TestConstants.DefaultTestTimeout)]
+ public async Task ShouldStopAnimationsThatHappenRightBeforeScreenshot()
+ {
+ await using IBrowser browser = await BrowserLauncher.LaunchAsync().ConfigureAwait(false);
+ await using IBrowserContext context = await browser.NewContextAsync().ConfigureAwait(false);
+ IPage page = await context.NewPageAsync().ConfigureAwait(false);
+ await page.GoToAsync(Prefix + "/rotate-z.html").ConfigureAwait(false);
+
+ // Stop rotating bar.
+ await page.EvalOnSelectorAsync("div", "el => el.style.setProperty('animation', 'none')").ConfigureAwait(false);
+ byte[] buffer1;
+
+ // Start rotating bar right before screenshot.
+ ScreenshotDecorations.TestHookBeforeScreenshot.Value = () => page.EvalOnSelectorAsync("div", "el => el.style.removeProperty('animation')");
+ try
+ {
+ buffer1 = await page.ScreenshotAsync(new() { Animations = ScreenshotAnimations.Disabled }).ConfigureAwait(false);
+ }
+ finally
+ {
+ ScreenshotDecorations.TestHookBeforeScreenshot.Value = null;
+ }
+
+ await RafRafAsync(page).ConfigureAwait(false);
+ byte[] buffer2 = await page.ScreenshotAsync(new() { Animations = ScreenshotAnimations.Disabled }).ConfigureAwait(false);
+ Assert.That(ScreenshotComparer.Matches(buffer1, buffer2, threshold: 0.2f), Is.True);
+ }
+
[PlaywrightTest("page-screenshot.spec.ts", "should resume infinite animations")]
[Test]
[Timeout(TestConstants.DefaultTestTimeout)]
@@ -925,6 +953,52 @@ public async Task ShouldFireTransitionendForFiniteTransitions()
Assert.That(await page.EvaluateAsync("() => window['__TRANSITION_END']").ConfigureAwait(false), Is.True);
}
+ [PlaywrightTest("page-screenshot.spec.ts", "should capture screenshots after layoutchanges in transitionend event")]
+ [Test]
+ [Timeout(TestConstants.DefaultTestTimeout)]
+ public async Task ShouldCaptureScreenshotsAfterLayoutchangesInTransitionendEvent()
+ {
+ await using IBrowser browser = await BrowserLauncher.LaunchAsync().ConfigureAwait(false);
+ await using IBrowserContext context = await browser.NewContextAsync().ConfigureAwait(false);
+ IPage page = await context.NewPageAsync().ConfigureAwait(false);
+ await page.GoToAsync(Prefix + "/css-transition.html").ConfigureAwait(false);
+ ILocator div = page.Locator("div");
+
+ // Upstream reads the unpatched Date from window.builtins (test-only injected
+ // script); without a clock installed that is the page Date.
+ await div.EvaluateAsync(@"el => {
+ el.addEventListener('transitionend', () => {
+ const builtinDate = (window.builtins && window.builtins.Date) || Date;
+ const time = builtinDate.now();
+ // Block main thread for 200ms, emulating heavy layout.
+ while (builtinDate.now() - time < 200) {}
+ const h1 = document.createElement('h1');
+ h1.textContent = 'woof-woof';
+ document.body.append(h1);
+ }, false);
+ }").ConfigureAwait(false);
+
+ // make sure transition is actually running
+ byte[] running1 = await page.ScreenshotAsync().ConfigureAwait(false);
+ await RafRafAsync(page).ConfigureAwait(false);
+ byte[] running2 = await page.ScreenshotAsync().ConfigureAwait(false);
+ Assert.That(running2, Is.Not.EqualTo(running1));
+
+ // 1. Make a screenshot that finishes all finite animations
+ // and triggers layout.
+ byte[] screenshot1 = await page.ScreenshotAsync(new() { Animations = ScreenshotAnimations.Disabled }).ConfigureAwait(false);
+
+ // 2. Make a second screenshot after h1 is on screen.
+ await Assertions.Expect(page.Locator("h1")).ToBeVisibleAsync().ConfigureAwait(false);
+ await Assertions.Expect(page.Locator("h1")).ToHaveTextAsync("woof-woof").ConfigureAwait(false);
+ byte[] screenshot2 = await page.ScreenshotAsync().ConfigureAwait(false);
+
+ // 3. Make sure both screenshots are equal, meaning that
+ // first screenshot actually was taken after transitionend
+ // changed layout.
+ Assert.That(screenshot2, Is.EqualTo(screenshot1));
+ }
+
[PlaywrightTest("page-screenshot.spec.ts", "should not change animation with playbackRate equal to 0")]
[Test]
[Timeout(TestConstants.DefaultTestTimeout)]
diff --git a/src/PlaywrightNative/Helpers/ScreenshotDecorations.cs b/src/PlaywrightNative/Helpers/ScreenshotDecorations.cs
index 629a3821..91b62502 100644
--- a/src/PlaywrightNative/Helpers/ScreenshotDecorations.cs
+++ b/src/PlaywrightNative/Helpers/ScreenshotDecorations.cs
@@ -175,6 +175,12 @@ internal static class ScreenshotDecorations
style.remove();
})()";
+ ///
+ /// Test-only hook (upstream __testHookBeforeScreenshot ). Runs after the
+ /// page is prepared and right before the capture.
+ ///
+ internal static readonly AsyncLocal> TestHookBeforeScreenshot = new AsyncLocal>();
+
private static readonly ConcurrentDictionary ScreenshotGates = new ConcurrentDictionary();
///
@@ -295,6 +301,11 @@ internal static async Task CaptureAsync(
await ScreenshotMask.ApplyAsync(page, mask, maskColor, tags).ConfigureAwait(false);
await WaitForFontsAsync(page).ConfigureAwait(false);
+ if (TestHookBeforeScreenshot.Value != null)
+ {
+ await TestHookBeforeScreenshot.Value().ConfigureAwait(false);
+ }
+
return await capture().ConfigureAwait(false);
}
catch (Exception ex) when (DestroyedContext.IsDestroyedContext(ex)