E1: 탐색기 누르기 · 고르기 모듈 Selection · ClickPolicy (lib/core/selection.dart) + 약속 시험 한 장
- 기준: wiki "10 wiki/규칙/파일 탐색기 선택 규칙 (사용자 지시 모음)" 규칙 2~6 · 8 - ClickPolicy.decide: 누르기 (한 번 · 두 번 · 길게 · 오른쪽) · 종류 · 선택 모드 · 펼침 · 목록 · 다시 → 할 일 하나 - Selection: 파일 고름/취소 · 폴더 = 자기 + 안 전체 · 안의 하나만 빼기 · Rsync 폴더 하나 - 관리자 결정 2 (10/22): 하나를 뺀 폴더를 다시 누르면 그 폴더와 안의 것 전체 취소 - 목록을 읽지 못하면 표시를 바꾸지 않고 던짐 (반쯤 바뀐 상태 없음) - 약속 시험 test/selection_contract_test.dart 23개 (규칙 표 16 + 실제 폴더 7), analyze 0 뒤 같은 판 2회 통과 - 지문 (git blob): selection.dartsplit_pagesfeb2d80a· 시험ceb4e16e- 화면은 아직 이 모듈을 부르지 않음 (E2) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
parent
6e1e191779
commit
88790819be
|
|
@ -0,0 +1,225 @@
|
||||||
|
import 'file_ops.dart' show isSameOrInside, samePath;
|
||||||
|
import 'vfs.dart' show vDirname;
|
||||||
|
|
||||||
|
/// 파일 탐색기의 누르기 · 고르기 (E1, 관리자 c5 승인 10/22).
|
||||||
|
///
|
||||||
|
/// 기준: wiki "10 wiki/규칙/파일 탐색기 선택 규칙 (사용자 지시 모음)" 의 규칙 2~6 · 8. 화면은 이 모듈을 부르기만 한다.
|
||||||
|
///
|
||||||
|
/// 약속
|
||||||
|
/// - [ClickPolicy.decide]: 누르기 (한 번 · 두 번 · 길게 · 오른쪽) · 항목 종류 · 선택 모드 · 펼침 → 할 일 하나 ([ClickAction]).
|
||||||
|
/// 모르는 조합은 [ClickAction.none] (아무것도 하지 않음). 화면 · 기기 · 시각을 모른다 (같은 입력 = 같은 결과).
|
||||||
|
/// - [Selection]: 창 하나의 표시 (고른 경로 집합).
|
||||||
|
/// · 파일: 누르면 고름, 다시 누르면 취소 (규칙 5)
|
||||||
|
/// · 폴더: 누르면 자기 + 안의 것 전체, 다시 누르면 자기와 안의 것 전체 취소 (규칙 6)
|
||||||
|
/// · 고른 폴더 안의 항목 하나를 누르면 그것만 빠짐 (나머지는 고른 채)
|
||||||
|
/// · 그렇게 하나를 뺀 폴더를 다시 누르면 그 폴더와 안의 것 전체 취소 (관리자 결정 10/22)
|
||||||
|
/// · Rsync ([Selection.rsync]): 폴더만, 하나만 (다시 누르면 취소) (규칙 8)
|
||||||
|
/// 오류: 안의 목록을 읽지 못하면 ([SelectionLister] 가 던짐) 표시를 바꾸지 않고 그대로 던진다 (반쯤 바뀐 상태를 남기지 않음).
|
||||||
|
/// - 경로 비교는 [samePath] · [isSameOrInside] (Windows 대소문자 무시 · WebDAV 주소) 를 그대로 쓴다.
|
||||||
|
|
||||||
|
/// 줄의 종류
|
||||||
|
enum ItemKind {
|
||||||
|
file,
|
||||||
|
folder,
|
||||||
|
|
||||||
|
/// 저장 장치 · 서버 · 즐겨찾기 같은 트리 맨 위 (고를 수 없음)
|
||||||
|
root,
|
||||||
|
|
||||||
|
/// 목록의 ".." (위 폴더로)
|
||||||
|
up,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 누르기
|
||||||
|
enum Press { tap, doubleTap, longPress, secondary }
|
||||||
|
|
||||||
|
/// 누르기의 결과 (화면이 할 일)
|
||||||
|
enum ClickAction {
|
||||||
|
none,
|
||||||
|
|
||||||
|
/// 지금 줄로 (파일 한 번 누르기 = 선택)
|
||||||
|
focus,
|
||||||
|
|
||||||
|
/// 실행 (파일) · 들어가기 (목록의 폴더 · ..)
|
||||||
|
open,
|
||||||
|
|
||||||
|
/// 트리 폴더 펼치기
|
||||||
|
expand,
|
||||||
|
|
||||||
|
/// 트리 폴더 접기
|
||||||
|
collapse,
|
||||||
|
|
||||||
|
/// 기능 메뉴 (복사 · 이동 · 삭제 …)
|
||||||
|
menu,
|
||||||
|
|
||||||
|
/// 표시 바꾸기 ([Selection.toggle])
|
||||||
|
toggleMark,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 규칙 2 · 3 · 5 · 6: 누르기 → 할 일
|
||||||
|
class ClickPolicy {
|
||||||
|
const ClickPolicy._();
|
||||||
|
|
||||||
|
/// [selectMode] 선택 모드 (가운데 [선택]) · [expanded] 그 폴더가 펼쳐져 있는지 ·
|
||||||
|
/// [list] 트리가 아니라 목록 (폴더 + 목록 배치의 오른쪽) 의 줄인지 ·
|
||||||
|
/// [repeat] 같은 줄을 두 번 누르기 시간 안에 다시 누른 것인지 (폴더는 두 번 누르기를 기다리지 않으므로,
|
||||||
|
/// 두 번째 누르기로 방금 펼친 것을 접지 않게)
|
||||||
|
static ClickAction decide(Press press, ItemKind kind,
|
||||||
|
{required bool selectMode, bool expanded = false, bool list = false, bool repeat = false}) {
|
||||||
|
final real = kind == ItemKind.file || kind == ItemKind.folder;
|
||||||
|
switch (press) {
|
||||||
|
case Press.longPress:
|
||||||
|
case Press.secondary:
|
||||||
|
return real ? ClickAction.menu : ClickAction.none;
|
||||||
|
case Press.doubleTap:
|
||||||
|
// 두 번 = 실행 (파일만, 선택 모드 밖). 폴더 · 선택 모드는 한 번 누르기로 이미 처리됨
|
||||||
|
return kind == ItemKind.file && !selectMode ? ClickAction.open : ClickAction.none;
|
||||||
|
case Press.tap:
|
||||||
|
if (kind == ItemKind.up) return ClickAction.open;
|
||||||
|
if (selectMode && real) return ClickAction.toggleMark;
|
||||||
|
if (kind == ItemKind.file) return ClickAction.focus;
|
||||||
|
if (list && kind == ItemKind.folder) return ClickAction.open;
|
||||||
|
if (!expanded) return ClickAction.expand;
|
||||||
|
return repeat ? ClickAction.none : ClickAction.collapse;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 폴더 안의 경로들 (바로 아래만). 읽지 못하면 던진다.
|
||||||
|
typedef SelectionLister = Future<List<String>> Function(String dir);
|
||||||
|
|
||||||
|
/// 창 하나의 표시 (규칙 5 · 6 · 8)
|
||||||
|
class Selection {
|
||||||
|
Selection({this.rsync = false});
|
||||||
|
|
||||||
|
/// Rsync 화면: 폴더만 · 하나만 (규칙 8)
|
||||||
|
final bool rsync;
|
||||||
|
|
||||||
|
final List<String> _marked = [];
|
||||||
|
|
||||||
|
/// 고른 폴더에서 하나를 빼서 나눈 폴더 (다시 누르면 전체 취소 - 관리자 결정 10/22)
|
||||||
|
final List<String> _split = [];
|
||||||
|
|
||||||
|
/// 범위 고르기의 시작 (마지막으로 고른 것)
|
||||||
|
String? anchor;
|
||||||
|
|
||||||
|
/// 고른 경로 (바깥 것만 - 폴더를 고르면 안의 것은 따로 들지 않음)
|
||||||
|
List<String> get marked => List.unmodifiable(_marked);
|
||||||
|
|
||||||
|
bool get isEmpty => _marked.isEmpty;
|
||||||
|
|
||||||
|
/// [path] 가 표시되었는지 (자기 자신, 또는 위 폴더가 표시됨). Rsync 는 그 폴더만.
|
||||||
|
bool isMarked(String path) => _marked.any((m) => rsync ? samePath(path, m) : isSameOrInside(path, m));
|
||||||
|
|
||||||
|
/// [path] 를 [Selection] 의 규칙대로 바꾼다. 폴더를 나눌 때만 [list] 로 안을 읽는다.
|
||||||
|
Future<void> toggle(String path, {required bool isDir, required SelectionLister list}) async {
|
||||||
|
if (rsync) {
|
||||||
|
if (!isDir) return;
|
||||||
|
final again = _marked.length == 1 && samePath(_marked.first, path);
|
||||||
|
_marked.clear();
|
||||||
|
if (!again) _marked.add(path);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (isMarked(path)) {
|
||||||
|
final owner = _marked.firstWhere((m) => isSameOrInside(path, m));
|
||||||
|
if (samePath(owner, path)) {
|
||||||
|
_clearInside(path);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 위 폴더 [owner] 로 함께 골라진 것: 그것만 빼고 owner 에서 path 까지 가는 길의 다른 항목은 고른 채로
|
||||||
|
final keep = <String>[];
|
||||||
|
final splits = <String>[];
|
||||||
|
var dir = owner;
|
||||||
|
while (!samePath(dir, path)) {
|
||||||
|
final kids = await list(dir);
|
||||||
|
splits.add(dir);
|
||||||
|
String? next;
|
||||||
|
for (final k in kids) {
|
||||||
|
if (isSameOrInside(path, k)) {
|
||||||
|
next = k;
|
||||||
|
} else {
|
||||||
|
keep.add(k);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (next == null) break;
|
||||||
|
dir = next;
|
||||||
|
}
|
||||||
|
_marked.removeWhere((m) => samePath(m, owner));
|
||||||
|
_marked.addAll(keep);
|
||||||
|
_split.addAll(splits);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (isDir && _split.any((s) => samePath(s, path))) {
|
||||||
|
// 하나를 뺀 폴더를 다시 누름: 그 폴더와 안의 것 전체 취소
|
||||||
|
_clearInside(path);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_marked.removeWhere((m) => isSameOrInside(m, path)); // 안에 따로 고른 것은 이 폴더 하나로
|
||||||
|
_split.removeWhere((s) => isSameOrInside(s, path));
|
||||||
|
_marked.add(path);
|
||||||
|
anchor = path;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// [from] (보통 [anchor]) 부터 [to] 까지 [visible] (보이는 차례) 의 것을 모두 고른다. 둘 중 하나가 없으면 그대로.
|
||||||
|
void range(List<String> visible, String to, {String? from}) {
|
||||||
|
final a = visible.indexWhere((x) => samePath(x, from ?? anchor ?? ''));
|
||||||
|
final b = visible.indexWhere((x) => samePath(x, to));
|
||||||
|
if (a < 0 || b < 0 || rsync) return;
|
||||||
|
for (var i = a < b ? a : b; i <= (a < b ? b : a); i++) {
|
||||||
|
if (!isMarked(visible[i])) _marked.add(visible[i]);
|
||||||
|
}
|
||||||
|
_normalize();
|
||||||
|
anchor = to;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// [paths] 를 모두 고른다 (모두 선택 · 틀에 맞는 것)
|
||||||
|
void addAll(Iterable<String> paths) {
|
||||||
|
if (rsync) return;
|
||||||
|
for (final x in paths) {
|
||||||
|
if (!isMarked(x)) _marked.add(x);
|
||||||
|
}
|
||||||
|
_normalize();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// [paths] 중 고른 것은 풀고 안 고른 것은 고른다 (뒤집기)
|
||||||
|
void invert(Iterable<String> paths) {
|
||||||
|
if (rsync) return;
|
||||||
|
for (final x in paths) {
|
||||||
|
if (_marked.any((m) => samePath(m, x))) {
|
||||||
|
_marked.removeWhere((m) => samePath(m, x));
|
||||||
|
} else if (!isMarked(x)) {
|
||||||
|
_marked.add(x);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_normalize();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 지워지거나 옮겨진 경로를 표시에서 뺀다 (안의 것 포함)
|
||||||
|
void forget(String path) {
|
||||||
|
_marked.removeWhere((m) => isSameOrInside(m, path));
|
||||||
|
_split.removeWhere((s) => isSameOrInside(s, path));
|
||||||
|
}
|
||||||
|
|
||||||
|
void clear() {
|
||||||
|
_marked.clear();
|
||||||
|
_split.clear();
|
||||||
|
anchor = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 일할 대상: 고른 것, 없으면 지금 줄 [focused] 하나 (없으면 빈 목록)
|
||||||
|
List<String> targets(String? focused) => _marked.isNotEmpty ? marked : [?focused];
|
||||||
|
|
||||||
|
/// [dir] 바로 안에서 고른 개수 (폴더 이름 옆 표시 · 아래 줄 숫자)
|
||||||
|
int countIn(String dir) => _marked.where((m) => samePath(vDirname(m), dir)).length;
|
||||||
|
|
||||||
|
void _clearInside(String path) {
|
||||||
|
_marked.removeWhere((m) => isSameOrInside(m, path));
|
||||||
|
_split.removeWhere((s) => isSameOrInside(s, path));
|
||||||
|
if (anchor != null && isSameOrInside(anchor!, path)) anchor = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 다른 표시 안에 든 표시는 뺀다 (폴더 하나로 안의 것까지 고른 것이므로)
|
||||||
|
void _normalize() {
|
||||||
|
final all = List.of(_marked);
|
||||||
|
_marked.removeWhere((m) => all.any((o) => !identical(o, m) && !samePath(o, m) && isSameOrInside(m, o)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,155 @@
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:jj_mkvmaker/core/selection.dart';
|
||||||
|
import 'package:path/path.dart' as p;
|
||||||
|
|
||||||
|
/// E1 약속 시험 (모듈 하나에 한 장): lib/core/selection.dart 맨 위 약속을 그대로 본다.
|
||||||
|
/// 기준은 wiki "파일 탐색기 선택 규칙 (사용자 지시 모음)" 규칙 2~6 · 8 과 관리자 결정 (10/22).
|
||||||
|
/// 폴더 안 목록은 실제 파일 시스템 (dart:io) 에서 읽는다.
|
||||||
|
void main() {
|
||||||
|
group('ClickPolicy (규칙 2 · 3 · 5 · 6)', () {
|
||||||
|
// (누르기, 종류, 선택 모드, 펼침, 목록, 다시, 기대) - 규칙 표 그대로
|
||||||
|
const table = [
|
||||||
|
(Press.tap, ItemKind.file, false, false, false, false, ClickAction.focus), // 규칙 2 한 번 = 선택
|
||||||
|
(Press.doubleTap, ItemKind.file, false, false, false, false, ClickAction.open), // 규칙 2 두 번 = 실행
|
||||||
|
(Press.longPress, ItemKind.file, false, false, false, false, ClickAction.menu), // 규칙 2 길게 = 메뉴
|
||||||
|
(Press.secondary, ItemKind.folder, false, false, false, false, ClickAction.menu),
|
||||||
|
(Press.longPress, ItemKind.root, false, false, false, false, ClickAction.none), // 장치 · .. 는 메뉴 없음
|
||||||
|
(Press.tap, ItemKind.folder, false, false, false, false, ClickAction.expand), // 규칙 3 펼치기
|
||||||
|
(Press.tap, ItemKind.folder, false, true, false, false, ClickAction.collapse), // 규칙 3 접기
|
||||||
|
(Press.tap, ItemKind.folder, false, true, false, true, ClickAction.none), // 두 번 누르기의 두 번째는 접지 않음
|
||||||
|
(Press.tap, ItemKind.root, false, true, false, false, ClickAction.collapse),
|
||||||
|
(Press.tap, ItemKind.folder, false, false, true, false, ClickAction.open), // 목록의 폴더 = 들어가기
|
||||||
|
(Press.tap, ItemKind.up, false, false, true, false, ClickAction.open),
|
||||||
|
(Press.tap, ItemKind.file, true, false, false, false, ClickAction.toggleMark), // 규칙 5
|
||||||
|
(Press.tap, ItemKind.folder, true, false, false, false, ClickAction.toggleMark), // 규칙 6
|
||||||
|
(Press.tap, ItemKind.root, true, false, false, false, ClickAction.expand), // 장치는 고르지 않음
|
||||||
|
(Press.doubleTap, ItemKind.file, true, false, false, false, ClickAction.none),
|
||||||
|
(Press.longPress, ItemKind.file, true, false, false, false, ClickAction.menu),
|
||||||
|
];
|
||||||
|
for (final (press, kind, sel, exp, list, rep, want) in table) {
|
||||||
|
test('$press $kind 선택=$sel 펼침=$exp 목록=$list 다시=$rep → $want', () {
|
||||||
|
expect(ClickPolicy.decide(press, kind, selectMode: sel, expanded: exp, list: list, repeat: rep), want);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
group('Selection (규칙 5 · 6 · 8)', () {
|
||||||
|
late Directory tmp;
|
||||||
|
late String show, e1, e2, e3, sub, s1, notes;
|
||||||
|
var reads = 0;
|
||||||
|
Future<List<String>> lister(String dir) async {
|
||||||
|
reads++;
|
||||||
|
return [for (final e in Directory(dir).listSync()) e.path]..sort();
|
||||||
|
}
|
||||||
|
|
||||||
|
setUp(() {
|
||||||
|
tmp = Directory.systemTemp.createTempSync('jj_sel_');
|
||||||
|
show = p.join(tmp.path, 'Show');
|
||||||
|
sub = p.join(show, 'Extras');
|
||||||
|
Directory(sub).createSync(recursive: true);
|
||||||
|
e1 = p.join(show, 'ep01.mkv');
|
||||||
|
e2 = p.join(show, 'ep02.mkv');
|
||||||
|
e3 = p.join(show, 'ep03.mkv');
|
||||||
|
s1 = p.join(sub, 'bonus.mkv');
|
||||||
|
notes = p.join(tmp.path, 'notes.txt');
|
||||||
|
for (final f in [e1, e2, e3, s1, notes]) {
|
||||||
|
File(f).writeAsStringSync('x');
|
||||||
|
}
|
||||||
|
reads = 0;
|
||||||
|
});
|
||||||
|
tearDown(() => tmp.deleteSync(recursive: true));
|
||||||
|
|
||||||
|
test('파일: 누르면 고름 · 다시 누르면 취소 (규칙 5)', () async {
|
||||||
|
final s = Selection();
|
||||||
|
await s.toggle(e1, isDir: false, list: lister);
|
||||||
|
expect(s.marked, [e1]);
|
||||||
|
await s.toggle(e2, isDir: false, list: lister);
|
||||||
|
expect(s.marked, [e1, e2]);
|
||||||
|
await s.toggle(e1, isDir: false, list: lister);
|
||||||
|
expect(s.marked, [e2]);
|
||||||
|
expect(reads, 0); // 파일은 목록을 읽지 않음
|
||||||
|
});
|
||||||
|
|
||||||
|
test('폴더: 자기 + 안 전체 · 다시 누르면 전체 취소 (규칙 6)', () async {
|
||||||
|
final s = Selection();
|
||||||
|
await s.toggle(e1, isDir: false, list: lister); // 안에 따로 고른 것은 폴더 하나로 합쳐짐
|
||||||
|
await s.toggle(show, isDir: true, list: lister);
|
||||||
|
expect(s.marked, [show]);
|
||||||
|
for (final x in [show, e1, e2, e3, sub, s1]) {
|
||||||
|
expect(s.isMarked(x), isTrue, reason: x);
|
||||||
|
}
|
||||||
|
expect(s.isMarked(notes), isFalse);
|
||||||
|
await s.toggle(show, isDir: true, list: lister);
|
||||||
|
expect(s.isEmpty, isTrue);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('고른 폴더 안의 하나를 누르면 그것만 빠짐 (깊은 곳도)', () async {
|
||||||
|
final s = Selection();
|
||||||
|
await s.toggle(show, isDir: true, list: lister);
|
||||||
|
await s.toggle(e1, isDir: false, list: lister);
|
||||||
|
expect(s.isMarked(e1), isFalse);
|
||||||
|
for (final x in [e2, e3, sub, s1]) {
|
||||||
|
expect(s.isMarked(x), isTrue, reason: x);
|
||||||
|
}
|
||||||
|
expect(s.isMarked(show), isFalse);
|
||||||
|
// 두 단계 아래
|
||||||
|
final t = Selection();
|
||||||
|
await t.toggle(show, isDir: true, list: lister);
|
||||||
|
await t.toggle(s1, isDir: false, list: lister);
|
||||||
|
expect(t.isMarked(s1), isFalse);
|
||||||
|
expect([e1, e2, e3].every(t.isMarked), isTrue);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('하나를 뺀 폴더를 다시 누르면 그 폴더와 안 전체 취소 (관리자 결정 10/22)', () async {
|
||||||
|
final s = Selection();
|
||||||
|
await s.toggle(show, isDir: true, list: lister);
|
||||||
|
await s.toggle(e1, isDir: false, list: lister);
|
||||||
|
await s.toggle(notes, isDir: false, list: lister); // 바깥 것은 남음
|
||||||
|
await s.toggle(show, isDir: true, list: lister);
|
||||||
|
expect(s.marked, [notes]);
|
||||||
|
// 그 뒤 다시 누르면 처음처럼 전체 고름
|
||||||
|
await s.toggle(show, isDir: true, list: lister);
|
||||||
|
expect(s.isMarked(e1), isTrue);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('목록을 읽지 못하면 표시를 바꾸지 않고 던짐', () async {
|
||||||
|
final s = Selection();
|
||||||
|
await s.toggle(show, isDir: true, list: lister);
|
||||||
|
Future<List<String>> broken(String dir) async => throw FileSystemException('읽지 못함', dir);
|
||||||
|
await expectLater(s.toggle(e1, isDir: false, list: broken), throwsA(isA<FileSystemException>()));
|
||||||
|
expect(s.marked, [show]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('범위 · 모두 · 뒤집기 · 지운 것 빼기 · 대상', () async {
|
||||||
|
final s = Selection();
|
||||||
|
final visible = [e1, e2, e3];
|
||||||
|
await s.toggle(e1, isDir: false, list: lister);
|
||||||
|
s.range(visible, e3);
|
||||||
|
expect(s.marked, [e1, e2, e3]);
|
||||||
|
s.invert([e2]);
|
||||||
|
expect(s.marked, [e1, e3]);
|
||||||
|
s.forget(e1);
|
||||||
|
expect(s.marked, [e3]);
|
||||||
|
s.addAll([show]);
|
||||||
|
expect(s.marked, [show]);
|
||||||
|
expect(s.targets(notes), [show]);
|
||||||
|
s.clear();
|
||||||
|
expect(s.targets(notes), [notes]);
|
||||||
|
expect(s.targets(null), isEmpty);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Rsync: 폴더만 · 하나만 · 다시 누르면 취소 (규칙 8)', () async {
|
||||||
|
final s = Selection(rsync: true);
|
||||||
|
await s.toggle(e1, isDir: false, list: lister);
|
||||||
|
expect(s.isEmpty, isTrue);
|
||||||
|
await s.toggle(show, isDir: true, list: lister);
|
||||||
|
await s.toggle(sub, isDir: true, list: lister);
|
||||||
|
expect(s.marked, [sub]);
|
||||||
|
expect(s.isMarked(s1), isFalse); // Rsync 는 고른 그 폴더만
|
||||||
|
await s.toggle(sub, isDir: true, list: lister);
|
||||||
|
expect(s.isEmpty, isTrue);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue