├── .github └── workflows │ └── test.yml ├── .gitignore ├── .mise.toml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── assets ├── browser_protocol.json └── js_protocol.json ├── birdie_snapshots ├── created_page_with_reference_html.accepted ├── element_inner_html.accepted ├── element_outer_html.accepted ├── enocded_call_argument_with_dynamic_value.accepted ├── enum_decoder_function.accepted ├── enum_encoder_function.accepted ├── list_of_greetings.accepted ├── list_of_links.accepted ├── opened_sample_page.accepted ├── outer_html.accepted ├── runtime_evaluate_params.accepted └── runtime_evaluate_response.accepted ├── codegen.sh ├── doc_assets └── header_1.png ├── gleam.toml ├── manifest.toml ├── src ├── chrobot.gleam ├── chrobot │ ├── chrome.gleam │ ├── install.gleam │ ├── internal │ │ ├── keymap.gleam │ │ └── utils.gleam │ ├── protocol.gleam │ └── protocol │ │ ├── browser.gleam │ │ ├── debugger.gleam │ │ ├── dom.gleam │ │ ├── dom_debugger.gleam │ │ ├── emulation.gleam │ │ ├── fetch.gleam │ │ ├── input.gleam │ │ ├── io.gleam │ │ ├── log.gleam │ │ ├── network.gleam │ │ ├── page.gleam │ │ ├── performance.gleam │ │ ├── profiler.gleam │ │ ├── runtime.gleam │ │ ├── security.gleam │ │ └── target.gleam └── chrobot_ffi.erl ├── test ├── chrobot_test.gleam ├── chrome_test.gleam ├── codegen │ ├── download_protocol.gleam │ ├── generate_bindings.gleam │ └── generate_bindings_test.gleam ├── mock_server.gleam ├── protocol │ └── runtime_test.gleam └── test_utils.gleam ├── test_assets ├── reference_website.html └── runtime_evaluate_response.json └── vendor └── justin_fork ├── .gitignore ├── CHANGELOG.md ├── README.md ├── gleam.toml ├── manifest.toml ├── src └── justin_fork.gleam └── test └── justin_fork_test.gleam /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: test 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | 9 | jobs: 10 | test: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v4 14 | - uses: erlef/setup-beam@v1 15 | with: 16 | otp-version: "27" 17 | gleam-version: "1.6.2" 18 | rebar3-version: "3" 19 | # elixir-version: "1.15.4" 20 | - uses: browser-actions/setup-chrome@v1 21 | id: setup-chrome 22 | - run: | 23 | ${{ steps.setup-chrome.outputs.chrome-path }} --version 24 | - run: gleam deps download 25 | - run: gleam test 26 | env: 27 | CHROBOT_TEST_BROWSER_PATH: ${{ steps.setup-chrome.outputs.chrome-path }} 28 | - run: gleam format --check src test 29 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.beam 2 | *.ez 3 | /build 4 | erl_crash.dump 5 | /chrome 6 | .DS_Store 7 | test/playground_.gleam 8 | test/debugging_.gleam 9 | -------------------------------------------------------------------------------- /.mise.toml: -------------------------------------------------------------------------------- 1 | [tools] 2 | erlang = "27" 3 | gleam = "1.6.2" 4 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## [3.0.4] 2024-12-10 2 | 3 | - Move to `envoy` since `gleam_erlang` `os.get_env` was removed 4 | - Update to stdlib 0.47 5 | 6 | ## [3.0.3] 2024-11-26 7 | 8 | - Update to `gleam_stlidb` 0.44 9 | 10 | ## [3.0.2] 2024-10-20 11 | 12 | - Use `erlang/port` instead of `otp/port` since it is deprecated 13 | 14 | ## [3.0.1] 2024-10-15 15 | 16 | - Ensure compatibility with gleam 1.5 17 | - Update dependency ranges (mainly httpc and json) 18 | 19 | ## [3.0.0] 2024-08-07 20 | 21 | This update restructures the project to move all modules under the `chrobot` namespace. 22 | 23 | - All `protocol` modules are now under `chrobot/protocol` 24 | - `chrome` is now `chrobot/chrome` 25 | - `browser_install` is now `chrobot/install` 26 | 27 | ## [2.3.0] 2024-08-07 28 | 29 | - Add query selectors that run on elements (Remote Objects) 30 | 31 | ## [2.2.5] 2024-08-12 32 | 33 | - Upgrade to Gleam 1.4.1 34 | - Upgrade `simplifile` to 2.0.0 35 | 36 | ## [2.2.4] 2024-06-22 37 | 38 | - Implement more accurate polling 39 | - Make `poll` function part of the public `chrobot` API 40 | 41 | ## [2.2.3] 2024-06-08 42 | 43 | - Add `launch_window` function to launch browser in headful mode 44 | 45 | ## [2.2.2] 2024-06-07 46 | 47 | This update brings basic utilities for integration testing and some conveniences in the high level `chrobot` module 48 | 49 | - Add `click` and `focus` functions 50 | - Add `press_key`, `type_text`, and text input related functions 51 | 52 | ## [2.1.2] 2024-05-29 53 | 54 | - Improve message parsing performance A LOT 🚀 55 | - This should have a big impact on the speed of generating PDFs and taking screenshots 56 | 57 | ## [2.1.1] 2024-05-25 58 | 59 | - Rename the install module to browser_install 60 | 61 | ## [2.1.0] 2024-05-25 62 | 63 | - Allow setting launch config through environment 64 | - Make logging prettier 65 | - Add browser installation script 66 | 67 | ## [2.0.0] 2024-05-17 68 | 69 | - **Breaking Change:** Added `log_level` to `chrome.BrowserConfig`, this means any `launch_with_config` calls must 70 | be amended with this extra parameter 71 | 72 | - Adjusted browser logging behaviour 73 | 74 | ## [1.2.0] 2024-05-16 75 | 76 | - Move codegen scripts to `/test` to fix published package 77 | 78 | ## [1.1.0] 2024-05-16 79 | 80 | - Remove unused `glexec` dependency 81 | - Trying to pass a dynamic value to an enocder now logs a warning 82 | 83 | ## [1.0.0] 2024-05-16 84 | 85 | Initial release -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2024 Jonas Gruenwald 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 |

4 | 5 |

Chrobot

6 | 7 |

8 | ⛭ Typed browser automation for the BEAM ⛭ 9 |

10 |

11 | 12 | Package Version 13 | 14 | 15 | Hex Docs 16 | 17 | Target: Erlang 18 |

19 | 20 | ## About 21 | 22 | Chrobot provides a set of typed bindings to the stable version of the [Chrome Devtools Protocol](https://chromedevtools.github.io/devtools-protocol/), based on its published JSON specification. 23 | 24 | The typed interface is achieved by generating Gleam code for type definitions as well as encoder / decoder functions from the parsed JSON specification file. 25 | 26 | Chrobot also exposes some handy high level abstractions for browser automation, and handles managing a browser instance via an Erlang Port and communicating with it for you. 27 | 28 | You could use it for 29 | 30 | * Generating PDFs from HTML 31 | * Web scraping 32 | * Web archiving 33 | * Browser integration tests 34 | 35 | > 🦝 The generated protocol bindings are largely untested and I would consider this package experimental, use at your own peril! 36 | 37 | ## Setup 38 | 39 | ### Package 40 | 41 | Install as a Gleam package 42 | 43 | ```sh 44 | gleam add chrobot 45 | ``` 46 | 47 | Install as an Elixir dependency with mix 48 | 49 | ```elixir 50 | # in your mix.exs 51 | defp deps do 52 | [ 53 | {:chrobot, "~> 3.0.0", app: false, manager: :rebar3} 54 | ] 55 | end 56 | ``` 57 | 58 | ### Browser 59 | 60 | #### System Installation 61 | 62 | Chrobot can use an existing system installation of Google Chrome or Chromium, if you already have one. 63 | 64 | 65 | #### Browser Install Tool 66 | 67 | Chrobot comes with a simple utility to install a version of [Google Chrome for Testing](https://github.com/GoogleChromeLabs/chrome-for-testing) directly inside your project. 68 | Chrobot will automatically pick up this local installation when started via the `launch` command, and will prioritise it over a system installation of Google Chrome. 69 | 70 | You can run the browser installer tool from gleam like so: 71 | 72 | ```sh 73 | gleam run -m chrobot/install 74 | ``` 75 | 76 | Or when using Elixir with Mix: 77 | 78 | ```sh 79 | mix run -e :chrobot@install.main 80 | ``` 81 | 82 | Please [check the `install` docs for more information](https://hexdocs.pm/chrobot/chrobot/install.html) – this installation method will not work everywhere and comes with some caveats! 83 | 84 | #### GitHub Actions 85 | 86 | If you want to use chrobot inside a Github Action, for example to run integration tests, 87 | you can use the [setup-chrome](https://github.com/browser-actions/setup-chrome) action to get a Chrome installation, like so: 88 | 89 | ```yml 90 | # -- snip -- 91 | - uses: browser-actions/setup-chrome@v1 92 | id: setup-chrome 93 | - run: gleam deps download 94 | - run: gleam test 95 | env: 96 | CHROBOT_BROWSER_PATH: ${{ steps.setup-chrome.outputs.chrome-path }} 97 | ``` 98 | 99 | If you are using `launch` to start chrobot, it should pick up the Chrome executable from `CHROBOT_BROWSER_PATH`. 100 | 101 | ## Examples 102 | 103 | ### Take a screenshot of a website 104 | 105 | ```gleam 106 | import chrobot 107 | 108 | pub fn main() { 109 | // Open the browser and navigate to the gleam homepage 110 | let assert Ok(browser) = chrobot.launch() 111 | let assert Ok(page) = 112 | browser 113 | |> chrobot.open("https://gleam.run", 30_000) 114 | let assert Ok(_) = chrobot.await_selector(page, "body") 115 | 116 | // Take a screenshot and save it as 'hi_lucy.png' 117 | let assert Ok(screenshot) = chrobot.screenshot(page) 118 | let assert Ok(_) = chrobot.to_file(screenshot, "hi_lucy") 119 | let assert Ok(_) = chrobot.quit(browser) 120 | } 121 | ``` 122 | 123 | ### Generate a PDF document with [lustre](http://lustre.build/) 124 | 125 | ```gleam 126 | import chrobot 127 | import lustre/element.{text} 128 | import lustre/element/html 129 | 130 | fn build_page() { 131 | html.body([], [ 132 | html.h1([], [text("Spanakorizo")]), 133 | html.h2([], [text("Ingredients")]), 134 | html.ul([], [ 135 | html.li([], [text("1 onion")]), 136 | html.li([], [text("1 clove(s) of garlic")]), 137 | html.li([], [text("70 g olive oil")]), 138 | html.li([], [text("salt")]), 139 | html.li([], [text("pepper")]), 140 | html.li([], [text("2 spring onions")]), 141 | html.li([], [text("1/2 bunch dill")]), 142 | html.li([], [text("250 g round grain rice")]), 143 | html.li([], [text("150 g white wine")]), 144 | html.li([], [text("1 liter vegetable stock")]), 145 | html.li([], [text("1 kilo spinach")]), 146 | html.li([], [text("lemon zest, of 2 lemons")]), 147 | html.li([], [text("lemon juice, of 2 lemons")]), 148 | ]), 149 | html.h2([], [text("To serve")]), 150 | html.ul([], [ 151 | html.li([], [text("1 lemon")]), 152 | html.li([], [text("feta cheese")]), 153 | html.li([], [text("olive oil")]), 154 | html.li([], [text("pepper")]), 155 | html.li([], [text("oregano")]), 156 | ]), 157 | ]) 158 | |> element.to_document_string() 159 | } 160 | 161 | pub fn main() { 162 | let assert Ok(browser) = chrobot.launch() 163 | let assert Ok(page) = 164 | browser 165 | |> chrobot.create_page(build_page(), 10_000) 166 | 167 | // Store as 'recipe.pdf' 168 | let assert Ok(doc) = chrobot.pdf(page) 169 | let assert Ok(_) = chrobot.to_file(doc, "recipe") 170 | let assert Ok(_) = chrobot.quit(browser) 171 | } 172 | ``` 173 | 174 | ### Scrape a Website 175 | 176 | > 🍄‍🟫 **Just a quick reminder:** 177 | > Please be mindful of the load you are putting on other people's web services when you are scraping them programmatically! 178 | 179 | 180 | ```gleam 181 | import chrobot 182 | import gleam/io 183 | import gleam/list 184 | import gleam/result 185 | 186 | pub fn main() { 187 | let assert Ok(browser) = chrobot.launch() 188 | let assert Ok(page) = 189 | browser 190 | |> chrobot.open("https://books.toscrape.com/", 30_000) 191 | 192 | let assert Ok(_) = chrobot.await_selector(page, "body") 193 | let assert Ok(page_items) = chrobot.select_all(page, ".product_pod h3 a") 194 | let assert Ok(title_results) = 195 | list.map(page_items, fn(i) { chrobot.get_attribute(page, i, "title") }) 196 | |> result.all() 197 | io.debug(title_results) 198 | let assert Ok(_) = chrobot.quit(browser) 199 | } 200 | 201 | ``` 202 | 203 | ### Write an Integration Test for a WebApp 204 | 205 | ```gleam 206 | import chrobot 207 | import gleam/dynamic 208 | import gleeunit/should 209 | 210 | pub fn package_search_test() { 211 | let assert Ok(browser) = chrobot.launch() 212 | use <- chrobot.defer_quit(browser) 213 | let assert Ok(page) = chrobot.open(browser, "https://hexdocs.pm/", 10_000) 214 | let assert Ok(input_field) = chrobot.await_selector(page, "input#search") 215 | let assert Ok(Nil) = chrobot.focus(page, input_field) 216 | let assert Ok(Nil) = chrobot.type_text(page, "chrobot") 217 | let assert Ok(Nil) = chrobot.press_key(page, "Enter") 218 | let assert Ok(result_link) = chrobot.await_selector(page, "#search-results a") 219 | let assert Ok(package_href) = 220 | chrobot.get_property(page, result_link, "href", dynamic.string) 221 | package_href 222 | |> should.equal("https://hexdocs.pm/chrobot/") 223 | } 224 | ``` 225 | 226 | ### Use from Elixir 227 | 228 | ```elixir 229 | # ( output / logging removed for brevity ) 230 | iex(1)> {:ok, browser} = :chrobot.launch() 231 | iex(2)> {:ok, page} = :chrobot.open(browser, "https://example.com", 10_000) 232 | iex(3)> {:ok, object} = :chrobot.select(page, "h1") 233 | iex(4)> {:ok,text} = :chrobot.get_text(page, object) 234 | iex(5)> text 235 | "Example Domain" 236 | ``` 237 | 238 | 239 | ## Documentation & Guide 240 | 241 | The full documentation can be found at . 242 | 243 | 🗼 To learn about the high level abstractions, look at the [`chrobot` module documentation](https://hexdocs.pm/chrobot/chrobot.html). 244 | 245 | 📠 To learn how to use the protocol bindings directly, look at the [`protocol` module documentation](https://hexdocs.pm/chrobot/chrobot/protocol.html). 246 | 247 | -------------------------------------------------------------------------------- /birdie_snapshots/created_page_with_reference_html.accepted: -------------------------------------------------------------------------------- 1 | --- 2 | version: 1.1.8 3 | title: Created Page with Reference HTML 4 | file: ./test/chrobot_test.gleam 5 | test_name: create_page_test 6 | --- 7 | 8 | 9 | 10 | 11 | 12 |
13 | The World Wide Web project 14 | 15 | 16 |
17 | 18 |

World Wide Web

The WorldWideWeb (W3) is a wide-area 19 | hypermedia information retrieval 20 | initiative aiming to give universal 21 | access to a large universe of documents.

22 | Everything there is online about 23 | W3 is linked directly or indirectly 24 | to this document, including an executive 25 | summary of the project, Mailing lists 26 | , Policy , November's W3 news , 27 | Frequently Asked Questions . 28 |

29 |
30 |
What's out there? 31 |
32 |
Pointers to the 33 | world's online information, 34 | subjects 35 | , W3 servers, etc. 36 |
37 |
Help 38 |
39 |
on the browser you are using 40 |
41 |
Software Products 42 |
43 |
A list of W3 project 44 | components and their current state. 45 | (e.g. Line Mode ,X11 Viola , NeXTStep 46 | , Servers , Tools , Mail robot , 47 | Library ) 48 |
49 |
Technical 50 |
51 |
Details of protocols, formats, 52 | program internals etc 53 |
54 |
Bibliography 55 |
56 |
Paper documentation 57 | on W3 and references. 58 |
59 |
People 60 |
61 |
A list of some people involved 62 | in the project. 63 |
64 |
History 65 |
66 |
A summary of the history 67 | of the project. 68 |
69 |
How can I help ? 70 |
71 |
If you would like 72 | to support the web.. 73 |
74 |
Getting code 75 |
76 |
Getting the code by 77 | anonymous FTP , etc. 78 |
79 |
80 | 81 |
82 |

And now for something completely different

83 |
84 | Wibble 85 |
86 |
87 | Wobble 88 |
89 | 90 | 🤖 91 | 92 |
93 | Hello Joe 94 |
95 | 96 |
    97 |
  1. один
  2. 98 |
  3. два
  4. 99 |
  5. три
  6. 100 |
101 | 102 | 107 | 108 |
109 | 110 | 111 |
112 | 113 |
114 | 115 | 116 |
117 |
118 | 119 | 130 | 131 | 132 | -------------------------------------------------------------------------------- /birdie_snapshots/element_inner_html.accepted: -------------------------------------------------------------------------------- 1 | --- 2 | version: 1.1.4 3 | title: Element Inner HTML 4 | file: ./test/chrobot_test.gleam 5 | test_name: get_html_test 6 | --- 7 | 8 | The World Wide Web project 9 | 10 | 11 | -------------------------------------------------------------------------------- /birdie_snapshots/element_outer_html.accepted: -------------------------------------------------------------------------------- 1 | --- 2 | version: 1.1.4 3 | title: Element Outer HTML 4 | file: ./test/chrobot_test.gleam 5 | test_name: get_html_test 6 | --- 7 |
8 | The World Wide Web project 9 | 10 | 11 |
-------------------------------------------------------------------------------- /birdie_snapshots/enocded_call_argument_with_dynamic_value.accepted: -------------------------------------------------------------------------------- 1 | --- 2 | version: 1.1.4 3 | title: Enocded CallArgument with dynamic value 4 | file: ./test/protocol/runtime_test.gleam 5 | test_name: enocde_dynamic_test 6 | --- 7 | {"objectId":"1","value":null} -------------------------------------------------------------------------------- /birdie_snapshots/enum_decoder_function.accepted: -------------------------------------------------------------------------------- 1 | --- 2 | version: 1.1.4 3 | title: Enum decoder function 4 | file: ./test/internal/generate_bindings_test.gleam 5 | test_name: gen_enum_encoder_decoder_test 6 | --- 7 | @internal 8 | pub fn decode__certificate_transparency_compliance( 9 | value__: dynamic.Dynamic 10 | ) { 11 | case dynamic.string(value__){ 12 | Ok("unknown") -> Ok(CertificateTransparencyComplianceUnknown) 13 | Ok("not-compliant") -> Ok(CertificateTransparencyComplianceNotCompliant) 14 | Ok("compliant") -> Ok(CertificateTransparencyComplianceCompliant) 15 | Error(error) -> Error(error) 16 | Ok(other) -> Error([dynamic.DecodeError(expected: "valid enum property", found:other, path: ["enum decoder"])])} 17 | } 18 | -------------------------------------------------------------------------------- /birdie_snapshots/enum_encoder_function.accepted: -------------------------------------------------------------------------------- 1 | --- 2 | version: 1.1.4 3 | title: Enum encoder function 4 | file: ./test/internal/generate_bindings_test.gleam 5 | test_name: gen_enum_encoder_decoder_test 6 | --- 7 | @internal 8 | pub fn encode__certificate_transparency_compliance( 9 | value__: CertificateTransparencyCompliance 10 | ) { 11 | case value__{ 12 | CertificateTransparencyComplianceUnknown -> "unknown" 13 | CertificateTransparencyComplianceNotCompliant -> "not-compliant" 14 | CertificateTransparencyComplianceCompliant -> "compliant" 15 | } 16 | |> json.string() 17 | } 18 | -------------------------------------------------------------------------------- /birdie_snapshots/list_of_greetings.accepted: -------------------------------------------------------------------------------- 1 | --- 2 | version: 1.1.8 3 | title: List of greetings 4 | file: ./test/chrobot_test.gleam 5 | test_name: select_all_from_test 6 | --- 7 | One 8 | Two 9 | Three -------------------------------------------------------------------------------- /birdie_snapshots/list_of_links.accepted: -------------------------------------------------------------------------------- 1 | --- 2 | version: 1.1.4 3 | title: List of links 4 | file: ./test/chrobot_test.gleam 5 | test_name: select_all_test 6 | --- 7 | http://info.cern.ch/hypertext/WWW/WhatIs.html 8 | http://info.cern.ch/hypertext/WWW/Summary.html 9 | http://info.cern.ch/hypertext/WWW/Administration/Mailing/Overview.html 10 | http://info.cern.ch/hypertext/WWW/Policy.html 11 | http://info.cern.ch/hypertext/WWW/News/9211.html 12 | http://info.cern.ch/hypertext/WWW/FAQ/List.html 13 | http://info.cern.ch/hypertext/DataSources/Top.html 14 | http://info.cern.ch/hypertext/DataSources/bySubject/Overview.html 15 | http://info.cern.ch/hypertext/DataSources/WWW/Servers.html 16 | http://info.cern.ch/hypertext/WWW/Help.html 17 | http://info.cern.ch/hypertext/WWW/Status.html 18 | http://info.cern.ch/hypertext/WWW/LineMode/Browser.html 19 | http://info.cern.ch/hypertext/WWW/Status.html#35 20 | http://info.cern.ch/hypertext/WWW/NeXT/WorldWideWeb.html 21 | http://info.cern.ch/hypertext/WWW/Daemon/Overview.html 22 | http://info.cern.ch/hypertext/WWW/Tools/Overview.html 23 | http://info.cern.ch/hypertext/WWW/MailRobot/Overview.html 24 | http://info.cern.ch/hypertext/WWW/Status.html#57 25 | http://info.cern.ch/hypertext/WWW/Technical.html 26 | http://info.cern.ch/hypertext/WWW/Bibliography.html 27 | http://info.cern.ch/hypertext/WWW/People.html 28 | http://info.cern.ch/hypertext/WWW/History.html 29 | http://info.cern.ch/hypertext/WWW/Helping.html 30 | http://info.cern.ch/hypertext/README.html 31 | http://info.cern.ch/hypertext/WWW/LineMode/Defaults/Distribution.html -------------------------------------------------------------------------------- /birdie_snapshots/opened_sample_page.accepted: -------------------------------------------------------------------------------- 1 | --- 2 | version: 1.1.5 3 | title: Opened Sample Page 4 | file: ./test/chrobot_test.gleam 5 | test_name: open_test 6 | --- 7 | 8 | Chrobot Test Page 9 | 10 | 11 |

Chrobot Test Page

12 |
wobble
13 | 14 | 15 | -------------------------------------------------------------------------------- /birdie_snapshots/outer_html.accepted: -------------------------------------------------------------------------------- 1 | --- 2 | version: 1.1.4 3 | title: Outer HTML 4 | file: ./test/chrobot_test.gleam 5 | test_name: get_outer_html_test 6 | --- 7 | 8 |

9 | I am HTML 10 |

11 |

12 | I am the hyperstructure 13 |

14 |

15 | I am linked to you 16 |

