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
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
<ApplicationManifest>app.manifest</ApplicationManifest>
<RuntimeIdentifier>$(NETCoreSdkRuntimeIdentifier)</RuntimeIdentifier>
<SelfContained Condition="'$(SelfContained)' == ''">false</SelfContained>
<WarningsNotAsErrors>NU1901;NU1902;NU1903;NU1904</WarningsNotAsErrors>
</PropertyGroup>

<ItemGroup>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
<RuntimeIdentifier>$(NETCoreSdkRuntimeIdentifier)</RuntimeIdentifier>
<SelfContained Condition="'$(SelfContained)' == ''">false</SelfContained>
<ApplicationManifest>app.manifest</ApplicationManifest>
<WarningsNotAsErrors>NU1901;NU1902;NU1903;NU1904</WarningsNotAsErrors>
</PropertyGroup>

<ItemGroup>
Expand Down
1 change: 1 addition & 0 deletions lib/PuppeteerSharp.Tests/CefSharp.Dom.Tests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
<AssemblyName>CefSharp.Dom.Tests</AssemblyName>
<WarningsNotAsErrors>NU1901;NU1902;NU1903;NU1904</WarningsNotAsErrors>
</PropertyGroup>
<ItemGroup>
<Compile Remove="AccessibilityTests\**" />
Expand Down
63 changes: 63 additions & 0 deletions lib/PuppeteerSharp.Tests/JSHandleTests/ClickablePointTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
using System.Threading.Tasks;
using CefSharp.Dom;
using PuppeteerSharp.Tests.Attributes;
using Xunit;
using Xunit.Abstractions;

namespace PuppeteerSharp.Tests.JSHandleTests
{
[Collection(TestConstants.TestFixtureCollectionName)]
public class ClickablePointTests : DevToolsContextBaseTest
{
public ClickablePointTests(ITestOutputHelper output) : base(output)
{
}

[PuppeteerFact]
public async Task ShouldWork()
{
await DevToolsContext.EvaluateExpressionAsync(@"document.body.style.padding = '0';
document.body.style.margin = '0';
document.body.innerHTML = '<div style=""cursor: pointer; width: 120px; height: 60px; margin: 30px; padding: 15px;""></div>';
");

await DevToolsContext.EvaluateExpressionAsync("new Promise(resolve => requestAnimationFrame(() => requestAnimationFrame(resolve)));");

var divHandle = await DevToolsContext.QuerySelectorAsync("div");

var clickablePoint = await divHandle.ClickablePointAsync();

// margin + middle point offset
Assert.Equal(clickablePoint.X, 45 + 60);
Assert.Equal(clickablePoint.Y, 45 + 30);

clickablePoint = await divHandle.ClickablePointAsync(new Offset { X = 10, Y = 15 });

// margin + offset
Assert.Equal(clickablePoint.X, 30 + 10);
Assert.Equal(clickablePoint.Y, 30 + 15);
}

[PuppeteerFact]
public async Task ShouldWorkForIFrames()
{
await DevToolsContext.GoToAsync(TestConstants.ServerUrl + "/frames/one-frame.html");

await DevToolsContext.EvaluateExpressionAsync("new Promise(resolve => requestAnimationFrame(() => requestAnimationFrame(resolve)));");

var frame = DevToolsContext.FirstChildFrame();

var divHandle = await frame.QuerySelectorAsync("div");

var clickablePoint = await divHandle.ClickablePointAsync();

Assert.Equal(160, clickablePoint.X);
Assert.Equal(27, clickablePoint.Y);

clickablePoint = await divHandle.ClickablePointAsync(new Offset { X = 10, Y = 15 });

Assert.Equal(28, clickablePoint.X);
Assert.Equal(33, clickablePoint.Y);
}
}
}
13 changes: 13 additions & 0 deletions lib/PuppeteerSharp.Tests/TouchScreenTests/TouchScreenTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -40,5 +40,18 @@ public async Task ShouldReportTouches()
"Touchend: 0"
}, await DevToolsContext.EvaluateExpressionAsync<string[]>("getResult()"));
}

