Skip to content

Add Radio Now Playing screen#163

Merged
phanan merged 5 commits into
masterfrom
feat/radio-now-playing-screen
Mar 30, 2026
Merged

Add Radio Now Playing screen#163
phanan merged 5 commits into
masterfrom
feat/radio-now-playing-screen

Conversation

@phanan
Copy link
Copy Markdown
Member

@phanan phanan commented Mar 30, 2026

Summary

  • New full-screen Radio Now Playing sheet, opened by tapping the radio mini player
  • Matches the standard Now Playing design with radio-appropriate controls:
    • Station artwork (large, with frosted blur background)
    • Stream title / station name with marquee text
    • Live indicator (red dot + "LIVE")
    • Play/pause (forward/rewind shown but disabled)
    • Volume slider
    • Drag-to-dismiss gesture
  • Intentionally omits: progress bar, download, queue, like, repeat mode, info sheet

Test plan

  • Play a radio station, tap the mini player — verify the Radio Now Playing screen opens
  • Verify station artwork, name, and stream title display correctly
  • Verify play/pause works, forward/rewind are dimmed
  • Verify "LIVE" indicator shows when playing, "Connecting…" when loading
  • Drag down to dismiss — verify it closes
  • Stop radio while screen is open — verify it auto-closes
  • Run flutter test

Summary by CodeRabbit

  • New Features

    • Added a radio "now playing" screen showing station artwork, stream info, marquee titles, connection/live indicator, playback controls, and a tappable mini-player to open it.
  • Enhancements

    • Downward drag-to-dismiss with animated panel and frosted/blurry background.
    • Saved volume is applied/synced when entering radio mode.
  • Tests

    • Added unit tests for text selection, play/loading states, auto-close behavior, and volume syncing.

A full-screen sheet for radio playback matching the standard Now Playing
design but without inapplicable controls:
- Station artwork with frosted blur background
- Stream title and station name with marquee text
- Live indicator (red dot + LIVE label)
- Play/pause control (forward/rewind disabled and dimmed)
- Volume slider
- Drag-to-dismiss gesture
- No progress bar, download, queue, like, repeat, or info sheet

Tapping the radio mini player now opens this screen.
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Mar 30, 2026

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: e1528648-e16f-48db-8707-5c51b9b567ca

📥 Commits

Reviewing files that changed from the base of the PR and between 6cd8a63 and 4ed1f66.

📒 Files selected for processing (1)
  • lib/ui/screens/radio_now_playing.dart

📝 Walkthrough

Walkthrough

Adds a new full-screen RadioNowPlayingScreen with frosted artwork backdrop, marquee titles, playback and volume controls, vertical drag-to-dismiss, wires mini-player tap to open it, and ensures persisted volume is applied to the radio player when entering radio mode.

Changes

Cohort / File(s) Summary
Now-Playing Screen
lib/ui/screens/radio_now_playing.dart
New RadioNowPlayingScreen StatefulWidget: selects station artwork (network or asset), renders blurred/frosted background, marquee text, live/connecting indicators, play/pause + volume controls, and vertical drag-to-dismiss (pop if drag > 15% height).
Screen Exports
lib/ui/screens/screens.dart
Exports the new radio_now_playing.dart file.
Mini-Player Navigation
lib/ui/widgets/mini_player.dart
Adds _openRadioNowPlaying() that pushes NowPlayingPageRoute for RadioNowPlayingScreen; wraps radio mini-player row in InkWell to open on tap; retains play/pause toggle behavior.
Audio/Volume Sync
lib/audio_handler.dart
enterRadioMode applies persisted preferences.volume to provided radio player; setVolume(double) now also propagates volume to _radioPlayer (null-safe await).
Tests
test/ui/screens/radio_now_playing_test.dart
Adds tests verifying title selection (streamTitle vs station), LIVE vs Connecting states, control enablement, mediaItemForStation mapping, auto-close when station inactive, and volume sync on entry.

Sequence Diagram

