Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
103af24
Run continuation asynchronously in context of events
nvborisenko Nov 14, 2025
031d721
Add unit test
nvborisenko Jan 12, 2026
2adcb7c
Update README.md
nvborisenko Jan 27, 2026
42e63be
Revert "Update README.md"
nvborisenko Jan 27, 2026
811a196
Merge remote-tracking branch 'upstream/main' into continuation-context
nvborisenko Jan 27, 2026
f413f93
Fix collection was modified issue
nvborisenko Jan 27, 2026
a5a51d7
Revert "Fix collection was modified issue"
nvborisenko Jan 27, 2026
a976a91
RunContinuationsAsynchronously for RejectOn method
nvborisenko Jan 29, 2026
a1f8a25
Merge remote-tracking branch 'upstream/main' into continuation-context
nvborisenko Jan 30, 2026
d7df026
Minimize race condition when awaiting navigation events
nvborisenko Jan 30, 2026
b5adaa7
Temporary increase test timeout to avoid interruption
nvborisenko Jan 30, 2026
249deb0
Default task scheduler
nvborisenko Jan 30, 2026
795cf7b
Remove event handler
nvborisenko Jan 31, 2026
fa760ec
Thread safe changing load states
nvborisenko Jan 31, 2026
ba153bc
Avoid UnobserveException in ContinueWith
nvborisenko Jan 31, 2026
82a8a7a
Return back 30s test timeout
nvborisenko Jan 31, 2026
74290cb
Revert "Avoid UnobserveException in ContinueWith"
nvborisenko Feb 5, 2026
df39fd5
Revert "Remove event handler"
nvborisenko Feb 5, 2026
fbb6c42
Revert "Default task scheduler"
nvborisenko Feb 5, 2026
3efaecb
Return original List type for loadStates
nvborisenko Feb 5, 2026
e970e45
Merge remote-tracking branch 'upstream/main' into continuation-context
nvborisenko Apr 2, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions src/Playwright.Tests/PageRunAndWaitForResponseTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<string>(@"() => {
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()
{
Expand Down
53 changes: 41 additions & 12 deletions src/Playwright/Core/Frame.cs
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ namespace Microsoft.Playwright.Core;
internal class Frame : ChannelOwner, IFrame
{
private readonly List<WaitUntilState> _loadStates = new();
private readonly object _loadStatesLock = new();
internal readonly List<Frame> _childFrames = new();

internal Frame(ChannelOwner parent, string guid, FrameInitializer initializer) : base(parent, guid)
Expand Down Expand Up @@ -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)
{
Expand Down Expand Up @@ -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");
}
Expand Down Expand Up @@ -326,16 +339,32 @@ await waiter.WaitForEventAsync<WaitUntilState>(this, "LoadState", s =>
await waiter.WaitForPromiseAsync(Task.FromException<object>(ex)).ConfigureAwait(false);
}

if (!_loadStates.Select(s => s.ToValueString()).Contains(waitUntil.Value.ToValueString()))
// Set the subscription first
var (loadStateTask, loadStateDispose) = waiter.GetWaitForEventTask<WaitUntilState>(
this,
"LoadState",
e =>
{
waiter.Log($" \"{e}\" event fired");
return e.ToValueString() == waitUntil.Value.ToValueString();
});

bool containsWaitUntilState;
lock (_loadStatesLock)
{
await waiter.WaitForEventAsync<WaitUntilState>(
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;
Expand Down
4 changes: 2 additions & 2 deletions src/Playwright/Core/Waiter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,7 @@ internal void RejectOnTimeout(int? timeout, string message)

var cts = new CancellationTokenSource();
RejectOn(
new TaskCompletionSource<bool>().Task.WithTimeout(timeout.Value, _ => new TimeoutException(message), cts.Token),
new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously).Task.WithTimeout(timeout.Value, _ => new TimeoutException(message), cts.Token),
() => cts.Cancel());
}

Expand All @@ -172,7 +172,7 @@ internal Task<object> WaitForEventAsync(object eventSource, string e)
{
var info = eventSource.GetType().GetEvent(e) ?? eventSource.GetType().BaseType.GetEvent(e);

var eventTsc = new TaskCompletionSource<T>();
var eventTsc = new TaskCompletionSource<T>(TaskCreationOptions.RunContinuationsAsynchronously);
void EventHandler(object sender, T e)
{
try
Expand Down
Loading