Skip to content
Merged
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
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -389,7 +389,7 @@ Test directory structure demonstrates comprehensive coverage:

**IMPORTANT: Test Expectations Files Rules:**
- `TestExpectations.upstream.json`: This file should NEVER be edited unless syncing with the upstream Puppeteer project. It contains expectations that match the upstream Puppeteer test expectations.
- `TestExpectations.local.json`: Use this file for local overrides and PuppeteerSharp-specific test expectations. Add entries here to skip or mark tests that fail due to .NET-specific issues or features not yet implemented.
- `TestExpectations.local.json`: Use this file for local overrides and PuppeteerSharp-specific test expectations. Add entries here to skip or mark tests that fail due to .NET-specific issues or features not yet implemented. Never add entries to this file that are meant to match upstream expectations and never add entries without explicit confirmation.

#### Test Server (`PuppeteerSharp.TestServer/`)
- ASP.NET Core server for hosting test pages
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,13 @@
"expectations": ["SKIP"],
"comment": "TODO: add a comment explaining why this expectation is required (include links to issues)"
},
{
"testIdPattern": "[page.spec] Page Page.resize should resize the browser window to fit page content",
"platforms": ["darwin", "linux", "win32"],
"parameters": ["webDriverBiDi"],
"expectations": ["FAIL"],
"comment": "Not supported by WebDriver BiDi"
},
{
"testIdPattern": "[pipe.spec] *",
"platforms": ["darwin", "linux", "win32"],
Expand Down
2 changes: 2 additions & 0 deletions lib/PuppeteerSharp.TestServer/wwwroot/style-404.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
<!DOCTYPE html>
<link rel="stylesheet" href="/fonts-404?helvetica|arial"/>
52 changes: 16 additions & 36 deletions lib/PuppeteerSharp.Tests/AccessibilityTests/AccessibilityTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -113,42 +113,22 @@ public async Task ShouldReportUninterestingNodes()
await Page.SetContentAsync("<textarea autofocus>hi</textarea>");
await Page.FocusAsync("textarea");

// This object has more children than in upstream.
// Because upstream uses `toMatchObject` which stops going deeper if the element has not Children.
Assert.That(
FindFocusedNode(await Page.Accessibility.SnapshotAsync(new AccessibilitySnapshotOptions
{
InterestingOnly = false
})),
Is.EqualTo(new SerializedAXNode
{
Role = "textbox",
Name = "",
Value = "hi",
Focused = true,
Multiline = true,
Children = new SerializedAXNode[]
{
new() {
Role = "generic",
Name = "",
Children = new SerializedAXNode[]
{
new() {
Role = "StaticText",
Name = "hi",
Children = new SerializedAXNode[]
{
new()
{
Role = "InlineTextBox",
}
}
}
}
}
}
}));
// Upstream uses toMatchObject (partial matching), so we assert individual properties
var focusedNode = FindFocusedNode(await Page.Accessibility.SnapshotAsync(new AccessibilitySnapshotOptions
{
InterestingOnly = false
}));
Assert.That(focusedNode.Role, Is.EqualTo("textbox"));
Assert.That(focusedNode.Name, Is.EqualTo(""));
Assert.That(focusedNode.Value, Is.EqualTo("hi"));
Assert.That(focusedNode.Focused, Is.True);
Assert.That(focusedNode.Multiline, Is.True);
Assert.That(focusedNode.Children, Has.Length.EqualTo(1));
Assert.That(focusedNode.Children[0].Role, Is.EqualTo("generic"));
Assert.That(focusedNode.Children[0].Name, Is.EqualTo(""));
Assert.That(focusedNode.Children[0].Children, Has.Length.EqualTo(1));
Assert.That(focusedNode.Children[0].Children[0].Role, Is.EqualTo("StaticText"));
Assert.That(focusedNode.Children[0].Children[0].Name, Is.EqualTo("hi"));
}

