mirror of https://github.com/cutefishos/settings
You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
286 lines
10 KiB
C++
286 lines
10 KiB
C++
// SPDX-FileCopyrightText: 2026 Reion Wong <reionwong@gmail.com>
|
|
// SPDX-License-Identifier: BSD-3-Clause
|
|
|
|
#include <gtest/gtest.h>
|
|
|
|
#include <algorithm>
|
|
#include <cmath>
|
|
#include <optional>
|
|
#include <string>
|
|
#include <vector>
|
|
|
|
#include "pages/battery/batteryhistory.h"
|
|
#include "pages/battery/timeformat.h"
|
|
#include "ui/label.h"
|
|
#include "ui/test_input.h"
|
|
#include "ui/window.h"
|
|
#include "window/options.h"
|
|
#include "window/page.h"
|
|
#include "window/settingswindow.h"
|
|
|
|
using namespace cutefish::ui;
|
|
using cutefish::upower::HistorySample;
|
|
using namespace settings;
|
|
|
|
namespace {
|
|
|
|
std::string dumpLine(const Window& window, const std::string& name)
|
|
{
|
|
const std::string dump = window.layoutDump();
|
|
const size_t at = dump.find("\"" + name + "\"");
|
|
if (at == std::string::npos)
|
|
return {};
|
|
const size_t start = dump.rfind('\n', at) + 1;
|
|
return dump.substr(start, dump.find('\n', at) - start);
|
|
}
|
|
|
|
// The view's logical rect in window coordinates, from its dump line.
|
|
RectF windowRect(const Window& window, const std::string& name)
|
|
{
|
|
const std::string line = dumpLine(window, name);
|
|
const size_t device = line.find(" device ");
|
|
if (device == std::string::npos)
|
|
return {-1, -1, -1, -1};
|
|
long x, y, width, height;
|
|
sscanf(line.c_str() + device, " device %ld %ld %ld %ld", &x, &y, &width, &height);
|
|
const double scale = window.scale();
|
|
return {x / scale, y / scale, width / scale, height / scale};
|
|
}
|
|
|
|
bool isHidden(const Window& window, const std::string& name)
|
|
{
|
|
return dumpLine(window, name).ends_with(" hidden");
|
|
}
|
|
|
|
View* findView(View* view, const std::string& name)
|
|
{
|
|
if (view->name() == name)
|
|
return view;
|
|
for (const auto& child : view->children()) {
|
|
if (View* found = findView(child.get(), name))
|
|
return found;
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
struct BatteryPageTest : testing::Test
|
|
{
|
|
void SetUp() override
|
|
{
|
|
setOptions({.mockBattery = true});
|
|
settingsWindow = std::make_unique<SettingsWindow>();
|
|
window().setScale(1.5);
|
|
window().resize({900, 610});
|
|
settingsWindow->reveal();
|
|
settingsWindow->switchToPage("battery");
|
|
// Through the curve's reveal.
|
|
for (uint64_t ms = 0; ms <= 1000; ms += 100)
|
|
TestInput::presentFrame(window(), ms * 1000000);
|
|
window().grab();
|
|
}
|
|
|
|
void TearDown() override { setOptions({}); }
|
|
|
|
Window& window() { return settingsWindow->window(); }
|
|
|
|
std::string labelText(const std::string& name)
|
|
{
|
|
auto* label = dynamic_cast<Label*>(findView(settingsWindow->currentPage(), name));
|
|
return label ? label->text() : std::string();
|
|
}
|
|
|
|
std::unique_ptr<SettingsWindow> settingsWindow;
|
|
};
|
|
|
|
} // namespace
|
|
|
|
TEST_F(BatteryPageTest, ShowsTheSampleBattery)
|
|
{
|
|
EXPECT_FALSE(isHidden(window(), "battery:history"));
|
|
EXPECT_FALSE(isHidden(window(), "battery:health"));
|
|
EXPECT_FALSE(isHidden(window(), "battery:lastcharged"));
|
|
EXPECT_FALSE(isHidden(window(), "battery:charging"));
|
|
}
|
|
|
|
TEST_F(BatteryPageTest, TheRangePickerEndsWithTheCard)
|
|
{
|
|
const RectF range = windowRect(window(), "battery:range");
|
|
const RectF card = windowRect(window(), "battery:graphcard");
|
|
// Compact, to sit in the title row.
|
|
EXPECT_EQ(range.height, 32);
|
|
EXPECT_EQ(range.x + range.width, card.x + card.width);
|
|
// Below the title row with the section's label spacing.
|
|
EXPECT_EQ(card.y, range.y + range.height + 8);
|
|
// 240 for the graph and the card's padding.
|
|
EXPECT_EQ(card.height, 240 + 2 * 12);
|
|
const RectF graph = windowRect(window(), "battery:graph");
|
|
EXPECT_EQ(graph.x, card.x + 16);
|
|
EXPECT_EQ(graph.width, card.width - 32);
|
|
EXPECT_EQ(graph.height, 240);
|
|
}
|
|
|
|
TEST_F(BatteryPageTest, TheTitleCentresOnThePicker)
|
|
{
|
|
View* page = settingsWindow->currentPage();
|
|
const RectF range = findView(page, "battery:range")->geometry();
|
|
const RectF title = findView(page, "battery:historytitle")->geometry();
|
|
// Where Qt's RowLayout puts it, measured with the QML: y 8 next to the 32
|
|
// high picker.
|
|
EXPECT_EQ(title.y, 8);
|
|
EXPECT_EQ(title.height, 17);
|
|
EXPECT_EQ(range.y, 0);
|
|
}
|
|
|
|
TEST_F(BatteryPageTest, ThePlotLeavesRoomForTheAxes)
|
|
{
|
|
const RectF graph = windowRect(window(), "battery:graph");
|
|
const RectF plot = windowRect(window(), "graph:plot");
|
|
const RectF legend = windowRect(window(), "graph:legend");
|
|
EXPECT_EQ(plot.x, graph.x);
|
|
EXPECT_GT(plot.y, legend.y + legend.height);
|
|
// The value labels on the right, the time labels below.
|
|
EXPECT_LT(plot.x + plot.width, graph.x + graph.width - 30);
|
|
EXPECT_LT(plot.y + plot.height, graph.y + graph.height - 15);
|
|
}
|
|
|
|
TEST_F(BatteryPageTest, TheHoveringPointerReadsTheCurveOff)
|
|
{
|
|
EXPECT_TRUE(isHidden(window(), "graph:readout"));
|
|
const RectF plot = windowRect(window(), "graph:plot");
|
|
TestInput::movePointer(window(), {plot.x + plot.width / 2, plot.y + plot.height / 2});
|
|
window().grab();
|
|
EXPECT_FALSE(isHidden(window(), "graph:readout"));
|
|
// A whole percentage of the sample day, then the state and the time.
|
|
const std::string value = labelText("graph:readout:value");
|
|
ASSERT_FALSE(value.empty());
|
|
EXPECT_EQ(value.back(), '%');
|
|
const int percent = std::stoi(value);
|
|
EXPECT_GE(percent, 26);
|
|
EXPECT_LE(percent, 90);
|
|
EXPECT_NE(labelText("graph:readout:time").find(" · "), std::string::npos);
|
|
const RectF readout = windowRect(window(), "graph:readout");
|
|
const RectF legend = windowRect(window(), "graph:legend");
|
|
EXPECT_EQ(readout.y, legend.y);
|
|
EXPECT_EQ(readout.height, legend.height);
|
|
|
|
// Off the plot, the legend comes back.
|
|
TestInput::movePointer(window(), {plot.x + plot.width / 2, plot.y + plot.height + 40});
|
|
window().grab();
|
|
EXPECT_TRUE(isHidden(window(), "graph:readout"));
|
|
TestInput::movePointer(window(), {plot.x + plot.width / 2, plot.y + plot.height / 2});
|
|
TestInput::leavePointer(window());
|
|
window().grab();
|
|
EXPECT_TRUE(isHidden(window(), "graph:readout"));
|
|
}
|
|
|
|
TEST_F(BatteryPageTest, SevenDaysPlotsAWeek)
|
|
{
|
|
const RectF range = windowRect(window(), "battery:range");
|
|
TestInput::click(window(), {range.x + range.width * 3 / 4, range.y + range.height / 2});
|
|
for (uint64_t ms = 1000; ms <= 2000; ms += 100)
|
|
TestInput::presentFrame(window(), ms * 1000000);
|
|
window().grab();
|
|
const RectF plot = windowRect(window(), "graph:plot");
|
|
TestInput::movePointer(window(), {plot.x + 1, plot.y + plot.height / 2});
|
|
window().grab();
|
|
// A week's reading names the day.
|
|
const std::string time = labelText("graph:readout:time");
|
|
bool named = false;
|
|
for (int day = 0; day < 7; ++day)
|
|
named = named || time.find(" · " + timeformat::shortDayName(1792000000 + day * 86400) + " ") != std::string::npos;
|
|
EXPECT_TRUE(named) << time;
|
|
}
|
|
|
|
TEST(BatteryRevealTest, TheCurveDrawsInFromTheLeft)
|
|
{
|
|
setOptions({.mockBattery = true});
|
|
auto settingsWindow = std::make_unique<SettingsWindow>();
|
|
Window& window = settingsWindow->window();
|
|
window.setScale(1.5);
|
|
window.resize({900, 610});
|
|
settingsWindow->reveal();
|
|
settingsWindow->switchToPage("battery");
|
|
// Lays the page out and starts the reveal, then its first tick.
|
|
TestInput::presentFrame(window, 1000000);
|
|
TestInput::presentFrame(window, 2000000);
|
|
const RectF plot = windowRect(window, "graph:plot");
|
|
// The curve's pixels in a column: the line in either colour, against the
|
|
// grey and white of the card and the grid.
|
|
const auto curveAt = [&](const Image& image, double x) {
|
|
int count = 0;
|
|
for (double y = plot.y; y < plot.y + plot.height; y += 1) {
|
|
const Color pixel = image.pixel(int(x * 1.5), int(y * 1.5));
|
|
if (std::max({pixel.r, pixel.g, pixel.b}) - std::min({pixel.r, pixel.g, pixel.b}) > 60)
|
|
++count;
|
|
}
|
|
return count;
|
|
};
|
|
// Half way through 700 ms of an out-cubic: seven eighths drawn.
|
|
const Image early = TestInput::presentFrame(window, 352000000);
|
|
EXPECT_GT(curveAt(early, plot.x + plot.width * 0.5), 0);
|
|
EXPECT_EQ(curveAt(early, plot.x + plot.width * 0.97), 0);
|
|
const Image done = TestInput::presentFrame(window, 800000000);
|
|
EXPECT_GT(curveAt(done, plot.x + plot.width * 0.97), 0);
|
|
settingsWindow.reset();
|
|
setOptions({});
|
|
}
|
|
|
|
TEST(BatteryHistoryTest, StartsWithTheValueInForce)
|
|
{
|
|
const uint32_t now = 100000;
|
|
// Newest first, as UPower returns them.
|
|
const std::vector<HistorySample> window{{99000, 50, 2}, {95000, 60, 2}};
|
|
const std::vector<HistorySample> all{{99000, 50, 2}, {95000, 60, 2}, {90000, 70, 1}, {80000, 75, 1}};
|
|
const auto values = BatteryHistory::plotted(window, all, now, 10000);
|
|
ASSERT_EQ(values.size(), 4u);
|
|
// The sample before the window, moved to its start.
|
|
EXPECT_EQ(values[0].time, 90000u);
|
|
EXPECT_EQ(values[0].value, 70);
|
|
EXPECT_EQ(values[1].time, 95000u);
|
|
EXPECT_EQ(values[2].time, 99000u);
|
|
// The last value runs on to the present.
|
|
EXPECT_EQ(values[3].time, now);
|
|
EXPECT_EQ(values[3].value, 50);
|
|
}
|
|
|
|
TEST(BatteryHistoryTest, SkipsRestartsWrongClocksAndDuplicates)
|
|
{
|
|
const uint32_t now = 100000;
|
|
// Newest first, as UPower returns them.
|
|
const std::vector<HistorySample> window{
|
|
{99500, 40, 2},
|
|
// From the future.
|
|
{200000, 45, 2},
|
|
// Stamped before the clock was set: older than the one recorded before it.
|
|
{96500, 58, 2},
|
|
// Two in one second: the later one wins.
|
|
{97000, 56, 2},
|
|
{97000, 55, 2},
|
|
// A restart marker.
|
|
{96000, 0, 0},
|
|
{95000, 60, 2},
|
|
};
|
|
const auto values = BatteryHistory::plotted(window, std::nullopt, now, 10000);
|
|
ASSERT_EQ(values.size(), 4u);
|
|
EXPECT_EQ(values[0].time, 95000u);
|
|
EXPECT_EQ(values[0].value, 60);
|
|
EXPECT_EQ(values[1].time, 97000u);
|
|
EXPECT_EQ(values[1].value, 56);
|
|
EXPECT_EQ(values[2].time, 99500u);
|
|
EXPECT_EQ(values[3].time, now);
|
|
EXPECT_EQ(values[3].value, 40);
|
|
}
|
|
|
|
TEST(BatteryHistoryTest, AnEmptyWindowPlotsNothing)
|
|
{
|
|
EXPECT_TRUE(BatteryHistory::plotted({}, std::nullopt, 100000, 86400).empty());
|
|
}
|
|
|
|
TEST(TimeFormatTest, CountsLocalDays)
|
|
{
|
|
const int64_t noon = 1792000000;
|
|
EXPECT_EQ(timeformat::daysBetween(noon, noon), 0);
|
|
EXPECT_EQ(timeformat::daysBetween(noon - 86400, noon), 1);
|
|
EXPECT_EQ(timeformat::daysBetween(noon - 3 * 86400, noon), 3);
|
|
}
|