mirror of
https://github.com/yhirose/cpp-httplib.git
synced 2026-04-11 19:28:30 +00:00
301 lines
12 KiB
C++
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;
|
|
}
|