import 'package:flutter_test/flutter_test.dart';
import 'package:consumer_app/core/config/app_config.dart';
import 'package:consumer_app/l10n/app_localizations_de.dart';
import 'package:consumer_app/l10n/backend_localizations.dart';
void main() {
test('uses a safe provider-neutral geo fallback', () {
final config = AppConfig.fallback();
expect(
config.apiBaseUrl.toString(),
'http://localhost:8888/MeinWeimar/WORDPRESSNEU/wp-json/meinweimar/v1',
);
expect(config.geo.dataBasis, 'wordpress-geo-config');
expect(config.geo.provider, 'unconfigured');
expect(config.geo.defaultMapZoom, 13);
expect(config.geo.defaultLatitude, closeTo(50.9795, 0.0001));
expect(config.geo.defaultLongitude, closeTo(11.3235, 0.0001));
expect(config.geo.mapConfigured, isFalse);
expect(config.geo.tileUrlTemplate, isEmpty);
expect(config.geo.attribution, isEmpty);
expect(config.geo.routingConfigured, isFalse);
expect(config.geo.navigationProvider, 'google_maps');
expect(config.geo.externalNavigationUrlTemplate, isEmpty);
expect(config.geo.geocodingConfigured, isFalse);
});
test('maps centrally configured geo services without inventing URLs', () {
final config = AppConfig.fromJson({
'geo': {
'data_basis': 'openstreetmap',
'provider': 'osm',
'default_map_zoom': 15,
'default_radius_km': 0.1,
'default_latitude': 50.98,
'default_longitude': 11.33,
'map_configured': true,
'tile_url_template': 'https://tiles.example/{z}/{x}/{y}.png',
'tile_styles': {
'dark': 'https://dark.example/{z}/{x}/{y}.png',
'satellite': '',
},
'routing_url': 'https://routing.example/route',
'navigation_provider': 'google_maps',
'external_navigation_url_template':
'https://navigate.example/?to={coordinates}',
'geocoding_url': '',
'attribution': 'Map data',
'user_agent_contact': 'contact@example.test',
},
}, baseUrl: Uri.parse('https://example.test/wp-json/meinweimar/v1'));
expect(config.geo.mapConfigured, isTrue);
expect(config.geo.defaultMapZoom, 15);
expect(config.geo.defaultRadiusKm, 0.1);
expect(config.geo.defaultLatitude, 50.98);
expect(config.geo.tileStyles['dark'], contains('dark.example'));
expect(config.geo.tileUrlTemplate, contains('{z}/{x}/{y}'));
expect(config.geo.routingConfigured, isTrue);
expect(config.geo.navigationProvider, 'google_maps');
expect(
config.geo.externalNavigationUrlTemplate,
'https://navigate.example/?to={coordinates}',
);
expect(config.geo.geocodingConfigured, isFalse);
expect(config.geo.attribution, 'Map data');
});
test('maps the centrally configured GPS marker', () {
final config = AppConfig.fromJson({
'geo': {
'markers': {
'user': {
'kind': 'user',
'app_url': 'https://example.test/wp-json/meinweimar/v1/media/9',
'image_url': 'https://example.test/user-location.png',
'source': 'user',
},
},
},
});
expect(config.geo.userMarker['kind'], 'user');
expect(
config.geo.userMarker['app_url'],
'https://example.test/wp-json/meinweimar/v1/media/9',
);
});
test(
'fails closed when a stale API payload says configured without tiles',
() {
final config = AppConfig.fromJson(<String, dynamic>{
'geo': <String, dynamic>{
'provider': 'configured',
'map_configured': false,
'tile_url_template': '',
'tile_styles': <String, dynamic>{},
},
}, baseUrl: Uri.parse('http://localhost:8888/'));
expect(config.geo.mapConfigured, isFalse);
expect(config.geo.provider, 'unconfigured');
},
);
test('fails closed when tile metadata is incomplete', () {
final config = AppConfig.fromJson(<String, dynamic>{
'geo': <String, dynamic>{
'map_configured': true,
'tile_url_template': 'https://tiles.example/{z}/{x}/{y}.png',
'attribution': '',
'user_agent_contact': '',
},
}, baseUrl: Uri.parse('http://localhost:8888/'));
expect(config.geo.mapConfigured, isFalse);
expect(config.geo.provider, 'unconfigured');
});
test('maps light and dark logo variants from the central brand config', () {
final config = AppConfig.fromJson({
'brand': {
'wordmark': 'MeinWeimar',
'logo_light': {
'url': 'https://example.test/light.svg',
'app_url': 'https://example.test/wp-json/meinweimar/v1/media/1',
'alt': 'Light logo',
},
'logo_dark': {
'url': 'https://example.test/dark.svg',
'app_url': 'https://example.test/wp-json/meinweimar/v1/media/2',
'alt': 'Dark logo',
},
'navigation_logo': {
'url': 'https://example.test/navigation.svg',
'app_url': 'https://example.test/wp-json/meinweimar/v1/media/5',
'alt': 'Navigation logo',
},
'primary_color': '#ff1e6e',
'secondary_color': '#20242e',
},
});
expect(
config.brand.logoLightUrl,
'https://example.test/wp-json/meinweimar/v1/media/1',
);
expect(config.brand.logoLightAlt, 'Light logo');
expect(
config.brand.logoDarkUrl,
'https://example.test/wp-json/meinweimar/v1/media/2',
);
expect(config.brand.logoDarkAlt, 'Dark logo');
expect(config.brand.logoUrl, config.brand.logoLightUrl);
expect(
config.brand.navigationLogoUrl,
'https://example.test/wp-json/meinweimar/v1/media/5',
);
expect(config.brand.navigationLogoAlt, 'Navigation logo');
expect(config.brand.navigationLogoIsSvg, isTrue);
});
test('maps centrally configured navigation presentation fields', () {
final config = AppConfig.fromJson({
'navigation': [
{
'id': 'map',
'label': 'Karte',
'label_key': 'navMap',
'icon': 'map',
'route': '/karte/',
'enabled': true,
'primary': true,
},
{
'id': 'classifieds',
'label': 'Kleinanzeigen',
'label_key': 'moduleClassifieds',
'icon': 'sell',
'route': '/kleinanzeigen/',
'enabled': true,
'primary': false,
},
],
});
expect(config.navigation.first.id, 'map');
expect(config.navigation.first.labelKey, 'navMap');
expect(config.navigation.first.icon, 'map');
expect(config.navigation.first.primary, isTrue);
expect(config.navigation.last.labelKey, 'moduleClassifieds');
expect(config.navigation.last.primary, isFalse);
});
test('maps ordered localized home sections and custom heading icons', () {
final config = AppConfig.fromJson({
'home_sections': [
{
'id': 'events',
'enabled': true,
'title': {'de_DE': 'Was heute los ist', 'en_US': 'What is on today'},
'title_key': 'homeEventsTitle',
'icon': 'calendar_today',
'icon_image': {
'app_url': 'https://example.test/events-heading.svg',
'alt': 'Events',
},
},
{
'id': 'quick_access',
'enabled': false,
'title': {'de_DE': 'Einstiege'},
'title_key': 'homeQuickAccessTitle',
'icon': 'bolt',
'items': [
{
'route': 'places',
'category': 'essen-trinken',
'label': {'de_DE': 'Essen & Trinken'},
'image': {
'app_url': 'https://example.test/food.jpg',
'alt': 'Essen & Trinken',
},
},
],
},
],
});
expect(config.homeSections.map((section) => section.id), [
'events',
'quick_access',
]);
expect(config.homeSections.first.title['de_DE'], 'Was heute los ist');
expect(config.homeSections.first.icon, 'calendar_today');
expect(
config.homeSections.first.iconUrl,
'https://example.test/events-heading.svg',
);
expect(config.homeSections.last.enabled, isFalse);
expect(config.homeSections.last.quickItems, hasLength(1));
expect(config.homeSections.last.quickItems.single.route, 'places');
expect(
config.homeSections.last.quickItems.single.category,
'essen-trinken',
);
expect(
config.homeSections.last.quickItems.single.imageUrl,
'https://example.test/food.jpg',
);
});
test('maps backend hero, palette, menu and text configuration', () {
final config = AppConfig.fromJson({
'colors': {
'primary_color': '#123456',
'ink_color': '#20242e',
'accent_color': '#abcdef',
},
'hero': {
'title': {'de_DE': 'Entdecke Weimar'},
'primary_label': {'de_DE': 'Los gehts'},
'primary_url': {'de_DE': '/entdecken/'},
'height_mode': 'image',
'image_fit': 'contain',
'colors': {
'title': '#ffffff',
'body': '#f0f0f0',
'eyebrow': '#ff1e6e',
'button': '#ff1e6e',
'button_text': '#20242e',
'secondary_button': '#20242e',
'secondary_button_text': '#ffffff',
},
'image': {
'url': 'https://example.test/hero.jpg',
'app_url': 'https://example.test/wp-json/meinweimar/v1/media/3',
'width': 1600,
'height': 900,
},
},
'app_navigation_configured': true,
'app_navigation': {
'bottom': [
{
'id': 'classifieds',
'label_key': 'moduleClassifieds',
'icon': 'sell',
'icon_image': {
'url': 'https://example.test/classifieds.svg',
'app_url': 'https://example.test/wp-json/meinweimar/v1/media/4',
'alt': 'Kleinanzeigen',
},
},
],
'fullscreen': [
{'id': 'jobs', 'label_key': 'moduleJobs', 'icon': 'work'},
],
},
'app_texts': {
'de_DE': {'homeQuickAccessTitle': 'Willkommen in Weimar'},
},
});
expect(config.colors.primary, 0xFF123456);
expect(config.colors.ink, 0xFF20242E);
expect(config.hero.title['de_DE'], 'Entdecke Weimar');
expect(config.hero.heightMode, 'image');
expect(config.hero.imageFit, 'contain');
expect(config.hero.imageWidth, 1600);
expect(config.hero.imageHeight, 900);
expect(config.hero.colors.button, '#ff1e6e');
expect(config.hero.colors.buttonText, '#20242e');
expect(
config.hero.imageUrl,
'https://example.test/wp-json/meinweimar/v1/media/3',
);
expect(
config.bottomNavigation.single.iconUrl,
'https://example.test/wp-json/meinweimar/v1/media/4',
);
expect(config.fullscreenNavigation.single.id, 'jobs');
expect(
config.appTexts['de_DE']?['homeQuickAccessTitle'],
'Willkommen in Weimar',
);
});
test('maps the shared localized module surface hero contract', () {
final config = AppConfig.fromJson({
'surface_heroes': {
'jobs': {
'title': {'de_DE': 'Jobs in Weimar'},
'body': {'de_DE': 'Lokale Stellenangebote'},
'image': {
'app_url': 'https://example.test/jobs-hero.jpg',
'alt': 'Jobs in Weimar',
'width': 1200,
'height': 640,
},
'image_fit': 'contain',
'colors': {
'title': '#ffffff',
'body': '#f0f0f0',
'button': '#ff1e6e',
'button_text': '#20242e',
},
},
},
});
final hero = config.surfaceHeroes['jobs'];
expect(hero, isNotNull);
expect(hero!.title['de_DE'], 'Jobs in Weimar');
expect(hero.body['de_DE'], 'Lokale Stellenangebote');
expect(hero.imageUrl, 'https://example.test/jobs-hero.jpg');
expect(hero.imageFit, 'contain');
expect(hero.colors.button, '#ff1e6e');
expect(hero.colors.buttonText, '#20242e');
});
test('uses backend UI text overrides with generated locale fallback', () {
final base = AppLocalizationsDe();
final localizations = BackendAppLocalizations(
base: base,
overrides: {
'homeQuickAccessTitle': 'Individueller Schnellzugriff',
'resultsCount': '{count} Treffer aus dem Backend',
},
);
expect(localizations.homeQuickAccessTitle, 'Individueller Schnellzugriff');
expect(localizations.resultsCount(3), '3 Treffer aus dem Backend');
expect(localizations.navStart, 'Start');
});
test(
'keeps empty configured navigation surfaces available for canonical fallback',
() {
final config = AppConfig.fromJson({
'app_navigation_configured': true,
'app_navigation': <String, dynamic>{},
});
expect(config.bottomNavigation, isEmpty);
expect(config.fullscreenNavigation, isEmpty);
},
);
test(
'maps the central content-type field schema without inventing fields',
() {
final config = AppConfig.fromJson({
'content_schemas': {
'places': {
'address.line1': {
'type': 'string',
'required': true,
'input': 'text',
'read_only': false,
'values': [],
},
'place_details.rating_average': {
'type': 'number',
'required': false,
'input': 'rating',
'read_only': false,
'values': [],
'filter': {'enabled': true, 'kind': 'range'},
'seo': {'indexable': true},
'privacy': 'public',
},
},
'jobs': {
'listing.salary_min': {
'type': 'number',
'required': false,
'input': 'number',
'read_only': false,
'unit': 'currency',
'filter': {'enabled': true, 'kind': 'range'},
'conditions': [
{
'field': 'listing.offer_type',
'operator': 'equals',
'value': 'employment',
},
],
'seo': {'indexable': true},
'privacy': 'public',
},
'listing.employment_type': {
'type': 'string',
'required': false,
'input': 'enum',
'read_only': false,
'values': ['full_time', 'part_time'],
},
},
},
});
expect(
config.contentSchemas['places']?['address.line1']?.required,
isTrue,
);
expect(
config.contentSchemas['places']?['place_details.rating_average']?.type,
'number',
);
final salary = config.contentSchemas['jobs']?['listing.salary_min'];
expect(salary?.unit, 'currency');
expect(
config.contentSchemas['jobs']?['listing.employment_type']?.values,
['full_time', 'part_time'],
);
expect(salary?.filterEnabled, isTrue);
expect(salary?.filterKind, 'range');
expect(salary?.conditions.single['operator'], 'equals');
expect(salary?.seoIndexable, isTrue);
expect(config.contentSchemas.containsKey('jobs'), isTrue);
},
);
test(
'maps category profiles and field conditions from the WordPress contract',
() {
final config = AppConfig.fromJson({
'content_categories': {
'classifieds': {
'taxonomy': 'mw_classified_category',
'profiles': {
'electronics': {
'slugs': ['electronics', 'elektronik'],
'labels': {'electronics': 'Electronics'},
'fields': ['brand', 'model'],
'required': ['condition'],
},
},
},
},
'content_schemas': {
'classifieds': {
'listing.brand': {
'type': 'string',
'required': false,
'input': 'text',
'category_conditions': ['electronics'],
'conditions': [
{
'field': 'category',
'operator': 'in',
'values': ['electronics'],
},
],
},
},
},
});
expect(
config.contentCategories['classifieds']?.taxonomy,
'mw_classified_category',
);
expect(
config
.contentCategories['classifieds']
?.profiles['electronics']
?.fields,
['brand', 'model'],
);
expect(
config
.contentCategories['classifieds']
?.profiles['electronics']
?.labels['electronics'],
'Electronics',
);
expect(
config
.contentSchemas['classifieds']?['listing.brand']
?.categoryConditions,
['electronics'],
);
},
);
test('maps fixed-field icon media separately from content schemas', () {
final config = AppConfig.fromJson({
'field_icons': {
'experience_years': {
'url': 'https://example.test/icons/experience.svg',
'app_url': 'https://example.test/media/42',
'alt': 'Erfahrung',
},
},
'content_schemas': {
'jobs': {
'listing.experience_years': {
'type': 'number',
'required': false,
'input': 'number',
'read_only': false,
},
},
},
});
expect(
config.fieldIcons['experience_years']?.url,
'https://example.test/media/42',
);
expect(config.fieldIcons['experience_years']?.isSvg, isTrue);
expect(
config.contentSchemas['jobs']?['listing.experience_years']?.type,
'number',
);
});
}
