Skip to content

Commit 98901d7

Browse files
Ortesclaude
andcommitted
test: cover multi-track audio selection
Add tests for the new audio-track feature so the patch is exercised and codecov/patch stops reporting 0% on the changed lines: - AudioTrack model: equality, hashCode, toString, optional language - ChewieController: hasAudioTracks, setAudioTracks, selectAudioTrack, copyWith propagation, onAudioTrackChanged callback - AudioTrackDialog widget: tile rendering, selection check icon, language hint, tap-to-pick and dismiss-with-null - MaterialControls & MaterialDesktopControls: options menu surfaces the Audio entry only when >1 track and drives selection end to end Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent edc56b1 commit 98901d7

4 files changed

Lines changed: 369 additions & 0 deletions

File tree

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
import 'package:chewie/chewie.dart';
2+
import 'package:flutter_test/flutter_test.dart';
3+
import 'package:video_player/video_player.dart';
4+
5+
const _tracks = <AudioTrack>[
6+
AudioTrack(id: '0', label: 'English', language: 'en'),
7+
AudioTrack(id: '1', label: 'French', language: 'fr'),
8+
];
9+
10+
ChewieController _controllerWith({
11+
List<AudioTrack> audioTracks = const <AudioTrack>[],
12+
Object? activeAudioTrackId,
13+
void Function(AudioTrack track)? onAudioTrackChanged,
14+
}) {
15+
// autoInitialize/autoPlay stay false, so no platform plugin call is made.
16+
return ChewieController(
17+
videoPlayerController: VideoPlayerController.networkUrl(
18+
Uri.parse('https://example.com/v.mp4'),
19+
),
20+
audioTracks: audioTracks,
21+
activeAudioTrackId: activeAudioTrackId,
22+
onAudioTrackChanged: onAudioTrackChanged,
23+
);
24+
}
25+
26+
void main() {
27+
// Needed so VideoPlayerController construction and setLooping (a no-op while
28+
// uninitialized) don't trip over the missing test binding.
29+
TestWidgetsFlutterBinding.ensureInitialized();
30+
31+
group('ChewieController.hasAudioTracks', () {
32+
test('is false with no tracks', () {
33+
expect(_controllerWith().hasAudioTracks, isFalse);
34+
});
35+
36+
test('is false with a single track (nothing to choose)', () {
37+
expect(
38+
_controllerWith(
39+
audioTracks: const [AudioTrack(id: '0', label: 'A')],
40+
).hasAudioTracks,
41+
isFalse,
42+
);
43+
});
44+
45+
test('is true with two or more tracks', () {
46+
expect(_controllerWith(audioTracks: _tracks).hasAudioTracks, isTrue);
47+
});
48+
});
49+
50+
test('setAudioTracks replaces the tracks and notifies listeners', () {
51+
final controller = _controllerWith();
52+
var notified = 0;
53+
controller.addListener(() => notified++);
54+
55+
controller.setAudioTracks(_tracks);
56+
57+
expect(controller.audioTracks, _tracks);
58+
expect(controller.hasAudioTracks, isTrue);
59+
expect(notified, 1);
60+
});
61+
62+
test(
63+
'selectAudioTrack updates the active id, fires the callback and notifies',
64+
() {
65+
AudioTrack? selected;
66+
final controller = _controllerWith(
67+
audioTracks: _tracks,
68+
onAudioTrackChanged: (track) => selected = track,
69+
);
70+
var notified = 0;
71+
controller.addListener(() => notified++);
72+
73+
controller.selectAudioTrack(_tracks[1]);
74+
75+
expect(controller.activeAudioTrackId, '1');
76+
expect(selected, _tracks[1]);
77+
expect(notified, 1);
78+
},
79+
);
80+
81+
test('selectAudioTrack works without an onAudioTrackChanged callback', () {
82+
final controller = _controllerWith(audioTracks: _tracks);
83+
expect(() => controller.selectAudioTrack(_tracks[0]), returnsNormally);
84+
expect(controller.activeAudioTrackId, '0');
85+
});
86+
87+
test('copyWith carries the audio-track fields over', () {
88+
final controller = _controllerWith();
89+
final copy = controller.copyWith(
90+
audioTracks: _tracks,
91+
activeAudioTrackId: '1',
92+
);
93+
94+
expect(copy.audioTracks, _tracks);
95+
expect(copy.activeAudioTrackId, '1');
96+
});
97+
98+
test('copyWith keeps the original audio-track fields when omitted', () {
99+
final controller = _controllerWith(
100+
audioTracks: _tracks,
101+
activeAudioTrackId: '0',
102+
);
103+
final copy = controller.copyWith(autoPlay: false);
104+
105+
expect(copy.audioTracks, _tracks);
106+
expect(copy.activeAudioTrackId, '0');
107+
});
108+
}
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
import 'package:chewie/chewie.dart';
2+
import 'package:flutter/material.dart';
3+
import 'package:flutter_test/flutter_test.dart';
4+
import 'package:video_player/video_player.dart';
5+
6+
const _tracks = <AudioTrack>[
7+
AudioTrack(id: '0', label: 'English', language: 'en'),
8+
AudioTrack(id: '1', label: 'French', language: 'fr'),
9+
];
10+
11+
/// Builds a [ChewieController] that never touches the video_player platform
12+
/// (autoInitialize/autoPlay stay false) but exposes the two audio tracks.
13+
ChewieController _controller(
14+
Widget controls, {
15+
void Function(AudioTrack)? onChanged,
16+
}) {
17+
return ChewieController(
18+
videoPlayerController: VideoPlayerController.networkUrl(
19+
Uri.parse('https://example.com/v.mp4'),
20+
),
21+
autoPlay: false,
22+
autoInitialize: false,
23+
audioTracks: _tracks,
24+
activeAudioTrackId: '0',
25+
onAudioTrackChanged: onChanged,
26+
customControls: controls,
27+
);
28+
}
29+
30+
/// Opens the options menu via [optionsIcon], taps the "Audio" entry, then picks
31+
/// "French" — exercising the controls' audio-track code path end to end.
32+
Future<void> _runFlow(
33+
WidgetTester tester, {
34+
required Widget controls,
35+
required IconData optionsIcon,
36+
}) async {
37+
AudioTrack? chosen;
38+
final controller = _controller(controls, onChanged: (t) => chosen = t);
39+
40+
await tester.pumpWidget(
41+
MaterialApp(
42+
home: Scaffold(body: Chewie(controller: controller)),
43+
),
44+
);
45+
// Let the show-controls-on-initialize timer fire so the controls become
46+
// interactive (otherwise AbsorbPointer swallows the tap).
47+
await tester.pump(const Duration(milliseconds: 300));
48+
49+
// Open the options bottom sheet.
50+
await tester.tap(find.byIcon(optionsIcon));
51+
await tester.pumpAndSettle();
52+
expect(find.text('Audio'), findsOneWidget);
53+
54+
// Open the audio-track dialog.
55+
await tester.tap(find.text('Audio'));
56+
await tester.pumpAndSettle();
57+
expect(find.text('English'), findsOneWidget);
58+
expect(find.text('French'), findsOneWidget);
59+
60+
// Pick a track.
61+
await tester.tap(find.text('French'));
62+
await tester.pumpAndSettle();
63+
64+
expect(chosen, _tracks[1]);
65+
expect(controller.activeAudioTrackId, '1');
66+
}
67+
68+
void main() {
69+
testWidgets('MaterialControls: options menu exposes audio-track selection', (
70+
tester,
71+
) async {
72+
await _runFlow(
73+
tester,
74+
controls: const MaterialControls(),
75+
optionsIcon: Icons.more_vert,
76+
);
77+
});
78+
79+
testWidgets(
80+
'MaterialDesktopControls: options menu exposes audio-track selection',
81+
(tester) async {
82+
await _runFlow(
83+
tester,
84+
controls: const MaterialDesktopControls(),
85+
optionsIcon: Icons.settings,
86+
);
87+
},
88+
);
89+
90+
testWidgets('the Audio entry is hidden when there is only one track', (
91+
tester,
92+
) async {
93+
final controller = ChewieController(
94+
videoPlayerController: VideoPlayerController.networkUrl(
95+
Uri.parse('https://example.com/v.mp4'),
96+
),
97+
autoPlay: false,
98+
autoInitialize: false,
99+
audioTracks: const [AudioTrack(id: '0', label: 'English')],
100+
customControls: const MaterialControls(),
101+
);
102+
103+
await tester.pumpWidget(
104+
MaterialApp(
105+
home: Scaffold(body: Chewie(controller: controller)),
106+
),
107+
);
108+
await tester.pump(const Duration(milliseconds: 300));
109+
110+
await tester.tap(find.byIcon(Icons.more_vert));
111+
await tester.pumpAndSettle();
112+
113+
expect(find.text('Audio'), findsNothing);
114+
});
115+
}
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
import 'package:chewie/chewie.dart';
2+
import 'package:chewie/src/material/widgets/audio_track_dialog.dart';
3+
import 'package:flutter/material.dart';
4+
import 'package:flutter_test/flutter_test.dart';
5+
6+
const _tracks = <AudioTrack>[
7+
AudioTrack(id: '0', label: 'English', language: 'en'),
8+
AudioTrack(id: '1', label: 'French', language: 'fr'),
9+
AudioTrack(id: '2', label: 'No language'),
10+
];
11+
12+
/// Pumps a screen with a button that opens the [AudioTrackDialog] in a modal
13+
/// bottom sheet, taps it, and lets the caller inspect/interact with the sheet.
14+
/// The chosen track (or null) lands in [resultHolder] once the sheet closes.
15+
Future<void> _openDialog(
16+
WidgetTester tester, {
17+
required Object? selectedId,
18+
required List<AudioTrack?> resultHolder,
19+
}) async {
20+
await tester.pumpWidget(
21+
MaterialApp(
22+
home: Scaffold(
23+
body: Builder(
24+
builder: (context) => Center(
25+
child: ElevatedButton(
26+
onPressed: () async {
27+
final track = await showModalBottomSheet<AudioTrack>(
28+
context: context,
29+
builder: (_) =>
30+
AudioTrackDialog(tracks: _tracks, selectedId: selectedId),
31+
);
32+
resultHolder.add(track);
33+
},
34+
child: const Text('open'),
35+
),
36+
),
37+
),
38+
),
39+
),
40+
);
41+
42+
await tester.tap(find.text('open'));
43+
await tester.pumpAndSettle();
44+
}
45+
46+
void main() {
47+
testWidgets('renders one tile per track with its label', (tester) async {
48+
await _openDialog(tester, selectedId: '0', resultHolder: []);
49+
50+
expect(find.byType(ListTile), findsNWidgets(_tracks.length));
51+
expect(find.text('English'), findsOneWidget);
52+
expect(find.text('French'), findsOneWidget);
53+
expect(find.text('No language'), findsOneWidget);
54+
});
55+
56+
testWidgets('shows a check icon next to the selected track only', (
57+
tester,
58+
) async {
59+
await _openDialog(tester, selectedId: '1', resultHolder: []);
60+
61+
expect(find.byIcon(Icons.check), findsOneWidget);
62+
});
63+
64+
testWidgets('shows the language code only for tracks that have one', (
65+
tester,
66+
) async {
67+
await _openDialog(tester, selectedId: null, resultHolder: []);
68+
69+
expect(find.text('en'), findsOneWidget);
70+
expect(find.text('fr'), findsOneWidget);
71+
});
72+
73+
testWidgets('tapping a track pops the sheet with that track', (tester) async {
74+
final result = <AudioTrack?>[];
75+
await _openDialog(tester, selectedId: '0', resultHolder: result);
76+
77+
await tester.tap(find.text('French'));
78+
await tester.pumpAndSettle();
79+
80+
expect(result, [_tracks[1]]);
81+
});
82+
83+
testWidgets('dismissing the sheet pops with null', (tester) async {
84+
final result = <AudioTrack?>[];
85+
await _openDialog(tester, selectedId: '0', resultHolder: result);
86+
87+
// Tap the scrim above the sheet to dismiss it.
88+
await tester.tapAt(const Offset(10, 10));
89+
await tester.pumpAndSettle();
90+
91+
expect(result, [null]);
92+
});
93+
}

