diff --git a/CHANGELOG.md b/CHANGELOG.md
index a850750..7228a6e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -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
diff --git a/birdie_snapshots/created_page_with_reference_html.accepted b/birdie_snapshots/created_page_with_reference_html.accepted
index e9e9b2b..8164082 100644
--- a/birdie_snapshots/created_page_with_reference_html.accepted
+++ b/birdie_snapshots/created_page_with_reference_html.accepted
@@ -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
@@ -87,6 +87,24 @@ test_name: create_page_test
Wobble
+ 🤖
+
+
+ Hello Joe
+
+
+
+ - один
+ - два
+ - три
+
+
+
+
diff --git a/birdie_snapshots/list_of_greetings.accepted b/birdie_snapshots/list_of_greetings.accepted
new file mode 100644
index 0000000..6f5f8f2
--- /dev/null
+++ b/birdie_snapshots/list_of_greetings.accepted
@@ -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
\ No newline at end of file
diff --git a/gleam.toml b/gleam.toml
index 5a899ad..204de85 100644
--- a/gleam.toml
+++ b/gleam.toml
@@ -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"]
diff --git a/src/chrobot.gleam b/src/chrobot.gleam
index 09432db..5a6d9bd 100644
--- a/src/chrobot.gleam
+++ b/src/chrobot.gleam
@@ -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}
@@ -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,
@@ -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,
@@ -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.
@@ -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 {
@@ -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)
+ }
+ }
+}
diff --git a/test/chrobot_test.gleam b/test/chrobot_test.gleam
index 78e2a2b..46290cb 100644
--- a/test/chrobot_test.gleam
+++ b/test/chrobot_test.gleam
@@ -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 =
@@ -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 =
diff --git a/test_assets/reference_website.html b/test_assets/reference_website.html
index 0a0876a..9a704f2 100644
--- a/test_assets/reference_website.html
+++ b/test_assets/reference_website.html
@@ -91,6 +91,24 @@
And now for something completely different
Wobble
+ 🤖
+
+
+ Hello Joe
+
+
+
+ - один
+ - два
+ - три
+
+
+
+