A second feature needs it: discussions name their writers as reports and suggestions do, by what their public profile shows. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
308 lines
10 KiB
Dart
308 lines
10 KiB
Dart
import 'package:flutter_test/flutter_test.dart';
|
|
import 'package:halkyon_learn_flutter/src/core/content/published_ref.dart';
|
|
import 'package:halkyon_learn_flutter/src/core/content/rich_content.dart';
|
|
import 'package:halkyon_learn_flutter/src/core/profile/public_person.dart';
|
|
import 'package:halkyon_learn_flutter/src/core/router/route_paths.dart';
|
|
import 'package:halkyon_learn_flutter/src/feature/auth/model/auth_status.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/card/widget/card_tile.dart';
|
|
import 'package:halkyon_learn_flutter/src/feature/feedback/model/feedback.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:material_ui/material_ui.dart' hide Card;
|
|
|
|
import '../../support/fake_auth_gateway.dart';
|
|
import '../../support/fake_card_gateway.dart';
|
|
import '../../support/fake_feedback_gateways.dart';
|
|
import '../../support/fake_studyset_gateway.dart';
|
|
import '../../support/pump_app.dart';
|
|
import '../../support/test_dependencies.dart';
|
|
|
|
const origin = PublishedRef(publication: 'pub-1', item: 'cat', version: 2);
|
|
|
|
CardContent card(String front, String back, {PublishedRef? published}) =>
|
|
CardContent(
|
|
front: RichContent.plain(front),
|
|
back: RichContent.plain(back),
|
|
published: published,
|
|
);
|
|
|
|
/// A suggestion of Bob's on Alice's card [item], proposing its back be
|
|
/// [back].
|
|
FeedbackThread suggestion(String item, String back, {bool unseen = false}) =>
|
|
FeedbackThread(
|
|
threadId: 'thread-1',
|
|
publicId: 'pub-1',
|
|
studysetTitle: 'Japanese',
|
|
location: 'halkyon.eu/@alice/japanese',
|
|
kind: FeedbackKind.suggestion,
|
|
writer: const PublicPerson(handle: 'bob', displayName: 'Bob'),
|
|
target: FeedbackTarget(
|
|
kind: FeedbackTargetKind.card,
|
|
item: item,
|
|
version: 2,
|
|
),
|
|
createdAt: DateTime.utc(2026, 10, 9),
|
|
body: 'Kinder.',
|
|
proposed: {
|
|
'schemaVersion': 9,
|
|
'type': 'frontBack',
|
|
'front': RichContent.plain('猫').toJson(),
|
|
'back': RichContent.plain(back).toJson(),
|
|
},
|
|
forMe: true,
|
|
unseen: unseen,
|
|
);
|
|
|
|
void main() {
|
|
late FakeStudysetGateway studysets;
|
|
late FakeCardGateway cardGateway;
|
|
late CardRepository cards;
|
|
late FakeFeedbackWriteGateway written;
|
|
late FakeFeedbackServerGateway server;
|
|
|
|
setUp(() {
|
|
studysets = FakeStudysetGateway();
|
|
cardGateway = FakeCardGateway();
|
|
cards = CardRepository(cardGateway);
|
|
written = FakeFeedbackWriteGateway();
|
|
server = FakeFeedbackServerGateway();
|
|
});
|
|
tearDown(() => cards.dispose());
|
|
|
|
/// A copy of Alice's Japanese, under [license], on this device.
|
|
void copy({String license = 'CC-BY-SA-4.0'}) => studysets.receiveRemoteChange(
|
|
'japanese',
|
|
encodeStudysetPayload(
|
|
StudysetContent(
|
|
title: 'Japanese',
|
|
description: RichContent.empty(),
|
|
license: license,
|
|
published: const PublishedRef(
|
|
publication: 'pub-1',
|
|
item: 'root',
|
|
version: 2,
|
|
),
|
|
publishedAt: 'halkyon.eu/@alice/japanese',
|
|
),
|
|
),
|
|
);
|
|
|
|
Future<void> show(
|
|
WidgetTester tester,
|
|
String location, {
|
|
bool identified = true,
|
|
}) async {
|
|
tester.view.physicalSize = const Size(1200, 1600);
|
|
tester.view.devicePixelRatio = 1;
|
|
addTearDown(tester.view.reset);
|
|
await pumpApp(
|
|
tester,
|
|
testDependencies(
|
|
authGateway: FakeAuthGateway(
|
|
status: SignedIn(
|
|
authUserId: 'github-person',
|
|
isIdentified: identified,
|
|
),
|
|
),
|
|
studysetGateway: studysets,
|
|
cardGateway: cardGateway,
|
|
feedbackWriteGateway: written,
|
|
feedbackServerGateway: server,
|
|
),
|
|
location: location,
|
|
);
|
|
await tester.pump(const Duration(seconds: 1));
|
|
await tester.pumpAndSettle();
|
|
}
|
|
|
|
Future<void> chooseCardOption(WidgetTester tester, String item) async {
|
|
await tester.tap(
|
|
find.descendant(
|
|
of: find.byType(CardTile),
|
|
matching: find.byTooltip('Options'),
|
|
),
|
|
);
|
|
await tester.pumpAndSettle();
|
|
await tester.tap(find.text(item));
|
|
// A menu item acts once the menu has closed, a frame later.
|
|
await tester.pump();
|
|
await tester.pump();
|
|
await tester.pumpAndSettle();
|
|
}
|
|
|
|
testWidgets("a copy's card is reported to its author too, when asked", (
|
|
tester,
|
|
) async {
|
|
copy();
|
|
await cards.create('japanese', card('猫', 'cat', published: origin));
|
|
await show(tester, RoutePaths.studyset('japanese'));
|
|
|
|
await chooseCardOption(tester, 'Report an error');
|
|
await tester.tap(find.text('Send to the author too'));
|
|
await tester.pumpAndSettle();
|
|
await tester.tap(find.widgetWithText(FilledButton, 'Report'));
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(find.text('Sent to the author.'), findsOneWidget);
|
|
final thread = written.threads.single;
|
|
expect(thread.kind, FeedbackKind.report);
|
|
expect(thread.json['target'], {
|
|
'kind': 'card',
|
|
'item': 'cat',
|
|
'version': 2,
|
|
});
|
|
});
|
|
|
|
testWidgets("a card of the person's own is reported to them alone", (
|
|
tester,
|
|
) async {
|
|
copy();
|
|
await cards.create('japanese', card('猫', 'cat'));
|
|
await show(tester, RoutePaths.studyset('japanese'));
|
|
|
|
await chooseCardOption(tester, 'Report an error');
|
|
|
|
expect(find.text('Send to the author too'), findsNothing);
|
|
});
|
|
|
|
testWidgets("a copy's menu reports the studyset to its author and shows "
|
|
'what is reported and suggested on it', (tester) async {
|
|
copy();
|
|
server.gathered = [suggestion('cat', 'a cat')];
|
|
await show(tester, RoutePaths.studyset('japanese'));
|
|
Future<void> choose(String option) async {
|
|
// The studyset's options, in the app bar: the card list has none.
|
|
await tester.tap(find.byTooltip('Options').first);
|
|
await tester.pumpAndSettle();
|
|
await tester.tap(find.text(option));
|
|
await tester.pumpAndSettle();
|
|
}
|
|
|
|
await choose('Report to the author');
|
|
await tester.tap(find.text('Content'));
|
|
await tester.tap(find.widgetWithText(FilledButton, 'Report'));
|
|
await tester.pumpAndSettle();
|
|
expect(written.threads.single.json['target'], {
|
|
'kind': 'studyset',
|
|
'item': 'root',
|
|
'version': 2,
|
|
});
|
|
|
|
await choose('Reports and suggestions');
|
|
expect(find.textContaining('Kinder.'), findsOneWidget);
|
|
});
|
|
|
|
testWidgets("a copy's card is suggested to its author from its editor", (
|
|
tester,
|
|
) async {
|
|
copy();
|
|
final id = await cards.create(
|
|
'japanese',
|
|
card('猫', 'a cat', published: origin),
|
|
);
|
|
await show(tester, RoutePaths.card('japanese', id));
|
|
|
|
await tester.tap(find.byTooltip('More actions'));
|
|
await tester.pumpAndSettle();
|
|
await tester.tap(find.text('Suggest to the author'));
|
|
await tester.pump();
|
|
await tester.pump();
|
|
await tester.pumpAndSettle();
|
|
await tester.enterText(find.byType(TextField).last, 'Kinder.');
|
|
await tester.tap(find.text('Send'));
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(find.text('Suggestion sent.'), findsOneWidget);
|
|
final thread = written.threads.single;
|
|
expect(thread.kind, FeedbackKind.suggestion);
|
|
expect(thread.json['body'], 'Kinder.');
|
|
expect(
|
|
(thread.json['proposed']! as Map<String, Object?>).containsKey(
|
|
'published',
|
|
),
|
|
isFalse,
|
|
);
|
|
});
|
|
|
|
testWidgets('no suggestion under "no derivatives"', (tester) async {
|
|
copy(license: 'CC-BY-ND-4.0');
|
|
final id = await cards.create(
|
|
'japanese',
|
|
card('猫', 'a cat', published: origin),
|
|
);
|
|
await show(tester, RoutePaths.card('japanese', id));
|
|
|
|
await tester.tap(find.byTooltip('More actions'));
|
|
await tester.pumpAndSettle();
|
|
await tester.tap(find.text('Suggest to the author'));
|
|
await tester.pump();
|
|
await tester.pump();
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(
|
|
find.text('Its license forbids adaptations: no suggestions.'),
|
|
findsOneWidget,
|
|
);
|
|
expect(written.threads, isEmpty);
|
|
});
|
|
|
|
testWidgets("the author's inbox shows a suggestion beside their card, and "
|
|
'accepting it changes the card', (tester) async {
|
|
final id = await cards.create('japanese', card('猫', 'cat'));
|
|
server.gathered = [suggestion(id, 'a kind cat', unseen: true)];
|
|
await show(tester, RoutePaths.profile);
|
|
|
|
// The Profile tab says something is new.
|
|
expect(find.text('1'), findsWidgets);
|
|
expect(find.text('Inbox'), findsOneWidget);
|
|
await tester.tap(find.textContaining('Kinder.'));
|
|
await tester.pumpAndSettle();
|
|
expect(server.seen, ['thread-1']);
|
|
expect(find.text('Proposed'), findsOneWidget);
|
|
expect(find.text('Yours now'), findsOneWidget);
|
|
await tester.tap(find.text('Accept'));
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(
|
|
find.text(
|
|
'Taken into your studyset. The next release credits its writer.',
|
|
),
|
|
findsOneWidget,
|
|
);
|
|
expect((await cards.byId(id))!.content.back.plainText, 'a kind cat');
|
|
expect(written.decisions.single.decision, FeedbackDecision.accepted);
|
|
});
|
|
|
|
testWidgets('the author replies, closes, and blocks after asking', (
|
|
tester,
|
|
) async {
|
|
server.gathered = [suggestion('gone', 'a cat')];
|
|
await show(tester, RoutePaths.profile);
|
|
await tester.tap(find.textContaining('Kinder.'));
|
|
await tester.pumpAndSettle();
|
|
|
|
await tester.enterText(find.byType(TextField).last, 'Thanks!');
|
|
await tester.tap(find.byTooltip('Send'));
|
|
await tester.pumpAndSettle();
|
|
expect(written.messages.single.json['body'], 'Thanks!');
|
|
|
|
await tester.tap(find.text('Close'));
|
|
await tester.pumpAndSettle();
|
|
expect(written.decisions.single.decision, FeedbackDecision.closed);
|
|
|
|
await tester.tap(find.text('Block Bob'));
|
|
await tester.pumpAndSettle();
|
|
expect(find.text('Block Bob?'), findsOneWidget);
|
|
await tester.tap(find.text('Block Bob').last);
|
|
await tester.pumpAndSettle();
|
|
expect(server.blocked, ['bob']);
|
|
});
|
|
|
|
testWidgets('a person without an identity has no inbox', (tester) async {
|
|
await show(tester, RoutePaths.profile, identified: false);
|
|
|
|
expect(find.text('Inbox'), findsNothing);
|
|
});
|
|
}
|