diff --git a/integration_test/explorer_use_check_test.dart b/integration_test/explorer_use_check_test.dart index 181bc79..98339e8 100644 --- a/integration_test/explorer_use_check_test.dart +++ b/integration_test/explorer_use_check_test.dart @@ -14,7 +14,7 @@ import 'package:jj_mkvmaker/platform/windows/process_media_tool.dart'; import 'package:jj_mkvmaker/services/platform_services.dart'; import 'package:jj_mkvmaker/ui/explorer/job_view.dart' show TransferJobView; import 'package:jj_mkvmaker/ui/explorer_page.dart'; -import 'package:jj_mkvmaker/ui/notice.dart' show appNoticeDuration, noticeDurationForTest; +import 'package:jj_mkvmaker/ui/notice.dart' show NoticeTiming; import 'package:jj_mkvmaker/ui/theme.dart'; import 'package:path/path.dart' as p; @@ -35,8 +35,6 @@ void main() { t.view.physicalSize = Size(w.toDouble(), h.toDouble()); t.view.devicePixelRatio = 1; addTearDown(t.view.reset); - noticeDurationForTest = const Duration(hours: 1); - addTearDown(() => noticeDurationForTest = appNoticeDuration); await t.runAsync(() => i18n.apply('ko', save: false)); useTestRecycleBin(); // 캡처 비교가 흔들리지 않게 시험 폴더 이름 · 파일 시각을 고정 (관리자 지시 10/22) - 끝나면 지움 @@ -66,7 +64,11 @@ void main() { final shotKey = GlobalKey(); await t.runAsync(() => t.pumpWidget(RepaintBoundary( key: shotKey, - child: MaterialApp(theme: buildTheme(), debugShowCheckedModeBanner: false, home: ExplorerPage(device: device, c: c)), + // 알림이 실제 시간으로 닫히지 않게 (정한 자리에서 noticeTimesOut 으로 닫는다) + child: NoticeTiming( + duration: const Duration(hours: 1), + child: MaterialApp(theme: buildTheme(), debugShowCheckedModeBanner: false, home: ExplorerPage(device: device, c: c)), + ), ))); Future settle([int n = 12]) async { for (var i = 0; i < n; i++) { @@ -100,8 +102,8 @@ void main() { } // 복사 · 이동이 끝나 진행 패널이 다 내려갈 때까지 (실제 시간, 10초까지) - 캡처가 PC 빠르기에 따라 달라지지 않게 - // 알림은 실제 시간 10초 (appNoticeDuration) 로 닫히면 찍는 때와 겹쳐 장면이 실행마다 달라진다 (14 · 27) - - // 이 시험은 저절로 닫히지 않게 두고, 정한 자리에서 "시간이 다 되어 닫힘" 으로 닫는다 (앱의 알림 시간은 그대로) + // 알림은 실제 시간 10초 (앱의 값) 로 닫히면 찍는 때와 겹쳐 장면이 실행마다 달라진다 (14 · 27) - + // 이 시험은 NoticeTiming 으로 저절로 닫히지 않게 두고, 정한 자리에서 "시간이 다 되어 닫힘" 으로 닫는다 (앱의 알림 시간은 그대로) Future noticeTimesOut() async { t.state(find.byType(ScaffoldMessenger).first).removeCurrentSnackBar(); await settle(3); diff --git a/lib/ui/notice.dart b/lib/ui/notice.dart index f221f29..ad2d623 100644 --- a/lib/ui/notice.dart +++ b/lib/ui/notice.dart @@ -3,7 +3,7 @@ import 'package:flutter/material.dart'; /// 단추가 있는 아래 알림 (SnackBar) 을 띄우는 곳은 이것 하나 (E4 U2, 관리자 승인 10/22). /// /// 약속 -/// - [NoticeLife.timed] (기본): [noticeDuration] (앱은 [appNoticeDuration]) 뒤 저절로 닫힌다. 앱 전체 한 값이다. +/// - [NoticeLife.timed] (기본): [appNoticeDuration] 뒤 저절로 닫힌다. 앱 전체 한 값이다 (화면을 찍는 시험만 [NoticeTiming] 으로 정함). /// (Flutter 3.29 부터 단추가 있는 SnackBar 는 기본으로 닫히지 않아, 끝 알림이 다음 일을 하는 내내 아래 줄을 가렸다 - 점검 U2) /// - [NoticeLife.untilClosed]: 사용자가 ✕ 를 누르거나 부르는 쪽이 돌려받은 것으로 닫을 때까지 둔다. /// 상태가 남는 중요한 안내 (248 DNS · 198 감지한 언어) 와, 부르는 쪽이 일이 끝나면 닫는 것 (다운로드 준비 · 시작 전 [취소]) 만. @@ -12,13 +12,18 @@ import 'package:flutter/material.dart'; /// - [messenger] 가 없으면 아무것도 하지 않고 null. const appNoticeDuration = Duration(seconds: 10); -/// [NoticeLife.timed] 알림이 저절로 닫히기까지. 앱은 늘 [appNoticeDuration] (U2 의 값 - 바꾸는 곳 없음). -/// 화면을 찍는 시험만 [noticeDurationForTest] 로 정한다 (실제 시간 10초가 찍는 때와 겹쳐 장면이 실행마다 달라지지 않게) -Duration get noticeDuration => _noticeDuration; -Duration _noticeDuration = appNoticeDuration; +/// [NoticeLife.timed] 알림이 저절로 닫히기까지를 아래 화면에 정해 준다. 없으면 [appNoticeDuration] (앱은 늘 없음 - U2 의 값). +/// 화면을 찍는 시험이 MaterialApp 을 이것으로 감싸 정한다 (실제 시간 10초가 찍는 때와 겹쳐 장면이 실행마다 달라지지 않게) +class NoticeTiming extends InheritedWidget { + final Duration duration; + const NoticeTiming({super.key, required this.duration, required super.child}); -@visibleForTesting -set noticeDurationForTest(Duration d) => _noticeDuration = d; + /// [context] 위에서 정한 알림 시간 (없으면 [appNoticeDuration]) + static Duration of(BuildContext context) => context.getInheritedWidgetOfExactType()?.duration ?? appNoticeDuration; + + @override + bool updateShouldNotify(NoticeTiming old) => duration != old.duration; +} enum NoticeLife { timed, untilClosed } @@ -38,7 +43,7 @@ ScaffoldFeatureController? showNotice( if (clearOthers) messenger.clearSnackBars(); return messenger.showSnackBar(SnackBar( content: content ?? Text(text!), - duration: life == NoticeLife.timed ? noticeDuration : const Duration(days: 1), + duration: life == NoticeLife.timed ? NoticeTiming.of(messenger.context) : const Duration(days: 1), persist: life == NoticeLife.untilClosed, showCloseIcon: true, action: action == null ? null : SnackBarAction(label: action.$1, onPressed: action.$2), diff --git a/test/browser_page_test.dart b/test/browser_page_test.dart index 2245bdc..fc4624b 100644 --- a/test/browser_page_test.dart +++ b/test/browser_page_test.dart @@ -355,12 +355,12 @@ void main() { await tester.tap(find.text('실행 취소')); await tester.pumpAndSettle(); expect(bm.tree.findByUrl('https://www.youtube.com/watch?v=abc'), isNotNull); - await tester.tap(find.byTooltip('더 보기').last); // 실행 취소 알림은 noticeDuration (E4 U2) 뒤 저절로 사라진다 + await tester.tap(find.byTooltip('더 보기').last); // 실행 취소 알림은 appNoticeDuration (E4 U2) 뒤 저절로 사라진다 await tester.pumpAndSettle(); await tester.tap(find.text('삭제').last); await tester.pumpAndSettle(); expect(find.text('실행 취소'), findsOneWidget); - await tester.pump(noticeDuration + const Duration(seconds: 1)); + await tester.pump(appNoticeDuration + const Duration(seconds: 1)); await tester.pumpAndSettle(); expect(find.text('실행 취소'), findsNothing); bm.undoRemove(); diff --git a/test/notice_contract_test.dart b/test/notice_contract_test.dart index a7bbf20..671b47a 100644 --- a/test/notice_contract_test.dart +++ b/test/notice_contract_test.dart @@ -11,20 +11,19 @@ void main() { messenger = key.currentState!; } - testWidgets('단추가 있어도 timed 는 noticeDuration 뒤 저절로 닫힘 (U2) · 앱 전체 한 값', (tester) async { + testWidgets('단추가 있어도 timed 는 appNoticeDuration 뒤 저절로 닫힘 (U2) · 앱 전체 한 값', (tester) async { await open(tester); var pressed = 0; showNotice(messenger, text: '복사했습니다', action: ('폴더 열기', () => pressed++)); await tester.pumpAndSettle(); expect(find.text('복사했습니다'), findsOneWidget); expect(find.byIcon(Icons.close), findsOneWidget, reason: '✕ 로 바로 닫기'); - await tester.pump(noticeDuration - const Duration(seconds: 1)); + await tester.pump(appNoticeDuration - const Duration(seconds: 1)); expect(find.text('복사했습니다'), findsOneWidget, reason: '정해진 시간 전에는 남음'); await tester.pump(const Duration(seconds: 2)); await tester.pumpAndSettle(); expect(find.text('복사했습니다'), findsNothing); expect(pressed, 0); - expect(noticeDuration, const Duration(seconds: 10)); expect(appNoticeDuration, const Duration(seconds: 10), reason: '앱의 값 (U2) - 시험이 정하지 않으면 이것'); }); @@ -65,4 +64,19 @@ void main() { expect(find.text('되돌리기 알림'), findsOneWidget); expect(showNotice(null, text: 'x'), isNull); }); + + testWidgets('NoticeTiming: 감싼 화면의 timed 알림은 그 시간 뒤 닫힘 (없으면 appNoticeDuration)', (tester) async { + final key = GlobalKey(); + await tester.pumpWidget(NoticeTiming( + duration: const Duration(seconds: 30), + child: MaterialApp(scaffoldMessengerKey: key, home: const Scaffold(body: SizedBox())))); + showNotice(key.currentState, text: '오래 남음', action: ('보기', () {})); + await tester.pump(); + await tester.pump(const Duration(milliseconds: 500)); // 들어오는 움직임이 끝난 뒤부터 센다 + await tester.pump(appNoticeDuration + const Duration(seconds: 5)); + expect(find.text('오래 남음'), findsOneWidget, reason: '10초가 지나도 정한 30초 전에는 남음'); + await tester.pump(const Duration(seconds: 20)); + await tester.pumpAndSettle(); + expect(find.text('오래 남음'), findsNothing); + }); }