mirror of https://gitlab.com/m3f_usm/SmartStopAPK
feat: calls to services and the construction of the UI are incorporated
parent
b66e4e19bc
commit
1541036acc
118
App.tsx
118
App.tsx
|
@ -1,118 +0,0 @@
|
||||||
/**
|
|
||||||
* Sample React Native App
|
|
||||||
* https://github.com/facebook/react-native
|
|
||||||
*
|
|
||||||
* @format
|
|
||||||
*/
|
|
||||||
|
|
||||||
import React from 'react';
|
|
||||||
import type {PropsWithChildren} from 'react';
|
|
||||||
import {
|
|
||||||
SafeAreaView,
|
|
||||||
ScrollView,
|
|
||||||
StatusBar,
|
|
||||||
StyleSheet,
|
|
||||||
Text,
|
|
||||||
useColorScheme,
|
|
||||||
View,
|
|
||||||
} from 'react-native';
|
|
||||||
|
|
||||||
import {
|
|
||||||
Colors,
|
|
||||||
DebugInstructions,
|
|
||||||
Header,
|
|
||||||
LearnMoreLinks,
|
|
||||||
ReloadInstructions,
|
|
||||||
} from 'react-native/Libraries/NewAppScreen';
|
|
||||||
|
|
||||||
type SectionProps = PropsWithChildren<{
|
|
||||||
title: string;
|
|
||||||
}>;
|
|
||||||
|
|
||||||
function Section({children, title}: SectionProps): JSX.Element {
|
|
||||||
const isDarkMode = useColorScheme() === 'dark';
|
|
||||||
return (
|
|
||||||
<View style={styles.sectionContainer}>
|
|
||||||
<Text
|
|
||||||
style={[
|
|
||||||
styles.sectionTitle,
|
|
||||||
{
|
|
||||||
color: isDarkMode ? Colors.white : Colors.black,
|
|
||||||
},
|
|
||||||
]}>
|
|
||||||
{title}
|
|
||||||
</Text>
|
|
||||||
<Text
|
|
||||||
style={[
|
|
||||||
styles.sectionDescription,
|
|
||||||
{
|
|
||||||
color: isDarkMode ? Colors.light : Colors.dark,
|
|
||||||
},
|
|
||||||
]}>
|
|
||||||
{children}
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function App(): JSX.Element {
|
|
||||||
const isDarkMode = useColorScheme() === 'dark';
|
|
||||||
|
|
||||||
const backgroundStyle = {
|
|
||||||
backgroundColor: isDarkMode ? Colors.darker : Colors.lighter,
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<SafeAreaView style={backgroundStyle}>
|
|
||||||
<StatusBar
|
|
||||||
barStyle={isDarkMode ? 'light-content' : 'dark-content'}
|
|
||||||
backgroundColor={backgroundStyle.backgroundColor}
|
|
||||||
/>
|
|
||||||
<ScrollView
|
|
||||||
contentInsetAdjustmentBehavior="automatic"
|
|
||||||
style={backgroundStyle}>
|
|
||||||
<Header />
|
|
||||||
<View
|
|
||||||
style={{
|
|
||||||
backgroundColor: isDarkMode ? Colors.black : Colors.white,
|
|
||||||
}}>
|
|
||||||
<Section title="Step One">
|
|
||||||
Edit <Text style={styles.highlight}>App.tsx</Text> to change this
|
|
||||||
screen and then come back to see your edits.
|
|
||||||
</Section>
|
|
||||||
<Section title="See Your Changes">
|
|
||||||
<ReloadInstructions />
|
|
||||||
</Section>
|
|
||||||
<Section title="Debug">
|
|
||||||
<DebugInstructions />
|
|
||||||
</Section>
|
|
||||||
<Section title="Learn More">
|
|
||||||
Read the docs to discover what to do next:
|
|
||||||
</Section>
|
|
||||||
<LearnMoreLinks />
|
|
||||||
</View>
|
|
||||||
</ScrollView>
|
|
||||||
</SafeAreaView>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
|
||||||
sectionContainer: {
|
|
||||||
marginTop: 32,
|
|
||||||
paddingHorizontal: 24,
|
|
||||||
},
|
|
||||||
sectionTitle: {
|
|
||||||
fontSize: 24,
|
|
||||||
fontWeight: '600',
|
|
||||||
},
|
|
||||||
sectionDescription: {
|
|
||||||
marginTop: 8,
|
|
||||||
fontSize: 18,
|
|
||||||
fontWeight: '400',
|
|
||||||
},
|
|
||||||
highlight: {
|
|
||||||
fontWeight: '700',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
export default App;
|
|
|
@ -1,17 +0,0 @@
|
||||||
/**
|
|
||||||
* @format
|
|
||||||
*/
|
|
||||||
|
|
||||||
import 'react-native';
|
|
||||||
import React from 'react';
|
|
||||||
import App from '../App';
|
|
||||||
|
|
||||||
// Note: import explicitly to use the types shiped with jest.
|
|
||||||
import {it} from '@jest/globals';
|
|
||||||
|
|
||||||
// Note: test renderer must be required after react-native.
|
|
||||||
import renderer from 'react-test-renderer';
|
|
||||||
|
|
||||||
it('renders correctly', () => {
|
|
||||||
renderer.create(<App />);
|
|
||||||
});
|
|
|
@ -1,6 +1,11 @@
|
||||||
apply plugin: "com.android.application"
|
apply plugin: "com.android.application"
|
||||||
apply plugin: "com.facebook.react"
|
apply plugin: "com.facebook.react"
|
||||||
|
|
||||||
|
project.ext.vectoricons = [
|
||||||
|
iconFontNames: ['MaterialCommunityIcons.ttf']
|
||||||
|
]
|
||||||
|
apply from: file("../../node_modules/react-native-vector-icons/fonts.gradle")
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This is the configuration block to customize your React Native Android app.
|
* This is the configuration block to customize your React Native Android app.
|
||||||
* By default you don't need to apply any configuration, just uncomment the lines you need.
|
* By default you don't need to apply any configuration, just uncomment the lines you need.
|
||||||
|
|
2
index.js
2
index.js
|
@ -3,7 +3,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {AppRegistry} from 'react-native';
|
import {AppRegistry} from 'react-native';
|
||||||
import App from './App';
|
|
||||||
import {name as appName} from './app.json';
|
import {name as appName} from './app.json';
|
||||||
|
import App from './src/presentation/screens/App';
|
||||||
|
|
||||||
AppRegistry.registerComponent(appName, () => App);
|
AppRegistry.registerComponent(appName, () => App);
|
||||||
|
|
|
@ -14,6 +14,7 @@
|
||||||
13B07FC11A68108700A75B9A /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 13B07FB71A68108700A75B9A /* main.m */; };
|
13B07FC11A68108700A75B9A /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 13B07FB71A68108700A75B9A /* main.m */; };
|
||||||
7699B88040F8A987B510C191 /* libPods-SmartStop-SmartStopTests.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 19F6CBCC0A4E27FBF8BF4A61 /* libPods-SmartStop-SmartStopTests.a */; };
|
7699B88040F8A987B510C191 /* libPods-SmartStop-SmartStopTests.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 19F6CBCC0A4E27FBF8BF4A61 /* libPods-SmartStop-SmartStopTests.a */; };
|
||||||
81AB9BB82411601600AC10FF /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 81AB9BB72411601600AC10FF /* LaunchScreen.storyboard */; };
|
81AB9BB82411601600AC10FF /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 81AB9BB72411601600AC10FF /* LaunchScreen.storyboard */; };
|
||||||
|
904C36942B12DB0600B0C7C3 /* MaterialCommunityIcons.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 90BEE4642AFC5DCD0014196C /* MaterialCommunityIcons.ttf */; };
|
||||||
/* End PBXBuildFile section */
|
/* End PBXBuildFile section */
|
||||||
|
|
||||||
/* Begin PBXContainerItemProxy section */
|
/* Begin PBXContainerItemProxy section */
|
||||||
|
@ -43,6 +44,7 @@
|
||||||
5DCACB8F33CDC322A6C60F78 /* libPods-SmartStop.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-SmartStop.a"; sourceTree = BUILT_PRODUCTS_DIR; };
|
5DCACB8F33CDC322A6C60F78 /* libPods-SmartStop.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-SmartStop.a"; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
81AB9BB72411601600AC10FF /* LaunchScreen.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; name = LaunchScreen.storyboard; path = SmartStop/LaunchScreen.storyboard; sourceTree = "<group>"; };
|
81AB9BB72411601600AC10FF /* LaunchScreen.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; name = LaunchScreen.storyboard; path = SmartStop/LaunchScreen.storyboard; sourceTree = "<group>"; };
|
||||||
89C6BE57DB24E9ADA2F236DE /* Pods-SmartStop-SmartStopTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SmartStop-SmartStopTests.release.xcconfig"; path = "Target Support Files/Pods-SmartStop-SmartStopTests/Pods-SmartStop-SmartStopTests.release.xcconfig"; sourceTree = "<group>"; };
|
89C6BE57DB24E9ADA2F236DE /* Pods-SmartStop-SmartStopTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SmartStop-SmartStopTests.release.xcconfig"; path = "Target Support Files/Pods-SmartStop-SmartStopTests/Pods-SmartStop-SmartStopTests.release.xcconfig"; sourceTree = "<group>"; };
|
||||||
|
90BEE4642AFC5DCD0014196C /* MaterialCommunityIcons.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = MaterialCommunityIcons.ttf; sourceTree = "<group>"; };
|
||||||
ED297162215061F000B7C4FE /* JavaScriptCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = JavaScriptCore.framework; path = System/Library/Frameworks/JavaScriptCore.framework; sourceTree = SDKROOT; };
|
ED297162215061F000B7C4FE /* JavaScriptCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = JavaScriptCore.framework; path = System/Library/Frameworks/JavaScriptCore.framework; sourceTree = SDKROOT; };
|
||||||
/* End PBXFileReference section */
|
/* End PBXFileReference section */
|
||||||
|
|
||||||
|
@ -116,6 +118,7 @@
|
||||||
83CBB9F61A601CBA00E9B192 = {
|
83CBB9F61A601CBA00E9B192 = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
90BEE45D2AFC5DCD0014196C /* Fonts */,
|
||||||
13B07FAE1A68108700A75B9A /* SmartStop */,
|
13B07FAE1A68108700A75B9A /* SmartStop */,
|
||||||
832341AE1AAA6A7D00B99B32 /* Libraries */,
|
832341AE1AAA6A7D00B99B32 /* Libraries */,
|
||||||
00E356EF1AD99517003FC87E /* SmartStopTests */,
|
00E356EF1AD99517003FC87E /* SmartStopTests */,
|
||||||
|
@ -137,6 +140,15 @@
|
||||||
name = Products;
|
name = Products;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
|
90BEE45D2AFC5DCD0014196C /* Fonts */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
90BEE4642AFC5DCD0014196C /* MaterialCommunityIcons.ttf */,
|
||||||
|
);
|
||||||
|
name = Fonts;
|
||||||
|
path = "../node_modules/react-native-vector-icons/Fonts";
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
BBD78D7AC51CEA395F1C20DB /* Pods */ = {
|
BBD78D7AC51CEA395F1C20DB /* Pods */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
@ -244,6 +256,7 @@
|
||||||
files = (
|
files = (
|
||||||
81AB9BB82411601600AC10FF /* LaunchScreen.storyboard in Resources */,
|
81AB9BB82411601600AC10FF /* LaunchScreen.storyboard in Resources */,
|
||||||
13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */,
|
13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */,
|
||||||
|
904C36942B12DB0600B0C7C3 /* MaterialCommunityIcons.ttf in Resources */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
|
|
|
@ -0,0 +1,8 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>IDEDidComputeMac32BitWarning</key>
|
||||||
|
<true/>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
|
@ -2,6 +2,10 @@
|
||||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
<plist version="1.0">
|
<plist version="1.0">
|
||||||
<dict>
|
<dict>
|
||||||
|
<key>UIAppFonts</key>
|
||||||
|
<array>
|
||||||
|
<string>MaterialCommunityIcons.ttf</string>
|
||||||
|
</array>
|
||||||
<key>CFBundleDevelopmentRegion</key>
|
<key>CFBundleDevelopmentRegion</key>
|
||||||
<string>en</string>
|
<string>en</string>
|
||||||
<key>CFBundleDisplayName</key>
|
<key>CFBundleDisplayName</key>
|
||||||
|
|
|
@ -1,3 +0,0 @@
|
||||||
module.exports = {
|
|
||||||
preset: 'react-native',
|
|
||||||
};
|
|
|
@ -0,0 +1,10 @@
|
||||||
|
const esModules = [
|
||||||
|
'@react-native',
|
||||||
|
'react-native',
|
||||||
|
'react-native-vector-icons',
|
||||||
|
].join('|');
|
||||||
|
|
||||||
|
export default {
|
||||||
|
preset: 'react-native',
|
||||||
|
transformIgnorePatterns: [`node_modules/(?!${esModules})`],
|
||||||
|
};
|
13
package.json
13
package.json
|
@ -10,8 +10,12 @@
|
||||||
"test": "jest"
|
"test": "jest"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"axios": "^1.6.2",
|
||||||
|
"moment": "^2.29.4",
|
||||||
"react": "18.2.0",
|
"react": "18.2.0",
|
||||||
"react-native": "0.72.6"
|
"react-native": "0.72.6",
|
||||||
|
"react-native-device-info": "^10.11.0",
|
||||||
|
"react-native-vector-icons": "^10.0.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "^7.20.0",
|
"@babel/core": "^7.20.0",
|
||||||
|
@ -22,9 +26,14 @@
|
||||||
"@react-native-community/eslint-config": "^3.2.0",
|
"@react-native-community/eslint-config": "^3.2.0",
|
||||||
"@react-native/eslint-config": "^0.72.2",
|
"@react-native/eslint-config": "^0.72.2",
|
||||||
"@react-native/metro-config": "^0.72.11",
|
"@react-native/metro-config": "^0.72.11",
|
||||||
|
"@testing-library/react-native": "^12.4.1",
|
||||||
"@tsconfig/react-native": "^3.0.0",
|
"@tsconfig/react-native": "^3.0.0",
|
||||||
|
"@types/jest": "^29.5.8",
|
||||||
|
"@types/node": "^20.10.3",
|
||||||
"@types/react": "^18.0.24",
|
"@types/react": "^18.0.24",
|
||||||
|
"@types/react-native-vector-icons": "^6.4.17",
|
||||||
"@types/react-test-renderer": "^18.0.0",
|
"@types/react-test-renderer": "^18.0.0",
|
||||||
|
"axios-mock-adapter": "^1.22.0",
|
||||||
"babel-jest": "^29.2.1",
|
"babel-jest": "^29.2.1",
|
||||||
"eslint": "^8.19.0",
|
"eslint": "^8.19.0",
|
||||||
"husky": "^8.0.3",
|
"husky": "^8.0.3",
|
||||||
|
@ -32,6 +41,8 @@
|
||||||
"metro-react-native-babel-preset": "0.76.8",
|
"metro-react-native-babel-preset": "0.76.8",
|
||||||
"prettier": "^2.4.1",
|
"prettier": "^2.4.1",
|
||||||
"react-test-renderer": "18.2.0",
|
"react-test-renderer": "18.2.0",
|
||||||
|
"ts-node": "^10.9.1",
|
||||||
|
"tslib": "^2.6.2",
|
||||||
"typescript": "4.8.4"
|
"typescript": "4.8.4"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
|
|
|
@ -0,0 +1,9 @@
|
||||||
|
module.exports = {
|
||||||
|
dependencies: {
|
||||||
|
'react-native-vector-icons': {
|
||||||
|
platforms: {
|
||||||
|
ios: null,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
|
@ -0,0 +1,8 @@
|
||||||
|
interface Arrival {
|
||||||
|
carPlate: string;
|
||||||
|
planned: string;
|
||||||
|
estimatedGPS: string;
|
||||||
|
distanceGPS: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Arrival;
|
|
@ -0,0 +1,7 @@
|
||||||
|
import AuthRequest from '../../infraestructure/api/models/AuthRequest';
|
||||||
|
|
||||||
|
interface AuthRepository {
|
||||||
|
auth(request: AuthRequest): Promise<string>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AuthRepository;
|
|
@ -0,0 +1,10 @@
|
||||||
|
import DeviceRequest from '../../infraestructure/api/models/DeviceRequest';
|
||||||
|
import DeviceInfoResponse from '../../infraestructure/api/models/DeviceInfoResponse';
|
||||||
|
import WhoAmIResponse from './WhoAmIResponse';
|
||||||
|
|
||||||
|
interface DevicesRepository {
|
||||||
|
whoAmI(request: DeviceRequest): Promise<WhoAmIResponse>;
|
||||||
|
getDeviceInfo(request: DeviceRequest): Promise<DeviceInfoResponse>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default DevicesRepository;
|
|
@ -0,0 +1,13 @@
|
||||||
|
import Arrival from './Arrival';
|
||||||
|
|
||||||
|
interface LineDetail {
|
||||||
|
lineNumber: string;
|
||||||
|
description: string;
|
||||||
|
locomotionType: number;
|
||||||
|
backgroundColor: string;
|
||||||
|
letterColor: string;
|
||||||
|
lineMessage: string;
|
||||||
|
arrivals: Arrival[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export default LineDetail;
|
|
@ -0,0 +1,6 @@
|
||||||
|
interface WhoAmIResponse {
|
||||||
|
stopNumber: string;
|
||||||
|
stopName: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default WhoAmIResponse;
|
|
@ -0,0 +1,371 @@
|
||||||
|
import {jest} from '@jest/globals';
|
||||||
|
|
||||||
|
import BusStopInfoService from './BusStopInfoService';
|
||||||
|
|
||||||
|
const service = new BusStopInfoService([
|
||||||
|
{
|
||||||
|
lineNumber: '803010',
|
||||||
|
description: 'Tucapel',
|
||||||
|
locomotionType: 1,
|
||||||
|
backgroundColor: 'Hexadecimal',
|
||||||
|
letterColor: 'Hexadecimal',
|
||||||
|
lineMessage: '',
|
||||||
|
arrivals: [
|
||||||
|
{
|
||||||
|
carPlate: 'RPDA-98',
|
||||||
|
planned: '',
|
||||||
|
estimatedGPS: '15:08',
|
||||||
|
distanceGPS: '1.0 KM',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
carPlate: 'WYXYZ-22',
|
||||||
|
planned: '',
|
||||||
|
estimatedGPS: '15:42',
|
||||||
|
distanceGPS: '5.0 KM',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
carPlate: 'ABCA-65',
|
||||||
|
planned: '',
|
||||||
|
estimatedGPS: '17:18',
|
||||||
|
distanceGPS: '13.4 KM',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
lineNumber: '5487',
|
||||||
|
description: 'Centauro',
|
||||||
|
locomotionType: 1,
|
||||||
|
backgroundColor: 'Hexadecimal',
|
||||||
|
letterColor: 'Hexadecimal',
|
||||||
|
lineMessage: 'Sin info. GPS, la informacion es estimada',
|
||||||
|
arrivals: [
|
||||||
|
{
|
||||||
|
carPlate: 'PLKJ-32',
|
||||||
|
planned: '15:13',
|
||||||
|
estimatedGPS: '15:13',
|
||||||
|
distanceGPS: '',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
carPlate: 'GHLK-11',
|
||||||
|
planned: '15:39',
|
||||||
|
estimatedGPS: '15:39',
|
||||||
|
distanceGPS: '',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
carPlate: 'DFQW-55',
|
||||||
|
planned: '17:22',
|
||||||
|
estimatedGPS: '17:22',
|
||||||
|
distanceGPS: '',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
jest.useFakeTimers({legacyFakeTimers: false});
|
||||||
|
|
||||||
|
describe('BusStopInfoService tests', () => {
|
||||||
|
it('should be defined', () => {
|
||||||
|
expect(BusStopInfoService).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getBuses', () => {
|
||||||
|
it('should return a list of buses', () => {
|
||||||
|
const buses = service.getLines();
|
||||||
|
|
||||||
|
expect(buses).toHaveLength(2);
|
||||||
|
expect(buses).toMatchObject([
|
||||||
|
{
|
||||||
|
lineNumber: '803010',
|
||||||
|
description: 'Tucapel',
|
||||||
|
locomotionType: 1,
|
||||||
|
backgroundColor: 'Hexadecimal',
|
||||||
|
letterColor: 'Hexadecimal',
|
||||||
|
lineMessage: '',
|
||||||
|
arrivals: [
|
||||||
|
{
|
||||||
|
carPlate: 'RPDA-98',
|
||||||
|
planned: '',
|
||||||
|
estimatedGPS: '15:08',
|
||||||
|
distanceGPS: '1.0 KM',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
carPlate: 'WYXYZ-22',
|
||||||
|
planned: '',
|
||||||
|
estimatedGPS: '15:42',
|
||||||
|
distanceGPS: '5.0 KM',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
carPlate: 'ABCA-65',
|
||||||
|
planned: '',
|
||||||
|
estimatedGPS: '17:18',
|
||||||
|
distanceGPS: '13.4 KM',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
lineNumber: '5487',
|
||||||
|
description: 'Centauro',
|
||||||
|
locomotionType: 1,
|
||||||
|
backgroundColor: 'Hexadecimal',
|
||||||
|
letterColor: 'Hexadecimal',
|
||||||
|
lineMessage: 'Sin info. GPS, la informacion es estimada',
|
||||||
|
arrivals: [
|
||||||
|
{
|
||||||
|
carPlate: 'PLKJ-32',
|
||||||
|
planned: '15:13',
|
||||||
|
estimatedGPS: '15:13',
|
||||||
|
distanceGPS: '',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
carPlate: 'GHLK-11',
|
||||||
|
planned: '15:39',
|
||||||
|
estimatedGPS: '15:39',
|
||||||
|
distanceGPS: '',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
carPlate: 'DFQW-55',
|
||||||
|
planned: '17:22',
|
||||||
|
estimatedGPS: '17:22',
|
||||||
|
distanceGPS: '',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return a bus by lineNumber', () => {
|
||||||
|
const bus = service.getLinesByNumber('803010');
|
||||||
|
|
||||||
|
expect(bus).toMatchObject({
|
||||||
|
lineNumber: '803010',
|
||||||
|
description: 'Tucapel',
|
||||||
|
locomotionType: 1,
|
||||||
|
backgroundColor: 'Hexadecimal',
|
||||||
|
letterColor: 'Hexadecimal',
|
||||||
|
lineMessage: '',
|
||||||
|
arrivals: [
|
||||||
|
{
|
||||||
|
carPlate: 'RPDA-98',
|
||||||
|
planned: '',
|
||||||
|
estimatedGPS: '15:08',
|
||||||
|
distanceGPS: '1.0 KM',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
carPlate: 'WYXYZ-22',
|
||||||
|
planned: '',
|
||||||
|
estimatedGPS: '15:42',
|
||||||
|
distanceGPS: '5.0 KM',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
carPlate: 'ABCA-65',
|
||||||
|
planned: '',
|
||||||
|
estimatedGPS: '17:18',
|
||||||
|
distanceGPS: '13.4 KM',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('pruneBusList', () => {
|
||||||
|
it('should return a list of buses', () => {
|
||||||
|
const prunedList = service.pruneBusList(0, 3);
|
||||||
|
|
||||||
|
expect(prunedList).toHaveLength(2);
|
||||||
|
expect(prunedList).toMatchObject([
|
||||||
|
{
|
||||||
|
lineNumber: '803010',
|
||||||
|
description: 'Tucapel',
|
||||||
|
locomotionType: 1,
|
||||||
|
backgroundColor: 'Hexadecimal',
|
||||||
|
letterColor: 'Hexadecimal',
|
||||||
|
lineMessage: '',
|
||||||
|
arrivals: [
|
||||||
|
{
|
||||||
|
carPlate: 'RPDA-98',
|
||||||
|
planned: '',
|
||||||
|
estimatedGPS: '15:08',
|
||||||
|
distanceGPS: '1.0 KM',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
carPlate: 'WYXYZ-22',
|
||||||
|
planned: '',
|
||||||
|
estimatedGPS: '15:42',
|
||||||
|
distanceGPS: '5.0 KM',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
carPlate: 'ABCA-65',
|
||||||
|
planned: '',
|
||||||
|
estimatedGPS: '17:18',
|
||||||
|
distanceGPS: '13.4 KM',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
lineNumber: '5487',
|
||||||
|
description: 'Centauro',
|
||||||
|
locomotionType: 1,
|
||||||
|
backgroundColor: 'Hexadecimal',
|
||||||
|
letterColor: 'Hexadecimal',
|
||||||
|
lineMessage: 'Sin info. GPS, la informacion es estimada',
|
||||||
|
arrivals: [
|
||||||
|
{
|
||||||
|
carPlate: 'PLKJ-32',
|
||||||
|
planned: '15:13',
|
||||||
|
estimatedGPS: '15:13',
|
||||||
|
distanceGPS: '',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
carPlate: 'GHLK-11',
|
||||||
|
planned: '15:39',
|
||||||
|
estimatedGPS: '15:39',
|
||||||
|
distanceGPS: '',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
carPlate: 'DFQW-55',
|
||||||
|
planned: '17:22',
|
||||||
|
estimatedGPS: '17:22',
|
||||||
|
distanceGPS: '',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getNextArraival', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.useFakeTimers({legacyFakeTimers: false});
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
jest.useRealTimers();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return next arraival', () => {
|
||||||
|
jest.setSystemTime(new Date('2024-11-25T16:33:37').getTime());
|
||||||
|
|
||||||
|
const nextArraival = service.getNextArraival('803010');
|
||||||
|
|
||||||
|
expect(nextArraival).toMatchObject({
|
||||||
|
carPlate: 'ABCA-65',
|
||||||
|
planned: '',
|
||||||
|
estimatedGPS: '17:18',
|
||||||
|
distanceGPS: '13.4 KM',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw an error if not arraival found', () => {
|
||||||
|
jest.setSystemTime(new Date('2024-11-25T20:33:37').getTime());
|
||||||
|
|
||||||
|
expect(() => {
|
||||||
|
service.getNextArraival('803010');
|
||||||
|
}).toThrow('No next arrival');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('isArraivalTimeBetween tests', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.useFakeTimers({legacyFakeTimers: false});
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
jest.useRealTimers();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return < a 3 minutos', () => {
|
||||||
|
jest.setSystemTime(new Date('2024-11-25T17:17:37').getTime());
|
||||||
|
const arraival = {
|
||||||
|
carPlate: 'ABCA-65',
|
||||||
|
planned: '',
|
||||||
|
estimatedGPS: '17:18',
|
||||||
|
distanceGPS: '13.4 KM',
|
||||||
|
};
|
||||||
|
|
||||||
|
const arraivalTimeText = service.checkArraivalTime(arraival);
|
||||||
|
|
||||||
|
expect(arraivalTimeText).toStrictEqual('< a 3 minutos');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return Entre 3 a 5 minutos', () => {
|
||||||
|
jest.setSystemTime(new Date('2024-11-25T17:14:59').getTime());
|
||||||
|
const arraival = {
|
||||||
|
carPlate: 'ABCA-65',
|
||||||
|
planned: '',
|
||||||
|
estimatedGPS: '17:18',
|
||||||
|
distanceGPS: '13.4 KM',
|
||||||
|
};
|
||||||
|
|
||||||
|
const arraivalTimeText = service.checkArraivalTime(arraival);
|
||||||
|
|
||||||
|
expect(arraivalTimeText).toStrictEqual('Entre 3 a 5 minutos');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return Menos de 10 minutos', () => {
|
||||||
|
jest.setSystemTime(new Date('2024-11-25T17:09:37').getTime());
|
||||||
|
const arraival = {
|
||||||
|
carPlate: 'ABCA-65',
|
||||||
|
planned: '',
|
||||||
|
estimatedGPS: '17:18',
|
||||||
|
distanceGPS: '13.4 KM',
|
||||||
|
};
|
||||||
|
|
||||||
|
const arraivalTimeText = service.checkArraivalTime(arraival);
|
||||||
|
|
||||||
|
expect(arraivalTimeText).toStrictEqual('Menos de 10 minutos');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return Más de 10 minutos', () => {
|
||||||
|
jest.setSystemTime(new Date('2024-11-25T17:00:37').getTime());
|
||||||
|
const arraival = {
|
||||||
|
carPlate: 'ABCA-65',
|
||||||
|
planned: '',
|
||||||
|
estimatedGPS: '17:18',
|
||||||
|
distanceGPS: '13.4 KM',
|
||||||
|
};
|
||||||
|
|
||||||
|
const arraivalTimeText = service.checkArraivalTime(arraival);
|
||||||
|
|
||||||
|
expect(arraivalTimeText).toStrictEqual('Más de 10 minutos');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getLineNumberFromDescription tests', () => {
|
||||||
|
it('should return a list of numbers', () => {
|
||||||
|
const numbers = service.getLineNumberFromDescription('10N');
|
||||||
|
|
||||||
|
expect(numbers).toBe('10');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw an error if no numbers found', () => {
|
||||||
|
expect(() => {
|
||||||
|
service.getLineNumberFromDescription('N');
|
||||||
|
}).toThrow('No numbers found');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getLineLetterFromDescription tests', () => {
|
||||||
|
it('should return a list of letters', () => {
|
||||||
|
const letters = service.getLineLetterFromDescription('10N');
|
||||||
|
|
||||||
|
expect(letters).toBe('N');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw an error if no letters found', () => {
|
||||||
|
expect(() => {
|
||||||
|
service.getLineLetterFromDescription('10');
|
||||||
|
}).toThrow('No letters found');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getLineDescription tests', () => {
|
||||||
|
it('should return a line description', () => {
|
||||||
|
const description = service.getLineDescription(
|
||||||
|
'10K',
|
||||||
|
'Vía Láctea 10K - Leonera',
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(description).toBe('Vía Láctea - Leonera');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,102 @@
|
||||||
|
import moment from 'moment';
|
||||||
|
import LineDetail from '../repositories/LineDetail';
|
||||||
|
import {getHours, getMinutes, getSeconds} from '../../utils/DateUtils';
|
||||||
|
import Arrival from '../repositories/Arrival';
|
||||||
|
|
||||||
|
// TODO: Change to a better name or split ?
|
||||||
|
class BusStopInfoService {
|
||||||
|
private lines: LineDetail[];
|
||||||
|
|
||||||
|
constructor(lines: LineDetail[]) {
|
||||||
|
this.lines = lines;
|
||||||
|
}
|
||||||
|
|
||||||
|
getLines(): LineDetail[] {
|
||||||
|
return this.lines;
|
||||||
|
}
|
||||||
|
|
||||||
|
getLinesByNumber(lineNumber: string): LineDetail {
|
||||||
|
const line = this.lines.find(bus => bus.lineNumber === lineNumber);
|
||||||
|
|
||||||
|
if (!line) {
|
||||||
|
throw new Error('No buses found');
|
||||||
|
}
|
||||||
|
|
||||||
|
return line;
|
||||||
|
}
|
||||||
|
|
||||||
|
pruneBusList(start: number, howMany: number): LineDetail[] {
|
||||||
|
const end = start + howMany;
|
||||||
|
return this.lines.slice(start, end);
|
||||||
|
}
|
||||||
|
|
||||||
|
getNextArraival(lineNumber: string): Arrival {
|
||||||
|
const currentTime = moment();
|
||||||
|
|
||||||
|
const nextArraival = this.getLinesByNumber(lineNumber).arrivals.find(
|
||||||
|
arrival => {
|
||||||
|
const compareTime = moment();
|
||||||
|
compareTime.set('hour', getHours(arrival.estimatedGPS));
|
||||||
|
compareTime.set('minute', getMinutes(arrival.estimatedGPS));
|
||||||
|
compareTime.set('second', getSeconds(arrival.estimatedGPS));
|
||||||
|
|
||||||
|
if (compareTime.isAfter(currentTime)) {
|
||||||
|
return arrival;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!nextArraival) {
|
||||||
|
throw new Error('No next arrival');
|
||||||
|
}
|
||||||
|
|
||||||
|
return nextArraival;
|
||||||
|
}
|
||||||
|
|
||||||
|
checkArraivalTime(arrival: Arrival): string {
|
||||||
|
const compareTime = moment();
|
||||||
|
compareTime.set('hour', getHours(arrival.estimatedGPS));
|
||||||
|
compareTime.set('minute', getMinutes(arrival.estimatedGPS));
|
||||||
|
compareTime.set('second', getSeconds(arrival.estimatedGPS));
|
||||||
|
|
||||||
|
const now = moment();
|
||||||
|
const diffInMinutes = compareTime.diff(now, 'minutes');
|
||||||
|
|
||||||
|
if (diffInMinutes <= 3) {
|
||||||
|
return '< a 3 minutos';
|
||||||
|
} else if (diffInMinutes > 3 && diffInMinutes < 6) {
|
||||||
|
return 'Entre 3 a 5 minutos';
|
||||||
|
} else if (diffInMinutes >= 6 && diffInMinutes <= 10) {
|
||||||
|
return 'Menos de 10 minutos';
|
||||||
|
} else {
|
||||||
|
return 'Más de 10 minutos';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getLineNumberFromDescription(description: string): string {
|
||||||
|
const numbers = description.match(/\d+/g);
|
||||||
|
|
||||||
|
if (!numbers) {
|
||||||
|
throw new Error('No numbers found');
|
||||||
|
}
|
||||||
|
|
||||||
|
return numbers.join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
getLineLetterFromDescription(description: string): string {
|
||||||
|
const letters = description.match(/[a-zA-Z]+/g);
|
||||||
|
|
||||||
|
if (!letters) {
|
||||||
|
throw new Error('No letters found');
|
||||||
|
}
|
||||||
|
|
||||||
|
return letters.join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
getLineDescription(description: string, line: string): string {
|
||||||
|
const space = ' '; // this is to eliminate the extra space left when applying the replace.
|
||||||
|
return line.replace(`${space}${description}`, '');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default BusStopInfoService;
|
|
@ -0,0 +1,30 @@
|
||||||
|
import axios from 'axios';
|
||||||
|
import AuthResponse from '../models/AuthResponse';
|
||||||
|
import AuthRequest from '../models/AuthRequest';
|
||||||
|
|
||||||
|
class AuthAPI {
|
||||||
|
private baseURL: string;
|
||||||
|
|
||||||
|
constructor(baseURL: string) {
|
||||||
|
this.baseURL = baseURL;
|
||||||
|
}
|
||||||
|
|
||||||
|
async auth({username, password}: AuthRequest): Promise<AuthResponse> {
|
||||||
|
if (!username) {
|
||||||
|
throw new Error('username is required');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!password) {
|
||||||
|
throw new Error('password is required');
|
||||||
|
}
|
||||||
|
|
||||||
|
const {data} = await axios.post<AuthResponse>(`${this.baseURL}/api/auth/`, {
|
||||||
|
username,
|
||||||
|
password,
|
||||||
|
});
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AuthAPI;
|
|
@ -0,0 +1,72 @@
|
||||||
|
import axios from 'axios';
|
||||||
|
import DeviceRequest from '../models/DeviceRequest';
|
||||||
|
import {GetInfoDeviceResponse} from '../models/GetInfoDeviceResponse';
|
||||||
|
import WhoamiResponse from '../models/WhoamiResponse';
|
||||||
|
import responseDevicesMock from './response-devices-mock.json';
|
||||||
|
|
||||||
|
const KEYAUTORIZACION = 'token';
|
||||||
|
|
||||||
|
class DevicesAPI {
|
||||||
|
private baseURL: string;
|
||||||
|
|
||||||
|
constructor(baseURL: string) {
|
||||||
|
this.baseURL = baseURL;
|
||||||
|
}
|
||||||
|
|
||||||
|
async whoami({token, deviceId}: DeviceRequest): Promise<WhoamiResponse> {
|
||||||
|
if (!token) {
|
||||||
|
throw new Error('token is required');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!deviceId) {
|
||||||
|
throw new Error('deviceId is required');
|
||||||
|
}
|
||||||
|
const {data} = await axios.post<WhoamiResponse>(
|
||||||
|
`${this.baseURL}/api/dispositivos/whoami/`,
|
||||||
|
{
|
||||||
|
whoami: {idDispositivo: deviceId, KeyAuthorizacion: KEYAUTORIZACION},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${token}`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getInfoDevice({
|
||||||
|
token,
|
||||||
|
deviceId,
|
||||||
|
}: DeviceRequest): Promise<GetInfoDeviceResponse> {
|
||||||
|
if (!token) {
|
||||||
|
throw new Error('token is required');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!deviceId) {
|
||||||
|
throw new Error('deviceId is required');
|
||||||
|
}
|
||||||
|
|
||||||
|
const {data} = await axios.post<GetInfoDeviceResponse>(
|
||||||
|
`${this.baseURL}/api/dispositivos/getInfoDevice/`,
|
||||||
|
{
|
||||||
|
GetInfoDevice: {
|
||||||
|
idDispositivo: deviceId,
|
||||||
|
KeyAuthorizacion: KEYAUTORIZACION,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${token}`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
return data; // rial implementation
|
||||||
|
|
||||||
|
// return responseDevicesMock; // mock implementation
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default DevicesAPI;
|
|
@ -0,0 +1,59 @@
|
||||||
|
import axios from 'axios';
|
||||||
|
import MockAdapter from 'axios-mock-adapter';
|
||||||
|
import AuthAPI from '../AuthAPI';
|
||||||
|
|
||||||
|
const mockAdapter = new MockAdapter(axios);
|
||||||
|
|
||||||
|
const BASE_URL = 'https://test.cl';
|
||||||
|
|
||||||
|
const authAPI = new AuthAPI(BASE_URL);
|
||||||
|
|
||||||
|
describe('AuthAPI tests', () => {
|
||||||
|
it('should be defined', () => {
|
||||||
|
expect(AuthAPI).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return a token', async () => {
|
||||||
|
mockAdapter.onPost(`${BASE_URL}/api/auth/`).reply(200, {
|
||||||
|
token: 'token',
|
||||||
|
});
|
||||||
|
|
||||||
|
const token = await authAPI.auth({
|
||||||
|
username: 'username',
|
||||||
|
password: 'password',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(token).toMatchObject({token: 'token'});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw an error if username is not provided', async () => {
|
||||||
|
await expect(
|
||||||
|
authAPI.auth({
|
||||||
|
password: 'password',
|
||||||
|
username: '',
|
||||||
|
}),
|
||||||
|
).rejects.toThrow('username is required');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw an error if password is not provided', async () => {
|
||||||
|
await expect(
|
||||||
|
authAPI.auth({
|
||||||
|
username: 'username',
|
||||||
|
password: '',
|
||||||
|
}),
|
||||||
|
).rejects.toThrow('password is required');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return an error if it fails to request a token', async () => {
|
||||||
|
mockAdapter.onPost(`${BASE_URL}/api/auth/`).reply(400, {
|
||||||
|
error: 'error',
|
||||||
|
});
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
authAPI.auth({
|
||||||
|
username: 'username',
|
||||||
|
password: 'password',
|
||||||
|
}),
|
||||||
|
).rejects.toThrow('Request failed with status code 400');
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,107 @@
|
||||||
|
import axios from 'axios';
|
||||||
|
import MockAdapter from 'axios-mock-adapter';
|
||||||
|
import DevicesAPI from '../DevicesAPI';
|
||||||
|
|
||||||
|
const mockAdapter = new MockAdapter(axios);
|
||||||
|
|
||||||
|
const BASE_URL = 'https://test.cl';
|
||||||
|
|
||||||
|
const deviceAPI = new DevicesAPI(BASE_URL);
|
||||||
|
|
||||||
|
describe('devices tests', () => {
|
||||||
|
it('should be defined', () => {
|
||||||
|
expect(deviceAPI).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('whoami', () => {
|
||||||
|
it('should return stop information by device id', async () => {
|
||||||
|
mockAdapter.onPost(`${BASE_URL}/api/dispositivos/whoami/`).reply(200, {
|
||||||
|
stopNumber: 'stopNumber',
|
||||||
|
stopName: 'stopName',
|
||||||
|
});
|
||||||
|
|
||||||
|
const response = await deviceAPI.whoami({
|
||||||
|
token: 'token',
|
||||||
|
deviceId: 'deviceId',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(response).toMatchObject({
|
||||||
|
stopNumber: 'stopNumber',
|
||||||
|
stopName: 'stopName',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw an error if token is not provided', async () => {
|
||||||
|
await expect(
|
||||||
|
deviceAPI.whoami({token: '', deviceId: 'deviceId'}),
|
||||||
|
).rejects.toThrow('token is required');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw an error if deviceId is not provided', async () => {
|
||||||
|
await expect(
|
||||||
|
deviceAPI.whoami({token: 'token', deviceId: ''}),
|
||||||
|
).rejects.toThrow('deviceId is required');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return an error if it fails to request stop information', async () => {
|
||||||
|
mockAdapter.onPost(`${BASE_URL}/api/dispositivos/whoami/`).reply(400, {
|
||||||
|
error: 'error',
|
||||||
|
});
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
deviceAPI.whoami({
|
||||||
|
token: 'token',
|
||||||
|
deviceId: 'deviceId',
|
||||||
|
}),
|
||||||
|
).rejects.toThrow('Request failed with status code 400');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getInfoDevice', () => {
|
||||||
|
it('should return stop information by device id', async () => {
|
||||||
|
mockAdapter
|
||||||
|
.onPost(`${BASE_URL}/api/dispositivos/getInfoDevice/`)
|
||||||
|
.reply(200, {
|
||||||
|
stopNumber: 'stopNumber',
|
||||||
|
stopName: 'stopName',
|
||||||
|
});
|
||||||
|
|
||||||
|
const response = await deviceAPI.getInfoDevice({
|
||||||
|
token: 'token',
|
||||||
|
deviceId: 'deviceId',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(response).toMatchObject({
|
||||||
|
stopNumber: 'stopNumber',
|
||||||
|
stopName: 'stopName',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw an error if token is not provided', async () => {
|
||||||
|
await expect(
|
||||||
|
deviceAPI.getInfoDevice({token: '', deviceId: 'deviceId'}),
|
||||||
|
).rejects.toThrow('token is required');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw an error if deviceId is not provided', async () => {
|
||||||
|
await expect(
|
||||||
|
deviceAPI.getInfoDevice({token: 'token', deviceId: ''}),
|
||||||
|
).rejects.toThrow('deviceId is required');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return an error if it fails to request stop information', async () => {
|
||||||
|
mockAdapter
|
||||||
|
.onPost(`${BASE_URL}/api/dispositivos/getInfoDevice/`)
|
||||||
|
.reply(400, {
|
||||||
|
error: 'error',
|
||||||
|
});
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
deviceAPI.getInfoDevice({
|
||||||
|
token: 'token',
|
||||||
|
deviceId: 'deviceId',
|
||||||
|
}),
|
||||||
|
).rejects.toThrow('Request failed with status code 400');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,6 @@
|
||||||
|
interface AuthRequest {
|
||||||
|
username: string;
|
||||||
|
password: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AuthRequest;
|
|
@ -0,0 +1,5 @@
|
||||||
|
interface AuthResponse {
|
||||||
|
token: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AuthResponse;
|
|
@ -0,0 +1,13 @@
|
||||||
|
import Llegada from './Llegada';
|
||||||
|
|
||||||
|
interface DetalleLinea {
|
||||||
|
Linea: string;
|
||||||
|
Descripcion: string;
|
||||||
|
TipoLocomocion: number;
|
||||||
|
colorFondo: string;
|
||||||
|
colorLetra: string;
|
||||||
|
Llegadas: Llegada[];
|
||||||
|
Mensajelinea: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default DetalleLinea;
|
|
@ -0,0 +1,8 @@
|
||||||
|
import LineDetail from './LineDetail';
|
||||||
|
|
||||||
|
interface DeviceInfoResponse {
|
||||||
|
stopMessage: string;
|
||||||
|
lineDetails: LineDetail[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export default DeviceInfoResponse;
|
|
@ -0,0 +1,6 @@
|
||||||
|
interface DeviceRequest {
|
||||||
|
token: string;
|
||||||
|
deviceId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default DeviceRequest;
|
|
@ -0,0 +1,8 @@
|
||||||
|
import DetalleLinea from './DetalleLinea';
|
||||||
|
|
||||||
|
export interface GetInfoDeviceResponse {
|
||||||
|
GetInfoDeviceResponse: {
|
||||||
|
DetalleLineas: DetalleLinea[];
|
||||||
|
MensajeParadero: string;
|
||||||
|
};
|
||||||
|
}
|
|
@ -0,0 +1,8 @@
|
||||||
|
interface Llegada {
|
||||||
|
Patente: string;
|
||||||
|
Planificada: string;
|
||||||
|
EstimadaGPS: string;
|
||||||
|
DistanciaGPS: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Llegada;
|
|
@ -0,0 +1,8 @@
|
||||||
|
interface WhoamiResponse {
|
||||||
|
WhoamiResponse: {
|
||||||
|
NroParadero: string;
|
||||||
|
NombreParadero: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default WhoamiResponse;
|
|
@ -0,0 +1,6 @@
|
||||||
|
const BASE_URL = 'https://transporte.hz.kursor.cl';
|
||||||
|
const LOGIN_METHOD = '/api/auth/';
|
||||||
|
const WHOAMI_METHOD = '/api/dispositivos/whoami/';
|
||||||
|
const INFO_DEVICE_METHOD = '/api/dispositivos/getInfoDevice/';
|
||||||
|
|
||||||
|
export {BASE_URL, LOGIN_METHOD, WHOAMI_METHOD, INFO_DEVICE_METHOD};
|
|
@ -0,0 +1,13 @@
|
||||||
|
describe('useDevices tests', () => {
|
||||||
|
it('should be defined', () => {
|
||||||
|
expect(true).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return stop information', () => {
|
||||||
|
expect(true).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return a list of devices', () => {
|
||||||
|
expect(true).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,9 @@
|
||||||
|
describe('useLogin tests', () => {
|
||||||
|
it('should be defined', () => {
|
||||||
|
expect(true).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return a token', () => {
|
||||||
|
expect(true).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,212 @@
|
||||||
|
import {useCallback, useEffect, useMemo, useState} from 'react';
|
||||||
|
import DevicesRepositoryImpl from '../repositories/DevicesRepositoryImpl';
|
||||||
|
import DevicesAPI from '../api/clients/DevicesAPI';
|
||||||
|
import AuthRepositoryImpl from '../repositories/AuthRepositoryImpl';
|
||||||
|
import AuthAPI from '../api/clients/AuthAPI';
|
||||||
|
import LineDetail from '../../domain/repositories/LineDetail';
|
||||||
|
|
||||||
|
import {Line} from '../../presentation/screens/BusStopInfoScreen';
|
||||||
|
import BusStopInfoService from '../../domain/services/BusStopInfoService';
|
||||||
|
|
||||||
|
export enum Status {
|
||||||
|
LOADING = 'LOADING',
|
||||||
|
SUCCESS = 'SUCCESS',
|
||||||
|
ERROR = 'ERROR',
|
||||||
|
}
|
||||||
|
|
||||||
|
interface State {
|
||||||
|
status: Status;
|
||||||
|
lines: LineDetail[];
|
||||||
|
displayedLines: Line[];
|
||||||
|
currentIndex: number;
|
||||||
|
stopMessage: string;
|
||||||
|
stopName: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const DEVICE_ID = 'TTM543870hyt';
|
||||||
|
const BASE_URL = 'https://transporte.hz.kursor.cl';
|
||||||
|
const USER = 'usuario1';
|
||||||
|
const PASSWORD = 'usuario1';
|
||||||
|
|
||||||
|
const useDevices = () => {
|
||||||
|
const [state, setState] = useState<State>({
|
||||||
|
status: Status.LOADING,
|
||||||
|
currentIndex: 0,
|
||||||
|
stopMessage: '',
|
||||||
|
displayedLines: [],
|
||||||
|
lines: [],
|
||||||
|
stopName: 'Sin información - Sin información',
|
||||||
|
});
|
||||||
|
|
||||||
|
const baseUrl = BASE_URL; // TODO: remoteconfig ?
|
||||||
|
const username = USER;
|
||||||
|
const password = PASSWORD;
|
||||||
|
const deviceApi = useMemo(() => new DevicesAPI(baseUrl), [baseUrl]);
|
||||||
|
const authApi = useMemo(() => new AuthAPI(baseUrl), [baseUrl]);
|
||||||
|
const devicesRepository = useMemo(
|
||||||
|
() => new DevicesRepositoryImpl(deviceApi),
|
||||||
|
[deviceApi],
|
||||||
|
);
|
||||||
|
const authRepository = useMemo(
|
||||||
|
() => new AuthRepositoryImpl(authApi),
|
||||||
|
[authApi],
|
||||||
|
);
|
||||||
|
|
||||||
|
const setDisplayedLines = useCallback(
|
||||||
|
(lineDetails: LineDetail[], stopMessage: string, stopName: string) => {
|
||||||
|
if (!lineDetails || !stopName) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
let busStopInfoService = new BusStopInfoService(lineDetails);
|
||||||
|
|
||||||
|
const linesWithArrivals = lineDetails
|
||||||
|
.map(line => {
|
||||||
|
try {
|
||||||
|
busStopInfoService.getNextArraival(line.lineNumber);
|
||||||
|
|
||||||
|
return line;
|
||||||
|
} catch (error) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.filter((line): line is LineDetail => line !== undefined);
|
||||||
|
|
||||||
|
busStopInfoService = new BusStopInfoService(linesWithArrivals);
|
||||||
|
|
||||||
|
const linesToDisplay: Line[] = busStopInfoService
|
||||||
|
.pruneBusList(state.currentIndex, 21) // result 7 * 3
|
||||||
|
.map(line => {
|
||||||
|
try {
|
||||||
|
const nextArraival = busStopInfoService.getNextArraival(
|
||||||
|
line.lineNumber,
|
||||||
|
);
|
||||||
|
|
||||||
|
const estimatedArrivalTimeInMinutes =
|
||||||
|
busStopInfoService.checkArraivalTime(nextArraival);
|
||||||
|
|
||||||
|
const lineLetter =
|
||||||
|
busStopInfoService.getLineLetterFromDescription(
|
||||||
|
line.description,
|
||||||
|
);
|
||||||
|
const lineNumber =
|
||||||
|
busStopInfoService.getLineNumberFromDescription(
|
||||||
|
line.description,
|
||||||
|
);
|
||||||
|
|
||||||
|
const lineDescription = busStopInfoService.getLineDescription(
|
||||||
|
line.description,
|
||||||
|
line.lineNumber,
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
backgroundColor: line.backgroundColor,
|
||||||
|
estimatedArrivalTimeInMinutes,
|
||||||
|
letterColor: line.letterColor,
|
||||||
|
lineLetter,
|
||||||
|
lineNumber,
|
||||||
|
lineDescription,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.filter((line): line is Line => line !== undefined);
|
||||||
|
|
||||||
|
setState(prevState => {
|
||||||
|
const displayedLines =
|
||||||
|
linesToDisplay.length === 0
|
||||||
|
? prevState.displayedLines
|
||||||
|
: linesToDisplay;
|
||||||
|
|
||||||
|
return {
|
||||||
|
...prevState,
|
||||||
|
displayedLines,
|
||||||
|
stopMessage,
|
||||||
|
stopName,
|
||||||
|
lines: lineDetails,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
} catch (error: unknown) {
|
||||||
|
setState(prevState => ({
|
||||||
|
...prevState,
|
||||||
|
status: Status.ERROR,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[state.currentIndex],
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const init = async () => {
|
||||||
|
try {
|
||||||
|
const token = await authRepository.auth({username, password});
|
||||||
|
|
||||||
|
const {lineDetails, stopMessage} =
|
||||||
|
await devicesRepository.getDeviceInfo({
|
||||||
|
deviceId: DEVICE_ID,
|
||||||
|
token,
|
||||||
|
});
|
||||||
|
|
||||||
|
const {stopName} = await devicesRepository.whoAmI({
|
||||||
|
deviceId: DEVICE_ID,
|
||||||
|
token,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!lineDetails) {
|
||||||
|
setState((prevState: State) => ({
|
||||||
|
...prevState,
|
||||||
|
status: Status.ERROR,
|
||||||
|
}));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setState((prevState: State) => ({
|
||||||
|
...prevState,
|
||||||
|
...{
|
||||||
|
lines: lineDetails,
|
||||||
|
stopMessage,
|
||||||
|
stopName,
|
||||||
|
status: Status.SUCCESS,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
} catch (error) {
|
||||||
|
setState((prevState: State) => ({
|
||||||
|
...prevState,
|
||||||
|
status: Status.ERROR,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
init();
|
||||||
|
}, [authRepository, devicesRepository, password, username]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setDisplayedLines(state.lines, state.stopMessage, state.stopName);
|
||||||
|
}, [setDisplayedLines, state.lines, state.stopMessage, state.stopName]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const interval = setInterval(() => {
|
||||||
|
setState(prevState => {
|
||||||
|
const isGreatherThanLinesLength =
|
||||||
|
(prevState.currentIndex || 1) + 21 >= state.lines.length;
|
||||||
|
|
||||||
|
return {
|
||||||
|
...prevState,
|
||||||
|
currentIndex: isGreatherThanLinesLength
|
||||||
|
? 0
|
||||||
|
: (prevState.currentIndex || 1 + 21) % state.lines.length,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}, 5000);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
clearInterval(interval);
|
||||||
|
};
|
||||||
|
}, [state.lines, state.status]);
|
||||||
|
|
||||||
|
return {state};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useDevices;
|
|
@ -0,0 +1,19 @@
|
||||||
|
import AuthRepository from '../../domain/repositories/AuthRepository';
|
||||||
|
import AuthAPI from '../api/clients/AuthAPI';
|
||||||
|
import AuthRequest from '../api/models/AuthRequest';
|
||||||
|
|
||||||
|
class AuthRepositoryImpl implements AuthRepository {
|
||||||
|
private authAPI: AuthAPI;
|
||||||
|
|
||||||
|
constructor(authAPI: AuthAPI) {
|
||||||
|
this.authAPI = authAPI;
|
||||||
|
}
|
||||||
|
|
||||||
|
async auth(request: AuthRequest): Promise<string> {
|
||||||
|
const {token} = await this.authAPI.auth(request);
|
||||||
|
|
||||||
|
return token;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AuthRepositoryImpl;
|
|
@ -0,0 +1,29 @@
|
||||||
|
import DevicesRepository from '../../domain/repositories/DevicesRepository';
|
||||||
|
import DevicesAPI from '../api/clients/DevicesAPI';
|
||||||
|
import DeviceInfoResponse from '../api/models/DeviceInfoResponse';
|
||||||
|
import toDeviceInfoResponse from './mappers/toDeviceInfoResponse';
|
||||||
|
import WhoAmIResponse from '../../domain/repositories/WhoAmIResponse';
|
||||||
|
import DeviceRequest from '../api/models/DeviceRequest';
|
||||||
|
import toWhoAmIResponse from './mappers/toWhoAmIResponse';
|
||||||
|
|
||||||
|
class DevicesRepositoryImpl implements DevicesRepository {
|
||||||
|
private devicesAPI: DevicesAPI;
|
||||||
|
|
||||||
|
constructor(devicesAPI: DevicesAPI) {
|
||||||
|
this.devicesAPI = devicesAPI;
|
||||||
|
}
|
||||||
|
|
||||||
|
async whoAmI(request: DeviceRequest): Promise<WhoAmIResponse> {
|
||||||
|
const response = await this.devicesAPI.whoami(request);
|
||||||
|
|
||||||
|
return toWhoAmIResponse(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getDeviceInfo(request: DeviceRequest): Promise<DeviceInfoResponse> {
|
||||||
|
const response = await this.devicesAPI.getInfoDevice(request);
|
||||||
|
|
||||||
|
return toDeviceInfoResponse(response);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default DevicesRepositoryImpl;
|
|
@ -0,0 +1,24 @@
|
||||||
|
import AuthAPI from '../../api/clients/AuthAPI';
|
||||||
|
import AuthRepositoryImpl from '../AuthRepositoryImpl';
|
||||||
|
|
||||||
|
jest.mock('../../api/clients/AuthAPI');
|
||||||
|
|
||||||
|
describe('AuthRepositoryImpl tests', () => {
|
||||||
|
it('should be defined', () => {
|
||||||
|
expect(AuthRepositoryImpl).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return a token', async () => {
|
||||||
|
const mockedAuthAPI = new AuthAPI(
|
||||||
|
'https://api.example.com',
|
||||||
|
) as jest.Mocked<AuthAPI>;
|
||||||
|
|
||||||
|
mockedAuthAPI.auth.mockResolvedValue({token: 'TOKEN'});
|
||||||
|
|
||||||
|
const repo = new AuthRepositoryImpl(mockedAuthAPI);
|
||||||
|
|
||||||
|
const token = await repo.auth({username: 'username', password: 'password'});
|
||||||
|
|
||||||
|
expect(token).toBe('TOKEN');
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,93 @@
|
||||||
|
import DevicesRepositoryImpl from '../DevicesRepositoryImpl';
|
||||||
|
import DevicesAPI from '../../api/clients/DevicesAPI';
|
||||||
|
import DeviceInfoResponse from '../../api/models/DeviceInfoResponse';
|
||||||
|
import LineDetail from '../../../domain/repositories/LineDetail';
|
||||||
|
|
||||||
|
jest.mock('../../api/clients/DevicesAPI');
|
||||||
|
|
||||||
|
describe('DevicesRepositoryImpl tests', () => {
|
||||||
|
it('should be defined', () => {
|
||||||
|
expect(DevicesRepositoryImpl).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return a whoami response', async () => {
|
||||||
|
const mockedDevicesAPI = new DevicesAPI(
|
||||||
|
'https://api.example.com',
|
||||||
|
) as jest.Mocked<DevicesAPI>;
|
||||||
|
|
||||||
|
mockedDevicesAPI.whoami.mockResolvedValue({
|
||||||
|
WhoamiResponse: {NombreParadero: 'stopName', NroParadero: 'stopNumber'},
|
||||||
|
});
|
||||||
|
|
||||||
|
const repo = new DevicesRepositoryImpl(mockedDevicesAPI);
|
||||||
|
|
||||||
|
const whoami = await repo.whoAmI({
|
||||||
|
token: 'token',
|
||||||
|
deviceId: 'deviceId',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(whoami).toMatchObject({
|
||||||
|
stopNumber: 'stopNumber',
|
||||||
|
stopName: 'stopName',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return a device info response', async () => {
|
||||||
|
const mockedDevicesAPI = new DevicesAPI(
|
||||||
|
'https://api.example.com',
|
||||||
|
) as jest.Mocked<DevicesAPI>;
|
||||||
|
|
||||||
|
mockedDevicesAPI.getInfoDevice.mockResolvedValue({
|
||||||
|
GetInfoDeviceResponse: {
|
||||||
|
MensajeParadero: 'stopMessage',
|
||||||
|
DetalleLineas: [
|
||||||
|
{
|
||||||
|
colorFondo: 'colorFondo',
|
||||||
|
colorLetra: 'colorLetra',
|
||||||
|
Descripcion: 'Descripcion',
|
||||||
|
Linea: 'Linea',
|
||||||
|
Llegadas: [
|
||||||
|
{
|
||||||
|
DistanciaGPS: 'DistanciaGPS',
|
||||||
|
EstimadaGPS: 'EstimadaGPS',
|
||||||
|
Patente: 'Patente',
|
||||||
|
Planificada: 'Planificada',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
Mensajelinea: 'Mensajelinea',
|
||||||
|
TipoLocomocion: 0,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const repo = new DevicesRepositoryImpl(mockedDevicesAPI);
|
||||||
|
|
||||||
|
const deviceInfo = await repo.getDeviceInfo({
|
||||||
|
token: 'token',
|
||||||
|
deviceId: 'deviceId',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(deviceInfo).toMatchObject({
|
||||||
|
lineDetails: [
|
||||||
|
{
|
||||||
|
arrivals: [
|
||||||
|
{
|
||||||
|
carPlate: 'Patente',
|
||||||
|
distanceGPS: 'DistanciaGPS',
|
||||||
|
estimatedGPS: 'EstimadaGPS',
|
||||||
|
planned: 'Planificada',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
backgroundColor: 'colorFondo',
|
||||||
|
description: 'Descripcion',
|
||||||
|
letterColor: 'colorLetra',
|
||||||
|
lineMessage: 'Mensajelinea',
|
||||||
|
lineNumber: 'Linea',
|
||||||
|
locomotionType: 0,
|
||||||
|
} as LineDetail,
|
||||||
|
],
|
||||||
|
stopMessage: 'stopMessage',
|
||||||
|
} as DeviceInfoResponse);
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,136 @@
|
||||||
|
import {GetInfoDeviceResponse} from '../../../api/models/GetInfoDeviceResponse';
|
||||||
|
import toDeviceInfoResponse from '../toDeviceInfoResponse';
|
||||||
|
|
||||||
|
describe('toDeviceInfoResponse tests', () => {
|
||||||
|
it('should be defined', () => {});
|
||||||
|
|
||||||
|
it('should map to DeviceInfoResponse', () => {
|
||||||
|
const responseFromBackEnd: GetInfoDeviceResponse = {
|
||||||
|
GetInfoDeviceResponse: {
|
||||||
|
DetalleLineas: [
|
||||||
|
{
|
||||||
|
Linea: '803010',
|
||||||
|
Descripcion: 'Tucapel',
|
||||||
|
TipoLocomocion: 1,
|
||||||
|
colorFondo: 'Hexadecimal',
|
||||||
|
colorLetra: 'Hexadecimal',
|
||||||
|
Llegadas: [
|
||||||
|
{
|
||||||
|
Patente: 'RPDA-98',
|
||||||
|
Planificada: '',
|
||||||
|
EstimadaGPS: '15:08',
|
||||||
|
DistanciaGPS: '1.0 KM',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Patente: 'WYXYZ-22',
|
||||||
|
Planificada: '',
|
||||||
|
EstimadaGPS: '15:42',
|
||||||
|
DistanciaGPS: '5.0 KM',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Patente: 'ABCA-65',
|
||||||
|
Planificada: '',
|
||||||
|
EstimadaGPS: '16:18',
|
||||||
|
DistanciaGPS: '13.4 KM',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
Mensajelinea: '',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Linea: '5487',
|
||||||
|
Descripcion: 'Centauro',
|
||||||
|
TipoLocomocion: 1,
|
||||||
|
colorFondo: 'Hexadecimal',
|
||||||
|
colorLetra: 'Hexadecimal',
|
||||||
|
Llegadas: [
|
||||||
|
{
|
||||||
|
Patente: 'PLKJ-32',
|
||||||
|
Planificada: '15:13',
|
||||||
|
EstimadaGPS: '',
|
||||||
|
DistanciaGPS: '',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Patente: 'GHLK-11',
|
||||||
|
Planificada: '15:39',
|
||||||
|
EstimadaGPS: '',
|
||||||
|
DistanciaGPS: '',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Patente: 'DFQW-55',
|
||||||
|
Planificada: '16:22',
|
||||||
|
EstimadaGPS: '',
|
||||||
|
DistanciaGPS: '',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
Mensajelinea: 'Sin info. GPS, la informacion es estimada',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
MensajeParadero: 'No considerar, uso futuro',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const mappedResponse = toDeviceInfoResponse(responseFromBackEnd);
|
||||||
|
|
||||||
|
expect(mappedResponse).toMatchObject({
|
||||||
|
lineDetails: [
|
||||||
|
{
|
||||||
|
lineNumber: '803010',
|
||||||
|
description: 'Tucapel',
|
||||||
|
locomotionType: 1,
|
||||||
|
backgroundColor: 'Hexadecimal',
|
||||||
|
letterColor: 'Hexadecimal',
|
||||||
|
lineMessage: '',
|
||||||
|
arrivals: [
|
||||||
|
{
|
||||||
|
carPlate: 'RPDA-98',
|
||||||
|
planned: '',
|
||||||
|
estimatedGPS: '15:08',
|
||||||
|
distanceGPS: '1.0 KM',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
carPlate: 'WYXYZ-22',
|
||||||
|
planned: '',
|
||||||
|
estimatedGPS: '15:42',
|
||||||
|
distanceGPS: '5.0 KM',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
carPlate: 'ABCA-65',
|
||||||
|
planned: '',
|
||||||
|
estimatedGPS: '16:18',
|
||||||
|
distanceGPS: '13.4 KM',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
lineNumber: '5487',
|
||||||
|
description: 'Centauro',
|
||||||
|
locomotionType: 1,
|
||||||
|
backgroundColor: 'Hexadecimal',
|
||||||
|
letterColor: 'Hexadecimal',
|
||||||
|
lineMessage: 'Sin info. GPS, la informacion es estimada',
|
||||||
|
arrivals: [
|
||||||
|
{
|
||||||
|
carPlate: 'PLKJ-32',
|
||||||
|
planned: '15:13',
|
||||||
|
estimatedGPS: '',
|
||||||
|
distanceGPS: '',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
carPlate: 'GHLK-11',
|
||||||
|
planned: '15:39',
|
||||||
|
estimatedGPS: '',
|
||||||
|
distanceGPS: '',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
carPlate: 'DFQW-55',
|
||||||
|
planned: '16:22',
|
||||||
|
estimatedGPS: '',
|
||||||
|
distanceGPS: '',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
stopMessage: 'No considerar, uso futuro',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,21 @@
|
||||||
|
import toWhoAmIResponse from '../toWhoAmIResponse';
|
||||||
|
|
||||||
|
describe('whoamiMapper', () => {
|
||||||
|
it('should be defined', () => {
|
||||||
|
expect(toWhoAmIResponse).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should map to WhoamIResponse', () => {
|
||||||
|
expect(
|
||||||
|
toWhoAmIResponse({
|
||||||
|
WhoamiResponse: {
|
||||||
|
NroParadero: '37477',
|
||||||
|
NombreParadero: "O'Higgins - entre Angol y Salas",
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
).toMatchObject({
|
||||||
|
stopNumber: '37477',
|
||||||
|
stopName: "O'Higgins - entre Angol y Salas",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,52 @@
|
||||||
|
import Arrival from '../../../domain/repositories/Arrival';
|
||||||
|
import LineDetail from '../../../domain/repositories/LineDetail';
|
||||||
|
import DeviceInfoResponse from '../../api/models/DeviceInfoResponse';
|
||||||
|
import {GetInfoDeviceResponse} from '../../api/models/GetInfoDeviceResponse';
|
||||||
|
|
||||||
|
const toDeviceInfoResponse = (
|
||||||
|
response: GetInfoDeviceResponse,
|
||||||
|
): DeviceInfoResponse => {
|
||||||
|
const {
|
||||||
|
GetInfoDeviceResponse: {DetalleLineas, MensajeParadero},
|
||||||
|
} = response;
|
||||||
|
|
||||||
|
const mappedLines: LineDetail[] = DetalleLineas.map(linea => {
|
||||||
|
const {
|
||||||
|
Linea,
|
||||||
|
Descripcion,
|
||||||
|
TipoLocomocion,
|
||||||
|
colorFondo,
|
||||||
|
colorLetra,
|
||||||
|
Mensajelinea,
|
||||||
|
Llegadas,
|
||||||
|
} = linea;
|
||||||
|
|
||||||
|
const mappedArrivals: Arrival[] = Llegadas.map(llegada => {
|
||||||
|
const {Patente, Planificada, EstimadaGPS, DistanciaGPS} = llegada;
|
||||||
|
|
||||||
|
return {
|
||||||
|
carPlate: Patente,
|
||||||
|
planned: Planificada,
|
||||||
|
estimatedGPS: EstimadaGPS,
|
||||||
|
distanceGPS: DistanciaGPS,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
lineNumber: Linea,
|
||||||
|
description: Descripcion,
|
||||||
|
locomotionType: TipoLocomocion,
|
||||||
|
backgroundColor: colorFondo,
|
||||||
|
letterColor: colorLetra,
|
||||||
|
lineMessage: Mensajelinea,
|
||||||
|
arrivals: mappedArrivals,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
lineDetails: mappedLines,
|
||||||
|
stopMessage: MensajeParadero,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default toDeviceInfoResponse;
|
|
@ -0,0 +1,13 @@
|
||||||
|
import WhoAmIResponse from '../../../domain/repositories/WhoAmIResponse';
|
||||||
|
import WhoamiResponse from '../../api/models/WhoamiResponse';
|
||||||
|
|
||||||
|
const toWhoAmIResponse = (response: WhoamiResponse): WhoAmIResponse => {
|
||||||
|
const {NroParadero, NombreParadero} = response.WhoamiResponse;
|
||||||
|
|
||||||
|
return {
|
||||||
|
stopNumber: NroParadero,
|
||||||
|
stopName: NombreParadero,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default toWhoAmIResponse;
|
|
@ -0,0 +1,27 @@
|
||||||
|
import {View, Text, StyleSheet, StyleProp, ViewStyle} from 'react-native';
|
||||||
|
|
||||||
|
interface BannerProps {
|
||||||
|
text: string;
|
||||||
|
style?: StyleProp<ViewStyle>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Banner = ({text, style}: BannerProps) => {
|
||||||
|
return (
|
||||||
|
<View style={StyleSheet.compose(styles.bannerContainer, style)}>
|
||||||
|
<Text style={styles.bannerText}>{text}</Text>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
bannerContainer: {
|
||||||
|
justifyContent: 'center',
|
||||||
|
paddingLeft: 8,
|
||||||
|
},
|
||||||
|
bannerText: {
|
||||||
|
color: 'white',
|
||||||
|
fontWeight: 'bold',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default Banner;
|
|
@ -0,0 +1,21 @@
|
||||||
|
import {ReactNode} from 'react';
|
||||||
|
import {View, StyleSheet, StyleProp, ViewStyle} from 'react-native';
|
||||||
|
|
||||||
|
interface ContainerProps {
|
||||||
|
children: ReactNode;
|
||||||
|
style?: StyleProp<ViewStyle>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Container = ({children, style}: ContainerProps) => {
|
||||||
|
return (
|
||||||
|
<View style={StyleSheet.compose(styles.container, style)}>{children}</View>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
container: {
|
||||||
|
flex: 1,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default Container;
|
|
@ -0,0 +1,66 @@
|
||||||
|
import {View, Text, StyleSheet, StyleProp, ViewStyle} from 'react-native';
|
||||||
|
import Container from './Container';
|
||||||
|
import Icon from 'react-native-vector-icons/MaterialCommunityIcons';
|
||||||
|
|
||||||
|
interface HeaderProps {
|
||||||
|
title?: string;
|
||||||
|
subTitle?: string;
|
||||||
|
style?: StyleProp<ViewStyle>;
|
||||||
|
}
|
||||||
|
const Header = ({title, subTitle, style}: HeaderProps) => {
|
||||||
|
return (
|
||||||
|
<Container style={style}>
|
||||||
|
<View style={styles.contentContainer}>
|
||||||
|
<View style={styles.iconContainer}>
|
||||||
|
<Icon name="bus" size={70} color={'white'} />
|
||||||
|
</View>
|
||||||
|
<View style={styles.titleContainer}>
|
||||||
|
<Text style={styles.title}>{title}</Text>
|
||||||
|
{subTitle && <Text style={styles.subTitle}>{subTitle}</Text>}
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const defaultProps = {
|
||||||
|
title: 'Sin información',
|
||||||
|
subTitle: 'Sin información',
|
||||||
|
};
|
||||||
|
|
||||||
|
Header.defaultProps = defaultProps;
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
container: {
|
||||||
|
backgroundColor: 'orange',
|
||||||
|
},
|
||||||
|
contentContainer: {
|
||||||
|
flex: 1,
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'stretch',
|
||||||
|
},
|
||||||
|
iconContainer: {
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignContent: 'center',
|
||||||
|
overflow: 'hidden',
|
||||||
|
},
|
||||||
|
titleContainer: {
|
||||||
|
flex: 1,
|
||||||
|
flexDirection: 'column',
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
marginRight: 16,
|
||||||
|
},
|
||||||
|
title: {
|
||||||
|
fontSize: 24,
|
||||||
|
fontWeight: 'bold',
|
||||||
|
color: 'white',
|
||||||
|
},
|
||||||
|
subTitle: {
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: 'normal',
|
||||||
|
color: 'white',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default Header;
|
|
@ -0,0 +1,20 @@
|
||||||
|
import {render} from '@testing-library/react-native';
|
||||||
|
import Banner from '../Banner';
|
||||||
|
|
||||||
|
describe('Banner tests', () => {
|
||||||
|
it('should be defined', () => {
|
||||||
|
expect(Banner).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render correctly', () => {
|
||||||
|
const {toJSON} = render(<Banner text="text" />);
|
||||||
|
|
||||||
|
expect(toJSON()).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should get text correctly', () => {
|
||||||
|
const {getByText} = render(<Banner text="text" />);
|
||||||
|
|
||||||
|
expect(getByText('text')).toBeDefined();
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,19 @@
|
||||||
|
import {render} from '@testing-library/react-native';
|
||||||
|
import Container from '../Container';
|
||||||
|
import {Text} from 'react-native';
|
||||||
|
|
||||||
|
describe('Container tests', () => {
|
||||||
|
it('should be defined', () => {
|
||||||
|
expect(Container).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render correctly', () => {
|
||||||
|
const {toJSON} = render(
|
||||||
|
<Container>
|
||||||
|
<Text>Test</Text>
|
||||||
|
</Container>,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(toJSON()).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,21 @@
|
||||||
|
import {render} from '@testing-library/react-native';
|
||||||
|
import Header from '../Header';
|
||||||
|
|
||||||
|
describe('Header tests', () => {
|
||||||
|
it('should be defined', () => {
|
||||||
|
expect(Header).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render correctly', () => {
|
||||||
|
const {toJSON} = render(<Header title="Title" subTitle="Subtitle" />);
|
||||||
|
|
||||||
|
expect(toJSON()).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render a title and a subtitle', () => {
|
||||||
|
const {getByText} = render(<Header title="Title" subTitle="Subtitle" />);
|
||||||
|
|
||||||
|
expect(getByText('Title')).toBeDefined();
|
||||||
|
expect(getByText('Subtitle')).toBeDefined();
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,23 @@
|
||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`Banner tests should render correctly 1`] = `
|
||||||
|
<View
|
||||||
|
style={
|
||||||
|
{
|
||||||
|
"justifyContent": "center",
|
||||||
|
"paddingLeft": 8,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Text
|
||||||
|
style={
|
||||||
|
{
|
||||||
|
"color": "white",
|
||||||
|
"fontWeight": "bold",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
text
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
`;
|
|
@ -0,0 +1,15 @@
|
||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`Container tests should render correctly 1`] = `
|
||||||
|
<View
|
||||||
|
style={
|
||||||
|
{
|
||||||
|
"flex": 1,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Text>
|
||||||
|
Test
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
`;
|
|
@ -0,0 +1,87 @@
|
||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`Header tests should render correctly 1`] = `
|
||||||
|
<View
|
||||||
|
style={
|
||||||
|
{
|
||||||
|
"flex": 1,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<View
|
||||||
|
style={
|
||||||
|
{
|
||||||
|
"alignItems": "stretch",
|
||||||
|
"flex": 1,
|
||||||
|
"flexDirection": "row",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<View
|
||||||
|
style={
|
||||||
|
{
|
||||||
|
"alignContent": "center",
|
||||||
|
"justifyContent": "center",
|
||||||
|
"overflow": "hidden",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Text
|
||||||
|
allowFontScaling={false}
|
||||||
|
selectable={false}
|
||||||
|
style={
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"color": "white",
|
||||||
|
"fontSize": 70,
|
||||||
|
},
|
||||||
|
undefined,
|
||||||
|
{
|
||||||
|
"fontFamily": "Material Design Icons",
|
||||||
|
"fontStyle": "normal",
|
||||||
|
"fontWeight": "normal",
|
||||||
|
},
|
||||||
|
{},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
>
|
||||||
|
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
<View
|
||||||
|
style={
|
||||||
|
{
|
||||||
|
"alignItems": "center",
|
||||||
|
"flex": 1,
|
||||||
|
"flexDirection": "column",
|
||||||
|
"justifyContent": "center",
|
||||||
|
"marginRight": 16,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Text
|
||||||
|
style={
|
||||||
|
{
|
||||||
|
"color": "white",
|
||||||
|
"fontSize": 24,
|
||||||
|
"fontWeight": "bold",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Title
|
||||||
|
</Text>
|
||||||
|
<Text
|
||||||
|
style={
|
||||||
|
{
|
||||||
|
"color": "white",
|
||||||
|
"fontSize": 16,
|
||||||
|
"fontWeight": "normal",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Subtitle
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
`;
|
|
@ -0,0 +1,18 @@
|
||||||
|
import {SafeAreaView, StyleSheet} from 'react-native';
|
||||||
|
import BusStopInfoScreen from './BusStopInfoScreen';
|
||||||
|
|
||||||
|
const App = () => {
|
||||||
|
return (
|
||||||
|
<SafeAreaView style={styles.container}>
|
||||||
|
<BusStopInfoScreen />
|
||||||
|
</SafeAreaView>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
container: {
|
||||||
|
flex: 1,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default App;
|
|
@ -0,0 +1,197 @@
|
||||||
|
import {ActivityIndicator, StyleSheet, View} from 'react-native';
|
||||||
|
import Container from '../components/Container';
|
||||||
|
import Header from '../components/Header';
|
||||||
|
import {Text} from 'react-native';
|
||||||
|
import useDevices, {Status} from '../../infraestructure/hooks/useDevices';
|
||||||
|
import Banner from '../components/Banner';
|
||||||
|
|
||||||
|
export interface Line {
|
||||||
|
lineNumber: string;
|
||||||
|
lineLetter: string;
|
||||||
|
letterColor: string;
|
||||||
|
backgroundColor: string;
|
||||||
|
estimatedArrivalTimeInMinutes: string;
|
||||||
|
lineDescription: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TableProps {
|
||||||
|
data: Line[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const Table = ({data}: TableProps) => {
|
||||||
|
const rows = 7;
|
||||||
|
const columns = 3;
|
||||||
|
|
||||||
|
const baseTableData: Line[][] = Array.from({length: rows}, () =>
|
||||||
|
new Array(columns).fill({
|
||||||
|
lineNumber: '',
|
||||||
|
lineLetter: '',
|
||||||
|
letterColor: '',
|
||||||
|
backgroundColor: '',
|
||||||
|
estimatedArrivalTimeInMinutes: '',
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
data.map((item, index) => {
|
||||||
|
const row = Math.floor(index / columns);
|
||||||
|
const column = index % columns;
|
||||||
|
baseTableData[row][column] = item;
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={tableStyles.table}>
|
||||||
|
{baseTableData.map((row, rowIndex) => (
|
||||||
|
<View key={rowIndex} style={tableStyles.row}>
|
||||||
|
{row.map((cell, cellIndex) => {
|
||||||
|
if (cell.lineNumber === '') {
|
||||||
|
return <View style={tableStyles.cell} key={cellIndex} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={[tableStyles.cell]} key={cellIndex}>
|
||||||
|
<View style={tableStyles.lineInformationContainer}>
|
||||||
|
<Text style={tableStyles.lineNumber}>{cell.lineNumber}</Text>
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
tableStyles.letterContainer,
|
||||||
|
{backgroundColor: `#${cell.backgroundColor}` || 'grey'},
|
||||||
|
]}>
|
||||||
|
<Text style={tableStyles.lineLetter}>
|
||||||
|
{cell.lineLetter}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
tableStyles.timeContainer,
|
||||||
|
{backgroundColor: `#${cell.backgroundColor}` || 'grey'},
|
||||||
|
]}>
|
||||||
|
<Text style={tableStyles.time} numberOfLines={1}>
|
||||||
|
{cell.lineDescription}
|
||||||
|
</Text>
|
||||||
|
<Text style={tableStyles.time}>
|
||||||
|
{cell.estimatedArrivalTimeInMinutes}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</View>
|
||||||
|
))}
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const BusStopInfoScreen = () => {
|
||||||
|
const {
|
||||||
|
state: {status, displayedLines, stopName},
|
||||||
|
} = useDevices();
|
||||||
|
|
||||||
|
const splitStopName = stopName.split('-');
|
||||||
|
let title = splitStopName[0] ? `${splitStopName[0].trim()}` : '';
|
||||||
|
const subTitle = splitStopName[1] ? `${splitStopName[1].trim()}` : '';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Container style={styles.container}>
|
||||||
|
<Header
|
||||||
|
style={styles.headerContainer}
|
||||||
|
subTitle={subTitle}
|
||||||
|
title={title}
|
||||||
|
/>
|
||||||
|
<Banner
|
||||||
|
style={styles.bannerContainer}
|
||||||
|
text="Buses que se detienen en esta parada"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<View style={styles.bodyContainer}>
|
||||||
|
{status === Status.LOADING ? (
|
||||||
|
<ActivityIndicator />
|
||||||
|
) : (
|
||||||
|
<Table data={displayedLines} />
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
<View style={styles.footerContainer} />
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
container: {
|
||||||
|
justifyContent: 'center',
|
||||||
|
},
|
||||||
|
headerContainer: {
|
||||||
|
flex: 1.5,
|
||||||
|
backgroundColor: 'orange',
|
||||||
|
},
|
||||||
|
bannerContainer: {
|
||||||
|
flex: 0.5,
|
||||||
|
backgroundColor: 'grey',
|
||||||
|
},
|
||||||
|
bodyContainer: {
|
||||||
|
flex: 10,
|
||||||
|
justifyContent: 'center',
|
||||||
|
},
|
||||||
|
busContainer: {
|
||||||
|
backgroundColor: 'brown',
|
||||||
|
flexDirection: 'column',
|
||||||
|
},
|
||||||
|
footerContainer: {
|
||||||
|
flex: 1,
|
||||||
|
backgroundColor: 'grey',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const tableStyles = StyleSheet.create({
|
||||||
|
table: {
|
||||||
|
flex: 1,
|
||||||
|
backgroundColor: 'white',
|
||||||
|
},
|
||||||
|
row: {
|
||||||
|
flex: 1,
|
||||||
|
flexDirection: 'row',
|
||||||
|
},
|
||||||
|
cell: {
|
||||||
|
flex: 1,
|
||||||
|
borderWidth: 1,
|
||||||
|
flexDirection: 'column',
|
||||||
|
borderColor: 'grey',
|
||||||
|
},
|
||||||
|
lineInformationContainer: {
|
||||||
|
flex: 1,
|
||||||
|
flexDirection: 'row',
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
},
|
||||||
|
timeContainer: {
|
||||||
|
alignItems: 'center',
|
||||||
|
backgroundColor: 'blue',
|
||||||
|
},
|
||||||
|
lineNumber: {
|
||||||
|
fontSize: 20,
|
||||||
|
marginRight: 8,
|
||||||
|
fontWeight: 'bold',
|
||||||
|
color: 'grey',
|
||||||
|
},
|
||||||
|
lineLetter: {
|
||||||
|
fontSize: 14,
|
||||||
|
color: 'white',
|
||||||
|
fontWeight: 'bold',
|
||||||
|
},
|
||||||
|
letterContainer: {
|
||||||
|
paddingHorizontal: 16,
|
||||||
|
paddingVertical: 8,
|
||||||
|
borderRadius: 50,
|
||||||
|
},
|
||||||
|
time: {
|
||||||
|
fontSize: 12,
|
||||||
|
fontWeight: 'bold',
|
||||||
|
color: 'white',
|
||||||
|
},
|
||||||
|
lineDescription: {
|
||||||
|
fontSize: 12,
|
||||||
|
fontWeight: 'bold',
|
||||||
|
color: 'white',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default BusStopInfoScreen;
|
|
@ -0,0 +1,22 @@
|
||||||
|
/**
|
||||||
|
* @param {date} string in format 'HH:mm:ss'
|
||||||
|
*/
|
||||||
|
const getHours = (date: string): number => {
|
||||||
|
return Number.parseInt(date.split(':')[0], 10);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {date} string in format 'HH:mm:ss'
|
||||||
|
*/
|
||||||
|
const getMinutes = (date: string): number => {
|
||||||
|
return Number.parseInt(date.split(':')[1], 10);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {date} string in format 'HH:mm:ss'
|
||||||
|
*/
|
||||||
|
const getSeconds = (date: string): number => {
|
||||||
|
return Number.parseInt(date.split(':')[2], 10);
|
||||||
|
};
|
||||||
|
|
||||||
|
export {getHours, getMinutes, getSeconds};
|
|
@ -0,0 +1,33 @@
|
||||||
|
import {getHours, getMinutes, getSeconds} from '../DateUtils';
|
||||||
|
|
||||||
|
describe('DateUtils tests', () => {
|
||||||
|
describe('getHours tests', () => {
|
||||||
|
it('should be defined', () => {
|
||||||
|
expect(getHours).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return 13', () => {
|
||||||
|
expect(getHours('13:00:00')).toBe(13);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getMinutes tests', () => {
|
||||||
|
it('should be defined', () => {
|
||||||
|
expect(getMinutes).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return 30', () => {
|
||||||
|
expect(getMinutes('13:30:00')).toBe(30);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getSeconds tests', () => {
|
||||||
|
it('should be defined', () => {
|
||||||
|
expect(getSeconds).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return 13', () => {
|
||||||
|
expect(getSeconds('13:30:13')).toBe(13);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
Loading…
Reference in New Issue