Add cxyz Android project structure and initial configuration
- Introduced a new Flutter project for the cxyz Android manga/comic reader, including essential files such as pubspec.yaml, AndroidManifest.xml, and Gradle configuration. - Added .gitignore files to exclude unnecessary files and directories for both the main project and Android-specific files. - Implemented initial README.md with project description, features, setup instructions, and API details for ComicLand, MangaRead, and MangaTown. - Configured analysis options and dependencies in pubspec.yaml to support Flutter development. - Established a pnpm workspace and lockfile for dependency management. - Included various Android resources such as icons, styles, and launch backgrounds for a complete application setup.
This commit is contained in:
@@ -0,0 +1,21 @@
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:cxyz_android/sources/comicland/urls.dart';
|
||||
|
||||
void main() {
|
||||
test('parseComicLandUrl extracts slug and chapter', () {
|
||||
final parts = parseComicLandUrl(
|
||||
'https://comicland.org/comic/the-delinquent-girl/chapter/12',
|
||||
);
|
||||
expect(parts.slug, 'the-delinquent-girl');
|
||||
expect(parts.chapterIndex, '12');
|
||||
});
|
||||
|
||||
test('transferLinkBasename pads chapter numbers', () {
|
||||
expect(transferLinkBasename('12', 'Chapter 12', null), 'chapter 012.cbz');
|
||||
expect(transferLinkBasename('1.5', null, null), 'chapter 001.5.cbz');
|
||||
});
|
||||
|
||||
test('sanitizeComicDirName strips illegal chars', () {
|
||||
expect(sanitizeComicDirName('A/B:C*'), 'ABC');
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:cxyz_android/sources/mangaread/mangaread_source.dart';
|
||||
import 'package:cxyz_android/sources/source.dart';
|
||||
|
||||
void main() {
|
||||
test('latest pagination maps offset/limit to distinct site pages', () async {
|
||||
final source = MangaReadSource();
|
||||
final page1 = await source.loadCatalog(
|
||||
CatalogMenuKind.latest,
|
||||
offset: 0,
|
||||
limit: 30,
|
||||
);
|
||||
final page2 = await source.loadCatalog(
|
||||
CatalogMenuKind.latest,
|
||||
offset: 30,
|
||||
limit: 30,
|
||||
);
|
||||
|
||||
expect(page1, isNotEmpty);
|
||||
expect(page2, isNotEmpty);
|
||||
expect(page1.first.slug, isNot(page2.first.slug));
|
||||
final overlap = page1.map((c) => c.slug).toSet().intersection(
|
||||
page2.map((c) => c.slug).toSet(),
|
||||
);
|
||||
expect(overlap, isEmpty);
|
||||
}, timeout: const Timeout(Duration(seconds: 60)));
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:cxyz_android/sources/mangatown/mangatown_source.dart';
|
||||
import 'package:cxyz_android/sources/source.dart';
|
||||
|
||||
@Tags(['network'])
|
||||
void main() {
|
||||
test(
|
||||
'latest pagination: offset 0 and 30 yield different first slugs',
|
||||
() async {
|
||||
final source = MangaTownSource();
|
||||
final first = await source.loadCatalog(
|
||||
CatalogMenuKind.latest,
|
||||
offset: 0,
|
||||
limit: 30,
|
||||
);
|
||||
final second = await source.loadCatalog(
|
||||
CatalogMenuKind.latest,
|
||||
offset: 30,
|
||||
limit: 30,
|
||||
);
|
||||
expect(first, isNotEmpty);
|
||||
expect(second, isNotEmpty);
|
||||
expect(first.first.slug, isNot(second.first.slug));
|
||||
final firstSlugs = first.map((c) => c.slug).toSet();
|
||||
expect(
|
||||
second.any((c) => firstSlugs.contains(c.slug)),
|
||||
isFalse,
|
||||
reason: 'page 2 should not overlap page 1',
|
||||
);
|
||||
},
|
||||
timeout: const Timeout(Duration(seconds: 90)),
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:cxyz_android/sources/mangatown/urls.dart';
|
||||
import 'package:cxyz_android/sources/source.dart';
|
||||
|
||||
void main() {
|
||||
group('mangatown catalog URLs', () {
|
||||
test('latest page 1 and 2', () {
|
||||
expect(catalogLatestUrl(), 'https://m.mangatown.com/latest');
|
||||
expect(catalogLatestUrl(page: 2), 'https://m.mangatown.com/latest/2.html');
|
||||
});
|
||||
|
||||
test('hot total and period queries with pagination', () {
|
||||
expect(catalogHotUrl(), 'https://m.mangatown.com/hot');
|
||||
expect(
|
||||
catalogHotUrl(page: 2),
|
||||
'https://m.mangatown.com/hot/2.html',
|
||||
);
|
||||
expect(
|
||||
catalogHotUrl(period: MangaTownHotPeriod.today),
|
||||
'https://m.mangatown.com/hot?tviews.za',
|
||||
);
|
||||
expect(
|
||||
catalogHotUrl(period: MangaTownHotPeriod.week, page: 2),
|
||||
'https://m.mangatown.com/hot/2.html?wviews.za',
|
||||
);
|
||||
expect(
|
||||
catalogHotUrl(period: MangaTownHotPeriod.month, page: 2),
|
||||
'https://m.mangatown.com/hot/2.html?mviews.za',
|
||||
);
|
||||
});
|
||||
|
||||
test('directory default and filters', () {
|
||||
expect(
|
||||
directoryUrl(const CatalogBrowseParams()),
|
||||
'https://m.mangatown.com/directory/0-0-0-0-0-0',
|
||||
);
|
||||
expect(
|
||||
directoryUrl(
|
||||
const CatalogBrowseParams(
|
||||
demographic: 'seinen',
|
||||
genre: 'action',
|
||||
status: 'ongoing',
|
||||
initial: 'a',
|
||||
),
|
||||
page: 2,
|
||||
),
|
||||
'https://m.mangatown.com/directory/seinen-action-0-ongoing-a-0/2.html',
|
||||
);
|
||||
});
|
||||
|
||||
test('search URL', () {
|
||||
expect(
|
||||
catalogSearchUrl(query: 'naruto'),
|
||||
'https://m.mangatown.com/search?name=naruto',
|
||||
);
|
||||
expect(
|
||||
catalogSearchUrl(query: 'naruto', page: 2),
|
||||
'https://m.mangatown.com/search/2.html?name=naruto',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
group('mangatown series/chapter helpers', () {
|
||||
test('mangaSlugFromUrl and listingUrlForSlug', () {
|
||||
expect(
|
||||
mangaSlugFromUrl('https://m.mangatown.com/manga/ao_no_souyoku/c005/'),
|
||||
'ao_no_souyoku',
|
||||
);
|
||||
expect(
|
||||
listingUrlForSlug('ao_no_souyoku'),
|
||||
'https://m.mangatown.com/manga/ao_no_souyoku',
|
||||
);
|
||||
});
|
||||
|
||||
test('chapterBaseUrl and chapterPageUrl', () {
|
||||
expect(
|
||||
chapterBaseUrl('https://m.mangatown.com/manga/ao_no_souyoku/c005/3.html'),
|
||||
'https://m.mangatown.com/manga/ao_no_souyoku/c005/',
|
||||
);
|
||||
expect(
|
||||
chapterPageUrl('https://m.mangatown.com/manga/ao_no_souyoku/c005/', 1),
|
||||
'https://m.mangatown.com/manga/ao_no_souyoku/c005/',
|
||||
);
|
||||
expect(
|
||||
chapterPageUrl('https://m.mangatown.com/manga/ao_no_souyoku/c005/', 5),
|
||||
'https://m.mangatown.com/manga/ao_no_souyoku/c005/5.html',
|
||||
);
|
||||
});
|
||||
|
||||
test('chapterKeyFromUrl and chapterNumberFromTitle', () {
|
||||
expect(
|
||||
chapterKeyFromUrl('https://m.mangatown.com/manga/ao_no_souyoku/c005/'),
|
||||
'c005',
|
||||
);
|
||||
expect(chapterNumberFromTitle('C.5'), '5');
|
||||
expect(chapterNumberFromTitle('C.031'), '031');
|
||||
});
|
||||
|
||||
test('absoluteUrl upgrades protocol-relative', () {
|
||||
expect(
|
||||
absoluteUrl('//zjcdn.mangahere.org/a.jpg', 'https://m.mangatown.com/'),
|
||||
'https://zjcdn.mangahere.org/a.jpg',
|
||||
);
|
||||
});
|
||||
|
||||
test('hotPeriodForMenu mapping', () {
|
||||
expect(hotPeriodForMenu(CatalogMenuKind.popular), MangaTownHotPeriod.total);
|
||||
expect(hotPeriodForMenu(CatalogMenuKind.hotToday), MangaTownHotPeriod.today);
|
||||
expect(hotPeriodForMenu(CatalogMenuKind.hotWeek), MangaTownHotPeriod.week);
|
||||
expect(hotPeriodForMenu(CatalogMenuKind.hotMonth), MangaTownHotPeriod.month);
|
||||
expect(hotPeriodForMenu(CatalogMenuKind.latest), isNull);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
/// Mirrors DownloadQueue session drain gating used in `_pump`.
|
||||
bool shouldNotifyQueueDrained({
|
||||
required bool sessionHadWork,
|
||||
required int queuedRemaining,
|
||||
}) {
|
||||
return sessionHadWork && queuedRemaining == 0;
|
||||
}
|
||||
|
||||
void main() {
|
||||
group('queue drained gating', () {
|
||||
test('does not fire when no work was processed', () {
|
||||
expect(
|
||||
shouldNotifyQueueDrained(sessionHadWork: false, queuedRemaining: 0),
|
||||
isFalse,
|
||||
);
|
||||
});
|
||||
|
||||
test('fires when work ran and queue is empty', () {
|
||||
expect(
|
||||
shouldNotifyQueueDrained(sessionHadWork: true, queuedRemaining: 0),
|
||||
isTrue,
|
||||
);
|
||||
});
|
||||
|
||||
test('does not fire while items remain', () {
|
||||
expect(
|
||||
shouldNotifyQueueDrained(sessionHadWork: true, queuedRemaining: 2),
|
||||
isFalse,
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:cxyz_android/services/ntfy_notifier.dart';
|
||||
import 'package:cxyz_android/services/ntfy_settings.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:image/image.dart' as img;
|
||||
|
||||
void main() {
|
||||
group('NtfyNotifier.formatBytes', () {
|
||||
test('formats B KB MB', () {
|
||||
expect(NtfyNotifier.formatBytes(500), '500 B');
|
||||
expect(NtfyNotifier.formatBytes(1536), '1.5 KB');
|
||||
expect(NtfyNotifier.formatBytes(5 * 1024 * 1024), '5.0 MB');
|
||||
});
|
||||
});
|
||||
|
||||
group('payload titles and body', () {
|
||||
test('completed title is scannable', () {
|
||||
expect(
|
||||
NtfyNotifier.completedTitle('One Piece', 'Chapter 1100'),
|
||||
'Downloaded · One Piece · Chapter 1100',
|
||||
);
|
||||
});
|
||||
|
||||
test('failed title includes series and chapter', () {
|
||||
expect(
|
||||
NtfyNotifier.failedTitle('One Piece', 'Chapter 1100'),
|
||||
'Download failed · One Piece · Chapter 1100',
|
||||
);
|
||||
});
|
||||
|
||||
test('drained title includes counts', () {
|
||||
expect(
|
||||
NtfyNotifier.drainedTitle(ok: 5, failed: 1),
|
||||
'Queue finished · 5 downloaded · 1 failed',
|
||||
);
|
||||
});
|
||||
|
||||
test('completed body includes sizes and savings', () {
|
||||
final body = NtfyNotifier.completedBody(
|
||||
imageCount: 36,
|
||||
rawBytes: 10 * 1024 * 1024,
|
||||
cbzBytes: 8 * 1024 * 1024,
|
||||
);
|
||||
expect(body, contains('Pages: 36'));
|
||||
expect(body, contains('Images:'));
|
||||
expect(body, contains('CBZ:'));
|
||||
expect(body, contains('saved 20%'));
|
||||
});
|
||||
|
||||
test('completed body appends note', () {
|
||||
final body = NtfyNotifier.completedBody(
|
||||
imageCount: 1,
|
||||
rawBytes: 100,
|
||||
cbzBytes: 90,
|
||||
note: 'Attachment skipped (15 MB limit)',
|
||||
);
|
||||
expect(body, contains('Attachment skipped (15 MB limit)'));
|
||||
});
|
||||
});
|
||||
|
||||
group('NtfyAttachmentMode', () {
|
||||
test('fromStorage defaults to none', () {
|
||||
expect(NtfyAttachmentMode.fromStorage(null), NtfyAttachmentMode.none);
|
||||
expect(NtfyAttachmentMode.fromStorage('nope'), NtfyAttachmentMode.none);
|
||||
expect(
|
||||
NtfyAttachmentMode.fromStorage('firstImage'),
|
||||
NtfyAttachmentMode.firstImage,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
group('preview resize', () {
|
||||
test('resizePreviewIsolate shrinks large image under 250 KB', () {
|
||||
final big = img.Image(width: 2000, height: 3000);
|
||||
img.fill(big, color: img.ColorRgb8(40, 80, 120));
|
||||
// Add noise so JPEG does not compress to near-zero.
|
||||
for (var y = 0; y < big.height; y += 17) {
|
||||
for (var x = 0; x < big.width; x += 13) {
|
||||
big.setPixelRgb(x, y, (x * 3) % 255, (y * 5) % 255, 90);
|
||||
}
|
||||
}
|
||||
final raw = Uint8List.fromList(img.encodeJpg(big, quality: 95));
|
||||
expect(raw.length, greaterThan(250 * 1024));
|
||||
|
||||
final out = resizePreviewForTest(raw);
|
||||
expect(out, isNotNull);
|
||||
expect(out!.length, lessThanOrEqualTo(250 * 1024));
|
||||
|
||||
final decoded = img.decodeImage(out)!;
|
||||
final longest =
|
||||
decoded.width > decoded.height ? decoded.width : decoded.height;
|
||||
expect(longest, lessThanOrEqualTo(720));
|
||||
});
|
||||
});
|
||||
|
||||
group('encodeNtfyHeader', () {
|
||||
test('leaves plain ASCII unchanged', () {
|
||||
expect(encodeNtfyHeader('Downloaded Chapter 1'), 'Downloaded Chapter 1');
|
||||
});
|
||||
|
||||
test('RFC 2047-encodes middle-dot titles', () {
|
||||
final encoded = encodeNtfyHeader('Downloaded · Series · Ch 1');
|
||||
expect(encoded.startsWith('=?UTF-8?B?'), isTrue);
|
||||
expect(encoded.endsWith('?='), isTrue);
|
||||
expect(encoded.codeUnits.every((c) => c >= 0x20 && c <= 0x7e), isTrue);
|
||||
});
|
||||
|
||||
test('RFC 2047-encodes multiline message', () {
|
||||
final encoded = encodeNtfyHeader('Pages: 3\nImages: 1 MB\nCBZ: 0.9 MB');
|
||||
expect(encoded.startsWith('=?UTF-8?B?'), isTrue);
|
||||
expect(encoded.contains('\n'), isFalse);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:cxyz_android/services/reader_zoom.dart';
|
||||
|
||||
void main() {
|
||||
test('clamp stays in range and snaps to step', () {
|
||||
expect(ReaderZoom.clamp(0.1), ReaderZoom.min);
|
||||
expect(ReaderZoom.clamp(3.0), ReaderZoom.max);
|
||||
expect(ReaderZoom.clamp(0.66), closeTo(0.7, 0.001));
|
||||
expect(ReaderZoom.clamp(1.0), 1.0);
|
||||
});
|
||||
|
||||
test('percentLabel formats', () {
|
||||
expect(ReaderZoom.percentLabel(0.7), '70%');
|
||||
expect(ReaderZoom.percentLabel(1.0), '100%');
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,166 @@
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:cxyz_android/sources/source.dart';
|
||||
import 'package:cxyz_android/sources/source_flags.dart';
|
||||
import 'package:cxyz_android/sources/comicland/comicland_source.dart';
|
||||
import 'package:cxyz_android/sources/mangaread/mangaread_source.dart';
|
||||
import 'package:cxyz_android/sources/mangaread/urls.dart';
|
||||
import 'package:cxyz_android/sources/mangatown/mangatown_source.dart';
|
||||
import 'package:cxyz_android/sources/registry.dart';
|
||||
|
||||
void main() {
|
||||
group('parseEnabledSourceIds', () {
|
||||
test('default comicland token', () {
|
||||
expect(parseEnabledSourceIds('comicland'), {SourceId.comicland});
|
||||
});
|
||||
|
||||
test('comma-separated both', () {
|
||||
expect(
|
||||
parseEnabledSourceIds('comicland,mangaread'),
|
||||
{SourceId.comicland, SourceId.mangaread},
|
||||
);
|
||||
});
|
||||
|
||||
test('mangaread only', () {
|
||||
expect(parseEnabledSourceIds('mangaread'), {SourceId.mangaread});
|
||||
});
|
||||
|
||||
test('all enables every SourceId', () {
|
||||
expect(parseEnabledSourceIds('all'), SourceId.values.toSet());
|
||||
expect(parseEnabledSourceIds('ALL'), SourceId.values.toSet());
|
||||
expect(parseEnabledSourceIds('all,mangaread'), SourceId.values.toSet());
|
||||
});
|
||||
|
||||
test('mangatown token', () {
|
||||
expect(parseEnabledSourceIds('mangatown'), {SourceId.mangatown});
|
||||
});
|
||||
|
||||
test('ignores unknown and falls back when empty', () {
|
||||
expect(parseEnabledSourceIds(''), {SourceId.comicland});
|
||||
expect(parseEnabledSourceIds('nope,also-bad'), {SourceId.comicland});
|
||||
});
|
||||
|
||||
test('trims and is case-insensitive', () {
|
||||
expect(
|
||||
parseEnabledSourceIds(' MangaRead , ComicLand '),
|
||||
{SourceId.mangaread, SourceId.comicland},
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
group('SourceRegistry gating', () {
|
||||
test('registers only enabled ids', () {
|
||||
final reg = SourceRegistry(
|
||||
enabled: {SourceId.mangaread},
|
||||
mangaRead: MangaReadSource(),
|
||||
);
|
||||
expect(reg.all.map((s) => s.id), [SourceId.mangaread]);
|
||||
expect(reg.hasMultiple, isFalse);
|
||||
});
|
||||
|
||||
test('dual sources preserve declaration order', () {
|
||||
final reg = SourceRegistry(
|
||||
enabled: {SourceId.mangaread, SourceId.comicland},
|
||||
comicLand: ComicLandSource(),
|
||||
mangaRead: MangaReadSource(),
|
||||
);
|
||||
expect(reg.all.map((s) => s.id).toList(), [
|
||||
SourceId.comicland,
|
||||
SourceId.mangaread,
|
||||
]);
|
||||
expect(reg.hasMultiple, isTrue);
|
||||
});
|
||||
|
||||
test('all three sources preserve declaration order', () {
|
||||
final reg = SourceRegistry(
|
||||
enabled: {
|
||||
SourceId.mangatown,
|
||||
SourceId.mangaread,
|
||||
SourceId.comicland,
|
||||
},
|
||||
comicLand: ComicLandSource(),
|
||||
mangaRead: MangaReadSource(),
|
||||
mangaTown: MangaTownSource(),
|
||||
);
|
||||
expect(reg.all.map((s) => s.id).toList(), [
|
||||
SourceId.comicland,
|
||||
SourceId.mangaread,
|
||||
SourceId.mangatown,
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
group('catalogMenus capabilities', () {
|
||||
test('ComicLand exposes newest, popular, official, uncensored, recommended, search', () {
|
||||
final kinds =
|
||||
ComicLandSource().catalogMenus.map((m) => m.kind).toList();
|
||||
expect(kinds, [
|
||||
CatalogMenuKind.latest,
|
||||
CatalogMenuKind.popular,
|
||||
CatalogMenuKind.official,
|
||||
CatalogMenuKind.uncensored,
|
||||
CatalogMenuKind.recommended,
|
||||
CatalogMenuKind.search,
|
||||
]);
|
||||
expect(
|
||||
ComicLandSource().catalogMenus.first.title,
|
||||
'Newest',
|
||||
);
|
||||
});
|
||||
|
||||
test('MangaRead exposes latest and search only', () {
|
||||
final kinds =
|
||||
MangaReadSource().catalogMenus.map((m) => m.kind).toList();
|
||||
expect(kinds, [
|
||||
CatalogMenuKind.latest,
|
||||
CatalogMenuKind.search,
|
||||
]);
|
||||
expect(kinds.contains(CatalogMenuKind.popular), isFalse);
|
||||
});
|
||||
|
||||
test('MangaTown exposes latest, hot variants, directory, search', () {
|
||||
final kinds =
|
||||
MangaTownSource().catalogMenus.map((m) => m.kind).toList();
|
||||
expect(kinds, [
|
||||
CatalogMenuKind.latest,
|
||||
CatalogMenuKind.popular,
|
||||
CatalogMenuKind.hotToday,
|
||||
CatalogMenuKind.hotWeek,
|
||||
CatalogMenuKind.hotMonth,
|
||||
CatalogMenuKind.directory,
|
||||
CatalogMenuKind.search,
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
group('mangaread urls', () {
|
||||
test('mangaSlugFromUrl', () {
|
||||
expect(
|
||||
mangaSlugFromUrl(
|
||||
'https://www.mangaread.org/manga/the-chaebeols-youngest-son/chapter-560/',
|
||||
),
|
||||
'the-chaebeols-youngest-son',
|
||||
);
|
||||
});
|
||||
|
||||
test('mangaBaseUrlFromListingUrl', () {
|
||||
expect(
|
||||
mangaBaseUrlFromListingUrl(
|
||||
'https://www.mangaread.org/manga/foo-bar/chapter-1/',
|
||||
),
|
||||
'https://www.mangaread.org/manga/foo-bar/',
|
||||
);
|
||||
});
|
||||
|
||||
test('listingUrlForSlug', () {
|
||||
expect(
|
||||
listingUrlForSlug('foo-bar'),
|
||||
'https://www.mangaread.org/manga/foo-bar/',
|
||||
);
|
||||
});
|
||||
|
||||
test('chapterNumberFromTitle', () {
|
||||
expect(chapterNumberFromTitle('Chapter 560'), '560');
|
||||
expect(chapterNumberFromTitle('Ch. 12.5'), '12.5');
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:cxyz_android/services/reader_direction.dart';
|
||||
import 'package:cxyz_android/services/source_reader_defaults.dart';
|
||||
|
||||
void main() {
|
||||
group('SourceReaderDefaults.modeFor', () {
|
||||
test('comicland is webtoon', () {
|
||||
expect(SourceReaderDefaults.modeFor('comicland'), ReaderMode.webtoon);
|
||||
});
|
||||
|
||||
test('mangaread and mangatown are paged', () {
|
||||
expect(SourceReaderDefaults.modeFor('mangaread'), ReaderMode.paged);
|
||||
expect(SourceReaderDefaults.modeFor('mangatown'), ReaderMode.paged);
|
||||
});
|
||||
});
|
||||
|
||||
group('SourceReaderDefaults.directionFor', () {
|
||||
test('mangaread and mangatown are rtl', () {
|
||||
expect(
|
||||
SourceReaderDefaults.directionFor('mangaread'),
|
||||
ReadingDirection.rtl,
|
||||
);
|
||||
expect(
|
||||
SourceReaderDefaults.directionFor('mangatown'),
|
||||
ReadingDirection.rtl,
|
||||
);
|
||||
});
|
||||
|
||||
test('comicland has no built-in direction', () {
|
||||
expect(SourceReaderDefaults.directionFor('comicland'), isNull);
|
||||
});
|
||||
});
|
||||
|
||||
group('SourceReaderDefaults.resolveMode', () {
|
||||
test('stored override wins', () {
|
||||
expect(
|
||||
SourceReaderDefaults.resolveMode('mangatown', 'webtoon'),
|
||||
ReaderMode.webtoon,
|
||||
);
|
||||
expect(
|
||||
SourceReaderDefaults.resolveMode('comicland', 'paged'),
|
||||
ReaderMode.paged,
|
||||
);
|
||||
});
|
||||
|
||||
test('empty / unknown falls back to source default', () {
|
||||
expect(
|
||||
SourceReaderDefaults.resolveMode('mangatown', ''),
|
||||
ReaderMode.paged,
|
||||
);
|
||||
expect(
|
||||
SourceReaderDefaults.resolveMode('mangatown', null),
|
||||
ReaderMode.paged,
|
||||
);
|
||||
expect(
|
||||
SourceReaderDefaults.resolveMode('comicland', ''),
|
||||
ReaderMode.webtoon,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
group('SourceReaderDefaults.resolveDirection', () {
|
||||
test('stored override wins', () {
|
||||
expect(
|
||||
SourceReaderDefaults.resolveDirection(
|
||||
'mangatown',
|
||||
'ltr',
|
||||
globalFallback: ReadingDirection.rtl,
|
||||
),
|
||||
ReadingDirection.ltr,
|
||||
);
|
||||
});
|
||||
|
||||
test('source default when unset', () {
|
||||
expect(
|
||||
SourceReaderDefaults.resolveDirection(
|
||||
'mangaread',
|
||||
null,
|
||||
globalFallback: ReadingDirection.ltr,
|
||||
),
|
||||
ReadingDirection.rtl,
|
||||
);
|
||||
});
|
||||
|
||||
test('global fallback for comicland when unset', () {
|
||||
expect(
|
||||
SourceReaderDefaults.resolveDirection(
|
||||
'comicland',
|
||||
null,
|
||||
globalFallback: ReadingDirection.rtl,
|
||||
),
|
||||
ReadingDirection.rtl,
|
||||
);
|
||||
expect(
|
||||
SourceReaderDefaults.resolveDirection(
|
||||
'comicland',
|
||||
'',
|
||||
globalFallback: ReadingDirection.ltr,
|
||||
),
|
||||
ReadingDirection.ltr,
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
import 'package:cxyz_android/main.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
void main() {
|
||||
testWidgets('App builds', (tester) async {
|
||||
await tester.pumpWidget(const ProviderScope(child: CxyzApp()));
|
||||
// Catalog app bar title from ComicLand source.
|
||||
expect(find.text('ComicLand'), findsOneWidget);
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user