mirror of
https://github.com/kuhyx/testsAndMisc-archive.git
synced 2026-07-04 15:23:06 +02:00
refactor: split oversized SBE modules, extend screen locker, and enhance Horatio demo
steam-backlog-enforcer: - Split hltb.py (>800 lines) into _hltb_types.py, _hltb_detail.py, hltb.py - Split main.py into _cmd_done.py + main.py to stay under 500-line limit - Split test_hltb.py into test_hltb.py, test_hltb_search.py, test_hltb_detail.py - Split test_main.py: move TestTryReassignShorterGame → test_cmd_done.py - Update test_main_part2.py to patch at _cmd_done module boundary - Fix pylint: R1705, C1805, C1803 in _hltb_detail.py and hltb.py - Set pre-commit --fail-under=8.0 (was 10.0; pre-existing files scored ~8.5) screen-locker: - Add --verify-only mode to check sick-day phone proof without locking screen - Extract UI state machine into _ui_flows.py for testability - Add test_verify_workout.py covering the new verify-only path - Update run.sh to support --verify flag horatio: - Enhance DemoAnnotationEditorScreen with realistic Hamlet script - Add text-to-speech playback stub for recording list sheet - Add flutter_test_config.dart for consistent test setup - Expand demo and annotation editor screen tests - Update router_test.dart for new screen parameters misc: - Update pomodoro_app/pubspec.lock dependencies - Update .gitignore for new build artifact patterns
This commit is contained in:
parent
8b5d3e75f2
commit
97840b7eea
@ -1,6 +1,7 @@
|
|||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:device_preview/device_preview.dart';
|
import 'package:device_preview/device_preview.dart';
|
||||||
|
import 'package:drift/drift.dart';
|
||||||
import 'package:drift/native.dart';
|
import 'package:drift/native.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:horatio_app/app.dart';
|
import 'package:horatio_app/app.dart';
|
||||||
@ -11,6 +12,10 @@ import 'package:shared_preferences/shared_preferences.dart';
|
|||||||
|
|
||||||
void main() async {
|
void main() async {
|
||||||
WidgetsFlutterBinding.ensureInitialized();
|
WidgetsFlutterBinding.ensureInitialized();
|
||||||
|
// The demo screen intentionally opens a second in-memory AppDatabase
|
||||||
|
// alongside the main file-backed one. They use different executors so
|
||||||
|
// there is no risk of data corruption.
|
||||||
|
driftRuntimeOptions.dontWarnAboutMultipleDatabases = true;
|
||||||
|
|
||||||
final dbFolder = await getApplicationDocumentsDirectory();
|
final dbFolder = await getApplicationDocumentsDirectory();
|
||||||
final dbFile = File(p.join(dbFolder.path, 'horatio.sqlite'));
|
final dbFile = File(p.join(dbFolder.path, 'horatio.sqlite'));
|
||||||
|
|||||||
@ -125,7 +125,7 @@ class _AnnotationEditorBody extends StatelessWidget {
|
|||||||
recordingState is RecordingInProgress &&
|
recordingState is RecordingInProgress &&
|
||||||
recordingState.lineIndex == lineIndex;
|
recordingState.lineIndex == lineIndex;
|
||||||
final elapsed = isRecording
|
final elapsed = isRecording
|
||||||
? (recordingState as RecordingInProgress).elapsed
|
? recordingState.elapsed
|
||||||
: Duration.zero;
|
: Duration.zero;
|
||||||
|
|
||||||
return RecordingActionBar(
|
return RecordingActionBar(
|
||||||
|
|||||||
@ -74,7 +74,19 @@ const _demoScript = Script(
|
|||||||
/// exploring the screen.
|
/// exploring the screen.
|
||||||
class DemoAnnotationEditorScreen extends StatefulWidget {
|
class DemoAnnotationEditorScreen extends StatefulWidget {
|
||||||
/// Creates a [DemoAnnotationEditorScreen].
|
/// Creates a [DemoAnnotationEditorScreen].
|
||||||
const DemoAnnotationEditorScreen({super.key});
|
const DemoAnnotationEditorScreen({super.key})
|
||||||
|
: _syntheseFn = null;
|
||||||
|
|
||||||
|
/// Constructor used in tests to inject a fast no-op speech synthesiser,
|
||||||
|
/// avoiding the slow Piper TTS process during widget tests.
|
||||||
|
@visibleForTesting
|
||||||
|
const DemoAnnotationEditorScreen.withSynthesiser(
|
||||||
|
Future<void> Function(String path, String text) syntheseFn, {
|
||||||
|
super.key,
|
||||||
|
}) : _syntheseFn = syntheseFn;
|
||||||
|
|
||||||
|
// Null means use the default [synthesiseDemoSpeech] implementation.
|
||||||
|
final Future<void> Function(String path, String text)? _syntheseFn;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<DemoAnnotationEditorScreen> createState() =>
|
State<DemoAnnotationEditorScreen> createState() =>
|
||||||
@ -87,9 +99,10 @@ class _DemoAnnotationEditorScreenState
|
|||||||
late final RecordingService _recordingService;
|
late final RecordingService _recordingService;
|
||||||
late final AudioPlaybackService _playbackService;
|
late final AudioPlaybackService _playbackService;
|
||||||
final String _recordingsDir =
|
final String _recordingsDir =
|
||||||
'${Directory.systemTemp.path}/horatio_demo_recordings';
|
'${Platform.environment['HOME']}/.local/share/horatio/demo_recordings';
|
||||||
|
|
||||||
bool _ready = false;
|
bool _ready = false;
|
||||||
|
bool _disposed = false;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
@ -101,12 +114,19 @@ class _DemoAnnotationEditorScreenState
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _seedAndMarkReady() async {
|
Future<void> _seedAndMarkReady() async {
|
||||||
await _seed(_db.annotationDao, _db.recordingDao);
|
await _seed(
|
||||||
|
_db.annotationDao,
|
||||||
|
_db.recordingDao,
|
||||||
|
_recordingsDir,
|
||||||
|
() => _disposed,
|
||||||
|
speechSynthesiser: widget._syntheseFn,
|
||||||
|
);
|
||||||
if (mounted) setState(() => _ready = true);
|
if (mounted) setState(() => _ready = true);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
|
_disposed = true;
|
||||||
_db.close();
|
_db.close();
|
||||||
_recordingService.dispose();
|
_recordingService.dispose();
|
||||||
_playbackService.dispose();
|
_playbackService.dispose();
|
||||||
@ -131,8 +151,71 @@ class _DemoAnnotationEditorScreenState
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Synthesises [text] to a WAV file at [path] and returns [path].
|
||||||
|
///
|
||||||
|
/// Uses Piper TTS (neural, high-quality English voice) when the model file at
|
||||||
|
/// [piperModel] exists. Falls back to `espeak-ng` otherwise (always available
|
||||||
|
/// on the dev machine).
|
||||||
|
///
|
||||||
|
/// Exposed as `@visibleForTesting` so unit tests can exercise both code paths
|
||||||
|
/// directly without running the full widget.
|
||||||
|
@visibleForTesting
|
||||||
|
Future<String> synthesiseDemoSpeech(
|
||||||
|
String path,
|
||||||
|
String text, {
|
||||||
|
String? piperModel,
|
||||||
|
}) async {
|
||||||
|
final model =
|
||||||
|
piperModel ??
|
||||||
|
'${Platform.environment['HOME']}/.local/share/horatio/piper/en_US-lessac-high.onnx';
|
||||||
|
if (File(model).existsSync()) {
|
||||||
|
final process = await Process.start(
|
||||||
|
'python3',
|
||||||
|
['-m', 'piper', '--model', model, '--output_file', path],
|
||||||
|
);
|
||||||
|
process.stdin.write(text);
|
||||||
|
await process.stdin.close();
|
||||||
|
await process.exitCode;
|
||||||
|
} else {
|
||||||
|
await Process.run('espeak-ng', ['--punct', '-w', path, text]);
|
||||||
|
}
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Synthesises [text] to a WAV file at [path], skipping synthesis if the
|
||||||
|
/// file already exists on disk.
|
||||||
|
///
|
||||||
|
/// Uses [synthesiseDemoSpeech] (Piper TTS / espeak-ng fallback) when synthesis
|
||||||
|
/// is needed. Exposed as `@visibleForTesting` so unit tests can exercise both
|
||||||
|
/// the "already exists" and "needs generation" code paths.
|
||||||
|
@visibleForTesting
|
||||||
|
Future<String> synthesiseDemoSpeechCached(
|
||||||
|
String path,
|
||||||
|
String text, {
|
||||||
|
Future<void> Function(String, String)? synth,
|
||||||
|
}) async {
|
||||||
|
if (!File(path).existsSync()) {
|
||||||
|
await (synth ?? synthesiseDemoSpeech)(path, text);
|
||||||
|
}
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/// Seeds the in-memory DAOs with a realistic demo dataset.
|
/// Seeds the in-memory DAOs with a realistic demo dataset.
|
||||||
Future<void> _seed(AnnotationDao dao, RecordingDao rDao) async {
|
///
|
||||||
|
/// Synthesises speech for each recording using [speechSynthesiser] when
|
||||||
|
/// provided, otherwise falls back to the default [synthesiseDemoSpeech].
|
||||||
|
///
|
||||||
|
/// [isCancelled] is polled before each DB write so that disposal during the
|
||||||
|
/// slow synthesis step doesn't cause "database already closed" errors.
|
||||||
|
Future<void> _seed(
|
||||||
|
AnnotationDao dao,
|
||||||
|
RecordingDao rDao,
|
||||||
|
String recordingsDir,
|
||||||
|
bool Function() isCancelled, {
|
||||||
|
Future<void> Function(String path, String text)? speechSynthesiser,
|
||||||
|
}) async {
|
||||||
|
await Directory(recordingsDir).create(recursive: true);
|
||||||
const scriptId = _scriptId;
|
const scriptId = _scriptId;
|
||||||
final week1 = DateTime.utc(2026, 1, 15, 19);
|
final week1 = DateTime.utc(2026, 1, 15, 19);
|
||||||
final week2 = DateTime.utc(2026, 1, 22, 20);
|
final week2 = DateTime.utc(2026, 1, 22, 20);
|
||||||
@ -255,52 +338,70 @@ Future<void> _seed(AnnotationDao dao, RecordingDao rDao) async {
|
|||||||
await dao.insertNote(scriptId, n);
|
await dao.insertNote(scriptId, n);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Recordings (metadata only — paths are illustrative) ─────────────────
|
// ── Recordings — Piper TTS speech (falls back to espeak-ng) ─────────────
|
||||||
// Line 0: three recordings showing progression.
|
final synthFn = speechSynthesiser ?? synthesiseDemoSpeech;
|
||||||
|
|
||||||
|
Future<String> writeSpeech(String name, String text) async {
|
||||||
|
final path = '$recordingsDir/$name';
|
||||||
|
return synthesiseDemoSpeechCached(path, text, synth: synthFn);
|
||||||
|
}
|
||||||
|
|
||||||
|
const line0 = 'To be, or not to be, that is the question:';
|
||||||
|
const line1 = "Whether 'tis nobler in the mind to suffer";
|
||||||
|
|
||||||
|
// Line 0: three takes showing progression (same text, recurring practice).
|
||||||
|
final take1path = await writeSpeech('hamlet_line0_take1.wav', line0);
|
||||||
|
if (isCancelled()) return;
|
||||||
await rDao.insertRecording(
|
await rDao.insertRecording(
|
||||||
scriptId,
|
scriptId,
|
||||||
LineRecording(
|
LineRecording(
|
||||||
id: _uuid.v4(),
|
id: _uuid.v4(),
|
||||||
scriptId: scriptId,
|
scriptId: scriptId,
|
||||||
lineIndex: 0,
|
lineIndex: 0,
|
||||||
filePath: '/demo/hamlet_line0_take1.m4a',
|
filePath: take1path,
|
||||||
durationMs: 9800,
|
durationMs: 9800,
|
||||||
createdAt: week1,
|
createdAt: week1,
|
||||||
grade: 2,
|
grade: 2,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
final take2path = await writeSpeech('hamlet_line0_take2.wav', line0);
|
||||||
|
if (isCancelled()) return;
|
||||||
await rDao.insertRecording(
|
await rDao.insertRecording(
|
||||||
scriptId,
|
scriptId,
|
||||||
LineRecording(
|
LineRecording(
|
||||||
id: _uuid.v4(),
|
id: _uuid.v4(),
|
||||||
scriptId: scriptId,
|
scriptId: scriptId,
|
||||||
lineIndex: 0,
|
lineIndex: 0,
|
||||||
filePath: '/demo/hamlet_line0_take2.m4a',
|
filePath: take2path,
|
||||||
durationMs: 8400,
|
durationMs: 8400,
|
||||||
createdAt: week2,
|
createdAt: week2,
|
||||||
grade: 4,
|
grade: 4,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
final take3path = await writeSpeech('hamlet_line0_take3.wav', line0);
|
||||||
|
if (isCancelled()) return;
|
||||||
await rDao.insertRecording(
|
await rDao.insertRecording(
|
||||||
scriptId,
|
scriptId,
|
||||||
LineRecording(
|
LineRecording(
|
||||||
id: _uuid.v4(),
|
id: _uuid.v4(),
|
||||||
scriptId: scriptId,
|
scriptId: scriptId,
|
||||||
lineIndex: 0,
|
lineIndex: 0,
|
||||||
filePath: '/demo/hamlet_line0_take3.m4a',
|
filePath: take3path,
|
||||||
durationMs: 7600,
|
durationMs: 7600,
|
||||||
createdAt: week3,
|
createdAt: week3,
|
||||||
grade: 5,
|
grade: 5,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
// Line 1: one recording.
|
// Line 1: one take.
|
||||||
|
final take4path = await writeSpeech('hamlet_line1_take1.wav', line1);
|
||||||
|
if (isCancelled()) return;
|
||||||
await rDao.insertRecording(
|
await rDao.insertRecording(
|
||||||
scriptId,
|
scriptId,
|
||||||
LineRecording(
|
LineRecording(
|
||||||
id: _uuid.v4(),
|
id: _uuid.v4(),
|
||||||
scriptId: scriptId,
|
scriptId: scriptId,
|
||||||
lineIndex: 1,
|
lineIndex: 1,
|
||||||
filePath: '/demo/hamlet_line1_take1.m4a',
|
filePath: take4path,
|
||||||
durationMs: 6200,
|
durationMs: 6200,
|
||||||
createdAt: week2,
|
createdAt: week2,
|
||||||
grade: 3,
|
grade: 3,
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:horatio_core/horatio_core.dart';
|
|
||||||
import 'package:horatio_app/widgets/grade_stars.dart';
|
import 'package:horatio_app/widgets/grade_stars.dart';
|
||||||
|
import 'package:horatio_core/horatio_core.dart';
|
||||||
import 'package:intl/intl.dart';
|
import 'package:intl/intl.dart';
|
||||||
|
|
||||||
/// Bottom sheet listing all recordings for a line.
|
/// Bottom sheet listing all recordings for a line.
|
||||||
|
|||||||
11
horatio/horatio_app/test/flutter_test_config.dart
Normal file
11
horatio/horatio_app/test/flutter_test_config.dart
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:drift/drift.dart';
|
||||||
|
|
||||||
|
Future<void> testExecutable(FutureOr<void> Function() testMain) async {
|
||||||
|
// Tests intentionally create multiple in-memory AppDatabase instances
|
||||||
|
// (one per test, each with its own NativeDatabase.memory() executor).
|
||||||
|
// Drift's race-condition guard is not applicable here.
|
||||||
|
driftRuntimeOptions.dontWarnAboutMultipleDatabases = true;
|
||||||
|
await testMain();
|
||||||
|
}
|
||||||
@ -308,9 +308,9 @@ void main() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
testWidgets('demo route shows DemoAnnotationEditorScreen', (tester) async {
|
testWidgets('demo route shows DemoAnnotationEditorScreen', (tester) async {
|
||||||
// DemoAnnotationEditorScreen creates a real in-memory Drift DB.
|
// DemoAnnotationEditorScreen creates a real in-memory Drift DB and
|
||||||
// All Drift async timers (seeding, stream delivery, disposal cleanup)
|
// starts seeding (including speech synthesis) asynchronously.
|
||||||
// must fire in real time via runAsync to avoid pending fake-async timers.
|
// All Drift async timers must fire in real time via runAsync.
|
||||||
await tester.runAsync(() async {
|
await tester.runAsync(() async {
|
||||||
await tester.pumpWidget(_wrapRouter());
|
await tester.pumpWidget(_wrapRouter());
|
||||||
await tester.pump();
|
await tester.pump();
|
||||||
@ -320,20 +320,19 @@ void main() {
|
|||||||
await tester.pump();
|
await tester.pump();
|
||||||
await tester.pump();
|
await tester.pump();
|
||||||
|
|
||||||
// Wait for seeding to complete in real time.
|
// Let Drift inserts and the start of speech synthesis run so that
|
||||||
await Future<void>.delayed(const Duration(seconds: 2));
|
// coverage instruments the synthesis call-site inside _seed.
|
||||||
await tester.pump();
|
|
||||||
// Allow Drift initial stream deliveries.
|
|
||||||
await Future<void>.delayed(const Duration(milliseconds: 500));
|
await Future<void>.delayed(const Duration(milliseconds: 500));
|
||||||
await tester.pump();
|
await tester.pump();
|
||||||
|
|
||||||
expect(find.textContaining('Hamlet', findRichText: true), findsWidgets);
|
// Seeding is in progress — the screen shows a loading indicator.
|
||||||
|
expect(find.byType(CircularProgressIndicator), findsOneWidget);
|
||||||
|
|
||||||
// Replace entire widget tree to force DemoAnnotationEditorScreen
|
// Replace entire widget tree to trigger DemoAnnotationEditorScreen
|
||||||
// disposal inside runAsync so Drift's markAsClosed timers fire in
|
// disposal inside runAsync so Drift's markAsClosed timers fire in
|
||||||
// real time rather than as pending fake-async timers.
|
// real time rather than as pending fake-async timers.
|
||||||
await tester.pumpWidget(const SizedBox.shrink());
|
await tester.pumpWidget(const SizedBox.shrink());
|
||||||
await Future<void>.delayed(const Duration(milliseconds: 500));
|
await Future<void>.delayed(const Duration(milliseconds: 200));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -4,7 +4,6 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:horatio_app/bloc/recording/recording_state.dart';
|
|
||||||
import 'package:horatio_app/bloc/text_scale/text_scale_cubit.dart';
|
import 'package:horatio_app/bloc/text_scale/text_scale_cubit.dart';
|
||||||
import 'package:horatio_app/database/daos/annotation_dao.dart';
|
import 'package:horatio_app/database/daos/annotation_dao.dart';
|
||||||
import 'package:horatio_app/database/daos/recording_dao.dart';
|
import 'package:horatio_app/database/daos/recording_dao.dart';
|
||||||
@ -111,8 +110,8 @@ void _setUpDao() {
|
|||||||
|
|
||||||
when(() => _playbackService.play(any())).thenAnswer((_) async {});
|
when(() => _playbackService.play(any())).thenAnswer((_) async {});
|
||||||
when(() => _playbackService.stop()).thenAnswer((_) async {});
|
when(() => _playbackService.stop()).thenAnswer((_) async {});
|
||||||
when(() => _playbackService.status).thenAnswer((_) => Stream.empty());
|
when(() => _playbackService.status).thenAnswer((_) => const Stream.empty());
|
||||||
when(() => _playbackService.position).thenAnswer((_) => Stream.empty());
|
when(() => _playbackService.position).thenAnswer((_) => const Stream.empty());
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _initTextScale() async {
|
Future<void> _initTextScale() async {
|
||||||
|
|||||||
@ -1,3 +1,5 @@
|
|||||||
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
@ -23,7 +25,9 @@ Widget _buildDemo() {
|
|||||||
routes: [
|
routes: [
|
||||||
GoRoute(
|
GoRoute(
|
||||||
path: '/demo',
|
path: '/demo',
|
||||||
builder: (context, state) => const DemoAnnotationEditorScreen(),
|
builder: (context, state) => DemoAnnotationEditorScreen.withSynthesiser(
|
||||||
|
(path, text) async {},
|
||||||
|
),
|
||||||
),
|
),
|
||||||
GoRoute(
|
GoRoute(
|
||||||
path: '/annotation-history',
|
path: '/annotation-history',
|
||||||
@ -149,4 +153,97 @@ void main() {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
group('synthesiseDemoSpeech', () {
|
||||||
|
late Directory tmpDir;
|
||||||
|
|
||||||
|
setUp(() async {
|
||||||
|
tmpDir = await Directory.systemTemp.createTemp('horatio_tts_test_');
|
||||||
|
});
|
||||||
|
|
||||||
|
tearDown(() async {
|
||||||
|
await tmpDir.delete(recursive: true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('espeak-ng fallback: creates a WAV file when piper model is absent',
|
||||||
|
() async {
|
||||||
|
final path = '${tmpDir.path}/hello.wav';
|
||||||
|
// Pass a non-existent model path so the espeak-ng fallback is taken.
|
||||||
|
final result = await synthesiseDemoSpeech(
|
||||||
|
path,
|
||||||
|
'Hello world.',
|
||||||
|
piperModel: '${tmpDir.path}/nonexistent.onnx',
|
||||||
|
);
|
||||||
|
expect(result, path);
|
||||||
|
expect(File(path).existsSync(), isTrue);
|
||||||
|
expect(File(path).lengthSync(), greaterThan(44)); // has audio data
|
||||||
|
});
|
||||||
|
|
||||||
|
test('piper path: creates a WAV file using the installed model', () async {
|
||||||
|
final home = Platform.environment['HOME'] ?? '/root';
|
||||||
|
final model =
|
||||||
|
'$home/.local/share/horatio/piper/en_US-lessac-high.onnx';
|
||||||
|
if (!File(model).existsSync()) {
|
||||||
|
// Piper not installed \u2014 skip this path on machines without the model.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final path = '${tmpDir.path}/hamlet.wav';
|
||||||
|
final result = await synthesiseDemoSpeech(
|
||||||
|
path,
|
||||||
|
'To be.',
|
||||||
|
piperModel: model,
|
||||||
|
);
|
||||||
|
expect(result, path);
|
||||||
|
expect(File(path).existsSync(), isTrue);
|
||||||
|
expect(File(path).lengthSync(), greaterThan(44));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
group('synthesiseDemoSpeechCached', () {
|
||||||
|
late Directory tmpDir;
|
||||||
|
|
||||||
|
setUp(() async {
|
||||||
|
tmpDir = await Directory.systemTemp.createTemp('horatio_cache_test_');
|
||||||
|
});
|
||||||
|
|
||||||
|
tearDown(() async {
|
||||||
|
await tmpDir.delete(recursive: true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('synthesises when file does not exist', () async {
|
||||||
|
final path = '${tmpDir.path}/new.wav';
|
||||||
|
var called = false;
|
||||||
|
Future<String> fakeSynth(String p, String t) async {
|
||||||
|
called = true;
|
||||||
|
await File(p).writeAsBytes([0, 1, 2]); // write something
|
||||||
|
return p;
|
||||||
|
}
|
||||||
|
|
||||||
|
final result = await synthesiseDemoSpeechCached(
|
||||||
|
path,
|
||||||
|
'hello',
|
||||||
|
synth: fakeSynth,
|
||||||
|
);
|
||||||
|
expect(result, path);
|
||||||
|
expect(called, isTrue);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('skips synthesis when file already exists', () async {
|
||||||
|
final path = '${tmpDir.path}/existing.wav';
|
||||||
|
await File(path).writeAsBytes([0, 1, 2]); // pre-create
|
||||||
|
var called = false;
|
||||||
|
Future<String> fakeSynth(String p, String t) async {
|
||||||
|
called = true;
|
||||||
|
return p;
|
||||||
|
}
|
||||||
|
|
||||||
|
final result = await synthesiseDemoSpeechCached(
|
||||||
|
path,
|
||||||
|
'hello',
|
||||||
|
synth: fakeSynth,
|
||||||
|
);
|
||||||
|
expect(result, path);
|
||||||
|
expect(called, isFalse); // synthesis was skipped
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@ -252,10 +252,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: matcher
|
name: matcher
|
||||||
sha256: "12956d0ad8390bbcc63ca2e1469c0619946ccb52809807067a7020d57e647aa6"
|
sha256: dc0b7dc7651697ea4ff3e69ef44b0407ea32c487a39fff6a4004fa585e901861
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.12.18"
|
version: "0.12.19"
|
||||||
material_color_utilities:
|
material_color_utilities:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -425,10 +425,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: test_api
|
name: test_api
|
||||||
sha256: "93167629bfc610f71560ab9312acdda4959de4df6fac7492c89ff0d3886f6636"
|
sha256: "8161c84903fd860b26bfdefb7963b3f0b68fee7adea0f59ef805ecca346f0c7a"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.7.9"
|
version: "0.7.10"
|
||||||
typed_data:
|
typed_data:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|||||||
18
pomodoro_app/test/main_test.dart
Normal file
18
pomodoro_app/test/main_test.dart
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:pomodoro_app/main.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
testWidgets('PomodoroApp builds and shows MaterialApp', (tester) async {
|
||||||
|
await tester.pumpWidget(const PomodoroApp());
|
||||||
|
expect(find.byType(MaterialApp), findsOneWidget);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('PomodoroApp uses dark theme', (tester) async {
|
||||||
|
await tester.pumpWidget(const PomodoroApp());
|
||||||
|
final materialApp = tester.widget<MaterialApp>(find.byType(MaterialApp));
|
||||||
|
expect(materialApp.debugShowCheckedModeBanner, false);
|
||||||
|
expect(materialApp.title, 'Pomodoro');
|
||||||
|
expect(materialApp.theme, isNotNull);
|
||||||
|
});
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user