sequenceDiagram
    actor User
    participant MiniPlayer as Mini-Player
    participant Navigator
    participant NowPlaying as RadioNowPlayingScreen
    participant RadioProv as RadioPlayerProvider

    User->>MiniPlayer: tap mini-player row
    MiniPlayer->>Navigator: push(NowPlayingPageRoute)
    Navigator->>NowPlaying: build/display screen
    NowPlaying->>RadioProv: subscribe/read state (active, loading, playing, currentStation)
    RadioProv-->>NowPlaying: emits state updates
    NowPlaying->>NowPlaying: render artwork, frosted background, titles, indicators, controls
    User->>NowPlaying: drag downward
    NowPlaying->>Navigator: pop() if drag > 15% height
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Poem

🐰
A rabbit hops to the frosted light,
Tunes that sparkle through the night.
Tap the row and watch it bloom,
Drag to fold the cozy room.
Little paws applaud the tune.

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and accurately summarizes the main change: adding a new Radio Now Playing screen as a full-screen modal opened from the radio mini-player.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/radio-now-playing-screen

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

phanan added 3 commits March 30, 2026 14:46
Radio uses a separate AudioPlayer, so setVolume now also adjusts the
radio player when active. The saved volume preference is applied when
entering radio mode.
Verify text display logic, LIVE indicator states, disabled controls,
media item mapping, auto-close on inactive, and volume sync.
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@lib/ui/screens/radio_now_playing.dart`:
- Around line 21-39: The top-level Listener is capturing pointer events for the
whole screen (including VolumeSlider) and causing _onPointerMove/_onPointerUp to
fire during slider drags; move the dismiss gesture handling onto a dedicated
drag area (the header/handle) instead: remove or stop using the global Listener
for vertical drag handling and attach a localized gesture recognizer (e.g., a
GestureDetector or a Listener scoped to the header widget) that uses
onVerticalDragUpdate/onVerticalDragEnd (or calls _onPointerMove/_onPointerUp
equivalents) so only drags starting on the header trigger sheet translation;
also update the other occurrence referenced at lines 79-81 the same way so
VolumeSlider and other controls remain fully interactive.
- Around line 56-58: The code creates stationImage using
CachedNetworkImageProvider and feeds it into DecorationImage (used in
radio_now_playing.dart for the blurred background and 256px artwork) which lacks
error handling; replace those DecorationImage usages with explicit
Image/CachedNetworkImage widgets that provide errorBuilder/errorWidget fallbacks
and preserve the asset fallback (the current 'assets/images/default-image.webp')
for both the foreground artwork and the blurred background; specifically update
the logic around the stationImage variable and the widgets that render the
blurred background and the 256px artwork so they use CachedNetworkImage (or
Image.network with errorBuilder) and on error display the asset image, mirroring
the approach used in mini_player.dart (errorWidget handling).
- Around line 46-50: Add a private boolean flag (e.g. _hasBeenActive = false) to
the State of the RadioNowPlaying screen, set _hasBeenActive = true whenever
RadioPlayerProvider.active becomes true (e.g. in build or the provider listener
after play() assigns currentStation), and change the auto-close guard from if
(!radioPlayer.active) to if (!radioPlayer.active && _hasBeenActive) so the route
is only popped when the screen was previously active at least once; update
references in the State (e.g. RadioNowPlayingState / RadioNowPlayingScreen)
accordingly.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 53f80034-4884-48f4-b91c-f9998abd9d2d

📥 Commits

Reviewing files that changed from the base of the PR and between 7dbbd32 and 8576528.

📒 Files selected for processing (3)
  • lib/ui/screens/radio_now_playing.dart
  • lib/ui/screens/screens.dart
  • lib/ui/widgets/mini_player.dart

Comment thread lib/ui/screens/radio_now_playing.dart Outdated
Comment on lines +46 to +50
if (!radioPlayer.active) {
WidgetsBinding.instance.addPostFrameCallback((_) {
if (mounted) Navigator.of(context).pop();
});
return const SizedBox.shrink();
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

fd -t f "radio_player_provider.dart" -x cat -n {}

Repository: koel/player

Length of output: 6735


🏁 Script executed:

fd -t f "radio_now_playing.dart" -x cat -n {}

Repository: koel/player

Length of output: 12164


Guard the auto-close so the first inactive frame doesn't pop the route.

RadioPlayerProvider.active is only currentStation != null. When the screen first builds before currentStation is assigned in play(), the initial !radioPlayer.active frame schedules pop() via addPostFrameCallback. This means the sheet closes before the next rebuild can show the "Connecting…" state, even though playback is starting.

Track whether the screen has been active at least once before closing:

Guard with _hasBeenActive
 class _RadioNowPlayingScreenState extends State<RadioNowPlayingScreen> {
   var _dragOffset = 0.0;
+  var _hasBeenActive = false;
 
   `@override`
   Widget build(BuildContext context) {
     return Consumer<RadioPlayerProvider>(
       builder: (context, radioPlayer, _) {
-        if (!radioPlayer.active) {
+        _hasBeenActive = _hasBeenActive || radioPlayer.active;
+        if (_hasBeenActive && !radioPlayer.active) {
           WidgetsBinding.instance.addPostFrameCallback((_) {
             if (mounted) Navigator.of(context).pop();
           });
           return const SizedBox.shrink();
         }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/ui/screens/radio_now_playing.dart` around lines 46 - 50, Add a private
