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 +
+ +
    +
  1. один
  2. +
  3. два
  4. +
  5. три
  6. +
+ + +
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 +
+ +
    +
  1. один
  2. +
  3. два
  4. +
  5. три
  6. +
+ + +