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:
Tizian 「ティツィアーン」 2026-10-19 20:08:06 +02:00
commit 649499be23
Signed by: raisondetredev
GPG key ID: 07F6FCF405488B64
4 changed files with 0 additions and 0 deletions

View file

@ -0,0 +1,93 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:halkyon_learn_flutter/src/feature/geo_map/data/geo_map_repository.dart';
import 'package:halkyon_learn_flutter/src/feature/geo_map/data/map_groups.dart';
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
test('the bundled map of Japan has the 47 prefectures, Tokyo where it '
'is', () async {
final map = (await GeoMapRepository().load('japan'))!;
expect(map.regions, hasLength(47));
expect(map.region('JP-13')!.name, 'Tokyo');
expect(map.region('JP-13')!.nameIn('ja'), '東京都');
// The Imperial Palace.
expect(map.regionAt((139.7528, 35.6852))?.code, 'JP-13');
// Osaka Castle.
expect(map.regionAt((135.5262, 34.6873))?.code, 'JP-27');
});
test('the bundled world has every country with an ISO code, named in '
"the studysets' languages", () async {
final map = (await GeoMapRepository().load('world'))!;
expect(map.regions, hasLength(239));
final cyprus = map.region('CY')!;
expect(cyprus.nameIn('de'), 'Zypern');
expect(cyprus.nameIn('ja'), 'キプロス');
// Limassol, and Kyrenia in the north, which counts as Cyprus.
expect(map.regionAt((33.04, 34.68))?.code, 'CY');
expect(map.regionAt((33.32, 35.33))?.code, 'CY');
// Vaduz, Andorra la Vella, the Vatican's gardens.
expect(map.regionAt((9.52, 47.14))?.code, 'LI');
expect(map.regionAt((1.52, 42.51))?.code, 'AD');
expect(map.region('VA')!.marker, isNotNull);
expect(map.region('XK')!.name, 'Kosovo');
expect(map.region('TW')!.name, 'Taiwan');
});
test("every country is in a group or picked at once, and every group's "
'member is on the map', () async {
final map = (await GeoMapRepository().load('world'))!;
final codes = {for (final region in map.regions) region.code};
for (final group in worldGroups) {
expect(codes, containsAll(group.members), reason: group.id);
}
for (final code in codes) {
expect(
map.groupsOf(code).isNotEmpty || worldUngrouped.contains(code),
isTrue,
reason: code,
);
}
expect(worldUngrouped, everyElement(isIn(codes)));
});
test("every prefecture is in one of Japan's groups", () async {
final map = (await GeoMapRepository().load('japan'))!;
for (final region in map.regions) {
expect(map.groupsOf(region.code), hasLength(1), reason: region.code);
}
});
test("the maps as one: Japan's prefectures within Japan, Tokyo in Kantō "
'in East Asia', () async {
final maps = GeoMapRepository();
final atlas = await maps.atlas();
expect(atlas.world, await maps.load('world'));
expect(atlas.inside['JP'], await maps.load('japan'));
expect(atlas.mapOf('JP-13')?.id, 'japan');
expect(atlas.mapOf('JP')?.id, 'world');
expect(
[
for (final level in atlas.levelsOf('JP-13'))
(level.map.id, level.group?.id),
],
[
('world', null),
('world', 'eastAsia'),
('japan', null),
('japan', 'kanto'),
],
);
expect(await maps.atlas(), same(atlas));
});
test('a map this app does not bundle is none', () async {
expect(await GeoMapRepository().load('atlantis'), isNull);
});
}

View file

