webdriver.h 9.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278
  1. // webdriver.h — Thin W3C WebDriver client using cpp-httplib + nlohmann/json.
  2. // SPDX-License-Identifier: MIT
  3. //
  4. // Usage:
  5. // webdriver::Session session; // starts headless Firefox via geckodriver
  6. // session.navigate("http://localhost:8080");
  7. // auto el = session.css("h1");
  8. // assert(el.text() == "Hello!");
  9. // // session destructor closes the browser
  10. #pragma once
  11. #include "httplib.h"
  12. #include <nlohmann/json.hpp>
  13. #include <stdexcept>
  14. #include <string>
  15. #include <thread>
  16. #include <vector>
  17. namespace webdriver {
  18. using json = nlohmann::json;
  19. // ─── Errors ──────────────────────────────────────────────────
  20. class Error : public std::runtime_error {
  21. public:
  22. using std::runtime_error::runtime_error;
  23. };
  24. // ─── Forward declarations ────────────────────────────────────
  25. class Session;
  26. // ─── Element ─────────────────────────────────────────────────
  27. class Element {
  28. friend class Session;
  29. httplib::Client *cli_;
  30. std::string session_id_;
  31. std::string element_id_;
  32. public:
  33. Element(httplib::Client *cli, const std::string &session_id,
  34. const std::string &element_id)
  35. : cli_(cli), session_id_(session_id), element_id_(element_id) {}
  36. std::string url(const std::string &suffix = "") const {
  37. return "/session/" + session_id_ + "/element/" + element_id_ + suffix;
  38. }
  39. public:
  40. std::string text() const {
  41. auto res = cli_->Get(url("/text"));
  42. if (!res || res->status != 200) {
  43. throw Error("Failed to get element text");
  44. }
  45. return json::parse(res->body)["value"].get<std::string>();
  46. }
  47. std::string attribute(const std::string &name) const {
  48. auto res = cli_->Get(url("/attribute/" + name));
  49. if (!res || res->status != 200) { return ""; }
  50. auto val = json::parse(res->body)["value"];
  51. return val.is_null() ? "" : val.get<std::string>();
  52. }
  53. std::string property(const std::string &name) const {
  54. auto res = cli_->Get(url("/property/" + name));
  55. if (!res || res->status != 200) { return ""; }
  56. auto val = json::parse(res->body)["value"];
  57. return val.is_null() ? "" : val.get<std::string>();
  58. }
  59. void click() const {
  60. auto res = cli_->Post(url("/click"), "{}", "application/json");
  61. if (!res || res->status != 200) { throw Error("Failed to click element"); }
  62. }
  63. void send_keys(const std::string &keys) const {
  64. json body = {{"text", keys}};
  65. auto res = cli_->Post(url("/value"), body.dump(), "application/json");
  66. if (!res || res->status != 200) {
  67. throw Error("Failed to send keys to element");
  68. }
  69. }
  70. void clear() const {
  71. auto res = cli_->Post(url("/clear"), "{}", "application/json");
  72. if (!res || res->status != 200) { throw Error("Failed to clear element"); }
  73. }
  74. std::string tag_name() const {
  75. auto res = cli_->Get(url("/name"));
  76. if (!res || res->status != 200) { throw Error("Failed to get tag name"); }
  77. return json::parse(res->body)["value"].get<std::string>();
  78. }
  79. bool is_displayed() const {
  80. auto res = cli_->Get(url("/displayed"));
  81. if (!res || res->status != 200) { return false; }
  82. return json::parse(res->body)["value"].get<bool>();
  83. }
  84. };
  85. // ─── Session ─────────────────────────────────────────────────
  86. class Session {
  87. httplib::Client cli_;
  88. std::string session_id_;
  89. // W3C WebDriver uses this key for element references
  90. static constexpr const char *ELEMENT_KEY =
  91. "element-6066-11e4-a52e-4f735466cecf";
  92. std::string extract_element_id(const json &value) const {
  93. if (value.contains(ELEMENT_KEY)) {
  94. return value[ELEMENT_KEY].get<std::string>();
  95. }
  96. // Fallback: try "ELEMENT" (older protocol)
  97. if (value.contains("ELEMENT")) {
  98. return value["ELEMENT"].get<std::string>();
  99. }
  100. throw Error("No element identifier in response: " + value.dump());
  101. }
  102. std::string url(const std::string &suffix) const {
  103. return "/session/" + session_id_ + suffix;
  104. }
  105. public:
  106. explicit Session(const std::string &host = "127.0.0.1", int port = 4444)
  107. : cli_(host, port) {
  108. cli_.set_read_timeout(std::chrono::seconds(30));
  109. cli_.set_connection_timeout(std::chrono::seconds(5));
  110. json caps = {
  111. {"capabilities",
  112. {{"alwaysMatch",
  113. {{"moz:firefoxOptions", {{"args", json::array({"-headless"})}}}}}}}};
  114. auto res = cli_.Post("/session", caps.dump(), "application/json");
  115. if (!res) { throw Error("Cannot connect to geckodriver"); }
  116. if (res->status != 200) {
  117. throw Error("Failed to create session: " + res->body);
  118. }
  119. auto body = json::parse(res->body);
  120. session_id_ = body["value"]["sessionId"].get<std::string>();
  121. }
  122. ~Session() {
  123. try {
  124. cli_.Delete(url(""));
  125. } catch (...) {}
  126. }
  127. // Non-copyable, non-movable (owns a session)
  128. Session(const Session &) = delete;
  129. Session &operator=(const Session &) = delete;
  130. // ─── Navigation ──────────────────────────────────────────
  131. void navigate(const std::string &nav_url) {
  132. json body = {{"url", nav_url}};
  133. auto res = cli_.Post(url("/url"), body.dump(), "application/json");
  134. if (!res || res->status != 200) {
  135. throw Error("Failed to navigate to: " + nav_url);
  136. }
  137. }
  138. std::string title() {
  139. auto res = cli_.Get(url("/title"));
  140. if (!res || res->status != 200) { throw Error("Failed to get title"); }
  141. return json::parse(res->body)["value"].get<std::string>();
  142. }
  143. std::string current_url() {
  144. auto res = cli_.Get(url("/url"));
  145. if (!res || res->status != 200) {
  146. throw Error("Failed to get current URL");
  147. }
  148. return json::parse(res->body)["value"].get<std::string>();
  149. }
  150. // ─── Find elements ──────────────────────────────────────
  151. Element find(const std::string &using_, const std::string &value) {
  152. json body = {{"using", using_}, {"value", value}};
  153. auto res = cli_.Post(url("/element"), body.dump(), "application/json");
  154. if (!res || res->status != 200) {
  155. throw Error("Element not found: " + using_ + "=" + value);
  156. }
  157. auto eid = extract_element_id(json::parse(res->body)["value"]);
  158. return Element(&cli_, session_id_, eid);
  159. }
  160. std::vector<Element> find_all(const std::string &using_,
  161. const std::string &value) {
  162. json body = {{"using", using_}, {"value", value}};
  163. auto res = cli_.Post(url("/elements"), body.dump(), "application/json");
  164. if (!res || res->status != 200) { return {}; }
  165. std::vector<Element> elements;
  166. for (auto &v : json::parse(res->body)["value"]) {
  167. elements.emplace_back(&cli_, session_id_, extract_element_id(v));
  168. }
  169. return elements;
  170. }
  171. // Convenience: find by CSS selector
  172. Element css(const std::string &selector) {
  173. return find("css selector", selector);
  174. }
  175. std::vector<Element> css_all(const std::string &selector) {
  176. return find_all("css selector", selector);
  177. }
  178. // ─── Wait ────────────────────────────────────────────────
  179. // Poll for an element until it appears or timeout
  180. Element wait_for(const std::string &selector, int timeout_ms = 5000) {
  181. auto deadline = std::chrono::steady_clock::now() +
  182. std::chrono::milliseconds(timeout_ms);
  183. while (std::chrono::steady_clock::now() < deadline) {
  184. try {
  185. return css(selector);
  186. } catch (...) {
  187. std::this_thread::sleep_for(std::chrono::milliseconds(200));
  188. }
  189. }
  190. throw Error("Timeout waiting for element: " + selector);
  191. }
  192. // Wait until a JS expression returns truthy
  193. bool wait_until(const std::string &script, int timeout_ms = 5000) {
  194. auto deadline = std::chrono::steady_clock::now() +
  195. std::chrono::milliseconds(timeout_ms);
  196. while (std::chrono::steady_clock::now() < deadline) {
  197. auto result = execute_script(script);
  198. if (result != "null" && result != "false" && result != "" &&
  199. result != "0" && result != "undefined") {
  200. return true;
  201. }
  202. std::this_thread::sleep_for(std::chrono::milliseconds(200));
  203. }
  204. return false;
  205. }
  206. // ─── Execute script ─────────────────────────────────────
  207. std::string execute_script(const std::string &script,
  208. const json &args = json::array()) {
  209. json body = {{"script", script}, {"args", args}};
  210. auto res = cli_.Post(url("/execute/sync"), body.dump(), "application/json");
  211. if (!res || res->status != 200) {
  212. throw Error("Failed to execute script: " + script);
  213. }
  214. auto val = json::parse(res->body)["value"];
  215. if (val.is_null()) { return "null"; }
  216. if (val.is_string()) { return val.get<std::string>(); }
  217. return val.dump();
  218. }
  219. // ─── Page source ────────────────────────────────────────
  220. std::string page_source() {
  221. auto res = cli_.Get(url("/source"));
  222. if (!res || res->status != 200) {
  223. throw Error("Failed to get page source");
  224. }
  225. return json::parse(res->body)["value"].get<std::string>();
  226. }
  227. };
  228. } // namespace webdriver