17 | -------------------------------------------------------------------------------- /birdie_snapshots/runtime_evaluate_params.accepted: -------------------------------------------------------------------------------- 1 | --- 2 | version: 1.1.4 3 | title: Runtime.evaluate params 4 | file: ./test/protocol/runtime_test.gleam 5 | test_name: evaluate_test 6 | --- 7 | {"awaitPromise":false,"userGesture":true,"returnByValue":true,"silent":false,"expression":"document.querySelector(\"h1\")"} -------------------------------------------------------------------------------- /birdie_snapshots/runtime_evaluate_response.accepted: -------------------------------------------------------------------------------- 1 | --- 2 | version: 1.1.4 3 | title: Runtime.evaluate response 4 | file: ./test/protocol/runtime_test.gleam 5 | test_name: evaluate_test 6 | --- 7 | Ok(EvaluateResponse(RemoteObject(RemoteObjectTypeObject, Some(RemoteObjectSubtypeNode), Some("HTMLHeadingElement"), None, None, Some("h1"), Some(RemoteObjectId("8282282834669415287.1.3079"))), None)) -------------------------------------------------------------------------------- /codegen.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # rm -r src/protocol 3 | set -e 4 | gleam run -m codegen/generate_bindings 5 | gleam format 6 | gleam check 7 | echo "Done & Dusted! 🧹" -------------------------------------------------------------------------------- /doc_assets/header_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JonasGruenwald/chrobot/d5399537f64da8cfbfe85eb39090f0bf4ee0ac34/doc_assets/header_1.png -------------------------------------------------------------------------------- /gleam.toml: -------------------------------------------------------------------------------- 1 | name = "chrobot" 2 | version = "3.0.4" 3 | 4 | description = "A browser automation tool and interface to the Chrome DevTools Protocol." 5 | licences = ["MIT"] 6 | repository = { type = "github", user = "JonasGruenwald", repo = "chrobot" } 7 | links = [] 8 | 9 | [documentation] 10 | pages = [ 11 | { title = "Changelog", path = "changelog.html", source = "./CHANGELOG.md" }, 12 | ] 13 | 14 | [dependencies] 15 | gleam_stdlib = ">= 0.47.0 and < 2.0.0" 16 | gleam_json = ">= 1.0.0 and < 3.0.0" 17 | gleam_erlang = ">= 0.25.0 and < 1.0.0" 18 | gleam_otp = ">= 0.10.0 and < 1.0.0" 19 | gleam_httpc = ">= 2.2.0 and < 4.0.0" 20 | gleam_http = ">= 3.6.0 and < 4.0.0" 21 | filepath = ">= 1.0.0 and < 2.0.0" 22 | simplifile = ">= 2.0.1 and < 3.0.0" 23 | gleam_community_ansi = ">= 1.4.0 and < 2.0.0" 24 | spinner = ">= 1.1.0 and < 2.0.0" 25 | envoy = ">= 1.0.2 and < 2.0.0" 26 | 27 | [dev-dependencies] 28 | gleeunit = ">= 1.0.0 and < 2.0.0" 29 | justin_fork = { path = "./vendor/justin_fork" } 30 | birdie = ">= 1.1.8 and < 2.0.0" 31 | mist = ">= 1.2.0 and < 4.0.0" 32 | gleam_regexp = ">= 1.0.0 and < 2.0.0" 33 | -------------------------------------------------------------------------------- /manifest.toml: -------------------------------------------------------------------------------- 1 | # This file was generated by Gleam 2 | # You typically do not need to edit this file 3 | 4 | packages = [ 5 | { name = "argv", version = "1.0.2", build_tools = ["gleam"], requirements = [], otp_app = "argv", source = "hex", outer_checksum = "BA1FF0929525DEBA1CE67256E5ADF77A7CDDFE729E3E3F57A5BDCAA031DED09D" }, 6 | { name = "birdie", version = "1.2.4", build_tools = ["gleam"], requirements = ["argv", "edit_distance", "filepath", "glance", "gleam_community_ansi", "gleam_erlang", "gleam_stdlib", "justin", "rank", "simplifile", "trie_again"], otp_app = "birdie", source = "hex", outer_checksum = "769AE13AB5B5B84E724E9966037DCCB5BD63B2F43C52EF80B4BF3351F64E469E" }, 7 | { name = "birl", version = "1.7.1", build_tools = ["gleam"], requirements = ["gleam_stdlib", "ranger"], otp_app = "birl", source = "hex", outer_checksum = "5C66647D62BCB11FE327E7A6024907C4A17954EF22865FE0940B54A852446D01" }, 8 | { name = "edit_distance", version = "2.0.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "edit_distance", source = "hex", outer_checksum = "A1E485C69A70210223E46E63985FA1008B8B2DDA9848B7897469171B29020C05" }, 9 | { name = "envoy", version = "1.0.2", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "envoy", source = "hex", outer_checksum = "95FD059345AA982E89A0B6E2A3BF1CF43E17A7048DCD85B5B65D3B9E4E39D359" }, 10 | { name = "filepath", version = "1.1.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "filepath", source = "hex", outer_checksum = "67A6D15FB39EEB69DD31F8C145BB5A421790581BD6AA14B33D64D5A55DBD6587" }, 11 | { name = "glance", version = "1.1.0", build_tools = ["gleam"], requirements = ["gleam_stdlib", "glexer"], otp_app = "glance", source = "hex", outer_checksum = "E155BA1A787FD11827048355021C0390D2FE9A518485526F631A9D472858CC6D" }, 12 | { name = "gleam_community_ansi", version = "1.4.1", build_tools = ["gleam"], requirements = ["gleam_community_colour", "gleam_stdlib"], otp_app = "gleam_community_ansi", source = "hex", outer_checksum = "4CD513FC62523053E62ED7BAC2F36136EC17D6A8942728250A9A00A15E340E4B" }, 13 | { name = "gleam_community_colour", version = "1.4.1", build_tools = ["gleam"], requirements = ["gleam_json", "gleam_stdlib"], otp_app = "gleam_community_colour", source = "hex", outer_checksum = "386CB9B01B33371538672EEA8A6375A0A0ADEF41F17C86DDCB81C92AD00DA610" }, 14 | { name = "gleam_crypto", version = "1.4.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_crypto", source = "hex", outer_checksum = "8AE56026B3E05EBB1F076778478A762E9EB62B31AEEB4285755452F397029D22" }, 15 | { name = "gleam_erlang", version = "0.33.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_erlang", source = "hex", outer_checksum = "A1D26B80F01901B59AABEE3475DD4C18D27D58FA5C897D922FCB9B099749C064" }, 16 | { name = "gleam_http", version = "3.7.2", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_http", source = "hex", outer_checksum = "8A70D2F70BB7CFEB5DF048A2183FFBA91AF6D4CF5798504841744A16999E33D2" }, 17 | { name = "gleam_httpc", version = "3.0.0", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_http", "gleam_stdlib"], otp_app = "gleam_httpc", source = "hex", outer_checksum = "091CDD2BEC8092E82707BEA03FB5205A2BBBDE4A2F551E3C069E13B8BC0C428E" }, 18 | { name = "gleam_json", version = "2.1.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_json", source = "hex", outer_checksum = "0A57FB5666E695FD2BEE74C0428A98B0FC11A395D2C7B4CDF5E22C5DD32C74C6" }, 19 | { name = "gleam_otp", version = "0.16.0", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_stdlib"], otp_app = "gleam_otp", source = "hex", outer_checksum = "FA0EB761339749B4E82D63016C6A18C4E6662DA05BAB6F1346F9AF2E679E301A" }, 20 | { name = "gleam_regexp", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_regexp", source = "hex", outer_checksum = "A3655FDD288571E90EE9C4009B719FEF59FA16AFCDF3952A76A125AF23CF1592" }, 21 | { name = "gleam_stdlib", version = "0.47.0", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "3B22D46743C46498C8355365243327AC731ECD3959216344FA9CF9AD348620AC" }, 22 | { name = "glearray", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "glearray", source = "hex", outer_checksum = "B99767A9BC63EF9CC8809F66C7276042E5EFEACAA5B25188B552D3691B91AC6D" }, 23 | { name = "gleeunit", version = "1.2.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleeunit", source = "hex", outer_checksum = "F7A7228925D3EE7D0813C922E062BFD6D7E9310F0BEE585D3A42F3307E3CFD13" }, 24 | { name = "glexer", version = "2.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "glexer", source = "hex", outer_checksum = "25E87F25706749E40C3CDC72D2E52AEA12260B23D14FD9E09A1B524EF393485E" }, 25 | { name = "glisten", version = "6.0.0", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_otp", "gleam_stdlib", "logging", "telemetry"], otp_app = "glisten", source = "hex", outer_checksum = "912132751031473CB38F454120124FFC96AF6B0EA33D92C9C90DB16327A2A972" }, 26 | { name = "gramps", version = "2.0.3", build_tools = ["gleam"], requirements = ["gleam_crypto", "gleam_erlang", "gleam_http", "gleam_stdlib"], otp_app = "gramps", source = "hex", outer_checksum = "3CCAA6E081225180D95C79679D383BBF51C8D1FDC1B84DA1DA444F628C373793" }, 27 | { name = "hpack_erl", version = "0.3.0", build_tools = ["rebar3"], requirements = [], otp_app = "hpack", source = "hex", outer_checksum = "D6137D7079169D8C485C6962DFE261AF5B9EF60FBC557344511C1E65E3D95FB0" }, 28 | { name = "justin", version = "1.0.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "justin", source = "hex", outer_checksum = "7FA0C6DB78640C6DC5FBFD59BF3456009F3F8B485BF6825E97E1EB44E9A1E2CD" }, 29 | { name = "justin_fork", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], source = "local", path = "vendor/justin_fork" }, 30 | { name = "logging", version = "1.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "logging", source = "hex", outer_checksum = "1098FBF10B54B44C2C7FDF0B01C1253CAFACDACABEFB4B0D027803246753E06D" }, 31 | { name = "mist", version = "3.0.0", build_tools = ["gleam"], requirements = ["birl", "gleam_erlang", "gleam_http", "gleam_otp", "gleam_stdlib", "glisten", "gramps", "hpack_erl", "logging"], otp_app = "mist", source = "hex", outer_checksum = "CDA1A74E768419235E16886463EC4722EFF4AB3F8D820A76EAD45D7C167D7282" }, 32 | { name = "ranger", version = "1.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "ranger", source = "hex", outer_checksum = "B8F3AFF23A3A5B5D9526B8D18E7C43A7DFD3902B151B97EC65397FE29192B695" }, 33 | { name = "rank", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "rank", source = "hex", outer_checksum = "5660E361F0E49CBB714CC57CC4C89C63415D8986F05B2DA0C719D5642FAD91C9" }, 34 | { name = "repeatedly", version = "2.1.2", build_tools = ["gleam"], requirements = [], otp_app = "repeatedly", source = "hex", outer_checksum = "93AE1938DDE0DC0F7034F32C1BF0D4E89ACEBA82198A1FE21F604E849DA5F589" }, 35 | { name = "simplifile", version = "2.2.0", build_tools = ["gleam"], requirements = ["filepath", "gleam_stdlib"], otp_app = "simplifile", source = "hex", outer_checksum = "0DFABEF7DC7A9E2FF4BB27B108034E60C81BEBFCB7AB816B9E7E18ED4503ACD8" }, 36 | { name = "spinner", version = "1.3.0", build_tools = ["gleam"], requirements = ["gleam_community_ansi", "gleam_stdlib", "glearray", "repeatedly"], otp_app = "spinner", source = "hex", outer_checksum = "B824C4CFDA6AC912D14365BF365F2A52C4DA63EF2D768D2A1C46D9BF7AF669E7" }, 37 | { name = "telemetry", version = "1.3.0", build_tools = ["rebar3"], requirements = [], otp_app = "telemetry", source = "hex", outer_checksum = "7015FC8919DBE63764F4B4B87A95B7C0996BD539E0D499BE6EC9D7F3875B79E6" }, 38 | { name = "trie_again", version = "1.1.2", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "trie_again", source = "hex", outer_checksum = "5B19176F52B1BD98831B57FDC97BD1F88C8A403D6D8C63471407E78598E27184" }, 39 | ] 40 | 41 | [requirements] 42 | birdie = { version = ">= 1.1.8 and < 2.0.0" } 43 | envoy = { version = ">= 1.0.2 and < 2.0.0" } 44 | filepath = { version = ">= 1.0.0 and < 2.0.0" } 45 | gleam_community_ansi = { version = ">= 1.4.0 and < 2.0.0" } 46 | gleam_erlang = { version = ">= 0.25.0 and < 1.0.0" } 47 | gleam_http = { version = ">= 3.6.0 and < 4.0.0" } 48 | gleam_httpc = { version = ">= 2.2.0 and < 4.0.0" } 49 | gleam_json = { version = ">= 1.0.0 and < 3.0.0" } 50 | gleam_otp = { version = ">= 0.10.0 and < 1.0.0" } 51 | gleam_regexp = { version = ">= 1.0.0 and < 2.0.0" } 52 | gleam_stdlib = { version = ">= 0.47.0 and < 2.0.0" } 53 | gleeunit = { version = ">= 1.0.0 and < 2.0.0" } 54 | justin_fork = { path = "./vendor/justin_fork" } 55 | mist = { version = ">= 1.2.0 and < 4.0.0" } 56 | simplifile = { version = ">= 2.0.1 and < 3.0.0" } 57 | spinner = { version = ">= 1.1.0 and < 2.0.0" } 58 | -------------------------------------------------------------------------------- /src/chrobot/install.gleam: -------------------------------------------------------------------------------- 1 | //// This module provides basic browser installation functionality, allowing you 2 | //// to install a local version of [Google Chrome for Testing](https://github.com/GoogleChromeLabs/chrome-for-testing) in the current directory on macOS and Linux. 3 | //// 4 | //// ## Usage 5 | //// 6 | //// You may run browser installation directly with 7 | //// 8 | //// ```sh 9 | //// gleam run -m chrobot/install 10 | //// ``` 11 | //// When running directly, you can configure the browser version to install by setting the `CHROBOT_TARGET_VERSION` environment variable, 12 | //// it will default to `latest`. 13 | //// You may also set the directory to install under, with `CHROBOT_TARGET_PATH`. 14 | //// 15 | //// The browser will be installed into a directory called `chrome` under the target directory. 16 | //// There is no support for managing multiple browser installations, if an installation is already present for the same version, 17 | //// the script will overwrite it. 18 | //// 19 | //// To uninstall browsers installed by this tool just remove the `chrome` directory created by it, or delete an individual browser 20 | //// installation from inside it. 21 | //// 22 | //// ## Caveats 23 | //// 24 | //// This module attempts to rudimentarily mimic the functionality of the [puppeteer install script](https://pptr.dev/browsers-api), 25 | //// the only goal is to have a quick and convenient way to install browsers locally, for more advanced management of browser 26 | //// installations, please seek out other tools. 27 | //// 28 | //// Supported platforms are limited by what the Google Chrome for Testing distribution supports, which is currently: 29 | //// 30 | //// * linux64 31 | //// * mac-arm64 32 | //// * mac-x64 33 | //// * win32 34 | //// * win64 35 | //// 36 | //// Notably, this distribution **unfortunately does not support ARM64 on Linux**. 37 | //// 38 | //// ### Linux Dependencies 39 | //// 40 | //// The tool does **not** install dependencies on Linux, you must install them yourself. 41 | //// 42 | //// On debian / ubuntu based systems you may install dependencies with the following command: 43 | //// 44 | //// ```sh 45 | //// sudo apt-get update && sudo apt-get install -y \ 46 | //// ca-certificates \ 47 | //// fonts-liberation \ 48 | //// libasound2 \ 49 | //// libatk-bridge2.0-0 \ 50 | //// libatk1.0-0 \ 51 | //// libc6 \ 52 | //// libcairo2 \ 53 | //// libcups2 \ 54 | //// libdbus-1-3 \ 55 | //// libexpat1 \ 56 | //// libfontconfig1 \ 57 | //// libgbm1 \ 58 | //// libgcc1 \ 59 | //// libglib2.0-0 \ 60 | //// libgtk-3-0 \ 61 | //// libnspr4 \ 62 | //// libnss3 \ 63 | //// libpango-1.0-0 \ 64 | //// libpangocairo-1.0-0 \ 65 | //// libstdc++6 \ 66 | //// libx11-6 \ 67 | //// libx11-xcb1 \ 68 | //// libxcb1 \ 69 | //// libxcomposite1 \ 70 | //// libxcursor1 \ 71 | //// libxdamage1 \ 72 | //// libxext6 \ 73 | //// libxfixes3 \ 74 | //// libxi6 \ 75 | //// libxrandr2 \ 76 | //// libxrender1 \ 77 | //// libxss1 \ 78 | //// libxtst6 \ 79 | //// lsb-release \ 80 | //// wget \ 81 | //// xdg-utils 82 | //// ``` 83 | 84 | import chrobot/chrome 85 | import chrobot/internal/utils 86 | import envoy 87 | import filepath as path 88 | import gleam/dynamic 89 | import gleam/erlang/os 90 | import gleam/http/request 91 | import gleam/http/response 92 | import gleam/httpc 93 | import gleam/io 94 | import gleam/json 95 | import gleam/list 96 | import gleam/result 97 | import gleam/string 98 | import simplifile as file 99 | 100 | const version_list_endpoint = "https://googlechromelabs.github.io/chrome-for-testing/known-good-versions-with-downloads.json" 101 | 102 | pub type InstallationError { 103 | InstallationError 104 | } 105 | 106 | pub fn main() { 107 | install() 108 | } 109 | 110 | /// Install a local version of Google Chrome for Testing. 111 | /// This function is meant to be called in a script, it will log errors and warnings to the console. 112 | /// and return a generic error if installation fails. 113 | pub fn install() { 114 | let target_version = 115 | result.unwrap(envoy.get("CHROBOT_TARGET_VERSION"), "latest") 116 | let target_path = result.unwrap(envoy.get("CHROBOT_TARGET_PATH"), ".") 117 | install_with_config(target_path, target_version) 118 | } 119 | 120 | /// Install a specific local version of Google Chrome for Testing to a specific directory. 121 | /// This function is meant to be called in a script, it will log errors and warnings to the console. 122 | /// and return a generic error if installation fails. 123 | pub fn install_with_config( 124 | to target_path: String, 125 | version target_version: String, 126 | ) { 127 | let chrome_dir_path = path.join(target_path, "chrome") 128 | let chrome_dir_path = case chrome_dir_path { 129 | "./chrome" -> "chrome" 130 | other -> other 131 | } 132 | io.println( 133 | "\nPreparing to install Chrome for Testing (" 134 | <> target_version 135 | <> ") into " 136 | <> chrome_dir_path 137 | <> ".\n", 138 | ) 139 | 140 | // Existing version sanity check 141 | case chrome.get_local_chrome_path_at(chrome_dir_path) { 142 | Ok(local_chrome_path) -> { 143 | utils.warn( 144 | "You already have a local Chrome installation at this path:\n" 145 | <> local_chrome_path 146 | <> " 147 | Chrobot does not support managing multiple browser installations, 148 | you are encouraged to remove old installations manually if you no longer need them.", 149 | ) 150 | } 151 | Error(_) -> Nil 152 | } 153 | 154 | use platform <- assert_ok(resolve_platform(), "Platform unsupported") 155 | let p = utils.start_progress("Fetching available versions...") 156 | use req <- assert_ok( 157 | request.to(version_list_endpoint), 158 | "Failed to build version request", 159 | ) 160 | use res <- assert_ok( 161 | httpc.send(req), 162 | "Version list request failed, ensure you have an active internet connection.", 163 | ) 164 | use <- assert_true( 165 | res.status == 200, 166 | "Version list request returned a non-200 status code.", 167 | ) 168 | 169 | use <- assert_is_json( 170 | res, 171 | "Version list request returned a response that is not JSON.", 172 | ) 173 | 174 | use payload <- assert_ok( 175 | json.decode(res.body, dynamic.dynamic), 176 | "Failed to parse version list JSON", 177 | ) 178 | 179 | use version_list <- assert_ok( 180 | parse_version_list(payload), 181 | "Failed to decode version list JSON - Maybe the API has changed or is down?", 182 | ) 183 | 184 | use version <- assert_ok( 185 | select_version(target_version, version_list), 186 | "Failed to find version " <> target_version <> " in version list", 187 | ) 188 | 189 | use download <- assert_ok( 190 | select_download(version, platform), 191 | "Failed to find download for platform " 192 | <> platform 193 | <> " in version " 194 | <> version.version, 195 | ) 196 | 197 | utils.stop_progress(p) 198 | 199 | io.println( 200 | "\nSelected version " 201 | <> version.version 202 | <> " for platform " 203 | <> download.platform 204 | <> "\n", 205 | ) 206 | 207 | let p = utils.start_progress("Downloading Chrome for Testing...") 208 | 209 | use download_request <- assert_ok( 210 | new_download_request(download.url), 211 | "Failed to build download request", 212 | ) 213 | use download_res <- assert_ok( 214 | httpc.send_bits(download_request), 215 | "Download request failed, ensure you have an active internet connection", 216 | ) 217 | use <- assert_true( 218 | download_res.status == 200, 219 | "Download request returned a non-200 status code", 220 | ) 221 | 222 | utils.set_progress(p, "Writing download to disk...") 223 | 224 | let download_path = 225 | path.join( 226 | chrome_dir_path, 227 | "chrome_download_" <> download.platform <> version.revision <> ".zip", 228 | ) 229 | 230 | let installation_dir = 231 | path.join(chrome_dir_path, platform <> "-" <> version.version) 232 | 233 | use _ <- assert_ok( 234 | file.create_directory_all(installation_dir), 235 | "Failed to create directory", 236 | ) 237 | 238 | use _ <- assert_ok( 239 | file.write_bits(download_res.body, to: download_path), 240 | "Failed to write download to disk", 241 | ) 242 | 243 | utils.set_progress(p, "Extracting download...") 244 | 245 | use _ <- assert_ok( 246 | unzip(download_path, installation_dir), 247 | "Failed to extract downloaded .zip archive", 248 | ) 249 | 250 | use _ <- assert_ok( 251 | file.delete(download_path), 252 | "Failed to remove downloaded .zip archive! The installation should otherwise have succeeded.", 253 | ) 254 | 255 | // Find the executable binary 256 | use haystack <- assert_ok( 257 | file.get_files(installation_dir), 258 | "Failed to scan installation directory for executable", 259 | ) 260 | 261 | use executable <- assert_ok( 262 | list.find(haystack, fn(file) { 263 | chrome.is_local_chrome_path(file, os.family()) 264 | }), 265 | "Failed to find executable in installation directory", 266 | ) 267 | 268 | utils.stop_progress(p) 269 | 270 | case os.family() { 271 | os.Linux -> { 272 | utils.hint( 273 | "You can run the following command to check wich depencies are missing on your system:", 274 | ) 275 | utils.show_cmd("ldd \"" <> executable <> "\" | grep not") 276 | } 277 | _ -> Nil 278 | } 279 | 280 | utils.info( 281 | "Chrome for Testing (" 282 | <> version.version 283 | <> ") installed successfully! The executable is located at:\n" 284 | <> executable 285 | <> "\n" 286 | <> "When using the `launch` command, chrobot should automatically use this local installation.", 287 | ) 288 | 289 | Ok(executable) 290 | } 291 | 292 | type VersionItem { 293 | VersionItem(version: String, revision: String, downloads: List(DownloadItem)) 294 | } 295 | 296 | type DownloadItem { 297 | DownloadItem(platform: String, url: String) 298 | } 299 | 300 | fn select_version( 301 | target: String, 302 | version_list: List(VersionItem), 303 | ) -> Result(VersionItem, Nil) { 304 | case target { 305 | "latest" -> { 306 | list.last(version_list) 307 | } 308 | _ -> { 309 | case string.contains(target, ".") { 310 | // Try for exact match 311 | True -> { 312 | list.find(version_list, fn(item) { item.version == target }) 313 | } 314 | False -> { 315 | // Try to find first major version matching the target 316 | list.reverse(version_list) 317 | |> list.find(fn(item) { 318 | case string.split(item.version, ".") { 319 | [major, ..] if major == target -> True 320 | _ -> False 321 | } 322 | }) 323 | } 324 | } 325 | } 326 | } 327 | } 328 | 329 | fn select_download(version: VersionItem, platform: String) { 330 | list.find(version.downloads, fn(item) { item.platform == platform }) 331 | } 332 | 333 | fn parse_version_list(input: dynamic.Dynamic) { 334 | let download_item_decoder = 335 | dynamic.decode2( 336 | DownloadItem, 337 | dynamic.field("platform", dynamic.string), 338 | dynamic.field("url", dynamic.string), 339 | ) 340 | let download_list_item_decoder = fn(list_item: dynamic.Dynamic) { 341 | dynamic.field("chrome", dynamic.list(download_item_decoder))(list_item) 342 | } 343 | let version_item_decoder = 344 | dynamic.decode3( 345 | VersionItem, 346 | dynamic.field("version", dynamic.string), 347 | dynamic.field("revision", dynamic.string), 348 | dynamic.field("downloads", download_list_item_decoder), 349 | ) 350 | 351 | dynamic.field("versions", dynamic.list(version_item_decoder))(input) 352 | } 353 | 354 | fn resolve_platform() -> Result(String, String) { 355 | case os.family(), get_arch() { 356 | os.Darwin, "aarch64" <> _ -> { 357 | Ok("mac-arm64") 358 | } 359 | os.Darwin, _ -> { 360 | Ok("mac-x64") 361 | } 362 | os.Linux, "x86_64" <> _ -> { 363 | io.println("") 364 | utils.warn( 365 | "You appear to be on linux, just to let you know, dependencies are not installed automatically by this script, 366 | you must install them yourself! Please check the docs of the install module for further information.", 367 | ) 368 | Ok("linux64") 369 | } 370 | os.WindowsNt, "x86_64" <> _ -> { 371 | Ok("win64") 372 | } 373 | os.WindowsNt, _ -> { 374 | utils.warn( 375 | "The installer thinks you are on a 32-bit Windows system and is installing 32-bit Chrome, 376 | this is unusual, please verify this is correct", 377 | ) 378 | Ok("win32") 379 | } 380 | _, architecture -> { 381 | utils.err("Could not resolve an appropriate platform for your system. 382 | Please note that the available platforms are limited by what Google Chrome for Testing supports, 383 | notably, ARM64 on Linux is unfortunately not supported at the moment. 384 | Your architecture is: " <> architecture <> ".") 385 | Error("Unsupported system: " <> architecture) 386 | } 387 | } 388 | } 389 | 390 | fn assert_is_json( 391 | res, 392 | human_error: String, 393 | apply fun: fn() -> Result(b, InstallationError), 394 | ) { 395 | case response.get_header(res, "content-type") { 396 | Ok("application/json") -> fun() 397 | Ok("application/json" <> _) -> fun() 398 | _ -> { 399 | io.println("") 400 | utils.err(human_error) 401 | Error(InstallationError) 402 | } 403 | } 404 | } 405 | 406 | fn assert_ok( 407 | result: Result(a, e), 408 | human_error: String, 409 | apply fun: fn(a) -> Result(b, InstallationError), 410 | ) -> Result(b, InstallationError) { 411 | case result { 412 | Ok(x) -> fun(x) 413 | Error(err) -> { 414 | io.println("") 415 | utils.err(human_error) 416 | io.debug(err) 417 | Error(InstallationError) 418 | } 419 | } 420 | } 421 | 422 | fn assert_true( 423 | condition: Bool, 424 | human_error: String, 425 | apply fun: fn() -> Result(a, InstallationError), 426 | ) -> Result(a, InstallationError) { 427 | case condition { 428 | True -> fun() 429 | False -> { 430 | io.println("") 431 | utils.err(human_error) 432 | Error(InstallationError) 433 | } 434 | } 435 | } 436 | 437 | /// Attempt unzip of the downloaded file 438 | /// Notes: 439 | /// The erlang standard library unzip function, does not restore file permissions, and 440 | /// chrome consists of a bunch of executables, setting them all to executable 441 | /// manually is a bit annoying. 442 | /// Therefore, we try to use the system unzip command via a shell instead, 443 | /// and only fall back to the erlang unzip if that fails. 444 | fn unzip(from: String, to: String) { 445 | run_command("unzip -q " <> from <> " -d " <> to) 446 | use installation_dir_entries <- result.try( 447 | file.read_directory(to) 448 | |> result.replace_error(Nil), 449 | ) 450 | 451 | let was_extracted = 452 | list.map(installation_dir_entries, fn(i) { 453 | file.is_directory(path.join(to, i)) 454 | }) 455 | |> list.any(fn(check) { 456 | case check { 457 | Ok(True) -> True 458 | _ -> False 459 | } 460 | }) 461 | 462 | case was_extracted { 463 | True -> Ok(Nil) 464 | False -> { 465 | // In this fallback method we extract the zip using erlang unzip, and then set the executable bit on all files 466 | // As you can imagine, this is not ideal, and may cause issues, therefore we warn the user. 467 | utils.warn( 468 | "Failed to extract downloaded .zip archive using system unzip command, falling back to erlang unzip. 469 | You might run into permission issues when attempting to run the installed binary, this is not ideal!", 470 | ) 471 | use _ <- result.try(erl_unzip(from, to)) 472 | use installation_files <- result.try( 473 | file.get_files(to) 474 | |> result.replace_error(Nil), 475 | ) 476 | list.each(installation_files, fn(i) { 477 | case file.is_file(i) { 478 | Ok(True) -> { 479 | let _ = set_executable(i) 480 | Nil 481 | } 482 | _ -> { 483 | Nil 484 | } 485 | } 486 | }) 487 | Ok(Nil) 488 | } 489 | } 490 | } 491 | 492 | fn new_download_request(url: String) { 493 | use base_req <- result.try(request.to(url)) 494 | Ok(request.set_body(base_req, <<>>)) 495 | } 496 | 497 | @external(erlang, "chrobot_ffi", "get_arch") 498 | fn get_arch() -> String 499 | 500 | @external(erlang, "chrobot_ffi", "unzip") 501 | fn erl_unzip(from: String, to: String) -> Result(Nil, Nil) 502 | 503 | @external(erlang, "chrobot_ffi", "run_command") 504 | fn run_command(command: String) -> String 505 | 506 | @external(erlang, "chrobot_ffi", "set_executable") 507 | fn set_executable(file: String) -> Result(Nil, Nil) 508 | -------------------------------------------------------------------------------- /src/chrobot/internal/utils.gleam: -------------------------------------------------------------------------------- 1 | import envoy 2 | import gleam/erlang/process.{type CallError, type Subject} as p 3 | import gleam/io 4 | import gleam/json 5 | import gleam/option.{type Option, None, Some} 6 | import gleam/string 7 | import gleam_community/ansi 8 | import spinner 9 | 10 | /// Very very naive but should be fine 11 | fn term_supports_color() -> Bool { 12 | case envoy.get("TERM") { 13 | Ok("dumb") -> False 14 | _ -> True 15 | } 16 | } 17 | 18 | pub fn add_optional( 19 | prop_encoders: List(#(String, json.Json)), 20 | value: option.Option(a), 21 | callback: fn(a) -> #(String, json.Json), 22 | ) { 23 | case value { 24 | option.Some(a) -> [callback(a), ..prop_encoders] 25 | option.None -> prop_encoders 26 | } 27 | } 28 | 29 | pub fn alert_encode_dynamic(input_value) { 30 | warn( 31 | "You passed a dymamic value to a protocol encoder! 32 | Dynamic values cannot be encoded, the value will be set to null instead. 33 | This is unlikely to be intentional, you should fix that part of your code.", 34 | ) 35 | io.println("The value was: " <> string.inspect(input_value)) 36 | json.null() 37 | } 38 | 39 | fn align(content: String) { 40 | string.replace(content, "\n", "\n ") 41 | } 42 | 43 | pub fn err(content: String) { 44 | case term_supports_color() { 45 | True -> { 46 | { 47 | "[-_-] ERR! " 48 | |> ansi.bg_red() 49 | |> ansi.white() 50 | |> ansi.bold() 51 | <> " " 52 | <> align(content) 53 | |> ansi.red() 54 | } 55 | |> io.println() 56 | } 57 | False -> { 58 | io.println("[-_-] ERR! " <> content) 59 | } 60 | } 61 | } 62 | 63 | pub fn warn(content: String) { 64 | case term_supports_color() { 65 | True -> { 66 | { 67 | "[O_O] HEY! " 68 | |> ansi.bg_yellow() 69 | |> ansi.black() 70 | |> ansi.bold() 71 | <> " " 72 | <> align(content) 73 | |> ansi.yellow() 74 | } 75 | |> io.println() 76 | } 77 | False -> { 78 | io.println("[O_O] HEY! " <> content) 79 | } 80 | } 81 | } 82 | 83 | pub fn hint(content: String) { 84 | case term_supports_color() { 85 | True -> { 86 | { 87 | "[>‿0] HINT " 88 | |> ansi.bg_cyan() 89 | |> ansi.black() 90 | |> ansi.bold() 91 | <> " " 92 | <> align(content) 93 | |> ansi.cyan() 94 | } 95 | |> io.println() 96 | } 97 | False -> { 98 | io.println("[>‿0] HINT " <> content) 99 | } 100 | } 101 | } 102 | 103 | pub fn info(content: String) { 104 | case term_supports_color() { 105 | True -> { 106 | { 107 | "[0‿0] INFO " 108 | |> ansi.bg_white() 109 | |> ansi.black() 110 | |> ansi.bold() 111 | <> " " 112 | <> align(content) 113 | |> ansi.white() 114 | } 115 | |> io.println() 116 | } 117 | False -> { 118 | io.println("[0‿0] INFO " <> content) 119 | } 120 | } 121 | } 122 | 123 | pub fn start_progress(text: String) -> Option(spinner.Spinner) { 124 | case term_supports_color() { 125 | True -> { 126 | let spinner = 127 | spinner.new(text) 128 | |> spinner.with_colour(ansi.blue) 129 | |> spinner.start() 130 | Some(spinner) 131 | } 132 | False -> { 133 | io.println("Progress: " <> text) 134 | None 135 | } 136 | } 137 | } 138 | 139 | pub fn set_progress(spinner: Option(spinner.Spinner), text: String) -> Nil { 140 | case spinner { 141 | Some(spinner) -> spinner.set_text(spinner, text) 142 | None -> { 143 | io.println("Progress: " <> text) 144 | Nil 145 | } 146 | } 147 | } 148 | 149 | pub fn stop_progress(spinner: Option(spinner.Spinner)) -> Nil { 150 | case spinner { 151 | Some(spinner) -> spinner.stop(spinner) 152 | None -> Nil 153 | } 154 | } 155 | 156 | pub fn show_cmd(content: String) { 157 | case term_supports_color() { 158 | True -> { 159 | { "\n " <> ansi.dim("$") <> " " <> ansi.bold(content) <> "\n" } 160 | |> io.println() 161 | } 162 | False -> { 163 | io.println("\n $ " <> content <> "\n") 164 | } 165 | } 166 | } 167 | 168 | pub fn try_call_with_subject( 169 | subject: Subject(request), 170 | make_request: fn(Subject(response)) -> request, 171 | reply_subject: Subject(response), 172 | within timeout: Int, 173 | ) -> Result(response, CallError(response)) { 174 | // Monitor the callee process so we can tell if it goes down (meaning we 175 | // won't get a reply) 176 | let monitor = p.monitor_process(p.subject_owner(subject)) 177 | 178 | // Send the request to the process over the channel 179 | p.send(subject, make_request(reply_subject)) 180 | 181 | // Await a reply or handle failure modes (timeout, process down, etc) 182 | let result = 183 | p.new_selector() 184 | |> p.selecting(reply_subject, Ok) 185 | |> p.selecting_process_down(monitor, fn(down: p.ProcessDown) { 186 | Error(p.CalleeDown(reason: down.reason)) 187 | }) 188 | |> p.select(timeout) 189 | 190 | // Demonitor the process and close the channels as we're done 191 | p.demonitor_process(monitor) 192 | 193 | // Prepare an appropriate error (if present) for the caller 194 | case result { 195 | Error(Nil) -> Error(p.CallTimeout) 196 | Ok(res) -> res 197 | } 198 | } 199 | 200 | @external(erlang, "chrobot_ffi", "get_time_ms") 201 | pub fn get_time_ms() -> Int 202 | -------------------------------------------------------------------------------- /src/chrobot/protocol.gleam: -------------------------------------------------------------------------------- 1 | //// > ⚙️ This module was generated from the Chrome DevTools Protocol version **1.3** 2 | //// For reference: [See the DevTools Protocol API Docs](https://chromedevtools.github.io/devtools-protocol/1-3/) 3 | //// 4 | //// This is the protocol definition entrypoint, it contains an overview of the protocol structure, 5 | //// and a function to retrieve the version of the protocol used to generate the current bindings. 6 | //// The protocol version is also displayed in the box above, which appears on every generated module. 7 | //// 8 | //// ## ⚠️ Really Important Notes 9 | //// 10 | //// 1) It's best never to work with the DOM domain for automation, 11 | //// [an explanation of why can be found here](https://github.com/puppeteer/puppeteer/pull/71#issuecomment-314599749). 12 | //// Instead, to automate DOM interaction, JavaScript can be injected using the Runtime domain. 13 | //// 14 | //// 2) Unfortunately, I haven't found a good way to map dynamic properties to gleam attributes bidirectionally. 15 | //// **This means all dynamic values you supply to commands will be silently dropped**! 16 | //// It's important to realize this to avoid confusion, for example in `runtime.call_function_on` 17 | //// you may want to supply arguments which can be any value, but it won't work. 18 | //// The only path to do that as far as I can tell, is write the protocol call logic yourself, 19 | //// perhaps taking the codegen code as a basis. 20 | //// Check the `call_custom_function_on` function from `chrobot` which does this for the mentioned function 21 | //// 22 | //// ## Structure 23 | //// 24 | //// Each domain in the protocol is represented as a module under `protocol/`. 25 | //// 26 | //// In general, the bindings are generated through codegen, directly from the JSON protocol schema [published here](https://github.com/ChromeDevTools/devtools-protocol), 27 | //// however there are some little adjustments that needed to be made, to make the protocol schema usable, mainly due to 28 | //// what I believe are minor bugs in the protocol. 29 | //// To see these changes, check the `apply_protocol_patches` function in `chrobot/internal/generate_bindings`. 30 | //// 31 | //// Domains may depend on the types of other domains, these dependencies are mirrored in the generated bindings where possible. 32 | //// In some case, type references to other modules have been replaced by the respective inner type, because the references would 33 | //// create a circular dependency. 34 | //// 35 | //// ## Types 36 | //// 37 | //// The generated bindings include a mirror of the type defitions of each type in the protocol spec, 38 | //// alongside with an `encode__` function to encode the type into JSON in order to send it to the browser 39 | //// and a `decode__` function in order to decode the type out of a payload sent from the browser. Encoders and 40 | //// decoders are marked internal and should be used through command functions which are described below. 41 | //// 42 | //// Notes: 43 | //// - Some object properties in the protocol have the type `any`, in this case the value is considered as dynamic 44 | //// by decoders, and encoders will not encode it, setting it to `null` instead in the payload 45 | //// - Object types that don't specify any properties are treated as a `Dict(String,String)` 46 | //// 47 | //// Additional type definitions and encoders / decoders are generated, 48 | //// for any enumerable property in the protocol, as well as the return values of commands. 49 | //// These special type definitions are marked with a comment to indicate 50 | //// the fact that they are not part of the protocol spec, but rather generated dynamically to support the bindings. 51 | //// 52 | //// 53 | //// ## Commands 54 | //// 55 | //// A function is generated for each command, named after the command (in snake case). 56 | //// The function handles both encoding the parameters to sent to the browser via the protocol, and decoding the response. 57 | //// A `ProtocolError` error is returned if the decoding fails, this would mean there is a bug in the protocol 58 | //// or the generated bindings. 59 | //// 60 | //// The first parameter to the command function is always a `callback` of the form 61 | //// 62 | //// ```gleam 63 | //// fn(method: String, parameters: Option(Json)) -> Result(Dynamic, RequestError) 64 | //// ``` 65 | //// 66 | //// By using this callback you can take advantage of the generated protocol encoders/decoders 67 | //// while also passing in your browser subject to direct the command to, and passing along additional 68 | //// arguments, like the `sessionId` which is required for some operations. 69 | //// 70 | //// 71 | //// ## Events 72 | //// 73 | //// Events are not implemented yet! 74 | //// 75 | //// 76 | //// 77 | 78 | // --------------------------------------------------------------------------- 79 | // | !!!!!! This is an autogenerated file - Do not edit manually !!!!!! | 80 | // | Run `codegen.sh` to regenerate. | 81 | // --------------------------------------------------------------------------- 82 | 83 | const version_major = "1" 84 | 85 | const version_minor = "3" 86 | 87 | /// Get the protocol version as a tuple of major and minor version 88 | pub fn version() { 89 | #(version_major, version_minor) 90 | } 91 | -------------------------------------------------------------------------------- /src/chrobot/protocol/browser.gleam: -------------------------------------------------------------------------------- 1 | //// > ⚙️ This module was generated from the Chrome DevTools Protocol version **1.3** 2 | //// ## Browser Domain 3 | //// 4 | //// The Browser domain defines methods and events for browser managing. 5 | //// 6 | //// [📖 View this domain on the DevTools Protocol API Docs](https://chromedevtools.github.io/devtools-protocol/1-3/Browser/) 7 | 8 | // --------------------------------------------------------------------------- 9 | // | !!!!!! This is an autogenerated file - Do not edit manually !!!!!! | 10 | // | Run `codegen.sh` to regenerate. | 11 | // --------------------------------------------------------------------------- 12 | 13 | import chrobot/chrome 14 | import chrobot/internal/utils 15 | import gleam/dynamic 16 | import gleam/json 17 | import gleam/option 18 | import gleam/result 19 | 20 | /// This type is not part of the protocol spec, it has been generated dynamically 21 | /// to represent the response to the command `get_version` 22 | pub type GetVersionResponse { 23 | GetVersionResponse( 24 | /// Protocol version. 25 | protocol_version: String, 26 | /// Product name. 27 | product: String, 28 | /// Product revision. 29 | revision: String, 30 | /// User-Agent. 31 | user_agent: String, 32 | /// V8 version. 33 | js_version: String, 34 | ) 35 | } 36 | 37 | @internal 38 | pub fn decode__get_version_response(value__: dynamic.Dynamic) { 39 | use protocol_version <- result.try(dynamic.field( 40 | "protocolVersion", 41 | dynamic.string, 42 | )(value__)) 43 | use product <- result.try(dynamic.field("product", dynamic.string)(value__)) 44 | use revision <- result.try(dynamic.field("revision", dynamic.string)(value__)) 45 | use user_agent <- result.try(dynamic.field("userAgent", dynamic.string)( 46 | value__, 47 | )) 48 | use js_version <- result.try(dynamic.field("jsVersion", dynamic.string)( 49 | value__, 50 | )) 51 | 52 | Ok(GetVersionResponse( 53 | protocol_version: protocol_version, 54 | product: product, 55 | revision: revision, 56 | user_agent: user_agent, 57 | js_version: js_version, 58 | )) 59 | } 60 | 61 | /// Reset all permission management for all origins. 62 | /// 63 | /// Parameters: 64 | /// - `browser_context_id` : BrowserContext to reset permissions. When omitted, default browser context is used. 65 | /// 66 | /// Returns: 67 | /// 68 | pub fn reset_permissions( 69 | callback__, 70 | browser_context_id browser_context_id: option.Option(String), 71 | ) { 72 | callback__( 73 | "Browser.resetPermissions", 74 | option.Some(json.object( 75 | [] 76 | |> utils.add_optional(browser_context_id, fn(inner_value__) { 77 | #("browserContextId", json.string(inner_value__)) 78 | }), 79 | )), 80 | ) 81 | } 82 | 83 | /// Close browser gracefully. 84 | /// 85 | pub fn close(callback__) { 86 | callback__("Browser.close", option.None) 87 | } 88 | 89 | /// Returns version information. 90 | /// - `protocol_version` : Protocol version. 91 | /// - `product` : Product name. 92 | /// - `revision` : Product revision. 93 | /// - `user_agent` : User-Agent. 94 | /// - `js_version` : V8 version. 95 | /// 96 | pub fn get_version(callback__) { 97 | use result__ <- result.try(callback__("Browser.getVersion", option.None)) 98 | 99 | decode__get_version_response(result__) 100 | |> result.replace_error(chrome.ProtocolError) 101 | } 102 | 103 | /// Allows a site to use privacy sandbox features that require enrollment 104 | /// without the site actually being enrolled. Only supported on page targets. 105 | /// 106 | /// Parameters: 107 | /// - `url` 108 | /// 109 | /// Returns: 110 | /// 111 | pub fn add_privacy_sandbox_enrollment_override(callback__, url url: String) { 112 | callback__( 113 | "Browser.addPrivacySandboxEnrollmentOverride", 114 | option.Some(json.object([#("url", json.string(url))])), 115 | ) 116 | } 117 | -------------------------------------------------------------------------------- /src/chrobot/protocol/dom_debugger.gleam: -------------------------------------------------------------------------------- 1 | //// > ⚙️ This module was generated from the Chrome DevTools Protocol version **1.3** 2 | //// ## DOMDebugger Domain 3 | //// 4 | //// DOM debugging allows setting breakpoints on particular DOM operations and events. JavaScript 5 | //// execution will stop on these operations as if there was a regular breakpoint set. 6 | //// 7 | //// [📖 View this domain on the DevTools Protocol API Docs](https://chromedevtools.github.io/devtools-protocol/1-3/DOMDebugger/) 8 | 9 | // --------------------------------------------------------------------------- 10 | // | !!!!!! This is an autogenerated file - Do not edit manually !!!!!! | 11 | // | Run `codegen.sh` to regenerate. | 12 | // --------------------------------------------------------------------------- 13 | 14 | import chrobot/chrome 15 | import chrobot/internal/utils 16 | import chrobot/protocol/dom 17 | import chrobot/protocol/runtime 18 | import gleam/dynamic 19 | import gleam/json 20 | import gleam/option 21 | import gleam/result 22 | 23 | /// DOM breakpoint type. 24 | pub type DOMBreakpointType { 25 | DOMBreakpointTypeSubtreeModified 26 | DOMBreakpointTypeAttributeModified 27 | DOMBreakpointTypeNodeRemoved 28 | } 29 | 30 | @internal 31 | pub fn encode__dom_breakpoint_type(value__: DOMBreakpointType) { 32 | case value__ { 33 | DOMBreakpointTypeSubtreeModified -> "subtree-modified" 34 | DOMBreakpointTypeAttributeModified -> "attribute-modified" 35 | DOMBreakpointTypeNodeRemoved -> "node-removed" 36 | } 37 | |> json.string() 38 | } 39 | 40 | @internal 41 | pub fn decode__dom_breakpoint_type(value__: dynamic.Dynamic) { 42 | case dynamic.string(value__) { 43 | Ok("subtree-modified") -> Ok(DOMBreakpointTypeSubtreeModified) 44 | Ok("attribute-modified") -> Ok(DOMBreakpointTypeAttributeModified) 45 | Ok("node-removed") -> Ok(DOMBreakpointTypeNodeRemoved) 46 | Error(error) -> Error(error) 47 | Ok(other) -> 48 | Error([ 49 | dynamic.DecodeError( 50 | expected: "valid enum property", 51 | found: other, 52 | path: ["enum decoder"], 53 | ), 54 | ]) 55 | } 56 | } 57 | 58 | /// Object event listener. 59 | pub type EventListener { 60 | EventListener( 61 | /// `EventListener`'s type. 62 | type_: String, 63 | /// `EventListener`'s useCapture. 64 | use_capture: Bool, 65 | /// `EventListener`'s passive flag. 66 | passive: Bool, 67 | /// `EventListener`'s once flag. 68 | once: Bool, 69 | /// Script id of the handler code. 70 | script_id: runtime.ScriptId, 71 | /// Line number in the script (0-based). 72 | line_number: Int, 73 | /// Column number in the script (0-based). 74 | column_number: Int, 75 | /// Event handler function value. 76 | handler: option.Option(runtime.RemoteObject), 77 | /// Event original handler function value. 78 | original_handler: option.Option(runtime.RemoteObject), 79 | /// Node the listener is added to (if any). 80 | backend_node_id: option.Option(dom.BackendNodeId), 81 | ) 82 | } 83 | 84 | @internal 85 | pub fn encode__event_listener(value__: EventListener) { 86 | json.object( 87 | [ 88 | #("type", json.string(value__.type_)), 89 | #("useCapture", json.bool(value__.use_capture)), 90 | #("passive", json.bool(value__.passive)), 91 | #("once", json.bool(value__.once)), 92 | #("scriptId", runtime.encode__script_id(value__.script_id)), 93 | #("lineNumber", json.int(value__.line_number)), 94 | #("columnNumber", json.int(value__.column_number)), 95 | ] 96 | |> utils.add_optional(value__.handler, fn(inner_value__) { 97 | #("handler", runtime.encode__remote_object(inner_value__)) 98 | }) 99 | |> utils.add_optional(value__.original_handler, fn(inner_value__) { 100 | #("originalHandler", runtime.encode__remote_object(inner_value__)) 101 | }) 102 | |> utils.add_optional(value__.backend_node_id, fn(inner_value__) { 103 | #("backendNodeId", dom.encode__backend_node_id(inner_value__)) 104 | }), 105 | ) 106 | } 107 | 108 | @internal 109 | pub fn decode__event_listener(value__: dynamic.Dynamic) { 110 | use type_ <- result.try(dynamic.field("type", dynamic.string)(value__)) 111 | use use_capture <- result.try(dynamic.field("useCapture", dynamic.bool)( 112 | value__, 113 | )) 114 | use passive <- result.try(dynamic.field("passive", dynamic.bool)(value__)) 115 | use once <- result.try(dynamic.field("once", dynamic.bool)(value__)) 116 | use script_id <- result.try(dynamic.field( 117 | "scriptId", 118 | runtime.decode__script_id, 119 | )(value__)) 120 | use line_number <- result.try(dynamic.field("lineNumber", dynamic.int)( 121 | value__, 122 | )) 123 | use column_number <- result.try(dynamic.field("columnNumber", dynamic.int)( 124 | value__, 125 | )) 126 | use handler <- result.try(dynamic.optional_field( 127 | "handler", 128 | runtime.decode__remote_object, 129 | )(value__)) 130 | use original_handler <- result.try(dynamic.optional_field( 131 | "originalHandler", 132 | runtime.decode__remote_object, 133 | )(value__)) 134 | use backend_node_id <- result.try(dynamic.optional_field( 135 | "backendNodeId", 136 | dom.decode__backend_node_id, 137 | )(value__)) 138 | 139 | Ok(EventListener( 140 | type_: type_, 141 | use_capture: use_capture, 142 | passive: passive, 143 | once: once, 144 | script_id: script_id, 145 | line_number: line_number, 146 | column_number: column_number, 147 | handler: handler, 148 | original_handler: original_handler, 149 | backend_node_id: backend_node_id, 150 | )) 151 | } 152 | 153 | /// This type is not part of the protocol spec, it has been generated dynamically 154 | /// to represent the response to the command `get_event_listeners` 155 | pub type GetEventListenersResponse { 156 | GetEventListenersResponse( 157 | /// Array of relevant listeners. 158 | listeners: List(EventListener), 159 | ) 160 | } 161 | 162 | @internal 163 | pub fn decode__get_event_listeners_response(value__: dynamic.Dynamic) { 164 | use listeners <- result.try(dynamic.field( 165 | "listeners", 166 | dynamic.list(decode__event_listener), 167 | )(value__)) 168 | 169 | Ok(GetEventListenersResponse(listeners: listeners)) 170 | } 171 | 172 | /// Returns event listeners of the given object. 173 | /// 174 | /// Parameters: 175 | /// - `object_id` : Identifier of the object to return listeners for. 176 | /// - `depth` : The maximum depth at which Node children should be retrieved, defaults to 1. Use -1 for the 177 | /// entire subtree or provide an integer larger than 0. 178 | /// - `pierce` : Whether or not iframes and shadow roots should be traversed when returning the subtree 179 | /// (default is false). Reports listeners for all contexts if pierce is enabled. 180 | /// 181 | /// Returns: 182 | /// - `listeners` : Array of relevant listeners. 183 | /// 184 | pub fn get_event_listeners( 185 | callback__, 186 | object_id object_id: runtime.RemoteObjectId, 187 | depth depth: option.Option(Int), 188 | pierce pierce: option.Option(Bool), 189 | ) { 190 | use result__ <- result.try(callback__( 191 | "DOMDebugger.getEventListeners", 192 | option.Some(json.object( 193 | [#("objectId", runtime.encode__remote_object_id(object_id))] 194 | |> utils.add_optional(depth, fn(inner_value__) { 195 | #("depth", json.int(inner_value__)) 196 | }) 197 | |> utils.add_optional(pierce, fn(inner_value__) { 198 | #("pierce", json.bool(inner_value__)) 199 | }), 200 | )), 201 | )) 202 | 203 | decode__get_event_listeners_response(result__) 204 | |> result.replace_error(chrome.ProtocolError) 205 | } 206 | 207 | /// Removes DOM breakpoint that was set using `setDOMBreakpoint`. 208 | /// 209 | /// Parameters: 210 | /// - `node_id` : Identifier of the node to remove breakpoint from. 211 | /// - `type_` : Type of the breakpoint to remove. 212 | /// 213 | /// Returns: 214 | /// 215 | pub fn remove_dom_breakpoint( 216 | callback__, 217 | node_id node_id: dom.NodeId, 218 | type_ type_: DOMBreakpointType, 219 | ) { 220 | callback__( 221 | "DOMDebugger.removeDOMBreakpoint", 222 | option.Some( 223 | json.object([ 224 | #("nodeId", dom.encode__node_id(node_id)), 225 | #("type", encode__dom_breakpoint_type(type_)), 226 | ]), 227 | ), 228 | ) 229 | } 230 | 231 | /// Removes breakpoint on particular DOM event. 232 | /// 233 | /// Parameters: 234 | /// - `event_name` : Event name. 235 | /// 236 | /// Returns: 237 | /// 238 | pub fn remove_event_listener_breakpoint( 239 | callback__, 240 | event_name event_name: String, 241 | ) { 242 | callback__( 243 | "DOMDebugger.removeEventListenerBreakpoint", 244 | option.Some(json.object([#("eventName", json.string(event_name))])), 245 | ) 246 | } 247 | 248 | /// Removes breakpoint from XMLHttpRequest. 249 | /// 250 | /// Parameters: 251 | /// - `url` : Resource URL substring. 252 | /// 253 | /// Returns: 254 | /// 255 | pub fn remove_xhr_breakpoint(callback__, url url: String) { 256 | callback__( 257 | "DOMDebugger.removeXHRBreakpoint", 258 | option.Some(json.object([#("url", json.string(url))])), 259 | ) 260 | } 261 | 262 | /// Sets breakpoint on particular operation with DOM. 263 | /// 264 | /// Parameters: 265 | /// - `node_id` : Identifier of the node to set breakpoint on. 266 | /// - `type_` : Type of the operation to stop upon. 267 | /// 268 | /// Returns: 269 | /// 270 | pub fn set_dom_breakpoint( 271 | callback__, 272 | node_id node_id: dom.NodeId, 273 | type_ type_: DOMBreakpointType, 274 | ) { 275 | callback__( 276 | "DOMDebugger.setDOMBreakpoint", 277 | option.Some( 278 | json.object([ 279 | #("nodeId", dom.encode__node_id(node_id)), 280 | #("type", encode__dom_breakpoint_type(type_)), 281 | ]), 282 | ), 283 | ) 284 | } 285 | 286 | /// Sets breakpoint on particular DOM event. 287 | /// 288 | /// Parameters: 289 | /// - `event_name` : DOM Event name to stop on (any DOM event will do). 290 | /// 291 | /// Returns: 292 | /// 293 | pub fn set_event_listener_breakpoint(callback__, event_name event_name: String) { 294 | callback__( 295 | "DOMDebugger.setEventListenerBreakpoint", 296 | option.Some(json.object([#("eventName", json.string(event_name))])), 297 | ) 298 | } 299 | 300 | /// Sets breakpoint on XMLHttpRequest. 301 | /// 302 | /// Parameters: 303 | /// - `url` : Resource URL substring. All XHRs having this substring in the URL will get stopped upon. 304 | /// 305 | /// Returns: 306 | /// 307 | pub fn set_xhr_breakpoint(callback__, url url: String) { 308 | callback__( 309 | "DOMDebugger.setXHRBreakpoint", 310 | option.Some(json.object([#("url", json.string(url))])), 311 | ) 312 | } 313 | -------------------------------------------------------------------------------- /src/chrobot/protocol/emulation.gleam: -------------------------------------------------------------------------------- 1 | //// > ⚙️ This module was generated from the Chrome DevTools Protocol version **1.3** 2 | //// ## Emulation Domain 3 | //// 4 | //// This domain emulates different environments for the page. 5 | //// 6 | //// [📖 View this domain on the DevTools Protocol API Docs](https://chromedevtools.github.io/devtools-protocol/1-3/Emulation/) 7 | 8 | // --------------------------------------------------------------------------- 9 | // | !!!!!! This is an autogenerated file - Do not edit manually !!!!!! | 10 | // | Run `codegen.sh` to regenerate. | 11 | // --------------------------------------------------------------------------- 12 | 13 | import chrobot/internal/utils 14 | import chrobot/protocol/dom 15 | import gleam/dynamic 16 | import gleam/json 17 | import gleam/option 18 | import gleam/result 19 | 20 | /// Screen orientation. 21 | pub type ScreenOrientation { 22 | ScreenOrientation( 23 | /// Orientation type. 24 | type_: ScreenOrientationType, 25 | /// Orientation angle. 26 | angle: Int, 27 | ) 28 | } 29 | 30 | /// This type is not part of the protocol spec, it has been generated dynamically 31 | /// to represent the possible values of the enum property `type` of `ScreenOrientation` 32 | pub type ScreenOrientationType { 33 | ScreenOrientationTypePortraitPrimary 34 | ScreenOrientationTypePortraitSecondary 35 | ScreenOrientationTypeLandscapePrimary 36 | ScreenOrientationTypeLandscapeSecondary 37 | } 38 | 39 | @internal 40 | pub fn encode__screen_orientation_type(value__: ScreenOrientationType) { 41 | case value__ { 42 | ScreenOrientationTypePortraitPrimary -> "portraitPrimary" 43 | ScreenOrientationTypePortraitSecondary -> "portraitSecondary" 44 | ScreenOrientationTypeLandscapePrimary -> "landscapePrimary" 45 | ScreenOrientationTypeLandscapeSecondary -> "landscapeSecondary" 46 | } 47 | |> json.string() 48 | } 49 | 50 | @internal 51 | pub fn decode__screen_orientation_type(value__: dynamic.Dynamic) { 52 | case dynamic.string(value__) { 53 | Ok("portraitPrimary") -> Ok(ScreenOrientationTypePortraitPrimary) 54 | Ok("portraitSecondary") -> Ok(ScreenOrientationTypePortraitSecondary) 55 | Ok("landscapePrimary") -> Ok(ScreenOrientationTypeLandscapePrimary) 56 | Ok("landscapeSecondary") -> Ok(ScreenOrientationTypeLandscapeSecondary) 57 | Error(error) -> Error(error) 58 | Ok(other) -> 59 | Error([ 60 | dynamic.DecodeError( 61 | expected: "valid enum property", 62 | found: other, 63 | path: ["enum decoder"], 64 | ), 65 | ]) 66 | } 67 | } 68 | 69 | @internal 70 | pub fn encode__screen_orientation(value__: ScreenOrientation) { 71 | json.object([ 72 | #("type", encode__screen_orientation_type(value__.type_)), 73 | #("angle", json.int(value__.angle)), 74 | ]) 75 | } 76 | 77 | @internal 78 | pub fn decode__screen_orientation(value__: dynamic.Dynamic) { 79 | use type_ <- result.try(dynamic.field("type", decode__screen_orientation_type)( 80 | value__, 81 | )) 82 | use angle <- result.try(dynamic.field("angle", dynamic.int)(value__)) 83 | 84 | Ok(ScreenOrientation(type_: type_, angle: angle)) 85 | } 86 | 87 | pub type DisplayFeature { 88 | DisplayFeature( 89 | /// Orientation of a display feature in relation to screen 90 | orientation: DisplayFeatureOrientation, 91 | /// The offset from the screen origin in either the x (for vertical 92 | /// orientation) or y (for horizontal orientation) direction. 93 | offset: Int, 94 | /// A display feature may mask content such that it is not physically 95 | /// displayed - this length along with the offset describes this area. 96 | /// A display feature that only splits content will have a 0 mask_length. 97 | mask_length: Int, 98 | ) 99 | } 100 | 101 | /// This type is not part of the protocol spec, it has been generated dynamically 102 | /// to represent the possible values of the enum property `orientation` of `DisplayFeature` 103 | pub type DisplayFeatureOrientation { 104 | DisplayFeatureOrientationVertical 105 | DisplayFeatureOrientationHorizontal 106 | } 107 | 108 | @internal 109 | pub fn encode__display_feature_orientation(value__: DisplayFeatureOrientation) { 110 | case value__ { 111 | DisplayFeatureOrientationVertical -> "vertical" 112 | DisplayFeatureOrientationHorizontal -> "horizontal" 113 | } 114 | |> json.string() 115 | } 116 | 117 | @internal 118 | pub fn decode__display_feature_orientation(value__: dynamic.Dynamic) { 119 | case dynamic.string(value__) { 120 | Ok("vertical") -> Ok(DisplayFeatureOrientationVertical) 121 | Ok("horizontal") -> Ok(DisplayFeatureOrientationHorizontal) 122 | Error(error) -> Error(error) 123 | Ok(other) -> 124 | Error([ 125 | dynamic.DecodeError( 126 | expected: "valid enum property", 127 | found: other, 128 | path: ["enum decoder"], 129 | ), 130 | ]) 131 | } 132 | } 133 | 134 | @internal 135 | pub fn encode__display_feature(value__: DisplayFeature) { 136 | json.object([ 137 | #("orientation", encode__display_feature_orientation(value__.orientation)), 138 | #("offset", json.int(value__.offset)), 139 | #("maskLength", json.int(value__.mask_length)), 140 | ]) 141 | } 142 | 143 | @internal 144 | pub fn decode__display_feature(value__: dynamic.Dynamic) { 145 | use orientation <- result.try(dynamic.field( 146 | "orientation", 147 | decode__display_feature_orientation, 148 | )(value__)) 149 | use offset <- result.try(dynamic.field("offset", dynamic.int)(value__)) 150 | use mask_length <- result.try(dynamic.field("maskLength", dynamic.int)( 151 | value__, 152 | )) 153 | 154 | Ok(DisplayFeature( 155 | orientation: orientation, 156 | offset: offset, 157 | mask_length: mask_length, 158 | )) 159 | } 160 | 161 | pub type DevicePosture { 162 | DevicePosture( 163 | /// Current posture of the device 164 | type_: DevicePostureType, 165 | ) 166 | } 167 | 168 | /// This type is not part of the protocol spec, it has been generated dynamically 169 | /// to represent the possible values of the enum property `type` of `DevicePosture` 170 | pub type DevicePostureType { 171 | DevicePostureTypeContinuous 172 | DevicePostureTypeFolded 173 | } 174 | 175 | @internal 176 | pub fn encode__device_posture_type(value__: DevicePostureType) { 177 | case value__ { 178 | DevicePostureTypeContinuous -> "continuous" 179 | DevicePostureTypeFolded -> "folded" 180 | } 181 | |> json.string() 182 | } 183 | 184 | @internal 185 | pub fn decode__device_posture_type(value__: dynamic.Dynamic) { 186 | case dynamic.string(value__) { 187 | Ok("continuous") -> Ok(DevicePostureTypeContinuous) 188 | Ok("folded") -> Ok(DevicePostureTypeFolded) 189 | Error(error) -> Error(error) 190 | Ok(other) -> 191 | Error([ 192 | dynamic.DecodeError( 193 | expected: "valid enum property", 194 | found: other, 195 | path: ["enum decoder"], 196 | ), 197 | ]) 198 | } 199 | } 200 | 201 | @internal 202 | pub fn encode__device_posture(value__: DevicePosture) { 203 | json.object([#("type", encode__device_posture_type(value__.type_))]) 204 | } 205 | 206 | @internal 207 | pub fn decode__device_posture(value__: dynamic.Dynamic) { 208 | use type_ <- result.try(dynamic.field("type", decode__device_posture_type)( 209 | value__, 210 | )) 211 | 212 | Ok(DevicePosture(type_: type_)) 213 | } 214 | 215 | pub type MediaFeature { 216 | MediaFeature(name: String, value: String) 217 | } 218 | 219 | @internal 220 | pub fn encode__media_feature(value__: MediaFeature) { 221 | json.object([ 222 | #("name", json.string(value__.name)), 223 | #("value", json.string(value__.value)), 224 | ]) 225 | } 226 | 227 | @internal 228 | pub fn decode__media_feature(value__: dynamic.Dynamic) { 229 | use name <- result.try(dynamic.field("name", dynamic.string)(value__)) 230 | use value <- result.try(dynamic.field("value", dynamic.string)(value__)) 231 | 232 | Ok(MediaFeature(name: name, value: value)) 233 | } 234 | 235 | /// Clears the overridden device metrics. 236 | /// 237 | pub fn clear_device_metrics_override(callback__) { 238 | callback__("Emulation.clearDeviceMetricsOverride", option.None) 239 | } 240 | 241 | /// Clears the overridden Geolocation Position and Error. 242 | /// 243 | pub fn clear_geolocation_override(callback__) { 244 | callback__("Emulation.clearGeolocationOverride", option.None) 245 | } 246 | 247 | /// Enables CPU throttling to emulate slow CPUs. 248 | /// 249 | /// Parameters: 250 | /// - `rate` : Throttling rate as a slowdown factor (1 is no throttle, 2 is 2x slowdown, etc). 251 | /// 252 | /// Returns: 253 | /// 254 | pub fn set_cpu_throttling_rate(callback__, rate rate: Float) { 255 | callback__( 256 | "Emulation.setCPUThrottlingRate", 257 | option.Some(json.object([#("rate", json.float(rate))])), 258 | ) 259 | } 260 | 261 | /// Sets or clears an override of the default background color of the frame. This override is used 262 | /// if the content does not specify one. 263 | /// 264 | /// Parameters: 265 | /// - `color` : RGBA of the default background color. If not specified, any existing override will be 266 | /// cleared. 267 | /// 268 | /// Returns: 269 | /// 270 | pub fn set_default_background_color_override( 271 | callback__, 272 | color color: option.Option(dom.RGBA), 273 | ) { 274 | callback__( 275 | "Emulation.setDefaultBackgroundColorOverride", 276 | option.Some(json.object( 277 | [] 278 | |> utils.add_optional(color, fn(inner_value__) { 279 | #("color", dom.encode__rgba(inner_value__)) 280 | }), 281 | )), 282 | ) 283 | } 284 | 285 | /// Overrides the values of device screen dimensions (window.screen.width, window.screen.height, 286 | /// window.innerWidth, window.innerHeight, and "device-width"/"device-height"-related CSS media 287 | /// query results). 288 | /// 289 | /// Parameters: 290 | /// - `width` : Overriding width value in pixels (minimum 0, maximum 10000000). 0 disables the override. 291 | /// - `height` : Overriding height value in pixels (minimum 0, maximum 10000000). 0 disables the override. 292 | /// - `device_scale_factor` : Overriding device scale factor value. 0 disables the override. 293 | /// - `mobile` : Whether to emulate mobile device. This includes viewport meta tag, overlay scrollbars, text 294 | /// autosizing and more. 295 | /// - `screen_orientation` : Screen orientation override. 296 | /// 297 | /// Returns: 298 | /// 299 | pub fn set_device_metrics_override( 300 | callback__, 301 | width width: Int, 302 | height height: Int, 303 | device_scale_factor device_scale_factor: Float, 304 | mobile mobile: Bool, 305 | screen_orientation screen_orientation: option.Option(ScreenOrientation), 306 | ) { 307 | callback__( 308 | "Emulation.setDeviceMetricsOverride", 309 | option.Some(json.object( 310 | [ 311 | #("width", json.int(width)), 312 | #("height", json.int(height)), 313 | #("deviceScaleFactor", json.float(device_scale_factor)), 314 | #("mobile", json.bool(mobile)), 315 | ] 316 | |> utils.add_optional(screen_orientation, fn(inner_value__) { 317 | #("screenOrientation", encode__screen_orientation(inner_value__)) 318 | }), 319 | )), 320 | ) 321 | } 322 | 323 | /// Emulates the given media type or media feature for CSS media queries. 324 | /// 325 | /// Parameters: 326 | /// - `media` : Media type to emulate. Empty string disables the override. 327 | /// - `features` : Media features to emulate. 328 | /// 329 | /// Returns: 330 | /// 331 | pub fn set_emulated_media( 332 | callback__, 333 | media media: option.Option(String), 334 | features features: option.Option(List(MediaFeature)), 335 | ) { 336 | callback__( 337 | "Emulation.setEmulatedMedia", 338 | option.Some(json.object( 339 | [] 340 | |> utils.add_optional(media, fn(inner_value__) { 341 | #("media", json.string(inner_value__)) 342 | }) 343 | |> utils.add_optional(features, fn(inner_value__) { 344 | #("features", json.array(inner_value__, of: encode__media_feature)) 345 | }), 346 | )), 347 | ) 348 | } 349 | 350 | /// Emulates the given vision deficiency. 351 | /// 352 | /// Parameters: 353 | /// - `type_` : Vision deficiency to emulate. Order: best-effort emulations come first, followed by any 354 | /// physiologically accurate emulations for medically recognized color vision deficiencies. 355 | /// 356 | /// Returns: 357 | /// 358 | pub fn set_emulated_vision_deficiency( 359 | callback__, 360 | type_ type_: SetEmulatedVisionDeficiencyType, 361 | ) { 362 | callback__( 363 | "Emulation.setEmulatedVisionDeficiency", 364 | option.Some( 365 | json.object([ 366 | #("type", encode__set_emulated_vision_deficiency_type(type_)), 367 | ]), 368 | ), 369 | ) 370 | } 371 | 372 | /// This type is not part of the protocol spec, it has been generated dynamically 373 | /// to represent the possible values of the enum property `type` of `setEmulatedVisionDeficiency` 374 | pub type SetEmulatedVisionDeficiencyType { 375 | SetEmulatedVisionDeficiencyTypeNone 376 | SetEmulatedVisionDeficiencyTypeBlurredVision 377 | SetEmulatedVisionDeficiencyTypeReducedContrast 378 | SetEmulatedVisionDeficiencyTypeAchromatopsia 379 | SetEmulatedVisionDeficiencyTypeDeuteranopia 380 | SetEmulatedVisionDeficiencyTypeProtanopia 381 | SetEmulatedVisionDeficiencyTypeTritanopia 382 | } 383 | 384 | @internal 385 | pub fn encode__set_emulated_vision_deficiency_type( 386 | value__: SetEmulatedVisionDeficiencyType, 387 | ) { 388 | case value__ { 389 | SetEmulatedVisionDeficiencyTypeNone -> "none" 390 | SetEmulatedVisionDeficiencyTypeBlurredVision -> "blurredVision" 391 | SetEmulatedVisionDeficiencyTypeReducedContrast -> "reducedContrast" 392 | SetEmulatedVisionDeficiencyTypeAchromatopsia -> "achromatopsia" 393 | SetEmulatedVisionDeficiencyTypeDeuteranopia -> "deuteranopia" 394 | SetEmulatedVisionDeficiencyTypeProtanopia -> "protanopia" 395 | SetEmulatedVisionDeficiencyTypeTritanopia -> "tritanopia" 396 | } 397 | |> json.string() 398 | } 399 | 400 | @internal 401 | pub fn decode__set_emulated_vision_deficiency_type(value__: dynamic.Dynamic) { 402 | case dynamic.string(value__) { 403 | Ok("none") -> Ok(SetEmulatedVisionDeficiencyTypeNone) 404 | Ok("blurredVision") -> Ok(SetEmulatedVisionDeficiencyTypeBlurredVision) 405 | Ok("reducedContrast") -> Ok(SetEmulatedVisionDeficiencyTypeReducedContrast) 406 | Ok("achromatopsia") -> Ok(SetEmulatedVisionDeficiencyTypeAchromatopsia) 407 | Ok("deuteranopia") -> Ok(SetEmulatedVisionDeficiencyTypeDeuteranopia) 408 | Ok("protanopia") -> Ok(SetEmulatedVisionDeficiencyTypeProtanopia) 409 | Ok("tritanopia") -> Ok(SetEmulatedVisionDeficiencyTypeTritanopia) 410 | Error(error) -> Error(error) 411 | Ok(other) -> 412 | Error([ 413 | dynamic.DecodeError( 414 | expected: "valid enum property", 415 | found: other, 416 | path: ["enum decoder"], 417 | ), 418 | ]) 419 | } 420 | } 421 | 422 | /// Overrides the Geolocation Position or Error. Omitting any of the parameters emulates position 423 | /// unavailable. 424 | /// 425 | /// Parameters: 426 | /// - `latitude` : Mock latitude 427 | /// - `longitude` : Mock longitude 428 | /// - `accuracy` : Mock accuracy 429 | /// 430 | /// Returns: 431 | /// 432 | pub fn set_geolocation_override( 433 | callback__, 434 | latitude latitude: option.Option(Float), 435 | longitude longitude: option.Option(Float), 436 | accuracy accuracy: option.Option(Float), 437 | ) { 438 | callback__( 439 | "Emulation.setGeolocationOverride", 440 | option.Some(json.object( 441 | [] 442 | |> utils.add_optional(latitude, fn(inner_value__) { 443 | #("latitude", json.float(inner_value__)) 444 | }) 445 | |> utils.add_optional(longitude, fn(inner_value__) { 446 | #("longitude", json.float(inner_value__)) 447 | }) 448 | |> utils.add_optional(accuracy, fn(inner_value__) { 449 | #("accuracy", json.float(inner_value__)) 450 | }), 451 | )), 452 | ) 453 | } 454 | 455 | /// Overrides the Idle state. 456 | /// 457 | /// Parameters: 458 | /// - `is_user_active` : Mock isUserActive 459 | /// - `is_screen_unlocked` : Mock isScreenUnlocked 460 | /// 461 | /// Returns: 462 | /// 463 | pub fn set_idle_override( 464 | callback__, 465 | is_user_active is_user_active: Bool, 466 | is_screen_unlocked is_screen_unlocked: Bool, 467 | ) { 468 | callback__( 469 | "Emulation.setIdleOverride", 470 | option.Some( 471 | json.object([ 472 | #("isUserActive", json.bool(is_user_active)), 473 | #("isScreenUnlocked", json.bool(is_screen_unlocked)), 474 | ]), 475 | ), 476 | ) 477 | } 478 | 479 | /// Clears Idle state overrides. 480 | /// 481 | pub fn clear_idle_override(callback__) { 482 | callback__("Emulation.clearIdleOverride", option.None) 483 | } 484 | 485 | /// Switches script execution in the page. 486 | /// 487 | /// Parameters: 488 | /// - `value` : Whether script execution should be disabled in the page. 489 | /// 490 | /// Returns: 491 | /// 492 | pub fn set_script_execution_disabled(callback__, value value: Bool) { 493 | callback__( 494 | "Emulation.setScriptExecutionDisabled", 495 | option.Some(json.object([#("value", json.bool(value))])), 496 | ) 497 | } 498 | 499 | /// Enables touch on platforms which do not support them. 500 | /// 501 | /// Parameters: 502 | /// - `enabled` : Whether the touch event emulation should be enabled. 503 | /// - `max_touch_points` : Maximum touch points supported. Defaults to one. 504 | /// 505 | /// Returns: 506 | /// 507 | pub fn set_touch_emulation_enabled( 508 | callback__, 509 | enabled enabled: Bool, 510 | max_touch_points max_touch_points: option.Option(Int), 511 | ) { 512 | callback__( 513 | "Emulation.setTouchEmulationEnabled", 514 | option.Some(json.object( 515 | [#("enabled", json.bool(enabled))] 516 | |> utils.add_optional(max_touch_points, fn(inner_value__) { 517 | #("maxTouchPoints", json.int(inner_value__)) 518 | }), 519 | )), 520 | ) 521 | } 522 | 523 | /// Overrides default host system timezone with the specified one. 524 | /// 525 | /// Parameters: 526 | /// - `timezone_id` : The timezone identifier. List of supported timezones: 527 | /// https://source.chromium.org/chromium/chromium/deps/icu.git/+/faee8bc70570192d82d2978a71e2a615788597d1:source/data/misc/metaZones.txt 528 | /// If empty, disables the override and restores default host system timezone. 529 | /// 530 | /// Returns: 531 | /// 532 | pub fn set_timezone_override(callback__, timezone_id timezone_id: String) { 533 | callback__( 534 | "Emulation.setTimezoneOverride", 535 | option.Some(json.object([#("timezoneId", json.string(timezone_id))])), 536 | ) 537 | } 538 | 539 | /// Allows overriding user agent with the given string. 540 | /// `userAgentMetadata` must be set for Client Hint headers to be sent. 541 | /// 542 | /// Parameters: 543 | /// - `user_agent` : User agent to use. 544 | /// - `accept_language` : Browser language to emulate. 545 | /// - `platform` : The platform navigator.platform should return. 546 | /// 547 | /// Returns: 548 | /// 549 | pub fn set_user_agent_override( 550 | callback__, 551 | user_agent user_agent: String, 552 | accept_language accept_language: option.Option(String), 553 | platform platform: option.Option(String), 554 | ) { 555 | callback__( 556 | "Emulation.setUserAgentOverride", 557 | option.Some(json.object( 558 | [#("userAgent", json.string(user_agent))] 559 | |> utils.add_optional(accept_language, fn(inner_value__) { 560 | #("acceptLanguage", json.string(inner_value__)) 561 | }) 562 | |> utils.add_optional(platform, fn(inner_value__) { 563 | #("platform", json.string(inner_value__)) 564 | }), 565 | )), 566 | ) 567 | } 568 | -------------------------------------------------------------------------------- /src/chrobot/protocol/fetch.gleam: -------------------------------------------------------------------------------- 1 | //// > ⚙️ This module was generated from the Chrome DevTools Protocol version **1.3** 2 | //// ## Fetch Domain 3 | //// 4 | //// A domain for letting clients substitute browser's network layer with client code. 5 | //// 6 | //// [📖 View this domain on the DevTools Protocol API Docs](https://chromedevtools.github.io/devtools-protocol/1-3/Fetch/) 7 | 8 | // --------------------------------------------------------------------------- 9 | // | !!!!!! This is an autogenerated file - Do not edit manually !!!!!! | 10 | // | Run `codegen.sh` to regenerate. | 11 | // --------------------------------------------------------------------------- 12 | 13 | import chrobot/chrome 14 | import chrobot/internal/utils 15 | import chrobot/protocol/io 16 | import chrobot/protocol/network 17 | import gleam/dynamic 18 | import gleam/json 19 | import gleam/option 20 | import gleam/result 21 | 22 | /// Unique request identifier. 23 | pub type RequestId { 24 | RequestId(String) 25 | } 26 | 27 | @internal 28 | pub fn encode__request_id(value__: RequestId) { 29 | case value__ { 30 | RequestId(inner_value__) -> json.string(inner_value__) 31 | } 32 | } 33 | 34 | @internal 35 | pub fn decode__request_id(value__: dynamic.Dynamic) { 36 | value__ |> dynamic.decode1(RequestId, dynamic.string) 37 | } 38 | 39 | /// Stages of the request to handle. Request will intercept before the request is 40 | /// sent. Response will intercept after the response is received (but before response 41 | /// body is received). 42 | pub type RequestStage { 43 | RequestStageRequest 44 | RequestStageResponse 45 | } 46 | 47 | @internal 48 | pub fn encode__request_stage(value__: RequestStage) { 49 | case value__ { 50 | RequestStageRequest -> "Request" 51 | RequestStageResponse -> "Response" 52 | } 53 | |> json.string() 54 | } 55 | 56 | @internal 57 | pub fn decode__request_stage(value__: dynamic.Dynamic) { 58 | case dynamic.string(value__) { 59 | Ok("Request") -> Ok(RequestStageRequest) 60 | Ok("Response") -> Ok(RequestStageResponse) 61 | Error(error) -> Error(error) 62 | Ok(other) -> 63 | Error([ 64 | dynamic.DecodeError( 65 | expected: "valid enum property", 66 | found: other, 67 | path: ["enum decoder"], 68 | ), 69 | ]) 70 | } 71 | } 72 | 73 | pub type RequestPattern { 74 | RequestPattern( 75 | /// Wildcards (`'*'` -> zero or more, `'?'` -> exactly one) are allowed. Escape character is 76 | /// backslash. Omitting is equivalent to `"*"`. 77 | url_pattern: option.Option(String), 78 | /// If set, only requests for matching resource types will be intercepted. 79 | resource_type: option.Option(network.ResourceType), 80 | /// Stage at which to begin intercepting requests. Default is Request. 81 | request_stage: option.Option(RequestStage), 82 | ) 83 | } 84 | 85 | @internal 86 | pub fn encode__request_pattern(value__: RequestPattern) { 87 | json.object( 88 | [] 89 | |> utils.add_optional(value__.url_pattern, fn(inner_value__) { 90 | #("urlPattern", json.string(inner_value__)) 91 | }) 92 | |> utils.add_optional(value__.resource_type, fn(inner_value__) { 93 | #("resourceType", network.encode__resource_type(inner_value__)) 94 | }) 95 | |> utils.add_optional(value__.request_stage, fn(inner_value__) { 96 | #("requestStage", encode__request_stage(inner_value__)) 97 | }), 98 | ) 99 | } 100 | 101 | @internal 102 | pub fn decode__request_pattern(value__: dynamic.Dynamic) { 103 | use url_pattern <- result.try(dynamic.optional_field( 104 | "urlPattern", 105 | dynamic.string, 106 | )(value__)) 107 | use resource_type <- result.try(dynamic.optional_field( 108 | "resourceType", 109 | network.decode__resource_type, 110 | )(value__)) 111 | use request_stage <- result.try(dynamic.optional_field( 112 | "requestStage", 113 | decode__request_stage, 114 | )(value__)) 115 | 116 | Ok(RequestPattern( 117 | url_pattern: url_pattern, 118 | resource_type: resource_type, 119 | request_stage: request_stage, 120 | )) 121 | } 122 | 123 | /// Response HTTP header entry 124 | pub type HeaderEntry { 125 | HeaderEntry(name: String, value: String) 126 | } 127 | 128 | @internal 129 | pub fn encode__header_entry(value__: HeaderEntry) { 130 | json.object([ 131 | #("name", json.string(value__.name)), 132 | #("value", json.string(value__.value)), 133 | ]) 134 | } 135 | 136 | @internal 137 | pub fn decode__header_entry(value__: dynamic.Dynamic) { 138 | use name <- result.try(dynamic.field("name", dynamic.string)(value__)) 139 | use value <- result.try(dynamic.field("value", dynamic.string)(value__)) 140 | 141 | Ok(HeaderEntry(name: name, value: value)) 142 | } 143 | 144 | /// Authorization challenge for HTTP status code 401 or 407. 145 | pub type AuthChallenge { 146 | AuthChallenge( 147 | /// Source of the authentication challenge. 148 | source: option.Option(AuthChallengeSource), 149 | /// Origin of the challenger. 150 | origin: String, 151 | /// The authentication scheme used, such as basic or digest 152 | scheme: String, 153 | /// The realm of the challenge. May be empty. 154 | realm: String, 155 | ) 156 | } 157 | 158 | /// This type is not part of the protocol spec, it has been generated dynamically 159 | /// to represent the possible values of the enum property `source` of `AuthChallenge` 160 | pub type AuthChallengeSource { 161 | AuthChallengeSourceServer 162 | AuthChallengeSourceProxy 163 | } 164 | 165 | @internal 166 | pub fn encode__auth_challenge_source(value__: AuthChallengeSource) { 167 | case value__ { 168 | AuthChallengeSourceServer -> "Server" 169 | AuthChallengeSourceProxy -> "Proxy" 170 | } 171 | |> json.string() 172 | } 173 | 174 | @internal 175 | pub fn decode__auth_challenge_source(value__: dynamic.Dynamic) { 176 | case dynamic.string(value__) { 177 | Ok("Server") -> Ok(AuthChallengeSourceServer) 178 | Ok("Proxy") -> Ok(AuthChallengeSourceProxy) 179 | Error(error) -> Error(error) 180 | Ok(other) -> 181 | Error([ 182 | dynamic.DecodeError( 183 | expected: "valid enum property", 184 | found: other, 185 | path: ["enum decoder"], 186 | ), 187 | ]) 188 | } 189 | } 190 | 191 | @internal 192 | pub fn encode__auth_challenge(value__: AuthChallenge) { 193 | json.object( 194 | [ 195 | #("origin", json.string(value__.origin)), 196 | #("scheme", json.string(value__.scheme)), 197 | #("realm", json.string(value__.realm)), 198 | ] 199 | |> utils.add_optional(value__.source, fn(inner_value__) { 200 | #("source", encode__auth_challenge_source(inner_value__)) 201 | }), 202 | ) 203 | } 204 | 205 | @internal 206 | pub fn decode__auth_challenge(value__: dynamic.Dynamic) { 207 | use source <- result.try(dynamic.optional_field( 208 | "source", 209 | decode__auth_challenge_source, 210 | )(value__)) 211 | use origin <- result.try(dynamic.field("origin", dynamic.string)(value__)) 212 | use scheme <- result.try(dynamic.field("scheme", dynamic.string)(value__)) 213 | use realm <- result.try(dynamic.field("realm", dynamic.string)(value__)) 214 | 215 | Ok(AuthChallenge(source: source, origin: origin, scheme: scheme, realm: realm)) 216 | } 217 | 218 | /// Response to an AuthChallenge. 219 | pub type AuthChallengeResponse { 220 | AuthChallengeResponse( 221 | /// The decision on what to do in response to the authorization challenge. Default means 222 | /// deferring to the default behavior of the net stack, which will likely either the Cancel 223 | /// authentication or display a popup dialog box. 224 | response: AuthChallengeResponseResponse, 225 | /// The username to provide, possibly empty. Should only be set if response is 226 | /// ProvideCredentials. 227 | username: option.Option(String), 228 | /// The password to provide, possibly empty. Should only be set if response is 229 | /// ProvideCredentials. 230 | password: option.Option(String), 231 | ) 232 | } 233 | 234 | /// This type is not part of the protocol spec, it has been generated dynamically 235 | /// to represent the possible values of the enum property `response` of `AuthChallengeResponse` 236 | pub type AuthChallengeResponseResponse { 237 | AuthChallengeResponseResponseDefault 238 | AuthChallengeResponseResponseCancelAuth 239 | AuthChallengeResponseResponseProvideCredentials 240 | } 241 | 242 | @internal 243 | pub fn encode__auth_challenge_response_response( 244 | value__: AuthChallengeResponseResponse, 245 | ) { 246 | case value__ { 247 | AuthChallengeResponseResponseDefault -> "Default" 248 | AuthChallengeResponseResponseCancelAuth -> "CancelAuth" 249 | AuthChallengeResponseResponseProvideCredentials -> "ProvideCredentials" 250 | } 251 | |> json.string() 252 | } 253 | 254 | @internal 255 | pub fn decode__auth_challenge_response_response(value__: dynamic.Dynamic) { 256 | case dynamic.string(value__) { 257 | Ok("Default") -> Ok(AuthChallengeResponseResponseDefault) 258 | Ok("CancelAuth") -> Ok(AuthChallengeResponseResponseCancelAuth) 259 | Ok("ProvideCredentials") -> 260 | Ok(AuthChallengeResponseResponseProvideCredentials) 261 | Error(error) -> Error(error) 262 | Ok(other) -> 263 | Error([ 264 | dynamic.DecodeError( 265 | expected: "valid enum property", 266 | found: other, 267 | path: ["enum decoder"], 268 | ), 269 | ]) 270 | } 271 | } 272 | 273 | @internal 274 | pub fn encode__auth_challenge_response(value__: AuthChallengeResponse) { 275 | json.object( 276 | [#("response", encode__auth_challenge_response_response(value__.response))] 277 | |> utils.add_optional(value__.username, fn(inner_value__) { 278 | #("username", json.string(inner_value__)) 279 | }) 280 | |> utils.add_optional(value__.password, fn(inner_value__) { 281 | #("password", json.string(inner_value__)) 282 | }), 283 | ) 284 | } 285 | 286 | @internal 287 | pub fn decode__auth_challenge_response(value__: dynamic.Dynamic) { 288 | use response <- result.try(dynamic.field( 289 | "response", 290 | decode__auth_challenge_response_response, 291 | )(value__)) 292 | use username <- result.try(dynamic.optional_field("username", dynamic.string)( 293 | value__, 294 | )) 295 | use password <- result.try(dynamic.optional_field("password", dynamic.string)( 296 | value__, 297 | )) 298 | 299 | Ok(AuthChallengeResponse( 300 | response: response, 301 | username: username, 302 | password: password, 303 | )) 304 | } 305 | 306 | /// This type is not part of the protocol spec, it has been generated dynamically 307 | /// to represent the response to the command `get_response_body` 308 | pub type GetResponseBodyResponse { 309 | GetResponseBodyResponse( 310 | /// Response body. 311 | body: String, 312 | /// True, if content was sent as base64. 313 | base64_encoded: Bool, 314 | ) 315 | } 316 | 317 | @internal 318 | pub fn decode__get_response_body_response(value__: dynamic.Dynamic) { 319 | use body <- result.try(dynamic.field("body", dynamic.string)(value__)) 320 | use base64_encoded <- result.try(dynamic.field("base64Encoded", dynamic.bool)( 321 | value__, 322 | )) 323 | 324 | Ok(GetResponseBodyResponse(body: body, base64_encoded: base64_encoded)) 325 | } 326 | 327 | /// This type is not part of the protocol spec, it has been generated dynamically 328 | /// to represent the response to the command `take_response_body_as_stream` 329 | pub type TakeResponseBodyAsStreamResponse { 330 | TakeResponseBodyAsStreamResponse(stream: io.StreamHandle) 331 | } 332 | 333 | @internal 334 | pub fn decode__take_response_body_as_stream_response(value__: dynamic.Dynamic) { 335 | use stream <- result.try(dynamic.field("stream", io.decode__stream_handle)( 336 | value__, 337 | )) 338 | 339 | Ok(TakeResponseBodyAsStreamResponse(stream: stream)) 340 | } 341 | 342 | /// Disables the fetch domain. 343 | /// 344 | pub fn disable(callback__) { 345 | callback__("Fetch.disable", option.None) 346 | } 347 | 348 | /// Enables issuing of requestPaused events. A request will be paused until client 349 | /// calls one of failRequest, fulfillRequest or continueRequest/continueWithAuth. 350 | /// 351 | /// Parameters: 352 | /// - `patterns` : If specified, only requests matching any of these patterns will produce 353 | /// fetchRequested event and will be paused until clients response. If not set, 354 | /// all requests will be affected. 355 | /// - `handle_auth_requests` : If true, authRequired events will be issued and requests will be paused 356 | /// expecting a call to continueWithAuth. 357 | /// 358 | /// Returns: 359 | /// 360 | pub fn enable( 361 | callback__, 362 | patterns patterns: option.Option(List(RequestPattern)), 363 | handle_auth_requests handle_auth_requests: option.Option(Bool), 364 | ) { 365 | callback__( 366 | "Fetch.enable", 367 | option.Some(json.object( 368 | [] 369 | |> utils.add_optional(patterns, fn(inner_value__) { 370 | #("patterns", json.array(inner_value__, of: encode__request_pattern)) 371 | }) 372 | |> utils.add_optional(handle_auth_requests, fn(inner_value__) { 373 | #("handleAuthRequests", json.bool(inner_value__)) 374 | }), 375 | )), 376 | ) 377 | } 378 | 379 | /// Causes the request to fail with specified reason. 380 | /// 381 | /// Parameters: 382 | /// - `request_id` : An id the client received in requestPaused event. 383 | /// - `error_reason` : Causes the request to fail with the given reason. 384 | /// 385 | /// Returns: 386 | /// 387 | pub fn fail_request( 388 | callback__, 389 | request_id request_id: RequestId, 390 | error_reason error_reason: network.ErrorReason, 391 | ) { 392 | callback__( 393 | "Fetch.failRequest", 394 | option.Some( 395 | json.object([ 396 | #("requestId", encode__request_id(request_id)), 397 | #("errorReason", network.encode__error_reason(error_reason)), 398 | ]), 399 | ), 400 | ) 401 | } 402 | 403 | /// Provides response to the request. 404 | /// 405 | /// Parameters: 406 | /// - `request_id` : An id the client received in requestPaused event. 407 | /// - `response_code` : An HTTP response code. 408 | /// - `response_headers` : Response headers. 409 | /// - `binary_response_headers` : Alternative way of specifying response headers as a \0-separated 410 | /// series of name: value pairs. Prefer the above method unless you 411 | /// need to represent some non-UTF8 values that can't be transmitted 412 | /// over the protocol as text. (Encoded as a base64 string when passed over JSON) 413 | /// - `body` : A response body. If absent, original response body will be used if 414 | /// the request is intercepted at the response stage and empty body 415 | /// will be used if the request is intercepted at the request stage. (Encoded as a base64 string when passed over JSON) 416 | /// - `response_phrase` : A textual representation of responseCode. 417 | /// If absent, a standard phrase matching responseCode is used. 418 | /// 419 | /// Returns: 420 | /// 421 | pub fn fulfill_request( 422 | callback__, 423 | request_id request_id: RequestId, 424 | response_code response_code: Int, 425 | response_headers response_headers: option.Option(List(HeaderEntry)), 426 | binary_response_headers binary_response_headers: option.Option(String), 427 | body body: option.Option(String), 428 | response_phrase response_phrase: option.Option(String), 429 | ) { 430 | callback__( 431 | "Fetch.fulfillRequest", 432 | option.Some(json.object( 433 | [ 434 | #("requestId", encode__request_id(request_id)), 435 | #("responseCode", json.int(response_code)), 436 | ] 437 | |> utils.add_optional(response_headers, fn(inner_value__) { 438 | #( 439 | "responseHeaders", 440 | json.array(inner_value__, of: encode__header_entry), 441 | ) 442 | }) 443 | |> utils.add_optional(binary_response_headers, fn(inner_value__) { 444 | #("binaryResponseHeaders", json.string(inner_value__)) 445 | }) 446 | |> utils.add_optional(body, fn(inner_value__) { 447 | #("body", json.string(inner_value__)) 448 | }) 449 | |> utils.add_optional(response_phrase, fn(inner_value__) { 450 | #("responsePhrase", json.string(inner_value__)) 451 | }), 452 | )), 453 | ) 454 | } 455 | 456 | /// Continues the request, optionally modifying some of its parameters. 457 | /// 458 | /// Parameters: 459 | /// - `request_id` : An id the client received in requestPaused event. 460 | /// - `url` : If set, the request url will be modified in a way that's not observable by page. 461 | /// - `method` : If set, the request method is overridden. 462 | /// - `post_data` : If set, overrides the post data in the request. (Encoded as a base64 string when passed over JSON) 463 | /// - `headers` : If set, overrides the request headers. Note that the overrides do not 464 | /// extend to subsequent redirect hops, if a redirect happens. Another override 465 | /// may be applied to a different request produced by a redirect. 466 | /// 467 | /// Returns: 468 | /// 469 | pub fn continue_request( 470 | callback__, 471 | request_id request_id: RequestId, 472 | url url: option.Option(String), 473 | method method: option.Option(String), 474 | post_data post_data: option.Option(String), 475 | headers headers: option.Option(List(HeaderEntry)), 476 | ) { 477 | callback__( 478 | "Fetch.continueRequest", 479 | option.Some(json.object( 480 | [#("requestId", encode__request_id(request_id))] 481 | |> utils.add_optional(url, fn(inner_value__) { 482 | #("url", json.string(inner_value__)) 483 | }) 484 | |> utils.add_optional(method, fn(inner_value__) { 485 | #("method", json.string(inner_value__)) 486 | }) 487 | |> utils.add_optional(post_data, fn(inner_value__) { 488 | #("postData", json.string(inner_value__)) 489 | }) 490 | |> utils.add_optional(headers, fn(inner_value__) { 491 | #("headers", json.array(inner_value__, of: encode__header_entry)) 492 | }), 493 | )), 494 | ) 495 | } 496 | 497 | /// Continues a request supplying authChallengeResponse following authRequired event. 498 | /// 499 | /// Parameters: 500 | /// - `request_id` : An id the client received in authRequired event. 501 | /// - `auth_challenge_response` : Response to with an authChallenge. 502 | /// 503 | /// Returns: 504 | /// 505 | pub fn continue_with_auth( 506 | callback__, 507 | request_id request_id: RequestId, 508 | auth_challenge_response auth_challenge_response: AuthChallengeResponse, 509 | ) { 510 | callback__( 511 | "Fetch.continueWithAuth", 512 | option.Some( 513 | json.object([ 514 | #("requestId", encode__request_id(request_id)), 515 | #( 516 | "authChallengeResponse", 517 | encode__auth_challenge_response(auth_challenge_response), 518 | ), 519 | ]), 520 | ), 521 | ) 522 | } 523 | 524 | /// Causes the body of the response to be received from the server and 525 | /// returned as a single string. May only be issued for a request that 526 | /// is paused in the Response stage and is mutually exclusive with 527 | /// takeResponseBodyForInterceptionAsStream. Calling other methods that 528 | /// affect the request or disabling fetch domain before body is received 529 | /// results in an undefined behavior. 530 | /// Note that the response body is not available for redirects. Requests 531 | /// paused in the _redirect received_ state may be differentiated by 532 | /// `responseCode` and presence of `location` response header, see 533 | /// comments to `requestPaused` for details. 534 | /// 535 | /// Parameters: 536 | /// - `request_id` : Identifier for the intercepted request to get body for. 537 | /// 538 | /// Returns: 539 | /// - `body` : Response body. 540 | /// - `base64_encoded` : True, if content was sent as base64. 541 | /// 542 | pub fn get_response_body(callback__, request_id request_id: RequestId) { 543 | use result__ <- result.try(callback__( 544 | "Fetch.getResponseBody", 545 | option.Some(json.object([#("requestId", encode__request_id(request_id))])), 546 | )) 547 | 548 | decode__get_response_body_response(result__) 549 | |> result.replace_error(chrome.ProtocolError) 550 | } 551 | 552 | /// Returns a handle to the stream representing the response body. 553 | /// The request must be paused in the HeadersReceived stage. 554 | /// Note that after this command the request can't be continued 555 | /// as is -- client either needs to cancel it or to provide the 556 | /// response body. 557 | /// The stream only supports sequential read, IO.read will fail if the position 558 | /// is specified. 559 | /// This method is mutually exclusive with getResponseBody. 560 | /// Calling other methods that affect the request or disabling fetch 561 | /// domain before body is received results in an undefined behavior. 562 | /// 563 | /// Parameters: 564 | /// - `request_id` 565 | /// 566 | /// Returns: 567 | /// - `stream` 568 | /// 569 | pub fn take_response_body_as_stream( 570 | callback__, 571 | request_id request_id: RequestId, 572 | ) { 573 | use result__ <- result.try(callback__( 574 | "Fetch.takeResponseBodyAsStream", 575 | option.Some(json.object([#("requestId", encode__request_id(request_id))])), 576 | )) 577 | 578 | decode__take_response_body_as_stream_response(result__) 579 | |> result.replace_error(chrome.ProtocolError) 580 | } 581 | -------------------------------------------------------------------------------- /src/chrobot/protocol/io.gleam: -------------------------------------------------------------------------------- 1 | //// > ⚙️ This module was generated from the Chrome DevTools Protocol version **1.3** 2 | //// ## IO Domain 3 | //// 4 | //// Input/Output operations for streams produced by DevTools. 5 | //// 6 | //// [📖 View this domain on the DevTools Protocol API Docs](https://chromedevtools.github.io/devtools-protocol/1-3/IO/) 7 | 8 | // --------------------------------------------------------------------------- 9 | // | !!!!!! This is an autogenerated file - Do not edit manually !!!!!! | 10 | // | Run `codegen.sh` to regenerate. | 11 | // --------------------------------------------------------------------------- 12 | 13 | import chrobot/chrome 14 | import chrobot/internal/utils 15 | import chrobot/protocol/runtime 16 | import gleam/dynamic 17 | import gleam/json 18 | import gleam/option 19 | import gleam/result 20 | 21 | /// This is either obtained from another method or specified as `blob:` where 22 | /// `` is an UUID of a Blob. 23 | pub type StreamHandle { 24 | StreamHandle(String) 25 | } 26 | 27 | @internal 28 | pub fn encode__stream_handle(value__: StreamHandle) { 29 | case value__ { 30 | StreamHandle(inner_value__) -> json.string(inner_value__) 31 | } 32 | } 33 | 34 | @internal 35 | pub fn decode__stream_handle(value__: dynamic.Dynamic) { 36 | value__ |> dynamic.decode1(StreamHandle, dynamic.string) 37 | } 38 | 39 | /// This type is not part of the protocol spec, it has been generated dynamically 40 | /// to represent the response to the command `read` 41 | pub type ReadResponse { 42 | ReadResponse( 43 | /// Set if the data is base64-encoded 44 | base64_encoded: option.Option(Bool), 45 | /// Data that were read. 46 | data: String, 47 | /// Set if the end-of-file condition occurred while reading. 48 | eof: Bool, 49 | ) 50 | } 51 | 52 | @internal 53 | pub fn decode__read_response(value__: dynamic.Dynamic) { 54 | use base64_encoded <- result.try(dynamic.optional_field( 55 | "base64Encoded", 56 | dynamic.bool, 57 | )(value__)) 58 | use data <- result.try(dynamic.field("data", dynamic.string)(value__)) 59 | use eof <- result.try(dynamic.field("eof", dynamic.bool)(value__)) 60 | 61 | Ok(ReadResponse(base64_encoded: base64_encoded, data: data, eof: eof)) 62 | } 63 | 64 | /// This type is not part of the protocol spec, it has been generated dynamically 65 | /// to represent the response to the command `resolve_blob` 66 | pub type ResolveBlobResponse { 67 | ResolveBlobResponse( 68 | /// UUID of the specified Blob. 69 | uuid: String, 70 | ) 71 | } 72 | 73 | @internal 74 | pub fn decode__resolve_blob_response(value__: dynamic.Dynamic) { 75 | use uuid <- result.try(dynamic.field("uuid", dynamic.string)(value__)) 76 | 77 | Ok(ResolveBlobResponse(uuid: uuid)) 78 | } 79 | 80 | /// Close the stream, discard any temporary backing storage. 81 | /// 82 | /// Parameters: 83 | /// - `handle` : Handle of the stream to close. 84 | /// 85 | /// Returns: 86 | /// 87 | pub fn close(callback__, handle handle: StreamHandle) { 88 | callback__( 89 | "IO.close", 90 | option.Some(json.object([#("handle", encode__stream_handle(handle))])), 91 | ) 92 | } 93 | 94 | /// Read a chunk of the stream 95 | /// 96 | /// Parameters: 97 | /// - `handle` : Handle of the stream to read. 98 | /// - `offset` : Seek to the specified offset before reading (if not specified, proceed with offset 99 | /// following the last read). Some types of streams may only support sequential reads. 100 | /// - `size` : Maximum number of bytes to read (left upon the agent discretion if not specified). 101 | /// 102 | /// Returns: 103 | /// - `base64_encoded` : Set if the data is base64-encoded 104 | /// - `data` : Data that were read. 105 | /// - `eof` : Set if the end-of-file condition occurred while reading. 106 | /// 107 | pub fn read( 108 | callback__, 109 | handle handle: StreamHandle, 110 | offset offset: option.Option(Int), 111 | size size: option.Option(Int), 112 | ) { 113 | use result__ <- result.try(callback__( 114 | "IO.read", 115 | option.Some(json.object( 116 | [#("handle", encode__stream_handle(handle))] 117 | |> utils.add_optional(offset, fn(inner_value__) { 118 | #("offset", json.int(inner_value__)) 119 | }) 120 | |> utils.add_optional(size, fn(inner_value__) { 121 | #("size", json.int(inner_value__)) 122 | }), 123 | )), 124 | )) 125 | 126 | decode__read_response(result__) 127 | |> result.replace_error(chrome.ProtocolError) 128 | } 129 | 130 | /// Return UUID of Blob object specified by a remote object id. 131 | /// 132 | /// Parameters: 133 | /// - `object_id` : Object id of a Blob object wrapper. 134 | /// 135 | /// Returns: 136 | /// - `uuid` : UUID of the specified Blob. 137 | /// 138 | pub fn resolve_blob(callback__, object_id object_id: runtime.RemoteObjectId) { 139 | use result__ <- result.try(callback__( 140 | "IO.resolveBlob", 141 | option.Some( 142 | json.object([#("objectId", runtime.encode__remote_object_id(object_id))]), 143 | ), 144 | )) 145 | 146 | decode__resolve_blob_response(result__) 147 | |> result.replace_error(chrome.ProtocolError) 148 | } 149 | -------------------------------------------------------------------------------- /src/chrobot/protocol/log.gleam: -------------------------------------------------------------------------------- 1 | //// > ⚙️ This module was generated from the Chrome DevTools Protocol version **1.3** 2 | //// ## Log Domain 3 | //// 4 | //// Provides access to log entries. 5 | //// 6 | //// [📖 View this domain on the DevTools Protocol API Docs](https://chromedevtools.github.io/devtools-protocol/1-3/Log/) 7 | 8 | // --------------------------------------------------------------------------- 9 | // | !!!!!! This is an autogenerated file - Do not edit manually !!!!!! | 10 | // | Run `codegen.sh` to regenerate. | 11 | // --------------------------------------------------------------------------- 12 | 13 | import chrobot/internal/utils 14 | import chrobot/protocol/network 15 | import chrobot/protocol/runtime 16 | import gleam/dynamic 17 | import gleam/json 18 | import gleam/option 19 | import gleam/result 20 | 21 | /// Log entry. 22 | pub type LogEntry { 23 | LogEntry( 24 | /// Log entry source. 25 | source: LogEntrySource, 26 | /// Log entry severity. 27 | level: LogEntryLevel, 28 | /// Logged text. 29 | text: String, 30 | category: option.Option(LogEntryCategory), 31 | /// Timestamp when this entry was added. 32 | timestamp: runtime.Timestamp, 33 | /// URL of the resource if known. 34 | url: option.Option(String), 35 | /// Line number in the resource. 36 | line_number: option.Option(Int), 37 | /// JavaScript stack trace. 38 | stack_trace: option.Option(runtime.StackTrace), 39 | /// Identifier of the network request associated with this entry. 40 | network_request_id: option.Option(network.RequestId), 41 | /// Identifier of the worker associated with this entry. 42 | worker_id: option.Option(String), 43 | /// Call arguments. 44 | args: option.Option(List(runtime.RemoteObject)), 45 | ) 46 | } 47 | 48 | /// This type is not part of the protocol spec, it has been generated dynamically 49 | /// to represent the possible values of the enum property `source` of `LogEntry` 50 | pub type LogEntrySource { 51 | LogEntrySourceXml 52 | LogEntrySourceJavascript 53 | LogEntrySourceNetwork 54 | LogEntrySourceStorage 55 | LogEntrySourceAppcache 56 | LogEntrySourceRendering 57 | LogEntrySourceSecurity 58 | LogEntrySourceDeprecation 59 | LogEntrySourceWorker 60 | LogEntrySourceViolation 61 | LogEntrySourceIntervention 62 | LogEntrySourceRecommendation 63 | LogEntrySourceOther 64 | } 65 | 66 | @internal 67 | pub fn encode__log_entry_source(value__: LogEntrySource) { 68 | case value__ { 69 | LogEntrySourceXml -> "xml" 70 | LogEntrySourceJavascript -> "javascript" 71 | LogEntrySourceNetwork -> "network" 72 | LogEntrySourceStorage -> "storage" 73 | LogEntrySourceAppcache -> "appcache" 74 | LogEntrySourceRendering -> "rendering" 75 | LogEntrySourceSecurity -> "security" 76 | LogEntrySourceDeprecation -> "deprecation" 77 | LogEntrySourceWorker -> "worker" 78 | LogEntrySourceViolation -> "violation" 79 | LogEntrySourceIntervention -> "intervention" 80 | LogEntrySourceRecommendation -> "recommendation" 81 | LogEntrySourceOther -> "other" 82 | } 83 | |> json.string() 84 | } 85 | 86 | @internal 87 | pub fn decode__log_entry_source(value__: dynamic.Dynamic) { 88 | case dynamic.string(value__) { 89 | Ok("xml") -> Ok(LogEntrySourceXml) 90 | Ok("javascript") -> Ok(LogEntrySourceJavascript) 91 | Ok("network") -> Ok(LogEntrySourceNetwork) 92 | Ok("storage") -> Ok(LogEntrySourceStorage) 93 | Ok("appcache") -> Ok(LogEntrySourceAppcache) 94 | Ok("rendering") -> Ok(LogEntrySourceRendering) 95 | Ok("security") -> Ok(LogEntrySourceSecurity) 96 | Ok("deprecation") -> Ok(LogEntrySourceDeprecation) 97 | Ok("worker") -> Ok(LogEntrySourceWorker) 98 | Ok("violation") -> Ok(LogEntrySourceViolation) 99 | Ok("intervention") -> Ok(LogEntrySourceIntervention) 100 | Ok("recommendation") -> Ok(LogEntrySourceRecommendation) 101 | Ok("other") -> Ok(LogEntrySourceOther) 102 | Error(error) -> Error(error) 103 | Ok(other) -> 104 | Error([ 105 | dynamic.DecodeError( 106 | expected: "valid enum property", 107 | found: other, 108 | path: ["enum decoder"], 109 | ), 110 | ]) 111 | } 112 | } 113 | 114 | /// This type is not part of the protocol spec, it has been generated dynamically 115 | /// to represent the possible values of the enum property `level` of `LogEntry` 116 | pub type LogEntryLevel { 117 | LogEntryLevelVerbose 118 | LogEntryLevelInfo 119 | LogEntryLevelWarning 120 | LogEntryLevelError 121 | } 122 | 123 | @internal 124 | pub fn encode__log_entry_level(value__: LogEntryLevel) { 125 | case value__ { 126 | LogEntryLevelVerbose -> "verbose" 127 | LogEntryLevelInfo -> "info" 128 | LogEntryLevelWarning -> "warning" 129 | LogEntryLevelError -> "error" 130 | } 131 | |> json.string() 132 | } 133 | 134 | @internal 135 | pub fn decode__log_entry_level(value__: dynamic.Dynamic) { 136 | case dynamic.string(value__) { 137 | Ok("verbose") -> Ok(LogEntryLevelVerbose) 138 | Ok("info") -> Ok(LogEntryLevelInfo) 139 | Ok("warning") -> Ok(LogEntryLevelWarning) 140 | Ok("error") -> Ok(LogEntryLevelError) 141 | Error(error) -> Error(error) 142 | Ok(other) -> 143 | Error([ 144 | dynamic.DecodeError( 145 | expected: "valid enum property", 146 | found: other, 147 | path: ["enum decoder"], 148 | ), 149 | ]) 150 | } 151 | } 152 | 153 | /// This type is not part of the protocol spec, it has been generated dynamically 154 | /// to represent the possible values of the enum property `category` of `LogEntry` 155 | pub type LogEntryCategory { 156 | LogEntryCategoryCors 157 | } 158 | 159 | @internal 160 | pub fn encode__log_entry_category(value__: LogEntryCategory) { 161 | case value__ { 162 | LogEntryCategoryCors -> "cors" 163 | } 164 | |> json.string() 165 | } 166 | 167 | @internal 168 | pub fn decode__log_entry_category(value__: dynamic.Dynamic) { 169 | case dynamic.string(value__) { 170 | Ok("cors") -> Ok(LogEntryCategoryCors) 171 | Error(error) -> Error(error) 172 | Ok(other) -> 173 | Error([ 174 | dynamic.DecodeError( 175 | expected: "valid enum property", 176 | found: other, 177 | path: ["enum decoder"], 178 | ), 179 | ]) 180 | } 181 | } 182 | 183 | @internal 184 | pub fn encode__log_entry(value__: LogEntry) { 185 | json.object( 186 | [ 187 | #("source", encode__log_entry_source(value__.source)), 188 | #("level", encode__log_entry_level(value__.level)), 189 | #("text", json.string(value__.text)), 190 | #("timestamp", runtime.encode__timestamp(value__.timestamp)), 191 | ] 192 | |> utils.add_optional(value__.category, fn(inner_value__) { 193 | #("category", encode__log_entry_category(inner_value__)) 194 | }) 195 | |> utils.add_optional(value__.url, fn(inner_value__) { 196 | #("url", json.string(inner_value__)) 197 | }) 198 | |> utils.add_optional(value__.line_number, fn(inner_value__) { 199 | #("lineNumber", json.int(inner_value__)) 200 | }) 201 | |> utils.add_optional(value__.stack_trace, fn(inner_value__) { 202 | #("stackTrace", runtime.encode__stack_trace(inner_value__)) 203 | }) 204 | |> utils.add_optional(value__.network_request_id, fn(inner_value__) { 205 | #("networkRequestId", network.encode__request_id(inner_value__)) 206 | }) 207 | |> utils.add_optional(value__.worker_id, fn(inner_value__) { 208 | #("workerId", json.string(inner_value__)) 209 | }) 210 | |> utils.add_optional(value__.args, fn(inner_value__) { 211 | #("args", json.array(inner_value__, of: runtime.encode__remote_object)) 212 | }), 213 | ) 214 | } 215 | 216 | @internal 217 | pub fn decode__log_entry(value__: dynamic.Dynamic) { 218 | use source <- result.try(dynamic.field("source", decode__log_entry_source)( 219 | value__, 220 | )) 221 | use level <- result.try(dynamic.field("level", decode__log_entry_level)( 222 | value__, 223 | )) 224 | use text <- result.try(dynamic.field("text", dynamic.string)(value__)) 225 | use category <- result.try(dynamic.optional_field( 226 | "category", 227 | decode__log_entry_category, 228 | )(value__)) 229 | use timestamp <- result.try(dynamic.field( 230 | "timestamp", 231 | runtime.decode__timestamp, 232 | )(value__)) 233 | use url <- result.try(dynamic.optional_field("url", dynamic.string)(value__)) 234 | use line_number <- result.try(dynamic.optional_field( 235 | "lineNumber", 236 | dynamic.int, 237 | )(value__)) 238 | use stack_trace <- result.try(dynamic.optional_field( 239 | "stackTrace", 240 | runtime.decode__stack_trace, 241 | )(value__)) 242 | use network_request_id <- result.try(dynamic.optional_field( 243 | "networkRequestId", 244 | network.decode__request_id, 245 | )(value__)) 246 | use worker_id <- result.try(dynamic.optional_field("workerId", dynamic.string)( 247 | value__, 248 | )) 249 | use args <- result.try(dynamic.optional_field( 250 | "args", 251 | dynamic.list(runtime.decode__remote_object), 252 | )(value__)) 253 | 254 | Ok(LogEntry( 255 | source: source, 256 | level: level, 257 | text: text, 258 | category: category, 259 | timestamp: timestamp, 260 | url: url, 261 | line_number: line_number, 262 | stack_trace: stack_trace, 263 | network_request_id: network_request_id, 264 | worker_id: worker_id, 265 | args: args, 266 | )) 267 | } 268 | 269 | /// Violation configuration setting. 270 | pub type ViolationSetting { 271 | ViolationSetting( 272 | /// Violation type. 273 | name: ViolationSettingName, 274 | /// Time threshold to trigger upon. 275 | threshold: Float, 276 | ) 277 | } 278 | 279 | /// This type is not part of the protocol spec, it has been generated dynamically 280 | /// to represent the possible values of the enum property `name` of `ViolationSetting` 281 | pub type ViolationSettingName { 282 | ViolationSettingNameLongTask 283 | ViolationSettingNameLongLayout 284 | ViolationSettingNameBlockedEvent 285 | ViolationSettingNameBlockedParser 286 | ViolationSettingNameDiscouragedApiUse 287 | ViolationSettingNameHandler 288 | ViolationSettingNameRecurringHandler 289 | } 290 | 291 | @internal 292 | pub fn encode__violation_setting_name(value__: ViolationSettingName) { 293 | case value__ { 294 | ViolationSettingNameLongTask -> "longTask" 295 | ViolationSettingNameLongLayout -> "longLayout" 296 | ViolationSettingNameBlockedEvent -> "blockedEvent" 297 | ViolationSettingNameBlockedParser -> "blockedParser" 298 | ViolationSettingNameDiscouragedApiUse -> "discouragedAPIUse" 299 | ViolationSettingNameHandler -> "handler" 300 | ViolationSettingNameRecurringHandler -> "recurringHandler" 301 | } 302 | |> json.string() 303 | } 304 | 305 | @internal 306 | pub fn decode__violation_setting_name(value__: dynamic.Dynamic) { 307 | case dynamic.string(value__) { 308 | Ok("longTask") -> Ok(ViolationSettingNameLongTask) 309 | Ok("longLayout") -> Ok(ViolationSettingNameLongLayout) 310 | Ok("blockedEvent") -> Ok(ViolationSettingNameBlockedEvent) 311 | Ok("blockedParser") -> Ok(ViolationSettingNameBlockedParser) 312 | Ok("discouragedAPIUse") -> Ok(ViolationSettingNameDiscouragedApiUse) 313 | Ok("handler") -> Ok(ViolationSettingNameHandler) 314 | Ok("recurringHandler") -> Ok(ViolationSettingNameRecurringHandler) 315 | Error(error) -> Error(error) 316 | Ok(other) -> 317 | Error([ 318 | dynamic.DecodeError( 319 | expected: "valid enum property", 320 | found: other, 321 | path: ["enum decoder"], 322 | ), 323 | ]) 324 | } 325 | } 326 | 327 | @internal 328 | pub fn encode__violation_setting(value__: ViolationSetting) { 329 | json.object([ 330 | #("name", encode__violation_setting_name(value__.name)), 331 | #("threshold", json.float(value__.threshold)), 332 | ]) 333 | } 334 | 335 | @internal 336 | pub fn decode__violation_setting(value__: dynamic.Dynamic) { 337 | use name <- result.try(dynamic.field("name", decode__violation_setting_name)( 338 | value__, 339 | )) 340 | use threshold <- result.try(dynamic.field("threshold", dynamic.float)(value__)) 341 | 342 | Ok(ViolationSetting(name: name, threshold: threshold)) 343 | } 344 | 345 | /// Clears the log. 346 | /// 347 | pub fn clear(callback__) { 348 | callback__("Log.clear", option.None) 349 | } 350 | 351 | /// Disables log domain, prevents further log entries from being reported to the client. 352 | /// 353 | pub fn disable(callback__) { 354 | callback__("Log.disable", option.None) 355 | } 356 | 357 | /// Enables log domain, sends the entries collected so far to the client by means of the 358 | /// `entryAdded` notification. 359 | /// 360 | pub fn enable(callback__) { 361 | callback__("Log.enable", option.None) 362 | } 363 | 364 | /// start violation reporting. 365 | /// 366 | /// Parameters: 367 | /// - `config` : Configuration for violations. 368 | /// 369 | /// Returns: 370 | /// 371 | pub fn start_violations_report( 372 | callback__, 373 | config config: List(ViolationSetting), 374 | ) { 375 | callback__( 376 | "Log.startViolationsReport", 377 | option.Some( 378 | json.object([ 379 | #("config", json.array(config, of: encode__violation_setting)), 380 | ]), 381 | ), 382 | ) 383 | } 384 | 385 | /// Stop violation reporting. 386 | /// 387 | pub fn stop_violations_report(callback__) { 388 | callback__("Log.stopViolationsReport", option.None) 389 | } 390 | -------------------------------------------------------------------------------- /src/chrobot/protocol/performance.gleam: -------------------------------------------------------------------------------- 1 | //// > ⚙️ This module was generated from the Chrome DevTools Protocol version **1.3** 2 | //// ## Performance Domain 3 | //// 4 | //// This protocol domain has no description. 5 | //// 6 | //// [📖 View this domain on the DevTools Protocol API Docs](https://chromedevtools.github.io/devtools-protocol/1-3/Performance/) 7 | 8 | // --------------------------------------------------------------------------- 9 | // | !!!!!! This is an autogenerated file - Do not edit manually !!!!!! | 10 | // | Run `codegen.sh` to regenerate. | 11 | // --------------------------------------------------------------------------- 12 | 13 | import chrobot/chrome 14 | import chrobot/internal/utils 15 | import gleam/dynamic 16 | import gleam/json 17 | import gleam/option 18 | import gleam/result 19 | 20 | /// Run-time execution metric. 21 | pub type Metric { 22 | Metric( 23 | /// Metric name. 24 | name: String, 25 | /// Metric value. 26 | value: Float, 27 | ) 28 | } 29 | 30 | @internal 31 | pub fn encode__metric(value__: Metric) { 32 | json.object([ 33 | #("name", json.string(value__.name)), 34 | #("value", json.float(value__.value)), 35 | ]) 36 | } 37 | 38 | @internal 39 | pub fn decode__metric(value__: dynamic.Dynamic) { 40 | use name <- result.try(dynamic.field("name", dynamic.string)(value__)) 41 | use value <- result.try(dynamic.field("value", dynamic.float)(value__)) 42 | 43 | Ok(Metric(name: name, value: value)) 44 | } 45 | 46 | /// This type is not part of the protocol spec, it has been generated dynamically 47 | /// to represent the response to the command `get_metrics` 48 | pub type GetMetricsResponse { 49 | GetMetricsResponse( 50 | /// Current values for run-time metrics. 51 | metrics: List(Metric), 52 | ) 53 | } 54 | 55 | @internal 56 | pub fn decode__get_metrics_response(value__: dynamic.Dynamic) { 57 | use metrics <- result.try(dynamic.field( 58 | "metrics", 59 | dynamic.list(decode__metric), 60 | )(value__)) 61 | 62 | Ok(GetMetricsResponse(metrics: metrics)) 63 | } 64 | 65 | /// Disable collecting and reporting metrics. 66 | /// 67 | pub fn disable(callback__) { 68 | callback__("Performance.disable", option.None) 69 | } 70 | 71 | /// Enable collecting and reporting metrics. 72 | /// 73 | /// Parameters: 74 | /// - `time_domain` : Time domain to use for collecting and reporting duration metrics. 75 | /// 76 | /// Returns: 77 | /// 78 | pub fn enable( 79 | callback__, 80 | time_domain time_domain: option.Option(EnableTimeDomain), 81 | ) { 82 | callback__( 83 | "Performance.enable", 84 | option.Some(json.object( 85 | [] 86 | |> utils.add_optional(time_domain, fn(inner_value__) { 87 | #("timeDomain", encode__enable_time_domain(inner_value__)) 88 | }), 89 | )), 90 | ) 91 | } 92 | 93 | /// This type is not part of the protocol spec, it has been generated dynamically 94 | /// to represent the possible values of the enum property `timeDomain` of `enable` 95 | pub type EnableTimeDomain { 96 | EnableTimeDomainTimeTicks 97 | EnableTimeDomainThreadTicks 98 | } 99 | 100 | @internal 101 | pub fn encode__enable_time_domain(value__: EnableTimeDomain) { 102 | case value__ { 103 | EnableTimeDomainTimeTicks -> "timeTicks" 104 | EnableTimeDomainThreadTicks -> "threadTicks" 105 | } 106 | |> json.string() 107 | } 108 | 109 | @internal 110 | pub fn decode__enable_time_domain(value__: dynamic.Dynamic) { 111 | case dynamic.string(value__) { 112 | Ok("timeTicks") -> Ok(EnableTimeDomainTimeTicks) 113 | Ok("threadTicks") -> Ok(EnableTimeDomainThreadTicks) 114 | Error(error) -> Error(error) 115 | Ok(other) -> 116 | Error([ 117 | dynamic.DecodeError( 118 | expected: "valid enum property", 119 | found: other, 120 | path: ["enum decoder"], 121 | ), 122 | ]) 123 | } 124 | } 125 | 126 | /// Retrieve current values of run-time metrics. 127 | /// - `metrics` : Current values for run-time metrics. 128 | /// 129 | pub fn get_metrics(callback__) { 130 | use result__ <- result.try(callback__("Performance.getMetrics", option.None)) 131 | 132 | decode__get_metrics_response(result__) 133 | |> result.replace_error(chrome.ProtocolError) 134 | } 135 | -------------------------------------------------------------------------------- /src/chrobot/protocol/profiler.gleam: -------------------------------------------------------------------------------- 1 | //// > ⚙️ This module was generated from the Chrome DevTools Protocol version **1.3** 2 | //// ## Profiler Domain 3 | //// 4 | //// This protocol domain has no description. 5 | //// 6 | //// [📖 View this domain on the DevTools Protocol API Docs](https://chromedevtools.github.io/devtools-protocol/1-3/Profiler/) 7 | 8 | // --------------------------------------------------------------------------- 9 | // | !!!!!! This is an autogenerated file - Do not edit manually !!!!!! | 10 | // | Run `codegen.sh` to regenerate. | 11 | // --------------------------------------------------------------------------- 12 | 13 | import chrobot/chrome 14 | import chrobot/internal/utils 15 | import chrobot/protocol/runtime 16 | import gleam/dynamic 17 | import gleam/json 18 | import gleam/option 19 | import gleam/result 20 | 21 | /// Profile node. Holds callsite information, execution statistics and child nodes. 22 | pub type ProfileNode { 23 | ProfileNode( 24 | /// Unique id of the node. 25 | id: Int, 26 | /// Function location. 27 | call_frame: runtime.CallFrame, 28 | /// Number of samples where this node was on top of the call stack. 29 | hit_count: option.Option(Int), 30 | /// Child node ids. 31 | children: option.Option(List(Int)), 32 | /// The reason of being not optimized. The function may be deoptimized or marked as don't 33 | /// optimize. 34 | deopt_reason: option.Option(String), 35 | /// An array of source position ticks. 36 | position_ticks: option.Option(List(PositionTickInfo)), 37 | ) 38 | } 39 | 40 | @internal 41 | pub fn encode__profile_node(value__: ProfileNode) { 42 | json.object( 43 | [ 44 | #("id", json.int(value__.id)), 45 | #("callFrame", runtime.encode__call_frame(value__.call_frame)), 46 | ] 47 | |> utils.add_optional(value__.hit_count, fn(inner_value__) { 48 | #("hitCount", json.int(inner_value__)) 49 | }) 50 | |> utils.add_optional(value__.children, fn(inner_value__) { 51 | #("children", json.array(inner_value__, of: json.int)) 52 | }) 53 | |> utils.add_optional(value__.deopt_reason, fn(inner_value__) { 54 | #("deoptReason", json.string(inner_value__)) 55 | }) 56 | |> utils.add_optional(value__.position_ticks, fn(inner_value__) { 57 | #( 58 | "positionTicks", 59 | json.array(inner_value__, of: encode__position_tick_info), 60 | ) 61 | }), 62 | ) 63 | } 64 | 65 | @internal 66 | pub fn decode__profile_node(value__: dynamic.Dynamic) { 67 | use id <- result.try(dynamic.field("id", dynamic.int)(value__)) 68 | use call_frame <- result.try(dynamic.field( 69 | "callFrame", 70 | runtime.decode__call_frame, 71 | )(value__)) 72 | use hit_count <- result.try(dynamic.optional_field("hitCount", dynamic.int)( 73 | value__, 74 | )) 75 | use children <- result.try(dynamic.optional_field( 76 | "children", 77 | dynamic.list(dynamic.int), 78 | )(value__)) 79 | use deopt_reason <- result.try(dynamic.optional_field( 80 | "deoptReason", 81 | dynamic.string, 82 | )(value__)) 83 | use position_ticks <- result.try(dynamic.optional_field( 84 | "positionTicks", 85 | dynamic.list(decode__position_tick_info), 86 | )(value__)) 87 | 88 | Ok(ProfileNode( 89 | id: id, 90 | call_frame: call_frame, 91 | hit_count: hit_count, 92 | children: children, 93 | deopt_reason: deopt_reason, 94 | position_ticks: position_ticks, 95 | )) 96 | } 97 | 98 | /// Profile. 99 | pub type Profile { 100 | Profile( 101 | /// The list of profile nodes. First item is the root node. 102 | nodes: List(ProfileNode), 103 | /// Profiling start timestamp in microseconds. 104 | start_time: Float, 105 | /// Profiling end timestamp in microseconds. 106 | end_time: Float, 107 | /// Ids of samples top nodes. 108 | samples: option.Option(List(Int)), 109 | /// Time intervals between adjacent samples in microseconds. The first delta is relative to the 110 | /// profile startTime. 111 | time_deltas: option.Option(List(Int)), 112 | ) 113 | } 114 | 115 | @internal 116 | pub fn encode__profile(value__: Profile) { 117 | json.object( 118 | [ 119 | #("nodes", json.array(value__.nodes, of: encode__profile_node)), 120 | #("startTime", json.float(value__.start_time)), 121 | #("endTime", json.float(value__.end_time)), 122 | ] 123 | |> utils.add_optional(value__.samples, fn(inner_value__) { 124 | #("samples", json.array(inner_value__, of: json.int)) 125 | }) 126 | |> utils.add_optional(value__.time_deltas, fn(inner_value__) { 127 | #("timeDeltas", json.array(inner_value__, of: json.int)) 128 | }), 129 | ) 130 | } 131 | 132 | @internal 133 | pub fn decode__profile(value__: dynamic.Dynamic) { 134 | use nodes <- result.try(dynamic.field( 135 | "nodes", 136 | dynamic.list(decode__profile_node), 137 | )(value__)) 138 | use start_time <- result.try(dynamic.field("startTime", dynamic.float)( 139 | value__, 140 | )) 141 | use end_time <- result.try(dynamic.field("endTime", dynamic.float)(value__)) 142 | use samples <- result.try(dynamic.optional_field( 143 | "samples", 144 | dynamic.list(dynamic.int), 145 | )(value__)) 146 | use time_deltas <- result.try(dynamic.optional_field( 147 | "timeDeltas", 148 | dynamic.list(dynamic.int), 149 | )(value__)) 150 | 151 | Ok(Profile( 152 | nodes: nodes, 153 | start_time: start_time, 154 | end_time: end_time, 155 | samples: samples, 156 | time_deltas: time_deltas, 157 | )) 158 | } 159 | 160 | /// Specifies a number of samples attributed to a certain source position. 161 | pub type PositionTickInfo { 162 | PositionTickInfo( 163 | /// Source line number (1-based). 164 | line: Int, 165 | /// Number of samples attributed to the source line. 166 | ticks: Int, 167 | ) 168 | } 169 | 170 | @internal 171 | pub fn encode__position_tick_info(value__: PositionTickInfo) { 172 | json.object([ 173 | #("line", json.int(value__.line)), 174 | #("ticks", json.int(value__.ticks)), 175 | ]) 176 | } 177 | 178 | @internal 179 | pub fn decode__position_tick_info(value__: dynamic.Dynamic) { 180 | use line <- result.try(dynamic.field("line", dynamic.int)(value__)) 181 | use ticks <- result.try(dynamic.field("ticks", dynamic.int)(value__)) 182 | 183 | Ok(PositionTickInfo(line: line, ticks: ticks)) 184 | } 185 | 186 | /// Coverage data for a source range. 187 | pub type CoverageRange { 188 | CoverageRange( 189 | /// JavaScript script source offset for the range start. 190 | start_offset: Int, 191 | /// JavaScript script source offset for the range end. 192 | end_offset: Int, 193 | /// Collected execution count of the source range. 194 | count: Int, 195 | ) 196 | } 197 | 198 | @internal 199 | pub fn encode__coverage_range(value__: CoverageRange) { 200 | json.object([ 201 | #("startOffset", json.int(value__.start_offset)), 202 | #("endOffset", json.int(value__.end_offset)), 203 | #("count", json.int(value__.count)), 204 | ]) 205 | } 206 | 207 | @internal 208 | pub fn decode__coverage_range(value__: dynamic.Dynamic) { 209 | use start_offset <- result.try(dynamic.field("startOffset", dynamic.int)( 210 | value__, 211 | )) 212 | use end_offset <- result.try(dynamic.field("endOffset", dynamic.int)(value__)) 213 | use count <- result.try(dynamic.field("count", dynamic.int)(value__)) 214 | 215 | Ok(CoverageRange( 216 | start_offset: start_offset, 217 | end_offset: end_offset, 218 | count: count, 219 | )) 220 | } 221 | 222 | /// Coverage data for a JavaScript function. 223 | pub type FunctionCoverage { 224 | FunctionCoverage( 225 | /// JavaScript function name. 226 | function_name: String, 227 | /// Source ranges inside the function with coverage data. 228 | ranges: List(CoverageRange), 229 | /// Whether coverage data for this function has block granularity. 230 | is_block_coverage: Bool, 231 | ) 232 | } 233 | 234 | @internal 235 | pub fn encode__function_coverage(value__: FunctionCoverage) { 236 | json.object([ 237 | #("functionName", json.string(value__.function_name)), 238 | #("ranges", json.array(value__.ranges, of: encode__coverage_range)), 239 | #("isBlockCoverage", json.bool(value__.is_block_coverage)), 240 | ]) 241 | } 242 | 243 | @internal 244 | pub fn decode__function_coverage(value__: dynamic.Dynamic) { 245 | use function_name <- result.try(dynamic.field("functionName", dynamic.string)( 246 | value__, 247 | )) 248 | use ranges <- result.try(dynamic.field( 249 | "ranges", 250 | dynamic.list(decode__coverage_range), 251 | )(value__)) 252 | use is_block_coverage <- result.try(dynamic.field( 253 | "isBlockCoverage", 254 | dynamic.bool, 255 | )(value__)) 256 | 257 | Ok(FunctionCoverage( 258 | function_name: function_name, 259 | ranges: ranges, 260 | is_block_coverage: is_block_coverage, 261 | )) 262 | } 263 | 264 | /// Coverage data for a JavaScript script. 265 | pub type ScriptCoverage { 266 | ScriptCoverage( 267 | /// JavaScript script id. 268 | script_id: runtime.ScriptId, 269 | /// JavaScript script name or url. 270 | url: String, 271 | /// Functions contained in the script that has coverage data. 272 | functions: List(FunctionCoverage), 273 | ) 274 | } 275 | 276 | @internal 277 | pub fn encode__script_coverage(value__: ScriptCoverage) { 278 | json.object([ 279 | #("scriptId", runtime.encode__script_id(value__.script_id)), 280 | #("url", json.string(value__.url)), 281 | #("functions", json.array(value__.functions, of: encode__function_coverage)), 282 | ]) 283 | } 284 | 285 | @internal 286 | pub fn decode__script_coverage(value__: dynamic.Dynamic) { 287 | use script_id <- result.try(dynamic.field( 288 | "scriptId", 289 | runtime.decode__script_id, 290 | )(value__)) 291 | use url <- result.try(dynamic.field("url", dynamic.string)(value__)) 292 | use functions <- result.try(dynamic.field( 293 | "functions", 294 | dynamic.list(decode__function_coverage), 295 | )(value__)) 296 | 297 | Ok(ScriptCoverage(script_id: script_id, url: url, functions: functions)) 298 | } 299 | 300 | /// This type is not part of the protocol spec, it has been generated dynamically 301 | /// to represent the response to the command `get_best_effort_coverage` 302 | pub type GetBestEffortCoverageResponse { 303 | GetBestEffortCoverageResponse( 304 | /// Coverage data for the current isolate. 305 | result: List(ScriptCoverage), 306 | ) 307 | } 308 | 309 | @internal 310 | pub fn decode__get_best_effort_coverage_response(value__: dynamic.Dynamic) { 311 | use result <- result.try(dynamic.field( 312 | "result", 313 | dynamic.list(decode__script_coverage), 314 | )(value__)) 315 | 316 | Ok(GetBestEffortCoverageResponse(result: result)) 317 | } 318 | 319 | /// This type is not part of the protocol spec, it has been generated dynamically 320 | /// to represent the response to the command `start_precise_coverage` 321 | pub type StartPreciseCoverageResponse { 322 | StartPreciseCoverageResponse( 323 | /// Monotonically increasing time (in seconds) when the coverage update was taken in the backend. 324 | timestamp: Float, 325 | ) 326 | } 327 | 328 | @internal 329 | pub fn decode__start_precise_coverage_response(value__: dynamic.Dynamic) { 330 | use timestamp <- result.try(dynamic.field("timestamp", dynamic.float)(value__)) 331 | 332 | Ok(StartPreciseCoverageResponse(timestamp: timestamp)) 333 | } 334 | 335 | /// This type is not part of the protocol spec, it has been generated dynamically 336 | /// to represent the response to the command `stop` 337 | pub type StopResponse { 338 | StopResponse( 339 | /// Recorded profile. 340 | profile: Profile, 341 | ) 342 | } 343 | 344 | @internal 345 | pub fn decode__stop_response(value__: dynamic.Dynamic) { 346 | use profile <- result.try(dynamic.field("profile", decode__profile)(value__)) 347 | 348 | Ok(StopResponse(profile: profile)) 349 | } 350 | 351 | /// This type is not part of the protocol spec, it has been generated dynamically 352 | /// to represent the response to the command `take_precise_coverage` 353 | pub type TakePreciseCoverageResponse { 354 | TakePreciseCoverageResponse( 355 | /// Coverage data for the current isolate. 356 | result: List(ScriptCoverage), 357 | /// Monotonically increasing time (in seconds) when the coverage update was taken in the backend. 358 | timestamp: Float, 359 | ) 360 | } 361 | 362 | @internal 363 | pub fn decode__take_precise_coverage_response(value__: dynamic.Dynamic) { 364 | use result <- result.try(dynamic.field( 365 | "result", 366 | dynamic.list(decode__script_coverage), 367 | )(value__)) 368 | use timestamp <- result.try(dynamic.field("timestamp", dynamic.float)(value__)) 369 | 370 | Ok(TakePreciseCoverageResponse(result: result, timestamp: timestamp)) 371 | } 372 | 373 | /// This generated protocol command has no description 374 | /// 375 | pub fn disable(callback__) { 376 | callback__("Profiler.disable", option.None) 377 | } 378 | 379 | /// This generated protocol command has no description 380 | /// 381 | pub fn enable(callback__) { 382 | callback__("Profiler.enable", option.None) 383 | } 384 | 385 | /// Collect coverage data for the current isolate. The coverage data may be incomplete due to 386 | /// garbage collection. 387 | /// - `result` : Coverage data for the current isolate. 388 | /// 389 | pub fn get_best_effort_coverage(callback__) { 390 | use result__ <- result.try(callback__( 391 | "Profiler.getBestEffortCoverage", 392 | option.None, 393 | )) 394 | 395 | decode__get_best_effort_coverage_response(result__) 396 | |> result.replace_error(chrome.ProtocolError) 397 | } 398 | 399 | /// Changes CPU profiler sampling interval. Must be called before CPU profiles recording started. 400 | /// 401 | /// Parameters: 402 | /// - `interval` : New sampling interval in microseconds. 403 | /// 404 | /// Returns: 405 | /// 406 | pub fn set_sampling_interval(callback__, interval interval: Int) { 407 | callback__( 408 | "Profiler.setSamplingInterval", 409 | option.Some(json.object([#("interval", json.int(interval))])), 410 | ) 411 | } 412 | 413 | /// This generated protocol command has no description 414 | /// 415 | pub fn start(callback__) { 416 | callback__("Profiler.start", option.None) 417 | } 418 | 419 | /// Enable precise code coverage. Coverage data for JavaScript executed before enabling precise code 420 | /// coverage may be incomplete. Enabling prevents running optimized code and resets execution 421 | /// counters. 422 | /// 423 | /// Parameters: 424 | /// - `call_count` : Collect accurate call counts beyond simple 'covered' or 'not covered'. 425 | /// - `detailed` : Collect block-based coverage. 426 | /// - `allow_triggered_updates` : Allow the backend to send updates on its own initiative 427 | /// 428 | /// Returns: 429 | /// - `timestamp` : Monotonically increasing time (in seconds) when the coverage update was taken in the backend. 430 | /// 431 | pub fn start_precise_coverage( 432 | callback__, 433 | call_count call_count: option.Option(Bool), 434 | detailed detailed: option.Option(Bool), 435 | allow_triggered_updates allow_triggered_updates: option.Option(Bool), 436 | ) { 437 | use result__ <- result.try(callback__( 438 | "Profiler.startPreciseCoverage", 439 | option.Some(json.object( 440 | [] 441 | |> utils.add_optional(call_count, fn(inner_value__) { 442 | #("callCount", json.bool(inner_value__)) 443 | }) 444 | |> utils.add_optional(detailed, fn(inner_value__) { 445 | #("detailed", json.bool(inner_value__)) 446 | }) 447 | |> utils.add_optional(allow_triggered_updates, fn(inner_value__) { 448 | #("allowTriggeredUpdates", json.bool(inner_value__)) 449 | }), 450 | )), 451 | )) 452 | 453 | decode__start_precise_coverage_response(result__) 454 | |> result.replace_error(chrome.ProtocolError) 455 | } 456 | 457 | /// This generated protocol command has no description 458 | /// - `profile` : Recorded profile. 459 | /// 460 | pub fn stop(callback__) { 461 | use result__ <- result.try(callback__("Profiler.stop", option.None)) 462 | 463 | decode__stop_response(result__) 464 | |> result.replace_error(chrome.ProtocolError) 465 | } 466 | 467 | /// Disable precise code coverage. Disabling releases unnecessary execution count records and allows 468 | /// executing optimized code. 469 | /// 470 | pub fn stop_precise_coverage(callback__) { 471 | callback__("Profiler.stopPreciseCoverage", option.None) 472 | } 473 | 474 | /// Collect coverage data for the current isolate, and resets execution counters. Precise code 475 | /// coverage needs to have started. 476 | /// - `result` : Coverage data for the current isolate. 477 | /// - `timestamp` : Monotonically increasing time (in seconds) when the coverage update was taken in the backend. 478 | /// 479 | pub fn take_precise_coverage(callback__) { 480 | use result__ <- result.try(callback__( 481 | "Profiler.takePreciseCoverage", 482 | option.None, 483 | )) 484 | 485 | decode__take_precise_coverage_response(result__) 486 | |> result.replace_error(chrome.ProtocolError) 487 | } 488 | -------------------------------------------------------------------------------- /src/chrobot/protocol/security.gleam: -------------------------------------------------------------------------------- 1 | //// > ⚙️ This module was generated from the Chrome DevTools Protocol version **1.3** 2 | //// ## Security Domain 3 | //// 4 | //// Security 5 | //// 6 | //// [📖 View this domain on the DevTools Protocol API Docs](https://chromedevtools.github.io/devtools-protocol/1-3/Security/) 7 | 8 | // --------------------------------------------------------------------------- 9 | // | !!!!!! This is an autogenerated file - Do not edit manually !!!!!! | 10 | // | Run `codegen.sh` to regenerate. | 11 | // --------------------------------------------------------------------------- 12 | 13 | import chrobot/internal/utils 14 | import gleam/dynamic 15 | import gleam/json 16 | import gleam/option 17 | import gleam/result 18 | 19 | /// An internal certificate ID value. 20 | pub type CertificateId { 21 | CertificateId(Int) 22 | } 23 | 24 | @internal 25 | pub fn encode__certificate_id(value__: CertificateId) { 26 | case value__ { 27 | CertificateId(inner_value__) -> json.int(inner_value__) 28 | } 29 | } 30 | 31 | @internal 32 | pub fn decode__certificate_id(value__: dynamic.Dynamic) { 33 | value__ |> dynamic.decode1(CertificateId, dynamic.int) 34 | } 35 | 36 | /// A description of mixed content (HTTP resources on HTTPS pages), as defined by 37 | /// https://www.w3.org/TR/mixed-content/#categories 38 | pub type MixedContentType { 39 | MixedContentTypeBlockable 40 | MixedContentTypeOptionallyBlockable 41 | MixedContentTypeNone 42 | } 43 | 44 | @internal 45 | pub fn encode__mixed_content_type(value__: MixedContentType) { 46 | case value__ { 47 | MixedContentTypeBlockable -> "blockable" 48 | MixedContentTypeOptionallyBlockable -> "optionally-blockable" 49 | MixedContentTypeNone -> "none" 50 | } 51 | |> json.string() 52 | } 53 | 54 | @internal 55 | pub fn decode__mixed_content_type(value__: dynamic.Dynamic) { 56 | case dynamic.string(value__) { 57 | Ok("blockable") -> Ok(MixedContentTypeBlockable) 58 | Ok("optionally-blockable") -> Ok(MixedContentTypeOptionallyBlockable) 59 | Ok("none") -> Ok(MixedContentTypeNone) 60 | Error(error) -> Error(error) 61 | Ok(other) -> 62 | Error([ 63 | dynamic.DecodeError( 64 | expected: "valid enum property", 65 | found: other, 66 | path: ["enum decoder"], 67 | ), 68 | ]) 69 | } 70 | } 71 | 72 | /// The security level of a page or resource. 73 | pub type SecurityState { 74 | SecurityStateUnknown 75 | SecurityStateNeutral 76 | SecurityStateInsecure 77 | SecurityStateSecure 78 | SecurityStateInfo 79 | SecurityStateInsecureBroken 80 | } 81 | 82 | @internal 83 | pub fn encode__security_state(value__: SecurityState) { 84 | case value__ { 85 | SecurityStateUnknown -> "unknown" 86 | SecurityStateNeutral -> "neutral" 87 | SecurityStateInsecure -> "insecure" 88 | SecurityStateSecure -> "secure" 89 | SecurityStateInfo -> "info" 90 | SecurityStateInsecureBroken -> "insecure-broken" 91 | } 92 | |> json.string() 93 | } 94 | 95 | @internal 96 | pub fn decode__security_state(value__: dynamic.Dynamic) { 97 | case dynamic.string(value__) { 98 | Ok("unknown") -> Ok(SecurityStateUnknown) 99 | Ok("neutral") -> Ok(SecurityStateNeutral) 100 | Ok("insecure") -> Ok(SecurityStateInsecure) 101 | Ok("secure") -> Ok(SecurityStateSecure) 102 | Ok("info") -> Ok(SecurityStateInfo) 103 | Ok("insecure-broken") -> Ok(SecurityStateInsecureBroken) 104 | Error(error) -> Error(error) 105 | Ok(other) -> 106 | Error([ 107 | dynamic.DecodeError( 108 | expected: "valid enum property", 109 | found: other, 110 | path: ["enum decoder"], 111 | ), 112 | ]) 113 | } 114 | } 115 | 116 | /// An explanation of an factor contributing to the security state. 117 | pub type SecurityStateExplanation { 118 | SecurityStateExplanation( 119 | /// Security state representing the severity of the factor being explained. 120 | security_state: SecurityState, 121 | /// Title describing the type of factor. 122 | title: String, 123 | /// Short phrase describing the type of factor. 124 | summary: String, 125 | /// Full text explanation of the factor. 126 | description: String, 127 | /// The type of mixed content described by the explanation. 128 | mixed_content_type: MixedContentType, 129 | /// Page certificate. 130 | certificate: List(String), 131 | /// Recommendations to fix any issues. 132 | recommendations: option.Option(List(String)), 133 | ) 134 | } 135 | 136 | @internal 137 | pub fn encode__security_state_explanation(value__: SecurityStateExplanation) { 138 | json.object( 139 | [ 140 | #("securityState", encode__security_state(value__.security_state)), 141 | #("title", json.string(value__.title)), 142 | #("summary", json.string(value__.summary)), 143 | #("description", json.string(value__.description)), 144 | #( 145 | "mixedContentType", 146 | encode__mixed_content_type(value__.mixed_content_type), 147 | ), 148 | #("certificate", json.array(value__.certificate, of: json.string)), 149 | ] 150 | |> utils.add_optional(value__.recommendations, fn(inner_value__) { 151 | #("recommendations", json.array(inner_value__, of: json.string)) 152 | }), 153 | ) 154 | } 155 | 156 | @internal 157 | pub fn decode__security_state_explanation(value__: dynamic.Dynamic) { 158 | use security_state <- result.try(dynamic.field( 159 | "securityState", 160 | decode__security_state, 161 | )(value__)) 162 | use title <- result.try(dynamic.field("title", dynamic.string)(value__)) 163 | use summary <- result.try(dynamic.field("summary", dynamic.string)(value__)) 164 | use description <- result.try(dynamic.field("description", dynamic.string)( 165 | value__, 166 | )) 167 | use mixed_content_type <- result.try(dynamic.field( 168 | "mixedContentType", 169 | decode__mixed_content_type, 170 | )(value__)) 171 | use certificate <- result.try(dynamic.field( 172 | "certificate", 173 | dynamic.list(dynamic.string), 174 | )(value__)) 175 | use recommendations <- result.try(dynamic.optional_field( 176 | "recommendations", 177 | dynamic.list(dynamic.string), 178 | )(value__)) 179 | 180 | Ok(SecurityStateExplanation( 181 | security_state: security_state, 182 | title: title, 183 | summary: summary, 184 | description: description, 185 | mixed_content_type: mixed_content_type, 186 | certificate: certificate, 187 | recommendations: recommendations, 188 | )) 189 | } 190 | 191 | /// The action to take when a certificate error occurs. continue will continue processing the 192 | /// request and cancel will cancel the request. 193 | pub type CertificateErrorAction { 194 | CertificateErrorActionContinue 195 | CertificateErrorActionCancel 196 | } 197 | 198 | @internal 199 | pub fn encode__certificate_error_action(value__: CertificateErrorAction) { 200 | case value__ { 201 | CertificateErrorActionContinue -> "continue" 202 | CertificateErrorActionCancel -> "cancel" 203 | } 204 | |> json.string() 205 | } 206 | 207 | @internal 208 | pub fn decode__certificate_error_action(value__: dynamic.Dynamic) { 209 | case dynamic.string(value__) { 210 | Ok("continue") -> Ok(CertificateErrorActionContinue) 211 | Ok("cancel") -> Ok(CertificateErrorActionCancel) 212 | Error(error) -> Error(error) 213 | Ok(other) -> 214 | Error([ 215 | dynamic.DecodeError( 216 | expected: "valid enum property", 217 | found: other, 218 | path: ["enum decoder"], 219 | ), 220 | ]) 221 | } 222 | } 223 | 224 | /// Disables tracking security state changes. 225 | /// 226 | pub fn disable(callback__) { 227 | callback__("Security.disable", option.None) 228 | } 229 | 230 | /// Enables tracking security state changes. 231 | /// 232 | pub fn enable(callback__) { 233 | callback__("Security.enable", option.None) 234 | } 235 | 236 | /// Enable/disable whether all certificate errors should be ignored. 237 | /// 238 | /// Parameters: 239 | /// - `ignore` : If true, all certificate errors will be ignored. 240 | /// 241 | /// Returns: 242 | /// 243 | pub fn set_ignore_certificate_errors(callback__, ignore ignore: Bool) { 244 | callback__( 245 | "Security.setIgnoreCertificateErrors", 246 | option.Some(json.object([#("ignore", json.bool(ignore))])), 247 | ) 248 | } 249 | -------------------------------------------------------------------------------- /src/chrobot/protocol/target.gleam: -------------------------------------------------------------------------------- 1 | //// > ⚙️ This module was generated from the Chrome DevTools Protocol version **1.3** 2 | //// ## Target Domain 3 | //// 4 | //// Supports additional targets discovery and allows to attach to them. 5 | //// 6 | //// [📖 View this domain on the DevTools Protocol API Docs](https://chromedevtools.github.io/devtools-protocol/1-3/Target/) 7 | 8 | // --------------------------------------------------------------------------- 9 | // | !!!!!! This is an autogenerated file - Do not edit manually !!!!!! | 10 | // | Run `codegen.sh` to regenerate. | 11 | // --------------------------------------------------------------------------- 12 | 13 | import chrobot/chrome 14 | import chrobot/internal/utils 15 | import gleam/dynamic 16 | import gleam/json 17 | import gleam/option 18 | import gleam/result 19 | 20 | pub type TargetID { 21 | TargetID(String) 22 | } 23 | 24 | @internal 25 | pub fn encode__target_id(value__: TargetID) { 26 | case value__ { 27 | TargetID(inner_value__) -> json.string(inner_value__) 28 | } 29 | } 30 | 31 | @internal 32 | pub fn decode__target_id(value__: dynamic.Dynamic) { 33 | value__ |> dynamic.decode1(TargetID, dynamic.string) 34 | } 35 | 36 | /// Unique identifier of attached debugging session. 37 | pub type SessionID { 38 | SessionID(String) 39 | } 40 | 41 | @internal 42 | pub fn encode__session_id(value__: SessionID) { 43 | case value__ { 44 | SessionID(inner_value__) -> json.string(inner_value__) 45 | } 46 | } 47 | 48 | @internal 49 | pub fn decode__session_id(value__: dynamic.Dynamic) { 50 | value__ |> dynamic.decode1(SessionID, dynamic.string) 51 | } 52 | 53 | pub type TargetInfo { 54 | TargetInfo( 55 | target_id: TargetID, 56 | /// List of types: https://source.chromium.org/chromium/chromium/src/+/main:content/browser/devtools/devtools_agent_host_impl.cc?ss=chromium&q=f:devtools%20-f:out%20%22::kTypeTab%5B%5D%22 57 | type_: String, 58 | title: String, 59 | url: String, 60 | /// Whether the target has an attached client. 61 | attached: Bool, 62 | /// Opener target Id 63 | opener_id: option.Option(TargetID), 64 | ) 65 | } 66 | 67 | @internal 68 | pub fn encode__target_info(value__: TargetInfo) { 69 | json.object( 70 | [ 71 | #("targetId", encode__target_id(value__.target_id)), 72 | #("type", json.string(value__.type_)), 73 | #("title", json.string(value__.title)), 74 | #("url", json.string(value__.url)), 75 | #("attached", json.bool(value__.attached)), 76 | ] 77 | |> utils.add_optional(value__.opener_id, fn(inner_value__) { 78 | #("openerId", encode__target_id(inner_value__)) 79 | }), 80 | ) 81 | } 82 | 83 | @internal 84 | pub fn decode__target_info(value__: dynamic.Dynamic) { 85 | use target_id <- result.try(dynamic.field("targetId", decode__target_id)( 86 | value__, 87 | )) 88 | use type_ <- result.try(dynamic.field("type", dynamic.string)(value__)) 89 | use title <- result.try(dynamic.field("title", dynamic.string)(value__)) 90 | use url <- result.try(dynamic.field("url", dynamic.string)(value__)) 91 | use attached <- result.try(dynamic.field("attached", dynamic.bool)(value__)) 92 | use opener_id <- result.try(dynamic.optional_field( 93 | "openerId", 94 | decode__target_id, 95 | )(value__)) 96 | 97 | Ok(TargetInfo( 98 | target_id: target_id, 99 | type_: type_, 100 | title: title, 101 | url: url, 102 | attached: attached, 103 | opener_id: opener_id, 104 | )) 105 | } 106 | 107 | /// This type is not part of the protocol spec, it has been generated dynamically 108 | /// to represent the response to the command `attach_to_target` 109 | pub type AttachToTargetResponse { 110 | AttachToTargetResponse( 111 | /// Id assigned to the session. 112 | session_id: SessionID, 113 | ) 114 | } 115 | 116 | @internal 117 | pub fn decode__attach_to_target_response(value__: dynamic.Dynamic) { 118 | use session_id <- result.try(dynamic.field("sessionId", decode__session_id)( 119 | value__, 120 | )) 121 | 122 | Ok(AttachToTargetResponse(session_id: session_id)) 123 | } 124 | 125 | /// This type is not part of the protocol spec, it has been generated dynamically 126 | /// to represent the response to the command `create_browser_context` 127 | pub type CreateBrowserContextResponse { 128 | CreateBrowserContextResponse( 129 | /// The id of the context created. 130 | browser_context_id: String, 131 | ) 132 | } 133 | 134 | @internal 135 | pub fn decode__create_browser_context_response(value__: dynamic.Dynamic) { 136 | use browser_context_id <- result.try(dynamic.field( 137 | "browserContextId", 138 | dynamic.string, 139 | )(value__)) 140 | 141 | Ok(CreateBrowserContextResponse(browser_context_id: browser_context_id)) 142 | } 143 | 144 | /// This type is not part of the protocol spec, it has been generated dynamically 145 | /// to represent the response to the command `get_browser_contexts` 146 | pub type GetBrowserContextsResponse { 147 | GetBrowserContextsResponse( 148 | /// An array of browser context ids. 149 | browser_context_ids: List(String), 150 | ) 151 | } 152 | 153 | @internal 154 | pub fn decode__get_browser_contexts_response(value__: dynamic.Dynamic) { 155 | use browser_context_ids <- result.try(dynamic.field( 156 | "browserContextIds", 157 | dynamic.list(dynamic.string), 158 | )(value__)) 159 | 160 | Ok(GetBrowserContextsResponse(browser_context_ids: browser_context_ids)) 161 | } 162 | 163 | /// This type is not part of the protocol spec, it has been generated dynamically 164 | /// to represent the response to the command `create_target` 165 | pub type CreateTargetResponse { 166 | CreateTargetResponse( 167 | /// The id of the page opened. 168 | target_id: TargetID, 169 | ) 170 | } 171 | 172 | @internal 173 | pub fn decode__create_target_response(value__: dynamic.Dynamic) { 174 | use target_id <- result.try(dynamic.field("targetId", decode__target_id)( 175 | value__, 176 | )) 177 | 178 | Ok(CreateTargetResponse(target_id: target_id)) 179 | } 180 | 181 | /// This type is not part of the protocol spec, it has been generated dynamically 182 | /// to represent the response to the command `get_targets` 183 | pub type GetTargetsResponse { 184 | GetTargetsResponse( 185 | /// The list of targets. 186 | target_infos: List(TargetInfo), 187 | ) 188 | } 189 | 190 | @internal 191 | pub fn decode__get_targets_response(value__: dynamic.Dynamic) { 192 | use target_infos <- result.try(dynamic.field( 193 | "targetInfos", 194 | dynamic.list(decode__target_info), 195 | )(value__)) 196 | 197 | Ok(GetTargetsResponse(target_infos: target_infos)) 198 | } 199 | 200 | /// Activates (focuses) the target. 201 | /// 202 | /// Parameters: 203 | /// - `target_id` 204 | /// 205 | /// Returns: 206 | /// 207 | pub fn activate_target(callback__, target_id target_id: TargetID) { 208 | callback__( 209 | "Target.activateTarget", 210 | option.Some(json.object([#("targetId", encode__target_id(target_id))])), 211 | ) 212 | } 213 | 214 | /// Attaches to the target with given id. 215 | /// 216 | /// Parameters: 217 | /// - `target_id` 218 | /// - `flatten` : Enables "flat" access to the session via specifying sessionId attribute in the commands. 219 | /// We plan to make this the default, deprecate non-flattened mode, 220 | /// and eventually retire it. See crbug.com/991325. 221 | /// 222 | /// Returns: 223 | /// - `session_id` : Id assigned to the session. 224 | /// 225 | pub fn attach_to_target( 226 | callback__, 227 | target_id target_id: TargetID, 228 | flatten flatten: option.Option(Bool), 229 | ) { 230 | use result__ <- result.try(callback__( 231 | "Target.attachToTarget", 232 | option.Some(json.object( 233 | [#("targetId", encode__target_id(target_id))] 234 | |> utils.add_optional(flatten, fn(inner_value__) { 235 | #("flatten", json.bool(inner_value__)) 236 | }), 237 | )), 238 | )) 239 | 240 | decode__attach_to_target_response(result__) 241 | |> result.replace_error(chrome.ProtocolError) 242 | } 243 | 244 | /// Closes the target. If the target is a page that gets closed too. 245 | /// 246 | /// Parameters: 247 | /// - `target_id` 248 | /// 249 | /// Returns: 250 | /// 251 | pub fn close_target(callback__, target_id target_id: TargetID) { 252 | callback__( 253 | "Target.closeTarget", 254 | option.Some(json.object([#("targetId", encode__target_id(target_id))])), 255 | ) 256 | } 257 | 258 | /// Creates a new empty BrowserContext. Similar to an incognito profile but you can have more than 259 | /// one. 260 | /// 261 | /// Parameters: 262 | /// 263 | /// Returns: 264 | /// - `browser_context_id` : The id of the context created. 265 | /// 266 | pub fn create_browser_context(callback__) { 267 | use result__ <- result.try(callback__( 268 | "Target.createBrowserContext", 269 | option.None, 270 | )) 271 | 272 | decode__create_browser_context_response(result__) 273 | |> result.replace_error(chrome.ProtocolError) 274 | } 275 | 276 | /// Returns all browser contexts created with `Target.createBrowserContext` method. 277 | /// - `browser_context_ids` : An array of browser context ids. 278 | /// 279 | pub fn get_browser_contexts(callback__) { 280 | use result__ <- result.try(callback__( 281 | "Target.getBrowserContexts", 282 | option.None, 283 | )) 284 | 285 | decode__get_browser_contexts_response(result__) 286 | |> result.replace_error(chrome.ProtocolError) 287 | } 288 | 289 | /// Creates a new page. 290 | /// 291 | /// Parameters: 292 | /// - `url` : The initial URL the page will be navigated to. An empty string indicates about:blank. 293 | /// - `width` : Frame width in DIP (headless chrome only). 294 | /// - `height` : Frame height in DIP (headless chrome only). 295 | /// - `new_window` : Whether to create a new Window or Tab (chrome-only, false by default). 296 | /// - `background` : Whether to create the target in background or foreground (chrome-only, 297 | /// false by default). 298 | /// 299 | /// Returns: 300 | /// - `target_id` : The id of the page opened. 301 | /// 302 | pub fn create_target( 303 | callback__, 304 | url url: String, 305 | width width: option.Option(Int), 306 | height height: option.Option(Int), 307 | new_window new_window: option.Option(Bool), 308 | background background: option.Option(Bool), 309 | ) { 310 | use result__ <- result.try(callback__( 311 | "Target.createTarget", 312 | option.Some(json.object( 313 | [#("url", json.string(url))] 314 | |> utils.add_optional(width, fn(inner_value__) { 315 | #("width", json.int(inner_value__)) 316 | }) 317 | |> utils.add_optional(height, fn(inner_value__) { 318 | #("height", json.int(inner_value__)) 319 | }) 320 | |> utils.add_optional(new_window, fn(inner_value__) { 321 | #("newWindow", json.bool(inner_value__)) 322 | }) 323 | |> utils.add_optional(background, fn(inner_value__) { 324 | #("background", json.bool(inner_value__)) 325 | }), 326 | )), 327 | )) 328 | 329 | decode__create_target_response(result__) 330 | |> result.replace_error(chrome.ProtocolError) 331 | } 332 | 333 | /// Detaches session with given id. 334 | /// 335 | /// Parameters: 336 | /// - `session_id` : Session to detach. 337 | /// 338 | /// Returns: 339 | /// 340 | pub fn detach_from_target( 341 | callback__, 342 | session_id session_id: option.Option(SessionID), 343 | ) { 344 | callback__( 345 | "Target.detachFromTarget", 346 | option.Some(json.object( 347 | [] 348 | |> utils.add_optional(session_id, fn(inner_value__) { 349 | #("sessionId", encode__session_id(inner_value__)) 350 | }), 351 | )), 352 | ) 353 | } 354 | 355 | /// Deletes a BrowserContext. All the belonging pages will be closed without calling their 356 | /// beforeunload hooks. 357 | /// 358 | /// Parameters: 359 | /// - `browser_context_id` 360 | /// 361 | /// Returns: 362 | /// 363 | pub fn dispose_browser_context( 364 | callback__, 365 | browser_context_id browser_context_id: String, 366 | ) { 367 | callback__( 368 | "Target.disposeBrowserContext", 369 | option.Some( 370 | json.object([#("browserContextId", json.string(browser_context_id))]), 371 | ), 372 | ) 373 | } 374 | 375 | /// Retrieves a list of available targets. 376 | /// 377 | /// Parameters: 378 | /// 379 | /// Returns: 380 | /// - `target_infos` : The list of targets. 381 | /// 382 | pub fn get_targets(callback__) { 383 | use result__ <- result.try(callback__("Target.getTargets", option.None)) 384 | 385 | decode__get_targets_response(result__) 386 | |> result.replace_error(chrome.ProtocolError) 387 | } 388 | 389 | /// Controls whether to automatically attach to new targets which are considered to be related to 390 | /// this one. When turned on, attaches to all existing related targets as well. When turned off, 391 | /// automatically detaches from all currently attached targets. 392 | /// This also clears all targets added by `autoAttachRelated` from the list of targets to watch 393 | /// for creation of related targets. 394 | /// 395 | /// Parameters: 396 | /// - `auto_attach` : Whether to auto-attach to related targets. 397 | /// - `wait_for_debugger_on_start` : Whether to pause new targets when attaching to them. Use `Runtime.runIfWaitingForDebugger` 398 | /// to run paused targets. 399 | /// 400 | /// Returns: 401 | /// 402 | pub fn set_auto_attach( 403 | callback__, 404 | auto_attach auto_attach: Bool, 405 | wait_for_debugger_on_start wait_for_debugger_on_start: Bool, 406 | ) { 407 | callback__( 408 | "Target.setAutoAttach", 409 | option.Some( 410 | json.object([ 411 | #("autoAttach", json.bool(auto_attach)), 412 | #("waitForDebuggerOnStart", json.bool(wait_for_debugger_on_start)), 413 | ]), 414 | ), 415 | ) 416 | } 417 | 418 | /// Controls whether to discover available targets and notify via 419 | /// `targetCreated/targetInfoChanged/targetDestroyed` events. 420 | /// 421 | /// Parameters: 422 | /// - `discover` : Whether to discover available targets. 423 | /// 424 | /// Returns: 425 | /// 426 | pub fn set_discover_targets(callback__, discover discover: Bool) { 427 | callback__( 428 | "Target.setDiscoverTargets", 429 | option.Some(json.object([#("discover", json.bool(discover))])), 430 | ) 431 | } 432 | -------------------------------------------------------------------------------- /src/chrobot_ffi.erl: -------------------------------------------------------------------------------- 1 | -module(chrobot_ffi). 2 | -include_lib("kernel/include/file.hrl"). 3 | -export([open_browser_port/2, send_to_port/2, get_arch/0, unzip/2, set_executable/1, run_command/1, get_time_ms/0]). 4 | 5 | % --------------------------------------------------- 6 | % RUNTIME 7 | % --------------------------------------------------- 8 | 9 | % FFI to interact with the browser via a port from erlang 10 | % since gleam does not really support ports yet. 11 | % module: chrobot/chrome.gleam 12 | 13 | % The port is opened with the option "nouse_stdio" 14 | % which makes it use file descriptors 3 and 4 for stdin and stdout 15 | % This is what chrome expects when started with --remote-debugging-pipe. 16 | % A nice side effect of this is that chrome should quit when the pipe is closed, 17 | % avoiding the commmon port-related problem of zombie processes. 18 | open_browser_port(Command, Args) -> 19 | PortName = {spawn_executable, Command}, 20 | Options = [{args, Args}, binary, nouse_stdio, exit_status], 21 | try erlang:open_port(PortName, Options) of 22 | PortId -> 23 | erlang:link(PortId), 24 | {ok, PortId} 25 | catch 26 | error:Reason -> {error, Reason} 27 | end. 28 | 29 | send_to_port(Port, BinaryString) -> 30 | try erlang:port_command(Port, BinaryString) of 31 | true -> {ok, true} 32 | catch 33 | error:Reason -> {error, Reason} 34 | end. 35 | 36 | % --------------------------------------------------- 37 | % INSTALLER 38 | % --------------------------------------------------- 39 | 40 | % Utils for the installer script 41 | % module: chrobot/install.gleam 42 | 43 | % Get the architecture of the system 44 | get_arch() -> 45 | ArchCharlist = erlang:system_info(system_architecture), 46 | list_to_binary(ArchCharlist). 47 | 48 | % Run a shell command and return the output 49 | run_command(Command) -> 50 | CommandList = binary_to_list(Command), 51 | list_to_binary(os:cmd(CommandList)). 52 | 53 | % Unzip a file to a directory using the erlang stdlib zip module 54 | unzip(ZipFile, DestDir) -> 55 | ZipFileCharlist = binary_to_list(ZipFile), 56 | DestDirCharlist = binary_to_list(DestDir), 57 | try zip:unzip(ZipFileCharlist, [{cwd, DestDirCharlist}]) of 58 | {ok, _FileList} -> 59 | {ok, nil}; 60 | {error, _} = Error -> 61 | Error 62 | catch 63 | _:Reason -> 64 | {error, Reason} 65 | end. 66 | 67 | % Set the executable bit on a file 68 | set_executable(FilePath) -> 69 | FileInfo = file:read_file_info(FilePath), 70 | case FileInfo of 71 | {ok, FI} -> 72 | NewFI = FI#file_info{mode = 8#755}, 73 | case file:write_file_info(FilePath, NewFI) of 74 | ok -> {ok, nil}; 75 | {error, _} = Error -> Error 76 | end; 77 | {error, Reason} -> 78 | {error, Reason} 79 | end. 80 | 81 | % --------------------------------------------------- 82 | % UTILITIES 83 | % --------------------------------------------------- 84 | 85 | % Miscelaneous utilities 86 | % module: chrobot/internal/utils.gleam 87 | 88 | get_time_ms() -> 89 | os:system_time(millisecond). -------------------------------------------------------------------------------- /test/chrobot_test.gleam: -------------------------------------------------------------------------------- 1 | import birdie 2 | import chrobot 3 | import chrobot/chrome 4 | import chrobot/internal/utils 5 | import gleam/dynamic 6 | import gleam/erlang/process 7 | import gleam/io 8 | import gleam/list 9 | import gleam/result 10 | import gleam/string 11 | import gleeunit 12 | import gleeunit/should 13 | import mock_server 14 | import test_utils 15 | 16 | /// TEST SETUP 17 | /// The tests will only run if a browser path is set in the environment variable `CHROBOT_TEST_BROWSER_PATH`. 18 | pub fn main() { 19 | let test_browser_path = test_utils.try_get_browser_path() 20 | mock_server.start() 21 | 22 | case test_browser_path { 23 | Ok(browser_path) -> { 24 | io.println("Using test browser: " <> browser_path) 25 | gleeunit.main() 26 | } 27 | Error(_) -> { 28 | utils.err( 29 | "No test browser path was set! Please set the environment variable `CHROBOT_TEST_BROWSER_PATH` to run the tests.\n", 30 | ) 31 | let available_browser_path = 32 | result.lazy_or( 33 | chrome.get_local_chrome_path(), 34 | chrome.get_system_chrome_path, 35 | ) 36 | case available_browser_path { 37 | Ok(browser_path) -> { 38 | utils.hint( 39 | "A chrome path was detected on your system, you can run tests like this:", 40 | ) 41 | utils.show_cmd( 42 | "CHROBOT_TEST_BROWSER_PATH=\"" <> browser_path <> "\" gleam test\n", 43 | ) 44 | } 45 | Error(_) -> { 46 | utils.hint( 47 | "Consider installing a local version of chrome for the project:", 48 | ) 49 | utils.show_cmd("gleam run -m chrobot/install") 50 | } 51 | } 52 | panic as "See output above!" 53 | } 54 | } 55 | } 56 | 57 | pub fn open_test() { 58 | let browser = test_utils.get_browser_instance() 59 | let test_url = mock_server.get_url() 60 | use <- chrobot.defer_quit(browser) 61 | 62 | let page = 63 | chrobot.open(browser, test_url, 10_000) 64 | |> should.be_ok() 65 | 66 | chrobot.await_selector(page, "#wibble") 67 | |> should.be_ok() 68 | 69 | chrobot.get_all_html(page) 70 | |> should.be_ok() 71 | |> birdie.snap(title: "Opened Sample Page") 72 | } 73 | 74 | pub fn create_page_test() { 75 | let browser = test_utils.get_browser_instance() 76 | use <- chrobot.defer_quit(browser) 77 | let reference_html = test_utils.get_reference_html() 78 | let page = should.be_ok(chrobot.create_page(browser, reference_html, 10_000)) 79 | 80 | chrobot.get_all_html(page) 81 | |> should.be_ok() 82 | |> birdie.snap(title: "Created Page with Reference HTML") 83 | } 84 | 85 | pub fn eval_test() { 86 | use page <- test_utils.with_reference_page() 87 | let expression = "2 * Math.PI" 88 | chrobot.eval(page, expression) 89 | |> chrobot.as_value(dynamic.float) 90 | |> should.be_ok() 91 | |> should.equal(6.283185307179586) 92 | } 93 | 94 | pub fn eval_async_test() { 95 | use page <- test_utils.with_reference_page() 96 | let expression = 97 | "new Promise((resolve, reject) => setTimeout(() => resolve(42), 50))" 98 | chrobot.eval_async(page, expression) 99 | |> chrobot.as_value(dynamic.int) 100 | |> should.be_ok() 101 | |> should.equal(42) 102 | } 103 | 104 | pub fn eval_async_failure_test() { 105 | use page <- test_utils.with_reference_page() 106 | let expression = "Promise.reject(new Error('This is a test error'))" 107 | let result = chrobot.eval_async(page, expression) 108 | case result { 109 | Error(chrome.RuntimeException(text: text, column: column, line: line)) -> { 110 | text 111 | |> should.equal("Uncaught (in promise) Error: This is a test error") 112 | column 113 | |> should.equal(0) 114 | line 115 | |> should.equal(0) 116 | } 117 | other -> { 118 | utils.err( 119 | "Expected a chrome.RuntimeException, got: \n" <> string.inspect(other), 120 | ) 121 | panic as "Test failed! the result was not a chrome.RuntimeException!" 122 | } 123 | } 124 | } 125 | 126 | pub fn await_selector_test() { 127 | use page <- test_utils.with_reference_page() 128 | chrobot.await_selector(page, "body") 129 | |> should.be_ok() 130 | } 131 | 132 | pub fn await_selector_failure_test() { 133 | let browser = test_utils.get_browser_instance() 134 | use <- chrobot.defer_quit(browser) 135 | let reference_html = test_utils.get_reference_html() 136 | let page = 137 | chrobot.create_page(browser, reference_html, 10_000) 138 | |> should.be_ok() 139 | |> chrobot.with_timeout(100) 140 | 141 | chrobot.await_selector(page, "#bogus") 142 | |> should.be_error() 143 | } 144 | 145 | pub fn get_all_html_test() { 146 | let browser = test_utils.get_browser_instance() 147 | use <- chrobot.defer_quit(browser) 148 | let dummy_html = 149 | " 150 |

