Each card's foot now says who made the studyset: "By you", or for a copy of a published studyset its author's handle. Beside the star, a lock or a globe says whether it is out on Explore, and opens the Publish sheet. The instance says which of the person's studysets are published and not withdrawn (`PublishEndpoint.published`, additive: older apps don't call it), and the app remembers that in the person's folder, so the library shows it offline too; publishing, withdrawing and looking one up keep it current. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MMm7usAo6q9ZZRoPkWjoxi Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
378 lines
12 KiB
Dart
378 lines
12 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}) =>
|
|
studysets.receiveRemoteChange(
|
|
id,
|
|
encodeStudysetPayload(
|
|
StudysetContent(
|
|
title: title,
|
|
description: RichContent.empty(),
|
|
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('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,
|
|
);
|
|
await tester.tap(
|
|
find.descendant(
|
|
of: find.byType(SheetScaffold),
|
|
matching: find.text('猫'),
|
|
),
|
|
);
|
|
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);
|
|
});
|
|
}
|