finsh commit
This commit is contained in:
151
.gitignore
vendored
Normal file
151
.gitignore
vendored
Normal 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
82
README.md
Normal 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
0
common/__init__.py
Normal file
0
common/exceptions.py
Normal file
0
common/exceptions.py
Normal file
0
common/logger.py
Normal file
0
common/logger.py
Normal file
0
common/yaml_handler.py
Normal file
0
common/yaml_handler.py
Normal file
0
config/__init__.py
Normal file
0
config/__init__.py
Normal file
0
config/dev_env.yaml
Normal file
0
config/dev_env.yaml
Normal file
0
config/prod_env.yaml
Normal file
0
config/prod_env.yaml
Normal file
0
core/__init__.py
Normal file
0
core/__init__.py
Normal file
0
core/webdriver_factory.py
Normal file
0
core/webdriver_factory.py
Normal file
0
data/__init__.py
Normal file
0
data/__init__.py
Normal file
0
data/login_data.csv
Normal file
0
data/login_data.csv
Normal file
|
|
25
frontend/.gitignore
vendored
Normal file
25
frontend/.gitignore
vendored
Normal 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
70
frontend/README.md
Normal 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
21
frontend/components.json
Normal 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
23
frontend/eslint.config.js
Normal 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
13
frontend/index.html
Normal 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
48
frontend/package.json
Normal 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
generated
Normal file
3458
frontend/pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
1
frontend/public/vite.svg
Normal file
1
frontend/public/vite.svg
Normal 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
42
frontend/src/App.css
Normal 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
79
frontend/src/App.tsx
Normal 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;
|
||||||
1
frontend/src/assets/react.svg
Normal file
1
frontend/src/assets/react.svg
Normal 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 |
120
frontend/src/components/pages/dynamic-content.tsx
Normal file
120
frontend/src/components/pages/dynamic-content.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
97
frontend/src/components/pages/form-elements.tsx
Normal file
97
frontend/src/components/pages/form-elements.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
56
frontend/src/components/ui/button.tsx
Normal file
56
frontend/src/components/ui/button.tsx
Normal 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 }
|
||||||
69
frontend/src/components/ui/card.tsx
Normal file
69
frontend/src/components/ui/card.tsx
Normal 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 }
|
||||||
28
frontend/src/components/ui/checkbox.tsx
Normal file
28
frontend/src/components/ui/checkbox.tsx
Normal 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 }
|
||||||
114
frontend/src/components/ui/dialog.tsx
Normal file
114
frontend/src/components/ui/dialog.tsx
Normal 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,
|
||||||
|
}
|
||||||
25
frontend/src/components/ui/input.tsx
Normal file
25
frontend/src/components/ui/input.tsx
Normal 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 }
|
||||||
24
frontend/src/components/ui/label.tsx
Normal file
24
frontend/src/components/ui/label.tsx
Normal 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 }
|
||||||
40
frontend/src/components/ui/radio-group.tsx
Normal file
40
frontend/src/components/ui/radio-group.tsx
Normal 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 }
|
||||||
119
frontend/src/components/ui/select.tsx
Normal file
119
frontend/src/components/ui/select.tsx
Normal 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,
|
||||||
|
}
|
||||||
27
frontend/src/components/ui/switch.tsx
Normal file
27
frontend/src/components/ui/switch.tsx
Normal 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 }
|
||||||
53
frontend/src/components/ui/tabs.tsx
Normal file
53
frontend/src/components/ui/tabs.tsx
Normal 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 }
|
||||||
24
frontend/src/components/ui/textarea.tsx
Normal file
24
frontend/src/components/ui/textarea.tsx
Normal 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
120
frontend/src/index.css
Normal 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
10
frontend/src/main.tsx
Normal 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
1
frontend/src/vite-env.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
/// <reference types="vite/client" />
|
||||||
33
frontend/tsconfig.app.json
Normal file
33
frontend/tsconfig.app.json
Normal 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
17
frontend/tsconfig.json
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"files": [],
|
||||||
|
"references": [
|
||||||
|
{
|
||||||
|
"path": "./tsconfig.app.json"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "./tsconfig.node.json"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"compilerOptions": {
|
||||||
|
"baseUrl": ".",
|
||||||
|
"paths": {
|
||||||
|
"@/*": ["./src/*"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
25
frontend/tsconfig.node.json
Normal file
25
frontend/tsconfig.node.json
Normal 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
14
frontend/vite.config.ts
Normal 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
0
page_objects/__init__.py
Normal file
179
page_objects/base_page.py
Normal file
179
page_objects/base_page.py
Normal 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))
|
||||||
107
page_objects/dynamic_content_page.py
Normal file
107
page_objects/dynamic_content_page.py
Normal 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
|
||||||
127
page_objects/form_elements_page.py
Normal file
127
page_objects/form_elements_page.py
Normal 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)
|
||||||
0
page_objects/home_page.py
Normal file
0
page_objects/home_page.py
Normal file
0
page_objects/login_page.py
Normal file
0
page_objects/login_page.py
Normal file
5
pytest.ini
Normal file
5
pytest.ini
Normal 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
9
requirements.txt
Normal 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
0
tests/__init__.py
Normal file
49
tests/conftest.py
Normal file
49
tests/conftest.py
Normal 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}")
|
||||||
89
tests/test_dynamic_content.py
Normal file
89
tests/test_dynamic_content.py
Normal 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
102
tests/test_form_elements.py
Normal 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"
|
||||||
0
tests/test_login/__init__.py
Normal file
0
tests/test_login/__init__.py
Normal file
0
tests/test_login/test_login_flow.py
Normal file
0
tests/test_login/test_login_flow.py
Normal file
0
tests/test_shopping_cart/__init__.py
Normal file
0
tests/test_shopping_cart/__init__.py
Normal file
0
tests/test_shopping_cart/test_add_to_cart.py
Normal file
0
tests/test_shopping_cart/test_add_to_cart.py
Normal file
Reference in New Issue
Block a user