feat: color a site from its address, and offer a failed web page to the system browser

This commit is contained in:
randogoth 2026-10-11 22:32:44 +03:00
parent 56db479df0
commit 9d9f09ebc7
24 changed files with 597 additions and 25 deletions

View file

@ -133,6 +133,31 @@ void main() {
expect(find.textContaining('Try again'), findsOneWidget);
});
testWidgets('a web page that will not load can open in a web browser',
(tester) async {
final stubDeps = AppDependencies(
database: MewsDatabase.test(NativeDatabase.memory()),
fetch: FakeFetch(const {}),
strings: strings,
);
await tester.pumpWidget(MewsApp(deps: stubDeps, strings: strings));
await tester.pumpAndSettle();
// A web address that answers nothing: the error offers the way out,
// since the web can be shown outside this reader.
await stubDeps.browser.open('https://missing.example.org/');
await tester.pumpAndSettle();
expect(stubDeps.browser.error.value, isA<LoadFailed>());
expect(find.text('Open in a web browser'), findsOneWidget);
// A protocol page has nowhere else to go, so only trying again is
// offered there.
await stubDeps.browser.open('gemini://missing.example.org/');
await tester.pumpAndSettle();
expect(stubDeps.browser.error.value, isA<LoadFailed>());
expect(find.text('Open in a web browser'), findsNothing);
});
testWidgets('the hub opens the settings page, which applies a choice in place',
(tester) async {
await pumpApp(tester);

View file

@ -77,6 +77,17 @@ void main() {
expect(await db.allPins(), {'capsule.example:1965': presented});
});
test('a failed load keeps the address the reader followed', () async {
final controller = browser(FakeFetch(const {}));
// Neither the address nor its wap. subdomain answers; the wap. guess
// is not left standing in for what the reader followed.
await controller.open('https://capsule.example/post');
expect(controller.error.value, isA<LoadFailed>());
expect(controller.currentAddress, 'https://capsule.example/post');
});
test('a certificate request is answered by binding an identity', () async {
final controller = browser(FakeFetch(
{'gemini://capsule.example': '# Hello\n\nText.'},

View file

@ -0,0 +1,85 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:mews/mews/document.dart';
import 'package:mews/presentation/reader/oklch.dart';
import 'package:mews/presentation/reader/palettes.dart';
import 'package:mews/presentation/reader/site_palette.dart';
/// A wide sample of what the generator can produce: enough hosts to cover all
/// 24 hues, 3 background tints and both foregrounds.
Iterable<MewsPalette> _allPalettes(bool dark) => [
for (var i = 0; i < 2000; i++) sitePalette('gemini://h$i.example/', dark: dark)!,
];
void main() {
group('seed', () {
test('comes from the host', () {
expect(siteSeed('gemini://example.org/page'), 'example.org');
expect(siteSeed('gemini://Example.ORG/page'), 'example.org');
});
test('names the user on a personal page', () {
expect(siteSeed('gemini://example.org/~alice/'), 'alice@example.org');
expect(siteSeed('gemini://example.org/users/alice/x.gmi'), 'alice@example.org');
expect(siteSeed('gemini://example.org/Users/Alice'), 'alice@example.org');
});
test('is empty without a host', () {
expect(siteSeed('mews:settings'), '');
expect(siteSeed('not a url'), '');
});
});
group('palette', () {
test('is the same every time for one address', () {
expect(sitePalette('gemini://example.org/a', dark: false),
same(sitePalette('gemini://example.org/b', dark: false)));
});
test('differs between two users on one host', () {
final alice = sitePalette('gemini://example.org/~alice/', dark: false)!;
final bob = sitePalette('gemini://example.org/~bob/', dark: false)!;
expect(alice.link, isNot(bob.link));
});
test('is null for an address with no host', () {
expect(sitePalette('mews:settings', dark: false), isNull);
});
test('falls back to plain when the reader asks for host colors', () {
expect(
paletteFor(
pageVariant: MewsVariant.plain,
readerVariant: 'host',
dark: false,
url: 'mews:settings'),
paletteFor(pageVariant: MewsVariant.plain, readerVariant: 'plain', dark: false),
);
});
// Spec 5.1: every text and link color meets 7:1 against its background.
for (final dark in [false, true]) {
test('meets 7:1 contrast in ${dark ? 'dark' : 'light'} mode', () {
for (final palette in _allPalettes(dark)) {
expect(contrastRatio(palette.background, palette.foreground),
greaterThanOrEqualTo(7.0));
expect(contrastRatio(palette.background, palette.link),
greaterThanOrEqualTo(7.0));
expect(contrastRatio(palette.background, palette.visited),
greaterThanOrEqualTo(7.0));
}
});
test('stays no louder than the fixed palettes in ${dark ? 'dark' : 'light'} mode', () {
// Cool holds the most saturated link of the fixed palettes, so no
// generated color may pass it.
final cool = paletteFor(
pageVariant: MewsVariant.cool, readerVariant: 'cool', dark: dark);
final limit = chromaOf(cool.link);
for (final palette in _allPalettes(dark)) {
expect(chromaOf(palette.link), lessThanOrEqualTo(limit));
expect(chromaOf(palette.background), lessThanOrEqualTo(0.03));
}
});
}
});
}