@ -0,0 +1,88 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:halkyon_learn_flutter/src/feature/geo_map/data/geo_map_parser.dart';
import 'package:halkyon_learn_flutter/src/feature/geo_map/model/atlas.dart';
import 'package:halkyon_learn_flutter/src/feature/geo_map/model/geo_map.dart';
void main() {
/// A square region [code], the [index]th from the west.
String square(int index, String code) =>
'{"type": "Feature", "properties": {"code": "$code", "name": "$code"}, '
'"geometry": {"type": "Polygon", "coordinates": [[[$index, 0], '
'[${index + 1}, 0], [${index + 1}, 1], [$index, 1], [$index, 0]]]}}';
/// A map of a square region per code, side by side.
GeoMap mapOf(String id, List<String> codes, List<MapGroup> groups) {
final features = [
for (final (index, code) in codes.indexed) square(index, code),
];
return parseGeoMap(
id,
'{"type": "FeatureCollection", "features": [${features.join(', ')}]}',
groups: groups,
);
}
// A world of two groups sharing CC, EE in none, and JJ with a map of its
// own: two regions in a group, and one alone in its own.
const west = MapGroup(id: 'west', members: {'AA', 'BB', 'CC'});
const east = MapGroup(id: 'east', members: {'CC', 'DD', 'JJ'});
const north = MapGroup(id: 'north', members: {'JJ-1', 'JJ-2'});
const island = MapGroup(id: 'island', members: {'JJ-3'});
final world = mapOf(
'world',
[
'AA', 'BB', 'CC', 'DD', 'EE', 'JJ', //
],
const [west, east],
);
final inner = mapOf('inner', ['JJ-1', 'JJ-2', 'JJ-3'], const [north, island]);
final atlas = Atlas(world: world, inside: {'JJ': inner});
final whole = (map: world, group: null);
final westLevel = (map: world, group: west);
final eastLevel = (map: world, group: east);
final innerWhole = (map: inner, group: null);
final northLevel = (map: inner, group: north);
test('finds a region on whichever map holds it', () {
expect(atlas.mapOf('AA'), world);
expect(atlas.mapOf('JJ-2'), inner);
expect(atlas.region('JJ-2')?.code, 'JJ-2');
expect(atlas.region('ZZ'), isNull);
expect(atlas.countryOf(inner), 'JJ');
expect(atlas.countryOf(world), isNull);
});
test("a region's levels run from the whole world down through its groups "
"and its country's map, but a group of one", () {
expect(atlas.levelsOf('AA'), [whole, westLevel]);
expect(atlas.levelsOf('CC'), [whole, westLevel, eastLevel]);
expect(atlas.levelsOf('EE'), [whole]);
expect(atlas.levelsOf('JJ-1'), [whole, eastLevel, innerWhole, northLevel]);
expect(atlas.levelsOf('JJ-3'), [whole, eastLevel, innerWhole]);
// One a newer app knows: the whole world, where nothing is missing.
expect(atlas.levelsOf('ZZ'), [whole]);
});
test('a map opens on the deepest level showing every region it is '
'around; of two as deep, the first', () {
expect(atlas.levelFor(const []), whole);
expect(atlas.levelFor(['AA', 'BB']), westLevel);
expect(atlas.levelFor(['CC']), westLevel);
expect(atlas.levelFor(['CC', 'DD']), eastLevel);
expect(atlas.levelFor(['AA', 'DD']), whole);
expect(atlas.levelFor(['JJ-1', 'JJ-2']), northLevel);
expect(atlas.levelFor(['JJ-1', 'JJ-3']), innerWhole);
expect(atlas.levelFor(['JJ-1', 'DD']), eastLevel);
expect(atlas.levelFor(['JJ-1', 'JJ']), eastLevel);
expect(atlas.levelFor(['JJ-1', 'EE']), whole);
});
test("Back goes from a group to its map, from a country's map to the "
"world's group the country is in, and from the world nowhere", () {
expect(atlas.above(northLevel), innerWhole);
expect(atlas.above(innerWhole), eastLevel);
expect(atlas.above(westLevel), whole);
expect(atlas.above(whole), isNull);
});
}

View file