151 | I am HTML 152 |

153 |

154 | I am the hyperstructure 155 |

156 |

157 | I am linked to you 158 |

159 | " 160 | let page = 161 | chrobot.create_page(browser, dummy_html, 10_000) 162 | |> should.be_ok() 163 | let result = 164 | chrobot.get_all_html(page) 165 | |> should.be_ok() 166 | birdie.snap(result, title: "Outer HTML") 167 | } 168 | 169 | pub fn select_test() { 170 | use page <- test_utils.with_reference_page() 171 | let object_id = 172 | chrobot.select(page, "#wibble") 173 | |> should.be_ok 174 | let text_content = 175 | chrobot.get_text(page, object_id) 176 | |> should.be_ok() 177 | 178 | text_content 179 | |> should.equal("Wibble") 180 | } 181 | 182 | pub fn select_from_test() { 183 | use page <- test_utils.with_reference_page() 184 | let object_id = 185 | chrobot.select(page, ".greeting") 186 | |> should.be_ok 187 | 188 | let inner_object_id = 189 | chrobot.select_from(page, object_id, "span") 190 | |> should.be_ok 191 | 192 | let text_content = 193 | chrobot.get_text(page, inner_object_id) 194 | |> should.be_ok 195 | 196 | text_content 197 | |> should.equal("Joe") 198 | } 199 | 200 | pub fn get_html_test() { 201 | use page <- test_utils.with_reference_page() 202 | let object = 203 | chrobot.select(page, "header") 204 | |> should.be_ok 205 | 206 | let inner_html = 207 | chrobot.get_inner_html(page, object) 208 | |> should.be_ok 209 | 210 | let outer_html = 211 | chrobot.get_outer_html(page, object) 212 | |> should.be_ok 213 | 214 | birdie.snap(inner_html, title: "Element Inner HTML") 215 | birdie.snap(outer_html, title: "Element Outer HTML") 216 | } 217 | 218 | pub fn get_attribute_test() { 219 | use page <- test_utils.with_reference_page() 220 | let object_id = 221 | chrobot.select(page, "#wobble") 222 | |> should.be_ok 223 | 224 | let attribute = 225 | chrobot.get_attribute(page, object_id, "data-foo") 226 | |> should.be_ok 227 | 228 | attribute 229 | |> should.equal("bar") 230 | } 231 | 232 | pub fn select_all_test() { 233 | use page <- test_utils.with_reference_page() 234 | let object_ids = 235 | chrobot.select_all(page, "a") 236 | |> should.be_ok 237 | 238 | let hrefs = 239 | object_ids 240 | |> list.map(fn(object_id) { 241 | chrobot.get_attribute(page, object_id, "href") 242 | |> should.be_ok 243 | }) 244 | 245 | birdie.snap(string.join(hrefs, "\n"), title: "List of links") 246 | } 247 | 248 | pub fn select_all_from_test() { 249 | use page <- test_utils.with_reference_page() 250 | let object_id = 251 | chrobot.select(page, "ul") 252 | |> should.be_ok 253 | 254 | let inner_object_ids = 255 | chrobot.select_all_from(page, object_id, "li") 256 | |> should.be_ok 257 | 258 | let texts = 259 | inner_object_ids 260 | |> list.map(fn(inner_object_id) { 261 | chrobot.get_text(page, inner_object_id) 262 | |> should.be_ok 263 | }) 264 | 265 | birdie.snap(string.join(texts, "\n"), title: "List of greetings") 266 | } 267 | 268 | pub fn get_property_test() { 269 | use page <- test_utils.with_reference_page() 270 | let object_id = 271 | chrobot.select(page, "#demo-checkbox") 272 | |> should.be_ok 273 | 274 | chrobot.get_property(page, object_id, "checked", dynamic.bool) 275 | |> should.be_ok 276 | |> should.be_true 277 | } 278 | 279 | pub fn click_test() { 280 | use page <- test_utils.with_reference_page() 281 | 282 | // This is just a sanity check, to make sure the checkbox is checked before we click it 283 | let object_id = 284 | chrobot.select(page, "#demo-checkbox") 285 | |> should.be_ok 286 | 287 | chrobot.get_property(page, object_id, "checked", dynamic.bool) 288 | |> should.be_ok 289 | |> should.be_true 290 | 291 | // Click the checkbox 292 | chrobot.click(page, object_id) 293 | |> should.be_ok 294 | 295 | // After clicking the checkbox, it should be unchecked 296 | chrobot.get_property(page, object_id, "checked", dynamic.bool) 297 | |> should.be_ok 298 | |> should.be_false 299 | } 300 | 301 | pub fn type_test() { 302 | use page <- test_utils.with_reference_page() 303 | let object_id = 304 | chrobot.select(page, "#demo-text-input") 305 | |> should.be_ok 306 | 307 | chrobot.focus(page, object_id) 308 | |> should.be_ok 309 | 310 | chrobot.type_text(page, "Hello, World!") 311 | |> should.be_ok 312 | 313 | chrobot.get_property(page, object_id, "value", dynamic.string) 314 | |> should.be_ok 315 | |> should.equal("Hello, World!") 316 | } 317 | 318 | pub fn press_key_test() { 319 | use page <- test_utils.with_reference_page() 320 | let object_id = 321 | chrobot.select(page, "#demo-text-input") 322 | |> should.be_ok 323 | 324 | chrobot.focus(page, object_id) 325 | |> should.be_ok 326 | 327 | chrobot.press_key(page, "Enter") 328 | |> should.be_ok 329 | 330 | chrobot.get_property(page, object_id, "value", dynamic.string) 331 | |> should.be_ok 332 | |> should.equal("ENTER KEY PRESSED") 333 | } 334 | 335 | pub fn poll_test() { 336 | let initial_time = utils.get_time_ms() 337 | 338 | // this function will start returning "Success" in 200ms 339 | let poll_function = fn() { 340 | case utils.get_time_ms() - initial_time { 341 | time if time > 200 -> Ok("Success") 342 | _ -> Error(chrome.NotFoundError) 343 | } 344 | } 345 | 346 | chrobot.poll(poll_function, 500) 347 | |> should.be_ok() 348 | |> should.equal("Success") 349 | } 350 | 351 | pub fn poll_failure_test() { 352 | let initial_time = utils.get_time_ms() 353 | 354 | // this function will start returning "Success" in 200ms 355 | let poll_function = fn() { 356 | case utils.get_time_ms() - initial_time { 357 | time if time > 200 -> Ok("Success") 358 | _ -> Error(chrome.NotFoundError) 359 | } 360 | } 361 | 362 | case chrobot.poll(poll_function, 100) { 363 | Error(chrome.NotFoundError) -> { 364 | should.be_true(True) 365 | let elapsed_time = utils.get_time_ms() - initial_time 366 | // timeout should be within a 10ms window of accuracy 367 | { elapsed_time < 105 && elapsed_time > 95 } 368 | |> should.be_true() 369 | } 370 | _ -> { 371 | utils.err("Polling function didn't return the correct error") 372 | should.fail() 373 | } 374 | } 375 | } 376 | 377 | pub fn poll_timeout_failure_test() { 378 | let initial_time = utils.get_time_ms() 379 | 380 | // this function will return errors first 381 | // and after 100ms it will start blocking for 10s 382 | let poll_function = fn() { 383 | case utils.get_time_ms() - initial_time { 384 | time if time > 100 -> { 385 | process.sleep(10_000) 386 | Ok("Success") 387 | } 388 | _ -> Error(chrome.NotFoundError) 389 | } 390 | } 391 | 392 | // the timeout is 300ms, so the polling function will be interrupted 393 | // while it's blocking, it should still return the original error 394 | case chrobot.poll(poll_function, 300) { 395 | Error(chrome.NotFoundError) -> { 396 | should.be_true(True) 397 | let elapsed_time = utils.get_time_ms() - initial_time 398 | // timeout should be within a 10ms window of accuracy 399 | { elapsed_time < 305 && elapsed_time > 295 } 400 | |> should.be_true() 401 | } 402 | _ -> { 403 | utils.err("Polling function didn't return the correct error") 404 | should.fail() 405 | } 406 | } 407 | } 408 | -------------------------------------------------------------------------------- /test/codegen/download_protocol.gleam: -------------------------------------------------------------------------------- 1 | //// Download the latest protocol JSON files from the official repository 2 | //// and place them in the local assets folder. 3 | //// 4 | //// Protocol Repo is here: 5 | //// https://github.com/ChromeDevTools/devtools-protocol 6 | //// 7 | //// This script will panic if anything goes wrong, do not import this module anywere 8 | 9 | import gleam/http/request 10 | import gleam/httpc 11 | import gleam/io 12 | import simplifile as file 13 | 14 | const browser_protocol_url = "https://raw.githubusercontent.com/ChromeDevTools/devtools-protocol/master/json/browser_protocol.json" 15 | 16 | const js_protocol_url = "https://raw.githubusercontent.com/ChromeDevTools/devtools-protocol/master/json/js_protocol.json" 17 | 18 | const destination_dir = "./assets/" 19 | 20 | pub fn main() { 21 | download( 22 | from: browser_protocol_url, 23 | to: destination_dir <> "browser_protocol.json", 24 | ) 25 | download(from: js_protocol_url, to: destination_dir <> "js_protocol.json") 26 | } 27 | 28 | fn download(from origin_url: String, to destination_path: String) -> Nil { 29 | io.println("Making request to " <> origin_url) 30 | let assert Ok(request) = request.to(origin_url) 31 | let assert Ok(res) = httpc.send(request) 32 | case res.status { 33 | 200 -> { 34 | io.println("Writing response to " <> destination_path) 35 | let assert Ok(_) = file.write(res.body, to: destination_path) 36 | Nil 37 | } 38 | _ -> { 39 | io.println("Non-200 response from server!") 40 | panic 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /test/codegen/generate_bindings_test.gleam: -------------------------------------------------------------------------------- 1 | import birdie 2 | import codegen/generate_bindings.{ 3 | apply_protocol_patches, gen_domain_module, gen_root_module, 4 | get_stable_protocol, merge_protocols, parse_protocol, 5 | } 6 | import gleam/list 7 | import gleam/option 8 | import gleeunit/should 9 | 10 | pub fn parse_browser_protocol_test() { 11 | let assert Ok(protocol) = parse_protocol("./assets/browser_protocol.json") 12 | protocol.version.major 13 | |> should.equal("1") 14 | 15 | // Let's find the DOM domain 16 | let assert Ok(dom_domain) = 17 | list.find(protocol.domains, fn(d) { d.domain == "DOM" }) 18 | 19 | // It should have a types list 20 | option.is_some(dom_domain.types) 21 | |> should.be_true 22 | 23 | // The NodeId type should be on the DOM domain types 24 | let dom_types = option.unwrap(dom_domain.types, []) 25 | let assert Ok(node_id_type) = list.find(dom_types, fn(t) { t.id == "NodeId" }) 26 | 27 | // NodeId should have an inner type of "integer" 28 | let inner_type_is_int = case node_id_type.inner { 29 | generate_bindings.PrimitiveType("integer") -> True 30 | _ -> False 31 | } 32 | inner_type_is_int 33 | |> should.equal(True) 34 | } 35 | 36 | pub fn parse_js_protocol_test() { 37 | let assert Ok(protocol) = parse_protocol("./assets/js_protocol.json") 38 | protocol.version.major 39 | |> should.equal("1") 40 | 41 | // Let's find the Runtime domain 42 | let assert Ok(runtime_domain) = 43 | list.find(protocol.domains, fn(d) { d.domain == "Runtime" }) 44 | 45 | // It should have a types list 46 | option.is_some(runtime_domain.types) 47 | |> should.be_true 48 | 49 | // The DeepSerializedValue type should be on the Runtime domain types 50 | let runtime_types = option.unwrap(runtime_domain.types, []) 51 | let assert Ok(deep_serialized_value_type) = 52 | list.find(runtime_types, fn(t) { t.id == "DeepSerializedValue" }) 53 | 54 | // DeepSerializedValue should have an inner type of "object" with properties 55 | let assert Ok(target_properties) = case deep_serialized_value_type.inner { 56 | generate_bindings.ObjectType(option.Some(properties)) -> Ok(properties) 57 | _ -> Error("Did not find ObjectType with some properties") 58 | } 59 | 60 | // There should be a property named "type" in there 61 | let assert Ok(type_property) = 62 | list.find(target_properties, fn(p) { p.name == "type" }) 63 | 64 | // This "type" property should be of type string with enum values 65 | let assert Ok(enum_values) = case type_property.inner { 66 | generate_bindings.EnumType(values) -> Ok(values) 67 | _ -> Error("Property should was not an EnumType") 68 | } 69 | 70 | // One of the enum values should be "window" 71 | list.any(enum_values, fn(v) { v == "window" }) 72 | |> should.be_true 73 | } 74 | 75 | pub fn gen_enum_encoder_decoder_test() { 76 | let enum_type_name = "CertificateTransparencyCompliance" 77 | let enum_values = ["unknown", "not-compliant", "compliant"] 78 | generate_bindings.gen_enum_encoder(enum_type_name, enum_values) 79 | |> birdie.snap(title: "Enum encoder function") 80 | generate_bindings.gen_enum_decoder(enum_type_name, enum_values) 81 | |> birdie.snap(title: "Enum decoder function") 82 | } 83 | 84 | /// Just run all the functions, see if anything panics. 85 | /// We could snapshot the output here, but then again the output is just the codegen 86 | /// that's written to `protocol/*` and committed to vcs so we already have snapshots of 87 | /// it and would just duplicate those. 88 | pub fn general_bindings_gen_test() { 89 | let assert Ok(browser_protocol) = 90 | parse_protocol("./assets/browser_protocol.json") 91 | let assert Ok(js_protocol) = parse_protocol("./assets/js_protocol.json") 92 | let protocol = 93 | merge_protocols(browser_protocol, js_protocol) 94 | |> apply_protocol_patches() 95 | let stable_protocol = get_stable_protocol(protocol, False, False) 96 | gen_root_module(stable_protocol) 97 | list.each(stable_protocol.domains, fn(domain) { 98 | gen_domain_module(stable_protocol, domain) 99 | }) 100 | } 101 | -------------------------------------------------------------------------------- /test/mock_server.gleam: -------------------------------------------------------------------------------- 1 | //// The mock server listens on localhost and returns some fixed data, 2 | //// it can be used in tests, to avoid the need to request an external website 3 | 4 | import chrobot/internal/utils 5 | import gleam/bytes_tree 6 | import gleam/http/request.{type Request} 7 | import gleam/http/response.{type Response} 8 | import gleam/int 9 | import gleam/string 10 | import mist.{type Connection, type ResponseData} 11 | 12 | pub fn get_port() -> Int { 13 | 8182 14 | } 15 | 16 | pub fn get_url() -> String { 17 | "http://localhost:" <> int.to_string(get_port()) <> "/" 18 | } 19 | 20 | pub fn start() { 21 | let not_found = 22 | response.new(404) 23 | |> response.set_body(mist.Bytes(bytes_tree.from_string("Not found!"))) 24 | 25 | let result = 26 | fn(req: Request(Connection)) -> Response(ResponseData) { 27 | case request.path_segments(req) { 28 | [] -> return_test_page(req) 29 | _ -> not_found 30 | } 31 | } 32 | |> mist.new 33 | |> mist.port(get_port()) 34 | |> mist.start_http 35 | 36 | case result { 37 | Ok(_) -> Nil 38 | Error(err) -> { 39 | utils.err("The chrobot test server failed to start! 40 | The server tries to list on on port " <> int.to_string(get_port()) <> ", perhaps it's in use?") 41 | panic as string.inspect(err) 42 | } 43 | } 44 | } 45 | 46 | pub type MyMessage { 47 | Broadcast(String) 48 | } 49 | 50 | fn return_test_page(_request: Request(Connection)) -> Response(ResponseData) { 51 | let body = 52 | " 53 | 54 | 55 | Chrobot Test Page 56 | 57 | 58 |

