// The view picker and the layout it drives. // // What the picker saves is a property of the car, read by the web app as well, // so the two things worth guarding are that it offers exactly the keys the // server accepts — with the date shown but locked on, since a hidden date is a // 400 — and that a service card lays the columns out in the order it was given // rather than the catalogue's. import "package:flutter/material.dart"; import "package:flutter_test/flutter_test.dart"; import "package:intl/date_symbol_data_local.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_view_sheet.dart"; import "package:drivervault_phone/service_parts.dart"; void main() { TestWidgetsFlutterBinding.ensureInitialized(); setUpAll(() async { await initializeDateFormatting(); await loadTranslations(); appSettings.locale = "en-GB"; appSettings.dateFormat = "DMY_NUM"; }); group("serviceColumnRuns", () { test("the short columns share a line, the other three stand alone", () { expect( serviceColumnRuns(kServiceColumnKeys), [ ["date", "km", "nextDate", "nextKm"], ["parts"], ["notes"], ["file"], ], ); }); test("a column moved between two short ones splits the line", () { // The grouping follows the car's arrangement, not the catalogue's: this is // the part a hand-written card would get wrong by grouping the short // columns first and laying the blocks out after them, which would quietly // undo the arrangement. expect( serviceColumnRuns(["date", "km", "notes", "nextDate", "nextKm"]), [ ["date", "km"], ["notes"], ["nextDate", "nextKm"], ], ); }); test("hidden columns simply are not there", () { expect( serviceColumnRuns(["date", "notes"]), [ ["date"], ["notes"], ], ); // Date cannot be hidden, so a card always has at least one run — the tile // reads pieces.first without checking. expect(serviceColumnRuns(["date"]), [ ["date"] ]); }); }); group("CarViewSheet", () { // A surface tall enough for the whole sheet. Its list is lazy, so on a // phone-sized one the columns — the last of the three sections — are simply // not built and nothing below can be found. Future pump(WidgetTester tester, Car car) async { tester.view.physicalSize = const Size(800, 5000); tester.view.devicePixelRatio = 1.0; addTearDown(tester.view.reset); await tester.pumpWidget(MaterialApp( home: Scaffold( body: CarViewSheet(car: car, showProvider: false), ), )); await tester.pumpAndSettle(); } Car car({ List hiddenColumns = const [], List columnOrder = const [], List hiddenParts = const [], }) => Car.fromJson({ "id": "car1", "name": "bZ4X", "hiddenServiceColumns": hiddenColumns, "serviceColumnOrder": columnOrder, "hiddenServiceParts": hiddenParts, }); // The sheet stacks its sections in one list: tabs, Information rows, the // Service history columns, then the parts. Counted from the end rather than // the start so the tab rows — whose number depends on showProvider — don't // have to be worked out here. List ticks(WidgetTester tester) => tester .widgetList(find.byType(Checkbox)) .map((b) => b.value ?? false) .toList(); List columnTicks(WidgetTester tester) { final all = ticks(tester); final end = all.length - kServiceParts.length; return all.sublist(end - kServiceColumnKeys.length, end); } List partTicks(WidgetTester tester) { final all = ticks(tester); return all.sublist(all.length - kServiceParts.length); } testWidgets("offers every column, with the date locked on", (tester) async { await pump(tester, car()); // The section headings are rendered upper-cased. expect(find.text(t("car.viewPicker.serviceColumnsHeading").toUpperCase()), findsOneWidget); for (final key in kServiceColumnKeys) { expect(find.text(serviceColumnLabel(key)), findsWidgets, reason: "the $key column is not on offer"); } // The date's checkbox is on and cannot be turned off; every other column's // can. A picker that let the date be unticked would save a hidden set the // server refuses. final boxes = tester.widgetList(find.byType(Checkbox)).toList(); final locked = boxes.where((b) => b.onChanged == null).toList(); expect(locked, hasLength(1)); expect(locked.single.value, isTrue); }); testWidgets("a switched-off column comes back unticked", (tester) async { await pump(tester, car(hiddenColumns: ["parts", "file"])); expect(columnTicks(tester), [true, true, true, true, false, true, false]); }); testWidgets("offers every part, none of them locked", (tester) async { await pump(tester, car()); expect(find.text(t("car.viewPicker.servicePartsHeading").toUpperCase()), findsOneWidget); for (final part in kServiceParts) { expect(find.text(t(part.label)), findsWidgets, reason: "the ${part.key} part is not on offer"); } expect(partTicks(tester), List.filled(kServiceParts.length, true)); // Only the date is locked. Every part can be switched off: a service that // changed nothing is a real service, so the form needs none of them. final locked = tester .widgetList(find.byType(Checkbox)) .where((b) => b.onChanged == null); expect(locked, hasLength(1)); }); testWidgets("a switched-off part comes back unticked", (tester) async { await pump(tester, car(hiddenParts: ["oil", "cabinFilter"])); expect(partTicks(tester), [false, true, false]); // And it changes nothing about the columns, which are their own set. expect(columnTicks(tester), List.filled(kServiceColumnKeys.length, true)); }); testWidgets("the rows follow the car's arrangement", (tester) async { await pump(tester, car(columnOrder: ["notes", "date"])); final notes = tester.getTopLeft(find.text(serviceColumnLabel("notes")).last).dy; final date = tester.getTopLeft(find.text(serviceColumnLabel("date")).last).dy; final km = tester.getTopLeft(find.text(serviceColumnLabel("km")).last).dy; expect(notes, lessThan(date)); expect(date, lessThan(km)); }); }); }