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
4 changed files with 0 additions and 0 deletions
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
Loading…
Reference in a new issue