#include #include #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; int qr_border = -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; } bool drawQrCodeWithBorder(const char* text, int x, int y, int size, int border) override { qr_border = border; return drawQrCode(text, x, y, size); } }; } // 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(SmallMessageText, TinyPanelsKeepFivePixelCapitalsAndNormalFont) { TestDisplay display(72, 40, 6); mesh::ui::SmallMessageText text(display); text.setCursor(0, 0); text.print("A"); const char* rows[] = {"010", "101", "111", "101", "101"}; bool pixels[5][3] = {}; for (const auto& pixel : display.fills) { ASSERT_GE(pixel.x, 0); ASSERT_LT(pixel.x, 3); ASSERT_GE(pixel.y, 0); ASSERT_LT(pixel.y, 5); pixels[pixel.y][pixel.x] = true; } for (int y = 0; y < 5; ++y) for (int x = 0; x < 3; ++x) EXPECT_EQ(rows[y][x] == '1', pixels[y][x]); EXPECT_EQ(12, text.getTextWidth("ABC")); EXPECT_EQ(18, display.getTextWidth("ABC")); } TEST(SmallMessageText, LargerPanelsUseSixPixelCapitalsAndNormalFont) { TestDisplay display(128, 64, 6); mesh::ui::SmallMessageText text(display); text.setCursor(0, 0); text.print("A"); const char* rows[] = {"010", "101", "111", "101", "101", "101"}; bool pixels[6][3] = {}; for (const auto& pixel : display.fills) { ASSERT_GE(pixel.x, 0); ASSERT_LT(pixel.x, 3); ASSERT_GE(pixel.y, 0); ASSERT_LT(pixel.y, 6); pixels[pixel.y][pixel.x] = true; } for (int y = 0; y < 6; ++y) for (int x = 0; x < 3; ++x) EXPECT_EQ(rows[y][x] == '1', pixels[y][x]); EXPECT_EQ(11, text.getTextWidth("ABC")); EXPECT_EQ(18, display.getTextWidth("ABC")); } TEST(SmallMessageText, FontChoiceAndLineSpacingFollowPanelGeometry) { const int cases[][3] = { {128, 64, 6}, {64, 128, 6}, {160, 80, 6}, {72, 40, 5}, {40, 72, 5}, {128, 32, 5}, {32, 128, 5}, {64, 48, 5}, {48, 64, 5}}; for (const auto& dimensions : cases) { TestDisplay display(dimensions[0], dimensions[1]); mesh::ui::SmallMessageText text(display); EXPECT_EQ(dimensions[2], text.capitalHeight()); EXPECT_EQ(dimensions[2] + 1, text.glyphHeight()); EXPECT_EQ(dimensions[2] + 2, text.lineHeight()); EXPECT_EQ(0, text.lineCount(display.height() - text.glyphHeight() + 1)); EXPECT_EQ(1, text.lineCount(display.height() - text.glyphHeight())); } } TEST(SmallMessageText, CompactOriginAllowsFiveCompleteSixPixelMessageRows) { TestDisplay display(128, 64); mesh::ui::drawSmallMessageBody(display, std::string(61, 'W').c_str(), std::string(160, 'W').c_str()); ASSERT_FALSE(display.fills.empty()); bool final_dots[3] = {}; for (const auto& pixel : display.fills) { EXPECT_GE(pixel.x, 0); EXPECT_LT(pixel.x, 128); EXPECT_GE(pixel.y, 14); EXPECT_LT(pixel.y, 64); // The origin stays in its own line; its long name cannot cover the body. EXPECT_TRUE(pixel.y < 21 || pixel.y >= 22); for (int i = 0; i < 3; ++i) if (pixel.y == 59 && pixel.x == 120 + 2 * i) final_dots[i] = true; } for (bool dot : final_dots) EXPECT_TRUE(dot); mesh::ui::SmallMessageText text(display); EXPECT_EQ(5, text.lineCount(22)); EXPECT_EQ(4, text.lineCount(22, 56)); EXPECT_EQ(5, text.lineCount(22, 80)); // never extend past the panel EXPECT_EQ(0, text.lineCount(22, 20)); } TEST(SmallMessageText, TinyInterfaceKeepsThreeCompleteFivePixelRows) { TestDisplay display(72, 40); mesh::ui::SmallMessageText text(display); EXPECT_EQ(3, text.lineCount(17)); mesh::ui::drawSmallMessageBody(display, std::string(61, 'W').c_str(), std::string(160, 'W').c_str(), 10); bool final_dots[3] = {}; for (const auto& pixel : display.fills) { EXPECT_GE(pixel.x, 0); EXPECT_LT(pixel.x, 72); EXPECT_GE(pixel.y, 10); EXPECT_LT(pixel.y, 40); EXPECT_TRUE(pixel.y < 16 || pixel.y >= 17); for (int i = 0; i < 3; ++i) if (pixel.y == 35 && pixel.x == 60 + 2 * i) final_dots[i] = true; } for (bool dot : final_dots) EXPECT_TRUE(dot); } TEST(SmallMessageText, MessageBodyRespectsReservedFooter) { TestDisplay display(128, 64); mesh::ui::drawSmallMessageBody(display, "Ch 0 Public [4h]:", std::string(160, 'W').c_str(), 14, 56); ASSERT_FALSE(display.fills.empty()); for (const auto& pixel : display.fills) EXPECT_LT(pixel.y, 56); } TEST(SmallMessageText, ButtonHintKeepsFiveMessageRowsWithoutOverlappingText) { TestDisplay display(128, 64); mesh::ui::SmallMessageText text(display); const auto hint = mesh::ui::makeButtonReaderHintLayout( text, text.lineHeight(), display.height()); ASSERT_EQ(1, hint.line_count); EXPECT_EQ(56, hint.top); EXPECT_EQ(5, text.lineCount(2 * text.lineHeight(), hint.top)); mesh::ui::drawSmallMessageBody(display, "Ch 0 Public [4h]:", std::string(160, 'W').c_str(), text.lineHeight(), hint.top); ASSERT_FALSE(display.fills.empty()); for (const auto& pixel : display.fills) EXPECT_LT(pixel.y, hint.top); display.fills.clear(); mesh::ui::drawButtonReaderHint(text, hint); ASSERT_FALSE(display.fills.empty()); for (const auto& pixel : display.fills) { EXPECT_GE(pixel.x, 0); EXPECT_LE(pixel.x + pixel.width, display.width()); EXPECT_GE(pixel.y, hint.top); EXPECT_LE(pixel.y + pixel.height, display.height()); } } TEST(SmallMessageText, ButtonHintReflowsOnTinyAndRotatedScreens) { for (auto dimensions : {std::pair{72, 40}, {40, 72}, {128, 32}, {32, 128}, {64, 128}, {128, 64}}) { TestDisplay display(dimensions.first, dimensions.second); mesh::ui::SmallMessageText text(display); const auto navigation = mesh::ui::makeButtonReaderHintLayout( text, text.lineHeight(), display.height()); for (bool groups : {false, true}) { display.fills.clear(); const auto hint = mesh::ui::makeButtonReaderHintLayout( text, text.lineHeight(), display.height(), groups); EXPECT_EQ(navigation.top, hint.top); EXPECT_EQ(navigation.line_count, hint.line_count); for (int row = 0; row < hint.line_count; ++row) EXPECT_LE(text.getTextWidth(hint.lines[row]), display.width()); mesh::ui::drawButtonReaderHint(text, hint); ASSERT_FALSE(display.fills.empty()); for (const auto& pixel : display.fills) { EXPECT_GE(pixel.x, 0); EXPECT_LE(pixel.x + pixel.width, display.width()); EXPECT_GE(pixel.y, hint.top); EXPECT_LE(pixel.y + pixel.height, display.height()); } } } } TEST(SmallMessageText, GroupHintFitsV4FooterWithoutLosingMessageRows) { TestDisplay display(128, 64); mesh::ui::SmallMessageText text(display); const auto hint = mesh::ui::makeButtonReaderHintLayout( text, text.lineHeight(), display.height(), true); ASSERT_EQ(1, hint.line_count); EXPECT_STREQ("4 <<- 2 <- tap -> 1 ->> 3 hold: Exit", hint.lines[0]); EXPECT_EQ(6, text.capitalHeight()); EXPECT_EQ(108, text.getTextWidth(hint.lines[0])); EXPECT_GT(text.getTextWidth(hint.lines[0]), text.getTextWidth("4<<21>>3 hold:X")); EXPECT_LE(text.getTextWidth(hint.lines[0]), 128); EXPECT_EQ(56, hint.top); EXPECT_EQ(5, text.lineCount(2 * text.lineHeight(), hint.top)); } TEST(SmallMessageText, HintDropsExtraSpacesBeforeAddingRows) { for (int width : {64, 107, 108, 128}) { TestDisplay display(width, 128); mesh::ui::SmallMessageText text(display); const auto hint = mesh::ui::makeButtonReaderHintLayout( text, text.lineHeight(), display.height()); ASSERT_EQ(1, hint.line_count); EXPECT_STREQ(width >= text.getTextWidth("4 <<- 2 <- tap -> 1 ->> 3 hold: Exit") ? "4 <<- 2 <- tap -> 1 ->> 3 hold: Exit" : "4<<21>>3 hold:X", hint.lines[0]); EXPECT_LE(text.getTextWidth(hint.lines[0]), width); EXPECT_EQ(120, hint.top); } } TEST(SmallMessageText, Full160CharacterSamplesFitInFiveRows) { class RecordingText : public mesh::ui::SmallMessageText { public: std::string drawn; explicit RecordingText(DisplayDriver& display) : SmallMessageText(display) {} void print(const char* str) override { drawn += str; SmallMessageText::print(str); } }; const char* samples[] = { "GIANT KILLER: Out on the Mercerwood mesh today, just the V4 and a small " "battery Checking the smaller font so the rest of this message is visible. " "0123456789 END", "NimBLE-XIAO-Trial: Picopixel: 5 pixel letters and a compact channel line. " "More of this message now fits on the small screen. " "0123456789 0123456789 012345678 END"}; for (const char* sample : samples) { ASSERT_EQ(160U, strlen(sample)); TestDisplay display(128, 64); RecordingText text(display); EXPECT_EQ(5, mesh::ui::drawTextWrapped(text, 0, 22, 128, text.lineHeight(), text.lineCount(22), sample)); EXPECT_EQ(sample, text.drawn); // The button reader moves its header/origin up to reserve the hint, so // the same complete 160-character text still fits in five body rows. text.drawn.clear(); const auto hint = mesh::ui::makeButtonReaderHintLayout( text, text.lineHeight(), display.height()); const int message_y = 2 * text.lineHeight(); EXPECT_EQ(5, mesh::ui::drawTextWrapped(text, 0, message_y, 128, text.lineHeight(), text.lineCount(message_y, hint.top), sample)); EXPECT_EQ(sample, text.drawn); } } TEST(SmallMessageText, GlyphsStayInsideTinyAndRotatedScreens) { for (auto dimensions : {std::pair{72, 40}, {40, 72}, {128, 32}, {32, 128}, {64, 128}, {128, 64}}) { TestDisplay display(dimensions.first, dimensions.second); mesh::ui::SmallMessageText text(display); for (int c = 32; c <= 126; ++c) { display.fills.clear(); text.setCursor(0, display.height() - text.glyphHeight()); text.printWordWrap(std::string(160, char(c)).c_str(), display.width()); for (const auto& pixel : display.fills) { EXPECT_GE(pixel.x, 0); EXPECT_LT(pixel.x, display.width()); EXPECT_GE(pixel.y, display.height() - text.glyphHeight()); EXPECT_LT(pixel.y, display.height()); } } display.fills.clear(); text.setCursor(0, display.height() - text.glyphHeight() + 1); text.printWordWrap("must not start a clipped final line", display.width()); EXPECT_TRUE(display.fills.empty()); } } 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(DisplayDriver, V4QrUsesOnePixelBorderAndKeepsActionsBesideTheCode) { QrTestDisplay display(128, 64, 6); ASSERT_TRUE(mesh::ui::drawWiFiSetupQr( display, "MC-F7F0", "192.168.4.1", true, "HOLD STOP")); EXPECT_EQ(44, display.qr_size); EXPECT_EQ(1, display.qr_border); EXPECT_EQ(21 * 2 + 2 * display.qr_border, display.qr_size); ASSERT_EQ(5U, display.rows.size()); EXPECT_EQ("SCAN JOIN", display.rows[0].text); EXPECT_EQ("HOLD STOP", display.rows[1].text); EXPECT_EQ("192.168.4.1", display.rows.back().text); for (const auto& row : display.rows) { EXPECT_GE(row.x, display.qr_size + 4); EXPECT_LE(row.x + display.getTextWidth(row.text.c_str()), display.width()); EXPECT_LE(row.y + 8, display.height()); } } TEST(DisplayDriver, V4QrKeepsFallbackWhenRendererIsUnavailable) { TestDisplay display(128, 64, 6); EXPECT_FALSE(mesh::ui::drawWiFiSetupQr( display, "MC-F7F0", "192.168.4.1", true, "HOLD STOP")); EXPECT_TRUE(display.rows.empty()); } 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(); }