timelinemodeltest.cpp 23.4 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11 12 13 14
/*
    Copyright (C) 2018 Volker Krause <vkrause@kde.org>

    This program is free software; you can redistribute it and/or modify it
    under the terms of the GNU Library General Public License as published by
    the Free Software Foundation; either version 2 of the License, or (at your
    option) any later version.

    This program is distributed in the hope that it will be useful, but WITHOUT
    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
    License for more details.

    You should have received a copy of the GNU General Public License
15
    along with this program.  If not, see <https://www.gnu.org/licenses/>.
16 17
*/

18 19
#include "modelverificationpoint.h"

20
#include <countryinformation.h>
21
#include <pkpassmanager.h>
22
#include <reservationmanager.h>
23
#include <timelinemodel.h>
24
#include <tripgroupmanager.h>
25

26 27 28 29 30 31 32
#include <weatherforecast.h>
#include <weatherforecastmanager.h>

#include <KItinerary/Flight>
#include <KItinerary/Place>
#include <KItinerary/Reservation>

33 34 35
#if QT_VERSION >= QT_VERSION_CHECK(5, 11, 0)
#include <QAbstractItemModelTester>
#endif
36
#include <QDirIterator>
37 38 39 40 41 42 43 44 45 46 47
#include <QUrl>
#include <QtTest/qtest.h>
#include <QSignalSpy>
#include <QStandardPaths>

class TimelineModelTest : public QObject
{
    Q_OBJECT
private:
    void clearPasses(PkPassManager *mgr)
    {
48
        for (const auto &id : mgr->passes()) {
49
            mgr->removePass(id);
50
        }
51 52
    }

53 54
    void clearReservations(ReservationManager *mgr)
    {
55
        for (const auto &id : mgr->reservations()) {
56 57 58 59
            mgr->removeReservation(id);
        }
    }

60 61 62 63 64 65 66
    QByteArray readFile(const QString &fn)
    {
        QFile f(fn);
        f.open(QFile::ReadOnly);
        return f.readAll();
    }

67
private Q_SLOTS:
68 69
    void initTestCase()
    {
70
        qputenv("TZ", "UTC");
71 72 73
        QStandardPaths::setTestModeEnabled(true);
    }

74 75 76 77 78
    void init()
    {
        TripGroupManager::clear();
    }

79 80 81 82
    void testModel()
    {
        PkPassManager mgr;
        clearPasses(&mgr);
83 84 85 86
        ReservationManager resMgr;
        clearReservations(&resMgr);

        resMgr.setPkPassManager(&mgr);
87
        TimelineModel model;
88 89 90
#if QT_VERSION >= QT_VERSION_CHECK(5, 11, 0)
        QAbstractItemModelTester tester(&model);
#endif
91
        model.setReservationManager(&resMgr);
92 93 94 95 96

        QSignalSpy insertSpy(&model, &TimelineModel::rowsInserted);
        QVERIFY(insertSpy.isValid());
        QSignalSpy updateSpy(&model, &TimelineModel::dataChanged);
        QVERIFY(updateSpy.isValid());
97 98
        QSignalSpy rmSpy(&model, &TimelineModel::rowsRemoved);
        QVERIFY(rmSpy.isValid());
99

100 101
        QCOMPARE(model.rowCount(), 1);
        QCOMPARE(model.index(0, 0).data(TimelineModel::ElementTypeRole), TimelineModel::TodayMarker);
102 103
        mgr.importPass(QUrl::fromLocalFile(QLatin1String(SOURCE_DIR "/data/boardingpass-v1.pkpass")));
        QCOMPARE(insertSpy.size(), 1);
104 105
        QCOMPARE(insertSpy.at(0).at(1).toInt(), 0);
        QCOMPARE(insertSpy.at(0).at(2).toInt(), 0);
106
        QVERIFY(updateSpy.isEmpty());
107 108
        QCOMPARE(model.rowCount(), 2);
        QCOMPARE(model.index(0, 0).data(TimelineModel::ElementTypeRole), TimelineModel::Flight);
109 110 111 112

        mgr.importPass(QUrl::fromLocalFile(QLatin1String(SOURCE_DIR "/data/boardingpass-v2.pkpass")));
        QCOMPARE(insertSpy.size(), 1);
        QCOMPARE(updateSpy.size(), 1);
113 114
        QCOMPARE(updateSpy.at(0).at(0).toModelIndex().row(), 0);
        QCOMPARE(model.rowCount(), 2);
115

116
        clearReservations(&resMgr);
117 118 119
        QCOMPARE(insertSpy.size(), 1);
        QCOMPARE(updateSpy.size(), 1);
        QCOMPARE(rmSpy.size(), 1);
120
        QCOMPARE(model.rowCount(), 1);
121
    }
122 123 124 125 126 127 128

