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,43 @@
|
|||
import 'dart:convert';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:archive/archive.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/export/data/export_archive.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/export/data/studyset_markdown.dart';
|
||||
|
||||
final markdown = Uint8List.fromList(utf8.encode('# Spanish\n' * 100));
|
||||
final pdf = Uint8List.fromList(utf8.encode('%PDF-1.7 ' * 100));
|
||||
|
||||
final files = <ExportFile>[
|
||||
(path: 'Spanish.md', bytes: markdown),
|
||||
(path: 'files/pdfs/Grammar.PDF', bytes: pdf),
|
||||
];
|
||||
|
||||
void main() {
|
||||
test('packs the files in their order: what is compressed already as it '
|
||||
'is, the rest deflated', () {
|
||||
final zip = ZipDecoder().decodeBytes(zipExport(files));
|
||||
|
||||
expect(
|
||||
[for (final file in zip.files) file.name],
|
||||
[
|
||||
'Spanish.md',
|
||||
'files/pdfs/Grammar.PDF',
|
||||
],
|
||||
);
|
||||
expect(zip.files[0].content, markdown);
|
||||
expect(zip.files[0].compression, CompressionType.deflate);
|
||||
expect(zip.files[1].content, pdf);
|
||||
expect(zip.files[1].compression, CompressionType.none);
|
||||
});
|
||||
|
||||
test('packs the same on an isolate of its own', () async {
|
||||
final zip = ZipDecoder().decodeBytes(
|
||||
await IsolateExportArchiver().zip(files),
|
||||
);
|
||||
|
||||
expect(zip.files[0].content, markdown);
|
||||
expect(zip.files[1].content, pdf);
|
||||
});
|
||||
}
|
||||
|
|
@ -0,0 +1,350 @@
|
|||
import 'dart:convert';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/asset/data/asset_repository.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/citation/model/citation.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/citation/model/source.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/content/rich_content.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/export/data/studyset_markdown.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/learning_path/data/learning_path_repository.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/learning_path/model/exercise.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/learning_path/model/learning_path.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/learning_path/model/step_content.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/pdf_source/data/pdf_source_repository.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/studyset/data/studyset_payload.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/studyset/data/studyset_repository.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/studyset/model/license_link.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/studyset/model/studyset_content.dart';
|
||||
|
||||
import '../../../support/fake_asset_gateway.dart';
|
||||
import '../../../support/fake_asset_server_gateway.dart';
|
||||
import '../../../support/fake_card_gateway.dart';
|
||||
import '../../../support/fake_learning_path_gateway.dart';
|
||||
import '../../../support/fake_note_gateway.dart';
|
||||
import '../../../support/fake_pdf_source_gateway.dart';
|
||||
import '../../../support/fake_studyset_gateway.dart';
|
||||
import '../../../support/memory_asset_file_cache.dart';
|
||||
|
||||
void main() {
|
||||
late FakeStudysetGateway studysetGateway;
|
||||
late StudysetRepository studysets;
|
||||
late CardRepository cards;
|
||||
late NoteRepository notes;
|
||||
late LearningPathRepository paths;
|
||||
late AssetRepository assets;
|
||||
late PdfSourceRepository pdfs;
|
||||
late StudysetMarkdownExporter exporter;
|
||||
|
||||
setUp(() {
|
||||
studysetGateway = FakeStudysetGateway();
|
||||
studysets = StudysetRepository(studysetGateway);
|
||||
cards = CardRepository(FakeCardGateway());
|
||||
notes = NoteRepository(FakeNoteGateway());
|
||||
paths = LearningPathRepository(FakeLearningPathGateway());
|
||||
assets = AssetRepository(
|
||||
FakeAssetGateway(),
|
||||
FakeAssetServerGateway(),
|
||||
MemoryAssetFileCache(),
|
||||
);
|
||||
pdfs = PdfSourceRepository(FakePdfSourceGateway(), assets);
|
||||
exporter = StudysetMarkdownExporter(
|
||||
studysets,
|
||||
cards,
|
||||
notes,
|
||||
paths,
|
||||
pdfs,
|
||||
assets.fileOf,
|
||||
);
|
||||
});
|
||||
tearDown(() async {
|
||||
await studysets.dispose();
|
||||
await cards.dispose();
|
||||
await notes.dispose();
|
||||
await paths.dispose();
|
||||
await pdfs.dispose();
|
||||
await assets.dispose();
|
||||
});
|
||||
|
||||
final texts = (
|
||||
richText: (
|
||||
sound: 'Sound',
|
||||
video: 'YouTube video',
|
||||
spoiler: 'Spoiler',
|
||||
calloutName: (Object? kind) => 'Note',
|
||||
unavailable: 'not available on this device',
|
||||
),
|
||||
flashcards: 'Flashcards',
|
||||
notes: 'Notes',
|
||||
paths: 'Learning paths',
|
||||
files: 'Files',
|
||||
bibliography: 'Bibliography',
|
||||
question: 'Q',
|
||||
answer: 'A',
|
||||
hint: 'Hint',
|
||||
explanation: 'Explanation',
|
||||
untitledNote: 'Untitled note',
|
||||
untitledStep: 'Untitled step',
|
||||
stepKind: (StepKind kind) => switch (kind) {
|
||||
StepKind.info => 'Info',
|
||||
StepKind.singleChoice => 'Single choice',
|
||||
_ => kind.value,
|
||||
},
|
||||
ifRight: 'If right',
|
||||
ifWrong: 'If wrong',
|
||||
sources: 'Sources',
|
||||
);
|
||||
|
||||
Future<StudysetMarkdown> export(String id) =>
|
||||
exporter.export(id, texts: texts, style: CitationStyle.apa);
|
||||
|
||||
RichContent blocks(List<Map<String, Object?>> children) => RichContent({
|
||||
'document': {'type': 'page', 'children': children},
|
||||
});
|
||||
|
||||
test('writes the studyset, then its flashcards, notes, paths and '
|
||||
'bibliography, then its sub-studysets a level deeper', () async {
|
||||
const source = Source(authors: 'Ana López', year: '2024', title: 'Hola');
|
||||
final spanish = await studysets.create(
|
||||
StudysetContent(
|
||||
title: 'Spanish vocab',
|
||||
secondaryTitle: 'A1',
|
||||
description: RichContent.plain('Words for a first trip.'),
|
||||
license: 'CC-BY-4.0',
|
||||
licenseHolder: 'Ana',
|
||||
licenseYear: 2026,
|
||||
bibliography: const [source],
|
||||
),
|
||||
);
|
||||
await cards.create(
|
||||
spanish,
|
||||
CardContent(
|
||||
front: RichContent.plain('hola'),
|
||||
back: RichContent.plain('hello'),
|
||||
hints: [RichContent.plain('starts with h')],
|
||||
),
|
||||
);
|
||||
await cards.create(
|
||||
spanish,
|
||||
CardContent(
|
||||
type: CardType.multipleChoice,
|
||||
front: RichContent.plain('Which means goodbye?'),
|
||||
back: RichContent.empty(),
|
||||
answers: [
|
||||
CardAnswer(content: RichContent.plain('adiós'), correct: true),
|
||||
CardAnswer(content: RichContent.plain('hola')),
|
||||
],
|
||||
),
|
||||
);
|
||||
await notes.create(
|
||||
spanish,
|
||||
NoteContent(
|
||||
title: 'Pronunciation',
|
||||
body: RichContent.plain('The h is silent.'),
|
||||
),
|
||||
);
|
||||
final basics = await paths.createPath(
|
||||
spanish,
|
||||
LearningPathContent.titled('Basics'),
|
||||
);
|
||||
await paths.createStep(
|
||||
basics,
|
||||
StepKind.info,
|
||||
StepContent.empty(StepKind.info)
|
||||
.withField(StepField.title, 'Saying hello')
|
||||
.withField(StepField.body, RichContent.plain('Hola greets.')),
|
||||
);
|
||||
await paths.createStep(
|
||||
basics,
|
||||
StepKind.singleChoice,
|
||||
StepContent.empty(StepKind.singleChoice)
|
||||
.withField(StepField.title, 'Which one means goodbye?')
|
||||
.withField(
|
||||
StepField.exercise,
|
||||
const SingleChoice(options: ['hola', 'adiós'], correct: 1),
|
||||
)
|
||||
.withField(StepField.rightFeedback, RichContent.plain('¡Sí!')),
|
||||
);
|
||||
final food = await studysets.create(
|
||||
StudysetContent.titled('Food'),
|
||||
parentId: spanish,
|
||||
);
|
||||
await cards.create(
|
||||
food,
|
||||
CardContent(
|
||||
front: RichContent.plain('pan'),
|
||||
back: RichContent.plain('bread'),
|
||||
),
|
||||
);
|
||||
final citation = formatCitation(
|
||||
source,
|
||||
CitationStyle.apa,
|
||||
).map((run) => run.italic ? '*${run.text}*' : run.text).join();
|
||||
|
||||
final exported = await export(spanish);
|
||||
|
||||
expect(exported.markdown, '''
|
||||
# Spanish vocab
|
||||
|
||||
*A1*
|
||||
|
||||
Words for a first trip.
|
||||
|
||||
© 2026 Ana · [CC-BY-4.0](${licenseUrl('CC-BY-4.0')})
|
||||
|
||||
## Flashcards
|
||||
|
||||
- **Q:** hola
|
||||
- **A:** hello
|
||||
- **Hint:** starts with h
|
||||
- **Q:** Which means goodbye?
|
||||
- [x] adiós
|
||||
- [ ] hola
|
||||
|
||||
## Notes
|
||||
|
||||
### Pronunciation
|
||||
|
||||
The h is silent.
|
||||
|
||||
## Learning paths
|
||||
|
||||
### Basics
|
||||
|
||||
#### 1. Saying hello (Info)
|
||||
|
||||
Hola greets.
|
||||
|
||||
#### 2. Which one means goodbye? (Single choice)
|
||||
|
||||
- [ ] hola
|
||||
- [x] adiós
|
||||
|
||||
**If right:** ¡Sí!
|
||||
|
||||
## Bibliography
|
||||
|
||||
- $citation
|
||||
|
||||
## Food
|
||||
|
||||
### Flashcards
|
||||
|
||||
- **Q:** pan
|
||||
- **A:** bread
|
||||
''');
|
||||
expect(exported.files, isEmpty);
|
||||
expect(exported.missingFiles, 0);
|
||||
});
|
||||
|
||||
test("brings the studyset's files along: its PDFs under their names, the "
|
||||
'images in its content by their kind', () async {
|
||||
final spanish = await studysets.create(StudysetContent.titled('Spanish'));
|
||||
final png = Uint8List.fromList([137, 80, 78, 71, 1, 2, 3]);
|
||||
final cat = await assets.add(
|
||||
png,
|
||||
fileName: 'cat.png',
|
||||
mimeType: 'image/png',
|
||||
);
|
||||
await cards.create(
|
||||
spanish,
|
||||
CardContent(
|
||||
front: blocks([
|
||||
{
|
||||
'type': 'image',
|
||||
'data': {'url': assetUrl(cat)},
|
||||
},
|
||||
]),
|
||||
back: RichContent.plain('gato'),
|
||||
),
|
||||
);
|
||||
final kana = Uint8List.fromList(utf8.encode('%PDF-1.7 kana'));
|
||||
final hiragana = Uint8List.fromList(utf8.encode('%PDF-1.7 hiragana'));
|
||||
await pdfs.import(spanish, bytes: kana, fileName: 'kana.pdf');
|
||||
await pdfs.import(spanish, bytes: hiragana, fileName: 'kana.pdf');
|
||||
|
||||
final exported = await export(spanish);
|
||||
|
||||
final image = exported.files.singleWhere(
|
||||
(file) => file.path.startsWith('files/images/'),
|
||||
);
|
||||
expect(image.path, endsWith('.png'));
|
||||
expect(image.bytes, png);
|
||||
expect(exported.markdown, contains('- **Q:** '));
|
||||
expect(
|
||||
{
|
||||
for (final file in exported.files)
|
||||
if (file.path.startsWith('files/pdfs/')) file.path: file.bytes,
|
||||
},
|
||||
{'files/pdfs/kana.pdf': kana, 'files/pdfs/kana-2.pdf': hiragana},
|
||||
);
|
||||
expect(
|
||||
exported.markdown,
|
||||
contains(
|
||||
'## Files\n\n- [kana](files/pdfs/kana.pdf)\n'
|
||||
'- [kana](files/pdfs/kana-2.pdf)\n',
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
test('two studysets inside each other (moved so on two devices offline) '
|
||||
'are written once each, and the export ends', () async {
|
||||
studysetGateway
|
||||
..receiveRemoteChange(
|
||||
'a',
|
||||
encodeStudysetPayload(StudysetContent.titled('Grammar')),
|
||||
parentId: 'b',
|
||||
)
|
||||
..receiveRemoteChange(
|
||||
'b',
|
||||
encodeStudysetPayload(StudysetContent.titled('Particles')),
|
||||
parentId: 'a',
|
||||
);
|
||||
|
||||
final exported = await export('a');
|
||||
|
||||
expect('# Grammar'.allMatches(exported.markdown), hasLength(1));
|
||||
expect('## Particles'.allMatches(exported.markdown), hasLength(1));
|
||||
});
|
||||
|
||||
test("a file's link keeps working whatever its name: what would end a "
|
||||
'link is encoded, the file keeps its name', () async {
|
||||
final spanish = await studysets.create(StudysetContent.titled('Spanish'));
|
||||
final pdf = Uint8List.fromList(utf8.encode('%PDF-1.7 c#'));
|
||||
await pdfs.import(spanish, bytes: pdf, fileName: 'C# (100%).pdf');
|
||||
|
||||
final exported = await export(spanish);
|
||||
|
||||
expect(exported.files.single.path, 'files/pdfs/C#_(100%).pdf');
|
||||
expect(
|
||||
exported.markdown,
|
||||
contains('](files/pdfs/C%23_%28100%25%29.pdf)'),
|
||||
);
|
||||
});
|
||||
|
||||
test("a file this device can't have now is named, and counted", () async {
|
||||
final spanish = await studysets.create(StudysetContent.titled('Spanish'));
|
||||
await cards.create(
|
||||
spanish,
|
||||
CardContent(
|
||||
front: blocks([
|
||||
{
|
||||
'type': 'image',
|
||||
'data': {'url': assetUrl('gone')},
|
||||
},
|
||||
]),
|
||||
back: RichContent.plain('gato'),
|
||||
),
|
||||
);
|
||||
|
||||
final exported = await export(spanish);
|
||||
|
||||
expect(exported.files, isEmpty);
|
||||
expect(exported.missingFiles, 1);
|
||||
expect(exported.markdown, contains('(not available on this device)'));
|
||||
});
|
||||
}
|
||||
|
|
@ -0,0 +1,307 @@
|
|||
import 'dart:convert';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:archive/archive.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/asset/data/asset_payload.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/asset/data/asset_repository.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_repository.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/card/model/card_content.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/export/widget/export_studyset.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/study/data/review_repository.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/study/model/rating.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_asset_gateway.dart';
|
||||
import '../../../support/fake_asset_server_gateway.dart';
|
||||
import '../../../support/fake_card_gateway.dart';
|
||||
import '../../../support/fake_file_saver.dart';
|
||||
import '../../../support/fake_review_gateway.dart';
|
||||
import '../../../support/fake_studyset_gateway.dart';
|
||||
import '../../../support/memory_asset_file_cache.dart';
|
||||
import '../../../support/pump_app.dart';
|
||||
import '../../../support/test_dependencies.dart';
|
||||
|
||||
void main() {
|
||||
late FakeStudysetGateway studysets;
|
||||
late FakeCardGateway cardGateway;
|
||||
late FakeAssetGateway assetGateway;
|
||||
late MemoryAssetFileCache cache;
|
||||
late FakeFileSaver saver;
|
||||
late FakeReviewGateway reviewGateway;
|
||||
|
||||
setUp(() {
|
||||
studysets = FakeStudysetGateway()
|
||||
..receiveRemoteChange(
|
||||
'spanish',
|
||||
encodeStudysetPayload(StudysetContent.titled('Spanish vocab')),
|
||||
);
|
||||
cardGateway = FakeCardGateway();
|
||||
assetGateway = FakeAssetGateway();
|
||||
cache = MemoryAssetFileCache();
|
||||
saver = FakeFileSaver();
|
||||
reviewGateway = FakeReviewGateway();
|
||||
});
|
||||
|
||||
Future<String> card(CardContent content) {
|
||||
final cards = CardRepository(cardGateway);
|
||||
addTearDown(cards.dispose);
|
||||
return cards.create('spanish', content);
|
||||
}
|
||||
|
||||
/// Opens the studyset's Export content, and, unless [choose] is null,
|
||||
/// chooses it (the format's name, as the sheet shows it), sets
|
||||
/// [withProgress] and exports.
|
||||
Future<void> exportFromMenu(
|
||||
WidgetTester tester, {
|
||||
String? choose = 'Markdown',
|
||||
bool withProgress = true,
|
||||
}) async {
|
||||
tester.view.physicalSize = const Size(1200, 1000);
|
||||
tester.view.devicePixelRatio = 1;
|
||||
addTearDown(tester.view.reset);
|
||||
await pumpApp(
|
||||
tester,
|
||||
testDependencies(
|
||||
studysetGateway: studysets,
|
||||
cardGateway: cardGateway,
|
||||
assetGateway: assetGateway,
|
||||
assetFiles: cache,
|
||||
fileSaver: saver,
|
||||
reviewGateway: reviewGateway,
|
||||
),
|
||||
location: RoutePaths.studyset('spanish'),
|
||||
);
|
||||
await tester.pump(const Duration(seconds: 1));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(
|
||||
find.descendant(
|
||||
of: find.byType(AppBar),
|
||||
matching: find.byTooltip('Options'),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.text('Export content'));
|
||||
await tester.pumpAndSettle();
|
||||
if (choose == null) {
|
||||
return;
|
||||
}
|
||||
await tester.tap(find.text(choose));
|
||||
await tester.pumpAndSettle();
|
||||
if (!withProgress) {
|
||||
await tester.tap(find.text('With my progress'));
|
||||
await tester.pumpAndSettle();
|
||||
}
|
||||
await tester.tap(find.widgetWithText(FilledButton, 'Export'));
|
||||
await tester.pumpAndSettle();
|
||||
}
|
||||
|
||||
/// The archive saved, by its files' paths.
|
||||
Map<String, List<int>> savedArchive() => {
|
||||
for (final file in ZipDecoder().decodeBytes(saver.saved.single.bytes).files)
|
||||
file.name: file.content,
|
||||
};
|
||||
|
||||
testWidgets('Export content offers an archive first, with the progress '
|
||||
'and Markdown below; the progress is only asked about for the '
|
||||
'archive', (tester) async {
|
||||
await exportFromMenu(tester, choose: null);
|
||||
|
||||
expect(
|
||||
tester
|
||||
.widget<RadioGroup<ExportFormat>>(
|
||||
find.byType(RadioGroup<ExportFormat>),
|
||||
)
|
||||
.groupValue,
|
||||
ExportFormat.archive,
|
||||
);
|
||||
expect(
|
||||
tester
|
||||
.widget<CheckboxListTile>(
|
||||
find.widgetWithText(CheckboxListTile, 'With my progress'),
|
||||
)
|
||||
.value,
|
||||
isTrue,
|
||||
);
|
||||
await tester.tap(find.text('Markdown'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('With my progress'), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('an archive keeps the studyset with its reviews as a zip '
|
||||
'named after it, which the import takes back', (tester) async {
|
||||
final hola = await card(
|
||||
CardContent(
|
||||
front: RichContent.plain('hola'),
|
||||
back: RichContent.plain('hello'),
|
||||
),
|
||||
);
|
||||
await ReviewRepository(reviewGateway).record(
|
||||
cardId: hola,
|
||||
rating: Rating.good,
|
||||
reviewedAt: DateTime.utc(2026, 10),
|
||||
duration: const Duration(seconds: 2),
|
||||
);
|
||||
|
||||
await exportFromMenu(tester, choose: 'Archive');
|
||||
|
||||
final saved = saver.saved.single;
|
||||
expect(saved.name, 'Spanish vocab.zip');
|
||||
expect(saved.mimeType, 'application/zip');
|
||||
final files = savedArchive();
|
||||
expect(
|
||||
files.keys,
|
||||
containsAll(['archive.json', 'studyset.json', 'cards/$hola.json']),
|
||||
);
|
||||
expect(utf8.decode(files['reviews.json']!), contains('"card": "$hola"'));
|
||||
expect(find.text('Exported as an archive (.zip).'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('without the progress, an archive holds no reviews', (
|
||||
tester,
|
||||
) async {
|
||||
final hola = await card(
|
||||
CardContent(
|
||||
front: RichContent.plain('hola'),
|
||||
back: RichContent.plain('hello'),
|
||||
),
|
||||
);
|
||||
await ReviewRepository(reviewGateway).record(
|
||||
cardId: hola,
|
||||
rating: Rating.good,
|
||||
reviewedAt: DateTime.utc(2026, 10),
|
||||
duration: const Duration(seconds: 2),
|
||||
);
|
||||
|
||||
await exportFromMenu(tester, choose: 'Archive', withProgress: false);
|
||||
|
||||
expect(savedArchive().keys, isNot(contains('reviews.json')));
|
||||
});
|
||||
|
||||
testWidgets('an archive missing a file this device cannot have now is '
|
||||
'not kept, and says so', (tester) async {
|
||||
final elsewhere = await assetGateway.insert(
|
||||
sha256: 'f' * 64,
|
||||
mimeType: 'image/png',
|
||||
byteSize: 3,
|
||||
payload: encodeAssetPayload(fileName: 'cat.png'),
|
||||
);
|
||||
await card(
|
||||
CardContent(
|
||||
front: RichContent({
|
||||
'document': {
|
||||
'type': 'page',
|
||||
'children': [
|
||||
{
|
||||
'type': 'image',
|
||||
'data': {'url': assetUrl(elsewhere)},
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
back: RichContent.plain('gato'),
|
||||
),
|
||||
);
|
||||
|
||||
await exportFromMenu(tester, choose: 'Archive');
|
||||
|
||||
expect(saver.saved, isEmpty);
|
||||
expect(
|
||||
find.text(
|
||||
"1 file isn't on this device and couldn't be downloaded. Connect "
|
||||
'to the internet and try again.',
|
||||
),
|
||||
findsOneWidget,
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('as Markdown, it keeps the studyset as a Markdown file, named '
|
||||
'after it', (tester) async {
|
||||
await card(
|
||||
CardContent(
|
||||
front: RichContent.plain('hola'),
|
||||
back: RichContent.plain('hello'),
|
||||
),
|
||||
);
|
||||
|
||||
await exportFromMenu(tester);
|
||||
|
||||
final saved = saver.saved.single;
|
||||
expect(saved.name, 'Spanish vocab.md');
|
||||
expect(saved.mimeType, 'text/markdown');
|
||||
expect(
|
||||
utf8.decode(saved.bytes),
|
||||
startsWith(
|
||||
'# Spanish vocab\n\n## Flashcards\n\n'
|
||||
'- **Q:** hola\n - **A:** hello\n',
|
||||
),
|
||||
);
|
||||
expect(find.text('Exported as Markdown.'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('with files, it keeps a zip of the document and the files it '
|
||||
'links to', (tester) async {
|
||||
final assets = AssetRepository(
|
||||
assetGateway,
|
||||
FakeAssetServerGateway(),
|
||||
cache,
|
||||
);
|
||||
addTearDown(assets.dispose);
|
||||
final png = Uint8List.fromList([137, 80, 78, 71, 1, 2, 3]);
|
||||
final cat = await assets.add(
|
||||
png,
|
||||
fileName: 'cat.png',
|
||||
mimeType: 'image/png',
|
||||
);
|
||||
await card(
|
||||
CardContent(
|
||||
front: RichContent({
|
||||
'document': {
|
||||
'type': 'page',
|
||||
'children': [
|
||||
{
|
||||
'type': 'image',
|
||||
'data': {'url': assetUrl(cat)},
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
back: RichContent.plain('gato'),
|
||||
),
|
||||
);
|
||||
|
||||
await exportFromMenu(tester);
|
||||
|
||||
final saved = saver.saved.single;
|
||||
expect(saved.name, 'Spanish vocab.zip');
|
||||
final zip = ZipDecoder().decodeBytes(saved.bytes);
|
||||
final image = zip.files.singleWhere(
|
||||
(file) => file.name.startsWith('files/images/'),
|
||||
);
|
||||
expect(image.content, png);
|
||||
expect(
|
||||
utf8.decode(zip.findFile('Spanish vocab.md')!.content),
|
||||
contains(''),
|
||||
);
|
||||
expect(
|
||||
find.text('Exported as Markdown with 1 file (.zip).'),
|
||||
findsOneWidget,
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('cancelled where to keep it, it says nothing more', (
|
||||
tester,
|
||||
) async {
|
||||
saver.cancel = true;
|
||||
|
||||
await exportFromMenu(tester);
|
||||
|
||||
expect(saver.saved, isEmpty);
|
||||
expect(find.text('Exporting…'), findsNothing);
|
||||
expect(find.textContaining('Exported'), findsNothing);
|
||||
});
|
||||
}
|
||||
Loading…
Reference in a new issue