JJ_MKVMaker/test/xplore_tools_test.dart

172 lines
9.0 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:jj_mkvmaker/core/file_ops.dart';
import 'package:jj_mkvmaker/core/text_codec.dart';
import 'package:jj_mkvmaker/ui/explorer_grid.dart';
import 'package:jj_mkvmaker/ui/text_viewer.dart';
import 'package:path/path.dart' as p;
/// X-plore 비교 g · h: 격자 보기 · 글 뷰어
void main() {
late Directory tmp;
setUp(() => tmp = Directory.systemTemp.createTempSync('jj_xptools_'));
tearDown(() => tmp.deleteSync(recursive: true));
testWidgets('h 글 뷰어: CP949 를 알아보고 나눠 읽기 [더 읽기]', (t) async {
final text = List.generate(40, (i) => '줄 $i 한글').join('\n');
final f = File(p.join(tmp.path, 'k.txt'))..writeAsBytesSync(encodeText(text, 'CP949').bytes);
await t.pumpWidget(MaterialApp(home: TextViewerPage(path: f.path, chunk: 120)));
for (var i = 0; i < 20 && find.textContaining('줄 0').evaluate().isEmpty; i++) {
await t.runAsync(() => Future<void>.delayed(const Duration(milliseconds: 30)));
await t.pump();
}
expect(find.text('줄 0 한글'), findsOneWidget);
expect(find.text('더 읽기'), findsOneWidget, reason: '큰 파일은 나눠 읽음');
for (var k = 0; k < 10 && find.text('더 읽기').evaluate().isNotEmpty; k++) {
await t.tap(find.text('더 읽기'));
for (var i = 0; i < 5; i++) {
await t.runAsync(() => Future<void>.delayed(const Duration(milliseconds: 30)));
await t.pump();
}
}
expect(find.text('더 읽기'), findsNothing, reason: '다 읽음');
expect(find.text('CP949 (EUC-KR)'), findsOneWidget);
});
test('h 글 뷰어: 아주 긴 줄만 나눠 그린다 (줄 번호는 첫 조각에만, 이모지를 반으로 자르지 않음)', () {
expect(viewerRows('a\nbb', 10), [(1, 'a'), (2, 'bb')]);
final long = 'x' * 1000;
final rows = viewerRows('first\n$long\nlast', 10);
expect(rows.first, (1, 'first'));
expect(rows.last, (3, 'last'));
final mid = rows.sublist(1, rows.length - 1);
expect(mid.map((r) => r.$2).join(), long, reason: '글은 그대로 (빠지거나 겹치지 않음)');
expect(mid.first.$1, 2);
expect(mid.skip(1).every((r) => r.$1 == 0), isTrue);
expect(mid.every((r) => r.$2.length <= 40), isTrue, reason: '화면 네 줄쯤씩');
expect(viewerRows('y' * 80, 10).length, 1, reason: '8배까지는 그대로 (화면에서 줄바꿈)');
// 나누는 자리에 두 단위 글자 (😀) 가 걸리면 앞으로 당긴다
final emoji = '${'z' * 39}😀${'z' * 100}';
final cut = viewerRows(emoji, 10);
expect(cut.map((r) => r.$2).join(), emoji);
expect(cut.every((r) => !r.$2.endsWith('\uD83D')), isTrue);
});
testWidgets('h 글 뷰어: 줄바꿈 없는 큰 파일 (200KB 한 줄) 도 나눠 그려 멈추지 않는다', (t) async {
final f = File(p.join(tmp.path, 'one.txt'))..writeAsStringSync('${'0123456789' * 20000}END');
t.view.physicalSize = const Size(412, 900);
t.view.devicePixelRatio = 1;
addTearDown(t.view.reset);
await t.pumpWidget(MaterialApp(home: TextViewerPage(path: f.path)));
for (var i = 0; i < 40 && find.textContaining('0123456789').evaluate().isEmpty; i++) {
await t.runAsync(() => Future<void>.delayed(const Duration(milliseconds: 30)));
await t.pump();
}
final texts = t.widgetList<Text>(find.textContaining('0123456789')).toList();
expect(texts.length, greaterThan(1), reason: '한 줄을 여러 조각으로');
expect(texts.every((x) => (x.data ?? '').length < 2000), isTrue, reason: '조각마다 짧게');
expect(find.text('1'), findsOneWidget, reason: '줄 번호는 첫 조각에만');
});
testWidgets('h 글 뷰어 (28 지적 · 6366a2f 회귀): 긴 줄을 나눠 그려도 Ctrl+A → Ctrl+C 는 줄 전체 · 막대 컨트롤러', (t) async {
final line = List.generate(1109, (i) => 'abcdefghi${i % 10}').join();
final f = File(p.join(tmp.path, 'long.txt'))..writeAsStringSync('$line\nend');
t.view.physicalSize = const Size(412, 900);
t.view.devicePixelRatio = 1;
addTearDown(t.view.reset);
String? copied;
t.binding.defaultBinaryMessenger.setMockMethodCallHandler(SystemChannels.platform, (call) async {
if (call.method == 'Clipboard.setData') copied = (call.arguments as Map)['text'] as String?;
return null;
});
addTearDown(() => t.binding.defaultBinaryMessenger.setMockMethodCallHandler(SystemChannels.platform, null));
await t.pumpWidget(MaterialApp(home: TextViewerPage(path: f.path)));
for (var i = 0; i < 40 && find.textContaining('abcdefghi0').evaluate().isEmpty; i++) {
await t.runAsync(() => Future<void>.delayed(const Duration(milliseconds: 30)));
await t.pump();
}
expect(t.widgetList<Text>(find.textContaining('abcdefghi')).length, greaterThan(1), reason: '화면에는 나눠 그림');
await t.tapAt(const Offset(150, 140));
await t.pump();
await t.sendKeyDownEvent(LogicalKeyboardKey.controlLeft);
await t.sendKeyEvent(LogicalKeyboardKey.keyA);
await t.sendKeyEvent(LogicalKeyboardKey.keyC);
await t.sendKeyUpEvent(LogicalKeyboardKey.controlLeft);
await t.pump();
expect(copied, isNotNull);
expect(copied!.length, line.length + 4, reason: '줄 전체 + "\nend" (보이는 조각만이 아님)');
expect(copied, '$line\nend');
// 막대에 컨트롤러가 붙어 있다 (데스크톱에서 ScrollPosition 없음 assert 가 나지 않게)
final bar = t.widget<Scrollbar>(find.byType(Scrollbar));
expect(bar.controller, isNotNull);
expect(bar.controller!.hasClients, isTrue);
});
test('h 글 뷰어 (관리자 지시 10/19): 넓은 글자 (한글 · 한자) 는 두 칸으로 세어 네 줄 분량이 꽉 차게 나눈다', () {
final line = List.generate(600, (i) => i.isEven ? '가' : 'a').join();
final rows = viewerRows(line, 10, wide: 2);
expect(rows.map((r) => r.$2).join(), line);
double units(String s) => s.runes.fold(0.0, (a, c) => a + (isWideChar(c) ? 2 : 1));
for (final r in rows.take(rows.length - 1)) {
expect(units(r.$2), inInclusiveRange(39, 40), reason: '네 줄 (40칸) 에서 한 칸 넘게 비지 않음: ${r.$2}');
}
expect(isWideChar('漢'.codeUnitAt(0)), isTrue);
expect(isWideChar('ア'.codeUnitAt(0)), isTrue);
expect(isWideChar('A'.codeUnitAt(0)), isTrue);
expect(isWideChar('a'.codeUnitAt(0)), isFalse);
// 비율이 2 가 아닌 글꼴 (잰 값) 도 그대로
final r15 = viewerRows('가' * 400, 10, wide: 1.5);
expect(r15.first.$2.length, 26, reason: '40 / 1.5 = 26.6 → 26자');
});
testWidgets('h 글 뷰어: 한글이 섞인 긴 줄도 나눈 조각마다 화면 네 줄 (다섯째 반쪽 줄 없음)', (t) async {
// 한 글자씩 바뀌어 어디서든 줄을 바꿀 수 있는 글 (영문 낱말이 이어지면 낱말 단위로 줄이 바뀌어 보통 글처럼 줄 끝이 조금 빈다)
final line = List.generate(3000, (i) => i.isEven ? '한' : 'x').join();
final f = File(p.join(tmp.path, 'ko.txt'))..writeAsStringSync(line);
t.view.physicalSize = const Size(412, 900);
t.view.devicePixelRatio = 1;
addTearDown(t.view.reset);
await t.pumpWidget(MaterialApp(home: TextViewerPage(path: f.path)));
for (var i = 0; i < 40 && find.textContaining('한x한x').evaluate().isEmpty; i++) {
await t.runAsync(() => Future<void>.delayed(const Duration(milliseconds: 30)));
await t.pump();
}
final pieces = find.textContaining('한x한x');
expect(pieces.evaluate().length, greaterThan(1));
final first = t.renderObject<RenderParagraph>(find.descendant(of: pieces.first, matching: find.byType(RichText)));
final txt = t.widget<Text>(pieces.first).data!;
final lines = {for (final bx in first.getBoxesForSelection(TextSelection(baseOffset: 0, extentOffset: txt.length))) bx.top}.length;
expect(lines, 4, reason: '네 줄 분량으로 나눔 (다섯째 반쪽 줄 없음)');
});
testWidgets('g 격자 보기: 그림 · 동영상만, 누르면 열기', (t) async {
final png = File(p.join(tmp.path, 'a.png'))..writeAsBytesSync([0x89, 0x50]);
final opened = <String>[];
final entries = [
FileEntry(png.path, isDir: false, size: 2, modified: DateTime(2026)),
FileEntry(p.join(tmp.path, 'b.mkv'), isDir: false, size: 2, modified: DateTime(2026)),
FileEntry(p.join(tmp.path, 'c.txt'), isDir: false, size: 2, modified: DateTime(2026)),
FileEntry(p.join(tmp.path, 'dir'), isDir: true, modified: DateTime(2026)),
];
expect(ExplorerGridPage.pick(entries, const ['png']).map((e) => e.name), ['a.png', 'b.mkv']);
await t.pumpWidget(MaterialApp(
home: ExplorerGridPage(
title: 'T',
entries: entries,
imageExts: const ['png'],
videoThumb: (_) async => null,
onOpen: (e) async => opened.add(e.name),
),
));
await t.pump();
expect(find.text('a.png'), findsOneWidget);
expect(find.text('c.txt'), findsNothing);
await t.tap(find.text('b.mkv'));
expect(opened, ['b.mkv']);
});
}