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)