Skip to content

Commit 3a30e8a

Browse files
authored
Add ClickOptions.OffSet and TapOptions (#61)
* Add ClickOptions.OffSet * Extend TapAsync to support OffSet Resolves #60 Resolves #62 --------- Co-authored-by: amaitland <[email protected]>
1 parent 7e8604c commit 3a30e8a

File tree

11 files changed

+361
-4
lines changed

11 files changed

+361
-4
lines changed

lib/CefSharp.Dom.WinForms.Example/CefSharp.Dom.WinForms.Example.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
<ApplicationManifest>app.manifest</ApplicationManifest>
88
<RuntimeIdentifier>$(NETCoreSdkRuntimeIdentifier)</RuntimeIdentifier>
99
<SelfContained Condition="'$(SelfContained)' == ''">false</SelfContained>
10+
<WarningsNotAsErrors>NU1901;NU1902;NU1903;NU1904</WarningsNotAsErrors>
1011
</PropertyGroup>
1112

1213
<ItemGroup>

lib/CefSharp.Dom.Wpf.Example/CefSharp.Dom.Wpf.Example.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
<RuntimeIdentifier>$(NETCoreSdkRuntimeIdentifier)</RuntimeIdentifier>
88
<SelfContained Condition="'$(SelfContained)' == ''">false</SelfContained>
99
<ApplicationManifest>app.manifest</ApplicationManifest>
10+
<WarningsNotAsErrors>NU1901;NU1902;NU1903;NU1904</WarningsNotAsErrors>
1011
</PropertyGroup>
1112

1213
<ItemGroup>

lib/PuppeteerSharp.Tests/CefSharp.Dom.Tests.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
99
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
1010
<AssemblyName>CefSharp.Dom.Tests</AssemblyName>
11+
<WarningsNotAsErrors>NU1901;NU1902;NU1903;NU1904</WarningsNotAsErrors>
1112
</PropertyGroup>
1213
<ItemGroup>
1314
<Compile Remove="AccessibilityTests\**" />
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
using System.Threading.Tasks;
2+
using CefSharp.Dom;
3+
using PuppeteerSharp.Tests.Attributes;
4+
using Xunit;
5+
using Xunit.Abstractions;
6+
7+
namespace PuppeteerSharp.Tests.JSHandleTests
8+
{
9+
[Collection(TestConstants.TestFixtureCollectionName)]
10+
public class ClickablePointTests : DevToolsContextBaseTest
11+
{
12+
public ClickablePointTests(ITestOutputHelper output) : base(output)
13+
{
14+
}
15+
16+
[PuppeteerFact]
17+
public async Task ShouldWork()
18+
{
19+
await DevToolsContext.EvaluateExpressionAsync(@"document.body.style.padding = '0';
20+
document.body.style.margin = '0';
21+
document.body.innerHTML = '<div style=""cursor: pointer; width: 120px; height: 60px; margin: 30px; padding: 15px;""></div>';
22+
");
23+
24+
await DevToolsContext.EvaluateExpressionAsync("new Promise(resolve => requestAnimationFrame(() => requestAnimationFrame(resolve)));");
25+
26+
var divHandle = await DevToolsContext.QuerySelectorAsync("div");
27+
28+
var clickablePoint = await divHandle.ClickablePointAsync();
29+
30+
// margin + middle point offset
31+
Assert.Equal(clickablePoint.X, 45 + 60);
32+
Assert.Equal(clickablePoint.Y, 45 + 30);
33+
34+
clickablePoint = await divHandle.ClickablePointAsync(new Offset { X = 10, Y = 15 });
35+
36+
// margin + offset
37+
Assert.Equal(clickablePoint.X, 30 + 10);
38+
Assert.Equal(clickablePoint.Y, 30 + 15);
39+
}
40+
41+
[PuppeteerFact]
42+
public async Task ShouldWorkForIFrames()
43+
{
44+
await DevToolsContext.GoToAsync(TestConstants.ServerUrl + "/frames/one-frame.html");
45+
46+
await DevToolsContext.EvaluateExpressionAsync("new Promise(resolve => requestAnimationFrame(() => requestAnimationFrame(resolve)));");
47+
48+
var frame = DevToolsContext.FirstChildFrame();
49+
50+
var divHandle = await frame.QuerySelectorAsync("div");
51+
52+
var clickablePoint = await divHandle.ClickablePointAsync();
53+
54+
Assert.Equal(160, clickablePoint.X);
55+
Assert.Equal(27, clickablePoint.Y);
56+
57+
clickablePoint = await divHandle.ClickablePointAsync(new Offset { X = 10, Y = 15 });
58+
59+
Assert.Equal(28, clickablePoint.X);
60+
Assert.Equal(33, clickablePoint.Y);
61+
}
62+
}
63+
}

lib/PuppeteerSharp.Tests/TouchScreenTests/TouchScreenTests.cs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,5 +40,18 @@ public async Task ShouldReportTouches()
4040
"Touchend: 0"
4141
}, await DevToolsContext.EvaluateExpressionAsync<string[]>("getResult()"));
4242
}
43+
44+
[PuppeteerFact]
45+
public async Task ShouldReportClickForOffSet()
46+
{
47+
await DevToolsContext.EmulateAsync(_iPhone);
48+
await DevToolsContext.GoToAsync(TestConstants.ServerUrl + "/input/button.html");
49+
var button = await DevToolsContext.QuerySelectorAsync("button");
50+
await button.TapAsync(new CefSharp.Dom.Input.TapOptions { OffSet = new Offset(5, 5) });
51+
52+
var actual = await DevToolsContext.EvaluateExpressionAsync<string>("result");
53+
54+
Assert.Equal("Clicked", actual);
55+
}
4356
}
4457
}

