generated from ttipo/project-base
feat(flutter): 공유 다이어리 — 초대·참여·멤버와 함께 쓰기
다이어리를 가족·지인과 함께 쓰는 화면을 연다. - 다이어리 설정: 개인↔공유 전환(멤버가 있으면 확인), 멤버 목록·내보내기, 초대 QR·코드 복사, 다이어리 나가기 - 참여: 코드 입력 시트 · 초대 QR 스캐너 · 참여 확인 (명함 스캐너를 공용 QR 스캐너로 바꿔 함께 쓴다) - 만들 때 공유 다이어리를 고를 수 있다 - 다른 멤버의 기록·댓글은 그 사람 이름·사진으로, 수정·삭제는 내 것만 - 참여한 다이어리 표지는 서버가 준 주인의 아이들로 그린다 - 다른 멤버의 새 글·내 기록의 댓글을 알림으로 보여 주고, 공유 다이어리에 들어오면 다시 읽는다 - 알림 설정은 멤버마다 따로 저장한다 - 기기 저장 모드에서는 공유 다이어리를 쓸 수 없다고 안내한다 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -4,6 +4,7 @@ import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:tma_app/core/api/file_uploader.dart';
|
||||
import 'package:tma_app/features/diary/data/api_diary_repository.dart';
|
||||
import 'package:tma_app/features/diary/domain/diary.dart';
|
||||
import 'package:tma_app/features/diary/domain/diary_invitation.dart';
|
||||
import 'package:tma_app/features/diary/domain/mood.dart';
|
||||
|
||||
import 'support/fake_api.dart';
|
||||
@@ -113,10 +114,10 @@ void main() {
|
||||
expect(api.reported.single.message, isNot('서버 문구'));
|
||||
});
|
||||
|
||||
test('다이어리 수정은 바탕색을 보내지 않는다 (만들 때 정한 값을 유지한다)', () async {
|
||||
test('다이어리 수정은 바탕색·알림을 보내지 않는다 (바탕색은 만들 때 정하고, 알림은 멤버마다 따로 둔다)', () async {
|
||||
api.on('PATCH', '/diaries/:id', (r) {
|
||||
expect(r.json!.containsKey('tint'), isFalse);
|
||||
expect(r.json!['mutedNotices'], ['newComment']);
|
||||
expect(r.json!.containsKey('mutedNotices'), isFalse);
|
||||
return ok(_diaryJson());
|
||||
});
|
||||
final diary = Diary(
|
||||
@@ -133,6 +134,155 @@ void main() {
|
||||
expect(api.sent('PATCH', '/diaries/d1'), hasLength(1));
|
||||
});
|
||||
|
||||
test('내 알림 설정은 멤버 알림 경로로 보낸다', () async {
|
||||
api.on('PUT', '/diaries/:id/muted-notices', (r) {
|
||||
expect(r.json, {
|
||||
'mutedNotices': ['newPost'],
|
||||
});
|
||||
return noContent;
|
||||
});
|
||||
|
||||
await repo.setMutedNotices('d1', const {DiaryNotice.newPost});
|
||||
|
||||
expect(api.sent('PUT', '/diaries/d1/muted-notices'), hasLength(1));
|
||||
});
|
||||
|
||||
group('공유 다이어리', () {
|
||||
Map<String, Object?> sharedJson({bool isOwner = true}) => {
|
||||
..._diaryJson(),
|
||||
'shared': true,
|
||||
'isOwner': isOwner,
|
||||
'ownerId': isOwner ? 'me-uuid' : 'friend-uuid',
|
||||
'inviteCode': isOwner ? 'RUP-8F3K2Q' : null,
|
||||
'members': [
|
||||
{'userId': isOwner ? 'me-uuid' : 'friend-uuid', 'nickname': '콩이아빠', 'photoFileId': 'face-1', 'isOwner': true},
|
||||
{'userId': isOwner ? 'friend-uuid' : 'me-uuid', 'nickname': '콩이엄마', 'photoFileId': null, 'isOwner': false},
|
||||
],
|
||||
'targets': [
|
||||
{'id': 'p1', 'name': '콩이', 'photoFileId': 'pet-1', 'tint': '#FF8A5B', 'archived': false},
|
||||
{'id': 'self', 'name': '콩이아빠', 'photoFileId': 'face-1', 'tint': null, 'archived': false},
|
||||
],
|
||||
};
|
||||
|
||||
test('참여한 다이어리는 멤버의 나를 알아보고, 주인의 아이들을 서버가 준 이름·사진으로 담는다', () async {
|
||||
api.on('GET', '/diaries', (r) => ok(page([sharedJson(isOwner: false)])));
|
||||
api.on('GET', '/diary-posts', (r) => ok(page([
|
||||
{
|
||||
..._postJson(authorId: 'friend-uuid'),
|
||||
'author': {'userId': 'friend-uuid', 'nickname': '콩이아빠', 'photoFileId': 'face-1'},
|
||||
},
|
||||
])));
|
||||
api.on('GET', '/diary-comments', (r) => ok(page(const [])));
|
||||
|
||||
final book = await repo.fetch();
|
||||
|
||||
final diary = book.diaries.single;
|
||||
expect(diary.shared, isTrue);
|
||||
expect(diary.isOwner, isFalse);
|
||||
expect(diary.inviteCode, isNull);
|
||||
expect(diary.owner!.name, '콩이아빠');
|
||||
expect(diary.members.last.isMe, isTrue);
|
||||
expect(diary.targets.map((t) => t.name), ['콩이', '콩이아빠']);
|
||||
expect(diary.targets.first.photoPath, 'server-file:pet-1');
|
||||
expect(diary.targets.last.isSelf, isTrue);
|
||||
// 주인의 아이들이라 내 추억 보관함과 상관없이 책장에 둔다
|
||||
expect(diary.isArchivedWith({'p1'}), isFalse);
|
||||
final post = book.posts.single;
|
||||
expect(post.isMine, isFalse);
|
||||
expect(post.author!.name, '콩이아빠');
|
||||
expect(post.author!.photoPath, 'server-file:face-1');
|
||||
});
|
||||
|
||||
test('내 다이어리는 아이 요약을 담지 않는다 — 내 반려동물 목록으로 그린다', () {
|
||||
final diary = diaryFromApi(sharedJson(), myUserId: 'me-uuid');
|
||||
|
||||
expect(diary.isOwner, isTrue);
|
||||
expect(diary.inviteCode, 'RUP-8F3K2Q');
|
||||
expect(diary.targets, isEmpty);
|
||||
expect(diary.memberCount, 2);
|
||||
expect(diary.members.first.isMe, isTrue);
|
||||
});
|
||||
|
||||
test('만들 때 공유 여부를 보낸다', () async {
|
||||
api.on('POST', '/diaries', (r) {
|
||||
expect(r.json!['shared'], isTrue);
|
||||
return ok(sharedJson(), status: 201);
|
||||
});
|
||||
|
||||
final created = await repo.createDiary(
|
||||
name: '콩이의 하루',
|
||||
targetIds: const ['p1'],
|
||||
ribbon: kRibbonColors.first,
|
||||
tint: kDiaryTints.first,
|
||||
shared: true,
|
||||
);
|
||||
|
||||
expect(created!.shared, isTrue);
|
||||
});
|
||||
|
||||
test('개인 ↔ 공유 전환', () async {
|
||||
api.on('PUT', '/diaries/:id/sharing', (r) {
|
||||
expect(r.json, {'shared': false});
|
||||
return ok(_diaryJson());
|
||||
});
|
||||
|
||||
final diary = await repo.setShared('d1', false);
|
||||
|
||||
expect(diary.shared, isFalse);
|
||||
});
|
||||
|
||||
test('초대 코드로 미리 보고, 참여하면 참여한 다이어리의 기록까지 다시 읽는다', () async {
|
||||
api.on('GET', '/diary-invitations/:code', (r) => ok({
|
||||
'diaryId': 'd1',
|
||||
'name': '콩이의 하루',
|
||||
'ownerNickname': '콩이아빠',
|
||||
'memberCount': 2,
|
||||
'maxMembers': 4,
|
||||
'isMember': false,
|
||||
}));
|
||||
api.on('POST', '/diary-memberships', (r) {
|
||||
expect(r.json, {'code': 'RUP-8F3K2Q'});
|
||||
return ok(sharedJson(isOwner: false), status: 201);
|
||||
});
|
||||
api.on('GET', '/diaries', (r) => ok(page([sharedJson(isOwner: false)])));
|
||||
api.on('GET', '/diary-posts', (r) => ok(page(const [])));
|
||||
api.on('GET', '/diary-comments', (r) => ok(page(const [])));
|
||||
|
||||
final invitation = await repo.previewInvitation('RUP-8F3K2Q');
|
||||
final book = await repo.join(invitation.code);
|
||||
|
||||
expect(invitation.name, '콩이의 하루');
|
||||
expect(invitation.isFull, isFalse);
|
||||
expect(api.sent('GET', '/diary-invitations/RUP-8F3K2Q'), hasLength(1));
|
||||
expect(book.diaries.single.isOwner, isFalse);
|
||||
});
|
||||
|
||||
test('나가기 · 멤버 내보내기', () async {
|
||||
api.on('DELETE', '/diaries/:id/membership', (r) => noContent);
|
||||
api.on('DELETE', '/diaries/:id/members/:userId', (r) => noContent);
|
||||
|
||||
await repo.leave('d1');
|
||||
await repo.removeMember('d1', 'friend-uuid');
|
||||
|
||||
expect(api.sent('DELETE', '/diaries/d1/membership'), hasLength(1));
|
||||
expect(api.sent('DELETE', '/diaries/d1/members/friend-uuid'), hasLength(1));
|
||||
});
|
||||
});
|
||||
|
||||
group('parseInviteCode', () {
|
||||
test('QR 주소·코드·소문자·앞머리 없는 6자를 모두 초대 코드로 읽는다', () {
|
||||
expect(parseInviteCode('rup://diary-invite/RUP-8F3K2Q'), 'RUP-8F3K2Q');
|
||||
expect(parseInviteCode(' rup-8f3k2q '), 'RUP-8F3K2Q');
|
||||
expect(parseInviteCode('8F3K2Q'), 'RUP-8F3K2Q');
|
||||
});
|
||||
|
||||
test('초대 코드가 아니면 null', () {
|
||||
expect(parseInviteCode('RUP-8F3K'), isNull);
|
||||
expect(parseInviteCode('rup://pet-card/0428-117'), isNull);
|
||||
expect(parseInviteCode('RUP-../../'), isNull);
|
||||
});
|
||||
});
|
||||
|
||||
test('아이를 지울 때 다이어리 정리는 서버가 이미 했다', () async {
|
||||
await repo.dropTarget('p1');
|
||||
|
||||
|
||||
@@ -5,8 +5,8 @@ import 'package:tma_app/features/pet_card/data/received_card_repository.dart';
|
||||
import 'package:tma_app/features/pet_card/domain/card_style.dart';
|
||||
import 'package:tma_app/features/pet_card/domain/card_view_mode.dart';
|
||||
import 'package:tma_app/features/pet_card/domain/received_card.dart';
|
||||
import 'package:tma_app/features/pet_card/presentation/pages/card_scanner_page.dart';
|
||||
import 'package:tma_app/shared/widgets/icons/rup_icon.dart';
|
||||
import 'package:tma_app/shared/widgets/qr_scanner_page.dart';
|
||||
|
||||
import 'support/app_harness.dart';
|
||||
|
||||
|
||||
@@ -0,0 +1,397 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:tma_app/core/api/api_exception.dart';
|
||||
import 'package:tma_app/features/diary/data/diary_repository.dart';
|
||||
import 'package:tma_app/features/diary/domain/diary.dart';
|
||||
import 'package:tma_app/features/diary/domain/diary_book.dart';
|
||||
import 'package:tma_app/features/diary/domain/diary_invitation.dart';
|
||||
import 'package:tma_app/features/diary/domain/mood.dart';
|
||||
import 'package:tma_app/features/diary/presentation/pages/diary_page.dart';
|
||||
import 'package:tma_app/features/diary/presentation/pages/diary_settings_page.dart';
|
||||
|
||||
import 'support/app_harness.dart';
|
||||
|
||||
const String _kCode = 'RUP-8F3K2Q';
|
||||
|
||||
const DiaryMember _me = DiaryMember(id: kMeAuthorId, nickname: '나집사');
|
||||
const DiaryMember _dad = DiaryMember(id: 'dad', nickname: '콩이아빠', isOwner: true);
|
||||
const DiaryMember _mom = DiaryMember(id: 'mom', nickname: '콩이엄마');
|
||||
|
||||
/// 내가 만든 공유 다이어리 — 나(주인) + 콩이엄마
|
||||
Diary _ownedShared() => Diary(
|
||||
id: 'd1',
|
||||
name: '콩이 성장일기',
|
||||
targetIds: const ['pet-1'],
|
||||
ribbon: kRibbonColors.first,
|
||||
tint: kDiaryTints.first,
|
||||
shared: true,
|
||||
inviteCode: _kCode,
|
||||
members: const [DiaryMember(id: kMeAuthorId, nickname: '나집사', isOwner: true), _mom],
|
||||
);
|
||||
|
||||
/// 초대받아 참여한 다이어리 — 콩이아빠(주인) + 나
|
||||
Diary _joined({String id = 'd2', String name = '함께 쓰는 다이어리'}) => Diary(
|
||||
id: id,
|
||||
name: name,
|
||||
targetIds: const ['dad-pet'],
|
||||
ribbon: kRibbonColors.first,
|
||||
tint: kDiaryTints.first,
|
||||
shared: true,
|
||||
isOwner: false,
|
||||
members: const [_dad, _me],
|
||||
targets: const [DiaryTarget(id: 'dad-pet', name: '초코')],
|
||||
);
|
||||
|
||||
/// 서버 없이 공유 다이어리를 흉내 내는 저장소
|
||||
class _FakeSharedRepo implements DiaryRepository {
|
||||
_FakeSharedRepo(this.book, {this.invitation});
|
||||
|
||||
DiaryBook book;
|
||||
final DiaryInvitation? invitation;
|
||||
final calls = <String>[];
|
||||
|
||||
@override
|
||||
bool get canShare => true;
|
||||
|
||||
@override
|
||||
Future<DiaryBook> fetch() async => book;
|
||||
|
||||
@override
|
||||
Future<Diary?> createDiary({
|
||||
required String name,
|
||||
required List<String> targetIds,
|
||||
required Color ribbon,
|
||||
required Color tint,
|
||||
String? coverPath,
|
||||
bool shared = false,
|
||||
}) async {
|
||||
calls.add('create shared=$shared');
|
||||
final diary = Diary(
|
||||
id: 'new',
|
||||
name: name,
|
||||
targetIds: targetIds,
|
||||
ribbon: ribbon,
|
||||
tint: tint,
|
||||
shared: shared,
|
||||
inviteCode: shared ? _kCode : null,
|
||||
members: shared ? const [DiaryMember(id: kMeAuthorId, isOwner: true)] : const [],
|
||||
);
|
||||
book = book.addDiary(diary);
|
||||
return diary;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Diary> updateDiary(Diary diary) async => diary;
|
||||
|
||||
@override
|
||||
Future<void> setMutedNotices(String diaryId, Set<DiaryNotice> muted) async => calls.add('muted $muted');
|
||||
|
||||
@override
|
||||
Future<Diary> setShared(String diaryId, bool shared) async {
|
||||
calls.add('shared $diaryId $shared');
|
||||
final d = book.diary(diaryId)!;
|
||||
final next = Diary(
|
||||
id: d.id,
|
||||
name: d.name,
|
||||
targetIds: d.targetIds,
|
||||
ribbon: d.ribbon,
|
||||
tint: d.tint,
|
||||
shared: shared,
|
||||
inviteCode: shared ? _kCode : null,
|
||||
members: shared ? d.members.where((m) => m.isOwner).toList() : const [],
|
||||
);
|
||||
book = book.updateDiary(next);
|
||||
return next;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<DiaryInvitation> previewInvitation(String code) async {
|
||||
calls.add('preview $code');
|
||||
final found = invitation;
|
||||
if (found == null) throw ApiException.fromCode('DIARY_003');
|
||||
return found;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<DiaryBook> join(String code) async {
|
||||
calls.add('join $code');
|
||||
book = book.addDiary(_joined());
|
||||
return book;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> leave(String diaryId) async => calls.add('leave $diaryId');
|
||||
|
||||
@override
|
||||
Future<void> removeMember(String diaryId, String memberId) async {
|
||||
calls.add('remove $diaryId $memberId');
|
||||
// 서버처럼 멤버를 빼고 초대 코드를 바꾼다
|
||||
final d = book.diary(diaryId)!;
|
||||
book = book.updateDiary(Diary(
|
||||
id: d.id,
|
||||
name: d.name,
|
||||
targetIds: d.targetIds,
|
||||
ribbon: d.ribbon,
|
||||
tint: d.tint,
|
||||
shared: d.shared,
|
||||
inviteCode: 'RUP-NEW000',
|
||||
members: d.members.where((m) => m.id != memberId).toList(),
|
||||
));
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> deleteDiary(String diaryId) async {}
|
||||
|
||||
@override
|
||||
Future<DiaryPost> addPost({
|
||||
required String diaryId,
|
||||
required DateTime date,
|
||||
required String text,
|
||||
Mood? mood,
|
||||
String? photoPath,
|
||||
}) async =>
|
||||
DiaryPost(id: 'p-new', diaryId: diaryId, date: date, createdAt: date, text: text);
|
||||
|
||||
@override
|
||||
Future<DiaryPost> updatePostText(DiaryPost post, String text) async => post.copyWith(text: text);
|
||||
|
||||
@override
|
||||
Future<void> deletePost(String postId) async {}
|
||||
|
||||
@override
|
||||
Future<DiaryComment> addComment(String postId, String text) async =>
|
||||
DiaryComment(id: 'c-new', postId: postId, text: text, createdAt: DateTime.now());
|
||||
|
||||
@override
|
||||
Future<DiaryComment> updateComment(DiaryComment comment, String text) async => comment.copyWith(text: text);
|
||||
|
||||
@override
|
||||
Future<void> deleteComment(String commentId) async {}
|
||||
|
||||
@override
|
||||
Future<void> dropTarget(String targetId) async {}
|
||||
}
|
||||
|
||||
Future<_FakeSharedRepo> _launch(WidgetTester tester, DiaryBook book, {DiaryInvitation? invitation}) async {
|
||||
final repo = _FakeSharedRepo(book, invitation: invitation);
|
||||
await launchSignedIn(
|
||||
tester,
|
||||
extra: {'pets.v1': jsonEncode([samplePet().toJson()])},
|
||||
overrides: [diaryRepositoryProvider.overrideWithValue(repo)],
|
||||
);
|
||||
return repo;
|
||||
}
|
||||
|
||||
Future<void> _openShelf(WidgetTester tester) async {
|
||||
await tester.tap(find.bySemanticsLabel('일정·기록'));
|
||||
await settle(tester);
|
||||
await tester.tap(find.text('다이어리'));
|
||||
await settle(tester);
|
||||
}
|
||||
|
||||
Future<void> _openDiary(WidgetTester tester, String name) async {
|
||||
await _openShelf(tester);
|
||||
await tester.tap(find.text(name));
|
||||
await settle(tester);
|
||||
expect(find.byType(DiaryPage), findsOneWidget);
|
||||
}
|
||||
|
||||
Future<void> _openSettings(WidgetTester tester, String name) async {
|
||||
await _openDiary(tester, name);
|
||||
await tester.tap(find.bySemanticsLabel('다이어리 설정'));
|
||||
await settle(tester);
|
||||
expect(find.byType(DiarySettingsPage), findsOneWidget);
|
||||
}
|
||||
|
||||
Future<void> _tapVisible(WidgetTester tester, Finder finder) async {
|
||||
await tester.ensureVisible(finder);
|
||||
await tester.pump();
|
||||
await tester.tap(finder);
|
||||
await settle(tester);
|
||||
}
|
||||
|
||||
void main() {
|
||||
testWidgets('주인은 멤버와 초대 코드를 보고, 멤버를 내보낼 수 있다', (tester) async {
|
||||
final repo = await _launch(tester, DiaryBook(diaries: [_ownedShared()]));
|
||||
await _openDiary(tester, '콩이 성장일기');
|
||||
expect(find.text('2/4'), findsOneWidget);
|
||||
|
||||
await tester.tap(find.bySemanticsLabel('다이어리 설정'));
|
||||
await settle(tester);
|
||||
expect(find.text('멤버'), findsOneWidget);
|
||||
expect(find.text('2/4'), findsOneWidget);
|
||||
expect(find.text('주인'), findsOneWidget);
|
||||
expect(find.text('콩이엄마'), findsOneWidget);
|
||||
expect(find.text('2명 더 초대할 수 있어요'), findsOneWidget);
|
||||
await tester.ensureVisible(find.text(_kCode));
|
||||
expect(find.text(_kCode), findsOneWidget);
|
||||
|
||||
await _tapVisible(tester, find.bySemanticsLabel('콩이엄마님 내보내기'));
|
||||
expect(find.text('콩이엄마님을 내보낼까요?'), findsOneWidget);
|
||||
await tester.tap(find.widgetWithText(GestureDetector, '내보내기').last);
|
||||
await settle(tester);
|
||||
|
||||
expect(repo.calls, contains('remove d1 mom'));
|
||||
expect(find.text('콩이엄마님을 내보냈어요'), findsOneWidget);
|
||||
expect(find.text('1/4'), findsOneWidget);
|
||||
// 내보내면 서버가 초대 코드를 바꾼다 — 다시 읽어 새 코드를 보여 준다
|
||||
await tester.ensureVisible(find.text('RUP-NEW000'));
|
||||
expect(find.text(_kCode), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('멤버가 있는 다이어리를 개인으로 바꾸면 먼저 묻는다', (tester) async {
|
||||
final repo = await _launch(tester, DiaryBook(diaries: [_ownedShared()]));
|
||||
await _openSettings(tester, '콩이 성장일기');
|
||||
|
||||
await tester.tap(find.text('개인 다이어리'));
|
||||
await settle(tester);
|
||||
expect(find.text('개인 다이어리로 전환할까요?'), findsOneWidget);
|
||||
|
||||
await tester.tap(find.text('전환하기'));
|
||||
await settle(tester);
|
||||
|
||||
expect(repo.calls, contains('shared d1 false'));
|
||||
expect(find.text('개인 다이어리로 전환했어요'), findsOneWidget);
|
||||
expect(find.text('멤버'), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('참여한 다이어리는 만든 사람을 보여 주고, 나갈 수 있다', (tester) async {
|
||||
final repo = await _launch(tester, DiaryBook(diaries: [_joined()]));
|
||||
await _openSettings(tester, '함께 쓰는 다이어리');
|
||||
|
||||
expect(find.text('콩이아빠님이 만든 다이어리예요'), findsOneWidget);
|
||||
expect(find.bySemanticsLabel('다이어리 수정하기'), findsNothing);
|
||||
expect(find.text('내보내기'), findsNothing);
|
||||
expect(find.text('다이어리 제거'), findsNothing);
|
||||
|
||||
await _tapVisible(tester, find.text('다이어리 나가기'));
|
||||
expect(find.text('다이어리에서 나갈까요?'), findsOneWidget);
|
||||
await tester.tap(find.text('나가기'));
|
||||
await settle(tester);
|
||||
|
||||
expect(repo.calls, contains('leave d2'));
|
||||
expect(find.text('다이어리에서 나왔어요'), findsOneWidget);
|
||||
expect(find.byType(DiarySettingsPage), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('초대 코드를 입력하고 확인하면 공유 다이어리에 참여한다', (tester) async {
|
||||
final repo = await _launch(
|
||||
tester,
|
||||
DiaryBook.empty,
|
||||
invitation: const DiaryInvitation(
|
||||
code: _kCode,
|
||||
diaryId: 'd2',
|
||||
name: '함께 쓰는 다이어리',
|
||||
ownerNickname: '콩이아빠',
|
||||
memberCount: 1,
|
||||
maxMembers: 4,
|
||||
isMember: false,
|
||||
),
|
||||
);
|
||||
await _openShelf(tester);
|
||||
await tester.tap(find.bySemanticsLabel('새 다이어리'));
|
||||
await settle(tester);
|
||||
await tester.tap(find.text('공유 다이어리 참여하기'));
|
||||
await settle(tester);
|
||||
expect(find.text('공유 다이어리 참여'), findsOneWidget);
|
||||
|
||||
await tester.enterText(find.byType(TextField), 'rup-8f3k2q');
|
||||
await tester.pump();
|
||||
await tester.tap(find.text('참여하기'));
|
||||
await settle(tester);
|
||||
|
||||
expect(repo.calls, contains('preview $_kCode'));
|
||||
expect(find.text('함께 쓰는 다이어리에\n참여하시겠어요?'), findsOneWidget);
|
||||
await tester.tap(find.text('참여하기'));
|
||||
await settle(tester);
|
||||
|
||||
expect(repo.calls, contains('join $_kCode'));
|
||||
expect(find.text('공유 다이어리에 참여했어요'), findsOneWidget);
|
||||
expect(find.text('함께 쓰는 다이어리'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('없는 초대 코드는 서버 안내로 멈추고 시트를 열어 둔다', (tester) async {
|
||||
final repo = await _launch(tester, DiaryBook.empty);
|
||||
await _openShelf(tester);
|
||||
await tester.tap(find.bySemanticsLabel('새 다이어리'));
|
||||
await settle(tester);
|
||||
await tester.tap(find.text('공유 다이어리 참여하기'));
|
||||
await settle(tester);
|
||||
|
||||
await tester.enterText(find.byType(TextField), 'RUP-ZZZZZZ');
|
||||
await tester.pump();
|
||||
await tester.tap(find.text('참여하기'));
|
||||
await settle(tester);
|
||||
|
||||
expect(repo.calls, ['preview RUP-ZZZZZZ']);
|
||||
expect(find.text('공유 다이어리 참여'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('공유 다이어리로 만들 수 있다', (tester) async {
|
||||
final repo = await _launch(tester, DiaryBook.empty);
|
||||
await _openShelf(tester);
|
||||
await tester.tap(find.bySemanticsLabel('새 다이어리'));
|
||||
await settle(tester);
|
||||
await tester.tap(find.text('새 다이어리 만들기'));
|
||||
await settle(tester);
|
||||
|
||||
await tester.tap(find.text('공유 다이어리'));
|
||||
await tester.pump();
|
||||
expect(find.text('만든 뒤 가족·지인을 초대해 함께 기록할 수 있어요.'), findsOneWidget);
|
||||
await tester.enterText(find.byType(TextField).first, '가족 일기');
|
||||
await _tapVisible(tester, find.text('다이어리 만들기'));
|
||||
|
||||
expect(repo.calls, contains('create shared=true'));
|
||||
expect(find.text('공유 다이어리를 만들었어요'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('다른 멤버의 기록은 그 사람 이름으로 보이고, 고치거나 지울 수 없다', (tester) async {
|
||||
final today = DateTime.now();
|
||||
final post = DiaryPost(
|
||||
id: 'p-dad',
|
||||
diaryId: 'd2',
|
||||
date: DateTime(today.year, today.month, today.day),
|
||||
createdAt: today,
|
||||
text: '초코랑 산책했어요',
|
||||
authorId: 'dad',
|
||||
author: _dad,
|
||||
);
|
||||
final comment = DiaryComment(id: 'c-dad', postId: 'p-dad', text: '귀엽다', createdAt: today, authorId: 'dad', author: _dad);
|
||||
await _launch(tester, DiaryBook(diaries: [_joined()], posts: [post], comments: [comment]));
|
||||
await _openDiary(tester, '함께 쓰는 다이어리');
|
||||
|
||||
expect(find.text('초코랑 산책했어요'), findsOneWidget);
|
||||
expect(find.text('콩이아빠'), findsOneWidget);
|
||||
expect(find.bySemanticsLabel('기록 관리'), findsNothing);
|
||||
|
||||
await _tapVisible(tester, find.textContaining('댓글'));
|
||||
expect(find.text('귀엽다'), findsOneWidget);
|
||||
expect(find.text('수정'), findsNothing);
|
||||
expect(find.text('삭제'), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('다른 멤버의 새 글은 알림에 나온다', (tester) async {
|
||||
final now = DateTime.now();
|
||||
final post = DiaryPost(
|
||||
id: 'p-dad',
|
||||
diaryId: 'd2',
|
||||
date: DateTime(now.year, now.month, now.day),
|
||||
createdAt: now.subtract(const Duration(minutes: 5)),
|
||||
text: '초코랑 산책했어요',
|
||||
authorId: 'dad',
|
||||
author: _dad,
|
||||
);
|
||||
await _launch(tester, DiaryBook(diaries: [_joined()], posts: [post]));
|
||||
|
||||
// 새 글이 있으니 종에 점이 찍혀 있다
|
||||
await tester.tap(find.bySemanticsLabel('알림, 새 알림 있음'));
|
||||
await settle(tester);
|
||||
|
||||
expect(find.text('콩이아빠님이 새 글을 올렸어요'), findsOneWidget);
|
||||
expect(find.text('“초코랑 산책했어요”'), findsOneWidget);
|
||||
// 저녁 8시가 지났으면 기록 알림도 함께 나온다
|
||||
expect(find.textContaining('함께 쓰는 다이어리 ·'), findsWidgets);
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user