finsh commit

This commit is contained in:
shenjianZ 2025-09-14 08:51:47 +08:00
commit 838806b9e3
60 changed files with 5697 additions and 0 deletions

151
.gitignore vendored Normal file
View File

@ -0,0 +1,151 @@
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
pip-wheel-metadata/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
target/
# Jupyter Notebook
.ipynb_checkpoints
# IPython
profile_default/
ipython_config.py
# pyenv
.python-version
# celery beat schedule file
celerybeat-schedule
# SageMath parsed files
*.sage.py
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyderworkspace
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# Pyre type checker
.pyre/
# pytype static analyzer
.pytype/
# Cython debug symbols
cython_debug/
# VS Code settings
.vscode/
# Selenium Automation Project specific
/reports/allure_results/
/reports/allure_html/
/reports/screenshots/
/drivers/
*.log
# Frontend specific
/frontend/node_modules
/frontend/dist
/frontend/dist-ssr
/frontend/*.local
# Editor directories and files
/frontend/.vscode/*
!/frontend/.vscode/extensions.json
/frontend/.idea
/frontend/.DS_Store
/frontend/*.suo
/frontend/*.ntvs*
/frontend/*.njsproj
/frontend/*.sln
/frontend/*.sw?

82
README.md Normal file
View File

@ -0,0 +1,82 @@
# Selenium UI Automation Project
This is a UI automation testing project based on Selenium and Pytest. It provides a basic framework for web UI testing, including page object model (POM), test case management, and test report generation.
## Features
- **Page Object Model (POM)**: Separates UI elements and business logic from test cases for better maintainability.
- **Pytest Framework**: A powerful testing framework for writing simple and scalable test cases.
- **Allure Reports**: Generates beautiful and detailed test reports.
- **Automatic Screenshots**: Automatically captures screenshots on test failure and attaches them to the Allure report.
- **WebDriver Manager**: Automatically manages the browser driver (ChromeDriver).
## Tech Stack
- **Language**: Python 3.x
- **Automation Tool**: Selenium
- **Testing Framework**: Pytest
- **Reporting**: Allure
- **Driver Management**: webdriver-manager
## Environment Setup
1. **Install Python**: Make sure you have Python 3.x installed. You can download it from the [official Python website](https://www.python.org/).
2. **Install Allure**: Allure is required to generate test reports. Follow the official instructions to install it on your system:
- [Install Allure Commandline](https://allurereport.org/docs/gettingstarted-installation/)
## Installation and Execution
1. **Clone the Repository**:
```bash
git clone <repository_url>
cd selenium_automation_project
```
2. **Install Dependencies**:
It is recommended to use a virtual environment to manage dependencies.
```bash
# Create and activate a virtual environment (optional)
python -m venv venv
source venv/bin/activate # On Windows, use `venv\Scripts\activate`
# Install the required packages
pip install -r requirements.txt
```
3. **Run Tests**:
You can run all tests using the following command:
```bash
pytest
```
To run specific tests, you can use Pytest's markers. For example, to run only the smoke tests:
```bash
pytest -m smoke
```
For more detailed console output during test execution, you can use the `-v` (verbose) and `-s` (show print statements) flags:
```bash
pytest -vs
```
## Generating Test Reports
After running the tests, the Allure results will be saved in the `reports/allure_results` directory. You can generate the report in two ways:
### 1. Online Report (Temporary Preview)
This method starts a local web server to view the report. The report is temporary and will be gone once the server is stopped.
```bash
allure serve reports/allure_results
```
This command will automatically open the report in your default browser.
### 2. Offline Report (Persistent HTML)
This method generates a persistent HTML report that you can open and share.
```bash
allure generate reports/allure_results -o reports/allure_html --clean
```
This will create the report in the `reports/allure_html` directory. You can then open the `index.html` file in that directory to view the report.

0
common/__init__.py Normal file
View File

0
common/exceptions.py Normal file
View File

0
common/logger.py Normal file
View File

0
common/yaml_handler.py Normal file
View File

0
config/__init__.py Normal file
View File

0
config/dev_env.yaml Normal file
View File

0
config/prod_env.yaml Normal file
View File

0
core/__init__.py Normal file
View File

View File

0
data/__init__.py Normal file
View File

0
data/login_data.csv Normal file
View File

25
frontend/.gitignore vendored Normal file
View File

@ -0,0 +1,25 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
package-lock.json

70
frontend/README.md Normal file
View File

@ -0,0 +1,70 @@
# Vite + React + TypeScript + shadcn 模板
这是一个预配置了 Vite, React, TypeScript 和 shadcn 的入门模板,可以帮助你快速启动新项目。
## ✨ 特性
- ⚡️ **Vite**: 极速的下一代前端构建工具。
- ⚛️ **React**: 用于构建用户界面的 JavaScript 库。
- 📘 **TypeScript**: JavaScript 的超集,添加了类型支持。
- 🎨 **Tailwind CSS**: 一个功能类优先的 CSS 框架。
- 🧩 **shadcn**: 设计精美、可重用的组件,你可以直接复制粘贴到你的应用中。
## 🚀 快速上手
1. **克隆或使用此模板创建你的项目**
2. **安装依赖**
```bash
npm install
```
3. **启动开发服务器**
```bash
npm run dev
```
现在,在浏览器中打开指定的本地地址 (通常是 `http://localhost:5173`) 即可查看。
## 📦 添加组件
现在你可以开始向你的项目添加组件了。
```bash
npx shadcn@latest add [component-name]
```
例如,要添加一个 `button` 组件:
```bash
npx shadcn@latest add button
```
新添加的组件会出现在 `src/components/ui` 目录下。你可以像这样导入它:
**`src/App.tsx`**
```tsx
import { Button } from "@/components/ui/button"
function App() {
return (
<div className="flex min-h-svh flex-col items-center justify-center">
<Button>Click me</Button>
</div>
)
}
export default App
```
## 📜 可用脚本
`package.json` 中定义了以下脚本:
- `npm run dev`: 在开发模式下启动应用,支持热更新。
- `npm run build`: 为生产环境构建应用,输出到 `dist` 目录。
- `npm run lint`: 使用 ESLint 检查代码规范。
- `npm run preview`: 在本地预览生产构建的应用。
## 📄 许可证
本项目采用 MIT 许可证。

21
frontend/components.json Normal file
View File

@ -0,0 +1,21 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "new-york",
"rsc": false,
"tsx": true,
"tailwind": {
"config": "",
"css": "src/index.css",
"baseColor": "neutral",
"cssVariables": true,
"prefix": ""
},
"aliases": {
"components": "@/components",
"utils": "@/lib/utils",
"ui": "@/components/ui",
"lib": "@/lib",
"hooks": "@/hooks"
},
"iconLibrary": "lucide"
}

23
frontend/eslint.config.js Normal file
View File

@ -0,0 +1,23 @@
import js from '@eslint/js'
import globals from 'globals'
import reactHooks from 'eslint-plugin-react-hooks'
import reactRefresh from 'eslint-plugin-react-refresh'
import tseslint from 'typescript-eslint'
import { globalIgnores } from 'eslint/config'
export default tseslint.config([
globalIgnores(['dist']),
{
files: ['**/*.{ts,tsx}'],
extends: [
js.configs.recommended,
tseslint.configs.recommended,
reactHooks.configs['recommended-latest'],
reactRefresh.configs.vite,
],
languageOptions: {
ecmaVersion: 2020,
globals: globals.browser,
},
},
])

