Files
Tushar Rohilla e5295f36aa Add Afterglow 1.0.6, a PS5 internet radio and music visualizer
222 stations in 17 genres, 56 visualizer scenes, Ogg and Opus playback, station logos, crossfade, My Music from USB, a command box and live BPM sync.
2026-10-16 00:59:10 -04:00

197 lines
6.9 KiB
C++

// ps5-homebrew-ui - Behaviour tests for the Pulse Dashboard design.
// Copyright (C) 2026 BlackBearReloaded
// SPDX-License-Identifier: GPL-3.0-or-later
//
// The dashboard's focus moves spatially between tiles of different sizes.
// A focus tick is panned by the tile it lands on, so the tests can tell where
// the focus went from the sound alone, without reaching into the design.
#include "concept_fixture.hpp"
#include "concepts/concepts.hpp"
namespace
{
using hui::Action;
using hui::Direction;
using hui::audio::Cue;
// The stereo position of each tile's centre (ui::pan_for_x of its middle).
constexpr float kPanPlay = 0.125f; // columns 7 to 9
constexpr float kPanRenderer = 0.376f; // columns 10 to 12
constexpr float kPanLibrary = -0.376f; // columns 1 to 3
constexpr float kPanStorage = 0.0f; // columns 4 to 9: centred
constexpr float kPanSession = -0.125f; // columns 4 to 6
constexpr float kPanRecent = 0.376f; // columns 10 to 12
class Dashboard : public hui::testing::ConceptFixture
{
protected:
// The first cue of that kind the last send() asked for.
const hui::audio::CueEvent *event(Cue cue) const
{
for (const hui::audio::CueEvent &candidate : last_cues_)
{
if (candidate.cue == cue)
return &candidate;
}
return nullptr;
}
// Sends a direction and checks the focus landed on the tile with this pan.
void expect_move(Direction direction, float pan)
{
send(*design_, nav(direction));
const hui::audio::CueEvent *tick = event(Cue::focus);
ASSERT_NE(tick, nullptr);
EXPECT_NEAR(tick->pan, pan, 0.01f);
}
std::unique_ptr<hui::app::Concept> design_ = hui::concepts::make_dashboard(context_);
};
TEST_F(Dashboard, FocusMovesAlongTheTopRowAndTheEdgeRefuses)
{
expect_move(Direction::right, kPanPlay);
expect_move(Direction::right, kPanRenderer);
send(*design_, nav(Direction::right));
EXPECT_TRUE(played(Cue::error));
EXPECT_FALSE(played(Cue::focus));
}
TEST_F(Dashboard, AHeldDirectionAtTheEdgeStaysQuiet)
{
hui::InputFrame held = nav(Direction::left);
held.nav_repeat = true;
send(*design_, held);
EXPECT_TRUE(last_cues_.empty());
}
TEST_F(Dashboard, TilesOfDifferentSizesAreReachedSpatially)
{
// Down from the wide hero tile: the tile squarely below its left half.
expect_move(Direction::down, kPanLibrary);
// Right: the wide storage tile and the session tile are equally near;
// the tie goes to the earlier one in reading order.
expect_move(Direction::right, kPanStorage);
// Up from the wide tile: the tile above its centre, not the hero.
expect_move(Direction::up, kPanPlay);
// Down again does not skip the wide tile for the one in line below it.
expect_move(Direction::down, kPanStorage);
expect_move(Direction::down, kPanSession);
}
TEST_F(Dashboard, TheOppositeDirectionReturnsToWhereTheFocusCameFrom)
{
expect_move(Direction::right, kPanPlay);
expect_move(Direction::down, kPanStorage);
expect_move(Direction::down, kPanSession);
expect_move(Direction::right, kPanPlay); // achievements, below the play tile
expect_move(Direction::right, kPanRecent);
// Spatially the nearest tile to the left is the storage tile (pan 0), but
// the focus came from the achievements tile and goes back there.
expect_move(Direction::left, kPanPlay);
}
TEST_F(Dashboard, LowerTilesTickAtALowerPitch)
{
send(*design_, nav(Direction::right));
const hui::audio::CueEvent *top = event(Cue::focus);
ASSERT_NE(top, nullptr);
const float top_pitch = top->pitch;
send(*design_, nav(Direction::down));
const hui::audio::CueEvent *lower = event(Cue::focus);
ASSERT_NE(lower, nullptr);
EXPECT_LT(lower->pitch, top_pitch);
}
TEST_F(Dashboard, ExpandingTakesTheInputAndCollapsingGivesItBack)
{
send(*design_, press(Action::confirm));
EXPECT_TRUE(played(Cue::open));
expect_drawable(*design_);
EXPECT_TRUE(frame_.glass); // the detail view is a frosted overlay
// The frame graph's detail has a three-step window selector.
send(*design_, nav(Direction::right));
EXPECT_TRUE(played(Cue::slider));
EXPECT_FALSE(played(Cue::focus));
send(*design_, nav(Direction::right));
EXPECT_TRUE(played(Cue::slider));
send(*design_, nav(Direction::right));
EXPECT_TRUE(played(Cue::error));
send(*design_, press(Action::back), 1.5f);
EXPECT_TRUE(played(Cue::back));
expect_drawable(*design_);
EXPECT_FALSE(frame_.glass);
expect_move(Direction::right, kPanPlay);
// Coming back to the design finds the grid, not a detail view left open.
send(*design_, press(Action::confirm));
design_->enter();
expect_move(Direction::right, kPanRenderer);
expect_drawable(*design_);
EXPECT_FALSE(frame_.glass);
}
TEST_F(Dashboard, TheWeekChartStartsOnTodayAndStepsThroughTheDays)
{
send(*design_, nav(Direction::right));
send(*design_, press(Action::confirm));
// Today is the last bar: nothing lies to its right.
send(*design_, nav(Direction::right));
EXPECT_TRUE(played(Cue::error));
send(*design_, nav(Direction::left));
const hui::audio::CueEvent *first = event(Cue::slider);
ASSERT_NE(first, nullptr);
const float first_pitch = first->pitch;
send(*design_, nav(Direction::left));
const hui::audio::CueEvent *second = event(Cue::slider);
ASSERT_NE(second, nullptr);
EXPECT_LT(second->pitch, first_pitch); // earlier days sound lower
// The days are one row: up and down have nowhere to go.
send(*design_, nav(Direction::up));
EXPECT_TRUE(played(Cue::error));
}
// The host snapshots have no telemetry, so this is where the graph with real
// samples is exercised: the frame times arrive, the line appears, pausing
// (Square) stops it from growing.
TEST_F(Dashboard, TheGraphDrawsFrameTimesAsTheyArriveAndPauseHoldsIt)
{
expect_drawable(*design_);
const std::size_t empty = frame_.scene.instances().size();
const auto present = [this](int frames)
{
for (int i = 0; i < frames; ++i)
{
telemetry_.push(i % 50 == 0 ? 31.0f : 16.6f);
telemetry_.fps = 60.0f;
send(*design_, idle(), 0.0f);
}
};
present(120);
expect_drawable(*design_);
const std::size_t live = frame_.scene.instances().size();
EXPECT_GT(live, empty + 150); // a fill and a line segment per sample
send(*design_, press(Action::west));
EXPECT_TRUE(played(Cue::toggle));
expect_drawable(*design_);
const std::size_t paused = frame_.scene.instances().size();
present(60);
expect_drawable(*design_);
EXPECT_EQ(frame_.scene.instances().size(), paused);
send(*design_, press(Action::west));
EXPECT_TRUE(played(Cue::toggle));
present(60);
expect_drawable(*design_);
EXPECT_GT(frame_.scene.instances().size(), paused + 50);
// The detail view shows a longer window of the same history.
send(*design_, press(Action::confirm), 1.0f);
expect_drawable(*design_);
}
} // namespace