Files
wadamesh/test/test_avatar_text.cpp
Michael A. CojocariandClaude Opus 5.5 c7156b446e touch: chat icon initials are the first letters of the first and last word (#785)
The Initials look took the first two letters of the name, so "Eddie Smith"
showed ED. It now takes the first letter of the first word and of the last
word: ES, "john paul jones" JJ, "Eddie-Base" EB, "kd2abc_mobile" KM.

- Words break on whitespace and the separators node names use
  (- _ . / | , ; : + &); a combining accent is not a break.
- A word with no letter the font can draw (an emoji on its own) does not
  count. A name left with one word keeps the first two letters, which also
  covers scripts written without spaces.
- The #674 rules stay: undrawable accents fall back to the plain letter,
  "?" when there is nothing to draw.

test/test_avatar_text.cpp: the four expectations the rule changes, and the
#785 cases.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01He9DHDrX4s3uz37C3QzfdU
2026-11-01 19:59:34 -05:00

108 lines
6.2 KiB
C++

// SPDX-License-Identifier: GPL-3.0-or-later
// Avatar text (#674, #785): the initials and the emoji icon taken from a name.
// Build: g++ -std=c++17 -I src -o /tmp/test_avatar_text test/test_avatar_text.cpp
#include <assert.h>
#include <stdio.h>
#include <string.h>
#include <set>
#include "ui-touch/AvatarText.h"
// A font like the device's: ASCII and Latin-1, no flags or emoji.
static bool latinFont(uint32_t cp, void*) { return cp < 0x100; }
// A narrower one: ASCII only, so an accented letter needs its plain letter.
static bool asciiFont(uint32_t cp, void*) { return cp < 0x80; }
// The firmware's chained faces, which also carry Greek and Cyrillic.
static bool wideFont(uint32_t cp, void*) { return cp < 0x530; }
// The baked emoji set as the firmware has it: the rows that matter here. The joiner
// and the variation selectors are in the table as an empty 2 x 2 image (so text never
// shows them); the visible check is what the UI passes, true for a real picture only.
static const std::set<uint32_t> kTable = {0x200D, 0xFE0E, 0xFE0F, 0x1F1F8, 0x1F600, 0x1F44D};
static const std::set<uint32_t> kInvisible = {0x200D, 0xFE0E, 0xFE0F, 0x1F1F8};
static bool anyRow(uint32_t cp, void*) { return kTable.count(cp) != 0; } // the old check
static bool visibleRow(uint32_t cp, void*) { return kTable.count(cp) && !kInvisible.count(cp); }
static std::string ini(const char* name, AvatarText::HasGlyph has = latinFont) {
char out[12];
AvatarText::initials(name, out, sizeof out, has, nullptr);
return out;
}
// The names from the report: a flag (U+1F1F5 U+1F1F9) and an island with its emoji
// variation selector (U+1F3DD U+FE0F), and a man (U+1F468) after the name.
static const char kTristaoEdc[] = "\xF0\x9F\x87\xB5\xF0\x9F\x87\xB9\xF0\x9F\x8F\x9D\xEF\xB8\x8F Trist\xC3\xA3o EDC";
static const char kTristaoMobile[] = "Trist\xC3\xA3o Mobile \xF0\x9F\x91\xA8";
static const char kTristaoZwj[] = "Trist\xC3\xA3o \xF0\x9F\x91\xA8\xE2\x80\x8D\xF0\x9F\x92\xBB"; // man + ZWJ + laptop
int main() {
char eb[20];
// The bug, reproduced with the old rule (any table row counts): the island's
// variation selector is the "first emoji", an empty image, so the tile is blank.
assert(AvatarText::firstEmoji(kTristaoEdc, eb, sizeof eb, anyRow, nullptr));
assert(!strcmp(eb, "\xEF\xB8\x8F"));
assert(AvatarText::firstEmoji(kTristaoZwj, eb, sizeof eb, anyRow, nullptr));
assert(!strcmp(eb, "\xE2\x80\x8D"));
// Fixed: only a visible picture is an icon; these names keep their dot pattern.
assert(!AvatarText::firstEmoji(kTristaoEdc, eb, sizeof eb, visibleRow, nullptr));
assert(!AvatarText::firstEmoji(kTristaoMobile, eb, sizeof eb, visibleRow, nullptr));
assert(!AvatarText::firstEmoji(kTristaoZwj, eb, sizeof eb, visibleRow, nullptr));
assert(!AvatarText::firstEmoji("\xF0\x9F\x87\xB8\xF0\x9F\x87\xAA Sweden", eb, sizeof eb, visibleRow, nullptr));
// A real emoji still becomes the icon (#640), after a joiner too.
assert(AvatarText::firstEmoji("Base \xEF\xB8\x8F\xF0\x9F\x98\x80 camp", eb, sizeof eb, visibleRow, nullptr));
assert(!strcmp(eb, "\xF0\x9F\x98\x80"));
assert(!AvatarText::firstEmoji("\xC2\xA9 corp", eb, sizeof eb, visibleRow, nullptr));
assert(!AvatarText::firstEmoji("Plain name", eb, sizeof eb, visibleRow, nullptr));
// Initials: whole code points, in capitals, emoji and symbols skipped.
assert(ini("\xC3\xA3na") == "\xC3\x83N"); // "ãna" -> "ÃN"
assert(ini("Jo\xC3\xA3o") == "JO");
assert(ini("Jo""a\xCC\x83o") == "JO"); // a + combining tilde
assert(ini("\xC3\x89mile") == "\xC3\x89M"); // "Émile" -> "ÉM"
assert(ini("Zo\xC3\xAB") == "ZO");
assert(ini(kTristaoEdc) == "TE"); // was the two flag halves (#674)
assert(ini(kTristaoMobile) == "TM"); // the emoji word holds no letter
assert(ini("#hiking") == "HI");
assert(ini(" [base] camp") == "BC");
assert(ini("42 Node") == "4N");
assert(ini("\xD0\xB4\xD0\xB8\xD0\xBC\xD0\xB0", wideFont) == "\xD0\x94\xD0\x98"); // Cyrillic "дима" -> "ДИ"
// #785: the first letter of the first word and of the LAST word, not the first two.
assert(ini("Eddie Smith") == "ES");
assert(ini("john paul jones") == "JJ");
assert(ini("Eddie-Base") == "EB");
assert(ini("kd2abc_mobile") == "KM");
assert(ini("Home/Roof Node") == "HN");
assert(ini(" Eddie Smith ") == "ES"); // runs of spaces, edges
assert(ini("\xC3\xA9mile Zola") == "\xC3\x89Z"); // "émile Zola" -> "ÉZ"
assert(ini("Joa\xCC\x83o Silva") == "JS"); // the combining tilde is no break
assert(ini("Base \xF0\x9F\x91\x8D") == "BA"); // emoji-only word: one word left
assert(ini("\xF0\x9F\x91\x8D Base Camp") == "BC");
assert(ini("[Eddie] (Smith)") == "ES"); // brackets are skipped, not words
assert(ini("Eddie #2") == "E2");
assert(ini("\xD0\x94\xD0\xB8\xD0\xBC\xD0\xB0 \xD0\x9F\xD0\xB5\xD1\x82\xD1\x80\xD0\xBE\xD0\xB2", wideFont)
== "\xD0\x94\xD0\x9F"); // "Дима Петров" -> "ДП"
// A word whose letters the font cannot draw is passed over, like an emoji word.
assert(ini("Eddie \xD0\x94\xD0\xB8\xD0\xBC\xD0\xB0", latinFont) == "ED");
// One word: the first two letters, as before.
assert(ini("Eddie") == "ED");
assert(ini("E") == "E");
// A font without the accented capital: its plain letter.
assert(ini("\xC3\xA3na", asciiFont) == "AN");
// Nothing to draw: "?", never blank.
assert(ini("\xF0\x9F\x87\xB5\xF0\x9F\x87\xB9") == "?");
assert(ini("") == "?");
assert(ini(nullptr) == "?");
assert(ini("\xFF\xFE") == "?"); // malformed bytes
// Two letters only, and never past the buffer.
char small[4];
AvatarText::initials("\xC3\xA3\xC3\xA9", small, sizeof small, latinFont, nullptr);
assert(!strcmp(small, "\xC3\x83")); // the second does not fit
// The uppercase table the section labels use, moved here unchanged.
assert(AvatarText::upper('a') == 'A' && AvatarText::upper(0xE3) == 0xC3 && AvatarText::upper(0xF7) == 0xF7);
assert(AvatarText::upper(0x101) == 0x100 && AvatarText::upper(0x3C2) == 0x3A3 && AvatarText::upper(0x451) == 0x401);
puts("avatar text: emoji icon skips joiners, initials are first + last word, never blank PASS");
}