#include #include #include #include #include #include #include #include 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 rows; std::vector 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(); }