-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge branch 'feature/ui/countdown' of github.com:psmgeelen/projectte…
…a into feature/ui/countdown
- Loading branch information
Showing
19 changed files
with
369 additions
and
143 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,2 +1,9 @@ | ||
.App { | ||
} | ||
|
||
.countdown-area { | ||
width: 100%; | ||
text-align: center; | ||
font-weight: bold; | ||
font-size: 2rem; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
function toTimeStr(diff) { | ||
const seconds = Math.floor(diff / 1000); | ||
const minutes = Math.floor(seconds / 60); | ||
const hours = Math.floor(minutes / 60); | ||
|
||
const secondsStr = (seconds % 60).toString().padStart(2, '0'); | ||
const minutesStr = (minutes % 60).toString().padStart(2, '0'); | ||
const hoursStr = hours.toString().padStart(2, '0'); | ||
|
||
return `${hoursStr}:${minutesStr}:${secondsStr}`; | ||
} | ||
|
||
export function timeBetweenAsString({endTime=null, startTime=null}) { | ||
if (null === startTime) startTime = new Date(); | ||
if (null === endTime) endTime = new Date(); | ||
|
||
const diff = endTime - startTime; // in ms | ||
if (diff < 0) return '-' + toTimeStr(-diff); | ||
return toTimeStr(diff); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,65 @@ | ||
class CWaterPumpAPIImpl { | ||
constructor({ client, currentTime=null }) { | ||
this._client = client; | ||
this._currentTime = currentTime || Date.now; | ||
} | ||
|
||
async _execute(callback) { | ||
const start = this._currentTime(); | ||
const response = await callback(); | ||
const end = this._currentTime(); | ||
return { response, requestTime: end - start }; | ||
} | ||
|
||
async start(runTimeMs) { | ||
const { response: { data }, requestTime } = await this._execute( | ||
async () => await this._client.get('/pour_tea', { params: { milliseconds: runTimeMs } }) | ||
); | ||
return this.preprocessResponse({ response: data, requestTime }); | ||
} | ||
|
||
async stop() { | ||
const { response: { data }, requestTime } = await this._execute( | ||
async () => await this._client.get('/stop', { params: {} }) | ||
); | ||
return this.preprocessResponse({ response: data, requestTime }); | ||
} | ||
|
||
async status() { | ||
const { response: { data }, requestTime } = await this._execute( | ||
async () => await this._client.get('/status', { params: {} }) | ||
); | ||
return this.preprocessResponse({ response: data, requestTime }); | ||
} | ||
/////////////////////// | ||
// helper functions | ||
preprocessResponse({ response, requestTime }) { | ||
if(null == response) return null; | ||
if('error' in response) { | ||
throw new Error(response.error); | ||
} | ||
// make a deep copy of response | ||
response = JSON.parse(JSON.stringify(response)); | ||
// normal response | ||
// convert "water threshold" to "waterThreshold" | ||
response.waterThreshold = response["water threshold"]; | ||
delete response["water threshold"]; | ||
|
||
// convert "time left" to "timeLeft" and adjust time | ||
response.pump.timeLeft = response.pump["time left"]; | ||
delete response.pump["time left"]; | ||
|
||
// adjust time by network delay | ||
const oneWayTripTime = Math.round(requestTime / 2); | ||
response.time += oneWayTripTime; | ||
response.pump.timeLeft -= oneWayTripTime; | ||
|
||
const now = this._currentTime(); | ||
response.updated = now; | ||
response.pump.estimatedEndTime = response.pump.timeLeft + now; | ||
return response; | ||
} | ||
} | ||
|
||
export default CWaterPumpAPIImpl; | ||
export { CWaterPumpAPIImpl }; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,135 @@ | ||
import { CWaterPumpAPIImpl } from './CWaterPumpAPIImpl.js'; | ||
|
||
describe('CWaterPumpAPIImpl', () => { | ||
const DUMMY_STATUS = { | ||
pump: { | ||
"running": true, | ||
"time left": 1000, | ||
"water threshold": 100, | ||
}, | ||
time: 1000, | ||
}; | ||
// common test cases | ||
async function shouldThrowErrorFromResponse(apiCall) { | ||
const mockClient = { get: jest.fn() }; | ||
const errorMessage = 'Error ' + Math.random(); | ||
mockClient.get.mockResolvedValue({ data: { error: errorMessage } }); | ||
|
||
const api = new CWaterPumpAPIImpl({ client: mockClient }); | ||
await expect(apiCall(api)).rejects.toThrow(errorMessage); | ||
} | ||
|
||
async function shouldBeCalledWith(apiCall, url, params) { | ||
const mockClient = { get: jest.fn() }; | ||
mockClient.get.mockResolvedValue({ data: DUMMY_STATUS }); | ||
|
||
const api = new CWaterPumpAPIImpl({ client: mockClient }); | ||
await apiCall(api); | ||
|
||
expect(mockClient.get).toHaveBeenCalledWith(url, { params }); | ||
} | ||
|
||
async function shouldRethrowError(apiCall) { | ||
const mockClient = { get: jest.fn() }; | ||
mockClient.get.mockRejectedValue(new Error('Network Error')); | ||
|
||
const api = new CWaterPumpAPIImpl({ client: mockClient }); | ||
await expect(apiCall(api)).rejects.toThrow('Network Error'); | ||
} | ||
|
||
async function shouldPreprocessResponse(apiCall) { | ||
const mockClient = { get: jest.fn() }; | ||
mockClient.get.mockResolvedValue({ data: DUMMY_STATUS }); | ||
|
||
const api = new CWaterPumpAPIImpl({ client: mockClient }); | ||
const response = await apiCall(api); | ||
|
||
expect(response.waterThreshold).toBe(DUMMY_STATUS["water threshold"]); | ||
expect(response.pump.timeLeft).toBe(DUMMY_STATUS.pump["time left"]); | ||
expect(response).toHaveProperty('updated'); | ||
} | ||
// end of common test cases | ||
// tests per method | ||
describe('start', () => { | ||
it('common test cases', async () => { | ||
const T = Math.random() * 1000; | ||
const callback = async (api) => await api.start(T); | ||
await shouldThrowErrorFromResponse(callback); | ||
await shouldRethrowError(callback); | ||
await shouldPreprocessResponse(callback); | ||
await shouldBeCalledWith(callback, '/pour_tea', { milliseconds: T }); | ||
}); | ||
}); | ||
|
||
describe('stop', () => { | ||
it('common test cases', async () => { | ||
const callback = async (api) => await api.stop(); | ||
await shouldThrowErrorFromResponse(callback); | ||
await shouldRethrowError(callback); | ||
await shouldPreprocessResponse(callback); | ||
await shouldBeCalledWith(callback, '/stop', {}); | ||
}); | ||
}); | ||
|
||
describe('status', () => { | ||
it('common test cases', async () => { | ||
const callback = async (api) => await api.status(); | ||
await shouldThrowErrorFromResponse(callback); | ||
await shouldRethrowError(callback); | ||
await shouldPreprocessResponse(callback); | ||
await shouldBeCalledWith(callback, '/status', {}); | ||
}); | ||
}); | ||
// tests for helper function preprocessResponse | ||
describe('preprocessResponse', () => { | ||
it('should return null if response is null', () => { | ||
const api = new CWaterPumpAPIImpl({ client: {} }); | ||
expect(api.preprocessResponse({ response: null, requestTime: 0 })).toBeNull(); | ||
}); | ||
|
||
it('should throw error if response has error', () => { | ||
const api = new CWaterPumpAPIImpl({ client: {} }); | ||
const errorMessage = 'Error ' + Math.random(); | ||
expect(() => api.preprocessResponse({ | ||
response: { error: errorMessage }, | ||
requestTime: 0, | ||
})).toThrow(errorMessage); | ||
}); | ||
|
||
it('should preprocess response', () => { | ||
const api = new CWaterPumpAPIImpl({ client: {} }); | ||
const response = api.preprocessResponse({ response: DUMMY_STATUS, requestTime: 0 }); | ||
expect(response.waterThreshold).toBe(DUMMY_STATUS["water threshold"]); | ||
expect(response.pump.timeLeft).toBe(DUMMY_STATUS.pump["time left"]); | ||
}); | ||
|
||
it('should add field "updated" with current time', () => { | ||
const T = Math.random() * 1000; | ||
const api = new CWaterPumpAPIImpl({ client: {}, currentTime: () => T }); | ||
const response = api.preprocessResponse({ response: DUMMY_STATUS, requestTime: 0 }); | ||
expect(response.updated).toBe(T); | ||
}); | ||
|
||
/////////// | ||
// Scenario: | ||
// 00:00.000 - client sends request | ||
// 00:00.100 - server receives request and set 'time' to 00:00.100, timeLeft = 1234ms | ||
// 00:00.200 - server sends response | ||
// 00:00.300 - client receives response, but 'time' is 00:00.100 and timeLeft = 1234ms | ||
// total time: 300ms | ||
// on average, time to one-way trip is 150ms | ||
// so, we adjust time by 150ms i.e. time = 00:00.250, timeLeft = 1084ms | ||
// estimatedEndTime = 00:00.300 + 1084ms = 00:01.384 | ||
it('should adjust time', () => { | ||
const responseObj = JSON.parse(JSON.stringify(DUMMY_STATUS)); | ||
responseObj.time = 100; | ||
responseObj.pump["time left"] = 1234; | ||
|
||
const api = new CWaterPumpAPIImpl({ client: {}, currentTime: () => 300 }); | ||
const response = api.preprocessResponse({ response: responseObj, requestTime: 300 }); | ||
expect(response.time).toBe(250); | ||
expect(response.pump.timeLeft).toBe(1084); | ||
expect(response.pump.estimatedEndTime).toBe(1384); | ||
}); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
import React from "react"; | ||
import { connect } from "react-redux"; | ||
import TimerArea from "./TimerArea"; | ||
|
||
export function CurrentOperationInfoAreaComponent({ | ||
isRunning, estimatedEndTime | ||
}) { | ||
if (!isRunning) return null; | ||
return ( | ||
<div className="countdown-area"> | ||
<TimerArea startTime={null} endTime={estimatedEndTime} /> | ||
</div> | ||
); | ||
} | ||
|
||
export default connect( | ||
state => ({ | ||
isRunning: state.systemStatus.pump.running, | ||
estimatedEndTime: state.systemStatus.pump.estimatedEndTime, | ||
}), | ||
[] | ||
)(CurrentOperationInfoAreaComponent); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,19 +1,23 @@ | ||
import React from 'react'; | ||
import { connect } from 'react-redux'; | ||
import { Alert } from 'react-bootstrap'; | ||
import { useNotificationsSystem } from '../contexts/NotificationsContext'; | ||
import { NotificationsSystemActions } from '../store/slices/Notifications'; | ||
|
||
function NotificationsArea() { | ||
const NotificationsSystem = useNotificationsSystem(); | ||
const { currentNotifications } = NotificationsSystem; | ||
if(!currentNotifications) return null; | ||
|
||
const hideNotifications = () => { NotificationsSystem.clear(); }; | ||
function NotificationsArea({ hasNotifications, message, clearNotifications }) { | ||
if(!hasNotifications) return null; | ||
|
||
return ( | ||
<Alert variant="info" onClose={hideNotifications} dismissible> | ||
{currentNotifications.message} | ||
<Alert variant="info" onClose={clearNotifications} dismissible> | ||
{message} | ||
</Alert> | ||
); | ||
} | ||
|
||
export default NotificationsArea; | ||
export default connect( | ||
(state) => ({ | ||
hasNotifications: state.notifications.currentNotifications != null, | ||
message: state.notifications.currentNotifications?.message | ||
}), { | ||
clearNotifications: NotificationsSystemActions.clear | ||
} | ||
)(NotificationsArea); |
Oops, something went wrong.