| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300 |
- // test_webui.cpp — Browser-based E2E tests for Ch5 Web UI.
- // Uses webdriver.h (cpp-httplib + json.hpp) to control headless Firefox.
- //
- // Usage: test_webui <port>
- // port: the translate-server port (e.g. 18080)
- #include "webdriver.h"
- #include <cstdlib>
- #include <iostream>
- #include <string>
- // ─── Test framework (minimal) ────────────────────────────────
- static int pass_count = 0;
- static int fail_count = 0;
- #define PASS(label) \
- do { \
- std::cout << " PASS: " << (label) << "\n"; \
- ++pass_count; \
- } while (0)
- #define FAIL(label, detail) \
- do { \
- std::cout << " FAIL: " << (label) << "\n"; \
- std::cout << " " << (detail) << "\n"; \
- ++fail_count; \
- } while (0)
- #define ASSERT_TRUE(cond, label) \
- do { \
- if (cond) { \
- PASS(label); \
- } else { \
- FAIL(label, "condition was false"); \
- } \
- } while (0)
- #define ASSERT_CONTAINS(haystack, needle, label) \
- do { \
- if (std::string(haystack).find(needle) != std::string::npos) { \
- PASS(label); \
- } else { \
- FAIL(label, "'" + std::string(haystack) + "' does not contain '" + \
- std::string(needle) + "'"); \
- } \
- } while (0)
- #define ASSERT_ELEMENT_EXISTS(session, selector) \
- do { \
- try { \
- (session).css(selector); \
- PASS("Element " selector " exists"); \
- } catch (...) { FAIL("Element " selector " exists", "not found"); } \
- } while (0)
- // ─── Helpers ─────────────────────────────────────────────────
- static std::string base_url;
- void navigate_and_wait_for_models(webdriver::Session &session) {
- session.navigate(base_url);
- session.wait_until(
- "return document.querySelectorAll('#model-select option').length > 0",
- 5000);
- }
- void test_page_loads(webdriver::Session &session) {
- std::cout << "=== TC1: Page loads with correct structure\n";
- session.navigate(base_url);
- auto title = session.title();
- ASSERT_CONTAINS(title, "Translate", "Page title contains 'Translate'");
- // Verify main DOM elements exist
- ASSERT_ELEMENT_EXISTS(session, "#model-select");
- ASSERT_ELEMENT_EXISTS(session, "#input-text");
- ASSERT_ELEMENT_EXISTS(session, "#output-text");
- ASSERT_ELEMENT_EXISTS(session, "#target-lang");
- }
- void test_model_dropdown(webdriver::Session &session) {
- std::cout << "=== TC2: Model dropdown is populated\n";
- navigate_and_wait_for_models(session);
- // Note: WebDriver findElements cannot find <option> elements directly
- // in geckodriver/Firefox, so we use JS to count them.
- auto option_count = session.execute_script(
- "return document.querySelectorAll('#model-select option').length");
- ASSERT_TRUE(option_count != "0" && option_count != "null",
- "Model dropdown has options (count=" + option_count + ")");
- // Check that at least one option has a selected attribute
- auto selected_val = session.execute_script(
- "return document.querySelector('#model-select').value");
- ASSERT_TRUE(selected_val != "null" && !selected_val.empty(),
- "A model is selected (value='" + selected_val + "')");
- }
- void test_translation_sse(webdriver::Session &session) {
- std::cout << "=== TC3: Translation with SSE streaming\n";
- navigate_and_wait_for_models(session);
- // Clear and type input — debounce auto-translate triggers after 300ms
- auto input = session.css("#input-text");
- input.clear();
- input.send_keys("Hello world");
- // Wait for output to appear (debounce 300ms + LLM inference)
- bool has_output = session.wait_until(
- "return document.querySelector('#output-text').textContent.length > 0",
- 120000);
- ASSERT_TRUE(has_output, "Translation output appeared");
- auto output_text = session.execute_script(
- "return document.querySelector('#output-text').textContent");
- ASSERT_TRUE(!output_text.empty() && output_text != "null",
- "Output text is non-empty ('" + output_text.substr(0, 50) +
- "...')");
- // Wait for busy state to be cleared after completion
- bool busy_cleared = session.wait_until(
- "return !document.body.classList.contains('busy')", 120000);
- ASSERT_TRUE(busy_cleared, "Busy state cleared after translation");
- }
- void test_busy_state(webdriver::Session &session) {
- std::cout << "=== TC4: Busy state during translation\n";
- navigate_and_wait_for_models(session);
- auto input = session.css("#input-text");
- input.clear();
- // Clear previous output
- session.execute_script(
- "document.querySelector('#output-text').textContent = ''");
- input.send_keys(
- "I had a great time visiting Tokyo last spring. "
- "The cherry blossoms were beautiful and the food was amazing.");
- // Check busy state (debounce 300ms then translation starts)
- bool went_busy = session.wait_until(
- "return document.body.classList.contains('busy')", 5000);
- ASSERT_TRUE(went_busy, "Body gets 'busy' class during translation");
- // Wait for completion
- session.wait_until("return !document.body.classList.contains('busy')",
- 120000);
- PASS("Busy class removed after completion");
- }
- void test_empty_input(webdriver::Session &session) {
- std::cout << "=== TC5: Empty input does nothing\n";
- navigate_and_wait_for_models(session);
- // Clear input and output
- auto input = session.css("#input-text");
- input.clear();
- session.execute_script(
- "document.querySelector('#output-text').textContent = ''");
- // Trigger input event on empty textarea
- session.execute_script("document.querySelector('#input-text').dispatchEvent("
- " new Event('input'));");
- // Wait longer than debounce (300ms) — nothing should happen
- std::this_thread::sleep_for(std::chrono::milliseconds(1000));
- auto output_text = session.execute_script(
- "return document.querySelector('#output-text').textContent");
- ASSERT_TRUE(output_text.empty() || output_text == "null" || output_text == "",
- "No output for empty input");
- }
- void test_target_lang_selector(webdriver::Session &session) {
- std::cout << "=== TC6: Target language selector\n";
- navigate_and_wait_for_models(session);
- // Check available language options (use JS — WebDriver can't find <option>)
- auto lang_count = session.execute_script(
- "return document.querySelectorAll('#target-lang option').length");
- ASSERT_TRUE(lang_count != "0" && lang_count != "null",
- "Language selector has multiple options (count=" + lang_count +
- ")");
- // Switch to English and translate
- session.execute_script("document.querySelector('#target-lang').value = 'en';"
- "document.querySelector('#target-lang').dispatchEvent("
- " new Event('change'));");
- // Clear output, then type — debounce auto-translate triggers
- session.execute_script(
- "document.querySelector('#output-text').textContent = ''");
- auto input = session.css("#input-text");
- input.clear();
- input.send_keys("こんにちは");
- bool has_output = session.wait_until(
- "return document.querySelector('#output-text').textContent.length > 0",
- 120000);
- ASSERT_TRUE(has_output, "Translation with target_lang=en produced output");
- }
- void test_model_switch(webdriver::Session &session) {
- std::cout << "=== TC7: Model switching\n";
- navigate_and_wait_for_models(session);
- auto options = session.css_all("#model-select option");
- if (options.size() < 2) {
- PASS("Model switch skipped (only 1 model available)");
- return;
- }
- // Get current model
- auto current = session.execute_script(
- "return document.querySelector('#model-select').value");
- // Switch to a different model (pick the second option's value)
- auto other_value = options[1].attribute("value");
- if (other_value == current && options.size() > 2) {
- other_value = options[2].attribute("value");
- }
- session.execute_script(
- "document.querySelector('#model-select').value = '" + other_value +
- "';"
- "document.querySelector('#model-select').dispatchEvent("
- " new Event('change'));");
- // Wait for model switch to complete (SSE: downloading → loading → ready)
- bool ready = session.wait_until(
- "return !document.body.classList.contains('busy')", 120000);
- ASSERT_TRUE(ready, "Model switch completed");
- auto new_value = session.execute_script(
- "return document.querySelector('#model-select').value");
- ASSERT_TRUE(new_value == other_value,
- "Model changed to '" + other_value + "'");
- }
- void test_download_dialog_structure(webdriver::Session &session) {
- std::cout << "=== TC8: Download dialog DOM structure\n";
- session.navigate(base_url);
- ASSERT_ELEMENT_EXISTS(session, "#download-dialog");
- ASSERT_ELEMENT_EXISTS(session, "#download-progress");
- ASSERT_ELEMENT_EXISTS(session, "#download-status");
- ASSERT_ELEMENT_EXISTS(session, "#download-cancel");
- }
- // ─── Main ────────────────────────────────────────────────────
- int main(int argc, char *argv[]) {
- if (argc < 2) {
- std::cerr << "Usage: test_webui <server-port>\n";
- return 1;
- }
- int port = std::atoi(argv[1]);
- base_url = "http://127.0.0.1:" + std::to_string(port);
- std::cout << "=== Ch5 Web UI Browser Tests\n";
- std::cout << "=== Server: " << base_url << "\n\n";
- try {
- webdriver::Session session;
- test_page_loads(session);
- test_model_dropdown(session);
- test_translation_sse(session);
- test_busy_state(session);
- test_empty_input(session);
- test_target_lang_selector(session);
- test_model_switch(session);
- test_download_dialog_structure(session);
- } catch (const webdriver::Error &e) {
- std::cerr << "WebDriver error: " << e.what() << "\n";
- ++fail_count;
- } catch (const std::exception &e) {
- std::cerr << "Error: " << e.what() << "\n";
- ++fail_count;
- }
- std::cout << "\n=== Results: " << pass_count << " passed, " << fail_count
- << " failed\n";
- return fail_count > 0 ? 1 : 0;
- }
|