Files
roro9stack/test/test_app_manager/test_app_manager.cpp
T
twislaandClaude Opus 5.5 7b5df713ad
CI / build (pull_request) Successful in 1m38s
Site / build (pull_request) Successful in 10s
Shell: only its own replies, Apps by their names, and rm as Unix has it (#67)
The Shell shows the replies to its own commands and nothing else. The
console knows who each line is printed for (Console::As, Console::origin):
a command run from the Shell prints as the Shell's, and what answers it
later from another task carries that along (ls, tasks, du, cp, update
check, sd list, screenshot, gemini get). Ctrl+b shows everything instead.
This replaces the ten-second window, which was a guess.

An App's name with a capital opens it (Notes, Irc, Wifi, Gnss, Gemini, Lora,
Storage, Shell, System, Settings), from the Shell and from the consoles.

rm needs -r for a folder, here and over the consoles. In the Shell a file,
or a folder with something in it, is asked about unless -f; an empty folder
with -r goes without a word.

The Shell now hands its line to the main loop to run: run from inside the
key handler, rm on a folder overflowed the loop's stack and crashed the
device. `info` says which App is in front.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EhqxQ49eCju4CzKYNjZzwT
2026-10-07 11:35:29 +02:00

302 lines
11 KiB
C++

#include <unity.h>
#include <string>
#include <vector>
#include "app_manager.h"
using namespace roro;
void setUp() {}
void tearDown() {}
struct FakeApp : App {
explicit FakeApp(std::vector<std::string>* log, const char* tag) : log_(log), tag_(tag) {}
void onEnter() override { log_->push_back(std::string("enter ") + tag_); }
void onExit() override { log_->push_back(std::string("exit ") + tag_); }
bool onKey(const KeyEvent& e) override {
keys.push_back(e);
return handles;
}
void update(uint32_t nowMs) override { lastUpdate = nowMs; }
void draw(Canvas&) override {}
void help(std::vector<KeyHelp>& out) const override {
if (helpKey) out.push_back({helpKey, "does a thing"});
}
const char* helpTitle() const override { return scope; }
const char* helpKey = nullptr;
const char* scope = nullptr;
uint32_t lastUpdate = 0;
std::vector<KeyEvent> keys;
bool handles = false;
private:
std::vector<std::string>* log_;
const char* tag_;
};
struct Fixture {
std::vector<std::string> log;
FakeApp launcher{&log, "launcher"};
FakeApp notes{&log, "notes"};
FakeApp demo{&log, "demo"};
AppManager manager{launcher};
Fixture() {
manager.registerApp({"notes", "Notes", false, &notes});
manager.registerApp({"demo", "Demo", true, &demo});
manager.begin();
log.clear();
}
};
void test_launcher_is_in_foreground_after_begin() {
std::vector<std::string> log;
FakeApp launcher(&log, "launcher");
AppManager m(launcher);
m.begin();
TEST_ASSERT_EQUAL_PTR(&launcher, &m.foreground());
TEST_ASSERT_EQUAL_STRING("enter launcher", log[0].c_str());
}
void test_open_switches_foreground_with_exit_then_enter() {
Fixture f;
TEST_ASSERT_TRUE(f.manager.open("notes"));
TEST_ASSERT_EQUAL_PTR(&f.notes, &f.manager.foreground());
std::vector<std::string> expected = {"exit launcher", "enter notes"};
TEST_ASSERT_TRUE(f.log == expected);
}
void test_open_unknown_app_keeps_current_foreground() {
Fixture f;
TEST_ASSERT_FALSE(f.manager.open("nope"));
TEST_ASSERT_EQUAL_PTR(&f.launcher, &f.manager.foreground());
TEST_ASSERT_TRUE(f.log.empty());
}
void test_opening_the_foreground_app_again_does_nothing() {
Fixture f;
f.manager.open("notes");
f.log.clear();
TEST_ASSERT_TRUE(f.manager.open("notes"));
TEST_ASSERT_TRUE(f.log.empty());
}
void test_home_key_always_returns_to_launcher_without_reaching_the_app() {
Fixture f;
f.manager.open("notes");
f.notes.handles = true;
f.manager.handleKey(KeyEvent::of(Key::Home));
TEST_ASSERT_EQUAL_PTR(&f.launcher, &f.manager.foreground());
TEST_ASSERT_EQUAL(0, f.notes.keys.size());
}
void test_back_goes_to_the_app_first_and_it_can_keep_it() {
Fixture f;
f.manager.open("notes");
f.notes.handles = true; // e.g. leaving a sub-screen inside the app
f.manager.handleKey(KeyEvent::of(Key::Back));
TEST_ASSERT_EQUAL_PTR(&f.notes, &f.manager.foreground());
TEST_ASSERT_EQUAL(1, f.notes.keys.size());
}
void test_unhandled_back_returns_to_launcher() {
Fixture f;
f.manager.open("notes");
f.manager.handleKey(KeyEvent::of(Key::Back));
TEST_ASSERT_EQUAL_PTR(&f.launcher, &f.manager.foreground());
}
void test_back_on_launcher_stays_on_launcher() {
Fixture f;
f.manager.handleKey(KeyEvent::of(Key::Back));
TEST_ASSERT_EQUAL_PTR(&f.launcher, &f.manager.foreground());
TEST_ASSERT_TRUE(f.log.empty());
}
void test_other_keys_go_to_the_foreground_app() {
Fixture f;
f.manager.open("notes");
f.manager.handleKey(KeyEvent::character('a'));
TEST_ASSERT_EQUAL(1, f.notes.keys.size());
TEST_ASSERT_EQUAL('a', f.notes.keys[0].ch);
}
void test_visible_apps_exclude_hidden_ones_and_keep_registration_order() {
Fixture f;
auto visible = f.manager.visibleApps();
TEST_ASSERT_EQUAL(1, visible.size());
TEST_ASSERT_EQUAL_STRING("notes", visible[0]->id);
TEST_ASSERT_TRUE(f.manager.open("demo")); // hidden apps can still be opened
}
void test_switching_apps_requests_a_redraw() {
Fixture f;
f.manager.takeRedraw();
f.manager.open("notes");
TEST_ASSERT_TRUE(f.manager.takeRedraw());
TEST_ASSERT_FALSE(f.manager.takeRedraw());
}
void test_app_can_request_its_own_redraw() {
Fixture f;
f.manager.open("notes");
f.manager.takeRedraw();
f.notes.requestRedraw();
TEST_ASSERT_TRUE(f.manager.takeRedraw());
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);
}
void test_modal_app_keeps_home_and_back() {
Fixture f;
f.manager.openModal("demo");
f.manager.handleKey(KeyEvent::of(Key::Home));
f.manager.handleKey(KeyEvent::of(Key::Back));
TEST_ASSERT_EQUAL_PTR(&f.demo, &f.manager.foreground());
TEST_ASSERT_EQUAL(2, f.demo.keys.size()); // both reached the App
}
void test_ending_the_modal_returns_to_launcher_and_unlocks_home() {
Fixture f;
f.manager.openModal("demo");
f.manager.endModal();
TEST_ASSERT_EQUAL_PTR(&f.launcher, &f.manager.foreground());
f.manager.open("notes");
f.manager.handleKey(KeyEvent::of(Key::Home));
TEST_ASSERT_EQUAL_PTR(&f.launcher, &f.manager.foreground());
}
void test_modal_app_cannot_be_switched_away_by_open() {
Fixture f;
f.manager.openModal("demo");
TEST_ASSERT_FALSE(f.manager.open("notes"));
TEST_ASSERT_EQUAL_PTR(&f.demo, &f.manager.foreground());
}
// Issue #69: Fn+h lists the foreground App's keys, then the ones that work everywhere.
void test_help_lists_the_apps_keys_then_the_global_ones() {
Fixture f;
f.manager.begin();
f.manager.open("notes");
f.notes.helpKey = "n";
f.manager.handleKey(KeyEvent::of(Key::Help));
TEST_ASSERT_TRUE(f.manager.help().isOpen());
TEST_ASSERT_EQUAL_STRING("Notes", f.manager.help().title().c_str());
const auto& rows = f.manager.help().rows();
TEST_ASSERT_EQUAL_STRING("n", rows[0].keys);
TEST_ASSERT_EQUAL_STRING("Everywhere", rows[1].keys);
TEST_ASSERT_NULL(rows[1].action); // a heading
TEST_ASSERT_TRUE(rows.size() > 3);
TEST_ASSERT_EQUAL(0, f.notes.keys.size()); // the App never saw the key
TEST_ASSERT_TRUE(f.manager.takeRedraw());
}
void test_help_is_titled_with_the_scope_when_the_app_names_one() {
Fixture f;
f.manager.begin();
f.manager.handleKey(KeyEvent::of(Key::Help));
TEST_ASSERT_EQUAL_STRING("Launcher", f.manager.help().title().c_str());
f.manager.handleKey(KeyEvent::of(Key::Select)); // closes it
f.manager.open("notes");
f.notes.scope = "Notes: editor";
f.manager.handleKey(KeyEvent::of(Key::Help));
TEST_ASSERT_EQUAL_STRING("Notes: editor", f.manager.help().title().c_str());
}
void test_open_help_takes_every_key_and_any_but_the_arrows_closes_it() {
Fixture f;
f.manager.begin();
f.manager.open("notes");
f.manager.handleKey(KeyEvent::of(Key::Help));
f.manager.handleKey(KeyEvent::of(Key::Down)); // scrolls
TEST_ASSERT_TRUE(f.manager.help().isOpen());
f.manager.handleKey(KeyEvent::of(Key::Home)); // closes; doesn't go home
TEST_ASSERT_FALSE(f.manager.help().isOpen());
TEST_ASSERT_TRUE(&f.manager.foreground() == &f.notes);
TEST_ASSERT_EQUAL(0, f.notes.keys.size());
f.manager.handleKey(KeyEvent::character('x')); // closed: keys reach the App again
TEST_ASSERT_EQUAL(1, f.notes.keys.size());
}
void test_help_works_in_a_modal_app_and_closes_when_the_app_changes() {
Fixture f;
f.manager.begin();
f.manager.openModal("demo");
f.manager.handleKey(KeyEvent::of(Key::Help));
TEST_ASSERT_TRUE(f.manager.help().isOpen());
f.manager.endModal();
TEST_ASSERT_FALSE(f.manager.help().isOpen());
}
void test_the_help_panel_scrolls_within_its_rows() {
HelpModel m(3);
std::vector<KeyHelp> rows;
for (int i = 0; i < 8; i++) rows.push_back({"k", "a"});
m.open("T", rows);
m.onKey(KeyEvent::of(Key::Up));
TEST_ASSERT_EQUAL(0, m.top());
m.onKey(KeyEvent::of(Key::Right)); // a page
TEST_ASSERT_EQUAL(3, m.top());
for (int i = 0; i < 9; i++) m.onKey(KeyEvent::of(Key::Down));
TEST_ASSERT_EQUAL(5, m.top()); // the last row at the bottom, no further
m.onKey(KeyEvent::of(Key::Left));
TEST_ASSERT_EQUAL(2, m.top());
m.onKey(KeyEvent::of(Key::Tab));
TEST_ASSERT_FALSE(m.isOpen());
TEST_ASSERT_EQUAL(0, m.rows().size());
}
// Issue #67: an App is opened by its name with a capital letter.
void test_an_app_command_is_its_id_with_a_capital() {
TEST_ASSERT_EQUAL_STRING("Notes", AppManager::commandFor("notes").c_str());
TEST_ASSERT_EQUAL_STRING("Wifi", AppManager::commandFor("wifi-tools").c_str());
TEST_ASSERT_EQUAL_STRING("Irc", AppManager::commandFor("irc").c_str());
Fixture f;
TEST_ASSERT_NOT_NULL(f.manager.byCommand("Notes"));
TEST_ASSERT_EQUAL_STRING("notes", f.manager.byCommand("Notes")->id);
TEST_ASSERT_NULL(f.manager.byCommand("notes")); // small letters are commands, not Apps
TEST_ASSERT_NULL(f.manager.byCommand("Demo")); // hidden: not in the Launcher, not here
TEST_ASSERT_NULL(f.manager.byCommand("Nope"));
}
int main() {
UNITY_BEGIN();
RUN_TEST(test_launcher_is_in_foreground_after_begin);
RUN_TEST(test_open_switches_foreground_with_exit_then_enter);
RUN_TEST(test_open_unknown_app_keeps_current_foreground);
RUN_TEST(test_opening_the_foreground_app_again_does_nothing);
RUN_TEST(test_home_key_always_returns_to_launcher_without_reaching_the_app);
RUN_TEST(test_back_goes_to_the_app_first_and_it_can_keep_it);
RUN_TEST(test_unhandled_back_returns_to_launcher);
RUN_TEST(test_back_on_launcher_stays_on_launcher);
RUN_TEST(test_other_keys_go_to_the_foreground_app);
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);
RUN_TEST(test_modal_app_keeps_home_and_back);
RUN_TEST(test_ending_the_modal_returns_to_launcher_and_unlocks_home);
RUN_TEST(test_modal_app_cannot_be_switched_away_by_open);
RUN_TEST(test_help_lists_the_apps_keys_then_the_global_ones);
RUN_TEST(test_help_is_titled_with_the_scope_when_the_app_names_one);
RUN_TEST(test_open_help_takes_every_key_and_any_but_the_arrows_closes_it);
RUN_TEST(test_help_works_in_a_modal_app_and_closes_when_the_app_changes);
RUN_TEST(test_the_help_panel_scrolls_within_its_rows);
RUN_TEST(test_an_app_command_is_its_id_with_a_capital);
return UNITY_END();
}