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.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.
|
||||
* 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 App from './App';
|
||||
import {name as appName} from './app.json';
|
||||
import App from './src/presentation/screens/App';
|
||||
|
||||
AppRegistry.registerComponent(appName, () => App);
|
||||
|
|
|
@ -14,6 +14,7 @@
|
|||
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 */; };
|
||||
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 */
|
||||
|
||||
/* 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; };
|
||||
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>"; };
|
||||
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; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
|
@ -116,6 +118,7 @@
|
|||
83CBB9F61A601CBA00E9B192 = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
90BEE45D2AFC5DCD0014196C /* Fonts */,
|
||||
13B07FAE1A68108700A75B9A /* SmartStop */,
|
||||
832341AE1AAA6A7D00B99B32 /* Libraries */,
|
||||
00E356EF1AD99517003FC87E /* SmartStopTests */,
|
||||
|
@ -137,6 +140,15 @@
|
|||
name = Products;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
90BEE45D2AFC5DCD0014196C /* Fonts */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
90BEE4642AFC5DCD0014196C /* MaterialCommunityIcons.ttf */,
|
||||
);
|
||||
name = Fonts;
|
||||
path = "../node_modules/react-native-vector-icons/Fonts";
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
BBD78D7AC51CEA395F1C20DB /* Pods */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
|
@ -244,6 +256,7 @@
|
|||
files = (
|
||||
81AB9BB82411601600AC10FF /* LaunchScreen.storyboard in Resources */,
|
||||
13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */,
|
||||
904C36942B12DB0600B0C7C3 /* MaterialCommunityIcons.ttf in Resources */,
|
||||
);
|
||||
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">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>UIAppFonts</key>
|
||||
<array>
|
||||
<string>MaterialCommunityIcons.ttf</string>
|
||||
</array>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>en</string>
|
||||
<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"
|
||||
},
|
||||
"dependencies": {
|
||||
"axios": "^1.6.2",
|
||||
"moment": "^2.29.4",
|
||||
"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": {
|
||||
"@babel/core": "^7.20.0",
|
||||
|
@ -22,9 +26,14 @@
|
|||
"@react-native-community/eslint-config": "^3.2.0",
|
||||
"@react-native/eslint-config": "^0.72.2",
|
||||
"@react-native/metro-config": "^0.72.11",
|
||||
"@testing-library/react-native": "^12.4.1",
|
||||
"@tsconfig/react-native": "^3.0.0",
|
||||
"@types/jest": "^29.5.8",
|
||||
"@types/node": "^20.10.3",
|
||||
"@types/react": "^18.0.24",
|
||||
"@types/react-native-vector-icons": "^6.4.17",
|
||||
"@types/react-test-renderer": "^18.0.0",
|
||||
"axios-mock-adapter": "^1.22.0",
|
||||
"babel-jest": "^29.2.1",
|
||||
"eslint": "^8.19.0",
|
||||
"husky": "^8.0.3",
|
||||
|
@ -32,6 +41,8 @@
|
|||
"metro-react-native-babel-preset": "0.76.8",
|
||||
"prettier": "^2.4.1",
|
||||
"react-test-renderer": "18.2.0",
|
||||
"ts-node": "^10.9.1",
|
||||
"tslib": "^2.6.2",
|
||||
"typescript": "4.8.4"
|
||||
},
|
||||
"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