@ -0,0 +1,102 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:halkyon_learn_flutter/src/feature/geo_map/data/geo_map_parser.dart';
import 'package:halkyon_learn_flutter/src/feature/geo_map/model/geo_map.dart';
void main() {
// Two square regions side by side; the left one with a square hole.
const geojson = '''
{"type": "FeatureCollection", "features": [
{"type": "Feature",
"properties": {"code": "XX-01", "name": "West", "name_ja": "西"},
"geometry": {"type": "Polygon", "coordinates": [
[[0, 0], [10, 0], [10, 10], [0, 10], [0, 0]],
[[4, 4], [6, 4], [6, 6], [4, 6], [4, 4]]]}},
{"type": "Feature",
"properties": {"code": "XX-02", "name": "East"},
"geometry": {"type": "MultiPolygon", "coordinates": [
[[[10, 0], [20, 0], [20, 10], [10, 10], [10, 0]]],
[[[30, 0], [31, 0], [31, 1], [30, 1], [30, 0]]]]}}
]}''';
test('reads regions with their codes, names and areas', () {
final map = parseGeoMap('test', geojson);
expect(map.regions.map((region) => region.code), ['XX-01', 'XX-02']);
expect(map.region('XX-01')!.names, {'en': 'West', 'ja': '西'});
expect(map.region('XX-02')!.nameIn('ja'), isNull);
expect(map.region('XX-02')!.polygons, hasLength(2));
expect(map.bounds, (0.0, 0.0, 31.0, 10.0));
});
test('reads names by language, a label, and land no region holds', () {
final map = parseGeoMap('test', '''
{"type": "FeatureCollection", "features": [
{"type": "Feature",
"properties": {"code": "XX", "names": {"en": "Ex", "de": "Ix"},
"label": [1, 2]},
"geometry": {"type": "Polygon", "coordinates": [
[[0, 0], [10, 0], [10, 10], [0, 10], [0, 0]]]}},
{"type": "Feature", "properties": {},
"geometry": {"type": "Polygon", "coordinates": [
[[20, 0], [21, 0], [21, 1], [20, 1], [20, 0]]]}}
]}''');
final region = map.region('XX')!;
expect(region.name, 'Ex');
expect(region.nameIn('de'), 'Ix');
expect(region.marker, (1.0, 2.0));
expect(map.regions, hasLength(1));
expect(map.land, hasLength(1));
expect(map.regionAt((20.5, 0.5)), isNull);
});
test('a region without a label is marked in the middle of its largest '
'area', () {
final map = parseGeoMap('test', geojson);
expect(map.region('XX-02')!.marker, (15.0, 5.0));
});
test('a point past the antimeridian is found where it lies', () {
final map = parseGeoMap('test', '''
{"type": "FeatureCollection", "features": [
{"type": "Feature", "properties": {"code": "WS", "name": "Samoa"},
"geometry": {"type": "Polygon", "coordinates": [
[[-173, -14], [-171, -14], [-171, -13], [-173, -13], [-173, -14]]]}}
]}''');
// Samoa east of Fiji, as a view of the Pacific counts it.
expect(map.regionAt((188, -13.5))?.code, 'WS');
expect(map.regionAt((-172, -13.5))?.code, 'WS');
});
test('a group frames its members unless it has a frame of its own', () {
final map = parseGeoMap(
'test',
geojson,
groups: const [
MapGroup(id: 'both', members: {'XX-01', 'XX-02'}),
MapGroup(id: 'west', members: {'XX-01'}, frame: (-1, -1, 11, 11)),
],
);
expect(map.groupsOf('XX-01').map((group) => group.id), ['both', 'west']);
expect(map.frameOf(map.groups.first), (0.0, 0.0, 31.0, 10.0));
expect(map.frameOf(map.groups.last), (-1.0, -1.0, 11.0, 11.0));
});
test('finds the region at a point, outside its holes and on its '
'islands', () {
final map = parseGeoMap('test', geojson);
expect(map.regionAt((2, 2))?.code, 'XX-01');
expect(map.regionAt((5, 5)), isNull);
expect(map.regionAt((15, 5))?.code, 'XX-02');
expect(map.regionAt((30.5, 0.5))?.code, 'XX-02');
expect(map.regionAt((25, 5)), isNull);
});
test('refuses what is not a map', () {
expect(() => parseGeoMap('test', '{"type": "x"}'), throwsFormatException);
});
}

View file

