Files
pyxis/tests/native/test_nomadnet_image_compact.cpp
Torlando 929fa2c7ab feat(nomadnet): inline page images — parser, WebP decode, /media transport, LVGL render
Cumulative candidate on top of origin/main (7ae47d5).
Slices 1-5: compact-model image records, vendored decode-only libwebp,
wire contract + loader state machine, owner-loop /media dispatch with
retained page-Link reuse, screen render with bounded PSRAM LRU pixel store.

Local checkpoint only — not pushed.
2026-09-16 18:11:18 +00:00

158 lines
6.8 KiB
C++

// Host test: image records survive CompactPage::assign() (arena round-trip).
// Verifies the compact model faithfully carries ImageRecord data (alt text,
// url, width/height kind+value, alignment) from the parsed Document into the
// arena-backed CompactPage, and that the image_index on each IMAGE block
// resolves to the correct record.
#include <iostream>
#include <string>
#include "NomadNetCompactPage.h"
#include "NomadNetDocument.h"
using UI::LXMF::NomadNet::Alignment;
using UI::LXMF::NomadNet::BlockType;
using UI::LXMF::NomadNet::CompactPage;
using UI::LXMF::NomadNet::DocumentParser;
using UI::LXMF::NomadNet::ImageDimension;
int main() {
int failures = 0;
auto check = [&](const char* name, bool condition) {
if (!condition) { std::cerr << "FAIL: " << name << '\n'; ++failures; }
};
DocumentParser parser;
// 1. Canonical tag survives assign(): fields round-trip byte-for-byte.
{
const auto document = parser.parse(
"`(The RNS logo`w=n`a=c`:/media/demo.webp)");
CompactPage page;
check("canonical image compacts", page.assign(document));
check("exactly one image", page.images().size() == 1);
const auto& record = page.images()[0];
const auto alt = page.image_alt(record);
const auto url = page.image_url(record);
check("alt round-trips", alt.length == 12 &&
std::string(alt.value, alt.length) == "The RNS logo");
check("url round-trips (verbatim with leading ':')",
url.length == 17 && std::string(url.value, url.length) ==
":/media/demo.webp");
check("width NATIVE", record.width_kind == ImageDimension::NATIVE);
check("height NONE (unspecified)", record.height_kind == ImageDimension::NONE);
check("align CENTER", record.align == Alignment::CENTER);
// The block that references the image resolves to it.
bool block_ok = false;
for (const auto& block : page.blocks()) {
if (block.type == BlockType::IMAGE) {
block_ok = block.image_index == 0 &&
block.image_index < static_cast<int16_t>(page.images().size());
}
}
check("IMAGE block resolves to image record", block_ok);
}
// 2. Multiple images: distinct records, correct per-block resolution.
{
const auto document = parser.parse(
"`(one`w=100`a=l`:/a.webp)\n`(two`w=50%`a=r`:/b.webp)");
CompactPage page;
check("multi-image compacts", page.assign(document));
check("two images", page.images().size() == 2);
if (page.images().size() == 2) {
const auto& first = page.images()[0];
const auto& second = page.images()[1];
check("first url", std::string(
page.image_url(first).value,
page.image_url(first).length) == ":/a.webp");
check("second url", std::string(
page.image_url(second).value,
page.image_url(second).length) == ":/b.webp");
check("first width PIXELS 100",
first.width_kind == ImageDimension::PIXELS && first.width_value == 100);
check("second width PERCENT 50",
second.width_kind == ImageDimension::PERCENT && second.width_value == 50);
check("first align LEFT", first.align == Alignment::LEFT);
check("second align RIGHT", second.align == Alignment::RIGHT);
}
// Each IMAGE block points at the correct record.
std::size_t seen_images = 0;
bool resolution_ok = true;
for (const auto& block : page.blocks()) {
if (block.type != BlockType::IMAGE) continue;
if (block.image_index != static_cast<int16_t>(seen_images))
resolution_ok = false;
++seen_images;
}
check("blocks resolve to distinct records in order", resolution_ok &&
seen_images == 2);
}
// 3. clear() drops the image records; re-assign repopulates them.
{
CompactPage page;
const auto document = parser.parse("`(keep`:/k.webp)");
check("initial compact", page.assign(document));
check("one image before clear", page.images().size() == 1);
page.clear();
check("zero images after clear", page.images().size() == 0);
check("re-assign repopulates", page.assign(document) &&
page.images().size() == 1);
}
// 4. Move-assignment preserves image records and keeps every IMAGE
// block index valid. Regression: operator=(CompactPage&&) once
// swapped every member except _images, so the screen's
// `set_page` publish (candidate -> _page move) silently dropped
// all image records while blocks kept their image_index.
{
const auto document = parser.parse(
"text\n"
"`(moved`w=100`h=50`a=r`:/media/m.webp)\n"
"more");
CompactPage candidate;
check("candidate compacts", candidate.assign(document));
check("candidate has image", candidate.images().size() == 1);
CompactPage target;
target = std::move(candidate);
check("moved page keeps its image", target.images().size() == 1);
const auto& record = target.images()[0];
check("moved record fields survive",
record.width_kind == ImageDimension::PIXELS &&
record.width_value == 100 &&
record.height_kind == ImageDimension::PIXELS &&
record.height_value == 50 &&
record.align == Alignment::RIGHT &&
std::string(target.image_url(record).value,
target.image_url(record).length) ==
":/media/m.webp");
check("moved image alt survives",
std::string(target.image_alt(record).value,
target.image_alt(record).length) == "moved");
bool block_resolves = false;
for (const auto& block : target.blocks()) {
if (block.type != BlockType::IMAGE) continue;
block_resolves = block.image_index == 0 &&
static_cast<std::size_t>(block.image_index) <
target.images().size();
}
check("moved IMAGE block resolves to retained record", block_resolves);
// Move-construction (delegates to move-assignment) must preserve
// records too.
CompactPage fresh;
check("fresh compacts", fresh.assign(document) &&
fresh.images().size() == 1);
CompactPage constructed(std::move(fresh));
check("move-constructed page keeps its image",
constructed.images().size() == 1);
}
if (failures == 0)
std::cout << "image compact checks passed" << '\n';
else
std::cout << failures << " image compact checks FAILED" << '\n';
return failures == 0 ? 0 : 1;
}