mirror of
https://github.com/mikecarper/MeshCore.git
synced 2026-09-15 19:55:54 +00:00
417 lines
14 KiB
C++
417 lines
14 KiB
C++
#include <gtest/gtest.h>
|
|
|
|
#include <helpers/ui/DisplayDriver.h>
|
|
#include <helpers/ui/CompanionHomeLayout.h>
|
|
#include <helpers/ui/DisplayTextLayout.h>
|
|
#include <helpers/ui/WiFiSetupQrDisplay.h>
|
|
#include <helpers/ui/WiFiSetupQrPayload.h>
|
|
|
|
#include <string>
|
|
#include <vector>
|
|
|
|
ColorVal UIColor::window_bkg = 0;
|
|
ColorVal UIColor::title_bkg = 0;
|
|
ColorVal UIColor::title_txt = 0;
|
|
ColorVal UIColor::primary_txt = 1;
|
|
ColorVal UIColor::secondary_txt = 1;
|
|
ColorVal UIColor::warning_txt = 1;
|
|
ColorVal UIColor::popup_bkg = 0;
|
|
ColorVal UIColor::popup_txt = 1;
|
|
ColorVal UIColor::corp_blue = 1;
|
|
|
|
namespace {
|
|
|
|
bool isValidUTF8(const char* text) {
|
|
for (size_t offset = 0; text[offset] != 0;) {
|
|
uint8_t first = (uint8_t)text[offset];
|
|
size_t length = 1;
|
|
if (first >= 0xC2 && first <= 0xDF) {
|
|
length = 2;
|
|
} else if (first >= 0xE0 && first <= 0xEF) {
|
|
length = 3;
|
|
} else if (first >= 0xF0 && first <= 0xF4) {
|
|
length = 4;
|
|
} else if (first >= 0x80) {
|
|
return false;
|
|
}
|
|
for (size_t i = 1; i < length; ++i) {
|
|
if (text[offset + i] == 0
|
|
|| ((uint8_t)text[offset + i] & 0xC0) != 0x80) {
|
|
return false;
|
|
}
|
|
}
|
|
offset += length;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
class TestDisplay : public DisplayDriver {
|
|
public:
|
|
struct DrawnText {
|
|
int x;
|
|
int y;
|
|
std::string text;
|
|
};
|
|
|
|
struct FilledRect {
|
|
int x;
|
|
int y;
|
|
int width;
|
|
int height;
|
|
};
|
|
|
|
std::string printed;
|
|
std::vector<DrawnText> rows;
|
|
std::vector<FilledRect> fills;
|
|
int cursor_x = 0;
|
|
int cursor_y = 0;
|
|
int text_size = 1;
|
|
int glyph_width = 1;
|
|
|
|
TestDisplay(int width = 100, int height = 100, int glyph_width = 1)
|
|
: DisplayDriver(width, height), glyph_width(glyph_width) {}
|
|
|
|
bool isOn() override { return true; }
|
|
void turnOn() override {}
|
|
void turnOff() override {}
|
|
void clear() override {}
|
|
void startFrame(ColorVal) override {}
|
|
void setTextSize(int size) override { text_size = size > 0 ? size : 1; }
|
|
void setColor(ColorVal) override {}
|
|
void setCursor(int x, int y) override {
|
|
cursor_x = x;
|
|
cursor_y = y;
|
|
}
|
|
void print(const char* str) override {
|
|
printed = str;
|
|
rows.push_back({cursor_x, cursor_y, str});
|
|
}
|
|
void fillRect(int x, int y, int width, int height) override {
|
|
fills.push_back({x, y, width, height});
|
|
}
|
|
void drawRect(int, int, int, int) override {}
|
|
void drawXbm(int, int, const uint8_t*, int, int) override {}
|
|
uint16_t getTextWidth(const char* str) override {
|
|
uint16_t width = 0;
|
|
for (size_t i = 0; str[i] != 0; ++i) {
|
|
if (((uint8_t)str[i] & 0xC0) != 0x80) ++width;
|
|
}
|
|
return width * glyph_width * text_size;
|
|
}
|
|
void endFrame() override {}
|
|
};
|
|
|
|
class QrTestDisplay : public TestDisplay {
|
|
public:
|
|
int qr_x = -1;
|
|
int qr_y = -1;
|
|
int qr_size = -1;
|
|
|
|
QrTestDisplay(int width, int height, int glyph_width)
|
|
: TestDisplay(width, height, glyph_width) {}
|
|
|
|
bool drawQrCode(const char*, int x, int y, int size) override {
|
|
qr_x = x;
|
|
qr_y = y;
|
|
qr_size = size;
|
|
return true;
|
|
}
|
|
};
|
|
|
|
} // namespace
|
|
|
|
TEST(DisplayDriver, EllipsizesOnlyAtUTF8CodepointBoundaries) {
|
|
TestDisplay display;
|
|
display.drawTextEllipsized(0, 0, 5, "AB\xF0\x9F\x98\x80" "CDE");
|
|
EXPECT_EQ("AB...", display.printed);
|
|
EXPECT_TRUE(isValidUTF8(display.printed.c_str()));
|
|
}
|
|
|
|
TEST(DisplayDriver, QrCodeIsOptionalByDefault) {
|
|
TestDisplay display;
|
|
EXPECT_FALSE(display.drawQrCode("WIFI:T:nopass;S:MC-Set-90DF;;",
|
|
27, 55, 105));
|
|
}
|
|
|
|
TEST(DisplayDriver, CompactWiFiQrLeavesRoomForCompleteSetupAddress) {
|
|
QrTestDisplay display(128, 64, 6);
|
|
ASSERT_TRUE(mesh::ui::drawWiFiSetupQr(
|
|
display, "MC-F7F0", "192.168.4.1"));
|
|
|
|
EXPECT_EQ(0, display.qr_x);
|
|
EXPECT_EQ(0, display.qr_y);
|
|
EXPECT_EQ(58, display.qr_size);
|
|
ASSERT_FALSE(display.rows.empty());
|
|
const auto& address = display.rows.back();
|
|
EXPECT_EQ("192.168.4.1", address.text);
|
|
EXPECT_EQ(48, address.y);
|
|
EXPECT_LE(address.x + display.getTextWidth(address.text.c_str()),
|
|
display.width());
|
|
}
|
|
|
|
TEST(WiFiSetupQrPayload, EncodesOpenSetupNetwork) {
|
|
char payload[64];
|
|
EXPECT_TRUE(mesh::ui::buildWiFiSetupQrPayload(
|
|
payload, sizeof(payload), "MC-Set-90DF"));
|
|
EXPECT_STREQ("WIFI:S:MC-Set-90DF;;", payload);
|
|
|
|
char small_payload[32];
|
|
EXPECT_TRUE(mesh::ui::buildWiFiSetupQrPayload(
|
|
small_payload, sizeof(small_payload), "MC-90DF"));
|
|
EXPECT_STREQ("WIFI:S:MC-90DF;;", small_payload);
|
|
// MC-XXXX uses 16 payload bytes, within QR Version 1-L's 17-byte capacity.
|
|
EXPECT_LE(strlen(small_payload), 17U);
|
|
}
|
|
|
|
TEST(WiFiSetupQrPayload, EscapesProtectedNetworkFields) {
|
|
char payload[96];
|
|
EXPECT_TRUE(mesh::ui::buildWiFiSetupQrPayload(
|
|
payload, sizeof(payload), "Cafe;West", "p,ass"));
|
|
EXPECT_STREQ("WIFI:T:WPA;S:Cafe\\;West;P:p\\,ass;;", payload);
|
|
}
|
|
|
|
TEST(WiFiSetupQrPayload, RejectsMissingOrTruncatedFields) {
|
|
char payload[8];
|
|
EXPECT_FALSE(mesh::ui::buildWiFiSetupQrPayload(
|
|
payload, sizeof(payload), ""));
|
|
EXPECT_FALSE(mesh::ui::buildWiFiSetupQrPayload(
|
|
payload, sizeof(payload), "MC-Set-90DF"));
|
|
}
|
|
|
|
TEST(DisplayDriver, FixedBufferDoesNotSplitUTF8Codepoint) {
|
|
TestDisplay display;
|
|
std::string text(254, 'A');
|
|
text += "\xF0\x9F\x98\x80";
|
|
text += 'B';
|
|
display.drawTextEllipsized(0, 0, 255, text.c_str());
|
|
EXPECT_EQ(std::string(254, 'A'), display.printed);
|
|
EXPECT_TRUE(isValidUTF8(display.printed.c_str()));
|
|
}
|
|
|
|
TEST(DisplayDriver, WrapsCompleteTextAcrossAvailableRows) {
|
|
TestDisplay display;
|
|
EXPECT_EQ(3, mesh::ui::drawTextWrapped(
|
|
display, 4, 10, 3, 9, 3, "ABCDEFG"));
|
|
ASSERT_EQ(3U, display.rows.size());
|
|
EXPECT_EQ(4, display.rows[0].x);
|
|
EXPECT_EQ(10, display.rows[0].y);
|
|
EXPECT_EQ("ABC", display.rows[0].text);
|
|
EXPECT_EQ(4, display.rows[1].x);
|
|
EXPECT_EQ(19, display.rows[1].y);
|
|
EXPECT_EQ("DEF", display.rows[1].text);
|
|
EXPECT_EQ(28, display.rows[2].y);
|
|
EXPECT_EQ("G", display.rows[2].text);
|
|
}
|
|
|
|
TEST(DisplayDriver, WrappedTextKeepsUTF8CodepointsIntact) {
|
|
TestDisplay display;
|
|
EXPECT_EQ(2, mesh::ui::drawTextWrapped(
|
|
display, 0, 0, 3, 10, 2, "AB\xF0\x9F\x98\x80" "CDE"));
|
|
ASSERT_EQ(2U, display.rows.size());
|
|
EXPECT_EQ("AB\xF0\x9F\x98\x80", display.rows[0].text);
|
|
EXPECT_EQ("CDE", display.rows[1].text);
|
|
EXPECT_TRUE(isValidUTF8(display.rows[0].text.c_str()));
|
|
EXPECT_TRUE(isValidUTF8(display.rows[1].text.c_str()));
|
|
}
|
|
|
|
TEST(DisplayDriver, KeepsMaximumLengthSSIDWithoutEllipsis) {
|
|
TestDisplay display;
|
|
const std::string ssid = "1234567890ABCDEF1234567890ABCDEF";
|
|
EXPECT_EQ(3, mesh::ui::drawTextWrapped(
|
|
display, 0, 0, 12, 10, 3, ssid.c_str()));
|
|
ASSERT_EQ(3U, display.rows.size());
|
|
EXPECT_EQ(ssid,
|
|
display.rows[0].text + display.rows[1].text
|
|
+ display.rows[2].text);
|
|
}
|
|
|
|
TEST(DisplayDriver, LongSetupAddressStaysInsideItsRows) {
|
|
TestDisplay display(160, 160);
|
|
const std::string address =
|
|
"http://very-long-indicator-hostname-for-a-setup-page.example.invalid/"
|
|
"path/that/remains/longer/than/one/logical/display/row/and/continues/"
|
|
"through/the/second/bounded/row/without/reaching/the/pairing/status";
|
|
EXPECT_EQ(2, mesh::ui::drawTextWrapped(
|
|
display, 6, 99, 148, 13, 2, address.c_str()));
|
|
ASSERT_EQ(2U, display.rows.size());
|
|
for (const auto& row : display.rows) {
|
|
EXPECT_GE(row.x, 6);
|
|
EXPECT_LE(row.x + display.getTextWidth(row.text.c_str()), 154);
|
|
EXPECT_GE(row.y, 99);
|
|
EXPECT_LT(row.y, 125);
|
|
}
|
|
}
|
|
|
|
TEST(DisplayDriver, IndicatorHomeSeparatesInfoAndPairingGeometry) {
|
|
const mesh::ui::CompanionHomeLayout layout =
|
|
mesh::ui::makeLargeCompanionHomeLayout(160, 160);
|
|
|
|
EXPECT_EQ(128, layout.pairing.y + layout.pairing.height / 2);
|
|
EXPECT_EQ(48, layout.pairing.height);
|
|
EXPECT_FALSE(mesh::ui::displayRegionsOverlap(layout.info,
|
|
layout.pairing));
|
|
EXPECT_LE(layout.info.bottom() + 12, layout.pairing.y);
|
|
EXPECT_TRUE(mesh::ui::displayRegionContainsLine(
|
|
layout.info, layout.instruction_y, 12));
|
|
EXPECT_TRUE(mesh::ui::displayRegionContainsLine(
|
|
layout.info, layout.network_y, 12));
|
|
EXPECT_TRUE(mesh::ui::displayRegionContainsLine(
|
|
layout.pairing, layout.pairing_label_y, 12));
|
|
EXPECT_TRUE(mesh::ui::displayRegionContainsLine(
|
|
layout.pairing, layout.pairing_value_y, 24));
|
|
}
|
|
|
|
TEST(DisplayDriver, NativeIndicatorExpandsHomeTypographyRegions) {
|
|
EXPECT_TRUE(mesh::ui::usesExpandedCompanionHomeTypography(
|
|
160, 160, 480, 480));
|
|
EXPECT_FALSE(mesh::ui::usesExpandedCompanionHomeTypography(
|
|
160, 160, 320, 320));
|
|
EXPECT_FALSE(mesh::ui::usesExpandedCompanionHomeTypography(
|
|
128, 64, 480, 480));
|
|
|
|
const mesh::ui::CompanionHomeLayout layout =
|
|
mesh::ui::makeLargeCompanionHomeLayout(160, 160, true);
|
|
EXPECT_EQ(2, layout.info.x);
|
|
EXPECT_EQ(61, layout.info.y);
|
|
EXPECT_EQ(156, layout.info.width);
|
|
EXPECT_EQ(39, layout.info.height);
|
|
EXPECT_EQ(8, layout.pairing.x);
|
|
EXPECT_EQ(102, layout.pairing.y);
|
|
EXPECT_EQ(144, layout.pairing.width);
|
|
EXPECT_EQ(58, layout.pairing.height);
|
|
EXPECT_FALSE(mesh::ui::displayRegionsOverlap(layout.info,
|
|
layout.pairing));
|
|
EXPECT_TRUE(mesh::ui::displayRegionContainsLine(
|
|
layout.info, layout.instruction_y, 39));
|
|
EXPECT_TRUE(mesh::ui::displayRegionContainsLine(
|
|
layout.pairing, layout.pairing_label_y, 29));
|
|
EXPECT_TRUE(mesh::ui::displayRegionContainsLine(
|
|
layout.pairing, layout.pairing_value_y, 29));
|
|
}
|
|
|
|
TEST(DisplayDriver, IndicatorPairingValuesFitAtRenderedTextSizes) {
|
|
// Model the fallback font's fixed six-logical-pixel cell. It is the wider
|
|
// of the two paths for CONNECTED; the profile test reads the recovered
|
|
// VLW's real advances and independently checks that path.
|
|
TestDisplay display(160, 160, 6);
|
|
const mesh::ui::CompanionHomeLayout layout =
|
|
mesh::ui::makeLargeCompanionHomeLayout(display.width(),
|
|
display.height());
|
|
|
|
display.setTextSize(3);
|
|
EXPECT_EQ(108, display.getTextWidth("123456"));
|
|
EXPECT_LE(display.getTextWidth("123456"), layout.pairing.width);
|
|
mesh::ui::drawTextCenteredEllipsized(
|
|
display, layout.pairing, layout.pairing_value_y, "123456");
|
|
ASSERT_EQ(1U, display.rows.size());
|
|
EXPECT_EQ("123456", display.rows.back().text);
|
|
EXPECT_GE(display.rows.back().x, layout.pairing.x);
|
|
EXPECT_LE(display.rows.back().x + display.getTextWidth("123456"),
|
|
layout.pairing.right());
|
|
|
|
display.setTextSize(2);
|
|
EXPECT_EQ(108, display.getTextWidth("CONNECTED"));
|
|
EXPECT_LE(display.getTextWidth("CONNECTED"), layout.pairing.width);
|
|
mesh::ui::drawTextCenteredEllipsized(
|
|
display, layout.pairing, layout.pairing_value_y, "CONNECTED");
|
|
ASSERT_EQ(2U, display.rows.size());
|
|
EXPECT_EQ("CONNECTED", display.rows.back().text);
|
|
EXPECT_GE(display.rows.back().x, layout.pairing.x);
|
|
EXPECT_LE(display.rows.back().x + display.getTextWidth("CONNECTED"),
|
|
layout.pairing.right());
|
|
}
|
|
|
|
TEST(DisplayDriver, CompactPairingOwnsBounded128x64LowerRegion) {
|
|
EXPECT_TRUE(mesh::ui::usesCompactCompanionPairingLayout(128, 64));
|
|
EXPECT_FALSE(mesh::ui::usesCompactCompanionPairingLayout(160, 160));
|
|
EXPECT_FALSE(mesh::ui::usesCompactCompanionPairingLayout(128, 65));
|
|
|
|
const mesh::ui::CompactCompanionPairingLayout layout =
|
|
mesh::ui::makeCompactCompanionPairingLayout(128, 64);
|
|
const mesh::ui::DisplayRegion inbox = {0, 22, 128, 16};
|
|
|
|
EXPECT_EQ(4, layout.pairing.x);
|
|
EXPECT_EQ(38, layout.pairing.y);
|
|
EXPECT_EQ(120, layout.pairing.width);
|
|
EXPECT_EQ(26, layout.pairing.height);
|
|
EXPECT_FALSE(mesh::ui::displayRegionsOverlap(inbox, layout.pairing));
|
|
EXPECT_TRUE(mesh::ui::displayRegionContainsLine(
|
|
layout.pairing, layout.pairing_label_y, 8));
|
|
EXPECT_TRUE(mesh::ui::displayRegionContainsLine(
|
|
layout.pairing, layout.pairing_value_y, 16));
|
|
|
|
// The old instruction and Wi-Fi rows are entirely replaced while pairing
|
|
// is active, so neither can remain behind or overprint the new value.
|
|
EXPECT_TRUE(mesh::ui::displayRegionContainsLine(layout.pairing, 43, 8));
|
|
EXPECT_TRUE(mesh::ui::displayRegionContainsLine(layout.pairing, 54, 8));
|
|
}
|
|
|
|
TEST(DisplayDriver, CompactPairingValuesFitAtSizeTwo) {
|
|
TestDisplay display(128, 64, 6);
|
|
const mesh::ui::CompactCompanionPairingLayout layout =
|
|
mesh::ui::makeCompactCompanionPairingLayout(
|
|
display.width(), display.height());
|
|
|
|
display.setTextSize(2);
|
|
for (const char* value : {"123456", "CONNECTED"}) {
|
|
EXPECT_LE(display.getTextWidth(value), layout.pairing.width);
|
|
mesh::ui::drawTextCenteredEllipsized(
|
|
display, layout.pairing, layout.pairing_value_y, value);
|
|
EXPECT_EQ(value, display.rows.back().text);
|
|
EXPECT_GE(display.rows.back().x, layout.pairing.x);
|
|
EXPECT_LE(display.rows.back().x + display.getTextWidth(value),
|
|
layout.pairing.right());
|
|
}
|
|
}
|
|
|
|
TEST(DisplayDriver, LongHomeStatusCannotEnterPairingBlock) {
|
|
TestDisplay display(160, 160);
|
|
const mesh::ui::CompanionHomeLayout layout =
|
|
mesh::ui::makeLargeCompanionHomeLayout(display.width(),
|
|
display.height());
|
|
const std::string instruction(240, 'I');
|
|
const std::string network_status =
|
|
"Connected to a maximum-length-wireless-network-name at an "
|
|
"unexpectedly-long-address.example.invalid";
|
|
|
|
mesh::ui::drawTextCenteredEllipsized(
|
|
display, layout.info, layout.instruction_y, instruction.c_str());
|
|
mesh::ui::drawTextCenteredEllipsized(
|
|
display, layout.info, layout.network_y, network_status.c_str());
|
|
|
|
ASSERT_EQ(2U, display.rows.size());
|
|
for (const auto& row : display.rows) {
|
|
EXPECT_GE(row.x, layout.info.x);
|
|
EXPECT_LE(row.x + display.getTextWidth(row.text.c_str()),
|
|
layout.info.right());
|
|
EXPECT_LT(row.y, layout.pairing.y);
|
|
}
|
|
}
|
|
|
|
TEST(DisplayDriver, PairingRegionIsClearedBeforeStatusReplacement) {
|
|
TestDisplay display(160, 160);
|
|
const mesh::ui::CompanionHomeLayout layout =
|
|
mesh::ui::makeLargeCompanionHomeLayout(display.width(),
|
|
display.height());
|
|
|
|
mesh::ui::clearDisplayRegion(display, layout.pairing);
|
|
mesh::ui::drawTextCenteredEllipsized(
|
|
display, layout.pairing, layout.pairing_value_y, "123456");
|
|
mesh::ui::clearDisplayRegion(display, layout.pairing);
|
|
mesh::ui::drawTextCenteredEllipsized(
|
|
display, layout.pairing, layout.pairing_value_y, "CONNECTED");
|
|
|
|
ASSERT_EQ(2U, display.fills.size());
|
|
for (const auto& fill : display.fills) {
|
|
EXPECT_EQ(layout.pairing.x, fill.x);
|
|
EXPECT_EQ(layout.pairing.y, fill.y);
|
|
EXPECT_EQ(layout.pairing.width, fill.width);
|
|
EXPECT_EQ(layout.pairing.height, fill.height);
|
|
}
|
|
}
|
|
|
|
int main(int argc, char** argv) {
|
|
::testing::InitGoogleTest(&argc, argv);
|
|
return RUN_ALL_TESTS();
|
|
}
|