Add the curated Learning sources (#6)

Fifteen English Learning sources join ByteByteGo in the catalog: System
Design One, The Pragmatic Engineer (blog), Martin Fowler, Julia Evans,
Dan Luu, The Cloudflare Blog, Netflix Technology Blog, Engineering at
Meta, GitHub Blog: Engineering, Latent Space, Import AI, Interconnects,
Ahead of AI and Lilian Weng (on by default), and InfoQ (off).

Tests pin the Learning catalog, check that existing installs get the
new on-by-default Sources and that the Learn tab shows their posts from
the last 14 days. A live test times a full refresh. The README gets a
Learn feature bullet and a Learning sources table.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
CedricandClaude Opus 5.5 committed 2026-10-16 15:50:50 +02:00
1 parent a6b8d8b537
commit b634118ed8
6 files changed
+263 -2

No files matched your search

+34
View File
@@ -43,6 +43,40 @@ void main() {
expect(ids, isNot(contains('engadget')));
});
test('the Learning sources are the curated set, all English, with the agreed defaults', () {
final learning = {
for (final s in defaultSources)
if (s.kind == SourceKind.learning) s.name: (s.feedUrl, s.language, s.enabledByDefault),
};
const en = SourceLanguage.en;
expect(learning, {
'ByteByteGo': ('https://blog.bytebytego.com/feed', en, true),
'System Design One': ('https://newsletter.systemdesign.one/feed', en, true),
'The Pragmatic Engineer (blog)': ('https://blog.pragmaticengineer.com/rss/', en, true),
'Martin Fowler': ('https://martinfowler.com/feed.atom', en, true),
'Julia Evans': ('https://jvns.ca/atom.xml', en, true),
'Dan Luu': ('https://danluu.com/atom.xml', en, true),
'The Cloudflare Blog': ('https://blog.cloudflare.com/rss/', en, true),
'Netflix Technology Blog': ('https://netflixtechblog.com/feed', en, true),
'Engineering at Meta': ('https://engineering.fb.com/feed/', en, true),
'GitHub Blog: Engineering': ('https://github.blog/engineering/feed/', en, true),
'Latent Space': ('https://www.latent.space/feed', en, true),
'Import AI': ('https://importai.substack.com/feed', en, true),
'Interconnects': ('https://www.interconnects.ai/feed', en, true),
'Ahead of AI': ('https://magazine.sebastianraschka.com/feed', en, true),
'Lilian Weng': ('https://lilianweng.github.io/index.xml', en, true),
'InfoQ': ('https://feed.infoq.com/', en, false),
});
});
test('every Learning source says what it is for and links its homepage', () {
for (final s in defaultSources.where((s) => s.kind == SourceKind.learning)) {
expect(s.description, isNotEmpty, reason: s.name);
expect(Uri.parse(s.homepage).isScheme('https'), isTrue, reason: s.name);
}
});
test('every Source has its own id', () {
final ids = defaultSources.map((s) => s.id).toList();
+17
View File
@@ -2,10 +2,14 @@
import 'dart:io';
import 'package:daily_tech_news/data/default_sources.dart';
import 'package:daily_tech_news/models/news_source.dart';
import 'package:daily_tech_news/services/article_extractor.dart';
import 'package:daily_tech_news/services/consent_service.dart';
import 'package:daily_tech_news/services/feed_service.dart';
import 'package:daily_tech_news/services/storage.dart';
import 'package:daily_tech_news/state/app_state.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:shared_preferences/shared_preferences.dart';
void main() {
final live = Platform.environment['LIVE_FEEDS'] == '1';
@@ -29,6 +33,19 @@ void main() {
expect(failures, isEmpty);
}, skip: live ? false : 'set LIVE_FEEDS=1 to run', timeout: const Timeout(Duration(minutes: 3)));
test('a full refresh with all on-by-default Sources stays fast', () async {
SharedPreferences.setMockInitialValues({});
final state = AppState(await Storage.open(), feedService: FeedService());
final clock = Stopwatch()..start();
await state.init();
clock.stop();
// ignore: avoid_print
print('${state.enabledSources.length} Sources, ${state.articles.length} articles '
'(${state.articlesOf(SourceKind.learning).length} Learning) in '
'${clock.elapsedMilliseconds} ms; errors: ${state.feedErrors}');
expect(clock.elapsed, lessThan(const Duration(seconds: 15)));
}, skip: live ? false : 'set LIVE_FEEDS=1 to run', timeout: const Timeout(Duration(minutes: 1)));
test('reader view extracts the article body', () async {
final service = FeedService();
final extractor = ArticleExtractor(consent: ConsentService());
+20
View File
@@ -135,4 +135,24 @@ void main() {
expect(bytebytego.language, SourceLanguage.en);
expect(bytebytego.enabledByDefault, isTrue);
});
test('a fresh install shows posts of the last 14 days from the curated Learning sources',
() async {
final learning = defaultSources.where((s) => s.kind == SourceKind.learning);
final state = await launchApp(defaultSources, feeds: {
for (final s in learning)
s.feedUrl: [('${s.name}: recent', const Duration(days: 13)),
('${s.name}: old', const Duration(days: 15))],
});
expect(_titles(state.articlesOf(SourceKind.learning)).toSet(), {
'ByteByteGo: recent', 'System Design One: recent',
'The Pragmatic Engineer (blog): recent', 'Martin Fowler: recent', 'Julia Evans: recent',
'Dan Luu: recent', 'The Cloudflare Blog: recent', 'Netflix Technology Blog: recent',
'Engineering at Meta: recent', 'GitHub Blog: Engineering: recent', 'Latent Space: recent',
'Import AI: recent', 'Interconnects: recent', 'Ahead of AI: recent',
'Lilian Weng: recent',
// InfoQ is off by default.
});
});
}
+10 -2
View File
@@ -23,6 +23,13 @@ NewsSource _source(String id, {bool onByDefault = false}) => NewsSource(
enabledByDefault: onByDefault,
);
/// The curated Learning sources that are on by default (all but InfoQ).
const _learningOnByDefault = {
'bytebytego', 'systemdesignone', 'pragmaticengineer-blog', 'martinfowler', 'juliaevans',
'danluu', 'cloudflare-blog', 'netflix-techblog', 'meta-engineering', 'github-engineering',
'latent-space', 'import-ai', 'interconnects', 'ahead-of-ai', 'lilianweng',
};
/// Starts the app the way a launch does, against whatever is in preferences.
Future<AppState> _launch(List<NewsSource> curatedSources) async {
final state =
@@ -87,7 +94,7 @@ void main() {
expect(state.enabledSourceIds, {
'heise', 'verge', 'mit-tr', 'new-source',
// Added to the catalog later, on by default.
'techmeme', 'lobsters', 'the-decoder', 'simonwillison', 'bytebytego',
'techmeme', 'lobsters', 'the-decoder', 'simonwillison', ..._learningOnByDefault,
});
});
@@ -102,8 +109,9 @@ void main() {
expect(state.enabledSourceIds, {
'heise', 'golem', 't3n', 'verge',
'techmeme', 'lobsters', 'the-decoder', 'simonwillison', 'bytebytego',
'techmeme', 'lobsters', 'the-decoder', 'simonwillison', ..._learningOnByDefault,
});
expect(state.enabledSources.map((s) => s.id), isNot(contains('wired')));
expect(state.enabledSourceIds, isNot(contains('infoq')));
});
}