boolean flag (e.g. _hasBeenActive = false) to the State of the RadioNowPlaying
screen, set _hasBeenActive = true whenever RadioPlayerProvider.active becomes
true (e.g. in build or the provider listener after play() assigns
currentStation), and change the auto-close guard from if (!radioPlayer.active)
to if (!radioPlayer.active && _hasBeenActive) so the route is only popped when
the screen was previously active at least once; update references in the State
(e.g. RadioNowPlayingState / RadioNowPlayingScreen) accordingly.

Comment on lines +56 to +58
final ImageProvider<Object> stationImage = station.logo != null
? CachedNetworkImageProvider(station.logo!)
: const AssetImage('assets/images/default-image.webp') as ImageProvider<Object>;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

wc -l lib/ui/screens/radio_now_playing.dart lib/ui/widgets/mini_player.dart

Repository: koel/player

Length of output: 148


🏁 Script executed:

# Read the radio_now_playing.dart file to see lines 56-58, 68-70, 129-131
head -140 lib/ui/screens/radio_now_playing.dart | tail -85

Repository: koel/player

Length of output: 3717


🏁 Script executed:

# Read mini_player.dart around lines 147-154
sed -n '140,160p' lib/ui/widgets/mini_player.dart

Repository: koel/player

Length of output: 824


🏁 Script executed:

sed -n '120,140p' lib/ui/screens/radio_now_playing.dart | cat -n

Repository: koel/player

Length of output: 1226


🏁 Script executed:

sed -n '120,140p' lib/ui/screens/radio_now_playing.dart | cat -n

Repository: koel/player

Length of output: 1226


🏁 Script executed:

sed -n '55,75p' lib/ui/screens/radio_now_playing.dart | cat -n

Repository: koel/player

Length of output: 993


🏁 Script executed:

sed -n '125,145p' lib/ui/screens/radio_now_playing.dart | cat -n

Repository: koel/player

Length of output: 1225


🏁 Script executed:

# Let me get a clearer view of the exact line numbers for the two DecorationImage usages
grep -n "DecorationImage" lib/ui/screens/radio_now_playing.dart

Repository: koel/player

Length of output: 165


🏁 Script executed:

# Verify the mini_player.dart implementation more fully
sed -n '140,160p' lib/ui/widgets/mini_player.dart | cat -n

Repository: koel/player

Length of output: 971


🏁 Script executed:

# Also check the _radioDefaultIcon() function to understand the fallback
grep -n "_radioDefaultIcon" lib/ui/widgets/mini_player.dart -A 3

Repository: koel/player

Length of output: 387


Use Image/CachedNetworkImage with errorBuilder instead of DecorationImage for network logos.

