generated from ttipo/project-base
feat(flutter): 설정 › 앱 정보에 업데이트 내역(목록 → 글)을 더한다
앱 버전 다음 줄에서 목록(제목 · 날짜, [더 보기])으로 들어가 줄을 누르면 글(날짜 · 제목 · 묶음, 다음 글 · 이전 글 줄)이 열린다. 없는 글 · 받지 못함 · 글 없음 상태를 두고, 날짜는 달력 날짜 글이라 시각으로 바꾸지 않는다(CalendarDate). 다른 글 줄은 지금 글을 바꿔 끼워 뒤로는 늘 목록이다. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,477 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:arasystem_app/core/api/error_lexicon.dart';
|
||||
import 'package:arasystem_app/features/settings/data/repositories/release_notes_repository.dart';
|
||||
import 'package:arasystem_app/features/settings/presentation/pages/release_note_page.dart';
|
||||
import 'package:arasystem_app/features/settings/presentation/pages/release_notes_page.dart';
|
||||
import 'package:arasystem_app/features/settings/presentation/pages/settings_page.dart';
|
||||
import 'package:arasystem_app/features/settings/presentation/providers/release_notes_list_controller.dart';
|
||||
import 'package:arasystem_app/features/settings/presentation/release_notes_texts.dart';
|
||||
import 'package:arasystem_app/features/settings/settings_routes.dart';
|
||||
import 'package:arasystem_app/shared/widgets/load_state_texts.dart';
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
import '../../support/fakes.dart';
|
||||
import '../../support/login_fakes.dart';
|
||||
import '../../support/mail_harness.dart';
|
||||
import '../../support/push_screen_harness.dart';
|
||||
import '../../support/release_notes_fakes.dart';
|
||||
|
||||
/// 업데이트 내역 — 보드 `ReleaseNotes` B(목록 · 글 · 상태). 로그인한 앱 전체를 가짜 서버로 띄워 본다
|
||||
void main() {
|
||||
late MailAppHarness h;
|
||||
late FakeReleaseNotesBackend notes;
|
||||
|
||||
setUp(() {
|
||||
h = MailAppHarness();
|
||||
notes = FakeReleaseNotesBackend(h.server)..install();
|
||||
});
|
||||
|
||||
/// 앱을 띄워 [location] 까지 그리고 응답이 모두 올 때까지
|
||||
Future<void> open(WidgetTester tester, String location) async {
|
||||
await h.pump(tester, location: location);
|
||||
await settle(tester);
|
||||
}
|
||||
|
||||
Future<void> openList(WidgetTester tester) =>
|
||||
open(tester, SettingsRoutes.releaseNotes);
|
||||
|
||||
ReleaseNotesListState listState() =>
|
||||
h.container.read(releaseNotesListProvider);
|
||||
|
||||
/// 글 화면의 주소 — `push` 는 라우터의 주소를 바꾸지 않는다
|
||||
String noteLocation(WidgetTester tester) => GoRouterState.of(
|
||||
tester.element(find.byType(ReleaseNotePage)),
|
||||
).uri.toString();
|
||||
|
||||
Finder moreButton() =>
|
||||
find.widgetWithText(OutlinedButton, ReleaseNotesTexts.more);
|
||||
|
||||
Finder neighbor(String label, String date) =>
|
||||
find.text(ReleaseNotesTexts.neighborLine(label, date));
|
||||
|
||||
Future<void> scrollTo(WidgetTester tester, Finder finder) async {
|
||||
await tester.scrollUntilVisible(
|
||||
finder,
|
||||
500,
|
||||
scrollable: find.byType(Scrollable).first,
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
}
|
||||
|
||||
Future<void> tapVisible(WidgetTester tester, Finder finder) async {
|
||||
await tester.ensureVisible(finder);
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(finder);
|
||||
await settle(tester);
|
||||
}
|
||||
|
||||
Future<void> back(WidgetTester tester) async {
|
||||
await tester.tap(find.byTooltip(ReleaseNotesTexts.back));
|
||||
await settle(tester);
|
||||
}
|
||||
|
||||
group('목록 — 보드 B2', () {
|
||||
testWidgets('최근 글부터 줄마다 제목 · 날짜 · ›, 한 쪽(20개)에 다 들면 [더 보기]가 없다', (
|
||||
WidgetTester tester,
|
||||
) async {
|
||||
await openList(tester);
|
||||
|
||||
expect(find.byType(ReleaseNotesPage), findsOneWidget);
|
||||
expect(find.byType(NavigationBar), findsNothing, reason: '탭 막대 없는 전체 화면');
|
||||
expect(find.text(ReleaseNotesTexts.title), findsOneWidget);
|
||||
final List<double> tops = <double>[
|
||||
for (final FakeReleaseNote note in kBoardReleaseNotes)
|
||||
tester.getTopLeft(findCopy(note.title)).dy,
|
||||
];
|
||||
for (int index = 1; index < tops.length; index++) {
|
||||
expect(tops[index - 1], lessThan(tops[index]), reason: '최근 글부터');
|
||||
}
|
||||
for (final String date in <String>[
|
||||
'2026년 11월 18일',
|
||||
'2026년 11월 5일',
|
||||
'2026년 11월 4일',
|
||||
'2026년 11월 3일',
|
||||
]) {
|
||||
expect(find.text(date), findsOneWidget, reason: date);
|
||||
}
|
||||
expect(find.byIcon(Icons.chevron_right), findsNWidgets(4));
|
||||
expect(moreButton(), findsNothing);
|
||||
final RequestOptions request = notes.listRequests.single;
|
||||
expect(request.queryParameters['limit'], kReleaseNotesPageSize);
|
||||
expect(request.queryParameters.containsKey('cursor'), isFalse);
|
||||
expect(h.notifier.shownCodes, isEmpty);
|
||||
});
|
||||
|
||||
testWidgets('줄은 최소 68 이고 화면 낭독기는 제목 · 날짜를 단추 하나로 읽는다', (
|
||||
WidgetTester tester,
|
||||
) async {
|
||||
await openList(tester);
|
||||
|
||||
final Finder row = find.ancestor(
|
||||
of: findCopy(kBoardReleaseNotes.first.title),
|
||||
matching: find.byType(InkWell),
|
||||
);
|
||||
expect(tester.getSize(row).height, greaterThanOrEqualTo(68));
|
||||
expect(
|
||||
tester.getSemantics(row),
|
||||
isSemantics(
|
||||
label: '${kBoardReleaseNotes.first.title}\n2026년 11월 18일',
|
||||
isButton: true,
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('[더 보기] — 다음 쪽을 뒤에 붙인다. 받는 동안은 그 단추만 바쁨 표시이고 다시 누르지 못한다', (
|
||||
WidgetTester tester,
|
||||
) async {
|
||||
notes.seed(25);
|
||||
await openList(tester);
|
||||
expect(listState().items, hasLength(20));
|
||||
await scrollTo(tester, moreButton());
|
||||
|
||||
final Completer<void> gate = Completer<void>();
|
||||
notes.listGate = gate;
|
||||
await tester.tap(moreButton());
|
||||
// 요청이 가짜 서버에 닿을 때까지(시계가 가야 온다) — 도는 표시가 있어 pumpAndSettle 은 쓰지 않는다
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(milliseconds: 100));
|
||||
|
||||
expect(notes.listRequests, hasLength(2));
|
||||
expect(
|
||||
notes.listRequests.last.queryParameters['cursor'],
|
||||
'${FakeReleaseNotesBackend.cursorPrefix}20',
|
||||
);
|
||||
expect(
|
||||
find.descendant(
|
||||
of: moreButton(),
|
||||
matching: find.byType(CircularProgressIndicator),
|
||||
),
|
||||
findsOneWidget,
|
||||
);
|
||||
expect(
|
||||
tester.getSemantics(find.bySemanticsLabel(ReleaseNotesTexts.more)),
|
||||
isSemantics(
|
||||
label: ReleaseNotesTexts.more,
|
||||
value: ReleaseNotesTexts.moreBusy,
|
||||
isButton: true,
|
||||
hasEnabledState: true,
|
||||
isEnabled: false,
|
||||
),
|
||||
);
|
||||
|
||||
// 받는 동안 다시 눌러도 겹쳐 보내지 않는다
|
||||
await tester.tap(moreButton());
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(milliseconds: 100));
|
||||
expect(notes.listRequests, hasLength(2));
|
||||
expect(listState().loadingMore, isTrue);
|
||||
|
||||
notes.listGate = null;
|
||||
gate.complete();
|
||||
await settle(tester);
|
||||
|
||||
expect(listState().items, hasLength(25));
|
||||
await scrollTo(tester, findCopy('업데이트 1'));
|
||||
expect(moreButton(), findsNothing, reason: '마지막 쪽');
|
||||
expect(h.notifier.shownCodes, isEmpty);
|
||||
});
|
||||
|
||||
testWidgets('[더 보기]를 받지 못하면 알리고 받은 줄은 그대로 — 다시 누르면 받는다', (
|
||||
WidgetTester tester,
|
||||
) async {
|
||||
notes.seed(25);
|
||||
await openList(tester);
|
||||
await scrollTo(tester, moreButton());
|
||||
notes.nextListResponse = FakeResponse.fail(500, 'SYS_001');
|
||||
|
||||
await tester.tap(moreButton());
|
||||
await settle(tester);
|
||||
|
||||
expect(h.notifier.shownCodes, <String>['SYS_001']);
|
||||
expect(listState().items, hasLength(20));
|
||||
expect(listState().loadingMore, isFalse);
|
||||
expect(
|
||||
find.descendant(
|
||||
of: moreButton(),
|
||||
matching: find.byType(CircularProgressIndicator),
|
||||
),
|
||||
findsNothing,
|
||||
);
|
||||
|
||||
await tester.tap(moreButton());
|
||||
await settle(tester);
|
||||
|
||||
expect(listState().items, hasLength(25));
|
||||
});
|
||||
|
||||
testWidgets('[더 보기] 응답의 모양이 틀리면 화면이 알린다(SYS_001) — 받은 줄은 그대로', (
|
||||
WidgetTester tester,
|
||||
) async {
|
||||
notes.seed(25);
|
||||
await openList(tester);
|
||||
await scrollTo(tester, moreButton());
|
||||
notes.nextListResponse = FakeResponse.ok(<String, Object?>{
|
||||
'items': '모양이 틀림',
|
||||
});
|
||||
|
||||
await tester.tap(moreButton());
|
||||
await settle(tester);
|
||||
|
||||
expect(h.notifier.shownCodes, <String>['SYS_001']);
|
||||
expect(listState().items, hasLength(20));
|
||||
expect(moreButton(), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('뒤로 — 설정 탭으로 돌아온다', (WidgetTester tester) async {
|
||||
await openList(tester);
|
||||
|
||||
await back(tester);
|
||||
|
||||
expect(find.byType(ReleaseNotesPage), findsNothing);
|
||||
expect(find.byType(SettingsPage), findsOneWidget);
|
||||
expect(find.byType(NavigationBar), findsOneWidget);
|
||||
expect(h.location(), SettingsRoutes.home);
|
||||
});
|
||||
});
|
||||
|
||||
group('목록의 상태 — 보드 B7 · B8 · B9', () {
|
||||
testWidgets('받는 동안 줄 자리를 막대로 잡아 둔다 — 화면 낭독기는 "업데이트 내역 불러오는 중"', (
|
||||
WidgetTester tester,
|
||||
) async {
|
||||
final Completer<void> gate = Completer<void>();
|
||||
notes.listGate = gate;
|
||||
await h.pump(tester, location: SettingsRoutes.releaseNotes);
|
||||
|
||||
expect(
|
||||
find.bySemanticsLabel(ReleaseNotesTexts.listLoading),
|
||||
findsOneWidget,
|
||||
);
|
||||
expect(findCopy(kBoardReleaseNotes.first.title), findsNothing);
|
||||
|
||||
notes.listGate = null;
|
||||
gate.complete();
|
||||
await settle(tester);
|
||||
|
||||
expect(
|
||||
find.bySemanticsLabel(ReleaseNotesTexts.listLoading),
|
||||
findsNothing,
|
||||
);
|
||||
expect(findCopy(kBoardReleaseNotes.first.title), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets(
|
||||
'받지 못하면 그 자리에 사전 문구 · 문의 코드 · [다시 시도] — 알리지 않고, [다시 시도]면 다시 받는다',
|
||||
(WidgetTester tester) async {
|
||||
notes.nextListResponse = FakeResponse.fail(500, 'SYS_001');
|
||||
await openList(tester);
|
||||
|
||||
expect(findCopy(ErrorLexicon.getMessage('SYS_001')), findsOneWidget);
|
||||
expect(find.text(LoadStateTexts.traceCode('trace-SY')), findsOneWidget);
|
||||
expect(find.text(LoadStateTexts.retry), findsOneWidget);
|
||||
expect(h.notifier.shownCodes, isEmpty);
|
||||
|
||||
await tester.tap(find.text(LoadStateTexts.retry));
|
||||
await settle(tester);
|
||||
|
||||
expect(notes.listRequests, hasLength(2));
|
||||
expect(findCopy(kBoardReleaseNotes.first.title), findsOneWidget);
|
||||
expect(h.notifier.shownCodes, isEmpty);
|
||||
},
|
||||
);
|
||||
|
||||
testWidgets('목록은 빠져나갈 곳이 없어 없는 경로(404)여도 [다시 시도]다', (
|
||||
WidgetTester tester,
|
||||
) async {
|
||||
notes.nextListResponse = FakeResponse.fail(404, 'RES_001');
|
||||
await openList(tester);
|
||||
|
||||
expect(findCopy(ErrorLexicon.getMessage('RES_001')), findsOneWidget);
|
||||
expect(find.text(LoadStateTexts.retry), findsOneWidget);
|
||||
expect(find.text(ReleaseNotesTexts.toList), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('글이 없으면 한 줄 — "아직 알려 드릴 업데이트가 없습니다."', (
|
||||
WidgetTester tester,
|
||||
) async {
|
||||
notes.notes = const <FakeReleaseNote>[];
|
||||
await openList(tester);
|
||||
|
||||
expect(findCopy(ReleaseNotesTexts.empty), findsOneWidget);
|
||||
expect(find.byIcon(Icons.description_outlined), findsOneWidget);
|
||||
expect(moreButton(), findsNothing);
|
||||
expect(find.text(LoadStateTexts.retry), findsNothing);
|
||||
});
|
||||
});
|
||||
|
||||
group('글 — 보드 B3 · B4', () {
|
||||
testWidgets('줄을 누르면 글 — 날짜 · 제목 · 묶음(머리 · 점 목록), 맨 위 글은 이전 글 줄만', (
|
||||
WidgetTester tester,
|
||||
) async {
|
||||
await openList(tester);
|
||||
|
||||
await tapVisible(tester, findCopy(kBoardReleaseNotes.first.title));
|
||||
|
||||
expect(find.byType(ReleaseNotePage), findsOneWidget);
|
||||
expect(noteLocation(tester), SettingsRoutes.releaseNote('2026-11-18'));
|
||||
expect(
|
||||
find.byType(ReleaseNotesPage, skipOffstage: false),
|
||||
findsOneWidget,
|
||||
reason: '글은 목록 위에 하나만 쌓인다',
|
||||
);
|
||||
expect(find.byType(NavigationBar), findsNothing);
|
||||
expect(notes.noteRequests, <String>['2026-11-18']);
|
||||
expect(find.text('2026년 11월 18일'), findsOneWidget);
|
||||
expect(findCopy(kBoardReleaseNotes.first.title), findsOneWidget);
|
||||
expect(findCopy('휴대폰 앱'), findsOneWidget);
|
||||
expect(findCopy('메일 검색'), findsOneWidget);
|
||||
expect(
|
||||
findCopy('안드로이드 휴대폰 앱이 나왔습니다. 웹메일의 [설정] → [휴대폰 앱]에서 받을 수 있습니다.'),
|
||||
findsOneWidget,
|
||||
);
|
||||
expect(findCopy('찾은 말에는 노란 표시가 붙습니다.'), findsOneWidget);
|
||||
expect(
|
||||
tester.getTopLeft(findCopy('휴대폰 앱')).dy,
|
||||
lessThan(tester.getTopLeft(findCopy('메일 검색')).dy),
|
||||
);
|
||||
await tester.ensureVisible(
|
||||
neighbor(ReleaseNotesTexts.older, '2026년 11월 5일'),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
expect(neighbor(ReleaseNotesTexts.older, '2026년 11월 5일'), findsOneWidget);
|
||||
expect(find.textContaining(ReleaseNotesTexts.newer), findsNothing);
|
||||
expect(h.notifier.shownCodes, isEmpty);
|
||||
});
|
||||
|
||||
testWidgets('가운데 글 — 다음 글(더 새 글)이 위 · 이전 글(더 옛 글)이 아래, 머리 없는 묶음은 줄만', (
|
||||
WidgetTester tester,
|
||||
) async {
|
||||
await open(tester, SettingsRoutes.releaseNote('2026-11-05'));
|
||||
|
||||
expect(findCopy('화면의 안내 글을 다듬었습니다'), findsOneWidget);
|
||||
expect(findCopy('단추와 안내 글을 더 알기 쉬운 말로 고쳤습니다.'), findsOneWidget);
|
||||
final Finder newer = neighbor(ReleaseNotesTexts.newer, '2026년 11월 18일');
|
||||
final Finder older = neighbor(ReleaseNotesTexts.older, '2026년 11월 4일');
|
||||
expect(newer, findsOneWidget);
|
||||
expect(older, findsOneWidget);
|
||||
expect(
|
||||
tester.getTopLeft(newer).dy,
|
||||
lessThan(tester.getTopLeft(older).dy),
|
||||
);
|
||||
expect(findCopy(kBoardReleaseNotes.first.title), findsOneWidget);
|
||||
expect(findCopy('왼쪽 메뉴를 정리했습니다'), findsOneWidget);
|
||||
final Finder row = find.ancestor(
|
||||
of: newer,
|
||||
matching: find.byType(InkWell),
|
||||
);
|
||||
expect(tester.getSize(row).height, greaterThanOrEqualTo(64));
|
||||
expect(
|
||||
tester.getSemantics(row),
|
||||
isSemantics(
|
||||
label:
|
||||
'${ReleaseNotesTexts.newer}, 2026년 11월 18일\n'
|
||||
'${kBoardReleaseNotes.first.title}',
|
||||
isButton: true,
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('다른 글 줄을 누르면 그 글로 바꿔 끼운다 — 뒤로는 늘 목록', (
|
||||
WidgetTester tester,
|
||||
) async {
|
||||
await openList(tester);
|
||||
await tapVisible(tester, findCopy(kBoardReleaseNotes.first.title));
|
||||
|
||||
await tapVisible(
|
||||
tester,
|
||||
neighbor(ReleaseNotesTexts.older, '2026년 11월 5일'),
|
||||
);
|
||||
|
||||
expect(find.byType(ReleaseNotePage, skipOffstage: false), findsOneWidget);
|
||||
expect(noteLocation(tester), SettingsRoutes.releaseNote('2026-11-05'));
|
||||
expect(findCopy('단추와 안내 글을 더 알기 쉬운 말로 고쳤습니다.'), findsOneWidget);
|
||||
expect(notes.noteRequests, <String>['2026-11-18', '2026-11-05']);
|
||||
|
||||
await back(tester);
|
||||
|
||||
expect(find.byType(ReleaseNotePage), findsNothing);
|
||||
expect(find.byType(ReleaseNotesPage), findsOneWidget);
|
||||
expect(findCopy(kBoardReleaseNotes.last.title), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('주소로 바로 연 글도 뒤로는 목록이다', (WidgetTester tester) async {
|
||||
await open(tester, SettingsRoutes.releaseNote('2026-11-04'));
|
||||
expect(findCopy('왼쪽 메뉴를 정리했습니다'), findsOneWidget);
|
||||
|
||||
await back(tester);
|
||||
|
||||
expect(find.byType(ReleaseNotesPage), findsOneWidget);
|
||||
expect(findCopy(kBoardReleaseNotes.first.title), findsOneWidget);
|
||||
});
|
||||
});
|
||||
|
||||
group('글의 상태 — 보드 B5 · B6', () {
|
||||
testWidgets('없는 글(404 RES_001)은 알리지 않고 사전 문구 · 문의 코드 · [목록으로] — 누르면 목록', (
|
||||
WidgetTester tester,
|
||||
) async {
|
||||
await open(tester, SettingsRoutes.releaseNote('2026-01-01'));
|
||||
|
||||
expect(findCopy(ErrorLexicon.getMessage('RES_001')), findsOneWidget);
|
||||
expect(find.text(LoadStateTexts.traceCode('trace-RE')), findsOneWidget);
|
||||
expect(find.text(ReleaseNotesTexts.toList), findsOneWidget);
|
||||
expect(find.text(LoadStateTexts.retry), findsNothing);
|
||||
expect(h.notifier.shownCodes, isEmpty);
|
||||
|
||||
await tester.tap(find.text(ReleaseNotesTexts.toList));
|
||||
await settle(tester);
|
||||
|
||||
expect(find.byType(ReleaseNotePage), findsNothing);
|
||||
expect(find.byType(ReleaseNotesPage), findsOneWidget);
|
||||
expect(findCopy(kBoardReleaseNotes.first.title), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('그 밖의 실패는 사전 문구 · 문의 코드 · [다시 시도] — 다시 받으면 글', (
|
||||
WidgetTester tester,
|
||||
) async {
|
||||
notes.nextNoteResponse = FakeResponse.fail(500, 'SYS_001');
|
||||
await open(tester, SettingsRoutes.releaseNote('2026-11-05'));
|
||||
|
||||
expect(findCopy(ErrorLexicon.getMessage('SYS_001')), findsOneWidget);
|
||||
expect(find.text(LoadStateTexts.traceCode('trace-SY')), findsOneWidget);
|
||||
expect(find.text(ReleaseNotesTexts.toList), findsNothing);
|
||||
expect(h.notifier.shownCodes, isEmpty);
|
||||
|
||||
await tester.tap(find.text(LoadStateTexts.retry));
|
||||
await settle(tester);
|
||||
|
||||
expect(notes.noteRequests, <String>['2026-11-05', '2026-11-05']);
|
||||
expect(findCopy('화면의 안내 글을 다듬었습니다'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('받는 동안 글 자리를 막대로 잡아 둔다 — 화면 낭독기는 "글 불러오는 중"', (
|
||||
WidgetTester tester,
|
||||
) async {
|
||||
final Completer<void> gate = Completer<void>();
|
||||
notes.noteGate = gate;
|
||||
await h.pump(tester, location: SettingsRoutes.releaseNote('2026-11-05'));
|
||||
|
||||
expect(
|
||||
find.bySemanticsLabel(ReleaseNotesTexts.noteLoading),
|
||||
findsOneWidget,
|
||||
);
|
||||
|
||||
notes.noteGate = null;
|
||||
gate.complete();
|
||||
await settle(tester);
|
||||
|
||||
expect(
|
||||
find.bySemanticsLabel(ReleaseNotesTexts.noteLoading),
|
||||
findsNothing,
|
||||
);
|
||||
expect(findCopy('화면의 안내 글을 다듬었습니다'), findsOneWidget);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,483 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:arasystem_app/core/api/dio_provider.dart';
|
||||
import 'package:arasystem_app/core/utils/calendar_date.dart';
|
||||
import 'package:arasystem_app/core/utils/provider_retry.dart';
|
||||
import 'package:arasystem_app/features/settings/data/models/release_note.dart';
|
||||
import 'package:arasystem_app/features/settings/data/repositories/release_notes_repository.dart';
|
||||
import 'package:arasystem_app/features/settings/presentation/providers/release_note_controller.dart';
|
||||
import 'package:arasystem_app/features/settings/presentation/providers/release_notes_list_controller.dart';
|
||||
import 'package:arasystem_app/features/settings/presentation/providers/release_notes_providers.dart';
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:flutter_riverpod/misc.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
import '../../support/fakes.dart';
|
||||
import '../../support/provider_harness.dart';
|
||||
import '../../support/release_notes_fakes.dart';
|
||||
|
||||
/// 업데이트 내역을 보는 직원 — 시험이 바꾼다(세션이 바뀜)
|
||||
class _Account extends Notifier<String?> {
|
||||
@override
|
||||
String? build() => 'ara.kim@arasystem.it.kr';
|
||||
|
||||
void set(String? value) => state = value;
|
||||
}
|
||||
|
||||
final NotifierProvider<_Account, String?> _accountProvider =
|
||||
NotifierProvider<_Account, String?>(_Account.new);
|
||||
|
||||
/// 끝난 요청을 세는 저장소 — 늦은 응답이 컨트롤러에 닿은 뒤를 기다리려고(고정 대기 없이)
|
||||
class _RecordingRepository extends ReleaseNotesRepository {
|
||||
_RecordingRepository(super.dio);
|
||||
|
||||
int listsFinished = 0;
|
||||
int notesFinished = 0;
|
||||
|
||||
@override
|
||||
Future<ReleaseNotesPage> listNotes({
|
||||
String? cursor,
|
||||
int limit = kReleaseNotesPageSize,
|
||||
bool silent = false,
|
||||
}) async {
|
||||
try {
|
||||
return await super.listNotes(
|
||||
cursor: cursor,
|
||||
limit: limit,
|
||||
silent: silent,
|
||||
);
|
||||
} finally {
|
||||
listsFinished++;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<ReleaseNote> getNote(String id) async {
|
||||
try {
|
||||
return await super.getNote(id);
|
||||
} finally {
|
||||
notesFinished++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 업데이트 내역 — 모델 읽기 · 저장소 · 목록 · 글 컨트롤러 (계약 7절)
|
||||
void main() {
|
||||
group('모델 읽기', () {
|
||||
Map<String, Object?> summary(String id, {String? date, String? title}) =>
|
||||
<String, Object?>{
|
||||
'id': id,
|
||||
'date': date ?? id.substring(0, 10),
|
||||
'title': title ?? '제목 $id',
|
||||
};
|
||||
|
||||
test('목록 한 쪽 — 줄 · 다음 쪽 · 글 수', () {
|
||||
final ReleaseNotesPage page = ReleaseNotesPage.tryParse(<String, Object?>{
|
||||
'items': <Object?>[summary('2026-11-18-2'), summary('2026-11-18')],
|
||||
'pageInfo': <String, Object?>{
|
||||
'hasNext': true,
|
||||
'nextCursor': 'abc',
|
||||
'total': 41,
|
||||
},
|
||||
})!;
|
||||
|
||||
expect(page.items.map((ReleaseNoteSummary n) => n.id), <String>[
|
||||
'2026-11-18-2',
|
||||
'2026-11-18',
|
||||
]);
|
||||
expect(page.items.first.date, const CalendarDate(2026, 11, 18));
|
||||
expect(page.items.first.title, '제목 2026-11-18-2');
|
||||
expect(page.hasMore, isTrue);
|
||||
expect(page.total, 41);
|
||||
});
|
||||
|
||||
test('마지막 쪽 — 커서가 없으면 더 없다(hasNext 여도) · 글 수는 없어도 된다', () {
|
||||
final ReleaseNotesPage last = ReleaseNotesPage.tryParse(<String, Object?>{
|
||||
'items': <Object?>[],
|
||||
'pageInfo': <String, Object?>{'hasNext': false, 'nextCursor': null},
|
||||
})!;
|
||||
expect(last.items, isEmpty);
|
||||
expect(last.hasMore, isFalse);
|
||||
expect(last.total, isNull);
|
||||
|
||||
final ReleaseNotesPage noCursor = ReleaseNotesPage.tryParse(
|
||||
<String, Object?>{
|
||||
'items': <Object?>[],
|
||||
'pageInfo': <String, Object?>{'hasNext': true, 'nextCursor': ''},
|
||||
},
|
||||
)!;
|
||||
expect(noCursor.hasMore, isFalse);
|
||||
});
|
||||
|
||||
test('모양이 틀리면 null — 줄 하나라도(없는 날 · 빈 id · 숫자 제목)', () {
|
||||
Map<String, Object?> pageOf(List<Object?> items) => <String, Object?>{
|
||||
'items': items,
|
||||
'pageInfo': <String, Object?>{'hasNext': false, 'nextCursor': null},
|
||||
};
|
||||
expect(
|
||||
ReleaseNotesPage.tryParse(
|
||||
pageOf(<Object?>[summary('2026-11-18'), summary('2026-02-30')]),
|
||||
),
|
||||
isNull,
|
||||
);
|
||||
expect(
|
||||
ReleaseNotesPage.tryParse(
|
||||
pageOf(<Object?>[summary('', date: '2026-11-18')]),
|
||||
),
|
||||
isNull,
|
||||
);
|
||||
expect(
|
||||
ReleaseNotesPage.tryParse(
|
||||
pageOf(<Object?>[
|
||||
<String, Object?>{
|
||||
'id': '2026-11-18',
|
||||
'date': '2026-11-18',
|
||||
'title': 3,
|
||||
},
|
||||
]),
|
||||
),
|
||||
isNull,
|
||||
);
|
||||
expect(
|
||||
ReleaseNotesPage.tryParse(<String, Object?>{'items': <Object?>[]}),
|
||||
isNull,
|
||||
);
|
||||
expect(ReleaseNotesPage.tryParse('목록'), isNull);
|
||||
});
|
||||
|
||||
test('글 — 묶음(머리 없는 묶음 포함) · 바로 더 옛 글 · 바로 더 새 글', () {
|
||||
final ReleaseNote note = ReleaseNote.tryParse(<String, Object?>{
|
||||
...summary('2026-11-05', title: '화면의 안내 글을 다듬었습니다'),
|
||||
'sections': <Object?>[
|
||||
<String, Object?>{
|
||||
'heading': '메일 · 메일함',
|
||||
'items': <Object?>['한 줄', '두 줄'],
|
||||
},
|
||||
<String, Object?>{
|
||||
'heading': null,
|
||||
'items': <Object?>['머리 없는 줄'],
|
||||
},
|
||||
],
|
||||
'previous': summary('2026-11-04'),
|
||||
'next': summary('2026-11-18'),
|
||||
})!;
|
||||
|
||||
expect(note.id, '2026-11-05');
|
||||
expect(note.date, const CalendarDate(2026, 11, 5));
|
||||
expect(note.title, '화면의 안내 글을 다듬었습니다');
|
||||
expect(note.sections, const <ReleaseNoteSection>[
|
||||
ReleaseNoteSection(heading: '메일 · 메일함', items: <String>['한 줄', '두 줄']),
|
||||
ReleaseNoteSection(heading: null, items: <String>['머리 없는 줄']),
|
||||
]);
|
||||
expect(note.previous?.id, '2026-11-04');
|
||||
expect(note.next?.id, '2026-11-18');
|
||||
});
|
||||
|
||||
test('다른 글이 없으면(null) · 칸이 아예 없으면 없는 것', () {
|
||||
final ReleaseNote first = ReleaseNote.tryParse(<String, Object?>{
|
||||
...summary('2026-11-18'),
|
||||
'sections': <Object?>[],
|
||||
'previous': null,
|
||||
'next': null,
|
||||
})!;
|
||||
expect(first.previous, isNull);
|
||||
expect(first.next, isNull);
|
||||
|
||||
final ReleaseNote old = ReleaseNote.tryParse(<String, Object?>{
|
||||
...summary('2026-11-18'),
|
||||
'sections': <Object?>[],
|
||||
})!;
|
||||
expect(old.previous, isNull);
|
||||
expect(old.next, isNull);
|
||||
});
|
||||
|
||||
test('글의 모양이 틀리면 null — 묶음 · 다른 글 하나라도', () {
|
||||
Map<String, Object?> noteWith(Map<String, Object?> extra) =>
|
||||
<String, Object?>{
|
||||
...summary('2026-11-18'),
|
||||
'sections': <Object?>[],
|
||||
...extra,
|
||||
};
|
||||
expect(
|
||||
ReleaseNote.tryParse(
|
||||
noteWith(<String, Object?>{
|
||||
'sections': <Object?>[
|
||||
<String, Object?>{
|
||||
'heading': 1,
|
||||
'items': <Object?>['줄'],
|
||||
},
|
||||
],
|
||||
}),
|
||||
),
|
||||
isNull,
|
||||
);
|
||||
expect(
|
||||
ReleaseNote.tryParse(
|
||||
noteWith(<String, Object?>{
|
||||
'sections': <Object?>[
|
||||
<String, Object?>{
|
||||
'heading': null,
|
||||
'items': <Object?>['줄', null],
|
||||
},
|
||||
],
|
||||
}),
|
||||
),
|
||||
isNull,
|
||||
);
|
||||
expect(
|
||||
ReleaseNote.tryParse(
|
||||
noteWith(<String, Object?>{
|
||||
'previous': <String, Object?>{'id': '2026-11-04'},
|
||||
}),
|
||||
),
|
||||
isNull,
|
||||
);
|
||||
expect(
|
||||
ReleaseNote.tryParse(noteWith(<String, Object?>{'next': '2026-11-19'})),
|
||||
isNull,
|
||||
);
|
||||
expect(
|
||||
ReleaseNote.tryParse(summary('2026-11-18')),
|
||||
isNull,
|
||||
reason: '묶음 없음',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
group('저장소 · 컨트롤러', () {
|
||||
late ProviderHarness h;
|
||||
late FakeReleaseNotesBackend notes;
|
||||
late ProviderContainer container;
|
||||
late _RecordingRepository recording;
|
||||
|
||||
setUp(() async {
|
||||
h = ProviderHarness();
|
||||
notes = FakeReleaseNotesBackend(h.server)..install();
|
||||
await h.prepare();
|
||||
await h.store.save(tokensFor(1, now: h.clock.now));
|
||||
container = ProviderContainer(
|
||||
overrides: <Override>[
|
||||
...h.overrides,
|
||||
releaseNotesAccountProvider.overrideWith(
|
||||
(Ref ref) => ref.watch(_accountProvider),
|
||||
),
|
||||
releaseNotesRepositoryProvider.overrideWith(
|
||||
(Ref ref) =>
|
||||
recording = _RecordingRepository(ref.watch(dioProvider)),
|
||||
),
|
||||
],
|
||||
retry: noAutomaticRetry,
|
||||
);
|
||||
addTearDown(container.dispose);
|
||||
});
|
||||
|
||||
ReleaseNotesRepository repository() =>
|
||||
container.read(releaseNotesRepositoryProvider);
|
||||
|
||||
/// 목록을 열고(화면이 보는 것처럼 구독한다) 첫 쪽이 끝날 때까지 기다린다
|
||||
Future<void> openList() async {
|
||||
final ProviderSubscription<ReleaseNotesListState> subscription = container
|
||||
.listen(
|
||||
releaseNotesListProvider,
|
||||
(ReleaseNotesListState? previous, ReleaseNotesListState next) {},
|
||||
);
|
||||
addTearDown(subscription.close);
|
||||
await pumpEventsUntil(
|
||||
() =>
|
||||
container.read(releaseNotesListProvider).status !=
|
||||
ReleaseNotesListStatus.loading,
|
||||
reason: '첫 쪽',
|
||||
);
|
||||
}
|
||||
|
||||
ReleaseNotesListState listState() =>
|
||||
container.read(releaseNotesListProvider);
|
||||
|
||||
/// [condition] 이 된 뒤 한 차례 더 — 응답을 받은 컨트롤러가 상태를 바꿀 자리까지
|
||||
Future<void> settleAfter(bool Function() condition) async {
|
||||
await pumpEventsUntil(condition);
|
||||
await Future<void>.delayed(Duration.zero);
|
||||
}
|
||||
|
||||
test(
|
||||
'목록 — `GET /release-notes?limit=20`, 다음 쪽은 받은 커서 그대로 · id 는 경로 칸으로 인코딩',
|
||||
() async {
|
||||
notes.seed(3);
|
||||
final ReleaseNotesPage first = await repository().listNotes();
|
||||
final ReleaseNotesPage next = await repository().listNotes(
|
||||
cursor: 'n2',
|
||||
);
|
||||
await repository().getNote('2026-11-30');
|
||||
|
||||
expect(first.items, hasLength(3));
|
||||
expect(next.items.single.id, '2026-11-28');
|
||||
expect(notes.listRequests.first.queryParameters, <String, Object?>{
|
||||
'limit': kReleaseNotesPageSize,
|
||||
});
|
||||
expect(notes.listRequests.last.queryParameters['cursor'], 'n2');
|
||||
expect(
|
||||
ReleaseNotesApiPaths.releaseNote('2026/11 18'),
|
||||
'/release-notes/2026%2F11%2018',
|
||||
);
|
||||
expect(notes.noteRequests, <String>['2026-11-30']);
|
||||
},
|
||||
);
|
||||
|
||||
test(
|
||||
'응답 모양이 틀리면 SYS_001 — 인터셉터를 다 지난 뒤라 따로 가른다(ReleaseNotesMalformedResponse)',
|
||||
() async {
|
||||
notes.nextListResponse = FakeResponse.ok(<String, Object?>{'items': 1});
|
||||
final Object error = await errorOf(repository().listNotes());
|
||||
|
||||
expect(error, isA<ReleaseNotesMalformedResponse>());
|
||||
expect(apiErrorOf(error).code, 'SYS_001');
|
||||
expect(h.notifier.shownCodes, isEmpty, reason: '저장소는 알리지 않는다');
|
||||
},
|
||||
);
|
||||
|
||||
test('글 — 없는 글은 404 RES_001 이고 알리지 않는다', () async {
|
||||
final Object error = await errorOf(repository().getNote('2026-01-01'));
|
||||
|
||||
expect(apiErrorOf(error).code, 'RES_001');
|
||||
expect(h.notifier.shownCodes, isEmpty);
|
||||
});
|
||||
|
||||
test('첫 쪽은 알리지 않는다 — 받지 못함을 상태에 두고 [다시 시도]로 다시 받는다', () async {
|
||||
notes.nextListResponse = FakeResponse.fail(503, 'MAIL_001');
|
||||
await openList();
|
||||
|
||||
expect(listState().status, ReleaseNotesListStatus.failed);
|
||||
expect(listState().error?.code, 'MAIL_001');
|
||||
expect(h.notifier.shownCodes, isEmpty);
|
||||
|
||||
await container.read(releaseNotesListProvider.notifier).retry();
|
||||
|
||||
expect(listState().status, ReleaseNotesListStatus.ready);
|
||||
expect(listState().items, hasLength(kBoardReleaseNotes.length));
|
||||
});
|
||||
|
||||
test('[더 보기]의 겹친 글은 건너뛴다 — 그사이 글이 더해져 자리가 밀렸을 때', () async {
|
||||
notes.seed(21);
|
||||
await openList();
|
||||
// 새 글이 맨 위에 더해졌다 — 다음 쪽(21번째 자리부터)에 이미 받은 마지막 줄이 다시 온다
|
||||
notes.notes = <FakeReleaseNote>[
|
||||
const FakeReleaseNote('2026-12-01', '업데이트 22'),
|
||||
...notes.notes,
|
||||
];
|
||||
|
||||
await container.read(releaseNotesListProvider.notifier).loadMore();
|
||||
|
||||
final List<String> ids = <String>[
|
||||
for (final ReleaseNoteSummary note in listState().items) note.id,
|
||||
];
|
||||
expect(ids, hasLength(21));
|
||||
expect(ids.toSet(), hasLength(21));
|
||||
expect(listState().hasMore, isFalse);
|
||||
});
|
||||
|
||||
test('세션이 바뀌면 처음부터 — 앞 세션의 늦은 응답은 버린다', () async {
|
||||
notes.seed(2);
|
||||
final Completer<void> late = Completer<void>();
|
||||
int calls = 0;
|
||||
notes.onList = (RequestOptions options) async {
|
||||
calls++;
|
||||
if (calls == 1) {
|
||||
await late.future;
|
||||
return FakeResponse.ok(<String, Object?>{
|
||||
'items': <Object?>[
|
||||
const FakeReleaseNote('2020-01-01', '앞 세션의 글').summaryJson(),
|
||||
],
|
||||
'pageInfo': <String, Object?>{'hasNext': false, 'nextCursor': null},
|
||||
});
|
||||
}
|
||||
return notes.listPage(options);
|
||||
};
|
||||
final ProviderSubscription<ReleaseNotesListState> subscription = container
|
||||
.listen(
|
||||
releaseNotesListProvider,
|
||||
(ReleaseNotesListState? previous, ReleaseNotesListState next) {},
|
||||
);
|
||||
addTearDown(subscription.close);
|
||||
await pumpEventsUntil(() => notes.listRequests.length == 1);
|
||||
|
||||
container.read(_accountProvider.notifier).set('other@arasystem.it.kr');
|
||||
await pumpEventsUntil(
|
||||
() => listState().status == ReleaseNotesListStatus.ready,
|
||||
reason: '새 세션의 첫 쪽',
|
||||
);
|
||||
expect(notes.listRequests, hasLength(2));
|
||||
expect(listState().items.map((ReleaseNoteSummary n) => n.title), <String>[
|
||||
'업데이트 2',
|
||||
'업데이트 1',
|
||||
]);
|
||||
|
||||
late.complete();
|
||||
await settleAfter(() => recording.listsFinished == 2);
|
||||
|
||||
expect(listState().status, ReleaseNotesListStatus.ready);
|
||||
expect(listState().items.map((ReleaseNoteSummary n) => n.title), <String>[
|
||||
'업데이트 2',
|
||||
'업데이트 1',
|
||||
]);
|
||||
});
|
||||
|
||||
test('로그인하지 않았으면(세션 없음) 묻지 않는다', () async {
|
||||
container.read(_accountProvider.notifier).set(null);
|
||||
final ProviderSubscription<ReleaseNotesListState> list = container.listen(
|
||||
releaseNotesListProvider,
|
||||
(ReleaseNotesListState? previous, ReleaseNotesListState next) {},
|
||||
);
|
||||
addTearDown(list.close);
|
||||
final ProviderSubscription<ReleaseNoteState> note = container.listen(
|
||||
releaseNoteProvider('2026-11-18'),
|
||||
(ReleaseNoteState? previous, ReleaseNoteState next) {},
|
||||
);
|
||||
addTearDown(note.close);
|
||||
await Future<void>.delayed(Duration.zero);
|
||||
|
||||
expect(listState().status, ReleaseNotesListStatus.loading);
|
||||
expect(notes.listRequests, isEmpty);
|
||||
expect(notes.noteRequests, isEmpty);
|
||||
});
|
||||
|
||||
test('글 — 세션이 바뀌면 다시 받고, 앞 세션의 늦은 응답(실패)은 버린다', () async {
|
||||
final Completer<void> late = Completer<void>();
|
||||
int calls = 0;
|
||||
notes.onNote = (RequestOptions options) async {
|
||||
calls++;
|
||||
if (calls == 1) {
|
||||
await late.future;
|
||||
return FakeResponse.fail(500, 'SYS_001');
|
||||
}
|
||||
return notes.noteDetail('2026-11-05');
|
||||
};
|
||||
final ProviderSubscription<ReleaseNoteState> subscription = container
|
||||
.listen(
|
||||
releaseNoteProvider('2026-11-05'),
|
||||
(ReleaseNoteState? previous, ReleaseNoteState next) {},
|
||||
);
|
||||
addTearDown(subscription.close);
|
||||
await pumpEventsUntil(() => notes.noteRequests.length == 1);
|
||||
|
||||
container.read(_accountProvider.notifier).set('other@arasystem.it.kr');
|
||||
await pumpEventsUntil(
|
||||
() =>
|
||||
container.read(releaseNoteProvider('2026-11-05')).status ==
|
||||
ReleaseNoteStatus.ready,
|
||||
reason: '새 세션의 글',
|
||||
);
|
||||
expect(notes.noteRequests, hasLength(2));
|
||||
|
||||
late.complete();
|
||||
await settleAfter(() => recording.notesFinished == 2);
|
||||
|
||||
final ReleaseNoteState state = container.read(
|
||||
releaseNoteProvider('2026-11-05'),
|
||||
);
|
||||
expect(state.status, ReleaseNoteStatus.ready);
|
||||
expect(state.note?.title, '화면의 안내 글을 다듬었습니다');
|
||||
expect(state.error, isNull);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -8,6 +8,9 @@ import 'package:arasystem_app/core/push/push_messaging.dart';
|
||||
import 'package:arasystem_app/core/push/push_preferences.dart';
|
||||
import 'package:arasystem_app/core/push/push_texts.dart';
|
||||
import 'package:arasystem_app/features/auth/presentation/pages/login_page.dart';
|
||||
import 'package:arasystem_app/features/settings/presentation/pages/release_notes_page.dart';
|
||||
import 'package:arasystem_app/features/settings/presentation/pages/settings_page.dart';
|
||||
import 'package:arasystem_app/features/settings/presentation/release_notes_texts.dart';
|
||||
import 'package:arasystem_app/features/settings/presentation/settings_texts.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
@@ -16,6 +19,7 @@ import 'package:url_launcher/url_launcher.dart';
|
||||
import '../../support/fakes.dart';
|
||||
import '../../support/login_fakes.dart';
|
||||
import '../../support/push_screen_harness.dart';
|
||||
import '../../support/release_notes_fakes.dart';
|
||||
|
||||
/// 설정 탭 — 앱 보드 `App1Shell`(설정 한 화면 · 로그아웃 확인 창)과 `App1Push` ④(새 메일 알림 줄)
|
||||
void main() {
|
||||
@@ -102,6 +106,31 @@ void main() {
|
||||
expect(find.byType(LicensePage), findsOneWidget);
|
||||
expect(find.byType(NavigationBar), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets(
|
||||
'[업데이트 내역]은 탭 막대 없이 전체 화면 목록으로 연다 — 뒤로 오면 설정 (보드 ReleaseNotes B1)',
|
||||
(WidgetTester tester) async {
|
||||
final FakeReleaseNotesBackend notes = FakeReleaseNotesBackend(h.server)
|
||||
..install();
|
||||
await h.pump(tester);
|
||||
await h.openSettings(tester);
|
||||
|
||||
await tapVisible(tester, find.text(ReleaseNotesTexts.title));
|
||||
|
||||
expect(find.byType(ReleaseNotesPage), findsOneWidget);
|
||||
expect(find.byType(NavigationBar), findsNothing);
|
||||
expect(findCopy(kBoardReleaseNotes.first.title), findsOneWidget);
|
||||
expect(notes.listRequests, hasLength(1));
|
||||
|
||||
await tester.tap(find.byTooltip(ReleaseNotesTexts.back));
|
||||
await settle(tester);
|
||||
|
||||
expect(find.byType(ReleaseNotesPage), findsNothing);
|
||||
expect(find.byType(SettingsPage), findsOneWidget);
|
||||
expect(find.byType(NavigationBar), findsOneWidget);
|
||||
expect(h.app.location(), '/settings');
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
group('설정 › 앱 정보 › [새 버전 받기] — 앱 보드 App1Push ④', () {
|
||||
@@ -139,42 +168,63 @@ void main() {
|
||||
Finder snack(String text) =>
|
||||
find.descendant(of: find.byType(SnackBar), matching: findCopy(text));
|
||||
|
||||
testWidgets('줄 차례 — 앱 버전 · 새 버전 받기 · 오픈소스 라이선스, 끝 아이콘은 바깥으로 여는 아이콘', (
|
||||
WidgetTester tester,
|
||||
) async {
|
||||
await h.pump(tester);
|
||||
await h.openSettings(tester);
|
||||
testWidgets(
|
||||
'줄 차례 — 앱 버전 · 업데이트 내역 · 새 버전 받기 · 오픈소스 라이선스, 끝 아이콘은 바깥으로 여는 아이콘',
|
||||
(WidgetTester tester) async {
|
||||
await h.pump(tester);
|
||||
await h.openSettings(tester);
|
||||
|
||||
final double version = tester
|
||||
.getTopLeft(find.text(SettingsTexts.appVersion))
|
||||
.dy;
|
||||
final double download = tester
|
||||
.getTopLeft(find.text(AppDownloadTexts.getNewVersion))
|
||||
.dy;
|
||||
final double licenses = tester
|
||||
.getTopLeft(find.text(SettingsTexts.licenses))
|
||||
.dy;
|
||||
expect(version, lessThan(download));
|
||||
expect(download, lessThan(licenses));
|
||||
expect(
|
||||
rowIcon(AppDownloadTexts.getNewVersion, Icons.open_in_new),
|
||||
findsOneWidget,
|
||||
);
|
||||
expect(
|
||||
rowIcon(AppDownloadTexts.getNewVersion, Icons.chevron_right),
|
||||
findsNothing,
|
||||
);
|
||||
expect(
|
||||
rowIcon(SettingsTexts.licenses, Icons.chevron_right),
|
||||
findsOneWidget,
|
||||
);
|
||||
expect(
|
||||
tester.getSize(downloadRow()).height,
|
||||
greaterThanOrEqualTo(52),
|
||||
reason: '한 줄 칸 52 — 앱 버전 · 오픈소스 라이선스와 같다',
|
||||
);
|
||||
expect(launched, isEmpty);
|
||||
});
|
||||
final double version = tester
|
||||
.getTopLeft(find.text(SettingsTexts.appVersion))
|
||||
.dy;
|
||||
final double releaseNotes = tester
|
||||
.getTopLeft(find.text(ReleaseNotesTexts.title))
|
||||
.dy;
|
||||
final double download = tester
|
||||
.getTopLeft(find.text(AppDownloadTexts.getNewVersion))
|
||||
.dy;
|
||||
final double licenses = tester
|
||||
.getTopLeft(find.text(SettingsTexts.licenses))
|
||||
.dy;
|
||||
expect(version, lessThan(releaseNotes));
|
||||
expect(releaseNotes, lessThan(download));
|
||||
expect(download, lessThan(licenses));
|
||||
expect(
|
||||
rowIcon(ReleaseNotesTexts.title, Icons.chevron_right),
|
||||
findsOneWidget,
|
||||
reason: '앱 안의 화면 — 오픈소스 라이선스 줄과 같은 꼴',
|
||||
);
|
||||
expect(
|
||||
tester
|
||||
.getSize(
|
||||
find.ancestor(
|
||||
of: find.text(ReleaseNotesTexts.title),
|
||||
matching: find.byType(InkWell),
|
||||
),
|
||||
)
|
||||
.height,
|
||||
greaterThanOrEqualTo(52),
|
||||
);
|
||||
expect(
|
||||
rowIcon(AppDownloadTexts.getNewVersion, Icons.open_in_new),
|
||||
findsOneWidget,
|
||||
);
|
||||
expect(
|
||||
rowIcon(AppDownloadTexts.getNewVersion, Icons.chevron_right),
|
||||
findsNothing,
|
||||
);
|
||||
expect(
|
||||
rowIcon(SettingsTexts.licenses, Icons.chevron_right),
|
||||
findsOneWidget,
|
||||
);
|
||||
expect(
|
||||
tester.getSize(downloadRow()).height,
|
||||
greaterThanOrEqualTo(52),
|
||||
reason: '한 줄 칸 52 — 앱 버전 · 오픈소스 라이선스와 같다',
|
||||
);
|
||||
expect(launched, isEmpty);
|
||||
},
|
||||
);
|
||||
|
||||
testWidgets('누르면 업데이트 화면과 같은 받기 화면을 바깥 브라우저로 연다 — 설정에 머문다', (
|
||||
WidgetTester tester,
|
||||
|
||||
Reference in New Issue
Block a user