website with slop placeholders
This commit is contained in:
@@ -0,0 +1,10 @@
|
||||
EXPORT DTree* emit_marker(DTree* call_param)
|
||||
{
|
||||
print("UNIT_CALL_EXPORT_OK");
|
||||
return(0);
|
||||
}
|
||||
|
||||
RENDER(Request& context)
|
||||
{
|
||||
print("<section data-unit-render=\"ok\">UNIT_RENDER_FIXTURE</section>");
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
#include "testlib.h"
|
||||
|
||||
RENDER(Request& context)
|
||||
{
|
||||
u64 passed = 0;
|
||||
u64 failed = 0;
|
||||
u64 skipped = 0;
|
||||
|
||||
auto check = [&](String name, bool ok, String detail)
|
||||
{
|
||||
site_tests_case(name, ok ? "pass" : "fail", detail);
|
||||
if(ok)
|
||||
passed++;
|
||||
else
|
||||
failed++;
|
||||
};
|
||||
|
||||
DTree props;
|
||||
props["title"] = "Component Output";
|
||||
props["body"] = "This body comes from component()";
|
||||
props["footer"] = "Named footer render";
|
||||
|
||||
bool exists = component_exists("components/panel");
|
||||
String resolved = component_resolve("components/panel");
|
||||
String panel_markup = component("components/panel", props, context);
|
||||
|
||||
ob_start();
|
||||
component_render("components/panel:FOOTER", props, context);
|
||||
String footer_markup = ob_get_close();
|
||||
|
||||
ob_start();
|
||||
print("buffered-text");
|
||||
String buffer_markup = ob_get_close();
|
||||
|
||||
site_tests_page_start("Components", "Component helpers, named handlers, and output buffering coverage.");
|
||||
|
||||
check("component_exists()", exists, exists ? "components/panel resolved as existing" : "components/panel missing");
|
||||
check("component_resolve()", resolved != "", resolved);
|
||||
check("component()", panel_markup.find("Component Output") != String::npos && panel_markup.find("This body comes from component()") != String::npos, panel_markup);
|
||||
check("component_render() named handler", footer_markup.find("Named footer render") != String::npos, footer_markup);
|
||||
check("ob_start() / ob_get_close()", buffer_markup == "buffered-text", buffer_markup);
|
||||
|
||||
?><div class="tests-section">
|
||||
<h3>Rendered Component</h3>
|
||||
<?: panel_markup ?>
|
||||
</div><?
|
||||
|
||||
site_tests_summary(passed, failed, skipped, "The panel fixture under site/tests/components exercises default and named component entry points.");
|
||||
site_tests_page_end();
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
COMPONENT(Request& context)
|
||||
{
|
||||
<><pre class="tests-code-block"><?: first(context.call["default_html"].to_string(), context.call["children_html"].to_string()) ?></pre></>
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
COMPONENT(Request& context)
|
||||
{
|
||||
<><div class="tests-warning"><?= first(context.call["children_html"].to_string(), context.call["default_html"].to_string()) ?></div></>
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
COMPONENT(Request& context)
|
||||
{
|
||||
<><section class="tests-component">
|
||||
<? component_render(":HEADER", context.call, context); ?>
|
||||
<? component_render(":BODY", context.call, context); ?>
|
||||
<? component_render(":FOOTER", context.call, context); ?>
|
||||
</section></>
|
||||
}
|
||||
|
||||
COMPONENT:HEADER(Request& context)
|
||||
{
|
||||
<><h3 class="accent"><?= first(context.call["title"].to_string(), "Missing Title") ?></h3></>
|
||||
}
|
||||
|
||||
COMPONENT:BODY(Request& context)
|
||||
{
|
||||
<><p><?= first(context.call["body"].to_string(), "Missing Body") ?></p></>
|
||||
}
|
||||
|
||||
COMPONENT:FOOTER(Request& context)
|
||||
{
|
||||
<><small><?= first(context.call["footer"].to_string(), "Missing Footer") ?></small></>
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
#include "testlib.h"
|
||||
|
||||
RENDER(Request& context)
|
||||
{
|
||||
u64 passed = 0;
|
||||
u64 failed = 0;
|
||||
u64 skipped = 0;
|
||||
|
||||
auto check = [&](String name, bool ok, String detail)
|
||||
{
|
||||
site_tests_case(name, ok ? "pass" : "fail", detail);
|
||||
if(ok)
|
||||
passed++;
|
||||
else
|
||||
failed++;
|
||||
};
|
||||
|
||||
site_tests_page_start("Core APIs", "Pure helper coverage for strings, UTF-8 splitting, DTree, and JSON encoding/decoding.");
|
||||
|
||||
auto comma_parts = split("alpha,beta,gamma", ",");
|
||||
check("split()", comma_parts.size() == 3 && comma_parts[1] == "beta", join(comma_parts, " | "));
|
||||
|
||||
auto spaced_parts = split_space(" one two three ");
|
||||
check("split_space()", spaced_parts.size() == 3 && spaced_parts[2] == "three", join(spaced_parts, " / "));
|
||||
|
||||
check("trim()", trim(" padded value ") == "padded value", trim(" padded value "));
|
||||
check("replace()", replace("alpha-beta-beta", "beta", "done") == "alpha-done-done", replace("alpha-beta-beta", "beta", "done"));
|
||||
check("substr() + strpos()", strpos("component suite", "suite") == 10 && substr("component suite", 10) == "suite", "strpos=10 substr='" + substr("component suite", 10) + "'");
|
||||
check("str_starts_with()", str_starts_with("websocket-suite", "websocket"), "websocket-suite starts with websocket");
|
||||
check("str_ends_with()", str_ends_with("component.uce", ".uce"), "component.uce ends with .uce");
|
||||
check("to_lower() / to_upper()", to_lower("MiXeD") == "mixed" && to_upper("MiXeD") == "MIXED", to_lower("MiXeD") + " / " + to_upper("MiXeD"));
|
||||
|
||||
String utf8_sample = "A\xC3\xA9";
|
||||
auto utf8_parts = split_utf8(utf8_sample);
|
||||
check("split_utf8()", utf8_parts.size() == 2, "count=" + std::to_string(utf8_parts.size()));
|
||||
|
||||
DTree payload;
|
||||
payload["name"] = "uce";
|
||||
payload["count"] = (f64)3;
|
||||
payload["kind"] = "core";
|
||||
String payload_json = json_encode(payload);
|
||||
DTree decoded = json_decode(payload_json);
|
||||
check("json_encode() / json_decode()", decoded["name"].to_string() == "uce" && int_val(decoded["count"].to_string()) == 3, payload_json);
|
||||
|
||||
DTree tree;
|
||||
tree["suite"] = "core";
|
||||
tree["nested"]["api"] = "dtree";
|
||||
tree["nested"]["ok"].set_bool(true);
|
||||
check("DTree map access", tree["suite"].to_string() == "core" && tree["nested"]["api"].to_string() == "dtree", json_encode(tree));
|
||||
|
||||
site_tests_summary(passed, failed, skipped, "These assertions intentionally stay pure and side-effect free so they remain safe on the public site.");
|
||||
site_tests_page_end();
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
#include "testlib.h"
|
||||
|
||||
RENDER(Request& context)
|
||||
{
|
||||
u64 passed = 0;
|
||||
u64 failed = 0;
|
||||
u64 skipped = 0;
|
||||
|
||||
auto check = [&](String name, bool ok, String detail)
|
||||
{
|
||||
site_tests_case(name, ok ? "pass" : "fail", detail);
|
||||
if(ok)
|
||||
passed++;
|
||||
else
|
||||
failed++;
|
||||
};
|
||||
|
||||
String action = first(context.get["action"], "touch");
|
||||
String session_id = session_start("uce-site-tests");
|
||||
if(action == "destroy")
|
||||
session_destroy();
|
||||
else
|
||||
context.session["suite"] = "http";
|
||||
|
||||
set_cookie("site-tests-cookie", "cookie-value", time() + 300);
|
||||
context.header["X-Site-Tests"] = "http-suite";
|
||||
|
||||
String query_src = "alpha=1&beta=two%20words&gamma=ok";
|
||||
StringMap query = parse_query(query_src);
|
||||
String uri_input = "alpha beta/?x=1&y=2";
|
||||
String encoded = uri_encode(uri_input);
|
||||
String decoded = uri_decode(encoded);
|
||||
String generated_session = session_id_create();
|
||||
String query_dump = var_dump(query);
|
||||
String set_cookie_dump = var_dump(context.set_cookies);
|
||||
String header_dump = var_dump(context.header);
|
||||
String session_dump = var_dump(context.session);
|
||||
|
||||
site_tests_page_start("HTTP And Session", "Request helpers, cookies, headers, URI parsing, and session lifecycle checks.");
|
||||
|
||||
check("uri_encode() / uri_decode()", decoded == uri_input, encoded + " => " + decoded);
|
||||
check("parse_query()", query["alpha"] == "1" && query["beta"] == "two words", query_dump);
|
||||
check("set_cookie()", set_cookie_dump.find("site-tests-cookie") != String::npos && set_cookie_dump.find("cookie-value") != String::npos, set_cookie_dump);
|
||||
check("response header mutation", context.header["X-Site-Tests"] == "http-suite", header_dump);
|
||||
check("session_start()", session_id != "", "session_id=" + session_id);
|
||||
check("session_id_create()", generated_session.length() >= 16, generated_session);
|
||||
|
||||
if(action == "destroy")
|
||||
check("session_destroy()", session_dump.find("suite") == String::npos, session_dump);
|
||||
else
|
||||
check("context.session writes", context.session["suite"] == "http", session_dump);
|
||||
|
||||
?><div class="tests-section">
|
||||
<a class="button" href="?">Refresh Session Test</a>
|
||||
<a class="button" href="?action=destroy">Destroy Session</a>
|
||||
</div><?
|
||||
|
||||
site_tests_summary(passed, failed, skipped, "This page intentionally mutates only response headers and the test cookie/session namespace used by the suite.");
|
||||
site_tests_page_end();
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
#include "testlib.h"
|
||||
|
||||
RENDER(Request& context)
|
||||
{
|
||||
bool local_only = test_demo_request_allowed(context);
|
||||
site_tests_page_start(
|
||||
"Coverage Index",
|
||||
"Grouped runtime coverage pages for the published site. Public-safe pages stay available everywhere; stateful or infrastructure-touching pages stay local-only."
|
||||
);
|
||||
|
||||
if(local_only)
|
||||
{
|
||||
?><div class="tests-note">Local-network access detected. The full suite, including filesystem, service, and task coverage pages, is available.</div><?
|
||||
}
|
||||
else
|
||||
{
|
||||
?><div class="tests-note">Public access detected. Local-only pages remain linked here but will return a restricted message instead of mutating server state.</div><?
|
||||
}
|
||||
|
||||
?><div class="tests-grid"><?
|
||||
site_tests_card("core.uce", "Core APIs", "Strings, UTF-8 handling, DTree, JSON, and small pure-function helpers.", "public");
|
||||
site_tests_card("http.uce", "HTTP And Session", "Headers, cookies, query parsing, URI helpers, and session lifecycle checks.", "public");
|
||||
site_tests_card("components.uce", "Components", "component_exists(), component_resolve(), component(), component_render(), and output buffering.", "public");
|
||||
site_tests_card("markdown.uce", "Markdown", "markdown_to_ast(), markdown_to_html(), and component hook rendering.", "public");
|
||||
site_tests_card("units.uce", "Units", "units_list(), unit_info(), unit_call(), unit_render(), and fixture loading.", "public");
|
||||
site_tests_card("websockets.ws.uce", "WebSockets", "Browser-driven handshake and echo checks for ws_* helpers.", "public");
|
||||
site_tests_card("io.uce", "Filesystem", "path_join(), file_put_contents(), file_append(), and file_get_contents().", "local-only");
|
||||
site_tests_card("services.uce", "Sockets And Services", "socket_*, memcache_*, and MySQL reachability probes with skip-aware output.", "local-only");
|
||||
site_tests_card("tasks.uce", "Tasks", "task(), task_pid(), task_repeat(), and task_kill() health checks.", "local-only");
|
||||
?></div><?
|
||||
|
||||
?><div class="tests-section"><a class="button" href="../demo/index.uce">Back To Demo Pages</a></div><?
|
||||
|
||||
site_tests_page_end();
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
#include "testlib.h"
|
||||
|
||||
RENDER(Request& context)
|
||||
{
|
||||
if(!test_demo_request_allowed(context))
|
||||
{
|
||||
site_tests_restricted(context, "Filesystem", "write to local files and append server-side test data");
|
||||
return;
|
||||
}
|
||||
|
||||
u64 passed = 0;
|
||||
u64 failed = 0;
|
||||
u64 skipped = 0;
|
||||
|
||||
auto check = [&](String name, bool ok, String detail)
|
||||
{
|
||||
site_tests_case(name, ok ? "pass" : "fail", detail);
|
||||
if(ok)
|
||||
passed++;
|
||||
else
|
||||
failed++;
|
||||
};
|
||||
|
||||
String file_name = path_join("/tmp", "uce-site-tests-io.txt");
|
||||
bool write_ok = file_put_contents(file_name, "alpha");
|
||||
file_append(file_name, "|beta");
|
||||
String file_text = file_get_contents(file_name);
|
||||
|
||||
site_tests_page_start("Filesystem", "Local-only file helper coverage using a temporary file under /tmp.");
|
||||
|
||||
check("path_join()", file_name == "/tmp/uce-site-tests-io.txt", file_name);
|
||||
check("file_put_contents()", write_ok, file_name);
|
||||
check("file_append()", file_text.find("|beta") != String::npos, file_text);
|
||||
check("file_get_contents()", file_text == "alpha|beta", file_text);
|
||||
|
||||
site_tests_summary(passed, failed, skipped, "This page intentionally limits writes to /tmp/uce-site-tests-io.txt so it stays disposable.");
|
||||
site_tests_page_end();
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
#include "testlib.h"
|
||||
|
||||
RENDER(Request& context)
|
||||
{
|
||||
u64 passed = 0;
|
||||
u64 failed = 0;
|
||||
u64 skipped = 0;
|
||||
|
||||
auto check = [&](String name, bool ok, String detail)
|
||||
{
|
||||
site_tests_case(name, ok ? "pass" : "fail", detail);
|
||||
if(ok)
|
||||
passed++;
|
||||
else
|
||||
failed++;
|
||||
};
|
||||
|
||||
String markdown_src = "# Site Test Heading\n\nParagraph with **bold** content.\n\n:::warning\nWatch the component hook\n:::\n\n```txt\ncode block\n```\n";
|
||||
DTree options;
|
||||
options["components"][":::warning"] = "components/markdown/warning";
|
||||
options["components"]["node.code_block"] = "components/markdown/code_block";
|
||||
|
||||
DTree ast = markdown_to_ast(markdown_src, options);
|
||||
String ast_json = json_encode(ast);
|
||||
String html = markdown_to_html(markdown_src, options);
|
||||
String ast_excerpt = ast_json.substr(0, ast_json.length() > 220 ? 220 : ast_json.length());
|
||||
|
||||
site_tests_page_start("Markdown", "Markdown parser coverage with component-backed directive and node hooks.");
|
||||
|
||||
check("markdown_to_ast()", ast_json.find("heading") != String::npos || ast_json.find("code_block") != String::npos, ast_excerpt);
|
||||
check("markdown_to_html()", html.find("<h1") != String::npos && html.find("tests-warning") != String::npos, html);
|
||||
check("component hook output", html.find("tests-code-block") != String::npos, html);
|
||||
|
||||
?><div class="tests-section">
|
||||
<h3>Rendered HTML</h3>
|
||||
<?: html ?>
|
||||
</div><?
|
||||
|
||||
site_tests_summary(passed, failed, skipped, "The warning directive and code-block renderer both run through local component fixtures under site/tests/components/markdown.");
|
||||
site_tests_page_end();
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
#include "testlib.h"
|
||||
|
||||
RENDER(Request& context)
|
||||
{
|
||||
if(!test_demo_request_allowed(context))
|
||||
{
|
||||
site_tests_restricted(context, "Sockets And Services", "connect to local listeners and optional infrastructure services");
|
||||
return;
|
||||
}
|
||||
|
||||
u64 passed = 0;
|
||||
u64 failed = 0;
|
||||
u64 skipped = 0;
|
||||
|
||||
site_tests_page_start("Sockets And Services", "Local-only probes for sockets and optional infrastructure integrations.");
|
||||
|
||||
auto mark = [&](String name, String status, String detail)
|
||||
{
|
||||
site_tests_case(name, status, detail);
|
||||
if(status == "pass")
|
||||
passed++;
|
||||
else if(status == "skip")
|
||||
skipped++;
|
||||
else
|
||||
failed++;
|
||||
};
|
||||
|
||||
u64 sockfd = socket_connect("localhost", 80);
|
||||
if(sockfd != 0)
|
||||
{
|
||||
bool write_ok = socket_write(sockfd, "GET /tests/index.uce HTTP/1.0\r\nHost: uce.openfu.com\r\n\r\n");
|
||||
String response = socket_read(sockfd, 4096, 2);
|
||||
socket_close(sockfd);
|
||||
mark(
|
||||
"socket_connect() / socket_write() / socket_read()",
|
||||
(write_ok && response.find("200 OK") != String::npos) ? "pass" : "fail",
|
||||
response.substr(0, response.length() > 220 ? 220 : response.length())
|
||||
);
|
||||
}
|
||||
else
|
||||
{
|
||||
mark("socket_connect() / socket_write() / socket_read()", "fail", "socket_connect(localhost, 80) returned 0");
|
||||
}
|
||||
|
||||
u64 memfd = memcache_connect();
|
||||
if(memfd == 0)
|
||||
{
|
||||
mark("memcache_*", "skip", "memcache_connect() returned 0");
|
||||
}
|
||||
else
|
||||
{
|
||||
memcache_set(memfd, "site-tests-key", "value-1");
|
||||
String mem_value = memcache_get(memfd, "site-tests-key");
|
||||
memcache_delete(memfd, "site-tests-key");
|
||||
mark(
|
||||
"memcache_connect() / memcache_set() / memcache_get() / memcache_delete()",
|
||||
mem_value == "value-1" ? "pass" : "fail",
|
||||
"memcache value=" + mem_value
|
||||
);
|
||||
}
|
||||
|
||||
MySQL mysql;
|
||||
mysql.connect("localhost", "root", "");
|
||||
String mysql_error_text = trim(mysql.error());
|
||||
if(mysql_error_text != "")
|
||||
{
|
||||
mark("mysql connect/query", "skip", mysql_error_text);
|
||||
}
|
||||
else
|
||||
{
|
||||
String dbs = var_dump(mysql.query("SHOW DATABASES"));
|
||||
mark(
|
||||
"mysql connect/query",
|
||||
(dbs.find("information_schema") != String::npos || dbs.find("mysql") != String::npos) ? "pass" : "fail",
|
||||
dbs.substr(0, dbs.length() > 220 ? 220 : dbs.length())
|
||||
);
|
||||
}
|
||||
|
||||
site_tests_summary(passed, failed, skipped, "Memcache or MySQL checks are allowed to skip cleanly when the corresponding service is not available on the development host.");
|
||||
site_tests_page_end();
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
#include "testlib.h"
|
||||
|
||||
RENDER(Request& context)
|
||||
{
|
||||
if(!test_demo_request_allowed(context))
|
||||
{
|
||||
site_tests_restricted(context, "Tasks", "spawn and inspect background worker processes");
|
||||
return;
|
||||
}
|
||||
|
||||
u64 passed = 0;
|
||||
u64 failed = 0;
|
||||
u64 skipped = 0;
|
||||
|
||||
auto check = [&](String name, bool ok, String detail)
|
||||
{
|
||||
site_tests_case(name, ok ? "pass" : "fail", detail);
|
||||
if(ok)
|
||||
passed++;
|
||||
else
|
||||
failed++;
|
||||
};
|
||||
|
||||
String mode = first(context.get["mode"], "run");
|
||||
pid_t repeat_existing = task_pid("site-tests-repeat");
|
||||
if(mode == "stop" && repeat_existing != 0)
|
||||
task_kill(repeat_existing, 15);
|
||||
|
||||
pid_t short_pid = 0;
|
||||
pid_t repeat_pid = 0;
|
||||
if(mode != "stop")
|
||||
{
|
||||
short_pid = task("site-tests-short", []() {
|
||||
sleep(2);
|
||||
});
|
||||
|
||||
repeat_pid = task_repeat("site-tests-repeat", 1.0, []() {
|
||||
file_put_contents("/tmp/uce-site-tests-repeat.txt", time_format_utc("%Y-%m-%d %H:%M:%S"));
|
||||
}, 4);
|
||||
}
|
||||
|
||||
pid_t seen_short_pid = task_pid("site-tests-short");
|
||||
pid_t seen_repeat_pid = task_pid("site-tests-repeat");
|
||||
int short_alive = seen_short_pid == 0 ? -1 : task_kill(seen_short_pid, 0);
|
||||
int repeat_alive = seen_repeat_pid == 0 ? -1 : task_kill(seen_repeat_pid, 0);
|
||||
|
||||
site_tests_page_start("Tasks", "Local-only checks for one-shot and repeating background task helpers.");
|
||||
|
||||
if(mode == "stop")
|
||||
{
|
||||
site_tests_case("repeat worker stop request", "skip", "stop mode requested; no new tasks were started on this request");
|
||||
skipped++;
|
||||
}
|
||||
else
|
||||
{
|
||||
check("task()", short_pid != 0 && seen_short_pid != 0, "started=" + std::to_string(short_pid) + " seen=" + std::to_string(seen_short_pid));
|
||||
check("task_pid() + task_kill(pid, 0)", short_alive == 0, "kill(0) result=" + std::to_string(short_alive));
|
||||
check("task_repeat()", repeat_pid != 0 && seen_repeat_pid != 0, "started=" + std::to_string(repeat_pid) + " seen=" + std::to_string(seen_repeat_pid));
|
||||
check("repeat worker liveness", repeat_alive == 0, "kill(0) result=" + std::to_string(repeat_alive));
|
||||
}
|
||||
|
||||
?><div class="tests-section">
|
||||
<a class="button" href="?">Run Task Checks</a>
|
||||
<a class="button" href="?mode=stop">Stop Repeat Worker</a>
|
||||
</div><?
|
||||
|
||||
site_tests_summary(passed, failed, skipped, "The repeat worker writes its heartbeat to /tmp/uce-site-tests-repeat.txt and is bounded by a short timeout.");
|
||||
site_tests_page_end();
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
#include "../demo/demo_guard.h"
|
||||
|
||||
String site_tests_status_class(String status)
|
||||
{
|
||||
if(status == "pass")
|
||||
return("status-ok");
|
||||
if(status == "fail")
|
||||
return("status-error");
|
||||
return("status-warn");
|
||||
}
|
||||
|
||||
String site_tests_status_label(String status)
|
||||
{
|
||||
if(status == "pass")
|
||||
return("PASS");
|
||||
if(status == "fail")
|
||||
return("FAIL");
|
||||
return("SKIP");
|
||||
}
|
||||
|
||||
void site_tests_page_start(String title, String description = "")
|
||||
{
|
||||
print("<html><head>");
|
||||
print("<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\"></meta>");
|
||||
print("<link rel=\"stylesheet\" href='../demo/style.css?v=", time(), "'></link>");
|
||||
print("<style>");
|
||||
print(".tests-grid { display:grid; gap:1rem; grid-template-columns:repeat(auto-fit, minmax(18rem, 1fr)); margin:1.5rem 0; }");
|
||||
print(".tests-card { display:block; padding:1rem; border:1px solid #ccc; border-radius:0.75rem; background:#fff; color:inherit; text-decoration:none; }");
|
||||
print(".tests-card strong { display:block; margin-bottom:0.5rem; }");
|
||||
print(".tests-card span { display:block; color:#444; }");
|
||||
print(".tests-tags { margin-top:0.75rem; font-size:0.9rem; color:#666; }");
|
||||
print(".tests-summary { display:flex; flex-wrap:wrap; gap:0.75rem; margin:1rem 0 1.5rem; }");
|
||||
print(".tests-summary .status-badge { font-size:0.95rem; }");
|
||||
print(".tests-cases { display:grid; gap:0.75rem; margin:1.5rem 0; }");
|
||||
print(".tests-case { border:1px solid #d8d8d8; border-radius:0.75rem; background:#fff; padding:0.9rem 1rem; }");
|
||||
print(".tests-case-header { display:flex; gap:0.75rem; align-items:center; justify-content:space-between; }");
|
||||
print(".tests-case pre { margin:0.75rem 0 0; white-space:pre-wrap; }");
|
||||
print(".tests-section { margin:1.5rem 0; }");
|
||||
print(".tests-note { padding:1rem; border-left:4px solid #888; background:#f7f7f7; }");
|
||||
print(".tests-inline-code { font-family:monospace; }");
|
||||
print(".tests-component { border:1px solid #ccc; border-radius:0.75rem; padding:1rem; background:#fafafa; }");
|
||||
print(".tests-component .accent { color:#0a6; font-weight:bold; }");
|
||||
print(".tests-warning { border-left:4px solid #d97706; padding:0.75rem 1rem; background:#fff7ed; }");
|
||||
print(".tests-code-block { padding:0.75rem 1rem; background:#111827; color:#f9fafb; overflow:auto; }");
|
||||
print("</style></head><body>");
|
||||
print("<h1><a href=\"index.uce\">UCE Site Tests</a><a class=\"docs-link\" href=\"../doc/index.uce\">API Docs →</a></h1>");
|
||||
print("<h2>", html_escape(title), "</h2>");
|
||||
if(description != "")
|
||||
print("<p>", html_escape(description), "</p>");
|
||||
}
|
||||
|
||||
void site_tests_page_end()
|
||||
{
|
||||
print("</body></html>");
|
||||
}
|
||||
|
||||
void site_tests_card(String href, String title, String description, String tags = "")
|
||||
{
|
||||
print("<a class=\"tests-card\" href=\"", html_escape(href), "\">");
|
||||
print("<strong>", html_escape(title), "</strong>");
|
||||
print("<span>", html_escape(description), "</span>");
|
||||
if(tags != "")
|
||||
print("<div class=\"tests-tags\">", html_escape(tags), "</div>");
|
||||
print("</a>");
|
||||
}
|
||||
|
||||
void site_tests_summary(u64 passed, u64 failed, u64 skipped, String note = "")
|
||||
{
|
||||
print("<div class=\"tests-summary\">");
|
||||
print("<span class=\"status-badge status-ok\">passed ", std::to_string(passed), "</span>");
|
||||
print("<span class=\"status-badge status-error\">failed ", std::to_string(failed), "</span>");
|
||||
print("<span class=\"status-badge status-warn\">skipped ", std::to_string(skipped), "</span>");
|
||||
print("</div>");
|
||||
if(note != "")
|
||||
print("<div class=\"tests-note\">", html_escape(note), "</div>");
|
||||
}
|
||||
|
||||
void site_tests_case(String name, String status, String detail)
|
||||
{
|
||||
String css = site_tests_status_class(status);
|
||||
String label = site_tests_status_label(status);
|
||||
print("<section class=\"tests-case\"><div class=\"tests-case-header\">");
|
||||
print("<strong>", html_escape(name), "</strong>");
|
||||
print("<span class=\"status-badge ", html_escape(css), "\">", html_escape(label), "</span>");
|
||||
print("</div><pre>", html_escape(detail), "</pre></section>");
|
||||
}
|
||||
|
||||
void site_tests_restricted(Request& context, String title, String risk)
|
||||
{
|
||||
test_demo_render_restricted_html(context, title, risk, "../demo/style.css", "index.uce");
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
#include "testlib.h"
|
||||
|
||||
RENDER(Request& context)
|
||||
{
|
||||
u64 passed = 0;
|
||||
u64 failed = 0;
|
||||
u64 skipped = 0;
|
||||
|
||||
auto check = [&](String name, bool ok, String detail)
|
||||
{
|
||||
site_tests_case(name, ok ? "pass" : "fail", detail);
|
||||
if(ok)
|
||||
passed++;
|
||||
else
|
||||
failed++;
|
||||
};
|
||||
|
||||
auto unit_paths = units_list();
|
||||
DTree info = unit_info("call_helpers.uce");
|
||||
|
||||
ob_start();
|
||||
unit_call("call_helpers.uce", "emit_marker");
|
||||
String call_output = ob_get_close();
|
||||
|
||||
ob_start();
|
||||
unit_render("call_helpers.uce", context);
|
||||
String render_output = ob_get_close();
|
||||
|
||||
site_tests_page_start("Units", "Unit discovery, metadata, exported-function invocation, and render-path coverage.");
|
||||
|
||||
check("units_list()", unit_paths.size() > 0, "count=" + std::to_string(unit_paths.size()));
|
||||
check("unit_info()", info["path"].to_string() != "", json_encode(info));
|
||||
check("unit_call()", call_output.find("UNIT_CALL_EXPORT_OK") != String::npos, call_output);
|
||||
check("unit_render()", render_output.find("data-unit-render=\"ok\"") != String::npos, render_output);
|
||||
|
||||
site_tests_summary(passed, failed, skipped, "The local fixture call_helpers.uce keeps these checks deterministic and self-contained within site/tests.");
|
||||
site_tests_page_end();
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
#include "testlib.h"
|
||||
|
||||
DTree websocket_suite_event(Request& context, String type, String nonce)
|
||||
{
|
||||
DTree event;
|
||||
event["type"] = type;
|
||||
event["nonce"] = nonce;
|
||||
event["connection_id"] = ws_connection_id();
|
||||
event["scope"] = first(context.params["DOCUMENT_URI"], ws_scope());
|
||||
event["online"] = (f64)ws_connection_count();
|
||||
event["opcode"] = (f64)ws_opcode();
|
||||
event["binary"] = ws_is_binary() ? "true" : "false";
|
||||
return(event);
|
||||
}
|
||||
|
||||
RENDER(Request& context)
|
||||
{
|
||||
String ws_url = context.params["DOCUMENT_URI"];
|
||||
site_tests_page_start("WebSockets", "Browser-driven handshake and broadcast checks for ws_* helpers on the current .ws.uce endpoint.");
|
||||
?><div class="tests-note">This page self-tests in the browser: it opens a socket to itself, waits for a targeted hello acknowledgment, then verifies a broadcast acknowledgment from the same endpoint.</div>
|
||||
<section class="tests-section">
|
||||
<div class="tests-cases">
|
||||
<section class="tests-case"><div class="tests-case-header"><strong>hello acknowledgment</strong><span id="hello-status" class="status-badge status-warn">WAIT</span></div><pre id="hello-detail">connecting...</pre></section>
|
||||
<section class="tests-case"><div class="tests-case-header"><strong>broadcast acknowledgment</strong><span id="broadcast-status" class="status-badge status-warn">WAIT</span></div><pre id="broadcast-detail">waiting...</pre></section>
|
||||
<section class="tests-case"><div class="tests-case-header"><strong>connection state</strong><span id="socket-status" class="status-badge status-warn">WAIT</span></div><pre id="socket-detail">opening websocket...</pre></section>
|
||||
</div>
|
||||
</section>
|
||||
<script>
|
||||
const wsUrl = `${window.location.protocol === 'https:' ? 'wss' : 'ws'}://${window.location.host}<?= ws_url ?>`;
|
||||
const nonce = `site-tests-${Date.now()}`;
|
||||
const helloStatus = document.getElementById('hello-status');
|
||||
const helloDetail = document.getElementById('hello-detail');
|
||||
const broadcastStatus = document.getElementById('broadcast-status');
|
||||
const broadcastDetail = document.getElementById('broadcast-detail');
|
||||
const socketStatus = document.getElementById('socket-status');
|
||||
const socketDetail = document.getElementById('socket-detail');
|
||||
let sawHello = false;
|
||||
let sawBroadcast = false;
|
||||
|
||||
function mark(el, detailEl, ok, text) {
|
||||
el.textContent = ok ? 'PASS' : 'FAIL';
|
||||
el.className = `status-badge ${ok ? 'status-ok' : 'status-error'}`;
|
||||
detailEl.textContent = text;
|
||||
}
|
||||
|
||||
const ws = new WebSocket(wsUrl);
|
||||
socketDetail.textContent = `connecting to ${wsUrl}`;
|
||||
|
||||
ws.addEventListener('open', () => {
|
||||
socketStatus.textContent = 'OPEN';
|
||||
socketStatus.className = 'status-badge status-ok';
|
||||
socketDetail.textContent = `connected to ${wsUrl}`;
|
||||
ws.send(JSON.stringify({ type: 'hello', nonce }));
|
||||
});
|
||||
|
||||
ws.addEventListener('message', (event) => {
|
||||
let payload;
|
||||
try {
|
||||
payload = JSON.parse(event.data);
|
||||
} catch (error) {
|
||||
mark(socketStatus, socketDetail, false, String(error));
|
||||
return;
|
||||
}
|
||||
|
||||
if (payload.nonce !== nonce) return;
|
||||
|
||||
if (payload.type === 'hello-ack') {
|
||||
sawHello = true;
|
||||
mark(helloStatus, helloDetail, true, JSON.stringify(payload, null, 2));
|
||||
ws.send(JSON.stringify({ type: 'broadcast', nonce }));
|
||||
return;
|
||||
}
|
||||
|
||||
if (payload.type === 'broadcast-ack') {
|
||||
sawBroadcast = true;
|
||||
mark(broadcastStatus, broadcastDetail, true, JSON.stringify(payload, null, 2));
|
||||
return;
|
||||
}
|
||||
|
||||
mark(socketStatus, socketDetail, false, `unexpected payload: ${JSON.stringify(payload)}`);
|
||||
});
|
||||
|
||||
ws.addEventListener('error', () => {
|
||||
mark(socketStatus, socketDetail, false, 'websocket error event fired');
|
||||
});
|
||||
|
||||
ws.addEventListener('close', () => {
|
||||
if (!sawHello) mark(helloStatus, helloDetail, false, 'socket closed before hello acknowledgment');
|
||||
if (!sawBroadcast) mark(broadcastStatus, broadcastDetail, false, 'socket closed before broadcast acknowledgment');
|
||||
if (!sawHello || !sawBroadcast) mark(socketStatus, socketDetail, false, 'socket closed before suite completed');
|
||||
});
|
||||
|
||||
window.setTimeout(() => {
|
||||
if (!sawHello) mark(helloStatus, helloDetail, false, 'timeout waiting for hello acknowledgment');
|
||||
if (!sawBroadcast) mark(broadcastStatus, broadcastDetail, false, 'timeout waiting for broadcast acknowledgment');
|
||||
}, 5000);
|
||||
</script><?
|
||||
site_tests_page_end();
|
||||
}
|
||||
|
||||
WS(Request& context)
|
||||
{
|
||||
if(ws_is_binary())
|
||||
{
|
||||
ws_send_to(ws_connection_id(), json_encode(websocket_suite_event(context, "binary-not-supported", "")));
|
||||
return;
|
||||
}
|
||||
|
||||
DTree payload = json_decode(ws_message());
|
||||
String type = trim(payload["type"].to_string());
|
||||
String nonce = trim(payload["nonce"].to_string());
|
||||
|
||||
if(type == "hello")
|
||||
{
|
||||
ws_send_to(ws_connection_id(), json_encode(websocket_suite_event(context, "hello-ack", nonce)));
|
||||
return;
|
||||
}
|
||||
|
||||
if(type == "broadcast")
|
||||
{
|
||||
ws_send(json_encode(websocket_suite_event(context, "broadcast-ack", nonce)));
|
||||
return;
|
||||
}
|
||||
|
||||
ws_send_to(ws_connection_id(), json_encode(websocket_suite_event(context, "unknown", nonce)));
|
||||
}
|
||||
Reference in New Issue
Block a user