[PuppeteerFact]
public async Task ShouldReportClickForOffSet()
{
await DevToolsContext.EmulateAsync(_iPhone);
await DevToolsContext.GoToAsync(TestConstants.ServerUrl + "/input/button.html");
var button = await DevToolsContext.QuerySelectorAsync("button");
await button.TapAsync(new CefSharp.Dom.Input.TapOptions { OffSet = new Offset(5, 5) });

var actual = await DevToolsContext.EvaluateExpressionAsync<string>("result");

Assert.Equal("Clicked", actual);
}
}
}
132 changes: 128 additions & 4 deletions lib/PuppeteerSharp/ElementHandle.cs
Original file line number Diff line number Diff line change
Expand Up @@ -209,7 +209,7 @@ public async Task HoverAsync()
public async Task ClickAsync(ClickOptions options = null)
{
await ScrollIntoViewIfNeededAsync().ConfigureAwait(false);
var point = await ClickablePointAsync().ConfigureAwait(false);
var point = await ClickablePointAsync(options?.OffSet).ConfigureAwait(false);
await DevToolsContext.Mouse.ClickAsync(point.X, point.Y, options).ConfigureAwait(false);
}

Expand Down Expand Up @@ -288,17 +288,29 @@ private void CheckForFileAccess(string[] files)
}

/// <summary>
/// Scrolls element into view if needed, and then uses <see cref="Touchscreen.TapAsync(decimal, decimal)"/> to tap in the center of the element.
/// Scrolls element into view if needed, and then uses <see cref="Touchscreen.TapAsync(decimal, decimal)"/> to tap the element
/// at the specified <see cref="TapOptions.OffSet"/> or in the center of the element if OffSet is null.
/// </summary>
/// <param name="options">tap options</param>
/// <exception cref="PuppeteerException">if the element is detached from DOM</exception>
/// <returns>Task which resolves when the element is successfully tapped</returns>
public async Task TapAsync()
public async Task TapAsync(TapOptions options)
{
await ScrollIntoViewIfNeededAsync().ConfigureAwait(false);
var point = await ClickablePointAsync().ConfigureAwait(false);
var point = await ClickablePointAsync(options?.OffSet).ConfigureAwait(false);
await DevToolsContext.Touchscreen.TapAsync(point.X, point.Y).ConfigureAwait(false);
}

/// <summary>
/// Scrolls element into view if needed, and then uses <see cref="Touchscreen.TapAsync(decimal, decimal)"/> to tap in the center of the element.
/// </summary>
/// <exception cref="PuppeteerException">if the element is detached from DOM</exception>
/// <returns>Task which resolves when the element is successfully tapped</returns>
public async Task TapAsync()
{
await TapAsync(null).ConfigureAwait(false);
}

/// <summary>
/// Calls <c>focus</c> <see href="https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/focus"/> on the element.
/// </summary>
Expand Down Expand Up @@ -675,6 +687,24 @@ public async Task DragAndDropAsync(ElementHandle target, int delay = 0)
await DevToolsContext.Mouse.DragAndDropAsync(point.X, point.Y, targetPoint.X, targetPoint.Y, delay).ConfigureAwait(false);
}

/// <summary>
/// Returns the middle point within an element unless a specific offset is provided.
/// </summary>
/// <param name="offset">Optional offset.</param>
/// <exception cref="PuppeteerException">When the node is not visible or not an HTMLElement.</exception>
/// <returns>A <see cref="Task"/> that resolves to the clickable point.</returns>
public async Task<BoxModelPoint> ClickablePointAsync(Offset? offset = null)
{
var box = await ClickableBoxAsync().ConfigureAwait(false) ?? throw new PuppeteerException("Node is either not clickable or not an Element");

if (offset != null)
{
return new BoxModelPoint() { X = box.X + offset.Value.X, Y = box.Y + offset.Value.Y, };
}

return new BoxModelPoint() { X = box.X + (box.Width / 2), Y = box.Y + (box.Height / 2), };
}

/// <summary>
/// Gets a clickable point for the current element (currently the mid point).
/// </summary>
Expand Down Expand Up @@ -812,5 +842,99 @@ private decimal ComputeQuadArea(BoxModelPoint[] quad)
}
return Math.Abs(area);
}

private async Task<BoundingBox> ClickableBoxAsync()
{
var boxes = await EvaluateFunctionAsync<BoundingBox[]>(@"element => {
if (!(element instanceof Element)) {
return null;
}
return [...element.getClientRects()].map(rect => {
return {x: rect.x, y: rect.y, width: rect.width, height: rect.height};
});
}").ConfigureAwait(false);

if (boxes == null || boxes.Length == 0)
{
return null;
}

await IntersectBoundingBoxesWithFrameAsync(boxes).ConfigureAwait(false);

var frame = ExecutionContext.Frame;
var parentFrame = frame.ParentFrame;
while (parentFrame != null)
{
var handle = await frame.FrameElementAsync().ConfigureAwait(false)
?? throw new PuppeteerException("Unsupported frame type");

var parentBox = await handle.EvaluateFunctionAsync<BoundingBox>(@"element => {
// Element is not visible.
if (element.getClientRects().length === 0) {
return null;
}
const rect = element.getBoundingClientRect();
const style = window.getComputedStyle(element);
return {
x:
rect.left +
parseInt(style.paddingLeft, 10) +
parseInt(style.borderLeftWidth, 10),
y:
rect.top +
parseInt(style.paddingTop, 10) +
parseInt(style.borderTopWidth, 10),
};
}").ConfigureAwait(false);

if (parentBox == null)
{
return null;
}

foreach (var box in boxes)
{
box.X += parentBox.X;
box.Y += parentBox.Y;
}

await handle.IntersectBoundingBoxesWithFrameAsync(boxes).ConfigureAwait(false);
frame = parentFrame;
parentFrame = frame.ParentFrame;
}

var resultBox = boxes.FirstOrDefault(box => box.Width >= 1 && box.Height >= 1);

return resultBox;
}