lib/ui/widgets/mini_player.dart:147-154 properly handles failed station logo URLs with CachedNetworkImage and errorWidget, but this screen uses DecorationImage with CachedNetworkImageProvider at lines 68 and 129, which lacks error handling. If a logo URL is invalid or returns 404, both the blurred background and the 256px artwork will fail to render with no fallback. Use Image/CachedNetworkImage with errorBuilder for the foreground art and preserve an asset fallback for the background.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/ui/screens/radio_now_playing.dart` around lines 56 - 58, The code creates
stationImage using CachedNetworkImageProvider and feeds it into DecorationImage
(used in radio_now_playing.dart for the blurred background and 256px artwork)
which lacks error handling; replace those DecorationImage usages with explicit
Image/CachedNetworkImage widgets that provide errorBuilder/errorWidget fallbacks
and preserve the asset fallback (the current 'assets/images/default-image.webp')
for both the foreground artwork and the blurred background; specifically update
the logic around the stationImage variable and the widgets that render the
blurred background and the 256px artwork so they use CachedNetworkImage (or
Image.network with errorBuilder) and on error display the asset image, mirroring
the approach used in mini_player.dart (errorWidget handling).

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
lib/audio_handler.dart (1)

62-66: Consider awaiting setVolume call or document fire-and-forget pattern.

radioPlayer.setVolume() from just_audio returns a Future but is called without await. While the practical timing risk here is low—playback doesn't start until several lines later after setUrl() and play() are awaited—the pattern is inconsistent with the setVolume() method elsewhere, which properly awaits both players. Either await the call in enterRadioMode directly despite it being synchronous, or add a comment documenting the intentional fire-and-forget pattern if used consistently:

void enterRadioMode(AudioPlayer radioPlayer) {
  _isRadioMode = true;
  _radioPlayer = radioPlayer;
  radioPlayer.setVolume(preferences.volume); // Fire-and-forget; playback starts later
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/audio_handler.dart` around lines 62 - 66, enterRadioMode currently calls
radioPlayer.setVolume(preferences.volume) without awaiting the Future; make this
consistent with other places that await setVolume by changing enterRadioMode to
async (Future<void> enterRadioMode(AudioPlayer radioPlayer)) and awaiting
radioPlayer.setVolume(preferences.volume), keeping _isRadioMode and _radioPlayer
assignments as-is and updating any callers to handle the Future, or if you
deliberately want fire-and-forget, leave the signature but add a clear comment
above the setVolume call explaining the intentional non-awaited behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@lib/ui/screens/radio_now_playing.dart`:
- Around line 101-103: The Container inside SafeArea sets height: screenHeight
which can overflow because screenHeight includes safe-area insets; update the
Radio Now Playing layout by removing the explicit height or replacing height:
screenHeight with height: double.infinity (or compute usable height via
MediaQuery.of(context).size.height - MediaQuery.of(context).padding.vertical) so
the Container (in the SafeArea) expands correctly; look for the
SafeArea/Container block in radio_now_playing.dart and adjust the Container's
height property accordingly.

In `@test/ui/screens/radio_now_playing_test.dart`:
- Around line 6-98: Replace the pure unit tests with real widget tests: convert
tests using test() to testWidgets() that pump the RadioNowPlayingScreen via
WidgetTester.pumpWidget(), mock or provide a test double for RadioPlayerProvider
(and its mediaItemForStation) to control streamTitle, playing and loading
states, and assert rendered UI (primary/subtitle text, LIVE pill vs
"Connecting…", disabled forward/rewind buttons, auto-pop/pop behavior when
radioPlayer.active changes, and volume sync) by verifying widget tree and button
properties; follow the pattern in test/ui/widgets/song_card_test.dart for
mocking, pumpWidget usage, and finding text/buttons.

---

Nitpick comments:
In `@lib/audio_handler.dart`:
- Around line 62-66: enterRadioMode currently calls
radioPlayer.setVolume(preferences.volume) without awaiting the Future; make this
consistent with other places that await setVolume by changing enterRadioMode to
async (Future<void> enterRadioMode(AudioPlayer radioPlayer)) and awaiting
radioPlayer.setVolume(preferences.volume), keeping _isRadioMode and _radioPlayer
assignments as-is and updating any callers to handle the Future, or if you
deliberately want fire-and-forget, leave the signature but add a clear comment
above the setVolume call explaining the intentional non-awaited behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: b20cec3c-7464-40cf-87d8-2a19e107069c

📥 Commits

Reviewing files that changed from the base of the PR and between 8576528 and 6cd8a63.

📒 Files selected for processing (3)
  • lib/audio_handler.dart
  • lib/ui/screens/radio_now_playing.dart
  • test/ui/screens/radio_now_playing_test.dart

Comment on lines +101 to +103
SafeArea(
child: Container(
height: screenHeight,
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Container height constrains to full screen but is inside SafeArea.

Setting height: screenHeight on a Container inside SafeArea may cause overflow on devices with notches/home indicators since screenHeight includes the safe area insets. Consider using double.infinity or removing the explicit height to let the layout expand naturally within the SafeArea.

Suggested fix
                     SafeArea(
                       child: Container(
-                        height: screenHeight,
                         padding: const EdgeInsets.all(24),
                         child: Column(
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
SafeArea(
child: Container(
height: screenHeight,
SafeArea(
child: Container(
padding: const EdgeInsets.all(24),
child: Column(
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/ui/screens/radio_now_playing.dart` around lines 101 - 103, The Container
inside SafeArea sets height: screenHeight which can overflow because
screenHeight includes safe-area insets; update the Radio Now Playing layout by
removing the explicit height or replacing height: screenHeight with height:
double.infinity (or compute usable height via MediaQuery.of(context).size.height
- MediaQuery.of(context).padding.vertical) so the Container (in the SafeArea)
expands correctly; look for the SafeArea/Container block in
radio_now_playing.dart and adjust the Container's height property accordingly.

