Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
using System.Globalization;
using System.Text.Json;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.Playwright;
using NUnit.Framework;
using PlaywrightNative.NUnit;
Expand Down Expand Up @@ -244,6 +245,41 @@ public async Task ShouldWorkWithCsp()
await context.CloseAsync().ConfigureAwait(false);
}

[PlaywrightTest("browsercontext-expose-function.spec.ts", "should call binding from pagehide handler when navigating away")]
[Test]
[Timeout(TestConstants.DefaultTestTimeout)]
public async Task ShouldCallBindingFromPagehideHandlerWhenNavigatingAway()
{
if (TestConstants.IsChromium)
{
Assert.Ignore("upstream it.fixme(chromium): Chromium drops CDP events from the old document after the frame host swap");
}

EnsureServer();
IBrowserContext context = await _browser.NewContextAsync().ConfigureAwait(false);
IPage page = await context.NewPageAsync().ConfigureAwait(false);
List<string> calls = new List<string>();
await context.ExposeBindingAsync("reportFromPage", (BindingSource source, string message) => { calls.Add(message); }).ConfigureAwait(false);
static string Html(string body) => "<!doctype html>" + body + "<script>addEventListener('pagehide', () => window.reportFromPage('pagehide of ' + location.pathname));</script>";
Server.SetRoute("/a.html", async http =>
{
http.Response.StatusCode = 200;
http.Response.ContentType = "text/html";
await http.Response.WriteAsync(Html("<a id=\"next\" href=\"/b.html\">next</a>")).ConfigureAwait(false);
});
Server.SetRoute("/b.html", async http =>
{
http.Response.StatusCode = 200;
http.Response.ContentType = "text/html";
await http.Response.WriteAsync(Html("<p>b</p>")).ConfigureAwait(false);
});
await page.GoToAsync(Prefix + "/a.html").ConfigureAwait(false);
await page.ClickAsync("#next").ConfigureAwait(false);
await page.WaitForURLAsync("**/b.html").ConfigureAwait(false);
await PollEqualAsync(() => Task.FromResult(calls.Contains("pagehide of /a.html")), true).ConfigureAwait(false);
await context.CloseAsync().ConfigureAwait(false);
}

private async Task CloseLeftoverContextsAsync()
{
if (_browser == null)
Expand Down
42 changes: 42 additions & 0 deletions src/PlaywrightNative.Tests/LibraryHarParityTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -281,6 +281,48 @@ public async Task ShouldIncludeRequest()
Assert.That(entry.GetProperty("request").GetProperty("bodySize").GetInt32(), Is.EqualTo(0));
}

[PlaywrightTest("har.spec.ts", "should populate entry startedDateTime from the browser")]
[Test]
[Timeout(TestConstants.DefaultTestTimeout)]
public async Task ShouldPopulateEntryStartedDateTimeFromTheBrowser()
{
EnsureServer();
await using HarSession session = await PageWithHarAsync().ConfigureAwait(false);
await session.Page.GoToAsync(EmptyPage).ConfigureAwait(false);

// The browser issues a request after a short delay, then we deliberately
// block the test thread. If `startedDateTime` is populated from our clock
// at observation time it can land inside the busy-loop window (i.e. close
// to `unblockedAt`); if it comes from the browser via the debugging
// protocol it will be tied to when the browser actually sent the request.
// Protocol events keep flowing on other threads in .NET, so the response
// wait is registered before the busy loop instead of after it.
// `window.builtins` is upstream test-only injected state. No clock is
// installed here, so the page `setTimeout` is the same function.
Task<IResponse> responseTask = session.Page.WaitForResponseAsync("**/delayed-fetch");
await session.Page.EvaluateAsync("() => { setTimeout(() => { void fetch('/delayed-fetch'); }, 50); }").ConfigureAwait(false);

long blockUntil = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds() + 300;
while (DateTimeOffset.UtcNow.ToUnixTimeMilliseconds() < blockUntil)
{
// Busy loop.
}

long unblockedAt = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();

await responseTask.ConfigureAwait(false);
JsonElement log = await session.GetLogAsync().ConfigureAwait(false);

JsonElement entry = log.GetProperty("entries").EnumerateArray()
.First(e => e.GetProperty("request").GetProperty("url").GetString().EndsWith("/delayed-fetch", StringComparison.Ordinal));
long startedAt = DateTimeOffset.Parse(entry.GetProperty("startedDateTime").GetString(), CultureInfo.InvariantCulture).ToUnixTimeMilliseconds();

// The recorded time should be tied to when the browser actually sent the
// request (during the busy loop), not to when we observed the protocol
// event (after the busy loop).
Assert.That(startedAt, Is.LessThan(unblockedAt - 100));
}