13
frontend/index.html Normal file
View File

@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite + React + TS</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

48
frontend/package.json Normal file
View File

@ -0,0 +1,48 @@
{
"name": "shadcn-vite-project",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc -b && vite build",
"lint": "eslint .",
"preview": "vite preview"
},
"dependencies": {
"@radix-ui/react-checkbox": "^1.1.1",
"@radix-ui/react-dialog": "^1.1.1",
"@radix-ui/react-label": "^2.1.0",
"@radix-ui/react-radio-group": "^1.2.0",
"@radix-ui/react-select": "^2.1.1",
"@radix-ui/react-slot": "^1.2.3",
"@radix-ui/react-switch": "^1.1.0",
"@radix-ui/react-tabs": "^1.1.0",
"@tailwindcss/vite": "^4.1.12",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"lucide-react": "^0.539.0",
"react": "^19.1.1",
"react-dom": "^19.1.1",
"react-router-dom": "^6.25.1",
"tailwind-merge": "^3.3.1",
"tailwindcss": "^4.1.12"
},
"devDependencies": {
"@eslint/js": "^9.33.0",
"@types/node": "^24.3.0",
"@types/react": "^19.1.10",
"@types/react-dom": "^19.1.7",
"@types/react-router-dom": "^5.3.3",
"@vitejs/plugin-react": "^5.0.0",
"eslint": "^9.33.0",
"eslint-plugin-react-hooks": "^5.2.0",
"eslint-plugin-react-refresh": "^0.4.20",
"globals": "^16.3.0",
"tw-animate-css": "^1.3.6",
"typescript": "~5.8.3",
"typescript-eslint": "^8.39.1",
"vite": "^7.1.2"
},
"packageManager": "pnpm@10.14.0+sha512.ad27a79641b49c3e481a16a805baa71817a04bbe06a38d17e60e2eaee83f6a146c6a688125f5792e48dd5ba30e7da52a5cda4c3992b9ccf333f9ce223af84748"
}

3458
frontend/pnpm-lock.yaml Normal file

File diff suppressed because it is too large Load Diff

1
frontend/public/vite.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="256" height="256" viewBox="0 0 256 256"><path fill="#41D1FF" d="M208.4 27.8c-10-5-21.1-7.8-32.8-7.8c-27.5 0-51.2 16.4-62.2 40.2c-11-23.8-34.7-40.2-62.2-40.2c-11.8 0-22.8 2.8-32.8 7.8c-40.2 19.8-64.4 60-64.4 104.4c0 69.4 61.4 123.8 128 123.8s128-54.4 128-123.8c0-44.4-24.2-84.6-64.4-104.4z"></path><path fill="#3494E6" d="M128 256c66.6 0 128-54.4 128-123.8c0-44.4-24.2-84.6-64.4-104.4c-10-5-21.1-7.8-32.8-7.8c-27.5 0-51.2 16.4-62.2 40.2c-11-23.8-34.7-40.2-62.2-40.2c-11.8 0-22.8 2.8-32.8 7.8C24.2 47.6 0 87.8 0 132.2C0 201.6 61.4 256 128 256z"></path><path fill="#fff" d="m128 184.6l-42.6-85.1h20.2l32.4 65.8l32.4-65.8h20.2L128 184.6z"></path></svg>

After

Width:  |  Height:  |  Size: 695 B

42
frontend/src/App.css Normal file
View File

@ -0,0 +1,42 @@
#root {
max-width: 1280px;
margin: 0 auto;
padding: 2rem;
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);
}
@keyframes logo-spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
@media (prefers-reduced-motion: no-preference) {
a:nth-of-type(2) .logo {
animation: logo-spin infinite 20s linear;
}
}
.card {
padding: 2em;
}
.read-the-docs {
color: #888;
}

79
frontend/src/App.tsx Normal file
View File

@ -0,0 +1,79 @@
import {
BrowserRouter as Router,
Routes,
Route,
Link,
NavLink,
} from "react-router-dom";
import { FormElementsPage } from "./components/pages/form-elements";
import { DynamicContentPage } from "./components/pages/dynamic-content";
import { cn } from "./lib/utils";
function App() {
return (
<Router>
<div className="flex h-screen bg-gray-100">
{/* Sidebar */}
<aside className="w-64 bg-white border-r">
<div className="p-4">
<Link to="/">
<h1 className="text-2xl font-bold">Test App</h1>
</Link>
</div>
<nav className="mt-4">
<ul>
<li>
<NavLink
to="/form-elements"
className={({ isActive }) =>
cn(
"block px-4 py-2 text-gray-700 hover:bg-gray-200",
isActive && "bg-gray-300"
)
}
>
Form Elements
</NavLink>
</li>
<li>
<NavLink
to="/dynamic-content"
className={({ isActive }) =>
cn(
"block px-4 py-2 text-gray-700 hover:bg-gray-200",
isActive && "bg-gray-300"
)
}
>
Dynamic Content
</NavLink>
</li>
</ul>
</nav>
</aside>
{/* Main Content */}
<main className="flex-1 overflow-y-auto">
<Routes>
<Route path="/form-elements" element={<FormElementsPage />} />
<Route
path="/dynamic-content"
element={<DynamicContentPage />}
/>
<Route
path="/"
element={
<div className="p-8">
<h1 className="text-2xl">Welcome to the Test App!</h1>
<p>Select a page from the sidebar to start testing.</p>
</div>
}
/>
</Routes>
</main>
</div>
</Router>
);
}
export default App;

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg>

After

Width:  |  Height:  |  Size: 4.0 KiB

View File

@ -0,0 +1,120 @@
import { Button } from "@/components/ui/button";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { useEffect, useState } from "react";
export function DynamicContentPage() {
const [loadedText, setLoadedText] = useState("");
const [enableButton, setEnableButton] = useState(false);
useEffect(() => {
const timer = setTimeout(() => {
setLoadedText("This text was loaded after a 3-second delay.");
}, 3000);
return () => clearTimeout(timer);
}, []);
return (
<div className="container mx-auto p-8 space-y-8">
<h1 className="text-3xl font-bold">Dynamic Content Test Page</h1>
{/* Delayed Text */}
<Card>
<CardHeader>
<CardTitle>Delayed Content</CardTitle>
<CardDescription>
This section demonstrates content that appears after a delay.
</CardDescription>
</CardHeader>
<CardContent>
<p id="delayed-text">{loadedText || "Loading..."}</p>
</CardContent>
</Card>
{/* Dynamic Button */}
<Card>
<CardHeader>
<CardTitle>Dynamic Button State</CardTitle>
<CardDescription>
Click the first button to enable the second one.
</CardDescription>
</CardHeader>
<CardContent className="flex space-x-4">
<Button onClick={() => setEnableButton(true)}>Enable Button</Button>
<Button disabled={!enableButton}>Initially Disabled</Button>
</CardContent>
</Card>
{/* Tabs */}
<Card>
<CardHeader>
<CardTitle>Tabs</CardTitle>
<CardDescription>
Click the tabs to switch between content.
</CardDescription>
</CardHeader>
<CardContent>
<Tabs defaultValue="account" className="w-[400px]">
<TabsList>
<TabsTrigger value="account">Account</TabsTrigger>
<TabsTrigger value="password">Password</TabsTrigger>
</TabsList>
<TabsContent value="account">
This is the content for the Account tab.
</TabsContent>
<TabsContent value="password">
This is the content for the Password tab.
</TabsContent>
</Tabs>
</CardContent>
</Card>
{/* Alerts and Modals */}
<Card>
<CardHeader>
<CardTitle>Alerts and Modals</CardTitle>
<CardDescription>
Trigger browser alerts and dialog modals.
</CardDescription>
</CardHeader>
<CardContent className="flex space-x-4">
<Button onClick={() => alert("This is a browser alert!")}>
Show Alert
</Button>
<Dialog>
<DialogTrigger asChild>
<Button variant="outline">Open Modal</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Modal Title</DialogTitle>
<DialogDescription>
This is a modal dialog. You can interact with elements inside
it.
</DialogDescription>
</DialogHeader>
<DialogFooter>
<Button type="submit">Save changes</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</CardContent>
</Card>
</div>
);
}

