Skip to content

Commit

Permalink
Add element.SelectAsync and element.EvaluateFunctionAsync (#1292)
Browse files Browse the repository at this point in the history
* Add element.SelectAsync and element.EvaluateFunctionAsync

* ops

* Pass param array as array
  • Loading branch information
kblok authored Sep 22, 2019
1 parent 6f813bd commit 12b3645
Show file tree
Hide file tree
Showing 4 changed files with 104 additions and 35 deletions.
26 changes: 10 additions & 16 deletions lib/PuppeteerSharp/DOMWorld.cs
Original file line number Diff line number Diff line change
Expand Up @@ -307,22 +307,16 @@ internal async Task FocusAsync(string selector)
await handle.DisposeAsync().ConfigureAwait(false);
}

internal Task<string[]> SelectAsync(string selector, params string[] values)
=> QuerySelectorAsync(selector).EvaluateFunctionAsync<string[]>(@"(element, values) => {
if (element.nodeName.toLowerCase() !== 'select')
throw new Error('Element is not a <select> element.');
const options = Array.from(element.options);
element.value = undefined;
for (const option of options) {
option.selected = values.includes(option.value);
if (option.selected && !element.multiple)
break;
}
element.dispatchEvent(new Event('input', { 'bubbles': true }));
element.dispatchEvent(new Event('change', { 'bubbles': true }));
return options.filter(option => option.selected).map(option => option.value);
}", new[] { values });
internal async Task<string[]> SelectAsync(string selector, params string[] values)
{
if (!((await QuerySelectorAsync(selector).ConfigureAwait(false)) is ElementHandle handle))
{
throw new SelectorException($"No node found for selector: {selector}", selector);
}
var result = await handle.SelectAsync(values).ConfigureAwait(false);
await handle.DisposeAsync();
return result;
}

