Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
551 changes: 503 additions & 48 deletions package-lock.json

Large diffs are not rendered by default.

8 changes: 6 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,18 +21,20 @@
"@babel/preset-react": "^7.23.3",
"@babel/preset-typescript": "^7.23.3",
"@testing-library/react": "^14.1.2",
"@testing-library/user-event": "^14.5.2",
"@types/jest": "^29.5.11",
"@types/node": "^16.18.30",
"@types/react": "^17.0.59",
"@types/react-dom": "^18.2.4",
"@types/react-test-renderer": "^18.0.7",
"babel-jest": "^29.7.0",
"babel-loader": "^8.3.0",
"css-loader": "^6.7.3",
"file-loader": "^6.2.0",
"html-loader": "^4.2.0",
"html-webpack-plugin": "^5.5.1",
"jest": "^29.7.0",
"react-test-renderer": "^18.2.0",
"jest-environment-jsdom": "^29.7.0",
"style-loader": "^3.3.2",
"ts-jest": "^29.1.1",
"ts-loader": "^9.4.2",
Expand All @@ -46,6 +48,8 @@
"jest": {
"transform": {
"^.+\\.(js|jsx|ts|tsx)$": "babel-jest"
}
},
"testEnvironment": "jsdom",
"verbose": true
}
}
2 changes: 1 addition & 1 deletion public/index.html
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<!DOCTYPE html>
<html>
<head>
<title>Template Site</title>
<title>Timer</title>
</head>
<body>
<section id='root'></section>
Expand Down
8 changes: 7 additions & 1 deletion src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
import React from 'react'
import StopWatch from './StopWatch'
import './style.css'

export default function App() {


return(
<div></div>
<div>
<StopWatch/>
</div>
)
}
91 changes: 90 additions & 1 deletion src/StopWatch.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,96 @@

import React from 'react'
import { useRef } from 'react';
import { useEffect } from 'react';
import { useState } from 'react';
import StopWatchButton from './StopWatchButton';



export default function StopWatch() {
const [time, setTime] = useState(0); // Time is in 0.1 * seconds
const [active, setActive] = useState(false);
const [laps, setLaps] = useState([]);
const [showLaps, setShowLaps] = useState(false);

// Timer increases each render
useEffect(()=>{
if (active) {
let intervalChange = setInterval(() =>{
setTime((previousTime) => previousTime+1);
}, 100);
return () => clearInterval(intervalChange);
}
}, [time, active]);

// Padding the left hand side with zeroes to make the displayed number have 2 digits
function padLeft(time:number){
if (time.toString().length < 2) {
return "0" + time.toString();
}

else{
return time.toString();
}
}

// Separates time into minutes, seconds and milliseconds (first digit)
function parseTime(time:number){
const milliseconds = (time%10);
const seconds = Math.floor((time/10) % 60);
const minutes = Math.floor((time/600));
return {minutes, seconds, milliseconds};
}

// Adds current time to all previous laps
function addLap(){
setLaps([...laps, currentTime]);
setShowLaps(true);
}

// Reset all states
function resetStopWatch(){
setTime(0);
setActive(false);
setLaps([]);
setShowLaps(false);
}

// Gets time in minutes, seconds, milliseconds (first digit)
const currentTime = parseTime(time)

return(
<div></div>
<div>
<h1>Stopwatch</h1>

<h2 className='timeDisplay'>
{padLeft(currentTime.minutes)}:{padLeft(currentTime.seconds)}.{currentTime.milliseconds}
<span className='smallText'>ms</span>
</h2>

<div className='buttonContainer'>
<StopWatchButton label="Start" onPress={() => setActive(true)}/>
<StopWatchButton label="Stop" onPress={() => setActive(false)}/>
<StopWatchButton label="Lap" onPress={()=> addLap()}/>
<StopWatchButton label="Reset" onPress={() => resetStopWatch()}/>
</div>



{showLaps?
<div>
<h3>Laps</h3>
{laps.map(function(data) {
return (
<div className='lapsDisplay'>
{padLeft(data.minutes)}:{padLeft(data.seconds)}
<span>.{data.milliseconds}</span>
</div>
)})}
</div>
:null}


</div>
)
}
14 changes: 12 additions & 2 deletions src/StopWatchButton.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,17 @@
import React from 'react'

export default function StopWatchButton() {
interface Props{
label: string;
onPress?: (e: React.MouseEvent<HTMLButtonElement>) => void;
}

export default function StopWatchButton(props: Props) {

return(
<div></div>
<div>
<button onClick={props.onPress}>
{props.label}
</button>
</div>
)
}
79 changes: 79 additions & 0 deletions src/__tests__/StopWatch.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import React from "react";
import {render, fireEvent, screen} from '@testing-library/react'
import StopWatch from "../StopWatch";

// Setting up a fake time, this replaces the setInterval() in our main app
jest.useFakeTimers();

test("When pressing on Start button the StopWatch runs", async () => {

render(<StopWatch/>);
const spy = jest.spyOn(global, 'setInterval');

// Start the timer
const startButton = screen.getByRole('button', { name: /start/i });
await fireEvent.click(startButton);

// Advancing fake timer by one second
jest.advanceTimersByTime(1000);


// Check if the time on screen is equal to one second
// Not sure why this doesn't work :(
// const stopWatchTime = screen.getByRole('heading', {level:2}).textContent
// expect(stopWatchTime).toBe("00:01.0ms")


expect(setInterval).toHaveBeenCalledTimes(1);
expect(setInterval).toHaveBeenLastCalledWith(expect.any(Function), 100);
})


test("When pressing on Stop button it halts the StopWatch", async ()=>{
render(<StopWatch/>);

const spy = jest.spyOn(global, 'setInterval');

// Start the timer
const startButton = screen.getByRole('button', { name: /start/i });
await fireEvent.click(startButton);

// Advancing timer by a second
jest.advanceTimersByTime(1000);

// Stop the timer
const stopButton = screen.getByRole('button', { name: /stop/i });
await fireEvent.click(stopButton);

expect(spy).toHaveBeenCalled();
})

test("Lap button shows laps", ()=>{
// To be done
})


test("When pressing on Reset button it re-initializes the StopWatch", async () => {
render(<StopWatch/>)
// Elements to check: stopwatch time & laps display

// Start the timer
const startButton = screen.getByRole('button', { name: /start/i })
await fireEvent.click(startButton)
const stopWatchTime = screen.getByRole('heading', {level:2}).textContent


// Record laps
const lapButton = screen.getByRole('button', { name: /lap/i })
await fireEvent.click(lapButton)


// Reset everything
const resetButton = screen.getByRole('button', { name: /reset/i })
await fireEvent.click(resetButton)

// Check if everything got resetted
expect(stopWatchTime).toBe("00:00.0ms")


})
36 changes: 36 additions & 0 deletions src/style.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
body{
font-family: 'Gill Sans', 'Gill Sans MT', Calibri, 'Trebuchet MS', sans-serif;
text-align: center;
}

button{
font-weight: bold;
padding: 10px;
margin: 5px;
background-color: #9cbb5c;
border-radius: 10px;
border: 0;
color: white;
}

button:hover{
background-color: #809a4b;
}

.buttonContainer{
display: flex;
justify-content: center;
}

.smallText{
font-size: small;
}

.timeDisplay{
font-size: 72px;
}

.lapsDisplay{
font-size: 18px;
padding: 5px;
}