View File

@ -0,0 +1,97 @@
import { Button } from "@/components/ui/button";
import { Checkbox } from "@/components/ui/checkbox";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Switch } from "@/components/ui/switch";
import { Textarea } from "@/components/ui/textarea";
export function FormElementsPage() {
return (
<div className="container mx-auto p-8 space-y-8">
<h1 className="text-3xl font-bold">Form Elements Test Page</h1>
<form
className="space-y-6 p-6 border rounded-lg shadow-lg"
onSubmit={(e) => {
e.preventDefault();
alert("Form Submitted!");
}}
>
{/* Text Input */}
<div className="space-y-2">
<Label htmlFor="text-input">Text Input</Label>
<Input id="text-input" placeholder="Enter some text" />
</div>
{/* Checkbox */}
<div className="flex items-center space-x-2">
<Checkbox id="checkbox-input" />
<Label htmlFor="checkbox-input">Accept terms and conditions</Label>
</div>
{/* Radio Group */}
<div className="space-y-2">
<Label>Choose an option</Label>
<RadioGroup defaultValue="option-one">
<div className="flex items-center space-x-2">
<RadioGroupItem value="option-one" id="r1" />
<Label htmlFor="r1">Option One</Label>
</div>
<div className="flex items-center space-x-2">
<RadioGroupItem value="option-two" id="r2" />
<Label htmlFor="r2">Option Two</Label>
</div>
<div className="flex items-center space-x-2">
<RadioGroupItem value="option-three" id="r3" />
<Label htmlFor="r3">Option Three</Label>
</div>
</RadioGroup>
</div>
{/* Select/Dropdown */}
<div className="space-y-2">
<Label htmlFor="select-input">Select a fruit</Label>
<Select>
<SelectTrigger id="select-input">
<SelectValue placeholder="Fruit" />
</SelectTrigger>
<SelectContent>
<SelectItem value="apple">Apple</SelectItem>
<SelectItem value="banana">Banana</SelectItem>
<SelectItem value="blueberry">Blueberry</SelectItem>
<SelectItem value="grapes">Grapes</SelectItem>
<SelectItem value="pineapple">Pineapple</SelectItem>
</SelectContent>
</Select>
</div>
{/* Textarea */}
<div className="space-y-2">
<Label htmlFor="textarea-input">Your Message</Label>
<Textarea id="textarea-input" placeholder="Type your message here." />
</div>
{/* Switch */}
<div className="flex items-center space-x-2">
<Switch id="switch-input" />
<Label htmlFor="switch-input">Airplane Mode</Label>
</div>
{/* Buttons */}
<div className="flex space-x-4">
<Button type="submit">Submit</Button>
<Button type="button" variant="outline">Cancel</Button>
<Button type="button" disabled>Disabled</Button>
</div>
</form>
</div>
);
}

View File

@ -0,0 +1,56 @@
import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const buttonVariants = cva(
"inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/90",
destructive:
"bg-destructive text-destructive-foreground hover:bg-destructive/90",
outline:
"border border-input bg-background hover:bg-accent hover:text-accent-foreground",
secondary:
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
ghost: "hover:bg-accent hover:text-accent-foreground",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default: "h-10 px-4 py-2",
sm: "h-9 rounded-md px-3",
lg: "h-11 rounded-md px-8",
icon: "h-10 w-10",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
)
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
asChild?: boolean
}
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : "button"
return (
<Comp
className={cn(buttonVariants({ variant, size, className }))}
ref={ref}
{...props}
/>
)
}
)
Button.displayName = "Button"
export { Button, buttonVariants }

View File

@ -0,0 +1,69 @@
import * as React from "react"
import { cn } from "@/lib/utils"
const Card = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("rounded-lg border bg-card text-card-foreground shadow-sm", className)}
{...props}
/>
))
Card.displayName = "Card"
const CardHeader = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("flex flex-col space-y-1.5 p-6", className)}
{...props}
/>
))
CardHeader.displayName = "CardHeader"
const CardTitle = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLHeadingElement>
>(({ className, ...props }, ref) => (
<h3
ref={ref}
className={cn("text-2xl font-semibold leading-none tracking-tight", className)}
{...props}
/>
))
CardTitle.displayName = "CardTitle"
const CardDescription = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement>
>(({ className, ...props }, ref) => (
<p
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
))
CardDescription.displayName = "CardDescription"
const CardContent = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
))
CardContent.displayName = "CardContent"
const CardFooter = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div ref={ref} className={cn("flex items-center p-6 pt-0", className)} {...props} />
))
CardFooter.displayName = "CardFooter"
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }

View File

@ -0,0 +1,28 @@
import * as React from "react"
import * as CheckboxPrimitive from "@radix-ui/react-checkbox"
import { Check } from "lucide-react"
import { cn } from "@/lib/utils"
const Checkbox = React.forwardRef<
React.ElementRef<typeof CheckboxPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>
>(({ className, ...props }, ref) => (
<CheckboxPrimitive.Root
ref={ref}
className={cn(
"peer h-4 w-4 shrink-0 rounded-sm border border-primary ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground",
className
)}
{...props}
>
<CheckboxPrimitive.Indicator
className={cn("flex items-center justify-center text-current")}
>
<Check className="h-4 w-4" />
</CheckboxPrimitive.Indicator>
</CheckboxPrimitive.Root>
))
Checkbox.displayName = CheckboxPrimitive.Root.displayName
export { Checkbox }

View File

