diff --git a/README.md b/README.md index e1d3868..e2cf214 100644 --- a/README.md +++ b/README.md @@ -104,7 +104,7 @@ cd front-end/ && npm run dev # will launch the front end of the application - By default, the application runs locally on ports 8000 (back) and 4242 (front). Access it @ [**http://localhost:4242**](http://localhost:4242). - The application features 6 major pages: - [x] **Home**: the main page, where your statistics are displayed and you can navigate to **Artwork Generation** and **Lyrics**. - - [ ] **Tests** *(TBD)*: the unit tests page, to check the application's integrity. + - [x] **Tests** : the unit tests page, to check the application's integrity. - [x] **Artwork Generation**: the page where you can generate artwork from a local file or an iTunes search. - [x] **Processed Images**: the page where you can download a background image and a YouTube thumbnail. - [x] **Lyrics**: the page where you can fetch lyrics from Genius and convert them to lyrics blocks. @@ -139,8 +139,9 @@ cd front-end/ && npm run dev # will launch the front end of the application - documentation strings are added to the codebase - the artwork generation page is reworked to welcome YT section and better UX - the logger system is reinforced to log more actions and be customizable -- ***[1.3.0]** Aug 19 2024*: **Lyrics Fetch** — Project will support lyrics fetching from Genius and their conversion to lyrics blocks. [#089](https://github.com/Thomas-Fernandes/GTFR-CG/pull/89) - - ***[1.3.1]** Aug 19 2024*: The project's front end will be fully migrated to **React Typescript with Vite**. [#088](https://github.com/Thomas-Fernandes/GTFR-CG/pull/88) +- ***[1.3.0]** Aug 19 2024*: **Lyrics Fetch** — Project now supports lyrics fetching from Genius and their conversion to lyrics blocks. [#089](https://github.com/Thomas-Fernandes/GTFR-CG/pull/89) + - ***[1.3.1]** Aug 19 2024*: Project's front end is fully migrated to **React Typescript with Vite**. [#088](https://github.com/Thomas-Fernandes/GTFR-CG/pull/88) + - ***[1.3.2]** Sep ?? 2024*: Project now has a unit test page, better .env handling with a tutorial file. [#???](#card_file_box-changelog) - ***[1.4.0]** Coming later...*: **Lyrics Cards** — Project will support automatic cards generation from text blocks. [#???](#card_file_box-changelog) - ***[1.5.0]** Coming later...*: **Boost!** — Project will see its existing functionalities sharpened. [#???](#card_file_box-changelog) - ***[1.6.0]** Coming later...*: **Koh-Lanta** — Project will be unified in an all-in-one application. [#???](#card_file_box-changelog) diff --git a/front-end/src/App.css b/front-end/src/App.css index f545458..16ecb26 100644 --- a/front-end/src/App.css +++ b/front-end/src/App.css @@ -369,4 +369,14 @@ input[type="file"] { } .bold { font-weight: bold; +} + +.t-green { + color: #007f00; +} +.t-yellow { + color: #7f7f00; +} +.t-red { + color: #7f0000; } \ No newline at end of file diff --git a/front-end/src/App.tsx b/front-end/src/App.tsx index e9c045c..a54ad10 100644 --- a/front-end/src/App.tsx +++ b/front-end/src/App.tsx @@ -7,6 +7,7 @@ import Home from "./pages/Home/Home"; import Lyrics from "./pages/Lyrics/Lyrics"; import ProcessedImages from "./pages/ProcessedImages/ProcessedImages"; import Redirect from "./pages/Redirect/Redirect"; +import Tests from "./pages/Tests/Tests"; import "./App.css"; @@ -18,6 +19,7 @@ const App = (): JSX.Element => { } /> } /> } /> + } /> } /> } /> diff --git a/front-end/src/constants/Common.tsx b/front-end/src/constants/Common.tsx index 126c122..eb2ec19 100644 --- a/front-end/src/constants/Common.tsx +++ b/front-end/src/constants/Common.tsx @@ -77,6 +77,7 @@ export const TITLE = { PREFIX: "GTFR-CG - ", REDIRECT: "Redirect", + TESTS: "Tests", HOME: "Home", ARTWORK_GENERATION: "Artwork Generation", PROCESSED_IMAGES: "Processed Images", @@ -86,6 +87,7 @@ export const TITLE = { export const PATHS = { redirect: "/redirect", home: "/home", + tests: "/tests", artworkGeneration: "/artwork-generation", processedImages: "/processed-images", lyrics: "/lyrics", diff --git a/front-end/src/pages/Home/Home.css b/front-end/src/pages/Home/Home.css index a1b6996..effb01f 100644 --- a/front-end/src/pages/Home/Home.css +++ b/front-end/src/pages/Home/Home.css @@ -1,10 +1,16 @@ #home { - div.navbar.home { - width: 50%; + div.navbar { + &.home { + width: 50%; + + button { + height: 100px; + width: -webkit-fill-available; + } + } - button { - height: 100px; - width: -webkit-fill-available; + &.tests { + margin-top: 24px; } } @@ -36,4 +42,4 @@ } } } -} +} \ No newline at end of file diff --git a/front-end/src/pages/Home/Home.tsx b/front-end/src/pages/Home/Home.tsx index 1b6d6e3..fa99072 100644 --- a/front-end/src/pages/Home/Home.tsx +++ b/front-end/src/pages/Home/Home.tsx @@ -135,6 +135,11 @@ const Home = (): JSX.Element => { +
+ +

