main
dart 121 lines 3.59 KB
Raw
1 import 'package:flutter/material.dart';
2 import 'package:flutter_riverpod/flutter_riverpod.dart';
3 import 'package:flutter_test/flutter_test.dart';
4 import 'package:localmind/core/models/enums.dart';
5 import 'package:localmind/features/tts/providers/tts_providers.dart';
6 import 'package:localmind/features/voice_mode/providers/voice_mode_provider.dart';
7 import 'package:localmind/features/voice_mode/views/components/voice_transcript.dart';
8
9 void main() {
10 testWidgets('highlights the word reported by native TTS progress', (
11 tester,
12 ) async {
13 const response = 'Hello world from LocalMind';
14 const ttsState = TtsState(
15 isSpeaking: true,
16 activeEngine: EngineId.system,
17 playingContent: response,
18 chunks: [response],
19 currentChunkIndex: 0,
20 spokenCharOffset: 6,
21 );
22
23 await tester.pumpWidget(
24 ProviderScope(
25 overrides: [ttsProvider.overrideWith(() => _StubTtsNotifier(ttsState))],
26 child: const MaterialApp(
27 home: Scaffold(
28 body: VoiceTranscript(
29 phase: VoiceModePhase.speaking,
30 transcript: '',
31 response: response,
32 ),
33 ),
34 ),
35 ),
36 );
37
38 final richText = tester.widget<RichText>(find.byType(RichText).last);
39 final root = richText.text as TextSpan;
40 final wordSpan = root.children!.whereType<TextSpan>().singleWhere(
41 (span) => span.text == 'world',
42 );
43
44 expect(wordSpan.style?.fontWeight, FontWeight.w700);
45 expect(wordSpan.style?.color, isNot(root.style?.color));
46 });
47
48 test('resetProgress clears a native spoken character offset', () {
49 const state = TtsState(spokenCharOffset: 12);
50
51 expect(state.copyWith(resetProgress: true).spokenCharOffset, -1);
52 });
53
54 testWidgets('auto-scrolls to keep a later spoken word visible', (
55 tester,
56 ) async {
57 final response = List.generate(80, (index) => 'word$index').join(' ');
58 final activeOffset = response.indexOf('word70');
59 final ttsState = TtsState(
60 isSpeaking: true,
61 activeEngine: EngineId.system,
62 playingContent: response,
63 chunks: [response],
64 currentChunkIndex: 0,
65 spokenCharOffset: 0,
66 );
67 final notifier = _StubTtsNotifier(ttsState);
68
69 await tester.pumpWidget(
70 ProviderScope(
71 overrides: [ttsProvider.overrideWith(() => notifier)],
72 child: MaterialApp(
73 home: Scaffold(
74 body: SizedBox(
75 width: 320,
76 child: VoiceTranscript(
77 phase: VoiceModePhase.speaking,
78 transcript: '',
79 response: response,
80 ),
81 ),
82 ),
83 ),
84 ),
85 );
86
87 notifier.setSpokenCharOffset(activeOffset);
88 await tester.pump();
89
90 final updatedText = tester.widget<RichText>(find.byType(RichText).last);
91 final updatedRoot = updatedText.text as TextSpan;
92 final activeWord = updatedRoot.children!.whereType<TextSpan>().singleWhere(
93 (span) => span.text == 'word70',
94 );
95 expect(activeWord.style?.fontWeight, FontWeight.w700);
96
97 await tester.pump();
98 await tester.pump(const Duration(milliseconds: 300));
99
100 final scrollView = tester.widget<SingleChildScrollView>(
101 find.byKey(const ValueKey('speaking-scroll')),
102 );
103 expect(scrollView.controller!.offset, greaterThan(0));
104 });
105 }
106
107 class _StubTtsNotifier extends TtsNotifier {
108 _StubTtsNotifier(this.initialState);
109
110 final TtsState initialState;
111
112 @override
113 TtsState build() => initialState;
114
115 void setSpokenCharOffset(int offset) {
116 state = state.copyWith(
117 spokenCharOffset: offset,
118 playingContent: state.playingContent,
119 );
120 }
121 }