browser_test 나누기 (646 → 165 + 419): 화면 시험은 browser_page_test, 대역은 support/browser_doubles
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>modular
parent
d031584e7a
commit
7d724ee9ca
|
|
@ -0,0 +1,419 @@
|
|||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:jj_mkvmaker/app/app_controller.dart';
|
||||
import 'package:jj_mkvmaker/app/bookmarks_controller.dart';
|
||||
import 'package:jj_mkvmaker/app/download_manager.dart';
|
||||
import 'package:jj_mkvmaker/app/net_guard.dart';
|
||||
import 'package:jj_mkvmaker/core/bookmarks.dart';
|
||||
import 'package:jj_mkvmaker/core/dns_resolver.dart';
|
||||
import 'package:jj_mkvmaker/core/download_detect.dart';
|
||||
import 'package:jj_mkvmaker/platform/windows/desktop_storage_service.dart';
|
||||
import 'package:jj_mkvmaker/platform/windows/process_media_tool.dart';
|
||||
import 'package:jj_mkvmaker/services/downloader.dart';
|
||||
import 'package:jj_mkvmaker/services/platform_services.dart';
|
||||
import 'package:jj_mkvmaker/ui/browser_page.dart';
|
||||
import 'package:jj_mkvmaker/ui/downloads_page.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
import 'package:jj_mkvmaker/ui/notice.dart';
|
||||
|
||||
import 'support/temp_dirs.dart';
|
||||
import 'support/browser_doubles.dart';
|
||||
|
||||
void main() {
|
||||
deleteTempDirsAtEnd();
|
||||
testWidgets('다운로드 알림: 브라우저 화면이 닫혀도 "목록 보기" 가 되고, 준비가 끝나면 사라진다', (tester) async {
|
||||
tester.view.physicalSize = const Size(1500, 900);
|
||||
tester.view.devicePixelRatio = 1;
|
||||
addTearDown(tester.view.reset);
|
||||
final dir = Directory.systemTemp.createTempSync('jj_dlbar_');
|
||||
addTearDown(() => dir.deleteSync(recursive: true));
|
||||
final c = AppController(PlatformServices(mediaTool: ProcessMediaTool('x', 'y'), storage: DesktopStorageService()));
|
||||
final bm = BookmarksController(p.join(dir.path, 'bookmarks.json'));
|
||||
final d = DownloadManager(
|
||||
backends: [RecordingDownloadBackend(DownloadKind.video)], settings: () => c.settings, readClipboard: () async => null);
|
||||
final navKey = GlobalKey<NavigatorState>();
|
||||
late BrowserHost host;
|
||||
Future<void> settle() async {
|
||||
for (var i = 0; i < 10; i++) {
|
||||
await tester.pump(const Duration(milliseconds: 100));
|
||||
}
|
||||
}
|
||||
|
||||
await tester.pumpWidget(MaterialApp(navigatorKey: navKey, home: const Scaffold(body: Text('MKV 화면'))));
|
||||
navKey.currentState!.push(MaterialPageRoute<void>(
|
||||
builder: (_) => BrowserPage(
|
||||
c: c,
|
||||
bookmarks: bm,
|
||||
downloads: d,
|
||||
initialUrl: 'https://www.youtube.com/watch?v=abc',
|
||||
viewBuilder: (h, url) {
|
||||
host = h;
|
||||
h.attach(RecordingWebNav());
|
||||
return const ColoredBox(color: Colors.white);
|
||||
},
|
||||
),
|
||||
));
|
||||
await settle();
|
||||
host.onUrl('https://www.youtube.com/watch?v=abc');
|
||||
await tester.pump();
|
||||
await tester.tap(find.text('다운로드'));
|
||||
await settle();
|
||||
expect(find.text('목록 보기'), findsOneWidget);
|
||||
final t = d.tasks.single;
|
||||
|
||||
// MKV 화면으로 (브라우저 화면이 닫힘) → 알림의 "목록 보기" 를 눌러도 다운로드 목록이 열린다
|
||||
navKey.currentState!.popUntil((r) => r.isFirst);
|
||||
await settle();
|
||||
expect(find.byType(BrowserPage), findsNothing);
|
||||
await tester.tap(find.text('목록 보기'));
|
||||
await settle();
|
||||
expect(find.byType(DownloadsPage), findsOneWidget);
|
||||
expect(find.text('목록 보기'), findsNothing); // 누르면 알림은 닫힘
|
||||
// 다시 열어도 (위쪽 버튼 등) 목록 화면이 겹쳐 쌓이지 않는다
|
||||
await DownloadsPage.open(navKey.currentState!, d);
|
||||
await settle();
|
||||
expect(find.byType(DownloadsPage, skipOffstage: false), findsOneWidget);
|
||||
expect(t.state, DownloadState.downloading);
|
||||
|
||||
// 다른 동영상: 받기 준비가 끝나면 (진행률이 나오면) 알림이 저절로 사라진다
|
||||
navKey.currentState!.popUntil((r) => r.isFirst);
|
||||
navKey.currentState!.push(MaterialPageRoute<void>(
|
||||
builder: (_) => BrowserPage(
|
||||
c: c,
|
||||
bookmarks: bm,
|
||||
downloads: d,
|
||||
initialUrl: 'https://www.youtube.com/watch?v=def',
|
||||
viewBuilder: (h, url) {
|
||||
host = h;
|
||||
h.attach(RecordingWebNav());
|
||||
return const ColoredBox(color: Colors.white);
|
||||
},
|
||||
),
|
||||
));
|
||||
await settle();
|
||||
host.onUrl('https://www.youtube.com/watch?v=def');
|
||||
await tester.pump();
|
||||
await tester.tap(find.text('다운로드'));
|
||||
await settle();
|
||||
expect(find.text('목록 보기'), findsOneWidget);
|
||||
await tester.pump(const Duration(seconds: 30));
|
||||
expect(find.text('목록 보기'), findsOneWidget, reason: '준비 중에는 그대로');
|
||||
expect(find.byIcon(Icons.close), findsOneWidget); // [✕] 로 바로 닫을 수도 있다
|
||||
d.tasks.first.progress = 0.1;
|
||||
d.refresh();
|
||||
await settle();
|
||||
expect(find.text('목록 보기'), findsNothing);
|
||||
|
||||
// [✕] 누르면 바로 닫힘
|
||||
host.onUrl('https://www.youtube.com/watch?v=ghi');
|
||||
await tester.pump();
|
||||
await tester.tap(find.text('다운로드'));
|
||||
await settle();
|
||||
expect(find.text('목록 보기'), findsOneWidget);
|
||||
await tester.tap(find.byIcon(Icons.close));
|
||||
await settle();
|
||||
expect(find.text('목록 보기'), findsNothing);
|
||||
d.dispose();
|
||||
});
|
||||
|
||||
testWidgets('250: DNS 서버에 닿지 않아 못 연 페이지는 엔진 오류 화면 대신 이유 · [시스템 DNS 로 바꾸기] [다시 시도]', (tester) async {
|
||||
tester.view.physicalSize = const Size(1400, 900);
|
||||
tester.view.devicePixelRatio = 1;
|
||||
addTearDown(tester.view.reset);
|
||||
final dir = Directory.systemTemp.createTempSync('jj_browser_dns_');
|
||||
addTearDown(() => dir.deleteSync(recursive: true));
|
||||
final g = NetGuard.instance;
|
||||
addTearDown(() {
|
||||
g.failure.value = null;
|
||||
g.noticeBlockers.value = 0;
|
||||
});
|
||||
final c = AppController(PlatformServices(mediaTool: ProcessMediaTool('x', 'y'), storage: DesktopStorageService()));
|
||||
final bm = BookmarksController(p.join(dir.path, 'bookmarks.json'));
|
||||
late BrowserHost host;
|
||||
final nav = RecordingWebNav();
|
||||
await tester.pumpWidget(MaterialApp(
|
||||
home: BrowserPage(
|
||||
c: c,
|
||||
bookmarks: bm,
|
||||
initialUrl: 'https://blocked.example/',
|
||||
viewBuilder: (h, url) {
|
||||
host = h;
|
||||
h.attach(nav);
|
||||
return const ViewProbe();
|
||||
},
|
||||
),
|
||||
));
|
||||
await tester.pump();
|
||||
const msg = 'DNS 서버 (192.0.2.1) 에 닿지 않아 blocked.example 을(를) 열 수 없습니다.';
|
||||
// 읽기 시작 → 프록시가 DNS 서버에 닿지 못함 → 끝 (엔진은 ERR_TUNNEL_CONNECTION_FAILED 화면)
|
||||
host.onProgress(0.1);
|
||||
host.onUrl('https://blocked.example/');
|
||||
g.reportError('blocked.example', const DnsException('timeout', unreachable: true, server: '192.0.2.1'));
|
||||
ViewProbe.created = 0;
|
||||
host.onProgress(1);
|
||||
await tester.pump();
|
||||
expect(find.text(msg), findsOneWidget);
|
||||
// 덮개를 띄워도 웹 뷰는 그대로 (새로 만들면 페이지를 다시 읽어 덮개가 걷혔다 - 931)
|
||||
expect(ViewProbe.created, 0);
|
||||
// 248: 덮개가 보이는 동안은 아래 알림을 막는다
|
||||
expect(g.noticeBlockers.value, 1);
|
||||
// [다시 시도]: 덮개를 걷고 다시 읽기
|
||||
await tester.tap(find.text('다시 시도'));
|
||||
await tester.pump();
|
||||
expect(find.text(msg), findsNothing);
|
||||
expect(nav.reloads, 1);
|
||||
expect(ViewProbe.created, 0);
|
||||
expect(g.noticeBlockers.value, 0);
|
||||
// 다른 이름은 막지 않는다 · 서버가 다시 답한 뒤에는 지난 실패로 막지 않는다
|
||||
host.onProgress(0.1);
|
||||
host.onUrl('https://ok.example/');
|
||||
host.onProgress(1);
|
||||
await tester.pump();
|
||||
expect(find.textContaining('에 닿지 않아'), findsNothing);
|
||||
host.onProgress(0.1);
|
||||
host.onUrl('https://blocked.example/');
|
||||
g.reportError('blocked.example', const DnsException('timeout', unreachable: true, server: '192.0.2.1'));
|
||||
g.resolver.onAnswer?.call('blocked.example', '192.0.2.1');
|
||||
host.onProgress(1);
|
||||
await tester.pump();
|
||||
expect(find.text(msg), findsNothing);
|
||||
// [시스템 DNS 로 바꾸기]
|
||||
host.onProgress(0.1);
|
||||
g.reportError('blocked.example', const DnsException('timeout', unreachable: true, server: '192.0.2.1'));
|
||||
host.onProgress(1);
|
||||
await tester.pump();
|
||||
await tester.tap(find.text('시스템 DNS 로 바꾸기'));
|
||||
await tester.pump(const Duration(milliseconds: 400));
|
||||
expect(c.settings.network.mode, 'system');
|
||||
expect(find.text(msg), findsNothing);
|
||||
expect(nav.reloads, 2);
|
||||
|
||||
// 931 (노트 10+): 엔진이 본문 오류 (ERR_TUNNEL_CONNECTION_FAILED) 를 알림 - 그 이름의 실패 기록이 없어도 DNS 가 닿지 않는 중이면 덮개.
|
||||
// 오류 뒤에 오는 읽기 끝이 덮개를 지우지 않는다
|
||||
g.failure.value = DnsFailure('192.0.2.1', 'x', DateTime.now());
|
||||
host.onProgress(0.1);
|
||||
host.onUrl('https://example.org/');
|
||||
host.onMainFrameError!('https://example.org/', 'net::ERR_TUNNEL_CONNECTION_FAILED');
|
||||
host.onProgress(1);
|
||||
await tester.pump();
|
||||
expect(find.text('DNS 서버 (192.0.2.1) 에 닿지 않아 example.org 을(를) 열 수 없습니다.'), findsOneWidget);
|
||||
// 932 (노트 10+): Android 는 본문 오류 뒤에 엔진의 오류 페이지를 또 읽는다 (진행률 0 → 100) - 덮개가 걷히면 안 된다
|
||||
host.onProgress(0.1);
|
||||
await tester.pump();
|
||||
host.onProgress(1);
|
||||
await tester.pump();
|
||||
expect(find.text('DNS 서버 (192.0.2.1) 에 닿지 않아 example.org 을(를) 열 수 없습니다.'), findsOneWidget);
|
||||
// DNS 와 상관없는 오류 (이름 기록도 없음) 는 엔진 화면 그대로
|
||||
await tester.tap(find.text('다시 시도'));
|
||||
await tester.pump();
|
||||
host.onProgress(0.1);
|
||||
host.onMainFrameError!('https://example.org/', 'net::ERR_CONNECTION_REFUSED');
|
||||
host.onProgress(1);
|
||||
await tester.pump();
|
||||
expect(find.textContaining('에 닿지 않아'), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('Android 뒤로 키: 앞 웹 페이지가 있으면 웹 뒤로, 없으면 화면 닫기 · 위쪽 ← 는 늘 닫기', (tester) async {
|
||||
tester.view.physicalSize = const Size(1500, 900);
|
||||
tester.view.devicePixelRatio = 1;
|
||||
addTearDown(tester.view.reset);
|
||||
debugDefaultTargetPlatformOverride = TargetPlatform.android;
|
||||
final dir = Directory.systemTemp.createTempSync('jj_back_');
|
||||
addTearDown(() => dir.deleteSync(recursive: true));
|
||||
final c = AppController(PlatformServices(mediaTool: ProcessMediaTool('x', 'y'), storage: DesktopStorageService()));
|
||||
final bm = BookmarksController(p.join(dir.path, 'bookmarks.json'));
|
||||
final navKey = GlobalKey<NavigatorState>();
|
||||
late BrowserHost host;
|
||||
final nav = RecordingWebNav();
|
||||
Future<void> open() async {
|
||||
navKey.currentState!.push(MaterialPageRoute<void>(
|
||||
builder: (_) => BrowserPage(
|
||||
c: c,
|
||||
bookmarks: bm,
|
||||
initialUrl: 'https://example.com/',
|
||||
viewBuilder: (h, url) {
|
||||
host = h;
|
||||
h.attach(nav);
|
||||
return const ColoredBox(color: Colors.white);
|
||||
},
|
||||
),
|
||||
));
|
||||
await tester.pumpAndSettle();
|
||||
}
|
||||
|
||||
await tester.pumpWidget(MaterialApp(navigatorKey: navKey, home: const Scaffold(body: Text('MKV 화면'))));
|
||||
await open();
|
||||
host.onHistory(true, false);
|
||||
await tester.pump();
|
||||
await tester.binding.handlePopRoute(); // 기기의 뒤로 키
|
||||
await tester.pumpAndSettle();
|
||||
expect(nav.backs, 1);
|
||||
expect(find.byType(BrowserPage), findsOneWidget);
|
||||
host.onHistory(false, true);
|
||||
await tester.pump();
|
||||
await tester.binding.handlePopRoute();
|
||||
await tester.pumpAndSettle();
|
||||
expect(nav.backs, 1);
|
||||
expect(find.byType(BrowserPage), findsNothing);
|
||||
|
||||
// 위쪽 ← (화면 이동) 은 웹 기록이 있어도 브라우저 화면을 닫는다
|
||||
await open();
|
||||
host.onHistory(true, false);
|
||||
await tester.pump();
|
||||
await tester.tap(find.byTooltip('뒤로'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(nav.backs, 1);
|
||||
expect(find.byType(BrowserPage), findsNothing);
|
||||
debugDefaultTargetPlatformOverride = null; // 끝나기 전에 되돌려야 한다 (테스트 검사)
|
||||
});
|
||||
|
||||
testWidgets('브라우저: ☆ 추가 · 표시줄 · 다운로드 버튼 · 관리 패널 (삭제 · 실행 취소)', (tester) async {
|
||||
tester.view.physicalSize = const Size(1500, 900);
|
||||
tester.view.devicePixelRatio = 1;
|
||||
addTearDown(tester.view.reset);
|
||||
final dir = tempDirUntilEnd('jj_bm_');
|
||||
|
||||
final c = AppController(PlatformServices(mediaTool: ProcessMediaTool('x', 'y'), storage: DesktopStorageService()));
|
||||
final bm = BookmarksController(p.join(dir.path, 'bookmarks.json'));
|
||||
final video = RecordingDownloadBackend(DownloadKind.video);
|
||||
final d = DownloadManager(backends: [video], settings: () => c.settings, readClipboard: () async => null);
|
||||
final nav = RecordingWebNav();
|
||||
late BrowserHost host;
|
||||
|
||||
await tester.pumpWidget(MaterialApp(
|
||||
home: BrowserPage(
|
||||
c: c,
|
||||
bookmarks: bm,
|
||||
downloads: d,
|
||||
initialUrl: 'https://www.google.com/',
|
||||
viewBuilder: (h, url) {
|
||||
host = h;
|
||||
h.attach(nav);
|
||||
return const ColoredBox(color: Colors.white);
|
||||
},
|
||||
),
|
||||
));
|
||||
// 기본 즐겨찾기 표시줄
|
||||
expect(find.text('YouTube'), findsOneWidget);
|
||||
// 터치 화면: 즐겨찾기를 길게 누르면 오른쪽 클릭과 같은 메뉴
|
||||
await tester.longPress(find.text('YouTube'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('수정 · 이동'), findsOneWidget);
|
||||
await tester.tapAt(const Offset(5, 880));
|
||||
await tester.pumpAndSettle();
|
||||
ButtonStyleButton dl() =>
|
||||
tester.widget<ButtonStyleButton>(find.ancestor(of: find.text('다운로드'), matching: find.byWidgetPredicate((w) => w is ButtonStyleButton)));
|
||||
// 27: 동영상 페이지가 아니어도 받아 볼 수 있다 (덜 눈에 띄는 버튼 · 안내). 137: 테두리만 있는 버튼
|
||||
expect(dl().onPressed, isNotNull);
|
||||
expect(dl(), isA<OutlinedButton>());
|
||||
expect(find.byTooltip('다운로드 (이 페이지에서 동영상을 찾지 못했지만 받아 볼 수 있습니다)'), findsOneWidget);
|
||||
|
||||
// 주소창 입력 → 이동
|
||||
await tester.enterText(find.byType(TextField).first, 'youtube.com/watch?v=abc');
|
||||
await tester.testTextInput.receiveAction(TextInputAction.done);
|
||||
expect(nav.loads.last, 'https://youtube.com/watch?v=abc');
|
||||
host.onUrl('https://www.youtube.com/watch?v=abc');
|
||||
host.onTitle('재미있는 영상');
|
||||
await tester.pump();
|
||||
expect(dl().onPressed, isNotNull);
|
||||
expect(dl(), isA<FilledButton>(), reason: '137: 동영상 페이지는 진한 버튼');
|
||||
await tester.tap(find.text('다운로드'));
|
||||
await tester.pump();
|
||||
expect(video.started, ['https://www.youtube.com/watch?v=abc']);
|
||||
|
||||
// ☆ → Chrome 처럼 "즐겨찾기 추가됨" 창: 이름 · 폴더 고르기, 그 자리에서 새 폴더
|
||||
await tester.tap(find.byTooltip('즐겨찾기 추가 (Ctrl+D)'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('즐겨찾기 추가됨'), findsOneWidget);
|
||||
expect(bm.tree.parentOf(bm.tree.findByUrl('https://www.youtube.com/watch?v=abc')!.id)!.id, BookmarkTree.barId);
|
||||
await tester.tap(find.text('새 폴더'));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.enterText(find.byType(TextField).last, '음악');
|
||||
await tester.tap(find.text('확인'));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.text('완료'));
|
||||
await tester.pumpAndSettle();
|
||||
final saved = bm.tree.findByUrl('https://www.youtube.com/watch?v=abc')!;
|
||||
final folder = bm.tree.parentOf(saved.id)!;
|
||||
expect(folder.title, '음악');
|
||||
expect(bm.tree.parentOf(folder.id)!.id, BookmarkTree.barId);
|
||||
// 표시줄에는 폴더가 보인다
|
||||
expect(find.widgetWithText(TextButton, '음악'), findsOneWidget);
|
||||
|
||||
// 관리 패널 → 삭제 → 실행 취소
|
||||
await tester.tap(find.byTooltip('즐겨찾기 관리 (Ctrl+Shift+B)'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('즐겨찾기'), findsWidgets);
|
||||
await tester.tap(find.byTooltip('더 보기').last);
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.text('삭제').last);
|
||||
await tester.pumpAndSettle();
|
||||
expect(bm.tree.findByUrl('https://www.youtube.com/watch?v=abc'), isNull);
|
||||
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.pumpAndSettle();
|
||||
await tester.tap(find.text('삭제').last);
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('실행 취소'), findsOneWidget);
|
||||
await tester.pump(noticeDuration + const Duration(seconds: 1));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('실행 취소'), findsNothing);
|
||||
bm.undoRemove();
|
||||
await tester.pumpAndSettle();
|
||||
expect(bm.tree.findByUrl('https://www.youtube.com/watch?v=abc'), isNotNull);
|
||||
|
||||
// 패널에서 누르면 그 주소로 이동
|
||||
await tester.tap(find.text('OpenSubtitles').last);
|
||||
expect(nav.loads.last, 'https://www.opensubtitles.com/');
|
||||
|
||||
// 화면 분할: 작업 현황 (다운로드 진행 · 편집 목록)
|
||||
await tester.tap(find.byTooltip('작업 현황 보기 · 화면 분할 (Ctrl+Shift+J)'));
|
||||
await tester.pump(const Duration(milliseconds: 300));
|
||||
expect(find.text('작업 현황'), findsOneWidget);
|
||||
expect(find.text('쉬는 중'), findsOneWidget);
|
||||
expect(find.text('다운로드 1개'), findsOneWidget);
|
||||
await tester.tap(find.byTooltip('작업 현황 닫기'));
|
||||
await tester.pump(const Duration(milliseconds: 300));
|
||||
expect(find.text('작업 현황'), findsNothing);
|
||||
d.dispose();
|
||||
});
|
||||
|
||||
testWidgets('66-2: 처음 열면 위쪽 막대 아래에 화면 옮기기 안내 한 줄 (알림을 막지 않음) · ✕ 로 닫고 다시 열면 없음', (tester) async {
|
||||
tester.view.physicalSize = const Size(1500, 900);
|
||||
tester.view.devicePixelRatio = 1;
|
||||
addTearDown(tester.view.reset);
|
||||
final dir = Directory.systemTemp.createTempSync('jj_hint_');
|
||||
addTearDown(() => dir.deleteSync(recursive: true));
|
||||
final c = AppController(PlatformServices(mediaTool: ProcessMediaTool('x', 'y'), storage: DesktopStorageService()));
|
||||
final bm = BookmarksController(p.join(dir.path, 'bookmarks.json'));
|
||||
Widget page() => MaterialApp(
|
||||
home: BrowserPage(
|
||||
c: c,
|
||||
bookmarks: bm,
|
||||
initialUrl: 'https://example.com/',
|
||||
viewBuilder: (h, url) {
|
||||
h.attach(RecordingWebNav());
|
||||
return const ColoredBox(color: Colors.white);
|
||||
},
|
||||
),
|
||||
);
|
||||
await tester.pumpWidget(page());
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.textContaining('위쪽 막대를 좌우로 밀면'), findsOneWidget);
|
||||
expect(find.byType(SnackBar), findsNothing, reason: '알림 (다운로드 등) 을 뒤로 밀지 않게 알림이 아니라 한 줄');
|
||||
expect(c.settings.browser.swipeHinted, isTrue);
|
||||
await tester.tap(find.byTooltip('안내 닫기'));
|
||||
await tester.pump();
|
||||
expect(find.textContaining('위쪽 막대를 좌우로 밀면'), findsNothing);
|
||||
// 다시 열면 없음 (한 번만)
|
||||
await tester.pumpWidget(const SizedBox());
|
||||
await tester.pumpWidget(page());
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.textContaining('위쪽 막대를 좌우로 밀면'), findsNothing);
|
||||
});
|
||||
}
|
||||
|
|
@ -1,105 +1,17 @@
|
|||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:jj_mkvmaker/app/app_controller.dart';
|
||||
import 'package:jj_mkvmaker/app/bookmarks_controller.dart';
|
||||
import 'package:jj_mkvmaker/app/download_manager.dart';
|
||||
import 'package:jj_mkvmaker/app/net_guard.dart';
|
||||
import 'package:jj_mkvmaker/app/settings.dart';
|
||||
import 'package:jj_mkvmaker/core/bookmarks.dart';
|
||||
import 'package:jj_mkvmaker/core/dns_resolver.dart';
|
||||
import 'package:jj_mkvmaker/core/download_detect.dart';
|
||||
import 'package:jj_mkvmaker/core/web_address.dart';
|
||||
import 'package:jj_mkvmaker/platform/windows/desktop_storage_service.dart';
|
||||
import 'package:jj_mkvmaker/platform/windows/process_media_tool.dart';
|
||||
import 'package:jj_mkvmaker/services/downloader.dart';
|
||||
import 'package:jj_mkvmaker/services/platform_services.dart';
|
||||
import 'package:jj_mkvmaker/ui/browser_page.dart';
|
||||
import 'package:jj_mkvmaker/ui/downloads_page.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
|
||||
import 'support/temp_dirs.dart';
|
||||
import 'package:jj_mkvmaker/ui/notice.dart';
|
||||
|
||||
class _Backend implements DownloadBackend {
|
||||
@override
|
||||
final DownloadKind kind;
|
||||
final started = <String>[];
|
||||
void Function()? changed;
|
||||
_Backend(this.kind);
|
||||
@override
|
||||
Future<(String, List<PlaylistEntry>)?> expandPlaylist(String url) async => null;
|
||||
@override
|
||||
Future<void> start(DownloadTask t, void Function() changed) async {
|
||||
this.changed = changed;
|
||||
started.add(t.source);
|
||||
t.state = DownloadState.downloading;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> pause(DownloadTask t) async {}
|
||||
@override
|
||||
Future<void> cancel(DownloadTask t) async {}
|
||||
@override
|
||||
Future<void> shutdown() async {}
|
||||
}
|
||||
|
||||
class _Nav implements WebNav {
|
||||
int paused = 0;
|
||||
@override
|
||||
Future<void> pauseMedia() async => paused++;
|
||||
final scripts = <String>[];
|
||||
@override
|
||||
Future<void> runScript(String js) async => scripts.add(js);
|
||||
final loads = <String>[];
|
||||
@override
|
||||
Future<void> load(String url) async => loads.add(url);
|
||||
int backs = 0;
|
||||
@override
|
||||
Future<void> back() async => backs++;
|
||||
@override
|
||||
Future<void> forward() async {}
|
||||
int reloads = 0;
|
||||
@override
|
||||
Future<void> reload() async => reloads++;
|
||||
@override
|
||||
Future<void> stop() async {}
|
||||
var exported = 0;
|
||||
@override
|
||||
Future<void> exportCookies() async => exported++;
|
||||
@override
|
||||
Future<void> clearData() async {}
|
||||
@override
|
||||
Future<Object?> evaluate(String js) async {
|
||||
scripts.add(js);
|
||||
return null;
|
||||
}
|
||||
|
||||
final javaScript = <bool>[];
|
||||
@override
|
||||
Future<void> setJavaScript(bool on) async => javaScript.add(on);
|
||||
}
|
||||
|
||||
/// 웹 뷰 대신: 몇 번 새로 만들어졌는지 센다 (250 - 덮개를 띄워도 웹 뷰를 다시 만들지 않아야 함)
|
||||
class _Probe extends StatefulWidget {
|
||||
static int created = 0;
|
||||
const _Probe();
|
||||
@override
|
||||
State<_Probe> createState() => _ProbeState();
|
||||
}
|
||||
|
||||
class _ProbeState extends State<_Probe> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_Probe.created++;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => const ColoredBox(color: Colors.white);
|
||||
}
|
||||
import 'support/browser_doubles.dart';
|
||||
|
||||
void main() {
|
||||
deleteTempDirsAtEnd();
|
||||
|
|
@ -207,7 +119,7 @@ void main() {
|
|||
});
|
||||
|
||||
test('브라우저 페이지 받기 (YouTube 외 사이트도)', () {
|
||||
final v = _Backend(DownloadKind.video);
|
||||
final v = RecordingDownloadBackend(DownloadKind.video);
|
||||
final d = DownloadManager(backends: [v], settings: () => AppSettings()..download.root = r'D:\dl');
|
||||
expect(d.addPage('https://vimeo.com/12345')!.kind, DownloadKind.video);
|
||||
expect(d.addPage('https://vimeo.com/12345'), isNull); // 이미 받는 중
|
||||
|
|
@ -217,7 +129,7 @@ void main() {
|
|||
});
|
||||
|
||||
test('다 받은 영상 → 편집 목록 알림 (한 번만, 조각 파일 제외)', () async {
|
||||
final v = _Backend(DownloadKind.video);
|
||||
final v = RecordingDownloadBackend(DownloadKind.video);
|
||||
final d = DownloadManager(backends: [v], settings: () => AppSettings()..download.root = r'D:\dl');
|
||||
final got = <String>[];
|
||||
d.onFinished.listen((t) => got.add(t.id));
|
||||
|
|
@ -233,365 +145,6 @@ void main() {
|
|||
});
|
||||
});
|
||||
|
||||
testWidgets('다운로드 알림: 브라우저 화면이 닫혀도 "목록 보기" 가 되고, 준비가 끝나면 사라진다', (tester) async {
|
||||
tester.view.physicalSize = const Size(1500, 900);
|
||||
tester.view.devicePixelRatio = 1;
|
||||
addTearDown(tester.view.reset);
|
||||
final dir = Directory.systemTemp.createTempSync('jj_dlbar_');
|
||||
addTearDown(() => dir.deleteSync(recursive: true));
|
||||
final c = AppController(PlatformServices(mediaTool: ProcessMediaTool('x', 'y'), storage: DesktopStorageService()));
|
||||
final bm = BookmarksController(p.join(dir.path, 'bookmarks.json'));
|
||||
final d = DownloadManager(
|
||||
backends: [_Backend(DownloadKind.video)], settings: () => c.settings, readClipboard: () async => null);
|
||||
final navKey = GlobalKey<NavigatorState>();
|
||||
late BrowserHost host;
|
||||
Future<void> settle() async {
|
||||
for (var i = 0; i < 10; i++) {
|
||||
await tester.pump(const Duration(milliseconds: 100));
|
||||
}
|
||||
}
|
||||
|
||||
await tester.pumpWidget(MaterialApp(navigatorKey: navKey, home: const Scaffold(body: Text('MKV 화면'))));
|
||||
navKey.currentState!.push(MaterialPageRoute<void>(
|
||||
builder: (_) => BrowserPage(
|
||||
c: c,
|
||||
bookmarks: bm,
|
||||
downloads: d,
|
||||
initialUrl: 'https://www.youtube.com/watch?v=abc',
|
||||
viewBuilder: (h, url) {
|
||||
host = h;
|
||||
h.attach(_Nav());
|
||||
return const ColoredBox(color: Colors.white);
|
||||
},
|
||||
),
|
||||
));
|
||||
await settle();
|
||||
host.onUrl('https://www.youtube.com/watch?v=abc');
|
||||
await tester.pump();
|
||||
await tester.tap(find.text('다운로드'));
|
||||
await settle();
|
||||
expect(find.text('목록 보기'), findsOneWidget);
|
||||
final t = d.tasks.single;
|
||||
|
||||
// MKV 화면으로 (브라우저 화면이 닫힘) → 알림의 "목록 보기" 를 눌러도 다운로드 목록이 열린다
|
||||
navKey.currentState!.popUntil((r) => r.isFirst);
|
||||
await settle();
|
||||
expect(find.byType(BrowserPage), findsNothing);
|
||||
await tester.tap(find.text('목록 보기'));
|
||||
await settle();
|
||||
expect(find.byType(DownloadsPage), findsOneWidget);
|
||||
expect(find.text('목록 보기'), findsNothing); // 누르면 알림은 닫힘
|
||||
// 다시 열어도 (위쪽 버튼 등) 목록 화면이 겹쳐 쌓이지 않는다
|
||||
await DownloadsPage.open(navKey.currentState!, d);
|
||||
await settle();
|
||||
expect(find.byType(DownloadsPage, skipOffstage: false), findsOneWidget);
|
||||
expect(t.state, DownloadState.downloading);
|
||||
|
||||
// 다른 동영상: 받기 준비가 끝나면 (진행률이 나오면) 알림이 저절로 사라진다
|
||||
navKey.currentState!.popUntil((r) => r.isFirst);
|
||||
navKey.currentState!.push(MaterialPageRoute<void>(
|
||||
builder: (_) => BrowserPage(
|
||||
c: c,
|
||||
bookmarks: bm,
|
||||
downloads: d,
|
||||
initialUrl: 'https://www.youtube.com/watch?v=def',
|
||||
viewBuilder: (h, url) {
|
||||
host = h;
|
||||
h.attach(_Nav());
|
||||
return const ColoredBox(color: Colors.white);
|
||||
},
|
||||
),
|
||||
));
|
||||
await settle();
|
||||
host.onUrl('https://www.youtube.com/watch?v=def');
|
||||
await tester.pump();
|
||||
await tester.tap(find.text('다운로드'));
|
||||
await settle();
|
||||
expect(find.text('목록 보기'), findsOneWidget);
|
||||
await tester.pump(const Duration(seconds: 30));
|
||||
expect(find.text('목록 보기'), findsOneWidget, reason: '준비 중에는 그대로');
|
||||
expect(find.byIcon(Icons.close), findsOneWidget); // [✕] 로 바로 닫을 수도 있다
|
||||
d.tasks.first.progress = 0.1;
|
||||
d.refresh();
|
||||
await settle();
|
||||
expect(find.text('목록 보기'), findsNothing);
|
||||
|
||||
// [✕] 누르면 바로 닫힘
|
||||
host.onUrl('https://www.youtube.com/watch?v=ghi');
|
||||
await tester.pump();
|
||||
await tester.tap(find.text('다운로드'));
|
||||
await settle();
|
||||
expect(find.text('목록 보기'), findsOneWidget);
|
||||
await tester.tap(find.byIcon(Icons.close));
|
||||
await settle();
|
||||
expect(find.text('목록 보기'), findsNothing);
|
||||
d.dispose();
|
||||
});
|
||||
|
||||
testWidgets('250: DNS 서버에 닿지 않아 못 연 페이지는 엔진 오류 화면 대신 이유 · [시스템 DNS 로 바꾸기] [다시 시도]', (tester) async {
|
||||
tester.view.physicalSize = const Size(1400, 900);
|
||||
tester.view.devicePixelRatio = 1;
|
||||
addTearDown(tester.view.reset);
|
||||
final dir = Directory.systemTemp.createTempSync('jj_browser_dns_');
|
||||
addTearDown(() => dir.deleteSync(recursive: true));
|
||||
final g = NetGuard.instance;
|
||||
addTearDown(() {
|
||||
g.failure.value = null;
|
||||
g.noticeBlockers.value = 0;
|
||||
});
|
||||
final c = AppController(PlatformServices(mediaTool: ProcessMediaTool('x', 'y'), storage: DesktopStorageService()));
|
||||
final bm = BookmarksController(p.join(dir.path, 'bookmarks.json'));
|
||||
late BrowserHost host;
|
||||
final nav = _Nav();
|
||||
await tester.pumpWidget(MaterialApp(
|
||||
home: BrowserPage(
|
||||
c: c,
|
||||
bookmarks: bm,
|
||||
initialUrl: 'https://blocked.example/',
|
||||
viewBuilder: (h, url) {
|
||||
host = h;
|
||||
h.attach(nav);
|
||||
return const _Probe();
|
||||
},
|
||||
),
|
||||
));
|
||||
await tester.pump();
|
||||
const msg = 'DNS 서버 (192.0.2.1) 에 닿지 않아 blocked.example 을(를) 열 수 없습니다.';
|
||||
// 읽기 시작 → 프록시가 DNS 서버에 닿지 못함 → 끝 (엔진은 ERR_TUNNEL_CONNECTION_FAILED 화면)
|
||||
host.onProgress(0.1);
|
||||
host.onUrl('https://blocked.example/');
|
||||
g.reportError('blocked.example', const DnsException('timeout', unreachable: true, server: '192.0.2.1'));
|
||||
_Probe.created = 0;
|
||||
host.onProgress(1);
|
||||
await tester.pump();
|
||||
expect(find.text(msg), findsOneWidget);
|
||||
// 덮개를 띄워도 웹 뷰는 그대로 (새로 만들면 페이지를 다시 읽어 덮개가 걷혔다 - 931)
|
||||
expect(_Probe.created, 0);
|
||||
// 248: 덮개가 보이는 동안은 아래 알림을 막는다
|
||||
expect(g.noticeBlockers.value, 1);
|
||||
// [다시 시도]: 덮개를 걷고 다시 읽기
|
||||
await tester.tap(find.text('다시 시도'));
|
||||
await tester.pump();
|
||||
expect(find.text(msg), findsNothing);
|
||||
expect(nav.reloads, 1);
|
||||
expect(_Probe.created, 0);
|
||||
expect(g.noticeBlockers.value, 0);
|
||||
// 다른 이름은 막지 않는다 · 서버가 다시 답한 뒤에는 지난 실패로 막지 않는다
|
||||
host.onProgress(0.1);
|
||||
host.onUrl('https://ok.example/');
|
||||
host.onProgress(1);
|
||||
await tester.pump();
|
||||
expect(find.textContaining('에 닿지 않아'), findsNothing);
|
||||
host.onProgress(0.1);
|
||||
host.onUrl('https://blocked.example/');
|
||||
g.reportError('blocked.example', const DnsException('timeout', unreachable: true, server: '192.0.2.1'));
|
||||
g.resolver.onAnswer?.call('blocked.example', '192.0.2.1');
|
||||
host.onProgress(1);
|
||||
await tester.pump();
|
||||
expect(find.text(msg), findsNothing);
|
||||
// [시스템 DNS 로 바꾸기]
|
||||
host.onProgress(0.1);
|
||||
g.reportError('blocked.example', const DnsException('timeout', unreachable: true, server: '192.0.2.1'));
|
||||
host.onProgress(1);
|
||||
await tester.pump();
|
||||
await tester.tap(find.text('시스템 DNS 로 바꾸기'));
|
||||
await tester.pump(const Duration(milliseconds: 400));
|
||||
expect(c.settings.network.mode, 'system');
|
||||
expect(find.text(msg), findsNothing);
|
||||
expect(nav.reloads, 2);
|
||||
|
||||
// 931 (노트 10+): 엔진이 본문 오류 (ERR_TUNNEL_CONNECTION_FAILED) 를 알림 - 그 이름의 실패 기록이 없어도 DNS 가 닿지 않는 중이면 덮개.
|
||||
// 오류 뒤에 오는 읽기 끝이 덮개를 지우지 않는다
|
||||
g.failure.value = DnsFailure('192.0.2.1', 'x', DateTime.now());
|
||||
host.onProgress(0.1);
|
||||
host.onUrl('https://example.org/');
|
||||
host.onMainFrameError!('https://example.org/', 'net::ERR_TUNNEL_CONNECTION_FAILED');
|
||||
host.onProgress(1);
|
||||
await tester.pump();
|
||||
expect(find.text('DNS 서버 (192.0.2.1) 에 닿지 않아 example.org 을(를) 열 수 없습니다.'), findsOneWidget);
|
||||
// 932 (노트 10+): Android 는 본문 오류 뒤에 엔진의 오류 페이지를 또 읽는다 (진행률 0 → 100) - 덮개가 걷히면 안 된다
|
||||
host.onProgress(0.1);
|
||||
await tester.pump();
|
||||
host.onProgress(1);
|
||||
await tester.pump();
|
||||
expect(find.text('DNS 서버 (192.0.2.1) 에 닿지 않아 example.org 을(를) 열 수 없습니다.'), findsOneWidget);
|
||||
// DNS 와 상관없는 오류 (이름 기록도 없음) 는 엔진 화면 그대로
|
||||
await tester.tap(find.text('다시 시도'));
|
||||
await tester.pump();
|
||||
host.onProgress(0.1);
|
||||
host.onMainFrameError!('https://example.org/', 'net::ERR_CONNECTION_REFUSED');
|
||||
host.onProgress(1);
|
||||
await tester.pump();
|
||||
expect(find.textContaining('에 닿지 않아'), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('Android 뒤로 키: 앞 웹 페이지가 있으면 웹 뒤로, 없으면 화면 닫기 · 위쪽 ← 는 늘 닫기', (tester) async {
|
||||
tester.view.physicalSize = const Size(1500, 900);
|
||||
tester.view.devicePixelRatio = 1;
|
||||
addTearDown(tester.view.reset);
|
||||
debugDefaultTargetPlatformOverride = TargetPlatform.android;
|
||||
final dir = Directory.systemTemp.createTempSync('jj_back_');
|
||||
addTearDown(() => dir.deleteSync(recursive: true));
|
||||
final c = AppController(PlatformServices(mediaTool: ProcessMediaTool('x', 'y'), storage: DesktopStorageService()));
|
||||
final bm = BookmarksController(p.join(dir.path, 'bookmarks.json'));
|
||||
final navKey = GlobalKey<NavigatorState>();
|
||||
late BrowserHost host;
|
||||
final nav = _Nav();
|
||||
Future<void> open() async {
|
||||
navKey.currentState!.push(MaterialPageRoute<void>(
|
||||
builder: (_) => BrowserPage(
|
||||
c: c,
|
||||
bookmarks: bm,
|
||||
initialUrl: 'https://example.com/',
|
||||
viewBuilder: (h, url) {
|
||||
host = h;
|
||||
h.attach(nav);
|
||||
return const ColoredBox(color: Colors.white);
|
||||
},
|
||||
),
|
||||
));
|
||||
await tester.pumpAndSettle();
|
||||
}
|
||||
|
||||
await tester.pumpWidget(MaterialApp(navigatorKey: navKey, home: const Scaffold(body: Text('MKV 화면'))));
|
||||
await open();
|
||||
host.onHistory(true, false);
|
||||
await tester.pump();
|
||||
await tester.binding.handlePopRoute(); // 기기의 뒤로 키
|
||||
await tester.pumpAndSettle();
|
||||
expect(nav.backs, 1);
|
||||
expect(find.byType(BrowserPage), findsOneWidget);
|
||||
host.onHistory(false, true);
|
||||
await tester.pump();
|
||||
await tester.binding.handlePopRoute();
|
||||
await tester.pumpAndSettle();
|
||||
expect(nav.backs, 1);
|
||||
expect(find.byType(BrowserPage), findsNothing);
|
||||
|
||||
// 위쪽 ← (화면 이동) 은 웹 기록이 있어도 브라우저 화면을 닫는다
|
||||
await open();
|
||||
host.onHistory(true, false);
|
||||
await tester.pump();
|
||||
await tester.tap(find.byTooltip('뒤로'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(nav.backs, 1);
|
||||
expect(find.byType(BrowserPage), findsNothing);
|
||||
debugDefaultTargetPlatformOverride = null; // 끝나기 전에 되돌려야 한다 (테스트 검사)
|
||||
});
|
||||
|
||||
testWidgets('브라우저: ☆ 추가 · 표시줄 · 다운로드 버튼 · 관리 패널 (삭제 · 실행 취소)', (tester) async {
|
||||
tester.view.physicalSize = const Size(1500, 900);
|
||||
tester.view.devicePixelRatio = 1;
|
||||
addTearDown(tester.view.reset);
|
||||
final dir = tempDirUntilEnd('jj_bm_');
|
||||
|
||||
final c = AppController(PlatformServices(mediaTool: ProcessMediaTool('x', 'y'), storage: DesktopStorageService()));
|
||||
final bm = BookmarksController(p.join(dir.path, 'bookmarks.json'));
|
||||
final video = _Backend(DownloadKind.video);
|
||||
final d = DownloadManager(backends: [video], settings: () => c.settings, readClipboard: () async => null);
|
||||
final nav = _Nav();
|
||||
late BrowserHost host;
|
||||
|
||||
await tester.pumpWidget(MaterialApp(
|
||||
home: BrowserPage(
|
||||
c: c,
|
||||
bookmarks: bm,
|
||||
downloads: d,
|
||||
initialUrl: 'https://www.google.com/',
|
||||
viewBuilder: (h, url) {
|
||||
host = h;
|
||||
h.attach(nav);
|
||||
return const ColoredBox(color: Colors.white);
|
||||
},
|
||||
),
|
||||
));
|
||||
// 기본 즐겨찾기 표시줄
|
||||
expect(find.text('YouTube'), findsOneWidget);
|
||||
// 터치 화면: 즐겨찾기를 길게 누르면 오른쪽 클릭과 같은 메뉴
|
||||
await tester.longPress(find.text('YouTube'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('수정 · 이동'), findsOneWidget);
|
||||
await tester.tapAt(const Offset(5, 880));
|
||||
await tester.pumpAndSettle();
|
||||
ButtonStyleButton dl() =>
|
||||
tester.widget<ButtonStyleButton>(find.ancestor(of: find.text('다운로드'), matching: find.byWidgetPredicate((w) => w is ButtonStyleButton)));
|
||||
// 27: 동영상 페이지가 아니어도 받아 볼 수 있다 (덜 눈에 띄는 버튼 · 안내). 137: 테두리만 있는 버튼
|
||||
expect(dl().onPressed, isNotNull);
|
||||
expect(dl(), isA<OutlinedButton>());
|
||||
expect(find.byTooltip('다운로드 (이 페이지에서 동영상을 찾지 못했지만 받아 볼 수 있습니다)'), findsOneWidget);
|
||||
|
||||
// 주소창 입력 → 이동
|
||||
await tester.enterText(find.byType(TextField).first, 'youtube.com/watch?v=abc');
|
||||
await tester.testTextInput.receiveAction(TextInputAction.done);
|
||||
expect(nav.loads.last, 'https://youtube.com/watch?v=abc');
|
||||
host.onUrl('https://www.youtube.com/watch?v=abc');
|
||||
host.onTitle('재미있는 영상');
|
||||
await tester.pump();
|
||||
expect(dl().onPressed, isNotNull);
|
||||
expect(dl(), isA<FilledButton>(), reason: '137: 동영상 페이지는 진한 버튼');
|
||||
await tester.tap(find.text('다운로드'));
|
||||
await tester.pump();
|
||||
expect(video.started, ['https://www.youtube.com/watch?v=abc']);
|
||||
|
||||
// ☆ → Chrome 처럼 "즐겨찾기 추가됨" 창: 이름 · 폴더 고르기, 그 자리에서 새 폴더
|
||||
await tester.tap(find.byTooltip('즐겨찾기 추가 (Ctrl+D)'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('즐겨찾기 추가됨'), findsOneWidget);
|
||||
expect(bm.tree.parentOf(bm.tree.findByUrl('https://www.youtube.com/watch?v=abc')!.id)!.id, BookmarkTree.barId);
|
||||
await tester.tap(find.text('새 폴더'));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.enterText(find.byType(TextField).last, '음악');
|
||||
await tester.tap(find.text('확인'));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.text('완료'));
|
||||
await tester.pumpAndSettle();
|
||||
final saved = bm.tree.findByUrl('https://www.youtube.com/watch?v=abc')!;
|
||||
final folder = bm.tree.parentOf(saved.id)!;
|
||||
expect(folder.title, '음악');
|
||||
expect(bm.tree.parentOf(folder.id)!.id, BookmarkTree.barId);
|
||||
// 표시줄에는 폴더가 보인다
|
||||
expect(find.widgetWithText(TextButton, '음악'), findsOneWidget);
|
||||
|
||||
// 관리 패널 → 삭제 → 실행 취소
|
||||
await tester.tap(find.byTooltip('즐겨찾기 관리 (Ctrl+Shift+B)'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('즐겨찾기'), findsWidgets);
|
||||
await tester.tap(find.byTooltip('더 보기').last);
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.text('삭제').last);
|
||||
await tester.pumpAndSettle();
|
||||
expect(bm.tree.findByUrl('https://www.youtube.com/watch?v=abc'), isNull);
|
||||
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.pumpAndSettle();
|
||||
await tester.tap(find.text('삭제').last);
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('실행 취소'), findsOneWidget);
|
||||
await tester.pump(noticeDuration + const Duration(seconds: 1));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('실행 취소'), findsNothing);
|
||||
bm.undoRemove();
|
||||
await tester.pumpAndSettle();
|
||||
expect(bm.tree.findByUrl('https://www.youtube.com/watch?v=abc'), isNotNull);
|
||||
|
||||
// 패널에서 누르면 그 주소로 이동
|
||||
await tester.tap(find.text('OpenSubtitles').last);
|
||||
expect(nav.loads.last, 'https://www.opensubtitles.com/');
|
||||
|
||||
// 화면 분할: 작업 현황 (다운로드 진행 · 편집 목록)
|
||||
await tester.tap(find.byTooltip('작업 현황 보기 · 화면 분할 (Ctrl+Shift+J)'));
|
||||
await tester.pump(const Duration(milliseconds: 300));
|
||||
expect(find.text('작업 현황'), findsOneWidget);
|
||||
expect(find.text('쉬는 중'), findsOneWidget);
|
||||
expect(find.text('다운로드 1개'), findsOneWidget);
|
||||
await tester.tap(find.byTooltip('작업 현황 닫기'));
|
||||
await tester.pump(const Duration(milliseconds: 300));
|
||||
expect(find.text('작업 현황'), findsNothing);
|
||||
d.dispose();
|
||||
});
|
||||
|
||||
test('즐겨찾기 파일 저장 · 다시 불러오기 · 순서 바꾸기', () async {
|
||||
final dir = Directory.systemTemp.createTempSync('jj_bm2_');
|
||||
addTearDown(() => dir.deleteSync(recursive: true));
|
||||
|
|
@ -609,38 +162,4 @@ void main() {
|
|||
expect(b.tree.bar.children!.first.title, 'B');
|
||||
expect(b.tree.bar.children!.any((n) => n.isFolder && n.title == '영상'), isTrue);
|
||||
});
|
||||
|
||||
testWidgets('66-2: 처음 열면 위쪽 막대 아래에 화면 옮기기 안내 한 줄 (알림을 막지 않음) · ✕ 로 닫고 다시 열면 없음', (tester) async {
|
||||
tester.view.physicalSize = const Size(1500, 900);
|
||||
tester.view.devicePixelRatio = 1;
|
||||
addTearDown(tester.view.reset);
|
||||
final dir = Directory.systemTemp.createTempSync('jj_hint_');
|
||||
addTearDown(() => dir.deleteSync(recursive: true));
|
||||
final c = AppController(PlatformServices(mediaTool: ProcessMediaTool('x', 'y'), storage: DesktopStorageService()));
|
||||
final bm = BookmarksController(p.join(dir.path, 'bookmarks.json'));
|
||||
Widget page() => MaterialApp(
|
||||
home: BrowserPage(
|
||||
c: c,
|
||||
bookmarks: bm,
|
||||
initialUrl: 'https://example.com/',
|
||||
viewBuilder: (h, url) {
|
||||
h.attach(_Nav());
|
||||
return const ColoredBox(color: Colors.white);
|
||||
},
|
||||
),
|
||||
);
|
||||
await tester.pumpWidget(page());
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.textContaining('위쪽 막대를 좌우로 밀면'), findsOneWidget);
|
||||
expect(find.byType(SnackBar), findsNothing, reason: '알림 (다운로드 등) 을 뒤로 밀지 않게 알림이 아니라 한 줄');
|
||||
expect(c.settings.browser.swipeHinted, isTrue);
|
||||
await tester.tap(find.byTooltip('안내 닫기'));
|
||||
await tester.pump();
|
||||
expect(find.textContaining('위쪽 막대를 좌우로 밀면'), findsNothing);
|
||||
// 다시 열면 없음 (한 번만)
|
||||
await tester.pumpWidget(const SizedBox());
|
||||
await tester.pumpWidget(page());
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.textContaining('위쪽 막대를 좌우로 밀면'), findsNothing);
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,86 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:jj_mkvmaker/core/download_detect.dart';
|
||||
import 'package:jj_mkvmaker/services/downloader.dart';
|
||||
import 'package:jj_mkvmaker/ui/browser_page.dart';
|
||||
|
||||
/// 브라우저 시험의 대역 (browser_test · browser_page_test 가 같이 쓴다)
|
||||
/// 받기를 시작한 주소를 적어 두는 다운로드 엔진 (실제로 받지 않음)
|
||||
class RecordingDownloadBackend implements DownloadBackend {
|
||||
@override
|
||||
final DownloadKind kind;
|
||||
final started = <String>[];
|
||||
void Function()? changed;
|
||||
RecordingDownloadBackend(this.kind);
|
||||
@override
|
||||
Future<(String, List<PlaylistEntry>)?> expandPlaylist(String url) async => null;
|
||||
@override
|
||||
Future<void> start(DownloadTask t, void Function() changed) async {
|
||||
this.changed = changed;
|
||||
started.add(t.source);
|
||||
t.state = DownloadState.downloading;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> pause(DownloadTask t) async {}
|
||||
@override
|
||||
Future<void> cancel(DownloadTask t) async {}
|
||||
@override
|
||||
Future<void> shutdown() async {}
|
||||
}
|
||||
|
||||
/// 받은 조작 (멈춤 · 스크립트 · 주소 · 뒤로 · 쿠키 · JavaScript) 을 적어 두는 웹뷰 조작
|
||||
class RecordingWebNav implements WebNav {
|
||||
int paused = 0;
|
||||
@override
|
||||
Future<void> pauseMedia() async => paused++;
|
||||
final scripts = <String>[];
|
||||
@override
|
||||
Future<void> runScript(String js) async => scripts.add(js);
|
||||
final loads = <String>[];
|
||||
@override
|
||||
Future<void> load(String url) async => loads.add(url);
|
||||
int backs = 0;
|
||||
@override
|
||||
Future<void> back() async => backs++;
|
||||
@override
|
||||
Future<void> forward() async {}
|
||||
int reloads = 0;
|
||||
@override
|
||||
Future<void> reload() async => reloads++;
|
||||
@override
|
||||
Future<void> stop() async {}
|
||||
var exported = 0;
|
||||
@override
|
||||
Future<void> exportCookies() async => exported++;
|
||||
@override
|
||||
Future<void> clearData() async {}
|
||||
@override
|
||||
Future<Object?> evaluate(String js) async {
|
||||
scripts.add(js);
|
||||
return null;
|
||||
}
|
||||
|
||||
final javaScript = <bool>[];
|
||||
@override
|
||||
Future<void> setJavaScript(bool on) async => javaScript.add(on);
|
||||
}
|
||||
|
||||
/// 웹 뷰 대신: 몇 번 새로 만들어졌는지 센다 (250 - 덮개를 띄워도 웹 뷰를 다시 만들지 않아야 함)
|
||||
class ViewProbe extends StatefulWidget {
|
||||
static int created = 0;
|
||||
const ViewProbe({super.key});
|
||||
@override
|
||||
State<ViewProbe> createState() => _ViewProbeState();
|
||||
}
|
||||
|
||||
class _ViewProbeState extends State<ViewProbe> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
ViewProbe.created++;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => const ColoredBox(color: Colors.white);
|
||||
}
|
||||
Loading…
Reference in New Issue