Chrobot Test Page

59 |
wobble
60 | 61 | 62 | " 63 | 64 | response.new(200) 65 | |> response.set_body(mist.Bytes(bytes_tree.from_string(body))) 66 | |> response.set_header("content-type", "text/html") 67 | } 68 | -------------------------------------------------------------------------------- /test/protocol/runtime_test.gleam: -------------------------------------------------------------------------------- 1 | import birdie 2 | import chrobot/chrome 3 | import chrobot/protocol/runtime 4 | import gleam/dynamic 5 | import gleam/json 6 | import gleam/option.{type Option, None, Some} 7 | import gleam/string 8 | import gleeunit/should 9 | import simplifile as file 10 | 11 | /// This module havs some types with dynamic values. 12 | /// We can't currently encode them, which could lead to confusion, 13 | /// encoders also don't have a failure mode, so we can't return an error. 14 | /// We should ensure a message is logged to stdio when this happens. 15 | pub fn enocde_dynamic_test() { 16 | // We can't assert that it actually logs, but we can **hope** 17 | runtime.encode__call_argument(runtime.CallArgument( 18 | value: Some(dynamic.from("My dynamic value")), 19 | unserializable_value: None, 20 | object_id: Some(runtime.RemoteObjectId("1")), 21 | )) 22 | |> json.to_string() 23 | |> birdie.snap("Enocded CallArgument with dynamic value") 24 | } 25 | 26 | pub fn evaluate_test() { 27 | let mock_callback = fn(method, params: Option(json.Json)) -> Result( 28 | dynamic.Dynamic, 29 | chrome.RequestError, 30 | ) { 31 | method 32 | |> should.equal("Runtime.evaluate") 33 | 34 | params 35 | |> should.be_some() 36 | |> json.to_string() 37 | |> birdie.snap("Runtime.evaluate params") 38 | 39 | let assert Ok(response_file) = 40 | file.read("test_assets/runtime_evaluate_response.json") 41 | let assert Ok(response) = json.decode(response_file, dynamic.dynamic) 42 | 43 | Ok(response) 44 | } 45 | 46 | runtime.evaluate( 47 | mock_callback, 48 | expression: "document.querySelector(\"h1\")", 49 | object_group: None, 50 | include_command_line_api: None, 51 | silent: Some(False), 52 | context_id: None, 53 | return_by_value: Some(True), 54 | user_gesture: Some(True), 55 | await_promise: Some(False), 56 | ) 57 | |> string.inspect() 58 | |> birdie.snap("Runtime.evaluate response") 59 | } 60 | -------------------------------------------------------------------------------- /test/test_utils.gleam: -------------------------------------------------------------------------------- 1 | //// Shared test utilities 2 | 3 | import chrobot 4 | import chrobot/chrome 5 | import envoy 6 | import gleeunit/should 7 | import simplifile as file 8 | 9 | /// Try to get the path to the browser to use for tests 10 | /// If the CHROBOT_TEST_BROWSER_PATH environment variable is not set, this will return an error 11 | /// -> use in test setup to validate that the environment variable is set 12 | pub fn try_get_browser_path() { 13 | envoy.get("CHROBOT_TEST_BROWSER_PATH") 14 | } 15 | 16 | /// Get the path to the browser to use for tests 17 | /// If the CHROBOT_TEST_BROWSER_PATH environment variable is not set, this will panic 18 | /// -> use in tests to get the browser path 19 | /// -> we can assume that the environment variable is set, as we have already validated it in the test setup 20 | pub fn get_browser_path() { 21 | let assert Ok(browser_path) = try_get_browser_path() 22 | browser_path 23 | } 24 | 25 | pub fn get_browser_instance() { 26 | let browser_path = get_browser_path() 27 | let config = 28 | chrome.BrowserConfig( 29 | path: browser_path, 30 | args: chrome.get_default_chrome_args(), 31 | start_timeout: 5000, 32 | log_level: chrome.LogLevelWarnings, 33 | ) 34 | let browser = should.be_ok(chrome.launch_with_config(config)) 35 | browser 36 | } 37 | 38 | /// for use with a use expression 39 | pub fn with_reference_page(apply fun) { 40 | let browser = get_browser_instance() 41 | let reference_html = get_reference_html() 42 | let assert Ok(page) = chrobot.create_page(browser, reference_html, 10_000) 43 | should.be_ok(chrobot.await_selector(page, "body")) 44 | fun(page) 45 | chrobot.quit(browser) 46 | } 47 | 48 | pub fn get_reference_html() { 49 | let assert Ok(content) = file.read("test_assets/reference_website.html") 50 | content 51 | } 52 | -------------------------------------------------------------------------------- /test_assets/reference_website.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
9 | The World Wide Web project 10 | 11 | 12 |
13 | 14 |

