Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat/082-unit-tests: various unit tests on an auxiliary page #92

Merged
merged 8 commits into from
Sep 14, 2024
7 changes: 4 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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)
Expand Down
10 changes: 10 additions & 0 deletions front-end/src/App.css
Original file line number Diff line number Diff line change
Expand Up @@ -369,4 +369,14 @@ input[type="file"] {
}
.bold {
font-weight: bold;
}

.t-green {
color: #007f00;
}
.t-yellow {
color: #7f7f00;
}
.t-red {
color: #7f0000;
}
2 changes: 2 additions & 0 deletions front-end/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand All @@ -18,6 +19,7 @@ const App = (): JSX.Element => {
<Route path={PATHS.processedImages} element={<ProcessedImages />} />
<Route path={PATHS.artworkGeneration} element={<ArtworkGeneration />} />
<Route path={PATHS.redirect} element={<Redirect />} />
<Route path={PATHS.tests} element={<Tests />} />
<Route path={PATHS.home} element={<Home />} />
<Route path="*" element={<Home />} />
</Routes>
Expand Down
2 changes: 2 additions & 0 deletions front-end/src/constants/Common.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ export const TITLE = {
PREFIX: "GTFR-CG - ",

REDIRECT: "Redirect",
TESTS: "Tests",
HOME: "Home",
ARTWORK_GENERATION: "Artwork Generation",
PROCESSED_IMAGES: "Processed Images",
Expand All @@ -86,6 +87,7 @@ export const TITLE = {
export const PATHS = {
redirect: "/redirect",
home: "/home",
tests: "/tests",
artworkGeneration: "/artwork-generation",
processedImages: "/processed-images",
lyrics: "/lyrics",
Expand Down
18 changes: 12 additions & 6 deletions front-end/src/pages/Home/Home.css
Original file line number Diff line number Diff line change
@@ -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;
}
}

Expand Down Expand Up @@ -36,4 +42,4 @@
}
}
}
}
}
5 changes: 5 additions & 0 deletions front-end/src/pages/Home/Home.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,11 @@ const Home = (): JSX.Element => {
</div>
</div>

<div className="tests navbar">
<button type="button" onClick={() => navigate(PATHS.tests)}>
<span className="right">{TITLE.TESTS}</span>
</button>
</div>
<div className="hidden">
<p>Genius Token: '{ geniusToken }'</p>
</div>
Expand Down
49 changes: 49 additions & 0 deletions front-end/src/pages/Tests/Test.tsx
Original file line number Diff line number Diff line change
@@ -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<TestResult>) => Promise<void>;

export type TestProps = {
title: string;
func: TestFunc;
buttonRef: RefObject<HTMLButtonElement>;
};

export const Test = (props: TestProps) => {
const { title, func, buttonRef } = props;

const [result, setResult] = useState<TestResult>({} as TestResult);
const [testIsRunning, setTestIsRunning] = useState(false);

const runTest = async (func: TestFunc) => {
setResult({} as TestResult);
setTestIsRunning(true);
await func(setResult);
setTestIsRunning(false);
};

return (
<div id={`test-${title.replace(" ", "_").toLowerCase()}`} className="test flex-row">
<h3>{title}</h3>
{ isEmpty(result) ?
<button type="button" ref={buttonRef} onClick={() => runTest(func)}>
{testIsRunning ? "Running..." : "Run Test"}
</button>
:
<p>
Test completed<br/>
<span className={`${result?.successful ? "t-green" : "t-red"}`}>{result?.successful ? "successfully" : "with a failure"}</span><br/>
in {result?.duration} milliseconds
</p>
}
</div>
);
}
53 changes: 53 additions & 0 deletions front-end/src/pages/Tests/Tests.css
Original file line number Diff line number Diff line change
@@ -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;
}
}
}
}
}
}
}
152 changes: 152 additions & 0 deletions front-end/src/pages/Tests/Tests.tsx
Original file line number Diff line number Diff line change
@@ -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<HTMLButtonElement>(null);
const refStatistics = useRef<HTMLButtonElement>(null);
const refSnacks = useRef<HTMLButtonElement>(null);
const refItunes = useRef<HTMLButtonElement>(null);
const refGenius = useRef<HTMLButtonElement>(null);

const navigate = useNavigate();

// ENV-VAR
const testGeniusToken = async (setter: StateSetter<TestResult>) => {
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<TestResult>) => {
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<TestResult>) => {
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<TestResult>) => {
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<TestResult>) => {
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 (
<div id="tests">
<div id="toast-container"></div>
<span className="top-bot-spacer" />

<div className="navbar">
<button type="button" onClick={() => navigate(PATHS.home)}>
<span className="left">{TITLE.HOME}</span>
</button>
</div>

<h1>Tests</h1>

<button type="button" className={clickedRunAll ? "hidden" : ""} id="run-all" onClick={handleRunAll}>Run All Tests</button>

<div id="page" className="flex-row">
<div className="column">
<TestsBoard {...(boards.find((b) => b.id === "env-var"))} />
<TestsBoard {...(boards.find((b) => b.id === "front-end"))} />
</div>

<div className="column">
<TestsBoard {...(boards.find((b) => b.id === "api"))} />
</div>
</div>

<span className="top-bot-spacer" />
</div>
);
};

export default Tests;
20 changes: 20 additions & 0 deletions front-end/src/pages/Tests/TestsBoard.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<div id={id} className="board">
<h2 className="title">{title}</h2>
{ tests?.map((test) => (
<Test key={test.title} title={test.title} func={test.func} buttonRef={test.buttonRef} />
))}
</div>
);
};
Loading