diff --git a/App.tsx b/App.tsx
index 0329d0c..fbe36a9 100644
--- a/App.tsx
+++ b/App.tsx
@@ -1,20 +1,21 @@
-import { StatusBar } from 'expo-status-bar';
-import { StyleSheet, Text, View } from 'react-native';
+import {SafeAreaView, StyleSheet} from 'react-native';
+import StopWatch from "./src/StopWatch";
+import React from "react";
export default function App() {
- return (
-
- Open up App.tsx to start working on your app!
-
-
- );
+ return (
+
+
+
+ )
}
+
const styles = StyleSheet.create({
- container: {
- flex: 1,
- backgroundColor: '#fff',
- alignItems: 'center',
- justifyContent: 'center',
- },
-});
+ container: {
+ flex: 1,
+ flexDirection: 'column',
+ alignItems: 'center',
+ backgroundColor: 'black'
+ }
+})
\ No newline at end of file
diff --git a/package-lock.json b/package-lock.json
index ea36168..a360b56 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -9208,9 +9208,9 @@
}
},
"node_modules/expo": {
- "version": "49.0.21",
- "resolved": "https://registry.npmjs.org/expo/-/expo-49.0.21.tgz",
- "integrity": "sha512-JpHL6V0yt8/fzsmkAdPdtsah+lU6Si4ac7MDklLYvzEil7HAFEsN/pf06wQ21ax4C+BL27hI6JJoD34tzXUCJA==",
+ "version": "49.0.22",
+ "resolved": "https://registry.npmjs.org/expo/-/expo-49.0.22.tgz",
+ "integrity": "sha512-1hhcphaKN74gDqEmGzU4sqxnusLi/i8SsWZ04rRn7b6zdyEchyudVLN3SOzeIUgfGmn7AcXm78JAQ7+e0WqSyw==",
"dependencies": {
"@babel/runtime": "^7.20.0",
"@expo/cli": "0.10.16",
@@ -9225,7 +9225,7 @@
"expo-font": "~11.4.0",
"expo-keep-awake": "~12.3.0",
"expo-modules-autolinking": "1.5.1",
- "expo-modules-core": "1.5.12",
+ "expo-modules-core": "1.5.13",
"fbemitter": "^3.0.0",
"invariant": "^2.2.4",
"md5-file": "^3.2.3",
@@ -9415,9 +9415,9 @@
}
},
"node_modules/expo-modules-core": {
- "version": "1.5.12",
- "resolved": "https://registry.npmjs.org/expo-modules-core/-/expo-modules-core-1.5.12.tgz",
- "integrity": "sha512-mY4wTDU458dhwk7IVxLNkePlYXjs9BTgk4NQHBUXf0LapXsvr+i711qPZaFNO4egf5qq6fQV+Yfd/KUguHstnQ==",
+ "version": "1.5.13",
+ "resolved": "https://registry.npmjs.org/expo-modules-core/-/expo-modules-core-1.5.13.tgz",
+ "integrity": "sha512-cKRsiHKwpDPRkBgMW3XdUWmEUDzihEPWXAyeo629BXpJ6uX6a66Zbz63SEXhlgsbLq8FB77gvYku3ceBqb+hHg==",
"dependencies": {
"compare-versions": "^3.4.0",
"invariant": "^2.2.4"
diff --git a/package.json b/package.json
index bf0f5a3..ac35609 100644
--- a/package.json
+++ b/package.json
@@ -12,10 +12,10 @@
"dependencies": {
"expo": "~49.0.15",
"expo-status-bar": "~1.6.0",
- "react": "18.2.0",
- "react-native": "0.72.6",
+ "jest": "^29.2.1",
"jest-expo": "~49.0.0",
- "jest": "^29.2.1"
+ "react": "18.2.0",
+ "react-native": "0.72.6"
},
"devDependencies": {
"@babel/core": "^7.20.0",
diff --git a/src/StopWatch.tsx b/src/StopWatch.tsx
index 5c7eb74..75aad88 100644
--- a/src/StopWatch.tsx
+++ b/src/StopWatch.tsx
@@ -1,8 +1,112 @@
-import { View } from 'react-native';
+import {StyleSheet, Text, View} from 'react-native';
+import React, {useEffect, useState} from "react";
+import LapList, {LapModel} from "./components/LapList";
+import StopWatchButton from "./StopWatchButton";
+import {formatTime} from "./common/TimeFormatter";
export default function StopWatch() {
- return (
-
-
- );
-}
\ No newline at end of file
+
+ const [elapsedTime, setElapsedTime] = useState(0)
+ const [isRunning, setIsRunning] = useState(false)
+ const [laps, setLaps] = useState([])
+ const [lastLapTime, setLastLapTime] = useState(0)
+
+ const onLapClicked = () => {
+ if (isRunning) {
+ // Calculate the lap time
+ const lapTime = elapsedTime - lastLapTime;
+ setLaps((prevLaps) => [...prevLaps, lapTime]);
+ setLastLapTime(elapsedTime);
+ }
+ }
+
+ const onResetClicked = () => {
+ setIsRunning(false)
+ setLaps([])
+ setLastLapTime(0)
+ setElapsedTime(0)
+
+ }
+
+ const onStopClicked = () => {
+ setIsRunning(false)
+ }
+
+ const onStartClicked = () => {
+ setIsRunning(true)
+ }
+
+ useEffect(() => {
+ let intervalId: number;
+
+ if (isRunning) {
+ intervalId = setInterval(() => {
+ setElapsedTime((prevElapsedTime) => prevElapsedTime + 100);
+ }, 100);
+ }
+
+ return () => {
+ if (intervalId) {
+ clearInterval(intervalId);
+ }
+ };
+ }, [isRunning]);
+
+ return (
+ <>
+
+ {formatTime(elapsedTime)}
+
+
+
+
+
+
+
+
+
+
+ {laps.length > 0 && ({
+ title: `Lap ${index + 1}`,
+ time: lap,
+ }))
+ }/>}
+
+ >
+
+ )
+}
+
+const styles = StyleSheet.create({
+ displayTIme: {
+ flex: 1,
+ alignItems: 'flex-end',
+ flexDirection: 'row',
+ paddingHorizontal: 10,
+ },
+
+ buttons: {
+ flex: 1,
+ flexDirection: 'row',
+ gap: 10,
+ alignItems: 'flex-end',
+ paddingHorizontal: 10,
+ },
+
+ lapList: {
+ flex: 2,
+ width: '100%',
+ paddingHorizontal: 16,
+ marginTop: 16
+ },
+
+ time: {
+ fontSize: 70,
+ fontWeight: 'normal',
+ width: "100%",
+ textAlign: 'center',
+ color: 'white'
+
+ }
+})
\ No newline at end of file
diff --git a/src/StopWatchButton.tsx b/src/StopWatchButton.tsx
index 8768555..06403a2 100644
--- a/src/StopWatchButton.tsx
+++ b/src/StopWatchButton.tsx
@@ -1,8 +1,30 @@
-import { View } from 'react-native';
-
-export default function StopWatchButton() {
- return (
-
-
- );
-}
\ No newline at end of file
+import {Button, StyleSheet, View} from 'react-native';
+
+interface StopWatchButtonProps {
+ label: string
+ onPress: () => void
+
+}
+
+export default function StopWatchButton({label, onPress}: StopWatchButtonProps) {
+ return (
+
+
+ )
+}
+
+
+const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ backgroundColor: 'gray',
+ height: 50,
+ justifyContent: 'center',
+ borderRadius: 10,
+ overflow: 'hidden'
+ },
+ text: {
+ color: '#fff',
+ }
+})
\ No newline at end of file
diff --git a/src/common/TimeFormatter.ts b/src/common/TimeFormatter.ts
new file mode 100644
index 0000000..a6c3d8a
--- /dev/null
+++ b/src/common/TimeFormatter.ts
@@ -0,0 +1,12 @@
+export const formatTime = (time: number): string => {
+ const minutes = Math.floor(time / (1000 * 60));
+ const seconds = Math.floor((time % (1000 * 60)) / 1000);
+ const milliseconds = Math.floor((time % 1000) / 10);
+
+ // Pad single-digit seconds and milliseconds with leading zeros
+ const formattedMinutes = minutes < 10 ? `0${minutes}` : `${minutes}`;
+ const formattedSeconds = seconds < 10 ? `0${seconds}` : `${seconds}`;
+ const formattedMilliseconds = milliseconds < 10 ? `0${milliseconds}` : `${milliseconds}`;
+
+ return `${formattedMinutes}:${formattedSeconds}:${formattedMilliseconds}`;
+};
\ No newline at end of file
diff --git a/src/components/LapList.tsx b/src/components/LapList.tsx
new file mode 100644
index 0000000..d955db4
--- /dev/null
+++ b/src/components/LapList.tsx
@@ -0,0 +1,39 @@
+import {ScrollView, StyleSheet, Text, View} from "react-native";
+import React from "react";
+import {formatTime} from "../common/TimeFormatter";
+
+export interface LapModel {
+ title: string
+ time: number
+
+}
+
+interface LapGroupProps {
+ laps: LapModel[]
+}
+
+export default function LapList({laps}: LapGroupProps) {
+ return (
+
+
+ {laps.map((lap, index) => (
+
+ {lap.title}
+ {formatTime(lap.time)}
+
+ ))}
+
+ )
+}
+
+const styles = StyleSheet.create({
+ container: {
+ width: '100%',
+ paddingHorizontal: 10,
+ },
+
+ text: {
+ fontSize: 16,
+ color: 'white'
+ },
+})
\ No newline at end of file
diff --git a/tests/Stopwatch.test.js b/tests/Stopwatch.test.js
index d5e9f1f..e96c920 100644
--- a/tests/Stopwatch.test.js
+++ b/tests/Stopwatch.test.js
@@ -1,55 +1,80 @@
import React from 'react';
-import { render, fireEvent } from '@testing-library/react-native';
+import {render, fireEvent, within, act} from '@testing-library/react-native';
import Stopwatch from '../src/Stopwatch';
describe('Stopwatch', () => {
- test('renders initial state correctly', () => {
- const { getByText, queryByTestId } = render();
-
- expect(getByText('00:00:00')).toBeTruthy();
- expect(queryByTestId('lap-list')).toBeNull();
- });
-
- test('starts and stops the stopwatch', () => {
- const { getByText, queryByText } = render();
-
- fireEvent.press(getByText('Start'));
- expect(queryByText(/(\d{2}:){2}\d{2}/)).toBeTruthy();
-
- fireEvent.press(getByText('Stop'));
- expect(queryByText(/(\d{2}:){2}\d{2}/)).toBeNull();
- });
-
- test('pauses and resumes the stopwatch', () => {
- const { getByText } = render();
-
- fireEvent.press(getByText('Start'));
- fireEvent.press(getByText('Pause'));
- const pausedTime = getByText(/(\d{2}:){2}\d{2}/).textContent;
-
- fireEvent.press(getByText('Resume'));
- expect(getByText(/(\d{2}:){2}\d{2}/).textContent).not.toBe(pausedTime);
- });
-
- test('records and displays lap times', () => {
- const { getByText, getByTestId } = render();
-
- fireEvent.press(getByText('Start'));
- fireEvent.press(getByText('Lap'));
- expect(getByTestId('lap-list')).toContainElement(getByText(/(\d{2}:){2}\d{2}/));
-
- fireEvent.press(getByText('Lap'));
- expect(getByTestId('lap-list').children.length).toBe(2);
- });
-
- test('resets the stopwatch', () => {
- const { getByText, queryByTestId } = render();
-
- fireEvent.press(getByText('Start'));
- fireEvent.press(getByText('Lap'));
- fireEvent.press(getByText('Reset'));
-
- expect(getByText('00:00:00')).toBeTruthy();
- expect(queryByTestId('lap-list')).toBeNull();
- });
+ test('renders initial state correctly', () => {
+ const {getByText, queryByTestId} = render();
+
+ expect(getByText('00:00:00')).toBeTruthy();
+ expect(queryByTestId('lap-list')).toBeNull();
+ });
+
+ test('starts and stops the stopwatch', () => {
+ const {getByText, queryByText} = render();
+
+ fireEvent.press(getByText('Start'));
+ expect(queryByText(/(\d{2}:){2}\d{2}/)).toBeTruthy();
+
+ fireEvent.press(getByText('Stop'));
+ expect(queryByText(/(\d{2}:){2}\d{2}/)).not.toBeNull();
+ });
+
+ test('pauses and resumes the stopwatch', async () => {
+ const {getByText} = render();
+
+ await act(async () => {
+
+ fireEvent.press(getByText('Start'));
+ await new Promise((resolve) => setTimeout(resolve, 100))
+
+ fireEvent.press(getByText('Stop'));
+
+ const pausedTime = getByText(/(\d{2}:){2}\d{2}/).children.join();
+
+ fireEvent.press(getByText('Start'));
+
+ await new Promise((resolve) => setTimeout(resolve, 100))
+
+ expect(getByText(/(\d{2}:){2}\d{2}/).children.join()).not.toBe(pausedTime);
+ })
+ });
+
+ test('records and displays lap times', async () => {
+ const {getByText, getByTestId} = render();
+
+ await act(async () => {
+ fireEvent.press(getByText('Start'));
+
+ await new Promise((resolve) => setTimeout(resolve, 10))
+
+ await fireEvent.press(getByText('Lap'));
+
+
+ const lapList = getByTestId("lap-list");
+
+ expect(lapList).not.toBeNull()
+
+
+ fireEvent.press(getByText('Lap'));
+
+ const {queryAllByText} = within(lapList);
+
+ const matchingElements = queryAllByText(/(\d{2}:){2}\d{2}/);
+
+ expect(matchingElements.length).toBe(2);
+
+ })
+ });
+
+ test('resets the stopwatch', () => {
+ const {getByText, queryByTestId} = render();
+
+ fireEvent.press(getByText('Start'));
+ fireEvent.press(getByText('Lap'));
+ fireEvent.press(getByText('Reset'));
+
+ expect(getByText('00:00:00')).toBeTruthy();
+ expect(queryByTestId('lap-list')).toBeNull();
+ });
});