Genius Token: '{ geniusToken }'

diff --git a/front-end/src/pages/Tests/Test.tsx b/front-end/src/pages/Tests/Test.tsx new file mode 100644 index 0000000..52ce035 --- /dev/null +++ b/front-end/src/pages/Tests/Test.tsx @@ -0,0 +1,49 @@ +import { RefObject, useState } from "react"; + +import { StateSetter } from "../../common/Types"; +import { isEmpty } from "../../common/utils/ObjUtils"; + +export type TestResult = { + successful: boolean; + prompt: string; + duration?: number; +}; + +export type TestFunc = (setter: StateSetter) => Promise; + +export type TestProps = { + title: string; + func: TestFunc; + buttonRef: RefObject; +}; + +export const Test = (props: TestProps) => { + const { title, func, buttonRef } = props; + + const [result, setResult] = useState({} as TestResult); + const [testIsRunning, setTestIsRunning] = useState(false); + + const runTest = async (func: TestFunc) => { + setResult({} as TestResult); + setTestIsRunning(true); + await func(setResult); + setTestIsRunning(false); + }; + + return ( +
+

{title}

+ { isEmpty(result) ? + + : +

+ Test completed
+ {result?.successful ? "successfully" : "with a failure"}
+ in {result?.duration} milliseconds +

+ } +
+ ); +} \ No newline at end of file diff --git a/front-end/src/pages/Tests/Tests.css b/front-end/src/pages/Tests/Tests.css new file mode 100644 index 0000000..503363a --- /dev/null +++ b/front-end/src/pages/Tests/Tests.css @@ -0,0 +1,53 @@ +#tests { + button[id="run-all"] { + width: 35%; + margin: 0 auto 2% auto + } + #page { + width: 80%; + margin: auto; + gap: 1rem; + + .column { + width: 100%; + margin: auto; + gap: 1rem; + + .board { + background: #ffffff; + box-shadow: 0 0 10px rgba(0,0,0,0.1); + border: 4px solid #0000001a; + border-radius: 1em; + padding-bottom: 2%; + + .title { + font-size: xx-large; + margin: 1em 0; + } + + .test { + margin: 2% 4%; + gap: 1%; + + h3 { + min-width: 42%; + justify-content: center; + } + button { + width: -webkit-fill-available; + } + p { + width: -webkit-fill-available; + font-size: 16px; + text-align: center; + + span { + display: contents; + font-size: inherit; + } + } + } + } + } + } +} diff --git a/front-end/src/pages/Tests/Tests.tsx b/front-end/src/pages/Tests/Tests.tsx new file mode 100644 index 0000000..b29fd43 --- /dev/null +++ b/front-end/src/pages/Tests/Tests.tsx @@ -0,0 +1,152 @@ +import { JSX, useRef, useState } from "react"; +import { useNavigate } from "react-router-dom"; + +import { is2xxSuccessful, objectToQueryString, sendRequest } from "../../common/Requests"; +import { dismissToast, sendToast } from "../../common/Toast"; +import { ItunesResponse, LyricsResponse, StateSetter } from "../../common/Types"; +import useTitle from "../../common/UseTitle"; +import { API, BACKEND_URL, ITUNES_URL, PATHS, TITLE, TOAST_TYPE } from "../../constants/Common"; + +import { TestResult } from "./Test"; +import { TestsBoard } from "./TestsBoard"; + +import "./Tests.css"; + +const Tests = (): JSX.Element => { + useTitle(TITLE.TESTS); + + const refGeniusToken = useRef(null); + const refStatistics = useRef(null); + const refSnacks = useRef(null); + const refItunes = useRef(null); + const refGenius = useRef(null); + + const navigate = useNavigate(); + + // ENV-VAR + const testGeniusToken = async (setter: StateSetter) => { + const start = Date.now(); + await sendRequest("GET", BACKEND_URL + API.GENIUS_TOKEN).then((response) => { + setter({ successful: is2xxSuccessful(response.status), prompt: response.message, duration: Date.now() - start }); + }).catch((error) => { + setter({ successful: false, prompt: error.message, duration: Date.now() - start }); + }); + }; + const testStatistics = async (setter: StateSetter) => { + const start = Date.now(); + await sendRequest("GET", BACKEND_URL + API.STATISTICS).then((response) => { + setter({ successful: is2xxSuccessful(response.status), prompt: response.message, duration: Date.now() - start }); + }).catch((error) => { + setter({ successful: false, prompt: error.message, duration: Date.now() - start }); + }); + }; + + // FRONT-END + const testSnacks = async (setter: StateSetter) => { + const start = Date.now(); + const toastContainer = document.getElementById("toast-container"); + + if (!toastContainer) { + setter({ successful: false, prompt: "Toast container not found", duration: Date.now() - start }); + return; + } + + sendToast("Testing snacks", TOAST_TYPE.INFO); + if (toastContainer.childElementCount === 0) { + setter({ successful: false, prompt: "Snacks not working", duration: Date.now() - start }); + } else { + setter({ successful: true, prompt: "Snacks tested", duration: Date.now() - start }); + dismissToast(toastContainer.lastElementChild as HTMLElement, 0); + } + }; + + // API + const testItunes = async (setter: StateSetter) => { + const start = Date.now(); + const data = { term: "hello", country: "US", entity: "album", limit: 10 }; + const queryString = objectToQueryString(data); + + await sendRequest("POST", ITUNES_URL + "/search" + queryString).then((response: ItunesResponse) => { + setter({ successful: response.resultCount > 0, prompt: "iTunes API test successful", duration: Date.now() - start }); + }).catch((error) => { + setter({ successful: false, prompt: error.message, duration: Date.now() - start }); + }); + }; + const testGenius = async (setter: StateSetter) => { + const start = Date.now(); + const body = { artist: "Adele", songName: "Hello" }; + await sendRequest("POST", BACKEND_URL + API.LYRICS.GET_LYRICS, body).then((response: LyricsResponse) => { + setter({ successful: is2xxSuccessful(response.status), prompt: response.message, duration: Date.now() - start }); + }).catch((error) => { + setter({ successful: false, prompt: error.message, duration: Date.now() - start }); + }); + }; + + const boards = [ + { + id: "env-var", + title: "Environment Variables", + tests: [ + { title: "Genius Token", func: testGeniusToken, buttonRef: refGeniusToken }, + { title: "Statistics", func: testStatistics, buttonRef: refStatistics }, + ], + }, + { + id: "front-end", + title: "Front-End Components", + tests: [ + { title: "Snacks", func: testSnacks, buttonRef: refSnacks }, + ], + }, + { + id: "api", + title: "API", + tests: [ + { title: "iTunes", func: testItunes, buttonRef: refItunes }, + { title: "Genius Lyrics", func: testGenius, buttonRef: refGenius }, + ], + }, + ]; + + const [clickedRunAll, setClickedRunAll] = useState(false); + const handleRunAll = () => { + setClickedRunAll(true); + if (refGeniusToken.current) refGeniusToken.current.click(); + if (refStatistics.current) refStatistics.current.click(); + if (refSnacks.current) refSnacks.current.click(); + if (refItunes.current) refItunes.current.click(); + if (refGenius.current) refGenius.current.click(); + }; + + return ( +
+
+ + +
+ +
+ +