World Wide Web

The WorldWideWeb (W3) is a wide-area 16 | hypermedia information retrieval 17 | initiative aiming to give universal 18 | access to a large universe of documents.

19 | Everything there is online about 20 | W3 is linked directly or indirectly 21 | to this document, including an executive 22 | summary of the project, Mailing lists 24 | , Policy , November's W3 news , 26 | Frequently Asked Questions . 27 |

28 |
29 |
What's out there? 30 |
31 |
Pointers to the 32 | world's online information, 33 | subjects 34 | , W3 servers, etc. 35 |
36 |
Help 37 |
38 |
on the browser you are using 39 |
40 |
Software Products 41 |
42 |
A list of W3 project 43 | components and their current state. 44 | (e.g. Line Mode ,X11 Viola , NeXTStep 47 | , Servers , Tools , Mail robot , 51 | Library ) 52 |
53 |
Technical 54 |
55 |
Details of protocols, formats, 56 | program internals etc 57 |
58 |
Bibliography 59 |
60 |
Paper documentation 61 | on W3 and references. 62 |
63 |
People 64 |
65 |
A list of some people involved 66 | in the project. 67 |
68 |
History 69 |
70 |
A summary of the history 71 | of the project. 72 |
73 |
How can I help ? 74 |
75 |
If you would like 76 | to support the web.. 77 |
78 |
Getting code 79 |
80 |
Getting the code by 81 | anonymous FTP , etc. 82 |
83 |
84 | 85 |
86 |