Comment on lines +6 to +98
void main() {
group('Radio Now Playing screen contract', () {
test('displays stream title as primary when available', () {
final station = RadioStation.fake(id: 's1', name: 'Jazz FM');
const streamTitle = 'Miles Davis - So What';

// Primary text: stream title when available, station name otherwise
final primary = streamTitle ?? station.name;
expect(primary, 'Miles Davis - So What');

// Subtitle: station name when stream title is set, "Radio" otherwise
final subtitle = streamTitle != null ? station.name : 'Radio';
expect(subtitle, 'Jazz FM');
});

test('displays station name as primary when no stream title', () {
final station = RadioStation.fake(id: 's1', name: 'Jazz FM');
const String? streamTitle = null;

final primary = streamTitle ?? station.name;
expect(primary, 'Jazz FM');

final subtitle = streamTitle != null ? station.name : 'Radio';
expect(subtitle, 'Radio');
});

test('LIVE indicator shown only when playing, not loading', () {
// Simulates the conditional logic in the build method
var playing = true;
var loading = false;

expect(loading, isFalse);
expect(playing, isTrue);
// → shows LIVE pill

loading = true;
playing = false;
expect(loading, isTrue);
// → shows "Connecting…"

loading = false;
playing = false;
// → shows nothing (paused)
expect(!loading && !playing, isTrue);
});

test('forward and rewind controls are disabled', () {
// The screen renders forward/rewind with onPressed: null
// This verifies the design contract
const forwardEnabled = false;
const rewindEnabled = false;

expect(forwardEnabled, isFalse);
expect(rewindEnabled, isFalse);
});

test('mediaItemForStation produces correct values for display', () {
final station = RadioStation.fake(id: 's1', name: 'Classic Rock');
final mediaItem = RadioPlayerProvider.mediaItemForStation(station);

expect(mediaItem.id, 'radio-s1');
expect(mediaItem.title, 'Classic Rock');
expect(mediaItem.artist, 'Radio');
});

test('mediaItemForStation with stream title swaps title and artist', () {
final station = RadioStation.fake(id: 's1', name: 'Classic Rock');
final mediaItem = RadioPlayerProvider.mediaItemForStation(
station,
streamTitle: 'AC/DC - Thunderstruck',
);

expect(mediaItem.title, 'AC/DC - Thunderstruck');
expect(mediaItem.artist, 'Classic Rock');
});

test('screen auto-closes when radio becomes inactive', () {
// The screen checks radioPlayer.active and pops if false.
// This verifies the contract: active=false → should pop
RadioStation? currentStation = RadioStation.fake(id: 's1');
expect(currentStation != null, isTrue); // active

currentStation = null;
expect(currentStation != null, isFalse); // inactive → pop
});

test('volume is applied to radio player on enter', () {
// audioHandler.enterRadioMode sets volume from preferences.
// This verifies the contract that volume should be synced.
const savedVolume = 0.7;
expect(savedVolume, inInclusiveRange(0.0, 1.0));
});
});
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check if there are other widget tests in the repo that could serve as a pattern
fd -e dart -p test | xargs rg -l "pumpWidget|WidgetTester" | head -5

Repository: koel/player

Length of output: 259


🏁 Script executed:

#!/bin/bash
# Read the actual test file to verify the observations
cat -n test/ui/screens/radio_now_playing_test.dart

Repository: koel/player

Length of output: 4257


🏁 Script executed:

#!/bin/bash
# Check what one of the real widget tests looks like
head -50 test/ui/widgets/song_card_test.dart

Repository: koel/player

Length of output: 1282


🏁 Script executed:

#!/bin/bash
# Check if RadioPlayerProvider exists and has mediaItemForStation method
fd -e dart -p lib | xargs rg -l "class RadioPlayerProvider" | head -1

Repository: koel/player

Length of output: 95


🏁 Script executed:

#!/bin/bash
# Look for RadioStation class to understand its structure
fd -e dart -p lib | xargs rg -l "class RadioStation" | head -1

Repository: koel/player

Length of output: 89


🏁 Script executed:

#!/bin/bash
# Read RadioPlayerProvider to check mediaItemForStation implementation
cat -n lib/providers/radio_player_provider.dart | head -100

Repository: koel/player

Length of output: 3927


🏁 Script executed:

#!/bin/bash
# Look for RadioStation.fake factory
fd -e dart -p lib | xargs rg -A 5 "factory RadioStation.fake"

Repository: koel/player

Length of output: 349


🏁 Script executed:

#!/bin/bash
# Check if there's a RadioNowPlayingScreen widget that should be tested
fd -e dart -p lib | xargs rg -l "class RadioNowPlayingScreen"

Repository: koel/player

Length of output: 92


🏁 Script executed:

#!/bin/bash
# Check if a real widget test should use pumpWidget/testWidgets
# Look at the actual structure used in widget tests
rg "pumpWidget|testWidgets" test/ui/widgets/song_card_test.dart -A 2

Repository: koel/player

Length of output: 305


Replace most tests with actual widget tests using WidgetTester.

The test file uses test() but never actually renders or interacts with the RadioNowPlayingScreen widget. Most "contract tests" only assert on local variables and constants:

  • Lines 8–30: Test local variable logic (streamTitle ?? station.name) without touching the widget.
  • Lines 32–50: Assert on local playing/loading booleans, not widget state.
  • Lines 52–60: Assert that hardcoded false constants are false.
  • Lines 82–90: Assert that null is not equal to a non-null value.
  • Lines 92–97: Assert that 0.7 is in range [0, 1].

These will pass regardless of whether RadioNowPlayingScreen is correct. Only the mediaItemForStation tests (lines 62–80) exercise real code.

Use testWidgets() and pumpWidget() to render the actual screen with a mocked RadioPlayerProvider and verify rendered text, button states, and navigation behavior. See test/ui/widgets/song_card_test.dart for the pattern.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/ui/screens/radio_now_playing_test.dart` around lines 6 - 98, Replace the
pure unit tests with real widget tests: convert tests using test() to
testWidgets() that pump the RadioNowPlayingScreen via WidgetTester.pumpWidget(),
mock or provide a test double for RadioPlayerProvider (and its
mediaItemForStation) to control streamTitle, playing and loading states, and
assert rendered UI (primary/subtitle text, LIVE pill vs "Connecting…", disabled
forward/rewind buttons, auto-pop/pop behavior when radioPlayer.active changes,
and volume sync) by verifying widget tree and button properties; follow the
pattern in test/ui/widgets/song_card_test.dart for mocking, pumpWidget usage,
and finding text/buttons.

Replace the global Listener with a GestureDetector on the content
column. GestureDetector participates in the gesture arena so the
VolumeSlider wins its own drag events while vertical drags on empty
areas still trigger the sheet dismiss.
@phanan phanan merged commit a2ba939 into master Mar 30, 2026
1 of 2 checks passed
@phanan phanan deleted the feat/radio-now-playing-screen branch March 30, 2026 13:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant