diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml
index 98a16a8..b7e1b9f 100644
--- a/.github/workflows/pull_request.yml
+++ b/.github/workflows/pull_request.yml
@@ -18,7 +18,7 @@ jobs:
- name: Checkout repository
uses: actions/checkout@v6
- name: Setup .NET
- uses: actions/checkout@v5
+ uses: actions/setup-dotnet@v5
with:
dotnet-version: 10.0.x
- name: Setup Node.js
@@ -34,12 +34,12 @@ jobs:
- name: Test solution
run: dotnet test
- name: Upload server test coverage
- uses: action/upload-artifact@v4
+ uses: actions/upload-artifact@v4
with:
name: server-test-coverage
path: tests/GroundsForSupport.Server.Tests/TestResults/
- name: Upload client test coverage
- uses: action/upload-artifact@v4
+ uses: actions/upload-artifact@v4
with:
name: client-test-coverage
path: src/GroundsForSupport.Client/coverage/
@@ -50,13 +50,17 @@ jobs:
- name: Checkout repository
uses: actions/checkout@v6
- name: Setup .NET
- uses: actions/checkout@v5
+ uses: actions/setup-dotnet@v5
with:
dotnet-version: 10.0.x
- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version: lts/*
+ - name: Restore dependencies
+ run: dotnet restore
+ - name: Build solution
+ run: dotnet build --no-restore
- name: Format dotnet solution
run: dotnet format
- name: Lint and format client
diff --git a/.gitignore b/.gitignore
index 7b97f43..f588023 100644
--- a/.gitignore
+++ b/.gitignore
@@ -503,15 +503,4 @@ dist
dist-ssr
*.local
-# Editor directories and files
-.vscode/*
-!.vscode/extensions.json
-.idea
-.DS_Store
-*.suo
-*.ntvs*
-*.njsproj
-*.sln
-*.sw?
-
coverage/
diff --git a/GroundsForSupport.sln b/GroundsForSupport.sln
new file mode 100644
index 0000000..37e03a0
--- /dev/null
+++ b/GroundsForSupport.sln
@@ -0,0 +1,59 @@
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio Version 17
+VisualStudioVersion = 17.0.31903.59
+MinimumVisualStudioVersion = 10.0.40219.1
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{827E0CD3-B72D-47B6-A68D-7590B98EB39B}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GroundsForSupport.Server", "src\GroundsForSupport.Server\GroundsForSupport.Server.csproj", "{9B4FDA2C-7F40-43EB-AA84-77A07CBBFC10}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{0AB3BF05-4346-4AA6-1389-037BE0695223}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "GroundsForSupport.Server.Tests", "GroundsForSupport.Server.Tests", "{56565355-472A-D7A5-BBD6-5A9243A600D2}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GroundsForSupport.API.Tests", "tests\GroundsForSupport.Server.Tests\GroundsForSupport.API.Tests.csproj", "{73D83B11-C5BC-4C4B-82BF-2DCEE689EDDC}"
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|Any CPU = Debug|Any CPU
+ Debug|x64 = Debug|x64
+ Debug|x86 = Debug|x86
+ Release|Any CPU = Release|Any CPU
+ Release|x64 = Release|x64
+ Release|x86 = Release|x86
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {9B4FDA2C-7F40-43EB-AA84-77A07CBBFC10}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {9B4FDA2C-7F40-43EB-AA84-77A07CBBFC10}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {9B4FDA2C-7F40-43EB-AA84-77A07CBBFC10}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {9B4FDA2C-7F40-43EB-AA84-77A07CBBFC10}.Debug|x64.Build.0 = Debug|Any CPU
+ {9B4FDA2C-7F40-43EB-AA84-77A07CBBFC10}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {9B4FDA2C-7F40-43EB-AA84-77A07CBBFC10}.Debug|x86.Build.0 = Debug|Any CPU
+ {9B4FDA2C-7F40-43EB-AA84-77A07CBBFC10}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {9B4FDA2C-7F40-43EB-AA84-77A07CBBFC10}.Release|Any CPU.Build.0 = Release|Any CPU
+ {9B4FDA2C-7F40-43EB-AA84-77A07CBBFC10}.Release|x64.ActiveCfg = Release|Any CPU
+ {9B4FDA2C-7F40-43EB-AA84-77A07CBBFC10}.Release|x64.Build.0 = Release|Any CPU
+ {9B4FDA2C-7F40-43EB-AA84-77A07CBBFC10}.Release|x86.ActiveCfg = Release|Any CPU
+ {9B4FDA2C-7F40-43EB-AA84-77A07CBBFC10}.Release|x86.Build.0 = Release|Any CPU
+ {73D83B11-C5BC-4C4B-82BF-2DCEE689EDDC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {73D83B11-C5BC-4C4B-82BF-2DCEE689EDDC}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {73D83B11-C5BC-4C4B-82BF-2DCEE689EDDC}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {73D83B11-C5BC-4C4B-82BF-2DCEE689EDDC}.Debug|x64.Build.0 = Debug|Any CPU
+ {73D83B11-C5BC-4C4B-82BF-2DCEE689EDDC}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {73D83B11-C5BC-4C4B-82BF-2DCEE689EDDC}.Debug|x86.Build.0 = Debug|Any CPU
+ {73D83B11-C5BC-4C4B-82BF-2DCEE689EDDC}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {73D83B11-C5BC-4C4B-82BF-2DCEE689EDDC}.Release|Any CPU.Build.0 = Release|Any CPU
+ {73D83B11-C5BC-4C4B-82BF-2DCEE689EDDC}.Release|x64.ActiveCfg = Release|Any CPU
+ {73D83B11-C5BC-4C4B-82BF-2DCEE689EDDC}.Release|x64.Build.0 = Release|Any CPU
+ {73D83B11-C5BC-4C4B-82BF-2DCEE689EDDC}.Release|x86.ActiveCfg = Release|Any CPU
+ {73D83B11-C5BC-4C4B-82BF-2DCEE689EDDC}.Release|x86.Build.0 = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+ GlobalSection(NestedProjects) = preSolution
+ {9B4FDA2C-7F40-43EB-AA84-77A07CBBFC10} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
+ {56565355-472A-D7A5-BBD6-5A9243A600D2} = {0AB3BF05-4346-4AA6-1389-037BE0695223}
+ {73D83B11-C5BC-4C4B-82BF-2DCEE689EDDC} = {56565355-472A-D7A5-BBD6-5A9243A600D2}
+ EndGlobalSection
+EndGlobal
diff --git a/src/GroundsForSupport.Client/eslint.config.js b/src/GroundsForSupport.Client/eslint.config.js
index 5eba67f..5ae55ff 100644
--- a/src/GroundsForSupport.Client/eslint.config.js
+++ b/src/GroundsForSupport.Client/eslint.config.js
@@ -7,12 +7,22 @@ import eslintConfigPrettier from 'eslint-config-prettier';
import { defineConfig, globalIgnores } from 'eslint/config';
export default defineConfig([
- globalIgnores(['dist']),
+ globalIgnores(['dist', 'node_modules', 'coverage']),
{
+ rules: {
+ '@typescript-eslint/no-floating-promises': 'error',
+ },
files: ['**/*.{ts,tsx}'],
extends: [
js.configs.recommended,
- tseslint.configs.recommended,
+ tseslint.configs.recommendedTypeChecked,
+ {
+ languageOptions: {
+ parserOptions: {
+ projectService: true,
+ },
+ },
+ },
reactHooks.configs.flat.recommended,
reactRefresh.configs.vite,
],
diff --git a/src/GroundsForSupport.Client/index.html b/src/GroundsForSupport.Client/index.html
index f1cea22..8e49db4 100644
--- a/src/GroundsForSupport.Client/index.html
+++ b/src/GroundsForSupport.Client/index.html
@@ -4,14 +4,14 @@
-
groundsforsupport-client
+ Grounds For Support
diff --git a/src/GroundsForSupport.Client/package-lock.json b/src/GroundsForSupport.Client/package-lock.json
index 378c8da..be14638 100644
--- a/src/GroundsForSupport.Client/package-lock.json
+++ b/src/GroundsForSupport.Client/package-lock.json
@@ -16,6 +16,7 @@
"@testing-library/dom": "^10.4.1",
"@testing-library/jest-dom": "^6.9.1",
"@testing-library/react": "^16.3.0",
+ "@testing-library/user-event": "^14.6.1",
"@types/node": "^24.10.1",
"@types/react": "^19.2.7",
"@types/react-dom": "^19.2.3",
@@ -1197,6 +1198,20 @@
}
}
},
+ "node_modules/@testing-library/user-event": {
+ "version": "14.6.1",
+ "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-14.6.1.tgz",
+ "integrity": "sha512-vq7fv0rnt+QTXgPxr5Hjc210p6YKq2kmdziLgnsZGgLJ9e6VAShx1pACLuRjd/AS/sr7phAR58OIIpf0LlmQNw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12",
+ "npm": ">=6"
+ },
+ "peerDependencies": {
+ "@testing-library/dom": ">=7.21.4"
+ }
+ },
"node_modules/@tybys/wasm-util": {
"version": "0.10.1",
"resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz",
diff --git a/src/GroundsForSupport.Client/package.json b/src/GroundsForSupport.Client/package.json
index 4abf119..04f08ca 100644
--- a/src/GroundsForSupport.Client/package.json
+++ b/src/GroundsForSupport.Client/package.json
@@ -20,6 +20,7 @@
"@testing-library/dom": "^10.4.1",
"@testing-library/jest-dom": "^6.9.1",
"@testing-library/react": "^16.3.0",
+ "@testing-library/user-event": "^14.6.1",
"@types/node": "^24.10.1",
"@types/react": "^19.2.7",
"@types/react-dom": "^19.2.3",
diff --git a/src/GroundsForSupport.Client/public/CaskaydiaCoveNFM-Regular.eot b/src/GroundsForSupport.Client/public/CaskaydiaCoveNFM-Regular.eot
new file mode 100644
index 0000000..a2dfa73
Binary files /dev/null and b/src/GroundsForSupport.Client/public/CaskaydiaCoveNFM-Regular.eot differ
diff --git a/src/GroundsForSupport.Client/public/CaskaydiaCoveNFM-Regular.svg b/src/GroundsForSupport.Client/public/CaskaydiaCoveNFM-Regular.svg
new file mode 100644
index 0000000..9de7977
--- /dev/null
+++ b/src/GroundsForSupport.Client/public/CaskaydiaCoveNFM-Regular.svg
@@ -0,0 +1,45391 @@
+
+
+
diff --git a/src/GroundsForSupport.Client/public/CaskaydiaCoveNFM-Regular.woff b/src/GroundsForSupport.Client/public/CaskaydiaCoveNFM-Regular.woff
new file mode 100644
index 0000000..3b1b065
Binary files /dev/null and b/src/GroundsForSupport.Client/public/CaskaydiaCoveNFM-Regular.woff differ
diff --git a/src/GroundsForSupport.Client/public/CaskaydiaCoveNFM-Regular.woff2 b/src/GroundsForSupport.Client/public/CaskaydiaCoveNFM-Regular.woff2
new file mode 100644
index 0000000..9358aff
Binary files /dev/null and b/src/GroundsForSupport.Client/public/CaskaydiaCoveNFM-Regular.woff2 differ
diff --git a/src/GroundsForSupport.Client/public/vite.svg b/src/GroundsForSupport.Client/public/vite.svg
deleted file mode 100644
index e7b8dfb..0000000
--- a/src/GroundsForSupport.Client/public/vite.svg
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git a/src/GroundsForSupport.Client/src/App.css b/src/GroundsForSupport.Client/src/App.css
index b9d355d..c4d5437 100644
--- a/src/GroundsForSupport.Client/src/App.css
+++ b/src/GroundsForSupport.Client/src/App.css
@@ -1,42 +1,85 @@
-#root {
- max-width: 1280px;
- margin: 0 auto;
- padding: 2rem;
+header {
+ background-color: #181818;
+ flex-direction: column;
+ justify-content: center;
+ align-items: center;
+ gap: 0.5rem;
+ padding: 1rem;
+ display: flex;
+ box-shadow: 0 8px 12px #000000b0;
+}
+
+header img {
+ aspect-ratio: 1;
+ border: 2px solid #b39cd0;
+ border-radius: 0.25rem;
+ width: 12rem;
+}
+
+header .info {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: 0.25rem;
+ max-width: 31.25rem;
text-align: center;
}
-.logo {
- height: 6em;
- padding: 1.5em;
- will-change: filter;
- transition: filter 300ms;
-}
-.logo:hover {
- filter: drop-shadow(0 0 2em #646cffaa);
-}
-.logo.react:hover {
- filter: drop-shadow(0 0 2em #61dafbaa);
+form {
+ background-color: #181818;
+ border-radius: 0.25rem;
+ flex-direction: column;
+ gap: 0.5rem;
+ width: 100%;
+ max-width: 31.25rem;
+ padding: 1rem;
+ display: flex;
+ box-shadow: 0 4px 8px #0000001a;
}
-@keyframes logo-spin {
- from {
- transform: rotate(0deg);
- }
- to {
- transform: rotate(360deg);
- }
+form .group {
+ flex-direction: column;
+ gap: 0.5rem;
+ display: flex;
}
-@media (prefers-reduced-motion: no-preference) {
- a:nth-of-type(2) .logo {
- animation: logo-spin infinite 20s linear;
- }
+form label {
+ font-weight: 700;
}
-.card {
- padding: 2em;
+form input {
+ color: #e4e4e4;
+ background-color: #282828;
+ border: 1px solid #444;
+ border-radius: 0.25rem;
+ padding: 0.5rem;
}
-.read-the-docs {
- color: #888;
+form input[type='number']::-webkit-outer-spin-button {
+ -webkit-appearance: none;
+ margin: 0;
+}
+
+form input[type='number']::-webkit-inner-spin-button {
+ -webkit-appearance: none;
+ margin: 0;
+}
+
+form input[type='number'] {
+ appearance: textfield;
+}
+
+.error-message {
+ color: #ff6b6b;
+ font-size: 0.875rem;
+}
+
+form button {
+ color: #fff;
+ background-color: #b39cd0;
+ border: none;
+ border-radius: 0.25rem;
+ padding: 0.75rem;
+ font-weight: 700;
+ transition: background-color 0.3s;
}
diff --git a/src/GroundsForSupport.Client/src/App.tsx b/src/GroundsForSupport.Client/src/App.tsx
index 04d45ac..01e704f 100644
--- a/src/GroundsForSupport.Client/src/App.tsx
+++ b/src/GroundsForSupport.Client/src/App.tsx
@@ -1,43 +1,121 @@
-import { useState } from 'react';
-import reactLogo from './assets/react.svg';
-import viteLogo from '/vite.svg';
import '@/App.css';
+import { useRef, useState } from 'react';
function App() {
- const [count, setCount] = useState(0);
+ const [amount, setAmount] = useState('');
+ const [email, setEmail] = useState('');
+ const [errors, setErrors] = useState<{ amount?: string; email?: string }>({});
+
+ const amountInputRef = useRef(null);
+ const emailInputRef = useRef(null);
+
+ function handleAmountInput(event: React.ChangeEvent) {
+ setErrors((prevErrors) => ({ ...prevErrors, amount: undefined }));
+ const value = parseInt(event.target.value);
+ setAmount(isNaN(value) ? '' : value);
+ }
+
+ function handleEmailInput(event: React.ChangeEvent) {
+ setErrors((prevErrors) => ({ ...prevErrors, email: undefined }));
+ setEmail(event.target.value);
+ }
+
+ function validateForm() {
+ const newErrors: { amount?: string; email?: string } = {};
+
+ if (amount === '' || amount <= 0) {
+ newErrors.amount = 'Please enter a valid amount greater than 0.';
+ }
+
+ if (email.trim() !== '' && emailInputRef.current?.validity.typeMismatch) {
+ newErrors.email = 'Please enter a valid email address.';
+ }
+
+ setErrors(newErrors);
+
+ const isValid = Object.keys(newErrors).length === 0;
+
+ if (isValid === false) {
+ if (newErrors.amount) {
+ amountInputRef.current?.focus();
+ } else if (newErrors.email) {
+ emailInputRef.current?.focus();
+ }
+ }
+
+ return isValid;
+ }
+
+ function handleSubmit(event: React.FormEvent) {
+ event.preventDefault();
+
+ const isValid = validateForm();
+
+ if (isValid === false) {
+ return;
+ }
+
+ // TODO: We will stop displaying our form
+ // and we will initialize the stripe embedded
+ // elements and pass along the amount and email
+ alert(`Form is valid! Amount: ${amount}, Email: ${email}`);
+ }
return (
<>
-
- Vite + React
-
-
-
- Edit src/App.tsx and save to test HMR
-
-
- Click on the Vite and React logos to learn more
+
+
+
+ {errors.amount}
+
+
+
+
+
+ {errors.email}
+
+
+
+
+
>
);
}
diff --git a/src/GroundsForSupport.Client/src/assets/react.svg b/src/GroundsForSupport.Client/src/assets/react.svg
deleted file mode 100644
index 6c87de9..0000000
--- a/src/GroundsForSupport.Client/src/assets/react.svg
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git a/src/GroundsForSupport.Client/src/index.css b/src/GroundsForSupport.Client/src/index.css
index 08a3ac9..a06c4bb 100644
--- a/src/GroundsForSupport.Client/src/index.css
+++ b/src/GroundsForSupport.Client/src/index.css
@@ -1,68 +1,80 @@
-:root {
- font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
- line-height: 1.5;
- font-weight: 400;
-
- color-scheme: light dark;
- color: rgba(255, 255, 255, 0.87);
- background-color: #242424;
-
- font-synthesis: none;
- text-rendering: optimizeLegibility;
- -webkit-font-smoothing: antialiased;
- -moz-osx-font-smoothing: grayscale;
+@font-face {
+ font-family: 'CaskaydiaCove NFM';
+ src: url('/CaskaydiaCoveNFM-Regular.eot');
+ src: url('/CaskaydiaCoveNFM-Regular.eot?#iefix') format('embedded-opentype'),
+ url('/CaskaydiaCoveNFM-Regular.woff2') format('woff2'),
+ url('/CaskaydiaCoveNFM-Regular.woff') format('woff'),
+ url('/CaskaydiaCoveNFM-Regular.svg#CaskaydiaCoveNFM-Regular') format('svg');
+ font-weight: normal;
+ font-style: normal;
+ font-display: swap;
}
-a {
- font-weight: 500;
- color: #646cff;
- text-decoration: inherit;
-}
-a:hover {
- color: #535bf2;
-}
+/* CSS Reset */
-body {
+* {
margin: 0;
- display: flex;
- place-items: center;
- min-width: 320px;
- min-height: 100vh;
+ padding: 0;
}
-h1 {
- font-size: 3.2em;
- line-height: 1.1;
+*,
+*::before,
+*::after {
+ box-sizing: border-box;
}
button {
- border-radius: 8px;
- border: 1px solid transparent;
- padding: 0.6em 1.2em;
- font-size: 1em;
- font-weight: 500;
- font-family: inherit;
- background-color: #1a1a1a;
+ background: none;
+ border: none;
+ outline: none;
cursor: pointer;
- transition: border-color 0.25s;
-}
-button:hover {
- border-color: #646cff;
-}
-button:focus,
-button:focus-visible {
- outline: 4px auto -webkit-focus-ring-color;
+ font-family: inherit;
+ font-size: inherit;
+ color: inherit;
}
-@media (prefers-color-scheme: light) {
- :root {
- color: #213547;
- background-color: #ffffff;
- }
- a:hover {
- color: #747bff;
- }
- button {
- background-color: #f9f9f9;
- }
+a {
+ text-decoration: none;
+ color: inherit;
}
+
+ul {
+ list-style-type: none;
+}
+
+img {
+ max-width: 100%;
+ display: block;
+}
+
+table {
+ width: 100%;
+ border-collapse: collapse;
+}
+
+/* Base Styles */
+
+:root {
+ --bg-color: #323232;
+ --text-color: #E4E4E4;
+
+ font-size: 16px;
+ font-family: 'CaskaydiaCove NFM', monospace;
+ font-size: 16px;
+}
+
+html,
+body {
+ height: 100%;
+ background-color: var(--bg-color);
+ color: var(--text-color);
+}
+
+main {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ padding: 1rem;
+ padding-top: 2rem;
+}
+
diff --git a/src/GroundsForSupport.Client/tests/App.spec.tsx b/src/GroundsForSupport.Client/tests/App.spec.tsx
index 97b8ce7..7eed8bd 100644
--- a/src/GroundsForSupport.Client/tests/App.spec.tsx
+++ b/src/GroundsForSupport.Client/tests/App.spec.tsx
@@ -1,15 +1,134 @@
-import { expect, test } from 'vitest';
+import { beforeEach, describe, expect, test, vi } from 'vitest';
import App from '@/App.tsx';
-import { render, fireEvent } from '@testing-library/react';
+import { render } from '@testing-library/react';
import '@testing-library/jest-dom';
+import userEvent, { type UserEvent } from '@testing-library/user-event';
-test('counter button increments the count', () => {
- const screen = render();
+describe('profile info', () => {
+ test('it should display a profile image', () => {
+ const screen = render();
+
+ const profileImage = screen.getByAltText('Profile picture of Stevan Freeborn');
- const button = screen.getByRole('button');
+ expect(profileImage).toBeInTheDocument();
+ });
- fireEvent.click(button);
- fireEvent.click(button);
+ test('it should display name', () => {
+ const screen = render();
- expect(button).toHaveTextContent('count is 2');
+ const nameElement = screen.getByRole('heading', { name: 'Stevan Freeborn' });
+
+ expect(nameElement).toBeInTheDocument();
+ });
+});
+
+describe('form', () => {
+ let user: UserEvent;
+
+ beforeEach(() => {
+ user = userEvent.setup();
+ });
+
+ test('it should have an amount input', () => {
+ const screen = render();
+
+ const amountInput = screen.getByLabelText('Amount');
+
+ expect(amountInput).toBeInTheDocument();
+ });
+
+ test('it should have an email input', () => {
+ const screen = render();
+
+ const emailInput = screen.getByLabelText('Email');
+
+ expect(emailInput).toBeInTheDocument();
+ });
+
+ test('it should have a button to submit the form', () => {
+ const screen = render();
+
+ const submitButton = screen.getByRole('button', { name: 'Buy' });
+
+ expect(submitButton).toBeInTheDocument();
+ });
+
+ test('it should require the amount to be provided', async () => {
+ const screen = render();
+
+ const submitButton = screen.getByRole('button', { name: 'Buy' });
+
+ await user.click(submitButton);
+
+ const errorMessage = screen.getByText('Please enter a valid amount greater than 0.');
+ expect(errorMessage).toBeInTheDocument();
+ });
+
+ test('it should clear amount error when input changes', async () => {
+ const screen = render();
+
+ const amountInput = screen.getByLabelText('Amount');
+ const submitButton = screen.getByRole('button', { name: 'Buy' });
+
+ await user.click(submitButton);
+
+ let errorMessage = screen.queryByText('Please enter a valid amount greater than 0.');
+ expect(errorMessage).toBeInTheDocument();
+
+ await userEvent.type(amountInput, '10');
+
+ errorMessage = screen.queryByText('Please enter a valid amount greater than 0.');
+
+ expect(errorMessage).not.toBeInTheDocument();
+ });
+
+ test('it should allow form to be submitted without email', async () => {
+ const windowSpy = vi.spyOn(window, 'alert').mockImplementation(() => {});
+
+ const screen = render();
+
+ const amountInput = screen.getByLabelText('Amount');
+ const submitButton = screen.getByRole('button', { name: 'Buy' });
+
+ await user.type(amountInput, '10');
+ await user.click(submitButton);
+
+ expect(windowSpy).toHaveBeenCalledWith('Form is valid! Amount: 10, Email: ');
+ });
+
+ test('it should validate email if provided', async () => {
+ const screen = render();
+
+ const amountInput = screen.getByLabelText('Amount');
+ const emailInput = screen.getByLabelText('Email');
+ const submitButton = screen.getByRole('button', { name: 'Buy' });
+
+ await user.type(amountInput, '10');
+ await user.type(emailInput, 'invalid-email');
+ await user.click(submitButton);
+
+ const errorMessage = screen.getByText('Please enter a valid email address.');
+ expect(errorMessage).toBeInTheDocument();
+ });
+
+ test('it should clear email error when input changes', async () => {
+ const screen = render();
+
+ const amountInput = screen.getByLabelText('Amount');
+ const emailInput = screen.getByLabelText('Email');
+ const submitButton = screen.getByRole('button', { name: 'Buy' });
+
+ await user.type(amountInput, '10');
+ await user.type(emailInput, 'invalid-email');
+ await user.click(submitButton);
+
+ let errorMessage = screen.queryByText('Please enter a valid email address.');
+ expect(errorMessage).toBeInTheDocument();
+
+ await user.clear(emailInput);
+ await user.type(emailInput, 'test@test.com');
+
+ errorMessage = screen.queryByText('Please enter a valid email address.');
+ expect(errorMessage).not.toBeInTheDocument();
+ });
});