    void testNestedElements()
    {
        ReservationManager resMgr;
        clearReservations(&resMgr);

        TimelineModel model;
129 130 131
#if QT_VERSION >= QT_VERSION_CHECK(5, 11, 0)
        QAbstractItemModelTester tester(&model);
#endif
132
        model.setHomeCountryIsoCode(QStringLiteral("DE"));
133 134 135 136 137 138 139 140 141 142 143
        model.setReservationManager(&resMgr);

        QSignalSpy insertSpy(&model, &TimelineModel::rowsInserted);
        QVERIFY(insertSpy.isValid());
        QSignalSpy updateSpy(&model, &TimelineModel::dataChanged);
        QVERIFY(updateSpy.isValid());
        QSignalSpy rmSpy(&model, &TimelineModel::rowsRemoved);
        QVERIFY(rmSpy.isValid());

        QCOMPARE(model.rowCount(), 1);
        QCOMPARE(model.index(0, 0).data(TimelineModel::ElementTypeRole), TimelineModel::TodayMarker);
144
        resMgr.importReservation(readFile(QLatin1String(SOURCE_DIR "/data/haus-randa-v1.json")));
145
        QCOMPARE(insertSpy.size(), 3);
146 147 148 149 150
        QCOMPARE(insertSpy.at(0).at(1).toInt(), 0);
        QCOMPARE(insertSpy.at(0).at(2).toInt(), 0);
        QCOMPARE(insertSpy.at(1).at(1).toInt(), 1);
        QCOMPARE(insertSpy.at(1).at(2).toInt(), 1);
        QVERIFY(updateSpy.isEmpty());
151 152
        QCOMPARE(model.rowCount(), 4);
        QCOMPARE(model.index(0, 0).data(TimelineModel::ElementTypeRole), TimelineModel::CountryInfo);
153
        QCOMPARE(model.index(1, 0).data(TimelineModel::ElementTypeRole), TimelineModel::Hotel);
154 155 156
        QCOMPARE(model.index(1, 0).data(TimelineModel::ElementRangeRole), TimelineModel::RangeBegin);
        QCOMPARE(model.index(2, 0).data(TimelineModel::ElementTypeRole), TimelineModel::Hotel);
        QCOMPARE(model.index(2, 0).data(TimelineModel::ElementRangeRole), TimelineModel::RangeEnd);
157 158

        // move end date of a hotel booking: dataChanged on RangeBegin, move (or del/ins) on RangeEnd
159
        resMgr.importReservation(readFile(QLatin1String(SOURCE_DIR "/data/haus-randa-v2.json")));
160
        QCOMPARE(insertSpy.size(), 4);
161 162
        QCOMPARE(updateSpy.size(), 1);
        QCOMPARE(rmSpy.size(), 1);
163 164 165 166 167
        QCOMPARE(updateSpy.at(0).at(0).toModelIndex().row(), 1);
        QCOMPARE(insertSpy.at(2).at(1).toInt(), 0);
        QCOMPARE(insertSpy.at(2).at(2).toInt(), 0);
        QCOMPARE(rmSpy.at(0).at(1), 2);
        QCOMPARE(model.rowCount(), 4);
168 169

        // delete a split element
170
        const auto resId = model.data(model.index(1, 0), TimelineModel::ReservationIdsRole).toStringList().value(0);
171
        QVERIFY(!resId.isEmpty());
172
        resMgr.removeReservation(resId);
173
        QCOMPARE(rmSpy.size(), 4);
174 175 176
        QCOMPARE(model.rowCount(), 1);
        QCOMPARE(model.index(0, 0).data(TimelineModel::ElementTypeRole), TimelineModel::TodayMarker);
    }
177 178 179 180 181 182 183

