Merge pull request #11 from brendan-ch/bugfix/fix-data-flickering

This commit is contained in:
Brendan Chen
2025-01-22 22:37:53 -08:00
committed by GitHub
7 changed files with 359 additions and 46 deletions

View File

@@ -1,5 +1,5 @@
import { GetterSetterRepository } from "../repositories/GetterSetterRepository"; import { GetterSetterRepository } from "../repositories/GetterSetterRepository";
import { IEta, IRoute, IShuttle, IStop, ISystem } from "../entities/entities"; import { IEntityWithId, IEta, IRoute, IShuttle, IStop, ISystem } from "../entities/entities";
export class ApiResponseError extends Error { export class ApiResponseError extends Error {
constructor(message: string) { constructor(message: string) {
@@ -8,6 +8,11 @@ export class ApiResponseError extends Error {
} }
} }
/**
* Class which can load data into a repository from the
* Passio Go API. Supports automatic pruning of all data types
* which inherit from `IEntityWithId`.
*/
export class ApiBasedRepositoryLoader { export class ApiBasedRepositoryLoader {
supportedSystemIds = ["263"]; supportedSystemIds = ["263"];
baseUrl = "https://passiogo.com/mapGetData.php"; baseUrl = "https://passiogo.com/mapGetData.php";
@@ -17,12 +22,25 @@ export class ApiBasedRepositoryLoader {
) { ) {
} }
private async constructExistingEntityIdSet<T extends IEntityWithId>(entitySearchCallback: () => Promise<T[]>) {
const existingEntities = await entitySearchCallback();
const ids = new Set<string>();
existingEntities.forEach((entity) => {
ids.add(entity.id);
});
return ids;
}
public async fetchAndUpdateSystemData() { public async fetchAndUpdateSystemData() {
const params = { const params = {
getSystems: "2", getSystems: "2",
}; };
const query = new URLSearchParams(params).toString(); const query = new URLSearchParams(params).toString();
const systemIds = await this.constructExistingEntityIdSet(async () => {
return await this.repository.getSystems();
})
try { try {
const response = await fetch(`${this.baseUrl}?${query}`); const response = await fetch(`${this.baseUrl}?${query}`);
const json = await response.json(); const json = await response.json();
@@ -41,10 +59,16 @@ export class ApiBasedRepositoryLoader {
}; };
await this.repository.addOrUpdateSystem(constructedSystem); await this.repository.addOrUpdateSystem(constructedSystem);
systemIds.delete(constructedSystem.id);
})); }));
} else { } else {
throw new Error("Received JSON object does not contain `all` field") throw new Error("Received JSON object does not contain `all` field")
} }
// Prune systems
await Promise.all(Array.from(systemIds).map(async (systemId) => {
await this.repository.removeSystemIfExists(systemId);
}));
} catch(e: any) { } catch(e: any) {
throw new ApiResponseError(e.message); throw new ApiResponseError(e.message);
} }
@@ -58,6 +82,10 @@ export class ApiBasedRepositoryLoader {
} }
public async fetchAndUpdateRouteDataForSystemId(systemId: string) { public async fetchAndUpdateRouteDataForSystemId(systemId: string) {
const routeIdsToPrune = await this.constructExistingEntityIdSet(async () => {
return await this.repository.getRoutesBySystemId(systemId);
});
const params = { const params = {
getRoutes: "2", getRoutes: "2",
}; };
@@ -89,8 +117,14 @@ export class ApiBasedRepositoryLoader {
}; };
await this.repository.addOrUpdateRoute(constructedRoute); await this.repository.addOrUpdateRoute(constructedRoute);
routeIdsToPrune.delete(constructedRoute.id);
})) }))
} }
await Promise.all(Array.from(routeIdsToPrune).map(async (routeId) => {
await this.repository.removeRouteIfExists(routeId);
}));
} catch(e: any) { } catch(e: any) {
throw new ApiResponseError(e.message); throw new ApiResponseError(e.message);
} }
@@ -106,6 +140,10 @@ export class ApiBasedRepositoryLoader {
public async fetchAndUpdateStopAndPolylineDataForRoutesWithSystemId(systemId: string) { public async fetchAndUpdateStopAndPolylineDataForRoutesWithSystemId(systemId: string) {
// Fetch from the API // Fetch from the API
// Pass JSON output into two different methods to update repository // Pass JSON output into two different methods to update repository
const stopIdsToPrune = await this.constructExistingEntityIdSet(async () => {
return await this.repository.getStopsBySystemId(systemId);
});
const params = { const params = {
getStops: "2", getStops: "2",
}; };
@@ -126,9 +164,13 @@ export class ApiBasedRepositoryLoader {
}); });
const json = await response.json(); const json = await response.json();
await this.updateStopDataForSystemAndApiResponse(systemId, json); await this.updateStopDataForSystemAndApiResponse(systemId, json, stopIdsToPrune);
await this.updateOrderedStopDataForExistingStops(json); await this.updateOrderedStopDataForExistingStops(json);
await this.updatePolylineDataForExistingRoutesAndApiResponse(json); await this.updatePolylineDataForExistingRoutesAndApiResponse(json);
await Promise.all(Array.from(stopIdsToPrune).map(async (stopId) => {
await this.repository.removeStopIfExists(stopId);
}));
} catch(e: any) { } catch(e: any) {
throw new ApiResponseError(e.message); throw new ApiResponseError(e.message);
} }
@@ -143,6 +185,10 @@ export class ApiBasedRepositoryLoader {
} }
public async fetchAndUpdateShuttleDataForSystemId(systemId: string) { public async fetchAndUpdateShuttleDataForSystemId(systemId: string) {
const shuttleIdsToPrune = await this.constructExistingEntityIdSet(async () => {
return await this.repository.getShuttlesBySystemId(systemId);
});
const params = { const params = {
getBuses: "2" getBuses: "2"
}; };
@@ -182,8 +228,14 @@ export class ApiBasedRepositoryLoader {
} }
await this.repository.addOrUpdateShuttle(constructedShuttle); await this.repository.addOrUpdateShuttle(constructedShuttle);
shuttleIdsToPrune.delete(constructedShuttle.id);
})); }));
} }
await Promise.all(Array.from(shuttleIdsToPrune).map(async (shuttleId) => {
await this.repository.removeShuttleIfExists(shuttleId);
}));
} catch(e: any) { } catch(e: any) {
throw new ApiResponseError(e.message); throw new ApiResponseError(e.message);
} }
@@ -240,7 +292,11 @@ export class ApiBasedRepositoryLoader {
} }
} }
protected async updateStopDataForSystemAndApiResponse(systemId: string, json: any) { protected async updateStopDataForSystemAndApiResponse(
systemId: string,
json: any,
setOfIdsToPrune: Set<string> = new Set(),
) {
if (json.stops) { if (json.stops) {
const jsonStops = Object.values(json.stops); const jsonStops = Object.values(json.stops);
@@ -256,6 +312,8 @@ export class ApiBasedRepositoryLoader {
}; };
await this.repository.addOrUpdateStop(constructedStop); await this.repository.addOrUpdateStop(constructedStop);
setOfIdsToPrune.delete(constructedStop.id);
})); }));
} }
} }