[PlaywrightTest("har.spec.ts", "should include response")]
[Test]
[Timeout(TestConstants.DefaultTestTimeout)]
Expand Down
51 changes: 51 additions & 0 deletions src/PlaywrightNative.Tests/LibraryTracingParityTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,57 @@ public async Task ShouldNotThrowWhenStoppingWithoutStartButNotExporting()
await context.Tracing.StopAsync().ConfigureAwait(false);
}

[PlaywrightTest("tracing.spec.ts", "start should return a disposable that discards the trace")]
[Test]
[Timeout(TestConstants.DefaultTestTimeout)]
public async Task StartShouldReturnADisposableThatDiscardsTheTrace()
{
string path = Path.Combine(Path.GetTempPath(), "pwsharp-trace-" + Path.GetRandomFileName() + ".zip");
try
{
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);
IAsyncDisposable disposable = await context.Tracing.StartAsync(name: default).ConfigureAwait(false);
await page.GoToAsync(EmptyPage).ConfigureAwait(false);
await disposable.DisposeAsync().ConfigureAwait(false);
PlaywrightException error = Assert.CatchAsync<PlaywrightException>(
() => context.Tracing.StopAsync(new TracingStopOptions { Path = path }));
Assert.That(error, Is.Not.Null);
Assert.That(error.Message, Does.Contain("Must start tracing before stopping"));
}
finally
{
TryDelete(path);
}
}

[PlaywrightTest("tracing.spec.ts", "startChunk should return a disposable that discards the chunk")]
[Test]
[Timeout(TestConstants.DefaultTestTimeout)]
public async Task StartChunkShouldReturnADisposableThatDiscardsTheChunk()
{
string path = Path.Combine(Path.GetTempPath(), "pwsharp-trace-" + Path.GetRandomFileName() + ".zip");
try
{
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 context.Tracing.StartAsync().ConfigureAwait(false);
IAsyncDisposable disposable = await context.Tracing.StartChunkAsync(name: default).ConfigureAwait(false);
await page.GoToAsync(EmptyPage).ConfigureAwait(false);
await disposable.DisposeAsync().ConfigureAwait(false);
PlaywrightException error = Assert.CatchAsync<PlaywrightException>(
() => context.Tracing.StopChunkAsync(new TracingStopChunkOptions { Path = path }));
Assert.That(error, Is.Not.Null);
Assert.That(error.Message, Does.Contain("Must start tracing before stopping"));
}
finally
{
TryDelete(path);
}
}