    void testCountryInfos()
    {
        ReservationManager resMgr;
        clearReservations(&resMgr);

        TimelineModel model;
184 185 186
#if QT_VERSION >= QT_VERSION_CHECK(5, 11, 0)
        QAbstractItemModelTester tester(&model);
#endif
187
        model.setHomeCountryIsoCode(QStringLiteral("DE"));
188 189 190 191 192
        model.setReservationManager(&resMgr);

        QCOMPARE(model.rowCount(), 1);
        QCOMPARE(model.index(0, 0).data(TimelineModel::ElementTypeRole), TimelineModel::TodayMarker);

193
        resMgr.importReservation(readFile(QLatin1String(SOURCE_DIR "/data/flight-txl-lhr-sfo.json")));
194 195
        QCOMPARE(model.rowCount(), 5); //  2x country info, 2x flights, today marker

196 197 198 199
        QCOMPARE(model.index(0, 0).data(TimelineModel::ElementTypeRole), TimelineModel::CountryInfo);
        auto countryInfo = model.index(0, 0).data(TimelineModel::CountryInformationRole).value<CountryInformation>();
        QCOMPARE(countryInfo.drivingSide(), KItinerary::KnowledgeDb::DrivingSide::Left);
        QCOMPARE(countryInfo.drivingSideDiffers(), true);
200
        QCOMPARE(countryInfo.powerPlugCompatibility(), CountryInformation::Incompatible);
201 202
        QCOMPARE(model.index(1, 0).data(TimelineModel::ElementTypeRole), TimelineModel::Flight);

203 204 205 206
        QCOMPARE(model.index(2, 0).data(TimelineModel::ElementTypeRole), TimelineModel::CountryInfo);
        countryInfo = model.index(2, 0).data(TimelineModel::CountryInformationRole).value<CountryInformation>();
        QCOMPARE(countryInfo.drivingSide(), KItinerary::KnowledgeDb::DrivingSide::Right);
        QCOMPARE(countryInfo.drivingSideDiffers(), false);
207
        QCOMPARE(countryInfo.powerPlugCompatibility(), CountryInformation::Incompatible);
208 209 210 211
        QCOMPARE(model.index(3, 0).data(TimelineModel::ElementTypeRole), TimelineModel::Flight);
        QCOMPARE(model.index(4, 0).data(TimelineModel::ElementTypeRole), TimelineModel::TodayMarker);

        // remove the GB flight should also remove the GB country info
212
        auto resId = model.index(1, 0).data(TimelineModel::ReservationIdsRole).toStringList().value(0);
213
        resMgr.removeReservation(resId);
214 215 216 217 218 219
        QCOMPARE(model.rowCount(), 3);
        QCOMPARE(model.index(0, 0).data(TimelineModel::ElementTypeRole), TimelineModel::CountryInfo);
        QCOMPARE(model.index(1, 0).data(TimelineModel::ElementTypeRole), TimelineModel::Flight);
        QCOMPARE(model.index(2, 0).data(TimelineModel::ElementTypeRole), TimelineModel::TodayMarker);

        // remove the US flight should also remove the US country info
220
        resId = model.index(1, 0).data(TimelineModel::ReservationIdsRole).toStringList().value(0);
221 222 223
        resMgr.removeReservation(resId);
        QCOMPARE(model.rowCount(), 1);
        QCOMPARE(model.index(0, 0).data(TimelineModel::ElementTypeRole), TimelineModel::TodayMarker);
224
    }
225 226 227 228 229 230 231 232 233 234 235