[Test, PuppeteerTest("accessibility.spec", "Accessibility", "get snapshots while the tree is re-calculated")]
Expand Down
60 changes: 24 additions & 36 deletions lib/PuppeteerSharp.Tests/AccessibilityTests/RootOptionTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -91,46 +91,34 @@ public async Task ShouldReturnNullWhenTheElementIsNoLongerInDOM()
[Test, PuppeteerTest("accessibility.spec", "root option", "should support the interestingOnly option")]
public async Task ShouldSupportTheInterestingOnlyOption()
{
await Page.SetContentAsync("<div><button>My Button</button></div>");
var div = await Page.QuerySelectorAsync("div");
await Page.SetContentAsync("<div><button>My Button</button></div><div class=\"uninteresting\"></div>");
var div = await Page.QuerySelectorAsync("div.uninteresting");
Assert.That(await Page.Accessibility.SnapshotAsync(new AccessibilitySnapshotOptions
{
Root = div
}), Is.Null);
Assert.That(
await Page.Accessibility.SnapshotAsync(new AccessibilitySnapshotOptions
{
Root = div,
InterestingOnly = false
}),
Is.EqualTo(new SerializedAXNode
{
Role = "generic",
Name = "",
Children = new[]
{
new SerializedAXNode
{
Role = "button",
Name = "My Button",
Children = new[]
{
new SerializedAXNode
{
Role = "StaticText",
Name = "My Button",
Children = new SerializedAXNode[]
{
new()
{
Role = "InlineTextBox",
}
}
}
}
}
}
}));

var divWithButton = await Page.QuerySelectorAsync("div");
var snapshot = await Page.Accessibility.SnapshotAsync(new AccessibilitySnapshotOptions
{
Root = divWithButton
});
Assert.That(snapshot.Name, Is.EqualTo("My Button"));
Assert.That(snapshot.Role, Is.EqualTo("button"));

var fullSnapshot = await Page.Accessibility.SnapshotAsync(new AccessibilitySnapshotOptions
{
Root = divWithButton,
InterestingOnly = false
});
Assert.That(fullSnapshot.Role, Is.EqualTo("generic"));
Assert.That(fullSnapshot.Name, Is.EqualTo(""));
Assert.That(fullSnapshot.Children, Has.Length.EqualTo(1));
Assert.That(fullSnapshot.Children[0].Role, Is.EqualTo("button"));
Assert.That(fullSnapshot.Children[0].Name, Is.EqualTo("My Button"));
Assert.That(fullSnapshot.Children[0].Children, Has.Length.EqualTo(1));
Assert.That(fullSnapshot.Children[0].Children[0].Role, Is.EqualTo("StaticText"));
Assert.That(fullSnapshot.Children[0].Children[0].Name, Is.EqualTo("My Button"));
}
}
}
2 changes: 1 addition & 1 deletion lib/PuppeteerSharp.Tests/OOPIFTests/OOPIFTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -239,7 +239,7 @@ await FrameUtils.AttachFrameAsync(
"frame1",
TestConstants.CrossProcessHttpPrefix + "/empty.html"
);
var frame = await frameTask.WithTimeout();
var frame = await frameTask.WithTimeout(5_000);
await frame.EvaluateFunctionAsync(@"() => {
const button = document.createElement('button');
button.id = 'test-button';
Expand Down
8 changes: 4 additions & 4 deletions lib/PuppeteerSharp.Tests/PageTests/PageEventsConsoleTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -186,18 +186,18 @@ await Page.EvaluateFunctionAsync(@"() => {
[Test, PuppeteerTest("page.spec", "Page Page.Events.Console", "should trigger correct Log")]
public async Task ShouldTriggerCorrectLog()
{
// Navigate to about:blank first (different origin than the test server)
await Page.GoToAsync(TestConstants.AboutBlank);
// Navigate to localhost (one origin)
await Page.GoToAsync(TestConstants.EmptyPage);
var messageTask = new TaskCompletionSource<ConsoleMessage>();

Page.Console += (_, e) => messageTask.TrySetResult(e.Message);

// Fetch from a different origin to trigger CORS error
// Fetch from 127.0.0.1 (different origin) to trigger CORS error
await Task.WhenAll(
messageTask.Task,
Page.EvaluateFunctionAsync(
"async url => await fetch(url).catch(() => {})",
TestConstants.EmptyPage));
$"{TestConstants.CrossProcessUrl}/empty.html"));

var message = await messageTask.Task;
Assert.That(message.Text, Does.Contain("Access-Control-Allow-Origin"));
Expand Down
38 changes: 38 additions & 0 deletions lib/PuppeteerSharp.Tests/PageTests/ResizeTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
using System.Threading.Tasks;
using NUnit.Framework;
using PuppeteerSharp.Nunit;

namespace PuppeteerSharp.Tests.PageTests
{
public class ResizeTests : PuppeteerPageBaseTest
{
[Test, PuppeteerTest("page.spec", "Page Page.resize", "should resize the browser window to fit page content")]
public async Task ShouldResizeTheBrowserWindowToFitPageContent()
{
var options = TestConstants.DefaultBrowserOptions();
options.DefaultViewport = null;

await using var browser = await Puppeteer.LaunchAsync(options);
var page = await browser.NewPageAsync();

var contentWidth = 500;
var contentHeight = 400;
var resizedTask = page.EvaluateFunctionAsync(
"() => new Promise(resolve => { window.onresize = resolve; })");
await page.ResizeAsync(contentWidth, contentHeight);
await resizedTask;

var innerSize = await page.EvaluateFunctionAsync<Size>(
"() => ({ width: window.innerWidth, height: window.innerHeight })");
Assert.That(innerSize.Width, Is.EqualTo(contentWidth));
Assert.That(innerSize.Height, Is.EqualTo(contentHeight));
}

private sealed class Size
{
public int Width { get; set; }

public int Height { get; set; }
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -666,12 +666,15 @@ public async Task ShouldWorkWithEncodedServerNegative2()
var requests = new List<IRequest>();
Page.AddRequestInterceptor(request =>
{
requests.Add(request);
if (!request.Url.Contains("favicon.ico"))
{
requests.Add(request);
}

return request.ContinueAsync(new Payload(), 0);
});
var response =
await Page.GoToAsync(
$"data:text/html,<link rel=\"stylesheet\" href=\"{TestConstants.ServerUrl}/fonts?helvetica|arial\"/>");
await Page.GoToAsync($"{TestConstants.ServerUrl}/style-404.html");
Assert.That(response.Status, Is.EqualTo(HttpStatusCode.OK));
Assert.That(requests, Has.Count.EqualTo(2));
Assert.That(requests[1].Response.Status, Is.EqualTo(HttpStatusCode.NotFound));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -541,10 +541,14 @@ public async Task ShouldWorkWithEncodedServerNegative2()
var requests = new List<IRequest>();
Page.Request += async (_, e) =>
{
requests.Add(e.Request);
if (!e.Request.Url.Contains("favicon.ico"))
{
requests.Add(e.Request);
}

await e.Request.ContinueAsync();
};
var response = await Page.GoToAsync($"data:text/html,<link rel=\"stylesheet\" href=\"{TestConstants.ServerUrl}/fonts?helvetica|arial\"/>");
var response = await Page.GoToAsync($"{TestConstants.ServerUrl}/style-404.html");
Assert.That(response.Status, Is.EqualTo(HttpStatusCode.OK));
Assert.That(requests, Has.Count.EqualTo(2));

Expand Down
17 changes: 10 additions & 7 deletions lib/PuppeteerSharp.Tests/TracingTests/TracingTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -59,11 +59,11 @@ public async Task ShouldRunWithCustomCategoriesProvided()
{
await Page.Tracing.StartAsync(new TracingOptions
{
Screenshots = true,
Path = _file,
Categories = new List<string>
{
"disabled-by-default-v8.cpu_profiler.hires"
"-*",
"disabled-by-default-devtools.timeline.frame",
}
});

Expand All @@ -73,11 +73,14 @@ await Page.Tracing.StartAsync(new TracingOptions

using var document = JsonDocument.Parse(jsonString);
var root = document.RootElement;
var metadata = root.GetProperty("metadata");
var traceConfig = metadata.GetProperty("trace-config");

var traceConfigString = traceConfig.GetString();
Assert.That(traceConfigString, Does.Contain("disabled-by-default-v8.cpu_profiler.hires"));
var traceEvents = root.GetProperty("traceEvents");
foreach (var traceEvent in traceEvents.EnumerateArray())
{
if (traceEvent.TryGetProperty("cat", out var cat))
{
Assert.That(cat.GetString(), Is.Not.EqualTo("toplevel"));
}
}
}

[Test, PuppeteerTest("tracing.spec", "Tracing", "should run with default categories")]
Expand Down
4 changes: 4 additions & 0 deletions lib/PuppeteerSharp/Bidi/BidiPage.cs
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,10 @@ public override async Task EmulateTimezoneAsync(string timezoneId)
}
}

/// <inheritdoc />
public override Task ResizeAsync(int contentWidth, int contentHeight)
=> throw new NotSupportedException("Resize is not yet supported in WebDriver BiDi.");

/// <inheritdoc />
public override Task EmulateIdleStateAsync(EmulateIdleOverrides idleOverrides = null)
=> _cdpEmulationManager.EmulateIdleStateAsync(idleOverrides);
Expand Down
2 changes: 1 addition & 1 deletion lib/PuppeteerSharp/BrowserData/Chrome.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ public static class Chrome
/// <summary>
/// Default chrome build.
/// </summary>
public static string DefaultBuildId => "138.0.7204.101";
public static string DefaultBuildId => "145.0.7632.46";

internal static async Task<string> ResolveBuildIdAsync(ChromeReleaseChannel channel)
=> (await GetLastKnownGoodReleaseForChannel(channel).ConfigureAwait(false)).Version;
Expand Down
16 changes: 16 additions & 0 deletions lib/PuppeteerSharp/Cdp/CdpPage.cs
Original file line number Diff line number Diff line change
Expand Up @@ -394,6 +394,22 @@ public override async Task SetViewportAsync(ViewPortOptions viewport)
}
}

/// <inheritdoc/>
public override async Task ResizeAsync(int contentWidth, int contentHeight)
{
var response = await PrimaryTargetClient.SendAsync<BrowserGetWindowForTargetResponse>(
"Browser.getWindowForTarget").ConfigureAwait(false);

await PrimaryTargetClient.SendAsync(
"Browser.setContentsSize",
new BrowserSetContentsSizeRequest
{
WindowId = response.WindowId,
Width = contentWidth,
Height = contentHeight,
}).ConfigureAwait(false);
}

/// <inheritdoc/>
public override Task EmulateNetworkConditionsAsync(NetworkConditions networkConditions)
=> FrameManager.NetworkManager.EmulateNetworkConditionsAsync(networkConditions);
Expand Down
43 changes: 28 additions & 15 deletions lib/PuppeteerSharp/Cdp/CdpWebWorker.cs
Original file line number Diff line number Diff line change
Expand Up @@ -92,22 +92,35 @@ public override async Task CloseAsync()
switch (_targetType)
{
case TargetType.ServiceWorker:
if (CdpCDPSession.Connection != null)
{
await CdpCDPSession.Connection.SendAsync(
"Target.closeTarget",
new TargetCloseTargetRequest()
{
TargetId = _id,
}).ConfigureAwait(false);

await CdpCDPSession.Connection.SendAsync(
"Target.detachFromTarget",
new TargetDetachFromTargetRequest()
{
SessionId = Client.Id,
}).ConfigureAwait(false);
}

break;
case TargetType.SharedWorker:
// For service and shared workers we need to close the target and detach to allow
// the worker to stop.
await CdpCDPSession.Connection.SendAsync(
"Target.closeTarget",
new TargetCloseTargetRequest()
{
TargetId = _id,
}).ConfigureAwait(false);

await CdpCDPSession.Connection.SendAsync(
"Target.detachFromTarget",
new TargetDetachFromTargetRequest()
{
SessionId = Client.Id,
}).ConfigureAwait(false);
if (CdpCDPSession.Connection != null)
{
await CdpCDPSession.Connection.SendAsync(
"Target.closeTarget",
new TargetCloseTargetRequest()
{
TargetId = _id,
}).ConfigureAwait(false);
}

break;
default:
await EvaluateFunctionAsync(@"() => {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
namespace PuppeteerSharp.Cdp.Messaging
{
internal class BrowserGetWindowForTargetResponse
{
public int WindowId { get; set; }
}
}
Loading
Loading