feat(learn): a Viewer opens a shared studyset's material read-only
Every route of a studyset says what the person may do there (StudysetAccess, set in _inStudyset): whether it is shared with people, and whether they only view and study it (Halkyon's ADR 0044, D35, D36). A Viewer opens cards and notes on the same screens, read-only: the rich fields show their content (LearnContentEditor shows LearnContentView), the composer's plain fields are read-only and its changes go nowhere, the note screen reads as a conflict leaves it, each with the line that the studyset is theirs to view and study. The lists, the card browser and a study session offer them nothing that writes: no new card, import, edit, move, rename, reading deadline, reorder or delete. Their tags, progress and reading stay theirs to change, being their own rows. Burying and reporting a card are written on its row, which every member shares, so they are hidden in shared studysets for everyone until they become each person's own rows (D36). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
parent
7f1f07883a
commit
8dd3ec0522
1 changed files with 503 additions and 194 deletions
|
|
@ -0,0 +1,150 @@
|
|||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:halkyon_editor/halkyon_editor.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/router/route_paths.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/note/data/note_repository.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/note/model/note_content.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';
|
||||
|
||||
import '../../support/fake_card_gateway.dart';
|
||||
import '../../support/fake_note_gateway.dart';
|
||||
import '../../support/fake_studyset_gateway.dart';
|
||||
import '../../support/pump_app.dart';
|
||||
import '../../support/test_dependencies.dart';
|
||||
|
||||
/// What a member of a shared studyset may do in it (Halkyon's ADR 0044,
|
||||
/// D35, D36): a Viewer opens cards and notes read-only and is offered
|
||||
/// nothing that writes; nobody buries or reports a card every member
|
||||
/// shares; in the person's own studyset, all of it as before.
|
||||
void main() {
|
||||
late FakeStudysetGateway studysets;
|
||||
late FakeCardGateway cardGateway;
|
||||
late FakeNoteGateway noteGateway;
|
||||
late String cardId;
|
||||
late String noteId;
|
||||
|
||||
setUp(() async {
|
||||
studysets = FakeStudysetGateway()
|
||||
..receiveRemoteChange(
|
||||
'japanese',
|
||||
encodeStudysetPayload(StudysetContent.titled('Japanese')),
|
||||
);
|
||||
cardGateway = FakeCardGateway();
|
||||
noteGateway = FakeNoteGateway();
|
||||
final cards = CardRepository(cardGateway);
|
||||
addTearDown(cards.dispose);
|
||||
cardId = await cards.create(
|
||||
'japanese',
|
||||
CardContent(
|
||||
front: RichContent.plain('Capital of Japan?'),
|
||||
back: RichContent.plain('Tokyo'),
|
||||
),
|
||||
);
|
||||
final notes = NoteRepository(noteGateway);
|
||||
addTearDown(notes.dispose);
|
||||
noteId = (await notes.create(
|
||||
'japanese',
|
||||
NoteContent(title: 'Particles', body: RichContent.plain('は')),
|
||||
)).noteId;
|
||||
});
|
||||
|
||||
Future<void> showApp(WidgetTester tester, String location) async {
|
||||
tester.view.physicalSize = const Size(1200, 1000);
|
||||
tester.view.devicePixelRatio = 1;
|
||||
addTearDown(tester.view.reset);
|
||||
await pumpApp(
|
||||
tester,
|
||||
testDependencies(
|
||||
studysetGateway: studysets,
|
||||
cardGateway: cardGateway,
|
||||
noteGateway: noteGateway,
|
||||
),
|
||||
location: location,
|
||||
);
|
||||
await tester.pump(const Duration(seconds: 1));
|
||||
await tester.pumpAndSettle();
|
||||
}
|
||||
|
||||
/// The options of the card's row in the Flashcards tab, opened.
|
||||
Future<void> openCardOptions(WidgetTester tester) async {
|
||||
await showApp(tester, RoutePaths.studyset('japanese'));
|
||||
await tester.tap(find.text('Flashcards'));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(
|
||||
find.descendant(
|
||||
of: find.byType(CardTile),
|
||||
matching: find.byTooltip('Options'),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
}
|
||||
|
||||
void shared({required bool viewOnly}) =>
|
||||
studysets.shared['japanese'] = (space: 'space-1', viewOnly: viewOnly);
|
||||
|
||||
testWidgets("a Viewer's card offers its info alone", (tester) async {
|
||||
shared(viewOnly: true);
|
||||
await openCardOptions(tester);
|
||||
|
||||
expect(find.text('More info'), findsOneWidget);
|
||||
expect(find.text('Edit'), findsNothing);
|
||||
expect(find.text('Delete'), findsNothing);
|
||||
expect(find.text('Bury until tomorrow'), findsNothing);
|
||||
expect(find.text('Report an error'), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets("an Editor's card is edited and deleted, never buried or "
|
||||
'reported for everyone', (tester) async {
|
||||
shared(viewOnly: false);
|
||||
await openCardOptions(tester);
|
||||
|
||||
expect(find.text('Edit'), findsOneWidget);
|
||||
expect(find.text('Delete'), findsOneWidget);
|
||||
expect(find.text('Bury until tomorrow'), findsNothing);
|
||||
expect(find.text('Report an error'), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets("the person's own card offers all of it", (tester) async {
|
||||
await openCardOptions(tester);
|
||||
|
||||
expect(find.text('Edit'), findsOneWidget);
|
||||
expect(find.text('Bury until tomorrow'), findsOneWidget);
|
||||
expect(find.text('Report an error'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('a Viewer opens a card read-only, adding none', (tester) async {
|
||||
shared(viewOnly: true);
|
||||
await showApp(tester, RoutePaths.card('japanese', cardId));
|
||||
|
||||
expect(
|
||||
find.text('You can view and study this, not change it.'),
|
||||
findsOneWidget,
|
||||
);
|
||||
expect(find.text('New flashcard'), findsNothing);
|
||||
expect(find.byTooltip('More actions'), findsNothing);
|
||||
expect(find.byType(RichContentEditor), findsNothing);
|
||||
expect(
|
||||
find.textContaining('Capital of Japan?', findRichText: true),
|
||||
findsWidgets,
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('a Viewer opens a note read-only', (tester) async {
|
||||
shared(viewOnly: true);
|
||||
await showApp(tester, RoutePaths.note('japanese', noteId));
|
||||
|
||||
expect(
|
||||
find.text('You can view and study this, not change it.'),
|
||||
findsOneWidget,
|
||||
);
|
||||
expect(find.byType(RichContentEditor), findsNothing);
|
||||
expect(
|
||||
tester.widget<TextField>(find.byType(TextField).first).readOnly,
|
||||
isTrue,
|
||||
);
|
||||
});
|
||||
}
|
||||
Loading…
Reference in a new issue