And now for something completely different

87 |
88 | Wibble 89 |
90 |
91 | Wobble 92 |
93 | 94 | 🤖 95 | 96 |
97 | Hello Joe 98 |
99 | 100 |
    101 |
  1. один
  2. 102 |
  3. два
  4. 103 |
  5. три
  6. 104 |
105 | 106 |
    107 |
  • One
  • 108 |
  • Two
  • 109 |
  • Three
  • 110 |
111 | 112 |
113 | 114 | 115 |
116 | 117 |
118 | 119 | 120 |
121 |
122 | 123 | 134 | 135 | 136 | -------------------------------------------------------------------------------- /test_assets/runtime_evaluate_response.json: -------------------------------------------------------------------------------- 1 | { 2 | "result": { 3 | "type": "object", 4 | "subtype": "node", 5 | "className": "HTMLHeadingElement", 6 | "description": "h1", 7 | "objectId": "8282282834669415287.1.3079", 8 | "preview": { 9 | "type": "object", 10 | "subtype": "node", 11 | "description": "h1", 12 | "overflow": true, 13 | "properties": [ 14 | { 15 | "name": "align", 16 | "type": "string", 17 | "value": "" 18 | }, 19 | { 20 | "name": "title", 21 | "type": "string", 22 | "value": "" 23 | }, 24 | { 25 | "name": "lang", 26 | "type": "string", 27 | "value": "" 28 | }, 29 | { 30 | "name": "translate", 31 | "type": "boolean", 32 | "value": "true" 33 | }, 34 | { 35 | "name": "dir", 36 | "type": "string", 37 | "value": "" 38 | } 39 | ] 40 | } 41 | } 42 | } -------------------------------------------------------------------------------- /vendor/justin_fork/.gitignore: -------------------------------------------------------------------------------- 1 | *.beam 2 | *.ez 3 | build 4 | erl_crash.dump 5 | -------------------------------------------------------------------------------- /vendor/justin_fork/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## v1.0.0 - 2023-12-30 4 | 5 | - Initial release. 6 | -------------------------------------------------------------------------------- /vendor/justin_fork/README.md: -------------------------------------------------------------------------------- 1 | # justin (Fork) 2 | 3 | Forked from: https://github.com/lpil/justin 4 | 5 | Convert between snake_case, camelCase, and other cases in Gleam. 6 | 7 | [![Package Version](https://img.shields.io/hexpm/v/justin)](https://hex.pm/packages/justin) 8 | [![Hex Docs](https://img.shields.io/badge/hex-docs-ffaff3)](https://hexdocs.pm/justin/) 9 | 10 | ```sh 11 | gleam add justin 12 | ``` 13 | ```gleam 14 | import justin 15 | 16 | pub fn main() { 17 | justin.snake_case("Hello World") 18 | // -> "hello_world" 19 | 20 | justin.camel_case("Hello World") 21 | // -> "helloWorld" 22 | 23 | justin.pascal_case("Hello World") 24 | // -> "HelloWorld" 25 | 26 | justin.kebab_case("Hello World") 27 | // -> "hello-world 28 | 29 | justin.sentence_case("hello-world") 30 | // -> "Hello world" 31 | } 32 | ``` 33 | 34 | Further documentation can be found at . 35 | 36 | ## Fork Notes 37 | 38 | This fork of lpil/justin adds support for the following use case: 39 | 40 | ```gleam 41 | justin.snake_case("DOMDebugger") 42 | |> should.equal("dom_debugger") 43 | justin.snake_case("CSSLayerData") 44 | |> should.equal("css_layer_data") 45 | ``` 46 | 47 | Which is deliberately not supported upstream, see: 48 | https://github.com/lpil/justin/pull/2 -------------------------------------------------------------------------------- /vendor/justin_fork/gleam.toml: -------------------------------------------------------------------------------- 1 | name = "justin_fork" 2 | version = "1.0.0" 3 | description = "Convert between snake_case, camelCase, and other cases in Gleam" 4 | licences = ["Apache-2.0"] 5 | repository = { type = "github", user = "lpil", repo = "justin" } 6 | links = [ 7 | { title = "Website", href = "https://gleam.run" }, 8 | { title = "Sponsor", href = "https://github.com/sponsors/lpil" }, 9 | ] 10 | gleam = ">= 0.32.0" 11 | 12 | 13 | [dependencies] 14 | gleam_stdlib = ">= 0.34.0 and < 2.0.0" 15 | 16 | [dev-dependencies] 17 | gleeunit = ">= 1.0.0 and < 2.0.0" 18 | -------------------------------------------------------------------------------- /vendor/justin_fork/manifest.toml: -------------------------------------------------------------------------------- 1 | # This file was generated by Gleam 2 | # You typically do not need to edit this file 3 | 4 | packages = [ 5 | { name = "gleam_stdlib", version = "0.37.0", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "5398BD6C2ABA17338F676F42F404B9B7BABE1C8DC7380031ACB05BBE1BCF3742" }, 6 | { name = "gleeunit", version = "1.1.2", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleeunit", source = "hex", outer_checksum = "72CDC3D3F719478F26C4E2C5FED3E657AC81EC14A47D2D2DEBB8693CA3220C3B" }, 7 | ] 8 | 9 | [requirements] 10 | gleam_stdlib = { version = ">= 0.34.0 and < 2.0.0" } 11 | gleeunit = { version = ">= 1.0.0 and < 2.0.0" } 12 | -------------------------------------------------------------------------------- /vendor/justin_fork/src/justin_fork.gleam: -------------------------------------------------------------------------------- 1 | import gleam/list 2 | import gleam/string 3 | 4 | /// Convert a string to a `snake_case`. 5 | /// 6 | /// # Examples 7 | /// 8 | /// ```gleam 9 | /// snake_case("Hello World") 10 | /// // -> "hello_world" 11 | /// ``` 12 | /// 13 | pub fn snake_case(text: String) -> String { 14 | text 15 | |> split_words 16 | |> string.join("_") 17 | |> string.lowercase 18 | } 19 | 20 | /// Convert a string to a `camelCase`. 21 | /// 22 | /// # Examples 23 | /// 24 | /// ```gleam 25 | /// camel_case("Hello World") 26 | /// // -> "helloWorld" 27 | /// ``` 28 | /// 29 | pub fn camel_case(text: String) -> String { 30 | text 31 | |> split_words 32 | |> list.index_map(fn(word, i) { 33 | case i { 34 | 0 -> string.lowercase(word) 35 | _ -> string.capitalise(word) 36 | } 37 | }) 38 | |> string.concat 39 | } 40 | 41 | /// Convert a string to a `PascalCase`. 42 | /// 43 | /// # Examples 44 | /// 45 | /// ```gleam 46 | /// pascal_case("Hello World") 47 | /// // -> "HelloWorld" 48 | /// ``` 49 | /// 50 | pub fn pascal_case(text: String) -> String { 51 | text 52 | |> split_words 53 | |> list.map(string.capitalise) 54 | |> string.concat 55 | } 56 | 57 | /// Convert a string to a `kebab-case`. 58 | /// 59 | /// # Examples 60 | /// 61 | /// ```gleam 62 | /// kabab_case("Hello World") 63 | /// // -> "hello-world 64 | /// ``` 65 | /// 66 | pub fn kebab_case(text: String) -> String { 67 | text 68 | |> split_words 69 | |> string.join("-") 70 | |> string.lowercase 71 | } 72 | 73 | /// Convert a string to a `Sentence case`. 74 | /// 75 | /// # Examples 76 | /// 77 | /// ```gleam 78 | /// sentence_case("hello-world") 79 | /// // -> "Hello world 80 | /// ``` 81 | /// 82 | pub fn sentence_case(text: String) -> String { 83 | text 84 | |> split_words 85 | |> string.join(" ") 86 | |> string.capitalise 87 | } 88 | 89 | fn split_words(text: String) -> List(String) { 90 | text 91 | |> string.to_graphemes 92 | |> split(False, "", []) 93 | } 94 | 95 | fn split( 96 | in: List(String), 97 | up: Bool, 98 | word: String, 99 | words: List(String), 100 | ) -> List(String) { 101 | case in { 102 | [] if word == "" -> list.reverse(words) 103 | [] -> list.reverse(add(words, word)) 104 | 105 | ["\n", ..in] 106 | | ["\t", ..in] 107 | | ["!", ..in] 108 | | ["?", ..in] 109 | | ["#", ..in] 110 | | [".", ..in] 111 | | ["-", ..in] 112 | | ["_", ..in] 113 | | [" ", ..in] -> split(in, False, "", add(words, word)) 114 | 115 | [g, ..in] -> { 116 | case is_upper(g) { 117 | // Lowercase, not a new word 118 | False -> split(in, False, word <> g, words) 119 | // Uppercase and inside an uppercase word 120 | True if up -> { 121 | case in { 122 | [] 123 | | ["\n", ..] 124 | | ["\t", ..] 125 | | ["!", ..] 126 | | ["?", ..] 127 | | ["#", ..] 128 | | [".", ..] 129 | | ["-", ..] 130 | | ["_", ..] 131 | | [" ", ..] -> split(in, up, word <> g, words) 132 | [nxt, ..] -> { 133 | case is_upper(nxt) { 134 | True -> split(in, up, word <> g, words) 135 | // It's a new word if the next letter is lowercase 136 | False -> { 137 | split(in, False, g, add(words, word)) 138 | } 139 | } 140 | } 141 | } 142 | } 143 | 144 | // Uppercase otherwise, a new word 145 | True -> split(in, True, g, add(words, word)) 146 | } 147 | } 148 | } 149 | } 150 | 151 | fn add(words: List(String), word: String) -> List(String) { 152 | case word { 153 | "" -> words 154 | _ -> [word, ..words] 155 | } 156 | } 157 | 158 | fn is_upper(g: String) -> Bool { 159 | string.lowercase(g) != g 160 | } 161 | -------------------------------------------------------------------------------- /vendor/justin_fork/test/justin_fork_test.gleam: -------------------------------------------------------------------------------- 1 | import gleam/list 2 | import gleeunit 3 | import justin_fork as justin 4 | 5 | pub fn main() { 6 | gleeunit.main() 7 | } 8 | 9 | const snake_cases = [ 10 | #("", ""), #("snake case", "snake_case"), #("snakeCase", "snake_case"), 11 | #("SNAKECase", "snake_case"), #("Snake-Case", "snake_case"), 12 | #("Snake_Case", "snake_case"), #("SnakeCase", "snake_case"), 13 | #("Snake.Case", "snake_case"), #("SNAKE_CASE", "snake_case"), 14 | #("--snake-case--", "snake_case"), #("snake#case", "snake_case"), 15 | #("snake?!case", "snake_case"), #("snake\tcase", "snake_case"), 16 | #("snake\ncase", "snake_case"), #("λambdaΛambda", "λambda_λambda"), 17 | ] 18 | 19 | const camel_cases = [ 20 | #("", ""), #("snake case", "snakeCase"), #("snakeCase", "snakeCase"), 21 | #("Snake-Case", "snakeCase"), #("SNAKECase", "snakeCase"), 22 | #("Snake_Case", "snakeCase"), #("SnakeCase", "snakeCase"), 23 | #("Snake.Case", "snakeCase"), #("SNAKE_CASE", "snakeCase"), 24 | #("--snake-case--", "snakeCase"), #("snake#case", "snakeCase"), 25 | #("snake?!case", "snakeCase"), #("snake\tcase", "snakeCase"), 26 | #("snake\ncase", "snakeCase"), #("λambda_λambda", "λambdaΛambda"), 27 | ] 28 | 29 | const pascal_cases = [ 30 | #("", ""), #("snake case", "SnakeCase"), #("snakeCase", "SnakeCase"), 31 | #("SNAKECase", "SnakeCase"), #("Snake-Case", "SnakeCase"), 32 | #("Snake_Case", "SnakeCase"), #("SnakeCase", "SnakeCase"), 33 | #("Snake.Case", "SnakeCase"), #("SNAKE_CASE", "SnakeCase"), 34 | #("--snake-case--", "SnakeCase"), #("snake#case", "SnakeCase"), 35 | #("snake?!case", "SnakeCase"), #("snake\tcase", "SnakeCase"), 36 | #("snake\ncase", "SnakeCase"), #("λambda_λambda", "ΛambdaΛambda"), 37 | ] 38 | 39 | const kebab_cases = [ 40 | #("", ""), #("snake case", "snake-case"), #("snakeCase", "snake-case"), 41 | #("SNAKECase", "snake-case"), #("Snake-Case", "snake-case"), 42 | #("Snake_Case", "snake-case"), #("SnakeCase", "snake-case"), 43 | #("Snake.Case", "snake-case"), #("SNAKE_CASE", "snake-case"), 44 | #("--snake-case--", "snake-case"), #("snake#case", "snake-case"), 45 | #("snake?!case", "snake-case"), #("snake\tcase", "snake-case"), 46 | #("snake\ncase", "snake-case"), #("λambda_λambda", "λambda-λambda"), 47 | ] 48 | 49 | const sentence_cases = [ 50 | #("", ""), #("snake case", "Snake case"), #("snakeCase", "Snake case"), 51 | #("SNAKECase", "Snake case"), #("Snake-Case", "Snake case"), 52 | #("Snake_Case", "Snake case"), #("SnakeCase", "Snake case"), 53 | #("Snake.Case", "Snake case"), #("SNAKE_CASE", "Snake case"), 54 | #("--snake-case--", "Snake case"), #("snake#case", "Snake case"), 55 | #("snake?!case", "Snake case"), #("snake\tcase", "Snake case"), 56 | #("snake\ncase", "Snake case"), #("λambda_λambda", "Λambda λambda"), 57 | ] 58 | 59 | fn run_cases(cases: List(#(String, String)), function: fn(String) -> String) { 60 | use #(in, out) <- list.each(cases) 61 | let real = function(in) 62 | case real == out { 63 | True -> Nil 64 | False -> panic as { in <> " should be " <> out <> ", got " <> real } 65 | } 66 | } 67 | 68 | pub fn snake_test() { 69 | run_cases(snake_cases, justin.snake_case) 70 | } 71 | 72 | pub fn camel_test() { 73 | run_cases(camel_cases, justin.camel_case) 74 | } 75 | 76 | pub fn pascal_test() { 77 | run_cases(pascal_cases, justin.pascal_case) 78 | } 79 | 80 | pub fn kebab_test() { 81 | run_cases(kebab_cases, justin.kebab_case) 82 | } 83 | 84 | pub fn sentence_test() { 85 | run_cases(sentence_cases, justin.sentence_case) 86 | } 87 | --------------------------------------------------------------------------------