Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Element query selectors #14

Merged
merged 3 commits into from
Sep 7, 2024
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
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