From 5480773d786eb116bab3295a80a4ef3d27466c2d Mon Sep 17 00:00:00 2001 From: Dmitry Gozman Date: Thu, 12 Nov 2026 11:09:03 +0000 Subject: [PATCH 1/2] feat(assertions): add Expect(callback).ToPassAsync() Fixes: https://github.com/microsoft/playwright-dotnet/issues/2314 --- src/Playwright.MSTest/PlaywrightTest.cs | 11 ++ src/Playwright.NUnit/PlaywrightTest.cs | 11 ++ src/Playwright.NUnit/WorkerAwareTest.cs | 10 + .../Assertions/FunctionAssertionsTests.cs | 178 ++++++++++++++++++ src/Playwright.Xunit.v3/PlaywrightTest.cs | 11 ++ src/Playwright.Xunit/PlaywrightTest.cs | 11 ++ src/Playwright/Assertions.cs | 28 +++ src/Playwright/Core/FunctionAssertions.cs | 87 ++++++++- 8 files changed, 344 insertions(+), 3 deletions(-) create mode 100644 src/Playwright.Tests/Assertions/FunctionAssertionsTests.cs diff --git a/src/Playwright.MSTest/PlaywrightTest.cs b/src/Playwright.MSTest/PlaywrightTest.cs index c997dc39a..031dad667 100644 --- a/src/Playwright.MSTest/PlaywrightTest.cs +++ b/src/Playwright.MSTest/PlaywrightTest.cs @@ -22,6 +22,7 @@ * SOFTWARE. */ +using System; using System.Threading.Tasks; using Microsoft.Playwright.TestAdapter; using Microsoft.VisualStudio.TestTools.UnitTesting; @@ -48,15 +49,25 @@ public async Task PlaywrightSetup() public static void SetDefaultExpectTimeout(float timeout) => Assertions.SetDefaultExpectTimeout(timeout); + public static void SetDefaultExpectToPassTimeout(float timeout) => Assertions.SetDefaultExpectToPassTimeout(timeout); + public ILocatorAssertions Expect(ILocator locator) => Assertions.Expect(locator); public IPageAssertions Expect(IPage page) => Assertions.Expect(page); public IAPIResponseAssertions Expect(IAPIResponse response) => Assertions.Expect(response); + public IFunctionAssertions Expect(Func callback) => Assertions.Expect(callback); + + public IFunctionAssertions Expect(Action callback) => Assertions.Expect(callback); + public ILocatorAssertions Expect(ILocator locator, string message) => Assertions.Expect(locator, message); public IPageAssertions Expect(IPage page, string message) => Assertions.Expect(page, message); public IAPIResponseAssertions Expect(IAPIResponse response, string message) => Assertions.Expect(response, message); + + public IFunctionAssertions Expect(Func callback, string message) => Assertions.Expect(callback, message); + + public IFunctionAssertions Expect(Action callback, string message) => Assertions.Expect(callback, message); } diff --git a/src/Playwright.NUnit/PlaywrightTest.cs b/src/Playwright.NUnit/PlaywrightTest.cs index 64ff05b5a..758a06d6c 100644 --- a/src/Playwright.NUnit/PlaywrightTest.cs +++ b/src/Playwright.NUnit/PlaywrightTest.cs @@ -22,6 +22,7 @@ * SOFTWARE. */ +using System; using System.Threading.Tasks; using Microsoft.Playwright.TestAdapter; using NUnit.Framework; @@ -48,15 +49,25 @@ public async Task PlaywrightSetup() public static void SetDefaultExpectTimeout(float timeout) => Assertions.SetDefaultExpectTimeout(timeout); + public static void SetDefaultExpectToPassTimeout(float timeout) => Assertions.SetDefaultExpectToPassTimeout(timeout); + public ILocatorAssertions Expect(ILocator locator) => Assertions.Expect(locator); public IPageAssertions Expect(IPage page) => Assertions.Expect(page); public IAPIResponseAssertions Expect(IAPIResponse response) => Assertions.Expect(response); + public IFunctionAssertions Expect(Func callback) => Assertions.Expect(callback); + + public IFunctionAssertions Expect(Action callback) => Assertions.Expect(callback); + public ILocatorAssertions Expect(ILocator locator, string message) => Assertions.Expect(locator, message); public IPageAssertions Expect(IPage page, string message) => Assertions.Expect(page, message); public IAPIResponseAssertions Expect(IAPIResponse response, string message) => Assertions.Expect(response, message); + + public IFunctionAssertions Expect(Func callback, string message) => Assertions.Expect(callback, message); + + public IFunctionAssertions Expect(Action callback, string message) => Assertions.Expect(callback, message); } diff --git a/src/Playwright.NUnit/WorkerAwareTest.cs b/src/Playwright.NUnit/WorkerAwareTest.cs index 658d55315..186fab258 100644 --- a/src/Playwright.NUnit/WorkerAwareTest.cs +++ b/src/Playwright.NUnit/WorkerAwareTest.cs @@ -31,6 +31,7 @@ using Microsoft.Playwright.TestAdapter; using NUnit.Framework; using NUnit.Framework.Interfaces; +using NUnit.Framework.Internal; namespace Microsoft.Playwright.NUnit; @@ -44,6 +45,15 @@ static WorkerAwareTest() // Workers are pooled until the end of the test run, so their services are disposed when the process exits. // This closes the browsers. A remote browser is disconnected abruptly otherwise. AppDomain.CurrentDomain.ProcessExit += (_, _) => DisposeAllWorkers(); + + // NUnit fails the test for a recorded assertion failure, also when the exception is caught. + FunctionAssertions.AttemptRunner = async callback => + { + using (new TestExecutionContext.IsolatedContext()) + { + await callback().ConfigureAwait(false); + } + }; } private static void DisposeAllWorkers() diff --git a/src/Playwright.Tests/Assertions/FunctionAssertionsTests.cs b/src/Playwright.Tests/Assertions/FunctionAssertionsTests.cs new file mode 100644 index 000000000..136cc7a7d --- /dev/null +++ b/src/Playwright.Tests/Assertions/FunctionAssertionsTests.cs @@ -0,0 +1,178 @@ +/* + * MIT License + * + * Copyright (c) Microsoft Corporation. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and / or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +using System.Diagnostics; + +namespace Microsoft.Playwright.Tests.Assertions; + +public class FunctionAssertionsTests : PageTestEx +{ + [PlaywrightTest("playwright-test/expect-to-pass.spec.ts", "should retry predicate")] + public async Task ShouldRetryPredicate() + { + var i = 0; + await Expect(() => + { + Assert.AreEqual(3, ++i); + }).ToPassAsync(); + Assert.AreEqual(3, i); + + i = 0; + await Expect(async () => + { + await Task.Delay(50); + Assert.AreEqual(3, ++i); + }).ToPassAsync(); + Assert.AreEqual(3, i); + } + + [PlaywrightTest] + public async Task ShouldRetryPredicateWithStaticExpect() + { + var i = 0; + await Microsoft.Playwright.Assertions.Expect(async () => + { + await Task.Delay(50); + Assert.AreEqual(3, ++i); + }).ToPassAsync(); + Assert.AreEqual(3, i); + } + + [PlaywrightTest("playwright-test/expect-to-pass.spec.ts", "should respect timeout")] + public async Task ShouldRespectTimeout() + { + var exception = await PlaywrightAssert.ThrowsAsync(() => Expect(() => + { + Assert.AreEqual(2, 1); + }).ToPassAsync(new() { Timeout = 100 })); + StringAssert.Contains("Timeout 100ms exceeded while waiting on the predicate", exception.Message); + StringAssert.Contains("But was: 1", exception.Message); + Assert.IsInstanceOf(exception.InnerException); + } + + [PlaywrightTest("playwright-test/expect-to-pass.spec.ts", "should not fail when used with web-first assertion")] + public async Task ShouldNotFailWhenUsedWithWebFirstAssertion() + { + var i = 0; + await Expect(async () => + { + if (++i < 3) + { + await Expect(Page.Locator("body")).ToHaveTextAsync("foo", new() { Timeout = 1 }); + } + }).ToPassAsync(); + Assert.AreEqual(3, i); + } + + [PlaywrightTest("playwright-test/expect-to-pass.spec.ts", "should respect interval")] + public async Task ShouldRespectInterval() + { + var probes = 0; + var stopwatch = Stopwatch.StartNew(); + await PlaywrightAssert.ThrowsAsync(() => Expect(() => + { + ++probes; + Assert.AreEqual(2, 1); + }).ToPassAsync(new() { Timeout = 1000, Intervals = [0, 10000] })); + Assert.AreEqual(2, probes); + Assert.Less(stopwatch.ElapsedMilliseconds, 5000); + } + + [PlaywrightTest("playwright-test/expect-to-pass.spec.ts", "should use custom message")] + public async Task ShouldUseCustomMessage() + { + var exception = await PlaywrightAssert.ThrowsAsync(() => Expect(() => + { + Assert.AreEqual(3, 1); + }, "Custom message").ToPassAsync(new() { Timeout = 1 })); + StringAssert.StartsWith("Custom message\n\n", exception.Message); + + exception = await PlaywrightAssert.ThrowsAsync(() => Expect(async () => + { + await Task.Yield(); + Assert.AreEqual(3, 1); + }, "Custom message").ToPassAsync(new() { Timeout = 1 })); + StringAssert.StartsWith("Custom message\n\n", exception.Message); + } + + [PlaywrightTest] + public async Task ShouldBeAbleToSetDefaultTimeout() + { + try + { + SetDefaultExpectTimeout(1111); + SetDefaultExpectToPassTimeout(100); + var exception = await PlaywrightAssert.ThrowsAsync(() => Expect(() => + { + Assert.AreEqual(2, 1); + }).ToPassAsync()); + StringAssert.Contains("Timeout 100ms exceeded while waiting on the predicate", exception.Message); + + SetDefaultExpectToPassTimeout(30000); + var i = 0; + await Expect(async () => + { + await Task.Delay(400); + Assert.AreEqual(4, ++i); + }).ToPassAsync(); + } + finally + { + SetDefaultExpectTimeout(5000); + SetDefaultExpectToPassTimeout(30000); + } + } + + [PlaywrightTest] + public async Task ShouldUseTheLastIntervalForTheFollowingRetries() + { + var probes = 0; + await Expect(() => + { + Assert.AreEqual(5, ++probes); + }).ToPassAsync(new() { Intervals = [1] }); + Assert.AreEqual(5, probes); + } + + [PlaywrightTest] + public async Task ShouldTimeOutWhileTheCallbackIsRunning() + { + var stopwatch = Stopwatch.StartNew(); + var exception = await PlaywrightAssert.ThrowsAsync(() => Expect(() => Task.Delay(10000)).ToPassAsync(new() { Timeout = 100 })); + Assert.AreEqual("Timeout 100ms exceeded while waiting on the predicate", exception.Message); + Assert.Less(stopwatch.ElapsedMilliseconds, 5000); + } + + [PlaywrightTest] + public async Task ShouldRetryWithoutTimeout() + { + var i = 0; + await Expect(async () => + { + await Task.Yield(); + Assert.AreEqual(3, ++i); + }).ToPassAsync(new() { Timeout = 0, Intervals = [10] }); + Assert.AreEqual(3, i); + } +} diff --git a/src/Playwright.Xunit.v3/PlaywrightTest.cs b/src/Playwright.Xunit.v3/PlaywrightTest.cs index 47e336117..a7df7b21d 100644 --- a/src/Playwright.Xunit.v3/PlaywrightTest.cs +++ b/src/Playwright.Xunit.v3/PlaywrightTest.cs @@ -22,6 +22,7 @@ * SOFTWARE. */ +using System; using System.Threading.Tasks; using Microsoft.Playwright.TestAdapter; @@ -47,15 +48,25 @@ public override async ValueTask InitializeAsync() public static void SetDefaultExpectTimeout(float timeout) => Assertions.SetDefaultExpectTimeout(timeout); + public static void SetDefaultExpectToPassTimeout(float timeout) => Assertions.SetDefaultExpectToPassTimeout(timeout); + public ILocatorAssertions Expect(ILocator locator) => Assertions.Expect(locator); public IPageAssertions Expect(IPage page) => Assertions.Expect(page); public IAPIResponseAssertions Expect(IAPIResponse response) => Assertions.Expect(response); + public IFunctionAssertions Expect(Func callback) => Assertions.Expect(callback); + + public IFunctionAssertions Expect(Action callback) => Assertions.Expect(callback); + public ILocatorAssertions Expect(ILocator locator, string message) => Assertions.Expect(locator, message); public IPageAssertions Expect(IPage page, string message) => Assertions.Expect(page, message); public IAPIResponseAssertions Expect(IAPIResponse response, string message) => Assertions.Expect(response, message); + + public IFunctionAssertions Expect(Func callback, string message) => Assertions.Expect(callback, message); + + public IFunctionAssertions Expect(Action callback, string message) => Assertions.Expect(callback, message); } diff --git a/src/Playwright.Xunit/PlaywrightTest.cs b/src/Playwright.Xunit/PlaywrightTest.cs index 48f6a5d51..0336cda71 100644 --- a/src/Playwright.Xunit/PlaywrightTest.cs +++ b/src/Playwright.Xunit/PlaywrightTest.cs @@ -22,6 +22,7 @@ * SOFTWARE. */ +using System; using System.Threading.Tasks; using Microsoft.Playwright.TestAdapter; @@ -47,15 +48,25 @@ public override async Task InitializeAsync() public static void SetDefaultExpectTimeout(float timeout) => Assertions.SetDefaultExpectTimeout(timeout); + public static void SetDefaultExpectToPassTimeout(float timeout) => Assertions.SetDefaultExpectToPassTimeout(timeout); + public ILocatorAssertions Expect(ILocator locator) => Assertions.Expect(locator); public IPageAssertions Expect(IPage page) => Assertions.Expect(page); public IAPIResponseAssertions Expect(IAPIResponse response) => Assertions.Expect(response); + public IFunctionAssertions Expect(Func callback) => Assertions.Expect(callback); + + public IFunctionAssertions Expect(Action callback) => Assertions.Expect(callback); + public ILocatorAssertions Expect(ILocator locator, string message) => Assertions.Expect(locator, message); public IPageAssertions Expect(IPage page, string message) => Assertions.Expect(page, message); public IAPIResponseAssertions Expect(IAPIResponse response, string message) => Assertions.Expect(response, message); + + public IFunctionAssertions Expect(Func callback, string message) => Assertions.Expect(callback, message); + + public IFunctionAssertions Expect(Action callback, string message) => Assertions.Expect(callback, message); } diff --git a/src/Playwright/Assertions.cs b/src/Playwright/Assertions.cs index 222952ea6..00fdf3720 100644 --- a/src/Playwright/Assertions.cs +++ b/src/Playwright/Assertions.cs @@ -41,6 +41,17 @@ public static class Assertions /// public static void SetDefaultExpectTimeout(float timeout) => AssertionsBase.SetDefaultTimeout(timeout); + /// + /// Sets the default timeout for all future calls. + /// + /// The timeout in milliseconds. + /// + /// + /// The default timeout is 30 seconds. does not change it. + /// + /// + public static void SetDefaultExpectToPassTimeout(float timeout) => FunctionAssertions.SetDefaultTimeout(timeout); + public static ILocatorAssertions Expect(ILocator locator) => new LocatorAssertions(locator, false); public static IPageAssertions Expect(IPage page) => new PageAssertions(page, false); @@ -49,6 +60,13 @@ public static class Assertions public static IFunctionAssertions Expect(Func callback) => new FunctionAssertions(callback); + /// + /// Creates assertions that retry a synchronous . + /// + /// Callback to retry. + /// Assertions for the given callback. + public static IFunctionAssertions Expect(Action callback) => new FunctionAssertions(callback); + /// /// Creates assertions that prefix any failure message with , providing /// extra context in test reports. @@ -67,4 +85,14 @@ public static class Assertions /// The API response to assert against. /// Message to prepend to any assertion failure. public static IAPIResponseAssertions Expect(IAPIResponse response, string message) => new APIResponseAssertions(response, false, message); + + /// + /// Callback to retry. + /// Message to prepend to any assertion failure. + public static IFunctionAssertions Expect(Func callback, string message) => new FunctionAssertions(callback, message); + + /// + /// Callback to retry. + /// Message to prepend to any assertion failure. + public static IFunctionAssertions Expect(Action callback, string message) => new FunctionAssertions(callback, message); } diff --git a/src/Playwright/Core/FunctionAssertions.cs b/src/Playwright/Core/FunctionAssertions.cs index 557fd4c78..d1fa5f910 100644 --- a/src/Playwright/Core/FunctionAssertions.cs +++ b/src/Playwright/Core/FunctionAssertions.cs @@ -23,19 +23,100 @@ */ using System; +using System.Diagnostics; +using System.Linq; +using System.Threading; using System.Threading.Tasks; namespace Microsoft.Playwright.Core; internal class FunctionAssertions : IFunctionAssertions { + private static readonly int[] _defaultIntervals = { 100, 250, 500, 1000 }; + private static float _defaultTimeout = 30_000; private readonly Func _callback; + private readonly string? _customMessage; - public FunctionAssertions(Func callback) + public FunctionAssertions(Func callback, string? customMessage = null) { _callback = callback ?? throw new ArgumentNullException(nameof(callback)); + _customMessage = customMessage; } - public Task ToPassAsync(FunctionAssertionsToPassOptions? options = default) - => throw new NotImplementedException(); + public FunctionAssertions(Action callback, string? customMessage = null) + { + if (callback == null) + { + throw new ArgumentNullException(nameof(callback)); + } + _callback = () => + { + callback(); + return Task.CompletedTask; + }; + _customMessage = customMessage; + } + + internal static Func, Task> AttemptRunner { get; set; } = callback => callback(); + + public static void SetDefaultTimeout(float timeout) => _defaultTimeout = timeout; + + public async Task ToPassAsync(FunctionAssertionsToPassOptions? options = default) + { + var timeout = options?.Timeout ?? _defaultTimeout; + var intervals = options?.Intervals?.ToArray() ?? _defaultIntervals; + var stopwatch = Stopwatch.StartNew(); + Exception? lastError = null; + for (var attempt = 0; ; attempt++) + { + var remaining = timeout - stopwatch.Elapsed.TotalMilliseconds; + if (timeout > 0 && remaining <= 0) + { + break; + } + var task = RunCallbackAsync(); + if (timeout > 0 && !await CompletesWithinAsync(task, remaining).ConfigureAwait(false)) + { + break; + } + try + { + await task.ConfigureAwait(false); + return; + } + catch (Exception e) + { + lastError = e; + } + + var interval = intervals.Length == 0 ? 1000 : intervals[Math.Min(attempt, intervals.Length - 1)]; + if (timeout > 0 && stopwatch.Elapsed.TotalMilliseconds + interval >= timeout) + { + break; + } + await Task.Delay(interval).ConfigureAwait(false); + } + + var prefix = _customMessage != null ? _customMessage + "\n\n" : string.Empty; + var message = $"Timeout {timeout}ms exceeded while waiting on the predicate"; + if (lastError == null) + { + throw new PlaywrightAssertionException($"{prefix}{message}"); + } + throw new PlaywrightAssertionException($"{prefix}{lastError.Message}\nCall log:\n - {message}", lastError); + } + + private static async Task CompletesWithinAsync(Task task, double timeout) + { + using var cts = new CancellationTokenSource(); + var completed = await Task.WhenAny(task, Task.Delay(TimeSpan.FromMilliseconds(timeout), cts.Token)).ConfigureAwait(false) == task; + cts.Cancel(); + if (!completed) + { + _ = task.ContinueWith(t => t.Exception, CancellationToken.None, TaskContinuationOptions.OnlyOnFaulted, TaskScheduler.Default); + } + return completed; + } + + private async Task RunCallbackAsync() => await AttemptRunner(_callback).ConfigureAwait(false); } From f0cb6856b2dbe16e9ff076eaa1989fd46ba74540 Mon Sep 17 00:00:00 2001 From: Dmitry Gozman Date: Thu, 12 Nov 2026 11:27:13 +0000 Subject: [PATCH 2/2] fix(assertions): run the ToPassAsync callback on the thread pool Fixes: https://github.com/microsoft/playwright-dotnet/issues/2314 --- docs/threading.md | 1 + .../Assertions/FunctionAssertionsTests.cs | 5 +++ src/Playwright.Tests/ThreadingModelTests.cs | 33 +++++++++++++++++++ src/Playwright/Core/FunctionAssertions.cs | 8 ++++- 4 files changed, 46 insertions(+), 1 deletion(-) diff --git a/docs/threading.md b/docs/threading.md index 64777d1df..e23689f81 100644 --- a/docs/threading.md +++ b/docs/threading.md @@ -18,6 +18,7 @@ Where user code runs: | The same handler after it awaited a Playwright call or anything else | thread pool | | Predicate of a wait (`WaitForRequestAsync(r => ...)`, `WaitForURLAsync(url => ...)`) | dispatcher | | Action of `RunAndWaitFor*Async` | thread pool | +| Callback of `Expect(callback).ToPassAsync()`, every attempt | thread pool | | Code after `await` of any Playwright call | thread pool, or the caller's `SynchronizationContext` | ## Rules of thumb diff --git a/src/Playwright.Tests/Assertions/FunctionAssertionsTests.cs b/src/Playwright.Tests/Assertions/FunctionAssertionsTests.cs index 136cc7a7d..366ceb281 100644 --- a/src/Playwright.Tests/Assertions/FunctionAssertionsTests.cs +++ b/src/Playwright.Tests/Assertions/FunctionAssertionsTests.cs @@ -162,6 +162,11 @@ public async Task ShouldTimeOutWhileTheCallbackIsRunning() var exception = await PlaywrightAssert.ThrowsAsync(() => Expect(() => Task.Delay(10000)).ToPassAsync(new() { Timeout = 100 })); Assert.AreEqual("Timeout 100ms exceeded while waiting on the predicate", exception.Message); Assert.Less(stopwatch.ElapsedMilliseconds, 5000); + + stopwatch.Restart(); + exception = await PlaywrightAssert.ThrowsAsync(() => Expect(() => Thread.Sleep(10000)).ToPassAsync(new() { Timeout = 100 })); + Assert.AreEqual("Timeout 100ms exceeded while waiting on the predicate", exception.Message); + Assert.Less(stopwatch.ElapsedMilliseconds, 5000); } [PlaywrightTest] diff --git a/src/Playwright.Tests/ThreadingModelTests.cs b/src/Playwright.Tests/ThreadingModelTests.cs index 68f6e21b3..e01a28c22 100644 --- a/src/Playwright.Tests/ThreadingModelTests.cs +++ b/src/Playwright.Tests/ThreadingModelTests.cs @@ -174,6 +174,39 @@ await Page.RunAndWaitForConsoleMessageAsync(async () => Assert.False(onDispatcher); } + [PlaywrightTest] + public async Task ToPassCallbackRunsOffTheDispatcherThread() + { + // Every attempt runs on the thread pool, also when a handler starts the retries in its + // synchronous part, so the callback may block and a zero interval does not spin there. + var result = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + Page.Console += async (_, _) => + { + try + { + Assert.True(OnDispatcherThread); + int attempts = 0; + bool onDispatcher = false; + await Expect(() => + { + onDispatcher |= OnDispatcherThread; + if (!Page.EvaluateAsync("1 + 1").Wait(5000) || ++attempts < 3) + { + throw new InvalidOperationException("Retry"); + } + }).ToPassAsync(new() { Intervals = [0] }); + Assert.False(OnDispatcherThread); + result.SetResult(onDispatcher); + } + catch (Exception ex) + { + result.SetException(ex); + } + }; + await Page.EvaluateAsync("console.log('hi')"); + Assert.False(await result.Task); + } + [PlaywrightTest] public async Task SubscribingRightAfterStartingAWaitKeepsBothHandlers() { diff --git a/src/Playwright/Core/FunctionAssertions.cs b/src/Playwright/Core/FunctionAssertions.cs index d1fa5f910..669142a7f 100644 --- a/src/Playwright/Core/FunctionAssertions.cs +++ b/src/Playwright/Core/FunctionAssertions.cs @@ -27,6 +27,7 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; +using Microsoft.Playwright.Transport; namespace Microsoft.Playwright.Core; @@ -118,5 +119,10 @@ private static async Task CompletesWithinAsync(Task task, double timeout) return completed; } - private async Task RunCallbackAsync() => await AttemptRunner(_callback).ConfigureAwait(false); + private async Task RunCallbackAsync() + { + // The callback is user code: it runs on the thread pool, also when ToPassAsync is called on the dispatcher thread. + await Dispatcher.ExitToThreadPool(); + await AttemptRunner(_callback).ConfigureAwait(false); + } }