    void testWeatherElements()
    {
        using namespace KItinerary;

        ReservationManager resMgr;
        clearReservations(&resMgr);
        WeatherForecastManager weatherMgr;
        weatherMgr.setTestModeEnabled(true);

        TimelineModel model;
236 237 238
#if QT_VERSION >= QT_VERSION_CHECK(5, 11, 0)
        QAbstractItemModelTester tester(&model);
#endif
239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262
        model.setReservationManager(&resMgr);
        model.setWeatherForecastManager(&weatherMgr);
        QCOMPARE(model.rowCount(), 1); // no weather data, as we don't know where we are

        // Add an element that will result in a defined location
        GeoCoordinates geo;
        geo.setLatitude(52.0f);
        geo.setLongitude(13.0f);
        Airport a;
        a.setGeo(geo);
        Flight f;
        f.setArrivalAirport(a);
        f.setDepartureTime(QDateTime(QDate(2018, 1, 1), QTime(0, 0)));
        FlightReservation res;
        res.setReservationFor(f);
        resMgr.addReservation(res);

        QCOMPARE(model.rowCount(), 11); // 1x flight, 1x today, 9x weather
        QCOMPARE(model.index(0, 0).data(TimelineModel::ElementTypeRole), TimelineModel::Flight);
        QCOMPARE(model.index(1, 0).data(TimelineModel::ElementTypeRole), TimelineModel::TodayMarker);
        QCOMPARE(model.index(2, 0).data(TimelineModel::ElementTypeRole), TimelineModel::WeatherForecast);
        auto fc = model.index(2, 0).data(TimelineModel::WeatherForecastRole).value<WeatherForecast>();
        QVERIFY(fc.isValid());
        QCOMPARE(fc.dateTime().date(), QDate::currentDate());
263 264
        QCOMPARE(fc.minimumTemperature(), 13.0f);
        QCOMPARE(fc.maximumTemperature(), 52.0f);
265 266 267 268
        QCOMPARE(model.index(10, 0).data(TimelineModel::ElementTypeRole), TimelineModel::WeatherForecast);
        fc = model.index(10, 0).data(TimelineModel::WeatherForecastRole).value<WeatherForecast>();
        QVERIFY(fc.isValid());
        QCOMPARE(fc.dateTime().date(), QDate::currentDate().addDays(8));
269 270 271 272 273 274 275

        // Add a flight one day from now changing location mid-day
        geo.setLatitude(46.0f);
        geo.setLongitude(8.0f);
        a.setGeo(geo);
        f.setArrivalAirport(a);
        f.setDepartureTime(QDateTime(QDate::currentDate().addDays(1), QTime(12, 0)));
276
        f.setArrivalTime(QDateTime(QDate::currentDate().addDays(1), QTime(14, 0)));
277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298
        res.setReservationFor(f);
        resMgr.addReservation(res);

        QCOMPARE(model.rowCount(), 13); // 2x flight, 1x today, 10x weather
        QCOMPARE(model.index(0, 0).data(TimelineModel::ElementTypeRole), TimelineModel::Flight);
        QCOMPARE(model.index(1, 0).data(TimelineModel::ElementTypeRole), TimelineModel::TodayMarker);
        QCOMPARE(model.index(2, 0).data(TimelineModel::ElementTypeRole), TimelineModel::WeatherForecast);
        fc = model.index(2, 0).data(TimelineModel::WeatherForecastRole).value<WeatherForecast>();
        QVERIFY(fc.isValid());
        QCOMPARE(fc.dateTime().date(), QDate::currentDate());
        QCOMPARE(model.index(3, 0).data(TimelineModel::ElementTypeRole), TimelineModel::WeatherForecast);
        fc = model.index(3, 0).data(TimelineModel::WeatherForecastRole).value<WeatherForecast>();
        QVERIFY(fc.isValid());
        QCOMPARE(fc.minimumTemperature(), 13.0f);
        QCOMPARE(fc.maximumTemperature(), 52.0f);
        QCOMPARE(fc.dateTime(), QDateTime(QDate::currentDate().addDays(1), QTime(0, 0)));
        QCOMPARE(model.index(4, 0).data(TimelineModel::ElementTypeRole), TimelineModel::Flight);
        QCOMPARE(model.index(5, 0).data(TimelineModel::ElementTypeRole), TimelineModel::WeatherForecast);
        fc = model.index(5, 0).data(TimelineModel::WeatherForecastRole).value<WeatherForecast>();
        QVERIFY(fc.isValid());
        QCOMPARE(fc.minimumTemperature(), 8.0f);
        QCOMPARE(fc.maximumTemperature(), 46.0f);
299
        QCOMPARE(fc.dateTime(), QDateTime(QDate::currentDate().addDays(1), QTime(14, 0)));
300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359
        QCOMPARE(model.index(6, 0).data(TimelineModel::ElementTypeRole), TimelineModel::WeatherForecast);
        fc = model.index(6, 0).data(TimelineModel::WeatherForecastRole).value<WeatherForecast>();
        QCOMPARE(fc.minimumTemperature(), 8.0f);
        QCOMPARE(fc.maximumTemperature(), 46.0f);
        QVERIFY(fc.isValid());
        QCOMPARE(fc.dateTime(), QDateTime(QDate::currentDate().addDays(2), QTime(0, 0)));

        // check we get update signals for all weather elements
        QSignalSpy spy(&model, &TimelineModel::dataChanged);
        QVERIFY(spy.isValid());
        emit weatherMgr.forecastUpdated();
        QCOMPARE(spy.size(), 10);

        fc = model.index(3, 0).data(TimelineModel::WeatherForecastRole).value<WeatherForecast>();
        QVERIFY(fc.isValid());
        QCOMPARE(fc.minimumTemperature(), 13.0f);
        QCOMPARE(fc.maximumTemperature(), 52.0f);
        QCOMPARE(fc.dateTime(), QDateTime(QDate::currentDate().addDays(1), QTime(0, 0)));
        fc = model.index(9, 0).data(TimelineModel::WeatherForecastRole).value<WeatherForecast>();
        QVERIFY(fc.isValid());
        QCOMPARE(fc.minimumTemperature(), 8.0f);
        QCOMPARE(fc.maximumTemperature(), 46.0f);
        QCOMPARE(fc.dateTime(), QDateTime(QDate::currentDate().addDays(5), QTime(0, 0)));

        // add a location change far in the future, this must not change anything
        geo.setLatitude(60.0f);
        geo.setLongitude(11.0f);
        a.setGeo(geo);
        f.setArrivalAirport(a);
        f.setDepartureTime(QDateTime(QDate::currentDate().addYears(1), QTime(6, 0)));
        res.setReservationFor(f);
        resMgr.addReservation(res);
        QCOMPARE(model.rowCount(), 14);

        fc = model.index(3, 0).data(TimelineModel::WeatherForecastRole).value<WeatherForecast>();
        QVERIFY(fc.isValid());
        QCOMPARE(fc.minimumTemperature(), 13.0f);
        QCOMPARE(fc.maximumTemperature(), 52.0f);
        QCOMPARE(fc.dateTime(), QDateTime(QDate::currentDate().addDays(1), QTime(0, 0)));
        fc = model.index(9, 0).data(TimelineModel::WeatherForecastRole).value<WeatherForecast>();
        QVERIFY(fc.isValid());
        QCOMPARE(fc.minimumTemperature(), 8.0f);
        QCOMPARE(fc.maximumTemperature(), 46.0f);
        QCOMPARE(fc.dateTime(), QDateTime(QDate::currentDate().addDays(5), QTime(0, 0)));

        // result is the same when data hasn't been added incrementally
        model.setReservationManager(nullptr);
        model.setReservationManager(&resMgr);
        QCOMPARE(model.rowCount(), 14);

        fc = model.index(3, 0).data(TimelineModel::WeatherForecastRole).value<WeatherForecast>();
        QVERIFY(fc.isValid());
        QCOMPARE(fc.minimumTemperature(), 13.0f);
        QCOMPARE(fc.maximumTemperature(), 52.0f);
        QCOMPARE(fc.dateTime(), QDateTime(QDate::currentDate().addDays(1), QTime(0, 0)));
        fc = model.index(9, 0).data(TimelineModel::WeatherForecastRole).value<WeatherForecast>();
        QVERIFY(fc.isValid());
        QCOMPARE(fc.minimumTemperature(), 8.0f);
        QCOMPARE(fc.maximumTemperature(), 46.0f);
        QCOMPARE(fc.dateTime(), QDateTime(QDate::currentDate().addDays(5), QTime(0, 0)));
360 361

        // clean up
362
        auto resId = model.index(13, 0).data(TimelineModel::ReservationIdsRole).toStringList().value(0);
363
        resMgr.removeReservation(resId);
364
        resId = model.index(4, 0).data(TimelineModel::ReservationIdsRole).toStringList().value(0);
365 366
        resMgr.removeReservation(resId);
        QCOMPARE(model.rowCount(), 11);
367 368 369 370 371 372 373 374 375 376 377 378 379 380

        // test case: two conesequtive location changes, the first one to an unknown location
        // result: the weather element before the first location change ends with the start of that
        // result 2: we get a second weather element the same day after the second location change
        // TODO
    }