Tests

+ + + +
+
+ b.id === "env-var"))} /> + b.id === "front-end"))} /> +
+ +
+ b.id === "api"))} /> +
+
+ + +
+ ); +}; + +export default Tests; \ No newline at end of file diff --git a/front-end/src/pages/Tests/TestsBoard.tsx b/front-end/src/pages/Tests/TestsBoard.tsx new file mode 100644 index 0000000..2b097e8 --- /dev/null +++ b/front-end/src/pages/Tests/TestsBoard.tsx @@ -0,0 +1,20 @@ +import { Test, TestProps } from "./Test"; + +type TestsBoardProps = { + id?: string; + title?: string; + tests?: TestProps[]; +}; + +export const TestsBoard = (props: TestsBoardProps) => { + const { id, title, tests } = props; + + return ( +
+

{title}

+ { tests?.map((test) => ( + + ))} +
+ ); +}; \ No newline at end of file diff --git a/src/routes/artwork_generation.py b/src/routes/artwork_generation.py index c61f388..9838999 100644 --- a/src/routes/artwork_generation.py +++ b/src/routes/artwork_generation.py @@ -22,7 +22,7 @@ def useItunesImage() -> Response: """ Interprets the fetched iTunes URL and saves the image to the user's folder. :return: [Response] The response to the request. """ - log.debug("POST - Generating artwork using an iTunes image...") + log.log("POST - Generating artwork using an iTunes image...") body = literal_eval(request.get_data(as_text=True)) image_url: Optional[str] = body.get("url") if image_url is None: @@ -60,7 +60,7 @@ def useLocalImage() -> Response: """ Saves the uploaded image to the user's folder. :return: [Response] The response to the request. """ - log.debug("POST - Generating artwork using a local image...") + log.log("POST - Generating artwork using a local image...") if "file" not in request.files: return createApiResponse(const.HttpStatus.BAD_REQUEST.value, const.ERR_NO_FILE) file = request.files["file"] @@ -149,7 +149,7 @@ def useYoutubeThumbnail() -> Response: """ Handles the extraction and processing of a YouTube thumbnail from a given URL. :return: [Response] The response to the request. """ - log.debug("POST - Generating artwork using a YouTube thumbnail...") + log.log("POST - Generating artwork using a YouTube thumbnail...") body = literal_eval(request.get_data(as_text=True)) youtube_url: Optional[str] = body.get("url") if youtube_url is None: diff --git a/src/routes/home.py b/src/routes/home.py index 9c15054..00b3cf6 100644 --- a/src/routes/home.py +++ b/src/routes/home.py @@ -17,7 +17,7 @@ def getGeniusToken() -> Response: """ Returns the Genius API token. :return: [Response] The response to the request. """ - log.debug("GET - Fetching Genius API token...") + log.log("GET - Fetching Genius API token...") token = session.get(const.SessionFields.genius_token.value, "") return createApiResponse(const.HttpStatus.OK.value, "Genius API token fetched successfully.", {"token": token}) @@ -27,6 +27,6 @@ def getStatistics() -> Response: """ Returns the statistics as a JSON object. :return: [Response] The response to the request. """ - log.debug("GET - Fetching statistics...") + log.log("GET - Fetching statistics...") stats = getJsonStatsFromFile() return createApiResponse(const.HttpStatus.OK.value, "Statistics fetched successfully.", stats) diff --git a/src/routes/lyrics.py b/src/routes/lyrics.py index 99c340e..72a1ff0 100644 --- a/src/routes/lyrics.py +++ b/src/routes/lyrics.py @@ -83,7 +83,7 @@ def getGeniusLyrics() -> Response: """ Fetches the lyrics of a song from Genius.com. :return: [Response] The response to the request. """ - log.debug("POST - Fetching lyrics from Genius...") + log.log("POST - Fetching lyrics from Genius...") body = literal_eval(request.get_data(as_text=True)) song_name: Optional[str] = body.get("songName") artist: Optional[str] = body.get("artist")