Files
DriverVault/Phone App/test/models_format_test.dart
T
tajniak81andClaude Opus 5 a203414ceb Phone App: the garage on the car's own screen
The one place a service badge is worth reading is the car it belongs to, and
that is the one place the app could not be opened: Android Auto runs no Flutter
engine, so a Flutter app is simply absent from the head unit. The same APK now
carries a second face — Car App Library templates the host draws itself, in
Kotlin under android/app/src/main/kotlin/com/drivervault/phoneapp/car/.

Two screens. The garage lists a car per row with its due badge on the second
line, worst first, because the host renders only the first handful of rows and
the car this list exists to mention is the overdue one rather than whichever was
added first. A tap opens what that car has coming: the odometer, the next
service, and the reminders the server holds for it — typed in and auto-derived
from documents and the service schedule alike, in the order it sorted them.

None of it is a second implementation of the app. VaultStore reads the session
the phone signed in with — the active server's base and token — out of
shared_preferences' own store, which both halves share, so a server switched on
the phone is the server the car reads from with nothing to keep in step; only
cc_active_base is new, because an untouched home entry carries no address of its
own, its base being kDefaultApiBase, a compile-time define nothing outside Dart
can see. CarStrings reads the same assets/i18n files by the same dot paths, so a
badge on the head unit is the string format.dart already puts on the phone, in
the language the account chose: of the 32 keys the car screens ask for, 28 are
keys a phone screen already used, and only carApp.* is theirs. CarFormat is
format.dart's twin — same date pattern and number grouping from the account's
settings, same worst-of-date-and-km service badge. A new test reads the Kotlin
for the keys it looks up and fails if any is missing from a language file, since
the analyzer's reach stops at the Dart.

It only reads. A screen you cannot type into is a poor place to edit a car and a
driver is a poor person to ask, so VaultApi has no write in it to reach for by
accident.

Three things the README now says out loud. The service is declared IOT, the
closest category the library defines for something that is a garage rather than
a map or a media player, which matters to a store submission and not to a
sideload. The app lock does not reach the head unit: the flag is in memory and
the credentials behind it in encrypted storage, neither readable from the car
service, and there is no fingerprint reader in a dashboard to satisfy it with.
And the home charger is not on there — the chargers endpoint relays its plugin's
payload verbatim with no shape to read, and the serial the control card is
driven by is never persisted, so the car would have nothing to name.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-29 18:26:46 +02:00

585 lines
24 KiB
Dart