[PlaywrightTest("tracing.spec.ts", "should use the correct title for event driven callbacks")]
[Test]
[Timeout(TestConstants.DefaultTestTimeout)]
Expand Down
1 change: 1 addition & 0 deletions src/PlaywrightNative/Chromium/CRNetworkManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1363,6 +1363,7 @@ private void OnRequestWillBeSent(JsonElement? parameters, CRSession session, boo
request.DocumentUrl = isNavigationRequest ? url : frame?.Url;
request.TimestampSeconds = ResourceTimingParser.ReadDouble(p, "timestamp");
double wallTime = ResourceTimingParser.ReadDouble(p, "wallTime");
request.WallTimeMs = wallTime * 1000;
if (wallTime <= 0)
{
wallTime = request.TimestampSeconds;
Expand Down
6 changes: 6 additions & 0 deletions src/PlaywrightNative/Chromium/CRRequest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -330,6 +330,12 @@ internal CRResponse Response
/// </summary>
internal double TimestampSeconds { get; set; }

/// <summary>
/// Gets or sets the browser <c>wallTime</c> from <c>Network.requestWillBeSent</c>
/// (milliseconds since the Unix epoch).
/// </summary>
internal double WallTimeMs { get; set; }

/// <summary>
/// Official <c>Network.requestServedFromCache</c> / memory-cache.
/// </summary>
Expand Down
5 changes: 4 additions & 1 deletion src/PlaywrightNative/Chromium/ChromiumRequest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
namespace PlaywrightNative.Chromium
{
/// <summary>Public <see cref="IRequest"/> wrapping <see cref="CRRequest"/>.</summary>
internal sealed partial class ChromiumRequest : IRequest, IHasOwningPage
internal sealed partial class ChromiumRequest : IRequest, IHasOwningPage, IHasWallTime
{
private readonly CRRequest _crRequest;
private readonly Func<CRResponse, ChromiumResponse> _resolveResponse;
Expand Down Expand Up @@ -79,6 +79,9 @@ public IFrame Frame
/// <inheritdoc/>
public IPage OwningPage => ResolveFrame()?.Page;

/// <inheritdoc/>
public double WallTimeMs => _crRequest.WallTimeMs;

/// <inheritdoc/>
public Dictionary<string, string> Headers
{
Expand Down
5 changes: 5 additions & 0 deletions src/PlaywrightNative/Helpers/HarRecorder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -930,6 +930,11 @@ private static double Elapsed(float start, float end)

private static DateTimeOffset StartedAt(IRequest request)
{
if (request is IHasWallTime hasWallTime && hasWallTime.WallTimeMs > 0)
{
return DateTimeOffset.FromUnixTimeMilliseconds((long)Math.Round(hasWallTime.WallTimeMs));
}

RequestTimingResult timing = request?.Timing;
if (timing != null && timing.StartTime > 0)
{
Expand Down
30 changes: 30 additions & 0 deletions src/PlaywrightNative/Helpers/IHasWallTime.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/*
* 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.
*/
namespace PlaywrightNative.Helpers
{
/// <summary>
/// Browser wall time for a request, as reported by the engine
/// (upstream <c>Request.wallTimeMs()</c>).
/// </summary>
internal interface IHasWallTime
{
/// <summary>
/// Milliseconds since the Unix epoch when the browser sent the request,
/// or <c>0</c> when the engine did not report it.
/// </summary>
double WallTimeMs { get; }
}
}
7 changes: 6 additions & 1 deletion src/PlaywrightNative/Helpers/OfficialTraceSession.cs
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ internal sealed class OfficialTraceSession
private readonly bool _apiOnly;
private TracingStartOptions _options;
private bool _recording;
private bool _chunkRecording;
private bool _networkAttached;
private int _callId;
private int _shaIndex;
Expand Down Expand Up @@ -170,6 +171,7 @@ internal void Start(TracingStartOptions options, bool chunk)

_options = next;
_recording = true;
_chunkRecording = true;
_traceLines.Clear();
_resources.Clear();
_chunkCallIds.Clear();
Expand Down Expand Up @@ -842,7 +844,9 @@ async Task StopInnerAsync()
string tracesDir;
lock (_gate)
{
if (!_recording)
// Upstream ends the chunk on every stopChunk; a second stopChunk
// (or an exporting stop) without a new startChunk has nothing to save.
if (!_recording || (!_chunkRecording && (keepRecording || !string.IsNullOrEmpty(path))))
{
if (!string.IsNullOrEmpty(path))
{
Expand Down Expand Up @@ -912,6 +916,7 @@ async Task StopInnerAsync()
}
else
{
_chunkRecording = false;
WriteContextOptions();
}
}
Expand Down
37 changes: 33 additions & 4 deletions src/PlaywrightNative/MicrosoftSurfaceCompatExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,29 @@ public static Task StartAsync(this ITracing tracing, PlaywrightNative.TracingSta
}),
};

/// <summary>
/// Expanded-parameter start overload. Returns a disposable that stops
/// tracing without exporting, which discards the trace.
/// </summary>
public static async Task<IAsyncDisposable> StartAsync(
this ITracing tracing,
string name = default,
string title = default,
bool? screenshots = default,
bool? snapshots = default,
bool? sources = default)
{
await tracing.StartAsync(new Microsoft.Playwright.TracingStartOptions
{
Name = name,
Title = title,
Screenshots = screenshots,
Snapshots = snapshots,
Sources = sources,
}).ConfigureAwait(false);
return AddInitScriptHelper.CreateDisposable(() => tracing.StopAsync());
}

/// <summary>Expanded-parameter stop overload.</summary>
public static Task StopAsync(this ITracing tracing, string path)
=> tracing.StopAsync(new TracingStopOptions { Path = path });
Expand All @@ -262,14 +285,20 @@ public static Task StopChunkAsync(this ITracing tracing, string path = default)
_ => tracing.StopChunkAsync(new TracingStopChunkOptions { Path = path }),
};

/// <summary>Expanded-parameter start-chunk overload.</summary>
public static Task StartChunkAsync(this ITracing tracing, string name = default, string title = default)
=> tracing switch
/// <summary>
/// Expanded-parameter start-chunk overload. Returns a disposable that stops
/// the chunk without exporting, which discards the chunk.
/// </summary>
public static async Task<IAsyncDisposable> StartChunkAsync(this ITracing tracing, string name = default, string title = default)
{
await (tracing switch
{
CRTracing chromium => chromium.StartChunkAsync(name, title),
EmptyTracing empty => empty.StartChunkAsync(name, title),
_ => tracing.StartChunkAsync(new TracingStartChunkOptions { Name = name, Title = title }),
};
}).ConfigureAwait(false);
return AddInitScriptHelper.CreateDisposable(() => tracing.StopChunkAsync());
}

/// <summary>HAR recording helper on tracing.</summary>
public static Task<IAsyncDisposable> StartHarAsync(
Expand Down
3 changes: 2 additions & 1 deletion src/PlaywrightNative/WebKit/WKNetworkManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -852,7 +852,8 @@ private WKRequest CreateRequestFromWillBeSent(JsonElement p, bool allowRoute)

request.DocumentUrl = isNavigationRequest ? url : frame?.Url;
request.TimestampSeconds = ResourceTimingParser.ReadDouble(p, "timestamp");
double wallTime = ResourceTimingParser.ReadDouble(p, "wallTime");
double wallTime = ResourceTimingParser.ReadDouble(p, "walltime");
request.WallTimeMs = wallTime * 1000;
if (wallTime <= 0)
{
wallTime = request.TimestampSeconds;
Expand Down
5 changes: 4 additions & 1 deletion src/PlaywrightNative/WebKit/WKRequest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ namespace PlaywrightNative.WebKit
/// corresponding response and redirect chain. Mirrors <c>CRRequest</c>.
/// Implements <see cref="IRequest"/> directly.
/// </summary>
internal partial class WKRequest : IRequest, IHasOwningPage
internal partial class WKRequest : IRequest, IHasOwningPage, IHasWallTime
{
private readonly TaskCompletionSource<string> _finished =
new TaskCompletionSource<string>(TaskCreationOptions.RunContinuationsAsynchronously);
Expand Down Expand Up @@ -152,6 +152,9 @@ public IFrame Frame
/// <inheritdoc/>
public IPage OwningPage => _frame?.Page;

/// <inheritdoc/>
public double WallTimeMs { get; set; }

/// <inheritdoc/>
public byte[] PostDataBuffer => _postDataBuffer;

Expand Down
Loading