Skip to content

Commit

Permalink
Element query selectors (#14)
Browse files Browse the repository at this point in the history
* implement select from remote element

* implement select all from remote element

* prepare release
  • Loading branch information
JonasGruenwald authored Sep 7, 2024
1 parent 1f82e77 commit 3c93ba8
Show file tree
Hide file tree
Showing 7 changed files with 241 additions and 53 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
## [2.3.0] 2024-07

- Add query selectors that run on elements (Remote Objects)

## [2.2.5] 2024-08-12

- Upgrade to Gleam 1.4.1
Expand Down
20 changes: 19 additions & 1 deletion birdie_snapshots/created_page_with_reference_html.accepted
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
---
version: 1.1.5
version: 1.1.8
title: Created Page with Reference HTML
file: ./test/chrobot_test.gleam
test_name: create_page_test
Expand Down Expand Up @@ -87,6 +87,24 @@ test_name: create_page_test
Wobble
</div>

<span>🤖</span>

<div class="greeting">
Hello <span>Joe</span>
</div>

<ol>
<li>один</li>
<li>два</li>
<li>три</li>
</ol>

<ul>
<li>One</li>
<li>Two</li>
<li>Three</li>
</ul>

<div>
<label for="demo-checkbox">This is a demo checkbox to test page interactivity</label>
<input type="checkbox" id="demo-checkbox" name="demo-checkbox" value="demo-checkbox" checked="">
Expand Down
9 changes: 9 additions & 0 deletions birdie_snapshots/list_of_greetings.accepted
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
version: 1.1.8
title: List of greetings
file: ./test/chrobot_test.gleam
test_name: select_all_from_test
---
One
Two
Three
2 changes: 1 addition & 1 deletion gleam.toml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
name = "chrobot"
version = "2.2.5"
version = "2.3.0"

description = "A browser automation tool and interface to the Chrome DevTools Protocol."
licences = ["MIT"]
Expand Down
203 changes: 152 additions & 51 deletions src/chrobot.gleam
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import gleam/bit_array
import gleam/bool
import gleam/dynamic
import gleam/erlang/process.{type Subject}
import gleam/io
import gleam/json
import gleam/list
import gleam/option.{type Option, None, Some}
Expand Down Expand Up @@ -277,7 +278,10 @@ pub fn to_file(

/// Evaluate some JavaScript on the page and return the result,
/// which will be a [`runtime.RemoteObject`](/chrobot/protocol/runtime.html#RemoteObject) reference.
pub fn eval(on page: Page, js expression: String) {
pub fn eval(
on page: Page,
js expression: String,
) -> Result(runtime.RemoteObject, RequestError) {
runtime.evaluate(
page_caller(page),
expression: expression,
Expand All @@ -293,7 +297,10 @@ pub fn eval(on page: Page, js expression: String) {
|> handle_eval_response()
}

pub fn eval_to_value(on page: Page, js expression: String) {
pub fn eval_to_value(
on page: Page,
js expression: String,
) -> Result(runtime.RemoteObject, RequestError) {
runtime.evaluate(
page_caller(page),
expression: expression,
Expand Down Expand Up @@ -655,60 +662,55 @@ pub fn select(on page: Page, matching selector: String) {
|> handle_object_id_response()
}

/// Run [`Element.querySelector`](https://developer.mozilla.org/en-US/docs/Web/API/Element/querySelector) on the given
/// element and return a single [`runtime.RemoteObjectId`](/chrobot/protocol/runtime.html#RemoteObjectId)
/// for the first matching child element.
pub fn select_from(
on page: Page,
from item: runtime.RemoteObjectId,
matching selector: String,
) -> Result(runtime.RemoteObjectId, RequestError) {
let declaration =
"function select_from(selector)
{
return this.querySelector(selector)
}
"
call_custom_function_on_object(page_caller(page), declaration, item, [
StringArg(selector),
])
|> handle_object_id_response()
}

/// Run [`document.querySelectorAll`](https://developer.mozilla.org/en-US/docs/Web/API/Document/querySelectorAll) on the page and return a list of [`runtime.RemoteObjectId`](/chrobot/protocol/runtime.html#RemoteObjectId) items
/// for all matching elements.
pub fn select_all(on page: Page, matching selector: String) {
pub fn select_all(
on page: Page,
matching selector: String,
) -> Result(List(runtime.RemoteObjectId), RequestError) {
let selector_code = "window.document.querySelectorAll(\"" <> selector <> "\")"
let result = eval(page, selector_code)
case result {
Ok(runtime.RemoteObject(object_id: Some(remote_object_id), ..)) -> {
use result_properties <- result.try(runtime.get_properties(
page_caller(page),
remote_object_id,
own_properties: Some(True),
))
eval(page, selector_code)
|> handle_select_all_response(page)
}

case result_properties {
runtime.GetPropertiesResponse(
result: _,
internal_properties: _,
exception_details: Some(exception),
) -> {
Error(chrome.RuntimeException(
text: exception.text,
line: exception.line_number,
column: exception.column_number,
))
}
runtime.GetPropertiesResponse(
result: property_descriptors,
internal_properties: _internal_props,
exception_details: None,
) -> {
Ok(
list.filter_map(property_descriptors, fn(prop_descriptor) {
case prop_descriptor {
runtime.PropertyDescriptor(
value: Some(runtime.RemoteObject(
object_id: Some(object_id),
..,
)),
..,
) -> {
Ok(object_id)
}
_ -> Error(Nil)
}
}),
)
}
}
}
Ok(_) -> {
Ok([])
}
Error(any) -> Error(any)
/// Run [`Element.querySelectorAll`](https://developer.mozilla.org/en-US/docs/Web/API/Element/querySelectorAll) on the given
/// element and return a list of [`runtime.RemoteObjectId`](/chrobot/protocol/runtime.html#RemoteObjectId) items
/// for all matching child elements.
pub fn select_all_from(
on page: Page,
from item: runtime.RemoteObjectId,
matching selector: String,
) -> Result(List(runtime.RemoteObjectId), RequestError) {
let declaration =
"function select_all_from(selector)
{
return this.querySelectorAll(selector)
}
"
call_custom_function_on_object(page_caller(page), declaration, item, [
StringArg(selector),
])
|> handle_select_all_response(page)
}

/// Continously attempt to run a selector, until it succeeds.
Expand Down Expand Up @@ -901,6 +903,61 @@ fn handle_object_id_response(response) {
}
}

fn handle_select_all_response(
result: Result(runtime.RemoteObject, chrome.RequestError),
page: Page,
) -> Result(List(runtime.RemoteObjectId), RequestError) {
case result {
Ok(runtime.RemoteObject(object_id: Some(remote_object_id), ..)) -> {
use result_properties <- result.try(runtime.get_properties(
page_caller(page),
remote_object_id,
own_properties: Some(True),
))

case result_properties {
runtime.GetPropertiesResponse(
result: _,
internal_properties: _,
exception_details: Some(exception),
) -> {
Error(chrome.RuntimeException(
text: exception.text,
line: exception.line_number,
column: exception.column_number,
))
}
runtime.GetPropertiesResponse(
result: property_descriptors,
internal_properties: _internal_props,
exception_details: None,
) -> {
Ok(
list.filter_map(property_descriptors, fn(prop_descriptor) {
case prop_descriptor {
runtime.PropertyDescriptor(
value: Some(runtime.RemoteObject(
object_id: Some(object_id),
..,
)),
..,
) -> {
Ok(object_id)
}
_ -> Error(Nil)
}
}),
)
}
}
}
Ok(_) -> {
Ok([])
}
Error(any) -> Error(any)
}
}

/// Type wrapper to let you pass in custom arguments of different types
/// to a JavaScript function as a list of the same type
pub type CallArgument {
Expand Down Expand Up @@ -1032,3 +1089,47 @@ pub fn call_custom_function_on_raw(
_ -> Ok(decoded_response)
}
}

/// This is a version of `call_custom_function_on` which returns remote objects instead of values.
/// Useful when you want to pass the result to another function that expects a remote object.
pub fn call_custom_function_on_object(
callback,
function_declaration function_declaration: String,
object_id object_id: runtime.RemoteObjectId,
args arguments: List(CallArgument),
) {
// Make call
let encoded_arguments = encode_custom_arguments(arguments)
let payload =
Some(
json.object([
#("functionDeclaration", json.string(function_declaration)),
#("objectId", runtime.encode__remote_object_id(object_id)),
#("arguments", encoded_arguments),
#("returnByValue", json.bool(False)),
]),
)
// Parse response
use result <- result.try(callback("Runtime.callFunctionOn", payload))
use decoded_response <- result.try(
runtime.decode__call_function_on_response(result)
|> result.replace_error(chrome.ProtocolError),
)

// Ensure response contains an object reference
case decoded_response {
runtime.CallFunctionOnResponse(
result: _,
exception_details: Some(exception),
) -> {
Error(chrome.RuntimeException(
text: exception.text,
line: exception.line_number,
column: exception.column_number,
))
}
runtime.CallFunctionOnResponse(result: result, exception_details: None) -> {
Ok(result)
}
}
}
38 changes: 38 additions & 0 deletions test/chrobot_test.gleam
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,24 @@ pub fn select_test() {
|> should.equal("Wibble")
}

pub fn select_from_test() {
use page <- test_utils.with_reference_page()
let object_id =
chrobot.select(page, ".greeting")
|> should.be_ok

let inner_object_id =
chrobot.select_from(page, object_id, "span")
|> should.be_ok

let text_content =
chrobot.get_text(page, inner_object_id)
|> should.be_ok

text_content
|> should.equal("Joe")
}

pub fn get_html_test() {
use page <- test_utils.with_reference_page()
let object =
Expand Down Expand Up @@ -227,6 +245,26 @@ pub fn select_all_test() {
birdie.snap(string.join(hrefs, "\n"), title: "List of links")
}

pub fn select_all_from_test() {
use page <- test_utils.with_reference_page()
let object_id =
chrobot.select(page, "ul")
|> should.be_ok

let inner_object_ids =
chrobot.select_all_from(page, object_id, "li")
|> should.be_ok

let texts =
inner_object_ids
|> list.map(fn(inner_object_id) {
chrobot.get_text(page, inner_object_id)
|> should.be_ok
})

birdie.snap(string.join(texts, "\n"), title: "List of greetings")
}

pub fn get_property_test() {
use page <- test_utils.with_reference_page()
let object_id =
Expand Down
18 changes: 18 additions & 0 deletions test_assets/reference_website.html
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,24 @@ <h2>And now for something completely different</h2>
Wobble
</div>

<span>🤖</span>

<div class="greeting">
Hello <span>Joe</span>
</div>

<ol>
<li>один</li>
<li>два</li>
<li>три</li>
</ol>

<ul>
<li>One</li>
<li>Two</li>
<li>Three</li>
</ul>

<div>
<label for="demo-checkbox">This is a demo checkbox to test page interactivity</label>
<input type="checkbox" id="demo-checkbox" name="demo-checkbox" value="demo-checkbox" checked>
Expand Down

0 comments on commit 3c93ba8

Please sign in to comment.