// Parses server-shaped JSON through the models and checks the status/format
// helpers.
//
// These cover the parts where a mistake is invisible until it reaches a user: a
// derived field the server left out must stay null rather than becoming a
// plausible-looking zero, the badge wording has to match the web app's, and an
// unsupported locale (settable from the web, whose lists are wider than the
// phone's) must not throw out of every date on screen.
import "dart:convert";
import "dart:io";
import "package:flutter_test/flutter_test.dart";
import "package:intl/date_symbol_data_local.dart";
import "package:drivervault_phone/format.dart";
import "package:drivervault_phone/i18n.dart";
import "package:drivervault_phone/main.dart";
import "package:drivervault_phone/models.dart";
import "package:drivervault_phone/screens/car_form_sheet.dart";
import "package:drivervault_phone/screens/car_view_sheet.dart";
import "package:drivervault_phone/service_parts.dart";
void main() {
// rootBundle (used by loadTranslations) needs the binding initialised.
TestWidgetsFlutterBinding.ensureInitialized();
setUpAll(() async {
await initializeDateFormatting();
await loadTranslations();
// Polish exercises both the region-aware number grouping and the localized
// status wording (one/few/many plurals), so the badge labels below are the
// Polish strings — the same text the web app renders under pl.
appSettings.locale = "pl-PL";
appSettings.currency = "PLN";
appSettings.dateFormat = "DMY";
});
test("FuelEntry keeps uncomputed derived fields null, not zero", () {
final partial = FuelEntry.fromJson({
"id": "a",
"car": "c",
"date": "2026-07-01T00:00:00Z",
"km": 1000,
"liters": 20.0,
"cost": 120.0,
"fullTank": false,
"hasFile": false,
});
expect(partial.consumptionL100, isNull);
expect(partial.distanceKm, isNull);
expect(formatConsumption(partial.consumptionL100), "—");
final full = FuelEntry.fromJson({
"id": "b",
"car": "c",
"date": "2026-07-10T00:00:00Z",
"km": 1500,
"liters": 35.0,
"cost": 210.0,
"fullTank": true,
"consumptionL100": 6.85,
"distanceKm": 500,
"kmPerLiter": 14.6,
"pricePerLiter": 6.0,
"fileName": "receipt.pdf",
"hasFile": true,
});
expect(full.consumptionL100, 6.85);
// 6.85 as a float64 is really 6.8499…, so one-decimal rounding yields 6.8 —
// the same answer JS toFixed(1) gives the web app.
expect(formatConsumption(full.consumptionL100), "6.8 L/100km");
expect(full.hasFile, isTrue);
expect(full.fileName, "receipt.pdf");
});
test("expiryStatus wording follows the server assessment", () {
CarDocument doc(String state, int? days) => CarDocument.fromJson({
"id": "d",
"car": "c",
"type": "insurance",
"title": "OC",
"expiry": {"state": state, "daysUntilExpiry": days},
"hasFile": false,
});
expect(expiryStatus(doc("expired", -5).expiry).label, "Wygasło 5 dni temu");
expect(expiryStatus(doc("expiring_soon", 0).expiry).label, "Wygasa dzisiaj");
expect(expiryStatus(doc("expiring_soon", 12).expiry).label, "Odnowienie za 12 dni");
expect(expiryStatus(doc("valid", 200).expiry).label, "Ważne · 200 dni");
expect(expiryStatus(doc("no_expiry", null).expiry).label, "Bezterminowe");
expect(expiryStatus(doc("expired", -5).expiry).key, StatusKey.overdue);
});
test("reminderStatus leads with the driving trigger", () {
Reminder rem(Map<String, dynamic> extra) =>
Reminder.fromJson({"id": "r", "car": "c", "title": "t", "type": "service", ...extra});
expect(reminderStatus(rem({"status": "done", "done": true})).label, "Gotowe");
expect(reminderStatus(rem({"status": "no_trigger"})).label, "Brak wyzwalacza");
expect(
reminderStatus(rem({"status": "overdue", "daysLeft": -3, "kmLeft": -200})).label,
"Zaległe 3 dni · 200 km");
expect(reminderStatus(rem({"status": "due_soon", "daysLeft": 0})).label, "Termin za dzisiaj");
// The km count is grouped per the chosen locale (pl-PL groups thousands with
// a space), which is the whole point of routing every number through the one
// helper. Asserted as start + end so the exact space glyph (ICU uses a
// non-breaking space) does not make the test brittle.
final due = reminderStatus(rem({"status": "upcoming", "daysLeft": 40, "kmLeft": 5000})).label;
expect(due, startsWith("Termin za 40 dni · 5"));
expect(due, endsWith("000 km"));
});
test("TechnicalCheck: a failed check derives no next date", () {
final failed = TechnicalCheck.fromJson({
"id": "t",
"car": "c",
"date": "2026-07-01T00:00:00Z",
"result": "failed",
"cost": 99.0,
"expiry": {"state": "no_expiry", "daysUntilExpiry": null},
"hasFile": false,
});
expect(failed.nextCheckDate, isNull);
expect(failed.passed, isFalse);
});
test("Maintenance derived warranty + total", () {
final m = MaintenanceEntry.fromJson({
"id": "m",
"car": "c",
"date": "2026-07-01T00:00:00Z",
"km": 1000,
"type": "repair",
"status": "completed",
"laborCost": 100.0,
"partsCost": 50.0,
"totalCost": 150.0,
"warrantyActive": true,
"warrantyDaysLeft": 10,
"hasFile": false,
});
expect(m.totalCost, 150.0);
expect(warrantyStatus(m)!.label, "Gwarancja kończy się za 10 dni");
expect(warrantyStatus(m)!.key, StatusKey.soon);
final noWarranty = MaintenanceEntry.fromJson(
{"id": "m", "car": "c", "date": "2026-07-01T00:00:00Z", "hasFile": false});
expect(warrantyStatus(noWarranty), isNull);
});
test("money and numbers follow the chosen locale/currency", () {
expect(formatMoney(1234.5).contains("zł"), isTrue);
expect(formatMoney(null), "—");
// An unsupported language must not throw — it can be set from the web.
appSettings.locale = "rm-CH";
expect(() => formatDate(DateTime(2026, 7, 17)), returnsNormally);
expect(() => formatMoney(10), returnsNormally);
expect(() => formatKm(15000), returnsNormally);
appSettings.locale = "pl-PL";
});
test("a half-known date prints at its own precision, not padded to a day", () {
// What the web app can now enter for a build date. The phone's picker only
// makes full dates, but it has to read and keep these.
appSettings.dateFormat = "DMY_NUM";
expect(formatPartialDate("2015"), "2015");
expect(formatPartialDate("2015-03"), "03-2015");
expect(formatPartialDate("2015-03-10"), "10-03-2015");
expect(formatPartialDate(""), "—");
// The year and the month must not be shuffled by the local time zone, which
// is what parsing "2015-03" as a UTC instant would risk.
appSettings.dateFormat = "YMD";
expect(formatPartialDate("2015-01"), "2015-01");
expect(formatPartialDate("2015-12"), "2015-12");
appSettings.dateFormat = "DMY_NUM";
});
test("Car carries the technical check interval", () {
final car = Car.fromJson({"id": "c", "name": "Yaris", "technicalCheckIntervalDays": 730});
expect(car.technicalCheckIntervalDays, 730);
});
test("ChargingSession keeps uncomputed derived fields null, not zero", () {
final partial = ChargingSession.fromJson({
"id": "a",
"car": "c",
"date": "2026-07-01T00:00:00Z",
"km": 1000,
"kwh": 22.5,
"cost": 30.0,
"fullCharge": false,
"hasFile": false,
});
expect(partial.consumptionKwh100, isNull);
expect(partial.distanceKm, isNull);
expect(formatKwhConsumption(partial.consumptionKwh100), "—");
expect(formatKmPerKwh(partial.kmPerKwh), "—");
expect(formatKwh(partial.kwh), "22.50 kWh");
final closed = ChargingSession.fromJson({
"id": "b",
"car": "c",
"date": "2026-07-20T00:00:00Z",
"km": 1400,
"kwh": 60.0,
"cost": 90.0,
"fullCharge": true,
"pricePerKwh": 1.5,
"distanceKm": 400,
"consumptionKwh100": 15.0,
"kmPerKwh": 6.67,
});
expect(closed.consumptionKwh100, 15.0);
expect(formatKwhConsumption(closed.consumptionKwh100), "15.0 kWh/100km");
});
test("ChargingStats leaves averages null when nothing is computable", () {
final stats = ChargingStats.fromJson({
"entries": 1,
"totalKwh": 22.5,
"totalCost": 30.0,
"trackedDistanceKm": 0,
});
expect(stats.entries, 1);
expect(stats.avgConsumptionKwh100, isNull);
expect(formatKwhConsumption(stats.avgConsumptionKwh100), "—");
});
test("Car carries the provider link and the view arrangement", () {
final car = Car.fromJson({
"id": "c",
"name": "bZ4X",
"provider": "toyota",
"providerVehicleId": "VIN123",
"hiddenTabs": ["fuel"],
"hiddenFields": ["differentialOil"],
"tabOrder": ["info", "charging"],
"fieldOrder": ["vin"],
"metricOrder": ["evRange", "odometer"],
"hiddenServiceColumns": ["parts"],
"serviceColumnOrder": ["notes", "date"],
});
expect(car.provider, "toyota");
expect(car.providerVehicleId, "VIN123");
expect(car.hiddenTabs, ["fuel"]);
expect(car.tabOrder, ["info", "charging"]);
expect(car.metricOrder, ["evRange", "odometer"]);
expect(car.hiddenServiceColumns, ["parts"]);
expect(car.serviceColumnOrder, ["notes", "date"]);
// A car nobody has arranged carries empty lists, not nulls — the callers
// read them directly.
final plain = Car.fromJson({"id": "d", "name": "Yaris"});
expect(plain.provider, "");
expect(plain.hiddenTabs, isEmpty);
expect(plain.tabOrder, isEmpty);
expect(plain.hiddenServiceColumns, isEmpty);
expect(plain.serviceColumnOrder, isEmpty);
});
test("the Service history columns: date arranges but never hides", () {
// Mirrors hideableServiceColumns/arrangeableServiceColumns in the API's
// cars.go: the arrangeable set is the hideable one plus the date, and the
// server rejects a hidden set that names anything else — so the picker must
// never offer the date as something to switch off.
expect(kHideableServiceColumnKeys, isNot(contains("date")));
expect(kServiceColumnKeys, contains("date"));
expect({...kHideableServiceColumnKeys, "date"}, kServiceColumnKeys.toSet());
// A field key is not a column key. The two catalogues are separate lists of
// similar-looking strings, and sending one where the other belongs is a 400
// from the server with nothing on screen to explain it.
for (final key in kCarInfoFieldKeys) {
expect(kServiceColumnKeys, isNot(contains(key)));
}
// Every part shares the one "parts" column, so adding a part must not add a
// column — that is what keeps the key set from growing without end.
for (final part in kServiceParts) {
expect(kServiceColumnKeys, isNot(contains(part.key)));
}
// A column switched back on returns to where it was rather than to the end,
// because the stored order covers the hidden columns too.
final arranged = arrangeKeys(kServiceColumnKeys, ["notes", "km", "date"]);
expect(arranged.take(3).toList(), ["notes", "km", "date"]);
expect(arranged.toSet(), kServiceColumnKeys.toSet());
});
test("changedParts reads a record through the shared catalogue", () {
ServiceRecord record({bool oil = false, bool engine = false, bool cabin = false}) =>
ServiceRecord.fromJson({
"id": "s",
"car": "c",
"date": "2026-03-01T00:00:00Z",
"km": 90000,
"changedOil": oil,
"changedEngineAirFilter": engine,
"changedCabinAirFilter": cabin,
});
final all = kServiceParts;
expect(changedParts(record(), all).isEmpty, isTrue);
expect(changedParts(record(oil: true), all).map((p) => p.key), ["oil"]);
// Catalogue order, not the order the fields happen to be read in.
expect(
changedParts(record(cabin: true, oil: true), all).map((p) => p.key),
["oil", "cabinFilter"],
);
// A record written before a part existed carries no field for it, which
// reads as "not changed" rather than as a missing value.
final old = ServiceRecord.fromJson({"id": "s", "car": "c", "km": 0});
expect(changedParts(old, all).isEmpty, isTrue);
});
test("a part a car has switched off leaves its records alone", () {
final car = Car.fromJson({"id": "c", "name": "bZ4X", "hiddenServiceParts": ["oil"]});
expect(car.hiddenServiceParts, ["oil"]);
expect(visibleParts(car).map((p) => p.key), ["engineFilter", "cabinFilter"]);
// A car nobody has configured offers every part — the hidden set, so one
// added in a later release is on by default.
expect(visibleParts(Car.fromJson({"id": "d", "name": "Yaris"})).length, kServiceParts.length);
// The chips follow the car, so an oil change recorded before the part was
// switched off stops being shown...
final oiled = ServiceRecord.fromJson({
"id": "s", "car": "c", "km": 90000,
"changedOil": true, "changedCabinAirFilter": true,
});
expect(changedParts(oiled, visibleParts(car)).map((p) => p.key), ["cabinFilter"]);
// ...but the record still says so, which is what brings the chip back if the
// part is switched on again. Nothing here writes to the record.
expect(oiled.changedOil, isTrue);
});
test("arrangeKeys: partial orders keep every key, unknown ones are dropped", () {
// The keys the stored order names lead; the rest follow in catalogue order,
// which is what puts a tab added in a later release at the end of somebody's
// page rather than in the middle of it.
expect(
arrangeKeys(kCarTabKeys, ["charging", "info"]).take(2).toList(),
["charging", "info"],
);
expect(arrangeKeys(kCarTabKeys, ["charging", "info"]).length, kCarTabKeys.length);
expect(arrangeKeys(kCarTabKeys, ["charging", "info"]).toSet(), kCarTabKeys.toSet());
// A key from a newer release, and a duplicate, are both ignored.
final arranged = arrangeKeys(kCarTabKeys, ["tyres", "vin", "fuel", "fuel"]);
expect(arranged.first, "fuel");
expect(arranged.length, kCarTabKeys.length);
// No arrangement at all means the catalogue's own order.
expect(arrangeKeys(kCarInfoFieldKeys, const []), kCarInfoFieldKeys);
});
test("ProviderSnapshot: an unreachable provider parses as an answer, not a failure", () {
final closed = ProviderSnapshot.fromJson({
"provider": "toyota",
"label": "MyToyota",
"unavailable": true,
"detail": "this vehicle is not on your MyToyota account",
});
expect(closed.unavailable, isTrue);
expect(closed.vehicle, isNull);
expect(closed.metrics, isEmpty);
expect(closed.detail, contains("MyToyota"));
final open = ProviderSnapshot.fromJson({
"provider": "toyota",
"label": "MyToyota",
"fetchedAt": "2026-08-21T09:30:00Z",
"vehicle": {"id": "VIN123", "vin": "VIN123", "name": "bZ4X", "make": "Toyota", "year": 2024},
"metrics": [
{"key": "odometer", "value": "16138", "unit": "km"},
{"key": "batteryLevel", "value": "72", "unit": "%"},
],
"sections": [
{"id": "telemetry", "status": "ok", "fields": [{"key": "a.b", "value": "1"}]},
{"id": "notifications", "status": "error", "error": "upstream said no"},
],
"suggestedCurrentKm": 16138,
});
expect(open.vehicle!.subtitle, "Toyota 2024");
expect(open.metrics.first.key, "odometer");
expect(open.sections.last.status, "error");
expect(open.sections.first.fields.single.key, "a.b");
expect(open.suggestedCurrentKm, 16138);
expect(open.fetchedAt, isNotNull);
});
// The car screen looks these up by key at render time — t("car.tabs.$key"),
// t("enums.fuelType.$v") and the rest — so a missing entry is invisible to the
// analyzer and shows up as a raw key path on the screen. Every catalogue the
// UI iterates is checked here, in every language the app ships.
group("every catalogue key has a label", () {
void expectLabelled(String key) {
for (final lang in translatedLanguages) {
appSettings.locale = "$lang-${lang.toUpperCase()}";
expect(t(key), isNot(key), reason: "$key is missing from $lang.json");
expect(t(key).trim(), isNotEmpty, reason: "$key is blank in $lang.json");
}
}
tearDown(() => appSettings.locale = "pl-PL");
test("car tabs", () {
for (final key in kCarTabKeys) {
// "provider" has no key of its own by design: a linked car's tab is
// named after the service ("MyToyota"), and an unlinked one falls back
// to car.tabs.connected. Both apps label it that way.
if (key == "provider") continue;
expectLabelled("car.tabs.$key");
}
expectLabelled("car.tabs.connected");
});
test("Service history columns", () {
// serviceColumnLabel maps rather than derives, so a column added to the
// catalogue without an entry in that map would render as its own key.
for (final key in kServiceColumnKeys) {
for (final lang in translatedLanguages) {
appSettings.locale = "$lang-${lang.toUpperCase()}";
expect(serviceColumnLabel(key), isNot(key),
reason: "the $key column has no label in $lang.json");
expect(serviceColumnLabel(key).trim(), isNotEmpty,
reason: "the $key column is blank in $lang.json");
}
}
});
test("the parts a service can change", () {
// Both wordings: the form's and the card's chip. A part with only one of
// them reaches a screen as a raw key path.
for (final part in kServiceParts) {
expectLabelled(part.label);
expectLabelled(part.chipLabel);
}
});
test("Information rows", () {
for (final key in kCarInfoFieldKeys) {
expectLabelled("car.info.$key");
}
expectLabelled("car.info.technicalCheckInterval");
});
test("the delete dialog's per-collection counts", () {
// Plural objects rather than plain strings, so they are checked through
// the plural path instead of expectLabelled.
for (final key in ["services", "maintenance", "fuel", "charging", "documents", "parts"]) {
for (final lang in translatedLanguages) {
appSettings.locale = "$lang-${lang.toUpperCase()}";
final one = t("car.delete.$key", n: 1);
final many = t("car.delete.$key", n: 5);
expect(one, isNot("car.delete.$key"), reason: "car.delete.$key missing from $lang.json");
expect(one, contains("1"));
expect(many, contains("5"));
}
}
});
test("the enums the pickers and tiles share", () {
for (final key in kFuelTypes) {
expectLabelled("enums.fuelType.$key");
}
for (final key in ["repair", "inspection", "bodywork", "tyres", "diagnostics", "recall",
"warranty", "other"]) {
expectLabelled("enums.maintenanceType.$key");
}
for (final key in ["scheduled", "in_progress", "completed"]) {
expectLabelled("enums.maintenanceStatus.$key");
}
for (final key in ["insurance", "pollution", "registration", "inspection", "roadTax",
"warranty", "other"]) {
expectLabelled("enums.documentType.$key");
}
for (final key in ["maintenance", "document", "service", "inspection", "other"]) {
expectLabelled("enums.reminderType.$key");
}
});
test("the roles the admin screen assigns", () {
// Mirrors the roles in the API's users.go; the picker labels every one it
// offers, including a superadmin's own role shown to an admin who cannot
// assign it.
for (final key in ["user", "admin", "superadmin"]) {
expectLabelled("admin.roles.$key");
}
});
test("the connected service's headline readings", () {
// Mirrors headlineMetricSpecs + unmeasuredMetricKeys in the API's
// vehicleproviders.go: every reading it can report needs a label here.
for (final key in ["odometer", "fuelLevel", "fuelRange", "batteryLevel", "evRange",
"evRangeWithAc", "chargingStatus", "location"]) {
expectLabelled("car.provider.metrics.$key");
}
});
});
test("no key is left behind in a translation", () {
// Every key English has, the other languages have too. t() falls back to
// English for a missing one, so a gap is invisible until someone reads a
// half-translated screen — this is what makes it a test failure instead.
final en = json.decode(
File("assets/i18n/en.json").readAsStringSync()) as Map<String, dynamic>;
List<String> flatten(Map<String, dynamic> tree, [String prefix = ""]) => [
for (final e in tree.entries)
if (e.value is Map<String, dynamic>)
...flatten(e.value as Map<String, dynamic>, "$prefix${e.key}.")
else
"$prefix${e.key}",
];
dynamic lookup(Map<String, dynamic> tree, String key) {
dynamic node = tree;
for (final part in key.split(".")) {
if (node is! Map || !node.containsKey(part)) return null;
node = node[part];
}
return node;
}
final keys = flatten(en);
expect(keys, isNotEmpty);
for (final lang in translatedLanguages.where((l) => l != "en")) {
final other = json.decode(
File("assets/i18n/$lang.json").readAsStringSync()) as Map<String, dynamic>;
final missing = keys.where((k) => lookup(other, k) == null).toList();
expect(missing, isEmpty, reason: "$lang.json is missing ${missing.length} key(s)");
}
});
test("the car screens ask for keys the language files carry", () {
// The Android Auto screens are Kotlin (android/.../phoneapp/car/), where the
// analyzer and every test above stop. They read these very files by these
// very dot paths — CarStrings.kt is i18n.dart's t() over the copies bundled
// into the APK — so a key mistyped there resolves to nothing, and a head
// unit is where you would find out. Read the sources and ask them here.
final dir = Directory("android/app/src/main/kotlin/com/drivervault/phoneapp/car");
final sources = dir.listSync().whereType<File>().where((f) => f.path.endsWith(".kt"));
expect(sources, isNotEmpty, reason: "the car screens moved — point this test at them");
final lookups = RegExp(r'strings\.t\(\s*"([^"]+)"');
final keys = <String>{
for (final file in sources)
...lookups.allMatches(file.readAsStringSync()).map((m) => m.group(1)!),
};
// The car's own strings are in there, so the pattern still matches calls.
expect(keys, contains("carApp.signIn"));
for (final key in keys) {
for (final lang in translatedLanguages) {
appSettings.locale = "$lang-${lang.toUpperCase()}";
// A plural fails this too, deliberately: CarStrings picks no CLDR
// category, so a car screen must not ask for a key that needs one.
expect(t(key), isNot(key), reason: "$key is missing from $lang.json");
}
}
appSettings.locale = "pl-PL";
});
test("a day count reads as prose in each language's own plural forms", () {
appSettings.locale = "en-GB";
expect(t("car.info.daysValue", n: 1), "1 day");
expect(t("car.info.daysValue", n: 365), "365 days");
// Polish splits one/few/many, which an English-style n==1 test would miss.
appSettings.locale = "pl-PL";
expect(t("car.info.daysValue", n: 1), "1 dzień");
expect(t("car.info.daysValue", n: 365), "365 dni");
appSettings.locale = "pl-PL";
});
test("ImportResult reads the server's per-collection counts", () {
final res = ImportResult.fromJson(
{"carsImported": 2, "servicesImported": 11, "partsImported": 4});
expect(res.cars, 2);
expect(res.services, 11);
expect(res.parts, 4);
});
}