Files
biz_xfj_cloud/test/common/print_await_tracker_test.dart
T
2026-09-22 13:29:10 +08:00

217 lines
7.5 KiB
Dart

// ignore_for_file: depend_on_referenced_packages
import 'package:fake_async/fake_async.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:pj_supermarket_cloud/common/index.dart';
PrintHistoryEntity _task(PrintStatus status) => PrintHistoryEntity(
orderId: '8001',
serviceOrderId: '8001',
servicePrintId: '9001',
invoiceUrl: 'https://example.test/invoice.png',
printType: '1',
status: status,
createTime: DateTime(2026, 1, 1),
);
ServicePrintRecord _record(String status) => ServicePrintRecord(
id: '9001',
orderId: '8001',
printType: '1',
printNo: 1,
retryVersion: 0,
attemptCount: 0,
status: status,
);
PrintAwaitTracker buildTracker(
FakeAsync async, {
required Future<List<PrintHistoryEntity>> Function(String orderId) taskReader,
Future<List<ServicePrintRecord>> Function(String orderId)? recordsReader,
bool Function()? availableReader,
Future<void> Function()? pendingPuller,
}) {
// fake_async 只虚拟化 Timer,不虚拟化 DateTime.now;用固定起点 + 已流逝
// 虚拟时长拼出与定时器同步推进的注入时钟。
final baseTime = DateTime(2026, 1, 1);
return PrintAwaitTracker(
orderIds: const ['8001'],
printType: '1',
clock: () => baseTime.add(async.elapsed),
taskReader: taskReader,
recordsReader: recordsReader ?? (_) async => const [],
availableReader: availableReader ?? (() => true),
pendingPuller: pendingPuller ?? () async {},
);
}
void main() {
test('打印机物理不可用时直接进入 offline 终态', () {
fakeAsync((async) {
final tracker = buildTracker(
async,
taskReader: (_) async => [],
availableReader: () => false,
);
tracker.start();
expect(tracker.phase, PrintAwaitPhase.offline);
expect(tracker.phase.isTerminal, isTrue);
async.elapse(const Duration(seconds: 20));
// offline 后不再轮询,阶段保持不变。
expect(tracker.phase, PrintAwaitPhase.offline);
tracker.dispose();
});
});
test('本地任务从 NONE 变 SUCCESS 时经历 printing 后进入 done', () {
fakeAsync((async) {
var status = PrintStatus.NONE;
final tracker =
buildTracker(async, taskReader: (_) async => [_task(status)]);
final phases = <PrintAwaitPhase>[];
tracker.addListener(() => phases.add(tracker.phase));
tracker.start();
async.elapse(const Duration(seconds: 1));
expect(tracker.phase, PrintAwaitPhase.printing);
status = PrintStatus.SUCCESS;
async.elapse(const Duration(seconds: 1));
expect(tracker.phase, PrintAwaitPhase.done);
expect(tracker.phase.isTerminal, isTrue);
expect(phases,
containsAll([PrintAwaitPhase.printing, PrintAwaitPhase.done]));
tracker.dispose();
});
});
test('寻单与补拉宽限内都没有任务才判定本单无需打印', () {
fakeAsync((async) {
var pullCount = 0;
final tracker = buildTracker(
async,
taskReader: (_) async => [],
pendingPuller: () async => pullCount++,
);
tracker.start();
// 启动立即补拉一次。
expect(pullCount, 1);
async.elapse(const Duration(seconds: 3));
expect(tracker.phase, PrintAwaitPhase.preparing);
// 寻单窗口到期触发第二次补拉,进入宽限期。
async.elapse(const Duration(seconds: 5));
expect(pullCount, 2);
expect(tracker.phase, PrintAwaitPhase.preparing);
// 宽限期结束仍无任务痕迹才收尾。
async.elapse(const Duration(seconds: 5));
expect(pullCount, 2);
expect(tracker.phase, PrintAwaitPhase.noPrintNeeded);
tracker.dispose();
});
});
test('MQTT 断连时补拉把任务落库后进入打印中', () {
fakeAsync((async) {
var hasTask = false;
var pullCount = 0;
Future<void> pull() async {
pullCount++;
// 寻单到期那次补拉成功把服务端待办拉回本地队列。
if (pullCount >= 2) hasTask = true;
}
final tracker = buildTracker(
async,
taskReader: (_) async =>
hasTask ? [_task(PrintStatus.NONE)] : const <PrintHistoryEntity>[],
pendingPuller: pull,
);
tracker.start();
async.elapse(const Duration(seconds: 3));
expect(tracker.phase, PrintAwaitPhase.preparing);
async.elapse(const Duration(seconds: 4));
expect(hasTask, isTrue);
expect(tracker.phase, PrintAwaitPhase.printing);
tracker.dispose();
});
});
test('服务端记录确认有票后暴露 taskLocated 供角标提前浮现', () {
fakeAsync((async) {
var located = false;
final tracker = buildTracker(
async,
taskReader: (_) async => [],
recordsReader: (_) async =>
located ? [_record('0')] : const <ServicePrintRecord>[],
);
tracker.start();
expect(tracker.taskLocated, isFalse);
located = true;
async.elapse(const Duration(seconds: 2));
expect(tracker.taskLocated, isTrue);
// 记录还是待生成状态,界面阶段保持准备中。
expect(tracker.phase, PrintAwaitPhase.preparing);
tracker.dispose();
});
});
test('服务端已发布但本地未收到任务时保持准备中,不虚报打印中', () {
fakeAsync((async) {
var dispatched = false;
final tracker = buildTracker(
async,
taskReader: (_) async => [],
recordsReader: (_) async =>
dispatched ? [_record('1')] : const <ServicePrintRecord>[],
);
tracker.start();
dispatched = true;
async.elapse(const Duration(seconds: 2));
// 服务端 status '1' 只代表已发布;本地队列没有任务就不是真的在打印。
expect(tracker.phase, PrintAwaitPhase.preparing);
// 但任务存在已被确认,收银角标可以提前浮现"正在准备发票"。
expect(tracker.taskLocated, isTrue);
// 有票但本地一直收不到(MQTT 断连且补拉无果)时,等待到总上限收尾。
async.elapse(const Duration(seconds: 14));
expect(tracker.phase, PrintAwaitPhase.timeout);
tracker.dispose();
});
});
test('任务长时间未完成时在等待上限进入 timeout', () {
fakeAsync((async) {
final tracker = buildTracker(
async, taskReader: (_) async => [_task(PrintStatus.NONE)]);
tracker.start();
async.elapse(const Duration(seconds: 14));
expect(tracker.phase, PrintAwaitPhase.printing);
async.elapse(const Duration(seconds: 2));
expect(tracker.phase, PrintAwaitPhase.timeout);
tracker.dispose();
});
});
test('本地任务进入 ERROR 判定打印失败', () {
fakeAsync((async) {
final tracker = buildTracker(
async, taskReader: (_) async => [_task(PrintStatus.ERROR)]);
tracker.start();
async.elapse(const Duration(seconds: 1));
expect(tracker.phase, PrintAwaitPhase.failed);
tracker.dispose();
});
});
test('非期望凭证类型的任务不参与等待判定', () {
fakeAsync((async) {
final other = _task(PrintStatus.NONE)..printType = '2';
final tracker =
buildTracker(async, taskReader: (_) async => [other]);
tracker.start();
// 寻单窗口加补拉宽限全部走完,printType '2' 的兑换券任务对发票等待不可见。
async.elapse(const Duration(seconds: 11));
expect(tracker.phase, PrintAwaitPhase.noPrintNeeded);
tracker.dispose();
});
});
}