chore: Halkyon Learn moves into apps/learn
Its server, client, app and curator, and the tools that write its assets (maps, Anki fixtures, icons), as they are; paths are fixed in the commits that follow. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
parent
11abd9afce
commit
649499be23
3 changed files with 0 additions and 0 deletions
|
|
@ -0,0 +1,195 @@
|
|||
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<String> 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<PublishStateReady> 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<String> 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);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
@ -0,0 +1,60 @@
|
|||
import 'package:flutter_test/flutter_test.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 '../../support/fake_publish_gateway.dart';
|
||||
import '../../support/memory_published_store.dart';
|
||||
|
||||
void main() {
|
||||
const japanese = Publication(
|
||||
publicId: 'p',
|
||||
slug: 'japanese',
|
||||
location: 'halkyon.eu/@raisondetredev/japanese',
|
||||
);
|
||||
|
||||
test('the studysets out on Explore: as the device remembered, then as the '
|
||||
'instance says, which the device remembers', () async {
|
||||
final store = MemoryPublishedStore({'kanji'});
|
||||
final publishing = PublishRepository(
|
||||
FakePublishGateway(publications: {'japanese': japanese}),
|
||||
store,
|
||||
);
|
||||
addTearDown(publishing.dispose);
|
||||
final seen = <Set<String>>[];
|
||||
final subscription = publishing.watchPublished().listen(seen.add);
|
||||
addTearDown(subscription.cancel);
|
||||
await pumpEventQueue();
|
||||
|
||||
await publishing.refreshPublished();
|
||||
await pumpEventQueue();
|
||||
|
||||
expect(seen, [
|
||||
{'kanji'},
|
||||
{'japanese'},
|
||||
]);
|
||||
expect(store.kept, {'japanese'});
|
||||
});
|
||||
|
||||
test('publishing adds the studyset, withdrawing takes it away, and looking '
|
||||
'one up brings what the instance says', () async {
|
||||
final gateway = FakePublishGateway(publications: {'japanese': japanese});
|
||||
final publishing = PublishRepository(gateway, MemoryPublishedStore());
|
||||
addTearDown(publishing.dispose);
|
||||
final seen = <Set<String>>[];
|
||||
final subscription = publishing.watchPublished().listen(seen.add);
|
||||
addTearDown(subscription.cancel);
|
||||
await pumpEventQueue();
|
||||
|
||||
await publishing.publication('japanese');
|
||||
await publishing.publish('kanji', slug: 'kanji', notes: '');
|
||||
await publishing.withdraw('japanese');
|
||||
await pumpEventQueue();
|
||||
|
||||
expect(seen, [
|
||||
<String>{},
|
||||
{'japanese'},
|
||||
{'japanese', 'kanji'},
|
||||
{'kanji'},
|
||||
]);
|
||||
});
|
||||
}
|
||||
|
|
@ -0,0 +1,406 @@
|
|||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:halkyon_app/halkyon_app.dart';
|
||||
import 'package:halkyon_app/testing.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/content/rich_content.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/router/route_paths.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/profile/model/public_profile.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/publish/model/publication.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/studyset/data/studyset_payload.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/studyset/model/studyset_content.dart';
|
||||
import 'package:halkyon_ui/halkyon_ui.dart';
|
||||
import 'package:lucide_icons_flutter/lucide_icons.dart';
|
||||
import 'package:material_ui/material_ui.dart';
|
||||
|
||||
import '../../support/fake_card_gateway.dart';
|
||||
import '../../support/fake_profile_gateway.dart';
|
||||
import '../../support/fake_publish_gateway.dart';
|
||||
import '../../support/fake_studyset_gateway.dart';
|
||||
import '../../support/pump_app.dart';
|
||||
import '../../support/test_dependencies.dart';
|
||||
|
||||
const _profile = PublicProfile(
|
||||
handle: 'raisondetredev',
|
||||
address: 'raisondetredev@halkyon.eu',
|
||||
);
|
||||
|
||||
void main() {
|
||||
late FakeStudysetGateway studysets;
|
||||
late FakeCardGateway cards;
|
||||
late FakePublishGateway publishing;
|
||||
|
||||
setUp(() {
|
||||
studysets = FakeStudysetGateway();
|
||||
cards = FakeCardGateway();
|
||||
publishing = FakePublishGateway();
|
||||
});
|
||||
|
||||
/// A studyset on this device before the app starts.
|
||||
void stored(
|
||||
String id,
|
||||
String title, {
|
||||
String? license,
|
||||
String? parent,
|
||||
String description = '',
|
||||
}) => studysets.receiveRemoteChange(
|
||||
id,
|
||||
encodeStudysetPayload(
|
||||
StudysetContent(
|
||||
title: title,
|
||||
description: description.isEmpty
|
||||
? RichContent.empty()
|
||||
: RichContent.plain(description),
|
||||
license: license,
|
||||
),
|
||||
),
|
||||
parentId: parent,
|
||||
);
|
||||
|
||||
/// Opens the screen of the studyset [id] as a person with an identity
|
||||
/// ([identified]), whose profile is [profile], in a window [width] wide.
|
||||
Future<void> showStudyset(
|
||||
WidgetTester tester,
|
||||
String id, {
|
||||
PublicProfile? profile = _profile,
|
||||
bool identified = true,
|
||||
double width = 1200,
|
||||
}) async {
|
||||
tester.view.physicalSize = Size(width, 1000);
|
||||
tester.view.devicePixelRatio = 1;
|
||||
addTearDown(tester.view.reset);
|
||||
await pumpApp(
|
||||
tester,
|
||||
testDependencies(
|
||||
authGateway: FakeAuthGateway(
|
||||
status: SignedIn(
|
||||
authUserId: 'github-person',
|
||||
isIdentified: identified,
|
||||
),
|
||||
),
|
||||
studysetGateway: studysets,
|
||||
cardGateway: cards,
|
||||
profileGateway: FakeProfileGateway(profile: profile),
|
||||
publishGateway: publishing,
|
||||
),
|
||||
location: RoutePaths.studyset(id),
|
||||
);
|
||||
await tester.pump(const Duration(seconds: 1));
|
||||
await tester.pumpAndSettle();
|
||||
}
|
||||
|
||||
Future<void> openSheet(WidgetTester tester) async {
|
||||
await tester.tap(find.text('Share'));
|
||||
await tester.pumpAndSettle();
|
||||
}
|
||||
|
||||
testWidgets('a private studyset is published under a name made from its '
|
||||
"title, at its author's handle", (tester) async {
|
||||
stored('japanese', 'Japanese', license: 'CC-BY-SA-4.0');
|
||||
await showStudyset(tester, 'japanese');
|
||||
expect(find.byIcon(LucideIcons.lock), findsOneWidget);
|
||||
|
||||
await openSheet(tester);
|
||||
expect(find.text('Private · only you can see it'), findsOneWidget);
|
||||
expect(
|
||||
find.text('Found at halkyon.eu/@raisondetredev/japanese'),
|
||||
findsOneWidget,
|
||||
);
|
||||
expect(find.text('Allows publishing.'), findsOneWidget);
|
||||
await tester.enterText(
|
||||
find.widgetWithText(TextField, 'What changed'),
|
||||
'The first lessons.',
|
||||
);
|
||||
await tester.tap(find.text('Publish v1'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(publishing.publishes.single, (
|
||||
studysetId: 'japanese',
|
||||
slug: 'japanese',
|
||||
notes: 'The first lessons.',
|
||||
));
|
||||
expect(find.text('Published v1.'), findsOneWidget);
|
||||
expect(find.textContaining('Published · v1'), findsOneWidget);
|
||||
expect(find.text('halkyon.eu/@raisondetredev/japanese'), findsOneWidget);
|
||||
expect(
|
||||
find.text('Fixed since you first published: others refer to it.'),
|
||||
findsOneWidget,
|
||||
);
|
||||
expect(find.text('Publish v2'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets("Explore's description starts as the studyset's own and goes "
|
||||
'with the release; the next publish starts from it', (tester) async {
|
||||
stored(
|
||||
'japanese',
|
||||
'Japanese',
|
||||
license: 'CC-BY-SA-4.0',
|
||||
description: 'From N5 on.',
|
||||
);
|
||||
await showStudyset(tester, 'japanese');
|
||||
await openSheet(tester);
|
||||
|
||||
expect(find.text('Description on Explore'), findsOneWidget);
|
||||
expect(find.text('From N5 on.'), findsWidgets);
|
||||
await tester.tap(find.text('Publish v1'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(publishing.descriptions.single?.plainText, 'From N5 on.');
|
||||
expect(find.text('Publish v2'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('a published studyset shows a globe, its releases and where '
|
||||
'it is', (tester) async {
|
||||
stored('japanese', 'Japanese', license: 'CC-BY-NC-ND-4.0');
|
||||
publishing = FakePublishGateway(
|
||||
publications: {
|
||||
'japanese': Publication(
|
||||
publicId: 'p',
|
||||
slug: 'nihongo',
|
||||
location: 'halkyon.eu/@raisondetredev/nihongo',
|
||||
releases: [
|
||||
Release(
|
||||
version: 2,
|
||||
releasedAt: DateTime.utc(2026, 10, 9),
|
||||
license: 'CC-BY-NC-ND-4.0',
|
||||
notes: 'N4 kanji',
|
||||
),
|
||||
Release(
|
||||
version: 1,
|
||||
releasedAt: DateTime.utc(2026, 10, 2),
|
||||
license: 'CC-BY-NC-ND-4.0',
|
||||
),
|
||||
],
|
||||
),
|
||||
},
|
||||
);
|
||||
await showStudyset(tester, 'japanese');
|
||||
expect(find.byIcon(LucideIcons.globe), findsOneWidget);
|
||||
|
||||
await openSheet(tester);
|
||||
|
||||
expect(find.text('Releases'), findsOneWidget);
|
||||
expect(find.text('N4 kanji'), findsOneWidget);
|
||||
expect(
|
||||
find.text(
|
||||
'Allows publishing as it is: learners keep their changes to '
|
||||
'themselves.',
|
||||
),
|
||||
findsOneWidget,
|
||||
);
|
||||
expect(
|
||||
tester.widget<TextField>(find.widgetWithText(TextField, 'Name')).readOnly,
|
||||
isTrue,
|
||||
);
|
||||
expect(find.text('Publish v3'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('a published studyset is taken off Explore, after asking', (
|
||||
tester,
|
||||
) async {
|
||||
stored('japanese', 'Japanese', license: 'CC-BY-4.0');
|
||||
publishing = FakePublishGateway(
|
||||
publications: {
|
||||
'japanese': Publication(
|
||||
publicId: 'p',
|
||||
slug: 'japanese',
|
||||
location: 'halkyon.eu/@raisondetredev/japanese',
|
||||
releases: [
|
||||
Release(
|
||||
version: 1,
|
||||
releasedAt: DateTime.utc(2026, 10, 9),
|
||||
license: 'CC-BY-4.0',
|
||||
),
|
||||
],
|
||||
),
|
||||
},
|
||||
);
|
||||
await showStudyset(tester, 'japanese');
|
||||
|
||||
await openSheet(tester);
|
||||
await tester.ensureVisible(find.text('Take off Explore'));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.text('Take off Explore'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('Take it off Explore?'), findsOneWidget);
|
||||
await tester.tap(find.text('Take off Explore').last);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text('Taken off Explore.'), findsOneWidget);
|
||||
expect(find.text('Withdrawn · copies get no updates'), findsOneWidget);
|
||||
expect(find.text('Take off Explore'), findsNothing);
|
||||
expect(find.text('Publish v2'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('without a public profile it asks for one first, and leads '
|
||||
'there', (tester) async {
|
||||
stored('japanese', 'Japanese', license: 'CC0-1.0');
|
||||
await showStudyset(tester, 'japanese', profile: null);
|
||||
|
||||
await openSheet(tester);
|
||||
expect(
|
||||
find.text('A release names you by your public profile. Make one first.'),
|
||||
findsOneWidget,
|
||||
);
|
||||
expect(find.textContaining('Publish v'), findsNothing);
|
||||
await tester.tap(find.text('Make a public profile'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text('Public profile'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets("a licence that doesn't allow passing it on is said, and the "
|
||||
'studysets inside the instance refuses are named', (tester) async {
|
||||
stored('japanese', 'Japanese', license: 'CC-BY-4.0');
|
||||
stored('kanji', 'Kanji', parent: 'japanese');
|
||||
publishing.refusals.add(
|
||||
PublishRefused(PublishProblem.licenseNotOpen, items: ['kanji']),
|
||||
);
|
||||
await showStudyset(tester, 'japanese');
|
||||
|
||||
await openSheet(tester);
|
||||
await tester.tap(find.text('Publish v1'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(
|
||||
// Tests speak American English.
|
||||
find.text("These studysets' licenses don't allow passing them on: Kanji"),
|
||||
findsOneWidget,
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('a copy keeps the licence its original asks for', (
|
||||
tester,
|
||||
) async {
|
||||
stored('japanese', 'Japanese', license: 'CC-BY-4.0');
|
||||
publishing.refusals.add(
|
||||
PublishRefused(PublishProblem.licenseMustStay, license: 'CC-BY-SA-4.0'),
|
||||
);
|
||||
await showStudyset(tester, 'japanese');
|
||||
|
||||
await openSheet(tester);
|
||||
await tester.tap(find.text('Publish v1'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(
|
||||
find.text(
|
||||
'Its original asks adaptations to stay under CC-BY-SA-4.0: choose '
|
||||
'it under Edit, for every studyset in it.',
|
||||
),
|
||||
findsOneWidget,
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('a studyset without a licence says so', (tester) async {
|
||||
stored('japanese', 'Japanese');
|
||||
await showStudyset(tester, 'japanese');
|
||||
|
||||
await openSheet(tester);
|
||||
|
||||
expect(find.text('None'), findsOneWidget);
|
||||
expect(
|
||||
find.text("Doesn't allow passing it on. Choose another under Edit."),
|
||||
findsOneWidget,
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('a card changed differently on two devices is named, and '
|
||||
'opens', (tester) async {
|
||||
stored('japanese', 'Japanese', license: 'CC-BY-4.0');
|
||||
final repository = CardRepository(cards);
|
||||
final id = await repository.create(
|
||||
'japanese',
|
||||
CardContent(
|
||||
front: RichContent.plain('猫'),
|
||||
back: RichContent.plain('cat'),
|
||||
),
|
||||
);
|
||||
final created = (await repository.cardsOf('japanese')).single;
|
||||
await repository.save(
|
||||
created,
|
||||
CardContent(
|
||||
front: RichContent.plain('猫'),
|
||||
back: RichContent.plain('a cat'),
|
||||
),
|
||||
);
|
||||
cards.receiveRemoteRevision(
|
||||
id: 'rev-elsewhere',
|
||||
cardId: id,
|
||||
parents: created.heads,
|
||||
payload: encodeCardPayload(
|
||||
CardContent(
|
||||
front: RichContent.plain('猫'),
|
||||
back: RichContent.plain('the cat'),
|
||||
),
|
||||
),
|
||||
);
|
||||
publishing.refusals.add(
|
||||
PublishRefused(PublishProblem.conflicts, items: [id]),
|
||||
);
|
||||
await showStudyset(tester, 'japanese');
|
||||
|
||||
await openSheet(tester);
|
||||
await tester.tap(find.text('Publish v1'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(
|
||||
find.text(
|
||||
'These were changed differently on two devices. Settle them first:',
|
||||
),
|
||||
findsOneWidget,
|
||||
);
|
||||
final unsettled = find.descendant(
|
||||
of: find.byType(SheetScaffold),
|
||||
matching: find.text('猫'),
|
||||
);
|
||||
await tester.ensureVisible(unsettled);
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(unsettled);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.byType(SheetScaffold), findsNothing);
|
||||
expect(publishing.publishes, hasLength(1));
|
||||
});
|
||||
|
||||
testWidgets('on a phone it is an icon that says the studyset is private', (
|
||||
tester,
|
||||
) async {
|
||||
stored('japanese', 'Japanese', license: 'CC-BY-4.0');
|
||||
await showStudyset(tester, 'japanese', width: 400);
|
||||
|
||||
expect(find.byTooltip('Share: private'), findsOneWidget);
|
||||
await tester.tap(find.byTooltip('Share: private'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('Private · only you can see it'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('an anonymous person is told how to publish, and the instance '
|
||||
"isn't asked", (tester) async {
|
||||
stored('japanese', 'Japanese', license: 'CC-BY-4.0');
|
||||
publishing.fail = Exception('must not be asked');
|
||||
await showStudyset(tester, 'japanese', identified: false);
|
||||
|
||||
await openSheet(tester);
|
||||
|
||||
expect(
|
||||
find.text('Sign in to have a public profile and publish.'),
|
||||
findsOneWidget,
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('without the instance, it says so and tries again', (
|
||||
tester,
|
||||
) async {
|
||||
stored('japanese', 'Japanese', license: 'CC-BY-4.0');
|
||||
publishing.fail = Exception('offline');
|
||||
await showStudyset(tester, 'japanese');
|
||||
|
||||
await openSheet(tester);
|
||||
expect(find.text("Publishing can't be reached right now."), findsOneWidget);
|
||||
publishing.fail = null;
|
||||
await tester.tap(find.text('Retry'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text('Publish v1'), findsOneWidget);
|
||||
});
|
||||
}
|
||||
Loading…
Reference in a new issue