lib/PuppeteerSharp/ElementHandle.cs

Lines changed: 128 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -209,7 +209,7 @@ public async Task HoverAsync()
209209
public async Task ClickAsync(ClickOptions options = null)
210210
{
211211
await ScrollIntoViewIfNeededAsync().ConfigureAwait(false);
212-
var point = await ClickablePointAsync().ConfigureAwait(false);
212+
var point = await ClickablePointAsync(options?.OffSet).ConfigureAwait(false);
213213
await DevToolsContext.Mouse.ClickAsync(point.X, point.Y, options).ConfigureAwait(false);
214214
}
215215

@@ -288,17 +288,29 @@ private void CheckForFileAccess(string[] files)
288288
}
289289

290290
/// <summary>
291-
/// Scrolls element into view if needed, and then uses <see cref="Touchscreen.TapAsync(decimal, decimal)"/> to tap in the center of the element.
291+
/// Scrolls element into view if needed, and then uses <see cref="Touchscreen.TapAsync(decimal, decimal)"/> to tap the element
292+
/// at the specified <see cref="TapOptions.OffSet"/> or in the center of the element if OffSet is null.
292293
/// </summary>
294+
/// <param name="options">tap options</param>
293295
/// <exception cref="PuppeteerException">if the element is detached from DOM</exception>
294296
/// <returns>Task which resolves when the element is successfully tapped</returns>
295-
public async Task TapAsync()
297+
public async Task TapAsync(TapOptions options)
296298
{
297299
await ScrollIntoViewIfNeededAsync().ConfigureAwait(false);
298-
var point = await ClickablePointAsync().ConfigureAwait(false);
300+
var point = await ClickablePointAsync(options?.OffSet).ConfigureAwait(false);
299301
await DevToolsContext.Touchscreen.TapAsync(point.X, point.Y).ConfigureAwait(false);
300302
}
301303

304+
/// <summary>
305+
/// Scrolls element into view if needed, and then uses <see cref="Touchscreen.TapAsync(decimal, decimal)"/> to tap in the center of the element.
306+
/// </summary>
307+
/// <exception cref="PuppeteerException">if the element is detached from DOM</exception>
308+
/// <returns>Task which resolves when the element is successfully tapped</returns>
309+
public async Task TapAsync()
310+
{
311+
await TapAsync(null).ConfigureAwait(false);
312+
}
313+
302314
/// <summary>
303315
/// Calls <c>focus</c> <see href="https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/focus"/> on the element.
304316
/// </summary>
@@ -675,6 +687,24 @@ public async Task DragAndDropAsync(ElementHandle target, int delay = 0)
675687
await DevToolsContext.Mouse.DragAndDropAsync(point.X, point.Y, targetPoint.X, targetPoint.Y, delay).ConfigureAwait(false);
676688
}
677689