@ -0,0 +1,114 @@
import * as React from "react"
import * as DialogPrimitive from "@radix-ui/react-dialog"
import { X } from "lucide-react"
import { cn } from "@/lib/utils"
const Dialog = DialogPrimitive.Root
const DialogTrigger = DialogPrimitive.Trigger
const DialogPortal = DialogPrimitive.Portal
const DialogClose = DialogPrimitive.Close
const DialogOverlay = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Overlay
ref={ref}
className={cn(
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
className
)}
{...props}
/>
))
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
const DialogContent = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
>(({ className, children, ...props }, ref) => (
<DialogPortal>
<DialogOverlay />
<DialogPrimitive.Content
ref={ref}
className={cn(
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
className
)}
{...props}
>
{children}
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
<X className="h-4 w-4" />
<span className="sr-only">Close</span>
</DialogPrimitive.Close>
</DialogPrimitive.Content>
</DialogPortal>
))
DialogContent.displayName = DialogPrimitive.Content.displayName
const DialogHeader = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn("flex flex-col space-y-1.5 text-center sm:text-left", className)}
{...props}
/>
)
DialogHeader.displayName = "DialogHeader"
const DialogFooter = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
className
)}
{...props}
/>
)
DialogFooter.displayName = "DialogFooter"
const DialogTitle = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Title
ref={ref}
className={cn("text-lg font-semibold leading-none tracking-tight", className)}
{...props}
/>
))
DialogTitle.displayName = DialogPrimitive.Title.displayName
const DialogDescription = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Description
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
))
DialogDescription.displayName = DialogPrimitive.Description.displayName
export {
Dialog,
DialogPortal,
DialogOverlay,
DialogClose,
DialogTrigger,
DialogContent,
DialogHeader,
DialogFooter,
DialogTitle,
DialogDescription,
}

View File

@ -0,0 +1,25 @@
import * as React from "react"
import { cn } from "@/lib/utils"
export interface InputProps
extends React.InputHTMLAttributes<HTMLInputElement> {}
const Input = React.forwardRef<HTMLInputElement, InputProps>(
({ className, type, ...props }, ref) => {
return (
<input
type={type}
className={cn(
"flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
className
)}
ref={ref}
{...props}
/>
)
}
)
Input.displayName = "Input"
export { Input }

View File

@ -0,0 +1,24 @@
import * as React from "react"
import * as LabelPrimitive from "@radix-ui/react-label"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const labelVariants = cva(
"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
)
const Label = React.forwardRef<
React.ElementRef<typeof LabelPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> &
VariantProps<typeof labelVariants>
>(({ className, ...props }, ref) => (
<LabelPrimitive.Root
ref={ref}
className={cn(labelVariants(), className)}
{...props}
/>
))
Label.displayName = LabelPrimitive.Root.displayName
export { Label }

View File

@ -0,0 +1,40 @@
import * as React from "react"
import * as RadioGroupPrimitive from "@radix-ui/react-radio-group"
import { Circle } from "lucide-react"
import { cn } from "@/lib/utils"
const RadioGroup = React.forwardRef<
React.ElementRef<typeof RadioGroupPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Root>
>(({ className, ...props }, ref) => {
return <RadioGroupPrimitive.Root
className={cn("grid gap-2", className)}
{...props}
ref={ref}
/>
})
RadioGroup.displayName = RadioGroupPrimitive.Root.displayName
const RadioGroupItem = React.forwardRef<
React.ElementRef<typeof RadioGroupPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Item>
>(({ className, ...props }, ref) => {
return (
<RadioGroupPrimitive.Item
ref={ref}
className={cn(
"aspect-square h-4 w-4 rounded-full border border-primary text-primary ring-offset-background focus:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
className
)}
{...props}
>
<RadioGroupPrimitive.Indicator className="flex items-center justify-center">
<Circle className="h-2.5 w-2.5 fill-current text-current" />
</RadioGroupPrimitive.Indicator>
</RadioGroupPrimitive.Item>
)
})
RadioGroupItem.displayName = RadioGroupPrimitive.Item.displayName
export { RadioGroup, RadioGroupItem }

View File

@ -0,0 +1,119 @@
import * as React from "react"
import * as SelectPrimitive from "@radix-ui/react-select"
import { Check, ChevronDown } from "lucide-react"
import { cn } from "@/lib/utils"
const Select = SelectPrimitive.Root
const SelectGroup = SelectPrimitive.Group
const SelectValue = SelectPrimitive.Value
const SelectTrigger = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger>
>(({ className, children, ...props }, ref) => (
<SelectPrimitive.Trigger
ref={ref}
className={cn(
"flex h-10 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
className
)}
{...props}
>
{children}
<SelectPrimitive.Icon asChild>
<ChevronDown className="h-4 w-4 opacity-50" />
</SelectPrimitive.Icon>
</SelectPrimitive.Trigger>
))
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName
const SelectContent = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
>(({ className, children, position = "popper", ...props }, ref) => (
<SelectPrimitive.Portal>
<SelectPrimitive.Content
ref={ref}
className={cn(
"relative z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
position === "popper" &&
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
className
)}
position={position}
{...props}
>
<SelectPrimitive.Viewport
className={cn(
"p-1",
position === "popper" &&
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]"
)}
>
{children}
</SelectPrimitive.Viewport>
</SelectPrimitive.Content>
</SelectPrimitive.Portal>
))
SelectContent.displayName = SelectPrimitive.Content.displayName
const SelectLabel = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Label>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>
>(({ className, ...props }, ref) => (
<SelectPrimitive.Label
ref={ref}
className={cn("py-1.5 pl-8 pr-2 text-sm font-semibold", className)}
{...props}
/>
))
SelectLabel.displayName = SelectPrimitive.Label.displayName
const SelectItem = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>
>(({ className, children, ...props }, ref) => (
<SelectPrimitive.Item
ref={ref}
className={cn(
"relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className
)}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<SelectPrimitive.ItemIndicator>
<Check className="h-4 w-4" />
</SelectPrimitive.ItemIndicator>
</span>
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
</SelectPrimitive.Item>
))
SelectItem.displayName = SelectPrimitive.Item.displayName
const SelectSeparator = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>
>(({ className, ...props }, ref) => (
<SelectPrimitive.Separator
ref={ref}
className={cn("-mx-1 my-1 h-px bg-muted", className)}
{...props}
/>
))
SelectSeparator.displayName = SelectPrimitive.Separator.displayName
export {
Select,
SelectGroup,
SelectValue,
SelectTrigger,
SelectContent,
SelectLabel,
SelectItem,
SelectSeparator,
}

View File

@ -0,0 +1,27 @@
import * as React from "react"
import * as SwitchPrimitives from "@radix-ui/react-switch"
import { cn } from "@/lib/utils"
const Switch = React.forwardRef<
React.ElementRef<typeof SwitchPrimitives.Root>,
React.ComponentPropsWithoutRef<typeof SwitchPrimitives.Root>
>(({ className, ...props }, ref) => (
<SwitchPrimitives.Root
className={cn(
"peer inline-flex h-[24px] w-[44px] shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input",
className
)}
{...props}
ref={ref}
>
<SwitchPrimitives.Thumb
className={cn(
"pointer-events-none block h-5 w-5 rounded-full bg-background shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-5 data-[state=unchecked]:translate-x-0"
)}
/>
</SwitchPrimitives.Root>
))
Switch.displayName = SwitchPrimitives.Root.displayName
export { Switch }

View File

