Skip to content
41 changes: 41 additions & 0 deletions src/PuppeteerSharp.Contrib.Extensions/ElementHandleExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -280,6 +280,47 @@ public static async Task<bool> IsEmptyAsync(this IElementHandle elementHandle)
}").ConfigureAwait(false);
}

/// <summary>
/// Clicks at random point of an element.
/// </summary>
/// <param name="elementHandle">An <see cref="IElementHandle"/>.</param>
/// <param name="isCircular"><c>true</c> if the element is circular.</param>
/// <returns>A <see cref="Task"/> representing the result of the asynchronous operation.</returns>
public static async Task ClickAtRandomPointAsync(this IElementHandle elementHandle, bool isCircular = false)
{
Random rnd = new();
var box = await elementHandle.BoundingBoxAsync();

if (box == null)
{
await elementHandle.ClickAsync();
}
else
{
if (isCircular)
{
double r = (double)Math.Min(box.Width, box.Height);
r = rnd.NextDouble() * r;
var a = rnd.NextDouble() * 2 * Math.PI;
var sin = Math.Sin(a);
var cos = Math.Cos(a);
var x = ((double)box.Width / 2) + (cos * r);
var y = ((double)box.Height / 2) + (sin * r);
await elementHandle.ClickAsync(new PuppeteerSharp.Input.ClickOptions
{
OffSet = new Offset((decimal)x, (decimal)y),
});
}
else
{
await elementHandle.ClickAsync(new PuppeteerSharp.Input.ClickOptions
{
OffSet = new Offset(rnd.Next((int)box.Width * 100) / 100, rnd.Next((int)box.Height * 100) / 100),
});
}
}
}

private static async Task<string> GetPropertyValueAsync(this IElementHandle elementHandle, string propertyName)
{
var property = await elementHandle.GuardFromNull().GetPropertyAsync(propertyName).ConfigureAwait(false);
Expand Down
29 changes: 29 additions & 0 deletions src/PuppeteerSharp.Contrib.Extensions/IFrameExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
using System;
using System.Linq;
using System.Threading.Tasks;

namespace PuppeteerSharp.Contrib.Extensions
{
/// <summary>
/// Extension methods for <see cref="IFrame"/>.
/// </summary>
public static class IFrameExtensions
{
/// <summary>
/// Waits for the specific element to be removed from iframe's DOM.
/// </summary>
/// <param name="iframe">A <see cref="IFrame"/>.</param>
/// <param name="selector">A selector to query iframe for.</param>
/// <param name="timeout">Maximum time to wait for in milliseconds. Pass 0 to disable timeout. Pass null to use default timeout.</param>
/// <returns>A <see cref="Task"/> representing the result of the asynchronous operation.</returns>
public static async Task WaitForElementsRemovedFromDOMAsync(this IFrame iframe, string selector, int? timeout = null)
{
var options = new WaitForFunctionOptions { Polling = WaitForFunctionPollingOption.Mutation };
if (timeout.HasValue) options.Timeout = timeout;
await iframe.GuardFromNull().WaitForFunctionAsync(
string.Format("async () => document.querySelector('{0}') === null", selector),
options)
.ConfigureAwait(false);
}
}
}
7 changes: 7 additions & 0 deletions src/PuppeteerSharp.Contrib.Extensions/InternalExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,13 @@ internal static IResponse GuardFromNull(this IResponse response)
return response;
}

internal static IFrame GuardFromNull(this IFrame frame)
{
if (frame == null) throw new ArgumentNullException(nameof(frame));

return frame;
}

