From 3d440ae81b1254eb9b0cc8b0f20c40f20c9283bb Mon Sep 17 00:00:00 2001 From: Davin Ricketts Date: Mon, 29 Jan 2024 19:33:15 -0500 Subject: [PATCH 1/2] Initialize project --- src/App.css | 71 ---------------------------------- src/App.tsx | 4 +- src/StopWatch.test.tsx | 68 --------------------------------- src/StopWatch.tsx | 74 +----------------------------------- src/StopWatchButton.test.tsx | 66 -------------------------------- src/StopWatchButton.tsx | 51 +------------------------ 6 files changed, 5 insertions(+), 329 deletions(-) delete mode 100644 src/App.css delete mode 100644 src/StopWatch.test.tsx delete mode 100644 src/StopWatchButton.test.tsx diff --git a/src/App.css b/src/App.css deleted file mode 100644 index e5deef95..00000000 --- a/src/App.css +++ /dev/null @@ -1,71 +0,0 @@ -html, body, #root, .stopwatch, .stopwatch-content, .stopwatch-buttons { - height: -webkit-fill-available; -} - -body { - background-color: #f1f1f1; - margin: 0px; -} - -.stopwatch-title { - background-color: #303030; - margin: 0px; - color: white; - padding-left: 16px; - padding: 10px 0px 10px 16px; -} - -.stopwatch-content { - display: flex; -} - -.stopwatch-buttons { - display: flex; - flex-direction: column; - background-color: #ebebeb; - padding: 16px 12px; - width: 200px; -} - -.stopwatch-buttons button:focus { - outline: none; - border: 2px solid #000000; -} - -.stopwatch-buttons button { - margin: 7px 0px; - background-color: #fafafa; - border: 0px solid #fafafa; - text-align: left; - border-radius: 0.5rem; - padding: 7px 0px 7px 15px; - box-shadow: 0.5px 0.5px gray; -} - -.stopwatch-time { - margin-left: auto; - margin-right: auto; - margin-top: 20px; - padding: 50px; - background-color: #ffffff; - height: fit-content; - border-radius: 0.75rem; - width: 50%; - text-align: -webkit-center; - box-shadow: 0.5px 0.5px gray; -} - -.stopwatch-time p { - font-size: xxx-large; -} - -.stopwatch-laptimes ul { - list-style: none; - padding: 0px; -} - -.stopwatch-laptimes li { - padding: 10px 0px; - border-bottom: 1px solid #ebebeb; - font-size: x-large; -} \ No newline at end of file diff --git a/src/App.tsx b/src/App.tsx index 8c90fc53..95351af9 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,9 +1,7 @@ import React from 'react' -import './App.css' -import StopWatch from './StopWatch' export default function App() { return( - +
) } \ No newline at end of file diff --git a/src/StopWatch.test.tsx b/src/StopWatch.test.tsx deleted file mode 100644 index 643343c3..00000000 --- a/src/StopWatch.test.tsx +++ /dev/null @@ -1,68 +0,0 @@ -import React from 'react'; -import { render, fireEvent, screen } from '@testing-library/react'; -import StopWatch, { formatTime } from './StopWatch'; - -// Test the formatTime function -describe('formatTime', () => { - test('formats time less than an hour correctly', () => { - expect(formatTime(5900)).toBe('00:59:00'); - expect(formatTime(6000)).toBe('01:00:00'); - expect(formatTime(359900)).toBe('59:59:00'); - }); - - test('formats time greater than an hour correctly', () => { - expect(formatTime(360000)).toBe('01:00:00:00'); - expect(formatTime(366100)).toBe('01:01:01:00'); - }); -}); - -test('renders correctly', () => { - const { getByText } = render(); - const stopwatchElement = getByText('StopWatch'); - expect(stopwatchElement).not.toBeNull(); -}); - -// Use fake timers for timer-related tests -jest.useFakeTimers(); - -test('starts timer when start button is clicked', () => { - const setIntervalSpy = jest.spyOn(global, 'setInterval'); - render(); - const startButton = screen.getByRole('button', { name: /start/i }); - fireEvent.click(startButton); - jest.advanceTimersByTime(1000); - expect(setIntervalSpy).toHaveBeenCalledTimes(1); - expect(setIntervalSpy).toHaveBeenLastCalledWith(expect.any(Function), 10); -}); - -test('stops timer when stop button is clicked', () => { - const clearIntervalSpy = jest.spyOn(global, 'clearInterval'); - const setIntervalSpy = jest.spyOn(global, 'setInterval'); - setIntervalSpy.mockImplementation(() => 123 as unknown as NodeJS.Timeout); - render(); - const startButton = screen.getByRole('button', { name: /start/i }); - fireEvent.click(startButton); - jest.advanceTimersByTime(1000); - const stopButton = screen.getByRole('button', { name: /stop/i }); - fireEvent.click(stopButton); - expect(clearIntervalSpy).toHaveBeenCalledWith(123); -}); - -beforeEach(() => { - jest.useRealTimers(); -}); - -afterEach(() => { - jest.useFakeTimers(); - jest.clearAllMocks(); -}); - -test('resets timer when reset button is clicked', () => { - const { getByRole, getByText } = render(); - const startButton = getByRole('button', { name: /start/i }); - fireEvent.click(startButton); - jest.advanceTimersByTime(1000); - const resetButton = getByRole('button', { name: /reset/i }); - fireEvent.click(resetButton); - expect(getByText('00:00:00')).not.toBeNull(); -}); \ No newline at end of file diff --git a/src/StopWatch.tsx b/src/StopWatch.tsx index 2e06473c..d58edc78 100644 --- a/src/StopWatch.tsx +++ b/src/StopWatch.tsx @@ -1,77 +1,7 @@ -import React, { useState, useEffect, useCallback } from 'react' -import StopWatchButton from './StopWatchButton' - -// Function to format the time. This is necessary since both the time and lap times need to be formatted -export function formatTime(time: number): string { - // Format the time in mm:ss:ms. Display hours only if reached - const hours = Math.floor(time / 360000); - const minutes = Math.floor((time % 360000) / 6000); - const seconds = Math.floor((time % 6000) / 100); - const milliseconds = time % 100; - // Format the minutes, seconds, and milliseconds to be two digits - const formattedMinutes = minutes.toString().padStart(2, '0'); - const formattedSeconds = seconds.toString().padStart(2, '0'); - const formattedMilliseconds = milliseconds.toString().padStart(2, '0'); - // If stopwatch reaches at least an hour, display the hours - if (hours > 0) { - const formattedHours = hours.toString().padStart(2, '0'); - return `${formattedHours}:${formattedMinutes}:${formattedSeconds}:${formattedMilliseconds}`; - } - // Combine the values into a string - const formattedTime = `${formattedMinutes}:${formattedSeconds}:${formattedMilliseconds}`; - return formattedTime; -} +import React from 'react' export default function StopWatch() { - // State to track the time, whether the timer is on/off, and the lap times - const [time, setTime] = useState(0); - const [timerOn, setTimerOn] = useState(false); - const [lapTimes, setLapTimes] = useState([]); - - // Stops the timer, resets the time, and clears the lap times. useCallback is used to prevent unnecessary re-renders - const handleReset = useCallback(() => { - setTimerOn(false); - setTime(0); - setLapTimes([]); - }, []); - - // Every time timerOn changes, we start or stop the timer - // useEffect is necessary since setInterval changes the state and we don't want to create an infinite loop - useEffect(() => { - let interval: ReturnType | null = null; - - if (timerOn) { - interval = setInterval(() => setTime(time => time + 1), 10) - } - - return () => {clearInterval(interval)} // Clears the interval when the component unmounts or timerOn changes - }, [timerOn]) - return( -
-