@ -0,0 +1,53 @@
import * as React from "react"
import * as TabsPrimitive from "@radix-ui/react-tabs"
import { cn } from "@/lib/utils"
const Tabs = TabsPrimitive.Root
const TabsList = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.List>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.List>
>(({ className, ...props }, ref) => (
<TabsPrimitive.List
ref={ref}
className={cn(
"inline-flex h-10 items-center justify-center rounded-md bg-muted p-1 text-muted-foreground",
className
)}
{...props}
/>
))
TabsList.displayName = TabsPrimitive.List.displayName
const TabsTrigger = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger>
>(({ className, ...props }, ref) => (
<TabsPrimitive.Trigger
ref={ref}
className={cn(
"inline-flex items-center justify-center whitespace-nowrap rounded-sm px-3 py-1.5 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow-sm",
className
)}
{...props}
/>
))
TabsTrigger.displayName = TabsPrimitive.Trigger.displayName
const TabsContent = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content>
>(({ className, ...props }, ref) => (
<TabsPrimitive.Content
ref={ref}
className={cn(
"mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
className
)}
{...props}
/>
))
TabsContent.displayName = TabsPrimitive.Content.displayName
export { Tabs, TabsList, TabsTrigger, TabsContent }

View File

@ -0,0 +1,24 @@
import * as React from "react"
import { cn } from "@/lib/utils"
export interface TextareaProps
extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {}
const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
({ className, ...props }, ref) => {
return (
<textarea
className={cn(
"flex min-h-[80px] w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
className
)}
ref={ref}
{...props}
/>
)
}
)
Textarea.displayName = "Textarea"
export { Textarea }

120
frontend/src/index.css Normal file
View File

@ -0,0 +1,120 @@
@import "tailwindcss";
@import "tw-animate-css";
@custom-variant dark (&:is(.dark *));
@theme inline {
--radius-sm: calc(var(--radius) - 4px);
--radius-md: calc(var(--radius) - 2px);
--radius-lg: var(--radius);
--radius-xl: calc(var(--radius) + 4px);
--color-background: var(--background);
--color-foreground: var(--foreground);
--color-card: var(--card);
--color-card-foreground: var(--card-foreground);
--color-popover: var(--popover);
--color-popover-foreground: var(--popover-foreground);
--color-primary: var(--primary);
--color-primary-foreground: var(--primary-foreground);
--color-secondary: var(--secondary);
--color-secondary-foreground: var(--secondary-foreground);
--color-muted: var(--muted);
--color-muted-foreground: var(--muted-foreground);
--color-accent: var(--accent);
--color-accent-foreground: var(--accent-foreground);
--color-destructive: var(--destructive);
--color-border: var(--border);
--color-input: var(--input);
--color-ring: var(--ring);
--color-chart-1: var(--chart-1);
--color-chart-2: var(--chart-2);
--color-chart-3: var(--chart-3);
--color-chart-4: var(--chart-4);
--color-chart-5: var(--chart-5);
--color-sidebar: var(--sidebar);
--color-sidebar-foreground: var(--sidebar-foreground);
--color-sidebar-primary: var(--sidebar-primary);
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
--color-sidebar-accent: var(--sidebar-accent);
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
--color-sidebar-border: var(--sidebar-border);
--color-sidebar-ring: var(--sidebar-ring);
}
:root {
--radius: 0.625rem;
--background: oklch(1 0 0);
--foreground: oklch(0.145 0 0);
--card: oklch(1 0 0);
--card-foreground: oklch(0.145 0 0);
--popover: oklch(1 0 0);
--popover-foreground: oklch(0.145 0 0);
--primary: oklch(0.205 0 0);
--primary-foreground: oklch(0.985 0 0);
--secondary: oklch(0.97 0 0);
--secondary-foreground: oklch(0.205 0 0);
--muted: oklch(0.97 0 0);
--muted-foreground: oklch(0.556 0 0);
--accent: oklch(0.97 0 0);
--accent-foreground: oklch(0.205 0 0);
--destructive: oklch(0.577 0.245 27.325);
--border: oklch(0.922 0 0);
--input: oklch(0.922 0 0);
--ring: oklch(0.708 0 0);
--chart-1: oklch(0.646 0.222 41.116);
--chart-2: oklch(0.6 0.118 184.704);
--chart-3: oklch(0.398 0.07 227.392);
--chart-4: oklch(0.828 0.189 84.429);
--chart-5: oklch(0.769 0.188 70.08);
--sidebar: oklch(0.985 0 0);
--sidebar-foreground: oklch(0.145 0 0);
--sidebar-primary: oklch(0.205 0 0);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.97 0 0);
--sidebar-accent-foreground: oklch(0.205 0 0);
--sidebar-border: oklch(0.922 0 0);
--sidebar-ring: oklch(0.708 0 0);
}
.dark {
--background: oklch(0.145 0 0);
--foreground: oklch(0.985 0 0);
--card: oklch(0.205 0 0);
--card-foreground: oklch(0.985 0 0);
--popover: oklch(0.205 0 0);
--popover-foreground: oklch(0.985 0 0);
--primary: oklch(0.922 0 0);
--primary-foreground: oklch(0.205 0 0);
--secondary: oklch(0.269 0 0);
--secondary-foreground: oklch(0.985 0 0);
--muted: oklch(0.269 0 0);
--muted-foreground: oklch(0.708 0 0);
--accent: oklch(0.269 0 0);
--accent-foreground: oklch(0.985 0 0);
--destructive: oklch(0.704 0.191 22.216);
--border: oklch(1 0 0 / 10%);
--input: oklch(1 0 0 / 15%);
--ring: oklch(0.556 0 0);
--chart-1: oklch(0.488 0.243 264.376);
--chart-2: oklch(0.696 0.17 162.48);
--chart-3: oklch(0.769 0.188 70.08);
--chart-4: oklch(0.627 0.265 303.9);
--chart-5: oklch(0.645 0.246 16.439);
--sidebar: oklch(0.205 0 0);
--sidebar-foreground: oklch(0.985 0 0);
--sidebar-primary: oklch(0.488 0.243 264.376);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.269 0 0);
--sidebar-accent-foreground: oklch(0.985 0 0);
--sidebar-border: oklch(1 0 0 / 10%);
--sidebar-ring: oklch(0.556 0 0);
}
@layer base {
* {
@apply border-border outline-ring/50;
}
body {
@apply bg-background text-foreground;
}
}

10
frontend/src/main.tsx Normal file
View File

@ -0,0 +1,10 @@
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import './index.css'
import App from './App.tsx'
createRoot(document.getElementById('root')!).render(
<StrictMode>
<App />
</StrictMode>,
)

1
frontend/src/vite-env.d.ts vendored Normal file
View File

@ -0,0 +1 @@
/// <reference types="vite/client" />

View File

@ -0,0 +1,33 @@
{
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
"target": "ES2022",
"useDefineForClassFields": true,
"lib": ["ES2022", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"moduleDetection": "force",
"noEmit": true,
"jsx": "react-jsx",
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"erasableSyntaxOnly": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true,
"baseUrl": ".",
"paths": {
"@/*": [
"./src/*"
]
}
},
"include": ["src"]
}

17
frontend/tsconfig.json Normal file
View File

@ -0,0 +1,17 @@
{
"files": [],
"references": [
{
"path": "./tsconfig.app.json"
},
{
"path": "./tsconfig.node.json"
}
],
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
}
}
}

