-module(chrobot). -compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch]). -export([open/3, with_timeout/2, to_file/2, as_value/2, quit/1, defer_quit/2, page_caller/1, close/1, screenshot/1, pdf/1, down_key/3, up_key/3, press_key/2, insert_char/2, type_text/2, await_load_event/2, launch/0, launch_window/0, launch_with_config/1, launch_with_env/0, eval/2, eval_to_value/2, eval_async/2, get_all_html/1, select_all/2, select/2, call_custom_function_on/5, to_value/3, get_attribute/3, get_property/4, get_text/2, get_inner_html/2, get_outer_html/2, call_custom_function_on_raw/4, click/2, click_selector/2, focus/2, focus_selector/2, poll/2, await_selector/2, create_page/3]). -export_type([page/0, encoded_file/0, call_argument/0]). -type page() :: {page, gleam@erlang@process:subject(chrome:message()), integer(), protocol@target:target_id(), protocol@target:session_id()}. -type encoded_file() :: {encoded_file, binary(), binary()}. -type call_argument() :: {string_arg, binary()} | {int_arg, integer()} | {float_arg, float()} | {bool_arg, boolean()} | {array_arg, list(call_argument())}. -spec open(gleam@erlang@process:subject(chrome:message()), binary(), integer()) -> {ok, page()} | {error, chrome:request_error()}. open(Browser_subject, Url, Time_out) -> gleam@result:'try'( protocol@target:create_target( fun(Method, Params) -> chrome:call(Browser_subject, Method, Params, none, Time_out) end, Url, {some, 1920}, {some, 1080}, none, none ), fun(Target_response) -> gleam@result:'try'( protocol@target:attach_to_target( fun(Method@1, Params@1) -> chrome:call( Browser_subject, Method@1, Params@1, none, Time_out ) end, erlang:element(2, Target_response), {some, true} ), fun(Session_response) -> {ok, {page, Browser_subject, Time_out, erlang:element(2, Target_response), erlang:element(2, Session_response)}} end ) end ). -spec with_timeout(page(), integer()) -> page(). with_timeout(Page, Time_out) -> {page, erlang:element(2, Page), Time_out, erlang:element(4, Page), erlang:element(5, Page)}. -spec to_file(encoded_file(), binary()) -> {ok, nil} | {error, simplifile:file_error()}. to_file(Input, Path) -> Res = begin _pipe = gleam@bit_array:base64_decode(erlang:element(2, Input)), gleam@result:replace_error( _pipe, {unknown, <<"Could not decode base64 string"/utf8>>} ) end, gleam@result:'try'( Res, fun(Binary) -> simplifile_erl:write_bits( <<<>/binary, (erlang:element(3, Input))/binary>>, Binary ) end ). -spec as_value( {ok, protocol@runtime:remote_object()} | {error, chrome:request_error()}, fun((gleam@dynamic:dynamic_()) -> {ok, AJBZ} | {error, any()}) ) -> {ok, AJBZ} | {error, chrome:request_error()}. as_value(Result, Decoder) -> case Result of {ok, {remote_object, _, _, _, {some, Value}, _, _, _}} -> _pipe = Decoder(Value), gleam@result:replace_error(_pipe, protocol_error); {error, Something} -> {error, Something}; _ -> {error, not_found_error} end. -spec quit(gleam@erlang@process:subject(chrome:message())) -> {ok, nil} | {error, gleam@erlang@process:call_error(nil)}. quit(Browser) -> chrome:quit(Browser). -spec defer_quit( gleam@erlang@process:subject(chrome:message()), fun(() -> any()) ) -> {ok, nil} | {error, gleam@erlang@process:call_error(nil)}. defer_quit(Browser, Body) -> Body(), chrome:quit(Browser). -spec pass_session(protocol@target:session_id()) -> gleam@option:option(binary()). pass_session(Session_id) -> case Session_id of {session_id, Value} -> {some, Value} end. -spec page_caller(page()) -> fun((binary(), gleam@option:option(gleam@json:json())) -> {ok, gleam@dynamic:dynamic_()} | {error, chrome:request_error()}). page_caller(Page) -> fun(Method, Params) -> chrome:call( erlang:element(2, Page), Method, Params, pass_session(erlang:element(5, Page)), erlang:element(3, Page) ) end. -spec close(page()) -> {ok, gleam@dynamic:dynamic_()} | {error, chrome:request_error()}. close(Page) -> protocol@target:close_target(page_caller(Page), erlang:element(4, Page)). -spec screenshot(page()) -> {ok, encoded_file()} | {error, chrome:request_error()}. screenshot(Page) -> gleam@result:'try'( protocol@page:capture_screenshot( page_caller(Page), {some, capture_screenshot_format_png}, none, none ), fun(Response) -> {ok, {encoded_file, erlang:element(2, Response), <<"png"/utf8>>}} end ). -spec pdf(page()) -> {ok, encoded_file()} | {error, chrome:request_error()}. pdf(Page) -> gleam@result:'try'( protocol@page:print_to_pdf( page_caller(Page), {some, false}, {some, false}, none, none, none, none, none, none, none, none, none, none, none, none ), fun(Response) -> {ok, {encoded_file, erlang:element(2, Response), <<"pdf"/utf8>>}} end ). -spec down_key(page(), binary(), integer()) -> {ok, nil} | {error, chrome:request_error()}. down_key(Page, Key, Modifiers) -> Key_data_result = chrobot@internal@keymap:get_key_data(Key), case Key_data_result of {ok, Key_data} -> Text@1 = (case erlang:element(5, Key_data) of {some, Text} -> {some, Text}; none -> {some, Key} end), _pipe = protocol@input:dispatch_key_event( page_caller(Page), (case Text@1 of {some, _} -> dispatch_key_event_type_key_down; none -> dispatch_key_event_type_raw_key_down end), {some, Modifiers}, none, Text@1, Text@1, none, erlang:element(3, Key_data), erlang:element(4, Key_data), erlang:element(2, Key_data), none, none, (case erlang:element(6, Key_data) of {some, 3} -> {some, true}; _ -> none end), none, erlang:element(6, Key_data) ), gleam@result:replace(_pipe, nil); {error, nil} -> chrobot@internal@utils:warn( <<<<"You are attempting to trigger a key which is not supported by the chrobot virtual keyboard. The key to be pressed down is: '"/utf8, Key/binary>>/binary, "'. Chrobot simulates a US keyboard layout, it's best to stick to ASCII characters and DOM key names!"/utf8>> ), {error, not_found_error} end. -spec up_key(page(), binary(), integer()) -> {ok, nil} | {error, chrome:request_error()}. up_key(Page, Key, Modifiers) -> Key_data_result = chrobot@internal@keymap:get_key_data(Key), case Key_data_result of {ok, Key_data} -> _pipe = protocol@input:dispatch_key_event( page_caller(Page), dispatch_key_event_type_key_up, {some, Modifiers}, none, erlang:element(5, Key_data), erlang:element(5, Key_data), none, erlang:element(3, Key_data), erlang:element(4, Key_data), erlang:element(2, Key_data), none, none, (case erlang:element(6, Key_data) of {some, 3} -> {some, true}; _ -> none end), none, erlang:element(6, Key_data) ), gleam@result:replace(_pipe, nil); {error, nil} -> chrobot@internal@utils:warn( <<<<"You are attempting to trigger a key which is not supported by the chrobot virtual keyboard. The key to be released is: '"/utf8, Key/binary>>/binary, "'. Chrobot simulates a US keyboard layout, it's best to stick to ASCII characters and DOM key names!"/utf8>> ), {error, not_found_error} end. -spec press_key(page(), binary()) -> {ok, nil} | {error, chrome:request_error()}. press_key(Page, Key) -> gleam@result:'try'( down_key(Page, Key, 0), fun(_) -> up_key(Page, Key, 0) end ). -spec insert_char(page(), binary()) -> {ok, nil} | {error, chrome:request_error()}. insert_char(Page, Key) -> _pipe = protocol@input:dispatch_key_event( page_caller(Page), dispatch_key_event_type_char, none, none, {some, Key}, none, none, none, none, none, none, none, none, none, none ), gleam@result:replace(_pipe, nil). -spec type_text(page(), binary()) -> {ok, nil} | {error, chrome:request_error()}. type_text(Page, Input) -> _pipe = gleam@string:to_graphemes(Input), _pipe@1 = gleam@list:map( _pipe, fun(Char) -> case chrobot@internal@keymap:get_key_data(Char) of {ok, _} -> press_key(Page, Char); {error, _} -> insert_char(Page, Char) end end ), _pipe@2 = gleam@result:all(_pipe@1), gleam@result:replace(_pipe@2, nil). -spec await_load_event(gleam@erlang@process:subject(chrome:message()), page()) -> {ok, gleam@dynamic:dynamic_()} | {error, chrome:request_error()}. await_load_event(Browser, Page) -> gleam@result:'try'( protocol@page:enable(page_caller(Page)), fun(_) -> chrome:listen_once( Browser, <<"Page.loadEventFired"/utf8>>, erlang:element(3, Page) ) end ). -spec validate_launch( {ok, gleam@erlang@process:subject(chrome:message())} | {error, chrome:launch_error()} ) -> {ok, gleam@erlang@process:subject(chrome:message())} | {error, chrome:launch_error()}. validate_launch(Launch_result) -> gleam@result:'try'( Launch_result, fun(Instance) -> {Major, Minor} = protocol:version(), Target_protocol_version = <<<>/binary, Minor/binary>>, Version_response = begin _pipe = chrome:get_version(Instance), gleam@result:replace_error(_pipe, unresponsive_after_start) end, gleam@result:'try'( Version_response, fun(Actual_version) -> case Target_protocol_version =:= erlang:element( 2, Actual_version ) of true -> {ok, Instance}; false -> {error, {protocol_version_mismatch, Target_protocol_version, erlang:element(2, Actual_version)}} end end ) end ). -spec launch() -> {ok, gleam@erlang@process:subject(chrome:message())} | {error, chrome:launch_error()}. launch() -> Launch_result = validate_launch(chrome:launch()), case Launch_result of {error, could_not_find_executable} -> chrobot@internal@utils:err( <<"Chrobot could not find a chrome executable to launch!\n"/utf8>> ), chrobot@internal@utils:hint( <<"You can install a local version of chrome for testing with this command:"/utf8>> ), chrobot@internal@utils:show_cmd( <<"gleam run -m browser_install"/utf8>> ), Launch_result; Other -> Other end. -spec launch_window() -> {ok, gleam@erlang@process:subject(chrome:message())} | {error, chrome:launch_error()}. launch_window() -> Launch_result = validate_launch(chrome:launch_window()), case Launch_result of {error, could_not_find_executable} -> chrobot@internal@utils:err( <<"Chrobot could not find a chrome executable to launch!\n"/utf8>> ), chrobot@internal@utils:hint( <<"You can install a local version of chrome for testing with this command:"/utf8>> ), chrobot@internal@utils:show_cmd( <<"gleam run -m browser_install"/utf8>> ), Launch_result; Other -> Other end. -spec launch_with_config(chrome:browser_config()) -> {ok, gleam@erlang@process:subject(chrome:message())} | {error, chrome:launch_error()}. launch_with_config(Config) -> validate_launch(chrome:launch_with_config(Config)). -spec launch_with_env() -> {ok, gleam@erlang@process:subject(chrome:message())} | {error, chrome:launch_error()}. launch_with_env() -> validate_launch(chrome:launch_with_env()). -spec handle_eval_response( {ok, protocol@runtime:evaluate_response()} | {error, chrome:request_error()} ) -> {ok, protocol@runtime:remote_object()} | {error, chrome:request_error()}. handle_eval_response(Eval_response) -> case Eval_response of {ok, {evaluate_response, _, {some, Exception}}} -> {error, {runtime_exception, erlang:element(3, Exception), erlang:element(4, Exception), erlang:element(5, Exception)}}; {ok, {evaluate_response, Result_data, none}} -> {ok, Result_data}; {error, Other} -> {error, Other} end. -spec eval(page(), binary()) -> {ok, protocol@runtime:remote_object()} | {error, chrome:request_error()}. eval(Page, Expression) -> _pipe = protocol@runtime:evaluate( page_caller(Page), Expression, none, none, {some, false}, none, {some, false}, {some, true}, {some, false} ), handle_eval_response(_pipe). -spec eval_to_value(page(), binary()) -> {ok, protocol@runtime:remote_object()} | {error, chrome:request_error()}. eval_to_value(Page, Expression) -> _pipe = protocol@runtime:evaluate( page_caller(Page), Expression, none, none, {some, false}, none, {some, true}, {some, true}, {some, false} ), handle_eval_response(_pipe). -spec eval_async(page(), binary()) -> {ok, protocol@runtime:remote_object()} | {error, chrome:request_error()}. eval_async(Page, Expression) -> _pipe = protocol@runtime:evaluate( page_caller(Page), Expression, none, none, {some, false}, none, {some, false}, {some, true}, {some, true} ), handle_eval_response(_pipe). -spec get_all_html(page()) -> {ok, binary()} | {error, chrome:request_error()}. get_all_html(Page) -> _pipe = eval(Page, <<"window.document.documentElement.outerHTML"/utf8>>), as_value(_pipe, fun gleam@dynamic:string/1). -spec select_all(page(), binary()) -> {ok, list(protocol@runtime:remote_object_id())} | {error, chrome:request_error()}. select_all(Page, Selector) -> Selector_code = <<<<"window.document.querySelectorAll(\""/utf8, Selector/binary>>/binary, "\")"/utf8>>, Result = eval(Page, Selector_code), case Result of {ok, {remote_object, _, _, _, _, _, _, {some, Remote_object_id}}} -> gleam@result:'try'( protocol@runtime:get_properties( page_caller(Page), Remote_object_id, {some, true} ), fun(Result_properties) -> case Result_properties of {get_properties_response, _, _, {some, Exception}} -> {error, {runtime_exception, erlang:element(3, Exception), erlang:element(4, Exception), erlang:element(5, Exception)}}; {get_properties_response, Property_descriptors, _, none} -> {ok, gleam@list:filter_map( Property_descriptors, fun(Prop_descriptor) -> case Prop_descriptor of {property_descriptor, _, {some, {remote_object, _, _, _, _, _, _, {some, Object_id}}}, _, _, _, _, _, _, _, _} -> {ok, Object_id}; _ -> {error, nil} end end )} end end ); {ok, _} -> {ok, []}; {error, Any} -> {error, Any} end. -spec handle_object_id_response( {ok, protocol@runtime:remote_object()} | {error, chrome:request_error()} ) -> {ok, protocol@runtime:remote_object_id()} | {error, chrome:request_error()}. handle_object_id_response(Response) -> case Response of {ok, {remote_object, _, _, _, _, _, _, {some, Remote_object_id}}} -> {ok, Remote_object_id}; {ok, _} -> {error, not_found_error}; {error, Any} -> {error, Any} end. -spec select(page(), binary()) -> {ok, protocol@runtime:remote_object_id()} | {error, chrome:request_error()}. select(Page, Selector) -> Selector_code = <<<<"window.document.querySelector(\""/utf8, Selector/binary>>/binary, "\")"/utf8>>, _pipe = eval(Page, Selector_code), handle_object_id_response(_pipe). -spec encode_custom_arg(call_argument()) -> gleam@json:json(). encode_custom_arg(Arg) -> case Arg of {string_arg, Value} -> gleam@json:string(Value); {int_arg, Value@1} -> gleam@json:int(Value@1); {float_arg, Value@2} -> gleam@json:float(Value@2); {bool_arg, Value@3} -> gleam@json:bool(Value@3); {array_arg, Value@4} -> gleam@json:array(Value@4, fun encode_custom_arg/1) end. -spec encode_custom_arguments(list(call_argument())) -> gleam@json:json(). encode_custom_arguments(Input) -> gleam@json:array( Input, fun(Arg) -> gleam@json:object([{<<"value"/utf8>>, encode_custom_arg(Arg)}]) end ). -spec call_custom_function_on( fun((binary(), gleam@option:option(gleam@json:json())) -> {ok, gleam@dynamic:dynamic_()} | {error, chrome:request_error()}), binary(), protocol@runtime:remote_object_id(), list(call_argument()), fun((gleam@dynamic:dynamic_()) -> {ok, AJMI} | {error, any()}) ) -> {ok, AJMI} | {error, chrome:request_error()}. call_custom_function_on( Callback, Function_declaration, Object_id, Arguments, Value_decoder ) -> Encoded_arguments = encode_custom_arguments(Arguments), Payload = {some, gleam@json:object( [{<<"functionDeclaration"/utf8>>, gleam@json:string(Function_declaration)}, {<<"objectId"/utf8>>, protocol@runtime:encode__remote_object_id(Object_id)}, {<<"arguments"/utf8>>, Encoded_arguments}, {<<"returnByValue"/utf8>>, gleam@json:bool(true)}] )}, gleam@result:'try'( Callback(<<"Runtime.callFunctionOn"/utf8>>, Payload), fun(Result) -> gleam@result:'try'( begin _pipe = protocol@runtime:decode__call_function_on_response( Result ), gleam@result:replace_error(_pipe, protocol_error) end, fun(Decoded_response) -> case Decoded_response of {call_function_on_response, _, {some, Exception}} -> {error, {runtime_exception, erlang:element(3, Exception), erlang:element(4, Exception), erlang:element(5, Exception)}}; {call_function_on_response, {remote_object, _, _, _, {some, Value}, _, _, _}, none} -> _pipe@1 = Value_decoder(Value), gleam@result:replace_error(_pipe@1, protocol_error); _ -> {error, not_found_error} end end ) end ). -spec to_value( page(), protocol@runtime:remote_object_id(), fun((gleam@dynamic:dynamic_()) -> {ok, AJMJ} | {error, any()}) ) -> {ok, AJMJ} | {error, chrome:request_error()}. to_value(Page, Remote_object_id, Decoder) -> Declaration = <<"function to_value(){ return JSON.stringify(this) }"/utf8>>, call_custom_function_on( page_caller(Page), Declaration, Remote_object_id, [], Decoder ). -spec get_attribute(page(), protocol@runtime:remote_object_id(), binary()) -> {ok, binary()} | {error, chrome:request_error()}. get_attribute(Page, Item, Attribute_name) -> Declaration = <<"function get_arg(attribute_name) { return this.getAttribute(attribute_name) } "/utf8>>, call_custom_function_on( page_caller(Page), Declaration, Item, [{string_arg, Attribute_name}], fun gleam@dynamic:string/1 ). -spec get_property( page(), protocol@runtime:remote_object_id(), binary(), fun((gleam@dynamic:dynamic_()) -> {ok, AJMP} | {error, any()}) ) -> {ok, AJMP} | {error, chrome:request_error()}. get_property(Page, Item, Property_name, Property_decoder) -> Declaration = <<"function get_prop(property_name) { return this[property_name] } "/utf8>>, call_custom_function_on( page_caller(Page), Declaration, Item, [{string_arg, Property_name}], Property_decoder ). -spec get_text(page(), protocol@runtime:remote_object_id()) -> {ok, binary()} | {error, chrome:request_error()}. get_text(Page, Item) -> get_property(Page, Item, <<"innerText"/utf8>>, fun gleam@dynamic:string/1). -spec get_inner_html(page(), protocol@runtime:remote_object_id()) -> {ok, binary()} | {error, chrome:request_error()}. get_inner_html(Page, Item) -> get_property(Page, Item, <<"innerHTML"/utf8>>, fun gleam@dynamic:string/1). -spec get_outer_html(page(), protocol@runtime:remote_object_id()) -> {ok, binary()} | {error, chrome:request_error()}. get_outer_html(Page, Item) -> get_property(Page, Item, <<"outerHTML"/utf8>>, fun gleam@dynamic:string/1). -spec call_custom_function_on_raw( fun((binary(), gleam@option:option(gleam@json:json())) -> {ok, gleam@dynamic:dynamic_()} | {error, chrome:request_error()}), binary(), protocol@runtime:remote_object_id(), list(call_argument()) ) -> {ok, protocol@runtime:call_function_on_response()} | {error, chrome:request_error()}. call_custom_function_on_raw( Callback, Function_declaration, Object_id, Arguments ) -> Encoded_arguments = encode_custom_arguments(Arguments), Payload = {some, gleam@json:object( [{<<"functionDeclaration"/utf8>>, gleam@json:string(Function_declaration)}, {<<"objectId"/utf8>>, protocol@runtime:encode__remote_object_id(Object_id)}, {<<"arguments"/utf8>>, Encoded_arguments}, {<<"returnByValue"/utf8>>, gleam@json:bool(true)}] )}, gleam@result:'try'( Callback(<<"Runtime.callFunctionOn"/utf8>>, Payload), fun(Result) -> gleam@result:'try'( begin _pipe = protocol@runtime:decode__call_function_on_response( Result ), gleam@result:replace_error(_pipe, protocol_error) end, fun(Decoded_response) -> case Decoded_response of {call_function_on_response, _, {some, Exception}} -> {error, {runtime_exception, erlang:element(3, Exception), erlang:element(4, Exception), erlang:element(5, Exception)}}; _ -> {ok, Decoded_response} end end ) end ). -spec click(page(), protocol@runtime:remote_object_id()) -> {ok, nil} | {error, chrome:request_error()}. click(Page, Item) -> Declaration = <<"function click_el(){ return this.click() }"/utf8>>, _pipe = call_custom_function_on_raw( page_caller(Page), Declaration, Item, [] ), gleam@result:replace(_pipe, nil). -spec click_selector(page(), binary()) -> {ok, nil} | {error, chrome:request_error()}. click_selector(Page, Selector) -> gleam@result:'try'( select(Page, Selector), fun(Item) -> click(Page, Item) end ). -spec focus(page(), protocol@runtime:remote_object_id()) -> {ok, nil} | {error, chrome:request_error()}. focus(Page, Item) -> Declaration = <<"function focus_el(){ return this.focus() }"/utf8>>, _pipe = call_custom_function_on_raw( page_caller(Page), Declaration, Item, [] ), gleam@result:replace(_pipe, nil). -spec focus_selector(page(), binary()) -> {ok, nil} | {error, chrome:request_error()}. focus_selector(Page, Selector) -> gleam@result:'try'( select(Page, Selector), fun(Item) -> focus(Page, Item) end ). -spec do_poll( fun(() -> {ok, AIZG} | {error, chrome:request_error()}), integer(), gleam@option:option(chrome:request_error()) ) -> {ok, AIZG} | {error, chrome:request_error()}. do_poll(Callback, Deadline, Previous_error) -> Available_time = Deadline - chrobot_ffi:get_time_ms(), gleam@bool:guard( Available_time < 0, {error, chrome_agent_timeout}, fun() -> Result = begin _pipe = Callback, _pipe@1 = gleam@otp@task:async(_pipe), gleam@otp@task:try_await(_pipe@1, Available_time) end, Available_time@1 = (Deadline - chrobot_ffi:get_time_ms()) - 5, case Result of {error, timeout} -> case Previous_error of {some, Err} -> {error, Err}; none -> {error, chrome_agent_timeout} end; {error, {exit, _}} -> case Previous_error of {some, Err} -> {error, Err}; none -> {error, chrome_agent_timeout} end; {ok, {ok, Res}} -> {ok, Res}; {ok, {error, Err@1}} when Available_time@1 > 0 -> gleam_erlang_ffi:sleep(5), do_poll(Callback, Deadline, {some, Err@1}); {ok, {error, Err@2}} -> {error, Err@2} end end ). -spec poll(fun(() -> {ok, AIZB} | {error, chrome:request_error()}), integer()) -> {ok, AIZB} | {error, chrome:request_error()}. poll(Callback, Timeout) -> Deadline = chrobot_ffi:get_time_ms() + Timeout, do_poll(Callback, Deadline, none). -spec await_selector(page(), binary()) -> {ok, protocol@runtime:remote_object_id()} | {error, chrome:request_error()}. await_selector(Page, Selector) -> Polly = fun() -> _pipe = eval( Page, <<<<"window.document.querySelector(\""/utf8, Selector/binary>>/binary, "\")"/utf8>> ), handle_object_id_response(_pipe) end, poll(Polly, erlang:element(3, Page)). -spec create_page( gleam@erlang@process:subject(chrome:message()), binary(), integer() ) -> {ok, page()} | {error, chrome:request_error()}. create_page(Browser, Html, Time_out) -> gleam@result:'try'( open(Browser, <<"about:blank"/utf8>>, Time_out), fun(Created_page) -> gleam@result:'try'( await_selector(Created_page, <<"body"/utf8>>), fun(_) -> Payload = <<<<"window.document.open(); window.document.write(`"/utf8, Html/binary>>/binary, "`); window.document.close(); "/utf8>>, gleam@result:'try'( eval(Created_page, Payload), fun(_) -> {ok, Created_page} end ) end ) end ).