@ -0,0 +1,493 @@
import 'package:flutter/gestures.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:halkyon_learn_flutter/l10n/app_localizations.dart';
import 'package:halkyon_learn_flutter/src/core/l10n/app_languages.dart';
import 'package:halkyon_learn_flutter/src/core/theme/learn_theme.dart';
import 'package:halkyon_learn_flutter/src/feature/geo_map/data/geo_map_repository.dart';
import 'package:halkyon_learn_flutter/src/feature/geo_map/model/atlas.dart';
import 'package:halkyon_learn_flutter/src/feature/geo_map/model/geo_map.dart';
import 'package:halkyon_learn_flutter/src/feature/geo_map/widget/region_map.dart';
import 'package:material_ui/material_ui.dart';
void main() {
late Atlas atlas;
setUpAll(() async {
TestWidgetsFlutterBinding.ensureInitialized();
atlas = await GeoMapRepository().atlas();
});
/// Shows the maps at a phone's size, anew unless [key] is the one shown;
/// what is picked lands in the list, and shows as picked.
Future<List<String>> show(
WidgetTester tester, {
Key? key,
CountryTaps countryTaps = CountryTaps.pickOrOpen,
Set<String> around = const {},
String? right,
String? wrong,
bool readOnly = false,
bool namesCountryParts = true,
}) async {
tester.view
..physicalSize = const Size(400, 640)
..devicePixelRatio = 1;
addTearDown(tester.view.reset);
final picked = <String>[];
final shown = key ?? UniqueKey();
await tester.pumpWidget(
MaterialApp(
theme: learnTheme(Brightness.light),
localizationsDelegates: appLocalizationsDelegates,
supportedLocales: AppLocalizations.supportedLocales,
home: Scaffold(
body: Padding(
padding: const EdgeInsets.all(16),
child: StatefulBuilder(
builder: (context, setState) => RegionMap(
key: shown,
atlas: atlas,
countryTaps: countryTaps,
around: around,
selected: picked.lastOrNull,
right: right,
wrong: wrong,
namesCountryParts: namesCountryParts,
onRegionTapped: readOnly
? null
: (code) => setState(() => picked.add(code)),
),
),
),
),
),
);
await tester.pumpAndSettle();
return picked;
}
RegionMapState state(WidgetTester tester) =>
tester.state<RegionMapState>(find.byType(RegionMap));
Future<void> tapAt(WidgetTester tester, GeoPoint point) async {
await tester.tapAt(state(tester).globalOffsetOf(point));
await tester.pumpAndSettle();
}
/// The chip [label], also where the row's end hides it.
Finder chip(String label) =>
find.widgetWithText(ActionChip, label, skipOffstage: false);
Finder back(String label) => find.widgetWithText(TextButton, label);
/// Taps the chip [label], scrolling the row of chips to it first.
Future<void> choose(WidgetTester tester, String label) async {
await tester.scrollUntilVisible(
chip(label),
100,
scrollable: find.descendant(
of: find.byType(RegionMap),
matching: find.byType(Scrollable),
),
);
// Until the row stops: a tap on a moving list only stops it.
await tester.pumpAndSettle();
await tester.tap(chip(label));
await tester.pumpAndSettle();
}
/// How far apart [a] and [b] show, in pixels.
double apart(WidgetTester tester, GeoPoint a, GeoPoint b) =>
(state(tester).globalOffsetOf(a) - state(tester).globalOffsetOf(b))
.distance;
const tokyo = (139.69, 35.69);
testWidgets("while answering, Japan's parts have no chips, which would "
'name the answer or where it lies; the map opens them at a tap, and the '
"world's continents keep theirs", (tester) async {
final picked = await show(
tester,
countryTaps: CountryTaps.open,
// The questions studied, all prefectures: on the whole of Japan.
around: {'JP-13', 'JP-27'},
namesCountryParts: false,
);
expect(back('East Asia'), findsOneWidget);
expect(chip('Hokkaidō'), findsNothing);
expect(chip('Kantō'), findsNothing);
await tapAt(tester, tokyo);
expect(find.text('Kantō'), findsOneWidget);
await tapAt(tester, tokyo);
expect(picked, ['JP-13']);
await tester.tap(back('Japan'));
await tester.pumpAndSettle();
expect(chip('Okinawa'), findsNothing);
await tester.tap(back('East Asia'));
await tester.pumpAndSettle();
await tester.tap(back('World'));
await tester.pumpAndSettle();
expect(chip('Europe'), findsOneWidget);
});
testWidgets('Japan is picked in East Asia, and once it is, a chip opens '
'its prefectures in layers; Back goes up one', (tester) async {
final picked = await show(tester);
await choose(tester, 'East Asia');
expect(chip('Prefectures'), findsNothing);
await tapAt(tester, tokyo);
expect(picked, ['JP']);
await tester.tap(chip('Prefectures'));
await tester.pumpAndSettle();
expect(chip('Kantō'), findsOneWidget);
expect(back('East Asia'), findsOneWidget);
// Tokyo, on the whole of Japan: Kantō opens, nothing is picked yet.
await tapAt(tester, tokyo);
expect(chip('Kantō'), findsNothing);
expect(find.text('Kantō'), findsOneWidget);
expect(picked, ['JP']);
await tapAt(tester, tokyo);
expect(picked, ['JP', 'JP-13']);
await tester.tap(back('Japan'));
await tester.pumpAndSettle();
await choose(tester, 'Kansai');
// Osaka Castle.
await tapAt(tester, (135.53, 34.69));
expect(picked, ['JP', 'JP-13', 'JP-27']);
await tester.tap(back('Japan'));
await tester.pumpAndSettle();
await tester.tap(back('East Asia'));
await tester.pumpAndSettle();
expect(find.text('East Asia'), findsOneWidget);
// Osaka is within Japan; Seoul isn't.
expect(chip('Prefectures'), findsOneWidget);
await tapAt(tester, (126.98, 37.57));
expect(picked.last, 'KR');
expect(chip('Prefectures'), findsNothing);
await tester.tap(back('World'));
await tester.pumpAndSettle();
expect(chip('Europe'), findsOneWidget);
expect(find.byType(TextButton), findsNothing);
});
testWidgets('studying a prefecture, a tap on Japan opens it; studying a '
'country, it picks Japan and no chip offers the prefectures', (
tester,
) async {
var picked = await show(tester, countryTaps: CountryTaps.open);
await choose(tester, 'East Asia');
expect(chip('Prefectures'), findsNothing);
await tapAt(tester, tokyo);
expect(picked, isEmpty);
expect(chip('Kantō'), findsOneWidget);
picked = await show(tester, countryTaps: CountryTaps.pick);
await choose(tester, 'East Asia');
expect(chip('Prefectures'), findsNothing);
await tapAt(tester, tokyo);
expect(picked, ['JP']);
});
testWidgets('a region picked comes to the middle of the map, at the zoom '
'it had, as far as the layer still fills it: shown whole, it stays '
'put', (tester) async {
final picked = await show(tester, around: {'JP-13', 'JP-27'});
await choose(tester, 'Kantō');
final canvas = tester.getRect(
find.descendant(
of: find.byType(RegionMap),
matching: find.byType(ClipRRect),
),
);
final chiba = atlas.region('JP-12')!.marker;
final whole = state(tester).globalOffsetOf(chiba);
await tapAt(tester, chiba);
expect(picked, ['JP-12']);
expect(state(tester).globalOffsetOf(chiba), whole);
// Zoomed in around Chiba, it stays where it was, off the middle, and
// comes to it.
final mouse = TestPointer(1, PointerDeviceKind.mouse);
await tester.sendEventToBinding(
mouse.hover(state(tester).globalOffsetOf(chiba)),
);
await tester.sendKeyDownEvent(LogicalKeyboardKey.controlLeft);
await tester.sendEventToBinding(mouse.scroll(const Offset(0, -200)));
await tester.sendKeyUpEvent(LogicalKeyboardKey.controlLeft);
await tester.pumpAndSettle();
final zoom = apart(tester, tokyo, chiba);
expect(
(state(tester).globalOffsetOf(chiba) - canvas.center).distance,
greaterThan(20),
);
await tapAt(tester, chiba);
expect(picked, ['JP-12', 'JP-12']);
expect(
(state(tester).globalOffsetOf(chiba) - canvas.center).distance,
lessThan(2),
);
expect(apart(tester, tokyo, chiba), closeTo(zoom, 0.5));
});
testWidgets('a map that grows or shrinks keeps where it shows and its zoom, '
'never shown whole again', (tester) async {
tester.view
..physicalSize = const Size(400, 640)
..devicePixelRatio = 1;
addTearDown(tester.view.reset);
var height = 500.0;
late StateSetter resize;
await tester.pumpWidget(
MaterialApp(
theme: learnTheme(Brightness.light),
localizationsDelegates: appLocalizationsDelegates,
supportedLocales: AppLocalizations.supportedLocales,
home: Scaffold(
body: StatefulBuilder(
builder: (context, setState) {
resize = setState;
return SizedBox(
height: height,
child: RegionMap(atlas: atlas, around: const {'JP-13'}),
);
},
),
),
),
);
await tester.pumpAndSettle();
final mouse = TestPointer(1, PointerDeviceKind.mouse);
await tester.sendEventToBinding(
mouse.hover(state(tester).globalOffsetOf(tokyo)),
);
await tester.sendKeyDownEvent(LogicalKeyboardKey.controlLeft);
await tester.sendEventToBinding(mouse.scroll(const Offset(0, -200)));
await tester.sendKeyUpEvent(LogicalKeyboardKey.controlLeft);
await tester.pumpAndSettle();
const kyoto = (135.77, 35.01);
final zoomed = apart(tester, tokyo, kyoto);
// As a caption below it grows by a line.
resize(() => height = 470);
await tester.pumpAndSettle();
expect(apart(tester, tokyo, kyoto), closeTo(zoomed, 0.5));
});
testWidgets('a group of one region picks it at once', (tester) async {
final picked = await show(tester, around: {'JP-13', 'JP-27'});
await choose(tester, 'Hokkaidō');
// Naha, on Okinawa's main island.
await tapAt(tester, (127.68, 26.21));
expect(picked, ['JP-01', 'JP-47']);
expect(chip('Kantō'), findsOneWidget);
});
testWidgets('a country in two groups opens the one nearer the tap: '
'Moscow Europe, Siberia East Asia', (tester) async {
await show(tester);
await tapAt(tester, (37.6, 55.75));
expect(find.text('Europe'), findsOneWidget);
await tester.tap(back('World'));
await tester.pumpAndSettle();
await tapAt(tester, (100, 62));
expect(find.text('East Asia'), findsOneWidget);
});
testWidgets('Antarctica, in no group, is picked on the whole world', (
tester,
) async {
final picked = await show(tester);
await tapAt(tester, (0, -80));
expect(picked, ['AQ']);
});
testWidgets('a tiny country is picked on its ring and a tap beside it '
'picks the land there, without zooming; Ctrl and scroll zoom', (
tester,
) async {
final picked = await show(tester);
await choose(tester, 'Europe');
final liechtenstein = atlas.region('LI')!.marker;
const zurich = (8.54, 47.37);
final before = apart(tester, liechtenstein, zurich);
await tapAt(tester, liechtenstein);
expect(picked, ['LI']);
// In Switzerland, just off Liechtenstein's ring.
await tester.tapAt(
state(tester).globalOffsetOf(liechtenstein) - const Offset(12, 0),
);
await tester.pumpAndSettle();
expect(picked, ['LI', 'CH']);
expect(apart(tester, liechtenstein, zurich), before);
// Scrolling alone leaves the map, and says how to zoom.
final mouse = TestPointer(1, PointerDeviceKind.mouse);
await tester.sendEventToBinding(
mouse.hover(state(tester).globalOffsetOf(liechtenstein)),
);
await tester.sendEventToBinding(mouse.scroll(const Offset(0, -200)));
await tester.pump();
expect(apart(tester, liechtenstein, zurich), before);
final note = find.ancestor(
of: find.text('Ctrl + scroll to zoom'),
matching: find.byType(AnimatedOpacity),
);
expect(tester.widget<AnimatedOpacity>(note).opacity, 1);
await tester.pumpAndSettle(const Duration(seconds: 2));
expect(tester.widget<AnimatedOpacity>(note).opacity, 0);
await tester.sendKeyDownEvent(LogicalKeyboardKey.controlLeft);
await tester.sendEventToBinding(mouse.scroll(const Offset(0, -200)));
await tester.sendKeyUpEvent(LogicalKeyboardKey.controlLeft);
await tester.pump();
expect(apart(tester, liechtenstein, zurich), greaterThan(before * 2));
expect(tester.widget<AnimatedOpacity>(note).opacity, 0);
});
testWidgets('across the antimeridian, Samoa is picked on its ring though '
"American Samoa's is near", (tester) async {
final picked = await show(tester);
await choose(tester, 'Oceania');
await tapAt(tester, atlas.region('WS')!.marker);
expect(picked, ['WS']);
});
testWidgets('read-only, the layers still open, but nothing is picked', (
tester,
) async {
final picked = await show(tester, readOnly: true);
await choose(tester, 'Europe');
await tapAt(tester, atlas.region('LI')!.marker);
expect(find.text('Europe'), findsOneWidget);
expect(picked, isEmpty);
});
testWidgets('it opens on the deepest layer showing what it is around', (
tester,
) async {
await show(tester, around: {'FR', 'DE'});
expect(find.text('Europe'), findsOneWidget);
expect(back('World'), findsOneWidget);
await show(tester, around: {'JP-13', 'JP-27'});
expect(chip('Kantō'), findsOneWidget);
expect(back('East Asia'), findsOneWidget);
await show(tester, around: {'JP-13', 'JP-14'});
expect(find.text('Kantō'), findsOneWidget);
expect(back('Japan'), findsOneWidget);
await show(tester, around: {'FR', 'JP-13'});
expect(chip('Europe'), findsOneWidget);
expect(find.byType(TextButton), findsNothing);
});
testWidgets('checked, it opens on the layer of the right and the wrong '
'region, or on the whole world when they share none', (tester) async {
await show(tester, right: 'JP-13', wrong: 'JP-14');
expect(find.text('Kantō'), findsOneWidget);
expect(chip('Kantō'), findsNothing);
await show(tester, right: 'JP-13', wrong: 'KR');
expect(find.text('East Asia'), findsOneWidget);
await show(tester, right: 'CY', wrong: 'JP');
expect(chip('Europe'), findsOneWidget);
});
testWidgets('once checked, it flies to a layer showing the right and the '
'wrong region, unless it shows them already', (tester) async {
const key = ValueKey('map');
await show(tester, key: key, around: {'JP-13', 'JP-14'});
expect(find.text('Kantō'), findsOneWidget);
await show(tester, key: key, right: 'JP-13', wrong: 'JP-14');
expect(find.text('Kantō'), findsOneWidget);
await show(tester, key: key, right: 'JP-13', wrong: 'KR');
expect(find.text('East Asia'), findsOneWidget);
});
testWidgets('every layer zooms, the whole world and Japan too', (
tester,
) async {
await show(tester);
const kyoto = (135.77, 35.01);
final mouse = TestPointer(1, PointerDeviceKind.mouse);
Future<void> zoomIn() async {
await tester.sendEventToBinding(
mouse.hover(state(tester).globalOffsetOf(tokyo)),
);
await tester.sendKeyDownEvent(LogicalKeyboardKey.controlLeft);
await tester.sendEventToBinding(mouse.scroll(const Offset(0, -200)));
await tester.sendKeyUpEvent(LogicalKeyboardKey.controlLeft);
await tester.pump();
}
var before = apart(tester, tokyo, kyoto);
await zoomIn();
expect(apart(tester, tokyo, kyoto), greaterThan(before * 2));
await show(tester, around: {'JP-13', 'JP-27'});
expect(back('East Asia'), findsOneWidget);
before = apart(tester, tokyo, kyoto);
await zoomIn();
expect(apart(tester, tokyo, kyoto), greaterThan(before * 2));
});
testWidgets('in a page that scrolls, the map is never taller than its '
'share of the screen, so all of it shows while zooming', (
tester,
) async {
tester.view
..physicalSize = const Size(1600, 900)
..devicePixelRatio = 1;
addTearDown(tester.view.reset);
await tester.pumpWidget(
MaterialApp(
theme: learnTheme(Brightness.light),
localizationsDelegates: appLocalizationsDelegates,
supportedLocales: AppLocalizations.supportedLocales,
home: Scaffold(
body: SingleChildScrollView(
child: RegionMap(atlas: atlas, onRegionTapped: (_) {}),
),
),
),
);
await tester.pumpAndSettle();
final map = tester.getSize(
find.descendant(
of: find.byType(RegionMap),
matching: find.byType(ClipRRect),
),
);
expect(map.width, 1600);
expect(map.height, 900 * RegionMapState.screenShare);
});
}