690+
/// <summary>
691+
/// Returns the middle point within an element unless a specific offset is provided.
692+
/// </summary>
693+
/// <param name="offset">Optional offset.</param>
694+
/// <exception cref="PuppeteerException">When the node is not visible or not an HTMLElement.</exception>
695+
/// <returns>A <see cref="Task"/> that resolves to the clickable point.</returns>
696+
public async Task<BoxModelPoint> ClickablePointAsync(Offset? offset = null)
697+
{
698+
var box = await ClickableBoxAsync().ConfigureAwait(false) ?? throw new PuppeteerException("Node is either not clickable or not an Element");
699+
700+
if (offset != null)
701+
{
702+
return new BoxModelPoint() { X = box.X + offset.Value.X, Y = box.Y + offset.Value.Y, };
703+
}
704+
705+
return new BoxModelPoint() { X = box.X + (box.Width / 2), Y = box.Y + (box.Height / 2), };
706+
}
707+
678708
/// <summary>
679709
/// Gets a clickable point for the current element (currently the mid point).
680710
/// </summary>
@@ -812,5 +842,99 @@ private decimal ComputeQuadArea(BoxModelPoint[] quad)
812842
}
813843
return Math.Abs(area);
814844
}
845+
846+
private async Task<BoundingBox> ClickableBoxAsync()
847+
{
848+
var boxes = await EvaluateFunctionAsync<BoundingBox[]>(@"element => {
849+
if (!(element instanceof Element)) {
850+
return null;
851+
}
852+
return [...element.getClientRects()].map(rect => {
853+
return {x: rect.x, y: rect.y, width: rect.width, height: rect.height};
854+
});
855+
}").ConfigureAwait(false);
856+
857+
if (boxes == null || boxes.Length == 0)
858+
{
859+
return null;
860+
}
861+
862+
await IntersectBoundingBoxesWithFrameAsync(boxes).ConfigureAwait(false);
863+
864+
var frame = ExecutionContext.Frame;
865+
var parentFrame = frame.ParentFrame;
866+
while (parentFrame != null)
867+
{
868+
var handle = await frame.FrameElementAsync().ConfigureAwait(false)
869+
?? throw new PuppeteerException("Unsupported frame type");
870+
871+
var parentBox = await handle.EvaluateFunctionAsync<BoundingBox>(@"element => {
872+
// Element is not visible.
873+
if (element.getClientRects().length === 0) {
874+
return null;
875+
}
876+
const rect = element.getBoundingClientRect();
877+
const style = window.getComputedStyle(element);
878+
return {
879+
x:
880+
rect.left +
881+
parseInt(style.paddingLeft, 10) +
882+
parseInt(style.borderLeftWidth, 10),
883+
y:
884+
rect.top +
885+
parseInt(style.paddingTop, 10) +
886+
parseInt(style.borderTopWidth, 10),
887+
};
888+
}").ConfigureAwait(false);
889+
890+
if (parentBox == null)
891+
{
892+
return null;
893+
}
894+
895+
foreach (var box in boxes)
896+
{
897+
box.X += parentBox.X;
898+
box.Y += parentBox.Y;
899+
}
900+
901+
await handle.IntersectBoundingBoxesWithFrameAsync(boxes).ConfigureAwait(false);
902+
frame = parentFrame;
903+
parentFrame = frame.ParentFrame;
904+
}
905+
906+
var resultBox = boxes.FirstOrDefault(box => box.Width >= 1 && box.Height >= 1);
907+
908+
return resultBox;
909+
}
910+
911+
private async Task IntersectBoundingBoxesWithFrameAsync(BoundingBox[] boxes)
912+
{
913+
var documentBox = await EvaluateFunctionAsync<BoundingBox>(@"() => {
914+
return {
915+
width: document.documentElement.clientWidth,
916+
height: document.documentElement.clientHeight,
917+
};
918+
}").ConfigureAwait(false);
919+
920+
foreach (var box in boxes)
921+
{
922+
IntersectBoundingBox(box, documentBox.Width, documentBox.Height);
923+
}
924+
}
925+
926+
private void IntersectBoundingBox(BoundingBox box, decimal width, decimal height)
927+
{
928+
box.Width = Math.Max(
929+
box.X >= 0
930+
? Math.Min(width - box.X, box.Width)
931+
: Math.Min(width, box.Width + box.X),
932+
0);
933+
box.Height = Math.Max(
934+
box.Y >= 0
935+
? Math.Min(height - box.Y, box.Height)
936+
: Math.Min(height, box.Height + box.Y),
937+
0);
938+
}
815939
}
816940
}

