import 'package:flutter_test/flutter_test.dart'; import 'package:halkyon_learn_flutter/src/core/content/rich_content.dart'; import 'package:halkyon_learn_flutter/src/feature/card/data/card_payload.dart'; import 'package:halkyon_learn_flutter/src/feature/card/data/card_repository.dart'; import 'package:halkyon_learn_flutter/src/feature/card/model/card_content.dart'; import 'package:halkyon_learn_flutter/src/feature/learning_path/data/learning_path_repository.dart'; import 'package:halkyon_learn_flutter/src/feature/note/data/note_repository.dart'; import 'package:halkyon_learn_flutter/src/feature/profile/data/profile_repository.dart'; import 'package:halkyon_learn_flutter/src/feature/profile/model/public_profile.dart'; import 'package:halkyon_learn_flutter/src/feature/publish/bloc/publish_bloc.dart'; import 'package:halkyon_learn_flutter/src/feature/publish/data/publish_repository.dart'; import 'package:halkyon_learn_flutter/src/feature/publish/model/publication.dart'; import 'package:halkyon_learn_flutter/src/feature/publish/model/unsettled_item.dart'; import '../../support/fake_card_gateway.dart'; import '../../support/fake_learning_path_gateway.dart'; import '../../support/fake_note_gateway.dart'; import '../../support/fake_profile_gateway.dart'; import '../../support/fake_publish_gateway.dart'; import '../../support/memory_published_store.dart'; RichContent text(String text) => RichContent.plain(text); CardContent card(String front, String back) => CardContent(front: text(front), back: text(back)); void main() { late FakePublishGateway publishing; late FakeCardGateway cardGateway; late CardRepository cards; late List calls; setUp(() { publishing = FakePublishGateway(); cardGateway = FakeCardGateway(); cards = CardRepository(cardGateway); calls = []; }); PublishBloc bloc() => PublishBloc( PublishRepository(publishing, MemoryPublishedStore()), ProfileRepository( FakeProfileGateway( profile: const PublicProfile( handle: 'raisondetredev', address: 'raisondetredev@halkyon.eu', ), ), ), cards, NoteRepository(FakeNoteGateway()), LearningPathRepository(FakeLearningPathGateway()), studysetId: 'japanese', sync: () async => calls.add('sync'), ); /// Publishes through a started [bloc] and returns where it ends. Future publish(PublishBloc bloc) async { bloc.add(const PublishEventStarted()); await bloc.stream.firstWhere((state) => state is PublishStateReady); bloc.add( const PublishEventRequested( slug: 'japanese', notes: 'First lessons.', subtree: ['japanese'], ), ); return await bloc.stream.firstWhere( (state) => state is PublishStateReady && !state.publishing, ) as PublishStateReady; } /// A card in Japanese, edited on another device on [parents]' heads to /// [content]. Future editedElsewhere( CardContent mine, CardContent theirs, ) async { final id = await cards.create('japanese', card('猫', 'cat')); final created = (await cards.cardsOf('japanese')).single; await cards.save(created, mine); cardGateway.receiveRemoteRevision( id: 'rev-elsewhere', cardId: id, parents: created.heads, payload: encodeCardPayload(theirs), ); return id; } test('reads the publication with the profile', () async { final publisher = bloc()..add(const PublishEventStarted()); final ready = await publisher.stream.firstWhere( (state) => state is PublishStateReady, ) as PublishStateReady; expect(ready.publication, isNull); expect(ready.profile!.handle, 'raisondetredev'); await publisher.close(); }); test('syncs before publishing: the instance builds the release from what ' 'it holds', () async { final publisher = bloc(); final ready = await publish(publisher); expect(ready.outcome, PublishOutcome.published); expect(ready.publication!.latest!.version, 1); expect(calls, ['sync']); expect(publishing.publishes.single.notes, 'First lessons.'); await publisher.close(); }); test('changes from two devices that merge are saved merged, and ' 'publishing tries again', () async { final id = await editedElsewhere(card('ねこ', 'cat'), card('猫', 'a cat')); publishing.refusals.add( PublishRefused(PublishProblem.conflicts, items: [id]), ); final publisher = bloc(); final ready = await publish(publisher); expect(ready.outcome, PublishOutcome.published); expect(publishing.publishes, hasLength(2)); expect(cardGateway.written.last.parents, hasLength(2)); expect((await cards.cardsOf('japanese')).single.heads, hasLength(1)); expect(calls, ['sync', 'sync']); await publisher.close(); }); test('changes that differ on the same side are left to the person, ' 'named', () async { final id = await editedElsewhere(card('猫', 'a cat'), card('猫', 'the cat')); final written = cardGateway.written.length; publishing.refusals.add( PublishRefused(PublishProblem.conflicts, items: [id]), ); final publisher = bloc(); final ready = await publish(publisher); expect(ready.problem, PublishProblem.conflicts); expect(ready.unsettled, [ UnsettledItem( kind: UnsettledKind.card, id: id, studysetId: 'japanese', title: '猫', ), ]); expect(publishing.publishes, hasLength(1)); expect(cardGateway.written, hasLength(written)); await publisher.close(); }); test('a refusal says why; an instance out of reach, that nothing was ' 'published', () async { publishing.refusals.add(PublishRefused(PublishProblem.nothingChanged)); final publisher = bloc(); expect((await publish(publisher)).problem, PublishProblem.nothingChanged); publishing.fail = Exception('offline'); publisher.add( const PublishEventRequested(slug: 'japanese', notes: '', subtree: []), ); final failed = await publisher.stream.firstWhere( (state) => state is PublishStateReady && !state.publishing, ) as PublishStateReady; expect(failed.outcome, PublishOutcome.failed); expect(failed.publication, isNull); await publisher.close(); }); group('A name from a title', () { test('is lower-case letters and digits with hyphens between', () { expect(slugFrom('Japanese'), 'japanese'); expect(slugFrom(' N5 — Kanji & Kana! '), 'n5-kanji-kana'); expect(slugFrom('Japanisch für Anfänger'), 'japanisch-fuer-anfaenger'); expect(slugFrom('Café crème'), 'cafe-creme'); }); test('is empty for a title without Latin letters or digits', () { expect(slugFrom('日本語'), ''); }); test('is at most 50 characters, never ending in a hyphen', () { final slug = slugFrom('${'a' * 49} b'); expect(slug, 'a' * 49); }); }); }