Files
cpp-httplib/docs-util/llm-app/test_webui.cpp

301 lines
12 KiB
C++

// 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;
}