Public Access
Add widget kit, input layer with Compose Key, Launcher and demo apps
Steps 5 and 6 of M0. - lib/ui (host-tested): ListModel, LineEditor (UTF-8, byte-limited), wrapText, ToastQueue, DialogModel - lib/input (host-tested): KeyMapper turns keyboard state into logical keys (Fn arrows, ` Back, Fn+` Home) and composes accents with opt - src/ui: off-screen 16-bit frame pushed in one transfer (fixes the flicker), Status Bar, list, text, line editor, dialog, Toast widgets, Notifier (Toast + beep + LED), vendored public-domain Latin-1 fonts - Apps: minimal Launcher, Widget demo, Diagnostics (SD erase now behind a confirmation dialog) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EhqxQ49eCju4CzKYNjZzwT
This commit is contained in:
@@ -18,7 +18,9 @@ struct FakeApp : App {
|
||||
keys.push_back(e);
|
||||
return handles;
|
||||
}
|
||||
void update(uint32_t nowMs) override { lastUpdate = nowMs; }
|
||||
void draw(Canvas&) override {}
|
||||
uint32_t lastUpdate = 0;
|
||||
std::vector<KeyEvent> keys;
|
||||
bool handles = false;
|
||||
|
||||
@@ -145,6 +147,14 @@ void test_app_can_request_its_own_redraw() {
|
||||
TEST_ASSERT_FALSE(f.manager.takeRedraw());
|
||||
}
|
||||
|
||||
void test_update_reaches_only_the_foreground_app() {
|
||||
Fixture f;
|
||||
f.manager.open("notes");
|
||||
f.manager.update(42);
|
||||
TEST_ASSERT_EQUAL(42, f.notes.lastUpdate);
|
||||
TEST_ASSERT_EQUAL(0, f.launcher.lastUpdate);
|
||||
}
|
||||
|
||||
int main() {
|
||||
UNITY_BEGIN();
|
||||
RUN_TEST(test_launcher_is_in_foreground_after_begin);
|
||||
@@ -159,5 +169,6 @@ int main() {
|
||||
RUN_TEST(test_visible_apps_exclude_hidden_ones_and_keep_registration_order);
|
||||
RUN_TEST(test_switching_apps_requests_a_redraw);
|
||||
RUN_TEST(test_app_can_request_its_own_redraw);
|
||||
RUN_TEST(test_update_reaches_only_the_foreground_app);
|
||||
return UNITY_END();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
#include <unity.h>
|
||||
|
||||
#include "dialog_model.h"
|
||||
|
||||
using namespace roro;
|
||||
|
||||
void setUp() {}
|
||||
void tearDown() {}
|
||||
|
||||
void test_starts_on_the_safe_first_button_and_undecided() {
|
||||
DialogModel d({"Cancel", "Format"});
|
||||
TEST_ASSERT_EQUAL(0, d.selected());
|
||||
TEST_ASSERT_EQUAL(DialogModel::kPending, d.result());
|
||||
}
|
||||
|
||||
void test_left_right_and_tab_move_between_buttons() {
|
||||
DialogModel d({"Cancel", "Format"});
|
||||
d.onKey(KeyEvent::of(Key::Right));
|
||||
TEST_ASSERT_EQUAL(1, d.selected());
|
||||
d.onKey(KeyEvent::of(Key::Right));
|
||||
TEST_ASSERT_EQUAL(1, d.selected());
|
||||
d.onKey(KeyEvent::of(Key::Tab));
|
||||
TEST_ASSERT_EQUAL(0, d.selected());
|
||||
d.onKey(KeyEvent::of(Key::Left));
|
||||
TEST_ASSERT_EQUAL(0, d.selected());
|
||||
}
|
||||
|
||||
void test_select_confirms_the_highlighted_button() {
|
||||
DialogModel d({"Cancel", "Format"});
|
||||
d.onKey(KeyEvent::of(Key::Right));
|
||||
d.onKey(KeyEvent::of(Key::Select));
|
||||
TEST_ASSERT_EQUAL(1, d.result());
|
||||
}
|
||||
|
||||
void test_back_cancels() {
|
||||
DialogModel d({"Cancel", "Format"});
|
||||
d.onKey(KeyEvent::of(Key::Right));
|
||||
d.onKey(KeyEvent::of(Key::Back));
|
||||
TEST_ASSERT_EQUAL(DialogModel::kCancelled, d.result());
|
||||
}
|
||||
|
||||
void test_dialog_consumes_every_key_while_open() {
|
||||
DialogModel d({"OK"});
|
||||
TEST_ASSERT_TRUE(d.onKey(KeyEvent::character('x')));
|
||||
TEST_ASSERT_TRUE(d.onKey(KeyEvent::of(Key::Back)));
|
||||
}
|
||||
|
||||
int main() {
|
||||
UNITY_BEGIN();
|
||||
RUN_TEST(test_starts_on_the_safe_first_button_and_undecided);
|
||||
RUN_TEST(test_left_right_and_tab_move_between_buttons);
|
||||
RUN_TEST(test_select_confirms_the_highlighted_button);
|
||||
RUN_TEST(test_back_cancels);
|
||||
RUN_TEST(test_dialog_consumes_every_key_while_open);
|
||||
return UNITY_END();
|
||||
}
|
||||
@@ -0,0 +1,195 @@
|
||||
#include <unity.h>
|
||||
|
||||
#include <vector>
|
||||
|
||||
#include "key_mapper.h"
|
||||
|
||||
using namespace roro;
|
||||
|
||||
void setUp() {}
|
||||
void tearDown() {}
|
||||
|
||||
static RawKeys chars(std::vector<char> c) {
|
||||
RawKeys r;
|
||||
r.chars = c;
|
||||
return r;
|
||||
}
|
||||
|
||||
static RawKeys withFn(std::vector<char> c) {
|
||||
RawKeys r = chars(c);
|
||||
r.fn = true;
|
||||
return r;
|
||||
}
|
||||
|
||||
static RawKeys optOnly() {
|
||||
RawKeys r;
|
||||
r.opt = true;
|
||||
return r;
|
||||
}
|
||||
|
||||
static std::vector<KeyEvent> press(KeyMapper& m, const RawKeys& r) { return m.update(r); }
|
||||
|
||||
static void release(KeyMapper& m) { m.update(RawKeys{}); }
|
||||
|
||||
void test_plain_character() {
|
||||
KeyMapper m;
|
||||
auto ev = press(m, chars({'a'}));
|
||||
TEST_ASSERT_EQUAL(1, ev.size());
|
||||
TEST_ASSERT_EQUAL(static_cast<int>(Key::Char), static_cast<int>(ev[0].key));
|
||||
TEST_ASSERT_EQUAL('a', ev[0].ch);
|
||||
}
|
||||
|
||||
void test_held_key_is_not_repeated_on_later_changes() {
|
||||
KeyMapper m;
|
||||
press(m, chars({'a'}));
|
||||
auto ev = press(m, chars({'a', 'b'})); // rolling: b pressed while a still held
|
||||
TEST_ASSERT_EQUAL(1, ev.size());
|
||||
TEST_ASSERT_EQUAL('b', ev[0].ch);
|
||||
}
|
||||
|
||||
void test_same_key_again_after_release() {
|
||||
KeyMapper m;
|
||||
press(m, chars({'a'}));
|
||||
release(m);
|
||||
TEST_ASSERT_EQUAL(1, press(m, chars({'a'})).size());
|
||||
}
|
||||
|
||||
void test_fn_combos_are_arrows() {
|
||||
KeyMapper m;
|
||||
TEST_ASSERT_EQUAL(static_cast<int>(Key::Up), static_cast<int>(press(m, withFn({';'}))[0].key));
|
||||
release(m);
|
||||
TEST_ASSERT_EQUAL(static_cast<int>(Key::Down), static_cast<int>(press(m, withFn({'.'}))[0].key));
|
||||
release(m);
|
||||
TEST_ASSERT_EQUAL(static_cast<int>(Key::Left), static_cast<int>(press(m, withFn({','}))[0].key));
|
||||
release(m);
|
||||
TEST_ASSERT_EQUAL(static_cast<int>(Key::Right), static_cast<int>(press(m, withFn({'/'}))[0].key));
|
||||
}
|
||||
|
||||
void test_backtick_is_back_and_fn_backtick_is_home() {
|
||||
KeyMapper m;
|
||||
TEST_ASSERT_EQUAL(static_cast<int>(Key::Back), static_cast<int>(press(m, chars({'`'}))[0].key));
|
||||
release(m);
|
||||
TEST_ASSERT_EQUAL(static_cast<int>(Key::Home), static_cast<int>(press(m, withFn({'`'}))[0].key));
|
||||
}
|
||||
|
||||
void test_enter_delete_tab() {
|
||||
KeyMapper m;
|
||||
RawKeys r;
|
||||
r.enter = true;
|
||||
TEST_ASSERT_EQUAL(static_cast<int>(Key::Select), static_cast<int>(press(m, r)[0].key));
|
||||
release(m);
|
||||
r = RawKeys{};
|
||||
r.del = true;
|
||||
TEST_ASSERT_EQUAL(static_cast<int>(Key::Delete), static_cast<int>(press(m, r)[0].key));
|
||||
release(m);
|
||||
r = RawKeys{};
|
||||
r.tab = true;
|
||||
TEST_ASSERT_EQUAL(static_cast<int>(Key::Tab), static_cast<int>(press(m, r)[0].key));
|
||||
}
|
||||
|
||||
void test_ctrl_and_alt_flags_are_carried() {
|
||||
KeyMapper m;
|
||||
RawKeys r = chars({'c'});
|
||||
r.ctrl = true;
|
||||
auto ev = press(m, r);
|
||||
TEST_ASSERT_TRUE(ev[0].ctrl);
|
||||
TEST_ASSERT_EQUAL('c', ev[0].ch);
|
||||
}
|
||||
|
||||
void test_compose_acute() {
|
||||
KeyMapper m;
|
||||
TEST_ASSERT_EQUAL(0, press(m, optOnly()).size());
|
||||
release(m);
|
||||
TEST_ASSERT_EQUAL(0, press(m, chars({'\''})).size());
|
||||
release(m);
|
||||
auto ev = press(m, chars({'e'}));
|
||||
TEST_ASSERT_EQUAL(1, ev.size());
|
||||
TEST_ASSERT_EQUAL(0xE9, ev[0].ch); // é
|
||||
}
|
||||
|
||||
void test_compose_while_holding_opt() {
|
||||
KeyMapper m;
|
||||
RawKeys r = chars({'`'});
|
||||
r.opt = true;
|
||||
TEST_ASSERT_EQUAL(0, press(m, r).size()); // grave accent, not Back
|
||||
release(m);
|
||||
auto ev = press(m, chars({'a'}));
|
||||
TEST_ASSERT_EQUAL(0xE0, ev[0].ch); // à
|
||||
}
|
||||
|
||||
void test_compose_table() {
|
||||
struct Case {
|
||||
char accent, base;
|
||||
uint32_t expected;
|
||||
} cases[] = {{'^', 'e', 0xEA}, {'"', 'e', 0xEB}, {',', 'c', 0xE7}, {',', 'C', 0xC7},
|
||||
{'~', 'n', 0xF1}, {'\'', 'E', 0xC9}, {'`', 'u', 0xF9}, {'"', 'i', 0xEF}};
|
||||
for (auto& c : cases) {
|
||||
KeyMapper m;
|
||||
press(m, optOnly());
|
||||
release(m);
|
||||
press(m, chars({c.accent}));
|
||||
release(m);
|
||||
auto ev = press(m, chars({c.base}));
|
||||
TEST_ASSERT_EQUAL_HEX32(c.expected, ev[0].ch);
|
||||
}
|
||||
}
|
||||
|
||||
void test_unknown_combination_types_both_characters() {
|
||||
KeyMapper m;
|
||||
press(m, optOnly());
|
||||
release(m);
|
||||
press(m, chars({'\''}));
|
||||
release(m);
|
||||
auto ev = press(m, chars({'z'}));
|
||||
TEST_ASSERT_EQUAL(2, ev.size());
|
||||
TEST_ASSERT_EQUAL('\'', ev[0].ch);
|
||||
TEST_ASSERT_EQUAL('z', ev[1].ch);
|
||||
}
|
||||
|
||||
void test_opt_followed_by_a_non_accent_types_it_normally() {
|
||||
KeyMapper m;
|
||||
press(m, optOnly());
|
||||
release(m);
|
||||
auto ev = press(m, chars({'x'}));
|
||||
TEST_ASSERT_EQUAL(1, ev.size());
|
||||
TEST_ASSERT_EQUAL('x', ev[0].ch);
|
||||
}
|
||||
|
||||
void test_pressing_opt_twice_cancels() {
|
||||
KeyMapper m;
|
||||
press(m, optOnly());
|
||||
release(m);
|
||||
press(m, optOnly());
|
||||
release(m);
|
||||
auto ev = press(m, chars({'`'}));
|
||||
TEST_ASSERT_EQUAL(static_cast<int>(Key::Back), static_cast<int>(ev[0].key));
|
||||
}
|
||||
|
||||
void test_compose_state_is_visible_for_the_ui() {
|
||||
KeyMapper m;
|
||||
TEST_ASSERT_EQUAL(0, m.pendingCompose());
|
||||
press(m, optOnly());
|
||||
TEST_ASSERT_EQUAL('*', m.pendingCompose()); // armed, no accent yet
|
||||
release(m);
|
||||
press(m, chars({'^'}));
|
||||
TEST_ASSERT_EQUAL('^', m.pendingCompose());
|
||||
}
|
||||
|
||||
int main() {
|
||||
UNITY_BEGIN();
|
||||
RUN_TEST(test_plain_character);
|
||||
RUN_TEST(test_held_key_is_not_repeated_on_later_changes);
|
||||
RUN_TEST(test_same_key_again_after_release);
|
||||
RUN_TEST(test_fn_combos_are_arrows);
|
||||
RUN_TEST(test_backtick_is_back_and_fn_backtick_is_home);
|
||||
RUN_TEST(test_enter_delete_tab);
|
||||
RUN_TEST(test_ctrl_and_alt_flags_are_carried);
|
||||
RUN_TEST(test_compose_acute);
|
||||
RUN_TEST(test_compose_while_holding_opt);
|
||||
RUN_TEST(test_compose_table);
|
||||
RUN_TEST(test_unknown_combination_types_both_characters);
|
||||
RUN_TEST(test_opt_followed_by_a_non_accent_types_it_normally);
|
||||
RUN_TEST(test_pressing_opt_twice_cancels);
|
||||
RUN_TEST(test_compose_state_is_visible_for_the_ui);
|
||||
return UNITY_END();
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
#include <unity.h>
|
||||
|
||||
#include "line_editor.h"
|
||||
|
||||
using namespace roro;
|
||||
|
||||
void setUp() {}
|
||||
void tearDown() {}
|
||||
|
||||
void test_typing_appends_at_cursor() {
|
||||
LineEditor e(32);
|
||||
e.insert('h');
|
||||
e.insert('i');
|
||||
TEST_ASSERT_EQUAL_STRING("hi", e.text().c_str());
|
||||
TEST_ASSERT_EQUAL(2, e.cursor());
|
||||
}
|
||||
|
||||
void test_accented_characters_are_stored_as_utf8() {
|
||||
LineEditor e(32);
|
||||
e.insert(0xE9); // é
|
||||
TEST_ASSERT_EQUAL_STRING("\xC3\xA9", e.text().c_str());
|
||||
TEST_ASSERT_EQUAL(2, e.cursor());
|
||||
}
|
||||
|
||||
void test_cursor_moves_over_whole_characters() {
|
||||
LineEditor e(32);
|
||||
e.setText("a\xC3\xA9z"); // aéz
|
||||
TEST_ASSERT_EQUAL(4, e.cursor());
|
||||
e.left();
|
||||
TEST_ASSERT_EQUAL(3, e.cursor());
|
||||
e.left();
|
||||
TEST_ASSERT_EQUAL(1, e.cursor()); // skipped both bytes of é
|
||||
e.right();
|
||||
TEST_ASSERT_EQUAL(3, e.cursor());
|
||||
}
|
||||
|
||||
void test_cursor_stops_at_both_ends() {
|
||||
LineEditor e(32);
|
||||
e.setText("ab");
|
||||
e.right();
|
||||
TEST_ASSERT_EQUAL(2, e.cursor());
|
||||
e.home();
|
||||
e.left();
|
||||
TEST_ASSERT_EQUAL(0, e.cursor());
|
||||
e.end();
|
||||
TEST_ASSERT_EQUAL(2, e.cursor());
|
||||
}
|
||||
|
||||
void test_insert_in_the_middle() {
|
||||
LineEditor e(32);
|
||||
e.setText("ac");
|
||||
e.left();
|
||||
e.insert('b');
|
||||
TEST_ASSERT_EQUAL_STRING("abc", e.text().c_str());
|
||||
TEST_ASSERT_EQUAL(2, e.cursor());
|
||||
}
|
||||
|
||||
void test_backspace_removes_the_whole_character_before_the_cursor() {
|
||||
LineEditor e(32);
|
||||
e.setText("a\xC3\xA9");
|
||||
e.backspace();
|
||||
TEST_ASSERT_EQUAL_STRING("a", e.text().c_str());
|
||||
e.home();
|
||||
e.backspace();
|
||||
TEST_ASSERT_EQUAL_STRING("a", e.text().c_str());
|
||||
}
|
||||
|
||||
void test_insert_rejected_when_it_would_exceed_max_bytes() {
|
||||
LineEditor e(3);
|
||||
e.setText("ab");
|
||||
TEST_ASSERT_FALSE(e.insert(0xE9)); // 2 bytes, only 1 left
|
||||
TEST_ASSERT_TRUE(e.insert('c'));
|
||||
TEST_ASSERT_FALSE(e.insert('d'));
|
||||
TEST_ASSERT_EQUAL_STRING("abc", e.text().c_str());
|
||||
}
|
||||
|
||||
void test_set_text_truncates_on_a_character_boundary() {
|
||||
LineEditor e(2);
|
||||
e.setText("a\xC3\xA9");
|
||||
TEST_ASSERT_EQUAL_STRING("a", e.text().c_str());
|
||||
}
|
||||
|
||||
void test_three_and_four_byte_characters() {
|
||||
LineEditor e(32);
|
||||
e.insert(0x20AC); // €
|
||||
e.insert(0x1F600); // 😀
|
||||
TEST_ASSERT_EQUAL(7, e.text().size());
|
||||
e.backspace();
|
||||
TEST_ASSERT_EQUAL_STRING("\xE2\x82\xAC", e.text().c_str());
|
||||
}
|
||||
|
||||
int main() {
|
||||
UNITY_BEGIN();
|
||||
RUN_TEST(test_typing_appends_at_cursor);
|
||||
RUN_TEST(test_accented_characters_are_stored_as_utf8);
|
||||
RUN_TEST(test_cursor_moves_over_whole_characters);
|
||||
RUN_TEST(test_cursor_stops_at_both_ends);
|
||||
RUN_TEST(test_insert_in_the_middle);
|
||||
RUN_TEST(test_backspace_removes_the_whole_character_before_the_cursor);
|
||||
RUN_TEST(test_insert_rejected_when_it_would_exceed_max_bytes);
|
||||
RUN_TEST(test_set_text_truncates_on_a_character_boundary);
|
||||
RUN_TEST(test_three_and_four_byte_characters);
|
||||
return UNITY_END();
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
#include <unity.h>
|
||||
|
||||
#include "list_model.h"
|
||||
|
||||
using namespace roro;
|
||||
|
||||
void setUp() {}
|
||||
void tearDown() {}
|
||||
|
||||
void test_empty_list_has_no_selection() {
|
||||
ListModel l(5);
|
||||
TEST_ASSERT_EQUAL(-1, l.selected());
|
||||
l.down();
|
||||
TEST_ASSERT_EQUAL(-1, l.selected());
|
||||
}
|
||||
|
||||
void test_selection_starts_at_first_item() {
|
||||
ListModel l(5);
|
||||
l.setCount(3);
|
||||
TEST_ASSERT_EQUAL(0, l.selected());
|
||||
}
|
||||
|
||||
void test_down_and_up_move_the_selection() {
|
||||
ListModel l(5);
|
||||
l.setCount(3);
|
||||
l.down();
|
||||
TEST_ASSERT_EQUAL(1, l.selected());
|
||||
l.up();
|
||||
TEST_ASSERT_EQUAL(0, l.selected());
|
||||
}
|
||||
|
||||
void test_selection_wraps_around_at_both_ends() {
|
||||
ListModel l(5);
|
||||
l.setCount(3);
|
||||
l.up();
|
||||
TEST_ASSERT_EQUAL(2, l.selected());
|
||||
l.down();
|
||||
TEST_ASSERT_EQUAL(0, l.selected());
|
||||
}
|
||||
|
||||
void test_scrolls_to_keep_the_selection_visible() {
|
||||
ListModel l(3);
|
||||
l.setCount(10);
|
||||
for (int i = 0; i < 4; i++) l.down(); // selected 4
|
||||
TEST_ASSERT_EQUAL(4, l.selected());
|
||||
TEST_ASSERT_EQUAL(2, l.firstVisible()); // rows 2,3,4
|
||||
for (int i = 0; i < 3; i++) l.up(); // selected 1
|
||||
TEST_ASSERT_EQUAL(1, l.firstVisible());
|
||||
}
|
||||
|
||||
void test_wrapping_to_the_end_scrolls_to_the_last_page() {
|
||||
ListModel l(3);
|
||||
l.setCount(10);
|
||||
l.up();
|
||||
TEST_ASSERT_EQUAL(9, l.selected());
|
||||
TEST_ASSERT_EQUAL(7, l.firstVisible());
|
||||
}
|
||||
|
||||
void test_shrinking_the_list_keeps_selection_in_range() {
|
||||
ListModel l(3);
|
||||
l.setCount(10);
|
||||
l.up(); // 9
|
||||
l.setCount(4);
|
||||
TEST_ASSERT_EQUAL(3, l.selected());
|
||||
TEST_ASSERT_EQUAL(1, l.firstVisible());
|
||||
l.setCount(0);
|
||||
TEST_ASSERT_EQUAL(-1, l.selected());
|
||||
}
|
||||
|
||||
void test_page_moves_by_visible_rows_without_wrapping() {
|
||||
ListModel l(3);
|
||||
l.setCount(10);
|
||||
l.pageDown();
|
||||
TEST_ASSERT_EQUAL(3, l.selected());
|
||||
l.pageDown();
|
||||
l.pageDown();
|
||||
l.pageDown();
|
||||
TEST_ASSERT_EQUAL(9, l.selected());
|
||||
l.pageUp();
|
||||
TEST_ASSERT_EQUAL(6, l.selected());
|
||||
}
|
||||
|
||||
int main() {
|
||||
UNITY_BEGIN();
|
||||
RUN_TEST(test_empty_list_has_no_selection);
|
||||
RUN_TEST(test_selection_starts_at_first_item);
|
||||
RUN_TEST(test_down_and_up_move_the_selection);
|
||||
RUN_TEST(test_selection_wraps_around_at_both_ends);
|
||||
RUN_TEST(test_scrolls_to_keep_the_selection_visible);
|
||||
RUN_TEST(test_wrapping_to_the_end_scrolls_to_the_last_page);
|
||||
RUN_TEST(test_shrinking_the_list_keeps_selection_in_range);
|
||||
RUN_TEST(test_page_moves_by_visible_rows_without_wrapping);
|
||||
return UNITY_END();
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
#include <unity.h>
|
||||
|
||||
#include "text_wrap.h"
|
||||
|
||||
using namespace roro;
|
||||
|
||||
void setUp() {}
|
||||
void tearDown() {}
|
||||
|
||||
// Every character (code point) is 6 px wide.
|
||||
static int measure(const std::string& s) {
|
||||
int n = 0;
|
||||
for (unsigned char c : s)
|
||||
if ((c & 0xC0) != 0x80) n++;
|
||||
return n * 6;
|
||||
}
|
||||
|
||||
void test_short_text_is_one_line() {
|
||||
auto lines = wrapText("hello", 60, measure);
|
||||
TEST_ASSERT_EQUAL(1, lines.size());
|
||||
TEST_ASSERT_EQUAL_STRING("hello", lines[0].c_str());
|
||||
}
|
||||
|
||||
void test_wraps_at_word_boundaries() {
|
||||
auto lines = wrapText("hello big world", 60, measure); // 10 chars per line
|
||||
TEST_ASSERT_EQUAL(2, lines.size());
|
||||
TEST_ASSERT_EQUAL_STRING("hello big", lines[0].c_str());
|
||||
TEST_ASSERT_EQUAL_STRING("world", lines[1].c_str());
|
||||
}
|
||||
|
||||
void test_breaks_words_longer_than_a_line() {
|
||||
auto lines = wrapText("abcdefghijklmnop", 60, measure);
|
||||
TEST_ASSERT_EQUAL(2, lines.size());
|
||||
TEST_ASSERT_EQUAL_STRING("abcdefghij", lines[0].c_str());
|
||||
TEST_ASSERT_EQUAL_STRING("klmnop", lines[1].c_str());
|
||||
}
|
||||
|
||||
void test_never_splits_a_multibyte_character() {
|
||||
// 12 x 'é' (2 bytes each) at 10 chars per line
|
||||
std::string s;
|
||||
for (int i = 0; i < 12; i++) s += "\xC3\xA9";
|
||||
auto lines = wrapText(s, 60, measure);
|
||||
TEST_ASSERT_EQUAL(2, lines.size());
|
||||
TEST_ASSERT_EQUAL(20, lines[0].size());
|
||||
TEST_ASSERT_EQUAL(4, lines[1].size());
|
||||
}
|
||||
|
||||
void test_explicit_newlines_start_new_lines_and_keep_empty_ones() {
|
||||
auto lines = wrapText("a\n\nb", 60, measure);
|
||||
TEST_ASSERT_EQUAL(3, lines.size());
|
||||
TEST_ASSERT_EQUAL_STRING("", lines[1].c_str());
|
||||
}
|
||||
|
||||
void test_empty_text_is_no_lines() { TEST_ASSERT_EQUAL(0, wrapText("", 60, measure).size()); }
|
||||
|
||||
int main() {
|
||||
UNITY_BEGIN();
|
||||
RUN_TEST(test_short_text_is_one_line);
|
||||
RUN_TEST(test_wraps_at_word_boundaries);
|
||||
RUN_TEST(test_breaks_words_longer_than_a_line);
|
||||
RUN_TEST(test_never_splits_a_multibyte_character);
|
||||
RUN_TEST(test_explicit_newlines_start_new_lines_and_keep_empty_ones);
|
||||
RUN_TEST(test_empty_text_is_no_lines);
|
||||
return UNITY_END();
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
#include <unity.h>
|
||||
|
||||
#include "toast_queue.h"
|
||||
|
||||
using namespace roro;
|
||||
|
||||
void setUp() {}
|
||||
void tearDown() {}
|
||||
|
||||
void test_nothing_shown_when_empty() {
|
||||
ToastQueue q(3000);
|
||||
TEST_ASSERT_NULL(q.current(0));
|
||||
}
|
||||
|
||||
void test_toast_is_shown_for_its_duration() {
|
||||
ToastQueue q(3000);
|
||||
q.push("hello", NotificationLevel::Info, 1000);
|
||||
TEST_ASSERT_NOT_NULL(q.current(1000));
|
||||
TEST_ASSERT_EQUAL_STRING("hello", q.current(3999)->text.c_str());
|
||||
TEST_ASSERT_NULL(q.current(4000));
|
||||
}
|
||||
|
||||
void test_queued_toasts_follow_each_other() {
|
||||
ToastQueue q(3000);
|
||||
q.push("one", NotificationLevel::Info, 0);
|
||||
q.push("two", NotificationLevel::Info, 100);
|
||||
TEST_ASSERT_EQUAL_STRING("one", q.current(2999)->text.c_str());
|
||||
TEST_ASSERT_EQUAL_STRING("two", q.current(3000)->text.c_str());
|
||||
TEST_ASSERT_EQUAL_STRING("two", q.current(5999)->text.c_str());
|
||||
TEST_ASSERT_NULL(q.current(6000));
|
||||
}
|
||||
|
||||
void test_a_toast_queued_later_gets_its_full_duration() {
|
||||
ToastQueue q(3000);
|
||||
q.push("one", NotificationLevel::Info, 0);
|
||||
TEST_ASSERT_NULL(q.current(10000));
|
||||
q.push("two", NotificationLevel::Info, 20000);
|
||||
TEST_ASSERT_NOT_NULL(q.current(22999));
|
||||
}
|
||||
|
||||
void test_flood_keeps_the_visible_toast_and_drops_the_oldest_waiting() {
|
||||
ToastQueue q(3000, 2);
|
||||
q.push("a", NotificationLevel::Info, 0);
|
||||
q.push("b", NotificationLevel::Info, 0);
|
||||
q.push("c", NotificationLevel::Info, 0);
|
||||
TEST_ASSERT_EQUAL_STRING("a", q.current(0)->text.c_str());
|
||||
TEST_ASSERT_EQUAL_STRING("c", q.current(3000)->text.c_str());
|
||||
}
|
||||
|
||||
void test_dismiss_shows_the_next_one_immediately() {
|
||||
ToastQueue q(3000);
|
||||
q.push("one", NotificationLevel::Info, 0);
|
||||
q.push("two", NotificationLevel::Info, 0);
|
||||
q.dismiss(500);
|
||||
TEST_ASSERT_EQUAL_STRING("two", q.current(500)->text.c_str());
|
||||
}
|
||||
|
||||
void test_reports_when_a_new_toast_appears() {
|
||||
ToastQueue q(3000);
|
||||
q.push("one", NotificationLevel::Info, 0);
|
||||
TEST_ASSERT_TRUE(q.takeChanged());
|
||||
TEST_ASSERT_FALSE(q.takeChanged());
|
||||
q.current(3000); // expires
|
||||
TEST_ASSERT_TRUE(q.takeChanged());
|
||||
}
|
||||
|
||||
int main() {
|
||||
UNITY_BEGIN();
|
||||
RUN_TEST(test_nothing_shown_when_empty);
|
||||
RUN_TEST(test_toast_is_shown_for_its_duration);
|
||||
RUN_TEST(test_queued_toasts_follow_each_other);
|
||||
RUN_TEST(test_a_toast_queued_later_gets_its_full_duration);
|
||||
RUN_TEST(test_flood_keeps_the_visible_toast_and_drops_the_oldest_waiting);
|
||||
RUN_TEST(test_dismiss_shows_the_next_one_immediately);
|
||||
RUN_TEST(test_reports_when_a_new_toast_appears);
|
||||
return UNITY_END();
|
||||
}
|
||||
Reference in New Issue
Block a user