View File

@ -0,0 +1,25 @@
{
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
"target": "ES2023",
"lib": ["ES2023"],
"module": "ESNext",
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"moduleDetection": "force",
"noEmit": true,
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"erasableSyntaxOnly": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true
},
"include": ["vite.config.ts"]
}

14
frontend/vite.config.ts Normal file
View File

@ -0,0 +1,14 @@
import path from "path"
import tailwindcss from "@tailwindcss/vite"
import react from "@vitejs/plugin-react"
import { defineConfig } from "vite"
// https://vite.dev/config/
export default defineConfig({
plugins: [react(), tailwindcss()],
resolve: {
alias: {
"@": path.resolve(__dirname, "./src"),
},
},
})

0
page_objects/__init__.py Normal file
View File

179
page_objects/base_page.py Normal file
View File

@ -0,0 +1,179 @@
from selenium.webdriver.remote.webdriver import WebDriver
from selenium.webdriver.remote.webelement import WebElement
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
import time
class BasePage:
"""
The BasePage class serves as a foundation for all page objects.
It encapsulates common Selenium operations to promote code reuse and
improve test maintenance.
"""
def __init__(self, driver: WebDriver, base_url: str = "http://120.53.89.168:90"):
"""
Initializes the BasePage with a WebDriver instance and a base URL.
:param driver: The WebDriver instance to interact with the browser.
:param base_url: The base URL of the web application under test.
"""
self.driver = driver
self.base_url = base_url
self.wait = WebDriverWait(driver, 10) # Default explicit wait of 10 seconds
def open_url(self, path: str):
"""
Navigates to a specific path relative to the base URL.
:param path: The relative path to open (e.g., "/form-elements").
"""
url = self.base_url + path
self.driver.get(url)
def find_element(self, locator: tuple) -> WebElement:
"""
Finds and returns a web element using an explicit wait.
:param locator: A tuple containing the locator strategy and value
(e.g., (By.ID, "my-element")).
:return: The located WebElement.
"""
return self.wait.until(EC.presence_of_element_located(locator))
def find_visible_element(self, locator: tuple) -> WebElement:
"""
Finds and returns a web element that is visible on the page.
:param locator: A tuple containing the locator strategy and value.
:return: The located and visible WebElement.
"""
return self.wait.until(EC.visibility_of_element_located(locator))
def click(self, locator: tuple):
"""
Waits for an element to be clickable and then clicks on it.
:param locator: A tuple containing the locator strategy and value.
"""
element = self.wait.until(EC.element_to_be_clickable(locator))
element.click()
def send_keys(self, locator: tuple, text: str):
"""
Finds an element, clears its content, and sends keys to it.
:param locator: A tuple containing the locator strategy and value.
:param text: The text to send to the element.
"""
element = self.find_element(locator)
element.clear()
element.send_keys(text)
def get_text(self, locator: tuple) -> str:
"""
Finds an element and returns its text content.
:param locator: A tuple containing the locator strategy and value.
:return: The text content of the element.
"""
element = self.find_element(locator)
return element.text
def is_element_selected(self, locator: tuple) -> bool:
"""
Checks if a checkbox or radio button element is selected.
:param locator: A tuple containing the locator strategy and value.
:return: True if the element is selected, False otherwise.
"""
element = self.find_element(locator)
return element.is_selected()
def is_element_enabled(self, locator: tuple) -> bool:
"""
Checks if a form element is enabled.
:param locator: A tuple containing the locator strategy and value.
:return: True if the element is enabled, False otherwise.
"""
element = self.find_element(locator)
return element.is_enabled()
def switch_to_alert(self):
"""
Switches the driver's focus to a browser alert.
:return: The alert object.
"""
return self.wait.until(EC.alert_is_present())
def back(self):
"""
Navigates one step backward in the browser history.
"""
self.driver.back()
def forward(self):
"""
Navigates one step forward in the browser history.
"""
self.driver.forward()
def refresh(self):
"""
Refreshes the current page.
"""
self.driver.refresh()
def take_screenshot(self, name: str = "screenshot"):
"""
Captures a screenshot of the current browser window.
:param name: Base name for the screenshot file.
:return: The filename of the saved screenshot.
"""
timestamp = time.strftime("%Y%m%d_%H%M%S")
filename = f"{name}_{timestamp}.png"
self.driver.save_screenshot(filename)
return filename
def find_elements(self, locator: tuple):
"""
Finds and returns a list of web elements using an explicit wait.
:param locator: A tuple containing the locator strategy and value
(e.g., (By.CLASS_NAME, "items")).
:return: A list of located WebElements.
"""
return self.wait.until(EC.presence_of_all_elements_located(locator))
def scroll_into_view(self, locator: tuple):
"""
Scrolls the page until the specified element is in view.
:param locator: A tuple containing the locator strategy and value.
"""
element = self.find_element(locator)
self.driver.execute_script("arguments[0].scrollIntoView(true);", element)
def click_by_js(self, locator: tuple):
"""
Clicks on an element using JavaScript execution.
Useful when standard Selenium click does not work.
:param locator: A tuple containing the locator strategy and value.
"""
element = self.find_element(locator)
self.driver.execute_script("arguments[0].click();", element)
def wait_for_text(self, locator: tuple, text: str):
"""
Waits until the specified text is present within an element.
:param locator: A tuple containing the locator strategy and value.
:param text: The text to wait for in the element.
:return: True if the text is present, False otherwise.
"""
return self.wait.until(EC.text_to_be_present_in_element(locator, text))

View File

