알림 시간을 시험 전용 길이 대신 주입으로: NoticeTiming (InheritedWidget, 없으면 appNoticeDuration 10초)

- lib/ui/notice.dart 의 noticeDurationForTest 를 지움 (lib 에 시험 전용 길이를 두지 않음 - 관리자 10/23)
- 탐색기 점검 시험은 MaterialApp 을 NoticeTiming 으로 감싼다. 약속 시험: 감싸면 그 시간, 없으면 10초
- 확인: 캡처 2회 90/90 같음 · 앞 판 (5303663) 과도 90/90 같음 → base7

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
modular
RyuJongJin 2026-10-23 04:53:09 +09:00
parent 53036633d3
commit 8c561a6133
4 changed files with 40 additions and 19 deletions

View File

@ -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<void> 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<void> noticeTimesOut() async {
t.state<ScaffoldMessengerState>(find.byType(ScaffoldMessenger).first).removeCurrentSnackBar();
await settle(3);

View File

@ -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<NoticeTiming>()?.duration ?? appNoticeDuration;
@override
bool updateShouldNotify(NoticeTiming old) => duration != old.duration;
}
enum NoticeLife { timed, untilClosed }
@ -38,7 +43,7 @@ ScaffoldFeatureController<SnackBar, SnackBarClosedReason>? 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),

View File

@ -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();

View File

@ -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<ScaffoldMessengerState>();
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);
});
}