#include #include #include #include "note_text.h" using namespace roro::notes; void setUp() {} void tearDown() {} static void type(NoteText& t, const std::string& s) { for (char c : s) TEST_ASSERT_TRUE(t.insert(static_cast(c))); } void test_typing_and_rows() { NoteText t(10, 4); TEST_ASSERT_EQUAL_size_t(1, t.rows().size()); // an empty note has one empty row type(t, "Hello\nworld"); auto rows = t.rows(); TEST_ASSERT_EQUAL_size_t(2, rows.size()); TEST_ASSERT_EQUAL_STRING("Hello", rows[0].c_str()); TEST_ASSERT_EQUAL_STRING("world", rows[1].c_str()); TEST_ASSERT_EQUAL_INT(1, t.cursorRow()); TEST_ASSERT_EQUAL_INT(5, t.cursorCol()); t.insert('\n'); TEST_ASSERT_EQUAL_size_t(3, t.rows().size()); // the empty line the cursor is on TEST_ASSERT_EQUAL_INT(2, t.cursorRow()); TEST_ASSERT_EQUAL_INT(0, t.cursorCol()); t.backspace(); t.backspace(); TEST_ASSERT_EQUAL_STRING("Hello\nworl", t.text().c_str()); TEST_ASSERT_EQUAL_STRING("Hello", t.firstLine().c_str()); } void test_wrapping_at_spaces() { NoteText t(10, 6); type(t, "one two three four five"); auto rows = t.rows(); TEST_ASSERT_EQUAL_size_t(3, rows.size()); TEST_ASSERT_EQUAL_STRING("one two ", rows[0].c_str()); // a line owns the space it ends with TEST_ASSERT_EQUAL_STRING("three four ", rows[1].c_str()); // ten characters and that space TEST_ASSERT_EQUAL_STRING("five", rows[2].c_str()); TEST_ASSERT_EQUAL_INT(2, t.cursorRow()); TEST_ASSERT_EQUAL_INT(4, t.cursorCol()); // Every byte is on exactly one row. std::string all; for (auto& r : rows) all += r; TEST_ASSERT_EQUAL_STRING(t.text().c_str(), all.c_str()); } void test_a_long_word_is_cut() { NoteText t(5, 6); type(t, "abcdefghijkl"); auto rows = t.rows(); TEST_ASSERT_EQUAL_size_t(3, rows.size()); TEST_ASSERT_EQUAL_STRING("abcde", rows[0].c_str()); TEST_ASSERT_EQUAL_STRING("fghij", rows[1].c_str()); TEST_ASSERT_EQUAL_STRING("kl", rows[2].c_str()); } void test_left_right_over_accents() { NoteText t(10, 4); t.insert(0xE9); // é t.insert('t'); t.insert(0xE9); TEST_ASSERT_EQUAL_STRING("\xC3\xA9t\xC3\xA9", t.text().c_str()); TEST_ASSERT_EQUAL_INT(3, t.cursorCol()); t.left(); TEST_ASSERT_EQUAL_size_t(3, t.cursor()); t.left(); t.left(); t.left(); // already at the start TEST_ASSERT_EQUAL_size_t(0, t.cursor()); t.right(); TEST_ASSERT_EQUAL_size_t(2, t.cursor()); t.backspace(); TEST_ASSERT_EQUAL_STRING("t\xC3\xA9", t.text().c_str()); t.insert(0x20AC); // €, three bytes TEST_ASSERT_EQUAL_size_t(3, t.cursor()); t.backspace(); TEST_ASSERT_EQUAL_STRING("t\xC3\xA9", t.text().c_str()); } void test_up_and_down_keep_their_column() { NoteText t(20, 6); TEST_ASSERT_TRUE(t.setText("a long first line\nab\n\nanother long line")); t.lineEnd(); TEST_ASSERT_EQUAL_INT(17, t.cursorCol()); t.down(); // a short line: as far right as it goes TEST_ASSERT_EQUAL_INT(1, t.cursorRow()); TEST_ASSERT_EQUAL_INT(2, t.cursorCol()); t.down(); // an empty one TEST_ASSERT_EQUAL_INT(0, t.cursorCol()); t.down(); // and back out to the column it came from TEST_ASSERT_EQUAL_INT(3, t.cursorRow()); TEST_ASSERT_EQUAL_INT(17, t.cursorCol()); t.down(); // the last line: stays TEST_ASSERT_EQUAL_INT(3, t.cursorRow()); t.up(); t.up(); t.up(); TEST_ASSERT_EQUAL_INT(0, t.cursorRow()); TEST_ASSERT_EQUAL_INT(17, t.cursorCol()); t.up(); // the first line: stays TEST_ASSERT_EQUAL_INT(0, t.cursorRow()); t.left(); // moving sideways forgets the column t.down(); TEST_ASSERT_EQUAL_INT(2, t.cursorCol()); t.down(); t.down(); TEST_ASSERT_EQUAL_INT(16, t.cursorCol()); } void test_up_and_down_through_wrapped_lines() { NoteText t(10, 6); TEST_ASSERT_TRUE(t.setText("one two three four five\nend")); // "one two " / "three four " / "five" / "end" t.toEnd(); TEST_ASSERT_EQUAL_INT(3, t.cursorRow()); t.up(); TEST_ASSERT_EQUAL_INT(2, t.cursorRow()); TEST_ASSERT_EQUAL_INT(3, t.cursorCol()); t.up(); TEST_ASSERT_EQUAL_INT(1, t.cursorRow()); t.lineEnd(); // on the space the line ends with, not past it TEST_ASSERT_EQUAL_INT(1, t.cursorRow()); TEST_ASSERT_EQUAL_INT(10, t.cursorCol()); t.right(); // one more is the start of the next line TEST_ASSERT_EQUAL_INT(2, t.cursorRow()); TEST_ASSERT_EQUAL_INT(0, t.cursorCol()); t.lineStart(); t.left(); TEST_ASSERT_EQUAL_INT(1, t.cursorRow()); t.lineStart(); TEST_ASSERT_EQUAL_INT(0, t.cursorCol()); TEST_ASSERT_EQUAL_size_t(8, t.cursor()); } void test_a_final_newline_gives_a_last_empty_line() { NoteText t(10, 6); TEST_ASSERT_TRUE(t.setText("ab\n")); TEST_ASSERT_EQUAL_size_t(2, t.rows().size()); t.down(); TEST_ASSERT_EQUAL_size_t(3, t.cursor()); TEST_ASSERT_EQUAL_INT(1, t.cursorRow()); t.down(); TEST_ASSERT_EQUAL_size_t(3, t.cursor()); t.up(); TEST_ASSERT_EQUAL_size_t(0, t.cursor()); t.lineEnd(); TEST_ASSERT_EQUAL_size_t(2, t.cursor()); } void test_the_screen_follows_the_cursor() { NoteText t(10, 3); std::string text; for (int i = 0; i < 20; i++) text += "line " + std::to_string(i) + "\n"; TEST_ASSERT_TRUE(t.setText(text)); TEST_ASSERT_EQUAL_STRING("line 0", t.rows()[0].c_str()); for (int i = 0; i < 5; i++) t.down(); auto rows = t.rows(); TEST_ASSERT_EQUAL_STRING("line 3", rows[0].c_str()); // scrolled a line at a time TEST_ASSERT_EQUAL_STRING("line 5", rows[2].c_str()); TEST_ASSERT_EQUAL_INT(2, t.cursorRow()); t.up(); t.up(); t.up(); TEST_ASSERT_EQUAL_STRING("line 2", t.rows()[0].c_str()); TEST_ASSERT_EQUAL_INT(0, t.cursorRow()); t.toEnd(); rows = t.rows(); TEST_ASSERT_EQUAL_size_t(3, rows.size()); TEST_ASSERT_EQUAL_STRING("line 18", rows[0].c_str()); TEST_ASSERT_EQUAL_STRING("", rows[2].c_str()); TEST_ASSERT_EQUAL_INT(2, t.cursorRow()); t.toStart(); TEST_ASSERT_EQUAL_STRING("line 0", t.rows()[0].c_str()); t.pageDown(); TEST_ASSERT_EQUAL_size_t(14, t.cursor()); // two lines down t.pageUp(); TEST_ASSERT_EQUAL_size_t(0, t.cursor()); TEST_ASSERT_TRUE(t.percent() == 0); } // Deleting above the screen must not leave the screen starting in the middle of a line. void test_edits_keep_the_screen_on_line_starts() { NoteText t(10, 3); TEST_ASSERT_TRUE(t.setText("aaaa bbbb cccc dddd eeee ffff gggg hhhh iiii jjjj")); t.toEnd(); t.rows(); for (int i = 0; i < 30; i++) { t.backspace(); auto rows = t.rows(); TEST_ASSERT_TRUE(rows.size() <= 3); int row = t.cursorRow(); TEST_ASSERT_TRUE(row >= 0 && row < static_cast(rows.size())); std::string all; for (auto& r : rows) all += r; TEST_ASSERT_TRUE(t.text().size() >= all.size()); TEST_ASSERT_EQUAL_STRING(t.text().substr(t.text().size() - all.size()).c_str(), all.c_str()); } } void test_full_and_too_big() { NoteText t(38, 8); TEST_ASSERT_FALSE(t.setText(std::string(NoteText::kMaxBytes + 1, 'x'))); TEST_ASSERT_TRUE(t.setText(std::string(NoteText::kMaxBytes - 1, 'x'))); uint32_t before = t.revision(); t.toEnd(); TEST_ASSERT_FALSE(t.insert(0xE9)); // two bytes: one too many TEST_ASSERT_FALSE(t.insertText(" ")); TEST_ASSERT_EQUAL_UINT32(before, t.revision()); TEST_ASSERT_TRUE(t.insert('y')); TEST_ASSERT_FALSE(t.insert('z')); TEST_ASSERT_TRUE(t.revision() != before); // One paragraph of 16 KB: moving through it still works. t.up(); t.up(); t.lineStart(); TEST_ASSERT_EQUAL_size_t((NoteText::kMaxBytes / 38 - 2) * 38, t.cursor()); // two rows of 38 above the last } void test_crlf_becomes_lf() { NoteText t(38, 8); TEST_ASSERT_TRUE(t.setText("a\r\nb\r\n\rc")); TEST_ASSERT_EQUAL_STRING("a\nb\n\rc", t.text().c_str()); } // The device hands over the string it read the file into: no second copy, and never a bigger // buffer afterwards, whatever is typed. void test_adopting_a_buffer_never_reallocates() { std::string body; body.reserve(NoteText::kMaxBytes); body = "one\r\ntwo\r\n"; const char* buffer = body.data(); NoteText t(38, 8, std::move(body)); TEST_ASSERT_EQUAL_STRING("one\ntwo\n", t.text().c_str()); TEST_ASSERT_EQUAL_PTR(buffer, t.text().data()); t.toEnd(); while (t.insert('x')) {} TEST_ASSERT_EQUAL_size_t(NoteText::kMaxBytes, t.text().size()); TEST_ASSERT_EQUAL_PTR(buffer, t.text().data()); TEST_ASSERT_TRUE(t.setText("replaced")); TEST_ASSERT_EQUAL_PTR(buffer, t.text().data()); NoteText fresh(38, 8); const char* own = fresh.text().data(); while (fresh.insert('y')) {} TEST_ASSERT_EQUAL_PTR(own, fresh.text().data()); } void test_names() { TEST_ASSERT_EQUAL_STRING("shopping-list", nameFromFirstLine("Shopping list!", "x").c_str()); TEST_ASSERT_EQUAL_STRING("ete-a-la-plage-2026", nameFromFirstLine(" \xC3\x89t\xC3\xA9 \xC3\xA0 la plage (2026)", "x").c_str()); TEST_ASSERT_EQUAL_STRING("note-20261006-0919", nameFromFirstLine("", "20261006-0919").c_str()); TEST_ASSERT_EQUAL_STRING("note-20261006-0919", nameFromFirstLine("\xE2\x82\xAC ?!", "20261006-0919").c_str()); TEST_ASSERT_TRUE(nameFromFirstLine(std::string(100, 'a'), "x").size() <= 33); TEST_ASSERT_EQUAL_STRING("strase", nameFromFirstLine("Stra\xC3\x9F" "e", "x").c_str()); } void test_titles() { TEST_ASSERT_EQUAL_STRING("Shopping list", titleFrom("\n \r\n Shopping list \r\nmilk\n", 24).c_str()); TEST_ASSERT_EQUAL_STRING("abcde", titleFrom("abcdefgh", 5).c_str()); TEST_ASSERT_EQUAL_STRING("\xC3\xA9t", titleFrom("\xC3\xA9t\xC3", 24).c_str()); // a character cut by the end of what was read TEST_ASSERT_EQUAL_STRING("\xC3\xA9", titleFrom("\xC3\xA9t\xC3\xA9", 1).c_str()); TEST_ASSERT_EQUAL_STRING("", titleFrom(" \n\n", 24).c_str()); TEST_ASSERT_EQUAL_STRING("", titleFrom("", 24).c_str()); } int main() { UNITY_BEGIN(); RUN_TEST(test_typing_and_rows); RUN_TEST(test_wrapping_at_spaces); RUN_TEST(test_a_long_word_is_cut); RUN_TEST(test_left_right_over_accents); RUN_TEST(test_up_and_down_keep_their_column); RUN_TEST(test_up_and_down_through_wrapped_lines); RUN_TEST(test_a_final_newline_gives_a_last_empty_line); RUN_TEST(test_the_screen_follows_the_cursor); RUN_TEST(test_edits_keep_the_screen_on_line_starts); RUN_TEST(test_full_and_too_big); RUN_TEST(test_crlf_becomes_lf); RUN_TEST(test_adopting_a_buffer_never_reallocates); RUN_TEST(test_names); RUN_TEST(test_titles); return UNITY_END(); }