1
0

test_webui.cpp 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300
  1. // test_webui.cpp — Browser-based E2E tests for Ch5 Web UI.
  2. // Uses webdriver.h (cpp-httplib + json.hpp) to control headless Firefox.
  3. //
  4. // Usage: test_webui <port>
  5. // port: the translate-server port (e.g. 18080)
  6. #include "webdriver.h"
  7. #include <cstdlib>
  8. #include <iostream>
  9. #include <string>
  10. // ─── Test framework (minimal) ────────────────────────────────
  11. static int pass_count = 0;
  12. static int fail_count = 0;
  13. #define PASS(label) \
  14. do { \
  15. std::cout << " PASS: " << (label) << "\n"; \
  16. ++pass_count; \
  17. } while (0)
  18. #define FAIL(label, detail) \
  19. do { \
  20. std::cout << " FAIL: " << (label) << "\n"; \
  21. std::cout << " " << (detail) << "\n"; \
  22. ++fail_count; \
  23. } while (0)
  24. #define ASSERT_TRUE(cond, label) \
  25. do { \
  26. if (cond) { \
  27. PASS(label); \
  28. } else { \
  29. FAIL(label, "condition was false"); \
  30. } \
  31. } while (0)
  32. #define ASSERT_CONTAINS(haystack, needle, label) \
  33. do { \
  34. if (std::string(haystack).find(needle) != std::string::npos) { \
  35. PASS(label); \
  36. } else { \
  37. FAIL(label, "'" + std::string(haystack) + "' does not contain '" + \
  38. std::string(needle) + "'"); \
  39. } \
  40. } while (0)
  41. #define ASSERT_ELEMENT_EXISTS(session, selector) \
  42. do { \
  43. try { \
  44. (session).css(selector); \
  45. PASS("Element " selector " exists"); \
  46. } catch (...) { FAIL("Element " selector " exists", "not found"); } \
  47. } while (0)
  48. // ─── Helpers ─────────────────────────────────────────────────
  49. static std::string base_url;
  50. void navigate_and_wait_for_models(webdriver::Session &session) {
  51. session.navigate(base_url);
  52. session.wait_until(
  53. "return document.querySelectorAll('#model-select option').length > 0",
  54. 5000);
  55. }
  56. void test_page_loads(webdriver::Session &session) {
  57. std::cout << "=== TC1: Page loads with correct structure\n";
  58. session.navigate(base_url);
  59. auto title = session.title();
  60. ASSERT_CONTAINS(title, "Translate", "Page title contains 'Translate'");
  61. // Verify main DOM elements exist
  62. ASSERT_ELEMENT_EXISTS(session, "#model-select");
  63. ASSERT_ELEMENT_EXISTS(session, "#input-text");
  64. ASSERT_ELEMENT_EXISTS(session, "#output-text");
  65. ASSERT_ELEMENT_EXISTS(session, "#target-lang");
  66. }
  67. void test_model_dropdown(webdriver::Session &session) {
  68. std::cout << "=== TC2: Model dropdown is populated\n";
  69. navigate_and_wait_for_models(session);
  70. // Note: WebDriver findElements cannot find <option> elements directly
  71. // in geckodriver/Firefox, so we use JS to count them.
  72. auto option_count = session.execute_script(
  73. "return document.querySelectorAll('#model-select option').length");
  74. ASSERT_TRUE(option_count != "0" && option_count != "null",
  75. "Model dropdown has options (count=" + option_count + ")");
  76. // Check that at least one option has a selected attribute
  77. auto selected_val = session.execute_script(
  78. "return document.querySelector('#model-select').value");
  79. ASSERT_TRUE(selected_val != "null" && !selected_val.empty(),
  80. "A model is selected (value='" + selected_val + "')");
  81. }
  82. void test_translation_sse(webdriver::Session &session) {
  83. std::cout << "=== TC3: Translation with SSE streaming\n";
  84. navigate_and_wait_for_models(session);
  85. // Clear and type input — debounce auto-translate triggers after 300ms
  86. auto input = session.css("#input-text");
  87. input.clear();
  88. input.send_keys("Hello world");
  89. // Wait for output to appear (debounce 300ms + LLM inference)
  90. bool has_output = session.wait_until(
  91. "return document.querySelector('#output-text').textContent.length > 0",
  92. 120000);
  93. ASSERT_TRUE(has_output, "Translation output appeared");
  94. auto output_text = session.execute_script(
  95. "return document.querySelector('#output-text').textContent");
  96. ASSERT_TRUE(!output_text.empty() && output_text != "null",
  97. "Output text is non-empty ('" + output_text.substr(0, 50) +
  98. "...')");
  99. // Wait for busy state to be cleared after completion
  100. bool busy_cleared = session.wait_until(
  101. "return !document.body.classList.contains('busy')", 120000);
  102. ASSERT_TRUE(busy_cleared, "Busy state cleared after translation");
  103. }
  104. void test_busy_state(webdriver::Session &session) {
  105. std::cout << "=== TC4: Busy state during translation\n";
  106. navigate_and_wait_for_models(session);
  107. auto input = session.css("#input-text");
  108. input.clear();
  109. // Clear previous output
  110. session.execute_script(
  111. "document.querySelector('#output-text').textContent = ''");
  112. input.send_keys(
  113. "I had a great time visiting Tokyo last spring. "
  114. "The cherry blossoms were beautiful and the food was amazing.");
  115. // Check busy state (debounce 300ms then translation starts)
  116. bool went_busy = session.wait_until(
  117. "return document.body.classList.contains('busy')", 5000);
  118. ASSERT_TRUE(went_busy, "Body gets 'busy' class during translation");
  119. // Wait for completion
  120. session.wait_until("return !document.body.classList.contains('busy')",
  121. 120000);
  122. PASS("Busy class removed after completion");
  123. }
  124. void test_empty_input(webdriver::Session &session) {
  125. std::cout << "=== TC5: Empty input does nothing\n";
  126. navigate_and_wait_for_models(session);
  127. // Clear input and output
  128. auto input = session.css("#input-text");
  129. input.clear();
  130. session.execute_script(
  131. "document.querySelector('#output-text').textContent = ''");
  132. // Trigger input event on empty textarea
  133. session.execute_script("document.querySelector('#input-text').dispatchEvent("
  134. " new Event('input'));");
  135. // Wait longer than debounce (300ms) — nothing should happen
  136. std::this_thread::sleep_for(std::chrono::milliseconds(1000));
  137. auto output_text = session.execute_script(
  138. "return document.querySelector('#output-text').textContent");
  139. ASSERT_TRUE(output_text.empty() || output_text == "null" || output_text == "",
  140. "No output for empty input");
  141. }
  142. void test_target_lang_selector(webdriver::Session &session) {
  143. std::cout << "=== TC6: Target language selector\n";
  144. navigate_and_wait_for_models(session);
  145. // Check available language options (use JS — WebDriver can't find <option>)
  146. auto lang_count = session.execute_script(
  147. "return document.querySelectorAll('#target-lang option').length");
  148. ASSERT_TRUE(lang_count != "0" && lang_count != "null",
  149. "Language selector has multiple options (count=" + lang_count +
  150. ")");
  151. // Switch to English and translate
  152. session.execute_script("document.querySelector('#target-lang').value = 'en';"
  153. "document.querySelector('#target-lang').dispatchEvent("
  154. " new Event('change'));");
  155. // Clear output, then type — debounce auto-translate triggers
  156. session.execute_script(
  157. "document.querySelector('#output-text').textContent = ''");
  158. auto input = session.css("#input-text");
  159. input.clear();
  160. input.send_keys("こんにちは");
  161. bool has_output = session.wait_until(
  162. "return document.querySelector('#output-text').textContent.length > 0",
  163. 120000);
  164. ASSERT_TRUE(has_output, "Translation with target_lang=en produced output");
  165. }
  166. void test_model_switch(webdriver::Session &session) {
  167. std::cout << "=== TC7: Model switching\n";
  168. navigate_and_wait_for_models(session);
  169. auto options = session.css_all("#model-select option");
  170. if (options.size() < 2) {
  171. PASS("Model switch skipped (only 1 model available)");
  172. return;
  173. }
  174. // Get current model
  175. auto current = session.execute_script(
  176. "return document.querySelector('#model-select').value");
  177. // Switch to a different model (pick the second option's value)
  178. auto other_value = options[1].attribute("value");
  179. if (other_value == current && options.size() > 2) {
  180. other_value = options[2].attribute("value");
  181. }
  182. session.execute_script(
  183. "document.querySelector('#model-select').value = '" + other_value +
  184. "';"
  185. "document.querySelector('#model-select').dispatchEvent("
  186. " new Event('change'));");
  187. // Wait for model switch to complete (SSE: downloading → loading → ready)
  188. bool ready = session.wait_until(
  189. "return !document.body.classList.contains('busy')", 120000);
  190. ASSERT_TRUE(ready, "Model switch completed");
  191. auto new_value = session.execute_script(
  192. "return document.querySelector('#model-select').value");
  193. ASSERT_TRUE(new_value == other_value,
  194. "Model changed to '" + other_value + "'");
  195. }
  196. void test_download_dialog_structure(webdriver::Session &session) {
  197. std::cout << "=== TC8: Download dialog DOM structure\n";
  198. session.navigate(base_url);
  199. ASSERT_ELEMENT_EXISTS(session, "#download-dialog");
  200. ASSERT_ELEMENT_EXISTS(session, "#download-progress");
  201. ASSERT_ELEMENT_EXISTS(session, "#download-status");
  202. ASSERT_ELEMENT_EXISTS(session, "#download-cancel");
  203. }
  204. // ─── Main ────────────────────────────────────────────────────
  205. int main(int argc, char *argv[]) {
  206. if (argc < 2) {
  207. std::cerr << "Usage: test_webui <server-port>\n";
  208. return 1;
  209. }
  210. int port = std::atoi(argv[1]);
  211. base_url = "http://127.0.0.1:" + std::to_string(port);
  212. std::cout << "=== Ch5 Web UI Browser Tests\n";
  213. std::cout << "=== Server: " << base_url << "\n\n";
  214. try {
  215. webdriver::Session session;
  216. test_page_loads(session);
  217. test_model_dropdown(session);
  218. test_translation_sse(session);
  219. test_busy_state(session);
  220. test_empty_input(session);
  221. test_target_lang_selector(session);
  222. test_model_switch(session);
  223. test_download_dialog_structure(session);
  224. } catch (const webdriver::Error &e) {
  225. std::cerr << "WebDriver error: " << e.what() << "\n";
  226. ++fail_count;
  227. } catch (const std::exception &e) {
  228. std::cerr << "Error: " << e.what() << "\n";
  229. ++fail_count;
  230. }
  231. std::cout << "\n=== Results: " << pass_count << " passed, " << fail_count
  232. << " failed\n";
  233. return fail_count > 0 ? 1 : 0;
  234. }