finsh commit
This commit is contained in:
commit
838806b9e3
|
|
@ -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?
|
||||
|
|
@ -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,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
|
||||
|
|
@ -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 许可证。
|
||||
|
|
@ -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"
|
||||
}
|
||||
|
|
@ -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,
|
||||
},
|
||||
},
|
||||
])
|
||||
|
|
@ -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>
|
||||
|
|
@ -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"
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -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 |
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
@ -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 |
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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 }
|
||||
|
|
@ -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 }
|
||||
|
|
@ -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 }
|
||||
|
|
@ -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,
|
||||
}
|
||||
|
|
@ -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 }
|
||||
|
|
@ -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 }
|
||||
|
|
@ -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 }
|
||||
|
|
@ -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,
|
||||
}
|
||||
|
|
@ -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 }
|
||||
|
|
@ -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 }
|
||||
|
|
@ -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 }
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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>,
|
||||
)
|
||||
|
|
@ -0,0 +1 @@
|
|||
/// <reference types="vite/client" />
|
||||
|
|
@ -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"]
|
||||
}
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
{
|
||||
"files": [],
|
||||
"references": [
|
||||
{
|
||||
"path": "./tsconfig.app.json"
|
||||
},
|
||||
{
|
||||
"path": "./tsconfig.node.json"
|
||||
}
|
||||
],
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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"]
|
||||
}
|
||||
|
|
@ -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,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))
|
||||
|
|
@ -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
|
||||
|
|
@ -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,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.
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
selenium
|
||||
pytest
|
||||
webdriver-manager
|
||||
allure-pytest
|
||||
pytest-xdist
|
||||
pytest-rerunfailures
|
||||
pytest-ordering
|
||||
pytest-html
|
||||
pytest-metadata
|
||||
|
|
@ -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}")
|
||||
|
|
@ -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."
|
||||
|
|
@ -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"
|
||||
Loading…
Reference in New Issue