StopWatch

-
-
- setTimerOn(true)}> - setTimerOn(false)}> - setLapTimes([...lapTimes, time])} timerOn={timerOn} lapTimes={lapTimes}> - -
-
-

{formatTime(time)}

- {/* Display the numbered lap times */} - {lapTimes.length > 0 && ( -
-

Lap times

-
    - {lapTimes.map((lapTime, index) => { - return
  • {(index + 1)+'.'} {formatTime(lapTime)}
  • - })} -
-
- )} -
-
-
+
) } \ No newline at end of file diff --git a/src/StopWatchButton.test.tsx b/src/StopWatchButton.test.tsx deleted file mode 100644 index c1f7b46d..00000000 --- a/src/StopWatchButton.test.tsx +++ /dev/null @@ -1,66 +0,0 @@ -import React from 'react'; -import { render, fireEvent } from '@testing-library/react'; -import StopWatchButton from './StopWatchButton'; - -test('calls onClick prop when clicked', () => { - const handleClick = jest.fn(); - const { getByRole } = render(); - - fireEvent.click(getByRole('button', { name: /start/i })); - - expect(handleClick).toHaveBeenCalled(); -}); - -test('renders with correct text', () => { - const { getByRole } = render( {}} />); - expect(getByRole('button').textContent).toBe('Start'); -}); - -test('calls onClick prop when clicked with type stop', () => { - const handleClick = jest.fn(); - const { getByRole } = render(); - - fireEvent.click(getByRole('button', { name: /stop/i })); - - expect(handleClick).toHaveBeenCalled(); -}); - -test('calls onClick prop when clicked with type reset', () => { - const handleClick = jest.fn(); - const { getByRole } = render(); - - fireEvent.click(getByRole('button', { name: /reset/i })); - - expect(handleClick).toHaveBeenCalled(); -}); - -test('renders with correct text for type reset', () => { - const { getByRole } = render( {}} />); - expect(getByRole('button').textContent).toBe('Reset'); -}); - -test('calls onClick prop when clicked with type lap', () => { - const handleClick = jest.fn(); - const { getByRole } = render(); - - fireEvent.click(getByRole('button', { name: /lap/i })); - - expect(handleClick).toHaveBeenCalled(); -}); - -test('renders with correct text for type lap', () => { - const { getByRole } = render( {}} timerOn={true} />); - expect(getByRole('button').textContent).toBe('Record Lap'); -}); - -test('does not throw error when clicked without onClick prop', () => { - const { getByRole } = render(); - - expect(() => fireEvent.click(getByRole('button', { name: /start/i }))).not.toThrow(); -}); - -test('lap button is disabled when timer is not running', () => { - const { getByRole } = render( {}} timerOn={false} />); - const buttonElement = getByRole('button') as HTMLButtonElement; - expect(buttonElement.disabled).toBe(true); -}); \ No newline at end of file diff --git a/src/StopWatchButton.tsx b/src/StopWatchButton.tsx index 7f12a5f1..dbd7c174 100644 --- a/src/StopWatchButton.tsx +++ b/src/StopWatchButton.tsx @@ -1,54 +1,7 @@ import React from 'react' -// Maximum number of laps that can be recorded -const maxLaps = 25; - -// Define the props for the StopWatchButton component -type StopWatchButtonProps = { - type: 'start' | 'stop' | 'lap' | 'reset'; - onClick?: () => void; - timerOn?: boolean; - time?: number; - lapTimes?: number[]; -}; - - export default function StopWatchButton({ type, onClick, timerOn, time, lapTimes }: StopWatchButtonProps) { - // Determine the button text based on the type and add corresponding tabIndex - let buttonText, tabIndex; - switch(type) { - case 'start': - buttonText = 'Start'; - tabIndex = 1; - break; - case 'stop': - buttonText = 'Stop'; - tabIndex = 2; - break; - case 'lap': - buttonText = 'Record Lap'; - tabIndex = 3; - break; - case 'reset': - buttonText = 'Reset'; - tabIndex = 4; - break; - default: - buttonText = ''; - tabIndex = 0; - } - // Determine whether the reset or lap buttons should be disabled - const isLapDisabled = !timerOn || (lapTimes && lapTimes.length === 25); - const isResetDisabled = time === 0; +export default function StopWatchButton() { return( - +
) } \ No newline at end of file From 01759ce4d1cd0a93af7fd3a40218c2c4f64a954d Mon Sep 17 00:00:00 2001 From: Davin Ricketts Date: Mon, 29 Jan 2024 23:54:09 -0500 Subject: [PATCH 2/2] Completed Technical Challenge --- src/App.css | 29 ++++++++++++ src/App.tsx | 7 ++- src/StopWatch.test.tsx | 87 ++++++++++++++++++++++++++++++++++ src/StopWatch.tsx | 100 ++++++++++++++++++++++++++++++++++++++-- src/StopWatchButton.tsx | 28 ++++++++--- 5 files changed, 239 insertions(+), 12 deletions(-) create mode 100644 src/App.css create mode 100644 src/StopWatch.test.tsx diff --git a/src/App.css b/src/App.css new file mode 100644 index 00000000..52bae846 --- /dev/null +++ b/src/App.css @@ -0,0 +1,29 @@ +body{ + font-family: 'roboto', sans-serif; +} + +.container{ + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; +} + +.stopwatch-display{ + font-size: 100px; + font-family: 'roboto mono', monospace; +} + +.button{ + margin: 0.5em; + font-size: 25px; + width: 7rem; + padding: 0.5em; +} + +.lap-list{ + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; +} diff --git a/src/App.tsx b/src/App.tsx index 95351af9..77f3e2d3 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,7 +1,12 @@ import React from 'react' +import StopWatch from './StopWatch' +import './App.css' + export default function App() { return( -
+
+ +
) } \ No newline at end of file diff --git a/src/StopWatch.test.tsx b/src/StopWatch.test.tsx new file mode 100644 index 00000000..6196f90d --- /dev/null +++ b/src/StopWatch.test.tsx @@ -0,0 +1,87 @@ +import React from 'react'; +import { render, fireEvent } from '@testing-library/react'; // Import render and fireEvent from testing-library +import StopWatchButton from './StopWatchButton'; // Import the StopWatchButton component +import StopWatch from './StopWatch'; // Import the StopWatch component + +describe('StopWatch component', () => { + // Test to ensure that the StopWatch component renders without crashing + test('renders without crashing', () => { + render(); + }); + + // Test to check if clicking the start button triggers the onStart function + test('clicking start button triggers onStart function', () => { + // Create a mock function for onStart + const onStart = jest.fn(); + // Render the StopWatchButton component with the mock onStart function + const { getByText } = render(); + + // Simulate a click on the start button + fireEvent.click(getByText('Start')); + // Assert that onStart function has been called + expect(onStart).toHaveBeenCalled(); + }); + + // Test to check if clicking the stop button triggers the onStop function + test('clicking stop button triggers onStop function', () => { + // Create a mock function for onStop + const onStop = jest.fn(); + // Render the StopWatchButton component with the mock onStop function + const { getByText } = render(); + + // Simulate a click on the stop button + fireEvent.click(getByText('Stop')); + // Assert that onStop function has been called + expect(onStop).toHaveBeenCalled(); + }); + + // Test to check if clicking the lap button triggers the onLap function + test('clicking lap button triggers onLap function', () => { + // Create a mock function for onLap + const onLap = jest.fn(); + // Render the StopWatchButton component with the mock onLap function + const { getByText } = render(); + + // Simulate a click on the lap button + fireEvent.click(getByText('Lap')); + // Assert that onLap function has been called + expect(onLap).toHaveBeenCalled(); + }); + + // Test to check if clicking the reset button triggers the onReset function + test('clicking reset button triggers onReset function', () => { + // Create a mock function for onReset + const onReset = jest.fn(); + // Render the StopWatchButton component with the mock onReset function + const { getByText } = render(); + + // Simulate a click on the reset button + fireEvent.click(getByText('Reset')); + // Assert that onReset function has been called + expect(onReset).toHaveBeenCalled(); + }); +}); diff --git a/src/StopWatch.tsx b/src/StopWatch.tsx index d58edc78..a6f37ce3 100644 --- a/src/StopWatch.tsx +++ b/src/StopWatch.tsx @@ -1,7 +1,97 @@ -import React from 'react' +import React, { useState, useRef, useEffect } from "react"; +import StopWatchButton from "./StopWatchButton"; export default function StopWatch() { - return( -
- ) -} \ No newline at end of file + // Define state variables using the useState hook + const [startTime, setStartTime] = useState(null); // Start time of the stopwatch + const [elaspedTime, setElaspedTime] = useState(null); // Elapsed time since starting the stopwatch + const [isRunning, setIsRunning] = useState(null); // Flag to indicate if the stopwatch is running or not + const [lapHistory, setLapHistory] = useState([]); // Array to store lap times + const [lapTime, setLapTime] = useState(null); // Last lap time recorded + const intervalRef = useRef(null); // Reference to the interval used for updating the stopwatch + + // Calculate the elapsed time in seconds + let secondsPassed: number = 0; + + // Function to format time in hours:minutes:seconds.milliseconds format + const formatTime = (time: number): string => { + const hours = Math.floor(time / 3600); + const minutes = Math.floor((time % 3600) / 60).toString().padStart(2, '0'); + const seconds = Math.floor(time % 60).toString().padStart(2, '0'); + const milliseconds = Math.floor((time % 1) * 1000).toString().padStart(3, '0'); + + if (hours > 0) { + return `${hours}:${minutes}:${seconds}.${milliseconds}`; + } else { + return `${minutes}:${seconds}.${milliseconds}`; + } + }; + + // Function to handle starting the stopwatch + const handleStart = () => { + if (!isRunning) { + const currentTime = Date.now(); + // Set start time and elapsed time + setStartTime(currentTime - elaspedTime); + setElaspedTime(currentTime); + setLapHistory([...lapHistory]); + setLapTime(currentTime); + // Start the interval for updating the elapsed time + clearInterval(intervalRef.current); + intervalRef.current = setInterval(() => { + setElaspedTime(Date.now() - startTime); + }, 50); + setIsRunning(true); // Set running flag to true + } + }; + + // Function to handle stopping the stopwatch + const handleStop = () => { + clearInterval(intervalRef.current); // Clear the interval + setIsRunning(false); // Set running flag to false + }; + + // Calculate the elapsed time in seconds if both start and elapsed times are set + if (startTime != null && elaspedTime != null) { + secondsPassed = (elaspedTime - startTime) / 1000; + } + + // Function to handle recording a lap time + const handleLap = () => { + if (isRunning) { + const lapElapsedTime: number = Date.now() - lapTime; + setLapHistory([...lapHistory, lapElapsedTime]); // Add lap time to lap history + setLapTime(Date.now()); // Set the current time as the last lap time + } + }; + + // Function to handle resetting the stopwatch + const handleReset = () => { + clearInterval(intervalRef.current); // Clear the interval + setElaspedTime(0); // Reset elapsed time + setStartTime(null); // Reset start time + setIsRunning(false); // Set running flag to false + setLapHistory([]); // Clear lap history + setLapTime(null); // Reset last lap time + }; + + // Render the stopwatch component + return ( +
+

Stopwatch

+
{formatTime(secondsPassed)}
+ +
    +

    Lap Time

    + {lapHistory.map((lapTime, index) => ( +
  1. {formatTime((lapTime/1000))}
  2. + ))} +
+
+ ); +} diff --git a/src/StopWatchButton.tsx b/src/StopWatchButton.tsx index dbd7c174..86de1dca 100644 --- a/src/StopWatchButton.tsx +++ b/src/StopWatchButton.tsx @@ -1,7 +1,23 @@ -import React from 'react' +import React from 'react'; -export default function StopWatchButton() { - return( -
- ) -} \ No newline at end of file +// Define the Props type for the StopWatchButton component +type Props = { + onStart: () => void; // Function to handle the start button click event + onStop: () => void; // Function to handle the stop button click event + onLap: () => void; // Function to handle the lap button click event + onReset: () => void; // Function to handle the reset button click event +} + +// Define the StopWatchButton functional component +export default function StopWatchButton({ onStart, onStop, onLap, onReset }: Props) { + + return ( + // Render the buttons for start, stop, lap, and reset +
+ + + + +
+ ); +}