@ -0,0 +1,107 @@
import time
from selenium.webdriver.common.by import By
from .base_page import BasePage
class DynamicContentPage(BasePage):
"""
Page Object for the Dynamic Content test page.
"""
# --- Locators ---
_DELAYED_TEXT = (By.ID, "delayed-text")
_ENABLE_BUTTON = (By.XPATH, "//button[text()='Enable Button']")
_INITIALLY_DISABLED_BUTTON = (By.XPATH, "//button[text()='Initially Disabled']")
By.XPATH, '//*[@id="radix-«r0»-trigger-password"]'
_ACCOUNT_TAB = (By.XPATH, "//button[@role='tab' and contains(@id,'trigger-account')]")
_PASSWORD_TAB = (By.XPATH, "//button[@role='tab' and contains(@id,'trigger-password')]")
_ACTIVE_TAB_CONTENT = (By.XPATH, "//div[@role='tabpanel' and @data-state='active']")
_SHOW_ALERT_BUTTON = (By.XPATH, "//button[text()='Show Alert']")
_OPEN_MODAL_BUTTON = (By.XPATH, "//button[text()='Open Modal']")
_MODAL_TITLE = (By.ID, "radix-") # This ID is dynamic, a better locator is needed.
# A better locator for the modal title would be:
_MODAL_TITLE_BETTER = (By.XPATH, "//h2[text()='Modal Title']")
def __init__(self, driver):
"""Initializes the DynamicContentPage with the WebDriver."""
super().__init__(driver)
self.page_path = "/dynamic-content"
# --- Page Actions ---
def open(self):
"""Navigates to the dynamic content page."""
self.open_url(self.page_path)
def get_delayed_text(self) -> str:
"""
Waits for the delayed text to be visible and returns its content.
The wait is handled by find_visible_element.
"""
# We need to wait for the text "Loading..." to disappear first.
# This is a more complex wait condition. For simplicity, we'll just wait for visibility.
time.sleep(4)
element = self.find_visible_element(self._DELAYED_TEXT)
return element.text
def click_enable_button(self):
"""Clicks the button that enables the initially disabled button."""
self.click(self._ENABLE_BUTTON)
def is_initially_disabled_button_enabled(self) -> bool:
"""Checks if the initially disabled button is now enabled."""
return self.is_element_enabled(self._INITIALLY_DISABLED_BUTTON)
def switch_to_tab(self, tab_name: str):
"""
Switches to the specified tab.
:param tab_name: 'account' or 'password'.
"""
if tab_name.lower() == 'account':
self.click(self._ACCOUNT_TAB)
elif tab_name.lower() == 'password':
self.click(self._PASSWORD_TAB)
else:
raise ValueError("Invalid tab name. Must be 'account' or 'password'.")
time.sleep(1) # Add a short delay to allow the tab content to update
def get_active_tab_content(self) -> str:
"""
Gets the text of the currently visible tab panel.
This requires checking which panel is visible.
"""
element = self.find_visible_element(self._ACTIVE_TAB_CONTENT)
return element.text
# def get_active_tab_content(self) -> str:
# """
# Gets the text of the currently active tab panel based on hidden attribute.
# """
# panels = [self._ACCOUNT_TAB_CONTENT, self._PASSWORD_TAB_CONTENT]
# for panel in panels:
# element = self.find_element(panel)
# if element.get_attribute("hidden") in [None, "false"]:
# return element.text
# return ""
def trigger_alert(self):
"""Clicks the button to show a browser alert."""
self.click(self._SHOW_ALERT_BUTTON)
def get_alert_text_and_accept(self) -> str:
"""Switches to an alert, gets its text, and accepts it."""
alert = self.switch_to_alert()
text = alert.text
alert.accept()
return text
def open_modal(self):
"""Clicks the button to open the modal dialog."""
self.click(self._OPEN_MODAL_BUTTON)
def get_modal_title(self) -> str:
"""Waits for the modal to be visible and returns its title."""
# Using the more robust locator
title_element = self.find_visible_element(self._MODAL_TITLE_BETTER)
return title_element.text

View File

@ -0,0 +1,127 @@
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import Select
from .base_page import BasePage
class FormElementsPage(BasePage):
"""
Page Object for the Form Elements test page.
Encapsulates all locators and actions related to this page.
"""
# --- Locators ---
_TEXT_INPUT = (By.ID, "text-input")
_CHECKBOX = (By.ID, "checkbox-input")
_RADIO_OPTION_ONE = (By.ID, "r1")
_RADIO_OPTION_TWO = (By.ID, "r2")
_RADIO_OPTION_THREE = (By.ID, "r3")
_SELECT_DROPDOWN = (By.ID, "select-input")
_TEXTAREA = (By.ID, "textarea-input")
_SWITCH = (By.ID, "switch-input")
_SUBMIT_BUTTON = (By.XPATH, "//button[@type='submit']")
_CANCEL_BUTTON = (By.XPATH, "//button[text()='Cancel']")
_DISABLED_BUTTON = (By.XPATH, "//button[text()='Disabled']")
def __init__(self, driver):
"""Initializes the FormElementsPage with the WebDriver."""
super().__init__(driver)
self.page_path = "/form-elements"
# --- Page Actions ---
def open(self):
"""Navigates to the form elements page."""
self.open_url(self.page_path)
def enter_text_in_input(self, text: str):
"""Enters text into the main text input field."""
self.send_keys(self._TEXT_INPUT, text)
def get_text_from_input(self) -> str:
"""Gets the current value from the text input field."""
return self.find_element(self._TEXT_INPUT).get_attribute("value")
def select_checkbox(self):
"""Clicks the checkbox to select it."""
self.click(self._CHECKBOX)
def is_checkbox_selected(self) -> bool:
"""Checks if the checkbox is selected."""
checkbox = self.find_element(self._CHECKBOX)
return checkbox.get_attribute("data-state") == "checked"
def choose_radio_option(self, option: int):
"""
Selects a radio button option.
:param option: 1 for Option One, 2 for Option Two, 3 for Option Three.
"""
if option == 1:
self.click(self._RADIO_OPTION_ONE)
elif option == 2:
self.click(self._RADIO_OPTION_TWO)
elif option == 3:
self.click(self._RADIO_OPTION_THREE)
else:
raise ValueError("Invalid option number. Must be 1, 2, or 3.")
def is_radio_option_selected(self, option: int) -> bool:
"""Checks if a specific radio button option is selected."""
locator = None
if option == 1:
locator = self._RADIO_OPTION_ONE
elif option == 2:
locator = self._RADIO_OPTION_TWO
elif option == 3:
locator = self._RADIO_OPTION_THREE
else:
return False
radio_button = self.find_element(locator)
return radio_button.get_attribute("data-state") == "checked"
def select_fruit_by_visible_text(self, text: str):
"""
Selects an option from the custom shadcn dropdown by its visible text.
:param text: The visible text of the option to select (e.g., "Apple").
"""
# 1. Click the dropdown trigger to open the options
self.click(self._SELECT_DROPDOWN)
# 2. Define the locator for the desired option based on its text
# The options are typically in a popover, so we wait for them to be visible.
option_locator = (By.XPATH, f"//div[@role='option' and .//span[text()='{text}']]")
# 3. Click the option
self.click(option_locator)
def get_selected_fruit(self) -> str:
"""Gets the currently selected value from the dropdown trigger."""
return self.get_text(self._SELECT_DROPDOWN)
def enter_message_in_textarea(self, message: str):
"""Enters text into the textarea field."""
self.send_keys(self._TEXTAREA, message)
def get_message_from_textarea(self) -> str:
"""Gets the current value from the textarea field."""
return self.find_element(self._TEXTAREA).get_attribute("value")
def toggle_switch(self):
"""Clicks the switch to toggle its state."""
self.click(self._SWITCH)
def is_switch_on(self) -> bool:
"""
Checks if the switch is in the 'on' state.
This depends on how state is represented (e.g., aria-checked, class).
For shadcn, it's often a data attribute `data-state`.
"""
switch_element = self.find_element(self._SWITCH)
return switch_element.get_attribute("data-state") == "checked"
def click_submit_button(self):
"""Clicks the submit button."""
self.click(self._SUBMIT_BUTTON)
def is_disabled_button_enabled(self) -> bool:
"""Checks if the 'Disabled' button is enabled."""
return self.is_element_enabled(self._DISABLED_BUTTON)

View File

View File

5
pytest.ini Normal file
View File

@ -0,0 +1,5 @@
[pytest]
addopts = -vs --alluredir=reports/allure_results --clean-alluredir
markers =
smoke: mark a test as a smoke test.
regression: mark a test as a regression test.

9
requirements.txt Normal file
View File

@ -0,0 +1,9 @@
selenium
pytest
webdriver-manager
allure-pytest
pytest-xdist
pytest-rerunfailures
pytest-ordering
pytest-html
pytest-metadata