    void testMultiTraveller()
    {
        using namespace KItinerary;

        ReservationManager resMgr;
        clearReservations(&resMgr);
        TimelineModel model;
381 382 383
#if QT_VERSION >= QT_VERSION_CHECK(5, 11, 0)
        QAbstractItemModelTester tester(&model);
#endif
384 385 386 387 388 389 390 391 392 393 394
        model.setReservationManager(&resMgr);
        QCOMPARE(model.rowCount(), 1); // 1x TodayMarker

        QSignalSpy insertSpy(&model, &TimelineModel::rowsInserted);
        QVERIFY(insertSpy.isValid());
        QSignalSpy updateSpy(&model, &TimelineModel::dataChanged);
        QVERIFY(updateSpy.isValid());
        QSignalSpy rmSpy(&model, &TimelineModel::rowsRemoved);
        QVERIFY(rmSpy.isValid());

        // full import at runtime
395
        resMgr.importReservation(readFile(QLatin1String(SOURCE_DIR "/data/google-multi-passenger-flight.json")));
396 397 398 399 400 401
        QCOMPARE(model.rowCount(), 3); // 2x Flight, 1x TodayMarger
        QCOMPARE(insertSpy.count(), 2);
        QCOMPARE(updateSpy.count(), 2);
        QCOMPARE(model.index(0, 0).data(TimelineModel::ElementTypeRole), TimelineModel::Flight);
        QCOMPARE(model.index(1, 0).data(TimelineModel::ElementTypeRole), TimelineModel::Flight);
        QCOMPARE(model.index(2, 0).data(TimelineModel::ElementTypeRole), TimelineModel::TodayMarker);
402 403
        QCOMPARE(model.index(0, 0).data(TimelineModel::ReservationIdsRole).toStringList().size(), 2);
        QCOMPARE(model.index(1, 0).data(TimelineModel::ReservationIdsRole).toStringList().size(), 2);
404 405 406 407 408 409 410 411

        // already existing data
        model.setReservationManager(nullptr);
        model.setReservationManager(&resMgr);
        QCOMPARE(model.rowCount(), 3); // 2x Flight, 1x TodayMarger
        QCOMPARE(model.index(0, 0).data(TimelineModel::ElementTypeRole), TimelineModel::Flight);
        QCOMPARE(model.index(1, 0).data(TimelineModel::ElementTypeRole), TimelineModel::Flight);
        QCOMPARE(model.index(2, 0).data(TimelineModel::ElementTypeRole), TimelineModel::TodayMarker);
412 413
        QCOMPARE(model.index(0, 0).data(TimelineModel::ReservationIdsRole).toStringList().size(), 2);
        QCOMPARE(model.index(1, 0).data(TimelineModel::ReservationIdsRole).toStringList().size(), 2);
414 415 416 417

        // update splits element
        updateSpy.clear();
        insertSpy.clear();
418
        auto resId = model.index(1, 0).data(TimelineModel::ReservationIdsRole).toStringList().value(0);
419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448
        QVERIFY(!resId.isEmpty());
        auto res = resMgr.reservation(resId).value<FlightReservation>();
        auto flight = res.reservationFor().value<Flight>();
        flight.setDepartureTime(flight.departureTime().addDays(1));
        res.setReservationFor(flight);
        resMgr.updateReservation(resId, res);
        QCOMPARE(model.rowCount(), 4);
        QCOMPARE(updateSpy.count(), 1);
        QCOMPARE(insertSpy.count(), 1);
        QCOMPARE(rmSpy.count(), 0);

        // update merges two elements
        updateSpy.clear();
        insertSpy.clear();
        rmSpy.clear();
        flight.setDepartureTime(flight.departureTime().addDays(-1));
        res.setReservationFor(flight);
        resMgr.updateReservation(resId, res);
        QCOMPARE(model.rowCount(), 3);
        QCOMPARE(updateSpy.count(), 1);
        QCOMPARE(rmSpy.count(), 1);
        QCOMPARE(insertSpy.count(), 0);

        // removal of merged items
        updateSpy.clear();
        rmSpy.clear();
        clearReservations(&resMgr);
        QCOMPARE(model.rowCount(), 1);
        QCOMPARE(rmSpy.count(), 2);
        QCOMPARE(updateSpy.count(), 2);
449
    }
450 451 452 453 454 455 456 457 458

