Initial commit: Aeroflot Flights Web Angular 12 application
This commit is contained in:
+8
@@ -0,0 +1,8 @@
|
||||
<div class="map-wrapper">
|
||||
<div id="map" class="map"></div>
|
||||
<loader-sheet *ngIf="isLoading"></loader-sheet>
|
||||
<no-directions-sheet
|
||||
*ngIf="isNoDirections && !isLoading"
|
||||
(dismiss)="hideNoDirections()">
|
||||
</no-directions-sheet>
|
||||
</div>
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
.map-wrapper {
|
||||
position: relative;
|
||||
height: 800px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.map {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
|
||||
+780
@@ -0,0 +1,780 @@
|
||||
import { AfterViewInit, Component, OnInit } from '@angular/core';
|
||||
import { DictionariesService } from '@app/modules/components/page-filters/services/dictionaries-service';
|
||||
import { FlightsMapFiltersStateService, IFlightsMapFilterState } from '@app/shared/services/filters/flights-map-filters-state.service';
|
||||
import { environment } from '@environment';
|
||||
import * as L from 'leaflet';
|
||||
import { from, of, Subject, Subscription } from 'rxjs';
|
||||
import { catchError, distinctUntilChanged, finalize, map, switchMap, takeUntil, tap } from 'rxjs/operators';
|
||||
import { FlightsMapApiService, IDestinationsRequestParams } from '../../services/flights-map-api.service';
|
||||
import { CityCategoryService } from '../../services/category-city.service';
|
||||
import { IDestinationsResponse } from '@typings/responses';
|
||||
import * as moment from 'moment';
|
||||
import { CityModel } from '@app/modules/components/page-filters/models';
|
||||
import { IDestinationResponse } from '../../../../../typings/responses';
|
||||
import { airports } from '@app/shared/services';
|
||||
|
||||
|
||||
export const markerBlue = L.icon({
|
||||
iconUrl: 'assets/img/leaflet/marker-blue.png',
|
||||
iconSize: [15, 15],
|
||||
iconAnchor: [5, 5],
|
||||
popupAnchor: [0, -10]
|
||||
});
|
||||
|
||||
export const markerBlueSmall = L.icon({
|
||||
iconUrl: 'assets/img/leaflet/marker-blue-small.png',
|
||||
iconSize: [11, 11],
|
||||
iconAnchor: [5, 5],
|
||||
popupAnchor: [0, -10]
|
||||
});
|
||||
|
||||
export const markerOrange = L.icon({
|
||||
iconUrl: 'assets/img/leaflet/marker-orange.png',
|
||||
iconSize: [20, 20],
|
||||
iconAnchor: [10, 10],
|
||||
popupAnchor: [0, -20]
|
||||
});
|
||||
|
||||
const directRoutePolyLine : L.PolylineOptions =
|
||||
{
|
||||
color : '#2457ff',
|
||||
weight : 1,
|
||||
opacity: 1
|
||||
}
|
||||
|
||||
const dashRoutePolyLine: L.PolylineOptions =
|
||||
{
|
||||
color : '#2433ff',
|
||||
weight : 1,
|
||||
opacity : 1,
|
||||
dashArray: '4 14'
|
||||
}
|
||||
|
||||
type CountryType = 'ru' | 'other';
|
||||
|
||||
@Component({
|
||||
selector: 'flights-map-body',
|
||||
templateUrl: './flights-map-body.component.html',
|
||||
styleUrls: ['./flights-map-body.component.scss']
|
||||
})
|
||||
export class FlightsMapBodyComponent implements OnInit, AfterViewInit {
|
||||
|
||||
private currentFilterState: IFlightsMapFilterState;
|
||||
|
||||
private map!: L.Map
|
||||
|
||||
/** код города ИЛИ код аэропорта → маркер */
|
||||
private markerIndex = new Map<string, L.Marker>();
|
||||
|
||||
private airportToCityCode = new Map<string, string>();
|
||||
|
||||
private highlighted: Set<L.Marker> = new Set<L.Marker>();
|
||||
private highlightedLayer: L.LayerGroup<L.Marker>;
|
||||
|
||||
private zoomLayers: Record<CountryType, Record<number, L.LayerGroup>> = { ru: {}, other: {} };
|
||||
|
||||
private departurePopup?: L.Popup;
|
||||
private routePopup?: L.Popup;
|
||||
|
||||
private destinationsSub?: Subscription;
|
||||
private destinationsLayer: L.LayerGroup;
|
||||
private destinations: IDestinationsResponse = { data : { routes: []} };
|
||||
|
||||
private skipNextFetchOnce = false; //флаг для пропуска повторного запроса при синхронизации UI-фильтра
|
||||
|
||||
private destroy$ = new Subject<void>()
|
||||
|
||||
isLoading = true;
|
||||
isNoDirections: boolean;
|
||||
|
||||
constructor(
|
||||
private dictService: DictionariesService,
|
||||
private cityCategoryService: CityCategoryService,
|
||||
private filterStateService: FlightsMapFiltersStateService,
|
||||
private apiService: FlightsMapApiService
|
||||
) { }
|
||||
|
||||
async ngOnInit(): Promise<void> {
|
||||
}
|
||||
|
||||
async ngAfterViewInit(): Promise<void> {
|
||||
await this.dictService.ready$;
|
||||
|
||||
this.initMap();
|
||||
this.initMarkers();
|
||||
this.watchRouteChanges();
|
||||
this.watchDateChanges();
|
||||
|
||||
this.isLoading = false;
|
||||
}
|
||||
|
||||
private initMap() {
|
||||
const baseMapURl = environment.mapApiUrl;
|
||||
|
||||
const southWest: L.LatLngExpression = [ -70, -185 ];
|
||||
const northEast: L.LatLngExpression = [ 80, 200 ];
|
||||
|
||||
|
||||
this.map = L.map('map',
|
||||
{
|
||||
center: [53, 45],
|
||||
zoom: 5,
|
||||
attributionControl: false,
|
||||
maxBounds: [ southWest, northEast ],
|
||||
maxBoundsViscosity: 1
|
||||
});
|
||||
|
||||
[2, 3, 4, 5, 6].forEach(z => {
|
||||
this.zoomLayers.ru[z] = L.layerGroup();
|
||||
this.zoomLayers.other[z] = L.layerGroup();
|
||||
});
|
||||
|
||||
L.tileLayer(baseMapURl, {
|
||||
maxZoom: 6,
|
||||
minZoom: 3,
|
||||
}).addTo(this.map);
|
||||
|
||||
this.destinationsLayer = L.layerGroup().addTo(this.map);
|
||||
this.highlightedLayer = L.layerGroup().addTo(this.map);
|
||||
}
|
||||
|
||||
private initMarkers() {
|
||||
this.dictService.citiesAll.forEach(city => {
|
||||
|
||||
if (city.location?.lat == null || city.location?.lon == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const marker = L.marker(
|
||||
[city.location.lat, city.location.lon],
|
||||
{
|
||||
icon: markerBlueSmall,
|
||||
title: city.code,
|
||||
}
|
||||
)
|
||||
.on('click', ()=> this.handleMarkerClick(city.code))
|
||||
.bindTooltip(city.name, {
|
||||
permanent : true,
|
||||
direction : 'top',
|
||||
className : 'city-label'
|
||||
});
|
||||
|
||||
const countryType: CountryType = city.country_code === 'RU' ? 'ru' : 'other';
|
||||
const zMin = this.cityCategoryService.zoomLevel(city.code);
|
||||
|
||||
this.zoomLayers[countryType][zMin].addLayer(marker);
|
||||
|
||||
this.markerIndex.set(city.code, marker);
|
||||
});
|
||||
|
||||
this.dictService.airportsAll?.forEach(airport => {
|
||||
const cityMarker = this.markerIndex.get(airport.city_code);
|
||||
|
||||
if (!cityMarker) return;
|
||||
|
||||
this.markerIndex.set(airport.code, cityMarker);
|
||||
this.airportToCityCode.set(airport.code, airport.city_code);
|
||||
});
|
||||
|
||||
this.updateVisibility();
|
||||
this.map.on('zoomend', () => this.updateVisibility());
|
||||
|
||||
}
|
||||
|
||||
private getMarkerByAnyCode(code: string): L.Marker | undefined {
|
||||
return this.markerIndex.get(code);
|
||||
}
|
||||
|
||||
private updateHighlight(state: IFlightsMapFilterState): void {
|
||||
|
||||
this.highlighted.forEach(marker =>
|
||||
{
|
||||
marker.setIcon(markerBlueSmall);
|
||||
|
||||
const code = (marker as any).options?.title as string;
|
||||
const zlvl = this.cityCategoryService.zoomLevel(code);
|
||||
const countryType = this.dictService.getCityByCode(code).country_code === 'RU' ? 'ru': 'other';
|
||||
|
||||
this.moveBetweenLayer(marker, this.highlightedLayer, this.zoomLayers[countryType][zlvl]);
|
||||
});
|
||||
|
||||
this.highlighted.clear();
|
||||
this.highlightedLayer.clearLayers();
|
||||
|
||||
const codes = [state.departure, state.arrival].filter(Boolean) as string[];
|
||||
|
||||
codes.forEach(code => {
|
||||
const marker = this.getMarkerByAnyCode(code);
|
||||
|
||||
if (!marker) { return; }
|
||||
|
||||
marker.setIcon(markerOrange);
|
||||
this.highlighted.add(marker);
|
||||
|
||||
const zLvl = this.cityCategoryService.zoomLevel(code);
|
||||
const countryType = this.dictService.getCityByCode(code).country_code === 'RU' ? 'ru': 'other';
|
||||
this.moveBetweenLayer(marker, this.zoomLayers[countryType][zLvl], this.highlightedLayer);
|
||||
});
|
||||
|
||||
this.updateVisibility();
|
||||
}
|
||||
|
||||
private updateVisibility(): void
|
||||
{
|
||||
this.updateMarkers();
|
||||
this.updateHighlightedTooltips();
|
||||
|
||||
// if(this.currentFilterState){
|
||||
// this.fetchAndDraw(this.currentFilterState);
|
||||
// }
|
||||
|
||||
}
|
||||
|
||||
private updateMarkers() {
|
||||
const z = this.map.getZoom();
|
||||
|
||||
(['ru', 'other'] as const).forEach(countryType => {
|
||||
|
||||
|
||||
Object.entries(this.zoomLayers[countryType]).forEach(([lvl, layer]) =>
|
||||
{
|
||||
|
||||
const layerShouldBeVisible = +lvl <= z;
|
||||
|
||||
if(countryType === 'ru')
|
||||
{
|
||||
|
||||
if(!this.currentFilterState?.international && layerShouldBeVisible)
|
||||
{
|
||||
this.map.addLayer(layer);
|
||||
}
|
||||
else {
|
||||
this.map.removeLayer(layer);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if(!this.currentFilterState?.domestic && layerShouldBeVisible)
|
||||
{
|
||||
this.map.addLayer(layer);
|
||||
}
|
||||
else {
|
||||
this.map.removeLayer(layer);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
private updateHighlightedTooltips() {
|
||||
const z = this.map.getZoom();
|
||||
|
||||
if(z <= 3)
|
||||
{
|
||||
this.markerIndex.forEach((m) => {
|
||||
|
||||
if(this.highlighted.has(m))
|
||||
{
|
||||
return;
|
||||
}
|
||||
m.closeTooltip();
|
||||
});
|
||||
}
|
||||
else if(this.highlighted.size >= 2)
|
||||
{
|
||||
this.markerIndex.forEach((m) => {
|
||||
|
||||
if(this.highlighted.has(m))
|
||||
{
|
||||
return;
|
||||
}
|
||||
m.closeTooltip();
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
this.markerIndex.forEach((m) => {
|
||||
m.openTooltip();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private updateIntermediateTooltip() {
|
||||
const routes = this.destinations?.data.routes;
|
||||
|
||||
if(!routes) {return;}
|
||||
|
||||
for (let i = 0; i < routes.length; i++)
|
||||
{
|
||||
const route = routes[i].route;
|
||||
|
||||
if(route.length <= 2){ continue; }
|
||||
|
||||
for (let cityIndex = 1; cityIndex < route.length - 1; cityIndex++)
|
||||
{
|
||||
const intermediateCityCode = route[cityIndex];
|
||||
|
||||
const intermediateCityMarker = this.getMarkerByAnyCode(intermediateCityCode);
|
||||
|
||||
if(!intermediateCityMarker) {continue;}
|
||||
|
||||
intermediateCityMarker.openTooltip();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private watchRouteChanges() {
|
||||
this.filterStateService.state$
|
||||
.pipe(
|
||||
distinctUntilChanged(
|
||||
(a, b) => a.departure === b.departure &&
|
||||
a.arrival === b.arrival &&
|
||||
a.connections === b.connections &&
|
||||
a.domestic === b.domestic &&
|
||||
a.international === b.international
|
||||
),
|
||||
takeUntil(this.destroy$),
|
||||
tap(_=> this.isLoading = true)
|
||||
)
|
||||
.subscribe(state => {
|
||||
this.currentFilterState = state;
|
||||
this.updateHighlight(state);
|
||||
|
||||
this.hideNoDirections();
|
||||
|
||||
if (this.skipNextFetchOnce) {
|
||||
this.skipNextFetchOnce = false; // пропускаем ровно один раз
|
||||
return;
|
||||
}
|
||||
|
||||
this.fetchAndDraw(state);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
private watchDateChanges() {
|
||||
this.filterStateService.state$
|
||||
.pipe(
|
||||
distinctUntilChanged(
|
||||
(a, b) => a.date === b.date
|
||||
),
|
||||
takeUntil(this.destroy$)
|
||||
)
|
||||
.subscribe(state => {
|
||||
this.currentFilterState = state;
|
||||
|
||||
if(this.destinations?.data?.routes?.length > 0)
|
||||
{
|
||||
this.showRoutePopup(this.destinations.data.routes);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
private handleMarkerClick(cityCode: string) : void
|
||||
{
|
||||
if(!this.currentFilterState.departure)
|
||||
{
|
||||
this.filterStateService.setDeparture(cityCode);
|
||||
}
|
||||
else if(this.currentFilterState.departure && !this.currentFilterState.arrival)
|
||||
{
|
||||
if (cityCode !== this.currentFilterState.departure)
|
||||
{
|
||||
this.filterStateService.setArrival(cityCode);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
this.filterStateService.setDeparture(cityCode);
|
||||
this.filterStateService.setArrival(undefined);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
private fetchAndDraw(state: IFlightsMapFilterState)
|
||||
{
|
||||
|
||||
this.clearRoutes();
|
||||
this.clearPopup();
|
||||
|
||||
const dateFrom = new Date();
|
||||
dateFrom.setDate(dateFrom.getDate() - 1);
|
||||
dateFrom.setHours(0, 0, 0, 0);
|
||||
|
||||
const dateTo = new Date();
|
||||
dateTo.setMonth(dateFrom.getMonth() + 6);
|
||||
dateTo.setHours(0, 0, 0, 0);
|
||||
|
||||
|
||||
if((state.departure && state.arrival) && (state.departure != state.arrival))
|
||||
{
|
||||
this.fetchAndDrawRoute(state.departure, state.arrival, dateFrom, dateTo, state.connections? 1: 0);
|
||||
}
|
||||
else if(state.departure && !state.arrival)
|
||||
{
|
||||
this.fetchAndDrawSpider(state.departure, dateFrom, dateTo);
|
||||
}else
|
||||
{
|
||||
this.isLoading = false;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private fetchAndDrawRoute(departure: string, arrival: string, dateFrom: Date, dateTo: Date, connections: number) {
|
||||
|
||||
const base: IDestinationsRequestParams = {
|
||||
departure: departure,
|
||||
arrival: arrival,
|
||||
dateFrom: dateFrom,
|
||||
dateTo: dateTo,
|
||||
connections: connections
|
||||
};
|
||||
|
||||
const withConn1: IDestinationsRequestParams = { ...base, connections: 1 };
|
||||
|
||||
this.destinationsSub = this.apiService.getDestinations(base).pipe(
|
||||
switchMap(first => {
|
||||
const hasRoutes = !!first?.data?.routes?.length;
|
||||
|
||||
if (hasRoutes || base.connections === 1) {
|
||||
return of({ res: first, usedConn: base.connections ?? 0 });
|
||||
}
|
||||
// второй заход с пересадками
|
||||
return this.apiService.getDestinations(withConn1).pipe(
|
||||
map((second: IDestinationsResponse) => {
|
||||
|
||||
const hasSecond = !!second?.data?.routes?.length;
|
||||
|
||||
return {
|
||||
res: hasSecond ? second : first,
|
||||
usedConn: hasSecond ? 1 : 0
|
||||
};
|
||||
})
|
||||
);
|
||||
}),
|
||||
tap(({ usedConn }) => {
|
||||
// если фолбэк (показывать с пересадкой) дал маршруты и в UI ещё выключены пересадки — включим их
|
||||
|
||||
this.isLoading = true;
|
||||
|
||||
if (usedConn === 1 && !this.currentFilterState.connections) {
|
||||
this.skipNextFetchOnce = true; // не дёргать повторно fetch
|
||||
this.filterStateService.setConnections(true); // обновим UI-состояние
|
||||
}
|
||||
}),
|
||||
map(res => this.filterRoutes(res.res)),
|
||||
catchError(() => of<IDestinationsResponse>({ data: { routes: [] } })),
|
||||
takeUntil(this.destroy$),
|
||||
finalize(() => (this.isLoading = false))
|
||||
)
|
||||
.subscribe(destinations => {
|
||||
this.destinations = destinations;
|
||||
this.buildRoute(destinations);
|
||||
this.updateIntermediateTooltip();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
private fetchAndDrawSpider(cityFromCode: string, dateFrom: Date, dateTo: Date)
|
||||
{
|
||||
const params: IDestinationsRequestParams = {
|
||||
departure : cityFromCode,
|
||||
dateFrom: dateFrom,
|
||||
dateTo: dateTo
|
||||
};
|
||||
|
||||
this.currentFilterState.connections = false;
|
||||
this.destinationsSub = this.getDestinations(params)
|
||||
.pipe(
|
||||
finalize(() => (this.isLoading = false))
|
||||
)
|
||||
.subscribe(destinations =>
|
||||
this.buildSpider(destinations)
|
||||
);
|
||||
}
|
||||
|
||||
private getDestinations(params: IDestinationsRequestParams) {
|
||||
return this.apiService.getDestinations(params).pipe(
|
||||
catchError(() => of<IDestinationsResponse>({
|
||||
data: { routes: [] }
|
||||
}))
|
||||
);
|
||||
}
|
||||
|
||||
private filterRoutes(res: IDestinationsResponse): IDestinationsResponse {
|
||||
const routes = res?.data?.routes ?? [];
|
||||
const { domestic, international, connections } = this.currentFilterState;
|
||||
|
||||
const isDomestic = (r: { route: string[] }) =>
|
||||
r.route.every(code => this.dictService.ruCitiesCodes.has(this.dictService.getCityCodeByAirportCode(code)));
|
||||
|
||||
const isInternational = (r: { route: string[] }) =>
|
||||
r.route.some(code => this.dictService.otherCitiesCodes.has(this.dictService.getCityCodeByAirportCode(code)));
|
||||
|
||||
const hasConnections = (r: { isDirect: boolean }) => !r.isDirect;
|
||||
|
||||
const predicates: Array<(r: any) => boolean> = [];
|
||||
|
||||
if (domestic && !international) {
|
||||
predicates.push(isDomestic);
|
||||
} else if (international && !domestic) {
|
||||
predicates.push(isInternational);
|
||||
}
|
||||
|
||||
if (connections) {
|
||||
predicates.push(hasConnections);
|
||||
}
|
||||
|
||||
const filtered = predicates.length
|
||||
? routes.filter(r => predicates.every(p => p(r)))
|
||||
: routes;
|
||||
|
||||
return {
|
||||
...res,
|
||||
data: {
|
||||
...res.data,
|
||||
routes: filtered
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private buildRoute(res: IDestinationsResponse): void
|
||||
{
|
||||
const routes = res.data?.routes ?? [];
|
||||
|
||||
if(routes.length == 0)
|
||||
{
|
||||
this.isNoDirections = true;
|
||||
}
|
||||
|
||||
if (!routes.length) { return; }
|
||||
|
||||
let line : L.Polyline;
|
||||
|
||||
routes.filter(_ => _.isDirect).forEach(path => {
|
||||
this.drawPolyline(path.route, directRoutePolyLine, this.destinationsLayer);
|
||||
});
|
||||
|
||||
routes.filter(_ => !_.isDirect).forEach(path => {
|
||||
this.drawPolyline(path.route, dashRoutePolyLine, this.destinationsLayer);
|
||||
});
|
||||
|
||||
|
||||
this.showRoutePopup(routes);
|
||||
}
|
||||
|
||||
private buildSpider(res: IDestinationsResponse): void
|
||||
{
|
||||
const routes = res.data?.routes ?? [];
|
||||
if (!routes.length) { return; }
|
||||
|
||||
const fromCode = routes[0].route[0];
|
||||
const destCodes = new Set<string>();
|
||||
|
||||
routes.forEach(path => {
|
||||
if (Array.isArray(path.route) && path.route.length > 1)
|
||||
{
|
||||
const dest = path.route[path.route.length - 1];
|
||||
if (dest !== fromCode) { destCodes.add(dest); }
|
||||
}
|
||||
});
|
||||
|
||||
destCodes.forEach(code => {
|
||||
this.drawPolyline([fromCode, code], directRoutePolyLine, this.destinationsLayer);
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
private drawPolyline(cities: string[], style: L.PolylineOptions, target: L.LayerGroup | L.Map = this.map): L.Polyline | undefined
|
||||
{
|
||||
const segments: L.LatLng[] = [];
|
||||
|
||||
const visibleCities = cities.filter(code => {
|
||||
const m = this.getMarkerByAnyCode(code);
|
||||
return m && this.map.hasLayer(m);
|
||||
});
|
||||
|
||||
if (visibleCities.length < 2) { return; }
|
||||
|
||||
for (let i = 0; i < visibleCities.length - 1; i++) {
|
||||
const from = this.getMarkerByAnyCode(visibleCities[i])!;
|
||||
const to = this.getMarkerByAnyCode(visibleCities[i + 1])!;
|
||||
|
||||
const arc = this.buildGreatCircle(from.getLatLng(), to.getLatLng());
|
||||
segments.push(...(i === 0 ? arc : arc.slice(1)));
|
||||
}
|
||||
|
||||
return L.polyline(segments, style).addTo(target);
|
||||
|
||||
}
|
||||
|
||||
private clearRoutes()
|
||||
{
|
||||
this.destinationsSub?.unsubscribe();
|
||||
this.destinationsLayer?.clearLayers();
|
||||
this.destinations = {data: {routes:[]}};
|
||||
}
|
||||
|
||||
private clearPopup(){
|
||||
if (this.routePopup) {
|
||||
this.map.removeLayer(this.routePopup);
|
||||
this.routePopup = undefined;
|
||||
}
|
||||
|
||||
if(this.departurePopup){
|
||||
this.map.removeLayer(this.departurePopup);
|
||||
this.departurePopup = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
private showRoutePopup(rotues: IDestinationResponse[]): void {
|
||||
|
||||
const firstRoute = rotues[0].route;
|
||||
const departureCode = firstRoute[0];
|
||||
const arrivalCode = firstRoute[firstRoute.length -1];
|
||||
|
||||
const markerDeparture = this.getMarkerByAnyCode(departureCode);
|
||||
const markerArrival = this.getMarkerByAnyCode(arrivalCode);
|
||||
if (!markerArrival) { return; }
|
||||
|
||||
|
||||
const cityDeparture = this.dictService.getCityByCode(this.airportToCityCode.get(departureCode));
|
||||
const cityArrival = this.dictService.getCityByCode(this.airportToCityCode.get(arrivalCode));
|
||||
|
||||
if(!cityArrival) { return; }
|
||||
|
||||
|
||||
this.clearPopup();
|
||||
|
||||
const linkUrl = this.getLink();
|
||||
|
||||
//ToDo: translate
|
||||
const buyTicketText = 'Купить билет';
|
||||
|
||||
const htmlDeparture = `
|
||||
<div class="popup-header-test">
|
||||
<span>${cityDeparture.name}</span>
|
||||
</div>
|
||||
`;
|
||||
|
||||
const htmlInermediate = `
|
||||
<div class="popup-header-test">
|
||||
<span>${cityDeparture.name}</span>
|
||||
</div>
|
||||
`;
|
||||
|
||||
const htmlArrival = `
|
||||
<div class="popup-header-test">
|
||||
<span>${cityArrival.name}</span>
|
||||
|
||||
</div>
|
||||
<div style="text-align:center;">
|
||||
<a href="${linkUrl}" target="_blank" class="popup-buy-ticket">
|
||||
${buyTicketText}
|
||||
</a>
|
||||
</div>
|
||||
`;
|
||||
|
||||
this.departurePopup = L.popup({
|
||||
closeButton: true,
|
||||
autoClose: false,
|
||||
closeOnClick: false
|
||||
})
|
||||
.setLatLng(markerDeparture.getLatLng())
|
||||
.setContent(htmlDeparture)
|
||||
.openOn(this.map);
|
||||
|
||||
|
||||
this.routePopup = L.popup({
|
||||
closeButton: true,
|
||||
autoClose: false,
|
||||
closeOnClick: false
|
||||
})
|
||||
.setLatLng(markerArrival.getLatLng())
|
||||
.setContent(htmlArrival)
|
||||
.openOn(this.map);
|
||||
}
|
||||
|
||||
private moveBetweenLayer(marker: L.Marker, from: L.LayerGroup, to: L.LayerGroup)
|
||||
{
|
||||
if (from?.hasLayer(marker)) {
|
||||
from.removeLayer(marker);
|
||||
}
|
||||
|
||||
if (!to?.hasLayer(marker)) {
|
||||
to.addLayer(marker);
|
||||
}
|
||||
}
|
||||
|
||||
private buildGreatCircle(from: L.LatLng, to: L.LatLng, segments = 64): L.LatLng[]
|
||||
{
|
||||
const φ1 = this.deg2rad(from.lat);
|
||||
const λ1 = this.deg2rad(from.lng);
|
||||
const φ2 = this.deg2rad(to.lat);
|
||||
const λ2 = this.deg2rad(to.lng);
|
||||
|
||||
const Δ = 2 * Math.asin(
|
||||
Math.sqrt(
|
||||
Math.sin((φ2 - φ1) / 2) ** 2 +
|
||||
Math.cos(φ1) * Math.cos(φ2) *
|
||||
Math.sin((λ2 - λ1) / 2) ** 2
|
||||
)
|
||||
);
|
||||
|
||||
if (Δ === 0) { return [from, to]; } // совпадают
|
||||
|
||||
const points: L.LatLng[] = [];
|
||||
|
||||
for (let i = 0; i <= segments; i++)
|
||||
{
|
||||
const f = i / segments;
|
||||
const A = Math.sin((1 - f) * Δ) / Math.sin(Δ);
|
||||
const B = Math.sin(f * Δ) / Math.sin(Δ);
|
||||
|
||||
const x =
|
||||
A * Math.cos(φ1) * Math.cos(λ1) +
|
||||
B * Math.cos(φ2) * Math.cos(λ2);
|
||||
|
||||
const y =
|
||||
A * Math.cos(φ1) * Math.sin(λ1) +
|
||||
B * Math.cos(φ2) * Math.sin(λ2);
|
||||
|
||||
const z =
|
||||
A * Math.sin(φ1) +
|
||||
B * Math.sin(φ2);
|
||||
|
||||
const φi = Math.atan2(z, Math.sqrt(x * x + y * y));
|
||||
const λi = Math.atan2(y, x);
|
||||
|
||||
points.push(L.latLng(this.rad2deg(φi), this.rad2deg(λi)));
|
||||
}
|
||||
|
||||
return points;
|
||||
}
|
||||
|
||||
private deg2rad(deg: number): number { return (deg * Math.PI) / 180; }
|
||||
private rad2deg(rad: number): number { return (rad * 180) / Math.PI; }
|
||||
|
||||
private getLink(): string {
|
||||
const state = this.currentFilterState;
|
||||
|
||||
const d = new Date();
|
||||
d.setHours(0,0,0,0);
|
||||
|
||||
const date = moment(state.date ?? d).format('YYYYMMDD');
|
||||
const params = `${state.departure}.${date}.${state.arrival}`;
|
||||
|
||||
return `https://www.aeroflot.ru/sb/app/ru-ru#/search?adults=1&cabin=economy&children=0&infants=0&routes=${params}&autosearch=Y&utm_source=aflwebbot&utm_medium=referral&utm_campaign=ref_3015_general_rf_button.index__all_flight.map`;
|
||||
}
|
||||
|
||||
hideNoDirections(): void {
|
||||
this.isNoDirections = false;
|
||||
}
|
||||
}
|
||||
+83
@@ -0,0 +1,83 @@
|
||||
<section>
|
||||
<p-accordion expandIcon="" collapseIcon="" [activeIndex]="0">
|
||||
<p-accordionTab [selected]="true" [disabled]="true">
|
||||
<div class="flights-map-filter-content">
|
||||
|
||||
<div class="flights-map-filter-header">
|
||||
<h3>{{ 'FLIGHTS-MAP.ROUTE' | translate }}</h3>
|
||||
</div>
|
||||
|
||||
<div class="flights-map-filter-content-cities">
|
||||
<city-autocomplete
|
||||
label="SHARED.DEPARTURE_CITY"
|
||||
[(ngModel)]="departure"
|
||||
[placeholder]="departurePlaceholder"
|
||||
data-testid="route-departure-city-input">
|
||||
</city-autocomplete>
|
||||
|
||||
<div class="change-container">
|
||||
<button
|
||||
class="button-change"
|
||||
pButton
|
||||
type="button"
|
||||
(click)="exchange()">
|
||||
<svg class="svg--change-city">
|
||||
<use xlink:href="/assets/img/sprite.svg#changeCity"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<city-autocomplete
|
||||
label="SHARED.ARRIVAL_CITY"
|
||||
[(ngModel)]="arrival"
|
||||
[placeholder]="arrivalPlaceholder"
|
||||
data-testid="route-arrival-city-input"
|
||||
></city-autocomplete>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="flights-map-filter-info">
|
||||
<p>{{'FLIGHTS-MAP.FILTER_INFO' | translate}}</p>
|
||||
</div>
|
||||
|
||||
<div class="flights-map-filter-content-checkboxes">
|
||||
|
||||
<toggle-switch
|
||||
[disabled]="departure ? false : true"
|
||||
[(ngModel)]="domestic"
|
||||
label="{{ 'FLIGHTS-MAP.DOMESTIC_FLIGHTS' | translate }}">
|
||||
</toggle-switch>
|
||||
|
||||
<toggle-switch
|
||||
[disabled]="departure ? false : true"
|
||||
|
||||
[(ngModel)]="international"
|
||||
label="{{ 'FLIGHTS-MAP.INTERNATIONAL_FLIGHTS' | translate }}">
|
||||
</toggle-switch>
|
||||
|
||||
<toggle-switch
|
||||
[disabled]="departure && arrival ? false : true"
|
||||
[(ngModel)]="connections"
|
||||
label="{{ 'FLIGHTS-MAP.CONNECTING_FLIGHTS' | translate }}">
|
||||
</toggle-switch>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="flighs-map-filter-date">
|
||||
<calendar-input
|
||||
label="SHARED.FLIGHT_DATE"
|
||||
[(ngModel)]="date"
|
||||
|
||||
[minDate]="minDate"
|
||||
[maxDate]="maxDate"
|
||||
[disabledDates]="disabledDates"
|
||||
data-testid="route-calendar-input"
|
||||
>
|
||||
</calendar-input>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</p-accordionTab>
|
||||
</p-accordion>
|
||||
</section>
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
.flights-map-filter-header{
|
||||
padding: 10px 0;
|
||||
}
|
||||
|
||||
.flights-map-filter-info{
|
||||
padding: 10px 0;
|
||||
}
|
||||
|
||||
.mt2{
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
// .svg--change-city {
|
||||
// transform: rotate(180deg) !important;
|
||||
// }
|
||||
|
||||
// .change-container {
|
||||
// justify-content: right !important;
|
||||
// }
|
||||
|
||||
|
||||
|
||||
+154
@@ -0,0 +1,154 @@
|
||||
import { Component, OnInit, Input, OnDestroy, ChangeDetectorRef, Inject } from '@angular/core';
|
||||
import { ScheduleFilterValidationService } from '@app/features/schedule/components/schedule-filter/services/schedule-filter-validation.service';
|
||||
import { UserLocationService } from '@app/shared/services/user-location/user-location.service';
|
||||
import { FlightsMapFiltersStateService, IFlightsMapFilterState } from '@shared/services/filters/flights-map-filters-state.service';
|
||||
import { Subject } from 'rxjs';
|
||||
import { takeUntil } from 'rxjs/operators';
|
||||
import { AppSettings } from '@shared/models-legacy';
|
||||
import { APP_SETTINGS } from '@shared/services';
|
||||
import { OnlineBoardApiService } from "@app/features/online-board/services/api.service";
|
||||
import { FlightsMapApiService } from '../../services/flights-map-api.service';
|
||||
|
||||
@Component({
|
||||
selector: 'flights-map-filter',
|
||||
templateUrl: './flights-map-filter.component.html',
|
||||
styleUrls: ['./flights-map-filter.component.scss'],
|
||||
providers: [ScheduleFilterValidationService],
|
||||
})
|
||||
export class FlightsMapFilterComponent implements OnInit, OnDestroy {
|
||||
|
||||
private currentFilterState: IFlightsMapFilterState;
|
||||
|
||||
withReturn: boolean;
|
||||
|
||||
private destroy$ = new Subject<void>();
|
||||
|
||||
constructor(
|
||||
public validationService: ScheduleFilterValidationService,
|
||||
private apiService: FlightsMapApiService,
|
||||
@Inject(APP_SETTINGS) public settings: AppSettings,
|
||||
private filterStateService: FlightsMapFiltersStateService,
|
||||
private locationService: UserLocationService,
|
||||
private cdr : ChangeDetectorRef
|
||||
) { }
|
||||
|
||||
|
||||
ngOnInit() {
|
||||
|
||||
this.filterStateService.state$
|
||||
.pipe(takeUntil(this.destroy$))
|
||||
.subscribe(state => {
|
||||
this.currentFilterState = state;
|
||||
|
||||
this.cdr.markForCheck();
|
||||
});
|
||||
|
||||
this.locationService.location.subscribe((location) => {
|
||||
// set default state only if user permitted geo position
|
||||
// search and filter isn't filled
|
||||
if (location && !this.departure && !this.arrival) {
|
||||
this.filterStateService.setDeparture(location);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.destroy$.next();
|
||||
this.destroy$.complete();
|
||||
}
|
||||
|
||||
|
||||
get departure() {
|
||||
return this.currentFilterState.departure;
|
||||
}
|
||||
|
||||
set departure(departure: string)
|
||||
{
|
||||
this.filterStateService.setDeparture(departure);
|
||||
}
|
||||
|
||||
get arrival()
|
||||
{
|
||||
return this.currentFilterState.arrival;
|
||||
}
|
||||
|
||||
set arrival(arrival: string){
|
||||
this.filterStateService.setArrival(arrival);
|
||||
}
|
||||
|
||||
get connections()
|
||||
{
|
||||
return this.currentFilterState.connections;
|
||||
}
|
||||
|
||||
set connections(showConenctions: boolean)
|
||||
{
|
||||
this.filterStateService.setConnections(showConenctions);
|
||||
}
|
||||
|
||||
get domestic()
|
||||
{
|
||||
return this.currentFilterState.domestic;
|
||||
}
|
||||
|
||||
set domestic(showDomestic: boolean)
|
||||
{
|
||||
this.filterStateService.setDomestic(showDomestic);
|
||||
}
|
||||
|
||||
get international()
|
||||
{
|
||||
return this.currentFilterState.international;
|
||||
}
|
||||
|
||||
set international(showInternational: boolean)
|
||||
{
|
||||
this.filterStateService.setInternational(showInternational);
|
||||
}
|
||||
|
||||
get date()
|
||||
{
|
||||
return this.currentFilterState.date;
|
||||
}
|
||||
|
||||
set date(date: Date)
|
||||
{
|
||||
this.filterStateService.setDate(date);
|
||||
}
|
||||
|
||||
get departurePlaceholder() {
|
||||
return 'FLIGHTS-MAP.FILTER_DEPARTURE_PLACEHOLDER'
|
||||
}
|
||||
get arrivalPlaceholder() {
|
||||
return 'FLIGHTS-MAP.FILTER_ARRIVAL_PLACEHOLDER'
|
||||
}
|
||||
|
||||
get minDate(){
|
||||
return this.currentFilterState.minDate;
|
||||
}
|
||||
|
||||
get maxDate(){
|
||||
return this.currentFilterState.maxDate;
|
||||
}
|
||||
|
||||
get disabledDates()
|
||||
{
|
||||
return this.currentFilterState.disabledDates;
|
||||
}
|
||||
|
||||
set disabledDates(disabledDates: Date[]){
|
||||
this.filterStateService.setDisabledDates(disabledDates);
|
||||
}
|
||||
|
||||
exchange() {
|
||||
[
|
||||
this.validationService.departureError,
|
||||
this.validationService.arrivalError,
|
||||
] = [null, null];
|
||||
[this.departure, this.arrival] = [this.arrival, this.departure];
|
||||
}
|
||||
|
||||
resetReturnDateRange() {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
}
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
<meta-tags
|
||||
[title]="'SEO.FLIGHTS-MAP.MAIN.TITLE' | translate"
|
||||
[description]="'SEO.FLIGHTS-MAP.MAIN.DESCRIPTION' | translate"
|
||||
[noRobots]="false"
|
||||
></meta-tags>
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'flights-map-meta-tags',
|
||||
templateUrl: './flights-map-meta-tags.component.html',
|
||||
styleUrls: ['./flights-map-meta-tags.component.scss']
|
||||
})
|
||||
export class FlightsMapMetaTagsComponent{}
|
||||
+1
@@ -0,0 +1 @@
|
||||
<aero-title [title]="title | translate"></aero-title>
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'flights-map-start-page-title',
|
||||
templateUrl: './flights-map-start-page-title.component.html',
|
||||
styleUrls: ['./flights-map-start-page-title.component.scss']
|
||||
})
|
||||
export class FlightsMapStartPageTitleComponent {
|
||||
|
||||
title = 'FLIGHTS-MAP.TITLE';
|
||||
|
||||
}
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
<flights-map-meta-tags></flights-map-meta-tags>
|
||||
<div>
|
||||
<page-layout [withScrollOverlay]="false" [withScrollUp]="true">
|
||||
<flights-map-start-page-title title></flights-map-start-page-title>
|
||||
|
||||
<flights-page-tabs
|
||||
[viewType]="ViewType.FlightsMap"
|
||||
header-left
|
||||
></flights-page-tabs>
|
||||
|
||||
<ng-container content-left>
|
||||
<!-- <online-board-filter></online-board-filter>
|
||||
|
||||
<search-history></search-history> -->
|
||||
|
||||
<flights-map-filter></flights-map-filter>
|
||||
</ng-container>
|
||||
|
||||
<ng-container >
|
||||
<section class="frame">
|
||||
<!-- <h2>{{ 'BOARD.BOARD-START' | translate }}</h2> -->
|
||||
<flights-map-body></flights-map-body>
|
||||
</section>
|
||||
</ng-container>
|
||||
|
||||
</page-layout>
|
||||
</div>
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { ViewType } from '@shared/enumerators/flight-request-type.enum';
|
||||
|
||||
@Component({
|
||||
selector: 'flights-map-start-page',
|
||||
templateUrl: './flights-map-start-page.component.html',
|
||||
styleUrls: ['./flights-map-start-page.component.scss']
|
||||
})
|
||||
export class FlightsMapStartPageComponent implements OnInit {
|
||||
ViewType = ViewType;
|
||||
|
||||
constructor() {
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
}
|
||||
}
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
<!-- Простынка с лоадером -->
|
||||
<div class="loading-sheet">
|
||||
<div class="page-loader__loader">
|
||||
<div class="loader-circle"></div>
|
||||
<div class="loader-line-mask">
|
||||
<div class="loader-line"></div>
|
||||
</div>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 33.795 32.307">
|
||||
<path
|
||||
id="plane_afl_small"
|
||||
d="M20.824,12.956c0,.388,0,.823,0,1.29L35.729,26.527l.007,1.633-15.1-7.255a70.654,70.654,0,0,1-.947,9.23l6.09,3.575.011.462-6.457-2.2c-.139.5-.276.788-.426.785a.561.561,0,0,1-.437-.774,35.562,35.562,0,0,0-6.415,2.288l-.006-.469,6.037-3.658a71.009,71.009,0,0,1-1.149-9.227L1.96,28.408l-.02-1.627,14.708-12.5c-.011-.469-.015-.9-.019-1.291-.048-6.082.858-11.015,2.014-11.021s2.138,4.91,2.18,10.99"
|
||||
transform="translate(36,-2) rotate(90)"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
+67
@@ -0,0 +1,67 @@
|
||||
:host {
|
||||
--loader-color: #2b62ab;
|
||||
}
|
||||
|
||||
.loading-sheet {
|
||||
position: absolute;
|
||||
inset: 0; // top:0; right:0; bottom:0; left:0;
|
||||
background: rgba(180, 180, 180, 0.302); // затемнение карты
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1000; // поверх карты
|
||||
pointer-events: all; // блокирует клики по карте
|
||||
}
|
||||
|
||||
.page-loader__loader {
|
||||
position: relative;
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
color: var(--loader-color); // для svg
|
||||
|
||||
.loader-circle {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
margin: 0;
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
border-radius: 50%;
|
||||
box-shadow: inset 0 0 0 2px var(--loader-color);
|
||||
transition-duration: 0.3s;
|
||||
}
|
||||
|
||||
.loader-line-mask {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 30px;
|
||||
height: 60px;
|
||||
margin: 0;
|
||||
overflow: hidden;
|
||||
transform-origin: 30px 30px;
|
||||
animation: rotate 1.2s infinite linear;
|
||||
|
||||
.loader-line {
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
border-radius: 50%;
|
||||
box-shadow: inset 0 0 0 2px rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
}
|
||||
|
||||
svg {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
|
||||
@keyframes rotate {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
}
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'loader-sheet',
|
||||
templateUrl: './loader-sheet.component.html',
|
||||
styleUrls: ['./loader-sheet.component.scss']
|
||||
})
|
||||
export class LoaderSheetComponent implements OnInit {
|
||||
|
||||
constructor() { }
|
||||
|
||||
ngOnInit(): void {
|
||||
}
|
||||
|
||||
}
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
<div class="no-directions-sheet" (click)="dismiss.emit()">
|
||||
<div class="no-directions-card" (click)="$event.stopPropagation()">
|
||||
<p> {{'FLIGHTS-MAP.NO_DIRECTIONS_INFO' | translate}}</p>
|
||||
</div>
|
||||
</div>
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
.no-directions-sheet {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: rgba(180, 180, 180, 0.302);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1000;
|
||||
pointer-events: all;
|
||||
padding: 16px; // чтобы не прилипало к краям на мобилках
|
||||
}
|
||||
|
||||
.no-directions-card {
|
||||
background: #fff;
|
||||
padding: 16px 18px;
|
||||
border-radius: 5px;
|
||||
max-width: 520px;
|
||||
width: min(520px, 100%);
|
||||
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.18);
|
||||
border: 1px solid rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
.no-directions-card p {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
line-height: 1.4;
|
||||
color: rgba(0, 0, 0, 0.78);
|
||||
text-align: center;
|
||||
}
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
import { Component, EventEmitter, Output } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'no-directions-sheet',
|
||||
templateUrl: './no-directions-sheet.component.html',
|
||||
styleUrls: ['./no-directions-sheet.component.scss']
|
||||
})
|
||||
export class NoDirectionsSheetComponent {
|
||||
|
||||
@Output() dismiss = new EventEmitter<void>();
|
||||
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { RouterModule, Routes } from '@angular/router';
|
||||
import { FlightsMapStartPageComponent } from './components/flights-map-start-page/flights-map-start-page.component';
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
path: '',
|
||||
component: FlightsMapStartPageComponent
|
||||
}
|
||||
]
|
||||
|
||||
@NgModule({
|
||||
imports: [RouterModule.forChild(routes)],
|
||||
exports: [RouterModule],
|
||||
})
|
||||
export class FlightsMapRoutingModule { }
|
||||
@@ -0,0 +1,47 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { ComponentsModule } from '@components/components.module';
|
||||
import { ToolkitModule } from '@toolkit/toolkit.module';
|
||||
import { FlightsMapRoutingModule } from './flights-map-routing.module'
|
||||
import { FlightsMapStartPageComponent } from './components/flights-map-start-page/flights-map-start-page.component';
|
||||
import { FlightsMapStartPageTitleComponent } from './components/flights-map-start-page-title/flights-map-start-page-title.component';
|
||||
import { FlightsModule } from '@modules/flights.module';
|
||||
import { FlightsMapFilterComponent } from './components/flights-map-filter/flights-map-filter.component';
|
||||
import { AccordionModule } from 'primeng/accordion';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { CheckboxModule } from 'primeng/checkbox';
|
||||
import { FlightsMapBodyComponent } from './components/flights-map-body/flights-map-body.component';
|
||||
import { FlightsMapApiService } from './services/flights-map-api.service';
|
||||
import { FlightsMapMetaTagsComponent } from './components/flights-map-meta-tags/flights-map-meta-tags.component';
|
||||
import { SharedModule } from "../../shared/shared.module";
|
||||
import { OnlineBoardApiService } from '../online-board/services/api.service';
|
||||
import { LoaderSheetComponent } from './components/loader-sheet/loader-sheet.component';
|
||||
import { NoDirectionsSheetComponent } from './components/no-directions-sheet/no-directions-sheet.component';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
FlightsMapStartPageComponent,
|
||||
FlightsMapStartPageTitleComponent,
|
||||
FlightsMapFilterComponent,
|
||||
FlightsMapBodyComponent,
|
||||
FlightsMapMetaTagsComponent,
|
||||
LoaderSheetComponent,
|
||||
NoDirectionsSheetComponent
|
||||
],
|
||||
imports: [
|
||||
CommonModule,
|
||||
FormsModule,
|
||||
FlightsMapRoutingModule,
|
||||
ComponentsModule,
|
||||
ToolkitModule,
|
||||
FlightsModule,
|
||||
AccordionModule,
|
||||
CheckboxModule,
|
||||
SharedModule
|
||||
],
|
||||
providers: [
|
||||
FlightsMapApiService,
|
||||
OnlineBoardApiService,
|
||||
]
|
||||
})
|
||||
export class FlightsMapModule { }
|
||||
@@ -0,0 +1,58 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class CityCategoryService {
|
||||
|
||||
private _population1kk = new Set<string>([
|
||||
"QIN","THR","FRU","KZN","LED","IST","HKG","PEE","UFA",
|
||||
"OVB","CEK","CAI","SVX","EVN","DEL","MSQ",
|
||||
"KJA","KUF","NQZ","OMS","ALA","SHA","VOG",
|
||||
"BAK","BKK","BJS","MOW","GOJ","VVO","KHV"
|
||||
]);
|
||||
|
||||
private _population500k = new Set<string>([
|
||||
"TJM","TOF","SKD","ULY","PEZ","MCX","KEJ",
|
||||
"IJK","REN","ASF","NOZ","BAX","RTW","DOH",
|
||||
"AUH","SSH","TAS","CIT","SGN","CAN","HRB",
|
||||
"VRA","KGF",
|
||||
]);
|
||||
|
||||
private _population100k = new Set<string>([
|
||||
"MMK","GDX","SGC","DYR","ESL","OSS","KVD","KVK","GUW","SCW",
|
||||
"SCO","NJC","NBC","UGC","UUD","YKS","MJZ","SKX","STW","KSN",
|
||||
"KGD","HMA","HTA","RGK","GRV","ABA","PKC","NAL","MQF","MRV",
|
||||
"OSW","ARH","UUS","NUX","BHK","PYJ","CSY","BQS","DLM"
|
||||
]);
|
||||
|
||||
private _popularResorts = new Set<string>([
|
||||
"AER","AYT","BJV","CMB","DPS","DXB","GOI",
|
||||
"HAV","HKT","HRG","MLE","NHA","SEZ","SYX","IKT"
|
||||
])
|
||||
|
||||
get population1kk(): Set<string> { return this._population1kk; }
|
||||
get population500k(): Set<string> { return this._population500k; }
|
||||
get population100k(): Set<string> { return this._population100k; }
|
||||
get popularResorts(): Set<string> { return this._popularResorts; }
|
||||
|
||||
zoomLevel(code: string): number
|
||||
{
|
||||
if (this._population1kk.has(code) || this._popularResorts.has(code))
|
||||
{
|
||||
return 2;
|
||||
}
|
||||
|
||||
if (this._population500k.has(code))
|
||||
{
|
||||
return 5;
|
||||
}
|
||||
|
||||
if (this._population100k.has(code))
|
||||
{
|
||||
return 6;
|
||||
}
|
||||
|
||||
return 6;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
import { HttpParams } from '@angular/common/http';
|
||||
import { Injectable } from '@angular/core';
|
||||
import { EndpointService } from '@app/shared/services/api/endpoint.service';
|
||||
import { ApiFormatterService } from '@app/shared/services/api/formatter.service';
|
||||
import { ApiService } from '@shared/services/api/api.service';
|
||||
import { IStringValues } from '@typings/common/util';
|
||||
import { IDestinationsResponse, IDaysResponse } from '@typings/responses';
|
||||
import { Observable, of } from 'rxjs';
|
||||
import { delay } from 'rxjs/operators';
|
||||
|
||||
export type IDestinationsRequestParams = {
|
||||
departure: string;
|
||||
arrival?: string;
|
||||
dateFrom: Date;
|
||||
dateTo?: Date;
|
||||
timeFrom?: string;
|
||||
timeTo?: string;
|
||||
connections?: number;
|
||||
};
|
||||
|
||||
type IDestinationsHttpParams = IStringValues<IDestinationsRequestParams>;
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class FlightsMapApiService {
|
||||
|
||||
constructor(private apiService: ApiService,
|
||||
private apiFormatter: ApiFormatterService,
|
||||
private endpointService: EndpointService) { }
|
||||
|
||||
|
||||
getDestinations(requestParams: IDestinationsRequestParams): Observable<IDestinationsResponse>{
|
||||
const url = this.endpointService.buildLocalizedURL('destinations', '1');
|
||||
|
||||
const httpParams: IDestinationsHttpParams = {
|
||||
...requestParams,
|
||||
dateFrom: this.apiFormatter.formatDateOnly(requestParams.dateFrom),
|
||||
dateTo: this.apiFormatter.formatDateOnly(requestParams.dateTo ?? this.addDays(requestParams.dateFrom, 1)),
|
||||
connections: String(requestParams.connections ?? 0),
|
||||
};
|
||||
|
||||
let params = new HttpParams({
|
||||
fromObject: httpParams,
|
||||
});
|
||||
|
||||
return this.apiService.cacheFirst$<IDestinationsResponse>(url, params);
|
||||
}
|
||||
|
||||
public getFlightDaysByRoute(date: Date, src: string, dest: string, connections: boolean): Observable<IDaysResponse> {
|
||||
const formatDate = this.apiFormatter.formatDateOnly(date);
|
||||
|
||||
let param = '';
|
||||
|
||||
if(src && !dest)
|
||||
{
|
||||
param = `departure/${src}`
|
||||
}
|
||||
else if(src && dest)
|
||||
{
|
||||
param = connections ? `connections/${src}-${dest}-1` : `route/${src}-${dest}`;
|
||||
}
|
||||
else
|
||||
{
|
||||
return of<null>();
|
||||
}
|
||||
|
||||
const url = this.endpointService.buildLocalizedURL(
|
||||
`days/${formatDate}/200/${param}/flights-map/`, //`days/${formatDate}/185/${param}/flights-map/` расширено до 200, чтобы при нахождении на сайте нескольких дней актуальность календаря
|
||||
'v1', // сохранялась по заранее запрошенным данным (не делать автообновление, возможно на будущую доработку по автообновлению)
|
||||
);
|
||||
console.log(url);
|
||||
const httpParams = new HttpParams({
|
||||
});
|
||||
|
||||
return this.apiService.cacheFirst$<IDaysResponse>(url, httpParams);
|
||||
}
|
||||
|
||||
private addDays(date: Date, days: number): Date {
|
||||
const newDate = new Date(date.getTime() + (1000 * 60 * 60 * 24 * days));
|
||||
|
||||
return newDate;
|
||||
}
|
||||
}
|
||||
+62
@@ -0,0 +1,62 @@
|
||||
<div class="filter-content">
|
||||
<div class="p-field">
|
||||
<label class="label--filter">{{
|
||||
'SHARED.FLIGHT_NUMBER' | translate
|
||||
}}</label>
|
||||
<tooltip *ngIf="validationService.flightNumberError">{{
|
||||
validationService.flightNumberError | translate
|
||||
}}</tooltip>
|
||||
|
||||
<div
|
||||
class="number-input-composite"
|
||||
[ngClass]="{
|
||||
'has-value': flightNumber,
|
||||
'has-error': validationService.flightNumberError
|
||||
}"
|
||||
>
|
||||
<div class="prefix">SU</div>
|
||||
<input
|
||||
pInputText
|
||||
type="text"
|
||||
class="input--filter input--flight-number ui-inputtext"
|
||||
[(ngModel)]="flightNumber"
|
||||
(change)="addZeros()"
|
||||
autocomplete="off"
|
||||
maxlength="5"
|
||||
placeholder="{{
|
||||
'SHARED.FLIGHT_NUMBER_PLACEHOLDER' | translate
|
||||
}}"
|
||||
data-testid="flight-number-input"
|
||||
/>
|
||||
<button
|
||||
pButton
|
||||
label=" "
|
||||
class="button-clear"
|
||||
(click)="clearInput()"
|
||||
data-testid="flight-number-clear-button"
|
||||
></button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<calendar-input
|
||||
label="SHARED.FLIGHT_DATE"
|
||||
[(ngModel)]="date"
|
||||
[error]="validationService.dateError"
|
||||
[minDate]="minDate"
|
||||
[maxDate]="maxDate"
|
||||
[disabledDates]="disabledDates"
|
||||
data-testid="flight-number-calendar"
|
||||
>
|
||||
</calendar-input>
|
||||
</div>
|
||||
|
||||
<div class="filter-button">
|
||||
<button
|
||||
class="search-button color blue-light"
|
||||
pButton
|
||||
type="button"
|
||||
label="{{ 'SHARED.SEARCH' | translate }}"
|
||||
(click)="search()"
|
||||
data-testid="flight-number-search-button"
|
||||
></button>
|
||||
</div>
|
||||
+157
@@ -0,0 +1,157 @@
|
||||
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { OnlineBoardFlightNumberFilterValidationService } from '@online-board/components/filter/components/flight-number-filter/services/online-board-flight-number-filter-validation.service';
|
||||
import { IOnlineBoardRouteData } from '@online-board/types';
|
||||
import { getMockPipe } from '@shared/pipes/pipe.mock';
|
||||
import { OnlineBoardFiltersStateService } from '@shared/services/filters/online-board-filters-state.service';
|
||||
import { UserLocationService } from '@shared/services/user-location/user-location.service';
|
||||
import { getUserLocationServiceMock } from '@shared/services/user-location/user-location.service.mock';
|
||||
import { BehaviorSubject } from 'rxjs';
|
||||
|
||||
import { OnlineBoardFlightNumberFilterComponent } from './flight-number-filter.component';
|
||||
|
||||
describe('FlightNumberFilterComponent', () => {
|
||||
let component: OnlineBoardFlightNumberFilterComponent;
|
||||
let fixture: ComponentFixture<OnlineBoardFlightNumberFilterComponent>;
|
||||
let locationService;
|
||||
const data: IOnlineBoardRouteData = {
|
||||
urlParams: {},
|
||||
};
|
||||
|
||||
beforeEach(async () => {
|
||||
locationService = getUserLocationServiceMock('MOW');
|
||||
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [
|
||||
OnlineBoardFlightNumberFilterComponent,
|
||||
getMockPipe('translate'),
|
||||
],
|
||||
providers: [
|
||||
OnlineBoardFlightNumberFilterValidationService,
|
||||
OnlineBoardFiltersStateService,
|
||||
{
|
||||
provide: UserLocationService,
|
||||
useValue: locationService,
|
||||
},
|
||||
{
|
||||
provide: ActivatedRoute,
|
||||
useValue: {
|
||||
data: new BehaviorSubject(data),
|
||||
},
|
||||
},
|
||||
],
|
||||
schemas: [NO_ERRORS_SCHEMA],
|
||||
}).compileComponents();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(
|
||||
OnlineBoardFlightNumberFilterComponent,
|
||||
);
|
||||
component = fixture.componentInstance;
|
||||
data.urlParams = undefined;
|
||||
|
||||
jasmine.clock().install();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jasmine.clock().uninstall();
|
||||
});
|
||||
|
||||
it('should add zeros to flight number', () => {
|
||||
component.flightNumber = '1';
|
||||
component.addZeros();
|
||||
expect(component.flightNumber).toBe('0001');
|
||||
});
|
||||
|
||||
it('should clear flight number', () => {
|
||||
component.flightNumber = '0001';
|
||||
component.clearInput();
|
||||
expect(component.flightNumber).toBe(undefined);
|
||||
});
|
||||
|
||||
it('should emit valid params', () => {
|
||||
const flightNumber = '0001';
|
||||
const date = new Date(2022, 5, 6);
|
||||
|
||||
component.onSearch.emit = jasmine.createSpy('onSearch.emit');
|
||||
component.flightNumber = flightNumber;
|
||||
component.date = date;
|
||||
component.search();
|
||||
|
||||
expect(component.onSearch.emit).toHaveBeenCalledWith({
|
||||
flightNumber,
|
||||
carrier: 'SU',
|
||||
date,
|
||||
});
|
||||
});
|
||||
|
||||
it('should not emit invalid params', () => {
|
||||
const flightNumber = '0001';
|
||||
const date = new Date('invalid');
|
||||
|
||||
component.onSearch.emit = jasmine.createSpy('onSearch.emit');
|
||||
component.flightNumber = flightNumber;
|
||||
component.date = date;
|
||||
component.search();
|
||||
|
||||
expect(component.onSearch.emit).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should set state on init', () => {
|
||||
data.urlParams = {
|
||||
flightNumber: {
|
||||
flightNumber: '0001',
|
||||
carrier: 'SU',
|
||||
date: new Date(2022, 5, 6),
|
||||
},
|
||||
};
|
||||
|
||||
expect(component.flightNumber).toBe(undefined);
|
||||
expect(component.date).toBe(undefined);
|
||||
|
||||
component.ngOnInit();
|
||||
|
||||
expect(component.flightNumber).toBe('0001');
|
||||
expect(component.date).toEqual(new Date(2022, 5, 6));
|
||||
});
|
||||
|
||||
it('should not set state on init if urlParams were not provided', () => {
|
||||
data.urlParams = undefined;
|
||||
const setStateMock = jasmine.createSpy('setState');
|
||||
(component as any).setState = setStateMock;
|
||||
component.ngOnInit();
|
||||
|
||||
expect(setStateMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should not set state on init if there was no flightNumber field in urlParams', () => {
|
||||
data.urlParams = {};
|
||||
const setStateMock = jasmine.createSpy('setState');
|
||||
(component as any).setState = setStateMock;
|
||||
component.ngOnInit();
|
||||
|
||||
expect(setStateMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should set default state on init if geolocation is permitted', () => {
|
||||
const mockedToday = new Date(2022, 4, 28);
|
||||
expect(component.flightNumber).toBe(undefined);
|
||||
expect(component.date).toBe(undefined);
|
||||
|
||||
jasmine.clock().mockDate(mockedToday);
|
||||
locationService.locate();
|
||||
component.ngOnInit();
|
||||
|
||||
expect(component.flightNumber).toBe(undefined);
|
||||
expect(component.date.toDateString()).toBe(mockedToday.toDateString());
|
||||
});
|
||||
|
||||
it('should not set default state on init if geolocation is not permitted', () => {
|
||||
component.ngOnInit();
|
||||
|
||||
expect(component.flightNumber).toBe(undefined);
|
||||
expect(component.date).toBe(undefined);
|
||||
});
|
||||
});
|
||||
+145
@@ -0,0 +1,145 @@
|
||||
import { Component, EventEmitter, Input, Output } from '@angular/core';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { Subscription } from 'rxjs/Subscription';
|
||||
import { OnlineBoardFlightNumberFilterValidationService } from '@online-board/components/filter/components/flight-number-filter/services/online-board-flight-number-filter-validation.service';
|
||||
import { IOnlineBoardRouteData } from '@online-board/types';
|
||||
import { OnlineBoardFiltersStateService } from '@shared/services/filters/online-board-filters-state.service';
|
||||
import { UserLocationService } from '@shared/services/user-location/user-location.service';
|
||||
import { IParsedFlightId } from '@typings/flight/flight-id';
|
||||
import { OnlineBoardApiService } from '@online-board/services/api.service';
|
||||
import { StateService } from '@shared/services/state.service';
|
||||
|
||||
@Component({
|
||||
selector: 'online-board-flight-number-filter',
|
||||
templateUrl: './flight-number-filter.component.html',
|
||||
providers: [OnlineBoardFlightNumberFilterValidationService],
|
||||
})
|
||||
export class OnlineBoardFlightNumberFilterComponent {
|
||||
@Input() minDate: Date;
|
||||
@Input() maxDate: Date;
|
||||
disabledDates: Date[];
|
||||
|
||||
@Output() onSearch = new EventEmitter<IParsedFlightId>();
|
||||
|
||||
constructor(
|
||||
public validationService: OnlineBoardFlightNumberFilterValidationService,
|
||||
private apiService: OnlineBoardApiService,
|
||||
private route: ActivatedRoute,
|
||||
private locationService: UserLocationService,
|
||||
private state: OnlineBoardFiltersStateService,
|
||||
private stateService: StateService,
|
||||
) {
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.route.data.subscribe((data: IOnlineBoardRouteData) => {
|
||||
// There is no urlParams.flightNumber field
|
||||
// in data object if flight number search page
|
||||
// isn't opened
|
||||
if (!data.urlParams?.flightNumber) {
|
||||
return;
|
||||
}
|
||||
this.setState(data.urlParams.flightNumber);
|
||||
|
||||
});
|
||||
this.locationService.location.subscribe((location) => {
|
||||
// set default state only if user permitted geo position
|
||||
// search and filter isn't filled
|
||||
if (location && !this.flightNumber) {
|
||||
this.setDefaultState();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
}
|
||||
|
||||
get flightNumber() {
|
||||
return this.state.flightNumber;
|
||||
}
|
||||
|
||||
set flightNumber(flightNumber: string) {
|
||||
this.state.flightNumber = flightNumber;
|
||||
if (flightNumber.length >= 4 && /^\d\d\d\d/.test(flightNumber)) {
|
||||
var date = new Date();
|
||||
date.setUTCHours(0,0,0,0);
|
||||
date.setDate(date.getDate() - 1);
|
||||
this.apiService
|
||||
.getFlightDaysByNumber(date, flightNumber)
|
||||
.then((res) => {
|
||||
this.disabledDates = new Array();
|
||||
for(var i=0;i<res.days.length;i++) {
|
||||
if (res.days[i] == '0') {
|
||||
this.disabledDates.push(new Date(date));
|
||||
}
|
||||
date.setDate(date.getDate() + 1);
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
});
|
||||
} else {
|
||||
this.disabledDates = new Array();
|
||||
}
|
||||
}
|
||||
|
||||
get suffix() {
|
||||
return this.state.suffix;
|
||||
}
|
||||
|
||||
set suffix(suffix: string) {
|
||||
this.state.suffix = suffix;
|
||||
}
|
||||
|
||||
get date() {
|
||||
return this.state.flightNumberDate;
|
||||
}
|
||||
|
||||
set date(date: Date) {
|
||||
this.state.flightNumberDate = date;
|
||||
}
|
||||
|
||||
addZeros() {
|
||||
this.flightNumber = this.flightNumber.padStart(4, '0').toUpperCase();
|
||||
if (this.flightNumber.slice(-1)[0] > '9') {
|
||||
this.flightNumber = this.flightNumber.padStart(5, '0');
|
||||
}
|
||||
}
|
||||
|
||||
clearInput() {
|
||||
this.flightNumber = undefined;
|
||||
}
|
||||
|
||||
async search() {
|
||||
const params = this.getSearchParams();
|
||||
const areParamsValid = this.validationService.validate(params);
|
||||
if (!areParamsValid) {
|
||||
return;
|
||||
}
|
||||
this.stateService.set("boardnumber", params);
|
||||
|
||||
this.onSearch.emit(params);
|
||||
}
|
||||
|
||||
private setState(params: IParsedFlightId) {
|
||||
// uncomment in the following line if want to use 123D instead of 0123D
|
||||
this.flightNumber = (params.flightNumber+params.suffix).slice(/*+params.flightNumber > 999 ?*/ -5 /*: -4*/);
|
||||
this.date = params.date;
|
||||
this.suffix = params.suffix;
|
||||
}
|
||||
|
||||
private setDefaultState() {
|
||||
this.date = new Date();
|
||||
}
|
||||
|
||||
private getSearchParams(): IParsedFlightId {
|
||||
const suffix = this.flightNumber.length > 0 && this.flightNumber.slice(-1)[0] > '9' ? this.flightNumber.slice(-1) : "";
|
||||
const flightNumber = (suffix.length > 0) ? this.flightNumber.slice(0,-1) : this.flightNumber;
|
||||
|
||||
return {
|
||||
flightNumber: flightNumber,
|
||||
date: this.date,
|
||||
carrier: 'SU',
|
||||
suffix: suffix,
|
||||
};
|
||||
}
|
||||
}
|
||||
+99
@@ -0,0 +1,99 @@
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
import { OnlineBoardFlightNumberFilterValidationService } from '@online-board/components/filter/components/flight-number-filter/services/online-board-flight-number-filter-validation.service';
|
||||
import { IParsedFlightId } from '@typings/flight/flight-id';
|
||||
|
||||
describe('OnlineBoardFlightNumberFilterValidationService', () => {
|
||||
let service: OnlineBoardFlightNumberFilterValidationService;
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
providers: [OnlineBoardFlightNumberFilterValidationService],
|
||||
});
|
||||
|
||||
service = TestBed.inject(
|
||||
OnlineBoardFlightNumberFilterValidationService,
|
||||
);
|
||||
});
|
||||
|
||||
it('should return false and set flightNumberError because flight number is not provided', () => {
|
||||
const params: Partial<IParsedFlightId> = {};
|
||||
|
||||
expect(service.validate(params)).toBe(false);
|
||||
expect(service.flightNumberError).toBe(
|
||||
'BOARD.FLIGHT_NUMBER-ERROR-EMPTY',
|
||||
);
|
||||
expect(service.dateError).toBe(null);
|
||||
});
|
||||
|
||||
it('should return false because flight number does not meet pattern', () => {
|
||||
const params: Partial<IParsedFlightId> = {
|
||||
flightNumber: 'invalid',
|
||||
};
|
||||
|
||||
expect(service.validate(params)).toBe(false);
|
||||
expect(service.flightNumberError).toBe(
|
||||
'BOARD.FLIGHT_NUMBER-ERROR-ONLY-NUMBER',
|
||||
);
|
||||
expect(service.dateError).toBe(null);
|
||||
});
|
||||
|
||||
it('should return false because flight number`s length is more than 4', () => {
|
||||
const params: Partial<IParsedFlightId> = {
|
||||
flightNumber: '11111',
|
||||
};
|
||||
|
||||
expect(service.validate(params)).toBe(false);
|
||||
expect(service.flightNumberError).toBe(
|
||||
'BOARD.FLIGHT_NUMBER-ERROR-ONLY-NUMBER',
|
||||
);
|
||||
expect(service.dateError).toBe(null);
|
||||
});
|
||||
|
||||
it('should return false because date is not provided', () => {
|
||||
const params: Partial<IParsedFlightId> = {
|
||||
flightNumber: '0001',
|
||||
};
|
||||
|
||||
expect(service.validate(params)).toBe(false);
|
||||
expect(service.flightNumberError).toBe(null);
|
||||
expect(service.dateError).toBe('SHARED.DATE_FORMAT-WRONG');
|
||||
});
|
||||
|
||||
it('should return false because date is invalid', () => {
|
||||
const params: Partial<IParsedFlightId> = {
|
||||
flightNumber: '0001',
|
||||
date: new Date('invalid'),
|
||||
};
|
||||
|
||||
expect(service.validate(params)).toBe(false);
|
||||
expect(service.flightNumberError).toBe(null);
|
||||
expect(service.dateError).toBe('SHARED.DATE_FORMAT-WRONG');
|
||||
});
|
||||
|
||||
it('should return true because all params are valid', () => {
|
||||
const params: Partial<IParsedFlightId> = {
|
||||
flightNumber: '0001',
|
||||
date: new Date(2022, 5, 6),
|
||||
};
|
||||
|
||||
expect(service.validate(params)).toBe(true);
|
||||
expect(service.flightNumberError).toBe(null);
|
||||
expect(service.dateError).toBe(null);
|
||||
});
|
||||
|
||||
it('should clear errors before validation', () => {
|
||||
const params: Partial<IParsedFlightId> = {
|
||||
flightNumber: '0001',
|
||||
date: new Date('invalid'),
|
||||
};
|
||||
|
||||
expect(service.validate(params)).toBe(false);
|
||||
expect(service.flightNumberError).toBe(null);
|
||||
expect(service.dateError).toBe('SHARED.DATE_FORMAT-WRONG');
|
||||
|
||||
params.date = new Date(2022, 5, 6);
|
||||
expect(service.validate(params)).toBe(true);
|
||||
expect(service.flightNumberError).toBe(null);
|
||||
expect(service.dateError).toBe(null);
|
||||
});
|
||||
});
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { IParsedFlightId } from '@typings/flight/flight-id';
|
||||
import * as moment from 'moment';
|
||||
|
||||
@Injectable()
|
||||
export class OnlineBoardFlightNumberFilterValidationService {
|
||||
flightNumberError: string | null = null;
|
||||
dateError: string | null = null;
|
||||
|
||||
validate(params: Partial<IParsedFlightId>) {
|
||||
this.dateError = this.flightNumberError = null;
|
||||
|
||||
if (!params.flightNumber) {
|
||||
this.flightNumberError = 'BOARD.FLIGHT_NUMBER-ERROR-EMPTY';
|
||||
return false;
|
||||
}
|
||||
|
||||
const reg = new RegExp('^\\d\\d\\d\\d?[A-Za-z]?$');
|
||||
if (!reg.test(params.flightNumber) || params.flightNumber.length > 5) {
|
||||
this.flightNumberError = 'BOARD.FLIGHT_NUMBER-ERROR-ONLY-NUMBER';
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!params.date || !moment(params.date).isValid()) {
|
||||
this.dateError = 'SHARED.DATE_FORMAT-WRONG';
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
+58
@@ -0,0 +1,58 @@
|
||||
<div class="filter-content">
|
||||
<city-autocomplete
|
||||
label="SHARED.DEPARTURE_CITY"
|
||||
[(ngModel)]="departure"
|
||||
[(error)]="validationService.departureError"
|
||||
[placeholder]="departurePlaceholder"
|
||||
data-testid="route-departure-city-input"
|
||||
></city-autocomplete>
|
||||
|
||||
<div class="change-container">
|
||||
<button
|
||||
class="button-change"
|
||||
pButton
|
||||
type="button"
|
||||
(click)="exchange()"
|
||||
>
|
||||
<svg class="svg--change-city">
|
||||
<use xlink:href="/assets/img/sprite.svg#changeCity" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<city-autocomplete
|
||||
label="SHARED.ARRIVAL_CITY"
|
||||
[(ngModel)]="arrival"
|
||||
[(error)]="validationService.arrivalError"
|
||||
[placeholder]="arrivalPlaceholder"
|
||||
data-testid="route-arrival-city-input"
|
||||
></city-autocomplete>
|
||||
|
||||
<calendar-input
|
||||
label="SHARED.FLIGHT_DATE"
|
||||
[(ngModel)]="date"
|
||||
[error]="validationService.dateError"
|
||||
[minDate]="minDate"
|
||||
[maxDate]="maxDate"
|
||||
[disabledDates]="disabledDates"
|
||||
data-testid="route-calendar-input"
|
||||
>
|
||||
</calendar-input>
|
||||
</div>
|
||||
|
||||
<time-selector
|
||||
[fullView]="false"
|
||||
[(ngModel)]="timeRange"
|
||||
label="{{ 'SHARED.FLIGHT_TIME' | translate }}"
|
||||
>
|
||||
</time-selector>
|
||||
|
||||
<div class="filter-button">
|
||||
<button
|
||||
class="search-button color blue-light"
|
||||
pButton
|
||||
type="button"
|
||||
label="{{ 'SHARED.SEARCH' | translate }}"
|
||||
(click)="search()"
|
||||
data-testid="route-search-button"
|
||||
></button>
|
||||
</div>
|
||||
+224
@@ -0,0 +1,224 @@
|
||||
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { DictionariesService } from '@modules/components/page-filters/services/dictionaries-service';
|
||||
import { getDictionariesServiceMock } from '@modules/components/page-filters/services/dictionaries-service.mock';
|
||||
import { OnlineBoardRouteFilterValidationService } from '@online-board/components/filter/components/route-filter/services/online-board-route-filter-validation.service';
|
||||
import { IOnlineBoardRouteData } from '@online-board/types';
|
||||
import { getMockPipe } from '@shared/pipes/pipe.mock';
|
||||
import { OnlineBoardFiltersStateService } from '@shared/services/filters/online-board-filters-state.service';
|
||||
import { UserLocationService } from '@shared/services/user-location/user-location.service';
|
||||
import { getUserLocationServiceMock } from '@shared/services/user-location/user-location.service.mock';
|
||||
import { StationCodeValidationService } from '@shared/services/validators/station-code.service';
|
||||
import { areSame } from '@utils/date';
|
||||
import MobileUtils from '@utils/mobile';
|
||||
import { BehaviorSubject } from 'rxjs';
|
||||
|
||||
import { OnlineBoardRouteFilterComponent } from './online-board-route-filter.component';
|
||||
|
||||
describe('OnlineBoardRouteFilterComponent', () => {
|
||||
let component: OnlineBoardRouteFilterComponent;
|
||||
let fixture: ComponentFixture<OnlineBoardRouteFilterComponent>;
|
||||
let locationService: UserLocationService;
|
||||
const data: IOnlineBoardRouteData = {
|
||||
urlParams: {},
|
||||
};
|
||||
let isMobileMock;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [
|
||||
OnlineBoardRouteFilterComponent,
|
||||
getMockPipe('translate'),
|
||||
],
|
||||
providers: [
|
||||
OnlineBoardRouteFilterValidationService,
|
||||
StationCodeValidationService,
|
||||
OnlineBoardFiltersStateService,
|
||||
{
|
||||
provide: DictionariesService,
|
||||
useValue: getDictionariesServiceMock(),
|
||||
},
|
||||
{
|
||||
provide: UserLocationService,
|
||||
useValue: getUserLocationServiceMock('MOW'),
|
||||
},
|
||||
{
|
||||
provide: ActivatedRoute,
|
||||
useValue: {
|
||||
data: new BehaviorSubject(data),
|
||||
},
|
||||
},
|
||||
],
|
||||
schemas: [NO_ERRORS_SCHEMA],
|
||||
}).compileComponents();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(OnlineBoardRouteFilterComponent);
|
||||
component = fixture.componentInstance;
|
||||
locationService = TestBed.inject(UserLocationService);
|
||||
|
||||
isMobileMock = spyOn(MobileUtils, 'isMobile');
|
||||
data.urlParams.route = undefined;
|
||||
jasmine.clock().install();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jasmine.clock().uninstall();
|
||||
});
|
||||
|
||||
it('should return appropriate departure placeholder', () => {
|
||||
expect(component.departurePlaceholder).toBe('SHARED.CITY_PLACEHOLDER');
|
||||
component.arrival = 'MOW';
|
||||
expect(component.departurePlaceholder).toBe('SHARED.ALL_DIRECTIONS');
|
||||
});
|
||||
|
||||
it('should return appropriate arrival placeholder', () => {
|
||||
expect(component.arrivalPlaceholder).toBe('SHARED.CITY_PLACEHOLDER');
|
||||
component.departure = 'MOW';
|
||||
expect(component.arrivalPlaceholder).toBe('SHARED.ALL_DIRECTIONS');
|
||||
});
|
||||
|
||||
it('should exchange stations', () => {
|
||||
component.arrival = 'MOW';
|
||||
component.departure = 'GDX';
|
||||
component.exchange();
|
||||
|
||||
expect(component.arrival).toBe('GDX');
|
||||
expect(component.departure).toBe('MOW');
|
||||
});
|
||||
|
||||
it('should not emit invalid params', () => {
|
||||
component.onSearch.emit = jasmine.createSpy('onSearch.emit');
|
||||
component.arrival = 'AAA';
|
||||
|
||||
component.search();
|
||||
expect(component.onSearch.emit).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should emit valid params', async () => {
|
||||
const date = new Date(2022, 4, 28);
|
||||
component.onSearch.emit = jasmine.createSpy('onSearch.emit');
|
||||
component.arrival = 'MOW';
|
||||
component.departure = 'GDX';
|
||||
component.date = date;
|
||||
|
||||
await component.search();
|
||||
expect(component.onSearch.emit).toHaveBeenCalledWith({
|
||||
arrival: 'MOW',
|
||||
departure: 'GDX',
|
||||
date,
|
||||
timeFrom: undefined,
|
||||
timeTo: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it('should emit valid params with time range', async () => {
|
||||
const date = new Date(2022, 4, 28);
|
||||
component.onSearch.emit = jasmine.createSpy('onSearch.emit');
|
||||
component.arrival = 'MOW';
|
||||
component.departure = 'GDX';
|
||||
component.date = date;
|
||||
component.timeRange = {
|
||||
timeFrom: '1300',
|
||||
timeTo: '1700',
|
||||
};
|
||||
|
||||
await component.search();
|
||||
expect(component.onSearch.emit).toHaveBeenCalledWith({
|
||||
arrival: 'MOW',
|
||||
departure: 'GDX',
|
||||
date,
|
||||
timeFrom: '1300',
|
||||
timeTo: '1700',
|
||||
});
|
||||
});
|
||||
|
||||
it('should set state on init', () => {
|
||||
const date = new Date(2022, 4, 28);
|
||||
data.urlParams.route = {
|
||||
arrival: 'MOW',
|
||||
date,
|
||||
};
|
||||
|
||||
component.ngOnInit();
|
||||
expect(component.arrival).toBe('MOW');
|
||||
expect(component.date).toEqual(date);
|
||||
expect(component.timeRange).toBe(undefined);
|
||||
});
|
||||
|
||||
it('should set time range on init', () => {
|
||||
data.urlParams.route = {
|
||||
arrival: 'MOW',
|
||||
date: new Date(2022, 4, 28),
|
||||
timeFrom: '1300',
|
||||
timeTo: '1700',
|
||||
};
|
||||
|
||||
component.ngOnInit();
|
||||
expect(component.timeRange.timeFrom).toBe('1300');
|
||||
expect(component.timeRange.timeTo).toBe('1700');
|
||||
});
|
||||
|
||||
it('should set time range on init only if provided both time range fields', () => {
|
||||
data.urlParams.route = {
|
||||
arrival: 'MOW',
|
||||
date: new Date(2022, 4, 28),
|
||||
timeFrom: '1300',
|
||||
};
|
||||
|
||||
component.ngOnInit();
|
||||
expect(component.timeRange).toBe(undefined);
|
||||
});
|
||||
|
||||
it('should not set state on init if url params were not provided', () => {
|
||||
data.urlParams = undefined;
|
||||
const setStateMock = jasmine.createSpy('setState');
|
||||
(component as any).setState = setStateMock;
|
||||
|
||||
component.ngOnInit();
|
||||
expect(setStateMock).not.toHaveBeenCalled();
|
||||
data.urlParams = {};
|
||||
});
|
||||
|
||||
it('should not set state on init if where if no route field in url params', () => {
|
||||
const setStateMock = jasmine.createSpy('setState');
|
||||
(component as any).setState = setStateMock;
|
||||
|
||||
component.ngOnInit();
|
||||
expect(setStateMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should set default state on init if geolocation is permitted and device is not mobile', () => {
|
||||
isMobileMock.and.returnValue(false);
|
||||
const mockedToday = new Date(2022, 5, 6);
|
||||
jasmine.clock().mockDate(mockedToday);
|
||||
|
||||
locationService.locate();
|
||||
component.ngOnInit();
|
||||
|
||||
expect(component.departure).toBe('MOW');
|
||||
expect(areSame(component.date, mockedToday)).toBe(true);
|
||||
expect(component.timeRange).toBe(undefined);
|
||||
});
|
||||
|
||||
it('should set default time range on init if geolocation is permitted and device is mobile', () => {
|
||||
isMobileMock.and.returnValue(true);
|
||||
const mockedToday = new Date(2022, 5, 6, 14, 21);
|
||||
jasmine.clock().mockDate(mockedToday);
|
||||
|
||||
locationService.locate();
|
||||
component.ngOnInit();
|
||||
|
||||
expect(component.timeRange?.timeFrom).toBe('1300');
|
||||
expect(component.timeRange?.timeTo).toBe('1700');
|
||||
});
|
||||
|
||||
it('should not set default state on init if geolocation is not permitted', () => {
|
||||
component.ngOnInit();
|
||||
|
||||
expect(component.departure).toBe(undefined);
|
||||
expect(component.date).toBe(undefined);
|
||||
});
|
||||
});
|
||||
+170
@@ -0,0 +1,170 @@
|
||||
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { OnlineBoardRouteFilterValidationService } from '@online-board/components/filter/components/route-filter/services/online-board-route-filter-validation.service';
|
||||
import { IOnlineBoardRoutePageUrlParams } from '@online-board/services/url/types';
|
||||
import { IOnlineBoardRouteData } from '@online-board/types';
|
||||
import { OnlineBoardFiltersStateService } from '@shared/services/filters/online-board-filters-state.service';
|
||||
import { UserLocationService } from '@shared/services/user-location/user-location.service';
|
||||
import { IUrlTimeRange } from '@typings/common/url';
|
||||
import MobileUtils from '@utils/mobile';
|
||||
import { getDefaultTimeRangeOnMobile } from '@utils/time-range';
|
||||
import { OnlineBoardApiService } from '@online-board/services/api.service';
|
||||
import { StateService } from '@shared/services/state.service';
|
||||
|
||||
@Component({
|
||||
selector: 'online-board-route-filter',
|
||||
templateUrl: './online-board-route-filter.component.html',
|
||||
providers: [OnlineBoardRouteFilterValidationService],
|
||||
})
|
||||
export class OnlineBoardRouteFilterComponent implements OnInit {
|
||||
@Input() minDate: Date;
|
||||
@Input() maxDate: Date;
|
||||
disabledDates: Date[];
|
||||
|
||||
@Output() onSearch = new EventEmitter<IOnlineBoardRoutePageUrlParams>();
|
||||
|
||||
constructor(
|
||||
public validationService: OnlineBoardRouteFilterValidationService,
|
||||
private apiService: OnlineBoardApiService,
|
||||
private route: ActivatedRoute,
|
||||
private locationService: UserLocationService,
|
||||
private state: OnlineBoardFiltersStateService,
|
||||
private stateService: StateService,
|
||||
) {}
|
||||
|
||||
ngOnInit() {
|
||||
this.route.data.subscribe((data: IOnlineBoardRouteData) => {
|
||||
if (data.urlParams?.route) {
|
||||
this.setState(data.urlParams.route);
|
||||
}
|
||||
});
|
||||
|
||||
this.locationService.location.subscribe((location) => {
|
||||
// set default state only if user permitted geo position
|
||||
// search and filter isn't filled
|
||||
if (location && !this.departure && !this.arrival) {
|
||||
this.setDefaultState(location);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
get date() {
|
||||
return this.state.routeDate;
|
||||
}
|
||||
|
||||
set date(date: Date) {
|
||||
this.state.routeDate = date;
|
||||
}
|
||||
|
||||
get arrival() {
|
||||
return this.state.arrival;
|
||||
}
|
||||
|
||||
set arrival(arrival: string) {
|
||||
this.state.arrival = arrival;
|
||||
this.updateCalendar();
|
||||
}
|
||||
|
||||
get departure() {
|
||||
return this.state.departure;
|
||||
}
|
||||
|
||||
set departure(departure: string) {
|
||||
this.state.departure = departure;
|
||||
this.updateCalendar();
|
||||
}
|
||||
|
||||
get timeRange() {
|
||||
return this.state.timeRange;
|
||||
}
|
||||
|
||||
set timeRange(timeRange: IUrlTimeRange) {
|
||||
this.state.timeRange = timeRange;
|
||||
}
|
||||
|
||||
get departurePlaceholder() {
|
||||
return this.arrival
|
||||
? 'SHARED.ALL_DIRECTIONS'
|
||||
: 'SHARED.CITY_PLACEHOLDER';
|
||||
}
|
||||
|
||||
get arrivalPlaceholder() {
|
||||
return this.departure
|
||||
? 'SHARED.ALL_DIRECTIONS'
|
||||
: 'SHARED.CITY_PLACEHOLDER';
|
||||
}
|
||||
|
||||
async updateCalendar() {
|
||||
const isDepartureValid = await this.validationService.validateCode(this.departure);
|
||||
const isArrivalValid = await this.validationService.validateCode(this.arrival);
|
||||
|
||||
if (isDepartureValid || isArrivalValid) {
|
||||
var date = new Date();
|
||||
date.setUTCHours(0,0,0,0);
|
||||
date.setDate(date.getDate() - 1);
|
||||
this.apiService
|
||||
.getFlightDaysByRoute(date, isDepartureValid ? this.departure: undefined, isArrivalValid ? this.arrival: undefined)
|
||||
.then((res) => {
|
||||
this.disabledDates = new Array();
|
||||
for(var i=0;i<res.days.length;i++) {
|
||||
if (res.days[i] == '0') {
|
||||
this.disabledDates.push(new Date(date));
|
||||
}
|
||||
date.setDate(date.getDate() + 1);
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
});
|
||||
} else {
|
||||
this.disabledDates = new Array();
|
||||
}
|
||||
}
|
||||
|
||||
exchange() {
|
||||
[this.departure, this.arrival] = [this.arrival, this.departure];
|
||||
}
|
||||
|
||||
async search() {
|
||||
const params = this.getSearchParams();
|
||||
const areParamsValid = await this.validationService.validate(params);
|
||||
if (!areParamsValid) {
|
||||
return;
|
||||
}
|
||||
this.stateService.set("boardroute", params);
|
||||
|
||||
this.onSearch.emit(params);
|
||||
}
|
||||
|
||||
private setState(params: IOnlineBoardRoutePageUrlParams) {
|
||||
this.departure = params.departure;
|
||||
this.arrival = params.arrival;
|
||||
this.date = params.date;
|
||||
|
||||
if (params.timeFrom && params.timeTo) {
|
||||
this.timeRange = {
|
||||
timeFrom: params.timeFrom,
|
||||
timeTo: params.timeTo,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private setDefaultState(userLocation: string) {
|
||||
this.departure = userLocation;
|
||||
this.date = new Date();
|
||||
|
||||
if (MobileUtils.isMobile()) {
|
||||
this.timeRange = getDefaultTimeRangeOnMobile();
|
||||
}
|
||||
this.search();
|
||||
}
|
||||
|
||||
private getSearchParams(): IOnlineBoardRoutePageUrlParams {
|
||||
return {
|
||||
arrival: this.arrival,
|
||||
departure: this.departure,
|
||||
date: this.date,
|
||||
timeFrom: this.timeRange?.timeFrom,
|
||||
timeTo: this.timeRange?.timeTo,
|
||||
};
|
||||
}
|
||||
}
|
||||
+147
@@ -0,0 +1,147 @@
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
import { DictionariesService } from '@modules/components/page-filters/services/dictionaries-service';
|
||||
import { getDictionariesServiceMock } from '@modules/components/page-filters/services/dictionaries-service.mock';
|
||||
import { OnlineBoardRouteFilterValidationService } from '@online-board/components/filter/components/route-filter/services/online-board-route-filter-validation.service';
|
||||
import { IOnlineBoardRoutePageUrlParams } from '@online-board/services/url';
|
||||
import { StationCodeValidationService } from '@shared/services/validators/station-code.service';
|
||||
|
||||
describe('OnlineBoardRouteFilterValidationService', () => {
|
||||
let service: OnlineBoardRouteFilterValidationService;
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
providers: [
|
||||
OnlineBoardRouteFilterValidationService,
|
||||
StationCodeValidationService,
|
||||
{
|
||||
provide: DictionariesService,
|
||||
useValue: getDictionariesServiceMock(),
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
service = TestBed.inject(OnlineBoardRouteFilterValidationService);
|
||||
});
|
||||
|
||||
it('should return false because departure and arrival are not provided', async () => {
|
||||
const params: Partial<IOnlineBoardRoutePageUrlParams> = {};
|
||||
const result = await service.validate(params);
|
||||
|
||||
expect(result).toBe(false);
|
||||
expect(service.departureError).toBe('SHARED.DEPARTURE-CITY-ERROR');
|
||||
expect(service.arrivalError).toBe('SHARED.ARRIVAL-CITY-ERROR');
|
||||
});
|
||||
|
||||
it('should return false because departure is not valid', async () => {
|
||||
const params: Partial<IOnlineBoardRoutePageUrlParams> = {
|
||||
departure: 'AAA',
|
||||
};
|
||||
const result = await service.validate(params);
|
||||
|
||||
expect(result).toBe(false);
|
||||
expect(service.departureError).toBe('SHARED.DEPARTURE-CITY-ERROR');
|
||||
expect(service.arrivalError).toBe(null);
|
||||
});
|
||||
|
||||
it('should return false because arrival is not valid', async () => {
|
||||
const params: Partial<IOnlineBoardRoutePageUrlParams> = {
|
||||
arrival: 'AAA',
|
||||
};
|
||||
const result = await service.validate(params);
|
||||
|
||||
expect(result).toBe(false);
|
||||
expect(service.departureError).toBe(null);
|
||||
expect(service.arrivalError).toBe('SHARED.ARRIVAL-CITY-ERROR');
|
||||
});
|
||||
|
||||
it('should return false because arrival equals to departure', async () => {
|
||||
const params: Partial<IOnlineBoardRoutePageUrlParams> = {
|
||||
arrival: 'MOW',
|
||||
departure: 'MOW',
|
||||
};
|
||||
const result = await service.validate(params);
|
||||
|
||||
expect(result).toBe(false);
|
||||
expect(service.departureError).toBe(
|
||||
'SHARED.ARRIVAL-EQUALS-DEPARTURE-ERROR',
|
||||
);
|
||||
expect(service.arrivalError).toBe(null);
|
||||
});
|
||||
|
||||
it('should return false because date is not provided', async () => {
|
||||
const params: Partial<IOnlineBoardRoutePageUrlParams> = {
|
||||
arrival: 'MOW',
|
||||
};
|
||||
const result = await service.validate(params);
|
||||
|
||||
expect(result).toBe(false);
|
||||
expect(service.departureError).toBe(null);
|
||||
expect(service.arrivalError).toBe(null);
|
||||
expect(service.dateError).toBe('SHARED.DATE_FORMAT-WRONG');
|
||||
});
|
||||
|
||||
it('should return false because date is invalid', async () => {
|
||||
const params: Partial<IOnlineBoardRoutePageUrlParams> = {
|
||||
arrival: 'MOW',
|
||||
date: new Date('invalid'),
|
||||
};
|
||||
const result = await service.validate(params);
|
||||
|
||||
expect(result).toBe(false);
|
||||
expect(service.departureError).toBe(null);
|
||||
expect(service.arrivalError).toBe(null);
|
||||
expect(service.dateError).toBe('SHARED.DATE_FORMAT-WRONG');
|
||||
});
|
||||
|
||||
it('should return true because all params are valid', async () => {
|
||||
function assertTrue(result: boolean) {
|
||||
expect(result).toBe(true);
|
||||
expect(service.departureError).toBe(null);
|
||||
expect(service.arrivalError).toBe(null);
|
||||
expect(service.dateError).toBe(null);
|
||||
}
|
||||
const date = new Date(2022, 4, 28);
|
||||
|
||||
const arrivalParams: Partial<IOnlineBoardRoutePageUrlParams> = {
|
||||
arrival: 'MOW',
|
||||
date,
|
||||
};
|
||||
const arrivalResult = await service.validate(arrivalParams);
|
||||
assertTrue(arrivalResult);
|
||||
|
||||
const departureParams: Partial<IOnlineBoardRoutePageUrlParams> = {
|
||||
departure: 'MOW',
|
||||
date,
|
||||
};
|
||||
const departureResult = await service.validate(departureParams);
|
||||
assertTrue(departureResult);
|
||||
|
||||
const routeParams: Partial<IOnlineBoardRoutePageUrlParams> = {
|
||||
departure: 'MOW',
|
||||
arrival: 'GDX',
|
||||
date,
|
||||
};
|
||||
const routeResult = await service.validate(routeParams);
|
||||
assertTrue(routeResult);
|
||||
});
|
||||
|
||||
it('should clear errors before validation', async () => {
|
||||
const invalidParams: Partial<IOnlineBoardRoutePageUrlParams> = {};
|
||||
const invalidResult = await service.validate(invalidParams);
|
||||
|
||||
expect(invalidResult).toBe(false);
|
||||
expect(service.departureError).toBe('SHARED.DEPARTURE-CITY-ERROR');
|
||||
expect(service.arrivalError).toBe('SHARED.ARRIVAL-CITY-ERROR');
|
||||
|
||||
const validParams: Partial<IOnlineBoardRoutePageUrlParams> = {
|
||||
arrival: 'MOW',
|
||||
date: new Date(2022, 4, 28),
|
||||
};
|
||||
const validResult = await service.validate(validParams);
|
||||
|
||||
expect(validResult).toBe(true);
|
||||
expect(service.departureError).toBe(null);
|
||||
expect(service.arrivalError).toBe(null);
|
||||
expect(service.dateError).toBe(null);
|
||||
});
|
||||
});
|
||||
+58
@@ -0,0 +1,58 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { IOnlineBoardRoutePageUrlParams } from '@online-board/services/url';
|
||||
import { StationCodeValidationService } from '@shared/services/validators/station-code.service';
|
||||
import * as moment from 'moment';
|
||||
|
||||
@Injectable()
|
||||
export class OnlineBoardRouteFilterValidationService {
|
||||
dateError: string;
|
||||
departureError: string;
|
||||
arrivalError: string;
|
||||
|
||||
constructor(private stationValidator: StationCodeValidationService) {}
|
||||
|
||||
async validateCode(code: string) {
|
||||
return this.stationValidator.isStationCodeValid(code);
|
||||
}
|
||||
|
||||
async validate(params: Partial<IOnlineBoardRoutePageUrlParams>) {
|
||||
this.dateError = this.departureError = this.arrivalError = null;
|
||||
|
||||
if (!params.departure && !params.arrival) {
|
||||
this.departureError = 'SHARED.DEPARTURE-CITY-ERROR';
|
||||
this.arrivalError = 'SHARED.ARRIVAL-CITY-ERROR';
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
const isDepartureValid = params.departure
|
||||
? await this.stationValidator.isStationCodeValid(params.departure)
|
||||
: true;
|
||||
|
||||
if (!isDepartureValid) {
|
||||
this.departureError = 'SHARED.DEPARTURE-CITY-ERROR';
|
||||
return false;
|
||||
}
|
||||
|
||||
const isArrivalValid = params.arrival
|
||||
? await this.stationValidator.isStationCodeValid(params.arrival)
|
||||
: true;
|
||||
|
||||
if (!isArrivalValid) {
|
||||
this.arrivalError = 'SHARED.ARRIVAL-CITY-ERROR';
|
||||
return false;
|
||||
}
|
||||
|
||||
if (params.departure === params.arrival) {
|
||||
this.departureError = 'SHARED.ARRIVAL-EQUALS-DEPARTURE-ERROR';
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!params.date || !moment(params.date).isValid()) {
|
||||
this.dateError = 'SHARED.DATE_FORMAT-WRONG';
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
<section class="frame">
|
||||
<p-accordion
|
||||
(onOpen)="handleOpen($event)"
|
||||
(onClose)="handleClose()"
|
||||
expandIcon=""
|
||||
collapseIcon=""
|
||||
>
|
||||
<p-accordionTab
|
||||
[selected]="isSelected('flight')"
|
||||
data-testid="flight-filter"
|
||||
>
|
||||
<p-header>
|
||||
{{ 'BOARD.FLIGHT_NUMBER' | translate }}
|
||||
<arrow-down-icon
|
||||
[color]="getIconColor('flight')"
|
||||
[rotated]="isSelected('flight')"
|
||||
></arrow-down-icon>
|
||||
</p-header>
|
||||
<online-board-flight-number-filter
|
||||
[minDate]="settings.boardMinDate"
|
||||
[maxDate]="settings.boardMaxDate"
|
||||
(onSearch)="handleFlightNumberSearch($event)"
|
||||
></online-board-flight-number-filter>
|
||||
</p-accordionTab>
|
||||
<p-accordionTab
|
||||
[selected]="isSelected('route')"
|
||||
data-testid="route-filter"
|
||||
>
|
||||
<p-header>
|
||||
{{ 'BOARD.ROUTE' | translate }}
|
||||
<arrow-down-icon
|
||||
[color]="getIconColor('route')"
|
||||
[rotated]="isSelected('route')"
|
||||
></arrow-down-icon>
|
||||
</p-header>
|
||||
<online-board-route-filter
|
||||
[minDate]="settings.boardMinDate"
|
||||
[maxDate]="settings.boardMaxDate"
|
||||
(onSearch)="handleRouteSearch($event)"
|
||||
></online-board-route-filter>
|
||||
</p-accordionTab>
|
||||
</p-accordion>
|
||||
</section>
|
||||
+134
@@ -0,0 +1,134 @@
|
||||
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { OnlineBoardFilterComponent } from '@online-board/components/filter/online-board-filter.component';
|
||||
import { OnlineBoardFilterService } from '@online-board/services/filter/filter.service';
|
||||
import { getOnlineBoardFilterServiceMock } from '@online-board/services/filter/filter.service.mock';
|
||||
import { IOnlineBoardRoutePageUrlParams } from '@online-board/services/url';
|
||||
import { getMockPipe } from '@shared/pipes/pipe.mock';
|
||||
import { APP_SETTINGS } from '@shared/services';
|
||||
import { OnlineBoardFiltersStateService } from '@shared/services/filters/online-board-filters-state.service';
|
||||
import { IParsedFlightId } from '@typings/flight/flight-id';
|
||||
|
||||
describe('OnlineBoardFilterComponent', () => {
|
||||
let component: OnlineBoardFilterComponent;
|
||||
let fixture: ComponentFixture<OnlineBoardFilterComponent>;
|
||||
let filterService: OnlineBoardFilterService;
|
||||
let state: OnlineBoardFiltersStateService;
|
||||
|
||||
const startValidationDatesUpdatingIntervalMock = jasmine.createSpy(
|
||||
'startValidationDatesUpdatingInterval',
|
||||
);
|
||||
const stopValidationDatesUpdatingIntervalMock = jasmine.createSpy(
|
||||
'stopValidationDatesUpdatingInterval',
|
||||
);
|
||||
|
||||
const toFlightNumberPageMock = jasmine.createSpy('toFlightNumberPage');
|
||||
const toRoutePageMock = jasmine.createSpy('toRoutePage');
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [
|
||||
OnlineBoardFilterComponent,
|
||||
getMockPipe('translate'),
|
||||
],
|
||||
providers: [
|
||||
OnlineBoardFiltersStateService,
|
||||
{
|
||||
provide: APP_SETTINGS,
|
||||
useValue: {
|
||||
startValidationDatesUpdatingInterval:
|
||||
startValidationDatesUpdatingIntervalMock,
|
||||
stopValidationDatesUpdatingInterval:
|
||||
stopValidationDatesUpdatingIntervalMock,
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: OnlineBoardFilterService,
|
||||
useValue: getOnlineBoardFilterServiceMock(
|
||||
toFlightNumberPageMock,
|
||||
toRoutePageMock,
|
||||
),
|
||||
},
|
||||
],
|
||||
schemas: [NO_ERRORS_SCHEMA],
|
||||
}).compileComponents();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(OnlineBoardFilterComponent);
|
||||
component = fixture.componentInstance;
|
||||
filterService = TestBed.inject(OnlineBoardFilterService);
|
||||
state = TestBed.inject(OnlineBoardFiltersStateService);
|
||||
|
||||
startValidationDatesUpdatingIntervalMock.calls.reset();
|
||||
stopValidationDatesUpdatingIntervalMock.calls.reset();
|
||||
toFlightNumberPageMock.calls.reset();
|
||||
toRoutePageMock.calls.reset();
|
||||
});
|
||||
|
||||
it('Should start dates updating interval on init', () => {
|
||||
component.ngOnInit();
|
||||
expect(startValidationDatesUpdatingIntervalMock).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('Should stop dates updating interval on destroy', () => {
|
||||
component.ngOnDestroy();
|
||||
expect(stopValidationDatesUpdatingIntervalMock).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should set selected tab', () => {
|
||||
expect(state.selectedTab).toBe('route');
|
||||
|
||||
component.handleOpen({ index: 0 });
|
||||
expect(state.selectedTab).toBe('flight');
|
||||
|
||||
component.handleOpen({ index: 1 });
|
||||
expect(state.selectedTab).toBe('route');
|
||||
});
|
||||
|
||||
it('should clear selected tab if selected tab was clicked', () => {
|
||||
expect(state.selectedTab).toBe('route');
|
||||
component.handleClose();
|
||||
|
||||
expect(state.selectedTab).toBe(undefined);
|
||||
});
|
||||
|
||||
it('should return selected tab', () => {
|
||||
expect(component.selectedTab === state.selectedTab).toBe(true);
|
||||
});
|
||||
|
||||
it('should return true if tab is selected', () => {
|
||||
expect(component.selectedTab).toBe('route');
|
||||
|
||||
expect(component.isSelected('flight')).toBe(false);
|
||||
expect(component.isSelected('route')).toBe(true);
|
||||
});
|
||||
|
||||
it('should return appropriate icon color', () => {
|
||||
expect(component.selectedTab).toBe('route');
|
||||
|
||||
expect(component.getIconColor('flight')).toBe('blue');
|
||||
expect(component.getIconColor('route')).toBe('gray');
|
||||
});
|
||||
|
||||
it('should call toFlightNumberPage', () => {
|
||||
const params: IParsedFlightId = {
|
||||
flightNumber: '0001',
|
||||
carrier: 'SU',
|
||||
date: new Date(2022, 4, 28),
|
||||
};
|
||||
|
||||
component.handleFlightNumberSearch(params);
|
||||
expect(filterService.toFlightNumberPage).toHaveBeenCalledWith(params);
|
||||
});
|
||||
|
||||
it('should call toRoutePage', () => {
|
||||
const params: IOnlineBoardRoutePageUrlParams = {
|
||||
arrival: 'MOW',
|
||||
date: new Date(2022, 4, 28),
|
||||
};
|
||||
|
||||
component.handleRouteSearch(params);
|
||||
expect(filterService.toRoutePage).toHaveBeenCalledWith(params);
|
||||
});
|
||||
});
|
||||
+87
@@ -0,0 +1,87 @@
|
||||
import { Component, Inject, OnDestroy, OnInit } from '@angular/core';
|
||||
import { OnlineBoardFilterService } from '@online-board/services/filter/filter.service';
|
||||
import { IOnlineBoardRoutePageUrlParams } from '@online-board/services/url';
|
||||
import { AppSettings } from '@shared/models-legacy';
|
||||
import { APP_SETTINGS } from '@shared/services';
|
||||
import {
|
||||
IOnlineBoardFilterSelectedTab,
|
||||
OnlineBoardFiltersStateService,
|
||||
} from '@shared/services/filters/online-board-filters-state.service';
|
||||
import { IArrowIconColor } from '@toolkit/icons/arrow-down/arrow-down-icon.component';
|
||||
import { IParsedFlightId } from '@typings/flight/flight-id';
|
||||
import { StateService } from '@shared/services/state.service';
|
||||
|
||||
type IAccordionToggleEvent = {
|
||||
index: number;
|
||||
};
|
||||
|
||||
@Component({
|
||||
selector: 'online-board-filter',
|
||||
templateUrl: './online-board-filter.component.html',
|
||||
})
|
||||
export class OnlineBoardFilterComponent implements OnInit, OnDestroy {
|
||||
constructor(
|
||||
@Inject(APP_SETTINGS) public settings: AppSettings,
|
||||
private filtersService: OnlineBoardFilterService,
|
||||
private state: OnlineBoardFiltersStateService,
|
||||
private stateService: StateService,
|
||||
) {
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
//this.settings.startValidationDatesUpdatingInterval();
|
||||
if (this.state.selectedTab == 'flight') {
|
||||
var saved_params = this.stateService.get("boardnumber");
|
||||
if (saved_params) {
|
||||
this.filtersService.toFlightNumberPage(saved_params);
|
||||
}
|
||||
}
|
||||
if (this.state.selectedTab == 'route') {
|
||||
var saved_params = this.stateService.get("boardroute");
|
||||
if (saved_params) {
|
||||
this.filtersService.toRoutePage(saved_params);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
//this.settings.stopValidationDatesUpdatingInterval();
|
||||
}
|
||||
|
||||
get selectedTab() {
|
||||
return this.state.selectedTab;
|
||||
}
|
||||
|
||||
getIconColor(value: IOnlineBoardFilterSelectedTab): IArrowIconColor {
|
||||
return this.isSelected(value) ? 'gray' : 'blue';
|
||||
}
|
||||
|
||||
isSelected(value: IOnlineBoardFilterSelectedTab) {
|
||||
return this.selectedTab === value;
|
||||
}
|
||||
|
||||
handleOpen(event: IAccordionToggleEvent) {
|
||||
switch (event.index) {
|
||||
case 0: {
|
||||
this.state.selectedTab = 'flight';
|
||||
return;
|
||||
}
|
||||
case 1: {
|
||||
this.state.selectedTab = 'route';
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
handleClose() {
|
||||
this.state.clearSelectedTab();
|
||||
}
|
||||
|
||||
handleFlightNumberSearch(params: IParsedFlightId) {
|
||||
return this.filtersService.toFlightNumberPage(params);
|
||||
}
|
||||
|
||||
handleRouteSearch(params: IOnlineBoardRoutePageUrlParams) {
|
||||
return this.filtersService.toRoutePage(params);
|
||||
}
|
||||
}
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { AccordionModule } from 'primeng/accordion';
|
||||
import { ComponentsModule } from '@components/components.module';
|
||||
import { ToolkitModule } from '@toolkit/toolkit.module';
|
||||
import { OnlineBoardFilterComponent } from './online-board-filter.component';
|
||||
import { OnlineBoardFlightNumberFilterComponent } from './components/flight-number-filter/flight-number-filter.component';
|
||||
import { OnlineBoardRouteFilterComponent } from './components/route-filter/online-board-route-filter.component';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
OnlineBoardFilterComponent,
|
||||
OnlineBoardFlightNumberFilterComponent,
|
||||
OnlineBoardRouteFilterComponent,
|
||||
],
|
||||
exports: [OnlineBoardFilterComponent],
|
||||
imports: [
|
||||
CommonModule,
|
||||
FormsModule,
|
||||
ComponentsModule,
|
||||
AccordionModule,
|
||||
TranslateModule,
|
||||
ToolkitModule,
|
||||
],
|
||||
})
|
||||
export class OnlineBoardFilterModule {}
|
||||
@@ -0,0 +1,134 @@
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
import { DictionariesService } from '@modules/components/page-filters/services/dictionaries-service';
|
||||
import { CanActivateArrivalSearch } from '@online-board/guards/can-activate-arrival-search';
|
||||
import { CanActivateDateParams } from '@online-board/guards/utils/can-activate-date-params';
|
||||
import { CanActivateRouteParams } from '@online-board/guards/utils/can-activate-route-params';
|
||||
import { CanActivateStations } from '@online-board/guards/utils/can-activate-stations';
|
||||
import { OnlineBoardDateValidationService } from '@online-board/services/date-validation.service';
|
||||
import { OnlineBoardNavigationService } from '@online-board/services/navigation.service';
|
||||
import { OnlineBoardUrlParserService } from '@online-board/services/url/url-parser.service';
|
||||
import { AppSettingsService } from '@shared/services';
|
||||
import { StationCodeValidationService } from '@shared/services/validators/station-code.service';
|
||||
import { TimeRangeValidationService } from '@shared/services/validators/time-range.service';
|
||||
|
||||
describe('CanActivateArrivalSearch', () => {
|
||||
let service: CanActivateArrivalSearch;
|
||||
|
||||
const boardMinDate = new Date(2022, 4, 23);
|
||||
const boardMaxDate = new Date(2022, 4, 29);
|
||||
const appSettingsService: Partial<AppSettingsService> = {
|
||||
getSettings: () => {
|
||||
return Promise.resolve({
|
||||
boardMinDate,
|
||||
boardMaxDate,
|
||||
}) as any;
|
||||
},
|
||||
};
|
||||
|
||||
const toNotFoundMock = jasmine.createSpy('toNotFound');
|
||||
const toStartPageMock = jasmine.createSpy('toStartPage');
|
||||
const navigationService: Partial<OnlineBoardNavigationService> = {
|
||||
toNotFound: toNotFoundMock,
|
||||
toStartPage: toStartPageMock,
|
||||
};
|
||||
|
||||
const dictionariesService: Partial<DictionariesService> = {
|
||||
ready$: Promise.resolve(),
|
||||
getCityOrAirport(code: string) {
|
||||
const map = new Map();
|
||||
map.set('MOW', true);
|
||||
map.set('LED', true);
|
||||
map.set('KUF', true);
|
||||
map.set('SVO', true);
|
||||
|
||||
return map.has(code) as any;
|
||||
},
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
providers: [
|
||||
CanActivateArrivalSearch,
|
||||
CanActivateRouteParams,
|
||||
CanActivateStations,
|
||||
CanActivateDateParams,
|
||||
StationCodeValidationService,
|
||||
OnlineBoardDateValidationService,
|
||||
TimeRangeValidationService,
|
||||
OnlineBoardUrlParserService,
|
||||
{
|
||||
provide: AppSettingsService,
|
||||
useValue: appSettingsService,
|
||||
},
|
||||
{
|
||||
provide: OnlineBoardNavigationService,
|
||||
useValue: navigationService,
|
||||
},
|
||||
{
|
||||
provide: DictionariesService,
|
||||
useValue: dictionariesService,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
service = TestBed.inject(CanActivateArrivalSearch);
|
||||
});
|
||||
|
||||
it('should return true for valid arrival params without time range', async () => {
|
||||
const params = 'MOW-20220528';
|
||||
const result = await service.canActivate({
|
||||
params: {
|
||||
params,
|
||||
},
|
||||
} as any);
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should return true for valid arrival params with time range', async () => {
|
||||
const params = 'MOW-20220528-06002200';
|
||||
const result = await service.canActivate({
|
||||
params: {
|
||||
params,
|
||||
},
|
||||
} as any);
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should redirect to not found because arrival city is invalid', async () => {
|
||||
const params = 'ABA-20220528';
|
||||
const result = await service.canActivate({
|
||||
params: {
|
||||
params,
|
||||
},
|
||||
} as any);
|
||||
|
||||
expect(result).not.toBe(true);
|
||||
expect(toNotFoundMock).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should redirect to start page because date is invalid', async () => {
|
||||
const params = 'MOW-20220521';
|
||||
const result = await service.canActivate({
|
||||
params: {
|
||||
params,
|
||||
},
|
||||
} as any);
|
||||
|
||||
expect(result).not.toBe(true);
|
||||
expect(toStartPageMock).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should redirect to start page because time range is invalid', async () => {
|
||||
const params = 'MOW-20220528-22000600';
|
||||
const result = await service.canActivate({
|
||||
params: {
|
||||
params,
|
||||
},
|
||||
} as any);
|
||||
|
||||
expect(result).not.toBe(true);
|
||||
expect(toStartPageMock).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,20 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { ActivatedRouteSnapshot, CanActivate } from '@angular/router';
|
||||
import { OnlineBoardUrlParserService } from '@online-board/services/url/url-parser.service';
|
||||
import { CanActivateRouteParams } from './utils/can-activate-route-params';
|
||||
|
||||
@Injectable()
|
||||
export class CanActivateArrivalSearch implements CanActivate {
|
||||
constructor(
|
||||
private urlService: OnlineBoardUrlParserService,
|
||||
private routeParamsService: CanActivateRouteParams,
|
||||
) {}
|
||||
|
||||
canActivate(route: ActivatedRouteSnapshot) {
|
||||
const urlParams = this.urlService.parseArrivalSearchUrlParams(
|
||||
route.params.params,
|
||||
);
|
||||
|
||||
return this.routeParamsService.canActivate(urlParams);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
import { DictionariesService } from '@modules/components/page-filters/services/dictionaries-service';
|
||||
import { CanActivateDepartureSearch } from '@online-board/guards/can-activate-departure-search';
|
||||
import { CanActivateDateParams } from '@online-board/guards/utils/can-activate-date-params';
|
||||
import { CanActivateRouteParams } from '@online-board/guards/utils/can-activate-route-params';
|
||||
import { CanActivateStations } from '@online-board/guards/utils/can-activate-stations';
|
||||
import { OnlineBoardDateValidationService } from '@online-board/services/date-validation.service';
|
||||
import { OnlineBoardNavigationService } from '@online-board/services/navigation.service';
|
||||
import { OnlineBoardUrlParserService } from '@online-board/services/url/url-parser.service';
|
||||
import { AppSettingsService } from '@shared/services';
|
||||
import { StationCodeValidationService } from '@shared/services/validators/station-code.service';
|
||||
import { TimeRangeValidationService } from '@shared/services/validators/time-range.service';
|
||||
|
||||
describe('CanActivateDepartureSearch', () => {
|
||||
let service: CanActivateDepartureSearch;
|
||||
|
||||
const boardMinDate = new Date(2022, 4, 23);
|
||||
const boardMaxDate = new Date(2022, 4, 29);
|
||||
const appSettingsService: Partial<AppSettingsService> = {
|
||||
getSettings: () => {
|
||||
return Promise.resolve({
|
||||
boardMinDate,
|
||||
boardMaxDate,
|
||||
}) as any;
|
||||
},
|
||||
};
|
||||
|
||||
const toNotFoundMock = jasmine.createSpy('toNotFound');
|
||||
const toStartPageMock = jasmine.createSpy('toStartPage');
|
||||
const navigationService: Partial<OnlineBoardNavigationService> = {
|
||||
toNotFound: toNotFoundMock,
|
||||
toStartPage: toStartPageMock,
|
||||
};
|
||||
|
||||
const dictionariesService: Partial<DictionariesService> = {
|
||||
ready$: Promise.resolve(),
|
||||
getCityOrAirport(code: string) {
|
||||
const map = new Map();
|
||||
map.set('MOW', true);
|
||||
map.set('LED', true);
|
||||
map.set('KUF', true);
|
||||
map.set('SVO', true);
|
||||
|
||||
return map.has(code) as any;
|
||||
},
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
providers: [
|
||||
CanActivateDepartureSearch,
|
||||
CanActivateRouteParams,
|
||||
CanActivateStations,
|
||||
CanActivateDateParams,
|
||||
StationCodeValidationService,
|
||||
OnlineBoardDateValidationService,
|
||||
TimeRangeValidationService,
|
||||
OnlineBoardUrlParserService,
|
||||
{
|
||||
provide: AppSettingsService,
|
||||
useValue: appSettingsService,
|
||||
},
|
||||
{
|
||||
provide: OnlineBoardNavigationService,
|
||||
useValue: navigationService,
|
||||
},
|
||||
{
|
||||
provide: DictionariesService,
|
||||
useValue: dictionariesService,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
service = TestBed.inject(CanActivateDepartureSearch);
|
||||
});
|
||||
|
||||
it('should return true for valid departure params without time range', async () => {
|
||||
const params = 'MOW-20220528';
|
||||
const result = await service.canActivate({
|
||||
params: {
|
||||
params,
|
||||
},
|
||||
} as any);
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should return true for valid departure params with time range', async () => {
|
||||
const params = 'MOW-20220528-06002200';
|
||||
const result = await service.canActivate({
|
||||
params: {
|
||||
params,
|
||||
},
|
||||
} as any);
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should redirect to not found because departure city is invalid', async () => {
|
||||
const params = 'ABA-20220528';
|
||||
const result = await service.canActivate({
|
||||
params: {
|
||||
params,
|
||||
},
|
||||
} as any);
|
||||
|
||||
expect(result).not.toBe(true);
|
||||
expect(toNotFoundMock).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should redirect to start page because date is invalid', async () => {
|
||||
const params = 'MOW-20220521';
|
||||
const result = await service.canActivate({
|
||||
params: {
|
||||
params,
|
||||
},
|
||||
} as any);
|
||||
|
||||
expect(result).not.toBe(true);
|
||||
expect(toStartPageMock).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should redirect to start page because time range is invalid', async () => {
|
||||
const params = 'MOW-20220528-22000600';
|
||||
const result = await service.canActivate({
|
||||
params: {
|
||||
params,
|
||||
},
|
||||
} as any);
|
||||
|
||||
expect(result).not.toBe(true);
|
||||
expect(toStartPageMock).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,20 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { ActivatedRouteSnapshot, CanActivate } from '@angular/router';
|
||||
import { OnlineBoardUrlParserService } from '@online-board/services/url/url-parser.service';
|
||||
import { CanActivateRouteParams } from './utils/can-activate-route-params';
|
||||
|
||||
@Injectable()
|
||||
export class CanActivateDepartureSearch implements CanActivate {
|
||||
constructor(
|
||||
private urlService: OnlineBoardUrlParserService,
|
||||
private routeParamsService: CanActivateRouteParams,
|
||||
) {}
|
||||
|
||||
canActivate(route: ActivatedRouteSnapshot) {
|
||||
const urlParams = this.urlService.parseDepartureSearchUrlParams(
|
||||
route.params.params,
|
||||
);
|
||||
|
||||
return this.routeParamsService.canActivate(urlParams);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,183 @@
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
import { CanActivateOnlineBoardDetails } from '@online-board/guards/can-activate-details';
|
||||
import { CanActivateDateParams } from '@online-board/guards/utils/can-activate-date-params';
|
||||
import { CanActivateRouteParams } from '@online-board/guards/utils/can-activate-route-params';
|
||||
import { OnlineBoardNavigationService } from '@online-board/services/navigation.service';
|
||||
import { OnlineBoardRequestParserService } from '@online-board/services/request/request-parser.service';
|
||||
import { OnlineBoardUrlParserService } from '@online-board/services/url';
|
||||
import { IParsedFlightId } from '@typings/flight/flight-id';
|
||||
|
||||
describe('CanActivateOnlineBoardDetails', () => {
|
||||
let service: CanActivateOnlineBoardDetails;
|
||||
const canActivateDateParamsMock = jasmine.createSpy(
|
||||
'canActivateDateParams',
|
||||
);
|
||||
const validateDateParamsMock = jasmine.createSpy('validateDateParams');
|
||||
const validateRouteParamsMock = jasmine.createSpy('validateRouteParams');
|
||||
const parseMock = jasmine.createSpy('parse');
|
||||
const toDetailsPageMock = jasmine.createSpy('toDetailsPage');
|
||||
const parseFlightIdMock = jasmine.createSpy('parseFlightId');
|
||||
const activatedRouteParams = {
|
||||
params: 'SU0001-20220528',
|
||||
};
|
||||
const activatedRouteQueryParams = {
|
||||
request: 'request string',
|
||||
};
|
||||
|
||||
const activatedRoute = {} as any;
|
||||
const parsedFlightId: IParsedFlightId = {
|
||||
flightNumber: '0001',
|
||||
carrier: 'SU',
|
||||
date: new Date(2022, 4, 28),
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
providers: [
|
||||
CanActivateOnlineBoardDetails,
|
||||
{
|
||||
provide: OnlineBoardUrlParserService,
|
||||
useValue: {
|
||||
parseFlightId: parseFlightIdMock,
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: CanActivateDateParams,
|
||||
useValue: {
|
||||
canActivate: canActivateDateParamsMock,
|
||||
validateDateParams: validateDateParamsMock,
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: CanActivateRouteParams,
|
||||
useValue: {
|
||||
validateRouteParams: validateRouteParamsMock,
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: OnlineBoardRequestParserService,
|
||||
useValue: {
|
||||
parse: parseMock,
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: OnlineBoardNavigationService,
|
||||
useValue: {
|
||||
toDetailsPage: toDetailsPageMock,
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
service = TestBed.inject(CanActivateOnlineBoardDetails);
|
||||
canActivateDateParamsMock.calls.reset();
|
||||
validateRouteParamsMock.calls.reset();
|
||||
parseMock.calls.reset();
|
||||
toDetailsPageMock.calls.reset();
|
||||
|
||||
activatedRoute.params = activatedRouteParams;
|
||||
activatedRoute.queryParams = activatedRouteQueryParams;
|
||||
|
||||
parseFlightIdMock
|
||||
.withArgs(activatedRoute.params.params)
|
||||
.and.returnValue(parsedFlightId);
|
||||
});
|
||||
|
||||
it('should return date validation result because it is not true', async () => {
|
||||
const mockedResult = {};
|
||||
canActivateDateParamsMock
|
||||
.withArgs(parsedFlightId)
|
||||
.and.returnValue(Promise.resolve(mockedResult));
|
||||
|
||||
const result = await service.canActivate(activatedRoute);
|
||||
expect(result).toBe(mockedResult as any);
|
||||
});
|
||||
|
||||
it('should return true because route params are valid and there is no request', async () => {
|
||||
activatedRoute.queryParams = {
|
||||
request: '',
|
||||
};
|
||||
canActivateDateParamsMock
|
||||
.withArgs(parsedFlightId)
|
||||
.and.returnValue(Promise.resolve(true));
|
||||
|
||||
const result = await service.canActivate(activatedRoute);
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should return true if route params and request are valid', async () => {
|
||||
const mockedParsingResult = {
|
||||
type: 'flight-number',
|
||||
params: {},
|
||||
} as any;
|
||||
|
||||
canActivateDateParamsMock
|
||||
.withArgs(parsedFlightId)
|
||||
.and.returnValue(Promise.resolve(true));
|
||||
parseMock
|
||||
.withArgs(activatedRoute.queryParams.request)
|
||||
.and.returnValue(mockedParsingResult);
|
||||
validateDateParamsMock
|
||||
.withArgs(mockedParsingResult.params)
|
||||
.and.returnValue(Promise.resolve(true));
|
||||
|
||||
const result = await service.canActivate(activatedRoute);
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should call toDetailsPage because flight number params are invalid', async () => {
|
||||
const mockedParsingResult = {
|
||||
type: 'flight-number',
|
||||
params: {},
|
||||
} as any;
|
||||
|
||||
canActivateDateParamsMock
|
||||
.withArgs(parsedFlightId)
|
||||
.and.returnValue(Promise.resolve(true));
|
||||
parseMock
|
||||
.withArgs(activatedRoute.queryParams.request)
|
||||
.and.returnValue(mockedParsingResult);
|
||||
validateDateParamsMock
|
||||
.withArgs(mockedParsingResult.params)
|
||||
.and.returnValue(Promise.resolve({}));
|
||||
|
||||
const result = await service.canActivate(activatedRoute);
|
||||
expect(result).not.toBe(true);
|
||||
expect(toDetailsPageMock).toHaveBeenCalledWith({
|
||||
flightId: {
|
||||
flightNumber: '0001',
|
||||
carrier: 'SU',
|
||||
date: '',
|
||||
},
|
||||
date: new Date(2022, 4, 28),
|
||||
});
|
||||
});
|
||||
|
||||
it('should call toDetailsPage because route params are invalid', async () => {
|
||||
const mockedParsingResult = {
|
||||
type: 'route',
|
||||
params: {},
|
||||
} as any;
|
||||
|
||||
canActivateDateParamsMock
|
||||
.withArgs(parsedFlightId)
|
||||
.and.returnValue(Promise.resolve(true));
|
||||
parseMock
|
||||
.withArgs(activatedRoute.queryParams.request)
|
||||
.and.returnValue(mockedParsingResult);
|
||||
validateRouteParamsMock
|
||||
.withArgs(mockedParsingResult.params)
|
||||
.and.returnValue(Promise.resolve({}));
|
||||
|
||||
const result = await service.canActivate(activatedRoute);
|
||||
expect(result).not.toBe(true);
|
||||
expect(toDetailsPageMock).toHaveBeenCalledWith({
|
||||
flightId: {
|
||||
flightNumber: '0001',
|
||||
carrier: 'SU',
|
||||
date: '',
|
||||
},
|
||||
date: new Date(2022, 4, 28),
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,76 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { ActivatedRouteSnapshot, CanActivate } from '@angular/router';
|
||||
import { OnlineBoardNavigationService } from '@online-board/services/navigation.service';
|
||||
import { OnlineBoardRequestParserService } from '@online-board/services/request/request-parser.service';
|
||||
import { OnlineBoardUrlParserService } from '@online-board/services/url';
|
||||
import { CanActivateDateParams } from './utils/can-activate-date-params';
|
||||
import { CanActivateRouteParams } from './utils/can-activate-route-params';
|
||||
import { CanActivateRedirrect } from './utils/can-activate-redirrect-to-not-found';
|
||||
|
||||
@Injectable()
|
||||
export class CanActivateOnlineBoardDetails implements CanActivate {
|
||||
constructor(
|
||||
private urlService: OnlineBoardUrlParserService,
|
||||
private dateParamsService: CanActivateDateParams,
|
||||
private routeParamsService: CanActivateRouteParams,
|
||||
private requestService: OnlineBoardRequestParserService,
|
||||
private navigationService: OnlineBoardNavigationService,
|
||||
private canRedirrect: CanActivateRedirrect,
|
||||
) { }
|
||||
|
||||
async canActivate(route: ActivatedRouteSnapshot) {
|
||||
const urlParams = this.urlService.parseFlightId(route.params.params);
|
||||
|
||||
if (!this.canRedirrect.validateFlightNumber(urlParams.flightNumber)) {
|
||||
return this.navigationService.toNotFound();
|
||||
}
|
||||
|
||||
const validationResult = await this.dateParamsService.canActivate(
|
||||
urlParams,
|
||||
);
|
||||
|
||||
if (validationResult !== true) {
|
||||
return validationResult;
|
||||
}
|
||||
|
||||
const requestValidationResult = await this.canActivateQueryParams(
|
||||
route,
|
||||
);
|
||||
|
||||
if (requestValidationResult !== true) {
|
||||
return this.navigationService.toDetailsPage({
|
||||
flightId: {
|
||||
flightNumber: urlParams.flightNumber,
|
||||
carrier: urlParams.carrier,
|
||||
suffix: urlParams.suffix,
|
||||
date: '',
|
||||
},
|
||||
date: urlParams.date,
|
||||
});
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private async canActivateQueryParams(route: ActivatedRouteSnapshot) {
|
||||
const { request } = route.queryParams;
|
||||
if (!request) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const onlineBoardRequest = this.requestService.parse(request);
|
||||
|
||||
switch (onlineBoardRequest.type) {
|
||||
case 'flight-number': {
|
||||
return this.dateParamsService.validateDateParams(
|
||||
onlineBoardRequest.params,
|
||||
);
|
||||
}
|
||||
case 'route': {
|
||||
return this.routeParamsService.validateRouteParams(
|
||||
onlineBoardRequest.params,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+73
@@ -0,0 +1,73 @@
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
import { CanActivateFlightNumberSearch } from '@online-board/guards/can-activate-flight-number-search';
|
||||
import { CanActivateDateParams } from '@online-board/guards/utils/can-activate-date-params';
|
||||
import { OnlineBoardDateValidationService } from '@online-board/services/date-validation.service';
|
||||
import { OnlineBoardNavigationService } from '@online-board/services/navigation.service';
|
||||
import { OnlineBoardUrlParserService } from '@online-board/services/url/url-parser.service';
|
||||
import { AppSettingsService } from '@shared/services';
|
||||
import { TimeRangeValidationService } from '@shared/services/validators/time-range.service';
|
||||
|
||||
describe('CanActivateFlightNumberSearch', () => {
|
||||
let service: CanActivateFlightNumberSearch;
|
||||
|
||||
const toStartPageMock = jasmine.createSpy('toStartPage');
|
||||
const navigationService: Partial<OnlineBoardNavigationService> = {
|
||||
toStartPage: toStartPageMock,
|
||||
};
|
||||
|
||||
const boardMinDate = new Date(2022, 4, 23);
|
||||
const boardMaxDate = new Date(2022, 4, 29);
|
||||
const appSettingsService: Partial<AppSettingsService> = {
|
||||
getSettings: () => {
|
||||
return Promise.resolve({
|
||||
boardMinDate,
|
||||
boardMaxDate,
|
||||
}) as any;
|
||||
},
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
providers: [
|
||||
CanActivateDateParams,
|
||||
CanActivateFlightNumberSearch,
|
||||
TimeRangeValidationService,
|
||||
OnlineBoardDateValidationService,
|
||||
OnlineBoardUrlParserService,
|
||||
{
|
||||
provide: OnlineBoardNavigationService,
|
||||
useValue: navigationService,
|
||||
},
|
||||
{
|
||||
provide: AppSettingsService,
|
||||
useValue: appSettingsService,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
service = TestBed.inject(CanActivateFlightNumberSearch);
|
||||
});
|
||||
|
||||
it('should return true for valid flight number search page params', async () => {
|
||||
const params = 'SU0001-20220525';
|
||||
const result = await service.canActivate({
|
||||
params: {
|
||||
params,
|
||||
},
|
||||
} as any);
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should redirect to to start page because date is invalid', async () => {
|
||||
const params = 'SU0001-20220521';
|
||||
const result = await service.canActivate({
|
||||
params: {
|
||||
params,
|
||||
},
|
||||
} as any);
|
||||
|
||||
expect(result).not.toBe(true);
|
||||
expect(toStartPageMock).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,26 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { ActivatedRouteSnapshot, CanActivate } from '@angular/router';
|
||||
import { CanActivateDateParams } from '@online-board/guards/utils/can-activate-date-params';
|
||||
import { OnlineBoardUrlParserService } from '@online-board/services/url/url-parser.service';
|
||||
import { CanActivateRedirrect } from './utils/can-activate-redirrect-to-not-found';
|
||||
import { OnlineBoardNavigationService } from '../services/navigation.service';
|
||||
|
||||
@Injectable()
|
||||
export class CanActivateFlightNumberSearch implements CanActivate {
|
||||
constructor(
|
||||
private urlService: OnlineBoardUrlParserService,
|
||||
private dateParamsService: CanActivateDateParams,
|
||||
private canRedirrect: CanActivateRedirrect,
|
||||
private navigationService: OnlineBoardNavigationService,
|
||||
) {}
|
||||
|
||||
async canActivate(route: ActivatedRouteSnapshot) {
|
||||
const urlParams = this.urlService.parseFlightId(route.params.params);
|
||||
|
||||
if (!this.canRedirrect.validateFlightNumber(urlParams.flightNumber)) {
|
||||
return this.navigationService.toNotFound();
|
||||
}
|
||||
|
||||
return this.dateParamsService.canActivate(urlParams);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
import { DictionariesService } from '@modules/components/page-filters/services/dictionaries-service';
|
||||
import { CanActivateRouteSearch } from '@online-board/guards/can-activate-route-search';
|
||||
import { CanActivateDateParams } from '@online-board/guards/utils/can-activate-date-params';
|
||||
import { CanActivateRouteParams } from '@online-board/guards/utils/can-activate-route-params';
|
||||
import { CanActivateStations } from '@online-board/guards/utils/can-activate-stations';
|
||||
import { OnlineBoardDateValidationService } from '@online-board/services/date-validation.service';
|
||||
import { OnlineBoardNavigationService } from '@online-board/services/navigation.service';
|
||||
import { OnlineBoardUrlParserService } from '@online-board/services/url/url-parser.service';
|
||||
import { AppSettingsService } from '@shared/services';
|
||||
import { StationCodeValidationService } from '@shared/services/validators/station-code.service';
|
||||
import { TimeRangeValidationService } from '@shared/services/validators/time-range.service';
|
||||
|
||||
describe('CanActivateRouteSearch', () => {
|
||||
let service: CanActivateRouteSearch;
|
||||
|
||||
const boardMinDate = new Date(2022, 4, 23);
|
||||
const boardMaxDate = new Date(2022, 4, 29);
|
||||
const appSettingsService: Partial<AppSettingsService> = {
|
||||
getSettings: () => {
|
||||
return Promise.resolve({
|
||||
boardMinDate,
|
||||
boardMaxDate,
|
||||
}) as any;
|
||||
},
|
||||
};
|
||||
|
||||
const toNotFoundMock = jasmine.createSpy('toNotFound');
|
||||
const toStartPageMock = jasmine.createSpy('toStartPage');
|
||||
const navigationService: Partial<OnlineBoardNavigationService> = {
|
||||
toNotFound: toNotFoundMock,
|
||||
toStartPage: toStartPageMock,
|
||||
};
|
||||
|
||||
const dictionariesService: Partial<DictionariesService> = {
|
||||
ready$: Promise.resolve(),
|
||||
getCityOrAirport(code: string) {
|
||||
const map = new Map();
|
||||
map.set('MOW', true);
|
||||
map.set('LED', true);
|
||||
map.set('KUF', true);
|
||||
map.set('SVO', true);
|
||||
|
||||
return map.has(code) as any;
|
||||
},
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
providers: [
|
||||
CanActivateRouteSearch,
|
||||
CanActivateRouteParams,
|
||||
CanActivateStations,
|
||||
CanActivateDateParams,
|
||||
StationCodeValidationService,
|
||||
OnlineBoardDateValidationService,
|
||||
TimeRangeValidationService,
|
||||
OnlineBoardUrlParserService,
|
||||
{
|
||||
provide: AppSettingsService,
|
||||
useValue: appSettingsService,
|
||||
},
|
||||
{
|
||||
provide: OnlineBoardNavigationService,
|
||||
useValue: navigationService,
|
||||
},
|
||||
{
|
||||
provide: DictionariesService,
|
||||
useValue: dictionariesService,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
service = TestBed.inject(CanActivateRouteSearch);
|
||||
});
|
||||
|
||||
it('should return true for valid route params without time range', async () => {
|
||||
const params = 'MOW-LED-20220528';
|
||||
const result = await service.canActivate({
|
||||
params: {
|
||||
params,
|
||||
},
|
||||
} as any);
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should return true for valid route params with time range', async () => {
|
||||
const params = 'MOW-LED-20220528-06002200';
|
||||
const result = await service.canActivate({
|
||||
params: {
|
||||
params,
|
||||
},
|
||||
} as any);
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should redirect to not found because departure city is invalid', async () => {
|
||||
const params = 'ABA-LED-20220528';
|
||||
const result = await service.canActivate({
|
||||
params: {
|
||||
params,
|
||||
},
|
||||
} as any);
|
||||
|
||||
expect(result).not.toBe(true);
|
||||
expect(toNotFoundMock).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should redirect to not found because arrival city is invalid', async () => {
|
||||
const params = 'MOW-ABA-20220528';
|
||||
const result = await service.canActivate({
|
||||
params: {
|
||||
params,
|
||||
},
|
||||
} as any);
|
||||
|
||||
expect(result).not.toBe(true);
|
||||
expect(toNotFoundMock).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should redirect to start page because date is invalid', async () => {
|
||||
const params = 'MOW-LED-20220521';
|
||||
const result = await service.canActivate({
|
||||
params: {
|
||||
params,
|
||||
},
|
||||
} as any);
|
||||
|
||||
expect(result).not.toBe(true);
|
||||
expect(toStartPageMock).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should redirect to start page because time range is invalid', async () => {
|
||||
const params = 'MOW-LED-20220528-22000600';
|
||||
const result = await service.canActivate({
|
||||
params: {
|
||||
params,
|
||||
},
|
||||
} as any);
|
||||
|
||||
expect(result).not.toBe(true);
|
||||
expect(toStartPageMock).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,20 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { ActivatedRouteSnapshot, CanActivate } from '@angular/router';
|
||||
import { OnlineBoardUrlParserService } from '@online-board/services/url/url-parser.service';
|
||||
import { CanActivateRouteParams } from './utils/can-activate-route-params';
|
||||
|
||||
@Injectable()
|
||||
export class CanActivateRouteSearch implements CanActivate {
|
||||
constructor(
|
||||
private urlService: OnlineBoardUrlParserService,
|
||||
private routeParamsService: CanActivateRouteParams,
|
||||
) {}
|
||||
|
||||
async canActivate(route: ActivatedRouteSnapshot) {
|
||||
const urlParams = this.urlService.parseRouteSearchUrlParams(
|
||||
route.params.params,
|
||||
);
|
||||
|
||||
return this.routeParamsService.canActivate(urlParams);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
import { CanActivateDateParams } from '@online-board/guards/utils/can-activate-date-params';
|
||||
import { OnlineBoardDateValidationService } from '@online-board/services/date-validation.service';
|
||||
import { OnlineBoardNavigationService } from '@online-board/services/navigation.service';
|
||||
import { OnlineBoardUrlParserService } from '@online-board/services/url/url-parser.service';
|
||||
import { AppSettingsService } from '@shared/services';
|
||||
import { TimeRangeValidationService } from '@shared/services/validators/time-range.service';
|
||||
|
||||
describe('CanActivateDateParams', () => {
|
||||
let service: CanActivateDateParams;
|
||||
const boardMinDate = new Date(2022, 4, 23);
|
||||
const boardMaxDate = new Date(2022, 4, 29);
|
||||
const toStartPageMock = jasmine.createSpy('toStartPage');
|
||||
const navigationService: Partial<OnlineBoardNavigationService> = {
|
||||
toStartPage: toStartPageMock,
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
providers: [
|
||||
CanActivateDateParams,
|
||||
OnlineBoardDateValidationService,
|
||||
TimeRangeValidationService,
|
||||
OnlineBoardUrlParserService,
|
||||
{
|
||||
provide: AppSettingsService,
|
||||
useValue: {
|
||||
getSettings: () => {
|
||||
return Promise.resolve({
|
||||
boardMinDate,
|
||||
boardMaxDate,
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: OnlineBoardNavigationService,
|
||||
useValue: navigationService,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
toStartPageMock.calls.reset();
|
||||
service = TestBed.inject(CanActivateDateParams);
|
||||
});
|
||||
|
||||
it('can activate with valid date', async () => {
|
||||
const validDate = new Date(2022, 4, 25);
|
||||
const result = await service.canActivate({
|
||||
date: validDate,
|
||||
});
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('can activate with valid date and time range', async () => {
|
||||
const validDate = new Date(2022, 4, 25);
|
||||
const result = await service.canActivate({
|
||||
date: validDate,
|
||||
timeFrom: '0600',
|
||||
timeTo: '2200',
|
||||
});
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('redirect to start page because of invalid date', async () => {
|
||||
const validDate = new Date(2022, 4, 21);
|
||||
const result = await service.canActivate({
|
||||
date: validDate,
|
||||
});
|
||||
|
||||
expect(result).not.toBe(true);
|
||||
expect(toStartPageMock).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('redirect to start page because of invalid time range', async () => {
|
||||
const validDate = new Date(2022, 4, 25);
|
||||
const result = await service.canActivate({
|
||||
date: validDate,
|
||||
timeFrom: '1200',
|
||||
timeTo: '1000',
|
||||
});
|
||||
|
||||
expect(result).not.toBe(true);
|
||||
expect(toStartPageMock).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,39 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { OnlineBoardDateValidationService } from '@online-board/services/date-validation.service';
|
||||
import { OnlineBoardNavigationService } from '@online-board/services/navigation.service';
|
||||
import { IOnlineBoardUrlDateParams } from '@online-board/services/url/types';
|
||||
import { TimeRangeValidationService } from '@shared/services/validators/time-range.service';
|
||||
import { CanActivateRedirrect } from './can-activate-redirrect-to-not-found';
|
||||
|
||||
@Injectable()
|
||||
export class CanActivateDateParams {
|
||||
constructor(
|
||||
private dateValidator: OnlineBoardDateValidationService,
|
||||
private timeRangeValidator: TimeRangeValidationService,
|
||||
private navigationService: OnlineBoardNavigationService,
|
||||
private canRedirrect: CanActivateRedirrect,
|
||||
) {}
|
||||
|
||||
public async canActivate(params: IOnlineBoardUrlDateParams) {
|
||||
const validationResult = await this.validateDateParams(params);
|
||||
|
||||
const isTimeRangeValid = this.canRedirrect.validateTimeRange(params.timeFrom, params.timeTo);
|
||||
|
||||
if (!isTimeRangeValid || !params.date) {
|
||||
return this.navigationService.toNotFound();
|
||||
}
|
||||
|
||||
return validationResult ? true : this.navigationService.toStartPage();
|
||||
}
|
||||
|
||||
public async validateDateParams(params: IOnlineBoardUrlDateParams) {
|
||||
const isDateValid = await this.dateValidator.isDateValid(params.date);
|
||||
|
||||
const isTimeRangeValid = this.timeRangeValidator.isTimeRangeValid(
|
||||
params.timeFrom,
|
||||
params.timeTo,
|
||||
);
|
||||
|
||||
return !(!isDateValid || !isTimeRangeValid);
|
||||
}
|
||||
}
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
|
||||
@Injectable()
|
||||
export class CanActivateRedirrect {
|
||||
public validateStation(station: string) {
|
||||
if (!station) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const pattern: RegExp = /\w{3}/g;
|
||||
let match = station.match(pattern);
|
||||
|
||||
return match && match[0] == station;
|
||||
}
|
||||
|
||||
public validateTimeRange(timeFrom: string, timeTo: string) {
|
||||
const pattern: RegExp = /\d{4}/g;
|
||||
|
||||
if (!timeFrom && !timeTo) {
|
||||
return true;
|
||||
}
|
||||
|
||||
let matchFrom = timeFrom.match(pattern);
|
||||
let matchTo = timeTo.match(pattern);
|
||||
|
||||
return matchFrom && matchFrom[0] == timeFrom && matchTo && matchTo[0] == timeTo;
|
||||
}
|
||||
|
||||
public validateFlightNumber(number: string) {
|
||||
const pattern: RegExp = /\d{4}/g;
|
||||
let match = number.match(pattern);
|
||||
|
||||
return match && match[0] == number
|
||||
}
|
||||
}
|
||||
+218
@@ -0,0 +1,218 @@
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
import { DictionariesService } from '@modules/components/page-filters/services/dictionaries-service';
|
||||
import { getDictionariesServiceMock } from '@modules/components/page-filters/services/dictionaries-service.mock';
|
||||
import { CanActivateDateParams } from '@online-board/guards/utils/can-activate-date-params';
|
||||
import { CanActivateRouteParams } from '@online-board/guards/utils/can-activate-route-params';
|
||||
import { CanActivateStations } from '@online-board/guards/utils/can-activate-stations';
|
||||
import { OnlineBoardDateValidationService } from '@online-board/services/date-validation.service';
|
||||
import { OnlineBoardNavigationService } from '@online-board/services/navigation.service';
|
||||
import { IOnlineBoardRoutePageUrlParams } from '@online-board/services/url/types';
|
||||
import { OnlineBoardUrlParserService } from '@online-board/services/url/url-parser.service';
|
||||
import { AppSettingsService } from '@shared/services';
|
||||
import { StationCodeValidationService } from '@shared/services/validators/station-code.service';
|
||||
import { TimeRangeValidationService } from '@shared/services/validators/time-range.service';
|
||||
|
||||
describe('CanActivateRouteParams', () => {
|
||||
let service: CanActivateRouteParams;
|
||||
|
||||
const boardMinDate = new Date(2022, 4, 23);
|
||||
const boardMaxDate = new Date(2022, 4, 29);
|
||||
const appSettingsService: Partial<AppSettingsService> = {
|
||||
getSettings: () => {
|
||||
return Promise.resolve({
|
||||
boardMinDate,
|
||||
boardMaxDate,
|
||||
}) as any;
|
||||
},
|
||||
};
|
||||
|
||||
const toNotFoundMock = jasmine.createSpy('toNotFound');
|
||||
const toStartPageMock = jasmine.createSpy('toStartPage');
|
||||
const navigationService: Partial<OnlineBoardNavigationService> = {
|
||||
toNotFound: toNotFoundMock,
|
||||
toStartPage: toStartPageMock,
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
providers: [
|
||||
CanActivateRouteParams,
|
||||
CanActivateStations,
|
||||
CanActivateDateParams,
|
||||
StationCodeValidationService,
|
||||
OnlineBoardDateValidationService,
|
||||
TimeRangeValidationService,
|
||||
OnlineBoardUrlParserService,
|
||||
{
|
||||
provide: AppSettingsService,
|
||||
useValue: appSettingsService,
|
||||
},
|
||||
{
|
||||
provide: OnlineBoardNavigationService,
|
||||
useValue: navigationService,
|
||||
},
|
||||
{
|
||||
provide: DictionariesService,
|
||||
useValue: getDictionariesServiceMock(),
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
toStartPageMock.calls.reset();
|
||||
toNotFoundMock.calls.reset();
|
||||
service = TestBed.inject(CanActivateRouteParams);
|
||||
});
|
||||
|
||||
it('should return true for valid arrival params without time range', async () => {
|
||||
const params: IOnlineBoardRoutePageUrlParams = {
|
||||
arrival: 'MOW',
|
||||
date: new Date(2022, 4, 25),
|
||||
};
|
||||
const activationResult = await service.canActivate(params);
|
||||
expect(activationResult).toBe(true);
|
||||
|
||||
const validationResult = await service.validateRouteParams(params);
|
||||
expect(validationResult).toBe(true);
|
||||
});
|
||||
|
||||
it('should return true for valid arrival params with time range', async () => {
|
||||
const params: IOnlineBoardRoutePageUrlParams = {
|
||||
arrival: 'MOW',
|
||||
date: new Date(2022, 4, 25),
|
||||
timeFrom: '0600',
|
||||
timeTo: '2200',
|
||||
};
|
||||
|
||||
const activationResult = await service.canActivate(params);
|
||||
expect(activationResult).toBe(true);
|
||||
|
||||
const validationResult = await service.validateRouteParams(params);
|
||||
expect(validationResult).toBe(true);
|
||||
});
|
||||
|
||||
it('should return true for valid departure params without time range', async () => {
|
||||
const params: IOnlineBoardRoutePageUrlParams = {
|
||||
departure: 'MRS',
|
||||
date: new Date(2022, 4, 25),
|
||||
};
|
||||
|
||||
const activationResult = await service.canActivate(params);
|
||||
expect(activationResult).toBe(true);
|
||||
|
||||
const validationResult = await service.validateRouteParams(params);
|
||||
expect(validationResult).toBe(true);
|
||||
});
|
||||
|
||||
it('should return true for valid departure params with time range', async () => {
|
||||
const params: IOnlineBoardRoutePageUrlParams = {
|
||||
departure: 'MRS',
|
||||
date: new Date(2022, 4, 25),
|
||||
timeFrom: '0600',
|
||||
timeTo: '2200',
|
||||
};
|
||||
|
||||
const activationResult = await service.canActivate(params);
|
||||
expect(activationResult).toBe(true);
|
||||
|
||||
const validationResult = await service.validateRouteParams(params);
|
||||
expect(validationResult).toBe(true);
|
||||
});
|
||||
|
||||
it('should return true for valid route params without time range', async () => {
|
||||
const params: IOnlineBoardRoutePageUrlParams = {
|
||||
departure: 'MRS',
|
||||
arrival: 'MOW',
|
||||
date: new Date(2022, 4, 25),
|
||||
};
|
||||
|
||||
const activationResult = await service.canActivate(params);
|
||||
expect(activationResult).toBe(true);
|
||||
|
||||
const validationResult = await service.validateRouteParams(params);
|
||||
expect(validationResult).toBe(true);
|
||||
});
|
||||
|
||||
it('should return true for valid route params with time range', async () => {
|
||||
const params: IOnlineBoardRoutePageUrlParams = {
|
||||
departure: 'MRS',
|
||||
arrival: 'MOW',
|
||||
date: new Date(2022, 4, 25),
|
||||
timeFrom: '0600',
|
||||
timeTo: '2200',
|
||||
};
|
||||
|
||||
const activationResult = await service.canActivate(params);
|
||||
expect(activationResult).toBe(true);
|
||||
|
||||
const validationResult = await service.validateRouteParams(params);
|
||||
expect(validationResult).toBe(true);
|
||||
});
|
||||
|
||||
it('should not return true because date is invalid', async () => {
|
||||
const params: IOnlineBoardRoutePageUrlParams = {
|
||||
departure: 'MRS',
|
||||
arrival: 'MOW',
|
||||
date: new Date(2022, 4, 21),
|
||||
timeFrom: '0600',
|
||||
timeTo: '2200',
|
||||
};
|
||||
|
||||
const activationResult = await service.canActivate(params);
|
||||
expect(activationResult).not.toBe(true);
|
||||
expect(toStartPageMock).toHaveBeenCalled();
|
||||
|
||||
const validationResult = await service.validateRouteParams(params);
|
||||
expect(validationResult).not.toBe(true);
|
||||
});
|
||||
|
||||
it('should not return true because time range is invalid', async () => {
|
||||
const params: IOnlineBoardRoutePageUrlParams = {
|
||||
departure: 'MRS',
|
||||
arrival: 'MOW',
|
||||
date: new Date(2022, 4, 25),
|
||||
timeFrom: '0600',
|
||||
timeTo: '0300',
|
||||
};
|
||||
|
||||
const activationResult = await service.canActivate(params);
|
||||
expect(activationResult).not.toBe(true);
|
||||
expect(toStartPageMock).toHaveBeenCalled();
|
||||
|
||||
const validationResult = await service.validateRouteParams(params);
|
||||
expect(validationResult).not.toBe(true);
|
||||
});
|
||||
|
||||
it('should not return true because departure is invalid', async () => {
|
||||
const params: IOnlineBoardRoutePageUrlParams = {
|
||||
departure: 'ABA',
|
||||
arrival: 'MOW',
|
||||
date: new Date(2022, 4, 25),
|
||||
timeFrom: '0600',
|
||||
timeTo: '0300',
|
||||
};
|
||||
|
||||
const activationResult = await service.canActivate(params);
|
||||
expect(activationResult).not.toBe(true);
|
||||
expect(toNotFoundMock).toHaveBeenCalled();
|
||||
|
||||
const validationResult = await service.validateRouteParams(params);
|
||||
expect(validationResult).not.toBe(true);
|
||||
});
|
||||
|
||||
it('should not return true because arrival is invalid', async () => {
|
||||
const params: IOnlineBoardRoutePageUrlParams = {
|
||||
departure: 'MRS',
|
||||
arrival: 'ABA',
|
||||
date: new Date(2022, 4, 25),
|
||||
timeFrom: '0600',
|
||||
timeTo: '0300',
|
||||
};
|
||||
|
||||
const result = await service.canActivate(params);
|
||||
expect(result).not.toBe(true);
|
||||
expect(toNotFoundMock).toHaveBeenCalled();
|
||||
|
||||
const validationResult = await service.validateRouteParams(params);
|
||||
expect(validationResult).not.toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,56 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { IOnlineBoardRoutePageUrlParams } from '@online-board/services/url/types';
|
||||
import { CanActivateDateParams } from './can-activate-date-params';
|
||||
import { CanActivateStations } from './can-activate-stations';
|
||||
import { CanActivateRedirrect } from './can-activate-redirrect-to-not-found';
|
||||
import { OnlineBoardNavigationService } from '../../services/navigation.service';
|
||||
|
||||
@Injectable()
|
||||
export class CanActivateRouteParams {
|
||||
constructor(
|
||||
private stationsService: CanActivateStations,
|
||||
private dateParamsService: CanActivateDateParams,
|
||||
private canRedirrect: CanActivateRedirrect,
|
||||
private navigationService: OnlineBoardNavigationService,
|
||||
) {}
|
||||
|
||||
public async canActivate(params: IOnlineBoardRoutePageUrlParams) {
|
||||
|
||||
if (!this.canRedirrect.validateStation(params.arrival) || !this.canRedirrect.validateStation(params.departure)) {
|
||||
return this.navigationService.toNotFound();
|
||||
}
|
||||
|
||||
const canActivateStations = await this.stationsService.canActivate(
|
||||
params,
|
||||
);
|
||||
|
||||
if (canActivateStations !== true) {
|
||||
return canActivateStations;
|
||||
}
|
||||
|
||||
if (!params.date || !this.canRedirrect.validateTimeRange(params.timeFrom, params.timeTo)) {
|
||||
return this.navigationService.toNotFound();
|
||||
}
|
||||
|
||||
const canActivateDateParams = await this.dateParamsService.canActivate(
|
||||
params,
|
||||
);
|
||||
|
||||
if (canActivateDateParams !== true) {
|
||||
return canActivateDateParams;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public async validateRouteParams(params: IOnlineBoardRoutePageUrlParams) {
|
||||
const canActivateStations = await this.stationsService.validateStations(
|
||||
params,
|
||||
);
|
||||
|
||||
const canActivateDateParams =
|
||||
await this.dateParamsService.validateDateParams(params);
|
||||
|
||||
return canActivateDateParams && canActivateStations;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
import { DictionariesService } from '@modules/components/page-filters/services/dictionaries-service';
|
||||
import { getDictionariesServiceMock } from '@modules/components/page-filters/services/dictionaries-service.mock';
|
||||
import { CanActivateStations } from '@online-board/guards/utils/can-activate-stations';
|
||||
import { OnlineBoardNavigationService } from '@online-board/services/navigation.service';
|
||||
import { StationCodeValidationService } from '@shared/services/validators/station-code.service';
|
||||
|
||||
describe('CanActivateStations', () => {
|
||||
let service: CanActivateStations;
|
||||
const toNotFoundMock = jasmine.createSpy('toNotFound');
|
||||
const navigationService: Partial<OnlineBoardNavigationService> = {
|
||||
toNotFound: toNotFoundMock,
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
providers: [
|
||||
CanActivateStations,
|
||||
StationCodeValidationService,
|
||||
{
|
||||
provide: DictionariesService,
|
||||
useValue: getDictionariesServiceMock(),
|
||||
},
|
||||
{
|
||||
provide: OnlineBoardNavigationService,
|
||||
useValue: navigationService,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
toNotFoundMock.calls.reset();
|
||||
service = TestBed.inject(CanActivateStations);
|
||||
});
|
||||
|
||||
it('can activate arrival station', async () => {
|
||||
const result = await service.canActivate({
|
||||
arrival: 'MOW',
|
||||
});
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('can activate departure station', async () => {
|
||||
const result = await service.canActivate({
|
||||
departure: 'MRS',
|
||||
});
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('can activate both stations', async () => {
|
||||
const result = await service.canActivate({
|
||||
arrival: 'MOW',
|
||||
departure: 'MRS',
|
||||
});
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('redirect to not found because arrival is not valid', async () => {
|
||||
const result = await service.canActivate({
|
||||
arrival: 'ABA',
|
||||
departure: 'LED',
|
||||
});
|
||||
|
||||
expect(result).not.toBe(true);
|
||||
expect(toNotFoundMock).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('redirect to not found because departure is not valid', async () => {
|
||||
const result = await service.canActivate({
|
||||
arrival: 'MOW',
|
||||
departure: 'ABA',
|
||||
});
|
||||
|
||||
expect(result).not.toBe(true);
|
||||
expect(toNotFoundMock).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('redirect to not found because both stations are not valid', async () => {
|
||||
const result = await service.canActivate({
|
||||
arrival: 'KZN',
|
||||
departure: 'ABA',
|
||||
});
|
||||
|
||||
expect(result).not.toBe(true);
|
||||
expect(toNotFoundMock).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should redirect to not found because neither departure nor arrival were provided', async () => {
|
||||
const result = await service.canActivate({} as any);
|
||||
|
||||
expect(result).not.toBe(true);
|
||||
expect(toNotFoundMock).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,48 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { StationCodeValidationService } from '@shared/services/validators/station-code.service';
|
||||
import { OnlineBoardNavigationService } from '@online-board/services/navigation.service';
|
||||
import { IOnlineBoardRoutePageUrlParams } from '@online-board/services/url/types';
|
||||
import { CanActivate } from '@angular/router';
|
||||
import { CanActivateRedirrect } from './can-activate-redirrect-to-not-found';
|
||||
|
||||
type IStations = Pick<IOnlineBoardRoutePageUrlParams, 'arrival' | 'departure'>;
|
||||
|
||||
@Injectable()
|
||||
export class CanActivateStations {
|
||||
constructor(
|
||||
private stationValidator: StationCodeValidationService,
|
||||
private navigationService: OnlineBoardNavigationService,
|
||||
private canRedirrect: CanActivateRedirrect,
|
||||
) {}
|
||||
|
||||
public async canActivate(stations: IStations) {
|
||||
let validationResult = await this.validateStations(stations);
|
||||
|
||||
validationResult = validationResult && this.canRedirrect.validateStation(stations.departure) && this.canRedirrect.validateStation(stations.arrival);
|
||||
|
||||
return validationResult ? true : this.navigationService.toNotFound();
|
||||
}
|
||||
|
||||
public async validateStations(stations: IStations) {
|
||||
if (!stations.arrival && !stations.departure) {
|
||||
return false;
|
||||
}
|
||||
|
||||
let isArrivalValid = true;
|
||||
let isDepartureValid = true;
|
||||
|
||||
if (stations.arrival) {
|
||||
isArrivalValid = await this.stationValidator.isStationCodeValid(
|
||||
stations.arrival,
|
||||
);
|
||||
}
|
||||
|
||||
if (stations.departure) {
|
||||
isDepartureValid = await this.stationValidator.isStationCodeValid(
|
||||
stations.departure,
|
||||
);
|
||||
}
|
||||
|
||||
return !(!isArrivalValid || !isDepartureValid);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { RouterModule, Routes } from '@angular/router';
|
||||
import { CanActivateArrivalSearch } from './guards/can-activate-arrival-search';
|
||||
import { CanActivateDepartureSearch } from './guards/can-activate-departure-search';
|
||||
import { CanActivateOnlineBoardDetails } from './guards/can-activate-details';
|
||||
import { CanActivateRouteSearch } from './guards/can-activate-route-search';
|
||||
import { CanActivateFlightNumberSearch } from './guards/can-activate-flight-number-search';
|
||||
import { ArrivalSearchPageComponent } from './pages/search/arrival/arrival-search-page.component';
|
||||
import { DepartureSearchPageComponent } from './pages/search/departure/departure-search-page.component';
|
||||
import { OnlineBoardFlightDetailsPageComponent } from './pages/flight-details/flight-details-page.component';
|
||||
import { FlightNumberSearchPageComponent } from './pages/search/flight-number-search/flight-number-search-page.component';
|
||||
import { RouteSearchPageComponent } from './pages/search/route-search/route-search-page.component';
|
||||
import { OnlineBoardStartPageComponent } from './pages/start/online-board-start-page.component';
|
||||
import { OnlineBoardDetailsBreadCrumbsResolver } from './resolvers/bread-crumbs/details-bread-crumbs.resolver';
|
||||
import { OnlineBoardRouteSearchBreadCrumbsResolver } from './resolvers/bread-crumbs/route-search-bread-crumbs.resolver';
|
||||
import { OnlineBoardArrivalUrlParamsResolver } from './resolvers/url-params/arrival-url-params.resolver';
|
||||
import { OnlineBoardDepartureUrlParamsResolver } from './resolvers/url-params/departure-url-params.resolver';
|
||||
import { OnlineBoardDetailsUrlParamsResolver } from './resolvers/url-params/details-url-params.resolver';
|
||||
import { OnlineBoardFlightNumberUrlParamsResolver } from './resolvers/url-params/flight-number-url-params.resolver';
|
||||
import { OnlineBoardRequestDataResolver } from './resolvers/request-data.resolver';
|
||||
import { OnlineBoardRouteUrlParamsResolver } from './resolvers/url-params/route-url-params.resolver';
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
path: 'flight/:params', // params are {flightNumber}-{flightDate}
|
||||
component: FlightNumberSearchPageComponent,
|
||||
canActivate: [CanActivateFlightNumberSearch],
|
||||
resolve: {
|
||||
urlParams: OnlineBoardFlightNumberUrlParamsResolver,
|
||||
breadcrumbs: OnlineBoardRouteSearchBreadCrumbsResolver,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'departure/:params', // params are {departure}-{flightDate}-{timeFrom}{timeTo}
|
||||
component: DepartureSearchPageComponent,
|
||||
canActivate: [CanActivateDepartureSearch],
|
||||
resolve: {
|
||||
urlParams: OnlineBoardDepartureUrlParamsResolver,
|
||||
breadcrumbs: OnlineBoardRouteSearchBreadCrumbsResolver,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'arrival/:params', // params are {arrival}-{flightDate}-{timeFrom}{timeTo}
|
||||
component: ArrivalSearchPageComponent,
|
||||
canActivate: [CanActivateArrivalSearch],
|
||||
resolve: {
|
||||
urlParams: OnlineBoardArrivalUrlParamsResolver,
|
||||
breadcrumbs: OnlineBoardRouteSearchBreadCrumbsResolver,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'route/:params', // params are {departure}-{arrival}-{flightDate}-{timeFrom}{timeTo}
|
||||
component: RouteSearchPageComponent,
|
||||
canActivate: [CanActivateRouteSearch],
|
||||
resolve: {
|
||||
urlParams: OnlineBoardRouteUrlParamsResolver,
|
||||
breadcrumbs: OnlineBoardRouteSearchBreadCrumbsResolver,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: ':params', // params are {flightNumber}-{flightDate}
|
||||
component: OnlineBoardFlightDetailsPageComponent,
|
||||
canActivate: [CanActivateOnlineBoardDetails],
|
||||
resolve: {
|
||||
urlParams: OnlineBoardDetailsUrlParamsResolver,
|
||||
requestData: OnlineBoardRequestDataResolver,
|
||||
breadcrumbs: OnlineBoardDetailsBreadCrumbsResolver,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '',
|
||||
component: OnlineBoardStartPageComponent,
|
||||
},
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
imports: [RouterModule.forChild(routes)],
|
||||
exports: [RouterModule],
|
||||
})
|
||||
export class OnlineBoardRoutingModule {}
|
||||
@@ -0,0 +1,141 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { NgModule } from '@angular/core';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { ComponentsModule } from '@components/components.module';
|
||||
import { FlightsModule } from '@modules/flights.module';
|
||||
import { SharedModule } from '@shared';
|
||||
import { ToolkitModule } from '@toolkit/toolkit.module';
|
||||
import { PopularRequestsModule } from '../popular-requests/popular-requests.module';
|
||||
import { OnlineBoardFilterModule } from './components/filter/online-board-filter.module';
|
||||
import { CanActivateArrivalSearch } from './guards/can-activate-arrival-search';
|
||||
import { CanActivateDepartureSearch } from './guards/can-activate-departure-search';
|
||||
import { CanActivateOnlineBoardDetails } from './guards/can-activate-details';
|
||||
import { CanActivateFlightNumberSearch } from './guards/can-activate-flight-number-search';
|
||||
import { CanActivateRouteSearch } from './guards/can-activate-route-search';
|
||||
import { CanActivateDateParams } from './guards/utils/can-activate-date-params';
|
||||
import { CanActivateRouteParams } from './guards/utils/can-activate-route-params';
|
||||
import { CanActivateStations } from './guards/utils/can-activate-stations';
|
||||
import { OnlineBoardRoutingModule } from './online-board-routing.module';
|
||||
import { CanActivateRedirrect } from './guards/utils/can-activate-redirrect-to-not-found';
|
||||
import { OnlineBoardFlightsMiniListComponent } from './pages/flight-details/components/flights-mini-list/online-board-flights-mini-list.component';
|
||||
import { OnlineBoardFlightDetailsMetaTagsComponent } from './pages/flight-details/components/meta-tags/flight-details-meta-tags.component';
|
||||
import { OnlineBoardFlightDetailsTitleComponent } from './pages/flight-details/components/title/flight-details-title.component';
|
||||
import { OnlineBoardDetailsViewComponent } from './pages/flight-details/components/view/online-board-details.component';
|
||||
import { OnlineBoardFlightDetailsPageComponent } from './pages/flight-details/flight-details-page.component';
|
||||
import { ArrivalSearchPageComponent } from './pages/search/arrival/arrival-search-page.component';
|
||||
import { OnlineBoardArrivalMetaTagsComponent } from './pages/search/arrival/components/meta-tags/online-board-arrival-meta-tags.component';
|
||||
import { OnlineBoardArrivalTitleComponent } from './pages/search/arrival/components/title/online-board-arrival-title.component';
|
||||
import { OnlineBoardSearchPageBaseComponent } from './pages/search/components/base/search-page-base.component';
|
||||
import { OnlineBoardSearchComponent } from './pages/search/components/view/online-board-search.component';
|
||||
import { OnlineBoardDepartureMetaTagsComponent } from './pages/search/departure/components/meta-tags/online-board-departure-meta-tags.component';
|
||||
import { OnlineBoardDepartureTitleComponent } from './pages/search/departure/components/title/online-board-departure-title.component';
|
||||
import { DepartureSearchPageComponent } from './pages/search/departure/departure-search-page.component';
|
||||
import { OnlineBoardFlightNumberMetaTagsComponent } from './pages/search/flight-number-search/components/meta-tags/online-board-flight-number-meta-tags.component';
|
||||
import { OnlineBoardFlightNumberTitleComponent } from './pages/search/flight-number-search/components/title/online-board-flight-number-title.component';
|
||||
import { FlightNumberSearchPageComponent } from './pages/search/flight-number-search/flight-number-search-page.component';
|
||||
import { OnlineBoardRouteMetaTagsComponent } from './pages/search/route-search/components/meta-tags/online-board-route-meta-tags.component';
|
||||
import { OnlineBoardRouteTitleComponent } from './pages/search/route-search/components/title/online-board-route-title.component';
|
||||
import { RouteSearchPageComponent } from './pages/search/route-search/route-search-page.component';
|
||||
import { OnlineBoardStartPageMetaTagsComponent } from './pages/start/components/meta-tags/online-board-start-page-meta-tags.component';
|
||||
import { OnlineBoardStartPageTitleComponent } from './pages/start/components/title/start-page-title.component';
|
||||
import { OnlineBoardStartPageComponent } from './pages/start/online-board-start-page.component';
|
||||
import { OnlineBoardDetailsBreadCrumbsResolver } from './resolvers/bread-crumbs/details-bread-crumbs.resolver';
|
||||
import { OnlineBoardRouteSearchBreadCrumbsResolver } from './resolvers/bread-crumbs/route-search-bread-crumbs.resolver';
|
||||
import { OnlineBoardRequestDataResolver } from './resolvers/request-data.resolver';
|
||||
import { OnlineBoardArrivalUrlParamsResolver } from './resolvers/url-params/arrival-url-params.resolver';
|
||||
import { OnlineBoardDepartureUrlParamsResolver } from './resolvers/url-params/departure-url-params.resolver';
|
||||
import { OnlineBoardDetailsUrlParamsResolver } from './resolvers/url-params/details-url-params.resolver';
|
||||
import { OnlineBoardFlightNumberUrlParamsResolver } from './resolvers/url-params/flight-number-url-params.resolver';
|
||||
import { OnlineBoardRouteUrlParamsResolver } from './resolvers/url-params/route-url-params.resolver';
|
||||
import { OnlineBoardApiService } from './services/api.service';
|
||||
import { OnlineBoardBreadCrumbService } from './services/bread-crumb.service';
|
||||
import { OnlineBoardDataSourceService } from './services/data-source.service';
|
||||
import { OnlineBoardDateValidationService } from './services/date-validation.service';
|
||||
import { OnlineBoardFilterService } from './services/filter/filter.service';
|
||||
import { OnlineBoardMetaTagsService } from './services/meta-tags.service';
|
||||
import { OnlineBoardNavigationService } from './services/navigation.service';
|
||||
import { OnlineBoardRequestBuilderService } from './services/request/request-builder.service';
|
||||
import { OnlineBoardRequestParserService } from './services/request/request-parser.service';
|
||||
import { OnlineBoardTitleService } from './services/title.service';
|
||||
import { FadeService } from './services/fade.service';
|
||||
|
||||
import {
|
||||
ONLINE_BOARD_URL_BASE,
|
||||
OnlineBoardUrlBuilderService,
|
||||
OnlineBoardUrlParserService,
|
||||
} from './services/url';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
OnlineBoardStartPageComponent,
|
||||
FlightNumberSearchPageComponent,
|
||||
DepartureSearchPageComponent,
|
||||
ArrivalSearchPageComponent,
|
||||
RouteSearchPageComponent,
|
||||
OnlineBoardFlightDetailsPageComponent,
|
||||
OnlineBoardArrivalMetaTagsComponent,
|
||||
OnlineBoardArrivalTitleComponent,
|
||||
OnlineBoardSearchComponent,
|
||||
OnlineBoardDepartureMetaTagsComponent,
|
||||
OnlineBoardDepartureTitleComponent,
|
||||
OnlineBoardRouteTitleComponent,
|
||||
OnlineBoardRouteMetaTagsComponent,
|
||||
OnlineBoardFlightNumberMetaTagsComponent,
|
||||
OnlineBoardFlightNumberTitleComponent,
|
||||
OnlineBoardDetailsViewComponent,
|
||||
OnlineBoardFlightDetailsTitleComponent,
|
||||
OnlineBoardFlightDetailsMetaTagsComponent,
|
||||
OnlineBoardStartPageMetaTagsComponent,
|
||||
OnlineBoardStartPageTitleComponent,
|
||||
OnlineBoardFlightsMiniListComponent,
|
||||
OnlineBoardSearchPageBaseComponent,
|
||||
],
|
||||
providers: [
|
||||
OnlineBoardApiService,
|
||||
OnlineBoardDataSourceService,
|
||||
OnlineBoardDateValidationService,
|
||||
CanActivateFlightNumberSearch,
|
||||
CanActivateRouteSearch,
|
||||
CanActivateArrivalSearch,
|
||||
CanActivateDepartureSearch,
|
||||
CanActivateStations,
|
||||
CanActivateRedirrect,
|
||||
CanActivateDateParams,
|
||||
CanActivateRouteParams,
|
||||
OnlineBoardNavigationService,
|
||||
OnlineBoardUrlBuilderService,
|
||||
OnlineBoardUrlParserService,
|
||||
OnlineBoardRequestBuilderService,
|
||||
OnlineBoardRequestParserService,
|
||||
OnlineBoardFilterService,
|
||||
OnlineBoardArrivalUrlParamsResolver,
|
||||
OnlineBoardDepartureUrlParamsResolver,
|
||||
OnlineBoardFlightNumberUrlParamsResolver,
|
||||
OnlineBoardRouteUrlParamsResolver,
|
||||
OnlineBoardTitleService,
|
||||
OnlineBoardMetaTagsService,
|
||||
OnlineBoardDetailsUrlParamsResolver,
|
||||
OnlineBoardRequestDataResolver,
|
||||
OnlineBoardBreadCrumbService,
|
||||
OnlineBoardRouteSearchBreadCrumbsResolver,
|
||||
OnlineBoardDetailsBreadCrumbsResolver,
|
||||
FadeService,
|
||||
CanActivateOnlineBoardDetails,
|
||||
{
|
||||
provide: ONLINE_BOARD_URL_BASE,
|
||||
useValue: 'onlineboard',
|
||||
},
|
||||
],
|
||||
imports: [
|
||||
CommonModule,
|
||||
OnlineBoardRoutingModule,
|
||||
FlightsModule,
|
||||
SharedModule,
|
||||
ToolkitModule,
|
||||
FormsModule,
|
||||
ComponentsModule,
|
||||
OnlineBoardFilterModule,
|
||||
PopularRequestsModule,
|
||||
],
|
||||
})
|
||||
export class OnlineBoardModule {}
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
<section
|
||||
class="frame frame-left-panel single-flights-details afl-scrollbar"
|
||||
scrollContainer
|
||||
>
|
||||
<flights-details-list-flight
|
||||
*ngFor="let $flight of flights"
|
||||
[flightDate]="$flight.dateToSearchBy"
|
||||
[isBoard]="true"
|
||||
[isSchedule]="false"
|
||||
[scrollTo]="isFlightSelected($flight)"
|
||||
[flight]="$flight"
|
||||
(click)="openFlight($flight)"
|
||||
[ngClass]="{ selected: isFlightSelected($flight) }"
|
||||
>
|
||||
</flights-details-list-flight>
|
||||
</section>
|
||||
+61
@@ -0,0 +1,61 @@
|
||||
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { OnlineBoardFlightsMiniListComponent } from '@online-board/pages/flight-details/components/flights-mini-list/online-board-flights-mini-list.component';
|
||||
|
||||
describe('OnlineBoardFlightsMiniListComponent', () => {
|
||||
let fixture: ComponentFixture<OnlineBoardFlightsMiniListComponent>;
|
||||
let component: OnlineBoardFlightsMiniListComponent;
|
||||
|
||||
const emitMock = jasmine.createSpy('emit');
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [OnlineBoardFlightsMiniListComponent],
|
||||
schemas: [NO_ERRORS_SCHEMA],
|
||||
});
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(OnlineBoardFlightsMiniListComponent);
|
||||
component = fixture.componentInstance;
|
||||
|
||||
(component.open as any) = {
|
||||
emit: emitMock,
|
||||
};
|
||||
emitMock.calls.reset();
|
||||
});
|
||||
|
||||
it('should not emit open event because no flight was provided', () => {
|
||||
component.openFlight();
|
||||
expect(emitMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should emit open event', () => {
|
||||
const flight = { pId: '111' } as any;
|
||||
component.openFlight(flight);
|
||||
expect(emitMock).toHaveBeenCalledWith(flight);
|
||||
});
|
||||
|
||||
it('should return false because no flights were provided', () => {
|
||||
component.flight = { pId: '111' } as any;
|
||||
|
||||
expect(component.isFlightSelected({ pId: '111' } as any)).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false because flights array contains only one item', () => {
|
||||
const flight = { pId: '111' } as any;
|
||||
component.flightsLegacy = [flight];
|
||||
|
||||
expect(component.isFlightSelected(flight)).toBe(false);
|
||||
});
|
||||
|
||||
it('should return true if flight is selected', () => {
|
||||
const firstFlight = { pId: '111' } as any;
|
||||
const secondFlight = { pId: '222' } as any;
|
||||
component.flightsLegacy = [firstFlight, secondFlight] as any;
|
||||
component.flight = firstFlight;
|
||||
|
||||
expect(component.isFlightSelected(firstFlight)).toBe(true);
|
||||
expect(component.isFlightSelected(secondFlight)).toBe(false);
|
||||
});
|
||||
});
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
EventEmitter,
|
||||
Input,
|
||||
Output,
|
||||
} from '@angular/core';
|
||||
import { FlightModel } from '@shared/models-legacy';
|
||||
import { compareFlightsByPId } from '@utils/flight/p-id';
|
||||
|
||||
@Component({
|
||||
selector: 'online-board-flights-mini-list',
|
||||
templateUrl: './online-board-flights-mini-list.component.html',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class OnlineBoardFlightsMiniListComponent {
|
||||
@Input() flight: FlightModel;
|
||||
@Input('flights') flightsLegacy: FlightModel[] = [];
|
||||
|
||||
@Output() open = new EventEmitter<FlightModel>();
|
||||
|
||||
public openFlight(flight?: FlightModel) {
|
||||
if (!flight) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.open.emit(flight);
|
||||
}
|
||||
|
||||
public isFlightSelected(flight: FlightModel) {
|
||||
if (this.flights.length === 1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return compareFlightsByPId(this.flight as any, flight as any);
|
||||
}
|
||||
|
||||
get flights() {
|
||||
return this.flightsLegacy.length ? this.flightsLegacy : [this.flight];
|
||||
}
|
||||
}
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
<meta-tags
|
||||
[title]="title"
|
||||
[description]="description"
|
||||
[canonical]="'fullURL'"
|
||||
></meta-tags>
|
||||
+111
@@ -0,0 +1,111 @@
|
||||
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { DictionariesService } from '@modules/components/page-filters/services/dictionaries-service';
|
||||
import { TranslatePipe } from '@ngx-translate/core';
|
||||
import { OnlineBoardFlightDetailsMetaTagsComponent } from '@online-board/pages/flight-details/components/meta-tags/flight-details-meta-tags.component';
|
||||
import { OnlineBoardUrlParserService } from '@online-board/services/url';
|
||||
import { DateTransformerService } from '@shared/services/date/date-transformer.service';
|
||||
import { BehaviorSubject } from 'rxjs';
|
||||
|
||||
describe('OnlineBoardFlightDetailsMetaTagsComponent', () => {
|
||||
let fixture: ComponentFixture<OnlineBoardFlightDetailsMetaTagsComponent>;
|
||||
let component: OnlineBoardFlightDetailsMetaTagsComponent;
|
||||
|
||||
const date = new Date(2022, 4, 28);
|
||||
const transformedDate = '2022-05-28';
|
||||
const routeParams = {
|
||||
params: 'SU0001-20220528',
|
||||
};
|
||||
const title = 'flight details meta tag title';
|
||||
const description = 'flight details meta tag description';
|
||||
|
||||
const getCityOrAirportMock = jasmine.createSpy('getCityOrAirport');
|
||||
|
||||
const transformMock = jasmine
|
||||
.createSpy('transform')
|
||||
.withArgs('SEO.BOARD.FLIGHT-DETAILS.TITLE', {
|
||||
flightNumber: 'SU 0001',
|
||||
date: transformedDate,
|
||||
})
|
||||
.and.returnValue(title)
|
||||
.withArgs('SEO.BOARD.FLIGHT-DETAILS.DESCRIPTION', {
|
||||
flightNumber: 'SU 0001',
|
||||
date: transformedDate,
|
||||
})
|
||||
.and.returnValue(description);
|
||||
|
||||
const parseFlightIdMock = jasmine
|
||||
.createSpy('parseFlightId')
|
||||
.withArgs(routeParams.params)
|
||||
.and.returnValue({
|
||||
flightNumber: '0001',
|
||||
carrier: 'SU',
|
||||
date,
|
||||
});
|
||||
|
||||
const toStringMock = jasmine
|
||||
.createSpy('toString')
|
||||
.withArgs(date)
|
||||
.and.returnValue(transformedDate);
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [OnlineBoardFlightDetailsMetaTagsComponent],
|
||||
providers: [
|
||||
{
|
||||
provide: ActivatedRoute,
|
||||
useValue: {
|
||||
params: new BehaviorSubject(routeParams),
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: DictionariesService,
|
||||
useValue: {
|
||||
ready$: Promise.resolve(),
|
||||
getCityOrAirport: getCityOrAirportMock,
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: TranslatePipe,
|
||||
useValue: {
|
||||
transform: transformMock,
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: OnlineBoardUrlParserService,
|
||||
useValue: {
|
||||
parseFlightId: parseFlightIdMock,
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: DateTransformerService,
|
||||
useValue: {
|
||||
toString: toStringMock,
|
||||
},
|
||||
},
|
||||
],
|
||||
schemas: [NO_ERRORS_SCHEMA],
|
||||
}).compileComponents();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(
|
||||
OnlineBoardFlightDetailsMetaTagsComponent,
|
||||
);
|
||||
component = fixture.componentInstance;
|
||||
|
||||
getCityOrAirportMock.calls.reset();
|
||||
transformMock.calls.reset();
|
||||
parseFlightIdMock.calls.reset();
|
||||
toStringMock.calls.reset();
|
||||
});
|
||||
|
||||
it('should set title and description fields', async () => {
|
||||
component.ngOnInit();
|
||||
await Promise.resolve();
|
||||
|
||||
expect(component.title).toBe(title);
|
||||
expect(component.description).toBe(description);
|
||||
});
|
||||
});
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
import { Component, Input } from '@angular/core';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { MetaBaseComponent } from '@components/meta-base/meta-base.component';
|
||||
import { DictionariesService } from '@modules/components/page-filters/services/dictionaries-service';
|
||||
import { TranslatePipe } from '@ngx-translate/core';
|
||||
import { OnlineBoardUrlParserService } from '@online-board/services/url/url-parser.service';
|
||||
import { DateTransformerService } from '@shared/services/date/date-transformer.service';
|
||||
import { IFlight } from '@typings/flight/flight';
|
||||
|
||||
@Component({
|
||||
selector: 'online-board-flight-details-meta-tags',
|
||||
templateUrl: './flight-details-meta-tags.component.html',
|
||||
})
|
||||
export class OnlineBoardFlightDetailsMetaTagsComponent extends MetaBaseComponent {
|
||||
@Input() flight: IFlight;
|
||||
|
||||
constructor(
|
||||
protected route: ActivatedRoute,
|
||||
protected dictionaries: DictionariesService,
|
||||
private translatePipe: TranslatePipe,
|
||||
private urlService: OnlineBoardUrlParserService,
|
||||
private dateService: DateTransformerService,
|
||||
) {
|
||||
super(route, dictionaries);
|
||||
}
|
||||
|
||||
private translate(tag: 'TITLE' | 'DESCRIPTION', params: string) {
|
||||
const { flightNumber, carrier, date } =
|
||||
this.urlService.parseFlightId(params);
|
||||
|
||||
const transformedDate = this.dateService.toString(date);
|
||||
|
||||
return this.translatePipe.transform(`SEO.BOARD.FLIGHT-DETAILS.${tag}`, {
|
||||
flightNumber: `${carrier} ${flightNumber}`,
|
||||
date: transformedDate,
|
||||
});
|
||||
}
|
||||
|
||||
protected translateTitle(params: string): string {
|
||||
return this.translate('TITLE', params);
|
||||
}
|
||||
|
||||
protected translateDescription(params: string): string {
|
||||
return this.translate('DESCRIPTION', params);
|
||||
}
|
||||
}
|
||||
+1
@@ -0,0 +1 @@
|
||||
<aero-title [title]="title"></aero-title>
|
||||
+146
@@ -0,0 +1,146 @@
|
||||
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { TranslatePipe } from '@ngx-translate/core';
|
||||
import { OnlineBoardFlightDetailsTitleComponent } from '@online-board/pages/flight-details/components/title/flight-details-title.component';
|
||||
import { OnlineBoardUrlBuilderService } from '@online-board/services/url';
|
||||
import { RouteType } from '@typings/enums';
|
||||
import { IFlightId } from '@typings/flight/flight-id';
|
||||
|
||||
describe('OnlineBoardFlightDetailsTitleComponent', () => {
|
||||
let fixture: ComponentFixture<OnlineBoardFlightDetailsTitleComponent>;
|
||||
let component: OnlineBoardFlightDetailsTitleComponent;
|
||||
|
||||
const firstFlightId: IFlightId = {
|
||||
flightNumber: '0001',
|
||||
carrier: 'SU',
|
||||
date: '2022-05-28T00:00:00',
|
||||
};
|
||||
const secondFlightId: IFlightId = {
|
||||
flightNumber: '0002',
|
||||
carrier: 'SU',
|
||||
date: '2022-05-28T00:00:00',
|
||||
};
|
||||
|
||||
const titleBase = 'Информация о рейсе';
|
||||
const pluralTitleBase = 'Информация о рейсах';
|
||||
|
||||
const transformMock = jasmine
|
||||
.createSpy('transform')
|
||||
.withArgs('SHARED.FLIGHT-INFO')
|
||||
.and.returnValue(titleBase)
|
||||
.withArgs('SHARED.FLIGHTS-INFO')
|
||||
.and.returnValue(pluralTitleBase);
|
||||
|
||||
const formatFlightIdMock = jasmine
|
||||
.createSpy('formatFlightId')
|
||||
.withArgs(firstFlightId)
|
||||
.and.returnValue('SU 0001')
|
||||
.withArgs(secondFlightId)
|
||||
.and.returnValue('SU 0002');
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [OnlineBoardFlightDetailsTitleComponent],
|
||||
providers: [
|
||||
{
|
||||
provide: TranslatePipe,
|
||||
useValue: {
|
||||
transform: transformMock,
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: OnlineBoardUrlBuilderService,
|
||||
useValue: {
|
||||
formatFlightId: formatFlightIdMock,
|
||||
},
|
||||
},
|
||||
],
|
||||
schemas: [NO_ERRORS_SCHEMA],
|
||||
}).compileComponents();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(
|
||||
OnlineBoardFlightDetailsTitleComponent,
|
||||
);
|
||||
component = fixture.componentInstance;
|
||||
|
||||
transformMock.calls.reset();
|
||||
formatFlightIdMock.calls.reset();
|
||||
});
|
||||
|
||||
it('should set empty string to title if flight was not provided', () => {
|
||||
component.ngOnChanges();
|
||||
|
||||
expect(component.title).toBe('');
|
||||
});
|
||||
|
||||
it('should set title for non connecting flight', () => {
|
||||
component.flight = {
|
||||
routeType: RouteType.DIRECT,
|
||||
flightId: firstFlightId,
|
||||
leg: {
|
||||
departure: {
|
||||
latest: {
|
||||
city: 'Москва',
|
||||
},
|
||||
},
|
||||
arrival: {
|
||||
latest: {
|
||||
city: 'Самара',
|
||||
},
|
||||
},
|
||||
},
|
||||
} as any;
|
||||
|
||||
component.ngOnChanges();
|
||||
expect(component.title).toBe(
|
||||
'Информация о рейсе: SU 0001, Москва - Самара',
|
||||
);
|
||||
});
|
||||
|
||||
it('should set title for connecting flight', () => {
|
||||
component.flight = {
|
||||
routeType: RouteType.CONNECTING,
|
||||
flights: [
|
||||
{
|
||||
routeType: RouteType.DIRECT,
|
||||
flightId: firstFlightId,
|
||||
leg: {
|
||||
departure: {
|
||||
latest: {
|
||||
city: 'Москва',
|
||||
},
|
||||
},
|
||||
arrival: {
|
||||
latest: {
|
||||
city: 'Самара',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
routeType: RouteType.DIRECT,
|
||||
flightId: secondFlightId,
|
||||
leg: {
|
||||
departure: {
|
||||
latest: {
|
||||
city: 'Самара',
|
||||
},
|
||||
},
|
||||
arrival: {
|
||||
latest: {
|
||||
city: 'Казань',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
} as any;
|
||||
|
||||
component.ngOnChanges();
|
||||
expect(component.title).toBe(
|
||||
'Информация о рейсах: SU 0001, SU 0002, Москва - Казань',
|
||||
);
|
||||
});
|
||||
});
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { DetailsTitleBase } from '@components/title-base/details-title-base';
|
||||
import { TranslatePipe } from '@ngx-translate/core';
|
||||
import { OnlineBoardUrlBuilderService } from '@online-board/services/url/url-builder.service';
|
||||
|
||||
@Component({
|
||||
selector: 'online-board-flight-details-title',
|
||||
templateUrl: './flight-details-title.component.html',
|
||||
})
|
||||
export class OnlineBoardFlightDetailsTitleComponent extends DetailsTitleBase {
|
||||
constructor(
|
||||
protected translatePipe: TranslatePipe,
|
||||
protected urlService: OnlineBoardUrlBuilderService,
|
||||
) {
|
||||
super(translatePipe, urlService);
|
||||
}
|
||||
|
||||
protected get titleKey() {
|
||||
return 'SHARED.FLIGHT-INFO';
|
||||
}
|
||||
|
||||
protected get pluralTitleKey() {
|
||||
return 'SHARED.FLIGHTS-INFO';
|
||||
}
|
||||
|
||||
protected translateTitle(): string {
|
||||
return `${this.base}: ${this.flightInfo}, ${this.departure} - ${this.arrival}`;
|
||||
}
|
||||
|
||||
protected translatePluralTitle() {
|
||||
return this.translateTitle();
|
||||
}
|
||||
}
|
||||
+79
@@ -0,0 +1,79 @@
|
||||
<div *ngIf="flightLegacy">
|
||||
<page-layout scrollUp [withScrollUp]="false">
|
||||
<ng-container title>
|
||||
<ng-content select="[title]"></ng-content>
|
||||
</ng-container>
|
||||
<details-back
|
||||
header-left
|
||||
class="p-print-none"
|
||||
[viewType]="ViewType.Onlineboard"
|
||||
></details-back>
|
||||
<online-board-flights-mini-list
|
||||
content-left
|
||||
class="p-print-none"
|
||||
(open)="handleOpenEvent($any($event))"
|
||||
[flights]="flightsLegacy"
|
||||
[flight]="flightLegacy"
|
||||
></online-board-flights-mini-list>
|
||||
|
||||
<card [roundedTop]="true" sticky-content>
|
||||
<day-tabs
|
||||
[flight]="flightLegacy"
|
||||
class="board-details-day-selector"
|
||||
caption="SHARED.FLIGHT_DATE"
|
||||
[selectedDate]="flightLegacy.dateToSearchBy"
|
||||
[dateTo]="settings.boardMaxDate"
|
||||
[dateFrom]="settings.boardMinDate"
|
||||
[minDate]="settings.boardMinDate"
|
||||
[maxDate]="settings.boardMaxDate"
|
||||
(tabClick)="handleDateChanged($event)"
|
||||
></day-tabs>
|
||||
<board-details-header
|
||||
[flight]="flightLegacy"
|
||||
></board-details-header>
|
||||
</card>
|
||||
|
||||
<ng-container>
|
||||
<page-loader *ngIf="loading" [canCancel]="false"></page-loader>
|
||||
|
||||
<ng-container *ngIf="!loading">
|
||||
<div [ngSwitch]="flightLegacy.routeType">
|
||||
<div class="single-flight" *ngSwitchCase="RouteType.Direct">
|
||||
<flight-board-details
|
||||
[flight]="flightLegacy"
|
||||
[leg]="flightLegacy.leg"
|
||||
></flight-board-details>
|
||||
</div>
|
||||
<div
|
||||
class="multi-flight"
|
||||
*ngSwitchCase="RouteType.MultiLeg"
|
||||
>
|
||||
<flight-details-full-route
|
||||
[viewType]="ViewType.Onlineboard"
|
||||
[legs]="flightLegacy.legs"
|
||||
></flight-details-full-route>
|
||||
<!--aka Timeline by KS-->
|
||||
|
||||
<ng-container
|
||||
*ngFor="let leg of flightLegacy.legs; let i = index"
|
||||
>
|
||||
<flight-board-details
|
||||
[flight]="flightLegacy"
|
||||
[leg]="leg"
|
||||
></flight-board-details>
|
||||
<transfer
|
||||
[leg]="leg"
|
||||
[viewType]="ViewType.Onlineboard"
|
||||
*ngIf="leg.next"
|
||||
>
|
||||
</transfer>
|
||||
</ng-container>
|
||||
</div>
|
||||
</div>
|
||||
<section class="frame">
|
||||
<flight-schedule [flight]="flightLegacy"></flight-schedule>
|
||||
</section>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
</page-layout>
|
||||
</div>
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
@use 'src/styles/variables' as *;
|
||||
@use 'src/styles/screen';
|
||||
|
||||
:host {
|
||||
::ng-deep .flight-props-caption {
|
||||
width: 30%;
|
||||
}
|
||||
|
||||
// todo: remove after removing all usages of flight-props component
|
||||
::ng-deep .flight-details-section__caption {
|
||||
width: calc(30% - #{$space-xl}) !important;
|
||||
}
|
||||
|
||||
online-board-flights-mini-list {
|
||||
@include screen.smTablet {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.board-details-day-selector {
|
||||
@include screen.mobile {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
padding: $space-xl;
|
||||
}
|
||||
}
|
||||
}
|
||||
+100
@@ -0,0 +1,100 @@
|
||||
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { OnlineBoardDetailsViewComponent } from '@online-board/pages/flight-details/components/view/online-board-details.component';
|
||||
import DeprecatedFlightUtils from '@shared/models-legacy/get-flight.strategy';
|
||||
import { APP_SETTINGS } from '@shared/services';
|
||||
import { RouteType } from '@typings/enums';
|
||||
|
||||
describe('OnlineBoardDetailsViewComponent', () => {
|
||||
let fixture: ComponentFixture<OnlineBoardDetailsViewComponent>;
|
||||
let component: OnlineBoardDetailsViewComponent;
|
||||
let getFirstLegMock;
|
||||
|
||||
const emitOpenMock = jasmine.createSpy('open.emit');
|
||||
const emitDateChangeMock = jasmine.createSpy('dateChange.emit');
|
||||
|
||||
const flightLegacy: any = {
|
||||
routeType: RouteType.DIRECT,
|
||||
pId: '1',
|
||||
flightId: {
|
||||
flightNumber: '0001',
|
||||
carrier: 'SU',
|
||||
date: '2022-05-28T00:00:00',
|
||||
},
|
||||
leg: {
|
||||
departure: {
|
||||
latest: {
|
||||
city: 'Москва',
|
||||
},
|
||||
},
|
||||
arrival: {
|
||||
latest: {
|
||||
city: 'Самара',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [OnlineBoardDetailsViewComponent],
|
||||
providers: [
|
||||
{
|
||||
provide: APP_SETTINGS,
|
||||
useValue: {},
|
||||
},
|
||||
],
|
||||
schemas: [NO_ERRORS_SCHEMA],
|
||||
});
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(OnlineBoardDetailsViewComponent);
|
||||
component = fixture.componentInstance;
|
||||
|
||||
getFirstLegMock = spyOn(DeprecatedFlightUtils, 'getFirstLeg')
|
||||
.withArgs(flightLegacy)
|
||||
.and.returnValue(flightLegacy.leg as any);
|
||||
|
||||
emitOpenMock.calls.reset();
|
||||
emitDateChangeMock.calls.reset();
|
||||
|
||||
component.flightLegacy = flightLegacy;
|
||||
(component.open as any) = {
|
||||
emit: emitOpenMock,
|
||||
};
|
||||
(component.dateChange as any) = {
|
||||
emit: emitDateChangeMock,
|
||||
};
|
||||
});
|
||||
|
||||
it('should not call getFirstLeg from utils', () => {
|
||||
component.ngOnChanges({});
|
||||
expect(getFirstLegMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should set firstLeg', () => {
|
||||
component.ngOnChanges({
|
||||
flight: {} as any,
|
||||
});
|
||||
expect(component.firstLeg).toEqual(flightLegacy.leg);
|
||||
});
|
||||
|
||||
it('should not emit open event because pIds are equal', () => {
|
||||
component.handleOpenEvent({ pId: '1' } as any);
|
||||
expect(emitOpenMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should emit open event with provided flight', () => {
|
||||
const flight = { pId: '2' } as any;
|
||||
component.handleOpenEvent(flight);
|
||||
expect(emitOpenMock).toHaveBeenCalledWith(flight);
|
||||
});
|
||||
|
||||
it('should emit date', () => {
|
||||
const date = new Date(2022, 4, 28);
|
||||
component.handleDateChanged(date);
|
||||
|
||||
expect(emitDateChangeMock).toHaveBeenCalledWith(date);
|
||||
});
|
||||
});
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
import {
|
||||
Component,
|
||||
EventEmitter,
|
||||
Inject,
|
||||
Input,
|
||||
OnChanges,
|
||||
Output,
|
||||
SimpleChanges,
|
||||
} from '@angular/core';
|
||||
import { RouteTypeLegacy } from '@shared/enumerators';
|
||||
import { ViewType } from '@shared/enumerators/flight-request-type.enum';
|
||||
import { AppSettings, FlightModel, Leg } from '@shared/models-legacy';
|
||||
import DeprecatedFlightUtils from '@shared/models-legacy/get-flight.strategy';
|
||||
import { APP_SETTINGS } from '@shared/services';
|
||||
import { ISimpleFlight } from '@typings/flight/flight';
|
||||
|
||||
@Component({
|
||||
selector: 'online-board-details-view',
|
||||
templateUrl: './online-board-details.component.html',
|
||||
styleUrls: ['./online-board-details.component.scss'],
|
||||
})
|
||||
export class OnlineBoardDetailsViewComponent implements OnChanges {
|
||||
@Input() flightLegacy: FlightModel;
|
||||
@Input() flightsLegacy: FlightModel[];
|
||||
@Input() flight: ISimpleFlight;
|
||||
@Input() loading = false;
|
||||
@Input() searchDate: Date;
|
||||
|
||||
@Output() dateChange = new EventEmitter<Date>();
|
||||
@Output() open = new EventEmitter<ISimpleFlight>();
|
||||
|
||||
ViewType = ViewType;
|
||||
RouteType = RouteTypeLegacy;
|
||||
firstLeg: Leg;
|
||||
|
||||
constructor(@Inject(APP_SETTINGS) public settings: AppSettings) {}
|
||||
|
||||
ngOnChanges(changes: SimpleChanges) {
|
||||
if ('flight' in changes) {
|
||||
this.firstLeg = DeprecatedFlightUtils.getFirstLeg(
|
||||
this.flightLegacy,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
handleOpenEvent(flight: ISimpleFlight) {
|
||||
// Click on the currently opened flight in the list will cause
|
||||
// infinite loading state
|
||||
if (this.flightLegacy.id === flight.id) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.open.emit(flight);
|
||||
}
|
||||
|
||||
handleDateChanged(date: Date) {
|
||||
this.dateChange.emit(date);
|
||||
}
|
||||
}
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
<online-board-flight-details-meta-tags></online-board-flight-details-meta-tags>
|
||||
<online-board-details-view
|
||||
[flightLegacy]="dataSource.flightLegacy"
|
||||
[flightsLegacy]="dataSource.flightsLegacy"
|
||||
[flight]="dataSource.flight"
|
||||
[loading]="dataSource.loading"
|
||||
[searchDate]="searchDate"
|
||||
(open)="handleOpenEvent($event)"
|
||||
(dateChange)="handleDateChange($event)"
|
||||
>
|
||||
<online-board-flight-details-title
|
||||
title
|
||||
[flight]="dataSource.flight"
|
||||
></online-board-flight-details-title>
|
||||
</online-board-details-view>
|
||||
+262
@@ -0,0 +1,262 @@
|
||||
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { OnlineBoardFlightDetailsPageComponent } from '@online-board/pages/flight-details/flight-details-page.component';
|
||||
import { RefreshService } from '@online-board/pages/flight-details/services/refresh.service';
|
||||
import { OnlineBoardDataSourceService } from '@online-board/services/data-source.service';
|
||||
import { OnlineBoardNavigationService } from '@online-board/services/navigation.service';
|
||||
import { IOnlineBoardRouteData } from '@online-board/types';
|
||||
import { IFlightId } from '@typings/flight/flight-id';
|
||||
import { BehaviorSubject } from 'rxjs';
|
||||
|
||||
describe('OnlineBoardFlightDetailsPageComponent', () => {
|
||||
let fixture: ComponentFixture<OnlineBoardFlightDetailsPageComponent>;
|
||||
let component: OnlineBoardFlightDetailsPageComponent;
|
||||
|
||||
const data: IOnlineBoardRouteData = {};
|
||||
const loadFlightByFlightNumberMock = jasmine.createSpy(
|
||||
'loadFlightByFlightNumber',
|
||||
);
|
||||
const loadFlightsByRouteMock = jasmine.createSpy('loadFlightsByRoute');
|
||||
const loadFlightsByFlightNumberMock = jasmine.createSpy(
|
||||
'loadFlightsByFlightNumber',
|
||||
);
|
||||
const refreshStartMock = jasmine.createSpy('start');
|
||||
const refreshStopMock = jasmine.createSpy('stop');
|
||||
const toDetailsPageMock = jasmine.createSpy('toDetailsPage');
|
||||
const toNotFoundMock = jasmine.createSpy('toNotFound');
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [OnlineBoardFlightDetailsPageComponent],
|
||||
providers: [
|
||||
{
|
||||
provide: OnlineBoardDataSourceService,
|
||||
useValue: {
|
||||
loadFlightByFlightNumber: loadFlightByFlightNumberMock,
|
||||
loadFlightsByRoute: loadFlightsByRouteMock,
|
||||
loadFlightsByFlightNumber:
|
||||
loadFlightsByFlightNumberMock,
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: ActivatedRoute,
|
||||
useValue: {
|
||||
data: new BehaviorSubject(data),
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: OnlineBoardNavigationService,
|
||||
useValue: {
|
||||
toDetailsPage: toDetailsPageMock,
|
||||
toNotFound: toNotFoundMock,
|
||||
},
|
||||
},
|
||||
],
|
||||
schemas: [NO_ERRORS_SCHEMA],
|
||||
}).compileComponents();
|
||||
|
||||
TestBed.overrideComponent(OnlineBoardFlightDetailsPageComponent, {
|
||||
set: {
|
||||
providers: [
|
||||
{
|
||||
provide: RefreshService,
|
||||
useValue: {
|
||||
start: refreshStartMock,
|
||||
stop: refreshStopMock,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(
|
||||
OnlineBoardFlightDetailsPageComponent,
|
||||
);
|
||||
component = fixture.componentInstance;
|
||||
|
||||
loadFlightByFlightNumberMock.calls.reset();
|
||||
loadFlightsByFlightNumberMock.calls.reset();
|
||||
loadFlightsByRouteMock.calls.reset();
|
||||
|
||||
refreshStartMock.calls.reset();
|
||||
refreshStopMock.calls.reset();
|
||||
|
||||
toDetailsPageMock.calls.reset();
|
||||
toNotFoundMock.calls.reset();
|
||||
|
||||
loadFlightByFlightNumberMock.and.returnValue(Promise.resolve());
|
||||
|
||||
data.urlParams = {
|
||||
details: {
|
||||
flightNumber: '0001',
|
||||
carrier: 'SU',
|
||||
date: new Date(2022, 4, 28),
|
||||
},
|
||||
};
|
||||
data.requestData = {
|
||||
raw: 'onlineboard-arrival-MOW-20220528',
|
||||
parsed: {
|
||||
type: 'route',
|
||||
params: {
|
||||
arrival: 'MOW',
|
||||
date: new Date(2022, 4, 27),
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
it('should not call loadFlightByFlightNumber and store params', () => {
|
||||
data.urlParams.details = undefined;
|
||||
component.ngOnInit();
|
||||
|
||||
expect(component.params).toBe(undefined);
|
||||
expect(loadFlightByFlightNumberMock).not.toHaveBeenCalled();
|
||||
|
||||
data.urlParams = undefined;
|
||||
component.ngOnInit();
|
||||
|
||||
expect(component.params).toBe(undefined);
|
||||
expect(loadFlightByFlightNumberMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should not call dataSource methods and store request', () => {
|
||||
data.urlParams.details = undefined;
|
||||
data.requestData = undefined;
|
||||
component.ngOnInit();
|
||||
|
||||
expect(component.requestQueryParam).toBe(undefined);
|
||||
expect(loadFlightByFlightNumberMock).not.toHaveBeenCalled();
|
||||
expect(loadFlightsByRouteMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should call loadFlightByFlightNumber and store params', () => {
|
||||
data.requestData = undefined;
|
||||
|
||||
component.ngOnInit();
|
||||
expect(component.params).toEqual(data.urlParams.details);
|
||||
expect(loadFlightByFlightNumberMock).toHaveBeenCalledWith(
|
||||
data.urlParams.details,
|
||||
);
|
||||
});
|
||||
|
||||
it('should start refresh service', async () => {
|
||||
loadFlightByFlightNumberMock.and.returnValue(
|
||||
Promise.resolve({
|
||||
data: {
|
||||
routes: ['1'],
|
||||
},
|
||||
}),
|
||||
);
|
||||
data.requestData = undefined;
|
||||
|
||||
component.ngOnInit();
|
||||
await Promise.resolve();
|
||||
await Promise.resolve();
|
||||
expect(refreshStartMock).toHaveBeenCalledWith(data.urlParams.details);
|
||||
});
|
||||
|
||||
it('should navigate to not found if loadFlightByFlightNumber failed', async () => {
|
||||
data.requestData = undefined;
|
||||
loadFlightByFlightNumberMock.and.returnValue(Promise.reject());
|
||||
|
||||
component.ngOnInit();
|
||||
await Promise.resolve();
|
||||
await Promise.resolve();
|
||||
|
||||
expect(toNotFoundMock).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should navigate to not found if api request resolved with empty result', async () => {
|
||||
loadFlightByFlightNumberMock.and.returnValue(
|
||||
Promise.resolve({
|
||||
data: {
|
||||
routes: [],
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
component.ngOnInit();
|
||||
await Promise.resolve();
|
||||
|
||||
expect(refreshStartMock).not.toHaveBeenCalled();
|
||||
expect(toNotFoundMock).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should stop refresh service', () => {
|
||||
component.ngOnDestroy();
|
||||
|
||||
expect(refreshStopMock).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should call loadFlightsByRoute and store request', () => {
|
||||
component.ngOnInit();
|
||||
|
||||
expect(component.requestQueryParam).toBe(data.requestData.raw);
|
||||
expect(loadFlightsByRouteMock).toHaveBeenCalledWith(
|
||||
data.requestData.parsed.params,
|
||||
);
|
||||
});
|
||||
|
||||
it('should call loadFlightsByFlightNumber and store request', () => {
|
||||
data.requestData = {
|
||||
raw: 'onlineboard-flight-SU0001-20220528',
|
||||
parsed: {
|
||||
type: 'flight-number',
|
||||
params: {
|
||||
flightNumber: '0001',
|
||||
carrier: 'SU',
|
||||
date: new Date(2022, 4, 28),
|
||||
},
|
||||
},
|
||||
};
|
||||
component.ngOnInit();
|
||||
|
||||
expect(component.requestQueryParam).toBe(data.requestData.raw);
|
||||
expect(loadFlightsByFlightNumberMock).toHaveBeenCalledWith(
|
||||
data.requestData.parsed.params,
|
||||
);
|
||||
});
|
||||
|
||||
it('should return stored date', () => {
|
||||
component.ngOnInit();
|
||||
expect(component.searchDate).toEqual(data.urlParams.details.date);
|
||||
});
|
||||
|
||||
it('should navigate to details with stored data and provided date', () => {
|
||||
const date = new Date(2022, 4, 29);
|
||||
const flightId: IFlightId = {
|
||||
date: '2022-05-28T00:00:00',
|
||||
flightNumber: '0001',
|
||||
carrier: 'SU',
|
||||
};
|
||||
(component.dataSource as any).flight = { flightId };
|
||||
component.ngOnInit();
|
||||
component.handleDateChange(date);
|
||||
|
||||
expect(toDetailsPageMock).toHaveBeenCalledWith({
|
||||
flightId,
|
||||
date,
|
||||
request: data.requestData.raw,
|
||||
});
|
||||
});
|
||||
|
||||
it('should navigate to details with stored data and provided flight', () => {
|
||||
const flightId: IFlightId = {
|
||||
date: '2022-05-25T00:00:00',
|
||||
flightNumber: '0001',
|
||||
carrier: 'SU',
|
||||
};
|
||||
const flight: any = { flightId };
|
||||
component.ngOnInit();
|
||||
component.handleOpenEvent(flight);
|
||||
|
||||
expect(toDetailsPageMock).toHaveBeenCalledWith({
|
||||
flightId,
|
||||
date: new Date(2022, 4, 25),
|
||||
request: data.requestData.raw,
|
||||
});
|
||||
});
|
||||
});
|
||||
+98
@@ -0,0 +1,98 @@
|
||||
import { Component, OnDestroy, OnInit } from '@angular/core';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { OnlineBoardDataSourceService } from '@online-board/services/data-source.service';
|
||||
import { OnlineBoardNavigationService } from '@online-board/services/navigation.service';
|
||||
import { IOnlineBoardRequest } from '@online-board/services/request/request-parser.service';
|
||||
import { IOnlineBoardRouteData } from '@online-board/types';
|
||||
import { ISimpleFlight } from '@typings/flight/flight';
|
||||
import { IFlightId, IParsedFlightId } from '@typings/flight/flight-id';
|
||||
import { RefreshService } from './services/refresh.service';
|
||||
import FlightDateUtils from '@utils/flight/date';
|
||||
|
||||
@Component({
|
||||
selector: 'online-board-flight-details-page',
|
||||
templateUrl: './flight-details-page.component.html',
|
||||
providers: [RefreshService],
|
||||
})
|
||||
export class OnlineBoardFlightDetailsPageComponent
|
||||
implements OnInit, OnDestroy
|
||||
{
|
||||
params: IParsedFlightId;
|
||||
requestQueryParam: string;
|
||||
|
||||
constructor(
|
||||
public dataSource: OnlineBoardDataSourceService,
|
||||
private route: ActivatedRoute,
|
||||
private refreshService: RefreshService,
|
||||
private navigationService: OnlineBoardNavigationService,
|
||||
) {}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.route.data.subscribe({
|
||||
next: (data: IOnlineBoardRouteData) => {
|
||||
if (data.urlParams?.details) {
|
||||
this.handleUrlParamsChange(data.urlParams.details);
|
||||
}
|
||||
|
||||
if (data.requestData) {
|
||||
this.requestQueryParam = data.requestData.raw;
|
||||
|
||||
this.loadFlightsByRequest(data.requestData.parsed);
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this.refreshService.stop();
|
||||
}
|
||||
|
||||
get searchDate() {
|
||||
return this.params.date;
|
||||
}
|
||||
|
||||
handleDateChange(date: Date) {
|
||||
this.navigationService.toDetailsPage({
|
||||
flightId: this.dataSource.flight.flightId,
|
||||
date,
|
||||
request: this.requestQueryParam,
|
||||
});
|
||||
}
|
||||
|
||||
handleOpenEvent(flight: ISimpleFlight) {
|
||||
this.navigationService.toDetailsPage({
|
||||
flightId: flight.flightId,
|
||||
date: FlightDateUtils.getFlightDate(flight),
|
||||
request: this.requestQueryParam,
|
||||
});
|
||||
}
|
||||
|
||||
private handleUrlParamsChange(params: IParsedFlightId) {
|
||||
this.params = params;
|
||||
|
||||
this.dataSource
|
||||
.loadFlightByFlightNumber(this.params)
|
||||
.then((res) => {
|
||||
if (!res.data.routes.length) {
|
||||
return this.navigationService.toNotFound();
|
||||
}
|
||||
this.refreshService.start(res.data.routes[0].flightId);
|
||||
})
|
||||
.catch(() => {
|
||||
return this.navigationService.toNotFound();
|
||||
});
|
||||
}
|
||||
|
||||
private loadFlightsByRequest(request: IOnlineBoardRequest) {
|
||||
switch (request.type) {
|
||||
case 'route': {
|
||||
this.dataSource.loadFlightsByRoute(request.params);
|
||||
return;
|
||||
}
|
||||
case 'flight-number': {
|
||||
this.dataSource.loadFlightsByFlightNumber(request.params);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+121
@@ -0,0 +1,121 @@
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
import { RefreshService } from '@online-board/pages/flight-details/services/refresh.service';
|
||||
import { OnlineBoardDataSourceService } from '@online-board/services/data-source.service';
|
||||
import { APP_SETTINGS } from '@shared/services';
|
||||
import { FlightStatus } from '@typings/enums';
|
||||
import { IParsedFlightId } from '@typings/flight/flight-id';
|
||||
|
||||
describe('RefreshService', () => {
|
||||
let service: RefreshService;
|
||||
let dataSource: OnlineBoardDataSourceService;
|
||||
|
||||
const refreshInterval = 100;
|
||||
|
||||
const unsubscribeMock = jasmine.createSpy('unsubscribe');
|
||||
const loadFlightByFlightNumberMock = jasmine.createSpy(
|
||||
'loadFlightByFlightNumber',
|
||||
);
|
||||
|
||||
const params: IParsedFlightId = {
|
||||
flightNumber: '0001',
|
||||
carrier: 'SU',
|
||||
date: new Date(2022, 4, 28),
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
providers: [
|
||||
RefreshService,
|
||||
{
|
||||
provide: APP_SETTINGS,
|
||||
useValue: {
|
||||
refreshInterval,
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: OnlineBoardDataSourceService,
|
||||
useValue: {
|
||||
loadFlightByFlightNumber: loadFlightByFlightNumberMock,
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
unsubscribeMock.calls.reset();
|
||||
loadFlightByFlightNumberMock.calls.reset();
|
||||
|
||||
service = TestBed.inject(RefreshService);
|
||||
dataSource = TestBed.inject(OnlineBoardDataSourceService);
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
jasmine.clock().install();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jasmine.clock().uninstall();
|
||||
});
|
||||
|
||||
it('should call loadFlightByFlightNumber with provided params each 100 seconds', () => {
|
||||
service.start(params);
|
||||
jasmine.clock().tick(refreshInterval);
|
||||
|
||||
expect(loadFlightByFlightNumberMock).toHaveBeenCalledWith(params, true);
|
||||
|
||||
jasmine.clock().tick(refreshInterval);
|
||||
expect(loadFlightByFlightNumberMock).toHaveBeenCalledTimes(2);
|
||||
|
||||
jasmine.clock().tick(refreshInterval);
|
||||
expect(loadFlightByFlightNumberMock).toHaveBeenCalledTimes(3);
|
||||
});
|
||||
|
||||
it('should unsubscribe if there is running interval', () => {
|
||||
(service.updateInterval as any) = {
|
||||
unsubscribe: unsubscribeMock,
|
||||
};
|
||||
|
||||
service.start(params);
|
||||
expect(unsubscribeMock).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should unsubscribe', () => {
|
||||
expect(() => service.stop()).not.toThrowError();
|
||||
|
||||
(service.updateInterval as any) = {
|
||||
unsubscribe: unsubscribeMock,
|
||||
};
|
||||
service.stop();
|
||||
|
||||
expect(unsubscribeMock).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should call loadFlightByFlightNumber because flight has appropriate status', () => {
|
||||
(dataSource.flight as any) = { status: FlightStatus.IN_FLIGHT };
|
||||
|
||||
service.start(params);
|
||||
jasmine.clock().tick(refreshInterval);
|
||||
expect(loadFlightByFlightNumberMock).toHaveBeenCalledWith(params, true);
|
||||
service.stop();
|
||||
|
||||
(dataSource.flight as any) = { status: FlightStatus.SENT };
|
||||
|
||||
service.start(params);
|
||||
jasmine.clock().tick(refreshInterval);
|
||||
expect(loadFlightByFlightNumberMock).toHaveBeenCalledTimes(2);
|
||||
|
||||
const [first, second] =
|
||||
loadFlightByFlightNumberMock.calls.mostRecent().args;
|
||||
expect(first).toEqual(params);
|
||||
expect(second).toBe(true);
|
||||
service.stop();
|
||||
});
|
||||
|
||||
it('should not call loadFlightByFlightNumber because flight has not appropriate status', () => {
|
||||
(dataSource.flight as any) = { status: FlightStatus.CANCELLED };
|
||||
|
||||
service.start(params);
|
||||
jasmine.clock().tick(refreshInterval);
|
||||
expect(loadFlightByFlightNumberMock).not.toHaveBeenCalled();
|
||||
service.stop();
|
||||
});
|
||||
});
|
||||
+58
@@ -0,0 +1,58 @@
|
||||
import { Inject, Injectable } from '@angular/core';
|
||||
import * as signalR from "@microsoft/signalr";
|
||||
import { environment } from '@environment';
|
||||
import { AppSettings } from '@shared/models-legacy';
|
||||
import { APP_SETTINGS } from '@shared/services';
|
||||
import { FlightStatus } from '@typings/enums';
|
||||
import { OnlineBoardDataSourceService } from '@online-board/services/data-source.service';
|
||||
import { IFlightId, IParsedFlightId } from '@typings/flight/flight-id';
|
||||
import { FadeService } from '@online-board/services/fade.service';
|
||||
|
||||
@Injectable()
|
||||
export class RefreshService {
|
||||
connection: any;
|
||||
|
||||
constructor(
|
||||
@Inject(APP_SETTINGS) private settings: AppSettings,
|
||||
public dataSource: OnlineBoardDataSourceService,
|
||||
private fadeService: FadeService,
|
||||
) {}
|
||||
|
||||
init() {
|
||||
this.connection = new signalR.HubConnectionBuilder()
|
||||
.withUrl(environment.urlForTrackerHub)
|
||||
.withAutomaticReconnect()
|
||||
.build();
|
||||
|
||||
this.connection.on('Refresh', (flightId: string) => {
|
||||
const l = flightId.length;
|
||||
const params: IParsedFlightId = {
|
||||
carrier: flightId.substring(0,2),
|
||||
flightNumber: flightId.substring(2,6),
|
||||
suffix: flightId.substring(6,l-11),
|
||||
date: new Date(flightId.substring(l-10, l)),
|
||||
};
|
||||
this.dataSource.loadFlightByFlightNumber(params, true).finally(() => {
|
||||
});;
|
||||
});
|
||||
}
|
||||
|
||||
start(params: IFlightId) {
|
||||
if (!this.connection) {
|
||||
this.init();
|
||||
}
|
||||
|
||||
// if connection is disconnected then stop just console.log about it and returns, then we start it
|
||||
this.connection.stop().then(() => {
|
||||
this.fadeService.start();
|
||||
this.connection.start().then(() => {
|
||||
this.connection.invoke("Subscribe", params.carrier+params.flightNumber+params.suffix+"@"+params.dateLT);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
stop() {
|
||||
this.connection.stop();
|
||||
this.fadeService.stop();
|
||||
}
|
||||
}
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
<online-board-arrival-meta-tags></online-board-arrival-meta-tags>
|
||||
<online-board-search-page-base [requestType]="FlightRequestType.Arrival">
|
||||
<online-board-arrival-title title></online-board-arrival-title>
|
||||
</online-board-search-page-base>
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { FlightRequestType } from '@shared/enumerators/flight-request-type.enum';
|
||||
|
||||
@Component({
|
||||
selector: 'arrival-search-page',
|
||||
templateUrl: 'arrival-search-page.component.html',
|
||||
})
|
||||
export class ArrivalSearchPageComponent {
|
||||
FlightRequestType = FlightRequestType;
|
||||
}
|
||||
+1
@@ -0,0 +1 @@
|
||||
<meta-tags [title]="title" [description]="description"></meta-tags>
|
||||
+71
@@ -0,0 +1,71 @@
|
||||
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { OnlineBoardArrivalMetaTagsComponent } from '@online-board/pages/search/arrival/components/meta-tags/online-board-arrival-meta-tags.component';
|
||||
import { OnlineBoardMetaTagsService } from '@online-board/services/meta-tags.service';
|
||||
import { IOnlineBoardRouteData } from '@online-board/types';
|
||||
import { BehaviorSubject } from 'rxjs';
|
||||
|
||||
describe('OnlineBoardArrivalMetaTagsComponent', () => {
|
||||
let component: OnlineBoardArrivalMetaTagsComponent;
|
||||
let fixture: ComponentFixture<OnlineBoardArrivalMetaTagsComponent>;
|
||||
const getArrivalMetaTagsMock = jasmine.createSpy('getArrivalMetaTags');
|
||||
const data: IOnlineBoardRouteData = {
|
||||
urlParams: {},
|
||||
};
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [OnlineBoardArrivalMetaTagsComponent],
|
||||
providers: [
|
||||
{
|
||||
provide: OnlineBoardMetaTagsService,
|
||||
useValue: {
|
||||
getArrivalMetaTags: getArrivalMetaTagsMock,
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: ActivatedRoute,
|
||||
useValue: {
|
||||
data: new BehaviorSubject(data),
|
||||
},
|
||||
},
|
||||
],
|
||||
schemas: [NO_ERRORS_SCHEMA],
|
||||
}).compileComponents();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(OnlineBoardArrivalMetaTagsComponent);
|
||||
component = fixture.componentInstance;
|
||||
|
||||
data.urlParams.route = undefined;
|
||||
getArrivalMetaTagsMock.calls.reset();
|
||||
});
|
||||
|
||||
it('should not set title and description because route param is not provided', () => {
|
||||
component.ngOnInit();
|
||||
|
||||
expect(component.title).toBe('');
|
||||
expect(component.description).toBe('');
|
||||
});
|
||||
|
||||
it('should set title and description', async () => {
|
||||
data.urlParams.route = {
|
||||
arrival: 'MOW',
|
||||
date: new Date(2022, 4, 28),
|
||||
};
|
||||
|
||||
getArrivalMetaTagsMock.withArgs(data.urlParams.route).and.returnValue(
|
||||
Promise.resolve({
|
||||
title: 'arrival title',
|
||||
description: 'arrival description',
|
||||
}),
|
||||
);
|
||||
component.ngOnInit();
|
||||
await Promise.resolve();
|
||||
|
||||
expect(component.title).toBe('arrival title');
|
||||
expect(component.description).toBe('arrival description');
|
||||
});
|
||||
});
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { OnlineBoardMetaTagsService } from '@online-board/services/meta-tags.service';
|
||||
import { IOnlineBoardRouteData } from '@online-board/types';
|
||||
|
||||
@Component({
|
||||
selector: 'online-board-arrival-meta-tags',
|
||||
templateUrl: './online-board-arrival-meta-tags.component.html',
|
||||
})
|
||||
export class OnlineBoardArrivalMetaTagsComponent implements OnInit {
|
||||
title = '';
|
||||
description = '';
|
||||
|
||||
constructor(
|
||||
private route: ActivatedRoute,
|
||||
private metaTagsService: OnlineBoardMetaTagsService,
|
||||
) {}
|
||||
|
||||
ngOnInit() {
|
||||
this.route.data.subscribe(async (data: IOnlineBoardRouteData) => {
|
||||
if (data.urlParams.route) {
|
||||
const metaTags = await this.metaTagsService.getArrivalMetaTags(
|
||||
data.urlParams.route,
|
||||
);
|
||||
|
||||
this.title = metaTags.title;
|
||||
this.description = metaTags.description;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
+1
@@ -0,0 +1 @@
|
||||
<aero-title [title]="title"></aero-title>
|
||||
+66
@@ -0,0 +1,66 @@
|
||||
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { OnlineBoardArrivalTitleComponent } from '@online-board/pages/search/arrival/components/title/online-board-arrival-title.component';
|
||||
import { OnlineBoardTitleService } from '@online-board/services/title.service';
|
||||
import { IOnlineBoardRouteData } from '@online-board/types';
|
||||
import { BehaviorSubject } from 'rxjs';
|
||||
|
||||
describe('OnlineBoardArrivalTitleComponent', () => {
|
||||
let component: OnlineBoardArrivalTitleComponent;
|
||||
let fixture: ComponentFixture<OnlineBoardArrivalTitleComponent>;
|
||||
const getArrivalTitleMock = jasmine.createSpy('getArrivalTitle');
|
||||
const data: IOnlineBoardRouteData = {
|
||||
urlParams: {},
|
||||
};
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [OnlineBoardArrivalTitleComponent],
|
||||
providers: [
|
||||
{
|
||||
provide: OnlineBoardTitleService,
|
||||
useValue: {
|
||||
getArrivalTitle: getArrivalTitleMock,
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: ActivatedRoute,
|
||||
useValue: {
|
||||
data: new BehaviorSubject(data),
|
||||
},
|
||||
},
|
||||
],
|
||||
schemas: [NO_ERRORS_SCHEMA],
|
||||
}).compileComponents();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(OnlineBoardArrivalTitleComponent);
|
||||
component = fixture.componentInstance;
|
||||
|
||||
data.urlParams.route = undefined;
|
||||
getArrivalTitleMock.calls.reset();
|
||||
});
|
||||
|
||||
it('should not set title because route param is not provided', () => {
|
||||
component.ngOnInit();
|
||||
|
||||
expect(component.title).toBe('');
|
||||
});
|
||||
|
||||
it('should set title', async () => {
|
||||
data.urlParams.route = {
|
||||
arrival: 'MOW',
|
||||
date: new Date(2022, 4, 28),
|
||||
};
|
||||
|
||||
getArrivalTitleMock
|
||||
.withArgs(data.urlParams.route)
|
||||
.and.returnValue(Promise.resolve('arrival title'));
|
||||
component.ngOnInit();
|
||||
await Promise.resolve();
|
||||
|
||||
expect(component.title).toBe('arrival title');
|
||||
});
|
||||
});
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { OnlineBoardTitleService } from '@online-board/services/title.service';
|
||||
import { IOnlineBoardRouteData } from '@online-board/types';
|
||||
|
||||
@Component({
|
||||
selector: 'online-board-arrival-title',
|
||||
templateUrl: './online-board-arrival-title.component.html',
|
||||
})
|
||||
export class OnlineBoardArrivalTitleComponent implements OnInit {
|
||||
title = '';
|
||||
|
||||
constructor(
|
||||
private route: ActivatedRoute,
|
||||
private titleService: OnlineBoardTitleService,
|
||||
) {}
|
||||
|
||||
ngOnInit() {
|
||||
this.route.data.subscribe({
|
||||
next: async (data: IOnlineBoardRouteData) => {
|
||||
if (data.urlParams.route) {
|
||||
this.title = await this.titleService.getArrivalTitle(
|
||||
data.urlParams.route,
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
<same-url-navigation-detector
|
||||
(sameUrlNavigation)="handleSameUrlNavigation()"
|
||||
></same-url-navigation-detector>
|
||||
<online-board-search
|
||||
[flights]="dataSource.flightsLegacy"
|
||||
[loading]="dataSource.loading"
|
||||
[partners]="dataSource.partners"
|
||||
[searchDate]="params.date"
|
||||
[timeFrom]="params.timeFrom"
|
||||
[timeTo]="params.timeTo"
|
||||
[requestType]="requestType"
|
||||
[disabledDates]="disabledDates"
|
||||
(requestCancel)="handleRequestCancellation()"
|
||||
(searchDateChange)="handleDateChange($event)"
|
||||
(timeRangeChange)="handleTimeRangeChange($event)"
|
||||
(toDetails)="navigateToDetails($any($event))"
|
||||
>
|
||||
<ng-container title>
|
||||
<ng-content select="[title]"></ng-content>
|
||||
</ng-container>
|
||||
</online-board-search>
|
||||
+185
@@ -0,0 +1,185 @@
|
||||
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { OnlineBoardSearchPageBaseComponent } from '@online-board/pages/search/components/base/search-page-base.component';
|
||||
import { OnlineBoardDataSourceService } from '@online-board/services/data-source.service';
|
||||
import { OnlineBoardNavigationService } from '@online-board/services/navigation.service';
|
||||
import { OnlineBoardRequestBuilderService } from '@online-board/services/request/request-builder.service';
|
||||
import { IOnlineBoardRouteData } from '@online-board/types';
|
||||
import { IUrlTimeRange } from '@typings/common/url';
|
||||
import { IFlightId } from '@typings/flight/flight-id';
|
||||
import FlightDateUtils from '@utils/flight/date';
|
||||
import { BehaviorSubject } from 'rxjs';
|
||||
|
||||
describe('OnlineBoardSearchPageBaseComponent', () => {
|
||||
let fixture: ComponentFixture<OnlineBoardSearchPageBaseComponent>;
|
||||
let component: OnlineBoardSearchPageBaseComponent;
|
||||
let getFlightDateMock;
|
||||
|
||||
const data: IOnlineBoardRouteData = {};
|
||||
|
||||
const loadFlightsByRouteMock = jasmine.createSpy('loadFlightsByRoute');
|
||||
const stopLoadingMock = jasmine.createSpy('stopLoading');
|
||||
|
||||
const toStartPageMock = jasmine.createSpy('toStartPage');
|
||||
const toRoutePageMock = jasmine.createSpy('toRoutePage');
|
||||
const toDetailsPageMock = jasmine.createSpy('toDetailsPage');
|
||||
|
||||
const buildRouteRequestMock = jasmine.createSpy('buildRouteRequest');
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [OnlineBoardSearchPageBaseComponent],
|
||||
providers: [
|
||||
{
|
||||
provide: ActivatedRoute,
|
||||
useValue: {
|
||||
data: new BehaviorSubject(data),
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: OnlineBoardDataSourceService,
|
||||
useValue: {
|
||||
loadFlightsByRoute: loadFlightsByRouteMock,
|
||||
stopLoading: stopLoadingMock,
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: OnlineBoardNavigationService,
|
||||
useValue: {
|
||||
toStartPage: toStartPageMock,
|
||||
toRoutePage: toRoutePageMock,
|
||||
toDetailsPage: toDetailsPageMock,
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: OnlineBoardRequestBuilderService,
|
||||
useValue: {
|
||||
buildRouteRequest: buildRouteRequestMock,
|
||||
},
|
||||
},
|
||||
],
|
||||
schemas: [NO_ERRORS_SCHEMA],
|
||||
}).compileComponents();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(OnlineBoardSearchPageBaseComponent);
|
||||
component = fixture.componentInstance;
|
||||
|
||||
getFlightDateMock = spyOn(FlightDateUtils, 'getFlightDate');
|
||||
|
||||
loadFlightsByRouteMock.calls.reset();
|
||||
stopLoadingMock.calls.reset();
|
||||
|
||||
toRoutePageMock.calls.reset();
|
||||
toStartPageMock.calls.reset();
|
||||
toDetailsPageMock.calls.reset();
|
||||
|
||||
buildRouteRequestMock.calls.reset();
|
||||
|
||||
data.urlParams = {
|
||||
route: {
|
||||
date: new Date(2022, 4, 28),
|
||||
arrival: 'MRS',
|
||||
departure: 'MOW',
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
it('should not call dataSource.loadFlightsByRoute if no params were provided', () => {
|
||||
data.urlParams.route = undefined;
|
||||
|
||||
component.ngOnInit();
|
||||
expect(component.params).toBe(undefined);
|
||||
expect(loadFlightsByRouteMock).not.toHaveBeenCalled();
|
||||
|
||||
data.urlParams = undefined;
|
||||
|
||||
component.ngOnInit();
|
||||
expect(component.params).toBe(undefined);
|
||||
expect(loadFlightsByRouteMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should call dataSource.loadFlightsByRoute', () => {
|
||||
component.ngOnInit();
|
||||
expect(component.params).toBe(data.urlParams.route);
|
||||
expect(loadFlightsByRouteMock).toHaveBeenCalledWith(
|
||||
data.urlParams.route,
|
||||
);
|
||||
});
|
||||
|
||||
it('should call dataSource.loadFlightsByRoute with params from urlParams.route', () => {
|
||||
component.ngOnInit();
|
||||
expect(component.params).toBe(data.urlParams.route);
|
||||
|
||||
component.handleSameUrlNavigation();
|
||||
expect(loadFlightsByRouteMock.calls.count()).toBe(2);
|
||||
expect(loadFlightsByRouteMock.calls.mostRecent().args[0]).toEqual(
|
||||
data.urlParams.route,
|
||||
);
|
||||
});
|
||||
|
||||
it('should call dataSource.stopLoading and navigate to start page', () => {
|
||||
component.handleRequestCancellation();
|
||||
|
||||
expect(stopLoadingMock).toHaveBeenCalled();
|
||||
expect(toStartPageMock).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should navigate to route page with saved params and provided date', () => {
|
||||
const date = new Date(2022, 5, 28);
|
||||
|
||||
component.ngOnInit();
|
||||
component.handleDateChange(date);
|
||||
expect(toRoutePageMock).toHaveBeenCalledWith({
|
||||
...data.urlParams.route,
|
||||
date,
|
||||
});
|
||||
});
|
||||
|
||||
it('should navigate to route page with saved params and provided time range', () => {
|
||||
const timeRange: IUrlTimeRange = {
|
||||
timeFrom: '0000',
|
||||
timeTo: '1800',
|
||||
};
|
||||
|
||||
component.ngOnInit();
|
||||
component.handleTimeRangeChange(timeRange);
|
||||
expect(toRoutePageMock).toHaveBeenCalledWith({
|
||||
...data.urlParams.route,
|
||||
...timeRange,
|
||||
});
|
||||
|
||||
component.handleTimeRangeChange();
|
||||
expect(toRoutePageMock).toHaveBeenCalledWith({
|
||||
...data.urlParams.route,
|
||||
timeFrom: undefined,
|
||||
timeTo: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it('should navigate to details with flights info and request built from saved params', () => {
|
||||
const request = 'onlineboard-route-MOW-MRS-20220528';
|
||||
const dateStr = '2022-05-27T00:00:00';
|
||||
const date = new Date(dateStr);
|
||||
const flightId: IFlightId = {
|
||||
flightNumber: '0001',
|
||||
carrier: 'SU',
|
||||
date: dateStr,
|
||||
};
|
||||
|
||||
getFlightDateMock.withArgs({ flightId }).and.returnValue(date);
|
||||
buildRouteRequestMock
|
||||
.withArgs(data.urlParams.route)
|
||||
.and.returnValue(request);
|
||||
|
||||
component.ngOnInit();
|
||||
component.navigateToDetails({ flightId } as any);
|
||||
expect(toDetailsPageMock).toHaveBeenCalledWith({
|
||||
flightId,
|
||||
date,
|
||||
request,
|
||||
});
|
||||
});
|
||||
});
|
||||
+118
@@ -0,0 +1,118 @@
|
||||
import { Component, Input, OnInit } from '@angular/core';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { OnlineBoardDataSourceService } from '@online-board/services/data-source.service';
|
||||
import { OnlineBoardNavigationService } from '@online-board/services/navigation.service';
|
||||
import { OnlineBoardRequestBuilderService } from '@online-board/services/request/request-builder.service';
|
||||
import { IOnlineBoardRoutePageUrlParams } from '@online-board/services/url/types';
|
||||
import { OnlineBoardApiService } from '@online-board/services/api.service';
|
||||
import { OnlineBoardRouteFilterValidationService } from '@online-board/components/filter/components/route-filter/services/online-board-route-filter-validation.service';
|
||||
import { FlightRequestType } from '@shared/enumerators/flight-request-type.enum';
|
||||
import { IUrlTimeRange } from '@typings/common/url';
|
||||
import { ISimpleFlight } from '@typings/flight/flight';
|
||||
import { IOnlineBoardRouteData } from '@online-board/types';
|
||||
import { RefreshBoardService } from './services/refresh-board.service';
|
||||
import { ApiFormatterService } from '@shared/services/api/formatter.service';
|
||||
import FlightDateUtils from '@utils/flight/date';
|
||||
|
||||
@Component({
|
||||
selector: 'online-board-search-page-base',
|
||||
templateUrl: './search-page-base.component.html',
|
||||
providers: [OnlineBoardRouteFilterValidationService, RefreshBoardService],
|
||||
})
|
||||
export class OnlineBoardSearchPageBaseComponent implements OnInit {
|
||||
@Input() requestType: FlightRequestType;
|
||||
disabledDates: Date[];
|
||||
|
||||
params: IOnlineBoardRoutePageUrlParams;
|
||||
|
||||
constructor(
|
||||
public dataSource: OnlineBoardDataSourceService,
|
||||
private apiService: OnlineBoardApiService,
|
||||
private apiFormatter: ApiFormatterService,
|
||||
private validationService: OnlineBoardRouteFilterValidationService,
|
||||
private route: ActivatedRoute,
|
||||
private refreshBoardService: RefreshBoardService,
|
||||
private navigationService: OnlineBoardNavigationService,
|
||||
private requestBuilder: OnlineBoardRequestBuilderService,
|
||||
) {}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.route.data.subscribe({
|
||||
next: (data: IOnlineBoardRouteData) => {
|
||||
if (data.urlParams?.route) {
|
||||
this.params = data.urlParams.route;
|
||||
|
||||
this.dataSource.loadFlightsByRoute(this.params);
|
||||
this.refreshBoardService.start(this.apiFormatter.formatDateOnly(this.params.date), this.params);
|
||||
}
|
||||
this.updateCalendar();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this.refreshBoardService.stop();
|
||||
}
|
||||
|
||||
async updateCalendar() {
|
||||
const isDepartureValid = await this.validationService.validateCode(this.params.departure);
|
||||
const isArrivalValid = await this.validationService.validateCode(this.params.arrival);
|
||||
|
||||
if (isDepartureValid || isArrivalValid) {
|
||||
var date = new Date();
|
||||
date.setUTCHours(0,0,0,0);
|
||||
date.setDate(date.getDate() - 1);
|
||||
this.apiService
|
||||
.getFlightDaysByRoute(date, isDepartureValid ? this.params.departure: undefined, isArrivalValid ? this.params.arrival: undefined)
|
||||
.then((res) => {
|
||||
this.disabledDates = new Array();
|
||||
for(var i=0;i<res.days.length;i++) {
|
||||
if (res.days[i] == '0') {
|
||||
this.disabledDates.push(new Date(date));
|
||||
}
|
||||
date.setDate(date.getDate() + 1);
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
});
|
||||
} else {
|
||||
this.disabledDates = new Array();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public handleSameUrlNavigation() {
|
||||
this.dataSource.loadFlightsByRoute(this.params);
|
||||
}
|
||||
|
||||
public handleRequestCancellation() {
|
||||
this.dataSource.stopLoading();
|
||||
|
||||
return this.navigationService.toStartPage();
|
||||
}
|
||||
|
||||
public handleDateChange(date: Date) {
|
||||
return this.navigationService.toRoutePage({
|
||||
...this.params,
|
||||
date,
|
||||
});
|
||||
}
|
||||
|
||||
public handleTimeRangeChange(timeRange?: IUrlTimeRange) {
|
||||
return this.navigationService.toRoutePage({
|
||||
...this.params,
|
||||
timeFrom: timeRange?.timeFrom,
|
||||
timeTo: timeRange?.timeTo,
|
||||
});
|
||||
}
|
||||
|
||||
public navigateToDetails(flight: ISimpleFlight) {
|
||||
const request = this.requestBuilder.buildRouteRequest(this.params);
|
||||
|
||||
return this.navigationService.toDetailsPage({
|
||||
flightId: flight.flightId,
|
||||
date: FlightDateUtils.getFlightDate(flight),
|
||||
request,
|
||||
});
|
||||
}
|
||||
}
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
import { Inject, Injectable } from '@angular/core';
|
||||
import * as signalR from "@microsoft/signalr";
|
||||
import { environment } from '@environment';
|
||||
import { interval, Subscription } from 'rxjs';
|
||||
import { AppSettings } from '@shared/models-legacy';
|
||||
import { APP_SETTINGS } from '@shared/services';
|
||||
import { FlightStatus } from '@typings/enums';
|
||||
import { IOnlineBoardRoutePageUrlParams } from '@online-board/services/url/types';
|
||||
import { OnlineBoardDataSourceService } from '@online-board/services/data-source.service';
|
||||
import { IFlightId, IParsedFlightId } from '@typings/flight/flight-id';
|
||||
import { FadeService } from '@online-board/services/fade.service';
|
||||
|
||||
@Injectable()
|
||||
export class RefreshBoardService {
|
||||
connection: any;
|
||||
loadParams: IOnlineBoardRoutePageUrlParams;
|
||||
|
||||
constructor(
|
||||
@Inject(APP_SETTINGS) private settings: AppSettings,
|
||||
public dataSource: OnlineBoardDataSourceService,
|
||||
private fadeService: FadeService,
|
||||
) {}
|
||||
|
||||
init() {
|
||||
this.connection = new signalR.HubConnectionBuilder()
|
||||
.withUrl(environment.urlForTrackerHub)
|
||||
.withAutomaticReconnect()
|
||||
.build();
|
||||
|
||||
this.connection.on('RefreshDate', (date: string) => {
|
||||
this.dataSource.loadFlightsByRoute(this.loadParams, true);
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
start(flightDate: string, params: IOnlineBoardRoutePageUrlParams) {
|
||||
this.loadParams = params;
|
||||
if (!this.connection) {
|
||||
this.init();
|
||||
}
|
||||
|
||||
// if connection is disconnected then stop just console.log about it and returns, then we start it
|
||||
this.connection.stop().then(() => {
|
||||
this.fadeService.start();
|
||||
this.connection.start().then(() => {
|
||||
this.connection.invoke("SubscribeDate", flightDate, params.departure, params.arrival);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
stop() {
|
||||
this.connection.stop();
|
||||
this.fadeService.stop();
|
||||
}
|
||||
}
|
||||
+86
@@ -0,0 +1,86 @@
|
||||
<div *localisationReady>
|
||||
<page-layout [withScrollUp]="withScrollUp">
|
||||
<ng-container title>
|
||||
<ng-content select="[title]"></ng-content>
|
||||
</ng-container>
|
||||
<ng-container header-left>
|
||||
<spin-lock *ngIf="loading"></spin-lock>
|
||||
<flights-page-tabs
|
||||
[viewType]="ViewType.Onlineboard"
|
||||
></flights-page-tabs>
|
||||
</ng-container>
|
||||
|
||||
<ng-container content-left>
|
||||
<spin-lock *ngIf="loading"></spin-lock>
|
||||
<online-board-filter></online-board-filter>
|
||||
<search-history></search-history>
|
||||
</ng-container>
|
||||
|
||||
<ng-container sticky-content>
|
||||
<card [roundedTop]="true">
|
||||
<spin-lock *ngIf="loading"></spin-lock>
|
||||
<day-tabs
|
||||
class="board-day-selector"
|
||||
caption="SHARED.FLIGHT_DATE"
|
||||
[selectedDate]="searchDate"
|
||||
[disabledDates]="disabledDates"
|
||||
[dateTo]="settings.boardMaxDate"
|
||||
[dateFrom]="settings.boardMinDate"
|
||||
[minDate]="settings.boardMinDate"
|
||||
[maxDate]="settings.boardMaxDate"
|
||||
(tabClick)="handleDateChanged($event)"
|
||||
></day-tabs>
|
||||
<time-selector
|
||||
*ngIf="featureFlags.RESULTS_TIME_SELECTOR"
|
||||
[fullView]="true"
|
||||
[(ngModel)]="timeRange"
|
||||
[label]="
|
||||
requestType === FlightRequestType.Arrival
|
||||
? ('SHARED.ARRIVAL_TIME' | translate)
|
||||
: ('SHARED.DEPARTURE_TIME' | translate)
|
||||
"
|
||||
(slideComplete)="handleTimeChanged()"
|
||||
[disabled]="requestType === FlightRequestType.Flight"
|
||||
>
|
||||
</time-selector>
|
||||
</card>
|
||||
</ng-container>
|
||||
<ng-container>
|
||||
<page-loader
|
||||
data-testid="loader"
|
||||
*ngIf="loading"
|
||||
(searchCancel)="cancel()"
|
||||
></page-loader>
|
||||
|
||||
<ng-container *ngIf="!loading">
|
||||
<board-search-result
|
||||
[currentFlight]="currentFlight"
|
||||
[flights]="flights"
|
||||
(toDetails)="handleToDetailsEvent($event)"
|
||||
(changeCurrentFlight)="handleChangeCurrentFlightEvent($event)"
|
||||
></board-search-result>
|
||||
|
||||
<ng-container *ngIf="!flights?.length">
|
||||
<page-empty-list
|
||||
*ngIf="
|
||||
requestType !== FlightRequestType.Flight ||
|
||||
!partners?.length
|
||||
"
|
||||
[scrollTo]="true"
|
||||
></page-empty-list>
|
||||
|
||||
<partners-redirect-note
|
||||
*ngIf="
|
||||
requestType === FlightRequestType.Flight &&
|
||||
partners?.length
|
||||
"
|
||||
[scrollTo]="true"
|
||||
[partners]="partners"
|
||||
></partners-redirect-note>
|
||||
</ng-container>
|
||||
|
||||
<page-footer-notes></page-footer-notes>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
</page-layout>
|
||||
</div>
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
@use 'src/styles/variables' as vars;
|
||||
@use 'src/styles/screen';
|
||||
|
||||
.board-day-selector {
|
||||
@include screen.mobile {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
padding: vars.$space-xl;
|
||||
}
|
||||
}
|
||||
+146
@@ -0,0 +1,146 @@
|
||||
import {
|
||||
Component,
|
||||
EventEmitter,
|
||||
Inject,
|
||||
Input,
|
||||
OnChanges,
|
||||
Output,
|
||||
SimpleChanges,
|
||||
} from '@angular/core';
|
||||
import { partnersWithRedirect } from '@modules/components/partners-redirect-note/partners-redirect-note.component';
|
||||
import {
|
||||
FlightRequestType,
|
||||
ViewType,
|
||||
} from '@shared/enumerators/flight-request-type.enum';
|
||||
import { AppSettings } from '@shared/models-legacy';
|
||||
import { FlightModel, ListItem } from '@shared/models-legacy/flight.model';
|
||||
import { APP_SETTINGS } from '@shared/services';
|
||||
import { FeatureFlagsService } from '@shared/services/feature-flags.service';
|
||||
import { IUrlTimeRange } from '@typings/common/url';
|
||||
import { ISimpleFlight } from '@typings/flight/flight';
|
||||
import { findClosestFlight } from './utils/find-closest-flight';
|
||||
|
||||
@Component({
|
||||
selector: 'online-board-search',
|
||||
templateUrl: './online-board-search.component.html',
|
||||
styleUrls: ['./online-board-search.component.scss'],
|
||||
})
|
||||
export class OnlineBoardSearchComponent implements OnChanges {
|
||||
@Input() flights: ListItem<FlightModel>[] = [];
|
||||
@Input() partners: string[] = [];
|
||||
@Input() searchDate: Date;
|
||||
@Input() loading = false;
|
||||
@Input() requestType: FlightRequestType;
|
||||
@Input() timeFrom: string;
|
||||
@Input() timeTo: string;
|
||||
@Input() withScrollUp = true;
|
||||
@Input() disabledDates: Date[];
|
||||
|
||||
@Output() requestCancel = new EventEmitter<boolean>();
|
||||
@Output() searchDateChange = new EventEmitter<Date>();
|
||||
@Output() timeRangeChange = new EventEmitter<IUrlTimeRange>();
|
||||
@Output() toDetails = new EventEmitter<ISimpleFlight>();
|
||||
|
||||
timeRange: IUrlTimeRange;
|
||||
currentFlight: ListItem<FlightModel>;
|
||||
|
||||
FlightRequestType = FlightRequestType;
|
||||
ViewType = ViewType;
|
||||
|
||||
constructor(
|
||||
public featureFlags: FeatureFlagsService,
|
||||
@Inject(APP_SETTINGS) public settings: AppSettings,
|
||||
) {}
|
||||
|
||||
ngOnChanges(changes: SimpleChanges) {
|
||||
if ('flights' in changes) {
|
||||
if ('loading' in changes) {
|
||||
// Queue new macrotasks to let browser rerender after
|
||||
// creating flights (long task - about 60ms).
|
||||
// After removing populate.logic.ts setTimeout can be removed
|
||||
setTimeout(() => {
|
||||
this.setCurrentFlight(this.flights); // about 16ms - if not queued drops frames
|
||||
});
|
||||
} else {
|
||||
//console.log("Got autorefresh flights");
|
||||
//console.log(this.currentFlight);
|
||||
setTimeout(() => {
|
||||
if (this.currentFlight) {
|
||||
this.flights.forEach((flight) => {
|
||||
if (flight.id === this.currentFlight.id) {
|
||||
flight.expanded = this.currentFlight.expanded;
|
||||
this.currentFlight = flight;
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if ('partners' in changes) {
|
||||
setTimeout(() => {
|
||||
this.partners = partnersWithRedirect(this.partners);
|
||||
});
|
||||
}
|
||||
|
||||
if ('timeFrom' in changes || 'timeTo' in changes) {
|
||||
this.setTimeRange();
|
||||
}
|
||||
}
|
||||
|
||||
setCurrentFlight(flights?: ListItem<FlightModel>[]) {
|
||||
if (!this.flights) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.currentFlight) {
|
||||
this.currentFlight.expanded = false;
|
||||
}
|
||||
|
||||
this.currentFlight = findClosestFlight(
|
||||
flights,
|
||||
this.searchDate,
|
||||
this.requestType === FlightRequestType.Arrival,
|
||||
);
|
||||
|
||||
if (!this.currentFlight) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.currentFlight.expanded = true;
|
||||
}
|
||||
|
||||
setTimeRange() {
|
||||
if (!this.timeFrom || !this.timeTo) {
|
||||
this.timeRange = {};
|
||||
return;
|
||||
}
|
||||
|
||||
this.timeRange = {
|
||||
timeFrom: this.timeFrom,
|
||||
timeTo: this.timeTo,
|
||||
};
|
||||
}
|
||||
|
||||
handleDateChanged(date: Date) {
|
||||
this.searchDateChange.emit(date);
|
||||
}
|
||||
|
||||
handleTimeChanged() {
|
||||
this.timeRangeChange.emit(this.timeRange);
|
||||
}
|
||||
|
||||
handleToDetailsEvent(flight: ISimpleFlight) {
|
||||
this.toDetails.emit(flight);
|
||||
}
|
||||
|
||||
handleChangeCurrentFlightEvent(flight: ListItem<FlightModel>) {
|
||||
this.currentFlight = flight;
|
||||
//console.log("change current flight");
|
||||
//console.log(this.currentFlight);
|
||||
}
|
||||
|
||||
cancel() {
|
||||
this.requestCancel.emit(true);
|
||||
}
|
||||
}
|
||||
+565
@@ -0,0 +1,565 @@
|
||||
import { findClosestFlight } from '@online-board/pages/search/components/view/utils/find-closest-flight';
|
||||
|
||||
const mockedFlights = {
|
||||
yesterday: [
|
||||
{
|
||||
routeType: 'Direct',
|
||||
leg: {
|
||||
arrival: {
|
||||
times: {
|
||||
actual: {
|
||||
local: '2021-12-16T21:48:00',
|
||||
},
|
||||
},
|
||||
},
|
||||
departure: {
|
||||
times: {
|
||||
actual: {
|
||||
local: '2021-12-16T21:48:00',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
routeType: 'Direct',
|
||||
leg: {
|
||||
arrival: {
|
||||
times: {
|
||||
actual: {
|
||||
local: '2021-12-17T04:05:00',
|
||||
},
|
||||
},
|
||||
},
|
||||
departure: {
|
||||
times: {
|
||||
actual: {
|
||||
local: '2021-12-17T04:05:00',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
routeType: 'Direct',
|
||||
leg: {
|
||||
arrival: {
|
||||
times: {
|
||||
actual: {
|
||||
local: '2021-12-17T06:12:00',
|
||||
},
|
||||
},
|
||||
},
|
||||
departure: {
|
||||
times: {
|
||||
actual: {
|
||||
local: '2021-12-17T06:12:00',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
routeType: 'Direct',
|
||||
leg: {
|
||||
arrival: {
|
||||
times: {
|
||||
actual: {
|
||||
local: '2021-12-17T06:57:00',
|
||||
},
|
||||
},
|
||||
},
|
||||
departure: {
|
||||
times: {
|
||||
actual: {
|
||||
local: '2021-12-17T06:57:00',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
routeType: 'Direct',
|
||||
leg: {
|
||||
arrival: {
|
||||
times: {
|
||||
actual: {
|
||||
local: '2021-12-17T09:16:00',
|
||||
},
|
||||
},
|
||||
},
|
||||
departure: {
|
||||
times: {
|
||||
actual: {
|
||||
local: '2021-12-17T09:16:00',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
routeType: 'Direct',
|
||||
leg: {
|
||||
arrival: {
|
||||
times: {
|
||||
actual: {
|
||||
local: '2021-12-17T13:29:00',
|
||||
},
|
||||
},
|
||||
},
|
||||
departure: {
|
||||
times: {
|
||||
actual: {
|
||||
local: '2021-12-17T13:29:00',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
routeType: 'Direct',
|
||||
leg: {
|
||||
arrival: {
|
||||
times: {
|
||||
actual: {
|
||||
local: '2021-12-17T16:35:00',
|
||||
},
|
||||
},
|
||||
},
|
||||
departure: {
|
||||
times: {
|
||||
actual: {
|
||||
local: '2021-12-17T16:35:00',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
routeType: 'Direct',
|
||||
leg: {
|
||||
arrival: {
|
||||
times: {
|
||||
actual: {
|
||||
local: '2021-12-17T18:05:00',
|
||||
},
|
||||
},
|
||||
},
|
||||
departure: {
|
||||
times: {
|
||||
actual: {
|
||||
local: '2021-12-17T18:05:00',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
today: [
|
||||
{
|
||||
routeType: 'Direct',
|
||||
leg: {
|
||||
arrival: {
|
||||
times: {
|
||||
actual: {
|
||||
local: '2021-12-17T21:48:00',
|
||||
},
|
||||
scheduled: {
|
||||
local: '2021-12-17T21:40:00',
|
||||
},
|
||||
},
|
||||
},
|
||||
departure: {
|
||||
times: {
|
||||
actual: {
|
||||
local: '2021-12-17T21:48:00',
|
||||
},
|
||||
scheduled: {
|
||||
local: '2021-12-17T21:40:00',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
routeType: 'Direct',
|
||||
leg: {
|
||||
arrival: {
|
||||
times: {
|
||||
scheduled: {
|
||||
local: '2021-12-18T04:05:00',
|
||||
},
|
||||
},
|
||||
},
|
||||
departure: {
|
||||
times: {
|
||||
scheduled: {
|
||||
local: '2021-12-18T04:05:00',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
routeType: 'Direct',
|
||||
leg: {
|
||||
arrival: {
|
||||
times: {
|
||||
actual: {
|
||||
local: '2021-12-18T06:12:00',
|
||||
},
|
||||
scheduled: {
|
||||
local: '2021-12-18T05:55:00',
|
||||
},
|
||||
},
|
||||
},
|
||||
departure: {
|
||||
times: {
|
||||
actual: {
|
||||
local: '2021-12-18T06:12:00',
|
||||
},
|
||||
scheduled: {
|
||||
local: '2021-12-18T05:55:00',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
routeType: 'Direct',
|
||||
leg: {
|
||||
arrival: {
|
||||
times: {
|
||||
actual: {
|
||||
local: '2021-12-18T06:57:00',
|
||||
},
|
||||
scheduled: {
|
||||
local: '2021-12-18T07:00:00',
|
||||
},
|
||||
},
|
||||
},
|
||||
departure: {
|
||||
times: {
|
||||
actual: {
|
||||
local: '2021-12-18T06:57:00',
|
||||
},
|
||||
scheduled: {
|
||||
local: '2021-12-18T07:00:00',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
routeType: 'Direct',
|
||||
leg: {
|
||||
arrival: {
|
||||
times: {
|
||||
actual: {
|
||||
local: '2021-12-18T09:16:00',
|
||||
},
|
||||
scheduled: {
|
||||
local: '2021-12-18T09:05:00',
|
||||
},
|
||||
},
|
||||
},
|
||||
departure: {
|
||||
times: {
|
||||
actual: {
|
||||
local: '2021-12-18T09:16:00',
|
||||
},
|
||||
scheduled: {
|
||||
local: '2021-12-18T09:05:00',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
routeType: 'Direct',
|
||||
leg: {
|
||||
arrival: {
|
||||
times: {
|
||||
actual: {
|
||||
local: '2021-12-18T13:29:00',
|
||||
},
|
||||
scheduled: {
|
||||
local: '2021-12-18T13:10:00',
|
||||
},
|
||||
},
|
||||
},
|
||||
departure: {
|
||||
times: {
|
||||
actual: {
|
||||
local: '2021-12-18T13:29:00',
|
||||
},
|
||||
scheduled: {
|
||||
local: '2021-12-18T13:10:00',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
routeType: 'Direct',
|
||||
leg: {
|
||||
arrival: {
|
||||
times: {
|
||||
scheduled: {
|
||||
local: '2021-12-18T16:35:00',
|
||||
},
|
||||
},
|
||||
},
|
||||
departure: {
|
||||
times: {
|
||||
scheduled: {
|
||||
local: '2021-12-18T16:35:00',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
routeType: 'Direct',
|
||||
leg: {
|
||||
arrival: {
|
||||
times: {
|
||||
scheduled: {
|
||||
local: '2021-12-18T18:05:00',
|
||||
},
|
||||
},
|
||||
},
|
||||
departure: {
|
||||
times: {
|
||||
scheduled: {
|
||||
local: '2021-12-18T18:05:00',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
tomorrow: [
|
||||
{
|
||||
routeType: 'Direct',
|
||||
leg: {
|
||||
arrival: {
|
||||
times: {
|
||||
scheduled: {
|
||||
local: '2021-12-18T21:40:00',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
routeType: 'Direct',
|
||||
leg: {
|
||||
arrival: {
|
||||
times: {
|
||||
scheduled: {
|
||||
local: '2021-12-19T04:05:00',
|
||||
},
|
||||
},
|
||||
},
|
||||
departure: {
|
||||
times: {
|
||||
scheduled: {
|
||||
local: '2021-12-19T04:05:00',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
routeType: 'Direct',
|
||||
leg: {
|
||||
arrival: {
|
||||
times: {
|
||||
scheduled: {
|
||||
local: '2021-12-19T05:55:00',
|
||||
},
|
||||
},
|
||||
},
|
||||
departure: {
|
||||
times: {
|
||||
scheduled: {
|
||||
local: '2021-12-19T05:55:00',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
routeType: 'Direct',
|
||||
leg: {
|
||||
arrival: {
|
||||
times: {
|
||||
scheduled: {
|
||||
local: '2021-12-19T07:00:00',
|
||||
},
|
||||
},
|
||||
},
|
||||
departure: {
|
||||
times: {
|
||||
scheduled: {
|
||||
local: '2021-12-19T07:00:00',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
routeType: 'Direct',
|
||||
leg: {
|
||||
arrival: {
|
||||
times: {
|
||||
scheduled: {
|
||||
local: '2021-12-19T09:05:00',
|
||||
},
|
||||
},
|
||||
},
|
||||
departure: {
|
||||
times: {
|
||||
scheduled: {
|
||||
local: '2021-12-19T09:05:00',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
routeType: 'Direct',
|
||||
leg: {
|
||||
arrival: {
|
||||
times: {
|
||||
scheduled: {
|
||||
local: '2021-12-19T13:10:00',
|
||||
},
|
||||
},
|
||||
},
|
||||
departure: {
|
||||
times: {
|
||||
scheduled: {
|
||||
local: '2021-12-19T13:10:00',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
routeType: 'Direct',
|
||||
leg: {
|
||||
arrival: {
|
||||
times: {
|
||||
scheduled: {
|
||||
local: '2021-12-19T16:35:00',
|
||||
},
|
||||
},
|
||||
},
|
||||
departure: {
|
||||
times: {
|
||||
scheduled: {
|
||||
local: '2021-12-19T16:35:00',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
routeType: 'Direct',
|
||||
leg: {
|
||||
arrival: {
|
||||
times: {
|
||||
scheduled: {
|
||||
local: '2021-12-19T18:05:00',
|
||||
},
|
||||
},
|
||||
},
|
||||
departure: {
|
||||
times: {
|
||||
scheduled: {
|
||||
local: '2021-12-19T18:05:00',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
describe('findClosestFlight', () => {
|
||||
beforeEach(() => {
|
||||
jasmine.clock().install();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jasmine.clock().uninstall();
|
||||
});
|
||||
|
||||
it('should select the most closest future flight for today', () => {
|
||||
mockDate('2021-12-18T15:26:28');
|
||||
const flights = mockedFlights.today as any;
|
||||
const flight = findClosestFlight(flights, new Date('2021-12-18'), true);
|
||||
|
||||
expect(flight).toEqual(flights[6]);
|
||||
});
|
||||
|
||||
it('should select the most closest future flight for today if isArrival is false', () => {
|
||||
mockDate('2021-12-18T15:26:28');
|
||||
const flights = mockedFlights.today as any;
|
||||
const flight = findClosestFlight(
|
||||
flights,
|
||||
new Date('2021-12-18'),
|
||||
false,
|
||||
);
|
||||
|
||||
expect(flight).toEqual(flights[6]);
|
||||
});
|
||||
|
||||
it('should select the most closest completed flight for today', () => {
|
||||
mockDate('2021-12-18T13:46:28');
|
||||
const flights = mockedFlights.today as any;
|
||||
const flight = findClosestFlight(flights, new Date('2021-12-18'), true);
|
||||
|
||||
expect(flight).toEqual(flights[5]);
|
||||
});
|
||||
|
||||
it('should select last flight', () => {
|
||||
mockDate('2021-12-18T19:46:28');
|
||||
const flights = mockedFlights.today as any;
|
||||
const flight = findClosestFlight(flights, new Date('2021-12-18'), true);
|
||||
|
||||
expect(flight).toEqual(flights[7]);
|
||||
});
|
||||
|
||||
it('should select the first for tomorrow', () => {
|
||||
mockDate('2021-12-18T15:26:28');
|
||||
const flights = mockedFlights.tomorrow as any;
|
||||
const flight = findClosestFlight(flights, new Date('2021-12-17'), true);
|
||||
|
||||
expect(flight).toEqual(flights[0]);
|
||||
});
|
||||
|
||||
it('should select the last for yesterday', () => {
|
||||
mockDate('2021-12-18T15:26:28');
|
||||
const flights = mockedFlights.yesterday as any;
|
||||
const flight = findClosestFlight(flights, new Date('2021-12-19'), true);
|
||||
|
||||
expect(flight).toEqual(flights[7]);
|
||||
});
|
||||
|
||||
it('should return undefined if no flights were provided', () => {
|
||||
expect(findClosestFlight([], new Date('2021-12-19'), true)).toBe(
|
||||
undefined,
|
||||
);
|
||||
expect(findClosestFlight(undefined, new Date('2021-12-19'), true)).toBe(
|
||||
undefined,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
function mockDate(date: string) {
|
||||
const currentDate = new Date(date);
|
||||
jasmine.clock().mockDate(currentDate);
|
||||
}
|
||||
+65
@@ -0,0 +1,65 @@
|
||||
import { Moment } from 'moment';
|
||||
import * as moment from 'moment';
|
||||
import { FlightModel, getFirstLeg, getLastLeg } from '@shared/models-legacy';
|
||||
|
||||
export function findClosestFlight(
|
||||
flights: FlightModel[] = [],
|
||||
flightDate: moment.MomentInput,
|
||||
isArrival: boolean,
|
||||
) {
|
||||
if (!flights.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
const searchForToday = moment().isSame(flightDate, 'day');
|
||||
|
||||
return searchForToday
|
||||
? findClosestFlightForToday(flights, isArrival)
|
||||
: findClosestFlightNotForToday(flights, isArrival);
|
||||
}
|
||||
|
||||
function findClosestFlightForToday(flights: FlightModel[], isArrival: boolean) {
|
||||
const now = moment();
|
||||
let min = Number.MAX_VALUE;
|
||||
let candidate: FlightModel = flights[0];
|
||||
|
||||
flights.forEach((flight) => {
|
||||
const diff = getDiffFromNow(flight, now, isArrival);
|
||||
|
||||
// It is necessary to call Math.abs with min
|
||||
// because min can be negative too
|
||||
if (Math.abs(diff) < Math.abs(min)) {
|
||||
min = diff;
|
||||
candidate = flight;
|
||||
}
|
||||
});
|
||||
|
||||
return candidate;
|
||||
}
|
||||
|
||||
function findClosestFlightNotForToday(
|
||||
flights: FlightModel[],
|
||||
isArrival: boolean,
|
||||
) {
|
||||
const firstFlight = flights[0];
|
||||
const lastFlight = flights[flights.length - 1];
|
||||
|
||||
const firstFlightTime = getFlightTime(firstFlight, isArrival);
|
||||
|
||||
// For tomorrow returns first flight for yesterday - last
|
||||
return moment().isBefore(firstFlightTime) ? firstFlight : lastFlight;
|
||||
}
|
||||
|
||||
function getDiffFromNow(flight: FlightModel, now: Moment, isArrival: boolean) {
|
||||
const time = getFlightTime(flight, isArrival);
|
||||
|
||||
return moment(time).diff(now);
|
||||
}
|
||||
|
||||
function getFlightTime(flight: FlightModel, isArrival: boolean) {
|
||||
const times = isArrival
|
||||
? getLastLeg(flight).arrival.times
|
||||
: getFirstLeg(flight).departure.times;
|
||||
|
||||
return (times.actual ?? times.latest ?? times.scheduled).local;
|
||||
}
|
||||
+1
@@ -0,0 +1 @@
|
||||
<meta-tags [title]="title" [description]="description"></meta-tags>
|
||||
+74
@@ -0,0 +1,74 @@
|
||||
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { OnlineBoardArrivalMetaTagsComponent } from '@online-board/pages/search/arrival/components/meta-tags/online-board-arrival-meta-tags.component';
|
||||
import { OnlineBoardDepartureMetaTagsComponent } from '@online-board/pages/search/departure/components/meta-tags/online-board-departure-meta-tags.component';
|
||||
import { OnlineBoardMetaTagsService } from '@online-board/services/meta-tags.service';
|
||||
import { IOnlineBoardRouteData } from '@online-board/types';
|
||||
import { BehaviorSubject } from 'rxjs';
|
||||
|
||||
describe('OnlineBoardDepartureMetaTagsComponent', () => {
|
||||
let component: OnlineBoardDepartureMetaTagsComponent;
|
||||
let fixture: ComponentFixture<OnlineBoardDepartureMetaTagsComponent>;
|
||||
const getDepartureMetaTagsMock = jasmine.createSpy('getDepartureMetaTags');
|
||||
const data: IOnlineBoardRouteData = {
|
||||
urlParams: {},
|
||||
};
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [OnlineBoardArrivalMetaTagsComponent],
|
||||
providers: [
|
||||
{
|
||||
provide: OnlineBoardMetaTagsService,
|
||||
useValue: {
|
||||
getDepartureMetaTags: getDepartureMetaTagsMock,
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: ActivatedRoute,
|
||||
useValue: {
|
||||
data: new BehaviorSubject(data),
|
||||
},
|
||||
},
|
||||
],
|
||||
schemas: [NO_ERRORS_SCHEMA],
|
||||
}).compileComponents();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(
|
||||
OnlineBoardDepartureMetaTagsComponent,
|
||||
);
|
||||
component = fixture.componentInstance;
|
||||
|
||||
data.urlParams.route = undefined;
|
||||
getDepartureMetaTagsMock.calls.reset();
|
||||
});
|
||||
|
||||
it('should not set title and description because route param is not provided', () => {
|
||||
component.ngOnInit();
|
||||
|
||||
expect(component.title).toBe('');
|
||||
expect(component.description).toBe('');
|
||||
});
|
||||
|
||||
it('should set title and description', async () => {
|
||||
data.urlParams.route = {
|
||||
departure: 'MOW',
|
||||
date: new Date(2022, 4, 28),
|
||||
};
|
||||
|
||||
getDepartureMetaTagsMock.withArgs(data.urlParams.route).and.returnValue(
|
||||
Promise.resolve({
|
||||
title: 'departure title',
|
||||
description: 'departure description',
|
||||
}),
|
||||
);
|
||||
component.ngOnInit();
|
||||
await Promise.resolve();
|
||||
|
||||
expect(component.title).toBe('departure title');
|
||||
expect(component.description).toBe('departure description');
|
||||
});
|
||||
});
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { OnlineBoardMetaTagsService } from '@online-board/services/meta-tags.service';
|
||||
import { IOnlineBoardRouteData } from '@online-board/types';
|
||||
|
||||
@Component({
|
||||
selector: 'online-board-departure-meta-tags',
|
||||
templateUrl: './online-board-departure-meta-tags.component.html',
|
||||
})
|
||||
export class OnlineBoardDepartureMetaTagsComponent implements OnInit {
|
||||
title = '';
|
||||
description = '';
|
||||
|
||||
constructor(
|
||||
private route: ActivatedRoute,
|
||||
private metaTagsService: OnlineBoardMetaTagsService,
|
||||
) {}
|
||||
|
||||
ngOnInit() {
|
||||
this.route.data.subscribe(async (data: IOnlineBoardRouteData) => {
|
||||
if (data.urlParams.route) {
|
||||
const metaTags =
|
||||
await this.metaTagsService.getDepartureMetaTags(
|
||||
data.urlParams.route,
|
||||
);
|
||||
|
||||
this.title = metaTags.title;
|
||||
this.description = metaTags.description;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
+1
@@ -0,0 +1 @@
|
||||
<aero-title [title]="title"></aero-title>
|
||||
+66
@@ -0,0 +1,66 @@
|
||||
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { OnlineBoardDepartureTitleComponent } from '@online-board/pages/search/departure/components/title/online-board-departure-title.component';
|
||||
import { OnlineBoardTitleService } from '@online-board/services/title.service';
|
||||
import { IOnlineBoardRouteData } from '@online-board/types';
|
||||
import { BehaviorSubject } from 'rxjs';
|
||||
|
||||
describe('OnlineBoardDepartureTitleComponent', () => {
|
||||
let component: OnlineBoardDepartureTitleComponent;
|
||||
let fixture: ComponentFixture<OnlineBoardDepartureTitleComponent>;
|
||||
const getDepartureTitleMock = jasmine.createSpy('getDepartureTitle');
|
||||
const data: IOnlineBoardRouteData = {
|
||||
urlParams: {},
|
||||
};
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [OnlineBoardDepartureTitleComponent],
|
||||
providers: [
|
||||
{
|
||||
provide: OnlineBoardTitleService,
|
||||
useValue: {
|
||||
getDepartureTitle: getDepartureTitleMock,
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: ActivatedRoute,
|
||||
useValue: {
|
||||
data: new BehaviorSubject(data),
|
||||
},
|
||||
},
|
||||
],
|
||||
schemas: [NO_ERRORS_SCHEMA],
|
||||
}).compileComponents();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(OnlineBoardDepartureTitleComponent);
|
||||
component = fixture.componentInstance;
|
||||
|
||||
data.urlParams.route = undefined;
|
||||
getDepartureTitleMock.calls.reset();
|
||||
});
|
||||
|
||||
it('should not set title because route param is not provided', () => {
|
||||
component.ngOnInit();
|
||||
|
||||
expect(component.title).toBe('');
|
||||
});
|
||||
|
||||
it('should set title', async () => {
|
||||
data.urlParams.route = {
|
||||
departure: 'MOW',
|
||||
date: new Date(2022, 4, 28),
|
||||
};
|
||||
|
||||
getDepartureTitleMock
|
||||
.withArgs(data.urlParams.route)
|
||||
.and.returnValue(Promise.resolve('departure title'));
|
||||
component.ngOnInit();
|
||||
await Promise.resolve();
|
||||
|
||||
expect(component.title).toBe('departure title');
|
||||
});
|
||||
});
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { OnlineBoardTitleService } from '@online-board/services/title.service';
|
||||
import { IOnlineBoardRouteData } from '@online-board/types';
|
||||
|
||||
@Component({
|
||||
selector: 'online-board-departure-title',
|
||||
templateUrl: './online-board-departure-title.component.html',
|
||||
})
|
||||
export class OnlineBoardDepartureTitleComponent implements OnInit {
|
||||
title = '';
|
||||
|
||||
constructor(
|
||||
private route: ActivatedRoute,
|
||||
private titleService: OnlineBoardTitleService,
|
||||
) {}
|
||||
|
||||
ngOnInit() {
|
||||
this.route.data.subscribe({
|
||||
next: async (data: IOnlineBoardRouteData) => {
|
||||
if (data.urlParams.route) {
|
||||
this.title = await this.titleService.getDepartureTitle(
|
||||
data.urlParams.route,
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
<online-board-departure-meta-tags></online-board-departure-meta-tags>
|
||||
<online-board-search-page-base [requestType]="FlightRequestType.Departure">
|
||||
<online-board-departure-title title></online-board-departure-title>
|
||||
</online-board-search-page-base>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user