test/models/audio_track_test.dart

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import 'package:chewie/chewie.dart';
2+
import 'package:flutter_test/flutter_test.dart';
3+
4+
void main() {
5+
group('AudioTrack equality', () {
6+
test('two tracks with the same id, label and language are equal', () {
7+
const a = AudioTrack(id: '0', label: 'English', language: 'en');
8+
const b = AudioTrack(id: '0', label: 'English', language: 'en');
9+
expect(a, equals(b));
10+
expect(a.hashCode, b.hashCode);
11+
});
12+
13+
test('tracks differing in id are not equal', () {
14+
const a = AudioTrack(id: '0', label: 'English', language: 'en');
15+
const b = AudioTrack(id: '1', label: 'English', language: 'en');
16+
expect(a, isNot(equals(b)));
17+
});
18+
19+
test('tracks differing in label are not equal', () {
20+
const a = AudioTrack(id: '0', label: 'English', language: 'en');
21+
const b = AudioTrack(id: '0', label: 'Anglais', language: 'en');
22+
expect(a, isNot(equals(b)));
23+
});
24+
25+
test('tracks differing in language are not equal', () {
26+
const a = AudioTrack(id: '0', label: 'English', language: 'en');
27+
const b = AudioTrack(id: '0', label: 'English', language: 'fr');
28+
expect(a, isNot(equals(b)));
29+
});
30+
31+
test('an AudioTrack is not equal to another type', () {
32+
const a = AudioTrack(id: '0', label: 'English');
33+
expect(a == Object(), isFalse);
34+
});
35+
36+
test('the id may be any Object, not just a String', () {
37+
const a = AudioTrack(id: 0, label: 'English');
38+
const b = AudioTrack(id: 0, label: 'English');
39+
expect(a, equals(b));
40+
expect(a.hashCode, b.hashCode);
41+
});
42+
});
43+
44+
test('language is optional and defaults to null', () {
45+
const track = AudioTrack(id: '0', label: 'English');
46+
expect(track.language, isNull);
47+
});
48+
49+
test('toString lists the fields', () {
50+
const track = AudioTrack(id: '0', label: 'English', language: 'en');
51+
expect(track.toString(), 'AudioTrack(id: 0, label: English, language: en)');
52+
});
53+
}

0 commit comments

Comments
 (0)