diff --git a/src/Playwright.Tests/PageRunAndWaitForResponseTests.cs b/src/Playwright.Tests/PageRunAndWaitForResponseTests.cs index 89750a4b9..40a8b8c67 100644 --- a/src/Playwright.Tests/PageRunAndWaitForResponseTests.cs +++ b/src/Playwright.Tests/PageRunAndWaitForResponseTests.cs @@ -41,6 +41,17 @@ public async Task ShouldWork() Assert.AreEqual(Server.Prefix + "/digits/2.png", response.Url); } + [PlaywrightTest] + public async Task ShouldWorkWithAsyncContinuation() + { + await Page.GotoAsync(Server.EmptyPage); + var response = await Page.RunAndWaitForResponseAsync(() => Page.EvaluateAsync(@"() => { + fetch('/digits/1.png'); + }"), Server.Prefix + "/digits/1.png"); + // Should not deadlock here + Task.Run(() => Page.GotoAsync(Server.EmptyPage)).Wait(); + } + [PlaywrightTest("page-wait-for-response.spec.ts", "should respect timeout")] public Task ShouldRespectTimeout() { diff --git a/src/Playwright/Core/Frame.cs b/src/Playwright/Core/Frame.cs index 883c54023..2fdfc8dde 100644 --- a/src/Playwright/Core/Frame.cs +++ b/src/Playwright/Core/Frame.cs @@ -43,6 +43,7 @@ namespace Microsoft.Playwright.Core; internal class Frame : ChannelOwner, IFrame { private readonly List _loadStates = new(); + private readonly object _loadStatesLock = new(); internal readonly List _childFrames = new(); internal Frame(ChannelOwner parent, string guid, FrameInitializer initializer) : base(parent, guid) @@ -111,13 +112,19 @@ internal void OnLoadState(WaitUntilState? add, WaitUntilState? remove) { if (add.HasValue) { - _loadStates.Add(add.Value); + lock (_loadStatesLock) + { + _loadStates.Add(add.Value); + } LoadState?.Invoke(this, add.Value); } if (remove.HasValue) { - _loadStates.Remove(remove.Value); + lock (_loadStatesLock) + { + _loadStates.Remove(remove.Value); + } } if (this.ParentFrame == null && add == WaitUntilState.Load && this.Page != null) { @@ -238,7 +245,13 @@ public async Task WaitForLoadStateAsync(LoadState? state = default, FrameWaitFor { waiter = SetupNavigationWaiter("frame.WaitForLoadStateAsync", options?.Timeout); - if (_loadStates.Contains(loadState)) + bool containsLoadState; + lock (_loadStatesLock) + { + containsLoadState = _loadStates.Contains(loadState); + } + + if (containsLoadState) { waiter.Log($" not waiting, \"{state}\" event already fired"); } @@ -326,16 +339,32 @@ await waiter.WaitForEventAsync(this, "LoadState", s => await waiter.WaitForPromiseAsync(Task.FromException(ex)).ConfigureAwait(false); } - if (!_loadStates.Select(s => s.ToValueString()).Contains(waitUntil.Value.ToValueString())) + // Set the subscription first + var (loadStateTask, loadStateDispose) = waiter.GetWaitForEventTask( + this, + "LoadState", + e => + { + waiter.Log($" \"{e}\" event fired"); + return e.ToValueString() == waitUntil.Value.ToValueString(); + }); + + bool containsWaitUntilState; + lock (_loadStatesLock) { - await waiter.WaitForEventAsync( - this, - "LoadState", - e => - { - waiter.Log($" \"{e}\" event fired"); - return e.ToValueString() == waitUntil.Value.ToValueString(); - }).ConfigureAwait(false); + containsWaitUntilState = _loadStates.Any(s => s.ToValueString() == waitUntil.Value.ToValueString()); + } + + if (containsWaitUntilState) + { + // State is already present, no need to wait + waiter.Log($" \"{waitUntil}\" event was already fired"); + loadStateDispose(); + } + else + { + // Wait for the event + await waiter.WaitForPromiseAsync(loadStateTask, loadStateDispose).ConfigureAwait(false); } var request = navigatedEvent.NewDocument?.Request; diff --git a/src/Playwright/Core/Waiter.cs b/src/Playwright/Core/Waiter.cs index c5128d035..83ada1cf5 100644 --- a/src/Playwright/Core/Waiter.cs +++ b/src/Playwright/Core/Waiter.cs @@ -152,7 +152,7 @@ internal void RejectOnTimeout(int? timeout, string message) var cts = new CancellationTokenSource(); RejectOn( - new TaskCompletionSource().Task.WithTimeout(timeout.Value, _ => new TimeoutException(message), cts.Token), + new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously).Task.WithTimeout(timeout.Value, _ => new TimeoutException(message), cts.Token), () => cts.Cancel()); } @@ -172,7 +172,7 @@ internal Task WaitForEventAsync(object eventSource, string e) { var info = eventSource.GetType().GetEvent(e) ?? eventSource.GetType().BaseType.GetEvent(e); - var eventTsc = new TaskCompletionSource(); + var eventTsc = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); void EventHandler(object sender, T e) { try