0
tests/__init__.py Normal file
View File

49
tests/conftest.py Normal file
View File

@ -0,0 +1,49 @@
import pytest
import allure
from selenium import webdriver
from selenium.webdriver.chrome.service import Service as ChromeService
from webdriver_manager.chrome import ChromeDriverManager
@pytest.fixture(scope="function")
def driver():
"""
Initializes and returns a Chrome WebDriver instance for each test function.
Automatically quits the driver after the test function completes.
"""
# Initialize the Chrome WebDriver
driver = webdriver.Chrome()
# Maximize the browser window
driver.maximize_window()
# Yield the driver instance to the test function
yield driver
# Quit the driver after the test is done
driver.quit()
@pytest.hookimpl(tryfirst=True, hookwrapper=True)
def pytest_runtest_makereport(item, call):
"""
Hook to capture test results and attach screenshots on failure.
"""
# Execute all other hooks to obtain the report object
outcome = yield
report = outcome.get_result()
# Check if the test failed
if report.when == "call" and report.failed:
try:
# Get the driver instance from the test item
driver = item.funcargs["driver"]
# Define the path for the screenshot
screenshot_path = f"reports/screenshots/{item.name}.png"
# Save the screenshot
driver.save_screenshot(screenshot_path)
# Attach the screenshot to the Allure report
allure.attach.file(screenshot_path, name="Screenshot", attachment_type=allure.attachment_type.PNG)
except Exception as e:
print(f"Failed to capture screenshot: {e}")

View File

@ -0,0 +1,89 @@
import pytest
from page_objects.dynamic_content_page import DynamicContentPage
import time
@pytest.mark.regression
class TestDynamicContent:
"""
Test suite for the Dynamic Content page, focusing on asynchronous
and dynamic elements.
"""
def test_delayed_text_appears(self, driver):
"""
Tests that the delayed text appears after a few seconds.
"""
dynamic_page = DynamicContentPage(driver)
dynamic_page.open()
# The waiting logic is inside the get_delayed_text method
text = dynamic_page.get_delayed_text()
print(f"text :{text}")
assert "loaded after a 3-second delay." in text, \
"The delayed text did not appear or has incorrect content."
@pytest.mark.smoke
def test_button_enables_after_click(self, driver):
"""
Tests that a disabled button becomes enabled after an action.
"""
dynamic_page = DynamicContentPage(driver)
dynamic_page.open()
# Verify the button is initially disabled
assert not dynamic_page.is_initially_disabled_button_enabled(), \
"Button should be disabled initially."
# Click the button to enable the other one
dynamic_page.click_enable_button()
# Verify the button is now enabled
# Adding a small sleep to allow for DOM update, though explicit waits are better.
# The is_element_enabled method in BasePage uses an explicit wait, so this should be fine.
assert dynamic_page.is_initially_disabled_button_enabled(), \
"Button should be enabled after clicking the 'Enable' button."
def test_tabs_content_switching(self, driver):
"""
Tests that content switches correctly when different tabs are clicked.
"""
dynamic_page = DynamicContentPage(driver)
dynamic_page.open()
# Switch to Password tab and verify content
dynamic_page.switch_to_tab('password')
content = dynamic_page.get_active_tab_content()
assert "Password tab" in content, "Password tab content is not visible after switching."
# Switch back to Account tab and verify content
dynamic_page.switch_to_tab('account')
content = dynamic_page.get_active_tab_content()
assert "Account tab" in content, "Account tab content is not visible after switching back."
@pytest.mark.smoke
def test_alert_handling(self, driver):
"""
Tests the triggering and handling of a browser alert.
"""
dynamic_page = DynamicContentPage(driver)
dynamic_page.open()
dynamic_page.trigger_alert()
alert_text = dynamic_page.get_alert_text_and_accept()
assert alert_text == "This is a browser alert!", "The alert text is incorrect."
def test_modal_dialog(self, driver):
"""
Tests the opening of a modal dialog and verifies its content.
"""
dynamic_page = DynamicContentPage(driver)
dynamic_page.open()
dynamic_page.open_modal()
modal_title = dynamic_page.get_modal_title()
assert modal_title == "Modal Title", "The modal dialog title is incorrect."

102
tests/test_form_elements.py Normal file
View File

@ -0,0 +1,102 @@
import pytest
from page_objects.form_elements_page import FormElementsPage
@pytest.mark.regression
class TestFormElements:
"""
Test suite for the Form Elements page.
"""
def test_text_input(self, driver):
"""
Tests text entry and retrieval from the text input field.
"""
form_page = FormElementsPage(driver)
form_page.open()
test_text = "Hello, Selenium!"
form_page.enter_text_in_input(test_text)
retrieved_text = form_page.get_text_from_input()
assert retrieved_text == test_text, f"Expected '{test_text}', but got '{retrieved_text}'"
@pytest.mark.smoke
def test_checkbox_selection(self, driver):
"""
Tests the selection and deselection of a checkbox.
"""
form_page = FormElementsPage(driver)
form_page.open()
assert not form_page.is_checkbox_selected(), "Checkbox should be deselected initially"
form_page.select_checkbox()
assert form_page.is_checkbox_selected(), "Checkbox should be selected after clicking"
form_page.select_checkbox()
assert not form_page.is_checkbox_selected(), "Checkbox should be deselected after clicking again"
def test_radio_button_selection(self, driver):
"""
Tests that only one radio button can be selected at a time.
"""
form_page = FormElementsPage(driver)
form_page.open()
form_page.choose_radio_option(2)
assert form_page.is_radio_option_selected(2), "Radio option 2 should be selected"
assert not form_page.is_radio_option_selected(1), "Radio option 1 should not be selected"
form_page.choose_radio_option(3)
assert form_page.is_radio_option_selected(3), "Radio option 3 should be selected"
assert not form_page.is_radio_option_selected(2), "Radio option 2 should not be selected"
def test_dropdown_selection(self, driver):
"""
Tests selecting an option from the custom dropdown.
"""
form_page = FormElementsPage(driver)
form_page.open()
fruit_to_select = "Banana"
form_page.select_fruit_by_visible_text(fruit_to_select)
selected_fruit = form_page.get_selected_fruit()
assert selected_fruit == fruit_to_select, \
f"Expected '{fruit_to_select}' to be selected, but got '{selected_fruit}'"
def test_disabled_button_state(self, driver):
"""
Verifies that the 'Disabled' button is indeed disabled.
"""
form_page = FormElementsPage(driver)
form_page.open()
assert not form_page.is_disabled_button_enabled(), "The disabled button should not be enabled"
@pytest.mark.smoke
def test_form_submission(self, driver):
"""
A simple test to fill a field and click the submit button.
"""
form_page = FormElementsPage(driver)
form_page.open()
form_page.enter_text_in_input("Test submission")
form_page.click_submit_button()
alert = form_page.switch_to_alert()
alert_text = alert.text
alert.accept()
assert alert_text == "Form Submitted!", "Alert text after submission is incorrect"
def test_filure_case(self,driver):
"""
"""
form_page = FormElementsPage(driver)
form_page.open()
print("case1")
assert "" == "a", "error"

View File

View File

View File