internal async Task TapAsync(string selector)
{
Expand Down
44 changes: 37 additions & 7 deletions lib/PuppeteerSharp/ElementHandle.cs
Original file line number Diff line number Diff line change
Expand Up @@ -230,7 +230,7 @@ public async Task TapAsync()
/// Calls <c>focus</c> <see href="https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/focus"/> on the element.
/// </summary>
/// <returns>Task</returns>
public Task FocusAsync() => ExecutionContext.EvaluateFunctionAsync("element => element.focus()", this);
public Task FocusAsync() => EvaluateFunctionAsync("element => element.focus()");

/// <summary>
/// Focuses the element, and sends a <c>keydown</c>, <c>keypress</c>/<c>input</c>, and <c>keyup</c> event for each character in the text.
Expand Down Expand Up @@ -281,9 +281,9 @@ public async Task PressAsync(string key, PressOptions options = null)
/// <returns>Task which resolves to <see cref="ElementHandle"/> pointing to the frame element</returns>
public async Task<ElementHandle> QuerySelectorAsync(string selector)
{
var handle = await ExecutionContext.EvaluateFunctionHandleAsync(
var handle = await EvaluateFunctionHandleAsync(
"(element, selector) => element.querySelector(selector)",
this, selector).ConfigureAwait(false);
selector).ConfigureAwait(false);

if (handle is ElementHandle element)
{
Expand All @@ -301,9 +301,9 @@ public async Task<ElementHandle> QuerySelectorAsync(string selector)
/// <returns>Task which resolves to ElementHandles pointing to the frame elements</returns>
public async Task<ElementHandle[]> QuerySelectorAllAsync(string selector)
{
var arrayHandle = await ExecutionContext.EvaluateFunctionHandleAsync(
var arrayHandle = await EvaluateFunctionHandleAsync(
"(element, selector) => element.querySelectorAll(selector)",
this, selector).ConfigureAwait(false);
selector).ConfigureAwait(false);

var properties = await arrayHandle.GetPropertiesAsync().ConfigureAwait(false);
await arrayHandle.DisposeAsync().ConfigureAwait(false);
Expand Down Expand Up @@ -423,6 +423,36 @@ public Task<bool> IsIntersectingViewportAsync()
return visibleRatio > 0;
}", this);

/// <summary>
/// Triggers a `change` and `input` event once all the provided options have been selected.
/// If there's no `select` element matching `selector`, the method throws an exception.
/// </summary>
/// <example>
/// <code>
/// await handle.SelectAsync("blue"); // single selection
/// await handle.SelectAsync("red", "green", "blue"); // multiple selections
/// </code>
/// </example>
/// <param name="values">Values of options to select. If the `select` has the `multiple` attribute, all values are considered, otherwise only the first one is taken into account.</param>
/// <returns>A task that resolves to an array of option values that have been successfully selected.</returns>
public Task<string[]> SelectAsync(params string[] values)
=> EvaluateFunctionAsync<string[]>(@"(element, values) =>
{
if (element.nodeName.toLowerCase() !== 'select')
throw new Error('Element is not a <select> element.');
const options = Array.from(element.options);
element.value = undefined;
for (const option of options) {
option.selected = values.includes(option.value);
if (option.selected && !element.multiple)
break;
}
element.dispatchEvent(new Event('input', { 'bubbles': true }));
element.dispatchEvent(new Event('change', { 'bubbles': true }));
return options.filter(option => option.selected).map(option => option.value);
}", new[] { values });

private async Task<(decimal x, decimal y)> ClickablePointAsync()
{
GetContentQuadsResponse result = null;
Expand Down Expand Up @@ -484,7 +514,7 @@ private IEnumerable<BoxModelPoint> IntersectQuadWithViewport(IEnumerable<BoxMode

private async Task ScrollIntoViewIfNeededAsync()
{
var errorMessage = await ExecutionContext.EvaluateFunctionAsync<string>(@"async(element, pageJavascriptEnabled) => {
var errorMessage = await EvaluateFunctionAsync<string>(@"async(element, pageJavascriptEnabled) => {
if (!element.isConnected)
return 'Node is detached from document';
if (element.nodeType !== Node.ELEMENT_NODE)
Expand All @@ -504,7 +534,7 @@ private async Task ScrollIntoViewIfNeededAsync()
if (visibleRatio !== 1.0)
element.scrollIntoView({block: 'center', inline: 'center', behavior: 'instant'});
return null;
}", this, Page.JavascriptEnabled).ConfigureAwait(false);
}", Page.JavascriptEnabled).ConfigureAwait(false);

if (errorMessage != null)
{
Expand Down
12 changes: 2 additions & 10 deletions lib/PuppeteerSharp/Extensions.cs
100755 → 100644
Original file line number Diff line number Diff line change
Expand Up @@ -54,10 +54,7 @@ public static async Task<T> EvaluateFunctionAsync<T>(this ElementHandle elementH
throw new SelectorException("Error: failed to find element matching selector");
}

var newArgs = new object[args.Length + 1];
newArgs[0] = elementHandle;
args.CopyTo(newArgs, 1);
var result = await elementHandle.ExecutionContext.EvaluateFunctionAsync<T>(pageFunction, newArgs).ConfigureAwait(false);
var result = await elementHandle.EvaluateFunctionAsync<T>(pageFunction, args).ConfigureAwait(false);
await elementHandle.DisposeAsync().ConfigureAwait(false);
return result;
}
Expand Down Expand Up @@ -93,12 +90,7 @@ public static async Task<T> EvaluateFunctionAsync<T>(this Task<JSHandle> arrayHa
/// <returns>Task which resolves to the return value of <c>pageFunction</c></returns>
public static async Task<T> EvaluateFunctionAsync<T>(this JSHandle arrayHandle, string pageFunction, params object[] args)
{
var response = await arrayHandle.JsonValueAsync<object[]>().ConfigureAwait(false);

var newArgs = new object[args.Length + 1];
newArgs[0] = arrayHandle;
args.CopyTo(newArgs, 1);
var result = await arrayHandle.ExecutionContext.EvaluateFunctionAsync<T>(pageFunction, newArgs).ConfigureAwait(false);
var result = await arrayHandle.EvaluateFunctionAsync<T>(pageFunction, args).ConfigureAwait(false);
await arrayHandle.DisposeAsync().ConfigureAwait(false);
return result;
}
Expand Down
57 changes: 55 additions & 2 deletions lib/PuppeteerSharp/JSHandle.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using PuppeteerSharp.Helpers;
using PuppeteerSharp.Helpers.Json;
using PuppeteerSharp.Messaging;
Expand Down Expand Up @@ -55,11 +56,11 @@ internal JSHandle(ExecutionContext context, CDPSession client, RemoteObject remo
/// <returns>Task of <see cref="JSHandle"/></returns>
public async Task<JSHandle> GetPropertyAsync(string propertyName)
{
var objectHandle = await ExecutionContext.EvaluateFunctionHandleAsync(@"(object, propertyName) => {
var objectHandle = await EvaluateFunctionHandleAsync(@"(object, propertyName) => {
const result = { __proto__: null};
result[propertyName] = object[propertyName];
return result;
}", this, propertyName).ConfigureAwait(false);
}", propertyName).ConfigureAwait(false);
var properties = await objectHandle.GetPropertiesAsync().ConfigureAwait(false);
properties.TryGetValue(propertyName, out var result);
await objectHandle.DisposeAsync().ConfigureAwait(false);
Expand Down Expand Up @@ -166,6 +167,58 @@ public override string ToString()
return "JSHandle:" + RemoteObjectHelper.ValueFromRemoteObject<object>(RemoteObject)?.ToString();
}

/// <summary>
/// Executes a script in browser context
/// </summary>
/// <param name="pageFunction">Script to be evaluated in browser context</param>
/// <param name="args">Function arguments</param>
/// <remarks>
/// If the script, returns a Promise, then the method would wait for the promise to resolve and return its value.
/// <see cref="JSHandle"/> instances can be passed as arguments
/// </remarks>
/// <returns>Task which resolves to script return value</returns>
public Task<JSHandle> EvaluateFunctionHandleAsync(string pageFunction, params object[] args)
{
var list = new List<object>(args);
list.Insert(0, this);
return ExecutionContext.EvaluateFunctionHandleAsync(pageFunction, list.ToArray());
}

/// <summary>
/// Executes a function in browser context
/// </summary>
/// <param name="script">Script to be evaluated in browser context</param>
/// <param name="args">Arguments to pass to script</param>
/// <remarks>
/// If the script, returns a Promise, then the method would wait for the promise to resolve and return its value.
/// <see cref="JSHandle"/> instances can be passed as arguments
/// </remarks>
/// <returns>Task which resolves to script return value</returns>
public Task<JToken> EvaluateFunctionAsync(string script, params object[] args)
{
var list = new List<object>(args);
list.Insert(0, this);
return ExecutionContext.EvaluateFunctionAsync<JToken>(script, list.ToArray());
}

/// <summary>
/// Executes a function in browser context
/// </summary>
/// <typeparam name="T">The type to deserialize the result to</typeparam>
/// <param name="script">Script to be evaluated in browser context</param>
/// <param name="args">Arguments to pass to script</param>
/// <remarks>
/// If the script, returns a Promise, then the method would wait for the promise to resolve and return its value.
/// <see cref="JSHandle"/> instances can be passed as arguments
/// </remarks>
/// <returns>Task which resolves to script return value</returns>
public Task<T> EvaluateFunctionAsync<T>(string script, params object[] args)
{
var list = new List<object>(args);
list.Insert(0, this);
return ExecutionContext.EvaluateFunctionAsync<T>(script, list.ToArray());
}

internal object FormatArgument(ExecutionContext context)
{
if (ExecutionContext != context)
Expand Down

0 comments on commit 12b3645

Please sign in to comment.