    void testDayChange()
    {
        ReservationManager resMgr;
        clearReservations(&resMgr);
        WeatherForecastManager weatherMgr;
        weatherMgr.setTestModeEnabled(true);

        TimelineModel model;
459 460 461
#if QT_VERSION >= QT_VERSION_CHECK(5, 11, 0)
        QAbstractItemModelTester tester(&model);
#endif
462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488
        model.setHomeCountryIsoCode(QStringLiteral("DE"));
        model.setCurrentDateTime(QDateTime({2196, 10, 14}, {12, 34}));
        model.setReservationManager(&resMgr);
        model.setWeatherForecastManager(&weatherMgr);

        ModelVerificationPoint vp0(QLatin1String(SOURCE_DIR "/data/timeline/daychange-r0.model"));
        vp0.setRoleFilter({TimelineModel::ReservationIdsRole});
        QVERIFY(vp0.verify(&model));

        // changing the day should move the today marker
        model.setCurrentDateTime(QDateTime({2196, 10, 15}, {0, 15}));
        ModelVerificationPoint vp1(QLatin1String(SOURCE_DIR "/data/timeline/daychange-r1.model"));
        vp1.setRoleFilter({TimelineModel::ReservationIdsRole});
        QVERIFY(vp1.verify(&model));

        // load something to define the current location, so we get weather
        resMgr.importReservation(readFile(QLatin1String(SOURCE_DIR "/data/flight-txl-lhr-sfo.json")));
        ModelVerificationPoint vp2(QLatin1String(SOURCE_DIR "/data/timeline/daychange-r2.model"));
        vp2.setRoleFilter({TimelineModel::ReservationIdsRole});
        QVERIFY(vp2.verify(&model));

        // changing the day should move the today marker and weather one day forward
        model.setCurrentDateTime(QDateTime({2196, 10, 16}, {19, 30}));
        ModelVerificationPoint vp3(QLatin1String(SOURCE_DIR "/data/timeline/daychange-r3.model"));
        vp3.setRoleFilter({TimelineModel::ReservationIdsRole});
        QVERIFY(vp3.verify(&model));
    }
489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507