private async Task IntersectBoundingBoxesWithFrameAsync(BoundingBox[] boxes)
{
var documentBox = await EvaluateFunctionAsync<BoundingBox>(@"() => {
return {
width: document.documentElement.clientWidth,
height: document.documentElement.clientHeight,
};
}").ConfigureAwait(false);

foreach (var box in boxes)
{
IntersectBoundingBox(box, documentBox.Width, documentBox.Height);
}
}

private void IntersectBoundingBox(BoundingBox box, decimal width, decimal height)
{
box.Width = Math.Max(
box.X >= 0
? Math.Min(width - box.X, box.Width)
: Math.Min(width, box.Width + box.X),
0);
box.Height = Math.Max(
box.Y >= 0
? Math.Min(height - box.Y, box.Height)
: Math.Min(height, box.Height + box.Y),
0);
}
}
}
45 changes: 45 additions & 0 deletions lib/PuppeteerSharp/Frame.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
using System.Linq;
using System.Threading.Tasks;
using CefSharp.Dom.Input;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json.Linq;

namespace CefSharp.Dom
Expand Down Expand Up @@ -65,6 +66,8 @@ internal Frame(FrameManager frameManager, Frame parentFrame, string frameId, boo
Id = frameId;
IsMainFrame = isMainFrame;

Logger = frameManager.Connection.LoggerFactory.CreateLogger(GetType());

LifecycleEvents = new List<string>();

MainWorld = new DOMWorld(FrameManager, this, FrameManager.TimeoutSettings);
Expand Down Expand Up @@ -111,6 +114,11 @@ public List<Frame> ChildFrames
/// </summary>
public Frame ParentFrame { get; private set; }

/// <summary>
/// Logger.
/// </summary>
protected ILogger Logger { get; }

internal FrameManager FrameManager { get; }

/// <summary>
Expand Down Expand Up @@ -610,5 +618,42 @@ internal void Detach()
}
ParentFrame = null;
}

/// <summary>
/// The frame element associated with this frame (if any).
/// </summary>
/// <returns>Task which resolves to the frame element.</returns>
public async Task<ElementHandle> FrameElementAsync()
{
var parentFrame = ParentFrame;
if (parentFrame == null)
{
return null;
}

var list = await parentFrame.EvaluateFunctionHandleAsync(@"() => {
return document.querySelectorAll('iframe, frame');
}").ConfigureAwait(false);

await foreach (var iframe in list.TransposeIterableHandleAsync())
{
var frame = await iframe.ContentFrameAsync().ConfigureAwait(false);
if (frame?.Id == Id)
{
return iframe as ElementHandle;
}

try
{
await iframe.DisposeAsync().ConfigureAwait(false);
}
catch
{
Logger.LogWarning("FrameElementAsync: Error disposing iframe");
}
}

return null;
}
}
}
5 changes: 5 additions & 0 deletions lib/PuppeteerSharp/Input/ClickOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,5 +19,10 @@ public class ClickOptions
/// The button to use for the click. Defaults to <see cref="MouseButton.Left"/>
/// </summary>
public MouseButton Button { get; set; } = MouseButton.Left;

/// <summary>
/// Offset for the clickable point relative to the top-left corner of the border-box.
/// </summary>
public Offset? OffSet { get; set; }
}
}
13 changes: 13 additions & 0 deletions lib/PuppeteerSharp/Input/TapOptions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
namespace CefSharp.Dom.Input
{
/// <summary>
/// Options to use for <see cref="ElementHandle.TapAsync(TapOptions)"/>
/// </summary>
public class TapOptions
{
/// <summary>
/// Offset for the clickable point relative to the top-left corner of the border-box.
/// </summary>
public Offset? OffSet { get; set; }
}
}
Loading