View File

@@ -47,15 +47,10 @@ export class TimedApiBasedRepositoryLoader extends ApiBasedRepositoryLoader {
if (!this.shouldBeRunning) return; if (!this.shouldBeRunning) return;
try { try {
await this.repository.clearSystemData();
await this.fetchAndUpdateSystemData(); await this.fetchAndUpdateSystemData();
await this.repository.clearRouteData();
await this.fetchAndUpdateRouteDataForExistingSystemsInRepository(); await this.fetchAndUpdateRouteDataForExistingSystemsInRepository();
await this.repository.clearStopData();
await this.fetchAndUpdateStopAndPolylineDataForRoutesInExistingSystemsInRepository(); await this.fetchAndUpdateStopAndPolylineDataForRoutesInExistingSystemsInRepository();
await this.repository.clearShuttleData();
await this.fetchAndUpdateShuttleDataForExistingSystemsInRepository(); await this.fetchAndUpdateShuttleDataForExistingSystemsInRepository();
await this.repository.clearEtaData();
await this.fetchAndUpdateEtaDataForExistingStopsForSystemsInRepository(); await this.fetchAndUpdateEtaDataForExistingStopsForSystemsInRepository();
} catch (e) { } catch (e) {
console.error(e); console.error(e);

View File

@@ -19,6 +19,13 @@ export interface GetterSetterRepository extends GetterRepository {
addOrUpdateOrderedStop(orderedStop: IOrderedStop): Promise<void>; addOrUpdateOrderedStop(orderedStop: IOrderedStop): Promise<void>;
addOrUpdateEta(eta: IEta): Promise<void>; addOrUpdateEta(eta: IEta): Promise<void>;
removeSystemIfExists(systemId: string): Promise<ISystem | null>;
removeRouteIfExists(routeId: string): Promise<IRoute | null>;
removeShuttleIfExists(shuttleId: string): Promise<IShuttle | null>;
removeStopIfExists(stopId: string): Promise<IStop | null>;
removeOrderedStopIfExists(stopId: string, routeId: string): Promise<IOrderedStop | null>;
removeEtaIfExists(shuttleId: string, stopId: string): Promise<IEta | null>;
clearSystemData(): Promise<void>; clearSystemData(): Promise<void>;
clearRouteData(): Promise<void>; clearRouteData(): Promise<void>;
clearShuttleData(): Promise<void>; clearShuttleData(): Promise<void>;

View File

@@ -1,13 +1,5 @@
import { GetterSetterRepository } from "./GetterSetterRepository"; import { GetterSetterRepository } from "./GetterSetterRepository";
import { import { IEntityWithId, IEta, IOrderedStop, IRoute, IShuttle, IStop, ISystem } from "../entities/entities";
IEntityWithId,
IEta,
IOrderedStop,
IRoute,
IShuttle,
IStop,
ISystem
} from "../entities/entities";
/** /**
* An unoptimized in memory repository. * An unoptimized in memory repository.
@@ -148,6 +140,51 @@ export class UnoptimizedInMemoryRepository implements GetterSetterRepository {
} }
} }
private async removeEntityByMatcherIfExists<T>(callback: (value: T) => boolean, arrayToSearchIn: T[]) {
const index = arrayToSearchIn.findIndex(callback);
if (index > -1) {
const entityToReturn = arrayToSearchIn[index];
arrayToSearchIn.splice(index, 1);
return entityToReturn;
}
return null;
}
private async removeEntityByIdIfExists<T extends IEntityWithId>(entityId: string, arrayToSearchIn: T[]) {
return await this.removeEntityByMatcherIfExists((value) => value.id === entityId, arrayToSearchIn);
}
public async removeSystemIfExists(systemId: string): Promise<ISystem | null> {
return await this.removeEntityByIdIfExists(systemId, this.systems);
}
public async removeRouteIfExists(routeId: string): Promise<IRoute | null> {
return await this.removeEntityByIdIfExists(routeId, this.routes);
}
public async removeShuttleIfExists(shuttleId: string): Promise<IShuttle | null> {
return await this.removeEntityByIdIfExists(shuttleId, this.shuttles);
}
public async removeStopIfExists(stopId: string): Promise<IStop | null> {
return await this.removeEntityByIdIfExists(stopId, this.stops);
}
public async removeOrderedStopIfExists(stopId: string, routeId: string): Promise<IOrderedStop | null> {
return await this.removeEntityByMatcherIfExists((orderedStop) => {
return orderedStop.stopId === stopId
&& orderedStop.routeId === routeId
}, this.orderedStops);
}
public async removeEtaIfExists(shuttleId: string, stopId: string): Promise<IEta | null> {
return await this.removeEntityByMatcherIfExists((eta) => {
return eta.stopId === stopId
&& eta.shuttleId === shuttleId
}, this.etas);
}
public async clearSystemData() { public async clearSystemData() {
this.systems = []; this.systems = [];
} }
@@ -171,4 +208,5 @@ export class UnoptimizedInMemoryRepository implements GetterSetterRepository {
public async clearStopData(): Promise<void> { public async clearStopData(): Promise<void> {
this.stops = []; this.stops = [];
} }
} }

View File

@@ -7,7 +7,14 @@ import { fetchRouteDataSuccessfulResponse } from "../jsonSnapshots/fetchRouteDat
import { import {
fetchStopAndPolylineDataSuccessfulResponse fetchStopAndPolylineDataSuccessfulResponse
} from "../jsonSnapshots/fetchStopAndPolylineData/fetchStopAndPolylineDataSuccessfulResponse"; } from "../jsonSnapshots/fetchStopAndPolylineData/fetchStopAndPolylineDataSuccessfulResponse";
import { generateMockStops, generateMockSystems } from "../generators"; import {
generateMockEtas,
generateMockOrderedStops,
generateMockRoutes,
generateMockShuttles,
generateMockStops,
generateMockSystems
} from "../generators";
import { IStop } from "../../src/entities/entities"; import { IStop } from "../../src/entities/entities";
import { import {
fetchShuttleDataSuccessfulResponse fetchShuttleDataSuccessfulResponse
@@ -33,11 +40,19 @@ describe("ApiBasedRepositoryLoader", () => {
describe("fetchAndUpdateSystemData", () => { describe("fetchAndUpdateSystemData", () => {
it("updates system data in repository if response received", async () => { it("updates system data in repository if response received", async () => {
// Arrange
const systemsToPrune = generateMockSystems();
await Promise.all(systemsToPrune.map(async (system) => {
await loader.repository.addOrUpdateSystem(system);
}));
const numberOfSystemsInResponse = fetchSystemDataSuccessfulResponse.all.length; const numberOfSystemsInResponse = fetchSystemDataSuccessfulResponse.all.length;
updateGlobalFetchMockJson(fetchSystemDataSuccessfulResponse); updateGlobalFetchMockJson(fetchSystemDataSuccessfulResponse);
// Act
await loader.fetchAndUpdateSystemData(); await loader.fetchAndUpdateSystemData();
// Assert
const systems = await loader.repository.getSystems(); const systems = await loader.repository.getSystems();
if (loader.supportedSystemIds.length < numberOfSystemsInResponse) { if (loader.supportedSystemIds.length < numberOfSystemsInResponse) {
expect(systems).toHaveLength(loader.supportedSystemIds.length); expect(systems).toHaveLength(loader.supportedSystemIds.length);
@@ -81,12 +96,23 @@ describe("ApiBasedRepositoryLoader", () => {
}); });
describe("fetchAndUpdateRouteDataForSystemId", () => { describe("fetchAndUpdateRouteDataForSystemId", () => {
const systemId = "263";
it("updates route data in repository if response received", async () => { it("updates route data in repository if response received", async () => {
// Arrange
// Test pruning
const routesToPrune = generateMockRoutes();
await Promise.all(routesToPrune.map(async (route) => {
route.systemId = systemId;
await loader.repository.addOrUpdateRoute(route);
}));
updateGlobalFetchMockJson(fetchRouteDataSuccessfulResponse); updateGlobalFetchMockJson(fetchRouteDataSuccessfulResponse);
await loader.fetchAndUpdateRouteDataForSystemId("263"); // Act
await loader.fetchAndUpdateRouteDataForSystemId(systemId);
const routes = await loader.repository.getRoutesBySystemId("263"); // Assert
const routes = await loader.repository.getRoutesBySystemId(systemId);
expect(routes.length).toEqual(fetchRouteDataSuccessfulResponse.all.length) expect(routes.length).toEqual(fetchRouteDataSuccessfulResponse.all.length)
}); });
@@ -98,7 +124,7 @@ describe("ApiBasedRepositoryLoader", () => {
updateGlobalFetchMockJsonToThrowSyntaxError(); updateGlobalFetchMockJsonToThrowSyntaxError();
await assertAsyncCallbackThrowsApiResponseError(async () => { await assertAsyncCallbackThrowsApiResponseError(async () => {
await loader.fetchAndUpdateRouteDataForSystemId("263"); await loader.fetchAndUpdateRouteDataForSystemId(systemId);
}); });
}); });
}); });
@@ -120,14 +146,23 @@ describe("ApiBasedRepositoryLoader", () => {
}) })
describe("fetchAndUpdateStopAndPolylineDataForRoutesWithSystemId", () => { describe("fetchAndUpdateStopAndPolylineDataForRoutesWithSystemId", () => {
const systemId = "263";
it("updates stop and polyline data if response received", async () => { it("updates stop and polyline data if response received", async () => {
// Arrange
// Test pruning of stops only
const stopsToPrune = generateMockStops();
await Promise.all(stopsToPrune.map(async (stop) => {
stop.systemId = systemId;
await loader.repository.addOrUpdateStop(stop);
}));
updateGlobalFetchMockJson(fetchStopAndPolylineDataSuccessfulResponse); updateGlobalFetchMockJson(fetchStopAndPolylineDataSuccessfulResponse);
const stopsArray = Object.values(fetchStopAndPolylineDataSuccessfulResponse.stops); const stopsArray = Object.values(fetchStopAndPolylineDataSuccessfulResponse.stops);
await loader.fetchAndUpdateStopAndPolylineDataForRoutesWithSystemId("263"); await loader.fetchAndUpdateStopAndPolylineDataForRoutesWithSystemId(systemId);
const stops = await loader.repository.getStopsBySystemId("263"); const stops = await loader.repository.getStopsBySystemId(systemId);
expect(stops.length).toEqual(stopsArray.length); expect(stops.length).toEqual(stopsArray.length);
await Promise.all(stops.map(async (stop) => { await Promise.all(stops.map(async (stop) => {
@@ -135,7 +170,7 @@ describe("ApiBasedRepositoryLoader", () => {
expect(orderedStops.length).toBeGreaterThan(0); expect(orderedStops.length).toBeGreaterThan(0);
})); }));
const routes = await loader.repository.getRoutesBySystemId("263"); const routes = await loader.repository.getRoutesBySystemId(systemId);
routes.forEach((route) => { routes.forEach((route) => {
expect(route.polylineCoordinates.length).toBeGreaterThan(0); expect(route.polylineCoordinates.length).toBeGreaterThan(0);
}); });
@@ -145,7 +180,7 @@ describe("ApiBasedRepositoryLoader", () => {
updateGlobalFetchMockJsonToThrowSyntaxError(); updateGlobalFetchMockJsonToThrowSyntaxError();
await assertAsyncCallbackThrowsApiResponseError(async () => { await assertAsyncCallbackThrowsApiResponseError(async () => {
await loader.fetchAndUpdateStopAndPolylineDataForRoutesWithSystemId("263"); await loader.fetchAndUpdateStopAndPolylineDataForRoutesWithSystemId(systemId);
}); });
}) })
}); });
@@ -166,13 +201,20 @@ describe("ApiBasedRepositoryLoader", () => {
}); });
describe("fetchAndUpdateShuttleDataForSystemId", () => { describe("fetchAndUpdateShuttleDataForSystemId", () => {
const systemId = "263";
it("updates shuttle data in repository if response received", async () => { it("updates shuttle data in repository if response received", async () => {
const shuttlesToPrune = generateMockShuttles();
await Promise.all(shuttlesToPrune.map(async (shuttle) => {
shuttle.systemId = systemId;
await loader.repository.addOrUpdateShuttle(shuttle);
}))
updateGlobalFetchMockJson(fetchShuttleDataSuccessfulResponse); updateGlobalFetchMockJson(fetchShuttleDataSuccessfulResponse);
const busesInResponse = Object.values(fetchShuttleDataSuccessfulResponse.buses); const busesInResponse = Object.values(fetchShuttleDataSuccessfulResponse.buses);
await loader.fetchAndUpdateShuttleDataForSystemId("263"); await loader.fetchAndUpdateShuttleDataForSystemId(systemId);
const shuttles = await loader.repository.getShuttlesBySystemId("263"); const shuttles = await loader.repository.getShuttlesBySystemId(systemId);
expect(shuttles.length).toEqual(busesInResponse.length); expect(shuttles.length).toEqual(busesInResponse.length);
}); });
@@ -181,7 +223,7 @@ describe("ApiBasedRepositoryLoader", () => {
updateGlobalFetchMockJsonToThrowSyntaxError(); updateGlobalFetchMockJsonToThrowSyntaxError();
await assertAsyncCallbackThrowsApiResponseError(async () => { await assertAsyncCallbackThrowsApiResponseError(async () => {
await loader.fetchAndUpdateShuttleDataForSystemId("263"); await loader.fetchAndUpdateShuttleDataForSystemId(systemId);
}); });
}); });
}); });
@@ -221,9 +263,9 @@ describe("ApiBasedRepositoryLoader", () => {
}); });
describe("fetchAndUpdateEtaDataForStopId", () => { describe("fetchAndUpdateEtaDataForStopId", () => {
const stopId = "177666";
it("updates ETA data for stop id if response received", async () => { it("updates ETA data for stop id if response received", async () => {
updateGlobalFetchMockJson(fetchEtaDataSuccessfulResponse); updateGlobalFetchMockJson(fetchEtaDataSuccessfulResponse);
const stopId = "177666";
// @ts-ignore // @ts-ignore
const etasFromResponse = fetchEtaDataSuccessfulResponse.ETAs[stopId] const etasFromResponse = fetchEtaDataSuccessfulResponse.ETAs[stopId]

View File

@@ -1,10 +1,9 @@
import { afterEach, beforeAll, beforeEach, describe, expect, it, jest } from "@jest/globals"; import { afterEach, beforeAll, beforeEach, describe, expect, it, jest } from "@jest/globals";
import { TimedApiBasedRepositoryLoader } from "../../src/loaders/TimedApiBasedRepositoryLoader"; import { TimedApiBasedRepositoryLoader } from "../../src/loaders/TimedApiBasedRepositoryLoader";
import { resetGlobalFetchMockJson } from "../mockHelpers/fetchMockHelpers"; import { resetGlobalFetchMockJson } from "../mockHelpers/fetchMockHelpers";
import { GetterSetterRepository } from "../../src/repositories/GetterSetterRepository"; import { UnoptimizedInMemoryRepository } from "../../src/repositories/UnoptimizedInMemoryRepository";
describe("TimedApiBasedRepositoryLoader", () => { describe("TimedApiBasedRepositoryLoader", () => {
let repositoryMock: GetterSetterRepository;
let loader: TimedApiBasedRepositoryLoader; let loader: TimedApiBasedRepositoryLoader;
let spies: any; let spies: any;
@@ -16,15 +15,7 @@ describe("TimedApiBasedRepositoryLoader", () => {
beforeEach(() => { beforeEach(() => {
resetGlobalFetchMockJson(); resetGlobalFetchMockJson();
repositoryMock = { loader = new TimedApiBasedRepositoryLoader(new UnoptimizedInMemoryRepository());
clearSystemData: jest.fn(),
clearRouteData: jest.fn(),
clearStopData: jest.fn(),
clearShuttleData: jest.fn(),
clearEtaData: jest.fn(),
} as unknown as GetterSetterRepository;
loader = new TimedApiBasedRepositoryLoader(repositoryMock);
spies = { spies = {
fetchAndUpdateSystemData: jest.spyOn(loader, 'fetchAndUpdateSystemData'), fetchAndUpdateSystemData: jest.spyOn(loader, 'fetchAndUpdateSystemData'),
@@ -49,9 +40,6 @@ describe("TimedApiBasedRepositoryLoader", () => {
await loader.start(); await loader.start();
expect(loader["shouldBeRunning"]).toBe(true); expect(loader["shouldBeRunning"]).toBe(true);
Object.values(repositoryMock).forEach((mockFn) => {
expect(mockFn).toHaveBeenCalled();
});
Object.values(spies).forEach((spy: any) => { Object.values(spies).forEach((spy: any) => {
expect(spy).toHaveBeenCalled(); expect(spy).toHaveBeenCalled();
}); });
@@ -64,9 +52,6 @@ describe("TimedApiBasedRepositoryLoader", () => {
await loader.start(); await loader.start();
await loader.start(); await loader.start();
Object.values(repositoryMock).forEach((mockFn) => {
expect(mockFn).toHaveBeenCalledTimes(1);
});
Object.values(spies).forEach((spy: any) => { Object.values(spies).forEach((spy: any) => {
expect(spy).toHaveBeenCalledTimes(1); expect(spy).toHaveBeenCalledTimes(1);
}); });

View File

@@ -429,6 +429,194 @@ describe("UnoptimizedInMemoryRepository", () => {
}); });
}); });
describe("removeSystemIfExists", () => {
test("removes system given ID", async () => {
const mockSystems = generateMockSystems();
await Promise.all(mockSystems.map(async (system) => {
await repository.addOrUpdateSystem(system);
}));
const systemToRemove = mockSystems[0];
await repository.removeSystemIfExists(systemToRemove.id);
const remainingSystems = await repository.getSystems();
expect(remainingSystems).toHaveLength(mockSystems.length - 1);
});
test("does nothing if system doesn't exist", async () => {
const mockSystems = generateMockSystems();
await Promise.all(mockSystems.map(async (system) => {
await repository.addOrUpdateSystem(system);
}));
await repository.removeSystemIfExists("nonexistent-id");
const remainingSystems = await repository.getSystems();
expect(remainingSystems).toHaveLength(mockSystems.length);
});
});
describe("removeRouteIfExists", () => {
test("removes route given ID", async () => {
const systemId = "1";
const mockRoutes = generateMockRoutes();
await Promise.all(mockRoutes.map(async (route) => {
route.systemId = systemId;
await repository.addOrUpdateRoute(route);
}));
const routeToRemove = mockRoutes[0];
await repository.removeRouteIfExists(routeToRemove.id);
const remainingRoutes = await repository.getRoutesBySystemId(systemId);
expect(remainingRoutes).toHaveLength(mockRoutes.length - 1);
});
test("does nothing if route doesn't exist", async () => {
const systemId = "1";
const mockRoutes = generateMockRoutes();
await Promise.all(mockRoutes.map(async (route) => {
route.systemId = systemId;
await repository.addOrUpdateRoute(route);
}));
await repository.removeRouteIfExists("nonexistent-id");
const remainingRoutes = await repository.getRoutesBySystemId(systemId);
expect(remainingRoutes).toHaveLength(mockRoutes.length);
});
});
describe("removeShuttleIfExists", () => {
test("removes shuttle given ID", async () => {
const systemId = "1";
const mockShuttles = generateMockShuttles();
await Promise.all(mockShuttles.map(async (shuttle) => {
shuttle.systemId = systemId;
await repository.addOrUpdateShuttle(shuttle);
}));
const shuttleToRemove = mockShuttles[0];
await repository.removeShuttleIfExists(shuttleToRemove.id);
const remainingShuttles = await repository.getShuttlesBySystemId(systemId);
expect(remainingShuttles).toHaveLength(mockShuttles.length - 1);
});
test("does nothing if shuttle doesn't exist", async () => {
const systemId = "1";
const mockShuttles = generateMockShuttles();
await Promise.all(mockShuttles.map(async (shuttle) => {
shuttle.systemId = systemId;
await repository.addOrUpdateShuttle(shuttle);
}));
await repository.removeShuttleIfExists("nonexistent-id");
const remainingShuttles = await repository.getShuttlesBySystemId(systemId);
expect(remainingShuttles).toHaveLength(mockShuttles.length);
});
});
describe("removeStopIfExists", () => {
test("removes stop given ID", async () => {
const systemId = "1";
const mockStops = generateMockStops();
await Promise.all(mockStops.map(async (stop) => {
stop.systemId = systemId;
await repository.addOrUpdateStop(stop);
}));
const stopToRemove = mockStops[0];
await repository.removeStopIfExists(stopToRemove.id);
const remainingStops = await repository.getStopsBySystemId(systemId);
expect(remainingStops).toHaveLength(mockStops.length - 1);
});
test("does nothing if stop doesn't exist", async () => {
const systemId = "1";
const mockStops = generateMockStops();
await Promise.all(mockStops.map(async (stop) => {
stop.systemId = systemId;
await repository.addOrUpdateStop(stop);
}));
await repository.removeStopIfExists("nonexistent-id");
const remainingStops = await repository.getStopsBySystemId(systemId);
expect(remainingStops).toHaveLength(mockStops.length);
});
});
describe("removeOrderedStopIfExists", () => {
test("removes ordered stop given stop ID and route ID", async () => {
let mockOrderedStops = generateMockOrderedStops();
const routeId = mockOrderedStops[0].routeId;
mockOrderedStops = mockOrderedStops.filter((orderedStop) => orderedStop.routeId === routeId);
await Promise.all(mockOrderedStops.map(async (stop) => {
stop.routeId = routeId;
await repository.addOrUpdateOrderedStop(stop);
}));
const orderedStopToRemove = mockOrderedStops[0];
await repository.removeOrderedStopIfExists(orderedStopToRemove.stopId, orderedStopToRemove.routeId);
const remainingOrderedStops = await repository.getOrderedStopsByRouteId(routeId);
expect(remainingOrderedStops).toHaveLength(mockOrderedStops.length - 1);
});
test("does nothing if ordered stop doesn't exist", async () => {
let mockOrderedStops = generateMockOrderedStops();
const routeId = mockOrderedStops[0].routeId;
mockOrderedStops = mockOrderedStops.filter((orderedStop) => orderedStop.routeId === routeId);
await Promise.all(mockOrderedStops.map(async (stop) => {
stop.routeId = routeId;
await repository.addOrUpdateOrderedStop(stop);
}));
await repository.removeOrderedStopIfExists("nonexistent-stop-id", "nonexistent-route-id");
const remainingOrderedStops = await repository.getOrderedStopsByRouteId(routeId);
expect(remainingOrderedStops).toHaveLength(mockOrderedStops.length);
});
});
describe("removeEtaIfExists", () => {
test("removes eta given shuttle ID and stop ID", async () => {
let mockEtas = generateMockEtas();
const stopId = mockEtas[0].stopId;
mockEtas = mockEtas.filter((eta) => eta.stopId === stopId);
await Promise.all(mockEtas.map(async (eta) => {
eta.stopId = stopId;
await repository.addOrUpdateEta(eta);
}));
const etaToRemove = mockEtas[0];
await repository.removeEtaIfExists(etaToRemove.shuttleId, etaToRemove.stopId);
const remainingEtas = await repository.getEtasForStopId(stopId);
expect(remainingEtas).toHaveLength(mockEtas.length - 1);
});
test("does nothing if eta doesn't exist", async () => {
let mockEtas = generateMockEtas();
const stopId = mockEtas[0].stopId;
mockEtas = mockEtas.filter((eta) => eta.stopId === stopId);
await Promise.all(mockEtas.map(async (eta) => {
eta.stopId = stopId;
await repository.addOrUpdateEta(eta);
}));
await repository.removeEtaIfExists("nonexistent-shuttle-id", "nonexistent-stop-id");
const remainingEtas = await repository.getEtasForStopId(stopId);
expect(remainingEtas).toHaveLength(mockEtas.length);
});
});
describe("clearSystemData", () => { describe("clearSystemData", () => {
test("clears all systems from the repository", async () => { test("clears all systems from the repository", async () => {
const mockSystems = generateMockSystems(); const mockSystems = generateMockSystems();