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(); }); });