import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:spotiflac_android/utils/lyrics_parser.dart'; import 'package:spotiflac_android/widgets/aligned_lyric_pronunciation.dart'; const _primaryStyle = TextStyle(fontSize: 34, height: 1.3); const _pronunciationStyle = TextStyle(fontSize: 22, height: 1.35); LyricWord _word(String text, int start, [int? end]) => LyricWord( text: text, time: Duration(milliseconds: start), end: end == null ? null : Duration(milliseconds: end), ); LyricLine _line({int pronunciationOffset = 0}) => LyricLine( time: Duration.zero, text: '今日も歌う', words: [ _word('今', 0), _word('日', 200), _word('も', 500), _word('歌う', 800, 1500), ], romanization: 'kyou mo utau', romanizationWords: [ _word('kyo', pronunciationOffset, 200), _word('u ', 200, 400), _word('mo ', 500, 750), _word('utau', 800, 1500), ], ); LyricPronunciationLayout? _layout( LyricLine line, { double width = 320, double scale = 1, }) => LyricPronunciationLayout.measure( line: line, primaryStyle: _primaryStyle, pronunciationStyle: _pronunciationStyle, maxWidth: width, textScaler: TextScaler.linear(scale), textDirection: TextDirection.ltr, ); void main() { testWidgets('right-aligned voices move original and pronunciation together', ( tester, ) async { Future pump(TextAlign alignment) => tester.pumpWidget( MaterialApp( home: Align( alignment: Alignment.topLeft, child: SizedBox( width: 400, child: AlignedLyricPronunciation( layout: _layout(_line(), width: 400)!, visibility: 1, textAlign: alignment, primaryStyle: _primaryStyle, pronunciationStyle: _pronunciationStyle, textBuilder: (text, words, style) => Text(text, style: style), ), ), ), ), ); await pump(TextAlign.left); final original = tester.getTopLeft(find.text('今日')); final reading = tester.getTopLeft(find.text('kyou')); await pump(TextAlign.right); final shift = tester.getTopLeft(find.text('今日')) - original; expect(shift.dx, greaterThan(0)); expect(shift.dy, 0); expect(tester.getTopLeft(find.text('kyou')) - reading, shift); expect(tester.takeException(), isNull); }); test('does not guess alignment without matching word timing', () { expect(_layout(_line(pronunciationOffset: 100)), isNull); expect( _layout( const LyricLine( time: Duration.zero, text: 'Original', romanization: 'Pronunciation', ), ), isNull, ); }); for (final width in [320.0, 170.0]) { for (final scale in [1.0, 1.8]) { testWidgets( 'phrases stay paired when wrapping and hiding ($width, $scale)', (tester) async { final layout = _layout(_line(), width: width, scale: scale)!; final renderedWords = >{}; Future pump(double visibility) => tester.pumpWidget( MaterialApp( home: MediaQuery( data: MediaQueryData(textScaler: TextScaler.linear(scale)), child: Align( alignment: Alignment.topLeft, child: SizedBox( width: width, child: AlignedLyricPronunciation( layout: layout, visibility: visibility, primaryStyle: _primaryStyle, pronunciationStyle: _pronunciationStyle, textBuilder: (text, words, style) { renderedWords[text] = words; return Text(text, style: style); }, ), ), ), ), ), ); await pump(1); final block = find.byType(AlignedLyricPronunciation); final bounds = tester.getRect(block); expect( bounds.height, closeTo(layout.primaryHeight + layout.pronunciationHeight, 0.01), ); final primaryPositions = []; for (final (original, pronunciation) in [ ('今日', 'kyou'), ('も', 'mo'), ('歌う', 'utau'), ]) { final primary = tester.getRect(find.text(original)); final secondary = tester.getRect(find.text(pronunciation)); primaryPositions.add(primary.topLeft); expect(primary.left, closeTo(secondary.left, 0.01)); expect(secondary.top, greaterThanOrEqualTo(primary.bottom)); expect(secondary.right, lessThanOrEqualTo(bounds.right + 0.01)); } // Syllables stay together, and the end of a sliced phrase still // points to the next original word, not the end of the whole line. expect(renderedWords['kyou']!.map((word) => word.text), ['kyo', 'u']); expect(renderedWords['今日']!.last.end!.inMilliseconds, 500); expect(renderedWords['kyou']!.last.end!.inMilliseconds, 400); expect(renderedWords['utau']!.single.end!.inMilliseconds, 1500); final firstPosition = primaryPositions.first; for (final visibility in [0.5, 0.0]) { await pump(visibility); expect( tester.getSize(block).height, closeTo( layout.primaryHeight + layout.pronunciationHeight * visibility, 0.01, ), ); expect(tester.getTopLeft(find.text('今日')), firstPosition); expect( tester.getTopLeft(find.text('も')).dx, primaryPositions[1].dx, ); expect(tester.takeException(), isNull); } expect(find.text('kyou'), findsNothing); }, ); } } }