Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add an artificial ResponseFinished to pass IResponse alongside IRequest #2578

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
5 changes: 3 additions & 2 deletions src/Playwright.Tests/PageEventNetworkTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -128,15 +128,16 @@ public async Task ShouldSupportRedirects()
Page.Request += (_, e) => events[e.Url].Add(e.Method);
Page.Response += (_, e) => events[e.Url].Add(e.Status.ToString(CultureInfo.InvariantCulture));
Page.RequestFinished += (_, e) => events[e.Url].Add("DONE");
Page.ResponseFinished += (_, e) => events[e.Response.Url].Add("DONE_2");
Page.RequestFailed += (_, e) => events[e.Url].Add("FAIL");
Server.SetRedirect("/foo.html", "/empty.html");
var response = await Page.GotoAsync(FOO_URL);
await response.FinishedAsync();

var expected = new Dictionary<string, List<string>>
{
[FOO_URL] = new() { "GET", "302", "DONE" },
[EMPTY_URL] = new() { "GET", "200", "DONE" }
[FOO_URL] = new() { "GET", "302", "DONE", "DONE_2" },
[EMPTY_URL] = new() { "GET", "200", "DONE", "DONE_2" }
};

Assert.AreEqual(expected, events);
Expand Down
5 changes: 5 additions & 0 deletions src/Playwright.Tests/PageGotoTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,11 @@ public async Task ShouldFailWhenNavigatingToBadSSL()
{
Page.Request += (_, e) => Assert.NotNull(e);
Page.RequestFinished += (_, e) => Assert.NotNull(e);
Page.ResponseFinished += (_, e) =>
{
Assert.NotNull(e.Request);
Assert.NotNull(e.Response);
};
Page.RequestFailed += (_, e) => Assert.NotNull(e);

var exception = await PlaywrightAssert.ThrowsAsync<PlaywrightException>(() => Page.GotoAsync(HttpsServer.Prefix + "/empty.html"));
Expand Down
3 changes: 3 additions & 0 deletions src/Playwright.Tests/PageNetworkResponseTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,8 @@ public async Task ShouldWaitUntilResponseCompletes()
// Setup page to trap response.
bool requestFinished = false;
Page.RequestFinished += (_, e) => requestFinished = requestFinished || e.Url.Contains("/get");
bool responseFinished = false;
Page.ResponseFinished += (_, e) => responseFinished = responseFinished || e.Response.Url.Contains("/get");
// send request and wait for server response
var (pageResponse, _) = await TaskUtils.WhenAll(
Page.WaitForResponseAsync("**/*"),
Expand All @@ -163,6 +165,7 @@ public async Task ShouldWaitUntilResponseCompletes()
Assert.NotNull(pageResponse);
Assert.AreEqual((int)HttpStatusCode.OK, pageResponse.Status);
Assert.False(requestFinished);
Assert.False(responseFinished);

var responseText = pageResponse.TextAsync();
// Write part of the response and wait for it to be flushed.
Expand Down
12 changes: 12 additions & 0 deletions src/Playwright.Tests/ResourceTimingTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -68,11 +68,14 @@ public async Task ShouldWorkForSubresource()
await using var context = await NewContext();
var page = await context.NewPageAsync();
var requests = new List<IRequest>();
var responses = new List<IResponse>();

page.RequestFinished += (_, e) => requests.Add(e);
page.ResponseFinished += (_, e) => responses.Add(e.Response);
await page.GotoAsync(Server.Prefix + "/one-style.html");

Assert.AreEqual(2, requests.Count);
Assert.AreEqual(2, responses.Count);

var timing = requests[1].Timing;

Expand All @@ -82,6 +85,15 @@ public async Task ShouldWorkForSubresource()
Assert.GreaterOrEqual(timing.ResponseStart, timing.RequestStart);
Assert.GreaterOrEqual(timing.ResponseEnd, timing.ResponseStart);
Assert.Less(timing.ResponseEnd, 10000);

var timingFromResponse = responses[1].Request.Timing;

VerifyConnectionTimingConsistency(timingFromResponse);

Assert.GreaterOrEqual(timingFromResponse.RequestStart, 0);
Assert.GreaterOrEqual(timingFromResponse.ResponseStart, timingFromResponse.RequestStart);
Assert.GreaterOrEqual(timingFromResponse.ResponseEnd, timingFromResponse.ResponseStart);
Assert.Less(timingFromResponse.ResponseEnd, 10000);
}

[PlaywrightTest("resource-timing.spec.ts", "should work for SSL")]
Expand Down
36 changes: 35 additions & 1 deletion src/Playwright/API/Generated/IPage.cs
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,15 @@ public partial interface IPage
/// </summary>
event EventHandler<IRequest> RequestFinished;

/// <summary>
/// <para>
/// Emitted after request finishes successfully after downloading the response body to filter by response.
/// For a successful response, the sequence of events is <c>request</c>, <c>response</c>
/// and <c>requestfinished</c>.
/// </para>
/// </summary>
event EventHandler<(IRequest Request, IResponse? Response)> ResponseFinished;

/// <summary>
/// <para>
/// Emitted when <see cref="response"/> status and headers are received for a request.
Expand Down Expand Up @@ -3002,7 +3011,7 @@ public partial interface IPage

/// <summary>
/// <para>
/// Performs action and waits for a <see cref="IRequest"/> to finish loading. If predicate
/// Waits for a <see cref="IRequest"/> to finish loading. If predicate
/// is provided, it passes <see cref="IRequest"/> value into the <c>predicate</c> function
/// and waits for <c>predicate(request)</c> to return a truthy value. Will throw an
/// error if the page is closed before the <see cref="IPage.RequestFinished"/> event
Expand All @@ -3025,6 +3034,31 @@ public partial interface IPage
/// <param name="options">Call options</param>
Task<IRequest> RunAndWaitForRequestFinishedAsync(Func<Task> action, PageRunAndWaitForRequestFinishedOptions? options = default);

/// <summary>
/// <para>
/// Waits for a <see cref="IRequest"/> to finish loading. If predicate
/// is provided, it passes <see cref="IRequest"/> and <see cref="IResponse"/> value into the <c>predicate</c> function
/// and waits for <c>predicate(request, response)</c> to return a truthy value. Will throw an
/// error if the page is closed before the <see cref="IPage.RequestFinished"/> event
/// is fired.
/// </para>
/// </summary>
/// <param name="options">Call options</param>
Task<(IRequest Request, IResponse Response)> WaitForResponseFinishedAsync(PageWaitForResponseFinishedOptions? options = default);

/// <summary>
/// <para>
/// Performs action and waits for a <see cref="IRequest"/> to finish loading. If predicate
/// is provided, it passes <see cref="IRequest"/> and <see cref="IResponse"/> value into the <c>predicate</c> function
/// and waits for <c>predicate(request, response)</c> to return a truthy value. Will throw an
/// error if the page is closed before the <see cref="IPage.RequestFinished"/> event
/// is fired.
/// </para>
/// </summary>
/// <param name="action">Action that triggers the event.</param>
/// <param name="options">Call options</param>
Task<(IRequest Request, IResponse Response)> RunAndWaitForResponseFinishedAsync(Func<Task> action, PageRunAndWaitForResponseFinishedOptions? options = default);

/// <summary>
/// <para>
/// Returns the matched response. See <a href="https://playwright.dev/dotnet/docs/events#waiting-for-event">waiting
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
/*
* 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;
using System.Text.Json.Serialization;

#nullable enable

namespace Microsoft.Playwright;

public class PageRunAndWaitForResponseFinishedOptions
{
public PageRunAndWaitForResponseFinishedOptions() { }

public PageRunAndWaitForResponseFinishedOptions(PageRunAndWaitForResponseFinishedOptions clone)
{
if (clone == null)
{
return;
}

Predicate = clone.Predicate;
Timeout = clone.Timeout;
}

/// <summary>
/// <para>
/// Receives the <see cref="IRequest"/> and <see cref="IResponse"/> object and resolves to truthy value when the
/// waiting should resolve.
/// </para>
/// </summary>
[JsonPropertyName("predicate")]
public Func<(IRequest Request, IResponse? Response), bool>? Predicate { get; set; }

/// <summary>
/// <para>
/// Maximum time to wait for in milliseconds. Defaults to <c>30000</c> (30 seconds).
/// Pass <c>0</c> to disable timeout. The default value can be changed by using the
/// <see cref="IBrowserContext.SetDefaultTimeout"/>.
/// </para>
/// </summary>
[JsonPropertyName("timeout")]
public float? Timeout { get; set; }
}

#nullable disable
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
/*
* 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;
using System.Text.Json.Serialization;

#nullable enable

namespace Microsoft.Playwright;

public class PageWaitForResponseFinishedOptions
{
public PageWaitForResponseFinishedOptions() { }

public PageWaitForResponseFinishedOptions(PageWaitForResponseFinishedOptions clone)
{
if (clone == null)
{
return;
}

Predicate = clone.Predicate;
Timeout = clone.Timeout;
}

/// <summary>
/// <para>
/// Receives the <see cref="IRequest"/> and <see cref="IResponse"/> object and resolves to truthy value when the
/// waiting should resolve.
/// </para>
/// </summary>
[JsonPropertyName("predicate")]
public Func<(IRequest Request, IResponse? Response), bool>? Predicate { get; set; }

/// <summary>
/// <para>
/// Maximum time to wait for in milliseconds. Defaults to <c>30000</c> (30 seconds).
/// Pass <c>0</c> to disable timeout. The default value can be changed by using the
/// <see cref="IBrowserContext.SetDefaultTimeout"/>.
/// </para>
/// </summary>
[JsonPropertyName("timeout")]
public float? Timeout { get; set; }
}

#nullable disable
1 change: 1 addition & 0 deletions src/Playwright/Core/BrowserContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ internal BrowserContext(IChannelOwner parent, string guid, BrowserContextInitial
_requestFinishedImpl?.Invoke(this, e.Request);
e.Page?.FireRequestFinished(e.Request);
e.Response?.ReportFinished();
e.Page?.FireResponseFinished(e.Request, e.Response);
};
Channel.Response += (_, e) =>
{
Expand Down
17 changes: 17 additions & 0 deletions src/Playwright/Core/Page.cs
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,8 @@ internal Page(IChannelOwner parent, string guid, PageInitializer initializer) :

private event EventHandler<IRequest> _requestFinishedImpl;

private event EventHandler<(IRequest Request, IResponse Response)> _responseFinishedImpl;

private event EventHandler<IRequest> _requestFailedImpl;

private event EventHandler<IFileChooser> _fileChooserImpl;
Expand Down Expand Up @@ -163,6 +165,13 @@ public event EventHandler<IRequest> RequestFinished
remove => this._requestFinishedImpl = UpdateEventHandler("requestFinished", this._requestFinishedImpl, value, false);
}

// Response finished is an artificial event - actual is the requestFinished
public event EventHandler<(IRequest Request, IResponse Response)> ResponseFinished
{
add => this._responseFinishedImpl = UpdateEventHandler("requestFinished", this._responseFinishedImpl, value, true);
remove => this._responseFinishedImpl = UpdateEventHandler("requestFinished", this._responseFinishedImpl, value, false);
}

public event EventHandler<IRequest> RequestFailed
{
add => this._requestFailedImpl = UpdateEventHandler("requestFailed", this._requestFailedImpl, value, true);
Expand Down Expand Up @@ -357,6 +366,9 @@ public Task<IRequest> WaitForRequestAsync(Func<IRequest, bool> urlOrPredicate, P
public Task<IRequest> WaitForRequestFinishedAsync(PageWaitForRequestFinishedOptions options = default)
=> InnerWaitForEventAsync(PageEvent.RequestFinished, null, options?.Predicate, options?.Timeout);

public Task<(IRequest Request, IResponse Response)> WaitForResponseFinishedAsync(PageWaitForResponseFinishedOptions options = default)
=> InnerWaitForEventAsync(PageEvent.ResponseFinished, null, options?.Predicate, options?.Timeout);

public Task<IResponse> WaitForResponseAsync(string urlOrPredicate, PageWaitForResponseOptions options = default)
=> InnerWaitForEventAsync(PageEvent.Response, null, e => Context.UrlMatches(e.Url, urlOrPredicate), options?.Timeout);

Expand Down Expand Up @@ -384,6 +396,9 @@ public Task<IPage> RunAndWaitForPopupAsync(Func<Task> action, PageRunAndWaitForP
public Task<IRequest> RunAndWaitForRequestFinishedAsync(Func<Task> action, PageRunAndWaitForRequestFinishedOptions options = default)
=> InnerWaitForEventAsync(PageEvent.RequestFinished, action, options?.Predicate, options?.Timeout);

public Task<(IRequest Request, IResponse Response)> RunAndWaitForResponseFinishedAsync(Func<Task> action, PageRunAndWaitForResponseFinishedOptions options = default)
=> InnerWaitForEventAsync(PageEvent.ResponseFinished, action, options?.Predicate, options?.Timeout);

public Task<IWebSocket> RunAndWaitForWebSocketAsync(Func<Task> action, PageRunAndWaitForWebSocketOptions options = default)
=> InnerWaitForEventAsync(PageEvent.WebSocket, action, options?.Predicate, options?.Timeout);

Expand Down Expand Up @@ -986,6 +1001,8 @@ internal void OnFrameNavigated(Frame frame)

internal void FireRequestFinished(IRequest request) => _requestFinishedImpl?.Invoke(this, request);

internal void FireResponseFinished(IRequest request, IResponse response) => _responseFinishedImpl?.Invoke(this, (request, response));

internal void FireResponse(IResponse response) => _responseImpl?.Invoke(this, response);

internal void FireLoad() => Load?.Invoke(this, this);
Expand Down
5 changes: 5 additions & 0 deletions src/Playwright/Core/PageEvent.cs
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,11 @@ internal static class PageEvent
/// </summary>
public static PlaywrightEvent<IRequest> RequestFinished { get; } = new() { Name = "RequestFinished" };

/// <summary>
/// <see cref="PlaywrightEvent{T}"/> representing an artificial ResponseFinished event to pass <see cref="IResponse"/> in addition of <see cref="IRequest"/>.
/// </summary>
public static PlaywrightEvent<(IRequest Request, IResponse Response)> ResponseFinished { get; } = new() { Name = "ResponseFinished" };

/// <summary>
/// <see cref="PlaywrightEvent{T}"/> representing a <see cref="IPage.Crash"/>.
/// </summary>
Expand Down