lib/PuppeteerSharp/Frame.cs

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
using System.Linq;
44
using System.Threading.Tasks;
55
using CefSharp.Dom.Input;
6+
using Microsoft.Extensions.Logging;
67
using Newtonsoft.Json.Linq;
78

89
namespace CefSharp.Dom
@@ -65,6 +66,8 @@ internal Frame(FrameManager frameManager, Frame parentFrame, string frameId, boo
6566
Id = frameId;
6667
IsMainFrame = isMainFrame;
6768

69+
Logger = frameManager.Connection.LoggerFactory.CreateLogger(GetType());
70+
6871
LifecycleEvents = new List<string>();
6972

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

117+
/// <summary>
118+
/// Logger.
119+
/// </summary>
120+
protected ILogger Logger { get; }
121+
114122
internal FrameManager FrameManager { get; }
115123

116124
/// <summary>
@@ -610,5 +618,42 @@ internal void Detach()
610618
}
611619
ParentFrame = null;
612620
}
621+
622+
/// <summary>
623+
/// The frame element associated with this frame (if any).
624+
/// </summary>
625+
/// <returns>Task which resolves to the frame element.</returns>
626+
public async Task<ElementHandle> FrameElementAsync()
627+
{
628+
var parentFrame = ParentFrame;
629+
if (parentFrame == null)
630+
{
631+
return null;
632+
}
633+
634+
var list = await parentFrame.EvaluateFunctionHandleAsync(@"() => {
635+
return document.querySelectorAll('iframe, frame');
636+
}").ConfigureAwait(false);
637+
638+
await foreach (var iframe in list.TransposeIterableHandleAsync())
639+
{
640+
var frame = await iframe.ContentFrameAsync().ConfigureAwait(false);
641+
if (frame?.Id == Id)
642+
{
643+
return iframe as ElementHandle;
644+
}
645+
646+
try
647+
{
648+
await iframe.DisposeAsync().ConfigureAwait(false);
649+
}
650+
catch
651+
{
652+
Logger.LogWarning("FrameElementAsync: Error disposing iframe");
653+
}
654+
}
655+
656+
return null;
657+
}
613658
}
614659
}

lib/PuppeteerSharp/Input/ClickOptions.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,5 +19,10 @@ public class ClickOptions
1919
/// The button to use for the click. Defaults to <see cref="MouseButton.Left"/>
2020
/// </summary>
2121
public MouseButton Button { get; set; } = MouseButton.Left;
22+
23+
/// <summary>
24+
/// Offset for the clickable point relative to the top-left corner of the border-box.
25+
/// </summary>
26+
public Offset? OffSet { get; set; }
2227
}
2328
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
namespace CefSharp.Dom.Input
2+
{
3+
/// <summary>
4+
/// Options to use for <see cref="ElementHandle.TapAsync(TapOptions)"/>
5+
/// </summary>
6+
public class TapOptions
7+
{
8+
/// <summary>
9+
/// Offset for the clickable point relative to the top-left corner of the border-box.
10+
/// </summary>
11+
public Offset? OffSet { get; set; }
12+
}
13+
}

0 commit comments

Comments
 (0)