Explore lists its studysets in a grid as the library does: each card in the studyset's colour along the top, its title under its secondary title, the start of its description and what it holds, then, quieter, its author's handle, the release and the licence, and a compact Add. A tap opens the summary, which shows the colour too. The Publish sheet has a Description on Explore, starting from the latest release's, else the studyset's own, and sent with the release: a studyset may introduce itself to strangers while its own description stays empty in the library. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
406 lines
13 KiB
Dart
406 lines
13 KiB
Dart
import 'package:flutter_test/flutter_test.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/core/widget/adaptive_sheet.dart';
|
|
import 'package:halkyon_learn_flutter/src/feature/auth/model/auth_status.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:lucide_icons_flutter/lucide_icons.dart';
|
|
import 'package:material_ui/material_ui.dart';
|
|
|
|
import '../../support/fake_auth_gateway.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 with GitHub 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);
|
|
});
|
|
}
|