feat(mobile): UI-01 upload center, capture and auxiliary pages
Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
@@ -1,9 +1,11 @@
|
||||
/// 上传队列页 widget 测试(P3 计划 Task 9,需求 4.3)。
|
||||
/// 上传中心 widget 测试(UI-01 §4.4,P3 计划 Task 9 基础上重构)。
|
||||
///
|
||||
/// 打桩面:RecordingEngine(记录 retryNow/cancel,不出队)+ 内存 TaskStore
|
||||
/// 种子 + FakeAdapter 相册列表 + 临时目录准备器(准备失败重试换源)。
|
||||
/// 覆盖:任务行渲染(状态徽章/错误文案/相册名)、三维筛选交互、按状态
|
||||
/// 动作(重试/取消/删除)、批量重试条、写通知防抖实时刷新、行级进度条。
|
||||
/// 覆盖:顶部摘要卡(范围标注 + 四组任务数)、四组状态筛选 chips、
|
||||
/// 任务行渲染(状态徽章/状态专属文案/错误文案/相册名)、按状态动作
|
||||
/// (重试/重新选择/取消/移除记录 + 确认弹窗)、批量重试条口径(仅
|
||||
/// 可重试失败)、写通知防抖实时刷新、行级进度条(分数 + 百分比文案)。
|
||||
library;
|
||||
|
||||
import 'dart:io';
|
||||
@@ -127,6 +129,11 @@ void main() {
|
||||
});
|
||||
|
||||
Future<void> pumpQueue(WidgetTester tester) async {
|
||||
// 加高视口:摘要卡 + 筛选栏 + 批量条压缩列表区,默认 800x600 下
|
||||
// 第二行落在懒构建区外(ListView.builder 不构建不可见项)。
|
||||
tester.view.physicalSize = const Size(800, 1600);
|
||||
tester.view.devicePixelRatio = 1;
|
||||
addTearDown(tester.view.reset);
|
||||
await tester.pumpWidget(
|
||||
ProviderScope(
|
||||
overrides: [
|
||||
@@ -213,6 +220,10 @@ void main() {
|
||||
return task;
|
||||
}
|
||||
|
||||
/// 行级锚定:任务行 Padding 带 ValueKey('queue-task-{task.id}')。
|
||||
Finder rowOf(String filename) =>
|
||||
find.byKey(ValueKey('queue-task-task-$filename'));
|
||||
|
||||
/// 等防抖重查落定(写通知 → 200ms Timer → 异步查询;不 pumpAndSettle——
|
||||
/// uploading 行的不确定态进度条动画不会停)。
|
||||
Future<void> settleDebounce(WidgetTester tester) async {
|
||||
@@ -220,6 +231,63 @@ void main() {
|
||||
await tester.pump(const Duration(milliseconds: 120));
|
||||
}
|
||||
|
||||
/// 触发一个会走真实文件 IO 的按钮:在 runAsync(真实事件循环)内直接
|
||||
/// 调用其回调——widget 测试的 fake-async 区里 dart:io 完成回调不会被
|
||||
/// pump 冲刷,入队链路必须整体跑在真实区;返回后 pump 刷新徽章防抖。
|
||||
Future<void> pressButtonRealIo(WidgetTester tester, Finder button) async {
|
||||
final widget = button.evaluate().single.widget;
|
||||
final VoidCallback onPressed;
|
||||
if (widget is IconButton) {
|
||||
onPressed = widget.onPressed!;
|
||||
} else if (widget is TextButton) {
|
||||
onPressed = widget.onPressed!;
|
||||
} else {
|
||||
throw StateError('unsupported button type: $widget');
|
||||
}
|
||||
await tester.runAsync(() async {
|
||||
onPressed();
|
||||
await Future<void>.delayed(const Duration(milliseconds: 400));
|
||||
});
|
||||
await tester.pump(kQueueRefreshDebounce);
|
||||
await tester.pump(const Duration(milliseconds: 60));
|
||||
}
|
||||
|
||||
testWidgets('摘要卡:范围标注(时间·本设备)+ 四组任务数(按任务数口径)',
|
||||
(tester) async {
|
||||
await seed('done1.jpg', state: UploadTaskState.completed);
|
||||
await seed('done2.jpg', state: UploadTaskState.completed);
|
||||
await seed('up.jpg', state: UploadTaskState.uploading);
|
||||
await seed('q1.jpg');
|
||||
await seed('q2.jpg');
|
||||
await seed(
|
||||
'bad.jpg',
|
||||
state: UploadTaskState.retryableFailed,
|
||||
lastError: 'NETWORK_ERROR',
|
||||
);
|
||||
await pumpQueue(tester);
|
||||
|
||||
// 范围标注:未选相册 → 时间范围 · 本设备(跨设备记录在服务端历史页)。
|
||||
expect(find.text('Any time · This device'), findsOneWidget);
|
||||
|
||||
// 四组计数:摘要标签小写、chip 大写——find.text 区分大小写不冲突。
|
||||
void expectSummary(String label, String value) {
|
||||
final row = find.ancestor(
|
||||
of: find.text(label),
|
||||
matching: find.byType(Row),
|
||||
);
|
||||
expect(
|
||||
find.descendant(of: row, matching: find.text(value)),
|
||||
findsOneWidget,
|
||||
reason: '摘要「$label」= $value',
|
||||
);
|
||||
}
|
||||
|
||||
expectSummary('completed', '2');
|
||||
expectSummary('uploading', '1');
|
||||
expectSummary('waiting', '2');
|
||||
expectSummary('need attention', '1');
|
||||
});
|
||||
|
||||
testWidgets('各状态任务行:徽章文案 + 相册名(缓存)+ 缩略图占位',
|
||||
(tester) async {
|
||||
await seed('prep.jpg', state: UploadTaskState.preparing);
|
||||
@@ -230,42 +298,18 @@ void main() {
|
||||
await pumpQueue(tester);
|
||||
|
||||
// 状态徽章(en locale;断言锚定在行内——chip 栏文案与部分徽章同名)。
|
||||
expect(
|
||||
find
|
||||
.descendant(
|
||||
of: find.widgetWithText(ListTile, 'prep.jpg'),
|
||||
matching: find.text('Preparing'),
|
||||
)
|
||||
.evaluate(),
|
||||
isNotEmpty,
|
||||
);
|
||||
expect(
|
||||
find
|
||||
.descendant(
|
||||
of: find.widgetWithText(ListTile, 'ok.jpg'),
|
||||
matching: find.text('Completed'),
|
||||
)
|
||||
.evaluate(),
|
||||
isNotEmpty,
|
||||
);
|
||||
expect(
|
||||
find
|
||||
.descendant(
|
||||
of: find.widgetWithText(ListTile, 'net.jpg'),
|
||||
matching: find.text('Waiting for network'),
|
||||
)
|
||||
.evaluate(),
|
||||
isNotEmpty,
|
||||
);
|
||||
expect(
|
||||
find
|
||||
.descendant(
|
||||
of: find.widgetWithText(ListTile, 'alb2.jpg'),
|
||||
matching: find.text('Uploading'),
|
||||
)
|
||||
.evaluate(),
|
||||
isNotEmpty,
|
||||
);
|
||||
for (final (filename, badge) in [
|
||||
('prep.jpg', 'Preparing photo'),
|
||||
('ok.jpg', 'Completed'),
|
||||
('net.jpg', 'Waiting for network'),
|
||||
('alb2.jpg', 'Uploading'),
|
||||
]) {
|
||||
expect(
|
||||
find.descendant(of: rowOf(filename), matching: find.text(badge)),
|
||||
findsOneWidget,
|
||||
reason: '$filename 行徽章/状态文案 $badge',
|
||||
);
|
||||
}
|
||||
// 文件名。
|
||||
expect(find.text('ok.jpg'), findsOneWidget);
|
||||
// 相册名经 albums 缓存展示(en 主标题;3 条 alb-1 + 1 条 alb-2)。
|
||||
@@ -290,29 +334,102 @@ void main() {
|
||||
);
|
||||
await pumpQueue(tester);
|
||||
|
||||
// §4.4:失败行显示本地化原因(独立错误行),状态行附尝试次数。
|
||||
expect(find.text('Photo exceeds the 50MB limit'), findsOneWidget);
|
||||
expect(find.text('Network error. Check your connection and retry'),
|
||||
findsOneWidget);
|
||||
expect(
|
||||
find.text('Network error. Check your connection and retry · '
|
||||
'2 attempt(s)'),
|
||||
find.descendant(
|
||||
of: rowOf('flaky.jpg'),
|
||||
matching: find.text('Upload failed'),
|
||||
),
|
||||
findsOneWidget,
|
||||
);
|
||||
expect(
|
||||
find.descendant(
|
||||
of: rowOf('flaky.jpg'),
|
||||
matching: find.text('2 attempt(s)'),
|
||||
),
|
||||
findsOneWidget,
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('状态筛选:Failed chip → 仅失败行', (tester) async {
|
||||
testWidgets('等待网络行:徽章 + 自动恢复提示 + 保留手动重试', (tester) async {
|
||||
await seed(
|
||||
'net.jpg',
|
||||
state: UploadTaskState.waitingNetwork,
|
||||
lastError: 'NETWORK_ERROR',
|
||||
);
|
||||
await pumpQueue(tester);
|
||||
|
||||
expect(
|
||||
find.descendant(
|
||||
of: rowOf('net.jpg'),
|
||||
matching: find.text('Waiting for network'),
|
||||
),
|
||||
findsOneWidget,
|
||||
);
|
||||
// §4.4:等待网络附自动恢复说明(用户无需手动干预)。
|
||||
expect(
|
||||
find.descendant(
|
||||
of: rowOf('net.jpg'),
|
||||
matching: find.text('Resumes automatically when the network recovers'),
|
||||
),
|
||||
findsOneWidget,
|
||||
);
|
||||
// §4.4「保留已有重试」:等待网络行仍有刷新入口。
|
||||
expect(
|
||||
find.descendant(of: rowOf('net.jpg'), matching: find.byIcon(Icons.refresh)),
|
||||
findsOneWidget,
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('状态筛选:Needs attention chip → 仅失败行', (tester) async {
|
||||
await seed('bad.jpg', state: UploadTaskState.preparationFailed);
|
||||
await seed('good.jpg', state: UploadTaskState.completed);
|
||||
await pumpQueue(tester);
|
||||
|
||||
// 状态 chips 行横向滚动——Failed 在屏外,先滚入再点。
|
||||
await tester.ensureVisible(find.text('Failed'));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.text('Failed'));
|
||||
// 状态 chip 锚定 ChoiceChip(行内徽章可能与 chip 同文案)。
|
||||
await tester.tap(find.widgetWithText(ChoiceChip, 'Needs attention'));
|
||||
await settleDebounce(tester);
|
||||
|
||||
expect(find.text('bad.jpg'), findsOneWidget);
|
||||
expect(find.text('good.jpg'), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('状态筛选:In progress chip → 活任务(含等待网络)',
|
||||
(tester) async {
|
||||
await seed('live.jpg');
|
||||
await seed('net.jpg', state: UploadTaskState.waitingNetwork);
|
||||
await seed('done.jpg', state: UploadTaskState.completed);
|
||||
await seed('bad.jpg', state: UploadTaskState.preparationFailed);
|
||||
await pumpQueue(tester);
|
||||
|
||||
await tester.tap(find.widgetWithText(ChoiceChip, 'In progress'));
|
||||
await settleDebounce(tester);
|
||||
|
||||
// 排队 + 等待网络属「进行中」(自动恢复范畴);终态与需关注排除。
|
||||
expect(find.text('live.jpg'), findsOneWidget);
|
||||
expect(find.text('net.jpg'), findsOneWidget);
|
||||
expect(find.text('done.jpg'), findsNothing);
|
||||
expect(find.text('bad.jpg'), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('状态筛选:Completed chip → 仅已完成行', (tester) async {
|
||||
await seed('done.jpg', state: UploadTaskState.completed);
|
||||
await seed('gone.jpg', state: UploadTaskState.cancelled);
|
||||
await seed('q.jpg');
|
||||
await pumpQueue(tester);
|
||||
|
||||
await tester.tap(find.widgetWithText(ChoiceChip, 'Completed'));
|
||||
await settleDebounce(tester);
|
||||
|
||||
// §4.4:已完成组不含 cancelled(取消仅在「全部」可见)。
|
||||
expect(find.text('done.jpg'), findsOneWidget);
|
||||
expect(find.text('gone.jpg'), findsNothing);
|
||||
expect(find.text('q.jpg'), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('时间筛选:Today → 昨日任务排除', (tester) async {
|
||||
final now = DateTime.now();
|
||||
await seed('fresh.jpg', createdAt: now);
|
||||
@@ -330,7 +447,8 @@ void main() {
|
||||
expect(find.text('old.jpg'), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('相册筛选:下拉选中 alb-2 → 仅该相册行', (tester) async {
|
||||
testWidgets('相册筛选:下拉选中 alb-2 → 仅该相册行 + 摘要范围带相册名',
|
||||
(tester) async {
|
||||
await seed('one.jpg', albumId: 'alb-1');
|
||||
await seed('two.jpg', albumId: 'alb-2');
|
||||
await pumpQueue(tester);
|
||||
@@ -342,42 +460,43 @@ void main() {
|
||||
|
||||
expect(find.text('two.jpg'), findsOneWidget);
|
||||
expect(find.text('one.jpg'), findsNothing);
|
||||
// 摘要范围标注随筛选更新:相册名 · 时间 · 本设备。
|
||||
expect(find.text('Wedding Day · Any time · This device'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('按状态动作可见性:queued 取消 / completed 删除 / uploading 无',
|
||||
testWidgets('按状态动作可见性:queued 取消 / completed 移除记录 / uploading 无',
|
||||
(tester) async {
|
||||
await seed('q.jpg', state: UploadTaskState.queued);
|
||||
await seed('done.jpg', state: UploadTaskState.completed);
|
||||
await seed('up.jpg', state: UploadTaskState.uploading);
|
||||
await pumpQueue(tester);
|
||||
|
||||
// queued 行 → 取消图标;completed 行 → 删除图标;uploading 行无动作。
|
||||
final cancelTile = find.widgetWithText(ListTile, 'q.jpg');
|
||||
final doneTile = find.widgetWithText(ListTile, 'done.jpg');
|
||||
final upTile = find.widgetWithText(ListTile, 'up.jpg');
|
||||
// queued 行 → 取消图标;completed 行 → 移除记录(弱化文字按钮);
|
||||
// uploading 行无动作(传输中不打断)。
|
||||
expect(
|
||||
find
|
||||
.descendant(of: cancelTile, matching: find.byIcon(Icons.close))
|
||||
.evaluate()
|
||||
.length,
|
||||
1,
|
||||
find.descendant(of: rowOf('q.jpg'), matching: find.byIcon(Icons.close)),
|
||||
findsOneWidget,
|
||||
reason: 'queued 行有取消按钮',
|
||||
);
|
||||
expect(
|
||||
find
|
||||
.descendant(
|
||||
of: doneTile,
|
||||
matching: find.byIcon(Icons.delete_outline),
|
||||
)
|
||||
.evaluate()
|
||||
.length,
|
||||
1,
|
||||
reason: 'completed 行有删除按钮',
|
||||
find.descendant(
|
||||
of: rowOf('done.jpg'),
|
||||
matching: find.text('Remove record'),
|
||||
),
|
||||
findsOneWidget,
|
||||
reason: 'completed 行有移除记录按钮',
|
||||
);
|
||||
expect(
|
||||
find.descendant(of: upTile, matching: find.byType(IconButton)).evaluate(),
|
||||
find.descendant(of: rowOf('up.jpg'), matching: find.byType(IconButton))
|
||||
.evaluate(),
|
||||
isEmpty,
|
||||
reason: 'uploading 行无动作(传输中不打断)',
|
||||
reason: 'uploading 行无图标动作(传输中不打断)',
|
||||
);
|
||||
expect(
|
||||
find.descendant(of: rowOf('up.jpg'), matching: find.byType(TextButton))
|
||||
.evaluate(),
|
||||
isEmpty,
|
||||
reason: 'uploading 行无文字动作',
|
||||
);
|
||||
});
|
||||
|
||||
@@ -387,7 +506,7 @@ void main() {
|
||||
await pumpQueue(tester);
|
||||
|
||||
await tester.tap(find.descendant(
|
||||
of: find.widgetWithText(ListTile, 'q.jpg'),
|
||||
of: rowOf('q.jpg'),
|
||||
matching: find.byIcon(Icons.close),
|
||||
));
|
||||
await tester.pumpAndSettle();
|
||||
@@ -405,7 +524,7 @@ void main() {
|
||||
await pumpQueue(tester);
|
||||
|
||||
await tester.tap(find.descendant(
|
||||
of: find.widgetWithText(ListTile, 'flaky.jpg'),
|
||||
of: rowOf('flaky.jpg'),
|
||||
matching: find.byIcon(Icons.refresh),
|
||||
));
|
||||
await tester.pumpAndSettle();
|
||||
@@ -413,7 +532,7 @@ void main() {
|
||||
expect(engine.retryCalls, [('alice', task.id)]);
|
||||
});
|
||||
|
||||
testWidgets('准备失败重试:重新选图 → 换源重新入队为 queued',
|
||||
testWidgets('准备失败:重新选图(非普通重试)→ 换源重新入队为 queued',
|
||||
(tester) async {
|
||||
final task = await seed(
|
||||
'broken.jpg',
|
||||
@@ -426,14 +545,21 @@ void main() {
|
||||
pickedPaths.add(picked.path);
|
||||
await pumpQueue(tester);
|
||||
|
||||
// §4.4:准备失败行是「重新选择」(image_search 图标),不是刷新重试。
|
||||
expect(
|
||||
find.descendant(
|
||||
of: rowOf('broken.jpg'),
|
||||
matching: find.text('File unavailable'),
|
||||
),
|
||||
findsOneWidget,
|
||||
);
|
||||
final selectAgain = find.descendant(
|
||||
of: rowOf('broken.jpg'),
|
||||
matching: find.widgetWithText(TextButton, 'Select again'),
|
||||
);
|
||||
|
||||
// 换源复制是真实文件 IO——runAsync 放行真实事件循环完成。
|
||||
await tester.runAsync(() async {
|
||||
await tester.tap(find.descendant(
|
||||
of: find.widgetWithText(ListTile, 'broken.jpg'),
|
||||
matching: find.byIcon(Icons.refresh),
|
||||
));
|
||||
await Future<void>.delayed(const Duration(milliseconds: 400));
|
||||
});
|
||||
await pressButtonRealIo(tester, selectAgain);
|
||||
await settleDebounce(tester);
|
||||
|
||||
final updated = await store.get('alice', task.id);
|
||||
@@ -445,29 +571,49 @@ void main() {
|
||||
reason: '受管副本内容已替换为重选文件');
|
||||
});
|
||||
|
||||
testWidgets('删除流:completed 行删除 → 行消失 + 库内清除',
|
||||
testWidgets('移除记录:确认弹窗明确不删云端照片;确认后行消失 + 库内清除',
|
||||
(tester) async {
|
||||
final task = await seed('done.jpg', state: UploadTaskState.completed);
|
||||
await pumpQueue(tester);
|
||||
|
||||
await tester.tap(find.descendant(
|
||||
of: find.widgetWithText(ListTile, 'done.jpg'),
|
||||
matching: find.byIcon(Icons.delete_outline),
|
||||
of: rowOf('done.jpg'),
|
||||
matching: find.text('Remove record'),
|
||||
));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// §4.4:确认弹窗明确「仅清本机记录,不删云端照片」。
|
||||
expect(find.text('Remove this record?'), findsOneWidget);
|
||||
expect(
|
||||
find.textContaining('Photos already uploaded are not deleted'),
|
||||
findsOneWidget,
|
||||
);
|
||||
// 取消 → 记录保留。
|
||||
await tester.tap(find.text('Cancel'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('done.jpg'), findsOneWidget);
|
||||
|
||||
// 再删 → 确认 → 行消失。
|
||||
await tester.tap(find.descendant(
|
||||
of: rowOf('done.jpg'),
|
||||
matching: find.text('Remove record'),
|
||||
));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.text('Remove'));
|
||||
await settleDebounce(tester);
|
||||
|
||||
expect(find.text('done.jpg'), findsNothing);
|
||||
expect(await store.get('alice', task.id), isNull);
|
||||
});
|
||||
|
||||
testWidgets('批量重试条:出现条件 + 一键重试全部可自动重试任务',
|
||||
testWidgets('批量重试条:口径 = 可重试失败才重试(等待网络/准备失败不混入)',
|
||||
(tester) async {
|
||||
final auto1 = await seed(
|
||||
'f1.jpg',
|
||||
state: UploadTaskState.retryableFailed,
|
||||
lastError: 'INTERNAL_ERROR',
|
||||
);
|
||||
final auto2 = await seed(
|
||||
await seed(
|
||||
'f2.jpg',
|
||||
state: UploadTaskState.waitingNetwork,
|
||||
lastError: 'NETWORK_ERROR',
|
||||
@@ -479,23 +625,29 @@ void main() {
|
||||
);
|
||||
await pumpQueue(tester);
|
||||
|
||||
// 2 条可自动重试(retryableFailed + waitingNetwork)→ 批量条出现。
|
||||
// 批量条出现:标题计数与摘要「需关注」同口径(准备失败 + 上传失败),
|
||||
// 范围文案说明仅重试可自动重试的子集。
|
||||
expect(find.text('Retry all failed'), findsOneWidget);
|
||||
expect(find.text('Failed · 2'), findsOneWidget,
|
||||
reason: '失败计数 = 准备失败 + 上传失败(不含等待网络)');
|
||||
expect(find.text('Needs attention · 2'), findsOneWidget);
|
||||
expect(find.textContaining('Only failed uploads on this device'),
|
||||
findsOneWidget);
|
||||
|
||||
await tester.tap(find.text('Retry all failed'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(engine.retryCalls, containsAll([
|
||||
('alice', auto1.id),
|
||||
('alice', auto2.id),
|
||||
]));
|
||||
// §4.4:一键重试仅作用于 retryableFailed——等待网络自动恢复、准备
|
||||
// 失败需换源重选,均不进入批量重试。
|
||||
expect(engine.retryCalls, [('alice', auto1.id)]);
|
||||
expect(
|
||||
engine.retryCalls.map((c) => c.$2),
|
||||
isNot(contains('task-f3.jpg')),
|
||||
reason: '准备失败需换源重选,不进批量重试',
|
||||
);
|
||||
expect(
|
||||
engine.retryCalls.map((c) => c.$2),
|
||||
isNot(contains('task-f2.jpg')),
|
||||
reason: '等待网络自动恢复,不进批量重试',
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('无失败任务 → 无批量条', (tester) async {
|
||||
@@ -518,7 +670,7 @@ void main() {
|
||||
expect(find.text('second.jpg'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('行级进度:uploading 行订阅进度总线,分数更新',
|
||||
testWidgets('行级进度:uploading 行订阅进度总线,分数 + 百分比文案更新',
|
||||
(tester) async {
|
||||
final task = await seed('up.jpg', state: UploadTaskState.uploading);
|
||||
await pumpQueue(tester);
|
||||
@@ -529,7 +681,7 @@ void main() {
|
||||
|
||||
LinearProgressIndicator indicatorOf() => find
|
||||
.descendant(
|
||||
of: find.widgetWithText(ListTile, 'up.jpg'),
|
||||
of: rowOf('up.jpg'),
|
||||
matching: find.byType(LinearProgressIndicator),
|
||||
)
|
||||
.evaluate()
|
||||
@@ -542,5 +694,13 @@ void main() {
|
||||
await tester.pump();
|
||||
|
||||
expect(indicatorOf().value, 0.5, reason: '进度事件行级刷新为 50%');
|
||||
// §4.4:上传中行展示「上传中 {percent}%」文案。
|
||||
expect(
|
||||
find.descendant(
|
||||
of: rowOf('up.jpg'),
|
||||
matching: find.text('Uploading 50%'),
|
||||
),
|
||||
findsOneWidget,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user