/*
* Copyright 2013 Canonical Ltd.
* Copyright 2021 Robert Tari
*
* This program is free software: you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 3, as published
* by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranties of
* MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
* PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see .
*
* Authors:
* Charles Kerr
* Robert Tari
*/
#include "actions-mock.h"
#include "state-fixture.h"
#include
#include
#include
#include
#include
using namespace ayatana::indicator::datetime;
class MenuFixture: public StateFixture
{
private:
typedef StateFixture super;
protected:
std::shared_ptr m_menu_factory;
std::vector> m_menus;
void SetUp() override
{
super::SetUp();
// build the menus on top of the actions and state
m_menu_factory.reset(new MenuFactory(m_actions, m_state));
for(int i=0; ibuildMenu(Menu::Profile(i)));
}
void TearDown() override
{
m_menus.clear();
m_menu_factory.reset();
super::TearDown();
}
void InspectHeader(GMenuModel* menu_model, const std::string& name)
{
// check that there's a header menuitem
EXPECT_EQ(1,g_menu_model_get_n_items(menu_model));
gchar* str = nullptr;
g_menu_model_get_item_attribute(menu_model, 0, "x-ayatana-type", "s", &str);
EXPECT_STREQ("org.ayatana.indicator.root", str);
g_clear_pointer(&str, g_free);
g_menu_model_get_item_attribute(menu_model, 0, G_MENU_ATTRIBUTE_ACTION, "s", &str);
const auto action_name = name + "-header";
EXPECT_EQ(std::string("indicator.")+action_name, str);
g_clear_pointer(&str, g_free);
// check the header
auto dict = g_action_group_get_action_state(m_actions->action_group(), action_name.c_str());
EXPECT_TRUE(dict != nullptr);
EXPECT_TRUE(g_variant_is_of_type(dict, G_VARIANT_TYPE_VARDICT));
auto v = g_variant_lookup_value(dict, "accessible-desc", G_VARIANT_TYPE_STRING);
EXPECT_TRUE(v != nullptr);
g_variant_unref(v);
v = g_variant_lookup_value(dict, "label", G_VARIANT_TYPE_STRING);
EXPECT_TRUE(v != nullptr);
g_variant_unref(v);
v = g_variant_lookup_value(dict, "title", G_VARIANT_TYPE_STRING);
EXPECT_TRUE(v != nullptr);
g_variant_unref(v);
v = g_variant_lookup_value(dict, "visible", G_VARIANT_TYPE_BOOLEAN);
EXPECT_TRUE(v != nullptr);
g_variant_unref(v);
g_variant_unref(dict);
}
void InspectCalendar(GMenuModel* menu_model, Menu::Profile profile)
{
gchar* str = nullptr;
const char * expected_action;
if (profile == Menu::Desktop)
expected_action = "indicator.desktop.open-calendar-app";
else if (profile == Menu::Phone)
expected_action = "indicator.phone.open-calendar-app";
else
expected_action = nullptr;
const auto calendar_expected = ((profile == Menu::Desktop) || (profile == Menu::DesktopGreeter) || (profile == Menu::Phone))
&& (m_state->settings->show_calendar.get());
// get the calendar section
auto submenu = g_menu_model_get_item_link(menu_model, 0, G_MENU_LINK_SUBMENU);
auto section = g_menu_model_get_item_link(submenu, Menu::Calendar, G_MENU_LINK_SECTION);
// should be one or two items: a date label and maybe a calendar
ASSERT_TRUE(section != nullptr);
auto n_expected = calendar_expected ? 2 : 1;
EXPECT_EQ(n_expected, g_menu_model_get_n_items(section));
// look at the date menuitem
g_menu_model_get_item_attribute(section, 0, G_MENU_ATTRIBUTE_LABEL, "s", &str);
const auto now = m_state->clock->localtime();
EXPECT_EQ(now.format("%A, %e %B %Y"), str);
g_clear_pointer(&str, g_free);
g_menu_model_get_item_attribute(section, 0, G_MENU_ATTRIBUTE_ACTION, "s", &str);
if (expected_action != nullptr)
EXPECT_STREQ(expected_action, str);
else
EXPECT_TRUE(str == nullptr);
g_clear_pointer(&str, g_free);
// look at the calendar menuitem
if (calendar_expected)
{
g_menu_model_get_item_attribute(section, 1, "x-ayatana-type", "s", &str);
EXPECT_STREQ("org.ayatana.indicator.calendar", str);
g_clear_pointer(&str, g_free);
g_menu_model_get_item_attribute(section, 1, G_MENU_ATTRIBUTE_ACTION, "s", &str);
EXPECT_STREQ("indicator.calendar", str);
g_clear_pointer(&str, g_free);
g_menu_model_get_item_attribute(section, 1, "activation-action", "s", &str);
if (expected_action != nullptr)
EXPECT_STREQ(expected_action, str);
else
EXPECT_TRUE(str == nullptr);
g_clear_pointer(&str, g_free);
}
g_clear_object(§ion);
// now change the clock and see if the date label changes appropriately
auto tomorrow = now.add_days(1).start_of_day();
m_mock_state->mock_clock->set_localtime(tomorrow);
wait_msec();
section = g_menu_model_get_item_link(submenu, Menu::Calendar, G_MENU_LINK_SECTION);
g_menu_model_get_item_attribute(section, 0, G_MENU_ATTRIBUTE_LABEL, "s", &str);
EXPECT_EQ(tomorrow.format("%A, %e %B %Y"), str);
g_clear_pointer(&str, g_free);
g_clear_object(§ion);
// cleanup
g_object_unref(submenu);
}
private:
void InspectEmptySection(GMenuModel* menu_model, Menu::Section section)
{
// get the Appointments section
auto submenu = g_menu_model_get_item_link(menu_model, 0, G_MENU_LINK_SUBMENU);
auto menu_section = g_menu_model_get_item_link(submenu, section, G_MENU_LINK_SECTION);
EXPECT_EQ(0, g_menu_model_get_n_items(menu_section));
g_clear_object(&menu_section);
g_clear_object(&submenu);
}
std::vector build_some_appointments()
{
const auto now = m_state->clock->localtime();
const auto tomorrow = now.add_days(1);
Appointment a1; // an alarm clock appointment
a1.color = "red";
a1.summary = "http://www.example.com/";
a1.uid = "example";
a1.type = Appointment::ALARM;
a1.begin = a1.end = tomorrow;
Appointment a2; // a non-alarm appointment
a2.color = "green";
a2.summary = "http://www.monkey.com/";
a2.uid = "monkey";
a2.type = Appointment::EVENT;
a2.begin = a2.end = tomorrow;
return std::vector({a1, a2});
}
void InspectAppointmentMenuItem(GMenuModel* section,
int index,
const Appointment& appt)
{
// confirm it has the right x-ayatana-type
gchar * str = nullptr;
g_menu_model_get_item_attribute(section, index, "x-ayatana-type", "s", &str);
if (appt.is_alarm())
EXPECT_STREQ("org.ayatana.indicator.alarm", str);
else
EXPECT_STREQ("org.ayatana.indicator.appointment", str);
g_clear_pointer(&str, g_free);
// confirm it has a nonempty x-ayatana-time-format
g_menu_model_get_item_attribute(section, index, "x-ayatana-time-format", "s", &str);
EXPECT_TRUE(str && *str);
g_clear_pointer(&str, g_free);
// confirm the color hint, if it exists,
// is in the x-ayatana-color attribute
if (appt.color.empty())
{
EXPECT_FALSE(g_menu_model_get_item_attribute(section,
index,
"x-ayatana-color",
"s",
&str));
}
else
{
EXPECT_TRUE(g_menu_model_get_item_attribute(section,
index,
"x-ayatana-color",
"s",
&str));
EXPECT_EQ(appt.color, str);
}
g_clear_pointer(&str, g_free);
// confirm that alarms have an icon
if (appt.is_alarm())
{
auto v = g_menu_model_get_item_attribute_value(section,
index,
G_MENU_ATTRIBUTE_ICON,
nullptr);
EXPECT_TRUE(v != nullptr);
auto icon = g_icon_deserialize(v);
EXPECT_TRUE(icon != nullptr);
g_clear_object(&icon);
g_clear_pointer(&v, g_variant_unref);
}
}
void InspectAppointmentMenuItems(GMenuModel* section,
int first_appt_index,
const std::vector& appointments,
bool can_open_planner)
{
// try adding a few appointments and see if the menu updates itself
m_state->calendar_upcoming->appointments().set(appointments);
wait_msec(); // wait a moment for the menu to update
//auto submenu = g_menu_model_get_item_link(menu_model, 0, G_MENU_LINK_SUBMENU);
//auto section = g_menu_model_get_item_link(submenu, Menu::Appointments, G_MENU_LINK_SECTION);
const int n_add_event_buttons = can_open_planner ? 1 : 0;
EXPECT_EQ(n_add_event_buttons + appointments.size(), g_menu_model_get_n_items(section));
for (int i=0, n=appointments.size(); isettings->show_alarms.set(false);
for (int i=0, n=appointments.size(); isettings->show_alarms.set(true);
//g_clear_object(§ion);
//g_clear_object(&submenu);
}
void InspectDesktopAppointments(GMenuModel* menu_model, bool can_open_planner)
{
const int n_add_event_buttons = can_open_planner ? 1 : 0;
// get the Appointments section
auto submenu = g_menu_model_get_item_link(menu_model, 0, G_MENU_LINK_SUBMENU);
// there shouldn't be any menuitems when "show events" is false
m_state->settings->show_events.set(false);
wait_msec();
auto section = g_menu_model_get_item_link(submenu, Menu::Appointments, G_MENU_LINK_SECTION);
EXPECT_EQ(0, g_menu_model_get_n_items(section));
g_clear_object(§ion);
std::vector appointments;
m_state->settings->show_events.set(true);
m_state->calendar_upcoming->appointments().set(appointments);
wait_msec();
section = g_menu_model_get_item_link(submenu, Menu::Appointments, G_MENU_LINK_SECTION);
EXPECT_EQ(n_add_event_buttons, g_menu_model_get_n_items(section));
if (can_open_planner)
{
// when "show_events" is true,
// there should be an "add event" button even if there aren't any appointments
gchar* action = nullptr;
EXPECT_TRUE(g_menu_model_get_item_attribute(section, 0, G_MENU_ATTRIBUTE_ACTION, "s", &action));
const char* expected_action = "desktop.open-calendar-app";
EXPECT_EQ(std::string("indicator.")+expected_action, action);
EXPECT_TRUE(g_action_group_has_action(m_actions->action_group(), expected_action));
g_free(action);
}
g_clear_object(§ion);
// try adding a few appointments and see if the menu updates itself
appointments = build_some_appointments();
m_state->calendar_upcoming->appointments().set(appointments);
wait_msec(); // wait a moment for the menu to update
section = g_menu_model_get_item_link(submenu, Menu::Appointments, G_MENU_LINK_SECTION);
EXPECT_EQ(n_add_event_buttons + 2, g_menu_model_get_n_items(section));
InspectAppointmentMenuItems(section, 0, appointments, can_open_planner);
g_clear_object(§ion);
// cleanup
g_clear_object(&submenu);
}
void InspectPhoneAppointments(GMenuModel* menu_model, bool can_open_planner)
{
auto submenu = g_menu_model_get_item_link(menu_model, 0, G_MENU_LINK_SUBMENU);
// there shouldn't be any menuitems when "show events" is false
m_state->settings->show_events.set(false);
wait_msec();
section = g_menu_model_get_item_link(submenu, Menu::Appointments, G_MENU_LINK_SECTION);
EXPECT_EQ(0, g_menu_model_get_n_items(section));
g_clear_object(§ion);
// clear all the appointments
std::vector appointments;
m_state->settings->show_events.set(true);
m_state->calendar_upcoming->appointments().set(appointments);
wait_msec(); // wait a moment for the menu to update
// check that there's a "clock app" menuitem even when there are no appointments
auto section = g_menu_model_get_item_link(submenu, Menu::Appointments, G_MENU_LINK_SECTION);
const char* expected_action = "phone.open-alarm-app";
EXPECT_EQ(1, g_menu_model_get_n_items(section));
gchar* action = nullptr;
EXPECT_TRUE(g_menu_model_get_item_attribute(section, 0, G_MENU_ATTRIBUTE_ACTION, "s", &action));
EXPECT_EQ(std::string("indicator.")+expected_action, action);
EXPECT_TRUE(g_action_group_has_action(m_actions->action_group(), expected_action));
g_free(action);
g_clear_object(§ion);
// add some appointments and test them
appointments = build_some_appointments();
m_state->calendar_upcoming->appointments().set(appointments);
wait_msec(); // wait a moment for the menu to update
section = g_menu_model_get_item_link(submenu, Menu::Appointments, G_MENU_LINK_SECTION);
EXPECT_EQ(3, g_menu_model_get_n_items(section));
InspectAppointmentMenuItems(section, 1, appointments, can_open_planner);
g_clear_object(§ion);
// cleanup
g_clear_object(&submenu);
}
protected:
void InspectAppointments(GMenuModel* menu_model, Menu::Profile profile)
{
const auto can_open_planner = m_actions->desktop_has_calendar_app();
switch (profile)
{
case Menu::Desktop:
InspectDesktopAppointments(menu_model, can_open_planner);
break;
case Menu::DesktopGreeter:
InspectEmptySection(menu_model, Menu::Appointments);
break;
case Menu::Phone:
InspectPhoneAppointments(menu_model, can_open_planner);
break;
case Menu::PhoneGreeter:
InspectEmptySection(menu_model, Menu::Appointments);
break;
default:
g_warn_if_reached();
break;
}
}
void CompareLocationsTo(GMenuModel* menu_model, const std::vector& locations)
{
// get the Locations section
auto submenu = g_menu_model_get_item_link(menu_model, 0, G_MENU_LINK_SUBMENU);
auto section = g_menu_model_get_item_link(submenu, Menu::Locations, G_MENU_LINK_SECTION);
// confirm that section's menuitems mirror the "locations" vector
const auto n = locations.size();
ASSERT_EQ(n, g_menu_model_get_n_items(section));
for (guint i=0; i empty;
m_state->locations->locations.set(empty);
wait_msec();
CompareLocationsTo(menu_model, empty);
// add some locations and confirm the menu picked up our changes
Location l1 ("America/Chicago", "Dallas");
Location l2 ("America/Arizona", "Phoenix");
std::vector locations({l1, l2});
m_state->locations->locations.set(locations);
wait_msec();
CompareLocationsTo(menu_model, locations_expected ? locations : empty);
// now remove one of the locations...
locations.pop_back();
m_state->locations->locations.set(locations);
wait_msec();
CompareLocationsTo(menu_model, locations_expected ? locations : empty);
}
void InspectSettings(GMenuModel* menu_model, Menu::Profile profile)
{
std::string expected_action;
if (profile == Menu::Desktop)
expected_action = "indicator.desktop.open-settings-app";
else if (profile == Menu::Phone)
expected_action = "indicator.phone.open-settings-app";
// get the Settings section
auto submenu = g_menu_model_get_item_link(menu_model, 0, G_MENU_LINK_SUBMENU);
auto section = g_menu_model_get_item_link(submenu, Menu::Settings, G_MENU_LINK_SECTION);
if (expected_action.empty())
{
EXPECT_EQ(0, g_menu_model_get_n_items(section));
}
else
{
EXPECT_EQ(1, g_menu_model_get_n_items(section));
gchar* str = nullptr;
g_menu_model_get_item_attribute(section, 0, G_MENU_ATTRIBUTE_ACTION, "s", &str);
EXPECT_EQ(expected_action, str);
g_clear_pointer(&str, g_free);
}
g_clear_object(§ion);
g_object_unref(submenu);
}
};
TEST_F(MenuFixture, HelloWorld)
{
EXPECT_EQ(Menu::NUM_PROFILES, m_menus.size());
for (int i=0; imenu_model() != nullptr);
EXPECT_EQ(i, m_menus[i]->profile());
}
EXPECT_EQ(m_menus[Menu::Desktop]->name(), "desktop");
}
TEST_F(MenuFixture, Header)
{
for(auto& menu : m_menus)
InspectHeader(menu->menu_model(), menu->name());
}
TEST_F(MenuFixture, Sections)
{
for(auto& menu : m_menus)
{
// check that the header has a submenu
auto menu_model = menu->menu_model();
auto submenu = g_menu_model_get_item_link(menu_model, 0, G_MENU_LINK_SUBMENU);
EXPECT_TRUE(submenu != nullptr);
EXPECT_EQ(Menu::NUM_SECTIONS, g_menu_model_get_n_items(submenu));
g_object_unref(submenu);
}
}
TEST_F(MenuFixture, Calendar)
{
m_state->settings->show_calendar.set(true);
for(auto& menu : m_menus)
InspectCalendar(menu->menu_model(), menu->profile());
m_state->settings->show_calendar.set(false);
for(auto& menu : m_menus)
InspectCalendar(menu->menu_model(), menu->profile());
}
TEST_F(MenuFixture, Appointments)
{
for(auto& menu : m_menus)
InspectAppointments(menu->menu_model(), menu->profile());
// toggle can_open_planner() and test the desktop again
// to confirm that the "Add Event…" menuitem appears iff
// there's a calendar available user-agent
m_mock_actions->set_desktop_has_calendar_app (!m_actions->desktop_has_calendar_app());
std::shared_ptr