internal static IElementHandle GuardFromNull(this IElementHandle elementHandle)
{
if (elementHandle == null) throw new ArgumentNullException(nameof(elementHandle));
Expand Down
17 changes: 17 additions & 0 deletions src/PuppeteerSharp.Contrib.Extensions/PageExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -98,5 +98,22 @@ public static async Task<bool> HasUrlAsync(this IPage page, string regex, string
{
return await page.GuardFromNull().EvaluateFunctionAsync<bool>("(regex, flags) => RegExp(regex, flags).test(window.location.href)", regex, flags).ConfigureAwait(false);
}

/// <summary>
/// Waits for the specific element or elements to be removed from page's DOM.
/// </summary>
/// <param name="page">A <see cref="IPage"/>.</param>
/// <param name="selector">An element's selector to query page for.</param>
/// <param name="timeout">Maximum time to wait for in milliseconds. Pass 0 to disable timeout. Pass null to use Page default timeout.</param>
/// <returns>A <see cref="Task"/> representing the result of the asynchronous operation.</returns>
public static async Task WaitForElementsRemovedFromDOMAsync(this IPage page, string selector, int? timeout = null)
{
var options = new WaitForFunctionOptions { Polling = WaitForFunctionPollingOption.Mutation };
if (timeout.HasValue) options.Timeout = timeout;
await page.GuardFromNull().WaitForFunctionAsync(
string.Format("async () => document.querySelector('{0}') === null", selector),
options)
.ConfigureAwait(false);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,13 @@

<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<Version>6.0.0</Version>
<Version>6.1.0</Version>
<PackageReleaseNotes>
⬆️ Bump PuppeteerSharp to 12.0.0
👽️ Use PuppeteerSharp interfaces; IPage and IElementHandle
New extension for IPage and IFrame:
◼️ WaitForElementsRemovedFromDOMAsync (handy to wait for spinner to disappear etc)

New extensions for IPage:
◼️ HasUrlAsync

New extensions for IElementHandle:
◼️ HasAttributeValueAsync
◼️ IsEmptyAsync

Extensions for IResponse:
◼️ HasUrl
New extension for IElementHandle:
◼️ ClickAtRandomPointAsync (useful in case of need to emulate a click in detailed manner)
</PackageReleaseNotes>
<Authors>Henrik Lau Eriksson</Authors>
<Description>Contributions to the Headless Chrome .NET API 🌐🧪
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
using System;
using System.Linq;
using System.Threading.Tasks;
using NUnit.Framework;
using PuppeteerSharp.Contrib.Extensions;

namespace PuppeteerSharp.Contrib.Tests.Extensions
{
public class IFrameExtensionsTests : PuppeteerIFrameBaseTest
{
[Test]
public async Task WaitForElementsRemovedFromDOMAsync_should_return_if_page_has_no_elements_matching_the_selector()
{
await BlankFrame(IFrameId);
await IFrame.WaitForElementsRemovedFromDOMAsync(".svg-Wikipedia_wordmark");
}

[Test]
public async Task WaitForElementsRemovedFromDOMAsync_should_return_if_elements_matching_the_selector_are_removed_from_page()
{
Task.WaitAll(IFrame.WaitForElementsRemovedFromDOMAsync(".svg-Wikipedia_wordmark"), BlankFrame(IFrameId));
}

[Test]
public async Task WaitForElementsRemovedFromDOMAsync_should_throw_WaitTaskTimeoutException_if_element_is_present_in_DOM()
{
Assert.ThrowsAsync<WaitTaskTimeoutException>(async () => await IFrame.WaitForElementsRemovedFromDOMAsync(".svg-Wikipedia_wordmark", 1));
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,12 @@ protected override async Task SetUp() => await Page.SetContentAsync(@"
<div id='baz'>Baz</div>
</html>");

private async Task RemoveFoo() => await Page.SetContentAsync(@"
<html>
<div id='bar'>Bar</div>
<div id='baz'>Baz</div>
</html>");

[Test]
public async Task QuerySelectorWithContentAsync_should_return_the_first_element_that_match_the_selector_and_has_the_content()
{
Expand Down Expand Up @@ -84,5 +90,26 @@ public async Task HasUrlAsync_should_return_true_if_page_has_the_url()
Assert.False(await Page.HasUrlAsync("Missing"));
Assert.ThrowsAsync<ArgumentNullException>(async () => await ((IPage)null).HasUrlAsync(""));
}

[Test]
public async Task WaitForElementsRemovedFromDOMAsync_should_throw_WaitTaskTimeoutException_if_element_is_present_in_DOM()
{
Assert.ThrowsAsync<WaitTaskTimeoutException>(async () => await Page.WaitForElementsRemovedFromDOMAsync("#foo", 1));
}

[Test]
public async Task WaitForElementsRemovedFromDOMAsync_should_return_if_page_has_no_elements_matching_the_selector()
{
await RemoveFoo();
await Page.WaitForElementsRemovedFromDOMAsync("#foo");
Assert.Null(await Page.QuerySelectorWithContentAsync("div", "Foo"));
}

[Test]
public async Task WaitForElementsRemovedFromDOMAsync_should_return_if_elements_matching_the_selector_are_removed_from_page()
{
Task.WaitAll(Page.WaitForElementsRemovedFromDOMAsync("#foo"), RemoveFoo());
Assert.Null(await Page.QuerySelectorWithContentAsync("div", "Foo"));
}
}
}
48 changes: 48 additions & 0 deletions tests/PuppeteerSharp.Contrib.Tests/PuppeteerIFrameBaseTest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace PuppeteerSharp.Contrib.Tests
{
public abstract class PuppeteerIFrameBaseTest : PuppeteerPageBaseTest
{
protected IFrame IFrame { get; private set; }

protected const string IFrameId = "frameId";

protected async Task AppendFrameAsync(IPage page, string frameId, string url)
{
await page.EvaluateFunctionAsync(@"(frameId, url) => {
const frame = document.createElement('iframe');
frame.src = url;
frame.id = frameId;
document.body.appendChild(frame);
return new Promise(x => frame.onload = x);
}", frameId, url);
}

protected async Task NavigateFrameAsync(IPage page, string frameId, string url)
{
await page.EvaluateFunctionAsync(@"function navigateFrame(frameId, url) {
const frame = document.getElementById(frameId);
frame.src = url;
return new Promise(x => frame.onload = x);
}", frameId, url);
}
protected async Task BlankFrame(string frameId) => await NavigateFrameAsync(Page, frameId, "about:blank");

/// <remarks>
/// Seems like neither srcdoc attribute, nor loading html from filesystem are working, so tests here are a bit fragile, cause rely on a public common url.
/// </remarks>
protected override async Task SetUp()
{
await Page.SetContentAsync("<html></html>");
await AppendFrameAsync(Page, IFrameId, "https://wikipedia.org");
await Page.WaitForSelectorAsync("iframe");
var frames = Page.Frames;
IFrame = frames.First(f => f.ParentFrame == Page.MainFrame);
}
}
}