    void testContent_data()
    {
        QTest::addColumn<QString>("baseName");

        QDirIterator it(QLatin1String(SOURCE_DIR "/data/timeline/"), {QLatin1String("*.json")});
        while (it.hasNext()) {
            it.next();
            const auto baseName = it.fileInfo().baseName();
            QTest::newRow(baseName.toUtf8().constData()) << baseName;
        }
    }

    void testContent()
    {
        QFETCH(QString, baseName);
        ReservationManager resMgr;
        clearReservations(&resMgr);
        resMgr.importReservation(readFile(QLatin1String(SOURCE_DIR "/data/timeline/") + baseName + QLatin1String(".json")));
508 509
        TripGroupManager groupMgr;
        groupMgr.setReservationManager(&resMgr);
510 511

        TimelineModel model;
512 513 514
#if QT_VERSION >= QT_VERSION_CHECK(5, 11, 0)
        QAbstractItemModelTester tester(&model);
#endif
515 516 517
        model.setHomeCountryIsoCode(QStringLiteral("DE"));
        model.setCurrentDateTime(QDateTime({1996, 10, 14}, {12, 34}));
        model.setReservationManager(&resMgr);
518
        model.setTripGroupManager(&groupMgr);
519 520 521

        // check state is correct for data imported at the start
        ModelVerificationPoint vp(QLatin1String(SOURCE_DIR "/data/timeline/") + baseName + QLatin1String(".model"));
522
        vp.setRoleFilter({TimelineModel::ReservationIdsRole, TimelineModel::TripGroupIdRole});
523 524 525 526 527 528 529
        QVERIFY(vp.verify(&model));

        // retry with loading during runtime
        clearReservations(&resMgr);
        resMgr.importReservation(readFile(QLatin1String(SOURCE_DIR "/data/timeline/") + baseName + QLatin1String(".json")));
        QVERIFY(vp.verify(&model));
    }
530 531 532 533 534
};

QTEST_GUILESS_MAIN(TimelineModelTest)

#include "timelinemodeltest.moc"