diff --git a/App.tsx b/App.tsx
index 0329d0c..b47cc23 100644
--- a/App.tsx
+++ b/App.tsx
@@ -1,20 +1,19 @@
-import { StatusBar } from 'expo-status-bar';
-import { StyleSheet, Text, View } from 'react-native';
+import { SafeAreaView, StyleSheet } from "react-native";
+import StopWatch from "./src/Components/StopWatch/StopWatch";
export default function App() {
return (
-
- Open up App.tsx to start working on your app!
-
-
+
+
+
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
- backgroundColor: '#fff',
- alignItems: 'center',
- justifyContent: 'center',
+ alignItems: "center",
+ justifyContent: "center",
+ backgroundColor: "#463f3a",
},
});
diff --git a/README.md b/README.md
index 6b8b50e..77551c2 100644
--- a/README.md
+++ b/README.md
@@ -1,10 +1,42 @@
+# Joshua Fung Submission for Shopify 2024 Mobile Engineering Assessment
+
+This is my submission for the Shopify 2024 Mobile Engineering Assessment, thank you for your time and consideration! :)
+
+Truthfully, I would have liked to have put in more time and polished it more (as well as more test cases), however, I have been very tight on time and extremely jetlagged due to flying across the world for my upcoming academic exchange.
+
+I have included some considerations and changes I have made in the sections below:.
+
+## Considerations
+
+- The test cases seemed to suggest 4 separate buttons which seemed excessive from a user's perspective -- Instead, I went for a two button approach:
+
+ - When the stopwatch has not started / has stopped, we have the 'start' and 'reset' buttons
+ - Start -> Starts the stopwatch and begins counting time
+ - Reset -> Resets the saved time and clears all laps
+ - When the stopwatch is running, we have the 'stop' and 'lap' buttons
+ - Stop -> Pauses the stopwatch and halts its count
+ - Lap -> Takes the elapsed time and adds it to the state as another lap
+
+- File structure was also slightly modified to break it down into more subfolders. Initially I was going to put what is currently in `StopWatch.tsx` into a `Screens` folder, but I didn't want to deviate too much.
+
+## (Test) Changes
+
+- The given `records and displays lap time` test was having a lot of issues, incl. the usage of `toContainElement()` as it's meant for web DOM elements rather than React Native components -> Checked for existence instead
+- Given tests' usage of `.textContent` seemed to also be having issues -> Used `.props.children` instead
+- Wrapped fire events that caused state changed with `act()`
+ - However, I acknowledge that the fire event for pressing the "Lap" button is still triggering a warning (albeit being wrapped)
+
+---
+
# Technical Instructions
+
1. Fork this repo to your local Github account.
2. Create a new branch to complete all your work in.
3. Test your work using the provided tests
4. Create a Pull Request against the Shopify Main branch when you're done and all tests are passing
# Project Overview
+
The goal of this project is to implement a stopwatch application using React Native and TypeScript. The stopwatch should have the following functionality:
- Start the stopwatch to begin counting time.
@@ -15,6 +47,7 @@ The goal of this project is to implement a stopwatch application using React Nat
You will be provided with a basic project structure that includes the necessary files and dependencies. Your task is to write the code to implement the stopwatch functionality and ensure that it works correctly.
## Project Setup
+
To get started with the project, follow these steps:
1. Clone the project repository to your local development environment.
@@ -22,42 +55,50 @@ To get started with the project, follow these steps:
2. Install the required dependencies by running npm install in the project directory.
3. Familiarize yourself with the project structure. The main files you will be working with are:
- - /App.tsx: The main component that renders the stopwatch and handles its functionality.
- - src/Stopwatch.tsx: A separate component that represents the stopwatch display.
- - src/StopwatchButton.tsx: A separate component that represents the start, stop, and reset buttons.
+
+ - /App.tsx: The main component that renders the stopwatch and handles its functionality.
+ - src/Stopwatch.tsx: A separate component that represents the stopwatch display.
+ - src/StopwatchButton.tsx: A separate component that represents the start, stop, and reset buttons.
4. Review the existing code in the above files to understand the initial structure and component hierarchy.
## Project Goals
+
Your specific goals for this project are as follows:
1. Implement the stopwatch functionality:
- - The stopwatch should start counting when the user clicks the start button.
- - The stopwatch should stop counting when the user clicks the stop button.
- - The stopwatch should reset to zero when the user clicks the reset button.
- - The stopwatch should record and display laps when user clicks the lap button.
+
+ - The stopwatch should start counting when the user clicks the start button.
+ - The stopwatch should stop counting when the user clicks the stop button.
+ - The stopwatch should reset to zero when the user clicks the reset button.
+ - The stopwatch should record and display laps when user clicks the lap button.
2. Ensure code quality:
- - Write clean, well-structured, and maintainable code.
- - Follow best practices and adhere to the React and TypeScript coding conventions.
- - Pay attention to code readability, modularity, and performance.
+
+ - Write clean, well-structured, and maintainable code.
+ - Follow best practices and adhere to the React and TypeScript coding conventions.
+ - Pay attention to code readability, modularity, and performance.
3. Test your code:
- - Run the application and test the stopwatch functionality to ensure it works correctly.
- - Verify that the stopwatch starts, stops, resets, and records laps as expected.
+
+ - Run the application and test the stopwatch functionality to ensure it works correctly.
+ - Verify that the stopwatch starts, stops, resets, and records laps as expected.
4. Code documentation:
- - Document your code by adding comments and explanatory notes where necessary.
- - Provide clear explanations of the implemented functionality and any important details.
+
+ - Document your code by adding comments and explanatory notes where necessary.
+ - Provide clear explanations of the implemented functionality and any important details.
5. Version control:
- - Use Git for version control. Commit your changes regularly and push them to a branch in your forked repository.
- 6. Create a Pull Request:
- - Once you have completed the project goals, create a pull request to merge your changes into the main repository.
- - Provide a clear description of the changes made and any relevant information for the code review.
+ - Use Git for version control. Commit your changes regularly and push them to a branch in your forked repository.
+
+6. Create a Pull Request:
+ - Once you have completed the project goals, create a pull request to merge your changes into the main repository.
+ - Provide a clear description of the changes made and any relevant information for the code review.
## Getting Started
+
To start working on the project, follow these steps:
1. Clone the repository to your local development environment.
@@ -77,6 +118,7 @@ To start working on the project, follow these steps:
8. Once you have completed the project goals, create a pull request to merge your changes into the main repository.
## Resources
+
Here are some resources that may be helpful during your work on this project:
- [TypeScript Documentation](https://www.typescriptlang.org/docs/) - Official documentation for TypeScript, offering guidance on TypeScript features and usage.
diff --git a/package-lock.json b/package-lock.json
index ea36168..b8f8038 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -10,13 +10,14 @@
"dependencies": {
"expo": "~49.0.15",
"expo-status-bar": "~1.6.0",
- "jest": "^29.2.1",
+ "jest": "^29.7.0",
"jest-expo": "~49.0.0",
"react": "18.2.0",
"react-native": "0.72.6"
},
"devDependencies": {
"@babel/core": "^7.20.0",
+ "@testing-library/jest-dom": "^6.3.0",
"@testing-library/react-native": "^12.4.3",
"@tsconfig/react-native": "^3.0.2",
"@types/jest": "^29.5.11",
@@ -25,6 +26,12 @@
"typescript": "^5.3.3"
}
},
+ "node_modules/@adobe/css-tools": {
+ "version": "4.3.3",
+ "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.3.3.tgz",
+ "integrity": "sha512-rE0Pygv0sEZ4vBWHlAgJLGDU7Pm8xoO6p3wsEceb7GYAjScrOHpEo8KK/eVkAcnSM+slAEtXjA2JpdjLp4fJQQ==",
+ "dev": true
+ },
"node_modules/@ampproject/remapping": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.1.tgz",
@@ -6911,6 +6918,118 @@
"@sinonjs/commons": "^3.0.0"
}
},
+ "node_modules/@testing-library/jest-dom": {
+ "version": "6.3.0",
+ "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.3.0.tgz",
+ "integrity": "sha512-hJVIrkFizEQxoWsGBlycTcQhrpoCH4DhXfrnHFFXgkx3Xdm15zycsq5Ep+vpw4W8S0NJa8cxDHcuJib+1tEbhg==",
+ "dev": true,
+ "dependencies": {
+ "@adobe/css-tools": "^4.3.2",
+ "@babel/runtime": "^7.9.2",
+ "aria-query": "^5.0.0",
+ "chalk": "^3.0.0",
+ "css.escape": "^1.5.1",
+ "dom-accessibility-api": "^0.6.3",
+ "lodash": "^4.17.15",
+ "redent": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=14",
+ "npm": ">=6",
+ "yarn": ">=1"
+ },
+ "peerDependencies": {
+ "@jest/globals": ">= 28",
+ "@types/bun": "latest",
+ "@types/jest": ">= 28",
+ "jest": ">= 28",
+ "vitest": ">= 0.32"
+ },
+ "peerDependenciesMeta": {
+ "@jest/globals": {
+ "optional": true
+ },
+ "@types/bun": {
+ "optional": true
+ },
+ "@types/jest": {
+ "optional": true
+ },
+ "jest": {
+ "optional": true
+ },
+ "vitest": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@testing-library/jest-dom/node_modules/ansi-styles": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+ "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+ "dev": true,
+ "dependencies": {
+ "color-convert": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/@testing-library/jest-dom/node_modules/chalk": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz",
+ "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==",
+ "dev": true,
+ "dependencies": {
+ "ansi-styles": "^4.1.0",
+ "supports-color": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/@testing-library/jest-dom/node_modules/color-convert": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+ "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+ "dev": true,
+ "dependencies": {
+ "color-name": "~1.1.4"
+ },
+ "engines": {
+ "node": ">=7.0.0"
+ }
+ },
+ "node_modules/@testing-library/jest-dom/node_modules/color-name": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+ "dev": true
+ },
+ "node_modules/@testing-library/jest-dom/node_modules/has-flag": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
+ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/@testing-library/jest-dom/node_modules/supports-color": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
+ "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
+ "dev": true,
+ "dependencies": {
+ "has-flag": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
"node_modules/@testing-library/react-native": {
"version": "12.4.3",
"resolved": "https://registry.npmjs.org/@testing-library/react-native/-/react-native-12.4.3.tgz",
@@ -7360,6 +7479,15 @@
"sprintf-js": "~1.0.2"
}
},
+ "node_modules/aria-query": {
+ "version": "5.3.0",
+ "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz",
+ "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==",
+ "dev": true,
+ "dependencies": {
+ "dequal": "^2.0.3"
+ }
+ },
"node_modules/array-union": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz",
@@ -8618,6 +8746,12 @@
"node": ">=8"
}
},
+ "node_modules/css.escape": {
+ "version": "1.5.1",
+ "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz",
+ "integrity": "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==",
+ "dev": true
+ },
"node_modules/cssom": {
"version": "0.5.0",
"resolved": "https://registry.npmjs.org/cssom/-/cssom-0.5.0.tgz",
@@ -8875,6 +9009,15 @@
"prop-types": "*"
}
},
+ "node_modules/dequal": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz",
+ "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==",
+ "dev": true,
+ "engines": {
+ "node": ">=6"
+ }
+ },
"node_modules/destroy": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz",
@@ -8922,6 +9065,12 @@
"node": ">=8"
}
},
+ "node_modules/dom-accessibility-api": {
+ "version": "0.6.3",
+ "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz",
+ "integrity": "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==",
+ "dev": true
+ },
"node_modules/domexception": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/domexception/-/domexception-4.0.0.tgz",
@@ -9208,9 +9357,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 +9374,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 +9564,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..cc18285 100644
--- a/package.json
+++ b/package.json
@@ -12,13 +12,14 @@
"dependencies": {
"expo": "~49.0.15",
"expo-status-bar": "~1.6.0",
- "react": "18.2.0",
- "react-native": "0.72.6",
+ "jest": "^29.7.0",
"jest-expo": "~49.0.0",
- "jest": "^29.2.1"
+ "react": "18.2.0",
+ "react-native": "0.72.6"
},
"devDependencies": {
"@babel/core": "^7.20.0",
+ "@testing-library/jest-dom": "^6.3.0",
"@testing-library/react-native": "^12.4.3",
"@tsconfig/react-native": "^3.0.2",
"@types/jest": "^29.5.11",
diff --git a/src/Components/LapsList/LapsList.tsx b/src/Components/LapsList/LapsList.tsx
new file mode 100644
index 0000000..e00855e
--- /dev/null
+++ b/src/Components/LapsList/LapsList.tsx
@@ -0,0 +1,71 @@
+import React from "react";
+import { ScrollView, StyleSheet, Text, View } from "react-native";
+import { formatTime } from "../../utils/TimeUtils";
+
+interface LapsListInterface {
+ laps: number[];
+}
+
+/**
+ * Component which displays all saved laps
+ * @param props - LapsListInterface which only has a list of laps (each in miliseconds)
+ * @returns Component for a list of laps
+ */
+const LapsList = (props: LapsListInterface) => {
+ const { laps } = props;
+
+ return (
+
+ {laps.map((lap, index) => {
+ return (
+
+ Lap {index + 1}
+ {formatTime(lap)}
+
+ );
+ })}
+
+ );
+};
+
+const styles = StyleSheet.create({
+ container: {
+ width: "100%",
+ height: "50%",
+ display: "flex",
+ marginBottom: 50,
+ paddingHorizontal: 20,
+ borderRadius: 8,
+ backgroundColor: "#bcb8b1",
+ },
+ containerItems: {
+ width: "100%",
+ display: "flex",
+ justifyContent: "space-evenly",
+ paddingVertical: 20,
+ gap: 10,
+ },
+ lapItem: {
+ width: "100%",
+ display: "flex",
+ flexDirection: "row",
+ justifyContent: "space-evenly",
+ paddingVertical: 10,
+ gap: 15,
+ borderRadius: 8,
+ backgroundColor: "white",
+ },
+ lapTitle: {
+ fontWeight: "bold",
+ },
+});
+
+export default LapsList;
diff --git a/src/Components/StopWatch/StopWatch.tsx b/src/Components/StopWatch/StopWatch.tsx
new file mode 100644
index 0000000..bfbb846
--- /dev/null
+++ b/src/Components/StopWatch/StopWatch.tsx
@@ -0,0 +1,122 @@
+import React, { useEffect, useState } from "react";
+import { StyleSheet, Text, View } from "react-native";
+import StopWatchButton from "../StopWatchButton/StopWatchButton";
+import LapsList from "../LapsList/LapsList";
+import { formatTime } from "../../utils/TimeUtils";
+
+/**
+ * Stopwatch component that includes the time display, buttons, and laps list
+ * @returns A stopwatch component
+ */
+const StopWatch = () => {
+ const [isRunning, setIsRunning] = useState(false);
+ const [elapsedTime, setElapsedTime] = useState(0);
+ const [laps, setLaps] = useState([]);
+ const [startTime, setStartTime] = useState(0);
+
+ useEffect(() => {
+ let intervalID: number;
+ if (isRunning) {
+ intervalID = setInterval(() => {
+ const currentTime = Date.now();
+ // Computes elapsed time between current time and (state) start time
+ setElapsedTime(
+ (previousElapsedTime) =>
+ previousElapsedTime + (currentTime - startTime)
+ );
+ setStartTime(currentTime);
+ }, 1);
+ }
+ return () => clearInterval(intervalID);
+ }, [isRunning, startTime]);
+
+ const startTimer = () => {
+ setStartTime(Date.now());
+ setIsRunning(true);
+ };
+
+ const stopTimer = () => setIsRunning(false);
+
+ const resetTimer = () => {
+ setElapsedTime(0);
+ setLaps([]);
+ };
+
+ const addLap = (time: number) => {
+ setLaps([...laps, time]);
+ };
+
+ return (
+
+
+
+ {formatTime(elapsedTime)}
+
+
+ {isRunning ? (
+ <>
+
+ {
+ addLap(elapsedTime);
+ }}
+ title="Lap"
+ colour="#b5e2fa"
+ />
+ >
+ ) : (
+ <>
+
+
+ >
+ )}
+
+
+ {/* Only renders the laps list when we have at least one lap */}
+ {laps.length > 0 && }
+
+ );
+};
+
+const styles = StyleSheet.create({
+ container: {
+ width: "80%",
+ display: "flex",
+ flexDirection: "column",
+ alignItems: "center",
+ justifyContent: "center",
+ padding: 20,
+ },
+ stopwatchContainer: {
+ height: "40%",
+ display: "flex",
+ alignItems: "center",
+ justifyContent: "center",
+ padding: 10,
+ },
+ timeDisplay: {
+ fontSize: 50,
+ fontWeight: "bold",
+ color: "#e9ecef",
+ },
+ buttonContainer: {
+ display: "flex",
+ flexDirection: "row",
+ padding: 10,
+ gap: 8,
+ },
+});
+
+export default StopWatch;
diff --git a/src/Components/StopWatchButton/StopWatchButton.tsx b/src/Components/StopWatchButton/StopWatchButton.tsx
new file mode 100644
index 0000000..afda874
--- /dev/null
+++ b/src/Components/StopWatchButton/StopWatchButton.tsx
@@ -0,0 +1,44 @@
+import { StyleSheet, Text, TouchableHighlight, View } from "react-native";
+
+interface StopWatchButtonProps {
+ title: string;
+ onPress: () => void;
+ colour: string;
+}
+
+/**
+ * Reusable button component for the stopwatch
+ * @param props - StopWatchButtonProps which has a title, onPress function, and colour
+ * @returns A button component
+ */
+const StopWatchButton = (props: StopWatchButtonProps) => {
+ const { title, onPress, colour } = props;
+
+ return (
+
+
+
+ {title}
+
+
+
+ );
+};
+
+const styles = StyleSheet.create({
+ button: {
+ width: 80,
+ height: 40,
+ display: "flex",
+ alignItems: "center",
+ justifyContent: "center",
+ padding: 5,
+ borderRadius: 8,
+ },
+ buttonText: {
+ fontSize: 18,
+ fontWeight: "500",
+ },
+});
+
+export default StopWatchButton;
diff --git a/src/StopWatch.tsx b/src/StopWatch.tsx
deleted file mode 100644
index 5c7eb74..0000000
--- a/src/StopWatch.tsx
+++ /dev/null
@@ -1,8 +0,0 @@
-import { View } from 'react-native';
-
-export default function StopWatch() {
- return (
-
-
- );
-}
\ No newline at end of file
diff --git a/src/StopWatchButton.tsx b/src/StopWatchButton.tsx
deleted file mode 100644
index 8768555..0000000
--- a/src/StopWatchButton.tsx
+++ /dev/null
@@ -1,8 +0,0 @@
-import { View } from 'react-native';
-
-export default function StopWatchButton() {
- return (
-
-
- );
-}
\ No newline at end of file
diff --git a/src/utils/TimeUtils.ts b/src/utils/TimeUtils.ts
new file mode 100644
index 0000000..39aa8d2
--- /dev/null
+++ b/src/utils/TimeUtils.ts
@@ -0,0 +1,16 @@
+/**
+ * Utility function that formats time in miliseconds to a string in the format HH:MM:SS
+ * @param time - Time in miliseconds
+ * @returns String for the time in the format HH:MM:SS
+ */
+export const formatTime = (time: number) => {
+ const hours = Math.floor(time / 3600000);
+ const minutes = Math.floor((time - hours * 3600000) / 60000);
+ const seconds = Math.floor((time - hours * 3600000 - minutes * 60000) / 1000);
+
+ const hoursString = hours.toString().padStart(2, "0");
+ const minutesString = minutes.toString().padStart(2, "0");
+ const secondsString = seconds.toString().padStart(2, "0");
+
+ return `${hoursString}:${minutesString}:${secondsString}`;
+};
diff --git a/tests/Stopwatch.test.js b/tests/Stopwatch.test.js
index d5e9f1f..74b7588 100644
--- a/tests/Stopwatch.test.js
+++ b/tests/Stopwatch.test.js
@@ -1,55 +1,127 @@
-import React from 'react';
-import { render, fireEvent } from '@testing-library/react-native';
-import Stopwatch from '../src/Stopwatch';
+import React from "react";
+import { act, render, fireEvent } from "@testing-library/react-native";
+import Stopwatch from "../src/Components/StopWatch/StopWatch";
-describe('Stopwatch', () => {
- test('renders initial state correctly', () => {
+describe("Stopwatch", () => {
+ test("renders initial state correctly", () => {
const { getByText, queryByTestId } = render();
-
- expect(getByText('00:00:00')).toBeTruthy();
- expect(queryByTestId('lap-list')).toBeNull();
+
+ expect(getByText("00:00:00")).toBeTruthy();
+ expect(queryByTestId("lap-list")).toBeNull();
});
- test('starts and stops the stopwatch', () => {
+ test("starts and stops the stopwatch", () => {
const { getByText, queryByText } = render();
-
- fireEvent.press(getByText('Start'));
+
+ act(() => {
+ fireEvent.press(getByText("Start"));
+ });
+
expect(queryByText(/(\d{2}:){2}\d{2}/)).toBeTruthy();
- fireEvent.press(getByText('Stop'));
- expect(queryByText(/(\d{2}:){2}\d{2}/)).toBeNull();
+ act(() => {
+ fireEvent.press(getByText("Stop"));
+ });
+
+ expect(queryByText("00:00:00")).toBeTruthy();
});
- test('pauses and resumes the stopwatch', () => {
+ 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'));
+ act(() => {
+ fireEvent.press(getByText("Start"));
+ });
+
+ fireEvent.press(getByText("Stop"));
+ const pausedTime = getByText(/(\d{2}:){2}\d{2}/).props.children;
+
+ act(() => {
+ fireEvent.press(getByText("Start"));
+ });
+
expect(getByText(/(\d{2}:){2}\d{2}/).textContent).not.toBe(pausedTime);
});
- test('records and displays lap times', () => {
+ 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);
+ act(() => {
+ fireEvent.press(getByText("Start"));
+ });
+
+ act(() => {
+ fireEvent.press(getByText("Lap"));
+ });
+
+ expect(getByTestId("lap-list")).toBeTruthy();
+ expect(getByTestId("saved-lap-1")).toBeTruthy();
+
+ act(() => {
+ fireEvent.press(getByText("Lap"));
+ });
+
+ expect(getByTestId("saved-lap-1")).toBeTruthy();
+ expect(getByTestId("saved-lap-2")).toBeTruthy();
});
- test('resets the stopwatch', () => {
+ 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();
+ act(() => {
+ fireEvent.press(getByText("Start"));
+ });
+
+ act(() => {
+ fireEvent.press(getByText("Lap"));
+ });
+
+ fireEvent.press(getByText("Stop"));
+
+ act(() => {
+ fireEvent.press(getByText("Reset"));
+ });
+
+ expect(getByText("00:00:00")).toBeTruthy();
+ expect(queryByTestId("lap-list")).toBeNull();
+ });
+
+ it("should initially render the correct buttons of start and reset", () => {
+ const { getByText, queryByText } = render();
+
+ expect(getByText("Start")).toBeTruthy();
+ expect(getByText("Reset")).toBeTruthy();
+ expect(queryByText("Stop")).not.toBeTruthy();
+ expect(queryByText("Lap")).not.toBeTruthy();
+ });
+
+ it("should render the correct buttons of stop and lap when running", () => {
+ const { getByText, queryByText } = render();
+
+ act(() => {
+ fireEvent.press(getByText("Start"));
+ });
+
+ expect(getByText("Stop")).toBeTruthy();
+ expect(getByText("Lap")).toBeTruthy();
+ expect(queryByText("Start")).not.toBeTruthy();
+ expect(queryByText("Reset")).not.toBeTruthy();
+ });
+
+ it("should render the initial buttons after stopping a running stopwatch", () => {
+ const { getByText, queryByText } = render();
+
+ act(() => {
+ fireEvent.press(getByText("Start"));
+ });
+
+ act(() => {
+ fireEvent.press(getByText("Stop"));
+ });
+
+ expect(getByText("Start")).toBeTruthy();
+ expect(getByText("Reset")).toBeTruthy();
+ expect(queryByText("Stop")).not.toBeTruthy();
+ expect(queryByText("Lap")).not.toBeTruthy();
});
});